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

feat: 新增案例授权

上级 151ff894
...@@ -43,7 +43,7 @@ function handleClick(path: string) { ...@@ -43,7 +43,7 @@ function handleClick(path: string) {
</script> </script>
<template> <template>
<aside class="app-aside"> <aside class="app-aside" v-if="menuList.length > 0">
<nav class="nav"> <nav class="nav">
<el-menu :default-active="defaultActive" class="app-menu"> <el-menu :default-active="defaultActive" class="app-menu">
<template v-for="item in menuList" :key="item.path"> <template v-for="item in menuList" :key="item.path">
......
import { getCreateAuth, updateAuth } from '@/api/base'
import httpRequest from '@/utils/axios'
import type {
BookAuthorizeCreateItem,
CaseAuthorizeCreateItem,
ResourceDocumentCreateItem,
ResourceVideoCreateItem,
VideoAuthorizeCreateItem,
VideoUploadAuthParams,
VideoUploadRefreshParams,
} from './types'
// 获取课程列表
export function getCourseList() {
return httpRequest.get('/api/lab/v1/teacher/course/list')
}
// 获取实验列表
export function getExperimentList(params: { course_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/experiments', { params })
}
// 创建案例原文
export function createCase(data: CaseAuthorizeCreateItem) {
return httpRequest.post('/api/lab/v1/teacher/experiment-cases/create', data)
}
// 创建指导书
export function createBook(data: BookAuthorizeCreateItem) {
return httpRequest.post('/api/lab/v1/teacher/book/create', data)
}
// 创建操作视频
export function createVideo(data: VideoAuthorizeCreateItem) {
return httpRequest.post('/api/lab/v1/teacher/video/create', data)
}
// 获取上传视频凭证
export function getUploadVideoAuth(data: VideoUploadAuthParams) {
return httpRequest.post('/api/lab/v1/teacher/video/auth-create', data)
}
// 刷新上传视频地址凭证
export function updateUploadVideoAuth(data: VideoUploadRefreshParams) {
return httpRequest.post('/api/lab/v1/teacher/video/create-auth', data)
}
// 统一资源平台视频上传凭证
export function getResourceUploadVideoAuth(data: VideoUploadAuthParams) {
return getCreateAuth(data)
}
// 统一资源平台刷新上传视频地址凭证
export function updateResourceUploadVideoAuth(data: VideoUploadRefreshParams) {
return updateAuth(data)
}
// 创建统一资源平台视频
export function createResourceVideo(data: ResourceVideoCreateItem) {
return httpRequest.post('/api/resource/v1/resource/video/create', data)
}
// 创建统一资源平台课件
export function createResourceCourseware(data: ResourceDocumentCreateItem) {
return httpRequest.post('/api/resource/v1/resource/courseware/create', data)
}
// 创建统一资源平台教案
export function createResourceLessonPlan(data: ResourceDocumentCreateItem) {
return httpRequest.post('/api/resource/v1/resource/lesson-plan/create', data)
}
// 创建统一资源平台其他资料
export function createResourceOther(data: ResourceDocumentCreateItem) {
return httpRequest.post('/api/resource/v1/resource/other-information/create', data)
}
import axios from 'axios'
import type { CaseLibraryItem } from '../types'
export function useCaseLibrary() {
const list = ref<CaseLibraryItem[]>([])
const loading = ref(false)
function fetchList() {
loading.value = true
return axios.get(`https://webapp-pub.ezijing.com/case_library/case.json?_t=${Date.now()}`)
.then((res) => {
list.value = res.data
})
.finally(() => {
loading.value = false
})
}
return { list, loading, fetchList }
}
import { getCourseList } from '../api'
export interface CourseType {
id: string
name: string
}
const courses = ref<CourseType[]>([])
export function useGetCourseList() {
function updateCourses() {
getCourseList().then((res: any) => {
courses.value = res.data
})
}
return { courses, updateCourses }
}
import { getExperimentList } from '../api'
export interface ExperimentType {
id: string
name: string
}
export function useGetExperimentList() {
const experiments = ref<ExperimentType[]>([])
function updateExperiments(courseId?: string) {
if (!courseId) {
experiments.value = []
return
}
getExperimentList({ course_id: courseId }).then((res: any) => {
experiments.value = res.data
}).catch((err) => {
console.error('获取实验列表失败', err)
experiments.value = []
})
}
return { experiments, updateExperiments }
}
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/case-auth',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }],
},
]
export interface CaseLibraryFile {
type: string
type_name: string
name: string
url: string
size?: string
source_id?: string
created_at: string
updated_at: string
}
export interface CaseLibraryItem {
id: string
name: string
description: string
files: CaseLibraryFile[]
operator: { id: string; name: string }
created_at: string
updated_at: string
}
export type CaseFileSelectionType = 'case' | 'ppt' | 'book' | 'video' | 'dataset'
export type AuthorizePlatform = 'resource' | 'experiment'
export interface CaseAuthorizeCreateItem {
experiment_id: string
status: string
name: string
type: string
url: string
size: string
}
export interface BookAuthorizeCreateItem {
experiment_id: string
status: string
name: string
type: string
url: string
size: string
}
export interface VideoAuthorizeCreateItem {
experiment_id: string
status: string
name: string
source_id: string
}
export interface VideoUploadAuthParams {
title: string
file_name: string
}
export interface VideoUploadRefreshParams {
source_id: string
}
export interface ResourceDocumentCreateItem {
name: string
source: string
classification: string
knowledge_points?: string
url: string
type: string
size: string
}
export interface ResourceVideoCreateItem {
name: string
source: string
classification: string
knowledge_points: string
cover: string
source_id: string
}
<script setup lang="ts">
import { useCaseAuthorization } from '../composables/useCaseAuthorization'
const defaultProps = {
children: 'children',
label: 'category_name',
value: 'id',
}
const {
caseFileSectionConfig,
caseFileTypeMap,
categoryList,
courses,
experiments,
fileSelections,
groupedFiles,
list,
loading,
platformSelections,
progress,
progressText,
resourceClassification,
selectedCase,
selectedCaseId,
selectedCount,
selectedCourse,
selectedExperiment,
submitting,
getAllChecked,
getFileKey,
getFilesByType,
handleCaseChange,
handleToggleAll,
isTypeSelectable,
submitAuthorize,
} = useCaseAuthorization()
</script>
<template>
<AppCard title="案例授权">
<div class="case-auth-toolbar">
<el-checkbox-group v-model="platformSelections">
<el-checkbox label="resource">统一资源平台</el-checkbox>
<el-checkbox label="experiment">实操平台</el-checkbox>
</el-checkbox-group>
<el-tree-select
v-if="platformSelections.includes('resource')"
v-model="resourceClassification"
:data="categoryList"
:props="defaultProps"
node-key="id"
clearable
check-strictly
filterable
style="width: 240px"
placeholder="请选择统一资源分类" />
<el-select
v-if="platformSelections.includes('experiment')"
v-model="selectedCourse"
placeholder="请选择课程"
filterable
clearable
style="width: 200px">
<el-option v-for="item in courses" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select
v-if="platformSelections.includes('experiment')"
v-model="selectedExperiment"
placeholder="请选择实验"
filterable
clearable
style="width: 200px">
<el-option v-for="item in experiments" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-button type="primary" :loading="submitting" @click="submitAuthorize" :disabled="!selectedCount">
批量授权
</el-button>
</div>
<el-alert type="warning" :closable="false" class="case-auth-alert">
<div class="case-auth-alert__content">
<div class="case-auth-alert__title">重要提示:</div>
<div>1、授权到实操平台前请先创建课程和实验</div>
<div>2、授权到统一资源平台时必须选择分类</div>
<div class="case-auth-alert__title">授权资源映射说明:</div>
<div>1、案例原文 -> 实操平台:实验案例原文</div>
<div>2、案例指导书 -> 实操平台:实验案例指导书;统一资源平台:教案</div>
<div>3、案例PPT -> 统一资源平台:课件</div>
<div>4、案例操作视频 -> 统一资源平台:视频;实操平台:实验操作视频</div>
<div>5、案例数据集 -> 统一资源平台:其他资料</div>
</div>
</el-alert>
<div v-if="selectedCount || submitting || progress" class="case-auth-progress">
<div class="case-auth-progress__header">
<span>{{ progressText || `准备处理 0/${selectedCount}` }}</span>
<span>{{ progress }}%</span>
</div>
<el-progress :percentage="progress" :show-text="false" />
</div>
<div v-loading="loading" class="case-list">
<div
v-for="item in list"
:key="item.id"
class="case-card"
:class="{ 'case-card--active': selectedCaseId === item.id }"
@click="handleCaseChange(item.id)">
<div class="case-card__top">
<div>
<div class="case-card__name">{{ item.name }}</div>
<div class="case-card__id">{{ item.id }}</div>
</div>
<el-radio :model-value="selectedCaseId === item.id" :label="true">选择</el-radio>
</div>
<div class="case-card__desc">{{ item.description || '暂无案例简介' }}</div>
<div class="case-card__meta">
<el-tag
v-for="section in caseFileSectionConfig"
:key="section.key"
size="small"
:type="getFilesByType(item.files, caseFileTypeMap[section.key]).length ? 'success' : 'info'">
{{ section.title }} {{ getFilesByType(item.files, caseFileTypeMap[section.key]).length }}
</el-tag>
</div>
</div>
</div>
<div v-if="selectedCase" class="case-auth-sections">
<div
v-for="section in caseFileSectionConfig"
:key="section.key"
class="case-auth-section"
:class="{ 'case-auth-section--disabled': !isTypeSelectable(section.key) }">
<div class="case-auth-section__header">
<div class="case-auth-section__title">{{ section.title }}</div>
<el-checkbox
:model-value="getAllChecked(section.key)"
:disabled="!groupedFiles[section.key].length || !isTypeSelectable(section.key)"
@update:model-value="(value) => handleToggleAll(section.key, !!value)">
全选
</el-checkbox>
</div>
<el-checkbox-group v-model="fileSelections[section.key]" class="case-auth-options">
<el-checkbox
v-for="file in groupedFiles[section.key]"
:key="getFileKey(file)"
:label="getFileKey(file)"
:disabled="!isTypeSelectable(section.key)">
{{ file.name }}
</el-checkbox>
</el-checkbox-group>
<el-empty v-if="!groupedFiles[section.key].length" :description="section.emptyText" :image-size="72" />
</div>
</div>
</AppCard>
</template>
<style lang="scss" scoped>
.case-auth-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 16px;
}
.case-auth-alert {
margin-bottom: 16px;
}
.case-auth-alert__title {
font-weight: 600;
}
.case-auth-alert__content {
display: flex;
flex-direction: column;
gap: 6px;
line-height: 1.6;
}
.case-auth-progress {
margin-bottom: 16px;
padding: 12px 16px;
background: var(--el-fill-color-light);
border-radius: 8px;
}
.case-auth-progress__header {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-regular);
}
.case-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.case-card {
padding: 16px;
border: 1px solid var(--el-border-color);
border-radius: 10px;
background: #fff;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.case-card:hover,
.case-card--active {
border-color: var(--el-color-primary);
box-shadow: 0 0 0 2px rgb(64 158 255 / 12%);
}
.case-card__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.case-card__name {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.case-card__id {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.case-card__desc {
min-height: 44px;
font-size: 14px;
line-height: 1.6;
color: var(--el-text-color-regular);
}
.case-card__meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.case-auth-sections {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
margin-top: 16px;
}
.case-auth-section {
padding: 16px;
background: var(--el-fill-color-light);
border-radius: 8px;
min-height: 180px;
}
.case-auth-section--disabled {
opacity: 0.65;
}
.case-auth-section__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.case-auth-section__title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.case-auth-options {
display: flex;
flex-direction: column;
gap: 10px;
}
@media (max-width: 1200px) {
.case-list,
.case-auth-sections {
grid-template-columns: 1fr;
}
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论