sqlalchemy tree 樹形分類 無限極分類的管理。預排序樹,左右值樹。sqlalchemy-mptt


簡介:

無限極分類是一種比較常見的數據格式,生成組織結構,生成商品分類信息,權限管理當中的細節權限設置,都離不開無限極分類的管理。

常見的有鏈表式,即有一個Pid指向上級的ID,以此來設置結構。寫的時候簡單,用的時候效果一班,比如說,同一級沒有辦法手動重新排序,查詢所有子孫的時候不方便。

所以有了預排序樹,即左右值樹形管理。

優點還是挺多的。

可以快速確定關系,最短路徑,同級排序,查找所有子孫(最好的地方)

 

一:主要包

  1. sqlalchemy_mptt

    1. https://github.com/uralbash/sqlalchemy_mptt 
    2. pip install sqlalchemy_mptt 
  2. sqlalchemy-orm-tree

    1. https://github.com/monetizeio/sqlalchemy-orm-tree/
    2. pip install sqlalchemy-orm-tree
    3. 最后更新2015年8月27日,還沒有完整的示例代碼,僅有的幾行,還………………
    4. 放棄
  3. ztree

    1. http://www.treejs.cn/
    2. 這個是前台JS顯示用的,希望我能整合到flask-admin中去。

二:sqlalchemy_mptt

1.快速指南

源碼:

# !/usr/bin/python3
# -*- coding: utf-8 -*-
# @Time    : 2018-09-12 16:32
# @Author  : Jackadam
# @Email   :jackadam@sina.com
# @File    : mptt.py
# @Software: PyCharm
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy_mptt import mptt_sessionmaker
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy_mptt.mixins import BaseNestedSets

Base = declarative_base()


class Tree(Base, BaseNestedSets):
    __tablename__ = "tree"
    id = Column(Integer, primary_key=True)
    name = Column(String(8))

    def __repr__(self):
        return "<Node (%s)>" % self.id


engine = create_engine('sqlite:///mptt.db', echo=False)
mptt_ession = mptt_sessionmaker(sessionmaker(bind=engine))
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))



def print_tree(group_name, tab=1):
    """
    :param str group_name:要查找的樹的根的名稱
    :param int tab: 格式化用的-數量
    """
    group = db_session.query(Tree).filter_by(name=group_name).one_or_none()  # type: TreeGroup
    if not group:
        return
    # group found - print name and find children
    print('- ' * tab + group.name)
    for child_group in group.children:  # type: TreeGroup
        # new tabulation value for child record
        print_tree(child_group.name, tab * 2)

if __name__ == '__main__':
    # Base.metadata.create_all(bind=engine)
    # nodes=[]
    # node=Tree(name='中國')
    # nodes.append(node)
    # db_session.add_all(nodes)
    # db_session.commit()
    # nodes = []
    # ref_id=db_session.query(Tree.id).filter_by(name='中國').first()[0]
    # print(ref_id)
    # new_name=['河南','河北','山東','山西','陝西']
    # for i in new_name:
    #     print(i)
    #     node=Tree(name=i,parent_id=ref_id)
    #     nodes.append(node)
    # db_session.add_all(nodes)
    # db_session.commit()
    # nodes = []
    # ref_id = db_session.query(Tree.id).filter_by(name='河南').first()[0]
    # print(ref_id)
    # new_name = ['鄭州', '洛陽', '開封', '新鄉', '新鄭']
    # for i in new_name:
    #     print(i)
    #     node = Tree(name=i, parent_id=ref_id)
    #     nodes.append(node)
    # db_session.add_all(nodes)
    # db_session.commit()
    print_tree('中國')
    print_tree('河南')

2.結果樣式:

print_tree('中國')

- 中國
- - 河南
- - - - 鄭州
- - - - 洛陽
- - - - 開封
- - - - 新鄉
- - - - 新鄭
- - 河北
- - 山東
- - 山西
- - 陝西

print_tree('河南')


- 河南
- - 鄭州
- - 洛陽
- - 開封
- - 新鄉
- - 新鄭

3.優勢

我加入的順序可不是這個順序,但是數據結構,和sqlalchemy_mptt幫我們處理了層級數據結構,就顯示為樹形結構了。

再試試下面的代碼,可以列出所有的數。

def print_all_tree(tab=1):
    """
    :param int tab: 格式化用的-數量
    """
    group = db_session.query(Tree).filter_by(parent_id=None).all()  # type: TreeGroup
    print(group)
    if not group:
        return
    # group found - print name and find children
    for i in group:
        print('- ' * tab + i.name)
        for child_group in i.children:  # type: TreeGroup
            # new tabulation value for child record
            print_tree(child_group.name, tab * 2)    
    # nodes=[]
    # new_name = ['美國', '英國', '法國', '英國', '德國']
    # for i in new_name:
    #     print(i)
    #     node = Tree(name=i)
    #     nodes.append(node)
    # db_session.add_all(nodes)
    # db_session.commit()
    # ref_id = db_session.query(Tree.id).filter_by(name='美國').first()[0]
    # nodes = []
    # new_name = ['亞拉巴馬', '阿拉斯加', '亞利桑那', '阿肯色', '加利福尼亞']
    # for i in new_name:
    #     print(i)
    #     node = Tree(name=i,parent_id=ref_id)
    #     nodes.append(node)
    # db_session.add_all(nodes)
    # db_session.commit()
    print_all_tree(1)

三:數據結構分析

數據庫結構

數據庫里面的內容為

 

映射定義結構

分析:

我們只定義了主鍵id,name名字。但是數據庫里面多了幾列:lft,rgt,level,tree_id,parent_id

這些結構就是左右值樹用的東西,參照最上面的圖,每個名字都有左值和右值,可以很方便的查到結構。

比如說:

找到所有的子孫:查找Food的子孫,Food作為參考點,左值為1,右值為18,所有的子孫,就是數據庫中左值大於1,小於18的。

        查找Fruit的子孫,Fruit作為參考點,左值為2,右值為11,所有的子孫,就是數據庫中左值大於2,小於11的。

找到所有的子節點(不包括孫節點):查找Food的子節點,Food作為參考點,level=1,tree_id=1。

        那么所有的子節點為  tree_id=1,level=1+1   層級為2的。

查找最短路徑:一般用在導航中,也有用在組合顯示上,因為需要知道上一級,上N級的路徑結構:

        查找Banana的上級路徑,Banana作為參考點,左值為8,右值為9,那么路徑就是數據庫中左值小於8,並且右值大於9的。排序按左值或右值的升序降序,就隨便你了。

        結果是    Food--Fruit--Yellow--Banana


四:算法

1.簡介:

關於預排序樹的算法,有很多,我也寫不好,所以我用了sqlalchemy_mptt,還是大概介紹一下吧。

2.增加

按sqlalchemy_mptt的用法,

如果沒有parent_id,那么就創建為一個新樹的根節點,parent_id是空的,level是1,tree_id根據數據庫的情況順序向上加。

如果有提供parent_id,那么久創建為parent的子節點,parent_id是提供的,level是parent的level+1,tree_id和parent一致。

同時要更新受影響的其他節點。

左值處理一遍。

大於parent_id右值的所有左值,+2

右值處理一遍。

大於等於parent_id右值的所有右值,+2

3.刪除

和增加差不多,刪除一個節點以后,也要更新受影響的左值和右值。

4.移動

這個其實就是刪除一個老節點,再創建一個新節點。

5.完全不用擔心

因為這些功能,sqlalchem_mptt全都實現了,根本不需要你操心。具體怎么用,我還在學習中。

下面的東西,隨着我的學習,逐步更新。

 

五:ztree

1.ztree基礎顯示(帶編輯功能)

下面的代碼,保存為html,直接就可以了。

<!DOCTYPE html>
<HTML>
<HEAD>
    <TITLE> ZTREE DEMO - beforeEditName / beforeRemove / onRemove / beforeRename / onRename</TITLE>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <link rel="stylesheet" href="../../../css/demo.css" type="text/css">
    <link rel="stylesheet" href="../../../css/zTreeStyle/zTreeStyle.css" type="text/css">
    <script type="text/javascript" src="../../../js/jquery-1.4.4.min.js"></script>
    <script type="text/javascript" src="../../../js/jquery.ztree.core.js"></script>
    <script type="text/javascript" src="../../../js/jquery.ztree.excheck.js"></script>
    <script type="text/javascript" src="../../../js/jquery.ztree.exedit.js"></script>
    <SCRIPT type="text/javascript">
        <!--
        var setting = {
            view: {
                addHoverDom: addHoverDom,
                removeHoverDom: removeHoverDom,
                selectedMulti: false
            },
            edit: {
                enable: true,
                editNameSelectAll: true,
                showRemoveBtn: showRemoveBtn,
                showRenameBtn: showRenameBtn
            },
            data: {
                simpleData: {
                    enable: true
                }
            },
            callback: {
                beforeDrag: beforeDrag,
                beforeEditName: beforeEditName,
                beforeRemove: beforeRemove,
                beforeRename: beforeRename,
                onRemove: onRemove,
                onRename: onRename
            }
        };

        var zNodes = [
            {id: 1, pId: 0, name: "父節點 1", open: true},
            {id: 11, pId: 1, name: "葉子節點 1-1"},
            {id: 12, pId: 1, name: "葉子節點 1-2"},
            {id: 13, pId: 1, name: "葉子節點 1-3"},
            {id: 2, pId: 0, name: "父節點 2", open: true},
            {id: 21, pId: 2, name: "葉子節點 2-1"},
            {id: 22, pId: 2, name: "葉子節點 2-2"},
            {id: 23, pId: 2, name: "葉子節點 2-3"},
            {id: 3, pId: 0, name: "父節點 3", open: true},
            {id: 31, pId: 3, name: "葉子節點 3-1"},
            {id: 32, pId: 3, name: "葉子節點 3-2"},
            {id: 33, pId: 3, name: "葉子節點 3-3"}
        ];
        var log, className = "dark";

        function beforeDrag(treeId, treeNodes) {
            return false;
        }

        function beforeEditName(treeId, treeNode) {
            className = (className === "dark" ? "" : "dark");
            showLog("[ " + getTime() + " beforeEditName ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
            var zTree = $.fn.zTree.getZTreeObj("treeDemo");
            zTree.selectNode(treeNode);
            setTimeout(function () {
                if (confirm("進入節點 -- " + treeNode.name + " 的編輯狀態嗎?")) {
                    setTimeout(function () {
                        zTree.editName(treeNode);
                    }, 0);
                }
            }, 0);
            return false;
        }

        function beforeRemove(treeId, treeNode) {
            className = (className === "dark" ? "" : "dark");
            showLog("[ " + getTime() + " beforeRemove ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
            var zTree = $.fn.zTree.getZTreeObj("treeDemo");
            zTree.selectNode(treeNode);
            return confirm("確認刪除 節點 -- " + treeNode.name + " 嗎?");
        }

        function onRemove(e, treeId, treeNode) {
            showLog("[ " + getTime() + " onRemove ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
        }

        function beforeRename(treeId, treeNode, newName, isCancel) {
            className = (className === "dark" ? "" : "dark");
            showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " beforeRename ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name + (isCancel ? "</span>" : ""));
            if (newName.length == 0) {
                setTimeout(function () {
                    var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                    zTree.cancelEditName();
                    alert("節點名稱不能為空.");
                }, 0);
                return false;
            }
            return true;
        }

        function onRename(e, treeId, treeNode, isCancel) {
            showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " onRename ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name + (isCancel ? "</span>" : ""));
        }

        function showRemoveBtn(treeId, treeNode) {
            return !treeNode.isFirstNode;
        }

        function showRenameBtn(treeId, treeNode) {
            return !treeNode.isLastNode;
        }

        function showLog(str) {
            if (!log) log = $("#log");
            log.append("<li class='" + className + "'>" + str + "</li>");
            if (log.children("li").length > 8) {
                log.get(0).removeChild(log.children("li")[0]);
            }
        }

        function getTime() {
            var now = new Date(),
                h = now.getHours(),
                m = now.getMinutes(),
                s = now.getSeconds(),
                ms = now.getMilliseconds();
            return (h + ":" + m + ":" + s + " " + ms);
        }

        var newCount = 1;

        function addHoverDom(treeId, treeNode) {
            var sObj = $("#" + treeNode.tId + "_span");
            if (treeNode.editNameFlag || $("#addBtn_" + treeNode.tId).length > 0) return;
            var addStr = "<span class='button add' id='addBtn_" + treeNode.tId
                + "' title='add node' onfocus='this.blur();'></span>";
            sObj.after(addStr);
            var btn = $("#addBtn_" + treeNode.tId);
            if (btn) btn.bind("click", function () {
                var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                zTree.addNodes(treeNode, {id: (100 + newCount), pId: treeNode.id, name: "new node" + (newCount++)});
                return false;
            });
        };

        function removeHoverDom(treeId, treeNode) {
            $("#addBtn_" + treeNode.tId).unbind().remove();
        };

        function selectAll() {
            var zTree = $.fn.zTree.getZTreeObj("treeDemo");
            zTree.setting.edit.editNameSelectAll = $("#selectAll").attr("checked");
        }

        $(document).ready(function () {
            $.fn.zTree.init($("#treeDemo"), setting, zNodes);
            $("#selectAll").bind("click", selectAll);
        });
        //-->
    </SCRIPT>
    <style type="text/css">
        .ztree li span.button.add {
            margin-left: 2px;
            margin-right: -1px;
            background-position: -144px 0;
            vertical-align: top;
            *vertical-align: middle
        }
    </style>
</HEAD>

<BODY>

<ul id="treeDemo" class="ztree"></ul>

</BODY>
</HTML>
View Code

其中head引入了jquery,ztree的一些JS文件。

然后var setting,設置了ztree的一些參數

var zNodes,設置了一個簡單結構的數結構數據。

一些function,定義了鼠標移上去,移出去,點擊……的一些事件。

<style type="text/css">
.ztree li span.button.add {


這個樣式,定義了添加按鈕的圖標。

在body中

直接  <ul id="treeDemo" class="ztree"></ul>就可以顯示了

2.在flask中顯示

把整頁作為模板,動態傳入zNodes,就可以直接顯示了。

3.在flask-admin中顯示

因為我這個懶蛋,准備把flask-admin當前台用。數據展示也挺方便。

先寫一個頁面,單獨只顯示ztree

視圖函數:

用extra_css引入自定義css(ztree的)

用extra_js引入自定義js(ztree的)

用@expose('/')定義路由URL(flask-admin的)

tree_info,是讀取數據庫拿到的樹的基本信息。

class User_Groups_ModelView(ModelView):
    extra_css=[
        "../static/zTree_v3/css/demo.css",
        "../static/zTree_v3/css/zTreeStyle/zTreeStyle.css"
    ]
    extra_js = [
        "../static/zTree_v3/js/jquery.ztree.all.js",
        "../static/zTree_v3/js/jquery.ztree.core.js",
        "../static/zTree_v3/js/jquery.ztree.excheck.js",
        "../static/zTree_v3/js/jquery.ztree.exedit.js",
        "../static/zTree_v3/js/jquery.ztree.exhide.js",
        # "../static/zTree_v3/js/jquery-1.4.4.min.js",
    ]
    page_size = 20  # the number of entries to display on the list view
    can_create = False
    # can_edit = True
    # can_delete = False
    # can_view_details = True
    @expose('/')
    def index(self):
        tree_info=get_simple_json()
        return self.render('users/list.html',tree_info=tree_info)
View Code

get_simple_json就是按ztree的簡單結構,生成樹的基本數據。帶到模板去渲染

def get_simple_json():
    groups = db_session.query(User_Groups).all()
    # print(len(groups))
    result = []
    for i in groups:
        if i.parent_id == None:
            i.parent_id = 0
        _dict = {
            'id': i.id,
            'pId': i.parent_id,
            'name': i.groups_name,
        }
        result.append(_dict)
    # print(result)
    return result
View Code

模板文件

前面引入了flask-admin的基本模板

block body(顯示主體)

block model_menu_bar(菜單導航,就是新建--刪除--導出那一行)

<script></script>中間,寫了ztree的一些方法,用來動態的顯示出添加,編輯,刪除的小按鈕。

<ul id="treeDemo" class="ztree"></ul>

這個就是顯示了。

block tail

ztree的渲染,我把它寫在了尾部

{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% import 'admin/actions.html' as actionslib with context %}

{% block body %}
    {% block model_menu_bar %}
        <ul class="nav nav-tabs actions-nav">
            <li class="active">
                <a href="javascript:void(0)">{{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}</a>
            </li>

            {% if admin_view.can_create %}
                <li>
                    {%- if admin_view.create_modal -%}
                        {{ lib.add_modal_button(url=get_url('.create_view', url=return_url, modal=True), title=_gettext('Create New Record'), content=_gettext('Create')) }}
                    {% else %}
                        <a href="{{ get_url('.create_view', url=return_url) }}"
                           title="{{ _gettext('Create New Record') }}">{{ _gettext('Create') }}</a>
                    {%- endif -%}
                </li>
            {% endif %}

            {% if admin_view.can_export %}
                {{ model_layout.export_options() }}
            {% endif %}

            {% block model_menu_bar_before_filters %}{% endblock %}

            {% if filters %}
                <li class="dropdown">
                    {{ model_layout.filter_options() }}
                </li>
            {% endif %}

            {% if can_set_page_size %}
                <li class="dropdown">
                    {{ model_layout.page_size_form(page_size_url) }}
                </li>
            {% endif %}

            {% if actions %}
                <li class="dropdown">
                    {{ actionlib.dropdown(actions) }}
                </li>
            {% endif %}

            {% if search_supported %}
                <li>
                    {{ model_layout.search_form() }}
                </li>
            {% endif %}
            {% block model_menu_bar_after_filters %}{% endblock %}
        </ul>
    {% endblock %}
    <SCRIPT type="text/javascript">
        <!--

        var setting = {
            view: {
                addHoverDom: addHoverDom,
                removeHoverDom: removeHoverDom,
                selectedMulti: false
            },
            edit: {
                enable: true,
                editNameSelectAll: true,
                showRemoveBtn: showRemoveBtn,
                showRenameBtn: showRenameBtn
            },
            data: {
                simpleData: {
                    enable: true
                }
            },
            callback: {
                beforeDrag: beforeDrag,
                beforeEditName: beforeEditName,
                beforeRemove: beforeRemove,
                beforeRename: beforeRename,
                onRemove: onRemove,
                onRename: onRename
            }
        };

        var zNodes = {{ tree_info|safe }};
        var log, className = "dark";

        function beforeDrag(treeId, treeNodes) {
            alert('這里是增加?')
            return false;
        }

        function beforeEditName(treeId, treeNode) {
            className = (className === "dark" ? "" : "dark");
            showLog("[ " + getTime() + " beforeEditName ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
            var zTree = $.fn.zTree.getZTreeObj("treeDemo");
            zTree.selectNode(treeNode);
            setTimeout(function () {
                if (confirm("進入節點 -- " + treeNode.name + " 的編輯狀態嗎?")) {
                    setTimeout(function () {
                        zTree.editName(treeNode);
                    }, 0);
                }
            }, 0);
            return false;
        }

        function beforeRemove(treeId, treeNode) {
            className = (className === "dark" ? "" : "dark");
            showLog("[ " + getTime() + " beforeRemove ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
            var zTree = $.fn.zTree.getZTreeObj("treeDemo");
            zTree.selectNode(treeNode);
            return confirm("確認刪除 節點 -- " + treeNode.name + " 嗎?");
        }

        function onRemove(e, treeId, treeNode) {
            showLog("[ " + getTime() + " onRemove ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
        }

        function beforeRename(treeId, treeNode, newName, isCancel) {
            className = (className === "dark" ? "" : "dark");
            showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " beforeRename ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name + (isCancel ? "</span>" : ""));
            if (newName.length == 0) {
                setTimeout(function () {
                    var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                    zTree.cancelEditName();
                    alert("節點名稱不能為空.");
                }, 0);
                return false;
            }
            return true;
        }

        function onRename(e, treeId, treeNode, isCancel) {
            showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " onRename ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name + (isCancel ? "</span>" : ""));
        }

        function showRemoveBtn(treeId, treeNode) {
            return !treeNode.isFirstNode;
        }

        function showRenameBtn(treeId, treeNode) {
            return !treeNode.isLastNode;
        }

        function showLog(str) {
            if (!log) log = $("#log");
            log.append("<li class='" + className + "'>" + str + "</li>");
            if (log.children("li").length > 8) {
                log.get(0).removeChild(log.children("li")[0]);
            }
        }

        function getTime() {
            var now = new Date(),
                h = now.getHours(),
                m = now.getMinutes(),
                s = now.getSeconds(),
                ms = now.getMilliseconds();
            return (h + ":" + m + ":" + s + " " + ms);
        }

        var newCount = 1;

        function addHoverDom(treeId, treeNode) {
            var sObj = $("#" + treeNode.tId + "_span");
            if (treeNode.editNameFlag || $("#addBtn_"+treeNode.tId).length>0) return;
            var addStr = "<span class='button add' id='addBtn_" + treeNode.tId
                + "' title='增加節點' onfocus='this.blur();'></span>";
            sObj.after(addStr);
            var btn = $("#addBtn_"+treeNode.tId);
            if (btn) btn.bind("click", function(){
                var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                zTree.addNodes(treeNode, {id:(100 + newCount), pId:treeNode.id, name:"新節點" + (newCount++)});

                return false;
            });
        };
        function removeHoverDom(treeId, treeNode) {
            $("#addBtn_"+treeNode.tId).unbind().remove();
        };

        function selectAll() {
            var zTree = $.fn.zTree.getZTreeObj("treeDemo");
            zTree.setting.edit.editNameSelectAll = $("#selectAll").attr("checked");
        }

        //-->
    </SCRIPT>

    <ul id="treeDemo" class="ztree"></ul>

{% endblock %}
{% block tail %}
    <SCRIPT>
        $(document).ready(function () {
            $.fn.zTree.init($("#treeDemo"), setting, zNodes);
            $("#selectAll").bind("click", selectAll);
        });
    </SCRIPT>
{% endblock %}
View Code

有一個小問題

ztree的編輯功能當中沒有默認增加。所以增加這個功能寫在了,默認樣式表中也沒有這個樣式。

基礎示例當中有定義這個CSS。

.ztree li span.button.add {
    margin-left: 2px;
    margin-right: -1px;
    background-position: -144px 0;
    vertical-align: top;
    *vertical-align: middle
}

我就隨便寫進了demo.css,這樣顯示的就正常了。

4.定制flask-admin的顯示

待編輯功能寫完,再來研究這個問題。

六:ztree的編輯功能

1.簡介

默認的flask-admin功能是不能管理這個樹形結構的。它管理的是簡單結構,就是 id pid name 這種格式。

或許可以嘗試由簡單格式,自動創建為左右值樹格式

2.

3.


七:

 


八:

 

 


九:

 


十:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM