提交 868f99b4 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 教师端新增案例原文

上级 ae1a5049
import httpRequest from '@/utils/axios'
import type { CaseCreateItem } 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/lab/v1/teacher/experiment-cases/experiments', { params })
}
// 获取案例原文列表
export function getCaseList(params?: { name?: string; experiment_id?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/lab/v1/teacher/experiment-cases/list', { params })
}
// 获取案例原文详情
export function getCase(params: { id: string }) {
return httpRequest.get('/api/lab/v1/teacher/experiment-cases/detail', { params })
}
// 创建案例原文
export function createCase(data: CaseCreateItem) {
return httpRequest.post('/api/lab/v1/teacher/experiment-cases/create', data)
}
// 更新案例原文
export function updateCase(data: CaseCreateItem) {
return httpRequest.post('/api/lab/v1/teacher/experiment-cases/update', data)
}
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { CaseItem, CaseFormData, CaseCreateItem, CaseUpdateItem } from '../types'
import { ElMessageBox, ElMessage } from 'element-plus'
import AppUpload from '@/components/base/AppUpload.vue'
import { createCase, updateCase } from '../api'
import { useGetCourseList } from '../composables/useGetCourseList'
import { useGetExperimentList } from '../composables/useGetExperimentList'
import { useMapStore } from '@/stores/map'
interface Props {
data?: CaseItem | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
// 课程列表
const { courses } = useGetCourseList()
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
// 实验列表
const { experiments, updateExperiments } = useGetExperimentList()
const formRef = $ref<FormInstance>()
const form = reactive<CaseFormData>({
name: '',
course_id: '',
experiment_id: '',
status: '1',
url: '',
type: '',
size: '',
files: [],
protocol: false
})
watchEffect(() => {
if (!props.data) return
form.files = [{ name: props.data.name, url: props.data.url, type: props.data.type }]
Object.assign(form, { protocol: true, course_id: props.data.course.id }, props.data)
})
watchEffect(() => {
updateExperiments(form.course_id)
})
const checkProtocol = (rule: any, value: any, callback: any) => {
if (!value) {
return callback(new Error('请阅读并同意'))
} else {
callback()
}
}
const rules = ref<FormRules>({
files: [{ type: 'array', required: true, message: '请上传案例原文' }],
name: [{ required: true, message: '请输入案例原文名称', trigger: 'blur' }],
course_id: [{ required: true, message: '请选择关联实验课程', trigger: 'change' }],
experiment_id: [{ required: true, message: '请选择关联实验', trigger: 'change' }],
protocol: [{ validator: checkProtocol, message: '请阅读并同意', trigger: 'change' }]
})
const isUpdate = $computed(() => {
return !!props.data?.id
})
const title = $computed(() => {
return isUpdate ? '编辑案例原文' : '新增案例原文'
})
// 实验列表
const experimentList = $computed(() => {
if (!props.data) return experiments.value
return isUpdate && props.data.course.id === form.course_id
? [{ id: props.data.experiment_id, name: props.data.experiment.name }, ...experiments.value]
: experiments.value
})
function handleBeforeUpload() {
if (form.files.length) {
return ElMessageBox.confirm('系统仅支持1个案例原文,此操作将覆盖原有案例原文文件,确认上传新文件吗?', '提示')
}
return true
}
function handleUploadSuccess(file: any) {
form.name = file.name.split('.').shift()
form.size = (file.size / 1024 / 1024).toString()
form.url = file.raw.url
form.type = file.raw.type || 'unknown'
}
// 提交
function handleSubmit() {
formRef?.validate().then(() => {
isUpdate ? handleUpdate(form) : handleCreate(form)
})
}
// 新增
function handleCreate(params: CaseCreateItem) {
createCase(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
// 修改
function handleUpdate(params: CaseUpdateItem) {
updateCase(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="600px" @update:modelValue="$emit('update:modelValue')">
<el-form ref="formRef" :model="form" :rules="rules" label-width="124px">
<el-form-item label="案例原文文件" prop="files">
<AppUpload
v-model="form.files"
:limit="1"
:beforeUpload="handleBeforeUpload"
accept=".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.pdf,application/pdf,.ppt,.pptx,application/vnd.ms-powerpoint,.csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
@success="handleUploadSuccess">
<template #tip>案例原文文件支持格式包含:doc docx xls xlsx pdf ppt pptx,大小不超过50M</template>
</AppUpload>
</el-form-item>
<el-form-item label="案例原文名称" prop="name">
<el-input v-model="form.name"></el-input>
<p class="form-tips">案例原文名称自动取值于文件名称,可以进行二次修改。</p>
</el-form-item>
<el-form-item label="关联实验课程" prop="course_id">
<el-select v-model="form.course_id" filterable style="width: 100%" @change="form.experiment_id = ''">
<el-option v-for="item in courses" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="关联实验" prop="experiment_id">
<el-select v-model="form.experiment_id" filterable style="width: 100%">
<el-option v-for="item in experimentList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</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 prop="protocol">
<el-checkbox label="我已阅读并同意" v-model="form.protocol" />
<a
href="https://view.officeapps.live.com/op/view.aspx?src=https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/%E7%B4%AB%E8%8D%86%E6%95%99%E8%82%B2%E7%94%A8%E6%88%B7%E5%85%A5%E9%A9%BB%E5%8F%8A%E7%BD%91%E7%BB%9C%E6%95%99%E5%AD%A6%E8%B5%84%E6%BA%90%E5%8D%8F%E8%AE%AE(1).docx"
target="_blank"
>《紫荆教育用户入驻及网络教学资源协议》</a
>
</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>
<style lang="scss" scoped>
.form-tips {
font-size: 12px;
color: #999;
}
</style>
<script setup lang="ts">
import type { CaseItem } from '../types'
import { useMapStore } from '@/stores/map'
interface Props {
data: CaseItem
}
const props = defineProps<Props>()
const genFileClassNames = $computed(() => {
const extName = props.data.url?.split('.').pop() || ''
return {
'icon-file__word': extName.includes('doc'),
'icon-file__excel': extName.includes('xls'),
'icon-file__ppt': extName.includes('ppt'),
'icon-file__pdf': extName.includes('pdf')
}
})
const username = computed(() => {
const operator = props.data.created_operator
return operator.real_name || operator.nickname || operator.username
})
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
const statusName = computed(() => {
return status.find(item => item.value === props.data.status)?.label
})
const sizeName = computed(() => {
return props.data.size + 'MB'
})
</script>
<template>
<div class="list-item">
<div class="list-item-hd">
<div class="icon-file" :class="genFileClassNames"></div>
<div class="button-group">
<el-tooltip effect="dark" content="查看">
<div class="button icon-view" v-permission="'teacher-experiment-cases-detail'">
<router-link :to="`/admin/lab/case/${data.id}`" target="_blank"></router-link>
</div>
</el-tooltip>
<el-tooltip effect="dark" content="编辑">
<div
class="button icon-edit"
@click="$emit('clickEdit', data)"
v-permission="'teacher-experiment-cases-update'"></div>
</el-tooltip>
</div>
</div>
<div class="list-item-title">
<h2>{{ data.name }}</h2>
<h6>{{ statusName }}</h6>
</div>
<div class="list-item-main">
<ol>
<li>
<p>文件大小:{{ sizeName }}</p>
</li>
<li>
<p>观看次数:{{ data.pv }}</p>
</li>
<li>
<p>创建人:{{ username }}</p>
</li>
<li>
<p>创建时间:{{ data.created_time }}</p>
</li>
<li>
<p>更新时间:{{ data.updated_time }}</p>
</li>
</ol>
</div>
</div>
</template>
<style lang="scss" scoped>
.list-item {
position: relative;
padding: 30px;
background: linear-gradient(to bottom left, rgba(186, 20, 62, 0.1) 0%, rgba(255, 255, 255, 0.8) 60%);
border-radius: 12px;
&:hover {
box-shadow: 0 3px 18px rgba(0, 0, 0, 0.27);
}
.list-item-hd {
display: flex;
justify-content: space-between;
.button-group {
display: flex;
.button {
a {
display: block;
width: 100%;
height: 100%;
}
}
.button + .button {
margin-left: 12px;
}
}
}
.icon-file {
width: 30px;
height: 30px;
background: url(@/assets/images/icon_word.png) no-repeat;
background-size: contain;
}
.icon-file__word {
background: url(@/assets/images/icon_word.png) no-repeat;
background-size: contain;
}
.icon-file__excel {
background: url(@/assets/images/icon_excel.png) no-repeat;
background-size: contain;
}
.icon-file__ppt {
background: url(@/assets/images/icon_ppt.png) no-repeat;
background-size: contain;
}
.icon-file__pdf {
background: url(@/assets/images/icon_pdf.png) no-repeat;
background-size: contain;
}
.icon-edit {
width: 16px;
height: 16px;
background: url(@/assets/images/icon_edit.png) no-repeat;
background-size: contain;
cursor: pointer;
}
.icon-view {
width: 16px;
height: 16px;
background: url(@/assets/images/icon_view.png) no-repeat;
background-size: contain;
cursor: pointer;
}
.list-item-main {
line-height: 30px;
ol {
margin-left: 20px;
}
li {
margin-top: 24px;
font-size: 16px;
line-height: 1;
color: var(--main-color);
list-style: disc;
}
p {
color: #666;
}
}
.list-item-title {
margin: 26px 0 36px;
display: flex;
justify-content: space-between;
h2 {
flex: 1;
font-size: 18px;
font-family: Source Han Sans CN;
font-weight: bold;
line-height: 20px;
color: #333333;
}
h6 {
margin-left: 20px;
font-size: 16px;
font-family: Source Han Sans CN;
font-weight: 500;
line-height: 20px;
color: var(--main-color);
}
}
}
</style>
import { getCourseList } from '../api'
export interface CourseType {
id: string
name: string
}
const courses = ref<CourseType[]>([])
export function useGetCourseList() {
!courses.value.length &&
getCourseList().then((res: any) => {
courses.value = res.data
})
return { courses }
}
import { getExperimentList } from '../api'
export interface ExperimentType {
id: string
name: string
}
export function useGetExperimentList() {
const experiments = ref<ExperimentType[]>([])
function updateExperiments(courseId?: string) {
getExperimentList({ course_id: courseId }).then((res: any) => {
experiments.value = res.data
})
}
return { experiments, updateExperiments }
}
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/lab',
redirect: '/admin/lab/case'
},
{
path: '/admin/lab/case',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: ':id', component: () => import('./views/View.vue'), props: true }
]
}
]
export interface CaseItem {
created_operator: CaseOperator
created_time: string
delete_time: string
course: {
id: string
name: string
}
experiment: {
id: string
name: string
}
experiment_id: string
id: string
name: string
pv: string
size: string
status: string
type: string
updated_operator: CaseOperator
updated_time: string
url: string
}
export interface CaseOperator {
avatar: string
id: string
nickname: string
real_name: string
username: string
}
export interface CaseFormData {
id?: string
experiment_id: string
course_id: string
status: string
name: string
type: string
url: string
size: string
protocol: boolean
files: FileItem[]
}
export type CaseCreateItem = Pick<CaseFormData, 'experiment_id' | 'status' | 'name' | 'type' | 'url' | 'size'>
export type CaseUpdateItem = CaseCreateItem & { id?: string }
export interface FileItem {
name: string
url: string
raw?: File
type?: string
size?: number
}
<script setup lang="ts">
import type { CaseItem } from '../types'
import { CirclePlus } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import ListItem from '../components/ListItem.vue'
import { getCaseList } from '../api'
import { useGetExperimentList } from '../composables/useGetExperimentList'
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
// 实验列表
const { experiments, updateExperiments } = useGetExperimentList()
updateExperiments()
const route = useRoute()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getCaseList,
params: { name: '', experiment_id: route.query.experiment_id || '' }
},
filters: [
{ type: 'input', prop: 'name', label: '案例原文名称', placeholder: '请输入案例原文名称' },
{
type: 'select',
prop: 'experiment_id',
label: '关联实验',
options: experiments.value,
labelKey: 'name',
valueKey: 'id',
placeholder: '请选择关联实验'
}
],
columns: [{ label: '名称', prop: 'name' }]
}
})
let dialogVisible = $ref(false)
const rowData = ref<CaseItem | undefined | null>(null)
// 新增
function handleAdd() {
rowData.value = null
dialogVisible = true
}
// 编辑
function handleUpdate(row: CaseItem) {
rowData.value = row
dialogVisible = true
}
function onUpdateSuccess() {
appList?.refetch()
}
</script>
<template>
<AppCard title="案例原文管理" :hasBodyBackground="false">
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" :icon="CirclePlus" @click="handleAdd" v-permission="'teacher-experiment-cases-create'"
>新增案例原文</el-button
>
</template>
<template #body="{ data }: { data: CaseItem[] }">
<div class="list-card" v-if="data.length">
<ListItem v-for="item in data" :data="item" :key="item.id" @clickEdit="handleUpdate(item)"></ListItem>
</div>
<el-empty description="暂无数据" v-else />
</template>
</AppList>
</AppCard>
<FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
</template>
<style lang="scss" scoped>
:deep(.table-list-filter) {
padding: 30px 30px 10px;
background: #fff;
border-radius: 12px;
}
.list-card {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
</style>
<script setup lang="ts">
import type { CaseItem } from '../types'
import Preview from '@/components/Preview.vue'
import { useMapStore } from '@/stores/map'
import { getCase } from '../api'
interface Props {
id: string
}
const props = defineProps<Props>()
let detail = $ref<CaseItem | null>(null)
const username = computed(() => {
if (!detail) return ''
const operator = detail.created_operator
return operator.real_name || operator.nickname || operator.username
})
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
const statusName = computed(() => {
return status.find(item => item.value === detail?.status)?.label
})
const sizeName = computed(() => {
return detail?.size + 'MB'
})
provide('detail', $$(detail))
function fetchInfo() {
if (!props.id) return
getCase({ id: props.id }).then(res => {
detail = res.data.item
})
}
onMounted(() => {
fetchInfo()
})
</script>
<template>
<AppCard title="查看案例原文">
<template v-if="detail">
<el-descriptions>
<el-descriptions-item label="案例原文名称:">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="文件大小:">{{ sizeName }}</el-descriptions-item>
<el-descriptions-item label="观看次数:">{{ detail.pv }}</el-descriptions-item>
<el-descriptions-item label="关联实验课程:">{{ detail.course.name }}</el-descriptions-item>
<el-descriptions-item label="关联实验:">{{ detail.experiment.name }}</el-descriptions-item>
<el-descriptions-item label="有效状态:">{{ statusName }}</el-descriptions-item>
<el-descriptions-item label="创建人:">{{ username }}</el-descriptions-item>
<el-descriptions-item label="创建时间:">{{ detail.created_time }}</el-descriptions-item>
</el-descriptions>
<div style="height: 80vh">
<Preview :url="detail.url"></Preview>
</div>
</template>
</AppCard>
</template>
......@@ -46,6 +46,11 @@ const adminMenus: IMenuItem[] = [
path: '/admin/lab/experiment',
tag: 'v1-backend-experiment'
},
{
name: '案例原文管理',
path: '/admin/lab/case',
tag: 'teacher-experiment-cases'
},
{
name: '实验指导书管理',
path: '/admin/lab/book',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论