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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* 选项内容 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user