feat(doctor): 添加医生站报卡管理功能

- 新增医生报卡统计、列表查询、详情查看等API接口
- 实现报卡的提交、撤回、删除、批量操作等功能
- 添加报卡编辑和Word文档导出功能
- 构建完整的医生报卡管理界面,包含筛选、分页、状态显示等
- 实现报卡状态管理(待提交、已提交、已审核、已上报、失败、作废)
- 添加前端表格展示、弹窗详情、表单验证等交互功能
- 创建医生报卡更新DTO数据传输对象
This commit is contained in:
2026-03-09 14:52:00 +08:00
parent 46a99ecd55
commit c3776c642b
4 changed files with 839 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
import request from '@/utils/request';
export function getDoctorCardStatistics() {
return request({
url: '/card-management/doctor/statistics',
method: 'get',
});
}
export function getDoctorCardList(params) {
return request({
url: '/card-management/doctor/page',
method: 'get',
params: params,
});
}
export function getCardDetail(cardNo) {
return request({
url: `/card-management/detail/${cardNo}`,
method: 'get',
});
}
export function submitCard(cardNo) {
return request({
url: `/card-management/doctor/submit/${cardNo}`,
method: 'post',
});
}
export function withdrawCard(cardNo) {
return request({
url: `/card-management/doctor/withdraw/${cardNo}`,
method: 'post',
});
}
export function deleteCard(cardNo) {
return request({
url: `/card-management/doctor/${cardNo}`,
method: 'delete',
});
}
export function batchSubmitCards(cardNos) {
return request({
url: '/card-management/doctor/batch-submit',
method: 'post',
data: cardNos,
});
}
export function batchDeleteCards(cardNos) {
return request({
url: '/card-management/doctor/batch-delete',
method: 'post',
data: cardNos,
});
}
export function exportCardToWord(cardNo) {
return request({
url: `/card-management/doctor/export-word/${cardNo}`,
method: 'get',
responseType: 'blob',
});
}
export function updateDoctorCard(data) {
return request({
url: '/card-management/doctor/update',
method: 'post',
data: data,
});
}

View File

@@ -0,0 +1,745 @@
<template>
<div class="my-card-management-container">
<div class="page-header">
<h2>我的报卡</h2>
</div>
<div class="statistics-section">
<div class="stat-card total">
<div class="stat-icon">
<el-icon><Document /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ statistics.totalCount || 0 }}</div>
<div class="stat-label">总报卡数</div>
</div>
</div>
<div class="stat-card pending">
<div class="stat-icon">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ statistics.pendingFailedCount || 0 }}</div>
<div class="stat-label">待处理/失败</div>
</div>
</div>
<div class="stat-card success">
<div class="stat-icon">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ statistics.reportedCount || 0 }}</div>
<div class="stat-label">已成功上报</div>
</div>
</div>
</div>
<div class="filter-section">
<el-form :model="queryParams" :inline="true">
<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.status" placeholder="全部状态" clearable style="width: 140px">
<el-option label="全部状态" value="" />
<el-option label="待提交" value="0" />
<el-option label="已提交" value="1" />
<el-option label="已审核" value="2" />
<el-option label="已上报" value="3" />
<el-option label="失败" value="4" />
<el-option label="作废" value="6" />
</el-select>
</el-form-item>
<el-form-item label="关键词">
<el-input
v-model="queryParams.keyword"
placeholder="患者姓名/报卡名称"
clearable
style="width: 180px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>
应用筛选
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置条件
</el-button>
</el-form-item>
</el-form>
</div>
<div class="action-section">
<el-checkbox v-model="isAllSelected" @change="handleSelectAll">全选</el-checkbox>
<el-button type="primary" :disabled="selectedRows.length === 0" @click="handleBatchSubmit">
批量提交
</el-button>
<el-button type="danger" :disabled="selectedRows.length === 0" @click="handleBatchDelete">
批量删除
</el-button>
</div>
<div class="table-section">
<el-table
v-loading="loading"
:data="cardList"
@selection-change="handleSelectionChange"
border
stripe
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="卡片ID" prop="cardNo" min-width="150" />
<el-table-column label="患者姓名" prop="patName" width="100" />
<el-table-column label="身份证号" prop="idNo" min-width="180" />
<el-table-column label="联系电话" prop="phone" width="130" />
<el-table-column label="就诊卡号" prop="visitCardNo" width="130" />
<el-table-column label="报卡名称" prop="cardName" min-width="200" />
<el-table-column label="提交时间" prop="submitTime" width="160" align="center" />
<el-table-column label="状态" prop="status" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ getStatusName(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
<el-button
v-if="row.status === '0'"
type="primary"
link
size="small"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-if="row.status === '0'"
type="success"
link
size="small"
@click="handleSubmit(row)"
>
提交
</el-button>
<el-button
v-if="row.status === '1'"
type="warning"
link
size="small"
@click="handleWithdraw(row)"
>
撤回
</el-button>
<el-button
v-if="row.status === '3'"
type="info"
link
size="small"
@click="handleExport(row)"
>
导出
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-section">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
<el-dialog
v-model="detailVisible"
:title="detailMode === 'view' ? '报卡详情' : '编辑报卡'"
width="800px"
destroy-on-close
>
<el-descriptions v-if="detailMode === 'view'" :column="2" border>
<el-descriptions-item label="卡片编号">{{ currentCard.cardNo }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(currentCard.status)">{{ getStatusName(currentCard.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="患者姓名">{{ currentCard.patName }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ currentCard.idNo }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ currentCard.phone }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ currentCard.sex === '1' ? '男' : currentCard.sex === '2' ? '女' : '未知' }}</el-descriptions-item>
<el-descriptions-item label="年龄">{{ currentCard.age }}{{ getAgeUnit(currentCard.ageUnit) }}</el-descriptions-item>
<el-descriptions-item label="疾病名称">{{ currentCard.diseaseName }}</el-descriptions-item>
<el-descriptions-item label="发病日期">{{ currentCard.onsetDate }}</el-descriptions-item>
<el-descriptions-item label="诊断日期">{{ currentCard.diagDate }}</el-descriptions-item>
<el-descriptions-item label="报告单位">{{ currentCard.reportOrg }}</el-descriptions-item>
<el-descriptions-item label="报告医生">{{ currentCard.reportDoc }}</el-descriptions-item>
<el-descriptions-item label="填卡日期">{{ currentCard.reportDate }}</el-descriptions-item>
<el-descriptions-item label="现住址" :span="2">
{{ currentCard.addressProv }}{{ currentCard.addressCity }}{{ currentCard.addressCounty }}{{ currentCard.addressTown }}{{ currentCard.addressVillage }}{{ currentCard.addressHouse }}
</el-descriptions-item>
</el-descriptions>
<el-form v-else ref="editFormRef" :model="editForm" :rules="editFormRules" label-width="100px">
<!-- 患者基本信息 -->
<el-divider content-position="left">患者基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="患者姓名" prop="patName">
<el-input v-model="editForm.patName" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号" prop="idNo">
<el-input v-model="editForm.idNo" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="sex">
<el-input :value="editForm.sex === '1' ? '男' : editForm.sex === '2' ? '女' : '未知'" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年龄" prop="age">
<el-input :value="`${editForm.age}${getAgeUnit(editForm.ageUnit)}`" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="phone" required>
<el-input v-model="editForm.phone" />
</el-form-item>
</el-col>
</el-row>
<!-- 临床信息 -->
<el-divider content-position="left">临床信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="疾病名称" prop="diseaseName">
<el-input v-model="editForm.diseaseName" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发病日期" prop="onsetDate">
<el-date-picker v-model="editForm.onsetDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="诊断日期" prop="diagDate" required>
<el-date-picker v-model="editForm.diagDate" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="传染病类别" prop="diseaseCategory">
<el-select v-model="editForm.diseaseCategory" placeholder="请选择传染病类别" style="width: 100%">
<el-option label="甲类传染病" value="A" />
<el-option label="乙类传染病" value="B" />
<el-option label="丙类传染病" value="C" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 报告信息 -->
<el-divider content-position="left">报告信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报告单位" prop="reportOrg">
<el-input v-model="editForm.reportOrg" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报告医生" prop="reportDoc">
<el-input v-model="editForm.reportDoc" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="填卡日期" prop="reportDate">
<el-input v-model="editForm.reportDate" disabled />
</el-form-item>
</el-col>
</el-row>
<!-- 现住址 -->
<el-divider content-position="left">现住址</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="省" prop="addressProv">
<el-input v-model="editForm.addressProv" placeholder="请输入省份" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="市" prop="addressCity">
<el-input v-model="editForm.addressCity" placeholder="请输入城市" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="区县" prop="addressCounty">
<el-input v-model="editForm.addressCounty" placeholder="请输入区县" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="详细地址" prop="addressHouse">
<el-input v-model="editForm.addressHouse" type="textarea" placeholder="请输入详细地址" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="detailVisible = false">取消</el-button>
<el-button v-if="detailMode === 'edit'" type="primary" @click="handleSaveEdit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Document, Clock, CircleCheck, Search, Refresh } from '@element-plus/icons-vue';
import {
getDoctorCardStatistics,
getDoctorCardList,
submitCard,
withdrawCard,
batchSubmitCards,
batchDeleteCards,
exportCardToWord,
getCardDetail,
updateDoctorCard,
} from './api';
const loading = ref(false);
const cardList = ref([]);
const total = ref(0);
const selectedRows = ref([]);
const isAllSelected = ref(false);
const statistics = ref({
totalCount: 0,
pendingFailedCount: 0,
reportedCount: 0,
});
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
dateRange: [],
status: '',
keyword: '',
});
const detailVisible = ref(false);
const detailMode = ref('view');
const currentCard = ref({});
const editForm = reactive({});
const editFormRef = ref(null);
// 编辑表单验证规则
const editFormRules = {
phone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
diagDate: [
{ required: true, message: '请选择诊断日期', trigger: 'change' }
]
};
const statusMap = {
'0': { name: '待提交', type: 'warning' },
'1': { name: '已提交', type: 'primary' },
'2': { name: '已审核', type: 'success' },
'3': { name: '已上报', type: 'success' },
'4': { name: '失败', type: 'danger' },
'5': { name: '退回', type: 'danger' },
'6': { name: '作废', type: 'info' },
};
const ageUnitMap = {
'1': '岁',
'2': '月',
'3': '天',
};
function getStatusName(status) {
return statusMap[status]?.name || '未知';
}
function getStatusType(status) {
return statusMap[status]?.type || 'info';
}
function getAgeUnit(unit) {
return ageUnitMap[unit] || '岁';
}
async function getStatistics() {
try {
const res = await getDoctorCardStatistics();
if (res.code === 200) {
statistics.value = res.data || {};
}
} catch (error) {
console.error('获取统计数据失败:', error);
}
}
async function getList() {
loading.value = true;
try {
const params = { ...queryParams };
if (params.dateRange && params.dateRange.length === 2) {
params.startDate = params.dateRange[0];
params.endDate = params.dateRange[1];
}
delete params.dateRange;
const res = await getDoctorCardList(params);
if (res.code === 200) {
cardList.value = res.data?.list || [];
total.value = res.data?.total || 0;
}
} catch (error) {
console.error('获取列表失败:', error);
ElMessage.error('获取数据失败');
} finally {
loading.value = false;
}
}
function handleQuery() {
queryParams.pageNo = 1;
getList();
}
function handleReset() {
queryParams.pageNo = 1;
queryParams.pageSize = 10;
queryParams.dateRange = [];
queryParams.status = '';
queryParams.keyword = '';
getList();
}
function handleSelectionChange(selection) {
selectedRows.value = selection;
isAllSelected.value = selection.length === cardList.value.length && cardList.value.length > 0;
}
function handleSelectAll(val) {
if (val) {
selectedRows.value = [...cardList.value];
} else {
selectedRows.value = [];
}
}
async function handleView(row) {
try {
const res = await getCardDetail(row.cardNo);
if (res.code === 200) {
currentCard.value = res.data || {};
detailMode.value = 'view';
detailVisible.value = true;
}
} catch (error) {
ElMessage.error('获取详情失败');
}
}
async function handleEdit(row) {
try {
const res = await getCardDetail(row.cardNo);
if (res.code === 200) {
Object.assign(editForm, res.data || {});
detailMode.value = 'edit';
detailVisible.value = true;
}
} catch (error) {
ElMessage.error('获取详情失败');
}
}
async function handleSaveEdit() {
// 验证表单
try {
await editFormRef.value.validate();
} catch (error) {
ElMessage.error('表单验证失败,请检查输入');
return;
}
try {
const updateData = {
cardNo: editForm.cardNo,
phone: editForm.phone,
onsetDate: editForm.onsetDate,
diagDate: editForm.diagDate,
diseaseCategory: editForm.diseaseCategory,
addressProv: editForm.addressProv,
addressCity: editForm.addressCity,
addressCounty: editForm.addressCounty,
addressHouse: editForm.addressHouse,
};
const res = await updateDoctorCard(updateData);
if (res.code === 200) {
ElMessage.success('保存成功');
detailVisible.value = false;
getList();
} else {
ElMessage.error(res.msg || '保存失败');
}
} catch (error) {
ElMessage.error('保存失败:' + (error.message || '网络错误'));
}
}
async function handleSubmit(row) {
try {
await ElMessageBox.confirm('确认提交该报卡?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
const res = await submitCard(row.cardNo);
if (res.code === 200) {
ElMessage.success('提交成功');
getStatistics();
getList();
} else {
ElMessage.error(res.msg || '提交失败');
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('提交失败');
}
}
}
async function handleWithdraw(row) {
try {
await ElMessageBox.confirm('确认撤回该报卡?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
const res = await withdrawCard(row.cardNo);
if (res.code === 200) {
ElMessage.success('撤回成功');
getStatistics();
getList();
} else {
ElMessage.error(res.msg || '撤回失败');
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('撤回失败');
}
}
}
async function handleBatchSubmit() {
const validRows = selectedRows.value.filter(row => row.status === '0');
if (validRows.length === 0) {
ElMessage.warning('只能提交待提交状态的报卡');
return;
}
try {
await ElMessageBox.confirm(`确认提交选中的 ${validRows.length} 条报卡?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
const cardNos = validRows.map(row => row.cardNo);
const res = await batchSubmitCards(cardNos);
if (res.code === 200) {
ElMessage.success(res.msg || '批量提交成功');
getStatistics();
getList();
} else {
ElMessage.error(res.msg || '批量提交失败');
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('批量提交失败');
}
}
}
async function handleBatchDelete() {
const validRows = selectedRows.value.filter(row => row.status === '0');
if (validRows.length === 0) {
ElMessage.warning('只能删除待提交状态的报卡');
return;
}
try {
await ElMessageBox.confirm(`确认删除选中的 ${validRows.length} 条报卡?删除后状态将变为作废。`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
const cardNos = validRows.map(row => row.cardNo);
const res = await batchDeleteCards(cardNos);
if (res.code === 200) {
ElMessage.success(res.msg || '批量删除成功');
getStatistics();
getList();
} else {
ElMessage.error(res.msg || '批量删除失败');
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('批量删除失败');
}
}
}
async function handleExport(row) {
try {
const res = await exportCardToWord(row.cardNo);
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `传染病报告卡-${row.cardNo}.docx`;
link.click();
window.URL.revokeObjectURL(url);
} catch (error) {
ElMessage.error('导出失败');
}
}
onMounted(() => {
getStatistics();
getList();
});
</script>
<style scoped>
.my-card-management-container {
padding: 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 84px);
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
font-size: 20px;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.statistics-section {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 16px;
color: #fff;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.stat-card.pending {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
box-shadow: 0 4px 12px rgba(245, 87, 108, 0.3);
}
.stat-card.success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
box-shadow: 0 4px 12px rgba(0, 242, 254, 0.3);
}
.stat-icon {
width: 56px;
height: 56px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: 32px;
font-weight: bold;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
margin-top: 4px;
}
.filter-section {
background: #fff;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.action-section {
background: #fff;
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
gap: 16px;
}
.table-section {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.pagination-section {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
@media (max-width: 992px) {
.statistics-section {
grid-template-columns: 1fr;
}
}
</style>