在ThinkPHP5框架下引入Ueditor並實現向七牛雲物件儲存上傳圖片同時將圖片資訊儲存到MySQL資料庫,同時實現lazyload懶載入

豌豆爸爸Aaron發表於2019-06-10

這是我花了很多天的時間才得以真正實現的一組需求。

文章後面有完整Demo的GitHub連結。

一、 需求描述

1. 應用是基於ThinkPHP5開發的;

2. 伺服器環境是LNMP,PHP版本是7.2,資料庫是MySQL5.6;

3. 由使用者(包括管理員)上傳的圖片一類的媒體檔案不能直接上傳到應用目錄中,而要上傳到單獨的物件儲存伺服器上;

4. 需要使用富文字編輯器,編輯器中需要上傳的圖片也都要儲存到物件儲存伺服器;

5. 可以對已上傳的圖片進行刪改查操作。

二、 方案選型

1. 框架:ThinkPHP 5.0.24(比較常用)

2. 編輯器:ueditor1_4_3_3-utf8-php(停更前的最新版本)

3. 物件儲存:七牛雲(免費10G空間,官方SDK齊全)

4. 開發環境:windows+WAMPServer

三、 產品設計

本文要做的只是一個demo,其中只包含需求中說明的功能,而其他相關的功能比如登入、許可權之類的就不在本文的研究範圍內。

對以上需求和方案進行分析,可以總結出本文demo需要實現的具體功能如下:

1. bucket管理(七牛雲的儲存空間),增刪改查。

2. 圖片管理,上傳,增刪改查,上傳時將資訊儲存到資料庫中,查詢時從資料庫讀取資料並向雲端儲存空間獲取圖片。

3. ueditor演示,插入本地圖片時將圖片上傳到雲端儲存中並向資料庫插入記錄,插入遠端圖片時從資料庫讀取資料並向雲端儲存獲取圖片,檢視遠端列表時獲取縮圖,插入時需要獲取大圖。

四、 實現過程

說明:本文將這個需求當作一個單獨的專案來重現實現過程,而且這個實現過程中會對與本主題無關的但在開發過程中需要用到的內容做忽略處理(比如composer、比如其他前端框架),只關注問題本身。

這個demo使用的前端框架(庫或外掛)主要包括bootstrap、jquery、adminLte、jquery-lazyload等。

1. 在七牛雲上準備開發者賬號、儲存空間和圖片樣式:

這一過程在本文中大致省略,在七牛官網上一步一步的操作即可,操作完成後需要記下幾個引數:

access_key和secret_key(這裡暫時只記錄主賬號的key,更復雜許可權操作本文不深入研究);

儲存空間的名稱,本例建立兩個空間分別名為wandoubaba和wandoubaba_user;

每個空間分別設定各自的圖片樣式,本例都用同樣的樣式策略(具體樣式根據你的實際情況設定):

縮圖:w150.h150.cut

原圖:original

原圖水印圖:original.water

限制寬度等比縮放:w800.water

限制高度等比縮放:h480.water

此外還要對每個儲存空間分別繫結域名,七牛雲雖然會預設提供一個域名,但是這個預設的域名只能使用1個月,所以還是自己去繫結一個,需要把每個空間對應的域名單獨記錄下來。

2. 建立並設計資料庫:

mysql中建立資料庫,名為tp-ue-qn-db:

CREATE DATABASE `tp-ue-qn-db` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';

建立表db_bucket和db_picture:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for db_bucket
-- ----------------------------
DROP TABLE IF EXISTS `db_bucket`;
CREATE TABLE `db_bucket`  (
  `bucket_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'bucket名稱',
  `bucket_domain` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'bucket對應的domain',
  `bucket_description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '文字描述',
  `bucket_default` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '預設,0為否,1為是',
  `bucket_style_thumb` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '縮圖樣式名',
  `bucket_style_original` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '原圖樣式名',
  `bucket_style_water` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '原圖打水印樣式名',
  `bucket_style_fixwidth` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '限制寬度樣式名',
  `bucket_style_fixheight` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '限制高度樣式名',
  PRIMARY KEY (`bucket_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for db_picture
-- ----------------------------
DROP TABLE IF EXISTS `db_picture`;
CREATE TABLE `db_picture`  (
  `picture_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '圖片唯一ID',
  `picture_key` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '雲端儲存檔名',
  `bucket_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '儲存倉庫',
  `picture_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '本機檔案描述名',
  `picture_description` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '圖片描述',
  `picture_protected` tinyint(4) NULL DEFAULT NULL COMMENT '是否保護,0為不保護,1為保護',
  `admin_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '上傳者管理員ID,後臺上傳時儲存',
  `user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '上傳者使用者ID,使用者上傳時儲存',
  `create_time` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '建立時間',
  `update_time` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '編輯時間',
  PRIMARY KEY (`picture_id`) USING BTREE,
  INDEX `bucket_name`(`bucket_name`) USING BTREE,
  CONSTRAINT `db_picture_ibfk_1` FOREIGN KEY (`bucket_name`) REFERENCES `db_bucket` (`bucket_name`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

其中,在bucket表中直接將bucket_name設定為主鍵,同時還設定了thumb、original、water、fixwidth、fixheight這五個圖片樣式名,這是結合七牛雲的圖片規則而設定的。

3. 本地建立專案目錄,配置虛擬主機:

我在本地為專案建立目錄…/tp-ue-qn-db/,然後在命令列中進入這個目錄,執行composer命令安裝thinkphp5.0.24框架到www目錄:

composer create-project topthink/think=5.0.* www  --prefer-dist

執行後的結果:

微信圖片_20190609154005

你的執行過程提示可能與我不同,但是執行結果是一樣的。

接下來就可以為應用建立虛擬機器,建立過程省略,建立結果是將本地的http://localhost.tp-ue-qn-db/指向到本地的…/tp-ue-qn-db/www/public目錄,建立好可以試著執行一下,結果應該如下:

微信圖片_20190609154522

4. 引入第三方開發包:

接下來命令列進入www目錄,用composer引入七牛雲官方提供的php-sdk:

composer require qiniu/php-sdk

5. 引入ueditor外掛

官網下載地址:https://ueditor.baidu.com/website/download.html

我下載的是1.4.3.3 PHP 版本中的UTF-8版本,但是我遇到了下載不成功的問題,最後是用我的amazon測試主機中使用wget下載成功,然後再用ftp下載到我本地。

wget https://github.com/fex-team/ueditor/releases/download/v1.4.3.3/ueditor1_4_3_3-utf8-php.zip

下載後把壓縮包內容解壓到應該目錄下面,我的解壓路徑是:

.../tp-ue-qn-db/www/public/static/lib/ueditor

操作到這裡,專案目錄結構大概是這樣:

image

6. 做與本例無關的必要操作:

主要包括在thinkphp中配置資料庫連線、引入需要的前端框架(庫或外掛)、做一些thinkphp的視力模板等,這些操作必要但與本例無關,而且每個專案都不一樣,所以不做講解。

7. 建立相關檔案,程式設計:

主要目錄結構:

image

這裡只展示核心程式碼,完整的demo可以到github中去獲取。

(1) bucket.html

<div class="box">
    <div class="box-header">
        <span> 
            <a href="javascript:;" onclick="modal_show_iframe('新增儲存空間','{:url("index/picture/bucket_add")}',90)" class="btn btn-primary"><i class="fa fa-fw fa-plus-square"></i> 新增資料</a> 
        </span>
        <span class="pull-right">共有資料:<strong>{$count}</strong></span>
    </div>
    <div class="box-body" style="overflow-y: hidden;overflow-x: scroll;">
        <table class="table table-bordered table-strited table-hover text-nowrap">
            <thead>
                <tr>
                    <th scope="col" colspan="8">儲存空間</th>
                </tr>
                <tr>
                    <th>操作</th>
                    <th>名稱</th>
                    <th>域名</th>
                    <th>描述</th>
                    <th>預設</th>
                    <th>縮圖樣式</th>
                    <th>原圖樣式</th>
                    <th>原圖水印樣式</th>
                    <th>適應寬度樣式</th>
                    <th>適應高度樣式</th>
                </tr>
            </thead>
            <tbody>
            {volist name='list' id='vo'}
                <tr title="{$vo.bucket_description}">
                    <td class="td-manage">
                        <a title="編輯" href="javascript:;" onclick="modal_show_iframe('編輯儲存空間','{:url("index/picture/bucket_edit",["name"=>$vo.bucket_name])}','')"><i class="fa fa-fw fa-pencil-square-o"></i></a> 
                        <a title="刪除" href="javascript:;" onclick="ajax_post_confirm('{:url("index/picture/do_bucket_delete")}',{name:'{$vo.bucket_name}'},'{$vo.bucket_name}','刪除');"><i class="fa fa-fw fa-trash-o"></i></a>
                    </td>
                    <td><span class="name">{$vo.bucket_name}</span></td>
                    <td>{$vo.bucket_domain}</td>
                    <td>{$vo.bucket_description}</td>
                    <td>{$vo.bucket_default}</td>
                    <td>{$vo.bucket_style_thumb}</td>
                    <td>{$vo.bucket_style_original}</td>
                    <td>{$vo.bucket_style_water}</td>
                    <td>{$vo.bucket_style_fixwidth}</td>
                    <td>{$vo.bucket_style_fixheight}</td>
                </tr>
            {/volist}
            </tbody>
        </table>
    </div>
    <div class="box-footer">
        <div class="text-warning text-center">在電腦上操作會更舒服一些。</div>
    </div>
</div>

(2) bucket_add.html

<form method="post" class="form-horizontal">
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>空間名稱:</label>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="空間名稱,與雲上的bucket一致" id="bucket_name" name="bucket_name" rangelength="[1,50]" required>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>空間域名:</label>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="空間域名,http://.../形式,以/結尾" id="bucket_domain" name="bucket_domain" rangelength="[4,100]" required>
        </div>
    </div>        
    <div class="form-group">
        <label class="control-label col-sm-3">描述:</label>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="文字描述" id="bucket_description" name="bucket_description" maxlength="100">
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3">預設空間:</label>
        <div class="col-sm-9">
            <input type="checkbox" id="bucket_default" name="bucket_default" />
                勾選為預設,只可以有1個預設空間
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>縮圖樣式:</label>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="縮圖樣式名" id="bucket_style_thumb" name="bucket_style_thumb" rangelength="[3,100]" required>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>原圖樣式:</label>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="原圖樣式名" id="bucket_style_original" name="bucket_style_original" rangelength="[3,100]" required>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>原圖水印樣式:</label>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="原圖加水印樣式名" id="bucket_style_water" name="bucket_style_water" rangelength="[3,100]" required>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>適應寬度樣式:</label>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="適應寬度樣式名" id="bucket_style_fixwidth" name="bucket_style_fixwidth" rangelength="[3,100]" required>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>適應高度樣式:</label>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="適應高度樣式名" id="bucket_style_fixheight" name="bucket_style_fixheight" rangelength="[3,100]" required>
        </div>
    </div>

    <div class="form-group">
        <div class="col-sm-9 col-sm-offset-3">
            <button type="submit" class="btn btn-success disabled" disabled="true">提交資料</button>
        </div>
    </div>
</form>

對錶單進行前端驗證時不要忘了引入jquery-validation外掛。

$(function() {
        // 初始化checkbox的icheck樣式
        $('input[type="checkbox"],input[type="radio"]').iCheck({
            checkboxClass: 'icheckbox_minimal-blue',
            radioClass   : 'iradio_minimal-blue'
        })

        // 只有當表單中有資料變化時,提交按鈕才可用
        $("form").children().bind('input propertychange ifChecked ifUnchecked',function() {
            $(":submit").removeClass('disabled').removeAttr('disabled');
        });

        $("form").validate({
            rules: {
                bucket_domain: {
                    url: true
                }
            },
            submitHandler: function(form) {
                // 當驗證通過時執行ajax提交
                ajax_post("{:url('index/picture/do_bucket_add')}",$("form").serialize());
            }
        });
    });

(3) bucket_edit.html

<form method="post" class="form-horizontal">
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>空間名稱(只讀):</label>
        <div class="col-sm-9">
            <input value="{$bucket.bucket_name}" type="text" class="form-control" id="bucket_name" name="bucket_name" readonly="true">
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>空間域名:</label>
        <div class="col-sm-9">
            <input value="{$bucket.bucket_domain}" type="text" class="form-control" placeholder="空間域名,http://.../形式,以/結尾" id="bucket_domain" name="bucket_domain" rangelength="[4,100]" required>
        </div>
    </div>        
    <div class="form-group">
        <label class="control-label col-sm-3">描述:</label>
        <div class="col-sm-9">
            <input value="{$bucket.bucket_description}" type="text" class="form-control" placeholder="文字描述" id="bucket_description" name="bucket_description" maxlength="100">
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3">預設空間:</label>
        <div class="col-sm-9">
            <input {eq name="bucket.bucket_default" value="1"} checked="true" {/eq} type="checkbox" id="bucket_default" name="bucket_default" />
                勾選為預設,只可以有1個預設空間
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>縮圖樣式:</label>
        <div class="col-sm-9">
            <input value="{$bucket.bucket_style_thumb}" type="text" class="form-control" placeholder="縮圖樣式名" id="bucket_style_thumb" name="bucket_style_thumb" rangelength="[3,100]" required>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>原圖樣式:</label>
        <div class="col-sm-9">
            <input value="{$bucket.bucket_style_original}" type="text" class="form-control" placeholder="原圖樣式名" id="bucket_style_original" name="bucket_style_original" rangelength="[3,100]" required>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>原圖水印樣式:</label>
        <div class="col-sm-9">
            <input value="{$bucket.bucket_style_water}" type="text" class="form-control" placeholder="原圖加水印樣式名" id="bucket_style_water" name="bucket_style_water" rangelength="[3,100]" required>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>適應寬度樣式:</label>
        <div class="col-sm-9">
            <input value="{$bucket.bucket_style_fixwidth}" type="text" class="form-control" placeholder="適應寬度樣式名" id="bucket_style_fixwidth" name="bucket_style_fixwidth" rangelength="[3,100]" required>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>適應高度樣式:</label>
        <div class="col-sm-9">
            <input value="{$bucket.bucket_style_fixheight}" type="text" class="form-control" placeholder="適應高度樣式名" id="bucket_style_fixheight" name="bucket_style_fixheight" rangelength="[3,100]" required>
        </div>
    </div>

    <div class="form-group">
        <div class="col-sm-9 col-sm-offset-3">
            <button type="submit" class="btn btn-success disabled" disabled="true">提交資料</button>
        </div>
    </div>
</form>
$(function() {
        // 初始化checkbox的icheck樣式
        $('input[type="checkbox"],input[type="radio"]').iCheck({
            checkboxClass: 'icheckbox_minimal-blue',
            radioClass   : 'iradio_minimal-blue'
        })

        // 只有當表單中有資料變化時,提交按鈕才可用
        $("form").children().bind('input propertychange ifChecked ifUnchecked',function() {
            $(":submit").removeClass('disabled').removeAttr('disabled');
        });

        $("form").validate({
            rules: {
                bucket_domain: {
                    url: true
                }
            },
            submitHandler: function(form) {
                // 當驗證通過時執行ajax提交
                ajax_post("{:url('index/picture/do_bucket_edit')}",$("form").serialize());
            }
        });
    });

(4) picture.html

<div class="nav-tabs-custom">
    <ul id="main-nav" class="nav nav-tabs">
        <li class="header">空間 <i class="fa fa-arrow-right"></i> </li>
        {volist name="bucketlist" id="vo"}
            <li class="{if condition='$vo.bucket_default eq 1'}active{/if}">
                <a href="#{$vo.bucket_name}" data-toggle="tab">{$vo.bucket_name}</a>
            </li>
        {/volist}
    </ul>
    <div id="main-nav-tabs" class="tab-content">
        {volist name="bucketlist" id="vo"}
            <div class="tab-pane {eq name='vo.bucket_default' value='1'}active{/eq}" id="{$vo.bucket_name}">
                <div class="row">
                    <div class="col-xs-3">
                        <a href="javascript:;" onclick="modal_show_iframe('上傳圖片','{:url("index/picture/add",["bucket"=>$vo.bucket_name])}','')" class="btn btn-primary"><i class="fa fa-fw fa-plus-square"></i> 上傳圖片</a> 
                    </div>
                </div>
                <div class="row mt-3">
                {volist name="vo.child" id="vo_c" mod="6" empty="沒有圖片"}
                    <div class="col-xs-6 col-md-4 col-lg-2">
                        <div class="panel {eq name='vo_c.picture_protected' value='1'}panel-danger{else/}panel-info{/eq}">
                            <div class="panel-heading ellipsis">
                                <span title="{$vo_c.picture_name}">
                                    {$vo_c.picture_name}
                                </span>
                            </div>
                            <div class="panel-body">
                                <a href="{$vo.bucket_domain}{$vo_c.picture_key}-{$vo.bucket_style_water}" data-lightbox="qiniu-image">
                                    <img class="lazy img-responsive" 
                                        src="__STATIC__/img/loading-0.gif" 
                                        data-src="{$vo.bucket_domain}{$vo_c.picture_key}-{$vo.bucket_style_thumb}" 
                                        data-original="{$vo.bucket_domain}{$vo_c.picture_key}-{$vo.bucket_style_thumb}" 
                                        alt="">
                                </a>
                            </div>
                            <div class="panel-footer ellipsis">
                                <span title="{$vo_c.picture_description}">
                                    {$vo_c.picture_description}
                                </span><br/>
                                <span title="{$vo_c.create_time}">
                                    {$vo_c.create_time}
                                </span><br/>
                                <span title="{$vo_c.update_time}">
                                    {$vo_c.update_time}
                                </span><br/>
                                <span class="pull-right">
                                    <a href="javascript:;" onclick="modal_show_iframe('編輯圖片','{:url("index/picture/edit",["id"=>$vo_c.picture_id])}','')" title="編輯"><i class="fa fa-edit fa-fw"></i></a>
                                    <a href="javascript:;" onclick="ajax_post_confirm('{:url("index/picture/do_picture_delete")}',{id:'{$vo_c.picture_id}'},'{$vo_c.picture_name}','刪除');" title="刪除"><i class="fa fa-trash fa-fw"></i></a>
                                </span>
                            </div>
                        </div>
                    </div>
                    {eq name="mod" value="5"}
                        </div><div class="row">
                    {/eq}
                {/volist}
                </div>
            </div>
        {/volist}
        <!-- /.tab-pane -->
    </div>
    <!-- /.tab-content -->
</div>
$(function() {
    // 圖片lazyload懶載入
    $("img.lazy").lazyload();
    // 如果沒有預設空間,則預設啟用第1個空間
    if(!$("#main-nav-tabs .tab-pane.active")==false) {
        $("#main-nav a:first").tab("show");
    }
});

引入lazyload元件以實現圖片的懶載入,詳細資訊詳見網址:

https://appelsiini.net/projects/lazyload

引入lightbox2元件以實現圖片預覽,具體資訊詳見網址:

https://lokeshdhakar.com/projects/lightbox2/

(5) picture_add.html

<form method="post" enctype="multipart/form-data" class="form-horizontal">
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>選擇空間</label>
        <div class="col-sm-9">
            <select name="bucket_name" class="form-control">
                {volist name="bucketlist" id="vo"}
                <option value="{$vo.bucket_name}" 
                {empty name="bucket"}
                    {eq name="vo.bucket_default" value="1"} selected="true" {/eq}
                {else/}
                    {eq name="vo.bucket_name" value="$bucket"} selected="true" {/eq}
                {/empty}
                >{$vo.bucket_name}</option>
                {/volist}
            </select>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-3"><span class="text-red">*</span>圖片檔案</label>
        <div class="col-sm-9">
            <input type="file" class="form-control" placeholder="請選擇圖片檔案" id="picture_file" name="picture_file" accept="image/gif,image/jpeg,image/jpg,image/png,image/svg" required />
        </div>
    </div>
    <div class="form-group">
        <lable class="control-label col-sm-3">圖片標題</lable>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="給圖片設定一個標題,空白預設檔名" id="picture_name" name="picture_name" />
        </div>
    </div>
    <div class="form-group">
        <lable class="control-label col-sm-3">圖片描述</lable>
        <div class="col-sm-9">
            <input type="text" class="form-control" placeholder="給圖片編輯一段描述" id="picture_description" name="picture_description" />
        </div>
    </div>
    <div class="form-group">
        <lable class="control-label col-sm-3">許可權保護</lable>
        <div class="col-sm-9">
            <label>
                <input type="checkbox" id="picture_protected" name="picture_protected" />
                勾選表示設定許可權保護(輕易不要勾選)
            </label>            
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-9 col-sm-offset-3">
            <button type="submit" class="btn btn-success disabled" disabled="true">提交資料</button>
        </div>
    </div>
</form>
$(function() {
        // 初始化checkbox的icheck樣式
        $('input[type="checkbox"]').iCheck({
            checkboxClass: 'icheckbox_minimal-blue',
            radioClass   : 'iradio_minimal-blue'
        })

        // 只有當表單中有資料變化時,提交按鈕才可用
        $("form").children().bind('input propertychange ifChecked ifUnchecked',function() {
            $(":submit").removeClass('disabled').removeAttr('disabled');
        });

        $("form").validate({
            rules: {
                
            },
            submitHandler: function(form) {
                // 當驗證通過時執行ajax提交
                upload();
            }
        });
    });

    function upload() {
        var formData = new FormData();
        var file = $("[name='picture_file']")[0].files[0];
        formData.append("picture_file", file);
        formData.append("bucket_name", $("[name='bucket_name']").val());
        formData.append("picture_name", $("[name='picture_name']").val());
        formData.append("picture_description", $("[name='picture_description']").val());
        formData.append("picture_protected", $("[name='picture_protected']").is(':checked') ? 1 : 0);
        $.ajax({
            url: "{:url('index/picture/do_picture_add')}",
            type: 'POST',
            data: formData,
            // 告訴jQuery不要去處理髮送的資料
            processData: false,
            // 告訴jQuery不要去設定Content-Type請求頭
            contentType: false,
            beforeSend: function () {
                var loading = layer.load(1, {
                    shade: [0.1,'#fff'] //0.1透明度的白色背景
                });
            },
            success: function (data) {
                console.log(data);
                layer.closeAll();
                // 當ajax請求執行成功時執行
                if (data.status == true) {
                    // 返回result物件中的status元素值為1表示資料插入成功
                    layer.msg(data.message, {icon: 6, time: 2000});    // 使用H-ui的浮動提示框,2秒後自動消失
                    setTimeout(function() {
                        parent.location.reload();
                    }, 2000);    //2秒後對父頁面執行重新整理(相當於關閉了彈層同時更新了資料)
                } else {
                    // 返回result物件的status值不為1,表示資料插入失敗
                    layer.alert(data.message+"<p>請自行重新整理頁面</p>", {icon: 5});
                    // 頁面停留在這裡,不再執行任何動作
                }
            },
            error: function (data) {
                console.log(data);
            }
        });
    }

(6) picture_edit.html

<form method="post" enctype="multipart/form-data" class="form-horizontal">
    <div class="form-group hide">
        <lable class="control-label col-sm-3">圖片ID<span class="text-red">*</span></lable>
        <div class="col-sm-9">
            <input value="{$picture.picture_id}" type="text" class="form-control" id="picture_id" name="picture_id" readonly="true" required />
        </div>
    </div>
    <div class="form-group">
        <lable class="control-label col-sm-3">圖片標題<span class="text-red">*</span></lable>
        <div class="col-sm-9">
            <input value="{$picture.picture_name}" type="text" class="form-control" placeholder="給圖片設定一個標題" id="picture_name" name="picture_name" required />
        </div>
    </div>
    <div class="form-group">
        <lable class="control-label col-sm-3">圖片描述</lable>
        <div class="col-sm-9">
            <input value="{$picture.picture_description}" type="text" class="form-control" placeholder="給圖片編輯一段描述" id="picture_description" name="picture_description" />
        </div>
    </div>
    <div class="form-group">
        <lable class="control-label col-sm-3">許可權保護</lable>
        <div class="col-sm-9">
            <label>
                <input {eq name="picture.picture_protected" value="1"} checked="true" {/eq} type="checkbox" id="picture_protected" name="picture_protected" />
                勾選表示設定許可權保護(輕易不要勾選)
            </label>            
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-9 col-sm-offset-3">
            <button type="submit" class="btn btn-success disabled" disabled="true">提交資料</button>
        </div>
    </div>
</form>

js部分省略,詳見github。

(7) index/controller/Index.php

無邏輯處理,省略,詳見github。

(8) index/controller/Picture.php

class Picture extends Base
{
    public function index()
    {
        $this->view->assign('pagetitle', '圖片管理');
        // 載入bucket列表
        $bucketlist = BucketModel::all(function($query) {
            $query->order(['bucket_default'=>'desc', 'bucket_name'=>'asc']);
        });
        // 載入bucket裡的圖片
        $picturelist;
        // 遍歷bucket
        foreach($bucketlist as $n=>$bucket) {
            $picture = new PictureModel;
            $picturelist = $picture
                ->where(['bucket_name'=>$bucket->bucket_name])
                ->order(['create_time'=>'desc'])
                ->select();
            $bucketlist[$n]['child'] = $picturelist;
        }
        $this->view->assign('bucketlist', $bucketlist);
        return $this->view->fetch('picture/picture');
    }

    /**
     * 載入新增圖片頁面
     */
    public function add()
    {
        $this->view->assign('pagetitle', '上傳圖片');
        $bucket = input('?bucket') ? input('bucket') : '';
        $this->view->assign('bucket', $bucket);
        $bucketlist = BucketModel::all(function($query) {
            $query->order(['bucket_default'=>'desc', 'bucket_name'=>'asc']);
        });
        $this->view->assign('bucketlist', $bucketlist);
        return $this->view->fetch('picture/picture_add');
    }

    /**
     * 載入編輯圖片頁面
     * @return [type] [description]
     */
    public function edit()
    {
        $this->view->assign('pagetitle', '編輯圖片資訊');
        if(!input('?id')) {
            $this->error('引數錯誤');
            return;
        }
        $id = input('id');
        $picture = PictureModel::get($id);
        if(!$picture) {
            $this->error('引數錯誤');
            return;
        }
        $this->view->assign('picture', $picture);
        return $this->view->fetch('picture/picture_edit');
    }

    /**
     * 執行編輯圖片操作
     * @return [type] [description]
     */
    public function do_picture_edit()
    {
        $res = new Res;
        $res->data = input();
        $res->data['picture_protected'] = input('?picture_protected') ? 1 : 0;
        try {
            $picture = new PictureModel;
            $res->data_row_count = $picture->isUpdate(true)->allowField(true)->save([
                'picture_name'=>$res->data['picture_name'],
                'picture_description'=>$res->data['picture_description'],
                'picture_protected'=>$res->data['picture_protected']
            ],['picture_id'=>$res->data['picture_id']]);
            if($res->data_row_count) {
                $res->success();
            }
        } catch (\Exception $e) {
            $res->faild($e->getMessage());
        }
        return $res;
    }

    /**
     * 執行新增圖片操作
     * @return [type] [description]
     */
    public function do_picture_add()
    {
        $res = new Res;
        $picture_file = request()->file('picture_file');
        $picture = new PictureModel;
        $picture->bucket_name = input('bucket_name');
        $picture->picture_name = input('picture_name')?:$picture_file->getInfo('name');
        $picture->picture_description = input('picture_description')?:$picture->picture_name;
        $picture->picture_protected = input('picture_protected');
        // 由於demo中沒做登入部分,所以這裡獲取不到值
        // $picture->admin_id = Session::has('admin_infor')?Session::get('admin_infor')->admin_id:'';
        if($picture_file) {
            // 建立PictureService物件例項
            $pservice = new \app\common\controller\PictureService;
            try {
                // 呼叫up_file方法向指定空間上傳圖片
                $res = $pservice->up_picture($picture_file, $picture);
            } catch(\Exception $e) {
                $res->failed($e->getMessage());
            }
        }
        return $res;
    }

    /**
     * 執行刪除圖片的操作
     * @return [type] [description]
     */
    public function do_picture_delete()
    {
        $res = new Res;
        if(!input('?id')) {
            // 未取到id引數
            $res->failed('引數錯誤');
            return $res;
        }
        $id = input('id');
        try {
            $res->data = PictureModel::get($id);
            if(!$res->data) {
                // 取到的id引數沒有對應的記錄
                $res->failed('參錯錯誤');
                return $res;
            }
            if($res->data['picture_protected']) {
                $res->failed('不能刪除受保護的圖片');
                return $res;
            }
            // 建立QiniuService物件例項
            $qservice = new \app\common\controller\QiniuService;
            // 呼叫delete_file方法刪除指定bucket和指定key的檔案
            $res = $qservice->delete_file($res->data['bucket_name'], $res->data['picture_key']);
            if($res->status) {
                // 檔案刪除成功,開始刪除資料
                PictureModel::where(['picture_id'=>$id])->delete();
                $res->append_message('<li>資料庫記錄刪除成功</li>');
            }
        } catch(\Exception $e) {
            $res->failed($e->getMessage());
        }
        return $res;
    }

    /**
     * 載入空間管理頁面
     * @return [type] [description]
     */
    public function bucket()
    {
        $this->view->assign('pagetitle','儲存空間');
        $bucketlist = BucketModel::all(function($query) {
            $query->order(['bucket_default'=>'desc', 'bucket_name'=>'asc']);
        });
        $this->view->assign('list', $bucketlist);
        $this->view->assign('count', count($bucketlist));
        return $this->view->fetch('picture/bucket');
    }

    /**
     * 載入新增空間頁面
     * @return [type] [description]
     */
    public function bucket_add()
    {
        $this->view->assign('pagetitle', '新增儲存空間');
        return $this->view->fetch('picture/bucket_add');
    }

    /**
     * 執行新增空間操作
     * @return [type] [description]
     */
    public function do_bucket_add()
    {
        $res = new Res;
        $res->data = input();
        $res->data['bucket_default'] = input('?bucket_default') ? 1 : 0;
        $bucket = new BucketModel;
        $validate = Loader::validate('Bucket');
        if(!$validate->check($res->data)) {
            $res->failed($validate->getError());
            return $res;
        }
        if($res->data['bucket_default']) {
        $default = BucketModel::get(['bucket_default'=>1]);
            // 單獨驗證只可以有一條預設空間
            if($default) {
                $res->failed('只能有1個預設空間:已經存在預設空間'.$default->bucket_name);
                return $res;
            }
        }
        try {
            $res->data_row_count = $bucket->isUpdate(false)->allowField(true)->save([
                'bucket_name'        =>    $res->data['bucket_name'],
                'bucket_domain'        =>    $res->data['bucket_domain'],
                'bucket_description'=>    $res->data['bucket_description'],
                'bucket_default'=>    $res->data['bucket_default'],
                'bucket_style_thumb'=>    $res->data['bucket_style_thumb'],
                'bucket_style_original'=>    $res->data['bucket_style_original'],
                'bucket_style_water'=>    $res->data['bucket_style_water'],
                'bucket_style_fixwidth'=>    $res->data['bucket_style_fixwidth'],
                'bucket_style_fixheight'=>    $res->data['bucket_style_fixheight'],
            ]);
            if($res->data_row_count) {
                $res->success();
            }
        } catch(\Exception $e) {
            $res->failed($e->getMessage());
        }
        return $res;
    }

    /**
     * 載入編輯空間頁面
     * @return [type] [description]
     */
    public function bucket_edit()
    {
        $this->view->assign('pagetitle', '編輯儲存空間');
        if(!input('?name')) {
            $this->error('引數錯誤');
            return;
        }
        $name = input('name');
        $bucket = BucketModel::get(['bucket_name'=>$name]);
        if(!$bucket) {
            $this->error('引數錯誤');
            return;
        }
        $this->view->assign('bucket', $bucket);
        return $this->view->fetch('picture/bucket_edit');
    }

    /**
     * 執行修改空間(描述)操作
     * @return [type] [description]
     */
    public function do_bucket_edit()
    {
        $res = new Res;
        $res->data = input();
        $res->data['bucket_default'] = input('?bucket_default') ? 1 : 0;
        $validate = Loader::validate('Bucket');
        if(!$validate->scene('edit')->check($res->data)) {
            $res->failed($validate->getError());
            return $res;
        }
        $bucket = new BucketModel;
        if($res->data['bucket_default']) {
            $default = $bucket->where('bucket_default', 'eq', 1)->where('bucket_name','neq',$res->data['bucket_name'])->find();
            if($default) {
                $res->failed('只能有1個預設空間:已經存在預設空間'.$default->bucket_name);
                return $res;
            }
        }
        try {
            $res->data_row_count = $bucket->isUpdate(true)->allowField(true)->save([
                'bucket_domain'=>$res->data['bucket_domain'],
                'bucket_description'=>$res->data['bucket_description'],
                'bucket_default'=>$res->data['bucket_default'],
                'bucket_style_thumb'=>$res->data['bucket_style_thumb'],
                'bucket_style_original'=>$res->data['bucket_style_original'],
                'bucket_style_water'=>$res->data['bucket_style_water'],
                'bucket_style_fixwidth'=>$res->data['bucket_style_fixwidth'],
                'bucket_style_fixheight'=>$res->data['bucket_style_fixheight'],
            ], ['bucket_name'=>$res->data['bucket_name']]);
            if($res->data_row_count) {
                $res->success();
            } else {
                $res->failed('未更改任何資料');
            }
        } catch(\Exception $e) {
            $res->failed($e->getMessage());
        }
        return $res;
    }

    /**
     * 執行刪除空間(非預設)操作
     * @return [type] [description]
     */
    public function do_bucket_delete()
    {
        $res = new Res;
        $name = input('?name') ? input('name') : '';
        $bucket = BucketModel::get(['bucket_name'=>$name]);
        $res->data = $bucket;
        if(empty($bucket)) {
            $res->failed("引數錯誤");
            return $res;
        }
        if($bucket->bucket_default==1) {
            $res->failed("預設空間不允許刪除");
            return $res;
        }
        try {
            $res->data_row_count = BucketModel::where(['bucket_name'=>$name])->delete();    // 執行真刪除
            $res->success();
        } catch(\Exception $e) {
            $res->failed($e->getMessage());
        }
        return $res;
    }
}

(9) common/controller/QiniuService.php

QiniuService並沒有繼承common/controller/base,因為它不需要使用thinkphp的controller特性。

class QiniuService
{
    /**
     * 向七牛雲端儲存獲取指定bucket的token
     * @param  string $bucket [指定bucket名稱]
     * @return [type]         [description]
     */
    private function get_token($bucket)
    {
        $access_key = Env::get('qiniu.access_key');
        $secret_key = Env::get('qiniu.secret_key');

        $auth = new \Qiniu\Auth($access_key, $secret_key);
        $upload_token = $auth->uploadToken($bucket);
        return $upload_token;
    }

    private function generate_auth()
    {
        $access_key = Env::get('qiniu.access_key');
        $secret_key = Env::get('qiniu.secret_key');
        $auth = new \Qiniu\Auth($access_key, $secret_key);
        return $auth;
    }

    public function delete_file($bucket, $key)
    {
        $res = new Res;

        try {
            $auth = $this->generate_auth();
            $bucketManager = new \Qiniu\Storage\BucketManager($auth);

            $config = new \Qiniu\Config();
            $bucketManager = new \Qiniu\Storage\BucketManager($auth, $config);
            $err = $bucketManager->delete($bucket, $key);
            // dump($err->getResponse('statusCode')->statusCode);
            /*
            HTTP狀態碼    說明
            298    部分操作執行成功
            400    請求報文格式錯誤
            包括上傳時,上傳表單格式錯誤。例如incorrect region表示上傳域名與上傳空間的區域不符,此時需要升級 SDK 版本。
            401    認證授權失敗
            錯誤資訊包括金鑰資訊不正確;數字簽名錯誤;授權已超時,例如token not specified表示上傳請求中沒有帶 token ,可以抓包驗證後排查程式碼邏輯; token out of date表示 token 過期,推薦 token 過期時間設定為 3600 秒(1 小時),如果是客戶端上傳,建議每次上傳從服務端獲取新的 token;bad token表示 token 錯誤,說明生成 token 的演算法有問題,建議直接使用七牛服務端 SDK 生成 token。
            403    許可權不足,拒絕訪問。
            例如key doesn't match scope表示上傳檔案指定的 key 和上傳 token 中,putPolicy 的 scope 欄位不符。上傳指定的 key 必須跟 scope 裡的 key 完全匹配或者字首匹配;ExpUser can only upload image/audio/video/plaintext表示賬號是體驗使用者,體驗使用者只能上傳文字、圖片、音訊、視訊型別的檔案,完成實名認證即可解決;not allowed表示您是體驗使用者,若想繼續操作,請先前往實名認證。
            404    資源不存在
            包括空間資源不存在;映象源資源不存在。
            405    請求方式錯誤
            主要指非預期的請求方式。
            406    上傳的資料 CRC32 校驗錯誤
            413    請求資源大小大於指定的最大值
            419    使用者賬號被凍結
            478    映象回源失敗
            主要指映象源伺服器出現異常。
            502    錯誤閘道器
            503    服務端不可用
            504    服務端操作超時
            573    單個資源訪問頻率過高
            579    上傳成功但是回撥失敗
            包括業務伺服器異常;七牛伺服器異常;伺服器間網路異常。需要確認回撥伺服器接受 POST 請求,並可以給出 200 的響應。
            599    服務端操作失敗
            608    資源內容被修改
            612    指定資源不存在或已被刪除
            614    目標資源已存在
            630    已建立的空間數量達到上限,無法建立新空間。
            631    指定空間不存在
            640    呼叫列舉資源(list)介面時,指定非法的marker引數。
            701    在斷點續上傳過程中,後續上傳接收地址不正確或ctx資訊已過期。
             */
            if($err) {
                if($err->getResponse('statusCode')->statusCode==612) {
                    // 指定資源不存在或已被刪除
                    $res->success('目標檔案已不存在');
                } else {
                    $res->failed($err->message());
                }
            } else {
                $res->success();
            }
        } catch (\Exception $e) {
            $res->failed($e->getMessage());
        }
        return $res;
    }

    /**
     * 向指定七牛雲端儲存空間上傳檔案
     * @param  [type] $bucket [指定儲存空間bucket名稱]
     * @param  [type] $file   [需上傳的檔案]
     * @return [type]         [Res物件例項]
     */
    public function up_file($bucket, $file = null)
    {
        $token = $this->get_token($bucket);
        $res = new Res;
        $res->data = '';
        $res->result = ['token'=>$token];
        if($file) {
            // 要上傳圖片的本地路徑
            $file_path = $file->getRealPath();
            // 檔名字尾
            $ext = pathinfo($file->getInfo('name'), PATHINFO_EXTENSION);
            // 檔案字首(類似資料夾)
            $prefix = str_replace("-","",date('Y-m-d/'));
            // 上傳後儲存的檔名(無字尾)
            $file_name = uniqid();
            // 上傳後的完整檔名(含字首字尾)
            $key = $prefix.$file_name.'.'.$ext;
            // 域名
            $domain = Bucket::get(['bucket_name'=>$bucket])->bucket_domain;
            // 初始化UploadManager物件並進行檔案上傳
            $upload_manager = new \Qiniu\Storage\UploadManager();
            // 呼叫UploadManager的putFile方法進行檔案上傳
            list($ret, $err) = $upload_manager->putFile($token, $key, $file_path);
            if($err!==null) {
                $res->failed($err);
            } else {
                $res->success();
                $res->result['domain'] = $domain;
                $res->result['key'] = $ret['key'];
                $res->result['hash'] = $ret['hash'];
                $res->result['bucket'] = $bucket;
            }
        } else {
            $res->failed('未接收到檔案');
        }
        return $res;
    }

    /**
     * 從伺服器傳輸檔案到七牛雲
     * @param  [type] $bucket    目標bucket
     * @param  [type] $file_path 要傳輸檔案的伺服器路徑
     * @return [type]            res
     */
    public function transfer_file($bucket, $file_path)
    {
        // 構建鑑權物件
        $auth = $this->generate_auth();
        // 生成上傳 Token
        $token = $auth->uploadToken($bucket);
        // 檔案字尾
        $ext = pathinfo($file_path, PATHINFO_EXTENSION);
        // 檔案字首(類似資料夾)
        $prefix = str_replace("-","",date('Y-m-d/'));
        // 上傳到七牛後儲存的檔名(不帶字尾)
        $file_name = uniqid();
        // 上傳後的完整檔名(含字首字尾)
        $key = $prefix.$file_name.'.'.$ext;
        // 域名
        $domain = Bucket::get(['bucket_name'=>$bucket])->bucket_domain;
        $res = new Res;
        try {
            // 初始化 UploadManager 物件並進行檔案的上傳。
            $uploadMgr = new \Qiniu\Storage\UploadManager();
            // 呼叫 UploadManager 的 putFile 方法進行檔案的上傳。
            list($ret, $err) = $uploadMgr->putFile($token, $key, '.'.$file_path);
            if ($err !== null) {
                $res->failed();
                $res->result['obj'] = $err;
            } else {
                $res->success();
                $res->result['obj'] = $ret;
                $res->result['domain'] = $domain;
                $res->result['key'] = $ret['key'];
                $res->result['hash'] = $ret['hash'];
                $res->result['bucket'] = $bucket;
            }
        } catch (\Exception $e) {
            $res->failed($e->getMessage());
        }

        return $res;        
    }

    /**
     * 獲取七牛雲指定bucket儲存空間的檔案列表
     * @param  [type]  $bucket [指定儲存空間名稱]
     * @param  string  $marker [上次列舉返回的位置標記,作為本次列舉的起點資訊]
     * @param  string  $prefix [要列取檔案的公共字首]
     * @param  integer $limit  [本次列舉的條目數]
     * @return [type]          [description]
     */
    public function list_file($bucket, $marker='', $prefix='', $limit=100)
    {
        $auth = $this->generate_auth();
        $bucketManager = new \Qiniu\Storage\BucketManager($auth);
        $delimiter = '';
        // 列舉檔案
        list($ret, $err) = $bucketManager->listFiles($bucket, $prefix, $marker, $limit, $delimiter);
        if ($err !== null) {
            $result = $err;
        } else {
            if (array_key_exists('marker', $ret)) {
                echo "Marker:" . $ret["marker"] . "\n";
            }
            $result = $ret;
        }
        return $result;
    }
}

(10) common/controller/PictureService.php

class PictureService extends CommonBase
{
    /**
     * 從資料庫中找到第1個預設bucket
     * @return [type] [description]
     */
    private function default_bucket()
    {
        $bucket = new Bucket;
        // 向資料庫查詢bucket_default為1的記錄
        $default_bucket = $bucket->where(['bucket_default'=>1])->find();
        // 如果沒有bucket_default為1的記錄,再嘗試取第1條bucket記錄
        if(!$default_bucket) {
            $default_bucket = $bucket->where('1=1')->find();
        }
        // 如果實在取不到,這裡就算了,返回吧
        return $default_bucket;
    }

    public function up_picture($file, $picture)
    {
        $res = new Res;
        if(empty($picture->toArray()['bucket_name'])) {
            $bucket = $this->default_bucket();
            if($bucket) {
                $picture->bucket_name = $this->default_bucket()->bucket_name;
            } else {
                $res->failed('無法獲取bucket資訊');
                return $res;
            }
        }
        if(empty($picture->toArray()['picture_name'])) {
            $picture->picture_name = $file->getInfo('name');
        }
        if(empty($picture->toArray()['picture_description'])) {
            $picture->picture_description = $picture->picture_name;
        }
        if($file) {
            // 建立QiniuService物件例項
            $qservice = new QiniuService;
            try {
                // 呼叫up_file方法向指定空間上傳圖片
                $res = $qservice->up_file($picture->bucket_name, $file);
                if($res->status) {
                    // 上傳成功,寫入資料庫
                    $picture->picture_key = $res->result['key'];
                    //在我的專案中有一個自動生成全域性唯一且遞增ID的方法,但是demo中沒做相關配置部分
                    //demo中將picture_id直接設定成自增ID了
                    //$picture->picture_id = $this->apply_full_global_id_str();
                    $res_db = new Res;
                    $res_db->data_row_count = $picture->isUpdate(false)->allowField(true)->save();
                    if($res_db->data_row_count) {
                        // 寫入資料庫成功
                        $res_db->success();
                        $res_db->data = $picture;
                    }
                    // 將寫入資料庫的結果作為返回結果的一個屬性
                    $res->result["db"] = $res_db;
                }
            } catch(\Exception $e) {
                $res->failed($e->getMessage());
            }
        }
        return $res;
    }

    public function up_scrawl($ext = null, $content = null, $path = null)
    {
        // 儲存圖片到伺服器,取得伺服器路徑
        $file_path = $this->save_picture($ext, $content, $path);
        // 傳輸伺服器圖片到七牛雲,取得返回的url
        $url = $file_path;
        $res = new Res;
        $picture = new Picture;
        $picture->bucket_name = $this->default_bucket()->bucket_name;
        $picture->picture_name = pathinfo($file_path, PATHINFO_BASENAME);
        $picture->picture_description = $picture->picture_name;
        try {
            $qservice = new QiniuService;
            $res = $qservice->transfer_file($picture->bucket_name, $file_path);
            if($res->status) {
                // 儲存資料庫資訊
                $picture->picture_key = $res->result['key'];
                //在我的專案中有一個自動生成全域性唯一且遞增ID的方法,但是demo中沒做相關配置部分
                //demo中將picture_id直接設定成自增ID了
                // $picture->picture_id = $this->apply_full_global_id_str();
                $res_db = new Res;
                $res_db->data_row_count = $picture->isUpdate(false)->allowField(true)->save();
                if($res_db->data_row_count) {
                    // 寫入資料庫成功
                    $res_db->success();
                    $res_db->data = $picture;
                }
                // 將寫入資料庫的結果作為返回結果的一個屬性
                $res->result["db"] = $res_db;
                // 準備url
                // bucket對應的域名                
                $url = $res->result['domain'];
                // 圖片在bucket中的key
                $url .= $res->result['key'];
                // 預設插入水印板式
                $url .= '-'.Bucket::get(['bucket_name'=>$res->result['bucket']])->bucket_style_water;
            }
        } catch(\Exception $e) {
            $res->failed($e->getMessage());
            $url = '';
        }
        // 刪除伺服器圖片
        unlink('.'.$file_path);
        // 返回的是七牛雲上的url
        return $url;
    }
    
    /**
     * 在伺服器儲存圖片檔案
     * @param  [type] $ext     [description]
     * @param  [type] $content [description]
     * @param  [type] $path    [description]
     * @return [type]          [description]
     */
    private function save_picture($ext = null, $content = null, $path = null)
    {
        $full_path = '';
        if ($ext && $content) {
            do {
                $full_path = $path . uniqid() . '.' . $ext;
            } while (file_exists($full_path));
            $dir = dirname($full_path);
            if (!is_dir($_SERVER['DOCUMENT_ROOT'].$dir)) {
                mkdir($_SERVER['DOCUMENT_ROOT'].$dir, 0777, true);
            }
            file_put_contents($_SERVER['DOCUMENT_ROOT'].$full_path, $content);
        }
        return $full_path;
    }
}

(11) api/controller/Ueditor.php

class Ueditor extends ApiBase
{
    private $uploadfolder='/upload/';   //上傳地址

    private $scrawlfolder='/upload/_scrawl/';   //塗鴉儲存地址

    private $catchfolder='/upload/_catch/';   //遠端抓取地址

    private $configpath='/static/lib/ueditor/utf8-php/php/config.json';    //前後端通訊相關的配置

    private $config;


    public function index(){
        $this->type=input('edit_type','');

        date_default_timezone_set("Asia/chongqing");
        error_reporting(E_ERROR);
        header("Content-Type: text/html; charset=utf-8");

        $CONFIG = json_decode(preg_replace("/\/\*[\s\S]+?\*\//", "", file_get_contents($_SERVER['DOCUMENT_ROOT'].$this->configpath)), true);
        $this->config=$CONFIG;

        $action = input('action');
        switch ($action) {
            case 'config':
                $result =  json_encode($CONFIG);
                break;
        
                /* 上傳圖片 */
            case 'uploadimage':
                $result = $this->_qiniu_upload();
                break;
                /* 上傳塗鴉 */
            case 'uploadscrawl':
                $result = $this->_upload_scrawl();
                break;
                /* 上傳視訊,demo暫時沒有實現,可以檢視其他文章 */
            case 'uploadvideo':
                $result = $this->_upload(array('maxSize' => 1073741824,/*1G*/'exts'=>array('mp4', 'avi', 'wmv','rm','rmvb','mkv')));
                break;
                /* 上傳檔案,demo暫時沒有實現,可以檢視其他文章 */
            case 'uploadfile':
                $result = $this->_upload(array('exts'=>array('jpg', 'gif', 'png', 'jpeg','txt','pdf','doc','docx','xls','xlsx','zip','rar','ppt','pptx',)));
                break;
        
                /* 列出圖片 */
            case 'listimage':
                $result = $this->_qiniu_list($action);
                break;
                /* 列出檔案,demo暫時沒有實現,可以檢視其他文章 */
            case 'listfile':
                $result = $this->_list($action);
                break;        
                /* 抓取遠端檔案,demo暫時沒有實現,可以檢視其他文章 */
            case 'catchimage':
                $result = $this->_upload_catch();
                break;
        
            default:
                $result = json_encode(array('state'=> '請求地址出錯'));
                break;
        }
        
        /* 輸出結果 */
        if (isset($_GET["callback"]) && false ) {
            if (preg_match("/^[\w_]+$/", $_GET["callback"])) {
                echo htmlspecialchars($_GET["callback"]) . '(' . $result . ')';
            } else {
                echo json_encode(array(
                        'state'=> 'callback引數不合法'
                ));
            }
        } else {
            exit($result) ;
        }
    }
    private function _qiniu_upload($config=array())
    {
        $title = '';
        $url='';
        if(!empty($config)){
            $this->config=array_merge($this->config,$config);;
        }

        $file = request()->file('upfile');
        if($file){

            $picture = new Picture;
            // demo中暫時關閉關於admin的處理
            // $picture->admin_id = Session::has('admin_infor')?Session::get('admin_infor')->admin_id:'';
            $pservice = new PictureService;
            $res = $pservice->up_picture($file, $picture);
            if($res->status) {
                // bucket對應的域名                
                $url = $res->result['domain'];
                // 圖片在bucket中的key
                $url .= $res->result['key'];
                // 預設插入水印板式
                $url .= '-'.Bucket::get(['bucket_name'=>$res->result['bucket']])->bucket_style_water;

                $title = $res->result['key'];
                $state = 'SUCCESS';
            }else{
                $state = $res->message();
            }
        }else{
            $state = '未接收到檔案';
        }
        
        $response=array(
            "state" => $state,
            "url" => $url,
            "title" => $title,
            "original" =>$title,
        );
        return json_encode($response);
    }

    private function _upload_scrawl()
    {        
        $data = input('post.' . $this->config ['scrawlFieldName']);
        $url='';
        $title = '';
        $oriName = '';
        if (empty ($data)) {
            $state= 'Scrawl Data Empty!';
        } else {
            $pservice = new PictureService;
            // 在伺服器儲存圖片檔案
            $url = $pservice->up_scrawl('png', base64_decode($data), $this->scrawlfolder);
            if ($url) {
                $state = 'SUCCESS';
            } else {
                $state = 'Save scrawl file error!';
            }
        }
        $response=array(
        "state" => $state,
        "url" => $url,
        "title" => $title,
        "original" =>$oriName ,
        );
        return json_encode($response);
    }

    private function _qiniu_list($action)
    {
        /* 判斷型別 */
        switch ($action) {
            /* 列出檔案 */
            case 'listfile':
                $allowFiles = $this->config['fileManagerAllowFiles'];
                $listSize = $this->config['fileManagerListSize'];
                $prefix='/';
                break;
            /* 列出圖片 */
            case 'listimage':
            default:
                $allowFiles = $this->config['imageManagerAllowFiles'];
                $listSize = $this->config['imageManagerListSize'];
                $prefix='/';
        }
        // 這裡暫時沒有用20190606
        $start = 0;
        // 準備檔案列表
        $list = [];
        $picture = Picture::all();
        foreach($picture as $n=>$p) {
            $list[] = array(
                'url'=>$p->bucket->bucket_domain.$p->picture_key.'-'.$p->bucket->bucket_style_thumb,
                'title'=>$p->picture_name,
                'url_original'=>$p->bucket->bucket_domain.$p->picture_key.'-'.$p->bucket->bucket_style_water,
            );
        }
        /* 返回資料 */
        $result = json_encode(array(
            "state" => "SUCCESS",
            "list" => $list,
            "start" => $start,
            "total" => count($list)
        ));
        return $result;
    }

    /**
     * 遍歷獲取目錄下的指定型別的檔案
     * @param string $path
     * @param string $allowFiles
     * @param array $files
     * @return array
     */
    function getfiles($path, $allowFiles, &$files = array())
    {
        if (!is_dir($path)) return null;
        if(substr($path, strlen($path) - 1) != '/') $path .= '/';
        $handle = opendir($path);
        while (false !== ($file = readdir($handle))) {
            if ($file != '.' && $file != '..') {
                $path2 = $path . $file;
                if (is_dir($path2)) {
                    $this->getfiles($path2, $allowFiles, $files);
                } else {
                    if (preg_match("/\.(".$allowFiles.")$/i", $file)) {
                        $files[] = array(
                            'url'=> substr($path2, strlen($_SERVER['DOCUMENT_ROOT'])),
                            // 'document_root'=> $_SERVER['DOCUMENT_ROOT'],
                            // 'root_path'=> ROOT_PATH,
                            // 'path2'=> $path2,
                            // 'path'=> $path,
                            // 'mtime'=> filemtime($path2)
                        );
                    }
                }
            }
        }
        return $files;
    }
}

(12) 修改ueditor中的程式碼:

path-to-ueditor/ueditor.config.js

window.UEDITOR_CONFIG = {

        //為編輯器例項新增一個路徑,這個不能被註釋
        UEDITOR_HOME_URL: URL

        // 伺服器統一請求介面路徑
        
// 修改為自定義的serverUrl,demo中就是/api/ueditor/index
        , serverUrl: 
"/api/ueditor/index"
        //工具欄上的所有的功能按鈕和下拉框,可以在new編輯器的例項時選擇自己需要的重新定義
        // , toolbars: [[
        //     'fullscreen', 'source', '|', 
        //     'undo', 'redo', '|',
        //     'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 
        //     'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|',
        //     'rowspacingtop', 'rowspacingbottom', 'lineheight', '|',
        //     'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|',
        //     'directionalityltr', 'directionalityrtl', 'indent', '|',
        //     'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 
        //     'touppercase', 'tolowercase', '|',
        //     'link', 'unlink', 'anchor', '|', 
        //     'imagenone', 'imageleft', 'imageright', 'imagecenter', '|',
        //     'simpleupload', 'insertimage', 'emotion', 'scrawl', 'insertvideo', 'music', 'attachment', 'map', 'gmap', 'insertframe', 'insertcode', 'webapp', 'pagebreak', 'template', 'background', '|',
        //     'horizontal', 'date', 'time', 'spechars', 'snapscreen', 'wordimage', '|',
        //     'inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols', 'charts', '|',
        //     'print', 'preview', 'searchreplace', 'drafts', 'help'
        // ]]
        
// 修改:關閉不需要的按鈕
        , toolbars: [[
            'fullscreen', 'source', '|', 
            'undo', 'redo', '|',
            'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 
            'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|',
            'rowspacingtop', 'rowspacingbottom', 'lineheight', '|',
            'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|',
            'directionalityltr', 'directionalityrtl', 'indent', '|',
            'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 
            'touppercase', 'tolowercase', '|',
            'link', 'unlink', 'anchor', '|', 
            'imagenone', 'imageleft', 'imageright', 'imagecenter', '|',
            'simpleupload', 'insertimage', 'emotion', 'scrawl', 'map', 'insertframe', 'insertcode', 'pagebreak', 'template', '|',
            'horizontal', 'date', 'time', 'spechars', '|',
            'inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols', 'charts', '|',
            'print', 'preview', 'searchreplace', 'drafts', 'help'
        ]]

path-to-ueditor/ueditor.all.js

UE.commands['insertimage'] = {
    execCommand:function (cmd, opt) {

        ……if (img && /img/i.test(img.tagName) && (img.className != "edui-faked-video" || img.className.indexOf("edui-upload-video")!=-1) && !img.getAttribute("word_img")) {

            ……var floatStyle = first['floatStyle'];

        } else {
            var html = [], str = '', ci;
            ci = opt[0];
            if (opt.length == 1) {
                unhtmlData(ci);
                
// 修改:新增bootstrap的img-responsive樣式以支援響應式圖片
                str = '<img
class="img-responsive"
 src="' + ci.src + '" ' + (ci._src ? ' _src="' + ci._src + '" ' : '') +
                    (ci.width ? 'width="' + ci.width + '" ' : '') +
                    (ci.height ? ' height="' + ci.height + '" ' : '') +
                    (ci['floatStyle'] == 'left' || ci['floatStyle'] == 'right' ? ' style="float:' + ci['floatStyle'] + ';"' : '') +
                    (ci.title && ci.title != "" ? ' title="' + ci.title + '"' : '') +
                    (ci.border && ci.border != "0" ? ' border="' + ci.border + '"' : '') +
                    (ci.alt && ci.alt != "" ? ' alt="' + ci.alt + '"' : '') +
                    (ci.hspace && ci.hspace != "0" ? ' hspace = "' + ci.hspace + '"' : '') +
                    (ci.vspace && ci.vspace != "0" ? ' vspace = "' + ci.vspace + '"' : '') + '/>';
                if (ci['floatStyle'] == 'center') {
                    str = '<p style="text-align: center">' + str + '</p>';
                }
                html.push(str);

            } else {
                for (var i = 0; ci = opt[i++];) {
                    unhtmlData(ci);
                    
// 修改:新增bootstrap的img-responsive樣式以支援響應式圖片
                    str = '<p ' + (ci['floatStyle'] == 'center' ? 'style="text-align: center" ' : '') + '><img
class="img-responsive"
 src="' + ci.src + '" ' +
                        (ci.width ? 'width="' + ci.width + '" ' : '') + (ci._src ? ' _src="' + ci._src + '" ' : '') +
                        (ci.height ? ' height="' + ci.height + '" ' : '') +
                        ' style="' + (ci['floatStyle'] && ci['floatStyle'] != 'center' ? 'float:' + ci['floatStyle'] + ';' : '') +
                        (ci.border || '') + '" ' +
                        (ci.title ? ' title="' + ci.title + '"' : '') + ' /></p>';
                    html.push(str);
                }
            }
            ……
        }
        ……
    }
};
UE.plugin.register('simpleupload', function (){
    ……function callback(){
                    try{
                        var link, json, loader,
                            body = (iframe.contentDocument || iframe.contentWindow.document).body,
                            result = body.innerText || body.textContent || '';
                        json = (new Function("return " + result))();
                        link = me.options.imageUrlPrefix + json.url;
                        if(json.state == 'SUCCESS' && json.url) {
                            loader = me.document.getElementById(loadingId);
                            loader.setAttribute('src', link);
                            loader.setAttribute('_src', link);
                            loader.setAttribute('title', json.title || '');
                            loader.setAttribute('alt', json.original || '');
                            loader.removeAttribute('id');
                            domUtils.removeClasses(loader, 'loadingclass');
                            
// 修改:新增bootstrap的img-responsive樣式以支援響應式圖片 domUtils.addClass(loader, 'img-responsive');

                        } else {
                            showErrorLoader && showErrorLoader(json.state);
                        }
                    }catch(er){
                        showErrorLoader && showErrorLoader(me.getLang('simpleupload.loadError'));
                    }
                    form.reset();
                    domUtils.un(iframe, 'load', callback);
                }
    ……
});

path-to-ueditor/dialogs/image/image.js

/* 新增圖片到列表介面上 */
        pushData: function (list) {
            ……                    domUtils.on(img, 'load', (function(image){
                        return function(){
                            _this.scale(image, image.parentNode.offsetWidth, image.parentNode.offsetHeight);
                        }
                    })(img));
                    img.width = 113;
                    img.setAttribute('src', urlPrefix + list[i].url + (list[i].url.indexOf('?') == -1 ? '?noCache=':'&noCache=') + (+new Date()).toString(36) );
                    
// 修改:設定插入圖片時引用七牛的原圖(水印)樣式
                    img.setAttribute('_src', urlPrefix + list[i].url_original);
                    
// 修改:給圖片新增titleicon.setAttribute('title', list[i].title);

                    domUtils.addClass(icon, 'icon');

                    item.appendChild(img);
                    item.appendChild(icon);
                    this.list.insertBefore(item, this.clearFloat);
                }
            }
        },

path-to-editor/dialogs/image/image.html

<div id="tabhead" class="tabhead">
            <span class="tab" data-content-id="remote"><var id="lang_tab_remote"></var></span>
            <span class="tab focus" data-content-id="upload"><var id="lang_tab_upload"></var></span>
            <span class="tab" data-content-id="online"><var id="lang_tab_online"></var></span>
            
<!-- 修改,關閉圖片搜尋介面 -->
            <!-- <span class="tab" data-content-id="search"><var id="lang_tab_search"></var></span> -->
        </div>
        <div class="alignBar">
            ……
        </div>
        <div id="tabbody" class="tabbody">

            ……
            <!-- 搜尋圖片 -->
            
<!-- 修改:關閉圖片搜尋介面 -->
<!--             <div id="search" class="panel">
                <div class="searchBar">
                    <input id="searchTxt" class="searchTxt text" type="text" />
                    <select id="searchType" class="searchType">
                        <option value="&s=4&z=0"></option>
                        <option value="&s=1&z=19"></option>
                        <option value="&s=2&z=0"></option>
                        <option value="&s=3&z=0"></option>
                    </select>
                    <input id="searchReset" type="button"  />
                    <input id="searchBtn" type="button"  />
                </div>
                <div id="searchList" class="searchList"><ul id="searchListUl"></ul></div>
            </div>
 -->
        </div>

path-to-ueditor/third-part/webuploader/webuploader*.js

由於七牛雲在多執行緒上傳時會時常報錯,所以我們需要按照佇列一個一個去上傳就好了,上傳呼叫的是百度自家的webuploader元件。我沒有仔細研究ueditor到底呼叫的是哪一個檔案,乾脆就把所有檔案中的
threads:3
改成
threads:1

8. 除錯改錯和已知bug:

除錯改錯這個過程是必須要經歷的,有時候還是非常痛苦的,很多細小的忽視都會導致程式執行失敗,認真並耐心就好了。

已知bug:

程式裡使用unlink('.'.$file_path);這一句用來刪除塗鴉臨時儲存在應用伺服器上的檔案,但是有時候會出現刪不掉的情況。

9. GitHub:

我把完整的Demo上傳到的我的GitHub倉庫中,如需要完整原始碼可自行下載:

https://github.com/wandoubaba/tp-ue-qn-db

10. 效果演示:

image

image

image

image

image

image

image

image

image

image

11. 結束語

文字是我對前段時間所做研究的一個完整的覆盤,但是即使是覆盤,也並沒有一下子就執行成功,而且在覆盤時又除錯出了新的bug,由此可見,對一些在專案中學習到的新技術進行適當的覆盤重現,可以加深自己對技術的掌握,同時也能幫助到其他人,雖然多花了一些時間,但是我認為是值得的。

感謝你花時間讀完了文章,如果你對需求有更好的解決方法,或者發現文中的錯誤和不足,也請你不吝賜教,互相交流以共同進步。

相關文章