Files
his/openhis-ui-vue3/src/views/maintainSystem/Inspection/index.vue
2025-11-27 08:56:19 +08:00

808 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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="inspection-management">
<!-- 左侧导航 -->
<div class="side-nav">
<div
v-for="(item, index) in navItems"
:key="index"
:class="['nav-item', { active: activeNavIndex === index }]"
@click="activeNavIndex = index"
>
{{ item }}
</div>
</div>
<!-- 右侧主内容 -->
<div class="main-content">
<div class="header-actions">
<button class="add-new-btn" @click="addNewRow">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
新增
</button>
<button class="test-btn" @click="testComponentFunctionality">
测试组件
</button>
<!-- 错误提示区域 -->
<div v-if="hasError" class="error-message">
{{ errorMessage }}
</div>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th></th>
<th>*大类编码</th>
<th>*大类项目名称</th>
<th>*执行科室</th>
<th>序号</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in tableData" :key="rowIndex">
<td>{{ row.rowNum }}</td>
<!-- 大类编码 -->
<td>
<template v-if="editingIndex === rowIndex">
<input
type="text"
v-model="row.code"
class="editable-input"
placeholder="请输入大类编码"
>
</template>
<span v-else>{{ row.code || '-' }}</span>
</td>
<!-- 大类项目名称 -->
<td>
<template v-if="editingIndex === rowIndex">
<input
type="text"
v-model="row.name"
class="editable-input"
placeholder="请输入大类项目名称"
>
</template>
<span v-else>{{ row.name || '-' }}</span>
</td>
<!-- 执行科室 -->
<td>
<template v-if="editingIndex === rowIndex">
<select
v-model="row.department"
class="editable-select"
>
<option value="">请选择科室</option>
<option v-for="dept in departments" :key="dept" :value="dept">
{{ dept }}
</option>
</select>
</template>
<span v-else>{{ row.department || '-' }}</span>
</td>
<!-- 序号 -->
<td>
<template v-if="editingIndex === rowIndex">
<input
type="text"
v-model="row.orderNum"
class="editable-input"
placeholder="请输入序号"
>
</template>
<span v-else>{{ row.orderNum || '-' }}</span>
</td>
<!-- 备注 -->
<td>
<template v-if="editingIndex === rowIndex">
<input
type="text"
v-model="row.remark"
class="editable-input"
placeholder="请输入备注"
>
</template>
<span v-else>{{ row.remark || '-' }}</span>
</td>
<!-- 操作 -->
<td class="action-cell">
<template v-if="editingIndex === rowIndex">
<div class="action-btn confirm-btn" @click="saveRow" title="保存"></div>
<div class="action-btn delete-btn" @click="cancelEdit" title="取消"></div>
</template>
<template v-else>
<div
class="action-btn confirm-btn"
@click="toggleConfirm(rowIndex)"
:class="{ 'confirmed': row.isConfirmed }"
:title="row.isConfirmed ? '取消确认' : '确认'"
></div>
<div class="action-btn edit-btn" @click="editRow(rowIndex)" title="编辑"></div>
<div class="action-btn add-btn" @click="addChildRow(rowIndex)" title="添加子项"></div>
<div class="action-btn delete-btn" @click="deleteRow(rowIndex)" title="删除"></div>
</template>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 页码区域 -->
<div class="pagination">
<div class="page-btn" @click="changePage(currentPage - 1)" :disabled="currentPage === 1">上一页</div>
<div
v-for="page in totalPages"
:key="page"
class="page-btn"
:class="{ active: currentPage === page }"
@click="changePage(page)"
>
{{ page }}
</div>
<div class="page-btn" @click="changePage(currentPage + 1)" :disabled="currentPage === totalPages">下一页</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
// 组件名称
const name = 'InspectionManagement'
// 加载状态
const isLoading = ref(false)
const hasError = ref(false)
const errorMessage = ref('')
// 全局消息提示函数
const showMessage = (message, type = 'info') => {
// 在实际项目中这里可以使用Element Plus等UI库的消息组件
// 这里使用简单的实现
const messageContainer = document.createElement('div')
messageContainer.className = `message-toast message-${type}`
messageContainer.textContent = message
// 样式
Object.assign(messageContainer.style, {
position: 'fixed',
top: '20px',
right: '20px',
padding: '12px 20px',
borderRadius: '4px',
color: '#fff',
fontSize: '14px',
zIndex: '9999',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
transition: 'all 0.3s ease',
opacity: '0',
transform: 'translateX(100%)'
})
// 根据类型设置背景色
const bgColors = {
success: '#52c41a',
warning: '#faad14',
error: '#ff4d4f',
info: '#1890ff'
}
messageContainer.style.backgroundColor = bgColors[type] || bgColors.info
document.body.appendChild(messageContainer)
// 显示动画
setTimeout(() => {
messageContainer.style.opacity = '1'
messageContainer.style.transform = 'translateX(0)'
}, 10)
// 自动关闭
setTimeout(() => {
messageContainer.style.opacity = '0'
messageContainer.style.transform = 'translateX(100%)'
setTimeout(() => {
document.body.removeChild(messageContainer)
}, 300)
}, 3000)
}
// 响应式数据
const activeNavIndex = ref(0)
const navItems = ref(['检验类型', '检验项目', '套餐设置'])
const departments = ref(['医学检验科', '病理科', '放射科', '超声科', '心电图室'])
const editingIndex = ref(-1)
const currentPage = ref(1)
const totalPages = ref(3)
// 表格数据
const tableData = reactive([
{ rowNum: 1, code: '10', name: '临检(镜检)', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false },
{ rowNum: 1, code: '08', name: '临检(尿液)', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false },
{ rowNum: 2, code: '06', name: '免疫组化', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false },
{ rowNum: 3, code: '04', name: '免疫', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false },
{ rowNum: 4, code: '02', name: '常规', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false },
{ rowNum: 5, code: '09', name: '病理', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false },
{ rowNum: 6, code: '07', name: '临检(血液)', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false },
{ rowNum: 7, code: '05', name: '外送', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false },
{ rowNum: 8, code: '03', name: '妇科', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false },
{ rowNum: 9, code: '01', name: '生化', department: '医学检验科', orderNum: '999999', remark: '', isConfirmed: false }
])
// 编辑行
const editRow = (index) => {
editingIndex.value = index
}
// 保存行
const saveRow = () => {
// 获取正在编辑的行数据
const currentRow = tableData[editingIndex.value]
if (currentRow) {
// 确保必填字段不为空
if (!currentRow.code) {
showMessage('请输入大类编码', 'warning')
return
}
if (!currentRow.name) {
showMessage('请输入大类项目名称', 'warning')
return
}
if (!currentRow.department) {
showMessage('请选择执行科室', 'warning')
return
}
// 重置序号为数字格式
if (currentRow.orderNum && !/^\d+$/.test(currentRow.orderNum)) {
showMessage('序号必须为数字', 'warning')
return
}
// 检查编码唯一性
const duplicateIndex = tableData.findIndex((row, idx) =>
row.code === currentRow.code && idx !== editingIndex.value
)
if (duplicateIndex !== -1) {
showMessage('编码已存在,请使用其他编码', 'error')
return
}
// 修复updateRowNumbers函数的逻辑错误
updateRowNumbers()
// 保存成功提示
showMessage('保存成功', 'success')
}
// 重置编辑状态
editingIndex.value = -1
}
// 取消编辑
const cancelEdit = () => {
editingIndex.value = -1
}
// 切换确认状态
const toggleConfirm = (index) => {
tableData[index].isConfirmed = !tableData[index].isConfirmed
// 根据确认状态显示不同提示
showMessage(
tableData[index].isConfirmed ? '确认成功' : '已取消确认',
'info'
)
}
// 删除行
const deleteRow = (index) => {
if (confirm('确定要删除这条记录吗?删除后无法恢复。')) {
// 记录要删除行的行号
const rowNumToDelete = tableData[index].rowNum
// 删除该行
tableData.splice(index, 1)
// 重新计算页码(简单实现,实际项目中可能需要更复杂的逻辑)
const itemsPerPage = 10
totalPages.value = Math.ceil(tableData.length / itemsPerPage)
if (currentPage.value > totalPages.value && currentPage.value > 1) {
currentPage.value = totalPages.value
}
// 更新行号
updateRowNumbers()
// 删除成功提示
showMessage('删除成功', 'success')
}
}
// 添加新行
const addNewRow = () => {
// 计算新行的行号
let maxRowNum = 0
tableData.forEach(row => {
if (row.rowNum > maxRowNum) {
maxRowNum = row.rowNum
}
})
const newRow = {
rowNum: maxRowNum + 1,
code: '',
name: '',
department: '医学检验科',
orderNum: '',
remark: '',
isConfirmed: false
}
tableData.push(newRow)
// 重新计算页码
const itemsPerPage = 10
totalPages.value = Math.ceil(tableData.length / itemsPerPage)
currentPage.value = totalPages.value // 自动跳转到最后一页
// 添加成功提示
showMessage('新增记录成功', 'success')
editRow(tableData.length - 1)
}
// 添加子行
const addChildRow = (index) => {
// 查找当前行组的最大子行号
let maxChildNum = 0
const baseRowNum = tableData[index].rowNum
tableData.forEach(row => {
if (row.rowNum === baseRowNum && row !== tableData[index]) {
maxChildNum++
}
})
const newChildRow = {
rowNum: baseRowNum,
code: '',
name: '',
department: '医学检验科',
orderNum: '',
remark: '',
isConfirmed: false
}
// 在当前行后面插入子行
tableData.splice(index + 1, 0, newChildRow)
// 添加子行成功提示
showMessage('添加子项成功', 'success')
editRow(index + 1)
}
// 更新行号
const updateRowNumbers = () => {
try {
// 按行号分组
const rowGroups = {}
tableData.forEach(row => {
if (!rowGroups[row.rowNum]) {
rowGroups[row.rowNum] = []
}
rowGroups[row.rowNum].push(row)
})
// 重新分配行号,保持原有组的完整性
const sortedRowNums = Object.keys(rowGroups).map(num => parseInt(num)).sort((a, b) => a - b)
const newTableData = []
sortedRowNums.forEach((_, groupIndex) => {
const rows = rowGroups[sortedRowNums[groupIndex]]
rows.forEach(row => {
row.rowNum = groupIndex + 1 // 从1开始重新编号
newTableData.push(row)
})
})
// 清空并重新填充数据,保持响应性
tableData.splice(0, tableData.length, ...newTableData)
} catch (error) {
console.error('更新行号时出错:', error)
showMessage('行号更新失败', 'error')
}
}
// 组件初始化和测试
onMounted(async () => {
try {
isLoading.value = true
hasError.value = false
// 模拟异步数据加载
await new Promise(resolve => setTimeout(resolve, 500))
// 初始化测试 - 验证数据结构
if (!Array.isArray(tableData)) {
throw new Error('表格数据结构错误')
}
// 初始化成功提示
showMessage('组件初始化成功', 'info')
} catch (error) {
console.error('组件初始化失败:', error)
hasError.value = true
errorMessage.value = error.message || '组件初始化失败'
showMessage('组件加载失败,请刷新页面重试', 'error')
} finally {
isLoading.value = false
}
})
// 监听表格数据变化
watch(() => tableData.length, (newLength, oldLength) => {
console.log(`表格数据变化: ${oldLength} -> ${newLength}`)
})
// 验证组件功能的测试函数
const testComponentFunctionality = () => {
const tests = []
// 测试数据结构
tests.push({
name: '数据结构验证',
passed: Array.isArray(tableData) && tableData.every(row =>
typeof row === 'object' && row !== null
)
})
// 测试编辑状态管理
tests.push({
name: '编辑状态管理',
passed: typeof editingIndex.value === 'number'
})
// 测试分页功能
tests.push({
name: '分页功能',
passed: typeof currentPage.value === 'number' &&
typeof totalPages.value === 'number' &&
currentPage.value >= 1 &&
totalPages.value >= 1
})
// 输出测试结果
const passedTests = tests.filter(test => test.passed)
console.log('组件功能测试结果:', {
total: tests.length,
passed: passedTests.length,
failed: tests.length - passedTests.length,
details: tests
})
// 显示测试结果提示
if (passedTests.length === tests.length) {
showMessage('组件功能测试通过', 'success')
} else {
showMessage(`组件功能测试失败: ${tests.length - passedTests.length} 项测试未通过`, 'error')
}
return tests
}
// 更改页码
const changePage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
</script>
<style scoped>
/* 头部操作区样式 */
.header-actions {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 16px;
gap: 10px;
}
.add-new-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background-color: rgba(24, 144, 255, 0.1);
color: #1890FF;
border: 1px solid rgba(24, 144, 255, 0.3);
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.add-new-btn:hover {
background-color: rgba(24, 144, 255, 0.2);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
}
/* 测试按钮样式 */
.test-btn {
padding: 8px 16px;
background-color: #722ed1;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.test-btn:hover {
background-color: #9254de;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(114, 46, 209, 0.2);
}
/* 错误信息样式 */
.error-message {
color: #ff4d4f;
font-size: 14px;
margin-left: 10px;
padding: 8px 12px;
background-color: rgba(255, 77, 79, 0.1);
border-radius: 4px;
border: 1px solid rgba(255, 77, 79, 0.2);
}
/* 编辑按钮样式 */
.edit-btn {
background: rgba(250, 173, 20, 0.1);
color: #FAAD14;
}
/* 编辑状态输入框样式 */
.editable-input {
width: 100%;
border: 1px solid #E8E8E8;
border-radius: 4px;
padding: 4px 8px;
font-size: 14px;
color: #333;
background-color: #fff;
transition: all 0.2s ease;
}
.editable-input:focus {
outline: none;
border-color: #1890FF;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* 编辑状态选择框样式 */
.editable-select {
width: 100%;
border: 1px solid #E8E8E8;
border-radius: 4px;
padding: 4px 8px;
font-size: 14px;
color: #333;
background-color: #fff;
transition: all 0.2s ease;
}
.editable-select:focus {
outline: none;
border-color: #1890FF;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* CSS Reset 和全局样式 */
.inspection-management {
display: flex;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #FFFFFF;
color: #000000;
line-height: 1.5;
}
/* 左侧导航菜单 */
.side-nav {
width: 160px;
background: #FFFFFF;
border-right: 1px solid #e8e8e8;
padding: 16px 0;
height: 100vh;
position: fixed;
z-index: 1;
}
.nav-item {
padding: 8px 16px;
margin: 4px 8px;
font-size: 14px;
height: 40px;
display: flex;
align-items: center;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.nav-item:hover {
background: #F5F5F5;
}
.nav-item.active {
background: #1890FF;
color: #FFFFFF;
}
/* 右侧主内容区 */
.main-content {
flex: 1;
margin-left: 160px;
padding: 16px;
}
/* 表格样式 */
.table-container {
overflow-x: auto;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
margin-top: 16px;
}
.data-table {
width: 100%;
border-collapse: collapse;
min-width: 800px;
}
.data-table th, .data-table td {
padding: 8px 16px;
text-align: left;
font-size: 14px;
height: 40px;
}
.data-table th {
background-color: #F5F5F5;
font-weight: 500;
}
.data-table td {
font-weight: 400;
border-bottom: 1px solid #E8E8E8;
}
.data-table tr:hover td {
background-color: #E6F7FF;
}
/* 操作按钮样式 */
.action-cell {
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
}
.action-btn {
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
margin: 0 2px;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.action-btn::before {
font-size: 12px;
font-weight: bold;
}
.confirm-btn {
background: rgba(82, 196, 26, 0.1);
color: #52C41A;
transition: all 0.2s ease;
}
.confirm-btn.confirmed {
background: #52C41A;
color: #FFFFFF;
box-shadow: 0 2px 4px rgba(82, 196, 26, 0.3);
}
.confirm-btn::before {
content: '✓';
}
.add-btn {
background: rgba(24, 144, 255, 0.1);
color: #1890FF;
}
.add-btn::before {
content: '+';
}
.delete-btn {
background: rgba(255, 77, 79, 0.1);
color: #FF4D4F;
}
.delete-btn::before {
content: '×';
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 页码区域 */
.pagination {
display: flex;
justify-content: center;
margin-top: 16px;
align-items: center;
}
.page-btn {
padding: 8px 16px;
margin: 0 4px;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s ease;
}
.page-btn:hover {
background: #F5F5F5;
}
.page-btn.active {
background: #1890FF;
color: white;
}
.page-btn[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.side-nav {
width: 60px;
padding: 16px 5px;
}
.nav-item span {
display: none;
}
.main-content {
margin-left: 60px;
}
}
@media (max-width: 480px) {
.side-nav {
width: 50px;
padding: 16px 5px;
}
.main-content {
margin-left: 50px;
padding: 10px;
}
}
</style>