angular-file-upload组件上传Demo

效果图

1、前台html

<style>
    .radiogroup {
        font-size: 12px;
        width: 100px;
        min-height: 20px !important;
        height: 23px !important;
        float: left;
        padding-top: 0;
        padding-bottom: 0;
        margin-top: 0;
    }

    div.group-control .group-control-data {
        padding-top: 0px;
        vertical-align: bottom;
    }

    .property-grid-input-group input {
        font-size: 12px;
        vertical-align: text-bottom;
        margin-top: 0;
    }

    .property-grid-span-group-block {
        width: 100%;
    }

    .property-grid-input-group input {
        font-size: 12px;
    }

    /* sit-radio样式 begin */

    div.group-control .group-control-data span {
        vertical-align: bottom;
    }

    div.group-control .group-control-data input[type="radio"] {
        padding: 0;
        vertical-align: text-bottom;
    }

    .sit-radio div.group-control .group-control-data {
        padding-top: 2px;
    }

    /* sit-radio样式 end */

    .label-property-grid-control-readonly {
        display: block;
        font-size: 12px;
        color: #363636;
        background-color: #f6f6f6;
    }

    a,
    a:link,
    a:hover,
    a:active,
    a:visited {
        text-decoration: none;
    }

    .FlagRequired {
        color: red;
        padding: 3px;
        font-size: 14px;
        line-height: 22px;
    }

    .row .col_text {
        text-align: right;
    }

    .canvas .property-area-container div.command-bar-side-panel {
        padding-bottom: 40px;
    }

    .canvas .property-area-container>div.content .side-panel-property-area-body {
        overflow-x: hidden;
    }

    canvas {
        background-color: #f3f3f3;
        -webkit-box-shadow: 3px 3px 3px 0 #e3e3e3;
        -moz-box-shadow: 3px 3px 3px 0 #e3e3e3;
        box-shadow: 3px 3px 3px 0 #e3e3e3;
        border: 1px solid #c3c3c3;
        margin: 6px 0 0 6px;
    }

    audio,
    canvas,
    video {
        display: inline-block;
    }

    div.progress {
        height: 20px;
        border-radius: 0;
        margin-top: 0px;
        background-color: #ffffff;
    }

    a,
    a:link,
    a:hover,
    a:active,
    a:visited {
        text-decoration: none;
    }

    .selectBox .label-property-grid-control-readonly {
        display: block;
    }


    /** mask **/

    .flyover .mask {
        top: 0;
        left: 0;
        position: fixed;
        width: 100%;
        height: 100%;
        opacity: 0.5;
        background: black;
        z-index: 1049;
    }

    .flyover .alert {
        left: 50%;
        top: 50%;
        position: fixed;
        z-index: 1050;
    }

    .property-grid-input-group>textarea {
        width: 101%;
        height: 120px;
    }

    .boxUploadTitle .uploadTitle {
        margin-left: 10px;
        font-size: 16px;
    }

    .boxUploadTitle .num {
        font-size: 12px;
        margin-left: 10px;
    }

    .boxUploadTitle a {
        margin-left: 10px;
        height: 30px;
        line-height: 30px;
        position: relative;
        cursor: pointer;
        overflow: hidden;
        display: inline-block;
    }

    .boxUploadTitle a input {
        position: absolute;
        left: 0;
        top: 0;
        opacity: 0;
    }

    .validator-control[disabled] {
        color: #666666;
    }

    th {
        font-size: 15px;
    }
</style>
<div class="command-bar-side-panel">
    <button ng-click="vm.cancel()" class="btn side-panel-button">返回</button> 
    <button ng-click="vm.save()" class="btn side-panel-button" ng-show="vm.validInputs">保存</button>
</div>
<div class="side-panel-property-area-body" style="padding-right: 10px;">
    <div class="col-md-54 col-lg-54">
        <div class="row" style="margin-top: 10px;">
            <div class="col-md-40" style="clear: both;">

                <sit-property-grid sit-id="view_form" sit-layout="Horizontal" sit-type="Fixed" sit-mode="edit" sit-columns="2">
                    <sit-property sit-id="EquipmentCategoryCode" sit-name="EquipmentCategoryCode" sit-widget="sit-text" sit-value="vm.currentItem.EquipmentCategoryCode" sit-read-only="false" sit-validation="{required: true}">设备型号:</sit-property>
                    <sit-property sit-id="archives_fileType" sit-name="archives_fileType" sit-widget="sit-select" sit-value="vm.currentItem.archives_fileType.value" sit-options="vm.archives_fileType.options" sit-validation="{required: true}"
                        sit-to-display="'ItemName'" sit-to-keep="'ItemValue'">
                        文件类型:
                    </sit-property>
                    <sit-property sit-id="EquipmentName" sit-name="EquipmentName" sit-widget="sit-text" sit-value="vm.currentItem.EquipmentName" sit-read-only="true">设备名称:</sit-property>
                    <sit-property sit-id="EquipmentCode" sit-name="EquipmentCode" sit-widget="sit-text" sit-value="vm.currentItem.EquipmentCode" sit-read-only="true">设备编码:</sit-property>
                    <sit-property sit-id="Note" sit-name="Note"  sit-widget="sit-text" sit-value="vm.currentItem.Note" sit-read-only="false">备注:</sit-property>
                </sit-property-grid>
            </div>
            <div class="col-md-40" style="margin-top: 1px">
                <div class="boxUploadTitle" style="margin-bottom:10px;">
                    <!--<span class="uploadTitle">上传文件</span>-->
                    <!-- Example: nv-file-select="" uploader="{Object}" options="{Object}" filters="{String}" -->
                    <a href="javascript:void(0);" ng-click="vm.AddButtonHandler()" class="btn btn-primary">
                        <i class="ace-icon fa fa-plus"></i>
                        <input type="file" accept=".jpg,.png,.jpeg,.bmp,.gif,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf" nv-file-select="" uploader="uploader"
                            multiple />选择文件
                    </a>
                    <span class="num">数量: {{ uploader.queue.length }}</span>
                </div>
                <table class="table">
                    <thead>
                        <tr>
                            <th>缩略图</th>
                            <th>文件名</th>
                            <th ng-show="uploader.isHTML5">大小</th>
                            <th ng-show="uploader.isHTML5" nowrap style="display: none;">上传进度</th>
                            <th nowrap style="display: none;">上传状态</th>
                            <th style="display: none;">操作</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr ng-repeat="item in uploader.queue">
                            <td>
                                <!-- Image preview -->
                                <!--auto height-->
                                <!--<div ng-thumb="{ file: item.file, width: 80 }"></div>-->
                                <!--auto width-->
                                <div ng-show="uploader.isHTML5" ng-thumb="{ file: item._file, height: 60 }"></div>
                                <!--fixed width and height -->
                                <!--<div ng-thumb="{ file: item.file, width: 80, height: 80 }"></div>-->
                            </td>
                            <td>
                                <strong>{{ item._file.name }}</strong>
                            </td>
                            <td ng-show="uploader.isHTML5" nowrap>{{ item.file.size/1024/1024|number:2 }} MB</td>
                            <td ng-show="uploader.isHTML5" style="display: none;">
                                <div class="progress" style="margin-bottom: 0;">
                                    <div class="progress-bar" role="progressbar" ng-style="{ 'width': item.progress + '%' }"></div>
                                </div>
                            </td>
                            <td class="text-center" style="display: none;">
                                <span ng-show="item.isSuccess">
                                    <i class="glyphicon glyphicon-ok"></i>
                                </span>
                                <span ng-show="item.isCancel">
                                    <i class="glyphicon glyphicon-ban-circle"></i>
                                </span>
                                <span ng-show="item.isError">
                                    <i class="glyphicon glyphicon-remove"></i>
                                </span>
                            </td>
                            <td nowrap >
                                <button style="display: none;" type="button" class="btn btn-success btn-xs" ng-click="item.upload()" ng-disabled="item.isReady || item.isUploading || item.isSuccess">
                                    <span class="glyphicon glyphicon-upload"></span> 上传
                                </button>
                                <button style="display: none;" type="button" class="btn btn-warning btn-xs" ng-click="item.cancel()" ng-disabled="!item.isUploading">
                                    <span class="glyphicon glyphicon-ban-circle"></span> 取消
                                </button>
                                <button type="button" class="btn btn-danger btn-xs" ng-click="item.remove()">
                                    <span class="glyphicon glyphicon-trash"></span> 删除
                                </button>
                            </td>
                        </tr>
                    </tbody>
                </table>

                <!-- <div>
                    <div>
                        上传序列进度:
                        <div class="progress" style="">
                            <div class="progress-bar" role="progressbar" ng-style="{ 'width': uploader.progress + '%' }"></div>
                        </div>
                    </div>
                    <button type="button" class="btn btn-success btn-s" ng-click="uploader.uploadAll()" ng-disabled="!uploader.getNotUploadedItems().length">
                        <span class="glyphicon glyphicon-upload"></span> 上传所有
                    </button>
                    <button type="button" class="btn btn-warning btn-s" ng-click="uploader.cancelAll()" ng-disabled="!uploader.isUploading">
                        <span class="glyphicon glyphicon-ban-circle"></span> 取消 所有
                    </button>
                    <button type="button" class="btn btn-danger btn-s" ng-click="uploader.clearQueue()" ng-disabled="!uploader.queue.length">
                        <span class="glyphicon glyphicon-trash"></span> 删除 所有
                    </button>
                </div> -->

            </div>

        </div>


    </div>

</div>
<div class="loading" ng-show="vm.busy">
    <i class="fa fa-spin fa-spinner fa-lg"></i>
    loading ...
</div>

2、前台JS

(function () {
    'use strict';
    angular.module('Siemens.SimaticIT.EquipmentApp.EquipmentAccount').config(AddScreenStateConfig);

    AddScreenController.$inject = ['Siemens.SimaticIT.EquipmentApp.EquipmentAccount.EquipmentAccount.service', '$state',
        '$stateParams', 'common.base', '$filter', '$scope', 'FileUploader', 'CCS.CommonApp.common.service','$rootScope'];
    function AddScreenController(dataService, $state, $stateParams, common, $filter, $scope, FileUploader, commonService,$rootScope) {

        var self = this;
        var sidePanelManager, backendService, propertyGridHandler, uploader;
        activate();
        function activate() {
            init();
            registerEvents();

            sidePanelManager.setTitle('上传文档');
            sidePanelManager.open('e');
        }

        function init() {
            sidePanelManager = common.services.sidePanel.service;
            backendService = common.services.runtime.backendService;

            //Initialize Model Data
            self.currentItem = angular.copy($stateParams.selectedItem);
            console.log("self.currentItem -------------------- " + JSON.stringify(self.currentItem));
            self.validInputs = false;
            //Expose Model Methods
            self.save = save;
            self.cancel = cancel;
            self.addButtonHandler = addButtonHandler;
            //文件类型
            self.archives_fileType = {
                options: [],
                value: {},
                keep: 'ItemValue',//ItemValue
                label: 'ItemName'

            };
            archives_fileType_dic();
            uploader = $scope.uploader = new FileUploader({
                url: commonService.getMesApiAddress("equipment") + 'UploadFile/Upload'
              });

            // FILTERS
            // uploader.filters.push({
            //     name: 'imageFilter',
            //     fn: function (item /*{File|FileLikeObject}*/, options) {
            //         var type = '|' + item.type.slice(item.type.lastIndexOf('/') + 1) + '|';
            //         return '|jpg|png|jpeg|bmp|gif|doc|docx|xls|xlsx|ppt|pptx|pdf|'.indexOf(type) !== -1;
            //     }
            // });

            // uploader.filters.push({
            //     name: 'syncFilter',
            //     fn: function(item /*{File|FileLikeObject}*/, options) {
            //         console.log('syncFilter');
            //         return this.queue.length < 10;
            //     }
            // });

            // an async filter
            uploader.filters.push({
                name: 'asyncFilter',
                fn: function (item /*{File|FileLikeObject}*/, options, deferred) {
                    console.log('asyncFilter');
                    setTimeout(deferred.resolve, 1e3);
                }
            });

            // CALLBACKS
            uploader.onWhenAddingFileFailed = function (item /*{File|FileLikeObject}*/, filter, options) {
                console.info('onWhenAddingFileFailed', item, filter, options);
            };
            uploader.onAfterAddingFile = function (fileItem) {

                if (self.currentItem.archives_fileType) {
                    var myPostDatas = {
                        EquipmentCategoryCode: self.currentItem.EquipmentCategoryCode,
                        FileType: self.currentItem.archives_fileType.value.ItemValue,
                        EquipmentName: self.currentItem.EquipmentName,
                        EquipmentCode: self.currentItem.EquipmentCode,
                        Note: self.currentItem.Note,
                        userName:commonService.getLoginUser().loginName
                    };
                    fileItem.formData = [myPostDatas];
                    console.log("formData-------------" + JSON.stringify(fileItem.formData));
                }
                else {
                    backendService.genericError("请先选择【文件类型】", "提示");
                } 
            };
            uploader.onAfterAddingAll = function (addedFileItems) {
                console.info('onAfterAddingAll', addedFileItems);
            };
            uploader.onBeforeUploadItem = function (item) {
                console.info('onBeforeUploadItem', item);
            };
            uploader.onProgressItem = function (fileItem, progress) {
                console.info('onProgressItem', fileItem, progress);
            };
            uploader.onProgressAll = function (progress) {
                console.info('onProgressAll', progress);
            };
            uploader.onSuccessItem = function (fileItem, response, status, headers) {
                debugger;
                console.info('onSuccessItem------------------', fileItem, response, status, headers);
                if (response && response.success) {
                    fileItem._file.filePath = response.ResultData;
                    //触发父窗口的事件
                    $rootScope.$emit('to-parent', 'parent'); 
                }
                else { 
                    backendService.genericError(response.returnMsg, "提示");
                }
            };
            uploader.onErrorItem = function (fileItem, response, status, headers) { 
                console.info('onErrorItem-----------------', fileItem, response, status, headers);
            };
            uploader.onCancelItem = function (fileItem, response, status, headers) {
                console.info('onCancelItem', fileItem, response, status, headers);
            };
            uploader.onCompleteItem = function (fileItem, response, status, headers) {
                console.info('onCompleteItem', fileItem, response, status, headers);
            };
            uploader.onCompleteAll = function () {
                console.info('onCompleteAll');
            }; 
        }
        //文件类型
        function archives_fileType_dic() {
            var url = commonService.getDataItemDuatil("archives_fileType_dic").then(function (res) {
                var resultData = res.data.resultData;
                self.archives_fileType.options = resultData;
            });
        }
        function addButtonHandler() { }
        function registerEvents() {
            $scope.$on('sit-property-grid.validity-changed', onPropertyGridValidityChange);
        }

        function save() {
            var queueCount = uploader.queue.length;
            if (queueCount > 0) {
                //提交所有文档
                uploader.uploadAll();
                sidePanelManager.close();
            }
            else {
                backendService.genericError('请选择要上传的文档', "提示");
            }
        }

        function cancel() {
            sidePanelManager.close();
            $state.go('^');
        }

        function onSaveSuccess(data) {
            sidePanelManager.close();
            $state.go('^', {}, { reload: true });
        }

        function onPropertyGridValidityChange(event, params) {
            self.validInputs = params.validity;
        }
    }

    AddScreenStateConfig.$inject = ['$stateProvider'];
    function AddScreenStateConfig($stateProvider) {
        var screenStateName = 'home.Siemens_SimaticIT_EquipmentApp_EquipmentAccount_EquipmentAccount';
        var moduleFolder = 'Siemens.SimaticIT.EquipmentApp/modules/EquipmentAccount'; 
        var state = {
            name: screenStateName + '.archives',
            url: '/archives',
            views: {
                'property-area-container@': {
                    templateUrl: moduleFolder + '/EquipmentAccount-archives.html',
                    controller: AddScreenController,
                    controllerAs: 'vm'
                }
            },
            data: {
                title: 'add'
            },
            params: {
                selectedItem: null,
            }
        };
        $stateProvider.state(state);
    }
}());

3、后台

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ALP.Application.WebApi.Controllers.API;
using ALP.WebApi.Filter;
using ALP.WebApi.Models;
using ALP.Application.Busines.SystemManage;
using ALP.Application.Entity.SystemManage;
using System.Dynamic;
using ALP.Util;
using ALP.Util.WebControl;
using System.Web;
using System.IO;
using System.Text.RegularExpressions;
using System.Text;
using ALP.Application.Busines.EquipmentManage;
using ALP.Application.Entity.EquipmentManage;

namespace ALP.Application.WebApi.Controllers.EquipmentManage
{
    public class FileTypeModel
    {
        public string ItemName { set; get; }
        public string ItemValue { set; get; }
    }
    public class UploadFileController : ApiBaseController
    {
        // GET: UploadFile
        [HttpGet]
        [Route("UploadFile/test")]
        public HttpResponseMessage test()
        {
            HttpResponseMessage result1 = new HttpResponseMessage
            {
                //Content = new StringContent(BuildReturn(result, detailInfo, PUUID), Encoding.GetEncoding("UTF-8"), "application/json")
                Content = new StringContent("测试成功  0001" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), Encoding.GetEncoding("UTF-8"), "application/json")
            };
            return result1;
        }
        /// <summary>
        /// 文件上传
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        [Route("UploadFile/Upload")]
        public HttpResponseMessage Upload()
        {

            //型号
            string EquipmentCategoryCode = HttpContext.Current.Request.Form["EquipmentCategoryCode"].Replace("undefined", "");
            string userName  = HttpContext.Current.Request.Form["userName"].Replace("undefined", "");
            //文件类型
            string FileType = HttpContext.Current.Request.Form["FileType"].Replace("undefined", "");
            //设备名称
            string EquipmentName = HttpContext.Current.Request.Form["EquipmentName"].Replace("undefined", "");
            //设备编码
            string EquipmentCode = HttpContext.Current.Request.Form["EquipmentCode"].Replace("undefined", "");
            //备注
            string Note = HttpContext.Current.Request.Form["Note"].Replace("undefined", "");

            var result = new ResponseResult();
            //return Request.CreateResponse(HttpStatusCode.OK, result);
            //var fromData = Config.GetValue("fromData");
            string strAllowFileExtension = Config.GetValue("AllowFileExtension").ToLower();
            string[] arrAllowFileExtension = strAllowFileExtension.Split("|");
            int intMaxFileLength = Convert.ToInt32(Config.GetValue("MaxFileLength"));

            try
            {
                string message = "";
                EP_EquipmentManagee_BLL bllEQ = new EP_EquipmentManagee_BLL();
                if (string.IsNullOrEmpty(EquipmentCategoryCode))
                {
                    result.success = false;
                    result.returnMsg = $"文件型号不能为空!";
                    return Request.CreateResponse(HttpStatusCode.OK, result);
                }
                if (string.IsNullOrEmpty(FileType))
                {
                    result.success = false;
                    result.returnMsg = $"文件类型不能为空!";
                    return Request.CreateResponse(HttpStatusCode.OK, result);
                }
                //判断设备型号是否存在 
                Dictionary<string, string> map = new Dictionary<string, string>();
                map.Add("EquipmentCategoryCode", EquipmentCategoryCode);
                string msg = "";
                var eqDT = bllEQ.Get_Data(map, out msg);
                if (eqDT == null || eqDT.Rows.Count < 1)
                {
                    result.success = false;
                    result.returnMsg = EquipmentCode + " 设备型号不存在!";
                    return Request.CreateResponse(HttpStatusCode.OK, result);
                }

                if (!string.IsNullOrEmpty(EquipmentCode))
                {
                    //判断设备编码是否存在
                    map.Clear();
                    map.Add("EquipmentCode", EquipmentCode); 
                    var eqDT2 = bllEQ.Get_Data(map, out msg);
                    if (eqDT2 == null || eqDT2.Rows.Count<1)
                    {
                        result.success = false;
                        result.returnMsg = EquipmentCode+" 设备编码不存在!";
                        return Request.CreateResponse(HttpStatusCode.OK, result);
                    }

                }
                

                //上传的文件组
                var filelist = HttpContext.Current.Request.Files;
                var success = true;
                List<string> listFilePath = new List<string>();
                if (filelist.Count > 0)
                {
                    for (var i = 0; i < filelist.Count; i++)
                    {
                        string sFileName = "";
                        string sFilePath = "";
                        var file = filelist[i];
                        var fileExtention = Path.GetExtension(file.FileName).ToLower();
                        var fileName = file.FileName;
                        sFileName = fileName;

                        string sFileNameNoExt = GetFileNameWithoutExtension(fileName);
                        string sFullExtension = GetExtension(fileName);
                        if (!arrAllowFileExtension.Contains(fileExtention))
                        {
                            message = $"上传文件[{fileName}]的格式不符合要求!";
                            success = false;
                            break;
                        }
                        if (file.ContentLength > intMaxFileLength * 1024 * 1024)
                        {
                            message = $"上传文件[{fileName}]超过文件大小限制!";
                            success = false;
                            break;
                        }
                        int iCounter = 0;

                        var virtualPath = "/Upload/";

                        var fullDirPath = HttpContext.Current.Server.MapPath(virtualPath);//文件全路径
                        string sServerDir = fullDirPath;

                       
                        //判断文件名称是否重复
                        EP_EquipmentArchives_BLL bll = new EP_EquipmentArchives_BLL();
                        map.Clear();
                        map.Add("FileNameEQ", fileName);  
                        var fileDt = bll.Get_Data(map, out msg);
                        //if (fileDt != null && fileDt.Rows.Count > 0)
                        //{
                        //    result.success = false;
                        //    result.returnMsg = $"文件名{entity.FileName} 重复,请更换!";
                        //    return Request.CreateResponse(HttpStatusCode.OK, result);
                        //}
                        while (true)
                        {
                            sFilePath = System.IO.Path.Combine(sServerDir, sFileName);

                            if (System.IO.File.Exists(sFilePath))
                            {
                                iCounter++;
                                sFileName = sFileNameNoExt + "(" + iCounter + ")" + sFullExtension;
                            }
                            else
                            {
                                if (!Directory.Exists(fullDirPath))
                                {
                                    Directory.CreateDirectory(fullDirPath);
                                }
                                //var filePath = $"{fullDirPath}{fileName}";
                                try
                                {
                                    file.SaveAs(sFilePath);
                                    listFilePath.Add($@"{sFileName}");
                                    //listFilePath.Add($@"{dirPath}{sFileName}");

                                    //文件档案对象
                                    EP_EquipmentArchives entity = new EP_EquipmentArchives();
                                    entity.ModelNumber = EquipmentCategoryCode;
                                    entity.EquipmemtCode = EquipmentCode;
                                    entity.FileType = FileType;
                                    entity.FileName = fileName;//文件名称
                                    entity.FileURL = virtualPath + fileName;//文件相对路径
                                    entity.EnabledMark = true;

                                    entity.Uploader = userName;
                                    entity.UploadDate = DateTime.Now;
                                    entity.CreateUser = userName;
                                    entity.CreateDate = DateTime.Now;
                                    entity.Status = "0";

                                    int n = bll.Save_Entity("", entity, out msg);
                                    result.success = n > 0 ? true : false;
                                    result.returnMsg = msg;
                                    return Request.CreateResponse(HttpStatusCode.OK, result);
                                }
                                catch (Exception ex)
                                {
                                    message = "上传文件写入失败:" + ex.Message;
                                    success = false;
                                }

                                break;
                            }
                        }

                    }
                }
                else
                {
                    message = "没有选择上传文件!";
                    success = false;
                }

                if (listFilePath.Count > 0)
                {
                    result.resultData = listFilePath.ToArray().Join(",");
                }
                result.success = success;
                result.returnMsg = message;
                return Request.CreateResponse(HttpStatusCode.OK, result);
            }
            catch (Exception ex)
            {
                result.success = false;
                result.returnMsg = "操作失败:" + ex.Message;
                return Request.CreateResponse(HttpStatusCode.InternalServerError, result);
            }
        }

        private string GetFileNameWithoutExtension(string fileName)
        {
            int length = fileName.Length - 1, dotPos = fileName.IndexOf(".");

            if (dotPos == -1)
                return fileName;

            return fileName.Substring(0, dotPos);
        }

        private string GetExtension(string fileName)
        {
            int length = fileName.Length - 1, dotPos = fileName.IndexOf(".");

            if (dotPos == -1)
                return "";

            return fileName.Substring(dotPos);
        }
    }
}

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页