128 lines
2.8 KiB
Vue
128 lines
2.8 KiB
Vue
|
|
<template>
|
|||
|
|
<!-- ✅ 必须使用 NodeViewWrapper 作为根 -->
|
|||
|
|
<NodeViewWrapper
|
|||
|
|
class="ri-wrap"
|
|||
|
|
:class="{ selected }"
|
|||
|
|
contenteditable="false"
|
|||
|
|
ref="wrap"
|
|||
|
|
>
|
|||
|
|
<img
|
|||
|
|
ref="img"
|
|||
|
|
:src="node.attrs.src"
|
|||
|
|
:alt="node.attrs.alt || ''"
|
|||
|
|
:title="node.attrs.title || ''"
|
|||
|
|
:style="imgStyle"
|
|||
|
|
draggable="false"
|
|||
|
|
@mousedown.stop
|
|||
|
|
@click.stop
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 右下角拖拽手柄(等比缩放,写回 width) -->
|
|||
|
|
<span
|
|||
|
|
v-if="editor.isEditable"
|
|||
|
|
class="ri-handle"
|
|||
|
|
title="拖动调整大小"
|
|||
|
|
@mousedown.stop.prevent="onStartDrag"
|
|||
|
|
/>
|
|||
|
|
</NodeViewWrapper>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { NodeViewWrapper } from '@tiptap/vue-3'
|
|||
|
|
import { onBeforeUnmount, ref, computed } from 'vue'
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
editor: Object,
|
|||
|
|
node: Object,
|
|||
|
|
selected: Boolean,
|
|||
|
|
updateAttributes: Function,
|
|||
|
|
deleteNode: Function,
|
|||
|
|
getPos: Function,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const img = ref(null)
|
|||
|
|
const wrap = ref(null)
|
|||
|
|
|
|||
|
|
const imgStyle = computed(() => {
|
|||
|
|
const w = props.node.attrs.width || null
|
|||
|
|
return {
|
|||
|
|
width: w || 'auto',
|
|||
|
|
height: 'auto',
|
|||
|
|
maxWidth: '100%',
|
|||
|
|
display: 'block',
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
let dragging = false
|
|||
|
|
let startX = 0
|
|||
|
|
let startWidth = 0
|
|||
|
|
let cleanups = []
|
|||
|
|
|
|||
|
|
function onStartDrag(e) {
|
|||
|
|
if (!img.value) return
|
|||
|
|
const r = img.value.getBoundingClientRect()
|
|||
|
|
startX = e.clientX
|
|||
|
|
startWidth = r.width
|
|||
|
|
|
|||
|
|
dragging = true
|
|||
|
|
document.body.style.userSelect = 'none'
|
|||
|
|
document.body.style.cursor = 'nwse-resize'
|
|||
|
|
|
|||
|
|
const onMove = (ev) => {
|
|||
|
|
if (!dragging) return
|
|||
|
|
const dx = ev.clientX - startX
|
|||
|
|
const containerW = wrap.value?.parentElement?.getBoundingClientRect?.().width || 1200
|
|||
|
|
const newW = Math.max(40, Math.min(containerW, Math.round(startWidth + dx)))
|
|||
|
|
props.updateAttributes({ width: `${newW}px` })
|
|||
|
|
}
|
|||
|
|
const onUp = () => {
|
|||
|
|
dragging = false
|
|||
|
|
document.body.style.userSelect = ''
|
|||
|
|
document.body.style.cursor = ''
|
|||
|
|
window.removeEventListener('mousemove', onMove)
|
|||
|
|
window.removeEventListener('mouseup', onUp)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
window.addEventListener('mousemove', onMove)
|
|||
|
|
window.addEventListener('mouseup', onUp)
|
|||
|
|
cleanups.push(() => {
|
|||
|
|
window.removeEventListener('mousemove', onMove)
|
|||
|
|
window.removeEventListener('mouseup', onUp)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => cleanups.forEach((fn) => fn()))
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.ri-wrap {
|
|||
|
|
position: relative;
|
|||
|
|
display: inline-block;
|
|||
|
|
line-height: 0;
|
|||
|
|
max-width: 100%;
|
|||
|
|
border-radius: 10px;
|
|||
|
|
}
|
|||
|
|
.ri-wrap.selected {
|
|||
|
|
outline: 2px solid rgba(99,102,241,.35);
|
|||
|
|
outline-offset: 2px;
|
|||
|
|
}
|
|||
|
|
.ri-wrap img {
|
|||
|
|
max-width: 100%;
|
|||
|
|
height: auto;
|
|||
|
|
border-radius: 10px;
|
|||
|
|
box-shadow: 0 6px 20px rgba(2,6,23,.08);
|
|||
|
|
}
|
|||
|
|
.ri-handle {
|
|||
|
|
position: absolute;
|
|||
|
|
right: -6px;
|
|||
|
|
bottom: -6px;
|
|||
|
|
width: 12px;
|
|||
|
|
height: 12px;
|
|||
|
|
background: #6366f1;
|
|||
|
|
border: 2px solid #fff;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
box-shadow: 0 1px 4px rgba(2,6,23,.25);
|
|||
|
|
cursor: nwse-resize;
|
|||
|
|
}
|
|||
|
|
</style>
|