前端最新版本同步

This commit is contained in:
Zhang.WH
2025-09-25 10:36:07 +08:00
parent a3a06d6f3c
commit 1276dc4adb
117 changed files with 11964 additions and 2466 deletions

View File

@@ -0,0 +1,59 @@
import request from '@/utils/request'
// 查询诊疗项目列表
export function getTreeList(queryParams) {
return request({
url: '/document/definition/treeList',
method: 'get',
params: queryParams
})
}
// 初始化
export function init() {
return request({
url: '/document/definition/init',
method: 'get'
})
}
// 新增
export function add(data) {
return request({
url: '/document/definition/add',
method: 'post',
data
})
}
// 修改
export function update(data) {
return request({
url: '/document/definition/update',
method: 'post',
data
})
}
// 查询就诊科室
export function getLocationTree(query) {
return request({
url: '/charge-manage/register/org-list',
method: 'get',
params: query
})
}
// 查询就诊科室
export function getDefinitionById(id) {
return request({
url: `/document/definition/${id}`,
method: 'get'
})
}
export function deleteDefinition(id) {
return request({
url: `/document/definition/delete/${id}`,
method: 'delete'
})
}

View File

@@ -0,0 +1,250 @@
<template>
<!-- 病历文件基本信息弹窗 -->
<el-dialog
:title="title"
v-model="dialogVisible"
width="900px"
destroy-on-close
@open="handleOpen"
>
<!-- 使用el-form包裹表单 -->
<el-form :model="formData" ref="formRef" :rules="rules" label-width="120px">
<el-form-item label="一级菜单" prop="primaryMenuEnum">
<el-select
v-model="formData.primaryMenuEnum"
placeholder="请选择一级菜单"
value-key="value"
>
<el-option
v-for="item in props.docTypes || []"
:key="item.value"
:label="item.info"
:value="Number(item.value)"
/>
</el-select>
</el-form-item>
<el-form-item label="二级菜单" prop="subMenu">
<el-input v-model="formData.subMenu" placeholder="二级菜单"></el-input>
</el-form-item>
<el-form-item label="版本" prop="version">
<el-input v-model="formData.version" placeholder="请输入版本"></el-input>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称"></el-input>
</el-form-item>
<el-form-item label="文件" prop="vueRouter">
<el-select v-model="formData.vueRouter" placeholder="请选择文件路径">
<el-option
v-for="item in components"
:key="item.name"
:label="item.name"
:value="item.name"
/>
</el-select>
<!-- <el-input v-model="formData.vueRouter" placeholder="请输入文件路径"></el-input>s -->
</el-form-item>
<el-form-item label="显示顺序" prop="displayOrder">
<el-input-number v-model="formData.displayOrder" :min="1" label="描述文字"></el-input-number>
</el-form-item>
<el-form-item label="使用范围" prop="useRangeEnum">
<div class="radio-group">
<el-radio
v-for="item in props.useRanges"
:key="item.value"
v-model="formData.useRangeEnum"
:label="item.value"
>{{item.info}}</el-radio>
{{ formData.organizationIds }}
</div>
</el-form-item>
<!-- 科室选择框仅当使用范围为科室使用时显示 -->
<el-form-item v-if="formData.useRangeEnum === 2" label="科室选择" prop="organizationIds">
<el-transfer
v-model="formData.organizationIds"
filterable
:titles="['未分配科室', '已分配科室']"
:button-texts="['移除', '添加']"
:format="{
noChecked: '${total}',
hasChecked: '${checked}/${total}',
}"
:props="{key: 'id', label: 'name',}"
:data="transferData"
>
</el-transfer>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer"></div>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup >
import { ref, onMounted } from 'vue'
import useUserStore from '@/store/modules/user';
import { add, update,getLocationTree } from '../api';
import { ElMessage } from 'element-plus';
import { components } from '@/template';
const emits = defineEmits(['submitOk'])
const props = defineProps({
title: {
type: String,
default: '编辑病历文件信息'
},
formData: {
type: Object,
default: () => ({})
},
currentNodeData: {
type: Object,
default: () => ({})
},
docTypes: {
type: Array,
default: () => []
},
useRanges: {
type: Array,
default: () => []
},
})
const userStore = useUserStore();
const formRef = ref(null)
const dialogVisible= defineModel( 'dialogVisible', {
type: Boolean,
default: false
})
// 表单数据
const formData = ref({
primaryMenuEnum: undefined,
subMenu: '',
displayOrder: 1,
version: '',
name: '',
vueRouter: '',
useRangeEnum: 0, // 默认"暂不使用"0暂不使用1全院使用2科室使用
organizationIds: [], // 选中的科室ID列表整数类型
environment: '0'
});
// 表单验证规则(响应式,支持动态验证)
const rules = reactive({
primaryMenuEnum: [{ required: true, message: '请选择一级菜单', trigger: 'change' }],
// subMenu: [{ required: true, message: '请输入二级菜单', trigger: 'blur' }],
version: [{ required: true, message: '请输入版本', trigger: 'blur' }],
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
vueRouter: [{ required: true, message: '请输入文件路径', trigger: 'blur' }],
displayOrder: [{ required: true, message: '请设置显示顺序', trigger: 'change' }],
useRangeEnum: [{ required: true, message: '请选择使用范围', trigger: 'change' }],
organizationIds: [
{
required: () => formData.value.useRangeEnum === 2, // 仅当"科室使用"时必填
message: '请选择科室',
trigger: ['change', 'blur']
}
]
});
const transferData = ref([]);
/** 查询科室树数据TreeSelect和Transfer共用 */
const getLocationInfo = () => {
getLocationTree().then((response) => {
transferData.value = response?.data || [];
}).catch((error) => {
ElMessage.error('获取科室树失败');
});
}
/** 递归将所有ID转换为整数类型 */
const convertIdsToNumbers = (item) => {
return {
...item,
id: Number(item.id), // 确保ID为整数
...(item.children && item.children.length > 0
? { children: item.children.map((child ) => convertIdsToNumbers(child)) }
: {})
};
};
const handleOpen = () => {
if (props.formData) {
console.log('props.formData', props.formData);
formData.value= props.formData
} else {
resetForm();
formRef.value.resetFields()
}
}
// 提交表单
const submitForm = () => {
formRef.value.validate((valid) => {
if (valid) {
// 表单验证通过,执行保存操作
saveForm();
} else {
// 表单验证失败
ElMessage.error('请填写必填项');
return false;
}
});
};
// 保存表单
const saveForm =async () => {
const userStore = useUserStore()
console.log('提交表单数据:', formData.value);
let data = { ...formData.value, tenantId: userStore.tenantId }
if (formData.value.useRangeEnum !== 2) {
formData.value.organizationIds = [];
}
try {
// 如果有当前节点数据,表示是编辑操作
if (props.currentNodeData) {
data.id = props.currentNodeData.id; // 添加ID
data.busNo = props.currentNodeData.busNo;
data.hospitalId = props.currentNodeData.hospitalId;
console.log('data',data)
const res = await update(data);
if (res.code == 200) {
ElMessage.success('更新成功');
emits('submitOk')
}else {
ElMessage.error('保存失败');
}
} else {
// 新建操作
const res = await add(data);
if (res.code == 200) {
ElMessage.success('保存成功');
emits('submitOk')
}else {
ElMessage.error('保存失败',error);
}
}
}
catch (error) {
console.log(error)
// ElMessage.error('保存失败',error);
}
}
// 重置表单
const resetForm = () => {
formRef.value?.resetFields();
formData.value.useRangeEnum = 0;
formData.value.environment = '0';
formData.value.organizationIds = [];
};
onMounted(()=>{
getLocationInfo()
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,2 +1,436 @@
<template>
</template>
<div class="case-templates-container">
<!-- 顶部工具栏 -->
<div class="toolbar">
<el-button type="primary" @click="newTemplate">新建</el-button>
<el-button type="primary" @click="editTemplate">编辑</el-button>
<el-button @click="refresh">刷新</el-button>
<el-button type="danger" @click="deleteTemplate">删除</el-button>
<el-button @click="printTemplate">打印</el-button>
</div>
<div class="content-area">
<!-- 左侧病历类型树 -->
<div class="left-panel">
<div style="margin-bottom: 10px">
<el-tree-select
v-model="orgId"
:data="orgOptions"
:props="{
value: 'id',
label: 'name',
children: 'children',
}"
value-key="id"
placeholder="请选择就诊科室"
check-strictly
:expand-on-click-node="false"
:filter-node-method="filterNode"
ref="locationTreeRef"
node-key="id"
highlight-current
default-expand-all
@node-click="initTemplateTree"
@clear="handleOrgClear"
clearable
/>
</div>
<div class="search-box">
<el-input
placeholder="病历名称搜索..."
v-model="searchKeyword"
></el-input>
<el-button class="search-btn" @click="handleSearch">查询</el-button>
</div>
<el-tree
ref="templateTree"
:data="templateData"
:props="defaultProps"
node-key="id"
@node-click="handleNodeClick"
class="template-tree"
></el-tree>
<el-button @click="toggleExpand">{{ isExpanded ? '全部收起' : '全部展开' }}</el-button>
</div>
<div class="middle-panel">
<el-tabs v-model="activeName" type="card" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="打印预览" name="first">
<!-- {{components}} -->
<component :is="currentComponent" />
</el-tab-pane>
<!-- <el-tab-pane label="编辑内容" name="second">
<component :is="currentComponent" />
</el-tab-pane> -->
</el-tabs>
</div>
</div>
<EditTemplate
v-model:dialogVisible="dialogVisible"
:title="currentNodeData ? '编辑病历文件信息' : '病历文件基本信息'"
:formData="formData"
:currentNodeData="currentNodeData"
@submitOk="handleSubmitOk"
:docTypes="templateDataInit.docTypes"
:useRanges="templateDataInit.useRanges"
/>
</div>
</template>
<script setup>
// 从Vue导入所需的API
import { ref, reactive, onMounted, defineAsyncComponent, nextTick, watch } from 'vue';
import { ElMessageBox, ElMessage, ElLoading, ElTree } from 'element-plus';
import { getTreeList, init, getDefinitionById, deleteDefinition,getLocationTree } from './api';
import EditTemplate from './components/editTemplate.vue';
// 添加当前模板路径和组件的响应式变量
const currentComponent = ref('');
const currentNodeData = ref(null); // 存储当前选中的节点数据
const isExpanded = ref(true); // 控制树形结构的展开状态
// 弹窗可见性
const dialogVisible = ref(false);
const orgId = ref('');
const orgOptions = ref([]); // 科室选项整数ID
const locationTreeRef = ref(null); // TreeSelect引用
const templateTree = ref(null);
const searchKeyword = ref(''); // 搜索关键字
const activeName = ref('first');
// Transfer组件选项类型定义ID为整数类型
// 表单数据
const formData = reactive({
primaryMenuEnum: undefined,
subMenu: '',
displayOrder: 1,
version: '',
name: '',
vueRouter: '',
useRangeEnum: 0, // 默认"暂不使用"0暂不使用1全院使用2科室使用
organizationIds: [], // 选中的科室ID列表整数类型
environment: '0',
});
const initFormData = () => {
formData.primaryMenuEnum = undefined;
formData.subMenu = '';
formData.displayOrder = 1;
formData.version = '';
formData.name = '';
formData.vueRouter = '';
formData.useRangeEnum = 0;
formData.organizationIds = [];
formData.environment = '0';
};
const getLocationInfo = () => {
getLocationTree().then((response) => {
orgOptions.value = response?.data || [];
}).catch((error) => {
ElMessage.error('获取科室树失败');
});
}
const handleSubmitOk = () => {
dialogVisible.value = false;
initFormData();
refresh();
};
// 组件初始化时加载基础数据
onMounted(() => {
getInit(); // 加载模板初始化数据(一级菜单、使用范围等)
getLocationInfo(); // 加载科室树
handleOrgClear(); // 初始化模板树(加载所有科室模板)
});
/** 将树形结构转换为Transfer组件所需的格式不扁平化保留层级关系 */
const convertTreeToTransferFormat = (tree) => {
return tree.map((item) => {
const option = {
key: item.id, // 整数ID
label: item.name,
disabled: false,
};
// 如果有子节点递归处理Transfer组件会自动处理层级显示
if (item.children && item.children.length > 0) {
option.children = convertTreeToTransferFormat(item.children);
}
return option;
});
};
// 病历模板树数据
const templateData = ref([]);
const templateDataInit = ref({}); // 初始化数据(菜单、使用范围)
// 树配置(模板树)
const defaultProps = {
children: 'children',
label: 'name',
value: 'id',
};
/** 过滤节点科室TreeSelect搜索 */
const filterNode = (value, data) => {
if (!value) return true;
return data?.name.toLowerCase().includes(value.toLowerCase()); // 不区分大小写搜索
};
/** 统一的API错误处理函数 */
const handleApiError = (userMessage, logMessage) => {
return (error) => {
// 记录详细错误日志,包括错误对象和调用栈
console.error(`${logMessage}:`, error);
// 显示用户友好的错误提示
ElMessage.error(
`${userMessage}失败${error.message ? ': ' + error.message : ',请刷新页面重试'}`
);
};
};
/** 加载模板初始化数据(一级菜单、使用范围枚举) */
const getInit = async () => {
try {
const response = await init();
templateDataInit.value = response.data || {};
console.log('模板初始化数据:', templateDataInit.value);
} catch (error) {
handleApiError('初始化', '初始化接口异常')(error);
}
};
/** 清除科室选择时,加载所有科室模板 */
const handleOrgClear = () => {
orgId.value = '';
initTemplateTree({ id: '' });
};
/** 搜索模板(可扩展按科室+关键字筛选) */
const handleSearch = () => {
console.log('搜索模板,关键字:', searchKeyword.value);
initTemplateTree({ id: orgId.value });
};
/** 初始化病历模板树(按科室筛选) */
function initTemplateTree(data) {
const queryParams = {
organizationId: data.id || '', // 科室ID空表示所有科室
name: searchKeyword.value || '', // 模板名称(空表示不筛选)
};
getTreeList(queryParams)
.then((res) => {
templateData.value = res.data || [];
nextTick().then(() => {
expandTree(); // 展开树节点
})
// console.log('模板树数据(按科室筛选):', templateData.value);
})
.catch((error) => {
handleApiError('获取模板列表', '获取模板树失败')(error);
templateData.value = [];
});
}
/** 编辑模板(打开弹窗并回显数据) */
const editTemplate = async () => {
if (!currentNodeData.value) {
ElMessage.warning('请先选择一个模板节点');
return;
}
const loading = ElLoading.service({
lock: true,
text: '加载模板信息...',
background: 'rgba(0, 0, 0, 0.7)',
});
try {
const response = await getDefinitionById(currentNodeData.value.id);
if (response.data) {
openEditDialog(response.data);
}
} catch (error) {
handleApiError('加载模板信息', '加载模板信息失败')(error);
} finally {
loading.close();
}
};
/** 打开编辑弹窗,回显选中的模板数据并正确初始化科室选择 */
const openEditDialog = async (nodeData) => {
currentNodeData.value = nodeData;
console.log('回显模板数据:', nodeData);
// 回显表单数据(与接口返回字段匹配)
formData.primaryMenuEnum = nodeData.primaryMenuEnum;
formData.subMenu = nodeData.subMenu;
formData.version = nodeData.version;
formData.name = nodeData.name;
formData.vueRouter = nodeData.vueRouter;
formData.displayOrder = nodeData.displayOrder;
formData.useRangeEnum = nodeData.useRangeEnum;
formData.environment = nodeData.environment || '0';
formData.organizationIds = nodeData.organizationIds.map((id) => id.toString());
// 打开弹窗
dialogVisible.value = true;
};
// 处理节点点击,根据后台返回的路径加载组件
const handleNodeClick = async (data, node) => {
console.log('点击节点:', data, node);
// 检查是否为子节点没有children或children为空
// const isLeafNode = !data.children || data.children.length === 0;
if (node.isLeaf) {
// 存储当前节点数据
currentNodeData.value = data.document;
// 检查是否为子节点没有children或children为空
currentComponent.value = data.document.vueRouter || '';
} else {
currentNodeData.value = null;
currentComponent.value = null;
}
};
const toggleExpand = () => {
isExpanded.value = !isExpanded.value;
console.log('展开状态:', templateTree.value.store);
expandTree();
};
const expandTree = () => {
templateTree.value.store._getAllNodes().forEach((node) => {
node.expanded = isExpanded.value;
});
};
const refresh = () => {
initTemplateTree({ id: orgId.value });
};
// 新建模板
const newTemplate = () => {
dialogVisible.value = true;
};
// 删除模板
const deleteTemplate = async () => {
// 1. 检查是否选中节点
if (!currentNodeData.value) {
ElMessage.warning('请先选择一个模板节点');
return;
}
let loading = null;
const templateName = currentNodeData.value.name;
const templateId = currentNodeData.value.id;
try {
// 2. 显示确认对话框,增加操作描述
const confirmResult = await ElMessageBox.confirm(
`确定要删除模板「${templateName}」吗?<br>此操作不可撤销,删除后将无法恢复。`,
'删除确认',
{
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true,
center: true,
closeOnClickModal: false,
}
);
// 3. 用户确认删除后执行操作
if (confirmResult === 'confirm') {
// 显示加载状态
loading = ElLoading.service({
lock: true,
text: '正在删除模板...',
background: 'rgba(0, 0, 0, 0.7)',
});
// 调用删除API
await deleteDefinition(templateId);
// 删除成功处理
ElMessage.success(`模板「${templateName}」删除成功`);
// 清空当前选中状态
currentNodeData.value = null;
initFormData();
currentComponent.value = null;
// 刷新列表
refresh();
}
} catch (error) {
// 错误处理区分用户取消和API错误
if (error === 'cancel' || error === undefined) {
// 用户取消删除,不显示错误提示
console.log('用户取消删除操作');
} else {
// API错误或其他错误使用统一的错误处理函数
handleApiError('删除模板', '删除模板失败')(error);
}
} finally {
// 确保加载状态总是被关闭
if (loading) {
loading.close();
}
}
};
// 打印模板
const printTemplate = () => {
window.print();
};
// 标签页点击事件
const handleClick = (tab) => {
console.log('标签页点击:', tab);
};
</script>
<style scoped>
.case-templates-container {
display: flex;
flex-direction: column;
height: 91vh;
}
.toolbar {
display: flex;
padding: 10px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
gap: 10px;
}
.content-area {
display: flex;
flex: 1;
overflow: hidden;
}
.left-panel {
width: 280px;
border-right: 1px solid #ddd;
display: flex;
flex-direction: column;
padding: 10px;
}
.search-box {
display: flex;
margin-bottom: 10px;
}
.search-btn {
margin-left: 5px;
}
.template-tree {
flex: 1;
overflow-y: auto;
}
.middle-panel {
flex: 1;
overflow-y: auto;
padding: 10px;
}
</style>

View File

@@ -32,7 +32,7 @@
<PopoverList @search="handleSearch" :width="800" :modelValue="scope.row.name">
<template #popover-content="{}">
<DeviceList
v-if="scope.row.type == '2'"
v-if="scope.row.type == '2' || props.tab == 2 "
@selectRow="(row) => selectRow(row, scope.$index)"
:searchKey="searchKey"
/>
@@ -58,6 +58,26 @@
</el-form-item>
</template>
</el-table-column>
<el-table-column label="单位" align="center" prop="unitCode_dictText" width="100">
<template #default="scope">
<span v-if="!scope.row.isEdit">{{ scope.row.unitCode_dictText }}</span>
<el-form-item
v-else
:prop="`consumablesList.${scope.$index}.unitCode`"
:rules="rules.unitCode"
>
<el-select v-model="scope.row.unitCode" placeholder="">
<el-option
v-for="(item, index) in scope.row.unitCodeList"
@click="handleUnitCodeClick(item)"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</template>
</el-table-column>
<!-- <el-table-column label="使用范围" align="center" prop="rangeCode" width="150">
<template #default="scope">
<span v-if="!scope.row.isEdit">{{ scope.row.rangeCode_dictText }}</span>
@@ -70,7 +90,7 @@
</el-form-item>
</template>
</el-table-column> -->
<el-table-column label="范围" align="center" prop="rangeCode" width="250">
<!-- <el-table-column label="范围" align="center" prop="rangeCode" width="250">
<template #default="scope">
<span v-if="!scope.row.isEdit">{{ scope.row.rangeCode_dictText }}</span>
<el-form-item
@@ -88,7 +108,7 @@
</el-select>
</el-form-item>
</template>
</el-table-column>
</el-table-column> -->
<el-table-column label="启用状态" align="center" prop="statusEnum" width="250">
<template #default="scope">
<span v-if="!scope.row.isEdit">{{ scope.row.statusEnum_dictText }}</span>
@@ -208,6 +228,8 @@ watch(
quantity: item.quantity,
rangeCode: item.rangeCode,
rangeCode_dictText: item.rangeCode_dictText,
unitCode: item.unitCode,
unitCode_dictText: item.unitCode_dictText,
statusEnum: item.statusEnum,
statusEnum_dictText: item.statusEnum_enumText,
typeCode: item.typeCode,
@@ -259,8 +281,9 @@ function handleSave(row, index) {
itemNo: props.bindInfo.id,
devActId: row.devActId,
typeCode: props.bindInfo.typeCode,
rangeCode: row.rangeCode,
rangeCode: '3',
quantity: row.quantity,
unitCode: row.unitCode,
statusEnum: row.statusEnum,
id: row.id ? row.id : undefined,
devActTable: row.type == '1' ? 'wor_activity_definition' : 'adm_device_definition',
@@ -298,6 +321,10 @@ function initOptions() {
});
}
function handleUnitCodeClick(row, item) {
row.unitCode_dictText = item.label;
}
function handleSearch(value) {
searchKey.value = value;
}
@@ -306,6 +333,16 @@ initOptions();
function selectRow(row, index) {
form.consumablesList[index].devActId = row.id;
form.consumablesList[index].name = row.name;
if (row.minUnitCode == row.unitCode) {
form.consumablesList[index].unitCodeList = [
{ label: row.unitCode_dictText, value: row.unitCode },
];
} else {
form.consumablesList[index].unitCodeList = [
{ label: row.unitCode_dictText, value: row.unitCode },
{ label: row.minUnitCode_dictText, value: row.minUnitCode },
];
}
}
</script>

View File

@@ -185,6 +185,7 @@ import {
deleteImplementDepartment,
} from './components/implementDepartment';
import { debounce } from 'lodash-es';
import { fa } from 'element-plus/es/locales.mjs';
const { proxy } = getCurrentInstance();
const organization = ref([]);
const loading = ref(true);
@@ -256,12 +257,14 @@ function DiagnosisTreatmentList(row,index,type) {
}else if(type == 2){
params.searchKey = row.activityDefinitionId_dictText
}
console.log(params,'params');
getImplementDepartmentOne(params)
.then((res) => {
if (res.code === 200) {
projectList.value = [];
row.name = null;
projectList.value = res.data.records.map((item) => ({ value: item.id, info: item.name }));
catagoryList.value[index].projectList = projectList.value
} else {
proxy.$modal.msgError(res.msg);
@@ -278,7 +281,12 @@ function remoteMethod(query, row) {
statusEnum: 2,
activityCategoryCode: row.activityCategoryCode, // 确保已选诊疗目录
searchKey: query, // 模糊搜索关键字
...row,
categoryCode: row.activityCategoryCode,
pageSize:100,
};
console.log(params,row,query,'params');
getImplementDepartmentOne(params).then((res) => {
loading.value = false;
if (res.code === 200) {
@@ -291,6 +299,8 @@ function remoteMethod(query, row) {
proxy.$modal.msgError(res.msg);
}
});
loading.value = false;
}
/** 选择条数 */

View File

@@ -0,0 +1,161 @@
<template>
<div @keyup="handleKeyDown" tabindex="0" ref="tableWrapper">
<el-table
ref="adviceBaseRef"
height="400"
:data="adviceBaseList"
highlight-current-row
@current-change="handleCurrentChange"
row-key="patientId"
@cell-click="clickRow"
>
<el-table-column label="名称" align="center" prop="adviceName" />
<el-table-column label="类型" align="center" prop="activityType_enumText" />
<el-table-column label="包装单位" align="center" prop="unitCode_dictText" />
<el-table-column label="最小单位" align="center" prop="minUnitCode_dictText" />
<el-table-column label="规格" align="center" prop="volume" />
<el-table-column label="用法" align="center" prop="methodCode_dictText" />
<el-table-column label="频次" align="center" prop="rateCode_dictText" />
<el-table-column label="单次剂量" align="center" prop="dose" />
<el-table-column label="剂量单位" align="center" prop="doseUnitCode_dictText" />
<el-table-column label="注射药品" align="center" prop="injectFlag_enumText" />
<el-table-column label="皮试" align="center" prop="skinTestFlag_enumText" />
</el-table>
</div>
</template>
<script setup>
import { nextTick } from 'vue';
import { getAdviceBaseInfo } from './api';
import { throttle } from 'lodash-es';
const props = defineProps({
adviceQueryParams: {
type: Object,
default: '',
},
patientInfo: {
type: Object,
required: true,
},
});
const emit = defineEmits(['selectAdviceBase']);
const total = ref(0);
const adviceBaseRef = ref();
const tableWrapper = ref();
const currentIndex = ref(0); // 当前选中行索引
const currentSelectRow = ref({});
const queryParams = ref({
pageSize: 100,
pageNum: 1,
adviceTypes: '2,3',
});
const adviceBaseList = ref([]);
// 节流函数
const throttledGetList = throttle(
() => {
getList();
},
300,
{ leading: true, trailing: true }
);
// 监听adviceQueryParams变化
watch(
() => props.adviceQueryParams,
(newValue) => {
queryParams.value.searchKey = newValue.searchKey;
queryParams.value.adviceType = newValue.adviceType;
console.log(queryParams.value);
throttledGetList();
},
{ deep: true }
);
// 监听searchKey变化
watch(
() => props.adviceQueryParams?.searchKey,
(newVal) => {
queryParams.value.searchKey = newVal;
throttledGetList();
}
);
getList();
function getList() {
// queryParams.value.organizationId = '1922545444781481985';
getAdviceBaseInfo(queryParams.value).then((res) => {
adviceBaseList.value = res.data.records;
console.log(adviceBaseList.value)
total.value = res.data.total;
nextTick(() => {
currentIndex.value = 0;
if (adviceBaseList.value.length > 0) {
adviceBaseRef.value.setCurrentRow(adviceBaseList.value[0]);
}
});
});
}
// 处理键盘事件
const handleKeyDown = (event) => {
const key = event.key;
const data = adviceBaseList.value;
switch (key) {
case 'ArrowUp': // 上箭头
event.preventDefault(); // 阻止默认滚动行为
if (currentIndex.value > 0) {
currentIndex.value--;
setCurrentRow(data[currentIndex.value]);
}
break;
case 'ArrowDown': // 下箭头`
event.preventDefault();
if (currentIndex.value < data.length - 1) {
currentIndex.value++;
setCurrentRow(data[currentIndex.value]);
}
break;
case 'Enter': // 回车键
// const currentRow = adviceBaseRef.value.getSelectionRows();
event.preventDefault();
if (currentSelectRow.value) {
// 这里可以触发自定义逻辑,如弹窗、跳转等
emit('selectAdviceBase', currentSelectRow.value);
}
break;
}
};
// 设置选中行(带滚动)
const setCurrentRow = (row) => {
adviceBaseRef.value.setCurrentRow(row);
// 滚动到选中行
const tableBody = adviceBaseRef.value.$el.querySelector('.el-table__body-wrapper');
const currentRowEl = adviceBaseRef.value.$el.querySelector('.current-row');
if (tableBody && currentRowEl) {
currentRowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
};
// 当前行变化时更新索引
const handleCurrentChange = (currentRow) => {
currentIndex.value = adviceBaseList.value.findIndex((item) => item === currentRow);
currentSelectRow.value = currentRow;
};
function clickRow(row) {
emit('selectAdviceBase', row);
}
defineExpose({
handleKeyDown,
});
</script>
<style scoped>
.popover-table-wrapper:focus {
outline: 2px solid #409eff; /* 聚焦时的高亮效果 */
}
</style>

View File

@@ -0,0 +1,129 @@
import request from '@/utils/request'
/**
* 获取个人组套
* @param {*} queryParams
*/
export function getPersonalList(queryParams) {
return request({
url: '/personalization/orders-group-package/get-personal',
method: 'get',
params: queryParams
})
}
/**
* 获取科室组套
* @param {*} queryParams
*/
export function getDeptList(queryParams) {
return request({
url: '/personalization/orders-group-package/get-organization',
method: 'get',
params: queryParams
})
}
/**
* 获取全院组套
* @param {*} queryParams
*/
export function getAllList(queryParams) {
return request({
url: '/personalization/orders-group-package/get-hospital',
method: 'get',
params: queryParams
})
}
/**
* 保存个人组套
* @param {*} data
*/
export function savePersonal(data) {
return request({
url: '/personalization/orders-group-package/save-personal',
method: 'post',
data: data
})
}
/**
* 保存科室组套
* @param {*} data
*/
export function saveDepartment(data) {
return request({
url: '/personalization/orders-group-package/save-organization',
method: 'post',
data: data
})
}
/**
* 保存全院组套
* @param {*} data
*/
export function saveAll(data) {
return request({
url: '/personalization/orders-group-package/save-hospital',
method: 'post',
data: data
})
}
/**
* 查询组套明细
* @param {*} data
*/
export function queryGroupDetail(params) {
return request({
url: '/personalization/orders-group-package/get-group-package-detail',
method: 'get',
params: params
})
}
/**
* 删除组套
* @param {*} data
*/
export function deleteGroup(data) {
return request({
url: '/personalization/orders-group-package/group-package-detail?groupPackageId=' + data.groupPackageId,
method: 'delete'
})
}
/**
* 查询参与者下拉列表
* @param {*} data
*/
export function queryParticipantList(params) {
return request({
url: '/app-common/practitioner-list',
method: 'get',
params: params
})
}
/**
* 获取药品列表
*/
export function getAdviceBaseInfo(queryParams) {
return request({
url: '/doctor-station/advice/advice-base-info',
method: 'get',
params: queryParams
})
}
/**
* 获取科室列表
*/
export function getOrgTree() {
return request({
url: '/base-data-manage/organization/organization',
method: 'get',
})
}

View File

@@ -0,0 +1,686 @@
<template>
<div class="app-container">
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="个人" name="personal">
<div class="mb10">
<el-row :gutter="10" class="mb10">
<el-col :span="6">
<el-input v-model="personalQuery.searchKey" placeholder="请输入名称" clearable @keyup.enter="getPersonalListData" />
</el-col>
<el-col :span="1.5">
<el-button type="primary" icon="Search" @click="getPersonalListData">查询</el-button>
</el-col>
</el-row>
<el-button type="primary" plain icon="Plus" @click="handleAdd('personal')">新增</el-button>
</div>
<el-table :data="personalList" border v-loading="loading.personal">
<el-table-column prop="name" label="名称" />
<el-table-column prop="practitionerName" label="参与者" />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleEdit('personal', scope.row)">编辑</el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete('personal', scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="科室" name="department">
<div class="mb10">
<el-row :gutter="10" class="mb10">
<el-col :span="6">
<el-input v-model="departmentQuery.searchKey" placeholder="请输入名称" clearable @keyup.enter="getDepartmentListData" />
</el-col>
<el-col :span="1.5">
<el-button type="primary" icon="Search" @click="getDepartmentListData">查询</el-button>
</el-col>
</el-row>
<el-button type="primary" plain icon="Plus" @click="handleAdd('department')">新增</el-button>
</div>
<el-table :data="departmentList" border v-loading="loading.department">
<el-table-column prop="name" label="名称" />
<el-table-column prop="organizationName" label="科室" />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleEdit('department', scope.row)">编辑</el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete('department', scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="全院" name="hospital">
<div class="mb10">
<el-row :gutter="10" class="mb10">
<el-col :span="6">
<el-input v-model="hospitalQuery.searchKey" placeholder="请输入名称" clearable @keyup.enter="getHospitalListData" />
</el-col>
<el-col :span="1.5">
<el-button type="primary" icon="Search" @click="getHospitalListData">查询</el-button>
</el-col>
</el-row>
<el-button type="primary" plain icon="Plus" @click="handleAdd('hospital')">新增</el-button>
</div>
<el-table :data="hospitalList" border v-loading="loading.hospital">
<el-table-column prop="name" label="名称" />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleEdit('hospital', scope.row)">编辑</el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete('hospital', scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<!-- 新增/编辑弹窗 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="800px"
@close="handleDialogClose"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="参与者" prop="practitionerId" v-if="currentTab == 'personal'">
<el-select
v-model="formData.practitionerId"
placeholder="请选择参与者"
clearable
>
<el-option
v-for="item in participantListOptions"
:key="item.practitionerId"
:label="item.practitionerName"
:value="item.practitionerId"
/>
</el-select>
</el-form-item>
<el-form-item label="科室" prop="organizationId" v-if="currentTab == 'department'">
<el-tree-select
clearable
v-model="formData.organizationId"
:data="organization"
:props="{ value: 'id', label: 'name', children: 'children' }"
value-key="id"
check-strictly
default-expand-all
placeholder="请选择科室"
:render-after-expand="false"
@change="handleOrgChange"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-table
max-height="650"
ref="prescriptionRef"
:data="prescriptionList"
row-key="uniqueKey"
border
@cell-click="clickRow"
:expand-row-keys="expandOrder"
>
<el-table-column label="医嘱" align="center" prop="productName">
<template #default="scope">
<template v-if="getRowDisabled(scope.row)">
<el-popover
:popper-style="{ padding: '0' }"
placement="bottom-start"
:visible="scope.row.showPopover"
:width="1200"
>
<adviceBaseList
ref="adviceTableRef"
:popoverVisible="scope.row.showPopover"
:adviceQueryParams="adviceQueryParams"
@selectAdviceBase="(row) => selectAdviceBase(scope.row.uniqueKey, row)"
/>
<template #reference>
<el-input
:ref="'adviceRef' + scope.$index"
style="width: 50%"
v-model="scope.row.adviceName"
placeholder="请选择项目"
@input="(value) => handleInput(value, scope.row, scope.$index)"
@click="handleFocus(scope.row, scope.$index)"
@keyup.enter.stop="handleFocus(scope.row, scope.$index)"
@keydown="
(e) => {
if (!scope.row.showPopover) return;
// 拦截上下键和回车事件
if (['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) {
e.preventDefault();
// 传递事件到弹窗容器
adviceTableRef.handleKeyDown(e);
}
}
"
@blur="handleBlur(scope.row)"
/>
</template>
</el-popover>
</template>
<span v-else>{{ scope.row.adviceName }}</span>
</template>
</el-table-column>
<el-table-column label="数量" align="center" width="100" prop="sortNumber">
<template #default="scope">
<template v-if="!scope.row.groupPackageId">
<el-input
v-model="scope.row.sortNumber"
type="number"
min="1"
@change="handleQuantityChange(scope.row)"
/>
</template>
<span v-else>{{ scope.row.sortNumber }}</span>
</template>
</el-table-column>
<el-table-column label="单位" align="center" width="120" prop="unitCode">
<template #default="scope">
<template v-if="!scope.row.groupPackageId">
<el-select
v-model="scope.row.selectUnitCode"
placeholder="请选择单位"
@change="handleUnitChange(scope.row)"
>
<el-option
v-if="scope.row.minUnitCode"
:key="scope.row.minUnitCode"
:label="scope.row.minUnitCode_dictText || scope.row.minUnitCode"
:value="scope.row.minUnitCode"
/>
<el-option
v-if="scope.row.unitCode"
:key="scope.row.unitCode"
:label="scope.row.unitCode_dictText || scope.row.unitCode"
:value="scope.row.unitCode"
/>
</el-select>
</template>
<span>{{ scope.row.unitCodeName }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="80">
<template #default="scope">
<el-button
type="danger"
icon="Delete"
circle
size="small"
@click="handleDeleteRow(scope.$index, scope.row)"
/>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import {
getPersonalList,
getDeptList,
getAllList,
queryParticipantList,
savePersonal,
saveDepartment,
saveAll,
queryGroupDetail,
getOrgTree,
deleteGroup
} from './components/api.js';
import adviceBaseList from './components/adviceBaseList';
import { ElMessage } from 'element-plus';
import useUserStore from '@/store/modules/user';
// 定义tab相关数据
const activeTab = ref('personal')
// 定义各tab列表数据
const personalList = ref([])
const departmentList = ref([])
const hospitalList = ref([])
const prescriptionList = ref([]);
const expandOrder = ref([]); //目前的展开行
// 查询参数
const personalQuery = reactive({ searchKey: '' })
const departmentQuery = reactive({ searchKey: '' })
const hospitalQuery = reactive({ searchKey: '' })
// 加载状态
const loading = reactive({
personal: false,
department: false,
hospital: false
})
// 提交按钮加载状态
const submitLoading = ref(false)
// 弹窗相关数据
const dialogVisible = ref(false)
const dialogTitle = ref('')
const currentTab = ref('') // 记录当前操作的tab
const isEdit = ref(false) // 是否为编辑模式
const currentRow = ref(null) // 当前编辑的行数据
const participantListOptions = ref([])
const organization = ref([]);
const adviceQueryParams = reactive({})
// 表单数据
const formData = reactive({
id: undefined,
name: ''
})
const nextId = ref(1);
const rowIndex = ref(0);
// 表单验证规则
const formRules = {
name: [
{ required: true, message: '请输入名称', trigger: 'blur' }
]
}
// 表单引用
const formRef = ref()
// 页面加载时获取所有列表数据
onMounted(() => {
// 可以添加初始查询参数
const initialParams = {
searchKey: '' // 初始查询关键字
};
fetchAllData(initialParams)
getInit()
})
function getInit() {
queryParticipantList().then((res) => {
participantListOptions.value = res.data
});
getOrgTree().then(res => {
organization.value = res.data.records;
})
}
// 获取所有数据
function fetchAllData(params = {}) {
getPersonalListData(params)
getDepartmentListData(params)
getHospitalListData(params)
}
// 获取个人医嘱列表
function getPersonalListData(params = {}) {
// 合并查询参数
Object.assign(personalQuery, params);
loading.personal = true
getPersonalList(personalQuery).then(response => {
personalList.value = response.data
loading.personal = false
}).catch(error => {
console.error('获取个人医嘱列表失败:', error)
loading.personal = false
proxy.$modal.msgError('获取个人医嘱列表失败')
})
}
// 获取科室医嘱列表
function getDepartmentListData(params = {}) {
// 合并查询参数
Object.assign(departmentQuery, params);
loading.department = true
getDeptList(departmentQuery).then(response => {
departmentList.value = response.data
loading.department = false
}).catch(error => {
console.error('获取科室医嘱列表失败:', error)
loading.department = false
proxy.$modal.msgError('获取科室医嘱列表失败')
})
}
// 获取全院医嘱列表
function getHospitalListData(params = {}) {
// 合并查询参数
Object.assign(hospitalQuery, params);
loading.hospital = true
getAllList(hospitalQuery).then(response => {
hospitalList.value = response.data
loading.hospital = false
}).catch(error => {
console.error('获取全院医嘱列表失败:', error)
loading.hospital = false
proxy.$modal.msgError('获取全院医嘱列表失败')
})
}
// tab切换处理
function handleTabChange(tab) {
// 根据切换到的tab调用相应的接口获取数据
switch (tab) {
case 'personal':
getPersonalListData()
break
case 'department':
getDepartmentListData()
break
case 'hospital':
getHospitalListData()
break
}
}
// 新增按钮处理
function handleAdd(tab) {
currentTab.value = tab
isEdit.value = false
dialogTitle.value = '新增医嘱'
dialogVisible.value = true
// 重置表单
prescriptionList.value.unshift({
uniqueKey: nextId.value++,
showPopover: false,
check: false,
isEdit: true,
statusEnum: 1,
});
formData.groupPackageId = undefined
formData.name = ''
// 获取当前登录用户信息
const userStore = useUserStore()
const userId = userStore.id
const orgId = userStore.orgId
const practitionerId = userStore.practitionerId
// 根据不同tab设置默认值
if (tab === 'personal') {
// 个人医嘱默认设置当前用户为参与者
formData.practitionerId = practitionerId
} else if (tab === 'department') {
// 科室医嘱默认设置当前科室
formData.organizationId = orgId
}
// 全院医嘱不需要设置默认值
}
// 编辑按钮处理
function handleEdit(tab, row) {
currentTab.value = tab
isEdit.value = true
currentRow.value = row
dialogTitle.value = '编辑医嘱'
dialogVisible.value = true
formData.groupPackageId = row.groupPackageId
formData.name = row.name
formData.practitionerId = row.practitionerId
formData.organizationId = row.organizationId
// 填充表单数据
queryGroupDetail({ groupPackageId: row.groupPackageId }).then(res => {
prescriptionList.value = res.data.map(item => {
return {
groupPackageId: item.groupPackageId,
adviceDefinitionId: item.orderDefinitionId,
adviceTableName: item.orderDefinitionTable,
sortNumber: item.quantity,
selectUnitCode: item.unitCode,
adviceName: item.orderDefinitionName,
unitCodeName: item.unitCodeName
}
})
prescriptionList.value.unshift({
uniqueKey: nextId.value++,
showPopover: false,
check: false,
isEdit: true,
statusEnum: 1,
});
})
}
// 删除按钮处理
function handleDelete(tab, row) {
// 显示确认框
proxy.$modal.confirm('确定要删除该医嘱吗?').then(() => {
deleteGroup({groupPackageId: row.groupPackageId}).then(res => {
proxy.$modal.msgSuccess(res.msg)
fetchAllData()
})
}).catch(() => {})
}
// 提交表单
function submitForm() {
formRef.value.validate((valid) => {
if (valid) {
submitLoading.value = true
// 模拟提交操作这里应该调用相应的API
setTimeout(() => {
console.log('提交表单数据:', formData)
let params = {...formData}
// 过滤掉空列表项没有adviceDefinitionId的项
params.detailList = prescriptionList.value
.filter(item => item.adviceDefinitionId) // 过滤掉空列表项
.map(item => {
return {
orderDefinitionId: item.adviceDefinitionId,
orderDefinitionTable: item.adviceTableName,
quantity: item.sortNumber,
unitCode: item.selectUnitCode,
}
})
// 编辑模式
switch (currentTab.value) {
case 'personal':
savePersonal(params).then(res => {
if (res.code === 200) {
ElMessage.success(res.message)
// 重新获取数据以保持一致性
fetchAllData()
}
submitLoading.value = false
dialogVisible.value = false
// 清空处方列表
prescriptionList.value = []
})
break
case 'department':
saveDepartment(params).then(res => {
if (res.code === 200) {
ElMessage.success(res.message)
// 重新获取数据以保持一致性
fetchAllData()
}
submitLoading.value = false
dialogVisible.value = false
// 清空处方列表
prescriptionList.value = []
})
break
case 'hospital':
saveAll(params).then(res => {
if (res.code === 200) {
ElMessage.success(res.message)
// 重新获取数据以保持一致性
fetchAllData()
}
submitLoading.value = false
dialogVisible.value = false
// 清空处方列表
prescriptionList.value = []
})
break
}
}, 500)
}
})
}
/**
* 选择药品回调
*/
function selectAdviceBase(key, row) {
prescriptionList.value[rowIndex.value] = {
...prescriptionList.value[rowIndex.value],
...JSON.parse(JSON.stringify(row)),
};
prescriptionList.value[rowIndex.value].orgId = undefined;
prescriptionList.value[rowIndex.value].dose = undefined;
prescriptionList.value[rowIndex.value].doseUnitCode = row.doseUnitCode;
prescriptionList.value[rowIndex.value].minUnitCode = row.minUnitCode;
prescriptionList.value[rowIndex.value].unitCode =
row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode;
prescriptionList.value[rowIndex.value].categoryEnum = row.categoryCode;
prescriptionList.value[rowIndex.value].isEdit = false;
prescriptionList.value[rowIndex.value].definitionId = JSON.parse(
JSON.stringify(row)
).chargeItemDefinitionId;
prescriptionList.value.unshift({
uniqueKey: nextId.value++,
showPopover: false,
check: false,
isEdit: true,
statusEnum: 1,
});
expandOrder.value = [key];
}
function handleFocus(row, index) {
rowIndex.value = index;
row.showPopover = true;
// 将输入框的值传递给adviceBaseList组件作为查询条件
adviceQueryParams.searchKey = row.adviceName || '';
}
// 处理输入事件
function handleInput(value, row, index) {
// 更新查询参数
adviceQueryParams.searchKey = value || '';
// 显示弹窗
handleFocus(row, index);
}
function handleBlur(row) {
row.showPopover = false;
}
/**
* 点击行赋值
*/
function clickRow(row) {
}
function getRowDisabled(row) {
return row.isEdit;
}
// 处理数量变化
function handleQuantityChange(row) {
// 验证数量是否为正整数
if (row.sortNumber && row.sortNumber > 0) {
row.sortNumber = parseInt(row.sortNumber);
} else {
row.sortNumber = 1;
}
// 触发更新列表事件
prescriptionList.value = [...prescriptionList.value];
}
// 处理单位变化
function handleUnitChange(row) {
// 当单位改变时触发的逻辑可以在这里添加
console.log('单位已更改:', row.unitCode);
// 触发更新列表事件
prescriptionList.value = [...prescriptionList.value];
}
// 删除行
function handleDeleteRow(index, row) {
// 确保至少保留一行
if (prescriptionList.value.length <= 1) {
proxy.$modal.msgWarning('至少保留一行');
return;
}
// 确认删除
proxy.$modal.confirm('确定要删除该行吗?').then(() => {
prescriptionList.value.splice(index, 1);
proxy.$modal.msgSuccess('删除成功');
}).catch(() => {
// 取消删除
});
}
// 判断是否为叶子节点
function isLeafNode(node) {
return !node.children || node.children.length === 0;
}
// 弹窗关闭处理
function handleDialogClose() {
formRef.value.resetFields()
// 清空处方列表
prescriptionList.value = []
}
// 处理科室选择变化
function handleOrgChange(value) {
if (!value) return;
// 查找选中的节点
const findNode = (nodes) => {
for (let node of nodes) {
if (node.id === value) {
return node;
}
if (node.children && node.children.length > 0) {
const found = findNode(node.children);
if (found) return found;
}
}
return null;
};
const selectedNode = findNode(organization.value);
// 如果选中的不是叶子节点,则清空选择
if (selectedNode && !isLeafNode(selectedNode)) {
proxy.$modal.msgWarning('只能选择末级科室');
formData.organizationId = null;
}
}
// 获取当前实例用于使用proxy
const { proxy } = getCurrentInstance()
</script>
<style scoped>
.mb10 {
margin-bottom: 10px;
}
</style>