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

chore: 新增实验数据管理

上级 eab55605
...@@ -134,3 +134,33 @@ export function getConnectionList(params: { experiment_id: string }) { ...@@ -134,3 +134,33 @@ export function getConnectionList(params: { experiment_id: string }) {
export function getMaterialList(params: { experiment_id: string }) { export function getMaterialList(params: { experiment_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/marketing-material/all', { params }) return httpRequest.get('/api/lab/v1/experiment/marketing-material/all', { params })
} }
// 清空用户数据
export function clearMembers(data: { experiment_id: string }) {
return httpRequest.post('/api/resource/v1/backend/experiment-itinerary/clear-members', data)
}
// 清空用户事件数据
export function clearMemberEvents(data: { experiment_id: string; student_id?: string }) {
return httpRequest.post('/api/resource/v1/backend/experiment-itinerary/clear-member-events', data)
}
// 生成实验数据
export function generateStudentEvents(data: { experiment_id: string }) {
return httpRequest.post('/api/resource/v1/backend/experiment-itinerary/generate-student-events', data)
}
// 获取生成实验数据学生列表
export function getGenerateEventsStudentList(params: { experiment_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment-itinerary/students', { params })
}
// 查看事件数据
export function getStudentEventList(params: {
experiment_id: string
student_id: string
page?: number
'per-page'?: number
}) {
return httpRequest.get('/api/resource/v1/backend/experiment-itinerary/events', { params })
}
<script setup lang="ts">
import type { ExperimentItem } from '../types'
import { PictureFilled } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { clearMembers, clearMemberEvents } from '../api'
const props = defineProps<{
data: ExperimentItem
}>()
// 生成实验数据
function genData() {
window.open(`/admin/lab/experiment/ed/generate/${props.data.id}`)
}
// 清空用户事件数据
function clearUserEvent() {
ElMessageBox.confirm(
'该操作将会清空该实验所有学生对应的用户事件数据,且该操作不可逆!请慎重!确定要清空数据吗?',
'清空用户事件数据'
).then(() => {
clearMemberEvents({ experiment_id: props.data.id }).then(() => {
ElMessage({ type: 'success', message: '清空成功' })
})
})
}
// 清空用户数据
function clearUser() {
ElMessageBox.confirm(
'该操作将会清空该实验所有学生对应的用户及事件数据,且该操作不可逆!请慎重!确定要清空数据吗?',
'清空用户数据'
).then(() => {
clearMembers({ experiment_id: props.data.id }).then(() => {
ElMessage({ type: 'success', message: '清空成功' })
})
})
}
</script>
<template>
<el-dialog title="实验数据管理" width="700px" @update:modelValue="$emit('update:modelValue')">
<div class="box-list">
<div class="box">
<div class="box-pic">
<el-icon><PictureFilled /></el-icon>
</div>
<el-button type="primary" @click="genData">生成实验数据</el-button>
</div>
<div class="box">
<div class="box-pic">
<el-icon><PictureFilled /></el-icon>
</div>
<el-button type="primary" @click="clearUserEvent">清空用户事件数据</el-button>
</div>
<div class="box">
<div class="box-pic">
<el-icon><PictureFilled /></el-icon>
</div>
<el-button type="primary" @click="clearUser">清空用户数据</el-button>
</div>
<div class="box">
<div class="box-pic">
<el-icon><PictureFilled /></el-icon>
</div>
<el-button type="info" disabled>更多实验数据管理</el-button>
</div>
</div>
</el-dialog>
</template>
<style lang="scss" scoped>
.box-list {
display: flex;
column-gap: 20px;
}
.box {
flex: 1;
.el-button {
width: 100%;
}
}
.box-pic {
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: center;
height: 100px;
background-color: #e8e8e8;
}
</style>
king
...@@ -10,7 +10,10 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -10,7 +10,10 @@ export const routes: Array<RouteRecordRaw> = [
{ path: 'experiment', component: () => import('./views/Index.vue') }, { path: 'experiment', component: () => import('./views/Index.vue') },
{ path: 'experiment/:id', component: () => import('./views/View.vue'), props: true }, { path: 'experiment/:id', component: () => import('./views/View.vue'), props: true },
{ path: 'experiment/group/:id', component: () => import('./views/Group.vue'), props: true }, { path: 'experiment/group/:id', component: () => import('./views/Group.vue'), props: true },
{ path: 'experiment/report/:id', component: () => import('./views/Report.vue'), props: true } { path: 'experiment/report/:id', component: () => import('./views/Report.vue'), props: true },
{ path: 'experiment/ed/rule/:id', component: () => import('./views/EventDataRule.vue'), props: true },
{ path: 'experiment/ed/generate/:id', component: () => import('./views/EventDataGenerate.vue'), props: true },
{ path: 'experiment/ed/view/:id', component: () => import('./views/EventDataView.vue'), props: true }
] ]
} }
] ]
...@@ -31,6 +31,13 @@ export interface ExperimentItem { ...@@ -31,6 +31,13 @@ export interface ExperimentItem {
content: string content: string
procedure: string procedure: string
stu_commit_count: number stu_commit_count: number
itinerary?: ExperimentTrip
}
export interface ExperimentTrip {
id: string
name: string
type: string
} }
export interface ExperimentCreateItem { export interface ExperimentCreateItem {
......
<script setup lang="ts">
import { getGenerateEventsStudentList, generateStudentEvents, clearMemberEvents } from '../api'
import AppList from '@/components/base/AppList.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
interface Props {
id: string
}
const props = defineProps<Props>()
const detail = ref()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getGenerateEventsStudentList,
params: { experiment_id: props.id },
callback(data: { total: number; list: any; info: any }) {
const { total, list, info } = data
detail.value = info
return { list, total }
}
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '班级', prop: 'class_name' },
{ label: '学号', prop: 'sno_number' },
{ label: '姓名', prop: 'name' },
{ label: '状态', prop: 'generate_status_name' },
{ label: '数据量', prop: 'generate_total' },
{ label: '开始时间', prop: 'begin_time' },
{ label: '完成时间', prop: 'end_time' },
{ label: '操作', slots: 'table-x', width: 270 }
]
}
})
// 清空事件数据
function clearEventData(row: any) {
ElMessageBox.confirm(
'该操作将会清空该实验所有学生对应的用户事件数据,且该操作不可逆!请慎重!确定要清空数据吗?',
'清空用户事件数据'
).then(() => {
clearMemberEvents({ experiment_id: props.id, student_id: row.student_id }).then(() => {
handleRefresh()
ElMessage({ type: 'success', message: '清空成功' })
})
})
}
// 生成实验数据
function handleGenerate() {
generateStudentEvents({ experiment_id: props.id }).then(() => {
handleRefresh()
ElMessage({ type: 'success', message: '操作成功' })
})
}
// 刷新
function handleRefresh() {
appList?.refetch()
}
// 关闭
function handleClose() {
window.close()
}
</script>
<template>
<AppCard title="生成实验数据">
<el-descriptions :column="4" v-if="detail">
<el-descriptions-item label="实验名称:">{{ detail.experiment_name }}</el-descriptions-item>
<el-descriptions-item label="实验类型:">{{ detail.experiment_type_name }}</el-descriptions-item>
<el-descriptions-item label="用户旅程模板:">{{ detail.itinerary_name }}</el-descriptions-item>
<el-descriptions-item label="模板类型:">
{{ detail.itinerary_type_name }}
</el-descriptions-item>
</el-descriptions>
<AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary" text>
<router-link :to="`/admin/lab/experiment/ed/view/${id}?student_id=${row.student_id}`" target="_blank"
>查看事件数据
</router-link>
</el-button>
<el-button type="primary" text @click="clearEventData(row)">清空事件数据</el-button>
</template>
</AppList>
<el-row justify="center">
<el-button type="primary" @click="handleGenerate">生成实验数据</el-button>
<el-button @click="handleRefresh">刷新</el-button>
<el-button @click="handleClose">关闭</el-button>
</el-row>
</AppCard>
</template>
<script setup lang="ts">
const props = defineProps<{ id: string }>()
const route = useRoute()
const dmlURL = computed(() => {
return `${import.meta.env.VITE_DML_URL}/trip/template/${route.query.template_id as string}/rule?experiment_id=${
props.id
}`
})
</script>
<template>
<iframe :src="dmlURL" frameborder="0" class="iframe"></iframe>
</template>
<style lang="scss">
.iframe {
width: 100%;
height: 100%;
}
</style>
<script setup lang="ts">
import { getStudentEventList } from '../api'
interface Props {
id: string
}
const props = defineProps<Props>()
const route = useRoute()
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getStudentEventList,
params: { experiment_id: props.id, student_id: route.query.student_id }
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '用户ID', prop: 'experiment_member_id' },
{ label: '用户名称', prop: 'experiment_member_name' },
{ label: '事件名称', prop: 'event_name' },
{ label: '连接名称', prop: 'connection_name' },
{ label: '事件发生时间', prop: 'created_time' }
// { label: '操作', slots: 'table-x', width: 230 }
]
}
})
function handleClose() {
window.close()
}
</script>
<template>
<AppCard title="查看事件数据">
<AppList v-bind="listOptions" ref="appList">
<template #table-x>
<el-button type="primary" text>查看</el-button>
<el-button type="primary" text>编辑</el-button>
<el-button type="primary" text>删除</el-button>
</template>
</AppList>
<el-row justify="center">
<el-button @click="handleClose">关闭</el-button>
</el-row>
</AppCard>
</template>
...@@ -11,6 +11,7 @@ const StudentGroupDialog = defineAsyncComponent(() => import('../components/Stud ...@@ -11,6 +11,7 @@ const StudentGroupDialog = defineAsyncComponent(() => import('../components/Stud
const StudentListDialog = defineAsyncComponent(() => import('../components/StudentListDialog.vue')) const StudentListDialog = defineAsyncComponent(() => import('../components/StudentListDialog.vue'))
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 DMLDataDialog = defineAsyncComponent(() => import('../components/DMLDataDialog.vue'))
interface Props { interface Props {
id: string id: string
...@@ -82,6 +83,15 @@ const reportRulesVisible = $ref(false) ...@@ -82,6 +83,15 @@ const reportRulesVisible = $ref(false)
const dmlURL = computed(() => { const dmlURL = computed(() => {
return `${import.meta.env.VITE_DML_URL}?experiment_id=${props.id}` return `${import.meta.env.VITE_DML_URL}?experiment_id=${props.id}`
}) })
// 数据规则
function handleDataRule() {
window.open(
`/admin/lab/experiment/ed/rule/${props.id}?experiment_id=${props.id}&template_id=${detail?.itinerary?.id}`
)
}
const dmlDataVisible = ref(false)
</script> </script>
<template> <template>
...@@ -93,6 +103,10 @@ const dmlURL = computed(() => { ...@@ -93,6 +103,10 @@ const dmlURL = computed(() => {
</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 v-if="detail.type === '4'">
<el-button type="primary" @click="handleDataRule">实验数据规则</el-button>
<el-button type="primary" @click="dmlDataVisible = true">实验数据管理</el-button>
</template>
</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>
...@@ -152,4 +166,6 @@ const dmlURL = computed(() => { ...@@ -152,4 +166,6 @@ const dmlURL = computed(() => {
v-if="studentListVisible && rowData"></StudentListDialog> v-if="studentListVisible && rowData"></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>
<!-- 实验数据管理 -->
<DMLDataDialog v-model="dmlDataVisible" :data="detail" v-if="dmlDataVisible && detail"></DMLDataDialog>
</template> </template>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/student/cert',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
<script setup lang="ts">
import { PictureFilled } from '@element-plus/icons-vue'
</script>
<template>
<div>
<h1>1+X金融产品数字化营销职业技能等级证书实训平台</h1>
<div class="box-list">
<div class="box">
<h2 class="box-title">初 级</h2>
<div class="box-pic">
<el-icon><PictureFilled /></el-icon>
</div>
<div>
<a target="_blank" href="https://x-training.ezijing.com/home">
<el-button size="large" type="primary">实训实操</el-button>
</a>
</div>
<div>
<a target="_blank" href="https://x-learning.ezijing.com/exam/exam">
<el-button size="large" type="primary">模拟考试</el-button>
</a>
</div>
</div>
<div class="box">
<h2 class="box-title">中 级</h2>
<div class="box-pic">
<el-icon><PictureFilled /></el-icon>
</div>
<div>
<a target="_blank" href="https://x-training.ezijing.com/home">
<el-button size="large" type="primary">实训实操</el-button>
</a>
</div>
<div>
<a target="_blank" href="https://x-learning.ezijing.com/exam/exam">
<el-button size="large" type="primary">模拟考试</el-button>
</a>
</div>
</div>
<div class="box">
<h2 class="box-title">高 级</h2>
<div class="box-pic">
<el-icon><PictureFilled /></el-icon>
</div>
<div>
<el-button size="large" type="info" disabled>实训实操(上线中)</el-button>
</div>
<div>
<a target="_blank" href="https://x-learning.ezijing.com/exam/exam">
<el-button size="large" type="primary">模拟考试</el-button>
</a>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
h1 {
margin: 80px 0;
font-size: 30px;
text-align: center;
color: var(--main-color);
}
.box-list {
display: flex;
justify-content: center;
column-gap: 20px;
}
.box {
width: 330px;
background-color: #fff;
border-radius: 6px;
padding: 50px 40px;
box-sizing: border-box;
.el-button {
margin-top: 10px;
width: 100%;
}
}
.box-title {
margin-bottom: 20px;
text-align: center;
font-size: 20px;
color: var(--main-color);
}
.box-pic {
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
height: 144px;
background-color: #e8e8e8;
}
</style>
...@@ -28,6 +28,10 @@ const studentMenus: IMenuItem[] = [ ...@@ -28,6 +28,10 @@ const studentMenus: IMenuItem[] = [
{ {
name: '大赛成绩查询', name: '大赛成绩查询',
path: '/student/contest/score' path: '/student/contest/score'
},
{
name: '金融产品数字化营销证书',
path: '/student/cert'
} }
] ]
// 管理员菜单 // 管理员菜单
......
...@@ -56,7 +56,7 @@ httpRequest.interceptors.response.use( ...@@ -56,7 +56,7 @@ httpRequest.interceptors.response.use(
// 未授权 // 未授权
router.push('/401') router.push('/401')
} else { } else {
ElMessage.error(message) ElMessage.error(message || error.message)
console.error(`${status}: ${message}`) console.error(`${status}: ${message}`)
} }
} else { } else {
......
export interface Dictionary {
label: string
value: string | number
}
export function getNameByValue(value: string | number, list: Dictionary[]) {
return list.find(item => item.value == value)?.label || value
}
// json to array // json to array
export const json2Array = function (data: any, 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 }))
...@@ -41,3 +50,9 @@ export const reportScoreRule: Record<number, any> = { ...@@ -41,3 +50,9 @@ export const reportScoreRule: Record<number, any> = {
2: '自动评分' 2: '自动评分'
} }
export const reportScoreRuleList = json2Array(reportScoreRule) export const reportScoreRuleList = json2Array(reportScoreRule)
// 旅程类型
export const tripTemplateTypeList = [
{ label: '自由旅程', value: '1' },
{ label: '固定旅程', value: '2' }
]
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论