yxh
2026-09-29 6ab4b1e0b68e4c347110e37018fa005603f53904
ai优化
已修改26个文件
已添加3个文件
1708 ■■■■■ 文件已修改
src/api/ecg/appointment/index.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ecg/checktype/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ecg/devrent/index.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ecg/doctor/index.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ecg/jobrecord/index.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ecg/queue/index.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config/axios/service.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useIdleLogout.ts 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/permission.ts 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/auth.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dict.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/patientInfo.ts 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/stringUtil.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/appointment/AppointmentConfirm.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/appointment/alter.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/appointment/index.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/checktype/CheckTypeForm.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/devrent/index.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/doctor/DevDataEntry.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/doctor/DevDismantle.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/doctor/DevInstall.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/doctor/Routine.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/doctor/components/DevInstallPanel.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/doctor/components/DevReadyPanel.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/doctor/components/PatientInfoBlock.vue 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/doctor/components/RoutinePanel.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/doctor/index.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/jobrecord/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ecg/queue/index.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ecg/appointment/index.ts
@@ -26,6 +26,22 @@
  bookCheckType: number // é¢„约检查类型
  paid: number //已付款
  bookSeqNum: number
  // ========== ä»¥ä¸‹å­—段后端 AppointmentRespVO ä¸€ç›´æœ‰è¿”回,此前 TS æœªå£°æ˜Ž ==========
  // éœ€æ±‚1 ä½¿ç”¨ï¼šçŠ¶æ€ / å¼€å•医师 / å¼€å•日期 / è·ä»Šå¤©æ•°ï¼ˆç”± doctorTime è®¡ç®—)/ å°±è¯Šå¡å·
  status?: string // çŠ¶æ€ï¼ˆå­—å…¸ ecg_status)
  doctor?: string // å¼€å•医师
  doctorCode?: string // å¼€å•医师代码
  doctorTime?: Date | number | string // å¼€å•日期(开单时间)
  registerDate?: Date | number | string // ç™»è®°æ—¶é—´
  hospitalNo?: string // ä½é™¢å·
  medicalCard?: string // å°±è¯Šå¡å·ï¼ˆåº“中多为空,页面回退到 outpatientNo)
  doctorAdviceNo?: string // åŒ»å˜±å·ï¼ˆHIS root 2.16.156.10011.1.28;库中历史数据为空)
  outpatientNo?: string // é—¨è¯Šå·ï¼ˆå®žæµ‹éƒ¨åˆ†æ•°æ®å­˜çš„æ˜¯èº«ä»½è¯å·ï¼‰
  bookSrc?: number // é¢„约来源:0人工 10平台
  roomName?: string // è¯Šå®¤åç§°
  remark?: string // å¤‡æ³¨
  jjbz?: string // åŠ æ€¥æ ‡è®°
  createTime?: Date | number | string // åˆ›å»ºæ—¶é—´
}
// é¢„约 API
src/api/ecg/checktype/index.ts
@@ -15,6 +15,14 @@
  affinityCheckTypes: number[] | null// äº²å’Œçš„æ£€æŸ¥ç±»åž‹
  notes: string  // é¢„约注意事项
  seqPrefix: string // åºå·å‰ç¼€
  /**
   * æŽ’队序号规则:
   * 0=预约号(用预约系统的号,不再分配)
   * 1=签到序号(当天同检查类型连续递增,每 9 ä¸ªé¢„ç•™ 1 ä¸ªç»™åŠ æ€¥ï¼‰
   * 2=时段号(按预约时间段给号)
   * æœªé…ç½®(null/undefined) æ—¶åŽç«¯æŒ‰ 2 å¤„理,与改造前行为一致。
   */
  seqRule: number | null
}
// æ£€æŸ¥ç±»åž‹ API
src/api/ecg/devrent/index.ts
@@ -12,12 +12,26 @@
  patGender?: number | undefined | null // æ‚£è€…性别:1 ç”· 2 å¥³ï¼ˆåŽç«¯ä»Žé¢„约单补齐,仅展示)
  patDetails?: PatDetailsVO | undefined | null
  checkType?: number | undefined | null
  tolerance?: string | number | undefined | null // æ˜¯å¦å¤šè€è¯ï¼šåŽç«¯ä¸ºå­—符串 "0"(否) / "1"(是)
  // å¤šè€è¯ï¼š0 æ—  / 1 å¤šè€ / 2 å¤šé‡è€è¯ï¼ˆåŽç«¯ä¸ºå­—符串)
  tolerance?: string | number | undefined | null
  // ä»¥ä¸‹å­—段由后端从预约单补齐,仅用于医生端展示,不参与提交
  patBirthday?: number | undefined | null // å‡ºç”Ÿæ—¥æœŸï¼ˆæ¯«ç§’时间戳)
  registerDate?: number | undefined | null // ç™»è®°æ—¶é—´ï¼ˆæ¯«ç§’时间戳)
  patBirthday?: number | string | undefined | null // å‡ºç”Ÿæ—¥æœŸ
  registerDate?: number | string | undefined | null // ç™»è®°æ—¶é—´
  doctor?: string | undefined | null // å¼€å•医师
  doctorTime?: number | undefined | null // å¼€å•日期(毫秒时间戳)
  doctorTime?: number | string | undefined | null // å¼€å•日期
  // éœ€æ±‚1:患者信息 18 é¡¹ï¼ˆåŽç«¯ DevRentRespVO å·²æ‰©å­—段,均由预约单回填)
  patSrc?: number | undefined | null // æ‚£è€…来源:1门诊 2急诊 3住院 4体检
  patMobile?: string | undefined | null // æ‰‹æœºå·
  patPhone?: string | undefined | null // ç”µè¯
  patWardDesc?: string | undefined | null // ç—…区名称
  patBedNo?: string | undefined | null // ç—…床号(注意不是 bedNo,后者是诊疗床/工位号)
  medicalCard?: string | undefined | null // å°±è¯Šå¡å·ï¼ˆåº“中多为空)
  doctorAdviceNo?: string // åŒ»å˜±å·ï¼ˆHIS root 2.16.156.10011.1.28;库中历史数据为空)
  outpatientNo?: string | undefined | null // é—¨è¯Šå·
  patIdentityId?: string | undefined | null // èº«ä»½è¯å·
  // é˜Ÿåˆ—相关(由 setPatient ä»Ž QueueVO å¸¦å…¥ï¼Œä»…用于叫号面板展示)
  seqNum?: number | undefined | null // æŽ’队序号
  status?: number | undefined | null // é˜Ÿåˆ—状态(字典 ecg_queue_status)
  rentTime?: number | undefined | null // è£…机时间
  returnTime?: number | undefined | null // æ‹†æœºæ—¶é—´
  interference?: number | undefined | null // å¹²æ‰°æ•°
src/api/ecg/doctor/index.ts
@@ -145,6 +145,41 @@
  patientJump: async (params: PatientVO) => {
    return await request.get({ url: `/ecg/doctor/patient-jump`, params })
  },
  // ==================== éœ€æ±‚2:撤销「叫号 / æ£€æŸ¥å®Œæˆã€ ====================
  // æ’¤é”€æ£€æŸ¥å®Œæˆï¼šå·²å°±è¯Š(40) â†’ å€™è¯Šä¸­(20)。必须带 patId + checkType ç²¾ç¡®å®šä½
  undoFinishPatient: async (params: PatientVO) => {
    return await request.get({ url: `/ecg/doctor/undo-finish-patient`, params })
  },
  // æ’¤é”€å«å·ï¼šå°±è¯Šä¸­(30) â†’ å€™è¯Šä¸­(20)
  undoCallPatient: async (params: { roomId: number | null; bedNo: string | null }) => {
    return await request.get({ url: `/ecg/doctor/undo-call-patient`, params })
  },
  // å–本工位今日最近一位已完成患者(供撤销提示)
  lastFinishedPatient: async (params: { roomId: number | null; bedNo: string | null }) => {
    return await request.get({ url: `/ecg/doctor/last-finished-patient`, params })
  },
  // ==================== éœ€æ±‚3:停止候诊开关 ====================
  // åœæ­¢å€™è¯Šï¼šä¸å†æ–°å¢žå€™è¯Šæ‚£è€…(已有候诊仍可叫号)
  stopReady: async (params: { roomId: number | null; bedNo: string | null }) => {
    return await request.get({ url: `/ecg/doctor/stop-ready`, params })
  },
  // æ¢å¤å€™è¯Šï¼šé‡æ–°å¼€å§‹æŽ¥æ”¶å€™è¯Šæ‚£è€…
  resumeReady: async (params: { roomId: number | null; bedNo: string | null }) => {
    return await request.get({ url: `/ecg/doctor/resume-ready`, params })
  },
  // æŸ¥è¯¢åœæ­¢å€™è¯ŠçŠ¶æ€
  getStopReady: async (params: { roomId: number | null; bedNo: string | null }) => {
    return await request.get({ url: `/ecg/doctor/get-stop-ready`, params })
  },
  // ä¸‹è½½
 yddownload: async (id: number | string) => {
  return await request.get({ url: `/ecg/template/download/${id}` })
src/api/ecg/jobrecord/index.ts
@@ -13,6 +13,24 @@
  summary: number // ä½œä¸šæ¦‚要
  remark: string // å¤‡æ³¨
  paid: number
  // éœ€æ±‚1:患者信息 18 é¡¹ï¼ˆåŽç«¯æŒ‰ job_record.rentId â†’ dev_rent.applyNo â†’ appointment æ‰¹é‡å›žå¡«ï¼‰
  applyNo?: string // ç”³è¯·ç¼–号
  episodeId?: string // å°±è¯Šæµæ°´å·
  patGender?: number // æ€§åˆ«
  patBirthday?: number | string // å‡ºç”Ÿæ—¥æœŸ
  registerDate?: number | string // ç™»è®°æ—¶é—´
  doctor?: string // å¼€å•医师
  doctorTime?: number | string // å¼€å•日期
  patSrc?: number // æ‚£è€…来源:1门诊 2急诊 3住院 4体检
  patMobile?: string
  patPhone?: string
  patWardDesc?: string // ç—…区
  patBedNo?: string // ç—…床号
  medicalCard?: string // å°±è¯Šå¡å·
  doctorAdviceNo?: string // åŒ»å˜±å·ï¼ˆHIS root 2.16.156.10011.1.28;库中历史数据为空)
  outpatientNo?: string // é—¨è¯Šå·
  patIdentityId?: string // èº«ä»½è¯å·
  tolerance?: string // å¤šè€è¯ï¼š0无 1多耐 2多重耐药
}
export interface JobStatisticVO {
src/api/ecg/queue/index.ts
@@ -14,20 +14,30 @@
  bookSeqNum?: number //预约序号
  isVip?: number // æ˜¯å¦VIP患者
  seqNum?: number // æŽ’队序号
  status?: number // 5:过号 10:排队中 15:已召回 20:候诊准备 30:就诊中 40:就诊完成
  status?: number // 3:已过号-排队 5:过号 7:已过号-安装 10:排队中 15:已召回 20:候诊中 30:就诊中 33:已领用 34:已召回-安装 36:安装中 40:已就诊
  passed?: number // è¿‡å·æ ‡è®°
  expired?: number // é¢„约过期标记
  jumpFlag?: number //插队标记
  roomId?: number // è¯Šå®¤ç¼–号
  roomName?: string // è¯Šå®¤åç§°
  bedNo?: string // è¯Šç–—床编号
  bedNo?: string // è¯Šç–—床编号(工位号,非病床号!)
  patDetails?: PatDetailsVO
  // ä»¥ä¸‹å­—段由后端从预约单补齐(queue è¡¨ä¸å­˜ï¼‰
  tolerance?: string // æ˜¯å¦å¤šè€è¯ï¼š0 å¦ï¼Œ1 æ˜¯
  patBirthday?: number // å‡ºç”Ÿæ—¥æœŸï¼ˆæ—¶é—´æˆ³ï¼‰
  registerDate?: number // ç™»è®°æ—¶é—´ï¼ˆæ—¶é—´æˆ³ï¼‰
  tolerance?: string // å¤šè€è¯ï¼š0 æ— ï¼Œ1 å¤šè€ï¼Œ2 å¤šé‡è€è¯
  patBirthday?: number | string // å‡ºç”Ÿæ—¥æœŸ
  registerDate?: number | string // ç™»è®°æ—¶é—´
  doctor?: string // å¼€å•医师
  doctorTime?: number // å¼€å•日期/开单时间(时间戳)
  doctorTime?: number | string // å¼€å•日期/开单时间
  // éœ€æ±‚1:患者信息 18 é¡¹ï¼ˆåŒæ ·ç”±åŽç«¯ä»Žé¢„约单平铺回填)
  patSrc?: number // æ‚£è€…来源:1门诊 2急诊 3住院 4体检
  patMobile?: string // æ‰‹æœºå·
  patPhone?: string // ç”µè¯
  patWardDesc?: string // ç—…区名称
  patBedNo?: string // ç—…床号(真正的病床号)
  medicalCard?: string // å°±è¯Šå¡å·ï¼ˆåº“中多为空)
  doctorAdviceNo?: string // åŒ»å˜±å·ï¼ˆHIS root 2.16.156.10011.1.28;库中历史数据为空)
  outpatientNo?: string // é—¨è¯Šå·
  patIdentityId?: string // èº«ä»½è¯å·
}
// æŽ’队 API
src/config/axios/service.ts
@@ -205,6 +205,17 @@
}
const handleAuthorized = () => {
  const { t } = useI18n()
  // ã€ä¿®å¤ã€‘鉴权失败时**立即**清理本地凭证与用户缓存,不再等用户点击弹窗。
  //
  // åŽŸå› ï¼šåŽŸå…ˆåªæœ‰åœ¨ç”¨æˆ·ç‚¹ã€Œé‡æ–°ç™»å½•ã€åŽæ‰ removeToken/deleteUserCache。
  // è‹¥ç”¨æˆ·ä¸ç‚¹ï¼ˆæˆ–页面在启动阶段就被异常打断、弹窗都没显示出来),
  // å¤±æ•ˆçš„ token ä¸Žè„çš„用户缓存会一直留在 localStorage é‡Œï¼Œ
  // å¯¼è‡´"刷新页面 â†’ å†æ¬¡ç”¨æ—§ token é‰´æƒ â†’ å†æ¬¡å¤±è´¥"的循环,
  // è¡¨çŽ°ä¸ºæ¯æ¬¡å¯åŠ¨éƒ½å¼¹ã€Œç™»å½•è¶…æ—¶ï¼Œè¯·é‡æ–°ç™»å½•ã€ã€‚
  removeToken()
  deleteUserCache()
  if (!isRelogin.show) {
    // å¦‚果已经到重新登录页面则不进行弹窗提示
    if (window.location.href.includes('login?redirect=')) {
@@ -219,13 +230,10 @@
      type: 'warning'
    }).then(() => {
      resetRouter() // é‡ç½®é™æ€è·¯ç”±è¡¨
      deleteUserCache() // åˆ é™¤ç”¨æˆ·ç¼“å­˜
      removeToken()
      isRelogin.show = false
      // å¹²æŽ‰token后再走一次路由让它过router.beforeEach的校验
      //window.location.href = window.location.href
      window.location.href ="/login"
      window.location.href = '/login'
    })
  }
  return Promise.reject(t('sys.api.timeoutMessage'))
src/hooks/web/useIdleLogout.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
import { watch } from 'vue'
import { useIdle } from '@vueuse/core'
import { ElMessage } from 'element-plus'
/**
 * ç©ºé—²è¶…时自动退出(需求:30 åˆ†é’Ÿä¸æ“ä½œæ‰è¦æ±‚重新输入密码)。
 *
 * èƒŒæ™¯
 * ----
 * åŽç«¯ token æ˜¯**固定有效期**(`system_oauth2_client.access_token_validity_seconds`,
 * å½“前 default å®¢æˆ·ç«¯ = 3600 ç§’),与"用户是否有操作"无关。
 * å› æ­¤ä¼šå‡ºçŽ°ä¸¤ç§éƒ½ä¸ç†æƒ³çš„æƒ…å†µï¼š
 *   Â· é¡µé¢æŒ‚着不动 â†’ åˆ°ç‚¹è¢«è¸¢ï¼ˆç”¨æˆ·è§‰å¾—"我没停过怎么又要登录");
 *   Â· ä¸€ç›´æ“ä½œ â†’ token åˆ°æœŸä¹Ÿä¼šè¢«è¸¢ï¼ˆæœŸæœ›æ˜¯"持续操作就不该重登")。
 *
 * æœ¬æ¨¡å—在前端补上**真正的空闲检测**:
 *   Â· æœ‰ä»»ä½•é¼ æ ‡/键盘/触摸/滚动操作 â†’ è®¡æ—¶å™¨é‡ç½®ï¼›
 *   Â· è¿žç»­ 30 åˆ†é’Ÿ**无任何操作** â†’ ä¸»åŠ¨æ¸…ç†ç™»å½•æ€å¹¶è·³è½¬ç™»å½•é¡µï¼ˆè¦æ±‚é‡æ–°è¾“å¯†ç ï¼‰ï¼›
 *   Â· æœ‰æ“ä½œæ—¶ä¸ä¸»åŠ¨ç™»å‡ºï¼ˆtoken è‡ªèº«çš„续期由 axios æ‹¦æˆªå™¨çš„ refresh-token è´Ÿè´£ï¼‰ã€‚
 *
 * ç”¨æ³•(在 permission.ts ä¸­è°ƒç”¨ä¸€æ¬¡å³å¯ï¼Œå†…部自带幂等保护):
 *   setupIdleLogout()
 */
/** ç©ºé—²é˜ˆå€¼ï¼š30 åˆ†é’Ÿï¼ˆéœ€æ±‚指定) */
export const IDLE_TIMEOUT_MINUTES = 30
const IDLE_TIMEOUT_MS = IDLE_TIMEOUT_MINUTES * 60 * 1000
/**
 * è§†ä¸º"有操作"的事件。
 * åˆ»æ„**不**包含 mousemove çš„高频节流问题:useIdle å†…部已做节流,
 * ä¸” mousemove æ˜¯åˆ¤æ–­"人还在电脑前"的最可靠信号(例如只是阅读页面时会移动鼠标)。
 *
 * æ³¨æ„ï¼šè¯Šé—´å±/叫号屏是长时间无人操作的展示页面,但那些页面在路由白名单里
 * ï¼ˆè§ permission.ts çš„ whiteList),不会进入本逻辑。
 */
const ACTIVITY_EVENTS = [
  'mousemove',
  'mousedown',
  'mouseup',
  'wheel',
  'keydown',
  'keyup',
  'touchstart',
  'touchend',
  'click',
  'scroll'
] as const
let started = false
let idleWatcher: (() => void) | null = null
/** æ˜¯å¦å·²å¯åŠ¨ï¼ˆé¿å…é‡å¤æ³¨å†Œï¼‰ */
export function isIdleLogoutStarted(): boolean {
  return started
}
/** æ¸…理(一般不需要调用;登出后若再登录,用 resetIdleLogout é‡ç½®ï¼‰ */
export function stopIdleLogout(): void {
  if (idleWatcher) {
    idleWatcher()
    idleWatcher = null
  }
  started = false
}
/**
 * å¯åŠ¨ç©ºé—²ç™»å‡ºæ£€æµ‹ã€‚å¹‚ç­‰ï¼šé‡å¤è°ƒç”¨åªç”Ÿæ•ˆä¸€æ¬¡ã€‚
 *
 * @param onTimeout è‡ªå®šä¹‰è¶…时动作(默认:清凭证 + è·³ç™»å½•页)
 */
export function setupIdleLogout(onTimeout?: () => void): void {
  if (started) return
  started = true
  const { idle, lastActive, stop } = useIdle(IDLE_TIMEOUT_MS, {
    events: ACTIVITY_EVENTS as unknown as string[],
    listen: true
  })
  // æš´éœ² stop ä¾¿äºŽå¤–部停止
  idleWatcher = stop
  // useIdle çš„ idle æ˜¯åŸºäºŽ"距上次活动是否超过阈值"的响应式布尔值。
  // è¿™é‡Œç”¨ watch è€Œä¸æ˜¯ interval,避免轮询开销。
  watch(idle, (isIdle: boolean) => {
    if (!isIdle) return
    // åŒä¿é™©ï¼šå†æ¬¡æ ¸å¯¹è·ä¸Šæ¬¡æ´»åŠ¨ç¡®å®žå·²è¶…è¿‡é˜ˆå€¼
    const inactiveMs = Date.now() - (lastActive.value || 0)
    if (inactiveMs < IDLE_TIMEOUT_MS) return
    if (onTimeout) {
      onTimeout()
      return
    }
    doIdleLogout()
  })
}
/**
 * å®žé™…执行空闲登出:清理本地凭证并跳转登录页。
 *
 * ä¹‹æ‰€ä»¥ä¸è°ƒç”¨ userStore.loginOut()(那是"主动退出"):
 *   Â· é‚£ä¸ªæŽ¥å£ä¼šè¯·æ±‚后端注销 token â€” å¯¹æœ¬åœºæ™¯æ²¡å¿…要,且可能因网络问题卡住;
 *   Â· ç©ºé—²ç™»å‡ºåªéœ€ä¿è¯æœ¬åœ°å‡­è¯æ¸…干净 + å›žåˆ°ç™»å½•页。
 */
function doIdleLogout(): void {
  ElMessage.warning(`已超过 ${IDLE_TIMEOUT_MINUTES} åˆ†é’Ÿæœªæ“ä½œï¼Œè¯·é‡æ–°ç™»å½•`)
  cleanupCredentials()
  // ç”¨ replace é¿å…ç”¨æˆ·æŒ‰"返回"回到已登出的页面
  const redirect = encodeURIComponent(window.location.pathname + window.location.search)
  setTimeout(() => {
    window.location.replace(`/login?redirect=${redirect}`)
  }, 800)
}
/**
 * æ¸…理登录凭证与用户缓存。
 * ç”¨åŠ¨æ€ import é¿å…ä¸Ž permission.ts å½¢æˆå¾ªçŽ¯ä¾èµ–ã€‚
 */
function cleanupCredentials(): void {
  import('@/utils/auth')
    .then(({ removeToken }) => {
      removeToken()
      return import('@/hooks/web/useCache')
    })
    .then(({ deleteUserCache }) => {
      deleteUserCache()
    })
    .catch((e) => {
      // å…œåº•:即使 import å¤±è´¥ä¹Ÿç›´æŽ¥æ¸… localStorage,保证不会带着旧 token å›žåˆ°é¦–页
      try {
        localStorage.removeItem('ACCESS_TOKEN')
        localStorage.removeItem('REFRESH_TOKEN')
      } catch {
        /* ignore */
      }
      console.error('[idle-logout] æ¸…理凭证失败', e)
    })
}
src/permission.ts
@@ -1,7 +1,8 @@
import router from './router'
import type { RouteRecordRaw } from 'vue-router'
import { isRelogin } from '@/config/axios/service'
import { getAccessToken } from '@/utils/auth'
import { getAccessToken, removeToken } from '@/utils/auth'
import { deleteUserCache } from '@/hooks/web/useCache'
import { useTitle } from '@/hooks/web/useTitle'
import { useNProgress } from '@/hooks/web/useNProgress'
import { usePageLoading } from '@/hooks/web/usePageLoading'
@@ -10,6 +11,8 @@
import { useUserStoreWithOut } from '@/store/modules/user'
import { usePermissionStoreWithOut } from '@/store/modules/permission'
import { useRoomStoreWithOut } from "@/store/modules/room";
// éœ€æ±‚:30 åˆ†é’Ÿæ— æ“ä½œæ‰è¦æ±‚重新登录(前端空闲检测)
import { setupIdleLogout } from '@/hooks/web/useIdleLogout'
const { start, done } = useNProgress()
@@ -76,66 +79,104 @@
    return
  }
  if (to.path === '/login') {
    next({ path: '/' })
    return
  }
  // ==========================================================================
  // ã€ä¿®å¤ã€‘以下所有 await éƒ½å¿…须包在 try/catch å†…。
  //
  // èƒŒæ™¯ï¼šlocalStorage é‡Œå¯èƒ½æ®‹ç•™**已失效的 token**(服务端重启、令牌过期等)。
  // æ­¤æ—¶ getAccessToken() ä»æœ‰å€¼ â†’ å®ˆå«ä¸ä¼šè·³ç™»å½•页 â†’ ç»§ç»­ç”¨æ—§ token è¯·æ±‚后端
  // â†’ 401 â†’ handleAuthorized() è¿”回 Promise.reject('登录超时,请重新登录!')。
  // åŽŸå…ˆè¯¥ reject æ— äººæ•获,会一路冒泡到 setupRouter(),导致:
  //     [Vue Router warn] uncaught error during route navigation
  //     [Vue Router warn] Unexpected error when starting the router
  //     main.ts:76 Uncaught (in promise) ç™»å½•è¶…æ—¶,请重新登录!
  // **路由安装失败,整个页面起不来**(用户必须手动重新登录才恢复)。
  //
  // çŽ°åœ¨ç»Ÿä¸€å…œåº•ï¼šæ— è®ºé‰´æƒå¤±è´¥è¿˜æ˜¯å…¶ä»–å¼‚å¸¸ï¼Œéƒ½æ¸…æŽ‰æœ¬åœ°å¤±æ•ˆå‡­è¯å¹¶è½¬ç™»å½•é¡µï¼Œ
  // ä¿è¯è·¯ç”±å™¨ä¸€å®šèƒ½å®Œæˆåˆå§‹åŒ–。
  //
  // æ³¨ï¼šåŽŸå…ˆæ­¤å¤„è¿˜æœ‰ `if (to.path === '/login') next({path:'/'})`,
  // å›  '/login' å·²åœ¨ whiteList ä¸­æå‰ return,该分支永不可达,已移除。
  // ==========================================================================
  try {
    // èŽ·å–æ‰€æœ‰å­—å…¸
    const dictStore = useDictStoreWithOut()
    const userStore = useUserStoreWithOut()
    const roomStore = useRoomStoreWithOut()
    const checkTypeStore = useCheckTypeStoreWithOut()
    const permissionStore = usePermissionStoreWithOut()
    if (!dictStore.getIsSetDict) {
      await dictStore.setDictMap()
    }
    if (!checkTypeStore.getIsSetCheckType) {
      await checkTypeStore.setCheckTypeMap()
    }
  // èŽ·å–æ‰€æœ‰å­—å…¸
  const dictStore = useDictStoreWithOut()
  const userStore = useUserStoreWithOut()
  const roomStore = useRoomStoreWithOut()
  const checkTypeStore = useCheckTypeStoreWithOut()
  const permissionStore = usePermissionStoreWithOut()
  if (!dictStore.getIsSetDict) {
    await dictStore.setDictMap()
  }
  if (!checkTypeStore.getIsSetCheckType) {
    await checkTypeStore.setCheckTypeMap()
  }
  if (userStore.getIsSetUser ) {
    if (userStore.getIsSetUser) {
      // <<<【诊室选择】<<<
      if ( to.path !== '/roomselect' &&
      if (to.path !== '/roomselect' &&
          userStore.getRoles.includes("doctor") && !userStore.getRoles.includes("super_admin")
          && !roomStore.getIsSetRoom ) {
          next({path: `/roomselect?redirect=${to.fullPath}`})
          return
          && !roomStore.getIsSetRoom) {
        next({ path: `/roomselect?redirect=${to.fullPath}` })
        return
      }
      next()
      return
    }
    isRelogin.show = true
    await userStore.setUserInfoAction()
    isRelogin.show = false
    // åŽç«¯è¿‡æ»¤èœå•
    await permissionStore.generateRoutes()
    permissionStore.getAddRouters.forEach((route) => {
      router.addRoute(route as unknown as RouteRecordRaw) // åŠ¨æ€æ·»åŠ å¯è®¿é—®è·¯ç”±è¡¨
    })
    // <<<【诊室选择】<<<
    if (to.path !== '/roomselect' &&
        userStore.getRoles.includes("doctor") && !userStore.getRoles.includes("super_admin")
        && !roomStore.getIsSetRoom) {
      next({ path: `/roomselect?redirect=${to.fullPath}` })
      return
    }
    const redirectPath = from.query.redirect || to.path
    // ä¿®å¤è·³è½¬æ—¶ä¸å¸¦å‚数的问题
    const redirect = decodeURIComponent(redirectPath as string)
    const { paramsObject: query } = parseURL(redirect)
    const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect, query }
    next(nextData)
  } catch (e) {
    // å…œåº•:清理本地失效凭证,放行到登录页,绝不让异常中断路由初始化
    console.warn('[permission] è·¯ç”±å®ˆå«æ‰§è¡Œå¤±è´¥ï¼Œå·²æ¸…理本地凭证并转登录页:', e)
    isRelogin.show = false
    removeToken()
    deleteUserCache()
    next(`/login?redirect=${to.fullPath}`)
  }
  isRelogin.show = true
  await userStore.setUserInfoAction()
  isRelogin.show = false
  // åŽç«¯è¿‡æ»¤èœå•
  await permissionStore.generateRoutes()
  permissionStore.getAddRouters.forEach((route) => {
    router.addRoute(route as unknown as RouteRecordRaw) // åŠ¨æ€æ·»åŠ å¯è®¿é—®è·¯ç”±è¡¨
  })
  // <<<【诊室选择】<<<
  if ( to.path !== '/roomselect' &&
      userStore.getRoles.includes("doctor") && !userStore.getRoles.includes("super_admin")
      && !roomStore.getIsSetRoom ) {
    next({path: `/roomselect?redirect=${to.fullPath}`})
    return
  }
  const redirectPath = from.query.redirect || to.path
  // ä¿®å¤è·³è½¬æ—¶ä¸å¸¦å‚数的问题
  const redirect = decodeURIComponent(redirectPath as string)
  const {paramsObject: query} = parseURL(redirect)
  const nextData = to.path === redirect ? {...to, replace: true} : {path: redirect, query}
  next(nextData)
})
router.afterEach((to) => {
  useTitle(to?.meta?.title as string)
  done() // ç»“束Progress
  loadDone()
  // ==========================================================================
  // éœ€æ±‚:**30 åˆ†é’Ÿä¸æ“ä½œ**才要求重新输入密码。
  //
  // èƒŒæ™¯ï¼šåŽç«¯ token æ˜¯å›ºå®šæœ‰æ•ˆæœŸï¼ˆsystem_oauth2_client.access_token_validity_seconds,
  // å½“前 default å®¢æˆ·ç«¯ = 3600 ç§’),与用户是否有操作无关 â€”— æŒ‚着不动也会到点被踢。
  // è¿™é‡Œåœ¨å‰ç«¯è¡¥ä¸Š**真正的空闲检测**:
  //   Â· æœ‰é¼ æ ‡/键盘/触摸/滚动等操作 â†’ è®¡æ—¶é‡ç½®ï¼›
  //   Â· è¿žç»­ 30 åˆ†é’Ÿæ— ä»»ä½•操作   â†’ ä¸»åŠ¨æ¸…å‡­è¯å¹¶è·³ç™»å½•é¡µã€‚
  // ä»ªè¡¨ç›˜/叫号屏等白名单页面不会走到这里,不受影响。
  //
  // åªåœ¨**已登录**且非登录页时启动;setupIdleLogout å†…部幂等,重复调用无副作用。
  // ==========================================================================
  if (getAccessToken() && to.path !== '/login') {
    setupIdleLogout()
  }
})
src/utils/auth.ts
@@ -26,8 +26,15 @@
// åˆ é™¤token
export const removeToken = () => {
  //wsCache.delete(AccessTokenKey)
 // wsCache.delete(RefreshTokenKey)
  // ã€ä¿®å¤ã€‘原实现把下面两行 wsCache.delete æ³¨é‡ŠæŽ‰äº†ï¼Œä½¿æœ¬å‡½æ•°æˆä¸º**空操作**。
  // è€Œä»¥ä¸‹ä¸‰å¤„都依赖它清理凭证,空操作会引发连锁问题:
  //   1) store/modules/user.ts çš„ loginOut():退出登录后 token ä»ç•™åœ¨ localStorage,
  //      ä¸‹æ¬¡æ‰“开页面又拿旧 token åŽ»é‰´æƒ â†’ 401 â†’ æç¤º"登录超时,请重新登录!";
  //   2) config/axios/service.ts çš„ handleAuthorized():401 æ—¶æ¸…不掉失效凭证,
  //      åˆ·æ–°é¡µé¢ä¼šé‡å¤è§¦å‘同样的失败流程;
  //   3) permission.ts è·¯ç”±å®ˆå«çš„异常兜底:清理无效。
  wsCache.delete(AccessTokenKey)
  wsCache.delete(RefreshTokenKey)
}
/** æ ¼å¼åŒ–token(jwt格式) */
src/utils/dict.ts
@@ -149,6 +149,10 @@
  ECG_DEV_BRAND = 'ecg_dev_brand',
  ECG_DEV_CATEGORY = 'ecg_dev_category',
  ECG_PAT_SOURCE = 'ecg_patient_source',
  ecg_tolerance = 'ecg_tolerance',
  ecg_status = 'ecg_status',
  // å¤šè€ï¼ˆå¤šé‡è€è¯ï¼‰ï¼š0=无 1=多耐 2=多重耐药(见 _prep/09_fix_tolerance_dict.sql)
  // æ³¨ï¼šåŽŸå…ˆå‘½åä¸ºå°å†™ ecg_tolerance,与同组 ECG_* é£Žæ ¼ä¸ä¸€è‡´ï¼Œæ­¤å¤„统一为 ECG_TOLERANCE。
  ECG_TOLERANCE = 'ecg_tolerance',
  ECG_STATUS = 'ecg_status',
  // æŽ’队序号规则:0=预约号 1=签到序号 2=时段号(check_type.seq_rule,见 _prep/22_*.sql)
  ECG_SEQ_RULE = 'ecg_seq_rule',
}
src/utils/patientInfo.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,168 @@
import dayjs from 'dayjs'
import { calculateAge } from '@/utils/dateUtil'
/**
 * æ‚£è€…信息展示的公共格式化工具。
 *
 * èƒŒæ™¯ï¼ˆéœ€æ±‚1):患者信息需在**每个界面**展示 18 é¡¹ï¼š
 *   æµæ°´å·ã€æŽ’队序号、姓名、性别、出生日期、年龄、项目、病区、病床号、
 *   ç™»è®°æ—¶é—´ã€çŠ¶æ€ã€æ‚£è€…ç”µè¯ã€å¼€å•æ—¥æœŸã€è·ä»Šå¤©æ•°ã€æ¥æºï¼ˆé—¨æ€¥ä½ä½“æ£€ï¼‰ã€
 *   é—¨è¯Šå·ã€åŒ»å˜±å·ã€å¼€å•医师
 *
 * å…¶ä¸­ **年龄** ä¸Ž **距今天数** æ•°æ®åº“无对应列,必须实时计算。
 * åŽŸå…ˆè¯¥ç®—æ³•å†…è”åœ¨ RoutinePanel.vue ä¸­ï¼Œå¤šä¸ªç•Œé¢è‹¥å„自复制会造成口径不一致
 * ï¼ˆä¾‹å¦‚一个按 doctorTime ç®—、另一个按 createTime ç®—),故抽到此处统一。
 *
 * æ³¨æ„ï¼š
 *   Â· åŽç«¯è¿”回的生日/日期字段多为字符串("2026-09-29" / "2026-09-29 10:00:00"),
 *     æ•…这里统一用 dayjs å…¼å®¹è§£æžï¼Œé¿å… Date æž„造在不同浏览器下的差异。
 *   Â· æ‹¿ä¸åˆ°å€¼æ—¶ç»Ÿä¸€è¿”回 ''(而不是 "NaN岁" / "NaN天")。
 */
/**
 * å¹´é¾„:由生日实时计算,返回形如 "65岁";无生日/生日非法时返回 ''
 *
 * æ³¨æ„ï¼šåº“中 `pat_birthday` å¯èƒ½æ˜¯ `LocalDate`("2000-01-01")或
 * æ—¶é—´æˆ³/ISO å­—符串;也可能存在脏值(如 "0000-00-00")。
 * dayjs å¯¹è„å€¼è¿”回 Invalid,`diff` ä¼šå¾—到 NaN,故这里先显式校验。
 */
export function formatAge(birthday?: string | number | Date | null): string {
  if (birthday === undefined || birthday === null || birthday === '') return ''
  const d = dayjs(birthday as any)
  if (!d.isValid()) return ''
  const age = calculateAge(birthday as unknown as Date)
  if (!Number.isFinite(age) || age < 0 || age > 150) return ''
  return `${age}岁`
}
/**
 * è·ä»Šå¤©æ•°ï¼šæŒ‰**开单日期**(doctorTime)计算,返回形如 "3天";无值时返回 ''
 *
 * è¯´æ˜Žï¼šéœ€æ±‚1 çš„"距今天数"是给医生判断"这张单子压了多久",
 *       å› æ­¤åŸºå‡†å¿…须是**开单日期**,而不是创建时间/登记时间。
 */
export function formatDaysFromOrder(orderTime?: string | number | Date | null): string {
  if (orderTime === undefined || orderTime === null || orderTime === '') return ''
  const d = dayjs(orderTime)
  if (!d.isValid()) return ''
  const days = dayjs().startOf('day').diff(d.startOf('day'), 'day')
  if (!Number.isFinite(days)) return ''
  return `${days}天`
}
/**
 * è·ä»Šå¤©æ•°ï¼ˆæ•°å€¼ç‰ˆï¼Œä¾¿äºŽæŽ’序/条件样式)
 */
export function daysFromOrder(orderTime?: string | number | Date | null): number | null {
  if (orderTime === undefined || orderTime === null || orderTime === '') return null
  const d = dayjs(orderTime)
  if (!d.isValid()) return null
  return dayjs().startOf('day').diff(d.startOf('day'), 'day')
}
/**
 * æ—¥æœŸæ—¶é—´æ ¼å¼åŒ–(空值安全),默认 "YYYY-MM-DD HH:mm:ss"
 */
export function formatDateTime(
  value?: string | number | Date | null,
  format = 'YYYY-MM-DD HH:mm:ss'
): string {
  if (value === undefined || value === null || value === '') return ''
  const d = dayjs(value)
  return d.isValid() ? d.format(format) : ''
}
/**
 * æ—¥æœŸæ ¼å¼åŒ–(空值安全),默认 "YYYY-MM-DD"
 */
export function formatDateOnly(
  value?: string | number | Date | null,
  format = 'YYYY-MM-DD'
): string {
  return formatDateTime(value, format)
}
/**
 * ç©ºå€¼å®‰å…¨å±•示:null/undefined/'' ç»Ÿä¸€æ˜¾ç¤ºä¸º "-"
 */
export function displayOr(value?: string | number | null, placeholder = '-'): string {
  if (value === undefined || value === null || value === '') return placeholder
  return String(value)
}
/** å°±è¯Šå¡å·çš„解析结果:值 + å®žé™…来源标注 */
export interface CardNoResult {
  /** å±•示用值(无任何候选时为 '-') */
  value: string
  /** å€¼çš„真实来源;有真卡号时为空串(无需标注) */
  from: '' | '门诊号' | '身份证' | '住院号'
}
/**
 * åˆ¤æ–­ä¸€ä¸ªå€¼æ˜¯å¦"像身份证号"(18 ä½æ•°å­—末位可为 X,或 15 ä½çº¯æ•°å­—)。
 *
 * ä¸ºä»€ä¹ˆéœ€è¦æŒ‰**值的形态**判断,而不是按字段名:
 *   å®žæµ‹ `appointment.outpatient_no` æ··åˆå­˜æ”¾ä¸¤ç±»å†…容 â€”—
 *     18 ä½èº«ä»½è¯æ ¼å¼ 103604 è¡Œï¼ˆ65.2%)
 *     12 ä½çœŸé—¨è¯Šå·    54204 è¡Œï¼ˆ34.1%,形如 882609282283)
 *   è‹¥ä¸€å¾‹æŒ‰å­—段名标注为「门诊号」,那 65% çš„身份证号依然被错标。
 */
export function looksLikeIdCard(v?: string | null): boolean {
  if (!v) return false
  const s = String(v).trim()
  return /^[0-9]{17}[0-9Xx]$/.test(s) || /^[0-9]{15}$/.test(s)
}
/**
 * è§£æžã€Œé—¨è¯Šå·ã€åˆ—的展示值与来源。
 *
 * ã€èƒŒæ™¯ã€‘实测 HIS åœ¨ S0201ECG æŠ¥æ–‡çš„ `patientPerson.id.item[1]`
 * ï¼ˆroot `2.16.156.10011.1.15`,官方文档注释即「医保卡号」)
 * **恒为空**(222/222 æ¡ï¼‰ï¼Œå› æ­¤ `medical_card` å…¨åº“为 NULL â€”— è¯¦è§ã€Šéƒ¨ç½²æ¸…单》5.1。
 *
 * ã€åˆ—名变更(经确认)】由于该列**实际展示的是 `outpatient_no`**
 * ï¼ˆå®žæµ‹ 159,116 è¡Œï¼š65% å½¢å¦‚身份证、35% ä¸ºé—¨è¯Šå·ï¼›`medical_card` æ— ä¸€éžç©ºï¼‰ï¼Œ
 * é¡µé¢æ ‡ç­¾å·²ç”±ã€Œå°±è¯Šå¡å·ã€æ”¹ä¸ºã€Œ**门诊号**」,避免医护人员误读。
 *
 * å›žé€€ä¼˜å…ˆçº§ä¸Žæ ‡æ³¨è§„则:
 *   1. `medicalCard`               â†’ æ— æ ‡æ³¨ï¼ˆHIS è¡¥é½å¡å·åŽä¼šèµ°æ­¤åˆ†æ”¯ï¼Œæ ‡æ³¨è‡ªåŠ¨æ¶ˆå¤±ï¼‰
 *   2. `outpatientNo` éžèº«ä»½è¯å½¢æ€  â†’ æ ‡æ³¨ã€Œé—¨è¯Šå·ã€
 *   3. `outpatientNo` æ˜¯èº«ä»½è¯å½¢æ€  â†’ æ ‡æ³¨ã€Œèº«ä»½è¯ã€
 *   4. `patIdentityId`             â†’ æ ‡æ³¨ã€Œèº«ä»½è¯ã€
 *   5. `hospitalNo`(住院号)       â†’ æ ‡æ³¨ã€Œä½é™¢å·ã€
 *   6. éƒ½æ²¡æœ‰                      â†’ '-'
 *
 * > æ ‡æ³¨çš„作用:`outpatient_no` æ˜¯**混合列**,里面既有真门诊号也有身份证号,
 * > æ•…按**值形态**(而非字段名)标注,便于医护人员区分。
 *
 * @param row æ‚£è€…/预约/队列/租赁数据对象(兼容平铺字段与 patDetails)
 */
export function resolveCardNo(row: any): CardNoResult {
  if (row === undefined || row === null) return { value: '-', from: '' }
  const pd = row.patDetails || {}
  const medical = firstNonEmpty(row.medicalCard, pd.medicalCard)
  if (medical !== undefined) return { value: medical, from: '' }
  const outpatient = firstNonEmpty(row.outpatientNo, pd.outpatientNo)
  if (outpatient !== undefined) {
    // è¯¥åˆ—实测混合了身份证号,按形态标注而非按字段名
    return { value: outpatient, from: looksLikeIdCard(outpatient) ? '身份证' : '门诊号' }
  }
  const identity = firstNonEmpty(row.patIdentityId, pd.patIdentityId)
  if (identity !== undefined) return { value: identity, from: '身份证' }
  const hospital = firstNonEmpty(row.hospitalNo, pd.hospitalNo)
  if (hospital !== undefined) return { value: hospital, from: '住院号' }
  return { value: '-', from: '' }
}
/** å–第一个非空(null/undefined/'' å‡è§†ä¸ºç©ºï¼‰å€¼ï¼Œå…¨ç©ºæ—¶è¿”回 undefined */
export function firstNonEmpty(...vals: any[]): string | undefined {
  for (const v of vals) {
    if (v !== undefined && v !== null && v !== '') return String(v)
  }
  return undefined
}
src/utils/stringUtil.ts
@@ -3,10 +3,23 @@
  return value == null || value.trim() === '';
}
export function genderFormat(value: number): string {
  if ( 1===value ) {
    return "男";
  } else {
    return "女";
  }
/**
 * æ€§åˆ«æ ¼å¼åŒ–(1 ç”· / 2 å¥³ï¼‰ã€‚
 *
 * ã€ä¿®æ­£ã€‘原实现是 `if (1===value) "男" else "女"` â€”— ä¼šæŠŠ**任何非 1 çš„值**都当成"女",
 * åŒ…括 0(未知)与 null/undefined。实测库中 `pat_gender = 0` æœ‰ 2,123 è¡Œ
 * ï¼ˆè§ AppointmentServiceImpl çš„ UNKNOWN_GENDER å¸¸é‡è¯­ä¹‰ï¼‰ï¼Œè¿™äº›æ‚£è€…会被**错显为女性**。
 *
 * å¦å¤–注意:MySQL çš„ `tinyint(1)` ä¼šè¢« JDBC é©±åŠ¨æ˜ å°„ä¸º Boolean,
 * æ•…这里同时兼容 boolean / å­—符串数字("1"、"2")等形态。
 */
export function genderFormat(value: number | string | boolean | null | undefined): string {
  if (value === null || value === undefined || value === '') return '未知'
  if (value === true) return '男'
  if (value === false) return '未知'
  const n = Number(value)
  if (n === 1) return '男'
  if (n === 2) return '女'
  // 0 = æœªçŸ¥ï¼ˆHIS æœªæä¾›ï¼‰ï¼Œä¸è¦é»˜è®¤æˆ"女"
  return '未知'
}
src/views/ecg/appointment/AppointmentConfirm.vue
@@ -52,17 +52,9 @@
      </el-form-item>
      <el-form-item label="多耐药" prop="tolerance">
        <el-tag type="info" v-if="formData.tolerance == 0">否</el-tag>
        <el-tag type="danger" v-else>是</el-tag>
        <!-- <el-radio-group v-model="formData.tolerance" readonly>
          <el-radio
            v-for="dict in getIntDictOptions(DICT_TYPE.ecg_tolerance)"
            :key="dict.value"
            :value="dict.value"
          >
            {{ dict.label }}
          </el-radio>
        </el-radio-group> -->
        <!-- éœ€æ±‚6:统一走字典 ecg_tolerance(0=无 1=多耐 2=多重耐药);NULL æŒ‰"无"显示(Q12)。
             åŽŸå…ˆç¡¬ç¼–ç  v-if==0→否 / v-else→是,文案与字典不一致。 -->
        <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="formData.tolerance ?? 0" />
      </el-form-item>
    </div>
    <div class="form-row">
src/views/ecg/appointment/alter.vue
@@ -220,6 +220,56 @@
      />
      <el-table-column label="开单医生" align="center" prop="doctor" width="110px" />
      <el-table-column label="手机" align="center" prop="patMobile" />
      <!-- éœ€æ±‚1:补齐患者信息 18 é¡¹ä¸­ç¼ºå¤±çš„(年龄/性别/来源/登记时间/开单日期/距今天数/状态/门诊号/多耐) -->
      <el-table-column label="性别" align="center" width="70px">
        <template #default="scope">
          <!-- 1男/2女;0 æˆ–缺失(HIS æœªæä¾›ï¼Œå®žæµ‹åº“中有 2123 è¡Œ=0)显示"未知" -->
          <dict-tag v-if="scope.row.patGender === 1 || scope.row.patGender === 2"
                    :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.patGender" />
          <span v-else>未知</span>
        </template>
      </el-table-column>
      <!-- éœ€æ±‚1:医嘱号(HIS observationRequest.id.item[root=2.16.156.10011.1.28])。
           è¯¥èŠ‚ç‚¹åœ¨è§„èŒƒä¸­éžå¿…é¡»é¡¹ï¼ŒåŽ†å²æ•°æ®ï¼ˆæ–°å¢žåˆ—ä¹‹å‰é‡‡é›†çš„ï¼‰ä¸ºç©ºï¼Œæ˜¾ç¤º '-'。 -->
      <el-table-column label="医嘱号" align="center" prop="doctorAdviceNo" min-width="110px">
        <template #default="scope">
          {{ scope.row.doctorAdviceNo || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="年龄" align="center" width="80px">
        <template #default="scope">{{ formatAge(scope.row.patBirthday) || '-' }}</template>
      </el-table-column>
      <el-table-column label="来源" align="center" width="90px">
        <template #default="scope">
          <dict-tag v-if="scope.row.patSrc !== undefined && scope.row.patSrc !== null"
                    :type="DICT_TYPE.ECG_PAT_SOURCE" :value="scope.row.patSrc" />
          <span v-else>-</span>
        </template>
      </el-table-column>
      <!-- æœ¬åˆ—标签由「就诊卡号」改为「门诊号」(经确认):medical_card å…¨åº“为空,
           è¯¥åˆ—实际显示 outpatient_no;resolveCardNo ç»™å‡º**来源标注**(详见《部署清单》5.1)。 -->
      <el-table-column label="门诊号" align="center" min-width="140px">
        <template #default="scope">
          <span>{{ resolveCardNo(scope.row).value }}</span>
          <el-tag v-if="resolveCardNo(scope.row).from" size="small" type="info" effect="plain" class="ml-1">
            {{ resolveCardNo(scope.row).from }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="多耐" align="center" width="110px">
        <template #default="scope">
          <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="scope.row.tolerance ?? 0" />
        </template>
      </el-table-column>
      <el-table-column label="登记时间" align="center" min-width="150px">
        <template #default="scope">{{ formatDateTime(scope.row.registerDate) || '-' }}</template>
      </el-table-column>
      <el-table-column label="开单日期" align="center" min-width="150px">
        <template #default="scope">{{ formatDateTime(scope.row.doctorTime) || '-' }}</template>
      </el-table-column>
      <el-table-column label="距今天数" align="center" width="90px">
        <template #default="scope">{{ formatDaysFromOrder(scope.row.doctorTime) || '-' }}</template>
      </el-table-column>
      <!--      <el-table-column label="电话" align="center" prop="patPhone" />-->
      <!--      <el-table-column label="身份证号" align="center" prop="patIdentityId" />-->
      <!-- <el-table-column label="科室代码" align="center" prop="patDeptCode" /> -->
@@ -245,8 +295,8 @@
            v-hasPermi="['ecg:appointment:update']"
          >
            <dict-tag
              v-if="scope.row.status"
              :type="DICT_TYPE.ecg_status"
              v-if="scope.row.status !== null && scope.row.status !== undefined && scope.row.status !== ''"
              :type="DICT_TYPE.ECG_STATUS"
              :value="scope.row.status"
            />
            <div v-else>状态编辑</div>
@@ -310,6 +360,8 @@
import AppointmentForm from './AppointmentForm.vue'
import { useCheckTypeStore } from '@/store/modules/checkType'
import { getCheckTypeName } from '../../../utils/checkTypeFormatter'
// éœ€æ±‚1:年龄 / è·ä»Šå¤©æ•° / æ—¥æœŸæ ¼å¼åŒ–(库中无对应列,需实时计算)
import { formatAge, formatDaysFromOrder, formatDateTime, resolveCardNo } from '@/utils/patientInfo'
import { status } from 'nprogress'
import { log } from 'node:console'
src/views/ecg/appointment/index.vue
@@ -138,6 +138,27 @@
      <el-table-column label="就诊流水号" align="center" prop="episodeId" min-width="110px"/>
      <el-table-column label="申请号" align="center" prop="applyNo" min-width="110px"/>
      <el-table-column label="编号" align="center" prop="patId" />
      <!-- æœ¬åˆ—标签由「就诊卡号」改为「门诊号」(经确认)。原因(详见《部署清单》5.1):
           HIS åœ¨å¡å·æ§½ä½ä¼ ç©ºï¼Œmedical_card å…¨åº“ 159,116 è¡Œ**无一非空**;
           å®žæµ‹è¯¥åˆ— 99.9% æ˜¾ç¤ºçš„æ˜¯ outpatient_no(65% å½¢å¦‚身份证、35% ä¸ºé—¨è¯Šå·ï¼‰ã€‚
           è‹¥ç»§ç»­å«ã€Œå°±è¯Šå¡å·ã€ä¼šè®©åŒ»æŠ¤äººå‘˜è¯¯è¯»ï¼Œæ•…按实际含义命名。
           resolveCardNo ä»æŒ‰å€¼å½¢æ€ç»™å‡º**来源标注**(身份证/住院号/门诊号),
           ä¾¿äºŽåŒºåˆ†æ··åœ¨ outpatient_no é‡Œçš„身份证号。 -->
      <el-table-column label="门诊号" align="center" min-width="140px">
        <template #default="scope">
          <span>{{ resolveCardNo(scope.row).value }}</span>
          <el-tag v-if="resolveCardNo(scope.row).from" size="small" type="info" effect="plain" class="ml-1">
            {{ resolveCardNo(scope.row).from }}
          </el-tag>
        </template>
      </el-table-column>
      <!-- éœ€æ±‚1:医嘱号(HIS observationRequest.id.item[root=2.16.156.10011.1.28])。
           è¯¥èŠ‚ç‚¹åœ¨è§„èŒƒä¸­éžå¿…é¡»é¡¹ï¼ŒåŽ†å²æ•°æ®ï¼ˆæ–°å¢žåˆ—ä¹‹å‰é‡‡é›†çš„ï¼‰ä¸ºç©ºï¼Œæ˜¾ç¤º '-'。 -->
      <el-table-column label="医嘱号" align="center" prop="doctorAdviceNo" min-width="110px">
        <template #default="scope">
          {{ scope.row.doctorAdviceNo || '-' }}
        </template>
      </el-table-column>
<!--
      <el-table-column label="ç¼´è´¹" align="center" prop="paid">
        <template #default="scope">
@@ -150,6 +171,34 @@
          {{scope.row.patName}} <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.patGender" />
          <dict-tag :type="DICT_TYPE.ECG_PAT_SOURCE" :value="scope.row.patSrc" />
        </template>
      </el-table-column>
      <!-- éœ€æ±‚1:状态 -->
      <!-- æ³¨æ„ï¼šå¿…须显式判空,不能用 v-if="scope.row.status" â€”—
           çŠ¶æ€ '0'(未登记)是合法值,真值判断会把它误判为"无数据"而显示 '-' -->
      <el-table-column label="状态" align="center" prop="status" width="110px">
        <template #default="scope">
          <dict-tag
            v-if="scope.row.status !== null && scope.row.status !== undefined && scope.row.status !== ''"
            :type="DICT_TYPE.ECG_STATUS"
            :value="scope.row.status"
          />
          <span v-else>-</span>
        </template>
      </el-table-column>
      <!-- éœ€æ±‚1:年龄(库中无此列,由生日实时计算) -->
      <el-table-column label="年龄" align="center" width="80px">
        <template #default="scope">{{ formatAge(scope.row.patBirthday) }}</template>
      </el-table-column>
      <!-- éœ€æ±‚1:开单日期 + è·ä»Šå¤©æ•° -->
      <el-table-column label="开单日期" align="center" min-width="150px">
        <template #default="scope">{{ formatDateTime(scope.row.doctorTime) }}</template>
      </el-table-column>
      <el-table-column label="距今天数" align="center" width="90px">
        <template #default="scope">{{ formatDaysFromOrder(scope.row.doctorTime) }}</template>
      </el-table-column>
      <!-- éœ€æ±‚1:开单医师 -->
      <el-table-column label="开单医师" align="center" prop="doctor" min-width="100px">
        <template #default="scope">{{ scope.row.doctor || '-' }}</template>
      </el-table-column>
      <el-table-column
          label="预约日期"
@@ -216,6 +265,8 @@
<script setup lang="ts">
import {DICT_TYPE} from '@/utils/dict'
import { dateFormatter2 } from '@/utils/formatTime'
// éœ€æ±‚1:年龄 / è·ä»Šå¤©æ•° / å¼€å•日期格式化(库中无对应列,需实时计算)
import { formatAge, formatDaysFromOrder, formatDateTime, resolveCardNo } from '@/utils/patientInfo'
import download from '@/utils/download'
import { AppointmentApi, AppointmentVO } from '@/api/ecg/appointment'
import AppointmentForm from './AppointmentForm.vue'
src/views/ecg/checktype/CheckTypeForm.vue
@@ -52,8 +52,20 @@
      <el-form-item label="预约注意事项" prop="notes">
        <el-input v-model="formData.notes" placeholder="请输入预约注意事项" />
      </el-form-item>
      <el-form-item label="序号前缀" prop="notes">
      <el-form-item label="序号前缀" prop="seqPrefix">
        <el-input v-model="formData.seqPrefix" placeholder="请输入序号前缀" />
      </el-form-item>
      <!-- éœ€æ±‚:排队序号规则(签到时如何给患者排队序号) -->
      <el-form-item label="排队序号规则" prop="seqRule">
        <el-select v-model="formData.seqRule" placeholder="请选择排队序号规则" class="w-full">
          <el-option v-for="dict in getIntDictOptions(DICT_TYPE.ECG_SEQ_RULE)"
                     :key="dict.value" :label="dict.label" :value="dict.value" />
        </el-select>
        <div class="text-12px text-gray-500 leading-5 mt-1">
          é¢„约号:用预约系统下发的号,签到不再分配;<br />
          ç­¾åˆ°åºå·ï¼šå½“天同检查类型内连续递增(第 1 ä¸ªç­¾åˆ°=1 å·ï¼‰ï¼Œæ¯ 9 ä¸ªå·é¢„ç•™ 1 ä¸ªç»™åŠ æ€¥ï¼›<br />
          æ—¶æ®µå·ï¼šæŒ‰é¢„约时间段给号(<b>不选时默认此项</b>,与改造前行为一致)。
        </div>
      </el-form-item>
      <el-form-item label="亲和的检查类型" prop="affinityCheckTypes">
        <el-checkbox-group v-model="formData.affinityCheckTypes">
@@ -97,7 +109,9 @@
  timeslotReservedNum: undefined,
  affinityCheckTypes: [],
  notes: undefined,
  seqPrefix: undefined
  seqPrefix: undefined,
  // æŽ’队序号规则:留空即按后端默认(时段号)处理,与改造前行为一致
  seqRule: undefined
})
const formRules = reactive({
})
@@ -160,7 +174,8 @@
    timeslotReservedNum: undefined,
    affinityCheckTypes: [],
    notes: undefined,
    seqPrefix: undefined
    seqPrefix: undefined,
    seqRule: undefined
  }
  formRef.value?.resetFields()
}
src/views/ecg/devrent/index.vue
@@ -94,6 +94,73 @@
      <el-table-column label="设备编号" align="center" prop="devId" />
      <el-table-column label="患者编号" align="center" prop="patId" />
      <el-table-column label="患者名称" align="center" prop="patName" />
      <!-- éœ€æ±‚1:门诊号 + åŒ»å˜±å·ï¼ˆæ­¤å‰è¿™ä¸¤ä¸ªé¡µé¢é—漏了该列,本次补齐)
           æ ‡ç­¾ç”±ã€Œå°±è¯Šå¡å·ã€æ”¹ä¸ºã€Œé—¨è¯Šå·ã€ï¼ˆç»ç¡®è®¤ï¼‰ï¼šmedical_card å…¨åº“为空,
           è¯¥åˆ—实际显示 outpatient_no(详见《部署清单》5.1)。 -->
      <el-table-column label="门诊号" align="center" min-width="140px">
        <template #default="scope">
          <span>{{ resolveCardNo(scope.row).value }}</span>
          <el-tag v-if="resolveCardNo(scope.row).from" size="small" type="info" effect="plain" class="ml-1">
            {{ resolveCardNo(scope.row).from }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="医嘱号" align="center" prop="doctorAdviceNo" min-width="110px">
        <template #default="scope">
          {{ scope.row.doctorAdviceNo || '-' }}
        </template>
      </el-table-column>
      <!-- éœ€æ±‚1:补齐患者信息(后端 DevRentRespVO å·²æ‰©å­—段并回填) -->
      <el-table-column label="性别" align="center" width="70px">
        <template #default="scope">
          <!-- 1男/2女;0 æˆ–缺失(HIS æœªæä¾›ï¼Œå®žæµ‹åº“中有 2123 è¡Œ=0)显示"未知" -->
          <dict-tag v-if="scope.row.patGender === 1 || scope.row.patGender === 2"
                    :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.patGender" />
          <span v-else>未知</span>
        </template>
      </el-table-column>
      <el-table-column label="出生日期" align="center" width="110px">
        <template #default="scope">{{ formatDateOnly(scope.row.patBirthday) || '-' }}</template>
      </el-table-column>
      <el-table-column label="年龄" align="center" width="80px">
        <template #default="scope">{{ formatAge(scope.row.patBirthday) || '-' }}</template>
      </el-table-column>
      <el-table-column label="来源" align="center" width="90px">
        <template #default="scope">
          <dict-tag v-if="scope.row.patSrc !== undefined && scope.row.patSrc !== null"
                    :type="DICT_TYPE.ECG_PAT_SOURCE" :value="scope.row.patSrc" />
          <span v-else>-</span>
        </template>
      </el-table-column>
      <el-table-column label="病区" align="center" min-width="110px">
        <template #default="scope">{{ scope.row.patWardDesc || scope.row.patDetails?.wardDesc || '-' }}</template>
      </el-table-column>
      <el-table-column label="病床号" align="center" width="90px">
        <template #default="scope">{{ scope.row.patBedNo || scope.row.patDetails?.bedNo || '-' }}</template>
      </el-table-column>
      <el-table-column label="患者电话" align="center" min-width="120px">
        <template #default="scope">
          {{ scope.row.patMobile || scope.row.patPhone || scope.row.patDetails?.mobile || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="登记时间" align="center" min-width="150px">
        <template #default="scope">{{ formatDateTime(scope.row.registerDate) || '-' }}</template>
      </el-table-column>
      <el-table-column label="开单日期" align="center" min-width="150px">
        <template #default="scope">{{ formatDateTime(scope.row.doctorTime) || '-' }}</template>
      </el-table-column>
      <el-table-column label="距今天数" align="center" width="90px">
        <template #default="scope">{{ formatDaysFromOrder(scope.row.doctorTime) || '-' }}</template>
      </el-table-column>
      <el-table-column label="开单医师" align="center" min-width="100px">
        <template #default="scope">{{ scope.row.doctor || '-' }}</template>
      </el-table-column>
      <el-table-column label="多耐" align="center" width="110px">
        <template #default="scope">
          <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="scope.row.tolerance ?? 0" />
        </template>
      </el-table-column>
      <el-table-column label="检查类型" align="center" prop="checkType" width="100px">
        <template #default="scope">
          <span>{{getCheckTypeName(scope.row.checkType)}}</span>
@@ -174,6 +241,9 @@
import DevRentForm from './DevRentForm.vue'
import {tranlateCheckState} from "../../../utils/statusFormatter";
import {getCheckTypeName} from "../../../utils/checkTypeFormatter";
// éœ€æ±‚1:患者信息 18 é¡¹ï¼ˆå­—å…¸ + å¹´é¾„/距今天数等实时计算)
import { DICT_TYPE } from '@/utils/dict'
import { formatAge, formatDaysFromOrder, formatDateTime, formatDateOnly, resolveCardNo } from '@/utils/patientInfo'
/** è£…机拆机 åˆ—表 */
defineOptions({ name: 'DevRent' })
src/views/ecg/doctor/DevDataEntry.vue
@@ -15,8 +15,10 @@
            <el-input v-model="formData.patName" placeholder="请输入患者名称" />
          </el-form-item>
          <el-form-item label="患者描述" prop="patDeptDesc">
            ç§‘室: {{formData?.patDetails?.deptDesc}} ç—…区: {{formData?.patDetails?.wardDesc}} åºŠä½: {{formData?.patDetails?.bedNo}}
            ç§‘室: {{ formData?.patDetails?.deptDesc || '-' }} ç—…区: {{ formData.patWardDesc || formData?.patDetails?.wardDesc || '-' }} åºŠä½: {{ formData.patBedNo || formData?.patDetails?.bedNo || '-' }}
          </el-form-item>
          <!-- éœ€æ±‚1:患者信息 18 é¡¹ï¼ˆåŽç«¯ DevRentRespVO å·²æ‰©å­—段并回填) -->
          <PatientInfoBlock :data="formData" check-type-field="checkType" />
          <el-form-item label="录入时间" prop="entryTime">
            <el-date-picker
                v-model="formData.entryTime"
@@ -151,6 +153,8 @@
import {JobRecordApi, JobRecordVO} from "@/api/ecg/jobrecord";
import {isStringEmpty} from "@/utils/stringUtil";
import {curDayEnd, curDayStart} from "@/utils/dateUtil";
// éœ€æ±‚1:患者信息 18 é¡¹ç»Ÿä¸€å±•示块
import PatientInfoBlock from "@/views/ecg/doctor/components/PatientInfoBlock.vue";
/** è£…机拆机 è¡¨å• */
defineOptions({ name: 'DevDataEntry' })
@@ -236,6 +240,24 @@
    category: '',
    brand: '',
    model: '',
    // éœ€æ±‚1:患者信息 18 é¡¹ï¼ˆPatientInfoBlock ä¾èµ–这些字段,
    // ä¸åˆå§‹åŒ–会导致 Vue åœ¨æ¨¡æ¿ä¸­è¯»å–未定义属性并告警)
    applyNo: undefined,
    episodeId: undefined,
    patGender: undefined,
    patBirthday: undefined,
    registerDate: undefined,
    doctor: undefined,
    doctorTime: undefined,
    patSrc: undefined,
    patMobile: undefined,
    patPhone: undefined,
    medicalCard: undefined,
    outpatientNo: undefined,
    patIdentityId: undefined,
    tolerance: undefined,
    status: undefined,
    seqNum: undefined,
  }
  formRef.value?.resetFields()
}
src/views/ecg/doctor/DevDismantle.vue
@@ -16,8 +16,10 @@
            <el-input v-model="formData.patName" placeholder="请输入患者名称" />
          </el-form-item>
          <el-form-item label="患者描述" prop="patDeptDesc">
            ç§‘室: {{formData?.patDetails?.deptDesc}} ç—…区: {{formData?.patDetails?.wardDesc}} åºŠä½: {{formData?.patDetails?.bedNo}}
            ç§‘室: {{ formData?.patDetails?.deptDesc || '-' }} ç—…区: {{ formData.patWardDesc || formData?.patDetails?.wardDesc || '-' }} åºŠä½: {{ formData.patBedNo || formData?.patDetails?.bedNo || '-' }}
          </el-form-item>
          <!-- éœ€æ±‚1:患者信息 18 é¡¹ï¼ˆåŽç«¯ DevRentRespVO å·²æ‰©å­—段并回填) -->
          <PatientInfoBlock :data="formData" check-type-field="checkType" />
          <el-form-item label="装机时间" prop="rentTime">
            <el-date-picker
                v-model="formData.rentTime"
@@ -152,6 +154,8 @@
import {calculateHours, curDayEnd, curDayStart} from "@/utils/dateUtil";
import {AppointmentApi} from "@/api/ecg/appointment";
import {tranlateDevState} from "../../../utils/statusFormatter";
// éœ€æ±‚1:患者信息 18 é¡¹ç»Ÿä¸€å±•示块
import PatientInfoBlock from "@/views/ecg/doctor/components/PatientInfoBlock.vue";
/** è£…机拆机 è¡¨å• */
defineOptions({ name: 'DevrDismantle' })
@@ -287,6 +291,24 @@
    brand: '',
    model: '',
    devState: undefined,
    // éœ€æ±‚1:患者信息 18 é¡¹ï¼ˆPatientInfoBlock ä¾èµ–这些字段,
    // ä¸åˆå§‹åŒ–会导致 Vue åœ¨æ¨¡æ¿ä¸­è¯»å–未定义属性并告警)
    applyNo: undefined,
    episodeId: undefined,
    patGender: undefined,
    patBirthday: undefined,
    registerDate: undefined,
    doctor: undefined,
    doctorTime: undefined,
    patSrc: undefined,
    patMobile: undefined,
    patPhone: undefined,
    medicalCard: undefined,
    outpatientNo: undefined,
    patIdentityId: undefined,
    tolerance: undefined,
    status: undefined,
    seqNum: undefined,
  }
  formRef.value?.resetFields()
}
src/views/ecg/doctor/DevInstall.vue
@@ -9,7 +9,7 @@
import QueuePanel from "@/views/ecg/doctor/components/QueuePanel.vue";
import {DoctorApi, PatientStatisticVO, RoomBedVO} from '@/api/ecg/doctor';
import {QueueVO} from "@/api/ecg/queue";
import {ElNotification} from "element-plus";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
import {useRoomStore} from "@/store/modules/room";
const roomStore = useRoomStore();
@@ -102,6 +102,95 @@
    state.value = true
  else if (queueVO2.status === 30)
    state.value = false
  // éœ€æ±‚3:同步"停止候诊"开关状态
  await loadStopReady()
}
// ==================== éœ€æ±‚3:停止候诊开关 ====================
// å¼€å¯åŽã€å·²æœ‰å€™è¯Šç—…人继续叫号,但不再新增候诊病人】;关闭后恢复。
// æ³¨ï¼šè£…机工位(opType=2)本身不进"分流队列",该开关对装机工位
//     ä¸»è¦ç”¨äºŽ"停止接收新的待装机患者"的语义统一与状态可见。
const stopReady = ref<boolean>(false)
const stopReadyFlag = ref<boolean>(true)
const loadStopReady = async () => {
  try {
    stopReady.value = await DoctorApi.getStopReady(roomBedVO.value)
  } catch (e) {
    // æŸ¥è¯¢å¤±è´¥ä¸æ‰“断轮询
  } finally {
    stopReadyFlag.value = false
  }
}
const onStopReadyChange = async (val: boolean) => {
  stopReadyFlag.value = true
  try {
    if (val) {
      await DoctorApi.stopReady(roomBedVO.value)
      ElMessage.success('已停止候诊:不再接收新的候诊患者,已有患者仍可正常叫号')
    } else {
      await DoctorApi.resumeReady(roomBedVO.value)
      ElMessage.success('已恢复候诊:开始接收新的候诊患者')
    }
    await initLoad()
  } catch (e) {
    stopReady.value = !val
  } finally {
    stopReadyFlag.value = false
  }
}
// ==================== éœ€æ±‚2:撤销叫号 / æ’¤é”€è£…机完成 ====================
const undoFlag = ref<boolean>(false)
const undoCall = async () => {
  try {
    await ElMessageBox.confirm(
      '确定要撤销本次叫号吗?该患者将回到候诊区,可重新叫号。',
      '撤销叫号',
      { type: 'warning', confirmButtonText: '确定撤销', cancelButtonText: '取消' }
    )
  } catch {
    return
  }
  undoFlag.value = true
  try {
    const msg = await DoctorApi.undoCallPatient(roomBedVO.value)
    ElMessage.success(msg || '已撤销叫号')
    await initLoad()
  } finally {
    undoFlag.value = false
  }
}
const undoFinish = async () => {
  const last = await DoctorApi.lastFinishedPatient(roomBedVO.value)
  if (!last || !last.patId) {
    ElMessage.warning('本工位今日暂无已完成的检查记录')
    return
  }
  try {
    await ElMessageBox.confirm(
      `确定撤销「${last.patName || last.patId}」的完成操作吗?该患者将回到候诊区。`,
      '撤销完成',
      { type: 'warning', confirmButtonText: '确定撤销', cancelButtonText: '取消' }
    )
  } catch {
    return
  }
  undoFlag.value = true
  try {
    const msg = await DoctorApi.undoFinishPatient({
      ...roomBedVO.value,
      patId: last.patId,
      checkType: last.bookCheckType
    } as any)
    ElMessage.success(msg || '已撤销完成')
    await initLoad()
  } finally {
    undoFlag.value = false
  }
}
const bedControl = async () => {
@@ -202,6 +291,19 @@
      <el-button :disabled="passFlag" type="primary" @click="passNextPatient">过号</el-button>
      <el-button  type="primary" @click="callAgain">重叫</el-button>
      <el-button :disabled="bedControlFlag" type="primary" @click="bedControl">{{state?'暂停':'恢复'}}</el-button>
      <!-- éœ€æ±‚3:停止候诊开关(已有候诊病人继续叫号,但不再新增候诊病人) -->
      <el-switch
        v-model="stopReady"
        :disabled="stopReadyFlag"
        active-text="停止候诊"
        inactive-text="正常接诊"
        inline-prompt
        style="margin-left: 12px; --el-switch-on-color: #e6a23c"
        @change="onStopReadyChange"
      />
      <!-- éœ€æ±‚2:撤销误点叫号 / æ’¤é”€å®Œæˆï¼ˆå‡å›žåˆ°å€™è¯ŠåŒºï¼‰ -->
      <el-button :disabled="undoFlag" type="warning" plain @click="undoCall">撤销叫号</el-button>
      <el-button :disabled="undoFlag" type="warning" plain @click="undoFinish">撤销完成</el-button>
    </el-container>
  </el-container>
</template>
src/views/ecg/doctor/Routine.vue
@@ -9,7 +9,7 @@
import QueuePanel from '@/views/ecg/doctor/components/QueuePanel.vue'
import { DoctorApi, PatientStatisticVO, RoomBedVO, } from '@/api/ecg/doctor'
import { QueueVO } from '@/api/ecg/queue'
import { ElNotification } from 'element-plus'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import RoutinePanel from '@/views/ecg/doctor/components/RoutinePanel.vue'
import { useRoomStore } from '@/store/modules/room'
@@ -59,6 +59,96 @@
const bedControlFlag = ref<boolean>(true)
const finishFlag = ref<boolean>(true)
const passFlag = ref<boolean>(true)
// ==================== éœ€æ±‚3:停止候诊开关 ====================
// è¯­ä¹‰ï¼šå¼€å¯åŽã€å·²æœ‰å€™è¯Šç—…人继续叫号,但不再新增候诊病人】;关闭后恢复。
// ä¸Žä¸Šé¢çš„"暂停/恢复"是**两个独立概念**:暂停会把就诊中患者强制完成、
// å¹¶æŠŠå€™è¯Šæ‚£è€…踢出工位;本开关只拦截"新增",不动已有患者。
const stopReady = ref<boolean>(false)
const stopReadyFlag = ref<boolean>(true) // é˜²è¿žç‚¹
const loadStopReady = async () => {
  try {
    stopReady.value = await DoctorApi.getStopReady(roomBedVO)
  } catch (e) {
    // æŸ¥è¯¢å¤±è´¥ä¸æ‰“断轮询;保持上一次状态
  } finally {
    stopReadyFlag.value = false
  }
}
const onStopReadyChange = async (val: boolean) => {
  stopReadyFlag.value = true
  try {
    if (val) {
      await DoctorApi.stopReady(roomBedVO)
      ElMessage.success('已停止候诊:不再接收新的候诊患者,已有患者仍可正常叫号')
    } else {
      await DoctorApi.resumeReady(roomBedVO)
      ElMessage.success('已恢复候诊:开始接收新的候诊患者')
    }
    await initLoad()
  } catch (e) {
    // å¤±è´¥å›žæ»šå¼€å…³æ˜¾ç¤º
    stopReady.value = !val
  } finally {
    stopReadyFlag.value = false
  }
}
// ==================== éœ€æ±‚2:撤销叫号 / æ’¤é”€æ£€æŸ¥å®Œæˆ ====================
const undoFlag = ref<boolean>(false)
/** æ’¤é”€è¯¯ç‚¹"叫号":把当前就诊中的患者退回候诊区 */
const undoCall = async () => {
  try {
    await ElMessageBox.confirm(
      '确定要撤销本次叫号吗?该患者将回到候诊区,可重新叫号。',
      '撤销叫号',
      { type: 'warning', confirmButtonText: '确定撤销', cancelButtonText: '取消' }
    )
  } catch {
    return // ç”¨æˆ·å–消
  }
  undoFlag.value = true
  try {
    const msg = await DoctorApi.undoCallPatient(roomBedVO)
    ElMessage.success(msg || '已撤销叫号')
    await initLoad()
  } finally {
    undoFlag.value = false
  }
}
/** æ’¤é”€"检查完成":把最近一位已完成患者退回候诊区 */
const undoFinish = async () => {
  const last = await DoctorApi.lastFinishedPatient(roomBedVO)
  if (!last || !last.patId) {
    ElMessage.warning('本工位今日暂无已完成的检查记录')
    return
  }
  try {
    await ElMessageBox.confirm(
      `确定撤销「${last.patName || last.patId}」的检查完成吗?该患者将回到候诊区。`,
      '撤销检查完成',
      { type: 'warning', confirmButtonText: '确定撤销', cancelButtonText: '取消' }
    )
  } catch {
    return // ç”¨æˆ·å–消
  }
  undoFlag.value = true
  try {
    const msg = await DoctorApi.undoFinishPatient({
      ...roomBedVO,
      patId: last.patId,
      checkType: last.bookCheckType
    } as any)
    ElMessage.success(msg || '已撤销检查完成')
    await initLoad()
  } finally {
    undoFlag.value = false
  }
}
const finishNextPatient = async () => {
  finishFlag.value = true
@@ -116,6 +206,8 @@
  const queueVO2 = await DoctorApi.bedDoctorGet(roomBedVO)
  if (queueVO2.status === 20) state.value = true
  else if (queueVO2.status === 30) state.value = false
  // éœ€æ±‚3:同步"停止候诊"开关状态(其他医生/其他终端改动后本端也能看到)
  await loadStopReady()
}
const bedControl = async () => {
@@ -245,6 +337,19 @@
      <el-button :disabled="bedControlFlag" type="primary" @click="bedControl">{{
        state ? '暂停' : '恢复'
      }}</el-button>
      <!-- éœ€æ±‚3:停止候诊开关(已有候诊病人继续叫号,但不再新增候诊病人) -->
      <el-switch
        v-model="stopReady"
        :disabled="stopReadyFlag"
        active-text="停止候诊"
        inactive-text="正常接诊"
        inline-prompt
        style="margin-left: 12px; --el-switch-on-color: #e6a23c"
        @change="onStopReadyChange"
      />
      <!-- éœ€æ±‚2:撤销误点叫号 / æ’¤é”€æ£€æŸ¥å®Œæˆï¼ˆå‡å›žåˆ°å€™è¯ŠåŒºï¼‰ -->
      <el-button :disabled="undoFlag" type="warning" plain @click="undoCall">撤销叫号</el-button>
      <el-button :disabled="undoFlag" type="warning" plain @click="undoFinish">撤销完成</el-button>
    </el-container>
  </el-container>
</template>
src/views/ecg/doctor/components/DevInstallPanel.vue
@@ -15,9 +15,11 @@
          <el-input v-model="formData.patName" placeholder="请输入患者名称" />
        </el-form-item>
        <el-form-item label="患者描述" prop="patDetails">
          <dict-tag :type="DICT_TYPE.ECG_PAT_SOURCE" :value="formData.patDetails?.source || ''" />
          {{ formData?.patDetails?.deptDesc }} {{ formData?.patDetails?.wardDesc }} {{ formData?.patDetails?.bedNo }}
          <dict-tag :type="DICT_TYPE.ECG_PAT_SOURCE" :value="formData.patSrc ?? formData.patDetails?.source ?? ''" />
          {{ formData.patWardDesc || formData?.patDetails?.wardDesc || '-' }} {{ formData.patBedNo || formData?.patDetails?.bedNo || '-' }}
        </el-form-item>
        <!-- éœ€æ±‚1:患者信息 18 é¡¹ç»Ÿä¸€å±•示(后端已从预约单平铺回填,setPatient å·²èµ‹å€¼åˆ° formData) -->
        <PatientInfoBlock :data="formData" check-type-field="checkType" :span="12" />
        <el-form-item label="装机时间" prop="rentTime">
          <el-date-picker v-model="formData.rentTime" type="date" value-format="x" placeholder="选择装机时间" />
        </el-form-item>
@@ -33,10 +35,8 @@
          <el-input v-model="formData.remark" placeholder="请输入备注" />
        </el-form-item>
        <el-form-item label="多耐药" prop="tolerance">
          <el-tag v-if="formData.tolerance == 0" type="info">否</el-tag>
          <el-tag v-else-if="formData.tolerance == 1" type="danger">是</el-tag>
          <!-- å¯é€‰ï¼šé˜²æ­¢æ•°æ®å¼‚常时的兜底 -->
          <el-tag v-else type="warning">未知</el-tag>
          <!-- éœ€æ±‚6:统一走字典 ecg_tolerance(0=无 1=多耐 2=多重耐药);NULL æŒ‰"无"显示(Q12) -->
          <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="formData.tolerance ?? 0" />
        </el-form-item>
      </el-col>
    </el-row>
@@ -107,6 +107,8 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { tranlateDevState } from "../../../../utils/statusFormatter";
import { getCheckTypeName } from "../../../../utils/checkTypeFormatter";
// éœ€æ±‚1:患者信息 18 é¡¹ç»Ÿä¸€å±•示块
import PatientInfoBlock from "@/views/ecg/doctor/components/PatientInfoBlock.vue";
/** è®¾å¤‡è£…机 è¡¨å• */
defineComponent({ name: 'DevInstallPanel' })
@@ -166,6 +168,23 @@
  formData.value.patName = queueVO?.patName
  formData.value.checkType = queueVO?.bookCheckType
  formData.value.tolerance = queueVO?.tolerance // å¤šè€è¯ï¼ŒåŽç«¯ä»Žé¢„约单补齐
  // éœ€æ±‚1:以下平铺字段由后端从预约单回填到 QueueRespVO,
  // å¿…须显式赋值给 formData,否则模板里读不到。
  formData.value.patGender = queueVO?.patGender
  formData.value.patBirthday = queueVO?.patBirthday
  formData.value.registerDate = queueVO?.registerDate
  formData.value.doctor = queueVO?.doctor
  formData.value.doctorTime = queueVO?.doctorTime
  formData.value.patSrc = queueVO?.patSrc
  formData.value.patMobile = queueVO?.patMobile
  formData.value.patPhone = queueVO?.patPhone
  formData.value.patWardDesc = queueVO?.patWardDesc
  formData.value.patBedNo = queueVO?.patBedNo
  formData.value.medicalCard = queueVO?.medicalCard
  formData.value.outpatientNo = queueVO?.outpatientNo
    // éœ€æ±‚1:医嘱号(此前 3 ä¸ªé¢æ¿éƒ½é—漏了该字段的赋值,导致 PatientInfoBlock æ’显示 '-')
    formData.value.doctorAdviceNo = queueVO?.doctorAdviceNo
  formData.value.patIdentityId = queueVO?.patIdentityId
  getReadyRentInfo(formData.value)    // èŽ·å–é¢†ç”¨ä¿¡æ¯
  copyBarcode()
}
@@ -213,6 +232,25 @@
    detachment: undefined,
    remark: undefined,
    checkType: undefined,
    // éœ€æ±‚1:PatientInfoBlock ä¾èµ–的平铺字段(由 setPatient ä»Ž QueueVO èµ‹å€¼ï¼‰
    applyNo: undefined,
    episodeId: undefined,
    patGender: undefined,
    patBirthday: undefined,
    registerDate: undefined,
    doctor: undefined,
    doctorTime: undefined,
    patSrc: undefined,
    patMobile: undefined,
    patPhone: undefined,
    patWardDesc: undefined,
    patBedNo: undefined,
    medicalCard: undefined,
    doctorAdviceNo: undefined,
    outpatientNo: undefined,
    patIdentityId: undefined,
    status: undefined,
    seqNum: undefined,
  }
  formRef.value?.resetFields()
  devInfo.value = {
src/views/ecg/doctor/components/DevReadyPanel.vue
@@ -29,11 +29,13 @@
        <el-input v-model="formData.patName" placeholder="请输入患者名称" />
      </el-form-item>
     </el-col>
     <!-- éœ€æ±‚1:患者信息 18 é¡¹ç»Ÿä¸€å±•示(后端已从预约单平铺回填,setPatient å·²èµ‹å€¼åˆ° formData) -->
     <PatientInfoBlock :data="formData" check-type-field="checkType" :span="12" />
      <el-col :span="12">
  
      <el-form-item label="患者描述" prop="patDeptDesc">
        <dict-tag v-if="formData.patDetails?.source" :type="DICT_TYPE.ECG_PAT_SOURCE" :value="formData.patDetails?.source" />
        {{formData?.patDetails?.deptDesc}} {{formData?.patDetails?.wardDesc}} {{formData?.patDetails?.bedNo}}
        <dict-tag v-if="formData.patSrc ?? formData.patDetails?.source" :type="DICT_TYPE.ECG_PAT_SOURCE" :value="formData.patSrc ?? formData.patDetails?.source" />
        {{ formData.patWardDesc || formData?.patDetails?.wardDesc || '-' }} {{ formData.patBedNo || formData?.patDetails?.bedNo || '-' }}
      </el-form-item>
           </el-col>
      <el-col :span="12">
@@ -58,8 +60,8 @@
           </el-col>
      <el-col :span="12">
       <el-form-item label="多耐药" prop="tolerance">
         <el-tag type="info" v-if="formData.tolerance == 0">否</el-tag>
        <el-tag type="danger" v-else>是</el-tag>
         <!-- éœ€æ±‚6:统一走字典 ecg_tolerance(0=无 1=多耐 2=多重耐药);NULL æŒ‰"无"显示(Q12) -->
         <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="formData.tolerance ?? 0" />
        </el-form-item>
             </el-col>
      </el-row>
@@ -174,6 +176,8 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import {tranlateDevState} from "@/utils/statusFormatter";
import {getCheckTypeName, needAutoFeeConfirm} from "../../../../utils/checkTypeFormatter";
// éœ€æ±‚1:患者信息 18 é¡¹ç»Ÿä¸€å±•示块
import PatientInfoBlock from "@/views/ecg/doctor/components/PatientInfoBlock.vue";
/** è®¾å¤‡é¢†ç”¨ è¡¨å• */
defineComponent({ name: 'DevReadyPanel' })
@@ -312,6 +316,26 @@
    remark: undefined,
    patDetails: undefined,
    paid: undefined,
    // éœ€æ±‚1:PatientInfoBlock ä¾èµ–的平铺字段,先初始化为 undefined,
    // éšåŽç”± setPatient ä»Ž QueueVO èµ‹å€¼ï¼ˆä¸åˆå§‹åŒ–会读不到属性)
    applyNo: undefined,
    episodeId: undefined,
    patGender: undefined,
    patBirthday: undefined,
    registerDate: undefined,
    doctor: undefined,
    doctorTime: undefined,
    patSrc: undefined,
    patMobile: undefined,
    patPhone: undefined,
    patWardDesc: undefined,
    patBedNo: undefined,
    medicalCard: undefined,
    doctorAdviceNo: undefined,
    outpatientNo: undefined,
    patIdentityId: undefined,
    status: undefined,
    seqNum: undefined,
  }
  formRef.value?.resetFields()
@@ -331,6 +355,23 @@
  formData.value.patName = queueVO?.patName
  formData.value.checkType = queueVO?.bookCheckType
  formData.value.tolerance = queueVO?.tolerance // å¤šè€è¯ï¼ŒåŽç«¯ä»Žé¢„约单补齐
  // éœ€æ±‚1:以下平铺字段由后端从预约单回填到 QueueRespVO,
  // å¿…须显式赋值给 formData,否则模板里读不到(原先只赋值了上面 6 ä¸ªï¼‰ã€‚
  formData.value.patGender = queueVO?.patGender
  formData.value.patBirthday = queueVO?.patBirthday
  formData.value.registerDate = queueVO?.registerDate
  formData.value.doctor = queueVO?.doctor
  formData.value.doctorTime = queueVO?.doctorTime
  formData.value.patSrc = queueVO?.patSrc
  formData.value.patMobile = queueVO?.patMobile
  formData.value.patPhone = queueVO?.patPhone
  formData.value.patWardDesc = queueVO?.patWardDesc
  formData.value.patBedNo = queueVO?.patBedNo
  formData.value.medicalCard = queueVO?.medicalCard
  formData.value.outpatientNo = queueVO?.outpatientNo
    // éœ€æ±‚1:医嘱号(此前 3 ä¸ªé¢æ¿éƒ½é—漏了该字段的赋值,导致 PatientInfoBlock æ’显示 '-')
    formData.value.doctorAdviceNo = queueVO?.doctorAdviceNo
  formData.value.patIdentityId = queueVO?.patIdentityId
  getFreeRentInfo(formData.value)  // å¾…检查的信息  @ dev_rent è¡¨
  copyBarcode()
}
src/views/ecg/doctor/components/PatientInfoBlock.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,210 @@
<!--
  æ‚£è€…信息展示块(需求1:18 é¡¹ç»Ÿä¸€å±•示)
  èƒŒæ™¯ï¼š
    éœ€æ±‚要求「流水号、排队序号、姓名、性别、出生日期、年龄、项目、病区、病床号、
    ç™»è®°æ—¶é—´ã€çŠ¶æ€ã€æ‚£è€…ç”µè¯ã€å¼€å•æ—¥æœŸã€è·ä»Šå¤©æ•°ã€æ¥æºã€é—¨è¯Šå·ã€åŒ»å˜±å·ã€å¼€å•åŒ»å¸ˆã€
    åœ¨**每个界面**都有。
    ï¼ˆåŽŸå§‹éœ€æ±‚å†™çš„æ˜¯ã€Œå°±è¯Šå¡å·ã€ï¼›å›  HIS åœ¨å¡å·æ§½ä½ä¼ ç©ºã€medical_card å…¨åº“为空,
      è¯¥ä½ç½®å®žé™…展示 outpatient_no,经确认标签改为「门诊号」——详见《部署清单》5.1。)
  ä¸ºä»€ä¹ˆåšæˆç»„件:
    æœ‰ 10+ ä¸ªç•Œé¢éœ€è¦åŒä¸€å¥—字段。若各自复制粘贴,会出现
      Â· å£å¾„不一致(某个页面用 createTime å½“开单日期)
      Â· æ”¹ä¸€å¤„漏一处
    å› æ­¤ç»Ÿä¸€åˆ°è¿™é‡Œï¼›å„页面只需传数据对象。
  æ•°æ®å…¼å®¹æ€§ï¼š
    ä¸åŒé¡µé¢çš„æ•°æ®å½¢æ€ç•¥æœ‰å·®å¼‚,本组件按以下优先级取值:
      1) åŽç«¯å¹³é“ºå­—段(QueueRespVO / AppointmentRespVO / DevRentRespVO æ–°å¢žçš„ patWardDesc ç­‰ï¼‰
      2) patDetails JSON å¿«ç…§ï¼ˆæ—§æ•°æ®å¯èƒ½åªåœ¨è¿™é‡Œæœ‰ï¼‰
      3) '-' å ä½
-->
<script setup lang="ts">
import { computed } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import {
  formatAge,
  formatDaysFromOrder,
  formatDateOnly,
  formatDateTime,
  resolveCardNo
} from '@/utils/patientInfo'
import { getCheckTypeName } from '@/utils/checkTypeFormatter'
const props = defineProps<{
  /** æ‚£è€…/排队/预约/租赁 æ•°æ®å¯¹è±¡ï¼ˆå½¢æ€å…¼å®¹ï¼Œè§æ–‡ä»¶å¤´è¯´æ˜Žï¼‰ */
  data: any
  /** æ£€æŸ¥é¡¹ç›®å­—段名:队列用 bookCheckType,租赁用 checkType */
  checkTypeField?: string
  /** çŠ¶æ€å­—å…¸ï¼šé˜Ÿåˆ—ç”¨ ecg_queue_status,预约单用 ecg_status */
  statusDict?: string
  /** æ˜¯å¦æ˜¾ç¤ºæŽ’队序号(预约记录等无此字段的页面可关闭) */
  showSeqNum?: boolean
  /**
   * æ˜¯å¦æ˜¾ç¤ºåŒ»å˜±å·ï¼Œé»˜è®¤ **true**。
   *
   * åŽ†å²è¯´æ˜Žï¼šæ—©æœŸæŽ’æŸ¥å¾—å‡º"HIS æŠ¥æ–‡ä¸­ä¸å­˜åœ¨åŒ»å˜±å·"的结论,故当时默认关闭,
   * ä»¥å…ç”¨ç”³è¯·å·å†’充。**该结论已被推翻** â€”— HIS《交互服务通用接口文档》
   * 6.1/6.4/6.5 æ˜Žç¡® `observationRequest.id.item[root=2.16.156.10011.1.28]`
   * å³ã€ŒåŒ»å˜±ID/医嘱号」,且实测 S0201ECG 220 æ¡ã€S0202ECG 29 æ¡å‡æºå¸¦è¯¥å€¼ã€‚
   * åŽç«¯å·²æ–°å¢ž `doctorAdviceNo` å­—段并在解析时采集,故此处默认显示。
   *
   * æ³¨æ„è¯¥èŠ‚ç‚¹åœ¨è§„èŒƒä¸­**非必须项**,历史数据(新增列之前采集的)为空 â†’
   * æ˜¾ç¤º `-`,属预期。
   */
  showAdviceNo?: boolean
  /** æ¯åˆ—栅格宽度(默认 6 = ä¸€è¡Œ 4 ä¸ªï¼‰ */
  span?: number
}>()
const d = computed(() => props.data || {})
const pd = computed(() => d.value.patDetails || {})
const colSpan = computed(() => props.span ?? 6)
// åŒ»å˜±å·é»˜è®¤æ˜¾ç¤ºï¼ˆè¯¦è§ props è¯´æ˜Žï¼‰ï¼›ä»…在显式传 false æ—¶éšè—
const showAdvice = computed(() => props.showAdviceNo !== false)
const checkField = computed(() => props.checkTypeField ?? 'bookCheckType')
/** å–第一个有值的候选 */
function pick(...vals: any[]): any {
  for (const v of vals) {
    if (v !== undefined && v !== null && v !== '') return v
  }
  return undefined
}
const phone = computed(() =>
  pick(d.value.patMobile, d.value.patPhone, pd.value.mobile) ?? '-'
)
const ward = computed(() => pick(d.value.patWardDesc, pd.value.wardDesc) ?? '-')
// æ³¨æ„ï¼šd.bedNo åœ¨ queue é‡Œæ˜¯ã€è¯Šç–—床/工位号】,真正的病床号是 patBedNo / patDetails.bedNo
const patBed = computed(() => pick(d.value.patBedNo, pd.value.bedNo) ?? '-')
const cardNo = computed(() => resolveCardNo(d.value))
const source = computed(() => pick(d.value.patSrc, pd.value.source))
const checkType = computed(() => d.value[checkField.value])
</script>
<template>
  <el-row :gutter="20">
    <el-col :span="colSpan">
      <el-form-item label="就诊流水号">
        {{ d.episodeId || '-' }}
      </el-form-item>
    </el-col>
    <el-col v-if="showSeqNum" :span="colSpan">
      <el-form-item label="排队序号">
        {{ pick(d.seqNum, d.bookSeqNum) ?? '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="申请编号">
        {{ d.applyNo || '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <!-- æ ‡ç­¾ç”±ã€Œå°±è¯Šå¡å·ã€æ”¹ä¸ºã€Œé—¨è¯Šå·ã€ï¼ˆç»ç¡®è®¤ï¼Œè¯¦è§ã€Šéƒ¨ç½²æ¸…单》5.1):
           HIS åœ¨å¡å·æ§½ä½ä¼ ç©ºï¼Œmedical_card å…¨åº“ 159,116 è¡Œæ— ä¸€éžç©ºï¼›
           è¯¥ä½ç½®å®žé™…展示 outpatient_no。cardNo.from æ ‡æ³¨å€¼çš„真实来源,
           ç”¨äºŽåŒºåˆ†æ··åœ¨è¯¥å­—段里的身份证号 / ä½é™¢å·ã€‚ -->
      <el-form-item label="门诊号">
        <span>{{ cardNo.value }}</span>
        <el-tag v-if="cardNo.from" size="small" type="info" effect="plain" class="ml-1">
          {{ cardNo.from }}
        </el-tag>
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="姓名">
        {{ d.patName || '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="性别">
        <!-- 1 ç”· / 2 å¥³ï¼›0 æˆ–缺失(HIS æœªæä¾›ï¼Œå®žæµ‹åº“中有 2123 è¡Œ = 0)显示"未知"。
             æ³¨æ„ä¸èƒ½ç›´æŽ¥æŠŠ 0 äº¤ç»™ dict-tag â€”— å­—典里没有 0 è¿™ä¸€é¡¹ï¼Œä¼šæ¸²æŸ“成空白。 -->
        <dict-tag v-if="d.patGender === 1 || d.patGender === 2 || d.patGender === '1' || d.patGender === '2'"
                  :type="DICT_TYPE.SYSTEM_USER_SEX" :value="d.patGender" />
        <span v-else>未知</span>
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="出生日期">
        {{ formatDateOnly(d.patBirthday) || '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="年龄">
        {{ formatAge(d.patBirthday) || '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="项目">
        {{ checkType ? getCheckTypeName(checkType) : '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="来源">
        <dict-tag v-if="source !== undefined" :type="DICT_TYPE.ECG_PAT_SOURCE" :value="source" />
        <span v-else>-</span>
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="病区">
        {{ ward }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="病床号">
        {{ patBed }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="患者电话">
        {{ phone }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="登记时间">
        {{ formatDateTime(d.registerDate) || '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="开单日期">
        {{ formatDateTime(d.doctorTime) || '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="距今天数">
        {{ formatDaysFromOrder(d.doctorTime) || '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="开单医师">
        {{ d.doctor || '-' }}
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="状态">
        <dict-tag v-if="d.status !== undefined && d.status !== null && statusDict"
                  :type="statusDict" :value="d.status" />
        <span v-else>-</span>
      </el-form-item>
    </el-col>
    <el-col :span="colSpan">
      <el-form-item label="多耐">
        <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="d.tolerance ?? 0" />
      </el-form-item>
    </el-col>
    <el-col v-if="showAdvice" :span="colSpan">
      <el-form-item label="医嘱号">
        {{ d.doctorAdviceNo || '-' }}
      </el-form-item>
    </el-col>
  </el-row>
</template>
src/views/ecg/doctor/components/RoutinePanel.vue
@@ -24,7 +24,10 @@
      <el-col :span="6">
        <el-form-item label="联系电话" prop="episodeId">
          {{ formData?.patDetails?.mobile }}
          <!-- ä¿®æ­£ï¼šåŽŸè¯» `patDetails.mobile`(queue è¡¨çš„ JSON å¿«ç…§ï¼Œåªå« source/mobile/wardDesc/bedNo å››å­—段)。
               è€Œ setPatient å·²æŠŠåŽç«¯ä»Ž appointment å›žå¡«çš„**平铺字段** `patMobile` èµ‹è¿› formData,
               æ•…应以平铺字段优先,patDetails ä»…作旧数据回退。 -->
          {{ formData.patMobile || formData?.patDetails?.mobile || '-' }}
        </el-form-item>
      </el-col>
    </el-row>
@@ -57,9 +60,14 @@
      </el-col>
      <el-col :span="18">
        <el-form-item label="科室病区" prop="patDetails">
          <dict-tag v-if="formData.patDetails?.source" :type="DICT_TYPE.ECG_PAT_SOURCE"
            :value="formData.patDetails?.source" />
          {{ formData?.patDetails?.deptDesc }}-{{ formData?.patDetails?.wardDesc }}-{{ formData?.patDetails?.bedNo }}
          <!-- ä¿®æ­£ï¼šæ¥æºä¸Žç—…区/床号原读 `patDetails.*`(queue è¡¨çš„ JSON å¿«ç…§ï¼Œå¯èƒ½æ»žåŽä¸”常缺病区),
               æ”¹ä¸º**平铺字段优先**(后端已从 appointment å›žå¡« patSrc/patWardDesc/patBedNo),
               patDetails ä»…作旧数据回退。 -->
          <dict-tag v-if="formData.patSrc ?? formData.patDetails?.source"
                    :type="DICT_TYPE.ECG_PAT_SOURCE"
                    :value="formData.patSrc ?? formData.patDetails?.source" />
          {{ formData.patWardDesc || formData?.patDetails?.wardDesc || '-' }}
          - {{ formData.patBedNo || formData?.patDetails?.bedNo || '-' }}
        </el-form-item></el-col>
@@ -77,7 +85,13 @@
      </el-col>
      <el-col :span="6">
        <el-form-item label="医嘱号">
          {{ formData.applyNo }}
          <!-- ä¿®æ­£ï¼šæ­¤å¤„原先绑定的是 `formData.applyNo`(申请单号),
               ä¸Žã€ŒåŒ»å˜±å·ã€æ˜¯**两个不同的标识**:
                 ç”³è¯·å•号 apply_no           = HIS root 2.16.156.10011.1.24,例 2126091700011
                 åŒ»å˜±å·   doctor_advice_no   = HIS root 2.16.156.10011.1.28,例 112663505
               åŽŸå†™æ³•æ˜¯æŠŠç”³è¯·å•å·å†’å……æˆåŒ»å˜±å·ï¼Œå±žäºŽé”™è¯¯å±•ç¤ºã€‚
               çŽ°æ”¹ä¸ºè¯»åŽç«¯æ–°é‡‡é›†çš„ doctorAdviceNo(该节点规范中非必须项,历史数据为空 â†’ æ˜¾ç¤º '-')。 -->
          {{ formData.doctorAdviceNo || '-' }}
        </el-form-item>
      </el-col><el-col :span="6">
        <el-form-item label="登记时间">
@@ -85,6 +99,21 @@
        </el-form-item>
      </el-col>
    </el-row>
    <!-- éœ€æ±‚1:补齐本面板原先缺失的两项(排队序号、状态),使 18 é¡¹é½å…¨ -->
    <el-row>
      <el-col :span="6">
        <el-form-item label="排队序号">
          {{ formData?.seqNum ?? formData?.bookSeqNum ?? '-' }}
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="状态">
          <dict-tag v-if="formData?.status !== undefined && formData?.status !== null"
                    :type="DICT_TYPE.ECG_QUEUE_STATUS" :value="formData.status" />
          <span v-else>-</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row>
      <el-col :span="6">
@@ -99,9 +128,9 @@
      </el-col>
      <el-col :span="6">
        <el-form-item label="多耐药" prop="tolerance">
          <el-tag v-if="formData.tolerance == 0" type="info">否</el-tag>
          <el-tag v-else-if="formData.tolerance == 1" type="danger">是</el-tag>
          <el-tag v-else type="warning">未知</el-tag>
          <!-- éœ€æ±‚6:统一走字典 ecg_tolerance(0=无 1=多耐 2=多重耐药);
               NULL æŒ‰"无"显示(Q12)。原先硬编码"否/是/未知"三分支,与字典重复且文案不符。 -->
          <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="formData.tolerance ?? 0" />
        </el-form-item>
      </el-col>
      <el-col :span="6">
@@ -212,12 +241,26 @@
  formData.value.checkType = queueVO?.bookCheckType
  // ä»¥ä¸‹å­—段由后端从预约单补齐(queue è¡¨ä¸å­˜ï¼‰ï¼Œç”¨äºŽåŒ»ç”Ÿç«¯å±•示
  formData.value.patGender = queueVO?.patGender
  formData.value.patGender = queueVO?.patGender
  formData.value.tolerance = queueVO?.tolerance
  formData.value.patBirthday = queueVO?.patBirthday
  formData.value.registerDate = queueVO?.registerDate
  formData.value.doctor = queueVO?.doctor
  formData.value.doctorTime = queueVO?.doctorTime
  // éœ€æ±‚1:补齐排队序号 / çŠ¶æ€ / ç—…区 / åºŠå· / ç”µè¯ / æ¥æº / é—¨è¯Šå·
  // è¿™äº›å¹³é“ºå­—段由后端 QueueServiceImpl#fillAppointmentInfo å›žå¡«ï¼Œ
  // åŽŸå…ˆè¿™é‡Œæ²¡æœ‰èµ‹å€¼ï¼Œå¯¼è‡´æ¨¡æ¿é‡Œè¯»ä¸åˆ°ï¼ˆæ˜¾ç¤ºä¸ºç©ºï¼‰ã€‚
  formData.value.seqNum = queueVO?.seqNum
  formData.value.status = queueVO?.status
  formData.value.patSrc = queueVO?.patSrc
  formData.value.patMobile = queueVO?.patMobile
  formData.value.patPhone = queueVO?.patPhone
  formData.value.patWardDesc = queueVO?.patWardDesc
  formData.value.patBedNo = queueVO?.patBedNo
  formData.value.medicalCard = queueVO?.medicalCard
  formData.value.outpatientNo = queueVO?.outpatientNo
    // éœ€æ±‚1:医嘱号(此前 3 ä¸ªé¢æ¿éƒ½é—漏了该字段的赋值,导致 PatientInfoBlock æ’显示 '-')
    formData.value.doctorAdviceNo = queueVO?.doctorAdviceNo
  formData.value.patIdentityId = queueVO?.patIdentityId
  getFreeRentInfo(formData.value)
  copyBarcode()
}
@@ -336,6 +379,7 @@
    remark: undefined,
    patDetails: undefined,
    paid: undefined,
    doctorAdviceNo: undefined,
    roomId: props.room.roomId,
    bedNo: props.room.bedNo
  }
src/views/ecg/doctor/index.vue
@@ -10,7 +10,7 @@
import QueuePanel from "@/views/ecg/doctor/components/QueuePanel.vue";
import {DoctorApi, PatientStatisticVO, RoomBedVO} from '@/api/ecg/doctor';
import {QueueVO} from "@/api/ecg/queue";
import {ElNotification} from "element-plus";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
import DevReadyPanel from "@/views/ecg/doctor/components/DevReadyPanel.vue";
import {useRoomStore} from "@/store/modules/room";
@@ -71,6 +71,91 @@
  }
}
// ==================== éœ€æ±‚3:停止候诊开关 ====================
// å¼€å¯åŽã€å·²æœ‰å€™è¯Šç—…人继续叫号,但不再新增候诊病人】;关闭后恢复。
const stopReady = ref<boolean>(false)
const stopReadyFlag = ref<boolean>(true)
const loadStopReady = async () => {
  try {
    stopReady.value = await DoctorApi.getStopReady(roomBedVO)
  } catch (e) {
    // æŸ¥è¯¢å¤±è´¥ä¸æ‰“断轮询
  } finally {
    stopReadyFlag.value = false
  }
}
const onStopReadyChange = async (val: boolean) => {
  stopReadyFlag.value = true
  try {
    if (val) {
      await DoctorApi.stopReady(roomBedVO)
      ElMessage.success('已停止候诊:不再接收新的候诊患者,已有患者仍可正常叫号')
    } else {
      await DoctorApi.resumeReady(roomBedVO)
      ElMessage.success('已恢复候诊:开始接收新的候诊患者')
    }
    await initLoad()
  } catch (e) {
    stopReady.value = !val
  } finally {
    stopReadyFlag.value = false
  }
}
// ==================== éœ€æ±‚2:撤销叫号 / æ’¤é”€æ£€æŸ¥å®Œæˆ ====================
const undoFlag = ref<boolean>(false)
const undoCall = async () => {
  try {
    await ElMessageBox.confirm(
      '确定要撤销本次叫号吗?该患者将回到候诊区,可重新叫号。',
      '撤销叫号',
      { type: 'warning', confirmButtonText: '确定撤销', cancelButtonText: '取消' }
    )
  } catch {
    return
  }
  undoFlag.value = true
  try {
    const msg = await DoctorApi.undoCallPatient(roomBedVO)
    ElMessage.success(msg || '已撤销叫号')
    await initLoad()
  } finally {
    undoFlag.value = false
  }
}
const undoFinish = async () => {
  const last = await DoctorApi.lastFinishedPatient(roomBedVO)
  if (!last || !last.patId) {
    ElMessage.warning('本工位今日暂无已完成的检查记录')
    return
  }
  try {
    await ElMessageBox.confirm(
      `确定撤销「${last.patName || last.patId}」的检查完成吗?该患者将回到候诊区。`,
      '撤销检查完成',
      { type: 'warning', confirmButtonText: '确定撤销', cancelButtonText: '取消' }
    )
  } catch {
    return
  }
  undoFlag.value = true
  try {
    const msg = await DoctorApi.undoFinishPatient({
      ...roomBedVO,
      patId: last.patId,
      checkType: last.bookCheckType
    } as any)
    ElMessage.success(msg || '已撤销检查完成')
    await initLoad()
  } finally {
    undoFlag.value = false
  }
}
const callAgain = async () => {
  await DoctorApi.callPatientAgain(roomBedVO)
}
@@ -103,6 +188,8 @@
    state.value = true
  else if (queueVO2.status === 30)  // å·¥ä½çŠ¶æ€ æš‚停
    state.value = false
  // éœ€æ±‚3:同步"停止候诊"开关状态
  await loadStopReady()
}
const bedControl = async () => {
@@ -201,6 +288,19 @@
      <el-button :disabled="passFlag" type="primary" @click="passNextPatient">过号</el-button>
      <el-button  type="primary" @click="callAgain">重叫</el-button>
      <el-button :disabled="bedControlFlag" type="primary" @click="bedControl">{{state?'暂停':'恢复'}}</el-button>
      <!-- éœ€æ±‚3:停止候诊开关(已有候诊病人继续叫号,但不再新增候诊病人) -->
      <el-switch
        v-model="stopReady"
        :disabled="stopReadyFlag"
        active-text="停止候诊"
        inactive-text="正常接诊"
        inline-prompt
        style="margin-left: 12px; --el-switch-on-color: #e6a23c"
        @change="onStopReadyChange"
      />
      <!-- éœ€æ±‚2:撤销误点叫号 / æ’¤é”€æ£€æŸ¥å®Œæˆï¼ˆå‡å›žåˆ°å€™è¯ŠåŒºï¼‰ -->
      <el-button :disabled="undoFlag" type="warning" plain @click="undoCall">撤销叫号</el-button>
      <el-button :disabled="undoFlag" type="warning" plain @click="undoFinish">撤销完成</el-button>
    </el-container>
  </el-container>
</template>
src/views/ecg/jobrecord/index.vue
@@ -137,6 +137,68 @@
      <el-table-column label="设备编号" align="center" prop="devId" />
      <el-table-column label="患者编号" align="center" prop="patId" />
      <el-table-column label="患者名称" align="center" prop="patName" />
      <!-- éœ€æ±‚1:门诊号 + åŒ»å˜±å·ï¼ˆæ­¤å‰è¿™ä¸¤ä¸ªé¡µé¢é—漏了该列,本次补齐)
           æ ‡ç­¾ç”±ã€Œå°±è¯Šå¡å·ã€æ”¹ä¸ºã€Œé—¨è¯Šå·ã€ï¼ˆç»ç¡®è®¤ï¼‰ï¼šmedical_card å…¨åº“为空,
           è¯¥åˆ—实际显示 outpatient_no(详见《部署清单》5.1)。 -->
      <el-table-column label="门诊号" align="center" min-width="140px">
        <template #default="scope">
          <span>{{ resolveCardNo(scope.row).value }}</span>
          <el-tag v-if="resolveCardNo(scope.row).from" size="small" type="info" effect="plain" class="ml-1">
            {{ resolveCardNo(scope.row).from }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="医嘱号" align="center" prop="doctorAdviceNo" min-width="110px">
        <template #default="scope">
          {{ scope.row.doctorAdviceNo || '-' }}
        </template>
      </el-table-column>
      <!-- éœ€æ±‚1:补齐患者信息(后端 JobRecordDO å·²æ‰©å­—段并按 rentId æ‰¹é‡å›žå¡«ï¼‰ -->
      <el-table-column label="性别" align="center" width="70px">
        <template #default="scope">
          <!-- 1男/2女;0 æˆ–缺失(HIS æœªæä¾›ï¼‰æ˜¾ç¤º"未知" -->
          <dict-tag v-if="scope.row.patGender === 1 || scope.row.patGender === 2"
                    :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.patGender" />
          <span v-else>未知</span>
        </template>
      </el-table-column>
      <el-table-column label="出生日期" align="center" width="110px">
        <template #default="scope">{{ formatDateOnly(scope.row.patBirthday) || '-' }}</template>
      </el-table-column>
      <el-table-column label="年龄" align="center" width="80px">
        <template #default="scope">{{ formatAge(scope.row.patBirthday) || '-' }}</template>
      </el-table-column>
      <el-table-column label="来源" align="center" width="90px">
        <template #default="scope">
          <dict-tag v-if="scope.row.patSrc !== undefined && scope.row.patSrc !== null"
                    :type="DICT_TYPE.ECG_PAT_SOURCE" :value="scope.row.patSrc" />
          <span v-else>-</span>
        </template>
      </el-table-column>
      <el-table-column label="病区" align="center" min-width="110px">
        <template #default="scope">{{ scope.row.patWardDesc || '-' }}</template>
      </el-table-column>
      <el-table-column label="病床号" align="center" width="90px">
        <template #default="scope">{{ scope.row.patBedNo || '-' }}</template>
      </el-table-column>
      <el-table-column label="患者电话" align="center" min-width="120px">
        <template #default="scope">{{ scope.row.patMobile || scope.row.patPhone || '-' }}</template>
      </el-table-column>
      <el-table-column label="开单日期" align="center" min-width="150px">
        <template #default="scope">{{ formatDateTime(scope.row.doctorTime) || '-' }}</template>
      </el-table-column>
      <el-table-column label="距今天数" align="center" width="90px">
        <template #default="scope">{{ formatDaysFromOrder(scope.row.doctorTime) || '-' }}</template>
      </el-table-column>
      <el-table-column label="开单医师" align="center" min-width="100px">
        <template #default="scope">{{ scope.row.doctor || '-' }}</template>
      </el-table-column>
      <el-table-column label="多耐" align="center" width="110px">
        <template #default="scope">
          <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="scope.row.tolerance ?? 0" />
        </template>
      </el-table-column>
      <el-table-column
        label="作业时间"
        align="center"
@@ -192,6 +254,9 @@
import download from '@/utils/download'
import { JobRecordApi, JobRecordVO } from '@/api/ecg/jobrecord'
import JobRecordForm from './JobRecordForm.vue'
// éœ€æ±‚1:患者信息 18 é¡¹ï¼ˆå­—å…¸ + å¹´é¾„/距今天数等实时计算)
import { DICT_TYPE } from '@/utils/dict'
import { formatAge, formatDaysFromOrder, formatDateTime, formatDateOnly, resolveCardNo } from '@/utils/patientInfo'
/** å·¥ä½œè®°å½• åˆ—表(当前未被任何菜单/代码引用,保留备用) */
// åŽŸ name ä¸º 'JobRecord',与 JobStatistic.vue é‡åï¼ˆåŽè€…是菜单 2839 å”¯ä¸€åœ¨ç”¨çš„页面)。
src/views/ecg/queue/index.vue
@@ -182,22 +182,76 @@
<!--      <el-table-column label="id" align="center" prop="id" />-->
      <el-table-column label="就诊流水号" align="center" prop="episodeId" min-width="110px"/>
      <el-table-column label="申请号" align="center" prop="applyNo" min-width="110px"/>
      <el-table-column label="排队序号" align="center" prop="seqNum" />
      <el-table-column label="预约号" align="center" prop="bookSeqNum" />
      <el-table-column label="患者编号" align="center" prop="patId" />
      <el-table-column label="患者姓名" align="center" min-width="160px">
      <!-- æœ¬åˆ—标签由「就诊卡号」改为「门诊号」(经确认)。原因(详见《部署清单》5.1):
           HIS åœ¨å¡å·æ§½ä½ä¼ ç©ºï¼Œmedical_card å…¨åº“ 159,116 è¡Œ**无一非空**;
           å®žæµ‹è¯¥åˆ— 99.9% æ˜¾ç¤ºçš„æ˜¯ outpatient_no(65% å½¢å¦‚身份证、35% ä¸ºé—¨è¯Šå·ï¼‰ã€‚
           resolveCardNo ä»æŒ‰å€¼å½¢æ€ç»™å‡º**来源标注**(身份证/住院号/门诊号)。 -->
      <el-table-column label="门诊号" align="center" min-width="140px">
        <template #default="scope">
          {{scope.row.patName}} ({{genderFormat(scope.row.patGender)}})
          <dict-tag :type="DICT_TYPE.ECG_PAT_SOURCE" :value="scope.row.patDetails?.source" />
          <span>{{ resolveCardNo(scope.row).value }}</span>
          <el-tag v-if="resolveCardNo(scope.row).from" size="small" type="info" effect="plain" class="ml-1">
            {{ resolveCardNo(scope.row).from }}
          </el-tag>
        </template>
      </el-table-column>
      <!-- éœ€æ±‚1:医嘱号(HIS observationRequest.id.item[root=2.16.156.10011.1.28])。
           è¯¥èŠ‚ç‚¹åœ¨è§„èŒƒä¸­éžå¿…é¡»é¡¹ï¼ŒåŽ†å²æ•°æ®ï¼ˆæ–°å¢žåˆ—ä¹‹å‰é‡‡é›†çš„ï¼‰ä¸ºç©ºï¼Œæ˜¾ç¤º '-'。 -->
      <el-table-column label="医嘱号" align="center" prop="doctorAdviceNo" min-width="110px">
        <template #default="scope">
          {{ scope.row.doctorAdviceNo || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="患者姓名" align="center" prop="patName" min-width="90px" />
      <el-table-column label="性别" align="center" prop="patGender" width="70px">
        <template #default="scope">{{ genderFormat(scope.row.patGender) }}</template>
      </el-table-column>
      <el-table-column label="来源" align="center" width="90px">
        <template #default="scope">
          <!-- ä¼˜å…ˆç”¨é¢„约单平铺值,回退到 patDetails å¿«ç…§ -->
          <dict-tag :type="DICT_TYPE.ECG_PAT_SOURCE"
                    :value="scope.row.patSrc ?? scope.row.patDetails?.source ?? 0" />
        </template>
      </el-table-column>
      <el-table-column label="出生日期" align="center" min-width="110px">
        <template #default="scope">{{ formatDateOnly(scope.row.patBirthday) }}</template>
      </el-table-column>
      <el-table-column label="年龄" align="center" width="80px">
        <template #default="scope">{{ formatAge(scope.row.patBirthday) }}</template>
      </el-table-column>
      <el-table-column label="检查类型" align="center" prop="bookCheckType" min-width="100px">
        <template #default="scope">
          {{getCheckTypeName(scope.row.bookCheckType)}}
        </template>
      </el-table-column>
      <el-table-column label="患者详情" align="center" prop="patDetails" min-width="300px">
      <el-table-column label="病区" align="center" min-width="110px">
        <template #default="scope">{{ scope.row.patWardDesc || scope.row.patDetails?.wardDesc || '-' }}</template>
      </el-table-column>
      <el-table-column label="病床号" align="center" width="90px">
        <template #default="scope">
          <span>{{scope.row.patDetails?.mobile}} {{scope.row.patDetails?.wardDesc}} {{scope.row.patDetails?.bedNo}}</span>
          <!-- æ³¨æ„ï¼šscope.row.bedNo æ˜¯ã€è¯Šç–—床】(工位),不是病床号;
               ç—…床号取 patBedNo / patDetails.bedNo -->
          {{ scope.row.patBedNo || scope.row.patDetails?.bedNo || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="患者电话" align="center" min-width="120px">
        <template #default="scope">
          {{ scope.row.patMobile || scope.row.patPhone || scope.row.patDetails?.mobile || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="登记时间" align="center" min-width="150px">
        <template #default="scope">{{ formatDateTime(scope.row.registerDate) }}</template>
      </el-table-column>
      <el-table-column label="开单日期" align="center" min-width="150px">
        <template #default="scope">{{ formatDateTime(scope.row.doctorTime) }}</template>
      </el-table-column>
      <el-table-column label="距今天数" align="center" width="90px">
        <template #default="scope">{{ formatDaysFromOrder(scope.row.doctorTime) }}</template>
      </el-table-column>
      <el-table-column label="开单医师" align="center" prop="doctor" min-width="100px">
        <template #default="scope">{{ scope.row.doctor || '-' }}</template>
      </el-table-column>
      <el-table-column label="预约时段" align="center" prop="bookTimeslot" min-width="100px">
        <template #default="scope">
@@ -209,11 +263,10 @@
          <dict-tag :type="DICT_TYPE.ECG_QUEUE_STATUS" :value="scope.row.status" />
        </template>
      </el-table-column>
      <el-table-column label="预约号" align="center" prop="bookSeqNum" />
      <el-table-column label="排队号" align="center" prop="seqNum" />
      <el-table-column label="多耐药" width="150" align="center" prop="tolerance" >
         <template #default="scope">
          <dict-tag :type="DICT_TYPE.ecg_tolerance" :value="scope.row.tolerance" />
          <!-- éœ€æ±‚6:多耐走字典;NULL æŒ‰"无"显示(Q12) -->
          <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="scope.row.tolerance ?? 0" />
        </template>
      </el-table-column>
      <el-table-column label="VIP" align="center" prop="isVip" />
@@ -256,6 +309,8 @@
import AppointmentForm from "@/views/ecg/appointment/AppointmentForm.vue";
import {genderFormat} from "@/utils/stringUtil";
import {getCheckTypeName} from "../../../utils/checkTypeFormatter";
// éœ€æ±‚1:患者信息 18 é¡¹çš„公共格式化(年龄、距今天数等)
import { formatAge, formatDaysFromOrder, formatDateTime, formatDateOnly, resolveCardNo } from '@/utils/patientInfo'
/** æŽ’队 åˆ—表 */
defineOptions({ name: 'queue' })