Prechádzať zdrojové kódy

知识中心-知识分类,修复视频上传异常问题

jiapu 2 týždňov pred
rodič
commit
ce7fa27bb6
2 zmenil súbory, kde vykonal 233 pridanie a 8 odobranie
  1. 4 1
      cfc-web/src/main.js
  2. 229 7
      cfc-web/src/views/admin/ArticleEdit.vue

+ 4 - 1
cfc-web/src/main.js

@@ -10,7 +10,7 @@ import {
   Menu, Submenu, MenuItem, Empty, Drawer, Timeline, TimelineItem,
   Progress, Container, Header, Aside, Main, Avatar, Scrollbar, Divider,
   Tab, Badge, Tree, ColorPicker, Tooltip, Popover,
-  Slider, InputNumber, Link,
+  Slider, InputNumber, Link, Rate, Loading, Alert,
   MessageBox, Message
 } from 'element-ui'
 import 'element-ui/lib/theme-chalk/index.css'
@@ -70,6 +70,9 @@ Vue.use(Popover)
 Vue.use(Slider)
 Vue.use(InputNumber)
 Vue.use(Link)
+Vue.use(Rate)
+Vue.use(Loading)
+Vue.use(Alert)
 // MessageBox 没有 install 方法,Vue.use(MessageBox) ——这是错误的,不会注册 $confirm
 // 需要手动挂载到 prototype 上
 Vue.prototype.$confirm = MessageBox.confirm

+ 229 - 7
cfc-web/src/views/admin/ArticleEdit.vue

@@ -139,6 +139,7 @@ import {
 import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight.js'
 import { getTagList } from '@/api/dimension.js'
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+import { DomEditor, SlateTransforms, SlatePath } from '@wangeditor/editor'
 import '@wangeditor/editor/dist/css/style.css'
 
 export default {
@@ -257,6 +258,11 @@ export default {
         const baseURL = process.env.VUE_APP_BASE_API || ''
         const token = localStorage.getItem('token')
         this.editorConfig = {
+          hoverbarKeys: {
+            video: {
+              menuKeys: []
+            }
+          },
           MENU_CONF: {
             uploadImage: {
               server: baseURL + '/api/admin/articles/upload/image',
@@ -310,13 +316,14 @@ onError: (file, err, res) => {
       },
     handleEditorCreated(editor) {
       this.editorInstance = editor
-      this.bindVideoFullscreen(editor)
+      this.bindVideoInteractions(editor)
     },
-    bindVideoFullscreen(editor) {
+    bindVideoInteractions(editor) {
       const root = (editor && editor.getEditableContainer && editor.getEditableContainer())
         || (this.$el && this.$el.querySelector('.w-e-text-container'))
       if (!root || this._videoFsBound) return
       this._videoFsBound = true
+      this._videoEditorRoot = root
       this._onVideoDblClick = (e) => {
         const video = this.findEventVideo(e)
         if (!video) return
@@ -324,9 +331,11 @@ onError: (file, err, res) => {
         this.enterVideoFullscreen(video)
       }
       this._onVideoFsClick = (e) => {
-        const box = e.target && e.target.closest && (
-          e.target.closest('.w-e-textarea-video-container') || e.target.closest('[data-w-e-type="video"]')
-        )
+        if (this._justDraggedVideo) {
+          this._justDraggedVideo = false
+          return
+        }
+        const box = this.findVideoBox(e.target)
         if (!box) return
         const rect = box.getBoundingClientRect()
         const x = e.clientX - rect.left
@@ -338,8 +347,24 @@ onError: (file, err, res) => {
         e.stopPropagation()
         this.enterVideoFullscreen(video)
       }
+      this._onVideoMouseDown = (e) => {
+        this.onVideoDragMouseDown(editor, e)
+      }
+      this._onVideoMouseMove = (e) => {
+        this.onVideoDragMouseMove(editor, e)
+      }
+      this._onVideoMouseUp = (e) => {
+        this.onVideoDragMouseUp(editor, e)
+      }
       root.addEventListener('dblclick', this._onVideoDblClick)
       root.addEventListener('click', this._onVideoFsClick, true)
+      root.addEventListener('mousedown', this._onVideoMouseDown, true)
+      document.addEventListener('mousemove', this._onVideoMouseMove)
+      document.addEventListener('mouseup', this._onVideoMouseUp)
+    },
+    findVideoBox(el) {
+      if (!el || !el.closest) return null
+      return el.closest('.w-e-textarea-video-container') || el.closest('[data-w-e-type="video"]')
     },
     findEventVideo(e) {
       const t = e && e.target
@@ -347,16 +372,189 @@ onError: (file, err, res) => {
       if (t.tagName === 'VIDEO') return t
       return t.closest ? t.closest('video') : null
     },
+    isVideoDragIgnorePoint(box, clientX, clientY) {
+      const rect = box.getBoundingClientRect()
+      const x = clientX - rect.left
+      const y = clientY - rect.top
+      if (x >= rect.width - 36 && y <= 36) return true
+      if (y > rect.height - 44) return true
+      return false
+    },
+    onVideoDragMouseDown(editor, e) {
+      if (this.readonly || (editor && editor.isDisabled && editor.isDisabled())) return
+      if (e.button !== 0) return
+      const box = this.findVideoBox(e.target)
+      if (!box) return
+      if (this.isVideoDragIgnorePoint(box, e.clientX, e.clientY)) return
+      this._videoDragState = {
+        startX: e.clientX,
+        startY: e.clientY,
+        box: box,
+        dragging: false
+      }
+    },
+    onVideoDragMouseMove(editor, e) {
+      const state = this._videoDragState
+      if (!state) return
+      const dx = e.clientX - state.startX
+      const dy = e.clientY - state.startY
+      if (!state.dragging && (dx * dx + dy * dy) < 36) return
+      if (!state.dragging) {
+        state.dragging = true
+        state.box.classList.add('is-video-dragging')
+        const wrap = this.$el && this.$el.querySelector('.rich-editor')
+        if (wrap) wrap.classList.add('is-dragging-video')
+      }
+      e.preventDefault()
+      this.updateVideoDropIndicator(editor, e)
+    },
+    onVideoDragMouseUp(editor, e) {
+      const state = this._videoDragState
+      this._videoDragState = null
+      const wrap = this.$el && this.$el.querySelector('.rich-editor')
+      if (wrap) wrap.classList.remove('is-dragging-video')
+      this.clearVideoDropIndicator()
+      if (!state) return
+      if (state.box) state.box.classList.remove('is-video-dragging')
+      if (!state.dragging) return
+      this._justDraggedVideo = true
+      this.moveVideoToEvent(editor, state.box, e)
+    },
+    updateVideoDropIndicator(editor, e) {
+      const root = this.getVideoScrollRoot()
+      if (!root) return
+      const info = this.getVideoDropInfo(editor, e)
+      if (!info) {
+        this.clearVideoDropIndicator()
+        return
+      }
+      let line = this._videoDropLine
+      if (!line) {
+        line = document.createElement('div')
+        line.className = 'article-video-drop-line'
+        root.appendChild(line)
+        this._videoDropLine = line
+      } else if (line.parentNode !== root) {
+        root.appendChild(line)
+      }
+      const rootRect = root.getBoundingClientRect()
+      line.style.top = (info.y - rootRect.top + root.scrollTop) + 'px'
+      line.style.display = 'block'
+    },
+    getVideoScrollRoot() {
+      const root = this._videoEditorRoot
+      if (!root) return null
+      return root.querySelector('.w-e-scroll') || root
+    },
+    clearVideoDropIndicator() {
+      if (this._videoDropLine) {
+        this._videoDropLine.style.display = 'none'
+      }
+    },
+    getVideoDropInfo(editor, e) {
+      if (!editor) return null
+      const root = this.getVideoScrollRoot()
+      if (!root) return null
+      const rootRect = root.getBoundingClientRect()
+      if (e.clientY < rootRect.top || e.clientY > rootRect.bottom) return null
+      let path = null
+      let targetDom = null
+      try {
+        const range = DomEditor.findEventRange(editor, e)
+        if (range && range.anchor && range.anchor.path) {
+          path = range.anchor.path
+        }
+      } catch (err) {}
+      if (!path) {
+        const hit = document.elementFromPoint(e.clientX, e.clientY)
+        let cur = hit
+        while (cur && cur !== root) {
+          try {
+            const node = DomEditor.toSlateNode(editor, cur)
+            if (node && node.type) {
+              path = DomEditor.findPath(editor, node)
+              targetDom = cur
+              break
+            }
+          } catch (err) {}
+          cur = cur.parentNode
+        }
+      }
+      if (!path || !path.length) {
+        const last = editor.children && editor.children.length ? editor.children[editor.children.length - 1] : null
+        if (!last) return { toPath: [0], y: rootRect.top + 8 }
+        try {
+          targetDom = editor.toDOMNode(last)
+        } catch (err) {
+          return { toPath: [editor.children.length], y: rootRect.bottom - 8 }
+        }
+        const rect = targetDom.getBoundingClientRect()
+        return { toPath: [editor.children.length], y: rect.bottom }
+      }
+      const topIndex = path[0]
+      const block = editor.children[topIndex]
+      if (!targetDom && block && editor.toDOMNode) {
+        try {
+          targetDom = editor.toDOMNode(block)
+        } catch (err) {}
+      }
+      if (!targetDom) return { toPath: [topIndex], y: e.clientY }
+      const rect = targetDom.getBoundingClientRect()
+      const insertAfter = e.clientY > rect.top + rect.height / 2
+      return {
+        toPath: [insertAfter ? topIndex + 1 : topIndex],
+        y: insertAfter ? rect.bottom : rect.top
+      }
+    },
+    moveVideoToEvent(editor, box, e) {
+      if (!editor || !box) return
+      let videoNode = null
+      try {
+        videoNode = DomEditor.toSlateNode(editor, box)
+      } catch (err) {
+        return
+      }
+      if (!videoNode || videoNode.type !== 'video') return
+      let fromPath = null
+      try {
+        fromPath = DomEditor.findPath(editor, videoNode)
+      } catch (err) {
+        return
+      }
+      const info = this.getVideoDropInfo(editor, e)
+      if (!info || !info.toPath) return
+      let toPath = info.toPath.slice()
+      const max = editor.children ? editor.children.length : 0
+      if (toPath[0] > max) toPath[0] = max
+      if (SlatePath.equals(fromPath, toPath)) return
+      if (fromPath.length === 1 && toPath.length === 1 && toPath[0] === fromPath[0] + 1) return
+      try {
+        SlateTransforms.moveNodes(editor, { at: fromPath, to: toPath })
+      } catch (err) {
+        console.error('移动视频失败', err)
+      }
+    },
     enterVideoFullscreen(video) {
       if (!video) return
       const req = video.requestFullscreen || video.webkitRequestFullscreen || video.msRequestFullscreen
       if (req) req.call(video)
     },
     unbindVideoFullscreen() {
-      const root = (this.editorInstance && this.editorInstance.getEditableContainer && this.editorInstance.getEditableContainer())
+      const root = this._videoEditorRoot
+        || (this.editorInstance && this.editorInstance.getEditableContainer && this.editorInstance.getEditableContainer())
         || (this.$el && this.$el.querySelector('.w-e-text-container'))
       if (root && this._onVideoDblClick) root.removeEventListener('dblclick', this._onVideoDblClick)
       if (root && this._onVideoFsClick) root.removeEventListener('click', this._onVideoFsClick, true)
+      if (root && this._onVideoMouseDown) root.removeEventListener('mousedown', this._onVideoMouseDown, true)
+      if (this._onVideoMouseMove) document.removeEventListener('mousemove', this._onVideoMouseMove)
+      if (this._onVideoMouseUp) document.removeEventListener('mouseup', this._onVideoMouseUp)
+      this.clearVideoDropIndicator()
+      if (this._videoDropLine && this._videoDropLine.parentNode) {
+        this._videoDropLine.parentNode.removeChild(this._videoDropLine)
+      }
+      this._videoDropLine = null
+      this._videoEditorRoot = null
+      this._videoDragState = null
       this._videoFsBound = false
     },
     async loadCategories() {
@@ -658,10 +856,14 @@ onError: (file, err, res) => {
   flex-wrap: wrap;
 }
 .rich-editor /deep/ .w-e-text-container {
+  position: relative;
   min-height: 400px;
   max-height: 600px;
   overflow-y: auto;
 }
+.rich-editor /deep/ .w-e-scroll {
+  position: relative;
+}
 .rich-editor /deep/ .w-e-textarea-video-container,
 .rich-editor /deep/ [data-w-e-type="video"] {
   position: relative;
@@ -676,6 +878,15 @@ onError: (file, err, res) => {
   overflow: hidden;
   line-height: 0;
   text-align: left;
+  cursor: grab;
+}
+.rich-editor /deep/ .w-e-textarea-video-container.is-video-dragging,
+.rich-editor /deep/ [data-w-e-type="video"].is-video-dragging {
+  opacity: 0.45;
+  cursor: grabbing;
+}
+.rich-editor.is-dragging-video {
+  cursor: grabbing;
 }
 .rich-editor /deep/ .w-e-textarea-video-container video,
 .rich-editor /deep/ [data-w-e-type="video"] video {
@@ -685,7 +896,18 @@ onError: (file, err, res) => {
   height: auto;
   background: #000;
   object-fit: contain;
-  cursor: zoom-in;
+  cursor: grab;
+}
+.rich-editor /deep/ .article-video-drop-line {
+  position: absolute;
+  left: 10px;
+  right: 10px;
+  height: 3px;
+  background: #409eff;
+  border-radius: 2px;
+  pointer-events: none;
+  z-index: 60;
+  display: none;
 }
 .rich-editor /deep/ .w-e-textarea-video-container::after,
 .rich-editor /deep/ [data-w-e-type="video"]::after {