fix:项目初始化

This commit is contained in:
ouyangxiu
2025-06-10 17:11:41 +07:00
commit db9bffd2c5
713 changed files with 159746 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-05-16 10:46:46
* @LastEditors: 王涛
* @LastEditTime: 2021-01-27 10:44:03
-->
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span
v-if="item.redirect==='noRedirect'||index==levelList.length-1"
class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import pathToRegexp from 'path-to-regexp'
export default {
data() {
return {
levelList: null
}
},
watch: {
$route(route) {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
const matched = this.$route.matched.filter(item => item.meta && item.meta.title)
// const first = matched[0]
// if (!this.isDashboard(first)) {
// matched = [{ path: '/promptPage', meta: { title: '首页' }}].concat(matched)
// }
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'Home'.toLocaleLowerCase()
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 12px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: $themeColor;
cursor: text;
font-weight: bold;
font-size: 12px;
}
}
</style>
+53
View File
@@ -0,0 +1,53 @@
<!--
* @Author:
* @Mail:
* @Date: 2021-11-16 17:10:43
* @LastEditTime: 2022-04-07 20:17:07
* @LastEditors: Please set LastEditors
* @FilePath: /comics-admin/src/components/Hamburger/index.vue
-->
<template>
<div style="padding: 0 15px" @click="toggleClick">
<svg :class="{'is-active':isActive}" class="hamburger" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
import variables from '@/styles/variables.scss'
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
}
},
computed: {
variables() {
return variables
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
background-color: #fff;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>
+180
View File
@@ -0,0 +1,180 @@
<template>
<div :class="{'show':show}" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select ref="headerSearchSelect" v-model="search" :remote-method="querySearch" filterable default-first-option remote placeholder="Search" class="header-search-select" @change="change">
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
</el-select>
</div>
</template>
<script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js'
import path from 'path'
export default {
name: 'HeaderSearch',
data() {
return {
search: '',
options: [],
searchPool: [],
show: false,
fuse: undefined,
}
},
computed: {
routes() {
return this.$store.getters.permission_routes
},
},
watch: {
routes() {
this.searchPool = this.generateRoutes(this.routes)
},
searchPool(list) {
this.initFuse(list)
},
show(value) {
if (value) {
document.body.addEventListener('click', this.close)
} else {
document.body.removeEventListener('click', this.close)
}
},
},
mounted() {
this.searchPool = this.generateRoutes(this.routes)
},
methods: {
click() {
this.show = !this.show
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
}
},
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
this.options = []
this.show = false
},
change(val) {
this.$router.push(val.path)
this.search = ''
this.options = []
this.$nextTick(() => {
this.show = false
})
},
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true, // 是否按分数对结果列表排序
threshold: 0.4, // 匹配算法阀值,阀值为0时需完全匹配,1为完全不匹配
location: 0, // 确定文本中预期找到的模式的大致位置
distance: 100,
maxPatternLength: 32, // 模式的最大长度
minMatchCharLength: 1, // 模式的最小字符长度
keys: [
{
// 搜索标题与路径
name: 'title',
weight: 0.7,
},
{
name: 'path',
weight: 0.3,
},
],
})
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routes) {
// skip hidden router
if (router.hidden) {
continue
}
const data = {
path: path.resolve(basePath, router.path),
title: [...prefixTitle],
}
if (router.meta && router.meta.title) {
data.title = [...data.title, router.meta.title]
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
// recursive child routes
if (router.children) {
const tempRoutes = this.generateRoutes(
router.children,
data.path,
data.title
)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query)
} else {
this.options = []
}
},
},
}
</script>
<style lang="scss" scoped>
.header-search {
font-size: 0 !important;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
/deep/ .el-input__inner {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
}
}
</style>
+159
View File
@@ -0,0 +1,159 @@
<!--
* @Author:
* @Mail:
* @Date: 2021-11-13 14:46:20
* @LastEditTime: 2023-01-02 19:27:36
* @LastEditors: Please set LastEditors
* @FilePath: /web_admin/src/components/QuillEditor/index.vue
-->
<template>
<div>
<quill-editor v-model="content" ref="myQuillEditor" :options="editorOption" @ready="onEditorReady($event)"></quill-editor>
<!-- el-uploader不好手动触发上传功能 -->
<uploader
:style="{ display: 'none' }"
:options="{ ...options, target: '/api/web/file/uploadImg' }"
class="uploader-example"
@file-success="imageSuccess">
<uploader-drop>
<uploader-btn ref="editorUploadImg" :attrs="attrsImg"></uploader-btn>
</uploader-drop>
</uploader>
</div>
</template>
<script>
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import Quill from 'quill'
const Link = Quill.import('formats/link')
Link.PROTOCOL_WHITELIST = ['http', 'https', 'mailto', 'tel', 'yinseinner']
import { quillEditor } from 'vue-quill-editor'
import { ImageDrop } from 'quill-image-drop-module'
import ImageResize from 'quill-image-resize-module'
Quill.register('modules/imageDrop', ImageDrop)
Quill.register('modules/imageResize', ImageResize)
// 自定义字体类型
var fonts = ['SimSun', 'SimHei', 'Microsoft-YaHei', 'KaiTi', 'FangSong', 'Arial', 'Times-New-Roman', 'sans-serif']
var Font = Quill.import('formats/font')
Font.whitelist = fonts // 将字体加入到白名单
Quill.register(Font, true)
export default {
name: 'Editor',
components: {
quillEditor
},
props: {
textNode: {
default: ''
}
},
computed: {
options() {
return {
chunkSize: 1024 * 1024 * 30, // 切片大小
method: 'application/json',
testChunks: false,
allowDuplicateUploads: false, // 是否可以重复选择
query: {},
singleFile: false, // 是否只能选择单项
headers: {
Authorization: this.$store.getters.token
}
}
}
},
data() {
const _that = this
return {
content: '',
domain: this.$store.getters.getSystemConfig.webImgReq,
attrsImg: {
accept: ['image/*']
},
trackId: undefined,
editorOption: {
placeholder: '请输入帖子内容',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: 'ordered' }, { list: 'bullet' }],
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
[{ indent: '-1' }, { indent: '+1' }], // outdent/indent
[{ direction: 'rtl' }], // text direction
[{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
['link', 'image'],
['clean']
],
handlers: {
// 自定义上传图片
image: value => {
_that.$refs.editorUploadImg.$el.childNodes[0].click()
}
}
},
// //配置富文本框图片大小调整功能
// imageDrop: true,
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: ['Resize', 'DisplaySize', 'Toolbar']
}
}
}
// 暂存图片base64
}
},
mounted() {},
methods: {
// 父组件获取数据
getdata() {
const str = this.content.replace(this.domain, '')
return str
},
// 当文本框加载完成时,编译插入数据
async onEditorReady(e) {
const data = this.textNode.replace('img src="', 'img src="' + this.domain)
e.container.querySelector('.ql-blank').innerHTML = data
},
imageSuccess(rootFile, file, message) {
const { code, data } = JSON.parse(message)
if (code === 200) {
const reader = new FileReader()
reader.readAsDataURL(file.file)
reader.onload = e => {
const myeditor = this.$refs.myQuillEditor.quill
// editor方法获取当前光标位置
const index = myeditor.getSelection().index
myeditor.insertEmbed(index, 'image', this.domain + data.path)
// 调整光标到图片之后的位置上
myeditor.setSelection(index + 1)
}
}
},
clearInput() {
this.value = ''
}
}
}
</script>
+145
View File
@@ -0,0 +1,145 @@
<template>
<div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
<div class="rightPanel-background" />
<div class="rightPanel">
<div class="handle-button" :style="{'top':buttonTop+'px','background-color':theme}" @click="show=!show">
<i :class="show?'el-icon-close':'el-icon-setting'" />
</div>
<div class="rightPanel-items">
<slot />
</div>
</div>
</div>
</template>
<script>
import { addClass, removeClass } from '@/utils'
export default {
name: 'RightPanel',
props: {
clickNotClose: {
default: false,
type: Boolean
},
buttonTop: {
default: 250,
type: Number
}
},
data() {
return {
show: false
}
},
computed: {
theme() {
return this.$store.state.settings.theme
}
},
watch: {
show(value) {
if (value && !this.clickNotClose) {
this.addEventClick()
}
if (value) {
addClass(document.body, 'showRightPanel')
} else {
removeClass(document.body, 'showRightPanel')
}
}
},
mounted() {
this.insertToBody()
},
beforeDestroy() {
const elx = this.$refs.rightPanel
elx.remove()
},
methods: {
addEventClick() {
window.addEventListener('click', this.closeSidebar)
},
closeSidebar(evt) {
const parent = evt.target.closest('.rightPanel')
if (!parent) {
this.show = false
window.removeEventListener('click', this.closeSidebar)
}
},
insertToBody() {
const elx = this.$refs.rightPanel
const body = document.querySelector('body')
body.insertBefore(elx, body.firstChild)
}
}
}
</script>
<style>
.showRightPanel {
overflow: hidden;
position: relative;
width: calc(100% - 15px);
}
</style>
<style lang="scss" scoped>
.rightPanel-background {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
background: rgba(0, 0, 0, .2);
z-index: -1;
}
.rightPanel {
width: 100%;
max-width: 260px;
height: 100vh;
position: fixed;
top: 0;
right: 0;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
transition: all .25s cubic-bezier(.7, .3, .1, 1);
transform: translate(100%);
background: #fff;
z-index: 40000;
}
.show {
transition: all .3s cubic-bezier(.7, .3, .1, 1);
.rightPanel-background {
z-index: 20000;
opacity: 1;
width: 100%;
height: 100%;
}
.rightPanel {
transform: translate(0);
}
}
.handle-button {
width: 48px;
height: 48px;
position: absolute;
left: -48px;
text-align: center;
font-size: 24px;
border-radius: 6px 0 0 6px !important;
z-index: 0;
pointer-events: auto;
cursor: pointer;
color: #fff;
line-height: 48px;
i {
font-size: 24px;
line-height: 48px;
}
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
</div>
</template>
<script>
import screenfull from 'screenfull'
export default {
name: 'Screenfull',
data() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.destroy()
},
methods: {
click() {
if (!screenfull.enabled) {
this.$message({
message: 'you browser can not work',
type: 'warning'
})
return false
}
screenfull.toggle()
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.enabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.enabled) {
screenfull.off('change', this.change)
}
}
}
}
</script>
<style scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;;
width: 20px;
height: 20px;
vertical-align: 10px;
}
</style>
@@ -0,0 +1,93 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-10-05 11:37:08
* @LastEditors: 王涛
* @LastEditTime: 2020-11-24 17:04:24
-->
<template>
<el-dialog append-to-body class="img_dialog" :title="title" align="center" :visible.sync="dialogVisible" width="40%" :before-close="handleClose" :close-on-click-modal="false" :modal-append-to-body='false'>
<el-carousel v-if="isArry" indicator-position="outside">
<el-carousel-item v-for="item in mediaUrl" :key="item">
<div class="media-box" id="img-box" > <img :src="domain+item" /></div>
</el-carousel-item>
</el-carousel>
<div v-else class="media-box" id="img-box" > <img :src="mediaUrl" /></div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
baseConfig: {},
dialogVisible: false,
domain: this.$store.getters.getSystemConfig.webImgReq, // 存放图片的域名
mediaUrl: '',
title: '查看图片',
isArry: false
}
},
computed: {
token() {
return this.$store.state.token
}
},
mounted() {
},
methods: {
handleClose() {
if (this.player) {
this.player.dispose()
}
this.dialogVisible = false
this.mediaUrl = ''
},
open(url, type, data) {
if (typeof url === 'string') {
this.isArry = false
} else {
this.isArry = true
}
this.dialogVisible = true
this.title = '封面图查看'
this.mediaUrl = url
}
}
}
</script>
<style scoped lang="scss">
.my_data{
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.img_dialog{
::v-deep{
.el-dialog__body{
height: auto;
.el-carousel__item{
overflow: auto !important;
}
}
}
}
.media-box {
width: 100%;
height: 400px;
text-align: center;
overflow: hidden;
img {
height: 100%;
max-width: 100%;
}
#my-video {
width: 100% !important;
height: 100% !important;
border: none !important;
}
}
</style>
+165
View File
@@ -0,0 +1,165 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-10-05 11:36:16
* @LastEditors: 江涛
* @LastEditTime: 2021-03-03 17:50:26
-->
<template>
<div class="img-wrap">
<div v-if='showChangeBtn' class="changeLeft" @click="changeLeft">
<i class="el-icon-arrow-left"></i>
</div>
<div class="showImg">
<div v-if="showNoImg">暂无</div>
<el-image v-else :src="src" style="width: 100%, height: 100%" @click="checkMedia()" lazy >
<div slot="placeholder" class="image-slot" :style="{width: width, height: height, lineHeight: height}">
<i class="el-icon-loading"></i>加载中
</div>
</el-image>
</div>
<div v-if='showChangeBtn' class="changeRight" @click="changeRight">
<i class="el-icon-arrow-right"></i>
</div>
<!--封面图视频弹窗组件-->
<Dialog ref="dialog"></Dialog>
</div>
</template>
<script>
import Dialog from './components/dialog'
export default {
data() {
return {
showNoImg: false,
src: undefined,
showChangeBtn: false,
domain: this.$store.getters.getSystemConfig.webImgReq, // 存放图片的域名
imgsUrl: []
}
},
components: {
Dialog
},
props: ['width', 'height', 'urls'],
watch: {
urls(newval, oldval) {
this.urls = newval
// if (this.type && this.type === 'daichong') {
// this.domain = ''
// }
this.imgsUrl = []
if (this.urls) { // 判断是否存在
if (typeof (this.urls) === 'string') { // 判断是单张图片还是数组
this.src = this.domain + this.urls
this.showNoImg = false
} else {
if (this.urls.length) { // 判断是否为空数组
if (this.urls.length === 1) { // 判断数组中是否只有一张图片
this.showChangeBtn = false
} else {
this.showChangeBtn = true
}
this.urls.forEach(item => {
this.imgsUrl.push(this.domain + item)
})
this.src = this.imgsUrl[0]
this.showNoImg = false
} else {
this.showNoImg = true
}
}
} else {
this.showChangeBtn = false
this.showNoImg = true
}
}
},
mounted() {
this.imgsUrl = []
if (this.urls) { // 判断是否存在
if (typeof (this.urls) === 'string') { // 判断是单张图片还是数组
this.src = this.domain + this.urls
} else {
if (this.urls.length) { // 判断是否为空数组
if (this.urls.length === 1) { // 判断数组中是否只有一张图片
this.showChangeBtn = false
} else {
this.showChangeBtn = true
}
this.urls.forEach(item => {
this.imgsUrl.push(this.domain + item)
})
this.src = this.imgsUrl[0]
} else {
this.showNoImg = true
}
}
} else {
this.showChangeBtn = false
this.showNoImg = true
}
},
methods: {
changeLeft() {
const index = this.imgsUrl.indexOf(this.src)
if (index === 0) {
this.src = this.imgsUrl[this.imgsUrl.length - 1]
} else {
this.src = this.imgsUrl[index - 1]
}
},
changeRight() {
const index = this.imgsUrl.indexOf(this.src)
if (index === this.imgsUrl.length - 1) {
this.src = this.imgsUrl[0]
} else {
this.src = this.imgsUrl[index + 1]
}
},
// 查看媒体
checkMedia() {
if (this.urls) { // 判断是否存在
if (typeof (this.urls) === 'string') { // 判断是单张图片还是数组
this.$refs.dialog.open(this.domain + this.urls, 'img')
} else {
this.$refs.dialog.open(this.urls, 'img')
}
}
}
}
}
</script>
<style lang="scss">
.img-wrap {
display: flex;
justify-content: center;
cursor: pointer;
.changeLeft,
.changeRight {
width: 15px;
height: 15px;
line-height: 15px;
margin-top: 15%;
cursor: pointer;
}
.showImg {
width: 100%;
height:100%;
.image-slot {
background: #f5f7fa;
font-size: 12px;
color: #c0c4cc;
}
.el-image__error {
font-size: 12px;
}
.el-image{
width: 100%;
height: 100%;
}
}
}
</style>
+134
View File
@@ -0,0 +1,134 @@
<template>
<div>
<el-dialog
:title="title"
align="center"
:visible.sync="dialogVisible"
width="40%"
:before-close="handleClose"
:close-on-click-modal="false"
append-to-body>
<div :data="myData" class="my_data">
<span>视频高度:{{ this.myData.height }}</span>
<span>视频宽度:{{ this.myData.width }}</span>
<span>视频时长:{{ this.myData.playTime + "s" }}</span>
</div>
<!-- <div class="media-box" id="media-box"></div> -->
<div v-if="mediaUrl" style="width: 400px; heigth: 130px; background: red;margin-top:10px">
<DPlayer
ref="player"
:video="{
url: mediaUrl,
type: type === 'mp4' ? '' : 'hls',
pic: '',
}"
hotkey
:autoplay="true"
@playing="onPlayerPlay"
@pause="onPlayerPause"
@loadeddata="onPlayerLoadeddata"
@error="onPlayerError"
@ended="onPlayerEnded"
@waiting="onPlayerWaiting"></DPlayer>
</div>
</el-dialog>
</div>
</template>
<script>
// import videojs from 'video.js'
// import 'videojs-contrib-hls'
import { videoFeviewList } from '@/api/video.js'
import DPlayer from '../VueDplayerHls/index'
// import API from '@/api'
export default {
name: 'ShowVideo',
components: {
DPlayer
},
data() {
return {
dialogVisible: false,
mediaUrl: '',
title: '封面图查看',
params: {},
myData: {},
type: undefined,
domain: '/api/web/media/m3u8/' // 存放图片的域名
}
},
computed: {
token() {
return this.$store.state.user.token
}
},
methods: {
// / 监听视频播放的回调函数
onPlayerPlay() {},
// / 监听暂停播放的回调函数
onPlayerPause() {},
// / 监听当前视频数据加载完成
onPlayerLoadeddata() {},
// / 监听当前视频数据加载失败
onPlayerError() {},
// / 监听当前视频数据加载ing
onPlayerWaiting() {},
// / 监听当前视频播放完成
onPlayerEnded() {},
// 视频审查列表
videoFeviewList() {
videoFeviewList(this.params, this.id).then((res) => {
// this.myData = res.data,
})
},
handleClose() {
if (this.player) {
this.player.dispose()
}
this.dialogVisible = false
this.mediaUrl = ''
},
open(url, data, id) {
this.myData = { ...data }
this.dialogVisible = true
this.title = '视频查看'
this.mediaUrl =
this.domain + (url.match(RegExp(/.m3u8/)) ? url : url + '.m3u8')
if (url.indexOf('mp4') !== -1) {
this.mediaUrl = this.$store.getters.getSystemConfig.webImgReq + url
this.type = 'mp4'
} else {
this.mediaUrl =
this.domain + (url.match(RegExp(/.m3u8/)) ? url : url + '.m3u8')
this.videoFeviewList()
}
}
}
}
</script>
<style scoped lang="scss">
.my_data {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.media-box {
width: 100%;
height: 400px;
text-align: center;
overflow: hidden;
img {
height: 100%;
max-width: 100%;
}
#my-video {
width: 100% !important;
height: 100% !important;
border: none !important;
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>
+167
View File
@@ -0,0 +1,167 @@
<!--
* @Author: your name
* @Date: 2020-09-10 16:03:32
* @LastEditTime: 2021-03-24 11:27:17
* @LastEditors: 江涛
* @Description: In User Settings Edit
* @FilePath: /web-admin-MDDSP/src/components/TagSelect/index.vue
-->
<template>
<div class="reason-action-box">
<el-row :gutter="24">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-form-item label="二级标签:" label-width="75px">
<div style="max-width:470px;max-height:100px;overflow: auto">
<el-tag :key="index" v-for="(item,index) in useselectTag" closable :disable-transitions="false" @close="clickselectTag(item,index)">{{item}}</el-tag>
<el-button class="button-new-tag" size="mini" @click="showInput">+ 新增标签</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24" v-if="inputVisible">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-form-item label="搜索:" label-width="45px">
<el-autocomplete
class="inline-input"
v-model="inputValue"
:fetch-suggestions="querySearch"
placeholder="请输入内容"
:trigger-on-focus="false"></el-autocomplete>
<el-button @click="addTag">添加</el-button>
</el-form-item>
<el-form-item label-width="50px" style="margin-top:15px">
<div style="max-width:470px;height:500px;overflow: auto;min-width:300px">
<div v-for="(val, key, index) in tagsObj" :key='index'>
<el-button style="widht:120px" plain size="mini" type="info" v-for="(item,indexl) in val" :key="indexl" @click="clickCusTag(item,indexl,key)">{{item}}</el-button>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
</div>
</template>
<script>
// import API from '@/api'
import { mediatagList } from '@/api/mediaLibrary/mediaTag'
export default {
props: {
selectTag: {
default: ''
}
},
data() {
return {
mediatagRes: [], // 模糊查询使用的数据
allCusTag: {}, // 所有的标签
useselectTag: this.selectTag.length ? this.selectTag.split(',') : [], // 存储已选择标签
tagsObj: {}, // 剩余未选择的标签
inputValue: '',
inputVisible: false
}
},
mounted() {
mediatagList({ pageNum: 1, pageSize: 1000, status: true,
type: 2 }).then(res => {
if (res.code === 200) {
const aaa = {}
const bbb = []
res.data.list.filter(item => {
bbb.push({ value: item.name, label: item.name })
if (aaa[item.type]) {
aaa[item.type].push(item.name)
} else {
aaa[item.type] = []
aaa[item.type].push(item.name)
}
})
this.allCusTag = JSON.parse(JSON.stringify(aaa))
this.tagsObj = JSON.parse(JSON.stringify(aaa))
this.mediatagRes = bbb
this.removeTag()
}
})
},
methods: {
querySearch(queryString, cb) {
var restaurants = this.mediatagRes
var results = queryString ? restaurants.filter(this.createFilter(queryString)) : restaurants
// 调用 callback 返回建议列表的数据
cb(results)
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0)
}
},
addTag() {
this.useselectTag.push(this.inputValue)
this.removeTag()
},
showInput() {
this.inputVisible = true
},
// 第一次渲染时从标签列表中移除已有标签
removeTag() {
this.useselectTag.forEach(item => {
Object.keys(this.tagsObj).forEach(key => {
if (this.tagsObj[key].indexOf(item) !== -1 || this.tagsObj[key].indexOf(item) === 0) {
this.tagsObj[key].splice(this.tagsObj[key].indexOf(item), 1)
}
})
})
},
// 删除已选择标签
clickselectTag(data, index) {
this.useselectTag.splice(index, 1)
Object.keys(this.tagsObj).forEach(item => {
if (this.allCusTag[item].indexOf(data) !== -1 || this.allCusTag[item].indexOf(data) === 0) {
this.tagsObj[item].push(data)
}
})
},
// 点击客服标签
clickCusTag(data, index, key) {
this.tagsObj[key].splice(index, 1)
this.useselectTag.push(data)
},
// 返回已选择但不是客服标签的标签
NotCusTag() {
const arry = []
this.useselectTag.forEach((item) => {
if (this.allCusTag.indexOf(item) === -1) {
arry.push(item)
}
})
return arry
},
// 返回已选择的标签
returnSelectTag() {
return this.useselectTag
}
}
}
</script>
<style lang="scss" scoped>
.reason-action-box {
.el-tag {
padding: 0 10px;
margin-right: 5px;
}
.input {
width: 90px;
margin-left: 30px;
}
button {
padding: 9px 15px;
}
}
.reason-action-box button[data-v-2b5b3839]{
width: 80px;
padding: 9px 15px;
margin-right: 10px;
margin-left: 0px;
}
</style>
+272
View File
@@ -0,0 +1,272 @@
<template>
<el-form>
<div class="reason-action-box">
<el-row :gutter="24">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-form-item label="已选标签" v-if="type !== 'resourceManagement'">
<el-tag :key="index + 'tag'" v-for="(item, index) in selectTags" :disable-transitions="true" closable
style="margin: 2px" @close="handleTagClose(item)">
{{ item | changeName(allTags, 'id', 'name') }}
</el-tag>
</el-form-item>
<el-form-item label="已选标签" v-else>
<el-tag :key="index + 'tag'" v-for="(item, index) in selectTags" :disable-transitions="true" closable
style="margin: 2px" @close="handleTagClose(item)">
{{ item | changeName(newAllTags, 'id', 'name') }}
</el-tag>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-form-item label="可选标签">
<el-button :type="showTagsFlag ? 'warning' : 'success'" @click="showTagsFun">{{ showTagsFlag ? '隐藏标签' :
'展示标签'
}}</el-button>
<el-form-item label="标签分类过滤">
<el-select placeholder="标签分类" @change="changeTagsCategory" multiple v-model="tagCategory" clearable
filterable @clear="tagCategory = null">
<el-option :key="index" :label="item.name" :value="item.id" v-for="(item, index) in mediatagCategory" />
</el-select>
</el-form-item>
<el-form-item label="关键字过滤">
<el-input v-model="searchTagTxt" placeholder="请输入标签名称" clearable />
</el-form-item>
<el-form-item >
<el-button @click="tagFilter">搜索标签</el-button>
</el-form-item>
<div style="width: 1000px;height: 500px;overflow-y: auto;" v-if="type !== 'resourceManagement'">
<template v-for="(itemList, index) in newTagList">
<div v-if="itemList.showItem && tagCategory.length" :key="index">
<span v-show="showTagsFlag">{{ itemList.id | changeName(newTagList, 'id', 'name') }}</span>
<el-tag v-show="showTagsFlag" :key="index + 'tagData'" style="margin: 2px; cursor: pointer"
v-for="(item, index) in itemList.tags" :disable-transitions="true"
:effect="item && item.hasTopic ? 'dark' : 'light'" :type="fliterTagType(item, item.id, item.topicIds)"
@click="handleClickTag(item.id)">
{{ item.name }}
</el-tag>
</div>
<div v-else-if="!itemList.showItem && !tagCategory.length" :key="index">
<span v-show="showTagsFlag">{{ itemList.id | changeName(newTagList, 'id', 'name') }}</span>
<el-tag v-show="showTagsFlag" :key="index + 'tagData'" style="margin: 2px; cursor: pointer"
v-for="(item, index) in itemList.tags" :disable-transitions="true"
:effect="item && item.hasTopic ? 'dark' : 'light'" :type="fliterTagType(item, item.id, item.topicIds)"
@click="handleClickTag(item.id)">
{{ item.name }}
</el-tag>
</div>
</template>
</div>
<div style="width: 1000px;height: 500px;overflow-y: auto;" v-else>
<template v-for="(itemList, index) in newAllTagsCopy">
<div :key="index" v-if="itemList.showItem && tagCategory.length">
<span v-show="showTagsFlag">{{ itemList.id | changeName(allTags, 'id', 'name') }}</span>
<el-tag v-show="showTagsFlag" :key="index + 'tagData'" style="margin: 2px; cursor: pointer"
v-for="(item, index) in itemList.list" :disable-transitions="true"
:effect="item && item.hasTopic ? 'dark' : 'light'" :type="fliterTagType(item, item.id, item.topicIds)"
@click="handleClickTag(item.id)">
{{ item.name }}
</el-tag>
</div>
<div v-else-if="!itemList.showItem && !tagCategory.length" :key="index">
<span v-show="showTagsFlag">{{ itemList.id | changeName(newAllTagsCopy, 'id', 'name') }}</span>
<el-tag v-show="showTagsFlag" :key="index + 'tagData'" style="margin: 2px; cursor: pointer"
v-for="(item, index) in itemList.list" :disable-transitions="true"
:effect="item && item.hasTopic ? 'dark' : 'light'" :type="fliterTagType(item, item.id, item.topicIds)"
@click="handleClickTag(item.id)">
{{ item.name }}
</el-tag>
</div>
</template>
</div>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
</template>
<script>
// import API from '@/api'
import {
mediaAllList
} from '@/api/postManagement/newList'
export default {
props: {
formTags: {//传入以选择标签
default: []
},
allTags: {//所有标签
default: []
},
mediatagCategory: {//标签分类
default: []
},
type: {//标签分类
default: String
},
},
data() {
return {
selectTags: [],//已选择标签
newTagList: [],//经过分类的标签组
showTagsFlag: true,
tagCategory: [],
searchTagTxt: '',
newAllTagsCopy:[]
}
},
mounted() {
if (this.type !== 'resourceManagement') {
// 标签绑定标签分类
if (this.mediatagCategory.length) {
this.newTagList = JSON.parse(JSON.stringify(this.mediatagCategory))
this.newTagList.forEach(item => {
item.showItem = false
item.tags = this.allTags.filter(sItem => {
return item.id === sItem.categoryId
})
})
} else {
this.newTagList = [{ id: 0, name: '全部' }]
this.newTagList.forEach(item => {
item.showItem = false
item.tags = this.allTags
})
}
}
},
watch: {
'formTags': {
handler(n, o) {
this.selectTags = n
},
immediate: true
},
'allTags':{
handler(n, o) {
this.newAllTagsCopy = JSON.parse(JSON.stringify(n))
},
immediate: true
}
},
computed: {
newAllTags() {
let arr = []
if (this.type == 'resourceManagement') {
this.allTags.map(item => {
item.showItem = false
if (item.list && item.list.length) {
arr = [...arr, ...item.list]
}
})
return arr
} else {
return []
}
},
},
methods: {
changeTagsCategory() {
if (this.type !== 'resourceManagement') {
this.newTagList.forEach(element => {
if (this.tagCategory.includes(element.id)) {
element.showItem = true
} else {
element.showItem = false
}
});
} else {
for (let index = 0; index < this.newAllTagsCopy.length; index++) {
if (this.tagCategory.includes(this.newAllTagsCopy[index].id)) {
this.newAllTagsCopy[index].showItem = true
} else {
this.newAllTagsCopy[index].showItem = false
}
}
}
},
handleTagClose(tag) {
this.selectTags.splice(this.selectTags.indexOf(tag), 1)
this.$emit('selectTags', this.selectTags)
},
showTagsFun() {
this.showTagsFlag = !this.showTagsFlag
},
// 点击可选择列表中的标签
handleClickTag(tagID) {
if (this.selectTags.indexOf(tagID) === -1) {
this.selectTags.push(tagID)
} else {
this.selectTags.splice(this.selectTags.indexOf(tagID), 1)
}
this.$emit('selectTags', this.selectTags)
},
fliterTagType(item, tagID, topicIds) {
if (this.selectTags.indexOf(tagID) !== -1) {
return 'danger'
} else {
if (topicIds) {return 'default' } else {
if(this.searchTagTxt){
if (item.name.indexOf(this.searchTagTxt) !== -1) {
return 'warning'
}
}else {
return 'info'
}
}
}
},
getValue() {
return this.selectTags
},
// 标签搜索
async tagFilter(){
// TODO:请求所有分类标签列表
await mediaAllList({ pageNum: 1, pageSize: 100,name:this.searchTagTxt }).then((res) => {
if (res.code === 200) {
this.newAllTagsCopy = res.data.list
} else {
this.newAllTagsCopy = []
this.$message.error('请求标签分类失败')
}
})
}
}
}
</script>
<style lang="scss" scoped>
.reason-action-box {
// height: 200px;
.el-tag {
padding: 0 10px;
margin-right: 5px;
}
.input {
width: 90px;
margin-left: 30px;
}
button {
padding: 9px 15px;
}
}
.reason-action-box button[data-v-2b5b3839] {
width: 80px;
padding: 9px 15px;
margin-right: 10px;
margin-left: 0px;
}
</style>
@@ -0,0 +1,111 @@
<template>
<div class="upload-container">
<el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
upload
</el-button>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<el-upload
:multiple="true"
:file-list="fileList"
:show-file-list="true"
:on-remove="handleRemove"
:on-success="handleSuccess"
:before-upload="beforeUpload"
class="editor-slide-upload"
action="https://httpbin.org/post"
list-type="picture-card">
<el-button size="small" type="primary">
Click upload
</el-button>
</el-upload>
<el-button @click="dialogVisible = false">
Cancel
</el-button>
<el-button type="primary" @click="handleSubmit">
Confirm
</el-button>
</el-dialog>
</div>
</template>
<script>
// import { getToken } from 'api/qiniu'
export default {
name: 'EditorSlideUpload',
props: {
color: {
type: String,
default: '#1890ff'
}
},
data() {
return {
dialogVisible: false,
listObj: {},
fileList: []
}
},
methods: {
checkAllSuccess() {
return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
},
handleSubmit() {
const arr = Object.keys(this.listObj).map(v => this.listObj[v])
if (!this.checkAllSuccess()) {
this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
return
}
this.$emit('successCBK', arr)
this.listObj = {}
this.fileList = []
this.dialogVisible = false
},
handleSuccess(response, file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
this.listObj[objKeyArr[i]].url = response.files.file
this.listObj[objKeyArr[i]].hasSuccess = true
return
}
}
},
handleRemove(file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
delete this.listObj[objKeyArr[i]]
return
}
}
},
beforeUpload(file) {
const _self = this
const _URL = window.URL || window.webkitURL
const fileName = file.uid
this.listObj[fileName] = {}
return new Promise((resolve, reject) => {
const img = new Image()
img.src = _URL.createObjectURL(file)
img.onload = function() {
_self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
}
resolve(true)
})
}
}
}
</script>
<style lang="scss" scoped>
.editor-slide-upload {
margin-bottom: 20px;
::v-deep .el-upload--picture-card {
width: 100%;
}
}
</style>
@@ -0,0 +1,59 @@
let callbacks = []
function loadedTinymce() {
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
// check is successfully downloaded script
return window.tinymce
}
const dynamicLoadScript = (src, callback) => {
const existingScript = document.getElementById(src)
const cb = callback || function() {}
if (!existingScript) {
const script = document.createElement('script')
script.src = src // src url for the third-party library being loaded.
script.id = src
document.body.appendChild(script)
callbacks.push(cb)
const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
onEnd(script)
}
if (existingScript && cb) {
if (loadedTinymce()) {
cb(null, existingScript)
} else {
callbacks.push(cb)
}
}
function stdOnEnd(script) {
script.onload = function() {
// this.onload = null here is necessary
// because even IE9 works not like others
this.onerror = this.onload = null
for (const cb of callbacks) {
cb(null, script)
}
callbacks = null
}
script.onerror = function() {
this.onerror = this.onload = null
cb(new Error('Failed to load ' + src), script)
}
}
function ieOnEnd(script) {
script.onreadystatechange = function() {
if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
this.onreadystatechange = null
for (const cb of callbacks) {
cb(null, script) // there is no way to catch loading errors in IE8
}
callbacks = null
}
}
}
export default dynamicLoadScript
+291
View File
@@ -0,0 +1,291 @@
<template>
<div
:class="{ fullscreen: fullscreen }"
class="tinymce-container"
:style="{ width: containerWidth }">
<textarea :id="tinymceId" class="tinymce-textarea" />
<div class="editor-custom-btn-container">
<editorImage
color="#1890ff"
class="editor-upload-btn"
@successCBK="imageSuccessCBK"/>
</div>
</div>
</template>
<script>
/**
* docs:
* https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce
*/
import editorImage from './components/EditorImage'
import plugins from './plugins'
import toolbar from './toolbar'
import load from './dynamicLoadScript'
// import './plugins/media-plugin-magic-by-snowmile'
// why use this cdn, detail see https://github.com/PanJiaChen/tinymce-all-in-one
const tinymceCDN =
'https://cdn.jsdelivr.net/npm/tinymce-all-in-one@4.9.3/tinymce.min.js'
export default {
name: 'Tinymce',
components: { editorImage },
props: {
id: {
type: String,
default: function() {
return (
'vue-tinymce-' +
+new Date() +
((Math.random() * 1000).toFixed(0) + '')
)
}
},
value: {
type: String,
default: ''
},
toolbar: {
type: Array,
required: false,
default() {
return []
}
},
menubar: {
type: String,
default: 'file edit insert view format table'
},
height: {
type: [Number, String],
required: false,
default: 360
},
width: {
type: [Number, String],
required: false,
default: 'auto'
}
},
data() {
return {
hasChange: false,
hasInit: false,
tinymceId: this.id,
fullscreen: false,
domain: this.$store.getters.getSystemConfig.webImgReq, // 存放图片的域名
languageTypeList: {
en: 'en',
zh: 'zh_CN',
es: 'es_MX',
ja: 'ja'
}
}
},
computed: {
containerWidth() {
const width = this.width
if (/^[\d]+(\.[\d]+)?$/.test(width)) {
// matches `100`, `'100'`
return `${width}px`
}
return width
}
},
watch: {
value(val) {
if (!this.hasChange && this.hasInit) {
this.$nextTick(() =>
window.tinymce.get(this.tinymceId).setContent(val || '')
)
}
}
},
mounted() {
this.init()
},
activated() {
if (window.tinymce) {
this.initTinymce()
}
},
deactivated() {
this.destroyTinymce()
},
destroyed() {
this.destroyTinymce()
},
methods: {
init() {
// dynamic load tinymce from cdn
load(tinymceCDN, (err) => {
if (err) {
this.$message.error(err.message)
return
}
this.initTinymce()
})
},
initTinymce() {
const _this = this
window.tinymce.init({
selector: `#${this.tinymceId}`,
language: this.languageTypeList['zh'],
height: this.height,
body_class: 'panel-body ',
object_resizing: false,
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
menubar: this.menubar,
plugins: plugins,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
file_picker_types: 'file image media',
advlist_bullet_styles: 'square',
advlist_number_styles: 'default',
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: (editor) => {
if (_this.value) {
editor.setContent(_this.value)
}
_this.hasInit = true
editor.on('NodeChange Change KeyUp SetContent', (event) => {
this.hasChange = true
this.$emit('input', editor.getContent())
})
},
setup(editor) {
editor.on('FullscreenStateChanged', (e) => {
_this.fullscreen = e.state
})
},
images_upload_handler: function(blobInfo, succFun, failFun) {
const file = blobInfo.blob() // 转化为易于理解的file对象
const xhr = new XMLHttpRequest()
xhr.withCredentials = false
xhr.open('POST', '/api/web/mediafile/uploadImg')
xhr.onload = function() {
if (xhr.status !== 200) {
failFun('HTTP Error: ' + xhr.status)
return
}
const json = JSON.parse(xhr.responseText)
succFun(`${_this.domain}${json.data.path}`)
_this.$emit('addPictur', json.data.path)
}
const formData = new FormData()
formData.append('file', file, file.name) // 此处与源文档不一样
xhr.send(formData)
},
// it will try to keep these URLs intact
// https://www.tiny.cloud/docs-3x/reference/configuration/Configuration3x@convert_urls/
// https://stackoverflow.com/questions/5196205/disable-tinymce-absolute-to-relative-url-conversions
convert_urls: false
// 整合七牛上传
// images_dataimg_filter(img) {
// setTimeout(() => {
// const $image = $(img);
// $image.removeAttr('width');
// $image.removeAttr('height');
// if ($image[0].height && $image[0].width) {
// $image.attr('data-wscntype', 'image');
// $image.attr('data-wscnh', $image[0].height);
// $image.attr('data-wscnw', $image[0].width);
// $image.addClass('wscnph');
// }
// }, 0);
// return img
// },
// images_upload_handler(blobInfo, success, failure, progress) {
// progress(0);
// const token = _this.$store.getters.token;
// getToken(token).then(response => {
// const url = response.data.qiniu_url;
// const formData = new FormData();
// formData.append('token', response.data.qiniu_token);
// formData.append('key', response.data.qiniu_key);
// formData.append('file', blobInfo.blob(), url);
// upload(formData).then(() => {
// success(url);
// progress(100);
// })
// }).catch(err => {
// failure('出现未知问题,刷新页面,或者联系程序员')
// console.log(err);
// });
// },
})
},
destroyTinymce() {
const tinymce = window.tinymce.get(this.tinymceId)
if (this.fullscreen) {
tinymce.execCommand('mceFullScreen')
}
if (tinymce) {
tinymce.destroy()
}
},
setContent(value) {
window.tinymce.get(this.tinymceId).setContent(value)
},
getContent() {
window.tinymce.get(this.tinymceId).getContent()
},
imageSuccessCBK(arr) {
console.log(arr, '===========')
// arr.forEach((v) =>
// window.tinymce
// .get(this.tinymceId)
// .insertContent(`<img class="wscnph" style="width:164px;height:92px;margin-right:15px;" src="${v.url}" >`)
// );
}
}
}
</script>
<style lang="scss" scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container {
::v-deep {
.mce-fullscreen {
z-index: 10000;
}
}
}
.tinymce-textarea {
visibility: hidden;
z-index: -1;
}
.editor-custom-btn-container {
position: absolute;
right: 4px;
top: 4px;
/*z-index: 2005;*/
}
.fullscreen .editor-custom-btn-container {
z-index: 10000;
position: fixed;
}
.editor-upload-btn {
display: inline-block;
}
</style>
+7
View File
@@ -0,0 +1,7 @@
// Any plugins you want to use has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/
const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
export default plugins
+6
View File
@@ -0,0 +1,6 @@
// Here is a list of the toolbar
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
export default toolbar
+236
View File
@@ -0,0 +1,236 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-09-29 19:56:44
* @LastEditors: Please set LastEditors
* @LastEditTime: 2022-03-29 15:47:16
-->
<template>
<div>
<draggable v-model="locaImg" class="list-group" tag="ul" v-bind="dragOptions" @start="drag = true" @end="drag = false">
<div v-for="(item,index) in locaImg" :key="index" class="image-list" style="float: left;">
<img class="el-upload-list__item-thumbnail image-detail" :src="domain+item.comicsPic" alt="">
<!-- 遮罩层 -->
<a href="#">
<div class="image-mask">
<span class="add" @click="handlePictureCardPreview(domain+item.comicsPic)">
<i class="el-icon-view"></i>
</span>
<span class="delete" @click="removeImage(index)">
<i class="el-icon-delete"></i>
</span>
</div>
</a>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="">
</el-dialog>
<el-input v-model.number="item.high" placeholder="高" style="width: 70px" />
<el-input v-model.number="item.width" placeholder="宽" style="width: 70px" />
</div>
</draggable>
<el-upload action="/api/web/file/uploadImg" :show-file-list="false" list-type="picture-card" :file-list="fileList" :on-preview="handlePictureCardPreview" :on-change="handleChange" multiple :limit='100'>
<i class="el-icon-plus"></i>
</el-upload>
</div>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: { draggable },
data() {
return {
domain: this.$store.getters.getSystemConfig.webImgReq, // 播放视频的域名
returnURL: [],
dialogImageUrl: '',
dialogVisible: false,
fileList: [
// { name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' },
// { name: 'food2.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' }
],
drag: false, // 拖拽效果是否开启
locaImg: [],
}
},
props: {
img: {
type: Array,
default: function () {
return []
},
},
},
computed: {
dragOptions() {
return {
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost',
}
},
},
watch: {
// 监听拖动事件改变时,传出新的图片数组
drag() {
this.changeImgUrl()
},
img(New, old) {
this.locaImg = []
New.filter((item) => {
this.locaImg.push(item)
})
},
},
mounted() {
this.locaImg = []
if (this.img) {
this.img.filter((item) => {
this.locaImg.push(item)
})
}
},
methods: {
handleChange(file, fileList) {
if (file.status === 'success') {
// this.locaImg.push(file.response.data.path)
this.locaImg.push({
high: undefined,
width: undefined,
comicsPic: file.response.data.path,
})
this.changeImgUrl()
}
},
// 移除图片
removeImage(val) {
this.locaImg.splice(val, 1)
this.changeImgUrl()
},
// 预览
handlePictureCardPreview(url) {
this.dialogImageUrl = url
this.dialogVisible = true
},
changeImgUrl(arr) {
this.$emit('changeImgUrl', this.locaImg)
},
},
}
</script>
<style lang="scss" scoped>
.list-group {
padding: 0px;
margin: 0px;
}
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 148px;
height: 148px;
line-height: 148px;
text-align: center;
}
.avatar {
width: 148px;
height: 148px;
display: block;
}
.image-list {
overflow: hidden;
background-color: #fff;
border: 1px solid #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: 148px;
height: 202px;
margin: 0 8px 8px 0;
display: inline-block;
position: relative;
img {
width: 148px;
height: 148px;
}
}
.image-list a:hover .image-mask {
opacity: 1;
}
.el-icon-check {
color: #fff;
}
.input-box {
width: 80%;
}
.el-upload--picture-card {
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: 148px;
height: 148px;
line-height: 146px;
vertical-align: top;
}
.image-mask {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
text-align: center;
color: #fff;
opacity: 0;
font-size: 20px;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s;
display: flex;
flex-direction: row;
}
.image-detail {
width: 100%;
height: 100%;
position: relative;
}
.image-mask::after {
display: inline-block;
content: '';
height: 100%;
vertical-align: middle;
}
.image-mask span {
display: block;
cursor: pointer;
margin: 40% auto;
}
.image-mask .el-upload-list__item-delete {
position: static;
font-size: inherit;
color: inherit;
}
.add {
padding-left: 20px;
}
.delete {
padding-right: 20px;
}
// .el-icon-delete {
// z-index: 9999999;
// position: absolute;
// bottom: 70px;
// right: 10px;
// color: red;
// }
</style>
@@ -0,0 +1,374 @@
<template>
<div>
<div v-loading="loading" element-loading-text="图片上传中" customClass >
<div style="display: block;overflow: hidden;max-width: 655px;padding-top: 25px;">
<draggable v-model="locaImg" class="list-group" tag="ul" v-bind="dragOptions" @start="drag = true"
@end="drag = false">
<div v-for="(item, index) in locaImg" :key="index" class="image-list" style="float: left;">
<img class="el-upload-list__item-thumbnail image-detail" @load="imgLoad(item)" :src="item.imgLocalUrl||domain + item.comicsPic" alt="">
<!-- 遮罩层 -->
<a href="#">
<div class="image-mask">
<span class="add" @click="handlePictureCardPreview(item.imgLocalUrl||domain + item.comicsPic)">
<i class="el-icon-view"></i>
</span>
<span class="delete" @click="removeImage(index)">
<i class="el-icon-delete"></i>
</span>
</div>
</a>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="">
</el-dialog>
<el-input v-model.number="item.high" placeholder="高" style="width: 70px" />
<el-input v-model.number="item.width" placeholder="宽" style="width: 70px" />
</div>
</draggable>
</div>
<div style="display: block;overflow: hidden;max-width: 655px;">
<el-upload action="/api/web/file/uploadImg" ref="upload" :httpRequest="httpRequest" :show-file-list="false"
:auto-upload="false" list-type="text" :on-preview="handlePictureCardPreview" :on-change="handleChange"
multiple :limit='10000'>
<!-- <i class="el-icon-plus"></i> -->
<el-button slot="trigger" icon="el-icon-folder-opened" size="small" style="font-weight: bold;" type="primary">选取图片</el-button>
<el-button size="small" icon="el-icon-delete" style="margin-left: 10px;font-weight: bold;" @click="clearFiles" type="primary">清空选择图片</el-button>
<el-button size="small" icon="el-icon-delete" style="margin-left: 10px;font-weight: bold;" @click="clearUploadFiles" type="primary">清空上传图片
</el-button>
<el-button style="margin-left: 10px;font-weight: bold;" size="small" icon="el-icon-upload" type="primary" @click="submitUpload">上传图片</el-button>
</el-upload>
</div>
</div>
<div style="display: block;overflow: hidden;width: 655px;margin-top: 45px;">
<el-table :data="fileList" empty-text="暂无选择图片" :header-row-style="{ 'height': '30px !important' }"
header-cell-class-name="uploadHeader" height="200" size="mini" style="width: 100%">
<el-table-column prop="name" :label="`已选择图片(${fileList.length}张)`">
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import draggable from 'vuedraggable'
import axios from 'axios'
const CancelToken = axios.CancelToken
export const source = CancelToken.source()
import { upLoadFile } from '@/api/common'
export default {
components: { draggable },
data() {
const source = axios.CancelToken.source()
return {
domain: this.$store.getters.getSystemConfig.webImgReq, // 播放视频的域名
returnURL: [],
dialogImageUrl: '',
dialogVisible: false,
fileList: [],
drag: false, // 拖拽效果是否开启
locaImg: [],
loading: false,
source
}
},
props: {
img: {
type: Array,
default: function () {
return []
},
},
},
computed: {
dragOptions() {
return {
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost',
}
},
},
watch: {
// 监听拖动事件改变时,传出新的图片数组
drag() {
this.changeImgUrl()
},
img(New, old) {
this.locaImg = []
New.filter((item) => {
this.locaImg.push(item)
})
},
// locaImg: {
// handler: function (val, oldVal) {
// if (this.fileList.length !== 0 && val.length === this.fileList.length) {
// this.setIsUploadIng(false)
// }
// },
// deep: true
// }
},
mounted() {
this.locaImg = []
if (this.img) {
this.img.filter((item) => {
this.locaImg.push(item)
})
}
},
beforeDestroy() {
this.source.cancel('组件关闭取消所有请求')
this.source = axios.CancelToken.source()
},
methods: {
handleChange(file, fileList) {
this.fileList = fileList
},
imgLoad(item) {
// if (item.imgLocalUrl) {
// URL.revokeObjectURL(item.imgLocalUrl)
// }
},
clearFiles() {
if (this.$refs.upload && this.$refs.upload.clearFiles) {
this.$refs.upload.clearFiles()
}
this.fileList = []
},
clearUploadFiles() {
this.locaImg = []
this.changeImgUrl()
},
httpRequest() {
return false
},
async upLoad() {
this.loading = true
this.setIsUploadIng(true)
let isUploadFail = false
const filePathPromises = this.fileList.map(async item => {
try {
const fd = new FormData()
fd.append('file', item.raw)
fd.append('filename', item.name)
const response = await upLoadFile(fd, this.source)
response.data.imgLocalUrl = URL.createObjectURL(item.raw)
return response.data
} catch (error) {
isUploadFail = true
if (axios.isCancel(error)) {
console.log('CancelToken canceled', error.message)
} else {
this.source.cancel('有任意一个图片上传错误,取消所有请求')
this.source = axios.CancelToken.source()
this.$notify.error({
title: '上传失败',
message: '请您重新上传图片!',
duration: 0
})
}
this.loading = false
this.locaImg = []
}
})
for (const filePathPromise of filePathPromises) {
try {
const res = await filePathPromise
if (res && res.path) {
this.locaImg.push({
high: undefined,
width: undefined,
comicsPic: res.path,
imgLocalUrl: res.imgLocalUrl
})
} else {
isUploadFail = true
throw new Error('locaImg.push阶段错误,请求无返回数据')
}
} catch (error) {
isUploadFail = true
this.loading = false
this.locaImg = []
console.log(error)
}
}
!isUploadFail && this.clearFiles()
if (isUploadFail) {
this.locaImg = []
}
this.loading = false
this.setIsUploadIng(false)
this.changeImgUrl()
},
submitUpload() {
this.upLoad()
},
// 移除图片
removeImage(val) {
this.locaImg.splice(val, 1)
this.changeImgUrl()
},
// 预览
handlePictureCardPreview(url) {
this.dialogImageUrl = url
this.dialogVisible = true
},
changeImgUrl(arr) {
this.$emit('changeImgUrl', this.locaImg)
},
setIsUploadIng(status) {
this.$emit('setIsUploadIng', status)
}
}
}
</script>
<style lang="scss" scoped>
.list-group {
padding: 0px;
margin: 0px;
}
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 148px;
height: 148px;
line-height: 148px;
text-align: center;
}
.avatar {
width: 148px;
height: 148px;
display: block;
}
.image-list {
overflow: hidden;
background-color: #fff;
border: 1px solid #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: 148px;
height: 202px;
margin: 0 8px 8px 0;
display: inline-block;
position: relative;
img {
width: 148px;
height: 148px;
}
}
.image-list a:hover .image-mask {
opacity: 1;
}
.el-icon-check {
color: #fff;
}
.input-box {
width: 80%;
}
.el-upload--picture-card {
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: 148px;
height: 148px;
line-height: 146px;
vertical-align: top;
}
.image-mask {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
text-align: center;
color: #fff;
opacity: 0;
font-size: 20px;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s;
display: flex;
flex-direction: row;
}
.image-detail {
width: 100%;
height: 100%;
position: relative;
}
.image-mask::after {
display: inline-block;
content: '';
height: 100%;
vertical-align: middle;
}
.image-mask span {
display: block;
cursor: pointer;
margin: 40% auto;
}
.image-mask .el-upload-list__item-delete {
position: static;
font-size: inherit;
color: inherit;
}
.add {
padding-left: 20px;
}
.delete {
padding-right: 20px;
}
// .el-icon-delete {
// z-index: 9999999;
// position: absolute;
// bottom: 70px;
// right: 10px;
// color: red;
// }
// .el-table .uploadHeader{
// background-color: #d9d9d9 !important;
// }
</style>
<style lang="scss" >
.el-table .uploadHeader {
background-color: #666 !important;
padding: 0;
}
.el-table .uploadHeader+th {
padding: 0;
background-color: #666 !important;
border-bottom: 1px solid #dfdfdf !important;
}
</style>
+177
View File
@@ -0,0 +1,177 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-09-29 19:56:44
* @LastEditors: Please set LastEditors
* @LastEditTime: 2022-06-21 20:05:54
-->
<template>
<!-- 使用方法 <UploadImg @changeImgUrl='changeMobileAvatar' :img='form.mobileAvatar' :width='"100px"' :height='"100px"'></UploadImg>
changeMobileAvatar(data) {
this.form.mobileAvatar = data
}, -->
<el-upload class="avatar-uploader" action="/api/web/file/uploadImg" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<div ref="uploader">
<img v-if="imageUrl" :src="domain + imageUrl" class="avatar" />
<i v-if="imageUrl" @click.stop="handleRemove" class="el-icon-delete"></i>
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</div>
</el-upload>
</template>
<script>
export default {
data() {
return {
domain: this.$store.getters.getSystemConfig.webImgReq,
imageUrl: ''
}
},
props: {
img: {
type: String,
default: ''
},
width: {
type: String,
default: '180px'
},
height: {
type: String,
default: '180px'
}
},
watch: {
img: function(val, oldval) {
this.imageUrl = val
}
},
mounted() {
this.$refs['uploader'].style.width = this.width
this.$refs['uploader'].style.height = this.height
this.$refs['uploader'].style.lineHeight = this.height
if (this.img) {
this.imageUrl = this.img
} else {
this.imageUrl = ''
}
},
methods: {
handleAvatarSuccess(res, file) {
if (res.code === 200) {
this.$message({
type: 'success',
message: '上传成功!'
})
this.imageUrl = res.data.path
this.changeImgUrl()
} else {
this.$message({
type: 'error',
message: '上传失败!'
})
this.imageUrl = ''
}
},
changeImgUrl() {
this.$emit('changeImgUrl', this.imageUrl)
},
handleRemove(f) {
this.imageUrl = ''
this.$emit('onRemove', f)
},
// 上传前进行压缩
beforeAvatarUpload(file) {
const isJPG =
file.type === 'image/jpeg' ||
file.type === 'image/png' ||
file.type === 'image/gif' ||
file.type === 'image/webp'
// const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
this.$message({
type: 'error',
message: '上传图片只能是 JPG 格式!!'
})
}
// if (!isLt2M) {
// this.$message({
// type: 'error',
// message: '上传图片大小不能超过 2MB!!'
// })
// }
return isJPG
},
// 图片等比压缩
handleCompressImage(file, type) {
return new Promise((resolve, reject) => {
try {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (e) => {
const image = new Image()
image.src = e.target.result
image.onload = () => {
if (image.width > 720) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const imageWidth = 720
const imageHeight = (720 / image.width) * image.height
canvas.width = imageWidth
canvas.height = imageHeight
context.drawImage(image, 0, 0, imageWidth, imageHeight)
canvas.toBlob(
(blob) => {
resolve(new Blob([blob], { type: type }))
},
'image/jpeg',
0.9
)
} else {
resolve(file)
}
}
}
} catch (e) {
reject(e)
}
})
}
}
}
</script>
<style scoped>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100%;
height: 100%;
line-height: 100%;
text-align: center;
}
.avatar {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
display: block;
}
.el-icon-delete {
z-index: 9999999;
position: absolute;
bottom: 10px;
right: 10px;
color: red;
}
</style>
+217
View File
@@ -0,0 +1,217 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-09-29 19:56:44
* @LastEditors: Please set LastEditors
* @LastEditTime: 2022-03-29 15:36:11
-->
<template>
<div>
<draggable v-model="locaImg" class="list-group" tag="ul" v-bind="dragOptions" @start="drag = true" @end="drag = false">
<div v-for="(img,index) in locaImg" :key="index" class="image-list" style="float: left;">
<img class="el-upload-list__item-thumbnail image-detail" :src="domain+img" alt="">
<!-- 遮罩层 -->
<a href="#">
<div class="image-mask">
<span class="add" @click="handlePictureCardPreview(domain+img)">
<i class="el-icon-view"></i>
</span>
<span class="delete" @click="removeImage(index)">
<i class="el-icon-delete"></i>
</span>
</div>
</a>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="">
</el-dialog>
</div>
</draggable>
<el-upload action="/api/web/file/uploadImg" :show-file-list="false" list-type="picture-card" :file-list="fileList" :on-preview="handlePictureCardPreview" :on-change="handleChange" multiple :limit='100'>
<i class="el-icon-plus"></i>
</el-upload>
</div>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: { draggable },
data() {
return {
domain: this.$store.getters.getSystemConfig.webImgReq, // 播放视频的域名
returnURL: [],
dialogImageUrl: '',
dialogVisible: false,
fileList: [
// { name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' },
// { name: 'food2.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' }
],
drag: false, // 拖拽效果是否开启
locaImg: []
}
},
props: {
img: {
type: Array,
default: function() {
return []
}
}
},
computed: {
dragOptions() {
return {
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost'
}
}
},
watch: {
// 监听拖动事件改变时,传出新的图片数组
drag() {
this.changeImgUrl()
},
img(New, old) {
this.locaImg = []
New.filter((item) => {
this.locaImg.push(item)
})
}
},
mounted() {
this.locaImg = []
if (this.img) {
this.img.filter((item) => {
this.locaImg.push(item)
})
}
},
methods: {
handleChange(file, fileList) {
if (file.status === 'success') {
this.locaImg.push(file.response.data.path)
this.changeImgUrl()
}
},
// 移除图片
removeImage(val) {
this.locaImg.splice(val, 1)
this.changeImgUrl()
},
// 预览
handlePictureCardPreview(url) {
this.dialogImageUrl = url
this.dialogVisible = true
},
changeImgUrl(arr) {
this.$emit('changeImgUrl', this.locaImg)
}
}
}
</script>
<style lang="scss" scoped>
.list-group {
padding: 0px;
margin: 0px;
}
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 148px;
height: 148px;
line-height: 148px;
text-align: center;
}
.avatar {
width: 148px;
height: 148px;
display: block;
}
.image-list {
overflow: hidden;
background-color: #fff;
border: 1px solid #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: 148px;
height: 148px;
margin: 0 8px 8px 0;
display: inline-block;
position: relative;
}
.image-list a:hover .image-mask {
opacity: 1;
}
.el-icon-check {
color: #fff;
}
.input-box {
width: 80%;
}
.el-upload--picture-card {
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: 148px;
height: 148px;
line-height: 146px;
vertical-align: top;
}
.image-mask {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
text-align: center;
color: #fff;
opacity: 0;
font-size: 20px;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s;
display: flex;
flex-direction: row;
}
.image-detail {
width: 100%;
height: 100%;
position: relative;
}
.image-mask::after {
display: inline-block;
content: '';
height: 100%;
vertical-align: middle;
}
.image-mask span {
display: block;
cursor: pointer;
margin: 40% auto;
}
.image-mask .el-upload-list__item-delete {
position: static;
font-size: inherit;
color: inherit;
}
.add {
padding-left: 20px;
}
.delete {
padding-right: 20px;
}
</style>
+240
View File
@@ -0,0 +1,240 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-09-29 19:56:44
* @LastEditors: Please set LastEditors
* @LastEditTime: 2022-03-29 15:36:11
-->
<template>
<div>
<draggable v-model="locaImg" class="list-group" tag="ul" v-bind="dragOptions" @start="drag = true" @end="drag = false">
<div v-for="(img, index) in locaImg" :key="index" class="image-list" style="float: left">
<img class="el-upload-list__item-thumbnail image-detail" :src="domain + img.image" alt="" />
<!-- 遮罩层 -->
<a href="#">
<div class="image-mask">
<span class="add" @click="handlePictureCardPreview(domain + img.image)">
<i class="el-icon-view"></i>
</span>
<span class="delete" @click="removeImage(index)">
<i class="el-icon-delete"></i>
</span>
</div>
</a>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
<div class="input"><el-input v-model="locaImg[index].url" @input="changeImgUrl" :placeholder="`请输入第${index + 1}张图片跳转url`"></el-input></div>
</div>
</draggable>
<el-upload
action="/api/web/file/uploadImg"
:show-file-list="false"
list-type="picture-card"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:on-change="handleChange"
multiple
:limit="100">
<i class="el-icon-plus"></i>
</el-upload>
</div>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: { draggable },
data() {
return {
domain: this.$store.getters.getSystemConfig.webImgReq, // 播放视频的域名
returnURL: [],
dialogImageUrl: '',
dialogVisible: false,
fileList: [
// { name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' },
// { name: 'food2.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' }
],
drag: false, // 拖拽效果是否开启
locaImg: []
}
},
props: {
img: {
type: Array,
default: function() {
return []
}
}
},
computed: {
dragOptions() {
return {
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost'
}
}
},
watch: {
// 监听拖动事件改变时,传出新的图片数组
drag() {
this.changeImgUrl()
},
img(New, old) {
this.locaImg = []
New.filter(item => {
this.locaImg.push({
image: item.image,
url: item.url
})
})
}
},
mounted() {
this.locaImg = []
if (this.img) {
this.img.filter(item => {
this.locaImg.push({
image: item.image,
url: item.url
})
})
}
},
methods: {
handleChange(file, fileList) {
if (file.status === 'success') {
this.locaImg.push({ image: file.response.data.path, url: '' })
this.changeImgUrl()
}
},
// 移除图片
removeImage(val) {
this.locaImg.splice(val, 1)
this.changeImgUrl()
},
// 预览
handlePictureCardPreview(url) {
this.dialogImageUrl = url
this.dialogVisible = true
},
changeImgUrl() {
console.log(this.locaImg)
this.$emit('changeImgUrl', this.locaImg)
}
}
}
</script>
<style lang="scss" scoped>
.list-group {
padding: 0px;
margin: 0px;
}
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 148px;
height: 148px;
line-height: 148px;
text-align: center;
}
.avatar {
width: 148px;
height: 148px;
display: block;
}
.image-list {
background-color: #fff;
border: 1px solid #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: 148px;
height: 148px;
margin: 0 8px 8px 0;
display: inline-block;
position: relative;
margin-bottom: 50px;
.input {
height: 30px;
width: 250px;
position: absolute;
left: 0;
bottom: -35px;
}
}
.image-list a:hover .image-mask {
opacity: 1;
}
.el-icon-check {
color: #fff;
}
.input-box {
width: 80%;
}
.el-upload--picture-card {
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: 148px;
height: 148px;
line-height: 146px;
vertical-align: top;
}
.image-mask {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
text-align: center;
color: #fff;
opacity: 0;
font-size: 20px;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s;
display: flex;
flex-direction: row;
}
.image-detail {
width: 100%;
height: 100%;
position: relative;
}
.image-mask::after {
display: inline-block;
content: '';
height: 100%;
vertical-align: middle;
}
.image-mask span {
display: block;
cursor: pointer;
margin: 40% auto;
}
.image-mask .el-upload-list__item-delete {
position: static;
font-size: inherit;
color: inherit;
}
.add {
padding-left: 20px;
}
.delete {
padding-right: 20px;
}
</style>
+192
View File
@@ -0,0 +1,192 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-09-29 19:56:44
* @LastEditors: 王涛
* @LastEditTime: 2021-01-07 22:22:03
-->
<template>
<div class="box">
<div v-on:paste="handlePaste" v-drag="handleDrag" >
<img v-if='imageUrl' ref="preview" :src="domain+imageUrl" alt="" style="width:300px;height:300px;">
<div v-else style="width:300px;height:300px;border:1px solid;padding:20px;">
<div style="text-indent:2em;">将图片按<span style="color:red">cmd+C复制</span>到粘贴板<span style="color:red">cmd+V粘贴</span>至此处</div>
<div style="text-indent:2em;">或者将图片拖拽至此处</div>
</div>
</div>
</div>
</template>
<script>
import { mediaUploadImg } from '@/api/components/UploadImgPaste'
export default {
data() {
return {
domain: this.$store.getters.getSystemConfig.webImgReq, // 存放图片的域名
imageUrl: ''
}
},
props: {
img: {
type: String,
default: ''
}
},
watch: {
img: function(val) {
this.imageUrl = val
}
},
mounted() {
if (this.img) {
this.imageUrl = this.img
} else {
this.imageUrl = ''
}
},
methods: {
handleDrag(event) {
const items = Array.from(event.dataTransfer.items)
let file = null
if (!items || items.length === 0) {
this.$message.error('当前浏览器不支持本地')
return
}
// 搜索剪切板items
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
if (!file) {
this.$message.error('粘贴内容非图片')
return
}
// 此时file就是我们的剪切板中的图片对象
// 如果需要预览,可以执行下面代码
this.file = file
this.uploadPlans()
},
// 图片等比压缩
handleCompressImage(file, type) {
return new Promise((resolve, reject) => {
try {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (e) => {
const image = new Image()
image.src = e.target.result
image.onload = () => {
if (image.width > 720) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const imageWidth = 720
const imageHeight = (720 / image.width) * image.height
canvas.width = imageWidth
canvas.height = imageHeight
console.log(imageWidth, imageHeight)
context.drawImage(image, 0, 0, imageWidth, imageHeight)
canvas.toBlob(
(blob) => {
resolve(new Blob([blob], { type: type }))
},
'image/jpeg',
0.9
)
} else {
resolve(file)
}
}
}
} catch (e) {
reject(e)
}
})
},
async handlePaste(event) {
const items = (event.clipboardData || window.clipboardData).items
let file = null
if (!items || items.length === 0) {
this.$message.error('当前浏览器不支持本地')
return
}
// 搜索剪切板items
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
if (!file) {
this.$message.error('粘贴内容非图片')
return
}
// 此时file就是我们的剪切板中的图片对象
// 如果需要预览,可以执行下面代码
// 先进行压缩再上传
// const res = await this.handleCompressImage(file)
// this.file = res
this.file = file
this.uploadPlans()
},
// 上传文件成功后回调
uploadPlans() {
const file = this.file
if (!file) {
this.$message.error('请粘贴图片后上传')
return
}
this.loading = true
const form = new FormData()
form.append('file', file)
form.append('type', this.type)
mediaUploadImg(form).then((res) => {
if (res.code === 200) {
this.$message({
type: 'success',
message: '上传成功!'
})
this.imageUrl = res.data.path
this.changeImgUrl()
} else {
this.$message({
type: 'error',
message: '上传失败!'
})
this.imageUrl = ''
}
})
},
changeImgUrl() {
this.$emit('changeImgUrl', this.imageUrl)
}
}
}
</script>
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 148px;
height: 148px;
line-height: 148px;
text-align: center;
}
.avatar {
width: 148px;
height: 148px;
display: block;
}
</style>
+108
View File
@@ -0,0 +1,108 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-09-30 11:10:55
* @LastEditors: 江涛
* @LastEditTime: 2021-03-24 11:39:53
-->
<template>
<uploader
:options="options"
class="uploader-example"
@file-success="fileSuccess"
@file-added="fileAdd">
<uploader-unsupport></uploader-unsupport>
<uploader-drop>
<uploader-btn>选择文件</uploader-btn>
</uploader-drop>
<uploader-list></uploader-list>
</uploader>
</template>
<script>
// import { resumeUploadIsSuccess } from '@/api/video/video'
import { videoOk } from '@/api/video.js'
import { getToken } from '@/utils/auth'
export default {
props: {
type: {
type: String,
default: ''
}
},
data() {
return {
// 视频校验唯一码
trackId: '',
duration: 0
}
},
computed: {
options() {
return {
target: '/api/web/mediafile/videoBody',
method: 'application/json',
chunkSize: 2 * 1024 * 1024,
testChunks: false,
forceChunkSize: true,
maxChunkRetries: 3,
singleFile: true,
query: this.uploadQuery,
headers: {
Authorization: getToken()
}
}
}
},
methods: {
uploadQuery(file, chunk) {
return {
pos: chunk.offset,
taskId: this.trackId,
type: 1,
data: 'flie'
}
},
/**
* 上传视频-点击上传视频
*/
async fileSuccess(rootFile, file, message, chunk) {
const video = document.createElement('video')
video.preload = 'metadata'
video.src = URL.createObjectURL(rootFile.file)
video.onloadedmetadata = () => {
window.URL.revokeObjectURL(video.src)
// eslint-disable-next-line no-const-assign
this.duration = video.duration
}
const data = {
id: this.trackId,
videoUrl: JSON.parse(message).data.videoUri
}
videoOk(data).then((res) => {
if (res.code === 200) {
const param = {
id:video.id,
width: video.videoWidth,
height: video.videoHeight,
duration: this.duration,
videoUri: JSON.parse(message).data.videoUri,
size: file.size
}
this.$emit('changeVideoUrl', param)
} else {
this.$message({
type: 'error',
message: '视频上传失败!'
})
}
})
},
fileAdd() {
this.trackId = new Date().getTime()+''
}
}
}
</script>
+144
View File
@@ -0,0 +1,144 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-09-30 11:10:55
* @LastEditors: 江涛
* @LastEditTime: 2021-03-24 11:39:53
-->
<template>
<uploader :options="options" class="uploader-example" @file-success="fileSuccess" @file-added="fileAdd">
<uploader-unsupport></uploader-unsupport>
<uploader-drop>
<uploader-btn>选择文件</uploader-btn>
</uploader-drop>
<uploader-list></uploader-list>
</uploader>
</template>
<script>
// import { resumeUploadIsSuccess } from '@/api/video/video'
import { videoOk } from '@/api/video.js';
import { getToken } from '@/utils/auth';
import SparkMD5 from 'spark-md5';
export default {
props: {
type: {
type: String,
default: '',
},
},
data() {
return {
// 视频校验唯一码
trackId: '',
duration: 0,
};
},
computed: {
options() {
return {
target: '/api/web/mediafile/videoBody',
method: 'application/json',
chunkSize: 2 * 1024 * 1024,
testChunks: false,
forceChunkSize: true,
maxChunkRetries: 3,
singleFile: true,
query: this.uploadQuery,
headers: {
Authorization: getToken(),
},
};
},
},
methods: {
uploadQuery(file, chunk) {
return {
pos: chunk.offset,
taskId: this.trackId,
type: this.type,
data: 'file',
};
},
/**
* 上传视频-点击上传视频
*/
async fileSuccess(rootFile, file, message, chunk) {
const video = document.createElement('video');
video.preload = 'metadata';
video.src = URL.createObjectURL(rootFile.file);
video.onloadedmetadata = () => {
window.URL.revokeObjectURL(video.src);
// eslint-disable-next-line no-const-assign
this.duration = video.duration;
};
if (JSON.parse(message).code !== 200) {
alert(`任务id:${this.trackId}上传出错!`)
}
const data = {
id: JSON.parse(message).data.id,
videoUrl: JSON.parse(message).data.videoUri,
};
videoOk(data).then(res => {
if (res.code === 200) {
const param = {
id: data.id,
width: video.videoWidth,
height: video.videoHeight,
duration: this.duration,
videoUri: JSON.parse(message).data.videoUri,
size: file.size,
};
this.$emit('changeVideoUrl', param);
} else {
this.$message({
type: 'error',
message: '视频上传失败!',
});
}
});
},
async fileAdd(file) {
const fileReader = new FileReader();
const time = new Date().getTime();
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 10 * 1024 * 1000;
const chunks = Math.ceil(file.size / chunkSize);
const spark = new SparkMD5.ArrayBuffer();
file.pause();
loadNext();
fileReader.onload = e => {
if (this.type == 4 && file.fileType.indexOf('audio') == -1) {
file.cancel();
return this.$message.error('此视频归类仅可上传MP3格式内容!');
}
spark.append(e.target.result);
if (currentChunk < chunks) {
currentChunk++;
loadNext();
console.log('校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%');
} else {
this.trackId = spark.end();
this.$message('开始上传,请等待');
file.resume();
console.log(`MD5计算完毕:${file.name} \nMD5${spark} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
}
};
fileReader.onerror = function () {
this.$message(`文件${file.name}读取出错,请检查该文件`);
file.cancel();
};
function loadNext() {
const start = currentChunk * chunkSize;
const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
}
},
},
};
</script>
+45
View File
@@ -0,0 +1,45 @@
<!--
* @Author: 王涛
* @Mail: no
* @Date: 2020-09-30 11:10:55
* @LastEditors: 江涛
* @LastEditTime: 2021-03-03 20:26:11
-->
<template>
<el-upload class="upload-demo" action="/api/web/media/uploadImg" :on-success='success' :file-list="fileList">
<el-button size="mini" type="primary">点击上传</el-button>
</el-upload>
</template>
<script>
export default {
data() {
return {
fileList: []
}
},
methods: {
success(response, file, fileList) {
if (response.code === 200) {
const param = {
width: undefined,
height: undefined,
duration: undefined,
videoUri: response.data.path,
size: undefined
}
this.$message({
type: 'success',
message: '视频上传成功!'
})
this.$emit('changeVideoUrl', param)
} else {
this.$message({
type: 'error',
message: '视频上传失败!'
})
}
}
}
}
</script>
+140
View File
@@ -0,0 +1,140 @@
<template>
<div id="dplayer" ref="dplayer"></div>
</template>
<script>
import DPlayer from 'dplayer'
import Hls from 'hls.js';
window.Hls = Hls;
export default {
props: {
autoplay: {
type: Boolean,
default: false
},
theme: { // 主题色
type: String,
default: '#FADFA3'
},
loop: {
type: Boolean,
default: true
},
lang: {
type: String,
default: 'zh'
},
screenshot: { // 是否开启截图
type: Boolean,
default: false
},
hotkey: { // 是否开启热键
type: Boolean,
default: true
},
preload: {
type: String,
default: 'auto'
},
contextmenu: { // 自定义右键菜单
type: Array,
default: () => []
},
logo: {
type: String,
default: ''
},
video: {
type: Object, // 可选值:‘auto,'hls','flv','dash','webtorrent','normal'
required: true,
validator(value) {
return typeof value.url === 'string'
}
},
option: {
type: Object,
require: false,
default() {
return {}
}
}
},
data() {
return {
dp: null
}
},
watch: {
video(val) {
if (this.dp) {
this.dp.switchVideo(val) // 切换到其他视频
}
}
},
beforeDestroy() {
if (this.dp) {
this.dp.destroy()
}
},
mounted() {
const player = (this.dp = new DPlayer({
...{
container: this.$refs.dplayer,
autoplay: false,
theme: this.theme,
loop: this.loop,
lang: this.lang,
screenshot: this.screenshot,
hotkey: this.hotkey,
preload: this.preload,
contextmenu: this.contextmenu,
logo: this.logo,
video: {
url: this.video.url,
pic: this.video.pic,
type: this.video.type
}
},
...this.option
}))
player.video.muted = true
player.on('play', () => {
this.$emit('play')
})
player.on('playing', () => {
this.$emit('playing')
})
player.on('pause', () => {
this.$emit('pause')
})
player.on('canplay', () => {
this.$emit('canplay')
})
player.on('ended', () => {
this.$emit('ended')
})
player.on('error', () => {
this.$emit('error')
})
player.on('loadeddata', () => {
this.$emit('loadeddata')
if (this.autoplay) {
player.play()
player.video.muted = false
}
})
player.on('loadedmetadata', () => {
this.$emit('loadedmetadata')
})
player.on('waiting', () => {
this.$emit('waiting')
})
}
}
</script>
<style lang="scss" scoped>
#dplayer {
width: 100%;
height: 100%;
}
</style>
+26
View File
@@ -0,0 +1,26 @@
export function jumptabChange(val) {
switch (val) {
case 1:
return '漫画'
case 2:
return '动漫'
case 3:
return '视频'
}
}
export function jumptabChangeList() {
return [
{
value: 1,
label: '漫画'
},
{
value: 2,
label: '动漫'
},
{
value: 3,
label: '视频'
}
]
}