feat: 更新构建流程,添加 API 构建脚本和 SQL 文件复制脚本

- 修改 package.json,更新构建命令,添加 postbuild 脚本以复制 init.sql 文件。
- 新增 scripts/build-api.mjs,使用 esbuild 构建 API 代码。
- 新增 scripts/copy-init-sql.mjs,复制数据库初始化 SQL 文件到构建输出目录。
- 在 SubjectSelectionPage 组件中添加 totalScore 属性,增加历史最高分状态显示功能。
- 在 ExamSubjectPage 和 QuestionManagePage 中优化判断题答案处理逻辑。
- 在 OptionList 组件中将判断题选项文本从 'T' 和 'F' 改为 '对' 和 '错'。
- 在 QuizFooter 组件中调整样式,增加按钮和文本的可读性。
- 新增用户默认组测试用例,验证新用户创建后自动加入“全体用户”系统组。
- 新增 tsconfig.api.json,配置 API 相关 TypeScript 编译选项。
- 移除 vite.config.ts 中的 global 定义。
This commit is contained in:
2025-12-30 20:33:14 +08:00
parent 1822d8b4da
commit eb4504960e
31 changed files with 10221 additions and 150 deletions

View File

@@ -263,7 +263,9 @@ export class AdminUserController {
continue;
}
await UserModel.create({ name, phone, password });
const user = await UserModel.create({ name, phone, password });
// 统一规则:新用户默认加入“全体用户”系统组
await UserGroupModel.updateUserGroups(user.id, []);
imported++;
} catch (error: any) {
if (error.message === '手机号已存在') {

View File

@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import * as XLSX from 'xlsx';
import { UserModel, QuestionModel, QuizModel } from '../models';
import { UserModel, QuestionModel, QuizModel, UserGroupModel } from '../models';
export class BackupController {
// 导出用户数据
@@ -129,10 +129,12 @@ export class BackupController {
if (users && users.length > 0) {
for (const user of users) {
try {
await UserModel.create({
const createdUser = await UserModel.create({
name: user.姓名 || user.name,
phone: user.手机号 || user.phone
});
// 统一规则:恢复数据创建的用户也必须加入“全体用户”系统组
await UserGroupModel.updateUserGroups(createdUser.id, []);
restoredCount.users++;
} catch (error) {
console.log('用户已存在,跳过:', user. || user.phone);

View File

@@ -155,7 +155,19 @@ export class QuizController {
answer.isCorrect = false;
}
} else if (question.type === 'single' || question.type === 'judgment') {
const isCorrect = answer.userAnswer === question.answer;
const normalizeJudgment = (raw: unknown) => {
const v = String(raw ?? '').trim();
const yes = new Set(['A', 'T', 'TRUE', 'True', 'true', '1', '正确', '对', '是', 'Y', 'y', 'YES', 'yes']);
const no = new Set(['B', 'F', 'FALSE', 'False', 'false', '0', '错误', '错', '否', '不是', 'N', 'n', 'NO', 'no']);
if (yes.has(v)) return '正确';
if (no.has(v)) return '错误';
return v;
};
const isCorrect =
question.type === 'judgment'
? normalizeJudgment(answer.userAnswer) === normalizeJudgment(question.answer)
: answer.userAnswer === question.answer;
answer.score = isCorrect ? question.score : 0;
answer.isCorrect = isCorrect;
}

View File

@@ -100,7 +100,7 @@ export class UserController {
const existingUser = await UserModel.findByPhone(phone);
if (existingUser) {
if (existingUser) {
if (existingUser.password && existingUser.password !== password) {
return res.status(400).json({
success: false,
@@ -118,6 +118,11 @@ export class UserController {
});
} else {
const newUser = await UserModel.create({ name, phone, password });
// 自动加入"全体用户"组
const allUsersGroup = await UserGroupModel.getSystemGroup();
if (allUsersGroup) {
await UserGroupModel.addMember(allUsersGroup.id, newUser.id);
}
res.json({
success: true,
data: newUser

View File

@@ -2,6 +2,7 @@ import path from 'path';
import fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
// 在ES模块中创建require函数用于兼容CommonJS模块
const require = createRequire(import.meta.url);
@@ -98,6 +99,71 @@ const ensureIndex = async (createIndexSql: string) => {
await exec(createIndexSql);
};
const ensureUserGroupSchemaAndAllUsersMembership = async () => {
// 1) Ensure tables
await ensureTable(`
CREATE TABLE IF NOT EXISTS user_groups (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
description TEXT NOT NULL DEFAULT '',
is_system BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
await ensureTable(`
CREATE TABLE IF NOT EXISTS user_group_members (
group_id TEXT NOT NULL,
user_id TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (group_id, user_id),
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`);
// 2) Ensure indexes
await ensureIndex(
`CREATE INDEX IF NOT EXISTS idx_user_group_members_group_id ON user_group_members(group_id);`,
);
await ensureIndex(
`CREATE INDEX IF NOT EXISTS idx_user_group_members_user_id ON user_group_members(user_id);`,
);
// 3) Ensure system group exists
const existingSystemGroup = await get(
`SELECT id FROM user_groups WHERE is_system = 1 ORDER BY created_at ASC LIMIT 1`,
);
let systemGroupId = existingSystemGroup?.id as string | undefined;
if (!systemGroupId) {
const preferredId = 'all-users';
try {
await run(
`INSERT INTO user_groups (id, name, description, is_system) VALUES (?, ?, ?, 1)`,
[preferredId, '全体用户', '系统内置:新用户自动加入'],
);
systemGroupId = preferredId;
} catch {
const fallbackId = uuidv4();
await run(
`INSERT INTO user_groups (id, name, description, is_system) VALUES (?, ?, ?, 1)`,
[fallbackId, '全体用户', '系统内置:新用户自动加入'],
);
systemGroupId = fallbackId;
}
}
// 4) Backfill membership: ensure all existing users are in the system group
if (systemGroupId) {
await run(
`INSERT OR IGNORE INTO user_group_members (group_id, user_id)
SELECT ?, u.id FROM users u`,
[systemGroupId],
);
}
};
const migrateDatabase = async () => {
// 跳过迁移,因为数据库连接可能未初始化
console.log('跳过数据库迁移');
@@ -114,16 +180,22 @@ export const initDatabase = async () => {
if (!usersTableExists) {
// 读取并执行初始化SQL文件
const initSqlPath = path.join(path.dirname(import.meta.url.replace('file:///', '')), 'init.sql');
const initSqlPath = fileURLToPath(new URL('./init.sql', import.meta.url));
const initSql = fs.readFileSync(initSqlPath, 'utf8');
await exec(initSql);
console.log('数据库初始化成功');
// 用户组(含“全体用户”系统组)
await ensureUserGroupSchemaAndAllUsersMembership();
} else {
console.log('数据库表已存在,跳过初始化');
await ensureColumn('questions', "analysis TEXT NOT NULL DEFAULT ''", 'analysis');
await ensureColumn('quiz_records', "score_percentage REAL", 'score_percentage');
await ensureColumn('quiz_records', "status TEXT", 'status');
// 用户组(含“全体用户”系统组)
await ensureUserGroupSchemaAndAllUsersMembership();
}
} catch (error) {
console.error('数据库初始化失败:', error);

View File

@@ -11,6 +11,32 @@ CREATE TABLE users (
CREATE INDEX idx_users_phone ON users(phone);
CREATE INDEX idx_users_created_at ON users(created_at);
-- 用户组表
CREATE TABLE user_groups (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
description TEXT NOT NULL DEFAULT '',
is_system BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 用户组成员表
CREATE TABLE user_group_members (
group_id TEXT NOT NULL,
user_id TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (group_id, user_id),
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_user_group_members_group_id ON user_group_members(group_id);
CREATE INDEX idx_user_group_members_user_id ON user_group_members(user_id);
-- 内置系统用户组:全体用户
INSERT OR IGNORE INTO user_groups (id, name, description, is_system)
VALUES ('all-users', '全体用户', '系统内置:新用户自动加入', 1);
-- 题目表
CREATE TABLE questions (
id TEXT PRIMARY KEY,

View File

@@ -34,11 +34,28 @@ export interface ExcelQuestionData {
}
export class QuestionModel {
private static normalizeJudgmentAnswer(raw: unknown): string {
const v = String(raw ?? '').trim();
if (!v) return v;
// 兼容历史存储与导入A/B、T/F、true/false、1/0 等
const yes = new Set(['A', 'T', 'TRUE', 'True', 'true', '1', '正确', '对', '是', 'Y', 'y', 'YES', 'yes']);
const no = new Set(['B', 'F', 'FALSE', 'False', 'false', '0', '错误', '错', '否', '不是', 'N', 'n', 'NO', 'no']);
if (yes.has(v)) return '正确';
if (no.has(v)) return '错误';
return v;
}
// 创建题目
static async create(data: CreateQuestionData): Promise<Question> {
const id = uuidv4();
const optionsStr = data.options ? JSON.stringify(data.options) : null;
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
const normalizedAnswer =
data.type === 'judgment' && !Array.isArray(data.answer)
? this.normalizeJudgmentAnswer(data.answer)
: data.answer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
const category = data.category && data.category.trim() ? data.category.trim() : '通用';
const analysis = String(data.analysis ?? '').trim().slice(0, 255);
@@ -71,7 +88,11 @@ export class QuestionModel {
const question = questions[i];
const id = uuidv4();
const optionsStr = question.options ? JSON.stringify(question.options) : null;
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
const normalizedAnswer =
question.type === 'judgment' && !Array.isArray(question.answer)
? this.normalizeJudgmentAnswer(question.answer)
: question.answer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
@@ -132,7 +153,11 @@ export class QuestionModel {
const question = questions[i];
try {
const optionsStr = question.options ? JSON.stringify(question.options) : null;
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
const normalizedAnswer =
question.type === 'judgment' && !Array.isArray(question.answer)
? this.normalizeJudgmentAnswer(question.answer)
: question.answer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
@@ -291,7 +316,11 @@ export class QuestionModel {
}
if (data.answer !== undefined) {
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
const normalizedAnswer =
data.type === 'judgment' && !Array.isArray(data.answer)
? this.normalizeJudgmentAnswer(data.answer)
: data.answer;
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
fields.push('answer = ?');
values.push(answerStr);
}
@@ -353,6 +382,9 @@ export class QuestionModel {
return answerStr;
}
}
if (type === 'judgment') {
return this.normalizeJudgmentAnswer(answerStr);
}
return answerStr;
}

View File

@@ -329,6 +329,14 @@ export class QuizModel {
return answer;
}
}
if (type === 'judgment') {
const v = String(answer ?? '').trim();
const yes = new Set(['A', 'T', 'TRUE', 'True', 'true', '1', '正确', '对', '是', 'Y', 'y', 'YES', 'yes']);
const no = new Set(['B', 'F', 'FALSE', 'False', 'false', '0', '错误', '错', '否', '不是', 'N', 'n', 'NO', 'no']);
if (yes.has(v)) return '正确';
if (no.has(v)) return '错误';
return v;
}
return answer;
}

Binary file not shown.

View File

@@ -0,0 +1,20 @@
module.exports = {
apps: [
{
name: 'blv-oa-exam-backend',
script: 'dist/api/server.js',
cwd: 'R:/nodejsROOT/oa_exam/server',
interpreter: 'node',
node_args: '--enable-source-maps',
env: {
NODE_ENV: 'production',
PORT: '10012',
DB_PATH: 'R:/nodejsROOT/oa_exam/db/survey.db',
},
time: true,
autorestart: true,
max_restarts: 10,
restart_delay: 3000,
},
],
};

8370
deploy_bundle/server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
{
"name": "survey-system",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "concurrently \"npm run dev:api\" \"npm run dev:frontend\"",
"dev:api": "nodemon --exec tsx api/server.ts",
"dev:frontend": "vite",
"build": "vite build && node scripts/build-api.mjs",
"postbuild": "node scripts/copy-init-sql.mjs",
"preview": "vite preview",
"start": "node --enable-source-maps dist/api/server.js",
"test": "node --import tsx --test test/admin-task-stats.test.ts test/question-text-import.test.ts test/swipe-detect.test.ts test/user-tasks.test.ts test/user-records-subjectname.test.ts test/score-percentage.test.ts",
"check": "tsc --noEmit"
},
"dependencies": {
"@types/axios": "^0.9.36",
"antd": "^5.12.1",
"axios": "^1.13.2",
"concurrently": "^7.6.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^4.18.2",
"joi": "^17.11.0",
"multer": "^1.4.5-lts.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.1",
"recharts": "^3.6.0",
"sqlite3": "^5.1.7",
"tailwind-merge": "^3.4.0",
"uuid": "^9.0.1",
"xlsx": "^0.18.5",
"zustand": "^4.4.7"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/crypto-js": "^4.2.2",
"@types/express": "^4.17.21",
"@types/multer": "^1.4.11",
"@types/node": "^20.10.4",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.1.1",
"autoprefixer": "^10.4.23",
"crypto-js": "^4.2.0",
"nodemon": "^3.0.2",
"tailwindcss": "^3.3.6",
"tsx": "^4.21.0",
"esbuild": "^0.25.0",
"typescript": "^5.2.2",
"vite": "^4.5.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2020 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="477px" height="621px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 80.92 105.37"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:#008C8C}
.fil1 {fill:#008C8C;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="图层_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<path class="fil0" d="M72.67 62.65c2.16,-4 3.39,-8.58 3.39,-13.45 0,-15.66 -12.69,-28.35 -28.35,-28.35 -4.92,0 -9.54,1.26 -13.57,3.46l0 11.7c3.44,-3.54 8.25,-5.74 13.57,-5.74 10.46,0 18.93,8.47 18.93,18.93 0,10.45 -8.47,18.93 -18.93,18.93 -5.21,0 -9.93,-2.11 -13.35,-5.51 -3.4,-3.35 -4.19,-8.41 -4.36,-13.47l0 -31.15 0 -15.23 0 -0.35 0 -0.01 0 -1.04c3.33,-0.89 6.84,-1.37 10.46,-1.37 22.35,0 40.46,18.12 40.46,40.46 0,22.35 -18.11,40.46 -40.46,40.46 -22.34,0 -40.46,-18.11 -40.46,-40.46 0,-14.88 8.04,-27.89 20.01,-34.92l0 1.18 0 0 0 0.4 0 10.88 0 29.2c0,4.65 0.18,6.98 1.17,10.53 2.24,8.04 7.74,14.13 15.26,17.49 3.45,1.49 7.27,2.33 11.27,2.33 3.02,0 5.93,-0.48 8.66,-1.35 6.6,-2.95 12.23,-7.66 16.3,-13.55z"/>
<polygon class="fil1" points="15.53,105.37 15.53,97.52 65.39,97.52 65.39,105.37 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="#0A0B0D"/>
<path d="M26.6677 23.7149H8.38057V20.6496H5.33301V8.38159H26.6677V23.7149ZM8.38057 20.6496H23.6201V11.4482H8.38057V20.6496ZM16.0011 16.0021L13.8461 18.1705L11.6913 16.0021L13.8461 13.8337L16.0011 16.0021ZM22.0963 16.0008L19.9414 18.1691L17.7865 16.0008L19.9414 13.8324L22.0963 16.0008Z" fill="#32F08C"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/assets/正方形LOGO-c56db41d.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>宝来威考试平台</title>
<meta name="description" content="功能完善的在线问卷调查系统,支持多种题型、随机抽题、免注册答题等特性" />
<script type="module" crossorigin src="/assets/index-509f66ca.js"></script>
<link rel="stylesheet" href="/assets/index-46911e80.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

20
ecosystem.config.cjs Normal file
View File

@@ -0,0 +1,20 @@
module.exports = {
apps: [
{
name: 'blv-oa-exam-backend',
script: 'dist/api/server.js',
cwd: 'R:/nodejsROOT/oa_exam/server',
interpreter: 'node',
node_args: '--enable-source-maps',
env: {
NODE_ENV: 'production',
PORT: '10012',
DB_PATH: 'R:/nodejsROOT/oa_exam/db/survey.db',
},
time: true,
autorestart: true,
max_restarts: 10,
restart_delay: 3000,
},
],
};

699
package-lock.json generated
View File

@@ -39,6 +39,7 @@
"@vitejs/plugin-react": "^4.1.1",
"autoprefixer": "^10.4.23",
"crypto-js": "^4.2.0",
"esbuild": "^0.25.0",
"nodemon": "^3.0.2",
"tailwindcss": "^3.3.6",
"tsx": "^4.21.0",
@@ -481,9 +482,9 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
@@ -498,9 +499,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
@@ -515,9 +516,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
@@ -532,9 +533,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
@@ -549,9 +550,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
@@ -566,9 +567,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
@@ -583,9 +584,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
@@ -600,9 +601,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
@@ -617,9 +618,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
@@ -634,9 +635,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
@@ -651,9 +652,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
@@ -668,9 +669,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
@@ -685,9 +686,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
@@ -702,9 +703,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
@@ -719,9 +720,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
@@ -736,9 +737,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
@@ -753,9 +754,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
@@ -770,9 +771,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
@@ -787,9 +788,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
@@ -804,9 +805,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
@@ -821,9 +822,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
@@ -838,9 +839,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
@@ -855,9 +856,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
@@ -872,9 +873,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
@@ -889,9 +890,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
@@ -906,9 +907,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
@@ -3079,9 +3080,9 @@
]
},
"node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -3092,32 +3093,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/netbsd-x64": "0.27.2",
"@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.27.2",
"@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/escalade": {
@@ -7040,6 +7041,490 @@
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/netbsd-x64": "0.27.2",
"@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.27.2",
"@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

View File

@@ -7,10 +7,11 @@
"dev": "concurrently \"npm run dev:api\" \"npm run dev:frontend\"",
"dev:api": "nodemon --exec tsx api/server.ts",
"dev:frontend": "vite",
"build": "tsc && vite build",
"build": "vite build && node scripts/build-api.mjs",
"postbuild": "node scripts/copy-init-sql.mjs",
"preview": "vite preview",
"start": "node dist/api/server.js",
"test": "node --import tsx --test test/admin-task-stats.test.ts test/question-text-import.test.ts test/swipe-detect.test.ts test/user-tasks.test.ts test/user-records-subjectname.test.ts test/score-percentage.test.ts",
"start": "node --enable-source-maps dist/api/server.js",
"test": "node --import tsx --test test/admin-task-stats.test.ts test/question-text-import.test.ts test/swipe-detect.test.ts test/user-tasks.test.ts test/user-records-subjectname.test.ts test/score-percentage.test.ts test/user-default-group.test.ts",
"check": "tsc --noEmit"
},
"dependencies": {
@@ -48,6 +49,7 @@
"nodemon": "^3.0.2",
"tailwindcss": "^3.3.6",
"tsx": "^4.21.0",
"esbuild": "^0.25.0",
"typescript": "^5.2.2",
"vite": "^4.5.0"
}

35
scripts/build-api.mjs Normal file
View File

@@ -0,0 +1,35 @@
import { build } from 'esbuild';
import fs from 'node:fs';
import path from 'node:path';
const projectRoot = process.cwd();
const pkgPath = path.join(projectRoot, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const dependencies = Object.keys(pkg.dependencies ?? {});
const optionalDependencies = Object.keys(pkg.optionalDependencies ?? {});
const peerDependencies = Object.keys(pkg.peerDependencies ?? {});
const externals = Array.from(
new Set([
...dependencies,
...optionalDependencies,
...peerDependencies,
// also keep these as runtime externals
'sqlite3',
]),
);
await build({
entryPoints: ['api/server.ts'],
outfile: 'dist/api/server.js',
bundle: true,
platform: 'node',
format: 'esm',
target: ['node20'],
sourcemap: true,
logLevel: 'info',
external: externals,
});
console.log('Built backend -> dist/api/server.js');

13
scripts/copy-init-sql.mjs Normal file
View File

@@ -0,0 +1,13 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const projectRoot = process.cwd();
const src = path.join(projectRoot, 'api', 'database', 'init.sql');
const destDir = path.join(projectRoot, 'dist', 'api', 'database');
const dest = path.join(destDir, 'init.sql');
await fs.mkdir(destDir, { recursive: true });
await fs.copyFile(src, dest);
console.log(`Copied init.sql -> ${dest}`);

View File

@@ -29,6 +29,7 @@ interface ExamTask {
usedAttempts?: number;
maxAttempts?: number;
bestScore?: number;
totalScore?: number;
}
export const SubjectSelectionPage: React.FC = () => {
@@ -88,6 +89,35 @@ export const SubjectSelectionPage: React.FC = () => {
return tasks.filter(task => getTaskStatus(task) === status);
};
const getScoreStatus = (pct: number): '优秀' | '合格' | '不及格' => {
if (!Number.isFinite(pct)) return '不及格';
if (pct >= 80) return '优秀';
if (pct >= 60) return '合格';
return '不及格';
};
const getScoreStatusTagColor = (status: '优秀' | '合格' | '不及格') => {
if (status === '优秀') return 'green';
if (status === '合格') return 'blue';
return 'red';
};
const renderBestHistoryTag = (task: ExamTask, subject?: ExamSubject) => {
const usedAttempts = Number(task.usedAttempts) || 0;
if (usedAttempts <= 0) return null;
if (typeof task.bestScore !== 'number') return null;
const totalScore = Number(subject?.totalScore ?? task.totalScore) || 0;
const pct = totalScore > 0 ? (task.bestScore / totalScore) * 100 : NaN;
const status = getScoreStatus(pct);
return (
<Tag color={getScoreStatusTagColor(status)} className="text-xs">
{status}
</Tag>
);
};
const startQuiz = async (taskId: string) => {
if (!taskId) {
message.warning('请选择考试任务');
@@ -224,9 +254,7 @@ export const SubjectSelectionPage: React.FC = () => {
<Tag color="green" className="text-xs">
{usedAttempts}/{maxAttempts}
</Tag>
{typeof task.bestScore === 'number' ? (
<Tag color="green" className="text-xs"> {task.bestScore} </Tag>
) : null}
{renderBestHistoryTag(task, subject)}
</div>
</div>
<div className="flex justify-between items-center mb-2">
@@ -300,9 +328,7 @@ export const SubjectSelectionPage: React.FC = () => {
<Tag color={attemptsExhausted ? 'red' : 'default'} className="text-xs">
{usedAttempts}/{maxAttempts}
</Tag>
{typeof task.bestScore === 'number' ? (
<Tag color="green" className="text-xs"> {task.bestScore} </Tag>
) : null}
{renderBestHistoryTag(task, subject)}
</div>
</div>
<div className="flex justify-between items-center mb-2">
@@ -377,9 +403,7 @@ export const SubjectSelectionPage: React.FC = () => {
<Tag color="blue" className="text-xs">
{usedAttempts}/{maxAttempts}
</Tag>
{typeof task.bestScore === 'number' ? (
<Tag color="green" className="text-xs"> {task.bestScore} </Tag>
) : null}
{renderBestHistoryTag(task, subject)}
</div>
</div>
<div className="flex justify-between items-center mb-2">
@@ -407,11 +431,7 @@ export const SubjectSelectionPage: React.FC = () => {
</div>
</div>
{tasks.length === 0 && (
<div className="text-center py-8 bg-gray-50 border-dashed border-2 border-gray-200 rounded">
<Text type="secondary" className="text-sm"></Text>
</div>
)}
</div>
</div>

View File

@@ -730,9 +730,12 @@ const ExamSubjectPage = () => {
{Array.isArray(question.answer) ?
// 多选题:直接拼接答案,不需要转换
question.answer.join(', ') :
question.type === 'judgment' ?
// 判断题A=正确B=错误
(question.answer === 'A' ? '正确' : '错误') :
question.type === 'judgment' ? (() => {
const v = String(question.answer ?? '').trim();
if (['A', 'T', 'true', 'True', 'TRUE', '1', '正确', '对', ''].includes(v)) return '正确';
if (['B', 'F', 'false', 'False', 'FALSE', '0', '错误', '错', '否', '不是'].includes(v)) return '错误';
return v;
})() :
// 单选题:直接显示答案,不需要转换
question.answer}
</span>

View File

@@ -116,11 +116,19 @@ const QuestionManagePage = () => {
};
const handleEdit = (question: Question) => {
const normalizeJudgmentAnswer = (raw: unknown) => {
const v = String(raw ?? '').trim();
if (['A', 'T', 'true', 'True', 'TRUE', '1', '正确', '对', '是'].includes(v)) return '正确';
if (['B', 'F', 'false', 'False', 'FALSE', '0', '错误', '错', '否', '不是'].includes(v)) return '错误';
return v;
};
setEditingQuestion(question);
form.setFieldsValue({
...question,
options: question.options?.join('\n'),
analysis: question.analysis || ''
analysis: question.analysis || '',
answer: question.type === 'judgment' ? normalizeJudgmentAnswer(question.answer) : question.answer,
});
setModalVisible(true);
};
@@ -137,9 +145,17 @@ const QuestionManagePage = () => {
const handleSubmit = async (values: any) => {
try {
const normalizeJudgmentAnswer = (raw: unknown) => {
const v = String(raw ?? '').trim();
if (['A', 'T', 'true', 'True', 'TRUE', '1', '正确', '对', '是'].includes(v)) return '正确';
if (['B', 'F', 'false', 'False', 'FALSE', '0', '错误', '错', '否', '不是'].includes(v)) return '错误';
return v;
};
const formData = {
...values,
options: values.options ? values.options.split('\n').filter((opt: string) => opt.trim()) : undefined
options: values.options ? values.options.split('\n').filter((opt: string) => opt.trim()) : undefined,
answer: values.type === 'judgment' ? normalizeJudgmentAnswer(values.answer) : values.answer,
};
if (editingQuestion) {

View File

@@ -78,7 +78,7 @@ export const OptionList = ({ type, options, value, onChange, disabled }: OptionL
? 'bg-[#00897B] border-[#00897B] text-white'
: 'bg-white border-gray-300 text-gray-500'}
`}>
{type === 'judgment' ? (index === 0 ? 'T' : 'F') : getOptionLabel(index)}
{type === 'judgment' ? (index === 0 ? '' : '') : getOptionLabel(index)}
</div>
{/* 选项内容 */}

View File

@@ -24,24 +24,24 @@ export const QuizFooter = ({
const isLast = current === total - 1;
return (
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 px-2 py-1.5 safe-area-bottom z-30 shadow-[0_-2px_10px_rgba(0,0,0,0.05)]">
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 px-2 py-2 safe-area-bottom z-30 shadow-[0_-2px_10px_rgba(0,0,0,0.05)]">
<div className="max-w-md mx-auto flex items-center justify-between">
<Button
type="text"
icon={<LeftOutlined />}
onClick={onPrev}
disabled={isFirst}
className={`flex items-center text-gray-600 hover:text-[#00897B] text-xs ${isFirst ? 'opacity-30' : ''}`}
className={`flex items-center text-gray-600 hover:text-[#00897B] text-sm ${isFirst ? 'opacity-30' : ''}`}
>
</Button>
<div
onClick={onOpenSheet}
className="flex flex-col items-center justify-center -mt-4 bg-white rounded-full h-11 w-11 shadow-lg border border-gray-100 cursor-pointer active:scale-95 transition-transform"
className="flex flex-col items-center justify-center -mt-5 bg-white rounded-full h-14 w-14 shadow-lg border border-gray-100 cursor-pointer active:scale-95 transition-transform"
>
<AppstoreOutlined className="text-sm text-[#00897B] mb-0.5" />
<span className="text-[10px] text-gray-500 scale-90">
<AppstoreOutlined className="text-base text-[#00897B] mb-0.5" />
<span className="text-[12px] text-gray-500">
{answeredCount}/{total}
</span>
</div>
@@ -50,7 +50,7 @@ export const QuizFooter = ({
<Button
type="text"
onClick={onSubmit}
className="flex items-center text-[#00897B] font-medium hover:bg-teal-50 text-xs"
className="flex items-center text-[#00897B] font-medium hover:bg-teal-50 text-sm"
>
<RightOutlined />
</Button>
@@ -58,7 +58,7 @@ export const QuizFooter = ({
<Button
type="text"
onClick={onNext}
className="flex items-center text-gray-600 hover:text-[#00897B] text-xs"
className="flex items-center text-gray-600 hover:text-[#00897B] text-sm"
>
<RightOutlined />
</Button>

View File

@@ -0,0 +1,78 @@
import test from 'node:test';
import assert from 'node:assert/strict';
process.env.NODE_ENV = 'test';
process.env.DB_PATH = ':memory:';
const jsonFetch = async (
baseUrl: string,
path: string,
options?: { method?: string; body?: unknown },
) => {
const res = await fetch(`${baseUrl}${path}`, {
method: options?.method ?? 'GET',
headers: options?.body ? { 'Content-Type': 'application/json' } : undefined,
body: options?.body ? JSON.stringify(options.body) : undefined,
});
const text = await res.text();
let json: any = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { status: res.status, json, text };
};
test('新用户创建后默认加入“全体用户”系统组(含 validate 自动创建与管理员导入)', async () => {
const { initDatabase } = await import('../api/database');
await initDatabase();
const { app } = await import('../api/server');
const server = app.listen(0);
try {
const addr = server.address();
assert.ok(addr && typeof addr === 'object');
const baseUrl = `http://127.0.0.1:${addr.port}`;
// 1) validate 自动创建用户
const created = await jsonFetch(baseUrl, '/api/users/validate', {
method: 'POST',
body: { name: '默认组用户', phone: '13800139001', password: '' },
});
assert.equal(created.status, 200);
assert.equal(created.json?.success, true);
const userId = created.json?.data?.id as string;
assert.ok(userId);
const list1 = await jsonFetch(baseUrl, '/api/admin/users?page=1&limit=20');
assert.equal(list1.status, 200);
assert.equal(list1.json?.success, true);
const row1 = (list1.json?.data as any[]).find((u) => u.id === userId);
assert.ok(row1);
assert.ok(Array.isArray(row1.groups));
assert.ok(row1.groups.some((g: any) => g.isSystem === 1 || g.isSystem === true));
// 2) 管理员导入用户(模拟 Excel走 importUsers 的解析逻辑不方便,这里直接调用 createUser 接口覆盖管理端创建路径)
const createdAdmin = await jsonFetch(baseUrl, '/api/admin/users', {
method: 'POST',
body: { name: '管理员创建用户', phone: '13800139002', password: '', groupIds: [] },
});
assert.equal(createdAdmin.status, 200);
assert.equal(createdAdmin.json?.success, true);
const userId2 = createdAdmin.json?.data?.id as string;
assert.ok(userId2);
const list2 = await jsonFetch(baseUrl, '/api/admin/users?page=1&limit=50');
assert.equal(list2.status, 200);
assert.equal(list2.json?.success, true);
const row2 = (list2.json?.data as any[]).find((u) => u.id === userId2);
assert.ok(row2);
assert.ok(Array.isArray(row2.groups));
assert.ok(row2.groups.some((g: any) => g.isSystem === 1 || g.isSystem === true));
} finally {
server.close();
}
});

10
tsconfig.api.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./api",
"outDir": "./dist/api",
"types": ["node"]
},
"include": ["api/**/*"],
"exclude": ["node_modules", "dist", "src", "test", "deploy_bundle"]
}

View File

@@ -23,7 +23,4 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
define: {
global: 'globalThis',
},
});