first commit

This commit is contained in:
2026-03-21 09:12:47 +08:00
commit a1e76157c9
80 changed files with 506309 additions and 0 deletions

24
exam-viewer/.gitignore vendored Normal file
View File

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

3
exam-viewer/.vscode/extensions.json vendored Normal file
View File

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

5
exam-viewer/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

13
exam-viewer/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>exam-viewer</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1570
exam-viewer/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
exam-viewer/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "exam-viewer",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"element-plus": "^2.13.6",
"pinia": "^3.0.4",
"vue": "^3.5.30"
},
"devDependencies": {
"@types/node": "^24.12.0",
"@vitejs/plugin-vue": "^6.0.5",
"@vue/tsconfig": "^0.9.0",
"typescript": "~5.9.3",
"vite": "^8.0.1",
"vue-tsc": "^3.2.5"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

522
exam-viewer/src/App.vue Normal file
View File

@@ -0,0 +1,522 @@
<template>
<div class="app-container">
<el-container>
<el-aside width="220px" class="sidebar">
<div class="logo">
<h2>MB-330 考试学习</h2>
</div>
<el-menu
:default-active="String(currentTopic)"
@select="handleTopicSelect"
class="topic-menu"
>
<el-menu-item
v-for="topic in topics"
:key="topic"
:index="String(topic)"
>
<span>Topic {{ topic }}</span>
<el-badge :value="topicStats[topic] || 0" class="topic-badge" />
</el-menu-item>
</el-menu>
</el-aside>
<el-main class="main-content">
<div v-if="loading" class="loading-container">
<el-icon class="is-loading" :size="40"><Loading /></el-icon>
<p>加载中...</p>
</div>
<template v-else-if="currentQuestion">
<div class="question-header">
<h3>Topic {{ currentTopic }} - Question {{ currentQuestion.question_num }}</h3>
<div class="header-right">
<el-button
type="warning"
size="small"
@click="handleOpenPdf"
>
<el-icon><Document /></el-icon>
查看原PDF
</el-button>
<div class="jump-control">
<span>跳转到第</span>
<el-input-number
v-model="jumpQuestionNum"
:min="1"
:max="currentTopicQuestions.length"
size="small"
controls-position="right"
/>
<span></span>
<el-button type="primary" size="small" @click="handleJumpQuestion">
跳转
</el-button>
</div>
<span class="question-progress">
{{ currentQuestionIndex + 1 }} / {{ currentTopicQuestions.length }}
</span>
</div>
</div>
<div class="question-content">
<div class="bilingual-container">
<div class="language-panel english-panel">
<div class="panel-header">
<el-tag type="primary">English</el-tag>
</div>
<div class="stem-text">
{{ currentQuestion.stem }}
</div>
<div class="options-list">
<div
v-for="option in currentQuestion.options"
:key="option.label"
class="option-item"
:class="{
'correct-option': showAnswer && currentQuestion.answer.includes(option.label)
}"
>
<span class="option-label">{{ option.label }}.</span>
<span class="option-text">{{ option.text }}</span>
</div>
</div>
</div>
<div class="language-panel chinese-panel">
<div class="panel-header">
<el-tag type="success">中文</el-tag>
</div>
<div class="stem-text">
{{ currentQuestion.stem_cn || '待翻译...' }}
</div>
<div class="options-list">
<div
v-for="option in currentQuestion.options"
:key="option.label"
class="option-item"
:class="{
'correct-option': showAnswer && currentQuestion.answer.includes(option.label)
}"
>
<span class="option-label">{{ option.label }}.</span>
<span class="option-text">{{ option.text_cn || '待翻译...' }}</span>
</div>
</div>
</div>
</div>
<div v-if="showAnswer && currentQuestion.answer" class="answer-section">
<el-alert
:title="`正确答案: ${currentQuestion.answer}`"
type="success"
:closable="false"
show-icon
/>
</div>
</div>
<div class="question-actions">
<el-button
@click="handlePrevQuestion"
:disabled="currentQuestionIndex === 0"
>
<el-icon><ArrowLeft /></el-icon>
上一题
</el-button>
<el-button
type="primary"
@click="handleToggleAnswer"
>
{{ showAnswer ? '隐藏答案' : '显示答案' }}
</el-button>
<el-button
@click="handleNextQuestion"
:disabled="currentQuestionIndex === currentTopicQuestions.length - 1"
>
下一题
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
</template>
</el-main>
</el-container>
<Teleport to="body">
<div v-if="pdfDialogVisible" class="pdf-overlay" :class="{ 'is-maximized': isMaximized }">
<div class="pdf-modal">
<div class="pdf-modal-header">
<span class="pdf-modal-title">Topic {{ currentTopic }} - 原文PDF</span>
<div class="pdf-modal-actions">
<el-button
type="primary"
size="small"
circle
@click="toggleMaximize"
:title="isMaximized ? '还原' : '最大化'"
>
<el-icon v-if="isMaximized"><Minus /></el-icon>
<el-icon v-else><FullScreen /></el-icon>
</el-button>
<el-button
type="danger"
size="small"
circle
@click="closePdfDialog"
title="关闭"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
</div>
<div class="pdf-modal-body">
<iframe
:src="currentPdfUrl"
class="pdf-iframe"
frameborder="0"
></iframe>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch, computed } from 'vue'
import { useQuestionStore } from './stores/questions'
import { storeToRefs } from 'pinia'
const store = useQuestionStore()
const {
loading,
topics,
currentTopic,
currentQuestionIndex,
currentQuestion,
currentTopicQuestions,
showAnswer,
topicStats
} = storeToRefs(store)
const jumpQuestionNum = ref(1)
const pdfDialogVisible = ref(false)
const isMaximized = ref(false)
const currentPdfUrl = computed(() => {
const topicNum = String(currentTopic.value).padStart(2, '0')
return `/pdfs/topic_${topicNum}.pdf`
})
watch(currentQuestionIndex, (newIndex) => {
jumpQuestionNum.value = newIndex + 1
})
onMounted(() => {
store.loadQuestions()
})
function handleTopicSelect(index: string) {
store.setTopic(Number(index))
jumpQuestionNum.value = 1
}
function handlePrevQuestion() {
store.prevQuestion()
}
function handleNextQuestion() {
store.nextQuestion()
}
function handleToggleAnswer() {
store.toggleAnswer()
}
function handleJumpQuestion() {
const targetIndex = jumpQuestionNum.value - 1
if (targetIndex >= 0 && targetIndex < currentTopicQuestions.value.length) {
store.jumpToQuestion(targetIndex)
}
}
function handleOpenPdf() {
pdfDialogVisible.value = true
}
function toggleMaximize() {
isMaximized.value = !isMaximized.value
}
function closePdfDialog() {
pdfDialogVisible.value = false
isMaximized.value = false
}
</script>
<style scoped>
.app-container {
height: 100vh;
background-color: #f5f7fa;
}
.el-container {
height: 100%;
}
.sidebar {
background-color: #fff;
border-right: 1px solid #e4e7ed;
overflow-y: auto;
}
.logo {
padding: 20px;
text-align: center;
border-bottom: 1px solid #e4e7ed;
}
.logo h2 {
margin: 0;
color: #409eff;
font-size: 18px;
}
.topic-menu {
border-right: none;
}
.topic-badge {
margin-left: auto;
}
.main-content {
padding: 20px;
overflow-y: auto;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e4e7ed;
flex-wrap: wrap;
gap: 10px;
}
.question-header h3 {
margin: 0;
color: #303133;
}
.header-right {
display: flex;
align-items: center;
gap: 20px;
}
.jump-control {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #606266;
}
.jump-control .el-input-number {
width: 80px;
}
.question-progress {
color: #909399;
font-size: 14px;
}
.question-content {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.bilingual-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.language-panel {
padding: 16px;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.english-panel {
background-color: #fafafa;
}
.chinese-panel {
background-color: #f0f9eb;
}
.panel-header {
margin-bottom: 16px;
}
.stem-text {
font-size: 15px;
line-height: 1.8;
color: #303133;
margin-bottom: 20px;
padding: 12px;
background-color: #fff;
border-radius: 4px;
}
.options-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.option-item {
display: flex;
padding: 12px 16px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #dcdfe6;
transition: all 0.3s;
}
.option-item:hover {
border-color: #409eff;
background-color: #ecf5ff;
}
.option-item.correct-option {
border-color: #67c23a;
background-color: #f0f9eb;
}
.option-label {
font-weight: bold;
color: #409eff;
margin-right: 8px;
min-width: 24px;
}
.option-text {
color: #303133;
line-height: 1.6;
}
.answer-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
}
.question-actions {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 24px;
}
.pdf-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.pdf-modal {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
width: 90%;
height: 90vh;
transition: all 0.3s ease;
}
.pdf-overlay.is-maximized .pdf-modal {
width: 100%;
height: 100vh;
border-radius: 0;
}
.pdf-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
border-radius: 8px 8px 0 0;
flex-shrink: 0;
}
.pdf-overlay.is-maximized .pdf-modal-header {
border-radius: 0;
}
.pdf-modal-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.pdf-modal-actions {
display: flex;
gap: 8px;
}
.pdf-modal-body {
flex: 1;
overflow: hidden;
}
.pdf-iframe {
width: 100%;
height: 100%;
border: none;
}
@media (max-width: 1200px) {
.bilingual-container {
grid-template-columns: 1fr;
}
.question-header {
flex-direction: column;
align-items: flex-start;
}
.header-right {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,93 @@
<script setup lang="ts">
import { ref } from 'vue'
import viteLogo from '../assets/vite.svg'
import heroImg from '../assets/hero.png'
import vueLogo from '../assets/vue.svg'
const count = ref(0)
</script>
<template>
<section id="center">
<div class="hero">
<img :src="heroImg" class="base" width="170" height="179" alt="" />
<img :src="vueLogo" class="framework" alt="Vue logo" />
<img :src="viteLogo" class="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
</div>
<button class="counter" @click="count++">Count is {{ count }}</button>
</section>
<div class="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img class="logo" :src="viteLogo" alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://vuejs.org/" target="_blank">
<img class="button-icon" :src="vueLogo" alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div class="ticks"></div>
<section id="spacer"></section>
</template>

18
exam-viewer/src/main.ts Normal file
View File

@@ -0,0 +1,18 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import './style.css'
const app = createApp(App)
const pinia = createPinia()
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(pinia)
app.use(ElementPlus)
app.mount('#app')

View File

@@ -0,0 +1,111 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface QuestionOption {
label: string
text: string
text_cn?: string
}
export interface Question {
topic: number
question_num: number
stem: string
stem_cn?: string
options: QuestionOption[]
answer: string
}
export const useQuestionStore = defineStore('questions', () => {
const questions = ref<Question[]>([])
const currentTopic = ref<number>(1)
const currentQuestionIndex = ref<number>(0)
const showAnswer = ref<boolean>(false)
const loading = ref<boolean>(true)
const topics = computed(() => {
const topicSet = new Set(questions.value.map(q => q.topic))
return Array.from(topicSet).sort((a, b) => a - b)
})
const currentTopicQuestions = computed(() => {
return questions.value.filter(q => q.topic === currentTopic.value)
})
const currentQuestion = computed(() => {
return currentTopicQuestions.value[currentQuestionIndex.value] || null
})
const topicStats = computed(() => {
const stats: Record<number, number> = {}
questions.value.forEach(q => {
stats[q.topic] = (stats[q.topic] || 0) + 1
})
return stats
})
async function loadQuestions() {
try {
loading.value = true
let response = await fetch('/questions_translated.json')
if (!response.ok) {
response = await fetch('/questions.json')
}
const data = await response.json()
questions.value = data
} catch (error) {
console.error('Failed to load questions:', error)
} finally {
loading.value = false
}
}
function setTopic(topic: number) {
currentTopic.value = topic
currentQuestionIndex.value = 0
showAnswer.value = false
}
function nextQuestion() {
if (currentQuestionIndex.value < currentTopicQuestions.value.length - 1) {
currentQuestionIndex.value++
showAnswer.value = false
}
}
function prevQuestion() {
if (currentQuestionIndex.value > 0) {
currentQuestionIndex.value--
showAnswer.value = false
}
}
function jumpToQuestion(index: number) {
if (index >= 0 && index < currentTopicQuestions.value.length) {
currentQuestionIndex.value = index
showAnswer.value = false
}
}
function toggleAnswer() {
showAnswer.value = !showAnswer.value
}
return {
questions,
currentTopic,
currentQuestionIndex,
showAnswer,
loading,
topics,
currentTopicQuestions,
currentQuestion,
topicStats,
loadQuestions,
setTopic,
nextQuestion,
prevQuestion,
jumpToQuestion,
toggleAnswer
}
})

34
exam-viewer/src/style.css Normal file
View File

@@ -0,0 +1,34 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
height: 100%;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}

View File

@@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})