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

chore: 历史监控增加列表项

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'./.eslintrc-auto-import.json'
],
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
}
import pluginVue from 'eslint-plugin-vue'
import vueTsEslintConfig from '@vue/eslint-config-typescript'
import AutoImportJson from './.eslintrc-auto-import.json' with { type: 'json' }
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**', '**/lib/**'],
},
...pluginVue.configs['flat/essential'],
...vueTsEslintConfig(),
{
languageOptions: {
...AutoImportJson,
},
rules: {
'vue/block-lang': 'off',
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
},
},
]
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -9,7 +9,7 @@
"build:pre": "vue-tsc --noEmit && vite build --mode pre",
"preview": "vite preview --port 4173",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"lint": "eslint . --fix",
"deploy": "node ./deploy.js",
"cert": "node ./cert.js"
},
......@@ -44,7 +44,6 @@
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@tsconfig/node20": "^20.1.4",
"@types/ali-oss": "^6.16.8",
"@types/blueimp-md5": "^2.18.0",
......@@ -54,18 +53,18 @@
"@types/ua-parser-js": "^0.7.36",
"@types/video.js": "^7.3.52",
"@vitejs/plugin-vue": "^5.1.4",
"@vue-macros/reactivity-transform": "^1.1.2",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"@vue-macros/reactivity-transform": "^1.1.3",
"@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.6.0",
"chalk": "^5.2.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.25.0",
"eslint": "^9.14.0",
"eslint-plugin-vue": "^9.30.0",
"sass": "^1.58.3",
"typescript": "~5.4.5",
"typescript": "~5.6.3",
"unplugin-auto-import": "^0.17.8",
"vite": "^5.4.9",
"vite-plugin-checker": "^0.6.4",
"vite": "^5.4.10",
"vite-plugin-checker": "^0.8.0",
"vite-plugin-mkcert": "^1.17.6",
"vue-tsc": "^1.8.27"
"vue-tsc": "^2.1.10"
}
}
......@@ -5,7 +5,7 @@ const emit = defineEmits<{
(e: 'resize'): void
}>()
const props = defineProps<{ isLeftShow?: number }>()
defineProps<{ isLeftShow?: number }>()
const leftPanelVisible = $ref<boolean>(true)
const leftPanelWidth = useStorage('leftPanelWidth', 400)
......@@ -19,10 +19,10 @@ let dragFlag = $ref(false)
function useDrag() {
const dragDom = document.getElementById('panel-resize')
if (dragDom) {
dragDom.addEventListener('mousedown', e => {
dragDom.addEventListener('mousedown', (e) => {
e.preventDefault()
dragFlag = true
document.onmousemove = e => {
document.onmousemove = (e) => {
leftPanelWidth.value = e.clientX
}
document.onmouseup = () => {
......@@ -52,14 +52,12 @@ onMounted(() => {
<path
class="path-wapper"
d="M0 0l14.12 8.825A4 4 0 0116 12.217v61.566a4 4 0 01-1.88 3.392L0 86V0z"
fill="#e1e4eb"
></path>
fill="#e1e4eb"></path>
<path
class="path-arrow"
d="M10.758 48.766a.778.778 0 000-1.127L6.996 43l3.762-4.639a.778.778 0 000-1.127.85.85 0 00-1.172 0l-4.344 5.202a.78.78 0 000 1.128l4.344 5.202a.85.85 0 001.172 0z"
fill="#8D9EA7"
fill-rule="nonzero"
></path>
fill-rule="nonzero"></path>
</g>
</svg>
</div>
......
......@@ -34,7 +34,7 @@ const defaultActive = computed(() => {
}
return result
}, [])
const found = flatMenuList.reverse().find(item => {
const found = flatMenuList.reverse().find((item) => {
return route.path.includes(item.path)
})
return found ? found.path : '/'
......@@ -139,7 +139,10 @@ function handleClick(path: string) {
height: 66px;
background-color: #fff;
.logo {
width: 145px;
height: 40px;
img {
height: 100%;
}
}
}
.app-header-left {
......
......@@ -20,7 +20,7 @@ const appConfigList = [
studentMenus: [
{ name: '首页', path: '/' },
{ name: '我的实验', path: '/student/lab' },
{ name: '理论学习', path: import.meta.env.VITE_SAAS_LEARN_URL }
{ name: '理论学习', path: import.meta.env.VITE_SAAS_LEARN_URL },
],
adminMenus: [
{ name: '首页', path: '/' },
......@@ -34,22 +34,22 @@ const appConfigList = [
{ name: '实验指导书管理', path: '/admin/lab/book', tag: 'v1-teacher-book' },
{ name: '实验操作视频管理', path: '/admin/lab/video', tag: 'v1-teacher-video' },
{ name: '实验讨论交流', path: '/admin/lab/discuss', tag: 'v1-teacher-discussion' },
{ name: '实验成绩管理', path: '/admin/lab/score', tag: 'v1-teacher-record' }
]
}
]
{ name: '实验成绩管理', path: '/admin/lab/score', tag: 'v1-teacher-record' },
],
},
],
},
{
system: 'game',
title: '商业数据分析竞赛平台',
hosts: ['saas-game']
hosts: ['saas-game'],
},
{
system: 'dml',
title: '数智营销实践教学平台',
logo: '/logo.svg',
hosts: ['saas-dml-web'],
dmlURL: import.meta.env.VITE_DML_PRO_URL
dmlURL: import.meta.env.VITE_DML_PRO_URL,
},
{
system: 'swsjfxs',
......@@ -60,13 +60,13 @@ const appConfigList = [
studentMenus: [
{ name: '首页', path: '/' },
{ name: '我的大赛', path: '/student/contest' },
{ name: '大赛成绩查询', path: '/student/contest/score' }
{ name: '大赛成绩查询', path: '/student/contest/score' },
],
xTrainLabel: '商务数据分析理论训练',
labTrainLabel: '商务数据分析实操训练',
xExamLabel: '商务数据分析理论考试',
labExamLabel: '商务数据分析实操考试',
loginURL: import.meta.env.VITE_SWSJFXS_LOGIN_URL
loginURL: import.meta.env.VITE_SWSJFXS_LOGIN_URL,
},
{
system: 'swsjfxs',
......@@ -77,7 +77,7 @@ const appConfigList = [
studentMenus: [
{ name: '首页', path: '/' },
{ name: '我的大赛', path: '/student/contest' },
{ name: '大赛成绩查询', path: '/student/contest/score' }
{ name: '大赛成绩查询', path: '/student/contest/score' },
],
xTrainLabel: '理论训练',
labTrainLabel: '实操训练',
......@@ -86,7 +86,7 @@ const appConfigList = [
loginURL: import.meta.env.VITE_SWSJFXS_LOGIN_URL,
hideAvailableEvents: true, // 隐藏可参与赛项
hidePracticalTestPaper: true, // 隐藏实操试卷
hideContestToolbar: true // 隐藏大赛工具栏
hideContestToolbar: true, // 隐藏大赛工具栏
},
{
system: 'amo',
......@@ -97,7 +97,7 @@ const appConfigList = [
studentMenus: [
{ name: '首页', path: '/' },
{ name: '我的大赛', path: '/student/contest' },
{ name: '大赛成绩查询', path: '/student/contest/score' }
{ name: '大赛成绩查询', path: '/student/contest/score' },
],
xTrainLabel: '理论训练',
labTrainLabel: '实操训练',
......@@ -106,7 +106,7 @@ const appConfigList = [
loginURL: import.meta.env.VITE_SWSJFXS_LOGIN_URL,
hideAvailableEvents: true, // 隐藏可参与赛项
hidePracticalTestPaper: true, // 隐藏实操试卷
hideContestToolbar: true // 隐藏大赛工具栏
hideContestToolbar: true, // 隐藏大赛工具栏
},
{
system: 'default',
......@@ -117,7 +117,7 @@ const appConfigList = [
hosts: ['saas-lab-bda2'],
studentMenus: [
{ name: '首页', path: '/' },
{ name: '我的大赛', path: '/student/contest' }
{ name: '我的大赛', path: '/student/contest' },
// { name: '大赛成绩查询', path: '/student/contest/score' }
],
xTrainLabel: '理论训练',
......@@ -127,13 +127,35 @@ const appConfigList = [
// loginURL: import.meta.env.VITE_SWSJFXS_LOGIN_URL,
hideAvailableEvents: true, // 隐藏可参与赛项
hidePracticalTestPaper: true, // 隐藏实操试卷
hideContestToolbar: true // 隐藏大赛工具栏
}
hideContestToolbar: true, // 隐藏大赛工具栏
},
{
system: 'nac',
title: '首届网络视听行业职业技能大赛',
logo: '/images/nac_logo.png',
logoWidth: '40px',
// logo: 'https://webapp-pub.ezijing.com/website/base/images/logo_swsjfxs.png',
// favicon: 'https://webapp-pub.ezijing.com/website/base/images/favicon_swsjfxs.png',
hosts: ['saas-lab-nac'],
studentMenus: [
{ name: '首页', path: '/' },
{ name: '我的大赛', path: '/student/contest' },
// { name: '大赛成绩查询', path: '/student/contest/score' }
],
xTrainLabel: '理论训练',
labTrainLabel: '实操训练',
xExamLabel: '理论考试',
labExamLabel: '实操考试',
// loginURL: import.meta.env.VITE_SWSJFXS_LOGIN_URL,
hideAvailableEvents: true, // 隐藏可参与赛项
hidePracticalTestPaper: true, // 隐藏实操试卷
hideContestToolbar: true, // 隐藏大赛工具栏
},
]
export function useAppConfig() {
const found = appConfigList.find(item => {
return item.hosts.find(host => location.host.split('.').includes(host))
const found = appConfigList.find((item) => {
return item.hosts.find((host) => location.host.split('.').includes(host))
})
const appConfig = found || appConfigList[0]
......
......@@ -51,3 +51,8 @@ export function importScore(data: { competition_id: string; file: File }) {
export function getReportList(params: { competition_id: string; student_id: string }) {
return httpRequest.get('/api/lab/v1/expert/report/list', { params })
}
// 获取学员的直播记录
export function getLiveList(params: { experiment_id: string; student_id: string }) {
return httpRequest.get('/api/lab/v1/expert/check/stu-live-practice-records', { params })
}
<script setup lang="ts">
import type { RecordItem, ReportItem } from '../types'
import { getReportList } from '../api'
import { saveAs } from 'file-saver'
interface Props {
data: RecordItem
}
......@@ -15,16 +15,26 @@ async function fetchReport() {
onMounted(() => {
fetchReport()
})
// office文件
const getUrl = (url: string) => {
const fileExtensionName = url.split('.').pop() || ''
return ['pptx', 'doc', 'docx', 'xls', 'xlsx'].includes(fileExtensionName)
? `https://view.officeapps.live.com/op/view.aspx?src=${url}`
: url
}
</script>
<template>
<el-dialog title="查看报告" width="500px">
<ul>
<li v-for="item in reportList" :key="item.id" style="line-height: 24px">
<a :href="`https://view.officeapps.live.com/op/view.aspx?src=${item.url}`" target="_blank">
<a :href="getUrl(item.url)" target="_blank">
{{ item.name }}
<el-button size="small" type="primary" style="margin-left: 10px">查看</el-button>
</a>
<el-button size="small" type="primary" style="margin-left: 10px" @click="saveAs(item.url, item.name)"
>下载</el-button
>
</li>
</ul>
<template #footer>
......
......@@ -21,7 +21,7 @@ watchEffect(() => {
...item,
old_score: parseFloat(item.old_score),
ratio: parseFloat(item.ratio),
score: isNaN(parseFloat(item.score)) ? null : parseFloat(item.score)
score: isNaN(parseFloat(item.score)) ? 'null' : parseFloat(item.score)
}
})
})
......@@ -43,6 +43,9 @@ const score = $computed(() => {
// 提交
function handleSubmit() {
const scores = tableList.map((item: any) => {
if (item.type === '2') {
item.score = 0
}
return { id: item.id, score: item.score }
})
const params = { id: detail.id, scores: JSON.stringify(scores) }
......@@ -53,28 +56,43 @@ function handleSubmit() {
})
}
const qaURL = import.meta.env.VITE_QA_CENTER_URL
// 查看评分规则
const handleScoreRule = function () {
if (detail?.competition_rubric && detail?.competition_rubric?.url) {
window.open(detail?.competition_rubric?.url)
}
}
</script>
<template>
<el-dialog title="评分" :close-on-click-modal="false" width="700px" @update:modelValue="value => $emit('update:modelValue', value)">
<el-dialog
title="评分"
:close-on-click-modal="false"
width="700px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<el-form label-suffix=":" v-if="detail">
<el-form-item label="赛项名称">{{ detail.competition_id_name }}</el-form-item>
<el-form-item label="选手姓名">{{ detail.student_name }}</el-form-item>
<el-form-item label="选手ID">{{ detail.login_id }}</el-form-item>
<el-form-item label="报告">
<el-dropdown>
<el-button text>
查看报告<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu v-if="reportList.length">
<el-dropdown-item v-for="item in reportList" :key="item.id">
<a :href="item.url" target="_blank">{{ item.platform.name }}</a>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-form-item>
<div style="display: flex; justify-content: space-between">
<el-form-item label="报告" v-if="reportList.length">
<el-dropdown>
<el-button text>
查看报告<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu v-if="reportList.length">
<el-dropdown-item v-for="item in reportList" :key="item.id">
<a :href="item.url" target="_blank">{{ item.platform.name }}</a>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-form-item>
<el-button type="primary" style="margin-bottom: 20px" @click="handleScoreRule">查看评分规则</el-button>
</div>
</el-form>
<el-table :data="tableList" :header-cell-style="{ background: '#ededed' }">
<el-table-column label="评分规则" prop="name" align="center"></el-table-column>
......@@ -83,13 +101,25 @@ const qaURL = import.meta.env.VITE_QA_CENTER_URL
<el-table-column label="占比(%)" prop="ratio" align="center"></el-table-column>
<el-table-column label="评分" prop="score" align="center">
<template #default="{ row }">
<el-input-number :controls="false" v-model="row.score" step-strictly :step="0.1" :min="0" :max="row.old_score" style="width: 100%" />
<el-input-number
:disabled="row.type === '2'"
:controls="false"
v-model="row.score"
step-strictly
:step="0.1"
:min="0"
:max="row.old_score"
style="width: 100%"
placeholder="--"
/>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<el-table-column label="操作" align="center" v-if="false">
<template #default="{ row }">
<el-button text type="primary" v-if="row.type === '2'">
<a :href="`${qaURL}/exam/markingPaper?exam_id=${row.exam_id}&id_number=${detail.id_number}`" target="_blank">查看</a>
<a :href="`${qaURL}/exam/markingPaper?exam_id=${row.exam_id}&id_number=${detail.id_number}`" target="_blank"
>查看</a
>
</el-button>
</template>
</el-table-column>
......
......@@ -29,7 +29,7 @@ function scoreMethodText(value: string) {
</script>
<template>
<el-dialog title="同步1+X考试成绩" :close-on-click-modal="false">
<el-dialog title="同步1+同步线上考试成绩" :close-on-click-modal="false">
<el-form>
<el-form-item label="赛项名称">
<el-select v-model="form.competition" value-key="id">
......
<script setup lang="ts">
import { getLiveList } from '../api'
const props = defineProps<{ data: any }>()
defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
function formatDuration(seconds: number) {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
return minutes > 0 ? `${minutes}m${remainingSeconds}s` : `${remainingSeconds}s`
}
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getLiveList,
params: {
experiment_id: props.data?.eid,
student_id: props.data?.sid,
},
callback(res: any) {
return { list: res.items }
},
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '选手姓名', prop: 'student_name' },
{ label: '商品名称', prop: 'live_commodity.title' },
{
label: '直播时长',
prop: 'live_duration',
computed({ row }: { row: any }) {
return formatDuration(row.live_duration)
},
},
{
label: '文件大小(M)',
prop: 'live_video_size',
computed({ row }: { row: any }) {
return convertBytes(row.live_video_size, 'MB')
},
},
{ label: '上传时间', prop: 'live_end_time' },
{
label: '视频状态',
prop: 'live_video_addres',
computed({ row }: { row: any }) {
return row.live_video_addres ? '已上传' : '未上传'
},
},
{ label: '操作', slots: 'table-x', width: 130 },
],
}
})
const handleViewLive = function (row: any) {
window.open(
`https://saas-dml-pro.ezijing.com/one/live/test/view?experiment_id=${props.data.eid}&id=${row.live_practice_id}&record_id=${row.id}&student_id=${props.data.sid}`
)
}
/**
* 将字节转换为指定单位
* @param {number} bytes - 字节数
* @param {string} unit - 转换目标单位 ('KB', 'MB', 'GB', 'TB')
* @param {number} [decimalPlaces=2] - 保留的小数位数
* @returns {number} - 转换后的值
*/
function convertBytes(bytes: any, unit: any, decimalPlaces = 2) {
const units: any = {
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
TB: 1024 * 1024 * 1024 * 1024,
}
if (!units[unit]) {
throw new Error("无效的单位,请选择 'KB', 'MB', 'GB', 或 'TB'")
}
const result = bytes / units[unit]
return parseFloat(result.toFixed(decimalPlaces))
}
console.log(convertBytes(1048576, 'MB'), 'convertBytes(1048576,')
</script>
<template>
<el-dialog
title="查看直播录像"
:close-on-click-modal="false"
width="700px"
@update:modelValue="(value) => $emit('update:modelValue', value)">
<AppList v-bind="listOptions" ref="appList">
<template #table-count="{ row }"> {{ row.checked_count }}/{{ row.need_check_count }} </template>
<template #table-score="{ row }">
<span :class="{ 'is-info': row.score_name !== '--' }">{{ row.score_name }}</span>
</template>
<template #table-x="{ row }">
<el-button text type="primary" @click="handleViewLive(row)">观看回放</el-button>
</template>
</AppList>
<el-row justify="center">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</el-dialog>
</template>
<style lang="scss" scoped>
.result {
padding: 40px 0;
display: flex;
align-items: center;
justify-content: center;
span {
padding: 0 10px;
font-size: 40px;
color: var(--main-color);
}
}
</style>
<script setup lang="ts">
import type { ReportItem } from '../types'
import { Refresh, Upload } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import { getCheckList } from '../api'
import { getCheckList, getReportList, getCheckView } from '../api'
import { useFilterList } from '../composables/useFilterList'
const SyncExamDialog = defineAsyncComponent(() => import('../components/SyncExamDialog.vue'))
......@@ -9,6 +10,8 @@ const ImportExamDialog = defineAsyncComponent(() => import('../components/Import
const ImportScoreDialog = defineAsyncComponent(() => import('../components/ImportScoreDialog.vue'))
const ScoreViewPicturesDialog = defineAsyncComponent(() => import('../components/ScoreViewPicturesDialog.vue'))
const ReportDialog = defineAsyncComponent(() => import('../components/ReportDialog.vue'))
const ScoreDialog = defineAsyncComponent(() => import('../components/ScoreDialog.vue'))
const ViewLiveDialog = defineAsyncComponent(() => import('../components/ViewLiveDialog.vue'))
const route = useRoute()
......@@ -117,7 +120,7 @@ const listOptions = $computed(() => {
// return getModuleStatus(row, 5)
// }
// },
{ label: '操作', slots: 'table-x', width: 100 }
{ label: '操作', slots: 'table-x', width: 140 }
]
}
})
......@@ -158,6 +161,35 @@ function handleViewReport(row: any) {
// }
// return '未开始'
// }
// 评分
let dialogVisible = $ref(false)
const reportList = ref<ReportItem[]>([])
// 左侧
let detail = $ref<any>()
provide('detail', $$(detail))
const handleScore = function (row: any) {
getCheckView({ id: row.id }).then((res: any) => {
detail = res.data
getReportList({ competition_id: res.data.competition_id, student_id: res.data.student_id }).then((r: any) => {
reportList.value = r.data.items
dialogVisible = true
})
})
}
// 直播录像
let liveDialogVisible = $ref(false)
let liveParams: any = $ref()
const handleViewLive = function (row: any) {
liveParams = {
sid: row.student_id,
eid: row.experiment_id
}
liveDialogVisible = true
}
</script>
<template>
......@@ -194,12 +226,36 @@ function handleViewReport(row: any) {
<span :class="{ 'is-info': row.score_name !== '--' }">{{ row.score_name }}</span>
</template>
<template #table-x="{ row }">
<el-button text type="primary" v-if="row.publish_status === '0'" v-permission="'v1-expert-check-set-score'">
<!-- 评分从详情里移出 -->
<!-- <el-button text type="primary" v-if="row.publish_status === '0'" v-permission="'v1-expert-check-set-score'">
<router-link :to="`/admin/contest/check/${row.id}`" target="_blank">评分</router-link>
</el-button>
</el-button> -->
<el-button text type="primary" @click="handleScore(row)" v-permission="'v1-expert-check-set-score'"
>评分</el-button
>
<br />
<el-button text type="primary" @click="handleViewReport(row)">查看报告</el-button><br />
<el-button text type="primary" @click="handleViewPicture(row)">查看截图</el-button>
<template v-if="row.competition_report_count !== '0'">
<el-button text type="primary" @click="handleViewReport(row)" v-if="row.competition_report_count !== '0'"
>查看报告</el-button
><br />
</template>
<template v-if="row.competition_competitor_pictures && row.competition_competitor_pictures?.pictures">
<el-button
text
type="primary"
@click="handleViewPicture(row)"
v-if="row.competition_competitor_pictures && row.competition_competitor_pictures?.pictures"
>查看截图</el-button
>
<br />
</template>
<el-button
text
type="primary"
@click="handleViewLive(row)"
v-if="row.experiment_id !== '0' && row.experiment_id"
>查看直播录像</el-button
>
</template>
</AppList>
</AppCard>
......@@ -211,11 +267,16 @@ function handleViewReport(row: any) {
<ImportScoreDialog
v-model="importScoreVisible"
@update="onUpdateSuccess"
v-if="importScoreVisible"></ImportScoreDialog>
v-if="importScoreVisible"
></ImportScoreDialog>
<!-- 查看截图 -->
<ScoreViewPicturesDialog v-model="viewPictureVisible" :data="rowData" v-if="rowData"></ScoreViewPicturesDialog>
<!-- 查看报告 -->
<ReportDialog v-model="viewReportVisible" :data="rowData" v-if="rowData && viewReportVisible"></ReportDialog>
<!-- 评分弹窗 -->
<ScoreDialog v-model="dialogVisible" :reportList="reportList"></ScoreDialog>
<!-- 查看直播视频录像 -->
<ViewLiveDialog v-model="liveDialogVisible" v-if="liveDialogVisible" :data="liveParams"></ViewLiveDialog>
</template>
<style lang="scss">
......
......@@ -42,6 +42,7 @@ const listOptions = $computed(() => {
{ label: '赛项名称', prop: 'competition.name' },
{ label: '选手姓名', prop: 'student.name' },
{ label: '参赛ID', prop: 'login_id' },
{ label: '手机号', prop: 'mobile' },
{
label: '性别',
prop: 'student.gender',
......
......@@ -29,7 +29,7 @@ onMounted(() => {
fetchCompetition()
})
const platformKey = ref('career_data_analysis')
const platformKey = ref('data_marketing')
const platformKeys = ref([
{ platform_key: 'career_data_analysis', name: '商业数据分析实验' },
{ platform_key: 'data_marketing', name: '数据营销实操' }
......@@ -59,7 +59,11 @@ onUnmounted(() => {
timer && clearInterval(timer)
})
const statistics = reactive({ competitor_count: 0, complete_answer_competitor_count: 0, starting_answer_competitor_count: 0 })
const statistics = reactive({
competitor_count: 0,
complete_answer_competitor_count: 0,
starting_answer_competitor_count: 0
})
async function fetchStatistics(competition_id: string, platform_key = 'career_data_analysis') {
const res = await getCompetitionStatistics({ competition_id, platform_key })
Object.assign(statistics, res.data.detail)
......@@ -182,7 +186,7 @@ function getModuleStatus(row: any, index: number) {
if (status == 3) return '<span class="is-completed2">部分完成</span>'
if (status == 4) return '<span class="is-completed">全部完成</span>'
} catch (error) {
// console.log(error)
console.log(error)
}
return '<span class="not-started">未开始</span>'
}
......@@ -197,8 +201,13 @@ function getModuleStatus(row: any, index: number) {
<el-select v-model="currentCompetition" value-key="id" size="large" style="margin-right: 20px">
<el-option v-for="item in competitionList" :key="item.id" :value="item" :label="item.name"></el-option>
</el-select>
<el-select v-model="platformKey" size="large">
<el-option v-for="item in platformKeys" :key="item.platform_key" :value="item.platform_key" :label="item.name"></el-option>
<el-select v-model="platformKey" size="large" v-if="false">
<el-option
v-for="item in platformKeys"
:key="item.platform_key"
:value="item.platform_key"
:label="item.name"
></el-option>
</el-select>
</el-row>
<ul class="statistics">
......
......@@ -37,6 +37,7 @@ const listOptions = $computed(() => {
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '专家姓名', prop: 'name' },
{ label: '电话', prop: 'mobile' },
{ label: '所在单位', prop: 'company' },
{
label: '性别',
......
......@@ -107,7 +107,7 @@ export function getContestExpertList(params: { id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition/experts', { params })
}
// 获取赛项未绑定的评分专家列表
export function getContestUnbindExpertList(params: { competition_id: string }) {
export function getContestUnbindExpertList(params: { competition_id: string; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/expert/unbind-competition-list', { params })
}
// 更新赛项评分专家
......@@ -193,3 +193,30 @@ export function exportDrawLotStudentList(params: { draw_rule_id: string }) {
export function getStudentDrawLotInfo(params: { draw_rule_id: string; student_id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition-draw/student-draw-info', { params })
}
// 获取学员的抽签详情
export function getExperimentsList(params: {
type: number; name?: string; 'per-page': number
}) {
return httpRequest.get('/api/resource/v1/backend/competition/experiments', { params })
}
// 赛项绑定实验
export function bindExperiment(data: { id: string; experiment_id: any }) {
return httpRequest.post('/api/resource/v1/backend/competition/bind-experiment', data)
}
// 删除绑定的实验
export function unbindExperiment(data: { id: string, experiment_id: string }) {
return httpRequest.post('/api/resource/v1/backend/competition/unbind-experiment', data)
}
// 获取赛项列表
export function getClassList(params?: { id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition/classes', { params })
}
// 评分专家绑定班级
export function bindClass(data: { id: string; expert_id: string; class_id: string }) {
return httpRequest.post('/api/resource/v1/backend/competition/expert-bind-class', data)
}
......@@ -20,6 +20,7 @@ const listOptions = {
{ label: '序号', type: 'index', width: 60 },
{ label: '选手姓名', prop: 'student.name' },
{ label: '参赛ID', prop: 'login_id' },
{ label: '电话', prop: 'mobile' },
{
label: '性别',
prop: 'student.gender',
......
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { ContestItem, ContestCreateParams, ContestUpdateParams } from '../types'
import { ElMessage } from 'element-plus'
import { pick } from 'lodash-es'
import dayjs from 'dayjs'
import isBetween from 'dayjs/plugin/isBetween'
import { createContest, updateContest, getExamList } from '../api'
import { useMapStore } from '@/stores/map'
import { useGetTeacherList } from '../composables/useGetTeacherList'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
interface Props {
data?: ContestItem | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
dayjs.extend(isBetween)
// 赛项类型
const types = useMapStore().getMapValuesByKey('competition_type')
// 主办单位
const hostUnitList = useMapStore().getMapValuesByKey('host_unit')
// 承办单位
const organizerList = useMapStore().getMapValuesByKey('organizer')
// 技术支持单位
const technicalSupportUnitList = useMapStore().getMapValuesByKey('technical_support_unit')
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
// 指导老师
const { teachers } = useGetTeacherList()
const formRef = $ref<FormInstance>()
const form = reactive({
id: '',
name: '',
client_id: '',
host_unit_id: '',
organizer_ids: [],
technical_support_unit_id: '',
type: '1',
teacher_ids: [],
apply_expiration_date: '',
status: '1',
logo: '',
cover: '',
train_platform_uri: '',
competition_uri: '',
dateRange: undefined,
date: undefined,
datetimeRange: undefined,
datetimeRange2: undefined,
is_switchable_theory_practice: 0,
train_platform_configs: [
{ name: appConfig.xTrainLabel || '1+X理论考试', is_show: '1', type: '1', url: '', platform_key: 'x_exam' },
{
name: appConfig.labTrainLabel || '商业数据分析实验',
is_show: '0',
type: '2',
url: '',
platform_key: 'career_data_analysis'
},
{ name: '数据营销实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' }
],
competition_platform_configs: [
{
name: appConfig.xExamLabel || '1+X理论考试',
is_show: '1',
type: '1',
url: '',
exam_id: '',
platform_key: 'x_exam'
},
// {
// name: appConfig.labExamLabel || '商业数据分析实验',
// is_show: '0',
// type: '2',
// url: '',
// platform_key: 'career_data_analysis'
// },
{ name: '数据营销实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' }
]
})
watchEffect(() => {
if (!props.data) return
const host_unit_id = props.data.host_unit.id
const organizer_ids = props.data.organizers.map(item => item.id)
const technical_support_unit_id = props.data.technical_support_unit.id
const teacher_ids = props.data.teachers.map(item => item.id)
const dateRange = [new Date(props.data.start_range * 1000), new Date(props.data.end_range * 1000)]
const date = new Date(props.data.start_at * 1000)
const datetimeRange = [new Date(props.data.start_at * 1000), new Date(props.data.end_at * 1000)]
const datetimeRange2 = [
new Date(props.data.operational_start_time * 1000),
new Date(props.data.operational_end_time * 1000)
]
const is_switchable_theory_practice = parseInt(props.data.is_switchable_theory_practice)
const apply_expiration_date = props.data.apply_expiration_date * 1000
Object.assign(form, props.data, {
host_unit_id,
organizer_ids,
technical_support_unit_id,
teacher_ids,
dateRange,
date,
datetimeRange,
datetimeRange2,
apply_expiration_date,
is_switchable_theory_practice
})
})
const checkApplyExpirationDate = (rule: any, value: any, callback: any) => {
if (!value) {
callback(new Error('请选择报名截止日期'))
} else {
const [firstDate, secondDate] = form.dateRange || []
if (
!dayjs(value).isBetween(firstDate, secondDate, 'date', '[]') ||
!dayjs(value).isBefore(dayjs(form.date), 'date')
) {
callback(new Error('请选择赛项周期内的日期,且必须早于正式比赛日期'))
}
callback()
}
}
const checkTrainPlatformConfigs = (rule: any, value: any, callback: any) => {
const findItem = function (a: any, b: any) {
return form.train_platform_configs.find((item: any) => item[a] === b)
}
// 判断最少选择一项目
if (!findItem('is_show', '1')) {
callback(new Error('最少选择一项'))
} else {
form.train_platform_configs.forEach((item: any) => {
if (item.is_show === '1') {
if (item.type === '1') {
if (item.exam_id === '') {
callback(new Error(`请选择${item.name}`))
} else {
callback()
}
} else {
if (item.url === '') {
callback(new Error(`请填写${item.name}的链接`))
} else {
callback()
}
}
}
})
}
}
const checkCompetitionPlatformConfigs = (rule: any, value: any, callback: any) => {
const findItem = function (a: any, b: any) {
return form.competition_platform_configs.find((item: any) => item[a] === b)
}
// 判断最少选择一项目
if (!findItem('is_show', '1')) {
callback(new Error('最少选择一项'))
} else {
form.competition_platform_configs.forEach((item: any) => {
if (item.is_show === '1') {
if (item.type === '1') {
if (item.exam_id === '') {
callback(new Error(`请选择${item.name}`))
} else {
callback()
}
} else {
if (item.url === '') {
callback(new Error(`请填写${item.name}的链接`))
} else {
callback()
}
}
}
})
}
}
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入赛项名称' }],
host_unit_id: [{ required: true, message: '请选择主办单位' }],
organizer_ids: [{ type: 'array', required: true, message: '请选择承办单位' }],
technical_support_unit_id: [{ required: true, message: '请选择技术支持单位' }],
type: [{ required: true, message: '请选择赛项类型' }],
teacher_ids: [{ type: 'array', required: true, message: '请选择指导教师', trigger: 'change' }],
dateRange: [{ type: 'array', required: true, message: '请选择赛项周期', trigger: 'change' }],
date: [{ required: true, message: '请选择正式比赛日期', trigger: 'change' }],
datetimeRange: [{ type: 'array', required: true, message: '正式比赛理论答题时间', trigger: 'change' }],
datetimeRange2: [{ type: 'array', required: true, message: '正式比赛实操答题时间', trigger: 'change' }],
apply_expiration_date: [
{ required: true, message: '请选择报名截止日期' },
{
validator: checkApplyExpirationDate,
message: '请选择赛项周期内的日期,且必须早于正式比赛日期'
}
],
// train_platform_uri: [{ required: true, message: '请输入训练平台地址' }],
// competition_uri: [{ required: true, message: '请输入正式比赛地址' }],
status: [{ required: true, message: '请选择有效状态' }],
is_switchable_theory_practice: [{ required: true, message: '请选择' }],
logo: [{ required: true, message: '请上传赛项LOGO' }],
cover: [{ required: true, message: '请上传赛项封面' }],
train_platform_configs: [{ required: true, message: '' }, { validator: checkTrainPlatformConfigs }],
competition_platform_configs: [{ required: true, message: '' }, { validator: checkCompetitionPlatformConfigs }]
})
const isUpdate = $computed(() => {
return !!form.id
})
const title = $computed(() => {
return isUpdate ? '编辑赛项' : '新增赛项'
})
// 是否禁用赛项周期
// const disabledRange = $computed(() => {
// const [firstDate, secondDate] = form.dateRange || []
// return isUpdate && dayjs().isBetween(firstDate, secondDate, 'date', '[]')
// })
// 提交
function handleSubmit() {
function containsNumber(A: number, B: number): boolean {
// 将数字A和B转换成字符串,并检查A的字符串是否包含B的字符串
return A.toString().includes(B.toString())
}
formRef?.validate().then(() => {
const [firstDate, secondDate] = form.dateRange || []
const [firstDatetime, secondDatetime] = form.datetimeRange || []
const [firstDatetime2, secondDatetime2] = form.datetimeRange2 || []
const year = dayjs(form.date).year()
const month = dayjs(form.date).month()
const date = dayjs(form.date).date()
const mergedForm = {
...form,
organizer_ids: JSON.stringify(form.organizer_ids),
// teacher_ids: form.teacher_ids.join(','),
start_range: dayjs(firstDate).unix(),
end_range: dayjs(secondDate).endOf('date').unix(),
start_at: dayjs(firstDatetime).year(year).month(month).date(date).unix(),
end_at: dayjs(secondDatetime).year(year).month(month).date(date).unix(),
operational_start_time: dayjs(firstDatetime2).year(year).month(month).date(date).unix(),
operational_end_time: dayjs(secondDatetime2).year(year).month(month).date(date).unix(),
apply_expiration_date: dayjs(form.apply_expiration_date).endOf('date').unix()
}
// 判断正式比赛理论答题时间和选择的考试
const findExam = examList.find(item => item.exam_id === form.competition_platform_configs[0].exam_id)
const examStartTime = new Date(findExam?.start_time || '').getTime()
const examEndTime = new Date(findExam?.end_time || '').getTime()
// console.log(containsNumber(examStartTime, mergedForm.start_at), containsNumber(examEndTime, mergedForm.end_at))
const findXItem = form.competition_platform_configs.find(item => item.type === '1')
console.log(findXItem, 'findXItem', form.competition_platform_configs)
if (findXItem?.is_show === '1') {
if (
containsNumber(examStartTime, mergedForm.start_at) !== true ||
containsNumber(examEndTime, mergedForm.end_at) !== true
) {
ElMessage({ message: `正式比赛理论答题时间与${findExam?.name}的考试时间不符`, type: 'warning' })
return false
}
}
const params: ContestUpdateParams = pick(mergedForm, [
'operational_start_time',
'operational_end_time',
'id',
'name',
'client_id',
'host_unit_id',
'organizer_ids',
'technical_support_unit_id',
'type',
'start_range',
'end_range',
'start_at',
'end_at',
'apply_expiration_date',
'status',
'logo',
'cover',
// 'train_platform_uri',
// 'competition_uri',
'teacher_ids',
'train_platform_configs',
'competition_platform_configs',
'is_switchable_theory_practice'
])
console.log(isUpdate, 'isUpdate')
isUpdate ? handleUpdate(params) : handleCreate(params)
})
}
// 新增
function handleCreate(params: ContestCreateParams) {
// console.log(params, 'aaa')
createContest(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
// 修改
function handleUpdate(params: ContestUpdateParams) {
updateContest(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
// 赛项周期改变
function handleDateRangeChange(value: any) {
if (value) {
const [, secondDate] = form.dateRange || []
form.date = secondDate
}
}
const clientList = [
{ label: '商务数据分析师赛项', value: 'business_data_analyst' },
{ label: '全媒体运营师赛项', value: 'all_media_operator' },
{ label: '网络主播赛项', value: 'network_anchor_competition' }
]
let examList = $ref<Record<string, any>[]>([])
// 获取关联考试列表
function fetchExamList() {
getExamList({ project: 'x1', 'per-page': 1000 }).then(res => {
examList = res.data.list || []
})
}
onMounted(() => {
fetchExamList()
})
</script>
<template>
<el-dialog
:title="title"
:close-on-click-modal="false"
align-center
width="600px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="165px">
<el-form-item label="客户端标识" prop="client_id">
<el-select v-model="form.client_id" style="width: 100%" clearable>
<el-option v-for="item in clientList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="赛项名称" prop="name">
<el-input v-model="form.name" :disabled="isUpdate" />
</el-form-item>
<el-form-item label="主办单位" prop="host_unit_id">
<el-select v-model="form.host_unit_id" style="width: 100%">
<el-option v-for="item in hostUnitList" :key="item.id" :label="item.label" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="承办单位" prop="organizer_ids">
<el-select v-model="form.organizer_ids" multiple style="width: 100%">
<el-option v-for="item in organizerList" :key="item.id" :label="item.label" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="技术支持单位" prop="technical_support_unit_id">
<el-select v-model="form.technical_support_unit_id" style="width: 100%">
<el-option
v-for="item in technicalSupportUnitList"
:key="item.id"
:label="item.label"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="赛项类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio v-for="item in types" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="指导教师" prop="teacher_ids">
<el-select v-model="form.teacher_ids" multiple style="width: 100%">
<el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="赛项周期" prop="dateRange">
<el-date-picker
type="daterange"
range-separator="至"
v-model="form.dateRange"
style="width: 100%"
@change="handleDateRangeChange"
/>
</el-form-item>
<el-form-item label="正式比赛日期" prop="date">
<el-date-picker type="date" v-model="form.date" style="width: 100%" />
</el-form-item>
<el-form-item label="正式比赛理论答题时间" prop="datetimeRange">
<el-time-picker
is-range
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
v-model="form.datetimeRange"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="正式比赛实操答题时间" prop="datetimeRange2">
<el-time-picker
is-range
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
v-model="form.datetimeRange2"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="报名截止日期" prop="apply_expiration_date">
<el-date-picker type="date" v-model="form.apply_expiration_date" style="width: 100%" />
</el-form-item>
<el-form-item label="训练平台地址" prop="train_platform_configs">
<el-checkbox
true-label="1"
false-label="0"
style="margin-bottom: 10px"
v-model="item.is_show"
v-for="item in form.train_platform_configs"
:key="item.platform_key"
>
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" style="width: 200px" />
</div>
</el-checkbox>
</el-form-item>
<el-form-item label="正式比赛地址" prop="competition_platform_configs">
<el-checkbox
true-label="1"
false-label="0"
style="margin-bottom: 10px"
v-model="item.is_show"
v-for="item in form.competition_platform_configs"
:key="item.platform_key"
>
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" v-if="item.type === '2'" style="width: 200px" />
<el-select v-model="item.exam_id" filterable style="width: 200px" v-if="item.type === '1'">
<el-option
v-for="item in examList"
:key="item.exam_id"
:label="item.name"
:value="item.exam_id"
></el-option>
</el-select>
</div>
</el-checkbox>
</el-form-item>
<el-form-item label="是否允许客户端切换" prop="is_switchable_theory_practice">
<el-radio-group v-model="form.is_switchable_theory_practice">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="有效状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="赛项LOGO" prop="logo">
<AppUpload v-model="form.logo" accept="image/*"></AppUpload>
</el-form-item>
<el-form-item label="赛项封面" prop="cover">
<AppUpload v-model="form.cover" accept="image/*"></AppUpload>
</el-form-item>
<el-row justify="center">
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">取消</el-button>
</el-row>
</el-form>
</el-dialog>
</template>
......@@ -7,7 +7,7 @@ import { pick } from 'lodash-es'
import dayjs from 'dayjs'
import isBetween from 'dayjs/plugin/isBetween'
import { createContest, updateContest } from '../api'
import { createContest, updateContest, getExamList } from '../api'
import { useMapStore } from '@/stores/map'
import { useGetTeacherList } from '../composables/useGetTeacherList'
import { useAppConfig } from '@/composables/useAppConfig'
......@@ -43,7 +43,7 @@ const formRef = $ref<FormInstance>()
const form = reactive({
id: '',
name: '',
client_id: '',
client_id: 'client_1',
host_unit_id: '',
organizer_ids: [],
technical_support_unit_id: '',
......@@ -58,15 +58,41 @@ const form = reactive({
dateRange: undefined,
date: undefined,
datetimeRange: undefined,
datetimeRange2: undefined,
is_switchable_theory_practice: 0,
is_customer_anti_cheat: 0,
practical_competition_tag_name: '',
practical_environment_tag_name: '',
practical_answer_tag_name: '',
practical_report_abel_ame: '',
train_platform_configs: [
{ name: appConfig.xTrainLabel || '1+X理论考试', is_show: '1', type: '1', url: '', platform_key: 'x_exam' },
{ name: appConfig.labTrainLabel || '商业数据分析实验', is_show: '0', type: '2', url: '', platform_key: 'career_data_analysis' },
{ name: '数据营销实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' }
{ name: appConfig.xTrainLabel || '理论', is_show: '1', type: '1', url: '', platform_key: 'x_exam' },
// {
// name: appConfig.labTrainLabel || '商业数据分析实验',
// is_show: '0',
// type: '2',
// url: '',
// platform_key: 'career_data_analysis'
// },
{ name: '实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' }
],
competition_platform_configs: [
{ name: appConfig.xExamLabel || '1+X理论考试', is_show: '1', type: '1', url: '', platform_key: 'x_exam' },
{ name: appConfig.labExamLabel || '商业数据分析实验', is_show: '0', type: '2', url: '', platform_key: 'career_data_analysis' },
{ name: '数据营销实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' }
{
name: appConfig.xExamLabel || '理论',
is_show: '1',
type: '1',
url: '',
exam_id: '',
platform_key: 'x_exam'
},
// {
// name: appConfig.labExamLabel || '商业数据分析实验',
// is_show: '0',
// type: '2',
// url: '',
// platform_key: 'career_data_analysis'
// },
{ name: '实操', is_show: '0', type: '2', url: '', platform_key: 'data_marketing' }
]
})
watchEffect(() => {
......@@ -78,7 +104,27 @@ watchEffect(() => {
const dateRange = [new Date(props.data.start_range * 1000), new Date(props.data.end_range * 1000)]
const date = new Date(props.data.start_at * 1000)
const datetimeRange = [new Date(props.data.start_at * 1000), new Date(props.data.end_at * 1000)]
const datetimeRange2 = [
new Date(props.data.operational_start_time * 1000),
new Date(props.data.operational_end_time * 1000)
]
const is_switchable_theory_practice = parseInt(props.data.is_switchable_theory_practice)
const type = props.data?.type + ''
const status = props.data?.status + ''
const apply_expiration_date = props.data.apply_expiration_date * 1000
let is_customer_anti_cheat = 0
let practical_competition_tag_name = ''
let practical_environment_tag_name = ''
let practical_answer_tag_name = ''
let practical_report_abel_ame = ''
if (props.data.extended_field) {
is_customer_anti_cheat = props.data.extended_field?.is_customer_anti_cheat
practical_competition_tag_name = props.data.extended_field?.practical_competition_tag_name
practical_environment_tag_name = props.data.extended_field?.practical_environment_tag_name
practical_answer_tag_name = props.data.extended_field?.practical_answer_tag_name
practical_report_abel_ame = props.data.extended_field?.practical_report_abel_ame
}
Object.assign(form, props.data, {
host_unit_id,
organizer_ids,
......@@ -87,7 +133,16 @@ watchEffect(() => {
dateRange,
date,
datetimeRange,
apply_expiration_date
datetimeRange2,
apply_expiration_date,
is_switchable_theory_practice,
type,
status,
is_customer_anti_cheat,
practical_competition_tag_name,
practical_environment_tag_name,
practical_answer_tag_name,
practical_report_abel_ame
})
})
const checkApplyExpirationDate = (rule: any, value: any, callback: any) => {
......@@ -95,7 +150,10 @@ const checkApplyExpirationDate = (rule: any, value: any, callback: any) => {
callback(new Error('请选择报名截止日期'))
} else {
const [firstDate, secondDate] = form.dateRange || []
if (!dayjs(value).isBetween(firstDate, secondDate, 'date', '[]') || !dayjs(value).isBefore(dayjs(form.date), 'date')) {
if (
!dayjs(value).isBetween(firstDate, secondDate, 'date', '[]') ||
!dayjs(value).isBefore(dayjs(form.date), 'date')
) {
callback(new Error('请选择赛项周期内的日期,且必须早于正式比赛日期'))
}
callback()
......@@ -112,10 +170,18 @@ const checkTrainPlatformConfigs = (rule: any, value: any, callback: any) => {
} else {
form.train_platform_configs.forEach((item: any) => {
if (item.is_show === '1') {
if (item.url === '') {
callback(new Error(`请填写${item.name}的链接`))
if (item.type === '1') {
if (item.exam_id === '') {
callback(new Error(`请选择${item.name}`))
} else {
callback()
}
} else {
callback()
if (item.url === '') {
callback(new Error(`请填写${item.name}的链接`))
} else {
callback()
}
}
}
})
......@@ -132,10 +198,18 @@ const checkCompetitionPlatformConfigs = (rule: any, value: any, callback: any) =
} else {
form.competition_platform_configs.forEach((item: any) => {
if (item.is_show === '1') {
if (item.url === '') {
callback(new Error(`请填写${item.name}的链接`))
if (item.type === '1') {
if (item.exam_id === '') {
callback(new Error(`请选择${item.name}`))
} else {
callback()
}
} else {
callback()
if (item.url === '') {
callback(new Error(`请填写${item.name}的链接`))
} else {
callback()
}
}
}
})
......@@ -143,6 +217,10 @@ const checkCompetitionPlatformConfigs = (rule: any, value: any, callback: any) =
}
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入赛项名称' }],
practical_competition_tag_name: [{ required: true, message: '请输入' }],
practical_environment_tag_name: [{ required: true, message: '请输入' }],
practical_answer_tag_name: [{ required: true, message: '请输入' }],
practical_report_abel_ame: [{ required: true, message: '请输入' }],
host_unit_id: [{ required: true, message: '请选择主办单位' }],
organizer_ids: [{ type: 'array', required: true, message: '请选择承办单位' }],
technical_support_unit_id: [{ required: true, message: '请选择技术支持单位' }],
......@@ -150,7 +228,8 @@ const rules = ref<FormRules>({
teacher_ids: [{ type: 'array', required: true, message: '请选择指导教师', trigger: 'change' }],
dateRange: [{ type: 'array', required: true, message: '请选择赛项周期', trigger: 'change' }],
date: [{ required: true, message: '请选择正式比赛日期', trigger: 'change' }],
datetimeRange: [{ type: 'array', required: true, message: '请选择正式比赛时间', trigger: 'change' }],
datetimeRange: [{ type: 'array', required: true, message: '正式比赛理论答题时间', trigger: 'change' }],
datetimeRange2: [{ type: 'array', required: true, message: '正式比赛实操答题时间', trigger: 'change' }],
apply_expiration_date: [
{ required: true, message: '请选择报名截止日期' },
{
......@@ -161,6 +240,8 @@ const rules = ref<FormRules>({
// train_platform_uri: [{ required: true, message: '请输入训练平台地址' }],
// competition_uri: [{ required: true, message: '请输入正式比赛地址' }],
status: [{ required: true, message: '请选择有效状态' }],
is_switchable_theory_practice: [{ required: true, message: '请选择' }],
is_customer_anti_cheat: [{ required: true, message: '请选择' }],
logo: [{ required: true, message: '请上传赛项LOGO' }],
cover: [{ required: true, message: '请上传赛项封面' }],
train_platform_configs: [{ required: true, message: '' }, { validator: checkTrainPlatformConfigs }],
......@@ -169,19 +250,21 @@ const rules = ref<FormRules>({
const isUpdate = $computed(() => {
return !!form.id
})
const title = $computed(() => {
return isUpdate ? '编辑赛项' : '新增赛项'
})
// 是否禁用赛项周期
const disabledRange = $computed(() => {
const [firstDate, secondDate] = form.dateRange || []
return isUpdate && dayjs().isBetween(firstDate, secondDate, 'date', '[]')
})
// const disabledRange = $computed(() => {
// const [firstDate, secondDate] = form.dateRange || []
// return isUpdate && dayjs().isBetween(firstDate, secondDate, 'date', '[]')
// })
// 提交
function handleSubmit() {
function containsNumber(A: number, B: number): boolean {
// 将数字A和B转换成字符串,并检查A的字符串是否包含B的字符串
return A.toString().includes(B.toString())
}
formRef?.validate().then(() => {
const [firstDate, secondDate] = form.dateRange || []
const [firstDatetime, secondDatetime] = form.datetimeRange || []
const [firstDatetime2, secondDatetime2] = form.datetimeRange2 || []
const year = dayjs(form.date).year()
const month = dayjs(form.date).month()
const date = dayjs(form.date).date()
......@@ -193,9 +276,37 @@ function handleSubmit() {
end_range: dayjs(secondDate).endOf('date').unix(),
start_at: dayjs(firstDatetime).year(year).month(month).date(date).unix(),
end_at: dayjs(secondDatetime).year(year).month(month).date(date).unix(),
apply_expiration_date: dayjs(form.apply_expiration_date).endOf('date').unix()
operational_start_time: dayjs(firstDatetime2).year(year).month(month).date(date).unix(),
operational_end_time: dayjs(secondDatetime2).year(year).month(month).date(date).unix(),
apply_expiration_date: dayjs(form.apply_expiration_date).endOf('date').unix(),
extended_field: JSON.stringify({
practical_competition_tag_name: form.practical_competition_tag_name,
practical_environment_tag_name: form.practical_environment_tag_name,
practical_answer_tag_name: form.practical_answer_tag_name,
practical_report_abel_ame: form.practical_report_abel_ame,
is_customer_anti_cheat: form.is_customer_anti_cheat
})
}
// 判断正式比赛理论答题时间和选择的考试
const findExam = examList.find(item => item.exam_id === form.competition_platform_configs[0].exam_id)
const examStartTime = new Date(findExam?.start_time || '').getTime()
const examEndTime = new Date(findExam?.end_time || '').getTime()
// console.log(containsNumber(examStartTime, mergedForm.start_at), containsNumber(examEndTime, mergedForm.end_at))
const findXItem = form.competition_platform_configs.find(item => item.type === '1')
if (findXItem?.is_show === '1') {
if (
containsNumber(examStartTime, mergedForm.start_at) !== true ||
containsNumber(examEndTime, mergedForm.end_at) !== true
) {
ElMessage({ message: `正式比赛理论答题时间与${findExam?.name}的考试时间不符`, type: 'warning' })
return false
}
}
const params: ContestUpdateParams = pick(mergedForm, [
'operational_start_time',
'operational_end_time',
'id',
'name',
'client_id',
......@@ -215,9 +326,10 @@ function handleSubmit() {
// 'competition_uri',
'teacher_ids',
'train_platform_configs',
'competition_platform_configs'
'competition_platform_configs',
'is_switchable_theory_practice',
'extended_field'
])
console.log(isUpdate, 'isUpdate')
isUpdate ? handleUpdate(params) : handleCreate(params)
})
}
......@@ -248,110 +360,198 @@ function handleDateRangeChange(value: any) {
}
const clientList = [
{ label: '商务数据分析师赛项', value: 'business_data_analyst' },
{ label: '全媒体运营师赛项', value: 'all_media_operator' }
// { label: '商务数据分析师赛项', value: 'business_data_analyst' },
// { label: '全媒体运营师赛项', value: 'all_media_operator' },
// { label: '网络主播赛项', value: 'network_anchor_competition' }
{ label: '客户端1', value: 'client_1' },
{ label: '客户端2', value: 'client_2' },
{ label: '客户端3', value: 'client_3' },
{ label: '客户端4', value: 'client_4' },
{ label: '客户端5', value: 'client_5' }
]
let examList = $ref<Record<string, any>[]>([])
// 获取关联考试列表
function fetchExamList() {
getExamList({ project: 'x1', 'per-page': 1000 }).then(res => {
examList = res.data.list || []
})
}
onMounted(() => {
fetchExamList()
})
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" align-center width="600px" @update:modelValue="value => $emit('update:modelValue', value)">
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-form-item label="客户端标识" prop="client_id">
<el-select v-model="form.client_id" style="width: 100%" clearable>
<el-option v-for="item in clientList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="赛项名称" prop="name">
<el-input v-model="form.name" :disabled="isUpdate" />
</el-form-item>
<el-form-item label="主办单位" prop="host_unit_id">
<el-select v-model="form.host_unit_id" style="width: 100%">
<el-option v-for="item in hostUnitList" :key="item.id" :label="item.label" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="承办单位" prop="organizer_ids">
<el-select v-model="form.organizer_ids" multiple style="width: 100%">
<el-option v-for="item in organizerList" :key="item.id" :label="item.label" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="技术支持单位" prop="technical_support_unit_id">
<el-select v-model="form.technical_support_unit_id" style="width: 100%">
<el-option v-for="item in technicalSupportUnitList" :key="item.id" :label="item.label" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="赛项类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio v-for="item in types" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="指导教师" prop="teacher_ids">
<el-select v-model="form.teacher_ids" multiple style="width: 100%">
<el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="赛项周期" prop="dateRange">
<el-date-picker type="daterange" range-separator="至" v-model="form.dateRange" style="width: 100%" @change="handleDateRangeChange" />
</el-form-item>
<el-form-item label="正式比赛日期" prop="date">
<el-date-picker type="date" v-model="form.date" style="width: 100%" />
</el-form-item>
<el-form-item label="正式比赛时间" prop="datetimeRange">
<el-time-picker
is-range
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
v-model="form.datetimeRange"
style="width: 100%" />
</el-form-item>
<el-form-item label="报名截止日期" prop="apply_expiration_date">
<el-date-picker type="date" v-model="form.apply_expiration_date" style="width: 100%" />
</el-form-item>
<el-form-item label="训练平台地址" prop="train_platform_configs">
<el-checkbox
true-label="1"
false-label="0"
style="margin-bottom: 10px"
v-model="item.is_show"
v-for="item in form.train_platform_configs"
:key="item.platform_key">
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; width: 200px" />
<el-input v-model="item.url" />
</div>
</el-checkbox>
</el-form-item>
<el-form-item label="正式比赛地址" prop="competition_platform_configs">
<el-checkbox
true-label="1"
false-label="0"
style="margin-bottom: 10px"
v-model="item.is_show"
v-for="item in form.competition_platform_configs"
:key="item.platform_key">
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; width: 200px" />
<el-input v-model="item.url" />
</div>
</el-checkbox>
</el-form-item>
<el-form-item label="有效状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="赛项LOGO" prop="logo">
<AppUpload v-model="form.logo" accept="image/*"></AppUpload>
</el-form-item>
<el-form-item label="赛项封面" prop="cover">
<AppUpload v-model="form.cover" accept="image/*"></AppUpload>
</el-form-item>
<el-row justify="center">
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">取消</el-button>
</el-row>
</el-form>
</el-dialog>
<el-form ref="formRef" :model="form" :rules="rules" label-width="165px">
<div style="display: flex">
<div style="flex: 1; margin-right: 10px">
<el-form-item label="赛项名称" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="承办单位" prop="organizer_ids">
<el-select v-model="form.organizer_ids" multiple style="width: 100%">
<el-option v-for="item in organizerList" :key="item.id" :label="item.label" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="赛项类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio v-for="item in types" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="赛项周期" prop="dateRange">
<el-date-picker
type="daterange"
range-separator="至"
v-model="form.dateRange"
style="width: 100%"
@change="handleDateRangeChange"
/>
</el-form-item>
<el-form-item label="正式比赛日期" prop="date">
<el-date-picker type="date" v-model="form.date" style="width: 100%" />
</el-form-item>
<el-form-item label="正式比赛理论答题时间" prop="datetimeRange">
<el-time-picker
is-range
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
v-model="form.datetimeRange"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="训练平台地址" prop="train_platform_configs">
<el-checkbox
true-label="1"
false-label="0"
style="margin-bottom: 10px"
v-model="item.is_show"
v-for="item in form.train_platform_configs"
:key="item.platform_key"
>
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" style="width: 200px" />
</div>
</el-checkbox>
</el-form-item>
<el-form-item label="指导教师" prop="teacher_ids">
<el-select v-model="form.teacher_ids" multiple style="width: 100%">
<el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="实操赛题标签名称" prop="practical_competition_tag_name">
<el-input placeholder="请输入" v-model="form.practical_competition_tag_name" />
</el-form-item>
<el-form-item label="实操环境标签名称" prop="practical_environment_tag_name">
<el-input placeholder="请输入" v-model="form.practical_environment_tag_name" />
</el-form-item>
<el-form-item label="实操答题标签名称" prop="practical_answer_tag_name">
<el-input placeholder="请输入" v-model="form.practical_answer_tag_name" />
</el-form-item>
<el-form-item label="实操报告标签名称" prop="practical_report_abel_ame">
<el-input placeholder="请输入" v-model="form.practical_report_abel_ame" />
</el-form-item>
<el-form-item label="有效状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
</div>
<div style="flex: 1; margin-left: 10px">
<el-form-item label="主办单位" prop="host_unit_id">
<el-select v-model="form.host_unit_id" style="width: 100%">
<el-option v-for="item in hostUnitList" :key="item.id" :label="item.label" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="技术支持单位" prop="technical_support_unit_id">
<el-select v-model="form.technical_support_unit_id" style="width: 100%">
<el-option
v-for="item in technicalSupportUnitList"
:key="item.id"
:label="item.label"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="客户端" prop="type">
<el-radio-group v-model="form.type">
<el-radio v-for="item in types" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="客户端标识" prop="client_id">
<!-- <el-select v-model="form.client_id" style="width: 100%" clearable>
<el-option v-for="item in clientList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select> -->
<el-radio-group v-model="form.client_id">
<el-radio v-for="item in clientList" :key="item.value" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="报名截止日期" prop="apply_expiration_date">
<el-date-picker type="date" v-model="form.apply_expiration_date" style="width: 100%" />
</el-form-item>
<el-form-item label="正式比赛实操答题时间" prop="datetimeRange2">
<el-time-picker
is-range
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
v-model="form.datetimeRange2"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="正式比赛地址" prop="competition_platform_configs">
<el-checkbox
true-label="1"
false-label="0"
style="margin-bottom: 10px"
v-model="item.is_show"
v-for="item in form.competition_platform_configs"
:key="item.platform_key"
>
<div style="display: flex; align-items: center">
<!-- <span style="margin-right: 10px; width: 180px">{{ item.name }}</span> -->
<el-input v-model="item.name" style="margin-right: 10px; max-width: 130px" />
<el-input v-model="item.url" v-if="item.type === '2'" style="width: 200px" />
<el-select v-model="item.exam_id" filterable style="width: 200px" v-if="item.type === '1'">
<el-option
v-for="item in examList"
:key="item.exam_id"
:label="item.name"
:value="item.exam_id"
></el-option>
</el-select>
</div>
</el-checkbox>
</el-form-item>
<el-form-item label="客户端切换理论与实操" prop="is_switchable_theory_practice">
<el-radio-group v-model="form.is_switchable_theory_practice">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="客户端防作弊" prop="is_customer_anti_cheat">
<el-radio-group v-model="form.is_customer_anti_cheat">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="赛项LOGO" prop="logo">
<AppUpload v-model="form.logo" accept="image/*"></AppUpload>
</el-form-item>
<el-form-item label="赛项封面" prop="cover">
<AppUpload v-model="form.cover" accept="image/*"></AppUpload>
</el-form-item>
</div>
</div>
<el-row justify="center">
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">取消</el-button>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { ContestItem } from '../types'
import { getContestUnbindExpertList, getExpert } from '../api'
import { getContestUnbindExpertList, getExpert, getClassList } from '../api'
import { useMapStore } from '@/stores/map'
const emit = defineEmits<{
......@@ -15,7 +15,7 @@ const detail = $ref(inject('detail') as ContestItem)
let list = $ref<any>([])
function fetchList() {
getContestUnbindExpertList({ competition_id: detail.id }).then(res => {
getContestUnbindExpertList({ competition_id: detail.id, 'per-page': 1000 }).then(res => {
list = res.data.list || []
})
}
......@@ -29,7 +29,13 @@ function fetchInfo(id: string) {
info = undefined
}
}
const classes: any = $ref([])
let classList: any = $ref()
onMounted(() => {
getClassList({ id: detail.id }).then(res => {
classList = res.data
})
fetchList()
})
......@@ -46,7 +52,16 @@ const rules = ref<FormRules>({
// 添加
function handleSubmit() {
formRef?.validate().then(() => {
emit('add', Object.assign(info, { info: { role: form.role, id: '0' }, expert_id: form.id, id: '0' }))
const classesList = classList.filter((item: { id: string }) => classes.includes(item.id))
emit(
'add',
Object.assign(info, {
info: { role: form.role, id: '0', class_id: classes.toString() },
expert_id: form.id,
id: '0',
class_info: classesList
})
)
emit('update:modelValue', false)
})
}
......@@ -62,10 +77,15 @@ function handleSubmit() {
</el-select>
</el-form-item>
<el-form-item label="专家" prop="id">
<el-select v-model="form.id" @change="fetchInfo" style="width: 100%">
<el-select v-model="form.id" filterable @change="fetchInfo" style="width: 100%">
<el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="添加班级">
<el-select multiple v-model="classes" style="width: 100%">
<el-option v-for="item in classList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<template v-if="info">
<el-form-item label="联系电话">{{ info.mobile }}</el-form-item>
<el-form-item label="所在单位">{{ info.company }}</el-form-item>
......
......@@ -40,8 +40,10 @@ const listOptions = $computed(() => {
return {
hasPagination: false,
columns: [
{ type: 'expand', slots: 'class' },
{ label: '序号', type: 'index', width: 60 },
{ label: '专家姓名', prop: 'name' },
{ label: '电话', prop: 'mobile' },
{ label: '所在单位', prop: 'company' },
{
label: '性别',
......@@ -71,13 +73,15 @@ const listOptions = $computed(() => {
})
const dialogVisible = $ref(false)
// const classIds: any = ref()
function handleAdd(data: any) {
// classIds.value = class_ids
list.push(data)
}
// 保存
function handleSubmit() {
const experts = list.map((item: any) => {
return { id: item.id, expert_id: item.expert_id, role: item.info.role }
return { id: item.id, expert_id: item.expert_id, role: item.info.role, class_id: item.info.class_id }
})
const expertsLeader = experts.filter((item: any) => item.role === '1')
if (expertsLeader.length !== 1) {
......@@ -100,7 +104,13 @@ function handleRemoveClass(index: number) {
</script>
<template>
<el-dialog title="评分专家列表" :close-on-click-modal="false" @update:modelValue="value => $emit('update:modelValue', value)">
<el-dialog
width="1000px"
title="评分专家列表"
:close-on-click-modal="false"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<!-- <el-form :disabled="false"> -->
<el-form :disabled="disabled">
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
......@@ -109,6 +119,15 @@ function handleRemoveClass(index: number) {
<p>赛项名称: {{ detail.name }}</p>
</el-row>
</template>
<template #class="{ row }">
<div style="padding-left: 20px">
<h3>班级信息</h3>
<el-table :data="row.class_info" :border="false">
<el-table-column width="200" label="班级id" prop="id" />
<el-table-column label="班级名称" prop="name" />
</el-table>
</div>
</template>
<template #table-x="{ $index }">
<el-button link round type="danger" @click="handleRemoveClass($index)">移除</el-button>
</template>
......
<script setup lang="ts">
import { CirclePlus } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AppList from '@/components/base/AppList.vue'
import { unbindExperiment, getContest } from '../api'
import { useMapStore } from '@/stores/map'
const FormDialog = defineAsyncComponent(() => import('./ViewExperimentFormDialog.vue'))
interface Props {
id: string
pid: string
}
const props = defineProps<Props>()
const types = useMapStore().getMapValuesByKey('experiment_type')
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = {
hasPagination: false,
remote: {
httpRequest: getContest,
params: { id: props.pid },
callback(res: any) {
let list: any = []
if (Object.keys(res?.detail?.experiment).length) {
list = [res?.detail?.experiment]
}
return { list: list }
}
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '实验名称', prop: 'name' },
{
label: '实验类型',
prop: 'type',
computed({ row }: { row: any }) {
const type = types.find(item => item.value === row.type)?.label
return type || ''
}
},
{
label: '实验指导老师',
prop: 'teachers',
computed({ row }: { row: any }) {
let name = ''
if (row.teachers) {
name = row.teachers[0]?.name || ''
}
return name
}
},
{
label: '实验学生人数',
prop: 'classes',
computed({ row }: { row: any }) {
let num = ''
if (row.classes) {
num = row.classes[0]?.student_total
}
return num
}
},
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 180 }
]
}
let dialogVisible = $ref(false)
const rowData = ref<any>()
// 新增
function handleAdd() {
rowData.value = undefined
dialogVisible = true
}
// 查阅
function handleView(row: any) {
const qaURL = `/admin/lab/experiment/${row.id}`
window.open(qaURL)
}
// 删除
function handleRemoveClass(row: any) {
ElMessageBox.confirm('确定要删除吗?', '提示').then(() => {
unbindExperiment({ id: props.id, experiment_id: row.id }).then(() => {
ElMessage({ message: '删除成功', type: 'success' })
onUpdateSuccess()
})
})
}
function onUpdateSuccess() {
appList?.refetch()
}
</script>
<template>
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'competition-book-create'"
>关联</el-button
>
</template>
<template #table-x="{ row }">
<el-button link round type="info" @click="handleView(row)" v-permission="'competition-book-detail'"
>查阅</el-button
>
<el-button link round type="danger" @click="handleRemoveClass(row)" v-permission="'competition-book-delete'"
>删除</el-button
>
</template>
</AppList>
<FormDialog v-model="dialogVisible" :id="id" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
</template>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { ElMessage } from 'element-plus'
import { getExperimentsList, bindExperiment } from '../api'
interface Props {
id?: any
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const formRef = $ref<FormInstance>()
const form = reactive<any>({
id: ''
})
const title = $computed(() => {
return '关联实验'
})
let list = $ref<Record<string, any>[]>([])
const loading = ref(false)
// 获取关联考试列表
function fetchExamList(query?: string) {
loading.value = true
getExperimentsList({ type: 4, name: query, 'per-page': 1000 }).then((res: any) => {
loading.value = false
list = res.data?.list || []
})
}
onMounted(() => {
fetchExamList()
})
// 提交
function handleSubmit() {
bindExperiment({ id: props.id, experiment_id: form.id }).then(() => {
ElMessage({ message: '绑定成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
</script>
<template>
<el-dialog
:title="title"
:close-on-click-modal="false"
width="500px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<el-form ref="formRef" :model="form" label-width="90px">
<el-form-item label="选择实验">
<el-select
v-model="form.id"
filterable
remote
reserve-keyword
placeholder="输入实验名称"
remote-show-suffix
:remote-method="fetchExamList"
:loading="loading"
style="width: 340px"
>
<el-option v-for="item in list" :key="item.value" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-row justify="center">
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">取消</el-button>
</el-row>
</el-form>
</el-dialog>
</template>
......@@ -11,7 +11,8 @@ export const routes: Array<RouteRecordRaw> = [
component: AppLayout,
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: ':id', component: () => import('./views/View.vue'), props: true }
{ path: ':id', component: () => import('./views/View.vue'), props: true },
{ path: 'edit/:id', component: () => import('./views/Edit.vue'), props: true }
]
}
]
......@@ -13,6 +13,8 @@ export interface ContestItem {
end_range: number
start_at: number
end_at: number
operational_start_time: number
operational_end_time: number
apply_expiration_date: number
status: string
logo: string
......@@ -32,6 +34,9 @@ export interface ContestItem {
expert_count: number
train_platform_configs: any[]
competition_platform_configs: any[]
is_switchable_theory_practice: string,
is_customer_anti_cheat: string
extended_field: any
}
export interface ContestCreateParams {
......
<script setup lang="ts">
import type { ContestItem } from '../types'
// import { ElMessage } from 'element-plus'
import { getContest } from '../api'
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
interface Props {
id: string
}
const props = defineProps<Props>()
let detail = $ref<ContestItem | null>(null)
provide('detail', $$(detail))
// 获取赛项信息
function fetchInfo() {
getContest({ id: props.id }).then(res => {
detail = res.data.detail
})
}
onMounted(() => {
if (props.id === '1') return
fetchInfo()
})
</script>
<template>
<AppCard title="编辑赛项">
<FormDialog :data="detail"></FormDialog>
</AppCard>
</template>
<style lang="scss">
.top {
display: flex;
.el-descriptions {
flex: 1;
margin-top: 30px;
}
}
.top-cover {
width: 300px;
margin-right: 20px;
p {
font-weight: normal;
line-height: 30px;
font-size: 14px;
}
img {
width: 100%;
}
}
</style>
......@@ -8,7 +8,7 @@ import { useMapStore } from '@/stores/map'
// 赛项类型
const types = useMapStore().getMapValuesByKey('competition_type')
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
// const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const appList = $ref<InstanceType<typeof AppList> | null>(null)
......@@ -50,31 +50,34 @@ const listOptions = {
]
}
let dialogVisible = $ref(false)
const rowData = ref<ContestItem | undefined | null>(null)
// let dialogVisible = $ref(false)
// const rowData = ref<ContestItem | undefined | null>(null)
// 新增
function handleAdd() {
rowData.value = null
dialogVisible = true
}
// 编辑
function handleUpdate(row: ContestItem) {
rowData.value = row
dialogVisible = true
}
// function handleAdd() {
// rowData.value = null
// dialogVisible = true
// }
// // 编辑
// function handleUpdate(row: ContestItem) {
// rowData.value = row
// dialogVisible = true
// }
function onUpdateSuccess() {
appList?.refetch()
}
// function onUpdateSuccess() {
// appList?.refetch()
// }
</script>
<template>
<AppCard title="赛项管理">
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" :icon="CirclePlus" v-permission="'competition-create'" @click="handleAdd">
<!-- <el-button type="primary" :icon="CirclePlus" v-permission="'competition-create'" @click="handleAdd">
新增赛项
</el-button> -->
<el-button type="primary" :icon="CirclePlus" round v-permission="'competition-create'">
<router-link :to="`/admin/contest/items/edit/1`" target="_blank">新增赛项</router-link>
</el-button>
</template>
......@@ -82,9 +85,14 @@ function onUpdateSuccess() {
<el-button type="primary" round v-permission="'competition-detail'">
<router-link :to="`/admin/contest/items/${row.id}`" target="_blank">查看</router-link>
</el-button>
<el-button type="primary" round @click="handleUpdate(row)" v-permission="'competition-edit'">编辑</el-button>
<el-button type="primary" round v-permission="'competition-detail'">
<router-link :to="`/admin/contest/items/edit/${row.id}`" target="_blank" v-permission="'competition-edit'"
>编辑</router-link
>
</el-button>
<!-- <el-button type="primary" round @click="handleUpdate(row)" v-permission="'competition-edit'">编辑</el-button> -->
</template>
</AppList>
</AppCard>
<FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
<!-- <FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog> -->
</template>
......@@ -10,6 +10,7 @@ const ViewVideo = defineAsyncComponent(() => import('../components/ViewVideo.vue
const ViewQuestion = defineAsyncComponent(() => import('../components/ViewQuestion.vue'))
const ViewExam = defineAsyncComponent(() => import('../components/ViewExam.vue'))
const ViewDrawLots = defineAsyncComponent(() => import('../components/ViewDrawLots.vue'))
const ViewExperiment = defineAsyncComponent(() => import('../components/ViewExperiment.vue'))
const ScoringRulesDialog = defineAsyncComponent(() => import('../components/ScoringRulesDialog.vue'))
const ScoringExpertsDialog = defineAsyncComponent(() => import('../components/ScoringExpertsDialog.vue'))
const ContestantDialog = defineAsyncComponent(() => import('../components/ContestantDialog.vue'))
......@@ -121,10 +122,16 @@ function handleExperts() {
<template>
<AppCard title="查看赛项信息">
<template #header-aside>
<el-button type="primary" @click="scoringRulesVisible = true" v-permission="'competition-rule'">评分规则</el-button>
<el-button type="primary" @click="scoringRulesVisible = true" v-permission="'competition-rule'"
>评分规则</el-button
>
<el-button type="primary" @click="handleExperts" v-permission="'competition-bind-experts'">评分专家</el-button>
<el-button type="primary" @click="contestantVisible = true" v-permission="'competition-competitor-list'">参赛选手</el-button>
<el-button type="primary" @click="scoringRulesBookVisible = true" v-permission="'competition-rubric-update'">评分细则</el-button>
<el-button type="primary" @click="contestantVisible = true" v-permission="'competition-competitor-list'"
>参赛选手</el-button
>
<el-button type="primary" @click="scoringRulesBookVisible = true" v-permission="'competition-rubric-update'"
>评分细则</el-button
>
</template>
<div class="top" v-if="detail">
<div class="top-cover">
......@@ -137,11 +144,15 @@ function handleExperts() {
<el-descriptions-item label="主办单位:">{{ detail.host_unit.label }}</el-descriptions-item>
<el-descriptions-item label="指导教师:">{{ teacherText }}</el-descriptions-item>
<el-descriptions-item label="承办单位:">{{ orgText }}</el-descriptions-item>
<el-descriptions-item label="赛项周期:">{{ formatDate(detail.start_range) }} ~ {{ formatDate(detail.end_range) }}</el-descriptions-item>
<el-descriptions-item label="赛项周期:"
>{{ formatDate(detail.start_range) }} ~ {{ formatDate(detail.end_range) }}</el-descriptions-item
>
<el-descriptions-item label="技术支持单位:">{{ detail.technical_support_unit.label }}</el-descriptions-item>
<el-descriptions-item label="正式比赛日期:">{{ formatDate(detail.start_at) }}</el-descriptions-item>
<el-descriptions-item label="生效状态:">{{ statusText }}</el-descriptions-item>
<el-descriptions-item label="正式比赛时间:">{{ formatTime(detail.start_at) }} ~ {{ formatTime(detail.end_at) }}</el-descriptions-item>
<el-descriptions-item label="正式比赛时间:"
>{{ formatTime(detail.start_at) }} ~ {{ formatTime(detail.end_at) }}</el-descriptions-item
>
<el-descriptions-item label="专家组长:">{{ expertLeadersText }}</el-descriptions-item>
<el-descriptions-item label="专家:">{{ expertMembersText }}</el-descriptions-item>
</el-descriptions>
......@@ -150,30 +161,47 @@ function handleExperts() {
<AppCard title="训练指导书">
<ViewBook :id="id"></ViewBook>
</AppCard>
<AppCard title="操作视频">
<AppCard title="训练操作视频">
<ViewVideo :id="id"></ViewVideo>
</AppCard>
<AppCard title="大赛试题">
<AppCard title="比赛实操赛题">
<ViewQuestion :id="id"></ViewQuestion>
</AppCard>
<AppCard title="大赛试卷">
<AppCard title="比赛实操试卷">
<ViewExam :id="id"></ViewExam>
</AppCard>
<AppCard title="关联营销实验">
<ViewExperiment :id="detail?.id || ''" :pid="id"></ViewExperiment>
</AppCard>
<AppCard title="抽签加密">
<ViewDrawLots :id="id"></ViewDrawLots>
</AppCard>
<!-- 评分规则 -->
<ScoringRulesDialog v-model="scoringRulesVisible" :disabled="isStarted" @update="fetchRule" v-if="scoringRulesVisible && detail"></ScoringRulesDialog>
<ScoringRulesDialog
v-model="scoringRulesVisible"
:disabled="isStarted"
@update="fetchRule"
v-if="scoringRulesVisible && detail"
></ScoringRulesDialog>
<!-- 评分专家 -->
<ScoringExpertsDialog
v-model="scoringExpertsVisible"
:disabled="isStarted"
@update="fetchExperts"
v-if="scoringExpertsVisible && detail"></ScoringExpertsDialog>
v-if="scoringExpertsVisible && detail"
></ScoringExpertsDialog>
<!-- 参赛选手 -->
<ContestantDialog v-model="contestantVisible" :disabled="isStarted" v-if="contestantVisible && detail"></ContestantDialog>
<ContestantDialog
v-model="contestantVisible"
:disabled="isStarted"
v-if="contestantVisible && detail"
></ContestantDialog>
<!-- 评分细则 -->
<ScoringRulesBookDialog v-model="scoringRulesBookVisible" :disabled="isStarted" v-if="scoringRulesBookVisible && detail"></ScoringRulesBookDialog>
<ScoringRulesBookDialog
v-model="scoringRulesBookVisible"
:disabled="isStarted"
v-if="scoringRulesBookVisible && detail"
></ScoringRulesBookDialog>
</template>
<style lang="scss">
......
import httpRequest from '@/utils/axios'
// 获取日志列表
export function getLogs(params?: { competition_id?: string; client_id?: string; competitor_id?: string; competitor_name?: string; competitor_mobile?: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/competition/client-operate-logs', { params })
}
// 获取赛项列表
export function getContestList(params?: { page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/competition/menu-list', { params })
}
\ No newline at end of file
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/contest/log',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
<script setup lang="ts">
import { getContestList, getLogs } from '../api'
import AppList from '@/components/base/AppList.vue'
const appList = $ref<InstanceType<typeof AppList> | null>(null)
const form = reactive({
competition_id: '',
competitor_name: '',
competitor_mobile: '',
page: 1,
'per-page': 10
})
// 搜索
const onSubmit = () => {
requestLogs()
}
let total: number = $ref(0)
let logsList: any = $ref()
const requestLogs = () => {
const params = { ...form }
getLogs(params).then((res: any) => {
logsList = res.data.list.map((item: any) => {
item.client_detail = JSON.parse(item.client_detail)
return item
})
total = parseInt(res.data.total)
})
}
let contestList: any = $ref()
getContestList({ page: 1, 'per-page': 1000 }).then((res: any) => {
contestList = res.data?.list
})
const listOptions = $computed(() => {
return {
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '客户端', prop: 'name' },
{ label: '客户端版本', prop: 'client_version' },
{ label: '赛项', prop: 'competition.name' },
{ label: '选手姓名', prop: 'name' },
{ label: '手机号', prop: 'mobile' },
{ label: '时间', prop: 'created_time' },
{ label: 'IP', prop: 'client_detail.ip' },
{ label: '操作系统', prop: 'client_detail.os' },
{ label: '浏览器', prop: 'client_detail.browser' },
{ label: '操作', prop: 'operation_desc' }
],
data: logsList
}
})
onMounted(() => {
requestLogs()
})
// 页数改变
const pageSizeChange = (value: number) => {
form.page = 1
form['per-page'] = value
requestLogs()
}
const fetchList = (n: any) => {
form.page = n
requestLogs()
}
const reset = () => {
form.competition_id = ''
form.competitor_name = ''
form.competitor_mobile = ''
form.page = 1
form['per-page'] = 10
requestLogs()
}
</script>
<template>
<AppCard title="客户端日志">
<el-form :inline="true" :model="form">
<el-form-item label="赛项">
<el-select clearable filterable v-model="form.competition_id" placeholder="请选择赛项">
<el-option v-for="item in contestList" :label="item?.name" :value="item.id" :key="item?.id" />
</el-select>
</el-form-item>
<el-form-item label="选手姓名">
<el-input clearable v-model="form.competitor_name" placeholder="请输入选手姓名" />
</el-form-item>
<el-form-item label="选手手机号">
<el-input clearable v-model="form.competitor_mobile" placeholder="请输入选手手机号" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">搜索</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<AppList v-bind="listOptions" ref="appList" style="margin: 10px 0"> </AppList>
<div style="display: flex; justify-content: right">
<el-pagination
class="table-list-pagination"
background
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 30, 50]"
:page-size="form['per-page']"
:total="total"
v-model:current-page="form.page"
@size-change="pageSizeChange"
@current-change="fetchList"
>
</el-pagination>
</div>
</AppCard>
</template>
<style lang="scss"></style>
......@@ -58,6 +58,10 @@ const listOptions = computed(() => {
{ label: '用户数据量', prop: 'current_member_count' },
{ label: '标签数据量', prop: 'current_tag_count' },
{ label: '群组数据量', prop: 'current_group_count' },
{ label: '营销物料数量', prop: 'current_marketing_materials_count' },
{ label: '直播数量', prop: 'current_live_practice_count' },
{ label: '直播场次数', prop: 'current_live_practice_record_count' },
{ label: '直播总时长', prop: 'current_live_practice_durations' },
{ label: '实验使用时间', prop: 'time' },
{ label: '操作', slots: 'table-x' }
]
......
......@@ -60,6 +60,10 @@ const listOptions = computed(() => {
{ label: '用户数据量', prop: 'current_member_count' },
{ label: '标签数据量', prop: 'current_tag_count' },
{ label: '群组数据量', prop: 'current_group_count' },
{ label: '营销物料数量', prop: 'current_marketing_materials_count' },
{ label: '直播数量', prop: 'current_live_practice_count' },
{ label: '直播场次数', prop: 'current_live_practice_record_count' },
{ label: '直播总时长', prop: 'current_live_practice_durations' },
{ label: '实验使用时间', prop: 'time' },
{ label: '操作', slots: 'table-x' }
]
......
......@@ -181,3 +181,8 @@ export function copyExperiment(data: { experiment_id: string }) {
export function deleteExperiment(data: { experiment_id: string }) {
return httpRequest.post('/api/resource/v1/backend/experiment/delete', data)
}
// 获取实验成绩规则
export function getLiveCommodity(params: { experiment_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/live-commodity/all', { params })
}
\ No newline at end of file
......@@ -3,7 +3,7 @@ import type { FormInstance } from 'element-plus'
import type { ExperimentItem } from '../types'
import { ElMessage } from 'element-plus'
import { copyExperiment } from '../api'
import { useGetProjectList } from '@/composables/useGetProjectList'
// import { useGetProjectList } from '@/composables/useGetProjectList'
import { useGetTeacherList } from '../composables/useGetTeacherList'
import { pickBy } from 'lodash-es'
const props = defineProps<{
......@@ -31,7 +31,7 @@ onMounted(() => {
})
// 机构列表
const { organizations } = useGetProjectList()
// const { organizations } = useGetProjectList()
// 指导教师列表
const { teachers, updateTeachers } = useGetTeacherList()
......@@ -42,9 +42,9 @@ watch(
}
)
function handleOrgChange() {
form.sso_id = ''
}
// function handleOrgChange() {
// form.sso_id = ''
// }
async function handleSubmit() {
formRef.value?.validate().then(async () => {
......@@ -63,13 +63,13 @@ async function handleSubmit() {
<el-form-item label="实验名称" prop="experiment_name">
<el-input v-model="form.experiment_name" />
</el-form-item>
<el-form-item label="实验所属部门/学校" prop="organ_id">
<!-- <el-form-item label="实验所属部门/学校" prop="organ_id">
<el-select v-model="form.organ_id" style="width: 100%" @change="handleOrgChange" clearable>
<el-option v-for="item in organizations" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form-item> -->
<el-form-item label="指导老师" prop="sso_id">
<el-select v-model="form.sso_id" style="width: 100%" clearable>
<el-select filterable v-model="form.sso_id" style="width: 100%" clearable>
<el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.sso_id"></el-option>
</el-select>
</el-form-item>
......
......@@ -2,7 +2,7 @@
import type { FormInstance } from 'element-plus'
import type { ExperimentItem } from '../types'
import { ElMessage } from 'element-plus'
import { getTripConfig, updateTripConfig } from '../api'
import { getTripConfig, updateTripConfig, getLiveCommodity } from '../api'
import { useConnection, useUserAttr, useMetaEvent, useTag, useGroup, useMaterial } from '../composables/useAllData'
import { useDocumentVisibility } from '@vueuse/core'
......@@ -29,6 +29,92 @@ const dmlURL = computed(() => {
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/trip/template?experiment_id=${props.data.id}`
})
const experimentConfig: any = [
{
id: 1,
name: '基础配置',
is_checked: false,
pid: 0,
children: [
{ id: 2, name: '连接管理', is_checked: false, pid: 1, children: [] },
{ id: 3, name: '用户属性管理', is_checked: false, pid: 1, children: [] },
{ id: 4, name: '事件属性管理', is_checked: false, pid: 1, children: [] }
]
},
{
id: 5,
name: '营销策划',
is_checked: false,
pid: 0,
children: []
},
{
id: 6,
name: '用户画像',
is_checked: false,
pid: 0,
children: []
},
{
id: 7,
name: '用户识别',
is_checked: false,
pid: 0,
children: [
{ id: 8, name: '标签管理', is_checked: false, pid: 7, children: [] },
{ id: 9, name: '群组管理', is_checked: false, pid: 7, children: [] }
]
},
{
id: 10,
name: '营销内容设计',
is_checked: false,
pid: 0,
children: [
{ id: 11, name: '文本资料管理', is_checked: false, pid: 10, children: [] },
{ id: 12, name: '图片资料管理', is_checked: false, pid: 10, children: [] },
{ id: 13, name: '卡券资料管理', is_checked: false, pid: 10, children: [] },
{ id: 14, name: '视频资料管理', is_checked: false, pid: 10, children: [] },
{ id: 15, name: 'H5资料管理', is_checked: false, pid: 10, children: [] },
{ id: 16, name: '二维码资料管理', is_checked: false, pid: 10, children: [] },
{ id: 17, name: '语言资料管理', is_checked: false, pid: 10, children: [] },
{ id: 18, name: '小程序资料管理', is_checked: false, pid: 10, children: [] }
]
},
{
id: 19,
name: '自动化营销',
is_checked: false,
pid: 0,
children: []
},
{
id: 20,
name: '直播带货',
is_checked: false,
pid: 0,
children: [
{ id: 21, name: '商品品类管理', is_checked: false, pid: 20, children: [] },
{ id: 22, name: '商品属性管理', is_checked: false, pid: 20, children: [] },
{ id: 23, name: '商品管理', is_checked: false, pid: 20, children: [] },
{ id: 24, name: '直播练习', is_checked: false, pid: 20, children: [] },
{ id: 25, name: '直播话术管理', is_checked: false, pid: 20, children: [] }
]
},
{
id: 26,
name: '数据分析',
is_checked: false,
pid: 0,
children: [
{ id: 27, name: '用户分析', is_checked: false, pid: 26, children: [] },
{ id: 28, name: '标签群组分析', is_checked: false, pid: 26, children: [] },
{ id: 29, name: '事件分析', is_checked: false, pid: 26, children: [] },
{ id: 30, name: '营销分析', is_checked: false, pid: 26, children: [] }
]
}
]
const formRef = $ref<FormInstance>()
const form = reactive({
experiment_id: props.data.id,
......@@ -43,17 +129,23 @@ const form = reactive({
ids: ['教师维护的用户和事件数据'],
tag_ids: [],
group_ids: [],
material_ids: []
material_ids: [],
auth_config: experimentConfig,
is_use_common_live_commodities: 0,
live_commodity_ids: []
})
// 模板列表
let templateList = $ref<{ id: string; name: string }[]>([])
const checked = ref(false)
function fetchInfo() {
getTripConfig({ experiment_id: props.data.id }).then(res => {
const data = res.data
if (!data.itinerary?.id) return
templateList = [data.itinerary]
if (data.itinerary?.id) {
templateList = [data.itinerary]
}
const connect_ids = data.connections.map((item: any) => item.id)
if (!data.is_config_created) {
Object.assign(form, { itinerary_id: data.itinerary.id, connect_ids })
......@@ -81,8 +173,19 @@ function fetchInfo() {
is_use_common_materials: data.is_use_common_materials,
tag_ids,
group_ids,
material_ids
material_ids,
auth_config: data.auth_config && data.auth_config.length > 0 ? data.auth_config : experimentConfig,
live_commodity_ids: data?.live_commodities.reduce((a: any, b: any) => {
a.push(b.id)
return a
}, []),
is_use_common_live_commodities: data.is_use_common_live_commodities
})
// checked
if (data.auth_config.length) {
checked.value = data.auth_config.find((item: any) => item.is_checked === false) ? false : true
}
// form.auth_config = data.auth_config.length > 0 ? data.auth_config : experimentConfig
})
}
watchEffect(() => fetchInfo())
......@@ -92,7 +195,7 @@ watch(visibility, (current, previous) => {
if (current === 'visible' && previous === 'hidden') fetchInfo()
})
const step = ref(0)
const step = ref(-1)
// 上一步
function handlePrev() {
step.value--
......@@ -112,8 +215,11 @@ function handleSubmit() {
event_config: JSON.stringify(form.event_config),
tag_ids: JSON.stringify(form.tag_ids),
group_ids: JSON.stringify(form.group_ids),
material_ids: JSON.stringify(form.material_ids)
material_ids: JSON.stringify(form.material_ids),
auth_config: JSON.stringify(form.auth_config),
live_commodity_ids: JSON.stringify(form.live_commodity_ids)
}
params.itinerary_id = params.itinerary_id === '' ? (params.itinerary_id = '0') : params.itinerary_id
updateTripConfig(params).then(() => {
ElMessage({ message: '保存成功', type: 'success' })
emit('update')
......@@ -121,13 +227,71 @@ function handleSubmit() {
})
})
}
// 多选
const handleH2Check = (item: any) => {
if (item.is_checked) {
item.children.map((d: any) => {
d.is_checked = false
return d
})
} else {
item.children.map((d: any) => {
d.is_checked = true
return d
})
}
setTimeout(() => {
checked.value = form.auth_config.find((item: any) => item.is_checked === false) ? false : true
}, 100)
}
const handleItemCheck = (item: any) => {
const isCheck = item.children.findIndex((d: any) => d.is_checked === true)
if (isCheck === -1) {
item.is_checked = false
} else {
item.is_checked = true
}
setTimeout(() => {
checked.value = isChe()
}, 100)
function isChe() {
for (let i = 0; i < form.auth_config.length; i++) {
if (form.auth_config[i].is_checked === false) {
return false
}
const c = form.auth_config[i].children.findIndex((cItem: any) => cItem.is_checked === false)
if (c !== -1) {
return false
}
}
return true
}
}
// 直播商品列表
const liveList: any = ref([])
getLiveCommodity({ experiment_id: props.data.id }).then((res: any) => {
liveList.value = res.data?.items || []
})
// 功能全选按钮
const handleSelectAll = () => {
form.auth_config.map((item: any) => {
item.is_checked = !checked.value
item.children.map((cItem: any) => {
cItem.is_checked = !checked.value
})
})
}
</script>
<template>
<el-dialog
title="配置数字营销实验"
:close-on-click-modal="false"
width="600px"
width="1000px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<el-form ref="formRef" :model="form" label-suffix=":">
......@@ -136,7 +300,29 @@ function handleSubmit() {
<el-form-item label="实验类型">{{ data.type_name }} </el-form-item>
<el-form-item label="实验总成绩">{{ data.score }}</el-form-item>
</el-row>
<el-tabs v-model="step">
<el-tabs v-model="step" tab-position="left">
<el-tab-pane label="功能" :name="-1">
<el-checkbox @click="handleSelectAll" v-model="checked" label="全选" size="large" />
<div class="check-ul">
<div class="li" v-for="item in form?.auth_config" :key="item?.id">
<div class="check-h2">
<el-checkbox @click="handleH2Check(item)" v-model="item.is_checked" :label="item.name" size="large" />
</div>
<div class="check-item">
<template v-for="cItem in item.children" :key="cItem?.id">
<el-checkbox
@change="handleItemCheck(item)"
v-model="cItem.is_checked"
:label="cItem.name"
size="large"
/>
</template>
</div>
<el-divider />
</div>
</div>
<!-- <el-checkbox v-model="checked1" label="Option 1" size="large" /> -->
</el-tab-pane>
<el-tab-pane label="模板与连接" :name="0">
<el-form-item label="旅程模板" label-width="82" prop="itinerary_id">
<template v-if="templateList.length">
......@@ -244,6 +430,20 @@ function handleSubmit() {
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="直播商品" :name="6">
<el-form-item label="是否允许学生新建直播商品">
<el-radio-group v-model="form.is_use_common_live_commodities">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="学生能够使用的商品">
<el-checkbox-group v-model="form.live_commodity_ids" :disabled="form.is_use_common_live_commodities === 0">
<el-checkbox :label="item?.id" v-for="item in liveList" :key="item?.id">{{ item?.title }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-tab-pane>
<!-- <el-tab-pane label="旅程资源" :name="10">
<el-form-item label="是否允许学生新建如下资源">
<el-radio-group v-model="form.is_use_common">
......@@ -279,11 +479,33 @@ function handleSubmit() {
</el-form>
<template #footer>
<el-row justify="center">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)" v-if="step === 0">关闭</el-button>
<el-button round auto-insert-space @click="$emit('update:modelValue', false)" v-if="step === -1"
>关闭</el-button
>
<el-button round auto-insert-space @click="handlePrev" v-else>上一步</el-button>
<el-button type="primary" round auto-insert-space @click="handleNext" v-if="step < 2">下一步</el-button>
<el-button type="primary" round auto-insert-space @click="handleSubmit" v-else>保存</el-button>
<el-button type="primary" round auto-insert-space @click="handleNext" v-if="step < 6">下一步</el-button>
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
</el-row>
</template>
</el-dialog>
</template>
<style lang="scss">
.check-ul {
.li {
margin-bottom: 20px;
.check-h2 {
.el-checkbox__label {
color: #ba143e !important;
}
}
.check-item {
.el-checkbox__label {
color: #606266 !important;
}
}
}
.el-divider--horizontal {
margin: 0 !important;
}
}
</style>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import type { ExperimentItem } from '../types'
import { ElMessage } from 'element-plus'
import { getTripConfig, updateTripConfig } from '../api'
import { useConnection, useUserAttr, useMetaEvent, useTag, useGroup, useMaterial } from '../composables/useAllData'
import { useDocumentVisibility } from '@vueuse/core'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const props = defineProps<{
data: ExperimentItem
}>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const { connectionList } = useConnection(props.data.id)
const { userAttrList } = useUserAttr(props.data.id)
const { metaEventList } = useMetaEvent(props.data.id)
const { tagList } = useTag(props.data.id)
const { groupList } = useGroup(props.data.id)
const { materialList } = useMaterial(props.data.id)
const dmlURL = computed(() => {
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}/trip/template?experiment_id=${props.data.id}`
})
const formRef = $ref<FormInstance>()
const form = reactive({
experiment_id: props.data.id,
itinerary_id: '',
connect_ids: [],
user_attr_config: { is_all: true, items: [] },
event_config: { is_all: true, items: [] },
is_use_common: 0,
is_use_common_tags: 0,
is_use_common_groups: 0,
is_use_common_materials: 0,
ids: ['教师维护的用户和事件数据'],
tag_ids: [],
group_ids: [],
material_ids: []
})
// 模板列表
let templateList = $ref<{ id: string; name: string }[]>([])
function fetchInfo() {
getTripConfig({ experiment_id: props.data.id }).then(res => {
const data = res.data
if (!data.itinerary?.id) return
templateList = [data.itinerary]
const connect_ids = data.connections.map((item: any) => item.id)
if (!data.is_config_created) {
Object.assign(form, { itinerary_id: data.itinerary.id, connect_ids })
return
}
const user_attr_config = {
is_all: data.user_attr_config.is_all,
items: data.user_attr_config.items.map((item: any) => item.id)
}
const event_config = {
is_all: data.event_config.is_all,
items: data.event_config.items.map((item: any) => item.id)
}
const tag_ids = data.tags.map((item: any) => item.id)
const group_ids = data.groups.map((item: any) => item.id)
const material_ids = data.marketing_materials.map((item: any) => item.id)
Object.assign(form, {
itinerary_id: data.itinerary.id,
connect_ids,
user_attr_config,
event_config,
is_use_common: data.is_use_common,
is_use_common_tags: data.is_use_common_tags,
is_use_common_groups: data.is_use_common_groups,
is_use_common_materials: data.is_use_common_materials,
tag_ids,
group_ids,
material_ids
})
})
}
watchEffect(() => fetchInfo())
const visibility = useDocumentVisibility()
watch(visibility, (current, previous) => {
if (current === 'visible' && previous === 'hidden') fetchInfo()
})
const step = ref(0)
// 上一步
function handlePrev() {
step.value--
}
// 下一步
function handleNext() {
step.value++
}
// 提交
function handleSubmit() {
formRef?.validate().then(() => {
const params = {
...form,
connect_ids: JSON.stringify(form.connect_ids),
user_attr_config: JSON.stringify(form.user_attr_config),
event_config: JSON.stringify(form.event_config),
tag_ids: JSON.stringify(form.tag_ids),
group_ids: JSON.stringify(form.group_ids),
material_ids: JSON.stringify(form.material_ids)
}
updateTripConfig(params).then(() => {
ElMessage({ message: '保存成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
})
}
</script>
<template>
<el-dialog
title="配置数字营销实验"
:close-on-click-modal="false"
width="600px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<el-form ref="formRef" :model="form" label-suffix=":">
<el-row justify="space-between">
<el-form-item label="实验名称">{{ data.name }}</el-form-item>
<el-form-item label="实验类型">{{ data.type_name }} </el-form-item>
<el-form-item label="实验总成绩">{{ data.score }}</el-form-item>
</el-row>
<el-tabs v-model="step">
<el-tab-pane label="模板与连接" :name="0">
<el-form-item label="旅程模板" label-width="82" prop="itinerary_id">
<template v-if="templateList.length">
<el-select v-model="form.itinerary_id" style="width: 100%">
<el-option v-for="item in templateList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</template>
<template v-else>
<a :href="dmlURL" target="_blank">
<el-button type="primary">新建旅程模板</el-button>
</a>
</template>
</el-form-item>
<el-form-item label="连接" label-width="82" prop="connect_ids">
<el-select v-model="form.connect_ids" multiple style="width: 100%">
<el-option
v-for="item in connectionList"
:label="item.name"
:value="item.id"
:key="item.id"
disabled
></el-option>
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="元数据" :name="1">
<el-form-item label="用户属性" label-width="82">
<el-radio-group v-model="form.user_attr_config.is_all">
<el-radio :label="true">全部</el-radio>
<el-radio :label="false">部分</el-radio>
</el-radio-group>
<el-select
v-model="form.user_attr_config.items"
multiple
style="margin-left: 40px"
v-if="!form.user_attr_config.is_all"
>
<el-option v-for="item in userAttrList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="事件" label-width="82">
<el-radio-group v-model="form.event_config.is_all">
<el-radio :label="true">全部</el-radio>
<el-radio :label="false">部分</el-radio>
</el-radio-group>
<el-select
v-model="form.event_config.items"
multiple
style="margin-left: 40px"
v-if="!form.event_config.is_all"
>
<el-option v-for="item in metaEventList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="用户/事件数据" :name="2">
<el-form-item label="是否允许学生新建如下资源">
<el-radio-group v-model="form.is_use_common">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="用户/事件数据" label-width="118">
<el-select v-model="form.ids" multiple style="width: 100%" disabled>
<el-option value="教师维护的用户和事件数据"></el-option>
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="标签数据" :name="3">
<el-form-item label="是否允许学生新建如下资源">
<el-radio-group v-model="form.is_use_common_tags">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标签数据" label-width="118" prop="tag_ids">
<el-select v-model="form.tag_ids" multiple style="width: 100%">
<el-option v-for="item in tagList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="用户群组" :name="4">
<el-form-item label="是否允许学生新建如下资源">
<el-radio-group v-model="form.is_use_common_groups">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="用户群组" label-width="118" prop="group_ids">
<el-select v-model="form.group_ids" multiple style="width: 100%">
<el-option v-for="item in groupList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="营销资料" :name="5">
<el-form-item label="是否允许学生新建如下资源">
<el-radio-group v-model="form.is_use_common_materials">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="营销资料" label-width="118" prop="material_ids">
<el-select v-model="form.material_ids" multiple style="width: 100%">
<el-option v-for="item in materialList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
</el-tab-pane>
<!-- <el-tab-pane label="旅程资源" :name="10">
<el-form-item label="是否允许学生新建如下资源">
<el-radio-group v-model="form.is_use_common">
<el-radio :label="1">否</el-radio>
<el-radio :label="0">是</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="form.is_use_common === 1">
<el-divider />
<el-form-item label="用户/事件数据" label-width="118">
<el-select v-model="form.ids" multiple style="width: 100%" disabled>
<el-option value="教师维护的用户和事件数据"></el-option>
</el-select>
</el-form-item>
<el-form-item label="标签数据" label-width="118" prop="tag_ids">
<el-select v-model="form.tag_ids" multiple style="width: 100%">
<el-option v-for="item in tagList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="用户群组" label-width="118" prop="group_ids">
<el-select v-model="form.group_ids" multiple style="width: 100%">
<el-option v-for="item in groupList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="营销资料" label-width="118" prop="material_ids">
<el-select v-model="form.material_ids" multiple style="width: 100%">
<el-option v-for="item in materialList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</el-form-item>
</template>
</el-tab-pane> -->
</el-tabs>
</el-form>
<template #footer>
<el-row justify="center">
<el-button round auto-insert-space @click="$emit('update:modelValue', false)" v-if="step === 0">关闭</el-button>
<el-button round auto-insert-space @click="handlePrev" v-else>上一步</el-button>
<el-button type="primary" round auto-insert-space @click="handleNext" v-if="step < 2">下一步</el-button>
<el-button type="primary" round auto-insert-space @click="handleSubmit" v-else>保存</el-button>
</el-row>
</template>
</el-dialog>
</template>
......@@ -138,7 +138,7 @@ function handleUpdate(params: ExperimentCreateItem) {
<el-input-number v-model="form.length" :min="1" :max="20" step-strictly style="width: 100%" />
</el-form-item>
<el-form-item label="指导教师" prop="teachers_ids">
<el-select v-model="form.teachers_ids" multiple style="width: 100%">
<el-select filterable v-model="form.teachers_ids" multiple style="width: 100%">
<el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
......
......@@ -25,11 +25,11 @@ const form = reactive<any>({
is_show: '1',
// rule_list: [{ name: '实验报告', type: 1, percent: 100, rule_mode: 1 }],
rule_list: [],
exam_rules: []
exam_rules: [],
})
function fetchInfo() {
getExperimentGradeRule({ experiment_id: props.data.id }).then(res => {
getExperimentGradeRule({ experiment_id: props.data.id }).then((res) => {
const { detail } = res.data
if (!detail.id) return
let ruleList = []
......@@ -139,12 +139,12 @@ function handleSubmit(call?: any) {
// 当前评分方法
function currentRuleNames(value: number) {
const typeList = form.rule_list.map((item: any) => item.type)
const tempList = gradeRuleList.filter(item => {
const tempList = gradeRuleList.filter((item) => {
return item.value === value || item.value === 5 || !typeList.includes(item.value)
})
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, 10].includes(item.value as number))
} else {
return tempList.filter((item: any) => item.value <= 5)
}
......@@ -209,7 +209,7 @@ const marketingType = {
'5': '5、用户精准分群',
'6': '6、自动化营销旅程',
'7': '7、营销物料设计',
'8': '8、营销策划报告'
'8': '8、营销策划报告',
}
let marketingForm = $ref({
percent: 0,
......@@ -222,8 +222,8 @@ let marketingForm = $ref({
{ type: '5', percent: 15 },
{ type: '6', percent: 15 },
{ type: '7', percent: 15 },
{ type: '8', percent: 5 }
]
{ type: '8', percent: 5 },
],
})
let marketingShow = $ref(false)
const handleMarketingAdd = function () {
......@@ -263,8 +263,7 @@ onMounted(() => {
title="编辑实验成绩规则"
:close-on-click-modal="false"
width="800px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
@update:modelValue="(value) => $emit('update:modelValue', value)">
<el-form ref="formRef" :model="form" label-suffix=":">
<el-form-item label="实验名称">{{ data?.name }}</el-form-item>
<el-row>
......@@ -340,8 +339,7 @@ onMounted(() => {
:disabled="row.type === 1"
style="width: 100%"
@change="handleTypeChange(row)"
v-else
>
v-else>
<el-option v-for="item in currentRuleNames(row.type)" :key="item.value" v-bind="item"></el-option>
</el-select>
</template>
......@@ -393,27 +391,26 @@ onMounted(() => {
<el-input-number
@change="handlePercentChange(marketingForm, 1, 3)"
v-model="marketingForm.percent"
:min="0"
/>
:min="0" />
%
</el-form-item>
<el-button
style="margin-left: 20px"
type="primary"
:icon="!marketingShow ? Plus : Minus"
@click="handleMarketingAdd"
></el-button>
@click="handleMarketingAdd"></el-button>
</div>
<el-divider></el-divider>
<el-form-item
v-if="marketingShow"
style="margin-bottom: 10px"
label-width="150px"
:label="marketingType[item.type as '1']"
v-for="(item, index) in marketingForm.details"
>
<el-input-number @change="handleMarketingChildChange(item, index)" v-model="item.percent" :min="0" /> %
</el-form-item>
<template v-if="marketingShow">
<el-form-item
style="margin-bottom: 10px"
label-width="150px"
:label="marketingType[item.type as '1']"
v-for="(item, index) in marketingForm.details"
:key="index">
<el-input-number @change="handleMarketingChildChange(item, index)" v-model="item.percent" :min="0" /> %
</el-form-item>
</template>
<div class="total-c" v-if="marketingShow">
<p>营销策划小计:{{ marketingTotal }}%</p>
</div>
......
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { Document, CircleCheck, CircleClose, CircleCloseFilled } from '@element-plus/icons-vue'
import type { UploadFile } from 'element-plus'
const route = useRoute()
import { CircleCloseFilled } from '@element-plus/icons-vue'
const modelValue: any = defineModel()
const ruleFormRef = ref<FormInstance>()
// 移除上传文件
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 removeQuestion = (index: number) => {
modelValue.value.splice(index, 1)
}
......
......@@ -2,11 +2,8 @@
import type { FormInstance } from 'element-plus'
import { Document, CircleCheck, CircleClose, CircleCloseFilled } from '@element-plus/icons-vue'
import AppUpload from '@/components/base/AppUpload.vue'
import { getQuestionTags } from '../../api'
import type { UploadFile } from 'element-plus'
const route = useRoute()
const modelValue: any = defineModel()
const ruleFormRef = ref<FormInstance>()
......@@ -42,7 +39,7 @@ const options = $ref<{ id: number; name: string }[]>([
{ id: 405, name: 'H5资料' },
{ id: 406, name: '二维码资料' },
{ id: 407, name: '小程序资料' },
{ id: 408, name: '卡券资料' }
{ id: 408, name: '卡券资料' },
])
onMounted(() => {
const dom: any = document.querySelectorAll('.app-main')[0]
......@@ -57,7 +54,7 @@ const getTips = function (n: number) {
405: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M',
406: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M',
407: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M',
508: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M'
508: '试题文件支持格式包含:png jpg jpeg ,大小不超过5M',
}
return tipText[n]
}
......@@ -111,8 +108,7 @@ const getTips = function (n: number) {
@change="handleDownload(file)"
v-model="file.is_download"
size="large"
active-text="能否下载"
/>
active-text="能否下载" />
</div>
</template>
</AppUpload>
......
......@@ -10,16 +10,18 @@ onMounted(() => {
// 获取题
let list: any = $ref()
const getCurrentQuestions = function () {
getQuestions({ experiment_id: route.query.id }).then(res => {
getQuestions({ experiment_id: route.query.id }).then((res) => {
list = res.data.items
})
}
const selectAnswer = ['302', '202']
</script>
<template>
<el-dialog title="2023商业数据分析大赛决赛试题" :close-on-click-modal="false" width="50%" @update:modelValue="value => $emit('update:modelValue', value)">
<el-dialog
title="2023商业数据分析大赛决赛试题"
:close-on-click-modal="false"
width="50%"
@update:modelValue="(value) => $emit('update:modelValue', value)">
<el-card class="box-card" v-for="(item, index) in list" :key="item.id">
<template #header>
<div class="card-header">
......@@ -27,7 +29,9 @@ const selectAnswer = ['302', '202']
<el-form-item label="本题分值" style="margin: 0"> {{ item.score }} </el-form-item>
</div>
</template>
<div class="text item">正确答案:{{ ['302', '202'].includes(item.type) ? item.answer_info?.name : item?.answer || '上传成功' }}</div>
<div class="text item">
正确答案:{{ ['302', '202'].includes(item.type) ? item.answer_info?.name : item?.answer || '上传成功' }}
</div>
</el-card>
</el-dialog>
</template>
......
......@@ -12,9 +12,10 @@ const detail = $ref(inject('detail') as ExperimentItem)
// 列表配置
const listOptions = {
filters: [{ type: 'input', label: '班级名称', prop: 'name' }],
remote: {
httpRequest: getExperimentClassList,
params: { experiment_id: detail?.id }
params: { experiment_id: detail?.id },
},
columns: [
{ type: 'selection' },
......@@ -23,8 +24,8 @@ const listOptions = {
{ label: '班级名称', prop: 'name' },
{ label: '所属部门/学校', prop: 'organ_id_name' },
{ label: '人数', prop: 'student_nums' },
{ label: '专业', prop: 'specialty_id_name' }
]
{ label: '专业', prop: 'specialty_id_name' },
],
}
let multipleSelection = $ref<ClassItem[]>([])
......@@ -33,7 +34,7 @@ function handleSelectionChange(selection: ClassItem[]) {
}
function handleSubmit() {
const classes_id = multipleSelection.map(item => item.id).join(',')
const classes_id = multipleSelection.map((item) => item.id).join(',')
experimentAddClass({ experiment_id: detail.id, classes_id, type: 'add' }).then(() => {
ElMessage({ message: '关联成功', type: 'success' })
emit('update')
......@@ -42,11 +43,13 @@ function handleSubmit() {
}
</script>
<template>
<el-dialog title="关联班级" @update:modelValue="value => $emit('update:modelValue', value)">
<p>所属机构/学校:{{ detail.organ_id_name }}</p>
<el-dialog title="关联班级" @update:modelValue="(value) => $emit('update:modelValue', value)">
<p style="text-align: right">所属机构/学校:{{ detail.organ_id_name }}</p>
<AppList v-bind="listOptions" @selection-change="handleSelectionChange"></AppList>
<el-row justify="center">
<el-button type="primary" round :disabled="!multipleSelection.length" @click="handleSubmit">关联选择班级</el-button>
<el-button type="primary" round :disabled="!multipleSelection.length" @click="handleSubmit"
>关联选择班级</el-button
>
<el-button round auto-insert-space @click="$emit('update:modelValue', false)">取消</el-button>
</el-row>
</el-dialog>
......
......@@ -66,7 +66,12 @@ function handleUpdate(params: any) {
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="600px" @update:modelValue="value => $emit('update:modelValue', value)">
<el-dialog
:title="title"
:close-on-click-modal="false"
width="600px"
@update:modelValue="value => $emit('update:modelValue', value)"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="考试名称" prop="exam_id">
<el-select v-model="form.exam_id" filterable style="width: 100%">
......
......@@ -95,7 +95,7 @@ const listOptions = $computed(() => {
{ label: '生效状态', prop: 'status_name' },
{ label: '更新人', prop: 'updated_operator_name' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 220 }
{ label: '操作', slots: 'table-x', width: 250 }
]
}
})
......@@ -150,24 +150,28 @@ async function handleDelete(row: ExperimentItem) {
<AppCard title="实验管理">
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" :icon="CirclePlus" v-permission="'v1-backend-experiment-create'" @click="handleAdd">新增实验</el-button>
<el-button type="primary" :icon="CirclePlus" v-permission="'v1-backend-experiment-create'" @click="handleAdd"
>新增实验</el-button
>
</template>
<template #table-x="{ row }: { row: ExperimentItem }">
<el-button type="primary" round v-permission="'v1-backend-experiment-view'">
<router-link :to="`/admin/lab/experiment/${row.id}`" target="_blank">查看</router-link>
</el-button>
<el-button type="primary" round @click="handleUpdate(row)" v-permission="'v1-backend-experiment-update'">编辑</el-button>
<el-button type="primary" round @click="handleUpdate(row)" v-permission="'v1-backend-experiment-update'"
>编辑</el-button
>
<el-button type="primary" round :icon="Delete" @click="handleDelete(row)">删除</el-button>
<el-dropdown style="margin-left: 12px">
<!-- 功能按钮移入详情里 s v-if="false" -->
<el-dropdown style="margin-left: 12px" v-if="false">
<el-button type="primary" round :icon="MoreFilled"></el-button>
<template #dropdown>
<el-dropdown-menu>
<!-- <el-dropdown-item>
<router-link :to="`/admin/lab/experiment/${row.id}`" target="_blank">查看</router-link>
</el-dropdown-item>
<el-dropdown-item @click="handleUpdate(row)">编辑</el-dropdown-item> -->
<el-dropdown-item :icon="Setting" @click="handleUpdateDML(row)" v-if="row.type === '4'">配置数字营销</el-dropdown-item>
<el-dropdown-item :icon="Setting" @click="handleUpdateDML(row)" v-if="row.type === '4'"
>配置数字营销</el-dropdown-item
>
<template v-if="!row.stu_commit_count">
<el-dropdown-item :icon="Edit" @click="handleUpdateGradeRules(row)">编辑成绩规则</el-dropdown-item>
<el-dropdown-item :icon="EditPen">
......@@ -179,6 +183,8 @@ async function handleDelete(row: ExperimentItem) {
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 功能按钮移入详情里 end -->
<!-- <template v-if="row.type === '4'">
<el-button
type="primary"
......@@ -216,8 +222,18 @@ async function handleDelete(row: ExperimentItem) {
</AppCard>
<FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
<!-- 编辑实验成绩规则 -->
<GradeRulesDialog v-model="gradeRulesDialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="gradeRulesDialogVisible && rowData"></GradeRulesDialog>
<GradeRulesDialog
v-model="gradeRulesDialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="gradeRulesDialogVisible && rowData"
></GradeRulesDialog>
<!-- 配置数字营销实验 -->
<DMLFormDialog v-model="dmlDialogVisible" :data="rowData" v-if="dmlDialogVisible && rowData"></DMLFormDialog>
<CopyDialog v-model="copyDialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="copyDialogVisible && rowData"></CopyDialog>
<CopyDialog
v-model="copyDialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="copyDialogVisible && rowData"
></CopyDialog>
</template>
<script setup lang="ts">
import type { ExperimentItem, ClassItem } from '../types'
import { CirclePlus } from '@element-plus/icons-vue'
import { CirclePlus, CopyDocument, Setting, Edit, EditPen } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AppList from '@/components/base/AppList.vue'
......@@ -14,6 +14,9 @@ const StudentListDialog = defineAsyncComponent(() => import('../components/Stude
const ViewGradeRules = defineAsyncComponent(() => import('../components/ViewGradeRules.vue'))
const ViewReportRules = defineAsyncComponent(() => import('../components/ViewReportRules.vue'))
const ViewExam = defineAsyncComponent(() => import('../components/ViewExam.vue'))
const CopyDialog = defineAsyncComponent(() => import('../components/CopyDialog.vue'))
const DMLFormDialog = defineAsyncComponent(() => import('../components/DMLFormDialog.vue'))
const GradeRulesDialog = defineAsyncComponent(() => import('../components/GradeRulesDialog.vue'))
interface Props {
id: string
......@@ -87,18 +90,60 @@ const reportRulesVisible = $ref(false)
const dmlURL = computed(() => {
return `${appConfig.dmlURL || import.meta.env.VITE_DML_URL}?experiment_id=${props.id}`
})
// 复制
let copyDialogVisible = $ref(false)
async function handleCopy() {
copyDialogVisible = true
}
// 配置数字营销实验
let dmlDialogVisible = $ref(false)
function handleUpdateDML() {
dmlDialogVisible = true
}
let gradeRulesDialogVisible = $ref(false)
function handleUpdateGradeRules() {
gradeRulesDialogVisible = true
}
</script>
<template>
<AppCard title="实验管理">
<template #header>
<div>
<h2 class="app-card-hd__title">实验管理</h2>
<div class="btn-all" style="margin-bottom: 15px">
<el-button type="primary" v-if="detail?.type === '4'">
<a :href="dmlURL" target="_blank">进入实验平台</a>
</el-button>
<el-button type="primary" @click="gradeRulesVisible = true">查看成绩规则</el-button>
<el-button type="primary" @click="reportRulesVisible = true">查看报告规则</el-button>
<el-button type="primary" :icon="CopyDocument" @click="handleCopy()">复制实验</el-button>
<el-button type="primary" :icon="Setting" @click="handleUpdateDML()" :disabled="detail?.type !== '4'"
>配置数字营销</el-button
>
<template v-if="!detail?.stu_commit_count">
<el-button type="primary" :icon="Edit" @click="handleUpdateGradeRules()">编辑成绩规则</el-button>
<!-- <el-dropdown-item :icon="EditPen">
<router-link :to="`/admin/lab/experiment/report/${row.id}`" target="_blank">编辑报告规则</router-link>
</el-dropdown-item> -->
<el-button type="primary" :icon="EditPen">
<router-link :to="`/admin/lab/experiment/report/${detail?.id}`" target="_blank">编辑报告规则</router-link>
</el-button>
</template>
</div>
</div>
</template>
<el-descriptions title="基本信息" v-if="detail">
<template #extra>
<!-- <template #extra>
<el-button type="primary" v-if="detail.type === '4'">
<a :href="dmlURL" target="_blank">进入实验平台</a>
</el-button>
<el-button type="primary" @click="gradeRulesVisible = true">查看成绩规则</el-button>
<el-button type="primary" @click="reportRulesVisible = true">查看报告规则</el-button>
</template>
</template> -->
<el-descriptions-item :span="3" label="实验名称:">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="实验课程:">{{ detail.course_name }}</el-descriptions-item>
<el-descriptions-item label="所属机构/学校:">{{ detail.organ_id_name }}</el-descriptions-item>
......@@ -163,4 +208,13 @@ const dmlURL = computed(() => {
></StudentListDialog>
<ViewGradeRules v-model="gradeRulesVisible" :data="detail" v-if="gradeRulesVisible && detail"></ViewGradeRules>
<ViewReportRules v-model="reportRulesVisible" :experiment_id="id" v-if="reportRulesVisible"></ViewReportRules>
<CopyDialog v-model="copyDialogVisible" :data="detail" v-if="copyDialogVisible && detail"></CopyDialog>
<!-- 配置数字营销实验 -->
<DMLFormDialog v-model="dmlDialogVisible" :data="detail" v-if="dmlDialogVisible && detail"></DMLFormDialog>
<!-- 编辑实验成绩规则 -->
<GradeRulesDialog
v-model="gradeRulesDialogVisible"
:data="detail"
v-if="gradeRulesDialogVisible && detail"
></GradeRulesDialog>
</template>
......@@ -56,7 +56,7 @@ const getIframeUrl = function () {
</template>
<template #right>
<div class="lab-box">
<iframe allowfullscreen :src="iframeUrl" frameborder="0" class="iframe" ref="iframeRef"></iframe>
<iframe allowfullscreen allow="camera; microphone" :src="iframeUrl" frameborder="0" class="iframe" ref="iframeRef"></iframe>
</div>
</template>
</DragPanel>
......
......@@ -81,6 +81,19 @@ const learnURL = import.meta.env.VITE_SAAS_LEARN_URL
margin-left: 0 !important;
}
}
.system-nac {
.bg {
background: url(@/assets/images/nac_home_student_bg.png) no-repeat center center;
background-size: contain;
}
.link1,
.select1 {
display: none;
}
.select2 {
margin-left: 0 !important;
}
}
.system-x {
.bg {
background: url(@/assets/images/x_home_student_bg.png) no-repeat center center;
......
......@@ -61,9 +61,9 @@ function handleChange(id: string, type: number) {
background-size: contain;
}
}
.system-gdrtvu {
.system-nac {
.bg {
background: url(@/assets/images/gdrtvu_home_teacher_bg.png) no-repeat center center;
background: url(@/assets/images/nac_home_teacher_bg.png) no-repeat center center;
background-size: contain;
}
}
......
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
// import { ElMessage } from 'element-plus'
import AppUpload from '@/components/base/AppUpload.vue'
import { useCountdown } from '@/composables/useCountdown'
import { joinContest2, sendApplySMS } from '../api'
// import { useCountdown } from '@/composables/useCountdown'
import {
joinContest2,
// sendApplySMS
} from '../api'
const { second, disabled, start } = useCountdown()
const countdownText = $computed(() => {
return disabled.value ? `(${second.value})秒` : '点击获取'
})
// const { second, disabled, start } = useCountdown()
// const countdownText = $computed(() => {
// return disabled.value ? `(${second.value})秒` : '点击获取'
// })
// 发送验证码
function sendSMS() {
if (form.mobile) {
start()
sendApplySMS({ competition_id: form.competition_id, mobile: form.mobile }).catch(() => {
stop()
})
} else {
ElMessage({
showClose: true,
message: '请输入手机号',
type: 'warning'
})
new Error('请输入手机号')
}
}
// function sendSMS() {
// if (form.mobile) {
// start()
// sendApplySMS({ competition_id: form.competition_id, mobile: form.mobile }).catch(() => {
// stop()
// })
// } else {
// ElMessage({
// showClose: true,
// message: '请输入手机号',
// type: 'warning'
// })
// new Error('请输入手机号')
// }
// }
const router = useRouter()
const formRef = $ref<FormInstance>()
......@@ -45,7 +48,7 @@ const form = reactive({
photo: '',
// code: '',
protocol: false,
team_name: ''
team_name: '',
})
const checkProtocol = (rule: any, value: any, callback: any) => {
if (!value) {
......@@ -68,7 +71,7 @@ const rules = ref<FormRules>({
teacher_name: [{ required: true, message: '请输入指导教师' }],
photo: [{ required: true, message: '请上传证件照' }],
code: [{ required: true, message: '请输入验证码' }],
protocol: [{ validator: checkProtocol, trigger: 'change' }]
protocol: [{ validator: checkProtocol, trigger: 'change' }],
})
let dialogVisible = $ref(false)
......
......@@ -108,7 +108,7 @@ function uploadPicture(url: string) {
screenshotLoading = false
})
}
let resizeKey = $ref(0)
// let resizeKey = $ref(0)
function handleResize() {
resizeKey = Date.now()
}
......@@ -185,7 +185,7 @@ const reportDialogVisible = $ref(false)
</g>
</svg>
</div>
<iframe allowfullscreen :src="competitionUrl" :key="iframeKey" frameborder="0" class="iframe" ref="iframeRef"></iframe>
<iframe allowfullscreen allow="camera; microphone" :src="competitionUrl" :key="iframeKey" frameborder="0" class="iframe" ref="iframeRef"></iframe>
</div>
</template>
</DragPanel>
......
......@@ -298,7 +298,7 @@ function handleReportPreviewReady() {
</AppCard>
<div class="lab-box">
<el-empty description="您已经提交该实验,不能再进行操作,切换其他实验再做操作吧。" v-if="submitted" />
<iframe allowfullscreen :src="LAB_URL" :key="iframeKey" frameborder="0" class="iframe" ref="iframeRef" v-else></iframe>
<iframe allow="camera; microphone" allowfullscreen :src="LAB_URL" :key="iframeKey" frameborder="0" class="iframe" ref="iframeRef" v-else></iframe>
</div>
</template>
</DragPanel>
......
......@@ -33,27 +33,28 @@ const adminMenus: IMenuItem[] = [
{ name: '实验监控', path: '/admin/lab/dashboard' }
]
},
// {
// name: '技能大赛',
// path: '/admin/contest',
// children: [
// { name: '赛项管理', path: '/admin/contest/items', tag: 'competition' },
// { name: '参赛选手管理', path: '/admin/contest/contestants', tag: 'competition-competitor' },
// { name: '评分专家管理', path: '/admin/contest/experts', tag: 'expert' },
// { name: '大赛训练答疑', path: '/admin/contest/discuss', tag: 'v1-teacher-train-discussion' },
// { name: '大赛监控', path: '/admin/contest/dashboard', tag: 'v1-expert-statistic' },
// { name: '大赛评分', path: '/admin/contest/check', tag: 'v1-expert-check' },
// { name: '大赛发布成绩', path: '/admin/contest/score', tag: 'v1-expert-score' }
// ]
// },
// {
// name: '成绩分析',
// path: '/admin/contest/analyze',
// children: [
// { name: '赛项成绩画像', path: '/admin/contest/analyze/score' },
// { name: '学生个人成绩画像', path: '/admin/contest/analyze/student' }
// ]
// }
{
name: '技能大赛',
path: '/admin/contest',
children: [
{ name: '赛项管理', path: '/admin/contest/items', tag: 'competition' },
{ name: '参赛选手管理', path: '/admin/contest/contestants', tag: 'competition-competitor' },
{ name: '评分专家管理', path: '/admin/contest/experts', tag: 'expert' },
{ name: '大赛训练答疑', path: '/admin/contest/discuss', tag: 'v1-teacher-train-discussion' },
{ name: '大赛监控', path: '/admin/contest/dashboard', tag: 'v1-expert-statistic' },
{ name: '大赛评分', path: '/admin/contest/check', tag: 'v1-expert-check' },
{ name: '大赛发布成绩', path: '/admin/contest/score', tag: 'v1-expert-score' },
{ name: '客户端日志', path: '/admin/contest/log', tag: '' }
]
},
{
name: '成绩分析',
path: '/admin/contest/analyze',
children: [
{ name: '赛项成绩画像', path: '/admin/contest/analyze/score' },
{ name: '学生个人成绩画像', path: '/admin/contest/analyze/student' }
]
}
]
const appConfig = useAppConfig()
......
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["auto-imports.d.ts", "env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"allowJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["element-plus/global", "@vue-macros/reactivity-transform/macros-global"]
}
}
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["auto-imports.d.ts", "env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"allowJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["element-plus/global", "@vue-macros/reactivity-transform/macros-global"]
},
"files": [],
"references": [
{
"path": "./tsconfig.config.json"
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
......@@ -3,6 +3,11 @@
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}
......@@ -4,7 +4,7 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// import checker from 'vite-plugin-checker'
import checker from 'vite-plugin-checker'
import AutoImport from 'unplugin-auto-import/vite'
import ReactivityTransform from '@vue-macros/reactivity-transform/vite'
import mkcert from 'vite-plugin-mkcert'
......@@ -12,15 +12,15 @@ import mkcert from 'vite-plugin-mkcert'
export default defineConfig(() => ({
// base: mode === 'prod' ? 'https://webapp-pub.ezijing.com/website/prod/saas-lab/' : '/',
plugins: [
mkcert(),
vue(),
AutoImport({
imports: ['vue', 'vue-router', '@vueuse/core', '@vueuse/math'],
dts: true,
eslintrc: { enabled: true }
eslintrc: { enabled: true },
}),
ReactivityTransform(),
mkcert()
// checker({ vueTsc: true, eslint: { lintCommand: 'eslint "./src/**/*.{vue,js,jsx,ts,tsx}"' } })
checker({ vueTsc: true, eslint: { useFlatConfig: true, lintCommand: 'eslint "./src/**/*.{vue,js,jsx,ts,tsx}"' } }),
],
server: {
open: true,
......@@ -30,7 +30,7 @@ export default defineConfig(() => ({
// cert: fs.readFileSync(path.join(__dirname, './https/ezijing.com.pem'))
// },
proxy: {
'/api': 'https://saas-lab.ezijing.com'
'/api': 'https://saas-lab.ezijing.com',
// '/api/resource': {
// target: 'http://com-resource-admin-test.ezijing.com',
// changeOrigin: true,
......@@ -41,11 +41,11 @@ export default defineConfig(() => ({
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api\/lab/, '')
// }
}
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
}))
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论