This commit is contained in:
2026-04-24 09:24:08 +08:00
29 changed files with 155 additions and 52 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
_Last synced: 2026-04-21 | Base commit: `c5d407a`_
_Last synced: 2026-04-23 | Base commit: `41c200e`_
# 记忆索引
+1 -1
View File
@@ -1,4 +1,4 @@
<!-- Last updated: 2026-04-21 | Commit: c5d407a -->
<!-- Last updated: 2026-04-23 | Commit: 41c200e -->
# 项目约束
+49 -38
View File
@@ -1,5 +1,9 @@
# 应急就医(员工端)产品需求文档
> **所属端**:员工端 APP
> **模块路径**`employee-app/emergency-medical/`
> **最后更新**2026-04-23
## 模块概述
应急就医模块为油田员工提供紧急医疗场景下的一站式服务能力,涵盖应急资源地图查看(医院、医疗点、AED、救护车、急救包)、应急求助(拨打120、应急视频通话、应急电话)、急救提示与教学、大病就医预约挂号等功能,帮助员工在突发健康事件中快速获取周边急救资源和专业医疗支持。
@@ -30,15 +34,15 @@
└── 应急求助记录
```
### 预约状态流转图
### 大病就医预约状态流转图
```mermaid
stateDiagram-v2
[*] --> 待确认: 员工发起预约
待确认 --> 已确认: 管理人员确认并派单给驻场人员
待确认 --> 已取消: 员工取消 / 管理人员取消
已确认 --> 已完成: 驻场人员完成挂号,在应急APP点击已完成
待确认 --> 已确认: 操作人员确认并派单给驻场人员
待确认 --> 已取消: 员工取消 / 操作人员取消
已确认 --> 已取消: 员工取消 / 管理人员取消
已确认 --> 已完成: 驻场人员完成挂号,在应急APP点击已完成
```
## 页面清单
@@ -69,8 +73,8 @@ stateDiagram-v2
| 序号 | 页面/弹窗 | 文件 | 类型 |
|----|----------|------|------|
| 13 | 应急求助记录 | help-records.html | 页面 |
| 14 | 应急视频对话页面 | emergency-chat.html | 页面 |
| 13 | 应急视频对话页面 | emergency-chat.html | 页面 |
| 14 | 应急求助记录 | help-records.html | 页面 |
### 急救教学
| 序号 | 页面/弹窗 | 文件 | 类型 |
@@ -131,7 +135,7 @@ stateDiagram-v2
**业务规则:**
- 地图图标标记按资源类型使用不同图标进行区分
- 顶部Tab栏包含:油田医院(默认选中)、合作医院、医疗点、AED、救护车、急救包
- 点击标记弹出的卡片展示:医院图片、医院名称、距离/驾车时间、地址、联系电话和导航按钮
- 点击标记点位弹出的卡片展示:医院图片、医院名称、距离/驾车时间、地址、联系电话和导航按钮
- 模拟急救面板提供心梗、猝死、脑梗/脑出血三种疾病场景
**交互说明:**
@@ -153,7 +157,7 @@ stateDiagram-v2
![医疗点地图](screenshots/resource-map-clinic.jpg)
![急救包地图](screenshots/resource-map-kit.jpg)
以上四个地图页面与油田医院地图共享统一的页面结构,仅资源类型不同。
以上四个地图页面与油田医院地图共享统一的页面结构,仅资源类型不同(注:救护车不显示地图导航按钮)
**交互说明:** 同油田医院地图页面,底部Tab栏切换时跳转到对应资源地图页面,点击标记弹出该类型资源的概要卡片。
@@ -194,7 +198,6 @@ stateDiagram-v2
**交互说明:**
- 点击"联系电话"按钮,调起系统拨号
- 点击"导航"按钮,调起地图导航
- 点击返回箭头,返回地图页面
---
@@ -256,46 +259,54 @@ stateDiagram-v2
---
## 三、应急求助
### 12. 应急视频对话页面(emergency-chat.html
![页面截图](screenshots/emergency-chat.jpg)
**功能说明:** 应急视频拨打之后,产生了一条“应急视频”的进行中状态的工单,如当前工单未完成,后续主页点击“应急视频”时,进入聊天对话页面
**业务规则:**
- 聊天区域展示三种消息类型:系统消息(居中灰色)、员工消息(右侧蓝色气泡)、对方消息(左侧白色气泡)
- 参与人员列表展示通话参与者的姓名和角色(如"吴凤-主治医师"、"张旭-操作人员"
- 底部输入栏包含语音按钮和"按住说话"按钮
**交互说明:**
- 长按"按住说话"按钮,发送语音消息
- 点击参与人员头像,查看人员信息
- 点击返回箭头,退出通话页面
---
### 13. 急救提示书(emergency-tips.html
### 12. 急救提示书(emergency-tips.html
![页面截图](screenshots/emergency-tips.jpg)
**功能说明:** 根据选择的疾病类型(心梗/猝死/脑梗)展示对应的急救步骤指导,并列出最近的急救资源及距离信息。
**功能说明:** 从应急资源地图主页通过【模拟急救】按钮进入-根据选择的疾病类型(心梗/猝死/脑梗)展示对应的急救步骤指导,并列出最近的急救资源及距离信息。
**业务规则:**
- 顶部以红色醒目标题显示急救提示书名称(如"心梗急救提示书"
- 急救步骤以编号列表形式展示,共4个步骤
- 顶部以红色醒目标题显示急救提示书名称(如心梗急救提示书
- 急救步骤以编号列表形式展示
- 最近急救资源清单按距离排序展示,每项包含:资源类型图标、距离信息、步行/驾车时间
- 资源类型包括:AED、急救包、救护车、医院、医疗点
- 资源类型包括:AED、急救包、救护车、合作医院、油田医院、医疗点,每种资源展示一个距离最近的
**交互说明:**
- 点击资源项的"详情"按钮,跳转到对应资源详情页面
- 点击"导航"按钮,调起地图导航到该资源
- 点击"电话"按钮(救护车),调起系统拨号
- 点击资源项的详情按钮,跳转到对应资源详情页面
- 点击导航按钮,调起地图导航到该资源
- 点击电话按钮(救护车),调起系统拨号
- 点击返回箭头,返回地图页面
---
## 三、应急求助
### 13. 应急视频对话页面(emergency-chat.html
![页面截图](screenshots/emergency-chat.jpg)
**功能说明:** 点击主页【应急视频】按钮会发起三方(专业人员、操作人员、员工)视频通话,同时应急视频拨打之后,产生了一条”应急视频”的进行中状态的工单,如当前工单未完成,后续主页点击”应急视频”时,进入聊天对话页面
**业务规则:**
- 点击【应急视频】按钮后查询是否存在未结束的应急,若存在则直接进入当前应急单,若不存在自动拉起三方通话并新建应急单
- 参与本次应急视频通话的操作人员和专业人员选定规则:
* 根据员工所在单位反查所属的应急中心(若该单位未分配到指定的应急中心则使用默认应急中心)
* 查询当前应急中心的排班人员加到应急通话(若当前应急中心未设置排班则使用默认应急中心的排班设置)
* 若应急中心下同时存在多个值班人员优先查找处于空闲状态下的值班人员加入视频通话
* 若多个值班人员都处于空闲状态使用轮询策略
* 若当前应急中心和默认应急中心都没有设置值班人员 则无法发起应急通话,提示“暂无应急值班人员”
- 聊天区域展示三种消息类型:系统消息(居中灰色)、员工消息(右侧蓝色气泡)、对方消息(左侧白色气泡)
- 参与人员列表展示通话参与者的姓名和角色(如”吴凤-主治医师”、”张旭-操作人员”)
- 底部输入栏包含【+更多功能】、文字输入、语音输入以及发送按钮
**交互说明:**
- 点击【结束应急】弹出二次确认框,支持用户输入备注信息
- 长按”话筒图标”按钮,发送语音消息
- 点击【+】可重新发起视频通话、发送照片、拍摄、发送位置
- 点击返回箭头,退出通话页面
---
### 14. 应急求助记录(help-records.html
![页面截图](screenshots/help-records.jpg)
@@ -342,7 +353,7 @@ stateDiagram-v2
**功能说明:** 展示员工的大病就医预约记录列表,默认显示进行中的预约,支持Tab切换查看不同状态。
**业务规则:**
- Tab栏:全部、进行中(默认激活、已完成、已取消
- Tab栏:全部(默认激活,根据状态和时间双重倒序)、进行中、已完成、已取消
- 预约卡片展示:医院名称、状态标签、科室、医生、挂号类别、期望预约时间
- 进行中状态包含"待确认"和"已确认"两种子状态
- 底部提供"发起新预约"按钮
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@@ -42,11 +42,41 @@
.participant__name { font-size: 11px; color: #666; }
.participant__status { font-size: 10px; color: #52c41a; }
/* 底部输入 */
.chat-input-bar { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: #fff; border-top: 1px solid #f0f0f0; }
.btn-voice { width: 36px; height: 36px; border-radius: 50%; background: #f5f5f5; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
.voice-btn { flex: 1; height: 36px; border-radius: 18px; background: #f5f5f5; border: none; font-size: 14px; color: #666; cursor: pointer; }
/* 底部输入区域 */
.chat-input-area { background: #fff; border-top: 1px solid #f0f0f0; padding: 8px 12px; flex-shrink: 0; }
.chat-input-row { display: flex; align-items: center; gap: 8px; }
.chat-input-row input { flex: 1; background: #f5f5f5; border: none; border-radius: 20px; padding: 9px 16px; font-size: 14px; outline: none; }
.chat-input-row .plus-btn { width: 36px; height: 36px; border-radius: 50%; background: #1677ff; color: #fff; border: none; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.chat-input-row .send-btn { background: #1677ff; color: #fff; border: none; border-radius: 20px; padding: 8px 16px; font-size: 14px; cursor: pointer; flex-shrink: 0; }
.voice-toggle-btn { width: 36px; height: 36px; border-radius: 50%; border: none; background: #f5f5f5; color: #666; font-size: 18px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: background 0.2s; }
.voice-toggle-btn:active { background: #e8e8e8; }
.hold-to-talk { flex: 1; height: 36px; border: 1.5px solid #e8e8e8; border-radius: 20px; background: #f9f9f9; color: #333; font-size: 14px; font-weight: 500; cursor: pointer; display: none; align-items: center; justify-content: center; user-select: none; transition: background 0.15s; }
.hold-to-talk:active { background: #e6f4ff; border-color: #1677ff; color: #1677ff; }
.hold-to-talk.show { display: flex; }
.chat-input-row input.hide { display: none; }
.chat-input-row .send-btn.hide { display: none; }
.plus-panel { display: none; grid-template-columns: repeat(4, 1fr); gap: 12px; padding: 16px 8px 8px; }
.plus-panel.show { display: grid; }
.plus-item { display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; }
.plus-item .pi-icon { width: 52px; height: 52px; border-radius: 14px; background: #f5f5f5; display: flex; align-items: center; justify-content: center; font-size: 22px; }
.plus-item .pi-label { font-size: 11px; color: #666; }
.bottom-spacer { height: 34px; flex-shrink: 0; background: #fff; }
/* 结束应急按钮 */
.end-btn { position: absolute; right: 16px; background: #ff4d4f; color: #fff; border: none; border-radius: 14px; padding: 4px 12px; font-size: 13px; cursor: pointer; }
/* 二次确认弹窗 */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 999; justify-content: center; align-items: center; }
.modal-overlay.show { display: flex; }
.modal-box { width: 300px; background: #fff; border-radius: 12px; padding: 24px 20px 16px; text-align: center; }
.modal-title { font-size: 17px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.modal-desc { font-size: 13px; color: #999; margin-bottom: 16px; }
.modal-box textarea { width: 100%; height: 72px; border: 1px solid #e8e8e8; border-radius: 8px; padding: 10px; font-size: 13px; resize: none; outline: none; box-sizing: border-box; }
.modal-box textarea:focus { border-color: #1677ff; }
.modal-actions { display: flex; gap: 12px; margin-top: 16px; }
.modal-actions button { flex: 1; height: 36px; border-radius: 8px; border: none; font-size: 14px; cursor: pointer; }
.modal-cancel { background: #f5f5f5; color: #666; }
.modal-confirm { background: #ff4d4f; color: #fff; }
</style>
</head>
<body>
@@ -63,6 +93,7 @@
<div class="nav-bar">
<span class="nav-back" onclick="history.back()"></span>
<span class="nav-title">西安应急中心</span>
<button class="end-btn" onclick="showEndModal()">结束应急</button>
</div>
<div class="chat-area">
@@ -117,15 +148,77 @@
</div>
</div>
<!-- 底部输入 -->
<div class="chat-input-bar">
<button class="btn-voice">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/></svg>
</button>
<button class="voice-btn">按住说话</button>
<!-- 底部输入区域 -->
<div class="chat-input-area">
<div class="chat-input-row">
<button class="plus-btn" type="button" onclick="togglePlus()">+</button>
<button class="voice-toggle-btn" id="voiceToggle" type="button" onclick="toggleVoice()">🎤</button>
<input type="text" id="msgInput" placeholder="输入消息...">
<button class="hold-to-talk" id="holdToTalk" type="button">按住 说话</button>
<button class="send-btn" id="sendBtn" type="button">发送</button>
</div>
<div class="plus-panel" id="plusPanel">
<div class="plus-item"><div class="pi-icon">📹</div><div class="pi-label">视频通话</div></div>
<div class="plus-item"><div class="pi-icon">🖼️</div><div class="pi-label">照片</div></div>
<div class="plus-item"><div class="pi-icon">📷</div><div class="pi-label">拍摄</div></div>
<div class="plus-item"><div class="pi-icon">📍</div><div class="pi-label">发送位置</div></div>
</div>
</div>
<div class="bottom-spacer"></div>
<!-- [交互] 结束应急二次确认弹窗 -->
<div class="modal-overlay" id="endModal">
<div class="modal-box">
<div class="modal-title">确认结束应急</div>
<div class="modal-desc">请确定本次应急求助已完成后再执行该操作</div>
<textarea placeholder="请输入备注信息(选填)"></textarea>
<div class="modal-actions">
<button class="modal-cancel" onclick="hideEndModal()">取消</button>
<button class="modal-confirm" onclick="confirmEnd()">确认结束</button>
</div>
</div>
</div>
</div>
<script>
let isVoiceMode = false;
function toggleVoice() {
isVoiceMode = !isVoiceMode;
const toggleBtn = document.getElementById('voiceToggle');
const msgInput = document.getElementById('msgInput');
const holdBtn = document.getElementById('holdToTalk');
const sendBtn = document.getElementById('sendBtn');
if (isVoiceMode) {
toggleBtn.textContent = '⌨️';
msgInput.classList.add('hide');
sendBtn.classList.add('hide');
holdBtn.classList.add('show');
} else {
toggleBtn.textContent = '🎤';
msgInput.classList.remove('hide');
sendBtn.classList.remove('hide');
holdBtn.classList.remove('show');
}
}
function togglePlus() {
const plusPanel = document.getElementById('plusPanel');
plusPanel.classList.toggle('show');
}
function showEndModal() {
document.getElementById('endModal').classList.add('show');
}
function hideEndModal() {
document.getElementById('endModal').classList.remove('show');
}
function confirmEnd() {
hideEndModal();
history.back();
}
</script>
</body>
</html>
@@ -104,7 +104,6 @@
<div class="popup-card__unit">第四采油厂凤凰山医务室</div>
<div class="popup-card__actions">
<button class="popup-btn">联系电话</button>
<button class="popup-btn popup-btn--primary" >地图导航</button>
</div>
</div>
</div>
+1 -1
View File
@@ -452,7 +452,7 @@ function cardHTML(mod, uid) {
}).join('');
tagHtml = `<span class="module-card__tag module-card__tag--ready" data-pop="${uid}" onclick="event.stopPropagation();togglePop('${uid}')">${mod.pages.length} 页</span><div class="page-popover" id="pop-${uid}" onclick="event.stopPropagation()"><div class="page-popover__title">页面列表</div>${popItems}</div>`;
} else {
tagHtml = '<span class="module-card__tag module-card__tag--pending">待开发</span>';
tagHtml = '<span class="module-card__tag module-card__tag--pending">待设计</span>';
}
const body = has ? '' : `<div class="module-card__empty-hint">${ICONS.empty}暂无原型页面</div>`;