Compare commits

..
109 Commits
Author SHA1 Message Date
lianlonggang 4c30187435 feat(急救服务平台): 附近资源聚合展示与路径规划优化
- 新增 Map.createClusterOverlay:30m 内点位合并为带数字徽标的聚合
  marker,聚合点与独立 marker 按距离用户统一编号 1, 2, 3...
- 聚合徽标使用纯色圆 + 数量 N + 左上角位置编号角标,hover 显示簇内
  数量与最近资源名称
- 点击聚合 marker 弹 isCustom InfoWindow 列表,自带关闭按钮 + 选中
  触发原有路径规划回调
- 独立 marker 用 typeToIcon 纯 url 同步渲染,canvas 角标异步叠加
  失败不影响主渲染
- 修复 removeOverlayGroup 未关闭簇列表弹窗的 bug
- confirmHelp 加 createOverlay1 fallback,聚合层异常时回退原渲染
2026-09-16 09:52:47 +08:00
weixinandClaude Opus 4.7 9431553951 feat(新疆大屏): 新增体温异常预警提示音
- public:新增 temperature.mp3 体温异常告警音效
- src/utils/socket.ts:体温异常告警接入语音播报,播放提示音,停止告警时关闭音效

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-28 18:14:59 +08:00
lianlonggang 5293b509b2 fix(all): 去客户化处理 2026-08-06 18:01:00 +08:00
weixin f55544321b feat(应急就医): 新增摔倒预警提示音文件
- public:添加 fall_down.mp3 摔倒检测告警音效
2026-07-23 16:58:13 +08:00
weixin ecd1e5cd46 chore(config): 更新心理咨询热线
- 7*24小时心理咨询热线更新为400-6633-330
2026-07-01 10:55:27 +08:00
weixin a22c59f220 feat(应急就医):
三个预警的音效,心率和血氧只响一遍,一键报警不变
2026-06-29 15:21:41 +08:00
weixin c33ad0d807 feat(应急就医): WebSocket告警消息接入语音播报
心率异常/一键告警/血氧异常分别触发对应提示音,确认帮助后自动停止当前告警音效。
2026-06-17 14:43:29 +08:00
lianlonggang 7b6d6e52b2 fix(indexSun): 修复表单重置逻辑顺序问题
- 调整 getField() 调用时机以确保表单状态正确重置
- 优化表单记录和状态对象的初始化流程
- 确保字段获取在表单数据清空后执行
2026-04-14 10:47:15 +08:00
lianlonggang 336a881855 feat(indexSun): 添加年份输入字段
- 在表单中新增年份输入框配置
- 设置默认值为当前年份
- 使用 dayjs 获取当前年份作为默认值
- 配置字段类型为 input,标签显示为 年份
2026-04-14 10:34:39 +08:00
lianlonggang 9f6deb49a0 fix(healthConsultation): 修复医生头像图片加载问题
- 将 a-image 组件替换为原生 img 标签以解决图片显示问题
- 移除 ant-design-vue 的 Image 组件依赖
- 添加图片加载失败时的错误处理函数
- 设置默认头像图片路径并实现错误回退机制
- 更新图片获取逻辑使用新的默认头像资源
2026-04-10 18:23:06 +08:00
lianlonggang 4d9b1ac3cb 性别字典翻译问题修正 2026-04-10 12:38:05 +08:00
weixin e021bad791 update
1.修改新疆应急图例等问题
2026-01-22 14:48:32 +08:00
weixin 8095b6b503 update
1.关爱人员、急救人员名称超出后,替换为...
2026-01-21 09:15:52 +08:00
weixin 8cc8945b68 update
1.更换咨询大屏咨询电话
2026-01-15 17:48:14 +08:00
weixin bbb2cdc04e update 2026-01-08 16:49:21 +08:00
weixin c583f85516 update 2026-01-08 14:57:41 +08:00
weixin 849fc8778d update 2026-01-06 09:04:10 +08:00
weixin 49597a4dd2 update 2026-01-05 16:47:31 +08:00
weixin 1c908284e6 update 2025-12-29 16:41:28 +08:00
weixin 6241504095 update 2025-12-29 15:37:36 +08:00
17792275749 4f9d79461c 视频改为接口获取 2025-12-29 15:03:11 +08:00
17792275749 bb0536c7e4 首屏添加视频播放 2025-12-29 13:34:44 +08:00
weixin d95a6942cb update 2025-12-29 09:24:43 +08:00
weixin bbc5685be0 update
1.修改新疆反馈问题(12.24)
2.修改健康咨询大屏不定时刷新问题
2025-12-25 16:07:50 +08:00
weixin f943ee8df3 update
1.修改新疆反馈问题(12.24)
2025-12-25 09:40:20 +08:00
weixin 0b67be998a update 2025-12-24 17:10:59 +08:00
weixin 482d165651 update 2025-12-24 14:51:07 +08:00
weixin 822fecc3b1 update
1.根据新疆反馈(12.22),修改相关问题
2025-12-24 14:42:16 +08:00
weixin f2570ef494 update
1.根据新疆反馈(12.22),修改相关问题
2025-12-23 16:25:06 +08:00
weixin 8c9eafa2fb update
1.根据新疆反馈,修改相关问题
2025-12-17 14:02:31 +08:00
weixin ded845793a update
1.根据新疆反馈,修改相关问题
2025-12-17 11:16:06 +08:00
weixin 53195cc5c2 update
1.根据新疆反馈,修改相关问题
2025-12-16 18:46:11 +08:00
weixin 87490ae4b5 update
1.修改云坐席时,不显示地理位置相关
2.修改应急求助,单位部门字段互换
3.更新请求地址信息
2025-12-15 14:53:32 +08:00
weixin faa0fa991c update
1213问题修改
2025-12-13 19:00:02 +08:00
weixin 5589f92e72 update
1.修改规划路线时,路程计算问题
2025-12-04 16:25:56 +08:00
weixin 4db07874f5 update 2025-12-03 18:12:30 +08:00
weixin e09f90203f update 2025-12-03 17:46:49 +08:00
weixin 593c8bfe0b update
1.更换部分模块接口
2025-12-03 16:13:20 +08:00
weixin 61e83f6c78 update
1.修改“未获取位置信息”没有及时去掉问题
2025-11-24 17:51:11 +08:00
weixin 4e1c4b87c4 update
1.更新高德秘钥
2025-11-12 13:53:58 +08:00
weixin ea062a4ba9 update
1.替换高德地图key
2025-11-12 11:27:07 +08:00
lianlonggang cb8f23e9ab 处理咨询大屏图片地址问题 2025-11-05 17:46:46 +08:00
lianlonggang 9407ba979c 恢复代码 2025-11-05 16:42:12 +08:00
weixin 626fb84a6b update
1.更换首页图标
2025-11-05 16:36:16 +08:00
weixin c7f571399a update
1.调整首页图标及界面
2025-11-04 09:32:06 +08:00
weixin ef86526095 update
1.调整图标大小
2025-11-03 13:52:18 +08:00
weixin f617922cd3 update
1.应急事件显示信息过长,精简一下。
2.救护车图标配色调整
3.医院图标增大一点,显示图标及应急求助时的推荐图标
4.急救弹窗添加呼吸效果
5.急救路线搜索框,控制只搜索新疆境内
6.咨询大屏也要监听事件并弹出弹窗
2025-10-31 17:51:22 +08:00
lianlonggang f44e372210 错别字处理 2025-10-24 19:38:32 +08:00
weixin f12c73a911 update
1.修改血压为压力
2025-10-21 17:57:11 +08:00
weixin 17f9478233 update
1.修改跳转安眼逻辑
2025-10-14 14:45:13 +08:00
weixin 98ff86f89c update
1.添加新疆大屏新首页
2025-10-13 15:37:36 +08:00
lianlonggang 028cc03aeb docker替换变量,只在production环境进行处理 2025-10-13 14:53:54 +08:00
lianlonggang ba91d7ff13 主诉分类修改成应急求助信息 2025-10-13 14:12:07 +08:00
lianlonggang 5d2249d4c7 使用DOCKER环境变量替换网关链接 2025-10-13 14:10:33 +08:00
weixin 28c61a0731 update 2025-09-30 10:47:44 +08:00
huangzhen c9f075b106 健康咨询平台实时时间 2025-09-30 10:18:06 +08:00
huangzhen 7775b14bba 健康咨询平台实时时间 2025-09-30 10:11:54 +08:00
weixin e4d3e3ba1c update
1.修改大屏问题
2025-09-29 15:41:01 +08:00
weixin 7154314e93 update
1.修改大屏问题及添加新需求
2025-09-26 18:57:17 +08:00
huangzhen f024615260 Merge branch 'dev' of https://gitea.rk-health.com/renkang/xj-screen into dev 2025-09-25 14:57:47 +08:00
huangzhen d103785a45 健康咨询平台大屏 2025-09-25 14:57:31 +08:00
weixin c7abcf1f67 update
1.新需求2025.9.19新需求:
去掉"健康监测"副屏, 修改后健康终端报警点击查看详情弹出告警记录分页列表;
去掉【体检人数统计】【体检数据】【健康打卡数据】,增加【心血管监测】模块功能,【心血管监测】功能将原心血管监测大屏上的内容整理并移动到急救服务平台大屏中;
员工求助时,地图中推荐两个医院、两个健康小屋、两个AED坐标信息,并按距离排序,体现距离远近顺序;
地图中油田医院、健康小屋等各个图例,显示总数信息;
救护车坐标信息维护,并在大屏上标记;
右上角"增加【救助信息】板块, 当出现应急弹窗后或从今日应急选中应急事件 , 该板块需展示应急事件的相关信息(开发中);
2025-09-24 20:53:28 +08:00
weixin a97d9c64aa update
1.新疆大屏新需求
2025-09-19 18:34:40 +08:00
lianlonggang 1714ff19f5 使用DOCKER环境变量替换网关链接 2025-09-12 17:33:07 +08:00
xf bd2d066314 update: 大屏接口 2025-08-13 10:34:30 +08:00
xf 832291570f update: 隐藏服务详情周月年切换 2025-07-31 09:18:45 +08:00
weixin 3755349ea8 update
1.docker新疆配置更新
2.修改请求地址
2025-07-28 11:32:20 +08:00
weixin 68548362eb update
1.docker新疆配置
2025-07-25 10:53:47 +08:00
lianlonggang baec4e1ff0 使用临时域名 2025-07-25 10:04:20 +08:00
lianlonggang 0a59f01616 去掉新疆油田字眼 2025-07-24 15:55:46 +08:00
xf ff78173b25 update: 大屏接口 2025-07-15 09:05:21 +08:00
weixin a89cc758f7 update
1.优化
2025-07-11 09:05:03 +08:00
xf 8a386eba5b update: 大屏接口 2025-07-10 13:42:16 +08:00
xf b56efa9f7f update: 大屏接口 2025-07-10 10:08:28 +08:00
weixin 269e35d19b update
1.优化
2025-07-10 09:44:51 +08:00
weixin b61ca7f567 update
1.添加急救路线
2025-07-09 19:38:59 +08:00
xf b52d46cd76 update: 大屏接口 2025-07-09 18:48:47 +08:00
xf 4b8f3ae1ef update: 大屏接口 2025-07-09 18:31:19 +08:00
weixin cf129e6d23 update
1.优化
2025-07-09 17:33:51 +08:00
xf cd1b085499 update: 大屏接口 2025-07-09 17:30:39 +08:00
xf 055e28b457 update: 大屏接口 2025-07-09 17:18:00 +08:00
xf b0a16c6f0b update: 大屏接口 2025-07-09 15:20:59 +08:00
xf 91abcfd78d update: 大屏接口 2025-07-09 15:10:30 +08:00
weixin f6e5bb78da update
1.优化
2025-07-08 17:50:37 +08:00
xf 7565056a7e update: 大屏接口 2025-07-08 17:47:36 +08:00
weixin 726bd19ad3 update
1.修改部分问题(路线等问题)
2.添加定时器
2025-07-08 17:32:13 +08:00
xf 67a2ea75d1 update: 大屏接口 2025-07-08 17:20:31 +08:00
xf b55bc1857a update: 大屏接口 2025-07-08 17:12:55 +08:00
weixin 8a630dd1cd update
1.修改socket弹窗等
2025-07-08 16:00:47 +08:00
xf 07f14c7703 update: 大屏接口 2025-07-08 15:52:42 +08:00
xf a2da7e4be7 update: 大屏接口 2025-07-08 10:11:15 +08:00
weixin 01f3339833 update 2025-07-08 09:14:22 +08:00
xf aa71fd5c7e update: 新疆大屏 2025-07-06 18:03:41 +08:00
xf 86ed8341e2 update: 新疆大屏 2025-07-06 15:04:39 +08:00
xf 22c5b89f90 update: 初始化:新疆大屏基础搭建 2025-07-02 18:14:20 +08:00
xf 7e2346f5fb update: 初始化:新疆大屏基础搭建 2025-07-02 16:23:37 +08:00
xf ff493d83d9 update: 初始化:新疆大屏基础搭建 2025-07-02 13:48:50 +08:00
xf 1cbda373e0 update: 初始化 2025-07-01 10:05:17 +08:00
xf 0a6843406a update: 初始化 2025-06-30 11:10:12 +08:00
xf 8b9c3abf77 update: 初始化 2025-06-30 11:09:27 +08:00
weixin 2db1e6d33c update
1.青海大屏
2025-06-19 16:15:43 +08:00
weixin 25b8acdda6 update
1.修改websocket,多窗口可以同时弹窗
2025-05-23 14:16:01 +08:00
weixin 736ee91e93 update
1.手表监测弹窗上报时间/发生时间修改
2025-05-08 09:46:48 +08:00
weixin fd02cf131a update
1.修改手表监测来通知,更新页面数据
2025-04-27 15:24:55 +08:00
weixin 48a137133d update
1.右上角手表应急添加日月周累计(庆阳大屏)
2025-04-27 09:37:20 +08:00
weixin 5c3bf4a7ed update 2025-04-21 10:41:59 +08:00
weixin 35c5625e1b update
1.添加庆阳大屏柱状图
2025-04-21 10:24:47 +08:00
weixin 8dc44b465b update
1.修改查看健康终端报警,不需要时间筛选
2025-04-03 12:36:29 +08:00
weixin 58d3b5a933 update
1.修改table模块,改回原有逻辑
2025-03-25 15:34:10 +08:00
194 changed files with 31455 additions and 8918 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# 王昊地址
#VITE_GLOB_API_URL=http://192.168.1.27
#VITE_GLOB_API_URL=http://192.168.1.98
#开发环境
VITE_GLOB_API_URL=http://cqyt.dev.yg.dt.io
VITE_GLOB_API_URL=http://api-xj.tri.dt.io
#VITE_GLOB_API_URL=https://api.cqygjk.com
#健康监测工具报警-websocket
#VITE_GLOB_API_YIN_JI=ws://cqyt.dev.yg.dt.io/health-watch/websocket/watchMonitor
+1 -10
View File
@@ -1,12 +1,3 @@
#后台地址
VITE_GLOB_API_URL=https://api.cqygjk.com
#健康监测工具报警-websocket
VITE_GLOB_API_YIN_JI=wss://api.cqygjk.com/health-watch/websocket/watchMonitor
#应急求助-websocket
VITE_GLOB_API_JIAN_CE=wss://api.cqygjk.com/health-emergency/websocket/emergency
#西安云坐席
VITE_GLOB_API_XIAN_YUN=wss://api.cqygjk.com/health-emergency/websocket/emergency3
#银川云坐席
VITE_GLOB_API_YIN_CHUAN=wss://api.cqygjk.com/health-emergency/websocket/emergency4
VITE_GLOB_API_URL=http://api-xj.tri.dt.io
+32
View File
@@ -0,0 +1,32 @@
FROM nginx
MAINTAINER jeecgos@163.com
VOLUME /tmp
ENV LANG en_US.UTF-8
ENV VITE_GLOB_API_URL http://api-xj.tri.dt.io
RUN echo "server { \
#解决Router(mode: 'history')模式下,刷新路由地址不能找到页面的问题 \
location / { \
root /var/www/html/; \
index index.html index.htm; \
if (!-e \$request_filename) { \
rewrite ^(.*)\$ /index.html?s=\$1 last; \
break; \
} \
} \
access_log /var/log/nginx/access.log ; \
} " > /etc/nginx/conf.d/default.conf \
&& mkdir -p /var/www \
&& mkdir -p /var/www/html
ADD dist/ /var/www/html/
# 👇 新增:复制启动脚本(用于运行时替换环境变量)
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# 使用脚本作为入口点
ENTRYPOINT ["bash","/entrypoint.sh"]
EXPOSE 80
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# 设置默认值(构建时 .env 不生效时兜底)
: ${VITE_GLOB_API_URL:=https://api.xjygjk.com}
# 替换 index.html 中的占位符
sed -i "s@__VITE_GLOB_API_URL__@$VITE_GLOB_API_URL@g" /var/www/html/index.html
exec nginx -g 'daemon off;'
+10 -3
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="zh">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/screen.svg"/>
@@ -7,11 +7,18 @@
<title>应急就医服务中心</title>
<!-- 全局配置 -->
<script>
window._CONFIG = {};
// // 默认值(可选)
// window.__APP_ENV__ = {
// VITE_GLOB_API_URL: '__VITE_GLOB_API_URL__', // 占位符,Docker 替换
// };
window._AMapSecurityConfig = {
securityJsCode: "1b3b86585c863d22db1e7f7636a9e495",
securityJsCode: "54f1d830d2bb225c173b76f5780b65eb",
}
</script>
<!-- __APP_ENV__ 由 Vite 在构建时注入 -->
<!-- 开发环境:不注入或注入空值 -->
<!-- 生产环境:注入真实值(或占位符供 Docker 替换) -->
<!-- __INJECT_APP_ENV__ -->
</head>
<style>
html, body, #app {
+19331
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -5,6 +5,8 @@
"type": "module",
"scripts": {
"dev": "vite --open",
"pro": "vite --open --mode production",
"test": "vite --open --mode test",
"build": "vite build",
"build:dev": "vite build --mode dev",
"build:test": "vite build --mode test",
@@ -18,7 +20,10 @@
"@turf/turf": "^6.5.0",
"autoprefixer": "^10.4.16",
"axios": "^1.6.2",
"cnpm": "^9.4.0",
"dayjs": "^1.11.10",
"echarts": "5.4.3",
"echarts-gl": "^2.0.9",
"jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
@@ -30,6 +35,7 @@
"postcss-px2rem": "^0.3.0",
"prettier": "^3.1.0",
"qs": "^6.11.2",
"swiper": "^12.0.2",
"vue-router": "^4.2.5",
"vue3-seamless-scroll": "^2.0.1"
},
@@ -40,9 +46,8 @@
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"ant-design-vue": "^3.2.20",
"echarts": "^5.4.3",
"eslint": "^8.51.0",
"eslint-config-prettier": "^8.10.0",
"eslint-define-config": "^1.24.1",
@@ -52,7 +57,7 @@
"jest": "^29.7.0",
"less": "^4.2.0",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite": "^5.4.20",
"vite-plugin-resolve-externals": "^0.2.2",
"vue-tsc": "^1.8.5"
}
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+6 -10
View File
@@ -10,25 +10,21 @@
import { onMounted, watch } from 'vue';
import { getEmergencyCenterApi } from '/@/api/user.ts';
import { useUserStore } from '/@/store/modules/user.ts';
import { useRouter } from 'vue-router';
import { getBaseLogin } from '/@/enum/pageEnum.ts';
import { openTestModal } from '/@/utils/utils.ts';
dayjs.locale('zh-cn');
const locle = zhCN;
const userStore = useUserStore();
const router = useRouter();
onMounted(async () => {
openTestModal();
try {
if (userStore.getToken && getBaseLogin() === '/centralScreenLogin') {
const { result } = await getEmergencyCenterApi();
if (result?.screenPath) {
router.replace(result?.screenPath);
} else {
router.replace('centralScreen');
if (userStore.getToken) {
const { code, result } = await getEmergencyCenterApi();
if (code === 200) {
userStore.setCenterInfo(result);
}
userStore.setCenterInfo(result);
}
} catch (e) {}
if (userStore.getUserInfo) {
+16 -2
View File
@@ -31,10 +31,8 @@ service.interceptors.response.use(
},
(error) => {
if (error.response) {
console.log('response', error.response);
switch (error.response.data.code) {
case 401:
// debugger;
let path = getBaseLogin();
if (!path) {
path = toLoginByNowPath(router.currentRoute.value.path);
@@ -64,6 +62,22 @@ export const get = (url, params = {}) => {
});
});
};
export const Dictget = (url, params = {}) => {
return new Promise((resolve, reject) => {
service
.get(url, { params: params })
.then((res) => {
if (res.status === 200) {
resolve(res.data);
} else {
reject(res.data);
}
})
.catch((err) => {
reject(err.data);
});
});
};
export const post = (url: string, params: any) =>
new Promise((resolve, reject) => {
service
+5
View File
@@ -2,6 +2,7 @@ import { get, post } from '/@/api/request.ts';
enum Api {
getInputCode = '/sys/randomImage',
videoApi = '/health-emergency/api/anon/screen-animation/query',
Login = '/sys/login',
Logout = '/sys/logout',
GetUserInfo = '/sys/user/getUserInfo',
@@ -9,8 +10,12 @@ enum Api {
emergencyCenter = '/health-emergency/emergency/center/getCenterDetail',
}
export const getCodeInfo = (params: any) => get(Api.getInputCode + `/${params}`);
export const getVideo = () => get(Api.videoApi);
export function loginApi(params: any) {
return post(Api.Login, params);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="72.000000pt" height="176.000000pt" viewBox="0 0 72.000000 176.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,176.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M269 1715 c-107 -34 -194 -124 -225 -233 -43 -146 37 -312 185 -380
45 -22 71 -27 131 -27 130 1 234 66 292 183 63 127 39 271 -62 372 -82 82
-218 118 -321 85z m151 -190 c5 -38 7 -40 45 -43 22 -2 43 -6 47 -10 4 -4 8
-51 9 -105 1 -53 5 -100 10 -103 5 -3 9 -14 9 -25 0 -18 -9 -19 -165 -20 -91
-1 -171 -2 -177 -3 -23 -2 -21 41 1 47 19 5 21 13 21 95 0 49 3 117 6 151 l7
62 91 -3 91 -3 5 -40z"/>
<path d="M298 1536 c-24 -18 -23 -39 3 -62 22 -19 43 -13 63 19 8 12 5 21 -8
36 -22 24 -34 26 -58 7z"/>
<path d="M431 1366 c2 -102 18 -126 21 -32 1 36 2 76 2 88 1 12 -5 23 -12 26
-10 3 -13 -15 -11 -82z"/>
<path d="M267 1424 c-14 -15 6 -24 54 -24 36 0 49 4 47 13 -5 13 -89 23 -101
11z"/>
<path d="M272 1358 c5 -15 87 -18 91 -3 1 6 -20 11 -47 13 -36 2 -47 0 -44
-10z"/>
<path d="M283 1313 c-7 -2 -13 -9 -13 -14 0 -12 87 -12 94 0 11 17 -49 27 -81
14z"/>
<path d="M345 718 c-8 -308 -5 -393 15 -393 12 0 16 16 18 75 5 114 -9 560
-18 560 -4 0 -11 -109 -15 -242z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64">
<defs>
<radialGradient id="a" cx="32" cy="32" r="32" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="red" stop-opacity="0"/>
<stop offset="1" stop-color="red"/>
</radialGradient>
<radialGradient id="b" cy="32" r="20" xlink:href="#a"/>
</defs>
<g data-name="图层 2">
<g data-name="图层 1">
<circle cx="32" cy="32" r="6" style="fill:url(#a);opacity:1">
<animate attributeName="r" from="6" to="32" begin="0s" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" from="1" to="0" begin="0s" dur="2s" repeatCount="indefinite"/>
</circle>
<circle cx="32" cy="32" r="6" style="fill:url(#b);opacity:1">
<animate attributeName="r" from="6" to="32" begin="1s" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" from="1" to="0" begin="1s" dur="2s" repeatCount="indefinite"/>
</circle>
<circle cx="32" cy="32" r="6" style="fill:red"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

+11 -8
View File
@@ -8,7 +8,6 @@ html, body, #app {
}
.type-time {
padding-top: 3%;
font-size: 16px;
display: flex;
justify-content: right;
@@ -116,8 +115,9 @@ html, body, #app {
// 下拉框样式-start
.ant-select {
color: var(--antSelectFontColor) !important;
background-color: var(--antSelectBg);
color: #fff !important;
background-color: #082147 !important;
border-radius: 4px !important;
}
.ant-select-dropdown {
@@ -127,14 +127,15 @@ html, body, #app {
}
.ant-select-item {
color: #1b7ef2 !important;
color: #fff !important;
}
//框背景色
.ant-select:not(.ant-select-customize-input) .ant-select-selector {
color: var(--antSelectFontColor) !important;
background-color: transparent !important;
border: 1px solid var(--antSelectDropdownBorderColor) !important;
border: 1px solid transparent !important;
border-radius: 4px !important;
}
.ant-select-item-option-selected {
@@ -214,7 +215,7 @@ html, body, #app {
.ant-modal-title {
color: #576BAD !important;
background: url('/@/assets/images/dialog-light.png') no-repeat;
//background: url('/@/assets/images/dialog-light.png') no-repeat;
background-size: 100% 100%;
}
}
@@ -230,8 +231,10 @@ html, body, #app {
.ant-modal-title {
color: #ffffff !important;
background: url('/@/assets/images/dialog.png') no-repeat;
background-size: 100% 100%;
//background: url('/@/assets/images/dialog.png') no-repeat;
//background-size: 100% 100%;
background: #082147;
border: 1px solid #00CCFF;
}
}
+376 -23
View File
@@ -1,5 +1,4 @@
import { mapIcon1, mapIcon2, mapIcon3, mapIcon4, mapIcon7 } from '/@/components/chinaMap/chinaHooks.ts';
import { watchIcon } from '/@/components/secondaryScreen/secondMap/secondMapHooks.ts';
import { mapIcon1, mapIcon11, mapIcon3, mapIcon4, mapIcon41, mapIcon5, mapIcon51, watchIconR } from '/@/components/chinaMap/chinaHooks.ts';
import * as turf from '@turf/turf';
/**
@@ -18,29 +17,53 @@ export function createIcon({ icon, iconSize, imageSize }) {
/**
* @desc 根据类型创建Icon
* @type 1:油田医院,2:合作医院,3:医疗点
* @type 1:医院,2:健康小屋,4:AED
*/
export function typeToIcon(type: string, centerResource: boolean) {
const iconSize = [18, 44];
const imageSize = [18, 44];
export function typeToIcon(type: string) {
// const iconSize = [28, 49];
// const imageSize = [28, 49];
if (type == '1') {
return createIcon({ icon: mapIcon1, iconSize, imageSize });
}
if (type == '2') {
return createIcon({ icon: mapIcon4, iconSize, imageSize });
}
//关联医疗点
if (type == '3' && centerResource) {
return createIcon({ icon: mapIcon7, iconSize, imageSize });
// return createIcon({ icon: mapIcon1, iconSize, imageSize });
return mapIcon1;
}
if (type == '3') {
return createIcon({ icon: mapIcon2, iconSize, imageSize });
// return createIcon({ icon: mapIcon5, iconSize, imageSize });
return mapIcon5;
}
if (type == '5' || type == '4') {
if (type == '4') {
// return createIcon({ icon: mapIcon4, iconSize, imageSize });
return mapIcon4;
}
if (type == '5') {
// return createIcon({ icon: mapIcon3, iconSize, imageSize });
return mapIcon3;
}
if (type === 'user') {
// return createIcon({ icon: watchIconR, iconSize, imageSize });
return watchIconR;
}
}
export function typeToIcon1(type: string) {
const iconSize = [21, 52];
const imageSize = [21, 52];
if (type == '1') {
const iconS = [28, 65];
const imageS = [28, 65];
return createIcon({ icon: mapIcon11, iconSize: iconS, imageSize: imageS });
}
if (type == '3') {
return createIcon({ icon: mapIcon51, iconSize, imageSize });
}
if (type == '4') {
return createIcon({ icon: mapIcon41, iconSize, imageSize });
}
if (type == '5') {
return createIcon({ icon: mapIcon3, iconSize, imageSize });
}
if (type == '6') {
return createIcon({ icon: watchIcon, iconSize: [23, 51], imageSize: [23, 51] });
if (type === 'user') {
const iconS = [36, 88];
const imageS = [36, 88];
return createIcon({ icon: watchIconR, iconSize: iconS, imageSize: imageS });
}
}
@@ -69,14 +92,17 @@ export function createCircle(option) {
export function getLonLat(result = []) {
let arr = [];
for (let i = 0; i < result.length; i++) {
let { lon, lat, lng } = result[i];
if (lng) {
lon = lng;
let { lon, lat, lng, longitude, latitude } = result[i];
if (longitude) {
lon = longitude;
}
if (!lon || !lat) {
if (latitude) {
lat = latitude;
}
if (!longitude || !latitude) {
continue;
}
let point = turf.point([lon, lat]);
let point = turf.point([lon * 1, lat * 1]);
arr.push(point);
}
return arr;
@@ -96,3 +122,330 @@ export function getCenter(list = []) {
let centerObj = turf.center(features);
return centerObj?.geometry?.coordinates;
}
/**
* @desc 球面距离(haversine),返回米
* @param lng1 经度1
* @param lat1 纬度1
* @param lng2 经度2
* @param lat2 纬度2
*/
export function haversine(lng1: number, lat1: number, lng2: number, lat2: number): number {
const toRad = (d: number) => (d * Math.PI) / 180;
const R = 6371008.8; // 地球平均半径,单位米
const dLat = toRad(lat2 - lat1);
const dLng = toRad(lng2 - lng1);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* @desc 单链聚合:距离阈值(米)内的点归为同一簇;簇内不足2点返回 null
* @param items 待聚合的点位(必须含 lon/lat 经纬度)
* @param radiusMeters 聚合半径,单位米,默认 30
*/
export function clusterByRadius(items: any[] = [], radiusMeters = 30): any[][] {
const clusters: any[][] = [];
const visited = new Array(items.length).fill(false);
for (let i = 0; i < items.length; i++) {
if (visited[i]) continue;
const seed = items[i];
const seedLng = Number(seed.lon ?? seed.longitude ?? seed.lng);
const seedLat = Number(seed.lat ?? seed.latitude);
if (!seedLng || !seedLat) continue;
const cluster: any[] = [seed];
visited[i] = true;
for (let j = i + 1; j < items.length; j++) {
if (visited[j]) continue;
const cur = items[j];
const curLng = Number(cur.lon ?? cur.longitude ?? cur.lng);
const curLat = Number(cur.lat ?? cur.latitude);
if (!curLng || !curLat) continue;
const d = haversine(seedLng, seedLat, curLng, curLat);
if (d <= radiusMeters) {
cluster.push(cur);
visited[j] = true;
}
}
// 不足 2 个点不形成聚合(保留为独立 marker)
if (cluster.length >= 2) {
clusters.push(cluster);
}
}
return clusters;
}
/**
* @desc 计算簇的几何中心(取所有点的经纬度平均)
*/
export function getClusterCenter(cluster: any[] = []): [number, number] {
if (!cluster.length) return [0, 0];
let sumLng = 0;
let sumLat = 0;
let count = 0;
for (const item of cluster) {
const lng = Number(item.lon ?? item.longitude ?? item.lng);
const lat = Number(item.lat ?? item.latitude);
if (!lng || !lat) continue;
sumLng += lng;
sumLat += lat;
count++;
}
if (!count) return [0, 0];
return [sumLng / count, sumLat / count];
}
/**
* @desc 按数量返回聚合徽标的颜色
* @param count 簇内点数
*/
export function getClusterColor(count: number): { bg: string; border: string } {
if (count >= 10) return { bg: '#E74C3C', border: '#B83A2D' };
if (count >= 5) return { bg: '#F5A623', border: '#C77F12' };
return { bg: '#3FA9F5', border: '#2380C7' };
}
/**
* @desc 生成聚合 marker 的 HTML 内容(纯色圆形 + 数量 N + 左上角位置编号角标)
* @param count 簇内点数(主圈中心数字)
* @param items 簇内所有点位,用于挑最近资源并生成 title 提示
* @param userLocation [lng, lat] 用户位置,用于挑最近资源
* @param clusterNumber 聚合点本身的位置编号(按距离用户升序),左上角小角标显示;不传则不显示
*/
export function buildClusterIcon(
count: number,
items: any[] = [],
userLocation?: [number, number],
clusterNumber?: number
): string {
// 挑选最近的资源(仅用于 title 提示)
let nearest: any = items[0] || {};
if (userLocation && items.length > 1) {
let minDist = Infinity;
for (const it of items) {
const lng = Number(it.lon ?? it.longitude ?? it.lng);
const lat = Number(it.lat ?? it.latitude);
if (!lng || !lat) continue;
const d = haversine(userLocation[0], userLocation[1], lng, lat);
if (d < minDist) {
minDist = d;
nearest = it;
}
}
}
const { bg, border } = getClusterColor(count);
const nearestName = nearest.name || typeToLabel(nearest.type);
const tip = `附近 ${count} 个资源,最近:${nearestName}`;
// 左上角位置编号角标(仅当传了 clusterNumber 才显示)
const badgeHtml =
clusterNumber !== undefined && clusterNumber !== null
? `<div style="
position: absolute;
top: -6px;
left: -6px;
min-width: 22px;
height: 22px;
padding: 0 6px;
line-height: 22px;
border-radius: 11px;
background: #E74C3C;
border: 2px solid #fff;
color: #fff;
text-align: center;
font-weight: 700;
font-size: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
">${clusterNumber}</div>`
: '';
// 主圈:纯色圆 + 数量 N 在中心
return `
<div class="cluster-marker" title="${tip}" style="
position: relative;
width: 44px;
height: 44px;
cursor: pointer;
user-select: none;
">
<div style="
position: absolute;
left: 0;
top: 0;
width: 44px;
height: 44px;
line-height: 44px;
text-align: center;
border-radius: 50%;
background: ${bg};
border: 3px solid ${border};
color: #fff;
font-weight: 700;
font-size: 18px;
box-shadow: 0 2px 6px rgba(0,0,0,0.35);
">${count}</div>
${badgeHtml}
</div>`;
}
/**
* @desc 异步创建带数字角标的图标:图片加载完成后绘制到 canvas,再叠加红色数字角标
* @param originalIconUrl 原始图标 url(来自 typeToIcon
* @param text 角标文字(数字 1/2/3/...
* @param type 类型:1 医院 / 3 健康小屋 / 4 AED / 5 救护车,影响 canvas 尺寸
*/
export function createNumberedMarkerIcon(originalIconUrl: string, text: string | number, type: string | number): Promise<string> {
return new Promise((resolve) => {
// 异常兜底:url 为空直接返回空串,让调用方决定
if (!originalIconUrl) return resolve('');
// 兜底:不在浏览器环境(SSR / 测试),返回原 url
if (typeof window === 'undefined' || typeof document === 'undefined') {
return resolve(originalIconUrl);
}
const width = type == 1 ? 40 : 28;
const height = type == 1 ? 69 : 49;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height + 15;
const ctx = canvas.getContext('2d');
if (!ctx) return resolve(originalIconUrl);
const img = new Image();
// 不设置 crossOriginVite 的本地资源是 blob URL,无需 CORS
// 设置 crossOrigin=anonymous 在某些环境下反而触发 onerror。
img.onload = () => {
try {
ctx.drawImage(img, 0, 10, width, height);
const textX = type == 1 ? 32 : 22;
const textY = 11;
const textStr = String(text);
const textWidth = ctx.measureText(textStr).width;
const circleRadius = Math.max(textWidth / 2 + 1, 6);
ctx.beginPath();
ctx.arc(textX, textY, circleRadius, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
ctx.font = 'bold 10px Arial';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#ffffff';
ctx.fillText(textStr, type == 1 ? 35 : 24.5, 10.8);
resolve(canvas.toDataURL());
} catch (e) {
// canvas 污染等异常:降级到原 url
resolve(originalIconUrl);
}
};
img.onerror = () => resolve(originalIconUrl);
img.src = originalIconUrl;
// 兜底:5 秒未触发 onload/onerror 也降级
setTimeout(() => resolve(originalIconUrl), 5000);
});
}
/**
* @desc 类型编号 → 中文标签(用于簇列表)
* @param type 1 医院 / 3 健康小屋 / 4 AED / 5 救护车
*/
export function typeToLabel(type: string | number): string {
const map: Record<string, string> = {
'1': '医院',
'3': '健康小屋',
'4': 'AED',
'5': '救护车',
};
return map[String(type)] || '资源';
}
/**
* @desc 生成簇列表 InfoWindow 的 HTML 内容
* @param items 簇内点位
* @param userLocation [lng, lat] 用户位置,用于计算直线距离并排序
*/
export function buildClusterInfoContent(items: any[] = [], userLocation?: [number, number]): string {
// 计算直线距离并按距离升序
const list = items.map((it) => {
const lng = Number(it.lon ?? it.longitude ?? it.lng);
const lat = Number(it.lat ?? it.latitude);
let distance = Number(it.distance);
if (!distance && userLocation) {
distance = haversine(userLocation[0], userLocation[1], lng, lat);
}
return { ...it, _distance: distance || 0, _lng: lng, _lat: lat };
});
list.sort((a, b) => a._distance - b._distance);
const formatDist = (m: number) => (m >= 1000 ? `${(m / 1000).toFixed(2)} 公里` : `${Math.round(m)}`);
const rows = list
.map(
(it, idx) => `
<div class="cluster-row" data-idx="${idx}" style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
cursor: pointer;
border-bottom: 1px solid rgba(255,255,255,0.1);
transition: background 0.2s;
">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #3FA9F5;
flex-shrink: 0;
"></span>
<span style="color: #fff; font-size: 14px;">${it.name || typeToLabel(it.type)}</span>
<span style="color: rgba(255,255,255,0.6); font-size: 12px;">${typeToLabel(it.type)}</span>
</div>
<span style="color: rgba(255,255,255,0.85); font-size: 13px;">${formatDist(it._distance)}</span>
</div>`
)
.join('');
return `
<div class="cluster-info-window" style="
position: relative;
width: 260px;
background: rgba(15, 32, 60, 0.95);
border-radius: 8px;
padding: 12px 0;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
box-shadow: 0 4px 16px rgba(0,0,0,0.35);
box-sizing: border-box;
">
<button class="cluster-info-close" type="button" style="
position: absolute;
top: 6px;
right: 6px;
width: 22px;
height: 22px;
line-height: 18px;
text-align: center;
border-radius: 50%;
background: rgba(255,255,255,0.15);
border: 0;
color: #fff;
font-size: 16px;
cursor: pointer;
padding: 0;
font-family: inherit;
">×</button>
<div style="
padding: 0 36px 8px 14px;
font-size: 14px;
font-weight: 700;
border-bottom: 1px solid rgba(255,255,255,0.15);
">附近 ${items.length} 个资源</div>
<div class="cluster-rows">${rows}</div>
<div style="
padding: 6px 14px 0;
font-size: 12px;
color: rgba(255,255,255,0.5);
">点击列表项查看详情并规划路线</div>
</div>`;
}
+515 -27
View File
@@ -1,14 +1,28 @@
import AMapLoader from '@amap/amap-jsapi-loader';
import china from '/@/assets/lngLat/china.ts';
import chinaInner from '/@/assets/lngLat/chinaInner.ts';
import { createCircle, createIcon, getCenter, getLonLat, typeToIcon } from '/@/assets/mapUtils/commonFun.ts';
import { aircraftContent, alarmIcon, female, male } from '/@/components/chinaMap/chinaHooks.ts';
import {
buildClusterIcon,
buildClusterInfoContent,
clusterByRadius,
createCircle,
createIcon,
createNumberedMarkerIcon,
getCenter,
getClusterCenter,
getLonLat,
haversine,
typeToIcon,
typeToIcon1,
} from '/@/assets/mapUtils/commonFun.ts';
import { aircraftContent, alarmIcon, female, male, watchIconR } from '/@/components/chinaMap/chinaHooks.ts';
import { isFunction } from '/@/utils/is.ts';
import { Callback, ExtDataType, InitMap, MarkerOption, ResultType } from '/@/assets/mapUtils/mapFunTypes.ts';
import { defaultOption, getZoom, mapKey, mapPlugin } from '/@/assets/mapUtils/mapConstant.ts';
let AMap = null;
export const Map = {
aMap: null,
map: null,
overlayGroup: null, // 地图中marker
circleGroup: null, //直升机背景圆
@@ -19,6 +33,16 @@ export const Map = {
infoWindow: null, //地图弹窗
driveLine: null, // 导航轨迹线
emergencyMarker: null, // 应急人员marker
startMarker: null,
endMarker: null,
driving: null,
walking: null,
mapDriverOrWalkLoading: false,
layerGroup: null,
layerData: null,
clusterInfoWindow: null, // 簇列表弹窗(聚合 marker 点击后展示)
clusterRadius: 30, // 聚合半径,单位米
clusterUserLocation: null, // 用户位置 [lng, lat],用于列表显示距离
/**
* @desc 地图舒适化
* @param el dom元素 id选择器
@@ -34,6 +58,7 @@ export const Map = {
plugins: mapPlugin, // 需要使用的的插件列表,如比例尺'AMap.Scale'等
}).then((res) => {
AMap = res;
this.aMap = res;
const { zoom, center, mapStyle } = { ...defaultOption, ...option };
//基本地图加载
this.map = new AMap.Map(el, {
@@ -53,6 +78,18 @@ export const Map = {
this.drawOutLine();
}
});
const satellite = new AMap.TileLayer.Satellite({
zIndex: 11,
});
const roadNet = new AMap.TileLayer.RoadNet({
zIndex: 12,
});
this.layerData = { satellite, roadNet };
this.layerGroup = new AMap.LayerGroup([satellite, roadNet]);
this.layerGroup.setMap(this.map);
satellite.hide();
roadNet.hide();
resolve();
});
});
@@ -125,6 +162,11 @@ export const Map = {
}
this.clearActiveHospital();
this.clearInfoModal();
// 关闭簇列表弹窗(聚合 marker 列表弹窗)
if (this.clusterInfoWindow) {
this.clusterInfoWindow.close();
this.clusterInfoWindow = null;
}
// this.map.remove(this.airMarkerList);
}, //
/**
@@ -134,17 +176,22 @@ export const Map = {
* @param callback
* @param computedCenter 是否计算中心点
*/
createOverlay(result: ResultType[], extData?: ExtDataType, callback?: Callback, computedCenter?: boolean) {
createOverlay(result: ResultType[], extData?: ExtDataType, callback?: Callback, computedCenter?: boolean, iconType?: string) {
this.removeOverlayGroup();
this.overlayGroup = new AMap.OverlayGroup();
let list = getLonLat(result);
let center = getCenter(list);
let zoom = getZoom(list);
if (computedCenter) {
this.setZoomCenter(center, zoom);
}
// const list: any[] = getLonLat(result);
// const center = getCenter(list);
// const zoom = getZoom(list);
let arr: any[] = [];
for (let i = 0; i < result.length; i++) {
let { type, lon, lat, lng, centerResource } = result[i];
let { type, lon, lat, longitude, latitude, lng, centerResource } = result[i];
if (longitude) {
lon = longitude;
}
if (latitude) {
lat = latitude;
}
if (lng) {
lon = lng;
}
@@ -152,7 +199,7 @@ export const Map = {
continue;
}
// 创建一个 Icon
let startIcon = typeToIcon(type, centerResource);
let startIcon = typeToIcon1(type);
// 将 icon 传入 marker
let startMarker = new AMap.Marker({
// 点的坐标
@@ -167,6 +214,121 @@ export const Map = {
},
...extData?.extOption,
});
arr.push(startMarker);
// 可以调用setExtData()设置自定义属性
//将组 挂载到地图上
this.overlayGroup && this.overlayGroup.setMap(this.map);
//将需要批量控制的点放到一个组中,通过控制组,就可以操作组内所有所有覆盖物
this.overlayGroup && this.overlayGroup.addOverlay(startMarker);
}
this.overlayGroup &&
this.overlayGroup.on('click', function (e: any) {
//拿到点中携带的数据
if (!isFunction(callback)) return;
callback(e.target.getExtData());
});
if (computedCenter) {
this.map && this.map.setFitView(arr, false, [50, 50, 0, 50]);
}
return arr;
},
/**
* @desc 创建覆盖物组,用于批量处理点 (控制显隐,点击事件等)
* @param result
* @param extData
* @param callback
* @param computedCenter 是否计算中心点
*/
async createOverlay1(result: ResultType[], extData?: ExtDataType, callback?: Callback, computedCenter?: boolean, iconType?: string) {
this.removeOverlayGroup();
this.overlayGroup = new AMap.OverlayGroup();
const list: any[] = getLonLat(result);
const center = getCenter(list);
const zoom = getZoom(list);
if (computedCenter) {
this.setZoomCenter(center, zoom);
}
function createTextOverlayIcon(originalIconUrl, text, type) {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () {
const width = type == 1 ? 40 : 28;
const height = type == 1 ? 69 : 49;
// 设置画布大小(比原图稍大以容纳文字)
canvas.width = width;
canvas.height = height + 15; // 为文字留出空间
// 绘制原始图标
ctx.drawImage(img, 0, 10, width, height);
// 绘制圆形文字背景
const textX = type == 1 ? 32 : 22; // 文字X坐标(居右对齐)
const textY = 11; // 文字Y坐标
const textWidth = ctx.measureText(text).width;
const circleRadius = Math.max(textWidth / 2 + 1, 6); // 根据文字宽度计算半径
ctx.beginPath();
ctx.arc(textX, textY, circleRadius, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
// 绘制文字背景(可选)
// ctx.fillStyle = 'red';
// ctx.fillRect(0, 0, 28, 15);
// 绘制文字
ctx.font = `bold 10px Arial`;
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#ffffff';
ctx.fillText(text, type == 1 ? 35 : 24.5, 10.8);
resolve(canvas.toDataURL());
};
img.src = originalIconUrl;
});
}
let arr = [];
for (let i = 0; i < result.length; i++) {
let { type, lon, lat, longitude, latitude, lng, centerResource } = result[i];
if (longitude) {
lon = longitude;
}
if (latitude) {
lat = latitude;
}
if (lng) {
lon = lng;
}
if (!lon || !lat || !type) {
continue;
}
// 创建一个 Icon
const startIcon: any = typeToIcon(type);
const newIconUrl = await createTextOverlayIcon(startIcon, i + 1 + '', result[i]?.type);
// 将 icon 传入 marker
const startMarker = new AMap.Marker({
// 点的坐标
position: [lon, lat], // 指定点的icon,也可以是个url ,但这样就不能对图片进行设置了
icon: newIconUrl, //启用点击事件 如果需要点击事件,必须开启
clickable: true, //点额外携带的数据,用户自定义属性,支持JavaScript API任意数据类型
offset: [-18, -32],
maxZoom: 14,
extData: result[i],
label: {
content: isFunction(extData?.content) && extData?.content(result[i]),
},
zIndex: 99,
...extData?.extOption,
});
arr.push(startMarker);
// 可以调用setExtData()设置自定义属性
//将组 挂载到地图上
this.overlayGroup.setMap(this.map);
@@ -179,6 +341,228 @@ export const Map = {
if (!isFunction(callback)) return;
callback(e.target.getExtData());
});
return arr;
},
/**
* @desc 创建带聚合的覆盖物组:距离阈值内的点位合并为聚合 marker(最近资源图标+数字角标),
* 点击聚合 marker 弹出 InfoWindow 展示簇内点位列表;簇外点位独立渲染。
* @param result 待渲染的点位(医院/医疗点/AED 等)
* @param extData 与 createOverlay1 一致,content/extOption 用于 label 等
* @param callback 单个点位被选中时的回调(被聚合列表选中也会调用,签名相同)
* @param computedCenter 是否计算视野中心
* @param iconType 同 createOverlay1(保留参数占位)
* @param radius 聚合半径,单位米,默认 30
* @param userLocation 用户位置 [lng, lat],用于挑选最近资源 + 簇列表显示距离
*/
async createClusterOverlay(
result: ResultType[],
extData?: ExtDataType,
callback?: Callback,
computedCenter?: boolean,
iconType?: string,
radius?: number,
userLocation?: [number, number]
) {
this.removeOverlayGroup();
this.overlayGroup = new AMap.OverlayGroup();
// 关闭上一次的簇列表弹窗
if (this.clusterInfoWindow) {
this.clusterInfoWindow.close();
this.clusterInfoWindow = null;
}
const r = radius || this.clusterRadius || 30;
if (userLocation) {
this.clusterUserLocation = userLocation;
}
// 过滤掉缺少经纬度或类型的点
const validItems: any[] = [];
for (const item of result || []) {
const lon = Number(item.lon ?? item.longitude ?? item.lng);
const lat = Number(item.lat ?? item.latitude);
const type = item.type;
if (!lon || !lat || !type) continue;
validItems.push({ ...item, lon, lat });
}
// 单链聚类(30m 内合并)
const clusters = clusterByRadius(validItems, r);
const clusteredSet = new Set();
for (const c of clusters) {
for (const item of c) clusteredSet.add(item);
}
const singles = validItems.filter((it) => !clusteredSet.has(it));
const allMarkers: any[] = [];
// 收集所有"位置点":每个聚合点(按 cluster center)和每个独立点都是一个位置点
// 统一按距离用户位置升序排序后连续编号 1, 2, 3, ...
// 这样聚合点和独立 marker 不会各自从 #1 开始,地图上编号全局唯一
const positionPoints: any[] = [];
for (const cluster of clusters) {
const [cLng, cLat] = getClusterCenter(cluster);
positionPoints.push({ kind: 'cluster', cluster, cLng, cLat });
}
for (const item of singles) {
positionPoints.push({ kind: 'single', item });
}
if (userLocation) {
positionPoints.sort((a, b) => {
const da =
a.kind === 'cluster'
? haversine(userLocation[0], userLocation[1], a.cLng, a.cLat)
: haversine(userLocation[0], userLocation[1], a.item.lon, a.item.lat);
const db =
b.kind === 'cluster'
? haversine(userLocation[0], userLocation[1], b.cLng, b.cLat)
: haversine(userLocation[0], userLocation[1], b.item.lon, b.item.lat);
return da - db;
});
}
// 同步渲染聚合 marker(HTML,无异步依赖)
const singleMarkerRefs: Array<{ marker: any; item: any; number: number }> = [];
positionPoints.forEach((p, idx) => {
const number = idx + 1;
if (p.kind === 'cluster') {
// 聚合 marker:纯色圆 + 数量 N + 左上角位置编号角标
const clusterMarker: any = new AMap.Marker({
position: [p.cLng, p.cLat],
content: buildClusterIcon(p.cluster.length, p.cluster, userLocation, number),
clickable: true,
offset: new AMap.Pixel(-22, -22), // 主圈 44x44,中心对准坐标
extData: { __isCluster: true, items: p.cluster, clusterNumber: number },
zIndex: 110,
});
// 点击聚合 → 打开 InfoWindow
clusterMarker.on('click', () => {
this.openClusterInfoWindow(p.cluster, [p.cLng, p.cLat], callback);
});
this.overlayGroup.setMap(this.map);
this.overlayGroup.addOverlay(clusterMarker);
allMarkers.push(clusterMarker);
} else {
// 同步渲染独立 marker:先用 typeToIcon 的纯 url(保证渲染成功,无角标也行)
// 后续异步优化:用 canvas 画带角标的图标,setIcon 替换
const iconUrl: string = typeToIcon(p.item.type);
const startMarker: any = new AMap.Marker({
position: [p.item.lon, p.item.lat],
icon: iconUrl,
clickable: true,
offset: [-18, -32],
maxZoom: 14,
extData: p.item,
label: {
content: isFunction(extData?.content) && extData?.content(p.item),
},
zIndex: 99,
...extData?.extOption,
});
this.overlayGroup.setMap(this.map);
this.overlayGroup.addOverlay(startMarker);
allMarkers.push(startMarker);
// 记录独立 marker 引用,后续异步替换为带角标图标
singleMarkerRefs.push({ marker: startMarker, item: p.item, number });
}
});
// 异步优化:为独立 marker 叠加数字角标(canvas 失败也不影响主渲染流程)
// 主渲染已经完成(marker 已上地图),这里只是 setIcon 替换图标
singleMarkerRefs.forEach(({ marker, item, number }) => {
createNumberedMarkerIcon(typeToIcon(item.type), number, item.type)
.then((numberedUrl) => {
if (numberedUrl && marker && typeof marker.setIcon === 'function') {
try {
marker.setIcon(numberedUrl);
} catch (e) {
// 静默:setIcon 失败不影响
}
}
})
.catch(() => {
// 静默:canvas 失败保留原 url 图标
});
});
// 独立 marker 点击 → 直接走原回调
this.overlayGroup.on('click', (e: any) => {
const ext = e?.target?.getExtData?.();
if (!ext || ext.__isCluster) return; // 聚合已自行处理
if (!isFunction(callback)) return;
callback(ext);
});
if (computedCenter && allMarkers.length) {
try {
this.map && this.map.setFitView(allMarkers, false, [50, 50, 0, 50]);
} catch (e) {
console.warn('[createClusterOverlay] setFitView failed:', e);
}
}
return allMarkers;
},
/**
* @desc 打开簇列表 InfoWindow,点击列表项触发与单点点击相同的回调
*/
openClusterInfoWindow(cluster: any[], position: [number, number], callback?: Callback) {
if (!this.map || !AMap) return;
// 关闭上一次的
if (this.clusterInfoWindow) {
this.clusterInfoWindow.close();
this.clusterInfoWindow = null;
}
// 把 HTML 转成 DOM 元素直接传给 InfoWindow,避免 setTimeout + querySelector 的时序问题
// 事件直接绑在元素上,AMap 插入到地图 DOM 后 listener 依然生效(事件 listener 跟元素绑定,不跟 DOM 位置绑定)
const wrapper = document.createElement('div');
wrapper.innerHTML = buildClusterInfoContent(cluster, this.clusterUserLocation);
const root = wrapper.querySelector('.cluster-info-window') as HTMLElement | null;
const closeWindow = () => {
if (this.clusterInfoWindow) {
this.clusterInfoWindow.close();
this.clusterInfoWindow = null;
}
};
if (root) {
// 列表行点击
const rows = root.querySelectorAll('.cluster-row');
rows.forEach((row) => {
row.addEventListener('click', (e) => {
e.stopPropagation();
const idx = Number((row as HTMLElement).getAttribute('data-idx') || 0);
const target = cluster[idx];
if (!target) return;
closeWindow();
if (isFunction(callback)) callback(target);
});
row.addEventListener('mouseenter', () => {
(row as HTMLElement).style.background = 'rgba(255,255,255,0.08)';
});
row.addEventListener('mouseleave', () => {
(row as HTMLElement).style.background = 'transparent';
});
});
// 关闭按钮
const closeBtn = root.querySelector('.cluster-info-close');
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
closeWindow();
});
}
}
this.clusterInfoWindow = new AMap.InfoWindow({
position,
offset: new AMap.Pixel(0, -10),
content: wrapper,
isCustom: true,
autoMove: true,
closeWhenClickMap: true,
});
this.clusterInfoWindow.open(this.map, position);
},
clearOverlay() {
if (this.overlayGroup) {
@@ -309,13 +693,23 @@ export const Map = {
* @desc 创建地图详情弹窗
*
* */
createInfoModal({ option, content }) {
let { lat, lng } = option;
createInfoModal({ option, content }, flag = true) {
let { latitude, longitude } = option;
this.infoWindow = new AMap.InfoWindow({
position: [lng, lat],
offset: new AMap.Pixel(-5, -25),
position: [longitude, latitude],
offset: new AMap.Pixel(-8, -25),
content: content,
closeWhenClickMap: true,
closeWhenClickMap: flag,
});
this.infoWindow.open(this.map);
},
createEmerInfoModal({ option, content }, flag = true) {
let { latitude, longitude } = option;
this.infoWindow = new AMap.InfoWindow({
position: [longitude, latitude],
offset: new AMap.Pixel(0, -8),
content: content,
closeWhenClickMap: flag,
});
this.infoWindow.open(this.map);
},
@@ -362,6 +756,32 @@ export const Map = {
});
});
},
/**
* @desc 计算两个坐标点的步行时间
* @param {Array} listA 坐标点数组
* @param {Array} listB 坐标点数组
* @param {Function} callback 回调
* @return {Number} 时间
*/
getWalkingTime(listA: number[], listB: number[], callback?: Callback) {
let lnglatA = new AMap.LngLat(listA[0], listA[1]);
let lnglatB = new AMap.LngLat(listB[0], listB[1]);
const drivingOptions = {
policy: AMap.DrivingPolicy.LEAST_TIME,
};
let walking = new AMap.Walking(drivingOptions);
return new Promise((resolve, reject) => {
walking.search(lnglatA, lnglatB, (status, result) => {
if (status === 'complete') {
// console.log('result', result);
if (callback && isFunction(callback)) {
callback(result);
}
return resolve(result.routes[0]);
}
});
});
},
/**
* @desc 创建导航轨迹线路
@@ -387,7 +807,6 @@ export const Map = {
});
this.driveLine.on('error', function (result) {
let str = '定位失败:';
console.log('error', result);
switch (result.info) {
case 'PERMISSION_DENIED':
str += '浏览器阻止了定位操作';
@@ -402,7 +821,6 @@ export const Map = {
str += '未知错误';
break;
}
console.log(str);
return false;
});
},
@@ -417,27 +835,37 @@ export const Map = {
/**
* @desc 创建应急人员Marker
*/
createEmergencyMarker(option) {
createEmergencyMarker(option: any, isLabel = true, callback?: Callback) {
this.clearEmergencyMarker();
const { sex, address, name, position } = option;
const icon = sex == '1' ? female : male;
this.emergencyMarker = new AMap.Marker({
position: position,
icon: createIcon({ icon: icon, iconSize: [36, 36], imageSize: [36, 36] }),
offset: new AMap.Pixel(0, 0), //设置偏移量
label: {
content: `<div class="alarm-content" style="width: 180px" >
icon: createIcon({ icon: watchIconR, iconSize: [80, 80], imageSize: [80, 80] }),
offset: new AMap.Pixel(-40, -40), //设置偏移量
...(isLabel
? {
label: {
content: `<div class="alarm-content" style="width: 180px;z-index: 99" >
<div style="text-align: center;padding-bottom: 6px">${name}</div>
<div style="text-wrap: wrap">${address}</div>
</div>`,
offset: new AMap.Pixel(-100, 55),
},
offset: new AMap.Pixel(-120, -70),
},
}
: {}),
});
this.map.add(this.emergencyMarker);
this.setZoomCenter(position);
this.emergencyMarker.on('click', () => {
if (callback && isFunction(callback)) {
callback(option);
}
});
// this.setZoomCenter(position);
},
/**
* @desc 清除应急人员Marker
* @desc 清除应急人员Marker..
*
*/
clearEmergencyMarker() {
if (this.emergencyMarker) {
@@ -464,6 +892,30 @@ export const Map = {
});
});
},
/**
* @desc 根据坐标获取地址信息
* @param {Array} list 坐标点数组
*/
getInfoByPoint(list: number[]) {
const geocoder = new AMap.Geocoder({
city: '全国',
radius: 1000,
});
let lnglat = new AMap.LngLat(list[0], list[1]);
return new Promise((resolve, reject) => {
geocoder.getAddress(lnglat, (status, result) => {
if (status === 'complete' && result.regeocode) {
let code = result?.regeocode?.addressComponent?.adcode;
code = code.substring(0, 4) + '00';
if (code.substring(0, 3) !== '650') {
code = '650100';
}
return resolve(code);
}
return reject(result);
});
});
},
/**
* @desc 清除导航元素
*/
@@ -471,4 +923,40 @@ export const Map = {
this.clearNaviLine();
this.clearEmergencyMarker();
},
createSMarker(position: any) {
if (this.startMarker) {
this.startMarker?.setMap(null);
this.startMarker = null;
}
this.startMarker = new AMap.Marker({
position: position,
offset: new AMap.Pixel(0, 0),
});
this.map.add(this.startMarker);
this.setZoomCenter(position);
},
createEMarker(position: any) {
if (this.endMarker) {
this.endMarker?.setMap(null);
this.endMarker = null;
}
this.endMarker = new AMap.Marker({
position: position,
offset: new AMap.Pixel(0, 0),
});
this.map.add(this.endMarker);
this.setZoomCenter(position);
},
clearSOrE() {
if (this.startMarker) {
this.startMarker?.setMap(null);
this.startMarker = null;
}
if (this.endMarker) {
this.endMarker?.setMap(null);
this.endMarker = null;
}
},
};
+16 -4
View File
@@ -1,7 +1,7 @@
// 地图默认配置
export const defaultOption = {
center: [108.660704, 36.386058], //地图中心点
zoom: 6.5, //地图显示的缩放级别
center: [87.627313, 43.826809], //地图中心点
zoom: 11, //地图显示的缩放级别
mapStyle: 'darkblue', //地图样式
};
export const YinChuanOption = {
@@ -21,9 +21,21 @@ export const innerLineLight = {
strokeColor: '#93aaf8',
};
// 地图key
export const mapKey = '4bbcb216b889f2d612cbed05ae6979c8';
// export const mapKey = '4bbcb216b889f2d612cbed05ae6979c8';
// 2025年11月12日11:25:52 更新新的key
export const mapKey = '0075068dd9782107fc0e6a7209a6b494';
// 地图插件
export const mapPlugin = ['AMap.ToolBar', 'AMap.Driving', 'AMap.GeoJSON', 'AMap.MarkerCluster', 'AMap.PlaceSearch', 'AMap.Driving', 'AMap.Geocoder'];
export const mapPlugin = [
'AMap.ToolBar',
'AMap.Driving',
'AMap.GeoJSON',
'AMap.MarkerCluster',
'AMap.PlaceSearch',
'AMap.Driving',
'AMap.Walking',
'AMap.Geocoder',
'AMap.AutoComplete',
];
//根据数据量设置地图层级
export function getZoom(list = []) {
+2 -2
View File
@@ -4,12 +4,12 @@
--antSelectDropdownBorderColor: #1b7ef2;
--antSelectItemActive: #002e64;
--antSelectFontColor: #fff;
--antSelectBg: #000;
--antSelectBg: #082147;
/*服务详情数据*/
--typeTimeSelectColor: #45a2ff;
--typeTimeUnSelectColor: #a3a4a4;
/*表格 */
--tableHeadBg: rgba(35, 132, 221, 0.5);
--tableHeadBg: #073063;
--tableFontColor: #fff;
--tableRowBg: #00152b;
--tableRowBorder: #2384dd;
+20 -3
View File
@@ -13,14 +13,16 @@ export enum ServerDetailType {
export function useStatistics() {
const statistics = ref([
{
name: '最新呼入电话',
name: '本周呼入电话',
value: '',
key: 'gross',
dateType: '0',
},
{
name: '最新受理电话',
name: '本周受理电话',
value: '',
key: 'alerdyreceive',
key: 'gross', // 20251213临时处理方案
dateType: '0',
},
{
name: '突发重大伤病应急',
@@ -85,6 +87,21 @@ export const sexType = [
},
];
export const timeTabNew = [
{
name: '本周',
value: '2',
},
{
name: '本月',
value: '3',
},
{
name: '本年',
value: '4',
},
];
export function useSpin() {
const spinning = ref(true);
+3
View File
@@ -7,6 +7,8 @@ export enum Api {
complaint = '/health-emergency/emergency/tblHelpCenter/sicknessTypeStatistic',
orderDetail = '/health-emergency/emergency/order/list',
listCustom = '/health-watch/watch/watchMonitorData/listCustom',
listCustomNew = 'health-watch/watch/watchMonitorData/largeScreen/listCustom',
}
//获取全部部门
@@ -21,3 +23,4 @@ export const getDetail = (params: any) => get(Api.getDetail, params);
export const getOrderDetail = (params: any) => get(Api.orderDetail, params);
//副屏预警查看详情
export const getEarlyWarningDetail = (params: any) => get(Api.listCustom, params);
export const getEarlyWarningDetailNew = (params: any) => get(Api.listCustomNew, params);
+3 -3
View File
@@ -1,12 +1,12 @@
<template>
<div class="common-card" :class="[themeType]">
<public-title title="服务详情数据" :themeType="themeType" />
<public-title title="服务详情数据" themeType="dark" />
<div class="inner">
<div class="type-time">
<span
v-for="(item, index) in typeTime"
:key="index"
:class="[selectIndex === index ? 'select' : 'unSelect', themeType]"
:class="[selectIndex === index ? 'select' : 'unSelect', 'dark']"
@click="handleSelect(index, item.value)"
>{{ item.name }}</span
>
@@ -16,7 +16,7 @@
v-for="(item, index) in statistics"
:key="index"
class="server-item"
:class="[['gross', 'alerdyreceive'].includes(item.key) ? 'hasCursor' : '', themeType]"
:class="[['gross', 'alerdyreceive'].includes(item.key) ? 'hasCursor' : '', 'dark']"
@click="handleClick(item)"
>
<span>{{ item?.name }}</span>
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div class="common-card" :class="[themeType]">
<public-title title="主诉分类" :themeType="themeType">
<public-title title="应急求助信息" :themeType="themeType">
<template #right><span class="right-text" :class="[themeType]" @click="viewDetail">查看详情</span> </template>
</public-title>
<div class="zc-inner">
-1
View File
@@ -153,7 +153,6 @@
});
async function getScrollList() {
console.log(dayjs());
try {
const { code, result } = await getMonitorList({
pageSize: 10,
+1 -1
View File
@@ -7,5 +7,5 @@ export enum Api {
//健康终端报警
export const getMonitorList = (params: any) => get(Api.getMonitorList, params);
//长庆油田大病人数
//大病人数
export const getHealthDataList = (params: any) => get(Api.getHealthDataList, params);
+1 -4
View File
@@ -2,10 +2,7 @@
<div class="common-card" :class="themeType">
<public-title title="基层医疗点远程会诊" :theme-type="themeType" />
<div class="inner">
<div class="inner-item" data-text="应急就医-服务中心"></div>
<div class="inner-item" data-text="采油九厂-薛岔作业区"></div>
<div class="inner-item" data-text="第九采油厂-五谷城作业区"></div>
<div class="inner-item" data-text="采油十一厂-彭阳作业区"></div>
<div class="inner-item" data-text="服务中心"></div>
</div>
</div>
</template>
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div class="common-card" :class="themeType">
<public-title title="长庆油田大病人数" :themeType="themeType" />
<public-title title="大病人数" :themeType="themeType" />
<div class="inner" ref="healthChart"></div>
</div>
</template>
+6 -4
View File
@@ -2,7 +2,7 @@ import { getAllList, getAmbulanceList } from '/@/components/chinaMap/chinaMapApi
export enum TabType {
all = '', //所有资源
youTianYiYuan = '1', //油田医院
youTianYiYuan = '1', //医院
heZuoYiYuan = '2', //合作医院
yiLiaoDian = '3', //医疗点
jiuHuChe = '5', //救护车
@@ -105,7 +105,7 @@ export function mapApiSwitch(type, params: any) {
lat: 34, //纬度34
lon: 108, //经度
};
// 全部 油田 医疗点 合作医院
// 全部 医疗点 合作医院
if ([TabType.all, TabType.youTianYiYuan, TabType.yiLiaoDian, TabType.heZuoYiYuan, TabType.jiuHuChe, TabType.jiuHuChe].includes(type)) {
return getAllList({ ...basicParams, ...params });
}
@@ -115,12 +115,14 @@ export function mapApiSwitch(type, params: any) {
}
export const mapIcon1 = new URL('/@/assets/img/mapIcon1.png', import.meta.url).href;
export const mapIcon11 = new URL('/@/assets/img/mapIcon1-1.png', import.meta.url).href;
export const mapIcon2 = new URL('/@/assets/img/mapIcon2.png', import.meta.url).href;
export const mapIcon3 = new URL('/@/assets/img/mapIcon3.png', import.meta.url).href;
export const mapIcon4 = new URL('/@/assets/img/mapIcon4.png', import.meta.url).href;
export const mapIcon41 = new URL('/@/assets/img/mapIcon4-1.png', import.meta.url).href;
export const mapIcon5 = new URL('/@/assets/img/mapIcon5.png', import.meta.url).href;
export const mapIcon6 = new URL('/@/assets/img/mapIcon6.png', import.meta.url).href;
export const mapIcon7 = new URL('/@/assets/img/mapIcon7.png', import.meta.url).href;
export const mapIcon51 = new URL('/@/assets/img/mapIcon5-1.png', import.meta.url).href;
export const watchIconR = new URL('/@/assets/img/watchIconR.svg', import.meta.url).href;
export const legend1 = new URL('/@/assets/img/legend1.png', import.meta.url).href;
export const legend2 = new URL('/@/assets/img/legend2.png', import.meta.url).href;
+36 -12
View File
@@ -1,16 +1,39 @@
<template>
<a-modal
:class="['dialog', themeType]"
v-model:visible="props.openVis"
:title="props.title"
:width="props.width"
:maskClosable="true"
@cancel="handleCancel"
destroy-on-close
>
<slot name="container"></slot>
<template #footer></template>
</a-modal>
<template v-if="props.title">
<a-modal
:class="['dialog', themeType]"
v-model:visible="props.openVis"
:title="props.title"
:width="props.width"
:maskClosable="true"
@cancel="handleCancel"
v-bind="$attrs"
destroy-on-close
>
<template #title>
<slot name="modalTitle"></slot>
</template>
<slot name="container"></slot>
<template #footer></template>
</a-modal>
</template>
<template v-else>
<a-modal
:class="['dialog', themeType]"
v-model:visible="props.openVis"
:width="props.width"
:maskClosable="true"
@cancel="handleCancel"
v-bind="$attrs"
destroy-on-close
>
<template #title>
<slot name="modalTitle"></slot>
</template>
<slot name="container"></slot>
<template #footer></template>
</a-modal>
</template>
</template>
<script setup lang="ts">
@@ -26,6 +49,7 @@
},
title: {
type: String,
default: () => '',
},
});
const { themeType } = useTheme();
+14 -10
View File
@@ -1,8 +1,11 @@
// 步数
import {visibleChartLabel} from "/@/utils/utils.ts";
import { visibleChartLabel } from '/@/utils/utils.ts';
export function screenPie(chartData) {
const data = visibleChartLabel(chartData)
const data = visibleChartLabel(chartData).map((item) => {
item = { ...item, label: { show: item.value > 0 }, labelLine: { show: item.value > 0 } };
return item;
});
return {
color: ['#B750BE', '#6C63F0', '#3AACFF', '#ED589D', '#FB466C'],
tooltip: {
@@ -24,7 +27,7 @@ export function screenPie(chartData) {
color: '#ffffff',
fontSize: 16,
},
selectedMode: false
selectedMode: false,
},
series: [
{
@@ -35,13 +38,14 @@ export function screenPie(chartData) {
label: {
show: true,
position: 'outside',
formatter: '{c}人次',
color: '#ffffff',
fontSize: 14,
},
labelLine: {
show: true,
length: 6,
length2: 10,
length2: 30,
minTurnAngle: 120,
},
data: data,
@@ -52,7 +56,7 @@ export function screenPie(chartData) {
//左侧圆
export function circleRing(chartData, total, color) {
const data = visibleChartLabel(chartData)
const data = visibleChartLabel(chartData);
return {
color: color,
tooltip: {
@@ -74,7 +78,7 @@ export function circleRing(chartData, total, color) {
color: '#ffffff',
fontSize: 16,
},
selectedMode: false
selectedMode: false,
},
series: [
{
@@ -95,18 +99,18 @@ export function circleRing(chartData, total, color) {
label: {
show: true,
position: 'outside',
formatter: '{d}%',
formatter: '{d}%\n{c}人次',
color: '#ffffff',
fontSize: 14,
},
labelLine: {
show: true,
length: 4,
length2: 12,
length: 2,
length2: 30,
minTurnAngle: 120,
},
data: data,
},
],
};
}
}
+10 -2
View File
@@ -3,17 +3,23 @@ import { get, post } from '/@/api/request.ts';
export enum Api {
depart = '/health-watch/watch/watchDevice/getUnitList',
getWatchDataByOrgCode = '/health-watch/watch/watchData/getWatchDataByOrgCode',
getWatchDataByOrgCodeNew = '/health-watch/watch/watchData/getWatchDataByOrgCode/v2',
getMonitorList = '/health-watch/watch/watchMonitorData/getMonitorList',
sleepApi = '/health-watch/watch/watchData/getSleep',
sleepApi = '/health-watch/watch/watchData/getSleep/v2',
stepApi = '/health-watch/watch/watchData/getSdc',
stepNewApi = '/health-watch/watch/watchData/getSdc/v2',
listCustom = 'health-watch/watch/watchMonitorData/listCustom',
listCustomNew = 'health-watch/watch/watchMonitorData/largeScreen/listCustom',
}
// 单位-
export const allSecondaryDeparts = () => get(Api.depart);
//左側
export const getWatchDataByOrgCode = (params) => post(Api.getWatchDataByOrgCode, params);
export const getWatchDataByOrgCode = (params: any) => post(Api.getWatchDataByOrgCode, params);
export const getWatchDataByOrgCodeNew = (params: any) => post(Api.getWatchDataByOrgCodeNew, params);
//右-健康终端报警
export const getMonitorList = (params: any) => get(Api.getMonitorList, params);
@@ -21,6 +27,8 @@ export const getMonitorList = (params: any) => get(Api.getMonitorList, params);
export const getSleep = (params: any) => post(Api.sleepApi, params);
// 右-步数
export const getStep = (params: any) => post(Api.stepApi, params);
export const getStepNew = (params: any) => post(Api.stepNewApi, params);
//副屏-预警
export const getCustom = (params: any) => get(Api.listCustom, params);
export const getCustomNew = (params: any) => get(Api.listCustomNew, params);
@@ -16,7 +16,7 @@
import { onMounted, ref, watch } from 'vue';
import { cloneDeep } from 'lodash-es';
import ItemD from '/@/components/item-d/item-d.vue';
import { getWatchDataByOrgCode } from '/@/components/secondaryScreen/commonApi.ts';
import { getWatchDataByOrgCode, getWatchDataByOrgCodeNew } from '/@/components/secondaryScreen/commonApi.ts';
import { useSpin } from '/@/components/body-d-left/bodyLeftHooks.ts';
import { useSession, caching } from '/@/hooks/autoRefresh/caching.ts';
@@ -79,8 +79,14 @@
orgCode: String,
type: String,
themeType: String,
refreshState: {
type: Boolean,
default: () => false,
},
});
onMounted(() => {
init();
});
onMounted(() => {});
watch(props, () => {
init();
});
@@ -88,14 +94,15 @@
async function init() {
setSpin(true);
try {
const { code, result } = await getWatchDataByOrgCode(props);
// const { code, result } = await getWatchDataByOrgCode(props);
const { code, result } = await getWatchDataByOrgCodeNew(props);
if (code == 200) {
setValue(result);
setSession(PAGE2_LEFT, JSON.stringify(result));
// setSession(PAGE2_LEFT, JSON.stringify(result));
}
} catch {
const result = JSON.parse(getSession(PAGE2_LEFT));
setValue(result);
// const result = JSON.parse(getSession(PAGE2_LEFT));
// setValue(result);
}
}
@@ -105,8 +112,10 @@
data.forEach((item) => {
let resData = result.find((v) => v.type == item.type);
item.userCount = resData.userCount;
item.data.forEach((val) => {
item.data = item.data.map((val) => {
val.value = resData[val.key];
val = { ...val, label: { show: val.value > 0 }, labelLine: { show: val.value > 0 } };
return val;
});
});
dataList.value = data;
@@ -3,7 +3,7 @@
<public-title title="睡眠" :theme-type="themeType" />
<div class="inner">
<div class="title">
<span class="lightSleep">平均睡眠 {{ nums.avg }} 深睡 {{ nums.avgLong }} 浅睡 {{ nums.avgShort }}</span>
<span class="lightSleep">平均睡眠 {{ nums.avg || 0 }} 深睡 {{ nums.avgLong || 0 }} 浅睡 {{ nums.avgShort || 0 }}</span>
</div>
<div class="sleepChart" ref="sleepChart"></div>
</div>
@@ -47,7 +47,7 @@ export function sleepCharts({ time, short, long, awake, type }) {
grid: {
top: '15%',
left: '5%',
right: '5%',
right: '8%',
bottom: 30,
},
tooltip: {
@@ -26,14 +26,14 @@
</a-table>
</div>
<!--预警查看详情-->
<EarlyWarningDialog ref="earlyWarningRef"></EarlyWarningDialog>
<EarlyWarningDialog ref="earlyWarningRef" :org-codes="orgCode" />
</div>
</template>
<script lang="ts" setup>
import { onUnmounted, ref, watch } from 'vue';
import PublicTitle from '/@/components/publicTitle.vue';
import EarlyWarningDialog from '/@/views/components/earlyWarningDialog.vue';
import { getCustom, getMonitorList } from '/@/components/secondaryScreen/commonApi.ts';
import { getCustom, getCustomNew, getMonitorList } from '/@/components/secondaryScreen/commonApi.ts';
import { monitorColumns } from '/@/components/secondaryScreen/screen-right/screenRightHooks.ts';
import $bus from '/@/utils/bus.ts';
import { earlyWarning } from '/@/utils/busConstant.ts';
@@ -64,7 +64,7 @@
function handlePoliceDetail(record) {
$bus.emit(earlyWarning, record);
}
const threeCodes = ref();
watch(
props,
() => {
@@ -75,7 +75,12 @@
},
{ immediate: true, deep: true }
);
watch(
() => props.orgCode,
() => {
threeCodes.value = props.orgCode;
}
);
onUnmounted(() => {
window.removeEventListener('resize', setHeight);
});
@@ -89,7 +94,8 @@
async function init(page) {
try {
const { pageSize } = pagination.value;
const { code, result } = await getCustom({ pageSize, pageNo: page, orgCode: props.orgCode });
// const { code, result } = await getCustom({ pageSize, pageNo: page, orgCode: props.orgCode });
const { code, result } = await getCustomNew({ pageSize, pageNo: page, orgCodes: props.orgCode });
if (code == 200) {
setSession(PAGE2_RIGHT3, JSON.stringify({ page, result }));
setValue(result, page);
@@ -15,7 +15,7 @@
import { useSpin } from '/@/components/body-d-left/bodyLeftHooks.ts';
import { useStep } from '/@/components/secondaryScreen/screen-right/screenRightHooks.ts';
import { screenPie } from '/@/components/item-d/pieCharts.ts';
import { getStep } from '/@/components/secondaryScreen/commonApi.ts';
import { getStep, getStepNew } from '/@/components/secondaryScreen/commonApi.ts';
import { caching, useSession } from '/@/hooks/autoRefresh/caching.ts';
const props = defineProps({
@@ -41,7 +41,8 @@
orgCode: props.orgCode,
type: props.type,
};
const { code, result } = await getStep(params);
// const { code, result } = await getStep(params);
const { code, result } = await getStepNew(params);
if (code == 200) {
setValue(result);
setSession(PAGE2_RIGHT2, JSON.stringify(result));
@@ -9,13 +9,12 @@
:rowClassName="(record, index) => (index % 2 === 1 ? 'table-default' : 'table-striped')"
>
<template #bodyCell="{ column, record, index, text }">
<template v-if="column.customRender && isFunction(column.customRender)" v-html="column.customRender({ column, record, index, text })">
<template v-if="column?.customRender && isFunction(column.customRender)" v-html="column.customRender({ column, record, index, text })">
</template>
<slot v-else-if="column.slot" :name="column.slot" v-bind="{ column, record, index, text } || {}"></slot>
<slot v-else-if="column?.slot" :name="column.slot" v-bind="{ column, record, index, text } || {}"></slot>
<template v-else>
{{ text }}
</template>
<slot name="bodyCell" v-bind="{ column, record, index, text } || {}"></slot>
</template>
</a-table>
@@ -32,6 +31,7 @@
type: Function,
},
apiParams: {
type: Object,
default: () => {},
},
afterFetch: {
@@ -53,6 +53,7 @@
showTotal: (total) => `${total}`,
onChange: pageChange,
});
const emit = defineEmits(['tableLoad']);
function pageChange(page, pageS) {
current.value = page;
@@ -75,34 +76,35 @@
async function getRecords() {
try {
tableLoading.value = true;
emit('tableLoad', true);
const { code, result } = await props.api({ ...props.apiParams, pageSize: pageSize.value, pageNo: current.value });
if (code == 200) {
if (props.afterFetch && isFunction(props.afterFetch)) {
dataSource.value = props.afterFetch({ result, setPage: setPage, setDataSource: setDataSource });
} else {
dataSource.value = result?.records;
dataSource.value = result?.records ? result?.records : result;
}
if (result.hasOwnProperty('records')) {
paginationProp.value = { ...paginationProp.value, ...result };
paginationProp.value.pageSize = result.size;
}
paginationProp.value = { ...paginationProp.value, ...result };
paginationProp.value.pageSize = result.size;
}
} catch (e) {
dataSource.value = [];
tableLoading.value = false;
} finally {
tableLoading.value = false;
emit('tableLoad', false);
}
}
watch(
props,
(v, o) => {
if (JSON.stringify(v) !== JSON.stringify(o)) {
nextTick(() => {
pageSize.value = 10;
current.value = 1;
getRecords();
});
}
() => {
nextTick(() => {
pageSize.value = 10;
current.value = 1;
getRecords();
});
},
{
immediate: true,
@@ -115,8 +117,8 @@
<style scoped lang="less">
:deep(.ant-table-thead > tr > th) {
color: var(--tableFontColor) !important;
background: var(--tableHeadBg) !important;
border-top: 1px solid var(--tableHeadBg) !important;
background: #073063 !important;
border-top: 1px solid #073063 !important;
border-bottom: none;
padding: 9px 9px;
text-align: center;
@@ -12,12 +12,22 @@
<div v-show="isShow" :class="className" class="footer-data" style="--moveUp: 20px; --moveDown: -80px">
<div
v-for="item in footerList"
:style="{ cursor: item.key == 'allotDeviceNum' ? 'pointer' : 'default' }"
:style="{
cursor: item.key == 'allotDeviceNum' ? 'pointer' : 'default',
}"
:key="`footer-list-${item.key}`"
class="col-item nowrap"
@click="openWatchList(item)"
>
<div class="col-text">{{ item?.name }}</div>
<div :title="item.value" class="col-value nowrap">{{ item?.value }}</div>
<div
:title="item.value"
class="col-value nowrap"
:style="{
color: item.key == 'allotDeviceNum' ? '#0098fa' : '#fff',
}"
>{{ item?.value }}</div
>
</div>
</div>
<div
@@ -26,10 +36,10 @@
class="footer-dialog-con"
style="--moveUp: 20px; --moveDown: -180px"
>
<FooterDialog :detailLeft="detailLeft" :detailRight="detailRight" @closeDialog="closeDialog"></FooterDialog>
<FooterDialog :detailLeft="detailLeft" :detailRight="detailRight" @closeDialog="closeDialog" />
</div>
</div>
<WatchListDialog ref="watchDialogRef" @close="closeWatch"></WatchListDialog>
<WatchListDialog ref="watchDialogRef" @close="closeWatch" />
</div>
</template>
<script lang="ts" setup>
@@ -103,7 +113,7 @@
async (val) => {
let params = {
orgCodeLeaf: val.orgCode,
orgCode: null,
orgCode: props.orgCode,
};
const { code, result } = await footerApi(params);
if (code == 200) {
@@ -24,7 +24,7 @@ export function useDepart() {
async function getOptionData() {
const { result = [], code } = await allSecondaryDeparts();
if (code == 200) {
let res = [{ departName: '长庆油田', orgCode: 'A01' }, ...result];
let res = [...result];
setOption(res);
}
}
-2
View File
@@ -264,14 +264,12 @@
// console.log('绘制驾车路线完成');
searchFlag.value = false;
} else {
console.log('获取驾车数据失败:', result);
searchFlag.value = false;
}
});
AMap.Event.addListener(driving, 'complete', (res) => {
const data = JSON.parse(JSON.stringify(res));
searchFlag.value = false;
console.log('查询结果', data);
if (data.info == 'OK') {
const { routes } = data;
setTitle(routes[0]);
+2
View File
@@ -2,10 +2,12 @@ import { get } from '/@/api/request.ts';
export enum Api {
allList = '/health-emergency/emergency/tblAmbulanceGps/getAllList',
allListNew = '/health-emergency/emergency/LargeScreenNew/map',
getAmbulanceList = '/health-emergency/emergency/tblAmbulanceGps/getAmbulanceList',
}
// 获取所有资源
export const getAllList = (params) => get(Api.allList, params);
export const getAllListNew = (params) => get(Api.allListNew, params);
//获取救护车GPS信息
export const getAmbulanceList = (params) => get(Api.getAmbulanceList, params);

Some files were not shown because too many files have changed in this diff Show More