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:
+119
@@ -0,0 +1,119 @@
|
||||
package com.ruoyi.business.controller;
|
||||
|
||||
import java.util.List;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.business.domain.BizHospital;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
import com.ruoyi.business.domain.vo.BizHospitalExportVo;
|
||||
import com.ruoyi.business.domain.vo.BizHospitalImportVo;
|
||||
import com.ruoyi.business.service.IBizHospitalService;
|
||||
|
||||
/**
|
||||
* 医院字典 Controller (admin 资料库管理 / 医院管理)
|
||||
*
|
||||
* <p>CRUD + 导入/导出走 admin 路由 (前端 AdminLayout 角色控制);
|
||||
* 远程搜索 /search 为匿名公开 (医生注册下拉).
|
||||
* 注: 不加 @PreAuthorize — 与 BizExpert/BizOrg 等业务 controller 一致 (业务角色不走 RuoYi RBAC).
|
||||
*
|
||||
* @author guoju
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/business/hospital")
|
||||
public class BizHospitalController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IBizHospitalService bizHospitalService;
|
||||
|
||||
/** 分页列表 (admin 后台, 支持 hospital/province/city/hospitalCode/militaryHospital/type 筛选) */
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(BizHospital entity) {
|
||||
startPage();
|
||||
List<BizHospital> rows = bizHospitalService.selectList(entity);
|
||||
return getDataTable(rows);
|
||||
}
|
||||
|
||||
/** 详情 */
|
||||
@GetMapping("/{hospitalId}")
|
||||
public AjaxResult getInfo(@PathVariable("hospitalId") Long hospitalId) {
|
||||
return success(bizHospitalService.getById(hospitalId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程搜索 (匿名公开, 医生注册下拉"边输入边查询")
|
||||
* GET /business/hospital/search?keyword=xxx → [{hospitalId, province, city, hospital, ...}] LIMIT 30
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
public AjaxResult search(@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
return success(bizHospitalService.searchByKeyword(keyword));
|
||||
}
|
||||
|
||||
/** 删除前引用统计: 返回医院名被业务表 work_unit 引用的条数 */
|
||||
@GetMapping("/countUsage")
|
||||
public AjaxResult countUsage(@RequestParam("hospital") String hospital) {
|
||||
return success(bizHospitalService.countUsageByHospital(hospital));
|
||||
}
|
||||
|
||||
@Log(title = "医院管理", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody BizHospital entity) {
|
||||
return toAjax(bizHospitalService.insert(entity));
|
||||
}
|
||||
|
||||
@Log(title = "医院管理", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody BizHospital entity) {
|
||||
return toAjax(bizHospitalService.updateByPrimaryKey(entity));
|
||||
}
|
||||
|
||||
@Log(title = "医院管理", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{hospitalIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] hospitalIds) {
|
||||
return toAjax(bizHospitalService.deleteByPrimaryKeys(hospitalIds));
|
||||
}
|
||||
|
||||
/** 导出 (中文列头, 跟随当前筛选条件) */
|
||||
@Log(title = "医院管理", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, BizHospital entity) {
|
||||
List<BizHospital> list = bizHospitalService.selectList(entity);
|
||||
List<BizHospitalExportVo> exportList = new java.util.ArrayList<>(list.size());
|
||||
for (BizHospital h : list) {
|
||||
BizHospitalExportVo v = new BizHospitalExportVo();
|
||||
v.setHospitalId(h.getHospitalId());
|
||||
v.setProvince(h.getProvince());
|
||||
v.setCity(h.getCity());
|
||||
v.setHospital(h.getHospital());
|
||||
v.setOriginalHospital(h.getOriginalHospital());
|
||||
v.setHospitalCode(h.getHospitalCode());
|
||||
v.setMilitaryHospital(h.getMilitaryHospital());
|
||||
v.setType(h.getType());
|
||||
exportList.add(v);
|
||||
}
|
||||
ExcelUtil<BizHospitalExportVo> util = new ExcelUtil<>(BizHospitalExportVo.class);
|
||||
util.exportExcel(response, exportList, "医院数据");
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
@GetMapping("/importTemplate")
|
||||
public void importTemplate(HttpServletResponse response) {
|
||||
ExcelUtil<BizHospitalImportVo> util = new ExcelUtil<>(BizHospitalImportVo.class);
|
||||
util.importTemplateExcel(response, "医院数据");
|
||||
}
|
||||
|
||||
/** 批量导入 (Excel → biz_hospital, 去重, 返回 ImportResult) */
|
||||
@Log(title = "医院管理", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/importData")
|
||||
public AjaxResult importData(MultipartFile file) throws Exception {
|
||||
ImportResult result = bizHospitalService.importHospitals(file);
|
||||
return success(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.ruoyi.business.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
|
||||
/**
|
||||
* 医院字典 (biz_hospital)
|
||||
*
|
||||
* <p>admin 用户在"资料库管理 / 医院管理"下维护; 医生注册时通过 /business/hospital/search 远程搜索选择.
|
||||
*
|
||||
* <p>字段说明:
|
||||
* <ul>
|
||||
* <li>hospitalId — 雪花 ID (Long), 由 SnowflakeId.injectIfEmpty 生成; 53-bit, JS Number 安全</li>
|
||||
* <li>province/city — 省/市 (varchar 100)</li>
|
||||
* <li>hospital — 医院名称 (varchar 1000, 主显示名)</li>
|
||||
* <li>originalHospital — 原名称 (varchar 100)</li>
|
||||
* <li>hospitalCode — 医院 CODE (varchar 100, 如 H3301006)</li>
|
||||
* <li>militaryHospital — 是否军医院 (varchar 100, 现存数据为 '0'/'1')</li>
|
||||
* <li>type — 类型 (varchar 100, 现存数据为 '0'/'1')</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>说明: 原表 schema 中 id 为 int NOT NULL 无 auto_increment, 改 BIGINT 由应用生成雪花 ID (与项目其他表风格一致).
|
||||
*
|
||||
* @author guoju
|
||||
*/
|
||||
public class BizHospital {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 医院 ID (雪花 ID, BIGINT) */
|
||||
@Excel(name = "医院ID")
|
||||
private Long hospitalId;
|
||||
|
||||
/** 省 */
|
||||
@Excel(name = "省")
|
||||
private String province;
|
||||
|
||||
/** 市 */
|
||||
@Excel(name = "市")
|
||||
private String city;
|
||||
|
||||
/** 医院名称 (主显示名) */
|
||||
@Excel(name = "医院名称")
|
||||
private String hospital;
|
||||
|
||||
/** 原名称 */
|
||||
@Excel(name = "原名称")
|
||||
private String originalHospital;
|
||||
|
||||
/** 医院 CODE */
|
||||
@Excel(name = "医院CODE")
|
||||
private String hospitalCode;
|
||||
|
||||
/** 是否军医院 */
|
||||
@Excel(name = "是否军医院")
|
||||
private String militaryHospital;
|
||||
|
||||
/** 类型 */
|
||||
@Excel(name = "类型")
|
||||
private String type;
|
||||
|
||||
public Long getHospitalId() { return hospitalId; }
|
||||
public void setHospitalId(Long hospitalId) { this.hospitalId = hospitalId; }
|
||||
|
||||
public String getProvince() { return province; }
|
||||
public void setProvince(String province) { this.province = province; }
|
||||
|
||||
public String getCity() { return city; }
|
||||
public void setCity(String city) { this.city = city; }
|
||||
|
||||
public String getHospital() { return hospital; }
|
||||
public void setHospital(String hospital) { this.hospital = hospital; }
|
||||
|
||||
public String getOriginalHospital() { return originalHospital; }
|
||||
public void setOriginalHospital(String originalHospital) { this.originalHospital = originalHospital; }
|
||||
|
||||
public String getHospitalCode() { return hospitalCode; }
|
||||
public void setHospitalCode(String hospitalCode) { this.hospitalCode = hospitalCode; }
|
||||
|
||||
public String getMilitaryHospital() { return militaryHospital; }
|
||||
public void setMilitaryHospital(String militaryHospital) { this.militaryHospital = militaryHospital; }
|
||||
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.ruoyi.business.domain.vo;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
|
||||
/**
|
||||
* 医院字典导出 VO
|
||||
*
|
||||
* <p>仅用于 Excel 导出, 不参与业务逻辑.
|
||||
*
|
||||
* @author guoju
|
||||
*/
|
||||
public class BizHospitalExportVo {
|
||||
|
||||
@Excel(name = "医院ID", sort = 1)
|
||||
private Long hospitalId;
|
||||
|
||||
@Excel(name = "省", sort = 2)
|
||||
private String province;
|
||||
|
||||
@Excel(name = "市", sort = 3)
|
||||
private String city;
|
||||
|
||||
@Excel(name = "医院名称", sort = 4)
|
||||
private String hospital;
|
||||
|
||||
@Excel(name = "原名称", sort = 5)
|
||||
private String originalHospital;
|
||||
|
||||
@Excel(name = "医院CODE", sort = 6)
|
||||
private String hospitalCode;
|
||||
|
||||
@Excel(name = "是否军医院", sort = 7)
|
||||
private String militaryHospital;
|
||||
|
||||
@Excel(name = "类型", sort = 8)
|
||||
private String type;
|
||||
|
||||
public Long getHospitalId() { return hospitalId; }
|
||||
public void setHospitalId(Long hospitalId) { this.hospitalId = hospitalId; }
|
||||
|
||||
public String getProvince() { return province; }
|
||||
public void setProvince(String province) { this.province = province; }
|
||||
|
||||
public String getCity() { return city; }
|
||||
public void setCity(String city) { this.city = city; }
|
||||
|
||||
public String getHospital() { return hospital; }
|
||||
public void setHospital(String hospital) { this.hospital = hospital; }
|
||||
|
||||
public String getOriginalHospital() { return originalHospital; }
|
||||
public void setOriginalHospital(String originalHospital) { this.originalHospital = originalHospital; }
|
||||
|
||||
public String getHospitalCode() { return hospitalCode; }
|
||||
public void setHospitalCode(String hospitalCode) { this.hospitalCode = hospitalCode; }
|
||||
|
||||
public String getMilitaryHospital() { return militaryHospital; }
|
||||
public void setMilitaryHospital(String militaryHospital) { this.militaryHospital = militaryHospital; }
|
||||
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.ruoyi.business.domain.vo;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
|
||||
/**
|
||||
* 医院字典导入 VO
|
||||
*
|
||||
* <p>仅用于 Excel 批量导入 / 模板下载, 不参与业务逻辑.
|
||||
* 字段顺序与 Excel 列一致; hospitalId 不在导入范围 (由 SnowflakeId 生成).
|
||||
*
|
||||
* <p>必填: hospital (医院名称).
|
||||
* 其余字段允许空.
|
||||
*
|
||||
* @author guoju
|
||||
*/
|
||||
public class BizHospitalImportVo {
|
||||
|
||||
/** 省 */
|
||||
@Excel(name = "省", sort = 1)
|
||||
private String province;
|
||||
|
||||
/** 市 */
|
||||
@Excel(name = "市", sort = 2)
|
||||
private String city;
|
||||
|
||||
/** 医院名称 (必填) */
|
||||
@Excel(name = "医院名称", sort = 3)
|
||||
private String hospital;
|
||||
|
||||
/** 原名称 */
|
||||
@Excel(name = "原名称", sort = 4)
|
||||
private String originalHospital;
|
||||
|
||||
/** 医院 CODE */
|
||||
@Excel(name = "医院CODE", sort = 5)
|
||||
private String hospitalCode;
|
||||
|
||||
/** 是否军医院 (0=否, 1=是) */
|
||||
@Excel(name = "是否军医院", sort = 6)
|
||||
private String militaryHospital;
|
||||
|
||||
/** 类型 */
|
||||
@Excel(name = "类型", sort = 7)
|
||||
private String type;
|
||||
|
||||
public String getProvince() { return province; }
|
||||
public void setProvince(String province) { this.province = province; }
|
||||
|
||||
public String getCity() { return city; }
|
||||
public void setCity(String city) { this.city = city; }
|
||||
|
||||
public String getHospital() { return hospital; }
|
||||
public void setHospital(String hospital) { this.hospital = hospital; }
|
||||
|
||||
public String getOriginalHospital() { return originalHospital; }
|
||||
public void setOriginalHospital(String originalHospital) { this.originalHospital = originalHospital; }
|
||||
|
||||
public String getHospitalCode() { return hospitalCode; }
|
||||
public void setHospitalCode(String hospitalCode) { this.hospitalCode = hospitalCode; }
|
||||
|
||||
public String getMilitaryHospital() { return militaryHospital; }
|
||||
public void setMilitaryHospital(String militaryHospital) { this.militaryHospital = militaryHospital; }
|
||||
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.ruoyi.business.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.business.domain.BizHospital;
|
||||
|
||||
/**
|
||||
* 医院字典 Mapper (biz_hospital)
|
||||
*
|
||||
* <p>无 del_flag 列 — 删除走硬删 (admin 字典, workUnit 字符串引用, 删除不会破坏业务数据完整性).
|
||||
*
|
||||
* @author guoju
|
||||
*/
|
||||
public interface BizHospitalMapper {
|
||||
|
||||
/** 按主键查询 */
|
||||
BizHospital selectByPrimaryKey(@Param("hospitalId") Long hospitalId);
|
||||
|
||||
/** 分页 + 多条件模糊筛选 (admin 后台用, 支持 hospital/province/type/militaryHospital) */
|
||||
List<BizHospital> selectList(BizHospital entity);
|
||||
|
||||
/** 新增 */
|
||||
int insert(BizHospital entity);
|
||||
|
||||
/** 按主键更新 (动态 trim SET) */
|
||||
int updateByPrimaryKey(BizHospital entity);
|
||||
|
||||
/** 批量硬删 */
|
||||
int deleteByPrimaryKeys(@Param("hospitalIds") Long[] hospitalIds);
|
||||
|
||||
/**
|
||||
* 远程搜索: 按关键字模糊匹配 hospital / originalHospital / hospitalCode,
|
||||
* LIMIT 30 (医生注册下拉防爆).
|
||||
* keyword 为空时返回空集 (前端按"边输入边查询"约定不主动出建议).
|
||||
*/
|
||||
List<BizHospital> searchByKeyword(@Param("keyword") String keyword);
|
||||
|
||||
/**
|
||||
* 唯一性检查: 按 trim(hospital) + province + city 精确匹配 (case-insensitive).
|
||||
* hospitalId 非空时排除自身 (改名/同名异市用); 用于新增/编辑前的"同名医院"提示.
|
||||
* 返回匹配条数 (0 或 1).
|
||||
*/
|
||||
int countByTrimmedHospital(BizHospital entity);
|
||||
|
||||
/**
|
||||
* 统计指定医院名称被业务表 work_unit 字段引用的总条数.
|
||||
* 用于删除前给 admin 二次确认: "该医院已被 N 条专家/参会人引用".
|
||||
* 4 表 union: biz_expert / biz_signup_expert / biz_meeting_attendee / biz_execution_intent (有 work_unit 列)
|
||||
* 注: biz_support_intent 也有 work_unit 列, 一起并入.
|
||||
*/
|
||||
int countUsageByHospital(@Param("hospital") String hospital);
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.ruoyi.business.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.business.domain.BizHospital;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
|
||||
/**
|
||||
* 医院字典 Service 接口 (biz_hospital)
|
||||
*
|
||||
* @author guoju
|
||||
*/
|
||||
public interface IBizHospitalService {
|
||||
|
||||
/** 按主键查询 */
|
||||
BizHospital getById(Long hospitalId);
|
||||
|
||||
/** 分页 + 多条件筛选 */
|
||||
List<BizHospital> selectList(BizHospital entity);
|
||||
|
||||
/** 新增 (医院名查重 + 雪花 ID) */
|
||||
int insert(BizHospital entity);
|
||||
|
||||
/** 编辑 (医院名查重) */
|
||||
int updateByPrimaryKey(BizHospital entity);
|
||||
|
||||
/** 批量硬删 */
|
||||
int deleteByPrimaryKeys(Long[] hospitalIds);
|
||||
|
||||
/** 远程搜索 (医生注册下拉, LIMIT 30) */
|
||||
List<BizHospital> searchByKeyword(String keyword);
|
||||
|
||||
/** 唯一性检查: 同 name + province + city 是否已存在 (排除自身) */
|
||||
int countByTrimmedHospital(BizHospital entity);
|
||||
|
||||
/** 统计医院名被业务表 work_unit 引用的条数 (删除前二次确认用) */
|
||||
int countUsageByHospital(String hospital);
|
||||
|
||||
/** 批量导入 (Excel → biz_hospital, 批内/DB 去重, 返回 ImportResult) */
|
||||
ImportResult importHospitals(MultipartFile file) throws Exception;
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package com.ruoyi.business.service.impl;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.business.domain.BizHospital;
|
||||
import com.ruoyi.business.domain.dto.ImportResult;
|
||||
import com.ruoyi.business.domain.vo.BizHospitalImportVo;
|
||||
import com.ruoyi.business.mapper.BizHospitalMapper;
|
||||
import com.ruoyi.business.service.IBizHospitalService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.id.SnowflakeId;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
|
||||
/**
|
||||
* 医院字典 Service 实现 (biz_hospital)
|
||||
*
|
||||
* <p>要点:
|
||||
* <ul>
|
||||
* <li>所有字符串字段入库前 trim (防首尾空白脏数据)</li>
|
||||
* <li>医院名 + province + city 判重 (trim + case-insensitive)</li>
|
||||
* <li>主键走雪花 ID (SnowflakeId.injectIfEmpty) — 前提: id 列已 ALTER 为 BIGINT</li>
|
||||
* <li>删除走硬删 (字典表, workUnit 为字符串无外键, 删除不破坏业务完整性)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author guoju
|
||||
*/
|
||||
@Service
|
||||
public class BizHospitalServiceImpl implements IBizHospitalService {
|
||||
|
||||
@Autowired
|
||||
private BizHospitalMapper bizHospitalMapper;
|
||||
|
||||
@Override
|
||||
public BizHospital getById(Long hospitalId) {
|
||||
return bizHospitalMapper.selectByPrimaryKey(hospitalId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizHospital> selectList(BizHospital entity) {
|
||||
return bizHospitalMapper.selectList(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insert(BizHospital entity) {
|
||||
String hospital = trimToNull(entity.getHospital());
|
||||
if (hospital == null) {
|
||||
throw new ServiceException("医院名称不能为空");
|
||||
}
|
||||
entity.setHospital(hospital);
|
||||
normalize(entity);
|
||||
|
||||
// 同名医院查重 (trim + case-insensitive + province/city 匹配)
|
||||
BizHospital probe = new BizHospital();
|
||||
probe.setHospital(hospital);
|
||||
probe.setProvince(entity.getProvince());
|
||||
probe.setCity(entity.getCity());
|
||||
if (bizHospitalMapper.countByTrimmedHospital(probe) > 0) {
|
||||
throw new ServiceException("医院「" + hospital + "」已存在");
|
||||
}
|
||||
|
||||
SnowflakeId.injectIfEmpty(entity, "hospitalId");
|
||||
return bizHospitalMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateByPrimaryKey(BizHospital entity) {
|
||||
if (entity.getHospitalId() == null) {
|
||||
throw new ServiceException("医院 ID 不能为空");
|
||||
}
|
||||
String hospital = trimToNull(entity.getHospital());
|
||||
if (hospital == null) {
|
||||
throw new ServiceException("医院名称不能为空");
|
||||
}
|
||||
entity.setHospital(hospital);
|
||||
normalize(entity);
|
||||
|
||||
// 改名时同样查重 (排除自身)
|
||||
BizHospital probe = new BizHospital();
|
||||
probe.setHospital(hospital);
|
||||
probe.setProvince(entity.getProvince());
|
||||
probe.setCity(entity.getCity());
|
||||
probe.setHospitalId(entity.getHospitalId());
|
||||
if (bizHospitalMapper.countByTrimmedHospital(probe) > 0) {
|
||||
throw new ServiceException("医院「" + hospital + "」已存在");
|
||||
}
|
||||
|
||||
return bizHospitalMapper.updateByPrimaryKey(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByPrimaryKeys(Long[] hospitalIds) {
|
||||
return bizHospitalMapper.deleteByPrimaryKeys(hospitalIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BizHospital> searchByKeyword(String keyword) {
|
||||
return bizHospitalMapper.searchByKeyword(keyword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countByTrimmedHospital(BizHospital entity) {
|
||||
return bizHospitalMapper.countByTrimmedHospital(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countUsageByHospital(String hospital) {
|
||||
if (hospital == null || hospital.trim().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return bizHospitalMapper.countUsageByHospital(hospital.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入: Excel → biz_hospital.
|
||||
* <ol>
|
||||
* <li>ExcelUtil 反序列化</li>
|
||||
* <li>行级校验 (医院名必填) + 批内去重 + DB 去重</li>
|
||||
* <li>逐行 insert (雪花 ID), 失败记 ngList 不影响其它行</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Override
|
||||
public ImportResult importHospitals(MultipartFile file) throws Exception {
|
||||
ExcelUtil<BizHospitalImportVo> util = new ExcelUtil<>(BizHospitalImportVo.class);
|
||||
List<BizHospitalImportVo> importList = util.importExcel(file.getInputStream());
|
||||
if (importList == null || importList.isEmpty()) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
}
|
||||
ImportResult result = new ImportResult();
|
||||
Set<String> seenHospital = new HashSet<>();
|
||||
|
||||
for (int i = 0; i < importList.size(); i++) {
|
||||
BizHospitalImportVo vo = importList.get(i);
|
||||
int rowNo = i + 2; // Excel 行号 (1=表头)
|
||||
try {
|
||||
String hospital = trimToNull(vo.getHospital());
|
||||
if (hospital == null) {
|
||||
throw new ServiceException("医院名称不能为空");
|
||||
}
|
||||
|
||||
// 批内去重 (case-insensitive)
|
||||
String key = hospital.toLowerCase();
|
||||
if (!seenHospital.add(key)) {
|
||||
throw new ServiceException("医院「" + hospital + "」在文件中重复");
|
||||
}
|
||||
|
||||
// DB 去重
|
||||
BizHospital probe = new BizHospital();
|
||||
probe.setHospital(hospital);
|
||||
probe.setProvince(trimToNull(vo.getProvince()));
|
||||
probe.setCity(trimToNull(vo.getCity()));
|
||||
if (bizHospitalMapper.countByTrimmedHospital(probe) > 0) {
|
||||
throw new ServiceException("医院「" + hospital + "」已存在");
|
||||
}
|
||||
|
||||
BizHospital entity = new BizHospital();
|
||||
entity.setHospital(hospital);
|
||||
entity.setProvince(trimToNull(vo.getProvince()));
|
||||
entity.setCity(trimToNull(vo.getCity()));
|
||||
entity.setOriginalHospital(trimToNull(vo.getOriginalHospital()));
|
||||
entity.setHospitalCode(trimToNull(vo.getHospitalCode()));
|
||||
entity.setMilitaryHospital(trimToNull(vo.getMilitaryHospital()));
|
||||
entity.setType(trimToNull(vo.getType()));
|
||||
SnowflakeId.injectIfEmpty(entity, "hospitalId");
|
||||
bizHospitalMapper.insert(entity);
|
||||
|
||||
result.ok();
|
||||
} catch (Exception e) {
|
||||
result.fail(rowNo, e.getMessage() == null ? "导入失败" : e.getMessage());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 除 hospital 外的字段统一 trim (把空串压成 null, 避免脏数据) */
|
||||
private void normalize(BizHospital entity) {
|
||||
entity.setProvince(trimToNull(entity.getProvince()));
|
||||
entity.setCity(trimToNull(entity.getCity()));
|
||||
entity.setOriginalHospital(trimToNull(entity.getOriginalHospital()));
|
||||
entity.setHospitalCode(trimToNull(entity.getHospitalCode()));
|
||||
entity.setMilitaryHospital(trimToNull(entity.getMilitaryHospital()));
|
||||
entity.setType(trimToNull(entity.getType()));
|
||||
}
|
||||
|
||||
private String trimToNull(String s) {
|
||||
if (s == null) return null;
|
||||
String t = s.trim();
|
||||
return t.isEmpty() ? null : t;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.business.mapper.BizHospitalMapper">
|
||||
|
||||
<resultMap type="BizHospital" id="BizHospitalResult">
|
||||
<id property="hospitalId" column="id" />
|
||||
<result property="province" column="province" />
|
||||
<result property="city" column="city" />
|
||||
<result property="hospital" column="hospital" />
|
||||
<result property="originalHospital" column="original_hospital" />
|
||||
<result property="hospitalCode" column="hospital_code" />
|
||||
<result property="militaryHospital" column="military_hospital" />
|
||||
<result property="type" column="type" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFields">
|
||||
select id, province, city, hospital, original_hospital, hospital_code, military_hospital, type
|
||||
from biz_hospital
|
||||
</sql>
|
||||
|
||||
<select id="selectByPrimaryKey" resultMap="BizHospitalResult" parameterType="Long">
|
||||
<include refid="selectFields"/>
|
||||
where id = #{hospitalId}
|
||||
</select>
|
||||
|
||||
<!--
|
||||
admin 后台分页 + 多条件筛选.
|
||||
所有 string 字段都走 trim+LIKE 防首尾空白; province/militaryHospital/type 支持精确 + 模糊混合 (用 trim 防空白).
|
||||
-->
|
||||
<select id="selectList" resultMap="BizHospitalResult" parameterType="BizHospital">
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
<if test="hospital != null and hospital != ''">and trim(hospital) like concat('%', #{hospital}, '%')</if>
|
||||
<if test="province != null and province != ''">and trim(province) like concat('%', #{province}, '%')</if>
|
||||
<if test="city != null and city != ''">and trim(city) like concat('%', #{city}, '%')</if>
|
||||
<if test="hospitalCode != null and hospitalCode != ''">and trim(hospital_code) like concat('%', #{hospitalCode}, '%')</if>
|
||||
<if test="militaryHospital != null and militaryHospital != ''">and trim(military_hospital) = #{militaryHospital}</if>
|
||||
<if test="type != null and type != ''">and trim(type) = #{type}</if>
|
||||
</where>
|
||||
order by hospital_code asc, id asc
|
||||
</select>
|
||||
|
||||
<!--
|
||||
远程搜索: 三列 OR 模糊, LIMIT 30 防大表爆.
|
||||
keyword 为空: 不返回任何记录 (前端按"初始 options 为空"约定, 不主动出建议).
|
||||
-->
|
||||
<select id="searchByKeyword" resultMap="BizHospitalResult">
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (
|
||||
trim(hospital) like concat('%', #{keyword}, '%')
|
||||
or trim(original_hospital) like concat('%', #{keyword}, '%')
|
||||
or trim(hospital_code) like concat('%', #{keyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="keyword == null or keyword == ''">
|
||||
and 1 = 0
|
||||
</if>
|
||||
</where>
|
||||
order by hospital_code asc, id asc
|
||||
limit 30
|
||||
</select>
|
||||
|
||||
<!--
|
||||
ID 生成: 雪花 ID (Long), 由 service 层 SnowflakeId.injectIfEmpty 注入 hospitalId.
|
||||
注: 需先执行 ALTER TABLE biz_hospital MODIFY COLUMN id BIGINT NOT NULL (部署前 DDL);
|
||||
雪花值 (~10^15) 与现有 int 数据 (max 29248) 不冲突.
|
||||
-->
|
||||
<insert id="insert" parameterType="BizHospital">
|
||||
insert into biz_hospital
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
id,
|
||||
<if test="province != null and province != ''">province,</if>
|
||||
<if test="city != null and city != ''">city,</if>
|
||||
hospital,
|
||||
<if test="originalHospital != null and originalHospital != ''">original_hospital,</if>
|
||||
<if test="hospitalCode != null and hospitalCode != ''">hospital_code,</if>
|
||||
<if test="militaryHospital != null and militaryHospital != ''">military_hospital,</if>
|
||||
<if test="type != null and type != ''">type,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
#{hospitalId},
|
||||
<if test="province != null and province != ''">#{province},</if>
|
||||
<if test="city != null and city != ''">#{city},</if>
|
||||
#{hospital},
|
||||
<if test="originalHospital != null and originalHospital != ''">#{originalHospital},</if>
|
||||
<if test="hospitalCode != null and hospitalCode != ''">#{hospitalCode},</if>
|
||||
<if test="militaryHospital != null and militaryHospital != ''">#{militaryHospital},</if>
|
||||
<if test="type != null and type != ''">#{type},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<!--
|
||||
trim + 动态 SET: hospital 必填, 其余可空更新 (trim 防首尾空白污染).
|
||||
-->
|
||||
<update id="updateByPrimaryKey" parameterType="BizHospital">
|
||||
update biz_hospital
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="province != null">province = #{province},</if>
|
||||
<if test="city != null">city = #{city},</if>
|
||||
hospital = #{hospital},
|
||||
<if test="originalHospital != null">original_hospital = #{originalHospital},</if>
|
||||
<if test="hospitalCode != null">hospital_code = #{hospitalCode},</if>
|
||||
<if test="militaryHospital != null">military_hospital = #{militaryHospital},</if>
|
||||
<if test="type != null">type = #{type},</if>
|
||||
</trim>
|
||||
where id = #{hospitalId}
|
||||
</update>
|
||||
|
||||
<!-- 硬删 (无 del_flag) -->
|
||||
<delete id="deleteByPrimaryKeys" parameterType="Long">
|
||||
delete from biz_hospital where id in
|
||||
<foreach collection="hospitalIds" item="hid" open="(" separator="," close=")">
|
||||
#{hid}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!--
|
||||
唯一性检查: trim + case-insensitive (lower) + 省/市精确匹配.
|
||||
语义: 同名称 + 同省 + 同市 才算重复 (不同省/市的同名医院是不同医院, 允许).
|
||||
hospitalId 非空时排除自身 (编辑改名场景).
|
||||
-->
|
||||
<select id="countByTrimmedHospital" parameterType="BizHospital" resultType="int">
|
||||
select count(*) from biz_hospital
|
||||
where lower(trim(hospital)) = lower(trim(#{hospital}))
|
||||
<if test="province != null and province != ''">and trim(province) = trim(#{province})</if>
|
||||
<if test="province == null or province == ''">and province is null</if>
|
||||
<if test="city != null and city != ''">and trim(city) = trim(#{city})</if>
|
||||
<if test="city == null or city == ''">and city is null</if>
|
||||
<if test="hospitalId != null">and id != #{hospitalId}</if>
|
||||
</select>
|
||||
|
||||
<!--
|
||||
删除前引用次数统计: 跨 4 张业务表 UNION, 统计 work_unit 字段等于指定医院名的总条数.
|
||||
用 lower+trim 防空白/大小写差异导致漏数.
|
||||
注: 表名/字段名确认依据 (设计文档 + 现状):
|
||||
biz_expert.work_unit (varchar 200) 已审核通过的专家档案
|
||||
biz_signup_expert.work_unit (varchar 200) 报名记录
|
||||
biz_meeting_attendee.work_unit (varchar 200) 会议参会人
|
||||
biz_execution_intent.work_unit (varchar 200) 执行意向 (按 collation utf8mb4_unicode_ci)
|
||||
biz_support_intent.work_unit (varchar 200) 支持意向 (legacy 旧表, 部分场景仍写入)
|
||||
-->
|
||||
<select id="countUsageByHospital" parameterType="String" resultType="int">
|
||||
select (
|
||||
(select count(*) from biz_expert where lower(trim(work_unit)) = lower(trim(#{hospital})))
|
||||
+ (select count(*) from biz_signup_expert where lower(trim(work_unit)) = lower(trim(#{hospital})))
|
||||
+ (select count(*) from biz_meeting_attendee where lower(trim(work_unit)) = lower(trim(#{hospital})))
|
||||
+ (select count(*) from biz_execution_intent where lower(trim(work_unit)) = lower(trim(#{hospital})))
|
||||
+ (select count(*) from biz_support_intent where lower(trim(work_unit)) = lower(trim(#{hospital})))
|
||||
) as total
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -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