Hướng dẫn tạo Web App tra cứu sản phẩm bằng Google Sheets và Apps Script từ A–Z
Tìm kiếm mã sản phẩm thông minh không cần cài phần mềm: Bài viết hướng dẫn cách dùng Google Sheets làm cơ sở dữ liệu, Google Apps Script làm phần xử lý, HTML/JavaScript xây dựng giao diện tra cứu trên trình duyệt. Hệ thống có thể tìm theo mã, tên hàng, tự gợi ý các biến thể như EB309, EB-309, EB -309, EB - 309, EB 309… đồng thời tự co độ rộng cột cho gọn bảng.
Phù hợp: cửa hàng, kho hàng, kế toán nội bộ hoặc người muốn tạo trang tra cứu danh mục không cần mua phần mềm. Đây là ứng dụng tra cứu chỉ đọc: việc thêm, sửa sản phẩm được thực hiện trong Google Sheets.
1. Kết quả cần đạt được
- Tìm ngay khi gõ trong cột B (Mã SP) và C (Tên sản phẩm), không phân biệt chữ hoa/thường.
- Với mã kiểu
EB309, ưu tiên trùng liền mạch; nếu không có, tìm các biến thể dấu gạch và khoảng trắng trước, sau đó là kết quả cóEBhoặc309. - Với từ khóa thông thường, chấm điểm tương tự công thức LET trong Excel: trùng mã được điểm cao, tiếp đến chứa cụm, đủ từ và khớp từng từ.
- Ẩn B, D, E, F trên giao diện nhưng vẫn tìm theo cột B.
- Bảng có tiêu đề cố định, xen kẽ trắng/xanh nhạt, kẻ viền ô có dữ liệu, phân trang 50 dòng.
- Tự đo độ dài chữ trong tiêu đề và 50 dòng đang xem để co độ rộng từng cột, có thể cuộn ngang trên điện thoại.
2. Chuẩn bị Google Sheets đúng cấu trúc
Tạo một Google Sheets mới, đổi tên trang tính thành DanhMuc. Trong ví dụ này, dòng 5 là tiêu đề, dòng 6 bắt đầu dữ liệu. Hàng hóa nằm trong phạm vi cột A đến BZ và tối đa đến dòng 5000.
| Cột A | Cột B | Cột C | Cột D–F | Cột G |
|---|---|---|---|---|
| STT | Mã SP | Tên sản phẩm | Thông tin nội bộ | Tồn đầu |
| 1 | EB-309 | Sản phẩm EB 309 | ... | 6 |
| 2 | EB - 309 | Sản phẩm EB khác | ... | 4 |
Lấy ID bảng tính: Mở Google Sheets, trong đường dẫn dạng https://docs.google.com/spreadsheets/d/ID_BANG_TINH/edit, sao chép chuỗi nằm giữa /d/ và /edit. Chuỗi đó sẽ được dán vào Code.gs ở bước tiếp theo.
3. Tạo dự án Apps Script và file Code.gs
Từ Google Sheets, vào Tiện ích mở rộng (Extensions) → Apps Script. Đặt tên dự án là “Tra cứu danh mục”. Mở file Code.gs, xóa nội dung mẫu rồi dán toàn bộ đoạn mã sau:
/**
* WEB APP TRA CUU DANH MUC - LET + GOI Y + AUTO FIT
* Nguon: DanhMuc, tieu de dong 5, du lieu dong 6-5000, cot A:BZ.
* Tim trong B (Ma SP) va C (Ten san pham).
* Khong hien thi B, D, E, F tren giao dien.
*/
const TRA_CUU = {
SPREADSHEET_ID: '1GU5LRQqn9QNzuZ8_-FDgj9imVXXlmG0bxw_RTPES0Qc',
SHEET_NAME: 'DanhMuc',
HEADER_ROW: 5,
FIRST_DATA_ROW: 6,
LAST_ROW: 5000,
LAST_COL: 78, // BZ
VERSION: 'LET 4.1 - COT B C NGANG NHAU'
};
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('Tra cứu danh mục hàng hóa')
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function layDanhMuc() {
const ss = SpreadsheetApp.openById(TRA_CUU.SPREADSHEET_ID);
const sh = ss.getSheetByName(TRA_CUU.SHEET_NAME);
if (!sh) throw new Error('Không tìm thấy sheet DanhMuc.');
const soCot = Math.min(TRA_CUU.LAST_COL, sh.getMaxColumns());
if (soCot < 3) throw new Error('DanhMuc cần có cột A, B và C.');
const tieuDeGoc = sh.getRange(TRA_CUU.HEADER_ROW, 1, 1, soCot)
.getDisplayValues()[0];
const dongCuoi = Math.min(TRA_CUU.LAST_ROW, sh.getLastRow());
const mangGoc = dongCuoi >= TRA_CUU.FIRST_DATA_ROW
? sh.getRange(TRA_CUU.FIRST_DATA_ROW, 1,
dongCuoi - TRA_CUU.FIRST_DATA_ROW + 1, soCot).getDisplayValues()
: [];
// Khi co tu khoa: tim ca B va C. Khi tu khoa trong: phia giao dien chi hien dong co B.
const dong = mangGoc.filter(h =>
String(h[1] ?? '').trim() !== '' || String(h[2] ?? '').trim() !== ''
);
// Chi so mang tu 0: B=1, D=3, E=4, F=5.
const cotAn = new Set([1, 3, 4, 5]);
const cotGoc = [];
for (let c = 0; c < soCot; c++) {
if (cotAn.has(c)) continue;
const coTieuDe = String(tieuDeGoc[c] ?? '').trim() !== '';
const coDuLieu = dong.some(h => String(h[c] ?? '').trim() !== '');
if (coTieuDe || coDuLieu) cotGoc.push(c);
}
return {
phienBan: TRA_CUU.VERSION,
tieuDe: cotGoc.map(c => String(tieuDeGoc[c] ?? '')),
cotGoc: cotGoc,
duLieu: dong.map(h => cotGoc.map(c => String(h[c] ?? ''))),
maSP: dong.map(h => String(h[1] ?? '')),
tenSP: dong.map(h => String(h[2] ?? '')),
soDongNguon: mangGoc.length
};
}
function kiemTraKetNoi() {
const data = layDanhMuc();
Logger.log('Phiên bản: ' + data.phienBan);
Logger.log('Số dòng nguồn: ' + data.soDongNguon);
Logger.log('Số dòng có mã hoặc tên: ' + data.duLieu.length);
Logger.log('Số cột hiển thị: ' + data.tieuDe.length);
}
// Dung de kiem tra vi sao ma EB-309 khong xuat hien.
function kiemTraEB309() {
const data = layDanhMuc();
const chuanHoa = s => String(s ?? '').toLowerCase()
.normalize('NFC').replace(/[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/g, '-')
.replace(/[\u00a0\u2007\u202f]/g, ' ').trim();
let soDong = 0;
for (let i = 0; i < data.maSP.length; i++) {
if (chuanHoa(data.maSP[i] + ' ' + data.tenSP[i]).includes('eb-309')) {
Logger.log('Dòng phù hợp: mã=' + data.maSP[i] + '; tên=' + data.tenSP[i]);
soDong++;
}
}
Logger.log('Tìm thấy ' + soDong + ' dòng chứa EB-309 trong B/C (đến dòng 5000).');
}
Bắt buộc: Thay DAN_ID_GOOGLE_SHEETS_CUA_BAN_VAO_DAY bằng ID bảng tính của bạn, giữ nguyên hai dấu nháy đơn bao quanh ID. Nếu sheet không tên DanhMuc, hãy sửa SHEET_NAME.
Giải thích: doGet() hiển thị giao diện; layDanhMuc() đọc dữ liệu và chỉ trả về những cột cần hiển thị cộng với mã/tên dùng để tìm; kiemTraKetNoi() giúp kiểm tra kết nối.
4. Tạo Index.html để làm giao diện và thuật toán tìm kiếm
Trong trình soạn Apps Script, nhấn dấu + cạnh “Tệp” → chọn HTML → đặt tên chính xác Index (Apps Script tự tạo file Index.html). Dán toàn bộ mã giao diện bên dưới:
<!DOCTYPE html>
<html lang="vi">
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tra cứu danh mục hàng hóa</title>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; }
body { padding: clamp(4px, 1vw, 16px); background: #f3f7fb; color: #26394b; font-family: Arial, sans-serif; }
main { width: 100%; padding: clamp(8px, 1.25vw, 18px); background: #fff; border-radius: 10px; }
h2 { color: #176b45; font-size: clamp(17px, 2vw, 24px); text-align: center; margin: 4px 0 16px; }
.congcu { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
#tuKhoa { flex: 1 1 260px; min-width: 0; padding: 11px 12px; font-size: 16px; border: 2px solid #168c59; border-radius: 7px; outline: none; }
#tuKhoa:focus { border-color: #087dcb; }
button { padding: 9px 12px; border: 1px solid #aac5d8; background: #eaf4ff; border-radius: 6px; cursor: pointer; }
button:disabled { opacity: .48; cursor: default; }
#thongBao { min-height: 19px; margin: 9px 0; color: #536579; font-size: 13px; line-height: 1.5; }
.bang { width: 100%; max-width: 100%; max-height: 72vh; overflow: auto; }
table { width: max-content; min-width: 0; table-layout: fixed; border-collapse: collapse; font-size: 13px; }
th, td { padding: 6px 8px; line-height: 1.35; white-space: normal; overflow-wrap: anywhere; vertical-align: middle; }
th { position: sticky; top: 0; z-index: 1; color: #173c5a; text-align: left; background: #d5e9f7; border: 1px solid #8baabd; }
td.coDuLieu { border: 1px solid #8baabd; }
tr.trang td.coDuLieu { background: #fff; }
tr.xanh td.coDuLieu { background: #d9f1ff; }
.phanTrang { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 10px; padding: 14px 0 3px; }
#soTrang { font-size: 12px; color: #65758a; }
#phienBan { font-size: 12px; color: #087dcb; text-decoration: none; font-weight: 600; }
#phienBan:hover { text-decoration: underline; color: #005a96; }
@media (max-width: 700px) {
body { padding: 4px; }
main { padding: 8px; }
#tuKhoa { flex-basis: 100%; }
th, td { padding: 5px 6px; font-size: 12px; }
.bang { max-height: 69vh; }
}
</style>
</head>
<body>
<main>
<h2>TRA CỨU DANH MỤC HÀNG HÓA</h2>
<div class="congcu">
<input type="search" id="tuKhoa" placeholder="Tìm theo cột B hoặc C (ưu tiên ngang nhau)..." autocomplete="off" aria-label="Nhập từ khóa tìm kiếm">
<button type="button" id="xoa">Xóa</button>
<button type="button" id="taiLai">Tải lại dữ liệu</button>
</div>
<div id="thongBao" role="status">Đang tải dữ liệu...</div>
<div class="bang" id="khungBang">
<table id="bangDuLieu">
<colgroup id="cotBang"></colgroup>
<thead id="dongTieuDe"></thead>
<tbody id="dongKetQua"></tbody>
</table>
</div>
<div class="phanTrang">
<button type="button" id="truoc">Trước</button>
<span id="soTrang">Trang 1/1</span>
<button type="button" id="sau">Sau</button>
<a id="phienBan" href="https://tienichvietnam.blogspot.com/"
target="_blank" rel="noopener noreferrer">EDIT LH: dvtuan@hotmail.com</a>
</div>
</main>
<script>
const $ = id => document.getElementById(id);
const SO_DONG_MOT_TRANG = 50;
let tieuDe = [], duLieu = [], maSP = [], tenSP = [], cotGoc = [];
let ketQua = [], trang = 1, daTai = false, trangThaiTim = '';
// Giữ dấu tiếng Việt như công thức LET. Chuẩn hóa một số loại dấu gạch nối.
function chuanHoa(value) {
return String(value ?? '').toLowerCase().normalize('NFC')
.replace(/[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/g, '-')
.replace(/[\u00a0\u2007\u202f]/g, ' ')
.replace(/[\u200b-\u200d\ufeff]/g, '')
.trim().replace(/\s+/g, ' ');
}
function boDauPhanCach(value) {
return chuanHoa(value).replace(/[\s\-_]+/g, '');
}
// Hai cot B va C co diem nhu nhau. Trung trong ca hai cot khong duoc cong doi.
// Trung nguyen o: +10000; chua nguyen cum: +3000;
// du tat ca tu trong B/C: +1000; moi tu tim thay: +100.
function diemLET(ma, ten, q, words) {
const b = chuanHoa(ma);
const c = chuanHoa(ten);
const txt = b + ' ' + c;
let soTu = 0;
for (const word of words) if (txt.includes(word)) soTu++;
if (soTu === 0) return 0;
const exact = Number(b === q || c === q);
const phrase = Number(b.includes(q) || c.includes(q));
const duTu = Number(soTu === words.length);
return exact * 10000 + phrase * 3000 + duTu * 1000 + soTu * 100;
}
// Chuẩn hóa để nhận được EB-309, EB – 309, EB -309...
// Khác với chuanHoa(): hàm này GIỮ số lượng khoảng trắng.
function chuanHoaGiuKhoangTrang(value) {
return String(value ?? '').toLowerCase().normalize('NFC')
.replace(/[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/g, '-')
.replace(/[\u00a0\u2007\u202f]/g, ' ')
.replace(/[\u200b-\u200d\ufeff]/g, '');
}
function thoatRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// EB309 => EB + 309; cả EB-309/EB 309 cũng nhận dạng được.
function tachMa(q) {
const m = q.match(/^([\p{L}]+)[\s\-_]*(\d+)$/u);
return m ? { chu: m[1], so: m[2] } : null;
}
// Trả về số ký tự trống trước / sau gạch nối ở kết quả tốt nhất.
function timBienThe(str, regex, loai) {
regex.lastIndex = 0;
let match;
let best = null;
while ((match = regex.exec(str)) !== null) {
const truoc = match[1].length;
const sau = loai === 'gach' ? match[2].length : 0;
// Nhóm có dấu gạch: EB-309 -> EB -309 -> EB - 309 -> ...
// Các cách viết chỉ có khoảng trắng bên phải dấu '-' cũng được tìm.
let phu;
if (loai === 'gach') {
phu = truoc === 0 && sau === 0 ? 0 :
truoc >= 1 ? 1 + truoc * 10000 + sau : 200000 + sau;
} else {
phu = truoc;
}
if (best === null || phu < best) best = phu;
}
return best;
}
// B va C ngang nhau trong tat ca cap uu tien, khong co san pham trung.
// Kết quả được chia thành 5 cấp ưu tiên, không có sản phẩm trùng.
// 0: EB309 đúng liền mạch
// 1: EB-309 và mọi biến thể dấu gạch, khoảng trắng
// 2: EB 309, EB 309, EB 309...
// 3: Chứa EB
// 4: Chứa 309
function timMaLinhHoat(q, chu, so) {
const maLien = chu + so;
const regexGach = new RegExp(
thoatRegex(chu) + '(\\s*)-(\\s*)' + thoatRegex(so), 'g'
);
const regexCach = new RegExp(
thoatRegex(chu) + '(\\s+)' + thoatRegex(so), 'g'
);
const cacNhom = [[], [], [], [], []];
for (let i = 0; i < duLieu.length; i++) {
const b = chuanHoaGiuKhoangTrang(maSP[i]);
const c = chuanHoaGiuKhoangTrang(tenSP[i]);
// Khớp nguyên chuỗi mã liên tục, không cần trùng cả ô.
const lienB = b.includes(maLien);
const lienC = c.includes(maLien);
if (lienB || lienC) {
cacNhom[0].push({ i, nhom: 'Đúng mã', phu: (b === maLien || c === maLien) ? 0 : 1 });
continue;
}
const gachB = timBienThe(b, regexGach, 'gach');
const gachC = timBienThe(c, regexGach, 'gach');
if (gachB !== null || gachC !== null) {
cacNhom[1].push({
i, nhom: 'Có dấu gạch nối',
phu: Math.min(gachB ?? Infinity, gachC ?? Infinity)
});
continue;
}
const cachB = timBienThe(b, regexCach, 'cach');
const cachC = timBienThe(c, regexCach, 'cach');
if (cachB !== null || cachC !== null) {
cacNhom[2].push({ i, nhom: 'Có khoảng trắng', phu: Math.min(cachB ?? Infinity, cachC ?? Infinity) });
continue;
}
if (b.includes(chu) || c.includes(chu)) {
cacNhom[3].push({ i, nhom: 'Chứa ' + chu.toUpperCase(), phu: 0 });
continue;
}
if (b.includes(so) || c.includes(so)) {
cacNhom[4].push({ i, nhom: 'Chứa ' + so, phu: 0 });
}
}
// Nếu thực sự có mã EB309 liền mạch: chỉ hiển thị kết quả trực tiếp.
if (cacNhom[0].length) {
cacNhom[0].sort((a, b) => a.phu - b.phu || a.i - b.i);
return { rows: cacNhom[0], message: 'Đã tìm được mã ' + maLien.toUpperCase() + '.' };
}
for (const nhom of cacNhom) nhom.sort((a, b) => a.phu - b.phu || a.i - b.i);
const thongKe = [
'Có dấu gạch nối: ' + cacNhom[1].length,
'Có khoảng trắng: ' + cacNhom[2].length,
chu.toUpperCase() + ': ' + cacNhom[3].length,
so + ': ' + cacNhom[4].length
];
return {
rows: [...cacNhom[1], ...cacNhom[2], ...cacNhom[3], ...cacNhom[4]],
message: 'Không có ' + maLien.toUpperCase() + ' liền mạch. Gợi ý theo thứ tự: ' + thongKe.join(' → ') + '.'
};
}
function timKiem() {
if (!daTai) return;
const q = chuanHoa($('tuKhoa').value);
ketQua = [];
if (!q) {
for (let i = 0; i < duLieu.length; i++) {
if (String(maSP[i] ?? '').trim()) ketQua.push({ i, diem: 0 });
}
trangThaiTim = 'Đang hiển thị toàn bộ danh mục có Mã SP.';
} else {
const ma = tachMa(q);
if (ma) {
const kq = timMaLinhHoat(q, ma.chu, ma.so);
ketQua = kq.rows;
trangThaiTim = kq.message;
} else {
// Từ khóa thông thường: giữ nguyên chấm điểm Excel LET.
const words = q.split(' ').filter(Boolean);
for (let i = 0; i < duLieu.length; i++) {
const diem = diemLET(maSP[i], tenSP[i], q, words);
if (diem > 0) ketQua.push({ i, diem });
}
ketQua.sort((a, b) => b.diem - a.diem || a.i - b.i);
trangThaiTim = 'Kết quả theo mức độ khớp LET trong cột B và C.';
}
}
trang = 1;
hienThiKetQua();
}
function hienThiTieuDe() {
const tr = document.createElement('tr');
for (const name of tieuDe) {
const th = document.createElement('th');
th.textContent = name;
tr.appendChild(th);
}
$('dongTieuDe').replaceChildren(tr);
}
// Đo độ dài chữ thực tế trong tiêu đề và 50 dòng đang hiển thị.
function tuCoDoRongCot() {
if (!tieuDe.length) return;
const nho = window.innerWidth <= 700;
const fontSize = nho ? 12 : 13;
const them = nho ? 16 : 21;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
const start = (trang - 1) * SO_DONG_MOT_TRANG;
const rowsOnPage = ketQua.slice(start, start + SO_DONG_MOT_TRANG);
const widths = tieuDe.map((title, col) => {
const cotNguon = cotGoc[col];
let min = nho ? 56 : 65;
let max = nho ? 170 : 235;
if (cotNguon === 0) { min = 44; max = 64; }
if (cotNguon === 2) { min = nho ? 130 : 160; max = nho ? 275 : 425; }
if (cotNguon === 6) { min = 70; max = 118; }
ctx.font = 'bold ' + fontSize + 'px Arial';
let width = ctx.measureText(String(title ?? '')).width + them;
ctx.font = fontSize + 'px Arial';
for (const item of rowsOnPage) {
const val = String(duLieu[item.i]?.[col] ?? '');
if (val) width = Math.max(width, ctx.measureText(val).width + them);
}
return Math.ceil(Math.max(min, Math.min(max, width)));
});
const group = $('cotBang');
group.replaceChildren();
let total = 0;
for (const width of widths) {
const col = document.createElement('col');
col.style.width = width + 'px';
group.appendChild(col);
total += width;
}
const table = $('bangDuLieu');
table.style.width = total + 'px';
table.style.minWidth = '0';
}
function hienThiKetQua() {
const total = ketQua.length;
const maxTrang = Math.max(1, Math.ceil(total / SO_DONG_MOT_TRANG));
trang = Math.max(1, Math.min(trang, maxTrang));
const start = (trang - 1) * SO_DONG_MOT_TRANG;
const current = ketQua.slice(start, start + SO_DONG_MOT_TRANG);
const fragment = document.createDocumentFragment();
current.forEach((item, pos) => {
const tr = document.createElement('tr');
tr.className = (start + pos) % 2 === 0 ? 'trang' : 'xanh';
tieuDe.forEach((_, c) => {
const td = document.createElement('td');
const value = String(duLieu[item.i]?.[c] ?? '');
td.textContent = value;
if (value.trim()) td.className = 'coDuLieu';
tr.appendChild(td);
});
fragment.appendChild(tr);
});
$('dongKetQua').replaceChildren(fragment);
$('thongBao').textContent = trangThaiTim + ' ' + total + ' sản phẩm.';
$('soTrang').textContent = 'Trang ' + trang + '/' + maxTrang;
$('truoc').disabled = trang <= 1;
$('sau').disabled = trang >= maxTrang;
tuCoDoRongCot();
}
function taiDuLieu() {
daTai = false;
$('taiLai').disabled = true;
$('thongBao').textContent = 'Đang tải dữ liệu...';
google.script.run
.withSuccessHandler(data => {
tieuDe = data.tieuDe || [];
duLieu = data.duLieu || [];
maSP = data.maSP || [];
tenSP = data.tenSP || [];
cotGoc = data.cotGoc || [];
daTai = true;
hienThiTieuDe();
timKiem();
$('taiLai').disabled = false;
})
.withFailureHandler(err => {
$('thongBao').textContent = 'Lỗi đọc DanhMuc: ' + err.message;
$('taiLai').disabled = false;
})
.layDanhMuc();
}
$('tuKhoa').addEventListener('input', timKiem);
$('xoa').addEventListener('click', () => {
$('tuKhoa').value = '';
timKiem();
$('tuKhoa').focus();
});
$('taiLai').addEventListener('click', taiDuLieu);
$('truoc').addEventListener('click', () => {
if (trang > 1) { trang--; hienThiKetQua(); }
});
$('sau').addEventListener('click', () => {
if (trang * SO_DONG_MOT_TRANG < ketQua.length) { trang++; hienThiKetQua(); }
});
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(tuCoDoRongCot, 100);
});
taiDuLieu();
</script>
</body>
</html>
File giao diện gồm CSS để trình bày bảng và JavaScript để tìm kiếm. Mỗi khi nhập từ khóa, hàm timKiem() lọc sản phẩm; timMaLinhHoat() phân nhóm mã có dấu gạch, dấu cách; tuCoDoRongCot() đo độ dài chữ và co cột theo 50 dòng trên trang. Kết quả chỉ có trong phạm vi dữ liệu Web App đọc được.
5. Hiểu cách xếp hạng kết quả EB309
Giả sử người dùng nhập eb309. Nếu có mã liền mạch EB309 trong mã hoặc tên sản phẩm, ứng dụng ưu tiên kết quả đó. Nếu không có, lần lượt tìm:
EB-309(không có dấu cách);EB -309,EB - 309,EB - 309,EB - 309…;EB -309hoặc những trường hợp có nhiều dấu cách trước dấu gạch;EB 309,EB 309,EB 309…;- Sản phẩm chỉ chứa
EB; - Sản phẩm chứa
309mà chưa xuất hiện ở những nhóm trên.
Sản phẩm khớp nhiều nhóm chỉ xuất hiện một lần, tại nhóm có độ ưu tiên cao nhất. Dấu gạch Unicode được chuẩn hóa thành dấu “-” trước khi so sánh. Những mã chỉ gần giống là gợi ý, không khẳng định đó là cùng một mã hàng.
6. Chạy thử và cấp quyền
- Trong Apps Script, nhấn Lưu (Save).
- Chọn hàm
kiemTraKetNoiở thanh trên → Chạy (Run). - Nếu Google yêu cầu, đăng nhập và cấp các quyền cần thiết để script đọc bảng tính.
- Xem Nhật ký thực thi (Execution log) để xác nhận số dòng và số cột được đọc.
- Nếu lỗi “Không tìm thấy sheet DanhMuc”, kiểm tra tên sheet; nếu lỗi “You do not have permission”, kiểm tra quyền truy cập bảng tính của tài khoản triển khai.
7. Triển khai Web App và lấy link truy cập
- Chọn Triển khai (Deploy) → Tùy chọn triển khai mới (New deployment).
- Chọn loại Ứng dụng web (Web app).
- Ở Thực thi với tư cách (Execute as), chọn Tôi (Me) nếu muốn ứng dụng đọc bảng tính bằng quyền của chủ dự án.
- Ở Ai có quyền truy cập (Who has access), chọn đúng nhóm người dùng; chỉ chọn Bất kỳ ai (Anyone) nếu nội dung được phép công khai.
- Nhấn Triển khai (Deploy), cấp quyền nếu được hỏi và sao chép URL kết thúc bằng
/exec.
Lần sau khi sửa code, đừng tạo thêm Web App mới nếu không cần. Hãy vào Triển khai → Quản lý lượt triển khai (Manage deployments) → chọn bản đang dùng → bút chì → Phiên bản mới → Triển khai để giữ nguyên đường link.
8. Có thể nhúng Web App vào Blogger không?
Có thể, nếu Web App cho phép hiển thị trong iframe. Cách dễ nhất và ít rủi ro hơn là đặt một liên kết mở trực tiếp Web App trong bài Blogger. Nếu cần nhúng khung, bạn có thể bổ sung .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) trong chuỗi trả về của doGet(), rồi triển khai phiên bản mới.
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('Tra cứu danh mục hàng hóa')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
Trong Blogger, chuyển trình soạn thảo sang Chế độ xem HTML và chèn mã sau, thay URL mẫu bằng đường dẫn /exec của chính bạn:
<iframe src="https://script.google.com/macros/s/URL_WEB_APP_CUA_BAN/exec" style="width:100%;height:760px;border:0" loading="lazy" title="Tra cứu sản phẩm"></iframe>
Cảnh báo bảo mật: ALLOWALL cho phép website bất kỳ nhúng Web App và làm tăng rủi ro clickjacking; không nên dùng với ứng dụng có thao tác nhạy cảm. Hãy chỉ công khai Web App nếu dữ liệu được phép chia sẻ. Cột bị ẩn trên giao diện không đồng nghĩa với được bảo mật: mã SP ở cột B vẫn được gửi tới trình duyệt để phục vụ tìm kiếm.
9. Những lỗi thường gặp và cách xử lý
- “File HTML with name Index was not found”: chưa có file HTML tên
Indexhoặc tên viết sai chữ hoa/thường. - Không tìm ra EB309: kiểm tra mã có nằm trong B/C, từ dòng 6 đến 5000, đúng sheet và đúng file nguồn không. Nhấn “Tải lại dữ liệu” sau khi sửa bảng.
- Sửa code nhưng trang không đổi: đã lưu nhưng chưa chọn phiên bản mới cho bản triển khai
/exec. - Bảng quá rộng: vuốt ngang trên điện thoại; các cột chỉ co theo 50 dòng đang hiển thị, với giới hạn độ rộng và xuống dòng khi cần.
- Tải chậm: với dữ liệu lớn, hãy giảm
LAST_ROW/LAST_COL, hoặc tối ưu bằng tìm kiếm phía máy chủ và bộ nhớ đệm. - Người khác bị yêu cầu quyền truy cập: kiểm tra “Execute as” và “Who has access” trong bản triển khai.
10. Kết luận
Với một bảng Google Sheets và hai file Code.gs, Index.html, bạn có thể tự xây dựng trang Web App tra cứu hàng hóa: tìm theo mã hoặc tên sản phẩm, ưu tiên mã khớp, gợi ý mã gần giống khi viết khác dấu gạch/khác khoảng trắng, tự co cột và dùng được trên điện thoại. Bạn có thể tùy chỉnh vùng dữ liệu, màu sắc và quy tắc tìm kiếm theo danh mục thực tế.
Tài liệu tham khảo chính thức
- Ví dụ Tra giá sản phẩm (Link thư mục driver)
- Google Apps Script – Web Apps
- google.script.run – gọi dữ liệu từ HTML
- Quản lý bản triển khai Apps Script
- Quy tắc nhúng iframe và ALLOWALL
Từ khóa: tạo Web App Google Sheets, hướng dẫn Google Apps Script, tra cứu mã sản phẩm, tìm kiếm EB309, tạo trang tra cứu hàng hóa, Apps Script HTML, Blogger.
.png)
.png)