This commit is contained in:
parent
f55a608034
commit
1c9f69a53b
|
|
@ -32,7 +32,7 @@
|
|||
<div id="app" class="pi">
|
||||
<div class="app-loading">
|
||||
<div class="app-loading__logo">
|
||||
<img src="images/logo.png"/>
|
||||
<img src="/images/logo.png"/>
|
||||
</div>
|
||||
<div class="app-loading__loader"></div>
|
||||
<div class="app-loading__title">%VITE_APP_TITLE%</div>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<template>
|
||||
<NodeViewWrapper as="p" class="image-wrapper">
|
||||
<div class="image-box" ref="boxRef" @click="handleClick">
|
||||
<img :src="node.attrs.src" :alt="node.attrs.alt" :width="node.attrs.width" :height="node.attrs.height" :draggable="false"/>
|
||||
<!-- 操作菜单,仅在选中时显示 -->
|
||||
<div v-if="selected" class="image-menu">
|
||||
<button @click="deleteImage">删除</button>
|
||||
<button @click="replaceImage">配置</button>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="settingShow" title="配置" width="500" align-center>
|
||||
<el-form v-model="form" label-position="top">
|
||||
<el-form-item label="地址" prop="src">
|
||||
<el-input v-model="form.src"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="alt">
|
||||
<el-input v-model="form.alt"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="宽高" prop="width">
|
||||
<el-row :gutter="10" style="margin: 10px 0; width: 100%;">
|
||||
<el-col :span="12">
|
||||
<el-input v-model="form.width">
|
||||
<template #suffix>宽 </template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input v-model="form.height">
|
||||
<template #suffix>高 </template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="settingShow = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</NodeViewWrapper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {NodeViewWrapper, NodeViewProps} from '@tiptap/vue-3'
|
||||
import {computed, ref} from "vue"
|
||||
|
||||
const props = defineProps<NodeViewProps>();
|
||||
const selected = computed(() => props.selected)
|
||||
|
||||
let settingShow = ref(false)
|
||||
let form = ref({
|
||||
src: '',
|
||||
width: '',
|
||||
height: '',
|
||||
alt: ''
|
||||
})
|
||||
|
||||
function deleteImage() {
|
||||
props.deleteNode()
|
||||
}
|
||||
|
||||
function replaceImage() {
|
||||
form.value.src = props.node.attrs.src
|
||||
form.value.width = props.node.attrs.width
|
||||
form.value.height = props.node.attrs.height
|
||||
form.value.alt = props.node.attrs.alt
|
||||
settingShow.value = true
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
props.editor.commands.focus()
|
||||
}
|
||||
|
||||
function submit() {
|
||||
settingShow.value = false
|
||||
props.updateAttributes(form.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image-box img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 操作菜单 */
|
||||
.image-menu {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.image-menu button {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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)
|
||||
},
|
||||
})
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import {Extension, Command} from '@tiptap/core'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
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
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -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<ReturnType> {
|
||||
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
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
<template>
|
||||
<NodeViewWrapper class="table-wrapper" @contextmenu.prevent="showMenu($event)">
|
||||
<NodeViewContent as="table" v-bind="props.node.attrs" :class="isSelected ? 'ProseMirror-selectednode' : ''"/>
|
||||
<el-dropdown ref="tableDropdownRef" trigger="contextmenu" :teleported="false">
|
||||
<span></span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu ref="tableMenu" class="dropdown">
|
||||
<el-dropdown-item @click="editor.commands.mergeCells()" :disabled="!editor.can().mergeCells()">
|
||||
合并单元格
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="editor.commands.addRowBefore()" :disabled="!editor.can().addRowBefore()" divided>
|
||||
上加一行
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="editor.commands.addRowAfter()" :disabled="!editor.can().addRowAfter()">
|
||||
下加一行
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="editor.commands.addColumnBefore()"
|
||||
:disabled="!editor.can().addColumnBefore()">左加一列
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="editor.commands.addColumnAfter()"
|
||||
:disabled="!editor.can().addColumnAfter()">右加一列
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="editor.commands.deleteRow()" :disabled="!editor.can().deleteRow()" divided>删除行
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="editor.commands.deleteColumn()" :disabled="!editor.can().deleteColumn()">
|
||||
删除列
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</NodeViewWrapper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {NodeViewWrapper, NodeViewContent, NodeViewProps} from '@tiptap/vue-3'
|
||||
import {ref, computed} from 'vue'
|
||||
|
||||
const props = defineProps<NodeViewProps>()
|
||||
import {NodeSelection} from 'prosemirror-state'
|
||||
|
||||
const tableDropdownRef = ref(null)
|
||||
const tableMenu = ref(null)
|
||||
|
||||
const isSelected = computed(() => {
|
||||
const {selection} = props.editor.state
|
||||
|
||||
if (selection instanceof NodeSelection && selection.node.type.name === 'table') {
|
||||
return true
|
||||
}
|
||||
|
||||
const $from = selection.$from
|
||||
let inTable = false
|
||||
for (let depth = $from.depth; depth > 0; depth--) {
|
||||
const node = $from.node(depth)
|
||||
if (node.type.name === 'table') {
|
||||
inTable = true
|
||||
}
|
||||
}
|
||||
return inTable
|
||||
})
|
||||
|
||||
function showMenu(e) {
|
||||
const _menu = tableDropdownRef.value.$el
|
||||
_menu.style.position = 'absolute';
|
||||
// 点位置+边框
|
||||
_menu.style.left = e.layerX + 'px'
|
||||
// 组件高度 + 工具栏高度 + 偏移
|
||||
_menu.style.top = e.layerY + 'px'
|
||||
tableDropdownRef.value.handleOpen()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-wrapper {
|
||||
position: relative;
|
||||
:deep(.el-dropdown-menu) {
|
||||
list-style-type: disc;
|
||||
padding-left: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-popper) {
|
||||
line-height: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
<template>
|
||||
<NodeViewWrapper as="p" class="video-wrapper">
|
||||
<div class="video-box" ref="boxRef" @click="handleClick">
|
||||
<NodeViewContent as="video" v-bind="props.node.attrs" controls/>
|
||||
<div v-if="selected" class="video-menu">
|
||||
<button @click="deleteVideo">删除</button>
|
||||
<button @click="replaceVideo">配置</button>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="settingShow" title="配置" width="500" align-center>
|
||||
<el-form v-model="form" label-position="top">
|
||||
<el-form-item label="地址" prop="src">
|
||||
<el-input v-model="form.src"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="宽高" prop="width">
|
||||
<el-row :gutter="10" style="margin: 10px 0; width: 100%;">
|
||||
<el-col :span="12">
|
||||
<el-input v-model="form.width">
|
||||
<template #suffix>宽 </template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input v-model="form.height">
|
||||
<template #suffix>高 </template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="settingShow = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</NodeViewWrapper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {NodeViewWrapper, NodeViewProps, NodeViewContent} from '@tiptap/vue-3'
|
||||
import {computed, ref} from "vue"
|
||||
|
||||
const props = defineProps<NodeViewProps>();
|
||||
const selected = computed(() => props.selected)
|
||||
|
||||
let settingShow = ref(false)
|
||||
let form = ref({
|
||||
src: '',
|
||||
width: '',
|
||||
height: ''
|
||||
})
|
||||
|
||||
function deleteVideo() {
|
||||
props.deleteNode()
|
||||
}
|
||||
|
||||
function replaceVideo() {
|
||||
form.value.src = props.node.attrs.src
|
||||
form.value.width = props.node.attrs.width
|
||||
form.value.height = props.node.attrs.height
|
||||
settingShow.value = true
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
props.editor.commands.focus()
|
||||
}
|
||||
|
||||
function submit() {
|
||||
settingShow.value = false
|
||||
props.updateAttributes(form.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.video-box video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 操作菜单 */
|
||||
.video-menu {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-menu button {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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<string, any>
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
video: {
|
||||
setVideo: (options: { src: string }) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Node.create<VideoOptions>({
|
||||
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)
|
||||
},
|
||||
})
|
||||
|
|
@ -1,245 +1,7 @@
|
|||
<template>
|
||||
<div class="pi-editor">
|
||||
<div class="toolbar" ref="toolbarRef">
|
||||
<div class="box">
|
||||
<div class="group">
|
||||
<el-button text circle title="撤销" class="item"
|
||||
@click="editor.chain().focus().undo().run()"
|
||||
:disabled="!editor.can().undo()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-arrow-go-back'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="重做" class="item"
|
||||
@click="editor.chain().focus().redo().run()"
|
||||
:disabled="!editor.can().redo()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-arrow-go-forward'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="group">
|
||||
<el-color-picker v-model="fontColor" ref="colorPickerRef"
|
||||
@change="editor.chain().focus().setColor(fontColor).run()"
|
||||
:predefine="predefineColors" :teleported="false" :show-alpha="true"
|
||||
class="color-picker"></el-color-picker>
|
||||
<el-color-picker v-model="bgColor" ref="bgPickerRef"
|
||||
@change="bgColor ? editor.chain().focus().toggleHighlight({ color: bgColor }).run() : editor.chain().focus().unsetHighlight().run()"
|
||||
:predefine="predefineColors" :teleported="false" :show-alpha="true"
|
||||
class="color-picker"></el-color-picker>
|
||||
<el-button text circle title="文字颜色" class="item"
|
||||
@click="colorPickerRef.show()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-font-color'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="背景色" class="item"
|
||||
@click="bgPickerRef.show()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-background-color'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="粗体" class="item"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
:class="{ 'active': editor.isActive('bold') }">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-bold'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="斜体" class="item"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
:class="{ 'active': editor.isActive('italic') }">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-italic'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="下划线" class="item"
|
||||
@click="editor.chain().focus().toggleUnderline().run()"
|
||||
:class="{ 'active': editor.isActive('underline') }">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-underline'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="删除线" class="item"
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
:class="{ 'active': editor.isActive('strike') }">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-strikethrough'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="group">
|
||||
<el-button text circle title="左对齐" class="item"
|
||||
:class="{ 'active': editor.isActive({ textAlign: 'left' }) }"
|
||||
@click="editor.isActive({ textAlign: 'left' }) ? editor.chain().focus().unsetTextAlign().run() : editor.chain().focus().setTextAlign('left').run()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-align-left'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="居中对齐" class="item"
|
||||
:class="{ 'active': editor.isActive({ textAlign: 'center' }) }"
|
||||
@click="editor.isActive({ textAlign: 'center' }) ? editor.chain().focus().unsetTextAlign().run() : editor.chain().focus().setTextAlign('center').run()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-align-center'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="右对齐" class="item"
|
||||
:class="{ 'active': editor.isActive({ textAlign: 'right' }) }"
|
||||
@click="editor.isActive({ textAlign: 'right' }) ? editor.chain().focus().unsetTextAlign().run() : editor.chain().focus().setTextAlign('right').run()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-align-right'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="两端对齐" class="item"
|
||||
:class="{ 'active': editor.isActive({ textAlign: 'justify' }) }"
|
||||
@click="editor.isActive({ textAlign: 'justify' }) ? editor.chain().focus().unsetTextAlign().run() : editor.chain().focus().setTextAlign('justify').run()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-align-justify'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="group">
|
||||
<el-button text circle title="减少缩进" class="item"
|
||||
@click="editor.chain().focus().decreaseIndent().run()"
|
||||
:disabled="editor.can().decreaseIndent()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-indent-decrease'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="增加缩进" class="item"
|
||||
@click="editor.chain().focus().increaseIndent().run()"
|
||||
:disabled="editor.can().increaseIndent()">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-indent-increase'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="group">
|
||||
<el-button text circle title="有序列表" class="item"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
:class="{ 'active': editor.isActive('orderedList') }">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-list-ordered'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="无序列表" class="item"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
:class="{ 'active': editor.isActive('bulletList') }">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-list-unordered'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="group">
|
||||
<el-popover :visible="tableVisible" :width="220">
|
||||
<el-row style="margin: 10px 0;" :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-input v-model="tableRow">
|
||||
<template #suffix>行 </template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input v-model="tableCol">
|
||||
<template #suffix>列 </template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div style="text-align: right; margin: 0">
|
||||
<el-button size="small" text @click="tableVisible = false">取消</el-button>
|
||||
<el-button size="small" type="primary" @click="insertTable">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button text circle title="表格" class="item"
|
||||
@click="tableVisible = true">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-table'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button text circle title="图片" class="item"
|
||||
@click="showPicker1">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-image'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle title="视频" class="item"
|
||||
@click="showPicker2">
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-video'"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="group">
|
||||
<el-select v-model="tag" class="item" style="width:130px" clearable @change="tagChange">
|
||||
<template #prefix>
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-heading'"/>
|
||||
</el-icon>
|
||||
</template>
|
||||
<el-option label="段落" value="p">
|
||||
<template #default>
|
||||
<p>段落</p>
|
||||
</template>
|
||||
</el-option>
|
||||
<el-option label="H1" value="h1">
|
||||
<template #default>
|
||||
<h1>H1</h1>
|
||||
</template>
|
||||
</el-option>
|
||||
<el-option label="H2" value="h2">
|
||||
<template #default>
|
||||
<h2>H2</h2>
|
||||
</template>
|
||||
</el-option>
|
||||
<el-option label="H3" value="h3">
|
||||
<template #default>
|
||||
<h3>H3</h3>
|
||||
</template>
|
||||
</el-option>
|
||||
<el-option label="H4" value="h4">
|
||||
<template #default>
|
||||
<h4>H4</h4>
|
||||
</template>
|
||||
</el-option>
|
||||
<el-option label="H5" value="h5">
|
||||
<template #default>
|
||||
<h5>H5</h5>
|
||||
</template>
|
||||
</el-option>
|
||||
<el-option label="H6" value="h6">
|
||||
<template #default>
|
||||
<h6>H6</h6>
|
||||
</template>
|
||||
</el-option>
|
||||
<el-option label="预格式化" value="pre">
|
||||
<template #default>
|
||||
<pre>预格式化</pre>
|
||||
</template>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-select v-model="fontSize" class="item" style="width:130px" clearable allow-create filterable
|
||||
@change="editor.chain().focus().setMark('textStyle', { fontSize: fontSize }).run()">
|
||||
<template #prefix>
|
||||
<el-icon size="20px">
|
||||
<component :is="'pi-icon-font-size'"/>
|
||||
</el-icon>
|
||||
</template>
|
||||
<el-option label="12px" value="12px"/>
|
||||
<el-option label="14px" value="14px"/>
|
||||
<el-option label="16px" value="16px"/>
|
||||
<el-option label="18px" value="18px"/>
|
||||
<el-option label="22px" value="22px"/>
|
||||
<el-option label="24px" value="24px"/>
|
||||
<el-option label="36px" value="36px"/>
|
||||
<el-option label="72px" value="72px"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<editor-content class="body" :editor="editor"/>
|
||||
<Toolbar :editor="editorRef" :defaultConfig="toolbarConfig"/>
|
||||
<Editor v-model="html" :defaultConfig="editorConfig" @onCreated="editorRef = $event"/>
|
||||
<pi-asset-picker ref="pickerRef1" v-if="pickerVisible1" @success="saveSuccess1" @closed="pickerVisible1=false"
|
||||
type="image" :max="30" :multiple="true"></pi-asset-picker>
|
||||
<pi-asset-picker ref="pickerRef2" v-if="pickerVisible2" @success="saveSuccess2" @closed="pickerVisible2=false"
|
||||
|
|
@ -249,321 +11,78 @@
|
|||
|
||||
<script setup>
|
||||
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()
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pi-editor {
|
||||
border: 2px solid #eee;
|
||||
border-radius: 10px;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
visibility: inherit !important;
|
||||
}
|
||||
|
||||
.pi-editor .toolbar .box {
|
||||
background-color: #fff;
|
||||
border-bottom: none;
|
||||
box-shadow: 0 2px 2px -2px rgba(34, 47, 62, .1), 0 8px 8px -4px rgba(34, 47, 62, .07);
|
||||
padding: 4px 0;
|
||||
transition: box-shadow .5s;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pi-editor .toolbar .box .group {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.pi-editor .toolbar .box .group .item + .item {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
:deep(.toolbar .box .group .item.color .el-color-picker__trigger) {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
:deep(.group .color-picker) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.pi-editor .toolbar .box .group .el-button.active {
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.pi-editor .toolbar .box .group .el-button.active .el-icon {
|
||||
color: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.pi-editor .toolbar .box .group .el-button .el-icon {
|
||||
color: var(--el-color-black);
|
||||
}
|
||||
|
||||
.pi-editor .toolbar .box .group .el-button.is-disabled .el-icon {
|
||||
color: var(--el-button-disabled-text-color);
|
||||
}
|
||||
|
||||
.pi-editor .body {
|
||||
margin: 1em;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
resize: vertical;
|
||||
|
||||
:deep(ul) {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
:deep(ol) {
|
||||
list-style-type: decimal;
|
||||
padding-left: 1.5em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
:deep(.ProseMirror-selectednode) {
|
||||
border: 3px solid #b4d7ff;
|
||||
}
|
||||
|
||||
:deep(.selectedCell) {
|
||||
background-color: rgba(66, 185, 131, 0.2);
|
||||
}
|
||||
|
||||
:deep(.selection) {
|
||||
background-color: rgba(180, 213, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@
|
|||
<el-switch v-model="form.enable" :active-value="1" :inactive-value="0"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="memo">
|
||||
<el-input type="textarea" v-model="form.memo" placeholder="请输入备注" clearable></el-input>
|
||||
<pi-editor v-model="form.memo"></pi-editor>
|
||||
<!-- <el-input type="textarea" v-model="form.memo" placeholder="请输入备注" clearable></el-input>-->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue