Vue 2 门诊收费系统中的医保结算流程设计与实践

医保结算并不是"点击按钮、调用接口、显示成功"这么简单。它同时涉及患者身份认证、医保设备通信、统筹与个人账户分流、本地与异地政策差异、预结算确认、现金补差、凭证打印等多个环节。

如果把所有逻辑都堆在一个点击事件里,代码很快就会被大量 if/else、弹窗状态和异步回调淹没。本文结合一个门诊收费页面的实现,讨论如何在 Vue 2 项目中组织这类多分支、强交互的结算流程,以及其中值得复用的工程经验。

一、先把结算问题拆成三个维度

医保流程看似分支很多,实际上可以拆成三个相互独立的选择维度。

1. 参保区域

  • 本地参保
  • 异地参保

异地结算通常需要额外选择参保地,并在请求中携带参保地标识。某些地区或结算方式可能存在政策限制,因此应在进入设备交互前完成校验,避免患者完成身份认证后才被告知无法结算。

2. 结算模式

  • 普通门诊统筹
  • 门诊慢特病统筹
  • 本地个人账户
  • 异地个人账户

建议不要在业务代码里散落难以理解的数字常量,而是使用语义化枚举:

js 复制代码
const SETTLEMENT_MODE = Object.freeze({
  OUTPATIENT_POOLING: "outpatient_pooling",
  CHRONIC_POOLING: "chronic_pooling",
  LOCAL_ACCOUNT: "local_account",
  REMOTE_ACCOUNT: "remote_account",
});

即使后端仍要求传递数字编码,也应只在请求适配层进行转换。

3. 身份认证介质

  • 社会保障卡
  • 医保电子凭证
  • 居民身份证
  • 人脸认证

不同介质的采集方式不同,但成功后都应收敛为统一的参保人上下文,例如:

js 复制代码
function normalizeInsuredPerson(raw) {
  return {
    personId: raw.personId,
    personName: raw.personName,
    cardToken: raw.cardToken || "",
    credentialToken: raw.credentialToken || "",
    agencyCode: raw.agencyCode || "",
  };
}

后续预结算逻辑只依赖标准化数据,不需要反复判断身份来自实体卡、二维码还是人脸设备。

二、浏览器、业务后端与本地设备的协作

医保结算通常同时存在两类通信:

  1. 浏览器通过 HTTP 请求业务后端,完成参保信息查询、预结算、正式结算与凭证获取。
  2. 浏览器通过本地 WebSocket 中间件驱动读卡器、扫码器、密码键盘或人脸终端。

整体关系可以抽象为:
#mermaid-svg-c8K2OBd48hvjpdeA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-c8K2OBd48hvjpdeA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-c8K2OBd48hvjpdeA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-c8K2OBd48hvjpdeA .error-icon{fill:#552222;}#mermaid-svg-c8K2OBd48hvjpdeA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-c8K2OBd48hvjpdeA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-c8K2OBd48hvjpdeA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-c8K2OBd48hvjpdeA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-c8K2OBd48hvjpdeA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-c8K2OBd48hvjpdeA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-c8K2OBd48hvjpdeA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-c8K2OBd48hvjpdeA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-c8K2OBd48hvjpdeA .marker.cross{stroke:#333333;}#mermaid-svg-c8K2OBd48hvjpdeA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-c8K2OBd48hvjpdeA p{margin:0;}#mermaid-svg-c8K2OBd48hvjpdeA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-c8K2OBd48hvjpdeA .cluster-label text{fill:#333;}#mermaid-svg-c8K2OBd48hvjpdeA .cluster-label span{color:#333;}#mermaid-svg-c8K2OBd48hvjpdeA .cluster-label span p{background-color:transparent;}#mermaid-svg-c8K2OBd48hvjpdeA .label text,#mermaid-svg-c8K2OBd48hvjpdeA span{fill:#333;color:#333;}#mermaid-svg-c8K2OBd48hvjpdeA .node rect,#mermaid-svg-c8K2OBd48hvjpdeA .node circle,#mermaid-svg-c8K2OBd48hvjpdeA .node ellipse,#mermaid-svg-c8K2OBd48hvjpdeA .node polygon,#mermaid-svg-c8K2OBd48hvjpdeA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-c8K2OBd48hvjpdeA .rough-node .label text,#mermaid-svg-c8K2OBd48hvjpdeA .node .label text,#mermaid-svg-c8K2OBd48hvjpdeA .image-shape .label,#mermaid-svg-c8K2OBd48hvjpdeA .icon-shape .label{text-anchor:middle;}#mermaid-svg-c8K2OBd48hvjpdeA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-c8K2OBd48hvjpdeA .rough-node .label,#mermaid-svg-c8K2OBd48hvjpdeA .node .label,#mermaid-svg-c8K2OBd48hvjpdeA .image-shape .label,#mermaid-svg-c8K2OBd48hvjpdeA .icon-shape .label{text-align:center;}#mermaid-svg-c8K2OBd48hvjpdeA .node.clickable{cursor:pointer;}#mermaid-svg-c8K2OBd48hvjpdeA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-c8K2OBd48hvjpdeA .arrowheadPath{fill:#333333;}#mermaid-svg-c8K2OBd48hvjpdeA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-c8K2OBd48hvjpdeA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-c8K2OBd48hvjpdeA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c8K2OBd48hvjpdeA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-c8K2OBd48hvjpdeA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c8K2OBd48hvjpdeA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-c8K2OBd48hvjpdeA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-c8K2OBd48hvjpdeA .cluster text{fill:#333;}#mermaid-svg-c8K2OBd48hvjpdeA .cluster span{color:#333;}#mermaid-svg-c8K2OBd48hvjpdeA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-c8K2OBd48hvjpdeA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-c8K2OBd48hvjpdeA rect.text{fill:none;stroke-width:0;}#mermaid-svg-c8K2OBd48hvjpdeA .icon-shape,#mermaid-svg-c8K2OBd48hvjpdeA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c8K2OBd48hvjpdeA .icon-shape p,#mermaid-svg-c8K2OBd48hvjpdeA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-c8K2OBd48hvjpdeA .icon-shape .label rect,#mermaid-svg-c8K2OBd48hvjpdeA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c8K2OBd48hvjpdeA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-c8K2OBd48hvjpdeA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-c8K2OBd48hvjpdeA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTPS
本地 WebSocket
设备事件
Vue 2 收费页面
业务后端
医保服务平台
医保设备中间件
读卡/扫码/密码/人脸设备

设备通信是事件驱动的:前端发送"读取某类介质"的请求,中间件稍后推送成功、失败或取消事件。因此不能把 WebSocket 当作普通同步函数使用,而应建立清晰的状态:

js 复制代码
data() {
  return {
    deviceConnected: false,
    settlementStage: "idle",
    statusMessage: "",
    insuredPerson: null,
    preSettlement: null,
    settlementTransactionId: "",
  };
}

推荐至少区分以下阶段:

text 复制代码
idle
→ choosing_credential
→ reading_credential
→ loading_person
→ pre_settling
→ reviewing
→ waiting_confirmation
→ settling
→ supplementing_payment
→ completed / failed / cancelled

页面可以继续用多个 Element UI 弹窗承载交互,但流程判断最好依赖 settlementStage,而不是仅依赖"某个弹窗当前是否可见"。

三、用统一入口完成身份认证后的分流

实体卡、电子凭证、身份证和人脸认证的设备事件格式并不一致。如果每个事件处理函数都直接继续结算,后续会形成四套高度重复的流程。

更好的方式是:每种介质只负责采集和标准化身份,成功后统一进入分流方法。

js 复制代码
async function continueAfterIdentity() {
  this.settlementStage = "loading_person";

  if (this.credentialType !== "social_security_card") {
    await this.loadInsuredPerson();
  }

  switch (this.settlementMode) {
    case SETTLEMENT_MODE.OUTPATIENT_POOLING:
    case SETTLEMENT_MODE.CHRONIC_POOLING:
      return this.startPoolingPreSettlement();

    case SETTLEMENT_MODE.LOCAL_ACCOUNT:
      return this.requestAccountPassword();

    case SETTLEMENT_MODE.REMOTE_ACCOUNT:
      return this.startRemoteAccountSettlement();

    default:
      throw new Error("未选择有效的医保结算方式");
  }
}

这种设计的价值在于:身份介质负责"我是谁",结算模式负责"接下来怎么付",二者不互相污染。

四、门诊统筹结算主流程

统筹结算的核心不是一次结算请求,而是"预结算---人工确认---设备确认---正式结算"的闭环。
#mermaid-svg-auDLJltDUEazOsyy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-auDLJltDUEazOsyy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-auDLJltDUEazOsyy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-auDLJltDUEazOsyy .error-icon{fill:#552222;}#mermaid-svg-auDLJltDUEazOsyy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-auDLJltDUEazOsyy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-auDLJltDUEazOsyy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-auDLJltDUEazOsyy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-auDLJltDUEazOsyy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-auDLJltDUEazOsyy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-auDLJltDUEazOsyy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-auDLJltDUEazOsyy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-auDLJltDUEazOsyy .marker.cross{stroke:#333333;}#mermaid-svg-auDLJltDUEazOsyy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-auDLJltDUEazOsyy p{margin:0;}#mermaid-svg-auDLJltDUEazOsyy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-auDLJltDUEazOsyy .cluster-label text{fill:#333;}#mermaid-svg-auDLJltDUEazOsyy .cluster-label span{color:#333;}#mermaid-svg-auDLJltDUEazOsyy .cluster-label span p{background-color:transparent;}#mermaid-svg-auDLJltDUEazOsyy .label text,#mermaid-svg-auDLJltDUEazOsyy span{fill:#333;color:#333;}#mermaid-svg-auDLJltDUEazOsyy .node rect,#mermaid-svg-auDLJltDUEazOsyy .node circle,#mermaid-svg-auDLJltDUEazOsyy .node ellipse,#mermaid-svg-auDLJltDUEazOsyy .node polygon,#mermaid-svg-auDLJltDUEazOsyy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-auDLJltDUEazOsyy .rough-node .label text,#mermaid-svg-auDLJltDUEazOsyy .node .label text,#mermaid-svg-auDLJltDUEazOsyy .image-shape .label,#mermaid-svg-auDLJltDUEazOsyy .icon-shape .label{text-anchor:middle;}#mermaid-svg-auDLJltDUEazOsyy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-auDLJltDUEazOsyy .rough-node .label,#mermaid-svg-auDLJltDUEazOsyy .node .label,#mermaid-svg-auDLJltDUEazOsyy .image-shape .label,#mermaid-svg-auDLJltDUEazOsyy .icon-shape .label{text-align:center;}#mermaid-svg-auDLJltDUEazOsyy .node.clickable{cursor:pointer;}#mermaid-svg-auDLJltDUEazOsyy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-auDLJltDUEazOsyy .arrowheadPath{fill:#333333;}#mermaid-svg-auDLJltDUEazOsyy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-auDLJltDUEazOsyy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-auDLJltDUEazOsyy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-auDLJltDUEazOsyy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-auDLJltDUEazOsyy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-auDLJltDUEazOsyy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-auDLJltDUEazOsyy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-auDLJltDUEazOsyy .cluster text{fill:#333;}#mermaid-svg-auDLJltDUEazOsyy .cluster span{color:#333;}#mermaid-svg-auDLJltDUEazOsyy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-auDLJltDUEazOsyy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-auDLJltDUEazOsyy rect.text{fill:none;stroke-width:0;}#mermaid-svg-auDLJltDUEazOsyy .icon-shape,#mermaid-svg-auDLJltDUEazOsyy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-auDLJltDUEazOsyy .icon-shape p,#mermaid-svg-auDLJltDUEazOsyy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-auDLJltDUEazOsyy .icon-shape .label rect,#mermaid-svg-auDLJltDUEazOsyy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-auDLJltDUEazOsyy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-auDLJltDUEazOsyy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-auDLJltDUEazOsyy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 放弃
确认


选择统筹类型
选择身份介质
设备完成身份认证
补全参保人信息
发起统筹预结算
展示预结算明细
取消本次流程
医保终端展示金额
确认是否使用个人账户
提交正式统筹结算
是否仍有待支付金额
完成并打印凭证
进入个账或现金补差

预结算请求通常需要以下业务信息:

  • 认证方式与身份令牌
  • 就诊标识和处方标识集合
  • 是否包含挂号费
  • 本地/异地标识与参保地
  • 当前收费员等审计信息

预结算返回后,应把统筹支付、个人负担、账户余额、起付线和其他支付项集中展示,让收费员和患者确认,而不是立即提交最终结算。

正式结算成功后,要以服务端返回的待支付金额为准更新页面。若异地统筹后仍有个人负担,可再进入异地个人账户身份认证与支付;若个人账户不足,则继续走现金、扫码或院内储值等补差渠道。

五、本地与异地个人账户流程

本地个人账户

本地个账通常遵循:

text 复制代码
身份认证
→ 获取支付密码
→ 个账预结算
→ 展示预结算单
→ 患者确认
→ 个账正式结算
→ 处理剩余自费金额

这里要特别注意,密码属于高敏感瞬时数据。前端不应把它写入本地存储、URL、埋点或普通日志;完成请求后应尽快清空内存中的引用。

异地个人账户

异地个账通常多出"异地身份认证"和"参保地校验":

text 复制代码
校验参保地与政策限制
→ 异地身份认证
→ 获取账户余额
→ 获取支付密码
→ 确认个账支付金额
→ 提交异地个账支付
→ 使用其他方式补足剩余金额

对于"统筹结算后的异地个账补充支付",建议重新做一次有效身份认证。较早获取的二维码或身份令牌可能已经过期,不能默认复用。

六、金额处理:前端校验不能破坏服务端预结算结果

医保金额计算有两个基本原则。

第一,正式金额以服务端预结算和结算结果为准;前端计算主要服务于交互展示与输入校验。

第二,金额不要直接依赖 JavaScript 浮点运算。项目中可以使用 Decimal 类库,或统一转成"分"进行整数计算:

js 复制代码
function yuanToCent(value) {
  return Math.round((Number(value) || 0) * 100);
}

function centToYuan(value) {
  return (value / 100).toFixed(2);
}

一个很典型的 Vue 2 陷阱,是使用 watcher 监听预结算金额:

js 复制代码
watch: {
  "preSettlement.accountPayment"(value) {
    this.validateAccountPayment(value);
  },
}

当接口把整个预结算对象赋值给组件时,这个 watcher 也会触发。前端可能把服务端返回值误判为"用户输入超限",不仅弹出错误提示,还会反向修改接口数据。

更稳妥的做法是只在用户输入事件中校验:

vue 复制代码
<el-input
  v-model="preSettlement.accountPayment"
  @input="handleAccountPaymentInput"
/>
js 复制代码
function handleAccountPaymentInput(value) {
  const payment = yuanToCent(value);
  const available =
    yuanToCent(this.preSettlement.cardBalance) +
    yuanToCent(this.preSettlement.sharedBalance);

  if (payment > available) {
    this.$message.error("账户支付金额不能超过可用余额");
    return;
  }

  this.recalculateDisplayAmount(payment);
}

这样,接口初始化与用户编辑被明确区分,预结算数据不会被响应式副作用篡改。最终提交前仍需再执行一次完整校验,后端也必须进行权威校验。

七、设备事件处理要防重复、防串单

WebSocket 回调可能迟到、重复,甚至在用户切换患者后才返回。生产实现中建议为每次收费创建前端流程 ID,并校验所有回调是否仍属于当前流程:

js 复制代码
startSettlement() {
  this.flowId = createUniqueFlowId();
  this.settlementStage = "choosing_credential";
}

handleDeviceMessage(message) {
  if (message.flowId !== this.flowId) return;
  if (!this.isExpectedEvent(message.eventType)) return;

  this.dispatchDeviceEvent(message);
}

此外还应做到:

  • 确认按钮提交期间禁用,防止重复结算。
  • 每次正式结算携带幂等业务号。
  • 用户取消时通知设备中间件终止当前操作。
  • 连接断开后给出明确状态,不要自动重复提交结算请求。
  • 页面销毁时移除键盘监听、关闭连接或取消订阅。
  • 快捷键只能在目标弹窗可见且流程阶段正确时生效,并再次展示脱敏后的患者确认信息。

八、错误处理不要只有一个 Toast

医保流程持续时间长,且很多步骤需要患者配合。只弹出一个短暂 Toast 往往无法说明当前状态。

建议同时提供三层反馈:

  1. 流程状态弹窗:提示"等待刷卡""等待输入密码""正在预结算"等持续状态。
  2. 即时消息:提示某一步成功、失败或被取消。
  3. 可恢复操作:区分"重新读取身份""重新预结算""继续现金补差"和"放弃本次结算"。

错误对象也应带上所处阶段:

js 复制代码
function handleSettlementError(error, stage) {
  this.settlementStage = "failed";
  this.statusMessage = getSafeUserMessage(error, stage);
  reportSafeError({
    stage,
    errorCode: error.code,
    flowId: this.flowId,
  });
}

日志只记录流程 ID、阶段和错误码,不记录姓名、证件号、卡号、二维码令牌、密码、完整请求报文或结算凭证地址。

九、医保前端必须落实的数据安全边界

技术博客中的"脱敏"不能只停留在把姓名替换成星号。真实系统至少应做到:

  • 页面只展示业务必需的身份信息,并默认掩码。
  • 身份令牌、卡识别码、密码和人脸授权信息只保存在内存中。
  • 禁止在 console.log、监控埋点和错误上报中输出完整设备消息或接口响应。
  • 不在前端源码中硬编码机构编码、科室、操作员、内网地址和设备端口。
  • 浏览器到后端使用 HTTPS;设备通信由受控的本地代理完成,并校验消息来源。
  • 后端负责签名、鉴权、幂等、金额复核和审计,前端不能成为信任边界。
  • 打印地址使用短期授权链接,打印结束后及时失效。
  • 切换患者、取消结算或页面销毁时清空医保上下文。

可以封装统一的掩码函数,避免每个页面自行处理:

js 复制代码
function maskCredential(value = "") {
  if (value.length < 8) return "****";
  return `${value.slice(0, 3)}********${value.slice(-3)}`;
}

十、从单文件组件继续演进

当一个收费页面同时承载患者列表、处方、收费、医保设备、退费和打印时,单文件组件很容易增长到数千行。后续可以按职责拆分:

text 复制代码
views/toll/
├── TollPage.vue
├── components/
│   ├── SettlementModePanel.vue
│   ├── CredentialSelector.vue
│   ├── PoolingPreviewDialog.vue
│   ├── AccountPreviewDialog.vue
│   └── SettlementStatusDialog.vue
├── services/
│   ├── insuranceApi.js
│   └── insuranceDevice.js
└── composables/
    └── settlementMachine.js

Vue 2 项目可以使用 Vuex 模块、普通 service 类或可复用 mixin 承载流程;如果允许引入状态机库,也可以将所有合法状态迁移显式建模。关键不是采用哪种工具,而是把"界面显示""设备协议""业务流程""接口适配"四层分开。

结语

医保结算前端最难的部分,从来不是某一个接口,而是跨设备、跨区域、跨支付方式的长事务协作。

一套可靠的实现通常具备这些特征:身份认证入口统一、结算分支清晰、预结算与正式结算严格分离、金额由服务端定稿、设备事件可追踪且可取消、失败后能够恢复,同时敏感数据不进入日志和持久化存储。

当我们把流程从零散的弹窗和回调提升为明确的状态模型后,代码会更容易测试,收费人员也更清楚系统正在等待什么。对于医疗收费这类高可靠业务,这种可解释性本身就是系统质量的一部分。

相关推荐
moonrailgun1 小时前
用 Node.js 复刻 Codex Astra 的终端星光
前端·javascript·算法
海上彼尚1 小时前
Cursor 模型的强度实测排行
前端·人工智能·后端
默_笙2 小时前
💫 给金鱼建图书馆(下):Milvus 向量数据库,让 AI 拥有长期记忆
前端·javascript
liangshanbo12152 小时前
手写 Function.prototype.call 与 Function.prototype.apply 面试题满分答案
开发语言·javascript·原型模式
EatFan2 小时前
HTML前端利用flex解决input定位的问题
前端·css·html·css3
CoderYanger3 小时前
前端基础——JavaScript(WebAPI)(下篇)
java·开发语言·前端·javascript·程序人生·面试·职场和发展
kyriewen3 小时前
AI 改祖传模块后,代码评审总绕不开两个问题
前端·程序员·ai编程
飞翔的熊blabla3 小时前
# Webpack 5 + Jenkins 持久化缓存实战:前端构建从 288 秒降到 30 秒>
前端·webpack·jenkins
风骏时光牛马4 小时前
AI多模态:跨越感官边界的智能新体验 要不要开启工作任务模式,帮你配套对应的文案和配图思路?
前端