export type Store = { /** 当前用户 */ user: { id: number; /** 身份证号 */ identityNumber: string; /** 学号 */ studentCodeList: string[]; /** 显示的姓名 */ displayName: string; /** @deprecated 姓名,但可能为空 */ name: string; /** 学生手机号 */ phoneNumber: string; /** 属于那个学校 */ schoolDomain: SchoolDomain; /** 顶级机构id */ rootOrgId: number; /** 底照URL */ photoPath: string; /** 机构名称 */ orgName: string; /** 顶级机构名称 */ rootOrgName: string; /** 用户的另一个id */ key: string; /** 登录认证信息 */ token: string | null; /** 专业 */ specialty: string; }; /** 学生端配置。 Q代表qmth */ QECSConfig: { /** 学号登录别名 */ STUDENT_CODE_LOGIN_ALIAS: string; /** 身份证号登录别名 */ IDENTITY_NUMBER_LOGIN_ALIAS: string; /** 防作弊配置 !!!需对json进行转换!!! */ PREVENT_CHEATING_CONFIG: Partial< [ "FULL_SCREEN_TOP", "DISABLE_REMOTE_ASSISTANCE", "DISABLE_VIRTUAL_CAMERA", "DISABLE_MULTISCREEN" ] >; /** 学校是否有定制logo !!!需对json进行转换!!! */ IS_CUSTOM_MENU_LOGO: boolean; /** 定制logo的URL */ CUS_MENU_LOGO_FILE_URL: string; /** 定制登录页背景图的URL */ STUDENT_CLIENT_BG_PICTURE_URL: string; /** 学生端控制台设置 */ STUDENT_CLIENT_CONSOLE_CONFIG: string; /** 首页背景图 */ LOGO_FILE_URL: string; /** 产品名称,登录页显示 */ OE_STUDENT_SYS_NAME: string; /** 登录页可选的登录类型 !!!需对json进行转换!!! */ LOGIN_TYPE: Partial<["STUDENT_CODE", "IDENTITY_NUMBER"]>; ROOT_ORG_ID: number; /** @deprecated 登录支持的客户端类型。新版只支持Electron包。 */ LOGIN_SUPPORT: Partial<["NATIVE", "BROWSER"]>; }; /** 电脑时间管理 */ sysTime: { /** 与服务器差异,服务器时间大于学生端时间,则返回正数,否则返回负数 */ difference: number; /** 网络延迟 通过网络请求来判断 */ rtt: number; /** 上次的Date.now() 有的电脑上存在Date.now()不更新 */ lastTime: number; /** 判断时钟是否不走动了 */ isTimerFrozen: boolean; }; /** 网络状况 */ network: { /** 网络状况是即时的,如何获取? */ /** computed */ isOK: boolean; /** 百度不通,则网络不通 */ pingBaidu: boolean; /** 云平台api是否通畅 */ pingQmth: boolean; /** 阿里云文件存储 阿里云日志 分开? */ pingAli: boolean; /** websocket */ pingWebSocket: boolean; }; /** 菜单项 */ menus: Array<{ /** 菜单名称 */ name: string; /** 菜单指向链接,是path */ link: string; /** 此处要细化为 type = 'STU_NOTICE' | 'STU_MODIFY_PWD',在router中也要用 */ routeCode: string; }>; /** app下载 */ appDownload: { /** 是否开放下载 */ enabled: boolean; /** 下载链接 */ url: string; }; /** 站内消息 */ siteMessage: { messages: SiteMessage[]; /** 第一个未读消息 popup用 computed */ firstUnreadMessage: SiteMessage | null; /** 忽略的站内消息,不弹popup */ ignoreMessageIds: number[]; /** 未读消息总数 */ unreadCount: number; }; siteMessagesTimeStamp: number; /** 在线考试待考列表 */ examList: OnlineExam[]; /** 在线考试已结束的考试列表 */ endedExamList: OnlineExam[]; /** 在线作业 */ homeworkList: OnlineExam[]; /** 在线练习 */ practiceList: PracticeExam[]; /** 当前开考的exam,非断点续考才会需要发出这么网络请求,正常开考是从前页面带过来的 */ exam: { /** 考生的考试记录id */ examRecordDataId: number; /** 考试批次id */ examId: number; /** 当前试题序号,起始为1,初始为1. 和route里面的order不好算优先级和时机,暂时不用这个字段了 */ // order: number; /** 考试类型 */ examType: ExamType; /** 课程名称 */ courseName: string; /** 是否开启微信作答 */ weixinAnswerEnabled: boolean; /** 是否开启人脸比对 */ faceCheckEnabled: boolean; /** 是否开启活体检测 */ faceLivenessEnabled: boolean; /** 活体检测选项 */ faceLivenessOption: { /** 进入考试后多久开启活体检测(分钟) */ faceVerifyMinute: number; /** 活体检测类型 S1: FaceID S2: 自研活体 */ identificationOfLivingBodyScheme: "S1" | "S2"; }; /** 抓拍间隔(秒) */ SNAPSHOT_INTERVAL: number; /** 考试冻结(秒) */ freezeTime: number; /** 考试时长(秒) */ duration: number; /** 考试剩余时间(毫秒ms) */ remainTime: number; /** 是否开启微信作答方式 */ WEIXIN_ANSWER_ENABLED: boolean; /** 练习显示答案的类型 IN_PRACTICE:在练习过程中显示答案 NO_ANSWER:练习过程中不显示答案 */ practiceType: "IN_PRACTICE" | "NO_ANSWER"; /** 试卷结构 */ paperStruct: PaperStruct; /** 试题结构 */ examQuestionList: ExamQuestion[]; /** 试题过滤类型 */ questionFilterType: "ALL" | "ANSWERED" | "SIGNED" | "UNANSWERED"; allAudioPlayTimes: { audioName: string; times: number }[]; questionQrCodeScanned: { order: number }; questionAnswerFileUrl: { order: number; fileUrl: string; transferFileType: string; }[]; }; // /** 考试中的状态 */ // examing: {}; // /** 获取到的camera source */ camera: { /** 获取摄像头,且在多页面共享,减少打开摄像头失败的次数 */ stream: MediaStream | null; }; globalMaskCount: 0; spinMessage?: string; }; type SchoolDomain = `${string}.ecs.qmth.com.cn`; export type SiteMessage = { id: number; hasRead: boolean; hasRecalled: boolean; title: string; content: string; publishTime: string; }; export type ExamType = "ONLINE" | "ONLINE_HOMEWORK" | "PRACTICE"; type BaseExam = { /** 考试批次id */ examId: number; /** 考生id,用户的每场考试都有一个考生id */ examStudentId: number; /** 考生的考试记录id */ examRecordDataId: number; /** 考试类型 */ examType: ExamType; /** 课程名称 */ courseName: string; /** 课程层次 */ courseLevel: string; /** 专业名称 */ specialtyName: string; /** 考试开始时间。日期时间字符串,以前叫后台改为数字,但后台不改,遗留的设计错误。 */ startTime: string; /** 考试结束时间 */ endTime: string; }; type ExamCycle = { /** 考试周期是否开启循环 */ examCycleEnabled: boolean; /** 周循环中周几开启考试 1-7 */ examCycleWeek: number[]; /** 周期循环中开启的时间段 HHmm [['08:00', '10:30'], ['15:00', '17:00']] */ examCycleTimeRange: { timeRange: [string, string] }[]; }; export type OnlineExam = BaseExam & ExamCycle & { /** 课程id */ courseId: number; /** 是否显示考生承诺书 */ showUndertaking: boolean; /** 考生承诺书内容 */ undertaking: string; /** 是否启用人脸比对 */ faceEnable: boolean; /** 是否启用人脸比对的强制或非强制 */ faceCheck: boolean; /** 剩余考试次数 */ allowExamCount: number; /** 是否允许查看客观分 */ isObjScoreView: boolean; }; export type PracticeExam = ExamCycle & { /** 考试批次id */ examId: number; /** 考试批次名称 */ examName: string; /** 考试类型 */ examType: ExamType; /** 考生id,用户的每场考试都有一个考生id */ examStudentId: number; /** 课程id */ courseId: number; /** 课程名称 */ courseName: string; /** 课程code */ courseCode: string; /** 考试开始时间。 */ startTime: string; /** 考试结束时间 */ endTime: string; /** 练习次数 */ practiceCount: number; /** 剩余练习次数 */ allowExamCount: number; /** 最近正确率(后端已乘100) */ recentObjectiveAccuracy: number; /** 平均正确率(后端已乘100) */ aveObjectiveAccuracy: number; /** 最高正确率(后端已乘100) */ maxObjectiveAccuracy: number; }; export type OfflineExam = BaseExam & { /** 机构名称 */ orgName: string; /** 考试周期是否开启循环 */ offlineFiles: Array<{ offlineFileUrl: string; originalFileName: string }>; /** 试卷id */ paperId: string; }; export type OnlinePracticeRecord = { id: number; /** 考试开始时间。日期时间字符串,以前叫后台改为数字,但后台不改,遗留的设计错误。 */ startTime: string; /** 考试结束时间 */ endTime: string; /** 练习时长 类型待确认??? */ usedExamTime: number; /** 总题量 */ totalQuestionCount: number; /** 正确的数量 */ succQuestionNum: number; /** 错误的数量 */ failQuestionNum: number; /** 未答的数量 */ notAnsweredCount: number; /** 客观题正确率(后端已乘100) */ objectiveAccuracy: number; }; export type OnlinePracticeRecordResult = { courseName: string; courseCode: string; /** 客观题正确率(后端已乘100) */ objectiveAccuracy: string; paperStructInfos: Array<{ index: number; /** 题目分类 */ title: string; /** 题量 */ questionCount: number; /** 正确的数量 */ succQuestionNum: number; /** 错误的数量 */ failQuestionNum: number; /** 未答的数量 */ notAnsweredCount: number; }>; }; // export type PaperStruct = { // defaultPaper: { // /** index = mainNumber - 1 ??待确定 */ // questionGroupList: Array<{ // groupName: string; // groupScore: number; // questionWrapperList: Array<{ // questionId: string; // body: string; // examQuestionList: ExamQuestion[]; // /** 小题的列表,学生端用不着,只用到它的length */ // questionUnitWrapperList: Array<{ id: string }>; // questionUnitList: Array<{ // body: string; // }>; // }>; // }>; // }; // }; export type QuestionUnitItem = { body: string; questionOptionList: Array<{ body: string }>; questionType: string; rightAnswer: string[]; }; export type QuestionWrapperItem = { questionId: string; /** 试题先导内容,一般出现在套题中 */ body: string; /** 试题小题的信息列表 */ examQuestionList: ExamQuestion[]; /** 试题小题的内容列表,一般应该是用不到的 */ questionUnitList: QuestionUnitItem[]; /** 小题的列表,学生端用不着,只用到它的length */ questionUnitWrapperList: Array<{ id: string }>; limitedPlayTimes: number; }; export type PaperStruct = { defaultPaper: { /** index = mainNumber - 1 ??待确定 */ questionGroupList: Array<{ groupName: string; groupScore: number; /** 试题列表 */ questionWrapperList: QuestionWrapperItem[]; }>; }; }; export type ExamQuestion = { /** 试题id */ questionId: string; /** 题目序号 */ order: number; /** 题目在答题中的序号。 old groupOrder */ inGroupOrder: number; /** 限制音频播放次数。从paperStruct[][]['limitedPlayTimes']得到。前端添加。 */ limitedPlayTimes: number; questionScore: number; /** 学生填写的答案,和C端、App端结构一致 */ studentAnswer: string; /** 试题类型 */ questionType: | "SINGLE_CHOICE" | "MULTIPLE_CHOICE" | "TRUE_OR_FALSE" | "FILL_UP" | "ESSAY"; /** 小题乱序。细化??? */ optionPermutation: number[]; /** 大题名称 */ groupName: string; /** 什么的总分??? */ groupTotal: number; /** 是否被标上星号。 TODO: 改名为 isStarred */ isSign: boolean; /** 大题号 */ mainNumber: number; /** 小题号 */ subNumber: number; /** 试题内容。重点要重构音频、填空题###的处理逻辑 */ body: string; /** 是否为套题。此处要梳理数据结构,重新计算!!! */ isNestedQuestion: boolean; /** 文本作答的题目,是否开启音频作答。和考试的weixinAnswerEnabled有关。 */ answerType: "SINGLE_AUDIO" | null; /** 练习时有正确答案 */ rightAnswer?: string[]; /** 后端给的类型前端好像用不着,待确认??? */ questionOptionList: any; /** 题目中是否有音频 */ hasAudio: boolean; /** 试题内容。通过网络获取。 */ questionContent: string; /** 试题内容是否已通过网络获取到。 TODO: 改名为 gotQuestionContent */ getQuestionContent: boolean; /** 答案是否已经被用户更新过了 */ dirty: boolean; /** 只有第一题有此数据,用来像服务器保存音频播放次数 */ audioPlayTimes: { audioName: string; times: number }[]; }; export type ExamInProgress = { /** "S-101000" 请重试 */ code: "000000" | "S-101000"; /** 是否超过断点次数限制 */ isExceed: boolean; /** 允许的最大断点次数 */ maxInterruptNum: number; /** 允许的最大切屏次数 */ maxSwitchScreenCount: number; examId: number; examRecordDataId: number; }; // export const REMOTE_APPS = [ // ["qq", "QQ"], // ["teamviewer", "TeamViewer"], // ["lookmypc", "LookMyPC"], // ["xt", "协通"], // ["winaw32", "Symantec PCAnywhere"], // ["pcaquickconnect", "Symantec PCAnywhere"], // ["sessioncontroller", "Symantec PCAnywhere"], // [/sunloginclient/gi, "向日葵"], // [/sunloginremote/gi, "向日葵"], // [/选择免安装运行,截图识别/gi, "向日葵"], // ["wemeetapp", "腾讯会议"], // ["wechat", "微信"], // ] as const; // define process.env // clientVersion.ts 管理可用客户端版本。 // api: getCurrentClientVersion isSupportedClientVersion /** 得到当前客户端版本 1.9.* */ type GetCurrentClientVersion = () => string; type IsSupportedClientVersion = () => boolean; // updateManager.ts 应用(非客户端)是否应该更新了 // api: isNewWebAppAvailable isNewBackendAvailable getNewBackendDesc // native.ts 管理原生及系统命令,详细说明path和相对path // api: isElectron execCmd readFile getRemoteApps getVirtualCams // runtimeMonitor.ts 检测当前运行的环境是否合法 // 检测是否打开了控制台;检测是否使用了高版本的chrome(feature检测) // 检测onResize;截屏 // cache design // service worker ; image cache; audio cache/prefetch // router design 严格受控的页面跳转,既考虑刷新,也考虑缓存 // localStorage design // loginType accountValue // sessionStorage design // store 防破解?仅保存 user, QECSConfig 信息 // 开考流程 // 1. 确保每一次请求的结果都得到充分利用,即在页面中保存好信息,不做多余的重试,不在中途刷新页面,除敏感信息外要从上个页面带到下个页面 // 2. 开考和考试中的信息获取要隔离,方便从不同的页面中进入考试 // 3. 进入页面后,不轻易退出,要做足够的重试。退出的条件要设置充分,比如网络断了。