...
 
Commits (37)
    https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/bcb6e080f30f025a14fdd34e32e08181e1d91448 修复搜索条件错误的bug 2022-10-17T17:03:59+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/f14ac4fff7e4509376de266e4f3a741e680d9a96 修复搜索条件错误的bug 2022-10-17T18:18:25+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/9526266e0d792d4c2adb991277038c3efbbe3d5d 增加登录日志查看功能 2022-10-26T17:15:15+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/fd5685e67c5177c064324f8f65bc9f4e619eea3d 增加登录日志查看功能 2022-10-26T17:15:55+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/beff6f8b09c512eab6f8c151fa1bc9178cd51f5f 概况增加注册人数的查看 2022-11-16T22:40:51+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/459e5956067faaccd6fbb5db2be3a291e6ac2c3e 概况增加注册人数的查看 2022-11-22T21:55:39+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/a2adc3cced83cf951b5522bb211321a99a08883f 处理vue3 echart5+ 折线图表tooltip无效 2022-11-24T16:01:13+08:00 水边哇 690713167@qq.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/8171ff19b858e533405432cd3b684901b13bdbaa 处理vue3 echart5+ 折线图表tooltip无效 2022-11-24T17:18:09+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/41693106707707bd5d5a18f676711571d0cb47e7 新增学习记录功能 2022-11-26T20:59:48+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/b6b744a4e98510770f2c248510f0bd6881c27036 课程数据查看明细 2022-11-28T10:31:18+08:00 水边哇 690713167@qq.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/b550a5eb9682de3b8be900210fc41e0bce53b956 新增学习记录功能 2022-11-28T11:17:43+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/5b5f38d6cd0bb6094b824601bb7cd0e948d1efd9 地址 2022-12-01T15:27:06+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/8ee33e09f981c1525da118ec94303a13c050f43b el-dialog弹窗 v-model 更换为:model-value 2023-01-13T10:01:02+08:00 水边哇 690713167@qq.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/a1474b11b9fc41d1250cc9a657361e0f895de457 Merge remote-tracking branch 'origin/dev' into dev 2023-01-13T10:02:14+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/70cb8619a73ca32a8c3eb34f6f18d78c45f21aaf 地址 2023-01-13T10:03:13+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/8ac691f13bea62bef6890839488ea26ccf26c09c 增加视频云初始化功能 2023-02-19T13:54:34+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/cfcdf969f858f9cd0430e58cde5cda2d07f351b9 增加用户数据功能 2023-03-26T19:13:25+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/a012bb9a35ed6eb38f1c622261f921fc79dd9265 增加用户数据功能 2023-04-27T14:42:25+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/df7c2ea8b27dd3e21f11f5f259ef44cf9604954d 增加用户数据功能 2023-04-27T14:46:50+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/8067c22afe4513ff68f09e2b1b0c95d12af527c4 增加用户数据功能 2023-04-27T14:47:55+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/5d2e4b36a5668a321f4744fffad8e44163064834 增加用户数据功能 2023-05-06T14:49:50+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/b7f80c960200ad4520cc41899d7728a68a83ce3f 增加用户数据功能 2023-05-06T14:51:10+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/01cce975d380e0fb1cf568597b543f55984ade21 增加用户数据功能 2023-05-06T14:51:27+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/22602934522104b5bfbb50b235a54e76503c4aea 升级版本 2023-05-23T16:43:52+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/9634b96d8369e476a61e5f1b742e692d031a4913 处理私有化上传 2023-06-05T11:22:43+08:00 chenqt 1620969243@qq.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/e9a4890d5366673b09be965a908ea67c05dfc431 优化配置 2023-06-05T15:07:39+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/60bb2a8db88d0d47b53a410bdf4e615ef2f62a62 优化配置 2023-06-05T15:17:32+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/378a59309a53c24ce6b5c17141af7b142fd0f366 优化配置 2023-06-05T15:18:59+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/7394ea92dab9fb5fa1b3c38e8428725bb1aa21da 优化配置 2023-06-05T15:53:46+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/6412e9a301d4c1a9092eb55eba5c6ccd1a3778e2 优化配置 2023-06-05T16:37:29+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/5a32d814eacbc25a41a8cf66d0e310242c949943 优化配置 2023-06-06T09:55:57+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/13f774709f77602d97855c35eaf1fc2245ab02b9 增加打版工具 2023-07-17T14:11:27+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/6cc27e5106707c425f347256806a3db1236360f5 修改 2023-07-18T16:04:38+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/5df6b067ca0e74df1ca4cd788ea51700e7ac3ec8 处理文档上传保存异常 2023-07-19T11:56:09+08:00 chenqt 1620969243@qq.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/c086cc5b15dee9c63f5bef7c1290fb4d942b0be7 更新 2023-07-19T11:57:23+08:00 chenqt 1620969243@qq.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/4a33175bd80ec64d5ddb32f03496c96d23b08360 文档上传功能完善 2023-07-19T15:03:54+08:00 fengyw fengyw@roncoo.com https://gitcode.net/roncoocom/roncoo-education-admin/-/commit/275bcf7a81a3800689590e25ecfa30346f78dcea 增加说明 2023-07-20T10:57:30+08:00 fengyw fengyw@roncoo.com
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2016-现在 LingKe Ltd.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<assembly>
<id>${project.version}</id>
<includeBaseDirectory>true</includeBaseDirectory>
<formats>
<!--<format>dir</format>-->
<!--<format>zip</format>-->
<format>tar.gz</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.basedir}/../dist</directory>
<outputDirectory>admin</outputDirectory>
</fileSet>
</fileSets>
</assembly>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.roncoo</groupId>
<artifactId>distribution</artifactId>
<version>23.0.0-RELEASE</version>
<packaging>pom</packaging>
<name>distribution</name>
<profiles>
<profile>
<id>release-education</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<finalName>admin</finalName>
</build>
</profile>
</profiles>
</project>
{
"name": "roncoo-education-admin",
"version": "11.0.0-RELEASE",
"version": "13.0.0-RELEASE",
"scripts": {
"dev": "vite",
"build": "vite build",
......@@ -13,6 +13,7 @@
"echarts": "^5.2.2",
"js-cookie": "2.2.0",
"lockr": "^0.8.5",
"mitt": "^3.0.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-browserify": "^1.0.1",
......@@ -22,7 +23,9 @@
"vue-router": "^4.*",
"vuex": "4.*",
"wangeditor": "^4.*",
"sortablejs": "^1.13.0"
"sortablejs": "^1.13.0",
"simple-uploader.js": "^0.6.0",
"browser-md5-file": "^1.1.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.2.2",
......
<template>
<el-config-provider :locale="locale === 'en'?ElementPlusEn:ElementPlusZhCn">
<el-config-provider :locale="locale === 'en' ? ElementPlusEn : ElementPlusZhCn">
<router-view />
<preview v-model="ctrl.showImageViewer" :type="opts.type" :url="opts.imageList" :video-info="opts.videoInfo" @callback="handleClosePreview" />
<fixed-upload-panel />
</el-config-provider>
</template>
<script>
// This starter template is using Vue 3 experimental <script setup> SFCs
// Check out https://github.com/vuejs/rfcs/blob/script-setup-2/active-rfcs/0000-script-setup.md
import {useStore} from "vuex";
import {useI18n} from "vue-i18n";
import {onMounted, ref} from "vue";
import {ElConfigProvider} from 'element-plus'
import { useStore } from "vuex";
import { useI18n } from "vue-i18n";
import { onMounted, ref, reactive, onUnmounted } from "vue";
import { ElConfigProvider } from 'element-plus'
import ElementPlusZhCn from "element-plus/lib/locale/lang/zh-cn";
import ElementPlusEn from "element-plus/lib/locale/lang/en";
import FixedUploadPanel from '@/components/Upload/FixedUploadPanel.vue';
import Preview from '@/views/course/material/preview.vue'
import bus from '@/utils/bus';
export default {
components:{[ElConfigProvider.name]:ElConfigProvider},
setup(){
const {locale} = useI18n()
return {
locale,
ElementPlusZhCn,
ElementPlusEn
}
}
components: {
[ElConfigProvider.name]: ElConfigProvider,
FixedUploadPanel,
Preview
},
setup() {
const { locale } = useI18n()
const opts = reactive({
imageList: '',
type: '',
videoInfo: {}
})
const ctrl = reactive({
showImage: false,
showImageViewer: false
})
const cloneImageDialog = () => {
ctrl.showImage = false
}
const handleOpenImage = (url) => {
opts.imageList = url
ctrl.showImage = true
}
const handleOpenPreview = (row) => {
opts.type = row.type
opts.videoInfo = deepCopy(row.videoInfo)
ctrl.showImageViewer = true
}
const handleClosePreview = () => {
ctrl.showImageViewer = false
}
onMounted(() => {
bus.on('handleOpenImage', handleOpenImage)
bus.on('handleShowVideo', handleOpenPreview)
})
onUnmounted(() => {
bus.off('handleOpenImage')
bus.off('handleShowVideo')
})
return {
locale,
ElementPlusZhCn,
ElementPlusEn,
ctrl,
opts,
cloneImageDialog,
handleClosePreview
}
}
}
</script>
......
import request from '@/utils/request'
import upload from '@/utils/upload';
// 专区课程分页
export function zoneCoursePage(data) {
......@@ -139,3 +140,37 @@ export function courseSave(data) {
export function courseDelete(data) {
return request.delete('/course/admin/course/delete?id=' + data.id, data)
}
// 分页
export function userCourseRecord(params, pageCurrent = 1, pageSize = 20) {
return request({url: '/course/admin/user/course/record', method: 'post', data: {pageCurrent: pageCurrent, pageSize: pageSize, ...params}})
}
// 分页
export function userStudyePage(params, pageCurrent = 1, pageSize = 20) {
return request({url: '/course/admin/user/study/page', method: 'post', data: {pageCurrent: pageCurrent, pageSize: pageSize, ...params}})
}
/**
* 资源库添加
* @param data
*/
export function resourceLibrarySave(data) {
return upload({
url: '/resource/admin/material/save',
method: 'post',
data
})
}
/**
* 素材信息预览
* @param data
* @returns {*}
*/
export function resourceLibraryPreview(data) {
return request({
url: '/resource/admin/material/preview',
method: 'post',
data
})
}
......@@ -80,6 +80,11 @@ export function linkDelete(data) {
return request.delete('/system/admin/website/link/delete?id=' + data.id, data)
}
// 视频云初始化
export function videoInit() {
return request({url: '/system/admin/sys/config/video/init', method: 'get'})
}
// 系统配置--列出
export function sysConfigList(data) {
return request({url: '/system/admin/sys/config/list', method: 'post', data: data})
......
......@@ -44,3 +44,13 @@ export function usersEdit(data) {
export function usersDelete(data) {
return request.delete('/user/admin/users/delete?id=' + data.id, data)
}
// 登录日志
export function logLoginPage(params, pageCurrent = 1, pageSize = 20) {
return request({url: '/user/admin/log/login/page', method: 'post', data: {pageCurrent: pageCurrent, pageSize: pageSize, ...params}})
}
// 分页
export function userCoursePage(params, pageCurrent = 1, pageSize = 20) {
return request({url: '/course/admin/user/course/page', method: 'post', data: {pageCurrent: pageCurrent, pageSize: pageSize, ...params}})
}
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="title" width="600px" @close="handleClose">
<el-dialog :model-value="visible" :append-to-body="true" :title="title" width="600px" @close="handleClose">
<el-form class="filter-container" inline label-width="100px" size="mini">
<el-form-item label="课程名称">
<el-input v-model="queryParams.courseName"/>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="title" width="600px" @close="handleClose">
<el-dialog :model-value="visible" :append-to-body="true" :title="title" width="600px" @close="handleClose">
<el-form class="filter-container" inline label-width="100px" size="mini">
<el-form-item label="讲师名称">
<el-input v-model="queryParams.lecturerName"/>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="title" width="600px" @close="handleClose">
<el-dialog :model-value="visible" :append-to-body="true" :title="title" width="800px" @close="handleClose">
<el-form class="filter-container" inline label-width="100px" size="mini">
<el-form-item label="资源名称">
<el-input v-model="queryParams.resourceName"/>
......@@ -16,7 +16,8 @@
<el-table-column label="资源类型" prop="resourceType" :width="200">
<template #default="scope">
<span>{{ resourceTypeEnums[scope.row.resourceType] }}</span><br>
<span>{{ formatDuring(scope.row.videoLength * 1000) }}</span>
<span v-if="scope.row.resourceType<3">{{ formatDuring(scope.row.videoLength * 1000) }}</span>
<span v-else>{{ scope.row.docPage }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
......
<!--全局上传文件列表组件-->
<template>
<div v-show="fileList.length > 0" id="FixedUploadPanel" class="fixed_upload_panel">
<div class="upload_file_container" :class="{ 'hidden_list': hideList }">
<div class="upload_head">
<div class="upload_text">{{ $t('uploader.upload') }}{{ successList.length }}/{{ fileList.length }}</div>
<div class="dialog_btn">
<el-icon v-if="hideList" class="" :title="$t('uploader.showList')"
@click="hideList = false"
>
<FullScreen/>
</el-icon>
<el-icon v-else class="ctrl_min_btn" :title="$t('uploader.min')" @click="hideList = true">
<Connection/>
</el-icon>
<el-icon class="close_btn" :title="$t('uploader.closeList')" @click="handleClosePanel">
<Close/>
</el-icon>
</div>
</div>
<div v-show="!hideList" class="upload_file_list">
<div v-for="item in fileList" :key="item.uid" class="upload_file_item">
<div class="file_type"><i :class="item.type | fileType"/></div>
<div class="file_name">{{ item.name }}</div>
<div class="file_size">{{ getSize(item.size) }}</div>
<div class="file_progress_number">{{ item.progress }} %</div>
<div class="file_status c-brand">{{ tipList[item.status] }}</div>
<div v-if="item.status === 'uploading'" class="file_ctrl_btn" @click.stop="stopUpload(item)">
<el-icon class="">
<el-icon-video-pause/>
</el-icon>
</div>
<div v-if="item.status === 'fail'" class="file_ctrl_btn" @click.stop="startUpload(item)">
<el-icon class="">
<el-icon-video-play/>
</el-icon>
</div>
<div v-if="item.status === 'stop'" class="file_ctrl_btn" @click.stop="resumeUpload(item)">
<el-icon class="">
<el-icon-video-play/>
</el-icon>
</div>
<div class="file_progress" :style="{ width: item.progress + '%' }"/>
</div>
</div>
</div>
</div>
</template>
<script>
import uploadVideo from '@/utils/mixin/uploadVideo';
import uploadFiles from '@/utils/mixin/UploadFiles';
import {getSize} from '@/utils/utils';
import {saveResource} from '@/api/upload';
import bus from '@/utils/bus';
import {FullScreen, Close, Connection} from '@element-plus/icons-vue'
export default {
name: 'FixedUploadPanel',
components: {FullScreen, Close, Connection},
filters: {
fileType(val) {
if (val === 'image') {
return 'el-icon-picture-outline'
} else if (val === 'video') {
return 'el-icon-video-play'
} else {
return 'el-icon-tickets'
}
}
},
mixins: [uploadVideo, uploadFiles],
data() {
return {
that: this,
fileTypeInt: {'video': 1, 'audio': 2, 'document': 3, 'zip': 5, 'image': 4},
hideList: false,
files: [],
tipList: {
success: this.$t('uploader.success'),
fail: this.$t('uploader.fail'),
uploading: this.$t('uploader.uploading'),
stop: this.$t('uploader.stop'),
save: this.$t('uploader.save'),
saveSuccess: this.$t('uploader.saveSuccess'),
saveFail: this.$t('uploader.saveFail'),
transcoding: this.$t('uploader.transcoding'),
md5: this.$t('uploader.md5'),
merging: this.$t('uploader.merging'),
ready: this.$t('uploader.other')
}
}
},
computed: {
fileList() {
return [...this.$store.getters['upload/fileList']]
},
successList() {
const arr = []
this.fileList.map(item => {
if (item.status === 'saveSuccess') {
arr.push(item)
}
})
return arr
}
},
watch: {
fileList() {
if (!this.uploading) {
this.beginUpload()
}
}
},
mounted() {
// console.log(this.$store.getters)
const _that = this
window.onbeforeunload = function(e) {
const event = window.event || e;
if (_that.successList.length !== _that.fileList.length) {
event.returnValue = (_that.$t('uploader.noUploadSuccess'));
}
}
},
methods: {
getSize,
fileStatus(val) {
if (this.tipList[val]) {
return this.tipList[val]
} else {
return this.tipList['other']
}
},
beginUpload() {
let startFile
this.fileList.map(item => {
if (item.status === 'ready' && !startFile) {
startFile = item
}
})
if (startFile) {
this.hideList = false;
if (startFile.type === 'video' || startFile.type === 'audio') {
this.startUpload(startFile)
} else if (startFile.type === 'image') {
this.UploadFile(startFile)
} else {
this.UploadDoc(startFile)
}
}
},
// 保存文件
savaVideo(data, fileInfo) {
const form = {
resourceType: this.fileTypeInt[fileInfo.type],
resourceName: data.materialName,
resourceSize: fileInfo.materialSize,
resourceUrl: data.materialUrl || data.docUrl,
docPage: data.pageCount,
width: data.width,
height: data.height,
...fileInfo.custom
}
if (data.vid || data.fid) {
form.videoVid = data.vid || data.fid
}
this.$store.commit('upload/UPLOAD_FILE_STATUS', fileInfo)
fileInfo.status = 'save'
saveResource(form).then(res => {
console.log('saveResource', res)
if (res) {
fileInfo.status = 'saveSuccess'
bus.emit('uploadFileSuccess')
} else {
fileInfo.status = 'saveFail'
}
}).catch(() => {
fileInfo.status = 'saveFail'
});
this.nextUpload()
},
// 上传下一个
nextUpload() {
console.log('下一个')
let startFile
this.fileList.map(item => {
if (item.status === 'ready' && !startFile) {
startFile = item
}
})
// console.log(startFile)
if (startFile) {
if (startFile.type === 'video' || startFile.type === 'audio') {
this.startUpload(startFile)
} else if (startFile.type === 'image') {
this.UploadFile(startFile)
} else {
this.UploadDoc(startFile)
}
} else {
this.hideList = true
this.uploading = false
}
},
// 关闭面板
handleClosePanel() {
let startFile
this.fileList.map(item => {
if (item.status !== 'saveSuccess') {
startFile = item
}
})
if (startFile) {
this.$confirm(this.$t('uploader.uploadingTip'), this.$t('uploader.warning'), {
type: 'warning',
cancelButtonText: this.$t('cancel'),
confirmButtonText: this.$t('determine')
}).then(() => {
this.$store.commit('upload/REMOVE_LIST')
this.uploading = false
});
} else {
this.$store.commit('upload/REMOVE_LIST')
this.uploading = false
}
}
}
}
</script>
<style lang="scss" scoped>
.fixed_upload_panel {
position: fixed;
right: 0;
bottom: 0;
z-index: 9999;
}
.upload_file_container {
background-color: #fff;
box-shadow: -1px -1px 4px #ddd;
border-radius: 6px 0 0 0;
overflow: hidden;
width: 540px;
transition: width .3s;
&.hidden_list {
width: 250px;
}
.upload_head {
display: flex;
align-items: center;
padding: 0 12px;
height: 40px;
color: #666;
background-color: #f7f7f7;
border-bottom: 1px solid #eee;
.upload_text {
flex: 1;
}
}
.dialog_btn {
text-align: right;
width: 100px;
.ctrl_min_btn {
transform: rotateX(180deg);
}
i {
cursor: pointer;
padding: 2px;
font-size: 18px;
}
.close_btn {
margin-left: 10px;
}
}
}
.upload_file_list {
width: 540px;
max-height: 30vh;
overflow: auto;
.upload_file_item {
position: relative;
padding: 0 20px;
display: flex;
line-height: 45px;
border-bottom: 1px solid #dedede;
font-size: 12px;
div {
padding: 0 12px;
position: relative;
z-index: 2;
}
.file_type {
padding: 0;
font-size: 18px;
}
.file_name {
width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file_size {
width: 80px;
color: #666;
text-align: center;
}
.file_status {
text-align: center;
width: 80px;
}
.file_ctrl_btn {
font-size: 20px;
color: #237cc3;
}
.file_progress {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 0;
z-index: 1;
padding: 0;
background: #ebf7fe;
transition: width .5s;
}
}
}</style>
<!--
** 上传文件按钮
** params
**** refId: 关联ID, 如果存在,上传完成后自动调用关联接口,把资源与关联ID关联
**** fileType: 文件类型: video(视频)、image(图片)、 pdf、audio(音频)
**** mode: 上传模式: async(后台上传,不影响业务操作,上传完成关联refId) await(等待上传,上传完成执行回调,业务需要等待完成才可以继续进行)
**** @success 成功回调函数,mode=await时进行回调,返回文件名及文件链接
-->
<template>
<div v-if="fileInfo.status === 'uploading'" class="upload-btn uploading_panel" style="width: 80px;">
<el-progress class="progress-panel" :text-inside="true" :stroke-width="28" :percentage="fileInfo.progress"/>
<el-button class="close-btn" plain type="primary" @click="handleCloseUpload">{{ $t('cancelUpload') }}</el-button>
</div>
<span v-else>
<el-upload
ref="local"
multiple
class="upload-btn"
action="#"
:show-file-list="false"
:accept="accept"
:on-change="handleChange"
:before-upload="handleBeforeUpload"
:http-request="upload"
> <el-button :icon="icon" :type="btnType" :plain="plain" :class="btnClass">
{{ btnText }}
<slot name="btn-content"/>
</el-button>
</el-upload>
</span>
</template>
<script>
import uploadVideo from '@/utils/mixin/uploadVideo';
import uploadFiles from '@/utils/mixin/UploadFiles';
import {mapGetters} from 'vuex';
import {uploadDoc, saveResource} from '@/api/upload';
import {ElMessage} from 'element-plus';
export default {
name: 'UploaderBtn',
mixins: [uploadVideo, uploadFiles],
props: {
refId: { // 关联ID
type: [String, Number],
default: ''
},
imageTip: {
type: String,
default: ''
},
imageSize: {
type: Number,
default: 10
},
fileType: { // 文件类型
type: String,
default: 'file'
},
btnText: { // 按钮文本
type: String,
default: '点击上传'
},
plain: { // 是否为朴素按钮
type: Boolean,
default: true
},
icon: { // 按钮icon
type: String,
default: ''
},
btnType: { // 按钮类型
type: String,
default: 'primary'
},
custom: { // 保存文件拓展字段
type: Object,
default: () => {
return {}
}
},
maxLength: { // 最大上传数量
type: Number,
default: 5
},
mode: { // 文件类型
type: String,
default: 'await'
},
btnClass: {
type: String,
default: ''
}
},
data() {
return {
fileList: [],
fileInfo: {},
fileTypeInt: {'video': 1, 'audio': 2, 'document': 3, 'zip': 5, 'image': 4}
}
},
computed: {
...mapGetters({
'websiteInfo': 'app/websiteInfo'
}),
accept() {
if (this.fileType === 'video') {
return 'video/mp4,video/avi,video/mpg,video/mpeg,video/ram,video/flv,video/mov,video/asf,video/3gp,video/f4v,video/wmv,video/x-ms-wmv'
} else if (this.fileType === 'image') {
return 'image/jpeg,image/png,image/gif,image/x-icon'
} else if (this.fileType === 'audio') {
return 'audio/mpeg,audio/wma,audio/wav,audio/ape,audio/flac,audio/ogg,audio/aac'
} else if (this.fileType === 'document') {
return '.pdf,.ppt,.pptx,.doc,.docx,.xls,.xlsx'
} else if (this.fileType === 'zip') {
return '.zip,.rar'
}
return '*'
}
},
methods: {
handleChange(file, fileList) {
if (fileList.length > 1) {
fileList.splice(0, 1);
}
},
upload(file) {
this.status = 1
if (this.mode === 'async') {
this.$store.dispatch('upload/uploadFile', {refId: this.refId, file: file.file, custom: this.custom})
} else {
this.awaitUpload(file.file)
}
},
awaitUpload(_file) {
const type = _file.type.split('/')
const fileInfo = {
status: 'ready',
name: _file.name,
size: _file.size,
type: type[0],
uid: _file.uid,
progress: 0,
file: _file
}
let name = ''
const nameType = _file.name.split('.')
const nameTypes = nameType[nameType.length - 1]
if (_file.type) {
name = _file.type.split('/')[0]
} else {
if (['3gp', 'asf', 'avi', 'dat', 'flv', 'f4v', 'm4v', 'mkv', 'mov', 'mp4', 'mpe', 'mpg', 'mpeg', 'rmvb', 'vob', 'wmv'].indexOf(nameTypes) > -1) {
name = 'video'
_file.resourceType = 1
} else if (['aac', 'wav', 'wma', 'mp3'].indexOf(nameTypes) > -1) {
name = 'audio'
_file.resourceType = 2
} else if (['png', 'jpg', 'jpeg'].indexOf(nameTypes) > -1) {
name = 'image'
_file.resourceType = 4
} else if (['zip', 'rar'].indexOf(nameTypes) > -1) {
name = 'zip'
_file.resourceType = 5
} else if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'ppt', 'pptx'].indexOf(nameTypes) > -1) {
name = 'document'
_file.resourceType = 3
}
}
this.fileInfo = fileInfo
if (name === 'video' || name === 'audio') {
this.startUpload(fileInfo)
} else if (name === 'image') {
this.UploadFile(fileInfo, (data) => {
this.$emit('success', data)
})
} else if (['zip', 'pdf'].indexOf(name) > -1) {
this.UploadDoc(fileInfo)
}
},
handleExceed() {
this.$refs.local.clearFiles()
ElMessage.error('单次最多上传5个文件')
},
handleBeforeUpload(file) {
if (file.size === 0) {
ElMessage.error('不能上传大小为0kb的文件')
return false
}
let name = ''
const nameType = file.name.split('.')
const nameTypes = nameType[nameType.length - 1]?.toLowerCase();
if (['3gp', 'asf', 'avi', 'dat', 'flv', 'f4v', 'm4v', 'mkv', 'mov', 'mp4', 'mpe', 'mpg', 'mpeg', 'rmvb', 'vob', 'wmv'].indexOf(nameTypes) > -1) {
name = 'video'
} else if (['aac', 'wav', 'wma', 'mp3'].indexOf(nameTypes) > -1) {
name = 'audio'
} else if (['png', 'jpg', 'jpeg'].indexOf(nameTypes) > -1) {
name = 'image'
} else if (['zip', 'rar'].indexOf(nameTypes) > -1) {
name = 'zip'
} else if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'ppt', 'pptx'].indexOf(nameTypes) > -1) {
name = 'document'
}
switch (name) {
case 'video':
return this.beforeUploadVideo(name, nameTypes, file)
case 'audio':
return this.beforeUploadAudio(name, nameTypes, file)
case 'image':
return this.beforeUploadPic(name, nameTypes, file)
case 'document':
return this.beforeUploadDocument(name, nameTypes, file)
case 'zip':
return this.beforeUploadRar(name, nameTypes, file)
default:
ElMessage.error(this.$t('uploader.uploadError'))
return false
}
},
beforeUploadPic(type, nameType, file) {
// const ids = this.$store.state.upload.fileList.map(el => el.resourceName)
// if (ids.indexOf(file.name) > -1) {
// ElMessage.error(this.$t('learn.errorName'))
// return false
// }
const isLt10M = file.size / 1024 / 1024 < this.imageSize;
if (['jpg', 'jpeg', 'png'].indexOf(nameType) === -1) {
ElMessage.error(this.$t('uploader.errorUploadPic'))
return false
}
if (!isLt10M) {
ElMessage.error(this.imageTip || this.$t('uploader.errorUploadPicSize'))
return false;
}
},
beforeUploadVideo(type, nameType, file) {
const isLt10G = file.size / 1024 / 1024 / 1024 < 10;
if (['3gp', 'asf', 'avi', 'dat', 'flv', 'f4v', 'm4v', 'mkv', 'mov', 'mp4', 'mpe', 'mpg', 'mpeg', 'rmvb', 'vob', 'wmv'].indexOf(nameType) === -1) {
ElMessage.error(this.$t('uploader.errorUploadVideo'))
return false
}
if (!isLt10G) {
ElMessage.error(this.$t('uploader.errorUploadVideoSize'))
return false;
}
},
beforeUploadAudio(type, nameType, file) {
const isLt1G = file.size / 1024 / 1024 / 1024 < 1;
if (['aac', 'wav', 'wma', 'mp3'].indexOf(nameType) === -1) {
ElMessage.error(this.$t('uploader.errorUploadAudio'))
return false
}
if (!isLt1G) {
ElMessage.error(this.$t('uploader.errorUploadAudioSize'))
return false;
}
},
beforeUploadDocument(type, nameTypes, file) {
// const ids = this.$store.state.upload.fileList.map(el => el.resourceName)
// if (ids.indexOf(file.name) > -1) {
// ElMessage.error(this.$t('learn.errorName'))
// return false
// }
if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'ppt', 'pptx'].indexOf(nameTypes) > -1) {
const isLt1G = file.size / 1024 / 1024 / 1024 < 1;
if (!isLt1G) {
ElMessage.error(this.$t('uploader.errorUploadDocumentSize'))
return false;
}
} else {
ElMessage.error(this.$t('uploader.uploadErrorDocumentMessage'))
return false;
}
},
beforeUploadRar(type, nameTypes, file) {
// const ids = this.$store.state.upload.fileList.map(el => el.resourceName)
// if (ids.indexOf(file.name) > -1) {
// ElMessage.error(this.$t('learn.errorName'))
// return false
// }
if (['zip', 'rar'].indexOf(nameTypes) === -1) {
ElMessage.error(this.$t('uploader.errorUploadZip'))
return false
} else {
type = 'zip'
}
if (type === 'zip') {
const isLt2G = file.size / 1024 / 1024 / 1024 < 2;
if (!isLt2G) {
ElMessage.error('uploader.errorUploadZipSize')
return false;
}
} else {
ElMessage.error(this.$t('uploader.uploadZipErrorMessage'))
return false
}
},
uploadDoc(_file) {
const that = this
uploadDoc(_file, {
progress: function(p, checkpoint) {
// 断点记录点。浏览器重启后无法直接继续上传,您需要手动触发上传操作。
_file.tempCheckpoint = checkpoint;
console.log(checkpoint, p)
_file.progress = parseInt(p * 100)
_file.status = 'uploading'
},
// parallel: 5, // 分片数量
// partSize: 1024 * 1024 * 40, // 分片大小
meta: {year: 2020, people: 'test'},
mime: _file.file.type
}).then(result => {
that.fileUploading = false
_file.status = 'success'
_file.file = null;
_file.tempCheckpoint = null;
// 3 就是pdf 要设置动态读取 有权限读取 视频音频都是冷读取的一个月只读一次
if (_file.type === 'pdf') {
this.aliFileClient.putACL(result.name, 'private')
}
const fileUrl = that.aliyunOssUrl + result.name
const _data = {
url: fileUrl,
name: _file.name
}
if (that.saveFile) that.saveFile(_data, _file);
}).catch(error => {
that.fileUploading = false
console.error(error)
if (error.status === 0) {
_file.status = 'stop'
} else {
_file.status = 'fail'
}
})
},
// 取消上传
handleCloseUpload() {
if (this.fileInfo.type === 'video') {
this.stopUpload(this.fileInfo)
} else {
this.stopUploadFile(this.fileInfo)
}
},
// 保存文件
savaFile(data, fileInfo) {
if (this.refId) {
const refId = this.refId === '0' ? undefined : this.refId
const form = {
userNo: refId,
resourceName: fileInfo.name,
vodPlatform: fileInfo.videoType,
resourceType: this.fileTypeInt[fileInfo.type],
...this.custom,
...fileInfo
}
if (data.vid) {
form.videoVid = data.vid
} else {
form.resourceUrl = data.url
}
fileInfo.status = 'save'
saveResource(form).then(res => {
fileInfo.status = 'saveSuccess'
form.coursewareId = res.data
this.$emit('success', form)
}).catch(() => {
fileInfo.status = 'saveFail'
});
} else {
this.$emit('success', data)
}
// this.nextUpload()
},
// 保存文件
savaVideo(data, fileInfo) {
if (this.refId) {
const refId = this.refId === '0' ? undefined : this.refId
const form = {
materialName: fileInfo.name,
vodPlatform: fileInfo.videoType,
materialType: this.fileTypeInt[fileInfo.type],
...this.custom,
...fileInfo
}
if (data.vid) {
form.vid = data.vid
} else {
form.resourceUrl = data.url
}
fileInfo.status = 'save'
saveResource(form).then(res => {
fileInfo.status = 'saveSuccess'
form.coursewareId = res.data
this.$emit('success', form)
}).catch(() => {
fileInfo.status = 'saveFail'
});
} else {
this.$emit('success', data)
}
// this.nextUpload()
}
}
}
</script>
<style lang="scss" scoped>
.upload-btn {
display: inline-block;
// margin-left: 10px;
// margin-right: 10px;
}
.uploading_panel {
.progress-panel {
}
.close-btn {
display: none;
}
&:hover {
.progress-panel {
display: none;
}
.close-btn {
display: block;
}
}
}
</style>
<style lang="scss">
.uploading_panel {
.el-progress-bar__outer, .el-progress-bar__inner {
border-radius: 3px;
}
}
</style>
<template>
<el-dialog
v-model="visible"
:model-value="visible"
:before-close="handleClose"
width="450px"
title="修改密码"
......
export default {
name: '中文',
welcomeMsg: '欢迎来到领课教育系统'
welcomeMsg: '欢迎来到领课教育系统',
determine: '确定',
cancel: '取消',
uploader: {
success: '上传成功',
fail: '上传失败',
uploading: '正在上传',
stop: '暂停上传',
save: '正在保存',
saveSuccess: '保存成功',
saveFail: '保存失败',
md5: '校验MD5',
merging: '合并中',
transcoding: '转码中',
other: '准备中',
noUploadSuccess: '有文件未上传完成,确定离开当前页面吗?',
uploadingTip: '有文件上传未完成,关闭会造成数据丢失请确认',
uploadImgTips: '请上传JPG, PNG格式的图片,建议尺寸为900x500px,图片小于2M。',
uploadWebLogoTips: '请上传JPG, PNG格式的图片,建议尺寸为510x108px,图片小于2M。',
uploadIconLogoTips: '请上传JPG, PNG格式的图片,建议尺寸为32x32px,图片小于2M。',
uploadAvatarTips: '请上传JPG, PNG格式的图片,建议尺寸为183x183px,图片小于2M。',
uploadIconTips: '建议尺寸100*100px,支持格式JPG、JPEG、PNG',
warning: '警告',
upload: '文件上传',
showList: '展开列表',
min: '最小化',
closeList: '关闭列表',
uploadError: '上传的格式不正确,文件并不是视频(3GP、ASF、AVI、DAT、FLV、F4V、M4V、MKV、MOV、MP4、MPE、MPG、MPEG、RMVB、VOB、WMV)、音频(MP3、AAC、WAV、WMA)、图片(jpg、jpeg、png)、压缩包(zip、rar)、文档(pdf、doc、docx、xls、xlsx、txt 、ppt、pptx)',
errorName: '上传文件已存在',
errorUpload: '上传格式不正确,不是视频、音频、图片格式',
errorUploadPic: '上传图片只能是 jpg,jpeg,png 格式!',
errorUploadPicSize: '上传图片不能超过10M!',
errorUploadVideo: '上传视频只能是 3GP、ASF、AVI、DAT、FLV、F4V、M4V、MKV、MOV、MP4、MPE、MPG、MPEG、RMVB、VOB、WMV 格式!',
errorUploadVideoSize: '上传视频不能超过10G!',
errorUploadAudio: '上传音频只能是 aac、mp3、wav、wma 格式!',
errorUploadAudioSize: '上传音频不能超过1G!',
errorUploadDocument: '上传文档只能是 pdf、doc、docx、xls、xlsx、txt、ppt、pptx 格式!',
errorUploadDocumentSize: '上传文档不能超过1G!',
uploadErrorDocumentMessage: '上传格式不正确,不是文档格式',
errorUploadZip: '上传压缩包只能是 zip,rar 格式!',
errorUploadZipSize: '上传压缩包不能超过2G!',
uploadZipErrorMessage: '上传格式不正确,不是zip、rar压缩包格式',
uploadTitle: '本地素材上传',
storageLocation: '存储位置:',
uploadVideoAndAudio: '上传视频音频',
uploadVideoTip: '支持视频、音频、图片',
uploadDocument: '上传文档',
uploadDocumentTip: '支持导入文档格式文件',
uploadZip: '上传压缩包',
uploadZipTip: '支持导入压缩包文件',
uploadTip: '点击分类图标上传文件,单次最多上传5个文件',
uploadMaxFile: '单次最多上传5个文件'
},
}
......@@ -125,6 +125,13 @@ const asyncRouterMapList = [
component: () => import('@/views/course/list/chapter/index.vue'),
meta: {title: '章节管理'}
},
{
path: 'record',
name: 'CourseRecord',
hidden: true,
component: () => import('@/views/course/list/record/index.vue'),
meta: {title: '学习管理'}
},
{
path: 'resource',
name: 'CourseResource',
......@@ -149,8 +156,20 @@ const asyncRouterMapList = [
},
{
path: 'lecturer',
name: 'CourseLecturer',
name: 'UsersLecturer',
component: () => import('@/views/users/lecturer/index.vue')
},
{
path: 'record',
name: 'UserRecord',
hidden: true,
component: () => import('@/views/users/list/record/index.vue'),
meta: {title: '学习管理'}
},
{
path: 'logLogin',
name: 'usersLogLogin',
component: () => import('@/views/users/logLogin/index.vue')
}
]
},
......
......@@ -7,6 +7,7 @@ import menu from './modules/menu'
import tags from './modules/tags'
import opts from './modules/opts'
import permission from './modules/permission'
import upload from './modules/upload'
const store = createStore({
modules: {
......@@ -16,7 +17,8 @@ const store = createStore({
menu,
tags,
opts,
permission
permission,
upload
},
getters
})
......
const state = {
fileList: []
}
const mutations = {
UPLOAD_FILE: (state, file) => {
console.log(file)
state.fileList.push(file)
},
REMOVE_LIST: (state) => {
state.fileList = []
},
UPLOAD_FILE_STATUS: (state, file) => {
state.fileList.map(item => {
if (item.uid === file.uid) {
item = file
}
})
}
}
const actions = {
uploadFile({commit}, file) {
const _file = file.file
const type = _file.type.split('/')
const uploadInfo = {
status: 'ready',
name: _file.name,
materialName: _file.name,
materialSize: _file.size,
size: _file.size,
type: type[0],
uid: _file.uid,
progress: 0,
visibleRange: 1,
...file
}
const nameType = _file.name.split('.')
const nameTypes = nameType[nameType.length - 1]
if (['3gp', 'asf', 'avi', 'dat', 'flv', 'f4v', 'm4v', 'mkv', 'mov', 'mp4', 'mpe', 'mpg', 'mpeg', 'rmvb', 'vob', 'wmv'].indexOf(nameTypes) > -1) {
uploadInfo.resourceType = 1
} else if (['aac', 'wav', 'wma', 'mp3'].indexOf(nameTypes) > -1) {
_file.resourceType = 2
} else if (['png', 'jpg', 'jpeg'].indexOf(nameTypes) > -1) {
uploadInfo.resourceType = 4
} else if (['zip', 'rar'].indexOf(nameTypes) > -1) {
uploadInfo.resourceType = 5
uploadInfo.type = 'zip'
} else if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'ppt', 'pptx'].indexOf(nameTypes) > -1) {
uploadInfo.resourceType = 3
uploadInfo.type = 'document'
}
if (_file.type === '') {
const name = _file.name.split('.')
if (name[name.length - 1] === 'rar' || name[name.length - 1] === 'zip') {
uploadInfo.type = 'zip'
}
}
console.log(uploadInfo.type)
commit('UPLOAD_FILE', uploadInfo)
}
}
const getters = {
fileList(state) {
return state.fileList
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
......@@ -80,6 +80,7 @@
padding-top: 20px;
// flex: 1;
overflow-y: auto;
text-align: center;
}
}
......
import mitt from 'mitt';
const bus = mitt()
export default bus
import {uploadDoc, uploadPic} from '@/api/upload'
export default {
data() {
return {
filePlatType: 2, // 存储平台, 1:阿里云,2:本地存储(MinIO)
aliFileClient: undefined, // 阿里上传SDK实例
aliFileResumeClient: undefined, // 阿里续传SDK实例
tempFileCheckpoint: undefined, // 续传对象
fileDirectoryPath: '',
aliyunOssUrl: '',
fileUploading: false,
cancelToken: {},
type: 1
}
},
mounted() {
// this.initFileOssConfig()
},
methods: {
// 暂停上传
stopUploadFile(_file) {
if (this.type === 1) {
console.log(this.cancelToken)
if (this.cancelToken && this.cancelToken.cancel) {
this.cancelToken.cancel()
}
} else {
_file.status = 'stop'
this.aliClient && this.aliClient.cancel()
this.aliResumeClient && this.aliResumeClient.cancel()
}
},
// 上传本地(doc)
UploadDoc(_file, callback) {
this.type = 1
console.log('_file', _file)
const that = this
that.cancelToken = {}
uploadDoc(_file, (p) => {
_file.progress = p
_file.status = 'uploading'
}, that.cancelToken).then(res => {
const _data = {
materialUrl: res.fileUrl,
materialName: _file.name,
pageCount: res.pageCount,
...res
}
console.log(res, _data)
if (that.savaVideo) that.savaVideo(_data, _file);
if (callback) callback(_data)
}).catch(() => {
_file.status = 'saveFail'
})
},
getWidthHeight(file) {
return new Promise((resolve) => {
var reader = new FileReader();
reader.readAsDataURL(file.file);
reader.onload = function() { // 让页面中的img标签的src指向读取的路径
var img = new Image()
img.src = reader.result
if (img.complete) { // 如果存在浏览器缓存中
file.width = img.width
file.height = img.height
} else {
img.onload = function() {
file.width = img.width
file.height = img.height
resolve(file)
}
}
}
})
},
// 上传本地(minIO)
UploadFile(file, callback) {
this.getWidthHeight(file).then(_file => {
this.type = 1
console.log('_file', _file)
const that = this
that.cancelToken = {}
const form = new FormData()
form.append('picFile', _file.file)
uploadPic(form, (p) => {
_file.progress = p
_file.status = 'uploading'
}, that.cancelToken).then(res => {
const _data = {
materialUrl: res.fileUrl,
materialName: _file.name,
width: _file.width,
height: _file.height,
entConfigType: 1,
...res
}
console.log(res, _data)
if (that.savaVideo) that.savaVideo(_data, _file);
if (callback) callback(_data)
}).catch(() => {
_file.status = 'fail'
})
})
},
// ali-oss上传
multipartUploadFile(_file, callback) {
const that = this
if (that.fileUploading) {
return
}
that.fileUploading = true
if (this.filePlatType === 2) {
this.minIOUploadFile(_file, callback)
return false
}
_file.fileName = this.fileRandomString() + '.' + _file.file.type.substr(_file.file.type.indexOf('/') + 1)
_file.isAliUpload = true;
// object-key可以自定义为文件名(例如file.txt)或目录(例如abc/test/file.txt)的形式,实现将文件上传至当前Bucket或Bucket下的指定目录。
console.log(this.fileDirectoryPath + _file.fileName)
this.aliFileClient.multipartUpload(this.fileDirectoryPath + _file.fileName, _file.file, {
progress: function(p, checkpoint) {
// 断点记录点。浏览器重启后无法直接继续上传,您需要手动触发上传操作。
_file.tempCheckpoint = checkpoint;
console.log(checkpoint, p)
_file.progress = parseInt(p * 100)
_file.status = 'uploading'
},
// parallel: 5, // 分片数量
// partSize: 1024 * 1024 * 40, // 分片大小
meta: {year: 2020, people: 'test'},
mime: _file.file.type
}).then(result => {
that.fileUploading = false
_file.status = 'success'
_file.file = null;
_file.tempCheckpoint = null;
// 3 就是pdf 要设置动态读取 有权限读取 视频音频都是冷读取的一个月只读一次
if (_file.type === 'pdf') {
this.aliFileClient.putACL(result.name, 'private')
}
const fileUrl = that.aliyunOssUrl + result.name
const _data = {
url: fileUrl,
name: _file.name
}
if (that.savaVideo) that.savaVideo(_data, _file);
if (callback) callback(_data)
}).catch(error => {
that.fileUploading = false
that.uploading = false
console.error(error)
if (error.status === 0) {
_file.status = 'stop'
} else {
_file.status = 'fail'
}
if (callback) callback()
})
},
// ali-oss续传
aliResumeUploadFile(_file, callback) {
const that = this
// object-key可以自定义为文件名(例如file.txt)或目录(例如abc/test/file.txt)的形式,实现将文件上传至当前Bucket或Bucket下的指定目录。
this.aliFileResumeClient.multipartUpload(this.fileDirectoryPath + _file.fileName, _file.file, {
progress: function(p, checkpoint) {
// 断点记录点。浏览器重启后无法直接继续上传,您需要手动触发上传操作。
_file.tempCheckpoint = checkpoint;
_file.progress = parseInt(p * 100)
_file.status = 'uploading'
},
checkpoint: that.tempCheckpoint,
meta: {year: 2020, people: 'test'},
mime: _file.file.type
}).then(result => {
_file.status = 'success'
_file.file = undefined;
_file.tempCheckpoint = undefined;
// 3 就是pdf 要设置动态读取 有权限读取 视频音频都是冷读取的一个月只读一次
if (_file.type === 'pdf') {
this.aliFileClient.putACL(result.name, 'private')
}
const fileUrl = that.aliyunOssUrl + result.name
const _data = {
url: fileUrl,
name: _file.file.name
}
if (that.savaVideo) that.savaVideo(_data, _file);
if (callback) callback(_data)
}).catch(error => {
if (error.status === 0) {
_file.status = 'stop'
} else {
_file.status = 'fail'
}
})
},
fileRandomString(len = 32) {
const $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789';
const maxPos = $chars.length;
let pwd = '';
for (let i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
}
}
// 私有化分片上传
import SimpleUploader from 'simple-uploader.js'
import BMF from 'browser-md5-file';
export default {
data() {
return {
simpleClient: undefined, // 分片上传实例
videoPlatType: null,
vodModel: null,
_simFileList: {}
}
},
methods: {
// 初始化分片上传实例
initSimpleUpload(config) {
console.log(config)
if (this.simpleClient) return
const that = this
const bmf = new BMF();
const uploader = new SimpleUploader({
target: config.uploadUrl,
chunkSize: '2048000',
simultaneousUploads: 2000,
fileParameterName: 'file',
maxChunkRetries: 3,
testChunks: true, // 是否开启服务器分片校验
// 服务器分片校验函数,秒传及断点续传基础
checkChunkUploadedByResponse: function(chunk, message) {
let objMessage = {}
try {
objMessage = JSON.parse(message);
console.log(objMessage)
} catch (error) {
console.log(error)
}
if (objMessage.code !== 200) {
// ElMessage.error(objMessage.msg)
// opts.FileId = chunk.file.id
// const index = uploader.value.fileList.map((el) => el.id).indexOf(chunk.file.id)
// itemRefs[index]._fileError()
// statusSet(opts.FileId, 'progress');
}
if (objMessage.data?.uploadStatusId === 2) {
return true;
} else {
return false;
}
// return objMessage.code === 200
},
headers: {
token: that.$store.getters.token
// Authorization: Ticket.get() && "Bearer " + Ticket.get().access_token
},
query: {
isChunk: true,
...config
}
})
if (!uploader.support) return
this.simpleClient = uploader
// 文件添加 单个文件
uploader.on('fileAdded', function(file, event) {
console.log(file, event)
file.pause();
that._simFileList[file.file.uid].status = 'md5'
bmf.md5(
file.file,
(err, md5) => {
console.log('err:', err);
console.log('md5 string:', md5); // 97027eb624f85892c69c4bcec8ab0f11
file.uniqueIdentifier = md5;
console.log(uploader)
file.resume();
},
// progress => {
// console.log('progress number:', progress);
// const _file = that._simFileList[file.file.uid]
// _file.status = 'transcoding'
// _file.progress = parseInt((progress) * 100)
// },
);
// file.upload()
// that.computeMD5(file);
})
// 单个文件上传成功
uploader.on('fileSuccess', function(rootFile, file, message) {
console.log(rootFile, file, message)
const resultMsg = JSON.parse(message)
that._simFileList[file.file.uid].status = 'success'
if (that.savaVideo) {
that.savaVideo(Object.assign({materialName: file.name, vid: resultMsg.data.videoVid}, resultMsg.data), that._simFileList[file.file.uid])
}
})
uploader.on('fileProgress', function(rootFile, file, chunk) {
const _file = that._simFileList[file.file.uid]
_file.status = 'uploading'
_file.progress = parseInt((file.progress()) * 100)
})
// 根下的单个文件(文件夹)上传完成
uploader.on('fileComplete', function(rootFile) {
console.log(rootFile)
})
// 某个文件上传失败了
uploader.on('fileError', function(rootFile, file, message) {
console.log(rootFile, file, message)
})
},
// 分片上传
fenpianUpload(file, videoPlatType) {
this.videoPlatType = videoPlatType
this.vodModel = file.vodModel
this._simFileList[file.uid] = file;
this.simpleClient.addFile(file.file)
},
fenpianPause() {
this.simpleClient.pause()
},
fenpianResume() {
this.simpleClient.resume()
}
}
}
import {mapGetters} from 'vuex'
import {vodConfig} from '@/api/upload'
import PlvVideoUpload from '@polyv/vod-upload-js-sdk';
import UploadFragment from '@/utils/mixin/UploadFragment';
export default {
mixins: [UploadFragment],
data() {
return {
videoPlatType: 1, // 视频平台 1私有云、2保利威、3百家云、4获得场景
polyvClient: undefined, // 保利威SDK实例
tempCheckpoint: undefined, // 续传对象
directoryPath: '',
cancelToken: {},
uploading: false,
bucket: '',
uploadStatus: [],
uploadList: [],
isEncrypt: false,
progress: 0,
errorMessage: '',
huanTuoUploaderList: {}
}
},
filters: {
uploadStatus(val, list) {
if (val === 'success') {
return list[val]
} else if (val === 'fail') {
return list[val]
} else if (val === 'uploading') {
return list[val]
} else if (val === 'stop') {
return list[val]
} else if (val === 'save') {
return list[val]
} else if (val === 'saveSuccess') {
return list[val]
} else if (val === 'saveFail') {
return list[val]
} else {
return list['other']
}
}
},
computed: {
...mapGetters({
'websiteInfo': 'app/websiteInfo',
'userToken': 'userToken'
})
},
mounted() {
this.errorMessage = ''
},
methods: {
startUpload(_file, cb) {
console.log(_file)
if (this.uploading) {
return false
}
this.uploading = true
this.initOssConfig(_file, res => {
Object.assign(_file, res)
_file.vodModel = res.vodModel
if (this.videoPlatType === 2) {
// 上传保利威
this.getPolyvVideoSign(res.vodUploadConfig, () => {
this.polyvUpload(_file, cb)
})
} else {
// 其他的分片上传
this.initSimpleUpload(JSON.parse(res.vodUploadConfig))
this.fenpianUpload(_file, this.videoPlatType)
}
})
},
randomString(len = 32) {
const $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789';
const maxPos = $chars.length;
let pwd = '';
for (let i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
},
// 初始化上传配置
initOssConfig(file, cb) {
vodConfig({fileName: file.name, fileSize: file.size}).then(res => {
if (res) {
this.videoPlatType = res.vodPlatform // 1私有云、2保利威、3百家云、4获得场景
if (cb) {
console.log(res);
cb(res)
}
}
}).catch(msg => {
file.status = 'fail'
this.uploading = false
this.nextUpload && this.nextUpload();
this.$message({
type: 'error',
message: this.errorMessage || msg.message
})
})
},
// 初始化保利威上传sdk
initPolyv() {
this.polyvClient = new PlvVideoUpload({
events: {
Error: (err) => { // 错误事件回调
console.log(err);
this.errorMessage = err.message
},
UploadComplete: () => {
} // 全部上传任务完成回调
}
});
},
// 暂停上传
stopUpload(_file) {
this.uploading = false
if (this.videoPlatType === 2) {
_file.status = 'stop'
this.polyvClient.stopAll()
} else if (this.videoPlatType === 3) {
if (this.cancelToken && this.cancelToken.cancel) {
this.cancelToken.cancel()
}
} else {
_file.status = 'stop'
this.fenpianPause()
}
},
// 继续上传
resumeUpload(_file) {
_file.status = 'uploading'
if (this.videoPlatType === 2) {
this.polyvClient.resumeFile(_file.uploadId)
} else if (this.videoPlatType === 3) {
this.baijiayunUpload(_file)
} else {
this.fenpianResume()
}
},
// 获取保利威上传sign
getPolyvVideoSign(res, cb) {
if (!this.polyvClient) this.initPolyv()
this.polyvClient.updateUserData({
userid: res.userid, // Polyv云点播账号的ID
ptime: res.ptime, // 时间戳,注意:系统时间不正确会导致校验失败
sign: res.sign, // 是根据将secretkey和ts按照顺序拼凑起来的字符串进行MD5计算得到的值
hash: res.hash // 是根据将ts和writeToken按照顺序拼凑起来的字符串进行MD5计算得到的值
});
if (cb) {
cb()
}
},
// polyv上传
polyvUpload(_file) {
console.log('polyvUpload', _file)
const that = this
const fileSetting = {
title: undefined, // 标题
desc: undefined, // 描述
cataid: _file.vodUploadConfig.cataid, // 上传分类目录ID
tag: 'course', // 标签
luping: 1, // 是否录屏优化。当值为1时,上传的视频不再采取默认的压缩编码机制,视频尺寸不再压缩,保证视频的清晰度。默认值为0
keepsource: 0, // 是否源文件播放(不对视频进行编码):0为编码,1为不编码
state: undefined // 用户自定义数据,如果提交了该字段,会在视频上传完成事件回调时透传返回。
}
that.polyvClient.clearAll()
const uploader = that.polyvClient.addFile(
_file.file, // file 为待上传的文件对象
{
FileStarted: function(uploadInfo) { // 文件开始上传回调
console.log('文件上传开始: ' + uploadInfo.fileData.title);
},
FileProgress: function(uploadInfo) { // 文件上传过程返回上传进度信息回调
console.log('FileProgress', uploadInfo)
_file.status = 'uploading'
_file.progress = parseInt(uploadInfo.progress * 100)
console.log('文件上传中: ' + (uploadInfo.progress * 100).toFixed(2) + '%');
},
FileStopped: function(uploadInfo) { // 文件暂停上传回调
_file.status = 'stop'
that.uploading = false
console.log('文件上传停止: ' + uploadInfo.fileData.title);
},
FileSucceed: function(uploadInfo) { // 文件上传成功回调
that.uploading = false
_file.status = 'success'
_file.resourceUrl = uploadInfo.vid
_file.progress = 100
console.log('文件上传成功: ', uploadInfo.fileData);
if (that.savaVideo) {
that.savaVideo(Object.assign({materialName: _file.name, vid: uploadInfo.vid}, uploadInfo.fileData), _file)
}
},
FileFailed: function(uploadInfo) { // 文件上传失败回调
_file.status = 'fail'
if (that.nextUpload) that.nextUpload()
console.log('文件上传失败: ' + uploadInfo.fileData.title);
}
},
fileSetting
);
const uploaderid = uploader.id;
_file.uploadId = uploaderid
this.polyvClient.resumeFile(uploaderid);
},
baijiayunUpload(_file, cb) {
const _that = this
// console.log(_stat)
_that.cancelToken = {}
// console.log(res)
uploadBaijiayun(_file.file, _file.config.uploadUrl, (p) => {
// console.log(p)
_file.progress = parseInt(p)
_file.status = 'uploading'
if (cb) cb(parseInt(p))
}, _that.cancelToken).then(result => {
_that.uploading = false
if (result.msg === 'success') {
_file.status = 'success'
_file.file = null;
_that.savaVideo(Object.assign({
vid: result.fid,
materialName: _file.name
}, _file), _file)
} else {
this.$message.error('上传失败')
_file.status = 'fail'
if (_that.nextUpload) _that.nextUpload()
}
}).catch((msgs) => {
_that.uploading = false
_file.status = 'fail'
if (_that.nextUpload) _that.nextUpload()
console.log('error', msgs)
})
}
}
}
import axios from 'axios'
import {ElMessage, ElMessageBox} from 'element-plus'
import store from '@/store'
import {getToken} from '@/utils/auth'
import router from '@/router';
const BaseURL = '/gateway'
const pending = []; // 声明一个数组用于存储每个ajax请求的取消函数和ajax标识
const removePending = (config, isCancel) => {
for (const p in pending) {
if (pending[p].u === config.url + '&' + config.method) { // 当当前请求在数组中存在时执行函数体
if (isCancel) {
pending[p].f(); // 执行取消操作
}
pending.splice(p, 1); // 把这条记录从数组中移除
}
}
}
// create an axios instance
const service = axios.create({
baseURL: BaseURL, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 1800 * 10000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// removePending(config, true); // 在一个ajax发送前执行一下取消操作
if (store.getters.token) {
config.headers['token'] = getToken()
}
return config
},
error => {
console.log(error)
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
// removePending({
// url: response.config.url.replace(BaseURL, ''),
// method: response.config.method
// }, false); // 在一个ajax响应后再执行一下取消操作,把已经完成的请求从pending中移除
const res = response.data
// 关闭loading动画
store.dispatch('app/toggleLoading', false)
if (response.config.url.indexOf('baijiayun.com') > -1) {
return res
}
const token = getToken()
// if the custom code is not 200, it is judged as an error.
if (res.code !== 200) {
console.log(res.code);
// 300:非法的token; 500:其他客户端登录了; 50014:Token 过期了;
if (res.code <= 500 && res.code >= 300 && token) {
if (res.code === 306) {
console.log(11);
router.push({path: '/403'})
ElMessage.warning('权限不足,请联系管理员')
} else {
ElMessageBox.confirm(
'你已被登出,请重新登录',
'确定登出',
{
confirmButtonText: '重新登录',
showCancelButton: false,
type: 'warning'
}
).then(() => {
store.dispatch('user/logout').then(() => {
location.href = '/admin/login'
})
})
}
} else if (res.code === 600) {
ElMessageBox.confirm(
res.msg,
'登录异常',
{
confirmButtonText: '确定',
showCancelButton: false,
type: 'warning'
}
).then(() => {
store.dispatch('user/logout').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
} else {
ElMessage({
message: res.msg,
type: 'error',
duration: 5 * 1000
})
}
Promise.reject(response.data)
return response.data && response.data.data
} else {
return response.data && response.data.data
}
},
error => {
console.log('err' + error) // for debug
// if(error.message) {
ElMessage({
message: error.message,
type: 'error',
duration: 5 * 1000
})
// }
return Promise.reject(error)
}
)
export default service
......@@ -61,10 +61,10 @@ export function getPolyvVideoSign() {
vodConfig().then(res => {
//console.log(res)
polyvClient.updateUserData({
userid: res.polyvConfig.userid,
ptime: res.polyvConfig.ptime,
sign: res.polyvConfig.sign,
hash: res.polyvConfig.hash
userid: res.vodUploadConfig.userid,
ptime: res.vodUploadConfig.ptime,
sign: res.vodUploadConfig.sign,
hash: res.vodUploadConfig.hash
});
// 更新用户数据(由于sign等用户信息有效期为3分钟,需要每隔3分钟更新一次)
setTimeout(() => {
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="600" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="600" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item label="图片" prop="carouselHead">
......
......@@ -2,16 +2,9 @@
<div class="app-container">
<div class="page_head">
<div class="search_bar clearfix">
<el-form :model="seekForm" inline label-width="80px">
<el-form-item label="手机号码">
<el-input v-model="seekForm.carouselTitle" clearable/>
</el-form-item>
<el-form-item>
<el-button @click="seek()" type="primary"> 查询</el-button>
<el-button @click="resetSeek()">重置</el-button>
<el-button v-if="checkPermission('system:admin:website:carousel:save')" plain type="success" @click="openEditDialog(initData)">添加</el-button>
</el-form-item>
</el-form>
<el-button v-if="checkPermission('system:admin:website:carousel:save')" plain type="success" @click="openEditDialog(initData)">添加</el-button>
<br>
<br>
</div>
</div>
<el-table v-loading="tableData.loading" :data="tableData.list" border>
......@@ -37,7 +30,7 @@
<span :class="{ 'c-danger': scope.row.statusId === 0 }">{{ statusIdEnums[scope.row.statusId] }}</span>
</template>
</el-table-column>
<el-table-column :width="300" fixed="right" label="操作" prop="address">
<el-table-column :width="200" fixed="right" label="操作" prop="address">
<template #default="scope">
<el-button v-if="checkPermission('system:admin:website:carousel:edit')" plain type="primary" @click="openEditDialog(scope.row)">编辑</el-button>
<el-dropdown>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="600" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="600" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item label="友情名称" prop="linkName">
<el-input v-model="formModel.data.linkName" maxlength="255" show-word-limit></el-input>
......
......@@ -29,7 +29,7 @@
<span :class="{ 'c-danger': scope.row.statusId === 0 }">{{ statusIdEnums[scope.row.statusId] }}</span>
</template>
</el-table-column>
<el-table-column :width="300" fixed="right" label="操作" prop="address">
<el-table-column :width="200" fixed="right" label="操作" prop="address">
<template #default="scope">
<el-button v-if="checkPermission('system:admin:website:link:edit')" plain type="primary" @click="openEditDialog(scope.row)">编辑</el-button>
<el-dropdown>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="600" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="600" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item label="导航名称" prop="navTitle">
<el-input v-model="formModel.data.navTitle" maxlength="255" show-word-limit></el-input>
......
......@@ -3,7 +3,7 @@
<div class="page_head">
<div class="search_bar clearfix">
<el-form :model="seekForm" inline label-width="80px">
<el-form-item label="手机号码">
<el-form-item label="导航名称">
<el-input v-model="seekForm.navTitle" clearable/>
</el-form-item>
<el-form-item>
......@@ -29,7 +29,7 @@
<span :class="{ 'c-danger': scope.row.statusId === 0 }">{{ statusIdEnums[scope.row.statusId] }}</span>
</template>
</el-table-column>
<el-table-column :width="300" fixed="right" label="操作" prop="address">
<el-table-column :width="200" fixed="right" label="操作" prop="address">
<template #default="scope">
<el-button v-if="checkPermission('system:admin:website:nav:edit')" plain type="primary" @click="openEditDialog(scope.row)">编辑</el-button>
<el-dropdown>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item class="form-group" label="备注" prop="remark">
<el-input v-model="formModel.data.remark" maxlength="100" show-word-limit></el-input>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item class="form-group" label="课程" prop="courseName">
<el-input v-model="formModel.data.courseName" disabled style="width: 210px; margin-right: 20px"></el-input>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item class="form-group" label="名称" prop="zoneName">
<el-input v-model="formModel.data.zoneName" maxlength="100" show-word-limit></el-input>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="'添加'" :width="500" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="'添加'" :width="500" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item v-if="formModel.data.parentId" class="form-group" label="上级分类" prop="categoryName">
<el-input v-model="formModel.data.parentCategoryName" maxlength="100" disabled></el-input>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '分类修改' : '分类添加'" :width="500" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '分类修改' : '分类添加'" :width="500" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item class="form-group" label="名称" prop="categoryName">
<el-input v-model="formModel.data.categoryName" maxlength="100" show-word-limit></el-input>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.chapterId ? '节修改' : '节添加'" :width="500" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.chapterId ? '节修改' : '节添加'" :width="500" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item class="form-group" label="节名称" prop="periodName">
<el-input v-model="formModel.data.periodName" maxlength="100" show-word-limit></el-input>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '章修改' : '章添加'" :width="500" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '章修改' : '章添加'" :width="500" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item class="form-group" label="章名称" prop="chapterName">
<el-input v-model="formModel.data.chapterName" maxlength="100" show-word-limit></el-input>
......
......@@ -18,7 +18,10 @@
<template #default="scope">
<span>{{ scope.row.chapterName }}</span>
<span>{{ scope.row.periodName }}</span>
<span v-if="scope.row.resourceViewResp">{{ resourceTypeEnums[scope.row.resourceViewResp.resourceType] }}{{ scope.row.resourceViewResp.resourceName }} | {{ formatDuring(scope.row.resourceViewResp.videoLength*1000) }}</span>
<span v-if="scope.row.resourceViewResp">{{ resourceTypeEnums[scope.row.resourceViewResp.resourceType] }}{{ scope.row.resourceViewResp.resourceName }} |
<span v-if="scope.row.resourceViewResp.resourceType<3">{{ formatDuring(scope.row.resourceViewResp.videoLength * 1000) }}</span>
<span v-else>{{ scope.row.resourceViewResp.docPage }}</span>
</span>
</template>
</el-table-column>
<!-- <el-table-column label="章节描述" prop="chapterDesc"/>-->
......@@ -62,7 +65,7 @@ import {defineComponent, onMounted, reactive, toRefs} from 'vue';
import {useStore} from 'vuex';
import {useRoute} from 'vue-router';
import {courseChapterDelete, courseChapterEdit, courseChapterPage} from '@/api/course.js'
import { formatDuring} from '@/utils/utils.js'
import {formatDuring} from '@/utils/utils.js'
import Edit from './edit.vue';
import Add from './add.vue';
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="900" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="900" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-row>
<el-col :span="12">
......
......@@ -51,8 +51,9 @@
<span :class="{ 'c-danger': scope.row.statusId === 0 }">{{ statusIdEnums[scope.row.statusId] }}</span>
</template>
</el-table-column>
<el-table-column :width="200" fixed="right" label="操作" prop="address">
<el-table-column :width="280" fixed="right" label="操作" prop="address">
<template #default="scope">
<el-button plain type="success" @click="courseRecord(scope.row)">数据</el-button>
<el-button v-if="checkPermission('course:admin:course:edit')" plain type="success" @click="courseChapter(scope.row)">章节</el-button>
<el-dropdown>
<el-button> 更多操作<i class="el-icon-arrow-down"/></el-button>
......@@ -132,16 +133,21 @@ export default defineComponent({
});
};
//章节跳转
//章节
const courseChapter = function(row) {
this.$router.push({path: '/course/chapter', query: {courseId: row.id}});
}
//数据
const courseRecord = function(row) {
this.$router.push({path: '/course/record', query: {courseId: row.id}});
}
return {
...toRefs(state),
initData,
...toRefs(state), initData,
handleUpdateStatus,
courseChapter
courseChapter,
courseRecord
};
}
});
......
<template>
<div class="app-container">
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="课程记录" name="course">
<div class="page_head">
<div class="search_bar clearfix">
<el-form :model="seekForm" inline label-width="80px">
<el-form-item>
<el-button @click="seek()" type="primary"> 查询</el-button>
<el-button @click="resetSeek()">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
<el-table v-loading="tableData.loading" :data="tableData.list" border>
<el-table-column align="center" label="序号" type="index" width="60"/>
<el-table-column label="手机号码" prop="mobile" min-width="20"/>
<el-table-column label="用户昵称" prop="nickname" min-width="20"/>
<el-table-column label="学习进度" prop="courseProgress">
<template #default="scope">
<el-progress
:percentage="scope.row.courseProgress"
:stroke-width="25"
:text-inside="true"
/>
</template>
</el-table-column>
<el-table-column label="开始学习时间" prop="gmtCreate" min-width="30"/>
<el-table-column width="100" label="操作">
<template #default="scope">
<el-button plain type="primary" @click="studyRecord(scope.row)">明细</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination :current-page="page.pageCurrent" :layout="page.layout" :page-size="page.pageSize" :page-sizes="[20, 50, 100, 200]" :total="page.totalCount" background @size-change="handleSizeChange" @current-change="handleCurrentChange"/>
<study v-model="study.visible" :info="study.info" @close="studyCallback"/>
</el-tab-pane>
<el-tab-pane label="课程评论" name="comment">
</el-tab-pane>
<el-tab-pane label="课程收藏" name="collect">
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import UseTable from '@/composables/UseTable.js';
import {ElMessage} from 'element-plus';
import {defineComponent, onMounted, reactive, toRefs} from 'vue';
import {useStore} from 'vuex';
import {userCourseRecord, userStudyePage} from '@/api/course.js'
import {useRoute} from 'vue-router/dist/vue-router';
import Study from './study.vue';
export default defineComponent({
components: {Study},
setup() {
const route = useRoute()
const apis = reactive({
getList: userCourseRecord
})
const state = reactive({
...UseTable(apis, {courseId: route.query.courseId}),
activeName: 'course'
});
const store = useStore();
let study = reactive({
visible: false,
info: {}
})
const studyRecord = (row) => {
userStudyePage({userId: row.userId, courseId: route.query.courseId}).then((res) => {
study.info = res.list
study.visible = true
});
}
const studyCallback = () => {
study.visible = false
}
const handleClick = (target, action) => {
console.log(target.props.name)
}
return {
...toRefs(state),
study,
studyRecord,
studyCallback,
handleClick
};
}
});
</script>
<template>
<el-dialog :model-value="visible" :append-to-body="true" :title="title" width="800px" @close="cloneDialog">
<el-table :data="info" row-key="id" :tree-props="{ children: 'userStudyPeriodPageRespList' }" default-expand-all>
<el-table-column label="章节名称" prop="chapterName">
<template #default="scope">
<span>{{ scope.row.chapterName }}</span>
<span>{{ scope.row.periodName }}</span>
</template>
</el-table-column>
<el-table-column label="学习时间" prop="gmtCreate" width="200">
<template #default="scope">
<span v-if="scope.row.progress >0">{{ scope.row.gmtCreate }}</span>
</template>
</el-table-column>
<el-table-column label="学习进度" prop="courseProgress" width="200">
<template #default="scope">
<el-progress v-if="scope.row.progress" :percentage="scope.row.progress" :stroke-width="25" :text-inside="true"/>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script>
import {defineComponent, reactive, ref, toRefs, watch} from 'vue';
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: () => {
return false;
}
},
title: {
type: String,
default: '学习记录'
},
info: {
type: Object,
default: () => {
return {}
}
}
},
emits: ['update:modelValue'],
setup(props, {emit}) {
const state = reactive({});
const visible = ref(false);
let {modelValue, info} = toRefs(props);
// 弹窗是否要打开监控
watch(modelValue, async(val) => {
visible.value = val;
});
console.log('info', info)
const cloneDialog = () => {
visible.value = false;
emit('update:modelValue', false);
};
return {
...toRefs(state),
visible,
cloneDialog
};
}
});
</script>
<template>
<el-dialog v-model="modelValue" top="5vh" custom-class="preview_dialog" c :title="t('single.LearnTaskTimPreview')" :before-close="cloneDialog" :width="700" :append-to-body="true">
<div class="video_body_content">
<div class="video_content clearfix">
<div class="win_box">
<div v-if="ctrl.isVideo" id="player" ref="videoBox" class="video_win" />
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cloneDialog">{{ t('close') }}</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
import {useI18n} from 'vue-i18n';
import {computed, onMounted, onUnmounted, reactive, ref, toRefs, watch} from 'vue';
import {useStore} from 'vuex';
import {resourceLibraryPreview} from '@/api/course';
// import {jsonp} from 'vue-jsonp'
export default {
name: 'Preview',
props: {
modelValue: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'image'
},
videoInfo: {
type: Object,
default: () => {}
}
},
setup(props, {emit}) {
const {t} = useI18n()
const { type, videoInfo, modelValue} = toRefs(props)
const store = useStore()
const videoBox = ref(null)
const opts = reactive({
imageList: '',
video: {}
})
const map = reactive({
player: null,
userInfo: computed(() => {
return store.state.user.userInfo
})
})
const ctrl = reactive({
isVideo: false,
showImageViewer: true
})
const cloneDialog = () => {
emit('update:modelValue', false)
}
const getView = () => {
resourceLibraryPreview({id: videoInfo.value['id']}).then(res => {
opts.video = res
if (res.vodPlatform === 1 || res.vodPlatform === 4 || res.vodPlatform === 10) {
play(res)
} else if (res.vodPlatform === 2) {
baiJiaYunPlay(res)
}
})
}
const baiJiaYunPlay = (data) => {
console.log('baijiayun=>', data)
const _that = map
/* eslint-disable */
$('#player').html('<video class="video video-js vjs-default-skin"></video>')
const box = videoBox['value']
if (data.watchProgress >= 100 || Math.abs(data.coursewareLengthInt - data.biggestWatchLength) < 3) {
if (Math.abs(data.biggestWatchLength - data.lastWatchLength) < 3) {
data.lastWatchLength = 0
}
}
_that.player = new BjcPlayer(document.querySelector('.video'), {
width: box.offsetWidth,
height: box.offsetHeight,
token: data.videoConfig,
autoplay: false,
currentTime: data.lastWatchDuration,
vid: data.vid,
playbackRates: [],
showCurrentTimeDisplay: true,
showDurationDisplay: true,
disableSeek: 0, // 是否禁止拖拽, 0:可以, 1:禁止
user_name: _that.userInfo.userName, // 用户名, 主要用于数据统计
user_number: _that.userInfo.userNo, // 用户id, 主要用于数据统计
onended: function () {
console.log('onplayend event')
// 视频播放结束清空保存记录定时器任务
},
onplay: function () {
console.log('onplaybegin event')
},
onpause: function () {
console.log('onplaypause event')
},
ontimeupdate: function (data) {
console.log('ontimeupdate event')
},
onseeked: function (data) {
console.log('onseek event')
console.log('seekTime:' + data.currentTime)
},
onerror: function (data) {
console.log('onerror event')
console.log(data.msg)
}
})
console.log('plater', _that.player)
}
const play = (data, startTime = 0) => {
console.log('保利威播放', data)
data.videoConfig = JSON.parse(data.videoConfig)
window.s2j_onVideoPlay = () => {
}
window.s2j_onFullScreen = () => {
}
window.s2j_onNormalScreen = () => {
}
window.s2j_onPlayOver = () => {
console.log('保利威播放结束!')
// 视频播放结束清空保存记录定时器任务
}
window.s2j_onVideoPause = () => {
}
const box = videoBox['value']
if (!box) {
play(data, startTime)
return
}
window.s2j_onPlayerInitOver = () => {
// setTimeout(() => {
// map.player.j2s_seekVideo(data.lastWatchDuration)
// map.player.j2s_resumeVideo(data.lastWatchDuration)
// }, 900)
}
if (map.player) {
const param = {
watchStartTime: startTime || 0,
width: box.offsetWidth,
height: box.offsetHeight,
autoplay: false
}
if (data.vodPlatform === 4) {
param.url = data.videoUrl
} else if (data.vodPlatform === 10) {
if (data.videoConfig.hdUrl) {
param.url = data.videoConfig.hdUrl
} else {
param.url = data.videoConfig.sdUrl
}
} else {
param.ts = data.videoConfig.ts;
param.sign = data.videoConfig.sign;
param.vid = data.videoConfig.vid;
param.playsafe = data.videoConfig.token;
param.code = data.videoConfig.code;
}
map.player.changeVid(param)
} else {
const param = {
width: box.offsetWidth,
height: box.offsetHeight,
history_video_duration: 2,
forceH5: true,
hideSwitchPlayer: true,
autoplay: false,
speed: false,
loading_bg_img: videoInfo.value['coverUrl'],
watchStartTime: startTime,
viewerInfo: {
viewerId: map.userInfo.userNo,
viewerName: map.userInfo.userName
}
}
if (data.vodPlatform === 4) {
param.url = data.videoUrl
} else if (data.vodPlatform === 10) {
if (data.videoConfig.hdUrl) {
param.url = data.videoConfig.hdUrl
} else {
param.url = data.videoConfig.sdUrl
}
} else {
param.ts = data.videoConfig.ts;
param.sign = data.videoConfig.sign;
param.vid = data.videoConfig.vid;
param.playsafe = data.videoConfig.token;
param.code = data.videoConfig.code
}
map.player = window.polyvObject('#player').videoPlayer(param)
}
}
// const {ctx} = getCurrentInstance()
watch(() => modelValue.value, (val) => {
if (val) {
// jsonp('https://gateway.doityun.com/ip/info').then(res=>{
// opts.ipInfo = res
// })
ctrl.isVideo = type.value === 'video'
getView()
}else{
if(map.player) {
if ([1, 4, 10].includes(opts.video['vodPlatform'])) {
map.player.destroy()
map.player = null
} else if (opts.video['vodPlatform'] === 2) {
map.player.dispose()
map.player = null
$('#player').html('')
}
}
}
})
onMounted(()=>{
if(modelValue.value){
ctrl.isVideo = type.value === 'video'
getView()
}
})
onUnmounted(()=>{
if(map.player)
if (opts.video['vodPlatform'] === 2) {
map.player.destroy()
map.player = null
} else if (opts.video['vodPlatform'] === 3) {
map.player.dispose()
map.player = null
$('#player').html('')
}
})
return {
opts,
ctrl,
modelValue,
videoInfo,
videoBox,
t,
cloneDialog
}
}
}
</script>
<style scoped>
.video_body_content{
position: relative;
width: 100%;
min-height: 400px;
overflow: hidden;
}
.win_box{
display: flex;
align-items: center;
justify-content: center;
}
.video_content,.win_box,.video_win{
width: 100%;
height: 400px;
}
.video_win_img{
height: 400px;
}
.dialog-footer{
text-align: center;
}
</style>
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item class="form-group" label="资源名称" prop="resourceName">
<el-input v-model="formModel.data.resourceName" maxlength="100" show-word-limit></el-input>
......
......@@ -3,14 +3,15 @@
<div class="page_head">
<div class="search_bar clearfix">
<el-form :model="seekForm" inline label-width="80px">
<el-form-item label="手机号码">
<el-input v-model="seekForm.mobile" clearable/>
<el-form-item label="资源名称">
<el-input v-model="seekForm.resourceName" clearable/>
</el-form-item>
<el-form-item>
<el-button @click="seek()" type="primary"> 查询</el-button>
<el-button @click="resetSeek()">重置</el-button>
<el-button v-if="checkPermission('course:admin:resource:save')" plain type="success" @click="localUpload">上传</el-button>
<input id="file" type="file" style="display: none" @change="addUpload">
<!-- <el-button plain type="success" @click="localUpload">上传</el-button>
<input id="file" type="file" style="display: none" @change="addUpload"> -->
<uploader-btn icon="" :plain="false" btn-text="上传" mode="async" class="mgl10"/>
</el-form-item>
</el-form>
</div>
......@@ -28,12 +29,20 @@
<el-table-column label="资源类型" prop="resourceType" :width="200">
<template #default="scope">
<span>{{ resourceTypeEnums[scope.row.resourceType] }}</span><br>
<span>{{ formatDuring(scope.row.videoLength * 1000) }}</span>
<span v-if="scope.row.resourceType<3">{{ formatDuring(scope.row.videoLength * 1000) }}</span>
<span v-else>{{ scope.row.docPage }}</span>
</template>
</el-table-column>
<el-table-column label="视频状态" prop="resourceType" :width="100">
<el-table-column label="状态" prop="videoStatus" :width="100">
<template #default="scope">
<span>{{ videoStatusEnums[scope.row.videoStatus] }}</span>
<span v-if="scope.row.resourceType<3">{{ videoStatusEnums[scope.row.videoStatus] }}</span>
<span v-else>成功</span>
</template>
</el-table-column>
<el-table-column label="平台" prop="vodPlatform" :width="100">
<template #default="scope">
<span v-if="scope.row.resourceType<3">{{ vodPlatformEnums[scope.row.vodPlatform] }}</span>
<span v-else>{{ storagePlatformEnums[scope.row.storagePlatform] }}</span>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" :width="100"/>
......@@ -44,17 +53,29 @@
</el-table-column>
<el-table-column :width="200" fixed="right" label="操作" prop="address">
<template #default="scope">
<el-button v-if="checkPermission('course:admin:resource:edit')" plain type="primary" @click="openEditDialog(scope.row)">编辑</el-button>
<el-button v-if="checkPermission('course:admin:resource:edit')" plain type="primary"
@click="openEditDialog(scope.row)"
>编辑
</el-button>
<el-dropdown>
<el-button> 更多操作<i class="el-icon-arrow-down"/></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button v-if="checkPermission('course:admin:resource:edit') && scope.row.statusId == 0" plain type="success" @click="handleUpdateStatus(scope.row)">启用</el-button>
<el-button v-if="checkPermission('course:admin:resource:edit') && scope.row.statusId == 1" plain type="danger" @click="handleUpdateStatus(scope.row)">禁用</el-button>
<el-button v-if="checkPermission('course:admin:resource:edit') && scope.row.statusId == 0" plain
type="success" @click="handleUpdateStatus(scope.row)"
>启用
</el-button>
<el-button v-if="checkPermission('course:admin:resource:edit') && scope.row.statusId == 1" plain
type="danger" @click="handleUpdateStatus(scope.row)"
>禁用
</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button v-if="checkPermission('course:admin:resource:delete')" plain type="danger" @click="tableDelete(scope.row)">删除</el-button>
<el-button v-if="checkPermission('course:admin:resource:delete')" plain type="danger"
@click="tableDelete(scope.row)"
>删除
</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
......@@ -62,23 +83,28 @@
</template>
</el-table-column>
</el-table>
<el-pagination :current-page="page.pageCurrent" :layout="page.layout" :page-size="page.pageSize" :page-sizes="[20, 50, 100, 200]" :total="page.totalCount" background @size-change="handleSizeChange" @current-change="handleCurrentChange"/>
<el-pagination :current-page="page.pageCurrent" :layout="page.layout" :page-size="page.pageSize"
:page-sizes="[20, 50, 100, 200]" :total="page.totalCount" background @size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<edit v-model="editModel.visible" :form="editModel.form" @updateTable="closeEditDialog"/>
</div>
</template>
<script>
import UseTable from '@/composables/UseTable.js';
import {ElMessage} from 'element-plus';
import {defineComponent, onMounted, reactive, toRefs} from 'vue';
import {defineComponent, onMounted, onUnmounted, reactive, toRefs} from 'vue';
import {useStore} from 'vuex';
import {resourceDelete, resourceEdit, resourcePage} from '@/api/course.js'
import {polyvUpload} from '@/utils/vod.js'
import {getSize, formatDuring} from '@/utils/utils.js'
import Edit from './edit.vue';
import UploaderBtn from '@/components/Upload/UploaderBtn.vue';
import bus from '@/utils/bus';
export default defineComponent({
components: {
Edit
Edit,
UploaderBtn
},
setup() {
const apis = reactive({
......@@ -90,20 +116,12 @@ export default defineComponent({
...UseTable(apis, {}),
statusIdEnums: {},
resourceTypeEnums: {},
vodPlatformEnums: {},
storagePlatformEnums: {},
videoStatusEnums: {}
});
const store = useStore();
onMounted(() => {
store.dispatch('GetOpts', {enumName: 'StatusIdEnum', type: 'obj'}).then((res) => {
state.statusIdEnums = res;
});
store.dispatch('GetOpts', {enumName: 'ResourceTypeEnum', type: 'obj'}).then((res) => {
state.resourceTypeEnums = res;
});
store.dispatch('GetOpts', {enumName: 'VideoStatusEnum', type: 'obj'}).then((res) => {
state.videoStatusEnums = res;
});
});
const handleUpdateStatus = function(row) {
state.tableData.loading = true;
row.statusId = row.statusId ? 0 : 1
......@@ -122,17 +140,31 @@ export default defineComponent({
const myfile = document.getElementById('file');
myfile.click();
}
const addUpload = (e) => {
const file = e.target.files[0];
polyvUpload(file);
e.target.value = '';
state.seek();
}
onMounted(() => {
store.dispatch('GetOpts', {enumName: 'StatusIdEnum', type: 'obj'}).then((res) => {
state.statusIdEnums = res;
});
store.dispatch('GetOpts', {enumName: 'ResourceTypeEnum', type: 'obj'}).then((res) => {
state.resourceTypeEnums = res;
});
store.dispatch('GetOpts', {enumName: 'VodPlatformEnum', type: 'obj'}).then((res) => {
state.vodPlatformEnums = res;
});
store.dispatch('GetOpts', {enumName: 'StoragePlatformEnum', type: 'obj'}).then((res) => {
state.storagePlatformEnums = res;
});
store.dispatch('GetOpts', {enumName: 'VideoStatusEnum', type: 'obj'}).then((res) => {
state.videoStatusEnums = res;
});
bus.on('uploadFileSuccess', state.seek)
});
onUnmounted(() => {
bus.off('uploadFileSuccess');
});
return {
...toRefs(state),
handleUpdateStatus,
localUpload,
addUpload,
getSize,
formatDuring
};
......
<template>
<div class="dashboard-container">
<div class="info">
<p>特别说明:</p>
<p>1.演示环境仅提供查看功能,本地部署能体验更多功能</p>
<p>2.可提供有偿指导服务:请联系作者:18302045627(微信同号)</p>
<p>3.点播平台:私有化选项,属于付费功能,请联系作者:18302045627(微信同号)</p>
</div>
<div v-if="checkPermission('user:admin:stat:login')" class="title-info">
<span class="title">最近14天登录人数</span>
<span style="margin-left: 300px;color: red;">演示环境仅提供查看功能,本地部署能体验更多功能</span>
</div>
<login v-if="checkPermission('user:admin:stat:login')" :data="loginData"/>
<login v-if="checkPermission('user:admin:stat:login')" :data="statData"/>
<div v-if="checkPermission('system:admin:stat:vod')" class="title-info">
<span class="title">视频云使用情况</span>
</div>
......@@ -22,7 +27,7 @@ export default {
data() {
return {
vodData: {},
loginData: {}
statData: {}
}
},
mounted() {
......@@ -38,17 +43,22 @@ export default {
},
getLogin() {
statLogin().then(res => {
this.loginData = res
this.statData = res
})
}
}
};
</script>
<script setup>
</script>
<style lang="scss" scoped>
.info {
margin: 20px 25px;
padding: 5px;
background-color: #f5f7fa;
border-radius: 4px;
font-size: 16px;
}
.title-info {
margin: 20px 25px;
padding: 5px;
......@@ -56,9 +66,5 @@ export default {
border-left: 5px solid #50bfff;
border-radius: 4px;
font-size: 16px;
span {
}
}
</style>
......@@ -16,9 +16,8 @@ export default {
}
},
data() {
return {
myChart: undefined
}
this.myChart = {}
return {}
},
watch: {
data() {
......@@ -30,51 +29,41 @@ export default {
this.parseOption()
},
methods: {
parseOption: function() {
parseOption() {
const option = {
title: {
//text: '登录人数'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'line' // 默认为直线,可选为:'line' | 'shadow'
}
},
legend: {
data: ['登录人数']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
yAxis: {
axisLabel: {
interval: 0 // 设置成 0 强制显示所有标签。
},
splitLine: {
lineStyle: {
type: 'dashed'
}
},
type: 'value'
data: ['登录人数', '注册人数']
},
xAxis: {
axisLabel: {
interval: 0 // 设置成 0 强制显示所有标签。
},
type: 'category',
axisTick: {show: false},
data: this.data.dateList
},
yAxis: {
type: 'value'
},
series: [
{
name: '登录人数',
type: 'line',
stack: 'line',
data: this.data.loginList
data: this.data.loginList,
label: {
show: false,
position: 'center'
}
},
{
name: '注册人数',
type: 'line',
data: this.data.registerList
}
]
};
this.myChart.setOption(option)
......
......@@ -24,28 +24,27 @@ export default {
}
},
data() {
return {
pieOne: {},
pieTwo: {}
};
this.pieOne = {}
this.pieTwo = {}
return {}
},
mounted() {
this.pieOne = echarts.init(document.getElementById('cachePieOne'), 'light');
this.pieTwo = echarts.init(document.getElementById('cachePieTwo'), 'light');
this.vodOptions();
},
methods: {
vodOptions: function() {
// 总流量
const totalFlow = this.data.totalFlow;
// 已用流量
const usedFlow = this.data.usedFlow.toFixed(2);
const usedFlow = this.data.usedFlow ? this.data.usedFlow.toFixed(2) : 0;
// 剩余流量
const surplusFlow = (totalFlow - usedFlow).toFixed(2);
// 总空间
const totalSpace = this.data.totalSpace;
// 已用空间
const usedSpace = this.data.usedSpace.toFixed(2);
const usedSpace = this.data.usedSpace ? this.data.usedSpace.toFixed(2) : 0;
// 剩余流量
const surplusSpace = (totalSpace - usedSpace).toFixed(2);
const option1 = {
......
<template>
<el-dialog v-model="visible" :before-close="handleClose" :title="title" width="800px">
<el-dialog :model-value="visible" :before-close="handleClose" :title="title" width="800px">
<el-form ref="form" :model="newFormData" :rules="rules">
<el-form-item prop="configValue">
<!-- 文本类型 -->
......
<template>
<div class="app-container">
<el-form class="filter-container" inline label-width="100px">
<el-form-item>
<el-button v-if="checkPermission('system:admin:sys:config:video:init')" plain type="primary" @click="videoInitHandle">视频设置</el-button>
</el-form-item>
</el-form>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="站点设置" name="1">
<List :list="list" @reset="handleReset"></List>
</el-tab-pane>
<el-tab-pane label="系统设置" name="2">
<List :list="list" @reset="handleReset"></List>
</el-tab-pane>
<el-tab-pane label="视频设置" name="3">
<List :list="list" @reset="handleReset"></List>
</el-tab-pane>
......@@ -23,7 +25,7 @@
</div>
</template>
<script>
import {sysConfigList} from '@/api/system';
import {sysConfigList, videoInit} from '@/api/system';
import List from './list.vue';
export default {
......@@ -55,6 +57,16 @@ export default {
handleReset() {
this.map.configType = this.activeName;
this.listForList();
},
videoInitHandle() {
this.$confirm('视频云初始化', '视频云设置', {
confirmButtonText: '确认',
cancelButtonText: '取消'
}).then(() => {
videoInit().then((res) => {
this.$message.success(res);
});
});
}
}
};
......
......@@ -10,9 +10,7 @@
<el-table-column label="参数">
<template #default="scope">
<span v-if="(scope.row.contentType === 1 || scope.row.contentType === 2) && scope.row.configShow">{{ scope.row.configValue }}</span>
<el-button v-if="(scope.row.contentType === 1 || scope.row.contentType === 2) && !scope.row.configShow" type="text" @click="openRow(scope.row)">
【点击查看详情】
</el-button>
<el-button v-if="(scope.row.contentType === 1 || scope.row.contentType === 2) && !scope.row.configShow" type="text" @click="openRow(scope.row)">【点击查看详情】</el-button>
<img v-if="scope.row.contentType === 3" :alt="scope.row.configName" :src="scope.row.configValue" class="list_avatar"/>
<span v-if="scope.row.contentType === 4">{{ scope.row.configValue == 1 ? '开启' : '关闭' }}</span>
<span v-if="scope.row.contentType === 5 && scope.row.configKey === 'storagePlatform'">{{ storagePlatformEnum ? storagePlatformEnum[scope.row.configValue] : '' }}</span>
......
<template>
<el-dialog :title="title" v-model="visible" width="800px" :before-close="handleClose">
<el-dialog :title="title" :model-value="visible" width="800px" :before-close="handleClose">
<div class="dialog-content">
<div class="" v-html="value"/>
</div>
......
<template>
<el-dialog v-model="visible" :before-close="handleClose" :title="title" center width="600px">
<el-dialog :model-value="visible" :before-close="handleClose" :title="title" center width="600px">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
......
<template>
<el-dialog v-model="visible" :before-close="handleClose" :title="title" center width="600px">
<el-dialog :model-value="visible" :before-close="handleClose" :title="title" center width="600px">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType" disabled>
......
<template>
<el-dialog v-model="visible" :before-close="handleClose" :title="title" center width="600px">
<el-dialog :model-value="visible" :before-close="handleClose" :title="title" center width="600px">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item v-if="info.menuName" label="菜单名称" prop="menuName">
<el-input v-model="info.menuName" class="form-group" disabled/>
......
<template>
<el-dialog v-model="visible" :before-close="handleClose" :title="title" center width="600px">
<el-dialog :model-value="visible" :before-close="handleClose" :title="title" center width="600px">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="权限名称" class="form-group" prop="menuName">
<el-input v-model="form.menuName" maxlength="50" show-word-limit/>
......
<template>
<el-dialog
:title="title"
v-model="visible"
:model-value="visible"
width="600px"
center
:before-close="handleClose"
......
<template>
<el-dialog
:title="title"
v-model="visible"
:model-value="visible"
width="600px"
center
:before-close="handleClose"
......
<template>
<el-dialog v-model="visible" :before-close="handleClose" :title="title" center width="400px">
<el-dialog :model-value="visible" :before-close="handleClose" :title="title" center width="400px">
<div v-loading="ctrl.loading" style="min-height: 10vh">
<el-tree ref="tree" :data="availableList" :props="defaultProps" accordion highlight-current node-key="id" show-checkbox/>
</div>
......
<template>
<el-dialog
v-model="visible"
:model-value="visible"
:before-close="handleClose"
:title="title"
center
......
<template>
<el-dialog :title="title" v-model="visible" width="600px" center :before-close="handleClose">
<el-dialog :title="title" :model-value="visible" width="600px" center :before-close="handleClose">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="登录账号" prop="mobile">
<el-input v-model="form.mobile" class="form-group" maxlength="11" show-word-limit/>
......
<template>
<el-dialog v-model="visible" v-loading="dialogLoading" :before-close="handleClose" :title="title" center width="600px">
<el-dialog :model-value="visible" v-loading="dialogLoading" :before-close="handleClose" :title="title" center width="600px">
<el-form class="filter-container" inline label-width="80px">
<el-form-item class="filter-item" label="角色名称">
<el-input v-model="map.roleName"/>
......
<template>
<el-dialog
:title="title"
v-model="visible"
:model-value="visible"
width="600px"
center
:before-close="handleClose"
......
<template>
<el-dialog
:title="title"
v-model="visible"
:model-value="visible"
width="600px"
center
:before-close="handleClose"
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="800" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="800" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-row>
<el-col :span="12">
......
......@@ -3,8 +3,8 @@
<div class="page_head">
<div class="search_bar clearfix">
<el-form :model="seekForm" inline label-width="80px">
<el-form-item label="手机号码">
<el-input v-model="seekForm.mobile" clearable/>
<el-form-item label="讲师名称">
<el-input v-model="seekForm.lecturerName" clearable/>
</el-form-item>
<el-form-item>
<el-button @click="seek()" type="primary"> 查询</el-button>
......@@ -29,19 +29,19 @@
<span :class="{ 'c-danger': scope.row.statusId === 0 }">{{ statusIdEnums[scope.row.statusId] }}</span>
</template>
</el-table-column>
<el-table-column :width="300" fixed="right" label="操作" prop="address">
<el-table-column :width="200" fixed="right" label="操作" prop="address">
<template #default="scope">
<el-button v-if="checkPermission('user:admin:lecturer:edit')" plain type="primary" @click="openEditDialog(scope.row)">编辑</el-button>
<el-dropdown>
<el-button> 更多操作<i class="el-icon-arrow-down"/></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button v-if="checkPermission('user:admin:lecturer:edit') && scope.row.statusId == 0" plain type="success" @click="handleUpdateStatus(scope.row)">启用</el-button>
<el-button v-if="checkPermission('user:admin:lecturer:edit') && scope.row.statusId == 1" plain type="danger" @click="handleUpdateStatus(scope.row)">禁用</el-button>
<el-dropdown-item v-if="checkPermission('user:admin:lecturer:edit')">
<el-button v-if=" scope.row.statusId == 0" plain type="success" @click="handleUpdateStatus(scope.row)">启用</el-button>
<el-button v-if=" scope.row.statusId == 1" plain type="danger" @click="handleUpdateStatus(scope.row)">禁用</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button v-if="checkPermission('user:admin:lecturer:delete')" plain type="danger" @click="tableDelete(scope.row)">删除</el-button>
<el-dropdown-item v-if="checkPermission('user:admin:lecturer:delete')">
<el-button plain type="danger" @click="tableDelete(scope.row)">删除</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
......
<template>
<el-dialog v-model="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-dialog :model-value="visible" :append-to-body="true" :title="formModel.data.id ? '修改' : '添加'" :width="500" center @close="cloneDialog">
<el-form ref="ruleForm" :model="formModel.data" :rules="formModel.rules" class="demo-ruleForm" label-width="80px" @submit.prevent>
<el-form-item class="form-group" label="用户昵称" prop="nickname">
<el-input v-model="formModel.data.nickname" maxlength="100" show-word-limit></el-input>
......
......@@ -37,17 +37,17 @@
</el-table-column>
<el-table-column :width="200" fixed="right" label="操作" prop="address">
<template #default="scope">
<el-button v-if="checkPermission('user:admin:users:edit')" plain type="primary" @click="openEditDialog(scope.row)">编辑</el-button>
<el-button plain type="success" @click="userRecord(scope.row)">数据</el-button>
<el-dropdown>
<el-button> 更多操作<i class="el-icon-arrow-down"/></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button v-if="checkPermission('user:admin:users:edit') && scope.row.statusId == 0" plain type="success" @click="handleUpdateStatus(scope.row)">启用</el-button>
<el-button v-if="checkPermission('user:admin:users:edit') && scope.row.statusId == 1" plain type="danger" @click="handleUpdateStatus(scope.row)">禁用</el-button>
<el-dropdown-item v-if="checkPermission('user:admin:users:edit')">
<el-button plain type="primary" @click="openEditDialog(scope.row)">编辑</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button v-if="checkPermission('user:admin:users:delete')" plain type="danger" @click="tableDelete(scope.row)">删除</el-button>
<el-dropdown-item v-if="checkPermission('user:admin:users:edit')">
<el-button v-if=" scope.row.statusId == 0" plain type="success" @click="handleUpdateStatus(scope.row)">启用</el-button>
<el-button v-if="scope.row.statusId == 1" plain type="danger" @click="handleUpdateStatus(scope.row)">禁用</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
......@@ -106,9 +106,15 @@ export default defineComponent({
state.tableData.loading = false;
});
};
//数据
const userRecord = function(row) {
this.$router.push({path: '/users/record', query: {userId: row.id}});
}
return {
...toRefs(state),
handleUpdateStatus
handleUpdateStatus,
userRecord
};
}
});
......
<template>
<div class="app-container">
<div class="page_head">
<div class="search_bar clearfix">
<el-form :model="seekForm" inline label-width="80px">
<el-form-item>
<el-button @click="seek()" type="primary"> 查询</el-button>
<el-button @click="resetSeek()">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
<el-table v-loading="tableData.loading" :data="tableData.list" border>
<el-table-column align="center" label="序号" type="index" width="60"/>
<el-table-column label="封面" prop="courseLogo" min-width="20">
<template #default="scope">
<img :src="scope.row.courseLogo" :alt="scope.row.courseName"/>
</template>
</el-table-column>
<el-table-column label="课程名称" prop="courseName" min-width="20"/>
<el-table-column label="学习进度" prop="courseProgress">
<template #default="scope">
<el-progress
:percentage="scope.row.courseProgress"
:stroke-width="25"
:text-inside="true"
/>
</template>
</el-table-column>
<el-table-column label="开始学习时间" prop="gmtCreate" min-width="30"/>
<el-table-column width="100" label="操作">
<template #default="scope">
<el-button plain type="primary" @click="studyRecord(scope.row)">明细</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination :current-page="page.pageCurrent" :layout="page.layout" :page-size="page.pageSize" :page-sizes="[20, 50, 100, 200]" :total="page.totalCount" background @size-change="handleSizeChange" @current-change="handleCurrentChange"/>
<study v-model="study.visible" :info="study.info" @close="studyCallback"/>
</div>
</template>
<script>
import UseTable from '@/composables/UseTable.js';
import {ElMessage} from 'element-plus';
import {defineComponent, onMounted, reactive, toRefs} from 'vue';
import {useStore} from 'vuex';
import {userStudyePage} from '@/api/course.js'
import {userCoursePage} from '@/api/user.js'
import {useRoute} from 'vue-router/dist/vue-router';
import Study from './study.vue';
export default defineComponent({
components: {Study},
setup() {
const route = useRoute()
const apis = reactive({
getList: userCoursePage
})
const state = reactive({
...UseTable(apis, {userId: route.query.userId}),
loginStatusEnums: {}
});
const store = useStore();
onMounted(() => {
store.dispatch('GetOpts', {enumName: 'LoginStatusEnum', type: 'obj'}).then((res) => {
state.loginStatusEnums = res;
});
});
let study = reactive({
visible: false,
info: {}
})
const studyRecord = (row) => {
userStudyePage({courseId: row.courseId, userId: route.query.userId}).then((res) => {
study.info = res.list
study.visible = true
});
}
const studyCallback = () => {
study.visible = false
}
return {
...toRefs(state),
study,
studyRecord,
studyCallback
};
}
});
</script>
<template>
<el-dialog :model-value="visible" :append-to-body="true" :title="title" width="800px" @close="cloneDialog">
<el-table :data="info" row-key="id" :tree-props="{ children: 'userStudyPeriodPageRespList' }" default-expand-all>
<el-table-column label="章节名称" prop="chapterName">
<template #default="scope">
<span>{{ scope.row.chapterName }}</span>
<span>{{ scope.row.periodName }}</span>
</template>
</el-table-column>
<el-table-column label="学习时间" prop="gmtCreate" width="200">
<template #default="scope">
<span v-if="scope.row.progress >0">{{ scope.row.gmtCreate }}</span>
</template>
</el-table-column>
<el-table-column label="学习进度" prop="courseProgress" width="200">
<template #default="scope">
<el-progress v-if="scope.row.progress" :percentage="scope.row.progress" :stroke-width="25" :text-inside="true"/>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script>
import {defineComponent, reactive, ref, toRefs, watch} from 'vue';
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: () => {
return false;
}
},
title: {
type: String,
default: '学习记录'
},
info: {
type: Object,
default: () => {
return {}
}
}
},
emits: ['update:modelValue'],
setup(props, {emit}) {
const state = reactive({});
const visible = ref(false);
let {modelValue, info} = toRefs(props);
// 弹窗是否要打开监控
watch(modelValue, async(val) => {
visible.value = val;
});
console.log('info', info)
const cloneDialog = () => {
visible.value = false;
emit('update:modelValue', false);
};
return {
...toRefs(state),
visible,
cloneDialog
};
}
});
</script>
<template>
<div class="app-container">
<div class="page_head">
<div class="search_bar clearfix">
<el-form :model="seekForm" inline label-width="80px">
<el-form-item>
<el-button @click="seek()" type="primary"> 查询</el-button>
<el-button @click="resetSeek()">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
<el-table v-loading="tableData.loading" :data="tableData.list" border>
<el-table-column align="center" label="序号" type="index" width="60"/>
<el-table-column label="手机号码" prop="moblie"/>
<el-table-column label="登录IP" prop="loginIp"/>
<el-table-column label="登录地址" prop="province">
<template #default="scope">
{{ scope.row.province }} {{ scope.row.city }}
</template>
</el-table-column>
<el-table-column label="操作系统" prop="os"/>
<el-table-column label="浏览器" prop="browser"/>
<el-table-column label="登录时间" prop="gmtCreate"/>
<el-table-column label="登录状态">
<template #default="scope">
<span :class="{ 'c-danger': scope.row.loginStatus === 0 }">{{ loginStatusEnums[scope.row.loginStatus] }}</span>
</template>
</el-table-column>
</el-table>
<el-pagination :current-page="page.pageCurrent" :layout="page.layout" :page-size="page.pageSize" :page-sizes="[20, 50, 100, 200]" :total="page.totalCount" background @size-change="handleSizeChange" @current-change="handleCurrentChange"/>
</div>
</template>
<script>
import UseTable from '@/composables/UseTable.js';
import {ElMessage} from 'element-plus';
import {defineComponent, onMounted, reactive, toRefs} from 'vue';
import {useStore} from 'vuex';
import {logLoginPage} from '@/api/user.js'
export default defineComponent({
components: {},
setup() {
const apis = reactive({
getList: logLoginPage
})
const state = reactive({
...UseTable(apis, {}),
loginStatusEnums: {}
});
const store = useStore();
onMounted(() => {
store.dispatch('GetOpts', {enumName: 'LoginStatusEnum', type: 'obj'}).then((res) => {
state.loginStatusEnums = res;
});
});
return {
...toRefs(state)
};
}
});
</script>
......@@ -3,48 +3,49 @@ import vue from '@vitejs/plugin-vue'
import viteSvgIcons from 'vite-plugin-svg-icons';
import path from 'path';
const {resolve} = require("path");
const {resolve} = require('path');
// https://vitejs.dev/config/
export default defineConfig({
base: './',
plugins: [
vue(),
viteSvgIcons({
// 配置路劲在你的src里的svg存放文件
iconDirs: [path.resolve(process.cwd(), 'src/icons/svg')],
symbolId: 'icon-[name]',
}),
],
resolve: {
alias: {
"@": resolve(__dirname, "./src")
}
},
css: {
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove();
}
}
base: './',
plugins: [
vue(),
viteSvgIcons({
// 配置路劲在你的src里的svg存放文件
iconDirs: [path.resolve(process.cwd(), 'src/icons/svg')],
symbolId: 'icon-[name]'
})
],
resolve: {
alias: {
'@': resolve(__dirname, './src')
}
},
css: {
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove();
}
}
]
}
}
},
server: {
port: 9528, // 服务端口
proxy: { // 代理
'/gateway': {
target: 'http://dev-os.roncoos.com/gateway',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/gateway/, '')
}
},
},
]
}
},
server: {
port: 9528, // 服务端口
proxy: { // 代理
'/gateway': {
//target: 'http://localhost:8180',
target: 'https://dev-os.roncoos.com/gateway',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/gateway/, '')
}
}
}
})
此差异已折叠。