feat: 电子药箱小程序 - 4Tab药品管理

- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡
- 药品百科: 搜索 + 分类筛选 + 20种药品静态数据
- 惠教中心: 4条药品使用指南课程
- 我的: 家庭成员信息管理
- 自定义TabBar + navigation-bar组件
- SVG药品分类插图
This commit is contained in:
陈誉午
2026-07-29 11:20:52 +08:00
commit 994b267f5c
683 changed files with 43849 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
// utils/sse.ts
import { request } from './request'
import { BASE_URL } from '../env'
//唯一id
const sign = Date.now().toString(36) + Math.random().toString(36).slice(2)
const listeners: Record<string, ((data: any) => void)[]> = {}
let requestTask: any = null
let reconnectTimer: any = null
let requestTimer: any = null
// 不支持TextDecoder的话手动转码utf8
function utf8Decode(uint8: Uint8Array): string {
let str = '';
let i = 0;
while (i < uint8.length) {
const byte1 = uint8[i];
let charCode = byte1;
if (byte1 <= 0x7F) {
i += 1;
} else if (byte1 >= 0xC0 && byte1 <= 0xDF) {
const byte2 = uint8[i + 1];
charCode = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F);
i += 2;
} else if (byte1 >= 0xE0 && byte1 <= 0xEF) {
const byte2 = uint8[i + 1];
const byte3 = uint8[i + 2];
charCode = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
i += 3;
} else if (byte1 >= 0xF0 && byte1 <= 0xF7) {
const byte2 = uint8[i + 1];
const byte3 = uint8[i + 2];
const byte4 = uint8[i + 3];
charCode = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
i += 4;
}
str += String.fromCharCode(charCode);
}
return str;
}
export function startSSE() {
if (requestTimer) return //已经启动过就不重复
// if (wx.getStorageSync("sseId")) leave();
wx.setStorageSync("sseId", sign)
requestSSE()
//30s连接一次,保持连接
// requestTimer = setInterval(() => {
// requestSSE()
// }, 30000)
}
async function requestSSE() {
try {
requestTask = (wx as any).request({
url: `${BASE_URL}/api/auth/message/subscribe/${sign}`,
method: 'GET',
enableChunked: true,
responseType: 'arraybuffer',
header: { 'Content-Type': 'application/json' },
})
requestTask?.onChunkReceived?.((chunk: any) => {
const uint8 = new Uint8Array(chunk.data)
let chunkStr = ''
try {
//真机环境不支持TextDecoder
// @ts-ignore
chunkStr = new TextDecoder('utf-8').decode(uint8);
} catch {
chunkStr = utf8Decode(uint8);
}
const lines = chunkStr.trim().split('\n')
for (const line of lines) {
try {
// console.log(JSON.parse(line), 'lllllll')
if (JSON.parse(line).msg == 'auto close') {
requestSSE()
}
} catch {
if (line.startsWith('data:')) {
try {
const json = JSON.parse(line.slice(5))
const app = getApp<IAppOption>()
const data = json.data
const eventName = json.event
//分类保存
if (data && data.type) {
if (!app.globalData.sseStore) app.globalData.sseStore = {}
if (!app.globalData.sseStore[data.type]) app.globalData.sseStore[data.type] = []
//可能会传sku对应商品,直播评论,分类的通知
//存入传入的数组
app.globalData.sseStore[data.type].push(data)
//最多只保留100条
if (app.globalData.sseStore[data.type].length > 100) {
app.globalData.sseStore[data.type].shift()
}
}
if (eventName && listeners[eventName]) {
listeners[eventName].forEach(fn => fn(data))
}
} catch (err) {
console.error('解析返回数据失败:', err)
}
}
}
}
})
} catch (err) {
console.warn('sse请求失败:', err)
}
requestTask.onError?.((err: any) => {
console.warn('sse连接出错,5秒后重连', err)
scheduleReconnect()
})
requestTask.then?.(() => {
console.log('sse连接出错,5秒后重连')
scheduleReconnect()
})
}
//注册事件监听
export function addCustomEventListener(eventName: string, fn: (data: any) => void) {
if (!listeners[eventName]) listeners[eventName] = []
listeners[eventName].push(fn)
}
//移除事件监听
export function removeCustomEventListener(eventName: string, fn: (data: any) => void) {
if (!listeners[eventName]) return
listeners[eventName] = listeners[eventName].filter(f => f !== fn)
if (listeners[eventName].length === 0) delete listeners[eventName]
}
//加入频道
export async function join(name: string) {
const app = getApp<IAppOption>();
try {
if (app.globalData.joinedArray.includes(name)) {
return; //加入过就不加入了
}
app.globalData.joinedArray = [name, ...app.globalData.joinedArray]; //为什么在request后就不能push了??
await request({
options: {
url: `/auth/message/join`,
method: 'POST',
data: { id: sign, name }
},
isLoading: false
})
} catch {
app.globalData.joinedArray = app.globalData.joinedArray.filter(item => item !== name);
}
}
//离开频道
export async function leave(name: string) {
const app = getApp<IAppOption>()
try {
app.globalData.joinedArray = app.globalData.joinedArray.filter(item => item !== name);
await request({
options: {
url: `/auth/message/leave`,
method: 'POST',
data: { id: wx.getStorageSync('sseId'), name }
// data: { id: wx.getStorageSync('sseId'), name: String(shopId) }
},
isLoading: false
})
} catch {
app.globalData.joinedArray = [name, ...app.globalData.joinedArray];
}
}
//重连
function scheduleReconnect() {
stopSSE()
reconnectTimer = setTimeout(() => {
console.log('重连sse中...')
startSSE()
}, 5000)
}
//停止sse
export function stopSSE() {
if (requestTask) {
requestTask.abort?.()
requestTask = null
}
clearTimeout(reconnectTimer)
}
//获取sse的某个分类数据
export function getSSEStore(type: string) {
const app = getApp<IAppOption>()
return app.globalData.sseStore[type] || []
}
//删除sse的某个分类数据
export function clearSSEStore(type?: string) {
const app = getApp<IAppOption>()
if (type) {
app.globalData.sseStore[type] = []
} else {
Object.keys(app.globalData.sseStore).forEach(k => {
app.globalData.sseStore[k] = []
})
}
}