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

Merge branch 'master' into 202412

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