revert: restore develop to clean baseline 5132de36 (remove all AI changes)
This commit is contained in:
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
@@ -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 // 明确设置前端超时阈值,避免无限等待
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
@@ -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}`);
|
||||
};
|
||||
6
openhis-ui-vue3/src/types/vitalSign.d.ts
vendored
6
openhis-ui-vue3/src/types/vitalSign.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
export interface VitalSignDto {
|
||||
patientId: number;
|
||||
timeLabels: string[];
|
||||
temperaturePoints: number[];
|
||||
rawDataJson?: string;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
<!-- 修复 #562:v-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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">检查项目 > 检查方法</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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user