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'
class ClassService extends BasicService {
async lockScreen(data: { classId: any; status: 0 | 1 }) {
await post('/school/classScreenLock/lock', data)
}
protected findPageUrl(): string {
return '/org/class/list'
}

View File

@ -14,6 +14,7 @@ import MjTable, {
mj_form_text,
} from '#/components/mj/mj-table'
import { pattern_phone } from '#/components/mj/mj-table/fmt'
import hud from '#/utils/hud'
import obj from '#/utils/obj'
import { grades } from '#/store/basic-data'
@ -73,6 +74,7 @@ const addFields = [
'contact',
'phone',
'currencyFlag',
'workMode',
'gradeId',
'masterTeacherId',
]
@ -148,12 +150,27 @@ const editForm = {
name: 'VxeSwitch',
},
},
{
field: 'workMode',
title: '作业模式',
span: 12,
itemRender: {
name: 'VxeSwitch',
props: {
openValue: 1,
closeValue: 0,
openLabel: '已开启',
closeLabel: '未开启',
},
},
},
),
data: {},
},
addFields,
updateFields: ['id', ...addFields],
beforeSaveOrUpdate(formData: any) {
formData.workMode ??= 0
if (userInfo.value.isOnlyStaff) {
formData.schoolId = userInfo.value.schoolId
}
@ -176,6 +193,7 @@ const editForm = {
if (userInfo.value.gradeId) {
data.gradeId = userInfo.value.gradeId
}
data.workMode ??= 0
if (edit) {
gridRef.value.hideEditFormItems('schoolName')
return data
@ -200,6 +218,13 @@ const searchForm = {
//
const grid = {
columns: obj.joinObjs<any>(
{
title: '操作',
width: 160,
slots: {
default: 'operation',
},
},
{ type: 'seq' },
{
field: 'name',
@ -216,6 +241,12 @@ const grid = {
default: 'currencyFlag',
},
},
{
title: '作业模式',
slots: {
default: 'workMode',
},
},
{
field: 'contact',
title: '联系人',
@ -249,6 +280,35 @@ function currencyFlagChange(row: any) {
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>
<template>
@ -263,12 +323,30 @@ function currencyFlagChange(row: any) {
:service="classService"
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 }">
<el-switch
v-model="row.currencyFlag"
@click="currencyFlagChange(row)"
/>
</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>
</div>
</template>

View File

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

View File

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