Files
learning-system-portal/src/views/course/ManageListRemote.vue
2025-12-23 18:01:46 +08:00

3174 lines
101 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>
<!-- AI 播放器相关批量语种设置 / 开启AI处理 -->
<el-button
v-if="aiPermission"
type="primary"
@click="setLanguage()"
icon="el-icon-connection"
:disabled="selectedCourses.length === 0"
>
设置语种
</el-button>
<el-button
v-if="aiPermission"
type="primary"
@click="enableAI()"
icon="el-icon-switch-button"
:disabled="selectedCourses.length === 0"
>
开启AI处理
</el-button>
<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" @selection-change="handleSelectionChange">
<!-- AI 播放器相关多选勾选列 -->
<el-table-column v-if="aiPermission" type="selection" width="55"></el-table-column>
<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', 'status--audit']" 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="300px" 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="700px" @close="closeCode" class="common-course-dialog">
<div>
<el-form size="medium" label-width="70px">
<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="链接">
<div style="width:500px">
<el-input v-model="copyUrl" readonly class="input-with-select" id="text">
<el-button slot="append" @click="handleCopyUrl">复制</el-button>
</el-input>
</div>
</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>
<!-- AI 播放器相关AI翻译 / 开启AI处理 / AI设置 三个弹框 -->
<!-- 设置语种弹框 -->
<el-dialog
title="AI翻译"
:visible.sync="languageSetting.dlgShow"
width="500px"
:close-on-click-modal="false"
>
<div style="margin-bottom: 20px;">
<div style="margin-bottom: 15px;">请选择课程所支持语种</div>
<el-select
v-model="languageSetting.languageCode"
multiple
placeholder="请选择语种"
style="width: 100%;"
>
<el-option
v-for="lang in selectAllLang"
:key="lang.srclang"
:label="lang.label"
:value="lang.srclang"
></el-option>
</el-select>
</div>
<div style="color: #ff4d4f; font-size: 12px;">
仅支持对已开启AI处理的课程进行批量语种设置所选的课程中有{{languageSetting.aiSetNoNum}}个未开启AI处理的课程以上配置仅对{{languageSetting.aiSetNum}}个已开启AI处理的课程生效
</div>
<template #footer>
<el-button @click="languageSetting.dlgShow = false">取消</el-button>
<el-button type="primary" @click="confirmLanguageSetting">确认</el-button>
</template>
</el-dialog>
<!-- 开启AI处理弹框 -->
<el-dialog
title="开启AI处理"
:visible.sync="aiProcessSetting.dlgShow"
width="400px"
>
<div class="ai-process-dialog">
<!-- AI处理状态 -->
<div class="form-item">
<span class="form-label">
<el-tooltip class="item" effect="dark" :content="aiSetTip" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
AI处理
</span>
<span class="status-text">
{{ aiProcessSetting.aiSet === 1 ? '开启' : '关闭' }}
</span>
<el-switch
v-model="aiProcessSetting.aiSet"
:active-value="1"
:inactive-value="0"
></el-switch>
</div>
<div v-show="aiProcessSetting.aiSet === 1">
<!-- 是否生成AI摘要 -->
<div class="form-item">
<span class="form-label">
<el-tooltip class="item" effect="dark" :content="aiAbstractTip" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
AI摘要
</span>
<span class="status-text">
{{ aiProcessSetting.aiAbstract === 1 ? '开启' : '关闭' }}
</span>
<el-switch
v-model="aiProcessSetting.aiAbstract"
:active-value="1"
:inactive-value="0"
>
</el-switch>
</div>
<!-- 是否生成AI文稿 -->
<div class="form-item">
<span class="form-label">
<el-tooltip class="item" effect="dark" :content="aiDraftTip" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
AI文稿
</span>
<span class="status-text">
{{ aiProcessSetting.aiDraft === 1 ? '开启' : '关闭' }}
</span>
<el-switch
v-model="aiProcessSetting.aiDraft"
:active-value="1"
:inactive-value="0"
>
</el-switch>
</div>
<!-- 课程支持语种选择 -->
<div class="form-item" style="flex-flow: column;align-items: baseline;">
<span class="form-label">
<el-tooltip class="item" effect="dark" :content="aiTranslateTip" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
AI翻译语种
</span>
<el-select
v-model="aiProcessSetting.languageCode"
multiple
style="width: 100%;"
placeholder="请选择语种"
>
<el-option
v-for="lang in selectAllLang"
:key="lang.srclang"
:label="lang.label"
:value="lang.srclang"
></el-option>
</el-select>
</div>
</div>
<!-- 提示信息 -->
<div class="tips">
<span>已跳过{{aiProcessSetting.aiSetNum}}个已开启AI处理的课程仅更新剩余{{aiProcessSetting.aiSetNoNum}}</span>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="aiProcessSetting.dlgShow = false">取消</el-button>
<el-button type="primary" @click="confirmAiProcess">确认</el-button>
</span>
</el-dialog>
<!-- AI设置弹框 -->
<el-dialog
title="AI设置"
:visible.sync="aiSetting.dlgShow"
width="500px"
>
<div class="ai-setting-dialog">
<!-- AI功能状态 -->
<div class="form-item">
<span class="form-label">
<el-tooltip class="item" effect="dark" content="是否将课程进行AI处理" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
AI功能状态
</span>
<span class="status-text">
{{ aiSetting.aiSet === 1 ? '已开放' : '未开放' }}
</span>
<el-switch
v-model="aiSetting.aiSet"
:active-value="1"
:inactive-value="0"
></el-switch>
</div>
<div v-show="aiSetting.aiSet === 1">
<!-- AI摘要状态 -->
<div class="form-item">
<span class="form-label">AI摘要状态</span>
<span :class="aiSetting.aiAbstract === 1 ? 'custom-putaway' : 'custom-takeout'">
{{ aiSetting.aiAbstract === 1 ? '已上架' : '已下架' }}
</span>
<div class="action-buttons">
<el-button type="text" @click="changeAIKey('aiAbstract')">
{{ aiSetting.aiAbstract === 1 ? '下架' : '上架' }}
</el-button>
<el-button v-show="false" type="text">编辑</el-button>
</div>
</div>
<!-- AI文稿状态 -->
<div class="form-item">
<span class="form-label">AI文稿状态</span>
<span :class="aiSetting.aiDraft === 1 ? 'custom-putaway' : 'custom-takeout'">
{{ aiSetting.aiDraft === 1 ? '已上架' : '已下架' }}
</span>
<div class="action-buttons">
<el-button type="text" @click="changeAIKey('aiDraft')">
{{ aiSetting.aiDraft === 1 ? '下架' : '上架' }}
</el-button>
</div>
</div>
<!-- AI翻译状态 -->
<div class="form-item">
<span class="form-label">AI翻译状态</span>
<span :class="aiSetting.aiTranslate === 1 ? 'custom-putaway' : 'custom-takeout'">
{{ aiSetting.aiTranslate === 1 ? '已上架' : '已下架' }}
</span>
<div class="action-buttons">
<el-button type="text" @click="changeAIKey('aiTranslate')">
{{ aiSetting.aiTranslate === 1 ? '下架' : '上架' }}
</el-button>
<el-button v-show="false" type="text">编辑</el-button>
</div>
</div>
<!-- 支持语种 -->
<div class="form-item" style="align-items: flex-start;">
<span class="form-label" style="white-space: nowrap;">支持语种</span>
<div class="languages-list" v-show="false">
<div v-for="lang in aiSetting.languageCode" :key="lang" class="language-tag">
{{ getLanguageName(lang) }}
<span class="custom-takeout">已下架</span>
</div>
</div>
<el-select
v-model="aiSetting.languageCode"
multiple
style="width: 100%;"
placeholder="请选择语种"
>
<el-option
v-for="lang in selectAllLang"
:key="lang.srclang"
:label="lang.label"
:value="lang.srclang"
></el-option>
</el-select>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="aiSetting.dlgShow = false">取消</el-button>
<el-button type="primary" @click="confirmAISetting">确认</el-button>
</span>
</el-dialog>
</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, deepCopy } 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', 'selectAllLang']),
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,
// AI 播放器相关
aiPermission: false,
selectedCourses: [],
languageSetting: {
dlgShow: false,
languageCode: ['zh-CN', 'en-US'],
aiSetNum: 0,
aiSetNoNum: 0
},
aiProcessSetting: {
dlgShow: false,
aiSet: 1,
aiAbstract: 1,
aiDraft: 1,
languageCode: ['zh-CN', 'en-US'],
aiSetNum: 0,
aiSetNoNum: 0
},
aiSetting: {
dlgShow: false,
courseId: '',
aiSet: 1,
aiAbstract: 1,
aiDraft: 1,
aiTranslate: 1,
languageCode: ['zh-CN', 'en-US', 'vi-VN']
},
aiSetTip: '是否将课程进行AI处理',
aiAbstractTip: '一键提炼课程视频核心要点,助力学员课前高效掌握重点,快速筛选学习资源',
aiDraftTip: '分段展示视频内容并精准同步时间轴,实现视频进度与文稿双向定位,学习内容触手可及',
aiTranslateTip: '智能转换视频字幕与语音为多语种,支持全球学员按需切换语言,打破学习边界',
};
},
created() {
this.pickerOptions = this.buildPickerOptions();
},
mounted() {
this.getAiPermission();
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(',');
this.adjustTeacherWidth();
},
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(',');
this.adjustCreatorWidth();
},
handleCreatorClear() {
this.creatorSelected = [];
this.params.createUserId = '';
this.adjustCreatorWidth();
},
// 创建人下拉展开时,如果当前没有关键字,则清空上一次的查询结果
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;
}
this.adjustTeacherWidth();
},
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;
}
this.adjustCreatorWidth();
},
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 isLeaf = node.isLeaf || (node.data && node.data.leaf);
if (!isLeaf) return false;
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.$showManageMessage('请选择完整的培训时间范围', '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.$showManageMessage(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.$showManageMessage('置顶成功!', '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.$showManageMessage(res.message, 'error');
}
})
} else if (row.isTop == true) {
apiCourse.setTop(params).then(res => {
if (res.status === 200 && res.result === true) {
this.$showManageMessage('取消成功!', 'success')
this.searchData();
} else {
this.$showManageMessage(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.$showManageMessage('复制成功', 'success');
this.searchData();
} else {
this.$showManageMessage(rs.message || '复制失败', 'error');
}
}).catch(() => {
this.$showManageMessage('复制失败', '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.$showManageMessage('撤回成功', 'success');
this.searchData();
} else {
this.$showManageMessage(res.message || '撤回失败', 'error');
}
}).catch(() => {
this.$showManageMessage('撤回失败', '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.$showManageMessage('操作成功!', 'success');
this.dialogVisible = false;
this.searchData();
} else {
this.$showManageMessage(res.message, 'error');
}
})
} else {
this.$showManageMessage('暂未开放!', '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.$showManageMessage(`${actionText}成功`, 'success');
row.enabled = nextEnabled;
} else {
this.$showManageMessage(`${actionText}失败`, 'error');
}
} catch (error) {
this.$showManageMessage(`${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.$showManageMessage(rs.message, 'error');
}
}).catch(err => {
console.error('查询课程列表失败:', err);
this.$showManageMessage('查询失败,请稍后重试', '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.$showManageMessage('请提交课程再预览!', '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.$showManageMessage('复制成功', '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' });
},
// 克隆一个用于懒加载的节点:只保留当前层必要字段,不预置 children避免 Element Cascader 出现重复节点
cloneOrgNodeForLazy(node = {}) {
return {
id: node.id,
name: node.name,
leaf: node.leaf,
};
},
// 前端“伪懒加载”:根据本地全量机构树按需返回子节点
// 为了让 el-cascader 的 loading 动画有可见效果,这里故意加入一个很小的延时再 resolve
async loadResOwnerNodeFromCache(node, resolve) {
try {
const delayResolve = (nodes) => {
// 适当延时 150ms让 loading 动画可见,但又不会让用户感觉明显卡顿
setTimeout(() => resolve(nodes), 150);
};
// 如果当前节点在数据上被标记为叶子节点,直接返回空数组,不再触发“懒加载”
if (node && node.data && node.data.leaf) {
return resolve([]);
}
// 根节点level === 0返回所有一级机构只返回当前层的浅拷贝不带 children避免重复
if (node.level === 0) {
const roots = (this.resOwnerTreeAll || []).map(item => this.cloneOrgNodeForLazy(item));
return delayResolve(roots);
}
// 其他层级:根据当前节点 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) {
// 未找到对应节点,直接返回空
return resolve([]);
}
// 如果在树里这个节点本身就是叶子节点,同样直接返回空,不展示 loading
if (target.leaf) {
return resolve([]);
}
if (Array.isArray(target.children) && target.children.length > 0) {
// 有子节点时,只返回当前层的浅拷贝,不预置更深 children避免重复
const children = target.children.map(child => this.cloneOrgNodeForLazy(child));
// 做一个轻微延迟,让 loading 有一点点反馈
return delayResolve(children);
}
// 没有子节点,直接返回空
return resolve([]);
} catch (e) {
console.error('本地懒加载资源归属节点失败:', e);
resolve([]);
}
},
handleResOwnerChange(value) {
if (!value || value.length === 0) {
this.orgId = '';
this.orgName = '';
// 清空选择时,直接收起下拉
if (this.$refs.resOwnerCascader) {
this.$refs.resOwnerCascader.dropDownVisible = false;
}
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 || '';
}
// 选中后自动收起下拉
this.$refs.resOwnerCascader.dropDownVisible = false;
}
});
},
// 将后端机构节点转换为级联组件所需结构
mapOrgToCascaderNode(node = {}) {
const hasChildren = Array.isArray(node.childList) && node.childList.length > 0;
const children = hasChildren
? node.childList.map(child => this.mapOrgToCascaderNode(child))
: [];
return {
id: node.organizationId,
name: node.orgName,
children,
// childList 为空就标记为叶子节点,去掉右侧箭头,且不再触发懒加载
leaf: !hasChildren,
};
},
// 加载资源归属全量机构树
async loadAllResOwnerTree() {
try {
const res = await apiUserbasic.getAllOrgTree();
// 调试日志:打印原始机构树与映射后的树结构,用于排查“重复节点”问题
try {
const raw = res && res.result && res.result.orgTreeList
? JSON.parse(JSON.stringify(res.result.orgTreeList))
: [];
// 注意:这里日志仅用于前端调试,确认后可删除
console.log('[资源归属] getAllOrgTree 原始 orgTreeList =>', raw);
} catch (e) {
console.warn('[资源归属] 打印 orgTreeList 日志失败:', e);
}
if (res && res.status === 200 && res.result && Array.isArray(res.result.orgTreeList)) {
this.resOwnerTreeAll = res.result.orgTreeList.map(item => this.mapOrgToCascaderNode(item));
console.log('[资源归属] 映射后的 resOwnerTreeAll =>', this.resOwnerTreeAll);
// 默认进入非搜索模式,由懒加载从本地树按需返回节点
this.resOwnerOptions = [];
} else if (res && res.result && Array.isArray(res.result.orgTreeList)) {
// 兼容没有 status 字段但有 result 的情况
this.resOwnerTreeAll = res.result.orgTreeList.map(item => this.mapOrgToCascaderNode(item));
console.log('[资源归属] 映射后的 resOwnerTreeAll(无status) =>', this.resOwnerTreeAll);
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.$showManageMessage('xxx', 'success');
// },
delItem(row) {
// this.$showManageMessage('删除成功', '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.$showManageMessage('删除成功', '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.$showManageMessage('删除失败', 'error');
}
})
.catch((err) => {
// this.$showManageMessage('已取消删除', '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);
case 'aiSetting':
return this.setAI(row);
default:
return;
}
},
buildActions(row) {
const actions = [];
// 优先级按原有展示顺序
if (this.aiPermission) {
actions.push({ key: 'aiSetting', label: 'AI设置', 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) {
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 && 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) : [];
}
,
// 按已选标签宽度动态调整“授课教师”选择框宽度
adjustTeacherWidth() {
this.$nextTick(() => {
const select = this.$refs.teacherSelect;
this.adjustSelectWidth(select, 200, 800, 60);
});
},
// 按已选标签宽度动态调整“创建人”选择框宽度
adjustCreatorWidth() {
this.$nextTick(() => {
const select = this.$refs.creatorSelect;
this.adjustSelectWidth(select, 200, 800, 60);
});
},
// 通用:累加 el-select__tags 内所有 tag 的宽度,设置选择框宽度
adjustSelectWidth(selectRef, min = 200, max = 800, extra = 0) {
if (!selectRef || !selectRef.$el) return;
const el = selectRef.$el;
const tags = Array.from(el.querySelectorAll('.el-select__tags .el-tag'));
const inputInner = el.querySelector('.el-input__inner');
const padding = 24; // 预留箭头/输入余量
const tagsWidth = tags.reduce((sum, tag) => sum + (tag.getBoundingClientRect().width || 0), 0);
const target = Math.max(min, Math.min(tagsWidth + padding + extra, max));
el.style.width = `${target}px`;
if (inputInner) {
inputInner.style.width = `${target}px`;
}
},
// AI 播放器相关方法
getLanguageName(lang) {
const item = (this.selectAllLang || []).find(it => it.srclang === lang);
return (item && item.label) || '';
},
handleSelectionChange(val) {
this.selectedCourses = val || [];
},
getAIInfoByList(list = []) {
const total = list.length;
let aiSetNum = 0;
let aiSetNoNum = 0;
list.forEach(item => {
if (item.aiSet === 1) {
aiSetNum++;
} else {
aiSetNoNum++;
}
});
return {
selectNum: total,
aiSetNum,
aiSetNoNum
};
},
setAI(row) {
this.aiSetting = {
dlgShow: true,
...row
};
},
changeAIKey(key) {
this.aiSetting[key] = this.aiSetting[key] === 1 ? 0 : 1;
},
confirmAISetting() {
const item = deepCopy(this.aiSetting);
item.languageStatus = item.aiSet;
item.languageCode = item.languageCode || [];
if (!item.languageCode.includes('zh-CN')) {
item.languageCode.unshift('zh-CN');
}
this._benchAiSet(
[item],
() => {
this.$showManageMessage('AI设置保存成功', 'success');
this.aiSetting.dlgShow = false;
this.searchData();
},
() => {
this.$showManageMessage('AI设置保存失败', 'error');
}
);
},
setLanguage() {
if (!this.selectedCourses.length) return;
const info = this.getAIInfoByList(this.selectedCourses);
this.languageSetting = {
dlgShow: true,
languageCode: ['zh-CN', 'en-US'],
...info
};
},
enableAI() {
if (!this.selectedCourses.length) return;
const info = this.getAIInfoByList(this.selectedCourses);
this.aiProcessSetting = {
dlgShow: true,
aiSet: 1,
aiAbstract: 1,
aiDraft: 1,
languageCode: ['zh-CN', 'en-US'],
...info
};
},
confirmLanguageSetting() {
const courseList = deepCopy(this.selectedCourses || []);
let languageCode = deepCopy(this.languageSetting.languageCode || []);
if (!languageCode.includes('zh-CN')) {
languageCode.unshift('zh-CN');
}
courseList.forEach(item => {
item.languageCode = languageCode;
item.aiTranslate = item.aiSet;
item.languageStatus = item.aiSet;
});
this._benchAiSet(
courseList,
() => {
this.$showManageMessage('设置语种成功!', 'success');
this.languageSetting.dlgShow = false;
this.searchData();
},
() => {
this.$showManageMessage('设置语种失败!', 'error');
}
);
},
confirmAiProcess() {
const courseList = deepCopy(this.selectedCourses || []);
let { aiSet, aiAbstract, aiDraft, languageCode } = this.aiProcessSetting;
languageCode = languageCode || [];
if (!languageCode.includes('zh-CN')) {
languageCode.unshift('zh-CN');
}
courseList.forEach(item => {
item.aiSet = aiSet;
item.aiAbstract = aiAbstract;
item.aiDraft = aiDraft;
item.aiTranslate = aiSet;
item.languageStatus = aiSet;
item.languageCode = languageCode;
});
this._benchAiSet(
courseList,
() => {
this.$showManageMessage('开启AI处理成功', 'success');
this.aiProcessSetting.dlgShow = false;
this.searchData();
},
() => {
this.$showManageMessage('开启AI处理失败', 'error');
}
);
},
_benchAiSet(courseList, successCB, failCB) {
apiCourse.benchAiSet({ courseList }).then(res => {
if (res && res.status === 200) {
if (successCB) successCB(res);
} else if (failCB) {
failCB(res);
}
});
},
getAiPermission() {
apiCourse.listByUser({}).then(res => {
console.log('res', res);
if(res.code === 200){
let index = res.data.findIndex(item => item.permissionCode === 'KjbAiSetCode');
this.aiPermission = index !== -1;
console.log('index', index, this.aiPermission);
}
})
}
}
};
</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 {
min-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;
}
// AI 播放器相关样式
.form-item {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.form-label {
white-space: nowrap;
font-size: 14px;
color: #000000;
}
.status-text {
font-size: 14px;
color: #000000;
}
.tips {
color: #f56c6c;
font-size: 12px;
margin: 10px 0;
line-height: 1.5;
}
.languages-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.language-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
background-color: #f5f5f5;
font-size: 12px;
}
// 注意custom-putaway 和 custom-takeout 已在全局样式 src/assets/styles/btn.scss 中定义
// 无需在此重复定义
::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__empty-block {
width: 100% !important;
display: flex;
justify-content: center;
align-items: center;
}
::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;
}
.status--audit {
color: #FF9900;
}
.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 15px 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 15px 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 {
min-width: 180px;
width: auto;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 15px 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 15px 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 15px 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 15px 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 15px 0 10px;
&.is-focus .el-input__inner{
border-color: #4284F7;
}
}
.el-input__icon {
line-height: 32px;
}
}
}
.filter-field--resowner {
.el-cascader {
min-width: 200px;
width: auto;
line-height: 32px;
.el-input__inner {
min-width: 200px;
width: auto;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 15px 0 10px;
&:focus {
border-color: #4284F7;
}
}
&.is-focus .el-input__inner {
border-color: #4284F7;
}
}
.el-input {
.el-input__inner {
min-width: 200px;
width: auto;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 15px 0 10px;
}
}
}
.filter-field--creator {
.el-select {
min-width: 180px;
width: auto;
.el-select__input {
margin-left: 10px;
}
.el-input__inner {
min-width: 180px;
width: auto;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 15px 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: 136px;
height: 32px;
background: #FFFFFF;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, .2);
line-height: 32px;
padding: 0 15px 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 15px 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>