Initial commit of akmon project
This commit is contained in:
184
pages/ec/health/alertparse.uts
Normal file
184
pages/ec/health/alertparse.uts
Normal file
@@ -0,0 +1,184 @@
|
||||
// 告警消息解析工具
|
||||
// 参考 doc_eldercare/alert.md 设计
|
||||
|
||||
export type AlertParseResult {
|
||||
type : string; // 解析后的类型,如 'SOS', '健康数据', '定位', '通知', '计步', '围栏', '语音', '睡眠', '手表', '未知'
|
||||
title : string; // 简要标题
|
||||
content : string; // 展示内容
|
||||
time ?: string; // 时间
|
||||
raw : any; // 原始数据
|
||||
level ?: 'normal' | 'warn' | 'danger'; // 可选,紧急等级
|
||||
mid ?: string; // 设备ID
|
||||
smid ?: string; // 消息ID
|
||||
extra ?: UTSJSONObject; // 其它解析字段
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析推送消息主入口
|
||||
* @param msg 传入ps_push_msg_raw的raw_data对象或完整record
|
||||
*/
|
||||
export const parseAlertMessage = (msg : UTSJSONObject) : AlertParseResult => {
|
||||
// 兼容 record/raw_data
|
||||
let data : UTSJSONObject = msg.getJSON('raw_data') ?? {};
|
||||
|
||||
|
||||
const pushType = data.getNumber('pushType') ?? 0;
|
||||
const action = data.getNumber('actionRaw') ?? 0;
|
||||
const time = data.getString('Time') ?? data.getString('time') ?? data.getString('created_at') ?? '';
|
||||
// 1. SOS
|
||||
if (pushType === 1) {
|
||||
return {
|
||||
type: 'SOS',
|
||||
title: 'SOS求救',
|
||||
content: `${data.getString('Name') ?? ''}(${data.getString('MID') ?? ''}) 向您发出求救`,
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'danger',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { lon: data.getNumber('Lon'), lat: data.getNumber('Lat'), address: data.getString('Str') }
|
||||
};
|
||||
}
|
||||
// 2. 健康数据
|
||||
if (pushType === 2) {
|
||||
let arr : string[] = [];
|
||||
const h = data.getNumber('H');
|
||||
const o = data.getNumber('O');
|
||||
const w = data.getNumber('W');
|
||||
const x = data.getNumber('X');
|
||||
const y = data.getNumber('Y');
|
||||
if (h != null) arr.push(`心率:${h}`);
|
||||
if (o != null) arr.push(`血氧:${o}`);
|
||||
if (w != null) arr.push(`体温:${w}`);
|
||||
if (x != null) arr.push(`高压:${x}`);
|
||||
if (y != null) arr.push(`低压:${y}`);
|
||||
return {
|
||||
type: '健康数据',
|
||||
title: '健康数据推送',
|
||||
content: arr.length > 0 ? arr.join(',') : JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'normal',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { mid: data.getString('MID') ?? data.getString('mid') }
|
||||
};
|
||||
}
|
||||
// 3. 定位
|
||||
if (pushType === 3) {
|
||||
return {
|
||||
type: '定位',
|
||||
title: '定位推送',
|
||||
content: `${data.getString('Pro') ?? ''}${data.getString('City') ?? ''}${data.getString('Dist') ?? ''}${data.getString('Str') ?? ''} (${data.getNumber('Lon')},${data.getNumber('Lat')})`,
|
||||
time: data.getString('CT') ?? data.getString('UT') ?? time,
|
||||
raw: msg,
|
||||
level: 'normal',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { lon: data.getNumber('Lon'), lat: data.getNumber('Lat') }
|
||||
};
|
||||
}
|
||||
// 4. 通知
|
||||
if (pushType === 4) {
|
||||
const actionMapObj = {
|
||||
'-1': '设备在线离线', '4': '围栏内停留', '5': '离开围栏', '6': '进入围栏', '9': '低电报警', '11': '跌倒报警', '23': '高温报警',
|
||||
'26': '断开wifi', '28': 'wifi离线', '36': '防盗报警', '42': '布防告警', '44': '在家布防告警', '7': 'SOS报警', '10': '摘除报警',
|
||||
'22': '低温报警', '24': '更换SIM卡', '27': '连接wifi', '35': '社区养老报警', '37': '状态通知', '43': '撤防告警', '45': '八件套报警',
|
||||
'47': 'wifi不一致报警', '49': '红外报警', '50': 'NB按键报警', '51': 'NB防拆报警', '52': 'NB报警复位', '61': 'NB设备报警',
|
||||
'63': '人体存在报警', '67': 'NB测试报警', '85': '网关上线', '87': '删除子设备', '114': '烟感/气感/门磁事件', '116': 'SCA事件',
|
||||
'118': '防跌倒雷达', '121': '智能胸牌告警', '84': '网关离线', '86': '添加子设备', '113': '门磁事件', '115': '拉绳SOS',
|
||||
'117': '4G视频门磁', '119': 'd5网关子设备报警', '122': 'NB温湿度报警', '123': '气感报警', '125': '水浸报警', '127': '跌倒报警',
|
||||
'129': '燃气报警', '131': '对讲SOS', '134': 'AI智能报警', '124': '烟感报警', '126': '摄像头报警', '128': '井盖报警',
|
||||
'130': '红外报警', '132': 'ZML_SOS报警', '200': '设备信息变更'
|
||||
};
|
||||
let actionStr:string = actionMapObj.getString(action.toString()) ?? `通知类型:${action}`;
|
||||
return {
|
||||
type: '通知',
|
||||
title: actionStr,
|
||||
content: `${data.getString('Name') ?? ''} ${data.getString('Content') ?? ''}`.trim() ?? JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: (action === 9 || action === 11 || action === 23 || action === 7) ? 'danger' : 'warn',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { action }
|
||||
};
|
||||
}
|
||||
// 5. 计步/翻转(pushType=5 或有 Step/Roll 字段)
|
||||
if (pushType === 6) {
|
||||
return {
|
||||
type: '计步',
|
||||
title: '计步/翻转',
|
||||
content: `步数:${data.getNumber('Step') ?? '-'} 翻转:${data.getNumber('Roll') ?? '-'}`,
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'normal',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { step: data.getNumber('Step'), roll: data.getNumber('Roll'), mid: data.getString('MID') ?? data.getString('mid'), smid: data.getString('SMID') ?? data.getString('smid') }
|
||||
};
|
||||
}
|
||||
// 6. 围栏(进出围栏、进入围栏、离开围栏等)
|
||||
if (pushType === 7 || (pushType === 4 && (action === 6 || action === 5))) {
|
||||
let actionStr = '';
|
||||
if (pushType === 4) {
|
||||
if (action === 6) actionStr = '进入围栏';
|
||||
else if (action === 5) actionStr = '离开围栏';
|
||||
else actionStr = '围栏事件';
|
||||
} else {
|
||||
actionStr = '进出围栏';
|
||||
}
|
||||
return {
|
||||
type: '围栏',
|
||||
title: actionStr,
|
||||
content: data.getString('Content') ?? JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'warn',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { lon: data.getNumber('Lon'), lat: data.getNumber('Lat'), action }
|
||||
};
|
||||
}
|
||||
// 7. 微聊语音
|
||||
if (pushType === 8) {
|
||||
let msgType = data.getNumber('msgType') ?? data.getNumber('MType') ?? 0;
|
||||
let typeStr = msgType === 2 ? '语音' : '文字';
|
||||
return {
|
||||
type: '微聊',
|
||||
title: `微聊${typeStr}消息`,
|
||||
content: data.getString('content') ?? JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'normal',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { msgType }
|
||||
};
|
||||
}
|
||||
// 8. 睡眠带报警
|
||||
if (pushType === 9) {
|
||||
return {
|
||||
type: '睡眠报警',
|
||||
title: '睡眠带报警',
|
||||
content: data.getString('Content') ?? JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
level: 'warn',
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid'),
|
||||
extra: { action }
|
||||
};
|
||||
}
|
||||
// 9. 其它类型可继续补充...
|
||||
// 默认
|
||||
return {
|
||||
type: '未知',
|
||||
title: '未知推送',
|
||||
content: JSON.stringify(data),
|
||||
time,
|
||||
raw: msg,
|
||||
mid: data.getString('MID') ?? data.getString('mid'),
|
||||
smid: data.getString('SMID') ?? data.getString('smid')
|
||||
};
|
||||
};
|
||||
80
pages/ec/health/alerts.uvue
Normal file
80
pages/ec/health/alerts.uvue
Normal file
@@ -0,0 +1,80 @@
|
||||
<!-- 健康提醒列表 - uts-android 兼容版 -->
|
||||
<template>
|
||||
<view class="alerts-list-page">
|
||||
<view class="header">
|
||||
<text class="header-title">健康提醒</text>
|
||||
</view>
|
||||
<view v-for="alert in alerts" :key="alert.id" class="alert-item">
|
||||
<text class="alert-title">{{ alert.title }}</text>
|
||||
<text class="alert-desc">{{ alert.description }}</text>
|
||||
<text class="alert-patient">患者: {{ alert.elder_name }}</text>
|
||||
<text class="alert-time">{{ alert.created_at }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
const alerts = ref<any[]>([])
|
||||
const loadAlert = async () => {
|
||||
const result = await supa.from('ec_health_alerts')
|
||||
.select('*', { count: 'exact' })
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(100)
|
||||
.execute()
|
||||
if (result.data != null) alerts.value = result.data
|
||||
}
|
||||
onLoad(() => {
|
||||
loadAlert()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.alerts-list-page {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 0 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.alert-desc {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin: 6px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-patient {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
98
pages/ec/health/ecalert-history.uvue
Normal file
98
pages/ec/health/ecalert-history.uvue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<view class="alert-container">
|
||||
<view class="top-bar">
|
||||
<button @click="goRealtime" style="margin-right: 16rpx;">实时告警</button>
|
||||
<text class="title">历史健康告警</text>
|
||||
</view>
|
||||
<view class="section" style="margin-bottom: 24rpx;">
|
||||
<button @click="loadHistory">刷新历史</button>
|
||||
</view>
|
||||
<scroll-view style="height: 600rpx; border: 1px solid #ccc; padding: 8rpx;" scroll-y scroll-with-animation>
|
||||
<view v-for="(msg, idx) in messages" :key="idx" style="font-size: 26rpx; color: #333; margin-bottom: 12rpx;">
|
||||
<text>{{ msg.timeStr }}:</text>
|
||||
<text>{{ msg.content }}</text>
|
||||
<text>{{ msg.raw_data }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
|
||||
type AlertMessage = {
|
||||
type: string;
|
||||
mid?:string;
|
||||
content: string;
|
||||
timeStr: string;
|
||||
raw_data:string
|
||||
};
|
||||
|
||||
import { parseAlertMessage } from './alertparse.uts';
|
||||
import supa from '@/components/supadb/aksupainstance.uts';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
/** 历史告警消息列表,强类型 */
|
||||
messages: [] as AlertMessage[],
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
goRealtime() {
|
||||
uni.navigateTo({ url: '/pages/ec/health/ecalert' });
|
||||
},
|
||||
async loadHistory() {
|
||||
this.loading = true;
|
||||
let historyList: any[] = [];
|
||||
try {
|
||||
// UTS Android 风格获取 Supabase 查询结果
|
||||
const resp = await supa.from('ps_push_msg_raw').select('*',{}).order('created_at', { ascending: false }).limit(100).execute();
|
||||
if (resp && resp.error) {
|
||||
uni.showToast({ title: '获取历史告警失败', icon: 'none' });
|
||||
} else if (resp && Array.isArray(resp.data)) {
|
||||
historyList = resp.data;
|
||||
} else {
|
||||
historyList = [];
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '请求异常', icon: 'none' });
|
||||
}
|
||||
this.messages = (historyList || []).map((item): AlertMessage => {
|
||||
// item.raw_data 可能为 UTSJSONObject,parseAlertMessage 返回强类型对象
|
||||
const parseResult = parseAlertMessage(item.raw_data ? item.raw_data : item);
|
||||
return {
|
||||
type: parseResult.type,
|
||||
mid:parseResult.mid,
|
||||
content: (parseResult.mid ? `[${parseResult.mid}] ` : '') + parseResult.title + (parseResult.content ? (': ' + parseResult.content) : ''),
|
||||
timeStr: parseResult.time || '',
|
||||
raw_data: item.raw_data
|
||||
};
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadHistory();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.alert-container {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
</style>
|
||||
157
pages/ec/health/ecalert.uvue
Normal file
157
pages/ec/health/ecalert.uvue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<view class="alert-container">
|
||||
<view class="top-bar">
|
||||
<button @click="goHistory" style="margin-right: 16rpx;">历史告警</button>
|
||||
<text class="title">健康告警推送</text>
|
||||
</view>
|
||||
<scroll-view style="height: 600rpx; border: 1px solid #ccc; padding: 8rpx;" direction="vertical" scroll-with-animation>
|
||||
<view v-for="(msg, idx) in messages" :key="idx"
|
||||
style="font-size: 26rpx; color: #333; margin-bottom: 12rpx;">
|
||||
<text>{{ msg.timeStr }}:</text>
|
||||
<text>{{ msg.content }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { parseAlertMessage } from './alertparse.uts';
|
||||
import { AkSupaRealtime } from '@/components/supadb/aksuparealtime.uts';
|
||||
import { SUPA_KEY, WS_URL } from '@/ak/config.uts';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
wsUrl: WS_URL,
|
||||
channel: 'realtime:public:ps_push_msg_raw',
|
||||
messages: [] as Array<{ content : string; timeStr : string }>,
|
||||
realtime: null as AkSupaRealtime | null
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.initRealtime();
|
||||
},
|
||||
onUnload() {
|
||||
if (this.realtime) {
|
||||
this.realtime.close({});
|
||||
this.realtime = null;
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
// 页面回到前台时检查 WebSocket 连接状态,必要时重连
|
||||
if (!this.realtime || !this.realtime.isOpen) {
|
||||
console.log('onShow: WebSocket未连接,尝试重连');
|
||||
// this.initRealtime();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goHistory() {
|
||||
uni.navigateTo({ url: '/pages/ec/health/ecalert-history' });
|
||||
},
|
||||
// 重连相关参数
|
||||
reconnectDelay: 3000,
|
||||
reconnectMax: 10,
|
||||
reconnectCount: 0,
|
||||
reconnectTimer: null as any,
|
||||
initRealtime() {
|
||||
if (this.realtime) {
|
||||
this.realtime.close({});
|
||||
this.realtime = null;
|
||||
}
|
||||
const wsUrl: string = this.wsUrl as string;
|
||||
const channel: string = this.channel;
|
||||
const self = this;
|
||||
this.reconnectCount = 0;
|
||||
const createRealtime = () => {
|
||||
const newRealtime = new AkSupaRealtime({
|
||||
url: wsUrl,
|
||||
channel: channel,
|
||||
apikey: SUPA_KEY,
|
||||
onOpen() {
|
||||
self.reconnectCount = 0;
|
||||
if (self.reconnectTimer) {
|
||||
clearTimeout(self.reconnectTimer);
|
||||
self.reconnectTimer = null;
|
||||
}
|
||||
},
|
||||
onClose() {
|
||||
// 断开后自动重连
|
||||
self.tryReconnect();
|
||||
},
|
||||
onError(err) {
|
||||
// 错误后也尝试重连
|
||||
self.tryReconnect();
|
||||
},
|
||||
onMessage(data) {
|
||||
console.log(data)
|
||||
if (data && typeof data === 'object' && data.event === 'INSERT' && data.payload && data.payload.record) {
|
||||
const payload = data.payload;
|
||||
let content = '';
|
||||
let timeStr = '';
|
||||
const record = payload.record;
|
||||
if (record.raw_data) {
|
||||
content = record.raw_data;
|
||||
} else if (record.message) {
|
||||
content = record.message;
|
||||
} else {
|
||||
content = JSON.stringify(record);
|
||||
}
|
||||
if (record.created_at) {
|
||||
try {
|
||||
const d = new Date(record.created_at);
|
||||
timeStr = d.toLocaleString();
|
||||
} catch (e) {
|
||||
timeStr = record.created_at;
|
||||
}
|
||||
}
|
||||
const parseResult = parseAlertMessage(record.raw_data ? record.raw_data : record);
|
||||
console.log(parseResult)
|
||||
console.log(this.messages, self.messages)
|
||||
self.messages.unshift({
|
||||
content: (parseResult.mid ? `[${parseResult.mid}] ` : '') + parseResult.title + (parseResult.content ? (': ' + parseResult.content) : ''),
|
||||
timeStr: timeStr || parseResult.time || ''
|
||||
});
|
||||
if (self.messages.length > 100) self.messages.length = 100;
|
||||
}
|
||||
}
|
||||
});
|
||||
self.realtime = newRealtime;
|
||||
newRealtime.connect();
|
||||
};
|
||||
createRealtime();
|
||||
},
|
||||
tryReconnect() {
|
||||
if (this.reconnectCount >= this.reconnectMax) {
|
||||
console.warn('WebSocket重连已达最大次数');
|
||||
return;
|
||||
}
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
this.reconnectCount++;
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
console.log('WebSocket重连中...', this.reconnectCount);
|
||||
// this.initRealtime();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.alert-container {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
</style>
|
||||
41
pages/ec/health/quick-check.uvue
Normal file
41
pages/ec/health/quick-check.uvue
Normal file
@@ -0,0 +1,41 @@
|
||||
<!-- 养老管理系统 - 健康检查快捷入口 -->
|
||||
<template>
|
||||
<view class="quick-check-page">
|
||||
<view class="header">
|
||||
<text class="title">健康检查</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="desc">此处可快速录入健康检查数据或跳转到健康详情页面。</text>
|
||||
<!-- 可根据实际需求补充表单或快捷入口 -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// 可根据实际需求补充逻辑
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quick-check-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
.desc {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user