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

chore: update

上级 0841601b
差异被折叠。
...@@ -52,6 +52,7 @@ ...@@ -52,6 +52,7 @@
"unplugin-auto-import": "^0.11.2", "unplugin-auto-import": "^0.11.2",
"vite": "^3.1.0", "vite": "^3.1.0",
"vite-plugin-checker": "^0.5.1", "vite-plugin-checker": "^0.5.1",
"vite-plugin-mkcert": "^1.9.0",
"vue-tsc": "^0.40.5" "vue-tsc": "^0.40.5"
} }
} }
<script></script>
<template>
<el-button></el-button>
</template>
...@@ -71,7 +71,7 @@ const fetchList = (isReset = false) => { ...@@ -71,7 +71,7 @@ const fetchList = (isReset = false) => {
httpRequest(requestParams) httpRequest(requestParams)
.then((res: any) => { .then((res: any) => {
const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {} const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {}
page.total = total page.total = parseInt(total)
dataList.value = list dataList.value = list
}) })
// .catch(() => { // .catch(() => {
......
export function useCountdown(optionsSecond = 90) {
const second = ref(optionsSecond)
const disabled = ref(false)
let timer: number | null = null
function start() {
disabled.value = true
timer = window.setInterval(() => {
second.value--
if (second.value <= 0) {
stop()
}
}, 1000)
}
function stop() {
timer && clearInterval(timer)
disabled.value = false
}
onUnmounted(() => {
timer && clearInterval(timer)
timer = null
})
return { second, disabled, start, stop }
}
...@@ -16,8 +16,6 @@ import modules from './modules' ...@@ -16,8 +16,6 @@ import modules from './modules'
import { permissionDirective } from '@/utils/permission' import { permissionDirective } from '@/utils/permission'
import { useMapStore } from '@/stores/map'
const app = createApp(App) const app = createApp(App)
// 注册公共组件 // 注册公共组件
app.component('AppCard', AppCard).component('AppList', AppList).component('AppUpload', AppUpload) app.component('AppCard', AppCard).component('AppList', AppList).component('AppUpload', AppUpload)
...@@ -30,5 +28,3 @@ app.use(router) ...@@ -30,5 +28,3 @@ app.use(router)
app.use(ElementPlus, { locale: zhCn }) app.use(ElementPlus, { locale: zhCn })
app.mount('#app') app.mount('#app')
useMapStore().getMapList()
import httpRequest from '@/utils/axios' import httpRequest from '@/utils/axios'
import type { ExperimentCreateItem } from './types' import type { ContestCreateParams, ContestUpdateParams } from './types'
// 获取赛项列表 // 获取赛项列表
export function getContestItemList(params?: { name?: string; page?: number; 'per-page'?: number }) { export function getContestItemList(params?: { page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/list', { params }) return httpRequest.get('/api/resource/v1/backend/competition/list', { params })
} }
// 获取实验详情 // 获取赛项详情
export function getContestItem(params: { experiment_id: string; page?: number; 'per-page'?: number }) { export function getContestItem(params: { id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/view', { params }) return httpRequest.get('/api/resource/v1/backend/competition/detail', { params })
} }
// 创建实验 // 创建赛项
export function createExperiment(data: ExperimentCreateItem) { export function createContest(data: ContestCreateParams) {
return httpRequest.post('/api/resource/v1/backend/experiment/create', data) return httpRequest.post('/api/resource/v1/backend/competition/create', data)
} }
// 更新实验 // 更新赛项
export function updateExperiment(data: ExperimentCreateItem) { export function updateContest(data: ContestUpdateParams) {
return httpRequest.post('/api/resource/v1/backend/experiment/update', data) return httpRequest.post('/api/resource/v1/backend/competition/update', data)
} }
// 获取实验课程列表 // 更新评分规则
export function getExperimentCourseList(params: { organ_id: string }) { export function updateContestRules(data: ContestUpdateParams) {
return httpRequest.get('/api/resource/v1/backend/experiment/courses', { params }) return httpRequest.post('/api/resource/v1/backend/competition-rule/save', data)
} }
// 获取实验指导老师列表 // 获取赛项课程列表
export function getExperimentTeacherList(params: { organ_id: string }) { export function getContestCourseList(params: { organ_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/teachers', { params }) return httpRequest.get('/api/resource/v1/backend/competition/courses', { params })
} }
// 获取实验关联班级列表 // 获取赛项指导老师列表
export function getExperimentClassList(params: { experiment_id: string; page?: number; 'per-page'?: number }) { export function getContestTeacherList(params: { organ_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/class-add', { params }) return httpRequest.get('/api/resource/v1/backend/competition/teachers', { params })
} }
// 实验关联班级 // 获取赛项关联班级列表
export function getContestClassList(params: { experiment_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/competition/class-add', { params })
}
// 赛项关联班级
export function experimentAddClass(data: { experiment_id: string; classes_id: string; type: 'add' | 'delete' }) { export function experimentAddClass(data: { experiment_id: string; classes_id: string; type: 'add' | 'delete' }) {
return httpRequest.post('/api/resource/v1/backend/experiment/class-add', data) return httpRequest.post('/api/resource/v1/backend/competition/class-add', data)
} }
// 获取班级学生列表 // 获取班级学生列表
export function getClassStudentList(params: { class_id: string; page?: number; 'per-page'?: number }) { export function getClassStudentList(params: { class_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/class-students', { params }) return httpRequest.get('/api/resource/v1/backend/competition/class-students', { params })
} }
// 获取班级小组列表 // 获取班级小组列表
export function getExperimentClassGroupsList(params: { experiment_id: string; class_id: string }) { export function getContestClassGroupsList(params: { experiment_id: string; class_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/class-teams', { params }) return httpRequest.get('/api/resource/v1/backend/competition/class-teams', { params })
} }
// 获取实验小组 // 获取赛项小组
export function getExperimentGroup(params: { team_id: string; page?: number; 'per-page'?: number }) { export function getContestGroup(params: { team_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/team-view', { params }) return httpRequest.get('/api/resource/v1/backend/competition/team-view', { params })
} }
// 新增实验小组 // 新增赛项小组
export function experimentAddClassGroup(data: { experiment_id: string; class_id: string; name: string }) { export function experimentAddClassGroup(data: { experiment_id: string; class_id: string; name: string }) {
return httpRequest.post('/api/resource/v1/backend/experiment/team-add', data) return httpRequest.post('/api/resource/v1/backend/competition/team-add', data)
} }
// 实验小组添加学生 // 赛项小组添加学生
export function experimentGroupAddStudent(data: { team_id: string; students_id: string; type: 'add' | 'delete' }) { export function experimentGroupAddStudent(data: { team_id: string; students_id: string; type: 'add' | 'delete' }) {
return httpRequest.post('/api/resource/v1/backend/experiment/team-add-student', data) return httpRequest.post('/api/resource/v1/backend/competition/team-add-student', data)
}
// 获取赛项关联班级列表
export function getContestGroupStudentList(params: { team_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/competition/team-add-student', { params })
} }
// 获取实验关联班级列表 // 获取指导老师列表
export function getExperimentGroupStudentList(params: { team_id: string; page?: number; 'per-page'?: number }) { export function getTeacherList(params?: { name?: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/team-add-student', { params }) return httpRequest.get('/api/resource/v1/backend/teacher/faculty-advisers', { params })
} }
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { ContestItem, ExperimentCreateItem } from '../types' import type { ContestItem } from '../types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { createExperiment, updateExperiment } from '../api' import { updateContestRules } from '../api'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
interface Props {
data?: ContestItem | null
}
const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update'): void (e: 'update'): void
(e: 'update:modelValue', visible: boolean): void (e: 'update:modelValue', visible: boolean): void
}>() }>()
// 数据状态 const detail = $ref<ContestItem>(inject('detail'))
const status = useMapStore().getMapValuesByKey('system_status')
const formRef = $ref<FormInstance>() const formRef = $ref<FormInstance>()
const form = reactive<ExperimentCreateItem>({ const form = reactive({
organ_id: '',
status: '1',
course_id: '',
name: '', name: '',
length: 10, competition_id: '',
type: '', is_more_status: 1,
score: 100, rule_type: 1,
teachers_id: '', lowest_number: undefined,
teachers_ids: [] detail_list: []
}) })
watchEffect(() => {
if (!props.data) return
const score = parseFloat(props.data.score)
const length = parseFloat(props.data.length)
const teachers_ids = props.data.teacher.map(item => item.id)
Object.assign(form, props.data, { score, length, teachers_ids })
})
const rules = ref<FormRules>({ const rules = ref<FormRules>({
name: [{ required: true, message: '请输入赛项名称' }] name: [{ required: true, message: '请输入赛项名称' }]
}) })
const isUpdate = $computed(() => { watchEffect(() => {
return !!form.id form.name = detail.name
form.competition_id = detail.id
}) })
// 提交 // 提交
function handleSubmit() { function handleSubmit() {
formRef?.validate().then(() => { formRef?.validate().then(() => {
const params = { const params = {
...form, ...form
teachers_id: form.teachers_ids?.join(',') || ''
} }
isUpdate ? handleUpdate(params) : handleCreate(params) updateContestRules(params).then(() => {
}) ElMessage({ message: '保存成功', type: 'success' })
}
// 新增
function handleCreate(params: ExperimentCreateItem) {
createExperiment(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
emit('update') emit('update')
emit('update:modelValue', false) emit('update:modelValue', false)
}) })
}
// 修改
function handleUpdate(params: ExperimentCreateItem) {
updateExperiment(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
}) })
} }
</script> </script>
...@@ -83,12 +52,13 @@ function handleUpdate(params: ExperimentCreateItem) { ...@@ -83,12 +52,13 @@ function handleUpdate(params: ExperimentCreateItem) {
@update:modelValue="$emit('update:modelValue')" @update:modelValue="$emit('update:modelValue')"
> >
<el-form ref="formRef" :model="form" :rules="rules" label-width="160px"> <el-form ref="formRef" :model="form" :rules="rules" label-width="160px">
<el-form-item label="赛项名称" prop="name"> <el-form-item label="赛项名称">
<el-input v-model="form.name" :disabled="isUpdate" /> <el-input v-model="form.name" disabled />
</el-form-item> </el-form-item>
<el-form-item label="多人评分" prop="status"> <el-form-item label="多人评分" prop="is_more_status">
<el-radio-group v-model="form.status"> <el-radio-group v-model="form.is_more_status">
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio> <el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="多人评分成绩计算规则" prop="status"> <el-form-item label="多人评分成绩计算规则" prop="status">
...@@ -96,8 +66,8 @@ function handleUpdate(params: ExperimentCreateItem) { ...@@ -96,8 +66,8 @@ function handleUpdate(params: ExperimentCreateItem) {
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio> <el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="最低评分人数" prop="status"> <el-form-item label="最低评分人数" prop="lowest_number">
<el-input v-model="form.name" :disabled="isUpdate" /> <el-input-number v-model="form.lowest_number" step-strictly :controls="false" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-table> <el-table>
......
import { getTeacherList } from '../api'
interface TeacherType {
id: string
name: string
}
const teachers = ref<TeacherType[]>([])
export function useGetTeacherList() {
!teachers.value.length &&
getTeacherList().then((res: any) => {
teachers.value = res.data.list
})
return { teachers }
}
export interface ExperimentItemTeacher { import type { SystemDictionary } from '@/types'
export interface ContestItemTeacher {
id: string id: string
name: string name: string
} }
export interface ContestItem { export interface ContestItem {
course_id: string
course_name: string
created_operator: string
created_operator_name: string
created_time: string
delete_time: string
id: string id: string
length: string
name: string name: string
organ_id: string
organ_id_name: string
project_id: string
project_id_name: string
score: string
status: string
status_name: string
teacher: ExperimentItemTeacher[]
type: string type: string
type_name: string start_range: string
updated_operator: string end_range: string
updated_operator_name: string start_at: string
updated_time: string end_at: string
} apply_expiration_date: string
export interface ExperimentCreateItem {
id?: string
organ_id: string
status: string status: string
course_id: string logo: string
name: string cover: string
length: number train_platform_uri: string
type: string competition_uri: string
score: number publish_status: string
teachers_id: string
teachers_ids?: string[]
}
export interface ClassItem {
code: string
created_operator: string created_operator: string
created_operator_name: string
created_time: string
delete_time: string
id: string
name: string
organ_id: string
organ_id_name: string
project_id: string
project_id_name: string
specialty_id: string
specialty_id_name: string
start_year: string
start_year_name: string
status: string
status_name: string
student_nums: number
teacher_id: string
teacher_id_name: string
updated_operator: string updated_operator: string
updated_operator_name: string
updated_time: string
}
export interface StudentItem {
birthday: string
city: string
city_name: string
class_id: string
class_id_name: string
county: string
county_name: string
created_operator: string
created_operator_name: string
created_time: string created_time: string
delete_time: string
gender: string
gender_name: string
id: string
id_number: string
id_type: string
id_type_name: string
mobile: string
name: string
organ_id: string
organ_id_name: string
project_id: string
project_id_name: string
province: string
province_name: string
sno_number: string
specialty_id: string
specialty_id_name: string
sso_id: string
status: string
status_name: string
updated_operator: string
updated_operator_name: string
updated_time: string updated_time: string
teachers: ContestItemTeacher[]
host_unit: SystemDictionary
technical_support_unit: SystemDictionary
organizers: SystemDictionary[]
apply_count: number
expert_count: number
} }
export interface GroupItem { export interface ContestCreateParams {
class_id: string
created_operator: string
created_time: string
experiment_id: string
id: string
name: string name: string
host_unit_id: string
organizer_ids: string
technical_support_unit_id: string
type: string
start_range: number
end_range: number
start_at: number
end_at: number
apply_expiration_date: number
status: string status: string
student_num: number logo: string
updated_operator: string cover: string
updated_time: string train_platform_uri: string
experiment_name?: string competition_uri: string
course_name?: string teacher_ids: string[]
class_name?: string
} }
export type ContestUpdateParams = ContestCreateParams & { id: string }
...@@ -16,13 +16,25 @@ const listOptions = { ...@@ -16,13 +16,25 @@ const listOptions = {
columns: [ columns: [
{ label: '序号', type: 'index', width: 60 }, { label: '序号', type: 'index', width: 60 },
{ label: '赛项名称', prop: 'name' }, { label: '赛项名称', prop: 'name' },
{ label: '主办单位', prop: 'length' }, { label: '主办单位', prop: 'host_unit.label' },
{ label: '承办单位', prop: 'teacher_names' }, {
{ label: '技术支持单位', prop: 'type_name' }, label: '承办单位',
{ label: '赛项类型', prop: 'score' }, prop: 'organizers',
{ label: '指导老师', prop: 'score' }, computed({ row }: { row: ContestItem }) {
{ label: '报名人数', prop: 'score' }, return row.organizers.map(item => item.label).join(',')
{ label: '专家人数', prop: 'score' }, }
},
{ label: '技术支持单位', prop: 'technical_support_unit.label' },
{ label: '赛项类型', prop: 'type' },
{
label: '指导老师',
prop: 'teachers',
computed({ row }: { row: ContestItem }) {
return row.teachers.map(item => item.name).join(',')
}
},
{ label: '报名人数', prop: 'apply_count' },
{ label: '专家人数', prop: 'expert_count' },
{ label: '更新时间', prop: 'updated_time' }, { label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 200 } { label: '操作', slots: 'table-x', width: 200 }
] ]
......
<script setup lang="ts"> <script setup lang="ts">
import type { ContestItem } from '../types' import type { ContestItem } from '../types'
import { getContestItem } from '../api' import { getContestItem } from '../api'
import { useMapStore } from '@/stores/map'
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 JudgingRulesDialog = defineAsyncComponent(() => import('../components/JudgingRulesDialog.vue')) const JudgingRulesDialog = defineAsyncComponent(() => import('../components/JudgingRulesDialog.vue'))
...@@ -10,12 +12,34 @@ interface Props { ...@@ -10,12 +12,34 @@ interface Props {
} }
const props = defineProps<Props>() const props = defineProps<Props>()
// 赛项类型
const types = useMapStore().getMapValuesByKey('competition_type')
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
let detail = $ref<ContestItem | null>(null) let detail = $ref<ContestItem | null>(null)
provide('detail', $$(detail)) provide('detail', $$(detail))
// 赛项类型
const typeText = $computed(() => {
return types.find(item => item.value == detail?.type)?.label
})
// 承办单位
const orgText = $computed(() => {
return detail?.organizers.map(item => item.label).join('、')
})
// 指导教师
const teacherText = $computed(() => {
return detail?.teachers.map(item => item.name).join('、')
})
// 生效状态
const statusText = $computed(() => {
return status.find(item => item.value == detail?.status)?.label
})
function fetchInfo() { function fetchInfo() {
getContestItem({ experiment_id: props.id }).then(res => { getContestItem({ id: props.id }).then(res => {
detail = res.data detail = res.data.detail
}) })
} }
onMounted(fetchInfo) onMounted(fetchInfo)
...@@ -24,19 +48,32 @@ const judgingRulesVisible = $ref(true) ...@@ -24,19 +48,32 @@ const judgingRulesVisible = $ref(true)
</script> </script>
<template> <template>
<AppCard title="赛项信息"> <AppCard title="查看赛项信息">
<template #header-aside> <template #header-aside>
<el-button type="primary" @click="judgingRulesVisible = true">评分规则</el-button> <el-button type="primary" @click="judgingRulesVisible = true">评分规则</el-button>
<el-button type="primary">评分专家</el-button> <el-button type="primary">评分专家</el-button>
<el-button type="primary">参赛选手</el-button> <el-button type="primary">参赛选手</el-button>
<el-button type="primary">评分细则</el-button> <el-button type="primary">评分细则</el-button>
</template> </template>
<div class="top" v-if="detail">
<el-descriptions class="descriptions-box" v-if="detail"> <div class="top-cover">
<p>赛项封面:</p>
<img :src="detail.cover" />
</div>
<el-descriptions class="descriptions-box">
<el-descriptions-item label="赛项名称:">{{ detail.name }}</el-descriptions-item> <el-descriptions-item label="赛项名称:">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="赛项课程:">{{ detail.course_name }}</el-descriptions-item> <el-descriptions-item label="赛项类型:">{{ typeText }}</el-descriptions-item>
<el-descriptions-item label="所属机构/学校:">{{ detail.organ_id_name }}</el-descriptions-item> <el-descriptions-item label="主办单位:">{{ detail.host_unit.label }}</el-descriptions-item>
<el-descriptions-item label="指导教师:">{{ teacherText }}</el-descriptions-item>
<el-descriptions-item label="承办单位:">{{ orgText }}</el-descriptions-item>
<el-descriptions-item label="赛项周期:"
>{{ detail.start_range }} ~ {{ detail.end_range }}</el-descriptions-item
>
<el-descriptions-item label="技术支持单位:">{{ detail.technical_support_unit.label }}</el-descriptions-item>
<el-descriptions-item label="正式比赛日期:">{{ detail.start_at }} ~ {{ detail.end_at }}</el-descriptions-item>
<el-descriptions-item label="生效状态:">{{ statusText }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</div>
</AppCard> </AppCard>
<AppCard title="训练指导书"> <AppCard title="训练指导书">
<ViewBook></ViewBook> <ViewBook></ViewBook>
...@@ -44,5 +81,27 @@ const judgingRulesVisible = $ref(true) ...@@ -44,5 +81,27 @@ const judgingRulesVisible = $ref(true)
<AppCard title="操作视频"> <AppCard title="操作视频">
<ViewVideo></ViewVideo> <ViewVideo></ViewVideo>
</AppCard> </AppCard>
<JudgingRulesDialog v-model="judgingRulesVisible" v-if="judgingRulesVisible"></JudgingRulesDialog> <JudgingRulesDialog v-model="judgingRulesVisible" v-if="judgingRulesVisible && detail"></JudgingRulesDialog>
</template> </template>
<style lang="scss">
.top {
display: flex;
.el-descriptions {
flex: 1;
margin-top: 30px;
}
}
.top-cover {
width: 200px;
margin-right: 20px;
p {
font-weight: normal;
line-height: 30px;
font-size: 14px;
}
img {
width: 100%;
}
}
</style>
...@@ -23,7 +23,7 @@ function handleChange(id: string, type: number) { ...@@ -23,7 +23,7 @@ function handleChange(id: string, type: number) {
<div class="bg"> <div class="bg">
<router-link to="/student/lab" class="link1"></router-link> <router-link to="/student/lab" class="link1"></router-link>
<router-link to="/student/lab" class="link2"></router-link> <router-link to="/student/lab" class="link2"></router-link>
<router-link to="/student/lab" class="link3"></router-link> <router-link to="/student/contest" class="link3"></router-link>
</div> </div>
<div class="select-group"> <div class="select-group">
<el-select size="large" placeholder="我的实验课程" @change="handleChange($event, 1)"> <el-select size="large" placeholder="我的实验课程" @change="handleChange($event, 1)">
...@@ -31,13 +31,13 @@ function handleChange(id: string, type: number) { ...@@ -31,13 +31,13 @@ function handleChange(id: string, type: number) {
</el-select> </el-select>
<!-- <el-select size="large" placeholder="我的陪练项目" @change="handleChange($event, 2)"> <!-- <el-select size="large" placeholder="我的陪练项目" @change="handleChange($event, 2)">
<el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select> -->
<el-select size="large" placeholder="我的大赛项目" @change="handleChange($event, 3)"> <el-select size="large" placeholder="我的大赛项目" @change="handleChange($event, 3)">
<el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
<el-select size="large" placeholder="我的大赛成绩" @change="handleChange($event, 4)"> <el-select size="large" placeholder="我的大赛成绩" @change="handleChange($event, 4)">
<el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> --> </el-select>
</div> </div>
</template> </template>
......
import httpRequest from '@/utils/axios'
import type { ContestJoinParams } from './types'
// 获取学员可以报名的所有赛项
export function getContestList() {
return httpRequest.get('/api/lab/v1/student/competition/list')
}
// 获取学员已报名的赛项列表
export function getMyContestList() {
return httpRequest.get('/api/lab/v1/student/competition/my-list')
}
// 获取学员详情
export function getStudentInfo() {
return httpRequest.get('/api/lab/v1/student/competition/student-detail')
}
// 分发参赛短信
export function sendApplySMS(data: { competition_id: string; mobile: string }) {
return httpRequest.post('/api/lab/v1/student/competition/send-apply-sms', data)
}
// 大赛报名
export function joinContest(data: ContestJoinParams) {
return httpRequest.post('/api/lab/v1/student/competition/apply', data)
}
// 获取实验指导书
export function getExperimentBook(params: { competition_id: string }) {
return httpRequest.get('/api/lab/v1/student/competition/book', { params })
}
// 获取实验视频
export function getExperimentVideoList(params: { competition_id: string }) {
return httpRequest.get('/api/lab/v1/student/competition/videos', { params })
}
// 获取实验视频播放信息
export function getExperimentVideoPlayInfo(params: { source_id: string }) {
return httpRequest.get('/api/lab/v1/student/competition/replay-list', { params })
}
// 获取实验讨论交流
export function getExperimentDiscussList(params: {
competition_id: string
tag: number
page?: number
'per-page'?: number
}) {
return httpRequest.get('/api/lab/v1/student/competition/discussion-list', { params })
}
// 发表新话题
export function addExperimentDiscuss(data: { competition_id: string; title: string; content: string }) {
return httpRequest.post('/api/lab/v1/student/competition/discussion-create', data)
}
// 发表回复
export function addExperimentDiscussComment(data: { discussion_id: string; content: string }) {
return httpRequest.post('/api/lab/v1/student/competition/discussion-reply-create', data)
}
// 获取实验截图记录
export function getExperimentRecord(params: { competition_id: string }) {
return httpRequest.get('/api/lab/v1/student/competition/pictures', { params })
}
// 截图
export function uploadExperimentPicture(data: { competition_id: string; pictures: string }) {
return httpRequest.post('/api/lab/v1/student/competition/save-pictures', data)
}
<script setup lang="ts">
import type { ExperimentBookType } from '../types'
import Preview from '@/components/Preview.vue'
import { getExperimentBook } from '../api'
interface Props {
competition_id: string
}
const props = defineProps<Props>()
let detail = $ref<ExperimentBookType>()
function fetchInfo() {
getExperimentBook({ competition_id: props.competition_id }).then(res => {
detail = res.data.detail
})
}
watchEffect(() => {
fetchInfo()
})
const isEmpty = $computed(() => {
return !props.competition_id || !detail?.id
})
</script>
<template>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<Preview :url="detail.url"></Preview>
</template>
</template>
<script setup lang="ts">
import type { Contest } from '../types'
interface Props {
data: Contest
}
const props = defineProps<Props>()
const isMy = $computed(() => {
return !!props.data.login_id
})
</script>
<template>
<div class="contest-item">
<div class="contest-item-main">
<div class="contest-item-main__bg"><img :src="data.cover" /></div>
<div class="contest-item-main__inner">
<img :src="data.logo" class="logo" />
<div class="cover">
<router-link :to="`/student/contest/lab/${data.id}`" target="_blank" v-if="isMy">
<el-button round type="primary">我要训练</el-button>
</router-link>
<router-link :to="{ path: '/student/contest/join', query: { id: data.id, name: data.name } }" v-else>
<el-button round type="primary">我要报名</el-button>
</router-link>
</div>
</div>
</div>
<div class="contest-item-name">{{ data.name }}</div>
</div>
</template>
<style lang="scss">
.contest-item-main {
position: relative;
height: 200px;
border-radius: 10px;
overflow: hidden;
}
.contest-item-main__bg {
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.5;
}
}
.contest-item-main__inner {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1;
.logo {
width: 100%;
height: 100%;
object-fit: cover;
}
&:hover {
.cover {
display: flex;
}
}
.cover {
display: none;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
}
}
.contest-item-name {
margin-top: 10px;
padding: 10px;
font-size: 16px;
color: #fff;
text-align: center;
background-color: var(--main-color);
border-radius: 10px;
}
</style>
<script setup lang="ts">
import type { ExperimentDiscussType } from '../types'
import { Loading } from '@element-plus/icons-vue'
import { useInfiniteScroll } from '@vueuse/core'
import DiscussItem from './DiscussItem.vue'
import { getExperimentDiscussList } from '../api'
const DiscussPublish = defineAsyncComponent(() => import('./DiscussPublish.vue'))
interface Props {
competition_id: string
}
const props = defineProps<Props>()
const params = reactive({ tag: 1, page: 1, 'per-page': 10 })
let list = $ref<ExperimentDiscussType[]>([])
let hasMore = $ref(false)
let isLoading = $ref(false)
function fetchInfo(isForce = false) {
if (!props.competition_id) return
if (isForce) {
params.page = 1
}
isLoading = true
getExperimentDiscussList({ ...params, competition_id: props.competition_id })
.then(res => {
list = isForce ? res.data.list : [...list, ...res.data.list]
hasMore = !!res.data.list.length
})
.finally(() => {
isLoading = false
})
}
watch(
() => props.competition_id,
() => {
fetchInfo(true)
},
{ immediate: true }
)
const isEmpty = $computed(() => {
return !props.competition_id || !list.length
})
// 滚动加载
const scrollRef = ref<HTMLElement>()
useInfiniteScroll(
scrollRef,
() => {
if (!hasMore || isLoading) return
params.page++
fetchInfo()
},
{ distance: 10 }
)
</script>
<template>
<div class="discuss">
<el-radio-group size="small" v-model="params.tag" @change="fetchInfo(true)">
<el-radio :label="1">我的话题</el-radio>
<!-- <el-radio :label="2">我回复的</el-radio>
<el-radio :label="3">我的小组</el-radio>
<el-radio :label="4">我的班级</el-radio> -->
</el-radio-group>
<!-- 发表新话题 -->
<DiscussPublish :competition_id="competition_id" @update="fetchInfo(true)" v-if="competition_id"></DiscussPublish>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<div class="discuss-scroll" ref="scrollRef">
<DiscussItem v-for="item in list" :key="item.id" :data="item" @update="fetchInfo(true)"></DiscussItem>
<div class="tips" v-if="isLoading">
<el-icon class="is-loading">
<Loading />
</el-icon>
加载中...
</div>
<div class="tips" v-if="!hasMore">没有更多了</div>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.discuss {
display: flex;
height: 100%;
flex-direction: column;
.el-radio {
margin-right: 12px;
}
}
.discuss-scroll {
flex: 1;
overflow-y: auto;
}
.tips {
padding: 40px;
color: #555;
text-align: center;
}
.el-icon.is-loading {
animation: rotating 2s linear infinite;
}
</style>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { ExperimentDiscussType } from '../types'
import { ElMessage } from 'element-plus'
import { addExperimentDiscussComment } from '../api'
interface Props {
data: ExperimentDiscussType
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
}>()
const formRef = $ref<FormInstance>()
const form = reactive({ content: '' })
const rules = ref<FormRules>({
content: [{ required: true, message: '请输入话题评论内容', trigger: 'blur' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(handleAdd)
}
// 发布评论
function handleAdd() {
const params = { ...form, discussion_id: props.data.id }
addExperimentDiscussComment(params).then(() => {
ElMessage({ message: '评论成功', type: 'success' })
emit('update')
formRef?.resetFields()
})
}
</script>
<template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
style="border-top: 1px dashed #e6e6e6; border-bottom: 1px dashed #e6e6e6; padding: 20px 0"
>
<el-form-item prop="content">
<el-input type="textarea" v-model="form.content" :autosize="{ minRows: 4, maxRows: 6 }" />
</el-form-item>
<el-row justify="end">
<el-button type="primary" @click="handleSubmit">评论</el-button>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import type { ExperimentDiscussType } from '../types'
import { ChatLineRound } from '@element-plus/icons-vue'
import Avatar from '@/components/Avatar.vue'
const DiscussCommentPublish = defineAsyncComponent(() => import('./DiscussCommentPublish.vue'))
interface Props {
data: ExperimentDiscussType
}
defineProps<Props>()
defineEmits<{
(e: 'update'): void
}>()
const commentVisible = $ref(false)
</script>
<template>
<div class="discuss-item">
<div class="discuss-box">
<div class="discuss-box-user">
<div class="discuss-box-user__avatar"><Avatar :src="data.sso_user.avatar" /></div>
<div class="discuss-box-user__content">
<h3>{{ data.sso_user.real_name || data.sso_user.nickname || data.sso_user.username }}</h3>
<p>{{ data.created_time }}</p>
</div>
</div>
<div class="discuss-box-main">
<h3>{{ data.title }}</h3>
<div class="discuss-box-content" v-html="data.content"></div>
</div>
<div class="discuss-box-footer">
<div class="button-comment" :class="{ 'is-active': commentVisible }" @click="commentVisible = !commentVisible">
<el-icon><ChatLineRound></ChatLineRound></el-icon>{{ data.reply_count }}
</div>
</div>
</div>
<template v-if="commentVisible">
<!-- 我要评论 -->
<DiscussCommentPublish :data="data" @update="$emit('update')"></DiscussCommentPublish>
<div class="discuss-comment" v-for="item in data.competitionDiscussionReplies" :key="item.id">
<div class="discuss-comment__avatar">
<Avatar :src="item.sso_user.avatar" />
</div>
<div class="discuss-comment-content">
<span class="discuss-comment__username">{{
item.sso_user.real_name || item.sso_user.nickname || item.sso_user.username
}}</span
>
<span v-html="item.content"></span>
<p class="discuss-comment-time">{{ item.created_time }}</p>
</div>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.discuss-item {
margin-top: 10px;
border-top: 1px solid #e6e6e6;
}
.discuss-box {
padding: 20px 0 10px;
}
.discuss-box-user {
display: flex;
align-items: center;
}
.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-box-footer {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 10px;
.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-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>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { addExperimentDiscuss } from '../api'
interface Props {
competition_id: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
}>()
const formRef = $ref<FormInstance>()
const form = reactive({ title: '', content: '' })
const rules = ref<FormRules>({
title: [{ required: true, message: '请输入话题标题' }],
content: [{ required: true, message: '请输入话题描述' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(handleAdd)
}
// 创建话题
function handleAdd() {
const params = { ...form, competition_id: props.competition_id }
addExperimentDiscuss(params).then(() => {
ElMessage({ message: '发表成功', type: 'success' })
emit('update')
formRef?.resetFields()
})
}
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" style="padding: 20px 0 10px">
<el-form-item prop="title">
<el-input v-model="form.title" placeholder="话题标题"></el-input>
</el-form-item>
<el-form-item prop="content">
<el-input type="textarea" v-model="form.content" placeholder="话题描述" :autosize="{ minRows: 4, maxRows: 6 }" />
</el-form-item>
<el-row justify="end">
<el-button type="primary" @click="handleSubmit">发表</el-button>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import type { ExperimentRecord } from '../types'
import { ElMessageBox } from 'element-plus'
import ImageViewer from '@/components/ImageViewer.vue'
import { uploadExperimentPicture } from '../api'
interface Props {
competition_id: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
}>()
const detail = $ref<ExperimentRecord>(inject('detail'))
const isEmpty = $computed(() => {
return !props.competition_id || !detail
})
let imageViewerVisible = $ref<boolean>(false)
let imageViewerIndex = $ref<number>(0)
// 查看
function handlePreview(index: number) {
imageViewerVisible = true
imageViewerIndex = index
}
// 删除
function handleRemove(index: number) {
ElMessageBox.confirm('删除之后无法恢复,确认删除该截图吗?', '提示').then(() => {
const pictures = detail.pictures.filter((item, i) => i !== index)
uploadExperimentPicture({ competition_id: props.competition_id, pictures: JSON.stringify(pictures) }).then(() => {
emit('update')
})
})
}
</script>
<template>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<h3>实验过程</h3>
<ul class="picture-list">
<li v-for="(item, index) in detail.pictures" :key="item.url">
<img :src="item.url" />
<p>截图时间:{{ item.upload_time }}</p>
<div class="cover">
<div class="cover-inner">
<el-button type="primary" plain round @click="handlePreview(index)">查看</el-button>
<el-button type="primary" plain round @click="handleRemove(index)">删除</el-button>
</div>
</div>
</li>
</ul>
<ImageViewer v-model="imageViewerVisible" :index="imageViewerIndex" :items="detail.pictures"></ImageViewer>
</template>
</template>
<style lang="scss" scoped>
.result-score {
margin-bottom: 40px;
text-align: center;
h2 {
font-size: 18px;
font-weight: 400;
line-height: 30px;
color: #333333;
text-align: center;
}
.t1 {
margin: 20px auto 10px;
padding-top: 64px;
width: 80px;
height: 110px;
font-size: 24px;
font-weight: bold;
line-height: 1;
color: #ffffff;
background: url(@/assets/images/score_bg.png) no-repeat;
background-size: contain;
box-sizing: border-box;
}
.t2 {
font-size: 14px;
line-height: 24px;
color: #666666;
}
}
h3 {
font-size: 16px;
font-weight: 400;
line-height: 27px;
color: #333333;
}
.picture-list {
li {
position: relative;
height: 200px;
margin: 20px 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
p {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 0 10px;
font-size: 14px;
color: #fff;
line-height: 30px;
text-align: right;
background-color: rgba(0, 0, 0, 0.5);
}
&:hover {
.cover {
display: block;
}
}
}
.cover {
display: none;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.cover-inner {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.el-button {
width: 100px;
margin: 10px 0;
}
}
}
</style>
<script setup lang="ts">
import type { ExperimentVideoType } from '../types'
import VideoItem from './VideoItem.vue'
import { getExperimentVideoList } from '../api'
interface Props {
competition_id: string
}
const props = defineProps<Props>()
let list = $ref<ExperimentVideoType[]>([])
function fetchInfo() {
if (!props.competition_id) return
getExperimentVideoList({ competition_id: props.competition_id }).then(res => {
list = res.data.list
})
}
watchEffect(() => {
fetchInfo()
})
const isEmpty = $computed(() => {
return !props.competition_id || !list.length
})
</script>
<template>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<VideoItem v-for="item in list" :key="item.id" :data="item" :competition_id="competition_id"></VideoItem>
</template>
</template>
<style lang="scss" scoped></style>
<script setup lang="ts">
import type { ExperimentVideoType, PlayInfo } from '../types'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import { getExperimentVideoPlayInfo } from '../api'
// import { useLog } from '@/composables/useLog'
// const log = useLog()
interface Props {
competition_id: string
data: ExperimentVideoType
}
const props = defineProps<Props>()
let playList = $ref<PlayInfo[]>([])
function fetchInfo() {
getExperimentVideoPlayInfo({ source_id: props.data.source_id }).then(res => {
playList = res.data.play_info_list
// 日志上送
// log.upload({
// event: 'video_event',
// action: 'experiment_video_stu_watch_action',
// data: {
// competition_id: props.competition_id,
// course_id: props.course_id,
// video_id: props.data.id
// }
// })
})
}
const playUrl = $computed(() => {
return playList[0]?.PlayURL || ''
})
onMounted(() => {
fetchInfo()
})
</script>
<template>
<div class="video-item">
<h2>{{ data.name }}</h2>
<!-- <img :src="data.cover" /> -->
<div style="height: 200px" v-if="playUrl">
<AppVideoPlayer
:options="{ sources: [{ src: playUrl, type: 'application/x-mpegURL' }] }"
style="width: 100%; height: 100%"
></AppVideoPlayer>
</div>
</div>
</template>
<style lang="scss" scoped>
.video-item {
h2 {
font-size: 14px;
font-weight: 500;
color: #333;
padding: 10px 0;
text-align: center;
}
img {
width: 100%;
height: 200px;
object-fit: cover;
}
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/student/contest',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: 'join', component: () => import('./views/Join.vue') },
{ path: 'lab/:id', component: () => import('./views/Lab.vue'), props: true }
]
}
]
import type { SystemDictionary } from '@/types'
export interface Contest {
apply_expiration_date: string
competition_uri: string
cover: string
end_at: string
end_range: string
host_unit: SystemDictionary
id: string
login_id: string
logo: string
name: string
org_name: string
organizers: SystemDictionary[]
start_at: string
start_range: string
student_id: string
student_name: string
technical_support_unit: SystemDictionary
train_platform_uri: string
type: string
}
export type ContestJoinParams = {
competition_id: string
mode: string
picture: string
grade: string
teacher_name: string
sms_code: string
}
export type ExperimentBookType = {
id: string
name: string
competition_id: string
type: string
url: string
}
export interface ExperimentVideoType {
id: string
name: string
size: string
type: string
source_id: string
length: number
cover: string
}
export interface PlayInfo {
BitDepth: number
Bitrate: string
CreationTime: string
Definition: string
Duration: string
Encrypt: number
Format: string
Fps: string
HDRType: string
Height: number
JobId: string
ModificationTime: string
NarrowBandType: string
PlayURL: string
PreprocessStatus: string
Size: number
Specification: string
Status: string
StreamType: string
Width: number
}
export interface ExperimentDiscussType {
content: string
created_operator: string
created_time: string
id: string
is_reply: string
competitionDiscussionReplies: ExperimentDiscussCommentType[]
reply_count: number
sso_user: UserType
student_id: string
title: string
updated_time: string
}
export interface ExperimentDiscussCommentType {
content: string
created_time: string
discussion_id: string
id: string
role: string
sso_id: string
sso_user: UserType
}
export interface UserType {
avatar: string
id: string
nickname: string
real_name: string
username: string
}
export type ExperimentRecord = {
competition_id: string
created_time: string
id: string
pictures: ExperimentRecordFile[]
student_id: string
updated_time: string
}
export interface ExperimentRecordFile {
url: string
name: string
upload_time: string
size?: number
}
<script setup lang="ts">
import type { Contest } from '../types'
import ContestItem from '../components/ContestItem.vue'
import { getMyContestList, getContestList } from '../api'
let myContestList = $ref<Contest[]>([])
function fetchMyList() {
getMyContestList().then(res => {
myContestList = res.data.list
})
}
let contestList = $ref<Contest[]>([])
function fetchList() {
getContestList().then(res => {
contestList = res.data.list
})
}
onMounted(() => {
fetchMyList()
fetchList()
})
</script>
<template>
<AppCard title="我的赛项">
<div class="contest-list" v-if="myContestList.length">
<ContestItem :data="item" v-for="item in myContestList" :key="item.id"></ContestItem>
</div>
<el-empty description="暂无数据" v-else />
</AppCard>
<AppCard title="可参与赛项">
<div class="contest-list" v-if="contestList.length">
<ContestItem :data="item" v-for="item in contestList" :key="item.id"></ContestItem>
</div>
<el-empty description="暂无数据" v-else />
</AppCard>
</template>
<style lang="scss">
.contest-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
</style>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { ContestJoinParams } from '../types'
import AppUpload from '@/components/base/AppUpload.vue'
import { pick } from 'lodash-es'
import { contestModeList } from '@/utils/dictionary'
import { useMapStore } from '@/stores/map'
import { useCountdown } from '@/composables/useCountdown'
import { joinContest, getStudentInfo, sendApplySMS } from '../api'
const { second, disabled, start } = useCountdown()
const countdownText = $computed(() => {
return disabled.value ? `(${second.value})秒` : '点击获取'
})
// 发送验证码
function sendSMS() {
start()
sendApplySMS({ competition_id: form.competition_id, mobile: form.mobile }).catch(() => {
stop()
})
}
const router = useRouter()
const route = useRoute()
// 性别
const genderList = useMapStore().getMapValuesByKey('system_gender')
// 证件类型
const systemIdTypeList = useMapStore().getMapValuesByKey('system_id_type')
function fetchStudentInfo() {
getStudentInfo().then(res => {
const { student = {}, specialty = {}, org = {}, classes = [] } = res.data
const [studentClass = {}] = classes
Object.assign(form, {
student_name: student.name,
gender: student.gender.toString(),
id_type: student.id_type.toString(),
id_number: student.id_number,
mobile: student.mobile,
org_name: org.name,
province_name: student.info?.province_name,
city_name: student.info?.city_name,
county_name: student.info?.county_name,
specialty_name: specialty.name,
class_name: studentClass.name
})
})
}
onMounted(fetchStudentInfo)
const formRef = $ref<FormInstance>()
const form = reactive({
name: route.query.name as string,
competition_id: route.query.id as string,
mode: '1',
student_name: '',
gender: '1',
id_type: '1',
id_number: '',
mobile: '',
org_name: '',
province_name: '',
city_name: '',
county_name: '',
specialty_name: '',
grade: '',
class_name: '',
teacher_name: '',
picture: '',
sms_code: '',
protocol: false
})
const checkProtocol = (rule: any, value: any, callback: any) => {
if (!value) {
return callback(new Error('请确认'))
} else {
callback()
}
}
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入实验指导书名称', trigger: 'blur' }],
grade: [{ required: true, message: '请输入年级' }],
teacher_name: [{ required: true, message: '请输入指导教师' }],
picture: [{ required: true, message: '请上传证件照' }],
sms_code: [{ required: true, message: '请输入验证码' }],
protocol: [{ validator: checkProtocol, trigger: 'change' }]
})
let dialogVisible = $ref(false)
// 提交
function handleSubmit() {
formRef?.validate().then(handleJoin)
}
// 报名
function handleJoin() {
const params: ContestJoinParams = pick(form, [
'competition_id',
'mode',
'picture',
'grade',
'teacher_name',
'sms_code'
])
joinContest(params).then(() => {
dialogVisible = true
})
}
function handleCancel() {
router.replace('/student/contest')
}
</script>
<template>
<AppCard title="大赛报名">
<el-form ref="formRef" :model="form" :rules="rules" label-width="124px" style="width: 600px; margin: 0 auto">
<el-form-item label="报名赛项">
<el-input v-model="form.name" disabled />
</el-form-item>
<el-form-item label="参赛形式" prop="mode">
<el-radio-group v-model="form.mode">
<el-radio v-for="item in contestModeList" :key="item.value" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="form.student_name" disabled />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="form.gender" disabled>
<el-radio v-for="item in genderList" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="证件类型">
<el-radio-group v-model="form.id_type" disabled>
<el-radio v-for="item in systemIdTypeList" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="证件号码">
<el-input v-model="form.id_number" disabled />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.mobile" disabled />
</el-form-item>
<el-form-item label="学校名称">
<el-input v-model="form.org_name" disabled />
</el-form-item>
<el-form-item label="省">
<el-input v-model="form.province_name" disabled />
</el-form-item>
<el-form-item label="市">
<el-input v-model="form.city_name" disabled />
</el-form-item>
<el-form-item label="区/县">
<el-input v-model="form.county_name" disabled />
</el-form-item>
<el-form-item label="专业名称">
<el-input v-model="form.specialty_name" disabled />
</el-form-item>
<el-form-item label="年级" prop="grade">
<el-input v-model="form.grade" />
</el-form-item>
<el-form-item label="班级">
<el-input v-model="form.class_name" disabled />
</el-form-item>
<el-form-item label="指导教师" prop="teacher_name">
<el-input v-model="form.teacher_name" />
</el-form-item>
<el-form-item label="证件照" prop="picture">
<AppUpload v-model="form.picture" accept="image/*">
<template #tip>证件照说明:请上传蓝底1寸或蓝底2寸证件照,照片大小控制在500kb以内。</template>
</AppUpload>
</el-form-item>
<el-form-item label="验证码" prop="sms_code">
<el-input v-model="form.sms_code">
<template #append>
<el-button type="primary" :disabled="disabled" @click="sendSMS">{{ countdownText }}</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item prop="protocol">
<el-checkbox label="我已确认上述信息输入无误,并对上述信息填写内容负责。" v-model="form.protocol" />
</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="handleCancel">取消</el-button>
</el-row>
</el-form>
</AppCard>
<el-dialog title="报名成功" v-model="dialogVisible" :width="400" align-center center @closed="handleCancel">
<el-result icon="success" title="您已经成功报名赛项!">
<template #extra>
<el-button type="primary" @click="handleCancel">确定</el-button>
</template>
</el-result>
</el-dialog>
</template>
<script setup lang="ts">
import type { ExperimentRecord } from '../types'
import { HomeFilled } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { upload } from '@/utils/upload'
import { getExperimentRecord, uploadExperimentPicture } from '../api'
import dayjs from 'dayjs'
const Book = defineAsyncComponent(() => import('../components/Book.vue'))
const Video = defineAsyncComponent(() => import('../components/Video.vue'))
const Discuss = defineAsyncComponent(() => import('../components/Discuss.vue'))
const Result = defineAsyncComponent(() => import('../components/Result.vue'))
interface Props {
id: string
}
const props = defineProps<Props>()
// 左侧
const leftPanelVisible = $ref<boolean>(true)
let detail = $ref<ExperimentRecord>()
provide('detail', $$(detail))
function fetchInfo() {
getExperimentRecord({ competition_id: props.id }).then(res => {
const data = res.data.detail || {}
let pictures = []
try {
pictures = JSON.parse(data.pictures)
} catch (error) {
console.log(error)
}
detail = Object.assign(data, { pictures })
})
}
watchEffect(() => {
fetchInfo()
})
// 右侧
const LAB_URL = import.meta.env.VITE_LAB_URL
let iframeKey = $ref(Date.now())
// 返回首页
function handleBackHome() {
ElMessageBox.confirm('此操作将会强制返回到实验室首页,您当前的操作内容有可能丢失,确定返回首页吗?', '提示').then(
() => {
iframeKey = Date.now()
}
)
}
const iframeRef = $ref<HTMLIFrameElement>()
let screenshotLoading = $ref(false)
let screenshotTimestamp = $ref(0)
// 截图
function handleCapture() {
const pictures = detail?.pictures || []
if (pictures.length >= 20) {
ElMessage.error('已达到单个实验最大截图数量,无法截图,请在过程与结果功能中删除多余截图之后再进行操作。')
return
}
const iframeWindow = iframeRef?.contentWindow
if (!iframeWindow) return
screenshotTimestamp = Date.now()
screenshotLoading = true
// 发送截图消息
iframeWindow?.postMessage({ action: 'screenshot', timestamp: screenshotTimestamp }, '*')
}
// 截图之后
function handleCaptureCallback(event: MessageEvent) {
const { data } = event
if (data.action === 'screenshot' && data.timestamp === screenshotTimestamp) {
upload(data.blob).then(url => {
uploadPicture(url)
})
}
}
onMounted(() => {
window.addEventListener('message', handleCaptureCallback, false)
})
onUnmounted(() => {
window.removeEventListener('message', handleCaptureCallback, false)
})
// 上传截图
function uploadPicture(url: string) {
const pictures = detail?.pictures || []
pictures.unshift({ url, name: Date.now() + '.png', upload_time: dayjs().format('YYYY-MM-DD HH:mm:ss'), size: 1024 })
uploadExperimentPicture({ competition_id: props.id, pictures: JSON.stringify(pictures) }).then(() => {
fetchInfo()
screenshotLoading = false
})
}
</script>
<template>
<section class="lab">
<div class="lab-left" :class="{ 'is-hidden': !leftPanelVisible }">
<div class="lab-left__inner">
<el-tabs type="border-card" stretch>
<el-tab-pane label="实训指导" lazy>
<Book :competition_id="id"></Book>
</el-tab-pane>
<el-tab-pane label="操作视频" lazy>
<Video :competition_id="id"></Video>
</el-tab-pane>
<el-tab-pane label="讨论交流" lazy>
<Discuss :competition_id="id"></Discuss>
</el-tab-pane>
<el-tab-pane label="过程与结果" lazy>
<Result :competition_id="id" @update="fetchInfo"></Result>
</el-tab-pane>
</el-tabs>
</div>
<div class="panel-icon" @click="leftPanelVisible = !leftPanelVisible">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 86" aria-hidden="true" width="16" height="86">
<g fill="none" fill-rule="evenodd">
<path
class="path-wapper"
d="M0 0l14.12 8.825A4 4 0 0116 12.217v61.566a4 4 0 01-1.88 3.392L0 86V0z"
fill="#e1e4eb"
></path>
<path
class="path-arrow"
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-rule="nonzero"
></path>
</g>
</svg>
</div>
</div>
<div class="lab-right">
<AppCard>
<el-row justify="space-between">
<el-button type="primary" :icon="HomeFilled" @click="handleBackHome">返回首页</el-button>
<div>
<el-button type="primary" :loading="screenshotLoading" @click="handleCapture">截图</el-button>
</div>
</el-row>
</AppCard>
<div class="lab-box">
<iframe :src="LAB_URL" :key="iframeKey" frameborder="0" class="iframe" ref="iframeRef"></iframe>
</div>
</div>
</section>
</template>
<style lang="scss" scoped>
.lab {
display: flex;
height: calc(100vh - 110px);
}
.lab-left {
position: relative;
width: 400px;
padding: 20px;
background-color: #e1e4eb;
border-radius: 6px;
box-sizing: border-box;
transition: all 0.1s;
&.is-hidden {
width: 0;
padding: 0;
.panel-icon {
left: -20px;
right: 0;
}
.path-arrow {
transform-origin: center center;
transform: rotate(180deg);
}
}
.lab-left__inner {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
}
.el-tabs {
flex: 1;
border: 0;
overflow: hidden;
}
:deep(.el-tabs__header) {
background-color: #e1e4eb;
}
:deep(.el-tabs__item) {
padding: 0 14px !important;
border: 0;
border-radius: 6px 6px 0px 0px;
}
:deep(.el-tabs__content) {
height: calc(100% - 40px);
box-sizing: border-box;
}
:deep(.el-tab-pane) {
height: 100%;
overflow-y: auto;
}
}
.panel-icon {
position: absolute;
top: 50%;
right: -16px;
width: 16px;
height: 86px;
transform: translateY(-50%);
z-index: 100;
cursor: pointer;
}
.lab-right {
margin-left: 20px;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
}
.lab-box {
flex: 1;
width: 100%;
margin-top: 20px;
}
.iframe {
width: 100%;
height: 100%;
}
</style>
...@@ -12,7 +12,7 @@ const studentMenus: IMenuItem[] = [ ...@@ -12,7 +12,7 @@ const studentMenus: IMenuItem[] = [
{ {
name: '智能营销', name: '智能营销',
path: '/student/lab' path: '/student/lab'
} },
// { // {
// name: '智能陪练', // name: '智能陪练',
// path: '/student/ai' // path: '/student/ai'
...@@ -21,10 +21,10 @@ const studentMenus: IMenuItem[] = [ ...@@ -21,10 +21,10 @@ const studentMenus: IMenuItem[] = [
// name: '成绩分析', // name: '成绩分析',
// path: '/admin/contest/score' // path: '/admin/contest/score'
// }, // },
// { {
// name: '技能大赛', name: '技能大赛',
// path: '/student/contest' path: '/student/contest'
// } }
] ]
// 教师、管理员菜单 // 教师、管理员菜单
const adminMenus: IMenuItem[] = [ const adminMenus: IMenuItem[] = [
......
import type { UserType, ProjectType, OrganizationType, RoleType, PermissionType } from '@/types'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getUser, logout } from '@/api/base' import { getUser, logout } from '@/api/base'
import type { UserType, ProjectType, OrganizationType, RoleType, PermissionType } from '@/types' import { useMapStore } from '@/stores/map'
// 角色信息(1学员;5教师) // 角色信息(1学员;5教师)
interface Role { interface Role {
...@@ -40,6 +41,7 @@ export const useUserStore = defineStore({ ...@@ -40,6 +41,7 @@ export const useUserStore = defineStore({
this.project = project this.project = project
this.roles = roles this.roles = roles
this.permissions = permissions this.permissions = permissions
await useMapStore().getMapList()
}, },
async logout() { async logout() {
await logout() await logout()
......
...@@ -78,3 +78,9 @@ export interface MessageType { ...@@ -78,3 +78,9 @@ export interface MessageType {
type: 1 | 2 type: 1 | 2
updated_at: string updated_at: string
} }
export interface SystemDictionary {
id: string
label: string
value: string
}
// json to array // json to array
export const json2Array = function (data, isValueToNumber = true) { export const json2Array = function (data: any, isValueToNumber = true) {
return Object.keys(data).map(value => ({ label: data[value], value: isValueToNumber ? parseInt(value) : value })) return Object.keys(data).map(value => ({ label: data[value], value: isValueToNumber ? parseInt(value) : value }))
} }
// 组卷模式 // 参赛模式
export const paperType = { export const contestMode = {
1: '选题组卷', 1: '个人赛'
2: '自动组卷',
3: '自由组卷'
} }
// 组卷模式列表 // 参赛模式列表
export const paperTypeList = json2Array(paperType) export const contestModeList = json2Array(contestMode, false)
// 试题类型
export const questionType = {
1: '单选题',
2: '多选题',
3: '问答题',
5: '案例题',
6: '判断题',
7: '实操题',
8: '情景题'
}
// 试题类型列表
export const questionTypeList = json2Array(questionType, false)
// 试题难度
export const questionDifficulty = {
1: '易',
2: '中',
3: '难',
0: '无'
}
// 试题难度列表
export const questionDifficultyList = json2Array(questionDifficulty, false)
import fs from 'node:fs' // import fs from 'node:fs'
import path from 'node:path' // import path from 'node:path'
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
// import checker from 'vite-plugin-checker' // import checker from 'vite-plugin-checker'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import mkcert from 'vite-plugin-mkcert'
export default defineConfig(({ mode }) => ({ export default defineConfig(({ mode }) => ({
base: mode === 'prod' ? 'https://webapp-pub.ezijing.com/website/prod/saas-lab/' : '/', base: mode === 'prod' ? 'https://webapp-pub.ezijing.com/website/prod/saas-lab/' : '/',
...@@ -15,19 +16,21 @@ export default defineConfig(({ mode }) => ({ ...@@ -15,19 +16,21 @@ export default defineConfig(({ mode }) => ({
imports: ['vue', 'vue/macros', 'vue-router', '@vueuse/core'], imports: ['vue', 'vue/macros', 'vue-router', '@vueuse/core'],
dts: true, dts: true,
eslintrc: { enabled: true } eslintrc: { enabled: true }
}) }),
// checker({ vueTsc: true, eslint: { lintCommand: 'eslint "./src/**/*.{vue,js,jsx,ts,tsx}"' } }) // checker({ vueTsc: true, eslint: { lintCommand: 'eslint "./src/**/*.{vue,js,jsx,ts,tsx}"' } })
mkcert()
], ],
server: { server: {
open: true, open: true,
host: 'dev.ezijing.com', host: 'dev.ezijing.com',
https: { https: true,
key: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.key')), // https: {
cert: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.pem')) // key: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.key')),
}, // cert: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.pem'))
// },
proxy: { proxy: {
// '/api/lab': { // '/api/lab': {
// target: 'https://resource-api-test.ezijing.com', // target: 'http://test-resource-api.ezijing.com:8001',
// changeOrigin: true, // changeOrigin: true,
// rewrite: path => path.replace(/^\/api\/lab/, '') // rewrite: path => path.replace(/^\/api\/lab/, '')
// }, // },
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论