首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET MVC5 文章發布完整實作:KindEditor 整合、附件上傳、縮圖生成、未用附件清理(第8篇)

     編輯:ASP.NET 基礎

功能架構

文章發布包含四大子系統:

  1. 富文本編輯:KindEditor 整合圖片/檔案上傳、檔案管理
  2. 附件管理:上傳儲存、資料庫記錄、列表查詢、刪除
  3. 縮圖生成:首頁圖片自動裁切縮放
  4. 生命週期控制:文章儲存時歸屬已用附件、清理未用附件

一、KindEditor 整合

1. 下載與部署

官網 下載最新版,解壓後將 kindeditor 資料夾複製至專案 Scripts/ 目錄。

KindEditor 目錄結構

2. 編輯頁面引用

@section Scripts {
    <script src="~/Scripts/KindEditor/kindeditor-min.js"></script>
    <script>
        KindEditor.ready(function (K) {
            // 內容編輯器
            window.editor = K.create('#Content', {
                height: '500px',
                uploadJson: '@Url.Action("Upload", "Attachment")',
                fileManagerJson: '@Url.Action("FileManagerJson", "Attachment")',
                allowFileManager: true,
                formatUploadUrl: false
            });

            // 首頁圖片選擇器
            var editor2 = K.editor({
                fileManagerJson: '@Url.Action("FileManagerJson", "Attachment")'
            });
            K('#btnPicSelect').click(function () {
                editor2.loadPlugin('filemanager', function () {
                    editor2.plugin.filemanagerDialog({
                        viewType: 'VIEW', dirName: 'image',
                        clickFn: function (url, title) {
                            $.ajax({
                                type: "POST",
                                url: '@Url.Action("CreateThumbnail", "Attachment")',
                                data: { originalPicture: url },
                                async: false,
                                success: function (data) {
                                    if (data) {
                                        K('#CommonModel_DefaultPicUrl').val(data);
                                        K('#imgPreview').attr("src", data);
                                    } else alert("縮圖生成失敗");
                                    editor2.hideDialog();
                                }
                            });
                        }
                    });
                });
            });
        });
    </script>
}

3. 發布表單 View

@model Ninesky.Models.Article

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        <h4>發布文章</h4> <hr />
        @Html.ValidationSummary(true)

        <div class="form-group">
            @Html.LabelFor(m => m.CommonModel.CategoryID, new { @class = "control-label col-sm-2" })
            <div class="col-sm-10">
                <input id="CommonModel_CategoryID" name="CommonModel.CategoryID" 
                       data-options="url:'@Url.Action("JsonTree", "Category", new { model="Article" })'" 
                       class="easyui-combotree" style="height:34px;width:280px;" />
                @Html.ValidationMessageFor(m => m.CommonModel.CategoryID)
            </div>
        </div>

        @* Title, Author, Source, Intro 欄位 *@

        <div class="form-group">
            @Html.LabelFor(m => m.Content, new { @class = "control-label col-sm-2" })
            <div class="col-sm-10">
                @Html.TextAreaFor(m => m.Content, new { @class = "form-control", id = "Content", rows = 20 })
                @Html.ValidationMessageFor(m => m.Content)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(m => m.CommonModel.DefaultPicUrl, new { @class = "control-label col-sm-2" })
            <div class="col-sm-10">
                <img id="imgPreview" class="thumbnail" src="" style="max-width:160px;" />
                @Html.HiddenFor(m => m.CommonModel.DefaultPicUrl)
                <a id="btnPicSelect" class="easyui-linkbutton">選擇首頁圖片…</a>
            </div>
        </div>

        <div class="form-group">
            <div class="col-sm-offset-2 col-sm-10">
                <input type="submit" value="發布" class="btn btn-primary" />
            </div>
        </div>
    </div>
}

發布頁面效果

二、附件上傳控制器

AttachmentController.cs

public class AttachmentController : Controller
{
    private readonly IAttachmentService _attachmentService = new AttachmentService();

    // 上傳端點:供 KindEditor 呼叫
    public ActionResult Upload()
    {
        var config = WebConfigurationManager.OpenWebConfiguration("~")
            .GetSection("UploadConfig") as UploadConfig;

        HttpPostedFileBase file = Request.Files["imgFile"];
        if (file == null) return Json(new { error = 1, message = "請選擇檔案" });

        // 驗證副檔名、大小...
        string ext = Path.GetExtension(file.FileName).ToLower();
        string dir = Request.QueryString["dir"] ?? "image";
        string saveDir = Server.MapPath($"~/{config.Path}/{dir}/{DateTime.Now:yyyy-MM}/");
        Directory.CreateDirectory(saveDir);

        string fileName = $"{DateTime.Now:yyyyMMdd_HHmmss}{ext}";
        string savePath = Path.Combine(saveDir, fileName);
        string virtualPath = $"~/{config.Path}/{dir}/{DateTime.Now:yyyy-MM}/{fileName}";

        file.SaveAs(savePath);

        // 寫入資料庫
        _attachmentService.Add(new Attachment
        {
            FilePath = virtualPath,
            Extension = ext.TrimStart('.'),
            Owner = User.Identity.Name,
            UploadDate = DateTime.Now,
            Type = dir
        });

        return Json(new { error = 0, url = Url.Content(virtualPath) });
    }

    // 檔案管理列表:供 KindEditor fileManagerJson 呼叫
    public ActionResult FileManagerJson(int? id, string dir)
    {
        IQueryable query = _attachmentService.FindList(id, User.Identity.Name, dir, id == null);
        var list = query.Select(a => new AttachmentManagerViewModel
        {
            is_dir = false,
            has_file = false,
            filesize = new FileInfo(Server.MapPath(a.FilePath)).Length,
            is_photo = a.Type == "image",
            filetype = a.Extension,
            filename = Url.Content(a.FilePath),
            datetime = a.UploadDate.ToString("yyyy-MM-dd HH:mm:ss")
        }).ToList();

        return Json(new
        {
            moveup_dir_path = "",
            current_dir_path = "",
            current_url = "",
            total_count = list.Count,
            file_list = list
        }, JsonRequestBehavior.AllowGet);
    }

    // 縮圖生成
    public ActionResult CreateThumbnail(string originalPicture)
    {
        if (originalPicture.Contains("_s")) return Json(originalPicture); // 已是縮圖

        string thumbPath = originalPicture.Insert(originalPicture.LastIndexOf('.'), "_s");
        if (PictureHelper.CreateThumbnail(Server.MapPath(originalPicture), Server.MapPath(thumbPath), 160, 120))
        {
            // 縮圖也記錄到資料庫
            _attachmentService.Add(new Attachment
            {
                FilePath = "~" + thumbPath,
                Extension = Path.GetExtension(thumbPath).TrimStart('.'),
                Owner = User.Identity.Name,
                Type = "image",
                UploadDate = DateTime.Now
            });
            return Json(thumbPath);
        }
        return Json((string)null);
    }
}

三、縮圖生成核心類

Common/PictureHelper.cs

public static class PictureHelper
{
    public static bool CreateThumbnail(string sourcePath, string destPath, int width, int height)
    {
        using (Image src = Image.FromFile(sourcePath))
        {
            // 計算裁切區域(居中裁切填滿)
            double srcRatio = (double)src.Width / src.Height;
            double destRatio = (double)width / height;
            RectangleF srcRect;

            if (srcRatio > destRatio) // 來源較寬
            {
                float h = src.Height;
                float w = h * destRatio;
                srcRect = new RectangleF((src.Width - w) / 2, 0, w, h);
            }
            else // 來源較高
            {
                float w = src.Width;
                float h = w / destRatio;
                srcRect = new RectangleF(0, (src.Height - h) / 2, w, h);
            }

            using (Bitmap bmp = new Bitmap(width, height))
            using (Graphics g = Graphics.FromImage(bmp))
            {
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.SmoothingMode = SmoothingMode.HighQuality;
                g.Clear(Color.Transparent);
                g.DrawImage(src, new RectangleF(0, 0, width, height), srcRect, GraphicsUnit.Pixel);
                bmp.Save(destPath, ImageFormat.Jpeg);
            }
        }
        return true;
    }
}

四、文章儲存與附件生命週期

ArticleController - Add (POST)

[HttpPost]
[ValidateInput(false)]       // 允許 HTML 內容
[ValidateAntiForgeryToken]
public ActionResult Add(Article article)
{
    if (ModelState.IsValid)
    {
        // 固定欄位
        article.CommonModel.Hits = 0;
        article.CommonModel.Inputer = User.Identity.Name;
        article.CommonModel.Model = "Article";
        article.CommonModel.ReleaseDate = DateTime.Now;
        article.CommonModel.Status = 99; // 已發布

        article = _articleService.Add(article);

        if (article.ArticleID > 0)
        {
            // 附件歸屬與清理
            var attService = new AttachmentService();
            var uploads = attService.FindList(null, User.Identity.Name, string.Empty).ToList();

            foreach (var att in uploads)
            {
                string fileUrl = Url.Content(att.FilePath);
                bool used = (article.CommonModel.DefaultPicUrl?.Contains(fileUrl) ?? false) 
                         || article.Content.Contains(fileUrl);

                if (used)
                {
                    att.ModelID = article.ModelID; // 歸屬
                    attService.Update(att);
                }
                else
                {
                    // 刪除實體檔與資料庫記錄
                    System.IO.File.Delete(Server.MapPath(att.FilePath));
                    attService.Delete(att);
                }
            }
            return View("AddSuccess", article);
        }
    }
    return View(article);
}

KindEditor 上傳對話框

檔案管理器

首頁圖片選擇與縮圖

附件清理邏輯

五、關鍵設計總結

子系統關鍵技術解決問題
富文本編輯KindEditor + uploadJson/fileManagerJson圖片/檔案上傳、線上管理、插入編輯器
附件上傳HttpPostedFileBase + 配置驗證 + 資料庫記錄型別/大小限制、歸屬追蹤、防惡意上傳
縮圖生成Graphics.DrawImage + 居中裁切 + 高品質插值固定尺寸、不變形、自動聚焦主體
生命週期文章儲存時掃描 ModelID=null 附件,比對 URL 歸屬/清理避免孤兒檔案佔用空間、資料庫垃圾資料

已知限制與改進方向

  • KindEditor 批量上傳依賴 SWFUpload,Flash 不傳送 Cookie 導致驗證失敗 → 改用 HTML5 上傳插件或升級編輯器
  • 縮圖同步生成阻塞請求 → 佇列化處理
  • 附件儲存擴展至雲端存儲 → 實作 IFileStorage 介面

結語

本篇將文章發布所需的編輯、上傳、縮圖、清理串聯成完整鏈路。核心在於 ModelID=null 作為「暫存態」標記,文章儲存時統一歸屬與清理,確保資料一致性與儲存空間效率。KindEditor 雖屬經典專案,但其 uploadJson/fileManagerJson 協議設計清晰,易於對接自建後端。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved