我正在开发一项功能,用户可以添加产品图像,并且具有撤消/重做功能。我尽可能地减少它以显示我的问题。
我在 Vue Playground 中创建了一个关于我的问题的最小示例: 看这里
有一个名为
ImageItem
的组件,它在 v-for
范围内渲染。它获取一个包含对象的 prop imageModel
,并在内部使用该 imageModel
属性来访问属性 description
并使用文本区域显示/更改它。当添加、删除和添加回列表中的项目时,似乎与文本区域的 value
属性的绑定由于某种原因被破坏。
在我的示例应用程序中尝试以下操作:
为了完整性,我在这个问题中包含了代码。但它与 Vue Playground 中的代码相同。
应用程序.vue
<script setup>
import { ref } from 'vue'
import ImageItem from './ImageItem.vue';
import { ProductEditModel } from './ProductEditModel';
const productEditModel = ref(new ProductEditModel());
function addImage() {
productEditModel.value.addImage();
}
function handleChangeImageDescription(imageModel, description) {
productEditModel.value.changeImageDescription(imageModel, description);
}
function undo() { productEditModel.value.editHistory.undo(); }
function redo() { productEditModel.value.editHistory.redo(); }
</script>
<template>
<div v-for="productImage of productEditModel.images" :key="productImage.key">
<ImageItem :imageModel="productImage" @changeImageDescription="handleChangeImageDescription"></ImageItem>
</div>
<button @click="undo">Undo</button>
<button @click="redo">Redo</button>
<button @click="addImage">Add image</button>
</template>
ProductImage.js
export class ProductImage {
constructor()
{
this.key = Math.random();
this.description = '';
}
}
ImageItem.vue
<script setup>
import { defineProps, defineEmits } from 'vue';
const emit = defineEmits(['removeImage', 'changeImage', 'changeImageDescription']);
const props = defineProps({
imageModel: {
type: Object,
required: true
}
});
function handleDescriptionChange(description)
{
emit('changeImageDescription', props.imageModel, description);
}
</script>
<template>
<div class="image-item">
<img src="" alt="Example image here..." />
Description:
<textarea :value="props.imageModel.description" rows="4"
@change="(event) => handleDescriptionChange(event.target.value)" />
</div>
</template>
<style>
.image-item {
border: 1px solid #aaa;
padding: 10px;
}
.image-item * {
vertical-align: middle;
}
img {
min-width: 100px;
min-height: 100px;
border: 1px solid #d0d0d0;
}
</style>
EditHistory.js
export class EditHistory
{
constructor()
{
this.undoStack = [];
this.redoStack = [];
}
do(action)
{
action.execute();
this.undoStack.push(action);
this.redoStack = [];
}
undo(count = 1)
{
count = Math.min(this.undoStack.length, count);
for (let i = count; i > 0; i--)
{
const undoAction = this.undoStack.pop();
undoAction.unExecute();
this.redoStack.push(undoAction);
}
}
redo(count = 1)
{
count = Math.min(this.redoStack.length, count);
for (let i = count; i > 0; i--)
{
const redoAction = this.redoStack.pop();
redoAction.execute();
this.undoStack.push(redoAction);
}
}
canUndo()
{
return this.undoStack.length > 0;
}
canRedo()
{
return this.redoStack.length > 0;
}
}
ProductEditModel.js
import { AddImageAction } from "./AddImageAction";
import { ChangeImageDescriptionAction } from "./ChangeImageDescriptionAction";
import { EditHistory } from "./EditHistory";
export class ProductEditModel
{
constructor()
{
this.images = []; // Array of ProductImage instances.
this.editHistory = new EditHistory();
}
addImage()
{
this.editHistory.do(new AddImageAction(this));
}
changeImageDescription(imageModel, description)
{
this.editHistory.do(new ChangeImageDescriptionAction(this, imageModel, description));
}
}
AddImageAction.js
import { ProductImage } from './ProductImage.js';
export class AddImageAction
{
constructor(productEditModel)
{
this.productEditModel = productEditModel;
this.addedImageModel = null;
}
getDescription()
{
return "Afbeelding toevoegen";
}
execute()
{
this.addedImageModel = new ProductImage();
this.productEditModel.images.push(this.addedImageModel);
}
unExecute()
{
const index = this.productEditModel.images.indexOf(this.addedImageModel);
console.assert(index > -1);
this.productEditModel.images.splice(index, 1);
}
}
ChangeImageDescriptionAction.js
export class ChangeImageDescriptionAction
{
constructor(productEditModel, productImageEditModel, description)
{
this.productEditModel = productEditModel;
this.productImageEditModel = productImageEditModel;
this.description = description;
this.originalDescription = this.productImageEditModel.description;
}
getDescription()
{
return "Afbeelding omschrijving veranderen";
}
execute()
{
this.productImageEditModel.description = this.description;
console.log("Changed description to: " + this.description);
}
unExecute()
{
this.productImageEditModel.description = this.originalDescription;
console.log("Changed description back to original: " + this.originalDescription);
}
}
Vue 中的反应式类存在已知问题,这些问题限制了类的编写方式,但这里不适用。
this
并不总是所有类中的响应式对象,但它们对响应式数据进行操作,因此预计响应性不会出现问题。
问题在于历史记录存储类实例(这是可以通过不可变状态避免的设计限制),但它不会在重做时重用图像实例,因此
description
在旧实例上发生了更改。
修复方法是:
execute()
{
if (!this.addedImageModel)
this.addedImageModel = new ProductImage();
...