feat(desktop-client): 阶段4-6 Electron EXE 客户端(登录/课表/作业/通知/成绩/设置 + Socket.IO 实时 + IndexedDB 离线缓存与自动同步 + electron-builder 打包)
This commit is contained in:
1 parent
4566d7cd5e
commit
6e4c7c1050
42 files changed
+5767
-60
No files matched your search
@@ -0,0 +1,148 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import {
|
||||
PRIORITY_LABELS,
|
||||
SOCKET_EVENTS,
|
||||
type GradeDto,
|
||||
type HomeworkDto,
|
||||
type NotificationDto,
|
||||
type ServerToClientEvents,
|
||||
} from '@classhelper/shared';
|
||||
import { socketUrlOf } from '../config.js';
|
||||
|
||||
type RealtimeEventName = keyof ServerToClientEvents;
|
||||
type EventHandler = (payload: unknown) => void;
|
||||
|
||||
/**
|
||||
* 实时通道(学生端)。
|
||||
* 与 Web 管理端使用同一套事件名;收到事件后除了通知用户,
|
||||
* 还会把数据写回 IndexedDB,保证断网时看到的是最后同步到的内容。
|
||||
*/
|
||||
export const useRealtimeStore = defineStore('realtime', () => {
|
||||
const connected = ref(false);
|
||||
const connecting = ref(false);
|
||||
const lastEventName = ref<string | null>(null);
|
||||
const lastEventAt = ref<number | null>(null);
|
||||
const eventCount = ref(0);
|
||||
|
||||
const handlers = new Map<string, Set<EventHandler>>();
|
||||
let socket: Socket | null = null;
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (connected.value) return '实时连接正常';
|
||||
if (connecting.value) return '正在连接服务器';
|
||||
return '实时连接已断开';
|
||||
});
|
||||
|
||||
function dispatch(event: string, payload: unknown): void {
|
||||
for (const handler of handlers.get(event) ?? []) handler(payload);
|
||||
}
|
||||
|
||||
function mark(event: string): void {
|
||||
lastEventName.value = event;
|
||||
lastEventAt.value = Date.now();
|
||||
eventCount.value += 1;
|
||||
}
|
||||
|
||||
function bind(instance: Socket): void {
|
||||
instance.on('connect', () => {
|
||||
connected.value = true;
|
||||
connecting.value = false;
|
||||
dispatch('__connected__', null);
|
||||
});
|
||||
instance.on('disconnect', () => {
|
||||
connected.value = false;
|
||||
dispatch('__disconnected__', null);
|
||||
});
|
||||
instance.on('connect_error', (error: Error) => {
|
||||
connected.value = false;
|
||||
connecting.value = false;
|
||||
dispatch('__connect_error__', error.message);
|
||||
});
|
||||
|
||||
instance.on(SOCKET_EVENTS.notificationNew, (payload: NotificationDto) => {
|
||||
mark(SOCKET_EVENTS.notificationNew);
|
||||
ElMessage({
|
||||
message: `【${PRIORITY_LABELS[payload.priority] ?? payload.priority}】${payload.title}`,
|
||||
type: payload.priority === 'URGENT' ? 'error' : 'info',
|
||||
duration: 5000,
|
||||
showClose: true,
|
||||
});
|
||||
dispatch(SOCKET_EVENTS.notificationNew, payload);
|
||||
});
|
||||
|
||||
instance.on(SOCKET_EVENTS.homeworkNew, (payload: HomeworkDto) => {
|
||||
mark(SOCKET_EVENTS.homeworkNew);
|
||||
ElMessage.success(`新作业:${payload.title}`);
|
||||
dispatch(SOCKET_EVENTS.homeworkNew, payload);
|
||||
});
|
||||
|
||||
instance.on(SOCKET_EVENTS.homeworkUpdated, (payload: HomeworkDto & { deleted?: boolean }) => {
|
||||
mark(SOCKET_EVENTS.homeworkUpdated);
|
||||
dispatch(SOCKET_EVENTS.homeworkUpdated, payload);
|
||||
});
|
||||
|
||||
instance.on(SOCKET_EVENTS.gradeUpdated, (payload: GradeDto) => {
|
||||
mark(SOCKET_EVENTS.gradeUpdated);
|
||||
ElMessage.success(`成绩更新:${payload.examName} ${payload.score}/${payload.totalScore}`);
|
||||
dispatch(SOCKET_EVENTS.gradeUpdated, payload);
|
||||
});
|
||||
|
||||
instance.on(SOCKET_EVENTS.scheduleUpdated, (payload: unknown) => {
|
||||
mark(SOCKET_EVENTS.scheduleUpdated);
|
||||
dispatch(SOCKET_EVENTS.scheduleUpdated, payload);
|
||||
});
|
||||
}
|
||||
|
||||
function connect(serverUrl: string, token: string): void {
|
||||
disconnect();
|
||||
connecting.value = true;
|
||||
socket = io(socketUrlOf(serverUrl), {
|
||||
auth: { token, clientType: 'desktop' },
|
||||
transports: ['websocket', 'polling'],
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1500,
|
||||
reconnectionDelayMax: 10000,
|
||||
});
|
||||
bind(socket);
|
||||
}
|
||||
|
||||
function disconnect(): void {
|
||||
socket?.removeAllListeners();
|
||||
socket?.disconnect();
|
||||
socket = null;
|
||||
connected.value = false;
|
||||
connecting.value = false;
|
||||
}
|
||||
|
||||
function on(
|
||||
event: RealtimeEventName | '__connected__' | '__disconnected__' | '__connect_error__',
|
||||
handler: EventHandler,
|
||||
): void {
|
||||
const set = handlers.get(event) ?? new Set<EventHandler>();
|
||||
set.add(handler);
|
||||
handlers.set(event, set);
|
||||
}
|
||||
|
||||
function off(
|
||||
event: RealtimeEventName | '__connected__' | '__disconnected__' | '__connect_error__',
|
||||
handler: EventHandler,
|
||||
): void {
|
||||
handlers.get(event)?.delete(handler);
|
||||
}
|
||||
|
||||
return {
|
||||
connected,
|
||||
connecting,
|
||||
statusText,
|
||||
lastEventName,
|
||||
lastEventAt,
|
||||
eventCount,
|
||||
connect,
|
||||
disconnect,
|
||||
on,
|
||||
off,
|
||||
};
|
||||
});
|
||||
Reference in new issue
Block a user