2015年1月30日 星期五

關於詹事府

今天在 Wiki 讀到 "詹事府", 我雖愛讀歷史, 但卻是頭一回看到這詞彙. 原來這是以前封建時代隸屬於太子的東宮官署, 源自秦朝的詹事一職, 乃太子僚屬, 唐朝才開始設詹事府主掌東宮事務, 歷朝均沿襲之. 古書中常見的 "太子洗馬" 一職便是詹事府司經局下設的官職, 唐太宗時以犯顏直諫聞名的魏徵在玄武門之變前即在太子李建成的東宮任太子洗馬. 此官銜乍看之下會讓人以為是幫太子的坐騎刷洗的賤工作, 事實上它是從五品的中級文官, 也是士族子弟當官的捷徑, 漢朝時原本的官名為 "先馬", 隋朝時才改稱洗馬, 主要的工作是太子出行時為儀仗隊前導, 後來也掌朝廷典籍收藏, 逐漸變成太子的侍從官, 也就是機要秘書.

詹事府主官為詹事, 清朝時官階與大理寺卿 (司法院長) 同, 為正三品. 清代詹事府執掌與翰林院業務重疊度高, 例如史書修撰以及為皇上講課的侍讀侍講等, 所以官員也常兼翰林院職務. 但乾隆皇帝認為翰林院修撰乃進士一甲一名的專銜 (也就是聯考第一名才有資格的專職), 故下旨以後詹事府官員免兼翰林院修撰.

* 晚上去 YAMAHA 接姐姐時, 我考她是否知道太子洗馬? 她竟然知道耶! 原來她這學期曾向班上提出北宋科學家沈括的報告, 把他的身家調查了一番, 沈括的外公就擔任過太子洗馬 (沈括家世顯赫, 父祖均曾任大理寺丞).


2015年1月26日 星期一

用 jQuery EasyUI 打造輕量級 CMS (十)

今天在開無聊的會時把應用程式上傳安裝功能大翻修, 拿掉了原先的登錄按鈕, 因為先上傳再登錄感覺多此一舉, 所以合併為上傳同時登錄.

首先在系統安裝程式 install.php 裡添加 sys_apps 資料表, 用來記錄應用程式的安裝資訊 :

    //建立 sys_apps 資料表
    $data_array["id"]="smallint(6) NOT NULL AUTO_INCREMENT PRIMARY KEY";
    $data_array["app_name"]="varchar(255)";        //應用程式名稱
    $data_array["installed"]="char(1)";            //已安裝 "Y"/"N"
    $data_array["show_tabs"]="char(1)";            //顯示頁籤 "Y"/"N"
    $data_array["tab_names"]="varchar(255)";       //頁籤名稱
    $data_array["table_names"]="varchar(255)";     //資料表名稱
    $data_array["remark"]="varchar(255)";          //安裝結果
    $result=create_table("sys_apps",$data_array);
    if ($result) {$msg .= "建立資料表 sys_apps ... 完成!<br>";}
    $data_array=NULL;

這裡欄位 tab_names 用來記錄應用程式的頁籤名稱 (以逗號隔開), 以便於應用程式 app.php 中讀取後產生頁籤的 div 元素. 欄位 table_names 則是記錄應安裝程式 app_install.php 本身所建立的資料表名稱 (也是以逗號隔開),, 以便要刪除該應用程式時可以讀取此欄位值, 刪除資料庫中所有此應用程式之資料表. 安裝成功後, 欄位 installed 會設定為 "Y" 表示已安裝, 避免二次安裝. 而欄位 show_tabs 是從 jQueryUI 架站機來的, 此處似乎用不到, 可刪除. 

系統頁籤資料表 sys_tabs 中也要加入一筆紀錄 :

    //插入 sys_tabs 之應用程式標籤
    $data_array["tab_name"]="apps";
    $data_array["tab_label"]="應用程式";
    $data_array["tab_link"]="sys.php?op=apps";
    $data_array["tab_level"]=9;
    $data_array["tab_tip"]="應用程式";
    $data_array["tab_order"]=96;
    $result=insert("sys_tabs", $data_array);
    $data_array=NULL;

在頁籤 apps 中, 其資料來源 (href) 是取自系統程式 sys.php 中的 apps 模組, 因此要在 sys.php 裡添加一個 case, 裡面主要是一個放頁籤內容的 div 元素 :

  case "apps" : {
?>
<div class="tab" title="應用程式">
</div>
<?php
    break;
    }

然後我們要在這個空的頁籤放一個 table 元素轉化的 datagrid, 用來表列已上傳登錄的程式 :

  case "apps" : {
?>
<div class="tab" title="應用程式">
  <!--應用程式 sys_apps 列表-->
  <table id="sys_apps" title="應用程式列表" style="width:auto" data-options="tools:'#apps_tools',toolbar:'#apps_toolbar'"></table>
  <div id="apps_tools"> 
    <a href="#" id="reload_apps" class="icon-reload" title="重新載入"></a>
  </div>
  <div id="apps_toolbar" style="text-align:right;padding:2px;">
    <a href="#" id="upload_app" class="easyui-linkbutton" data-options="iconCls:'icon-add'">上傳</a>
    <a href="#" id="install_app" class="easyui-linkbutton" data-options="iconCls:'icon-ok'">安裝</a>
    <a href="#" id="remove_app" class="easyui-linkbutton" data-options="iconCls:'icon-remove'">移除</a>
  </div>   
  <!--上傳應用程式表單對話框-->
  <div id="upload_app_dialog" class="easyui-dialog" title="上傳應用程式" style="width:420px;height:200px;"  data-options="closed:'true',buttons:'#upload_app_buttons'">
    <form id="upload_app_form" style="padding:15px" method="post" enctype="multipart/form-data" target="upload_target">
      <div style="margin:10px">
        <label style="width:60px;display:inline-block;">主檔案 : </label>
        <input name="uploader[]" id="main" class="easyui-filebox"  data-options="missingMessage:'此欄位為必填',required:true,buttonText:'選擇主檔案',prompt:'app.php'" style="width:270px">
      </div>
      <div style="margin:10px">
        <label style="width:60px;display:inline-block;">安裝檔 : </label>
        <input name="uploader[]" id="install" class="easyui-filebox"  data-options="missingMessage:'此欄位為必填',required:true,buttonText:'選擇安裝檔',prompt:'app_install.php'" style="width:270px">
      </div>
      <div>
        <iframe id="upload_target" name="upload_target" src="#"           style="width:0;height:0;border:0px solid #ffffff;">
        </iframe>
      </div>
    </form>
  </div>
  <div id="upload_app_buttons" style="padding-right:15px;">
    <a href="#" id="clear_app" class="easyui-linkbutton" iconCls="icon-clear" style="width:90px">重設</a>
    <a href="#" id="upload_app_go" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">上傳</a>
  </div>
</div>


這裡除了作為 datagrid 的 table 外, 還有一個上傳應用程式檔案的 form (預設是關閉的 closed:true), 其編碼類型 enctype 必須為 multipart/form-data, 而其 target 屬性則指定一個 iframe 元素, 亦即後端程式的回應要投射到 iframe 裡, 這樣做的原因是因為我們無從得知後端何時可完成上傳作業,   乾脆讓後端完成後自己把回應訊息用一個函式 丟給 iframe 去顯示, 這函式原先放在 sys.php 中, 在 Chrome/Firefox 均無問題, 但在 IE 就會有語法錯誤, 我判斷是原因可能是此函式用 Ajax 傳回之故, 為了解決此問題, 便將此函式移到上一層的  main.php 中了 :  

    <!-- 應用程式上傳回呼函式 -->
    //不可放在 sys.php 內 (IE 會語法錯誤)
    function upload_app_callback(status,msg) { //必須在最上層, 不可放在 $ 內
      $("#upload_app_dialog").dialog("close");  //關閉上傳對話框
      $("#sys_apps").datagrid("reload",{op:"list_apps"});  //重載 datagrid
      if (status=="success") { //顯示上傳結果
        var msg=msg + '檔案上傳全部成功! <br>';
        var icon="info";
        }
      else {
        var msg=msg + '檔案上傳失敗, 請重新上傳! <br>';
        var icon="error";
        }
      $.messager.alert("上傳結果",msg,icon);
      }

應用程式由主程式 app.php 與其安裝檔 app_install.php 組成, 我們用兩個 filebox 元件來選取要上傳的檔案, 其 name 均為 uploader[], 上傳後會放在根目錄的 apps 子目錄下.  上面的 apps 應用程式模組中的 datagrid 與上傳表單的控制程式如下 :

  $(function(){
    //應用程式 sys_apps
    $('#sys_apps').datagrid({
      columns:[[
        {field:'id',title:'id',sortable:true},
        {field:'app_name',title:'名稱',sortable:true},
        {field:'installed',title:'已安裝',sortable:true},
        {field:'show_tabs',title:'顯示頁籤',sortable:true},
        {field:'tab_names',title:'頁籤名稱',align:'center',sortable:true},
        {field:'table_names',title:'資料表名稱',sortable:true},
        {field:'remark',title:'備註',sortable:true}
        ]],
      url:"sys.php",
      queryParams:{op:"list_apps"},
      fitColumns:true,
      singleSelect:true,
      pagination:true,
      pageSize:10,
      rownumbers:true
      });
    $("#clear_app").bind("click",function(){
      $("#upload_app_form")[0].reset();
      });
    $("#upload_app_go").bind("click",function(){
      var main=$("#main").filebox("getText");
      main=main.substr(main.lastIndexOf("\\") + 1, main.length);
      var app_main=main.split(".")[0]; //主檔名
      var install=$("#install").filebox("getText");
      install=install.substr(install.lastIndexOf("\\") + 1, install.length);
      var install_main=install.split("_")[0]; //安裝主檔名
      var reg=/.php$/i; //副檔名均為 .php
      if (!reg.test(main) || !reg.test(install)) {
        var msg="主程式與其安裝檔必須均為 .php 檔!<br> 請重新選取.";
        $.messager.alert("訊息",msg,"error");
        return;
        }
      reg=/_install.php$/i; //過濾出安裝檔
      if (!reg.test(install)) {
        var msg="安裝檔檔名格式不正確!<br>格式 : APP_install.php<br>";
        $.messager.alert("訊息",msg,"error");
        return;    
        }    
      if (app_main != install_main) { //主檔名不同
        msg="此兩檔案並非同一組應用程式! <br>" +
            "主程式與其安裝檔之檔名格式 : " +
            "APP.php 與 APP_install.php<br>請重新選取.<br>";
        $.messager.alert("訊息",msg,"error");
        return;
        } //end of if
      reg=/\W/g; //非英數字與底線
      if (reg.test(app_main) || reg.test(install_main)) {
        var msg="應用程式及其安裝檔檔名必須均為英數字或底線!<br>" +
                "請重新選取.<br>"; ;
        $.messager.alert("訊息",msg,"error");
        return;    
        }
      $("#upload_app_form").form("submit",{
        url:"sys.php?op=upload_app&app_main=" + app_main
        });
      });
    $("#upload_app").bind("click",function(){ //顯示上傳表單
      $("#upload_app_dialog").dialog("open").dialog("setTitle","上傳應用程式");
      $("#upload_app_form").form("clear");
      });
    $("#reload_apps").bind("click",function(){
      $("#sys_apps").datagrid("load",{op:"list_apps"});
      });
    $("#install_app").bind("click",function(){
      var row=$("#sys_apps").datagrid("getSelected");
      if (row) {
        if (row.installed=="N") {
          var url="apps/" + row.app_name + "_install.php";
          $.ajax({
            type:"POST",
            url:url,
            cache:false,
            dataType:"json",
            success:function(data) {
              $("#sys_apps").datagrid("reload",{op:"list_apps"}); //更新列表
              if (data.result=="success") {
                $.messager.alert("訊息","應用程式安裝成功!","info");
                }
              else {$.messager.alert("訊息",data.error,"error");}              
              },
            error:function(xhr, thrownError) {
              var msg='<p>應用程式安裝失敗!<br>狀態 : ' +
                      xhr.status + " - " + thrownError + '</p>';
              $.messager.alert("訊息",msg,"error");                      
              }
            });
          }
        else {$.messager.alert("訊息","此應用程式已安裝過了!","warning");}
        }
      else {$.messager.alert("訊息","請選擇要安裝的應用程式!","info");}
      });
    $("#remove_app").bind("click",function(){
      var row=$("#sys_apps").datagrid("getSelected");
      if (row) {
        $.messager.confirm("確認","確定要刪除這個應用程式嗎?",function(btn){
          if (btn){
            var params={op:"remove_app",id:row.id};
            var callback=function(data){
              if (data.status==="success"){
                $("#sys_apps").datagrid("reload",{op:"list_apps"});
                }
              else {$.messager.alert("訊息",data.msg,"error");}          
              };              
            $.post("sys.php",params,callback,"json");
            }
          })
        }
      else {$.messager.alert("訊息","請選擇要刪除的應用程式!","info");}
      });
    }); //end of jQuery

首先是 datagrid 的設定, 資料來源為 sys.php 中的 list_apps 模組, 其程式如下 :

  case "list_apps" : {
    $page=isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 1;
    $rows=isset($_REQUEST['rows']) ? intval($_REQUEST['rows']) : 10;
    $sort=isset($_REQUEST['sort']) ? $_REQUEST['sort'] : 'id';
    $order=isset($_REQUEST['order']) ? $_REQUEST['order'] : 'asc';
    if (isset($_REQUEST['search_field'])) { //有 search
      $where="WHERE ".$_REQUEST['search_field']." LIKE '%".
             $_REQUEST['search_what']."%'";
      }
    else {$where="";} //無 search
    $start=($page-1) * $rows;  //本頁第一個列索引 (0 起始)
    $SQL="SELECT COUNT(*) FROM `sys_apps`";
    $RS=run_sql($SQL);
    $total=$RS[0][0]; //紀錄總筆數
    $SQL="SELECT * FROM sys_apps ".$where." ORDER BY ".
         $sort." ".$order." LIMIT ".$start.",".$rows;
    $RS=run_sql($SQL);
    $apps=Array();
    if (is_array($RS)) {
      for ($i=0; $i<count($RS); $i++) {
        $apps[$i]=Array("id" => $RS[$i]["id"],
                        "app_name" => $RS[$i]["app_name"],
                        "installed" => $RS[$i]["installed"],
                        "show_tabs" => $RS[$i]["show_tabs"],
                        "tab_names" => $RS[$i]["tab_names"],
                        "table_names" => $RS[$i]["table_names"],
                        "remark" => $RS[$i]["remark"]
                        );
        }
      }
    $arr=array("total" => $total, "rows" => $apps);
    echo json_encode($arr);
    break;
    }

這邊其實用不到 search 功能 (因為應用程式不會很多, 不需要, 甚至根本不需要分頁, 但我是整組複製過來改, 懶得刪除了).

當按下上傳鈕 upload_app 時, 會顯示上傳表單 :

    $("#upload_app").bind("click",function(){ //顯示上傳表單
      $("#upload_app_dialog").dialog("open").dialog("setTitle","上傳應用程式");
      $("#upload_app_form").form("clear");
      });


按檔案盒右側的按鈕會彈出檔案選取視窗, 分別選好主程式與安裝程式後, 按上傳即可, 下圖是上傳工作日誌應用程式 :


注意, 選好檔案後, filebox 的文字欄位顯示的不是真實的位址, 而是一律以 fakepath 代替. 按鈕 upload_app_go 的 click 事件處理會先檢查上傳檔名是否符合既定的格式 (APP.php 與 APP_install.php), 首先必須從檔案盒的文字欄位取出路徑與檔名, 這必須使用 EasyUI 的 filebox 繼承自 textbox 的 getText 方法, 不能用 jQuery 的 val() 方法, 因為 EasyUI 又對 jQuery 物件進行了包裝. 然後用 lastIndexOf 取出最後一個倒斜線後面的檔名, 再辨別是否副檔名均為 php, 安裝檔是否以 "_install.php" 結尾等等, 通過後就將表單附帶應用程式主檔名提交給後端 sys.php 的 upload_app 模組處理 :

  case "upload_app" : { //ajax:處理 app 檔案上傳用
    $status="success";       //預設上傳成功
    $msg="";                 //執行結果字串
    $upload_dir="./apps/";   //設定上傳目錄
    $result=upload_files("uploader", $upload_dir);
    if (is_array($result)) { //成功傳回陣列
      for ($i=0; $i<count($result); $i++) {
        $msg .= $result[$i]["name"]." 上傳成功(".$result[$i]["size"].
                " bytes)<br>";
        }
      //登錄於 sys_apps 資料表
      $data_array["app_name"]=$_REQUEST["app_main"];
      $data_array["installed"]="N";
      $data_array["show_tabs"]="N";    //安裝時更新
      $data_array["tab_names"]="";     //安裝時填入
      $data_array["table_names"]="";   //安裝時填入
      $data_array["remark"]="";        //安裝時更新
      $result=insert("sys_apps", $data_array);
      }
    else { //上傳失敗
      $msg .= "檔案上傳失敗 : <br>".$result;
      $status="failure";
      } //end of else
    $para='"'.$status.'","'.$msg.'"'; //回傳回呼函式之參數
?>
<!-- 輸出 script 到 $op=apps 中的 iframe 並執行其中的回呼函式 -->
<script language="javascript" type="text/javascript">
  window.top.window.upload_app_callback(<?php echo $para; ?>);
</script>
<?php
    break;
    }

此模組主要是呼叫 upload_files() 這個上傳函式來處理檔案上傳, 成功的話就在 sys_apps 資料表中紀錄此應用程式檔案名稱與未安裝狀態, 最後把上傳結果放在呼叫最上層方法 upload_app_callback() 的參數字串 $para 中, 傳回給要求者 (sys.php 的 apps 模組). 這樣就會執行此最上層回呼函式來顯示上傳結果. 這個 upload_app_callback() 函式是放在版面布局程式 main.php 中, 已如上述.

上面這個上傳程式的主角是 upload_files() 函式, 收錄在 lib/files.php 函式庫中 :

/*-----------------------------------------------------------------------------
upload_files($uploader,$path="./")
功能 :
  此函數將上傳之多個檔案, 由暫存區移至指定路徑下.
參數 :                                
  $uploader : 上傳元件名稱, 乃陣列形式, 例如下列 input 中之 name 值
              檔案1 : <input type="file" name="uploader[]">
              檔案2 : <input type="file" name="uploader[]">
              上傳表單之 enctype 須設為 multipart/form-data :
              <form action="upload.php" method="post"
              enctype="multipart/form-data">
  $path     : 檔案儲存路徑, 例如 "./images" (預設值為目前程式所在目錄 ./)
傳回值 :
  成功傳回一個二維陣列, 失敗傳回錯誤字串. 陣列第一維為數字索引, 第二維為關聯式,
  有三個元素 : 例如第一個上傳檔案 :
  $result[0]["name"]=檔案名稱
  $result[0]["type"]=檔案類型
  $result[0]["size"]=檔案大小
範例 :
  <input type="file" name="uploader[]">
  $result=upload_files("uploader", "./images");
  for ($i=0; $i<count($result); $i++) {
       echo $result[$i]["name"]."上傳成功(".$result[$i]["size"]." bytes)<br>";
       }
-----------------------------------------------------------------------------*/
function upload_files($uploader,$path="./") {
  $counts=count($_FILES[$uploader]["name"]); //上傳檔案數
  for ($i=0; $i<$counts; $i++) {
       if ($_FILES[$uploader]["error"][$i]==0) { //上傳成功
           $file_name=$_FILES[$uploader]["name"][$i];
           $tmp_name=$_FILES[$uploader]["tmp_name"][$i];
           $new_name=$path.mb_convert_encoding($file_name,"big5","utf-8");
           if (move_uploaded_file($tmp_name, $new_name)) { //移動成功
               $result[$i]["name"]=$_FILES[$uploader]["name"][$i];
               $result[$i]["type"]=$_FILES[$uploader]["type"][$i];
               $result[$i]["size"]=$_FILES[$uploader]["size"][$i];
               } //end of if
           else {$result.=$file_name." 移動失敗<br>";} //移動失敗
           } //end of if
       else {$result.=$file_name." 上傳失敗<br>";} //上傳失敗
       } //end of for
  return $result;
  }

這個函式與之前在檔案上傳功能時用的 upload_file() 不同之處在於 upload_files() 可傳多檔, 而 upload_file() 只能傳單檔. 所以這裡必須有個迴圈來逐一處理上傳的每個檔案. PHP 函式 mb_convert_encoding() 用來處理中文檔名問題, 但這裡用不到 (檔名僅允許英數字與底線).

上傳成功時結果如下 :


這時點選該列應用程式, 再按安裝鈕, 就會執行上面 Script 中的事件處理程序 :

$("#install_app").bind("click",function(){...參考上面}

此程式會將上傳對話框中的表單以 Ajax 方式提交給已上傳到 ./apps/ 下的 APP_install.php 安裝程式執行, 該安裝程式必須將本身所建立的資料表名稱填入系統資料表 sys_apps 的 table_names 欄位 (以逗號隔開), 以及將所有自己的頁籤填入 tab_names 欄位 (這是繼承舊系統規劃而來, 用不到, 以後可能刪除), 然後傳回執行結果. 由於安裝程式可能出錯, 因此這裡採用 jQuery 最低層的 ajax() 方法來執行非同步請求, 以便能取得較多的錯誤訊息 :

          var url="apps/" + row.app_name + "_install.php";
          $.ajax({... 參考上面});

應用程式安裝成功後, 除了顯示結果訊息外, 同時也重新載入 datagrid 內容, 這時就會顯示已安裝 :


這個安裝程式 APP_install.php 範本如下 :

<?php
/*-----------------------------------------------------------------------------
Title        : 應用程式安裝檔
Author       : Tony
Version      : v1.0.0
Prototype    : 2015-01-07
Last Updated : 2015-01-09
Usage        : 安裝應用程式所使用之資料表, 填入初始值
Note         :
-----------------------------------------------------------------------------*/
/*=== 系統固定的部分 (勿修改) ===*/
session_start(); //啟動 session 功能
header('Content-type: text/html; charset=utf-8');
//檢查是否已登入, 否則回登入畫面
if (!isset($_SESSION["user_account"])) {header("Location: index.php");}
//設定台北時間
date_default_timezone_set("Asia/Taipei");
//匯入資料庫設定與函式庫
require_once("../db.php");           //匯入資料庫設定檔 (必須)
require_once("../lib/mysql.php");    //匯入資料庫模組   (必須)
//變數設定
$success=FALSE;   //整體成功或失敗旗標
$error=Array();   //儲存錯誤訊息用
$tabs=Array();    //儲存頁籤用
$tables=Array();  //儲存表單用

/*=== 應用程式頁籤範本 (只改 APP 名稱 & 複製修改) ===*/
//建立 APP_tabs 資料表 (必須)
$tables[]="APP_tabs";  //"APP" 要改
$data_array["id"]="smallint(6) NOT NULL AUTO_INCREMENT PRIMARY KEY";
$data_array["tab_name"]="varchar(255)";
$data_array["tab_label"]="varchar(255)";
$data_array["tab_link"]="varchar(255)";
$data_array["tab_level"]="tinyint(4)"; //1 (使用者) ~9 (管理者)
$data_array["tab_tip"]="varchar(255)";
$data_array["tab_order"]="tinyint(4)";
$result=create_table("APP_tabs",$data_array);    //"APP" 要改
if ($result===TRUE) {$success=TRUE;}
else {$error[]="建立資料表 APP_tabs ... 失敗!";} //"APP" 要改
$data_array=NULL;

//插入 APP_tabs (範本)
$tabs[]="TAB1";  //顯示頁籤標題用
$data_array["tab_name"]="TAB1";
$data_array["tab_label"]="TAB1";
$data_array["tab_link"]="apps/APP.php?op=TAB1";  //"APP" 要改
$data_array["tab_level"]=1;
$data_array["tab_tip"]="TAB1";
$data_array["tab_order"]=1;
$result=insert("APP_tabs", $data_array);         //"APP" 要改
if ($result===TRUE) {$success=TRUE;}
else {$error[]="插入資料表 APP_tabs ... 失敗!";}  //"APP" 要改
$data_array=NULL;

//插入 APP_tabs (範本)
$tabs[]="TAB2";  //顯示頁籤標題用
$data_array["tab_name"]="TAB2";
$data_array["tab_label"]="TAB2";
$data_array["tab_link"]="apps/APP.php?op=TAB2";  //"APP" 要改
$data_array["tab_level"]=1;
$data_array["tab_tip"]="TAB2";
$data_array["tab_order"]=1;
$result=insert("APP_tabs", $data_array);         //"APP" 要改
if ($result===TRUE) {$success=TRUE;}
else {$error[]="插入資料表 APP_tabs ... 失敗!";} //"APP" 要改
$data_array=NULL;

/*--- 新增其他頁籤請複製上面的 TAB1/TAB2 範本於此改寫 ---*/

/*=== 安裝其他資料表範本 (請複製修改, 完成後刪除範本) ===*/
//建立 APP_table1 資料表 (範本)
$tables[]="APP_table1"; //刪除 APP 時用到 (必須)
$data_array["id"]="int(10) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY";
$data_array["field1"]="varchar(255)";
$data_array["field2"]="char(1)";
$data_array["field3"]="text";      
$data_array["field4"]="date";
$data_array["field5"]="datetime";
$data_array["field6"]="smallint(6) unsigned NOT NULL DEFAULT '0'";
$data_array["field7"]="tinyint(3) unsigned NOT NULL DEFAULT '10'";
$data_array["field8"]="int(10) unsigned";          
$data_array["field9"]="bigint(20) unsigned";                        
$data_array["field10"]="float unsigned";
$result=create_table("APP_table1",$data_array);
if ($result===TRUE) {$success=TRUE;}
else {$error[]="建立資料表 APP_table1 ... 失敗!";} //"APP_table1" 要改
$data_array=NULL;

//插入 APP_table1 (AUTO_INCREMENT 不用填) (範本)
$SQL="INSERT INTO `app_table1` (".
     "`field1`,".
     "`field2`,".
     "`field3`,".
     "`field4`,".
     "`field5`,".
     "`field6`,".
     "`field7`,".
     "`field8`,".
     "`field9`,".
     "`field10`".
     "`) VALUES (".
     "'',".
     "'',".
     "'',".
     "'',".
     "'',".
     ",".
     ",".
     ",".
     ",".
     "".
     ")";
$result=run_sql($SQL);
if ($result===TRUE) {$success=TRUE;}
else {$error[]="插入資料表 APP_table1 ... 失敗!";} //"APP_table1" 要改
$data_array=NULL;

/*--- 新增其他資料表請複製上面的 APP_table1 範本於此改寫 ---*/

/*=== 更新系統 sys_apps 資料表 (只改 APP 名稱)===*/
//更新系統 sys_apps 資料表的 app_name='程式名稱' 欄位
$data_array["installed"]="Y";                  //已安裝 "Y"/"N"
$data_array["show_tabs"]="Y";                  //顯示頁籤 "Y"/"N"
$data_array["tab_names"]=join(",",$tabs);      //儲存 tabs 以便產生頁籤
$data_array["table_names"]=join(",",$tables);  //儲存 tables 以便刪除時清理
$data_array["remark"]="OK";                    //安裝結果
$result=update("sys_apps", $data_array, "app_name", "APP"); //"APP" 要改
if ($result===TRUE) {$success=TRUE;}
else {$error[]="更新資料表 sys_apps ... 失敗!";}
$data_array=NULL;

//在 sys_header_links 插入應用程式入口超連結
$data_array["title"]="APP";             //"APP" 要改            
$data_array["url"]="javascript:APP()";  //"APP" 要改                
$data_array["target"]="_self";
$data_array["sequence"]=1;
$data_array["hint"]="APP";              //"APP" 要改
$result=insert("sys_header_links", $data_array);
if ($result===TRUE) {$success=TRUE;}
else {$error[]="插入資料表 sys_header_links ... 失敗!";}
$data_array=NULL;

/*====== 輸出 JSON 格式之安裝結果 (不用改) ======*/
$arr["result"]=$success ? "success" : "failure";
$arr["error"]=join("<br>",$error);
echo json_encode($arr);
?>

注意,這裡我們用 join() 函式把所記錄的資料表名稱以逗號串接後寫入 table_names 欄位裡, 以備將來要刪除應用程式時斬草除根之用.

而主程式 APP.php 範本如下 :

<?php
/*-----------------------------------------------------------------------------
Title        : 應用程式
Translator   : Tony
Version      : v1.0.0
Prototype    : 2015-01-05
Last Updated : 2015-01-05
Usage        : 應用程式的主要功能寫在此檔中
Note         :
請根據安裝檔內寫入 tabs 的各頁籤 $tab 與各作業 $op 撰寫功能
Ajax/DataTable 的 url 格式 :
$url="apps/app.php?op=get_xxxx";
$url="apps/app.php?op=list_xxxx";
$url="apps/app.php?op=add_xxxx";
$url="apps/app.php?op=remove_xxxx";
$url="apps/app.php?op=update_xxxx";
-----------------------------------------------------------------------------*/
session_start(); //必須啟動才能用 session
header('Content-Type: text/html;charset=UTF-8');
require_once("../db.php");           //匯入資料庫設定檔 (必須)
require_once("../lib/mysql.php");    //匯入資料庫模組   (必須)
require_once("../lib/tools.php");    //匯入工具模組     (選項)
require_once("../lib/parse.php");    //匯入剖析模組     (選項)
require_once("../lib/file.php");     //匯入file模組     (選項)
require_once("../lib/http.php");     //匯入http模組     (選項)
$op=$_REQUEST["op"];   //功能
switch ($op) {
  case "TAB1" : { //for ajax
    echo "Hello TAB1! The time is ".date("Y-m-d H:i:s");
    break;
    }
  case "TAB2" : { //for ajax
    echo "Hello TAB2! The time is ".date("Y-m-d H:i:s");
    break;
    }
  default : { //for rendering
?>
    <div id="APP-tab" class="easyui-tabs" data-options="fit:'true'">
<?php
    $RS=search("APP_tabs");  //"APP" 要改
    if (is_array($RS)) {
      for ($i=0; $i<count($RS); $i++) {
?>
      <div class="tab" title="<?php echo $RS[$i]["tab_name"] ?>" data-options="href:'<?php echo $RS[$i]["tab_link"] ?>'">
      </div>
<?php
        } //for
      } //if
    } //default
  } //switch
?>
    </div>

以後要寫任何掛在此系統上的應用程式就用此兩範本去擴展即可.

最後, 點選要刪除的 app 列, 再按右上角的刪除鈕就可以刪除應用程式, 它會執行下列事件處理程序 :

    $("#remove_app").bind("click",function(){ ... 參考上面});

經過確認後會呼叫 sys.php 的 remove_app 模組來處理刪除工作 :

  case "remove_app" : {
    $id=$_REQUEST["id"];
    $RS=search("sys_apps","id",$id);
    $status="success";
    $msg="";
    if (is_array($RS)) { //有登錄
      //刪除此 app 所有資料表
      $app_name=$RS[0]["app_name"];
      $installed=$RS[0]["installed"];
      if ($installed=="Y") { //已安裝
        $tables=explode(",",$RS[0]["table_names"]);
        for ($i=0; $i<count($tables); $i++) {
          $result=drop_table($tables[$i]);
          if ($result===FALSE) {
            $status="failure";
            $msg .= "刪除資料表 ".$tables[$i]." 失敗<br>";
            }
          }
        //從 sys_header_links 刪除此 app 超連結
        $result=delete_record("sys_header_links","title",$app_name);
        if ($result===FALSE) {
          $status="failure";
          $msg .= "刪除應用程式在 sys_header_links 之超連結失敗<br>";
          }
        } //installed
      //從 sys_apps 刪除此 app 登錄
      $result=delete_record("sys_apps","id",$id);
      if ($result===FALSE) {
        $status="failure";
        $msg .= "刪除應用程式在 sys_apps 之登錄失敗<br>";
        }
      //刪除應用程式檔案
      $result=delete_file("./apps/".$app_name.".php");
      if ($result==FALSE) {
        $status="failure";
        $msg .= $app_name.".php 刪除失敗<br>";
        }
      $result=delete_file("./apps/".$app_name."_install.php");
      if ($result==FALSE) {
        $status="failure";
        $msg .= $app_name."_install.php 刪除失敗<br>";
        }
      } //registered
    else { //未上傳登錄
      $status="failure";
      $msg="刪除失敗 : 應用程式不存在!";
      }
    $arr["status"]=$status;
    $arr["msg"]=$msg;
    echo json_encode($arr);
    break;
    }


這裡我們先去讀取 sys_apps 中該應用程式的 table_names 欄位, 用 explode() 方法拆解出此應用所屬之資料表後, 在迴圈中用 drop_table 來刪除資料表. 另外就是刪除 sys_header_links 上的超連結紀錄. 最後去 apps/ 目錄下刪除此應用程式及其安裝檔案即可, 其中使用了收錄在 lib/file.php 函式庫中的 delete_file(), 此函式其實很簡單, 就是 PHP 的 unlink :

/*-----------------------------------------------------------------------------
delete_file($filename)
功能 :
  此函數指定路徑下之檔案與目錄列表.
參數 :
  $filename : 檔案名稱 (相對路徑, 例如 log/visitors.txt)
傳回值 :
  成功傳回 TRUE, 失敗傳回 FALSE.
範例 :
  $result=delete_file("a.txt");
  $result=delete_file("./a.txt");
  $result=delete_file("files/a.txt");
-----------------------------------------------------------------------------*/
function delete_file($filename) {
  return unlink($filename);
  }

因今天 (1/27) 下午才會進辦公室, 所以趁著早上閒閒把應用程式部分記錄完了, 希望以後要回頭參考時自己還看得懂, 雖然花了很多時間來紀錄, 但是值得, 因為這是此簡易架站系統最重要的部分, 而且隨著年華老去, 記性已不若以往, 上個月寫的東西這個月可能就印象模糊了, 唉.

2015年1月23日 星期五

參觀奇美博物館

今天請一天假跟菁菁她們班去台南參觀奇美博物館, 七班與八班各一部遊覽車, 8:20 出發, 下午 14:40 打道回府. 這是淑娟老師元旦一開始預約就上網搶到的, 據工作人員說, 目前已經預約到暑假了. 難怪昨天要請假時, 同事以羨慕口吻說現在都預約不到哩.

其實姐姐幼稚園時就參觀過舊奇美博物館了, 今天去看的是新奇美, 位於台南都會公園內 (以前是台糖土地), 占地非常廣闊, 除了那棟宏偉像白宮的主建物外, 園區跟高美館挺像的.



老師有申請導覽, 所以入場時每個人都掛了一個導覽器. 我先上去樓上美術館看畫, 收藏很豐富, 定期會更換展品. 剛開始我都是一個一個仔細看, 有導覽的必聽, 但這樣看根本看不完, 後面的只好走馬看花了. 其中我最喜歡的是朱利安杜培的 "豐收", 此幅據說是奇美鎮館之寶. 其次是 Charles Hermans 的舞者 (Two dancers).

下午要回去前還巧遇以前中山大學外文系的同學 Sara. 在逛紀念品店時就覺得擦身而過的人有點面熟, 但只是覺得像而已也不好亂認人. 後來菁菁全班到外面園區玩鬼抓人遊戲, 我要幫她們攝影時看到她剛好正面走來, 果然沒錯, 我叫住她問 : 你不是 Sara 嗎? 她也很驚訝哩. 掐指一算, 畢業也已十幾年囉, 那些老同學都沒連絡了, 不知大家可好.


2015年1月22日 星期四

用 jQuery EasyUI 打造輕量級 CMS (九)

最近系統的工作突然增多, 所以這兩天在公司的程式進度幾乎是零, 只好帶回家繼續寫. 下午同事反映工作日誌無法上傳檔案, 我想可能又是該死的 IE 搞鬼, 只要一升版就會讓某些功能失靈. 所以架站機要趕快寫好才行, 這樣新版工作日誌才能開工.

花了一個禮拜才把留言板功能搞定, 其實邏輯不難, 時間主要花在呈現方式, 原先是想要用 EasyUI 的 tooltip 元件來呈現留言, 試了兩天還是放棄了, 原因是 tooltip 預設就是隱藏, 要等游標移到上面時才會現身, 而留言板是進入頁面就顯示最近幾筆留言, 若還要使用者移到人名或日期上才顯示內容就有點怪了. 最後在網路上看到交談框 (speech bubble) :

# 那些 CSS 偽元素可以幫你做的 10 個效果
# Pure CSS speech bubbles
# How to Create CSS3 Speech Bubbles Without Images

於是我就突發奇想, 乾脆用交談框來製作留言板吧! 前兩天為此還研究了一下其製作原理, 詳見  :

# CSS3 的魔力 : 如何製作交談氣泡 (Speech bubble)

首先要在安裝程式 install.php 中添加資料表 sys_board :

    //建立 sys_board 資料表
    $data_array["id"]="smallint(6) NOT NULL AUTO_INCREMENT PRIMARY KEY";
    $data_array["poster"]="varchar(255)";             //作者
    $data_array["subject"]="varchar(255)";            //主題
    $data_array["content"]="varchar(255)";            //內容
    $data_array["post_time"]="datetime";              //時間
    $result=create_table("sys_board",$data_array);
    if ($result) {$msg .= "建立資料表 sys_board ... 完成!<br>";}

留言板的內容放在一個 panel 元件裡, 包括右上角的工具 tools 以及下方的分頁工具列 pagination :

  <div id="board" class="easyui-panel" title="留言板" style="padding:15px;height:100%" data-options="href:'sys.php?op=list_board',tools:'#board_tools',footer:'#board_pager',fit:true">
  </div>
  <div id="board_tools">
    <a href="#" id="add_post" class="icon-add" title="新增留言"></a>
    <a href="#" id="reload_board" class="icon-reload" title="重新載入"></a>
  </div>
  <div id="board_pager" class="easyui-pagination" data-options="total:<?php echo $total ?>,pageSize:10">
  </div>

注意這裡 Panel 的 fit 被設成 true, 表示其大小會擴大為與母元件同樣大 (這時 width 與 height 可以不設定).

分頁工具列需要給予 total 與 pageSize 這兩個屬性以便計算總頁數, 這要在一開始時便查詢 board 資料表, 取得總留言數 $total :

    $SQL="SELECT COUNT(*) FROM `sys_board`";
    $RS=run_sql($SQL);
    $total=$RS[0][0]; //紀錄總筆數

而 Panel 的資料來源是透過 Ajax 從 sys.php?op=list_board 取得 :

  case "list_board" : {
    $page=isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 1;
    $rows=isset($_REQUEST['rows']) ? intval($_REQUEST['rows']) : 10;
    $start=($page-1) * $rows;  //本頁第一個列索引 (0 起始)
    $SQL="SELECT COUNT(*) FROM `sys_board`";
    $RS=run_sql($SQL);
    $total=$RS[0][0]; //紀錄總筆數
    $SQL="SELECT * FROM sys_board ORDER BY post_time DESC ".
         "LIMIT ".$start.",".$rows;
    $RS=run_sql($SQL);
    if (is_array($RS)) {
      $icon=Array();
      $icon[]="ball";
      $icon[]="bambi";
      $icon[]="bear";
      $icon[]="corgi";
      $icon[]="cow";
      $icon[]="denchi";
      $icon[]="dorayaki";
      $icon[]="duck";
      $icon[]="kaeru";
      $icon[]="kuma";
      $icon[]="momo";
      $icon[]="saru";
      for ($i=0; $i<count($RS); $i++) {
        $gif=$icon[mt_rand(0,11)].".gif";
?>
  <div>
    <img src="images/<?php echo $gif ?>" style="margin-top:20px;border-width:0px;">  
    <?php echo $RS[$i]["poster"] ?> 張貼於 <?php echo $RS[$i]["post_time"] ?>
    主題 : <?php echo $RS[$i]["subject"] ?>
<?php
    if ($_SESSION["user_level"]==9) {
?>
    <a href="#" id="remove_post" class="easyui-linkbutton" data-options="iconCls:'icon-clear'" title="刪除留言" onclick="javascript:remove_post(<?php echo $RS[$i]['id'] ?>)"></a>
<?php
      }
?>
    <div class="arrow_box"><?php echo $RS[$i]["content"] ?></div>
  </div>
<?php
        }
      }
    else {
?>
  <div>沒有留言</div>
<?php
      }
    break;
    }


這程式會擷取前端分頁工具列傳出的 page 與 row 這兩個參數, 預設是顯示第一頁, 每頁 10 筆留言. 然後我在網路上下載了一組共 12 個 gif 圖檔 (放在系統根目錄的 images 子目錄下), 並用 PHP 的 mt_rand() 函式來隨機顯示在每筆留言的最前面. 若使用者為管理員, 還會顯示一個刪除按鈕以便移除不宜之留言 (傳入 id  給一個最上層的 remove_post 方法). 這個網頁排版參見 :

CSS3 的魔力 : 如何製作交談氣泡 (Speech bubble)

按下新增留言按鈕會顯示對話框, 同時清除裡面可能的舊資料 :

    $("#add_post").bind("click",function(){
      $("#board_dialog").dialog("open").dialog("setTitle","新增留言");
      $("#board_form").form("clear");
      });

這對話框預設是隱藏的 :

  <!--新增留言對話框-->
  <div id="board_dialog" class="easyui-dialog" title="新增留言" style="width:360px;" data-options="closed:'true',buttons:'#board_buttons'">
    <form id="board_form" style="padding:10px">
      <div style="margin:10px">
        <label style="width:50px;display:inline-block;">主題 : </label>
        <input name="subject" type="text" class="easyui-textbox" required="true" data-options="missingMessage:'此欄位為必填'"  style="width:230px">
      </div>
      <div style="margin:10px">
        <label style="vertical-align:top;width:50px;display:inline-block;">內容 : </label>
        <textarea name="content" class="easyui-validatebox" data-options="required:true" style="resize:none;width:225px;height:80px;border-radius:5px;"></textarea>
      </div>
    </form>
  </div>
  <div id="board_buttons" style="padding-right:15px;">
    <a href="#" id="clear_post" class="easyui-linkbutton" iconCls="icon-clear" style="width:90px">重設</a>
    <a href="#" id="save_post" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">確定</a>
  </div>


由於 EasyUI 並沒有推出自己的多行文字欄位元件, 這對話框中的留言內容使用 textarea 元素, 為了能進行欄位驗證, 必須將其樣式類別指定為 easyui-validatebox, 這樣還有個用處, 就是外觀會一致, 跟 easyui-textbox 一樣. 當按下確定鈕時, 就用 Ajax 上將表單提交給遠端執行 :

    $('#save_post').bind('click',function(){
      if (!$('#board_form').form('validate')){
        $.messager.alert('訊息','必填欄位未輸入!','warning');
        return;
        }
      var params=$('#board_form').serialize();
      params='op=add_post&' + params;
      var callback=function(data,textStatus){
        if (data.status==='success'){
          $('#board_dialog').dialog("close");
          $('#board').panel('open').panel('refresh');  //更新留言列表
          $('#board_pager').pagination('select',1);   //分頁條顯示第一頁
          $('#board_pager').pagination('refresh',{total:data.total}); //更新總數
          }
        else {$.messager.alert('訊息','留言失敗!','error');}
        }
      $.post('sys.php',params,callback,'json');
      });
    $("#clear_post").bind("click",function(){
      $("#board_form")[0].reset();
      });

遠端 sys.php 的 add_post 模組執行新增到資料表作業 :

  case "add_post" : {
    $SQL="SELECT COUNT(*) FROM `sys_board`";
    $RS=run_sql($SQL);
    $total=$RS[0][0]; //紀錄總筆數
    $data_array["poster"]=$_SESSION["user_name"];
    $data_array["subject"]=$_REQUEST["subject"];
    $data_array["content"]=$_REQUEST["content"];
    $data_array["post_time"]=date("Y-m-d H:i:s");;
    $result=insert("sys_board", $data_array);
    if ($result===TRUE) {
      $status="success";
      ++$total;
      $msg="ok";
      }
    else {
      $status="failure";
      $msg="資料新增錯誤!";
      }
    $arr["status"]=$status;
    $arr["total"]=$total;
    $arr["msg"]=$msg;
    echo json_encode($arr);
    break;
    }  

這裡比較重要的是要將留言總數加 1 後傳回給網頁中的分頁條更新顯示的留言總數. 就是上面的 :

$('#board_pager').pagination('refresh',{total:data.total}); //更新總數

關於分頁工具列還有一個需要處理的部分, 就是當使用者選取頁碼 (傳出 pageNumber 參數) 與選取每頁大小 (傳出 pageSize 參數) , 以及按下 refresh 鈕時, 必須向後端的 list_board 模組要求新資料, 這必須替分頁工具列加上 onRefresh 與 onSelectPage 方法,  他們都會由事件觸發得到 pageNumber 與 pageSize 參數, 我們將其分別改名為 page 與 row 參數傳遞給後端的 list_board 模組 (這是因為後端分頁程式我都維持一貫使用 page 表示 pageNumber, 用 row 表示 pageSize 之故) :

    $("#board_pager").pagination({
      onRefresh:function(pageNumber,pageSize) {
        var url='sys.php?op=list_board&page=' + pageNumber + '&row=' + pageSize;
$('#board').panel('refresh',url);    
        },
      onSelectPage:function(pageNumber,pageSize){
        var url='sys.php?op=list_board&page=' + pageNumber + '&row=' + pageSize;
$('#board').panel('refresh',url);
        }
      });

不管是按下更新鈕還是選擇另一個頁都會依所選的頁與大小重新載入分頁.

最後, 如果是管理員按下刪除鈕, 這時會呼叫最上層方法 remove_post(), 並傳入要刪除的留言的 id :

  function remove_post(id){
    $.messager.confirm('確認','確定要刪除這筆留言?',function(r){
      if(r){
        var params='op=remove_post&id=' + id;
        var callback=function(data,textStatus){
          if (data.status==='success'){
            $('#board_dialog').dialog("close");
            $('#board').panel('open').panel('refresh');  //更新留言列表
            $('#board_pager').pagination('refresh',{total:data.total}); //更新總數
            }
          else {$.messager.alert('訊息',"刪除留言失敗",'error');}
          }
        $.post('sys.php',params,callback,'json');
}
      });
    }

然後跳出確認框, 按確定後就呼叫遠端的 remove_post 模組來處理 :

  case "remove_post" : {
    $SQL="SELECT COUNT(*) FROM `sys_board`";
    $RS=run_sql($SQL);
    $total=$RS[0][0]; //紀錄總筆數
    $id=$_REQUEST["id"];
    $result=delete_record("sys_board", "id", $id);
    if ($result===TRUE) {$status="success";--$total;}
    else {$status="failure";}
    $arr["status"]=$status;
    $arr["total"]=$total;
    echo json_encode($arr);
    break;
    }

同樣的, 也要傳回 total 屬性給前端網頁以便更新分頁工具列上顯示的留言總數. OK, 留言板搞定了.

參考資料 :

http://ckeditor.com/
# CKEditor的使用方法

2015年1月20日 星期二

CSS3 的魔力 : 如何製作交談氣泡 (Speech bubble)

這幾天 EasyuiCMS 的開發進度慢了下來, 因為動手寫留言板功能時, 對於排版與呈現方式感到苦惱, 這就是完美主義者自戀自愛的代價.

上周末看到 EasyUI 的 tooltip 元件範例, 突然發現用 tooltip 來呈現留言很不錯, 但試了兩天還是放棄了, 原因是 tooltip 預設為隱藏, 等游標移到上面時才會現身, 而留言板是進入頁面就應該顯示最近幾筆留言, 若還要使用者移到人名或日期上才顯示內容, 還要多一步操作, 這樣就有點怪了.

在看過幾本 PHP 書籍的範例仍無頭緒後, 開始上網以留言板為 keyword 去搜尋, 啊哈, 經由 "那些 CSS 偽元素可以幫你做的 10 個效果" 找到下面兩個超棒網站 :

Pure CSS speech bubbles
How to Create CSS3 Speech Bubbles Without Images

實際測試後發現前者比較適合, 這位作者對 CSS3 的掌握真的是太神了, 主要是以 CSS3 的偽元素 before 與 after 來製作兩個一大一小 size 為 0 的三角形, 利用其上下左右方向的 border 寬度與 background-color 顏色不同, 使兩個三角形重疊出一個三角框, 然後將三角框的一邊與圓角方框的一邊重疊, 這樣就能做出交談氣泡的指引箭頭了. 我是透過詳讀第二個網站的詳細說明才真正了解其原理的.

OK, 現在就來測試一下. 我的留言板規劃是左邊隨機顯示一個圖檔與留言者, 其下顯示留言日期, 右方則顯示其留言內容, 因此我需要的是一個箭頭在左邊的交談氣泡, 也就是 "Pure CSS speech bubbles" 中 "Sample Examples" 倒數第二個圖. 觀其網頁原始碼, 可以發現其樣式設定在 default.css 這個檔 :

http://nicolasgallagher.com/pure-css-speech-bubbles/demo/default.css

我需要的是 triangle border 與其 left 部分, 也就是將 "BUBBLE WITH A BORDER AND TRIANGLE" 這區塊中的 top 與 right 部分去除即可.

  .triangle-border {
    position:relative;
    padding:15px;
    margin:1em 0 3em;
    border:1px solid #95B8E7;
    color:#333;
    background:#fff;
    /* css3 */
    -webkit-border-radius:10px;
    -moz-border-radius:10px;
    border-radius:10px;
    }
  .triangle-border:before {
    content:'';
    position:absolute;
    bottom:-20px;
    left:40px;
    border-width:20px 20px 0;
    border-style:solid;
    border-color:#5a8f00 transparent;
    /* reduce the damage in FF3.0 */
    display:block;
    width:0;
    }
  .triangle-border:after {
    content:"";
    position:absolute;
    bottom:-13px;
    left:47px;
    border-width:13px 13px 0;
    border-style:solid;
    border-color:#fff transparent;
    /* reduce the damage in FF3.0 */
    display:block;
    width:0;
    }
  .triangle-border.left {margin-left:30px;}
  /* creates the larger triangle */
  .triangle-border.left:before {
    top:10px;
    bottom:auto;
    left:-20px;
    border-width:10px 20px 10px 0;
    border-color:transparent #95B8E7;
    }
  /* creates the smaller  triangle */
  .triangle-border.left:after {
    top:11px;
    bottom:auto;
    left:-19px;
    border-width:9px 19px 9px 0;
    border-color:transparent #fff;
    }

這裡我修改了原作者關於邊框顏色與粗細的設定, 因為我的邊框只要 1px 即可, 原作是設為 5px, 太粗了. 需要修改的數據如上藍色部分, 目的是擠出一個邊框 1px 的三角形, 其計算原理如下 :


這裡我們的交談氣泡指標是以交談框的左上角為原點, 外三角往左 20px, 往下 10px 開始描繪, 故 triangle-border.left:before 裡, top 設為 10, left 設為 -20 (負為往左), 其邊框寬度要設為 10 (上), 20 (右), 10(下), 左邊要壓扁, 故為 0. 而內三角的底與外三角的底 (右方) 重疊, 高少 1px, 因此 triangle-border.left:after 的 top 設為 11, left 設為 -19, 其邊框寬度設為 9 (上), 19 (右), 9(下), 同樣左邊要壓扁, 故為 0. 重點是內三角邊框顏色設為白色, 剛好把外三角的顏色蓋掉, 只剩上下兩個 1px 的線.

網頁部分如下, 只要設定 P, SPAN, 或 DIV 元素的樣式類別為 "triangle-border left" 即可 :

  <div>
    <img src="male.png" style="border-width:0px;">
    <span class="triangle-border left">
      Hello Emma!
      How is everything going?
    </span>
  </div>
  <div>
    <img src="female.png" style="border-width:0px;">
    <span class="triangle-border left">
      Hi, Sam!
      Ya, just fine. And You?
    </span>
  </div>

測試範例 1 : http://mybidrobot.allalla.com/css3test/speech-bubble-1.htm [看原始碼]

下面是用 Chrome 瀏覽的結果, 看來很不錯 (版本 39.0.2171.95 m) :


下面是用 IE 瀏覽結果, 不佳 (版本 11.0.9600) :


下面是用 Firefox, 同樣不佳 (版本 35.0) :


最後是 Opera, 雖然渲染效果與 Chrome 稍有不同, 但也 OK (版本 26.0.1656.60) :


從這一點可看出, Chrome 與 Opera 還是瀏覽器界數一數二的優等生, 對於 CSS3 與 HTML5 的支援度是最棒的. Firefox 與 IE 則要加油了.

下午又找到一個更棒的 CSS 箭頭產生器, 直接選取所需要的屬性, 就會自動產生 CSS 檔 :

# CSS Arrow Please: CSS 箭頭產生器
# http://cssarrowplease.com/

我選了左方箭頭, 寬度 1px 的, 然後稍加調整屬性如下 :

    .arrow_box {
      padding: 15px;
      height: auto;
      border-radius: 10px;
      position: relative;
      background: #ffffff;
      border: 1px solid #95B8E7;
      }
    .arrow_box:after, .arrow_box:before {
      right: 100%;
      top: 50%;
      border: solid transparent;
      content: " ";
      height: 0;
      width: 0;
      position: absolute;
      pointer-events: none;
      }
    .arrow_box:after {
      border-right-color: #ffffff;
      border-width: 13px;
      margin-top: -10px;
      }
    .arrow_box:before {
      border-right-color: #95B8E7;
      border-width: 14px;
      margin-top: -11px;
      }

原理其實跟上面範例 1 是一樣的, 只是所用的樣式屬性稍有不同而已. 我改動了一些屬性值來達成與範例 1 同樣的結果. 只要把上面範例 1 的 class 改為 arrow_box 即可 :

  <div>
    <img src="male.png" style="border-width:0px;">
    <span class="arrow_box">
      Hello Emma!
      How is everything going?
    </span>
  </div>
  <div>
    <img src="female.png" style="border-width:0px;">
    <span class="arrow_box">
      Hi, Sam!
      Ya, just fine. And You?
    </span>
  </div>

如下範例 2 所示 :

測試範例 2 : http://mybidrobot.allalla.com/css3test/speech-bubble-2.htm [看原始碼]

這個做法比範例 1 要好, 在 Chrome, Firefox, 與 Opera 均正常, 僅 IE 無法覆蓋與三角形重疊的邊框. IE 到底是哪根筋不對啊? 是因為老大當太久了嗎?

上面兩個範例都有一個問題, 就是留言內容不可太長, 否則留言內容框會斷成兩節 :


這是因為我們使用 span 元素之故, 若使用 div 元素就不會這樣, 但如此一來留言框就會佔據整列了. 所以現在考慮改用向上箭頭 :

    .arrow_box {
      margin-top:30px;
      padding: 15px;
      height: auto;
      border-radius: 10px;
      position: relative;
      background: #ffffff;
      border: 1px solid #95B8E7;
      }
    .arrow_box:after, .arrow_box:before {
      bottom: 100%;
      left: 60px;
      border: solid transparent;
      content: " ";
      height: 0;
      width: 0;
      position: absolute;
      pointer-events: none;
      }
    .arrow_box:after {
      border-bottom-color: #ffffff;
      border-width: 15px;
      margin-left: -12px;
      }
    .arrow_box:before {
      border-bottom-color: #95B8E7;
      border-width: 16px;
      margin-left: -13px;
      }

網頁部分做了些修改, 加上姓名與時間 :

  <div>
    <img src="male.png" style="border-width:0px;"> Sam 於 2015-01-13 23:11:06
    <div class="arrow_box">
      Hello Emma!
      How is everything going?
    </div>
  </div>
  <div>
    <img src="female.png" style="border-width:0px;"> Emma 於 2015-01-13 23:12:50
    <div class="arrow_box">
      Hi, Sam!
      Ya, just fine. And You?
    </span>
  </div>

如下列範例 3 所示 :

測試範例 3 : http://mybidrobot.allalla.com/css3test/speech-bubble-3.htm [看原始碼]



2015年1月19日 星期一

考試週

又接近學期末了, 上周小狐狸們的月考, 段考陸續上場, 菁菁有進步, 數學考了 79, 社會 89, 國語 93, 自然 83, 還好沒有不及格. 她的好朋友予馨有給她訂下數學標準必須越過 70 分, 所以考前有賣力算題目.

因為姐姐段考跨周日 (週日還要考英檢中級), 所以這週只有我跟菁菁回鄉下, 週六時載爸去鳳山中山東路推拿後順路回鄉下. 週日下午又再去一次, 所以本周跑了鳳山兩趟. 走鳳仁路較慢, 不如從九如下交流道走建國路再轉鳳松路較快.

這兩天把小舅媽拿來的葫瓜分兩次煮完, 我覺得葫瓜的湯汁非常甜美, 而且烹煮也很簡單, 只是需時較久. 以後上市集時要把葫瓜納入購買清單裡. 或者菜園也種些, 也不需要太多照顧.




2015年1月16日 星期五

充實的一天

早上到了公司才發現手機又沒帶, 我的提醒貼完全破功 (記得早上鎖門時有看到, 但卻沒有摸摸前口袋).


不過沒關係, 今天很多事要做 :
  1. 改寫漏帳自動稽核程式
    上頭在催了 ....
  2. 交討厭的週報
    這是升官的代價, 每周必報 ...
  3. 寫年度績效考核面談表
    下周到期, 最好今天搞定, 寫些夢幻的東西也好
在不聊天, 專心一志的自我要求下, 終於按部就班攻克每一個山頭. 雖然筆電打開後一行程式碼都沒動, 但把這些難搞的全做完了, 也感到今天過得非常充實呢. 沒負擔的感覺真好.


兩本 jQuery Mobile 好書

今天在市圖網站找到兩本好書, 預約中 ...

# 互動式網站前台開發武功秘笈:使用HTML5、CSS3 和 jQuery
# jQuery Mobile智慧型手機程式開發:跨平台開發Android/iPhone/Windows Phone App超簡單



2015年1月15日 星期四

分年

今天請假回鄉下幫母親做分年, 早上 6:50 出發順路載二姨丈, 回到鄉下家時快八點了, 一看曬穀場有一台車, 原來是表哥跟阿興剛到.
8:20 出發前往慈恩塔, 順路去買金香與粄, 用的紅龜與發粄各一包, 給親友的 12 小包 (這放桌下不拿來拜). 今年可能因為有人指揮交通, 所以車子還可以開進去, 但是廣場人山人海, 到處香煙瀰漫. 我先點香去塔裡向母親稟告牲禮擺放桌次, 然後回供桌與諸親友焚香祭拜.
來給媽分年的親友 : 阿信叔, 阿見叔, 阿將伯, 小舅媽, 小阿姨, 左營阿姨, 二姨丈, 表哥與阿興. 感謝. 分年者, 意思是為今年往生者辦過年也, 每年農曆 12 月 25 日辦理, 但今年閏九月, 故提前為 11 月 25 日.
中午在美綠訂一桌 2500 請阿姨舅媽他們吃午飯, 因習俗分年完即各自回家, 不可在家請客. 美綠之前來過一次, 覺得這裡環境佳, 離家又近, 請客聚餐甚好.

2015年1月14日 星期三

用 jQuery EasyUI 打造輕量級 CMS (八)

這幾天主要在寫檔案上傳下載功能, 本以為很簡單, 只要套用應用程式上傳的程式碼即可快速解決, 沒想到弄了兩三天還沒搞定, 而且還發現之前寫的檔案函式庫 file.php 中的上傳函式有 bug.

寫檔案上傳下載參考了下列文章, 特別是下載部分, 一般的 PHP 書籍探討下載的較少, 我主要是參考 "php 檔案下載" 這篇, 採用 http 標頭來傳送檔案.

# Download a file by jQuery.Ajax
透過Javascript觸發檔案下載
# php 檔案下載
# PHP 如何取得 現在檔案的目錄 與 上層目錄
# Download File Using jQuery
# jQuery File Download Plugin for Ajax
# ASP.Net : Download file using jQuery
# jquery.fileDownload.js & jQuery UI Dialog

這陣子系統規劃已做了大幅度改變, 來不及紀錄, 因此只好就現況說明, 首先必須在系統安裝檔 install.php 增加一個 sys_files 來記錄已上傳的檔案資料 :

    //建立 sys_files 資料表
    $data_array["id"]="smallint(6) NOT NULL AUTO_INCREMENT PRIMARY KEY";
    $data_array["file_name"]="varchar(255)";          //檔案名稱
    $data_array["file_note"]="varchar(255)";          //檔案說明
    $data_array["file_type"]="varchar(255)";          //檔案類型
    $data_array["file_size"]="varchar(255)";          //檔案大小
    $data_array["uploader"]="varchar(255)";           //上傳者
    $data_array["upload_time"]="datetime";            //上傳時間
    $data_array["download_count"]="int(10) unsigned"; //下載次數
    $data_array["public"]="char(1)";                  //公開 "Y"/"N"
    $result=create_table("sys_files",$data_array);
    if ($result) {$msg .= "建立資料表 sys_files ... 完成!<br>";}

其中 file_name, file_type, file_size 用來儲存上傳函式的傳回值, public 用來讓上傳者選擇是否讓所上傳的檔案可被其他人下載. download_count 用來統計下載次數, 每次被下載時, 此欄之值就加 1, 因此它須賦予初值 0.

然後在系統功能檔 sys.php 裡增添一個 files 的 case :

  case "files" : {
?>

<?php
      break;
      }

我們的檔案列表用 datagrid 來顯示, 就放在上面的 ?> 與 <?php 中間 :

<div class="tab" title="檔案">
  <!--檔案 sys_files 列表-->
  <table id="sys_files" title="檔案列表" style="width:auto" data-options="tools:'#files_tools',toolbar:'#files_toolbar'"></table>
  <div id="files_tools">
    <a href="#" id="upload_file" class="icon-add" title="上傳檔案"></a>
    <a href="#" id="remove_file" class="icon-remove" title="刪除檔案"></a>
    <a href="#" id="reload_files" class="icon-reload" title="重新載入"></a>
    <a href="#" id="download_file" class="icon-save" title="下載另存"></a>
  </div>
  <div id="files_toolbar" style="text-align:right;padding:2px;">
    <select id="files_search_field" class="easyui-combobox" data-options="panelHeight:120">
      <option value="file_name">檔案名稱</option>
      <option value="file_note">檔案說明</option>
      <option value="file_type">檔案類型</option>
      <option value="uploader">上傳者</option>
      <option value="upload_time">上傳時間</option>
    </select>
    <input id="files_search_what" class="easyui-textbox">
  </div>

上面的 HTML 主要就是在頁籤裡頭放一個 id="sys_files" 的 datagrid, 它具有一個工具 (tools) 以及一個工具按鈕列 (toolbars), 前者用來放置控制檔案操作的小圖示 (上傳, 刪除, 下載, 更新); 後者用來放置搜尋框.

當然這個 datagrid 需要 JS 來初始化 :

    //檔案 sys_files
    $('#sys_files').datagrid({
      columns:[[
        {field:'id',title:'id',sortable:true},
        {field:'file_name',title:'檔案名稱',sortable:true},
        {field:'file_note',title:'檔案說明',sortable:true},
        {field:'file_type',title:'檔案類型',sortable:true},
        {field:'file_size',title:'檔案大小',align:'right',sortable:true},
        {field:'uploader',title:'上傳者',sortable:true},
        {field:'upload_time',title:'上傳時間',sortable:true},
        {field:'download_count',title:'下載次數',align:'right',sortable:true}
        ]],
      url:"sys.php",
      queryParams:{op:"list_files"},
      fitColumns:true,
      singleSelect:true,
      pagination:true,
      pageSize:10,
      rownumbers:true,
      onDblClickRow:function(index,row){
        var url="sys.php?op=download_file&id=" + row.id +
                "&file_name=" + row.file_name;
        window.open(url);
        }
      });
    $("#files_search_what").textbox({
      icons:[{
        iconCls:"icon-search",
        handler:function(e){
          $('#sys_files').datagrid("load",{
            op:"list_files",
            search_field:$("#files_search_field").combobox("getValue"),
            search_what:$("#files_search_what").textbox("getValue")
            });        
          }
        }]
      });

這裡很重要的一點是, url 與 queryParams 這兩個參數指定了 datagrid 的資料來源, 所有架站系統的功能我都整合在 sys.php 這個檔案裡 (雖然專家不建議這麼做, 好像難維護, 但我討厭一大堆零零散散的小檔案), 然後用 op 參數與 switch-case 指令來選擇要執行的功能. 所以要在 sys.php 裡增添一個 case=list_files 的模組來輸出 datagrid 所需的 json :

  case "list_files" : {
    $page=isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 1;
    $rows=isset($_REQUEST['rows']) ? intval($_REQUEST['rows']) : 10;
    $sort=isset($_REQUEST['sort']) ? $_REQUEST['sort'] : 'upload_time';
    $order=isset($_REQUEST['order']) ? $_REQUEST['order'] : 'desc';
    if (isset($_REQUEST['search_field'])) { //有 search
      if ($_SESSION["user_level"]==9) { //管理員 (搜尋全部)
        $where="WHERE ".$_REQUEST['search_field']." LIKE '%".
               $_REQUEST['search_what']."%'";
        }
      else { //非管理員 (搜尋公開的與自己上傳的)
        $where="WHERE (public='Y' OR uploader='".$_SESSION["user_name"].
               "') AND ".$_REQUEST['search_field']." LIKE '%".
               $_REQUEST['search_what']."%'";
        }
      }
    else { //無 search (顯示全部)
      if ($_SESSION["user_level"]==9) {$where="";} //管理員
      else { //非管理員 (只能看得到公開的與自己上傳的)
        $where="WHERE public='Y' OR uploader='".$_SESSION["user_name"]."'";
        }          
      }
    $start=($page-1) * $rows;  //本頁第一個列索引 (0 起始)
    $SQL="SELECT COUNT(*) FROM `sys_files`";
    $RS=run_sql($SQL);
    $total=$RS[0][0]; //紀錄總筆數
    $SQL="SELECT * FROM sys_files ".$where." ORDER BY ".
         $sort." ".$order." LIMIT ".$start.",".$rows;
    $RS=run_sql($SQL);
    $files=Array();
    if (is_array($RS)) {
      for ($i=0; $i<count($RS); $i++) {
        $files[$i]=Array("id" => $RS[$i]["id"],
                         "file_name" => $RS[$i]["file_name"],
                         "file_note" => $RS[$i]["file_note"],
                         "file_type" => $RS[$i]["file_type"],
                         "file_size" => $RS[$i]["file_size"],
                         "uploader" => $RS[$i]["uploader"],
                         "upload_time" => $RS[$i]["upload_time"],
                         "download_count" => $RS[$i]["download_count"]
                         );
        }
      }
    $arr=array("total" => $total, "rows" => $files);
    echo json_encode($arr);
    break;
    }

上面預設排序是依上傳時間 upload_time 倒序排列, 亦即最新上傳的會列在最上面. 管理員可以看到所有上傳的檔案列表, 而一般使用者只能看到自己上傳的檔案以及別人公開的檔案, 公開與否由 public 這欄位來決定, 上傳時若未指明, 預設為公開.

其次, 我們替 datagrid 指定了 Double Click 事件, 當用戶雙擊任何一列時就會觸發此事件, 並傳回其索引 index 與列物件 row, 透過 row.id 與 row.file_name 即可取得要下載的檔案的 id 與名稱, 組成 url 後開啟一個新視窗來進行下載.


接著是處理檔案上傳按鈕圖示 (id="upload_file") 被按下時的情況, 這時要顯示一個 dialog 交談框, 讓使用者上傳檔案, 所以在上面的 HTML 後面添加 :

  <!--上傳檔案表單對話框-->
  <div id="upload_file_dialog" class="easyui-dialog" title="上傳應用程式" style="width:420px;height:225px;"  data-options="closed:'true',buttons:'#upload_file_buttons'">
    <form id="upload_file_form" style="padding:15px" action="sys.php?op=upload_file" method="post" enctype="multipart/form-data" target="upload_target">
      <div style="margin:10px">
        <label style="width:60px;display:inline-block;">檔案 : </label>
        <input name="upload" class="easyui-filebox" data-options="buttonText:'選擇檔案',prompt:'',required:true" style="width:280px">
      </div>
      <div style="margin:10px">
        <label style="width:60px;display:inline-block;">說明 : </label>
        <input name="file_note" class="easyui-textbox" style="width:280px" required="true">
      </div>
      <div style="margin:10px">
        <label style="width:60px;display:inline-block;">公開 : </label>
        <select name="public" class="easyui-combobox" panelHeight="50"  style="width:80px">
          <option value="Y">Y</option>
          <option value="N">N</option>
        </select>
        <img src="images/uploading.gif" id="uploading" style="visibility:hidden;margin-left:20px;">
      </div>
      <div>
        <iframe id="upload_target" name="upload_target" src="#"           style="width:0;height:0;border:0px solid #ffffff;">
        </iframe>
      </div>
    </form>
  </div>
  <div id="upload_file_buttons" style="padding-right:15px;">
    <a href="#" id="clear_file" class="easyui-linkbutton" iconCls="icon-clear" style="width:90px">重設</a>
    <a href="#" id="upload_file_go" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">上傳</a>
  </div>
</div>

其事件處理的 JS 如下 :

    $("#clear_file").bind("click",function(){
      $("#upload_file_form")[0].reset();
      });
    $("#upload_file_go").bind("click",function(){
      $("#uploading").css("visibility","visible");
      $("#upload_file_form").form("submit");
      });
    $("#upload_file").bind("click",function(){
      $("#upload_file_dialog").dialog("open").dialog("setTitle","上傳檔案");
      $("#upload_file_form").form("clear");
      });


這個 dialog 對話框預設不可以顯示, 因此在 data-options 中要將 closed 屬性設為 true, 必須等按下右上角的 upload_file 按鈕時才呼叫 open 方法將其開啟. 這 dialog 內放了一個表單, 注意, 其編碼型態 enctype 須設為 multipart/form-data 以便傳送檔案. 而後端程式執行結果的輸出目的 target 設為 upload_target, 這是 dialog 對話框中一個看不到 (大小為 0) 的 iframe 的 id, 這 iframe 是作為一個前後端的中介站, 用來執行上傳完畢時要顯示的訊息框程式. 因為檔案上傳後, 前端網頁程式並不知道後端何時會完成收檔, 因此也無法適時跳出一個訊息框來顯示上傳完成之訊息, 因此我們偷偷在前端網頁設置一個看不到的 iframe 當窗口, 當後端完成檔案接收後, 就輸出一個顯示傳檔已完成的 JS 程式丟回這個 iframe 中執行來執行, 讓使用者誤以為我們前端網頁能預測傳檔何時能完成呢, 真是巧妙啊.

顯示上傳結果的 JS 如下之 upload_callback() 所示, 它首先會將顯示上傳中的 gif 動畫圖檔還原為隱藏狀態, 然後關閉上傳對話框, 重新載入 datagrid, 最後顯示後端傳回來的上傳結果 (參數 status 與 msg) :

    <!--Top Level Script -->
    function upload_callback(status,msg) { //必須在最上層, 不可放在 $ 內
      $("#uploading").css("visibility","hidden");
      $("#upload_file_dialog").dialog("close");
      $("#sys_files").datagrid("reload",{op:"list_files"});
      if (status=="success") {var msg=msg + '檔案上傳成功! <br>';}
      else {var msg=msg + '檔案上傳失敗, 請重新上傳! <br>';}
      $.messager.alert("上傳結果", msg);
      }

在此 dialog 中用來指定要上傳之檔案的元件是 id=upload 的 easyui-filebox 元件, 按下其選擇檔案按鈕會跳出作業系統的檔案選擇對話框, 當選定檔案後, 其文字欄位會顯示檔案名稱, 但路徑一律顯示 C:\fakepath. 按下上傳鈕會向後端提交表單, 這時對話框中會冒出一個 uploading 的動畫圖形, 顯示還在上傳中, 這是因為我們在對話框中偷偷放了一個預設看不見 (visibility:none) 的 uploading.gif 圖檔, 這是我在網路上蒐來的, 因為 EasyUI 本身沒有提供, 我把它放在根目錄的 images 下. 當檔案接收完成, 必須再度將此圖檔隱藏起來.

所以在 sys.php 中須增添一個 case=upload_file 來處理上傳檔案之接收 :

  case "upload_file" : {
    $status="success";       //預設上傳成功
    $msg="";                     //執行結果字串
    $upload_dir="./files/";  //設定上傳目錄
    $file_note=$_REQUEST["file_note"];     //取得檔案說明
    $public=empty($_REQUEST["public"])?"Y":$_REQUEST["public"]; //取得公開與否 (預設 Y)
    $result=upload_file("upload", $upload_dir);
    if ($result !== FALSE) {
      $msg .= $result["name"]." (".$result["size"]." bytes).<br>";
      $data_array["file_name"]=$result["name"];
      $data_array["file_note"]=$file_note;
      $data_array["file_type"]=$result["type"];
      $data_array["file_size"]=$result["size"];
      $data_array["uploader"]=$_SESSION["user_name"];
      $data_array["upload_time"]=date("Y-m-d H:i:s");
      $data_array["download_count"]=0;
      $data_array["public"]=$public;
      insert("sys_files", $data_array);
      } //end of if
    else { //上傳失敗
      $msg .= $file_name." 上傳失敗 (".$result["file_error"].")!<br>";
      $status="failure";
      } //end of else
    $para='"'.$status.'","'.$msg.'"'; //回傳回呼函式之參數
?>
<!-- 輸出 script 到 $op=files 中的 iframe 並執行其中的回呼函式 -->
<script language="javascript" type="text/javascript">
  window.top.window.upload_callback(<?php echo $para; ?>);
</script>
<?php
    break;
    }

上面這個 JS 是就是檔案接收程式最後的輸出, 目的地就是前端網頁表單的 target 所指的 iframe 元素, 它會呼叫我們放在前端網頁視窗最上層的函式 upload_callback(), 其實就是把參數傳進去顯示上傳結果而已.

這個檔案接收程式使用了一個自建的函式 upload_file(), 我把它寫在 /lib/ 下的 file.php 中, 因此必須載入此函式庫才能使用 :

/*-----------------------------------------------------------------------------
upload_file($uploader,$path="./")
功能 :
  此函數將上傳之單一檔案, 由暫存區移至指定路徑下.
參數 :                                
  $uploader : 上傳元件名稱, 即 <input type="file" name="uploader"> 中之 name 值
  $path     : 檔案儲存路徑, 例如 "./images"
傳回值 :
  成功傳回陣列, 失敗傳回 FALSE. 陣列有三個元素 :
  $result["name"]=檔案名稱
  $result["type"]=檔案類型
  $result["size"]=檔案大小
範例 :
  <input type="file" name="uploader">
  $result=upload_file("uploader", "./images");
  echo "檔案 ".$result["name"]." 上傳成功 (".$result["size"]." bytes)";
-----------------------------------------------------------------------------*/
function upload_file($uploader,$path="./") {
  if ($_FILES[$uploader]["error"]==0) { //上傳成功
      $tmp_name=$_FILES[$uploader]["tmp_name"];
      $new_name=$path.$_FILES[$uploader]["name"];
      if (move_uploaded_file($tmp_name, $new_name)) { //移動成功
          $result["name"]=$_FILES[$uploader]["name"];
          $result["type"]=$_FILES[$uploader]["type"];
          $result["size"]=$_FILES[$uploader]["size"];
          } //end of if
      else {$result=FALSE;} //移動失敗
      } //end of if
  else {$result=FALSE;} //上傳失敗
  return $result;
  }

此函式只要傳入兩個參數, 第一參數 uploader 就是前端上傳元件 input 的 name, 這裡就是 "upload", 第二參數是上傳目錄, 這裡要傳入 "./files/". 如果上傳成功, 它會傳回一個關聯式陣列, 其元素分別是 name (檔案名稱), type (檔案類型), 與 size (檔案大小). 若上傳失敗就傳回 false.

2015-01-26 補充 :

今天回頭測試下載功能, 發現中文檔名的檔案無法下載, Google 了一下, 發現是 PHP 對於中文檔名支援問題, 試了 iconv 與 mb_convert_encoding 兩種函式將 big5 轉成 utf-8 來處理都不得法, 上傳 OK 但下載 NG (因為 PHP 的 basename 不支援中文) :

$file_name=$_FILES[$uploader]["name"];
$new_name=$path.mb_convert_encoding($file_name,"big5","utf-8");

我參考葉建榮的 "PHP 與 MySQL 網站規劃管理應用" 這本書的作法, 乾脆限制只能上傳英數檔名之檔案, 然後用檔案說明欄描述之. 當然這樣做有些不完美, 或許可以參考下面這個網站的建議處理中文檔名上傳問題 :

# php 中文檔名上傳問題

他的做法是上傳到伺服器後改以英數字檔名儲存 (例如日期時間+tmp_name), 但是把中文檔名記錄在資料庫裡, 檔案列表顯示的是資料庫所記錄的原始中文檔名, 但下載時卻連結到相對應的英數字檔名. 這個做法不錯, 但是必須於資料表中增加一個欄位來記錄真實檔名, 以便與原始中文檔名對應.