feat: 新增锁屏、作业模式
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled

This commit is contained in:
shawko 2026-05-10 12:19:29 +08:00
parent 0fd4d0d39c
commit adbfc847ec
4 changed files with 186 additions and 19 deletions

View File

@ -1,6 +1,12 @@
import { post } from '#/api/http'
import { BasicService } from './basic' import { BasicService } from './basic'
class ClassService extends BasicService { class ClassService extends BasicService {
async lockScreen(data: { classId: any; status: 0 | 1 }) {
await post('/school/classScreenLock/lock', data)
}
protected findPageUrl(): string { protected findPageUrl(): string {
return '/org/class/list' return '/org/class/list'
} }

View File

@ -14,6 +14,7 @@ import MjTable, {
mj_form_text, mj_form_text,
} from '#/components/mj/mj-table' } from '#/components/mj/mj-table'
import { pattern_phone } from '#/components/mj/mj-table/fmt' import { pattern_phone } from '#/components/mj/mj-table/fmt'
import hud from '#/utils/hud'
import obj from '#/utils/obj' import obj from '#/utils/obj'
import { grades } from '#/store/basic-data' import { grades } from '#/store/basic-data'
@ -73,6 +74,7 @@ const addFields = [
'contact', 'contact',
'phone', 'phone',
'currencyFlag', 'currencyFlag',
'workMode',
'gradeId', 'gradeId',
'masterTeacherId', 'masterTeacherId',
] ]
@ -148,12 +150,27 @@ const editForm = {
name: 'VxeSwitch', name: 'VxeSwitch',
}, },
}, },
{
field: 'workMode',
title: '作业模式',
span: 12,
itemRender: {
name: 'VxeSwitch',
props: {
openValue: 1,
closeValue: 0,
openLabel: '已开启',
closeLabel: '未开启',
},
},
},
), ),
data: {}, data: {},
}, },
addFields, addFields,
updateFields: ['id', ...addFields], updateFields: ['id', ...addFields],
beforeSaveOrUpdate(formData: any) { beforeSaveOrUpdate(formData: any) {
formData.workMode ??= 0
if (userInfo.value.isOnlyStaff) { if (userInfo.value.isOnlyStaff) {
formData.schoolId = userInfo.value.schoolId formData.schoolId = userInfo.value.schoolId
} }
@ -176,6 +193,7 @@ const editForm = {
if (userInfo.value.gradeId) { if (userInfo.value.gradeId) {
data.gradeId = userInfo.value.gradeId data.gradeId = userInfo.value.gradeId
} }
data.workMode ??= 0
if (edit) { if (edit) {
gridRef.value.hideEditFormItems('schoolName') gridRef.value.hideEditFormItems('schoolName')
return data return data
@ -200,6 +218,13 @@ const searchForm = {
// //
const grid = { const grid = {
columns: obj.joinObjs<any>( columns: obj.joinObjs<any>(
{
title: '操作',
width: 160,
slots: {
default: 'operation',
},
},
{ type: 'seq' }, { type: 'seq' },
{ {
field: 'name', field: 'name',
@ -216,6 +241,12 @@ const grid = {
default: 'currencyFlag', default: 'currencyFlag',
}, },
}, },
{
title: '作业模式',
slots: {
default: 'workMode',
},
},
{ {
field: 'contact', field: 'contact',
title: '联系人', title: '联系人',
@ -249,6 +280,35 @@ function currencyFlagChange(row: any) {
row.currencyFlag = oldFlag row.currencyFlag = oldFlag
}) })
} }
//
function workModeChange(row: any) {
const oldMode = row.workMode === 1 ? 0 : 1
classService
.saveOrUpdate({
id: row.id,
workMode: row.workMode,
})
.catch(() => {
row.workMode = oldMode
})
}
function updateScreenLock(row: any, status: 0 | 1) {
const option = status === 1 ? '锁屏' : '解锁'
hud.confirmLoad(`确定${option}${row.name}】吗?`, {
task: async () => {
await classService.lockScreen({
classId: row.id,
status,
})
gridRef.value?.reload()
},
hideError: true,
option,
hideSuccess: false,
})
}
</script> </script>
<template> <template>
@ -263,12 +323,30 @@ function currencyFlagChange(row: any) {
:service="classService" :service="classService"
v-bind="$attrs" v-bind="$attrs"
> >
<template #operation="{ row }">
<el-button type="danger" @click="updateScreenLock(row, 1)">
锁屏
</el-button>
<el-button type="success" @click="updateScreenLock(row, 0)">
解锁
</el-button>
</template>
<template #currencyFlag="{ row }"> <template #currencyFlag="{ row }">
<el-switch <el-switch
v-model="row.currencyFlag" v-model="row.currencyFlag"
@click="currencyFlagChange(row)" @click="currencyFlagChange(row)"
/> />
</template> </template>
<template #workMode="{ row }">
<vxe-switch
v-model="row.workMode"
:close-value="0"
close-label="未开启"
:open-value="1"
open-label="已开启"
@change="workModeChange(row)"
/>
</template>
</MjTable> </MjTable>
</div> </div>
</template> </template>

View File

@ -212,20 +212,41 @@ function resetZoom() {
// ======================== ======================== // ======================== ========================
const currentIndex = ref(0) const currentIndex = ref(0)
const answerExpanded = ref(false)
const explanationExpanded = ref(false)
const currentQuestion = computed(() => { const currentQuestion = computed(() => {
return paperDetail.value[currentIndex.value] || null return paperDetail.value[currentIndex.value] || null
}) })
const hasAnswer = computed(() => Boolean(currentQuestion.value?.xkwTitle?.answer))
const hasExplanation = computed(() =>
Boolean(currentQuestion.value?.xkwTitle?.explanation),
)
function getTitleKey(item: any) { function getTitleKey(item: any) {
return ( return (
item?.xkwTitle?.titleId ?? item?.titleId ?? item?.xkwTitle?.id ?? item?.id item?.xkwTitle?.titleId ?? item?.titleId ?? item?.xkwTitle?.id ?? item?.id
) )
} }
function collapseAnswerAnalysis() {
answerExpanded.value = false
explanationExpanded.value = false
}
function toggleAnswer() {
answerExpanded.value = !answerExpanded.value
}
function toggleExplanation() {
explanationExpanded.value = !explanationExpanded.value
}
async function enterExplaining() { async function enterExplaining() {
initLeftWidth() initLeftWidth()
resetZoom() resetZoom()
collapseAnswerAnalysis()
phase.value = 'explaining' phase.value = 'explaining'
currentIndex.value = 0 currentIndex.value = 0
await callExplainApi(1) await callExplainApi(1)
@ -250,6 +271,7 @@ async function callExplainApi(status: number, overrideQuestionId?: string) {
function prevQuestion() { function prevQuestion() {
if (currentIndex.value > 0) { if (currentIndex.value > 0) {
currentIndex.value-- currentIndex.value--
collapseAnswerAnalysis()
callExplainApi(1) callExplainApi(1)
} }
} }
@ -257,12 +279,14 @@ function prevQuestion() {
function nextQuestion() { function nextQuestion() {
if (currentIndex.value < paperDetail.value.length - 1) { if (currentIndex.value < paperDetail.value.length - 1) {
currentIndex.value++ currentIndex.value++
collapseAnswerAnalysis()
callExplainApi(1) callExplainApi(1)
} }
} }
function goToQuestion(idx: number) { function goToQuestion(idx: number) {
currentIndex.value = idx currentIndex.value = idx
collapseAnswerAnalysis()
callExplainApi(1) callExplainApi(1)
} }
@ -402,8 +426,8 @@ async function getTitleStudentInfo(titleId: string, markType: number) {
<div class="explain-panel-header"> <div class="explain-panel-header">
<span class="panel-title"> {{ currentIndex + 1 }} </span> <span class="panel-title"> {{ currentIndex + 1 }} </span>
</div> </div>
<div class="explain-panel-body"> <div class="explain-panel-body question-panel-body">
<div class="explain-zoom-layer"> <div class="question-zoom-layer">
<template <template
v-if=" v-if="
Array.isArray(currentQuestion.xkwTitle?.questionList) && Array.isArray(currentQuestion.xkwTitle?.questionList) &&
@ -441,24 +465,34 @@ async function getTitleStudentInfo(titleId: string, markType: number) {
<span class="panel-title">答案与解析</span> <span class="panel-title">答案与解析</span>
</div> </div>
<div class="explain-panel-body"> <div class="explain-panel-body">
<div class="explain-zoom-layer"> <div class="answer-zoom-layer">
<!-- 答案 --> <!-- 答案 -->
<div class="info-section" v-if="currentQuestion.xkwTitle?.answer"> <div class="answer-analysis-section">
<div class="info-label">答案</div> <div class="answer-analysis-header">
<div class="info-content"> <span class="answer-analysis-title">答案</span>
</div>
<div v-if="answerExpanded && hasAnswer" class="info-content">
<latex-text :text="currentQuestion.xkwTitle.answer" /> <latex-text :text="currentQuestion.xkwTitle.answer" />
</div> </div>
<div v-else class="answer-analysis-collapsed">
{{ hasAnswer ? '答案已收起,点击底部“展开答案”查看' : '暂无答案' }}
</div>
</div> </div>
<!-- 解析 --> <!-- 解析 -->
<div class="answer-analysis-section">
<div class="answer-analysis-header">
<span class="answer-analysis-title">解析</span>
</div>
<div <div
class="info-section" v-if="explanationExpanded && hasExplanation"
v-if="currentQuestion.xkwTitle?.explanation" class="info-content"
> >
<div class="info-label">解析</div>
<div class="info-content">
<latex-text :text="currentQuestion.xkwTitle.explanation" /> <latex-text :text="currentQuestion.xkwTitle.explanation" />
</div> </div>
<div v-else class="answer-analysis-collapsed">
{{ hasExplanation ? '解析已收起,点击底部“展开解析”查看' : '暂无解析' }}
</div>
</div> </div>
<!-- 统计数据 --> <!-- 统计数据 -->
@ -531,6 +565,12 @@ async function getTitleStudentInfo(titleId: string, markType: number) {
</div> </div>
</div> </div>
<div class="footer-right"> <div class="footer-right">
<el-button :disabled="!hasAnswer" @click="toggleAnswer">
{{ answerExpanded ? '收起答案' : '展开答案' }}
</el-button>
<el-button :disabled="!hasExplanation" @click="toggleExplanation">
{{ explanationExpanded ? '收起解析' : '展开解析' }}
</el-button>
<el-button @click="resetZoom">重置缩放 {{ zoomPercent }}%</el-button> <el-button @click="resetZoom">重置缩放 {{ zoomPercent }}%</el-button>
<el-button type="danger" @click="endExplaining">结束讲解</el-button> <el-button type="danger" @click="endExplaining">结束讲解</el-button>
</div> </div>
@ -690,6 +730,7 @@ async function getTitleStudentInfo(titleId: string, markType: number) {
.explain-panel-header { .explain-panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
padding: 14px 20px; padding: 14px 20px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
} }
@ -706,12 +747,22 @@ async function getTitleStudentInfo(titleId: string, markType: number) {
overflow: hidden auto; overflow: hidden auto;
} }
.explain-zoom-layer { .question-panel-body {
overflow: auto;
}
.question-zoom-layer {
width: 100%;
transform: scale(var(--explain-zoom-scale));
transform-origin: left top;
}
.answer-zoom-layer {
zoom: var(--explain-zoom-scale); zoom: var(--explain-zoom-scale);
} }
@supports not (zoom: 1) { @supports not (zoom: 1) {
.explain-zoom-layer { .answer-zoom-layer {
width: calc(100% / var(--explain-zoom-scale)); width: calc(100% / var(--explain-zoom-scale));
transform: scale(var(--explain-zoom-scale)); transform: scale(var(--explain-zoom-scale));
transform-origin: left top; transform-origin: left top;
@ -727,6 +778,33 @@ async function getTitleStudentInfo(titleId: string, markType: number) {
border-bottom: 1px dashed #ebeef5; border-bottom: 1px dashed #ebeef5;
} }
.answer-analysis-section {
padding: 12px 0;
border-bottom: 1px dashed #ebeef5;
}
.answer-analysis-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.answer-analysis-title {
font-weight: 600;
color: #246cff;
}
.answer-analysis-collapsed {
padding: 18px 16px;
font-size: 14px;
color: #909399;
text-align: center;
background: #f7f8fa;
border: 1px dashed #dcdfe6;
border-radius: 8px;
}
.info-label { .info-label {
min-width: 60px; min-width: 60px;
font-weight: 600; font-weight: 600;
@ -852,19 +930,24 @@ async function getTitleStudentInfo(titleId: string, markType: number) {
.footer-center { .footer-center {
display: flex; display: flex;
flex: 1; flex: 1;
justify-content: center; min-width: 0;
margin: 0 16px;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden;
} }
.question-nav { .question-nav {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 6px; gap: 6px;
justify-content: center; width: max-content;
min-width: max-content;
margin: 0 auto;
} }
.nav-dot { .nav-dot {
display: inline-flex; display: inline-flex;
flex: 0 0 auto;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 32px; min-width: 32px;

View File

@ -33,8 +33,8 @@ export default defineConfig(async () => {
open: true, open: true,
proxy: { proxy: {
'/api': { '/api': {
// target: 'http://43.136.52.196:9053', target: 'http://43.136.52.196:9053',
target: 'https://admin.xuexiaole.com', // target: 'https://admin.xuexiaole.com',
changeOrigin: true, changeOrigin: true,
}, },
}, },