From 181e657db45d68036758a77964512512c782b5c8 Mon Sep 17 00:00:00 2001 From: qinzhenpen Date: Mon, 28 Sep 2026 15:55:40 +0800 Subject: [PATCH] feat(Navbar): add change password option and dialog Introduced a new dropdown item in the Navbar for changing the password, along with a corresponding ChangePasswordDialog component. The dialog is triggered when the user selects the change password option, enhancing user account management. Additionally, updated the command handling to manage the visibility of the password change dialog and added a success handler to prompt users to log out after a successful password change. --- src/layout/components/Navbar.vue | 34 +++ src/views/profile/ChangePasswordDialog.vue | 266 +++++++++++++++++++++ src/views/teacherEnd/exam/editor.vue | 20 +- 3 files changed, 310 insertions(+), 10 deletions(-) create mode 100644 src/views/profile/ChangePasswordDialog.vue diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue index 0de26a8..2d760b9 100644 --- a/src/layout/components/Navbar.vue +++ b/src/layout/components/Navbar.vue @@ -98,6 +98,13 @@ + + + + + + 修改密码 + @@ -112,6 +119,7 @@ + @@ -126,6 +134,7 @@ import SizeSelect from "@/components/SizeSelect"; import HeaderSearch from "@/components/HeaderSearch"; import RuoYiGit from "@/components/RuoYi/Git"; import RuoYiDoc from "@/components/RuoYi/Doc"; +import ChangePasswordDialog from "@/views/profile/ChangePasswordDialog.vue"; import useAppStore from "@/store/modules/app"; import useUserStore from "@/store/modules/user"; import useSettingsStore from "@/store/modules/settings"; @@ -145,6 +154,7 @@ const roleText = computed(() => { }); const studentTopTask = ref(null); +const changePasswordVisible = ref(false); const mentList = ref([ { name: "首页", path: "/index", icon: HomeFilled }, @@ -177,6 +187,9 @@ function toggleSideBar() { function handleCommand(command) { switch (command) { + case "changePassword": + changePasswordVisible.value = true; + break; case "logout": logout(); break; @@ -185,6 +198,27 @@ function handleCommand(command) { } } +function onPasswordChanged() { + // 密码修改成功后,引导用户重新登录,避免旧 token 仍持有访问权限 + setTimeout(() => { + ElMessageBox.confirm( + "密码已更新,建议立即重新登录。是否现在退出?", + "提示", + { + confirmButtonText: "立即退出", + cancelButtonText: "稍后再说", + type: "warning", + } + ) + .then(() => { + userStore.logOut().then(() => { + location.href = "/index"; + }); + }) + .catch(() => {}); + }, 200); +} + function logout() { ElMessageBox.confirm("确定注销并退出系统吗?", "提示", { confirmButtonText: "确定", diff --git a/src/views/profile/ChangePasswordDialog.vue b/src/views/profile/ChangePasswordDialog.vue new file mode 100644 index 0000000..18b49e2 --- /dev/null +++ b/src/views/profile/ChangePasswordDialog.vue @@ -0,0 +1,266 @@ + +
+ + + + 强度:{{ strength.label }} +
+ + + + + + + + + + + + + + diff --git a/src/views/teacherEnd/exam/editor.vue b/src/views/teacherEnd/exam/editor.vue index ef7c800..a0daf03 100644 --- a/src/views/teacherEnd/exam/editor.vue +++ b/src/views/teacherEnd/exam/editor.vue @@ -9,14 +9,14 @@ -
+

基础设置

填写考试名称、时间与班级,分值矩阵自动汇总

@@ -53,7 +53,7 @@
-
+

考试设置

设置试题展示、作答行为和多选题计分方式

@@ -103,7 +103,7 @@
-
+

选择试题

勾选题目并设置该题分值,选择结果实时同步到试卷概览

@@ -148,7 +148,7 @@
-
+

试卷概览

{{ form.questions.length }} 道题,共 {{ totalScore }} 分

导出 Word 试卷
@@ -214,7 +214,7 @@ import { saveAs } from "file-saver"; import * as examApi from "@/api/exam"; const route = useRoute(); const router = useRouter(); const id = computed(() => route.params.id); const isNew = computed(() => id.value === "new"); -const loading = ref(false); const activeStep = ref(1); const activeType = ref("SINGLE"); const status = ref("DRAFT"); const classes = ref([]); const bank = ref([]); const formRef = ref(); const duration = ref(60); +const loading = ref(false); const activeStep = ref(0); const activeType = ref("SINGLE"); const status = ref("DRAFT"); const classes = ref([]); const bank = ref([]); const formRef = ref(); const duration = ref(60); const typeOptions = [{ label: "单选题", value: "SINGLE" }, { label: "多选题", value: "MULTIPLE" }, { label: "判断题", value: "TRUE_FALSE" }, { label: "简答题", value: "SHORT_ANSWER" }]; const statusMeta = { DRAFT: { label: "未发布", tone: "info" }, NOT_STARTED: { label: "未开始", tone: "" }, IN_PROGRESS: { label: "进行中", tone: "success" }, ENDED: { label: "已结束", tone: "warning" } }; const form = reactive({ name: "", startTime: "", endTime: "", classIds: [], questionOrderMode: "FIXED", optionOrderMode: "FIXED", allowCopyQuestion: false, allowPasteAnswer: false, allowRollback: true, multipleScoreMode: "ALL_OR_NOTHING", questions: [] }); @@ -235,8 +235,8 @@ function selectedByType(type) { const ids = new Set(bank.value.filter((item) => function questionCount(type) { return bank.value.filter((item) => item.type === type).length; } function scoreByType(type) { return selectedQuestions(type).reduce((sum, item) => sum + Number(selectedScore(item.id)), 0); } function selectedQuestions(type) { const selected = new Set(form.questions.map((item) => item.questionId)); return bank.value.filter((item) => item.type === type && selected.has(item.id)); } -async function nextStep() { if (activeStep.value === 0) { try { await formRef.value.validate(); const start = new Date(form.startTime.replace(" ", "T")); const end = new Date(form.endTime.replace(" ", "T")); if (end <= start) return ElMessage.warning("结束时间必须晚于开始时间"); duration.value = Math.round((end - start) / 60000); } catch { return; } } if (activeStep.value === 1 && !form.questions.length) return ElMessage.warning("至少选择一道试题"); activeStep.value++; } -async function save(publish) { if (inProgress.value) { await examApi.updateExam(id.value, form); ElMessage.success("结束时间已更新"); return router.push("/exam/index"); } try { await formRef.value?.validate(); } catch { activeStep.value = 0; return; } if (!form.questions.length) { activeStep.value = 1; return ElMessage.warning("至少选择一道试题"); } const result = isNew.value ? await examApi.createExam(form) : await examApi.updateExam(id.value, form); const examId = result.data.id; if (publish) await examApi.publishExam(examId); ElMessage.success(publish ? "考试已保存并发布" : "考试已保存"); router.push("/exam/index"); } +async function nextStep() { if (activeStep.value === 0 && !form.questions.length) return ElMessage.warning("至少选择一道试题"); if (activeStep.value === 2) { try { await formRef.value.validate(); const start = new Date(form.startTime.replace(" ", "T")); const end = new Date(form.endTime.replace(" ", "T")); if (end <= start) return ElMessage.warning("结束时间必须晚于开始时间"); duration.value = Math.round((end - start) / 60000); } catch { return; } } activeStep.value++; } +async function save(publish) { if (inProgress.value) { await examApi.updateExam(id.value, form); ElMessage.success("结束时间已更新"); return router.push("/exam/index"); } try { await formRef.value?.validate(); } catch { activeStep.value = 2; return; } if (!form.questions.length) { activeStep.value = 0; return ElMessage.warning("至少选择一道试题"); } const result = isNew.value ? await examApi.createExam(form) : await examApi.updateExam(id.value, form); const examId = result.data.id; if (publish) await examApi.publishExam(examId); ElMessage.success(publish ? "考试已保存并发布" : "考试已保存"); router.push("/exam/index"); } async function importPaperQuestions({ file }) { const formData = new FormData(); formData.append("file", file); const { data } = await examApi.importQuestions(formData); bank.value = [...data, ...bank.value]; for (const q of data) if (!isSelected(q.id)) form.questions.push({ questionId: q.id, score: 1 }); ElMessage.success(`导入并选中 ${data.length} 道题`); } async function downloadTemplate() { saveAs(await examApi.downloadQuestionTemplate(), "考试题库导入模板.docx"); } async function exportPaper() { saveAs(await examApi.exportExamPaper(id.value), `${form.name}试卷.docx`); }