feat: 医院管理模块 (admin 端 CRUD + 导入导出)
- 后端: BizHospital controller/domain/vo/mapper/service + Mapper XML - 前端: ry-vue3/src/views/admin/Hospital.vue + api/business/hospital.js - 模块面向 admin 角色, 提供医院主数据维护 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 医院字典 API (biz_hospital)
|
||||
* 基础 CRUD 走 @/api/public 的 bizList/bizGet/bizAdd/bizUpdate/bizDelete (路径 /business/hospital/...)
|
||||
* 本文件只放特殊形状的端点: 远程搜索 / 引用统计 / 导入模板 / 导入 / 导出
|
||||
*/
|
||||
|
||||
// 医生注册下拉远程搜索 (匿名公开, keyword 为空不返回建议)
|
||||
export function searchHospital(keyword) {
|
||||
return request.get('/business/hospital/search', { params: { keyword } })
|
||||
}
|
||||
|
||||
// 删除前引用统计: 返回医院名被业务表 work_unit 引用的条数
|
||||
export function countHospitalUsage(hospital) {
|
||||
return request.get('/business/hospital/countUsage', { params: { hospital } })
|
||||
}
|
||||
|
||||
// 下载导入模板 (blob)
|
||||
export function downloadHospitalTemplate() {
|
||||
return request.get('/business/hospital/importTemplate', { responseType: 'blob' })
|
||||
}
|
||||
|
||||
// 批量导入 (FormData multipart, 返回 { code, msg, data: ImportResult })
|
||||
export function importHospital(file) {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
return request({
|
||||
url: '/business/hospital/importData',
|
||||
method: 'post',
|
||||
data: form,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// 导出 (跟随当前筛选条件, blob)
|
||||
export function exportHospital(params) {
|
||||
return request.post('/business/hospital/export', null, { params, responseType: 'blob' })
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div class="page-card admin-hospital">
|
||||
<div class="breadcrumb">首页 / 资料库管理 / 医院管理</div>
|
||||
|
||||
<!-- ========== 筛选区 ========== -->
|
||||
<el-form inline :model="q" class="filter-form" @keyup.enter="load">
|
||||
<el-form-item label="医院名称">
|
||||
<el-input v-model="q.hospital" placeholder="输入医院名称" clearable style="width:200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="省">
|
||||
<el-input v-model="q.province" placeholder="输入省" clearable style="width:140px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否军医院">
|
||||
<el-select v-model="q.militaryHospital" placeholder="全部" clearable style="width:140px">
|
||||
<el-option label="非军医院" value="0" />
|
||||
<el-option label="军医院" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- ========== 批量按钮区 ========== -->
|
||||
<div class="batch-bar">
|
||||
<el-button type="primary" @click="openAdd">新增医院</el-button>
|
||||
<el-button @click="openImport">批量导入</el-button>
|
||||
<el-button @click="onExport">导出</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ========== 表格 ========== -->
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="province" label="省" width="90" align="center" />
|
||||
<el-table-column prop="city" label="市" width="110" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="hospital" label="医院名称" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="hospitalCode" label="医院CODE" width="110" align="center" />
|
||||
<el-table-column label="是否军医院" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="militaryTagType(row.militaryHospital)" disable-transitions>{{ militaryLabel(row.militaryHospital) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="80" align="center" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<el-link :underline="false" type="primary" @click="onEdit(row)">编辑</el-link>
|
||||
<el-link :underline="false" type="danger" @click="onDelete(row)">删除</el-link>
|
||||
</div></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="page.pageNum"
|
||||
v-model:page-size="page.pageSize"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="load"
|
||||
@size-change="load"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ========== 新增/编辑弹窗 ========== -->
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑医院' : '新增医院'" width="560px">
|
||||
<el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
|
||||
<el-form-item label="医院名称" prop="hospital">
|
||||
<el-input v-model="form.hospital" placeholder="请输入医院名称(必填)" maxlength="1000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="省">
|
||||
<el-input v-model="form.province" placeholder="请输入省" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="市">
|
||||
<el-input v-model="form.city" placeholder="请输入市" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="原名称">
|
||||
<el-input v-model="form.originalHospital" placeholder="请输入原名称" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="医院CODE">
|
||||
<el-input v-model="form.hospitalCode" placeholder="如 H3301006" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否军医院">
|
||||
<el-select v-model="form.militaryHospital" placeholder="请选择" clearable style="width:160px">
|
||||
<el-option label="非军医院" value="0" />
|
||||
<el-option label="军医院" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-input v-model="form.type" placeholder="请输入类型" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== 批量导入 ========== -->
|
||||
<el-dialog v-model="importOpen" title="批量导入医院" width="400px" append-to-body :close-on-click-modal="false">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
accept=".xlsx, .xls"
|
||||
:auto-upload="false"
|
||||
:on-change="onImportFileChange"
|
||||
:on-remove="onImportFileRemove"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div style="margin-top:10px;font-size:13px;text-align:center">
|
||||
<span style="color:#909399;margin-right:8px">仅允许导入 xls、xlsx 格式文件</span>
|
||||
<el-link type="primary" :underline="false" @click="downloadImportTpl">下载模板</el-link>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="importOpen=false">取 消</el-button>
|
||||
<el-button type="primary" :loading="importing" @click="submitImport">确 定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<ImportResultDialog v-model="importResultOpen" :result="importResult" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
|
||||
import { countHospitalUsage, downloadHospitalTemplate, importHospital, exportHospital } from '@/api/business/hospital'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
import ImportResultDialog from '@/components/ImportResultDialog.vue'
|
||||
|
||||
const q = reactive({ hospital: '', province: '', militaryHospital: '' })
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await bizList('hospital', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
rows.value = (data && data.rows) || []
|
||||
page.total = (data && data.total) || 0
|
||||
} catch { rows.value = []; page.total = 0 }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function reset() {
|
||||
Object.assign(q, { hospital: '', province: '', militaryHospital: '' })
|
||||
page.pageNum = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function militaryLabel(v) {
|
||||
return v === '1' ? '军医院' : v === '0' ? '非军医院' : (v || '-')
|
||||
}
|
||||
function militaryTagType(v) {
|
||||
return v === '1' ? 'warning' : 'info'
|
||||
}
|
||||
|
||||
// ===== 新增/编辑 =====
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
hospitalId: null, province: '', city: '', hospital: '',
|
||||
originalHospital: '', hospitalCode: '', militaryHospital: '', type: ''
|
||||
})
|
||||
const rules = {
|
||||
hospital: [{ required: true, message: '请输入医院名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
isEdit.value = false
|
||||
Object.assign(form, {
|
||||
hospitalId: null, province: '', city: '', hospital: '',
|
||||
originalHospital: '', hospitalCode: '', militaryHospital: '', type: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function onEdit(row) {
|
||||
isEdit.value = true
|
||||
Object.assign(form, {
|
||||
hospitalId: row.hospitalId,
|
||||
province: row.province || '',
|
||||
city: row.city || '',
|
||||
hospital: row.hospital || '',
|
||||
originalHospital: row.originalHospital || '',
|
||||
hospitalCode: row.hospitalCode || '',
|
||||
militaryHospital: row.militaryHospital || '',
|
||||
type: row.type || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
await formRef.value.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await bizUpdate('hospital', { ...form })
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await bizAdd('hospital', { ...form })
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
// ===== 删除 (硬删, 带引用统计二次确认) =====
|
||||
async function onDelete(row) {
|
||||
let usage = 0
|
||||
try {
|
||||
const r = await countHospitalUsage(row.hospital)
|
||||
usage = r?.data || 0
|
||||
} catch { usage = 0 }
|
||||
const msg = usage > 0
|
||||
? `医院「${row.hospital}」已被 ${usage} 条业务记录(专家/参会人/意向)引用, 删除后这些记录仍保留医院名称文本, 是否继续删除?`
|
||||
: `确定删除医院「${row.hospital}」? 该操作不可恢复`
|
||||
try {
|
||||
await ElMessageBox.confirm(msg, '删除确认', { type: 'warning' })
|
||||
} catch { return }
|
||||
try {
|
||||
await bizDelete('hospital', row.hospitalId)
|
||||
ElMessage.success('删除成功')
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.msg || '删除失败') }
|
||||
}
|
||||
|
||||
// ===== 批量导入 =====
|
||||
const importOpen = ref(false)
|
||||
const importing = ref(false)
|
||||
const importResultOpen = ref(false)
|
||||
const importResult = ref(null)
|
||||
const importFile = ref(null)
|
||||
|
||||
function openImport() {
|
||||
importResult.value = null
|
||||
importFile.value = null
|
||||
importOpen.value = true
|
||||
}
|
||||
|
||||
function onImportFileChange(file) {
|
||||
importFile.value = file.raw
|
||||
}
|
||||
function onImportFileRemove() {
|
||||
importFile.value = null
|
||||
}
|
||||
|
||||
function downloadImportTpl() {
|
||||
downloadHospitalTemplate().then(res => {
|
||||
downloadBlob(res, '医院批量导入模板.xlsx')
|
||||
}).catch(() => ElMessage.error('模板下载失败'))
|
||||
}
|
||||
|
||||
async function submitImport() {
|
||||
if (!importFile.value) return ElMessage.warning('请先选择文件')
|
||||
importing.value = true
|
||||
try {
|
||||
const r = await importHospital(importFile.value)
|
||||
const result = r?.data
|
||||
if (!result || typeof result.okNum !== 'number') {
|
||||
ElMessage.error('导入失败, 返回数据异常')
|
||||
return
|
||||
}
|
||||
importResult.value = result
|
||||
importOpen.value = false
|
||||
importResultOpen.value = true
|
||||
load()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '导入失败')
|
||||
} finally { importing.value = false }
|
||||
}
|
||||
|
||||
// ===== 导出 =====
|
||||
async function onExport() {
|
||||
try {
|
||||
const res = await exportHospital({ ...q })
|
||||
downloadBlob(res, `医院数据_${dateStamp()}.xlsx`)
|
||||
} catch (e) { ElMessage.error(e?.msg || '导出失败') }
|
||||
}
|
||||
|
||||
function downloadBlob(res, filename) {
|
||||
const blob = res && res.data
|
||||
if (!blob) return ElMessage.error('导出失败')
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function dateStamp() {
|
||||
const d = new Date()
|
||||
const p = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}`
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-hospital { padding: 16px; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
|
||||
/* ========================================
|
||||
移动端适配 (≤768px) — 复用 SponsorOrgs.vue 标准模式
|
||||
======================================== */
|
||||
@media (max-width: 768px) {
|
||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||
|
||||
.filter-form {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
float: none !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
text-align: right !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
font-size: 13px !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
line-height: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.filter-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
line-height: 32px !important;
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.filter-form :deep(.el-select),
|
||||
.filter-form :deep(.el-input),
|
||||
.filter-form :deep(.el-date-editor),
|
||||
.filter-form :deep(.el-button),
|
||||
.filter-form :deep(.el-cascader) {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
.filter-form :deep(.el-button + .el-button) {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
.batch-bar {
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 8px !important;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.batch-bar::-webkit-scrollbar { height: 4px; }
|
||||
.batch-bar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||||
.batch-bar :deep(.action-btn),
|
||||
.batch-bar :deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px !important;
|
||||
padding: 0 10px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
:deep(.el-table) { font-size: 12px !important; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user