提交 8c25052d authored 作者: 王鹏飞's avatar 王鹏飞

Merge branch 'master' into 202412

...@@ -25,7 +25,7 @@ const props = withDefaults( ...@@ -25,7 +25,7 @@ const props = withDefaults(
limit: 10, limit: 10,
data() { data() {
return [] return []
} },
} }
) )
...@@ -38,7 +38,7 @@ const page = reactive({ total: 0, size: props.limit, currentPage: 1 }) ...@@ -38,7 +38,7 @@ const page = reactive({ total: 0, size: props.limit, currentPage: 1 })
const params = reactive({ ...props.remote?.params }) const params = reactive({ ...props.remote?.params })
watch( watch(
() => props.data, () => props.data,
list => { (list) => {
dataList.value = list || [] dataList.value = list || []
}, },
{ immediate: true } { immediate: true }
...@@ -147,8 +147,7 @@ defineExpose({ refetch, tableRef, params, loading }) ...@@ -147,8 +147,7 @@ defineExpose({ refetch, tableRef, params, loading })
clearable clearable
@change="search" @change="search"
style="width: 200px" style="width: 200px"
v-if="item.type === 'input'" v-if="item.type === 'input'" />
/>
<!-- select --> <!-- select -->
<el-select <el-select
v-model="params[item.prop]" v-model="params[item.prop]"
...@@ -156,14 +155,12 @@ defineExpose({ refetch, tableRef, params, loading }) ...@@ -156,14 +155,12 @@ defineExpose({ refetch, tableRef, params, loading })
clearable clearable
@change="search" @change="search"
v-if="item.type === 'select'" v-if="item.type === 'select'"
style="width: 200px" style="width: 200px">
>
<el-option <el-option
:label="option[item.labelKey] || option.label" :label="option[item.labelKey] || option.label"
:value="option[item.valueKey] || option.value" :value="option[item.valueKey] || option.value"
v-for="(option, index) in item.options" v-for="(option, index) in item.options"
:key="index" :key="index" />
/>
</el-select> </el-select>
</template> </template>
</el-form-item> </el-form-item>
...@@ -186,8 +183,7 @@ defineExpose({ refetch, tableRef, params, loading }) ...@@ -186,8 +183,7 @@ defineExpose({ refetch, tableRef, params, loading })
v-bind="$attrs" v-bind="$attrs"
style="height: 100%" style="height: 100%"
ref="tableRef" ref="tableRef"
:header-cell-style="{ background: '#EFEFEF' }" :header-cell-style="{ background: '#EFEFEF' }">
>
<el-table-column v-bind="item || {}" v-for="item in columns" :key="item.prop"> <el-table-column v-bind="item || {}" v-for="item in columns" :key="item.prop">
<template #default="scope" v-if="item.slots || item.computed"> <template #default="scope" v-if="item.slots || item.computed">
<slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot> <slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot>
...@@ -206,15 +202,14 @@ defineExpose({ refetch, tableRef, params, loading }) ...@@ -206,15 +202,14 @@ defineExpose({ refetch, tableRef, params, loading })
class="table-list-pagination" class="table-list-pagination"
background background
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 30, 50, 100]" :page-sizes="[10, 20, 30, 50]"
:page-size="page.size" :page-size="page.size"
:total="page.total" :total="page.total"
v-model:currentPage="page.currentPage" v-model:currentPage="page.currentPage"
@size-change="pageSizeChange" @size-change="pageSizeChange"
@current-change="fetchList()" @current-change="fetchList()"
:hide-on-single-page="true" :hide-on-single-page="true"
v-if="hasPagination" v-if="hasPagination">
>
</el-pagination> </el-pagination>
</div> </div>
</div> </div>
......
...@@ -4,12 +4,16 @@ import httpRequest from '@/utils/axios' ...@@ -4,12 +4,16 @@ import httpRequest from '@/utils/axios'
export function getStudentList(params?: { name?: string; organ_id?: string; page?: string; 'per-page'?: string }) { export function getStudentList(params?: { name?: string; organ_id?: string; page?: string; 'per-page'?: string }) {
return httpRequest.get('/api/resource/v1/learning/student/list', { params }) return httpRequest.get('/api/resource/v1/learning/student/list', { params })
} }
// // 导入学生 // 导入学生
export function importStudent(data: { file: any }) { export function importStudent(data: { url: string; name: string; size: number }) {
return httpRequest.post('/api/resource/v1/learning/student/import', data, { return httpRequest.post('/api/resource/v1/learning/student/import', data)
headers: { 'Content-Type': 'multipart/form-data' }
})
} }
// 批量修改学生
export function batchUpdateStudent(data: { url: string; name: string; size: number }) {
return httpRequest.post('/api/resource/v1/learning/student/batch-update', data)
}
// 导出学生 // 导出学生
export function exportStudent(params: { name: string; organ_id: string }) { export function exportStudent(params: { name: string; organ_id: string }) {
return httpRequest.get('/api/resource/v1/learning/student/download', { params, responseType: 'blob' }) return httpRequest.get('/api/resource/v1/learning/student/download', { params, responseType: 'blob' })
...@@ -50,10 +54,25 @@ export function getStuDetail(params?: { id: string }) { ...@@ -50,10 +54,25 @@ export function getStuDetail(params?: { id: string }) {
return httpRequest.get('/api/resource/v1/learning/student/view', { params }) return httpRequest.get('/api/resource/v1/learning/student/view', { params })
} }
// 班级搜索 // 班级搜索
export function getClassList(params?: { specialty_id: string; organ_id: string; page?: string; 'per-page'?: string }) { export function searchClass(params?: { specialty_id: string; organ_id: string; page?: string; 'per-page'?: string }) {
return httpRequest.get('/api/resource/v1/learning/student/search-class', { params }) return httpRequest.get('/api/resource/v1/learning/student/search-class', { params })
} }
// 获取专业列表 // 获取专业列表
export function getProList(params?: { name?: string; page?: string; 'per-page'?: string }) { export function getProList(params?: { name?: string; page?: string; 'per-page'?: string; sort?: string }) {
return httpRequest.get('/api/resource/v1/backend/specialty/list', { params }) return httpRequest.get('/api/resource/v1/backend/specialty/list', { params })
} }
// 获取班级列表
export function getClassList(params?: {
specialty_id: string
organ_id: string
page?: string
'per-page'?: string
sort?: string
}) {
return httpRequest.get('/api/resource/v1/learning/class/list', { params })
}
// 学生导入进度查询
export function getStudentProgress(params: { id: string }) {
return httpRequest.get('/api/resource/v1/learning/student/progress', { params })
}
...@@ -4,7 +4,7 @@ import { ElMessage } from 'element-plus' ...@@ -4,7 +4,7 @@ import { ElMessage } from 'element-plus'
import { useProjectList } from '@/composables/useGetProjectList' import { useProjectList } from '@/composables/useGetProjectList'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { addStudent, updateStudent, getStuDetail, getClassList, getProList } from '../api' import { addStudent, updateStudent, getStuDetail, searchClass, getProList } from '../api'
const store = useMapStore() const store = useMapStore()
const userStore = useUserStore() const userStore = useUserStore()
...@@ -112,7 +112,7 @@ if (userStore.roles[0].name !== '超级管理员') { ...@@ -112,7 +112,7 @@ if (userStore.roles[0].name !== '超级管理员') {
} }
// 获取班级列表 // 获取班级列表
const handleClassList = () => { const handleClassList = () => {
getClassList({ searchClass({
specialty_id: form.specialty_id, specialty_id: form.specialty_id,
organ_id: form.organ_id || userStore.organization?.id, organ_id: form.organ_id || userStore.organization?.id,
'per-page': '100' 'per-page': '100'
......
<script lang="ts" setup> <script lang="ts" setup>
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue' import { UploadFilled, Loading } from '@element-plus/icons-vue'
import { splitStrLast } from '@/utils/util' import { splitStrLast } from '@/utils/util'
import { importStudent } from '../api' import { importStudent, getStudentProgress } from '../api'
import { upload } from '@/utils/upload'
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
const upload = ref() const uploadRef = ref()
const fileList = ref([]) // 文件列表 const fileList = ref([]) // 文件列表
defineProps({ defineProps({
isShowImportDialog: { isShowImportDialog: {
type: Boolean type: Boolean,
} },
}) })
interface Emits { interface Emits {
(e: 'update:isShowImportDialog', isShowImportDialog: boolean): void (e: 'update:isShowImportDialog', isShowImportDialog: boolean): void
...@@ -31,26 +33,64 @@ const beforeUpload = (file: any) => { ...@@ -31,26 +33,64 @@ const beforeUpload = (file: any) => {
return true return true
} }
} }
const fetchFileUpload = (option: any) => { const loading = ref(false)
return new Promise(() => {
importStudent({ file: option.file }).then(() => { const progress = reactive({
ElMessage.success('导入数据成功') progress: '-',
emit('update:isShowImportDialog', false) total: '-',
message: '导入中',
status: 2,
})
let timer: any = null
const fetchFileUpload = async (option: any) => {
const file = option.file
const url = await upload(file)
const res = await importStudent({ url, name: file.name, size: file.size })
const id = res.data.id
loading.value = true
timer && clearInterval(timer)
timer = setInterval(() => {
getStudentProgress({ id })
.then((res) => {
const data = res.data
Object.assign(progress, data)
if (data.status === 3 || data.status === 4) {
clearInterval(timer)
loading.value = false
ElMessage({
type: data.status === 3 ? 'success' : 'error',
message: data.message,
})
emit('create') emit('create')
}
}) })
.catch(() => {
clearInterval(timer)
loading.value = false
}) })
}, 1000)
} }
onUnmounted(() => {
timer && clearInterval(timer)
})
const handleSubmitUpload = () => { const handleSubmitUpload = () => {
upload.value.submit() uploadRef.value.submit()
} }
</script> </script>
<template> <template>
<el-dialog :model-value="isShowImportDialog" draggable :before-close="handleCancel" title="批量导入学生" width="30%"> <el-dialog
draggable
:model-value="isShowImportDialog"
:close-on-click-modal="false"
:before-close="handleCancel"
title="批量导入学生"
width="500px"
custom-class="my-dialog">
<el-upload <el-upload
style="text-align: center" style="text-align: center"
class="file-import" class="file-import"
ref="upload" ref="uploadRef"
action="#" action="#"
accept=".xls,.xlsx" accept=".xls,.xlsx"
drag drag
...@@ -62,14 +102,18 @@ const handleSubmitUpload = () => { ...@@ -62,14 +102,18 @@ const handleSubmitUpload = () => {
<el-icon class="el-icon--upload"><upload-filled /></el-icon> <el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖至此处,点击上传</div> <div class="el-upload__text">将文件拖至此处,点击上传</div>
</el-upload> </el-upload>
<div style="margin-bottom: 10px; text-align: center"> <div style="margin-bottom: 10px; text-align: right">
导入模板下载:<a <a
href="/center_resource/%E5%AD%A6%E7%94%9F%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx" href="/center_resource/%E5%AD%A6%E7%94%9F%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx"
download="学生模板" download="批量导入学生模板">
><el-link type="primary">学生模板.xlsx</el-link></a <el-link type="primary">下载模板</el-link>
> </a>
</div>
<div class="dialog-loading" v-if="loading">
<el-icon class="is-loading"><Loading /></el-icon>
<p>批量导入中</p>
<p>{{ progress.progress }}/{{ progress.total }}</p>
</div> </div>
<template #footer> <template #footer>
<span> <span>
<el-button @click="handleCancel">取消</el-button> <el-button @click="handleCancel">取消</el-button>
...@@ -78,3 +122,24 @@ const handleSubmitUpload = () => { ...@@ -78,3 +122,24 @@ const handleSubmitUpload = () => {
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<style lang="scss">
.my-dialog {
position: relative;
}
.dialog-loading {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 16px;
line-height: 30px;
color: #fff;
}
</style>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { UploadFilled, Loading } from '@element-plus/icons-vue'
import { splitStrLast } from '@/utils/util'
import { batchUpdateStudent, getStudentProgress } from '../api'
import { upload } from '@/utils/upload'
const emit = defineEmits<Emits>()
const uploadRef = ref()
const fileList = ref([]) // 文件列表
defineProps({
modelValue: { type: Boolean },
})
interface Emits {
(e: 'update:modelValue', modelValue: boolean): void
(e: 'create'): void
}
// 取消
const handleCancel = () => {
emit('update:modelValue', false)
}
const beforeUpload = (file: any) => {
const suffix = splitStrLast(file.name, '.')
if (!['xlsx', 'xls'].includes(suffix)) {
ElMessage.warning('只能上传excel文件')
return false
} else {
return true
}
}
const loading = ref(false)
const progress = reactive({
progress: '-',
total: '-',
message: '导入中',
status: 2,
})
let timer: any = null
const fetchFileUpload = async (option: any) => {
const file = option.file
const url = await upload(file)
const res = await batchUpdateStudent({ url, name: file.name, size: file.size })
const id = res.data.id
loading.value = true
timer && clearInterval(timer)
timer = setInterval(() => {
getStudentProgress({ id })
.then((res) => {
const data = res.data
Object.assign(progress, data)
if (data.status === 3 || data.status === 4) {
clearInterval(timer)
loading.value = false
ElMessage({
type: data.status === 3 ? 'success' : 'error',
message: data.message,
})
emit('create')
}
})
.catch(() => {
clearInterval(timer)
loading.value = false
})
}, 1000)
}
const handleSubmitUpload = () => {
uploadRef.value.submit()
}
</script>
<template>
<el-dialog
draggable
:model-value="modelValue"
:close-on-click-modal="false"
:before-close="handleCancel"
title="批量修改学生"
width="500px">
<el-upload
style="text-align: center"
class="file-import"
ref="uploadRef"
action="#"
accept=".xls,.xlsx"
drag
:auto-upload="false"
:file-list="fileList"
:limit="1"
:before-upload="beforeUpload"
:http-request="fetchFileUpload">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖至此处,点击上传</div>
</el-upload>
<div style="margin-bottom: 10px; text-align: right">
<a href="/center_resource/%E5%AD%A6%E7%94%9F%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx" download="批量修改学生模板"
><el-link type="primary">下载模板</el-link></a
>
</div>
<div class="dialog-loading" v-if="loading">
<el-icon class="is-loading"><Loading /></el-icon>
<p>批量修改中</p>
<p>{{ progress.progress }}/{{ progress.total }}</p>
</div>
<template #footer>
<span>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmitUpload">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="scss">
.my-dialog {
position: relative;
}
.dialog-loading {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 16px;
line-height: 30px;
color: #fff;
}
</style>
import { getClassList, getProList } from '../api'
// 专业列表
export function useSpecialtyList() {
const items = ref([])
const params = reactive({ name: '' })
const fetchList = async () => {
const { data } = await getProList({ ...params, 'per-page': '100', sort: 'name' })
items.value = data.list
}
onMounted(fetchList)
return { items, params, fetchList }
}
// 班级列表
export function useClassList() {
const items = ref([])
const params = reactive({ organ_id: '', specialty_id: '' })
const fetchList = async () => {
const { data } = await getClassList({ ...params, 'per-page': '100', sort: 'name' })
items.value = data.list
}
watch(params, fetchList)
onMounted(fetchList)
return { items, params, fetchList }
}
...@@ -3,48 +3,91 @@ import { ElMessage } from 'element-plus' ...@@ -3,48 +3,91 @@ import { ElMessage } from 'element-plus'
import { useProjectList } from '@/composables/useGetProjectList' import { useProjectList } from '@/composables/useGetProjectList'
import AddStudent from '../components/AddStudent.vue' import AddStudent from '../components/AddStudent.vue'
import ImportStudent from '../components/ImportStudent.vue' import ImportStudent from '../components/ImportStudent.vue'
import UpdateStudent from '../components/UpdateStudent.vue'
// import SourceAnalysis from '../components/SourceAnalysis.vue' // import SourceAnalysis from '../components/SourceAnalysis.vue'
import { getStudentList, exportStudent, updateStudent } from '../api' import { getStudentList, exportStudent, updateStudent } from '../api'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useClassList, useSpecialtyList } from '../composables/useData'
// 判断当前用户是不是超级管理员 // 判断当前用户是不是超级管理员
const user = useUserStore().roles const user = useUserStore().roles
const isAdmin = !!user.find((item: any) => item.name === '超级管理员') const isAdmin = !!user.find((item: any) => item.name === '超级管理员')
const departmentList: any = useProjectList('', '79806610719731712').departmentList const { departmentList } = useProjectList('', '79806610719731712')
const appList = ref() const appList = ref()
const id = ref('') const id = ref('')
const title = ref('') const title = ref('')
const isEdit = ref('') const isEdit = ref('')
const isShowAddDialog = ref(false) const isShowAddDialog = ref(false)
const isShowImportDialog = ref(false) const isShowImportDialog = ref(false)
// 专业
const { items: specialtyList } = useSpecialtyList()
// 班级
let { items: classList, params } = useClassList()
// const isShowAnalysisDialog = ref(false) // const isShowAnalysisDialog = ref(false)
const listOptions = { const listOptions = computed(() => {
remote: { httpRequest: getStudentList, params: { name: '', organ_id: '' } }, return {
remote: {
httpRequest: getStudentList,
params: { name: '', organ_id: '', mobile: '', specialty_id: '', class_id: '' },
beforeRequest: (requestPrams: any) => {
params.organ_id = requestPrams.organ_id
params.specialty_id = requestPrams.specialty_id
return requestPrams
},
},
filters: [ filters: [
{
type: 'select',
prop: 'organ_id',
label: '所属部门/学校:',
options: departmentList.value,
labelKey: 'name',
valueKey: 'id',
},
{
type: 'select',
prop: 'specialty_id',
label: '所属专业:',
options: specialtyList.value,
labelKey: 'name',
valueKey: 'id',
},
{
type: 'select',
prop: 'class_id',
label: '所属班级:',
options: classList.value,
labelKey: 'name',
valueKey: 'id',
},
{ type: 'input', prop: 'name', label: '学生姓名:', placeholder: '学生姓名' }, { type: 'input', prop: 'name', label: '学生姓名:', placeholder: '学生姓名' },
{ type: 'select', prop: 'organ_id', slots: 'filter-department' } { type: 'input', prop: 'mobile', label: '学生电话:', placeholder: '学生电话' },
], ],
columns: [ columns: [
{ type: 'selection' }, // { type: 'selection' },
{ label: '序号', type: 'index', align: 'center' }, { label: '序号', type: 'index', align: 'center' },
{ label: '学号', prop: 'sno_number', align: 'center', minWidth: '200' }, { label: '学号', prop: 'sno_number', align: 'center', minWidth: '200' },
{ label: '姓名', prop: 'name', align: 'center', minWidth: '100' }, { label: '姓名', prop: 'name', align: 'center', minWidth: '100' },
{ label: '性别', prop: 'gender_name', align: 'center' }, { label: '性别', prop: 'gender_name', align: 'center' },
{ label: '出生年月', prop: 'birthday', align: 'center', minWidth: '200' },
{ label: '省', prop: 'province_name', align: 'center' },
{ label: '市', prop: 'city_name', align: 'center' },
{ label: '县', prop: 'county_name', align: 'center' },
{ label: '联系电话', prop: 'mobile', align: 'center', minWidth: '200' }, { label: '联系电话', prop: 'mobile', align: 'center', minWidth: '200' },
{ label: '部门/学校', prop: 'organ_id_name', align: 'center', minWidth: '200' }, { label: '部门/学校', prop: 'organ_id_name', align: 'center', minWidth: '200' },
{ label: '专业', prop: 'specialty_id_name', align: 'center', minWidth: '200' }, { label: '专业', prop: 'specialty_id_name', align: 'center', minWidth: '200' },
{ label: '班级', prop: 'class_id_name', align: 'center', minWidth: '200' }, { label: '班级', prop: 'class_id_name', align: 'center', minWidth: '200' },
{ label: '身份证号', prop: 'id_number', align: 'center', minWidth: '200' }, { label: '身份证号', prop: 'id_number', align: 'center', minWidth: '200' },
{ label: '出生年月', prop: 'birthday', align: 'center', minWidth: '200' },
{ label: '省', prop: 'province_name', align: 'center' },
{ label: '市', prop: 'city_name', align: 'center' },
{ label: '县', prop: 'county_name', align: 'center' },
{ label: '生效状态', slots: 'status', align: 'center' }, { label: '生效状态', slots: 'status', align: 'center' },
{ label: '更新时间', prop: 'updated_time', align: 'center', minWidth: '200' }, { label: '更新时间', prop: 'updated_time', align: 'center', minWidth: '200' },
{ label: '操作', slots: 'table-operate', align: 'center', minWidth: '200', fixed: 'right' } { label: '操作', slots: 'table-operate', align: 'center', minWidth: '200', fixed: 'right' },
] ],
} }
})
// 刷新页面 // 刷新页面
const handleRefresh = () => { const handleRefresh = () => {
...@@ -83,20 +126,25 @@ const handleChangeStatus = (row: any) => { ...@@ -83,20 +126,25 @@ const handleChangeStatus = (row: any) => {
const handleImport = () => { const handleImport = () => {
isShowImportDialog.value = true isShowImportDialog.value = true
} }
// 批量修改
const updateDialogVisible = ref(false)
const handleUpdate = () => {
updateDialogVisible.value = true
}
// 导出 // 导出
const handleExport = () => { const handleExport = () => {
const params = Object.assign({}, appList.value?.params) const params = Object.assign({}, appList.value?.params)
exportStudent(params).then((r: any) => { exportStudent(params).then((r: any) => {
const blob = new Blob([r], { type: 'application/vnd.ms-excel' }) const blob = new Blob([r], { type: 'application/vnd.ms-excel' })
if ('download' in document.createElement('a')) { if ('download' in document.createElement('a')) {
const elink = document.createElement('a') const link = document.createElement('a')
elink.download = '学员数据.xlsx' link.download = '学员数据.xlsx'
elink.style.display = 'none' link.style.display = 'none'
elink.href = URL.createObjectURL(blob) link.href = URL.createObjectURL(blob)
document.body.appendChild(elink) document.body.appendChild(link)
elink.click() link.click()
URL.revokeObjectURL(elink.href) URL.revokeObjectURL(link.href)
document.body.removeChild(elink) document.body.removeChild(link)
} }
}) })
} }
...@@ -115,17 +163,26 @@ const handleAnalysis = () => { ...@@ -115,17 +163,26 @@ const handleAnalysis = () => {
<template> <template>
<AppCard title="学生管理"> <AppCard title="学生管理">
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange" border stripe style="margin-top: 30px"> <AppList
<el-button type="primary" round @click="handleAddStudent" v-permission="'v1-learning-student-create'">新增学生</el-button> v-bind="listOptions"
<el-button type="primary" round @click="handleImport" v-permission="'v1-learning-student-import'">批量导入</el-button> ref="appList"
<el-button type="primary" round @click="handleExport" v-permission="'v1-learning-student-download'">导出</el-button> @selection-change="handleSelectionChange"
border
stripe
style="margin-top: 30px">
<el-button type="primary" round @click="handleAddStudent" v-permission="'v1-learning-student-create'"
>新增学生</el-button
>
<el-button type="primary" round @click="handleImport" v-permission="'v1-learning-student-import'"
>批量导入</el-button
>
<el-button type="primary" round @click="handleExport" v-permission="'v1-learning-student-download'"
>批量导出</el-button
>
<el-button type="primary" round @click="handleUpdate" v-permission="'v1-learning-student-import'"
>批量修改</el-button
>
<el-button type="primary" round @click="handleAnalysis">生源地分析</el-button> <el-button type="primary" round @click="handleAnalysis">生源地分析</el-button>
<template v-if="isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属部门/学校:</div>
<el-select @change="handleRefresh" clearable v-model="params.organ_id" placeholder="请选择所属部门/学校">
<el-option v-for="item in departmentList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<template #status="{ row }"> <template #status="{ row }">
<el-switch <el-switch
size="large" size="large"
...@@ -139,16 +196,28 @@ const handleAnalysis = () => { ...@@ -139,16 +196,28 @@ const handleAnalysis = () => {
</template> </template>
<template #table-operate="{ row }"> <template #table-operate="{ row }">
<el-space> <el-space>
<el-link type="primary" plain @click="handleDetail(row)" v-permission="'v1-learning-student-view'">查看</el-link> <el-link type="primary" plain @click="handleDetail(row)" v-permission="'v1-learning-student-view'"
<el-link type="primary" plain @click="handleEdit(row)" v-permission="'v1-learning-student-update'">编辑</el-link> >查看</el-link
>
<el-link type="primary" plain @click="handleEdit(row)" v-permission="'v1-learning-student-update'"
>编辑</el-link
>
</el-space> </el-space>
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
<!-- 新增学生 --> <!-- 新增学生 -->
<AddStudent v-if="isShowAddDialog === true" v-model:isShowAddDialog="isShowAddDialog" :id="id" :title="title" :isEdit="isEdit" @create="handleRefresh" /> <AddStudent
v-if="isShowAddDialog === true"
v-model:isShowAddDialog="isShowAddDialog"
:id="id"
:title="title"
:isEdit="isEdit"
@create="handleRefresh" />
<!-- 导入学生 --> <!-- 导入学生 -->
<ImportStudent v-if="isShowImportDialog" v-model:isShowImportDialog="isShowImportDialog" @create="handleRefresh" /> <ImportStudent v-if="isShowImportDialog" v-model:isShowImportDialog="isShowImportDialog" @create="handleRefresh" />
<!-- 导入学生 -->
<UpdateStudent v-if="updateDialogVisible" v-model="updateDialogVisible" @create="handleRefresh" />
<!-- 生源地分析 --> <!-- 生源地分析 -->
<!-- <SourceAnalysis v-if="isShowAnalysisDialog" v-model:isShowAnalysisDialog="isShowAnalysisDialog" /> --> <!-- <SourceAnalysis v-if="isShowAnalysisDialog" v-model:isShowAnalysisDialog="isShowAnalysisDialog" /> -->
</template> </template>
...@@ -238,3 +238,30 @@ export function setDownload(data: { course_id: string; information_id: string; c ...@@ -238,3 +238,30 @@ export function setDownload(data: { course_id: string; information_id: string; c
export function searchAssistant(params: { name: string; page?: string; 'per-page'?: string }) { export function searchAssistant(params: { name: string; page?: string; 'per-page'?: string }) {
return httpRequest.get('/api/resource/v1/course/course/search-teacher', { params }) return httpRequest.get('/api/resource/v1/course/course/search-teacher', { params })
} }
// 获取教师书籍列表
export function getBookList(data: { tel: string; name?: string; }) {
return httpRequest.post('/api/ebook/open/teacher/book/getList', data)
}
// 获取教师书籍详情
export function getBookDetail(data: { id: string; }) {
return httpRequest.post('/api/ebook/open/teacher/book/getInfoById', data)
}
// 获取教师书籍详情
export function getChapter(data: { book_id: string; }) {
return httpRequest.post('/api/ebook/open/teacher/chapter/getAllList', data)
}
// 获取知识图谱
export function getTagList(params: { type: string; parent_id: string; chapter_id?: string }) {
return httpRequest.get('/api/resource/v1/course/course/tag-list', { params })
}
// 关联知识图谱
export function addTagCourse(data: { tag_id: string; course_id: string; chapter_id: string; }) {
return httpRequest.post('/api/resource/v1/course/course/tag-add-course', data)
}
// /v1/course/course/tag-add-course
...@@ -105,7 +105,8 @@ const imgUrl = (val: any) => { ...@@ -105,7 +105,8 @@ const imgUrl = (val: any) => {
:chapterID="''" :chapterID="''"
:course_id="'0'" :course_id="'0'"
:btnInfo="{ btn_name: '课程资料', resource_type: '4' }" :btnInfo="{ btn_name: '课程资料', resource_type: '4' }"
@create="handleAdd" /> @create="handleAdd"
/>
</div> </div>
</template> </template>
<style> <style>
......
<script setup lang="ts">
import AddTextbookListDialog from '../stepTwoComponents/AddTextbookListDialog.vue'
const emit = defineEmits<Emits>()
const dataList: any = ref([])
const isShowAddDialog = ref(false)
interface Emits {
(e: 'information', dataList: any): void
}
const props = defineProps({
data: {
type: Array,
required: true
},
id: {
type: String
}
})
watch(
() => props.data,
value => {
if (value?.length > 0) {
dataList.value = [...props.data]
} else {
dataList.value = []
}
},
{ immediate: true }
)
watch(
() => dataList.value,
value => {
if (value?.length > 0) {
emit('information', dataList.value)
}
}
)
const handleAddData = () => {
isShowAddDialog.value = true
}
const handleAdd = (val: any) => {
dataList.value = [val]
emit('information', dataList.value)
}
const listOptions = $computed(() => {
return {
columns: [
{ label: '数字教材名称', prop: 'name' },
{
label: '封面',
prop: 'img',
computed(row: any) {
return `<img src="${row.row.img}" style="height:100px;display:block">`
}
},
{ label: '作者', prop: 'authors' },
// { label: '发布者', prop: 'user.real_name' },
// { label: '版号', prop: 'name' },
{ label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' }
],
data: dataList.value.length > 0 ? dataList.value : []
}
})
const handleDel = (row: any) => {
const index = dataList.value.findIndex((item: any) => item.id === row.id)
dataList.value.splice(index, 1)
emit('information', dataList.value)
}
const handleView = function (row: any) {
window.open(`https://zijingebook.ezijing.com/student/book?book_id=${row.id}`)
}
</script>
<template>
<div>
<el-button type="primary" @click="handleAddData">添加数字教材</el-button>
<AppList v-bind="listOptions" ref="appList">
<template #table-operate="{ row }">
<el-button plain @click="handleView(row)">查阅</el-button>
<el-button type="primary" plain @click="handleDel(row)">删除</el-button>
</template>
</AppList>
<!-- 添加资源 -->
<AddTextbookListDialog
v-if="isShowAddDialog === true"
v-model:isShowAddDialog="isShowAddDialog"
@create="handleAdd"
/>
</div>
</template>
<style>
.node_type {
background: #bf9d6b;
border-radius: 20px;
line-height: 1;
font-size: 12px;
font-weight: 400;
color: #ffffff;
text-align: center;
padding: 4px 8px;
margin-right: 14px;
}
</style>
<script setup lang="ts">
import { getTagList, addTagCourse } from '../../api'
import { Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const route = useRoute()
const emit = defineEmits<Emits>()
const props = defineProps({
isShowTextbookDialog2: {
type: Boolean,
required: true
},
course_id: {
type: String,
required: false
},
chapterName: {
type: String,
required: false
},
chapterID: {
type: String,
required: false
}
})
interface Emits {
(e: 'update:isShowTextbookDialog2', isShowVideoDialog2: boolean): void
(e: 'create', val: any): void
}
// 取消
const handleCancel = () => {
emit('update:isShowTextbookDialog2', false)
}
const chapterList = ref([])
onMounted(() => {
getTagList({ type: '1', parent_id: route.query.id as string }).then(res => {
chapterList.value = res.data.reduce((a: any, b: any) => {
b.index = '0'
if (b.children) {
b.children.map((m: any) => {
m.index = '1'
})
}
a.push(b)
return a
}, [])
console.log(chapterList.value, 'chapterList.value')
})
})
// emit('update:isShowTextbookDialog', false)
const handleView = function (id: any) {
window.open(`https://zijingebook.ezijing.com/student/book?book_id=${route.query.bid}&chapter_id=${id}`)
}
const handleGl = function (val: any) {
const params: any = {
tag_id: val.id,
course_id: props.course_id,
chapter_id: props.chapterID
}
addTagCourse(params).then(() => {
emit('update:isShowTextbookDialog2', false)
emit('create', val)
ElMessage.success('添加成功')
})
}
</script>
<template>
<el-drawer
:model-value="isShowTextbookDialog2"
draggable
:before-close="handleCancel"
size="60%"
title="关联知识图谱"
>
<el-tree :data="chapterList" node-key="id" :accordion="true" :expand-on-click-node="false" style="min-width: 100%">
<template #default="{ data }">
<span class="custom-tree-node">
<span class="node_title">{{ data.name }}</span>
<div v-if="course_id">
<!-- <el-button class="btn_operate" v-if="data.index === '1'" @click="handleView(data.id)">查阅 </el-button> -->
<el-button class="btn_operate" v-if="data.index === '1'" @click="handleGl(data)">
<el-icon><Plus /></el-icon>
&nbsp; 关联
</el-button>
</div>
</span>
</template>
</el-tree>
</el-drawer>
</template>
<style lang="scss" scoped>
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.btn_operate {
background-color: rgba(248, 241, 229, 0.39);
font-weight: 400;
color: #b2833d;
border: none;
border-radius: 16px;
margin: 3px;
}
.card-list {
display: flex;
flex-direction: column;
}
.card-list-con {
background: #fafafa;
padding: 20px;
display: flex;
flex-wrap: wrap;
}
.video-head {
position: relative;
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
}
.video-tool-btn {
padding: 10px 0 30px 0;
}
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
</style>
<script setup lang="ts">
import { getChapter, createCharacter } from '../../api'
import { Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const route = useRoute()
const emit = defineEmits<Emits>()
const props = defineProps({
isShowTextbookDialog: {
type: Boolean,
required: true
},
course_id: {
type: String,
required: false
},
chapterName: {
type: String,
required: false
},
chapterID: {
type: String,
required: false
},
drawerName: {
type: String,
required: false
}
})
interface Emits {
(e: 'update:isShowTextbookDialog', isShowVideoDialog: boolean): void
(e: 'create', val: any): void
}
// 取消
const handleCancel = () => {
emit('update:isShowTextbookDialog', false)
}
const chapterList = ref([])
onMounted(() => {
getChapter({ book_id: route.query?.bid as string }).then(res => {
chapterList.value = res.data.reduce((a: any, b: any) => {
b.index = '0'
if (b.children) {
b.children.map((m: any) => {
m.index = '1'
})
}
a.push(b)
return a
}, [])
console.log(chapterList.value, 'chapterList.value')
})
})
// emit('update:isShowTextbookDialog', false)
const handleView = function (id: any) {
window.open(`https://zijingebook.ezijing.com/student/book?book_id=${route.query.bid}&chapter_id=${id}`)
}
const handleGl = function (val: any) {
const params: any = {
name: val.name,
course_id: props.course_id,
resource_type: '13',
parent_id: props.chapterID,
resource_id: val.id
}
createCharacter(params).then(() => {
emit('update:isShowTextbookDialog', false)
emit('create', val)
ElMessage.success('添加成功')
})
}
</script>
<template>
<el-drawer
:model-value="isShowTextbookDialog"
draggable
:before-close="handleCancel"
size="60%"
:title="drawerName || '添加数字教材'"
>
<el-tree :data="chapterList" node-key="id" :accordion="true" :expand-on-click-node="false" style="min-width: 100%">
<template #default="{ data }">
<span class="custom-tree-node">
<span class="node_title">{{ data.name }}</span>
<div v-if="course_id">
<el-button class="btn_operate" v-if="data.index === '1'" @click="handleView(data.id)">查阅 </el-button>
<el-button class="btn_operate" v-if="data.index === '1'" @click="handleGl(data)">
<el-icon><Plus /></el-icon>
&nbsp; 关联
</el-button>
</div>
</span>
</template>
</el-tree>
</el-drawer>
</template>
<style lang="scss" scoped>
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.btn_operate {
background-color: rgba(248, 241, 229, 0.39);
font-weight: 400;
color: #b2833d;
border: none;
border-radius: 16px;
margin: 3px;
}
.card-list {
display: flex;
flex-direction: column;
}
.card-list-con {
background: #fafafa;
padding: 20px;
display: flex;
flex-wrap: wrap;
}
.video-head {
position: relative;
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
}
.video-tool-btn {
padding: 10px 0 30px 0;
}
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
</style>
<script setup lang="ts">
import { getBookList } from '../../api'
import { useUserStore } from '@/stores/user'
const appList = ref()
const userStore = useUserStore()
const userInfo: any = userStore.user
const emit = defineEmits<Emits>()
const props = defineProps({
isShowAddDialog: {
type: Boolean,
required: true
}
})
interface Emits {
(e: 'update:isShowAddDialog', isShowVideoDialog: boolean): void
(e: 'create', val: any): void
}
const listOptions = computed(() => {
return {
remote: {
httpRequest: getBookList,
params: { tel: userInfo.mobile, name: '' }
},
filters: [{ type: 'input', prop: 'name', placeholder: '书籍名称' }],
columns: [
// { type: 'selection' },
{ label: '数字教材名称', prop: 'name' },
{
label: '封面',
prop: 'img',
computed(row: any) {
return `<img src="${row.row.img}" style="height:100px;display:block">`
}
},
{ label: '作者', prop: 'authors' },
{ label: '发布者', prop: 'user.real_name' },
{
label: '审核状态',
prop: 'audit_status',
computed(row: any) {
const j: any = {
'1': '待发布',
'2': '审核中',
'3': '审核未通过',
'4': '审核通过'
}
return j[row?.row?.audit_status]
}
},
// { label: '版号', prop: 'name' },
{ label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' }
]
}
})
// 取消
const handleCancel = () => {
emit('update:isShowAddDialog', false)
}
const handleDetail = function (row: any) {
emit('update:isShowAddDialog', false)
emit('create', row)
}
const handleView = function (row: any) {
window.open(`https://zijingebook.ezijing.com/student/book?book_id=${row.id}`)
}
</script>
<template>
<el-drawer :model-value="isShowAddDialog" draggable :before-close="handleCancel" size="60%" title="添加数字教材">
<AppList v-bind="listOptions" ref="appList">
<!-- <el-button style="margin-bottom: 15px" type="primary" @click="addTextbook">添加</el-button> -->
<template #table-operate="{ row }">
<el-button plain @click="handleView(row)">查阅</el-button>
<el-button plain @click="handleDetail(row)">添加</el-button>
</template>
</AppList>
</el-drawer>
</template>
<style lang="scss" scoped>
.card-list {
display: flex;
flex-direction: column;
}
.card-list-con {
background: #fafafa;
padding: 20px;
display: flex;
flex-wrap: wrap;
}
.video-head {
position: relative;
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
}
.video-tool-btn {
padding: 10px 0 30px 0;
}
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
</style>
...@@ -75,7 +75,6 @@ const listOptions = computed(() => { ...@@ -75,7 +75,6 @@ const listOptions = computed(() => {
callback(data: any) { callback(data: any) {
data.list.forEach((item: any) => (item.check_status = false)) data.list.forEach((item: any) => (item.check_status = false))
tableData = data tableData = data
console.log(tableData.list, '123')
return { list: data.list, total: data.total } return { list: data.list, total: data.total }
}, },
params: { tab: tabValue, status: '1', authorized: '', name: '', course_id: props.course_id || '' } params: { tab: tabValue, status: '1', authorized: '', name: '', course_id: props.course_id || '' }
...@@ -211,11 +210,9 @@ const changeCard = () => { ...@@ -211,11 +210,9 @@ const changeCard = () => {
let checkBox = $ref([]) let checkBox = $ref([])
const select = function (row: any) { const select = function (row: any) {
checkBox = row checkBox = row
console.log(row)
} }
const addVideoList = function () { const addVideoList = function () {
console.log(checkBox, 'checkBox')
checkBox.forEach((item: any, index: number) => { checkBox.forEach((item: any, index: number) => {
addRequest(item, index, checkBox) addRequest(item, index, checkBox)
}) })
...@@ -223,7 +220,13 @@ const addVideoList = function () { ...@@ -223,7 +220,13 @@ const addVideoList = function () {
</script> </script>
<template> <template>
<el-drawer :model-value="isShowAddDialog" draggable :before-close="handleCancel" size="60%" :title="`添加${props.btnInfo.btn_name}`"> <el-drawer
:model-value="isShowAddDialog"
draggable
:before-close="handleCancel"
size="60%"
:title="`添加${props.btnInfo.btn_name}`"
>
<div class="video-head"> <div class="video-head">
<el-tabs @tab-change="tabChange" v-model="tabValue"> <el-tabs @tab-change="tabChange" v-model="tabValue">
<el-tab-pane label="我的资源" name="1"></el-tab-pane> <el-tab-pane label="我的资源" name="1"></el-tab-pane>
...@@ -248,7 +251,8 @@ const addVideoList = function () { ...@@ -248,7 +251,8 @@ const addVideoList = function () {
display: block; display: block;
height: 83px; height: 83px;
background-size: cover; background-size: cover;
background-image: url(${row.cover});`"></div> background-image: url(${row.cover});`"
></div>
</div> </div>
</template> </template>
<template #filter-type="{ params }"> <template #filter-type="{ params }">
...@@ -259,7 +263,8 @@ const addVideoList = function () { ...@@ -259,7 +263,8 @@ const addVideoList = function () {
:props="defaultProps" :props="defaultProps"
v-model="params.classification" v-model="params.classification"
:data="selectTree" :data="selectTree"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" /> :default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []"
/>
</template> </template>
<template #body="{ data }" v-if="isCard"> <template #body="{ data }" v-if="isCard">
<div class="card-list" v-if="data.length"> <div class="card-list" v-if="data.length">
...@@ -268,7 +273,14 @@ const addVideoList = function () { ...@@ -268,7 +273,14 @@ const addVideoList = function () {
<el-button type="primary" @click="addVideo">添加</el-button> <el-button type="primary" @click="addVideo">添加</el-button>
</div> </div>
<div class="card-list-con"> <div class="card-list-con">
<CardListItem v-for="(item, index) in data" :key="index" :tabIndex="tabValue" :data="item" :path="path" @add="handleAdd"> <CardListItem
v-for="(item, index) in data"
:key="index"
:tabIndex="tabValue"
:data="item"
:path="path"
@add="handleAdd"
>
<el-checkbox @change="checkboxSelect" v-model="item.check_status"></el-checkbox> <el-checkbox @change="checkboxSelect" v-model="item.check_status"></el-checkbox>
</CardListItem> </CardListItem>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { createCourse, getCourseDetails, updateCourse, getMajorList } from '../api' import { createCourse, getCourseDetails, updateCourse, getMajorList, getBookDetail } from '../api'
// setStatus // setStatus
import { useGetCategoryList } from '@/composables/useGetCategoryList' import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
...@@ -19,6 +19,9 @@ import AddLive from '../components/stepOneComponents/AddLive.vue' ...@@ -19,6 +19,9 @@ import AddLive from '../components/stepOneComponents/AddLive.vue'
import AddCourseData from '../components/stepOneComponents/AddCourseData.vue' import AddCourseData from '../components/stepOneComponents/AddCourseData.vue'
// 添加数字教材
import AddTextbook from '../components/stepOneComponents/AddTextbook.vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
const store = useMapStore() const store = useMapStore()
...@@ -74,7 +77,8 @@ const form = reactive<Record<string, any>>({ ...@@ -74,7 +77,8 @@ const form = reactive<Record<string, any>>({
exam_id: '', exam_id: '',
live_id: '', live_id: '',
specialty_id: [], specialty_id: [],
information_id: '' information_id: '',
book_id: ''
}) })
// 表单验证 // 表单验证
...@@ -111,6 +115,7 @@ const changeExam = (data: any) => { ...@@ -111,6 +115,7 @@ const changeExam = (data: any) => {
// 考试数据回显 // 考试数据回显
const examList: any = ref([]) const examList: any = ref([])
const information: any = ref([]) const information: any = ref([])
const textbookInfo: any = ref([])
// 获取详情 // 获取详情
let loading = $ref<boolean>(false) let loading = $ref<boolean>(false)
...@@ -123,6 +128,15 @@ function fetchDetail() { ...@@ -123,6 +128,15 @@ function fetchDetail() {
form.exam_id = res.data.examinations.map((item: any) => item.id).toString() form.exam_id = res.data.examinations.map((item: any) => item.id).toString()
form.specialty_id = res.data.specialty.map((item: any) => item.id) form.specialty_id = res.data.specialty.map((item: any) => item.id)
form.teacher_id = res.data.teachers.map((item: any) => item.id) form.teacher_id = res.data.teachers.map((item: any) => item.id)
// textbookList.value = res.data.books?.book_id
if (res.data.books.length) {
getBookDetail({ id: res.data.books[0]?.book_id }).then(res => {
if (res.data) {
textbookInfo.value = [res.data]
}
})
// res.data.books[0]?.book_id
}
loading = false loading = false
}) })
} }
...@@ -165,7 +179,7 @@ function handleCreate() { ...@@ -165,7 +179,7 @@ function handleCreate() {
// 操作第二部 // 操作第二部
router.push({ router.push({
path: '/course/update-course/stepTwo', path: '/course/update-course/stepTwo',
query: { id: res.data.id, isEditCourse: '0' } query: { id: res.data.id, isEditCourse: '0', bid: form.book_id }
}) })
} }
}) })
...@@ -178,7 +192,7 @@ function handleUpdate() { ...@@ -178,7 +192,7 @@ function handleUpdate() {
// 操作第二部 // 操作第二部
router.push({ router.push({
path: '/course/update-course/stepTwo', path: '/course/update-course/stepTwo',
query: { id: res.data.id, isEditCourse: '1' } query: { id: res.data.id, isEditCourse: '1', bid: form.book_id }
}) })
} }
}) })
...@@ -196,6 +210,10 @@ getMajorList({ name: '', 'per-page': '100' }).then((res: any) => { ...@@ -196,6 +210,10 @@ getMajorList({ name: '', 'per-page': '100' }).then((res: any) => {
const handleInformation = (val: any) => { const handleInformation = (val: any) => {
form.information_id = val.map((item: any) => item.id).toString() form.information_id = val.map((item: any) => item.id).toString()
} }
const handleTextbook = (val: any) => {
form.book_id = val[0].id
}
</script> </script>
<template> <template>
...@@ -229,7 +247,8 @@ const handleInformation = (val: any) => { ...@@ -229,7 +247,8 @@ const handleInformation = (val: any) => {
:data="selectTree" :data="selectTree"
node-key="id" node-key="id"
:default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []" :default-expanded-keys="selectTree.length ? [selectTree[0]?.id] : []"
placeholder="请选择课程分类" /> placeholder="请选择课程分类"
/>
</el-form-item> </el-form-item>
<el-form-item label="课程学分" prop="credit"> <el-form-item label="课程学分" prop="credit">
<el-input v-model="form.credit" maxlength="3" /> <el-input v-model="form.credit" maxlength="3" />
...@@ -241,14 +260,16 @@ const handleInformation = (val: any) => { ...@@ -241,14 +260,16 @@ const handleInformation = (val: any) => {
style="width: 100%" style="width: 100%"
clearable clearable
multiple multiple
v-permission="'v1-course-search-specialty'"> v-permission="'v1-course-search-specialty'"
>
<!-- :remote-method="handleMajorList" --> <!-- :remote-method="handleMajorList" -->
<el-option <el-option
v-for="(item, index) in majorList" v-for="(item, index) in majorList"
:key="index" :key="index"
:label="item.name" :label="item.name"
:value="item.id"></el-option> :value="item.id"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
...@@ -259,7 +280,8 @@ const handleInformation = (val: any) => { ...@@ -259,7 +280,8 @@ const handleInformation = (val: any) => {
:model="form" :model="form"
:rules="rules" :rules="rules"
label-suffix=":" label-suffix=":"
style="width: 100%; margin-top: 30px"> style="width: 100%; margin-top: 30px"
>
<el-form-item label="课程助教" prop="teacher_id"> <el-form-item label="课程助教" prop="teacher_id">
<!-- 添加助教 --> <!-- 添加助教 -->
<AddAssistant v-model="form.teachers" style="width: 100%"></AddAssistant> <AddAssistant v-model="form.teachers" style="width: 100%"></AddAssistant>
...@@ -295,7 +317,19 @@ const handleInformation = (val: any) => { ...@@ -295,7 +317,19 @@ const handleInformation = (val: any) => {
:data="information" :data="information"
style="width: 100%" style="width: 100%"
:id="id" :id="id"
@information="handleInformation"></AddCourseData> @information="handleInformation"
></AddCourseData>
</el-form-item>
<!-- AddDigitalTextbook -->
<el-form-item label="数字教材" prop="information_id">
<!-- 添加数字教材 -->
<AddTextbook
v-model="form.book_id"
:data="textbookInfo"
style="width: 100%"
:id="id"
@information="handleTextbook"
></AddTextbook>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
......
...@@ -12,6 +12,9 @@ import AddVideoDialog from '../components/stepTwoComponents/AddVideoDialog.vue' ...@@ -12,6 +12,9 @@ import AddVideoDialog from '../components/stepTwoComponents/AddVideoDialog.vue'
import AddExamDialog from '../components/stepTwoComponents/AddExamDialog.vue' import AddExamDialog from '../components/stepTwoComponents/AddExamDialog.vue'
import VideoPlayDialog from '../components/stepTwoComponents/VideoPlayDialog.vue' import VideoPlayDialog from '../components/stepTwoComponents/VideoPlayDialog.vue'
import AddChapterDialog from '../components/stepTwoComponents/AddChapterDialog.vue' import AddChapterDialog from '../components/stepTwoComponents/AddChapterDialog.vue'
import AddTextbookDialog from '../components/stepTwoComponents/AddTextbookDialog.vue'
import AddGraphbookDialog from '../components/stepTwoComponents/AddGraphbookDialog.vue'
import OpenRules from '../components/stepTwoComponents/OpenRules.vue' import OpenRules from '../components/stepTwoComponents/OpenRules.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
...@@ -24,6 +27,8 @@ const isShowSectionDialog = ref(false) ...@@ -24,6 +27,8 @@ const isShowSectionDialog = ref(false)
// 修改资源名称弹窗 // 修改资源名称弹窗
const isEditResourcesNameDialog = ref(false) const isEditResourcesNameDialog = ref(false)
// 资源id // 资源id
const isShowTextbookDialog = ref(false)
const isShowTextbookDialog2 = ref(false)
const resourceId = ref('') const resourceId = ref('')
const isShowLiveDialog = ref(false) const isShowLiveDialog = ref(false)
const isShowAddDialog = ref(false) const isShowAddDialog = ref(false)
...@@ -67,6 +72,14 @@ const btnList = [ ...@@ -67,6 +72,14 @@ const btnList = [
{ {
btn_name: '直播', btn_name: '直播',
resource_type: '6' resource_type: '6'
},
{
btn_name: '关联数字教材',
resource_type: '13'
},
{
btn_name: '关联知识图谱',
resource_type: '14'
} }
] ]
const defaultProps = { const defaultProps = {
...@@ -143,7 +156,8 @@ const handleDel = (node: any) => { ...@@ -143,7 +156,8 @@ const handleDel = (node: any) => {
} }
// 添加 // 添加
const handleAddDialog = (node: any, item: any) => { const handleAddDialog = (node: any, item: any, data: any) => {
console.log(data, 'data', dataSource)
chapterID.value = node.key chapterID.value = node.key
chapterName.value = node.label chapterName.value = node.label
btnInfo.value = item btnInfo.value = item
...@@ -164,10 +178,16 @@ const handleAddDialog = (node: any, item: any) => { ...@@ -164,10 +178,16 @@ const handleAddDialog = (node: any, item: any) => {
} else if (item.resource_type === '3') { } else if (item.resource_type === '3') {
isShowExamDialog.value = true isShowExamDialog.value = true
paper_use_list.value = [2] paper_use_list.value = [2]
} else if (item.resource_type === '13') {
isShowTextbookDialog.value = true
} else if (item.resource_type === '14') {
isShowTextbookDialog2.value = true
} }
} }
//查阅 //查阅
const handleConsult = (node: any) => { const handleConsult = (node: any) => {
console.log(node, 'node')
// 视频 // 视频
if (node.data.resource_type === '2') { if (node.data.resource_type === '2') {
getVideoDetails({ id: node.data.resource_id }).then(res => { getVideoDetails({ id: node.data.resource_id }).then(res => {
...@@ -175,6 +195,11 @@ const handleConsult = (node: any) => { ...@@ -175,6 +195,11 @@ const handleConsult = (node: any) => {
isShowVideoPlayDialog.value = true isShowVideoPlayDialog.value = true
}) })
} }
if (node.data.resource_type === '13') {
window.open(
`https://zijingebook.ezijing.com/student/book?book_id=${route.query.bid}&chapter_id=${node.data.resource_id}`
)
}
} }
// 下载 // 下载
const handleDownload = (node: any) => { const handleDownload = (node: any) => {
...@@ -252,6 +277,9 @@ const imgUrl = (node: any) => { ...@@ -252,6 +277,9 @@ const imgUrl = (node: any) => {
} else if (node.data.resource_type === '9') { } else if (node.data.resource_type === '9') {
// 考试 // 考试
return '/center_resource/test_img.png' return '/center_resource/test_img.png'
} else if (node.data.resource_type === '13') {
// 考试
return '/center_resource/docx_img.png'
} }
} }
const nodeType = (node: any) => { const nodeType = (node: any) => {
...@@ -275,6 +303,10 @@ const nodeType = (node: any) => { ...@@ -275,6 +303,10 @@ const nodeType = (node: any) => {
} else if (node.data.resource_type === '11') { } else if (node.data.resource_type === '11') {
// 考试 // 考试
return '教案' return '教案'
} else if (node.data.resource_type === '13') {
return '数字教材'
} else if (node.data.resource_type === '14') {
return '知识图谱'
} }
} }
const handleNodeExpand = (data: any) => { const handleNodeExpand = (data: any) => {
...@@ -355,7 +387,8 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -355,7 +387,8 @@ const handleChangeStatus = (node: any, data: any) => {
@node-drop="handleDrop" @node-drop="handleDrop"
style="min-width: 100%" style="min-width: 100%"
@node-expand="handleNodeExpand" @node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"> @node-collapse="handleNodeCollapse"
>
<!-- --> <!-- -->
<template #default="{ node, data }"> <template #default="{ node, data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
...@@ -365,9 +398,16 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -365,9 +398,16 @@ const handleChangeStatus = (node: any, data: any) => {
<span class="node_title">{{ node.label.length > 20 ? node.label.slice(0, 20) + '...' : node.label }}</span> <span class="node_title">{{ node.label.length > 20 ? node.label.slice(0, 20) + '...' : node.label }}</span>
<el-link <el-link
class="btn_edit" class="btn_edit"
v-if="data.depth === '3' && node.data.resource_type === '2'" v-if="
(data.depth === '3' && node.data.resource_type === '2') ||
node.data.resource_type === '13' ||
node.data.resource_type === '14'
"
@click="handleConsult(node)" @click="handleConsult(node)"
:disabled="node.data.resource.can_view !== true" :disabled="
(node.data.resource?.can_view !== true && node.data.resource_type !== '13') ||
node.data.resource_type === '14'
"
style="margin-left: 35px" style="margin-left: 35px"
>查阅</el-link >查阅</el-link
> >
...@@ -378,15 +418,18 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -378,15 +418,18 @@ const handleChangeStatus = (node: any, data: any) => {
node.data.resource_type !== '2' && node.data.resource_type !== '2' &&
node.data.resource_type !== '6' && node.data.resource_type !== '6' &&
node.data.resource_type !== '3' && node.data.resource_type !== '3' &&
node.data.resource_type !== '9' node.data.resource_type !== '9' &&
node.data.resource_type !== '13' &&
node.data.resource_type !== '14'
" "
@click="handleDownload(node)" @click="handleDownload(node)"
:disabled="node.data.resource.can_view !== true" :disabled="node.data.resource?.can_view !== true"
style="margin-left: 35px" style="margin-left: 35px"
>下载</el-link >下载</el-link
> >
<!-- data.depth !== '3' 资源--> <!-- data.depth !== '3' 资源-->
<el-link <el-link
v-if="node.data.resource_type !== '13' || node.data.resource_type !== '14'"
class="btn_edit" class="btn_edit"
@click="handleEdit(node, data.depth)" @click="handleEdit(node, data.depth)"
style="margin-left: 35px" style="margin-left: 35px"
...@@ -407,7 +450,8 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -407,7 +450,8 @@ const handleChangeStatus = (node: any, data: any) => {
class="btn_operate" class="btn_operate"
v-if="data.depth === '1' || data.depth === '2'" v-if="data.depth === '1' || data.depth === '2'"
@click="handleOpenRules(node, data)" @click="handleOpenRules(node, data)"
v-permission="'v1-course-set-chapter-rules'"> v-permission="'v1-course-set-chapter-rules'"
>
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
&nbsp; 开放规则 &nbsp; 开放规则
</el-button> </el-button>
...@@ -420,7 +464,8 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -420,7 +464,8 @@ const handleChangeStatus = (node: any, data: any) => {
class="btn_operate" class="btn_operate"
v-for="(item, index) in btnList" v-for="(item, index) in btnList"
:key="index" :key="index"
@click="handleAddDialog(node, item)"> @click="handleAddDialog(node, item, data)"
>
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
{{ item.btn_name }} {{ item.btn_name }}
</el-button> </el-button>
...@@ -431,8 +476,11 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -431,8 +476,11 @@ const handleChangeStatus = (node: any, data: any) => {
node.data.resource_type !== '2' && node.data.resource_type !== '2' &&
node.data.resource_type !== '6' && node.data.resource_type !== '6' &&
node.data.resource_type !== '3' && node.data.resource_type !== '3' &&
node.data.resource_type !== '9' node.data.resource_type !== '9' &&
"> node.data.resource_type !== '13' &&
node.data.resource_type !== '14'
"
>
<span class="btn_operate">学生下载控制:</span> <span class="btn_operate">学生下载控制:</span>
<el-switch <el-switch
class="btn_edit" class="btn_edit"
...@@ -449,7 +497,8 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -449,7 +497,8 @@ const handleChangeStatus = (node: any, data: any) => {
inactive-value="0" inactive-value="0"
inline-prompt inline-prompt
style="--el-switch-on-color: #aa1941" style="--el-switch-on-color: #aa1941"
@change="handleChangeStatus(node, data)"></el-switch> @change="handleChangeStatus(node, data)"
></el-switch>
</template> </template>
</span> </span>
</span> </span>
...@@ -471,7 +520,8 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -471,7 +520,8 @@ const handleChangeStatus = (node: any, data: any) => {
:chapterID="chapterID" :chapterID="chapterID"
:course_id="id" :course_id="id"
:sectionName="sectionName" :sectionName="sectionName"
:resourceId="resourceId" /> :resourceId="resourceId"
/>
<!-- 添加章 --> <!-- 添加章 -->
<AddChapterDialog <AddChapterDialog
v-if="isShowDialog === true" v-if="isShowDialog === true"
...@@ -480,7 +530,8 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -480,7 +530,8 @@ const handleChangeStatus = (node: any, data: any) => {
:course_id="id" :course_id="id"
:isEdit="isEdit" :isEdit="isEdit"
:chapterID="chapterID" :chapterID="chapterID"
:chapterName="chapterName" /> :chapterName="chapterName"
/>
<!-- 添加小节 --> <!-- 添加小节 -->
<AddSectionDialog <AddSectionDialog
v-if="isShowSectionDialog === true" v-if="isShowSectionDialog === true"
...@@ -490,7 +541,8 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -490,7 +541,8 @@ const handleChangeStatus = (node: any, data: any) => {
:chapterID="chapterID" :chapterID="chapterID"
:course_id="id" :course_id="id"
:isEdit="isEdit" :isEdit="isEdit"
:sectionName="sectionName" /> :sectionName="sectionName"
/>
<!-- 添加直播 --> <!-- 添加直播 -->
<AddLiveDialog <AddLiveDialog
v-if="isShowLiveDialog === true" v-if="isShowLiveDialog === true"
...@@ -499,7 +551,8 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -499,7 +551,8 @@ const handleChangeStatus = (node: any, data: any) => {
:chapterName="chapterName" :chapterName="chapterName"
:chapterID="chapterID" :chapterID="chapterID"
:course_id="id" :course_id="id"
:btnInfo="btnInfo" /> :btnInfo="btnInfo"
/>
<!-- 添加资源 --> <!-- 添加资源 -->
<AddVideoDialog <AddVideoDialog
v-if="isShowAddDialog === true" v-if="isShowAddDialog === true"
...@@ -508,7 +561,8 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -508,7 +561,8 @@ const handleChangeStatus = (node: any, data: any) => {
:chapterName="chapterName" :chapterName="chapterName"
:chapterID="chapterID" :chapterID="chapterID"
:course_id="id" :course_id="id"
:btnInfo="btnInfo" /> :btnInfo="btnInfo"
/>
<!-- 添加考试 --> <!-- 添加考试 -->
<AddExamDialog <AddExamDialog
...@@ -520,12 +574,14 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -520,12 +574,14 @@ const handleChangeStatus = (node: any, data: any) => {
:course_id="id" :course_id="id"
:btnInfo="btnInfo" :btnInfo="btnInfo"
:paper_use_list="paper_use_list" :paper_use_list="paper_use_list"
:isMultiple="false" /> :isMultiple="false"
/>
<!-- 视频查阅弹框 --> <!-- 视频查阅弹框 -->
<VideoPlayDialog <VideoPlayDialog
v-if="isShowVideoPlayDialog === true" v-if="isShowVideoPlayDialog === true"
v-model:isShowVideoPlayDialog="isShowVideoPlayDialog" v-model:isShowVideoPlayDialog="isShowVideoPlayDialog"
:videoOptions="videoUrl" /> :videoOptions="videoUrl"
/>
<OpenRules <OpenRules
v-model:isShowOpenRules="isShowOpenRules" v-model:isShowOpenRules="isShowOpenRules"
v-if="isShowOpenRules === true" v-if="isShowOpenRules === true"
...@@ -534,7 +590,24 @@ const handleChangeStatus = (node: any, data: any) => { ...@@ -534,7 +590,24 @@ const handleChangeStatus = (node: any, data: any) => {
:chapterID="chapterID" :chapterID="chapterID"
:course_id="id" :course_id="id"
:controlInfo="controlInfo" :controlInfo="controlInfo"
@create="handleFresh" /> @create="handleFresh"
/>
<AddTextbookDialog
v-if="isShowTextbookDialog === true"
v-model:isShowTextbookDialog="isShowTextbookDialog"
@create="handleFresh"
:chapterName="chapterName"
:chapterID="chapterID"
:course_id="id"
/>
<AddGraphbookDialog
v-if="isShowTextbookDialog2 === true"
v-model:isShowTextbookDialog2="isShowTextbookDialog2"
@create="handleFresh"
:chapterName="chapterName"
:chapterID="chapterID"
:course_id="id"
/>
</template> </template>
<style> <style>
......
...@@ -92,3 +92,41 @@ export function courseCopy(data: { id: string }) { ...@@ -92,3 +92,41 @@ export function courseCopy(data: { id: string }) {
export function setDownload(data: { course_id: string; information_id: string; can_download: string }) { export function setDownload(data: { course_id: string; information_id: string; can_download: string }) {
return httpRequest.post('/api/resource/v1/course/course/set-course-information-can-download ', data) return httpRequest.post('/api/resource/v1/course/course/set-course-information-can-download ', data)
} }
// 获取知识图谱
export function getTagList(params: { type: string; parent_id: string; chapter_id?: string }) {
return httpRequest.get('/api/resource/v1/course/course/tag-list', { params })
}
// 新增知识图谱
export function addTag(data: { name: string; course_id: string; status: string; parent_id?: string }) {
return httpRequest.post('/api/resource/v1/course/course/tag-add', data)
}
// 更新知识图谱
export function updateTag(data: { id: string; name: string; course_id: string; status: string; }) {
return httpRequest.post('/api/resource/v1/course/course/tag-update', data)
}
// 删除知识图谱
export function deleteTag(data: { id: string; course_id: string }) {
return httpRequest.post('/api/resource/v1/course/course/tag-delete', data)
}
// 查看关联的课程章节
export function viewTagCourse(params: { tag_id: string; course_id: string }) {
return httpRequest.get('/api/resource/v1/course/course/tag-course', { params })
}
// 查看关联的书籍章节
export function viewTagBook(params: { tag_id: string; course_id: string }) {
return httpRequest.get('/api/resource/v1/course/course/tag-book', { params })
}
// 导入
export function importTag(data: { course_id: string; file: any }) {
return httpRequest.post('/api/resource/v1/course/course/tag-import', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
\ No newline at end of file
...@@ -124,6 +124,11 @@ const handleMembers = () => { ...@@ -124,6 +124,11 @@ const handleMembers = () => {
members.value = useProjectList(props.data.organ_id).members members.value = useProjectList(props.data.organ_id).members
} }
// 点击知识图谱
const handleChart = () => {
router.push({ path: '/course/my/graph', query: { n: props.data?.name, bid: 19, cid: route.query.id } })
}
// 更改负责人确定 // 更改负责人确定
const handleSetBelong = () => { const handleSetBelong = () => {
setBelong({ id: id, belong_operator: form.members }).then((res: any) => { setBelong({ id: id, belong_operator: form.members }).then((res: any) => {
...@@ -223,6 +228,7 @@ watch( ...@@ -223,6 +228,7 @@ watch(
{{ props.data.status == 0 ? '课程上线' : '课程下线' }} {{ props.data.status == 0 ? '课程上线' : '课程下线' }}
</div> </div>
<div v-if="props.data.auth_belong" class="btn-item" @click="handleMembers">更改负责人</div> <div v-if="props.data.auth_belong" class="btn-item" @click="handleMembers">更改负责人</div>
<div class="btn-item" @click="handleChart">知识图谱</div>
</div> </div>
<el-dialog v-if="props.data.auth_belong" v-model="dialogFormVisible" title="更改负责人" center> <el-dialog v-if="props.data.auth_belong" v-model="dialogFormVisible" title="更改负责人" center>
<el-form :model="form"> <el-form :model="form">
......
...@@ -234,7 +234,8 @@ const handleDetail = (row: any) => { ...@@ -234,7 +234,8 @@ const handleDetail = (row: any) => {
inline-prompt inline-prompt
style="--el-switch-on-color: #aa1941" style="--el-switch-on-color: #aa1941"
@click="handleChangeStatus(row)" @click="handleChangeStatus(row)"
:disabled="!checkPermission('v1-course-set-course-information-can-download')"> :disabled="!checkPermission('v1-course-set-course-information-can-download')"
>
</el-switch> </el-switch>
</template> </template>
<template #table-operate="{ row }"> <template #table-operate="{ row }">
......
<script setup lang="ts">
import { viewTagBook } from '../api'
const route = useRoute()
const emit = defineEmits<Emits>()
const props = defineProps({
isShowGraphBookDialog: {
type: Boolean,
required: true
},
id: {
type: String,
required: false
}
})
interface Emits {
(e: 'update:isShowGraphBookDialog', isShowVideoDialog: boolean): void
(e: 'create', val: any): void
}
// 取消
const handleCancel = () => {
emit('update:isShowGraphBookDialog', false)
}
const chapterList = ref([])
onMounted(() => {
viewTagBook({ tag_id: props.id as string, course_id: route.query.cid as string }).then(res => {
chapterList.value = res.data.reduce((a: any, b: any) => {
b.index = '0'
if (b.children) {
b.children.map((m: any) => {
m.index = '1'
})
}
a.push(b)
return a
}, [])
console.log(chapterList.value, 'chapterList.value')
})
})
</script>
<template>
<el-drawer
:model-value="isShowGraphBookDialog"
draggable
:before-close="handleCancel"
size="60%"
title="关联数字教材章节"
>
<el-tree :data="chapterList" node-key="id" :accordion="true" :expand-on-click-node="false" style="min-width: 100%">
<template #default="{ data }">
<span class="custom-tree-node">
<span class="node_title">{{ data.name }}</span>
</span>
</template>
</el-tree>
</el-drawer>
</template>
<style lang="scss" scoped>
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.btn_operate {
background-color: rgba(248, 241, 229, 0.39);
font-weight: 400;
color: #b2833d;
border: none;
border-radius: 16px;
margin: 3px;
}
.card-list {
display: flex;
flex-direction: column;
}
.card-list-con {
background: #fafafa;
padding: 20px;
display: flex;
flex-wrap: wrap;
}
.video-head {
position: relative;
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
}
.video-tool-btn {
padding: 10px 0 30px 0;
}
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
</style>
<script setup lang="ts">
import { viewTagCourse } from '../api'
const route = useRoute()
const emit = defineEmits<Emits>()
const props = defineProps({
isShowGraphCourseDialog: {
type: Boolean,
required: true
},
id: {
type: String,
required: false
}
})
interface Emits {
(e: 'update:isShowGraphCourseDialog', isShowVideoDialog: boolean): void
(e: 'create', val: any): void
}
// 取消
const handleCancel = () => {
emit('update:isShowGraphCourseDialog', false)
}
const chapterList = ref([])
onMounted(() => {
viewTagCourse({ tag_id: props.id as string, course_id: route.query.cid as string }).then(res => {
chapterList.value = res.data.reduce((a: any, b: any) => {
b.index = '0'
if (b.children) {
b.children.map((m: any) => {
m.index = '1'
})
}
a.push(b)
return a
}, [])
console.log(chapterList.value, 'chapterList.value')
})
})
</script>
<template>
<el-drawer
:model-value="isShowGraphCourseDialog"
draggable
:before-close="handleCancel"
size="60%"
title="关联课程章节"
>
<el-tree :data="chapterList" node-key="id" :accordion="true" :expand-on-click-node="false" style="min-width: 100%">
<template #default="{ data }">
<span class="custom-tree-node">
<span class="node_title">{{ data.name }}</span>
</span>
</template>
</el-tree>
</el-drawer>
</template>
<style lang="scss" scoped>
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.btn_operate {
background-color: rgba(248, 241, 229, 0.39);
font-weight: 400;
color: #b2833d;
border: none;
border-radius: 16px;
margin: 3px;
}
.card-list {
display: flex;
flex-direction: column;
}
.card-list-con {
background: #fafafa;
padding: 20px;
display: flex;
flex-wrap: wrap;
}
.video-head {
position: relative;
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
}
.video-tool-btn {
padding: 10px 0 30px 0;
}
.video-head-icon {
position: absolute;
top: 0;
right: 0;
font-size: 30px;
color: #666;
cursor: pointer;
}
</style>
...@@ -9,7 +9,8 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -9,7 +9,8 @@ export const routes: Array<RouteRecordRaw> = [
{ path: '', component: () => import('./views/List.vue') }, { path: '', component: () => import('./views/List.vue') },
{ path: '/course/my/view', component: () => import('./views/View.vue') }, { path: '/course/my/view', component: () => import('./views/View.vue') },
// id: 章节id courseId课程id // id: 章节id courseId课程id
{ path: '/course/my/view/:id/:courseId', component: () => import('./views/ViewDetails.vue') } { path: '/course/my/view/:id/:courseId', component: () => import('./views/ViewDetails.vue') },
{ path: '/course/my/graph', component: () => import('./views/Graph.vue') }
] ]
} }
] ]
<script setup lang="ts">
import { getTagList, addTag, updateTag, deleteTag, importTag } from '../api'
import ViewGraphCourse from '../components/ViewGraphCourse.vue'
import ViewGraphBook from '../components/ViewGraphBook.vue'
import { ElMessage } from 'element-plus'
const route = useRoute()
// 获取列表
const tableData = ref([])
const updateTagList = () => {
getTagList({ type: '1', parent_id: route.query.cid as string }).then(res => {
tableData.value = res.data
})
}
updateTagList()
// 新增
const addTagRequest = () => {
addTag({ name: pName.value, course_id: route.query?.cid as string, status: '1', parent_id: addTagId.value }).then(
res => {
updateTagList()
ElMessage({
message: '添加成功',
type: 'success'
})
}
)
}
// 更新
const updateTagRequest = () => {
updateTag({ id: addTagId.value, name: pName.value, course_id: route.query?.cid as string, status: '1' }).then(res => {
updateTagList()
ElMessage({
message: '修改成功',
type: 'success'
})
})
}
const findParentIdById = (items: any, targetId: any) => {
// 使用栈来模拟递归过程,存储当前遍历元素和父级id
const stack = [...items] // 初始化栈为顶层元素
// 使用栈遍历
while (stack.length > 0) {
const currentItem = stack.pop() // 取出栈顶元素
// 检查当前元素是否是目标元素
if (currentItem.id === targetId) {
return currentItem.parentId // 找到目标元素,返回父级id
}
// 如果当前元素有children,继续遍历子元素
if (currentItem.children) {
for (const child of currentItem.children) {
child.parentId = currentItem.id // 为子元素添加父级id
stack.push(child) // 将子元素压入栈中
}
}
}
return undefined // 如果没有找到目标id,返回undefined
}
// 弹窗输入框
const pName: any = ref('')
const dialogVisible = ref(false)
const isUpdate = ref(false)
// 点击添加同级 子级的id
const addTagId = ref<any>('')
const handleAddTagSame = (scope: any) => {
const id = findParentIdById(tableData.value, scope?.row?.id)
addTagId.value = id || ''
isUpdate.value = false
dialogVisible.value = true
}
const handleAddTagChild = (scope: any) => {
addTagId.value = scope?.row.id
isUpdate.value = false
dialogVisible.value = true
}
const handleUpdateTag = (scope: any) => {
addTagId.value = scope?.row?.id
pName.value = scope?.row?.name
isUpdate.value = true
dialogVisible.value = true
}
// 输入完名称点击输入框
const handleConfirm = () => {
if (isUpdate) {
updateTagRequest()
} else {
addTagRequest()
}
dialogVisible.value = false
}
const handleDelete = (scope: any) => {
deleteTag({ id: scope.row?.id, course_id: route.query.cid as string }).then(res => {
updateTagList()
ElMessage({
message: '删除成功',
type: 'success'
})
})
}
const isShowGraphCourseDialog = ref(false)
const isShowGraphBookDialog = ref(false)
const handleCourseView = function (scope: any) {
addTagId.value = scope.row?.id
isShowGraphCourseDialog.value = true
}
const handleBookView = function (scope: any) {
addTagId.value = scope.row?.id
isShowGraphBookDialog.value = true
}
const formInline = reactive({
user: ''
})
// 导出
const handleTagDownload = () => {
// tagDownload({ course_id: route.query.cid as string }).then()
window.open(`/api/resource/v1/course/course/tag-download?course_id=${route.query.cid}`)
}
const fetchFileUpload = (option: any) => {
return new Promise(() => {
importTag({ course_id: route.query.cid as string, file: option.file }).then(() => {
updateTagList()
ElMessage.success('导入数据成功')
})
})
}
</script>
<template>
<AppCard title="知识图库">
<div style="margin-bottom: 15px">课程名称:{{ route.query?.n }}</div>
<!-- <div>
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="知识点">
<el-input v-model="formInline.user" placeholder="请输入" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary">搜索</el-button>
<el-button type="primary">重置</el-button>
</el-form-item>
</el-form>
</div> -->
<div style="display: flex">
<el-button type="primary" @click="handleAddTagSame">添加知识点</el-button>
<el-upload :show-file-list="false" ref="upload" action="#" :limit="1" :http-request="fetchFileUpload">
<el-button type="primary" style="margin: 0 15px">批量导入</el-button>
<!-- <el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖至此处,点击上传</div> -->
</el-upload>
<el-button type="primary" @click="handleTagDownload">导出</el-button>
</div>
<el-table
:data="tableData"
style="width: 100%; margin: 20px 0"
row-key="id"
highlight-current-row
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
align="center"
>
<!-- <el-table-column type="index"></el-table-column> -->
<el-table-column prop="name" label="知识点"> </el-table-column>
<!-- <el-table-column prop="update" label="更新人" width="180"> </el-table-column>
<el-table-column prop="time" label="更新时间" width="180"> </el-table-column> -->
<el-table-column fixed="right" label="操作">
<template #default="scope">
<el-button link type="primary" size="small" @click="handleAddTagSame(scope)"> 添加同级 </el-button>
<el-button link type="primary" size="small" @click="handleAddTagChild(scope)">添加子级</el-button>
<el-button link type="primary" size="small" @click="handleUpdateTag(scope)">编辑知识点</el-button>
<el-button link type="primary" size="small" @click="handleDelete(scope)">删除</el-button>
<el-button link type="primary" size="small" @click="handleCourseView(scope)">查看关联章节</el-button>
<el-button link type="primary" size="small" @click="handleBookView(scope)">查看关联数字教材</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogVisible" title="提示" width="500px">
<!-- <span>This is a message</span> -->
<el-input v-model="pName" placeholder="请输入知识点名称" />
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm"> 确认 </el-button>
</div>
</template>
</el-dialog>
<!-- <Operation :data="courseDetails" @update="handleFresh"></Operation>
<div class="course-view">
<ViewCourseInfoTop :data="courseDetails"></ViewCourseInfoTop>
<div class="course-left_info">
<ViewCourseInfo :data="courseDetails" class="info_bottom" :id="id"></ViewCourseInfo>
<ViewCourseChapter
v-if="Object.keys(courseDetails).length"
:data="chapters"
class="info_chapter"
></ViewCourseChapter>
</div>
</div> -->
</AppCard>
<ViewGraphCourse
:id="addTagId"
v-if="isShowGraphCourseDialog === true"
v-model:isShowGraphCourseDialog="isShowGraphCourseDialog"
/>
<ViewGraphBook
:id="addTagId"
v-if="isShowGraphBookDialog === true"
v-model:isShowGraphBookDialog="isShowGraphBookDialog"
/>
</template>
<style lang="scss">
.course-view {
border: 1px solid #bfbfbf;
border-radius: 10px;
display: flex;
flex-direction: column;
.course-left_info {
display: flex;
.info_bottom {
width: 67%;
}
.info_chapter {
width: 25%;
}
}
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.course_tip {
font-size: 12px;
font-weight: 400;
color: #999999;
margin-left: 16px;
line-height: 36px;
height: 36px;
}
.el-tree-node__content {
height: 42px;
margin-bottom: 15px;
background: #f4f4f4;
}
.btn_operate {
background-color: rgba(248, 241, 229, 0.39);
font-weight: 400;
color: #b2833d;
border: none;
border-radius: 16px;
margin: 3px;
}
.btn_control {
font-weight: 400;
color: #b2833d;
}
.btn_edit {
color: #b2833d;
margin-right: 10px;
}
.node_type {
background: #bf9d6b;
border-radius: 20px;
line-height: 1;
font-size: 12px;
font-weight: 400;
color: #ffffff;
text-align: center;
padding: 4px 8px;
margin-right: 14px;
}
.node_title {
font-size: 14px;
font-weight: 400;
color: #666666;
line-height: 1;
text-align: center;
}
</style>
import axios from 'axios'
import md5 from 'blueimp-md5'
import { getSignature, uploadFile } from '@/api/base'
export async function upload(blob: Blob | File) {
let fileType = 'png'
if (blob instanceof File && blob.name) {
const matches = blob.name.match(/\.(\w+)$/)
if (matches) {
fileType = matches[1]
}
} else if (blob.type) {
const mimeType = blob.type.split('/').pop()
if (mimeType) {
fileType = mimeType
}
}
const key =
'upload/resource-center/' + md5(new Date().getTime() + Math.random().toString(36).slice(-8)) + '.' + fileType
const response: any = await getSignature()
const params = {
key,
host: response.host,
OSSAccessKeyId: response.accessid,
policy: response.policy,
signature: response.signature,
success_action_status: '200',
file: blob,
url: `${response.host}/${key}`,
}
await uploadFile(params)
return params.url
}
export async function uploadFileByUrl(url: string) {
const res = await axios.get(url, { responseType: 'blob' })
return upload(res.data)
}
...@@ -33,7 +33,6 @@ export default defineConfig(() => ({ ...@@ -33,7 +33,6 @@ export default defineConfig(() => ({
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace(/^\/api\/qbs/, '') rewrite: path => path.replace(/^\/api\/qbs/, '')
}, },
'/api': 'https://resource-center.ezijing.com' '/api': 'https://resource-center.ezijing.com'
} }
}, },
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论