-

+
%VITE_APP_TITLE%
diff --git a/package.json b/package.json
index cf22a3a..522068b 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,8 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
+ "@wangeditor/editor": "^5.1.23",
+ "@wangeditor/editor-for-vue": "^5.1.12",
"axios": "1.12.0",
"cron-parser": "^4.9",
"cropperjs": "^1.6.2",
diff --git a/src/components/piEditor/ext/FontSize.ts b/src/components/piEditor/ext/FontSize.ts
deleted file mode 100644
index 38444ef..0000000
--- a/src/components/piEditor/ext/FontSize.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {TextStyle} from '@tiptap/extension-text-style'
-
-export default TextStyle.extend({
- addAttributes() {
- return {
- fontSize: {
- default: null,
- parseHTML: element => element.style.fontSize || null,
- renderHTML: attributes => {
- if (!attributes.fontSize) return {}
- return {
- style: `font-size: ${attributes.fontSize}`,
- }
- },
- },
- }
- },
-})
diff --git a/src/components/piEditor/ext/Image/Action.vue b/src/components/piEditor/ext/Image/Action.vue
deleted file mode 100644
index a9de392..0000000
--- a/src/components/piEditor/ext/Image/Action.vue
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
-
![]()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 宽
-
-
-
-
- 高
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/piEditor/ext/Image/index.ts b/src/components/piEditor/ext/Image/index.ts
deleted file mode 100644
index 3804894..0000000
--- a/src/components/piEditor/ext/Image/index.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import Action from "./Action.vue"
-import {VueNodeViewRenderer} from "@tiptap/vue-3"
-import { Image } from '@tiptap/extension-image'
-
-export default Image.extend({
- addAttributes() {
- return {
- ...this.parent?.(),
- class: {
- default: null,
- },
- style: {
- default: null,
- },
- width: {
- default: null,
- },
- height: {
- default: null
- },
- alt: {
- default: null
- }
- }
- },
- addNodeView() {
- return VueNodeViewRenderer(Action)
- },
-})
diff --git a/src/components/piEditor/ext/Indent.ts b/src/components/piEditor/ext/Indent.ts
deleted file mode 100644
index 31d7a86..0000000
--- a/src/components/piEditor/ext/Indent.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import {Extension, Command} from '@tiptap/core'
-
-declare module '@tiptap/core' {
- interface Commands
{
- indent: {
- increaseIndent: () => ReturnType
- decreaseIndent: () => ReturnType
- }
- }
-}
-
-export default Extension.create({
- name: 'indent',
- addOptions() {
- return {
- maxIndent: 50,
- types: ['paragraph', 'heading', 'code_block', 'bullet_list', 'ordered_list'],
- }
- },
- addGlobalAttributes() {
- return [
- {
- types: this.options.types,
- attributes: {
- 'indent': {
- default: 0,
- parseHTML: element => parseInt(element.getAttribute('indent') || '0', 10),
- renderHTML: attributes => {
- const level = attributes['indent']
- return level > 0 && {
- 'indent': level,
- style: `text-indent: ${level * 2}em;`
- }
- },
- },
- },
- },
- ]
- },
- addCommands() {
- return {
- increaseIndent:
- (): Command =>
- ({tr, state, dispatch}) => {
- const {from, to} = state.selection
- let modified = false
- state.doc.nodesBetween(from, to, (node, pos) => {
- if (this.options.types.includes(node.type.name)) {
- const indent = node.attrs['indent'] || 0
- if (indent + 1 > this.options.maxIndent) return
- tr.setNodeMarkup(pos, undefined, {
- ...node.attrs,
- 'indent': indent + 1,
- })
- modified = true
- }
- })
- if (modified) {
- dispatch && dispatch(tr)
- return false
- }
- return true
- },
-
- decreaseIndent:
- (): Command =>
- ({tr, state, dispatch}) => {
- const {from, to} = state.selection
- let modified = false
- state.doc.nodesBetween(from, to, (node, pos) => {
- if (this.options.types.includes(node.type.name)) {
- const indent = node.attrs['indent'] || 0
- if (indent - 1 < 0) return
- const newIndent = Math.max(indent - 1, 0)
- tr.setNodeMarkup(pos, undefined, {
- ...node.attrs,
- 'indent': newIndent,
- })
- modified = true
- }
- })
- if (modified) {
- dispatch && dispatch(tr)
- return false
- }
- return true
- },
- }
- },
- addKeyboardShortcuts() {
- return {
- Tab: () => this.editor.commands.increaseIndent(),
- 'Shift-Tab': () => this.editor.commands.decreaseIndent(),
- Backspace: () => {
- const {state} = this.editor
- const {selection} = state
- const {$from} = selection
- if ($from.parentOffset !== 0) return false
- const node = $from.node()
- if (!this.options.types.includes(node.type.name)) return false
- const indent = node.attrs['indent'] || 0
- if (indent > 0) {
- return this.editor.commands.decreaseIndent()
- }
- return false
- },
- }
- },
-})
diff --git a/src/components/piEditor/ext/Selection.ts b/src/components/piEditor/ext/Selection.ts
deleted file mode 100644
index 22a89a4..0000000
--- a/src/components/piEditor/ext/Selection.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import {Extension} from '@tiptap/core'
-import {Plugin, PluginKey} from '@tiptap/pm/state'
-import {Decoration, DecorationSet} from '@tiptap/pm/view'
-
-declare module '@tiptap/core' {
- interface Commands {
- selection: {
- setSelection: () => ReturnType
- clearSelection: () => ReturnType
- }
- }
-}
-
-export default Extension.create({
- name: 'selection',
- addOptions() {
- return {
- class: 'selection',
- }
- },
- addStorage() {
- return {
- fakeRange: null as { from: number, to: number } | null,
- }
- },
- addProseMirrorPlugins() {
- return [
- new Plugin({
- key: new PluginKey('selection'),
- props: {
- decorations: (state) => {
- const {fakeRange} = this.storage
- if (!fakeRange) return null
- const deco = Decoration.inline(
- fakeRange.from,
- fakeRange.to,
- {class: this.options.class},
- )
- return DecorationSet.create(state.doc, [deco])
- },
- handleClick: (view, pos, event) => {
- const { fakeRange } = this.storage
- if (!fakeRange) return false
-
- const clickedInsideFake =
- pos >= fakeRange.from && pos <= fakeRange.to
-
- if (!clickedInsideFake) {
- // 点击了 fake selection 外部,清除它
- this.storage.fakeRange = null
- view.dispatch(view.state.tr)
- }
-
- return false
- },
- },
- }),
- ]
- },
- addCommands() {
- return {
- setSelection: () => ({state, view}) => {
- const {from, to, empty} = state.selection
- if (!empty && from !== to) {
- this.storage.fakeRange = {from, to}
- view.dispatch(state.tr)
- }
- return true
- },
- clearSelection: () => ({state, view}) => {
- if (this.storage.fakeRange) {
- this.storage.fakeRange = null
- view.dispatch(state.tr)
- }
- return true
- },
- }
- },
-})
diff --git a/src/components/piEditor/ext/Table/Action.vue b/src/components/piEditor/ext/Table/Action.vue
deleted file mode 100644
index 37de2a8..0000000
--- a/src/components/piEditor/ext/Table/Action.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
-
-
-
-
- 合并单元格
-
-
- 上加一行
-
-
- 下加一行
-
- 左加一列
-
- 右加一列
-
- 删除行
-
-
- 删除列
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/piEditor/ext/Table/index.ts b/src/components/piEditor/ext/Table/index.ts
deleted file mode 100644
index c92f2a0..0000000
--- a/src/components/piEditor/ext/Table/index.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import {VueNodeViewRenderer} from "@tiptap/vue-3";
-import {NodeSelection} from 'prosemirror-state'
-import {Table} from '@tiptap/extension-table'
-import Action from "./Action.vue"
-
-export default Table.extend({
- selectable: true,
- atom: true,
- // 添加属性配置
- addAttributes() {
- return {
- ...this.parent?.(),
- width: {
- default: '100%',
- renderHTML: attributes => {
- return {
- width: attributes.width,
- }
- }
- },
- class: {
- default: null,
- renderHTML: attributes => {
- return {
- class: attributes.class,
- }
- }
- },
- style: {
- default: 'table-layout: fixed;border-collapse: collapse;',
- renderHTML: attributes => {
- if (!attributes.style) return {}
- return {
- style: attributes.style,
- }
- }
- }
- }
- },
- renderHTML({node, HTMLAttributes}) {
- return ['table', HTMLAttributes, ['tbody', 0]]
- },
- addKeyboardShortcuts() {
- return {
- Backspace: ({editor}) => {
- const {state, view} = editor
- const {selection} = state
-
- // 没有深度直接删除
- const {$from} = selection
- if ($from.depth == 0) {
- editor.commands.deleteSelection()
- return true
- }
-
- // 选中整个表格删除
- if (selection instanceof NodeSelection && selection.node.type.name === 'table') {
- const tr = state.tr.delete(selection.from, selection.to)
- view.dispatch(tr)
- return true
- }
- const pos = $from.before($from.depth) // 当前段落的开始位置
- const index = $from.index($from.depth - 1)
-
- if (index == 0) return false
- const parent = $from.node($from.depth - 1)
-
- // 查前一个节点是不是 table
- const beforeNode = parent.child(index - 1)
-
- if (beforeNode?.type.name === 'table') {
- const deletePos = pos - beforeNode.nodeSize
- view.dispatch(
- state.tr
- .setSelection(NodeSelection.create(state.doc, deletePos))
- .deleteSelection()
- )
- return true
- }
-
- return false
- },
- Delete: ({editor}) => {
- const {state, view} = editor
- const {selection} = state
-
- // 没有深度直接删除
- const {$from} = selection
- if ($from.depth == 0) {
- editor.commands.deleteSelection()
- return true
- }
-
- //选中整个表格删除
- if (selection instanceof NodeSelection && selection.node.type.name === 'table') {
- const tr = state.tr.delete(selection.from, selection.to)
- view.dispatch(tr)
- return true
- }
-
- // 在表格前删除
- const pos = $from.before($from.depth) // 当前段落的开始位置
- const index = $from.index($from.depth - 1)
- const parent = $from.node($from.depth - 1)
-
- if (index < parent.childCount - 1) {
- const afterNode = parent.child(index + 1)
- if (afterNode?.type.name === 'table') {
- const deletePos = pos + $from.node().nodeSize
- view.dispatch(
- state.tr
- .setSelection(NodeSelection.create(state.doc, deletePos))
- .deleteSelection()
- )
- return true
- }
- } else {
- if (index == 0) return false
- const beforeNode = parent.child(index - 1)
- if (beforeNode?.type.name === 'table') {
- const deletePos = pos - beforeNode.nodeSize
- view.dispatch(
- state.tr
- .setSelection(NodeSelection.create(state.doc, deletePos))
- .deleteSelection()
- )
- return true
- }
- }
- return false
- },
- }
- },
- addNodeView() {
- return VueNodeViewRenderer(Action)
- }
-})
diff --git a/src/components/piEditor/ext/Video/Action.vue b/src/components/piEditor/ext/Video/Action.vue
deleted file mode 100644
index e9f78e8..0000000
--- a/src/components/piEditor/ext/Video/Action.vue
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 宽
-
-
-
-
- 高
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/piEditor/ext/Video/index.ts b/src/components/piEditor/ext/Video/index.ts
deleted file mode 100644
index 4b9b149..0000000
--- a/src/components/piEditor/ext/Video/index.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-// extensions/Video.ts
-import {Node, mergeAttributes, Command, CommandProps} from '@tiptap/core'
-import {VueNodeViewRenderer} from "@tiptap/vue-3";
-import Action from "./Action.vue";
-
-export interface VideoOptions {
- HTMLAttributes: Record
-}
-
-declare module '@tiptap/core' {
- interface Commands {
- video: {
- setVideo: (options: { src: string }) => ReturnType
- }
- }
-}
-
-export default Node.create({
- name: 'video',
- group: 'block',
- atom: true,
- selectable: true,
- addOptions() {
- return {
- HTMLAttributes: {},
- }
- },
- addAttributes() {
- return {
- src: {
- default: null,
- },
- width: {
- default: '100%',
- renderHTML: attributes => {
- return {
- width: attributes.width,
- }
- }
- },
- height: {
- default: null
- },
- class: {
- default: null,
- renderHTML: attributes => {
- return {
- class: attributes.class,
- }
- }
- },
- style: {
- default: '',
- renderHTML: attributes => {
- if (!attributes.style) return {}
- return {
- style: attributes.style,
- }
- }
- }
- }
- },
- parseHTML() {
- return [
- {
- tag: 'video',
- },
- ]
- },
- renderHTML({HTMLAttributes}) {
- return [
- 'video',
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
- controls: true,
- }),
- ['source', {src: HTMLAttributes.src, type: 'video/mp4'}],
- ]
- },
- addCommands() {
- return {
- setVideo: (attrs: { src: string }): Command => ({commands}: CommandProps) => {
- return commands.insertContent({
- type: this.name,
- attrs,
- })
- },
- }
- },
- addNodeView() {
- return VueNodeViewRenderer(Action)
- },
-})
diff --git a/src/components/piEditor/index.vue b/src/components/piEditor/index.vue
index 3807080..1a02b71 100644
--- a/src/components/piEditor/index.vue
+++ b/src/components/piEditor/index.vue
@@ -1,245 +1,7 @@
-
-
+
+
import piAssetPicker from '@/components/piAsset/picker'
-import {Editor, EditorContent} from "@tiptap/vue-3";
-import StarterKit from "@tiptap/starter-kit";
-import TextAlign from "@tiptap/extension-text-align"
-import Underline from '@tiptap/extension-underline'
-import Highlight from '@tiptap/extension-highlight'
-import Color from '@tiptap/extension-color'
-import TableRow from '@tiptap/extension-table-row'
-import TableCell from '@tiptap/extension-table-cell'
-import TableHeader from '@tiptap/extension-table-header'
-import Image from './ext/Image'
-import Video from './ext/Video'
-import Indent from './ext/Indent'
-import FontSize from './ext/FontSize'
-import Table from './ext/Table'
-import Selection from "./ext/Selection";
+import '@wangeditor/editor/dist/css/style.css' // 引入 css
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import {ref, onUnmounted, watch, nextTick, onMounted} from "vue";
const emit = defineEmits(['update:modelValue'])
-
const props = defineProps({
modelValue: {type: String, default: ''},
})
+const editorRef = ref(null)
const pickerRef1 = ref(null)
const pickerRef2 = ref(null)
-const colorPickerRef = ref(null)
-const bgPickerRef = ref(null)
-const toolbarRef = ref(null)
const pickerVisible1 = ref(false)
const pickerVisible2 = ref(false)
-const predefineColors = ref([
- '#FFFFFF',
- '#000000',
- '#409EFF',
- '#67C23A',
- '#E6A23C',
- '#F56C6C',
- '#909399',
- '#303133',
- '#CDD0D6',
- '#E6E8EB',
- '#606266',
- '#EBEDF0'
-])
-let editor = ref(null)
-let fontSize = ref('12px')
-let fontColor = ref('')
-let bgColor = ref('')
-let tag = ref('')
-let tableVisible = ref(false)
-let tableRow = ref(2)
-let tableCol = ref(3)
+const toolbarConfig = {
+ toolbarKeys: [
+ 'headerSelect',
+ 'bold',
+ 'italic',
+ 'underline',
+ '|',
+ 'uploadImage',
+ 'insertTable',
+ ]
+}
-watch(() => props.modelValue, (value) => {
- const isSame = editor.value.getHTML() === value
- if (isSame) {
- return
- }
- editor.value.commands.setContent(value, false)
-})
+const editorConfig = {
+ placeholder: '请输入内容...',
+ readOnly: false,
+ autoFocus: true,
+ scroll: true,
+}
+
+let html = ref("")
+
+watch(() => props.modelValue, () => {
+ html.value = props.modelValue
+}, {deep: true})
onMounted(() => {
- // 保存 fakeSelection
- editor.value?.on('selectionUpdate', () => {
- editor.value?.commands.setSelection()
- })
- // 点击外部仍保留背景
- document.addEventListener('click', (e) => {
- const el = document.querySelector('.ProseMirror')
- if (!el?.contains(e.target)){
- // 只是触发 view 更新,让装饰生效
- editor.value?.view.dispatch(editor.value.state.tr)
- }
- })
-
- // 点击内部清除假选中
- document.querySelector('.ProseMirror')?.addEventListener('click', () => {
- editor.value?.commands.clearSelection()
- })
})
-
onUnmounted(() => {
- editor.value.destroy()
+
})
-editor.value = new Editor({
- content: props.modelValue,
- extensions: [
- StarterKit,
- Underline,
- TextAlign.configure({
- types: ['heading', 'paragraph'],
- }),
- Indent,
- Highlight.configure({
- multicolor: true, // 支持多种背景色
- }),
- Color,
- FontSize,
- Table,
- TableRow,
- TableCell.configure({
- HTMLAttributes: {
- style: 'border: 1px solid #ccc;padding: 0.4rem;'
- },
- }),
- TableHeader.configure({
- HTMLAttributes: {
- style: 'border: 1px solid #ccc;padding: 0.4rem;'
- },
- }),
- Image.configure({
- inline: false,
- allowBase64: true,
- }),
- Video.configure({
- inline: false,
- allowBase64: true,
- }),
- Selection
- ],
- onUpdate: () => {
- emit('update:modelValue', editor.value.getHTML())
- },
- onSelectionUpdate: () => {
- // 字体大小回显
- let _fontSize = editor.value.getAttributes('textStyle').fontSize
- fontSize.value = _fontSize ? _fontSize : '12px'
-
- // 标签回显
- if (editor.value.isActive('paragraph')) {
- tag.value = 'p'
- } else if (editor.value.isActive('heading', {level: 1})) {
- tag.value = 'h1'
- } else if (editor.value.isActive('heading', {level: 2})) {
- tag.value = 'h2'
- } else if (editor.value.isActive('heading', {level: 3})) {
- tag.value = 'h3'
- } else if (editor.value.isActive('heading', {level: 4})) {
- tag.value = 'h4'
- } else if (editor.value.isActive('heading', {level: 5})) {
- tag.value = 'h5'
- } else if (editor.value.isActive('heading', {level: 6})) {
- tag.value = 'h6'
- } else if (editor.value.isActive('codeBlock')) {
- tag.value = 'pre'
- }
- },
- onCreate: () => {
- setTimeout(() => {
- // 初始化光标
- const firstNode = editor.value.state.doc.content.firstChild
- const from1 = firstNode?.content?.content?.[0]?.nodeSize ? 1 : 0
- const to1 = from1 + (firstNode?.nodeSize || 0) - 2
- editor.value.commands.setTextSelection({from1, to1})
- }, 300)
- }
-})
-
-function tagChange() {
- if (tag.value === 'p') {
- editor.value.chain().focus().setParagraph().run()
- } else if (tag.value === 'h1') {
- editor.value.chain().focus().setHeading({level: 1}).run()
- } else if (tag.value === 'h2') {
- editor.value.chain().focus().setHeading({level: 2}).run()
- } else if (tag.value === 'h3') {
- editor.value.chain().focus().setHeading({level: 3}).run()
- } else if (tag.value === 'h4') {
- editor.value.chain().focus().setHeading({level: 4}).run()
- } else if (tag.value === 'h5') {
- editor.value.chain().focus().setHeading({level: 5}).run()
- } else if (tag.value === 'h6') {
- editor.value.chain().focus().setHeading({level: 6}).run()
- } else if (tag.value === 'pre') {
- editor.value.chain().focus().setCodeBlock().run()
- }
-}
-
-function insertTable() {
- editor.value.chain().focus().insertTable({rows: tableRow.value, cols: tableCol.value}).run()
- tableVisible.value = false
-}
-
function showPicker1() {
pickerVisible1.value = true
nextTick(() => {
- pickerRef1.value.open()
})
}
function showPicker2() {
pickerVisible2.value = true
nextTick(() => {
- pickerRef2.value.open()
})
}
function saveSuccess1(val) {
val.forEach(src => {
- editor.value.chain().focus().setImage({
- src: src,
- alt: ''
- }).run()
+
})
}
function saveSuccess2(val) {
- editor.value.chain().focus().setVideo({src: val}).run()
+
}
diff --git a/src/config/asset.ts b/src/config/asset.ts
index 637e126..180a5d0 100644
--- a/src/config/asset.ts
+++ b/src/config/asset.ts
@@ -1,7 +1,7 @@
import api from "@/api";
export default {
- uploadObj: api.system.file.upload,
+ uploadObj: api.system.upload,
fileListObj: api.system.file.list,
moveFileObj: api.system.file.move,
delFileObj: api.system.file.del,
diff --git a/src/views/monitor/crontab/save.vue b/src/views/monitor/crontab/save.vue
index 58852ff..69249e0 100644
--- a/src/views/monitor/crontab/save.vue
+++ b/src/views/monitor/crontab/save.vue
@@ -20,7 +20,8 @@
-
+
+
@@ -34,7 +35,7 @@
import {getCurrentInstance, ref} from 'vue'
import api from "@/api/index.js"
import piCron from "@/components/piCron"
-
+import piEditor from "@/components/piEditor"
defineExpose({
open