Show mt-comments data as table and export to Excel
// ==UserScript==
// @name Comments Table + Export Excel
// @namespace https://nguyengiathieu.lms.vnedu.vn/*
// @version 1.0
// @description Show mt-comments data as table and export to Excel
// @author You
// @match https://nguyengiathieu.lms.vnedu.vn/*
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
const PANEL_ID = 'tm-comment-table-box';
const FLOATING_BTN_ID = 'tm-show-comments-btn';
function normalizeText(text) {
return String(text || '')
.replace(/\s+/g, ' ')
.replace(/ /g, ' ')
.trim()
.toUpperCase();
}
function findPortletByCaption(captionText) {
const portlets = document.querySelectorAll('.portlet');
for (const portlet of portlets) {
const caption = portlet.querySelector('.portlet-title .caption');
if (!caption) continue;
const captionContent = normalizeText(caption.innerText);
if (captionContent.includes(normalizeText(captionText))) {
return portlet;
}
}
return null;
}
function pad2(number) {
return String(number).padStart(2, '0');
}
function formatDateTime(date) {
return `${pad2(date.getDate())}/${pad2(date.getMonth() + 1)}/${date.getFullYear()} ${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
}
function convertRelativeTimeToDateTime(relativeTime) {
const now = new Date();
const text = String(relativeTime || '').toLowerCase().trim();
const resultDate = new Date(now);
if (
text.includes('vừa xong') ||
text.includes('just now') ||
text.includes('now')
) {
return formatDateTime(resultDate);
}
const numberMatch = text.match(/\d+/);
const value = numberMatch ? parseInt(numberMatch[0], 10) : 0;
if (text.includes('phút')) {
resultDate.setMinutes(resultDate.getMinutes() - value);
} else if (text.includes('giờ')) {
resultDate.setHours(resultDate.getHours() - value);
} else if (text.includes('ngày')) {
resultDate.setDate(resultDate.getDate() - value);
} else if (text.includes('tuần')) {
resultDate.setDate(resultDate.getDate() - value * 7);
} else if (text.includes('tháng')) {
resultDate.setMonth(resultDate.getMonth() - value);
} else if (text.includes('năm')) {
resultDate.setFullYear(resultDate.getFullYear() - value);
} else {
return relativeTime;
}
return formatDateTime(resultDate);
}
function extractComments() {
const targetPortlet = findPortletByCaption('Ý KIẾN KHÁC');
if (!targetPortlet) {
alert('Không tìm thấy block có caption: Ý KIẾN KHÁC');
return [];
}
const comments = targetPortlet.querySelectorAll('.portlet-body .mt-comments .mt-comment');
return Array.from(comments).map((item, index) => {
const author = item.querySelector('.mt-comment-author')?.innerText.trim() || '';
const rawDate = item.querySelector('.mt-comment-date')?.innerText.trim() || '';
const calculatedDate = convertRelativeTimeToDateTime(rawDate);
const text = item.querySelector('.mt-comment-text')?.innerText.trim() || '';
return {
STT: index + 1,
MaPhuHuynh: author,
ThoiGian: calculatedDate,
YKien: text
};
});
}
function injectStyle() {
if (document.getElementById('tm-ykk-style')) return;
const style = document.createElement('style');
style.id = 'tm-ykk-style';
style.innerHTML = `
#${PANEL_ID} {
position: fixed;
top: 72px;
right: 24px;
width: 820px;
max-width: calc(100vw - 48px);
max-height: 82vh;
background: #ffffff;
border-radius: 14px;
z-index: 999999;
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.22);
font-family: Arial, Roboto, "Segoe UI", sans-serif;
overflow: hidden;
color: #1f2937;
border: 1px solid #dbe3ef;
}
.tm-ykk-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 14px 16px;
background: linear-gradient(135deg, #1e3a8a, #2563eb);
color: #ffffff;
cursor: pointer;
user-select: none;
}
.tm-ykk-title-wrap {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.tm-ykk-icon {
width: 34px;
height: 34px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.18);
font-size: 18px;
flex-shrink: 0;
}
.tm-ykk-title {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.tm-ykk-title strong {
font-size: 15px;
font-weight: 700;
letter-spacing: 0.2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tm-ykk-title span {
font-size: 12px;
color: rgba(255, 255, 255, 0.82);
}
.tm-ykk-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.tm-ykk-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 34px;
height: 26px;
padding: 0 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
color: #ffffff;
font-size: 12px;
font-weight: 700;
}
.tm-ykk-btn {
border: none;
border-radius: 9px;
padding: 8px 12px;
cursor: pointer;
font-size: 12px;
font-weight: 700;
transition: 0.15s ease;
color: #ffffff;
font-family: Arial, Roboto, "Segoe UI", sans-serif;
}
.tm-ykk-btn:hover {
filter: brightness(1.08);
}
.tm-ykk-btn-download {
background: #16a34a;
}
.tm-ykk-btn-close {
background: #ef4444;
}
.tm-ykk-collapse-indicator {
width: 28px;
height: 28px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.16);
font-size: 14px;
font-weight: 700;
}
.tm-ykk-body {
background: #f8fafc;
padding: 14px;
}
.tm-ykk-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
gap: 12px;
}
.tm-ykk-note {
font-size: 12px;
color: #64748b;
}
.tm-ykk-search {
width: 280px;
max-width: 100%;
border: 1px solid #cbd5e1;
background: #ffffff;
border-radius: 10px;
padding: 9px 11px;
font-size: 13px;
outline: none;
font-family: Arial, Roboto, "Segoe UI", sans-serif;
}
.tm-ykk-search:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
}
.tm-ykk-table-wrap {
max-height: calc(82vh - 150px);
overflow: auto;
background: #ffffff;
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.tm-ykk-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 13px;
font-family: Arial, Roboto, "Segoe UI", sans-serif;
}
.tm-ykk-table thead th {
position: sticky;
top: 0;
z-index: 2;
background: #eaf1ff;
color: #1e293b;
font-weight: 700;
text-align: left;
padding: 11px 10px;
border-bottom: 1px solid #cbd5e1;
white-space: nowrap;
}
.tm-ykk-table tbody td {
padding: 10px;
border-bottom: 1px solid #e5e7eb;
vertical-align: top;
color: #334155;
}
.tm-ykk-table tbody tr:nth-child(even) {
background: #f9fafb;
}
.tm-ykk-table tbody tr:hover {
background: #eff6ff;
}
.tm-ykk-table tbody tr:last-child td {
border-bottom: none;
}
.tm-ykk-center {
text-align: center;
}
.tm-ykk-code {
font-weight: 700;
color: #1d4ed8;
white-space: nowrap;
}
.tm-ykk-time {
white-space: nowrap;
color: #475569;
}
.tm-ykk-text {
white-space: pre-wrap;
line-height: 1.5;
min-width: 300px;
color: #1f2937;
}
#${FLOATING_BTN_ID} {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 999999;
background: linear-gradient(135deg, #2563eb, #1e3a8a);
color: white;
border: none;
padding: 12px 16px;
border-radius: 999px;
cursor: pointer;
font-size: 14px;
font-weight: 700;
font-family: Arial, Roboto, "Segoe UI", sans-serif;
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.35);
transition: 0.15s ease;
}
#${FLOATING_BTN_ID}:hover {
transform: translateY(-2px);
box-shadow: 0 16px 34px rgba(37, 99, 235, 0.42);
}
@media (max-width: 768px) {
#${PANEL_ID} {
top: 20px;
right: 12px;
left: 12px;
width: auto;
max-width: none;
}
.tm-ykk-header {
align-items: flex-start;
}
.tm-ykk-actions {
flex-wrap: wrap;
justify-content: flex-end;
}
.tm-ykk-toolbar {
flex-direction: column;
align-items: stretch;
}
.tm-ykk-search {
width: 100%;
}
}
`;
document.head.appendChild(style);
}
function hideFloatingButton() {
const btn = document.getElementById(FLOATING_BTN_ID);
if (btn) {
btn.style.display = 'none';
}
}
function showFloatingButton() {
const btn = document.getElementById(FLOATING_BTN_ID);
if (btn) {
btn.style.display = 'block';
}
}
function closePanelToButton() {
const panel = document.getElementById(PANEL_ID);
if (panel) {
panel.remove();
}
showFloatingButton();
}
function createUI(data) {
injectStyle();
const oldBox = document.getElementById(PANEL_ID);
if (oldBox) oldBox.remove();
hideFloatingButton();
const box = document.createElement('div');
box.id = PANEL_ID;
box.innerHTML = `
<div class="tm-ykk-header" id="tm-ykk-header">
<div class="tm-ykk-title-wrap">
<div class="tm-ykk-icon">💬</div>
<div class="tm-ykk-title">
<strong>Ý KIẾN KHÁC</strong>
<span>Click vào thanh này để thu gọn về nút ban đầu</span>
</div>
</div>
<div class="tm-ykk-actions">
<span class="tm-ykk-badge">${data.length}</span>
<button class="tm-ykk-btn tm-ykk-btn-download" id="tm-download-excel" title="Tải Excel">
Tải Excel
</button>
<button class="tm-ykk-btn tm-ykk-btn-close" id="tm-close-table" title="Đóng">
Đóng
</button>
<span class="tm-ykk-collapse-indicator">▼</span>
</div>
</div>
<div class="tm-ykk-body">
<div class="tm-ykk-toolbar">
<div class="tm-ykk-note">
Chỉ hiển thị dữ liệu trong block <b>Ý KIẾN KHÁC</b>.
</div>
<input
id="tm-ykk-search"
class="tm-ykk-search"
type="text"
placeholder="Tìm theo mã PH hoặc nội dung..."
/>
</div>
<div class="tm-ykk-table-wrap">
<table class="tm-ykk-table">
<thead>
<tr>
<th style="width:52px;">STT</th>
<th style="width:120px;">Mã PH</th>
<th style="width:155px;">Thời gian</th>
<th>Ý kiến</th>
</tr>
</thead>
<tbody id="tm-ykk-table-body">
${renderRows(data)}
</tbody>
</table>
</div>
</div>
`;
document.body.appendChild(box);
const header = document.getElementById('tm-ykk-header');
const closeBtn = document.getElementById('tm-close-table');
const downloadBtn = document.getElementById('tm-download-excel');
const searchInput = document.getElementById('tm-ykk-search');
header.addEventListener('click', () => {
closePanelToButton();
});
closeBtn.addEventListener('click', (event) => {
event.stopPropagation();
closePanelToButton();
});
downloadBtn.addEventListener('click', (event) => {
event.stopPropagation();
downloadExcel(data);
});
searchInput.addEventListener('click', (event) => {
event.stopPropagation();
});
searchInput.addEventListener('input', () => {
const keyword = normalizeText(searchInput.value);
const filteredData = data.filter(row => {
return (
normalizeText(row.MaPhuHuynh).includes(keyword) ||
normalizeText(row.ThoiGian).includes(keyword) ||
normalizeText(row.YKien).includes(keyword)
);
});
document.getElementById('tm-ykk-table-body').innerHTML = renderRows(filteredData);
});
}
function renderRows(data) {
if (!data.length) {
return `
<tr>
<td colspan="4" class="tm-ykk-center" style="padding:18px;color:#64748b;">
Không tìm thấy dữ liệu phù hợp.
</td>
</tr>
`;
}
return data.map(row => `
<tr>
<td class="tm-ykk-center">${escapeHtml(row.STT)}</td>
<td class="tm-ykk-code">${escapeHtml(row.MaPhuHuynh)}</td>
<td class="tm-ykk-time">${escapeHtml(row.ThoiGian)}</td>
<td class="tm-ykk-text">${escapeHtml(row.YKien)}</td>
</tr>
`).join('');
}
function downloadExcel(data) {
const html = `
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<table border="1">
<thead>
<tr>
<th>STT</th>
<th>Mã phụ huynh</th>
<th>Thời gian</th>
<th>Ý kiến</th>
</tr>
</thead>
<tbody>
${data.map(row => `
<tr>
<td>${escapeHtml(row.STT)}</td>
<td>${escapeHtml(row.MaPhuHuynh)}</td>
<td>${escapeHtml(row.ThoiGian)}</td>
<td>${escapeHtml(row.YKien)}</td>
</tr>
`).join('')}
</tbody>
</table>
</body>
</html>
`;
const fileName = `y_kien_khac_${new Date().toISOString().slice(0, 10)}.xls`;
const a = document.createElement('a');
a.href = 'data:application/vnd.ms-excel;charset=utf-8,' + encodeURIComponent(html);
a.download = fileName;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function addFloatingButton() {
injectStyle();
if (document.getElementById(FLOATING_BTN_ID)) return;
const btn = document.createElement('button');
btn.id = FLOATING_BTN_ID;
btn.innerText = '💬 Show Ý KIẾN KHÁC';
btn.addEventListener('click', () => {
const data = extractComments();
if (data.length === 0) {
alert('Không có dữ liệu trong block Ý KIẾN KHÁC.');
return;
}
createUI(data);
});
document.body.appendChild(btn);
}
window.addEventListener('load', () => {
setTimeout(addFloatingButton, 1000);
});
})();