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>
|