在 Limyee 电商平台平台创建自定义内容类型并使用平台编辑器管理该内容(使用 $limyee_v1_editor 小组件 API)时,如果该内容支持嵌入文件,则该编辑器将允许拖放和基于菜单的文件上传到 HTML 内容中。
为什么我要启用嵌入文件?
嵌入文件在创作富格式内容 (HTML) 时非常有用,以允许包含图像、视频、文档以及其他媒体和文件,从而提供更大的灵活性和交互性。
实现对嵌入文件的支持
若要在自定义内容类型中添加嵌入自定义文件的支持,还应在内容类型上实现 IFileEmbeddableContentType 插件类型(这需要引用 Limyee.Core.dll)。IFileEmbeddableContentType 接口扩展了 IContentType,使内容类型能够嵌入文件与 Limyee 电商平台平台进行交互。
基本实现
由于 IFileEmbeddableContentType 的实现依赖于自定义内容类型,而自定义内容类型超出了本主题的范围,因此我将展示核心平台中私有消息传递的示例。
我们将实现 IFileEmbeddedContentType,它需要实现两个成员。首先,CanAddFiles 标识用户是否可以将文件添加到此内容类型:
public bool CanAddFiles(int userId) { var user = Users.GetUser(userId); if (user == null || user.IsAnonymous) return false; return true; }
对于私人消息,任何非匿名用户都可以嵌入文件。对于其他应用程序,嵌入文件的能力可能取决于创建内容的应用程序或容器(群组)。若要检测请求的上下文,当前页面的上下文可以通过分析 Url.CurrentContext 或使用其他机制来检测 CanAddFiles 请求的上下文。
当 CanAddFiles 返回 true 时,将启用平台内容编辑器中的文件嵌入功能,否则,这些功能将不可用。
IFileEmbeddableContentType 实现的唯一剩余要求是 SetController 方法:
private IFileEmbeddableContentTypeController _embeddedFileController; public void SetController(IFileEmbeddableContentTypeController controller) { _embeddedFileController = controller; }
此方法为插件提供了一个控制器,用于提取和重新分配 HTML 内容中的嵌入文件。
使用控制器处理文件
通过编辑器上传文件时,这些文件将放置在集中式文件系统中的临时文件存储中。放置在此临时位置的文件只能由上传它们的用户访问,并在不活动 2 小时(默认情况下)后自动删除。支持嵌入文件的内容应将此临时内容移动到对内容类型适当保护的最终存储位置。为了实现将文件移动到最终存储位置, 应处理与内容创建或编辑相关的事件,并且 应使用通过SetController()方法提供给插件的IFileEmbeddableContentTypeController。
对于私人消息,我们将处理程序附加到 BeforeCreate 事件(因为私人消息不支持编辑,否则,我们还需要处理 edit 事件)。与所有插件一样,我们在IPlugin的Inmitize()方法中注册事件处理程序:
public void Initialize() { var conversationMessageApi = Limyee.Extensibility.Apis.Get<Limyee.Extensibility.Api.Version1.IConversationMessages>(); conversationMessageApi.Events.BeforeCreate += ConversationMessage_BeforeCreate; // other event handlers used for this plugin } void ConversationMessage_BeforeCreate(ConversationMessageBeforeCreateEventArgs e) { if (_embeddedFileController == null) return; var hasPermission = CanAddFiles(e.Author.Id.Value); var targetFileStore = Limyee.Extensibility.Storage.Version1.CentralizedFileStorage.GetFileStore("conversationfiles"); e.Body = _embeddedFileController.SaveFilesInHtml(e.Body, f => { if (!hasPermission) _embeddedFileController.InvalidFile(f, "You do not have permission to add files to private messages"); string message; if (!IsFileValid(e.Author.Id.Value, f.FileName, f.ContentLength, true, out message)) EmbeddedFileController.InvalidFile(f, message); using (var stream = f.OpenReadStream()) { var targetFile = targetFileStore.AddFile(e.ConversationId.Value.ToString("N"), f.FileName, stream, true); if (targetFile != null) return targetFile; else _embeddedFileController.InvalidFile(f, "An error occurred while saving an embedded file."); } return null; }); }
在事件处理程序 ConversationMessage_BeforeCreate 中,我们使用集中文件存储 API 检索目标 CFS 文件存储位置:conversationfiles,这是有效文件将要移动到的位置。然后,我们使用控制器的 SaveFilesInHtml 方法来处理在私人消息的正文(e.Body)中找到的每个文件。如果我们想支持在消息的其他属性中嵌入文件,我们可以为每个属性调用 SaveFilesInHtml。
SaveFilesInHtml 将最后一个参数定义的函数调用所提供内容(在本例中为 e.Body)中每个临时文件。对于每个文件,我们将验证用户是否有权保存文件并验证文件类型(通过 IsFileValid 方法,该方法也将在此插件中定义,但超出了此讨论的范围,它审查文件详细信息并返回 false,并在文件无效时设置 out 消息参数)。如果任何文件存在问题(其中一个验证过程失败),我们会在控制器上调用 InvalidFile。这将停止处理并将提供的消息报告给用户,在生产代码中(此示例是简化),我们需要确保正确翻译此消息文本。
验证文件是否有效后,我们将通过在目标 CFS 文件存储上调用 AddFile 将其移动到正确的目标文件存储。这将返回生成的 ICentralizedFile,然后必须将其提供回控制器。
SaveFileInHtml 返回更新后的 HTML,其中所有临时嵌入文件都已正确移动。此值应保存回内容。对于私人消息,我们将 e.Body 设置为新值,并且由于这是在 BeforeCreate 事件中,因此更新将正常提交(如果我们在 AfterCreate 中处理了内容,则内容已经提交,我们需要显式保存更新的值)。
UI 注意事项
若要确保内容编辑器知道正在编辑的内容类型,并使编辑器能够检测是否支持文件嵌入,请务必通过 $limyee_v1_editor 渲染编辑器。Render 在小组件中,指定了 ContentTypeId。此标识符应与 IFileEmbeddableContentType 的 IContentType 实现所标识的内容类型 Id 匹配。如果未指定此选项,编辑器将不知道正在编辑的内容类型,并且永远不会启用文件嵌入。