vue创建欢迎页、初始化数据库、管理员密码验证、页脚题库版本
This commit is contained in:
commit
0bf9ff662d
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal 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
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
505
README.md
Normal file
505
README.md
Normal 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
2
data/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
73
electron/db/config.js
Normal file
73
electron/db/config.js
Normal 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
263
electron/db/index.js
Normal 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
42
electron/db/path.js
Normal 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
356
electron/db/schema.js
Normal 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
93
electron/db/utils.js
Normal 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
163
electron/main.js
Normal 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
26
electron/preload.js
Normal 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])
|
||||
}
|
||||
})
|
43
electron/service/auth.service.js
Normal file
43
electron/service/auth.service.js
Normal 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
|
||||
};
|
61
electron/service/system.js
Normal file
61
electron/service/system.js
Normal 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
13
index.html
Normal 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
8
jsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
8646
package-lock.json
generated
Normal file
8646
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
package.json
Normal file
44
package.json
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
14
src/App.vue
Normal file
14
src/App.vue
Normal 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
31
src/assets/base.css
Normal 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
1
src/assets/logo.svg
Normal 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
1
src/assets/main.css
Normal file
@ -0,0 +1 @@
|
||||
@import './base.css';
|
57
src/components/common/Footer.vue
Normal file
57
src/components/common/Footer.vue
Normal 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>
|
41
src/components/common/Header.vue
Normal file
41
src/components/common/Header.vue
Normal 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>
|
9
src/components/common/Loading.vue
Normal file
9
src/components/common/Loading.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
33
src/main.js
Normal file
33
src/main.js
Normal 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
26
src/router/index.js
Normal 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
0
src/utils/validate.js
Normal file
187
src/views/AdminHomeView.vue
Normal file
187
src/views/AdminHomeView.vue
Normal 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>
|
91
src/views/StudentHomeView.vue
Normal file
91
src/views/StudentHomeView.vue
Normal 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
276
src/views/WelcomeView.vue
Normal 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);
|
||||
|
||||
// 修复:同时处理布尔值true和带success属性的对象
|
||||
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
22
vite.config.js
Normal 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))
|
||||
},
|
||||
},
|
||||
})
|
Loading…
Reference in New Issue
Block a user