Piranha CMS - 自定义块不会保存

Piranha CMS - Custom block won't save

对 Piranha 和 Vue 很陌生,但对 .Net Core 不是。试图了解如何创建自定义块。我创建了一个新块,试图将 HtmlBlock 和 ImageBlock 结合起来:

using Piranha.Extend;
using Piranha.Extend.Blocks;
using Piranha.Extend.Fields;

namespace YR.Models.Piranha.Blocks
{
    [BlockType(Name = "Card", Category = "Content", Icon = "fas fa-address-card", Component = "card-block")]
    public class CardBlock : Block
    {
        public ImageField ImgBody { get; set; }
        public SelectField<ImageAspect> Aspect { get; set; } = new SelectField<ImageAspect>();
        public HtmlField HtmlBody { get; set; }

        public override string GetTitle()
        {
            if (ImgBody != null && ImgBody.Media != null)
            {
                return ImgBody.Media.Filename;
            }
            return "No image selected";
        }
    }
}

如果我在 BlockTypeAttribute 中省略组件 属性,该块将起作用,保存图像和内容以草稿并在发布时完美更新网站。为了在管理器中获得完整的体验,我还尝试构建一个 Vue 组件,它只是结合了 html-block.vue 和 image-block.vue 组件。

这是我的 Vue 组件:

Vue.component("card-block", {
    props: ["uid", "toolbar", "model"],
    data: function () {
        return {
            imgBody: this.model.imgBody.value,
            htmlBody: this.model.htmlBody.value
        };
    },
    methods: {
        clear: function () {
            // clear media from block
        },
        onBlur: function (e) {
            this.model.htmlBody.value = e.target.innerHTML;
        },
        onChange: function (data) {
            this.model.htmlBody.value = data;
        },
        remove: function () {
            this.model.imgBody.id = null;
            this.model.imgBody.media = null;
        },
        select: function () {
            if (this.model.imgBody.media != null) {
                piranha.mediapicker.open(this.update, "Image", this.model.imgBody.media.folderId);
            } else {
                piranha.mediapicker.openCurrentFolder(this.update, "Image");
            }
        },
        update: function (media) {
            if (media.type === "Image") {
                this.model.imgBody.id = media.id;
                this.model.imgBody.media = media;
                
                // Tell parent that title has been updated
                this.$emit('update-title', {
                    uid: this.uid,
                    title: this.model.imgBody.media.filename
                });
            } else {
                console.log("No image was selected");
            }
        }
    },
    computed: {
        isEmpty: function () {
            return {
                htmlBody: piranha.utils.isEmptyHtml(this.model.htmlBody.value),
                imgBody: this.model.imgBody.media == null
            }
        },
        mediaUrl: function () {
            if (this.model.imgBody.media != null) {
                return piranha.utils.formatUrl(this.model.imgBody.media.publicUrl);
            } else {
                return piranha.utils.formatUrl("~/manager/assets/img/empty-image.png");
            }
        }
    },
    mounted: function () {
        piranha.editor.addInline(this.uid, this.toolbar, this.onChange);
        this.model.imgBody.getTitle = function () {
            if (this.model.imgBody.media != null) {
                return this.model.imgBody.media.filename;
            } else {
                return "No image selected";
            }
        };
    },
    beforeDestroy: function () {
        piranha.editor.remove(this.uid);
    },
    template:
        "<div class='block-body has-media-picker rounded' :class='{ empty: isEmpty }'>" + 
        "   <img class='rounded' :src='mediaUrl'>" +
        "   <div class='media-picker'>" +
        "    <div class='btn-group float-right'>" +
        "        <button :id='uid' class='btn btn-info btn-aspect text-center' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>" +
        "            <i v-if='model.aspect.value === 0' class='fas fa-cog'></i>" +
        "            <img v-else :src='iconUrl'>" +
        "        </button>" +
        "        <div class='dropdown-menu aspect-menu' :aria-labelledby='uid'>" +
        "            <label class='mb-0'>{{ piranha.resources.texts.aspectLabel }}</label>" +
        "            <div class='dropdown-divider'></div>" +
        "            <a v-on:click.prevent='selectAspect(0)' class='dropdown-item' :class='{ active: isAspectSelected(0) }' href='#'>" +
        "                <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
        "            </a>" +
        "           <a v-on:click.prevent='selectAspect(1)' class='dropdown-item' :class='{ active: isAspectSelected(1) }' href='#'>" +
        "                <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
        "            </a>" + 
        "           <a v-on:click.prevent='selectAspect(2)' class='dropdown-item' :class='{ active: isAspectSelected(2) }' href='#'>" +
        "                <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
        "            </a>" + 
        "           <a v-on:click.prevent='selectAspect(3)' class='dropdown-item' :class='{ active: isAspectSelected(3) }' href='#'>" +
        "                <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
        "            </a>" + 
        "           <a v-on:click.prevent='selectAspect(4)' class='dropdown-item' :class='{ active: isAspectSelected(4) }' href='#'>" +
        "                <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
        "            </a>" + 
        "        </div>" +
        "        <button v-on:click.prevent='select' class='btn btn-primary text-center'>" +
        "            <i class='fas fa-plus'></i>" +
        "        </button>" +
        "        <button v-on:click.prevent='remove' class='btn btn-danger text-center'>" +
        "            <i class='fas fa-times'></i>" +
        "         </button>" +
        "    </div>" +
        "    <div class='card text-left'>" +
        "        <div class='card-body' v-if='isEmpty'>" +
        "            &nbsp;" +
        "        </div>" +
        "        <div class='card-body' v-else>" +
        "            {{ model.body.media.filename }}" +
        "        </div>" +
        "    </div>" +
        "</div>" +
        "  <div contenteditable='true' :id='uid' spellcheck='false' v-html='htmlBody' v-on:blur='onBlur'></div>" +
        "</div>" 
});

它基本上是我在 GitHub 的 repo 中找到的两个 Vue 组件的混合体,但我对其进行了一些调整,使其不会在 DevTools 控制台中吐出错误。如果我可以通过保存它,我会重新访问这些项目。

所以,这是我想问@tidyui 或任何成功实施过类似项目的人的问题:

  1. 我的做法是否正确?我只想要三列,每列将包含我的 CardBlock,它下面有一张图片和一段内容,但我希望 CardBlock 是一个单元(有点像 Bootstrap 卡片)。是否已经有一种方法可以在不创建我自己的块的情况下执行此操作?我研究了嵌套 BlockGroups,但很快发现这是不可能的。

  2. 如果我在正确的轨道上,我需要帮助解决我在尝试保存草稿时遇到的错误。该错误与 Save ImageBlock error #1117 相同,似乎已在 8.2 中修复。我在 8.4.2.

我非常喜欢为 .Net Core 构建 CMS 的想法,而 Piranha 吹走了我为自己创建的 CMS。我只需要在正确的方向上稍微推动一下,拜托,我今天大部分时间都在做这个。

提前致谢, D

以防万一这对某人有帮助。事实证明我在将两个块合并在一起时做得很差。将此归因于食人鱼和 Vue.js 缺乏经验。我将文档中的代码与回购中的代码混合在一起。不要那样做——可以理解,文档仍然有点落后。我不是在向开发人员扔石头,我真的很喜欢他们创造的东西,并将继续努力熟练地使用它。

下面是我为 Vue 组件设计的。可能仍需要进行一些调整以更好地分离 Image-Block 和 Html-Block 代码,但它现在可以工作、保存并且不会在控制台中抛出错误。

/*global
    piranha
*/

Vue.component("card-block", {
    props: ["uid", "toolbar", "model"],
    data: function () {
        return {
            imgBody: this.model.imgBody.value,
            htmlBody: this.model.htmlBody.value
        };
    },
    methods: {
        clear: function () {
            // clear media from block
        },
        onBlur: function (e) {
            this.model.htmlBody.value = e.target.innerHTML;
        },
        onChange: function (data) {
            this.model.htmlBody.value = data;
        },
        select: function () {
            if (this.model.imgBody.media != null) {
                piranha.mediapicker.open(this.update, "Image", this.model.imgBody.media.folderId);
            } else {
                piranha.mediapicker.openCurrentFolder(this.update, "Image");
            }
        },
        remove: function () {
            this.model.imgBody.id = null;
            this.model.imgBody.media = null;
        },
        update: function (media) {
            if (media.type === "Image") {
                this.model.imgBody.id = media.id;
                this.model.imgBody.media = {
                    id: media.id,
                    folderId: media.folderId,
                    type: media.type,
                    filename: media.filename,
                    contentType: media.contentType,
                    publicUrl: media.publicUrl,
                };
                // Tell parent that title has been updated
                this.$emit('update-title', {
                    uid: this.uid,
                    title: this.model.imgBody.media.filename
                });
            } else {
                console.log("No image was selected");
            }
        },
        selectAspect: function (val) {
            this.model.aspect.value = val;
        },
        isAspectSelected(val) {
            return this.model.aspect.value === val;
        }
    },
    computed: {
        isImgEmpty: function (e) {
            return this.model.imgBody.media == null;
        },
        isHtmlEmpty: function () {
            return piranha.utils.isEmptyHtml(this.model.htmlBody.value);
        },
        mediaUrl: function () {
            if (this.model.imgBody.media != null) {
                return piranha.utils.formatUrl(this.model.imgBody.media.publicUrl);
            } else {
                return piranha.utils.formatUrl("~/manager/assets/img/empty-image.png");
            }
        },
        iconUrl: function () {
            if (this.model.aspect.value > 0) {
                if (this.model.aspect.value === 1 || this.model.aspect.value === 3) {
                    return piranha.utils.formatUrl("~/manager/assets/img/icons/img-landscape.svg");
                } else if (this.model.aspect.value == 2) {
                    return piranha.utils.formatUrl("~/manager/assets/img/icons/img-portrait.svg");
                } else if (this.model.aspect.value == 4) {
                    return piranha.utils.formatUrl("~/manager/assets/img/icons/img-square.svg");
                }
            }
            return null;
        }
    },
    mounted: function () {
        piranha.editor.addInline(this.uid, this.toolbar, this.onChange);
        this.model.getTitle = function () {
            if (this.model.imgBody.media != null) {
                return this.model.imgBody.media.filename;
            } else {
                return "No image selected";
            }
        };
    },
    beforeDestroy: function () {
        piranha.editor.remove(this.uid);
    },
    template:
       
        "<div class='block-body has-media-picker rounded' :class='{ empty: isImgEmpty }'>" + 
        "   <div class='image-block'>" +
        "       <img class='rounded' :src='mediaUrl'>" +
        "       <div class='media-picker'>" +
        "           <div class='btn-group float-right'>" +
        "               <button :id='uid + \"-aspect\"' class='btn btn-info btn-aspect text-center' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>" +
        "                <i v-if='model.aspect.value === 0' class='fas fa-cog'></i>" +
        "                   <img v-else :src='iconUrl'>" +
        "               </button>" +
        "               <div class='dropdown-menu aspect-menu' :aria-labelledby='uid + \"-aspect\"'>" +
        "                   <label class='mb-0'>{{ piranha.resources.texts.aspectLabel }}</label>" +
        "                   <div class='dropdown-divider'></div>" +
        "                   <a v-on:click.prevent='selectAspect(0)' class='dropdown-item' :class='{ active: isAspectSelected(0) }' href='#'>" +
        "                       <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-original.svg\")'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
        "                   </a>" +
        "                   <a v-on:click.prevent='selectAspect(1)' class='dropdown-item' :class='{ active: isAspectSelected(1) }' href='#'>" + 
        "                       <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-landscape.svg\")'><span>{{ piranha.resources.texts.aspectLandscape }}</span>" +
        "                   </a>" +
        "                   <a v-on:click.prevent='selectAspect(2)' class='dropdown-item' :class='{ active: isAspectSelected(2) }' href='#'>" +
        "                       <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-portrait.svg\")'><span>{{ piranha.resources.texts.aspectPortrait }}</span>" +
        "                   </a>" + 
        "                   <a v-on:click.prevent='selectAspect(3)' class='dropdown-item' :class='{ active: isAspectSelected(3) }' href='#'>" +
        "                       <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-landscape.svg\")'><span>{{ piranha.resources.texts.aspectWidescreen }}</span>" +
        "                   </a>" + 
        "                   <a v-on:click.prevent='selectAspect(4)' class='dropdown-item' :class='{ active: isAspectSelected(4) }' href='#'>" +
        "                       <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-square.svg\")'><span>{{ piranha.resources.texts.aspectSquare }}</span>" +
        "                   </a>" + 
        "               </div>" +
        "               <button v-on:click.prevent='select' class='btn btn-primary text-center'>" +
        "                   <i class='fas fa-plus'></i>" +
        "               </button>" +
        "               <button v-on:click.prevent='remove' class='btn btn-danger text-center'>" +
        "                   <i class='fas fa-times'></i>" +
        "               </button>" +
        "           </div>" +
        "           <div class='card text-left'>" +
        "               <div class='card-body' v-if='isImgEmpty'>" +
        "                   &nbsp;" +
        "               </div>" +
        "               <div class='card-body' v-else>" +
        "                   {{ model.imgBody.media.filename }}" +
        "               </div>" +
        "           </div>" +
        "       </div>" +
        "   </div>" +
        "   <br />" +
        "   <div class='html-block'>" +
        "       <div class='block-body border rounded' :class='{ empty: isHtmlEmpty }' >" + 
        "           <div contenteditable='true' :id='uid' v-html='htmlBody' v-on:blur='onBlur'></div> " + 
        "       </div>" +
        "   </div>" +
        "</div>"
});

我仍然很想得到一些确认,我没有做所有这些不必要的事情。如果有更好的方法,请不要犹豫。