提交 1ffc22ab authored 作者: lhh's avatar lhh

实验管理需求开发

上级 39f896f5
...@@ -193,3 +193,20 @@ export function exportDrawLotStudentList(params: { draw_rule_id: string }) { ...@@ -193,3 +193,20 @@ export function exportDrawLotStudentList(params: { draw_rule_id: string }) {
export function getStudentDrawLotInfo(params: { draw_rule_id: string; student_id: string }) { export function getStudentDrawLotInfo(params: { draw_rule_id: string; student_id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition-draw/student-draw-info', { params }) return httpRequest.get('/api/resource/v1/backend/competition-draw/student-draw-info', { params })
} }
// 获取学员的抽签详情
export function getExperimentsList(params: {
type: number; name?: string; 'per-page': number
}) {
return httpRequest.get('/api/resource/v1/backend/competition/experiments', { params })
}
// 赛项绑定实验
export function bindExperiment(data: { id: string; experiment_id: any }) {
return httpRequest.post('/api/resource/v1/backend/competition/bind-experiment', data)
}
// 删除绑定的实验
export function unbindExperiment(data: { id: string, experiment_id: string }) {
return httpRequest.post('/api/resource/v1/backend/competition/unbind-experiment', data)
}
\ No newline at end of file
...@@ -7,7 +7,7 @@ import { pick } from 'lodash-es' ...@@ -7,7 +7,7 @@ import { pick } from 'lodash-es'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import isBetween from 'dayjs/plugin/isBetween' import isBetween from 'dayjs/plugin/isBetween'
import { createContest, updateContest } from '../api' import { createContest, updateContest, getExamList } from '../api'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useGetTeacherList } from '../composables/useGetTeacherList' import { useGetTeacherList } from '../composables/useGetTeacherList'
import { useAppConfig } from '@/composables/useAppConfig' import { useAppConfig } from '@/composables/useAppConfig'
...@@ -70,14 +70,21 @@ const form = reactive({ ...@@ -70,14 +70,21 @@ const form = reactive({
{ name: '数据营销实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' } { name: '数据营销实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' }
], ],
competition_platform_configs: [ competition_platform_configs: [
{ name: appConfig.xExamLabel || '1+X理论考试', is_show: '1', type: '1', url: '', platform_key: 'x_exam' },
{ {
name: appConfig.labExamLabel || '商业数据分析实验', name: appConfig.xExamLabel || '1+X理论考试',
is_show: '0', is_show: '1',
type: '2', type: '1',
url: '', url: '',
platform_key: 'career_data_analysis' platform_key: 'x_exam',
exam_id: ''
}, },
// {
// name: appConfig.labExamLabel || '商业数据分析实验',
// is_show: '0',
// type: '2',
// url: '',
// platform_key: 'career_data_analysis'
// },
{ name: '数据营销实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' } { name: '数据营销实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' }
] ]
}) })
...@@ -194,6 +201,10 @@ const title = $computed(() => { ...@@ -194,6 +201,10 @@ const title = $computed(() => {
// }) // })
// 提交 // 提交
function handleSubmit() { function handleSubmit() {
function containsNumber(A: number, B: number): boolean {
// 将数字A和B转换成字符串,并检查A的字符串是否包含B的字符串
return A.toString().includes(B.toString())
}
formRef?.validate().then(() => { formRef?.validate().then(() => {
const [firstDate, secondDate] = form.dateRange || [] const [firstDate, secondDate] = form.dateRange || []
const [firstDatetime, secondDatetime] = form.datetimeRange || [] const [firstDatetime, secondDatetime] = form.datetimeRange || []
...@@ -210,6 +221,19 @@ function handleSubmit() { ...@@ -210,6 +221,19 @@ function handleSubmit() {
end_at: dayjs(secondDatetime).year(year).month(month).date(date).unix(), end_at: dayjs(secondDatetime).year(year).month(month).date(date).unix(),
apply_expiration_date: dayjs(form.apply_expiration_date).endOf('date').unix() apply_expiration_date: dayjs(form.apply_expiration_date).endOf('date').unix()
} }
// 判断正式比赛理论答题时间和选择的考试
const findExam = examList.find(item => item.exam_id === form.competition_platform_configs[0].exam_id)
const examStartTime = new Date(findExam?.start_time || '').getTime()
const examEndTime = new Date(findExam?.start_time || '').getTime()
if (
containsNumber(examStartTime, mergedForm.start_at) !== true ||
containsNumber(examEndTime, mergedForm.start_at) !== true
) {
ElMessage({ message: `正式比赛理论答题时间与${findExam?.name}的考试时间不符`, type: 'warning' })
return false
}
const params: ContestUpdateParams = pick(mergedForm, [ const params: ContestUpdateParams = pick(mergedForm, [
'id', 'id',
'name', 'name',
...@@ -267,6 +291,17 @@ const clientList = [ ...@@ -267,6 +291,17 @@ const clientList = [
{ label: '全媒体运营师赛项', value: 'all_media_operator' }, { label: '全媒体运营师赛项', value: 'all_media_operator' },
{ label: '网络主播赛项', value: 'network_anchor_competition' } { label: '网络主播赛项', value: 'network_anchor_competition' }
] ]
let examList = $ref<Record<string, any>[]>([])
// 获取关联考试列表
function fetchExamList() {
getExamList({ project: 'x1', 'per-page': 1000 }).then(res => {
examList = res.data.list || []
})
}
onMounted(() => {
fetchExamList()
})
</script> </script>
<template> <template>
...@@ -275,8 +310,9 @@ const clientList = [ ...@@ -275,8 +310,9 @@ const clientList = [
:close-on-click-modal="false" :close-on-click-modal="false"
align-center align-center
width="600px" width="600px"
@update:modelValue="value => $emit('update:modelValue', value)"> @update:modelValue="value => $emit('update:modelValue', value)"
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px"> >
<el-form ref="formRef" :model="form" :rules="rules" label-width="165px">
<el-form-item label="客户端标识" prop="client_id"> <el-form-item label="客户端标识" prop="client_id">
<el-select v-model="form.client_id" style="width: 100%" clearable> <el-select v-model="form.client_id" style="width: 100%" clearable>
<el-option v-for="item in clientList" :key="item.value" :label="item.label" :value="item.value"></el-option> <el-option v-for="item in clientList" :key="item.value" :label="item.label" :value="item.value"></el-option>
...@@ -301,7 +337,8 @@ const clientList = [ ...@@ -301,7 +337,8 @@ const clientList = [
v-for="item in technicalSupportUnitList" v-for="item in technicalSupportUnitList"
:key="item.id" :key="item.id"
:label="item.label" :label="item.label"
:value="item.id"></el-option> :value="item.id"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="赛项类型" prop="type"> <el-form-item label="赛项类型" prop="type">
...@@ -320,19 +357,21 @@ const clientList = [ ...@@ -320,19 +357,21 @@ const clientList = [
range-separator="至" range-separator="至"
v-model="form.dateRange" v-model="form.dateRange"
style="width: 100%" style="width: 100%"
@change="handleDateRangeChange" /> @change="handleDateRangeChange"
/>
</el-form-item> </el-form-item>
<el-form-item label="正式比赛日期" prop="date"> <el-form-item label="正式比赛日期" prop="date">
<el-date-picker type="date" v-model="form.date" style="width: 100%" /> <el-date-picker type="date" v-model="form.date" style="width: 100%" />
</el-form-item> </el-form-item>
<el-form-item label="正式比赛时间" prop="datetimeRange"> <el-form-item label="正式比赛理论答题时间" prop="datetimeRange">
<el-time-picker <el-time-picker
is-range is-range
range-separator="至" range-separator="至"
start-placeholder="开始时间" start-placeholder="开始时间"
end-placeholder="结束时间" end-placeholder="结束时间"
v-model="form.datetimeRange" v-model="form.datetimeRange"
style="width: 100%" /> style="width: 100%"
/>
</el-form-item> </el-form-item>
<el-form-item label="报名截止日期" prop="apply_expiration_date"> <el-form-item label="报名截止日期" prop="apply_expiration_date">
<el-date-picker type="date" v-model="form.apply_expiration_date" style="width: 100%" /> <el-date-picker type="date" v-model="form.apply_expiration_date" style="width: 100%" />
...@@ -344,11 +383,12 @@ const clientList = [ ...@@ -344,11 +383,12 @@ const clientList = [
style="margin-bottom: 10px" style="margin-bottom: 10px"
v-model="item.is_show" v-model="item.is_show"
v-for="item in form.train_platform_configs" v-for="item in form.train_platform_configs"
:key="item.platform_key"> :key="item.platform_key"
>
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> --> <!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; width: 200px" /> <el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" /> <el-input v-model="item.url" style="width: 200px" />
</div> </div>
</el-checkbox> </el-checkbox>
</el-form-item> </el-form-item>
...@@ -359,11 +399,20 @@ const clientList = [ ...@@ -359,11 +399,20 @@ const clientList = [
style="margin-bottom: 10px" style="margin-bottom: 10px"
v-model="item.is_show" v-model="item.is_show"
v-for="item in form.competition_platform_configs" v-for="item in form.competition_platform_configs"
:key="item.platform_key"> :key="item.platform_key"
>
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> --> <!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; width: 200px" /> <el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" /> <el-input v-model="item.url" v-if="item.type === '2'" style="width: 200px" />
<el-select v-model="item.exam_id" filterable style="width: 200px" v-if="item.type === '1'">
<el-option
v-for="item in examList"
:key="item.exam_id"
:label="item.name"
:value="item.exam_id"
></el-option>
</el-select>
</div> </div>
</el-checkbox> </el-checkbox>
</el-form-item> </el-form-item>
......
<script setup lang="ts">
import { CirclePlus } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AppList from '@/components/base/AppList.vue'
import { unbindExperiment, getContest } from '../api'
import { useMapStore } from '@/stores/map'
const FormDialog = defineAsyncComponent(() => import('./ViewExperimentFormDialog.vue'))
interface Props {
id: string
pid: string
}
const props = defineProps<Props>()
const types = useMapStore().getMapValuesByKey('experiment_type')
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = {
hasPagination: false,
remote: {
httpRequest: getContest,
params: { id: props.pid },
callback(res: any) {
let list: any = []
if (Object.keys(res?.detail?.experiment).length) {
list = [res?.detail?.experiment]
}
return { list: list }
}
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '实验名称', prop: 'name' },
{
label: '实验类型',
prop: 'type',
computed({ row }: { row: any }) {
const type = types.find(item => item.value === row.type)?.label
return type || ''
}
},
{
label: '实验指导老师',
prop: 'teachers',
computed({ row }: { row: any }) {
let name = ''
if (row.teachers) {
name = row.teachers[0]?.name || ''
}
return name
}
},
{
label: '实验学生人数',
prop: 'classes',
computed({ row }: { row: any }) {
let num = ''
if (row.classes) {
num = row.classes[0]?.student_total
}
return num
}
},
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 180 }
]
}
let dialogVisible = $ref(false)
const rowData = ref<any>()
// 新增
function handleAdd() {
rowData.value = undefined
dialogVisible = true
}
// 查阅
function handleView(row: any) {
const qaURL = `/admin/lab/experiment/${row.id}`
window.open(qaURL)
}
// 删除
function handleRemoveClass(row: any) {
ElMessageBox.confirm('确定要删除吗?', '提示').then(() => {
unbindExperiment({ id: props.id, experiment_id: row.id }).then(() => {
ElMessage({ message: '删除成功', type: 'success' })
onUpdateSuccess()
})
})
}
function onUpdateSuccess() {
appList?.refetch()
}
</script>
<template>
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'competition-book-create'"
>关联</el-button
>
</template>
<template #table-x="{ row }">
<el-button link round type="info" @click="handleView(row)" v-permission="'competition-book-detail'"
>查阅</el-button
>
<el-button link round type="danger" @click="handleRemoveClass(row)" v-permission="'competition-book-delete'"
>删除</el-button
>
</template>
</AppList>
<FormDialog v-model="dialogVisible" :id="id" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
</template>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { ElMessage } from 'element-plus'
import { getExperimentsList, bindExperiment } from '../api'
interface Props {
id?: any
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const formRef = $ref<FormInstance>()
const form = reactive<any>({
id: ''
})
const title = $computed(() => {
return '关联实验'
})
let list = $ref<Record<string, any>[]>([])
const loading = ref(false)
// 获取关联考试列表
function fetchExamList(query?: string) {
loading.value = true
getExperimentsList({ type: 4, name: query, 'per-page': 1000 }).then((res: any) => {
loading.value = false
list = res.data?.list || []
})
}
onMounted(() => {
fetchExamList()
})
// 提交
function handleSubmit() {
bindExperiment({ id: props.id, experiment_id: form.id }).then(() => {
ElMessage({ message: '绑定成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
</script>
<template>
<el-dialog
:title="title"
:close-on-click-modal="false"
width="500px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<el-form ref="formRef" :model="form" label-width="90px">
<el-form-item label="选择实验">
<el-select
v-model="form.id"
filterable
remote
reserve-keyword
placeholder="输入实验名称"
remote-show-suffix
:remote-method="fetchExamList"
:loading="loading"
style="width: 340px"
>
<el-option v-for="item in list" :key="item.value" :label="item.name" :value="item.id" />
</el-select>
</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>
...@@ -10,6 +10,7 @@ const ViewVideo = defineAsyncComponent(() => import('../components/ViewVideo.vue ...@@ -10,6 +10,7 @@ const ViewVideo = defineAsyncComponent(() => import('../components/ViewVideo.vue
const ViewQuestion = defineAsyncComponent(() => import('../components/ViewQuestion.vue')) const ViewQuestion = defineAsyncComponent(() => import('../components/ViewQuestion.vue'))
const ViewExam = defineAsyncComponent(() => import('../components/ViewExam.vue')) const ViewExam = defineAsyncComponent(() => import('../components/ViewExam.vue'))
const ViewDrawLots = defineAsyncComponent(() => import('../components/ViewDrawLots.vue')) const ViewDrawLots = defineAsyncComponent(() => import('../components/ViewDrawLots.vue'))
const ViewExperiment = defineAsyncComponent(() => import('../components/ViewExperiment.vue'))
const ScoringRulesDialog = defineAsyncComponent(() => import('../components/ScoringRulesDialog.vue')) const ScoringRulesDialog = defineAsyncComponent(() => import('../components/ScoringRulesDialog.vue'))
const ScoringExpertsDialog = defineAsyncComponent(() => import('../components/ScoringExpertsDialog.vue')) const ScoringExpertsDialog = defineAsyncComponent(() => import('../components/ScoringExpertsDialog.vue'))
const ContestantDialog = defineAsyncComponent(() => import('../components/ContestantDialog.vue')) const ContestantDialog = defineAsyncComponent(() => import('../components/ContestantDialog.vue'))
...@@ -121,10 +122,16 @@ function handleExperts() { ...@@ -121,10 +122,16 @@ function handleExperts() {
<template> <template>
<AppCard title="查看赛项信息"> <AppCard title="查看赛项信息">
<template #header-aside> <template #header-aside>
<el-button type="primary" @click="scoringRulesVisible = true" v-permission="'competition-rule'">评分规则</el-button> <el-button type="primary" @click="scoringRulesVisible = true" v-permission="'competition-rule'"
>评分规则</el-button
>
<el-button type="primary" @click="handleExperts" v-permission="'competition-bind-experts'">评分专家</el-button> <el-button type="primary" @click="handleExperts" v-permission="'competition-bind-experts'">评分专家</el-button>
<el-button type="primary" @click="contestantVisible = true" v-permission="'competition-competitor-list'">参赛选手</el-button> <el-button type="primary" @click="contestantVisible = true" v-permission="'competition-competitor-list'"
<el-button type="primary" @click="scoringRulesBookVisible = true" v-permission="'competition-rubric-update'">评分细则</el-button> >参赛选手</el-button
>
<el-button type="primary" @click="scoringRulesBookVisible = true" v-permission="'competition-rubric-update'"
>评分细则</el-button
>
</template> </template>
<div class="top" v-if="detail"> <div class="top" v-if="detail">
<div class="top-cover"> <div class="top-cover">
...@@ -137,11 +144,15 @@ function handleExperts() { ...@@ -137,11 +144,15 @@ function handleExperts() {
<el-descriptions-item label="主办单位:">{{ detail.host_unit.label }}</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="指导教师:">{{ teacherText }}</el-descriptions-item>
<el-descriptions-item label="承办单位:">{{ orgText }}</el-descriptions-item> <el-descriptions-item label="承办单位:">{{ orgText }}</el-descriptions-item>
<el-descriptions-item label="赛项周期:">{{ formatDate(detail.start_range) }} ~ {{ formatDate(detail.end_range) }}</el-descriptions-item> <el-descriptions-item label="赛项周期:"
>{{ formatDate(detail.start_range) }} ~ {{ formatDate(detail.end_range) }}</el-descriptions-item
>
<el-descriptions-item label="技术支持单位:">{{ detail.technical_support_unit.label }}</el-descriptions-item> <el-descriptions-item label="技术支持单位:">{{ detail.technical_support_unit.label }}</el-descriptions-item>
<el-descriptions-item label="正式比赛日期:">{{ formatDate(detail.start_at) }}</el-descriptions-item> <el-descriptions-item label="正式比赛日期:">{{ formatDate(detail.start_at) }}</el-descriptions-item>
<el-descriptions-item label="生效状态:">{{ statusText }}</el-descriptions-item> <el-descriptions-item label="生效状态:">{{ statusText }}</el-descriptions-item>
<el-descriptions-item label="正式比赛时间:">{{ formatTime(detail.start_at) }} ~ {{ formatTime(detail.end_at) }}</el-descriptions-item> <el-descriptions-item label="正式比赛时间:"
>{{ formatTime(detail.start_at) }} ~ {{ formatTime(detail.end_at) }}</el-descriptions-item
>
<el-descriptions-item label="专家组长:">{{ expertLeadersText }}</el-descriptions-item> <el-descriptions-item label="专家组长:">{{ expertLeadersText }}</el-descriptions-item>
<el-descriptions-item label="专家:">{{ expertMembersText }}</el-descriptions-item> <el-descriptions-item label="专家:">{{ expertMembersText }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
...@@ -159,21 +170,38 @@ function handleExperts() { ...@@ -159,21 +170,38 @@ function handleExperts() {
<AppCard title="大赛试卷"> <AppCard title="大赛试卷">
<ViewExam :id="id"></ViewExam> <ViewExam :id="id"></ViewExam>
</AppCard> </AppCard>
<AppCard title="关联实验">
<ViewExperiment :id="detail?.id || ''" :pid="id"></ViewExperiment>
</AppCard>
<AppCard title="抽签加密"> <AppCard title="抽签加密">
<ViewDrawLots :id="id"></ViewDrawLots> <ViewDrawLots :id="id"></ViewDrawLots>
</AppCard> </AppCard>
<!-- 评分规则 --> <!-- 评分规则 -->
<ScoringRulesDialog v-model="scoringRulesVisible" :disabled="isStarted" @update="fetchRule" v-if="scoringRulesVisible && detail"></ScoringRulesDialog> <ScoringRulesDialog
v-model="scoringRulesVisible"
:disabled="isStarted"
@update="fetchRule"
v-if="scoringRulesVisible && detail"
></ScoringRulesDialog>
<!-- 评分专家 --> <!-- 评分专家 -->
<ScoringExpertsDialog <ScoringExpertsDialog
v-model="scoringExpertsVisible" v-model="scoringExpertsVisible"
:disabled="isStarted" :disabled="isStarted"
@update="fetchExperts" @update="fetchExperts"
v-if="scoringExpertsVisible && detail"></ScoringExpertsDialog> v-if="scoringExpertsVisible && detail"
></ScoringExpertsDialog>
<!-- 参赛选手 --> <!-- 参赛选手 -->
<ContestantDialog v-model="contestantVisible" :disabled="isStarted" v-if="contestantVisible && detail"></ContestantDialog> <ContestantDialog
v-model="contestantVisible"
:disabled="isStarted"
v-if="contestantVisible && detail"
></ContestantDialog>
<!-- 评分细则 --> <!-- 评分细则 -->
<ScoringRulesBookDialog v-model="scoringRulesBookVisible" :disabled="isStarted" v-if="scoringRulesBookVisible && detail"></ScoringRulesBookDialog> <ScoringRulesBookDialog
v-model="scoringRulesBookVisible"
:disabled="isStarted"
v-if="scoringRulesBookVisible && detail"
></ScoringRulesBookDialog>
</template> </template>
<style lang="scss"> <style lang="scss">
......
...@@ -181,3 +181,8 @@ export function copyExperiment(data: { experiment_id: string }) { ...@@ -181,3 +181,8 @@ export function copyExperiment(data: { experiment_id: string }) {
export function deleteExperiment(data: { experiment_id: string }) { export function deleteExperiment(data: { experiment_id: string }) {
return httpRequest.post('/api/resource/v1/backend/experiment/delete', data) return httpRequest.post('/api/resource/v1/backend/experiment/delete', data)
} }
// 获取实验成绩规则
export function getLiveCommodity(params: { experiment_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-commodity/all', { params })
}
\ No newline at end of file
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import type { ExperimentItem } from '../types' import type { ExperimentItem } from '../types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getTripConfig, updateTripConfig } from '../api' import { getTripConfig, updateTripConfig, getLiveCommodity } from '../api'
import { useConnection, useUserAttr, useMetaEvent, useTag, useGroup, useMaterial } from '../composables/useAllData' import { useConnection, useUserAttr, useMetaEvent, useTag, useGroup, useMaterial } from '../composables/useAllData'
import { useDocumentVisibility } from '@vueuse/core' import { useDocumentVisibility } from '@vueuse/core'
...@@ -29,6 +29,92 @@ const dmlURL = computed(() => { ...@@ -29,6 +29,92 @@ const dmlURL = computed(() => {
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/trip/template?experiment_id=${props.data.id}` return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/trip/template?experiment_id=${props.data.id}`
}) })
const experimentConfig: any = [
{
id: 1,
name: '基础配置',
is_checked: false,
pid: 0,
children: [
{ id: 2, name: '连接管理', is_checked: false, pid: 1, children: [] },
{ id: 3, name: '用户属性管理', is_checked: false, pid: 1, children: [] },
{ id: 4, name: '事件属性管理', is_checked: false, pid: 1, children: [] }
]
},
{
id: 5,
name: '营销策划',
is_checked: false,
pid: 0,
children: []
},
{
id: 6,
name: '用户画像',
is_checked: false,
pid: 0,
children: []
},
{
id: 7,
name: '用户识别',
is_checked: false,
pid: 0,
children: [
{ id: 8, name: '标签管理', is_checked: false, pid: 7, children: [] },
{ id: 9, name: '群组管理', is_checked: false, pid: 7, children: [] }
]
},
{
id: 10,
name: '营销内容设计',
is_checked: false,
pid: 0,
children: [
{ id: 11, name: '文本资料管理', is_checked: false, pid: 10, children: [] },
{ id: 12, name: '图片资料管理', is_checked: false, pid: 10, children: [] },
{ id: 13, name: '卡券资料管理', is_checked: false, pid: 10, children: [] },
{ id: 14, name: '视频资料管理', is_checked: false, pid: 10, children: [] },
{ id: 15, name: 'H5资料管理', is_checked: false, pid: 10, children: [] },
{ id: 16, name: '二维码资料管理', is_checked: false, pid: 10, children: [] },
{ id: 17, name: '语言资料管理', is_checked: false, pid: 10, children: [] },
{ id: 18, name: '小程序资料管理', is_checked: false, pid: 10, children: [] }
]
},
{
id: 19,
name: '自动化营销',
is_checked: false,
pid: 0,
children: []
},
{
id: 20,
name: '直播带货',
is_checked: false,
pid: 0,
children: [
{ id: 21, name: '商品品类管理', is_checked: false, pid: 20, children: [] },
{ id: 22, name: '商品属性管理', is_checked: false, pid: 20, children: [] },
{ id: 23, name: '商品管理', is_checked: false, pid: 20, children: [] },
{ id: 24, name: '直播练习', is_checked: false, pid: 20, children: [] },
{ id: 25, name: '直播话术管理', is_checked: false, pid: 20, children: [] }
]
},
{
id: 26,
name: '数据分析',
is_checked: false,
pid: 0,
children: [
{ id: 27, name: '用户分析', is_checked: false, pid: 26, children: [] },
{ id: 28, name: '标签群组分析', is_checked: false, pid: 26, children: [] },
{ id: 29, name: '事件分析', is_checked: false, pid: 26, children: [] },
{ id: 30, name: '营销分析', is_checked: false, pid: 26, children: [] }
]
}
]
const formRef = $ref<FormInstance>() const formRef = $ref<FormInstance>()
const form = reactive({ const form = reactive({
experiment_id: props.data.id, experiment_id: props.data.id,
...@@ -43,7 +129,10 @@ const form = reactive({ ...@@ -43,7 +129,10 @@ const form = reactive({
ids: ['教师维护的用户和事件数据'], ids: ['教师维护的用户和事件数据'],
tag_ids: [], tag_ids: [],
group_ids: [], group_ids: [],
material_ids: [] material_ids: [],
auth_config: experimentConfig,
is_use_common_live_commodities: 1,
live_commodity_ids: []
}) })
// 模板列表 // 模板列表
...@@ -81,7 +170,13 @@ function fetchInfo() { ...@@ -81,7 +170,13 @@ function fetchInfo() {
is_use_common_materials: data.is_use_common_materials, is_use_common_materials: data.is_use_common_materials,
tag_ids, tag_ids,
group_ids, group_ids,
material_ids material_ids,
auth_config: data.auth_config ? data.auth_config : experimentConfig,
live_commodity_ids: data?.live_commodities.reduce((a: any, b: any) => {
a.push(b.id)
return a
}, []),
is_use_common_live_commodities: data.is_use_common_live_commodities
}) })
}) })
} }
...@@ -92,7 +187,7 @@ watch(visibility, (current, previous) => { ...@@ -92,7 +187,7 @@ watch(visibility, (current, previous) => {
if (current === 'visible' && previous === 'hidden') fetchInfo() if (current === 'visible' && previous === 'hidden') fetchInfo()
}) })
const step = ref(0) const step = ref(-1)
// 上一步 // 上一步
function handlePrev() { function handlePrev() {
step.value-- step.value--
...@@ -112,7 +207,9 @@ function handleSubmit() { ...@@ -112,7 +207,9 @@ function handleSubmit() {
event_config: JSON.stringify(form.event_config), event_config: JSON.stringify(form.event_config),
tag_ids: JSON.stringify(form.tag_ids), tag_ids: JSON.stringify(form.tag_ids),
group_ids: JSON.stringify(form.group_ids), group_ids: JSON.stringify(form.group_ids),
material_ids: JSON.stringify(form.material_ids) material_ids: JSON.stringify(form.material_ids),
auth_config: JSON.stringify(form.auth_config),
live_commodity_ids: JSON.stringify(form.live_commodity_ids)
} }
updateTripConfig(params).then(() => { updateTripConfig(params).then(() => {
ElMessage({ message: '保存成功', type: 'success' }) ElMessage({ message: '保存成功', type: 'success' })
...@@ -121,13 +218,32 @@ function handleSubmit() { ...@@ -121,13 +218,32 @@ function handleSubmit() {
}) })
}) })
} }
// 多选
const handleH2Check = (item: any) => {
item.children.map((d: any) => {
d.is_checked = !item.is_checked
return d
})
}
const handleItemCheck = (item: any) => {
const isCheck = item.children.findIndex((d: any) => d.is_checked === false)
isCheck === -1 ? (item.is_checked = true) : (item.is_checked = false)
}
// 直播商品列表
const liveList: any = ref([])
getLiveCommodity({ experiment_id: props.data.id }).then((res: any) => {
liveList.value = res.data?.items || []
})
</script> </script>
<template> <template>
<el-dialog <el-dialog
title="配置数字营销实验" title="配置数字营销实验"
:close-on-click-modal="false" :close-on-click-modal="false"
width="600px" width="1000px"
@update:modelValue="value => $emit('update:modelValue', value)" @update:modelValue="value => $emit('update:modelValue', value)"
> >
<el-form ref="formRef" :model="form" label-suffix=":"> <el-form ref="formRef" :model="form" label-suffix=":">
...@@ -136,7 +252,28 @@ function handleSubmit() { ...@@ -136,7 +252,28 @@ function handleSubmit() {
<el-form-item label="实验类型">{{ data.type_name }} </el-form-item> <el-form-item label="实验类型">{{ data.type_name }} </el-form-item>
<el-form-item label="实验总成绩">{{ data.score }}</el-form-item> <el-form-item label="实验总成绩">{{ data.score }}</el-form-item>
</el-row> </el-row>
<el-tabs v-model="step"> <el-tabs v-model="step" tab-position="left">
<el-tab-pane label="功能" :name="-1">
<div class="check-ul">
<div class="li" v-for="item in form?.auth_config" :key="item?.id">
<div class="check-h2">
<el-checkbox @click="handleH2Check(item)" v-model="item.is_checked" :label="item.name" size="large" />
</div>
<div class="check-item">
<template v-for="cItem in item.children" :key="cItem?.id">
<el-checkbox
@change="handleItemCheck(item)"
v-model="cItem.is_checked"
:label="cItem.name"
size="large"
/>
</template>
</div>
<el-divider />
</div>
</div>
<!-- <el-checkbox v-model="checked1" label="Option 1" size="large" /> -->
</el-tab-pane>
<el-tab-pane label="模板与连接" :name="0"> <el-tab-pane label="模板与连接" :name="0">
<el-form-item label="旅程模板" label-width="82" prop="itinerary_id"> <el-form-item label="旅程模板" label-width="82" prop="itinerary_id">
<template v-if="templateList.length"> <template v-if="templateList.length">
...@@ -244,6 +381,20 @@ function handleSubmit() { ...@@ -244,6 +381,20 @@ function handleSubmit() {
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="直播商品" :name="6">
<el-form-item label="是否允许学生新建直播商品">
<el-radio-group v-model="form.is_use_common_live_commodities">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="学生能够使用的商品">
<el-checkbox-group v-model="form.live_commodity_ids" :disabled="form.is_use_common_live_commodities === 1">
<el-checkbox :label="item?.id" v-for="item in liveList" :key="item?.id">{{ item?.title }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-tab-pane>
<!-- <el-tab-pane label="旅程资源" :name="10"> <!-- <el-tab-pane label="旅程资源" :name="10">
<el-form-item label="是否允许学生新建如下资源"> <el-form-item label="是否允许学生新建如下资源">
<el-radio-group v-model="form.is_use_common"> <el-radio-group v-model="form.is_use_common">
...@@ -279,7 +430,9 @@ function handleSubmit() { ...@@ -279,7 +430,9 @@ function handleSubmit() {
</el-form> </el-form>
<template #footer> <template #footer>
<el-row justify="center"> <el-row justify="center">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)" v-if="step === 0">关闭</el-button> <el-button round auto-insert-space @click="$emit('update:modelValue', false)" v-if="step === -1"
>关闭</el-button
>
<el-button round auto-insert-space @click="handlePrev" v-else>上一步</el-button> <el-button round auto-insert-space @click="handlePrev" v-else>上一步</el-button>
<el-button type="primary" round auto-insert-space @click="handleNext" v-if="step < 2">下一步</el-button> <el-button type="primary" round auto-insert-space @click="handleNext" v-if="step < 2">下一步</el-button>
<el-button type="primary" round auto-insert-space @click="handleSubmit" v-else>保存</el-button> <el-button type="primary" round auto-insert-space @click="handleSubmit" v-else>保存</el-button>
...@@ -287,3 +440,23 @@ function handleSubmit() { ...@@ -287,3 +440,23 @@ function handleSubmit() {
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<style lang="scss">
.check-ul {
.li {
margin-bottom: 20px;
.check-h2 {
.el-checkbox__label {
color: #ba143e !important;
}
}
.check-item {
.el-checkbox__label {
color: #606266 !important;
}
}
}
.el-divider--horizontal {
margin: 0 !important;
}
}
</style>
...@@ -95,7 +95,7 @@ const listOptions = $computed(() => { ...@@ -95,7 +95,7 @@ const listOptions = $computed(() => {
{ label: '生效状态', prop: 'status_name' }, { label: '生效状态', prop: 'status_name' },
{ label: '更新人', prop: 'updated_operator_name' }, { label: '更新人', prop: 'updated_operator_name' },
{ label: '更新时间', prop: 'updated_time' }, { label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 220 } { label: '操作', slots: 'table-x', width: 250 }
] ]
} }
}) })
...@@ -150,24 +150,28 @@ async function handleDelete(row: ExperimentItem) { ...@@ -150,24 +150,28 @@ async function handleDelete(row: ExperimentItem) {
<AppCard title="实验管理"> <AppCard title="实验管理">
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #header-buttons> <template #header-buttons>
<el-button type="primary" :icon="CirclePlus" v-permission="'v1-backend-experiment-create'" @click="handleAdd">新增实验</el-button> <el-button type="primary" :icon="CirclePlus" v-permission="'v1-backend-experiment-create'" @click="handleAdd"
>新增实验</el-button
>
</template> </template>
<template #table-x="{ row }: { row: ExperimentItem }"> <template #table-x="{ row }: { row: ExperimentItem }">
<el-button type="primary" round v-permission="'v1-backend-experiment-view'"> <el-button type="primary" round v-permission="'v1-backend-experiment-view'">
<router-link :to="`/admin/lab/experiment/${row.id}`" target="_blank">查看</router-link> <router-link :to="`/admin/lab/experiment/${row.id}`" target="_blank">查看</router-link>
</el-button> </el-button>
<el-button type="primary" round @click="handleUpdate(row)" v-permission="'v1-backend-experiment-update'">编辑</el-button> <el-button type="primary" round @click="handleUpdate(row)" v-permission="'v1-backend-experiment-update'"
>编辑</el-button
>
<el-button type="primary" round :icon="Delete" @click="handleDelete(row)">删除</el-button>
<!-- 功能按钮移入详情里 s v-if="false" -->
<el-dropdown style="margin-left: 12px"> <el-dropdown style="margin-left: 12px">
<el-button type="primary" round :icon="MoreFilled"></el-button> <el-button type="primary" round :icon="MoreFilled"></el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<!-- <el-dropdown-item> <el-dropdown-item :icon="Setting" @click="handleUpdateDML(row)" v-if="row.type === '4'"
<router-link :to="`/admin/lab/experiment/${row.id}`" target="_blank">查看</router-link> >配置数字营销</el-dropdown-item
</el-dropdown-item> >
<el-dropdown-item @click="handleUpdate(row)">编辑</el-dropdown-item> -->
<el-dropdown-item :icon="Setting" @click="handleUpdateDML(row)" v-if="row.type === '4'">配置数字营销</el-dropdown-item>
<template v-if="!row.stu_commit_count"> <template v-if="!row.stu_commit_count">
<el-dropdown-item :icon="Edit" @click="handleUpdateGradeRules(row)">编辑成绩规则</el-dropdown-item> <el-dropdown-item :icon="Edit" @click="handleUpdateGradeRules(row)">编辑成绩规则</el-dropdown-item>
<el-dropdown-item :icon="EditPen"> <el-dropdown-item :icon="EditPen">
...@@ -179,6 +183,8 @@ async function handleDelete(row: ExperimentItem) { ...@@ -179,6 +183,8 @@ async function handleDelete(row: ExperimentItem) {
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<!-- 功能按钮移入详情里 end -->
<!-- <template v-if="row.type === '4'"> <!-- <template v-if="row.type === '4'">
<el-button <el-button
type="primary" type="primary"
...@@ -216,8 +222,18 @@ async function handleDelete(row: ExperimentItem) { ...@@ -216,8 +222,18 @@ async function handleDelete(row: ExperimentItem) {
</AppCard> </AppCard>
<FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog> <FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
<!-- 编辑实验成绩规则 --> <!-- 编辑实验成绩规则 -->
<GradeRulesDialog v-model="gradeRulesDialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="gradeRulesDialogVisible && rowData"></GradeRulesDialog> <GradeRulesDialog
v-model="gradeRulesDialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="gradeRulesDialogVisible && rowData"
></GradeRulesDialog>
<!-- 配置数字营销实验 --> <!-- 配置数字营销实验 -->
<DMLFormDialog v-model="dmlDialogVisible" :data="rowData" v-if="dmlDialogVisible && rowData"></DMLFormDialog> <DMLFormDialog v-model="dmlDialogVisible" :data="rowData" v-if="dmlDialogVisible && rowData"></DMLFormDialog>
<CopyDialog v-model="copyDialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="copyDialogVisible && rowData"></CopyDialog> <CopyDialog
v-model="copyDialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="copyDialogVisible && rowData"
></CopyDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ExperimentItem, ClassItem } from '../types' import type { ExperimentItem, ClassItem } from '../types'
import { CirclePlus } from '@element-plus/icons-vue' import { CirclePlus, CopyDocument, Setting } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import AppList from '@/components/base/AppList.vue' import AppList from '@/components/base/AppList.vue'
...@@ -14,6 +14,8 @@ const StudentListDialog = defineAsyncComponent(() => import('../components/Stude ...@@ -14,6 +14,8 @@ const StudentListDialog = defineAsyncComponent(() => import('../components/Stude
const ViewGradeRules = defineAsyncComponent(() => import('../components/ViewGradeRules.vue')) const ViewGradeRules = defineAsyncComponent(() => import('../components/ViewGradeRules.vue'))
const ViewReportRules = defineAsyncComponent(() => import('../components/ViewReportRules.vue')) const ViewReportRules = defineAsyncComponent(() => import('../components/ViewReportRules.vue'))
const ViewExam = defineAsyncComponent(() => import('../components/ViewExam.vue')) const ViewExam = defineAsyncComponent(() => import('../components/ViewExam.vue'))
const CopyDialog = defineAsyncComponent(() => import('../components/CopyDialog.vue'))
const DMLFormDialog = defineAsyncComponent(() => import('../components/DMLFormDialog.vue'))
interface Props { interface Props {
id: string id: string
...@@ -87,18 +89,46 @@ const reportRulesVisible = $ref(false) ...@@ -87,18 +89,46 @@ const reportRulesVisible = $ref(false)
const dmlURL = computed(() => { const dmlURL = computed(() => {
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}?experiment_id=${props.id}` return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}?experiment_id=${props.id}`
}) })
// 复制
let copyDialogVisible = $ref(false)
async function handleCopy() {
copyDialogVisible = true
}
// 配置数字营销实验
let dmlDialogVisible = $ref(false)
function handleUpdateDML() {
dmlDialogVisible = true
}
</script> </script>
<template> <template>
<AppCard title="实验管理"> <AppCard title="实验管理">
<template #header>
<div>
<h2 class="app-card-hd__title">实验管理</h2>
<div class="btn-all" style="margin-bottom: 15px">
<el-button type="primary" v-if="detail?.type === '4'">
<a :href="dmlURL" target="_blank">进入实验平台</a>
</el-button>
<el-button type="primary" @click="gradeRulesVisible = true">查看成绩规则</el-button>
<el-button type="primary" @click="reportRulesVisible = true">查看报告规则</el-button>
<el-button type="primary" :icon="CopyDocument" @click="handleCopy()">复制实验</el-button>
<el-button type="primary" :icon="Setting" @click="handleUpdateDML()" :disabled="detail?.type !== '4'"
>配置数字营销</el-button
>
</div>
</div>
</template>
<el-descriptions title="基本信息" v-if="detail"> <el-descriptions title="基本信息" v-if="detail">
<template #extra> <!-- <template #extra>
<el-button type="primary" v-if="detail.type === '4'"> <el-button type="primary" v-if="detail.type === '4'">
<a :href="dmlURL" target="_blank">进入实验平台</a> <a :href="dmlURL" target="_blank">进入实验平台</a>
</el-button> </el-button>
<el-button type="primary" @click="gradeRulesVisible = true">查看成绩规则</el-button> <el-button type="primary" @click="gradeRulesVisible = true">查看成绩规则</el-button>
<el-button type="primary" @click="reportRulesVisible = true">查看报告规则</el-button> <el-button type="primary" @click="reportRulesVisible = true">查看报告规则</el-button>
</template> </template> -->
<el-descriptions-item :span="3" label="实验名称:">{{ detail.name }}</el-descriptions-item> <el-descriptions-item :span="3" label="实验名称:">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="实验课程:">{{ detail.course_name }}</el-descriptions-item> <el-descriptions-item label="实验课程:">{{ detail.course_name }}</el-descriptions-item>
<el-descriptions-item label="所属机构/学校:">{{ detail.organ_id_name }}</el-descriptions-item> <el-descriptions-item label="所属机构/学校:">{{ detail.organ_id_name }}</el-descriptions-item>
...@@ -163,4 +193,7 @@ const dmlURL = computed(() => { ...@@ -163,4 +193,7 @@ const dmlURL = computed(() => {
></StudentListDialog> ></StudentListDialog>
<ViewGradeRules v-model="gradeRulesVisible" :data="detail" v-if="gradeRulesVisible && detail"></ViewGradeRules> <ViewGradeRules v-model="gradeRulesVisible" :data="detail" v-if="gradeRulesVisible && detail"></ViewGradeRules>
<ViewReportRules v-model="reportRulesVisible" :experiment_id="id" v-if="reportRulesVisible"></ViewReportRules> <ViewReportRules v-model="reportRulesVisible" :experiment_id="id" v-if="reportRulesVisible"></ViewReportRules>
<CopyDialog v-model="copyDialogVisible" :data="detail" v-if="copyDialogVisible && detail"></CopyDialog>
<!-- 配置数字营销实验 -->
<DMLFormDialog v-model="dmlDialogVisible" :data="detail" v-if="dmlDialogVisible && detail"></DMLFormDialog>
</template> </template>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论