vue创建欢迎页、初始化数据库、管理员密码验证、页脚题库版本

This commit is contained in:
chenqiang 2025-08-03 20:20:06 +08:00
commit 0bf9ff662d
32 changed files with 11161 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
dist-electron
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

505
README.md Normal file
View File

@ -0,0 +1,505 @@
# 统计技能电子考试系统
## 需求说明
### 环境
Windows 7 64位 单机版
Windows 10 64位 单机版
### 其他
不同身份的人,在考试过程中使用的题是不一样的。
### 试题结构说明
- 试题类型:单选题、多选题、填空题、问答题
- 试题内容:
+ 单选题:每题包括问题描述、问题配图(可选,base64可能有多个、问题数据集较大的json字符串可能有多个数据集每题可能包含多个小题每小题有4个选项每个小题都应该有唯一正确的答案。
+ 填空题:每题包括问题描述、问题配图(可选,base64可能有多个、问题数据集较大的json字符串可能有多个数据集每题可能包含多个填空处包括数据集内的填空处和题目内的填空处每小题有一个填空框每个空都应该有正确答案。
+ 填表题:每题包括问题描述、待完成的数据表格,需要考生按照要求把表格中的空白格填入正确的数据。每个空格都有唯一的正确答案。
+ 问答题:每题包括问题描述、问题配图(可选,base64可能有多个、问题数据集较大的json字符串可能有多个数据集每题可能有多个问题每个问题都需要一个长文本答案每个问题都应该有一个参考答案问答题需要人工评分
- 考试方式:进入考试时程序从题库中随机抽取指定类型、指定数量的试题并按随机顺序排列,考试者需要在规定时间内完成所有试题的填写。
- 考试结束考试结束后程序将本次考试的试题、答题、正确答案参考答案、考试时间、考试人员等信息进行排版并保存为PDF文件文件名称为“姓名_考试时间.pdf”保存在电脑桌面上。
- 评分标准:
+ 单选题:与正确答案完全一致才算正确。
+ 多选题:与正确答案完全一致才算正确。
+ 填空题:与正确答案完全一致才算正确。
+ 问答题:人工评分。
#### 试题示例
```text
某企业2025年6月30日资产负债表数据如下
流动资产1200万元其中存货300万元
流动负债800万元
计算该企业的流动比率(保留两位小数)。
正确答案1.50
```
```text
某市政府2025年一般公共预算支出明细如下
基本支出(人员经费 + 日常公用经费180万元
项目支出发展类项目20万元
总支出200万元
计算基本支出占总支出的百分比(保留一位小数)。
正确答案90.0%
```
```text
某企业2024年财务报表及某市政府2025年公共预算支出明细如下表所示请根据表格数据完成计算。
表1企业A 2024年财务报表摘要单位万元
项目 金额
营业收入 5000
净利润 800
年初应收账款余额 400
年末应收账款余额 600
流动资产 1500
流动负债 1000
平均净资产 4000
表2某市政府2025年公共预算支出明细单位万元
支出类别 金额
基本支出(人员经费) 1200
基本支出(公用经费) 300
项目支出(发展类) 500
项目支出(民生类) 200
总预算支出 2200
问题与填空要求
企业财务指标计算:
计算企业A的 流动比率(保留两位小数):流动比率 = ______
计算企业A的 应收账款周转率(次数,保留一位小数):周转率 = ______次
计算企业A的 净资产收益率ROE百分比保留一位小数ROE = ______%
公共预算支出分析:
计算基本支出(人员经费 + 公用经费)占总预算支出的 百分比(保留一位小数):占比 = ______%
若上级要求基本支出占比不超过 60%判断该政府是否达标填写“是”或“否”______
正确答案:
1.50
10.0
20.0
68.2
```
### 页面规划
- 欢迎页提供2个入口”参加考试“和”管理系统“点击”参加考试“进入考试页面点击”管理系统“进入管理页面欢迎页要展示软件版本号和题库版本号。
+ 管理入口:提供用户管理、试题管理、考试管理等功能。
+ 输入管理密码页:输入正确的管理密码才可以进入管理系统。
+ 试题管理提供试题的添加、删除、修改、查询等功能。添加、修改中支持上传图片和导入数据集可能是excel文件。每次试题变化后都要为当前题库生成唯一的正序的题库版本号这是单机软件因为在实际上机考试时要确认各台机器的题库版本号是否一致
+ 人员管理提供考试人员的添加、删除、修改、查询等功能。支持批量导入人员信息excel文件。支持批量删除人员信息。
+ 考试管理:提供考试的添加、删除、修改、查询等功能,考试信息包括考试名称、时间、各类型试题题量(各类题型的小题数)、分值设置(各类试题的小题分值)。
+ 系统初始化
+ 参加考试:提供考试页面,包括试题展示、答题、提交等功能。
+ 输入考生信息页:输入正确的考生姓名、准考证号才可以进入考试系统,信息来源于人员管理中维护的数据。
+ 考试确认页:随机抽题组生成本次试卷,结合考生信息、考试时间等形成唯一考试数据,展示考试说明(固定内容),提供“开始考试”入口。
+ 考试页面展示试题支持单选题、多选题、填空题、问答题的答题功能。考试页面强制全屏具备防切出机制切出5次后在第6次切出时强制结束考试。考试页面具备倒计时功能
+ 单选题:支持单选项选择。
+ 填空题:支持填空内容输入。
+ 填表题:支持数据表格的补充填写。
+ 问答题:支持长文本输入。
+ 提交页提供提交按钮点击提交按钮将考试数据提交入库生成试卷PDF文件。
## 实现方案
### 技术选型
Electron + SQLite + bootstrap + fontawesome
### 管理员密码
t2t6a9
### 数据库设计
本应用是单机应用,没有网络环境,所以需要考虑到单机环境下的数据管理问题。
单机环境下我们使用SQLite数据库设计两个库system.db和user.db其中system.db为系统库系统管理员的操作可以更改system.db中的数据user.db为用户库考生的操作可以更改user.db中的数据。
本应用需要数据与应用分离,所以需要将数据库文件与应用程序文件分离,数据库文件的路径需要在应用程序中配置。
#### 系统库 system.db
说明system.db是系统数据库只有系统管理员的操作才会更改系统库。
##### 系统配置
###### config 系统配置表
| 字段名 | 字段类型 | 字段描述 |
| --- | --- | --- |
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
| key | text | 配置名称 |
| value | text | 配置值 |
> 已知的配置项:
+ 管理员密码admin_password
+ 题库版本号question_bank_version
+ 考试版本号exam_version
##### 数据字典
###### dict_types 字典类型表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|type_code|TEXT|字典类型编码(唯一标识,如 "question_category"、"user_role"|
|type_name|TEXT NOT NULL|字典类型名称(如 "题型分类"、"用户角色"|
|description|TEXT|字典类型的描述信息(可选)|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
###### dict_items 字典项表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|字典项唯一标识(自增主键)|
|type_code|TEXT NOT NULL|关联到 dict_types.type_code表示字典项所属类型如 "question_type"|
|item_code|TEXT NOT NULL|字典项编码(唯一标识,如 "choice"、"admin"|
|item_name|TEXT NOT NULL|字典项名称(如 "选择题"、"管理员"|
|item_value|TEXT|字典项的额外信息(如排序、扩展字段,可选)|
|parent_code|TEXT|父级字典项编码(用于构建树形结构,如 "objective" 是 "choice" 的父级)|
|is_active|BOOLEAN DEFAULT 1|是否启用1=启用0=禁用)|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
> 字典示例
-- 1. 定义字典类型
INSERT INTO dict_types (type_code, type_name, description) VALUES
('question_category', '题型分类', '用于区分客观题和主观题'),
('question_type', '题型', '存储所有题型(选择题、填空题等)');
-- 2. 插入题型分类(客观题、主观题)
INSERT INTO dict_items (type_code, item_code, item_name, item_value) VALUES
('question_category', 'objective', '客观题', '有固定答案,机器可自动评分'),
('question_category', 'subjective', '主观题', '需人工评分,答案不唯一');
-- 3. 插入题型(选择题、填空题等),并关联到分类
INSERT INTO dict_items (type_code, item_code, item_name, parent_code, item_value) VALUES
('question_type', 'choice', '选择题', 'objective', '包含单选和多选'),
('question_type', 'fill_blank', '填空题', 'objective', '填写空白处的答案'),
('question_type', 'fill_table', '填表题', 'objective', '填写表格内容'),
('question_type', 'true_false', '判断题', 'objective', '判断对错'),
('question_type', 'short_answer', '问答题', 'subjective', '简短回答问题'),
('question_type', 'analysis', '分析题', 'subjective', '需要分析问题'),
('question_type', 'essay', '论述题', 'subjective', '详细论述');
##### 试题
###### questions 题干表(问题主表)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|question_type|TEXT NOT NULL|问题类型(关联到 dict_types.type_code|
|question_name|TEXT NOT NULL DEFAULT ''|问题名称|
|question_description|TEXT NOT NULL DEFAULT ''|问题描述|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### question_datasets 题干的配套数据集表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|question_id|INTEGER NOT NULL|关联到 questions.id|
|dataset_name|TEXT NOT NULL DEFAULT ''|数据集名称|
|dataset_data|TEXT NOT NULL|数据集数据的json字符串|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### question_images 题干的配图表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|question_id|INTEGER NOT NULL|关联到 questions.id|
|image_name|TEXT NOT NULL DEFAULT ''|图片名称|
|image_base64|TEXT NOT NULL|图片文件的base64编码|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### question_fill_table 填表问题的表格表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|question_id|INTEGER NOT NULL|关联到 questions.id|
|table_name|TEXT NOT NULL DEFAULT ''|表格名称|
|table_data|TEXT NOT NULL|表格数据的json字符串|
|table_description|TEXT NOT NULL DEFAULT ''|表格描述|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### question_fill_table_blanks 填表问题的表格填空表(含答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|question_id|INTEGER NOT NULL|关联到 questions.id|
|dataset_id|INTEGER NOT NULL|关联到 question_dataset.id|
|cell_position|TEXT NOT NULL|填空单元格的位置(如 "A1"、"B2"|
|cell_type|TEXT NOT NULL DEFAULT 'number'|填空单元格的类型(如 "text"、"number"|
|correct_answer|TEXT NOT NULL DEFAULT ''|填空单元格的正确答案(如 "123"、"456"|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### question_choices 选择题问题表(含答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|question_id|INTEGER NOT NULL|关联到 questions.id|
|choice_description|TEXT NOT NULL DEFAULT ''|选择题的描述含占位符__blank__|
|choice_type|TEXT NOT NULL DEFAULT 'single'|选择题类型(如 "single"、"multiple"|
|choice_options|TEXT NOT NULL|选项的数组字符串|
|correct_answers|TEXT NOT NULL|正确答案的索引序列(如 "0,1"、"2,3,4"|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### question_fill_blanks 填空题问题表(含答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|question_id|INTEGER NOT NULL|关联到 questions.id|
|blank_description|TEXT NOT NULL DEFAULT ''|填空题的描述含填空占位符__blank__|
|blank_count|INTEGER NOT NULL DEFAULT 0|填空数量|
|correct_answers|TEXT NOT NULL DEFAULT ''|填空处的正确答案序列(如 "123,456"|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### question_short 简答题/论述题/分析题表(含参考答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|question_id|INTEGER NOT NULL|关联到 questions.id|
|short_ask|TEXT NOT NULL DEFAULT ''|简答题/论述题/分析题的问题|
|short_answer_ref|TEXT NOT NULL DEFAULT ''|简答题/论述题/分析题的参考答案|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### question_judge 判断题表(含答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|question_id|INTEGER NOT NULL|关联到 questions.id|
|judge_ask|TEXT NOT NULL DEFAULT ''|判断题的问题|
|judge_answer|INTEGER NOT NULL|判断题的答案0 或 1|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
##### 考试
###### exam 考试表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|exam_name|TEXT NOT NULL DEFAULT ''|考试名称|
|exam_description|TEXT NOT NULL DEFAULT ''|考试描述|
|exam_examinee_type|TEXT NOT NULL DEFAULT ''|考试考生类型(如 "student"、"teacher"|
|exam_notice|TEXT NOT NULL DEFAULT ''|考试须知长文本Markdown语法可展示在页面上|
|exam_minutes|INTEGER NOT NULL DEFAULT 0|考试时长(分钟)|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### exam_question_set 考试题型配置表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|exam_id|INTEGER NOT NULL|关联到 exam.id|
|question_type|TEXT NOT NULL|问题类型(关联到 dict_types.type_code|
|question_count|INTEGER NOT NULL DEFAULT 0|该类型问题的数量|
|question_score|INTEGER NOT NULL DEFAULT 0|该类型问题的总分数|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
##### 考生
###### examinee 考生表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|examinee_type|TEXT NOT NULL DEFAULT ''|考生类型(如 "student"、"teacher"|
|examinee_name|TEXT NOT NULL DEFAULT ''|考生姓名|
|examinee_account|TEXT NOT NULL DEFAULT ''|考生学号/工号|
|examinee_phone|TEXT NOT NULL DEFAULT ''|考生手机号|
|examinee_unit|TEXT NOT NULL DEFAULT ''|考生单位|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
#### 考生库 user.db
##### 操作日志
###### examinee_log 考生操作日志表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|examinee_id|INTEGER NOT NULL|关联到 exam_examinee.id|
|operation|TEXT NOT NULL DEFAULT ''|操作类型(如 "login"、"logout"|
|operation_time|TIMESTAMP NOT NULL|操作时间|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
##### 参加考试
###### examinee_exam 考生考试表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|examinee_id|INTEGER NOT NULL|关联到 exam_examinee.id|
|exam_name|TEXT NOT NULL DEFAULT ''|考试名称|
|exam_description|TEXT NOT NULL DEFAULT ''|考试描述|
|exam_examinee_type|TEXT NOT NULL DEFAULT ''|考试考生类型(如 "student"、"teacher"|
|exam_notice|TEXT NOT NULL DEFAULT ''|考试须知长文本Markdown语法可展示在页面上|
|exam_minutes|INTEGER NOT NULL DEFAULT 0|考试时长(分钟)|
|start_time|TEXT NOT NULL|考试开始时间|
|latest_end_time|TEXT NOT NULL|考试最晚结束时间|
|end_time|TEXT NOT NULL|考试结束时间|
|exam_duration|INTEGER NOT NULL DEFAULT 0|实际考试时长(分钟)|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP|
###### examinee_papers 考生试卷主表
这是在考生参加时从system库中的exam表中查询出的考试信息。
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|examinee_id|INTEGER NOT NULL|关联到 examinee.id|
|exam_id|INTEGER NOT NULL|关联到 exam.id|
|paper_minutes|INTEGER NOT NULL DEFAULT 0|试卷时长(分钟)|
|paper_total_score|INTEGER NOT NULL DEFAULT 0|试卷总分根据system库中的exam_question_set表计算得到|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
这种表应该是只读的表所以不需要updated_at字段。
###### paper_questions 考生试卷问题表
这是从system库中根据exam_question_set表中的配置从questions表中查询出的数据组成本次考试的试卷。
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|examinee_paper_id|INTEGER NOT NULL|关联到 examinee_papers.id|
|question_type|TEXT NOT NULL|问题类型(关联到 dict_types.type_code|
|question_name|TEXT NOT NULL DEFAULT ''|问题名称|
|question_description|TEXT NOT NULL DEFAULT ''|问题描述|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
这种表应该是只读的表所以不需要updated_at字段。
###### paper_question_datasets 题干的配套数据集表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|paper_question_id|INTEGER NOT NULL|关联到 paper_questions.id|
|dataset_name|TEXT NOT NULL DEFAULT ''|数据集名称|
|dataset_data|TEXT NOT NULL|数据集数据的json字符串|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
###### paper_question_images 题干的配图表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|paper_question_id|INTEGER NOT NULL|关联到 paper_questions.id|
|image_name|TEXT NOT NULL DEFAULT ''|图片名称|
|image_base64|TEXT NOT NULL|图片文件的base64编码|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
###### paper_question_fill_table 填表问题的表格表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|paper_question_id|INTEGER NOT NULL|关联到 paper_questions.id|
|table_name|TEXT NOT NULL DEFAULT ''|表格名称|
|table_data|TEXT NOT NULL|表格数据的json字符串|
|table_description|TEXT NOT NULL DEFAULT ''|表格描述|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
###### paper_question_fill_table_blanks 填表问题的表格填空表(含答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|paper_question_id|INTEGER NOT NULL|关联到 paper_questions.id|
|paper_dataset_id|INTEGER NOT NULL|关联到 paper_datasets.id|
|cell_position|TEXT NOT NULL|填空单元格的位置(如 "A1"、"B2"|
|cell_type|TEXT NOT NULL DEFAULT 'number'|填空单元格的类型(如 "text"、"number"|
|correct_answer|TEXT NOT NULL DEFAULT ''|填空单元格的正确答案(如 "123"、"456"|
|score|REAL NOT NULL DEFAULT 0|填空单元格的分值|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
###### paper_question_choices 选择题问题表(含答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|paper_question_id|INTEGER NOT NULL|关联到 paper_questions.id|
|choice_description|TEXT NOT NULL DEFAULT ''|选择题的描述含占位符__blank__|
|choice_type|TEXT NOT NULL DEFAULT 'single'|选择题类型(如 "single"、"multiple"|
|choice_options|TEXT NOT NULL|选项的数组字符串|
|correct_answers|TEXT NOT NULL|正确答案的索引序列(如 "0,1"、"2,3,4"|
|score|REAL NOT NULL DEFAULT 0|选择题的分值|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
###### paper_question_fill_blanks 填空题问题表(含答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|paper_question_id|INTEGER NOT NULL|关联到 paper_questions.id|
|blank_description|TEXT NOT NULL DEFAULT ''|填空题的描述含填空占位符__blank__|
|blank_count|INTEGER NOT NULL DEFAULT 0|填空数量|
|correct_answers|TEXT NOT NULL DEFAULT ''|填空处的正确答案序列(如 "123,456"|
|score|REAL NOT NULL DEFAULT 0|填空题的分值|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
###### paper_question_short 简答题/论述题/分析题表(含参考答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|paper_question_id|INTEGER NOT NULL|关联到 paper_questions.id|
|short_ask|TEXT NOT NULL DEFAULT ''|简答题/论述题/分析题的问题|
|short_answer_ref|TEXT NOT NULL DEFAULT ''|简答题/论述题/分析题的参考答案|
|score|REAL NOT NULL DEFAULT 0|简答题/论述题/分析题的分值|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
###### paper_question_judge 判断题表(含答案)
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|paper_question_id|INTEGER NOT NULL|关联到 paper_questions.id|
|judge_ask|TEXT NOT NULL DEFAULT ''|判断题的问题|
|judge_answer|INTEGER NOT NULL|判断题的答案0 或 1|
|score|REAL NOT NULL DEFAULT 0|判断题的分值|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|
###### paper_answers 考生试卷答案表
|字段名|字段类型|字段描述|
|---|---|---|
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|examinee_paper_id|INTEGER NOT NULL|关联到 examinee_papers.id|
|paper_question_id|INTEGER NOT NULL|关联到 paper_questions.id|
|paper_question_item_id|INTEGER NOT NULL|多态的方式关联到各个具体的小题表id|
|answer|TEXT NOT NULL DEFAULT ''|考生的答案|
|get_score|REAL NOT NULL DEFAULT 0|考生的得分|
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP|

2
data/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

73
electron/db/config.js Normal file
View File

@ -0,0 +1,73 @@
// 删除或注释掉原来的路径相关代码
// import path from 'path';
// import fs from 'fs';
// import { app } from 'electron';
// 导入统一的路径工具函数
import { getSystemDbPath } from './path.js';
import { openDatabase, executeWithRetry } from './utils.js';
/**
* 从config表获取配置项
* @param {string} key - 配置项键名
* @returns {Promise<{key: string, value: string} | null>} 配置项对象如果不存在返回null
*/
async function getConfig(key) {
try {
// 使用统一的数据库路径
const dbPath = getSystemDbPath();
const db = await openDatabase(dbPath);
const result = await executeWithRetry(db, async () => {
return await db.getAsync('SELECT * FROM config WHERE key = ?', [key]);
});
return result;
} catch (error) {
console.error(`获取配置项${key}失败:`, error);
throw error;
}
}
/**
* 更新或插入配置项
* @param {string} key - 配置项键名
* @param {string} value - 配置项值
* @returns {Promise<void>}
*/
async function setConfig(key, value) {
try {
// 使用统一的数据库路径
const dbPath = getSystemDbPath();
const db = await openDatabase(dbPath);
// 先检查是否存在
const existing = await executeWithRetry(db, async () => {
return await db.getAsync('SELECT * FROM config WHERE key = ?', [key]);
});
if (existing) {
// 更新
await executeWithRetry(db, async () => {
await db.runAsync('UPDATE config SET value = ? WHERE key = ?', [value, key]);
console.log(`成功更新配置项: ${key}`);
});
} else {
// 插入
await executeWithRetry(db, async () => {
await db.runAsync('INSERT INTO config (key, value) VALUES (?, ?)', [key, value]);
console.log(`成功插入配置项: ${key}`);
});
}
} catch (error) {
console.error(`设置配置项${key}失败:`, error);
throw error;
}
}
export {
getConfig,
setConfig
};

263
electron/db/index.js Normal file
View File

@ -0,0 +1,263 @@
// 将 CommonJS 导入改为 ES 模块导入(注意添加 .js 扩展名)
import { getSystemDbPath, getUserDbPath } from './path.js';
import { systemSchema, userSchema, defaultData } from './schema.js';
import { openDatabase, batchInsert } from './utils.js';
import * as argon2 from "argon2";
// 数据库连接池
const dbConnections = new Map();
// 获取数据库连接
async function getDbConnection(dbPath) {
if (dbConnections.has(dbPath)) {
console.log(`使用现有数据库连接: ${dbPath}`);
return dbConnections.get(dbPath);
}
const db = await openDatabase(dbPath);
dbConnections.set(dbPath, db);
return db;
}
// 关闭所有数据库连接
function closeAllConnections() {
dbConnections.forEach((db, path) => {
try {
db.close();
console.log(`关闭数据库连接: ${path}`);
} catch (error) {
console.error(`关闭数据库连接失败: ${path}`, error);
}
});
dbConnections.clear();
}
// 检查数据库是否已初始化
async function checkDatabaseInitialized() {
try {
console.log('开始检查数据库初始化状态...');
const systemDb = await getDbConnection(getSystemDbPath());
console.log('成功打开系统数据库');
const result = await systemDb.getAsync('SELECT value FROM config WHERE key = ?', ['initialized']);
console.log('查询初始化状态结果:', result);
const isInitialized = result && result.value === 'true';
console.log('数据库初始化状态:', isInitialized ? '已初始化' : '未初始化');
return isInitialized;
} catch (error) {
console.error('检查数据库初始化状态失败:', error);
return false;
}
}
// 初始化系统数据库
async function initializeSystemDatabase() {
console.log('开始初始化系统数据库...');
const systemDbPath = getSystemDbPath();
const systemDb = await getDbConnection(systemDbPath);
try {
// 开始事务
await systemDb.runAsync('BEGIN TRANSACTION');
console.log('开始事务');
// 创建表结构
console.log('开始创建系统数据库表结构...');
await systemDb.execAsync(systemSchema.config.trim());
console.log('创建 config 表成功');
await systemDb.execAsync(systemSchema.dictTypes.trim());
console.log('创建 dict_types 表成功');
await systemDb.execAsync(systemSchema.dictItems.trim());
console.log('创建 dict_items 表成功');
await systemDb.execAsync(systemSchema.questions.trim());
console.log('创建 questions 表成功');
await systemDb.execAsync(systemSchema.questionDatasets.trim());
console.log('创建 question_datasets 表成功');
await systemDb.execAsync(systemSchema.questionImages.trim());
console.log('创建 question_images 表成功');
await systemDb.execAsync(systemSchema.questionFillTable.trim());
console.log('创建 question_fill_table 表成功');
await systemDb.execAsync(systemSchema.questionFillTableBlanks.trim());
console.log('创建 question_fill_table_blanks 表成功');
await systemDb.execAsync(systemSchema.questionChoices.trim());
console.log('创建 question_choices 表成功');
await systemDb.execAsync(systemSchema.questionFillBlanks.trim());
console.log('创建 question_fill_blanks 表成功');
await systemDb.execAsync(systemSchema.questionJudge.trim());
console.log('创建 question_judge 表成功');
await systemDb.execAsync(systemSchema.questionShort.trim());
console.log('创建 question_short 表成功');
await systemDb.execAsync(systemSchema.exam.trim());
console.log('创建 exam 表成功');
await systemDb.execAsync(systemSchema.examQuestionSet.trim());
console.log('创建 exam_question_set 表成功');
await systemDb.execAsync(systemSchema.examinee.trim());
console.log('创建 examinee 表成功');
// 插入默认数据
console.log('开始插入默认数据...');
const plainPassword = defaultData.config.find(item => item.key === 'admin_password').value;
const hashedPassword = await argon2.hash(plainPassword);
// 更新密码为哈希值
const configData = defaultData.config.map(item => {
if (item.key === 'admin_password') {
return { ...item, value: hashedPassword };
}
return item;
});
await batchInsert(systemDb, 'config', configData);
console.log('插入 config 表数据成功');
await batchInsert(systemDb, 'dict_types', defaultData.dictTypes);
console.log('插入 dict_types 表数据成功');
await batchInsert(systemDb, 'dict_items', defaultData.dictItems);
console.log('插入 dict_items 表数据成功');
// 提交事务
await systemDb.runAsync('COMMIT');
console.log('提交事务成功');
return true;
} catch (error) {
// 处理错误
await systemDb.runAsync('ROLLBACK');
console.error('回滚事务:', error);
throw error;
}
}
// 初始化用户数据库
async function initializeUserDatabase() {
console.log('开始初始化用户数据库...');
const userDbPath = getUserDbPath();
const userDb = await getDbConnection(userDbPath);
try {
// 开始事务
await userDb.runAsync('BEGIN TRANSACTION');
console.log('开始事务');
// 创建表结构
console.log('开始创建用户数据库表结构...');
await userDb.execAsync(userSchema.examineeLog.trim());
console.log('创建 examinee_log 表成功');
await userDb.execAsync(userSchema.examineeExam.trim());
console.log('创建 examinee_exam 表成功');
await userDb.execAsync(userSchema.examineePapers.trim());
console.log('创建 examinee_papers 表成功');
await userDb.execAsync(userSchema.paperQuestions.trim());
console.log('创建 paper_questions 表成功');
await userDb.execAsync(userSchema.paperQuestionChoices.trim());
console.log('创建 paper_question_choices 表成功');
await userDb.execAsync(userSchema.paperQuestionBlanks.trim());
console.log('创建 paper_question_blanks 表成功');
await userDb.execAsync(userSchema.paperQuestionJudge.trim());
console.log('创建 paper_question_judge 表成功');
await userDb.execAsync(userSchema.paperQuestionFillTable.trim());
console.log('创建 paper_question_fill_table 表成功');
await userDb.execAsync(userSchema.paperQuestionFillTableBlanks.trim());
console.log('创建 paper_question_fill_table_blanks 表成功');
await userDb.execAsync(userSchema.paperQuestionSubjective.trim());
console.log('创建 paper_question_subjective 表成功');
// 提交事务
await userDb.runAsync('COMMIT');
console.log('提交事务成功');
return true;
} catch (error) {
// 处理错误
await userDb.runAsync('ROLLBACK');
console.error('回滚事务:', error);
throw error;
}
}
// 初始化数据库
async function initializeDatabase() {
try {
console.log('开始初始化数据库...');
// 确保只有一个初始化请求在执行
if (global.isInitializing) {
console.log('数据库初始化已在进行中,等待完成...');
while (global.isInitializing) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return global.initResult;
}
global.isInitializing = true;
global.initResult = false;
// 先初始化系统数据库
console.log('开始初始化系统数据库...');
const systemResult = await initializeSystemDatabase();
console.log('系统数据库初始化结果:', systemResult ? '成功' : '失败');
if (!systemResult) {
throw new Error('系统数据库初始化失败');
}
// 再初始化用户数据库
console.log('开始初始化用户数据库...');
const userResult = await initializeUserDatabase();
console.log('用户数据库初始化结果:', userResult ? '成功' : '失败');
if (!userResult) {
throw new Error('用户数据库初始化失败');
}
// 更新初始化状态
console.log('更新数据库初始化状态...');
const systemDb = await getDbConnection(getSystemDbPath());
await systemDb.runAsync('UPDATE config SET value = ? WHERE key = ?', ['true', 'initialized']);
console.log('数据库初始化状态更新成功');
console.log('数据库整体初始化成功');
global.initResult = true;
return true;
} catch (error) {
console.error('数据库初始化失败:', error);
global.initResult = false;
return false;
} finally {
global.isInitializing = false;
}
}
// 应用退出时关闭所有连接
process.on('exit', closeAllConnections);
// 替换module.exports为export
export {
initializeDatabase,
checkDatabaseInitialized,
};

42
electron/db/path.js Normal file
View File

@ -0,0 +1,42 @@
// 将 CommonJS 导入改为 ES 模块导入
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
// 获取当前文件所在目录
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 获取项目根目录
const getProjectRoot = () => {
// 从当前文件路径向上回溯找到vue目录
return path.resolve(__dirname, '../..');
};
// 确保数据目录存在
const ensureDataDirExists = () => {
const projectRoot = getProjectRoot();
const dataDir = path.join(projectRoot, 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
return dataDir;
};
// 获取系统数据库路径
const getSystemDbPath = () => {
const dataDir = ensureDataDirExists();
return path.join(dataDir, 'system.db');
};
// 获取用户数据库路径
const getUserDbPath = () => {
const dataDir = ensureDataDirExists();
return path.join(dataDir, 'user.db');
};
// 导出函数
export {
getSystemDbPath,
getUserDbPath
};

356
electron/db/schema.js Normal file
View File

@ -0,0 +1,356 @@
// 辅助函数将db.run包装为Promise
const runAsync = (db, sql, params = []) => {
return new Promise((resolve, reject) => {
db.run(sql, params, function(err) {
if (err) reject(err);
else resolve(this);
});
});
};
export { runAsync };
// 系统数据库表结构
const systemSchema = {
config: `
CREATE TABLE IF NOT EXISTS config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL,
value TEXT NOT NULL,
protected INTEGER DEFAULT 0
);
`,
dictTypes: `
CREATE TABLE IF NOT EXISTS dict_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type_code TEXT NOT NULL UNIQUE,
type_name TEXT NOT NULL,
description TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`,
dictItems: `
CREATE TABLE IF NOT EXISTS dict_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type_code TEXT NOT NULL,
item_code TEXT NOT NULL,
item_name TEXT NOT NULL,
item_value TEXT,
parent_code TEXT,
is_active BOOLEAN DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(type_code, item_code),
FOREIGN KEY (type_code) REFERENCES dict_types(type_code)
);
`,
questions: `
CREATE TABLE IF NOT EXISTS questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_type TEXT NOT NULL,
question_name TEXT NOT NULL DEFAULT '',
question_description TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (question_type) REFERENCES dict_types(type_code)
);
`,
questionDatasets: `
CREATE TABLE IF NOT EXISTS question_datasets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id INTEGER NOT NULL,
dataset_name TEXT NOT NULL DEFAULT '',
dataset_data TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (question_id) REFERENCES questions(id)
);
`,
questionImages: `
CREATE TABLE IF NOT EXISTS question_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id INTEGER NOT NULL,
image_name TEXT NOT NULL DEFAULT '',
image_base64 TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (question_id) REFERENCES questions(id)
);
`,
questionFillTable: `
CREATE TABLE IF NOT EXISTS question_fill_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id INTEGER NOT NULL,
table_name TEXT NOT NULL DEFAULT '',
table_data TEXT NOT NULL,
table_description TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (question_id) REFERENCES questions(id)
);
`,
questionFillTableBlanks: `
CREATE TABLE IF NOT EXISTS question_fill_table_blanks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id INTEGER NOT NULL,
dataset_id INTEGER NOT NULL,
cell_position TEXT NOT NULL,
cell_type TEXT NOT NULL DEFAULT 'number',
correct_answer TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (question_id) REFERENCES questions(id),
FOREIGN KEY (dataset_id) REFERENCES question_datasets(id)
);
`,
questionChoices: `
CREATE TABLE IF NOT EXISTS question_choices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id INTEGER NOT NULL,
choice_description TEXT NOT NULL DEFAULT '',
choice_type TEXT NOT NULL DEFAULT 'single',
choice_options TEXT NOT NULL,
correct_answers TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (question_id) REFERENCES questions(id)
);
`,
questionFillBlanks: `
CREATE TABLE IF NOT EXISTS question_fill_blanks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id INTEGER NOT NULL,
blank_description TEXT NOT NULL DEFAULT '',
blank_count INTEGER NOT NULL DEFAULT 0,
correct_answers TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (question_id) REFERENCES questions(id)
);
`,
questionJudge: `
CREATE TABLE IF NOT EXISTS question_judge (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id INTEGER NOT NULL,
judge_ask TEXT NOT NULL DEFAULT '',
judge_answer INTEGER NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (question_id) REFERENCES questions(id)
);
`,
questionShort: `
CREATE TABLE IF NOT EXISTS question_short (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id INTEGER NOT NULL,
short_ask TEXT NOT NULL DEFAULT '',
short_answer_ref TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (question_id) REFERENCES questions(id)
);
`,
exam: `
CREATE TABLE IF NOT EXISTS exam (
id INTEGER PRIMARY KEY AUTOINCREMENT,
exam_name TEXT NOT NULL DEFAULT '',
exam_description TEXT NOT NULL DEFAULT '',
exam_examinee_type TEXT NOT NULL DEFAULT '',
exam_notice TEXT NOT NULL DEFAULT '',
exam_minutes INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`,
examQuestionSet: `
CREATE TABLE IF NOT EXISTS exam_question_set (
id INTEGER PRIMARY KEY AUTOINCREMENT,
exam_id INTEGER NOT NULL,
question_type TEXT NOT NULL,
question_count INTEGER NOT NULL DEFAULT 0,
question_score INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (exam_id) REFERENCES exam(id),
FOREIGN KEY (question_type) REFERENCES dict_types(type_code)
);
`,
examinee: `
CREATE TABLE IF NOT EXISTS examinee (
id INTEGER PRIMARY KEY AUTOINCREMENT,
examinee_type TEXT NOT NULL DEFAULT '',
examinee_name TEXT NOT NULL DEFAULT '',
examinee_account TEXT NOT NULL DEFAULT '',
examinee_phone TEXT NOT NULL DEFAULT '',
examinee_unit TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`
};
// 用户数据库表结构
const userSchema = {
examineeLog: `
CREATE TABLE IF NOT EXISTS examinee_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
examinee_id INTEGER NOT NULL,
operation TEXT NOT NULL DEFAULT '',
operation_time TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`,
examineeExam: `
CREATE TABLE IF NOT EXISTS examinee_exam (
id INTEGER PRIMARY KEY AUTOINCREMENT,
examinee_id INTEGER NOT NULL,
exam_name TEXT NOT NULL DEFAULT '',
exam_description TEXT NOT NULL DEFAULT '',
exam_examinee_type TEXT NOT NULL DEFAULT '',
exam_notice TEXT NOT NULL DEFAULT '',
exam_minutes INTEGER NOT NULL DEFAULT 0,
start_time TEXT NOT NULL,
latest_end_time TEXT NOT NULL,
end_time TEXT NOT NULL,
exam_duration INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`,
examineePapers: `
CREATE TABLE IF NOT EXISTS examinee_papers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
examinee_id INTEGER NOT NULL,
exam_id INTEGER NOT NULL,
paper_minutes INTEGER NOT NULL DEFAULT 0,
paper_total_score INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`,
paperQuestions: `
CREATE TABLE IF NOT EXISTS paper_questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
examinee_paper_id INTEGER NOT NULL,
question_type TEXT NOT NULL,
question_name TEXT NOT NULL DEFAULT '',
question_description TEXT NOT NULL DEFAULT '',
question_score INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`,
paperQuestionChoices: `
CREATE TABLE IF NOT EXISTS paper_question_choices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
paper_question_id INTEGER NOT NULL,
choice_options TEXT NOT NULL,
user_answers TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
);
`,
paperQuestionBlanks: `
CREATE TABLE IF NOT EXISTS paper_question_blanks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
paper_question_id INTEGER NOT NULL,
blank_count INTEGER NOT NULL DEFAULT 0,
user_answers TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
);
`,
paperQuestionJudge: `
CREATE TABLE IF NOT EXISTS paper_question_judge (
id INTEGER PRIMARY KEY AUTOINCREMENT,
paper_question_id INTEGER NOT NULL,
judge_ask TEXT NOT NULL DEFAULT '',
user_answer INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
);
`,
paperQuestionFillTable: `
CREATE TABLE IF NOT EXISTS paper_question_fill_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
paper_question_id INTEGER NOT NULL,
table_name TEXT NOT NULL DEFAULT '',
table_data TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
);
`,
paperQuestionFillTableBlanks: `
CREATE TABLE IF NOT EXISTS paper_question_fill_table_blanks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
paper_question_fill_table_id INTEGER NOT NULL,
cell_position TEXT NOT NULL,
cell_type TEXT NOT NULL DEFAULT 'number',
user_answer TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (paper_question_fill_table_id) REFERENCES paper_question_fill_table(id)
);
`,
paperQuestionSubjective: `
CREATE TABLE IF NOT EXISTS paper_question_subjective (
id INTEGER PRIMARY KEY AUTOINCREMENT,
paper_question_id INTEGER NOT NULL,
subjective_type TEXT NOT NULL,
user_answer TEXT NOT NULL DEFAULT '',
score INTEGER NOT NULL DEFAULT -1,
scored_by TEXT NOT NULL DEFAULT '',
scored_at TEXT DEFAULT CURRENT_TIMESTAMP,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
);
`
};
// 初始化默认数据
// 系统配置默认数据
const plainPassword = 't2t6a9'; // 明文密码变量定义
// 注意在实际初始化数据库时需要使用argon2对plainPassword进行哈希
// 这里只定义默认数据结构哈希操作应在index.js中的初始化函数中完成
const defaultData = {
config: [
{ key: 'admin_password', value: plainPassword, protected: 1 },
{ key: 'question_bank_version', value: '1', protected: 1 },
{ key: 'exam_version', value: '1', protected: 1 },
{ key: 'initialized', value: '1', protected: 1 }
],
// 字典类型默认数据
dictTypes: [
{ type_code: 'question_category', type_name: '题型分类', description: '用于区分客观题和主观题' },
{ type_code: 'question_type', type_name: '题型', description: '存储所有题型(选择题、填空题等)' },
{ type_code: 'user_role', type_name: '用户角色', description: '区分不同用户类型' }
],
// 字典项默认数据
dictItems: [
// 题型分类
{ type_code: 'question_category', item_code: 'objective', item_name: '客观题', item_value: '有固定答案,机器可自动评分', parent_code: null },
{ type_code: 'question_category', item_code: 'subjective', item_name: '主观题', item_value: '需人工评分,答案不唯一', parent_code: null },
// 题型
{ type_code: 'question_type', item_code: 'choice', item_name: '选择题', item_value: '包含单选和多选', parent_code: 'objective' },
{ type_code: 'question_type', item_code: 'fill_blank', item_name: '填空题', item_value: '填写空白处的答案', parent_code: 'objective' },
{ type_code: 'question_type', item_code: 'fill_table', item_name: '填表题', item_value: '填写表格内容', parent_code: 'objective' },
{ type_code: 'question_type', item_code: 'true_false', item_name: '判断题', item_value: '判断对错', parent_code: 'objective' },
{ type_code: 'question_type', item_code: 'short_answer', item_name: '问答题', item_value: '简短回答问题', parent_code: 'subjective' },
{ type_code: 'question_type', item_code: 'analysis', item_name: '分析题', item_value: '需要分析问题', parent_code: 'subjective' },
{ type_code: 'question_type', item_code: 'essay', item_name: '论述题', item_value: '详细论述', parent_code: 'subjective' },
// 用户角色
{ type_code: 'user_role', item_code: 'admin', item_name: '管理员', item_value: '系统管理员', parent_code: null },
{ type_code: 'user_role', item_code: 'student', item_name: '考生', item_value: '参加考试的用户', parent_code: null }
]
};
// 导出对象
export {
systemSchema,
userSchema,
defaultData
};

93
electron/db/utils.js Normal file
View File

@ -0,0 +1,93 @@
// 添加到文件开头
import sqlite3 from 'sqlite3';
import { promisify } from 'util';
sqlite3.verbose();
// 数据库连接池简单实现
const dbConnections = new Map();
// 打开数据库并添加到连接池
async function openDatabase(dbPath) {
console.log(`打开数据库连接: ${dbPath}`);
// 检查是否已有连接
if (dbConnections.has(dbPath)) {
console.log(`使用现有数据库连接: ${dbPath}`);
return dbConnections.get(dbPath);
}
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error(`打开数据库失败: ${err.message}`);
reject(err);
return;
}
console.log(`成功打开数据库: ${dbPath}`);
// promisify数据库方法
db.getAsync = promisify(db.get).bind(db);
db.allAsync = promisify(db.all).bind(db);
db.runAsync = promisify(db.run).bind(db);
db.execAsync = promisify(db.exec).bind(db);
// 添加到连接池
dbConnections.set(dbPath, db);
resolve(db);
});
});
}
// 添加重试机制的执行函数
async function executeWithRetry(db, operation, maxRetries = 3) {
let retries = 0;
while (retries < maxRetries) {
try {
return await operation();
} catch (error) {
if (error.code === 'SQLITE_BUSY' && retries < maxRetries - 1) {
retries++;
console.log(`数据库忙,正在重试 (${retries}/${maxRetries})...`);
await new Promise(resolve => setTimeout(resolve, 500 * retries)); // 指数退避
} else {
throw error;
}
}
}
}
// 修改batchInsert函数使用重试机制
async function batchInsert(db, table, data) {
if (!data || data.length === 0) {
return;
}
const columns = Object.keys(data[0]).join(', ');
const placeholders = data.map(() => {
return '(' + Object.keys(data[0]).map(() => '?').join(', ') + ')';
}).join(', ');
const values = [];
data.forEach(item => {
Object.values(item).forEach(value => {
values.push(value);
});
});
const sql = `INSERT INTO ${table} (${columns}) VALUES ${placeholders}`;
return executeWithRetry(db, async () => {
await db.runAsync(sql, values);
console.log(`成功插入 ${data.length} 条记录到 ${table}`);
});
}
// 导出修改后的函数
export {
openDatabase,
batchInsert,
executeWithRetry
};

163
electron/main.js Normal file
View File

@ -0,0 +1,163 @@
// 确保所有导入都使用 ES 模块语法
import { app, BrowserWindow, ipcMain } from 'electron';
import path from 'path';
import { fileURLToPath } from 'url';
import { checkDatabaseInitialized, initializeDatabase } from './db/index.js';
import { getSystemConfig } from './service/system.js';
// 定义 __dirname 和 __filename
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 引入认证服务
import { initAuthIpc } from './service/auth.service.js';
// 确保在应用最开始处添加单实例锁检查
// 尝试获取单实例锁
const gotTheLock = app.requestSingleInstanceLock();
// 如果获取锁失败,说明已有实例在运行,直接退出
if (!gotTheLock) {
app.quit();
} else {
// 设置第二个实例启动时的处理
app.on('second-instance', (event, commandLine, workingDirectory) => {
// 当用户尝试启动第二个实例时,聚焦到已有的主窗口
const mainWindow = BrowserWindow.getAllWindows()[0];
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
function createWindow() {
const mainWindow = new BrowserWindow({
fullscreen: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
})
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:5173/')
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
}
}
// Initalize app
app.whenReady().then(() => {
setupApp()
createWindow()
setupIpcMain()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
// Check database initialization status
async function setupApp() {
try {
console.log('应用启动 - 检查数据库初始化状态...');
// 使用全局变量防止重复初始化检查
if (global.dbInitCheck) {
console.log('数据库初始化检查已完成');
return;
}
global.dbInitCheck = true;
const isInitialized = await checkDatabaseInitialized();
console.log('数据库初始化状态:', isInitialized);
// 只检查状态,不自动初始化
// if (!isInitialized) {
// console.log('数据库未初始化,开始初始化...');
// const initResult = await initializeDatabase();
// console.log('数据库初始化结果:', initResult ? '成功' : '失败');
// }
} catch (error) {
console.error('数据库检查过程中出错:', error);
}
}
// Setup IPC communication
// 在文件适当位置添加以下代码
function setupIpcMain() {
// 数据库相关
ipcMain.handle('check-database-initialized', async () => {
try {
return await checkDatabaseInitialized();
} catch (error) {
console.error('Failed to check database initialization:', error);
return false;
}
});
ipcMain.handle('initialize-database', async () => {
try {
return await initializeDatabase();
} catch (error) {
console.error('Failed to initialize database:', error);
return false;
}
});
// 认证相关
ipcMain.handle('auth-admin-login', async (event, credentials) => {
try {
return await adminLogin(credentials);
} catch (error) {
console.error('Login failed:', error);
return null;
}
});
// 系统相关
ipcMain.handle('system-get-config', async () => {
try {
return await getSystemConfig();
} catch (error) {
console.error('Failed to get system config:', error);
return null;
}
});
// 初始化认证相关IPC
initAuthIpc();
}
// 确保在 app.whenReady() 中调用 setupIpcMain()
// 删除重复的app.whenReady()调用,只保留一个
// 保留一个正确的初始化代码块
// 保留一个app.whenReady()调用
app.whenReady().then(() => {
setupApp()
createWindow()
setupIpcMain()
});
// 删除下面这段重复的代码
// app.whenReady().then(() => {
// setupApp()
// createWindow()
// setupIpcMain()
// });
// 在应用退出前关闭所有数据库连接
app.on('will-quit', () => {
console.log('应用即将退出...');
// closeAllConnections();
});

26
electron/preload.js Normal file
View File

@ -0,0 +1,26 @@
const { contextBridge, ipcRenderer } = require('electron');
// 暴露API给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 数据库相关
checkDatabaseInitialized: () => ipcRenderer.invoke('check-database-initialized'),
initializeDatabase: () => ipcRenderer.invoke('initialize-database'),
// 认证相关
adminLogin: (credentials) => ipcRenderer.invoke('admin-login', credentials),
// 系统相关
getSystemConfig: () => ipcRenderer.invoke('system-get-config')
});
// 这里可以添加预加载脚本
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})

View File

@ -0,0 +1,43 @@
import argon2 from 'argon2';
import { getConfig } from '../db/config.js';
import { ipcMain } from 'electron';
/**
* 管理员登录验证
* @param {string} password - 用户输入的密码
* @returns {Promise<{success: boolean, message: string}>}
*/
async function verifyAdminPassword(password) {
try {
const config = await getConfig('admin_password');
if (!config || !config.value) {
return { success: false, message: '管理员密码未设置' };
}
const isMatch = await argon2.verify(config.value, password);
if (isMatch) {
return { success: true, message: '登录成功' };
} else {
return { success: false, message: '密码错误' };
}
} catch (error) {
console.error('验证管理员密码失败:', error);
return { success: false, message: '验证过程发生错误' };
}
}
/**
* 初始化认证相关的IPC处理程序
*/
function initAuthIpc() {
// 管理员登录验证 - 使用正确的通道名称
ipcMain.handle('admin-login', async (event, credentials) => {
// 从credentials对象中获取密码
return await verifyAdminPassword(credentials.password);
});
}
export {
verifyAdminPassword,
initAuthIpc
};

View File

@ -0,0 +1,61 @@
// 将 CommonJS 导入改为 ES 模块导入
import { getSystemDbPath } from '../db/path.js';
import { openDatabase } from '../db/utils.js';
// 获取系统配置
async function getSystemConfig() {
try {
const systemDb = await openDatabase(getSystemDbPath());
const configs = await systemDb.allAsync('SELECT key, value FROM config');
// 不要关闭连接,由连接池管理
const configMap = {};
configs.forEach(config => {
configMap[config.key] = config.value;
});
return configMap;
} catch (error) {
console.error('Get system config failed:', error);
throw error;
}
}
// 更新系统配置
async function updateSystemConfig(config) {
try {
const systemDb = await openDatabase(getSystemDbPath());
await systemDb.runAsync('BEGIN TRANSACTION');
for (const [key, value] of Object.entries(config)) {
await systemDb.runAsync('UPDATE config SET value = ? WHERE key = ?', [value, key]);
}
await systemDb.runAsync('COMMIT');
// 不要关闭连接,由连接池管理
return true;
} catch (error) {
await systemDb.runAsync('ROLLBACK');
// 不要关闭连接,由连接池管理
console.error('Update system config failed:', error);
throw error;
}
}
// 增加题库版本号
async function increaseQuestionBandVersion() {
try {
const systemDb = await openDatabase(getSystemDbPath());
await systemDb.runAsync('UPDATE config SET value = value + 1 WHERE key = ?', ['question_bank_version']);
// 不要关闭连接,由连接池管理
return true;
} catch (error) {
console.error('Increase question bank version failed:', error);
throw error;
}
}
export {
getSystemConfig,
updateSystemConfig,
increaseQuestionBandVersion
};

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>统计技能考试系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

8646
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "electron-exam",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "electron/main.js",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"start": "vite --mode electron",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"electron:dev": "vite --mode electron",
"electron:build": "vite build && electron-builder"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@fortawesome/fontawesome-svg-core": "^7.0.0",
"@fortawesome/free-brands-svg-icons": "^7.0.0",
"@fortawesome/free-regular-svg-icons": "^7.0.0",
"@fortawesome/free-solid-svg-icons": "^7.0.0",
"@fortawesome/vue-fontawesome": "^3.1.1",
"@popperjs/core": "^2.11.8",
"argon2": "^0.43.1",
"bootstrap": "^5.3.7",
"element-plus": "^2.10.5",
"fs": "^0.0.1-security",
"popper.js": "^1.16.1",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"vue": "^3.5.18",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"electron": "^37.2.5",
"electron-builder": "^26.0.12",
"vite": "^7.0.6",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-vue-devtools": "^8.0.0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

14
src/App.vue Normal file
View File

@ -0,0 +1,14 @@
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<div class="app-container">
<RouterView />
</div>
</template>
<style scoped>
/* 可以在这里添加全局样式 */
</style>

31
src/assets/base.css Normal file
View File

@ -0,0 +1,31 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--primary-color: #4096ff; /* 定义主色调与Element Plus保持一致 */
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

1
src/assets/main.css Normal file
View File

@ -0,0 +1 @@
@import './base.css';

View File

@ -0,0 +1,57 @@
<template>
<el-footer class="footer-container">
<el-row type="flex" justify="space-between" align="middle" height="100%">
<el-col :span="12">
<p>© {{ thisYear }} 统计技能考试系统 - 版权所有</p>
</el-col>
<el-col :span="12">
<p style="text-align: right !important;">题库版本{{ questionBankVersion || '未知' }}</p>
</el-col>
</el-row>
</el-footer>
</template>
<style scoped>
.footer-container {
width: 100%;
background-color: #f8f9fa;
color: #6c757d;
padding: 1rem;
border-top: 1px solid #e9ecef;
height: 60px; /* 固定高度 */
box-sizing: border-box;
}
.text-left, .text-right {
margin: 0;
line-height: 1.5;
}
/* 增强text-right类的优先级 */
.text-right {
text-align: right !important;
}
</style>
<script setup>
import { ElFooter, ElRow, ElCol } from 'element-plus'
import { ref, onMounted } from 'vue'
// 使ref
const thisYear = ref(new Date().getFullYear())
const questionBankVersion = ref('')
//
onMounted(async () => {
try {
// electronAPI
const config = await window.electronAPI.getSystemConfig()
console.log(config)
//
questionBankVersion.value = config.question_bank_version || '未知'
} catch (error) {
console.error('获取题库版本失败:', error)
questionBankVersion.value = '获取失败'
}
})
</script>

View File

@ -0,0 +1,41 @@
<template>
<el-header class="header-container">
<el-row type="flex" justify="center" height="100%">
<el-col>
<h1 class="display-5">
<font-awesome-icon icon="graduation-cap" class="me-3" />
统计技能考试系统
</h1>
</el-col>
</el-row>
</el-header>
</template>
<style scoped>
.header-container {
width: 100%;
background-color: #1e40af; /* 深蓝色背景 */
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
height: 80px; /* 设置固定高度 */
padding: 1.5rem 0;
}
h1.display-5 {
margin: 0;
font-size: 2rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.me-3 {
margin-right: 0.75rem;
}
</style>
<script setup>
import { ElHeader, ElRow, ElCol } from 'element-plus'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
</script>

View File

@ -0,0 +1,9 @@
<template>
</template>
<script setup>
</script>
<style scoped>
</style>

33
src/main.js Normal file
View File

@ -0,0 +1,33 @@
import './assets/main.css'
// 引入Element Plus CSS
import 'element-plus/dist/index.css'
// 引入Bootstrap CSS
import 'bootstrap/dist/css/bootstrap.min.css'
// 引入Popper.js
import 'popper.js/dist/umd/popper.min.js'
// 引入bootstrap JS
import 'bootstrap/dist/js/bootstrap.min.js'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 引入Element Plus
import ElementPlus from 'element-plus'
// 引入FontAwesome核心库和组件
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
// 导入使用的FontAwesome图标
import { fas } from '@fortawesome/free-solid-svg-icons'
// 添加图标到库中
library.add(fas)
const app = createApp(App)
// 使用Element Plus
app.use(ElementPlus)
// 全局注册FontAwesomeIcon组件
app.component('FontAwesomeIcon', FontAwesomeIcon)
app.use(router)
app.mount('#app')

26
src/router/index.js Normal file
View File

@ -0,0 +1,26 @@
import { createRouter, createWebHistory } from 'vue-router'
import WelcomeView from '../views/WelcomeView.vue'
import StudentHomeView from '../views/StudentHomeView.vue'
import AdminHomeView from '../views/AdminHomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'welcome',
component: WelcomeView,
},
{
path: '/student-home',
name: 'student-home',
component: StudentHomeView,
},
{
path: '/admin-home',
name: 'admin-home',
component: AdminHomeView,
},
],
})
export default router

0
src/utils/validate.js Normal file
View File

187
src/views/AdminHomeView.vue Normal file
View File

@ -0,0 +1,187 @@
<template>
<div class="min-h-screen flex flex-col bg-gray-50">
<!-- 顶部标题栏 -->
<header class="bg-primary text-white py-4 px-6 shadow-md flex justify-between items-center">
<div class="container mx-auto flex justify-between items-center">
<h1 class="text-2xl font-bold flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-cog" /> 考试系统 - 管理中心
</h1>
<el-button type="danger" size="small" @click="logout" class="flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-sign-out-alt" /> 退出
</el-button>
</div>
</header>
<!-- 中间内容区 -->
<main class="flex-grow flex h-[calc(100vh-120px)]">
<!-- 左侧导航菜单 -->
<aside class="w-64 bg-gray-800 text-white p-4 overflow-y-auto flex-shrink-0">
<nav class="space-y-2">
<el-menu :default-openeds="['1']" class="bg-transparent border-0 text-white" active-text-color="#4096ff">
<el-menu-item index="1-1" class="text-white">
<template #title>
<span class="flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-home" /> 管理员首页
</span>
</template>
</el-menu-item>
<el-sub-menu index="2">
<template #title>
<span class="flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-cog" /> 系统管理
</span>
</template>
<el-menu-item index="2-1" class="flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-sliders" /> 配置项管理
</el-menu-item>
<el-menu-item index="2-2" class="flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-book" /> 字典管理
</el-menu-item>
</el-sub-menu>
<el-menu-item index="3" class="text-white">
<template #title>
<span class="flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-file-question" /> 试题管理
</span>
</template>
</el-menu-item>
<el-menu-item index="4" class="text-white">
<template #title>
<span class="flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-calendar-check" /> 考试管理
</span>
</template>
</el-menu-item>
<el-menu-item index="5" class="text-white">
<template #title>
<span class="flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-users" /> 考生管理
</span>
</template>
</el-menu-item>
</el-menu>
</nav>
</aside>
<!-- 右侧内容区域 -->
<div class="flex-grow p-6 overflow-auto bg-gray-50">
<div class="bg-white rounded-lg shadow-md p-6 min-h-full">
<h2 class="text-xl font-semibold text-gray-800 mb-6 flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-home" /> 管理员首页
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div class="bg-blue-50 p-4 rounded-lg border border-blue-100 hover:shadow-md transition-shadow duration-300">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-medium text-blue-700">总试题数</h3>
<FontAwesomeIcon icon="fa-solid fa-file-question" class="text-blue-500" />
</div>
<p class="text-3xl font-bold text-blue-900 mt-2">120</p>
</div>
<div class="bg-green-50 p-4 rounded-lg border border-green-100 hover:shadow-md transition-shadow duration-300">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-medium text-green-700">总考试数</h3>
<FontAwesomeIcon icon="fa-solid fa-calendar-check" class="text-green-500" />
</div>
<p class="text-3xl font-bold text-green-900 mt-2">8</p>
</div>
<div class="bg-purple-50 p-4 rounded-lg border border-purple-100 hover:shadow-md transition-shadow duration-300">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-medium text-purple-700">总考生数</h3>
<FontAwesomeIcon icon="fa-solid fa-users" class="text-purple-500" />
</div>
<p class="text-3xl font-bold text-purple-900 mt-2">156</p>
</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg border border-gray-100">
<h3 class="text-lg font-medium text-gray-700 mb-4 flex items-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-history" /> 最近考试
</h3>
<div class="overflow-x-auto">
<el-table :data="recentExams" style="width: 100%">
<el-table-column prop="name" label="考试名称" width="200" />
<el-table-column prop="startTime" label="开始时间" width="180" />
<el-table-column prop="endTime" label="结束时间" width="180" />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<span :class="{
'text-green-600': scope.row.status === '已完成',
'text-blue-600': scope.row.status === '进行中',
'text-gray-600': scope.row.status === '未开始'
}">
{{ scope.row.status }}
</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
</main>
<!-- 底部页脚 -->
<footer class="bg-gray-800 text-white py-4 px-6 text-center text-sm">
<div class="container mx-auto">
<p>© 2023 考试系统. 版权所有. 版本 v1.0.0</p>
</div>
</footer>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { ElButton, ElMenu, ElMenuItem, ElSubMenu, ElTable, ElTableColumn } from 'element-plus'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
const router = useRouter()
const logout = () => {
router.push('/')
}
//
const recentExams = [
{
name: '期末考试',
startTime: '2023-06-20 09:00',
endTime: '2023-06-20 11:00',
status: '已完成'
},
{
name: '期中考试',
startTime: '2023-04-15 14:00',
endTime: '2023-04-15 16:00',
status: '已完成'
},
{
name: '月考',
startTime: '2023-05-10 10:00',
endTime: '2023-05-10 11:30',
status: '已完成'
}
]
</script>
<style scoped>
/* 使用Bootstrap样式并自定义 */
@import 'bootstrap/dist/css/bootstrap.min.css';
/* 自定义样式 */
.bg-primary {
background-color: var(--primary-color) !important;
}
.el-menu {
--el-menu-bg-color: transparent !important;
--el-menu-text-color: #ffffff !important;
--el-menu-hover-bg-color: rgba(255, 255, 255, 0.1) !important;
}
.el-menu-item {
--el-menu-item-height: 40px !important;
}
.el-sub-menu__title {
--el-submenu-title-height: 40px !important;
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<div class="min-h-screen flex flex-col bg-gray-50">
<!-- 顶部标题栏 -->
<header class="bg-primary text-white py-4 px-6 shadow-md">
<div class="container mx-auto">
<h1 class="text-2xl font-bold text-center flex items-center justify-center gap-2">
<FontAwesomeIcon icon="fa-solid fa-user-graduate" /> 考试系统 - 考生首页
</h1>
</div>
</header>
<!-- 中间内容区 -->
<main class="flex-grow flex items-center justify-center p-6">
<div class="container mx-auto">
<div class="max-w-2xl mx-auto bg-white rounded-xl shadow-lg overflow-hidden border-2 border-primary/20">
<div class="p-8">
<div class="flex items-center justify-center mb-6">
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mr-3">
<FontAwesomeIcon icon="fa-solid fa-file-question" class="text-primary" />
</div>
<h2 class="text-xl font-semibold text-gray-800">考试须知</h2>
</div>
<div class="prose max-w-none mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
<ol class="list-decimal pl-5 space-y-3 text-gray-600">
<li class="flex items-start gap-2">
<span class="text-primary font-medium"></span>
<span>请确保网络连接稳定避免考试过程中断网</span>
</li>
<li class="flex items-start gap-2">
<span class="text-primary font-medium"></span>
<span>考试时间为60分钟超时系统将自动提交</span>
</li>
<li class="flex items-start gap-2">
<span class="text-primary font-medium"></span>
<span>请独立完成考试严禁作弊行为</span>
</li>
<li class="flex items-start gap-2">
<span class="text-primary font-medium"></span>
<span>考试过程中请勿刷新页面否则可能导致答案丢失</span>
</li>
<li class="flex items-start gap-2">
<span class="text-primary font-medium"></span>
<span>遇到技术问题请及时联系监考老师</span>
</li>
</ol>
</div>
<div class="flex justify-center mt-6">
<el-button type="primary" size="large" @click="startExam" class="flex items-center justify-center gap-2 w-full md:w-auto">
<FontAwesomeIcon icon="fa-solid fa-calendar-check" /> 开始考试
</el-button>
</div>
</div>
</div>
</div>
</main>
<!-- 底部页脚 -->
<footer class="bg-gray-800 text-white py-4 px-6 text-center text-sm">
<div class="container mx-auto">
<p>© 2023 考试系统. 版权所有. 版本 v1.0.0</p>
</div>
</footer>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { ElButton } from 'element-plus'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
const router = useRouter()
const startExam = () => {
//
alert('即将开始考试!')
}
</script>
<style scoped>
/* 使用Bootstrap样式并自定义 */
@import 'bootstrap/dist/css/bootstrap.min.css';
/* 自定义样式 */
.bg-primary {
background-color: var(--primary-color) !important;
}
.text-primary {
color: var(--primary-color) !important;
}
</style>

276
src/views/WelcomeView.vue Normal file
View File

@ -0,0 +1,276 @@
<template>
<div class="welcome-container">
<el-container>
<Header />
<!-- 主要内容区域 -->
<el-main>
<div class="d-flex align-items-center justify-content-center p-4" style="padding: 0; width: 600px;">
<!-- 数据库初始化提示卡片 -->
<div class="login-card bg-white rounded-4 shadow-lg p-5 w-100 max-w-md" id="init-section" v-show="!isDatabaseInitialized">
<div class="text-center">
<div class="mb-6">
<FontAwesomeIcon icon="fa-solid fa-database" class="text-primary" style="font-size: 64px;" />
</div>
<h2 class="display-6 mb-4">系统未初始化</h2>
<p class="fs-5 mb-6 text-muted">请点击下方按钮进行系统初始化初始化完成后将自动显示登录界面</p>
<button id="initialize-db" @click="initializeDatabase" class="btn btn-primary px-8 py-3 fs-5" :disabled="isInitializing">
<FontAwesomeIcon v-if="isInitializing" icon="fa-solid fa-spinner fa-spin" class="me-2" />
<FontAwesomeIcon v-else icon="fa-solid fa-sync-alt" class="me-2" />
{{ isInitializing ? '初始化中...' : '数据初始化' }}
</button>
</div>
</div>
<!-- Bootstrap登录卡片 -->
<div class="login-card bg-white rounded-4 shadow-lg p-5 w-100 max-w-md" id="login-section" style="height: 480px;" v-show="isDatabaseInitialized">
<!-- 原有登录卡片内容保持不变 -->
<!-- 登录类型切换标签页 -->
<ul class="nav nav-tabs fs-4 nav-justified" id="loginTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link rounded-0 active" id="exam-tab" data-bs-toggle="tab" data-bs-target="#exam-login" type="button" role="tab" aria-controls="exam-login" aria-selected="true">
<FontAwesomeIcon icon="fa-solid fa-user-graduate" class="me-2" />
考生登录
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link rounded-0" id="admin-tab" data-bs-toggle="tab" data-bs-target="#admin-login" type="button" role="tab" aria-controls="admin-login" aria-selected="false">
<FontAwesomeIcon icon="fa-solid fa-user-cog" class="me-2" />
系统管理
</button>
</li>
</ul>
<!-- 登录表单内容 -->
<div class="tab-content fs-5 p-4 border-1 border-start border-end border-bottom" id="loginTabContent" style="height: calc(100% - 60px);">
<!-- 原有表单内容保持不变 -->
<!-- 考生登录表单 -->
<div class="h-100 tab-pane fade show active" id="exam-login" role="tabpanel" aria-labelledby="exam-tab">
<form @submit.prevent="handleStudentLogin" class="d-flex flex-column h-100">
<div class="mb-3 flex-grow-1 d-flex flex-column justify-content-center">
<label for="examId" class="form-label">准考证号</label>
<div class="input-group input-group-lg">
<span class="input-group-text">
<FontAwesomeIcon icon="fa-solid fa-id-card" />
</span>
<input type="text" class="form-control" id="examId" v-model="studentId" required>
</div>
</div><!-- 结束.mb-3.flex-grow-1.d-flex.flex-column.justify-content-center -->
<div class="mb-3 flex-grow-1 d-flex flex-column justify-content-center">
<label for="idCard" class="form-label">身份证号</label>
<div class="input-group input-group-lg">
<span class="input-group-text">
<FontAwesomeIcon icon="fa-solid fa-id-card-alt" />
</span>
<input type="text" class="form-control" id="idCard" v-model="studentName" required>
</div>
</div><!-- 结束.mb-3.flex-grow-1.d-flex.flex-column.justify-content-center -->
<div class="mt-4 flex-grow-1 d-flex flex-column justify-content-end">
<button type="submit" class="btn btn-primary w-100 py-2 fs-5">
<FontAwesomeIcon icon="fa-solid fa-sign-in-alt" class="me-2" />
登录
</button>
</div><!-- 结束.mt-4.flex-grow-1.d-flex.flex-column.justify-content-end -->
</form>
</div><!-- 结束#exam-login -->
<!-- 管理员登录表单 -->
<div class="h-100 tab-pane fade" id="admin-login" role="tabpanel" aria-labelledby="admin-tab">
<form @submit.prevent="handleAdminLogin" class="d-flex flex-column h-100">
<div class="mb-3 flex-grow-1 d-flex flex-column justify-content-center">
<label for="password" class="form-label">管理员密码</label>
<div class="input-group input-group-lg">
<span class="input-group-text">
<FontAwesomeIcon icon="fa-solid fa-lock" />
</span>
<input type="password" class="form-control" id="password" v-model="adminPassword" required>
</div>
<div id="admin-error-message" class="text-danger mt-2" style="display: none;"></div>
</div><!-- 结束.mb-3.flex-grow-1.d-flex.flex-column.justify-content-center -->
<div class="mt-4 flex-grow-1 d-flex flex-column justify-content-end">
<button type="submit" class="btn btn-primary w-100 py-2 fs-5">
<FontAwesomeIcon icon="fa-solid fa-sign-in-alt" class="me-2" />
登录
</button>
</div><!-- 结束.mt-4.flex-grow-1.d-flex.flex-column.justify-content-end -->
</form>
</div><!-- 结束#admin-login -->
</div><!-- 结束#loginTabContent -->
</div><!-- 结束.login-card -->
</div><!-- 结束.d-flex.align-items-center.justify-content-center -->
</el-main>
<Footer />
</el-container>
</div><!-- 结束.welcome-container -->
</template>
<script setup>
//
import Header from '@/components/common/Header.vue'
import Footer from '@/components/common/Footer.vue'
import { useRouter } from 'vue-router'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
//
const router = useRouter()
const studentName = ref('') //
const studentId = ref('') //
const adminPassword = ref('')
const isDatabaseInitialized = ref(false)
const isInitializing = ref(false)
//
onMounted(async () => {
try {
console.log('组件挂载 - 开始检查数据库初始化状态');
const initialized = await window.electronAPI.checkDatabaseInitialized();
console.log('组件挂载 - 数据库初始化状态检查完成:', initialized);
isDatabaseInitialized.value = initialized;
} catch (error) {
console.error('检查数据库初始化状态失败:', error);
ElMessage.error('检查数据库初始化状态失败,请重试');
}
});
//
const initializeDatabase = async () => {
try {
console.log('初始化数据库 - 开始');
isInitializing.value = true;
ElMessage.info('开始初始化数据库...');
const result = await window.electronAPI.initializeDatabase();
console.log('初始化数据库 - 结果:', result);
// truesuccess
if (result === true || (result && result.success)) {
ElMessage.success('数据库初始化成功!');
console.log('初始化数据库 - 成功,更新初始化状态');
isDatabaseInitialized.value = true;
} else {
const errorMessage = result && result.error ? result.error : '未知错误';
ElMessage.error(`数据库初始化失败: ${errorMessage}`);
console.error('初始化数据库 - 失败:', errorMessage);
}
} catch (error) {
console.error('数据库初始化失败:', error);
ElMessage.error(`数据库初始化失败: ${error.message || '未知错误'}`);
} finally {
console.log('初始化数据库 - 结束');
isInitializing.value = false;
}
};
//
const handleStudentLogin = () => {
console.log('考生登录 - 开始', { studentId: studentId.value, studentName: studentName.value });
if (!studentName.value || !studentId.value) {
console.warn('考生登录 - 验证失败: 身份证号和准考证号不能为空');
alert('请输入身份证号和准考证号');
return;
}
console.log('考生登录 - 验证通过,跳转到学生首页');
router.push('/student-home');
};
//
const handleAdminLogin = async () => {
console.log('管理员登录 - 开始', { passwordLength: adminPassword.value.length });
//
const passwordError = validateAdminPassword(adminPassword.value);
if (passwordError) {
console.warn('管理员登录 - 验证失败:', passwordError);
const errorElement = document.getElementById('admin-error-message');
if (errorElement) {
errorElement.textContent = passwordError;
errorElement.style.display = 'block';
}
return;
}
//
const errorElement = document.getElementById('admin-error-message');
if (errorElement) {
errorElement.style.display = 'none';
}
try {
console.log('管理员登录 - 调用主进程登录方法');
// 使adminLogin
const result = await window.electronAPI.adminLogin({
username: 'admin',
password: adminPassword.value
});
console.log('管理员登录 - 登录结果:', result);
if (result && result.success) {
console.log('管理员登录 - 成功,跳转到管理首页');
ElMessage.success('登录成功');
router.push('/admin-home');
} else {
const errorMessage = result && result.message ? result.message : '登录失败';
console.warn('管理员登录 - 失败:', errorMessage);
ElMessage.error(errorMessage);
}
} catch (error) {
console.error('管理员登录 - 异常:', error);
ElMessage.error(`登录异常: ${error.message || '未知错误'}`);
}
};
//
const validateAdminPassword = (password) => {
//
if (!password) {
return '请输入管理员密码';
}
//
if (password.length < 4 || password.length > 32) {
return '密码长度必须在4-32个字符之间';
}
//
const regex = /^[A-Za-z0-9]+$/;
if (!regex.test(password)) {
return '密码只能包含英文大小写字母和数字';
}
return null;
};
</script>
<style scoped>/* 自定义样式 */
.bg-primary {
background-color: #1E88E5 !important;
/* 蓝色主题 */
}
.text-primary {
color: #1E88E5 !important;
}
/* 确保容器占满高度 */
.welcome-container,
.el-container {
height: 100vh;
display: flex;
flex-direction: column;
}
/* 让主内容区自动扩展并居中 */
.el-main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* 适配移动设备 */
@media (max-width: 640px) {
.max-w-md {
max-width: 100%;
}
}
</style>

22
vite.config.js Normal file
View File

@ -0,0 +1,22 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import electron from 'vite-plugin-electron'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
electron({
entry: 'electron/main.js',
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})