diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss index 8e559e7..15b8255 100644 --- a/src/assets/styles/index.scss +++ b/src/assets/styles/index.scss @@ -496,13 +496,13 @@ aside { // color: #eef5ff !important; // } -/* === 学生端分页:墨黑极简(覆盖上方裸深色 !important 规则) === */ +/* === 学生端分页:科技蓝(与学生端亮色主题一致,覆盖上方裸深色 !important 规则) === */ #app .studentLayout .pagination-container .el-pagination, #app .studentLayout .el-pagination { --el-pagination-bg-color: transparent !important; --el-pagination-button-bg-color: #ffffff !important; - --el-pagination-hover-color: #1f2937 !important; - --el-pagination-button-disabled-bg-color: #ffffff !important; + --el-pagination-hover-color: #2563eb !important; + --el-pagination-button-disabled-bg-color: #f9fafb !important; --el-pagination-button-disabled-color: #9ca3af !important; color: #4b5563 !important; } @@ -517,21 +517,30 @@ aside { border-radius: 8px !important; background-color: #ffffff !important; color: #374151 !important; - font-weight: 600; + font-weight: 500; } +/* 悬浮:软蓝浅底 + 蓝字蓝边 */ #app .studentLayout .pagination-container .el-pagination .el-pager li:hover, #app .studentLayout .el-pager li:hover, -#app .studentLayout .el-pagination button:hover { - border-color: #1f2937 !important; - background-color: #f9fafb !important; - color: #1f2937 !important; +#app .studentLayout .el-pagination button:hover:not(:disabled) { + border-color: #2563eb !important; + background-color: #eff6ff !important; + color: #2563eb !important; } +/* 当前页:实心教育蓝(与学生端主按钮同色系) */ #app .studentLayout .pagination-container .el-pagination .el-pager li.is-active, #app .studentLayout .el-pager li.is-active { - border-color: #1f2937 !important; - background: #1f2937 !important; + border-color: #2563eb !important; + background: #2563eb !important; + color: #ffffff !important; +} + +#app .studentLayout .pagination-container .el-pagination .el-pager li.is-active:hover, +#app .studentLayout .el-pager li.is-active:hover { + border-color: #1d4ed8 !important; + background: #1d4ed8 !important; color: #ffffff !important; } @@ -539,8 +548,8 @@ aside { #app .studentLayout .pagination-container .el-pagination .btn-next:disabled, #app .studentLayout .el-pagination .btn-prev:disabled, #app .studentLayout .el-pagination .btn-next:disabled { - background-color: #ffffff !important; - color: #9ca3af !important; + background-color: #f9fafb !important; + color: #b0c0d0 !important; border-color: #e5e7eb !important; } @@ -550,14 +559,23 @@ aside { font-weight: 400; } -#app .studentLayout .el-pagination .el-select .el-input__wrapper { +/* 每页条数下拉 + 跳页输入框:白底灰边,hover/focus 教育蓝 */ +#app .studentLayout .el-pagination .el-input__wrapper { background: #ffffff !important; border: 1px solid #e5e7eb !important; border-radius: 8px !important; box-shadow: none !important; } -#app .studentLayout .el-pagination .el-select .el-input__inner { +#app .studentLayout .el-pagination .el-input__wrapper:hover { + border-color: #3b82f6 !important; +} + +#app .studentLayout .el-pagination .el-input__wrapper.is-focus { + border-color: #2563eb !important; +} + +#app .studentLayout .el-pagination .el-input__inner { color: #374151 !important; } 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/channel/purchase.vue b/src/views/channel/purchase.vue index 6f31edb..2f789fd 100644 --- a/src/views/channel/purchase.vue +++ b/src/views/channel/purchase.vue @@ -270,7 +270,7 @@ const updateChart = () => { .map((item) => { if (item.value) { console.log(item); - + const [marketImportance, itemImportance] = parseCoordinates(item.basedOn, item.categoryType); console.log(item); return { diff --git a/src/views/channel/supplier.vue b/src/views/channel/supplier.vue index 235b5b4..9733b8d 100644 --- a/src/views/channel/supplier.vue +++ b/src/views/channel/supplier.vue @@ -94,26 +94,37 @@

Step 02

为供应渠道评分并比较结果

-

每项评分为 0–10 分,渠道名称和评分均由学生填写;系统会按第一步已保存的权重自动计算加权总分。

+

每项评分为 0–10 分,渠道名称与评分由学生填写;系统按第一步已保存的试产/量产权重分别计算两个加权总分。

-
+
-
{{ section.label }}

{{ section.title }}

- 加权总分 = Σ(维度评分 × 权重) +
渠道评分

供应渠道评分与加权对比

+ 试产加权总分 = Σ(评分 × 试产权重) · 量产加权总分 = Σ(评分 × 量产权重)
- +
- + + + + + + + + + + + - - - + + - + +
渠道{{ dimension.name }}加权总分
渠道维度评分(0–10 分)试产加权总分量产加权总分
{{ dimension.name }}Σ × 试产权重Σ × 量产权重
+
{{ weightedScore(row, section.weights) }}{{ weightedScore(row, 'smallBatch') }}{{ weightedScore(row, 'largeBatch') }}
@@ -268,15 +279,18 @@ function normalizeScores(source = {}) { }, {}); } -function createScoreRows(savedRows = []) { - const source = Array.isArray(savedRows) ? savedRows : []; +function createChannelRows(smallSaved = [], largeSaved = []) { + const small = Array.isArray(smallSaved) ? smallSaved : []; + const large = Array.isArray(largeSaved) ? largeSaved : []; return Array.from({ length: 5 }, (_, index) => { - const saved = source[index] || {}; + const s = small[index] || {}; + const l = large[index] || {}; return { id: `${Date.now()}-${index}-${Math.random().toString(16).slice(2)}`, - channel: String(saved.channel || ""), - scores: normalizeScores(saved.scores), - weightedTotal: saved.weightedTotal ?? null, + channel: String(s.channel || l.channel || ""), + scores: normalizeScores(s.scores ?? l.scores), + smallWeightedTotal: s.weightedTotal ?? null, + largeWeightedTotal: l.weightedTotal ?? null, }; }); } @@ -302,8 +316,7 @@ function createRiskRows(savedRows = []) { })); } -const smallBatchRows = ref(createScoreRows()); -const largeBatchRows = ref(createScoreRows()); +const channelRows = ref(createChannelRows()); const scenarioAnalyses = ref(createScenarioAnalyses()); const procurementRecommendation = ref(""); const riskRows = ref(createRiskRows()); @@ -312,16 +325,12 @@ const smallWeightTotal = computed(() => rows.value.reduce((sum, row) => sum + Nu const largeWeightTotal = computed(() => rows.value.reduce((sum, row) => sum + Number(row.largeBatchWeight || 0), 0)); const smallWeights = computed(() => rows.value.reduce((result, row) => ({ ...result, [row.key]: Number(row.smallBatchWeight || 0) }), {})); const largeWeights = computed(() => rows.value.reduce((result, row) => ({ ...result, [row.key]: Number(row.largeBatchWeight || 0) }), {})); -const scoreSections = computed(() => [ - { key: "small", label: "场景 A", title: "小批量试产(100台)渠道评分", rows: smallBatchRows.value, weights: smallWeights.value }, - { key: "large", label: "场景 B", title: "大规模量产(10000台)渠道评分", rows: largeBatchRows.value, weights: largeWeights.value }, -]); function scoreFor(row, key) { if (!Object.prototype.hasOwnProperty.call(row.scores, key)) row.scores[key] = 0; return { get score() { return row.scores[key]; }, - set score(value) { row.scores[key] = value; row.weightedTotal = null; }, + set score(value) { row.scores[key] = value; row.smallWeightedTotal = null; row.largeWeightedTotal = null; }, }; } @@ -330,18 +339,18 @@ function toNumber(value) { return Number.isFinite(numeric) ? numeric : 0; } -function weightedScore(row, weights) { +function weightedScore(row, scenario) { + const weights = scenario === "smallBatch" ? smallWeights.value : largeWeights.value; const score = rows.value.reduce((sum, item) => sum + toNumber(row.scores[item.key]) * toNumber(weights[item.key]) / 100, 0); return score.toFixed(2); } function channelRanking(scenario) { - const source = scenario === "smallBatch" ? smallBatchRows.value : largeBatchRows.value; - const weights = scenario === "smallBatch" ? smallWeights.value : largeWeights.value; - return source + const storedKey = scenario === "smallBatch" ? "smallWeightedTotal" : "largeWeightedTotal"; + return channelRows.value .map((row) => { - const stored = Number(row.weightedTotal); - return { channel: String(row.channel || "").trim(), score: Number.isFinite(stored) ? stored : Number(weightedScore(row, weights)) }; + const stored = Number(row[storedKey]); + return { channel: String(row.channel || "").trim(), score: Number.isFinite(stored) ? stored : Number(weightedScore(row, scenario)) }; }) .filter((item) => item.channel) .sort((left, right) => right.score - left.score || left.channel.localeCompare(right.channel, "zh-CN")); @@ -356,12 +365,6 @@ function formatScore(value) { return Number.isFinite(score) ? score.toFixed(2) : "--"; } -function syncChannelNames() { - largeBatchRows.value.forEach((row, index) => { - row.channel = String(smallBatchRows.value[index]?.channel || "").trim(); - }); -} - function buildStepOnePayload() { return { items: rows.value.map((row) => ({ @@ -373,16 +376,17 @@ function buildStepOnePayload() { } function buildStepTwoPayload() { - const serializeChannels = (source) => source.map((row) => ({ + const serializeChannels = (storedKey) => channelRows.value.map((row) => ({ channel: String(row.channel || "").trim(), scores: rows.value.map((dimension) => ({ dimension: dimension.name, score: toNumber(row.scores[dimension.key]), })), + weightedTotal: row[storedKey], })); return { - smallBatchChannels: serializeChannels(smallBatchRows.value), - largeBatchChannels: serializeChannels(largeBatchRows.value), + smallBatchChannels: serializeChannels("smallWeightedTotal"), + largeBatchChannels: serializeChannels("largeWeightedTotal"), }; } @@ -421,9 +425,7 @@ function applyStepOne(answer) { function applyStepTwo(answer) { const result = parseJson(answer?.step2Answer); if (!Array.isArray(result?.smallBatchChannels) || !Array.isArray(result?.largeBatchChannels)) return false; - smallBatchRows.value = createScoreRows(result.smallBatchChannels); - largeBatchRows.value = createScoreRows(result.largeBatchChannels); - syncChannelNames(); + channelRows.value = createChannelRows(result.smallBatchChannels, result.largeBatchChannels); return true; } @@ -451,14 +453,11 @@ async function saveStepOne() { async function saveStepTwo() { try { - syncChannelNames(); const result = (await validateSupplierEvaluationStepTwo(buildStepTwoPayload()))?.data || {}; if (!result?.valid) throw new Error(result?.message || "渠道评分校验未通过"); - smallBatchRows.value = createScoreRows(result.smallBatchChannels); - largeBatchRows.value = createScoreRows(result.largeBatchChannels); + channelRows.value = createChannelRows(result.smallBatchChannels, result.largeBatchChannels); await saveStudentTrainingAnswer(TASK_KEY, { currentStep: 2, saveAction: "SAVE", progressStatus: "IN_PROGRESS", step2Answer: JSON.stringify({ weights: result.weights, smallBatchChannels: result.smallBatchChannels, largeBatchChannels: result.largeBatchChannels }) }); stepTwoSaved.value = true; - syncChannelNames(); ElMessage.success("渠道评分已保存,可进入第 3 步形成采购建议"); } catch (error) { ElMessage.error(error?.message || "保存失败,请检查渠道和评分"); @@ -878,6 +877,35 @@ onMounted(async () => { .score-table th:first-child, .score-table td:first-child { width: 175px; } .score-table :deep(.el-input-number) { width: 100%; } +/* 合并评分表:双列组布局 */ +.merged-score-table { min-width: 1080px; table-layout: fixed; } +.merged-score-table .col-channel { width: 150px; } +.merged-score-table .col-dimension { width: auto; } +.merged-score-table .col-total { width: 110px; } + +/* 顶层分组表头:维度组中性底,总分组着色 */ +.merged-score-table .group-header th { + text-align: center; + font-size: var(--student-fs-body-sm); +} +.merged-score-table .group-dimensions { + background: #fafbfc; + color: var(--student-text); +} +.merged-score-table .group-total { color: #ffffff; font-weight: 800; } +.merged-score-table .group-total-small { background: var(--student-accent); } +.merged-score-table .group-total-large { background: var(--student-accent-deep); } + +/* 第二行子表头 */ +.merged-score-table .col-dimension, +.merged-score-table .col-total { background: #fafbfc; } +.merged-score-table .col-total-small { color: var(--student-accent); } +.merged-score-table .col-total-large { color: var(--student-accent-deep); } + +/* 列组竖向分隔线 */ +.merged-score-table .col-channel, +.merged-score-table .col-total-small { border-right: 2px solid var(--student-panel-border); } + .total-score { color: var(--student-accent) !important; font-size: var(--student-fs-data); 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 @@ + + + + + + diff --git a/src/views/studentDemo/index.vue b/src/views/studentDemo/index.vue index cb09e39..d908a8c 100644 --- a/src/views/studentDemo/index.vue +++ b/src/views/studentDemo/index.vue @@ -7,6 +7,23 @@ import { exchangeStudentDemoSession } from "@/api/studentDemo"; import { saveStudentDemoSession } from "@/utils/studentDemo"; import useUserStore from "@/store/modules/user"; +// 学生端演示登录态的角色:固定为学生 (roleId === 4),避免后端返回 +// 教师身份导致跳转到教师首页。后端若返回 demoTeachingClassId,会被 +// 透传保留,登录页/Navbar/AppMain 可据此识别为"演示学生"。 +const STUDENT_DEMO_ROLE_ID = "4"; + +function unwrapDemoPayload(payload) { + if (!payload || typeof payload !== "object") { + return { userInfo: { roleId: STUDENT_DEMO_ROLE_ID }, token: "" }; + } + // 兼容两种返回结构: + // 1) { token, roleId, userId, ... } —— 扁平结构 + // 2) { token, userInfo: { roleId, ... } } —— userInfo 嵌套 + const inner = payload.userInfo && typeof payload.userInfo === "object" ? payload.userInfo : payload; + const token = inner.token || payload.token || ""; + return { userInfo: { ...inner, token, roleId: STUDENT_DEMO_ROLE_ID }, token }; +} + const userStore = useUserStore(); const message = ref("正在连接教师演示会话…"); let readyTimer; @@ -15,15 +32,36 @@ function notifyReady() { if (window.opener) window.opener.postMessage({ type: "STUDENT_DEMO_READY" }, window.location.origin); } +function applyDemoSession(demoPayload) { + const { userInfo, token } = unwrapDemoPayload(demoPayload); + // 保存到 sessionStorage(getUserInfo/getToken 优先从这里读取) + // 注意:演示 session 只放在新窗口的 sessionStorage,避免覆盖 + // 教师主窗口的 cookie/sessionStorage 而把教师侧误切到学生身份。 + saveStudentDemoSession(userInfo); + // 立即更新 Pinia,避免 reload 之前出现教师内容闪烁 + userStore.token = token; + userStore.userInfo = userInfo; +} + function receiveTicket(event) { if (event.origin !== window.location.origin || event.source !== window.opener || event.data?.type !== "STUDENT_DEMO_TICKET") return; exchangeStudentDemoSession(event.data.ticket).then((res) => { - saveStudentDemoSession(res.data); - userStore.token = res.data.token; - userStore.userInfo = res.data; + if (!res || !res.data) { + message.value = "演示服务返回数据为空,请稍后重试"; + return; + } + applyDemoSession(res.data); window.clearInterval(readyTimer); window.removeEventListener("message", receiveTicket); - window.location.hash = "#/index"; + // 跳转学生端基础课首页 #/foundation/new-product-survey: + // roleId 已被改写为 4,filterDynamicRoutes 会加载 student 路由, + // 避开 /index 的角色分发逻辑(role==='3' 会落到 ), + // 强制落到学生工作台,避免任何教师侧兜底。 + const targetHash = "#/foundation/new-product-survey"; + if (window.location.hash !== targetHash) { + // 先改 hash 再 reload,避免仅 hash 变化时浏览器不重新加载 + window.location.hash = targetHash; + } window.location.reload(); }).catch(() => { message.value = "演示链接已失效,请返回教师端重新打开。"; }); } diff --git a/src/views/teacherEnd/exam/editor.vue b/src/views/teacherEnd/exam/editor.vue index fc05062..577e50d 100644 --- a/src/views/teacherEnd/exam/editor.vue +++ b/src/views/teacherEnd/exam/editor.vue @@ -26,24 +26,12 @@
- + - @@ -130,10 +118,6 @@
{{ idx + 1 }} {{ typeOptions.find(t => t.value === question.type)?.label || question.type }} - - - 分 -

{{ question.stem }}

@@ -240,12 +224,11 @@ function isSelected(questionId) { return form.questions.some((item) => item.ques function selectedScore(questionId) { return form.questions.find((item) => item.questionId === questionId)?.score || 1; } function toggleQuestion(question, checked) { if (checked) form.questions.push({ questionId: question.id, score: typeScores[question.type] || 1 }); else form.questions = form.questions.filter((item) => item.questionId !== question.id); } function removeQuestionFromPreview(question) { toggleQuestion(question, false); } -function setScore(questionId, score) { const item = form.questions.find((row) => row.questionId === questionId); if (item) item.score = Number(score); } + function selectedByType(type) { const ids = new Set(bank.value.filter((item) => item.type === type).map((item) => item.id)); return form.questions.filter((item) => ids.has(item.questionId)).length; } 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 scorePerQuestion(type) { return Number(typeScores[type] || 1); } -function setTypeScore(type, score) { const value = Number(score); if (!Number.isFinite(value) || value < 0.5) return; typeScores[type] = value; const ids = new Set(bank.value.filter((item) => item.type === type).map((item) => item.id)); for (const item of form.questions) if (ids.has(item.questionId)) item.score = value; } 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"); } @@ -397,8 +380,6 @@ function toBoolean(value, fallback) { if (value === undefined || value === null tbody tr:hover td { background: #fafcff; } } - .type-score-input { width: 112px; } - .fixed-step-actions { display: flex; align-items: center; @@ -690,32 +671,6 @@ function toBoolean(value, fallback) { if (value === undefined || value === null font-weight: 500; } - .q-score-chip { - display: inline-flex; - align-items: center; - gap: var(--sp-1); - margin-left: auto; - padding: 2px var(--sp-3); - border-radius: var(--r-control); - background: #ffffff; - border: 1px solid var(--edu-primary); - font-size: var(--fs-tag); - color: var(--edu-primary-deep); - - span { color: var(--edu-primary-deep); font-weight: 500; } - - :deep(.el-input-number) { width: 88px; } - :deep(.el-input-number .el-input__wrapper) { - padding: 2px 6px; - box-shadow: none !important; - background: transparent; - } - :deep(.el-input-number__decrease), - :deep(.el-input-number__increase) { - display: none; - } - } - .q-card-stem { margin: 0 0 var(--sp-2); color: var(--text-strong); diff --git a/src/views/teacherEnd/student/index.vue b/src/views/teacherEnd/student/index.vue index 0f921b3..bff1c41 100644 --- a/src/views/teacherEnd/student/index.vue +++ b/src/views/teacherEnd/student/index.vue @@ -486,7 +486,7 @@ const beforeImportStudents = (file) => { return true } const getClassList = () => { - indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId }).then((res) => { + return indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId }).then((res) => { classList.value = res.data }) } @@ -542,11 +542,15 @@ const initPassword = (row, status) => { } onMounted(async () => { await userStore.ensureSchoolProductConfig().catch(() => null) - getList() - getClassList() if (!isTeacherRosterMode.value) { getFacultyList() } + await getClassList() + // 默认展示第一个班级数据:把 select 绑到第一个教学班并立即拉取学生列表 + if (!paramsquery.value.schoolClassId && studentClassOptions.value.length) { + paramsquery.value.schoolClassId = studentClassOptions.value[0].schoolClassId + } + getList() })
题型题库题目数量选题数量每题分值小计
题型题库题目数量选题数量小计
{{ item.label }} {{ questionCount(item.value) }} {{ selectedByType(item.value) }} - - {{ scoreByType(item.value) }}