提交 e5239ad4 authored 作者: 王鹏飞's avatar 王鹏飞

Merge branch 'master' into dev

...@@ -81,6 +81,7 @@ export function getContestBook(params: { competition_id: string }) { ...@@ -81,6 +81,7 @@ export function getContestBook(params: { competition_id: string }) {
export function getContestBooks(params: { competition_id: string }) { export function getContestBooks(params: { competition_id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition-book/list', { params }) return httpRequest.get('/api/resource/v1/backend/competition-book/list', { params })
} }
// 添加赛项指导书 // 添加赛项指导书
export function createContestBook(data: ContestBookUpdateParams) { export function createContestBook(data: ContestBookUpdateParams) {
return httpRequest.post('/api/resource/v1/backend/competition-book/create', data) return httpRequest.post('/api/resource/v1/backend/competition-book/create', data)
...@@ -147,3 +148,21 @@ export function getContestantList(params?: { student_name?: string; page?: numbe ...@@ -147,3 +148,21 @@ export function getContestantList(params?: { student_name?: string; page?: numbe
export function getPlatformKeys(params?: { competition_id: string }) { export function getPlatformKeys(params?: { competition_id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition-book/platform-keys', { params }) return httpRequest.get('/api/resource/v1/backend/competition-book/platform-keys', { params })
} }
// 获取赛项试题
export function getQuestionList(params: { competition_id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition-question/list', { params })
}
// 添加大赛试题
export function createQuestion(data: ContestBookUpdateParams) {
return httpRequest.post('/api/resource/v1/backend/competition-question/create', data)
}
// 更新大赛试题
export function updateQuestion(data: ContestBookUpdateParams) {
return httpRequest.post('/api/resource/v1/backend/competition-question/update', data)
}
// 删除大赛试题
export function deleteQuestion(data: { id: string }) {
return httpRequest.post('/api/resource/v1/backend/competition-question/delete', data)
}
\ No newline at end of file
<script setup lang="ts">
import type { ContestBookItem } from '../types'
import { CirclePlus } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AppList from '@/components/base/AppList.vue'
import ViewQuestionPreviewDialog from './ViewQuestionPreviewDialog.vue'
import { getQuestionList, deleteQuestion } from '../api'
const FormDialog = defineAsyncComponent(() => import('./ViewQuestionFormDialog.vue'))
interface Props {
id: string
}
const props = defineProps<Props>()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = {
hasPagination: false,
remote: {
httpRequest: getQuestionList,
params: { competition_id: props.id },
callback(res: any) {
return res?.items ? { list: res?.items } : { list: [] }
}
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '试题名称', prop: 'name' },
{ label: '文件类型', prop: 'type' },
{ label: '创建人', prop: 'created_operator.real_name' },
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' },
{ label: '操作', slots: 'table-x', width: 180 }
]
}
let dialogVisible = $ref(false)
const rowData = ref<ContestBookItem>()
// 新增
function handleAdd() {
rowData.value = undefined
dialogVisible = true
}
// 编辑
function handleUpdate(row: ContestBookItem) {
rowData.value = row
dialogVisible = true
}
// 查阅
let viewVisible = $ref(false)
function handleView(row: ContestBookItem) {
rowData.value = row
viewVisible = true
}
// 删除
function handleRemoveClass(row: ContestBookItem) {
ElMessageBox.confirm('确定要删除吗?', '提示').then(() => {
deleteQuestion({ id: row.id }).then(() => {
ElMessage({ message: '删除成功', type: 'success' })
onUpdateSuccess()
})
})
}
function onUpdateSuccess() {
appList?.refetch()
}
</script>
<template>
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'competition-book-create'"
>新增</el-button
>
</template>
<template #table-x="{ row }">
<el-button link round type="info" @click="handleView(row)" v-permission="'competition-book-detail'"
>查阅</el-button
>
<el-button link round type="primary" @click="handleUpdate(row)" v-permission="'competition-book-update'"
>编辑</el-button
>
<el-button link round type="danger" @click="handleRemoveClass(row)" v-permission="'competition-book-delete'"
>删除</el-button
>
</template>
</AppList>
<FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
<ViewQuestionPreviewDialog
v-model="viewVisible"
:data="rowData"
v-if="viewVisible && rowData"
></ViewQuestionPreviewDialog>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { ContestItem, ContestBookItem, ContestBookUpdateParams } from '../types'
import { ElMessageBox, ElMessage } from 'element-plus'
import { pick } from 'lodash-es'
import AppUpload from '@/components/base/AppUpload.vue'
import { createQuestion, updateQuestion, getPlatformKeys } from '../api'
import { useMapStore } from '@/stores/map'
interface Props {
data?: ContestBookItem
}
const props = defineProps<Props>()
const route = useRoute()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const detail = $ref(inject('detail') as ContestItem)
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
const formRef = $ref<FormInstance>()
const form = reactive<any>({
competition_id: detail.id,
files: [],
url: '',
type: '',
name: '',
status: '1',
protocol: false,
platform_key: ''
})
watchEffect(() => {
if (!props.data) return
form.files = [JSON.parse(props.data.url)]
Object.assign(form, { protocol: true }, props.data)
})
const checkProtocol = (rule: any, value: any, callback: any) => {
if (!value) {
return callback(new Error('请阅读并同意'))
} else {
callback()
}
}
const rules = ref<FormRules>({
files: [{ type: 'array', required: true, message: '请上传大赛试题' }],
name: [{ required: true, message: '请输入大赛试题名称', trigger: 'blur' }],
course_id: [{ required: true, message: '请选择关联训练课程', trigger: 'change' }],
experiment_id: [{ required: true, message: '请选择关联训练', trigger: 'change' }],
protocol: [{ validator: checkProtocol, message: '请阅读并同意', trigger: 'change' }]
})
const isUpdate = $computed(() => {
return !!props.data?.id
})
const title = $computed(() => {
return isUpdate ? '编辑大赛试题' : '新增大赛试题'
})
function handleBeforeUpload() {
if (form.files.length) {
return ElMessageBox.confirm('系统仅支持1个大赛试题,此操作将覆盖原有大赛试题文件,确认上传新文件吗?', '提示')
}
return true
}
function handleUploadSuccess(file: any) {
const [name, type] = file.name.split('.')
form.name = name
form.type = type
}
// 提交
function handleSubmit() {
formRef?.validate().then(() => {
const [file] = form.files
const params = Object.assign({}, pick(form, ['competition_id', 'name', 'type', 'status', 'platform_key']), {
url: JSON.stringify({ name: file.name, url: file.url, size: file.size })
})
isUpdate ? handleUpdate(params) : handleCreate(params)
})
}
// 新增
function handleCreate(params: ContestBookUpdateParams) {
createQuestion(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
// 修改
function handleUpdate(params: ContestBookUpdateParams) {
params.id = props.data?.id
updateQuestion(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
// 获取平台标识列表
let platformKeys = $ref<{ platform_key: string; name: string }[]>([])
const getPlatformKeysList = function () {
console.log(route, 'route')
getPlatformKeys({ competition_id: route.params?.id as string }).then(res => {
if (res.data?.items) {
platformKeys = res.data?.items
if (form.platform_key === '') {
form.platform_key = res.data?.items[0]?.platform_key
}
}
})
}
getPlatformKeysList()
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="600px" @update:modelValue="$emit('update:modelValue')">
<el-form ref="formRef" :model="form" :rules="rules" label-width="124px">
<el-form-item label="大赛试题文件" prop="files">
<AppUpload
v-model="form.files"
:limit="1"
:beforeUpload="handleBeforeUpload"
accept=".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
@success="handleUploadSuccess">
<template #tip>大赛试题文件支持格式包含:doc docx,大小不超过50M</template>
</AppUpload>
</el-form-item>
<el-form-item label="大赛试题名称" prop="name">
<el-input v-model="form.name"></el-input>
<p class="form-tips">大赛试题名称自动取值于文件名称,可以进行二次修改。</p>
</el-form-item>
<el-form-item label="关联赛项">
<el-input :value="detail.name" disabled></el-input>
</el-form-item>
<el-form-item label="关联实验">
<el-radio-group v-model="form.platform_key">
<el-radio v-for="item in platformKeys" :key="item.platform_key" :label="item.platform_key">{{
item.name
}}</el-radio>
<!-- <el-radio :key="1" label="1">商业数据分析实验</el-radio>
<el-radio :key="1" label="2">数字营销实操</el-radio> -->
</el-radio-group>
</el-form-item>
<el-form-item label="有效状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="protocol">
<el-checkbox label="我已阅读并同意" v-model="form.protocol" />
<a
href="https://view.officeapps.live.com/op/view.aspx?src=https://webapp-pub.ezijing.com/center_resource/%E7%B4%AB%E8%8D%86%E6%95%99%E8%82%B2%E7%94%A8%E6%88%B7%E5%85%A5%E9%A9%BB%E5%8F%8A%E7%BD%91%E7%BB%9C%E6%95%99%E5%AD%A6%E8%B5%84%E6%BA%90%E5%8D%8F%E8%AE%AE(1).docx"
target="_blank"
>《紫荆教育用户入驻及网络教学资源协议》</a
>
</el-form-item>
<el-row justify="center">
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">取消</el-button>
</el-row>
</el-form>
</el-dialog>
</template>
<style lang="scss" scoped>
.form-tips {
font-size: 12px;
color: #999;
}
</style>
<script setup lang="ts">
import type { ContestBookItem } from '../types'
import Preview from '@/components/Preview.vue'
interface Props {
data: ContestBookItem
}
const props = defineProps<Props>()
const file = $computed(() => {
return JSON.parse(props.data.url)
})
</script>
<template>
<el-dialog title="查阅大赛试题">
<Preview :url="file.url" style="height: 60vh"></Preview>
</el-dialog>
</template>
...@@ -7,6 +7,7 @@ import dayjs from 'dayjs' ...@@ -7,6 +7,7 @@ import dayjs from 'dayjs'
const ViewBook = defineAsyncComponent(() => import('../components/ViewBook.vue')) const ViewBook = defineAsyncComponent(() => import('../components/ViewBook.vue'))
const ViewVideo = defineAsyncComponent(() => import('../components/ViewVideo.vue')) const ViewVideo = defineAsyncComponent(() => import('../components/ViewVideo.vue'))
const ViewQuestion = defineAsyncComponent(() => import('../components/ViewQuestion.vue'))
const ScoringRulesDialog = defineAsyncComponent(() => import('../components/ScoringRulesDialog.vue')) const ScoringRulesDialog = defineAsyncComponent(() => import('../components/ScoringRulesDialog.vue'))
const ScoringExpertsDialog = defineAsyncComponent(() => import('../components/ScoringExpertsDialog.vue')) const ScoringExpertsDialog = defineAsyncComponent(() => import('../components/ScoringExpertsDialog.vue'))
const ContestantDialog = defineAsyncComponent(() => import('../components/ContestantDialog.vue')) const ContestantDialog = defineAsyncComponent(() => import('../components/ContestantDialog.vue'))
...@@ -160,28 +161,35 @@ function handleExperts() { ...@@ -160,28 +161,35 @@ function handleExperts() {
<AppCard title="操作视频"> <AppCard title="操作视频">
<ViewVideo :id="id"></ViewVideo> <ViewVideo :id="id"></ViewVideo>
</AppCard> </AppCard>
<AppCard title="大赛试题">
<ViewQuestion :id="id"></ViewQuestion>
</AppCard>
<!-- 评分规则 --> <!-- 评分规则 -->
<ScoringRulesDialog <ScoringRulesDialog
v-model="scoringRulesVisible" v-model="scoringRulesVisible"
:disabled="isStarted" :disabled="isStarted"
@update="fetchRule" @update="fetchRule"
v-if="scoringRulesVisible && detail"></ScoringRulesDialog> v-if="scoringRulesVisible && detail"
></ScoringRulesDialog>
<!-- 评分专家 --> <!-- 评分专家 -->
<ScoringExpertsDialog <ScoringExpertsDialog
v-model="scoringExpertsVisible" v-model="scoringExpertsVisible"
:disabled="isStarted" :disabled="isStarted"
@update="fetchExperts" @update="fetchExperts"
v-if="scoringExpertsVisible && detail"></ScoringExpertsDialog> v-if="scoringExpertsVisible && detail"
></ScoringExpertsDialog>
<!-- 参赛选手 --> <!-- 参赛选手 -->
<ContestantDialog <ContestantDialog
v-model="contestantVisible" v-model="contestantVisible"
:disabled="isStarted" :disabled="isStarted"
v-if="contestantVisible && detail"></ContestantDialog> v-if="contestantVisible && detail"
></ContestantDialog>
<!-- 评分细则 --> <!-- 评分细则 -->
<ScoringRulesBookDialog <ScoringRulesBookDialog
v-model="scoringRulesBookVisible" v-model="scoringRulesBookVisible"
:disabled="isStarted" :disabled="isStarted"
v-if="scoringRulesBookVisible && detail"></ScoringRulesBookDialog> v-if="scoringRulesBookVisible && detail"
></ScoringRulesBookDialog>
</template> </template>
<style lang="scss"> <style lang="scss">
......
...@@ -86,3 +86,25 @@ export function updateTrainCount(data: { competition_id: string }) { ...@@ -86,3 +86,25 @@ export function updateTrainCount(data: { competition_id: string }) {
export function getScoreReport(params: { competition_id: string }) { export function getScoreReport(params: { competition_id: string }) {
return httpRequest.get('/api/lab/v1/student/competition/score-report', { params }) return httpRequest.get('/api/lab/v1/student/competition/score-report', { params })
} }
// 获取实验报告
export function getExperimentReport(params: { competition_id: string; platform_key: string }) {
return httpRequest.get('/api/lab/v1/student/competition/train-report', { params })
}
// 上传实验报告
export function uploadExperimentReport(data: {
competition_id: string
platform_key: string
name: string
url: string
size: number
desc: string
}) {
return httpRequest.post('/api/lab/v1/student/competition/save-train-report', data)
}
// 删除实验报告
export function deleteExperimentReport(data: { id: string }) {
return httpRequest.post('/api/lab/v1/student/competition/delete-train-report', data)
}
<script setup lang="ts"> <script setup lang="ts">
import type { ExperimentBookType } from '../types' import type { ExperimentBookType } from '../types'
import { CloseBold } from '@element-plus/icons-vue' import { Document, CloseBold } from '@element-plus/icons-vue'
import Preview from '@/components/Preview.vue' import Preview from '@/components/Preview.vue'
import { getExperimentBooks } from '../api' import { getExperimentBooks } from '../api'
......
<script setup lang="ts">
import type { Contest } from '../types'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { getExperimentReport, uploadExperimentReport } from '../api'
interface Props {
data: Contest
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const route = useRoute()
const formRef = $ref<FormInstance>()
const form = reactive<any>({ files: [] })
async function fetchInfo() {
const res = await getExperimentReport({
competition_id: props.data.id,
platform_key: route.query.type as string
})
const detail = res.data.detail
form.files = detail.id ? [detail] : []
}
onMounted(fetchInfo)
const rules = ref<FormRules>({
files: [{ required: true, message: '请上传实验报告文件', trigger: 'blur' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(update)
}
// 修改
const update = () => {
const [file] = form.files
uploadExperimentReport({
competition_id: props.data.id,
platform_key: route.query.type as string,
name: file.name,
url: file.url,
size: file.size,
desc: file.name
}).then(() => {
ElMessage({ message: '上传成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
</script>
<template>
<el-dialog
title="上传实验报告"
:close-on-click-modal="false"
width="600px"
@update:modelValue="$emit('update:modelValue')">
<el-form ref="formRef" :model="form" :rules="rules">
<el-form-item label="实验报告文件" prop="files">
<AppUpload
v-model="form.files"
:limit="1"
accept=".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.pdf,application/pdf,.ppt,.pptx,application/vnd.ms-powerpoint">
<template #tip>实验报告文件只能上传一个,支持格式包含:doc docx pdf ppt pptx,大小不超过50M</template>
</AppUpload>
</el-form-item>
<el-row justify="center">
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">取消</el-button>
</el-row>
</el-form>
</el-dialog>
</template>
...@@ -12,6 +12,7 @@ const Book = defineAsyncComponent(() => import('../components/Book.vue')) ...@@ -12,6 +12,7 @@ const Book = defineAsyncComponent(() => import('../components/Book.vue'))
const Video = defineAsyncComponent(() => import('../components/Video.vue')) const Video = defineAsyncComponent(() => import('../components/Video.vue'))
const Discuss = defineAsyncComponent(() => import('../components/Discuss.vue')) const Discuss = defineAsyncComponent(() => import('../components/Discuss.vue'))
const Result = defineAsyncComponent(() => import('../components/Result.vue')) const Result = defineAsyncComponent(() => import('../components/Result.vue'))
const ReportDialog = defineAsyncComponent(() => import('../components/ReportDialog.vue'))
const route = useRoute() const route = useRoute()
...@@ -133,6 +134,8 @@ const getExperimentId = computed(() => { ...@@ -133,6 +134,8 @@ const getExperimentId = computed(() => {
} }
return value return value
}) })
const reportDialogVisible = $ref(false)
</script> </script>
<template> <template>
...@@ -163,6 +166,7 @@ const getExperimentId = computed(() => { ...@@ -163,6 +166,7 @@ const getExperimentId = computed(() => {
<el-button type="primary" :icon="HomeFilled" @click="handleBackHome">返回首页</el-button> <el-button type="primary" :icon="HomeFilled" @click="handleBackHome">返回首页</el-button>
<div> <div>
<el-button type="primary" :loading="screenshotLoading" @click="handleCapture">截图</el-button> <el-button type="primary" :loading="screenshotLoading" @click="handleCapture">截图</el-button>
<el-button type="primary" @click="reportDialogVisible = true">上传实验报告</el-button>
</div> </div>
</el-row> </el-row>
</AppCard> </AppCard>
...@@ -173,15 +177,13 @@ const getExperimentId = computed(() => { ...@@ -173,15 +177,13 @@ const getExperimentId = computed(() => {
<path <path
class="path-wapper" class="path-wapper"
d="M0 0l14.12 8.825A4 4 0 0116 12.217v61.566a4 4 0 01-1.88 3.392L0 86V0z" d="M0 0l14.12 8.825A4 4 0 0116 12.217v61.566a4 4 0 01-1.88 3.392L0 86V0z"
fill="#f8f9fa" fill="#f8f9fa"></path>
></path>
<path <path
class="path-arrow" class="path-arrow"
style="transform: rotate(180deg); transform-origin: center" style="transform: rotate(180deg); transform-origin: center"
d="M10.758 48.766a.778.778 0 000-1.127L6.996 43l3.762-4.639a.778.778 0 000-1.127.85.85 0 00-1.172 0l-4.344 5.202a.78.78 0 000 1.128l4.344 5.202a.85.85 0 001.172 0z" d="M10.758 48.766a.778.778 0 000-1.127L6.996 43l3.762-4.639a.778.778 0 000-1.127.85.85 0 00-1.172 0l-4.344 5.202a.78.78 0 000 1.128l4.344 5.202a.85.85 0 001.172 0z"
fill="#8D9EA7" fill="#8D9EA7"
fill-rule="nonzero" fill-rule="nonzero"></path>
></path>
</g> </g>
</svg> </svg>
</div> </div>
...@@ -189,7 +191,12 @@ const getExperimentId = computed(() => { ...@@ -189,7 +191,12 @@ const getExperimentId = computed(() => {
</div> </div>
</template> </template>
</DragPanel> </DragPanel>
<ReportDialog
v-model="reportDialogVisible"
:data="competition"
v-if="reportDialogVisible && competition"></ReportDialog>
</template> </template>
<!-- 上传报告 -->
<style lang="scss" scoped> <style lang="scss" scoped>
.close-btn { .close-btn {
......
...@@ -274,7 +274,8 @@ function handleReportPreviewReady() { ...@@ -274,7 +274,8 @@ function handleReportPreviewReady() {
<el-button <el-button
type="primary" type="primary"
:disabled="disabled" :disabled="disabled"
v-if="experimentInfo?.report_upload_way === 2 && !experimentInfo?.is_commit_report"> v-if="experimentInfo?.report_upload_way === 2 && !experimentInfo?.is_commit_report"
>
<router-link :to="`/student/lab/report/${form.experiment_id}`" target="_blank">在线实验报告</router-link> <router-link :to="`/student/lab/report/${form.experiment_id}`" target="_blank">在线实验报告</router-link>
</el-button> </el-button>
<el-button <el-button
...@@ -308,22 +309,26 @@ function handleReportPreviewReady() { ...@@ -308,22 +309,26 @@ function handleReportPreviewReady() {
v-model="reportDialogVisible" v-model="reportDialogVisible"
:data="experimentInfo" :data="experimentInfo"
@update="fetchExperimentRecord" @update="fetchExperimentRecord"
v-if="reportDialogVisible && experimentInfo"></ReportDialog> v-if="reportDialogVisible && experimentInfo"
></ReportDialog>
<ReportFilePreview <ReportFilePreview
v-model="reportFilePreviewVisible" v-model="reportFilePreviewVisible"
:data="experimentInfo" :data="experimentInfo"
v-if="reportFilePreviewVisible && experimentInfo"></ReportFilePreview> v-if="reportFilePreviewVisible && experimentInfo"
></ReportFilePreview>
<!-- 实验准备 --> <!-- 实验准备 -->
<PrepareDialog <PrepareDialog
v-model="prepareDialogVisible" v-model="prepareDialogVisible"
:data="experimentInfo" :data="experimentInfo"
v-if="prepareDialogVisible && experimentInfo"></PrepareDialog> v-if="prepareDialogVisible && experimentInfo"
></PrepareDialog>
<!-- 实验结果 --> <!-- 实验结果 -->
<ResultDialog <ResultDialog
v-model="resultDialogVisible" v-model="resultDialogVisible"
:data="experimentInfo" :data="experimentInfo"
v-if="resultDialogVisible && experimentInfo"></ResultDialog> v-if="resultDialogVisible && experimentInfo"
></ResultDialog>
<!-- 导出在线报告 --> <!-- 导出在线报告 -->
<template v-if="experimentInfo?.id && isExport"> <template v-if="experimentInfo?.id && isExport">
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论