Files
learning-system-portal/src/views/course/ManageListRemote.vue

2598 lines
81 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="manage-list-remote">
<div class="filter-wrapper">
<div class="filter-row filter-row--primary">
<div class="filter-fields">
<div class="filter-field filter-field--name">
<el-input type="text" placeholder="课程名称" v-model="params.name" clearable maxlength="50"></el-input>
</div>
<div class="filter-field filter-field--category">
<el-cascader placeholder="课程分类" clearable v-model="sysTypeList" :props="defaultTypeProps"
:options="sysTypeListMap"></el-cascader>
</div>
<div class="filter-field filter-field--teacher teacher-filter">
<div class="teacher-select-wrapper">
<el-select
class="creator-select"
ref="teacherSelect"
v-model="teacherSelected"
multiple
filterable
remote
clearable
reserve-keyword
placeholder="授课教师"
:multiple-limit="5"
:remote-method="remoteSearchTeacher"
:loading="teacherLoading"
@input.native="limitTeacherInput"
@visible-change="handleTeacherVisibleChange"
@change="handleTeacherChange"
@clear="handleTeacherClear"
>
<el-option v-for="item in teacherOptions" :key="item.teacherId" :label="item.teacherName"
:value="item.teacherId">
<span>{{ item.teacherName }}</span>
<span v-if="item.teacherCode" class="option-code">{{ item.teacherCode }}</span>
</el-option>
</el-select>
</div>
</div>
<div class="filter-field filter-field--time learning-time-range">
<div :class="[
'grid-content',
'bg-purple',
'resetDatePicker',
!learningTimeRange || learningTimeRange.length === 0 ? 'noSplitDatePicker' : ''
]">
<el-date-picker v-model="learningTimeRange" type="daterange" align="right" unlink-panels clearable
value-format="yyyy-MM-dd 00:00:00" range-separator="" start-placeholder="培训时间"
:picker-options="pickerOptions" @change="handleLearningTimeRangeChange"></el-date-picker>
</div>
</div>
<div class="filter-field filter-field--status">
<el-select v-model="params.status" placeholder="审核状态" clearable>
<el-option label="-" value="1"></el-option>
<el-option label="审核中" value="2"></el-option>
<el-option label="审核通过" value="5"></el-option>
<el-option label="审核驳回" value="3"></el-option>
</el-select>
</div>
<div class="filter-field filter-field--publish">
<el-select v-model="params.publish" placeholder="发布状态" clearable>
<el-option label="已发布" :value="true"></el-option>
<el-option label="未发布" :value="false"></el-option>
</el-select>
</div>
</div>
<div class="filter-actions" v-show="!showAdvancedFilter">
<el-button type="text" class="toggle-link" @click="toggleAdvancedFilter">
{{ showAdvancedFilter ? '收起' : '展开' }}
<i :class="showAdvancedFilter ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
</el-button>
<el-tooltip content="查询" placement="top" effect="dark" popper-class="icon-btn-tooltip">
<div class="icon-btn icon-btn--search" @click="searchData(true)" aria-label="查询"></div>
</el-tooltip>
<el-tooltip content="重置" placement="top" effect="dark" popper-class="icon-btn-tooltip">
<div class="icon-btn icon-btn--reset" @click="reset" aria-label="重置"></div>
</el-tooltip>
</div>
</div>
<div v-if="showAdvancedFilter" class="filter-row filter-row--advanced advanced-filter">
<div class="filter-fields">
<div class="filter-field filter-field--enabled">
<el-select v-model="params.enabled" placeholder="启停用状态" clearable>
<el-option label="启用" :value="true"></el-option>
<el-option label="停用" :value="false"></el-option>
</el-select>
</div>
<div class="filter-field filter-field--open">
<el-select v-model="params.openCourse" placeholder="是否公开课" clearable>
<el-option label="是" :value="1"></el-option>
<el-option label="否" :value="0"></el-option>
</el-select>
</div>
<div class="filter-field filter-field--resowner">
<el-cascader
ref="resOwnerCascader"
v-model="resOwnerSelected"
placeholder="资源归属"
clearable
:props="resOwnerCascaderProps"
:options="resOwnerOptions"
:show-all-levels="false"
@change="handleResOwnerChange"
@input.native="limitResOwnerInput"
@clear="handleResOwnerClear"
@visible-change="handleResOwnerVisibleChange"
filterable
:filter-method="resOwnerFilterMethod"
></el-cascader>
</div>
<div class="filter-field filter-field--creator creator-filter">
<div class="teacher-select-wrapper">
<el-select
class="creator-select"
ref="creatorSelect"
v-model="creatorSelected"
multiple
filterable
remote
clearable
reserve-keyword
placeholder="创建人"
:multiple-limit="5"
:remote-method="remoteSearchCreator"
:loading="creatorLoading"
@input.native="limitCreatorInput"
@visible-change="handleCreatorVisibleChange"
@change="handleCreatorChange"
@clear="handleCreatorClear"
>
<el-option v-for="item in creatorOptions" :key="item.userId" :label="item.name" :value="item.userId">
<span>{{ item.name }}</span>
<span v-if="item.code" class="option-code">{{ item.code }}</span>
</el-option>
</el-select>
</div>
</div>
<div class="filter-field filter-field--create-from">
<el-select v-model="params.createFrom" placeholder="创建来源" clearable>
<el-option label="教师端" value="teacher"></el-option>
<el-option label="管理端" value="admin"></el-option>
</el-select>
</div>
</div>
<div class="filter-actions filter-actions--inline">
<el-button type="text" class="toggle-link" @click="toggleAdvancedFilter">
{{ showAdvancedFilter ? '收起' : '展开' }}
<i :class="showAdvancedFilter ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
</el-button>
<el-tooltip content="查询" placement="top" effect="dark" popper-class="icon-btn-tooltip">
<div class="icon-btn icon-btn--search" @click="searchData(true)" aria-label="查询"></div>
</el-tooltip>
<el-tooltip content="重置" placement="top" effect="dark" popper-class="icon-btn-tooltip">
<div class="icon-btn icon-btn--reset" @click="reset" aria-label="重置"></div>
</el-tooltip>
</div>
</div>
</div>
<div class="table-wrapper">
<div class="filter-extra-actions">
<div class="create-course-btn" @click="addNewCourse()" aria-label="新建课程">新建课程</div>
<el-tooltip v-if="showSetTopFeature" content="置顶排序" placement="top" effect="dark" popper-class="icon-btn-tooltip">
<div class="icon-btn icon-btn--top" @click="handleTopSort" aria-label="置顶排序"></div>
</el-tooltip>
<el-tooltip content="导出" placement="top" effect="dark" popper-class="icon-btn-tooltip">
<div class="icon-btn icon-btn--export" :class="{ 'is-loading': exportLoading, 'is-disabled': exportLoading }"
@click="!exportLoading && handleExport()" aria-label="导出"></div>
</el-tooltip>
</div>
<el-table :data="pageData" @sort-change="handleSortChange">
<el-table-column v-if="forChoose" label="选择" width="80" align="center">
<template slot-scope="scope" v-if="scope.row.published">
<el-button type="default" size="mini" @click="handleChoose(scope.row)">选择</el-button>
</template>
</el-table-column>
<el-table-column label="课程名称" prop="name" header-align="center" align="left" min-width="360"
show-overflow-tooltip fixed="left" sortable="custom">
<template slot-scope="scope">
<span class="course-name" @click="viewTopic(scope.row)">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="课程分类" prop="sysType" min-width="180" align="center" sortable="custom" show-overflow-tooltip>
<template slot-scope="scope">
<span class="common-cell common-cell-right single-line-ellipsis">{{ formatSysTypeChain(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="授课教师" prop="teacherName" :min-width="teacherColumnWidth" align="center" >
<template slot-scope="scope">
<span class="common-cell">{{ scope.row.teacherName }}</span>
</template>
</el-table-column>
<el-table-column label="课程时长" prop="courseDuration" min-width="110" align="center" sortable="custom">
<template slot-scope="scope">
<span class="common-cell common-cell-right">{{ formatCourseDuration(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="学习人数" prop="studys" min-width="110" align="center" sortable="custom">
<template slot-scope="scope">
<span class="common-cell common-cell-right">{{ scope.row.studys || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="课程评分" prop="score" min-width="110" align="center" sortable="custom">
<template slot-scope="scope">
<span class="common-cell common-cell-right">{{ formatScore(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="审核状态" prop="status" min-width="110" align="center" sortable="custom">
<template slot-scope="scope">
<span class="common-cell common-cell-right" v-if="scope.row.status == 1">-</span>
<span class="common-cell common-cell-right" v-if="scope.row.status == 2">审核中</span>
<span :class="['common-cell common-cell-right', 'status--pass']" v-if="scope.row.status == 5">审核通过</span>
<span :class="['common-cell common-cell-right', 'status--reject']" v-if="scope.row.status == 3">审核驳回</span>
</template>
</el-table-column>
<el-table-column label="发布状态" prop="published" min-width="110" align="center" sortable="custom">
<template slot-scope="scope">
<span class="common-cell common-cell-right">{{ scope.row.published == true ? '已发布' : '未发布' }}</span>
</template>
</el-table-column>
<el-table-column label="启停用状态" prop="enabled" min-width="130" align="center" sortable="custom">
<template slot-scope="scope">
<span class="common-cell common-cell-right">{{ scope.row.enabled == true ? '启用' : '停用' }}</span>
</template>
</el-table-column>
<el-table-column label="公开课" prop="openCourse" min-width="110" align="center" sortable="custom">
<template slot-scope="scope">
<span class="common-cell common-cell-right">{{ scope.row.openCourse == 1 ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column label="资源归属" prop="orgName" min-width="220" align="center" sortable="custom" show-overflow-tooltip>
<template slot-scope="scope">
<el-tooltip :content="scope.row.orgFullName || scope.row.orgName" placement="top" effect="dark">
<span class="common-cell common-cell-right">{{ scope.row.orgName }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="创建人" prop="sysCreateBy" min-width="130" align="center" sortable="custom" show-overflow-tooltip>
<template slot-scope="scope">
<span class="common-cell common-cell-right">{{ scope.row.sysCreateBy }}</span>
</template>
</el-table-column>
<el-table-column label="创建来源" min-width="140" align="center">
<template slot-scope="scope">
<span class="common-cell" v-if="scope.row.createFrom === 'teacher'">教师端</span>
<span class="common-cell" v-else-if="scope.row.createFrom === 'admin'">管理端</span>
<span class="common-cell" v-else>-</span>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="sysCreateTime" min-width="200" show-overflow-tooltip align="center"
sortable="custom">
<template slot-scope="scope">
<span class="common-cell">{{ scope.row.sysCreateTime }}</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="280px" fixed="right" header-align="center" align="left">
<template slot-scope="scope" class="btn-gl">
<template v-if="getInlineActions(scope.row).length">
<span v-for="(action, index) in getInlineActions(scope.row)" :key="action.key">
<el-button
type="text"
size="mini"
:class="action.className"
@click="handleAction(action.key, scope.row)"
>
{{ action.label }}
</el-button>
<el-divider
v-if="index < getInlineActions(scope.row).length - 1 || (index === getInlineActions(scope.row).length - 1 && getDropdownActions(scope.row).length)"
direction="vertical"
></el-divider>
</span>
</template>
<el-dropdown
v-if="getDropdownActions(scope.row).length"
type="text"
size="mini"
style="margin-left:0px"
trigger="click"
>
<el-button type="text" size="mini" class="action-link--more">
更多<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="action in getDropdownActions(scope.row)"
:key="action.key"
:class="action.className"
@click.native="handleAction(action.key, scope.row)"
>
{{ action.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="page.pageIndex" :page-sizes="[10, 20, 30, 40]" :page-size="page.pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="page.count"></el-pagination>
</div>
</div>
<!-- <div style="height: 100px;"></div> -->
<!--邀请审核-->
<el-dialog custom-class="g-dialog" title="邀请教师审核课程" :visible.sync="inviteTeacher.dlgShow">
<div style="display: flex;justify-content:flex-start;padding-bottom: 10px;">
<div style="padding: 0px 5px;"><el-input placeholder="姓名" v-model="inviteTeacher.params.name"></el-input></div>
<div style="padding: 0px 5px;"><el-button @click="findTeachers()" icon="el-icon-search"
type="primary">搜索</el-button></div>
</div>
<div>
<el-table max-height="500" border :data="inviteTeacher.list" style="width: 100%">
<el-table-column prop="name" label="姓名" width="180"></el-table-column>
<el-table-column prop="sex" label="性别"></el-table-column>
<el-table-column prop="code" label="工号"></el-table-column>
<el-table-column prop="orgInfo" label="组织"></el-table-column>
<el-table-column prop="orgInfo" label="选择">
<template slot-scope="scope">
<el-radio v-model="scope.row.checked">选择</el-radio>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="inviteTeacher.dlgShow = false"> </el-button>
<el-button type="primary" @click="enSure">确认</el-button>
</template>
</el-dialog>
<!--课程管理-->
<el-dialog custom-class="g-dialog" title="课程学习管理" width="900px" height="900px" :visible.sync="manageStudy.dlgShow"
:close-on-click-modal="false">
<manager :manageStudyData="manageStudyData" :isShowDialog="manageStudy.dlgShow"></manager>
<template #footer>
<el-button @click="manageStudy.dlgShow = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog title="二维码" :visible.sync="qrCodedialogVisible" width="900px" @close="closeCode" custom-class="g-dialog">
<div>
<el-form size="medium" label-width="100px">
<el-form-item label="二维码">
<div id="qrcode" ref="qrcode" class="qrcode-img" @mouseenter="showDownloadButton = true"
@mouseleave="showDownloadButton = false">
<div v-show="showDownloadButton" @click="downloadQrcode" class="downloadn-container">
<i class="el-icon-download" style="color: #409EFF;font-size:18px;margin-bottom:5px"></i>
<span>下载</span>
</div>
</div>
</el-form-item>
<el-form-item label="链接">
<el-input v-model="copyUrl" readonly class="input-with-select" id="text">
<el-button slot="append" @click="handleCopyUrl">复制</el-button>
</el-input>
</el-form-item>
<el-form-item label="">上述内容兼容PC端与移动端您可按需分享</el-form-item>
</el-form>
</div>
<span slot="footer" class="dialog-footer"><el-button @click="closeCode"> </el-button></span>
</el-dialog>
<!-- 审核 -->
<el-dialog title="审核" :visible.sync="dialogVisible" width="900px" custom-class="g-dialog">
<div v-show="expandDetails">
<div v-if="examin.detailType == 10">
<auditCourse1 :id="examin.examineId"></auditCourse1>
</div>
<div v-if="examin.detailType == 20">
<auditCourse2 :id="examin.examineId"></auditCourse2>
</div>
</div>
<div style="border-top: 1px solid #eee; background-color: #eee; padding: 5px;">
<div style=" text-align: center;margin-bottom: 10px;">
<el-button @click="expandDetails = !expandDetails">{{ expandDetails ? '详情折叠' : '详情展开' }}</el-button>
<el-button @click="isExamine = 1">直接审核</el-button>
<el-button @click="isExamine = 2">邀请审核</el-button>
</div>
<el-form label-width="80px" v-if="isExamine === 1">
<el-form-item label="审核">
<el-radio-group v-model="auditInfo.pass">
<el-radio :label="true">通过</el-radio>
<el-radio :label="false">不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见"><el-input v-model="auditInfo.remark" type="textarea"
rows="3"></el-input></el-form-item>
</el-form>
<div v-if="isExamine === 2">
<div style="display: flex;justify-content:flex-start;padding-bottom: 10px;">
<div style="padding: 0px 5px;"><el-input placeholder="姓名" v-model="inviteTeacher.params.name"></el-input>
</div>
<div style="padding: 0px 5px;"><el-button @click="findTeachers()" icon="el-icon-search"
type="primary">搜索</el-button></div>
</div>
<div>
<el-table v-if="inviteTeacher.list.length !== 0" max-height="500" border :data="inviteTeacher.list"
style="width: 100%;margin-bottom: 10px;">
<el-table-column prop="name" label="姓名" width="180"></el-table-column>
<el-table-column prop="code" label="工号"></el-table-column>
<el-table-column prop="orgInfo" label="组织"></el-table-column>
<el-table-column prop="orgInfo" label="选择">
<template slot-scope="scope">
<el-radio v-model="scope.row.checked">选择</el-radio>
</template>
</el-table-column>
</el-table>
<div>审核记录</div>
<el-table max-height="500" border :data="inviteTeacher.list" style="width: 100%;">
<el-table-column prop="name" label="姓名" width="180"></el-table-column>
<el-table-column prop="code" label="工号"></el-table-column>
<el-table-column prop="orgInfo" label="组织"></el-table-column>
<el-table-column prop="type" label="审核状态"></el-table-column>
<el-table-column prop="text" label="备注"></el-table-column>
</el-table>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="examineData()">提交</el-button>
</span>
</el-dialog>
<el-dialog v-if="showDetails" title="课程详情" :visible.sync="showDetails" @close="examin = {};" width="900px"
custom-class="g-dialog">
<div v-show="expandDetails">
<div v-if="examin.detailType == 10">
<auditCourse1 :showTest="true" :isDetails="false" :isShow="false" :id="examin.examineId"></auditCourse1>
</div>
<div v-if="examin.detailType == 20">
<auditCourse2 :showTest="true" :isDetails="false" :isShow="false" :id="examin.examineId"></auditCourse2>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="showDetails = false; examin = {};"> </el-button>
</span>
</el-dialog>
<div>
<course-form ref="courseForm" @submitSuccess="searchData" @close="searchData"></course-form>
</div>
<top-course-sorter v-if="showSetTopFeature" ref="topSorter" @sorted="onTopSorted"></top-course-sorter>
</div>
</template>
<script>
import courseForm from '@/components/Course/courseForm.vue';
import manager from '@/components/Study/manager.vue';
import QRCode from 'qrcodejs2';
import auditCourse1 from '@/components/Course/auditCourse1.vue';
import auditCourse2 from '@/components/Course/auditCourse2.vue';
import adminPage from '@/components/Administration/adminPage.vue';
import TopCourseSorter from '@/components/Course/TopCourseSorter.vue';
import apiResowner from '../../api/modules/resowner.js';
import apiType from '../../api/modules/type.js'
import { courseType } from '../../utils/tools.js';
import apiCourse from '@/api/modules/course.js';
// import {resOwnerIndexName,sysTypeIndexName} from '@/utils/type.js';
import { mapGetters, mapActions } from 'vuex';
import apiUserbasic from "@/api/boe/userbasic.js"
import apiTeacher from '../../api/modules/teacher.js';
export default {
name: 'manageCourse',
components: { courseForm, manager, auditCourse1, auditCourse2, adminPage, TopCourseSorter },
computed: {
...mapGetters(['resOwnerMap', 'sysTypeMap', 'userInfo', 'identity']),
resOwnerCascaderProps() {
// 搜索模式:关闭懒加载,直接使用全量 options
if (this.resOwnerSearchMode) {
return {
value: 'id',
label: 'name',
children: 'children',
checkStrictly: true, // 允许选择任意一级选项
};
}
// 非搜索模式:开启前端“伪懒加载”,数据从本地缓存树中按需取
return {
value: 'id',
label: 'name',
children: 'children',
lazy: true,
lazyLoad: this.loadResOwnerNodeFromCache,
checkStrictly: true, // 允许选择任意一级选项
};
},
// 动态计算“授课教师”列的最小宽度,避免固定 260px
teacherColumnWidth() {
return this.calcColumnWidth('授课教师', 'teacherName');
}
},
data() {
return {
audiences: [],
forChoose: false,
pageManage: false,
showDetails: false,
examin: {
detailType: '',
examineId: '',
examineName: '',
},
paperJson: { items: [] },
courseType: courseType,
sysTypeListMap: [],
sysTypeList: [],
resOwnerListMap: [],
resOwnerSelected: [],
// 资源归属全量树 & 级联选项
resOwnerTreeAll: [],
resOwnerOptions: [],
// 资源归属搜索模式开关(有关键字时为 true
resOwnerSearchMode: false,
showSetTopFeature: false,
page: {
pageIndex: 1,//第几页
pageSize: 10, // 每页多少条
count: 0
},
exportLoading: false,
resOwner: [],
resOwnerFilterWarned: false,
orgId: '',
orgName: '',
defaultProps: {
value: 'code',
label: 'name',
},
defaultTypeProps: {
value: 'id',
label: 'name',
},
showAdvancedFilter: false,
learningTimeRange: [],
pickerOptions: { shortcuts: [] },
manageStudyData: {},
expandDetails: true,
qrCodedialogVisible: false,
copyUrl: '',
qrcodeImgUrl: '',
showDownloadButton: false,
isExamine: 1,
auditInfo: {
pass: true,
remark: ''
},
dialogVisible: false,
currentPage4: 4,
inviteTeacher: {
//邀请
dlgShow: false,
params: { name: '' },
list: []
},
manageStudy: {
dlgShow: false
},
params: {
name: '',
teacherId: '',
learningTimeStart: '',
learningTimeEnd: '',
status: '',
publish: '',
enabled: '',
openCourse: '',
createUserId: '',
createFrom: '',
},
orderField: '', // 排序字段
orderAsc: true, // 排序顺序true-升序false-降序
pageData: [],
courseChooseShow: false,
courseChooseId: '',
teacherFilterList: [],
teacherSelected: [],
teacherOptions: [],
teacherLoading: false,
creatorSelected: [],
creatorOptions: [],
creatorLoading: false,
courseTypes: [
{ id: '1', img: this.webBaseUrl + '/images/ctype1.png', name: '微课', info: '一种单一课件的课程' },
{ id: '2', img: this.webBaseUrl + '/images/ctype2.png', name: '在线课', info: '有章节多课件的课程' },
{ id: '3', img: this.webBaseUrl + '/images/ctype3.png', name: '课程包', info: '微课和在线课组成' },
{ id: '4', img: this.webBaseUrl + '/images/ctype4.png', name: '线下课', info: 'XXXX' },
{ id: '5', img: this.webBaseUrl + '/images/ctype5.png', name: '直播课', info: 'XXXX' }
],
weike: {
onlyRequired: false,
dlgShow: false,
fileType: '',
info: {
shebei: ''
}
},
biaoke: {
dlgShow: false
},
recommend: {
dlgShow: false,
},
catalogs: {
addNewZhang: false,
addNewCell: false,
},
extendRefId: '',
extendRefType: '',
scrollbarStyleApplied: false,
};
},
created() {
this.pickerOptions = this.buildPickerOptions();
},
mounted() {
this.getAudiences()
let chooseFlag = this.$route.query.f;
this.extendRefId = this.$route.query.refId;
this.extendRefType = this.$route.query.refType;
if (chooseFlag && chooseFlag == 'choose') {
this.forChoose = true;
}
if (this.$route.query && this.$route.query.page && this.$route.query.page == 'manage') {
this.pageManage = true;
}
if (this.$route.query && this.$route.query.open && this.$route.query.open == 'new') {
this.addNewCourse();
}
this.loadShowSetTopFlag();
// this.getTree();
// this.getTypeData();
// this.searchData();
this.getResOwnerTree().then(rs => {
this.resOwnerListMap = rs;
});
// 取消全局课程分类
this.getSysTypeTree().then(rs => {
this.sysTypeListMap = rs;
})
//已经加载tree的情况下不需要再单独的加载一次
this.loadResOwners();
this.loadSysTypes();
// 加载资源归属全量树,供级联和搜索使用
this.loadAllResOwnerTree();
document.querySelector('#app').style.overflowX = 'hidden';
this.applyAppScrollbarStyle();
},
methods: {
toggleAdvancedFilter() {
this.showAdvancedFilter = !this.showAdvancedFilter;
},
// 计算文本宽度(通过隐藏 span 获取实际宽度)
getTextWidth(text = '') {
if (typeof document === 'undefined') return 0;
const span = document.createElement('span');
span.innerText = text;
span.style.cssText = 'position:absolute;visibility:hidden;font-size:14px;font-family:inherit;white-space:nowrap;';
document.body.appendChild(span);
const { width } = span.getBoundingClientRect();
document.body.removeChild(span);
return Math.ceil(width);
},
// 计算列宽(头+内容取最大值,加 padding并控制最小值
calcColumnWidth(label, prop, padding = 24, min = 260, max = Infinity) {
const contents = (this.pageData || []).map(row => this.getTextWidth((row && row[prop]) || ''));
const maxContentWidth = contents.length ? Math.max(...contents) : 0;
const labelWidth = this.getTextWidth(label || '');
const baseWidth = Math.max(maxContentWidth, labelWidth) + padding;
const clamped = Math.max(min, Math.min(baseWidth, max));
return `${clamped}px`;
},
applyAppScrollbarStyle() {
if (this.scrollbarStyleApplied || typeof document === 'undefined') return;
if (document.getElementById('app-scrollbar-style')) {
this.scrollbarStyleApplied = true;
return;
}
const style = document.createElement('style');
style.id = 'app-scrollbar-style';
style.innerHTML = `
#app::-webkit-scrollbar {
width: 6px;
height: 8px;
}
#app::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: #4284F7;
}
#app::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color:rgba(0, 0, 0, .1);
border-radius: 4px;
}
`;
document.head.appendChild(style);
this.scrollbarStyleApplied = true;
},
async loadShowSetTopFlag() {
try {
const res = await apiCourse.showSetTop();
this.showSetTopFeature = res && res.status === 200 && res.result === true;
} catch (error) {
this.showSetTopFeature = false;
}
},
async remoteSearchTeacher(keyword) {
const limited = (keyword || '').slice(0, 50);
if (this.$refs.teacherSelect && this.$refs.teacherSelect.query !== limited) {
this.$refs.teacherSelect.query = limited;
this.$nextTick(() => {
if (this.$refs.teacherSelect && this.$refs.teacherSelect.$refs && this.$refs.teacherSelect.$refs.input) {
this.$refs.teacherSelect.$refs.input.value = limited;
}
});
}
const query = limited.trim();
if (!query || query.length <= 1) {
this.teacherOptions = [];
return;
}
this.teacherLoading = true;
try {
const res = await apiTeacher.findByNameNew(query);
if (res && res.code === 200) {
const teacherList = res.data || [];
this.teacherOptions = teacherList
.map(item => this.formatTeacherItem(item))
.filter(item => item.teacherId);
} else {
this.teacherOptions = [];
}
} catch (error) {
this.teacherOptions = [];
} finally {
this.teacherLoading = false;
}
},
formatTeacherItem(item = {}) {
return {
teacherId: item.id,
teacherName: item.name,
teacherCode: item.mobile || '',
};
},
formatTeacherLabel(item = {}) {
if (!item.teacherName) {
return '';
}
return item.teacherCode ? `${item.teacherName}(${item.teacherCode})` : item.teacherName;
},
handleTeacherChange(value = []) {
this.params.teacherId = (value || []).join(',');
},
handleTeacherClear() {
this.teacherSelected = [];
this.teacherOptions = [];
this.params.teacherId = '';
},
// 授课教师下拉展开时,如果当前没有关键字,则清空上一次的查询结果
handleTeacherVisibleChange(visible) {
if (!visible) return;
// 打开时才处理
const select = this.$refs.teacherSelect;
const query = (select && select.query) || '';
if (!query) {
this.teacherOptions = [];
}
},
async remoteSearchCreator(keyword) {
const limited = (keyword || '').slice(0, 50);
if (this.$refs.creatorSelect && this.$refs.creatorSelect.query !== limited) {
this.$refs.creatorSelect.query = limited;
this.$nextTick(() => {
if (this.$refs.creatorSelect && this.$refs.creatorSelect.$refs && this.$refs.creatorSelect.$refs.input) {
this.$refs.creatorSelect.$refs.input.value = limited;
}
});
}
const query = limited.trim();
if (!query || query.length <= 1) {
this.creatorOptions = [];
return;
}
this.creatorLoading = true;
try {
const res = await apiUserbasic.selectUser(query);
if (res && res.status === 200) {
const resultList = res.result || [];
this.creatorOptions = resultList
.map(item => this.formatCreatorItem(item))
.filter(item => item.userId);
} else {
this.creatorOptions = [];
}
} catch (error) {
this.creatorOptions = [];
} finally {
this.creatorLoading = false;
}
},
formatCreatorItem(item = {}) {
return {
userId: item.id,
name: item.realName,
code: item.userNo,
};
},
formatCreatorLabel(item = {}) {
if (!item.name) {
return '';
}
return item.code ? `${item.name}(${item.code})` : item.name;
},
handleCreatorChange(value = []) {
this.params.createUserId = (value || []).slice(0, 5).join(',');
},
handleCreatorClear() {
this.creatorSelected = [];
this.params.createUserId = '';
},
// 创建人下拉展开时,如果当前没有关键字,则清空上一次的查询结果
handleCreatorVisibleChange(visible) {
if (!visible) return;
const select = this.$refs.creatorSelect;
const query = (select && select.query) || '';
if (!query) {
this.creatorOptions = [];
}
},
limitTeacherInput(event) {
const limited = (event && event.target && event.target.value ? event.target.value : '').slice(0, 50);
if (event && event.target && event.target.value !== limited) {
event.target.value = limited;
}
if (this.$refs.teacherSelect) {
this.$refs.teacherSelect.query = limited;
}
},
limitCreatorInput(event) {
const limited = (event && event.target && event.target.value ? event.target.value : '').slice(0, 50);
if (event && event.target && event.target.value !== limited) {
event.target.value = limited;
}
if (this.$refs.creatorSelect) {
this.$refs.creatorSelect.query = limited;
}
},
limitResOwnerInput(event) {
const limited = (event && event.target && event.target.value ? event.target.value : '').slice(0, 200);
if (event && event.target && event.target.value !== limited) {
event.target.value = limited;
}
const keyword = (limited || '').trim();
// 根据是否有关键字切换搜索模式
this.resOwnerSearchMode = !!keyword;
if (this.resOwnerSearchMode) {
// 搜索模式:使用全量树作为 options交给 filter-method 过滤
this.resOwnerOptions = this.resOwnerTreeAll;
} else {
// 非搜索模式:清空 options交给 lazyLoad 从缓存树按需“懒加载”
this.resOwnerOptions = [];
}
},
// 清空选择时,恢复到懒加载模式
handleResOwnerClear() {
this.resOwnerSearchMode = false;
this.resOwnerOptions = [];
// 清空输入框的文字,避免残留关键字
this.$nextTick(() => {
if (this.$refs.resOwnerCascader && this.$refs.resOwnerCascader.inputValue !== undefined) {
this.$refs.resOwnerCascader.inputValue = '';
}
});
},
// 下拉面板打开时,如果当前没有关键字,也确保是懒加载模式
handleResOwnerVisibleChange(visible) {
if (!visible) return;
// 如果没有关键字,就强制回到懒加载模式
const keyword = (this.$refs.resOwnerCascader && this.$refs.resOwnerCascader.inputValue) || '';
if (!keyword) {
this.resOwnerSearchMode = false;
this.resOwnerOptions = [];
}
},
resOwnerFilterMethod(node, keyword) {
if (!keyword) return true;
const text = (node.label || (node.data && node.data.name) || '').toString().toLowerCase();
const kw = keyword.toString().toLowerCase();
return text.includes(kw);
},
handleTopSort() {
if (this.$refs.topSorter) {
this.$refs.topSorter.open();
}
},
onTopSorted() {
this.searchData(true);
},
handleLearningTimeRangeChange(val) {
if (val && val.length === 2) {
this.params.learningTimeStart = val[0];
this.params.learningTimeEnd = val[1];
} else {
this.params.learningTimeStart = '';
this.params.learningTimeEnd = '';
}
},
buildPickerOptions() {
const shortcutConfigs = [
{
text: '今年以来',
range: () => {
const end = new Date();
const start = new Date(end.getFullYear(), 0, 1);
return [start, end];
}
},
{
text: '最近一年',
range: () => {
const end = new Date();
const start = new Date();
start.setTime(end.getTime() - 3600 * 1000 * 24 * 365);
return [start, end];
}
},
{
text: '最近三个月',
range: () => {
const end = new Date();
const start = new Date();
start.setTime(end.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
}
},
{
text: '最近一个月',
range: () => {
const end = new Date();
const start = new Date();
start.setTime(end.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
}
},
{
text: '最近一周',
range: () => {
const end = new Date();
const start = new Date();
start.setTime(end.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
}
}
];
return {
shortcuts: shortcutConfigs.map(item => ({
text: item.text,
onClick: picker => {
picker.$emit('pick', item.range());
}
}))
};
},
validateLearningTimeRange() {
const hasStart = !!this.params.learningTimeStart;
const hasEnd = !!this.params.learningTimeEnd;
if ((hasStart && !hasEnd) || (!hasStart && hasEnd)) {
this.$showMessage('请选择完整的培训时间范围', 'warning');
return false;
}
return true;
},
formatCourseDuration(row) {
// if (row.durationDesc) {
// return row.durationDesc;
// }
const duration = row.courseDuration
if (duration === undefined || duration === null || duration === 0) {
return '0';
}
const minutes = Math.round(duration / 60);
//if (minutes >= 60) {
//const hours = (minutes / 60).toFixed(1);
//return `${hours}小时`;
//}
return `${minutes || 0}`;
},
formatStudyCount(row) {
const count = row.studyCount !== undefined
? row.studyCount
: (row.studyNum !== undefined ? row.studyNum : (row.learners !== undefined ? row.learners : row.studyUsers));
if (count === undefined || count === null) {
return '-';
}
return count;
},
formatScore(row) {
let score;
// 优先取score其次取avgScore
if (row.score !== undefined && (row.score || row.score === 0)) {
score = row.score;
} else if (row.avgScore !== undefined && (row.avgScore || row.avgScore === 0)) {
score = row.avgScore;
} else {
return '-';
}
// 处理数值四舍五入保留1位小数
// 先转换为数字类型,避免字符串格式的数值问题
const numScore = Number(score);
// 检查是否为有效数字
if (isNaN(numScore)) {
return '-';
}
// 四舍五入保留1位小数
return numScore.toFixed(1);
},
getDurationNumber(row) {
const duration = row.duration !== undefined
? row.duration
: (row.studyDuration !== undefined ? row.studyDuration : row.totalDuration);
if (duration === undefined || duration === null) {
return 0;
}
return Number(duration) || 0;
},
sortByDuration(a, b) {
return this.getDurationNumber(a) - this.getDurationNumber(b);
},
formatSysTypeChain(row = {}) {
const codes = [row.sysType1, row.sysType2, row.sysType3];
const names = codes
.filter(code => code !== undefined && code !== null && code !== '')
.map(code => this.sysTypeName(code))
.filter(name => name && name !== '');
return names.length ? names.join('/') : '';
},
sysTypeName(code) {
// console.log('code', code);
// console.log('this.sysTypeMap', this.sysTypeMap);
if (code == '') { return ''; }
return this.sysTypeMap.get(code);
},
getStudyCountNumber(row) {
const count = row.studyCount !== undefined
? row.studyCount
: (row.studyNum !== undefined ? row.studyNum : (row.learners !== undefined ? row.learners : row.studyUsers));
if (count === undefined || count === null) {
return 0;
}
return Number(count) || 0;
},
sortByStudyCount(a, b) {
return this.getStudyCountNumber(a) - this.getStudyCountNumber(b);
},
getScoreNumber(row) {
if (row.score || row.score === 0) {
return Number(row.score) || 0;
}
if (row.avgScore || row.avgScore === 0) {
return Number(row.avgScore) || 0;
}
return 0;
},
sortByScore(a, b) {
return this.getScoreNumber(a) - this.getScoreNumber(b);
},
getOrderNumber(row) {
if (row.orderIndex || row.orderIndex === 0) {
return Number(row.orderIndex) || 0;
}
if (row.orderValue || row.orderValue === 0) {
return Number(row.orderValue) || 0;
}
if (row.sortIndex || row.sortIndex === 0) {
return Number(row.sortIndex) || 0;
}
return 0;
},
sortByOrderValue(a, b) {
return this.getOrderNumber(a) - this.getOrderNumber(b);
},
formatOrderValue(row) {
if (row.orderIndex || row.orderIndex === 0) {
return row.orderIndex;
}
if (row.orderValue || row.orderValue === 0) {
return row.orderValue;
}
if (row.sortIndex || row.sortIndex === 0) {
return row.sortIndex;
}
return '-';
},
buildQueryParams() {
const query = {
...this.params,
pageIndex: this.page.pageIndex,
pageSize: this.page.pageSize,
};
Object.keys(query).forEach(key => {
if (query[key] === '' || query[key] === null) {
delete query[key];
}
});
const [sysOne = '', sysTwo = '', sysThree = ''] = this.sysTypeList || [];
query.sysType1 = sysOne;
query.sysType2 = sysTwo;
query.sysType3 = sysThree;
if (this.orgId) {
query.orgId = this.orgId;
}
if (this.params.name) {
query.keyword = this.params.name;
}
if (this.$route.query.courseIds) query.courseIds = this.$route.query.courseIds.split(',');
if (this.$route.query.projectId) query.projectId = this.$route.query.projectId;
if (this.audiences && this.audiences.length > 0) {
query.audiences = this.audiences.join(',');
}
query.isCreateCourse = !this.pageManage;
// 添加排序参数
if (this.orderField) {
query.orderField = this.orderField;
query.orderAsc = this.orderAsc;
}
return query;
},
async handleExport() {
if (!this.validateLearningTimeRange()) {
return;
}
const query = this.buildQueryParams();
delete query.pageIndex;
delete query.pageSize;
this.exportLoading = true;
try {
const blob = await apiCourse.exportCourse(query);
if (!(blob instanceof Blob)) {
throw new Error('导出失败');
}
const link = document.createElement('a');
const url = window.URL.createObjectURL(blob);
const timestamp = new Date().toISOString().replace(/[-:T]/g, '').split('.')[0];
link.href = url;
link.download = `在线课列表.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
this.$showMessage(error.message || '导出失败', 'error');
} finally {
this.exportLoading = false;
}
},
getAudiences() {
apiUserbasic.getInAudienceIds().then(res => {
if (res.status == 200) {
this.audiences = res.result;
}
this.searchData();
})
},
handleChoose(row) { //选择课程
window.parent.selectCourse(row);
},
// 置顶
setTop(row) {
let params = {
ids: row.id,//课程id,多个使用逗号分隔,
title: row.name,//课程的名称,
top: !row.isTop,// top 是否置顶}
}
console.log(row.isTop);
if (row.isTop == false) {
// console.log('fa')
apiCourse.setTop(params).then(res => {
if (res.status === 200 && res.result === true) {
this.$showMessage('置顶成功!', 'success')
this.searchData();
} else if (res.status === 500) {
const confirmText = `<i class="el-icon-warning-outline"></i>已置顶10条课程若需继续置顶请对部分课程执行取消置顶操作`;
this.$confirm(confirmText, '置顶确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true,
type: 'warning',
customClass: 'custom-confirm-dialog'
}).then(() => {}).catch(() => { });
} else {
this.$showMessage(res.message, 'error');
}
})
} else if (row.isTop == true) {
apiCourse.setTop(params).then(res => {
if (res.status === 200 && res.result === true) {
this.$showMessage('取消成功!', 'success')
this.searchData();
} else {
this.$showMessage(res.message, 'error');
}
})
}
},
// 复制
copyCourse(item) {
const confirmText = `<i class="el-icon-warning-outline"></i>确认复制${item.name}吗?`;
this.$confirm(confirmText, '复制确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true,
type: 'warning',
customClass: 'custom-confirm-dialog'
}).then(() => {
const reqdata = {
id: item.id,
refId: this.extendRefId,
refType: this.extendRefType,
};
apiCourse.copyCourse(reqdata).then(rs => {
if (rs.status === 200) {
this.$showMessage('复制成功', 'success');
this.searchData();
} else {
this.$showMessage(rs.message || '复制失败', 'error');
}
}).catch(() => {
this.$showMessage('复制失败', 'error');
});
}).catch(() => { });
},
// 撤回接口
withdraw(row) {
this.$confirm(`确定撤回${row.name}的审核申请吗?`, '撤回确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: 'custom-confirm-dialog'
}).then(() => {
apiCourse.revokeSubmit(row.id).then((res) => {
if (res.status === 200 && res.result) {
this.$showMessage('撤回成功', 'success');
this.searchData();
} else {
this.$showMessage(res.message || '撤回失败', 'error');
}
}).catch(() => {
this.$showMessage('撤回失败', 'error');
});
}).catch(() => { });
},
reset() {
this.resOwner = [];
this.sysTypeList = [];
this.learningTimeRange = [];
this.orgId = '';
this.orgName = '';
this.resOwnerSelected = [];
this.teacherFilterList = [];
this.teacherSelected = [];
this.teacherOptions = [];
this.teacherLoading = false;
this.creatorSelected = [];
this.creatorOptions = [];
Object.assign(this.params, {
name: '',
teacherId: '',
learningTimeStart: '',
learningTimeEnd: '',
status: '',
publish: '',
enabled: '',
openCourse: '',
createUserId: '',
createFrom: '',
});
// 重置排序
this.orderField = '';
this.orderAsc = true;
this.searchData(true);
},
...mapActions({
getResOwnerTree: 'resOwner/getResOwnerTree',
loadResOwners: 'resOwner/loadResOwners',
getSysTypeTree: 'sysType/getSysTypeTree',
loadSysTypes: 'sysType/loadSysTypes'
}),
resOwnerName(code) {
if (code == '') { return ''; }
return this.resOwnerMap.get(code);
},
// 直接审核
examineData() {
if (this.isExamine == 1) {
let params = {
id: this.examin.examineId,//课程id,
title: this.examin.examineName,//课程的名称,
pass: this.auditInfo.pass,//Boolean 是否通过,
remark: this.auditInfo.remark,// 备注
}
apiCourse.audit(params).then(res => {
if (res.status === 200) {
this.$showMessage('操作成功!', 'success');
this.dialogVisible = false;
this.searchData();
} else {
this.$showMessage(res.message, 'error');
}
})
} else {
this.$showMessage('暂未开放!', 'warning');
}
},
addNewCourse() {
this.$refs.courseForm.initShow();
},
editCurriculum(row) {
this.$refs.courseForm.initShow(row);
},
// 课程启停
async isDisable(row) {
const nextEnabled = !row.enabled;
const actionText = nextEnabled ? '启用' : '停用';
try {
await this.$confirm(`<i class="el-icon-warning-outline"></i>确定${actionText}${row.name}吗?`, `${actionText}确认`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true,
customClass: 'custom-confirm-dialog'
});
} catch (error) {
return;
}
const params = {
ids: row.id,
title: row.name,
enabled: nextEnabled,
};
try {
const { status } = await apiCourse.setEnabled(params);
if (status === 200) {
this.$showMessage(`${actionText}成功`, 'success');
row.enabled = nextEnabled;
} else {
this.$showMessage(`${actionText}失败`, 'error');
}
} catch (error) {
this.$showMessage(`${actionText}失败`, 'error');
}
},
// 课程查询
searchData(pageReset) {
if (pageReset) {
this.page.pageIndex = 1;
}
if (!this.validateLearningTimeRange()) {
return;
}
console.log('apiCourse', apiCourse);
console.log('buildQueryParams', this.buildQueryParams());
const query = this.buildQueryParams();
apiCourse.managePage(query).then(rs => {
if (rs.status == 200) {
this.pageData = rs.result.list ? rs.result.list : [];
this.page.count = rs.result.count;
this.page.pageSize = rs.result.pageSize;
} else {
return this.$showMessage(rs.message, 'error');
}
}).catch(err => {
console.error('查询课程列表失败:', err);
this.$showMessage('查询失败,请稍后重试', 'error');
});
},
// 表格排序变化事件
handleSortChange({ column, prop, order }) {
// order: ascending(升序) | descending(降序) | null(取消排序)
console.log('排序变化:', { column, prop, order });
if (order) {
// 有排序:保存排序字段和顺序
this.orderField = prop;
this.orderAsc = order === 'ascending';
} else {
// 取消排序:清空排序字段
this.orderField = '';
this.orderAsc = true;
}
// 重新查询数据
this.searchData(true);
},
viewTopic(row) {
if (row.status == 1) {
return this.$showMessage('请提交课程再预览!', 'warning')
}
this.examin = {};
this.examin.detailType = row.type;
this.examin.examineId = row.id;
this.examin.examineName = row.name;
this.showDetails = true;
},
jumpDetails() {
// this.$router.push('/course/micro');
// 跳转打开新页面
let routeData = this.$router.resolve({ path: '/course/detail' }); // , query: { id: 1 }
window.open(this.webBaseUrl + routeData.href, '_blank');
},
toExamine(row) {
this.auditInfo = { pass: true };
this.examin.detailType = row.type;
this.examin.examineId = row.id;
this.examin.examineName = row.name;
this.dialogVisible = true;
},
enSure() {
// 确认事件
},
handleSizeChange(val) {
this.page.pageSize = val;
this.page.pageIndex = 1;
this.searchData();
},
handleCurrentChange(val) {
this.page.pageIndex = val;
this.searchData();
},
chooseInvite(row) {
//邀请老师审核
this.inviteTeacher.dlgShow = true;
},
findTeachers() {
this.inviteTeacher.list = [
{ id: '1', name: '李玉冰', type: '通过', text: '实用', sex: '男', code: '1023123', orgInfo: '教育技术中心', checked: false },
{ id: '2', name: '李玉冰', type: '未通过', text: '内容在调整', sex: '男', code: '1023123', orgInfo: '教育技术中心', checked: false },
{ id: '3', name: '李玉冰', type: '驳回', text: '内容重复', sex: '男', code: '1023123', orgInfo: '教育技术中心', checked: false }
];
},
showQrimage(row) {
// 使用本页二维码弹窗逻辑
this.qrCodedialogVisible = true;
// 与 TeacherList 保持一致的二维码生成逻辑
this.copyUrl = this.qrcodeImgUrl = `${process.env.VUE_APP_BOE_WEB_URL}/systemapi/xboe/m/course/manage/redirectDetail?courseId=${row.id}`;
this.$nextTick(() => {
this.crateQrcode();
});
},
handleCopyUrl() {
const ele = document.getElementById('text');
if (!ele) return;
ele.select();
document.execCommand('Copy');
this.$showMessage('复制成功', 'success');
},
downloadQrcode() {
const container = document.getElementById('qrcode');
if (!container) return;
const img = container.getElementsByTagName('img')[0];
if (!img) return;
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const tempUrl = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.style.display = 'none';
link.href = tempUrl;
link.setAttribute('download', '二维码.jpg');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
crateQrcode() {
// eslint-disable-next-line no-new
new QRCode('qrcode', {
width: 150,
height: 150,
text: this.qrcodeImgUrl
});
},
// 关闭弹框,清除已经生成的二维码
closeCode() {
this.qrCodedialogVisible = false;
// 逐个节点移除,避免事件丢失
const images = document.querySelectorAll('.qrcode-img img');
images.forEach(img => img.remove());
const canvas = document.querySelectorAll('.qrcode-img canvas');
canvas.forEach(c => c.remove());
},
showManageStudy(row) {
// 带课程详情跳转到课程管理页
sessionStorage.setItem('courseDetail', JSON.stringify(row));
this.$router.push({ path: '/iframe/course/coursemanage-remote' });
},
// 前端“伪懒加载”:根据本地全量机构树按需返回子节点
// 为了让 el-cascader 的 loading 动画有可见效果,这里故意加入一个很小的延时再 resolve
async loadResOwnerNodeFromCache(node, resolve) {
try {
const delayResolve = (nodes) => {
// 适当延时 150ms让 loading 动画可见,但又不会让用户感觉明显卡顿
setTimeout(() => resolve(nodes), 150);
};
// 根节点level === 0返回所有一级机构
if (node.level === 0) {
return delayResolve(this.resOwnerTreeAll || []);
}
// 其他层级:根据当前节点 id 在树中查找对应节点,再返回其 children
const currentId = node.value || (node.data && node.data.id);
if (!currentId) {
return delayResolve([]);
}
const findNodeById = (list = [], id) => {
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item.id === id) {
return item;
}
if (Array.isArray(item.children) && item.children.length > 0) {
const found = findNodeById(item.children, id);
if (found) return found;
}
}
return null;
};
const target = findNodeById(this.resOwnerTreeAll || [], currentId);
if (target && Array.isArray(target.children)) {
delayResolve(target.children);
} else {
delayResolve([]);
}
} catch (e) {
console.error('本地懒加载资源归属节点失败:', e);
resolve([]);
}
},
handleResOwnerChange(value) {
if (!value || value.length === 0) {
this.orgId = '';
this.orgName = '';
return;
}
// value 是选中的路径数组最后一个元素是选中的节点ID
const selectedId = value[value.length - 1];
this.orgId = selectedId;
// 通过级联选择器的 getCheckedNodes 方法获取选中的节点信息
this.$nextTick(() => {
if (this.$refs.resOwnerCascader) {
const checkedNodes = this.$refs.resOwnerCascader.getCheckedNodes();
if (checkedNodes && checkedNodes.length > 0) {
const lastNode = checkedNodes[checkedNodes.length - 1];
this.orgName = lastNode.label || lastNode.name || '';
}
}
});
},
// 将后端机构节点转换为级联组件所需结构
mapOrgToCascaderNode(node = {}) {
const children = Array.isArray(node.childList)
? node.childList.map(child => this.mapOrgToCascaderNode(child))
: [];
return {
id: node.organizationId,
name: node.orgName,
children,
};
},
// 加载资源归属全量机构树
async loadAllResOwnerTree() {
try {
const res = await apiUserbasic.getAllOrgTree();
if (res && res.status === 200 && res.result && Array.isArray(res.result.orgTreeList)) {
this.resOwnerTreeAll = res.result.orgTreeList.map(item => this.mapOrgToCascaderNode(item));
// 默认进入非搜索模式,由懒加载从本地树按需返回节点
this.resOwnerOptions = [];
} else if (res && res.result && Array.isArray(res.result.orgTreeList)) {
// 兼容没有 status 字段但有 result 的情况
this.resOwnerTreeAll = res.result.orgTreeList.map(item => this.mapOrgToCascaderNode(item));
this.resOwnerOptions = [];
} else {
this.resOwnerTreeAll = [];
this.resOwnerOptions = [];
}
} catch (error) {
console.error('加载资源归属全量树失败:', error);
this.resOwnerTreeAll = [];
this.resOwnerOptions = [];
}
},
showChooseCourse() {
this.courseChooseShow = true;
},
chooseCourseType(item, idx) {
this.courseChooseId = item.id;
},
toInputCourse() {
if (this.courseChooseId == '1') {
this.showWeike();
} else if (this.courseChooseId == '2') {
this.showBiaoke();
}
this.courseChooseShow = false;
},
newHandleClick() { },
showRecords(item) {
this.recommend.dlgShow = true;
},
showWeike() {
this.weike.dlgShow = true;
},
showBiaoke() {
this.biaoke.dlgShow = true;
},
// setTop(item, idx) {
// let msg = '已设置置顶';
// if (item.isTop) {
// item.isTop = false;
// msg = '已取消置顶';
// } else {
// item.isTop = true;
// }
// this.$showMessage('xxx', 'success');
// },
delItem(row) {
// this.$showMessage('删除成功', 'success');
// return false
this.$confirm(`<i class="el-icon-warning-outline"></i>确认删除${row.name}吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true,
type: 'warning',
customClass: 'custom-confirm-dialog'
}).then(async () => {
let params = {
id: row.id,
title: row.name,
};
try {
// {id:课程id,多个使用逗号分隔,Boolean erasable 是否物理删除,title:课程的名称, remark 备注}
const { status } = await apiCourse.del(params);
if (status === 200) {
this.$showMessage('删除成功', 'success'); //只是之前发布过的课程删除才可
// if(!row.erasable){
// let event = {
// key: "CourseDelete",//被管理员删除
// title: '被管理员删除课程',//事件的标题
// parameters:"author:"+row.sysCreateAid,//作者,一般这种情况不是管理员自己添加的
// content: '被管理员删除课程',//事件的内容
// objId: row.id,//关联的id
// objType: "1",//关联的类型
// objInfo:row.name,
// aid: this.userInfo.aid, //当前登录人的id
// aname: this.userInfo.name,//当前人的姓名
// status: 1 //状态直接写1
// }
// this.$store.dispatch("userTrigger", event);
// }
this.searchData();
}
} catch (error) {
this.$showMessage('删除失败', 'error');
}
})
.catch((err) => {
// this.$showMessage('已取消删除', 'info');
});
},
showAddCatalogZhang(bal) {
this.catalogs.addNewZhang = bal;
},
saveNewCatalogZhang() {
this.catalogs.addNewZhang = false;
},
handleAction(key, row) {
switch (key) {
case 'edit':
return this.editCurriculum(row);
case 'qrcode':
return this.showQrimage(row);
case 'manage':
return this.showManageStudy(row);
case 'withdraw':
return this.withdraw(row);
case 'delete':
return this.delItem(row);
case 'copy':
return this.copyCourse(row);
case 'toggleEnable':
return this.isDisable(row);
case 'toggleTop':
return this.setTop(row);
default:
return;
}
},
buildActions(row) {
const actions = [];
// 优先级按原有展示顺序
if (row.isPermission && row.status != 2) {
actions.push({ key: 'edit', label: '编辑', className: 'action-link--primary' });
}
if (row.published) {
actions.push({ key: 'qrcode', label: '二维码', className: 'action-link--primary' });
}
if (row.isPermission && !this.forChoose && row.published) {
actions.push({ key: 'manage', label: '管理', className: 'action-link--primary' });
}
if (row.isPermission && !this.forChoose && row.status == 2) {
actions.push({ key: 'withdraw', label: '撤回', className: 'action-link--primary' });
}
if ((row.isPermission && row.status != 2 && !row.published) || (row.isPermission && !row.enabled)) {
actions.push({ key: 'delete', label: '删除', className: 'action-link--danger' });
}
// 更多里的动作
if (row.isPermission && row.published) {
actions.push({ key: 'copy', label: '复制', className: 'action-link--primary' });
actions.push({ key: 'toggleEnable', label: row.enabled ? '停用' : '启用', className: 'action-link--primary' });
if (this.showSetTopFeature) {
actions.push({ key: 'toggleTop', label: row.isTop ? '取消置顶' : '置顶', className: 'action-link--primary' });
}
}
return actions;
},
getInlineActions(row) {
const actions = this.buildActions(row);
return actions.slice(0, 4);
},
getDropdownActions(row) {
const actions = this.buildActions(row);
return actions.length > 4 ? actions.slice(4) : [];
}
}
};
</script>
<style lang="scss" scoped>
#app::-webkit-scrollbar {
width: 6px;
height: 8px;
}
#app::-webkit-scrollbar-thumb {
border-radius: 6px;
background-color: rgb(78, 166, 255);
}
.noSplitDatePicker {
/* 初始隐藏范围选择器的分隔符与关闭图标 */
::v-deep .el-range-separator,
::v-deep .el-range__close-icon {
display: none !important;
}
}
// .resetDatePicker {
// ::v-deep .el-date-editor {
// width: 250px;
// }
// }
.sou {
padding: 0 0 0 0px !important;
}
.el-col {
padding: 0 0 0 10px !important;
}
.table-wrapper {
padding: 16px;
background-color: #ffffff;
margin-top: 10px;
border-radius: 6px;
}
.grid-content {
padding-right: 0px;
}
.org-name-cell {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.el-button--text {
font-size: 14px;
}
.filter-wrapper {
padding: 16px;
background-color: #ffffff;
border-radius: 6px;
}
.filter-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
// margin-bottom: 10px;
}
.filter-fields {
display: flex;
flex-wrap: wrap;
}
.filter-field {
margin-right: 10px;
margin-bottom: 0;
}
.filter-row--advanced {
margin-top: 10px;
}
.toggle-link {
padding: 0;
font-size: 14px;
color: #4284F7;
}
.option-code {
margin-left: 4px;
color: #999;
}
.teacher-filter {
::v-deep .choice {
width: 100%;
}
}
.creator-filter {
::v-deep .choice {
width: 100%;
}
}
.filter-actions--inline {
padding-top: 0;
}
.filter-actions .el-button+.el-button {
margin-left: 10px;
}
.filter-actions {
display: flex;
align-items: center;
}
.filter-actions .icon-btn+.icon-btn {
margin-left: 8px;
}
.filter-actions .toggle-link {
margin-right: 10px;
}
.icon-btn {
width: 32px;
height: 32px;
cursor: pointer;
background-repeat: no-repeat;
background-position: center;
background-size: 32px 32px;
background-color: transparent;
// transition: all 0.2s ease;
}
.icon-btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.icon-btn--search {
background-image: url('~@/assets/images/svg/search_active.svg');
}
.icon-btn--reset {
background-image: url('~@/assets/images/svg/reset.svg');
background-size: 15px 15px;
}
.icon-btn--reset:hover,
.icon-btn--reset:active {
background-image: url('~@/assets/images/svg/reset_active.svg');
background-size: 32px 32px;
}
.icon-btn--top {
background-image: url('~@/assets/images/svg/pintotop.svg');
background-size: 14px 14px;
}
.icon-btn--top:hover,
.icon-btn--top:active {
background-image: url('~@/assets/images/svg/pintotop_active.svg');
background-size: 32px 32px;
}
.icon-btn--export {
background-image: url('~@/assets/images/svg/export.svg');
background-size: 14px 14px;
}
.icon-btn--export:hover,
.icon-btn--export:active {
background-image: url('~@/assets/images/svg/export_active.svg');
background-size: 32px 32px;
}
.icon-btn.is-disabled {
cursor: not-allowed;
opacity: 0.6;
pointer-events: none;
}
.filter-extra-actions {
display: flex;
align-items: center;
justify-content: flex-start;
padding-bottom: 20px;
}
.filter-extra-actions .icon-btn,
.filter-extra-actions .create-course-btn {
margin-right: 10px;
}
.create-course-btn {
width: 120px;
height: 32px;
padding-left: 36px;
padding-right: 16px;
border-radius: 6px;
border: 1px solid #4284F7;
background-color: #ffffff;
background-image: url('~@/assets/images/svg/createCourse.svg');
background-repeat: no-repeat;
background-position: 12px center;
background-size: 14px 14px;
color: #4284F7;
font-weight: 400;
font-size: 14px;
line-height: 32px;
cursor: pointer;
transition: all 0.2s ease;
}
.create-course-btn:hover,
.create-course-btn:active {
background-color: #4284F7;
border-color: transparent;
color: #ffffff;
background-image: url('~@/assets/images/svg/createCourse_active.svg');
}
.toggle-link {
padding: 0;
}
/* 移除右对齐,保持左侧布局 */
::v-deep .el-table .el-table__body-wrapper::-webkit-scrollbar {
display: block;
}
.advanced-filter {
margin-top: 10px;
}
// .learning-time-range .el-date-editor {
// width: 250px;
// }
.pagination {
text-align: right;
padding-top: 20px;
::v-deep .el-pagination {
.el-pagination__total {
font-size: 14px;
color: #000000;
}
.el-pagination__sizes {
margin-right: 4px;
.el-input{
margin: 0;
width: 89px;
}
.el-input__inner {
width: 89px;
background: #F5F9FF;
border-radius: 4px;
border: 1px solid #DFDFDF;
height: 28px;
font-size: 14px;
color: #000000;
}
}
.btn-prev, .btn-next {
width: 28px;
height: 28px;
background: #F5F9FF;
border-radius: 4px;
border: 1px solid #DFDFDF;
// &:hover {
// background: #4284F7;
// color: #FFFFFF;
// }
}
.btn-quicknext{
background: transparent;
border: none;
line-height: 44px;
&:before {
content: '......';
}
}
.el-pager {
.number {
min-width: 28px;
height: 28px;
background: #F5F9FF;
border-radius: 4px;
border: 1px solid #DFDFDF;
font-weight: normal;
color: #000000;
margin: 0 4px;
&.active {
background: #4284F7;
color: #FFFFFF;
border: none;
}
}
}
.el-pagination__jump {
font-size: 14px;
color: #000000;
margin-left: 4px;
.el-input__inner {
width: 28px;
height: 28px;
background: #F5F9FF;
border-radius: 4px;
border: 1px solid #DFDFDF;
font-size: 14px;
color: #000000;
}
}
}
}
.course-types {
display: flex;
justify-content: center;
padding: 15px;
.course-type {
margin: 10px;
text-align: center;
padding: 5px;
cursor: pointer;
img {
width: 110px;
height: 110px;
}
.info {
padding-top: 10px;
}
}
.choose {
border: 2px solid #008000;
}
}
.el-aside {
padding: 5px 10px;
}
.cctree {
.cctree-chapter {
.cctree-chapter-name {
border-bottom: 1px solid #dddddd;
}
.cctree-chapter-cells {
margin: 0px;
list-style-type: circle;
padding: 0px;
.cctree-chapter-cell {
line-height: 30px;
list-style-type: circle;
padding-left: 25px;
}
}
}
}
.el-dialog__body {
overflow: hidden;
}
::v-deep .el-table .el-table__body-wrapper::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: #4284F7;
}
::v-deep .el-table .el-table__body-wrapper::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color:rgba(0, 0, 0, .1);
border-radius: 4px;
}
::v-deep .el-table .el-table__body-wrapper::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: #4284F7
}
::v-deep.el-table {
border-radius: 6px 6px 0 0;
td.el-table__cell {
border-bottom: 1px solid rgba(0, 0, 0, .1);
}
th.el-table__cell {
background: rgba(66, 132, 247, 0.1);
padding: 3px 0;
.cell {
font-weight: bold;
font-size: 14px;
color: #60769D;
}
.caret-wrapper {
.sort-caret {
border: 4px solid transparent;
&.ascending {
border-bottom-color: #C0C4CC;
top: 8px;
}
&.descending {
border-top-color: #C0C4CC;
bottom: 8px;
}
}
}
&.ascending .sort-caret.ascending {
border-bottom-color: #409EFF;
}
&.descending .sort-caret.descending{
border-top-color: #409EFF;
}
&.el-table--medium .el-table__cell {
padding: 5px 0;
}
}
.course-name {
font-weight: 400;
font-size: 14px;
color: #000000;
line-height: 20px;
}
.common-cell {
font-weight: 400;
font-size: 14px;
color: #000000;
}
.common-cell-right {
padding-right: 20px;
}
.single-line-ellipsis {
display: inline-block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: middle;
}
.status--pass {
color: #2EAD4D;
}
.status--reject {
color: #F41228;
}
.action-link--primary {
color: #4284F7;
font-weight: bold;
}
.action-link--danger {
color: #E32E2E;
font-weight: bold;
}
.action-link--more {
color: #999999;
font-weight: bold;
.el-icon-arrow-down {
font-weight: bold;
}
}
.action-link--bold {
font-weight: bold;
}
::v-deep .el-table--medium .el-table__cell {
padding: 5px 0;
}
}
.qrcode-img {
width: 150px;
height: 150px;
display: block;
position: relative;
.downloadn-container {
position: absolute;
width: 40px;
height: 45px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99;
background: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
cursor: pointer;
span {
color: #409eff;
display: block;
font-size: 12px;
line-height: 12px;
}
}
}
</style>
<style lang="scss">
.filter-field--name {
.el-input__inner {
width: 444px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
&:focus {
border-color: #4284F7;
}
}
}
.filter-field--category {
.el-cascader {
line-height: 32px;
.el-input__inner {
width: 180px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
color: #000000;
&:focus {
border: 1px solid #4284F7;
}
}
.el-input {
line-height: 32px;
&.is-focus .el-input__inner{
border-color: #4284F7;
}
}
.el-input__suffix {
line-height: 32px;
}
.el-input__icon {
line-height: 32px;
}
}
}
.filter-field--teacher {
.el-select {
.el-select__input {
margin-left: 10px;
}
.el-input__inner {
width: 180px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
}
.el-input {
&.is-focus .el-input__inner{
border-color: #4284F7;
}
}
}
}
.filter-field--status {
.el-select {
.el-select__input {
margin-left: 10px;
}
.el-input__inner {
width: 136px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
&.is-focus .el-input__inner{
border-color: #4284F7;
}
}
.el-input__icon {
line-height: 32px;
}
}
}
.filter-field--publish {
.el-select {
.el-select__input {
margin-left: 10px;
}
.el-input__inner {
width: 136px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
&.is-focus .el-input__inner{
border-color: #4284F7;
}
}
.el-input__icon {
line-height: 32px;
}
}
}
.filter-field--enabled {
.el-select {
.el-select__input {
margin-left: 10px;
}
.el-input__inner {
width: 136px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
&.is-focus .el-input__inner{
border-color: #4284F7;
}
}
.el-input__icon {
line-height: 32px;
}
}
}
.filter-field--open {
.el-select {
.el-select__input {
margin-left: 10px;
}
.el-input__inner {
width: 124px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
&.is-focus .el-input__inner{
border-color: #4284F7;
}
}
.el-input__icon {
line-height: 32px;
}
}
}
.filter-field--resowner {
.el-cascader {
width: 180px;
line-height: 32px;
.el-input__inner {
width: 180px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
&:focus {
border-color: #4284F7;
}
}
&.is-focus .el-input__inner {
border-color: #4284F7;
}
}
.el-input {
.el-input__inner {
width: 180px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
}
}
}
.filter-field--creator {
.el-select {
.el-select__input {
margin-left: 10px;
}
.el-input__inner {
width: 136px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
&.is-focus .el-input__inner{
border-color: #4284F7;
}
}
}
}
.filter-field--create-from {
.el-select {
.el-select__input {
margin-left: 10px;
}
.el-input__inner {
width: 180px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
&.is-focus .el-input__inner{
border-color: #4284F7;
}
}
.el-input__icon {
line-height: 32px;
}
}
}
.filter-field--time {
.el-input__inner {
width: 316px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 10px;
.el-input__icon {
line-height: 32px;
&.el-icon-date {
position: absolute;
right: 0;
}
&.el-icon-close {
margin-right: 8px;
}
}
.el-range-editor {
.is-active:hover{
border-color: #4284F7;
}
}
.el-range-separator {
line-height: 30px;
}
.el-range-input {
line-height: 30px;
}
}
.noSplitDatePicker {
.el-range-input {
text-align: left;
}
}
}
.icon-btn-tooltip {
height: 30px;
background: rgba(0, 0, 0, 1);
border-radius: 15px;
padding: 0 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
line-height: 30px;
.popper__arrow {
display: none !important;
}
.el-tooltip__popper__arrow {
display: none !important;
}
}
</style>