auto-imports.d.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
components.d.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/api/modules/personalCenter.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components/NormalHeader/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/router/index.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/exam/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/home/components/user-panel/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/personal-center/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
vite.config.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
auto-imports.d.ts
@@ -5,5 +5,5 @@ // Generated by unplugin-auto-import export {} declare global { const ElMessage: typeof import('element-plus/es')['ElMessage'] } components.d.ts
@@ -7,12 +7,14 @@ /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCol: typeof import('element-plus/es')['ElCol'] ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCountdown: typeof import('element-plus/es')['ElCountdown'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] @@ -32,6 +34,8 @@ ElSlider: typeof import('element-plus/es')['ElSlider'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] ExamAudio: typeof import('./src/components/ExamAudio/index.vue')['default'] ExamInfo: typeof import('./src/components/ExamInfo/index.vue')['default'] src/api/modules/personalCenter.js
New file @@ -0,0 +1,9 @@ import service from "@/api"; export const uploadImg = (fromData) => { return service.post("/api/student/upload/image", fromData, { headers: { "Content-Type": "multipart/form-data", }, }); }; src/components/NormalHeader/index.vue
@@ -9,8 +9,8 @@ <div class="user-container flex items-center"> <div class="avatar-container w-12 h-12 rounded-full overflow-hidden mr-3"> <div class="avatar-content" v-if="userInfo.imagePath""> <img :src="userInfo.imagePath" class="avatar-img" alt=""> <div class="avatar-content" v-if="userInfo.imagePath"> <img :src="'/api/files/'+userInfo.imagePath" class="avatar-img" alt=""> </div> <div class="avatar-content" :style="{ backgroundColor: getColor }"> <div class="name text-xl font-bold text-white">{{ getFirstName }}</div> src/router/index.js
@@ -66,6 +66,11 @@ path: '/folder', component: () => import('@/views/folder/index.vue'), }, //个人中心 { path: '/personal-center', component: () => import('@/views/personal-center/index.vue'), } ]; const router = createRouter({ src/views/exam/index.vue
@@ -1,5 +1,7 @@ <template> <div class="exam-container w-screen h-screen bg-slate-50 relative overflow-hidden"> <div class="exam-container w-screen h-screen bg-slate-50 relative overflow-hidden" > <div class="top-bg bg-blue-500"></div> <div class="exam-content"> <div class="exam-wrapper container mx-auto h-full flex flex-col"> @@ -24,8 +26,10 @@ <!-- 答题卡区 --> <div class="answer-wrapper answer-left mr-8 shadow-xl p-4 box-border"> <div class="wrapper h-full flex flex-col items-center"> <div class="title-wrapper w-full flex justify-between items-center mb-5"> <div class="title text-xl font-semibold ">答题卡</div> <div class="title-wrapper w-full flex justify-between items-center mb-5" > <div class="title text-xl font-semibold">答题卡</div> <AnswerTag></AnswerTag> </div> @@ -40,9 +44,13 @@ </div> <div class="submit-wrapper"> <el-button type="primary" class="submit-button" @click="submitExamHandle">提交试卷</el-button> <el-button type="primary" class="submit-button" @click="submitExamHandle" >提交试卷</el-button > </div> </div> </div> @@ -50,15 +58,20 @@ <div class="answer-wrapper answer-right grow shadow-xl p-4"> <div class="wrapper h-full flex flex-col"> <div class="title-wrapper w-full flex mb-5"> <div class="title text-xl font-semibold ">{{ examType[currentType] }} ({{ examStore.getActiveQuestion.questionScore }}分) <div class="title text-xl font-semibold"> {{ examType[currentType] }} ({{ examStore.getActiveQuestion.questionScore }}分) </div> </div> <div class="main-wrapper w-full grow relative my-5"> <div class="main-content absolute top-0 bottom-0 w-full"> <Transition appear name="fade-transform" mode="out-in"> <component :is="typeComponent[currentType]" :key="currentIndex"></component> <component :is="typeComponent[currentType]" :key="currentIndex" ></component> </Transition> </div> </div> @@ -66,10 +79,17 @@ <div class="tool-wrapper flex justify-end"> <div class="button-container flex items-center"> <div class="button-item"> <el-button class="tool-button" @click="prevQuestion">上一题</el-button> <el-button class="tool-button" @click="prevQuestion" >上一题</el-button > </div> <div class="button-item"> <el-button class="tool-button" type="primary" @click="nextQuestion">下一题</el-button> <el-button class="tool-button" type="primary" @click="nextQuestion" >下一题</el-button > </div> </div> </div> @@ -78,7 +98,6 @@ </div> </div> </div> <!-- 退出考试提示弹窗 --> <el-dialog v-model="quitDialog" title="注意" width="500"> @@ -89,9 +108,7 @@ <template #footer> <div class="dialog-footer"> <el-button @click="quitDialog = false">继续作答</el-button> <el-button type="danger" @click="confirmQuit"> 确定退出 </el-button> <el-button type="danger" @click="confirmQuit"> 确定退出 </el-button> </div> </template> </el-dialog> @@ -113,45 +130,50 @@ </el-dialog> <!-- 考试时间弹窗 --> <el-dialog v-model="timeDialog" align-center width="500" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false"> <el-dialog v-model="timeDialog" align-center width="500" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" > <div class="dialog-container flex flex-col items-center"> <div class="icon-container"> <el-icon :size="50" color="#3680fa"> <Timer /> </el-icon> </div> <div class="dialog-info"> 考试结束,系统自动收卷中....... </div> <div class="dialog-info">考试结束,系统自动收卷中.......</div> </div> </el-dialog> </div> </template> <script setup> import { ref, watchEffect, watch, onMounted } from 'vue'; import { storeToRefs } from 'pinia'; import { Close, Timer } from '@element-plus/icons-vue'; import AnswerTag from './components/answer-tag/index.vue'; import AnswerProgress from './components/answer-progress/index.vue'; import AnswerSheet from './components/answer-sheet/index.vue'; import AnswerTime from './components/answer-time/index.vue'; import { ref, watchEffect, watch, onMounted ,onUnmounted} from "vue"; import { storeToRefs } from "pinia"; import { Close, Timer } from "@element-plus/icons-vue"; import AnswerTag from "./components/answer-tag/index.vue"; import AnswerProgress from "./components/answer-progress/index.vue"; import AnswerSheet from "./components/answer-sheet/index.vue"; import AnswerTime from "./components/answer-time/index.vue"; import AnswerSingle from './components/answer-main/answer-single/index.vue'; import AnswerMultiple from './components/answer-main/answer-multiple/index.vue'; import AnswerAudio from './components/answer-main/answer-audio/index.vue'; import AnswerFill from './components/answer-main/answer-fill/index.vue'; import AnswerDetermine from './components/answer-main/answer-determine/index.vue'; import AnswerShort from './components/answer-main/answer-short/index.vue'; import AnswerCount from './components/answer-main/answer-count/index.vue'; import AnswerSingle from "./components/answer-main/answer-single/index.vue"; import AnswerMultiple from "./components/answer-main/answer-multiple/index.vue"; import AnswerAudio from "./components/answer-main/answer-audio/index.vue"; import AnswerFill from "./components/answer-main/answer-fill/index.vue"; import AnswerDetermine from "./components/answer-main/answer-determine/index.vue"; import AnswerShort from "./components/answer-main/answer-short/index.vue"; import AnswerCount from "./components/answer-main/answer-count/index.vue"; import { useExamStore, useUserStore } from '@/store/index.js'; import { useRouter } from 'vue-router'; import { useExamStore, useUserStore } from "@/store/index.js"; import { useRouter } from "vue-router"; import { submitExam } from '@/api/modules/exam.js'; import useWebScoket from '@/hooks/useWebScoket.js'; import { submitExam } from "@/api/modules/exam.js"; import useWebScoket from "@/hooks/useWebScoket.js"; import { ElMessage, ElMessageBox } from "element-plus"; const router = useRouter(); @@ -159,7 +181,8 @@ const userStore = useUserStore(); const { userInfo } = storeToRefs(userStore); const { currentType, currentIndex, examDetail, examType, examInfo } = storeToRefs(examStore); const { currentType, currentIndex, examDetail, examType, examInfo } = storeToRefs(examStore); const typeComponent = { 1: AnswerSingle, @@ -181,11 +204,11 @@ // heartBeatData: 'ping' // }); const { status, message, error, connect, disconnect, sendMessage } = useWebScoket({ url: '//192.168.3.64:8000/websocket/' + userInfo.value.id, heartBeatData: 'ping' }); const { status, message, error, connect, disconnect, sendMessage } = useWebScoket({ url: "//192.168.3.64:8000/websocket/" + userInfo.value.id, heartBeatData: "ping", }); // 上一题 const prevQuestion = () => { @@ -220,19 +243,18 @@ currentType.value = typeQuestion.questionType; currentIndex.value = typeQuestion.questionList.length - 1; } } else if (currentIndex.value < 0) { tempIndex--; if (examDetail.value[tempIndex]) { currentType.value = examDetail.value[tempIndex].questionType; currentIndex.value = examDetail.value[tempIndex].questionList.length - 1; currentIndex.value = examDetail.value[tempIndex].questionList.length - 1; } else { currentType.value = typeQuestion.questionType; currentIndex.value = 0; } } } }; // 退出考试 @@ -262,18 +284,20 @@ const timeOut = () => { const temp = { ...examInfo.value, titleList: examDetail.value titleList: examDetail.value, }; timeDialog.value = true; resetAllDialog(); disconnect(); submitExam(temp).then(res => { returnBack(); }).catch(() => { returnBack(); }); submitExam(temp) .then((res) => { returnBack(); }) .catch(() => { returnBack(); }); }; const returnBack = () => { @@ -284,30 +308,55 @@ watchEffect(() => { let progress = 0; examDetail.value.forEach(item => { item.questionList.forEach(question => { if (question.answer || (Array.isArray(question.answerList) && question.answerList.length)) { examDetail.value.forEach((item) => { item.questionList.forEach((question) => { if ( question.answer || (Array.isArray(question.answerList) && question.answerList.length) ) { progress += 1; } }); }); examStore.setProgress(progress); }); const answerTime = ref() watch(() => message.value, (msg) => { if(msg.commend=="delayed"){ answerTime.value.addTime(msg.data.addTimeM) }else if(msg.commend=="forceSubmit"){ confirmSubmit() const answerTime = ref(); watch( () => message.value, (msg) => { if (msg.commend == "delayed") { answerTime.value.addTime(msg.data.addTimeM); } else if (msg.commend == "forceSubmit") { confirmSubmit(); } } }); ); // document.addEventListener('blur', function() { // console.log('页面失去焦点'); // }); const warningNum = ref(0) const handleBlur = () => { if (document.visibilityState === "visible") { } else if (document.visibilityState === "hidden") { ElMessageBox.alert("考试过程中请勿离开考试页面!警告三次将强制收卷", "警告", { confirmButtonText: "确定", }); warningNum.value++ if (warningNum.value==3) { timeOut(); } } }; // -----------------------------------生命周期 onMounted(() => { // 连接webscoket connect(); document.addEventListener("visibilitychange", handleBlur); }); onUnmounted(()=>{ document.removeEventListener("visibilitychange", handleBlur); }) </script> <style lang="scss" scoped> src/views/home/components/user-panel/index.vue
@@ -1,73 +1,62 @@ <template> <div class="user-panel max-w-sm min-w-96 h-fit"> <el-card class="card"> <div class="panel-content flex flex-col items-center"> <!-- 学生信息 --> <el-dropdown> <div class="avatar-container w-40 h-40 rounded-full overflow-hidden el-dropdown-link" @click="stateClick()" > <div class="avatar-content" v-if="userInfo.imagePath" > <img :src="userInfo.imagePath" class="avatar-img" alt="" /> </div> <div class="avatar-content" :style="{ backgroundColor: getColor }" v-else > <div class="name text-5xl font-bold text-white"> {{ getFirstName }} </div> </div> </div> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>修改密码</el-dropdown-item> <el-dropdown-item>个人中心</el-dropdown-item> <el-dropdown-item @click="quit()" >退出登录</el-dropdown-item > </el-dropdown-menu> </template> </el-dropdown> <div class="name-container text-lg font-bold mt-5 mb-2"> {{ userInfo.realName }} </div> <div class="department-container text-base mb-10"> {{ userInfo.userName }} </div> <!-- 学生选项 --> <div class="tool-container grid grid-cols-3 gap-10"> <div class="tool-item text-center cursor-pointer" v-for="item in toolList" @click="toolClick(item)" > <div class="tool-icon mb-1"> <img :src="item.iconPath" class="width-img" alt="" /> </div> <div class="tool-title"> {{ item.title }} </div> </div> </div> <div class="user-panel max-w-sm min-w-96 h-fit"> <el-card class="card"> <div class="panel-content flex flex-col items-center"> <!-- 学生信息 --> <el-dropdown> <div class="avatar-container w-40 h-40 rounded-full overflow-hidden el-dropdown-link" @click="stateClick()" > <div class="avatar-content" v-if="userInfo.imagePath"> <img :src="'/api/files/'+userInfo.imagePath" class="avatar-img" alt="" /> </div> </el-card> </div> <div class="avatar-content" :style="{ backgroundColor: getColor }" v-else > <div class="name text-5xl font-bold text-white"> {{ getFirstName }} </div> </div> </div> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>修改密码</el-dropdown-item> <el-dropdown-item @click="goPersonalCenter()" >个人中心</el-dropdown-item > <el-dropdown-item @click="quit()">退出登录</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> <div class="name-container text-lg font-bold mt-5 mb-2"> {{ userInfo.realName }} </div> <div class="department-container text-base mb-10"> {{ userInfo.userName }} </div> <!-- 学生选项 --> <div class="tool-container grid grid-cols-3 gap-10"> <div class="tool-item text-center cursor-pointer" v-for="item in toolList" @click="toolClick(item)" > <div class="tool-icon mb-1"> <img :src="item.iconPath" class="width-img" alt="" /> </div> <div class="tool-title"> {{ item.title }} </div> </div> </div> </div> </el-card> </div> </template> <script setup> @@ -84,86 +73,89 @@ const router = useRouter(); // 获取列表 const toolList = ref([ { id: 1, title: "在线培训", iconPath: new URL("@/assets/icons/icon1.png", import.meta.url).href, path: "/train", }, { id: 2, title: "我的考试", iconPath: new URL("@/assets/icons/icon2.png", import.meta.url).href, path: "/exam-list", }, { id: 3, title: "我的成绩", iconPath: new URL("@/assets/icons/icon2.png", import.meta.url).href, path: "/grade-list", }, { id: 1, title: "在线培训", iconPath: new URL("@/assets/icons/icon1.png", import.meta.url).href, path: "/train", }, { id: 2, title: "我的考试", iconPath: new URL("@/assets/icons/icon2.png", import.meta.url).href, path: "/exam-list", }, { id: 3, title: "我的成绩", iconPath: new URL("@/assets/icons/icon2.png", import.meta.url).href, path: "/grade-list", }, ]); // 获取颜色 const getColor = computed(() => { return randomColor(); return randomColor(); }); // 获取用户信息 const getFirstName = computed(() => { return userInfo.value.realName && userInfo.value.realName[0]; return userInfo.value.realName && userInfo.value.realName[0]; }); // 点击事件 const toolClick = (item) => { if (item.path) { router.push(item.path); } if (item.path) { router.push(item.path); } }; // 用户选项 const dropdownRef = ref(); // 退出登录 const quit = () => { logout() .then(() => { router.push("/login").then(() => { userStore.setUserInfo(null); localStorage.clear("user"); }); }) .catch((err) => { console.log("退出登录失败,失败原因;", err); }); logout() .then(() => { router.push("/login").then(() => { userStore.setUserInfo(null); localStorage.clear("user"); }); }) .catch((err) => { console.log("退出登录失败,失败原因;", err); }); }; const goPersonalCenter = () => { router.push("/personal-center"); }; </script> <style lang="scss" scoped> .card { border-radius: 30px; border-radius: 30px; } .avatar-content { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; } .avatar-img { width: 100%; height: 100%; object-fit: cover; width: 100%; height: 100%; object-fit: cover; } .avatar-container { cursor: pointer; color: var(--el-color-primary); display: flex; align-items: center; cursor: pointer; color: var(--el-color-primary); display: flex; align-items: center; } .example-showcase .el-dropdown-link { cursor: pointer; color: var(--el-color-primary); display: flex; align-items: center; cursor: pointer; color: var(--el-color-primary); display: flex; align-items: center; } </style> src/views/personal-center/index.vue
New file @@ -0,0 +1,195 @@ <template> <div class="exam-list-container w-screen h-screen bg-slate-50 flex flex-col items-center" > <NormalHeader class="shrink-0"></NormalHeader> <div class="list-container container grow relative"> <div class="personal-center-box list-content absolute top-0 bottom-0 left-0 right-0 py-4" > <div class="information"> <el-card class="h-full" :body-style="{ height: '50%' }"> <div class="card-wrapper w-full h-full flex flex-col px-8 box-border" > <div>个人信息</div> <div class="img-box"> <input type="file" @change="changeHeadPortrait" accept=".jpg, .png" style="display: none" ref="fileHeadPortrait" id="fileHeadPortrait" /> <img class="img" id="headPortrait" @click="uploadImage" :src="'api/files/' + userData.imagePath" /> <span>{{ userData.userName }}</span> </div> <div> <el-form label-width="auto" style="max-width: 600px"> <el-form-item label="姓名"> {{ userData.realName }} </el-form-item> <el-form-item label="班级"> {{ userData.className.join(",") }} </el-form-item> <el-form-item label="注册时间"> {{ timestampToDate(userData.createTime) }} </el-form-item> </el-form> </div> </div> </el-card> </div> <div class="list-wrapper w-full h-full"> <el-card class="h-full" :body-style="{ height: '100%' }"> <el-tabs v-model="activeName" class="demo-tabs"> <el-tab-pane label="资料修改" name="information"> <el-form :model="informationForm" label-width="auto" style="max-width: 600px" > <el-form-item label="真实姓名"> <el-input v-model="informationForm.name" /> </el-form-item> <el-form-item label="年龄"> <el-input v-model="informationForm.age" /> </el-form-item> <el-form-item label="性别"> <el-select v-model="informationForm.sex" style="width: 100px" > <el-option label="男" value="1" /> <el-option label="女" value="2" /> </el-select> </el-form-item> <el-form-item label="出生年月"> <el-date-picker v-model="informationForm.birthDay" type="date" placeholder="Pick a day" :size="size" /> </el-form-item> <el-form-item label="手机"> <el-input v-model="informationForm.phone" /> </el-form-item></el-form ></el-tab-pane> <el-tab-pane label="密码修改" name="password"> <el-form :model="passwordForm" label-width="auto" style="max-width: 600px" > <el-form-item label="旧密码"> <el-input v-model="passwordForm.name" /> </el-form-item> <el-form-item label="新密码"> <el-input v-model="passwordForm.name" /> </el-form-item> <el-form-item label="确认密码"> <el-input v-model="passwordForm.name" /> </el-form-item> </el-form> </el-tab-pane> </el-tabs> </el-card> </div> </div> </div> </div> </template> <script setup> import { ref } from "vue"; import { uploadImg } from "@/api/modules/personalCenter.js"; import { ElMessage } from "element-plus"; const activeName = ref("information"); const userData = ref(JSON.parse(localStorage.getItem("user")).userInfo); const informationForm = ref({ name: userData.value.realName, sex: userData.value.sex, age: userData.value.age, phone: userData.value.phone, birthDay: userData.value.birthDay, }); const passwordForm = ref({ name: "" }); //头像上传 let formData = new FormData(); const uploadImage = () => { let logoFile = document.getElementById("fileHeadPortrait"); if (logoFile) { logoFile.click(); } }; const changeHeadPortrait = (e) => { // let platformLogo = document.getElementById("headPortrait"); if (e.target.files[0]) { // let reader = new FileReader(); // reader.onload = function (e) { // if (platformLogo) { // platformLogo.setAttribute("src", e.target.result); // } // }; // reader.readAsDataURL(e.target.files[0]); formData.set("file", e.target.files[0]); uploadImg(formData).then( () => { ElMessage({ showClose: true, message: "上传成功", type: "success", }); // window.location.reload(); }, (err) => { ElMessage.error(err.data.errorMsg); } ); } }; function timestampToDate(timestamp) { const date = new Date(timestamp); // 将时间戳转换为Date对象 const options = { year: "numeric", month: "long", day: "numeric" }; // 定义日期格式 return new Intl.DateTimeFormat("zh-CN", options).format(date); // 使用Intl.DateTimeFormat进行格式化 } </script> <style lang="scss" scoped> :deep(.el-tabs__nav-wrap:after) { display: none; } .personal-center-box { display: flex; justify-content: space-between; } .information { width: 500px; margin-right: 20px; } .img-box { display: flex; justify-content: center; align-items: center; flex-direction: column; margin-top: 30px; margin-bottom: 30px; } .img { height: 150px; width: 150px; border-radius: 100px; overflow: hidden; object-fit: cover; } </style> vite.config.js
@@ -30,7 +30,7 @@ proxy: { '/api': { // target: 'http://192.168.3.88:8000', target: 'http://42.193.1.25:8000', target: 'http://192.168.3.64:8000', changeOrigin: true, } }