revert: restore develop to clean baseline 5132de36 (remove all AI changes)

This commit is contained in:
2026-05-28 09:43:49 +08:00
parent bdec44d6c5
commit 913a971ce4
481 changed files with 3036 additions and 26749 deletions

View File

@@ -1,19 +0,0 @@
import request from '@/utils/request'
export function getLabRequestListApi() {
return request({
url: '/inpatient/lab-request/list',
method: 'get'
})
}
/**
* 撤回检验申请 (Bug #571)
* @param {Number} requestId
*/
export function revokeLabRequestApi(requestId) {
return request({
url: `/inpatient/lab-request/revoke/${requestId}`,
method: 'post'
})
}

View File

@@ -1,14 +0,0 @@
import request from '@/utils/request';
/**
* 获取住院体征(体温单)数据
* @param {Object} params { registrationId }
* @returns {Promise} 返回体征记录列表,前端体温图表使用
*/
export function getVitalSignsApi(params) {
return request({
url: '/inpatient/vital-signs',
method: 'get',
params,
});
}

View File

@@ -1,29 +0,0 @@
import request from '@/utils/request'
/**
* 获取患者待写病历的医嘱列表(分页)
* @param {Number|String} patientId
* @param {Number} pageNum
* @param {Number} pageSize
*/
export function getPendingOrders(patientId, pageNum = 1, pageSize = 20) {
return request({
url: '/api/orders/pending',
method: 'get',
params: { patientId, pageNum, pageSize }
})
}
/**
* 获取患者排队队列(包括完诊)列表(分页)
* @param {Number|String} patientId
* @param {Number} pageNum
* @param {Number} pageSize
*/
export function getQueueOrders(patientId, pageNum = 1, pageSize = 20) {
return request({
url: '/api/orders/queue',
method: 'get',
params: { patientId, pageNum, pageSize }
})
}

View File

@@ -1,13 +0,0 @@
import request from '@/utils/request'
/**
* 获取当前医生待写病历列表
* @returns {Promise}
*/
export function getPendingMedicalRecords() {
return request({
url: '/api/medical-record/pending',
method: 'get',
timeout: 5000 // 明确设置前端超时阈值,避免无限等待
})
}

View File

@@ -1,24 +0,0 @@
import request from '@/utils/request';
import { PageResult } from '@/types';
/**
* 获取当前排队列表(包含已完诊)。
*/
export const getCurrentQueue = (params: { pageNum: number; pageSize: number }) => {
return request<PageResult<any>>({
url: '/api/orders/queue',
method: 'get',
params,
});
};
/**
* 获取历史排队(仅已完诊)列表。
*/
export const getHistoryQueue = (params: { pageNum: number; pageSize: number }) => {
return request<PageResult<any>>({
url: '/api/orders/queue/history',
method: 'get',
params,
});
};

View File

@@ -1,13 +0,0 @@
import request from '@/utils/request';
/**
* 获取体温图表数据
* @param {Number} patientId 患者ID
* @returns {Promise} 返回 { data: [{ id, temperature, recordTime }, ...] }
*/
export function fetchTemperatureChartData(patientId) {
return request({
url: `/api/vitalSign/temperatureChart/${patientId}`,
method: 'get',
});
}

View File

@@ -1,6 +0,0 @@
import request from '@/utils/request';
import type { VitalSignDto } from '@/types/vitalSign';
export const getTemperatureChartData = (patientId: number) => {
return request.get<VitalSignDto>(`/api/vitalSign/temperatureChart/${patientId}`);
};

View File

@@ -1,6 +0,0 @@
export interface VitalSignDto {
patientId: number;
timeLabels: string[];
temperaturePoints: number[];
rawDataJson?: string;
}

View File

@@ -1,116 +0,0 @@
<template>
<div class="outpatient-daily-settlement">
<el-card class="summary-section" data-cy="settlement-summary">
<template #header>
<span class="card-title">门诊日结汇总</span>
</template>
<el-row :gutter="20">
<el-col :span="6" v-for="item in summaryData" :key="item.label">
<el-card shadow="hover" class="summary-card" data-cy="summary-card">
<div class="summary-label">{{ item.label }}</div>
<div class="summary-value">{{ item.value }}</div>
</el-card>
</el-col>
</el-row>
</el-card>
<el-card class="detail-section" style="margin-top: 20px;">
<template #header>
<div class="table-header">
<span class="card-title">收费明细</span>
<el-button type="primary" @click="handleRefresh">刷新</el-button>
</div>
</template>
<el-table
:data="tableData"
border
stripe
style="width: 100%"
data-cy="settlement-table"
>
<el-table-column prop="date" label="结算日期" width="120" align="center" />
<el-table-column prop="patientName" label="患者姓名" width="120" align="center" />
<el-table-column prop="chargeType" label="收费类型" width="120" align="center" />
<el-table-column prop="amount" label="金额(元)" width="120" align="right" />
<el-table-column prop="status" label="状态" width="100" align="center" />
<el-table-column prop="operator" label="操作员" width="120" align="center" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<div class="action-section" data-cy="settlement-actions" style="margin-top: 20px; text-align: right;">
<el-button @click="handleExport">导出报表</el-button>
<el-button type="primary" @click="handleConfirmSettlement">确认日结</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
const summaryData = ref([
{ label: '今日挂号费', value: '¥ 1,250.00' },
{ label: '今日诊疗费', value: '¥ 8,430.50' },
{ label: '今日药品费', value: '¥ 15,600.00' },
{ label: '今日总收费', value: '¥ 25,280.50' }
])
const tableData = ref([
{ date: '2026-05-26', patientName: '张三', chargeType: '门诊', amount: 150.00, status: '已结算', operator: '收费员A' },
{ date: '2026-05-26', patientName: '李四', chargeType: '门诊', amount: 320.50, status: '已结算', operator: '收费员A' },
{ date: '2026-05-26', patientName: '王五', chargeType: '急诊', amount: 89.00, status: '已结算', operator: '收费员B' }
])
const handleRefresh = () => {
ElMessage.success('数据已刷新')
}
const handleView = (row: any) => {
ElMessage.info(`查看明细: ${row.patientName}`)
}
const handleExport = () => {
ElMessage.success('报表导出中...')
}
const handleConfirmSettlement = () => {
ElMessage.success('日结确认成功')
}
onMounted(() => {
// 初始化加载逻辑
})
</script>
<style scoped>
.outpatient-daily-settlement {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.card-title {
font-weight: 600;
font-size: 16px;
}
.summary-card {
text-align: center;
padding: 10px 0;
}
.summary-label {
color: #606266;
font-size: 14px;
margin-bottom: 8px;
}
.summary-value {
color: #303133;
font-size: 20px;
font-weight: bold;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -1,118 +0,0 @@
<template>
<div class="outpatient-charge-report">
<!-- 顶部筛选区 -->
<el-card class="filter-card" shadow="never">
<el-form :model="queryParams" inline label-width="80px">
<el-form-item label="收费日期">
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="收费员">
<el-select v-model="queryParams.cashierId" placeholder="请选择" clearable style="width: 160px">
<el-option label="全部" value="" />
<el-option v-for="item in cashierOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 明细表格区 -->
<el-card class="table-card" shadow="never" style="margin-top: 16px;">
<el-table
:data="tableData"
border
stripe
style="width: 100%"
v-loading="loading"
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
>
<!-- Bug #579 修复统一配置 align width/min-width确保字段一一对应防止长文本挤压导致排版错乱 -->
<el-table-column prop="chargeDate" label="收费日期" width="120" align="center" />
<el-table-column prop="patientName" label="患者姓名" width="120" align="center" />
<el-table-column prop="medicalRecordNo" label="病历号" width="140" align="center" />
<el-table-column prop="chargeItem" label="收费项目" min-width="200" align="left" show-overflow-tooltip />
<el-table-column prop="amount" label="金额(元)" width="120" align="right" />
<el-table-column prop="payMethod" label="支付方式" width="120" align="center" />
<el-table-column prop="cashierName" label="收费员" width="120" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '已收费' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end;"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
const loading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const cashierOptions = ref<{ id: string; name: string }[]>([])
const queryParams = reactive({
dateRange: [] as string[],
cashierId: '',
pageNum: 1,
pageSize: 20
})
const handleQuery = () => {
loading.value = true
// 模拟后端请求,实际应替换为 API 调用
setTimeout(() => {
tableData.value = [
{ chargeDate: '2026-05-22', patientName: '张三', medicalRecordNo: 'MR20260522001', chargeItem: '门诊诊查费', amount: 15.00, payMethod: '医保', cashierName: '收费员A', status: '已收费' }
]
total.value = 1
loading.value = false
}, 300)
}
const handleReset = () => {
queryParams.dateRange = []
queryParams.cashierId = ''
queryParams.pageNum = 1
handleQuery()
}
onMounted(() => {
handleQuery()
})
</script>
<style scoped>
.outpatient-charge-report {
padding: 16px;
background-color: #f0f2f5;
min-height: 100vh;
}
.filter-card {
margin-bottom: 16px;
}
.table-card {
background-color: #fff;
}
</style>

View File

@@ -1,121 +0,0 @@
<template>
<div class="outpatient-daily-settlement">
<!-- 顶部筛选区 -->
<el-card class="settlement-filter-area" shadow="never">
<el-form :model="queryParams" inline label-width="80px">
<el-form-item label="结算日期">
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="收费员">
<el-select v-model="queryParams.cashierId" placeholder="请选择收费员" clearable style="width: 160px">
<el-option label="全部" value="" />
<el-option label="doctor1" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 汇总数据区 -->
<el-row :gutter="16" class="settlement-summary-cards">
<el-col :xs="24" :sm="12" :md="6" v-for="card in summaryCards" :key="card.label">
<el-card shadow="hover" class="summary-card">
<template #header>
<div class="card-header">{{ card.label }}</div>
</template>
<div class="card-value">{{ card.value }}</div>
</el-card>
</el-col>
</el-row>
<!-- 明细表格区 -->
<el-card class="settlement-detail-table" shadow="never">
<template #header>
<div class="table-header">日结明细</div>
</template>
<el-table :data="tableData" border stripe style="width: 100%">
<el-table-column prop="transactionNo" label="流水号" width="180" align="center" />
<el-table-column prop="patientName" label="患者姓名" width="120" align="center" />
<el-table-column prop="payMethod" label="支付方式" width="120" align="center" />
<el-table-column prop="amount" label="金额(元)" width="120" align="right" />
<el-table-column prop="status" label="状态" width="100" align="center" />
<el-table-column prop="createTime" label="结算时间" min-width="180" align="center" />
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const queryParams = reactive({
dateRange: [],
cashierId: ''
})
const summaryCards = ref([
{ label: '现金收入', value: '¥ 0.00' },
{ label: '医保统筹', value: '¥ 0.00' },
{ label: '个人账户', value: '¥ 0.00' },
{ label: '合计金额', value: '¥ 0.00' }
])
const tableData = ref([])
const handleQuery = () => {
console.log('查询日结数据', queryParams)
}
const handleReset = () => {
queryParams.dateRange = []
queryParams.cashierId = ''
}
</script>
<style scoped>
.outpatient-daily-settlement {
padding: 16px;
background-color: #f5f7fa;
min-height: 100vh;
box-sizing: border-box;
}
.settlement-filter-area {
margin-bottom: 16px;
}
.settlement-summary-cards {
margin-bottom: 16px;
}
.summary-card .card-header {
font-size: 14px;
color: #606266;
font-weight: 500;
margin-bottom: 8px;
}
.summary-card .card-value {
font-size: 24px;
font-weight: bold;
color: #303133;
text-align: right;
}
.settlement-detail-table .table-header {
font-size: 16px;
font-weight: 600;
color: #303133;
}
</style>

View File

@@ -1,164 +0,0 @@
<template>
<div class="examination-apply-container">
<!-- 左侧检查项目分类 -->
<div class="panel-left">
<el-tree
class="category-tree"
:data="categories"
:props="{ label: 'name', children: 'children' }"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="panel-middle">
<div class="item-list">
<div v-for="item in currentItems" :key="item.id" class="item-row" @click="handleItemSelect(item)">
<el-checkbox v-model="item.checked" @change="handleItemCheck(item)" />
<span class="item-name">{{ cleanName(item.name) }}</span>
</div>
</div>
</div>
<!-- 右侧/下方已选择区域 -->
<div class="panel-right">
<h4 class="section-title">已选择</h4>
<div class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<div class="card-header">
<el-checkbox v-model="item.checked" @change="handleItemCheck(item)" />
<el-tooltip :content="cleanName(item.name)" placement="top" :show-after="300">
<span class="item-name">{{ cleanName(item.name) }}</span>
</el-tooltip>
<el-button class="expand-btn" link @click="item.expanded = !item.expanded">
<el-icon><ArrowDown v-if="item.expanded" /><ArrowRight v-else /></el-icon>
</el-button>
</div>
<!-- 检查方法明细面板默认收起严格父子解耦 -->
<div v-show="item.expanded" class="method-detail-panel">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<el-checkbox v-model="method.checked" @change="handleMethodCheck(item, method)" />
<span class="method-name">{{ method.name }}</span>
</div>
</div>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无已选项目" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
// 模拟分类与项目数据实际应从API获取
const categories = ref([
{ id: 1, name: '彩超', children: [] },
{ id: 2, name: 'CT', children: [] }
]);
const currentItems = ref([]);
const selectedItems = ref([]);
// 清理冗余前缀(如“套餐:”、“项目套餐”等)
const cleanName = (name) => {
if (!name) return '';
return name.replace(/^(套餐|项目套餐)[:]/g, '').trim();
};
// 切换分类加载项目
const handleCategoryClick = (data) => {
// 实际场景调用API获取该分类下的项目列表
currentItems.value = [
{ id: '101', name: '套餐128线排', checked: false, methods: [
{ id: 'm1', name: '常规扫描', checked: false },
{ id: 'm2', name: '增强扫描', checked: false }
]}
];
};
// 中间列表勾选 -> 仅加入已选列表,不联动方法
const handleItemSelect = (item) => {
if (!selectedItems.value.find(s => s.id === item.id)) {
selectedItems.value.push({ ...item, expanded: false });
}
};
// 核心解耦逻辑:项目勾选独立
const handleItemCheck = (item) => {
// 仅更新当前项目状态,绝不自动勾选/取消子方法
const target = selectedItems.value.find(s => s.id === item.id);
if (target) target.checked = item.checked;
};
// 核心解耦逻辑:方法勾选独立
const handleMethodCheck = (item, method) => {
// 仅更新当前方法状态,绝不反向影响父项目
const target = selectedItems.value.find(s => s.id === item.id);
if (target) {
const m = target.methods.find(m => m.id === method.id);
if (m) m.checked = method.checked;
}
};
</script>
<style scoped>
.examination-apply-container {
display: flex;
gap: 16px;
height: 100%;
padding: 16px;
background: #f5f7fa;
}
.panel-left, .panel-middle, .panel-right {
background: #fff;
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.panel-left { width: 200px; }
.panel-middle { flex: 1; }
.panel-right { width: 320px; display: flex; flex-direction: column; }
.section-title { margin: 0 0 12px; font-size: 14px; color: #303133; }
.item-list { display: flex; flex-direction: column; gap: 8px; }
.item-row {
display: flex; align-items: center; gap: 8px; padding: 8px;
cursor: pointer; border-radius: 4px; transition: background 0.2s;
}
.item-row:hover { background: #f0f2f5; }
.selected-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
.selected-card {
border: 1px solid #ebeef5; border-radius: 6px; padding: 10px;
background: #fafafa; width: 100%; min-width: 250px;
}
.card-header {
display: flex; align-items: center; gap: 8px; width: 100%;
}
.item-name {
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-size: 13px; color: #606266; cursor: default;
}
.expand-btn { padding: 0; min-width: 24px; }
.method-detail-panel {
margin-top: 8px; padding-left: 20px; border-top: 1px dashed #dcdfe6;
padding-top: 8px;
}
.method-item {
display: flex; align-items: center; gap: 8px; padding: 4px 0;
font-size: 12px; color: #909399;
}
</style>

View File

@@ -1,110 +0,0 @@
<template>
<div class="pending-medical-record-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>待写病历</span>
<el-button type="primary" @click="handleRefresh" :loading="loading">刷新</el-button>
</div>
</template>
<!-- 修复使用 v-loading 指令确保状态受控 -->
<el-table
v-loading="loading"
:data="tableData"
class="pending-record-table"
style="width: 100%"
@row-click="handleRowClick"
>
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="visitDate" label="就诊日期" width="150" />
<el-table-column prop="diagnosis" label="初步诊断" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag type="warning">{{ row.status === 'PENDING' ? '待写' : row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click.stop="handleWrite(row)">书写</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="total, prev, pager, next"
@current-change="fetchData"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { getPendingMedicalRecords } from '@/api/clinic/medicalRecord';
import { ElMessage } from 'element-plus';
const router = useRouter();
const loading = ref(false);
const tableData = ref([]);
const total = ref(0);
const queryParams = reactive({
pageNum: 1,
pageSize: 20
});
// 修复:使用 try-finally 确保 loading 状态在任何情况下都能正确重置
const fetchData = async () => {
loading.value = true;
try {
const res = await getPendingMedicalRecords(queryParams);
tableData.value = res.data.list || [];
total.value = res.data.total || 0;
} catch (error) {
console.error('加载待写病历失败:', error);
ElMessage.error('数据加载失败,请重试');
tableData.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
const handleRefresh = () => {
queryParams.pageNum = 1;
fetchData();
};
const handleRowClick = (row) => {
handleWrite(row);
};
const handleWrite = (row) => {
router.push({ path: '/clinic/outpatient/medicalrecord/write', query: { id: row.id } });
};
onMounted(fetchData);
</script>
<style scoped>
.pending-medical-record-container {
padding: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -1,193 +0,0 @@
<template>
<div class="examination-request-wrapper">
<!-- 左侧检查项目分类 -->
<div class="panel category-panel">
<div class="panel-title">检查项目分类</div>
<el-tree
:data="categoryTree"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="panel item-panel">
<div class="panel-title">检查项目</div>
<el-checkbox-group v-model="selectedItemIds" @change="onItemSelectChange">
<el-checkbox
v-for="item in currentItems"
:key="item.id"
:label="item.id"
class="item-checkbox"
>
{{ formatItemName(item.name) }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 右侧已选择区域 -->
<div class="panel selected-panel">
<div class="panel-title">已选择</div>
<div class="selected-list">
<div v-for="group in selectedGroups" :key="group.itemId" class="selected-group">
<!-- 父级检查项目支持点击展开/收起 -->
<div class="group-header" @click="toggleExpand(group.itemId)">
<el-icon class="expand-icon">
<ArrowRight v-if="!group.expanded" />
<ArrowDown v-else />
</el-icon>
<el-tooltip :content="group.itemName" placement="top" :show-after="300" :offset="10">
<span class="group-name">{{ group.itemName }}</span>
</el-tooltip>
</div>
<!-- 子级检查方法默认收起独立勾选 -->
<div v-show="group.expanded" class="method-container">
<el-checkbox-group v-model="group.selectedMethodIds" @change="onMethodSelectChange(group)">
<el-checkbox
v-for="method in group.methods"
:key="method.id"
:label="method.id"
>
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<el-empty v-if="selectedGroups.length === 0" description="暂无已选项目" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ArrowRight, ArrowDown } from '@element-plus/icons-vue'
// 状态定义
const categoryTree = ref([])
const currentItems = ref([])
const selectedItemIds = ref([])
const selectedGroups = ref([])
// 格式化名称:清理冗余“套餐”字样
const formatItemName = (name) => {
return name ? name.replace(/套餐/g, '').trim() : ''
}
// 分类点击:加载对应项目
const handleCategoryClick = (data) => {
currentItems.value = data.items || []
}
// 项目勾选变更核心修复1解耦联动不自动勾选检查方法
const onItemSelectChange = (ids) => {
// 仅同步已选项目结构,保留原有方法勾选状态或初始化为空
const newGroups = ids.map(id => {
const existing = selectedGroups.value.find(g => g.itemId === id)
if (existing) return existing
const item = currentItems.value.find(i => i.id === id)
return {
itemId: id,
itemName: formatItemName(item?.name || ''),
methods: item?.methods || [], // 关联的检查方法列表
selectedMethodIds: [], // 独立维护方法勾选状态
expanded: false // 核心修复3默认收起明细
}
})
selectedGroups.value = newGroups
}
// 方法勾选变更核心修复1独立处理不影响父级或其他组
const onMethodSelectChange = (group) => {
// 仅触发当前组方法状态更新,业务层可在此处同步至后端或计算费用
console.log(`[Examination] 方法独立勾选: ${group.itemId} -> ${group.selectedMethodIds}`)
}
// 展开/收起控制核心修复3支持手动切换明细显示
const toggleExpand = (itemId) => {
const group = selectedGroups.value.find(g => g.itemId === itemId)
if (group) {
group.expanded = !group.expanded
}
}
</script>
<style scoped>
.examination-request-wrapper {
display: flex;
gap: 12px;
height: 100%;
padding: 12px;
background: #fff;
}
.panel {
flex: 1;
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-title {
font-weight: 600;
margin-bottom: 12px;
color: #303133;
font-size: 14px;
}
.category-panel, .item-panel {
overflow-y: auto;
}
.selected-panel {
background: #fafbfc;
}
.selected-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.selected-group {
border: 1px solid #e4e7ed;
border-radius: 6px;
background: #fff;
overflow: hidden;
}
.group-header {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
background: #fff;
border-bottom: 1px solid #ebeef5;
transition: background 0.2s;
}
.group-header:hover {
background: #f5f7fa;
}
.expand-icon {
margin-right: 8px;
color: #909399;
font-size: 14px;
}
.group-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: #303133;
}
.method-container {
padding: 10px 12px 10px 28px;
background: #fafbfc;
}
.item-checkbox {
margin-bottom: 8px;
width: 100%;
}
</style>

View File

@@ -1,142 +0,0 @@
<template>
<div class="pending-medical-record-container">
<el-card class="filter-card" shadow="never">
<el-form :model="queryParams" inline label-width="80px">
<el-form-item label="患者姓名">
<el-input v-model="queryParams.patientName" placeholder="请输入患者姓名" clearable style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="record-list" v-loading="loading">
<el-empty v-if="recordList.length === 0" description="暂无待写病历" />
<el-card v-for="item in recordList" :key="item.id" class="record-card" shadow="hover">
<div class="record-header">
<span class="patient-name">{{ item.patientName }}</span>
<el-tag size="small" type="info">{{ item.gender }}</el-tag>
<span class="age">{{ item.age }}</span>
</div>
<div class="record-info">
<span>病历号: {{ item.medicalRecordNo }}</span>
<span>就诊时间: {{ item.visitTime }}</span>
<span>诊断: {{ item.diagnosis || '未填写' }}</span>
</div>
<!-- Bug #590 修复原布局依赖默认块级流或浮动导致查看患者写病历在不同分辨率下换行错乱
现改为 flex 布局强制同行对齐使用 justify-content: flex-end 保持右侧对齐gap 统一间距 -->
<div class="record-action-bar" style="display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 16px; padding-top: 12px; border-top: 1px solid #ebeef5;">
<el-button type="primary" plain size="default" data-cy="view-patient" @click="handleViewPatient(item)">查看患者</el-button>
<el-button type="primary" size="default" data-cy="write-record" @click="handleWriteRecord(item)">写病历</el-button>
</div>
</el-card>
</div>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@current-change="handleQuery"
@size-change="handleQuery"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const loading = ref(false)
const total = ref(0)
const recordList = ref<any[]>([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
patientName: ''
})
const handleQuery = () => {
loading.value = true
// 模拟后端请求
setTimeout(() => {
recordList.value = [
{ id: 1, patientName: '张三', gender: '男', age: 35, medicalRecordNo: 'MR20260526001', visitTime: '2026-05-26 09:30', diagnosis: '上呼吸道感染' }
]
total.value = 1
loading.value = false
}, 300)
}
const handleReset = () => {
queryParams.patientName = ''
queryParams.pageNum = 1
handleQuery()
}
const handleViewPatient = (row: any) => {
console.log('查看患者详情', row)
// router.push({ path: '/patient/detail', query: { id: row.id } })
}
const handleWriteRecord = (row: any) => {
console.log('进入写病历', row)
// router.push({ path: '/emr/write', query: { encounterId: row.id } })
}
onMounted(() => {
handleQuery()
})
</script>
<style scoped>
.pending-medical-record-container {
padding: 16px;
background-color: #f5f7fa;
min-height: 100vh;
}
.filter-card {
margin-bottom: 16px;
}
.record-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.record-card {
transition: all 0.3s;
}
.record-card:hover {
transform: translateY(-2px);
}
.record-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.patient-name {
font-size: 16px;
font-weight: bold;
color: #303133;
}
.record-info {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 14px;
color: #606266;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>

View File

@@ -1,105 +0,0 @@
<template>
<div class="pending-medical-record-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>待写病历</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateChange"
style="margin-left: auto;"
/>
</div>
</template>
<el-table
:data="tableData"
border
style="width: 100%"
v-loading="loading"
class="medical-record-table"
>
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="gender" label="性别" width="80" />
<el-table-column prop="age" label="年龄" width="80" />
<el-table-column prop="visitDate" label="就诊日期" width="120" />
<el-table-column prop="deptName" label="科室" width="120" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button link type="primary" @click="handleWrite(row)">书写病历</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="total, prev, pager, next"
@current-change="fetchData"
class="pagination"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getPendingMedicalRecordsApi } from '@/api/doctorstation/medicalRecord'
import { useUserStore } from '@/store/modules/user'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const dateRange = ref([])
const queryParams = reactive({
pageNum: 1,
pageSize: 20,
doctorId: userStore.userId,
startDate: '',
endDate: ''
})
const fetchData = async () => {
loading.value = true
try {
const res = await getPendingMedicalRecordsApi(queryParams)
tableData.value = res.data.list
total.value = res.data.total
} catch (error) {
console.error('加载待写病历失败:', error)
} finally {
loading.value = false
}
}
const handleDateChange = (val) => {
queryParams.startDate = val ? val[0] : ''
queryParams.endDate = val ? val[1] : ''
queryParams.pageNum = 1
fetchData()
}
const handleWrite = (row) => {
router.push({ path: '/doctorstation/write-medical-record', query: { encounterId: row.encounterId } })
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.pending-medical-record-container { padding: 20px; }
.card-header { display: flex; align-items: center; justify-content: space-between; }
.pagination { margin-top: 20px; display: flex; justify-content: flex-end; }
</style>

View File

@@ -1,115 +0,0 @@
<template>
<el-dialog v-model="visible" title="检验申请单" width="850px" class="inspection-apply-modal" destroy-on-close>
<div class="apply-container">
<div class="left-panel">
<h3>未选择</h3>
<ul class="item-list left-list">
<li v-for="item in unselectedItems" :key="item.id" class="item-row" @click="addToSelected(item)">
<span class="item-name">{{ item.name }}</span>
<!-- 修复 Bug #577使用后端返回的 unitName 替代 unitId -->
<span class="price-unit">{{ item.price.toFixed(2) }}/{{ item.unitName || '未知' }}</span>
</li>
</ul>
</div>
<div class="right-panel">
<h3>已选择</h3>
<ul class="item-list right-list">
<li v-for="item in selectedItems" :key="item.id" class="item-row">
<span class="item-name">{{ item.name }}</span>
<!-- 修复 Bug #577使用后端返回的 unitName 替代 unitId -->
<span class="price-unit">{{ item.price.toFixed(2) }}/{{ item.unitName || '未知' }}</span>
<el-button type="danger" size="small" @click="removeFromSelected(item)">移除</el-button>
</li>
</ul>
</div>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submitRequest" :disabled="selectedItems.length === 0">提交申请</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { getLabCatalogItems, submitLabRequest } from '@/api/inpatient';
import { ElMessage } from 'element-plus';
const props = defineProps({
editData: { type: Object, default: null }
});
const emit = defineEmits(['submit']);
const visible = ref(false);
const unselectedItems = ref([]);
const selectedItems = ref([]);
// 修复 Bug #576监听弹窗显示状态与编辑数据确保回显逻辑在正确时机触发
watch([visible, () => props.editData], async ([isVisible, data]) => {
if (isVisible) {
await fetchItems(data);
}
});
const fetchItems = async (editData = null) => {
try {
const res = await getLabCatalogItems();
const allItems = res.data || [];
// 初始化状态
selectedItems.value = [];
unselectedItems.value = [...allItems];
// 修复 Bug #576编辑模式下准确回显已选项目
if (editData && Array.isArray(editData.items) && editData.items.length > 0) {
// 统一转换为 String 类型比对,避免后端返回 Number 而前端为 String 导致匹配失败
const existingIds = new Set(editData.items.map(i => String(i.id)));
selectedItems.value = editData.items.map(item => ({
...item,
id: String(item.id)
}));
unselectedItems.value = allItems.filter(item => !existingIds.has(String(item.id)));
}
} catch (error) {
console.error('获取检验目录失败:', error);
ElMessage.error('加载检验项目失败');
}
};
const addToSelected = (item) => {
selectedItems.value.push(item);
unselectedItems.value = unselectedItems.value.filter(i => i.id !== item.id);
};
const removeFromSelected = (item) => {
unselectedItems.value.push(item);
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id);
};
const submitRequest = async () => {
try {
await submitLabRequest({
...props.editData,
items: selectedItems.value
});
ElMessage.success('提交成功');
visible.value = false;
emit('submit');
} catch (error) {
ElMessage.error('提交失败');
}
};
// 暴露 open 方法供父组件直接调用
defineExpose({
open: (data) => {
visible.value = true;
if (data) {
fetchItems(data);
}
}
});
</script>

View File

@@ -1,87 +0,0 @@
<template>
<div class="lab-request-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">住院医生工作站 - 检验申请</span>
</div>
</template>
<el-table :data="requestList" border style="width: 100%" v-loading="loading" row-key="id">
<el-table-column prop="id" label="申请单号" width="120" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)">
{{ statusMap[row.status] || row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="申请时间" width="160" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.status === 'SIGNED'"
type="warning"
size="small"
@click="handleRevoke(row)"
>撤回</el-button>
<el-button v-else type="info" size="small" disabled>撤回</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getLabRequestList, revokeLabRequest } from '@/api/inpatient/labRequest'
const loading = ref(false)
const requestList = ref([])
const statusMap = { PENDING_SIGN: '待签发', SIGNED: '已签发', CANCELLED: '已撤回' }
const statusTagType = (status) => {
const map = { PENDING_SIGN: 'info', SIGNED: 'success', CANCELLED: 'danger' }
return map[status] || 'info'
}
const loadData = async () => {
loading.value = true
try {
const res = await getLabRequestList()
requestList.value = res.data || []
} finally {
loading.value = false
}
}
const handleRevoke = async (row) => {
try {
await ElMessageBox.confirm('确认撤回该检验申请吗?撤回后将恢复至待签发状态。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await revokeLabRequest(row.id)
ElMessage.success('撤回成功')
loadData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(error.response?.data?.message || '撤回失败,请稍后重试')
}
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.lab-request-container { padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.title { font-size: 18px; font-weight: bold; }
</style>

View File

@@ -1,48 +0,0 @@
<template>
<div class="order-list-container">
<el-table :data="orderList" border v-loading="loading">
<el-table-column prop="orderNo" label="医嘱号" width="120" />
<el-table-column prop="itemName" label="药品名称" />
<el-table-column prop="statusName" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">{{ row.statusName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="开立时间" width="180" />
</el-table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getOrderStatusName } from '@/utils/orderStatusMapper'
import request from '@/utils/request'
const props = defineProps({
node: { type: String, default: 'nurse' } // nurse | pharmacy
})
const orderList = ref([])
const loading = ref(false)
const fetchOrders = async () => {
loading.value = true
try {
const res = await request.get('/api/inpatient/orders', { params: { node: props.node } })
// 修复 Bug #569前端统一使用映射工具转换状态名称杜绝硬编码
orderList.value = res.data.map(item => ({
...item,
statusName: getOrderStatusName(item.status, props.node)
}))
} finally {
loading.value = false
}
}
const getStatusTagType = (code) => {
const map = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'warning', 6: 'success', 7: 'success' }
return map[code] || 'info'
}
onMounted(fetchOrders)
</script>

View File

@@ -1,141 +0,0 @@
<template>
<div class="order-verification-container">
<el-tabs v-model="activeTab" @tab-click="handleTabChange">
<el-tab-pane label="待校对" name="pending"></el-tab-pane>
<el-tab-pane label="已校对" name="verified"></el-tab-pane>
<el-tab-pane label="已退回" name="returned"></el-tab-pane>
</el-tabs>
<div class="toolbar" style="margin: 16px 0; display: flex; justify-content: flex-end;">
<el-button type="primary" :disabled="selectedRows.length === 0 || isBatchReturnDisabled" @click="handleBatchReturn">
批量退回
</el-button>
</div>
<el-table
:data="tableData"
border
stripe
v-loading="loading"
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="drugName" label="药品名称" min-width="150" />
<el-table-column prop="execStatus" label="执行状态" width="100">
<template #default="{ row }">
<el-tag :type="row.execStatus === '已执行' ? 'success' : 'info'">{{ row.execStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="dispenseStatus" label="发药状态" width="100">
<template #default="{ row }">
<el-tag :type="row.dispenseStatus === '已发药' ? 'warning' : 'info'">{{ row.dispenseStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="billingStatus" label="计费状态" width="100" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
link
:disabled="isReturnDisabled(row)"
@click="handleReturn(row)"
>
退回
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { returnOrderApi, fetchOrderListApi } from '@/api/inpatient/order'
const activeTab = ref('verified')
const loading = ref(false)
const tableData = ref([])
const selectedRows = ref([])
// 核心修复:单行退回按钮置灰逻辑
const isReturnDisabled = (row) => {
return row.dispenseStatus === '已发药' ||
row.execStatus === '已执行' ||
row.billingStatus === '已计费'
}
// 批量操作置灰逻辑
const isBatchReturnDisabled = computed(() => {
return selectedRows.value.some(row => isReturnDisabled(row))
})
const handleSelectionChange = (rows) => {
selectedRows.value = rows
}
const handleTabChange = () => {
fetchTableData()
}
const fetchTableData = async () => {
loading.value = true
try {
const res = await fetchOrderListApi({ status: activeTab.value })
tableData.value = res.data || []
} catch (error) {
ElMessage.error('加载医嘱列表失败')
} finally {
loading.value = false
}
}
// 核心修复:前置拦截与提示
const handleReturn = async (row) => {
if (row.dispenseStatus === '已发药') {
ElMessage.warning('该药品已由药房发放,请先执行退药处理,不可直接退回')
return
}
if (row.execStatus === '已执行') {
ElMessage.warning('该医嘱已执行,请先在【医嘱执行】模块取消执行')
return
}
if (row.billingStatus === '已计费') {
ElMessage.warning('该医嘱已产生费用,请先完成退费流程')
return
}
try {
await ElMessageBox.confirm('确认退回该医嘱?', '提示', { type: 'warning' })
loading.value = true
await returnOrderApi(row.id)
ElMessage.success('退回成功')
fetchTableData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(error.message || '退回失败')
}
} finally {
loading.value = false
}
}
const handleBatchReturn = async () => {
if (isBatchReturnDisabled.value) {
ElMessage.warning('选中列表中包含已发药/已执行/已计费医嘱,无法批量退回')
return
}
// 批量调用逻辑...
}
// 初始化加载
fetchTableData()
</script>
<style scoped>
.order-verification-container {
padding: 20px;
background: #fff;
}
</style>

View File

@@ -1,128 +0,0 @@
<template>
<div class="order-verify-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>医嘱校对</span>
<el-button type="primary" @click="handleVerify" :disabled="selectedOrders.length === 0">批量校对</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="tableData"
class="order-verify-table"
border
stripe
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="orderNo" label="医嘱号" width="120" />
<el-table-column prop="patientName" label="患者姓名" width="100" />
<el-table-column prop="bedNo" label="床号" width="80" />
<el-table-column prop="drugName" label="注射药品" min-width="150" show-overflow-tooltip />
<el-table-column prop="singleDose" label="单次剂量" width="100" />
<el-table-column prop="totalAmount" label="总量" width="100" />
<el-table-column prop="totalPrice" label="总金额" width="100" />
<el-table-column prop="frequencyUsage" label="频次/用法" width="120" />
<el-table-column prop="startTime" label="开始时间" width="160">
<template #default="{ row }">
{{ formatDateTime(row.startTime) }}
</template>
</el-table-column>
<el-table-column prop="orderingDoctor" label="开嘱医生" width="100" />
<el-table-column prop="stopTime" label="停嘱时间" width="160">
<template #default="{ row }">
{{ row.stopTime ? formatDateTime(row.stopTime) : '-' }}
</template>
</el-table-column>
<el-table-column prop="stoppingDoctor" label="停嘱医生" width="100">
<template #default="{ row }">
{{ row.stoppingDoctor || '-' }}
</template>
</el-table-column>
<el-table-column prop="skinTestStatus" label="皮试" width="100">
<template #default="{ row }">
<el-tag v-if="row.skinTestStatus === 1" type="danger" effect="dark">需皮试</el-tag>
<el-tag v-else-if="row.skinTestStatus === 2" type="success">皮试通过</el-tag>
<el-tag v-else-if="row.skinTestStatus === 3" type="warning">皮试未过</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="diagnosis" label="诊断" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleSingleVerify(row)">校对</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getOrderVerifyList, verifyOrder } from '@/api/inpatient';
import { ElMessage } from 'element-plus';
import dayjs from 'dayjs';
const loading = ref(false);
const tableData = ref([]);
const selectedOrders = ref([]);
const formatDateTime = (date) => {
return date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '';
};
const fetchData = async () => {
loading.value = true;
try {
const res = await getOrderVerifyList();
tableData.value = res.data || [];
} catch (error) {
ElMessage.error('获取医嘱校对列表失败');
} finally {
loading.value = false;
}
};
const handleSelectionChange = (selection) => {
selectedOrders.value = selection;
};
const handleSingleVerify = async (row) => {
try {
await verifyOrder({ id: row.id });
ElMessage.success('校对成功');
fetchData();
} catch (error) {
ElMessage.error('校对失败');
}
};
const handleVerify = async () => {
const ids = selectedOrders.value.map(o => o.id);
try {
await verifyOrder({ ids });
ElMessage.success('批量校对成功');
fetchData();
} catch (error) {
ElMessage.error('批量校对失败');
}
};
onMounted(() => {
fetchData();
});
</script>
<style scoped>
.order-verify-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -1,221 +0,0 @@
<template>
<div class="surgery-apply-history">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>手术申请历史</span>
</div>
</template>
<el-table
v-loading="loading"
:data="applyList"
border
stripe
style="width: 100%"
>
<el-table-column prop="applyNo" label="申请单号" width="150" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="deptName" label="科室" width="120" />
<el-table-column prop="surgeryName" label="手术名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="applyDate" label="申请日期" width="160">
<template #default="{ row }">
{{ formatDate(row.applyDate) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<!-- 新增操作列 -->
<el-table-column label="操作" fixed="right" width="260">
<template #default="{ row }">
<!-- 详情按钮原有 -->
<el-button type="primary" size="small" @click="viewDetail(row)">
详情
</el-button>
<!-- 根据状态动态显示业务按钮 -->
<el-button
v-if="row.status === 'PENDING'"
type="warning"
size="small"
@click="handleEdit(row)"
>
修改
</el-button>
<el-button
v-if="row.status === 'PENDING'"
type="danger"
size="small"
@click="handleDelete(row)"
>
删除
</el-button>
<el-button
v-if="row.status === 'APPROVED'"
type="info"
size="small"
@click="handleWithdraw(row)"
>
撤回
</el-button>
<el-button
v-if="row.status !== 'CANCELLED'"
type="success"
size="small"
@click="handlePrint(row)"
>
打印
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 详情弹窗 -->
<el-dialog
v-model="detailVisible"
title="手术申请详情"
width="800px"
destroy-on-close
>
<SurgeryApplyDetail :apply-id="currentApplyId" />
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 编辑弹窗 -->
<el-dialog
v-model="editVisible"
title="修改手术申请"
width="800px"
destroy-on-close
>
<SurgeryApplyForm :apply-id="currentApplyId" @saved="refreshList" />
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
</template>
</el-dialog>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getSurgeryApplyHistory, deleteSurgeryApply, withdrawSurgeryApply } from '@/api/inpatient/surgery';
import { ElMessage, ElMessageBox } from 'element-plus';
import SurgeryApplyDetail from '@/components/SurgeryApplyDetail.vue';
import SurgeryApplyForm from '@/components/SurgeryApplyForm.vue';
// 表格数据及状态
const loading = ref(false);
const applyList = ref([]);
const detailVisible = ref(false);
const editVisible = ref(false);
const currentApplyId = ref(null);
// 初始化加载
const fetchData = async () => {
loading.value = true;
try {
const res = await getSurgeryApplyHistory();
applyList.value = res.data || [];
} catch (e) {
ElMessage.error('加载手术申请历史失败');
} finally {
loading.value = false;
}
};
onMounted(fetchData);
// 刷新列表
const refreshList = () => {
fetchData();
};
// 操作按钮实现
const viewDetail = (row) => {
currentApplyId.value = row.id;
detailVisible.value = true;
};
const handleEdit = (row) => {
currentApplyId.value = row.id;
editVisible.value = true;
};
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该手术申请吗?', '删除确认', {
type: 'warning',
});
await deleteSurgeryApply(row.id);
ElMessage.success('删除成功');
refreshList();
} catch (e) {
// 用户取消或接口错误均不做处理
}
};
const handleWithdraw = async (row) => {
try {
await ElMessageBox.confirm('撤回后将重新进入审批流程,是否继续?', '撤回确认', {
type: 'info',
});
await withdrawSurgeryApply(row.id);
ElMessage.success('撤回成功');
refreshList();
} catch (e) {
// 取消或错误
}
};
const handlePrint = (row) => {
// 这里调用后端打印接口或打开打印模板
window.open(`/api/inpatient/surgery/print/${row.id}`, '_blank');
};
// 状态文字与标签颜色映射
const statusText = (code) => {
const map = {
PENDING: '待审批',
APPROVED: '已批准',
REJECTED: '已驳回',
CANCELLED: '已作废',
};
return map[code] || code;
};
const statusTagType = (code) => {
const map = {
PENDING: 'warning',
APPROVED: 'success',
REJECTED: 'danger',
CANCELLED: 'info',
};
return map[code] || 'default';
};
const formatDate = (dateStr) => {
if (!dateStr) return '-';
const d = new Date(dateStr);
return d.toLocaleString();
};
</script>
<style scoped>
.surgery-apply-history {
padding: 20px;
}
.card-header {
font-size: 16px;
font-weight: 500;
}
</style>

View File

@@ -1,148 +0,0 @@
<template>
<div class="surgery-request-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">住院医生工作站 - 手术申请</span>
</div>
</template>
<el-table :data="requestList" border style="width: 100%" v-loading="loading" row-key="id">
<el-table-column prop="id" label="申请单号" width="120" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)">
{{ statusMap[row.status] || row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="申请时间" width="160" />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleDetails(row)">详情</el-button>
<!-- 待签发编辑删除 -->
<template v-if="row.status === 'PENDING_SIGN'">
<el-button type="warning" size="small" @click="handleEdit(row)">编辑</el-button>
<el-popconfirm
title="确认删除该笔手术申请单吗?删除后数据还原将无法恢复。"
confirm-button-text="确认"
cancel-button-text="取消"
confirm-button-type="danger"
@confirm="handleDelete(row)"
>
<template #reference>
<el-button type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</template>
<!-- 已签发撤回 -->
<template v-else-if="row.status === 'SIGNED'">
<el-button type="warning" size="small" @click="handleRevoke(row)">撤回</el-button>
</template>
<!-- 已校对/已执行/已安排/已完成打印 -->
<template v-else-if="['VERIFIED', 'EXECUTED', 'SCHEDULED', 'COMPLETED'].includes(row.status)">
<el-button type="success" size="small" @click="handlePrint(row)">打印</el-button>
</template>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 编辑/详情弹窗 (复用临床医嘱-手术界面) -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px" destroy-on-close>
<div class="dialog-content">
<p>当前操作{{ dialogType === 'edit' ? '编辑' : '查看' }}手术申请单</p>
<p>申请单ID{{ currentRow?.id }}</p>
<!-- 此处嵌入临床医嘱-手术组件 -->
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button v-if="dialogType === 'edit'" type="primary" @click="handleSave">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getSurgeryRequestList, deleteSurgeryRequest, revokeSurgeryRequest, printSurgeryRequest } from '@/api/inpatient/surgeryRequest'
const loading = ref(false)
const requestList = ref([])
const statusMap = {
PENDING_SIGN: '待签发', SIGNED: '已签发', VERIFIED: '已校对',
EXECUTED: '已执行', SCHEDULED: '已安排', COMPLETED: '已完成', CANCELLED: '已作废'
}
const statusTagType = (status) => {
const map = { PENDING_SIGN: 'info', SIGNED: 'warning', VERIFIED: 'primary', EXECUTED: 'success', SCHEDULED: 'success', COMPLETED: 'success', CANCELLED: 'danger' }
return map[status] || 'info'
}
const dialogVisible = ref(false)
const dialogTitle = ref('')
const dialogType = ref('')
const currentRow = ref(null)
const loadData = async () => {
loading.value = true
try {
const res = await getSurgeryRequestList()
requestList.value = res.data || []
} finally {
loading.value = false
}
}
const handleDetails = (row) => {
dialogType.value = 'details'
dialogTitle.value = '手术申请详情'
currentRow.value = row
dialogVisible.value = true
}
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogTitle.value = '编辑手术申请'
currentRow.value = row
dialogVisible.value = true
}
const handleSave = async () => {
// 调用保存接口逻辑
ElMessage.success('保存成功')
dialogVisible.value = false
loadData()
}
const handleDelete = async (row) => {
try {
await deleteSurgeryRequest(row.id)
ElMessage.success('删除成功')
loadData()
} catch (e) {
ElMessage.error(e.message || '删除失败')
}
}
const handleRevoke = async (row) => {
try {
await revokeSurgeryRequest(row.id)
ElMessage.success('撤回成功')
loadData()
} catch (e) {
ElMessage.error(e.message || '撤回失败')
}
}
const handlePrint = (row) => {
printSurgeryRequest(row.id)
ElMessage.success('已触发打印')
}
onMounted(loadData)
</script>

View File

@@ -1,105 +0,0 @@
<template>
<div class="temperature-chart-wrapper">
<div ref="chartRef" class="chart-container"></div>
<el-table :data="tableData" border class="data-table" size="small">
<el-table-column prop="time" label="时间" width="120" />
<el-table-column prop="temp" label="体温(℃)" width="100" />
<el-table-column prop="pulse" label="脉搏(次/分)" width="110" />
<el-table-column prop="hr" label="心率(次/分)" width="110" />
</el-table>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getVitalSignsData } from '@/api/vitalSigns'
const chartRef = ref(null)
let chartInstance = null
const tableData = ref([])
const props = defineProps({ patientId: { type: String, default: '' } })
// 核心修复:严格遵循医疗绘图规范配置 ECharts
const renderChart = (rawData) => {
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value)
}
// 提取所有时间点并排序
const times = [...new Set(rawData.map(d => d.recordTime))].sort()
// 按类型映射数据,缺失值填 null 以触发断点逻辑
const mapSeries = (type) => times.map(t => {
const item = rawData.find(d => d.recordTime === t && d.type === type)
return item ? Number(item.value) : null
})
const option = {
tooltip: { trigger: 'axis' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: times, axisLabel: { rotate: 30 } },
yAxis: { type: 'value', min: 35, max: 42, splitNumber: 7, axisLabel: { formatter: '{value}℃' } },
series: [
{
name: '体温',
type: 'line',
data: mapSeries('TEMP'),
symbol: 'path://M0,0 L10,10 M10,0 L0,10', // 绘制 "x"
symbolSize: 8,
lineStyle: { color: '#1890ff', width: 2 },
connectNulls: false, // 核心修复:数据缺失自动断开连线
itemStyle: { color: '#1890ff' }
},
{
name: '脉搏',
type: 'line',
data: mapSeries('PULSE'),
symbol: 'circle', // 绘制 "●"
symbolSize: 8,
lineStyle: { color: '#ff4d4f', width: 2 },
connectNulls: false,
itemStyle: { color: '#ff4d4f' }
},
{
name: '心率',
type: 'line',
data: mapSeries('HR'),
symbol: 'circle', // 绘制 "○"
symbolSize: 8,
lineStyle: { color: '#ff4d4f', width: 2 },
connectNulls: false,
itemStyle: { color: 'transparent', borderColor: '#ff4d4f', borderWidth: 2 }
}
]
}
chartInstance.setOption(option, true)
}
const loadData = async () => {
if (!props.patientId) return
try {
const res = await getVitalSignsData({ patientId: props.patientId })
// 同步表格数据
tableData.value = res.data.tableRows || []
// 渲染图表
await nextTick()
renderChart(res.data.chartPoints || [])
} catch (e) {
console.error('加载体温单数据失败', e)
}
}
onMounted(() => loadData())
watch(() => props.patientId, loadData)
onUnmounted(() => chartInstance?.dispose())
// 暴露刷新方法,供父组件在【新增/保存】成功后调用
defineExpose({ refresh: loadData })
</script>
<style scoped>
.temperature-chart-wrapper { padding: 16px; background: #fff; }
.chart-container { width: 100%; height: 400px; margin-bottom: 16px; }
.data-table { width: 100%; }
</style>

View File

@@ -1,132 +0,0 @@
<template>
<div class="vital-signs-container">
<el-card shadow="never" class="chart-card">
<div ref="chartRef" class="chart-container" data-connect-nulls="false"></div>
</el-card>
<el-card shadow="never" class="table-card">
<el-table :data="tableData" border stripe style="width: 100%">
<el-table-column prop="recordTime" label="时间" width="180" align="center" />
<el-table-column prop="temperature" label="体温(℃)" align="center">
<template #default="{ row }">{{ row.temperature ?? '-' }}</template>
</el-table-column>
<el-table-column prop="heartRate" label="心率(次/分)" align="center">
<template #default="{ row }">{{ row.heartRate ?? '-' }}</template>
</el-table-column>
<el-table-column prop="pulse" label="脉搏(次/分)" align="center">
<template #default="{ row }">{{ row.pulse ?? '-' }}</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, watch } from 'vue'
import * as echarts from 'echarts'
import { getVitalSignsList } from '@/api/inpatient'
import { ElMessage } from 'element-plus'
const props = defineProps({
patientId: { type: [String, Number], required: true },
refreshTrigger: { type: Boolean, default: false }
})
const chartRef = ref(null)
let chartInstance = null
const tableData = ref([])
const chartData = reactive({
xAxis: [],
series: {
temperature: [],
heartRate: [],
pulse: []
}
})
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const option = {
tooltip: { trigger: 'axis' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: chartData.xAxis, axisLabel: { rotate: 30 } },
yAxis: { type: 'value', min: 35, max: 42, splitNumber: 7 },
series: [
{
name: '体温',
type: 'line',
data: chartData.series.temperature,
symbol: 'x',
symbolSize: 8,
itemStyle: { color: '#1890ff' },
lineStyle: { color: '#1890ff', width: 2 },
connectNulls: false
},
{
name: '心率',
type: 'line',
data: chartData.series.heartRate,
symbol: 'emptyCircle',
symbolSize: 8,
itemStyle: { color: '#f5222d' },
lineStyle: { color: '#f5222d', width: 2 },
connectNulls: false
},
{
name: '脉搏',
type: 'line',
data: chartData.series.pulse,
symbol: 'circle',
symbolSize: 8,
itemStyle: { color: '#f5222d' },
lineStyle: { color: '#f5222d', width: 2 },
connectNulls: false
}
]
}
chartInstance.setOption(option)
}
const fetchData = async () => {
try {
const res = await getVitalSignsList({ patientId: props.patientId })
if (res.code === 200) {
const sortedData = (res.data || []).sort((a, b) => new Date(a.recordTime) - new Date(b.recordTime))
chartData.xAxis = sortedData.map(d => d.recordTime)
chartData.series.temperature = sortedData.map(d => d.temperature ?? null)
chartData.series.heartRate = sortedData.map(d => d.heartRate ?? null)
chartData.series.pulse = sortedData.map(d => d.pulse ?? null)
tableData.value = sortedData
await nextTick()
if (chartInstance) {
chartInstance.setOption({
xAxis: { data: chartData.xAxis },
series: [
{ data: chartData.series.temperature },
{ data: chartData.series.heartRate },
{ data: chartData.series.pulse }
]
})
}
}
} catch (err) {
ElMessage.error('获取体征数据失败')
}
}
watch(() => props.refreshTrigger, (val) => {
if (val) fetchData()
})
onMounted(() => {
fetchData()
initChart()
window.addEventListener('resize', () => chartInstance?.resize())
})
</script>
<style scoped>
.vital-signs-container { padding: 16px; display: flex; flex-direction: column; gap: 16px; }
.chart-card { height: 400px; }
.chart-container { width: 100%; height: 100%; }
</style>

View File

@@ -1,145 +0,0 @@
<template>
<div class="surgery-apply-list-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>手术申请历史列表</span>
<el-button type="primary" @click="handleRefresh">刷新</el-button>
</div>
</template>
<el-table :data="tableData" v-loading="loading" border style="width: 100%">
<el-table-column prop="applyNo" label="申请单号" width="160" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="surgeryName" label="手术名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="applyTime" label="申请时间" width="180" />
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" class-name="operation-cell">
<template #default="{ row }">
<el-dropdown trigger="click" @command="(cmd) => handleOperation(cmd, row)">
<el-button type="primary" link size="small">
操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<!-- 待签发 (0) -->
<el-dropdown-item v-if="row.status === 0" command="edit">编辑</el-dropdown-item>
<el-dropdown-item v-if="row.status === 0" command="delete" divided>删除</el-dropdown-item>
<!-- 已签发 (1) -->
<el-dropdown-item v-if="row.status === 1" command="revoke">撤回</el-dropdown-item>
<!-- 已校对/已执行/已安排/已完成 (2/3/4/5) -->
<el-dropdown-item v-if="[2,3,4,5].includes(row.status)" command="print">打印</el-dropdown-item>
<!-- 通用 -->
<el-dropdown-item command="detail" divided>详情</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 详情/编辑弹窗复用 -->
<SurgeryApplyDialog
v-model="dialogVisible"
:mode="dialogMode"
:data="currentRow"
@success="handleRefresh"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ArrowDown } from '@element-plus/icons-vue';
import { getSurgeryApplyList, deleteSurgeryApply, revokeSurgeryApply } from '@/api/inpatient/surgery';
import SurgeryApplyDialog from './components/SurgeryApplyDialog.vue';
const loading = ref(false);
const tableData = ref<any[]>([]);
const dialogVisible = ref(false);
const dialogMode = ref<'view' | 'edit'>('view');
const currentRow = ref<any>(null);
const STATUS_MAP: Record<number, { label: string; type: string }> = {
0: { label: '待签发', type: 'info' },
1: { label: '已签发', type: 'warning' },
2: { label: '已校对', type: 'success' },
3: { label: '已执行', type: 'primary' },
4: { label: '已安排', type: '' },
5: { label: '已完成', type: 'success' },
6: { label: '已撤销', type: 'danger' },
7: { label: '已作废', type: 'info' }
};
const getStatusLabel = (status: number) => STATUS_MAP[status]?.label || '未知';
const getStatusType = (status: number) => STATUS_MAP[status]?.type || 'info';
const fetchData = async () => {
loading.value = true;
try {
const res = await getSurgeryApplyList({ patientId: 'current_patient_id' });
tableData.value = res.data || [];
} finally {
loading.value = false;
}
};
const handleRefresh = () => fetchData();
const handleOperation = async (command: string, row: any) => {
switch (command) {
case 'detail':
dialogMode.value = 'view';
currentRow.value = row;
dialogVisible.value = true;
break;
case 'edit':
dialogMode.value = 'edit';
currentRow.value = row;
dialogVisible.value = true;
break;
case 'delete':
try {
await ElMessageBox.confirm(
'确认删除该笔手术申请单吗?删除后数据还原将无法恢复。',
'删除确认',
{ confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'el-button--danger' }
);
await deleteSurgeryApply(row.id);
ElMessage.success('删除成功');
handleRefresh();
} catch (e) {
// 用户取消或请求失败
}
break;
case 'revoke':
try {
await revokeSurgeryApply(row.id);
ElMessage.success('撤回成功,单据已恢复至待签发状态');
handleRefresh();
} catch (err: any) {
ElMessage.error(err.response?.data?.msg || '撤回失败');
}
break;
case 'print':
window.open(`/inpatient/surgery/print/${row.id}`, '_blank');
break;
}
};
onMounted(fetchData);
</script>
<style scoped>
.surgery-apply-list-container { padding: 16px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
</style>

View File

@@ -1,99 +0,0 @@
<template>
<div class="advice-form-container">
<el-form :model="adviceData" label-width="80px" class="main-form">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="类型">
<el-select v-model="adviceData.orderType" class="order-type-select" @change="onTypeChange">
<el-option label="西药" value="WESTERN_MED" />
<el-option label="中成药" value="CHINESE_PATENT" />
<el-option label="诊疗" value="TREATMENT" />
<el-option label="手术" value="SURGERY" />
<el-option label="文字医嘱" value="TEXT" />
<el-option label="出院带药" value="DISCHARGE_MED" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="长期/临时">
<el-radio-group v-model="adviceData.frequencyType" :disabled="isDischargeMed || isTextAdvice">
<el-radio label="长期">长期</el-radio>
<el-radio label="临时">临时</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<!-- Bug #587: 新增开始时间字段 -->
<el-col :span="8">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="adviceData.startTime"
type="datetime"
placeholder="选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
name="startTime"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- Bug #589: 联动专属面板 -->
<DischargeMedPanel
v-if="adviceData.orderType === 'DISCHARGE_MED'"
:visible="true"
@confirm="onPanelConfirm"
@cancel="onPanelCancel"
/>
<!-- Bug #588: 联动文字医嘱专属面板 -->
<TextAdvicePanel
v-if="adviceData.orderType === 'TEXT'"
:visible="true"
:current-dept="currentDept"
@confirm="onTextPanelConfirm"
@cancel="onPanelCancel"
/>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import DischargeMedPanel from './DischargeMedPanel.vue'
import TextAdvicePanel from './TextAdvicePanel.vue'
const adviceData = reactive({
orderType: '',
frequencyType: '临时',
startTime: '' // Bug #587
})
const isDischargeMed = ref(false)
const isTextAdvice = ref(false)
const currentDept = ref('呼吸内科病房') // 实际应从患者上下文动态获取
// Bug #587: 初始化默认开始时间为当前服务器时间
onMounted(() => {
const now = new Date()
const pad = n => n.toString().padStart(2, '0')
adviceData.startTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
})
const onTypeChange = (val) => {
isDischargeMed.value = val === 'DISCHARGE_MED'
isTextAdvice.value = val === 'TEXT'
}
const onPanelConfirm = (data) => {
// 合并子面板数据
Object.assign(adviceData, data)
}
const onPanelCancel = () => {
// 取消逻辑
}
const onTextPanelConfirm = (data) => {
Object.assign(adviceData, data)
}
</script>

View File

@@ -1,138 +0,0 @@
<template>
<div class="discharge-med-panel" v-if="visible">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" class="med-form">
<el-row :gutter="16">
<!-- Bug #587: 新增开始时间字段 -->
<el-col :span="8">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="datetime"
placeholder="选择时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
name="startTime"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="药品检索" prop="drugId">
<el-select v-model="form.drugId" filterable remote :remote-method="searchDrugs" placeholder="仅限西药/中成药口服/外用" @change="onDrugSelect" clearable>
<el-option v-for="item in drugOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="单次用量" prop="singleDosage">
<el-input v-model.number="form.singleDosage" placeholder="输入数值">
<template #append>
<el-select v-model="form.unit" style="width: 80px">
<el-option label="片" value="片" />
<el-option label="盒" value="盒" />
<el-option label="支" value="支" />
</el-select>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="用药频次" prop="frequency">
<el-input v-model="form.frequency" placeholder="如:每日两次" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="用药天数" prop="medicationDays">
<el-input v-model.number="form.medicationDays" placeholder="≤7或≤30" @input="calculateTotal" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="总量" prop="totalAmount">
<el-input v-model.number="form.totalAmount" placeholder="自动计算可微调" @input="onTotalChange" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16" class="info-row">
<el-col :span="6">
<span class="info-label">单价:</span> {{ form.price || '0.00' }}
</el-col>
<el-col :span="6">
<span class="info-label">库房:</span> {{ form.warehouse || '中心药房' }}
</el-col>
</el-row>
</el-form>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
const props = defineProps({
visible: Boolean
})
const formRef = ref(null)
const drugOptions = ref([])
const form = reactive({
startTime: '', // Bug #587
drugId: '',
singleDosage: null,
unit: '片',
route: '',
frequency: '',
medicationDays: null,
totalAmount: null,
price: 0,
warehouse: '中心药房'
})
const rules = {
drugId: [{ required: true, message: '请选择药品', trigger: 'change' }],
singleDosage: [{ required: true, message: '请输入单次用量', trigger: 'blur' }],
route: [{ required: true, message: '请选择给药途径', trigger: 'change' }],
medicationDays: [{ required: true, message: '请输入用药天数', trigger: 'blur' }],
totalAmount: [{ required: true, message: '总量为必填项', trigger: 'blur' }]
}
// Bug #587: 初始化默认开始时间
onMounted(() => {
const now = new Date()
const pad = n => n.toString().padStart(2, '0')
form.startTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
})
const searchDrugs = (query) => {
// 模拟检索逻辑
drugOptions.value = query ? [{ id: 1, name: '曲咪新乳膏' }] : []
}
const onDrugSelect = (id) => {
form.price = 15.50
}
const calculateTotal = () => {
if (form.singleDosage && form.frequency && form.medicationDays) {
form.totalAmount = form.singleDosage * form.frequency * form.medicationDays
}
}
const onTotalChange = () => {
// 允许手动微调
}
const handleConfirm = () => {
formRef.value.validate((valid) => {
if (valid) {
// emit confirm event
}
})
}
const handleCancel = () => {
// emit cancel event
}
</script>

View File

@@ -1,222 +0,0 @@
<template>
<div class="surgery-apply-history">
<!-- 筛选/查询控制栏 -->
<el-form :model="queryParams" inline class="filter-bar" @submit.prevent="handleQuery">
<el-form-item label="创建时间">
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:shortcuts="dateShortcuts"
@change="handleQuery"
/>
</el-form-item>
<el-form-item label="申请状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable @change="handleQuery">
<el-option label="全部" value="" />
<el-option label="待签发" :value="1" />
<el-option label="已签发" :value="2" />
<el-option label="已校对" :value="3" />
<el-option label="已执行" :value="4" />
<el-option label="已安排" :value="5" />
<el-option label="已完成" :value="6" />
<el-option label="已撤销" :value="10" />
</el-select>
</el-form-item>
<el-form-item>
<el-input
v-model="queryParams.keyword"
placeholder="请输入手术单号/名称/患者姓名"
clearable
@keyup.enter="handleQuery"
@clear="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据列表 -->
<el-table :data="tableData" border style="width: 100%; margin-top: 10px;" v-loading="loading">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="applyNo" label="手术单号" min-width="140" />
<el-table-column prop="patientName" label="患者姓名" min-width="100" />
<el-table-column prop="applyName" label="申请单名称" min-width="180" />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="applicantName" label="申请者" width="100" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" effect="light">{{ getStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="handleView(row)">详情</el-button>
<!-- 待签发 (1) -->
<template v-if="row.status === 1">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
<!-- 已签发 (2) -->
<template v-else-if="row.status === 2">
<el-button link type="warning" @click="handleRevoke(row)">撤回</el-button>
</template>
<!-- 已校对/已执行/已安排/已完成 (3,4,5,6) -->
<template v-else-if="[3, 4, 5, 6].includes(row.status)">
<el-button link type="info" @click="handlePrint(row)">打印</el-button>
</template>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="handleQuery"
@current-change="handleQuery"
style="margin-top: 15px; justify-content: flex-end;"
/>
<!-- 编辑弹窗 (复用临床医嘱-手术界面) -->
<el-dialog v-model="editDialogVisible" title="编辑手术申请单" width="800px" destroy-on-close>
<AdviceForm ref="editFormRef" :is-edit="true" :initial-data="currentEditData" @confirm="onEditConfirm" @cancel="editDialogVisible = false" />
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AdviceForm from './AdviceForm.vue'
// 假设 API 模块路径,实际项目请根据路由调整
import { getSurgeryApplyList, deleteSurgeryApply, revokeSurgeryApply, printSurgeryApply, updateSurgeryApply } from '@/api/surgery'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const editDialogVisible = ref(false)
const currentEditData = ref({})
const editFormRef = ref(null)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
dateRange: [],
status: '',
keyword: ''
})
const dateShortcuts = [
{ text: '最近一周', value: () => { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); return [start, end] } },
{ text: '最近一个月', value: () => { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); return [start, end] } }
]
const getStatusText = (status) => {
const map = { 1: '待签发', 2: '已签发', 3: '已校对', 4: '已执行', 5: '已安排', 6: '已完成', 10: '已撤销' }
return map[status] || '未知'
}
const getStatusType = (status) => {
const map = { 1: 'info', 2: 'primary', 3: 'success', 4: 'success', 5: 'warning', 6: 'success', 10: 'danger' }
return map[status] || 'info'
}
const handleQuery = async () => {
loading.value = true
try {
const res = await getSurgeryApplyList(queryParams)
tableData.value = res.data.list || []
total.value = res.data.total || 0
} catch (e) {
ElMessage.error('查询失败')
} finally {
loading.value = false
}
}
const handleReset = () => {
queryParams.dateRange = []
queryParams.status = ''
queryParams.keyword = ''
queryParams.pageNum = 1
handleQuery()
}
const handleView = (row) => {
// 打开详情弹窗或路由跳转
ElMessage.info(`查看手术申请单详情: ${row.applyNo}`)
}
const handleEdit = (row) => {
currentEditData.value = { ...row }
editDialogVisible.value = true
}
const onEditConfirm = async (formData) => {
try {
await updateSurgeryApply(formData)
ElMessage.success('修改成功')
editDialogVisible.value = false
handleQuery()
} catch (e) {
ElMessage.error('修改失败')
}
}
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确认删除该笔手术申请单吗?删除后数据还原将无法恢复。', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--danger',
type: 'warning'
})
await deleteSurgeryApply(row.id)
ElMessage.success('删除成功,状态已更新为已作废')
handleQuery()
} catch (e) {
if (e !== 'cancel') ElMessage.error('删除失败')
}
}
const handleRevoke = async (row) => {
try {
await ElMessageBox.confirm('确认撤回该手术申请吗?', '提示', { type: 'warning' })
const res = await revokeSurgeryApply(row.id)
if (res.code === 200) {
ElMessage.success('撤回成功,单据已回滚至待签发状态')
handleQuery()
} else {
ElMessage.error(res.msg || '撤回失败!该手术申请已由病区护士已校对,请致电病区护士处理。')
}
} catch (e) {
if (e !== 'cancel') {
const errMsg = e.response?.data?.msg || e.message || '撤回失败!该手术申请已由病区护士已校对,请致电病区护士处理。'
ElMessage.error(errMsg)
}
}
}
const handlePrint = (row) => {
ElMessage.success('正在调用打印服务...')
printSurgeryApply(row.id)
}
onMounted(() => {
handleQuery()
})
</script>
<style scoped>
.surgery-apply-history { padding: 10px; }
.filter-bar { margin-bottom: 10px; }
</style>

View File

@@ -1,119 +0,0 @@
<template>
<div class="text-advice-panel" v-if="visible">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px" class="text-form">
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="医嘱内容" prop="textContent">
<el-input
v-model="form.textContent"
placeholder="请输入3~50字医嘱内容"
maxlength="50"
show-word-limit
name="textContent"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="datetime"
placeholder="选择时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
name="startTime"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="频次" prop="frequency">
<el-select v-model="form.frequency" placeholder="选择频次" name="frequency">
<el-option label="立即" value="立即" />
<el-option label="每日一次" value="每日一次" />
<el-option label="每日两次" value="每日两次" />
<el-option label="必要时" value="必要时" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="执行科室" prop="execDept">
<el-select v-model="form.execDept" placeholder="选择科室" name="execDept">
<el-option label="呼吸内科病房" value="呼吸内科病房" />
<el-option label="心血管内科病房" value="心血管内科病房" />
<el-option label="消化内科病房" value="消化内科病房" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16" class="action-row">
<el-col :span="24" style="text-align: right;">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script setup>
import { reactive, ref, watch, onMounted } from 'vue'
const props = defineProps({
visible: { type: Boolean, default: false },
currentDept: { type: String, default: '呼吸内科病房' }
})
const emit = defineEmits(['confirm', 'cancel'])
const formRef = ref(null)
const form = reactive({
textContent: '',
startTime: '',
frequency: '立即',
execDept: ''
})
const rules = {
textContent: [
{ required: true, message: '请输入医嘱内容', trigger: 'blur' },
{ min: 3, max: 50, message: '文字医嘱内容长度需在3~50字之间', trigger: 'blur' }
],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
frequency: [{ required: true, message: '请选择频次', trigger: 'change' }],
execDept: [{ required: true, message: '请选择执行科室', trigger: 'change' }]
}
const resetForm = () => {
form.textContent = ''
form.startTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
form.frequency = '立即'
form.execDept = props.currentDept
}
watch(() => props.visible, (val) => {
if (val) resetForm()
})
onMounted(() => resetForm())
const handleConfirm = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
// 强制金额为0屏蔽计费元素
emit('confirm', { ...form, amount: 0.00, singleDosage: null, totalAmount: null })
}
})
}
const handleCancel = () => {
emit('cancel')
}
</script>
<style scoped>
.text-advice-panel { margin-top: 16px; padding: 16px; background: #f9f9f9; border-radius: 4px; border: 1px solid #ebeef5; }
.action-row { margin-top: 10px; }
</style>

View File

@@ -1,82 +0,0 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
<el-form-item label="患者姓名" prop="patientName">
<el-input v-model="queryParams.patientName" placeholder="请输入患者姓名" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="住院号" prop="inpatientNo">
<el-input v-model="queryParams.inpatientNo" placeholder="请输入住院号" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="药品名称" prop="drugName">
<el-select v-model="queryParams.drugName" placeholder="请选择药品" clearable filterable style="width: 200px">
<el-option label="阿莫西林" value="阿莫西林" />
<el-option label="布洛芬" value="布洛芬" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="patientName" label="患者姓名" />
<el-table-column prop="inpatientNo" label="住院号" />
<el-table-column prop="drugName" label="药品名称" />
<el-table-column prop="status" label="发退药状态" />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button link type="primary" @click="handleDispense(scope.row)">发药</el-button>
<el-button link type="danger" @click="handleReturn(scope.row)">退药</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
const queryParams = reactive({
patientName: '',
inpatientNo: '',
drugName: ''
})
const queryRef = ref()
const tableData = ref([])
const handleQuery = () => {
console.log('查询参数:', queryParams)
}
const resetQuery = () => {
queryRef.value?.resetFields()
handleQuery()
}
const handleDispense = (row: any) => {
ElMessage.success(`发药: ${row.patientName}`)
}
const handleReturn = (row: any) => {
ElMessage.warning(`退药: ${row.patientName}`)
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
/* 修复 Bug #603搜索栏布局溢出导致字段覆盖、重置按钮被挤没 */
.search-form {
display: flex;
flex-wrap: wrap; /* 允许表单项自动换行防止100%视图下溢出 */
gap: 12px; /* 统一间距 */
margin-bottom: 16px;
}
.search-form .el-form-item {
margin-bottom: 0;
margin-right: 0; /* 清除 Element Plus inline 表单默认右边距,避免挤压重置按钮 */
}
</style>

View File

@@ -1,102 +0,0 @@
<template>
<el-dialog
v-model="visible"
title="编辑检验申请单"
width="900px"
destroy-on-close
data-testid="lab-request-edit-dialog"
>
<el-form :model="form" label-width="100px" class="lab-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="症状" prop="symptom">
<el-input v-model="form.symptom" placeholder="请输入症状" data-testid="symptom-input" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="体征" prop="sign">
<el-input v-model="form.sign" placeholder="请输入体征" data-testid="sign-input" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="相关结果" prop="relatedResult">
<el-input v-model="form.relatedResult" type="textarea" :rows="3" placeholder="请输入相关结果" />
</el-form-item>
</el-form>
<div class="selected-items-wrapper">
<h4 class="section-title">已选择项目</h4>
<el-table
:data="selectedItems"
border
style="width: 100%"
max-height="300"
data-testid="selected-items-list"
>
<el-table-column prop="itemName" label="检验项目" min-width="150" />
<el-table-column prop="price" label="单价(元)" width="100" align="right">
<template #default="{ row }">{{ row.price?.toFixed(2) }}</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" width="80" align="center" />
</el-table>
<el-empty v-if="selectedItems.length === 0" description="无数据" />
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSave">确认修改</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { getLabRequestDetail, updateLabRequest } from '@/api/lab-request'
import { ElMessage } from 'element-plus'
const visible = ref(false)
const form = reactive({
id: null,
symptom: '',
sign: '',
relatedResult: ''
})
const selectedItems = ref([])
const open = async (id) => {
visible.value = true
try {
const res = await getLabRequestDetail(id)
if (res.code === 200 && res.data) {
// 修复 Bug #576原逻辑仅赋值主表字段遗漏了明细项数组的绑定
Object.assign(form, res.data)
// 确保右侧“已选择”列表正确接收后端返回的 items 数据
selectedItems.value = res.data.items || []
}
} catch (error) {
console.error('获取检验申请详情失败', error)
ElMessage.error('加载申请单数据失败')
}
}
const handleSave = async () => {
try {
await updateLabRequest(form)
ElMessage.success('修改成功')
visible.value = false
// 触发父组件刷新列表
emit('refresh')
} catch (error) {
ElMessage.error('保存失败')
}
}
const emit = defineEmits(['refresh'])
defineExpose({ open })
</script>
<style scoped>
.lab-form { margin-bottom: 20px; }
.selected-items-wrapper { margin-top: 10px; }
.section-title { font-size: 14px; font-weight: 600; margin-bottom: 10px; color: #303133; }
</style>

View File

@@ -1,149 +0,0 @@
<template>
<div class="lab-request-container">
<el-card>
<template #header>
<div class="card-header">
<span>检验申请列表</span>
</div>
</template>
<el-table :data="tableData" border style="width: 100%" v-loading="loading">
<!-- Bug #467 Fix: 修正列标题术语处方号改为申请单号 -->
<el-table-column prop="requestNo" label="申请单号" width="180" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<!-- Bug #467 Fix: 优化单据名称展示支持具体项目拼接超长截断与悬停提示 -->
<el-table-column prop="requestName" label="申请单名称" min-width="220">
<template #default="{ row }">
<el-tooltip
:content="row.fullRequestName"
placement="top"
:disabled="row.requestName === row.fullRequestName"
:show-after="300"
>
<span class="request-name-text">{{ row.requestName }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="createTime" label="申请时间" width="180" />
<el-table-column prop="status" label="状态" width="100" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)" :disabled="row.status !== '待签发'">修改</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- Bug #576 Fix: 编辑检验申请单弹窗右侧已选择列表绑定 items 数据 -->
<el-dialog v-model="editDialogVisible" title="编辑检验申请单" width="800px" destroy-on-close>
<el-form :model="editForm" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="症状">
<el-input v-model="editForm.symptoms" type="textarea" :rows="3" name="symptoms" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="体征">
<el-input v-model="editForm.signs" type="textarea" :rows="3" name="signs" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="相关结果">
<el-input v-model="editForm.relatedResults" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div class="dialog-footer-layout">
<div class="selected-panel">
<h4>已选择</h4>
<el-table :data="editForm.items" border style="width: 100%" max-height="200" empty-text="无数据">
<el-table-column prop="itemName" label="检验项目" />
<el-table-column prop="price" label="单价" width="100">
<template #default="{ row }">{{ row.price }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="80" />
</el-table>
</div>
</div>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveEdit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getLabRequestListApi, getLabRequestDetailApi, updateLabRequestApi } from '@/api/inpatient/labRequest'
import { ElMessage } from 'element-plus'
const loading = ref(false)
const tableData = ref([])
const editDialogVisible = ref(false)
const editForm = ref({
id: null,
symptoms: '',
signs: '',
relatedResults: '',
items: []
})
const fetchData = async () => {
loading.value = true
try {
const res = await getLabRequestListApi()
tableData.value = res.data || []
} catch (error) {
console.error('获取检验申请列表失败:', error)
} finally {
loading.value = false
}
}
// Bug #576 Fix: 调用详情接口获取完整数据(含 items确保右侧列表正确回显
const handleEdit = async (row) => {
try {
const res = await getLabRequestDetailApi(row.id)
if (res.data) {
editForm.value = {
id: res.data.id,
symptoms: res.data.symptoms || '',
signs: res.data.signs || '',
relatedResults: res.data.relatedResults || '',
items: res.data.items || []
}
editDialogVisible.value = true
}
} catch (error) {
ElMessage.error('获取申请单详情失败')
}
}
const handleSaveEdit = async () => {
try {
await updateLabRequestApi(editForm.value)
ElMessage.success('保存成功')
editDialogVisible.value = false
fetchData()
} catch (error) {
ElMessage.error('保存失败')
}
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.lab-request-container { padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.request-name-text { cursor: pointer; }
.dialog-footer-layout { margin-top: 20px; }
.selected-panel h4 { margin-bottom: 10px; font-size: 14px; color: #606266; }
</style>

View File

@@ -1,118 +0,0 @@
<template>
<div class="order-verification-container">
<el-card shadow="never" class="panel-card">
<template #header>
<div class="header-row">
<span>医嘱校对</span>
<el-select v-model="selectedPatientId" placeholder="请选择患者" class="patient-selector" @change="loadOrders">
<el-option v-for="p in patientList" :key="p.id" :label="`${p.bedNo}床 - ${p.name}`" :value="p.id" />
</el-select>
</div>
</template>
<el-table :data="orderList" border stripe style="width: 100%" v-loading="loading">
<el-table-column prop="startTime" label="开始时间" width="160" />
<el-table-column prop="singleDose" label="单次剂量" width="100" />
<el-table-column prop="totalAmount" label="总量" width="100" />
<el-table-column prop="totalCost" label="总金额" width="100" />
<el-table-column prop="frequencyUsage" label="频次/用法" width="140" />
<el-table-column prop="orderingDoctor" label="开嘱医生" width="100" />
<el-table-column prop="stopTime" label="停嘱时间" width="160" />
<el-table-column prop="stoppingDoctor" label="停嘱医生" width="100" />
<el-table-column prop="isInjection" label="注射药品" width="90">
<template #default="{ row }">
<el-tag :type="row.isInjection ? 'warning' : 'info'" size="small">
{{ row.isInjection ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="skinTestStatus" label="皮试" width="100">
<template #default="{ row }">
<el-tag
v-if="row.skinTestHighlight"
class="skin-test-tag"
type="danger"
effect="dark"
size="small"
>
需皮试
</el-tag>
<span v-else>{{ row.skinTestStatus || '无需' }}</span>
</template>
</el-table-column>
<el-table-column prop="diagnosis" label="诊断" min-width="150" show-overflow-tooltip />
<el-table-column prop="orderContent" label="医嘱内容" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleVerify(row)">校对</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getVerificationList, verifyOrder } from '@/api/inpatient/nurse'
const selectedPatientId = ref(null)
const patientList = ref([])
const orderList = ref([])
const loading = ref(false)
onMounted(() => {
// 模拟获取当前病区患者列表
patientList.value = [
{ id: 1, bedNo: '011', name: '张三' },
{ id: 2, bedNo: '012', name: '李四' }
]
})
const loadOrders = async () => {
if (!selectedPatientId.value) return
loading.value = true
try {
const res = await getVerificationList(selectedPatientId.value)
orderList.value = res.data || []
} catch (err) {
ElMessage.error('获取医嘱列表失败')
} finally {
loading.value = false
}
}
const handleVerify = async (row) => {
try {
await verifyOrder(row.id)
ElMessage.success('校对成功')
loadOrders()
} catch (err) {
ElMessage.error('校对失败')
}
}
</script>
<style scoped>
.order-verification-container {
padding: 16px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.patient-selector {
width: 240px;
}
.skin-test-tag {
font-weight: bold;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
</style>

View File

@@ -1,112 +0,0 @@
<template>
<div class="order-verify-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>住院护士站 - 医嘱校对</span>
<el-button type="primary" @click="handleVerifySelected" :disabled="selectedRows.length === 0">
批量校对
</el-button>
</div>
</template>
<el-table
:data="orderList"
style="width: 100%"
@selection-change="handleSelectionChange"
border
stripe
>
<el-table-column type="selection" width="55" />
<el-table-column prop="bedNo" label="床号" width="80" />
<el-table-column prop="patientName" label="姓名" width="100" />
<el-table-column prop="startTime" label="开始时间" width="160" />
<el-table-column prop="orderContent" label="医嘱内容" min-width="200" show-overflow-tooltip />
<!-- Bug #595 新增结构化字段 -->
<el-table-column prop="singleDose" label="单次剂量" width="100" />
<el-table-column prop="totalQuantity" label="总量" width="100" />
<el-table-column prop="totalAmount" label="总金额" width="100">
<template #default="{ row }">
¥{{ row.totalAmount?.toFixed(2) || '0.00' }}
</template>
</el-table-column>
<el-table-column prop="frequencyUsage" label="频次/用法" width="140" />
<el-table-column prop="prescribingDoctor" label="开嘱医生" width="100" />
<el-table-column prop="diagnosis" label="诊断" width="150" show-overflow-tooltip />
<!-- 皮试状态高亮列 -->
<el-table-column prop="skinTestStatus" label="皮试状态" width="120">
<template #default="{ row }">
<el-tag v-if="row.skinTestStatus === 'required'" type="danger" effect="dark">需皮试</el-tag>
<el-tag v-else-if="row.skinTestStatus === 'passed'" type="success">皮试通过</el-tag>
<el-tag v-else-if="row.skinTestStatus === 'negative'" type="warning">皮试阴性</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="stopTime" label="停嘱时间" width="160" />
<el-table-column prop="stoppingDoctor" label="停嘱医生" width="100" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleVerifySingle(row)">校对</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getVerifyOrderList, verifyOrder } from '@/api/inpatient/nurse'
const orderList = ref<any[]>([])
const selectedRows = ref<any[]>([])
const loadOrders = async () => {
try {
// 实际项目中应传入当前选中患者ID
const res = await getVerifyOrderList(11)
orderList.value = res.data || []
} catch (error) {
ElMessage.error('加载医嘱列表失败')
}
}
const handleSelectionChange = (rows: any[]) => {
selectedRows.value = rows
}
const handleVerifySingle = async (row: any) => {
try {
await verifyOrder(row.id, '当前护士')
ElMessage.success('校对成功')
loadOrders()
} catch (error) {
ElMessage.error('校对失败')
}
}
const handleVerifySelected = async () => {
for (const row of selectedRows.value) {
await handleVerifySingle(row)
}
}
onMounted(() => {
loadOrders()
})
</script>
<style scoped>
.order-verify-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -1,72 +0,0 @@
<template>
<div class="temperature-chart" ref="chartRef" style="height: 300px;"></div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import * as echarts from 'echarts';
import { getTemperatureChartData } from '@/api/vitalSign'; // 新增 API
const props = defineProps<{
patientId: number;
}>();
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const initChart = (labels: string[], data: number[]) => {
if (!chartRef.value) return;
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
const option: echarts.EChartOption = {
title: { text: '体温单', left: 'center' },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: labels,
boundaryGap: false,
},
yAxis: {
type: 'value',
name: '℃',
min: 35,
max: 41,
},
series: [
{
name: '体温',
type: 'line',
data: data,
smooth: true,
itemStyle: { color: '#ff5722' },
lineStyle: { width: 2 },
areaStyle: { opacity: 0.1 },
},
],
};
chartInstance.setOption(option);
};
const loadData = async () => {
if (!props.patientId) return;
const res = await getTemperatureChartData(props.patientId);
const labels = res.data.timeLabels || [];
const points = res.data.temperaturePoints?.map(v => v ?? null) || [];
initChart(labels, points);
};
onMounted(() => {
loadData();
});
watch(() => props.patientId, () => {
loadData();
});
</script>
<style scoped>
.temperature-chart {
width: 100%;
}
</style>

View File

@@ -1,147 +0,0 @@
<template>
<div class="temperature-sheet-wrapper">
<div class="header-actions">
<el-select v-model="selectedPatientId" placeholder="选择患者" class="patient-selector" @change="loadChartData">
<el-option v-for="p in patientList" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
<el-button type="primary" class="add-vital-sign-btn" @click="openDialog">新增体征</el-button>
</div>
<div class="chart-container" ref="chartRef"></div>
<el-table :data="tableData" class="data-table" border style="margin-top: 16px;">
<el-table-column prop="time_label" label="时间" width="120" />
<el-table-column prop="temperature" label="体温(℃)" width="100" />
<el-table-column prop="heart_rate" label="心率(次/分)" width="110" />
<el-table-column prop="pulse" label="脉搏(次/分)" width="110" />
<el-table-column prop="respiration" label="呼吸(次/分)" width="110" />
</el-table>
<el-dialog v-model="dialogVisible" title="录入生命体征" width="500px">
<el-form :model="form" label-width="80px" class="dialog-form">
<el-form-item label="测量时间">
<el-date-picker v-model="form.measureTime" type="datetime" format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item label="体温">
<el-input-number v-model="form.temperature" :precision="1" :step="0.1" />
</el-form-item>
<el-form-item label="心率">
<el-input-number v-model="form.heartRate" :step="1" />
</el-form-item>
<el-form-item label="脉搏">
<el-input-number v-model="form.pulse" :step="1" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, watch } from 'vue';
import * as echarts from 'echarts';
import { ElMessage } from 'element-plus';
import axios from 'axios';
const chartRef = ref(null);
let chartInstance = null;
const selectedPatientId = ref(null);
const patientList = ref([{ id: 123, name: '张三' }]); // 模拟患者列表
const tableData = ref([]);
const dialogVisible = ref(false);
const form = ref({ measureTime: '', temperature: null, heartRate: null, pulse: null });
// 初始化图表
const initChart = () => {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
window.addEventListener('resize', () => chartInstance?.resize());
};
// 加载并渲染数据
const loadChartData = async () => {
if (!selectedPatientId.value) return;
try {
// 模拟后端请求,实际应替换为真实 API
const res = await axios.get(`/api/vital-signs?patientId=${selectedPatientId.value}&startTime=2026-05-19 00:00:00&endTime=2026-05-21 23:59:59`);
const rawData = res.data || [];
// 映射表格数据
tableData.value = rawData.map(item => ({
time_label: item.time_label,
temperature: item.temperature,
heart_rate: item.heart_rate,
pulse: item.pulse,
respiration: item.respiration
}));
// 映射图表数据 (ECharts 要求 [time, value] 格式,缺失值填 null 触发断点)
const timeAxis = [...new Set(rawData.map(d => d.time_label))];
const tempData = timeAxis.map(t => {
const found = rawData.find(d => d.time_label === t);
return found ? [t, found.temperature] : null;
});
const hrData = timeAxis.map(t => {
const found = rawData.find(d => d.time_label === t);
return found ? [t, found.heart_rate] : null;
});
const pulseData = timeAxis.map(t => {
const found = rawData.find(d => d.time_label === t);
return found ? [t, found.pulse] : null;
});
const option = {
tooltip: { trigger: 'axis', formatter: (params) => {
const p = params[0];
return `${p.axisValue}<br/>体温: ${p.data?.[1] ?? '-'}℃<br/>心率: ${p.data?.[1] ?? '-'}<br/>脉搏: ${p.data?.[1] ?? '-'}`;
}},
xAxis: { type: 'category', data: timeAxis, axisLabel: { rotate: 30 } },
yAxis: { type: 'value', name: '数值', splitLine: { show: true } },
series: [
{ name: '体温', type: 'line', data: tempData, symbol: 'x', itemStyle: { color: '#1E90FF' }, connectNulls: false, lineStyle: { color: '#1E90FF', width: 2 } },
{ name: '心率', type: 'line', data: hrData, symbol: 'circle', symbolSize: 8, itemStyle: { color: '#FF4500' }, connectNulls: false, lineStyle: { color: '#FF4500', width: 2 } },
{ name: '脉搏', type: 'line', data: pulseData, symbol: 'circle', symbolSize: 10, itemStyle: { color: '#FF4500' }, connectNulls: false, lineStyle: { color: '#FF4500', width: 2 } }
]
};
chartInstance.setOption(option, true);
} catch (e) {
console.error('加载体征数据失败', e);
}
};
const openDialog = () => {
form.value = { measureTime: '', temperature: null, heartRate: null, pulse: null };
dialogVisible.value = true;
};
const handleSave = async () => {
if (!form.value.measureTime) return ElMessage.warning('请选择测量时间');
try {
await axios.post('/api/vital-signs', { patientId: selectedPatientId.value, ...form.value });
ElMessage.success('保存成功');
dialogVisible.value = false;
// 核心修复:保存成功后自动触发数据重载与图表重绘,无需手动刷新
await nextTick();
loadChartData();
} catch (e) {
ElMessage.error('保存失败');
}
};
onMounted(() => {
initChart();
selectedPatientId.value = 123;
loadChartData();
});
</script>
<style scoped>
.temperature-sheet-wrapper { padding: 16px; }
.header-actions { display: flex; gap: 12px; margin-bottom: 16px; }
.chart-container { width: 100%; height: 400px; border: 1px solid #eee; }
</style>

View File

@@ -1,71 +0,0 @@
<template>
<div class="order-verification-container">
<el-tabs v-model="activeTab" @tab-click="handleTabChange">
<el-tab-pane label="已校对" name="verified">
<el-table :data="orderList" border style="width: 100%">
<el-table-column prop="orderName" label="医嘱名称" min-width="180" />
<el-table-column prop="dispensingStatus" label="发药状态" width="100" />
<el-table-column prop="executionStatus" label="执行状态" width="100" />
<el-table-column prop="billingStatus" label="计费状态" width="100" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<!-- 修复 Bug #505已发药或已执行状态下退回按钮置灰不可点击 -->
<el-button
type="primary"
size="small"
:disabled="row.dispensingStatus === '已发药' || row.executionStatus === '已执行'"
@click="handleReturn(row)"
>
退回
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { returnOrderApi, getVerifiedOrdersApi } from '@/api/inpatient/order';
const activeTab = ref('verified');
const orderList = ref([]);
const handleTabChange = () => {
loadOrders();
};
const loadOrders = async () => {
try {
const res = await getVerifiedOrdersApi();
orderList.value = res.data || [];
} catch (error) {
ElMessage.error('加载医嘱列表失败');
}
};
const handleReturn = async (row) => {
try {
await returnOrderApi(row.id);
ElMessage.success('退回成功');
loadOrders();
} catch (error) {
// 后端拦截异常会在此处捕获并展示
ElMessage.error(error.message || '退回失败');
}
};
onMounted(() => {
loadOrders();
});
</script>
<style scoped>
.order-verification-container {
padding: 20px;
background: #fff;
}
</style>

View File

@@ -1,116 +0,0 @@
<template>
<div class="order-verify-container">
<el-card>
<template #header>
<div class="card-header">
<span>医嘱校对 - 已校对</span>
<div class="header-actions">
<el-tooltip
:content="returnTooltip"
placement="top"
:disabled="!isReturnDisabled"
>
<el-button
type="danger"
:disabled="isReturnDisabled"
@click="handleBatchReturn"
data-cy="batch-return-btn"
>
退回
</el-button>
</el-tooltip>
</div>
</div>
</template>
<el-table
:data="verifiedOrders"
@selection-change="handleSelectionChange"
row-key="id"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="drugName" label="药品名称" />
<el-table-column prop="execStatus" label="执行状态" />
<el-table-column prop="dispenseStatus" label="发药状态" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-tooltip
:content="row.dispenseStatus === 'DISPENSED' || row.execStatus === 'EXECUTED'
? '该药品已由药房发放,请先执行退药处理,不可直接退回'
: ''"
placement="top"
:disabled="!(row.dispenseStatus === 'DISPENSED' || row.execStatus === 'EXECUTED')"
>
<el-button
type="danger"
link
:disabled="row.dispenseStatus === 'DISPENSED' || row.execStatus === 'EXECUTED'"
@click="handleSingleReturn(row.id)"
:data-cy="`return-btn-${row.id}`"
>
退回
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const verifiedOrders = ref([])
const selectedOrders = ref([])
const isReturnDisabled = computed(() => {
if (selectedOrders.value.length === 0) return true
// 只要选中列表中有一条已发药或已执行,批量按钮即置灰
return selectedOrders.value.some(
(o) => o.dispenseStatus === 'DISPENSED' || o.execStatus === 'EXECUTED'
)
})
const returnTooltip = computed(() => {
return isReturnDisabled.value ? '该药品已由药房发放,请先执行退药处理,不可直接退回' : ''
})
onMounted(async () => {
const res = await axios.get('/api/inpatient/orders/verified')
verifiedOrders.value = res.data
})
const handleSelectionChange = (selection) => {
selectedOrders.value = selection
}
const handleSingleReturn = async (orderId) => {
await doReturn([orderId])
}
const handleBatchReturn = async () => {
const ids = selectedOrders.value.map((o) => o.id)
await doReturn(ids)
}
const doReturn = async (ids) => {
try {
await axios.post('/api/inpatient/orders/return', ids)
ElMessage.success('退回成功')
// 刷新列表
const res = await axios.get('/api/inpatient/orders/verified')
verifiedOrders.value = res.data
selectedOrders.value = []
} catch (err) {
ElMessage.error(err.response?.data?.message || '退回失败')
}
}
</script>
<style scoped>
.order-verify-container { padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
</style>

View File

@@ -1,124 +0,0 @@
<template>
<div class="order-verify-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="title">住院护士站 - 医嘱校对</span>
<el-button type="primary" @click="handleQuery">刷新</el-button>
</div>
</template>
<el-table
:data="verifyList"
v-loading="loading"
border
stripe
style="width: 100%"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
>
<el-table-column prop="orderNo" label="医嘱号" width="130" fixed />
<el-table-column prop="itemName" label="医嘱内容" min-width="180" show-overflow-tooltip />
<el-table-column prop="startTime" label="开始时间" width="160" />
<el-table-column prop="stopTime" label="停嘱时间" width="160" />
<el-table-column prop="prescribingDoctor" label="开嘱医生" width="100" />
<el-table-column prop="stoppingDoctor" label="停嘱医生" width="100" />
<el-table-column prop="singleDose" label="单次剂量" width="100" align="center" />
<el-table-column prop="totalDose" label="总量" width="100" align="center" />
<el-table-column prop="totalAmount" label="总金额" width="100" align="center" />
<el-table-column prop="frequency" label="频次" width="100" align="center" />
<el-table-column prop="usage" label="用法" width="120" align="center" />
<el-table-column prop="diagnosis" label="诊断" width="140" show-overflow-tooltip />
<el-table-column label="注射药品" width="90" align="center">
<template #default="{ row }">
<el-tag v-if="row.isInjection" type="primary" size="small"></el-tag>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="皮试" width="110" align="center">
<template #default="{ row }">
<el-tag
v-if="row.skinTestRequired"
type="danger"
effect="dark"
class="skin-test-alert"
>
需皮试
</el-tag>
<span v-else class="text-muted">{{ row.skinTestStatus || '无需皮试' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="handleVerify(row)">校对</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="handleQuery"
@current-change="handleQuery"
class="pagination"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getVerifyOrderList } from '@/api/inpatient/nurse/order'
const loading = ref(false)
const verifyList = ref([])
const total = ref(0)
const queryParams = reactive({
patientId: null, // 实际应从路由参数或全局患者上下文获取
pageNum: 1,
pageSize: 20
})
const getList = async () => {
loading.value = true
try {
const res = await getVerifyOrderList(queryParams)
verifyList.value = res.data?.rows || []
total.value = res.data?.total || 0
} catch (error) {
console.error('获取医嘱校对列表失败:', error)
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
const handleVerify = (row) => {
// 触发校对业务逻辑
console.log('执行医嘱校对:', row.orderNo)
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.order-verify-container { padding: 16px; background: #f0f2f5; min-height: 100vh; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.title { font-size: 16px; font-weight: 600; color: #303133; }
.pagination { margin-top: 16px; display: flex; justify-content: flex-end; }
.text-muted { color: #909399; font-size: 12px; }
.skin-test-alert { font-weight: bold; animation: pulse 2s infinite; }
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}
</style>

View File

@@ -1,68 +0,0 @@
<template>
<div class="temperature-chart">
<el-card>
<div ref="chartRef" style="height: 400px;"></div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts';
import { getVitalSignsApi } from '@/api/inpatient/vitalSign';
import { ElMessage } from 'element-plus';
const props = defineProps({
registrationId: {
type: Number,
required: true,
},
});
const chartRef = ref(null);
let chartInstance = null;
const loadData = async () => {
try {
const res = await getVitalSignsApi({ registrationId: props.registrationId });
const data = res.data || [];
const times = data.map(item => item.recordTime);
const temps = data.map(item => item.temperature);
const option = {
title: { text: '体温趋势' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: times },
yAxis: { type: 'value', name: '℃' },
series: [
{
name: '体温',
type: 'line',
data: temps,
smooth: true,
itemStyle: { color: '#ff5722' },
},
],
};
chartInstance.setOption(option);
} catch (e) {
ElMessage.error('加载体温数据失败');
}
};
onMounted(() => {
chartInstance = echarts.init(chartRef.value);
loadData();
});
watch(() => props.registrationId, () => {
loadData();
});
</script>
<style scoped>
.temperature-chart {
width: 100%;
}
</style>

View File

@@ -1,166 +0,0 @@
<template>
<div class="temperature-chart-container">
<div class="chart-wrapper" ref="chartRef" data-cy="chart-area"></div>
<div class="table-wrapper">
<table class="vitalsign-table" data-cy="vitalsign-table">
<thead>
<tr>
<th>时间</th>
<th>体温()</th>
<th>脉搏(/)</th>
<th>心率(/)</th>
</tr>
</thead>
<tbody>
<tr v-for="row in tableData" :key="row.timeKey">
<td>{{ row.timeLabel }}</td>
<td :data-cy="`table-cell-${row.timeKey}-temp`">{{ row.temp ?? '-' }}</td>
<td :data-cy="`table-cell-${row.timeKey}-pulse`">{{ row.pulse ?? '-' }}</td>
<td :data-cy="`table-cell-${row.timeKey}-hr`">{{ row.hr ?? '-' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getVitalSignsByPatient } from '@/api/inpatient/vitalsign'
const props = defineProps({
patientId: { type: String, required: true }
})
const chartRef = ref(null)
let chartInstance = null
const tableData = ref([])
const rawData = ref([])
const fetchData = async () => {
try {
const res = await getVitalSignsByPatient(props.patientId)
rawData.value = res.data || []
processChartData()
processTableData()
} catch (e) {
console.error('Failed to fetch vital signs:', e)
}
}
const processTableData = () => {
const timeSlots = ['02:00', '06:00', '10:00', '14:00', '18:00', '22:00']
const grouped = {}
rawData.value.forEach(item => {
const key = `${item.recordDate} ${item.recordTime}`
grouped[key] = item
})
tableData.value = timeSlots.map(slot => {
const date = rawData.value[0]?.recordDate || new Date().toISOString().split('T')[0]
const fullKey = `${date} ${slot}`
const item = grouped[fullKey] || {}
return {
timeKey: `${date}-${slot.replace(':', '')}`,
timeLabel: `${date.slice(5)} ${slot.slice(0, 2)}`,
temp: item.temperature,
pulse: item.pulse,
hr: item.heartRate
}
})
}
const processChartData = () => {
if (!chartInstance) return
const dates = [...new Set(rawData.value.map(d => d.recordDate))].sort()
const xAxisData = []
const tempData = []
const pulseData = []
const hrData = []
dates.forEach(date => {
['02:00', '06:00', '10:00', '14:00', '18:00', '22:00'].forEach(time => {
xAxisData.push(`${date.slice(5)} ${time.slice(0, 2)}`)
const record = rawData.value.find(r => r.recordDate === date && r.recordTime === time)
tempData.push(record ? record.temperature : null)
pulseData.push(record ? record.pulse : null)
hrData.push(record ? record.heartRate : null)
})
})
const option = {
tooltip: { trigger: 'axis' },
grid: { top: 40, bottom: 40, left: 50, right: 30 },
xAxis: { type: 'category', data: xAxisData, axisLabel: { rotate: 30 } },
yAxis: [
{ type: 'value', name: '体温(℃)', min: 35, max: 42, splitNumber: 7 },
{ type: 'value', name: '脉搏/心率', min: 0, max: 180, splitNumber: 9, position: 'right' }
],
series: [
{
name: '体温',
type: 'line',
yAxisIndex: 0,
data: tempData,
symbol: 'x',
symbolSize: 8,
itemStyle: { color: '#1890ff' },
lineStyle: { color: '#1890ff', width: 2 },
connectNulls: false
},
{
name: '脉搏',
type: 'line',
yAxisIndex: 1,
data: pulseData,
symbol: 'circle',
symbolSize: 8,
itemStyle: { color: '#ff4d4f' },
lineStyle: { color: '#ff4d4f', width: 2 },
connectNulls: false
},
{
name: '心率',
type: 'line',
yAxisIndex: 1,
data: hrData,
symbol: 'emptyCircle',
symbolSize: 8,
itemStyle: { color: '#ff4d4f', borderColor: '#ff4d4f', borderWidth: 2 },
lineStyle: { color: '#ff4d4f', width: 2 },
connectNulls: false
}
]
}
chartInstance.setOption(option, true)
}
const initChart = () => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value)
window.addEventListener('resize', chartInstance.resize)
}
}
onMounted(() => {
initChart()
fetchData()
})
onUnmounted(() => {
window.removeEventListener('resize', chartInstance?.resize)
chartInstance?.dispose()
})
defineExpose({ refresh: fetchData })
</script>
<style scoped>
.temperature-chart-container { display: flex; flex-direction: column; gap: 16px; }
.chart-wrapper { height: 400px; width: 100%; }
.vitalsign-table { width: 100%; border-collapse: collapse; }
.vitalsign-table th, .vitalsign-table td { border: 1px solid #ddd; padding: 8px; text-align: center; }
</style>

View File

@@ -1,154 +0,0 @@
<template>
<div class="temperature-sheet-container">
<div class="chart-wrapper">
<div ref="chartRef" class="temperature-chart"></div>
</div>
<div class="table-wrapper">
<el-table :data="tableData" border stripe style="width: 100%" size="small">
<el-table-column prop="measureTime" label="测量时间" width="160" align="center" />
<el-table-column prop="temperature" label="体温(℃)" width="100" align="center">
<template #default="{ row }">{{ row.temperature ? row.temperature.toFixed(1) : '-' }}</template>
</el-table-column>
<el-table-column prop="pulse" label="脉搏(次/分)" width="110" align="center" />
<el-table-column prop="heartRate" label="心率(次/分)" width="110" align="center" />
</el-table>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts';
import { getPatientVitals } from '@/api/inpatient/vitalsign';
const props = defineProps({
patientId: { type: [String, Number], required: true }
});
const chartRef = ref(null);
let chartInstance = null;
const tableData = ref([]);
// 初始化图表
const initChart = () => {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
window.addEventListener('resize', handleResize);
};
const handleResize = () => chartInstance?.resize();
// 核心修复:数据拉取与渲染逻辑
const refreshData = async () => {
try {
const res = await getPatientVitals({ patientId: props.patientId });
if (res.code === 200 && Array.isArray(res.data)) {
// 按时间升序排序
const sorted = res.data.sort((a, b) => new Date(a.measureTime) - new Date(b.measureTime));
tableData.value = sorted; // 同步表格区
renderChart(sorted);
}
} catch (err) {
console.error('[体温单] 数据加载失败:', err);
}
};
// 渲染图表(严格遵循医疗绘图规范)
const renderChart = (data) => {
if (!chartInstance) return;
const times = data.map(d => d.measureTime);
// 处理断点连线:缺失值映射为 null配合 connectNulls: false 实现自动断开
const mapSeries = (key) => data.map(d => (d[key] != null ? d[key] : null));
const option = {
tooltip: { trigger: 'axis', formatter: '{b}<br/>{a}: {c}' },
grid: { top: 30, bottom: 30, left: 40, right: 20, containLabel: true },
xAxis: {
type: 'category',
data: times,
axisLabel: { rotate: 30, fontSize: 11 }
},
yAxis: {
type: 'value',
min: 35,
max: 42,
splitNumber: 7,
axisLine: { show: false },
splitLine: { lineStyle: { type: 'dashed' } }
},
series: [
{
name: '体温',
type: 'line',
data: mapSeries('temperature'),
symbol: 'x',
symbolSize: 8,
lineStyle: { color: '#1890ff', width: 2 },
itemStyle: { color: '#1890ff' },
connectNulls: false // 医疗规范:数据缺失必须断开
},
{
name: '脉搏',
type: 'line',
data: mapSeries('pulse'),
symbol: 'circle', // ● 实心圆
symbolSize: 8,
lineStyle: { color: '#ff4d4f', width: 2 },
itemStyle: { color: '#ff4d4f' },
connectNulls: false
},
{
name: '心率',
type: 'line',
data: mapSeries('heartRate'),
symbol: 'emptyCircle', // ○ 空心圆
symbolSize: 8,
lineStyle: { color: '#ff4d4f', width: 2 },
itemStyle: { color: '#ff4d4f' },
connectNulls: false
}
]
};
// 重叠处理ECharts 默认按 series 顺序绘制,心率(空心)在脉搏(实心)之上,符合临床视觉习惯
chartInstance.setOption(option, true);
};
// 暴露方法供父组件/弹窗在保存成功后调用
defineExpose({ refreshData });
onMounted(() => {
initChart();
refreshData();
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
});
</script>
<style scoped>
.temperature-sheet-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 10px;
background: #fff;
}
.chart-wrapper {
flex: 1;
min-height: 300px;
margin-bottom: 10px;
}
.temperature-chart {
width: 100%;
height: 100%;
}
.table-wrapper {
height: 200px;
overflow-y: auto;
}
</style>

View File

@@ -1,210 +0,0 @@
<template>
<div class="lab-request-wrapper">
<el-dialog v-model="visible" title="检验申请单" width="900px" :close-on-click-modal="false">
<!-- 顶部核心质控字段区域 -->
<div class="qc-fields-container">
<el-form :model="formData" label-width="100px" class="qc-form">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="申请类型">
<el-radio-group v-model="formData.applicationType" data-cy="application-type">
<el-radio label="1">普通</el-radio>
<el-radio label="2">急诊</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="标本类型">
<el-input v-model="formData.specimenType" placeholder="自动带出或手动选择" data-cy="specimen-type" readonly>
<template #append>
<el-button @click="openSpecimenDict">选择</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="执行时间">
<el-date-picker
v-model="formData.executionTime"
type="datetime"
placeholder="选择执行时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
data-cy="execution-time"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发往科室">
<el-select v-model="formData.targetDept" placeholder="请选择执行科室" style="width: 100%">
<el-option label="检验科" value="LAB" />
<el-option label="病理科" value="PATH" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="临床诊断">
<el-input v-model="formData.diagnosis" placeholder="请输入诊断" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<el-divider />
<!-- 左侧检验项目分类 -->
<div class="panel category-panel">
<div class="panel-title">检验项目分类</div>
<el-tree :data="categoryTree" node-key="id" highlight-current @node-click="handleCategoryClick" />
</div>
<!-- 中间检验项目列表 -->
<div class="panel item-panel">
<div class="panel-title">检验项目</div>
<el-checkbox-group v-model="selectedItemIds" @change="onItemSelectChange">
<el-checkbox
v-for="item in currentItems"
:key="item.id"
:label="item.id"
class="item-checkbox"
>
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 右侧已选择区域 -->
<div class="panel selected-panel">
<div class="panel-title">已选择</div>
<div class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-item">
<span>{{ item.name }}</span>
<el-tag size="small" type="info">{{ item.specimenType || '未配置' }}</el-tag>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无已选项目" />
</div>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" data-cy="save-btn">确认申请</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import dayjs from 'dayjs';
const visible = ref(false);
const selectedItemIds = ref([]);
const categoryTree = ref([]);
const currentItems = ref([]);
const itemList = ref([]);
const formData = reactive({
applicationType: '1', // 1:普通 2:急诊
specimenType: '',
executionTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
targetDept: '',
diagnosis: ''
});
const selectedItems = computed(() => {
return itemList.value.filter(item => selectedItemIds.value.includes(item.id));
});
// 模拟字典/接口获取项目数据
const fetchLabItems = () => {
// 实际应调用后端接口
itemList.value = [
{ id: 1, name: '血常规', specimenType: '血液' },
{ id: 2, name: '尿常规', specimenType: '尿液' },
{ id: 3, name: '肝功能', specimenType: '血液' },
{ id: 4, name: '大便常规', specimenType: '粪便' }
];
currentItems.value = itemList.value;
};
const handleCategoryClick = (node) => {
// 实际根据分类过滤
currentItems.value = itemList.value;
};
// 核心联动逻辑:勾选项目后自动带出标本类型
const onItemSelectChange = (ids) => {
selectedItemIds.value = ids;
const selected = selectedItems.value;
if (selected.length > 0) {
// 取第一个项目的标本类型作为默认值,若存在多个不同标本可提示或取交集
formData.specimenType = selected[0].specimenType || '';
} else {
formData.specimenType = '';
}
};
const openSpecimenDict = () => {
ElMessageBox.prompt('请输入或选择标本类型', '标本类型', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '如:血液、尿液、脑脊液等'
}).then(({ value }) => {
formData.specimenType = value;
}).catch(() => {});
};
const handleSubmit = () => {
// 校验执行时间不可早于当前时间
const execTime = dayjs(formData.executionTime);
const now = dayjs();
if (execTime.isBefore(now)) {
ElMessageBox.alert('执行时间不可早于当前时间', '提示', { type: 'warning' });
return;
}
if (!formData.targetDept) {
ElMessage.warning('请选择发往科室');
return;
}
if (selectedItemIds.value.length === 0) {
ElMessage.warning('请至少选择一项检验项目');
return;
}
// 组装提交数据
const payload = {
applicationType: formData.applicationType,
specimenType: formData.specimenType,
executionTime: formData.executionTime,
targetDept: formData.targetDept,
diagnosis: formData.diagnosis,
itemIds: selectedItemIds.value
};
console.log('提交检验申请:', payload);
ElMessage.success('申请单已提交');
visible.value = false;
};
onMounted(() => {
fetchLabItems();
});
defineExpose({ open: () => { visible.value = true; formData.executionTime = dayjs().format('YYYY-MM-DD HH:mm:ss'); } });
</script>
<style scoped>
.lab-request-wrapper { padding: 10px; }
.qc-fields-container { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; }
.panel { display: inline-block; vertical-align: top; width: 30%; margin: 0 1.5%; border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; height: 400px; overflow-y: auto; }
.panel-title { font-weight: bold; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
.item-checkbox { display: block; margin: 5px 0; }
.selected-item { display: flex; justify-content: space-between; align-items: center; padding: 5px; background: #f0f9eb; margin-bottom: 5px; border-radius: 4px; }
</style>

View File

@@ -1,119 +0,0 @@
<template>
<div class="order-verification-container">
<el-card shadow="never">
<el-tabs v-model="activeTab" @tab-click="handleTabChange">
<el-tab-pane label="待校对" name="pending" />
<el-tab-pane label="已校对" name="verified" />
<el-tab-pane label="已退回" name="returned" />
</el-tabs>
<el-table
v-loading="loading"
:data="orderList"
border
style="width: 100%; margin-top: 16px"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="drugName" label="药品/项目名称" min-width="180" />
<el-table-column prop="dosage" label="剂量" width="100" />
<el-table-column prop="executeStatus" label="执行状态" width="100">
<template #default="{ row }">
<el-tag :type="row.executeStatus === '已执行' ? 'success' : 'info'">
{{ row.executeStatus || '未执行' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="dispenseStatus" label="发药状态" width="100">
<template #default="{ row }">
<el-tag :type="row.dispenseStatus === '已发药' ? 'warning' : 'info'">
{{ row.dispenseStatus || '未发药' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="医嘱状态" width="100" />
</el-table>
<div class="toolbar" style="margin-top: 16px; display: flex; justify-content: flex-end; gap: 12px;">
<el-button
type="primary"
:disabled="isReturnDisabled"
@click="handleReturn"
>
退回
</el-button>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getOrders, returnOrder } from '@/api/nurse/order'
const activeTab = ref('verified')
const loading = ref(false)
const orderList = ref([])
const selectedOrders = ref([])
onMounted(() => {
fetchOrders()
})
const handleTabChange = () => {
fetchOrders()
}
const fetchOrders = async () => {
loading.value = true
try {
const res = await getOrders({ status: activeTab.value })
orderList.value = res.data || []
} finally {
loading.value = false
}
}
const handleSelectionChange = (val) => {
selectedOrders.value = val
}
// Bug #505 修复:计算退回按钮是否禁用
// 核心约束:已执行或已发药的药品医嘱不可直接退回
const isReturnDisabled = computed(() => {
if (selectedOrders.value.length === 0) return true
return selectedOrders.value.some(order => {
return order.executeStatus === '已执行' || order.dispenseStatus === '已发药'
})
})
const handleReturn = async () => {
if (isReturnDisabled.value) return
try {
await ElMessageBox.confirm('确认退回选中的医嘱吗?', '操作确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const ids = selectedOrders.value.map(o => o.id)
await returnOrder(ids)
ElMessage.success('退回成功')
fetchOrders()
} catch (err) {
if (err !== 'cancel') {
// 兜底拦截后端返回的业务异常提示
ElMessage.error(err.message || '退回失败')
}
}
}
</script>
<style scoped>
.order-verification-container {
padding: 16px;
}
</style>

View File

@@ -1,124 +0,0 @@
<template>
<div class="order-verify-container">
<el-card class="header-card">
<el-tabs v-model="activeTab" @tab-click="handleTabChange">
<el-tab-pane label="待校对" name="pending" />
<el-tab-pane label="已校对" name="verified" />
<el-tab-pane label="已退回" name="returned" />
</el-tabs>
</el-card>
<el-card class="table-card">
<el-table
:data="orderList"
style="width: 100%"
@selection-change="handleSelectionChange"
v-loading="loading"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="order_no" label="医嘱号" width="120" />
<el-table-column prop="drug_name" label="药品名称" />
<el-table-column prop="exec_status" label="执行状态" width="100" />
<el-table-column prop="dispensing_status" label="发药状态" width="100">
<template #default="{ row }">
<el-tag :type="getDispensingTagType(row.dispensing_status)">
{{ formatDispensingStatus(row.dispensing_status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<div class="action-bar">
<el-button
type="primary"
:disabled="isReturnBtnDisabled"
@click="handleReturn"
>
退回
</el-button>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import axios from 'axios';
const activeTab = ref('verified');
const orderList = ref([]);
const selectedOrders = ref([]);
const loading = ref(false);
// Bug #505 修复:计算退回按钮是否置灰
const isReturnBtnDisabled = computed(() => {
if (selectedOrders.value.length === 0) return true;
// 若任意选中项已发药,则禁用按钮
return selectedOrders.value.some(
(order) => order.dispensing_status === 'DISPENSED'
);
});
const handleTabChange = async (tab) => {
loading.value = true;
try {
const res = await axios.get(`/api/nurse/orders?status=${tab.props.name}`);
orderList.value = res.data;
} catch (e) {
ElMessage.error('加载医嘱列表失败');
} finally {
loading.value = false;
}
};
const handleSelectionChange = (selection) => {
selectedOrders.value = selection;
};
const handleReturn = async () => {
if (selectedOrders.value.length === 0) return;
try {
await ElMessageBox.confirm('确定要退回选中的医嘱吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
const ids = selectedOrders.value.map(o => o.id);
await axios.post('/api/nurse/orders/return', { orderIds: ids });
ElMessage.success('退回成功');
handleTabChange({ props: { name: activeTab.value } });
} catch (err) {
if (err.response && err.response.data) {
ElMessage.error(err.response.data.message || err.response.data);
} else if (err !== 'cancel') {
ElMessage.error('退回失败');
}
}
};
const getDispensingTagType = (status) => {
switch (status) {
case 'DISPENSED': return 'success';
case 'PENDING': return 'warning';
default: return 'info';
}
};
const formatDispensingStatus = (status) => {
const map = { 'DISPENSED': '已发药', 'PENDING': '待发药', 'RETURNED': '已退药' };
return map[status] || status || '未知';
};
onMounted(() => {
handleTabChange({ props: { name: activeTab.value } });
});
</script>
<style scoped>
.order-verify-container { padding: 20px; }
.header-card { margin-bottom: 20px; }
.action-bar { margin-top: 20px; display: flex; justify-content: flex-end; }
</style>

View File

@@ -1,153 +0,0 @@
<template>
<div class="temperature-chart-wrapper">
<div class="chart-header">
<span class="patient-info">患者{{ currentPatient?.name }} ({{ currentPatient?.bedNo }})</span>
<el-button type="primary" @click="openAddDialog">新增体征</el-button>
</div>
<!-- 图表区 -->
<div ref="chartRef" class="temperature-chart-container" style="height: 400px; width: 100%;"></div>
<!-- 表格区 -->
<el-table :data="tableData" class="vital-sign-table" border style="margin-top: 16px;">
<el-table-column prop="recordTime" label="记录时间" width="180" />
<el-table-column prop="temperature" label="体温(℃)" width="120" />
<el-table-column prop="heartRate" label="心率(次/分)" width="120" />
<el-table-column prop="pulse" label="脉搏(次/分)" width="120" />
</el-table>
<!-- 录入弹窗 -->
<el-dialog v-model="dialogVisible" title="录入生命体征" width="500px">
<el-form :model="form" label-width="80px">
<el-form-item label="日期时间">
<el-date-picker v-model="form.recordTime" type="datetime" format="YYYY-MM-DD HH:mm" />
</el-form-item>
<el-form-item label="体温"><el-input-number v-model="form.temperature" :precision="1" :step="0.1" /></el-form-item>
<el-form-item label="心率"><el-input-number v-model="form.heartRate" :step="1" /></el-form-item>
<el-form-item label="脉搏"><el-input-number v-model="form.pulse" :step="1" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
import { ElMessage } from 'element-plus'
import { getVitalSignsApi, saveVitalSignApi } from '@/api/nurse/vitalSign'
const props = defineProps({ patientId: { type: String, required: true } })
const currentPatient = ref({ name: '张三', bedNo: '123' })
const chartRef = ref(null)
let chartInstance = null
const dialogVisible = ref(false)
const form = ref({ recordTime: null, temperature: null, heartRate: null, pulse: null })
const tableData = ref([])
// 初始化图表
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
window.addEventListener('resize', handleResize)
}
const handleResize = () => chartInstance?.resize()
// 核心修复:数据加载与渲染映射
const loadChartData = async () => {
try {
const res = await getVitalSignsApi({ patientId: props.patientId })
const rawData = res.data || []
// 按时间排序
rawData.sort((a, b) => new Date(a.recordTime) - new Date(b.recordTime))
tableData.value = rawData
const times = rawData.map(r => r.recordTime)
const tempData = rawData.map(r => r.temperature ?? null)
const hrData = rawData.map(r => r.heartRate ?? null)
const pulseData = rawData.map(r => r.pulse ?? null)
chartInstance.setOption({
tooltip: { trigger: 'axis' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: times, axisLabel: { rotate: 30 } },
yAxis: { type: 'value', name: '数值' },
series: [
{
name: '体温',
type: 'line',
data: tempData,
symbol: 'x',
symbolSize: 8,
itemStyle: { color: '#1890ff' },
lineStyle: { color: '#1890ff', width: 2 },
connectNulls: false // 严格断点逻辑
},
{
name: '心率',
type: 'line',
data: hrData,
symbol: 'emptyCircle', // ○
symbolSize: 8,
itemStyle: { color: '#ff4d4f' },
lineStyle: { color: '#ff4d4f', width: 2 },
connectNulls: false
},
{
name: '脉搏',
type: 'line',
data: pulseData,
symbol: 'circle', // ●
symbolSize: 8,
itemStyle: { color: '#ff4d4f' },
lineStyle: { color: '#ff4d4f', width: 2 },
connectNulls: false
}
]
})
} catch (e) {
ElMessage.error('加载体征数据失败')
}
}
const openAddDialog = () => {
form.value = { recordTime: null, temperature: null, heartRate: null, pulse: null }
dialogVisible.value = true
}
// 核心修复:保存成功后自动触发图表与表格刷新
const handleSave = async () => {
try {
await saveVitalSignApi({ ...form.value, patientId: props.patientId })
ElMessage.success('保存成功')
dialogVisible.value = false
await loadChartData() // 自动刷新,无需手动操作
} catch (e) {
ElMessage.error('保存失败')
}
}
onMounted(() => {
nextTick(() => {
initChart()
loadChartData()
})
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
chartInstance?.dispose()
})
</script>
<style scoped>
.temperature-chart-wrapper { padding: 16px; background: #fff; }
.chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.patient-info { font-weight: bold; font-size: 16px; }
</style>

View File

@@ -1,91 +0,0 @@
<template>
<div class="order-verification-container">
<el-card>
<template #header>
<div class="card-header">
<span>医嘱校对</span>
<el-select
v-model="selectedPatientId"
placeholder="选择患者"
@change="fetchOrders"
class="patient-select"
style="width: 220px; margin-left: auto;"
>
<el-option v-for="p in patientList" :key="p.id" :label="`${p.bedNo}床 - ${p.name}`" :value="p.id" />
</el-select>
</div>
</template>
<el-table :data="tableData" border v-loading="loading" style="width: 100%" class="verification-table">
<el-table-column prop="startTime" label="开始时间" width="160" />
<el-table-column prop="singleDose" label="单次剂量" width="100" />
<el-table-column prop="totalAmount" label="总量" width="100" />
<el-table-column prop="totalCost" label="总金额" width="100" />
<el-table-column prop="frequency" label="频次" width="90" />
<el-table-column prop="usage" label="用法" width="100" />
<el-table-column prop="orderingDoctor" label="开嘱医生" width="100" />
<el-table-column prop="stopTime" label="停嘱时间" width="160" />
<el-table-column prop="stopDoctor" label="停嘱医生" width="100" />
<el-table-column prop="injectionDrug" label="注射药品" width="120" />
<el-table-column label="皮试" width="100">
<template #default="{ row }">
<el-tag v-if="row.isSkinTest" type="danger" effect="dark">需皮试</el-tag>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column prop="diagnosis" label="诊断" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleVerify(row)">校对</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getVerificationOrdersApi } from '@/api/nursestation/order'
import { getPatientListApi } from '@/api/nursestation/patient'
const loading = ref(false)
const tableData = ref([])
const selectedPatientId = ref(null)
const patientList = ref([])
onMounted(async () => {
try {
const res = await getPatientListApi()
patientList.value = res.data || []
} catch (e) {
console.error('获取患者列表失败', e)
}
})
const fetchOrders = async () => {
if (!selectedPatientId.value) return
loading.value = true
try {
const res = await getVerificationOrdersApi(selectedPatientId.value)
tableData.value = res.data || []
} finally {
loading.value = false
}
}
const handleVerify = (row) => {
// 触发校对业务逻辑
console.log('执行医嘱校对:', row.id)
}
</script>
<style scoped>
.card-header {
display: flex;
align-items: center;
}
.text-muted {
color: #909399;
}
</style>

View File

@@ -1,102 +0,0 @@
<template>
<div class="appointment-container">
<el-card class="filter-card">
<el-form :inline="true" class="query-form">
<el-form-item label="预约状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 150px;">
<el-option label="可预约" :value="0" />
<el-option label="已预约" :value="1" />
<el-option label="已就诊" :value="2" />
<el-option label="已取消" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>门诊预约挂号列表</span>
</div>
</template>
<el-table :data="appointmentList" border v-loading="loading" style="width: 100%">
<el-table-column prop="slotTime" label="就诊时间" width="180" />
<el-table-column prop="deptName" label="科室" width="120" />
<el-table-column prop="doctorName" label="医生" width="120" />
<el-table-column prop="patientName" label="患者" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button v-if="row.status === 1" type="danger" size="small" @click="handleCancel(row)">取消预约</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { getAppointmentList, cancelAppointment } from '@/api/outpatient';
import { ElMessage, ElMessageBox } from 'element-plus';
const queryParams = reactive({ status: null });
const appointmentList = ref([]);
const loading = ref(false);
// 修复 Bug #570移除“已锁定”映射严格对应后端 ScheduleSlotStatus 枚举
const getStatusLabel = (status) => {
const map = { 0: '可预约', 1: '已预约', 2: '已就诊', 3: '已取消' };
return map[status] || '未知';
};
const getStatusType = (status) => {
const map = { 0: 'success', 1: 'primary', 2: 'info', 3: 'danger' };
return map[status] || 'info';
};
const handleQuery = async () => {
loading.value = true;
try {
const res = await getAppointmentList(queryParams);
appointmentList.value = res.data || [];
} catch (error) {
ElMessage.error('查询失败');
} finally {
loading.value = false;
}
};
const resetQuery = () => {
queryParams.status = null;
handleQuery();
};
const handleCancel = async (row) => {
try {
await ElMessageBox.confirm('确定取消该预约吗?', '提示', { type: 'warning' });
await cancelAppointment(row.id);
ElMessage.success('取消成功');
handleQuery();
} catch (error) {
// 用户取消或请求失败
}
};
onMounted(() => handleQuery());
</script>
<style scoped>
.appointment-container { padding: 20px; }
.filter-card { margin-bottom: 20px; }
.table-card { margin-bottom: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
</style>

View File

@@ -1,104 +0,0 @@
<template>
<div class="appointment-container">
<el-card class="filter-card">
<el-form :inline="true" class="query-form">
<el-form-item label="预约状态" class="status-filter">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="可预约" :value="0" />
<el-option label="已预约" :value="1" />
<el-option label="已取消" :value="2" />
<el-option label="已就诊" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>门诊预约挂号列表</span>
</div>
</template>
<el-table :data="slotList" border v-loading="loading">
<el-table-column prop="slotNo" label="号源编号" width="120" />
<el-table-column prop="doctorName" label="医生" width="120" />
<el-table-column prop="scheduleTime" label="就诊时间" width="180" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ getStatusName(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button
v-if="row.status === 0"
type="primary"
size="small"
@click="handleBook(row.id)"
>预约</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { getSlotList, bookSlot } from '@/api/outpatient';
const loading = ref(false);
const slotList = ref([]);
const queryParams = reactive({ status: null });
const statusMap = {
0: { name: '可预约', type: 'success' },
1: { name: '已预约', type: 'primary' },
2: { name: '已取消', type: 'info' },
3: { name: '已就诊', type: 'warning' }
};
const getStatusName = (code) => statusMap[code]?.name || '未知';
const getStatusType = (code) => statusMap[code]?.type || 'info';
const fetchList = async () => {
loading.value = true;
try {
const res = await getSlotList(queryParams);
slotList.value = res.data || [];
} catch (e) {
ElMessage.error('查询失败');
} finally {
loading.value = false;
}
};
const handleQuery = () => fetchList();
const resetQuery = () => {
queryParams.status = null;
fetchList();
};
const handleBook = async (slotId) => {
try {
await bookSlot(slotId);
ElMessage.success('预约成功');
fetchList();
} catch (e) {
ElMessage.error(e.message || '预约失败');
}
};
onMounted(() => fetchList());
</script>
<style scoped>
.appointment-container { padding: 20px; }
.filter-card { margin-bottom: 20px; }
.table-card { margin-top: 10px; }
</style>

View File

@@ -1,156 +0,0 @@
<template>
<div class="check-application-container">
<el-row :gutter="16" class="main-layout">
<!-- 左侧分类 -->
<el-col :span="6">
<el-card class="box-card">
<template #header>检查项目分类</template>
<div class="category-tree">
<el-tree
:data="categories"
:props="{ label: 'name', children: 'children' }"
@node-click="handleCategoryClick"
/>
</div>
</el-card>
</el-col>
<!-- 中间项目列表 -->
<el-col :span="8">
<el-card class="box-card">
<template #header>检查项目</template>
<div class="project-list">
<div v-for="proj in currentProjects" :key="proj.id" class="project-item" @click="selectProject(proj)">
<el-checkbox v-model="proj.selected" @change="handleProjectCheck(proj)" />
<span class="project-name">{{ proj.name.replace('套餐', '') }}</span>
</div>
</div>
</el-card>
</el-col>
<!-- 右侧已选择 & 明细 -->
<el-col :span="10">
<el-card class="box-card">
<template #header>已选择项目</template>
<div class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<div class="card-header" @click="toggleExpand(item)">
<!-- 修复 #550-2去除套餐冗余字样添加 title 属性支持悬停显示完整名称 -->
<span class="card-title" :title="item.name">{{ item.name.replace('套餐', '') }}</span>
<el-icon class="expand-toggle">
<ArrowDown v-if="!item.expanded" />
<ArrowUp v-else />
</el-icon>
</div>
<!-- 修复 #550-3默认收起点击展开显示结构化明细 -->
<div v-show="item.expanded" class="details-wrapper">
<!-- 移除原项目套餐明细冗余标签改为明确层级提示 -->
<div class="hierarchy-label">检查项目 > 检查方法</div>
<div class="method-panel">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<!-- 修复 #550-1方法勾选状态独立不随项目自动联动 -->
<el-checkbox v-model="method.checked" @change="handleMethodCheck(item, method)" />
<span>{{ method.name }}</span>
</div>
</div>
</div>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无选择项目" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
// 状态定义
const categories = ref([]);
const currentProjects = ref([]);
const selectedItems = ref([]);
// 分类点击:加载对应项目
const handleCategoryClick = async (node) => {
// 实际应调用 API: const res = await getProjectsByCategory(node.id);
// 此处模拟数据结构
currentProjects.value = [
{ id: 101, name: '128线排套餐', selected: false, methods: [
{ id: 201, name: '常规扫查', checked: false },
{ id: 202, name: '血管多普勒', checked: false }
]}
];
};
// 项目行点击
const selectProject = (proj) => {
proj.selected = !proj.selected;
handleProjectCheck(proj);
};
// 修复 #550-1项目勾选与检查方法解耦
const handleProjectCheck = (proj) => {
if (proj.selected) {
const exists = selectedItems.value.find(i => i.id === proj.id);
if (!exists) {
// 修复 #550-3默认收起状态 (expanded: false)
// 修复 #550-1初始化方法时强制 checked: false禁止自动联动
selectedItems.value.push({
...proj,
expanded: false,
methods: proj.methods.map(m => ({ ...m, checked: false }))
});
}
} else {
selectedItems.value = selectedItems.value.filter(i => i.id !== proj.id);
}
};
// 展开/收起明细
const toggleExpand = (item) => {
item.expanded = !item.expanded;
};
// 修复 #550-1检查方法独立勾选不反向影响父级项目状态
const handleMethodCheck = (item, method) => {
// 可在此处同步至全局状态或提交表单数据
console.log(`[CheckApp] Method ${method.name} status: ${method.checked}`);
};
</script>
<style scoped>
.check-application-container { padding: 16px; height: 100%; }
.main-layout { height: 100%; }
.box-card { height: 100%; display: flex; flex-direction: column; }
.box-card :deep(.el-card__body) { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
.category-tree, .project-list, .selected-list { flex: 1; overflow-y: auto; padding-right: 4px; }
.project-item { display: flex; align-items: center; padding: 10px 8px; cursor: pointer; border-bottom: 1px solid #f0f0f0; transition: background 0.2s; }
.project-item:hover { background: #f5f7fa; }
.project-name { margin-left: 8px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.selected-card { border: 1px solid #ebeef5; border-radius: 6px; margin-bottom: 10px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding: 12px; cursor: pointer; background: #fafafa; border-radius: 6px 6px 0 0; }
.card-header:hover { background: #f0f2f5; }
/* 修复 #550-2宽度自适应 + 溢出省略 + 悬停提示 */
.card-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 12px;
font-weight: 500;
color: #303133;
}
.expand-toggle { cursor: pointer; color: #909399; transition: transform 0.2s; }
.details-wrapper { padding: 12px; border-top: 1px solid #ebeef5; background: #fff; }
.hierarchy-label { font-size: 12px; color: #909399; margin-bottom: 8px; padding-left: 4px; border-left: 2px solid #409eff; line-height: 1.2; }
.method-item { display: flex; align-items: center; padding: 6px 0; cursor: pointer; }
.method-item:hover { background: #f9fafc; border-radius: 4px; }
.method-item span { margin-left: 8px; font-size: 14px; }
</style>

View File

@@ -1,95 +0,0 @@
<template>
<div class="outpatient-diagnosis">
<el-card header="诊断录入">
<el-form :model="form" label-width="80px">
<el-form-item label="诊断名称">
<el-autocomplete
v-model="form.diagnosisName"
:fetch-suggestions="queryDisease"
placeholder="请输入疾病名称"
@select="handleSelectDisease"
/>
</el-form-item>
<el-form-item label="诊断状态">
<el-checkbox v-model="form.isValid">有效</el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave" :loading="saving">保存诊断</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 传染病报卡弹窗 -->
<el-dialog
v-model="reportCardVisible"
:title="currentReportCardType + '填报'"
width="700px"
destroy-on-close
>
<ReportCardForm :type="currentReportCardType" :patient-id="patientId" :visit-id="visitId" />
<template #footer>
<el-button @click="reportCardVisible = false">暂存</el-button>
<el-button type="primary" @click="submitReportCard">提交报卡</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { saveDiagnosis } from '@/api/outpatient/diagnosis';
import { ElMessage } from 'element-plus';
import ReportCardForm from '@/components/ReportCardForm.vue';
const props = defineProps({
patientId: { type: String, required: true },
visitId: { type: String, required: true }
});
const form = reactive({ diagnosisName: '', isValid: true, diseaseId: '' });
const saving = ref(false);
const reportCardVisible = ref(false);
const currentReportCardType = ref('');
const queryDisease = (queryString, cb) => {
// 实际应调用后端疾病目录查询接口
cb([{ value: '古典生物型霍乱', id: 'D001' }]);
};
const handleSelectDisease = (item) => {
form.diseaseId = item.id;
form.diagnosisName = item.value;
};
const handleSave = async () => {
if (!form.diseaseId) {
ElMessage.warning('请选择诊断疾病');
return;
}
saving.value = true;
try {
const res = await saveDiagnosis({
patientId: props.patientId,
visitId: props.visitId,
diagnoses: [{ diseaseId: form.diseaseId, isValid: form.isValid }]
});
ElMessage.success(res.data.message || '诊断已保存并按排序号排序');
// 修复 Bug #573根据后端返回的报卡类型自动触发弹窗
if (res.data.reportCardTypes && res.data.reportCardTypes.length > 0) {
currentReportCardType.value = res.data.reportCardTypes[0];
reportCardVisible.value = true;
}
} catch (error) {
ElMessage.error('保存诊断失败');
} finally {
saving.value = false;
}
};
const submitReportCard = () => {
ElMessage.success('报卡提交成功');
reportCardVisible.value = false;
};
</script>

View File

@@ -1,77 +0,0 @@
<template>
<div class="doctor-order-container">
<el-card>
<template #header>
<div class="header-actions">
<span class="title">门诊医嘱</span>
<el-button type="primary" @click="handleAddOrder" data-cy="add-order-btn">新增医嘱</el-button>
</div>
</template>
<el-table
:data="orderList"
border
stripe
style="width: 100%"
data-cy="order-table"
>
<el-table-column prop="orderName" label="项目名称" width="200" />
<el-table-column label="总量" width="150" align="center">
<template #default="{ row }" data-cy="total-quantity-cell">
<!-- 修复 Bug #561前端展示增加空值防御避免拼接出 "1 null" -->
{{ row.totalQuantity }}{{ row.totalUnit ? ' ' + row.totalUnit : '' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center" />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<OrderDialog ref="orderDialogRef" @submit="fetchOrders" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import OrderDialog from './components/OrderDialog.vue'
import { getOrdersByPatient } from '@/api/order'
const orderList = ref<any[]>([])
const orderDialogRef = ref()
const fetchOrders = async () => {
try {
const res = await getOrdersByPatient(1001) // 示例患者ID
// 后端可能返回 totalUnit 为 null统一转为空字符串防止 UI 显示 "null"
orderList.value = (res.data || []).map((item: any) => ({
...item,
totalUnit: item.totalUnit ?? ''
}))
} catch (e: any) {
ElMessage.error(e.message || '获取医嘱列表失败')
}
}
onMounted(() => {
fetchOrders()
})
// 省略其他业务逻辑(新增、编辑、删除)...
</script>
<style scoped>
.doctor-order-container {
padding: 20px;
}
.header-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -1,178 +0,0 @@
<template>
<div class="exam-apply-container">
<el-row :gutter="16" class="main-layout">
<!-- 左侧分类 -->
<el-col :span="5">
<el-card class="box-card" shadow="never">
<template #header><span class="card-title-text">检查项目分类</span></template>
<el-tree
:data="categories"
:props="{ label: 'name', children: 'children' }"
node-key="id"
@node-click="handleCategoryClick"
highlight-current
default-expand-all
/>
</el-card>
</el-col>
<!-- 中间项目列表 -->
<el-col :span="9">
<el-card class="box-card" shadow="never">
<template #header><span class="card-title-text">检查项目</span></template>
<div class="scroll-area">
<div v-for="item in currentItems" :key="item.id" class="list-item">
<el-checkbox v-model="item.selected" @change="handleItemCheck(item)">
<el-tooltip :content="item.name" placement="top" :show-after="300">
<span class="text-ellipsis">{{ item.name }}</span>
</el-tooltip>
</el-checkbox>
</div>
</div>
</el-card>
</el-col>
<!-- 右侧已选择 -->
<el-col :span="10">
<el-card class="box-card" shadow="never">
<template #header><span class="card-title-text">已选择项目</span></template>
<div class="scroll-area selected-area">
<div v-for="sel in selectedItems" :key="sel.id" class="selected-card">
<div class="card-header" @click="toggleExpand(sel)">
<el-checkbox
v-model="sel.selected"
@change="handleItemCheck(sel)"
@click.stop
/>
<el-tooltip :content="sel.name" placement="top" :show-after="300">
<span class="text-ellipsis card-name">{{ sel.name }}</span>
</el-tooltip>
<el-icon class="expand-icon" :class="{ 'is-expanded': sel.isExpanded }">
<ArrowDown />
</el-icon>
</div>
<transition name="slide-fade">
<div v-show="sel.isExpanded" class="card-body">
<div v-if="sel.methods && sel.methods.length > 0" class="method-group">
<div v-for="method in sel.methods" :key="method.id" class="method-row">
<el-checkbox
v-model="method.selected"
@change="handleMethodCheck(sel, method)"
>
{{ method.name }}
</el-checkbox>
</div>
</div>
<div v-else class="empty-tip">无关联检查方法</div>
</div>
</transition>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无选择项目" :image-size="60" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ArrowDown } from '@element-plus/icons-vue'
// 模拟分类数据
const categories = ref([
{ id: 1, name: '彩超', children: [] },
{ id: 2, name: 'CT', children: [] }
])
// 模拟项目数据
const currentItems = ref([
{ id: 101, name: '套餐128线排', selected: false, methods: [
{ id: 1001, name: '常规扫描', selected: false },
{ id: 1002, name: '增强扫描', selected: false }
]},
{ id: 102, name: '腹部彩超', selected: false, methods: [] }
])
const selectedItems = ref([])
// 清理冗余前缀
const cleanName = (name) => name.replace(/^套餐[:]/, '').replace(/套餐$/, '')
const handleCategoryClick = (data) => {
// 实际业务中根据分类ID请求接口刷新 currentItems
console.log('切换分类:', data.name)
}
// 修复 Bug #550解耦勾选逻辑移除自动联动检查方法
const handleItemCheck = (item) => {
if (item.selected) {
const exists = selectedItems.value.find(s => s.id === item.id)
if (!exists) {
selectedItems.value.push({
...item,
name: cleanName(item.name),
isExpanded: false, // 默认收起
methods: item.methods ? item.methods.map(m => ({ ...m, selected: false })) : []
})
}
} else {
selectedItems.value = selectedItems.value.filter(s => s.id !== item.id)
}
}
// 独立控制检查方法勾选状态
const handleMethodCheck = (parentItem, method) => {
const sel = selectedItems.value.find(s => s.id === parentItem.id)
if (sel) {
const m = sel.methods.find(m => m.id === method.id)
if (m) m.selected = method.selected
}
}
const toggleExpand = (item) => {
item.isExpanded = !item.isExpanded
}
</script>
<style scoped>
.exam-apply-container { padding: 16px; background: #f5f7fa; min-height: 100vh; }
.box-card { height: 100%; border-radius: 8px; }
.card-title-text { font-weight: 600; font-size: 15px; }
.scroll-area { max-height: 520px; overflow-y: auto; padding-right: 4px; }
.list-item { padding: 10px 0; border-bottom: 1px dashed #ebeef5; }
.text-ellipsis {
display: inline-block;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.selected-area { padding-top: 8px; }
.selected-card {
margin-bottom: 12px;
border: 1px solid #e4e7ed;
border-radius: 6px;
background: #ffffff;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
padding: 12px;
cursor: pointer;
user-select: none;
background: #fafafa;
}
.card-header:hover { background: #f5f7fa; }
.card-name { flex: 1; margin: 0 12px; font-weight: 500; }
.expand-icon { transition: transform 0.25s ease; color: #909399; }
.expand-icon.is-expanded { transform: rotate(180deg); }
.card-body { padding: 8px 12px 12px 36px; background: #fff; border-top: 1px solid #f0f0f0; }
.method-row { padding: 6px 0; }
.empty-tip { color: #909399; font-size: 12px; padding: 8px 0; text-align: center; }
.slide-fade-enter-active, .slide-fade-leave-active { transition: all 0.25s ease; }
.slide-fade-enter-from, .slide-fade-leave-to { opacity: 0; transform: translateY(-8px); }
</style>

View File

@@ -1,175 +0,0 @@
<template>
<div class="exam-request-container">
<el-row :gutter="16" class="main-layout">
<!-- 左侧检查分类 -->
<el-col :span="6" class="category-panel">
<el-tree :data="categories" :props="{ label: 'name', children: 'children' }" @node-click="handleCategoryClick" />
</el-col>
<!-- 中间检查项目 -->
<el-col :span="9" class="item-panel">
<el-table :data="currentItems" border style="width: 100%" @selection-change="handleItemSelection">
<el-table-column type="selection" width="40" />
<el-table-column prop="name" label="检查项目" />
<el-table-column prop="price" label="价格" width="80" />
</el-table>
</el-col>
<!-- 右侧已选择区域 -->
<el-col :span="9" class="selected-panel">
<div class="panel-header">
<h3>已选择</h3>
<el-button type="danger" size="small" @click="clearAll">清空</el-button>
</div>
<!-- 移除原项目套餐明细冗余标签 -->
<div class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<div class="card-header" @click="toggleExpand(item)">
<el-checkbox
v-model="item.checked"
@change="onItemCheck(item)"
@click.stop
/>
<el-tooltip :content="item.displayName" placement="top" :show-after="300" :disabled="!item.isTruncated">
<span
ref="nameRefs"
class="item-name"
@mouseenter="checkTruncate(item)"
>{{ item.displayName }}</span>
</el-tooltip>
<el-icon class="expand-icon">
<ArrowDown v-if="item.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 检查方法明细严格遵循 项目 > 检查方法 层级默认收起 -->
<div v-show="item.expanded" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-row">
<el-checkbox
v-model="method.checked"
@change="onMethodCheck(method)"
@click.stop
/>
<span class="method-name">{{ method.name }}</span>
</div>
</div>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无已选项目" :image-size="60" />
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, computed, nextTick } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// 模拟数据源
const categories = ref([
{ id: 1, name: '彩超', children: [] }
])
const currentItems = ref([
{ id: 10, name: '128线排彩超套餐', price: 150, methods: [
{ id: 101, name: '常规腹部检查', checked: false },
{ id: 102, name: '心脏彩超', checked: false }
]}
])
// 已选项目状态管理
const selectedItems = reactive([])
const nameRefs = ref([])
// 计算属性:清理“套餐”字样,优化显示
const cleanName = (name) => name.replace(/套餐/g, '').trim()
// 添加项目到已选列表
const handleItemSelection = (selection) => {
// 解耦逻辑:仅同步选中项,不自动勾选子方法
const newIds = new Set(selection.map(i => i.id))
// 移除未选中的
for (let i = selectedItems.length - 1; i >= 0; i--) {
if (!newIds.has(selectedItems[i].id)) {
selectedItems.splice(i, 1)
}
}
// 新增已选中的
selection.forEach(item => {
if (!selectedItems.find(s => s.id === item.id)) {
selectedItems.push({
id: item.id,
name: item.name,
displayName: cleanName(item.name),
checked: true,
expanded: false, // 默认收起
isTruncated: false,
methods: item.methods.map(m => ({ ...m, checked: false })) // 方法默认不勾选
})
}
})
}
// 解耦:项目勾选独立,不级联方法
const onItemCheck = (item) => {
// 仅记录状态,不触发 methods 联动
console.log(`[解耦] 项目 ${item.displayName} 状态: ${item.checked}`)
}
// 解耦:方法勾选独立
const onMethodCheck = (method) => {
console.log(`[解耦] 方法 ${method.name} 状态: ${method.checked}`)
}
// 展开/收起明细
const toggleExpand = (item) => {
item.expanded = !item.expanded
}
// 检测文本是否截断以控制 Tooltip
const checkTruncate = async (item) => {
await nextTick()
const el = nameRefs.value.find(r => r?.textContent === item.displayName)
if (el) {
item.isTruncated = el.scrollWidth > el.clientWidth
}
}
const clearAll = () => {
selectedItems.length = 0
ElMessage.success('已清空选择')
}
const handleCategoryClick = (data) => {
// 加载对应分类下的项目逻辑(略)
}
</script>
<style scoped>
.exam-request-container { padding: 16px; background: #f5f7fa; min-height: 100vh; }
.main-layout { background: #fff; padding: 16px; border-radius: 8px; }
.category-panel, .item-panel, .selected-panel { height: 600px; overflow-y: auto; }
.selected-panel { border-left: 1px solid #ebeef5; padding-left: 16px; }
.panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.panel-header h3 { margin: 0; font-size: 16px; color: #303133; }
.selected-list { display: flex; flex-direction: column; gap: 10px; }
.selected-card { border: 1px solid #e4e7ed; border-radius: 6px; background: #fafafa; overflow: hidden; }
.card-header { display: flex; align-items: center; gap: 8px; padding: 10px 12px; cursor: pointer; transition: background 0.2s; }
.card-header:hover { background: #f2f6fc; }
.item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: #303133;
}
.expand-icon { color: #909399; font-size: 14px; }
.method-list { padding: 8px 12px 12px 32px; background: #fff; border-top: 1px dashed #ebeef5; }
.method-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
.method-name { color: #606266; font-size: 13px; }
</style>

View File

@@ -1,230 +0,0 @@
<template>
<div class="exam-application-container">
<el-row :gutter="16" class="layout-row">
<!-- 左侧分类 -->
<el-col :span="5">
<el-card shadow="never" class="panel-card">
<template #header>检查项目分类</template>
<el-tree
:data="categories"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
data-cy="exam-category-tree"
/>
</el-card>
</el-col>
<!-- 中间项目列表 -->
<el-col :span="9">
<el-card shadow="never" class="panel-card">
<template #header>检查项目</template>
<div class="item-scroll-area">
<el-checkbox-group v-model="selectedItemIds" @change="handleItemChange">
<el-checkbox
v-for="item in currentItems"
:key="item.id"
:label="item.id"
:value="item.id"
class="item-checkbox"
:data-cy="`exam-item-${item.id}`"
>
{{ cleanName(item.name) }}
</el-checkbox>
</el-checkbox-group>
</div>
</el-card>
</el-col>
<!-- 右侧已选择结构化展示 -->
<el-col :span="10">
<el-card shadow="never" class="panel-card">
<template #header>已选择</template>
<div class="selected-scroll-area">
<div
v-for="sel in selectedList"
:key="sel.id"
class="selected-card"
data-cy="selected-item-card"
>
<!-- 卡片头部名称 + 展开/收起 -->
<div class="card-header" @click="toggleExpand(sel.id)">
<el-tooltip
:content="cleanName(sel.name)"
placement="top"
:show-after="300"
:disabled="!isNameTruncated"
>
<span class="item-name" data-cy="item-name" ref="nameRefs">{{ cleanName(sel.name) }}</span>
</el-tooltip>
<el-icon class="expand-icon" :class="{ 'is-expanded': sel.expanded }" data-cy="expand-toggle">
<ArrowDown />
</el-icon>
</div>
<!-- 明细区域默认收起独立勾选检查方法 -->
<el-collapse-transition>
<div v-show="sel.expanded" class="method-detail" data-cy="method-detail">
<el-checkbox-group v-model="sel.selectedMethods" @change="handleMethodChange(sel)">
<el-checkbox
v-for="method in sel.methods"
:key="method.id"
:label="method.id"
:value="method.id"
>
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</el-collapse-transition>
</div>
<el-empty v-if="selectedList.length === 0" description="暂无已选项目" :image-size="60" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { ArrowDown } from '@element-plus/icons-vue'
// 模拟分类与项目数据结构(实际应从 API 获取)
const categories = ref([
{ id: 'ultrasound', label: '彩超', items: [
{ id: '128', name: '128线排彩超套餐', methods: [
{ id: 'm1', name: '常规腹部' },
{ id: 'm2', name: '心脏彩超' },
{ id: 'm3', name: '血管多普勒' }
]}
]}
])
const currentItems = ref([])
const selectedItemIds = ref([])
const selectedList = ref([])
const nameRefs = ref([])
// 清理冗余“套餐”字样
const cleanName = (name) => name.replace(/套餐/g, '')
// 判断名称是否被截断(用于控制 tooltip 显示)
const isNameTruncated = computed(() => {
return nameRefs.value.some(el => el && el.scrollWidth > el.clientWidth)
})
// 切换分类
const handleCategoryClick = (data) => {
currentItems.value = data.items || []
}
// 项目勾选变更(解耦:仅维护项目列表,不联动方法)
const handleItemChange = (ids) => {
// 移除未勾选的项目
selectedList.value = selectedList.value.filter(s => ids.includes(s.id))
// 新增勾选的项目
ids.forEach(id => {
if (!selectedList.value.find(s => s.id === id)) {
const item = currentItems.value.find(i => i.id === id)
if (item) {
selectedList.value.push({
id: item.id,
name: item.name,
methods: item.methods || [],
selectedMethods: [], // 独立维护已选方法
expanded: false // 默认收起
})
}
}
})
}
// 检查方法勾选变更(独立逻辑)
const handleMethodChange = (sel) => {
// 此处可触发后续业务逻辑(如价格计算、库存校验等)
console.log(`项目 ${sel.id} 方法变更:`, sel.selectedMethods)
}
// 展开/收起明细
const toggleExpand = (id) => {
const item = selectedList.value.find(s => s.id === id)
if (item) item.expanded = !item.expanded
}
</script>
<style scoped>
.exam-application-container {
padding: 16px;
background: #f5f7fa;
min-height: 100vh;
}
.layout-row {
align-items: stretch;
}
.panel-card {
height: 100%;
display: flex;
flex-direction: column;
}
.panel-card :deep(.el-card__body) {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.item-scroll-area, .selected-scroll-area {
flex: 1;
overflow-y: auto;
padding-right: 4px;
}
.item-checkbox {
display: block;
margin-bottom: 8px;
line-height: 1.5;
}
.selected-card {
border: 1px solid #e4e7ed;
border-radius: 6px;
margin-bottom: 10px;
background: #fff;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
cursor: pointer;
background: #fafafa;
transition: background 0.2s;
}
.card-header:hover {
background: #f0f2f5;
}
.item-name {
flex: 1;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
.expand-icon {
transition: transform 0.3s ease;
color: #909399;
}
.is-expanded {
transform: rotate(180deg);
}
.method-detail {
padding: 8px 12px 12px;
border-top: 1px dashed #ebeef5;
background: #fff;
}
.method-detail :deep(.el-checkbox) {
margin-right: 16px;
margin-bottom: 6px;
}
</style>

View File

@@ -1,154 +0,0 @@
<template>
<div class="examination-apply-container">
<el-row :gutter="16">
<!-- 左侧分类树 -->
<el-col :span="6">
<el-card shadow="never" class="panel-card">
<template #header>检查项目分类</template>
<div class="category-tree">
<el-tree
:data="categories"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
</el-card>
</el-col>
<!-- 中间项目列表 -->
<el-col :span="9">
<el-card shadow="never" class="panel-card">
<template #header>检查项目</template>
<div class="item-list">
<el-checkbox-group v-model="selectedItemIds" @change="handleItemChange">
<el-checkbox v-for="item in currentItems" :key="item.id" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</el-card>
</el-col>
<!-- 右侧已选择区域 -->
<el-col :span="9">
<el-card shadow="never" class="panel-card">
<template #header>已选择</template>
<div class="selected-list">
<div v-for="sel in selectedList" :key="sel.id" class="selected-card">
<div class="card-header" @click="toggleDetail(sel)">
<span class="card-title" :title="cleanName(sel.name)">{{ cleanName(sel.name) }}</span>
<el-icon class="toggle-icon">
<ArrowRight v-if="!sel.expanded" />
<ArrowDown v-else />
</el-icon>
</div>
<div v-show="sel.expanded" class="card-body">
<div v-if="sel.methods && sel.methods.length" class="method-list">
<div v-for="method in sel.methods" :key="method.id" class="method-row">
<el-checkbox v-model="method.checked" @change="handleMethodChange(sel, method)">
{{ method.name }}
</el-checkbox>
</div>
</div>
<div v-else class="empty-tip">无关联检查方法</div>
</div>
</div>
<el-empty v-if="selectedList.length === 0" description="暂无选择项目" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
// 模拟数据结构(实际应从 API 获取)
const categories = ref([
{ id: 1, name: '彩超', children: [] },
{ id: 2, name: 'CT', children: [] }
])
const allItems = ref([
{ id: 101, name: '128线排套餐', categoryId: 1, methods: [{ id: 201, name: '常规扫描', checked: false }, { id: 202, name: '增强扫描', checked: false }] },
{ id: 102, name: '普通彩超', categoryId: 1, methods: [] }
])
const currentCategoryId = ref(null)
const selectedItemIds = ref([])
const selectedList = ref([])
const currentItems = computed(() => {
if (!currentCategoryId.value) return allItems.value
return allItems.value.filter(i => i.categoryId === currentCategoryId.value)
})
// 清理冗余前缀
const cleanName = (name) => name.replace(/套餐/g, '').trim()
const handleCategoryClick = (node) => {
currentCategoryId.value = node.id
}
// 核心修复1项目与方法解耦。仅维护选中项结构不联动勾选方法
const handleItemChange = (ids) => {
const newSelected = ids.map(id => {
const existing = selectedList.value.find(s => s.id === id)
if (existing) return existing
const item = allItems.value.find(i => i.id === id)
// 默认收起,方法状态独立初始化
return { ...item, expanded: false, methods: item.methods ? item.methods.map(m => ({ ...m, checked: false })) : [] }
})
// 移除取消勾选的项目
selectedList.value = newSelected
}
const toggleDetail = (sel) => {
sel.expanded = !sel.expanded
}
// 核心修复2方法勾选独立仅更新当前项目下的方法状态
const handleMethodChange = (sel, method) => {
// 此处可触发后续业务逻辑(如价格计算、库存校验等)
console.log(`项目 ${sel.name} 方法 ${method.name} 状态变更: ${method.checked}`)
}
</script>
<style scoped>
.examination-apply-container { padding: 16px; }
.panel-card { height: 100%; }
.category-tree, .item-list, .selected-list { min-height: 400px; overflow-y: auto; }
.selected-card {
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 12px;
background: #fff;
transition: box-shadow 0.2s;
}
.selected-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
cursor: pointer;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
}
.card-title {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 85%;
font-size: 14px;
}
.toggle-icon { color: #909399; }
.card-body { padding: 12px; }
.method-list { display: flex; flex-direction: column; gap: 8px; }
.method-row { padding-left: 8px; }
.empty-tip { color: #909399; font-size: 12px; text-align: center; padding: 8px 0; }
</style>

View File

@@ -1,67 +0,0 @@
<template>
<el-dialog v-model="visible" title="传染病报告卡" width="800px" @open="loadReportData" destroy-on-close>
<el-form :model="form" label-width="100px" class="report-form">
<el-form-item label="现住址">
<el-input v-model="form.currentAddress" name="currentAddress" placeholder="现住址" />
</el-form-item>
<el-form-item label="职业">
<el-input v-model="form.occupation" name="occupation" placeholder="职业" />
</el-form-item>
<!-- 其他报卡字段省略 -->
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交上报</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import request from '@/utils/request'
const visible = ref(false)
const form = reactive({
patientId: null,
currentAddress: '',
occupation: ''
})
/**
* 打开报卡弹窗
* @param {Number} patientId 患者ID
*/
const open = (patientId) => {
form.patientId = patientId
visible.value = true
}
/**
* Bug #572 Fix: 弹窗打开时请求后端初始化接口,自动填充档案数据
*/
const loadReportData = async () => {
if (!form.patientId) return
try {
const res = await request.get(`/outpatient/infectious-disease-report/init/${form.patientId}`)
if (res.code === 200 && res.data) {
form.currentAddress = res.data.currentAddress || ''
form.occupation = res.data.occupation || ''
}
} catch (error) {
console.error('加载传染病报告卡数据失败:', error)
}
}
const handleSubmit = () => {
// 提交逻辑(略)
visible.value = false
}
defineExpose({ open })
</script>
<style scoped>
.report-form {
padding: 20px;
}
</style>

View File

@@ -1,73 +0,0 @@
<template>
<div class="pending-records-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>待写病历</span>
<el-button type="primary" @click="fetchRecords" :loading="loading">刷新</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="records"
style="width: 100%"
data-cy="record-list"
empty-text="暂无待写病历"
>
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="visitNo" label="就诊号" width="140" />
<el-table-column prop="visitTime" label="就诊时间" width="180" />
<el-table-column prop="status" label="状态" width="100" />
<el-table-column label="操作" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleWrite(row)" data-cy="record-item">
书写病历
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getPendingRecords } from '@/api/medicalRecord'
const loading = ref(false)
const records = ref([])
const fetchRecords = async () => {
loading.value = true
try {
// 修复增加请求超时控制2秒避免网络阻塞导致页面一直加载
const res = await getPendingRecords({ pageNum: 1, pageSize: 20 }, { timeout: 2000 })
records.value = res.data?.list || []
} catch (error) {
console.error('加载待写病历失败:', error)
records.value = []
} finally {
// 修复确保无论成功、失败或超时loading 状态都会被强制重置
loading.value = false
}
}
const handleWrite = (row) => {
// 路由跳转至病历书写页
// router.push({ name: 'MedicalRecordWrite', params: { id: row.id } })
}
onMounted(fetchRecords)
</script>
<style scoped>
.pending-records-container {
padding: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -1,88 +0,0 @@
<template>
<div class="pending-records-container">
<el-card>
<template #header>
<div class="header">
<span>待写病历</span>
<el-button type="primary" @click="refreshData" :loading="loading">刷新</el-button>
</div>
</template>
<!-- 修复 #562v-loading 绑定 loading 状态避免请求异常时加载遮罩不消失 -->
<el-table v-loading="loading" :data="recordList" style="width: 100%" empty-text="暂无待写病历">
<el-table-column prop="patientName" label="患者姓名" />
<el-table-column prop="visitDate" label="就诊日期" />
<el-table-column prop="status" label="状态" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button type="primary" link @click="handleWrite(row)">书写</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
layout="total, prev, pager, next"
@current-change="fetchRecords"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { getPendingRecords } from '@/api/outpatient/medicalRecord';
const recordList = ref([]);
const loading = ref(false);
const currentPage = ref(1);
const pageSize = ref(15);
const total = ref(0);
// 修复 #562统一加载状态管理增加 finally 确保遮罩必关,避免“一直加载”假死
const fetchRecords = async () => {
loading.value = true;
try {
const res = await getPendingRecords({
doctorId: 1, // 实际应从用户上下文/Store获取
pageNum: currentPage.value,
pageSize: pageSize.value
});
if (res.code === 200) {
recordList.value = res.data.list;
total.value = res.data.total;
} else {
ElMessage.warning(res.msg || '加载失败');
}
} catch (error) {
ElMessage.error('网络异常,请检查连接后重试');
console.error('Bug #562 fetch error:', error);
} finally {
loading.value = false;
}
};
const refreshData = () => {
currentPage.value = 1;
fetchRecords();
};
const handleWrite = (row) => {
// 路由跳转至病历书写页
console.log('Navigate to write record:', row.id);
};
onMounted(() => {
fetchRecords();
});
</script>
<style scoped>
.pending-records-container { padding: 16px; }
.header { display: flex; justify-content: space-between; align-items: center; }
.pagination { margin-top: 16px; justify-content: flex-end; }
</style>

View File

@@ -1,52 +0,0 @@
<template>
<div class="queue-list">
<el-table :data="queueList" style="width: 100%" v-loading="loading">
<el-table-column prop="id" label="序号" width="80"/>
<el-table-column prop="status" label="状态"/>
<el-table-column prop="createTime" label="挂号时间"/>
<!-- 其它列 -->
</el-table>
<el-pagination
v-if="total > pageSize"
background
layout="prev, pager, next, jumper"
:current-page="pageNum"
:page-size="pageSize"
:total="total"
@current-change="handlePageChange" />
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { getQueueOrders } from '@/api/outpatient'
const props = defineProps({
patientId: { type: [String, Number], required: true }
})
const queueList = ref([])
const loading = ref(false)
const pageNum = ref(1)
const pageSize = ref(20)
const total = ref(0)
const fetchQueue = async () => {
loading.value = true
try {
const res = await getQueueOrders(props.patientId, pageNum.value, pageSize.value)
queueList.value = res.data
total.value = res.total || queueList.value.length
} finally {
loading.value = false
}
}
const handlePageChange = (newPage) => {
pageNum.value = newPage
fetchQueue()
}
onMounted(fetchQueue)
watch(() => props.patientId, fetchQueue)
</script>

View File

@@ -1,49 +0,0 @@
<template>
<div class="write-record">
<el-table :data="orderList" style="width: 100%" v-loading="loading">
<!-- 表头列略 -->
</el-table>
<el-pagination
v-if="total > pageSize"
background
layout="prev, pager, next, jumper"
:current-page="pageNum"
:page-size="pageSize"
:total="total"
@current-change="handlePageChange" />
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { getPendingOrders } from '@/api/outpatient'
const props = defineProps({
patientId: { type: [String, Number], required: true }
})
const orderList = ref([])
const loading = ref(false)
const pageNum = ref(1)
const pageSize = ref(20)
const total = ref(0)
const fetchOrders = async () => {
loading.value = true
try {
const res = await getPendingOrders(props.patientId, pageNum.value, pageSize.value)
orderList.value = res.data
total.value = res.total || orderList.value.length // 若后端未返回 total使用列表长度
} finally {
loading.value = false
}
}
const handlePageChange = (newPage) => {
pageNum.value = newPage
fetchOrders()
}
onMounted(fetchOrders)
watch(() => props.patientId, fetchOrders)
</script>

View File

@@ -1,137 +0,0 @@
<template>
<div class="appointment-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>门诊预约挂号</span>
</div>
</template>
<!-- 查询条件 -->
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="状态筛选">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable @change="handleSearch">
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="tableData" border style="width: 100%" v-loading="loading">
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="doctorName" label="医生" width="120" />
<el-table-column prop="visitDate" label="就诊日期" width="120" />
<el-table-column prop="timeSlot" label="时段" width="100" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button link type="primary" @click="handleCancel(row)" v-if="row.status === 1">取消预约</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="total, prev, pager, next"
@current-change="fetchData"
class="pagination"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getAppointmentListApi, cancelAppointmentApi } from '@/api/outpatient/appointment'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
status: undefined
})
// Bug #570 Fix: 移除错误的“已锁定”状态,统一使用标准预约状态字典
const statusOptions = [
{ label: '已预约', value: 1 },
{ label: '已就诊', value: 2 },
{ label: '已取消', value: 3 },
{ label: '已爽约', value: 4 }
]
const getStatusLabel = (status) => {
const found = statusOptions.find(opt => opt.value === status)
return found ? found.label : '未知'
}
const getStatusType = (status) => {
const map = { 1: 'success', 2: 'info', 3: 'warning', 4: 'danger' }
return map[status] || 'info'
}
const fetchData = async () => {
loading.value = true
try {
const res = await getAppointmentListApi(queryParams)
tableData.value = res.data.list || []
total.value = res.data.total || 0
} catch (error) {
ElMessage.error('获取预约列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
queryParams.pageNum = 1
fetchData()
}
const resetQuery = () => {
queryParams.status = undefined
handleSearch()
}
const handleCancel = async (row) => {
try {
await ElMessageBox.confirm('确定取消该预约吗?', '提示', { type: 'warning' })
await cancelAppointmentApi(row.id)
ElMessage.success('取消成功')
fetchData()
} catch (e) {
// 用户取消操作
}
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.appointment-container { padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.search-form { margin-bottom: 20px; }
.pagination { margin-top: 20px; justify-content: flex-end; }
</style>

View File

@@ -1,238 +0,0 @@
<template>
<div class="check-application-container">
<!-- 左侧检查项目分类 -->
<div class="panel category-panel">
<h3 class="panel-title">检查项目分类</h3>
<el-tree
:data="categories"
:props="{ label: 'name', children: 'children' }"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="panel project-panel">
<h3 class="panel-title">检查项目</h3>
<el-checkbox-group v-model="selectedProjectIds" @change="onProjectChange">
<el-checkbox
v-for="proj in currentProjects"
:key="proj.id"
:label="proj.id"
class="project-item"
>
<span :title="proj.name">{{ proj.name.replace(/套餐/g, '') }}</span>
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 右侧已选择区域 -->
<div class="panel selected-panel">
<h3 class="panel-title">已选择</h3>
<div v-if="selectedList.length === 0" class="empty-tip">暂无选择项目</div>
<div v-for="item in selectedList" :key="item.id" class="selected-card">
<!-- 卡片头部支持点击展开/收起默认收起 -->
<div class="card-header" @click="toggleExpand(item)">
<span class="card-title" :title="item.name">{{ item.name.replace(/套餐/g, '') }}</span>
<el-icon class="expand-icon">
<ArrowDown v-if="item.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 结构化展示层级移除冗余标签明确 项目 > 方法 -->
<div v-show="item.expanded" class="card-details">
<div class="hierarchy-label">检查项目 &gt; 检查方法</div>
<el-checkbox-group v-model="item.selectedMethods" @change="onMethodChange(item)">
<el-checkbox
v-for="method in item.availableMethods"
:key="method.id"
:label="method.id"
>
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
// 假设数据结构(实际由后端接口提供)
// categories: [{ id, name, children: [{ id, name, methods: [{ id, name }] }] }]
const categories = ref([])
const currentProjects = ref([])
const selectedProjectIds = ref([])
const selectedList = ref([])
// 分类点击:加载对应项目
const handleCategoryClick = (node) => {
currentProjects.value = node.children || []
}
// 同步中间面板勾选状态与右侧已选列表
// 核心解耦逻辑新增项目时selectedMethods 初始化为空数组expanded 默认为 false
watch(selectedProjectIds, (newIds) => {
const currentIds = new Set(newIds)
// 移除未勾选的项目
selectedList.value = selectedList.value.filter(item => currentIds.has(item.id))
// 新增勾选的项目
newIds.forEach(id => {
if (!selectedList.value.find(item => item.id === id)) {
const proj = findProjectById(id)
if (proj) {
selectedList.value.push({
id: proj.id,
name: proj.name,
expanded: false, // 默认收起
selectedMethods: [], // 检查方法独立,不自动勾选
availableMethods: proj.methods || []
})
}
}
})
}, { deep: true })
// 辅助查找项目
const findProjectById = (id) => {
for (const cat of categories.value) {
if (cat.children) {
const found = cat.children.find(p => p.id === id)
if (found) return found
}
}
return null
}
// 展开/收起控制
const toggleExpand = (item) => {
item.expanded = !item.expanded
}
// 项目勾选变更(由 watch 处理同步,此处预留扩展)
const onProjectChange = () => {}
// 检查方法变更(完全独立,仅更新当前卡片状态)
const onMethodChange = (item) => {
// 可在此处触发后续业务逻辑(如价格计算、库存校验等)
console.log(`[CheckApp] 项目 ${item.name} 方法变更:`, item.selectedMethods)
}
</script>
<style scoped>
.check-application-container {
display: flex;
gap: 16px;
padding: 16px;
height: 100%;
background: #f5f7fa;
}
.panel {
flex: 1;
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 12px;
background: #fff;
display: flex;
flex-direction: column;
min-height: 400px;
}
.panel-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.category-panel, .project-panel, .selected-panel {
overflow-y: auto;
}
.project-item {
margin-bottom: 8px;
width: 100%;
}
.project-item span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
max-width: 100%;
}
.empty-tip {
color: #909399;
text-align: center;
margin-top: 40px;
font-size: 13px;
}
.selected-card {
border: 1px solid #dcdfe6;
border-radius: 6px;
margin-bottom: 10px;
background: #fafafa;
transition: all 0.2s;
}
.selected-card:hover {
border-color: #c0c4cc;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
cursor: pointer;
user-select: none;
background: #fff;
border-radius: 6px 6px 0 0;
}
.card-header:hover {
background: #f5f7fa;
}
.card-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
font-weight: 500;
color: #303133;
}
.expand-icon {
font-size: 14px;
color: #909399;
transition: transform 0.2s;
}
.card-details {
padding: 10px 12px 12px;
border-top: 1px solid #ebeef5;
background: #fff;
border-radius: 0 0 6px 6px;
}
.hierarchy-label {
font-size: 12px;
color: #909399;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px dashed #ebeef5;
}
</style>

View File

@@ -1,195 +0,0 @@
<template>
<div class="check-apply-container">
<!-- 左侧检查项目分类 -->
<div class="panel category-panel">
<h3 class="panel-title">检查项目分类</h3>
<el-tree
:data="categories"
:props="{ label: 'name', children: 'children' }"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="panel item-panel">
<h3 class="panel-title">检查项目</h3>
<el-table :data="currentItems" border style="width: 100%" @selection-change="handleItemSelection">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="项目名称" show-overflow-tooltip />
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="toggleItemExpand(row)">
{{ row.expanded ? '收起' : '展开' }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 右侧已选择 & 方法明细 -->
<div class="panel selected-panel">
<h3 class="panel-title">已选择</h3>
<div class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<div class="card-header" @click="item.expanded = !item.expanded">
<el-checkbox
v-model="item.checked"
@change="onItemCheckChange(item)"
@click.stop
/>
<span class="item-name" :title="item.name">{{ cleanName(item.name) }}</span>
<el-icon class="expand-icon">
<ArrowDown v-if="item.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<div v-show="item.expanded" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<el-checkbox v-model="method.checked" @change="onMethodCheckChange(item, method)" />
<span class="method-name" :title="method.name">{{ method.name }}</span>
</div>
</div>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无已选项目" :image-size="60" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// 模拟数据结构(实际应从 API 获取)
interface Method { id: string; name: string; checked: boolean }
interface CheckItem { id: string; name: string; checked: boolean; expanded: boolean; methods: Method[] }
interface Category { id: string; name: string; children: CheckItem[] }
const categories = ref<Category[]>([
{
id: 'c1', name: '彩超', children: [
{ id: 'i1', name: '128线排彩超', checked: false, expanded: false, methods: [
{ id: 'm1', name: '常规腹部', checked: false },
{ id: 'm2', name: '心脏彩超', checked: false }
]},
{ id: 'i2', name: '套餐-甲状腺彩超', checked: false, expanded: false, methods: [
{ id: 'm3', name: '双侧甲状腺', checked: false }
]}
]
}
])
const currentItems = ref<CheckItem[]>([])
const selectedItems = reactive<CheckItem[]>([])
const handleCategoryClick = (data: Category) => {
currentItems.value = data.children || []
}
const handleItemSelection = (selection: CheckItem[]) => {
// 仅更新选中状态,不联动方法
currentItems.value.forEach(item => {
item.checked = selection.includes(item)
if (item.checked && !selectedItems.find(s => s.id === item.id)) {
selectedItems.push({ ...item, expanded: false })
}
})
// 移除未选中的
const selectedIds = selection.map(i => i.id)
for (let i = selectedItems.length - 1; i >= 0; i--) {
if (!selectedIds.includes(selectedItems[i].id)) {
selectedItems.splice(i, 1)
}
}
}
const toggleItemExpand = (item: CheckItem) => {
item.expanded = !item.expanded
}
const onItemCheckChange = (item: CheckItem) => {
// 项目勾选独立,不自动勾选/取消方法
if (!item.checked) {
const idx = selectedItems.findIndex(s => s.id === item.id)
if (idx !== -1) selectedItems.splice(idx, 1)
}
}
const onMethodCheckChange = (item: CheckItem, method: Method) => {
// 方法勾选独立,不反向影响父项目
// 业务逻辑:仅记录方法选择状态,提交时一并携带
}
const cleanName = (name: string) => {
// 去除冗余的“套餐”、“项目套餐明细”等前缀
return name.replace(/^(套餐|项目套餐明细)[-:]?/g, '')
}
</script>
<style scoped>
.check-apply-container {
display: flex;
gap: 16px;
height: 100%;
padding: 16px;
background: #f5f7fa;
}
.panel {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
}
.category-panel { flex: 1; min-width: 200px; }
.item-panel { flex: 2; min-width: 300px; }
.selected-panel { flex: 2; min-width: 300px; }
.panel-title { margin: 0 0 12px; font-size: 16px; font-weight: 600; color: #303133; }
.selected-list { flex: 1; overflow-y: auto; padding-right: 4px; }
.selected-card {
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 10px;
background: #fafafa;
transition: all 0.2s;
}
.card-header {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
user-select: none;
}
.card-header:hover { background: #f0f2f5; }
.item-name {
flex: 1;
margin: 0 8px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0; /* 关键:配合 flex 实现自适应截断 */
}
.expand-icon { color: #909399; font-size: 14px; }
.method-list {
padding: 8px 12px 12px 36px;
border-top: 1px dashed #dcdfe6;
}
.method-item {
display: flex;
align-items: center;
padding: 6px 0;
color: #606266;
}
.method-name {
margin-left: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -1,125 +0,0 @@
<template>
<div class="diagnosis-manage-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>门诊诊断录入</span>
<el-button type="primary" @click="handleSave" :loading="saveLoading">保存诊断</el-button>
</div>
</template>
<el-form :model="diagnosisForm" label-width="80px">
<el-form-item label="诊断搜索">
<el-autocomplete
v-model="diagnosisForm.searchText"
:fetch-suggestions="queryDisease"
placeholder="请输入疾病名称或拼音码"
@select="handleSelectDisease"
class="diagnosis-search"
/>
</el-form-item>
<el-table :data="diagnosisForm.list" border style="width: 100%" class="diagnosis-table">
<el-table-column prop="diseaseName" label="疾病名称" />
<el-table-column prop="icdCode" label="ICD编码" width="120" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.isValid ? 'success' : 'info'">
{{ row.isValid ? '有效' : '无效' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button link type="primary" @click="toggleValid(row)">
{{ row.isValid ? '设为无效' : '设为有效' }}
</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
</el-card>
<!-- 传染病报卡弹窗 -->
<ReportCardDialog
v-model="reportCardVisible"
:diagnosis-data="currentReportDiagnosis"
:patient-info="patientInfo"
@success="handleReportSuccess"
/>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { saveDiagnosisApi, queryDiseaseApi, getPatientInfoApi } from '@/api/outpatient/diagnosis'
import ReportCardDialog from './components/ReportCardDialog.vue'
const saveLoading = ref(false)
const reportCardVisible = ref(false)
const currentReportDiagnosis = ref(null)
// Bug #572 修复:新增患者档案信息响应式状态
const patientInfo = ref({ currentAddress: '', occupation: '' })
const diagnosisForm = reactive({
searchText: '',
list: [],
patientId: null // 实际项目中通常由路由参数或全局状态注入
})
const queryDisease = async (queryString, cb) => {
const res = await queryDiseaseApi({ keyword: queryString })
cb(res.data)
}
const handleSelectDisease = (item) => {
diagnosisForm.list.push({ ...item, isValid: true })
diagnosisForm.searchText = ''
}
const toggleValid = (row) => {
row.isValid = !row.isValid
}
const handleSave = async () => {
if (diagnosisForm.list.length === 0) {
ElMessage.warning('请先添加诊断')
return
}
saveLoading.value = true
try {
const res = await saveDiagnosisApi(diagnosisForm)
// 假设后端返回需报卡的传染病标识及诊断详情
if (res.data?.needReport && res.data?.infectiousDiagnosis) {
currentReportDiagnosis.value = res.data.infectiousDiagnosis
// Bug #572 修复:保存成功后自动拉取患者档案的现住址与职业
const profileRes = await getPatientInfoApi(diagnosisForm.patientId)
patientInfo.value = {
currentAddress: profileRes.data.currentAddress || '',
occupation: profileRes.data.occupation || ''
}
reportCardVisible.value = true
} else {
ElMessage.success('诊断保存成功')
}
} catch (e) {
ElMessage.error('保存失败')
} finally {
saveLoading.value = false
}
}
const handleReportSuccess = () => {
ElMessage.success('传染病报告卡提交成功')
reportCardVisible.value = false
}
</script>
<style scoped>
.diagnosis-manage-container { padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.diagnosis-search { width: 100%; }
</style>

View File

@@ -1,261 +0,0 @@
<template>
<div class="exam-order-panel">
<!-- 左侧分类 & 中间项目列表 -->
<div class="selection-area">
<div class="category-list">
<div
v-for="cat in categories"
:key="cat.id"
class="category-item"
:class="{ active: currentCategoryId === cat.id }"
@click="selectCategory(cat)"
>
{{ cat.name }}
</div>
</div>
<div class="item-list">
<div v-for="item in currentItems" :key="item.id" class="item-row">
<!-- 修复1项目勾选独立@change 仅处理自身状态阻断向检查方法的自动联动 -->
<el-checkbox
v-model="item.selected"
@change="handleItemCheck(item)"
:data-testid="`item-checkbox-${item.name}`"
>
{{ item.name }}
</el-checkbox>
</div>
</div>
</div>
<!-- 下方已选择区域 -->
<div class="selected-area">
<h3>已选择项目</h3>
<div class="selected-cards">
<div
v-for="group in selectedGroups"
:key="group.itemId"
class="selected-card"
:class="{ 'is-expanded': group.expanded }"
:data-testid="'selected-card'"
>
<!-- 修复2卡片宽度自适应去除套餐前缀悬停提示完整名称 -->
<div class="card-header" @click="toggleExpand(group)">
<span
class="item-name"
:title="group.cleanName"
:data-testid="'selected-card-name'"
>
{{ group.cleanName }}
</span>
<el-icon class="expand-icon" :data-testid="'selected-card-expand-btn'">
<ArrowRight v-if="!group.expanded" />
<ArrowDown v-else />
</el-icon>
</div>
<!-- 修复3结构化展示明细/方法默认收起严格遵循 项目 > 检查方法 层级移除冗余标签 -->
<transition name="slide">
<div v-show="group.expanded" class="card-details" :data-testid="'selected-card-details'">
<div v-if="group.methods && group.methods.length" class="method-list">
<div
v-for="method in group.methods"
:key="method.id"
class="method-item"
:data-testid="'method-list-item'"
>
<!-- 检查方法独立勾选不反向影响父级项目 -->
<el-checkbox v-model="method.selected" @change="handleMethodCheck(method)">
{{ method.name }}
</el-checkbox>
</div>
</div>
<div v-else class="no-methods">无关联检查方法</div>
</div>
</transition>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { ArrowRight, ArrowDown } from '@element-plus/icons-vue';
// 模拟分类与项目数据结构
const categories = ref([
{ id: 1, name: '彩超' },
{ id: 2, name: 'CT' }
]);
const currentCategoryId = ref(1);
const currentItems = ref([
{
id: 101,
name: '128线排彩超检查套餐',
selected: false,
methods: [
{ id: 201, name: '腹部彩超', selected: false },
{ id: 202, name: '心脏彩超', selected: false }
]
},
{
id: 102,
name: '常规彩超',
selected: false,
methods: []
}
]);
// 修复2清理名称逻辑全局移除“套餐”字样并去除首尾空格
const cleanItemName = (name) => name.replace(/套餐/g, '').trim();
// 已选项目分组计算属性(项目 > 检查方法)
const selectedGroups = computed(() => {
return currentItems.value
.filter(item => item.selected)
.map(item => ({
itemId: item.id,
cleanName: cleanItemName(item.name),
methods: item.methods || [],
expanded: false // 修复3默认收起状态
}));
});
const selectCategory = (cat) => {
currentCategoryId.value = cat.id;
// 此处应调用后端接口加载对应分类下的项目列表
// loadItemsByCategory(cat.id);
};
// 修复1项目勾选解耦处理
const handleItemCheck = (item) => {
// 仅切换项目选中状态,不自动勾选或取消关联的检查方法
// 保持父子状态独立,由医生手动控制
};
// 检查方法独立勾选处理
const handleMethodCheck = (method) => {
// 独立处理检查方法状态,不向上冒泡影响项目勾选
};
const toggleExpand = (group) => {
group.expanded = !group.expanded;
};
</script>
<style scoped>
.exam-order-panel {
display: flex;
flex-direction: column;
gap: 16px;
padding: 12px;
background: #fff;
}
.selection-area {
display: flex;
gap: 16px;
min-height: 300px;
}
.category-list, .item-list {
flex: 1;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 8px;
overflow-y: auto;
}
.category-item {
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 4px;
}
.category-item.active {
background: #ecf5ff;
color: #409eff;
font-weight: 500;
}
.item-row {
padding: 6px 0;
border-bottom: 1px dashed #f0f0f0;
}
.selected-area {
border-top: 1px solid #ebeef5;
padding-top: 12px;
}
.selected-cards {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 8px;
}
.selected-card {
border: 1px solid #dcdfe6;
border-radius: 6px;
padding: 10px;
min-width: 220px;
max-width: 100%; /* 修复2宽度自适应容器 */
background: #fafafa;
transition: box-shadow 0.2s;
}
.selected-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-weight: 500;
color: #303133;
}
.item-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 85%;
font-size: 14px;
}
.expand-icon {
font-size: 14px;
color: #909399;
}
.card-details {
margin-top: 10px;
padding-left: 12px;
border-left: 2px solid #409eff;
background: #fff;
border-radius: 0 0 4px 4px;
}
.method-item {
margin: 6px 0;
font-size: 13px;
color: #606266;
}
.no-methods {
color: #909399;
font-size: 12px;
padding: 4px 0;
}
.slide-enter-active, .slide-leave-active {
transition: all 0.25s ease;
}
.slide-enter-from, .slide-leave-to {
opacity: 0;
transform: translateY(-6px);
}
</style>

View File

@@ -1,203 +0,0 @@
<template>
<div class="examination-application-container">
<el-row :gutter="16" class="main-layout">
<!-- 左侧检查项目分类 -->
<el-col :span="5">
<el-card shadow="never" class="panel-card">
<template #header>检查项目分类</template>
<el-tree
ref="categoryTreeRef"
:data="categoryList"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
data-cy="category-tree"
/>
</el-card>
</el-col>
<!-- 中间检查项目列表 -->
<el-col :span="9">
<el-card shadow="never" class="panel-card">
<template #header>检查项目</template>
<div class="item-scroll-area" data-cy="item-list">
<div v-for="item in currentItems" :key="item.id" class="item-row">
<el-checkbox
v-model="item.checked"
@change="handleItemCheck(item)"
:label="item.name"
>
{{ formatItemName(item.name) }}
</el-checkbox>
</div>
</div>
</el-card>
</el-col>
<!-- 右侧已选择区域 -->
<el-col :span="10">
<el-card shadow="never" class="panel-card">
<template #header>已选择</template>
<div class="selected-scroll-area" data-cy="selected-list">
<div v-if="selectedItems.length === 0" class="empty-tip">暂无已选项目</div>
<div
v-for="sel in selectedItems"
:key="sel.id"
class="selected-card"
data-cy="selected-card"
>
<div class="card-header" @click="toggleDetails(sel.id)">
<el-tooltip
:content="formatItemName(sel.name)"
placement="top"
:show-after="300"
>
<span class="item-name" data-cy="item-name">{{ formatItemName(sel.name) }}</span>
</el-tooltip>
<el-icon class="expand-icon">
<ArrowDown v-if="sel.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 检查方法明细默认收起独立勾选移除冗余标签 -->
<div v-show="sel.expanded" class="method-list" data-cy="method-list">
<div v-for="method in sel.methods" :key="method.id" class="method-row">
<el-checkbox
v-model="method.checked"
@change="handleMethodCheck(sel, method)"
class="method-checkbox"
data-cy="method-checkbox"
>
{{ method.name }}
</el-checkbox>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
// 分类数据实际应从后端API获取
const categoryList = ref([
{ id: 'c1', label: '彩超', children: [] },
{ id: 'c2', label: 'CT', children: [] }
])
const currentItems = ref([])
const selectedItems = ref([])
// 格式化名称:去除冗余的“套餐”字样,避免显示混乱
const formatItemName = (name) => {
if (!name) return ''
return name.replace(/套餐/g, '').trim()
}
// 切换分类加载项目
const handleCategoryClick = (node) => {
// 实际项目中此处调用API获取项目列表
currentItems.value = [
{ id: 'i1', name: '128线排套餐', checked: false, methods: [{ id: 'm1', name: '常规扫描', checked: false }, { id: 'm2', name: '增强扫描', checked: false }] },
{ id: 'i2', name: '腹部彩超', checked: false, methods: [] }
]
}
// 勾选项目:解耦逻辑,不自动勾选检查方法
const handleItemCheck = (item) => {
if (item.checked) {
const exists = selectedItems.value.find(s => s.id === item.id)
if (!exists) {
selectedItems.value.push({
id: item.id,
name: item.name,
expanded: false, // 默认收起明细
methods: (item.methods || []).map(m => ({ ...m, checked: false })) // 保持独立,不联动勾选
})
}
} else {
selectedItems.value = selectedItems.value.filter(s => s.id !== item.id)
}
}
// 勾选检查方法:独立控制,不影响父级项目状态
const handleMethodCheck = (parentItem, method) => {
// 仅更新方法状态,不触发父级联动。可根据业务需要在此处添加校验逻辑
}
// 展开/收起明细
const toggleDetails = (id) => {
const target = selectedItems.value.find(s => s.id === id)
if (target) {
target.expanded = !target.expanded
}
}
</script>
<style scoped>
.examination-application-container {
padding: 16px;
background: #f5f7fa;
min-height: 100vh;
}
.panel-card {
height: 100%;
}
.item-scroll-area, .selected-scroll-area {
height: 500px;
overflow-y: auto;
padding: 8px 0;
}
.item-row {
padding: 8px 12px;
border-bottom: 1px solid #ebeef5;
}
.selected-card {
margin-bottom: 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
cursor: pointer;
background: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: #303133;
}
.expand-icon {
margin-left: 8px;
color: #909399;
}
.method-list {
padding: 8px 12px;
background: #fff;
}
.method-row {
padding: 6px 0;
border-bottom: 1px dashed #ebeef5;
}
.method-row:last-child {
border-bottom: none;
}
.empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
}
</style>

View File

@@ -1,91 +0,0 @@
<template>
<div class="order-list-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>门诊医嘱</span>
<el-button type="primary" @click="handleAddOrder">新增医嘱</el-button>
</div>
</template>
<el-table
:data="orderList"
border
style="width: 100%"
data-cy="order-list"
v-loading="loading"
>
<el-table-column prop="itemName" label="项目名称" min-width="150" />
<el-table-column label="总量" width="120" align="center">
<template #default="{ row }">
<span data-cy="total-quantity">
{{ row.quantity ?? 0 }} {{ row.unit || '' }}
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'PENDING' ? 'warning' : 'success'">
{{ row.status === 'PENDING' ? '待执行' : '已完成' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-button type="danger" link @click="handleCancel(row)">撤销</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getOrderDetails } from '@/api/outpatient/order'
const props = defineProps({
patientId: { type: String, required: true }
})
const loading = ref(false)
const orderList = ref([])
const fetchOrders = async () => {
loading.value = true
try {
const res = await getOrderDetails(props.patientId)
orderList.value = res.data || []
} catch (e) {
console.error('Failed to fetch orders:', e)
ElMessage.error('获取医嘱列表失败')
} finally {
loading.value = false
}
}
const handleAddOrder = () => {
// 触发新增医嘱逻辑
ElMessage.info('打开新增医嘱弹窗')
}
const handleCancel = (row) => {
ElMessage.warning(`撤销医嘱: ${row.itemName}`)
}
onMounted(() => {
fetchOrders()
})
</script>
<style scoped>
.order-list-container {
padding: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -1,115 +0,0 @@
<template>
<div class="pending-records-container">
<el-card shadow="never" class="pending-card">
<template #header>
<div class="card-header">
<span>待写病历</span>
<el-button type="primary" @click="fetchRecords" :loading="loading">刷新</el-button>
</div>
</template>
<!-- 显式加载遮罩匹配 E2E 测试选择器 -->
<div v-if="loading" class="loading-overlay" data-cy="loading-spinner">
<el-icon class="is-loading"><Loading /></el-icon>
<span>数据加载中...</span>
</div>
<el-table
:data="recordList"
border
style="width: 100%; margin-top: 16px"
data-cy="record-list"
v-show="!loading && recordList.length"
class="record-table"
>
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="visitDate" label="就诊日期" width="150" />
<el-table-column prop="diagnosis" label="初步诊断" min-width="200" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleWrite(row)" data-cy="record-item">
书写病历
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 当没有数据时的占位提示防止页面出现空白或错位 -->
<div v-else-if="!loading && !recordList.length" class="empty-state">
<el-empty description="暂无待写病历" />
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import { getPendingMedicalRecords } from '@/api/outpatient/medicalRecord'
const loading = ref(false)
const recordList = ref([])
const fetchRecords = async () => {
loading.value = true
try {
const res = await getPendingMedicalRecords()
// 假设后端返回 { data: [...] }
recordList.value = res.data || []
} catch (e) {
ElMessage.error('加载待写病历失败')
} finally {
loading.value = false
}
}
// 页面挂载后自动加载
onMounted(() => {
fetchRecords()
})
</script>
<style scoped>
.pending-records-container {
padding: 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 120px);
}
/* 卡片内部统一留白,避免内容贴边 */
.pending-card {
padding: 16px;
}
/* 表头与按钮对齐 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 加载遮罩居中显示 */
.loading-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #606266;
}
/* 表格在加载完成后保持固定高度,防止页面抖动 */
.record-table {
min-height: 200px;
}
/* 空数据占位样式 */
.empty-state {
padding: 40px 0;
text-align: center;
}
</style>

View File

@@ -1,238 +0,0 @@
<template>
<div class="check-request-container">
<div class="layout-wrapper">
<!-- 左侧检查项目分类 -->
<div class="panel left-panel">
<h3 class="panel-title">检查项目分类</h3>
<el-tree
:data="categoryTree"
:props="{ label: 'name', children: 'children' }"
@node-click="handleCategoryClick"
highlight-current
default-expand-all
class="category-tree"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="panel middle-panel">
<h3 class="panel-title">检查项目</h3>
<el-table
:data="currentItems"
@selection-change="handleItemSelection"
row-key="id"
border
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="项目名称" show-overflow-tooltip />
<el-table-column prop="spec" label="规格" width="120" />
</el-table>
</div>
<!-- 右侧已选择区域 & 检查方法 -->
<div class="panel right-panel">
<h3 class="panel-title">已选择项目</h3>
<div class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<div class="card-header" @click="toggleExpand(item)">
<el-checkbox
v-model="item.checked"
@change="handleItemCheckChange(item)"
@click.stop
/>
<el-tooltip :content="cleanName(item.name)" placement="top" :show-after="300">
<span class="item-name">{{ cleanName(item.name) }}</span>
</el-tooltip>
<el-icon class="expand-icon">
<ArrowDown v-if="item.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 检查方法/明细区域 (默认收起) -->
<transition name="slide-fade">
<div v-show="item.expanded" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<el-checkbox
v-model="method.checked"
@change="handleMethodCheckChange(item, method)"
@click.stop
/>
<span class="method-name" :title="method.name">{{ method.name }}</span>
</div>
</div>
</transition>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无已选项目" :image-size="60" />
</div>
<div class="panel-footer">
<el-button type="primary" @click="submitRequest" :disabled="selectedItems.length === 0">提交申请</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const categoryTree = ref([])
const currentItems = ref([])
const selectedItems = ref([])
const handleCategoryClick = (data) => {
currentItems.value = data.children || []
}
const handleItemSelection = (selection) => {
// 将选中的项目加入右侧列表,默认收起,独立勾选状态
selection.forEach(item => {
if (!selectedItems.value.find(s => s.id === item.id)) {
selectedItems.value.push({
...item,
checked: true,
expanded: false, // 默认收起
methods: item.methods || [] // 保持方法独立
})
}
})
// 处理取消选择
selectedItems.value = selectedItems.value.filter(s => selection.find(sel => sel.id === s.id))
}
const toggleExpand = (item) => {
item.expanded = !item.expanded
}
// 清理名称:去除“套餐”、“项目套餐明细”等冗余前缀/后缀
const cleanName = (name) => {
if (!name) return ''
return name.replace(/(项目套餐明细|套餐)/g, '').trim()
}
// 项目勾选变更:仅更新自身状态,不联动方法(解耦核心)
const handleItemCheckChange = (item) => {
// 明确不修改 item.methods 的 checked 状态,保持父子独立
}
// 方法勾选变更:仅更新自身状态
const handleMethodCheckChange = (item, method) => {
// 独立控制,不反向影响父级 item.checked
}
const submitRequest = async () => {
const payload = selectedItems.value
.filter(i => i.checked)
.map(i => ({
itemCode: i.code,
patientId: 'CURRENT_PATIENT_ID',
doctorId: 'CURRENT_DOCTOR_ID',
methods: i.methods.filter(m => m.checked).map(m => m.code)
}))
if (payload.length === 0) {
ElMessage.warning('请至少勾选一个项目或方法')
return
}
try {
// await submitCheckRequest(payload)
ElMessage.success('提交成功')
selectedItems.value = []
} catch (e) {
ElMessage.error(e.message || '提交失败')
}
}
</script>
<style scoped>
.check-request-container {
height: 100%;
padding: 16px;
box-sizing: border-box;
}
.layout-wrapper {
display: flex;
gap: 16px;
height: 100%;
}
.panel {
background: #fff;
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
}
.left-panel { width: 20%; }
.middle-panel { width: 45%; }
.right-panel { width: 35%; }
.panel-title {
margin: 0 0 12px;
font-size: 16px;
font-weight: 600;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.selected-list {
flex: 1;
overflow-y: auto;
padding-right: 4px;
}
.selected-card {
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 10px;
background: #fafafa;
width: 100%; /* 宽度自适应容器 */
box-sizing: border-box;
}
.card-header {
display: flex;
align-items: center;
padding: 10px;
cursor: pointer;
gap: 8px;
}
.item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.expand-icon {
font-size: 14px;
color: #909399;
}
.method-list {
padding: 0 10px 10px 30px;
border-top: 1px dashed #eee;
}
.method-item {
display: flex;
align-items: center;
padding: 6px 0;
gap: 8px;
}
.method-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
color: #606266;
}
.panel-footer {
margin-top: 12px;
text-align: right;
}
.slide-fade-enter-active, .slide-fade-leave-active {
transition: all 0.3s ease;
}
.slide-fade-enter-from, .slide-fade-leave-to {
opacity: 0;
transform: translateY(-10px);
}
</style>

View File

@@ -1,202 +0,0 @@
<template>
<div class="exam-apply-container">
<el-row :gutter="16" class="layout-grid">
<!-- 左侧检查项目分类 -->
<el-col :span="5">
<el-card class="panel-card" shadow="never">
<template #header>检查项目分类</template>
<el-tree
:data="categoryTree"
:props="{ label: 'name', children: 'children' }"
highlight-current
@node-click="handleCategorySelect"
/>
</el-card>
</el-col>
<!-- 中间检查项目列表 -->
<el-col :span="10">
<el-card class="panel-card" shadow="never">
<template #header>检查项目</template>
<div class="exam-item-list">
<el-checkbox-group v-model="selectedItemIds" @change="onItemChange">
<el-checkbox
v-for="item in currentItems"
:key="item.id"
:label="item.id"
:value="item.id"
class="item-checkbox"
>
{{ cleanName(item.name) }}
</el-checkbox>
</el-checkbox-group>
</div>
</el-card>
</el-col>
<!-- 右侧已选择区域 -->
<el-col :span="9">
<el-card class="panel-card" shadow="never">
<template #header>已选择</template>
<div class="selected-list">
<div
v-for="item in selectedItemsHierarchy"
:key="item.id"
class="selected-item-wrapper"
>
<!-- 项目头部点击展开/收起 -->
<div class="item-header" @click="toggleExpand(item.id)">
<el-icon class="expand-icon">
<ArrowDown v-if="expandedIds.has(item.id)" />
<ArrowRight v-else />
</el-icon>
<el-tooltip
:content="cleanName(item.name)"
placement="top"
:show-after="300"
:hide-after="0"
>
<span class="item-name">{{ cleanName(item.name) }}</span>
</el-tooltip>
</div>
<!-- 检查方法明细默认折叠解耦独立勾选 -->
<div v-show="expandedIds.has(item.id)" class="method-list exam-method-list">
<el-checkbox-group v-model="selectedMethodIds" @change="onMethodChange">
<el-checkbox
v-for="method in item.methods"
:key="method.id"
:label="method.id"
:value="method.id"
>
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<el-empty v-if="selectedItemIds.length === 0" description="暂无选择项目" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
// 类型定义
interface ExamMethod { id: string; name: string }
interface ExamItem { id: string; name: string; methods?: ExamMethod[] }
interface CategoryNode { id: string; name: string; children: ExamItem[] }
// 状态管理
const categoryTree = ref<CategoryNode[]>([])
const currentItems = ref<ExamItem[]>([])
const selectedItemIds = ref<string[]>([])
const selectedMethodIds = ref<string[]>([])
const expandedIds = ref<Set<string>>(new Set())
// 清理名称:去除冗余的“套餐”前缀及冒号
const cleanName = (name: string) => name.replace(/^套餐[:]/, '').trim()
// 构建已选项目的层级结构(项目 > 检查方法)
const selectedItemsHierarchy = computed(() => {
const allItems = categoryTree.value.flatMap(c => c.children || [])
return allItems.filter(item => selectedItemIds.value.includes(item.id))
})
// 分类切换
const handleCategorySelect = (data: CategoryNode) => {
currentItems.value = data.children || []
}
// 项目勾选变更(解耦:不联动检查方法)
const onItemChange = (ids: string[]) => {
selectedItemIds.value = ids
// 新增项目默认保持收起状态,不自动展开或勾选方法
}
// 方法勾选变更(独立维护)
const onMethodChange = (ids: string[]) => {
selectedMethodIds.value = ids
}
// 展开/收起控制
const toggleExpand = (id: string) => {
if (expandedIds.value.has(id)) {
expandedIds.value.delete(id)
} else {
expandedIds.value.add(id)
}
}
</script>
<style scoped>
.exam-apply-container {
padding: 16px;
height: 100%;
background: #f5f7fa;
}
.layout-grid {
height: 100%;
}
.panel-card {
height: 100%;
display: flex;
flex-direction: column;
}
.panel-card :deep(.el-card__body) {
flex: 1;
overflow-y: auto;
}
.exam-item-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.item-checkbox {
margin-right: 0;
}
.selected-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.selected-item-wrapper {
border: 1px solid #e4e7ed;
border-radius: 6px;
background: #fff;
overflow: hidden;
}
.item-header {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
background: #fafafa;
transition: background 0.2s;
}
.item-header:hover {
background: #f0f2f5;
}
.expand-icon {
margin-right: 8px;
color: #909399;
transition: transform 0.2s;
}
.item-name {
flex: 1;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.method-list {
padding: 10px 12px 10px 32px;
border-top: 1px dashed #ebeef5;
background: #fff;
}
</style>

View File

@@ -1,147 +0,0 @@
<template>
<div class="examination-page">
<!-- 检查项目分类树 -->
<el-tree
:data="categoryTree"
node-key="id"
:props="defaultProps"
@node-click="handleCategoryClick"
highlight-current
/>
<!-- 检查方法列表 -->
<el-table
v-if="methodList.length"
:data="methodList"
style="width: 100%; margin-top: 20px"
@row-click="handleMethodRowClick"
>
<el-table-column prop="name" label="检查方法" />
<el-table-column label="选择" width="80" align="center">
<template #default="{ row }">
<!-- 仅在用户手动点击时才勾选避免自动勾选冲突 -->
<el-checkbox
class="exam-method-checkbox"
:model-value="isSelected(row)"
@change="toggleMethodSelection(row, $event)"
/>
</template>
</el-table-column>
</el-table>
<!-- 已选检查项目卡片 -->
<div v-if="selectedMethod" class="selected-item-card">
<div class="card-header" @click="toggleDetail">
<span class="item-name">{{ selectedMethod.name }}</span>
<el-icon :class="{ rotated: detailVisible }"><arrow-down /></el-icon>
</div>
<div class="card-detail" v-show="detailVisible">
<!-- 只展示检查方法的层级信息避免套餐明细干扰 -->
<div class="hierarchy-tip">
检查项目 > 检查方法
</div>
<!-- 这里可以放置方法的具体描述或注意事项 -->
<p class="method-description">{{ selectedMethod.description }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { ArrowDown } from '@element-plus/icons-vue';
import { getExamCategories, getMethodsByCategory } from '@/api/outpatient/examination';
// 分类树数据
const categoryTree = ref([]);
const defaultProps = {
children: 'children',
label: 'name',
};
// 当前选中的检查方法(单选,解耦套餐与方法的勾选)
const selectedMethod = ref(null);
const detailVisible = ref(false);
// 方法列表
const methodList = ref([]);
// 加载检查项目分类
(async () => {
const res = await getExamCategories();
categoryTree.value = res.data || [];
})();
// 分类点击后加载对应的检查方法
const handleCategoryClick = async (node) => {
const res = await getMethodsByCategory(node.id);
methodList.value = res.data || [];
// 切换分类时自动清除已选方法,防止跨分类残留
selectedMethod.value = null;
detailVisible.value = false;
};
// 判断方法是否已被选中(仅用于 UI 绑定)
const isSelected = (method) => {
return selectedMethod.value && selectedMethod.value.id === method.id;
};
// 勾选框点击事件:完全由用户行为决定是否选中
const toggleMethodSelection = (method, checked) => {
if (checked) {
// 只保留当前选中的方法,自动取消其他已选(单选)
selectedMethod.value = method;
detailVisible.value = false;
} else {
// 取消勾选时清空选中状态
if (selectedMethod.value && selectedMethod.value.id === method.id) {
selectedMethod.value = null;
detailVisible.value = false;
}
}
};
// 行点击同样触发勾选,提升交互体验
const handleMethodRowClick = (row) => {
const currentlyChecked = isSelected(row);
toggleMethodSelection(row, !currentlyChecked);
};
// 展开/收起详情卡片
const toggleDetail = () => {
detailVisible.value = !detailVisible.value;
};
</script>
<style scoped>
.examination-page {
padding: 20px;
}
.selected-item-card {
margin-top: 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
cursor: pointer;
background-color: #f5f7fa;
}
.item-name {
font-weight: 600;
}
.card-detail {
padding: 10px;
background-color: #fff;
}
.hierarchy-tip {
color: #909399;
margin-bottom: 8px;
}
.rotated {
transform: rotate(180deg);
}
</style>

View File

@@ -1,255 +0,0 @@
<template>
<div class="exam-application-container">
<div class="layout-wrapper">
<!-- 左侧分类树 -->
<div class="panel category-panel">
<h3 class="panel-title">检查项目分类</h3>
<el-tree
:data="categoryTree"
:props="{ label: 'name', children: 'children' }"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
<!-- 中间项目列表 -->
<div class="panel item-panel">
<h3 class="panel-title">检查项目</h3>
<div class="item-list">
<div v-for="item in currentItems" :key="item.id" class="item-row">
<el-checkbox v-model="item.checked" @change="onItemCheck(item)">
{{ cleanName(item.name) }}
</el-checkbox>
</div>
</div>
</div>
<!-- 右侧/下方已选择区域核心修复区 -->
<div class="panel selected-panel">
<h3 class="panel-title">已选择</h3>
<div class="hierarchy-tip">检查项目 > 检查方法</div>
<div v-if="selectedGroups.length === 0" class="empty-tip">暂无选择项目</div>
<div v-for="group in selectedGroups" :key="group.itemId" class="selected-item-card">
<div class="card-header" @click="toggleExpand(group)">
<el-checkbox
v-model="group.checked"
@change="onGroupCheck(group)"
@click.stop
/>
<span class="item-name" :title="group.itemName">{{ group.itemName }}</span>
<el-icon class="expand-icon">
<ArrowRight v-if="!group.expanded" />
<ArrowDown v-else />
</el-icon>
</div>
<div v-show="group.expanded" class="card-detail">
<div v-for="method in group.methods" :key="method.id" class="method-row exam-method-checkbox">
<el-checkbox v-model="method.checked" @change="onMethodCheck(group, method)" />
<span class="method-name" :title="method.name">{{ method.name }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ArrowRight, ArrowDown } from '@element-plus/icons-vue'
// 模拟分类数据
const categoryTree = ref([
{ id: 1, name: '彩超', children: [] },
{ id: 2, name: 'CT', children: [] }
])
// 模拟当前分类下的项目
const currentItems = ref([
{ id: 101, name: '128线排套餐', checked: false, methods: [
{ id: 1001, name: '常规腹部', checked: false },
{ id: 1002, name: '血管多普勒', checked: false }
]},
{ id: 102, name: '心脏彩超', checked: false, methods: [] }
])
// 已选择分组数据(项目 > 方法 层级结构)
const selectedGroups = ref([])
// 清理名称中的“套餐”冗余字样
const cleanName = (name) => name.replace(/套餐/g, '')
// 点击分类加载项目(模拟)
const handleCategoryClick = (node) => {
// 实际应调用 API 获取项目列表
console.log('加载分类:', node.name)
}
// 项目勾选逻辑(解耦:不联动方法)
const onItemCheck = (item) => {
if (item.checked) {
// 若未加入已选列表,则初始化加入
const exists = selectedGroups.value.find(g => g.itemId === item.id)
if (!exists) {
selectedGroups.value.push({
itemId: item.id,
itemName: cleanName(item.name),
checked: true,
expanded: false, // 默认收起
methods: item.methods.map(m => ({ ...m, checked: false })) // 方法默认不勾选
})
}
} else {
// 取消勾选则从已选移除
selectedGroups.value = selectedGroups.value.filter(g => g.itemId !== item.id)
}
}
// 已选卡片项目勾选(保持独立状态)
const onGroupCheck = (group) => {
// 仅更新当前项目状态,不向下联动方法
// 若业务要求取消项目时同步取消方法,可在此处添加逻辑,但按需求保持解耦
}
// 方法勾选逻辑(完全独立)
const onMethodCheck = (group, method) => {
// 仅更新当前方法状态,不向上联动项目
}
// 展开/收起明细
const toggleExpand = (group) => {
group.expanded = !group.expanded
}
</script>
<style scoped>
.exam-application-container {
padding: 16px;
background: #f5f7fa;
height: 100%;
}
.layout-wrapper {
display: grid;
grid-template-columns: 240px 1fr 320px;
gap: 16px;
height: calc(100vh - 120px);
}
.panel {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
overflow-y: auto;
}
.panel-title {
margin: 0 0 12px;
font-size: 15px;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #ebeef5;
padding-bottom: 8px;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.item-row {
padding: 8px;
border-radius: 4px;
transition: background 0.2s;
}
.item-row:hover {
background: #f5f7fa;
}
/* 已选择区域核心样式 */
.selected-panel {
display: flex;
flex-direction: column;
}
.hierarchy-tip {
font-size: 12px;
color: #909399;
margin-bottom: 12px;
padding: 4px 8px;
background: #f0f2f5;
border-radius: 4px;
display: inline-block;
}
.empty-tip {
text-align: center;
color: #c0c4cc;
padding: 40px 0;
}
.selected-item-card {
border: 1px solid #dcdfe6;
border-radius: 6px;
margin-bottom: 10px;
background: #fff;
transition: box-shadow 0.2s;
}
.selected-item-card:hover {
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.card-header {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
user-select: none;
background: #fafafa;
border-radius: 6px 6px 0 0;
}
.item-name {
flex: 1;
margin-left: 10px;
font-weight: 500;
color: #303133;
/* 宽度自适应 + 超长省略 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.expand-icon {
margin-left: 8px;
color: #909399;
transition: transform 0.2s;
}
.card-detail {
padding: 8px 12px 12px 36px;
border-top: 1px dashed #ebeef5;
background: #fff;
}
.method-row {
display: flex;
align-items: center;
padding: 6px 0;
}
.method-name {
margin-left: 8px;
color: #606266;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -1,73 +0,0 @@
<template>
<div class="medical-record-pending">
<el-table :data="recordList" style="width: 100%" v-loading="loading">
<el-table-column prop="patientName" label="患者" min-width="120" />
<el-table-column prop="doctorName" label="医生" min-width="120" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="openRecord(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper" style="margin-top: 20px; text-align: right;">
<el-pagination
background
layout="total, prev, pager, next, sizes, jumper"
:total="total"
:page-size="pageSize"
:current-page="pageNum"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { getPendingMedicalRecordsApi } from '@/api/outpatient/medicalRecord';
const recordList = ref([]);
const total = ref(0);
const loading = ref(false);
const pageNum = ref(1);
const pageSize = ref(20);
const loadData = async () => {
loading.value = true;
try {
const res = await getPendingMedicalRecordsApi({ pageNum: pageNum.value, pageSize: pageSize.value });
recordList.value = res.data || [];
total.value = res.total || 0;
} catch (e) {
ElMessage.error('加载待写病历失败');
} finally {
loading.value = false;
}
};
const handlePageChange = (newPage) => {
pageNum.value = newPage;
loadData();
};
const handleSizeChange = (newSize) => {
pageSize.value = newSize;
pageNum.value = 1;
loadData();
};
onMounted(() => {
loadData();
});
</script>
<style scoped>
.pagination-wrapper {
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -1,73 +0,0 @@
<template>
<div class="order-list-container">
<el-card header="门诊医嘱管理">
<div class="toolbar mb-4">
<el-select v-model="selectedPatientId" placeholder="请选择患者" @change="fetchOrders" class="mr-4">
<el-option v-for="p in patientList" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
<el-button type="primary" @click="openSurgeryDialog">手术申请</el-button>
</div>
<el-table :data="orderList" v-loading="loading" border class="order-table">
<el-table-column prop="orderNo" label="医嘱号" width="120" />
<el-table-column prop="catalogName" label="项目名称" show-overflow-tooltip />
<el-table-column label="总量" width="120" align="center">
<template #default="{ row }">
<span class="total-quantity-cell">
{{ row.totalQuantity }} {{ row.totalUnit || '次' }}
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 0 ? 'info' : 'success'">
{{ row.status === 0 ? '待执行' : '已执行' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="开立时间" width="180" />
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getOrdersByPatientIdApi } from '@/api/outpatient/order';
const loading = ref(false);
const orderList = ref([]);
const patientList = ref([]);
const selectedPatientId = ref(null);
const fetchOrders = async () => {
if (!selectedPatientId.value) return;
loading.value = true;
try {
const res = await getOrdersByPatientIdApi(selectedPatientId.value);
orderList.value = res.data || [];
} catch (e) {
console.error('获取医嘱失败', e);
} finally {
loading.value = false;
}
};
const openSurgeryDialog = () => {
// 模拟跳转或弹窗逻辑
window.location.href = '/outpatient/doctor/surgery-apply';
};
onMounted(() => {
// 初始化患者列表(示例)
patientList.value = [{ id: 1, name: '测试患者' }];
selectedPatientId.value = 1;
fetchOrders();
});
</script>
<style scoped>
.order-list-container { padding: 20px; }
.toolbar { display: flex; align-items: center; }
.total-quantity-cell { font-weight: 500; }
</style>

View File

@@ -1,106 +0,0 @@
<template>
<div class="pending-medical-record-container">
<div class="header-bar">
<h3>待写病历</h3>
<el-button type="primary" :loading="loading" @click="fetchRecords">刷新</el-button>
</div>
<el-table v-loading="loading" :data="recordList" style="width: 100%" stripe>
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="visitNo" label="就诊号" width="150" />
<el-table-column prop="createTime" label="就诊时间" width="180" />
<el-table-column prop="diagnosis" label="初步诊断" show-overflow-tooltip />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleWrite(row)">书写</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="fetchRecords"
@size-change="fetchRecords"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { getPendingMedicalRecords } from '@/api/outpatient/medicalRecord'
const router = useRouter()
const loading = ref(false)
const recordList = ref([])
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
// 修复 #562使用 AbortController 取消重复/过期请求,防止并发堆积导致 UI 假死
let abortController = null
const fetchRecords = async () => {
if (abortController) {
abortController.abort()
}
abortController = new AbortController()
loading.value = true
try {
const res = await getPendingMedicalRecords({
pageNum: currentPage.value,
pageSize: pageSize.value,
status: 'pending'
}, { signal: abortController.signal })
recordList.value = res.data.list || []
total.value = res.data.total || 0
} catch (error) {
if (error.name !== 'AbortError') {
console.error('加载待写病历失败:', error)
}
} finally {
loading.value = false
}
}
const handleWrite = (row) => {
router.push({ name: 'EmrEditor', query: { recordId: row.id, visitNo: row.visitNo } })
}
onMounted(() => {
fetchRecords()
})
onUnmounted(() => {
if (abortController) {
abortController.abort()
}
})
</script>
<style scoped>
.pending-medical-record-container {
padding: 16px;
background: #fff;
border-radius: 8px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -1,127 +0,0 @@
<template>
<div class="pending-records-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>待写病历</span>
<el-button type="primary" @click="refreshData" :loading="loading">刷新</el-button>
</div>
</template>
<!-- 修复添加 loading 遮罩与数据表格 -->
<div v-loading="loading" element-loading-text="加载中..." class="table-wrapper">
<el-table
v-if="tableData.length > 0"
:data="tableData"
style="width: 100%"
border
stripe
data-testid="record-table"
>
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="visitNo" label="就诊号" width="150" />
<el-table-column prop="visitDate" label="就诊时间" width="180" />
<el-table-column prop="diagnosis" label="初步诊断" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleWrite(row)">书写病历</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无待写病历" />
</div>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { getPendingMedicalRecords } from '@/api/outpatient/medicalRecord';
const loading = ref(false);
const tableData = ref([]);
const pagination = ref({
pageNum: 1,
pageSize: 20,
total: 0
});
// 修复:使用 async/await 避免阻塞主线程,并添加超时保护
const fetchData = async () => {
loading.value = true;
try {
const res = await getPendingMedicalRecords({
pageNum: pagination.value.pageNum,
pageSize: pagination.value.pageSize
});
if (res.code === 200) {
tableData.value = res.data.list || [];
pagination.value.total = res.data.total || 0;
} else {
ElMessage.error(res.msg || '获取数据失败');
}
} catch (error) {
console.error('加载待写病历异常:', error);
ElMessage.error('网络请求超时或异常,请重试');
} finally {
loading.value = false;
}
};
const refreshData = () => {
pagination.value.pageNum = 1;
fetchData();
};
const handleSizeChange = (size) => {
pagination.value.pageSize = size;
fetchData();
};
const handlePageChange = (page) => {
pagination.value.pageNum = page;
fetchData();
};
const handleWrite = (row) => {
// 路由跳转逻辑占位
console.log('书写病历:', row.visitNo);
};
onMounted(() => {
fetchData();
});
</script>
<style scoped>
.pending-records-container {
padding: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.table-wrapper {
min-height: 300px;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -1,73 +0,0 @@
<template>
<div class="queue-list">
<!-- 当前排队 -->
<el-table :data="currentQueue" style="width: 100%">
<el-table-column prop="queueNo" label="排号" width="80"/>
<el-table-column prop="patientName" label="患者"/>
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 历史排队 -->
<el-divider>历史排队</el-divider>
<el-table :data="historyQueue" style="width: 100%">
<el-table-column prop="queueNo" label="排号" width="80"/>
<el-table-column prop="patientName" label="患者"/>
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="时间"/>
</el-table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useApi } from '@/utils/api';
const currentQueue = ref([]);
const historyQueue = ref([]);
function statusLabel(status) {
const map = {
WAIT: '待诊',
DIAGNOSE: '已诊',
FINISHED: '完诊',
CANCELLED: '已取消'
};
return map[status] || status;
}
function statusTagType(status) {
const map = {
WAIT: 'info',
DIAGNOSE: 'warning',
FINISHED: 'success',
CANCELLED: 'danger'
};
return map[status] || 'default';
}
onMounted(async () => {
// 当前排队(包含完诊)
const curRes = await useApi().get('/api/queue/current');
currentQueue.value = curRes.data;
// 历史排队
const histRes = await useApi().get('/api/queue/history', {
params: { startTime: '', endTime: '' }
});
historyQueue.value = histRes.data;
});
</script>
<style scoped>
.queue-list {
padding: 20px;
}
</style>

View File

@@ -1,162 +0,0 @@
<template>
<div class="exam-item-selector">
<div class="selector-layout">
<!-- 左侧检查项目分类 -->
<div class="panel category-panel">
<h4 class="panel-title">检查项目分类</h4>
<el-tree
:data="categoryTree"
node-key="id"
highlight-current
default-expand-all
@node-click="handleCategorySelect"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="panel item-panel">
<h4 class="panel-title">检查项目</h4>
<el-checkbox-group v-model="selectedItemIds" @change="onItemChange" class="item-list">
<el-checkbox v-for="item in currentItems" :key="item.id" :label="item.id" class="item-checkbox">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 右侧/下方已选择区域 -->
<div class="panel selected-panel">
<h4 class="panel-title">已选择</h4>
<!-- 修复3结构化展示 & 默认收起 -->
<el-collapse v-model="activeCollapseNames" accordion class="selected-collapse">
<el-collapse-item
v-for="group in selectedGroups"
:key="group.itemId"
:name="group.itemId"
>
<template #title>
<!-- 修复2宽度自适应/提示完整名称去除套餐前缀 -->
<el-tooltip :content="cleanName(group.itemName)" placement="top" :show-after="300">
<span class="group-title">{{ cleanName(group.itemName) }}</span>
</el-tooltip>
</template>
<div class="method-section">
<el-checkbox-group v-model="group.selectedMethodIds" @change="onMethodChange(group)">
<el-checkbox v-for="method in group.methods" :key="method.id" :label="method.id">
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
interface ExamMethod { id: string; name: string; }
interface ExamItem { id: string; name: string; methods: ExamMethod[]; }
interface SelectedGroup { itemId: string; itemName: string; methods: ExamMethod[]; selectedMethodIds: string[]; }
const categoryTree = ref<any[]>([]);
const currentItems = ref<ExamItem[]>([]);
const selectedItemIds = ref<string[]>([]);
const selectedGroups = ref<SelectedGroup[]>([]);
const activeCollapseNames = ref<string[]>([]); // 默认空数组,实现默认收起
// 清理名称:去除“套餐”等冗余字样
const cleanName = (name: string) => name.replace(/套餐/g, '').trim();
const handleCategorySelect = (node: any) => {
// 实际项目中替换为 API 请求
currentItems.value = node.items || [];
};
// 修复1项目勾选与检查方法解耦
const onItemChange = (newIds: string[]) => {
const addedIds = newIds.filter(id => !selectedItemIds.value.includes(id));
const removedIds = selectedItemIds.value.filter(id => !newIds.includes(id));
// 新增项目仅加入已选列表selectedMethodIds 初始化为空,杜绝自动勾选
addedIds.forEach(id => {
const item = currentItems.value.find(i => i.id === id);
if (item) {
selectedGroups.value.push({
itemId: item.id,
itemName: item.name,
methods: item.methods || [],
selectedMethodIds: []
});
}
});
// 移除项目:同步清理已选分组
selectedGroups.value = selectedGroups.value.filter(g => !removedIds.includes(g.itemId));
selectedItemIds.value = newIds;
};
// 修复1续方法勾选独立更新不反向影响项目状态
const onMethodChange = (group: SelectedGroup) => {
// 可在此处触发父组件数据同步或费用计算
};
defineExpose({ selectedGroups });
</script>
<style scoped>
.exam-item-selector {
height: 100%;
padding: 10px;
box-sizing: border-box;
}
.selector-layout {
display: flex;
gap: 12px;
height: 100%;
}
.panel {
flex: 1;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
display: flex;
flex-direction: column;
overflow: hidden;
background: #fff;
}
.panel-title {
margin: 0 0 10px 0;
font-size: 14px;
font-weight: bold;
color: #303133;
}
.item-list {
flex: 1;
overflow-y: auto;
}
.item-checkbox {
display: block;
margin-bottom: 8px;
width: 100%;
}
.selected-collapse {
flex: 1;
overflow-y: auto;
}
.group-title {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: #409eff;
}
.method-section {
padding: 8px 0 0 10px;
border-left: 2px solid #e4e7ed;
}
</style>

View File

@@ -1,40 +0,0 @@
<template>
<div class="order-container">
<el-table :data="orderList" class="order-table" border stripe>
<el-table-column prop="itemName" label="项目名称" min-width="150" />
<el-table-column label="总量" class="total-quantity-cell" width="120" align="center">
<template #default="{ row }">
{{ row.totalQuantity }} {{ row.totalUnit || '' }}
</template>
</el-table-column>
<el-table-column label="状态" prop="status" width="100" align="center" />
</el-table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getOrdersByPatient } from '@/api/outpatient/order';
const orderList = ref([]);
onMounted(async () => {
// 实际项目中应从路由或全局状态获取当前就诊患者ID
const patientId = 1;
try {
const res = await getOrdersByPatient(patientId);
orderList.value = res.data || [];
} catch (error) {
console.error('加载医嘱失败:', error);
}
});
</script>
<style scoped>
.order-container {
padding: 16px;
}
.total-quantity-cell {
font-weight: 500;
}
</style>

View File

@@ -1,204 +0,0 @@
<template>
<div class="exam-application-container">
<!-- 左侧检查项目分类 -->
<div class="panel category-panel">
<h3 class="panel-title">检查项目分类</h3>
<el-tree
:data="categories"
:props="{ label: 'name', children: 'items' }"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="panel item-panel">
<h3 class="panel-title">检查项目</h3>
<el-checkbox-group v-model="selectedItemIds" @change="handleItemChange">
<div v-for="item in currentItems" :key="item.id" class="item-row">
<el-checkbox :label="item.id">{{ item.name }}</el-checkbox>
</div>
</el-checkbox-group>
</div>
<!-- 右侧已选择区域 & 检查方法/明细 -->
<div class="panel selected-panel">
<h3 class="panel-title">已选择</h3>
<!-- 已移除冗余的项目套餐明细标签 -->
<div class="selected-list">
<div v-for="group in selectedGroups" :key="group.itemId" class="item-group">
<!-- 项目卡片宽度自适应悬浮提示完整名称清理套餐前缀 -->
<div class="item-card" @click="toggleDetail(group.itemId)">
<el-tooltip :content="group.itemName" placement="top" :show-after="500">
<span class="item-name">{{ group.itemName }}</span>
</el-tooltip>
<el-icon class="toggle-icon">
<ArrowDown v-if="!expandedIds.includes(group.itemId)" />
<ArrowUp v-else />
</el-icon>
</div>
<!-- 检查方法/明细区域默认收起独立勾选严格遵循 项目 > 检查方法 层级 -->
<el-collapse-transition>
<div v-show="expandedIds.includes(group.itemId)" class="method-detail-area">
<el-checkbox-group v-model="selectedMethodIds" @change="handleMethodChange">
<div v-for="method in group.methods" :key="method.id" class="method-row">
<el-checkbox :label="method.id">{{ method.name }}</el-checkbox>
</div>
</el-checkbox-group>
</div>
</el-collapse-transition>
</div>
<el-empty v-if="selectedGroups.length === 0" description="暂无已选项目" :image-size="60" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
// 状态定义
const categories = ref([]) // 实际应从后端API获取
const currentItems = ref([])
const selectedItemIds = ref([])
const selectedMethodIds = ref([])
const expandedIds = ref([]) // 控制明细展开/收起,默认空数组(收起)
// 已选择分组数据(严格遵循 项目 > 检查方法 层级)
const selectedGroups = computed(() => {
return selectedItemIds.value.map(id => {
const item = findItemById(id)
return {
itemId: item.id,
// 清理冗余的“套餐”前缀,支持多种常见写法
itemName: item.name.replace(/^(套餐|项目套餐)[:]/, ''),
methods: item.methods || [] // 关联的检查方法独立存储
}
})
})
// 分类点击:加载对应项目
const handleCategoryClick = (data) => {
currentItems.value = data.items || []
}
// 项目勾选变更:解耦处理,不联动检查方法
const handleItemChange = (ids) => {
selectedItemIds.value = ids
// 新增项目时保持默认收起状态,不自动展开明细
}
// 检查方法勾选变更:独立更新,不影响项目状态
const handleMethodChange = (ids) => {
selectedMethodIds.value = ids
}
// 展开/收起明细面板
const toggleDetail = (itemId) => {
const idx = expandedIds.value.indexOf(itemId)
if (idx > -1) {
expandedIds.value.splice(idx, 1)
} else {
expandedIds.value.push(itemId)
}
}
// 辅助方法根据ID查找项目实际应替换为缓存或API查询
const findItemById = (id) => {
// 模拟数据结构,实际应从全局状态或接口获取
return {
id,
name: '套餐128线排',
methods: [
{ id: `${id}_m1`, name: '常规扫描' },
{ id: `${id}_m2`, name: '增强扫描' }
]
}
}
</script>
<style scoped>
.exam-application-container {
display: flex;
gap: 16px;
height: 100%;
padding: 12px;
background: #fff;
}
.panel {
flex: 1;
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 12px;
overflow-y: auto;
background: #fafafa;
}
.panel-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.item-row {
padding: 6px 0;
}
.selected-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.item-group {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.item-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
cursor: pointer;
transition: background 0.2s;
min-width: 0; /* 允许flex子项收缩解决固定宽度遮挡问题 */
}
.item-card:hover {
background: #f5f7fa;
}
.item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: #303133;
margin-right: 8px;
}
.toggle-icon {
color: #909399;
font-size: 14px;
}
.method-detail-area {
padding: 8px 12px 12px 24px;
background: #f9fafc;
border-top: 1px dashed #e4e7ed;
}
.method-row {
padding: 4px 0;
color: #606266;
}
</style>

View File

@@ -1,191 +0,0 @@
<template>
<div class="exam-apply-container">
<el-row :gutter="16">
<!-- 左侧检查项目分类 -->
<el-col :span="5">
<el-card shadow="never" class="panel-card">
<template #header><span class="panel-title">检查项目分类</span></template>
<el-tree
:data="categoryTree"
:props="{ label: 'name', children: 'children' }"
highlight-current
@node-click="handleCategoryClick"
/>
</el-card>
</el-col>
<!-- 中间检查项目列表 -->
<el-col :span="10">
<el-card shadow="never" class="panel-card">
<template #header><span class="panel-title">检查项目</span></template>
<el-table
:data="currentItems"
row-key="id"
@selection-change="handleItemSelection"
style="width: 100%"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="name" label="项目名称" show-overflow-tooltip />
</el-table>
</el-card>
</el-col>
<!-- 右侧已选择区域核心修复区 -->
<el-col :span="9">
<el-card shadow="never" class="panel-card selected-panel">
<template #header><span class="panel-title">已选择</span></template>
<div class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<!-- 卡片头部项目勾选 + 名称 + 展开/收起 -->
<div class="card-header" @click="toggleExpand(item)">
<el-checkbox
v-model="item.checked"
@change="handleItemCheck(item)"
@click.stop
/>
<span class="item-name" :title="cleanName(item.name)">{{ cleanName(item.name) }}</span>
<el-icon class="expand-icon">
<ArrowDown v-if="item.isExpanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 检查方法/明细区域严格遵循 项目 > 检查方法 层级默认收起 -->
<transition name="el-fade-in">
<div v-if="item.isExpanded && item.methods?.length" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<el-checkbox
v-model="method.checked"
@change="handleMethodCheck(item, method)"
@click.stop
/>
<span class="method-name" :title="method.name">{{ method.name }}</span>
</div>
</div>
</transition>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无已选项目" :image-size="60" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
// 数据结构定义
interface ExamMethod { id: string; name: string; checked: boolean }
interface ExamItem { id: string; name: string; checked: boolean; isExpanded: boolean; methods: ExamMethod[] }
const categoryTree = ref<any[]>([])
const currentItems = ref<ExamItem[]>([])
const selectedItems = ref<ExamItem[]>([])
// 修复点2清理冗余“套餐”前缀支持完整名称提示
const cleanName = (name: string) => name.replace(/套餐/g, '').trim()
// 修复点3默认收起状态点击切换展开/收起
const toggleExpand = (item: ExamItem) => {
item.isExpanded = !item.isExpanded
}
// 修复点1项目勾选独立解耦不联动检查方法
const handleItemCheck = (item: ExamItem) => {
if (item.checked) {
if (!selectedItems.value.find(i => i.id === item.id)) {
selectedItems.value.push({ ...item, isExpanded: false })
}
} else {
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id)
}
}
// 修复点1检查方法勾选独立解耦仅更新自身状态
const handleMethodCheck = (item: ExamItem, method: ExamMethod) => {
const target = selectedItems.value.find(i => i.id === item.id)
if (target) {
const m = target.methods.find(m => m.id === method.id)
if (m) m.checked = method.checked
}
}
// 中间列表选择同步至已选区域
const handleItemSelection = (selection: ExamItem[]) => {
selection.forEach(item => {
if (!selectedItems.value.find(i => i.id === item.id)) {
selectedItems.value.push({ ...item, isExpanded: false })
}
})
selectedItems.value = selectedItems.value.filter(i => selection.some(s => s.id === i.id))
}
// 分类点击加载项目(含关联检查方法)
const handleCategoryClick = (data: any) => {
currentItems.value = data.items || []
}
</script>
<style scoped>
.exam-apply-container { padding: 16px; background: #f5f7fa; min-height: 100vh; }
.panel-card { height: 100%; }
.panel-title { font-weight: 600; font-size: 15px; }
.selected-panel { height: 600px; overflow: hidden; display: flex; flex-direction: column; }
.selected-list { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 10px; }
/* 修复点2卡片宽度自适应去除固定宽度限制 */
.selected-card {
border: 1px solid #dcdfe6;
border-radius: 6px;
background: #ffffff;
padding: 10px 12px;
width: 100%;
box-sizing: border-box;
transition: all 0.2s;
}
.selected-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card-header {
display: flex;
align-items: center;
cursor: pointer;
gap: 8px;
user-select: none;
}
/* 修复点2名称溢出省略悬停提示完整内容 */
.item-name {
flex: 1;
font-weight: 500;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 修复点3层级缩进与视觉分隔 */
.method-list {
margin-top: 8px;
padding-left: 24px;
border-left: 2px solid #409eff;
background: #f9fafc;
border-radius: 0 0 4px 4px;
}
.method-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
}
.method-name {
flex: 1;
color: #606266;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.expand-icon { color: #909399; font-size: 14px; }
</style>

View File

@@ -1,220 +0,0 @@
<template>
<div class="examination-app-container">
<!-- 左侧检查项目分类 -->
<div class="left-panel">
<div class="panel-title">检查分类</div>
<el-tree
:data="categoryTree"
node-key="id"
highlight-current
@node-click="handleCategorySelect"
data-cy="category-tree"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="middle-panel">
<div class="panel-title">检查项目</div>
<el-checkbox-group v-model="selectedItemIds" class="item-list">
<el-checkbox
v-for="item in currentCategoryItems"
:key="item.id"
:label="item.id"
@change="onItemCheckChange(item, $event)"
data-cy="item-checkbox"
>
{{ cleanItemName(item.name) }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 右侧已选择区域 -->
<div class="right-panel">
<div class="panel-title">已选择</div>
<div class="selected-list">
<div
v-for="selected in selectedItemsList"
:key="selected.id"
class="selected-card"
data-cy="selected-card"
>
<div class="card-header" @click="toggleCard(selected.id)">
<span class="card-name" data-cy="selected-card-name" :title="selected.name">
{{ selected.name }}
</span>
<span class="expand-toggle" data-cy="expand-toggle">
{{ selected.expanded ? '收起' : '展开' }}
</span>
</div>
<!-- 明细/检查方法区域默认收起 -->
<div v-show="selected.expanded" class="card-details">
<!-- 已移除冗余的项目套餐明细标签 -->
<div
v-for="method in selected.methods"
:key="method.id"
class="method-row"
data-cy="method-row"
>
<el-checkbox
v-model="method.checked"
@change="onMethodCheckChange(method, $event)"
data-cy="method-checkbox"
>
{{ method.name }}
</el-checkbox>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 数据状态
const categoryTree = ref([])
const currentCategoryItems = ref([])
const selectedItemIds = ref([])
const selectedItemsList = ref([])
/**
* 清理项目名称:去除“套餐”前缀/后缀及冗余符号
*/
const cleanItemName = (name) => {
if (!name) return ''
return name.replace(/^套餐[:\s]*/g, '').replace(/套餐$/g, '')
}
/**
* 分类切换
*/
const handleCategorySelect = (node) => {
// 实际项目中此处应调用 API 获取分类下的项目列表
currentCategoryItems.value = node.items || []
}
/**
* 项目勾选变更(核心解耦逻辑)
* 修复:勾选项目时不自动联动勾选检查方法,保持独立控制
*/
const onItemCheckChange = (item, checked) => {
if (checked) {
const exists = selectedItemsList.value.find(s => s.id === item.id)
if (!exists) {
selectedItemsList.value.push({
id: item.id,
name: cleanItemName(item.name),
expanded: false, // 默认收起状态
methods: (item.methods || []).map(m => ({ ...m, checked: false })) // 方法默认未勾选,彻底解耦
})
}
} else {
selectedItemsList.value = selectedItemsList.value.filter(s => s.id !== item.id)
}
}
/**
* 检查方法勾选变更
* 修复:独立控制,不反向影响父级项目状态
*/
const onMethodCheckChange = (method, checked) => {
method.checked = checked
}
/**
* 卡片展开/收起切换
*/
const toggleCard = (id) => {
const item = selectedItemsList.value.find(s => s.id === id)
if (item) {
item.expanded = !item.expanded
}
}
</script>
<style scoped>
.examination-app-container {
display: flex;
gap: 16px;
height: 100%;
padding: 12px;
box-sizing: border-box;
}
.left-panel, .middle-panel, .right-panel {
flex: 1;
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 12px;
overflow-y: auto;
background: #fff;
}
.panel-title {
font-weight: 600;
margin-bottom: 12px;
color: #303133;
font-size: 14px;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.selected-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.selected-card {
width: 100%; /* 修复:取消固定宽度,支持自适应 */
min-width: 0; /* 防止 flex 子项溢出 */
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 10px;
background: #fafafa;
transition: all 0.2s;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
}
.card-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: #303133;
}
.expand-toggle {
color: #409eff;
font-size: 12px;
margin-left: 8px;
flex-shrink: 0;
}
.card-details {
margin-top: 8px;
padding-left: 16px;
border-top: 1px dashed #ebeef5;
padding-top: 8px;
}
.method-row {
padding: 4px 0;
display: flex;
align-items: center;
}
</style>

View File

@@ -1,183 +0,0 @@
<template>
<div class="exam-apply-container">
<el-row :gutter="20">
<!-- 左侧检查项目分类 -->
<el-col :span="6">
<el-card header="检查项目分类" shadow="never">
<el-tree
:data="categoryTree"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</el-card>
</el-col>
<!-- 中间检查项目列表 -->
<el-col :span="9">
<el-card header="检查项目" shadow="never">
<el-checkbox-group v-model="selectedItemIds" class="item-list">
<el-checkbox
v-for="item in currentItems"
:key="item.id"
:label="item.id"
@change="handleItemCheck(item)"
>
{{ cleanItemName(item.name) }}
</el-checkbox>
</el-checkbox-group>
</el-card>
</el-col>
<!-- 右侧检查方法列表 -->
<el-col :span="9">
<el-card header="检查方法" shadow="never">
<el-checkbox-group v-model="selectedMethodIds" class="method-list">
<el-checkbox
v-for="method in currentMethods"
:key="method.id"
:label="method.id"
class="exam-method-checkbox"
>
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</el-card>
</el-col>
</el-row>
<!-- 下方已选择区域 -->
<el-card class="selected-area" header="已选择" shadow="never">
<div v-if="selectedItems.length === 0" class="empty-tip">暂无选择项目</div>
<div v-else class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-item-card">
<!-- 卡片头部名称+展开收起 -->
<div class="card-header" @click="toggleDetail(item)">
<el-icon class="toggle-icon">
<ArrowRight v-if="!item.expanded" />
<ArrowDown v-else />
</el-icon>
<el-tooltip :content="cleanItemName(item.name)" placement="top" :show-after="300">
<span class="item-name">{{ cleanItemName(item.name) }}</span>
</el-tooltip>
<span class="toggle-text">{{ item.expanded ? '收起' : '展开' }}</span>
</div>
<!-- 卡片明细严格遵循 项目 > 方法 层级 -->
<div v-show="item.expanded" class="card-detail">
<div class="hierarchy-tip">检查项目 > 检查方法</div>
<div v-if="item.methods && item.methods.length > 0" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<el-checkbox v-model="method.checked" @change="handleMethodCheck(item, method)">
{{ method.name }}
</el-checkbox>
</div>
</div>
<div v-else class="no-method">无关联检查方法</div>
</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ArrowRight, ArrowDown } from '@element-plus/icons-vue'
// 状态定义
const categoryTree = ref([])
const currentItems = ref([])
const currentMethods = ref([])
const selectedItemIds = ref([])
const selectedMethodIds = ref([])
const selectedItems = ref([])
// 清理名称:去除冗余的“套餐”字样
const cleanItemName = (name) => {
if (!name) return ''
return name.replace(/^套餐[:]/, '').replace(/套餐$/, '')
}
// 分类切换
const handleCategoryClick = (data) => {
// 实际项目中此处应调用API获取项目与方法
currentItems.value = data.items || []
currentMethods.value = data.methods || []
}
// 项目勾选(解耦:仅更新项目状态,不联动方法)
const handleItemCheck = (item) => {
const exists = selectedItems.value.find(i => i.id === item.id)
if (selectedItemIds.value.includes(item.id)) {
if (!exists) {
selectedItems.value.push({
...item,
name: cleanItemName(item.name),
expanded: false, // 默认收起
methods: item.methods || []
})
}
} else {
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id)
}
}
// 方法勾选(独立:仅更新当前方法状态)
const handleMethodCheck = (parentItem, method) => {
method.checked = !method.checked
}
// 展开/收起明细
const toggleDetail = (item) => {
item.expanded = !item.expanded
}
</script>
<style scoped>
.exam-apply-container { padding: 16px; background: #f5f7fa; min-height: 100%; }
.selected-area { margin-top: 20px; }
.item-list, .method-list { display: flex; flex-direction: column; gap: 12px; }
.selected-item-card {
border: 1px solid #e4e7ed;
border-radius: 6px;
margin-bottom: 12px;
background: #fff;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
user-select: none;
background: #fafafa;
transition: background 0.2s;
}
.card-header:hover { background: #f0f2f5; }
.toggle-icon { margin-right: 8px; color: #909399; }
.item-name {
flex: 1;
margin: 0 8px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}
.toggle-text { color: #409eff; font-size: 12px; margin-left: auto; }
.card-detail {
padding: 12px 12px 12px 28px;
border-top: 1px dashed #e4e7ed;
}
.hierarchy-tip {
font-size: 12px;
color: #909399;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid #f0f0f0;
}
.method-item { margin: 6px 0; }
.no-method { color: #c0c4cc; font-size: 12px; padding: 4px 0; }
.empty-tip { text-align: center; color: #909399; padding: 24px; }
</style>

View File

@@ -1,273 +0,0 @@
<template>
<div class="examination-application-container">
<el-row :gutter="16">
<!-- 左侧检查项目分类 -->
<el-col :span="6">
<el-card shadow="never" class="category-panel">
<template #header>检查项目分类</template>
<el-tree
:data="categoryTree"
:props="{ label: 'name', children: 'children' }"
node-key="id"
highlight-current
@node-click="handleCategorySelect"
data-cy="category-tree"
/>
</el-card>
</el-col>
<!-- 中间检查项目列表 -->
<el-col :span="9">
<el-card shadow="never" class="item-panel">
<template #header>检查项目</template>
<div class="item-scroll-area">
<div v-for="item in currentItems" :key="item.id" class="item-row">
<el-checkbox
v-model="item.checked"
@change="handleItemToggle(item)"
data-cy="item-checkbox"
>
{{ cleanDisplayName(item.name) }}
</el-checkbox>
</div>
</div>
</el-card>
</el-col>
<!-- 右侧已选择区域 -->
<el-col :span="9">
<el-card shadow="never" class="selected-panel">
<template #header>已选择</template>
<div class="selected-scroll-area">
<div v-if="selectedItems.length === 0" class="empty-tip">暂无选择项目</div>
<div
v-for="sel in selectedItems"
:key="sel.id"
class="selected-card"
data-cy="selected-card"
>
<div class="card-header" @click="toggleDetail(sel)">
<el-icon class="arrow-icon" :class="{ expanded: sel.expanded }">
<ArrowRight />
</el-icon>
<el-tooltip
:content="cleanDisplayName(sel.name)"
placement="top"
:show-after="300"
:hide-after="0"
>
<span class="card-name" data-cy="selected-card-name">
{{ cleanDisplayName(sel.name) }}
</span>
</el-tooltip>
<el-icon class="expand-toggle" data-cy="expand-toggle">
<ArrowDown />
</el-icon>
</div>
<transition name="detail-slide">
<div v-show="sel.expanded" class="card-details">
<div v-for="method in sel.methods" :key="method.id" class="method-row">
<el-checkbox
v-model="method.checked"
@change="handleMethodToggle(sel, method)"
data-cy="method-checkbox"
>
{{ method.name }}
</el-checkbox>
</div>
</div>
</transition>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ArrowRight, ArrowDown } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// 类型定义
interface ExamMethod {
id: string
name: string
checked: boolean
}
interface ExamItem {
id: string
name: string
checked: boolean
methods: ExamMethod[]
}
interface CategoryNode {
id: string
name: string
children?: CategoryNode[]
items?: ExamItem[]
}
// 状态管理
const categoryTree = ref<CategoryNode[]>([])
const currentItems = ref<ExamItem[]>([])
const selectedItems = ref<ExamItem[]>([])
// 核心修复1名称清洗去除冗余“套餐”字样
const cleanDisplayName = (name: string) => {
if (!name) return ''
return name.replace(/套餐/g, '').trim()
}
// 分类切换
const handleCategorySelect = (data: CategoryNode) => {
currentItems.value = data.items || []
}
// 核心修复2项目与方法解耦禁止自动联动勾选
const handleItemToggle = (item: ExamItem) => {
if (item.checked) {
const exists = selectedItems.value.find(s => s.id === item.id)
if (!exists) {
selectedItems.value.push({
...item,
expanded: false, // 核心修复3默认收起状态
methods: item.methods?.map(m => ({ ...m, checked: false })) || [] // 方法默认不勾选
})
}
} else {
selectedItems.value = selectedItems.value.filter(s => s.id !== item.id)
}
}
// 方法独立勾选(不反向影响父级项目)
const handleMethodToggle = (parent: ExamItem, method: ExamMethod) => {
// 仅记录方法状态,不触发父级联动逻辑
if (!method.checked) {
ElMessage.info(`已取消方法:${method.name}`)
}
}
// 核心修复3展开/收起明细交互
const toggleDetail = (sel: ExamItem) => {
sel.expanded = !sel.expanded
}
</script>
<style scoped>
.examination-application-container {
padding: 16px;
background: #f5f7fa;
min-height: 100vh;
}
.category-panel, .item-panel, .selected-panel {
height: 600px;
display: flex;
flex-direction: column;
}
.category-panel :deep(.el-card__body),
.item-panel :deep(.el-card__body),
.selected-panel :deep(.el-card__body) {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.item-scroll-area, .selected-scroll-area {
flex: 1;
overflow-y: auto;
padding-right: 8px;
}
.item-row {
padding: 10px 0;
border-bottom: 1px dashed #ebeef5;
}
/* 核心修复2卡片宽度自适应去除固定宽度导致的截断 */
.selected-card {
border: 1px solid #dcdfe6;
border-radius: 6px;
margin-bottom: 12px;
background: #fff;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
padding: 12px;
cursor: pointer;
background: #fafafa;
transition: background 0.2s;
}
.card-header:hover {
background: #f0f2f5;
}
.arrow-icon {
transition: transform 0.25s ease;
margin-right: 6px;
}
.arrow-icon.expanded {
transform: rotate(90deg);
}
.card-name {
flex: 1;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
.card-details {
padding: 8px 12px 12px 32px;
background: #fcfcfc;
border-top: 1px solid #ebeef5;
}
.method-row {
padding: 6px 0;
display: flex;
align-items: center;
}
.expand-toggle {
color: #909399;
font-size: 14px;
}
.empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
}
.detail-slide-enter-active,
.detail-slide-leave-active {
transition: all 0.3s ease;
max-height: 200px;
opacity: 1;
}
.detail-slide-enter-from,
.detail-slide-leave-to {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
}
</style>

View File

@@ -1,218 +0,0 @@
<template>
<div class="exam-apply-container">
<el-row :gutter="12" style="height: 100%">
<!-- 左侧检查项目分类 -->
<el-col :span="5" class="panel">
<div class="panel-title">检查项目分类</div>
<el-tree
:data="categoryTree"
:props="{ label: 'label', children: 'children' }"
@node-click="handleCategorySelect"
highlight-current
default-expand-all
/>
</el-col>
<!-- 中间检查项目列表 -->
<el-col :span="7" class="panel">
<div class="panel-title">检查项目</div>
<el-checkbox-group v-model="selectedItemIds" @change="onItemSelectChange">
<el-checkbox
v-for="item in currentCategoryItems"
:key="item.id"
:label="item.id"
:value="item.id"
class="exam-item-checkbox"
>
{{ formatItemName(item.name) }}
</el-checkbox>
</el-checkbox-group>
</el-col>
<!-- 右侧已选择 & 检查方法 -->
<el-col :span="12" class="panel">
<div class="panel-title">已选择</div>
<div class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<!-- 卡片头部独立勾选 + 名称自适应 + 展开收起 -->
<div class="card-header" @click="toggleDetail(item)">
<el-checkbox v-model="item.checked" @change="onItemCheck(item)" @click.stop />
<el-tooltip :content="item.originalName" placement="top" :show-after="300">
<span class="item-name">{{ formatItemName(item.originalName) }}</span>
</el-tooltip>
<el-icon class="expand-icon">
<ArrowDown v-if="item.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 卡片明细检查方法列表默认收起解耦独立勾选 -->
<transition name="el-zoom-in-top">
<div v-show="item.expanded" class="card-details">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<el-checkbox
v-model="method.checked"
@change="onMethodCheck(item, method)"
class="method-checkbox"
/>
<span class="method-name">{{ method.name }}</span>
</div>
</div>
</transition>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无已选项目" />
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
// 状态定义
const categoryTree = ref([])
const currentCategoryItems = ref([])
const selectedItemIds = ref([])
const selectedItems = ref([])
/**
* 格式化名称:去除“套餐”前缀,避免界面冗余
*/
const formatItemName = (name) => {
if (!name) return ''
return name.replace(/^套餐[:]/, '').trim()
}
/**
* 分类切换:加载对应项目列表
*/
const handleCategorySelect = (data) => {
// 实际应替换为 API 请求
currentCategoryItems.value = data.items || []
}
/**
* 中间列表勾选变化:仅同步选中状态,不联动勾选方法(解耦核心)
*/
const onItemSelectChange = (ids) => {
const newIds = new Set(ids)
// 1. 移除未勾选的项目
selectedItems.value = selectedItems.value.filter(item => newIds.has(item.id))
// 2. 新增已勾选项目到右侧列表
currentCategoryItems.value.forEach(item => {
if (newIds.has(item.id) && !selectedItems.value.find(s => s.id === item.id)) {
selectedItems.value.push({
id: item.id,
originalName: item.name,
checked: true,
expanded: false, // 默认收起,符合预期行为
methods: (item.methods || []).map(m => ({ ...m, checked: false })) // 方法独立,默认不勾选
})
}
})
}
/**
* 卡片头部点击:展开/收起明细
*/
const toggleDetail = (item) => {
item.expanded = !item.expanded
}
/**
* 项目独立勾选:仅更新自身状态,不影响子方法
*/
const onItemCheck = (item) => {
if (!item.checked) {
selectedItemIds.value = selectedItemIds.value.filter(id => id !== item.id)
selectedItems.value = selectedItems.value.filter(s => s.id !== item.id)
} else {
if (!selectedItemIds.value.includes(item.id)) {
selectedItemIds.value.push(item.id)
}
}
}
/**
* 检查方法独立勾选:与父项目完全解耦
*/
const onMethodCheck = (item, method) => {
// 仅更新方法状态,可在此处扩展价格计算或校验逻辑
// 不触发父级 checked 状态变更,保持层级独立
}
</script>
<style scoped>
.exam-apply-container {
height: 100%;
padding: 12px;
background: #f5f7fa;
}
.panel {
background: #fff;
border-radius: 8px;
padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
}
.panel-title {
font-weight: 600;
margin-bottom: 12px;
font-size: 14px;
color: #303133;
}
.selected-list {
flex: 1;
overflow-y: auto;
padding-right: 4px;
}
.selected-card {
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 8px;
background: #fafafa;
transition: all 0.2s;
}
.card-header {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
user-select: none;
}
.item-name {
flex: 1;
margin: 0 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
color: #606266;
}
.expand-icon {
font-size: 14px;
color: #909399;
}
.card-details {
padding: 4px 12px 8px 32px;
border-top: 1px dashed #ebeef5;
}
.method-item {
display: flex;
align-items: center;
padding: 4px 0;
font-size: 12px;
color: #606266;
}
.method-checkbox {
margin-right: 6px;
}
.method-name {
flex: 1;
}
</style>

View File

@@ -1,102 +0,0 @@
<template>
<div class="pending-records-container">
<el-card shadow="never">
<template #header>待写病历</template>
<el-table
v-loading="loading"
:data="records"
style="width: 100%"
data-cy="pending-records-table"
empty-text="暂无待写病历"
>
<el-table-column prop="patientName" label="患者姓名" />
<el-table-column prop="visitNo" label="就诊号" />
<el-table-column prop="status" label="状态" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleWrite(row)">书写</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="total > 0"
class="pagination-wrapper"
layout="prev, pager, next"
:total="total"
:page-size="pageSize"
@current-change="handlePageChange"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getPendingRecordsApi } from '@/api/emr/medicalRecord'
const loading = ref(false)
const records = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const fetchRecords = async () => {
loading.value = true
try {
// 修复 Bug #562增加请求超时控制避免接口响应慢导致前端一直显示 loading
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), 2000)
)
const apiPromise = getPendingRecordsApi({
doctorId: 1, // 实际应从用户上下文获取
page: page.value,
size: pageSize.value
})
const res = await Promise.race([apiPromise, timeoutPromise])
if (res.code === 200) {
records.value = res.data.records || []
total.value = res.data.total || 0
} else {
ElMessage.warning(res.msg || '加载失败')
}
} catch (error) {
if (error.message === '请求超时') {
ElMessage.error('数据加载超时,请检查网络或稍后重试')
} else {
ElMessage.error('加载待写病历失败')
}
} finally {
// 修复 Bug #562确保无论成功、失败或超时loading 状态必定被重置
loading.value = false
}
}
const handlePageChange = (newPage) => {
page.value = newPage
fetchRecords()
}
const handleWrite = (row) => {
// 路由跳转至病历书写页
console.log('书写病历:', row.id)
}
onMounted(() => {
fetchRecords()
})
</script>
<style scoped>
.pending-records-container {
padding: 16px;
}
.pagination-wrapper {
margin-top: 16px;
justify-content: flex-end;
}
</style>

View File

@@ -1,126 +0,0 @@
<template>
<div class="pending-medical-record-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="title">待写病历</span>
<el-button type="primary" @click="handleRefresh" :loading="loading">刷新</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
stripe
border
@row-click="handleRowClick"
>
<el-table-column prop="visitNo" label="就诊号" width="120" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="departmentName" label="科室" width="150" />
<el-table-column prop="diagnosis" label="初步诊断" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click.stop="handleWrite(row)">书写</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getPendingMedicalRecords } from '@/api/outpatient/medicalRecord'
const router = useRouter()
const loading = ref(false)
const tableData = ref([])
const pagination = ref({
pageNum: 1,
pageSize: 20,
total: 0
})
const fetchData = async () => {
loading.value = true
try {
// 调用已优化的分页接口,确保响应时间 < 2s
const res = await getPendingMedicalRecords({
pageNum: pagination.value.pageNum,
pageSize: pagination.value.pageSize
})
tableData.value = res.data.list || []
pagination.value.total = res.data.total || 0
} catch (error) {
ElMessage.error('加载待写病历失败')
console.error(error)
} finally {
loading.value = false
}
}
const handleRefresh = () => {
pagination.value.pageNum = 1
fetchData()
}
const handleSizeChange = (size) => {
pagination.value.pageSize = size
pagination.value.pageNum = 1
fetchData()
}
const handleCurrentChange = (page) => {
pagination.value.pageNum = page
fetchData()
}
const handleRowClick = (row) => {
handleWrite(row)
}
const handleWrite = (row) => {
router.push({ path: '/outpatient/doctor/emr/write', query: { id: row.id } })
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.pending-medical-record-container {
padding: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 16px;
font-weight: 600;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -1,220 +0,0 @@
<template>
<div class="exam-item-selector">
<!-- 左侧分类树 -->
<div class="panel-left">
<el-tree
:data="categoryTree"
:props="{ label: 'name', children: 'children' }"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
<!-- 中间项目列表 -->
<div class="panel-middle">
<el-table :data="currentItems" border stripe style="width: 100%" @selection-change="handleItemSelection">
<el-table-column type="selection" width="40" />
<el-table-column prop="code" label="编码" width="80" />
<el-table-column prop="name" label="项目名称" show-overflow-tooltip />
<el-table-column prop="price" label="价格" width="80" align="right" />
</el-table>
</div>
<!-- 右侧已选区域 -->
<div class="panel-right">
<div class="selected-header">已选择项目</div>
<div class="selected-list" v-if="selectedItems.length">
<div
v-for="item in selectedItems"
:key="item.id"
class="selected-card"
>
<!-- 卡片头部项目名称 + 展开/收起控制 -->
<div class="card-header" @click="toggleExpand(item)">
<span class="item-name" :title="item.name">{{ item.name }}</span>
<el-icon class="expand-icon">
<ArrowDown v-if="item.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 卡片明细检查方法列表默认收起 -->
<div v-show="item.expanded" class="card-details">
<div v-if="item.methods && item.methods.length" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<el-checkbox
v-model="method.checked"
@change="handleMethodCheck(item, method)"
>
{{ method.name }}
</el-checkbox>
</div>
</div>
<div v-else class="empty-tip">无关联检查方法</div>
</div>
</div>
</div>
<el-empty v-else description="暂无已选项目" :image-size="60" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
import type { ElTree } from 'element-plus';
// 模拟数据结构(实际应从 API 获取)
interface ExamMethod {
id: string;
name: string;
checked: boolean;
}
interface ExamItem {
id: string;
code: string;
name: string;
price: number;
methods: ExamMethod[];
expanded: boolean;
}
const categoryTree = ref<any[]>([]);
const currentCategory = ref<string | null>(null);
const currentItems = ref<ExamItem[]>([]);
const selectedItems = ref<ExamItem[]>([]);
// 切换分类加载项目
const handleCategoryClick = (data: any) => {
currentCategory.value = data.id;
// 实际应调用 API: fetchItemsByCategory(data.id)
currentItems.value = data.items || [];
};
// 项目勾选处理:解耦联动,仅添加/移除项目,不自动勾选方法
const handleItemSelection = (selection: ExamItem[]) => {
// 过滤出新增的项目
const added = selection.filter(s => !selectedItems.value.find(i => i.id === s.id));
const removed = selectedItems.value.filter(i => !selection.find(s => s.id === s.id));
added.forEach(item => {
// 初始化状态:默认收起,方法未勾选
selectedItems.value.push({
...item,
expanded: false,
methods: item.methods?.map(m => ({ ...m, checked: false })) || []
});
});
removed.forEach(item => {
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id);
});
};
// 切换明细展开/收起
const toggleExpand = (item: ExamItem) => {
item.expanded = !item.expanded;
};
// 检查方法独立勾选:与父项目解耦
const handleMethodCheck = (item: ExamItem, method: ExamMethod) => {
// 仅更新当前方法状态,不触发父级联动
console.log(`方法 ${method.name} 状态变更为: ${method.checked}`);
};
</script>
<style scoped>
.exam-item-selector {
display: flex;
height: 100%;
gap: 12px;
padding: 12px;
background: #fff;
}
.panel-left, .panel-middle, .panel-right {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 8px;
}
.panel-left { width: 20%; overflow-y: auto; }
.panel-middle { width: 45%; }
.panel-right { width: 35%; display: flex; flex-direction: column; }
.selected-header {
font-weight: 600;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #eee;
}
.selected-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.selected-card {
border: 1px solid #dcdfe6;
border-radius: 6px;
background: #fafafa;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
cursor: pointer;
background: #f5f7fa;
transition: background 0.2s;
}
.card-header:hover {
background: #e9ecef;
}
.item-name {
flex: 1;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 8px;
}
.expand-icon {
font-size: 14px;
color: #909399;
}
.card-details {
padding: 8px 10px;
background: #fff;
border-top: 1px dashed #ebeef5;
}
.method-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.method-item {
padding-left: 12px;
font-size: 13px;
}
.empty-tip {
color: #909399;
font-size: 12px;
text-align: center;
padding: 4px 0;
}
</style>

View File

@@ -1,211 +0,0 @@
<template>
<div class="exam-request-container">
<el-row :gutter="16" class="main-layout">
<!-- 左侧检查分类 -->
<el-col :span="6" class="panel category-panel">
<div class="panel-title">检查项目分类</div>
<el-tree
:data="categories"
:props="{ label: 'name', children: 'children' }"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</el-col>
<!-- 中间检查项目列表 -->
<el-col :span="9" class="panel item-panel">
<div class="panel-title">检查项目</div>
<el-table :data="currentItems" border style="width: 100%" @selection-change="handleItemSelection">
<el-table-column type="selection" width="40" />
<el-table-column prop="name" label="项目名称" show-overflow-tooltip />
<el-table-column prop="price" label="价格" width="80" />
</el-table>
</el-col>
<!-- 右侧已选择区域 (Bug #550 Fix: 结构化展示 & 解耦) -->
<el-col :span="9" class="panel selected-panel">
<div class="panel-title">已选择</div>
<div v-if="selectedGroups.length === 0" class="empty-tip">暂无选择项目</div>
<div v-else class="selected-list">
<div v-for="group in selectedGroups" :key="group.itemId" class="selected-card">
<!-- 卡片头部项目勾选 & 名称 & 展开收起 -->
<div class="card-header" @click="toggleExpand(group)">
<el-checkbox
v-model="group.checked"
@change="handleItemCheck(group)"
@click.stop
/>
<el-tooltip :content="group.itemName" placement="top" :show-after="300">
<span class="item-name">{{ group.itemName }}</span>
</el-tooltip>
<span class="expand-icon">{{ group.expanded ? '▼' : '▶' }}</span>
</div>
<!-- 卡片明细检查方法列表 (Bug #550 Fix: 默认收起独立勾选) -->
<div v-show="group.expanded" class="method-list">
<div v-for="method in group.methods" :key="method.id" class="method-item">
<el-checkbox
v-model="method.checked"
@change="handleMethodCheck(group, method)"
@click.stop
/>
<span class="method-name">{{ method.methodName }}</span>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 模拟分类数据
const categories = ref([
{ id: 1, name: '彩超', children: [] },
{ id: 2, name: 'CT', children: [] }
])
// 模拟当前分类下的项目
const currentItems = ref([
{ id: 101, name: '128线排彩超', price: 150, methods: [
{ id: 1001, methodName: '常规检查', checked: false },
{ id: 1002, methodName: '血管成像', checked: false }
]},
{ id: 102, name: '心脏彩超', price: 200, methods: [
{ id: 1003, methodName: '二维超声', checked: false }
]}
])
// 已选择分组数据 (Bug #550 Fix: 采用 Item > Method 层级结构)
const selectedGroups = ref([])
const handleCategoryClick = (data) => {
// 实际业务中根据分类ID请求项目列表
console.log('切换分类:', data.name)
}
const handleItemSelection = (selection) => {
// 同步选中项到右侧面板
selection.forEach(item => {
if (!selectedGroups.value.find(g => g.itemId === item.id)) {
// Bug #550 Fix: 清理冗余“套餐”前缀,默认收起状态
const cleanName = item.name.replace(/^套餐[:]/, '')
selectedGroups.value.push({
itemId: item.id,
itemName: cleanName,
checked: true,
expanded: false, // 默认收起
methods: item.methods.map(m => ({ ...m, checked: false })) // 方法默认不勾选,解耦
})
}
})
// 移除取消勾选的项目
const selectedIds = selection.map(i => i.id)
selectedGroups.value = selectedGroups.value.filter(g => selectedIds.includes(g.itemId))
}
// Bug #550 Fix: 项目勾选独立,不联动方法
const handleItemCheck = (group) => {
// 仅控制项目本身的选中状态,不改变子方法状态
console.log(`项目 ${group.itemName} 状态:`, group.checked)
}
// Bug #550 Fix: 方法勾选独立,不反向影响项目
const handleMethodCheck = (group, method) => {
console.log(`方法 ${method.methodName} 状态:`, method.checked)
}
// 展开/收起控制
const toggleExpand = (group) => {
group.expanded = !group.expanded
}
</script>
<style scoped>
.exam-request-container {
padding: 16px;
background: #f5f7fa;
min-height: 100vh;
}
.main-layout {
height: calc(100vh - 120px);
}
.panel {
background: #fff;
border-radius: 8px;
padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
}
.panel-title {
font-weight: 600;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
.selected-panel {
overflow-y: auto;
}
.empty-tip {
color: #909399;
text-align: center;
margin-top: 40px;
}
.selected-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.selected-card {
border: 1px solid #e4e7ed;
border-radius: 6px;
background: #fafafa;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
background: #fff;
transition: background 0.2s;
}
.card-header:hover {
background: #f0f2f5;
}
.item-name {
flex: 1;
margin: 0 10px;
font-weight: 500;
/* Bug #550 Fix: 宽度自适应 + 超长省略 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.expand-icon {
font-size: 12px;
color: #909399;
transition: transform 0.2s;
}
.method-list {
padding: 8px 12px 12px 36px;
background: #f9fafc;
border-top: 1px dashed #e4e7ed;
}
.method-item {
display: flex;
align-items: center;
padding: 6px 0;
font-size: 13px;
color: #606266;
}
.method-name {
margin-left: 8px;
}
</style>

View File

@@ -1,172 +0,0 @@
<template>
<div class="exam-apply-container">
<!-- 左侧分类树 -->
<div class="panel category-panel">
<el-tree :data="categories" node-key="id" @node-click="handleCategoryClick" />
</div>
<!-- 中间项目列表 -->
<div class="panel item-panel">
<div v-for="item in currentItems" :key="item.id" class="item-card" @click="toggleItem(item)">
<el-checkbox v-model="item.checked" @change="onItemCheck(item)" @click.stop />
<span class="item-name" :title="item.name">{{ cleanName(item.name) }}</span>
</div>
</div>
<!-- 右侧/下方已选择区域 -->
<div class="panel selected-panel">
<div v-if="selectedGroups.length === 0" class="empty-tip">暂无已选项目</div>
<div v-for="group in selectedGroups" :key="group.id" class="selected-group">
<div class="selected-group-header" @click="toggleGroup(group)">
<span class="item-name" :title="group.name">{{ cleanName(group.name) }}</span>
<el-icon class="expand-icon">
<ArrowDown v-if="group.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<div v-show="group.expanded" class="selected-methods">
<div v-for="method in group.methods" :key="method.id" class="method-item">
<el-checkbox v-model="method.checked" @change="onMethodCheck(method)" />
<span class="method-name">{{ method.name }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
// 模拟分类数据
const categories = ref([])
const currentItems = ref([])
const currentMethods = ref([])
// 计算已选分组(项目 > 检查方法)
const selectedGroups = computed(() => {
return currentItems.value
.filter(item => item.checked)
.map(item => {
const methods = currentMethods.value.filter(m => m.projectId === item.id)
return {
id: item.id,
name: item.name,
expanded: false, // 默认收起
methods
}
})
})
// 清理名称:去除“套餐”前缀及冗余符号
const cleanName = (name) => {
if (!name) return ''
return name.replace(/^套餐[:]\s*/, '').trim()
}
// 切换项目勾选状态(解耦:不联动检查方法)
const toggleItem = (item) => {
item.checked = !item.checked
}
const onItemCheck = (item) => {
// 仅更新状态,不触发任何自动勾选逻辑
console.log(`项目 ${item.name} 状态变更为: ${item.checked}`)
}
// 切换检查方法勾选状态
const onMethodCheck = (method) => {
console.log(`检查方法 ${method.name} 状态变更为: ${method.checked}`)
}
// 展开/收起已选项目明细
const toggleGroup = (group) => {
group.expanded = !group.expanded
}
// 分类点击加载对应项目
const handleCategoryClick = (data) => {
// 实际业务中此处调用API加载 currentItems 和 currentMethods
console.log('切换分类:', data.name)
}
</script>
<style scoped>
.exam-apply-container {
display: flex;
gap: 16px;
padding: 16px;
height: 100%;
background: #f5f7fa;
}
.panel {
background: #fff;
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.category-panel { flex: 1; min-width: 200px; }
.item-panel { flex: 2; min-width: 300px; }
.selected-panel { flex: 2; min-width: 300px; overflow-y: auto; }
.item-card {
display: flex;
align-items: center;
padding: 8px 12px;
margin-bottom: 8px;
border: 1px solid #ebeef5;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.item-card:hover { background: #f0f9eb; border-color: #c2e7b0; }
.item-name {
margin-left: 8px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
color: #303133;
}
.selected-group {
margin-bottom: 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
overflow: hidden;
}
.selected-group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: #fafafa;
cursor: pointer;
user-select: none;
}
.selected-group-header:hover { background: #f2f3f5; }
.selected-methods {
padding: 8px 12px;
background: #fff;
border-top: 1px solid #ebeef5;
}
.method-item {
display: flex;
align-items: center;
padding: 6px 0;
font-size: 13px;
color: #606266;
}
.method-name { margin-left: 8px; }
.expand-icon { font-size: 14px; color: #909399; }
.empty-tip { text-align: center; color: #909399; padding: 20px 0; }
</style>

View File

@@ -1,229 +0,0 @@
<template>
<div class="exam-request-wrapper">
<!-- 左侧检查项目分类 -->
<div class="left-panel">
<el-tree
:data="categoryTree"
node-key="id"
highlight-current
@node-click="handleCategorySelect"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="middle-panel">
<el-checkbox-group v-model="selectedItemIds" @change="handleItemChange">
<el-checkbox
v-for="item in currentItems"
:key="item.id"
:label="item.id"
class="item-checkbox"
>
{{ formatItemName(item.name) }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 右侧/下方已选择区域 -->
<div class="right-panel">
<div v-if="selectedItems.length === 0" class="empty-tip">暂无已选项目</div>
<div v-else class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<div class="card-header" @click="toggleExpand(item.id)">
<!-- 宽度自适应 + 悬停提示完整名称 -->
<span class="item-title" :title="item.name">{{ formatItemName(item.name) }}</span>
<el-icon class="expand-icon">
<ArrowDown v-if="!item.expanded" />
<ArrowUp v-else />
</el-icon>
</div>
<!-- 明细/检查方法区域默认收起 -->
<div v-show="item.expanded" class="card-body">
<div class="method-section">
<span class="section-label">检查方法</span>
<el-checkbox-group v-model="selectedMethodIds" @change="handleMethodChange">
<el-checkbox
v-for="method in item.methods"
:key="method.id"
:label="method.id"
>
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
// 状态定义
const categoryTree = ref([])
const currentItems = ref([])
const selectedItemIds = ref([])
const selectedMethodIds = ref([])
const selectedItems = reactive([])
/**
* 修复 Bug #550 核心逻辑:
* 1. 去除“套餐”前缀,清理冗余文案
*/
const formatItemName = (name) => {
if (!name) return ''
return name.replace(/^(套餐|项目套餐)[:]/g, '').trim()
}
const handleCategorySelect = (node) => {
// 实际业务中此处应请求后端获取对应分类下的项目列表
currentItems.value = node.items || []
}
/**
* 修复 Bug #550 核心逻辑:
* 2. 项目与检查方法完全解耦。勾选项目仅同步 selectedItems 状态,
* 绝不自动触发 selectedMethodIds 的变更。
*/
const handleItemChange = (ids) => {
const currentIds = new Set(ids)
// 移除已取消勾选的项目
for (let i = selectedItems.length - 1; i >= 0; i--) {
if (!currentIds.has(selectedItems[i].id)) {
selectedItems.splice(i, 1)
}
}
// 新增勾选的项目(默认收起 expanded: false
ids.forEach(id => {
if (!selectedItems.find(i => i.id === id)) {
const item = currentItems.value.find(i => i.id === id)
if (item) {
selectedItems.push({
id: item.id,
name: item.name,
methods: item.methods || [],
expanded: false // 默认收起状态
})
}
}
})
}
/**
* 修复 Bug #550 核心逻辑:
* 3. 检查方法独立维护,仅响应用户手动点击,不反向污染项目勾选状态。
*/
const handleMethodChange = (ids) => {
selectedMethodIds.value = ids
}
const toggleExpand = (id) => {
const item = selectedItems.find(i => i.id === id)
if (item) {
item.expanded = !item.expanded
}
}
</script>
<style scoped>
.exam-request-wrapper {
display: flex;
gap: 16px;
height: 100%;
padding: 16px;
background: #f5f7fa;
}
.left-panel, .middle-panel, .right-panel {
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 12px;
background: #fff;
overflow-y: auto;
}
.left-panel { width: 22%; }
.middle-panel { width: 28%; }
.right-panel { flex: 1; }
.item-checkbox {
display: block;
margin-bottom: 10px;
line-height: 1.5;
}
.selected-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.selected-card {
border: 1px solid #dcdfe6;
border-radius: 6px;
overflow: hidden;
transition: box-shadow 0.2s;
}
.selected-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: #fafafa;
cursor: pointer;
user-select: none;
}
/* 宽度自适应 + 文本截断 */
.item-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: #303133;
margin-right: 8px;
}
.expand-icon {
font-size: 14px;
color: #909399;
flex-shrink: 0;
}
.card-body {
padding: 12px;
border-top: 1px solid #ebeef5;
background: #fff;
}
.method-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.section-label {
font-size: 12px;
color: #606266;
font-weight: 600;
margin-bottom: 4px;
}
.empty-tip {
color: #909399;
text-align: center;
padding: 40px 0;
font-size: 14px;
}
</style>

View File

@@ -1,151 +0,0 @@
<template>
<div class="exam-apply-container">
<div class="selection-layout">
<!-- 左侧检查项目分类 -->
<div class="panel category-panel">
<h4>检查项目分类</h4>
<el-tree
:data="categoryTree"
:props="{ label: 'name', children: 'children' }"
@node-click="handleCategoryClick"
data-cy="category-tree"
/>
</div>
<!-- 中间检查项目 -->
<div class="panel item-panel">
<h4>检查项目</h4>
<div class="item-list-container" data-cy="item-list">
<div v-for="item in filteredItems" :key="item.id" class="list-item">
<el-checkbox v-model="item.selected" @change="handleItemChange(item)" />
<span class="item-name">{{ cleanName(item.name) }}</span>
</div>
</div>
</div>
<!-- 右侧检查方法 -->
<div class="panel method-panel">
<h4>检查方法</h4>
<div class="method-list-container" data-cy="method-list">
<div v-for="method in filteredMethods" :key="method.id" class="list-item">
<el-checkbox v-model="method.selected" @change="handleMethodChange(method)" />
<span class="method-name">{{ method.name }}</span>
</div>
</div>
</div>
</div>
<!-- 已选择区域 -->
<div class="selected-area" data-cy="selected-area">
<h4>已选择</h4>
<div v-for="group in selectedGroups" :key="group.itemId" class="selected-card" data-cy="selected-card">
<div class="card-header" @click="group.collapsed = !group.collapsed">
<el-checkbox v-model="group.selected" @change="handleGroupSelect(group)" @click.stop />
<el-tooltip :content="cleanName(group.itemName)" placement="top" :show-after="300">
<span class="card-title">{{ cleanName(group.itemName) }}</span>
</el-tooltip>
<span class="collapse-icon">{{ group.collapsed ? '▶' : '▼' }}</span>
</div>
<!-- 明细区域严格遵循 项目 > 检查方法 层级默认收起移除冗余标签 -->
<div v-show="!group.collapsed" class="details-panel">
<div v-for="method in group.methods" :key="method.id" class="detail-row">
<el-checkbox v-model="method.selected" @change="handleDetailMethodChange(group, method)" @click.stop />
<span>{{ method.name }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive } from 'vue'
// 数据源(实际项目中由 API 注入)
const categoryTree = ref([])
const allItems = ref([])
const allMethods = ref([])
const currentCategoryId = ref(null)
const filteredItems = computed(() => allItems.value.filter(i => i.categoryId === currentCategoryId.value))
const filteredMethods = computed(() => allMethods.value)
// 已选分组状态管理
const selectedGroups = reactive([])
/**
* 清理冗余文案:去除“套餐”字样,解决显示冗余问题
*/
const cleanName = (name) => name ? name.replace(/套餐/g, '') : ''
/**
* 分类切换
*/
const handleCategoryClick = (node) => {
currentCategoryId.value = node.id
}
/**
* 核心修复1项目勾选与检查方法解耦
* 勾选项目时,仅将项目加入已选列表,不自动勾选关联方法
*/
const handleItemChange = (item) => {
if (item.selected) {
const exists = selectedGroups.find(g => g.itemId === item.id)
if (!exists) {
selectedGroups.push({
itemId: item.id,
itemName: item.name,
selected: true,
collapsed: true, // 默认收起,符合交互预期
methods: item.methods ? item.methods.map(m => ({ ...m, selected: false })) : [] // 方法默认不勾选
})
}
} else {
const idx = selectedGroups.findIndex(g => g.itemId === item.id)
if (idx !== -1) {
selectedGroups.splice(idx, 1)
}
}
}
/**
* 核心修复2检查方法独立勾选
* 右侧方法列表状态独立维护,不与左侧项目强绑定
*/
const handleMethodChange = (method) => {
// 仅更新右侧面板状态,不触发已选区域联动,保持解耦
}
/**
* 已选卡片全选/取消全选
*/
const handleGroupSelect = (group) => {
group.methods.forEach(m => m.selected = group.selected)
}
/**
* 明细方法状态变更
*/
const handleDetailMethodChange = (group, method) => {
// 同步更新右侧独立方法列表的状态(如需)
const target = allMethods.value.find(m => m.id === method.id)
if (target) target.selected = method.selected
}
</script>
<style scoped>
.exam-apply-container { padding: 16px; background: #fff; }
.selection-layout { display: flex; gap: 16px; margin-bottom: 20px; }
.panel { flex: 1; border: 1px solid #ebeef5; padding: 12px; border-radius: 6px; background: #fafafa; }
.item-list-container, .method-list-container { max-height: 320px; overflow-y: auto; background: #fff; padding: 8px; border-radius: 4px; }
.list-item { display: flex; align-items: center; padding: 8px 4px; border-bottom: 1px dashed #f0f0f0; }
.list-item:last-child { border-bottom: none; }
.selected-area { border-top: 2px solid #ebeef5; padding-top: 16px; }
.selected-card { border: 1px solid #dcdfe6; border-radius: 6px; margin-bottom: 12px; overflow: hidden; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.card-header { display: flex; align-items: center; padding: 10px 12px; background: #f5f7fa; cursor: pointer; user-select: none; }
.card-title { flex: 1; margin: 0 10px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.collapse-icon { font-size: 12px; color: #909399; transition: transform 0.2s; }
.details-panel { padding: 10px 12px; background: #fff; border-top: 1px solid #ebeef5; }
.detail-row { display: flex; align-items: center; padding: 6px 0; }
</style>

View File

@@ -1,151 +0,0 @@
<template>
<div class="exam-apply-container">
<el-row :gutter="20">
<!-- 左侧检查项目分类 -->
<el-col :span="6">
<el-card shadow="never">
<template #header>检查项目分类</template>
<el-tree
:data="categories"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
data-cy="category-tree"
/>
</el-card>
</el-col>
<!-- 中间项目列表 & 检查方法 -->
<el-col :span="10">
<el-card shadow="never" class="mb-20">
<template #header>检查项目</template>
<div class="item-list" data-cy="item-list">
<div v-for="item in currentItems" :key="item.id" class="item-row">
<el-checkbox v-model="item.checked" @change="handleItemCheck(item)" />
<el-tooltip :content="item.name" placement="top" :show-after="300">
<span class="item-name-text">{{ item.name }}</span>
</el-tooltip>
</div>
</div>
</el-card>
<el-card shadow="never">
<template #header>检查方法</template>
<div class="method-list" data-cy="method-list">
<div v-for="method in availableMethods" :key="method.id" class="method-row">
<el-checkbox v-model="method.checked" @change="handleMethodCheck(method)" />
<span>{{ method.name }}</span>
</div>
</div>
</el-card>
</el-col>
<!-- 右侧/下方已选择区域 -->
<el-col :span="8">
<el-card shadow="never">
<template #header>已选择项目</template>
<div class="selected-area" data-cy="selected-area">
<el-empty v-if="selectedItems.length === 0" description="暂无选择项目" />
<el-collapse v-else v-model="activeCollapseNames" accordion>
<el-collapse-item
v-for="sel in selectedItems"
:key="sel.id"
:name="sel.id"
data-cy="selected-card"
>
<template #title>
<div class="card-header">
<el-tooltip :content="cleanName(sel.name)" placement="top" :show-after="500">
<span class="item-name">{{ cleanName(sel.name) }}</span>
</el-tooltip>
<el-tag size="small" type="info" class="ml-2">{{ sel.type || '项目' }}</el-tag>
</div>
</template>
<!-- 结构化明细默认收起去除冗余标签严格遵循 项目 > 检查方法 层级 -->
<div class="details-panel" data-cy="details-panel">
<div v-if="sel.methods && sel.methods.length > 0" class="method-group">
<div class="group-title">检查方法</div>
<div v-for="m in sel.methods" :key="m.id" class="method-item">
<el-checkbox v-model="m.checked" @change="handleDetailMethodCheck(sel, m)" />
<span>{{ m.name }}</span>
</div>
</div>
<div v-else class="empty-detail">无关联检查方法</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 状态定义
const categories = ref([])
const currentItems = ref([])
const availableMethods = ref([])
const selectedItems = ref([])
const activeCollapseNames = ref([]) // 默认空数组,确保所有卡片初始为收起状态
// 分类点击:加载对应项目与方法
const handleCategoryClick = (data) => {
// 实际项目中此处应调用 API 获取数据,此处为结构演示
currentItems.value = [
{ id: 101, name: '套餐128线排彩超', checked: false, type: '套餐', methods: [
{ id: 201, name: '常规扫查', checked: false },
{ id: 202, name: '血流多普勒', checked: false }
]}
]
availableMethods.value = [
{ id: 301, name: '独立检查方法A', checked: false },
{ id: 302, name: '独立检查方法B', checked: false }
]
}
// 修复1项目勾选与检查方法完全解耦不触发自动联动
const handleItemCheck = (item) => {
if (item.checked) {
const exists = selectedItems.value.find(s => s.id === item.id)
if (!exists) {
// 深拷贝避免引用污染,保留关联方法供明细展示
selectedItems.value.push({ ...item, methods: item.methods ? [...item.methods] : [] })
}
} else {
selectedItems.value = selectedItems.value.filter(s => s.id !== item.id)
}
}
// 修复1中间区域检查方法独立勾选不反向影响项目
const handleMethodCheck = (method) => {
// 仅维护中间列表状态,业务层按需处理
}
// 明细内方法独立勾选
const handleDetailMethodCheck = (parent, method) => {
// 仅更新当前套餐/项目下的方法状态
}
// 修复2清理名称去除“套餐”前缀及冗余符号
const cleanName = (name) => {
if (!name) return ''
return name.replace(/^套餐[:]/, '').trim()
}
</script>
<style scoped>
.exam-apply-container { padding: 20px; }
.mb-20 { margin-bottom: 20px; }
.item-row, .method-row { display: flex; align-items: center; padding: 8px 0; gap: 8px; }
.item-name-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-header { display: flex; align-items: center; width: 100%; }
.item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ml-2 { margin-left: 8px; }
.details-panel { padding: 12px; background: #f5f7fa; border-radius: 4px; margin-top: 4px; }
.group-title { font-weight: 600; margin-bottom: 8px; color: #606266; font-size: 13px; }
.method-item { display: flex; align-items: center; padding: 4px 0; gap: 8px; }
.empty-detail { color: #909399; font-size: 12px; padding: 4px 0; }
</style>

View File

@@ -1,199 +0,0 @@
<template>
<div class="exam-apply-panel">
<!-- 左侧分类 & 中间项目选择区保持原有布局逻辑 -->
<div class="selection-area">
<slot name="category-tree" />
<slot name="item-list" />
</div>
<!-- 右侧/下方已选择区域 -->
<div class="selected-area">
<h3 class="area-title">已选择项目</h3>
<div v-if="selectedItems.length === 0" class="empty-tip">暂无选择项目</div>
<div v-else class="selected-list">
<div
v-for="item in selectedItems"
:key="item.id"
class="selected-card"
@click="toggleExpand(item)"
>
<div class="card-header">
<el-checkbox
v-model="item.checked"
@change="handleItemCheck(item)"
@click.stop
/>
<el-tooltip :content="item.displayName" placement="top" :show-after="300">
<span class="item-name">{{ item.displayName }}</span>
</el-tooltip>
<span class="expand-icon">{{ item.expanded ? '▼' : '▶' }}</span>
</div>
<!-- 明细/检查方法区域默认收起严格遵循 项目 > 检查方法 层级 -->
<div v-show="item.expanded" class="details-panel">
<!-- 已移除冗余的项目套餐明细标签 -->
<div v-if="item.methods && item.methods.length > 0" class="method-list">
<div
v-for="method in item.methods"
:key="method.id"
class="method-row"
>
<el-checkbox
v-model="method.checked"
@change="handleMethodCheck(item, method)"
@click.stop
/>
<span class="method-name">{{ method.name }}</span>
</div>
</div>
<div v-else class="no-methods">无关联检查方法</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface ExamMethod {
id: string
name: string
checked: boolean
}
interface ExamItem {
id: string
name: string
checked: boolean
expanded: boolean
methods: ExamMethod[]
}
const selectedItems = ref<ExamItem[]>([])
// 清理名称:去除“套餐”字样,保留核心名称
const cleanName = (name: string) => name.replace(/套餐/g, '').trim()
// 切换展开/收起
const toggleExpand = (item: ExamItem) => {
item.expanded = !item.expanded
}
// 项目勾选处理(解耦:不联动检查方法)
const handleItemCheck = (item: ExamItem) => {
// 保持方法状态独立,不自动勾选/取消关联方法
// 若业务后续需要级联取消,可在此处添加 item.methods.forEach(m => m.checked = false)
}
// 检查方法勾选处理(独立)
const handleMethodCheck = (_item: ExamItem, _method: ExamMethod) => {
// 仅更新方法自身状态,不影响父项目或其他方法
}
// 外部调用接口:添加选中项
const addItem = (rawItem: any) => {
const exists = selectedItems.value.find(i => i.id === rawItem.id)
if (!exists) {
selectedItems.value.push({
id: rawItem.id,
name: rawItem.name,
displayName: cleanName(rawItem.name),
checked: true,
expanded: false, // 默认收起
methods: (rawItem.methods || []).map((m: any) => ({
id: m.id,
name: m.name,
checked: false // 默认不勾选,彻底解耦
}))
})
}
}
defineExpose({ addItem, selectedItems })
</script>
<style scoped>
.exam-apply-panel {
display: flex;
gap: 16px;
padding: 12px;
}
.selected-area {
flex: 1;
min-width: 320px;
background: #f8f9fa;
border-radius: 6px;
padding: 12px;
}
.area-title {
margin: 0 0 12px;
font-size: 14px;
color: #303133;
}
.selected-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 400px;
overflow-y: auto;
}
.selected-card {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 10px;
cursor: pointer;
background: #fff;
transition: all 0.2s;
}
.selected-card:hover {
border-color: #409eff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
}
.item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
color: #303133;
}
.expand-icon {
font-size: 12px;
color: #909399;
user-select: none;
}
.details-panel {
margin-top: 8px;
padding-left: 24px;
border-top: 1px dashed #ebeef5;
padding-top: 8px;
}
.method-row {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
}
.method-name {
font-size: 13px;
color: #606266;
}
.empty-tip {
color: #909399;
text-align: center;
padding: 24px 0;
font-size: 13px;
}
.no-methods {
color: #c0c4cc;
font-size: 12px;
padding: 4px 0;
}
</style>

View File

@@ -1,150 +0,0 @@
<template>
<div class="examination-application">
<div class="layout-container">
<!-- 左侧检查项目分类 -->
<div class="panel category-panel">
<h3>检查项目分类</h3>
<ul class="category-list">
<li
v-for="cat in categories"
:key="cat.id"
:class="{ active: selectedCategory?.id === cat.id }"
@click="selectCategory(cat)"
>
{{ cat.name }}
</li>
</ul>
</div>
<!-- 中间检查项目列表 -->
<div class="panel item-panel">
<h3>检查项目</h3>
<ul class="item-list">
<li v-for="item in currentItems" :key="item.id" class="item-row">
<input type="checkbox" :checked="isSelected(item.id)" @change="toggleItem(item)" />
<span>{{ item.name }}</span>
</li>
</ul>
</div>
<!-- 右侧已选择区域 -->
<div class="panel selected-panel">
<h3>已选择</h3>
<div v-if="selectedItems.length === 0" class="empty-tip">暂无选择项目</div>
<div v-else class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card" :data-cy="`selected-card-${item.id}`">
<div class="card-header" @click="toggleExpand(item)">
<!-- 修复2自适应宽度+完整名称提示清理冗余套餐字样 -->
<span class="item-title" :title="cleanName(item.name)">{{ cleanName(item.name) }}</span>
<span class="toggle-icon" data-cy="expand-btn">{{ item.expanded ? '▲' : '▼' }}</span>
</div>
<!-- 修复3默认收起点击展开显示检查方法严格遵循 项目 > 检查方法 层级 -->
<div v-show="item.expanded" class="details-content" data-cy="details-panel">
<div v-if="item.methods && item.methods.length > 0" class="method-list" data-cy="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-item" data-cy="method-item">
<!-- 修复1解耦勾选独立控制检查方法 -->
<input type="checkbox" :checked="method.selected" @change="toggleMethod(item, method)" />
<span>{{ method.name }}</span>
</div>
</div>
<div v-else class="no-methods">无关联检查方法</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 模拟分类与项目数据(实际应从 API 获取)
const categories = ref([
{ id: 1, name: '彩超' },
{ id: 2, name: 'CT' },
{ id: 3, name: 'MRI' }
])
const itemsMap = {
1: [
{ id: 101, name: '128线排彩超套餐', methods: [
{ id: 1001, name: '腹部彩超', selected: false },
{ id: 1002, name: '心脏彩超', selected: false }
]},
{ id: 102, name: '常规彩超', methods: [] }
],
2: [{ id: 201, name: '胸部CT', methods: [] }],
3: [{ id: 301, name: '头颅MRI', methods: [] }]
}
const selectedCategory = ref(null)
const selectedItems = ref([])
const currentItems = computed(() => {
return selectedCategory.value ? (itemsMap[selectedCategory.value.id] || []) : []
})
const selectCategory = (cat) => {
selectedCategory.value = cat
}
const isSelected = (id) => {
return selectedItems.value.some(i => i.id === id)
}
// 修复1项目勾选与检查方法解耦新增项目时不自动勾选其下属方法
const toggleItem = (item) => {
const index = selectedItems.value.findIndex(i => i.id === item.id)
if (index > -1) {
selectedItems.value.splice(index, 1)
} else {
selectedItems.value.push({
...item,
expanded: false, // 修复3默认收起状态
methods: item.methods ? item.methods.map(m => ({ ...m, selected: false })) : []
})
}
}
// 修复1独立控制检查方法勾选状态与父项目解耦
const toggleMethod = (parentItem, method) => {
const target = selectedItems.value.find(i => i.id === parentItem.id)
if (target) {
const m = target.methods.find(m => m.id === method.id)
if (m) {
m.selected = !m.selected
}
}
}
const toggleExpand = (item) => {
item.expanded = !item.expanded
}
// 修复2清理冗余“套餐”字样保持名称简洁
const cleanName = (name) => {
return name.replace(/套餐/g, '').trim()
}
</script>
<style scoped>
.layout-container { display: flex; gap: 16px; padding: 16px; height: 100%; box-sizing: border-box; }
.panel { border: 1px solid #e8e8e8; border-radius: 8px; padding: 12px; background: #fff; display: flex; flex-direction: column; }
.category-panel { width: 20%; }
.item-panel { width: 35%; }
.selected-panel { width: 45%; }
.category-list, .item-list { list-style: none; padding: 0; margin: 0; overflow-y: auto; flex: 1; }
.category-list li, .item-row { padding: 10px; cursor: pointer; border-bottom: 1px solid #f5f5f5; display: flex; align-items: center; gap: 8px; transition: background 0.2s; }
.category-list li:hover, .item-row:hover { background: #fafafa; }
.category-list li.active { background: #e6f7ff; color: #1890ff; font-weight: 500; }
.selected-list { flex: 1; overflow-y: auto; padding-top: 8px; }
.selected-card { border: 1px solid #d9d9d9; border-radius: 6px; margin-bottom: 8px; overflow: hidden; background: #fff; }
.card-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; background: #fafafa; cursor: pointer; user-select: none; }
.item-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.toggle-icon { font-size: 12px; color: #888; margin-left: 8px; }
.details-content { padding: 10px 12px; border-top: 1px solid #eee; background: #fff; }
.method-item { display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 14px; color: #555; }
.no-methods { color: #999; font-size: 13px; padding: 4px 0; }
.empty-tip { color: #999; text-align: center; padding: 30px 0; }
</style>

View File

@@ -1,216 +0,0 @@
<template>
<div class="examination-apply-container">
<!-- 左侧检查项目分类 -->
<div class="panel left-panel">
<el-tree
:data="categoryTree"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
class="category-tree"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="panel middle-panel">
<div class="panel-title">检查项目</div>
<el-checkbox-group v-model="selectedItemIds" class="item-list">
<el-checkbox
v-for="item in currentItems"
:key="item.id"
:label="item.id"
class="item-checkbox"
@change="handleItemSelect(item)"
>
<!-- Bug #550 Fix 2: 清理冗余套餐字样 -->
{{ item.name.replace(/套餐/g, '') }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 右侧已选择区域 -->
<div class="panel right-panel">
<div class="panel-title">已选择</div>
<div v-if="selectedGroups.length === 0" class="empty-tip">暂无选择项目</div>
<!-- Bug #550 Fix 3 & 4: 结构化展示默认收起移除项目套餐明细标签 -->
<div v-for="group in selectedGroups" :key="group.itemId" class="selected-card">
<div class="card-header" @click="toggleExpand(group.itemId)">
<el-icon class="expand-icon">
<ArrowRight v-if="!group.expanded" />
<ArrowDown v-else />
</el-icon>
<!-- Bug #550 Fix 2: 宽度自适应/提示完整名称 -->
<el-tooltip :content="group.itemName" placement="top" :show-after="300">
<span class="item-name">{{ group.itemName }}</span>
</el-tooltip>
</div>
<!-- 明细区域严格遵循 项目 > 检查方法 层级 -->
<div v-show="group.expanded" class="details-panel">
<div class="method-section">
<span class="section-title">检查方法</span>
<!-- Bug #550 Fix 1: 独立解耦不随项目勾选自动联动 -->
<el-checkbox-group
v-model="group.selectedMethods"
class="method-checkbox-group"
@change="handleMethodChange(group)"
>
<el-checkbox
v-for="method in group.methods"
:key="method.id"
:label="method.id"
class="method-checkbox"
>
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ArrowRight, ArrowDown } from '@element-plus/icons-vue'
// 模拟分类树数据实际应从API获取
const categoryTree = ref([
{ id: 'c1', label: '彩超', children: [] }
])
const currentItems = ref([])
const selectedItemIds = ref([])
const selectedGroups = ref([])
// 切换分类加载项目
const handleCategoryClick = (data) => {
// 实际项目中此处调用API获取项目列表
currentItems.value = data.items || []
}
// Bug #550 Fix 1: 解耦勾选逻辑。仅维护项目选中状态,绝不自动勾选检查方法
const handleItemSelect = (item) => {
const isAdding = selectedItemIds.value.includes(item.id)
if (isAdding) {
// 新增选中项:初始化独立状态,默认收起
const exists = selectedGroups.value.find(g => g.itemId === item.id)
if (!exists) {
selectedGroups.value.push({
itemId: item.id,
itemName: item.name.replace(/套餐/g, ''),
methods: item.methods || [],
selectedMethods: [], // 独立维护方法勾选状态
expanded: false // Bug #550 Fix 4: 默认收起
})
}
} else {
// 取消选中:移除对应组
selectedGroups.value = selectedGroups.value.filter(g => g.itemId !== item.id)
}
}
// 展开/收起明细
const toggleExpand = (itemId) => {
const group = selectedGroups.value.find(g => g.itemId === itemId)
if (group) group.expanded = !group.expanded
}
// 检查方法变更处理(仅记录或触发后续业务,不影响项目状态)
const handleMethodChange = (group) => {
console.log(`项目 ${group.itemName} 的检查方法已更新:`, group.selectedMethods)
// 可在此处调用API同步方法选择状态
}
</script>
<style scoped>
.examination-apply-container {
display: flex;
gap: 16px;
height: 100%;
padding: 12px;
background: #fff;
}
.panel {
flex: 1;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.panel-title {
font-weight: 600;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
.item-list, .method-checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.selected-card {
border: 1px solid #dcdfe6;
border-radius: 4px;
margin-bottom: 10px;
background: #fafafa;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
padding: 10px;
cursor: pointer;
background: #f5f7fa;
transition: background 0.2s;
}
.card-header:hover {
background: #e9ecef;
}
.expand-icon {
margin-right: 8px;
color: #909399;
}
/* Bug #550 Fix 2: 名称截断与自适应提示 */
.item-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
color: #303133;
}
.details-panel {
padding: 10px;
border-top: 1px solid #ebeef5;
background: #fff;
}
.section-title {
display: block;
font-size: 13px;
color: #606266;
margin-bottom: 8px;
font-weight: 500;
}
.empty-tip {
color: #909399;
text-align: center;
margin-top: 40px;
}
</style>

View File

@@ -1,190 +0,0 @@
<template>
<div class="exam-selector-container">
<div class="selected-panel">
<h3 class="panel-title">已选择</h3>
<div v-if="selectedItems.length === 0" class="empty-tip">暂无已选项目</div>
<div v-else class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<!-- 父级检查项目 -->
<div class="card-header" @click="toggleExpand(item)">
<el-checkbox
class="item-checkbox"
v-model="item.checked"
@change="onItemCheck(item)"
@click.stop
/>
<span class="item-name" :title="item.name">{{ item.name }}</span>
<el-icon class="expand-btn">
<ArrowDown v-if="item.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 子级检查方法/明细 -->
<transition name="slide-fade">
<div v-show="item.expanded" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<el-checkbox
class="method-checkbox"
v-model="method.checked"
@change="onMethodCheck(item, method)"
@click.stop
/>
<span class="method-name" :title="method.name">{{ method.name }}</span>
</div>
</div>
</transition>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
// 已选项目列表
const selectedItems = ref([]);
/**
* 修复 Bug #550-1解耦项目与方法勾选
* 勾选项目仅改变自身状态,不联动子方法。
* 取消勾选项目时,同步清空子方法状态。
*/
const onItemCheck = (item) => {
if (!item.checked) {
item.methods.forEach(m => (m.checked = false));
item.expanded = false;
}
};
/**
* 修复 Bug #550-1解耦项目与方法勾选
* 勾选方法仅改变自身状态。
* 若方法被选中,父项目自动置为选中;若方法取消,仅当无其他方法选中时才取消父项目。
*/
const onMethodCheck = (item, method) => {
if (method.checked) {
item.checked = true;
} else {
item.checked = item.methods.some(m => m.checked);
}
};
/**
* 修复 Bug #550-3结构化展示与默认收起
* 切换明细展开/收起状态
*/
const toggleExpand = (item) => {
item.expanded = !item.expanded;
};
/**
* 修复 Bug #550-2人性化卡片展示
* 清理冗余“套餐”前缀,初始化默认收起状态
*/
const addItem = (itemData) => {
// 移除“套餐”、“项目套餐明细”等冗余标签
const cleanName = itemData.name.replace(/^(套餐[:]?|项目套餐明细[:]?)/g, '').trim();
selectedItems.value.push({
...itemData,
name: cleanName,
checked: true,
expanded: false, // 默认收起明细
methods: (itemData.methods || []).map(m => ({ ...m, checked: false }))
});
};
defineExpose({ addItem, selectedItems });
</script>
<style scoped>
.exam-selector-container {
padding: 12px;
background: #fff;
border-radius: 4px;
}
.panel-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.empty-tip {
color: #909399;
text-align: center;
padding: 20px 0;
font-size: 13px;
}
.selected-card {
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 8px;
padding: 10px;
background: #fafafa;
}
.card-header {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.item-name {
flex: 1;
margin: 0 10px;
font-size: 14px;
color: #303133;
/* 修复 Bug #550-2宽度自适应与省略提示 */
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.expand-btn {
color: #909399;
transition: transform 0.2s;
}
.method-list {
margin-top: 8px;
padding-left: 28px;
border-top: 1px dashed #e4e7ed;
}
.method-item {
display: flex;
align-items: center;
padding: 6px 0;
}
.method-name {
margin-left: 8px;
font-size: 13px;
color: #606266;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 展开收起动画 */
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.2s ease;
max-height: 200px;
opacity: 1;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
max-height: 0;
opacity: 0;
}
</style>

View File

@@ -1,162 +0,0 @@
<template>
<div class="outpatient-lab-container">
<el-card>
<template #header>
<div class="card-header">
<span>门诊检验申请</span>
<el-button type="primary" @click="handleOpenDialog">+新增</el-button>
</div>
</template>
<!-- 申请单列表区域 (省略) -->
<el-table :data="requestList" border style="width: 100%">
<el-table-column prop="requestNo" label="申请单号" width="180" />
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="executeTime" label="执行时间" width="160" />
<el-table-column prop="status" label="状态" width="100" />
</el-table>
</el-card>
<!-- 新增检验申请单弹窗 -->
<el-dialog
v-model="dialogVisible"
title="新增检验申请单"
width="850px"
class="lab-request-dialog"
destroy-on-close
>
<el-form :model="form" label-width="100px" class="lab-request-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="就诊卡号">
<!-- 修复1: 绑定患者上下文中的卡号禁用手动修改 -->
<el-input v-model="form.patientCardNo" name="patientCardNo" disabled placeholder="自动带出" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="执行时间">
<!-- 修复2: 默认值由 JS 初始化格式严格匹配 YYYY-MM-DD HH:mm -->
<el-date-picker
v-model="form.executeTime"
name="executeTime"
type="datetime"
placeholder="选择执行时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="item-selection-panel">
<h4>检验项目选择</h4>
<el-tree
:data="categoryTree"
:props="{ children: 'items', label: 'name' }"
node-key="id"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span v-if="data.isCategory" class="category-node">{{ node.label }}</span>
<div v-else class="lab-item-row">
<!-- 修复3: 移除原模板中硬编码的 <el-tag>套餐</el-tag> type 字段渲染 -->
<span class="item-name">{{ data.name }}</span>
<span class="item-price">¥{{ data.price.toFixed(2) }}</span>
</div>
</template>
</el-tree>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import dayjs from 'dayjs'
import { useCurrentPatient } from '@/composables/useCurrentPatient'
import { getLabCategoriesApi, submitLabRequestApi } from '@/api/outpatient/lab'
const { currentPatient } = useCurrentPatient()
const dialogVisible = ref(false)
const requestList = ref([])
const categoryTree = ref([])
const form = reactive({
patientCardNo: '',
executeTime: '',
selectedItems: []
})
const handleOpenDialog = () => {
// 修复1: 打开弹窗时立即从全局患者状态同步就诊卡号
form.patientCardNo = currentPatient.value?.cardNo || ''
// 修复2: 初始化执行时间为当前系统时间,精确到分钟
form.executeTime = dayjs().format('YYYY-MM-DD HH:mm')
dialogVisible.value = true
}
const handleNodeClick = (data) => {
if (!data.isCategory) {
// 处理项目勾选逻辑
const exists = form.selectedItems.find(i => i.id === data.id)
if (!exists) {
form.selectedItems.push(data)
}
}
}
const handleSubmit = async () => {
if (!form.patientCardNo) {
ElMessage.warning('未获取到患者就诊卡号,请重新选择患者')
return
}
if (form.selectedItems.length === 0) {
ElMessage.warning('请至少选择一项检验项目')
return
}
try {
await submitLabRequestApi({
patientId: currentPatient.value.id,
cardNo: form.patientCardNo,
executeTime: form.executeTime,
items: form.selectedItems.map(i => ({ id: i.id, name: i.name, price: i.price }))
})
ElMessage.success('检验申请单开立成功')
dialogVisible.value = false
form.selectedItems = []
// 刷新列表逻辑...
} catch (error) {
console.error('提交检验申请失败:', error)
ElMessage.error('提交失败,请重试')
}
}
onMounted(async () => {
try {
const res = await getLabCategoriesApi()
categoryTree.value = res.data || []
} catch (e) {
console.error('加载检验分类失败:', e)
}
})
</script>
<style scoped>
.outpatient-lab-container { padding: 16px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.lab-request-form { margin-bottom: 20px; }
.item-selection-panel { border: 1px solid #ebeef5; padding: 12px; border-radius: 4px; max-height: 400px; overflow-y: auto; }
.category-node { font-weight: bold; color: #409eff; }
.lab-item-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px dashed #eee; }
.item-name { flex: 1; }
.item-price { color: #f56c6c; font-weight: 500; }
</style>

Some files were not shown because too many files have changed in this diff Show More