345 lines
9.6 KiB
TypeScript
345 lines
9.6 KiB
TypeScript
/**
|
||
* 自然写互动课堂PC端应用软件 V1.0
|
||
*
|
||
* index.ts - Pinia状态管理(全局Store)
|
||
*
|
||
* 功能说明:
|
||
* - 用户认证状态管理
|
||
* - 课堂状态管理(当前课堂/学生列表/笔迹数据)
|
||
* - 设备连接状态管理
|
||
* - 作业批改状态管理
|
||
* - WebSocket实时数据同步
|
||
* - 持久化存储(electron-store)
|
||
*/
|
||
|
||
import { defineStore } from 'pinia';
|
||
import { ref, computed, reactive } from 'vue';
|
||
|
||
/* ======================== 类型定义 ======================== */
|
||
|
||
/** 应用视图模式 */
|
||
type ViewMode = 'prepare' | 'lesson' | 'grade' | 'report';
|
||
|
||
/** 设备信息 */
|
||
interface DeviceState {
|
||
id: string;
|
||
name: string;
|
||
type: 'usb' | 'ble';
|
||
status: 'connected' | 'disconnected' | 'error';
|
||
battery: number;
|
||
}
|
||
|
||
/** 学生在线状态 */
|
||
interface StudentOnlineState {
|
||
studentId: string;
|
||
name: string;
|
||
penId: string;
|
||
online: boolean;
|
||
lastActive: number;
|
||
strokeCount: number;
|
||
}
|
||
|
||
/** 课堂互动数据 */
|
||
interface ClassroomLiveData {
|
||
classroomId: string;
|
||
className: string;
|
||
startTime: number;
|
||
onlineStudents: StudentOnlineState[];
|
||
totalStrokes: number;
|
||
isRecording: boolean;
|
||
}
|
||
|
||
/** 批改任务 */
|
||
interface GradeTask {
|
||
assignmentId: string;
|
||
studentId: string;
|
||
studentName: string;
|
||
status: 'pending' | 'ai_graded' | 'reviewed' | 'completed';
|
||
aiScore: number;
|
||
teacherScore: number;
|
||
feedback: string;
|
||
}
|
||
|
||
/* ======================== 用户Store ======================== */
|
||
|
||
/**
|
||
* 用户认证与信息状态管理
|
||
*/
|
||
export const useUserStore = defineStore('user', () => {
|
||
/** 是否已登录 */
|
||
const isLoggedIn = ref(false);
|
||
/** 当前用户信息 */
|
||
const userInfo = ref<{
|
||
userId: string;
|
||
name: string;
|
||
role: string;
|
||
phone: string;
|
||
schoolId: string;
|
||
schoolName: string;
|
||
avatar: string;
|
||
} | null>(null);
|
||
/** 登录时间 */
|
||
const loginTime = ref(0);
|
||
/** Token过期时间 */
|
||
const tokenExpiresAt = ref(0);
|
||
|
||
/** 用户角色显示名 */
|
||
const roleLabel = computed(() => {
|
||
const roleMap: Record<string, string> = {
|
||
admin: '管理员',
|
||
teacher: '教师',
|
||
student: '学生',
|
||
parent: '家长'
|
||
};
|
||
return roleMap[userInfo.value?.role || ''] || '未知';
|
||
});
|
||
|
||
/**
|
||
* 登录成功后设置用户状态
|
||
*/
|
||
function setLoggedIn(user: typeof userInfo.value, expiresAt: number): void {
|
||
isLoggedIn.value = true;
|
||
userInfo.value = user;
|
||
loginTime.value = Date.now();
|
||
tokenExpiresAt.value = expiresAt;
|
||
console.log(`[Store] 用户登录: ${user?.name} (${user?.role})`);
|
||
}
|
||
|
||
/**
|
||
* 退出登录
|
||
*/
|
||
function logout(): void {
|
||
isLoggedIn.value = false;
|
||
userInfo.value = null;
|
||
loginTime.value = 0;
|
||
tokenExpiresAt.value = 0;
|
||
console.log('[Store] 用户已退出');
|
||
}
|
||
|
||
return { isLoggedIn, userInfo, loginTime, tokenExpiresAt, roleLabel, setLoggedIn, logout };
|
||
});
|
||
|
||
/* ======================== 课堂Store ======================== */
|
||
|
||
/**
|
||
* 课堂状态管理
|
||
* 管理当前课堂的实时数据
|
||
*/
|
||
export const useClassroomStore = defineStore('classroom', () => {
|
||
/** 当前视图模式 */
|
||
const viewMode = ref<ViewMode>('prepare');
|
||
/** 当前课堂数据 */
|
||
const liveData = ref<ClassroomLiveData | null>(null);
|
||
/** 是否在课堂中 */
|
||
const isInClass = ref(false);
|
||
/** WebSocket连接状态 */
|
||
const wsConnected = ref(false);
|
||
|
||
/** 在线学生数 */
|
||
const onlineCount = computed(() =>
|
||
liveData.value?.onlineStudents.filter(s => s.online).length || 0
|
||
);
|
||
/** 总学生数 */
|
||
const totalStudents = computed(() =>
|
||
liveData.value?.onlineStudents.length || 0
|
||
);
|
||
/** 在线率 */
|
||
const onlineRate = computed(() => {
|
||
const total = totalStudents.value;
|
||
return total > 0 ? Math.round((onlineCount.value / total) * 100) : 0;
|
||
});
|
||
|
||
/**
|
||
* 开始课堂
|
||
*/
|
||
function startClass(classroomId: string, className: string, students: StudentOnlineState[]): void {
|
||
liveData.value = {
|
||
classroomId,
|
||
className,
|
||
startTime: Date.now(),
|
||
onlineStudents: students,
|
||
totalStrokes: 0,
|
||
isRecording: false
|
||
};
|
||
isInClass.value = true;
|
||
viewMode.value = 'lesson';
|
||
console.log(`[Store] 课堂开始: ${className}, 学生${students.length}人`);
|
||
}
|
||
|
||
/**
|
||
* 结束课堂
|
||
*/
|
||
function endClass(): void {
|
||
const duration = liveData.value ? Date.now() - liveData.value.startTime : 0;
|
||
console.log(`[Store] 课堂结束, 时长=${Math.round(duration / 60000)}分钟, ` +
|
||
`笔迹=${liveData.value?.totalStrokes}`);
|
||
isInClass.value = false;
|
||
liveData.value = null;
|
||
}
|
||
|
||
/**
|
||
* 更新学生在线状态
|
||
*/
|
||
function updateStudentStatus(studentId: string, online: boolean): void {
|
||
const student = liveData.value?.onlineStudents.find(s => s.studentId === studentId);
|
||
if (student) {
|
||
student.online = online;
|
||
student.lastActive = Date.now();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 累加笔迹数据计数
|
||
*/
|
||
function addStrokeCount(count: number): void {
|
||
if (liveData.value) {
|
||
liveData.value.totalStrokes += count;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换视图模式
|
||
*/
|
||
function setViewMode(mode: ViewMode): void {
|
||
viewMode.value = mode;
|
||
console.log(`[Store] 视图切换: ${mode}`);
|
||
}
|
||
|
||
return {
|
||
viewMode, liveData, isInClass, wsConnected,
|
||
onlineCount, totalStudents, onlineRate,
|
||
startClass, endClass, updateStudentStatus, addStrokeCount, setViewMode
|
||
};
|
||
});
|
||
|
||
/* ======================== 设备Store ======================== */
|
||
|
||
/**
|
||
* 设备连接状态管理
|
||
*/
|
||
export const useDeviceStore = defineStore('device', () => {
|
||
/** 已连接设备列表 */
|
||
const devices = ref<DeviceState[]>([]);
|
||
/** 正在扫描BLE */
|
||
const isScanning = ref(false);
|
||
|
||
/** 已连接设备数 */
|
||
const connectedCount = computed(() =>
|
||
devices.value.filter(d => d.status === 'connected').length
|
||
);
|
||
|
||
/**
|
||
* 添加或更新设备
|
||
*/
|
||
function upsertDevice(device: DeviceState): void {
|
||
const idx = devices.value.findIndex(d => d.id === device.id);
|
||
if (idx >= 0) {
|
||
devices.value[idx] = device;
|
||
} else {
|
||
devices.value.push(device);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 移除设备
|
||
*/
|
||
function removeDevice(deviceId: string): void {
|
||
devices.value = devices.value.filter(d => d.id !== deviceId);
|
||
}
|
||
|
||
/**
|
||
* 更新设备电量
|
||
*/
|
||
function updateBattery(deviceId: string, battery: number): void {
|
||
const device = devices.value.find(d => d.id === deviceId);
|
||
if (device) {
|
||
device.battery = battery;
|
||
}
|
||
}
|
||
|
||
return { devices, isScanning, connectedCount, upsertDevice, removeDevice, updateBattery };
|
||
});
|
||
|
||
/* ======================== 批改Store ======================== */
|
||
|
||
/**
|
||
* 作业批改状态管理
|
||
*/
|
||
export const useGradeStore = defineStore('grade', () => {
|
||
/** 当前批改的作业ID */
|
||
const currentAssignmentId = ref('');
|
||
/** 批改任务列表 */
|
||
const gradeTasks = ref<GradeTask[]>([]);
|
||
/** 当前批改的学生索引 */
|
||
const currentTaskIndex = ref(0);
|
||
|
||
/** 待批改数 */
|
||
const pendingCount = computed(() =>
|
||
gradeTasks.value.filter(t => t.status === 'ai_graded' || t.status === 'pending').length
|
||
);
|
||
/** 已完成数 */
|
||
const completedCount = computed(() =>
|
||
gradeTasks.value.filter(t => t.status === 'completed' || t.status === 'reviewed').length
|
||
);
|
||
/** 总体进度百分比 */
|
||
const progressPercent = computed(() => {
|
||
const total = gradeTasks.value.length;
|
||
return total > 0 ? Math.round((completedCount.value / total) * 100) : 0;
|
||
});
|
||
/** 当前批改任务 */
|
||
const currentTask = computed(() => gradeTasks.value[currentTaskIndex.value] || null);
|
||
|
||
/**
|
||
* 加载批改任务列表
|
||
*/
|
||
function loadTasks(assignmentId: string, tasks: GradeTask[]): void {
|
||
currentAssignmentId.value = assignmentId;
|
||
gradeTasks.value = tasks;
|
||
currentTaskIndex.value = 0;
|
||
console.log(`[Store] 加载批改任务: ${tasks.length}份作业`);
|
||
}
|
||
|
||
/**
|
||
* 提交教师批改结果
|
||
*/
|
||
function submitGrade(studentId: string, score: number, feedback: string): void {
|
||
const task = gradeTasks.value.find(t => t.studentId === studentId);
|
||
if (task) {
|
||
task.teacherScore = score;
|
||
task.feedback = feedback;
|
||
task.status = 'reviewed';
|
||
console.log(`[Store] 批改完成: ${task.studentName}, 分数=${score}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换到下一个待批改任务
|
||
*/
|
||
function nextTask(): boolean {
|
||
for (let i = currentTaskIndex.value + 1; i < gradeTasks.value.length; i++) {
|
||
if (gradeTasks.value[i].status !== 'completed' && gradeTasks.value[i].status !== 'reviewed') {
|
||
currentTaskIndex.value = i;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 切换到上一个任务
|
||
*/
|
||
function prevTask(): boolean {
|
||
if (currentTaskIndex.value > 0) {
|
||
currentTaskIndex.value--;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
return {
|
||
currentAssignmentId, gradeTasks, currentTaskIndex,
|
||
pendingCount, completedCount, progressPercent, currentTask,
|
||
loadTasks, submitGrade, nextTask, prevTask
|
||
};
|
||
});
|