Files
his/openhis-ui-vue3/src/views/maintainSystem/lisGroup/index.vue
2026-01-15 15:01:05 +08:00

930 lines
23 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="lis-group-maintain">
<!-- 标题区域 -->
<div class="header">
<h2>LIS分组维护</h2>
</div>
<!-- 表格展示区域 -->
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th style="width: 50px;"></th>
<th style="width: 150px;">卫生机构</th>
<th style="width: 150px;">日期</th>
<th style="width: 200px;">LIS分组名称</th>
<th style="width: 120px;">采血管</th>
<th style="width: 300px;">备注</th>
<th style="width: 150px;">操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in tableData"
:key="item.id || index"
:class="{ 'editing-row': item.editing, 'success-row': item.success, 'deleting-row': item.deleting }"
>
<td>{{ index + 1 }}</td>
<td>
<template v-if="item.editing">
<input
type="text"
v-model="item.healthInstitution"
disabled
>
</template>
<template v-else>
{{ item.healthInstitution }}
</template>
</td>
<td>
<template v-if="item.editing">
<input type="text" v-model="item.date" disabled>
</template>
<template v-else>
{{ item.date }}
</template>
</td>
<td>
<template v-if="item.editing">
<input
type="text"
v-model="item.lisGroupName"
placeholder="请输入分组名称"
:class="{ 'error-input': item.errors?.lisGroupName, 'focus-target': item.isNew }"
>
<span v-if="item.errors?.lisGroupName" class="error-message">{{ item.errors.lisGroupName }}</span>
</template>
<template v-else>
{{ item.lisGroupName }}
</template>
</td>
<td>
<template v-if="item.editing">
<select
v-model="item.bloodCollectionTube"
:class="{ 'error-input': item.errors?.bloodCollectionTube }"
>
<option value="">请选择采血管</option>
<option v-for="tube in bloodTubeOptions" :key="tube" :value="tube">
{{ tube }}
</option>
</select>
<span v-if="item.errors?.bloodCollectionTube" class="error-message">{{ item.errors.bloodCollectionTube }}</span>
</template>
<template v-else>
{{ item.bloodCollectionTube }}
</template>
</td>
<td>
<template v-if="item.editing">
<input type="text" v-model="item.remark" placeholder="请输入备注信息">
</template>
<template v-else>
{{ item.remark || '' }}
</template>
</td>
<td>
<div class="actions">
<template v-if="!item.editing">
<button class="btn btn-edit" @click="startEdit(item)"></button>
<button class="btn btn-primary" @click="addRow">+</button>
<button class="btn btn-delete" @click="deleteRow(index)">🗑</button>
</template>
<template v-else>
<button class="btn btn-confirm" @click="confirmEdit(item)"></button>
<button class="btn btn-cancel" @click="cancelEdit(item)">×</button>
</template>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页区域 -->
<div class="pagination">
<button class="pagination-btn" @click="prevPage" :disabled="currentPage <= 1"></button>
<span>{{ currentPage }}</span>
<button class="pagination-btn" @click="nextPage" :disabled="currentPage >= totalPages"></button>
</div>
</div>
</template>
<script>
import {computed, nextTick, onMounted, reactive, ref} from 'vue'
import {addLisGroup, delLisGroup, listLisGroup, updateLisGroup} from '@/api/system/checkType'
import {getDicts} from "@/api/system/dict/data";
import useUserStore from '@/store/modules/user';
import {ElMessage, ElMessageBox} from 'element-plus';
export default {
name: 'LisGroupMaintain',
setup() {
// 获取用户store
const userStore = useUserStore();
// 加载状态
const loading = ref(false)
// 采血管选项
const bloodTubeOptions = ref([]);
// 加载采血管颜色字典数据
const loadBloodTubeDict = async () => {
try {
const response = await getDicts('test_the_color_of_the_blood_collection_tube');
if (response.code === 200 && response.data) {
// 根据实际返回数据格式调整
bloodTubeOptions.value = response.data.map(item => item.label || item.dictLabel);
}
} catch (error) {
// 如果字典获取失败,使用默认选项作为备选
bloodTubeOptions.value = ['黄管', '紫管', '蓝管'];
}
};
// 分页相关数据
const currentPage = ref(1)
const pageSize = ref(10)
const totalItems = ref(2) // 总数据量,实际应用中应该从后端获取
// 总页数
const totalPages = computed(() => {
return Math.ceil(totalItems.value / pageSize.value)
})
// 获取当前用户的卫生机构名称
const getCurrentHospital = () => {
return userStore.hospitalName || userStore.orgName || '演示医院';
};
// 表格数据
const tableData = reactive([])
// 获取当前日期
const getCurrentDate = () => {
const date = new Date()
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 加载LIS分组数据
const loadLisGroups = async () => {
// 确保采血管字典数据已加载
if (bloodTubeOptions.value.length === 0) {
await loadBloodTubeDict();
}
try {
loading.value = true
// 构建查询参数
const query = {
pageNum: currentPage.value,
pageSize: pageSize.value
}
// 调用接口获取数据
const response = await listLisGroup(query)
// 清空现有数据
tableData.splice(0, tableData.length)
totalItems.value = 0
// 处理明确的数据结构
if (response && response.code === 200) {
// 清空现有数据
tableData.splice(0, tableData.length);
// 获取最内层的数据结构
const innerResponse = response.data;
if (innerResponse && innerResponse.code === 200) {
const data = innerResponse.data;
if (data && Array.isArray(data.records)) {
// 更新总记录数
totalItems.value = data.total || 0;
// 处理每条记录
data.records.forEach((item, index) => {
const formattedItem = {
id: item.id,
healthInstitution: item.hospital,
date: item.date,
lisGroupName: item.groupName,
bloodCollectionTube: item.tube,
remark: item.remark || '',
editing: false
};
tableData.push(formattedItem);
});
} else {
totalItems.value = 0;
}
} else {
totalItems.value = 0;
}
} else {
// 非成功状态码
const errorMsg = response.msg || response.message || `请求失败,状态码: ${response.code}`
showMessage(errorMsg, 'error')
totalItems.value = 0;
}
} catch (error) {
// 提取错误信息
let errorMessage = '加载LIS分组数据失败请稍后重试'
if (error.response) {
// 服务器响应了但状态码不是2xx
if (error.response.data) {
errorMessage = error.response.data.msg ||
error.response.data.message ||
error.response.data.error ||
`服务器错误 (${error.response.status})`
} else {
errorMessage = `服务器错误 (${error.response.status})`
}
} else if (error.request) {
// 请求已发出,但未收到响应
errorMessage = '网络错误,服务器未响应,请检查网络连接'
} else if (error.message) {
// 请求配置出错
errorMessage = error.message
}
// 显示错误信息
showMessage(errorMessage, 'error')
} finally {
loading.value = false
}
}
// 开始编辑
const startEdit = (item) => {
// 存储原始数据,用于取消操作
item.originalData = { ...item }
item.editing = true
}
// 验证数据
const validateData = (item) => {
const errors = {}
// 清除之前的错误信息
delete item.errors
// 验证LIS分组名称
if (!item.lisGroupName || item.lisGroupName.trim() === '') {
errors.lisGroupName = 'LIS分组名称不能为空'
} else if (item.lisGroupName.length > 50) {
errors.lisGroupName = 'LIS分组名称不能超过50个字符'
}
// 验证采血管
if (!item.bloodCollectionTube) {
errors.bloodCollectionTube = '请选择采血管'
}
// 验证备注长度
if (item.remark && item.remark.length > 200) {
errors.remark = '备注信息不能超过200个字符'
}
if (Object.keys(errors).length > 0) {
item.errors = errors
return false
}
return true
}
// 显示提示信息
const showMessage = (message, type = 'success') => {
// 使用Element Plus的消息组件
if (type === 'error') {
ElMessage.error(message)
} else {
ElMessage.success(message)
}
}
// 确认编辑
const confirmEdit = async (item) => {
// 验证数据
if (!validateData(item)) {
showMessage('请检查并修正错误信息', 'error')
return
}
// 检查是否有重复的分组名称(在同一个卫生机构内)
const duplicate = tableData.find(data =>
data.id !== item.id &&
data.healthInstitution === item.healthInstitution &&
data.lisGroupName === item.lisGroupName
)
if (duplicate) {
if (!item.errors) item.errors = {}
item.errors.lisGroupName = '该分组名称已存在'
showMessage('分组名称重复,请修改', 'error')
return
}
try {
loading.value = true
// 准备提交数据(使用后端期望的字段名)
const submitData = {
id: item.id,
hospital: item.healthInstitution, // 前端字段映射到后端字段
date: item.date,
groupName: item.lisGroupName, // 前端字段映射到后端字段
tube: item.bloodCollectionTube, // 前端字段映射到后端字段
remark: item.remark
}
let response
// 判断是新增还是修改
if (item.isNew) {
// 新增
response = await addLisGroup(submitData)
} else {
// 修改
response = await updateLisGroup(submitData)
}
// 处理响应
if (response.code === 200) {
// 保存编辑
item.editing = false
delete item.originalData
delete item.errors
// 先检查是否是新增记录再删除isNew属性
const isNewRecord = item.isNew
delete item.isNew
// 添加保存成功样式,包括新增记录
item.success = true
// 0.5秒后移除成功样式
setTimeout(() => {
item.success = false
}, 500)
showMessage('保存成功')
// 修改完成后重新请求后端本页数据,确保数据准确性
setTimeout(() => {
loadLisGroups()
}, 500)
} else {
showMessage(`保存失败: ${response.msg || '未知错误'}`, 'error')
}
} catch (error) {
showMessage(`保存失败: ${error.message || '未知错误'}`, 'error')
} finally {
loading.value = false
}
}
// 取消编辑
const cancelEdit = (item) => {
try {
if (item.isNew) {
// 如果是新增的行,从数组中移除
const index = tableData.findIndex(row => row.id === item.id)
if (index > -1) {
tableData.splice(index, 1)
// 更新总数
if (totalItems.value > 0) {
totalItems.value--
}
}
} else if (item.originalData) {
// 恢复原始数据
Object.assign(item, item.originalData)
delete item.originalData
item.editing = false
} else {
item.editing = false
}
} catch (error) {
showMessage('取消编辑失败', 'error')
}
}
// 添加新行
const addRow = async () => {
// 检查是否已有正在编辑的行
const editingRow = tableData.find(item => item.editing)
if (editingRow) {
showMessage('请先完成当前行的编辑', 'error')
return
}
const newId = String(Math.max(...tableData.map(item => parseInt(item.id) || 0), 0) + 1)
// 从用户信息中获取当前卫生机构
const currentHospital = getCurrentHospital()
tableData.push({
id: '',
healthInstitution: currentHospital, // 默认当前操作工号的卫生机构
date: getCurrentDate(), // 默认当前系统时间
lisGroupName: '',
bloodCollectionTube: '',
remark: '',
editing: true,
errors: null,
isNew: true // 标记为新记录
})
// 等待DOM更新后聚焦到第一个输入框
await nextTick()
const focusElement = document.querySelector('.focus-target')
if (focusElement) {
focusElement.focus()
// 清除focus-target类避免下次添加行时影响
focusElement.classList.remove('focus-target')
}
}
// 删除行
const deleteRow = async (index) => {
const item = tableData[index]
// 检查是否正在编辑
if (item.editing) {
showMessage('请先完成或取消编辑', 'error')
return
}
try {
// 使用Element Plus的确认对话框
await ElMessageBox.confirm(
`确定要删除"${item.lisGroupName}"这条记录吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
loading.value = true
// 添加删除行标记,触发渐隐效果
item.deleting = true
// 等待300ms让渐隐效果完成
await new Promise(resolve => setTimeout(resolve, 300))
// 如果是新添加的记录,直接从数组中移除
if (item.isNew) {
tableData.splice(index, 1)
showMessage('删除成功')
totalItems.value--
} else {
// 调用API删除
const response = await delLisGroup(item.id)
if (response.code === 200) {
// 更新总数
totalItems.value--
// 检查当前页码是否仍然有效
const newTotalPages = Math.ceil(totalItems.value / pageSize.value)
if (currentPage.value > newTotalPages && newTotalPages > 0) {
// 如果当前页码大于总页数且总页数大于0调整为最后一页
currentPage.value = newTotalPages
}
// 重新加载当前页数据,保持页码不变
loadLisGroups()
showMessage('删除成功')
} else {
showMessage(`删除失败: ${response.msg || '未知错误'}`, 'error')
// 如果删除失败,移除删除标记
item.deleting = false
}
}
} catch (error) {
if (error !== 'cancel') {
showMessage(`删除失败: ${error.message || '未知错误'}`, 'error')
// 如果删除失败,移除删除标记
if (tableData[index]) {
tableData[index].deleting = false
}
}
} finally {
loading.value = false
}
}
// 分页方法
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
// 调用加载数据方法
loadLisGroups()
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
// 调用加载数据方法
loadLisGroups()
}
}
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
// 调用加载数据方法
loadLisGroups()
}
}
// 组件挂载时加载数据
onMounted(() => {
// 加载采血管字典数据
loadBloodTubeDict();
// 加载表格数据
loadLisGroups()
})
return {
tableData,
bloodTubeOptions,
startEdit,
confirmEdit,
cancelEdit,
addRow,
deleteRow,
currentPage,
totalPages,
prevPage,
nextPage,
goToPage,
loading
}
}
}
</script>
<style scoped>
.lis-group-maintain {
padding: 20px;
min-height: 100vh;
}
.header {
height: 56px;
border-bottom: 1px solid #e8e8e8;
margin-bottom: 20px;
display: flex;
align-items: center;
}
.header h2 {
font-size: 18px;
font-weight: 500;
color: #333;
margin: 0;
}
.table-container {
margin-bottom: 20px;
overflow-x: auto;
border: 1px solid #e8e8e8;
border-radius: 2px;
}
.data-table {
width: 100%;
border-collapse: collapse;
text-align: left;
background-color: #fff;
}
.data-table th,
.data-table td {
padding: 12px;
border: 1px solid #e8e8e8;
word-break: break-all;
vertical-align: middle;
}
.data-table th {
background-color: #fafafa;
font-weight: 500;
}
.editing-row {
background-color: #f0f8ff;
}
/* 保存成功行样式 */
.success-row {
background-color: #f6ffed !important;
transition: background-color 0.3s ease;
}
.actions {
white-space: nowrap;
}
.pagination-section {
height: 48px;
display: flex;
justify-content: center;
align-items: center;
}
/* 分页样式 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
padding: 16px 0;
margin-top: 8px;
}
.pagination span {
font-size: 14px;
margin: 0 16px;
background-color: #1890FF;
color: white;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.pagination-btn {
background: none;
border: 1px solid #D9D9D9;
border-radius: 4px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.pagination-btn:hover {
border-color: #1890FF;
color: #1890FF;
}
.pagination-btn:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.pagination-number.active {
background-color: #1890ff;
color: #fff;
border-color: #1890ff;
}
.pagination-number.active:hover {
background-color: #40a9ff;
border-color: #40a9ff;
color: #fff;
}
.btn {
width: 24px;
height: 24px;
padding: 0;
margin-right: 5px;
border: 1px solid #d9d9d9;
background-color: #fff;
cursor: pointer;
border-radius: 4px;
font-size: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn:hover {
border-color: #40a9ff;
color: #40a9ff;
}
.btn-primary {
background-color: #1890ff;
color: #fff;
border-color: #1890ff;
}
.btn-primary:hover {
background-color: #40a9ff;
border-color: #40a9ff;
color: #fff;
}
.btn-confirm {
background-color: #52c41a;
color: #fff;
border-color: #52c41a;
}
.btn-confirm:hover {
background-color: #73d13d;
border-color: #73d13d;
color: #fff;
}
.btn-cancel {
background-color: #f5f5f5;
color: #333;
border-color: #d9d9d9;
}
.btn-edit {
background-color: #faad14;
color: #fff;
border-color: #faad14;
}
.btn-edit:hover {
background-color: #ffc53d;
border-color: #ffc53d;
color: #fff;
}
.btn-delete {
background-color: #ff4d4f;
color: #fff;
border-color: #ff4d4f;
}
.btn-delete:hover {
background-color: #ff7875;
border-color: #ff7875;
color: #fff;
}
input, select {
width: 100%;
padding: 4px 6px;
border: 1px solid #d9d9d9;
border-radius: 2px;
box-sizing: border-box;
}
input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
/* 错误状态样式 */
.error-input {
border-color: #ff4d4f !important;
}
.error-input:focus {
border-color: #ff4d4f !important;
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2) !important;
}
.error-message {
display: block;
font-size: 12px;
color: #ff4d4f;
margin-top: 2px;
line-height: 1.2;
}
/* 错误提示区域样式 */
.error-message-container {
padding: 12px 16px;
background-color: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 2px;
margin-bottom: 16px;
}
.error-text {
color: #ff4d4f;
font-size: 14px;
}
/* 加载状态样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
/* 确保页面容器相对定位,使加载遮罩正常工作 */
.lis-group-maintain {
position: relative;
}
.table-container {
overflow-x: auto;
border: 1px solid #e8e8e8;
border-radius: 4px;
margin-bottom: 16px;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 12px 8px;
text-align: center;
border-bottom: 1px solid #e8e8e8;
}
.data-table th {
background-color: #fafafa;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
/* 响应式设计 */
@media (max-width: 768px) {
.lis-group-maintain {
padding: 10px;
}
.header h1 {
font-size: 16px;
}
.table-container {
border-radius: 0;
}
.data-table {
font-size: 12px;
border-collapse: collapse;
min-width: 768px; /* 确保表格在小屏幕上可以横向滚动 */
}
.data-table th,
.data-table td {
padding: 8px;
text-align: center;
}
.btn {
width: 20px;
height: 20px;
font-size: 11px;
margin-right: 2px;
}
.pagination-btn {
width: 28px;
height: 28px;
font-size: 12px;
margin: 0 2px;
}
}
/* 平滑过渡效果 */
.btn,
.pagination-btn,
.editing-row,
tr {
transition: all 0.3s ease;
}
/* 删除行渐隐效果 */
.deleting-row {
opacity: 0;
height: 0;
overflow: hidden;
transition: all 0.3s ease;
}
/* 美化输入框和选择框的焦点样式 */
input:focus,
select:focus {
outline: none;
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
</style>