Vue 集成指南
本节介绍如何在 Vue 3 项目中正确集成 mark-img,包括实例管理、响应式状态同步和常见模式。所有示例均使用 <script setup> + TypeScript。
基础集成
vue
<template>
<div ref="containerRef" style="width: 100%; height: 500px" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import ImageMark from 'mark-img'
import 'mark-img/dist/style.css'
const props = defineProps<{
src: string
}>()
const containerRef = ref<HTMLDivElement | null>(null)
let imgMark: ImageMark | null = null
onMounted(() => {
imgMark = new ImageMark({
el: containerRef.value!,
src: props.src,
pluginOptions: {
shape: {
shapeList: [],
},
},
})
})
onUnmounted(() => {
imgMark?.destroy()
})
</script>TIP
ImageMark 实例不需要用 ref() 包裹为响应式对象,直接用普通变量存储即可。将其包裹为响应式会带来不必要的性能开销。
响应式状态同步
将 mark-img 的状态同步到 Vue 响应式数据:
vue
<template>
<div>
<div>缩放: {{ (scale * 100).toFixed(0) }}%</div>
<div>可撤销: {{ historyInfo.undo }} 步</div>
<div v-if="isDrawing">绘制中...</div>
<div ref="containerRef" style="width: 100%; height: 500px" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import ImageMark from 'mark-img'
const props = defineProps<{
src: string
}>()
const containerRef = ref<HTMLDivElement | null>(null)
let imgMark: ImageMark | null = null
const scale = ref(1)
const historyInfo = ref({ undo: 0, redo: 0 })
const isDrawing = ref(false)
onMounted(() => {
imgMark = new ImageMark({
el: containerRef.value!,
src: props.src,
pluginOptions: {
shape: { shapeList: [] },
},
})
.on('scale', (s) => { scale.value = s })
.on('history_change', (info) => { historyInfo.value = info })
.on('shape_start_drawing', () => { isDrawing.value = true })
.on('shape_end_drawing', () => { isDrawing.value = false })
})
onUnmounted(() => {
imgMark?.destroy()
})
</script>绘制工具栏
vue
<template>
<div>
<button @click="drawShape(ImageMarkRect, { shapeName: 'rect', x: 0, y: 0, width: 0, height: 0 })">
矩形
</button>
<button @click="drawShape(ImageMarkCircle, { shapeName: 'circle', x: 0, y: 0, r: 0 })">
圆形
</button>
<button @click="drawShape(ImageMarkPolygon, { shapeName: 'polygon', points: [] })">
多边形
</button>
</div>
</template>
<script setup lang="ts">
import ImageMark, { ImageMarkRect, ImageMarkCircle, ImageMarkPolygon } from 'mark-img'
const props = defineProps<{
imgMark: ImageMark | null
}>()
function drawShape(ShapeClass: any, data: any) {
if (!props.imgMark) return
props.imgMark.getShapePlugin()?.startDrawing(new ShapeClass(data, props.imgMark))
}
</script>撤销/重做按钮
vue
<template>
<div>
<button :disabled="historyInfo.undo === 0" @click="imgMark?.getHistoryPlugin()?.undo()">
撤销 ({{ historyInfo.undo }})
</button>
<button :disabled="historyInfo.redo === 0" @click="imgMark?.getHistoryPlugin()?.redo()">
重做 ({{ historyInfo.redo }})
</button>
</div>
</template>
<script setup lang="ts">
import ImageMark from 'mark-img'
defineProps<{
imgMark: ImageMark | null
historyInfo: { undo: number; redo: number }
}>()
</script>只读与选择模式切换
vue
<template>
<div>
<label>
<input type="checkbox" v-model="readonly" />
只读模式
</label>
<select v-model="selectMode">
<option value="single">单选</option>
<option value="multiple">多选</option>
</select>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import ImageMark from 'mark-img'
const props = defineProps<{
imgMark: ImageMark | null
}>()
const readonly = ref(false)
const selectMode = ref('single')
watch(readonly, (val) => {
props.imgMark?.setReadonly(val)
})
watch(selectMode, (val) => {
props.imgMark?.getSelectionPlugin()?.mode(val)
})
</script>右键菜单
通过监听事件实现自定义右键菜单:
vue
<template>
<div>
<div ref="containerRef" style="width: 100%; height: 500px" />
<div
v-if="contextMenu"
:style="{ position: 'fixed', left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
>
<button v-if="contextMenu.type === 'shape'" @click="handleDelete">删除</button>
<button v-if="contextMenu.type === 'container'" @click="handleDeleteAll">删除全部</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import ImageMark, { ImageMarkShape } from 'mark-img'
const containerRef = ref<HTMLDivElement | null>(null)
let imgMark: ImageMark | null = null
let tmpShape: ImageMarkShape | null = null
const contextMenu = ref<{ x: number; y: number; type: 'shape' | 'container' } | null>(null)
function hideMenu() {
contextMenu.value = null
}
function handleDelete() {
if (tmpShape) {
imgMark?.getShapePlugin()?.removeNode(tmpShape)
}
contextMenu.value = null
}
function handleDeleteAll() {
imgMark?.getShapePlugin()?.removeAllNodes()
contextMenu.value = null
}
onMounted(() => {
imgMark = new ImageMark({
el: containerRef.value!,
src: './example.jpg',
pluginOptions: {
shape: { shapeList: [] },
},
})
.on('shape_context_menu', (evt, shapeInstance) => {
evt.preventDefault()
tmpShape = shapeInstance
contextMenu.value = { x: evt.clientX, y: evt.clientY, type: 'shape' }
})
.on('container_context_menu', (evt) => {
evt.preventDefault()
contextMenu.value = { x: evt.clientX, y: evt.clientY, type: 'container' }
})
document.addEventListener('click', hideMenu)
})
onUnmounted(() => {
imgMark?.destroy()
document.removeEventListener('click', hideMenu)
})
</script>数据持久化
结合 localStorage 实现数据自动保存和恢复:
vue
<template>
<div ref="containerRef" style="width: 100%; height: 500px" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { debounce } from 'lodash-es'
import ImageMark from 'mark-img'
const props = defineProps<{
src: string
}>()
const containerRef = ref<HTMLDivElement | null>(null)
let imgMark: ImageMark | null = null
const initData = JSON.parse(localStorage.getItem('shapeList') || '[]')
const autoSave = debounce(() => {
const data = imgMark?.getShapePlugin()?.data || []
localStorage.setItem('shapeList', JSON.stringify(data))
}, 300)
onMounted(() => {
imgMark = new ImageMark({
el: containerRef.value!,
src: props.src,
pluginOptions: {
shape: { shapeList: initData },
},
}).on('shape_plugin_data_change', autoSave)
})
onUnmounted(() => {
imgMark?.destroy()
})
</script>图片源切换
当图片 src 变化时,需要销毁旧实例并创建新实例。推荐使用 key 强制重新挂载组件:
vue
<!-- 父组件 -->
<template>
<ImageAnnotator :src="currentSrc" :key="currentSrc" />
</template>也可以通过 watch 手动管理:
vue
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import ImageMark from 'mark-img'
const props = defineProps<{
src: string
}>()
const containerRef = ref<HTMLDivElement | null>(null)
let imgMark: ImageMark | null = null
function initImageMark() {
imgMark?.destroy()
imgMark = new ImageMark({
el: containerRef.value!,
src: props.src,
pluginOptions: {
shape: { shapeList: [] },
},
})
}
onMounted(() => {
initImageMark()
})
watch(() => props.src, () => {
initImageMark()
})
onUnmounted(() => {
imgMark?.destroy()
})
</script>完整的 Vue 集成示例可以参考项目源码中的 src_vue/views/Swiper/components/MarkImg.vue。