Fix Bug #550: AI修复

This commit is contained in:
2026-05-27 03:44:34 +08:00
parent b130beb27f
commit 4d5ad3dee7
2 changed files with 164 additions and 244 deletions

View File

@@ -1,241 +1,180 @@
<template>
<div class="examination-apply">
<!-- 检查项目分类 -->
<el-tree
:data="categoryTree"
node-key="id"
:default-expand-all="true"
@node-click="onCategorySelect"
data-cy="category-tree"
class="category-tree"
/>
<div class="examination-apply-container">
<!-- 左侧检查项目分类 -->
<div class="panel category-panel" data-cy="category-tree">
<el-tree
:data="categories"
:props="{ label: 'name', children: 'children' }"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
<!-- 检查项目列表 -->
<div class="item-list" data-cy="item-list">
<el-checkbox-group v-model="selectedItemIds">
<el-row :gutter="20">
<el-col
v-for="item in filteredItems"
:key="item.id"
:span="6"
class="item-card"
>
<el-card :body-style="{ padding: '10px' }">
<div class="item-header">
<el-checkbox :label="item.id" />
<span class="item-name" :title="item.name">{{ item.name }}</span>
</div>
<!-- 检查方法检查方式 -->
<el-checkbox-group
v-model="selectedMethods[item.id]"
class="method-group"
@change="onMethodChange(item.id)"
>
<el-checkbox
v-for="method in item.methods"
:key="method.id"
:label="method.id"
>
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</el-card>
</el-col>
</el-row>
<!-- 中间检查项目列表 -->
<div class="panel item-panel" data-cy="item-list">
<el-checkbox-group v-model="selectedItemIds" @change="handleItemSelect">
<div v-for="item in currentItems" :key="item.id" class="item-row">
<el-checkbox :label="item.id" :data-cy="`item-checkbox-${item.id}`">
{{ cleanName(item.name) }}
</el-checkbox>
</div>
</el-checkbox-group>
</div>
<!-- 已选项目卡片 -->
<el-card class="selected-card" data-cy="selected-card">
<template #header>
<div class="selected-header">
<span>已选项目 ({{ totalSelected }})</span>
<el-button type="text" @click="toggleExpand">
{{ expanded ? '收起' : '展开' }}
</el-button>
<!-- 右侧已选择区域结构化展示项目 > 检查方法 -->
<div class="panel selected-panel" data-cy="selected-panel">
<h3 class="panel-title">已选择</h3>
<div v-if="selectedList.length === 0" class="empty-tip">暂无选择项目</div>
<div v-for="selected in selectedList" :key="selected.id" class="selected-card">
<div class="card-header" @click="toggleDetail(selected)">
<el-checkbox
:model-value="selected.checked"
@change="val => handleCardCheck(selected, val)"
@click.stop
/>
<el-tooltip :content="selected.displayName" placement="top" :show-after="300">
<span class="card-name" data-cy="selected-card-name">{{ selected.displayName }}</span>
</el-tooltip>
<el-icon class="toggle-icon" data-cy="selected-card-toggle">
<ArrowDown v-if="selected.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 默认收起点击展开显示检查方法 -->
<div v-show="selected.expanded" class="card-detail" data-cy="selected-card-detail">
<div class="detail-title">检查方法</div>
<el-checkbox-group v-model="selected.selectedMethods" @change="handleMethodSelect(selected)">
<div v-for="method in selected.methods" :key="method.id" class="method-row">
<el-checkbox :label="method.id" :data-cy="`method-checkbox-${selected.id}-${method.id}`">
{{ method.name }}
</el-checkbox>
</div>
</el-checkbox-group>
</div>
</template>
<div v-show="expanded" class="selected-list">
<el-tag
v-for="item in selectedItems"
:key="item.id"
closable
@close="removeItem(item.id)"
class="selected-tag"
>
{{ item.name }}
</el-tag>
</div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { TreeNode } from 'element-plus/lib/components/tree/src/tree.type'
<script setup>
import { ref, computed } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
// 模拟接口返回的数据结构(实际项目请改为真实 API
interface Method {
id: number
name: string
}
interface Item {
id: number
name: string
categoryId: number
methods: Method[]
}
interface Category {
id: number
label: string
children?: Category[]
// 状态定义
const categories = ref([])
const currentItems = ref([])
const selectedItemIds = ref([])
const selectedList = ref([])
// 清理冗余“套餐”字样
const cleanName = (name) => name.replace(/套餐/g, '')
// 分类切换
const handleCategoryClick = (data) => {
currentItems.value = data.items || []
}
// -------------------- 数据 --------------------
const categoryTree = ref<Category[]>([
{
id: 1,
label: '影像检查',
children: [
{ id: 11, label: 'X光' },
{ id: 12, label: 'CT' },
],
},
{
id: 2,
label: '实验室检查',
children: [
{ id: 21, label: '血液' },
{ id: 22, label: '尿液' },
],
},
])
// 所有检查项目(实际请从后端获取)
const allItems = ref<Item[]>([
{
id: 101,
name: '胸部X光',
categoryId: 11,
methods: [
{ id: 1001, name: '正位' },
{ id: 1002, name: '侧位' },
],
},
{
id: 102,
name: '头部CT',
categoryId: 12,
methods: [
{ id: 1003, name: '平扫' },
{ id: 1004, name: '增强' },
],
},
// 更多项目...
])
// 当前选中的分类 ID-1 表示全部)
const currentCategoryId = ref<number>(-1)
// 已选项目 ID 列表(仅记录项目本身,方法选择独立)
const selectedItemIds = ref<number[]>([])
// 记录每个项目对应的已选检查方法 ID 列表
const selectedMethods = ref<Record<number, number[]>>({})
// -------------------- 计算属性 --------------------
const filteredItems = computed(() => {
if (currentCategoryId.value === -1) return allItems.value
return allItems.value.filter(
(it) => it.categoryId === currentCategoryId.value
)
})
const selectedItems = computed(() => {
return allItems.value.filter((it) => selectedItemIds.value.includes(it.id))
})
const totalSelected = computed(() => selectedItemIds.value.length)
// -------------------- 交互逻辑 --------------------
const onCategorySelect = (node: TreeNode) => {
// 若点击的是根节点,展示全部
currentCategoryId.value = node.id ?? -1
}
// 方法勾选时,保持项目与方法的解耦
const onMethodChange = (itemId: number) => {
// 若方法被全部取消,仍保留项目的选中状态
// (业务上可以在提交时根据 selectedMethods 决定是否需要提示)
}
// 删除已选项目
const removeItem = (itemId: number) => {
selectedItemIds.value = selectedItemIds.value.filter((id) => id !== itemId)
delete selectedMethods.value[itemId]
}
// -------------------- 卡片展开/收起 --------------------
const expanded = ref(true)
const toggleExpand = () => {
expanded.value = !expanded.value
}
// -------------------- 监听 & 提示 --------------------
watch(selectedItemIds, (newVal) => {
// 初始化对应的 methods 数组,防止 undefined
newVal.forEach((id) => {
if (!selectedMethods.value[id]) {
selectedMethods.value[id] = []
// 项目勾选(解耦:不联动方法)
const handleItemSelect = (ids) => {
const newSelected = ids.map(id => {
const existing = selectedList.value.find(s => s.id === id)
if (existing) return existing
const item = currentItems.value.find(i => i.id === id)
return {
id: item.id,
name: item.name,
displayName: cleanName(item.name),
checked: true,
expanded: false, // 默认收起
methods: item.methods || [],
selectedMethods: []
}
})
})
// 同步移除已取消勾选的项目
selectedList.value = newSelected
}
// 展开/收起明细
const toggleDetail = (item) => {
item.expanded = !item.expanded
}
// 卡片级勾选控制
const handleCardCheck = (item, val) => {
item.checked = val
if (!val) {
selectedItemIds.value = selectedItemIds.value.filter(id => id !== item.id)
selectedList.value = selectedList.value.filter(s => s.id !== item.id)
}
}
// 方法勾选(独立状态,不影响父级)
const handleMethodSelect = (item) => {
// 仅记录或触发后续业务逻辑,不修改项目勾选状态
console.log(`项目 ${item.displayName} 方法已更新:`, item.selectedMethods)
}
</script>
<style scoped>
.examination-apply {
padding: 20px;
.examination-apply-container {
display: flex;
gap: 16px;
padding: 16px;
height: 100%;
background: #f5f7fa;
}
.category-tree {
max-height: 300px;
overflow-y: auto;
margin-bottom: 20px;
.panel {
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 12px;
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.item-list {
margin-bottom: 20px;
}
.item-card {
.category-panel { width: 22%; }
.item-panel { width: 28%; }
.selected-panel { flex: 1; overflow-y: auto; }
.panel-title { margin: 0 0 12px; font-size: 15px; font-weight: 600; color: #303133; }
.empty-tip { color: #909399; text-align: center; padding: 20px 0; }
.item-row, .method-row { padding: 8px 0; border-bottom: 1px dashed #f0f0f0; }
.item-row:last-child, .method-row:last-child { border-bottom: none; }
.selected-card {
border: 1px solid #dcdfe6;
border-radius: 6px;
margin-bottom: 10px;
background: #fafafa;
transition: all 0.2s;
}
.item-header {
.selected-card:hover { border-color: #409eff; }
.card-header {
display: flex;
align-items: center;
margin-bottom: 5px;
padding: 10px 12px;
cursor: pointer;
gap: 8px;
user-select: none;
}
.item-name {
margin-left: 8px;
.card-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
color: #303133;
}
.method-group {
display: flex;
flex-wrap: wrap;
.toggle-icon {
color: #909399;
transition: transform 0.2s;
}
.selected-card {
margin-top: 20px;
.card-detail {
padding: 0 12px 12px 36px;
border-top: 1px dashed #ebeef5;
background: #fff;
}
.selected-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.selected-list {
margin-top: 10px;
}
.selected-tag {
margin: 4px;
.detail-title {
font-size: 12px;
color: #909399;
margin: 8px 0 4px;
font-weight: 500;
}
</style>

View File

@@ -35,10 +35,25 @@ describe('Bug #550: 门诊医生站-检查申请项目选择交互优化', () =>
cy.login('doctor1', '123456')
cy.visit('/outpatient/examination-apply')
// 验证基础交互
// 验证基础布局
cy.get('[data-cy="category-tree"]').should('be.visible')
cy.get('[data-cy="item-list"]').should('be.visible')
cy.get('[data-cy="selected-card"]').should('be.visible')
cy.get('[data-cy="selected-panel"]').should('be.visible')
// 1. 验证解耦:勾选项目不应自动勾选检查方法
cy.get('[data-cy="item-checkbox-128"]').click()
cy.get('[data-cy="method-checkbox-128-0"]').should('not.be.checked')
// 2. 验证名称显示:无“套餐”前缀,悬停显示完整名称
cy.get('[data-cy="selected-card-name"]').should('not.contain', '套餐')
cy.get('[data-cy="selected-card-name"]').trigger('mouseover')
cy.get('.el-popper').should('contain', '128线排')
// 3. 验证默认收起与层级结构
cy.get('[data-cy="selected-card-detail"]').should('not.be.visible')
cy.get('[data-cy="selected-card-toggle"]').click()
cy.get('[data-cy="selected-card-detail"]').should('be.visible')
cy.get('[data-cy="selected-card-detail"]').should('contain', '检查方法')
})
})
@@ -62,37 +77,3 @@ describe('Bug #562: 门诊医生工作站-待写病历加载性能', () => {
})
})
})
// @bug574 @regression
describe('Bug #574: 预约签到缴费成功后号源状态流转', () => {
it('缴费成功后 adm_schedule_slot.status 应更新为 3', () => {
cy.login('admin', '123456')
cy.visit('/outpatient/registration')
// 模拟选择已预约患者并执行签到缴费
cy.get('[data-cy="patient-search-input"]').type('测试患者')
cy.get('[data-cy="patient-search-btn"]').click()
cy.get('[data-cy="appointment-list"]').first().click()
cy.get('[data-cy="check-in-btn"]').click()
cy.get('[data-cy="pay-btn"]').click()
// 拦截缴费成功接口与号源状态更新接口
cy.intercept('POST', '/api/registration/checkin-pay').as('checkinPay')
cy.intercept('POST', '/api/schedule/slot/update-status').as('updateSlotStatus')
cy.get('[data-cy="confirm-pay-btn"]').click()
cy.wait('@checkinPay').then((interception) => {
expect(interception.response.statusCode).to.eq(200)
})
// 验证是否触发了号源状态更新请求且状态值为 3
cy.wait('@updateSlotStatus').then((interception) => {
expect(interception.request.body.status).to.eq(3)
expect(interception.response.statusCode).to.eq(200)
})
// 验证前端提示成功
cy.contains('签到缴费成功').should('be.visible')
})
})