資料夾管理工具(MVC+zTree+layer)(附原始碼)

Zery發表於2013-09-27

 

寫在前

 

         之前寫了一篇關於 資料夾與檔案的操作的文章  操作檔案方法簡單總結(File,Directory,StreamReader,StreamWrite ) 

把常用的對於檔案與資料夾的操作總結了一下,在文章的回覆中有提到 遺漏的方法,並建議做一個文件管理工具 ,一細想是可以啊,之後就開始構思,最後大至需求如下,

因為都是下班後在家中花時間做的,時間有點短,有BUG是正常的,希望大家諒解,再一個對windows的檔案許可權,幾乎是0接觸所以把許可權處理留到了後期,如果大家有關於資料夾許可權處理的好文章,希望大家給我留言,好的東西需要分享,本篇會寫一下幾個功能點的思路與做法,算是對自己的總結,如果對本專案有興趣的朋友請 狂點這裡

最後如果您覺得,有什麼地方做得不夠好,或者您有什麼建議 與 意見 請一定要指出! 在此非常感謝!  

 

 

一 工具功能點

 一 展示
1 以樹形展示單個磁碟內所有資料夾子資料夾,與檔名
2 支援子資料夾的層級展開
3 右側支援文字文件的預覽(二期)
4 若是圖片支援預覽(二期)
 
二 操作
 1 支援指定目錄下的資料夾新增,(右鍵)
 2 支援指定目錄下的資料夾刪除(右鍵)
 3 支援指定目錄下的資料夾,與檔案的重新命名((右鍵))
 4 支援指定目錄下的資料夾與檔案移動到另一指定的資料夾(託拽)
 5 支援指定目錄下的資料夾批量刪除
 6 支援指定目錄下的檔案搜尋 (只要檔名匹配即可)(二期)
 7 資料夾與檔案的訪問許可權處理
 
以上就是大至簡單的需求,然後根據需求,選擇了以下幾個小外掛
zTree:樹形外掛 個人覺得很不錯,國內的外掛,API文件解釋很詳細,至少我從來沒用過樹形外掛的,比較容易的就上手了。 zTree外掛地址
Layer:彈出層外掛,使用簡單,效果好,拿來即用   Layer外掛地址
 

二 工具基本介紹

 
先上幾張圖  新建資料夾,資料夾或檔案重新命名
 
 
刪除檔案或資料夾(支援批量刪除)
 
 
以拖拽形式批量移動資料夾
 
 
 
 
 

三 功能實現講解

 
3.1 載入資料夾樹
 
  根據zTree需要的引數配置就行了,具體引數請大家看zTree的API吧,著重寫一下載入時遇到的坑
實現思路:遍歷指定路徑下第一層所有資料夾與檔案並組成物件序列化成Json並返回Json資料給zTree繫結
 
    剛開始想著一次性把所有的檔案與資料夾都載入出來,然後用遞迴實現,但是因為我定義了一個類來儲存檔案的資訊,而且是以巢狀的形式存的,這樣的做法初看覺得
很有層次感到寫遞迴時,卻把自己坑了,也主要是以前沒認真寫過遞迴,導致這一方法糾結了一個晚上再加上午的一點時間,終於遞迴出來了,
然後一試驗,瀏覽器直接死了,數量太大巢狀的太深,載入一次至少要40秒以上,於是果斷放棄了,改為每次只載入一層
以下為儲存資料的類,與載入的方法 , 這裡只列出部分程式碼,具體程式碼在原始碼中都可找到
 
類設計
 1  public class zTreeModel
 2     {
 3         public zTreeModel()
 4         {
 5             _isPrent = false;
 6             _children = new List<zTreeModel>();
 7         }
 8 
 9         private bool _isPrent ;
10         private List<zTreeModel> _children = null;
11        
12         /// <summary>
13         ///根結點名字
14         /// </summary>
15         public string rootFullName { get; set; }
16 
17         /// <summary>
18         /// 父結點名稱
19         /// </summary>
20         public string pName { get; set; }
21         /// <summary>
22         /// 父結點全路徑名稱
23         /// </summary>
24         public string pFullName { get; set; }
25 
26         /// <summary>
27         /// 當前結點全路徑名稱
28         /// </summary>
29         public string fullName { get; set; }
30 
31         /// <summary>
32         /// 當前結點名稱
33         /// </summary>
34         public string name { get; set; }
35    
36         /// <summary>
37         /// 是否為父結點
38         /// </summary>
39         public bool isParent {
40             get { return _isPrent; }
41             set { _isPrent = value; }
42         }
43         /// <summary>
44         /// 是否為頂層節點
45         /// </summary>
46         public bool topNode { get; set; }
47 
48         /// <summary>
49         /// 是否為檔案 預設為False 
50         /// </summary>
51         public bool isFile { get; set; }
52 
53         /// <summary>
54         /// 是否開啟
55         /// </summary>
56         public bool open { get; set; }
57 
58         /// <summary>
59         /// 子結點
60         /// </summary>
61         public List<zTreeModel> children
62         {
63             get { return _children; }
64             set { _children = value; }
65         }
66     }
View Code

JavaScript

 1     var zTreeObj,
 2         setting = {
 3             view: {
 4                 selectedMulti: true,
 5                 showLine: true                
 6             },
 7             edit: {
 8                 drag: {
 9                     isMove: true,
10                     inner:true//拖拽後為同級節點,
11                 },//託拽操作配置
12                 enable: true,//節點可編輯
13                 showRemoveBtn: false,
14                 showRenameBtn: false,
15                 editNameSelectAll: true
16             },
17             data: {
18                 keep: {
19                     parent:true //刪除子節點後父節點不變為葉子節點
20                 },
21                 
22             },
23             async: {
24                 enable: true,
25                 autoParam:["fullName"],
26                 url: "/FileManager/GetSingleLevelNodes",
27                 type: "POST",
28                 
29             },
30             treeNode: {
31                 checked:true
32                     
33             },
34             callback:
35             {
36                // beforeExpand: zTreeBeforeExpand,//展開節點前的回撥方法
37 
38                 beforeRename: zTreeBeforeRename,//重新命名之前的回撥方法
39                 onRename: zTreeOnRename,//重新命名
40                 
41                 beforeRemove: zTreeBeforeRemove,//刪除前回撥方法
42                 onRemove: zTreeRemove,//刪除
43                 
44                 beforeRightClick: zTreeBeforeRightClick,//右鍵方法前的回撥
45                 onRightClick: zTreeOnRightClick,//右鍵方法
46                 
47                 beforeDrop: zTreeBeforeDrop,//用於捕獲節點拖拽操作結束之前的事件回撥函式,並且根據返回值確定是否允許此拖拽操作.如果返回 false,zTree 將恢復被拖拽的節點,也無法觸發 onDrop 事件回撥函式
48                 onDrop: zTreeOnDrop,//拖拽操作結束的事件
49                 beforeDrag: zTreeBeforeDrag//託拽前的方法 
50                 
51            }
52         };
53        
54     var zTreeNodes = "";
55     $(function () {
56         ReLoadTree();
57     });
58     
59     function ReLoadTree() {
60         $.ajax({
61             url: "/FileManager/GetDefaultFiles",
62             type: "POST",
63             async: false,
64             dataType: "json",
65             success: function (data) {
66                 zTreeNodes = data;
67             }
68         });
69         zTreeObj = $.fn.zTree.init($("#tree"), setting, zTreeNodes);
70 
71     }
View Code

C#

 1         public List<zTreeModel> GetDefaultFiles(string path)
 2         {
 3             zTreeModel treeModel = null;
 4             List<zTreeModel>  treeModelList = new List<zTreeModel>();
 5             if (Directory.Exists(path))
 6             {
 7                 //獲取子目錄
 8                 DirectoryInfo  directory =  new DirectoryInfo(path);
 9               
10                 try
11                 {
12                     var folders = directory.GetDirectories();
13                     //遍歷路徑下資料夾
14                     foreach (var folder in folders)
15                     {
16                         treeModel = new zTreeModel();
17                         treeModel.pName = folder.Parent == null ? " " : folder.Parent.Name;
18                         treeModel.pFullName = folder.Parent == null ? " " : folder.Parent.FullName;
19                         treeModel.rootFullName = folder.Root.FullName;
20                         treeModel.name = folder.Name;
21                         treeModel.fullName = folder.FullName;
22                         treeModel.isParent = true;
23                         treeModelList.Add(treeModel);
24                     }
25 
26                 }
27                 catch (UnauthorizedAccessException ex)//呼叫方沒有所要求的許可權。
28                 {
29                     return null;
30                 }
31 
32                 //獲取路徑下檔案 
33                 DirectoryInfo fileDirectory = new DirectoryInfo(path);
34                 
35                 try
36                 {
37                     var files = fileDirectory.GetFiles();
38                     foreach (var file in files)
39                     {
40                         treeModel = new zTreeModel();
41                         treeModel.pName = file.Directory == null ? "" : file.Directory.Name;
42                         treeModel.pFullName = file.DirectoryName;
43                         treeModel.rootFullName = file.Directory == null ? "" : file.Directory.Root.FullName;
44                         treeModel.name = file.Name;
45                         treeModel.fullName = file.FullName;
46                         treeModel.isFile = true;
47                         treeModelList.Add((treeModel));
48                     }
49 
50                 }
51                 catch (UnauthorizedAccessException ex) //呼叫方沒有所要求的許可權。
52                 {
53                     return null;
54                 }
55             }
56 
57             return treeModelList;
58 
59         }
View Code

 

3.2 新增資料夾節點

實現思路:彈出框寫資料夾名,然後前臺組裝新節點的json資料,並把新節點物件傳到Action中反序列化成物件,根據節點屬性建立資料夾,如果已存在相同檔名則提示,否則正常建立

javascript 

 1 //新增節點
 2     function zTreeAddNode() {
 3         var zTreeObj = $.fn.zTree.getZTreeObj("tree");
 4         var parent = zTreeObj.getSelectedNodes()[0]; //把選中的節點當做父節點 
 5         if (parent == undefined) {
 6             layer.alert('給新孩子找個父節點啊~~', 8);
 7             return false;
 8         } else if (parent.isParent == false) {
 9             layer.alert('親~只能選資料夾哦~', 8);
10             return false;
11         }
12 
13         ShowDialog(parent, zTreeObj);
14     }
15 
16     function ShowDialog(parent, zTreeObj) {
17 
18         $.layer({
19             shade: [0.5, '#000', true], //遮罩
20             dialog: {
21                 msg: '資料夾名:<input type="text" id="folderName"/>',
22                 title: ['新建資料夾', true],
23                 btns: 2,
24                 type: 1,
25                 area: ['503px', '395px'],
26                 btn: ['新增', '取消'],
27                 yes: function(index) {
28                  
29                     var name = $("#folderName").val();
30                     if (name == "") {
31                         layer.alert('啊喂~還沒寫名字呢~', 8);
32                         return false;
33                     }
34                     //拼裝新節點物件
35                     var nodes = { "fullName": parent.fullName + " \\ " + name, "name": name, "pName": parent.pName, "pFullName": parent.pFullName, "isParent": true };
36                     $.ajax({
37                         url: "/FileManager/AddNode",
38                         type: "POST",
39                         data: {
40                             "node": JSON.stringify(nodes)
41                         },
42                         dataType: "json",
43                         success: function(data) {
44                             if (!data.Status) {
45                                 layer.alert(data.Message, 8);
46                                 layer.close(index);
47                                 return false;
48                             }
49                             zTreeObj.addNodes(parent, nodes);
50                             layer.close(index);
51                         }
52                     });
53 
54                 },
55                 no: function(index) {
56                     layer.close(index);
57                 }
58             }
59         });
60 
61     }
View Code

C# 

 1         public OperationResult CreateFolder(zTreeModel zTree, OperationResult operation)
 2         {
 3             DirectoryInfo directory = new DirectoryInfo(zTree.fullName);
 4             if (directory.Exists)
 5             {
 6                 operation.Status = false;
 7                 operation.Message = "資料夾已存在了哦~";
 8             }
 9             else
10             {
11                 directory.Create();
12             }
13             return operation;
14         }
View Code

 

 

3.3  刪除樹上資料夾節點

     思路:取到選中節點,再取節點fullName 屬性,(此屬性儲存了檔案的全路徑),把fullName非同步傳入Action後呼叫方法,遞迴刪除資料夾,遞迴的方式是先遍歷到最深層的子資料夾如果有遇到不可訪問的檔案則刪除操作不能繼續進行,並提示使用者不能刪除的資料夾名,如果都可訪問則從最深層的子資料夾開始遞迴刪除,程式碼如下

JavaScript

 1     //刪除節點
 2     function zTreeRemove() {
 3         var treeObj = $.fn.zTree.getZTreeObj("tree");
 4         var node = treeObj.getSelectedNodes();
 5         var isParents = false;
 6         $.each(node, function(index, item) {
 7             if (item.isParent) {
 8                 isParents = true;
 9             }
10         });
11         if (isParents) {
12             var meg = "確定要把資料夾中所有檔案都刪了嗎?不能恢復的哦~也不能怪我哦~";
13             deleteNodes(node, meg);
14         } else {
15             var meg = "確定要刪除所以選中的檔案嗎?不能恢復的哦~";
16             deleteNodes(node, meg);
17         }
18     }
19     function deleteNodes(nodes, meg) {
20         $.layer({
21             shade: [0.5, '#000', true], //遮罩
22             dialog: {
23                 msg: meg,
24                 title: '刪除資料夾',
25                 btns: 2,
26                 type: 1,
27                 area: ['503px', '395px'],
28                 btn: ['刪除', '取消'],
29                 yes: function(index) {
30                     $.ajax({
31                         url: "/FileManager/DeleteNode",
32                         type: "POST",
33                         data: { "node": JSON.stringify(nodes) },
34                         dataType: "json",
35                         success: function(data) {
36                             if (!data.Status) {
37                                 layer.alert(data.Message, 8);
38                             } else {
39                                 var treeObj = $.fn.zTree.getZTreeObj("tree");
40                                 var selectedNodes = treeObj.getSelectedNodes();
41                                 for (var i = 0; i < selectedNodes.length; i++) {
42                                     treeObj.removeNode(selectedNodes[i]);
43                                 }
44                             }
45                         }
46                     });
47                     layer.close(index);
48                 }, no: function(index) {
49                     layer.close(index);
50                 }
51             }
52             
53         });
54 
55     }
View Code

C#

 1         public OperationResult Delete(DirectoryInfo directory, OperationResult operation)
 2         {
 3             //從深層子目錄開始遍歷刪除
 4             DirectoryInfo[] childFolder = directory.GetDirectories();
 5             
 6             if (operation.Status)
 7             {
 8                 //有元素就遍歷刪除,沒有則直接刪除資料夾
 9                 if (childFolder.Any())
10                 {
11                     foreach (var directoryInfo in childFolder)
12                     {
13                         Delete(directoryInfo, operation);
14                         FileInfo[] files = directoryInfo.GetFiles();
15                         try
16                         {
17                             foreach (var fileInfo in files)
18                             {
19                                 fileInfo.Delete();
20                             }
21                             directoryInfo.Delete();
22                         }
23                         catch (UnauthorizedAccessException ex)
24                         {
25                             operation.Status = false;
26                             operation.Message = string.Format("{0}此資料夾沒有許可權訪問!無法執行刪除操作", directoryInfo.FullName);
27                             return operation;
28                         }
29                     }
30                 }
31                 else
32                 {
33                     //驗證檔案是否能刪除
34                     try
35                     {
36                         directory.Delete();
37                     }
38                     catch (UnauthorizedAccessException ex)
39                     {
40                         operation.Status = false;
41                         operation.Message = string.Format("{0}此資料夾沒有許可權訪問!無法執行刪除操作", directory.FullName);
42                         return operation;
43                     }
44                 }
45             }
46             return operation;
47         }
View Code

 

 

3.4  右鍵方法 

思路:點選右鍵時觸發右鍵事件,在事件方法中把事先寫好的Html選單展示出來並繫結相應的js事件,根結點沒有刪除與重新命名操作,只能新增子節點

javaScript

 1     //右鍵方法
 2     function zTreeOnRightClick(event, treeId, treeNode) {
 3         if (treeId == undefined) {
 4             return false;
 5         }
 6        
 7         $("#rMenu").css({
 8             top: event.clientY + "px",
 9             left:event.clientX +"px",
10             display: "block",
11             "z-index": 1
12         });
13 
14         if (treeNode.topNode) {
15             showItem(["#addFolder"]);
16         } else {
17             showItem(["#addFolder", "#reName", "#deleteFile"]);
18         }
19 
20     }
21     //顯示右鍵選單
22     function showItem(itemArray) {
23         
24         for (var i = 0; i < itemArray.length; i++) {
25 
26             $(itemArray[i]).show();
27         }
28         $("#rMenu").hover(function() {
29             $("#addFolder").click(function() {
30                 //alert("第一次新增!");
31                 zTreeAddNode();
32                 $("#rMenu").hide();
33             });
34             $("#reName").click(function() {
35                 Rename();
36                 $("#rMenu").hide();
37             });
38             $("#deleteFile").click(function() {
39                 zTreeRemove();
40                 $("#rMenu").hide();
41             });
42         },
43             function() {
44                 for (var j = 0; j < itemArray.length; j++) {
45                     $(itemArray[j]).hide();
46                 }
47             });
48 
49     }
View Code

3.5 重新命名

思路:觸發重新命名方法後會使節點處於編輯狀態,失去焦點後自動儲存,在儲存時先要做驗證沒有相同的檔名,並重新整理節點的屬性

注意:因為要先驗證檔名是否已存在,所以先要非同步去檢查,但是檢查與執行重新命名的方法都是非同步的沒法分先後,且方法都要用的zTree提供的方法,所以每次重新命名後要重新載入一次整棵樹 體驗有點不太好

javascript

 1  function zTreeBeforeRename(treeId, treeNode, newName, isCancel) {
 2         //檔名長度不能超過260個字元(包括路徑)
 3         var zTreeObj = $.fn.zTree.getZTreeObj("tree");
 4         if ((treeNode.fullName + newName).length > 260) {
 5             layer.alert("啊喂~ 你檔名也太長了點吧!", 8);
 6             zTreeObj.editName(treeNode);
 7         }
 8         var status = false;
 9         var path = treeNode.pFullName + "\\" + newName;
10         //判斷新檔名是否已存在
11         $.ajax({
12             url: "/FileManager/CheckRename",
13             async:false,
14             type: "POST",
15             data: {
16                 "path": path,
17                 "isParent": treeNode.isParent
18             },
19             dataType: "json",
20             success: function (data) {
21                 status = data.Status;
22                 if (!data.Status) {
23                     layer.alert(data.Message, 8);
24                     //阻止因Alter反覆呼叫zTreeBeforeRename方法
25                     //zTreeObj.editName(treeNode);
26                     //return false;
27                 } else {
28                     //return true;
29                 }
30             }
31         });
32        
33         if (status) {
34             return true;
35         } else {
36             return false;
37         }
38 
39     }
40 
41     function Rename() {
42         var zTreeObj = $.fn.zTree.getZTreeObj("tree");
43         var nodes = zTreeObj.getSelectedNodes();//取到為選中節點陣列
44         zTreeObj.editName(nodes[0]);//把第一個節點變為編輯狀態
45 
46     }
47 
48     //重新命名
49     function zTreeOnRename(event, treeId, treeNode, isCancel) {
50 
51         //把檢查檔名放在此方法中
52 
53         var zTreeObj = $.fn.zTree.getZTreeObj("tree");
54       
55         var path = treeNode.fullName;
56         var destPath = treeNode.pFullName + "\\" + treeNode.name;
57         var isParent = treeNode.isParent;
58         //重新命名後,fullname,name 都要修改
59         $.ajax({
60             url: "/FileManager/RenameFiles",
61             async:false,
62             type: "POST",
63             data: {
64                 "path": path,
65                 "destPath": destPath,
66                 "isParent": isParent
67             },
68             dataType: "json",
69             success: function(data) {
70                 if (data.Status) {
71                     
72                     ReLoadTree();
73 
74                     //重新命名後重新整理父節點,更新子節點中的fullName等屬性
75                     //var selectNodes = zTreeObj.getSelectedNodes();
76                     //var parent = selectNodes[0].getParentNode();
77                     //zTreeObj.reAsyncChildNodes(parent, "refresh");
78                 }
79             }
80         });
81 
82 
83     }
View Code

C# 

 1  //檢查檔名是否存在
 2         public void CheckFileName(string path, bool isParent,OperationResult operation)
 3         {
 4             if (isParent)
 5             {
 6                 if (Directory.Exists(path))
 7                 {
 8                     operation.Status = false;
 9                     operation.Message = "資料夾已經存在了哦!";
10                 }
11             }
12             else
13             {
14                 if (File.Exists(path))
15                 {
16                     operation.Status = false;
17                     operation.Message = "檔案已經存在了哦!";
18                 }
19             }
20         }
21         //重新命名
22         public void RenameFileName(string path, string destPath, bool isParent, OperationResult operation)
23         {
24             if (isParent)
25             {
26                 DirectoryInfo directory = new DirectoryInfo(path);
27                 directory.MoveTo(destPath);
28             }
29             else
30             {
31                 FileInfo file = new FileInfo(path);
32                 file.MoveTo(destPath);
33             }
34         }
View Code

3.6 拖拽方法(移動資料夾,按Ctrl可多選)

思路:根節點不能被拖動,也不能拖為根節點,然後遍歷選中的節點,並傳到Action中反序列化成物件執行移動,並在移動後在前端把節點的屬性pName fullName pFullName更新,避免重新載入樹

javascript

 1     //拖拽前的方法 
 2     function zTreeBeforeDrag(treeId, treeNodes) {
 3         //根結點不能被移動,
 4         for (var i = 0; i < treeNodes.length; i++) {
 5             if (treeNodes[i].topNode) {
 6                 layer.alert("根結點不能被託動哦~", 8);
 7                 return false;
 8             }
 9         }
10     }
11     //捕獲節點拖拽操作結束之前的事件回撥函式,並且根據返回值確定是否允許此拖拽操作
12     //如果返回 false,zTree 將恢復被拖拽的節點,也無法觸發 onDrop 事件回撥函式
13 
14     function zTreeBeforeDrop(treeId, treeNodes, targetNode, moveType) {
15         //不能拖拽為根節點
16         if ((targetNode == null || (moveType != "inner"))) {
17             return false;
18         }
19         if (!targetNode.isParent) {
20             layer.alert("只能託動到資料夾內哦~", 8);
21             return false;
22         }
23        
24         $.ajax({
25             url: "FileManager/MoveFiles",
26             async: false,
27             type: "POST",
28             data: {
29                 "nodes": JSON.stringify(treeNodes),
30                 "targetNode": JSON.stringify(targetNode)
31             },
32             dataType: "json",
33             success: function (data) {
34                 if (!data.Status) {
35                     layer.alert(data.Message, 8);
36                     return false;
37                 } else {
38                     //節點拖動後要把的路徑改為新路徑
39                     for (var i = 0; i < treeNodes.length; i++) {
40                         treeNodes[i].fullName = targetNode.fullName + "\\" + treeNodes[i].name;
41                         treeNodes[i].pName = targetNode.name;
42                         treeNodes[i].pFullName = targetNode.fullName;
43                     }
44                 }
45             }
46         });
47     }
View Code

C# 

 1  public void  MoveFilesToFolder(List<zTreeModel> nodes, zTreeModel targetNode,OperationResult operation)
 2         {
 3             
 4 
 5             foreach (var node in nodes)
 6             {
 7                 //資料夾
 8                 if (node.isParent)
 9                 {
10                     DirectoryInfo directory = new DirectoryInfo(node.fullName);
11                     if (Directory.Exists(targetNode.fullName))
12                     {
13                         //要移動的新地址
14                         string newPath = targetNode.fullName + "\\" + node.name;
15                         directory.MoveTo(newPath);
16                     }
17                     else
18                     {
19                         operation.Status = false;
20                         operation.Message = string.Format("{0}資料夾不存在啊~!", node.fullName);
21                     }
22 
23                 }//檔案
24                 else
25                 {
26                     FileInfo file = new FileInfo(node.fullName);
27                     if (Directory.Exists(targetNode.fullName))
28                     {
29                         string newPath = targetNode.fullName + "\\" + node.name;
30                         file.MoveTo(newPath);
31                     }
32                     else
33                     {
34                         operation.Status = false;
35                         operation.Message = string.Format("{0}資料夾不存在啊~!", node.fullName);
36                     }
37                 }
38 
39             }
40           
41         }
View Code

 

 

四 總結

    雖然這個小工具功能並不強大,但至少是我從有想法到有行動到有成果的一個過程,並且我也享受這一過程,

在開發的過程中,瞭解了zTree的各種用法,對資料夾,檔案的操作也有了更深入的瞭解,

如果您覺得這個工具有那麼點意思 不妨點下推薦哦~您的推薦是我創作源源不斷的動力

 原始碼在這裡

 

相關文章