AI-News/frontend/app/components/nodes/ResizableImage.vue

128 lines
2.8 KiB
Vue
Raw Normal View History

2025-12-04 10:04:21 +08:00
<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>