Thử làm chương trình quản lý điểm mini bằng PHP, Javascript và Json

Mục tiêu của chương trình là tạo ra một TABLE thể hiện các cột điểm bộ môn của một danh sách học sinh, chương trình này phải đảm bảo một số yêu cầu sau đây:

  1. Có thể tương tác được mà không cần dùng form để submit, có thể dễ dàng thao tác bằng các phím Tab, Enter và các phím điều hướng.
  2. Điểm trung bình học kì được cập nhật tức thì nếu các cột điểm được nhập đầy đủ và hợp lệ.
  3. Có thể linh động về số cột điểm thường xuyên, điểm trung bình học kì được tính theo số cột điểm này.
  4. Có thể sort theo tất cả các cột.
  5. Toàn bộ dữ liệu được lưu tự động mà không cần phải bấm nút.

Trước khi tìm giải pháp cho các yêu cầu trên, ta cần thiết kế front-end cho trang web, trong đó chỉ đơn giản bao gồm một TABLE để thể hiện bảng điểm.

Thiết kế front-end

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Bảng điểm</title>
    <link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
    <table id=ScoreList>
        <thead>
            <th>STT</th>
            <th>Họ và tên</th>
            <th>Lớp</th>
            <th>Điểm 1.1</th>
            <th>Điểm 1.2</th>
            <th>Điểm 1.3</th>
            <th>Điểm 1.4</th>
            <th>Điểm 1.5</th>
            <th>Điểm 1.6</th>
            <th>GHK 1</th>
            <th>CHK 1</th>
            <th>TB 1</th>
        </thead>
        <tbody>
        <!-- Điểm của mỗi HS được thể hiện bằng một dòng tại đây -->
        </tbody>
    </table>
</body>
</html>

Vì số cột điểm thường xuyên ở mỗi bộ môn, mỗi trường được quy định khác nhau, thường là từ 3 đến 6 cột. Do đó, tôi để hẳn 6 cột, từ 1.1 đến 1.6 để giáo viên sử dụng một cách linh hoạt theo nhu cầu. Tiếp theo là file style.css để “trang trí” cho bảng điểm.

table {
    border-collapse: collapse;
    width: 100%;
}
tr:nth-child(even) {
    background: #F0FFF0;
}
tr:hover {
    background: #ADFF2F;
}
td, th {
    border: 1px solid #000;
    padding: 5px;
    text-align: center;
    vertical-align: middle;
}
tr td:nth-child(2) {
    text-align: justify;
    text-transform: capitalize;
}
th {
    background: #DCDCDC;
}
td {
    vertical-align: top;
}

Hầu hết các chức năng theo yêu cầu đã nêu đều có thể giải quyết bằng Javascript, tuy nhiên ngôn ngữ này chỉ đọc được file dữ liệu chứ không thể update dữ liệu trong file (trừ khi dùng Nodejs). Vậy nên tôi quyết định sử dụng ngôn ngữ PHP để tương tác với dữ liệu. Trong bài viết này, tôi không dùng MySQL mà sẽ thử dùng Json để thay thế, nếu sau này có cơ hội mở rộng quy mô chương trình này thì sẽ chuyển đổi qua MySQL hoặc Firebase Realtime Database.

Rồi, bây giờ ta sẽ thêm một đoạn code PHP vào khu vực <tbody></tbody>. Đoạn code này sẽ đọc nội dung từ file data.json, nếu có dữ liệu thì sẽ in ra thành từng dòng, ngược lại thì ta in ra một số dòng trống để người dùng nhập liệu.

<?
        $json_data = file_get_contents("data.json");
        if(!$json_data){
            for($i=1;$i<=200;$i++){
                echo "
                <tr>
                    <td>".$i."</td>
                    <td class='item_editable item".$i."_fullname' contenteditable='true' id='".$i."-1' ref=".$i."></td>
                    <td class='item_editable item".$i."_classname' contenteditable='true' id='".$i."-2' ref=".$i."></td>
                    <td class='item_editable item".$i."_score1a' contenteditable='true' id='".$i."-3' ref=".$i."></td>
                    <td class='item_editable item".$i."_score1b' contenteditable='true' id='".$i."-4' ref=".$i."></td>
                    <td class='item_editable item".$i."_score1c' contenteditable='true' id='".$i."-5' ref=".$i."></td>
                    <td class='item_editable item".$i."_score1d' contenteditable='true' id='".$i."-6' ref=".$i."></td>
                    <td class='item_editable item".$i."_score1e' contenteditable='true' id='".$i."-7' ref=".$i."></td>
                    <td class='item_editable item".$i."_score1f' contenteditable='true' id='".$i."-8' ref=".$i."></td>
                    <td class='item_editable item".$i."_score2' contenteditable='true' id='".$i."-9' ref=".$i."></td>
                    <td class='item_editable item".$i."_score3' contenteditable='true' id='".$i."-10' ref=".$i."></td>
                    <td class='item_editable item".$i."_score4' ref=".$i."></td>
                </tr>";
            }
        }
        $student_list = json_decode($json_data,true);
        if(count($student_list)!=0){
            foreach($student_list as $student){
                echo "
        <tr>
            <td>".$student['stt']."</td>
            <td class='item_editable item".$student['stt']."_fullname' contenteditable='true' id='".$student['stt']."-1' ref=".$student['stt'].">".trim($student['fullname'])."</td>
            <td class='item_editable item".$student['stt']."_classname' contenteditable='true' id='".$student['stt']."-2' ref=".$student['stt'].">".$student['classname']."</td>
            <td class='item_editable item".$student['stt']."_score1a' contenteditable='true' id='".$student['stt']."-3' ref=".$student['stt'].">".$student['score1a']."</td>
            <td class='item_editable item".$student['stt']."_score1b' contenteditable='true' id='".$student['stt']."-4' ref=".$student['stt'].">".$student['score1b']."</td>
            <td class='item_editable item".$student['stt']."_score1c' contenteditable='true' id='".$student['stt']."-5' ref=".$student['stt'].">".$student['score1c']."</td>
            <td class='item_editable item".$student['stt']."_score1d' contenteditable='true' id='".$student['stt']."-6' ref=".$student['stt'].">".$student['score1d']."</td>
            <td class='item_editable item".$student['stt']."_score1e' contenteditable='true' id='".$student['stt']."-7' ref=".$student['stt'].">".$student['score1e']."</td>
            <td class='item_editable item".$student['stt']."_score1f' contenteditable='true' id='".$student['stt']."-8' ref=".$student['stt'].">".$student['score1f']."</td>
            <td class='item_editable item".$student['stt']."_score2' contenteditable='true' id='".$student['stt']."-9' ref=".$student['stt'].">".$student['score2']."</td>
            <td class='item_editable item".$student['stt']."_score3' contenteditable='true' id='".$student['stt']."-10' ref=".$student['stt'].">".$student['score3']."</td>
            <td class='item_editable item".$student['stt']."_score4' ref=".$student['stt'].">".$student['score4']."</td>
        </tr>";
            }
        }
?>

Cần lưu ý rằng, mỗi cell trong bảng (ngoại trừ cột STT và TB 1) đều có tùy chọn  contenteditable=’true’ , điều này giúp cho mỗi cell có thêm tính năng chỉnh sửa như một textbox. Thuộc tính  id  của mỗi cell có dạng  m-n  dùng để khai báo địa chỉ dòng thứ  m  và cột thứ  n  của cell, điều này giúp chúng ta có thể tương tác qua lại giữa các cell.

Cấu trúc của file data.json như sau:

Từ trường  score1a  đến  score1f  là số điểm thường xuyên, cột nào không có điểm thì không cần nhập. Trường  score2  là số điểm kiểm tra giữa kì (hệ số 2),  score3  là điểm kiểm tra cuối kì (hệ số 3),  score4  là điểm trung bình cả học kì.

Tính năng sắp xếp trong bảng

Hiện nay, có một số plugin hỗ trợ việc sắp xếp (sort) bảng hết sức tiện lợi và hầu hết đều miễn phí, điển hình là plugin DataTables. Chúng ta có thể download nguồn về và tích hợp vào dự án web của mình hoặc kết nối trực tiếp theo link của nhà cung cấp.

Ta thêm phần khai báo sau vào phần <head></head> của trang web:

<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.5/css/jquery.dataTables.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.13.5/js/jquery.dataTables.js"></script>

Và thêm đoạn code Javascript sau vào bên dưới của TABLE:

<script language=javascript>
        $(document).ready( function () {
            $('#ScoreList').DataTable({
                paging: false
            });
        });
</script>

Trong đó, ScoreList id  của TABLE mà tôi đã đặt ban đầu, mời bạn xem lại đoạn code tạo bảng đã nêu ở trên để thấy phần khai báo này nhe. Tùy chọn  paging: false  để bảng không chia thành nhiều trang. Bây giờ thì bảng điểm của chúng ta đã có chức năng sort theo cột, hết sức tiện lợi, đáp ứng được yêu cầu số 4.

Tiếp theo đây, ta sẽ viết lệnh Javascript để xử lý 3 yêu cầu 1, 2 và 3.

Chế tính năng tương tác như Excel

Vì code Javascript khá dài, chúng ta nên viết riêng vào một file script.js và khai báo như sau:

<script src="script.js"></script>

Ngoài ra, trong chương trình này tôi sử dụng thư viện jQuery để thực hiện rất nhiều chức năng, có thể khai báo ở đầu trang web bằng dòng code sau:

<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>

Chức năng di chuyển bằng Tab và các phím điều hướng

Để dùng những phím này thao tác với các cell, ta dùng jQuery để nhận diện hành động bấm phím (keydown):

$(document).on('keydown', '.item_editable', function (e) {
    var item_address = $(this).attr('id').split('-');

    if (e.keyCode == '9') {
        //Tab key -> Enter key
        //if(item_address>1 || $("#"+(parseInt(item_address[0])+1)+"-"+item_address[1]).length){
        if (item_address[1] > 1 || $("#" + (parseInt(item_address[0]) + 1) + "-" + item_address[1]).length) {
            $(this).trigger(
                jQuery.Event('keypress', { keyCode: 13 })
            );
        }
    }

    //Get position of cursor in text
    var sel = document.getSelection();
    sel.modify("extend", "backward", "paragraphboundary");
    var pos = sel.toString().length;
    if (sel.anchorNode != undefined) sel.collapseToEnd();

    if (e.keyCode == '37' && pos == 0) {
        //Left arrow
        if (item_address[1] > 1) {
            $("#" + item_address[0] + "-" + (parseInt(item_address[1]) - 1)).focus();
        }
    }
    if (e.keyCode == '38') {
        //Up arrow
        if (item_address[0] > 1) {
            $("#" + (item_address[0] - 1) + "-" + item_address[1]).focus();
        }
    }
    if (e.keyCode == '39' && pos == $(this).html().length) {
        //Right arrow
        if (item_address[1] < 10) {
            $("#" + item_address[0] + "-" + (parseInt(item_address[1]) + 1)).focus();
        }
    }
    if (e.keyCode == '40') {
        //Down arrow
        if ($("#" + (parseInt(item_address[0]) + 1) + "-" + item_address[1]).length) {
            $("#" + (parseInt(item_address[0]) + 1) + "-" + item_address[1]).focus();
        }
    }
});

Các keyCode 9, 37, 38, 39, 40 lần lượt là phím Tab, Left, Up, Right, Down. Đây là các phím di chuyển phổ biến, tương tự như trong Excel. Để nhận biết vị trí và phạm vi di chuyển tại mỗi cell, chúng ta dựa vào thuộc tính  id  như đã đề cập ở trên. Tuy nhiên, khi bấm phím Left và Right thì cũng cần xem xét vị trí của con trỏ trong cell, nếu đang ở đầu của string thì có thể di chuyển sang cell bên trái, nếu đang ở cuối string thì có thể di chuyển sang cell bên phải, ngược lại thì di chuyển bình thường giữa các kí tự trong string. Trong Excel, ta phải bấm nút F2 mới có thể di chuyển trong phạm vi cell. Tiếp theo, ta sẽ thực hiện công việc tính toán với điểm số và sao lưu dữ liệu.

Tính điểm trung bình và sao lưu dữ liệu

Ngoại trừ 5 phím chức năng nêu trên, phải nhận dạng bằng  keydown , hầu hết các phím còn lại có thể nhận dạng bằng  keypress . Nhìn chung thì hai anh này đều có vai trò như nhau, nhưng jQuery không chịu gộp lại thì mình đành tùy cơ ứng biến vậy.

$(document).on('keypress', '.item_editable', function (e) {
    var item_id = $(this).attr('ref');
    var item_class = $(this).attr('class');
    var item_address = $(this).attr('id').split('-');

    var item_fullname = $(".item" + item_id + "_fullname").html();
    var item_classname = $(".item" + item_id + "_classname").html();
    var item_score1a = $(".item" + item_id + "_score1a").html();
    var item_score1b = $(".item" + item_id + "_score1b").html();
    var item_score1c = $(".item" + item_id + "_score1c").html();
    var item_score1d = $(".item" + item_id + "_score1d").html();
    var item_score1e = $(".item" + item_id + "_score1e").html();
    var item_score1f = $(".item" + item_id + "_score1f").html();
    var item_score2 = $(".item" + item_id + "_score2").html();
    var item_score3 = $(".item" + item_id + "_score3").html();
    var item_score4 = $(".item" + item_id + "_score4").html();
    var item_score_legal = 1;
    var item_score_list = [];
    var item_score_count = 0;
    var item_score_sum = 0;

    if (e.keyCode == '13') {
        //Các lệnh xử lí điểm khi bấm phím Enter

        return false;
    }
});

Khi ta bấm phím trong phạm vi một cell, hệ thống sẽ bắt đầu phân tích các thông tin của cả dòng, để khi điểm số có thay đổi thì điểm trung bình ở cuối dùng cũng sẽ thay đổi theo. Bây giờ tôi sẽ phân tích từng khối lệnh khi phím Enter được bấm.

        if (item_class == 'item_editable item' + item_id + '_fullname' || item_class == 'item_editable item' + item_id + '_classname') {
            //Nếu đang ở ô fullname hoặc ô classname
        }else{
            //Nếu đang ở các ô điểm
        }

Mỗi dòng trong bảng đều có 10 cell có thể tương tác được, bao gồm 2 cell Họ và tên (fullname) và Lớp (classname) cùng với 8 cell điểm số. Đối với 2 cell thông tin cá nhân, ta chủ yếu kiểm tra xem đó có phải dòng cuối cùng không, nếu là dòng cuối cùng thì khi Enter sẽ tự động sinh thêm một dòng nữa, để có thể nhập thêm học sinh (nếu cần). Đối với 8 cell điểm số, ta cần kiểm tra xem dữ liệu nhập vào có hợp lệ không, nếu có thì  push  vào một mảng (array) chứa các cột điểm, nếu cột điểm hệ số k thì ta push điểm đó k lần vào mảng. Tùy vào tổng số cột điểm của mỗi dòng, ta lấy tổng số điểm chia cho tổng số cột điểm, như vậy điểm trung bình sẽ không phụ thuộc vào số cột điểm thường xuyên của môn học nữa. Sau đây là code hoàn chỉnh cho câu lệnh rẻ nhánh còn trống phía trên:

        if (item_class == 'item_editable item' + item_id + '_fullname' || item_class == 'item_editable item' + item_id + '_classname') {
            if (item_fullname && item_class != 'item_editable item' + item_id + '_classname' && !$("#" + (parseInt(item_address[0]) + 1) + "-" + item_address[1]).length) {
                var newRowid = parseInt(item_address[0]) + 1;
                var newRow = "\
        <tr>\
            <td>"+ newRowid + "</td>\
            <td class='item_editable item"+ newRowid + "_fullname' contenteditable='true' id='" + newRowid + "-1' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_classname' contenteditable='true' id='" + newRowid + "-2' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_score1a' contenteditable='true' id='" + newRowid + "-3' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_score1b' contenteditable='true' id='" + newRowid + "-4' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_score1c' contenteditable='true' id='" + newRowid + "-5' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_score1d' contenteditable='true' id='" + newRowid + "-6' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_score1e' contenteditable='true' id='" + newRowid + "-7' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_score1f' contenteditable='true' id='" + newRowid + "-8' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_score2' contenteditable='true' id='" + newRowid + "-9' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_score3' contenteditable='true' id='" + newRowid + "-10' ref=" + newRowid + "></td>\
            <td class='item_editable item"+ newRowid + "_score4' ref=" + newRowid + "></td>\
        </tr>";
                $("#ScoreList").find('tbody').append(newRow);
                $("#" + newRowid + "-" + item_address[1]).focus();
            }
        }else{
            if (!isNaN(item_score1a) && item_score1a >= 0 && item_score1a <= 10) {
                item_score_list[0] = item_score1a;
            } else {
                alert('illegal!');
                item_score_legal = 0;
            }
            if (!isNaN(item_score1b) && item_score1b >= 0 && item_score1b <= 10) {
                item_score_list[1] = item_score1b;
            } else {
                alert('illegal!');
                item_score_legal = 0;
            }
            if (!isNaN(item_score1c) && item_score1c >= 0 && item_score1c <= 10) {
                item_score_list[2] = item_score1c;
            } else {
                alert('illegal!');
                item_score_legal = 0;
            }
            if (!isNaN(item_score1d) && item_score1d >= 0 && item_score1d <= 10) {
                item_score_list[3] = item_score1d;
            } else {
                alert('illegal!');
                item_score_legal = 0;
            }
            if (!isNaN(item_score1e) && item_score1e >= 0 && item_score1e <= 10) {
                item_score_list[4] = item_score1e;
            } else {
                alert('illegal!');
                item_score_legal = 0;
            }
            if (!isNaN(item_score1f) && item_score1f >= 0 && item_score1f <= 10) {
                item_score_list[5] = item_score1f;
            } else {
                alert('illegal!');
                item_score_legal = 0;
            }
            if (!isNaN(item_score2) && item_score2 >= 0 && item_score2 <= 10) {
                item_score_list[6] = item_score2;
                item_score_list[7] = item_score2;
            } else {
                alert('illegal!');
                item_score_legal = 0;
            }
            if (!isNaN(item_score3) && item_score3 >= 0 && item_score3 <= 10) {
                item_score_list[8] = item_score3;
                item_score_list[9] = item_score3;
                item_score_list[10] = item_score3;
            } else {
                alert('illegal!');
                item_score_legal = 0;
            }
            if (item_score_legal == 0) {
                item_score4 = "";
                $(".item" + item_id + "_score4").html(item_score4);
            }
            if (item_score3 && item_score_legal == 1) {
                item_score_count = item_score_list.length;
                for (i = 0; i < item_score_list.length; i++) {
                    if (item_score_list[i].length > 0) {
                        item_score_sum = item_score_sum + parseFloat(item_score_list[i]);
                    } else {
                        item_score_count--;
                    }
                }
                item_score4 = parseFloat(item_score_sum / item_score_count).toFixed(1);
                $(".item" + item_id + "_score4").html(item_score4);
            }
        }

Ngoài ra, khi bấm phím Enter, ta còn phải thực hiện một hành động quan trong nữa, đó là sao lưu dữ liệu. Bằng Javascript, ta có thể tạo một array và lần lượt thêm tất cả các dòng của bảng vào như là một phần tử của array đó. Cuối cùng, ta ghép các phần tử này thành một string hoàn chỉnh và gửi đi xử lí thông qua phương thức  ajax .

        var lastSTT = $(".item_editable:last").attr("ref");
        var score_list_json = "[";
        var stt = 0;
        for (j = 1; j <= lastSTT; j++) {
            item_fullname = $(".item" + j + "_fullname").html();
            item_classname = $(".item" + j + "_classname").html();
            item_score1a = $(".item" + j + "_score1a").html();
            item_score1b = $(".item" + j + "_score1b").html();
            item_score1c = $(".item" + j + "_score1c").html();
            item_score1d = $(".item" + j + "_score1d").html();
            item_score1e = $(".item" + j + "_score1e").html();
            item_score1f = $(".item" + j + "_score1f").html();
            item_score2 = $(".item" + j + "_score2").html();
            item_score3 = $(".item" + j + "_score3").html();
            item_score4 = $(".item" + j + "_score4").html();
            if (item_fullname) {
                stt++;
                if (stt > 1) {
                    score_list_json = score_list_json + ",";
                }
                score_list_json = score_list_json + '{"stt": ' + stt + ', "fullname": "' + item_fullname + '", "classname": "' + item_classname + '", "score1a": "' + item_score1a + '", "score1b": "' + item_score1b + '", "score1c": "' + item_score1c + '", "score1d": "' + item_score1d + '", "score1e": "' + item_score1e + '", "score1f": "' + item_score1f + '", "score2": "' + item_score2 + '", "score3": "' + item_score3 + '", "score4": "' + item_score4 + '"}';
            }
        }
        score_list_json = score_list_json + "]";

        $.ajax({
            type: "POST",
            url: "http://localhost/lab/table/ajax_update.php",
            data: 'data=' + score_list_json,
            cache: false,
            success: function (html_post) {
                //alert(html_post);
            }
        });

Như đã nói ở trên, Javascript có thể đọc và xử lý Json từ file text, nhưng không thể cập nhật nội dung của file text đó, cho nên ta buộc phải dùng php để thực hiện công việc này. Nội dung của file ajax_update.php cũng không quá phức tạp, chủ yếu là xử lí một số vấn đề lặt vặt liên quan đến string trong quá trình post bằng ajax.

<?
$data = strip_tags($_POST['data']);
$data = str_replace("\n","",$data);
$data = str_replace("\\","",$data);
$data = str_replace("  "," ",$data);

$fp = fopen('data.json', 'w');
fwrite($fp, $data);
fclose($fp);
?>

Vì phiên bản PHP mà tôi đang sử dụng là phiên bản 5.2.6 thời nhà Tống nên các lệnh không được gọn gàng và tối ưu nhất, cũng may là nó chạy được. Bạn đọc có thể tải mã nguồn đầy đủ của chương trình tại địa chi Github dưới đây:

https://github.com/huynhphusi/ScoreTable

Bình luận

Chia sẻ