PHP Ionic Angularjs Phonegap AJAX Javascript CSS MySQL jQuery Forum

ประยุกต์ใช้งาน ระบบสมาชิกใน app ionic material ร่วมกับ SQLite

09 November 2016 By


สำหรับเนื้อหานี้จะเป็นตอนที่สอง ต่อจากตอนที่แล้ว ที่เราวางโครงสร้างรูปแบบ
คร่าวๆ ของ app ว่าจะเป็นในลักษณะไหน 
 
แนวทางใช้งาน ionic material สร้างระบบสมาชิก ร่วมกับ SQLite 
http://www.ninenik.com/content.php?arti_id=734 via @ninenik
 
โดยในตอนที่สอง จะเป็นการประยุกต์ให้สามารถใช้งานได้จริง ตั้งแต่การสมัคร
สมาชิก แล้วบันทึกลงฐานข้อมูล จากนั้นลิ้งค์ไปหน้าโพรไฟล์ ดึงข้อมูลจากฐาน
ข้อมูลมาแสดงในหน้าโพรไฟล์ และเมื่อล็อกเอาท์ออกจากระบบ ก็จะแสดงหน้า
ล็อกอิน ให้ผู้ใช้ทำการกรอก username และ password ใหม่อีกครั้งถ้าต้องการ
ล็อกอิน ประมาณนี้เป็นต้น
 

ก่อนอื่น เราต้องตรวจสอบ cordova plugin ที่จำเป็นเพิ่มเติมก่อน ซึ่งจะได้แก่

 
SQLite - สำหรับใช้งาน db 
cordova-sqlite-storage 1.4.8 "Cordova sqlite storage plugin" 
Toast  - สำหรับแสดงสถานะการทำงาน 
cordova-plugin-x-toast 2.5.2 "Toast"   
Dialogs - สำหรับขึ้นข้อความแจ้งเตือน หรือแจ้งการทำงานcordova-plugin-dialogs
cordova-plugin-dialogs 1.2.1 "Notification"  
SpinnerDialog - สำหรับขึ้นแสดง loading พร้อมข้อความ (เคยติดตั้งแล้วในบทความ map)
cordova-plugin-spinner-dialog 1.3.1 "SpinnerDialog" 
 
 

1. ตรวจสอบ plugin ด้วยคำสั่ง

 
C:\phonegap\learn001>phonegap plugin ls   
 
หากยังไม่ได้ติดตั้ง plugin ข้างต้น ให้ทำการติดตั้งด้วยคำสั่ง ดังนี้
 

ติดตั้ง plugin SQLite ด้วยคำสั่ง

C:\phonegap\learn001>cordova plugin add cordova-sqlite-storage
 

ติดตั้ง plugin Toast ด้วยคำสั่ง

C:\phonegap\learn001>cordova plugin add cordova-plugin-x-toast 
 

ติดตั้ง plugin Dialogs ด้วยคำสั่ง

C:\phonegap\learn001>cordova plugin add cordova-plugin-dialogs
 

ติดตั้ง plugin SpinnerDialog ด้วยคำสั่ง

C:\phonegap\learn001>cordova plugin add cordova-plugin-spinner-dialog
 
 

2. กำหนดในส่วนของ controller ของหน้าล็อกอิน หน้าสมัครสมาชิก และหน้าโพรไฟล์

   ในส่วนของ controller ให้เปิดไฟล์ controllers.js ในโฟลเดอร์ js ขึ้นมา 
   แล้วปรับโค้ดให้เป็นดังนี้ (คำอธิบายแสดงในโค้ด หากมีอธิบายเพิ่มเติมจะแสดงด้านล่างโค้ด)
 

   AppCtrl ส่วนของ controller หลัก 

.controller('AppCtrl', function($scope, $ionicModal, $ionicPopover, $timeout, $ionicPopup) {

    $scope.loginData = {};
    $scope.isExpanded = false;
    $scope.hasHeaderFabLeft = false;
    $scope.hasHeaderFabRight = false;

    var navIcons = document.getElementsByClassName('ion-navicon');
    for (var i = 0; i < navIcons.length; i++) {
        navIcons.addEventListener('click', function() {
            this.classList.toggle('active');
        });
    }

    ////////////////////////////////////////
    // Layout Methods
    ////////////////////////////////////////

    $scope.hideNavBar = function() {
        document.getElementsByTagName('ion-nav-bar')[0].style.display = 'none';
    };

    $scope.showNavBar = function() {
        document.getElementsByTagName('ion-nav-bar')[0].style.display = 'block';
    };

    $scope.noHeader = function() {
        var content = document.getElementsByTagName('ion-content');
        for (var i = 0; i < content.length; i++) {
            if (content[i].classList.contains('has-header')) {
                content[i].classList.toggle('has-header');
            }
        }
    };

    $scope.setExpanded = function(bool) {
        $scope.isExpanded = bool;
    };

    $scope.setHeaderFab = function(location) {
        var hasHeaderFabLeft = false;
        var hasHeaderFabRight = false;

        switch (location) {
            case 'left':
                hasHeaderFabLeft = true;
                break;
            case 'right':
                hasHeaderFabRight = true;
                break;
        }

        $scope.hasHeaderFabLeft = hasHeaderFabLeft;
        $scope.hasHeaderFabRight = hasHeaderFabRight;
    };

    $scope.hasHeader = function() {
        var content = document.getElementsByTagName('ion-content');
        for (var i = 0; i < content.length; i++) {
            if (!content[i].classList.contains('has-header')) {
                content[i].classList.toggle('has-header');
            }
        }

    };

    $scope.hideHeader = function() {
        $scope.hideNavBar();
        $scope.noHeader();
    };

    $scope.showHeader = function() {
        $scope.showNavBar();
        $scope.hasHeader();
    };

    $scope.clearFabs = function() {
        var fabs = document.getElementsByClassName('button-fab');
        if (fabs.length && fabs.length > 1) {
            fabs[0].remove();
        }
    };

    $scope.noShadow = function() {
        var headerBar = document.getElementsByTagName('ion-header-bar');
        for (var i = 0; i < headerBar.length; i++) {
            if (!headerBar[i].classList.contains('no-shadow')) {
                headerBar[i].classList.add('no-shadow');
            }
        }
    };

    $scope.hasShadow = function() {
        var headerBar = document.getElementsByTagName('ion-header-bar');
        for (var i = 0; i < headerBar.length; i++) {
            if (headerBar[i].classList.contains('no-shadow')) {
                headerBar[i].classList.remove('no-shadow');
            }
        }

    };

    // ตัวแปรสำหรับกำหนด การซ่อนหรือแสดงเมนูสมาชิก false คือซ่อนเมนูสมาชิก
    $scope.showMemberMenu = false;
    // สร้างฟังก์ชั่น สำหรับเรียกใช้ เพื่อกำหนดการ กำหนดค่าตัวแปรเพื่อซ่อนหรือแสดงเมนู
    $scope.setMemberMenu = function(status){
        $scope.showMemberMenu = status;
    };

    // กำหนดตัวแปรไว้สำหรับเก็บ id ของสมาชิกที่สมัครในเครื่องนั้นๆ
    $scope.sesMemberID = null;
    // สร้างฟังก์ชั่นสำหรับกำหนดค่า id ให้สามารถเรียกใช้งานจาก $scope หลักได้
    $scope.setMemberID = function(memID){
        $scope.sesMemberID = memID;
    };
    // สร้างฟังก์ชั่นสำหรับเรียกดูค่า id ของสมาชิกจาก $scope หลักได้
    $scope.getMemberID = function(){
        return $scope.sesMemberID;
    };


})
 
 
    สังเกตว่าเรามีการเพิ่ม ตัวแปร sesMemberID เข้ามาเพื่อใช้เก็บค่า member_id ของสมาชิก
รวมทั้งมีการสร้างฟังก์ชั่นกำหนดค่า และฟังก์ชั่นเรียกใช้งานค่า member_id เพื่อไว้สำหรับเรียกใช้
จาก controller ย่อยอื่นๆ ได้
 
    

    LoginCtrl ส่วนของ controller หน้า login

    การทำงานของหน้านี้คือ เริ่มกำหนดค่าว่างในตัวแปรที่ใช้ในฟอร์มล็อกอิน ตรวจสอบข้อมูลที่กรอก
ในฟอร์มล็อกอินว่ากรอกข้อมูลครบหรือไม่ จากนั้นส่งค่าข้อมูลไปตรวจสอบกับ db ว่ามีข้อมูล
ของสมาชิกนั้นหรือไม่ หากมีข้อมูลก็ให้เข้าสู่ระบบ และไปยังหน้าโพรไฟล์ 
 
.controller('LoginCtrl', function($scope, $timeout, $stateParams, ionicMaterialInk
,$ionicPlatform, $cordovaDialogs, $cordovaToast, $state) {
    $scope.$parent.clearFabs();
    $scope.$parent.hasShadow();
    $timeout(function() {
        $scope.$parent.hideHeader();
    }, 0);

    // กำนหดค่าเริ่มต้นของฟอร์มหน้าสมัครสมาชิก ให้เป็นค่าว่าง
    $scope.login = {
        input_user:'',
        input_pass:''
    };

    // เรียกใช้ฟังก์ชั่นจาก AppCtrl หลัก เพื่อซ่อนเมนูสมาชิก โดยส่งค่า false เข้าไป
    $scope.$parent.setMemberMenu(false);

    // สร้างฟังก์ชั่นสำหรับเรียกใช้ Toast plugin
    $scope.showToast = function(str, duration, position){
        $ionicPlatform.ready(function() { // เตรียมก่อนเรียกใช้ plugin
            return $cordovaToast
                .show(str, duration, position)
                .then(function(success) {
                    // success
                }, function (error) {
                    // error
                });
        });
    };


    var db = null;

    $ionicPlatform.ready(function() { // เตรียมก่อนเรียกใช้ plugin
        // ตรวจสอบและทำการเชื่อมต่อกับ db
        db = window.sqlitePlugin.openDatabase({
            name: 'my.db', location: 'default'
        }, function (db) {
            $scope.showToast('Open DB Success','long','bottom');
        }, function (error) {
            $scope.showToast('Open database ERROR: ' + JSON.stringify(error),'long','bottom');
        });
    });

    // สร้างฟังก์ชั่นสำหรับล็อกอิน รับค่า data จาก object login
    $scope.loginMember = function(data){
        $ionicPlatform.ready(function() { // เตรียมก่อนเรียกใช้ plugin

            // ตรวจสอบถ้ากรอกข้อมูลไม่ครบ
            if(data.input_user=="" || data.input_pass==""){
                $cordovaDialogs.alert('โปรดกรอกข้อมูลให้ครบถ้วน', 'ข้อมูลจำเป็น', 'ตกลง')
                    .then(function() {
                        return false;
                    });
                return false;
            }

            // แสดงข้อมูลที่ส่งเข้ามา เวลาใช้จริงสามารถลบออกได้
            $scope.showToast(' user: ' + data.input_user +
                ' pass: ' + data.input_pass +'', 'long', 'bottom');


            // เริ่มทำงานของคำสั่ง db
            db.transaction(function (tx) {
                // กำหนดคำสั่ง sql ค่าที่รับมาตรวจสอบ แทนด้วย ?
                // คำสั่งนี้คือเช็คว่า มีสมาชิกที่ user และ pass ตรงหรือไม่ ตัวเล็กตัวใหญ่มีค่าต่างกัน
                var query = "SELECT member_id, member_user, member_phone" +
                    " FROM member WHERE member_user = ? AND member_pass = ? ";
                // ทำคำสั่ง sql ส่งค่าเข้าไปให้ตรงกับจำนวน ในที่นี้มี data.input_user และ data.input_pass
                tx.executeSql(query, [data.input_user, data.input_pass], function (tx, resultSet) {
                        // ถ้าพบรายการใน db
                        if(resultSet.rows.length){
                            // นำค่า ID ของสมาชิกไปเก็บในตัวแปร sesMemberID ด้วยฟังก์ชั่น setMemberID
                            // ที่อยู่ใน controller หลัก
                            $scope.$parent.setMemberID(resultSet.rows.item(0).member_id);
                            // ขึ้นแจ้ง เข้าสู่ระบบสำเร็จ และเปลี่ยนไปหน้า profile ด้วย $stat.go()
                            $cordovaDialogs.alert('เข้าสู่ระบบสำเร็จ', 'เข้าสู่ระบบ', 'ตกลง')
                                .then(function() {
                                    $state.go('app.profile');
                                });
                        }else{
                            // ไม่พบข้อมูล ขึ้นแจ้งเตือนว่ามีข้อผิดพลาด
                            $cordovaDialogs.alert('ชื่อหรือรหัสผ่านไม่ถูกต้อง', 'เกิดข้อผิดพลาด', 'ตกลง')
                                .then(function() {
                                    return false;
                                });
                            return false;
                        }
                    },
                    function (tx, error) {
                        // ข้อความแจ้ง SELECT error
                        $scope.showToast('SELECT error: ' + error.message,'long','bottom');
                    });
            }, function(error) {
                $scope.showToast('transaction error: ' + error.message,'long','bottom');
            }, function() {
                $scope.showToast('transaction ok','long','bottom');
            });

        });
    };


    ionicMaterialInk.displayEffect();
})
 
    สังเกตว่าใน LoginCtrl มีการเรียกใช้งาน $ionicPlatform, $cordovaDialogs, $cordovaToast, $state เพิ่มเข้ามา โดย
$ionicPlatform สำหรับตรวจสอบความพร้อมของอุปกรณ์ก่อนเรียกใช้ plugin
$cordovaDialogs สำหรับ alert แจ้งเตือนข้อความ
$cordovaToast  สำหรับข้อความแจ้งสถานะ
$state สำหรับเชื่อมโยงหรือลิ้งค์ไปหน้าต่างๆ 
 
 

    RegistCtrl ส่วนของ controller หน้า register

    การทำงานหน้านี้คือ เริ่มกำหนดตัวแปรค่าว่างให้กับข้อมูลในฟอร์มสมัครสมาชิก
ทำการตรวจสอบการกรอกข้อมูลในฟอร์มก่อนส่งข้อมูล ทำการส่งข้อมูลไปบันทึกใน db
จากนั้นลิ้งค์ไปยังหน้าโพรไฟล์
 
.controller('RegistCtrl', function($scope, $timeout, $stateParams, ionicMaterialInk
,$ionicPlatform, $cordovaDialogs, $cordovaToast, $state) {
    $scope.$parent.clearFabs();
    $scope.$parent.hasShadow();
    $timeout(function() {
        $scope.$parent.hideHeader();
    }, 0);

    // กำนหดค่าเริ่มต้นของฟอร์มหน้าสมัครสมาชิก ให้เป็นค่าว่าง
    $scope.reg = {
        input_user:'',
        input_pass:'',
        input_phone:''
    };

    // เรียกใช้ฟังก์ชั่นจาก AppCtrl หลัก เพื่อซ่อนเมนูสมาชิก โดยส่งค่า false เข้าไป
    $scope.$parent.setMemberMenu(false);

    // สร้างฟังก์ชั่นสำหรับเรียกใช้ Toast plugin
    $scope.showToast = function(str, duration, position){
        $ionicPlatform.ready(function() { // เตรียมก่อนเรียกใช้ plugin
            return $cordovaToast
                .show(str, duration, position)
                .then(function(success) {
                    // success
                }, function (error) {
                    // error
                });
        });
    };

    var db = null;

    $ionicPlatform.ready(function() { // เตรียมก่อนเรียกใช้ plugin
        // ตรวจสอบและทำการเชื่อมต่อกับ db
        db = window.sqlitePlugin.openDatabase({
            name: 'my.db', location: 'default'
        }, function (db) {
            // เชื่อมต่อ db สำเร็จ
            $scope.showToast('Open DB Success','long','bottom');

            // เริ่มทำงานของคำสั่ง db
            db.transaction(function (tx) {
                // เวลามีการทดสอบและเพิ่มฟิลด์หรือแก้ไขตารางควรเปิดคอมเม้นการลบตารางก่อน
                // นั้นหมายถึงตารางจะถูกสร้างและจัดรูปแบบใหม่ทุกครั้ง ข้อมูลตารางจะรีเซ็ต ล้างค่า
                // พอว่างโครงสร้างตารางเรียบร้อยแล้ว ให้ปิดคอมเม้นไว้เหมือนเดิม เพื่อให้ข้อมูลยังคงอยู่
 //               tx.executeSql('DROP TABLE IF EXISTS member');
                // ทำคำสั่งสร้างตาราง member ถ้ายังไม่มี
                tx.executeSql('' +
                    'CREATE TABLE IF NOT EXISTS member ' +
                    '(member_id integer primary key,' +
                    'member_user text, ' +
                    'member_pass text,' +
                    'member_phone text)' +
                    '');
            }, function (error) {
                $scope.showToast('transaction error: ' + error.message,'long','bottom');
            }, function () {
                $scope.showToast('transaction ok','long','bottom');
            });
        }, function (error) {
            $scope.showToast('Open database ERROR: ' + JSON.stringify(error),'long','bottom');
        });
    });

    // สร้างฟังก์ชั่นสำหรับสมัครสมาชิก รับค่า data จาก object reg
    $scope.registerMember = function(data){
        $ionicPlatform.ready(function() { // เตรียมก่อนเรียกใช้ plugin
            // ตรวจสอบให้กรอกข้อมูลให้ครบถ้วน
            if(data.input_user=="" || data.input_pass=="" || data.input_phone==""){
                $cordovaDialogs.alert('โปรดกรอกข้อมูลให้ครบถ้วน', 'ข้อมูลจำเป็น', 'ตกลง')
                    .then(function() {
                        return false;
                    });
                return false;
            }

            // แสดงข้อมูลที่ส่งเข้ามา เวลาใช้จริงสามารถลบออกได้
            $scope.showToast(' user: ' + data.input_user +
                ' pass: ' + data.input_pass +
                ' phone: ' + data.input_phone +'', 'long', 'bottom');


            // เริ่มทำงานของคำสั่ง db
            db.transaction(function (tx) {
                // จัดรูปแบบคำสั่ง sql สำหรับบันทึกข้อมูล ค่าที่รับมาบันทึกจะแทนด้วย ?
                // ในที่นี้จะส่งมาแค่ 3 ค่า ส่วน member_id นั้นเป็น PRIMARY KEY เป็น auto incremet
                // โดยอัตโนมัติ
                var query = "INSERT INTO member" +
                    " (member_user, member_pass, member_phone) " +
                    " VALUES (?,?,?)";

                // ทำงานคำสั่ง sql
                tx.executeSql(query, [data.input_user, data.input_pass,
                    data.input_phone], function(tx, res) {
                        // เมื่อทำการบันทึกข้อมูลสำเร็จ
                        // ใช้ค่า member_id จาก res.insertId แล้วกำหนดค่าด้วยฟังก์ชั่น setMemberID()
                        $scope.$parent.setMemberID(res.insertId);
                        $scope.showToast('insertId: ' + res.insertId + ' -- probably 1','long','bottom');
                        $scope.showToast('rowsAffected: ' + res.rowsAffected + ' -- should be 1','long','bottom');
                        // แจ้งการสมัครสมาชิกสำเร็จ และให้ไปที่หน้า profile
                        $cordovaDialogs.alert('ทำการสัมครสมัครสมาชิกเรียบร้อยแล้ว', 'สมัครสมาชิกใหม่', 'ตกลง')
                            .then(function() {
                                $state.go('app.profile');
                            });
                    },
                    function(tx, error) {
                        $scope.showToast('INSERT error: ' + error.message,'long','bottom');
                    });
            }, function(error) {
                $scope.showToast('transaction error: ' + error.message,'long','bottom');
            }, function() {
                $scope.showToast('transaction ok','long','bottom');
            });

        });
    };

    ionicMaterialInk.displayEffect();
})
 
 

    ProfileCtrl ส่วนของ controller หน้า profile

    การทำงานของหน้านี้ทำการเชื่อมกับ db แล้วไปดึงข้อมูลของสมาชิกที่ทำการล็อกอิน
สำหรับ หรือสมาชิกที่สมัครสมาชิกมาแสดง โดยอิงการดึงค่าจาก member_id ที่มีการเก็บ
ค่าไว้ใน $scope ที่อยู่ใน controller หลัก เมื่อพบข้อมูลก็นำมากำหนดในตัวแปร แล้วเรียกใช้
งานในหน้าโพรไฟล์อีกที
 
.controller('ProfileCtrl', function(
    $scope, $stateParams, $timeout,
    ionicMaterialMotion, ionicMaterialInk, $ionicPlatform,
    $cordovaSpinnerDialog, $cordovaToast) {

    $scope.$parent.showHeader();
    $scope.$parent.clearFabs();
    $scope.isExpanded = false;
    $scope.$parent.setExpanded(false);
    $scope.$parent.setHeaderFab(false);
    $scope.$parent.hasShadow();


    // กำหนดตัวแปรสำหรับค่าเริ่มต้นของข้อมูลหน้าโพรไลฟ์
    $scope.member_user = '';
    $scope.member_phone = '';

    // กำหนดให้แสดงเมนูสมาชิก
    $scope.$parent.setMemberMenu(true);

    // สร้างฟังก์ชั่นสำหรับเรียกใช้ Toast plugin
    $scope.showToast = function(str, duration, position){
        $ionicPlatform.ready(function() { // เตรียมก่อนเรียกใช้ plugin
            return $cordovaToast
                .show(str, duration, position)
                .then(function(success) {
                    // success
                }, function (error) {
                    // error
                });
        });
    };

    var db = null;

    $ionicPlatform.ready(function() { // เตรียมก่อนเรียกใช้ plugin
        // ตรวจสอบและทำการเชื่อมต่อกับ db
        db = window.sqlitePlugin.openDatabase({
            name: 'my.db', location: 'default'
        }, function (db) {
            // เชื่อมต่อ db สำเร็จ
            $scope.showToast('Open DB Success','long','bottom');

            // แสดง spin dialog plugin จะใช้หรือไม่ก็ได้ นำมาใช้เผื่อใครไปประยุกต์เพิ่ม
            $cordovaSpinnerDialog.show(null,"รอสักครู่..กำลังโหลดข้อมูล"); // แสดง loading
            $timeout(function() { // ซ่อนอัตโนมัติใน 300 มิลลิวินาที
                $cordovaSpinnerDialog.hide();
            }, 300);

            // เริ่มทำงานของคำสั่ง db
            db.transaction(function (tx) {
                // เตรียมคำสั่ง sql ดึงข้อมูลสมาชิกอิงจาก member_id ที่เราเก็บไว้ใน $scope หลัก
                var query = "SELECT member_user, member_phone" +
                    " FROM member WHERE member_id = ?";
                // ดึงค่า member_id จาก $scope หลักด้วยฟังก์ชั่น getMemberID()
                var id_member = $scope.$parent.getMemberID();

                // ทำคำสั่ง sql ดึงข้อมูลตาม member_id
                tx.executeSql(query, [id_member], function (tx, resultSet) {
                        // ถ้าพบข้อมูล
                        if(resultSet.rows.length){
                            // นำข้อมูลที่ได้ มาไว้ในตัวแปร member_user และ member_phone
                            // เพื่อแสดงในหน้า profile
                            $scope.member_user = resultSet.rows.item(0).member_user;
                            $scope.member_phone = resultSet.rows.item(0).member_phone;
                        }
                        $cordovaSpinnerDialog.hide();
                    },
                    function (tx, error) {
                        $scope.showToast('SELECT error: ' + error.message,'long','bottom');
                    });
            }, function (error) {
                $scope.showToast('transaction error: ' + error.message,'long','bottom');
            }, function () {
                $scope.showToast('transaction ok','long','bottom');
            });
        }, function (error) {
            $scope.showToast('Open database ERROR: ' + JSON.stringify(error),'long','bottom');
        });
    });



    $timeout(function() {
        ionicMaterialMotion.slideUp({
            selector: '.slide-up'
        });
    }, 300);

    $timeout(function() {
        ionicMaterialMotion.fadeSlideInRight({
            startVelocity: 3000
        });
    }, 700);

    ionicMaterialInk.displayEffect();
})
 
 

3. ทำการ build apk ไฟล์แล้วนำไปทดสอบติดตั้งบนมือถือ 

 
 ด้วยคำสั่ง 
 
C:\phonegap\learn001>phonegap build android  
 
ให้ทำการทดสอบติดตั้งในมือถือ android ของเรา จะได้หน้าตา app ประมาณนี้
ในตัวอย่างมีส่วนของแผนที่ด้วย หากต้องการเห็นผลแผนที่ด้วย
 อาจจะต้อง รีสตาร์ทเครื่องหนึ่งครั้ง
 
      


      
 
 

ดาวน์โหลดไฟล์ apk ตัวอย่างได้ที่ https://goo.gl/RysBtB


 

Tags:: ionicframework ionicmaterial

เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน และลงชื่อติดตาม


สมัครสมาชิกได้ที่        ล็อกอินได้ที่   





URL สำหรับอ้างอิง