CVNSS commited on
Commit
c609be2
·
verified ·
1 Parent(s): 8d21bab

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +340 -86
index.html CHANGED
@@ -2,142 +2,396 @@
2
  <html lang="vi">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <title>TỪ ĐIỂN TIẾNG VIỆT – HOÀNG PHÊ</title>
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
-
8
- <!-- PWA -->
9
- <link rel="manifest" href="./manifest.webmanifest" />
10
  <meta name="theme-color" content="#7a0000" />
11
 
12
  <style>
13
- body {
14
- margin: 0;
15
- font-family: "Times New Roman", serif;
16
- background: #fffaf2;
17
  }
18
-
19
- header {
20
- background: #7a0000;
21
- color: gold;
22
- padding: 15px;
23
- text-align: center;
24
- font-size: 20px;
25
- font-weight: bold;
26
  }
 
 
27
 
28
- main {
29
- max-width: 900px;
30
- margin: auto;
31
- padding: 20px;
32
- }
33
 
34
- input {
35
- width: 100%;
36
- padding: 12px;
37
- font-size: 16px;
38
- border: 2px solid #7a0000;
39
  }
40
 
41
- .entry {
42
- padding: 10px;
43
- border-bottom: 1px solid #ddd;
 
 
44
  }
45
-
46
- .word {
47
- font-size: 20px;
48
- color: #7a0000;
49
- font-weight: bold;
50
  }
 
 
 
51
 
52
- .pos {
53
- font-style: italic;
54
- color: #666;
55
- margin-left: 8px;
 
56
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- .meaning {
59
- margin-left: 12px;
 
60
  }
 
61
  </style>
62
  </head>
63
 
64
  <body>
65
- <header>TỪ ĐIỂN TIẾNG VIỆT – HOÀNG PHÊ</header>
 
 
 
66
 
67
  <main>
68
- <input id="searchInput" placeholder="Nhập từ cần tra (không dấu cũng được)" />
69
- <div id="stats">Đang tải dữ liệu…</div>
70
- <div id="results"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </main>
72
 
73
- <script>
74
- let dictionary = [];
 
75
 
 
 
 
 
76
  function normalizeVN(s) {
77
- return s.toLowerCase()
 
78
  .replace(/đ/g, "d")
79
  .normalize("NFD")
80
- .replace(/[\u0300-\u036f]/g, "");
 
 
 
81
  }
82
 
83
- fetch("./tu_dien_hoang_phe_clean.json")
84
- .then(res => res.json())
85
- .then(data => {
86
- dictionary = data.muc_tu;
87
- document.getElementById("stats").innerText =
88
- "Tổng mục từ: " + dictionary.length;
89
 
90
- render(dictionary.slice(0, 200));
91
- });
 
 
 
 
 
 
 
92
 
93
- const input = document.getElementById("searchInput");
 
 
 
 
 
94
 
95
- input.addEventListener("input", () => {
96
- const q = input.value.trim();
97
- const qn = normalizeVN(q);
98
 
99
- if (!qn) {
100
- render(dictionary.slice(0, 200));
101
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
 
104
- const result = dictionary.filter(e => {
105
- const w = normalizeVN(e.tu);
106
- const m = normalizeVN(e.nghia.join(" "));
107
- return w.includes(qn) || m.includes(qn);
108
- });
109
 
110
- document.getElementById("stats").innerText =
111
- "Kết quả: " + result.length;
 
 
112
 
113
- render(result.slice(0, 400));
114
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- function render(list) {
117
- const box = document.getElementById("results");
118
- box.innerHTML = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- list.forEach(item => {
121
  const div = document.createElement("div");
122
  div.className = "entry";
123
 
 
124
  let html = `
125
- <div class="word">
126
- ${item.tu}
127
- <span class="pos">(${item.tu_loai_day_du})</span>
128
- </div>
129
  <div class="meaning">
130
  `;
131
 
132
- item.nghia.forEach((n, i) => {
133
- html += `<div>${i + 1}. ${n}</div>`;
134
- });
 
 
135
 
136
- html += "</div>";
137
  div.innerHTML = html;
138
- box.appendChild(div);
139
- });
140
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  </script>
142
  </body>
143
  </html>
 
2
  <html lang="vi">
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <title>TỪ ĐIỂN TIẾNG VIỆT – HOÀNG PHÊ (INSTANT)</title>
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 
 
 
7
  <meta name="theme-color" content="#7a0000" />
8
 
9
  <style>
10
+ :root{
11
+ --red:#b30000; --yellow:#ffd700; --dark-red:#7a0000;
12
+ --bg:#fffaf2; --border:#e0c97f; --card:#ffffff;
13
+ --muted:#666; --ink:#222;
14
  }
15
+ *{ box-sizing:border-box; font-family:"Times New Roman", Georgia, serif; }
16
+ body{ margin:0; background:var(--bg); color:var(--ink); }
17
+ header{
18
+ background:linear-gradient(90deg,var(--red),var(--dark-red));
19
+ color:var(--yellow); padding:16px 24px; border-bottom:5px solid var(--yellow);
 
 
 
20
  }
21
+ header h1{ margin:0; font-size:22px; text-transform:uppercase; letter-spacing:1px; }
22
+ header small{ display:block; margin-top:6px; font-size:13px; color:#ffeaa7; }
23
 
24
+ main{ padding:20px; max-width:1100px; margin:auto; }
25
+ .panel{ background:var(--card); border:2px solid var(--border); padding:16px; margin-bottom:16px; }
 
 
 
26
 
27
+ .hint{
28
+ background:#fff3c4; border:1px dashed #d6b657; padding:12px; line-height:1.55; font-size:14px;
 
 
 
29
  }
30
 
31
+ .row{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
32
+ .searchBox{ position:relative; flex:1 1 520px; min-width:280px; }
33
+ input[type="text"]{
34
+ width:100%; padding:12px; font-size:16px;
35
+ border:2px solid var(--red); outline:none; background:#fff;
36
  }
37
+ input[type="text"]:focus{ border-color:var(--dark-red); }
38
+ select, button{
39
+ padding:10px 10px; font-size:14px;
40
+ border:2px solid var(--border); background:#fff; cursor:pointer;
 
41
  }
42
+ button.primary{ border-color:var(--red); }
43
+
44
+ .stats{ font-size:14px; color:#555; margin-top:10px; display:flex; gap:12px; flex-wrap:wrap; }
45
 
46
+ /* Suggest dropdown */
47
+ .suggest{
48
+ position:absolute; left:0; right:0; top:100%;
49
+ background:#fff; border:1px solid #ddd; z-index:50;
50
+ max-height: 320px; overflow:auto; display:none;
51
  }
52
+ .suggest.open{ display:block; }
53
+ .suggest .item{
54
+ padding:10px 10px; border-bottom:1px dotted #ddd;
55
+ display:flex; justify-content:space-between; gap:10px;
56
+ cursor:pointer;
57
+ }
58
+ .suggest .item:hover{ background:#fff7dc; }
59
+
60
+ .entry{ padding:12px 8px; border-bottom:1px dotted #ccc; line-height:1.65; }
61
+ .entry:last-child{ border-bottom:none; }
62
+ .word{ font-size:20px; font-weight:bold; color:var(--dark-red); display:flex; gap:8px; flex-wrap:wrap; align-items:baseline; }
63
+ .pos{ font-style:italic; color:var(--muted); font-size:14px; }
64
+ .meaning{ margin-top:6px; padding-left:12px; }
65
+ .meaning span{ display:block; margin-bottom:4px; }
66
+
67
+ .highlight{ background:#fff2b2; font-weight:bold; }
68
 
69
+ footer{
70
+ text-align:center; padding:12px; font-size:13px; color:#555;
71
+ border-top:2px solid var(--border); margin-top:30px;
72
  }
73
+ .kbd{ border:1px solid #bbb; background:#f7f7f7; padding:1px 6px; border-radius:4px; }
74
  </style>
75
  </head>
76
 
77
  <body>
78
+ <header>
79
+ <h1>TỪ ĐIỂN TIẾNG VIỆT – HOÀNG PHÊ</h1>
80
+ <small>Gõ là ra liền · Không dấu · Gợi ý · dev, 2026</small>
81
+ </header>
82
 
83
  <main>
84
+ <div class="panel hint">
85
+ <b>Gợi ý tra cứu (100 từ):</b>
86
+ Chỉ cần gõ vào ô tìm kiếm, kết quả sẽ hiển thị ngay lập tức. Bạn có thể gõ <b>không dấu</b>:
87
+ “an” sẽ khớp “ăn, ân, ắn…”. Danh sách <b>gợi ý</b> xuất hiện ngay dưới ô, bấm vào là tra nhanh.
88
+ Dùng bộ lọc <b>Từ loại</b> để chỉ xem danh từ/động từ/tính từ… Khi từ khóa quá ngắn, hãy gõ thêm 1–2 ký tự để
89
+ kết quả chính xác hơn. Nhấn <span class="kbd">Esc</span> để xoá nhanh, nhấn <span class="kbd">Enter</span> để “chốt” kết quả.
90
+ Nếu bạn tìm theo nghĩa, cứ gõ cụm từ, hệ thống sẽ dò trong phần định nghĩa.
91
+ </div>
92
+
93
+ <div class="panel">
94
+ <div class="row">
95
+ <div class="searchBox">
96
+ <input id="q" type="text" placeholder="Nhập từ cần tra… (không dấu cũng được)" autocomplete="off" />
97
+ <div id="suggest" class="suggest" aria-label="Gợi ý"></div>
98
+ </div>
99
+
100
+ <select id="pos">
101
+ <option value="">Tất cả từ loại</option>
102
+ <option value="d">Danh từ</option>
103
+ <option value="đg">Động từ</option>
104
+ <option value="t">Tính từ</option>
105
+ <option value="tr">Trợ từ</option>
106
+ <option value="c">Cảm từ</option>
107
+ <option value="p">Phụ từ</option>
108
+ <option value="k">Kết từ</option>
109
+ <option value="đ">Đại từ</option>
110
+ </select>
111
+
112
+ <button class="primary" id="clear">Xoá</button>
113
+ </div>
114
+
115
+ <div class="stats" id="stats">Đang tải dữ liệu…</div>
116
+ </div>
117
+
118
+ <div class="panel" id="results"></div>
119
  </main>
120
 
121
+ <footer>
122
+ Dữ liệu: Từ điển tiếng Việt (Hoàng Phê) · JSON đặt cùng thư mục
123
+ </footer>
124
 
125
+ <script>
126
+ /* =========================
127
+ 1) Normalize không dấu
128
+ ========================= */
129
  function normalizeVN(s) {
130
+ return (s || "")
131
+ .toLowerCase()
132
  .replace(/đ/g, "d")
133
  .normalize("NFD")
134
+ .replace(/[\u0300-\u036f]/g, "")
135
+ .replace(/[^a-z0-9\s\-]/g, " ")
136
+ .replace(/\s+/g, " ")
137
+ .trim();
138
  }
139
 
140
+ function escapeHTML(str){
141
+ return (str ?? "").replace(/[&<>"']/g, m => ({
142
+ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"
143
+ }[m]));
144
+ }
 
145
 
146
+ function highlightText(text, qNorm){
147
+ const safe = escapeHTML(text);
148
+ if(!qNorm) return safe;
149
+ // highlight theo qNorm (không dấu) trên chính text hiển thị: nhẹ và đủ dùng
150
+ const needle = qNorm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
151
+ if(!needle) return safe;
152
+ const re = new RegExp("(" + needle + ")", "ig");
153
+ return safe.replace(re, '<span class="highlight">$1</span>');
154
+ }
155
 
156
+ /* =========================
157
+ 2) Data + Index tối ưu
158
+ ========================= */
159
+ const posLabel = {
160
+ "d":"danh từ","đg":"động từ","t":"tính từ","tr":"trợ từ","c":"cảm từ","p":"phụ từ","k":"kết từ","đ":"đại từ"
161
+ };
162
 
163
+ let ALL = []; // entries đã precompute
164
+ let READY = false;
 
165
 
166
+ const LIMIT_RESULTS = 350; // render tối đa
167
+ const LIMIT_SUGGEST = 12; // gợi ý
168
+
169
+ function prepEntry(e){
170
+ const tu = e.tu || "";
171
+ const nghiaArr = Array.isArray(e.nghia) ? e.nghia : [];
172
+ const nghiaText = nghiaArr.join(" ");
173
+ return {
174
+ ...e,
175
+ _tuNorm: normalizeVN(tu),
176
+ _nghiaNorm: normalizeVN(nghiaText),
177
+ _posText: e.tu_loai_day_du || posLabel[e.tu_loai] || e.tu_loai
178
+ };
179
+ }
180
+
181
+ /* =========================
182
+ 3) Search tức thì (xếp hạng)
183
+ ========================= */
184
+ function searchInstant(qRaw, pos){
185
+ const qNorm = normalizeVN(qRaw);
186
+ if(!qNorm){
187
+ // mặc định hiển thị một phần đầu để chứng minh đã load
188
+ const base = pos ? ALL.filter(x => x.tu_loai === pos) : ALL;
189
+ return { qNorm, list: base.slice(0, 200) };
190
  }
191
 
192
+ const pool = pos ? ALL.filter(x => x.tu_loai === pos) : ALL;
 
 
 
 
193
 
194
+ const exact = [];
195
+ const prefix = [];
196
+ const contains = [];
197
+ const meaning = [];
198
 
199
+ for(const e of pool){
200
+ if(e._tuNorm === qNorm) exact.push(e);
201
+ else if(e._tuNorm.startsWith(qNorm)) prefix.push(e);
202
+ else if(e._tuNorm.includes(qNorm)) contains.push(e);
203
+ else if(e._nghiaNorm.includes(qNorm)) meaning.push(e);
204
+
205
+ // chặn sớm để luôn “ra liền”
206
+ if(exact.length + prefix.length + contains.length >= LIMIT_RESULTS) break;
207
+ }
208
+
209
+ const merged = [...exact, ...prefix, ...contains, ...meaning];
210
+ // loại trùng
211
+ const seen = new Set();
212
+ const out = [];
213
+ for(const e of merged){
214
+ const key = e.tu + "||" + e.tu_loai + "||" + (e.nghia?.[0] || "");
215
+ if(seen.has(key)) continue;
216
+ seen.add(key);
217
+ out.push(e);
218
+ if(out.length >= LIMIT_RESULTS) break;
219
+ }
220
+ return { qNorm, list: out };
221
+ }
222
 
223
+ function buildSuggest(qRaw, pos){
224
+ const qNorm = normalizeVN(qRaw);
225
+ if(!qNorm) return [];
226
+
227
+ const pool = pos ? ALL.filter(x => x.tu_loai === pos) : ALL;
228
+ const out = [];
229
+ for(const e of pool){
230
+ if(e._tuNorm.startsWith(qNorm) || e._tuNorm === qNorm){
231
+ out.push(e);
232
+ if(out.length >= LIMIT_SUGGEST) break;
233
+ }
234
+ }
235
+ if(out.length < LIMIT_SUGGEST){
236
+ for(const e of pool){
237
+ if(e._tuNorm.includes(qNorm) && !out.includes(e)){
238
+ out.push(e);
239
+ if(out.length >= LIMIT_SUGGEST) break;
240
+ }
241
+ }
242
+ }
243
+ return out;
244
+ }
245
+
246
+ /* =========================
247
+ 4) Render UI
248
+ ========================= */
249
+ const $ = (id) => document.getElementById(id);
250
+ const input = $("q");
251
+ const posSel = $("pos");
252
+ const resultsBox = $("results");
253
+ const statsBox = $("stats");
254
+ const suggestBox = $("suggest");
255
+ const btnClear = $("clear");
256
+
257
+ function renderResults(list, qNorm){
258
+ resultsBox.innerHTML = "";
259
+ if(!list.length){
260
+ resultsBox.innerHTML = `<div class="entry"><span class="muted">Không tìm thấy.</span></div>`;
261
+ return;
262
+ }
263
 
264
+ for(const item of list){
265
  const div = document.createElement("div");
266
  div.className = "entry";
267
 
268
+ const wordHtml = highlightText(item.tu || "", qNorm);
269
  let html = `
270
+ <div class="word">${wordHtml} <span class="pos">(${escapeHTML(item._posText || "")})</span></div>
 
 
 
271
  <div class="meaning">
272
  `;
273
 
274
+ const nghia = Array.isArray(item.nghia) ? item.nghia : [];
275
+ for(let i=0;i<nghia.length;i++){
276
+ html += `<span>${i+1}. ${highlightText(nghia[i], qNorm)}</span>`;
277
+ }
278
+ html += `</div>`;
279
 
 
280
  div.innerHTML = html;
281
+ resultsBox.appendChild(div);
282
+ }
283
  }
284
+
285
+ function renderSuggest(items){
286
+ suggestBox.innerHTML = "";
287
+ if(!items.length){
288
+ suggestBox.classList.remove("open");
289
+ return;
290
+ }
291
+ for(const it of items){
292
+ const row = document.createElement("div");
293
+ row.className = "item";
294
+ row.innerHTML = `
295
+ <div><b>${escapeHTML(it.tu || "")}</b> <span class="muted">(${escapeHTML(it._posText || "")})</span></div>
296
+ <div class="muted">↵</div>
297
+ `;
298
+ row.addEventListener("mousedown", (ev) => {
299
+ ev.preventDefault();
300
+ input.value = it.tu || "";
301
+ run(true);
302
+ suggestBox.classList.remove("open");
303
+ });
304
+ suggestBox.appendChild(row);
305
+ }
306
+ suggestBox.classList.add("open");
307
+ }
308
+
309
+ /* =========================
310
+ 5) Run: “gõ là ra liền”
311
+ - requestAnimationFrame để mượt
312
+ - không debounce dài (chỉ 1 frame)
313
+ ========================= */
314
+ let raf = 0;
315
+
316
+ function run(fromPick=false){
317
+ if(!READY) return;
318
+
319
+ const qRaw = input.value || "";
320
+ const pos = posSel.value || "";
321
+
322
+ const { qNorm, list } = searchInstant(qRaw, pos);
323
+ statsBox.textContent =
324
+ `Kết quả: ${list.length.toLocaleString("vi-VN")} · Tổng: ${ALL.length.toLocaleString("vi-VN")}` +
325
+ (pos ? ` · Lọc: ${posLabel[pos] || pos}` : "") +
326
+ (qRaw ? " · (hiển thị tức thì)" : "");
327
+
328
+ renderResults(list, qNorm);
329
+
330
+ if(!fromPick){
331
+ const sug = buildSuggest(qRaw, pos);
332
+ renderSuggest(sug);
333
+ }
334
+ }
335
+
336
+ function runNextFrame(){
337
+ cancelAnimationFrame(raf);
338
+ raf = requestAnimationFrame(() => run(false));
339
+ }
340
+
341
+ /* =========================
342
+ 6) Events
343
+ ========================= */
344
+ input.addEventListener("input", () => runNextFrame());
345
+ posSel.addEventListener("change", () => run(true));
346
+
347
+ btnClear.addEventListener("click", () => {
348
+ input.value = "";
349
+ suggestBox.classList.remove("open");
350
+ run(true);
351
+ input.focus();
352
+ });
353
+
354
+ document.addEventListener("click", (e) => {
355
+ if(!suggestBox.contains(e.target) && e.target !== input){
356
+ suggestBox.classList.remove("open");
357
+ }
358
+ });
359
+
360
+ input.addEventListener("keydown", (e) => {
361
+ if(e.key === "Escape"){
362
+ input.value = "";
363
+ suggestBox.classList.remove("open");
364
+ run(true);
365
+ }
366
+ if(e.key === "Enter"){
367
+ suggestBox.classList.remove("open");
368
+ run(true);
369
+ }
370
+ });
371
+
372
+ /* =========================
373
+ 7) Load JSON (cùng thư mục)
374
+ ========================= */
375
+ (async function boot(){
376
+ try{
377
+ statsBox.textContent = "Đang tải dữ liệu…";
378
+ const res = await fetch("./tu_dien_hoang_phe_clean.json", { cache: "no-cache" });
379
+ const data = await res.json();
380
+
381
+ const muc = Array.isArray(data.muc_tu) ? data.muc_tu : [];
382
+ ALL = muc.map(prepEntry);
383
+
384
+ READY = true;
385
+ statsBox.textContent = `Đã tải: ${ALL.length.toLocaleString("vi-VN")} mục từ · Gõ để tra ngay`;
386
+ run(true);
387
+ input.focus();
388
+
389
+ }catch(err){
390
+ console.error(err);
391
+ statsBox.textContent = "❌ Không load được JSON. Kiểm tra file cùng thư mục và quyền truy cập.";
392
+ resultsBox.innerHTML = `<div class="entry"><span class="muted">Không tải đư��c dữ liệu.</span></div>`;
393
+ }
394
+ })();
395
  </script>
396
  </body>
397
  </html>