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

chore: update

上级 f1368d8f
import httpRequest from '@/utils/axios'
// 获取讨论交流列表
export function getDiscussList(params?: {
competition_id?: string
student_name?: string
page?: number
page_size?: number
}) {
return httpRequest.get('/api/lab/v1/teacher/train-discussion/list', { params })
}
// 获取筛选条件
export function getFilterList() {
return httpRequest.get('/api/lab/v1/teacher/train-discussion/competitions')
}
// 获取讨论交流详情
export function getDiscuss(params: { id: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/lab/v1/teacher/train-discussion/view', { params })
}
// 获取讨论交流详情
export function submitDiscussComment(data: { id: string; content: string }) {
return httpRequest.post('/api/lab/v1/teacher/train-discussion/comment', data)
}
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { DiscussListItem, DiscussInfo, DiscussCommentItem } from '../types'
import { Loading } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useInfiniteScroll } from '@vueuse/core'
import DiscussItem from './DiscussItem.vue'
import { getDiscuss, submitDiscussComment } from '../api'
interface Props {
data: DiscussListItem
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const params = reactive({ id: props.data.id, page: 1, 'per-page': 10 })
let hasMore = $ref(false)
let isLoading = $ref(false)
const detail = reactive<{ info?: DiscussInfo; list: DiscussCommentItem[] }>({ info: undefined, list: [] })
function fetchInfo(isForce = false) {
if (!props.data.id) return
if (isForce) {
params.page = 1
}
isLoading = true
getDiscuss(params)
.then(res => {
detail.info = res.data.info
detail.list = isForce ? res.data.list : [...detail.list, ...res.data.list]
hasMore = !!res.data.list.length
})
.finally(() => {
isLoading = false
})
}
watch(
() => props.data.id,
() => {
fetchInfo(true)
},
{ immediate: true }
)
// 滚动加载
const scrollRef = ref<HTMLElement>()
useInfiniteScroll(
scrollRef,
() => {
if (!hasMore || isLoading) return
params.page++
fetchInfo()
},
{ distance: 10 }
)
const formRef = $ref<FormInstance>()
const form = reactive<{ content: string }>({ content: '' })
const rules = ref<FormRules>({
content: [{ required: true, message: '请输入回复内容' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(() => {
const params = { ...form, id: props.data.id }
submitDiscussComment(params).then(() => {
ElMessage({ message: '回复成功', type: 'success' })
emit('update')
fetchInfo(true)
formRef?.resetFields()
})
})
}
</script>
<template>
<el-dialog title="大赛训练答疑" :close-on-click-modal="false" @update:modelValue="$emit('update:modelValue')">
<div class="discuss-scroll" ref="scrollRef">
<DiscussItem :info="detail.info" :list="detail.list" v-if="detail.info"></DiscussItem>
<div class="tips" v-if="isLoading">
<el-icon class="is-loading">
<Loading />
</el-icon>
加载中...
</div>
<div class="tips" v-if="!hasMore">没有更多了</div>
</div>
<el-form
ref="formRef"
:rules="rules"
:model="form"
style="margin-top: 40px"
v-permission="'v1-teacher-discussion-comment'"
>
<el-form-item prop="content">
<el-input type="textarea" v-model="form.content" :autosize="{ minRows: 4, maxRows: 6 }" :maxlength="200" />
</el-form-item>
<el-row justify="end">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">取消</el-button>
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存回复</el-button>
</el-row>
</el-form>
</el-dialog>
</template>
<style lang="scss" scoped>
.discuss-scroll {
max-height: 400px;
overflow-y: auto;
}
.tips {
padding: 20px;
color: #555;
text-align: center;
}
.el-icon.is-loading {
animation: rotating 2s linear infinite;
}
</style>
<script setup lang="ts">
import type { DiscussInfo, DiscussCommentItem } from '../types'
import { ChatLineRound } from '@element-plus/icons-vue'
import Avatar from '@/components/Avatar.vue'
interface Props {
info: DiscussInfo
list: DiscussCommentItem[]
}
defineProps<Props>()
defineEmits<{
(e: 'update'): void
}>()
</script>
<template>
<div class="discuss-item">
<div class="discuss-box">
<div class="discuss-box-user">
<div class="discuss-box-user__avatar"><Avatar :src="info.created_operator_avatar" /></div>
<div class="discuss-box-user__content">
<h3>{{ info.student_id_name }}</h3>
<p>{{ info.created_time }}</p>
</div>
<div class="button-comment">
<el-icon><ChatLineRound></ChatLineRound></el-icon>{{ info.replies_num }}
</div>
</div>
<div class="discuss-box-main">
<h3>{{ info.title }}</h3>
<div class="discuss-box-content" v-html="info.content"></div>
</div>
</div>
<div class="discuss-comment" v-for="item in list" :key="item.id">
<div class="discuss-comment__avatar">
<Avatar :src="item.sso_id_avatar" />
</div>
<div class="discuss-comment-content">
<span class="discuss-comment__username">{{ item.sso_id_name }}</span
>
<span v-html="item.content"></span>
<p class="discuss-comment-time">{{ item.created_time }}</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.discuss-box {
padding-bottom: 20px;
border-bottom: 1px solid #e6e6e6;
}
.discuss-box-user {
display: flex;
align-items: center;
.button-comment {
display: flex;
align-items: center;
justify-content: center;
color: #333333;
cursor: pointer;
&.is-active {
color: var(--main-color);
}
}
.el-icon {
font-size: 16px;
margin-left: 10px;
margin-right: 5px;
}
}
.discuss-box-user__avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.discuss-box-user__content {
flex: 1;
margin-left: 10px;
overflow: hidden;
h3 {
font-size: 16px;
font-weight: 500;
line-height: 20px;
color: #333333;
}
p {
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: #999999;
}
}
.discuss-box-main {
margin-left: 50px;
h3 {
padding: 16px 0;
font-size: 16px;
font-weight: 500;
line-height: 20px;
color: #333333;
}
}
.discuss-box-content {
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: #333333;
}
.discuss-comment {
display: flex;
margin-top: 20px;
}
.discuss-comment__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.discuss-comment-content {
flex: 1;
margin-left: 10px;
color: #333;
overflow: hidden;
}
.discuss-comment__username {
font-size: 16px;
line-height: 20px;
color: var(--main-color);
}
.discuss-comment-time {
margin-top: 10px;
font-size: 14px;
line-height: 20px;
color: #999999;
text-align: right;
}
</style>
import { getFilterList } from '../api'
export interface FilterItem {
id: string
name: string
}
// 赛项
const competitions = ref<FilterItem[]>([])
export function useFilterList() {
getFilterList().then(res => {
competitions.value = res.data
})
return { competitions }
}
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/teacher/contest/discuss',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export interface DiscussListItem {
class_id: string
class_id_name: string
course_id: string
course_id_name: string
created_time: string
experiment_id: string
experiment_id_name: string
id: string
is_reply: string
is_reply_name: string
replies_num: number
sno_number: string
specialty_id: string
specialty_id_name: string
student_id: string
student_name: string
title: string
}
export interface DiscussInfo {
content: string
created_operator: string
created_operator_avatar: string
created_time: string
delete_time: string
experiment_id: string
id: string
is_reply: string
replies_num: number
status: string
student_id: string
student_id_name: string
title: string
updated_operator: string
updated_time: string
}
export interface DiscussCommentItem {
content: string
created_time: string
discussion_id: string
id: string
role: string
sso_id: string
sso_id_avatar: string
sso_id_name: string
}
<script setup lang="ts">
import type { DiscussListItem } from '../types'
import AppList from '@/components/base/AppList.vue'
import { getDiscussList } from '../api'
import { useFilterList } from '../composables/useFilterList'
const DiscussDialog = defineAsyncComponent(() => import('../components/DiscussDialog.vue'))
const { competitions } = useFilterList()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getDiscussList,
params: { competition_id: '', student_name: '' }
},
filters: [
{
type: 'select',
prop: 'competition_id',
label: '赛项',
placeholder: '请选择赛项',
options: competitions.value,
labelKey: 'name',
valueKey: 'id'
},
{ type: 'input', prop: 'student_name', label: '选手姓名', placeholder: '请输入选手姓名' }
],
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '赛项名称', prop: 'competition_name' },
{ label: '专业', prop: 'specialty_id_name' },
{ label: '班级', prop: 'class_id_name' },
{ label: '姓名', prop: 'student_name' },
{ label: '学号', prop: 'sno_number' },
{ label: '标题', prop: 'title' },
{ label: '评论数量', prop: 'replies_num', slots: 'table-replies-num' },
{ label: '更新时间', prop: 'created_time' },
{ label: '操作', slots: 'table-x', width: 100 }
]
}
})
let dialogVisible = $ref(false)
const rowData = ref<DiscussListItem>()
// 评论
function handleComment(row: DiscussListItem) {
rowData.value = row
dialogVisible = true
}
function onUpdateSuccess() {
appList?.refetch()
}
</script>
<template>
<AppCard title="训练答疑">
<AppList v-bind="listOptions" ref="appList">
<template #table-replies-num="{ row }">
<span :class="{ 'is-info': !!row.replies_num }">{{ row.replies_num }}</span>
</template>
<template #table-x="{ row }">
<el-button text type="primary" @click="handleComment(row)" v-permission="'v1-teacher-discussion-view'"
>答疑</el-button
>
</template>
</AppList>
</AppCard>
<DiscussDialog
v-model="dialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="dialogVisible && rowData"
></DiscussDialog>
</template>
<style lang="scss" scoped>
.is-success {
color: #63a103;
}
.is-info {
color: #b18862;
}
</style>
import httpRequest from '@/utils/axios'
// 获取实验记录列表
export function getExperimentRecordList(params?: {
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/check/list', { params })
}
// 获取筛选条件
export function getFilterList(params?: { leader?: number }) {
return httpRequest.get('/api/lab/v1/expert/check/condition', { params })
}
// 获取实验记录详情
export function getExperimentRecord(params: { experiment_id: string; student_id: string }) {
return httpRequest.get('/api/lab/v1/teacher/record/view', { params })
}
// 实验记录评分
export function checkExperimentRecord(data: {
experiment_id: string
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>
import { getFilterList } from '../api'
export interface FilterItem {
id: string
name: string
}
// 赛项
const competitions = ref<FilterItem[]>([])
// 学校
const schools = ref<FilterItem[]>([])
// 状态
const status = ref<FilterItem[]>([])
export function useFilterList() {
getFilterList().then(res => {
competitions.value = res.data.competition
schools.value = res.data.school
status.value = res.data.status
})
return { competitions, schools, status }
}
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/teacher/contest/record',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export interface RecordItem {
check_time: string
checker_id?: string
class_id: string
class_name: string
commit_time: string
course_name?: string
experiment_id: string
experiment_name: string
file?: string
pictures?: string
score: string
score_details?: string
sno_number: string
specialty_id: string
specialty_name: string
status: 0 | 1 | 2
status_name: string
student_id: string
student_name: string
}
export interface FileItem {
name: string
url: string
upload_time: string
}
<script setup lang="ts">
import type { RecordItem } from '../types'
import { Refresh, Upload } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import { getExperimentRecordList } from '../api'
import { useFilterList } from '../composables/useFilterList'
const ScoreDialog = defineAsyncComponent(() => import('../components/ScoreDialog.vue'))
const ImportDialog = defineAsyncComponent(() => import('../components/ImportDialog.vue'))
const { competitions, schools, status } = useFilterList()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getExperimentRecordList,
params: {
competition_id: '',
check_status: '',
organ_id: '',
login_id: '',
student_name: ''
}
},
filters: [
{
type: 'select',
prop: 'competition_id',
label: '赛项',
placeholder: '请选择赛项',
options: competitions.value,
labelKey: 'name',
valueKey: 'id'
},
{
type: 'select',
prop: 'check_status',
label: '状态',
placeholder: '请选择是否评分',
options: status.value,
labelKey: 'name',
valueKey: 'id'
},
{
type: 'select',
prop: 'organ_id',
label: '学校',
placeholder: '请选择学校',
options: schools.value,
labelKey: 'name',
valueKey: 'id'
},
{ type: 'input', prop: 'student_name', label: '考生姓名', placeholder: '请输入考生姓名' },
{ type: 'input', prop: 'login_id', label: '考生ID', placeholder: '请输入考生ID' }
],
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '赛项名称', prop: 'competition_id_name' },
{ label: '选手姓名', prop: 'student_name' },
{ label: '参赛ID', prop: 'login_id' },
{ label: '性别', prop: 'gender' },
{ label: '所在学校', prop: 'organ_id_name' },
{ label: '所在专业', prop: 'specialty_id_name' },
{ label: '所在班级', prop: 'class_id_name' },
{ label: '训练次数', prop: 'train_count' },
{ label: '评分规则', prop: 'competition_is_more_status' },
{ label: '已评分人数', prop: 'experiment_name', slots: 'table-count' },
{ label: '已评分', prop: 'checked_flag_name' },
{ label: '得分', prop: 'score', slots: 'table-score' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 100 }
]
}
})
const importVisible = $ref(false)
let dialogVisible = $ref(false)
const rowData = ref<RecordItem>()
// 评分
function handleScore(row: RecordItem) {
rowData.value = row
dialogVisible = true
}
function onUpdateSuccess() {
appList?.refetch()
}
</script>
<template>
<AppCard title="大赛评分">
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button
type="primary"
round
:icon="Refresh"
@click="importVisible = true"
v-permission="'v1-teacher-record-upload'"
>系统同步考试成绩</el-button
>
<el-button
type="primary"
round
:icon="Upload"
@click="importVisible = true"
v-permission="'v1-teacher-record-upload'"
>批量导入考试成绩</el-button
>
<el-button
type="primary"
round
:icon="Upload"
@click="importVisible = true"
v-permission="'v1-teacher-record-upload'"
>批量导入完整评分</el-button
>
</template>
<template #table-count="{ row }"> {{ row.checked_count }}/{{ row.need_check_count }} </template>
<template #table-score="{ row }">
<span :class="{ 'is-info': row.score !== '--' }">{{ row.score }}</span>
</template>
<template #table-x="{ row }">
<el-button
text
type="primary"
v-if="row.checked_flag"
@click="handleScore(row)"
v-permission="'v1-teacher-record-check'"
>评分</el-button
>
</template>
</AppList>
</AppCard>
<!-- 评分 -->
<ScoreDialog
v-model="dialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="dialogVisible && rowData"
></ScoreDialog>
<!-- 批量导入 -->
<ImportDialog v-model="importVisible" @update="onUpdateSuccess" v-if="importVisible"></ImportDialog>
</template>
<style lang="scss" scoped>
.is-success {
color: #63a103;
}
.is-info {
color: #b18862;
}
</style>
import httpRequest from '@/utils/axios'
// 获取实验记录列表
export function getExperimentRecordList(params?: {
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 })
}
// 获取筛选条件
export function getFilterList() {
return httpRequest.get('/api/lab/v1/expert/score/condition')
}
// 获取实验记录详情
export function getExperimentRecord(params: { experiment_id: string; student_id: string }) {
return httpRequest.get('/api/lab/v1/teacher/record/view', { params })
}
// 实验记录评分
export function checkExperimentRecord(data: {
experiment_id: string
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>
import { getFilterList } from '../api'
export interface FilterItem {
id: string
name: string
}
// 赛项
const competitions = ref<FilterItem[]>([])
export function useFilterList() {
getFilterList().then(res => {
competitions.value = res.data
})
return { competitions }
}
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/teacher/contest/score',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export interface RecordItem {
check_time: string
checker_id?: string
class_id: string
class_name: string
commit_time: string
course_name?: string
experiment_id: string
experiment_name: string
file?: string
pictures?: string
score: string
score_details?: string
sno_number: string
specialty_id: string
specialty_name: string
status: 0 | 1 | 2
status_name: string
student_id: string
student_name: string
}
export interface FileItem {
name: string
url: string
upload_time: string
}
<script setup lang="ts">
import type { RecordItem } from '../types'
import AppList from '@/components/base/AppList.vue'
import { getExperimentRecordList } from '../api'
import { useFilterList } from '../composables/useFilterList'
const ScoreDialog = defineAsyncComponent(() => import('../components/ScoreDialog.vue'))
const ImportDialog = defineAsyncComponent(() => import('../components/ImportDialog.vue'))
const { competitions } = useFilterList()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getExperimentRecordList,
params: {
competition_id: ''
}
},
filters: [
{
type: 'select',
prop: 'competition_id',
label: '赛项',
placeholder: '请选择赛项',
options: competitions.value,
labelKey: 'name',
valueKey: 'id'
}
],
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '赛项名称', prop: 'name' },
{ label: '主办单位', prop: 'host_unit_id_name' },
{ label: '赛项类型', prop: 'type_name' },
{ label: '指导老师', prop: 'teachers', slots: 'table-teachers' },
{ label: '报名人数', prop: 'all_competitor_count' },
{ label: '专家人数', prop: 'expert_count' },
{ label: '完赛人数', prop: 'complete_competitor_count' },
{ label: '已评分人数', prop: 'checked_competitor_count' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 240 }
]
}
})
const importVisible = $ref(false)
let dialogVisible = $ref(false)
const rowData = ref<RecordItem>()
// 评分
function handleScore(row: RecordItem) {
rowData.value = row
dialogVisible = true
}
function onUpdateSuccess() {
appList?.refetch()
}
</script>
<template>
<AppCard title="发布成绩">
<AppList v-bind="listOptions" ref="appList">
<template #table-teachers="{ row }"> {{ row.teachers.join() }} </template>
<template #table-x="{ row }">
<el-button round type="primary" @click="handleScore(row)" v-permission="'v1-teacher-record-check'"
>查看成绩</el-button
>
<el-button round type="primary" @click="handleScore(row)" v-permission="'v1-teacher-record-check'"
>发布赛项成绩</el-button
>
</template>
</AppList>
</AppCard>
<!-- 评分 -->
<ScoreDialog
v-model="dialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="dialogVisible && rowData"
></ScoreDialog>
<!-- 批量导入 -->
<ImportDialog v-model="importVisible" @update="onUpdateSuccess" v-if="importVisible"></ImportDialog>
</template>
<style lang="scss" scoped>
.is-success {
color: #63a103;
}
.is-info {
color: #b18862;
}
</style>
......@@ -54,15 +54,15 @@ const teacherMenus: IMenuItem[] = [
children: [
{
name: '训练答疑',
path: '/teacher/contest/items'
path: '/teacher/contest/discuss'
},
{
name: '大赛评分',
path: '/teacher/contest/contestants'
path: '/teacher/contest/record'
},
{
name: '发布成绩',
path: '/teacher/contest/judges'
path: '/teacher/contest/score'
}
]
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论