first commit
This commit is contained in:
@@ -0,0 +1,884 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
.learning-outline-layout {
|
||||
height: calc(100vh - 235px);
|
||||
}
|
||||
.learning-outline-tree {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.outline-context-menu {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
padding: 5px 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
.outline-context-menu li {
|
||||
min-width: 120px;
|
||||
padding: 7px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.outline-context-menu li:hover {
|
||||
background: #f2f6fc;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="课程名称">
|
||||
<el-select
|
||||
v-model="currentCourseId"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="courseNameRemoteSearch"
|
||||
placeholder="请选择或搜索课程名称"
|
||||
style="width: 100%"
|
||||
@change="courseChange"
|
||||
@clear="courseClear">
|
||||
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="课程类型">
|
||||
<el-select v-model="courseQuery.courseTypeId" clearable filterable placeholder="请选择课程类型" style="width: 100%">
|
||||
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="授课讲师">
|
||||
<el-input v-model="courseQuery.lecturerName" clearable placeholder="请输入授课讲师" style="width: 100%" @keyup.enter.native="loadCourses"></el-input>
|
||||
</search-item>
|
||||
<search-item label="课程状态">
|
||||
<el-select v-model="courseQuery.status" clearable placeholder="请选择课程状态" style="width: 100%">
|
||||
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="loadCourses">搜索</el-button>
|
||||
<el-button size="medium" @click="resetCourseQuery">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="10" class="mt10 learning-outline-layout">
|
||||
<el-col :span="6" style="height: 100%">
|
||||
<el-card shadow="never" style="height: 100%">
|
||||
<table-tool label="课程大纲">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openNodeForm('chapter')">新增章</el-button>
|
||||
</table-tool>
|
||||
<div class="learning-outline-tree">
|
||||
<el-tree
|
||||
ref="outlineTree"
|
||||
:data="outlineTreeData"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:expand-on-click-node="false"
|
||||
:props="{children:'children', label:'title'}"
|
||||
@node-click="nodeClick"
|
||||
@node-contextmenu="openContextMenu">
|
||||
<span slot-scope="{ node, data }">
|
||||
<i :class="data.nodeType === 'course' ? 'el-icon-collection' : (data.nodeType === 'chapter' ? 'el-icon-folder' : 'el-icon-document')"></i>
|
||||
<span>{{data.title}}</span>
|
||||
<el-tag v-if="data.status === 'disabled'" size="mini" type="info" style="margin-left: 6px">禁用</el-tag>
|
||||
<el-tag v-if="data.required" size="mini" type="warning" style="margin-left: 6px">必学</el-tag>
|
||||
</span>
|
||||
</el-tree>
|
||||
<el-empty v-if="treeData.length === 0" description="暂无课程大纲"></el-empty>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="18" style="height: 100%">
|
||||
<el-card shadow="never" style="height: 100%; overflow: auto">
|
||||
<el-empty v-if="!selectedNode.id" description="请选择左侧章/节节点"></el-empty>
|
||||
<el-tabs v-else v-model="activeTab">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form :model="nodeForm" ref="nodeForm" label-width="110px" :rules="nodeRules">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="节点类型">
|
||||
<el-tag :type="nodeForm.nodeType === 'chapter' ? 'primary' : 'success'">{{nodeForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-input-number v-model="nodeForm.sortOrder" :controls="false" :min="1" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="nodeForm.title" maxlength="100" placeholder="请输入章/节标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题" prop="subtitle">
|
||||
<el-input v-model="nodeForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="简介" prop="description">
|
||||
<el-input v-model="nodeForm.description" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入章/节简介"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveNodeBasic">保存基础信息</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="学习资料" name="resource">
|
||||
<table-tool label="学习资料">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openResourceForm()">新增资料</el-button>
|
||||
</table-tool>
|
||||
<el-table :data="resourceData" border @sort-change="resourcePageOrder">
|
||||
<el-table-column label="序号" type="index" width="70" :index="resourceIndex"></el-table-column>
|
||||
<el-table-column label="资料标题" prop="resourceTitle" sortable="custom" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="资料类型" prop="resourceType" sortable="custom" width="110">
|
||||
<template slot-scope="{row}">{{getResourceTypeName(row.resourceType)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件格式" prop="fileExt" sortable="custom" width="110"></el-table-column>
|
||||
<el-table-column label="学习时长(秒)" prop="durationSeconds" sortable="custom" width="140">
|
||||
<template slot-scope="{row}">{{formatDurationSeconds(row.durationSeconds)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sortOrder" sortable="custom" width="90"></el-table-column>
|
||||
<el-table-column label="必学" prop="required" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="row.required ? 'warning' : 'info'" size="mini">{{row.required ? '是' : '否'}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预览" prop="allowPreview" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">{{row.allowPreview ? '允许' : '不允许'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下载" prop="allowDownload" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">{{row.allowDownload ? '允许' : '不允许'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="row.status === 'enabled' ? 'success' : 'info'" size="mini">{{row.status === 'enabled' ? '启用' : '禁用'}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openResourceForm(row)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="deleteResource(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="mt20"
|
||||
@size-change="resourceSizeChange"
|
||||
@current-change="resourceNumberChange"
|
||||
:current-page="resourcePageForm.pageNumber"
|
||||
:page-sizes="[5,10,20,30,50]"
|
||||
:page-size="resourcePageForm.pageSize"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="resourcePageForm.totalCount">
|
||||
</el-pagination>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="完成规则" name="rule">
|
||||
<el-form :model="ruleForm" label-width="150px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否必学">
|
||||
<el-switch v-model="ruleForm.required"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学习方式">
|
||||
<el-select v-model="ruleForm.studyMode" style="width: 100%">
|
||||
<el-option v-for="item in studyModeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="完成规则">
|
||||
<el-select v-model="ruleForm.completionRule" style="width: 100%">
|
||||
<el-option v-for="item in completionRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="完成比例">
|
||||
<el-input-number v-model="ruleForm.completePercent" :min="0" :max="100" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最少学习时长(分钟)">
|
||||
<el-input-number v-model="ruleForm.minStudyMinutes" :min="0" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="解锁规则">
|
||||
<el-select v-model="ruleForm.unlockRule" style="width: 100%">
|
||||
<el-option v-for="item in unlockRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="允许跳过"><el-switch v-model="ruleForm.allowSkip"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="允许拖动"><el-switch v-model="ruleForm.allowDrag"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="暂停计时"><el-switch v-model="ruleForm.pauseCountTime"></el-switch></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="隐藏计时"><el-switch v-model="ruleForm.hiddenCountTime"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="无操作计时"><el-switch v-model="ruleForm.inactiveCountTime"></el-switch></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveRule">保存完成规则</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="状态设置" name="status">
|
||||
<el-form :model="nodeForm" label-width="110px">
|
||||
<el-form-item label="节点状态">
|
||||
<el-radio-group v-model="nodeForm.status">
|
||||
<el-radio label="enabled">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必学">
|
||||
<el-switch v-model="nodeForm.required"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveNodeBasic">保存状态</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<ul v-show="contextVisible" class="outline-context-menu" :style="{left: contextLeft + 'px', top: contextTop + 'px'}">
|
||||
<li @click="openNodeForm('chapter')">新增章</li>
|
||||
<li v-if="contextNode.nodeType === 'chapter'" @click="openNodeForm('section', contextNode)">新增节</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="openNodeForm(contextNode.nodeType, contextNode)">编辑</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="deleteNode(contextNode)">删除</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="toggleNode(contextNode)">{{contextNode.status === 'enabled' ? '禁用' : '启用'}}</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'up')">上移</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'down')">下移</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="openResourceForm(null, contextNode)">上传资料</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="setRequired(contextNode)">设置必学</li>
|
||||
</ul>
|
||||
|
||||
<el-dialog :title="nodeDialogTitle" :visible.sync="nodeDialogVisible" :close-on-click-modal="false" width="45%">
|
||||
<el-form :model="nodeDialogForm" ref="nodeDialogForm" label-width="100px" :rules="nodeRules">
|
||||
<el-form-item label="节点类型">
|
||||
<el-tag>{{nodeDialogForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="上级节点">
|
||||
<el-input v-model="nodeDialogForm.parentName" disabled placeholder="上级节点"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="nodeDialogForm.title" maxlength="100" placeholder="请输入标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题">
|
||||
<el-input v-model="nodeDialogForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="简介">
|
||||
<el-input v-model="nodeDialogForm.description" type="textarea" :rows="4" maxlength="2000" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值">
|
||||
<el-input-number v-model="nodeDialogForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必学">
|
||||
<el-switch v-model="nodeDialogForm.required"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="nodeDialogForm.status">
|
||||
<el-radio label="enabled">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="nodeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitNodeDialog">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="resourceForm.id ? '编辑学习资料' : '新增学习资料'" :visible.sync="resourceDialogVisible" :close-on-click-modal="false" width="55%">
|
||||
<el-form :model="resourceForm" ref="resourceForm" label-width="120px" :rules="resourceRules">
|
||||
<el-form-item label="所属章节">
|
||||
<el-input v-model="resourceForm.outlineName" disabled placeholder="所属章节"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="资料标题" prop="resourceTitle">
|
||||
<el-input v-model="resourceForm.resourceTitle" maxlength="100" placeholder="请输入资料标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资料类型" prop="resourceType">
|
||||
<el-select v-model="resourceForm.resourceType" placeholder="请选择资料类型" style="width: 100%" @change="resourceTypeChange">
|
||||
<el-option v-for="item in resourceTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-input-number v-model="resourceForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学习时长(秒)">
|
||||
<el-input-number v-model="resourceForm.durationSeconds" :min="0" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="resourceForm.status">
|
||||
<el-radio label="enabled">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="是否必学"><el-switch v-model="resourceForm.required"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="允许预览"><el-switch v-model="resourceForm.allowPreview"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="允许下载"><el-switch v-model="resourceForm.allowDownload"></el-switch></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item label="附件" prop="fileData">
|
||||
<file-upload
|
||||
:value.sync="resourceForm.fileData"
|
||||
:upload_number="1"
|
||||
upload_mode="drag"
|
||||
upload_result_category="array"
|
||||
complete_result
|
||||
:accept="fileAccept">
|
||||
</file-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="resourceDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitResource">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
courseQuery: {},
|
||||
courseOptions: [],
|
||||
courseTypeOptions: [],
|
||||
currentCourseId: "",
|
||||
treeData: [],
|
||||
selectedNode: {},
|
||||
activeTab: "basic",
|
||||
nodeForm: {},
|
||||
nodeDialogVisible: false,
|
||||
nodeDialogTitle: "",
|
||||
nodeDialogForm: {},
|
||||
nodeRules: {
|
||||
title: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
|
||||
},
|
||||
contextVisible: false,
|
||||
contextLeft: 0,
|
||||
contextTop: 0,
|
||||
contextNode: {},
|
||||
resourceData: [],
|
||||
resourcePageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "",
|
||||
pageOrderBy: ""
|
||||
},
|
||||
resourceDialogVisible: false,
|
||||
resourceForm: {},
|
||||
resourceRules: {
|
||||
resourceTitle: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
resourceType: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
fileData: [{ required: true, message: "请上传附件", trigger: ["blur", "change"] }]
|
||||
},
|
||||
fileAccept: "",
|
||||
ruleForm: {},
|
||||
statusOptions: [
|
||||
{ name: "草稿", code: "draft" },
|
||||
{ name: "待发布", code: "pending" },
|
||||
{ name: "已发布", code: "published" },
|
||||
{ name: "已下架", code: "offline" }
|
||||
],
|
||||
resourceTypeOptions: [
|
||||
{ name: "视频", code: "video", accept: ".mp4,.mov" },
|
||||
{ name: "音频", code: "audio", accept: ".mp3,.wav,.aac" },
|
||||
{ name: "PDF", code: "pdf", accept: ".pdf" },
|
||||
{ name: "PPT课件", code: "ppt", accept: ".ppt,.pptx" },
|
||||
{ name: "Word文档", code: "word", accept: ".doc,.docx" },
|
||||
{ name: "图片", code: "image", accept: ".jpg,.jpeg,.png" },
|
||||
{ name: "其他", code: "other", accept: "" }
|
||||
],
|
||||
studyModeOptions: [
|
||||
{ name: "视频", code: "video" },
|
||||
{ name: "音频", code: "audio" },
|
||||
{ name: "文档", code: "document" },
|
||||
{ name: "图片", code: "image" },
|
||||
{ name: "混合资料", code: "mixed" }
|
||||
],
|
||||
completionRuleOptions: [
|
||||
{ name: "完成所有必学资料", code: "all_required_resource" },
|
||||
{ name: "按资料学习进度", code: "resource_progress" },
|
||||
{ name: "达到最少学习时长", code: "min_study_time" },
|
||||
{ name: "管理员手动确认", code: "manual" }
|
||||
],
|
||||
unlockRuleOptions: [
|
||||
{ name: "自由学习", code: "free" },
|
||||
{ name: "顺序解锁", code: "sequential" }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentCourse() {
|
||||
return this.courseOptions.find(v => v.id === this.currentCourseId)
|
||||
},
|
||||
outlineTreeData() {
|
||||
if (!this.currentCourseId || !this.currentCourse) {
|
||||
return []
|
||||
}
|
||||
return [{
|
||||
id: "course-" + this.currentCourseId,
|
||||
courseId: this.currentCourseId,
|
||||
title: this.currentCourse.courseName,
|
||||
nodeType: "course",
|
||||
children: this.treeData
|
||||
}]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetCourseQuery() {
|
||||
this.courseQuery = {}
|
||||
this.currentCourseId = ""
|
||||
this.loadCourses()
|
||||
},
|
||||
getQueryParam(name) {
|
||||
return new URLSearchParams(window.location.search).get(name) || ""
|
||||
},
|
||||
async loadCourseTypes() {
|
||||
const resp = await this.$axios.post(loc() + "/courseTypes")
|
||||
if (resp.code === 0) {
|
||||
this.courseTypeOptions = resp.data
|
||||
}
|
||||
},
|
||||
async loadCourses(keepCurrent = false) {
|
||||
const resp = await this.$axios.post(loc() + "/courses", this.courseQuery)
|
||||
if (resp.code === 0) {
|
||||
this.courseOptions = resp.data
|
||||
if (!keepCurrent && (!this.currentCourseId || !this.courseOptions.some(v => v.id === this.currentCourseId))) {
|
||||
this.currentCourseId = this.courseOptions.length ? this.courseOptions[0].id : ""
|
||||
}
|
||||
await this.loadTree()
|
||||
}
|
||||
},
|
||||
courseNameRemoteSearch(query) {
|
||||
this.courseQuery.courseName = query
|
||||
this.loadCourses(true)
|
||||
},
|
||||
courseChange() {
|
||||
this.selectedNode = {}
|
||||
this.loadTree()
|
||||
},
|
||||
courseClear() {
|
||||
this.currentCourseId = ""
|
||||
this.selectedNode = {}
|
||||
this.treeData = []
|
||||
},
|
||||
async loadTree() {
|
||||
if (!this.currentCourseId) {
|
||||
this.treeData = []
|
||||
this.selectedNode = {}
|
||||
return
|
||||
}
|
||||
const resp = await this.$axios.post(loc() + "/tree", { courseId: this.currentCourseId })
|
||||
if (resp.code === 0) {
|
||||
this.treeData = resp.data
|
||||
let nodeToSelect = null
|
||||
if (this.selectedNode.id) {
|
||||
nodeToSelect = this.findNode(this.treeData, this.selectedNode.id)
|
||||
}
|
||||
if (!nodeToSelect) {
|
||||
nodeToSelect = this.findFirstOutlineNode(this.treeData)
|
||||
}
|
||||
if (nodeToSelect) {
|
||||
this.nodeClick(nodeToSelect)
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.outlineTree) {
|
||||
this.$refs.outlineTree.setCurrentKey(nodeToSelect.id)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.selectedNode = {}
|
||||
this.nodeForm = {}
|
||||
this.resourceData = []
|
||||
}
|
||||
}
|
||||
},
|
||||
findFirstOutlineNode(nodes) {
|
||||
if (!nodes || !nodes.length) return null
|
||||
for (const node of nodes) {
|
||||
if (node.nodeType !== "course") return node
|
||||
const child = this.findFirstOutlineNode(node.children || [])
|
||||
if (child) return child
|
||||
}
|
||||
return null
|
||||
},
|
||||
findNode(nodes, id) {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node
|
||||
const child = this.findNode(node.children || [], id)
|
||||
if (child) return child
|
||||
}
|
||||
return null
|
||||
},
|
||||
getParentName(type, node, course, isEdit) {
|
||||
if (type === "chapter") {
|
||||
return course ? course.courseName : ""
|
||||
}
|
||||
if (!isEdit) {
|
||||
return node ? node.title : ""
|
||||
}
|
||||
const parent = node && node.parentId ? this.findNode(this.treeData, node.parentId) : null
|
||||
return parent ? parent.title : (course ? course.courseName : "")
|
||||
},
|
||||
nodeClick(data) {
|
||||
if (data.nodeType === "course") {
|
||||
this.selectedNode = {}
|
||||
this.nodeForm = {}
|
||||
this.resourceData = []
|
||||
return
|
||||
}
|
||||
this.selectedNode = data
|
||||
this.nodeForm = Object.assign({}, data)
|
||||
this.activeTab = "basic"
|
||||
this.loadResources()
|
||||
this.loadRule()
|
||||
},
|
||||
openContextMenu(event, data) {
|
||||
event.preventDefault()
|
||||
this.contextNode = data
|
||||
this.contextVisible = true
|
||||
this.contextLeft = event.clientX
|
||||
this.contextTop = event.clientY
|
||||
},
|
||||
openNodeForm(type, node = null) {
|
||||
if (!this.currentCourseId) {
|
||||
this.$message.warning("请先选择课程")
|
||||
return
|
||||
}
|
||||
const course = this.courseOptions.find(v => v.id === this.currentCourseId)
|
||||
if (node && node.id && node.nodeType === type) {
|
||||
this.nodeDialogTitle = "编辑" + (type === "chapter" ? "章" : "节")
|
||||
this.nodeDialogForm = Object.assign({}, node, {
|
||||
parentName: this.getParentName(type, node, course, true)
|
||||
})
|
||||
} else {
|
||||
this.nodeDialogTitle = "新增" + (type === "chapter" ? "章" : "节")
|
||||
this.nodeDialogForm = {
|
||||
courseId: this.currentCourseId,
|
||||
courseName: course ? course.courseName : "",
|
||||
parentId: type === "section" && node ? node.id : "",
|
||||
parentName: this.getParentName(type, node, course, false),
|
||||
nodeType: type,
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
sortOrder: 1,
|
||||
required: false,
|
||||
status: "enabled"
|
||||
}
|
||||
}
|
||||
this.nodeDialogVisible = true
|
||||
this.$nextTick(() => this.$refs.nodeDialogForm && this.$refs.nodeDialogForm.clearValidate())
|
||||
},
|
||||
submitNodeDialog() {
|
||||
this.$refs.nodeDialogForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const resp = await this.$axios.post(loc() + "/saveNode", this.nodeDialogForm)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.nodeDialogVisible = false
|
||||
await this.loadTree()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
saveNodeBasic() {
|
||||
this.$refs.nodeForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const resp = await this.$axios.post(loc() + "/saveNode", this.nodeForm)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.selectedNode = Object.assign({}, this.nodeForm)
|
||||
await this.loadTree()
|
||||
await this.loadRule()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteNode(node) {
|
||||
this.$confirm("确定要删除该节点吗?若下级有内容将不允许删除。", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await this.$axios.post(loc() + "/deleteNode", { id: node.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.selectedNode = {}
|
||||
await this.loadTree()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
async toggleNode(node) {
|
||||
const status = node.status === "enabled" ? "disabled" : "enabled"
|
||||
const resp = await this.$axios.post(loc() + "/toggleNode", { id: node.id, status })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
await this.loadTree()
|
||||
}
|
||||
},
|
||||
async moveNode(node, direction) {
|
||||
const resp = await this.$axios.post(loc() + "/moveNode", { id: node.id, direction })
|
||||
if (resp.code === 0) {
|
||||
await this.loadTree()
|
||||
}
|
||||
},
|
||||
async setRequired(node) {
|
||||
const next = !node.required
|
||||
const data = Object.assign({}, node, { required: next })
|
||||
const resp = await this.$axios.post(loc() + "/saveNode", data)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
await this.loadTree()
|
||||
if (this.selectedNode.id === node.id) {
|
||||
this.nodeClick(Object.assign({}, node, { required: next }))
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadResources() {
|
||||
if (!this.selectedNode.id) return
|
||||
const resp = await this.$axios.post(loc() + "/resourcePage", Object.assign({}, this.resourcePageForm, { outlineId: this.selectedNode.id }))
|
||||
if (resp.code === 0) {
|
||||
this.resourceData = resp.data.list
|
||||
this.resourcePageForm.totalCount = resp.data.totalCount
|
||||
}
|
||||
},
|
||||
resourcePageOrder(column) {
|
||||
this.resourcePageForm.pageOrderName = column.prop
|
||||
this.resourcePageForm.pageOrderBy = column.order
|
||||
this.loadResources()
|
||||
},
|
||||
resourceSizeChange(val) {
|
||||
this.resourcePageForm.pageSize = val
|
||||
this.loadResources()
|
||||
},
|
||||
resourceNumberChange(val) {
|
||||
this.resourcePageForm.pageNumber = val
|
||||
this.loadResources()
|
||||
},
|
||||
resourceIndex(index) {
|
||||
return index + (this.resourcePageForm.pageNumber - 1) * this.resourcePageForm.pageSize + 1
|
||||
},
|
||||
openResourceForm(row = null, node = null) {
|
||||
const target = node || this.selectedNode
|
||||
if (!target.id) {
|
||||
this.$message.warning("请先选择章/节节点")
|
||||
return
|
||||
}
|
||||
if (row) {
|
||||
this.resourceForm = Object.assign({}, row, {
|
||||
outlineName: target.title,
|
||||
fileData: this.parseFiles(row.fileData)
|
||||
})
|
||||
} else {
|
||||
this.resourceForm = {
|
||||
courseId: target.courseId,
|
||||
outlineId: target.id,
|
||||
outlineName: target.title,
|
||||
resourceTitle: "",
|
||||
resourceType: "video",
|
||||
fileExt: "",
|
||||
fileData: [],
|
||||
durationSeconds: 0,
|
||||
sortOrder: 1,
|
||||
required: false,
|
||||
allowPreview: true,
|
||||
allowDownload: false,
|
||||
status: "enabled"
|
||||
}
|
||||
}
|
||||
this.resourceTypeChange(this.resourceForm.resourceType)
|
||||
this.resourceDialogVisible = true
|
||||
this.$nextTick(() => this.$refs.resourceForm && this.$refs.resourceForm.clearValidate())
|
||||
},
|
||||
resourceTypeChange(val) {
|
||||
const type = this.resourceTypeOptions.find(v => v.code === val)
|
||||
this.fileAccept = type ? type.accept : ""
|
||||
this.autoFillMediaDuration()
|
||||
},
|
||||
submitResource() {
|
||||
this.$refs.resourceForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const data = Object.assign({}, this.resourceForm)
|
||||
data.fileExt = this.getFileExt(data.fileData)
|
||||
if (!this.validResourceExt(data.resourceType, data.fileExt)) {
|
||||
this.$message.warning("当前资料类型不支持上传 ." + data.fileExt + " 格式文件")
|
||||
return
|
||||
}
|
||||
delete data.outlineName
|
||||
data.fileData = JSON.stringify(data.fileData || [])
|
||||
const resp = await this.$axios.post(loc() + "/saveResource", data)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.resourceDialogVisible = false
|
||||
this.loadResources()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteResource(row) {
|
||||
this.$confirm("确定要删除该学习资料吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await this.$axios.post(loc() + "/deleteResource", { id: row.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.loadResources()
|
||||
}
|
||||
})
|
||||
},
|
||||
async loadRule() {
|
||||
if (!this.selectedNode.id) return
|
||||
const resp = await this.$axios.post(loc() + "/getRule", {
|
||||
courseId: this.selectedNode.courseId,
|
||||
targetType: "outline",
|
||||
targetId: this.selectedNode.id
|
||||
})
|
||||
if (resp.code === 0) {
|
||||
this.ruleForm = Object.assign({}, resp.data, {
|
||||
minStudyMinutes: this.formatDurationMinutes(resp.data.minStudySeconds)
|
||||
})
|
||||
}
|
||||
},
|
||||
async saveRule() {
|
||||
const data = Object.assign({}, this.ruleForm, {
|
||||
minStudySeconds: Math.round((this.ruleForm.minStudyMinutes || 0) * 60)
|
||||
})
|
||||
delete data.minStudyMinutes
|
||||
const resp = await this.$axios.post(loc() + "/saveRule", data)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
await this.loadTree()
|
||||
await this.loadRule()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
},
|
||||
parseFiles(value) {
|
||||
if (!value) return []
|
||||
if (Array.isArray(value)) return value
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
getFileUrl(files) {
|
||||
const file = Array.isArray(files) && files.length ? files[0] : null
|
||||
if (!file) return ""
|
||||
return file.url || file.response?.data || file.data || ""
|
||||
},
|
||||
getFileExt(files) {
|
||||
const file = Array.isArray(files) && files.length ? files[0] : null
|
||||
if (!file) return ""
|
||||
const name = file.name || file.url || file.response?.data || ""
|
||||
const index = name.lastIndexOf(".")
|
||||
return index > -1 ? name.substring(index + 1).toLowerCase() : ""
|
||||
},
|
||||
formatDurationMinutes(durationSeconds) {
|
||||
const seconds = Number(durationSeconds || 0)
|
||||
return seconds > 0 ? Math.ceil(seconds / 60) : 0
|
||||
},
|
||||
formatDurationSeconds(durationSeconds) {
|
||||
const seconds = Math.ceil(Number(durationSeconds || 0))
|
||||
return seconds > 0 ? seconds + "秒" : "0秒"
|
||||
},
|
||||
autoFillMediaDuration() {
|
||||
if (!["video", "audio"].includes(this.resourceForm.resourceType)) return
|
||||
const url = this.getFileUrl(this.resourceForm.fileData)
|
||||
if (!url) return
|
||||
const media = document.createElement(this.resourceForm.resourceType === "video" ? "video" : "audio")
|
||||
media.preload = "metadata"
|
||||
media.onloadedmetadata = () => {
|
||||
window.URL.revokeObjectURL(media.src)
|
||||
if (isFinite(media.duration) && media.duration > 0) {
|
||||
// 媒体时长按秒保存,避免先取整到分钟再换算导致实际时长被放大。
|
||||
this.$set(this.resourceForm, "durationSeconds", Math.ceil(media.duration))
|
||||
}
|
||||
}
|
||||
media.onerror = () => {
|
||||
window.URL.revokeObjectURL(media.src)
|
||||
}
|
||||
media.src = url
|
||||
},
|
||||
validResourceExt(resourceType, fileExt) {
|
||||
const item = this.resourceTypeOptions.find(v => v.code === resourceType)
|
||||
if (!item || !item.accept) return true
|
||||
return item.accept.split(",").map(v => v.replace(".", "").toLowerCase()).includes(fileExt)
|
||||
},
|
||||
getResourceTypeName(code) {
|
||||
const item = this.resourceTypeOptions.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"resourceForm.fileData": {
|
||||
handler() {
|
||||
this.autoFillMediaDuration()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("click", () => this.contextVisible = false)
|
||||
},
|
||||
async created() {
|
||||
await this.loadCourseTypes()
|
||||
const courseId = this.getQueryParam("courseId")
|
||||
if (courseId) {
|
||||
this.currentCourseId = courseId
|
||||
this.courseQuery.courseId = courseId
|
||||
await this.loadCourses(true)
|
||||
this.$delete(this.courseQuery, "courseId")
|
||||
} else {
|
||||
await this.loadCourses()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user