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

chore: update

上级 5c092036
...@@ -36,11 +36,21 @@ const listOptions = { ...@@ -36,11 +36,21 @@ const listOptions = {
{ label: '更新时间', prop: 'updated_time' } { label: '更新时间', prop: 'updated_time' }
] ]
} }
const exportUrl = $computed(() => {
return `/api/resource/v1/backend/competition-competitor/export?competition_id=${detail.id}`
})
</script> </script>
<template> <template>
<el-dialog title="参赛选手列表"> <el-dialog title="参赛选手列表">
<AppList v-bind="listOptions" ref="appList"></AppList> <AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<a :href="exportUrl" target="_blank">
<el-button type="primary">导出</el-button>
</a>
</template>
</AppList>
<el-row justify="center"> <el-row justify="center">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button> <el-button round auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row> </el-row>
......
...@@ -67,7 +67,7 @@ const listOptions = $computed(() => { ...@@ -67,7 +67,7 @@ const listOptions = $computed(() => {
{ label: '所在班级', prop: 'class_id_name' }, { label: '所在班级', prop: 'class_id_name' },
{ label: '训练次数', prop: 'train_count' }, { label: '训练次数', prop: 'train_count' },
{ label: '评分规则', prop: 'competition_is_more_status' }, { label: '评分规则', prop: 'competition_is_more_status' },
{ label: '已评分人数', prop: 'experiment_name', slots: 'table-count' }, { label: '已评分人数', prop: 'checked_count', slots: 'table-count' },
{ label: '已评分', prop: 'checked_flag_name' }, { label: '已评分', prop: 'checked_flag_name' },
{ label: '得分', prop: 'score', slots: 'table-score' }, { label: '得分', prop: 'score', slots: 'table-score' },
{ label: '更新时间', prop: 'updated_time' }, { label: '更新时间', prop: 'updated_time' },
...@@ -138,8 +138,7 @@ function onUpdateSuccess() { ...@@ -138,8 +138,7 @@ function onUpdateSuccess() {
v-model="dialogVisible" v-model="dialogVisible"
:data="rowData" :data="rowData"
@update="onUpdateSuccess" @update="onUpdateSuccess"
v-if="dialogVisible && rowData" v-if="dialogVisible && rowData"></ScoreDialog>
></ScoreDialog>
<!-- 批量导入 --> <!-- 批量导入 -->
<ImportDialog v-model="importVisible" @update="onUpdateSuccess" v-if="importVisible"></ImportDialog> <ImportDialog v-model="importVisible" @update="onUpdateSuccess" v-if="importVisible"></ImportDialog>
</template> </template>
......
import httpRequest from '@/utils/axios' import httpRequest from '@/utils/axios'
// 获取实验记录列表 // 获取赛项成绩列表
export function getExperimentRecordList(params?: { export function getContestScoreList(params?: { competition_id?: string; page?: number; 'per-page'?: number }) {
course_id?: string
experiment_id?: string
specialty_id?: string
class_id?: string
student_name?: string
page?: number
page_size?: number
}) {
return httpRequest.get('/api/lab/v1/expert/score/list', { params }) return httpRequest.get('/api/lab/v1/expert/score/list', { params })
} }
...@@ -18,25 +10,12 @@ export function getFilterList() { ...@@ -18,25 +10,12 @@ export function getFilterList() {
return httpRequest.get('/api/lab/v1/expert/score/condition') return httpRequest.get('/api/lab/v1/expert/score/condition')
} }
// 获取实验记录详情 // 获取选手列表
export function getExperimentRecord(params: { experiment_id: string; student_id: string }) { export function getContestantsList(params: { competition_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/lab/v1/teacher/record/view', { params }) return httpRequest.get('/api/lab/v1/expert/score/competitors', { params })
} }
// 实验记录评分 // 发布成绩
export function checkExperimentRecord(data: { export function publishScore(data: { id: string }) {
experiment_id: string return httpRequest.post('/api/lab/v1/expert/score/publish', data)
student_id: string
operate: number
result: number
file: number
}) {
return httpRequest.post('/api/lab/v1/teacher/record/check', data)
}
// 批量导入实验记录评分
export function uploadCheckExperimentRecord(data: { file: File }) {
return httpRequest.post('/api/lab/v1/teacher/record/upload', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
} }
<script setup lang="ts">
import { Upload } from '@element-plus/icons-vue'
import { useFileDialog } from '@vueuse/core'
import { ElMessage } from 'element-plus'
import { uploadCheckExperimentRecord } from '../api'
const emit = defineEmits<{
(e: 'update'): void
}>()
// 批量导入
const { files, open } = useFileDialog()
function handleImport() {
open({
accept: '.csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel',
multiple: false
})
}
watchEffect(() => {
if (!files.value?.length) return
const [file] = files.value
uploadCheckExperimentRecord({ file }).then(() => {
ElMessage({ message: '导入成功', type: 'success' })
emit('update')
})
})
</script>
<template>
<el-dialog title="批量导入" :close-on-click-modal="false" width="400px">
<div class="box">
<el-button type="primary" round :icon="Upload" @click="handleImport">本地上传</el-button>
<p>
<a
href="https://webapp-pub.ezijing.com/project/saas-lab/%E5%AE%9E%E9%AA%8C%E6%88%90%E7%BB%A9%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx"
download
>下载模板</a
>
</p>
</div>
</el-dialog>
</template>
<style lang="scss" scoped>
.box {
padding: 20px 0;
display: flex;
align-items: center;
justify-content: center;
.el-button {
width: 220px;
}
p {
color: #999;
margin-left: 20px;
}
}
</style>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { RecordItem, FileItem } from '../types'
import { Document } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getExperimentRecord, checkExperimentRecord } from '../api'
interface Props {
data: RecordItem
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
let detail = $ref<RecordItem>()
function fetchInfo() {
getExperimentRecord({ experiment_id: props.data.experiment_id, student_id: props.data.student_id }).then(res => {
detail = res.data
})
}
watchEffect(() => {
fetchInfo()
})
// 实验报告文件
const file = $computed<FileItem>(() => {
try {
return detail?.file ? JSON.parse(detail.file) : null
} catch (error) {
console.log(error)
}
return null
})
// 实验过程截图
const pictures = $computed<FileItem[]>(() => {
try {
return detail?.pictures ? JSON.parse(detail.pictures) : []
} catch (error) {
console.log(error)
}
return []
})
const formRef = $ref<FormInstance>()
const form = reactive<{ operate?: number; result?: number; file?: number }>({
operate: undefined,
result: undefined,
file: undefined
})
const score = $computed<number>(() => {
const result = ((form.operate || 0) + (form.result || 0) + (form.file || 0)) / 3
return parseFloat(result.toFixed(2))
})
const rules = ref<FormRules>({
operate: [{ required: true, message: '请输入1~100数字' }],
result: [{ required: true, message: '请输入1~100数字' }],
file: [{ required: true, message: '请输入1~100数字' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(() => {
ElMessageBox.confirm('成绩评分不能修改,确认要保存该成绩吗?', '提示').then(() => {
const params: any = { ...form, experiment_id: props.data.experiment_id, student_id: props.data.student_id }
checkExperimentRecord(params).then(() => {
ElMessage({ message: '保存成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
})
})
}
</script>
<template>
<el-dialog
title="学生实验评分"
:close-on-click-modal="false"
width="700px"
@update:modelValue="$emit('update:modelValue')"
>
<el-form :rules="rules" label-width="120px" label-suffix=":" v-if="detail">
<el-row>
<el-col :span="12">
<el-form-item label="实验名称">{{ detail.experiment_name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实验课程名称">{{ detail.course_name }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="学生姓名">{{ detail.student_name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学生学号">{{ detail.sno_number }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="所属专业">{{ detail.specialty_name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属班级">{{ detail.class_name }}</el-form-item>
</el-col>
</el-row>
<el-form-item label="实验成绩" class="form-item-score">
<el-form
ref="formRef"
:model="form"
:rules="rules"
hide-required-asterisk
inline
label-position="top"
style="padding: 5px 0 20px"
>
<el-form-item label="实验操作" prop="operate">
<el-input-number :min="1" :max="100" :controls="false" step-strictly v-model="form.operate" />
</el-form-item>
<el-form-item label="实验结果" prop="result">
<el-input-number :min="1" :max="100" :controls="false" step-strictly v-model="form.result" />
</el-form-item>
<el-form-item label="实验报告" prop="file">
<el-input-number :min="1" :max="100" :controls="false" step-strictly v-model="form.file" />
</el-form-item>
<el-form-item label="综合实验成绩">
<el-input-number :min="0" :max="100" :controls="false" disabled v-model="score" />
</el-form-item>
</el-form>
</el-form-item>
<el-form-item label="实验报告文件">
<div v-if="file">
<a :href="file.url" target="_blank" class="file-item">
<el-icon><Document /></el-icon>{{ file.name }}
</a>
</div>
</el-form-item>
<el-form-item label="实验过程截图">
<ul class="picture-list">
<li v-for="item in pictures" :key="item.url">
<p class="t1">
<a :href="item.url" target="_blank">{{ item.name }}</a>
</p>
<p class="t2">截图时间:{{ item.upload_time }}</p>
</li>
</ul>
</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>
.file-item {
display: flex;
align-items: center;
color: var(--main-color);
.el-icon {
margin-right: 5px;
}
}
.picture-list {
width: 100%;
li {
display: flex;
justify-content: space-between;
}
a {
color: var(--main-color);
}
.t1 {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
.form-item-score {
padding-top: 10px;
background-color: #f8f9fb;
border-radius: 16px;
:deep(.el-form-item__label) {
text-align: center;
}
.el-input-number {
width: 100px;
}
}
</style>
<script setup lang="ts">
import type { ContestScoreItem } from '../types'
import { ElMessage } from 'element-plus'
import { publishScore } from '../api'
interface Props {
data: ContestScoreItem
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
// 发布
function handleSubmit() {
publishScore({ id: props.data.id }).then(() => {
ElMessage.success('发布成功')
emit('update')
emit('update:modelValue', false)
})
}
</script>
<template>
<el-dialog
title="发布成绩"
width="500px"
:close-on-click-modal="false"
@update:modelValue="$emit('update:modelValue')">
<div class="content">
<p>请确认赛项成绩之后再进行发布成绩!</p>
<p>
赛项名称:<b>{{ data.name }}</b>
</p>
<br />
<p>
<span>报名人数:{{ data.all_competitor_count }}</span>
<span>完赛人数:{{ data.complete_competitor_count }}</span>
<span>已评分人数:{{ data.checked_competitor_count }}</span>
</p>
<br />
<p>请确认是否发布成绩!</p>
</div>
<el-row justify="center" style="margin-top: 40px">
<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-dialog>
</template>
<style lang="scss" scoped>
.content {
text-align: center;
p {
margin: 10px 0;
}
b {
color: var(--main-color);
}
span + span {
margin-left: 40px;
}
}
</style>
\ No newline at end of file
<script setup lang="ts">
import type { ContestantScoreItem, CheckItem } from '../types'
interface Props {
data: ContestantScoreItem
}
const props = defineProps<Props>()
// 列表配置
const listOptions = $computed(() => {
const columns = [
{ label: '评分规则', prop: 'name' },
{ label: '分值', prop: 'old_score' },
{ label: '占比(%)', prop: 'ratio' },
{
label: '系统评分',
prop: 'organ_id_name',
computed({ row }: { row: CheckItem }): string {
if (row.type === '2') {
const scoreValues = Object.values(row.score)
return scoreValues[0] || ''
} else {
return '--'
}
}
}
]
props.data.check_details_experts?.forEach(item => {
columns.push({
label: item.name + '评分',
prop: item.sso_id,
computed({ row }: { row: CheckItem }): string {
if (row.type === '2') {
return '--'
} else {
return row.score[item.sso_id] || '--'
}
}
})
})
const data = props.data.check_details?.map(item => {
return { ...item, ...item.score }
})
return { columns, data }
})
</script>
<template>
<el-dialog title="评分详情">
<el-descriptions :column="2">
<el-descriptions-item label="赛项名称:" :span="2">{{ data.competition_id_name }}</el-descriptions-item>
<el-descriptions-item label="选手姓名:">{{ data.student_name }}</el-descriptions-item>
<el-descriptions-item label="选手ID:">{{ data.login_id }}</el-descriptions-item>
<el-descriptions-item label="评分规则:">{{ data.competition_is_more_status }}</el-descriptions-item>
<el-descriptions-item label="计算规则:">{{ data.competition_rule_type }}</el-descriptions-item>
</el-descriptions>
<AppList v-bind="listOptions"></AppList>
<p class="result">
最终得分:<span>{{ data.score_name }}</span>
</p>
<el-row justify="center" style="margin-top: 40px">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</el-dialog>
</template>
<style lang="scss" scoped>
.result {
display: flex;
align-items: center;
justify-content: center;
span {
padding: 0 10px;
font-size: 40px;
color: var(--main-color);
}
}
</style>
...@@ -5,6 +5,9 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -5,6 +5,9 @@ export const routes: Array<RouteRecordRaw> = [
{ {
path: '/teacher/contest/score', path: '/teacher/contest/score',
component: AppLayout, component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }] children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: ':id', component: () => import('./views/Detail.vue'), props: true }
]
} }
] ]
export interface RecordItem { export interface ContestScoreItem {
check_time: string id: string
checker_id?: string name: string
class_id: string host_unit_id: string
class_name: string type: string
commit_time: string publish_status: string
course_name?: string updated_time: string
experiment_id: string host_unit_id_name: string
experiment_name: string type_name: string
file?: string publish_status_name: string
pictures?: string teachers: string[]
expert_count: number
all_competitor_count: number
complete_competitor_count: number
checked_competitor_count: number
}
export interface ContestantScoreItem {
id: string
competition_id: string
student_id: string
login_id: string
grade: string
train_count: string
score: string score: string
score_details?: string score_status: string
sno_number: string updated_time: string
specialty_id: string specialty_id: string
specialty_name: string specialty_id_name: string
status: 0 | 1 | 2 class_id: string
status_name: string class_id_name: string
student_id: string
student_name: string student_name: string
sno_number: string
organ_id: string
organ_id_name: string
gender: string
competition_id_name: string
competition_is_more_status: string
competition_rule_type: string
competition_rubric: ScoreBook
checked_count: number
need_check_count: number
checked_flag: true
checked_flag_name: string
score_name: string
publish_status: string
publish_status_name: string
check_details_experts: CheckExpert[]
check_details: CheckItem[]
}
export interface CheckItem {
id: string
name: string
type: string
old_score: string
ratio: string
type_name: string
score: Record<string, string>
}
export interface CheckExpert {
sso_id: string
name: string
} }
export interface FileItem { export interface ScoreBook {
name: string name: string
url: string url: string
upload_time: string
} }
<script setup lang="ts">
import type { ContestantScoreItem } from '../types'
import AppList from '@/components/base/AppList.vue'
import { getContestantsList } from '../api'
const ScoreViewDialog = defineAsyncComponent(() => import('../components/ScoreViewDialog.vue'))
interface Props {
id: string
}
const props = defineProps<Props>()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getContestantsList,
params: { competition_id: props.id }
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '选手姓名', prop: 'student_name' },
{ label: '参赛ID', prop: 'login_id' },
{ label: '性别', prop: 'gender' },
{ label: '所在学校', prop: 'organ_id_name' },
{ label: '所在专业', prop: 'specialty_id_name' },
{ label: '所在年级', prop: 'grade' },
{ label: '训练次数', prop: 'train_count' },
{ label: '评分规则', prop: 'competition_is_more_status' },
{ label: '已评分人数', prop: 'checked_count', slots: 'table-count' },
{ label: '得分', prop: 'score_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 140 }
]
}
})
let dialogVisible = $ref(false)
const rowData = ref<ContestantScoreItem>()
// 查看评分详情
function handleView(row: ContestantScoreItem) {
rowData.value = row
dialogVisible = true
}
function onUpdateSuccess() {
appList?.refetch()
}
</script>
<template>
<AppCard title="发布成绩">
<el-descriptions>
<el-descriptions-item label="赛项名称:">{{ $route.query.name }}</el-descriptions-item>
</el-descriptions>
<AppList v-bind="listOptions" ref="appList">
<template #table-count="{ row }"> {{ row.checked_count }}/{{ row.need_check_count }} </template>
<template #table-x="{ row }">
<el-button text type="primary" @click="handleView(row)" v-permission="'v1-teacher-record-check'">
查看评分详情
</el-button>
</template>
</AppList>
</AppCard>
<ScoreViewDialog
:data="rowData"
v-model="dialogVisible"
@update="onUpdateSuccess"
v-if="dialogVisible && rowData"></ScoreViewDialog>
</template>
<script setup lang="ts"> <script setup lang="ts">
import type { RecordItem } from '../types' import type { ContestScoreItem } from '../types'
import AppList from '@/components/base/AppList.vue' import AppList from '@/components/base/AppList.vue'
import { getExperimentRecordList } from '../api' import { getContestScoreList } from '../api'
import { useFilterList } from '../composables/useFilterList' import { useFilterList } from '../composables/useFilterList'
const ScoreDialog = defineAsyncComponent(() => import('../components/ScoreDialog.vue')) const ScorePublishDialog = defineAsyncComponent(() => import('../components/ScorePublishDialog.vue'))
const ImportDialog = defineAsyncComponent(() => import('../components/ImportDialog.vue'))
const { competitions } = useFilterList() const { competitions } = useFilterList()
...@@ -15,10 +14,8 @@ const appList = $ref<InstanceType<typeof AppList> | null>(null) ...@@ -15,10 +14,8 @@ const appList = $ref<InstanceType<typeof AppList> | null>(null)
const listOptions = $computed(() => { const listOptions = $computed(() => {
return { return {
remote: { remote: {
httpRequest: getExperimentRecordList, httpRequest: getContestScoreList,
params: { params: { competition_id: '' }
competition_id: ''
}
}, },
filters: [ filters: [
{ {
...@@ -46,11 +43,10 @@ const listOptions = $computed(() => { ...@@ -46,11 +43,10 @@ const listOptions = $computed(() => {
] ]
} }
}) })
const importVisible = $ref(false)
let dialogVisible = $ref(false) let dialogVisible = $ref(false)
const rowData = ref<RecordItem>() const rowData = ref<ContestScoreItem>()
// 评分 // 发布成绩
function handleScore(row: RecordItem) { function handlePublish(row: ContestScoreItem) {
rowData.value = row rowData.value = row
dialogVisible = true dialogVisible = true
} }
...@@ -64,24 +60,26 @@ function onUpdateSuccess() { ...@@ -64,24 +60,26 @@ function onUpdateSuccess() {
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #table-teachers="{ row }"> {{ row.teachers.join() }} </template> <template #table-teachers="{ row }"> {{ row.teachers.join() }} </template>
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button round type="primary" @click="handleScore(row)" v-permission="'v1-teacher-record-check'" <el-button round type="primary" v-permission="'v1-teacher-record-check'">
>查看成绩</el-button <router-link :to="`/teacher/contest/score/${row.id}?name=${row.name}`" target="_blank">查看成绩</router-link>
> </el-button>
<el-button round type="primary" @click="handleScore(row)" v-permission="'v1-teacher-record-check'" <el-button
>发布赛项成绩</el-button round
> type="primary"
@click="handlePublish(row)"
v-permission="'v1-teacher-record-check'"
v-if="row.publish_status !== '1'">
发布赛项成绩
</el-button>
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
<!-- 评分 --> <!-- 发布赛项成绩 -->
<ScoreDialog <ScorePublishDialog
v-model="dialogVisible"
:data="rowData" :data="rowData"
v-model="dialogVisible"
@update="onUpdateSuccess" @update="onUpdateSuccess"
v-if="dialogVisible && rowData" v-if="dialogVisible && rowData"></ScorePublishDialog>
></ScoreDialog>
<!-- 批量导入 -->
<ImportDialog v-model="importVisible" @update="onUpdateSuccess" v-if="importVisible"></ImportDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论