cst-exam 项目复盘

cst-exam

项目基本架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
src/
├── routes/ # 页面级路由
│ ├── login/ # 学生登录页 -> /login
│ │ └── +page.svelte
│ │
│ ├── teacher/ # 教师模块 -> /teacher
│ │ ├── login/ # 教师登录页
│ │ ├── 其他页面/ # 教师子页面(例如试卷、成绩等)
│ │ ├── +page.svelte
│ │ ├── +error.svelte
│ │ └── +layout.svelte
│ │
│ ├── student/ # 学生模块 -> /student
│ │ ├── 其他页面/ # 学生子页面(例如练习、成绩等)
│ │ ├── +page.svelte
│ │ ├── +error.svelte
│ │ └── +layout.svelte
│ │
│ ├── admin/ # 管理员模块 -> /admin
│ │ ├── login/ # 管理员登录页
│ │ ├── 其他页面/ # 管理员子页面(如用户管理、题库管理等)
│ │ ├── +page.svelte
│ │ ├── +layout.svelte
│ │ └── +error.svelte
│ │
│ ├── +error.svelte # 全局错误页面
│ └── +page.svelte # 根页面(如用于重定向)

├── apis/ # 接口封装
│ ├── auth/ # 登录、注册、登出相关接口
│ │ ├── admin_login/ # 管理员登录接口
│ │ ├── teacher_login/ # 教师登录接口
│ │ └── student_login/ # 学生登录接口
│ │
│ ├── teacher/ # 教师相关接口
│ │ ├── 其他页面/ # 如试卷、成绩、题库等接口模块
│ │
│ ├── admin/ # 管理员相关接口
│ │ ├── 其他页面/ # 如用户管理、统计接口等
│ │
│ └── student/ # 学生相关接口(未展开)
│ ├── 其他页面/ # 学生不同页面接口等

├── stores/ # 状态管理
│ ├── teacherStore.js # 教师状态
│ ├── studentStore.js # 学生状态
│ └── adminStore.js # 管理员状态

├── components/ # 通用组件
│ ├── admin/ # 管理员模块专用组件
│ ├── student/ # 学生模块专用组件
│ ├── teacher/ # 教师模块专用组件
│ └── common/ # 通用组件(ButtonModalInput 等)

├── lib/ # 公共方法、封装逻辑
│ ├── toast.js # Toast 消息提示封装
│ └── utils/ # 工具函数与请求封装
│ ├── admin-http/ # 管理员接口请求封装
│ ├── teacher-http/ # 教师接口请求封装
│ └── student-http/ # 学生接口请求封装

├── app.html # 应用 HTML 模板

static/
└── assets/ # 静态资源(图标、图片)
├── icon/ # 图标资源
│ ├── admin/ # 管理员图标
│ ├── student/ # 管理员图标
│ └── teacher/ # 教师图标

└── image/ # 图片资源
├── admin/ # 管理员相关图片
├── student/ # 管理员图标
└── teacher/ # 教师相关图片

配置@/路径:

配置快捷导入的@/路径: 在 SvelteKit 中配置快捷路径(如 @/ 指向 src/),可以通过修改 vite.config.js 来实现。SvelteKit 基于 Vite,所以路径别名配置是一样的。

步骤如下:

  1. 安装路径别名辅助插件
1
pnpm add -D @rollup/plugin-alias
  1. 修改 vite.config.js ,添加 resolve.alias 配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
// vite.config.js
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite";
import path from "path";

export default defineConfig({
plugins: [sveltekit()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"), // 配置 @ 指向 src 目录
},
},
});

需要注意的是,上面的配置不生效!!!

需要在svelte.config.js中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
import adapter from "@sveltejs/adapter-node";
import path from "path";

const config = {
kit: {
adapter: adapter(),
alias: {
"@": path.resolve("./src"), // ✅ 配置 @ 指向 src
},
},
};

export default config;

原因:

配置代码统一格式化:

  1. 安装 Prettier 及相关插件
1
npm install -D prettier prettier-plugin-svelte
  1. 添加 .prettierrc 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"svelteSortOrder": "scripts-markup-styles-options",
"svelteStrictMode": false,
"svelteBracketNewLine": true,
"svelteAllowShorthand": true,
"plugins": ["prettier-plugin-svelte"]
}
  1. 添加 .prettierignore 忽略文件(可选)
1
2
3
node_modules.build;
dist;
public;
  1. VS Code 配置自动保存时格式化(推荐)

打开 VS Code 的设置(快捷键:Ctrl + ,),搜索并启用:

  • Format On Save
  • Editor: Default Formatter 选择 Prettier

或者编辑 .vscode/settings.json(推荐放进项目中):

1
2
3
4
5
6
7
8
9
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.exclude": {
"**/.DS_Store": true,
"**/.vscode": false
},
"prettier.requireConfig": true
}

更详细的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
.vscode/settings.json
{
// 文件树嵌套显示(更整洁)
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"tsconfig.json": "tsconfig.*.json, env.d.ts",
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .prettier*, prettier*, .editorconfig",
"*.svelte": "*.svelte.ts, *.svelte.js"
},

// 保存时自动修复(由 ESLint 处理)
"editor.codeActionsOnSave": {
"source.fixAll": true
},

// 格式化配置(由 Prettier 处理)
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",

// ESLint 启用与验证(包含 Svelte 文件)
"eslint.enable": true,
"eslint.validate": ["javascript", "typescript", "svelte"],

// Svelte 专属格式化
"[svelte]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

// JavaScript/HTML 格式化(可选)
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},

// Prettier 选项(根据你习惯可修改)
"prettier.printWidth": 100,
"prettier.singleQuote": true,
"prettier.semi": true,
"prettier.singleAttributePerLine": false,
"prettier.vueIndentScriptAndStyle": false
}

遇到的详细问题

1.重定向到登录页面(在项目首次打开的时候):

1
2
3
4
5
6
7
src / routes / +page.server.js;

import { redirect } from "@sveltejs/kit";

export function load() {
throw redirect(307, "/login");
}

🔄 效果:

  • 当用户访问根路径 / 时,会被立即重定向到 /login
  • 307 状态码表示临时重定向,是推荐的跳转方式。

统一组件化开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
Button.svelte
<script>
/**
* plain 是否为镂空按钮 true false
* size 按钮大小 small medium large
* type 按钮类型 primary success danger warning info
* disabled 是否禁用 true false
* round 是否为圆角按钮 true false
*/
export let plain = false
export let size = 'medium'
export let type = ''
export let disabled = false
export let round = false
export let iconurl = ''

$: classes = ['button', size, type, plain && 'plain', disabled && 'disabled', round && 'round']
.filter(Boolean)
.join(' ')
</script>

<button class={classes} {disabled}>
{#if iconurl}
<img class="icon" src={iconurl} alt="" />
{/if}
<span class="button-text"><slot /></span>
</button>

<style>
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5em 1em;
border: none;
font-size: 14px;
border-radius: 4px;
transition: all 0.2s ease;
color: #333;
}

.small {
font-size: 12px;
padding: 0.4em 1em;
}

.medium {
font-size: 14px;
padding: 0.5em 1.2em;
}

.large {
font-size: 16px;
padding: 0.6em 1.4em;
}

.primary {
background-color: #2a58ff;
color: white;
}

.success {
background-color: #28a745;
color: white;
}

.danger {
background-color: #dc3545;
color: white;
}

.warning {
background-color: #ffc107;
color: black;
}

.info {
background-color: #17a2b8;
color: white;
}

.plain.primary {
background-color: transparent;
border: 1px solid #2a58ff;
color: #2a58ff;
}
.plain.success {
background-color: transparent;
border: 1px solid #28a745;
color: #28a745;
}
.plain.danger {
background-color: transparent;
border: 1px solid #dc3545;
color: #dc3545;
}
.plain.warning {
background-color: transparent;
border: 1px solid #ffc107;
color: #ffc107;
}
.plain.info {
background-color: transparent;
border: 1px solid #17a2b8;
color: #17a2b8;
}

.round {
border-radius: 50px;
}

.button:not(.disabled):active {
transform: scale(0.97);
}

.icon-box {
align-items: center;
justify-content: center;
}

.icon {
width: 1em;
height: 1em;
margin-right: 0.5em;
display: inline-block;
vertical-align: middle;
object-fit: contain;
}
.button-text {
display: inline-block;
vertical-align: middle;
}
</style>
MessageBox .svelte

配置 axios

1
pnpm add axios

现阶段标注的问题

  • 创建班级需要添加课程名称,缺失了课程名称

    课程分类应该是被砍掉了,不用加

  • 班级管理中需要添加删除班级的`功能,这里缺少了

配置 mock 进行接口模拟测试

1
2
pnpm add mockjs -D
# 或者 npm/yarn 都行

临时放置代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
{
"code": 0,
"message": "success",
"data": {
"total": 5,
"records": {
"classId": 1,
"className": "java课程班级",
"teacherName": "张晓磊",
"createTime": "2025-04-13T00:00:00Z",
"total": 5,
"records": [
{
"ID": 2,
"Username": "123123",
"Account": "1231313",
"Password": "24324234",
"Name": "2342",
"Gender": "2",
"Phone": "12412412424",
"IdNumber": "446327826373182633",
"FrontPhoto": " ",
"ReversePhoto": " ",
"EntryTime": "2025-04-16T00:00:00Z",
"LastStudyTime": "2025-04-15T00:00:00Z",
"ExamNumber": "323061000942",
"StudentSource": 1,
"Email": "2526334@qq.com",
"Address": "广东省番禺区",
"Status": 1,
"ViolationReason": "发布不当言论",
"CreateTime": "2024-02-02T00:00:00Z",
"UpdateTime": "2025-04-15T00:00:00Z"
},
{
"ID": 17,
"Username": "",
"Account": "zhaoxiaolei_001",
"Password": "qF9cHlkJ6IL3",
"Name": "",
"Gender": "1",
"Phone": "13523452133",
"IdNumber": "",
"FrontPhoto": "",
"ReversePhoto": "",
"EntryTime": "2025-04-18T09:49:49.130205Z",
"LastStudyTime": "0001-01-01T00:00:00Z",
"ExamNumber": "",
"StudentSource": 0,
"Email": "user8765@gmail.com",
"Address": "",
"Status": 1,
"ViolationReason": "发布不当言论",
"CreateTime": "2025-
{
"code": 0,
"message": "success",
"data": {
"total": 3,
"records": [
{
"ID": 11,
"ClassName": "golang开发0",
"ClassNumber": 30,
"ClassStudentNumber": 0,
"TeacherID": 1,
"CreateTime": "2025-04-23 22:45:59",
"UpdateTime": "2025-04-23 22:45:59"
},
{
"ID": 13,
"ClassName": "php开发",
"ClassNumber": 30,
"ClassStudentNumber": 0,
"TeacherID": 1,
"CreateTime": "2025-04-28 17:19:25",
"UpdateTime": "2025-04-28 17:19:25"
},
{
"ID": 12,
"ClassName": "svelte开发",
"ClassNumber": 29,
"ClassStudentNumber": 0,
"TeacherID": 1,
"CreateTime": "2025-04-25 09:53:08",
"UpdateTime": "2025-04-28 17:20:23"
}
]
}
}

下载文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 下载模板文件
*/
const hander_download_document = () => {
TeacherManagement_API.get_templete_excel()
.then((Response) => {
if (Response.code === 0) {
const blob = new Blob([Response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "教师信息导入模板.xlsx";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
toast.success("下载成功");
} else {
toast.error("下载失败");
toast.error("下载失败");
}
})
.catch((error) => {
toast.error("下载失败");
console.log("下载失败", error);
});
};

压缩包文件下载:

1
2
3
const blob = new Blob([Response], {
type: "application/zip",
});

上传文件

❗ 为啥「点击上传」必须要 input?

浏览器不会允许 JavaScript 随意访问本地文件系统 ——

点击上传只能靠 <input type="file"> 元素打开文件选择框,不管你隐藏得多深,底层都是它。

✅ 但你可以让 input 完全“隐身”!

已完成模块记录

页面部分—整个页面模拟接口

  • 查看班级管理中的学生列表中的学生详细信息
  • 教师新增学生账号页面
  • 班级信息页面
  • 班级列表管理页面
  • 班级管理页面
  • 当个新增学生账号页面

页面部分—页面样式

  • 管理员端的教师列表页面
  • 学生列表中的学生详细信息页面
  • 教师新增学生账号页面
  • 班级列表管理页面
  • 班级管理页面
  • 当个新增学生账号页面

接口部分—对接情况

  • 查看详细班级信息—班级信息与学生列表
  • 举报学生—学生列表中的举报学生
  • 查看学生列表中的某一个详细的学生信息
  • 导出全部学生列表—pdf or excel
  • 教师新增学生账号接口
  • 班级信息列表
  • 创建班级接口
  • 编辑班级接口
  • 获取后端自动生成的账户密码
  • 根据班级id获取班级信息这个接口不能用 有问题,冲突了,而且信息不全,没有教师姓名,只有教师 id

统一状态管理设置

存储状态的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { writable } from "svelte/store";

/**
* 加密信息---基础加密
* 1. 加密后存储到本地
* 2. 解密后使用
*/
function encrypt_Data(data) {
return btoa(encodeURIComponent(JSON.stringify(data)));
}
function decrypt_Data(encrypted) {
try {
return JSON.parse(decodeURIComponent(atob(encrypted)));
} catch {
return null;
}
}

/**
* 设置用户信息
*/
const local_key = "adminUserInfo";

const default_adminUserInfo = decrypt_Data(localStorage.getItem(local_key)) || {
is_login: false,
user_info: {
id: "",
account: "",
name: "",
phone: "",
},
token: "",
};

const adminInforStore = function createAdminInforStore() {
const { subscribe, set } = writable(default_adminUserInfo);

subscribe((value) => {
localStorage.setItem(local_key, encrypt_Data(value));
});

return {
subscribe,
login: (userInfo) => {
set({
is_login: true,
user_info: {
id: userInfo.ID,
account: userInfo.Account,
name: userInfo.Name,
phone: userInfo.Phone,
},
token: userInfo.Token,
});
},

logout: () => {
set({
is_login: false,
user_info: {},
token: "",
});
},
};
};

export { adminInforStore };

在 Svelte 项目中获取 adminInforStore 数据有以下几种方式:

  1. 在 Svelte 组件中直接订阅 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script>
import { adminInforStore } from '$lib/stores/adminStore.js'

let userInfo
const unsubscribe = adminInforStore.subscribe(value => {
userInfo = value
})

onDestroy(unsubscribe) // 组件销毁时取消订阅
</script>

{#if $adminInforStore.is_login}
<p>欢迎, {$adminInforStore.user_info.name}</p>
{/if}
<script>
import { adminInforStore } from '$lib/stores/adminStore.js'
</script>

<div>
当前登录状态: {$adminInforStore.is_login ? '已登录' : '未登录'}
</div
import { adminInforStore } from '$lib/stores/adminStore.js'

export function checkAuth() {
let currentUser
const unsubscribe = adminInforStore.subscribe(user => {
currentUser = user
})
unsubscribe() // 立即取消订阅

return currentUser.is_login
}

注意事项:

  1. 使用 $ 前缀可以自动订阅和取消订阅(推荐方式)
  2. 手动订阅时务必记得取消订阅防止内存泄漏
  3. 数据已经是解密后的状态,无需额外处理

解决跨域的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 server: {
host: 'localhost',
port: 8080,
open: true,
proxy: {
'/api': {
target: '<https://y.w2w.io:6443>',
changeOrigin: true,
rewrite: path => path.replace(/^\\/api/, '')
}
}
}
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
plugins: [sveltekit()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src') // 配置 @ 指向 src 目录
}
},

server: {
host: 'localhost',
port: 8080,
open: true,
proxy: {
'/api': {
target: '<https://y.w2w.io:6443>',
changeOrigin: true,
rewrite: path => path.replace(/^\\/api/, '')
}
}
}
})

组件之间传递函数方法的三种方式

方法一:

使用 bind:this 获取组件实例 子组件暴露方法,父组件调用方法。

子组件:

1
2
3
4
5
6
7
<script>
export function resetDates() {
console.log('子组件方法被调用');
}
</script>

<p>我是子组件</p>

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import Child from './Child.svelte';

let childRef;

function callChildMethod() {
childRef.resetDates(); // 调用子组件方法
}
</script>

<Child bind:this={childRef} />

<button on:click={callChildMethod}>调用子组件方法</button>

方法二:

用事件派发 希望子组件触发动作但由父组件来处理逻辑,可以用 createEventDispatcher

子组件:

1
2
3
4
5
6
7
8
9
10
11
<script>
import { createEventDispatcher } from 'svelte';

const dispatch = createEventDispatcher();

function handleClick() {
dispatch('reset'); // 派发事件
}
</script>

<button on:click={handleClick}>通知父组件</button>

父组件:

1
2
3
4
5
6
7
<Child on:reset={handleReset} />

<script>
function handleReset() {
console.log('收到子组件的 reset 事件');
}
</script>

方法三:

使用全局状态管理store来解决