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

Merge branch 'master' into 202412

......@@ -9,3 +9,4 @@ VITE_EXAM_SHOW_URL=https://exam-show.zijing.chat
VITE_SWSJFXS_LOGIN_URL=http://172.16.3.203:1001/swsjfxs/login/index
VITE_SYS_FLAG=chat
VITE_STATIC_URL=https://saas-lab-api
VITE_SAAS_BI_URL=https://saas-bi.ezijing.com/data/dashboard
差异被折叠。
......@@ -5,7 +5,7 @@ const emit = defineEmits<{
(e: 'resize'): void
}>()
defineProps<{ isLeftShow?: number }>()
defineProps<{ isLeftShow?: boolean }>()
const leftPanelVisible = $ref<boolean>(true)
const leftPanelWidth = useStorage('leftPanelWidth', 400)
......@@ -42,7 +42,7 @@ onMounted(() => {
<template>
<section class="drag-panel">
<div v-show="isLeftShow !== 1" class="drag-panel-left" :class="{ 'is-hidden': !leftPanelVisible }">
<div v-if="!isLeftShow" class="drag-panel-left" :class="{ 'is-hidden': !leftPanelVisible }">
<div class="drag-cover" v-if="dragFlag"></div>
<slot name="left"></slot>
<div class="panel-resize" id="panel-resize"></div>
......@@ -73,6 +73,7 @@ onMounted(() => {
.drag-panel {
display: flex;
height: calc(100vh - 110px);
gap: 20px;
}
.drag-panel-left {
position: relative;
......@@ -123,7 +124,6 @@ onMounted(() => {
display: flex;
flex-direction: column;
height: 100%;
margin-left: 20px;
}
.drag-cover {
width: 100%;
......
......@@ -86,7 +86,11 @@ export function getExperimentReportRule(params: { experiment_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment-report/detail', { params })
}
// 更新实验报告规则
export function updateExperimentReportRule(data: { experiment_id: string; report_upload_way: number; detail_list: string }) {
export function updateExperimentReportRule(data: {
experiment_id: string
report_upload_way: number
detail_list: string
}) {
return httpRequest.post('/api/resource/v1/backend/experiment-report/save', data)
}
......@@ -98,7 +102,7 @@ export function getTripConfig(params: { experiment_id: string }) {
// 更新旅程配置
export function updateTripConfig(data: { experiment_id: string }) {
return httpRequest.post('/api/lab/v1/experiment/itinerary-config/save', data, {
headers: { 'Content-Type': 'application/json' }
headers: { 'Content-Type': 'application/json' },
})
}
......@@ -141,22 +145,50 @@ export function getQuestions(data: { experiment_id: string; types?: any }) {
return httpRequest.post('/api/resource/v1/backend/experiment-question/list', data)
}
// 获取老师创建的标签
// 试题获取老师创建的标签
export function getQuestionTags(params: { experiment_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment-question/tags', { params })
return httpRequest.get('/api/resource/v1/backend/experiment-question/tags2', { params })
}
// 获取群组
// 试题获取群组
export function getQuestionGroup(params: { experiment_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment-question/groups', { params })
}
// 试题获取营销资料
export function getQuestionMaterials(params: { experiment_id: string; type: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment-question/materials', { params })
}
// 试题获取事件
export function getQuestionEvents(params: { experiment_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment-question/events', { params })
}
// 获取Excel总条数
export function getExcelTotalLine(data: { file: File }) {
return httpRequest.post('/api/lab/v1/common/file/get-excel-total-line', data, {
headers: { 'Content-Type': 'multipart/form-data' },
})
}
// 判断是否维护了自动生成数据
export function checkAutoGenerateData(params: { experiment_id: string; event_id?: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment-question/check-auto-generate-data', { params })
}
// 获取实验下的所有理论考试
export function getAllExamList(params: { project: string; q?: string; name?: string; page?: number; 'per-page'?: number }) {
export function getAllExamList(params: {
project: string
q?: string
name?: string
page?: number
'per-page'?: number
}) {
return httpRequest.get('/api/resource/v1/backend/exam/search-all-exam', { params })
}
// 获取实验的理论考试列表
export function getExamList(params: { experiment_id: string, type: string }) {
export function getExamList(params: { experiment_id: string; type: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/exam-list', { params })
}
// 更新实验的理论考试
......@@ -185,4 +217,4 @@ export function deleteExperiment(data: { experiment_id: string }) {
// 获取实验成绩规则
export function getLiveCommodity(params: { experiment_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-commodity/all', { params })
}
\ No newline at end of file
}
......@@ -38,22 +38,22 @@ const experimentConfig: any = [
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: 4, name: '事件属性管理', is_checked: false, pid: 1, children: [] },
],
},
{
id: 5,
name: '营销策划',
is_checked: false,
pid: 0,
children: []
children: [],
},
{
id: 6,
name: '用户画像',
is_checked: false,
pid: 0,
children: []
children: [],
},
{
id: 7,
......@@ -62,8 +62,9 @@ const experimentConfig: any = [
pid: 0,
children: [
{ id: 8, name: '标签管理', is_checked: false, pid: 7, children: [] },
{ id: 9, name: '群组管理', is_checked: false, pid: 7, children: [] }
]
{ id: 9, name: '群组管理', is_checked: false, pid: 7, children: [] },
{ id: 71, name: '运营策略管理', is_checked: false, pid: 7, children: [] },
],
},
{
id: 10,
......@@ -78,15 +79,15 @@ const experimentConfig: any = [
{ 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: 18, name: '小程序资料管理', is_checked: false, pid: 10, children: [] },
],
},
{
id: 19,
name: '自动化营销',
is_checked: false,
pid: 0,
children: []
children: [],
},
{
id: 20,
......@@ -98,8 +99,9 @@ const experimentConfig: any = [
{ 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: 25, name: '直播话术管理', is_checked: false, pid: 20, children: [] },
{ id: 201, name: '订单管理', is_checked: false, pid: 20, children: [] },
],
},
{
id: 26,
......@@ -110,9 +112,9 @@ const experimentConfig: any = [
{ 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: [] }
]
}
{ id: 30, name: '营销分析', is_checked: false, pid: 26, children: [] },
],
},
]
const formRef = $ref<FormInstance>()
......@@ -132,7 +134,7 @@ const form = reactive({
material_ids: [],
auth_config: experimentConfig,
is_use_common_live_commodities: 0,
live_commodity_ids: []
live_commodity_ids: [],
})
// 模板列表
......@@ -141,7 +143,7 @@ let templateList = $ref<{ id: string; name: string }[]>([])
const checked = ref(false)
function fetchInfo() {
getTripConfig({ experiment_id: props.data.id }).then(res => {
getTripConfig({ experiment_id: props.data.id }).then((res) => {
const data = res.data
if (data.itinerary?.id) {
templateList = [data.itinerary]
......@@ -153,15 +155,46 @@ function fetchInfo() {
}
const user_attr_config = {
is_all: data.user_attr_config.is_all,
items: data.user_attr_config.items.map((item: any) => item.id)
items: data.user_attr_config.items.map((item: any) => item.id),
}
const event_config = {
is_all: data.event_config.is_all,
items: data.event_config.items.map((item: any) => item.id)
items: data.event_config.items.map((item: any) => item.id),
}
const tag_ids = data.tags.map((item: any) => item.id)
const group_ids = data.groups.map((item: any) => item.id)
const material_ids = data.marketing_materials.map((item: any) => item.id)
// 递归合并配置
interface MenuItem {
id: number
name: string
is_checked: boolean
pid: number
children: MenuItem[]
}
const mergeConfig = (defaultItems: MenuItem[], backendItems: MenuItem[]): MenuItem[] => {
return defaultItems.map((defaultItem) => {
const backendItem = backendItems.find((item: MenuItem) => item.id === defaultItem.id)
if (backendItem) {
return {
...defaultItem,
is_checked: backendItem.is_checked,
children:
defaultItem.children.length > 0 ? mergeConfig(defaultItem.children, backendItem.children || []) : [],
}
}
return defaultItem
})
}
// Ensure auth_config structure is maintained
let authConfig = experimentConfig
if (data.auth_config && data.auth_config.length > 0) {
authConfig = mergeConfig(experimentConfig, data.auth_config)
}
Object.assign(form, {
itinerary_id: data.itinerary.id,
connect_ids,
......@@ -174,18 +207,17 @@ function fetchInfo() {
tag_ids,
group_ids,
material_ids,
auth_config: data.auth_config && data.auth_config.length > 0 ? data.auth_config : experimentConfig,
auth_config: authConfig,
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
is_use_common_live_commodities: data.is_use_common_live_commodities,
})
// checked
if (data.auth_config.length) {
checked.value = data.auth_config.find((item: any) => item.is_checked === false) ? false : true
}
// form.auth_config = data.auth_config.length > 0 ? data.auth_config : experimentConfig
})
}
watchEffect(() => fetchInfo())
......@@ -217,7 +249,7 @@ function handleSubmit() {
group_ids: JSON.stringify(form.group_ids),
material_ids: JSON.stringify(form.material_ids),
auth_config: JSON.stringify(form.auth_config),
live_commodity_ids: JSON.stringify(form.live_commodity_ids)
live_commodity_ids: JSON.stringify(form.live_commodity_ids),
}
params.itinerary_id = params.itinerary_id === '' ? (params.itinerary_id = '0') : params.itinerary_id
updateTripConfig(params).then(() => {
......@@ -292,8 +324,7 @@ const handleSelectAll = () => {
title="配置数字营销实验"
:close-on-click-modal="false"
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-row justify="space-between">
<el-form-item label="实验名称">{{ data.name }}</el-form-item>
......@@ -314,8 +345,7 @@ const handleSelectAll = () => {
@change="handleItemCheck(item)"
v-model="cItem.is_checked"
:label="cItem.name"
size="large"
/>
size="large" />
</template>
</div>
<el-divider />
......@@ -343,8 +373,7 @@ const handleSelectAll = () => {
:label="item.name"
:value="item.id"
:key="item.id"
disabled
></el-option>
disabled></el-option>
</el-select>
</el-form-item>
</el-tab-pane>
......@@ -358,8 +387,7 @@ const handleSelectAll = () => {
v-model="form.user_attr_config.items"
multiple
style="margin-left: 40px"
v-if="!form.user_attr_config.is_all"
>
v-if="!form.user_attr_config.is_all">
<el-option v-for="item in userAttrList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
......@@ -372,8 +400,7 @@ const handleSelectAll = () => {
v-model="form.event_config.items"
multiple
style="margin-left: 40px"
v-if="!form.event_config.is_all"
>
v-if="!form.event_config.is_all">
<el-option v-for="item in metaEventList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
......
......@@ -44,13 +44,14 @@ const form = reactive<ExperimentCreateItem>({
requirements: '',
content: '',
procedure: '',
exam_status: '0'
exam_status: '0',
can_repeat_commit: '0',
})
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)
const teachers_ids = props.data.teacher.map((item) => item.id)
Object.assign(form, props.data, { score, length, teachers_ids })
})
......@@ -72,7 +73,7 @@ const rules = ref<FormRules>({
length: [{ required: true, message: '请输入实验学时' }],
type: [{ required: true, message: '请选择实验类型' }],
teachers_ids: [{ type: 'array', required: true, message: '请选择指导教师', trigger: 'change' }],
score: [{ required: true, message: '请输入实验总成绩' }]
score: [{ required: true, message: '请输入实验总成绩' }],
})
const isUpdate = $computed(() => {
return !!form.id
......@@ -90,7 +91,7 @@ function handleSubmit() {
formRef?.validate().then(() => {
const params = {
...form,
teachers_id: form.teachers_ids?.join(',') || ''
teachers_id: form.teachers_ids?.join(',') || '',
}
isUpdate ? handleUpdate(params) : handleCreate(params)
})
......@@ -118,8 +119,7 @@ function handleUpdate(params: ExperimentCreateItem) {
:title="title"
:close-on-click-modal="false"
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="145px">
<el-form-item label="实验所属部门/学校" prop="organ_id">
<el-select v-model="form.organ_id" style="width: 100%" :disabled="isUpdate" @change="handleOrgChange">
......@@ -164,8 +164,7 @@ function handleUpdate(params: ExperimentCreateItem) {
type="textarea"
maxlength="200"
show-word-limit
placeholder="请输入实验目的"
/>
placeholder="请输入实验目的" />
</el-form-item>
<el-form-item label="实验要求" prop="requirements">
<el-input
......@@ -174,8 +173,7 @@ function handleUpdate(params: ExperimentCreateItem) {
type="textarea"
maxlength="200"
show-word-limit
placeholder="请输入实验要求"
/>
placeholder="请输入实验要求" />
</el-form-item>
<el-form-item label="实验内容及原理" prop="content">
<el-input
......@@ -184,8 +182,7 @@ function handleUpdate(params: ExperimentCreateItem) {
type="textarea"
maxlength="200"
show-word-limit
placeholder="请输入实验内容及原理"
/>
placeholder="请输入实验内容及原理" />
</el-form-item>
<el-form-item label="实验步骤及过程" prop="procedure">
<el-input
......@@ -194,14 +191,19 @@ function handleUpdate(params: ExperimentCreateItem) {
type="textarea"
maxlength="200"
show-word-limit
placeholder="请输入实验步骤及过程"
/>
placeholder="请输入实验步骤及过程" />
</el-form-item>
<el-form-item label="有效状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否支持重复提交" prop="can_repeat_commit">
<el-radio-group v-model="form.can_repeat_commit">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</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>
......
......@@ -144,7 +144,7 @@ function currentRuleNames(value: number) {
})
if (props.data.type === '4') {
// 数字营销实验
return tempList.filter((item) => [1, 5, 6, 7, 8, 9, 10].includes(item.value as number))
return tempList.filter((item) => [1, 5, 6, 7, 8, 9, 11, 12].includes(item.value as number))
} else {
return tempList.filter((item: any) => item.value <= 5)
}
......@@ -181,10 +181,12 @@ function rowScore(percent = 0) {
}
// 编辑题
const handleEdit = function (type: number) {
const handleEdit = function (row: any) {
handleSubmit(function () {
window.open(
`/admin/lab/experiment/questions?id=${props.data.id}&type=${type}&name=${props.data.name}&type_name=${props.data.type_name}&score=${props.data.score}`
`/admin/lab/experiment/questions?id=${props.data.id}&type=${row.type}&name=${props.data.name}&type_name=${
props.data.type_name
}&score=${props.data.score}&percent=${row.percent}&row_score=${rowScore(row.percent)}`
)
})
}
......@@ -283,7 +285,7 @@ onMounted(() => {
<el-divider></el-divider>
<el-form-item>
<el-row justify="space-between" style="width: 100%">
<p>理论考试</p>
<p>理论成绩规则</p>
<el-button type="primary" :icon="Plus" @click="handleAddExamRule" :disabled="form.exam_rules.length >= 1">
</el-button>
</el-row>
......@@ -309,7 +311,7 @@ onMounted(() => {
</template>
</el-table-column>
<el-table-column width="90">
<template #default="{ row }">满分:{{ 100 || rowScore(row.percent) }} </template>
<template #default="{ row }">满分:{{ rowScore(row.percent) }} </template>
</el-table-column>
<el-table-column align="right">
<template #default="{ $index, row }">
......@@ -327,19 +329,14 @@ onMounted(() => {
</el-form-item>
<el-form-item>
<el-row justify="space-between" style="width: 100%">
<p>实操考试</p>
<p>实操成绩规则</p>
<el-button type="primary" :icon="Plus" @click="handleAdd"></el-button>
</el-row>
<el-table :data="form.rule_list" row-key="id">
<el-table-column prop="name" width="170">
<template #default="{ row }">
<el-input v-model="row.name" :maxlength="20" style="width: 100%" v-if="row.type === 5" />
<el-select
v-model="row.type"
:disabled="row.type === 1"
style="width: 100%"
@change="handleTypeChange(row)"
v-else>
<el-select v-model="row.type" style="width: 100%" @change="handleTypeChange(row)" v-else>
<el-option v-for="item in currentRuleNames(row.type)" :key="item.value" v-bind="item"></el-option>
</el-select>
</template>
......@@ -354,24 +351,23 @@ onMounted(() => {
<el-radio-group v-model="row.rule_mode" size="small">
<el-radio :label="1">人工评分</el-radio>
<!-- <el-radio :label="2" v-if="[2, 3].includes(row.type)">自动评分</el-radio> -->
<el-radio :label="2" :disabled="row.type === 1 || row.type === 8">自动评分</el-radio>
<el-radio :label="2" :disabled="row.type === 1 || row.type === 5 || row.type === 8">自动评分</el-radio>
</el-radio-group>
</template>
</el-table-column>
<el-table-column width="90">
<template #default="{ row }">满分:{{ 100 || rowScore(row.percent) }} </template>
<template #default="{ row }">满分:{{ rowScore(row.percent) }} </template>
</el-table-column>
<el-table-column align="right">
<template #default="{ $index, row }">
<div class="btn-box">
<!-- || row.type === 8 -->
<el-button
:disabled="row.type === 1"
style="padding: 0"
text
type="primary"
@click="handleEdit(row.type)"
v-if="row.type !== 1"
@click="handleEdit(row)"
v-if="row.type !== 1 && row.type !== 5"
>编辑</el-button
>
<el-button style="padding: 0" text type="primary" @click="handleRemove($index)">删除</el-button>
......
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { Plus, CircleCloseFilled } from '@element-plus/icons-vue'
import { useFileDialog } from '@vueuse/core'
import { upload } from '@/utils/upload'
import { getExcelTotalLine, checkAutoGenerateData } from '../../api'
import { useAppConfig } from '@/composables/useAppConfig'
import { useEvent } from '../../composables/useQuestion'
const appConfig = useAppConfig()
defineEmits(['remove'])
const route = useRoute()
const modelValue: any = defineModel()
const formRef = ref<FormInstance>()
const form = reactive({
questions: modelValue,
})
const { eventList } = useEvent(route.query.id as string)
const { files, open, reset } = useFileDialog({
accept:
'.csv, .xls, .xlsx, text/csv, application/csv,text/comma-separated-values, application/csv, application/excel,application/vnd.msexcel, text/anytext, application/vnd. ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
multiple: false,
})
let currentItem: any = null
function handleUpload(item: any) {
currentItem = item
open()
}
watchEffect(async () => {
if (!files.value?.length) return
const [file] = files.value
const url = await upload(file)
const res = await getExcelTotalLine({ file })
const total = res.data.row_count || 0
if (currentItem) {
currentItem.answer.data.file = {
name: file.name,
size: file.size,
type: file.type,
is_download: false,
total,
url,
}
}
reset()
})
const href = computed(() => {
const dmlURL = appConfig.dmlURL || import.meta.env.VITE_DML_URL
return `${dmlURL}/connect?experiment_id=${route.query.id}`
})
async function handleEventChange(item: any) {
if (item.answer.data.type != 2) return
const res = await checkAutoGenerateData({ experiment_id: route.query.id as string, event_id: item.answer.choose })
item.answer.data.exists = res.data.exists
}
function handleDataTypeChange(item: any) {
item.answer.rule.type = item.answer.data.type == 2 ? '3' : '1'
}
defineExpose({ formRef })
</script>
<template>
<el-form ref="formRef" label-width="120px" :model="form" scroll-to-error>
<el-card shadow="hover" :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="$emit('remove', index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40"
><CircleCloseFilled
/></el-icon>
<div class="head-box">
<el-form-item
label="本题分值"
class="head-r"
:prop="`questions.${index}.score`"
:rules="{ required: true, message: '请输入' }">
<el-input-number v-model="item.score" :min="0" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form-item label="题目标题" :prop="`questions.${index}.title`" :rules="{ required: true, message: '请输入' }">
<el-input v-model="item.title" placeholder="请输入" />
</el-form-item>
<el-form-item label="题干内容">
<el-input v-model="item.content" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
<el-form-item
label="选择事件"
:prop="`questions.${index}.answer.choose`"
:rules="{ required: true, message: '请输入' }">
<el-select v-model="item.answer.choose" @change="handleEventChange(item)">
<el-option v-for="item in eventList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="事件数据集">
<div>
<el-radio-group v-model="item.answer.data.type" @change="handleDataTypeChange(item)">
<el-radio label="1">指定文件</el-radio>
<el-radio label="2">自动生成</el-radio>
</el-radio-group>
<div class="form-item-box">
<template v-if="item.answer.data.type == 1">
<el-button :icon="Plus" size="large" @click="handleUpload(item)"></el-button>
<template v-if="item.answer.data.file.url">
<div class="file-info">
<p>{{ item.answer.data.file.name }}</p>
<p>总数据量:{{ item.answer.data.file.total }}</p>
</div>
<el-button type="primary" plain @click="item.answer.data.file = {}">删除数据集</el-button>
</template>
</template>
<template v-else>
<a :href="href" target="_blank">
<el-button type="primary">查看自动生成规则</el-button>
</a>
<el-alert
title="该事件尚未维护自动生成数据规则,请维护!"
type="info"
style="margin-left: 40px"
v-if="!item.answer.data.exists" />
</template>
</div>
</div>
</el-form-item>
<el-form-item label="评分规则">
<el-radio-group v-model="item.answer.rule.type">
<template v-if="item.answer.data.type == 1">
<el-radio label="1">上传成功+数据量匹配</el-radio>
<el-radio label="2">仅上传成功</el-radio>
</template>
<el-radio label="3">仅数据量匹配</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="答案解析" v-if="false">
<el-input v-model="item.answer_analysis" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
</el-card>
</el-form>
</template>
<style lang="scss" scoped>
.close {
position: absolute;
top: -10px;
right: -10px;
cursor: pointer;
}
.box-card {
padding-top: 20px;
position: relative;
margin-bottom: 30px;
overflow: visible;
}
.head-box {
display: flex;
margin-bottom: 30px;
.head-r {
margin-left: auto;
}
}
.form-item-box {
display: flex;
align-items: center;
p {
font-size: 13px;
margin: 0 20px 0 10px;
line-height: 1.4;
}
}
</style>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { CircleCloseFilled } from '@element-plus/icons-vue'
import { getQuestionGroup } from '../../api'
import { useGroup } from '../../composables/useQuestion'
defineEmits(['remove'])
const route = useRoute()
const modelValue: any = defineModel()
const ruleFormRef = ref<FormInstance>()
const formRef = ref<FormInstance>()
const form = reactive({
questions: modelValue,
})
const { groupList } = useGroup(route.query.id as string)
const removeQuestion = (index: number) => {
modelValue.value.splice(index, 1)
function getGroupCount(id: string) {
return groupList.value.find((item) => item.id === id)?.count || '未计算'
}
let options = $ref<{ id: string; name: string }[]>()
onMounted(() => {
const dom: any = document.querySelectorAll('.app-main')[0]
dom.style.overflow = 'visible'
getQuestionGroup({ experiment_id: route.query.id as string }).then(res => {
options = res.data.items
})
})
defineExpose({ formRef })
</script>
<template>
<el-card :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="removeQuestion(index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40"
><CircleCloseFilled
/></el-icon>
<div class="head-box">
<el-tabs v-model="item.type" type="card" class="demo-tabs">
<el-tab-pane label="静态群组" :name="301"></el-tab-pane>
<el-tab-pane label="动态群组" :name="302"></el-tab-pane>
</el-tabs>
<el-form-item label="本题分值" class="head-r">
<el-input-number v-model="item.score" :min="1" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form ref="ruleFormRef" label-width="120px" class="demo-ruleForm" status-icon>
<el-form-item label="题目标题" :required="true">
<el-form ref="formRef" label-width="120px" :model="form" scroll-to-error>
<el-card shadow="hover" :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="$emit('remove', index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40">
<CircleCloseFilled />
</el-icon>
<div class="head-box">
<el-tabs v-model="item.type" type="card">
<el-tab-pane label="静态群组" :name="301"></el-tab-pane>
<el-tab-pane label="动态群组" :name="302"></el-tab-pane>
</el-tabs>
<el-form-item
label="本题分值"
class="head-r"
:prop="`questions.${index}.score`"
:rules="{ required: true, message: '请输入' }">
<el-input-number v-model="item.score" :min="0" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form-item label="题目标题" :prop="`questions.${index}.title`" :rules="{ required: true, message: '请输入' }">
<el-input v-model="item.title" placeholder="请输入" />
</el-form-item>
<el-form-item label="题干内容">
<el-input v-model="item.content" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
<el-form-item label="正确答案" :required="true">
<span v-if="item.type === 301">创建静态群组成功</span>
<el-select v-else v-model="item.answer" class="m-2" placeholder="请选择" size="large">
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
<el-form-item label="评分规则">
<el-radio-group v-model="item.answer.rule.type">
<el-radio label="1">群组规则+计算结果</el-radio>
<el-radio label="2">仅群组规则</el-radio>
<el-radio label="3">仅计算结果</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="正确群组规则"
:prop="`questions.${index}.answer.choose`"
:rules="{ required: true, message: '请输入' }">
<el-select v-model="item.answer.choose" placeholder="请选择">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<p style="margin-left: 10px">满足该群组的用户数量:{{ getGroupCount(item.answer.choose) }}</p>
</el-form-item>
<el-form-item label="答案解析">
<el-form-item label="答案解析" v-if="false">
<el-input v-model="item.answer_analysis" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
</el-form>
</el-card>
</el-card>
</el-form>
</template>
<style lang="scss" scoped>
.close {
......
......@@ -2,39 +2,41 @@
import type { FormInstance } from 'element-plus'
import { CircleCloseFilled } from '@element-plus/icons-vue'
const modelValue: any = defineModel()
const ruleFormRef = ref<FormInstance>()
defineEmits(['remove'])
const removeQuestion = (index: number) => {
modelValue.value.splice(index, 1)
}
const modelValue: any = defineModel()
onMounted(() => {
const dom: any = document.querySelectorAll('.app-main')[0]
dom.style.overflow = 'visible'
const formRef = ref<FormInstance>()
const form = reactive({
questions: modelValue,
})
defineExpose({ formRef })
</script>
<template>
<el-card :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="removeQuestion(index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40"
><CircleCloseFilled
/></el-icon>
<div class="head-box">
<el-form-item label="本题分值" class="head-r">
<el-input-number v-model="item.score" :min="1" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form ref="ruleFormRef" label-width="120px" class="demo-ruleForm" status-icon>
<el-form-item label="题目标题" :required="true">
<el-form ref="formRef" label-width="120px" :model="form" scroll-to-error>
<el-card :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="$emit('remove', index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40">
<CircleCloseFilled />
</el-icon>
<div class="head-box">
<el-form-item
label="本题分值"
class="head-r"
:prop="`questions.${index}.score`"
:rules="{ required: true, message: '请输入' }">
<el-input-number v-model="item.score" :min="1" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form-item label="题目标题" :prop="`questions.${index}.title`" :rules="{ required: true, message: '请输入' }">
<el-input v-model="item.title" placeholder="请输入" />
</el-form-item>
<el-form-item label="题干内容">
<el-input v-model="item.content" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
</el-form>
</el-card>
</el-card>
</el-form>
</template>
<style lang="scss" scoped>
.close {
......
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { Document, CircleCheck, CircleClose, CircleCloseFilled } from '@element-plus/icons-vue'
import AppUpload from '@/components/base/AppUpload.vue'
import type { UploadFile } from 'element-plus'
import { CircleCloseFilled } from '@element-plus/icons-vue'
const modelValue: any = defineModel()
import { useMaterial } from '../../composables/useQuestion'
const ruleFormRef = ref<FormInstance>()
defineEmits(['remove'])
// 移除上传文件
const handleRemove = (file: UploadFile) => {
if (file) {
modelValue.value.forEach((item: any) => {
const index = item.files.findIndex((cItem: { url: string }) => cItem.url === file.url)
item.files.splice(index, 1)
})
}
}
const route = useRoute()
const handleDownload = (file: any) => {
if (file) {
modelValue.value.forEach((i: any) => {
const item: any = i.files.find((item: any) => item.url === file.url)
if (item) item.is_download = file.is_download
})
}
}
const modelValue: any = defineModel()
const removeQuestion = (index: number) => {
modelValue.value.splice(index, 1)
}
const formRef = ref<FormInstance>()
const form = reactive({ questions: modelValue })
const options = $ref<{ id: number; name: string }[]>([
{ id: 401, name: '文本资料' },
{ id: 402, name: '图片资料' },
{ id: 403, name: '语音资料' },
{ id: 404, name: '视频资料' },
{ id: 405, name: 'H5资料' },
{ id: 406, name: '二维码资料' },
{ id: 407, name: '小程序资料' },
{ id: 408, name: '卡券资料' },
])
onMounted(() => {
const dom: any = document.querySelectorAll('.app-main')[0]
dom.style.overflow = 'visible'
})
const options = [
{ id: 401, name: '文本', type: '1' },
{ id: 402, name: '图片', type: '2' },
{ id: 403, name: '语音', type: '3' },
{ id: 404, name: '视频', type: '4' },
{ id: 405, name: 'H5', type: '5' },
{ id: 406, name: '二维码', type: '6' },
{ id: 407, name: '小程序', type: '7' },
{ id: 408, name: '卡券', type: '8' },
]
const getTips = function (n: number) {
const tipText: any = {
402: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M',
403: '试题文件支持格式包含:mp3 wav,大小不超过5M',
404: '试题文件支持格式包含:帧率为25fps/输出码率为4M/输出格式为mp4,建议采用格式工厂等工具处理后上传。',
405: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M',
406: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M',
407: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M',
508: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M',
}
return tipText[n]
const aiOptions = [
{ label: '文心一言', value: '1' },
{ label: 'DeepSeek', value: '2' },
{ label: '通义千问', value: '3' },
{ label: '天工', value: '4' },
]
const { materialList } = useMaterial(route.query.id as string)
function filterMateriaList(id: number) {
const option = options.find((item) => item.id === id)
return materialList.value.filter((item) => item.type === option?.type)
}
defineExpose({ formRef })
</script>
<template>
<el-card :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="removeQuestion(index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40"
><CircleCloseFilled
/></el-icon>
<div class="head-box">
<el-select v-model="item.type" class="m-2" placeholder="请选择" size="large">
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-form-item label="本题分值" class="head-r">
<el-input-number v-model="item.score" :min="1" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form ref="ruleFormRef" label-width="120px" class="demo-ruleForm" status-icon>
<el-form-item label="题目标题" :required="true">
<el-form ref="formRef" label-width="120px" :model="form" scroll-to-error>
<el-card :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="$emit('remove', index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40">
<CircleCloseFilled />
</el-icon>
<div class="head-box">
<el-form-item
label="本题分值"
class="head-r"
:prop="`questions.${index}.score`"
:rules="{ required: true, message: '请输入' }">
<el-input-number v-model="item.score" :min="0" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form-item label="题目标题" :prop="`questions.${index}.title`" :rules="{ required: true, message: '请输入' }">
<el-input v-model="item.title" placeholder="请输入" />
</el-form-item>
<el-form-item label="题干内容">
<el-input v-model="item.content" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
<el-form-item label="正确答案" :required="true">
<span>上传成功</span>
<el-form-item
label="营销物料类别"
:prop="`questions.${index}.type`"
:rules="{ required: true, message: '请选择' }">
<el-radio-group v-model="item.type" placeholder="请选择">
<el-radio v-for="item in options" :key="item.id" :label="item.id">{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="答案解析">
<el-input v-model="item.answer_analysis" :rows="5" type="textarea" placeholder="请输入" />
<el-form-item
label="参考答案"
:prop="`questions.${index}.answer.choose`"
:rules="{ required: true, message: '请选择' }">
<el-select v-model="item.answer.choose" placeholder="请选择" clearable>
<el-option
v-for="materia in filterMateriaList(item.type)"
:key="materia.id"
:label="materia.name"
:value="materia.id"></el-option>
</el-select>
</el-form-item>
<el-form-item
label="评分AI"
:prop="`questions.${index}.answer.ai`"
:rules="{ required: true, message: '请选择' }">
<el-select v-model="item.answer.ai" placeholder="请选择">
<el-option v-for="item in aiOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="试题文件" v-if="item.type !== 401">
<AppUpload v-model="item.files">
<template #tip>{{ getTips(item.type) }}</template>
<template #file="{ file }">
<div class="upload-box">
<div class="upload-loading">
<el-icon style="margin-right: 5px"><Document /></el-icon>
<p style="margin-right: 5px">{{ file.name }}</p>
<p v-if="file.status === 'uploading'">{{ file.percentage }}%</p>
<template v-if="file.status === 'success'">
<el-icon class="succ-icon1" color="#67c23a" size="18" style="margin-left: 30px"
><CircleCheck
/></el-icon>
<el-icon class="succ-icon2" size="18" style="margin-left: 30px" @click="handleRemove(file)"
><CircleClose
/></el-icon>
</template>
</div>
<el-switch
@change="handleDownload(file)"
v-model="file.is_download"
size="large"
active-text="能否下载" />
</div>
</template>
</AppUpload>
<el-form-item label="答案解析" v-if="false">
<el-input v-model="item.answer_analysis" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
</el-form>
</el-card>
</el-card>
</el-form>
</template>
<style lang="scss" scoped>
.close {
......
<script setup lang="ts">
<script setup>
import { getQuestions } from '../../api'
const route: any = useRoute()
const route = useRoute()
onMounted(() => {
getCurrentQuestions()
})
// 获取题
let list: any = $ref()
const list = ref([])
const getCurrentQuestions = function () {
getQuestions({ experiment_id: route.query.id }).then((res) => {
list = res.data.items
list.value = res.data.items
})
}
</script>
<template>
<el-dialog
title="2023商业数据分析大赛决赛试题"
title="试题"
:close-on-click-modal="false"
width="50%"
@update:modelValue="(value) => $emit('update:modelValue', value)">
......@@ -46,10 +46,6 @@ const getCurrentQuestions = function () {
font-size: 14px;
}
.item {
// margin-bottom: 18px;
}
.box-card {
margin-bottom: 20px;
}
......
......@@ -37,11 +37,13 @@ window.onscroll = function () {
const typeName = computed(() => {
const name: any = {
'10': '用户/事件数据',
'6': '标签管理',
'7': '群组管理',
'8': '用户旅程',
'9': '营销资料管理',
'8': '用户旅程'
'10': '用户/事件数据',
'11': '用户数据导入/生成',
'12': '事件数据导入/生成',
}
return name[(route.query?.type as string) || '10']
})
......@@ -52,10 +54,10 @@ const typeName = computed(() => {
<div class="order-score">
<div class="order-score_flex">
<span class="el-tooltip question-total-number">{{ score }}</span>
<span class="el-tooltip paper-total-score">/100</span>
<span class="el-tooltip paper-total-score">/{{ route.query.row_score }}</span>
<em></em>
</div>
<div class="tip">注:每模块满分为100,请注意每题分值。</div>
<!-- <div class="tip">注:每模块满分为100,请注意每题分值。</div> -->
</div>
<div class="order-list">
<div class="title">{{ typeName }}</div>
......@@ -64,8 +66,7 @@ const typeName = computed(() => {
@click="handleOrder(index)"
:class="orderSite === index ? 'li active' : 'li'"
v-for="(item, index) in props.data"
:key="item"
>
:key="item">
{{ index + 1 }}
</div>
</div>
......
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { CircleCloseFilled } from '@element-plus/icons-vue'
import { getQuestionTags } from '../../api'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
import { useTag } from '../../composables/useQuestion'
defineEmits(['remove'])
const route = useRoute()
const modelValue: any = defineModel()
const ruleFormRef = ref<FormInstance>()
const formRef = ref<FormInstance>()
const form = reactive({
questions: modelValue,
})
const removeQuestion = (index: number) => {
modelValue.value.splice(index, 1)
}
const { tagList } = useTag(route.query.id as string)
let options = $ref<{ id: string; name: string }[]>()
onMounted(() => {
const dom: any = document.querySelectorAll('.app-main')[0]
dom.style.overflow = 'visible'
getQuestionTags({ experiment_id: route.query.id as string }).then(res => {
options = res.data.items
})
})
function getTagCount(id: string) {
return tagList.value.find((item) => item.id === id)?.count || '未计算'
}
const labelTitle = computed(() => {
return appConfig.system === 'dml' ? '标签目录' : '标签类型'
})
defineExpose({ formRef })
</script>
<template>
<el-card :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="removeQuestion(index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40"><CircleCloseFilled /></el-icon>
<div class="head-box">
<el-tabs v-model="item.type" type="card" class="demo-tabs">
<el-tab-pane :label="labelTitle" :name="201"></el-tab-pane>
<el-tab-pane label="标签" :name="202"></el-tab-pane>
</el-tabs>
<el-form-item label="本题分值" class="head-r">
<el-input-number v-model="item.score" :min="1" :max="100" controls-position="right" />
<el-form ref="formRef" label-width="120px" :model="form" scroll-to-error>
<el-card
shadow="hover"
:id="`site-card${index}`"
class="box-card"
v-for="(item, index) in form.questions"
:key="item">
<el-icon @click="$emit('remove', index)" v-if="form.questions.length > 1" class="close" size="28" color="#c01c40">
<CircleCloseFilled />
</el-icon>
<el-form-item
label="本题分值"
class="head-r"
:prop="`questions.${index}.score`"
:rules="{ required: true, message: '请输入' }">
<el-input-number v-model="item.score" :min="0" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form ref="ruleFormRef" label-width="120px" class="demo-ruleForm" status-icon>
<el-form-item label="题目标题" :required="true">
<el-form-item label="题目标题" :prop="`questions.${index}.title`" :rules="{ required: true, message: '请输入' }">
<el-input v-model="item.title" placeholder="请输入" />
</el-form-item>
<el-form-item label="题干内容">
<el-input v-model="item.content" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
<el-form-item label="正确答案" :required="true">
<span v-if="item.type === 201">创建{{ labelTitle }}成功</span>
<el-select v-else v-model="item.answer" class="m-2" placeholder="请选择" size="large">
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
<el-form-item label="评分规则">
<el-radio-group v-model="item.answer.rule.type">
<el-radio label="1">标签规则+计算结果</el-radio>
<el-radio label="2">仅标签规则</el-radio>
<el-radio label="3">仅计算结果</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="正确标签规则"
:prop="`questions.${index}.answer.choose`"
:rules="{ required: true, message: '请输入' }">
<el-select v-model="item.answer.choose">
<el-option v-for="item in tagList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<p style="margin-left: 10px">满足该标签的用户数量:{{ getTagCount(item.answer.choose) }}</p>
</el-form-item>
<el-form-item label="答案解析">
<el-form-item label="答案解析" v-if="false">
<el-input v-model="item.answer_analysis" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
</el-form>
</el-card>
</el-card>
</el-form>
</template>
<style lang="scss" scoped>
.close {
......
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { Document, CircleCheck, CircleClose, CircleCloseFilled } from '@element-plus/icons-vue'
import AppUpload from '@/components/base/AppUpload.vue'
import type { UploadFile } from 'element-plus'
import { Plus, CircleCloseFilled } from '@element-plus/icons-vue'
import { useFileDialog } from '@vueuse/core'
import { upload } from '@/utils/upload'
import { getExcelTotalLine, checkAutoGenerateData } from '../../api'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
defineEmits(['remove'])
const route = useRoute()
const modelValue: any = defineModel()
const ruleFormRef = ref<FormInstance>()
const formRef = ref<FormInstance>()
const form = reactive({
questions: modelValue,
})
// 移除上传文件
const handleRemove = (file: UploadFile) => {
if (file) {
modelValue.value.forEach((item: any) => {
const index = item.files.findIndex((cItem: { url: string }) => cItem.url === file.url)
item.files.splice(index, 1)
})
}
const { files, open, reset } = useFileDialog({
accept:
'.csv, .xls, .xlsx, text/csv, application/csv,text/comma-separated-values, application/csv, application/excel,application/vnd.msexcel, text/anytext, application/vnd. ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
multiple: false,
})
let currentItem: any = null
function handleUpload(item: any) {
currentItem = item
open()
}
const handleDownload = (file: any) => {
if (file) {
modelValue.value.forEach((i: any) => {
const item: any = i.files.find((item: any) => item.url === file.url)
if (item) item.is_download = file.is_download
})
watchEffect(async () => {
if (!files.value?.length) return
const [file] = files.value
const url = await upload(file)
const res = await getExcelTotalLine({ file })
const total = res.data.row_count || 0
if (currentItem) {
currentItem.answer.data.file = {
name: file.name,
size: file.size,
type: file.type,
is_download: false,
total,
url,
}
}
}
reset()
})
const removeQuestion = (index: number) => {
modelValue.value.splice(index, 1)
const href = computed(() => {
const dmlURL = appConfig.dmlURL || import.meta.env.VITE_DML_URL
return `${dmlURL}/connect?experiment_id=${route.query.id}`
})
async function handleDataTypeChange(item: any) {
item.answer.rule.type = item.answer.data.type == 2 ? '3' : '1'
if (item.answer.data.type != 2) return
const res = await checkAutoGenerateData({ experiment_id: route.query.id as string })
item.answer.data.exists = res.data.exists
}
onMounted(() => {
const dom: any = document.querySelectorAll('.app-main')[0]
dom.style.overflow = 'visible'
})
defineExpose({ formRef })
</script>
<template>
<el-card :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="removeQuestion(index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40"
><CircleCloseFilled
/></el-icon>
<div class="head-box">
<el-tabs v-model="item.type" type="card" class="demo-tabs">
<el-tab-pane label="用户数据" :name="101"></el-tab-pane>
<el-tab-pane label="事件数据" :name="102"></el-tab-pane>
</el-tabs>
<el-form-item label="本题分值" class="head-r">
<el-input-number v-model="item.score" :min="1" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form ref="ruleFormRef" label-width="120px" class="demo-ruleForm" status-icon>
<el-form-item label="题目标题" :required="true">
<el-form ref="formRef" label-width="120px" :model="form" scroll-to-error>
<el-card shadow="hover" :id="`site-card${index}`" class="box-card" v-for="(item, index) in modelValue" :key="item">
<el-icon @click="$emit('remove', index)" v-if="modelValue.length > 1" class="close" size="28" color="#c01c40"
><CircleCloseFilled
/></el-icon>
<div class="head-box">
<el-form-item
label="本题分值"
class="head-r"
:prop="`questions.${index}.score`"
:rules="{ required: true, message: '请输入' }">
<el-input-number v-model="item.score" :min="0" :max="100" controls-position="right" />
</el-form-item>
</div>
<el-form-item label="题目标题" :prop="`questions.${index}.title`" :rules="{ required: true, message: '请输入' }">
<el-input v-model="item.title" placeholder="请输入" />
</el-form-item>
<el-form-item label="题干内容">
<el-input v-model="item.content" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
<el-form-item label="正确答案" :required="true"> 导入成功 </el-form-item>
<el-form-item label="答案解析">
<el-input v-model="item.answer_analysis" :rows="5" type="textarea" placeholder="请输入" />
<el-form-item label="用户数据集">
<div>
<el-radio-group v-model="item.answer.data.type" @change="handleDataTypeChange(item)">
<el-radio label="1">指定文件</el-radio>
<el-radio label="2">自动生成</el-radio>
</el-radio-group>
<div class="form-item-box">
<template v-if="item.answer.data.type == 1">
<el-button :icon="Plus" size="large" @click="handleUpload(item)"></el-button>
<template v-if="item.answer.data.file.url">
<div class="file-info">
<p>{{ item.answer.data.file.name }}</p>
<p>总数据量:{{ item.answer.data.file.total }}</p>
</div>
<el-button type="primary" plain @click="item.answer.data.file = {}">删除数据集</el-button>
</template>
</template>
<template v-else>
<a :href="href" target="_blank">
<el-button type="primary">查看自动生成规则</el-button>
</a>
<el-alert
title="该实验尚未维护自动生成数据规则,请维护!"
type="info"
style="margin-left: 40px"
v-if="!item.answer.data.exists" />
</template>
</div>
</div>
</el-form-item>
<el-form-item label="试题文件">
<AppUpload v-model="item.files">
<template #tip>试题文件支持格式包含:png jpg doc docx xls xlsx pdf ppt pptx,大小不超过50M</template>
<template #file="{ file }">
<div class="upload-box">
<div class="upload-loading">
<el-icon style="margin-right: 5px"><Document /></el-icon>
<p style="margin-right: 5px">{{ file.name }}</p>
<p v-if="file.status === 'uploading'">{{ file.percentage }}%</p>
<template v-if="file.status === 'success'">
<el-icon class="succ-icon1" color="#67c23a" size="18" style="margin-left: 30px"
><CircleCheck
/></el-icon>
<el-icon class="succ-icon2" size="18" style="margin-left: 30px" @click="handleRemove(file)"
><CircleClose
/></el-icon>
</template>
</div>
<el-switch
@change="handleDownload(file)"
v-model="file.is_download"
size="large"
active-text="能否下载"
/>
</div>
<el-form-item label="评分规则">
<el-radio-group v-model="item.answer.rule.type">
<template v-if="item.answer.data.type == 1">
<el-radio label="1">上传成功+数据量匹配</el-radio>
<el-radio label="2">仅上传成功</el-radio>
</template>
</AppUpload>
<el-radio label="3">仅数据量匹配</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-card>
<el-form-item label="答案解析" v-if="false">
<el-input v-model="item.answer_analysis" :rows="5" type="textarea" placeholder="请输入" />
</el-form-item>
</el-card>
</el-form>
</template>
<style lang="scss" scoped>
.close {
......@@ -113,24 +150,14 @@ onMounted(() => {
margin-left: auto;
}
}
.upload-box {
.form-item-box {
display: flex;
.upload-loading {
width: 300px;
display: flex;
align-items: center;
&:hover {
.succ-icon2 {
display: block;
}
.succ-icon1 {
display: none;
}
}
.succ-icon2 {
display: none;
cursor: pointer;
}
align-items: center;
p {
font-size: 13px;
margin: 0 20px 0 10px;
line-height: 1.4;
}
}
</style>
import { getQuestionTags, getQuestionGroup, getQuestionMaterials, getQuestionEvents } from '../api'
// 标签类型
export interface TagType {
id: string
name: string
count: string
}
// 群组类型
export interface GroupType {
id: string
name: string
count: string
}
export interface MaterialType {
id: string
name: string
type: string
}
// 事件类型
interface EventType {
id: string
name: string
}
// 所有标签
const tagList = ref<TagType[]>([])
export function useTag(experiment_id: string) {
function fetchTagList() {
getQuestionTags({ experiment_id }).then((res: any) => {
tagList.value = res.data.items
})
}
onMounted(() => {
if (!tagList.value?.length) fetchTagList()
})
return { fetchTagList, tagList }
}
// 所有群组
const groupList = ref<GroupType[]>([])
export function useGroup(experiment_id: string) {
function fetchGroupList() {
getQuestionGroup({ experiment_id }).then((res: any) => {
groupList.value = res.data.items
})
}
onMounted(() => {
if (!groupList.value?.length) fetchGroupList()
})
return { fetchGroupList, groupList }
}
// 所有资料
const materialList = ref<MaterialType[]>([])
export function useMaterial(experiment_id: string, type = '') {
function fetchMaterialList() {
getQuestionMaterials({ experiment_id, type }).then((res: any) => {
materialList.value = res.data.items
})
}
onMounted(() => {
if (!materialList.value?.length) fetchMaterialList()
})
return { fetchMaterialList, materialList }
}
// 所有事件
const eventList = ref<EventType[]>([])
export function useEvent(experiment_id: string) {
function fetchMetaEventList() {
getQuestionEvents({ experiment_id }).then((res: any) => {
eventList.value = res.data.items
})
}
onMounted(() => {
if (!eventList.value?.length) fetchMetaEventList()
})
return { fetchMetaEventList, eventList }
}
......@@ -50,6 +50,7 @@ export interface ExperimentCreateItem {
content: string
procedure: string
exam_status: string
can_repeat_commit: '0' | '1'
}
export interface ClassItem {
......
......@@ -27,7 +27,7 @@ const params = reactive({
id: '',
course_id: '',
name: '',
status: ''
status: '',
})
watch(
() => params.course_id,
......@@ -50,12 +50,12 @@ const listOptions = $computed(() => {
},
callback(data: { total: number; list: ExperimentItem[] }) {
const { list, total } = data
const dataList = list.map(item => {
const teacher_names = item.teacher.map(teacher => teacher.name).join('、')
const dataList = list.map((item) => {
const teacher_names = item.teacher.map((teacher) => teacher.name).join('、')
return { ...item, teacher_names }
})
return { list: dataList, total }
}
},
},
filters: [
{
......@@ -65,7 +65,7 @@ const listOptions = $computed(() => {
placeholder: '请选择实验课程',
options: courses.value,
labelKey: 'name',
valueKey: 'id'
valueKey: 'id',
},
{
type: 'select',
......@@ -74,15 +74,15 @@ const listOptions = $computed(() => {
placeholder: '请选择实验',
options: courseExperiments.value,
labelKey: 'name',
valueKey: 'id'
valueKey: 'id',
},
{
type: 'select',
prop: 'status',
label: '生效状态',
placeholder: '请选择生效状态',
options: status
}
options: status,
},
],
columns: [
{ label: '序号', type: 'index', width: 60 },
......@@ -95,8 +95,8 @@ const listOptions = $computed(() => {
{ label: '生效状态', prop: 'status_name' },
{ label: '更新人', prop: 'updated_operator_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 250 }
]
{ label: '操作', slots: 'table-x', width: 250 },
],
}
})
......@@ -162,7 +162,7 @@ async function handleDelete(row: ExperimentItem) {
<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>
<el-button type="primary" round @click="handleDelete(row)">删除</el-button>
<!-- 功能按钮移入详情里 s v-if="false" -->
<el-dropdown style="margin-left: 12px" v-if="false">
......@@ -226,14 +226,12 @@ async function handleDelete(row: ExperimentItem) {
v-model="gradeRulesDialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="gradeRulesDialogVisible && rowData"
></GradeRulesDialog>
v-if="gradeRulesDialogVisible && rowData"></GradeRulesDialog>
<!-- 配置数字营销实验 -->
<DMLFormDialog v-model="dmlDialogVisible" :data="rowData" v-if="dmlDialogVisible && rowData"></DMLFormDialog>
<CopyDialog
v-model="copyDialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="copyDialogVisible && rowData"
></CopyDialog>
v-if="copyDialogVisible && rowData"></CopyDialog>
</template>
<script setup lang="ts">
<script setup>
import { updateQuestions, getQuestions } from '../api'
import { ElMessage } from 'element-plus'
const PreviewDialog = defineAsyncComponent(() => import('../components/Questions/PreviewDialog.vue'))
const QuestionsOrder = defineAsyncComponent(() => import('../components/Questions/QuestionsOrder.vue'))
const UserQuestion = defineAsyncComponent(() => import('../components/Questions/UserQuestion.vue'))
const EventQuestion = defineAsyncComponent(() => import('../components/Questions/EventQuestion.vue'))
const TagQuestion = defineAsyncComponent(() => import('../components/Questions/TagQuestion.vue'))
const GroupQuestion = defineAsyncComponent(() => import('../components/Questions/GroupQuestion.vue'))
const MaterialQuestion = defineAsyncComponent(() => import('../components/Questions/MaterialQuestion.vue'))
const JourneyQuestion = defineAsyncComponent(() => import('../components/Questions/JourneyQuestion.vue'))
const route: any = useRoute()
const router = useRouter()
const route = useRoute()
const getQuestionTypeInitValue = function () {
const type: any = { 10: 101, 6: 201, 7: 301, 9: 401, 8: 501 }
return type[parseInt(route.query?.type || '10')]
}
let data = $ref([
{
type: getQuestionTypeInitValue(),
score: 1,
title: '',
content: '',
answer_analysis: '',
answer: '',
files: []
/*
101实验用户数据题 102实验事件数据题
201实验标签类型题 202实验标签题
301实验静态群组题 302动态群组题
401实验营销文本资料题 402验营销图片资料题 403验营销语音资料题 404验营销视频资料题 405验营销H5资料题 406验营销二维码资料题 407验营销小程序资料题 408验营销卡券资料题
501用户旅程题
601实验报告题
*/
const types = computed(() => {
const typesJson = {
6: [201, 202],
7: [301, 302],
8: [501],
9: [401, 402, 403, 404, 405, 406, 407, 408],
10: [101, 102],
11: [101],
12: [102],
}
])
return typesJson[route.query.type] || route.query.type
})
const addQuestion = () => {
data.push({
type: getQuestionTypeInitValue(),
score: 1,
const type = computed(() => {
const type = { 6: 202, 7: 301, 8: 501, 9: 401, 10: 101, 11: 101, 12: 102 }
return type[route.query.type] || 101
})
const hasAdd = computed(() => {
return route.query.type != 11
})
const createDefaultQuestion = () => {
return {
type: type.value,
score: 0,
title: '',
content: '',
answer_analysis: '',
answer: '',
files: []
})
answer: {
choose: '',
data: { type: '1', file: {} },
rule: { type: '1' },
ai: '1',
},
}
}
const previewDialogVisible = $ref(false)
const questionRef = ref()
const handleSubmit = function () {
const type: any = { 10: '1', 6: '2', 7: '3', 9: '4', 8: '5' }
console.log(data, 'data')
const params = {
experiment_id: route.query.id,
module: type[route.query.type],
questions: data
}
const questions = ref([{ ...createDefaultQuestion() }])
//判断必填不能为空
function findItem(value: string) {
return data.findIndex((item: any) => item[value] === '')
}
if (findItem('title') !== -1) {
ElMessage.error(`第${findItem('title') + 1}题,题目不能为空`)
return false
const previewDialogVisible = ref(false)
async function getQuestion() {
const resp = await getQuestions({ experiment_id: route.query.id, types: types.value })
if (resp.data.items.length) {
questions.value = resp.data.items.map((item) => {
return { ...item, answer: JSON.parse(item.answer), score: parseInt(item.score) || 0, type: parseInt(item.type) }
})
}
updateQuestions(params).then(res => {
if (res.data.status) {
ElMessage({ message: '保存成功', type: 'success' })
router.go(0)
}
})
}
onMounted(() => {
getCurrentQuestions()
getQuestion()
})
// 获取题
const getCurrentQuestions = function () {
const typesJson: any = {
10: [101, 102],
6: [201, 202],
7: [301, 302],
9: [401, 402, 403, 404, 405, 406, 407, 408],
8: [501]
}
getQuestions({ experiment_id: route.query.id, types: typesJson[route.query.type] }).then(res => {
if (res.data?.items) {
if (res.data.items?.length) {
data = res.data.items.reduce((a: any, b: any) => {
b.type = parseInt(b.type)
b.files.map((item: any) => {
item.is_download === 'true' ? (item.is_download = true) : (item.is_download = false)
return item
})
a.push(b)
return a
}, [])
}
}
// 添加试题
const addQuestion = () => {
questions.value.push({ ...createDefaultQuestion() })
}
const removeQuestion = (index) => {
questions.value.splice(index, 1)
}
// 保存
const handleSubmit = async () => {
await unref(questionRef.value?.formRef).validate()
await updateQuestions({
experiment_id: route.query.id,
module: route.query.type,
questions: questions.value.map((item) => {
return { ...item, answer: JSON.stringify(item.answer) }
}),
})
ElMessage({ message: '保存成功', type: 'success' })
}
</script>
......@@ -108,31 +109,36 @@ const getCurrentQuestions = function () {
<p>实验类型: {{ route.query.type_name }}</p>
<p>实验总成绩:{{ route.query.score }}</p>
</div>
<el-button @click="previewDialogVisible = true">预览</el-button>
<el-button @click="previewDialogVisible = true" v-if="false">预览</el-button>
</div>
</AppCard>
<div class="content-bottom">
<AppCard class="l-card">
<UserQuestion v-model="data" v-if="route.query.type === '10'"></UserQuestion>
<TagQuestion v-model="data" v-if="route.query.type === '6'"></TagQuestion>
<GroupQuestion v-model="data" v-if="route.query.type === '7'"></GroupQuestion>
<MaterialQuestion v-model="data" v-if="route.query.type === '9'"></MaterialQuestion>
<JourneyQuestion v-model="data" v-if="route.query.type === '8'"></JourneyQuestion>
<TagQuestion v-model="questions" ref="questionRef" @remove="removeQuestion" v-if="route.query.type === '6'" />
<GroupQuestion v-model="questions" ref="questionRef" @remove="removeQuestion" v-if="route.query.type === '7'" />
<JourneyQuestion v-model="questions" ref="questionRef" @remove="removeQuestion" v-if="route.query.type === '8'" />
<MaterialQuestion
v-model="questions"
ref="questionRef"
@remove="removeQuestion"
v-if="route.query.type === '9'" />
<UserQuestion v-model="questions" ref="questionRef" @remove="removeQuestion" v-if="route.query.type === '11'" />
<EventQuestion v-model="questions" ref="questionRef" @remove="removeQuestion" v-if="route.query.type === '12'" />
<div class="btn-box">
<div class="btn-l">
<el-button type="primary" @click="addQuestion"> 添加试题 </el-button>
<el-button type="primary" @click="addQuestion" v-if="hasAdd">添加试题</el-button>
<!-- <el-button> 取消 </el-button> -->
</div>
<div class="btn-r">
<el-button type="primary" @click="handleSubmit"> 保存 </el-button>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</div>
</div>
</AppCard>
<AppCard class="r-card">
<QuestionsOrder :data="data"></QuestionsOrder>
<QuestionsOrder :data="questions"></QuestionsOrder>
</AppCard>
</div>
<PreviewDialog v-model="previewDialogVisible"></PreviewDialog>
<PreviewDialog v-model="previewDialogVisible" v-if="previewDialogVisible"></PreviewDialog>
</template>
<style lang="scss" scoped>
.btn-box {
......
<script setup lang="ts">
import type { ExperimentItem, ClassItem } from '../types'
import { CirclePlus, CopyDocument, Setting, Edit, EditPen } from '@element-plus/icons-vue'
import { CirclePlus } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AppList from '@/components/base/AppList.vue'
......@@ -28,7 +28,7 @@ provide('detail', $$(detail))
const teacherText = $computed(() => {
if (!detail) return ''
return detail.teacher.map(item => item.name).join('、')
return detail.teacher.map((item) => item.name).join('、')
})
const appList = $ref<InstanceType<typeof AppList> | null>(null)
......@@ -41,7 +41,7 @@ const listOptions = {
const { total, list, info } = data
detail = info
return { list, total }
}
},
},
columns: [
{ label: '序号', type: 'index', width: 60 },
......@@ -51,8 +51,8 @@ const listOptions = {
{ label: '已完成人数', prop: 'complete_nums' },
{ label: '未完成人数', prop: 'not_complete_nums' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 300 }
]
{ label: '操作', slots: 'table-x', width: 300 },
],
}
// 刷新
function handleRefetch() {
......@@ -120,16 +120,18 @@ function handleUpdateGradeRules() {
</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
>
<el-button type="primary" @click="handleCopy()">复制实验</el-button>
<el-button type="primary" @click="handleUpdateDML()" v-if="detail?.type === '4'">配置数字营销</el-button>
<el-button type="primary">
<router-link
:to="{ path: '/admin/lab/score', query: { course_id: detail?.course_id, experiment_id: detail?.id } }"
target="_blank"
>实验评分</router-link
>
</el-button>
<template v-if="!detail?.stu_commit_count">
<el-button type="primary" :icon="Edit" @click="handleUpdateGradeRules()">编辑成绩规则</el-button>
<!-- <el-dropdown-item :icon="EditPen">
<router-link :to="`/admin/lab/experiment/report/${row.id}`" target="_blank">编辑报告规则</router-link>
</el-dropdown-item> -->
<el-button type="primary" :icon="EditPen">
<el-button type="primary" @click="handleUpdateGradeRules()">编辑成绩规则</el-button>
<el-button type="primary">
<router-link :to="`/admin/lab/experiment/report/${detail?.id}`" target="_blank">编辑报告规则</router-link>
</el-button>
</template>
......@@ -197,15 +199,13 @@ function handleUpdateGradeRules() {
<StudentGroupDialog
v-model="studentGroupVisible"
:data="rowData"
v-if="studentGroupVisible && rowData"
></StudentGroupDialog>
v-if="studentGroupVisible && rowData"></StudentGroupDialog>
<!-- 查看学生 -->
<StudentListDialog
v-model="studentListVisible"
:data="rowData"
:experimentId="id"
v-if="studentListVisible && rowData"
></StudentListDialog>
v-if="studentListVisible && rowData"></StudentListDialog>
<ViewGradeRules v-model="gradeRulesVisible" :data="detail" v-if="gradeRulesVisible && detail"></ViewGradeRules>
<ViewReportRules v-model="reportRulesVisible" :experiment_id="id" v-if="reportRulesVisible"></ViewReportRules>
<CopyDialog v-model="copyDialogVisible" :data="detail" v-if="copyDialogVisible && detail"></CopyDialog>
......@@ -215,6 +215,5 @@ function handleUpdateGradeRules() {
<GradeRulesDialog
v-model="gradeRulesDialogVisible"
:data="detail"
v-if="gradeRulesDialogVisible && detail"
></GradeRulesDialog>
v-if="gradeRulesDialogVisible && detail"></GradeRulesDialog>
</template>
......@@ -24,14 +24,20 @@ export function getExperimentRecord(params: { experiment_id: string; student_id:
}
// 实验记录评分
export function checkExperimentRecord(data: { experiment_id: string; student_id: string; operate: number; result: number; file: number }) {
export function checkExperimentRecord(data: {
experiment_id: string
student_id: string
operate: number
result: number
file: number
}) {
return httpRequest.post('/api/lab/v1/teacher/record/check', data)
}
// 批量导入实验记录评分
export function uploadCheckExperimentRecord(data: { file: File }) {
return httpRequest.post('/api/lab/v1/teacher/record/upload', data, {
headers: { 'Content-Type': 'multipart/form-data' }
headers: { 'Content-Type': 'multipart/form-data' },
})
}
......@@ -50,7 +56,12 @@ export function getExperimentReport(params: { experiment_id: string; student_id:
return httpRequest.get('/api/lab/v1/teacher/experiment/report-achievement', { params })
}
// 批改学员实验报告成绩
export function updateExperimentReport(data: { experiment_id: string; student_id: string; score_detail: string; comment: string }) {
export function updateExperimentReport(data: {
experiment_id: string
student_id: string
score_detail: string
comment: string
}) {
return httpRequest.post('/api/lab/v1/teacher/experiment/report-check', data)
}
......@@ -106,3 +117,8 @@ export function syncExperimentExam(params: { experiment_id: string }) {
export function recordReject(params: { experiment_id: string; student_id: string }) {
return httpRequest.get('/api/lab/v1/teacher/record/reject', { params })
}
// 教师查看分数详情页面
export function getExperimentScoreDetail(params: { experiment_id: string; student_id: string; type: string }) {
return httpRequest.get('/api/lab/v1/teacher/experiment/score-detail', { params })
}
......@@ -9,6 +9,7 @@ const appConfig = useAppConfig()
const ScoreViewPicturesDialog = defineAsyncComponent(() => import('./ScoreViewPicturesDialog.vue'))
const ScoreViewPrepareDialog = defineAsyncComponent(() => import('./ScoreViewPrepareDialog.vue'))
const ScoreViewResultDialog = defineAsyncComponent(() => import('./ScoreViewResultDialog.vue'))
const ScoreViewAutoDialog = defineAsyncComponent(() => import('./ScoreViewAutoDialog.vue'))
interface Props {
data: RecordItem
......@@ -23,43 +24,45 @@ const emit = defineEmits<{
let experiment = $ref<any>()
let detail = $ref<any>()
async function fetchInfo() {
await getExperimentScore({ experiment_id: props.data.experiment_id, student_id: props.data.student_id }).then(async res => {
experiment = res.data.experiment
detail = res.data.achievement
if (detail.score_details) {
try {
form.score_details = JSON.parse(detail.score_details).map((item: any) => {
return {
...item,
percent: parseFloat(item.percent),
score: parseFloat(item.score),
commit_score: parseFloat(item.commit_score)
}
})
} catch (error) {
await getExperimentScore({ experiment_id: props.data.experiment_id, student_id: props.data.student_id }).then(
async (res) => {
experiment = res.data.experiment
detail = res.data.achievement
if (detail.score_details) {
try {
form.score_details = JSON.parse(detail.score_details).map((item: any) => {
return {
...item,
percent: parseFloat(item.percent),
score: parseFloat(item.score),
commit_score: parseFloat(item.commit_score),
}
})
} catch (error) {
await fetchTemplate()
console.log(error)
}
} else {
await fetchTemplate()
console.log(error)
}
} else {
await fetchTemplate()
}
if (experiment.report_upload_way === 2) {
await fetchReport()
if (experiment.report_upload_way === 2) {
await fetchReport()
}
}
})
)
}
watchEffect(() => {
fetchInfo()
})
async function fetchTemplate() {
await getExperimentScoreTemplate({ experiment_id: props.data.experiment_id }).then(res => {
await getExperimentScoreTemplate({ experiment_id: props.data.experiment_id }).then((res) => {
form.score_details = res.data.detail.rule_list.map((item: any) => {
return {
...item,
percent: parseFloat(item.percent),
score: parseFloat(item.score),
commit_score: parseFloat(item.commit_score)
commit_score: parseFloat(item.commit_score),
}
})
})
......@@ -68,16 +71,18 @@ async function fetchTemplate() {
// 获取实验报告分数
let report = $ref<any>()
async function fetchReport() {
await getExperimentReport({ experiment_id: props.data.experiment_id, student_id: props.data.student_id }).then(res => {
report = res.data.detail
const reportScore = parseFloat(report.score || 0)
form.score_details = form.score_details.map((item: any) => {
if (item.type === 1) {
item.commit_score = reportScore
}
return item
})
})
await getExperimentReport({ experiment_id: props.data.experiment_id, student_id: props.data.student_id }).then(
(res) => {
report = res.data.detail
const reportScore = parseFloat(report.score || 0)
form.score_details = form.score_details.map((item: any) => {
if (item.type === 1) {
item.commit_score = reportScore
}
return item
})
}
)
}
// 实验报告文件
......@@ -93,7 +98,7 @@ const file = $computed<FileItem>(() => {
const form = reactive<any>({
experiment_id: props.data.experiment_id,
student_id: props.data.student_id,
score_details: []
score_details: [],
})
const score = $computed<number>(() => {
......@@ -105,14 +110,16 @@ const score = $computed<number>(() => {
// 提交
function handleSubmit() {
ElMessageBox.confirm('更改成绩之后将以最新成绩为准,您可以查看批改成绩历史数据,确定需要修改成绩吗?', '提示').then(() => {
const params = { ...form, score_details: JSON.stringify(form.score_details) }
updateExperimentScore(params).then(() => {
ElMessage({ message: '保存成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
})
ElMessageBox.confirm('更改成绩之后将以最新成绩为准,您可以查看批改成绩历史数据,确定需要修改成绩吗?', '提示').then(
() => {
const params = { ...form, score_details: JSON.stringify(form.score_details) }
updateExperimentScore(params).then(() => {
ElMessage({ message: '保存成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
)
}
function scoreValue(value: any) {
......@@ -146,10 +153,22 @@ function getOperationUrl(type: number) {
return `${dmlURL}/material?experiment_id=${props.data.experiment_id}&student_id=${props.data.student_id}`
}
}
const autoVisible = ref(false)
const currentRow = ref(null)
function handleViewAuto(row: any) {
autoVisible.value = true
currentRow.value = row
}
</script>
<template>
<el-dialog title="学生实验评分" :close-on-click-modal="false" width="800px" @update:modelValue="value => $emit('update:modelValue', value)">
<el-dialog
title="学生实验评分"
:close-on-click-modal="false"
width="800px"
@update:modelValue="(value) => $emit('update:modelValue', value)">
<el-form label-width="120px" label-suffix=":" v-if="detail">
<el-row>
<el-col :span="12">
......@@ -189,12 +208,16 @@ function getOperationUrl(type: number) {
<p class="t1">{{ scoreValue(score) }}</p>
<p class="t2">满分:{{ experiment.score }}</p>
</div>
<el-table :data="form.score_details" stripe :header-cell-style="{ background: '#ededed' }" style="margin-top: 20px">
<el-table-column label="实验成绩组成项" prop="name" align="center"></el-table-column>
<el-table-column label="权重" prop="percent" align="center">
<el-table
:data="form.score_details"
stripe
:header-cell-style="{ background: '#ededed' }"
style="margin-top: 20px">
<el-table-column label="实验成绩组成项" prop="name" align="left"></el-table-column>
<el-table-column label="权重" prop="percent" align="center" width="80">
<template #default="{ row }"> {{ row.percent }}% </template>
</el-table-column>
<el-table-column label="满分" prop="score" align="center">100</el-table-column>
<el-table-column label="满分" prop="score" align="center" width="100">100</el-table-column>
<el-table-column label="得分" prop="commit_score" align="center">
<template #default="{ row }">
<el-input-number
......@@ -210,22 +233,29 @@ function getOperationUrl(type: number) {
<span v-else>{{ row.commit_score }}</span>
</template>
</el-table-column>
<el-table-column label="操作" prop="commit_score" align="center">
<el-table-column label="操作" align="center" width="180">
<template #default="{ row }">
<template v-if="row.type === 1">
<el-button text type="primary" v-if="experiment.report_upload_way === 2">
<el-button link type="primary" v-if="experiment.report_upload_way === 2">
<a :href="getOperationUrl(row.type)" target="_blank">批改</a>
</el-button>
<template v-if="experiment.report_upload_way === 1">
<el-button text type="primary" v-if="file?.url">
<el-button link type="primary" v-if="file?.url">
<a :href="file.url" target="_blank">查阅报告</a>
</el-button>
<p style="color: red" v-else>未上传</p>
</template>
</template>
<template v-else-if="[6, 7, 8, 9, 10].includes(row.type)">
<el-button text type="primary">
<a :href="`/admin/lab/score/score?id=${props.data.experiment_id}&sid=${props.data.student_id}&type=${row.type}`" target="_blank">查看结果</a>
<template v-if="[6, 7, 9, 11, 12].includes(row.type)">
<el-button link type="primary" @click="handleViewAuto(row)">查看自动评分结果</el-button>
</template>
<template v-if="[6, 7, 8, 9, 10, 11, 12].includes(row.type)">
<el-button link type="primary">
<a
:href="`/admin/lab/score/score?id=${props.data.experiment_id}&sid=${props.data.student_id}&type=${row.type}`"
target="_blank"
>查看原始结果</a
>
</el-button>
</template>
</template>
......@@ -253,7 +283,16 @@ function getOperationUrl(type: number) {
:student_id="data.student_id"
v-if="resultVisible"></ScoreViewResultDialog>
<!-- 实验截图 -->
<ScoreViewPicturesDialog v-model="pictureVisible" :data="detail" v-if="pictureVisible && detail"></ScoreViewPicturesDialog>
<ScoreViewPicturesDialog
v-model="pictureVisible"
:data="detail"
v-if="pictureVisible && detail"></ScoreViewPicturesDialog>
<!-- 实验截图 -->
<ScoreViewAutoDialog
v-model="autoVisible"
:data="detail"
:row="currentRow"
v-if="autoVisible"></ScoreViewAutoDialog>
</el-dialog>
</template>
......
<script setup lang="ts">
import type { RecordItem } from '../types'
import AppList from '@/components/base/AppList.vue'
import { getExperimentScoreDetail } from '../api'
import { gradeRule } from '@/utils/dictionary'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
interface Props {
data: RecordItem
row: any
}
const props = defineProps<Props>()
let datalist = $ref<any>([])
function fetchInfo() {
getExperimentScoreDetail({
experiment_id: props.data.experiment_id,
student_id: props.data.student_id,
type: props.row.type,
}).then((res) => {
datalist = res.data
})
}
watchEffect(() => {
fetchInfo()
})
const appList = $ref<InstanceType<typeof AppList> | null>(null)
const title = computed(() => {
return '查看' + gradeRule[props.row.type]
})
// 列表配置
const listOptions = computed(() => {
let columns: any = []
switch (props.row.type) {
case 6:
// 用户标签
columns = [
{ label: '序号', type: 'index', width: 60 },
{ label: '标签名称', prop: 'student_name' },
{ label: '正确标签名称', prop: 'teacher_name' },
{ label: '标签数据量', prop: 'student_total' },
{ label: '正确标签数据量', prop: 'teacher_total' },
{ label: '操作', prop: 'status', slots: 'table-x' },
]
break
case 7:
// 用户群组
columns = [
{ label: '序号', type: 'index', width: 60 },
{ label: '群组名称', prop: 'student_name' },
{ label: '正确群组名称', prop: 'teacher_name' },
{ label: '群组数据量', prop: 'student_total' },
{ label: '正确群组数据量', prop: 'teacher_total' },
{ label: '操作', prop: 'status', slots: 'table-x' },
]
break
case 9:
// 营销资料
columns = [
{ label: '序号', type: 'index', width: 60 },
{ label: '营销资料名称', prop: 'student_name' },
{ label: '营销资料类型', prop: 'type_name' },
{ label: '正确营销资料名称', prop: 'teacher_name' },
{ label: 'AI综合评分', prop: 'score' },
{ label: 'AI评价', prop: 'comment' },
]
break
case 11:
// 用户数据
columns = [
{ label: '序号', type: 'index', width: 60 },
{ label: '文件名称', prop: 'file_name' },
{ label: '用户数据量', prop: 'student_total' },
{ label: '正确答案', prop: 'teacher_total' },
{ label: '导入时间', prop: 'created_time' },
{ label: '状态', prop: 'status' },
]
break
case 12:
// 事件数据
columns = [
{ label: '序号', type: 'index', width: 60 },
{ label: '文件名称', prop: 'file_name' },
{ label: '事件名称', prop: 'event_name' },
{ label: '事件数据量', prop: 'student_total' },
{ label: '正确答案', prop: 'teacher_total' },
{ label: '导入时间', prop: 'created_time' },
{ label: '状态', prop: 'status' },
]
break
default:
break
}
return {
columns,
data: datalist,
}
})
function getOperationUrl(row: any) {
const type = props.row.type
const dmlURL = appConfig.dmlURL || import.meta.env.VITE_DML_URL
if (type === 6) {
// 用户标签
return `${dmlURL}/label?experiment_id=${props.data.experiment_id}&student_id=${props.data.student_id}&id=${row.id}&name=${row.student_name}`
} else if (type === 7) {
// 用户群组
return `${dmlURL}/group?experiment_id=${props.data.experiment_id}&student_id=${props.data.student_id}&id=${row.id}&name=${row.student_name}`
}
}
</script>
<template>
<el-dialog :title="title" width="800px">
<AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary" link>
<a :href="getOperationUrl(row)" target="_blank">查看</a>
</el-button>
</template>
</AppList>
<template #footer>
<el-row justify="center">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
......@@ -23,15 +23,15 @@ const appList = $ref<InstanceType<typeof AppList> | null>(null)
const params = reactive({
student_name: '',
course_id: '',
course_id: (route.query.course_id as string) || '',
experiment_id: (route.query.experiment_id as string) || '',
specialty_id: '',
class_id: ''
class_id: '',
})
const classList = $computed(() => {
const specialty = specialties.value.find(item => item.id === params.specialty_id)
const specialty = specialties.value.find((item) => item.id === params.specialty_id)
if (specialty) {
return classes.value.filter(item => item.specialty_id === specialty.id)
return classes.value.filter((item) => item.specialty_id === specialty.id)
}
return classes.value
})
......@@ -48,7 +48,7 @@ const listOptions = $computed(() => {
}
params.specialty_id = requestParams.specialty_id || ''
return requestParams
}
},
},
filterForm: { labelWidth: 100 },
filters: [
......@@ -59,7 +59,7 @@ const listOptions = $computed(() => {
placeholder: '请选择实验课程名称',
options: courses.value,
labelKey: 'name',
valueKey: 'id'
valueKey: 'id',
},
{
type: 'select',
......@@ -68,7 +68,7 @@ const listOptions = $computed(() => {
placeholder: '请选择实验名称',
options: experiments.value,
labelKey: 'name',
valueKey: 'id'
valueKey: 'id',
},
{
type: 'select',
......@@ -77,7 +77,7 @@ const listOptions = $computed(() => {
placeholder: '请选择专业',
options: specialties.value,
labelKey: 'name',
valueKey: 'id'
valueKey: 'id',
},
{
type: 'select',
......@@ -86,9 +86,9 @@ const listOptions = $computed(() => {
placeholder: '请选择班级',
options: classList,
labelKey: 'name',
valueKey: 'id'
valueKey: 'id',
},
{ type: 'input', prop: 'student_name', label: '学生姓名', placeholder: '请输入学生姓名' }
{ type: 'input', prop: 'student_name', label: '学生姓名', placeholder: '请输入学生姓名' },
],
columns: [
{ label: '序号', type: 'index', width: 60 },
......@@ -100,8 +100,8 @@ const listOptions = $computed(() => {
{ label: '提交状态', prop: 'status_name', slots: 'table-status' },
{ label: '考试成绩', prop: 'score', slots: 'table-score' },
{ label: '更新时间', prop: 'commit_time' },
{ label: '操作', slots: 'table-x', width: 210 }
]
{ label: '操作', slots: 'table-x', width: 210 },
],
}
})
const importVisible = $ref(false)
......@@ -138,9 +138,28 @@ async function handleReset(row: RecordItem) {
<template #header-buttons>
<el-row justify="space-between">
<div>
<el-button type="primary" round :icon="Upload" @click="importVisible = true" v-permission="'v1-teacher-record-upload'">批量导入</el-button>
<el-button type="primary" round :icon="Download" @click="exportVisible = true" v-permission="'v1-teacher-record-download'">批量导出</el-button>
<el-button type="primary" round :icon="Refresh" @click="syncVisible = true" v-permission="'v1-teacher-record-sync-theory-score'"
<el-button
type="primary"
round
:icon="Upload"
@click="importVisible = true"
v-permission="'v1-teacher-record-upload'"
>批量导入</el-button
>
<el-button
type="primary"
round
:icon="Download"
@click="exportVisible = true"
v-permission="'v1-teacher-record-download'"
>批量导出</el-button
>
<el-button
type="primary"
round
:icon="Refresh"
@click="syncVisible = true"
v-permission="'v1-teacher-record-sync-theory-score'"
>同步理论成绩</el-button
>
</div>
......@@ -158,16 +177,34 @@ async function handleReset(row: RecordItem) {
<span :class="{ 'is-info': row.score !== '--' }">{{ row.score }}</span>
</template>
<template #table-x="{ row }">
<el-button text type="primary" v-if="row.status === 1 || row.status === 2" @click="handleScore(row)" v-permission="'v1-teacher-record-check'"
<el-button
link
type="primary"
v-if="row.status === 1 || row.status === 2"
@click="handleScore(row)"
v-permission="'v1-teacher-record-check'"
>打分</el-button
>
<el-button text type="primary" v-if="row.status === 1 || row.status === 2" @click="handleReset(row)">重置</el-button>
<el-button text type="primary" v-if="row.has_score_log" @click="handleScoreLog(row)" v-permission="'v1-teacher-record-check'">查看历史成绩</el-button>
<el-button link type="primary" v-if="row.status === 1 || row.status === 2" @click="handleReset(row)"
>重置</el-button
>
<el-button
link
type="primary"
v-if="row.has_score_log"
@click="handleScoreLog(row)"
v-permission="'v1-teacher-record-check'"
>查看历史成绩</el-button
>
</template>
</AppList>
</AppCard>
<!-- 评分 -->
<ScoreDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible && rowData"></ScoreDialog>
<ScoreDialog
v-model="dialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="dialogVisible && rowData"></ScoreDialog>
<!-- 历史成绩 -->
<ScoreLogDialog v-model="scoreLogVisible" :data="rowData" v-if="scoreLogVisible && rowData"></ScoreLogDialog>
<!-- 批量导入 -->
......
......@@ -30,7 +30,12 @@ export function getExperimentVideoPlayInfo(params: { source_id: string }) {
}
// 获取实验讨论交流
export function getExperimentDiscussList(params: { experiment_id: string; tag: number; page?: number; 'per-page'?: number }) {
export function getExperimentDiscussList(params: {
experiment_id: string
tag: number
page?: number
'per-page'?: number
}) {
return httpRequest.get('/api/lab/v1/student/experiment-topic/list', { params })
}
// 发表新话题
......@@ -85,11 +90,21 @@ export function getExperimentReportTemplate(params: { experiment_id: string }) {
return httpRequest.get('/api/lab/v1/student/experiment/report-template', { params })
}
// 更新实验在线报告
export function updateExperimentReport(data: { experiment_id: string; experiment_address: string; experiment_date: string; detail: string }) {
export function updateExperimentReport(data: {
experiment_id: string
experiment_address: string
experiment_date: string
detail: string
}) {
return httpRequest.post('/api/lab/v1/student/experiment/upload-online-report', data)
}
// 缓存实验在线报告
export function cacheExperimentReport(data: { experiment_id: string; experiment_address: string; experiment_date: string; detail: string }) {
export function cacheExperimentReport(data: {
experiment_id: string
experiment_address: string
experiment_date: string
detail: string
}) {
return httpRequest.post('/api/lab/v1/student/experiment/cache-online-report', data)
}
// 获取实验在线报告缓存
......@@ -120,3 +135,8 @@ export function getExperimentQuestion(params: { experiment_id: string; id: strin
export function getExperimentExamList(params: { experiment_id: string }) {
return httpRequest.get('/api/lab/v1/student/experiment-exam/exams', { params })
}
// 学生查看分数详情页面
export function getExperimentScoreDetail(params: { experiment_id: string; type: string }) {
return httpRequest.get('/api/lab/v1/student/experiment-question/score-detail', { params })
}
......@@ -12,12 +12,16 @@ interface Props {
course_id?: string
}
const props = defineProps<Props>()
const emits = defineEmits(['empty'])
let list = $ref<ExperimentBookType[]>([])
function fetchInfo() {
if (!props.experiment_id) return
getExperimentBookList({ experiment_id: props.experiment_id }).then(res => {
getExperimentBookList({ experiment_id: props.experiment_id }).then((res) => {
list = res.data.items
if (list.length === 0) {
emits('empty')
}
})
}
watchEffect(() => {
......@@ -34,7 +38,7 @@ function handleView(row: ExperimentBookType) {
log.upload({
event: 'file_event',
action: 'experiment_book_stu_watch_action',
data: { experiment_id: props.experiment_id, course_id: props.course_id, book_id: row.id }
data: { experiment_id: props.experiment_id, course_id: props.course_id, book_id: row.id },
})
}
// 关闭
......
......@@ -8,12 +8,16 @@ interface Props {
course_id?: string
}
const props = defineProps<Props>()
const emits = defineEmits(['empty'])
let detail = $ref<ExperimentBookType>()
function fetchInfo() {
if (!props.experiment_id) return
getExperimentCase({ experiment_id: props.experiment_id }).then(res => {
detail = res.data.detail
getExperimentCase({ experiment_id: props.experiment_id }).then((res) => {
detail = res.data.detail || {}
if (detail && Object.keys(detail).length === 0) {
emits('empty')
}
})
}
watchEffect(() => {
......
......@@ -7,9 +7,15 @@ const appConfig = useAppConfig()
interface Props {
experiment_id: string
examStatus?: number
showInfo?: boolean
showIframe?: boolean
}
const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
showInfo: true,
showIframe: false,
})
const model = defineModel()
const emits = defineEmits(['empty'])
const cookies = useCookies()
......@@ -22,39 +28,46 @@ const currentExam = computed(() => {
// 考试平台 URL
const examURL = computed(() => {
if (!currentExam.value) return ''
return appConfig.system !== 'x' || props.examStatus !== 0
? `${import.meta.env.VITE_EXAM_SHOW_URL}/exam/${currentExam.value?.exam_id}`
: `${import.meta.env.VITE_EXAM_SHOW_URL}/exam/${
return appConfig.system === 'x' && props.examStatus === 0
? `${import.meta.env.VITE_EXAM_SHOW_URL}/exam/${
currentExam.value?.exam_id
}?has_time=0&has_submit=0&has_save=1&show_answer=1`
: `${import.meta.env.VITE_EXAM_SHOW_URL}/exam/${currentExam.value?.exam_id}`
// return `https://dev.ezijing.com:5173/exam/7003551966412406784?has_time=0&has_submit=0&has_save=1&show_answer=1`
})
async function fetchInfo() {
if (!props.experiment_id) return
const res = await getExperimentExamList({ experiment_id: props.experiment_id })
const resCookies = res.data.cookies
cookies.set(resCookies.key, resCookies.auth_key, { domain: '.ezijing.com', path: '/' })
list.value = res.data.items || []
model.value = examURL.value
if (list.value.length === 0) {
emits('empty')
}
}
onMounted(() => {
watchEffect(() => {
fetchInfo()
})
</script>
<template>
<template v-if="currentExam">
<el-form label-suffix=":" label-position="top" v-if="appConfig.system !== 'x'">
<el-form label-suffix=":" label-position="top" v-if="showInfo">
<el-form-item label="考试名称">{{ currentExam.exam_info.name }}</el-form-item>
<el-form-item label="考试时间"
>{{ currentExam.exam_info.start_time }}{{ currentExam.exam_info.end_time }}</el-form-item
>
</el-form>
<div style="width: 100%; height: 100%" v-if="props.examStatus === 0">
<iframe style="width: 100%; height: 100%" allowfullscreen class="iframe" :src="examURL" frameborder="0"></iframe>
</div>
<!-- <teleport to=".lab-box"> -->
<!-- </teleport> -->
<iframe
style="width: 100%; height: 100%"
allowfullscreen
class="iframe"
:src="examURL"
frameborder="0"
v-if="showIframe"></iframe>
</template>
<el-empty description="暂无数据" v-else />
</template>
......@@ -2,9 +2,10 @@
import { getExperimentQuestionList, getExperimentQuestion } from '../api'
import { Document } from '@element-plus/icons-vue'
import { filesize } from 'filesize'
import { saveAs } from 'file-saver'
interface Props {
experiment_id: string,
experiment_id: string
exam_status?: number
}
interface QuestionListItem {
......@@ -20,6 +21,7 @@ interface QuestionGroupList {
}
const props = defineProps<Props>()
const emits = defineEmits(['empty'])
const questionIndex = ref<number>(1)
const questionId = computed(() => {
......@@ -45,6 +47,9 @@ async function fetchList() {
if (!props.experiment_id) return
const res = await getExperimentQuestionList({ experiment_id: props.experiment_id })
questionList.value = res.data.items
if (questionList.value.length === 0) {
emits('empty')
}
}
watchEffect(() => {
if (props.experiment_id) fetchList()
......@@ -52,19 +57,21 @@ watchEffect(() => {
// 试题详情
const questionDetail = ref()
const file = ref()
async function fetchDetail() {
if (!questionId.value) return
const res = await getExperimentQuestion({ experiment_id: props.experiment_id, id: questionId.value })
const detail = res.data.detail
let files = []
const detail = res.data.detail || {}
let answer: any = {}
try {
files = JSON.parse(detail.files)
answer = JSON.parse(detail.answer)
file.value = answer.data?.file || {}
} catch (error) {
console.log(error)
}
questionDetail.value = { ...detail, files, score: parseFloat(detail.score) }
questionDetail.value = { ...detail, answer, score: parseFloat(detail.score) }
}
watch(questionId, id => {
watch(questionId, (id) => {
if (id) fetchDetail()
})
......@@ -79,7 +86,9 @@ function handleNext() {
}
function getQuestionTypeName(type: string) {
if (['101', '102'].includes(type)) return '用户/事件管理'
if (['101'].includes(type)) return '用户管理'
if (['102'].includes(type)) return '事件管理'
// if (['101', '102'].includes(type)) return '用户/事件管理'
if (['201', '202'].includes(type)) return '标签管理'
if (['301', '302'].includes(type)) return '群组管理'
if (['401', '402', '403', '404', '405', '406', '407'].includes(type)) return '营销资料管理'
......@@ -111,17 +120,18 @@ function customFloor(num: number) {
<el-card shadow="never" class="question-item" v-if="questionDetail">
<h3 class="question-item__title">{{ getQuestionTypeName(questionDetail.type) }}</h3>
<p class="question-item__stem">
{{ questionIndex }}{{ questionDetail.title }} <span v-if="props?.exam_status !== 1">{{ questionDetail.score }}分)</span>
{{ questionIndex }}{{ questionDetail.title }}
<span>{{ questionDetail.score }}分)</span>
</p>
<p class="question-item__content">{{ questionDetail.content }}</p>
<ul class="question-item__files">
<li class="question-item__files-item" v-for="(file, index) in questionDetail.files" :key="index">
<ul class="question-item__files" v-if="!!Object.keys(file).length">
<li class="question-item__files-item">
<el-icon><Document /></el-icon>
<div class="question-item__files-item__title">
<p>{{ file.name }}</p>
<p>{{ filesize(file.size) }}</p>
</div>
<a :href="file.url" target="_blank" download v-if="file.is_download">下载</a>
<a href="javascript:void(0)" :download="file.name" @click="saveAs(file.url, file.name)">下载</a>
<a :href="file.url" target="_blank">查看</a>
</li>
</ul>
......
<script setup lang="ts">
import { getExperimentScore } from '../api'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const ResultScoreViewAutoDialog = defineAsyncComponent(() => import('./ResultScoreViewAutoDialog.vue'))
interface Props {
experiment_id: string
}
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const props = defineProps<Props>()
let experiment = $ref<any>()
......@@ -16,7 +19,7 @@ const classText = $computed(() => {
})
function fetchInfo() {
getExperimentScore({ experiment_id: props.experiment_id }).then(res => {
getExperimentScore({ experiment_id: props.experiment_id }).then((res) => {
experiment = res.data.experiment
let scoreDetails = []
try {
......@@ -28,7 +31,7 @@ function fetchInfo() {
}
detail = Object.assign(res.data.achievement, {
score: parseFloat(res.data.achievement.score),
score_details: scoreDetails
score_details: scoreDetails,
})
})
}
......@@ -40,15 +43,54 @@ function getOperationUrl(type: number) {
if (type === 1) {
// 实验报告
return `/student/lab/report/view/${experiment.id}`
} else if (type === 6) {
// 用户标签
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/label?experiment_id=${experiment.id}&student_id=${
experiment.student.id
}`
} else if (type === 7) {
// 用户群组
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/group?experiment_id=${experiment.id}&student_id=${
experiment.student.id
}`
} else if (type === 8) {
// 用户旅程
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/trip/my/score?experiment_id=${experiment.id}&student_id=${experiment.student.id}`
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/trip/my/score?experiment_id=${
experiment.id
}&student_id=${experiment.student.id}`
} else if (type === 9) {
// 营销资料
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/material?experiment_id=${experiment.id}&student_id=${
experiment.student.id
}`
} else if (type === 11) {
// 用户数据
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/user?experiment_id=${experiment.id}&student_id=${
experiment.student.id
}`
} else if (type === 12) {
// 事件数据
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/user?experiment_id=${experiment.id}&student_id=${
experiment.student.id
}`
} else {
// 去首页
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/?experiment_id=${experiment.id}&student_id=${
experiment.student.id
}`
}
}
const autoVisible = ref(false)
const currentRow = ref(null)
function handleViewAuto(row: any) {
autoVisible.value = true
currentRow.value = row
}
</script>
<template>
<el-dialog title="实验成绩详情" width="600px">
<el-dialog title="实验成绩详情" width="800px">
<el-form label-width="120px" label-suffix=":" v-if="detail">
<el-form-item label="实验名称">{{ experiment.name }}</el-form-item>
<el-form-item label="实验课程名称">{{ experiment.course.name }}</el-form-item>
......@@ -71,7 +113,9 @@ function getOperationUrl(type: number) {
<el-row>
<el-col :span="12">
<el-form-item label="评分教师">
{{ detail.checker_sso_user.real_name || detail.checker_sso_user.nickname || detail.checker_sso_user.username }}
{{
detail.checker_sso_user.real_name || detail.checker_sso_user.nickname || detail.checker_sso_user.username
}}
</el-form-item>
</el-col>
<el-col :span="12">
......@@ -83,7 +127,11 @@ function getOperationUrl(type: number) {
<p>实验得分</p>
<p class="t1">{{ detail.score }}</p>
</div>
<el-table :data="detail.score_details" stripe :header-cell-style="{ background: '#ededed' }" v-if="detail.is_show === '1'">
<el-table
:data="detail.score_details"
stripe
:header-cell-style="{ background: '#ededed' }"
v-if="detail.is_show === '1'">
<el-table-column label="实验成绩组成项" prop="name" align="center"></el-table-column>
<el-table-column label="权重" prop="percent" align="center">
<template #default="{ row }">{{ row.percent }}%</template>
......@@ -99,6 +147,25 @@ function getOperationUrl(type: number) {
</template>
</template>
</el-table-column>
<el-table-column label="标准答案" align="center" width="80">
<template #default="{ row }">
<el-button type="primary" link @click="handleViewAuto(row)" v-if="[6, 7, 9, 11, 12].includes(row.type)"
>查看</el-button
>
</template>
</el-table-column>
<el-table-column label="我的答案" align="center" width="80">
<template #default="{ row }">
<el-button link type="primary">
<a :href="getOperationUrl(row.type)" target="_blank">查看</a>
</el-button>
</template>
</el-table-column>
<el-table-column label="评语" align="center" width="80" v-if="false">
<template #default="{ row }">
<el-button type="primary" link v-if="row.type === 9">查看</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form>
......@@ -107,6 +174,12 @@ function getOperationUrl(type: number) {
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
<ResultScoreViewAutoDialog
v-model="autoVisible"
:data="experiment"
:row="currentRow"
v-if="currentRow"></ResultScoreViewAutoDialog>
</el-dialog>
</template>
......
<script setup>
import AppList from '@/components/base/AppList.vue'
import { getExperimentScoreDetail } from '../api'
import { gradeRule } from '@/utils/dictionary'
import Preview from '@/components/Preview.vue'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
row: {
type: Object,
default: () => ({}),
},
})
let datalist = $ref([])
function fetchInfo() {
getExperimentScoreDetail({
experiment_id: props.data.id,
type: props.row.type,
}).then((res) => {
datalist = res.data
})
}
watchEffect(() => {
fetchInfo()
})
const title = computed(() => {
return '查看' + gradeRule[props.row.type]
})
// 列表配置
const listOptions = computed(() => {
let columns = []
switch (props.row.type) {
case 6:
// 用户标签
columns = [
{ label: '序号', type: 'index', width: 60 },
{ label: '标签名称', prop: 'tag_name' },
{ label: '操作', prop: 'status', slots: 'table-x' },
]
break
case 7:
// 用户群组
columns = [
{ label: '序号', type: 'index', width: 60 },
{ label: '群组名称', prop: 'group_name' },
{ label: '操作', prop: 'status', slots: 'table-x' },
]
break
case 9:
// 营销资料
columns = [
{ label: '序号', type: 'index', width: 60 },
{ label: '资料名称', prop: 'material_name' },
{
label: '资料类型',
prop: 'type_name',
computed() {
return '文本'
},
},
{ label: '评语', prop: 'comment' },
{ label: '操作', prop: 'status', slots: 'table-x' },
]
break
case 12:
// 事件数据
columns = [
{ label: '序号', type: 'index', width: 60 },
{ label: '事件名称', prop: 'event_name' },
{ label: '操作', prop: 'status', slots: 'table-x' },
]
break
default:
break
}
return {
columns,
data: datalist,
}
})
function getOperationUrl(row) {
const type = props.row.type
const dmlURL = appConfig.dmlURL || import.meta.env.VITE_DML_URL
if (type === 6) {
// 用户标签
return `${dmlURL}/label?experiment_id=${props.data.id}&student_id=${props.data.student.id}&id=${row.tag_id}&name=${row.tag_name}`
} else if (type === 7) {
// 用户群组
return `${dmlURL}/group?experiment_id=${props.data.id}&student_id=${props.data.student.id}&id=${row.group_id}&name=${row.group_name}`
} else if (type === 9) {
// 营销资料
return `${dmlURL}/material?experiment_id=${props.data.id}&student_id=${props.data.student.id}&id=${row.material_id}&name=${row.material_name}`
} else if (type === 12) {
const url = row.file.url
// 事件数据
return ['pptx', 'doc', 'docx', 'xls', 'xlsx'].includes(url)
? `https://view.officeapps.live.com/op/view.aspx?src=${url}`
: url
}
}
</script>
<template>
<el-dialog :title="title" width="800px">
<template v-if="row.type === 11">
<Preview v-for="(item, index) in datalist" :key="index" :url="item.file.url"></Preview>
</template>
<AppList v-bind="listOptions" ref="appList" v-else>
<template #table-x="{ row }">
<el-button type="primary" link>
<a :href="getOperationUrl(row)" target="_blank">查看</a>
</el-button>
</template>
</AppList>
<template #footer>
<el-row justify="center">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
......@@ -8,12 +8,16 @@ interface Props {
course_id?: string
}
const props = defineProps<Props>()
const emits = defineEmits(['empty'])
let list = $ref<ExperimentVideoType[]>([])
function fetchInfo() {
if (!props.experiment_id) return
getExperimentVideoList({ experiment_id: props.experiment_id }).then(res => {
getExperimentVideoList({ experiment_id: props.experiment_id }).then((res) => {
list = res.data.list
if (list.length === 0) {
emits('empty')
}
})
}
watchEffect(() => {
......@@ -33,8 +37,7 @@ const isEmpty = $computed(() => {
:key="item.id"
:data="item"
:course_id="course_id"
:experiment_id="experiment_id"
></VideoItem>
:experiment_id="experiment_id"></VideoItem>
</template>
</template>
......
......@@ -141,6 +141,7 @@ export interface ExperimentInfo {
is_commit_report: boolean
is_commit: boolean
exam_status: number
can_repeat_commit: 0 | 1
}
interface IdName {
......
// json to array
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 contestMode: Record<string, any> = {
'1': '个人赛'
'1': '个人赛',
}
// 参赛模式列表
export const contestModeList = json2Array(contestMode, false)
......@@ -15,7 +15,7 @@ export const scoreRule: Record<number, any> = {
1: '平均法',
2: '加权平均法',
3: '综合得分法',
4: '最高分'
4: '最高分',
}
// 参赛模式列表
export const scoreRuleList = json2Array(scoreRule, false)
......@@ -31,7 +31,9 @@ export const gradeRule: Record<number, any> = {
7: '用户群组',
8: '用户旅程',
9: '营销资料',
10: '用户/事件数据'
10: '用户/事件数据',
11: '用户数据',
12: '事件数据',
}
// 参赛模式列表
export const gradeRuleList = json2Array(gradeRule)
......@@ -39,6 +41,6 @@ export const gradeRuleList = json2Array(gradeRule)
// 实验报告评分规则
export const reportScoreRule: Record<number, any> = {
1: '人工评分',
2: '自动评分'
2: '自动评分',
}
export const reportScoreRuleList = json2Array(reportScoreRule)
import axios from 'axios'
import { getLocalFileChunk, uploadLocalFile } from '@/api/base'
export async function upload(file: Blob) {
......@@ -12,3 +13,8 @@ export async function upload(file: Blob) {
return res.data.detail.uri
}
export async function uploadFileByUrl(url: string) {
const res = await axios.get(url, { responseType: 'blob' })
return upload(res.data)
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论