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

Merge branch 'master' into gdrtvu

...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@tinymce/tinymce-vue": "^5.0.0", "@tinymce/tinymce-vue": "^5.0.0",
"@vant/area-data": "^1.5.1", "@vant/area-data": "^1.5.1",
"@vueuse/core": "^9.13.0", "@vueuse/core": "^9.13.0",
...@@ -1108,6 +1109,11 @@ ...@@ -1108,6 +1109,11 @@
"@jridgewell/sourcemap-codec": "1.4.14" "@jridgewell/sourcemap-codec": "1.4.14"
} }
}, },
"node_modules/@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
...@@ -7655,6 +7661,11 @@ ...@@ -7655,6 +7661,11 @@
"@jridgewell/sourcemap-codec": "1.4.14" "@jridgewell/sourcemap-codec": "1.4.14"
} }
}, },
"@microsoft/fetch-event-source": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
},
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@tinymce/tinymce-vue": "^5.0.0", "@tinymce/tinymce-vue": "^5.0.0",
"@vant/area-data": "^1.5.1", "@vant/area-data": "^1.5.1",
"@vueuse/core": "^9.13.0", "@vueuse/core": "^9.13.0",
......
...@@ -7,6 +7,7 @@ const appConfigList = [ ...@@ -7,6 +7,7 @@ const appConfigList = [
dmlURL: import.meta.env.VITE_DML_PRO_URL dmlURL: import.meta.env.VITE_DML_PRO_URL
}, },
{ {
system: 'default',
title: '商业数据分析实验室', title: '商业数据分析实验室',
logo: 'https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo.svg', logo: 'https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo.svg',
hosts: ['saas-lab'] hosts: ['saas-lab']
......
import httpRequest from '@/utils/axios'
// 获取实验列表
export function getExperimentList(params?: { page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/monitor-experiments', { params })
}
// 获取实时监控实验列表
export function getCurrentExperimentList(params?: { hour?: number; limit?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/current-monitor-experiments', { params })
}
<script setup lang="ts">
import type { ExperimentItem } from '../types'
import AppList from '@/components/base/AppList.vue'
import { getExperimentList } from '../api'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const appList = ref<InstanceType<typeof AppList> | null>(null)
const list = ref<ExperimentItem[]>([])
async function fetchInfo() {
const res = await getExperimentList()
list.value = res.data.items
}
onMounted(() => {
fetchInfo()
})
const dmlURL = computed(() => {
return appConfig.dmlURL || import.meta.env.VITE_DML_URL
})
// 实验列表
const listOptions = computed(() => {
return {
remote: {
httpRequest: getExperimentList,
callback(data: any) {
return { total: data.total, list: data.items }
}
},
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '所属机构', prop: 'org.department_name' },
{ label: '实验课程名称', prop: 'course_name' },
{ label: '实验名称', prop: 'name' },
{
label: '指导老师',
prop: 'teachers',
computed({ row }: { row: ExperimentItem }) {
return row.teachers.map(item => item.name).join(',')
}
},
{
label: '班级名称',
prop: 'student.specialty_name',
computed({ row }: { row: ExperimentItem }) {
return row.classes.map(item => item.name).join(',')
}
},
{
label: '班级人数',
prop: 'student.class_name',
computed({ row }: { row: ExperimentItem }) {
return row.classes.map(item => item.student_total).join(',')
}
},
{ label: '使用人数', prop: 'current_use_user_count' },
{ label: '用户数据量', prop: 'current_member_count' },
{ label: '标签数据量', prop: 'current_tag_count' },
{ label: '群组数据量', prop: 'current_group_count' },
{ label: '实验使用时间', prop: 'time' },
{ label: '操作', slots: 'table-x' }
]
}
})
</script>
<template>
<AppCard>
<ul class="statistics">
<template v-for="(item, index) in list" :key="item.id">
<el-popover :width="340" trigger="hover" v-if="index < 5">
<el-form label-suffix=":" class="statistics-form">
<el-form-item label="实验名称">{{ item.name }}</el-form-item>
<el-form-item label="班级人数">{{ item.classes.map(item => item.student_total).join(',') }}</el-form-item>
<el-form-item label="使用人数">{{ item.current_use_user_count }}</el-form-item>
</el-form>
<template #reference>
<li>
<h6>
<span>Top{{ index + 1 }}</span>
</h6>
<p>{{ item.name }}</p>
</li>
</template>
</el-popover>
</template>
</ul>
<h2 class="h2-title">实验列表</h2>
<AppList border v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary"><a :href="`${dmlURL}?experiment_id=${row.id}`" target="_blank">查看</a></el-button>
</template>
</AppList>
</AppCard>
</template>
<style lang="scss" scoped>
.statistics {
display: flex;
align-items: center;
justify-content: space-evenly;
margin: 90px 0;
&:empty {
display: none;
}
li {
width: 212px;
height: 212px;
background-color: rgba(247, 247, 247, 1);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
h6 {
color: rgba(178, 15, 60, 1);
span {
font-size: 18px;
}
}
p {
margin-top: 20px;
font-size: 18px;
color: rgba(96, 96, 96, 1);
text-align: center;
}
}
}
.statistics-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-form-item__content) {
font-size: 18px;
color: var(--main-color);
}
}
.h2-title {
padding-left: 5px;
font-size: 18px;
font-weight: 500;
line-height: 1;
margin: 20px 0;
border-left: 3px solid #aa1941;
}
</style>
<script setup lang="ts">
import type { ExperimentItem } from '../types'
import AppList from '@/components/base/AppList.vue'
import { getCurrentExperimentList } from '../api'
import { useAppConfig } from '@/composables/useAppConfig'
const appConfig = useAppConfig()
const appList = ref<InstanceType<typeof AppList> | null>(null)
const list = ref<ExperimentItem[]>([])
async function fetchInfo() {
const res = await getCurrentExperimentList()
list.value = res.data.items
}
let timer: null | number = null
onMounted(() => {
fetchInfo()
timer = setInterval(() => {
fetchInfo()
}, 10000)
})
onUnmounted(() => {
timer && clearInterval(timer)
})
const dmlURL = computed(() => {
return appConfig.dmlURL || import.meta.env.VITE_DML_URL
})
// 实验列表
const listOptions = computed(() => {
return {
data: list.value,
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '所属机构', prop: 'org.department_name' },
{ label: '实验课程名称', prop: 'course_name' },
{ label: '实验名称', prop: 'name' },
{
label: '指导老师',
prop: 'teachers',
computed({ row }: { row: ExperimentItem }) {
return row.teachers.map(item => item.name).join(',')
}
},
{
label: '班级名称',
prop: 'student.specialty_name',
computed({ row }: { row: ExperimentItem }) {
return row.classes.map(item => item.name).join(',')
}
},
{
label: '班级人数',
prop: 'student.class_name',
computed({ row }: { row: ExperimentItem }) {
return row.classes.map(item => item.student_total).join(',')
}
},
{ label: '正在使用人数', prop: 'current_use_user_count' },
{ label: '用户数据量', prop: 'current_member_count' },
{ label: '标签数据量', prop: 'current_tag_count' },
{ label: '群组数据量', prop: 'current_group_count' },
{ label: '实验使用时间', prop: 'time' },
{ label: '操作', slots: 'table-x' }
]
}
})
</script>
<template>
<AppCard>
<ul class="statistics">
<template v-for="(item, index) in list" :key="item.id">
<el-popover :width="340" trigger="hover" v-if="index < 5">
<el-form label-suffix=":" class="statistics-form">
<el-form-item label="实验名称">{{ item.name }}</el-form-item>
<el-form-item label="班级人数">{{ item.classes.map(item => item.student_total).join(',') }}</el-form-item>
<el-form-item label="正在使用人数">{{ item.current_use_user_count }}</el-form-item>
</el-form>
<template #reference>
<li>
<h6>
<span>Top{{ index + 1 }}</span>
</h6>
<p>{{ item.name }}</p>
</li>
</template>
</el-popover>
</template>
</ul>
<h2 class="h2-title">实验列表</h2>
<AppList border v-bind="listOptions" ref="appList">
<template #table-x="{ row }">
<el-button type="primary"><a :href="`${dmlURL}?experiment_id=${row.id}`" target="_blank">查看</a></el-button>
</template>
</AppList>
</AppCard>
</template>
<style lang="scss" scoped>
.statistics {
display: flex;
align-items: center;
justify-content: space-evenly;
margin: 90px 0;
&:empty {
display: none;
}
li {
width: 212px;
height: 212px;
background-color: rgba(247, 247, 247, 1);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
h6 {
color: rgba(178, 15, 60, 1);
span {
font-size: 18px;
}
}
p {
margin-top: 20px;
font-size: 18px;
color: rgba(96, 96, 96, 1);
text-align: center;
}
}
}
.statistics-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-form-item__content) {
font-size: 18px;
color: var(--main-color);
}
}
.h2-title {
padding-left: 5px;
font-size: 18px;
font-weight: 500;
line-height: 1;
margin: 20px 0;
border-left: 3px solid #aa1941;
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/lab/dashboard',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export interface OrgItem {
department_name: string
project_id: string
project_name: string
}
export interface ClassItem {
id: string
name: string
student_total: string
}
export interface TeacherItem {
id: string
name: string
}
export interface ExperimentItem {
id: string
name: string
time: string
org: OrgItem
classes: ClassItem[]
course_name: string
teachers: TeacherItem[]
current_use_user_count: string
current_member_count: string
current_tag_count: string
current_group_count: string
}
<script setup lang="ts">
import Live from '../components/Live.vue'
import History from '../components/History.vue'
const activeRadio = ref('1')
</script>
<template>
<el-radio-group v-model="activeRadio" style="margin-bottom: 10px">
<el-radio-button label="1">实时监控</el-radio-button>
<el-radio-button label="2">历史监控</el-radio-button>
</el-radio-group>
<Live v-if="activeRadio === '1'"></Live>
<History v-else></History>
</template>
...@@ -25,8 +25,8 @@ const form = reactive({ ...@@ -25,8 +25,8 @@ const form = reactive({
onMounted(() => { onMounted(() => {
form.organ_id = props.data.organ_id form.organ_id = props.data.organ_id
form.experiment_name = props.data.name + '(copy)' form.experiment_name = props.data.name + '(copy)'
const [teacher] = props.data.teacher // const [teacher] = props.data.teacher
form.sso_id = teacher.id // form.sso_id = teacher.sso_id
}) })
// 机构列表 // 机构列表
...@@ -34,9 +34,12 @@ const { organizations } = useGetProjectList() ...@@ -34,9 +34,12 @@ const { organizations } = useGetProjectList()
// 指导教师列表 // 指导教师列表
const { teachers, updateTeachers } = useGetTeacherList() const { teachers, updateTeachers } = useGetTeacherList()
watchEffect(() => { watch(
updateTeachers(form.organ_id) () => form.organ_id,
}) () => {
updateTeachers(form.organ_id)
}
)
function handleOrgChange() { function handleOrgChange() {
form.sso_id = '' form.sso_id = ''
...@@ -66,7 +69,7 @@ async function handleSubmit() { ...@@ -66,7 +69,7 @@ async function handleSubmit() {
</el-form-item> </el-form-item>
<el-form-item label="指导老师" prop="sso_id"> <el-form-item label="指导老师" prop="sso_id">
<el-select v-model="form.sso_id" style="width: 100%" clearable> <el-select v-model="form.sso_id" style="width: 100%" clearable>
<el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.sso_id"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
......
...@@ -3,6 +3,7 @@ import { getExperimentTeacherList } from '../api' ...@@ -3,6 +3,7 @@ import { getExperimentTeacherList } from '../api'
interface TeacherType { interface TeacherType {
id: string id: string
name: string name: string
sso_id: string
} }
export function useGetTeacherList() { export function useGetTeacherList() {
......
import httpRequest from '@/utils/axios'
// 聊天(流式响应)
export function qwenChat(data: any) {
return httpRequest.post('/api/lab/v1/experiment/qwen/chat', data, { headers: { 'Content-Type': 'application/json' } })
}
<script setup>
const emit = defineEmits(['success'])
const file = ref()
const onSuccess = res => {
file.value = res.data.detail
emit('success', file.value)
}
</script>
<template>
<el-upload
class="ai-upload"
drag
action="/api/lab/v1/experiment/qwen/upload-file"
accept=".csv, .xls, .xlsx, text/csv, application/csv,text/comma-separated-values, application/csv, application/excel,application/vnd.msexcel, text/anytext, application/vnd. ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
:data="{ purpose: 'file-extract' }"
:show-file-list="false"
:on-success="onSuccess">
<ul class="ai-upload-list" v-if="file">
<li>{{ file.filename }}</li>
</ul>
<div class="ai-upload-box">
<img src="@/assets/images/ai_plus.png" height="40" />
<div class="el-upload__text"><em>点击</em>上传数据文件</div>
</div>
</el-upload>
</template>
<style lang="scss">
.ai-upload {
.el-upload-dragger {
padding: 20px;
}
.ai-upload-list {
margin-bottom: 20px;
text-align: left;
li {
padding: 0 10px;
display: flex;
align-items: center;
min-height: 40px;
background: #ffffff;
box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.16);
}
}
.el-upload__text {
margin-top: 20px;
em {
text-decoration: underline;
}
}
}
</style>
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { ElMessage } from 'element-plus'
export function useChat() {
const messages = ref([])
const isLoading = ref(false)
async function post() {
isLoading.value = true
await fetchEventSource('/api/lab/v1/experiment/qwen/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'qwen-long', messages: messages.value }),
async onopen(response) {
if (response.ok) {
return response
} else {
throw response
}
},
onmessage(res) {
console.log(res.data)
try {
const message = JSON.parse(res.data)
if (message.error) {
ElMessage.error(message.error.message)
return
}
const id = message.id
const messageIndex = messages.value.findIndex(session => session.id === id)
let content = message?.choices[0]?.delta.content || ''
content = content.replaceAll('\n', '<br/>')
if (messageIndex === -1) {
messages.value.push({ id, role: 'assistant', content })
} else {
messages.value[messageIndex].content = messages.value[messageIndex].content + content
}
isLoading.value = false
} catch (error) {
console.log(error)
isLoading.value = false
}
},
onerror(err) {
isLoading.value = false
throw err
}
})
}
return { messages, post, isLoading }
}
import type { RouteRecordRaw } from 'vue-router'
export const routes: Array<RouteRecordRaw> = [
{
path: '/ai',
component: () => import('./views/Index.vue')
}
]
<script setup>
import Upload from '../components/Upload.vue'
import { useChat } from '../composabels/useChat'
const { messages, post, isLoading } = useChat()
const chatInput = ref('')
const onUploadSuccess = res => {
const message = { role: 'system', content: `fileid://${res.id}` }
messages.value.push(message)
}
async function postMessage() {
if (!chatInput.value) return
const message = { role: 'user', content: chatInput.value }
messages.value.push(message)
post(message)
chatInput.value = ''
}
const chatRef = ref()
function scrollToBottom() {
if (!chatRef.value) return
chatRef.value.scrollTo(0, chatRef.value.scrollHeight)
}
watch(messages.value, () => nextTick(() => scrollToBottom()))
</script>
<template>
<div class="ai-wrapper" :class="{ 'is-center': !messages.length }">
<header class="ai-header">
<div class="ai-header-inner">
<div class="ai-header-left">
<img src="https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo.svg" width="174" />
<div class="ai-header__title">AI商业数据分析</div>
</div>
<div class="ai-header-right">感知AI数据分析,让数据一触即知</div>
</div>
</header>
<main class="ai-main">
<Upload @success="onUploadSuccess" />
<div class="ai-message" ref="chatRef">
<template v-for="(item, index) in messages" :key="index">
<div class="ai-message-item" :class="item.role" v-if="item.role !== 'system'">
<div class="ai-message__avatar"><img :src="item.role === 'assistant' ? '/images/ai_avatar_bot.png' : '/images/ai_avatar_user.png'" /></div>
<div class="ai-message__content" v-html="item.content"></div>
</div>
</template>
<div class="ai-message-item" v-if="isLoading">
<div class="dot-flashing"></div>
</div>
</div>
<footer class="ai-footer">
<el-input placeholder="输入你想提问的问题" v-model="chatInput" @keyup.enter="postMessage">
<template #suffix>
<img src="@/assets/images/ai_send.png" class="ai-footer__button" @click="postMessage" />
</template>
</el-input>
</footer>
</main>
</div>
</template>
<style lang="scss">
.ai-wrapper {
height: 100vh;
display: flex;
align-items: center;
flex-direction: column;
&.is-center {
justify-content: space-evenly;
.ai-header {
box-shadow: none;
}
.ai-main {
flex: none;
}
.el-upload-dragger {
padding: 80px 0;
}
}
}
.ai-main {
display: flex;
flex-direction: column;
flex: 1;
width: 1000px;
overflow: hidden;
.ai-upload {
margin: 40px 0;
}
}
.ai-header {
width: 100%;
box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.16);
}
.ai-header-inner {
max-width: 1000px;
padding: 18px 0;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
}
.ai-header__title {
margin-top: 8px;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 16px;
color: #2c2c2c;
letter-spacing: 7px;
text-align: right;
}
.ai-header-right {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 24px;
color: #2c2c2c;
letter-spacing: 5px;
}
.ai-footer {
margin: 40px 10px;
.el-input__wrapper {
height: 60px;
font-size: 16px;
border-radius: 33px;
box-shadow: 0px 3px 12px 1px rgba(0, 0, 0, 0.12);
}
}
.ai-footer__button {
cursor: pointer;
}
.ai-message {
min-height: 100px;
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
overflow-x: hidden;
overflow-y: auto;
}
.ai-message-item {
margin-bottom: 30px;
padding: 10px;
border-radius: 12px;
background-color: #f5edef;
display: flex;
color: #000;
gap: 10px;
}
.ai-message-item.user {
align-self: flex-end;
color: #fff;
background-color: #ab2940;
flex-direction: row-reverse;
}
.ai-message__avatar {
width: 48px;
height: 48px;
background-color: #fff;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.ai-message__content {
flex: 1;
max-width: 100%;
font-size: 16px;
line-height: 24px;
word-break: break-word;
align-self: center;
}
.dot-flashing {
animation: dot-flashing 0.8s infinite alternate;
animation-delay: -0.2s;
animation-timing-function: ease;
margin: 7px 18px;
overflow: visible !important;
position: relative;
}
.dot-flashing,
.dot-flashing:after,
.dot-flashing:before {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
color: rgba(0, 0, 0, 0.1);
height: 8px;
width: 8px;
}
.dot-flashing:after,
.dot-flashing:before {
animation: dot-flashing 0.8s infinite alternate;
animation-timing-function: ease;
content: '';
display: inline-block;
position: absolute;
top: 0;
}
.dot-flashing:before {
left: -15px;
animation-delay: -0.4s;
}
.dot-flashing:after {
left: 15px;
animation-delay: 0s;
}
@keyframes dot-flashing {
0% {
background-color: #000;
}
50% {
background-color: rgba(0, 0, 0, 0.1);
}
to {
background-color: #000;
}
}
</style>
...@@ -275,6 +275,9 @@ function handleReportPreviewReady() { ...@@ -275,6 +275,9 @@ function handleReportPreviewReady() {
<el-button type="primary" :disabled="disabled" @click="handleSubmit">提交实验</el-button> <el-button type="primary" :disabled="disabled" @click="handleSubmit">提交实验</el-button>
</div> </div>
<div> <div>
<el-button type="primary" v-if="appConfig.system == 'default'">
<router-link to="/ai" target="_blank">AI数据分析</router-link>
</el-button>
<el-button type="primary" :disabled="disabled" :loading="screenshotLoading" @click="handleCapture">截图</el-button> <el-button type="primary" :disabled="disabled" :loading="screenshotLoading" @click="handleCapture">截图</el-button>
<el-button type="primary" :disabled="disabled" @click="prepareDialogVisible = true">实验准备</el-button> <el-button type="primary" :disabled="disabled" @click="prepareDialogVisible = true">实验准备</el-button>
<el-button type="primary" :disabled="disabled" @click="resultDialogVisible = true">实验结果</el-button> <el-button type="primary" :disabled="disabled" @click="resultDialogVisible = true">实验结果</el-button>
......
...@@ -29,7 +29,8 @@ const adminMenus: IMenuItem[] = [ ...@@ -29,7 +29,8 @@ const adminMenus: IMenuItem[] = [
{ name: '实验指导书管理', path: '/admin/lab/book', tag: 'v1-teacher-book' }, { name: '实验指导书管理', path: '/admin/lab/book', tag: 'v1-teacher-book' },
{ name: '实验操作视频管理', path: '/admin/lab/video', tag: 'v1-teacher-video' }, { name: '实验操作视频管理', path: '/admin/lab/video', tag: 'v1-teacher-video' },
{ name: '实验讨论交流', path: '/admin/lab/discuss', tag: 'v1-teacher-discussion' }, { 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' },
{ name: '实验监控', path: '/admin/lab/dashboard' }
] ]
}, },
// { // {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论