|
3 | 3 | <head> |
4 | 4 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
5 | 5 | <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> |
6 | | - <title>三碼中文輸入法 - 手機版</title> |
| 6 | + <title>三碼中文輸入法 - 手機滑動選字版</title> |
7 | 7 | <link rel="preconnect" href="https://fonts.googleapis.com/"> |
8 | 8 | <link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin> |
9 | 9 | <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap" rel="stylesheet"> |
|
28 | 28 | margin: 0; |
29 | 29 | padding: 0; |
30 | 30 | box-sizing: border-box; |
31 | | - -webkit-tap-highlight-color: transparent; /* 移除點擊高亮 */ |
| 31 | + -webkit-tap-highlight-color: transparent; |
32 | 32 | outline: none; |
33 | 33 | } |
34 | 34 |
|
|
99 | 99 | cursor: text; |
100 | 100 | } |
101 | 101 |
|
102 | | - /* 候選字與輸入區 */ |
| 102 | + /* 候選字與輸入區容器 */ |
103 | 103 | #input-area { |
104 | 104 | width: 100%; |
105 | 105 | background-color: var(--container-bg); |
|
110 | 110 | border: 1px solid var(--border-color); |
111 | 111 | } |
112 | 112 |
|
| 113 | + /* 編碼輸入顯示 */ |
113 | 114 | #main-input { |
114 | 115 | width: 100%; |
115 | 116 | padding: 8px; |
|
120 | 121 | border-bottom: 1px solid var(--border-color); |
121 | 122 | background-color: #fafafa; |
122 | 123 | color: #757575; |
| 124 | + white-space: nowrap; |
| 125 | + overflow-x: auto; |
123 | 126 | } |
124 | 127 |
|
| 128 | + /* 候選字橫向滾動容器 */ |
125 | 129 | #candidate-display { |
126 | 130 | display: flex; |
127 | | - flex-wrap: wrap; |
128 | 131 | align-items: center; |
129 | 132 | padding: 8px; |
130 | | - gap: 8px; |
131 | | - white-space: normal; |
| 133 | + gap: 12px; |
| 134 | + white-space: nowrap; /* 確保所有候選字在一行 */ |
| 135 | + overflow-x: scroll; /* 允許橫向滾動 */ |
| 136 | + -webkit-overflow-scrolling: touch; /* 提升 iOS 上的滾動體驗 */ |
132 | 137 | min-height: 40px; |
133 | 138 | } |
134 | 139 |
|
| 140 | + #candidate-display::-webkit-scrollbar { |
| 141 | + display: none; /* 隱藏滾動條 */ |
| 142 | + } |
| 143 | + |
135 | 144 | #candidate-display span { |
136 | 145 | cursor: pointer; |
137 | 146 | padding: 5px 8px; |
138 | 147 | border-radius: 4px; |
139 | 148 | transition: all 0.2s; |
140 | 149 | user-select: none; |
141 | 150 | white-space: nowrap; |
142 | | - font-size: 1.1rem; |
143 | | - flex-shrink: 0; |
144 | | - text-align: center; |
| 151 | + font-size: 1.2rem; /* 略微放大字體 */ |
| 152 | + flex-shrink: 0; /* 防止候選字被壓縮 */ |
145 | 153 | } |
146 | 154 |
|
147 | 155 | #candidate-display span:active, #candidate-display span:hover { |
|
182 | 190 |
|
183 | 191 | .key { |
184 | 192 | flex-grow: 1; |
185 | | - flex-basis: 0; /* 確保平均分配寬度 */ |
| 193 | + flex-basis: 0; |
186 | 194 | padding: 0; |
187 | 195 | border-radius: 6px; |
188 | 196 | background-color: var(--key-bg); |
|
191 | 199 | user-select: none; |
192 | 200 | transition: background-color 0.1s; |
193 | 201 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.2); |
194 | | - line-height: 50px; /* 設定行高以垂直置中文字 */ |
| 202 | + line-height: 50px; |
195 | 203 | min-height: 50px; |
196 | 204 | font-size: 1.2rem; |
197 | 205 | font-weight: bold; |
|
227 | 235 | <div id="text-output"></div> |
228 | 236 |
|
229 | 237 | <div id="input-area"> |
230 | | - <div id="main-input"></div> |
231 | | - <div id="candidate-display"></div> |
| 238 | + <div id="main-input">編碼顯示在此</div> |
| 239 | + <div id="candidate-display">候選字顯示在此...</div> |
232 | 240 | </div> |
233 | 241 |
|
234 | 242 | <div id="mode-display">模式:三碼次常</div> |
|
240 | 248 | <script> |
241 | 249 | // --- 資料處理與解析 (從 index-0.html 擷取核心部分) --- |
242 | 250 | let currentCodemap = {}; |
243 | | - let currentReverseCodemap = {}; |
244 | 251 |
|
245 | 252 | const keyname_map_upper = { |
246 | | - 'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F', 'g': 'G', |
247 | | - 'h': 'H', 'i': 'I', 'j': 'J', 'k': 'K', 'l': 'L', 'm': 'M', 'n': 'N', |
248 | | - 'o': 'O', 'p': 'P', 'q': 'Q', 'r': 'R', 's': 'S', 't': 'T', 'u': 'U', |
249 | | - 'v': 'V', 'w': 'W', 'x': 'X', 'y': 'Y', 'z': 'Z', |
250 | | - // 符號對應 index-0.html 的顯示 |
251 | | - "'": "〃", ';': ';', ',': ',', '.': '.', '/': '/', ' ': '空白鍵', 'enter': 'Enter', 'backspace': '刪除鍵', 'shift': '中/英', |
| 253 | + 'a': '一', 'b': '工', 'c': '人', 'd': '心', 'e': '大', 'f': '手', 'g': '木', |
| 254 | + 'h': '土', 'i': '口', 'j': '女', 'k': '艸', 'l': '田', 'm': '山', 'n': 'P', |
| 255 | + 'o': '金', 'p': '月', 'q': '水', 'r': '日', 's': '竹', 't': '禾', 'u': '卜', |
| 256 | + 'v': '戈', 'w': 'O', 'x': 'X', 'y': 'Y', 'z': 'Z', |
| 257 | + // 符號對應 |
| 258 | + "'": "〃", ';': ';', ',': ',', '.': '.', '/': '/', ' ': '空白', 'enter': '換行', 'backspace': '刪除', 'shift': '中/英', |
252 | 259 | }; |
253 | 260 | const keyname_map_lower = { |
254 | 261 | 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', |
255 | 262 | 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', |
256 | 263 | 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', |
257 | 264 | 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', |
258 | | - "'": "'", ';': ';', ',': ',', '.': '.', '/': '/', ' ': ' ', 'enter': 'Enter', 'backspace': '刪除鍵', 'shift': '中/英', |
| 265 | + // 符號對應 |
| 266 | + "'": "'", ';': ';', ',': ',', '.': '.', '/': '/', ' ': ' ', 'enter': 'Enter', 'backspace': 'Back', 'shift': 'Shift', |
259 | 267 | }; |
260 | 268 |
|
261 | 269 | // DOM 元素 |
|
270 | 278 | const MAX_CODE_LENGTH = 3; |
271 | 279 | let currentCode = ''; |
272 | 280 | let currentCandidates = []; |
273 | | - let currentPage = 0; |
274 | | - const candidatesPerPage = 10; |
275 | 281 | let isEnglishMode = false; |
276 | 282 |
|
277 | 283 | // 函式:格式化碼表資料為物件 (從 index-0.html 擷取) |
278 | 284 | function parseCinData(data) { |
279 | 285 | const newCodemap = {}; |
280 | | - const newReverseCodemap = {}; |
281 | 286 | const lines = data.trim().split('\n'); |
282 | 287 | lines.forEach(line => { |
283 | 288 | const parts = line.split('\t'); |
284 | 289 | if (parts.length >= 2 && !line.startsWith('#')) { |
285 | | - const code = parts[0].trim(); |
| 290 | + const code = parts[0].trim().toLowerCase(); // 確保編碼小寫 |
286 | 291 | const char = parts[1].trim(); |
287 | 292 | if (code && char) { |
288 | 293 | if (!newCodemap[code]) { |
289 | 294 | newCodemap[code] = []; |
290 | 295 | } |
291 | 296 | newCodemap[code].push(char); |
292 | | - |
293 | | - if (!newReverseCodemap[char]) { |
294 | | - newReverseCodemap[char] = []; |
295 | | - } |
296 | | - newReverseCodemap[char].push(code); |
297 | 297 | } |
298 | 298 | } |
299 | 299 | }); |
300 | | - return { newCodemap, newReverseCodemap }; |
| 300 | + return newCodemap; |
301 | 301 | } |
302 | 302 |
|
303 | 303 | // 函式:載入次常用字碼表 (僅載入 3codes2.txt) |
304 | 304 | async function loadSecondaryCodemap() { |
305 | 305 | const fileName = './3codes2.txt'; |
306 | 306 | try { |
307 | | - // 這裡假設 3codes2.txt 位於與此 HTML 檔案相同的目錄下 |
308 | 307 | const response = await fetch(fileName); |
309 | 308 | if (!response.ok) { |
310 | 309 | throw new Error('Network response was not ok'); |
311 | 310 | } |
312 | 311 | const text = await response.text(); |
313 | | - const { newCodemap, newReverseCodemap } = parseCinData(text); |
314 | | - currentCodemap = newCodemap; |
315 | | - currentReverseCodemap = newReverseCodemap; |
| 312 | + currentCodemap = parseCinData(text); |
316 | 313 | updateModeDisplay(); |
| 314 | + mainInput.textContent = '編碼表載入成功'; |
317 | 315 | } catch (error) { |
318 | 316 | console.error(`載入 ${fileName} 時發生錯誤:`, error); |
319 | | - textOutput.textContent = `錯誤:無法載入編碼檔 ${fileName}。請確認檔案存在且與此 HTML 位於相同目錄下。`; |
| 317 | + mainInput.textContent = `錯誤:無法載入編碼檔 ${fileName}。請確認檔案存在且與此 HTML 位於相同目錄下。`; |
320 | 318 | } |
321 | 319 | resetInputState(); |
322 | 320 | } |
323 | 321 |
|
324 | | - // --- 處理中文模式輸入的核心函數 (從 index-0.html 擷取) --- |
| 322 | + // --- 處理中文模式輸入的核心函數 --- |
325 | 323 | function handleChineseInput(code) { |
326 | | - // 統一處理數字鍵邏輯 (虛擬鍵盤沒有數字鍵,此處忽略) |
327 | | - if (code.match(/^[1-90]$/)) { |
328 | | - return; |
329 | | - } |
330 | | - |
| 324 | + |
331 | 325 | if (code === 'backspace') { |
332 | 326 | if (currentCode.length > 0) { |
333 | 327 | currentCode = currentCode.slice(0, -1); |
|
347 | 341 | return; |
348 | 342 | } |
349 | 343 |
|
350 | | - // 獨立的翻頁處理函數 (虛擬鍵盤移除翻頁鍵,此處忽略) |
351 | | - |
352 | 344 | if (code === ' ') { |
353 | 345 | if (currentCandidates.length > 0) { |
354 | 346 | selectCandidate(0); |
|
359 | 351 | } |
360 | 352 |
|
361 | 353 | const inputChar = code.toLowerCase(); |
362 | | - // 接受所有 26 字母和 5 符號的輸入 |
| 354 | + // 接受所有 26 字母和 5 符號的輸入: a-z, ; ' , . / |
363 | 355 | if (inputChar.match(/^[a-z;'./,<>]$/)) { |
364 | 356 | if (currentCode.length === MAX_CODE_LENGTH) { |
365 | 357 | if (currentCandidates.length > 0) { |
366 | | - selectCandidate(0); // 超過三碼,自動上字 |
| 358 | + selectCandidate(0); // 超過三碼,自動選第一個上字 |
367 | 359 | } |
368 | 360 | currentCode = inputChar; |
369 | 361 | } else { |
|
373 | 365 | } |
374 | 366 | } |
375 | 367 |
|
376 | | - // --- 處理英文模式輸入的新函數 (從 index-0.html 擷取) --- |
| 368 | + // --- 處理英文模式輸入 --- |
377 | 369 | function handleEnglishInput(char) { |
378 | 370 | // 處理功能鍵 |
379 | 371 | if (char === 'backspace') { |
|
397 | 389 | } |
398 | 390 | } |
399 | 391 |
|
400 | | - // --- 核心狀態更新與顯示函數 (中文模式專用) (從 index-0.html 擷取) --- |
| 392 | + // --- 核心狀態更新與顯示函數 (中文模式專用) --- |
401 | 393 | function updateStateAndDisplay() { |
402 | | - mainInput.textContent = formatCode(currentCode); |
403 | | - currentPage = 0; |
| 394 | + mainInput.textContent = `輸入編碼: ${formatCode(currentCode)}`; |
404 | 395 | currentCandidates = currentCodemap[currentCode] || []; |
405 | 396 |
|
406 | 397 | if (currentCode.length === MAX_CODE_LENGTH) { |
|
409 | 400 | resetInputState(); |
410 | 401 | return; |
411 | 402 | } else if (currentCandidates.length === 0) { |
412 | | - // 如果沒有候選字,清空輸入,不做任何輸出 |
| 403 | + // 沒有候選字,清空輸入 |
413 | 404 | resetInputState(); |
414 | 405 | return; |
415 | 406 | } |
|
418 | 409 | showCandidates(); |
419 | 410 | } |
420 | 411 |
|
421 | | - // --- 顯示候選字為列表 (從 index-0.html 擷取) --- |
| 412 | + // --- 顯示候選字為可滑動列表 (更新) --- |
422 | 413 | function showCandidates() { |
423 | 414 | candidateDisplay.innerHTML = ''; |
424 | 415 |
|
425 | | - const start = currentPage * candidatesPerPage; |
426 | | - const end = start + candidatesPerPage; |
427 | | - const displayedCandidates = currentCandidates.slice(start, end); |
428 | | - |
429 | | - displayedCandidates.forEach((candidate, index) => { |
430 | | - const span = document.createElement('span'); |
431 | | - const displayIndex = (index === 9) ? '0' : (index + 1).toString(); |
432 | | - span.textContent = `${displayIndex}. ${candidate}`; |
433 | | - span.onclick = () => selectCandidate(index); |
434 | | - candidateDisplay.appendChild(span); |
435 | | - }); |
436 | | - |
437 | 416 | if (currentCandidates.length === 0 && currentCode.length > 0) { |
438 | 417 | candidateDisplay.textContent = '無候選字'; |
| 418 | + return; |
439 | 419 | } else if (currentCandidates.length === 0) { |
440 | 420 | candidateDisplay.textContent = '候選字顯示在此...'; |
| 421 | + return; |
441 | 422 | } |
| 423 | + |
| 424 | + // 顯示所有候選字並支援點擊 |
| 425 | + currentCandidates.forEach((candidate, index) => { |
| 426 | + const span = document.createElement('span'); |
| 427 | + // 移除數字標記,直接顯示漢字 |
| 428 | + span.textContent = candidate; |
| 429 | + span.onclick = () => selectCandidate(index); |
| 430 | + candidateDisplay.appendChild(span); |
| 431 | + }); |
442 | 432 | } |
443 | 433 |
|
444 | | - // --- 選擇候選字並清空輸入 (從 index-0.html 擷取) --- |
| 434 | + // --- 選擇候選字並清空輸入 --- |
445 | 435 | function selectCandidate(index) { |
446 | | - const selectedCandidate = currentCandidates[currentPage * candidatesPerPage + index]; |
| 436 | + const selectedCandidate = currentCandidates[index]; |
447 | 437 | if (selectedCandidate) { |
448 | 438 | textOutput.textContent += selectedCandidate; |
449 | 439 | resetInputState(); |
450 | 440 | } |
451 | 441 | } |
452 | 442 |
|
453 | | - // --- 清空所有輸入狀態 (從 index-0.html 擷取) --- |
| 443 | + // --- 清空所有輸入狀態 --- |
454 | 444 | function resetInputState() { |
455 | 445 | currentCode = ''; |
456 | 446 | currentCandidates = []; |
457 | | - currentPage = 0; |
458 | | - mainInput.textContent = '編碼顯示在此'; |
| 447 | + mainInput.textContent = isEnglishMode ? '英文模式' : '編碼顯示在此'; |
459 | 448 | showCandidates(); |
460 | 449 | } |
461 | 450 |
|
462 | | - // --- 格式化編碼顯示 (從 index-0.html 擷取) --- |
| 451 | + // --- 格式化編碼顯示 --- |
463 | 452 | function formatCode(code) { |
464 | 453 | let displayString = ''; |
465 | 454 | for (const char of code) { |
466 | | - // 使用大寫字母顯示編碼 |
| 455 | + // 顯示基礎字根/符號 |
467 | 456 | displayString += keyname_map_upper[char] || char.toUpperCase(); |
468 | 457 | } |
469 | 458 | return displayString; |
470 | 459 | } |
471 | 460 |
|
472 | | - // --- 更新鍵盤按鍵顯示 (從 index-0.html 擷取並修改) --- |
| 461 | + // --- 更新鍵盤按鍵顯示 --- |
473 | 462 | function updateKeyboardDisplay() { |
474 | 463 | const map = isEnglishMode ? keyname_map_lower : keyname_map_upper; |
475 | 464 | document.querySelectorAll('.key[data-code]').forEach(keyElement => { |
476 | 465 | const code = keyElement.dataset.code; |
477 | 466 | if (map[code]) { |
478 | | - // 功能鍵使用原始標籤,字符鍵使用大小寫 |
| 467 | + // 功能鍵使用原始標籤,字符鍵使用大小寫/字根 |
479 | 468 | keyElement.textContent = map[code]; |
480 | 469 | } |
481 | 470 | }); |
482 | 471 | } |
483 | 472 |
|
484 | | - // --- 更新模式顯示 (從 index-0.html 擷取並修改) --- |
| 473 | + // --- 更新模式顯示 --- |
485 | 474 | function updateModeDisplay() { |
486 | 475 | modeDisplay.textContent = isEnglishMode ? '模式:英文' : '模式:三碼次常'; |
487 | 476 | } |
|
492 | 481 | const layout_mobile = [ |
493 | 482 | ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'], |
494 | 483 | ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';'], |
495 | | - ['Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', "'"], |
496 | | - ['Shift', 'Space', 'Back', 'Enter'] |
| 484 | + ['Shift', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', "'"], |
| 485 | + ['Space', 'Back', 'Enter'] // 移除 123 |
497 | 486 | ]; |
498 | 487 |
|
499 | 488 | keyboardContainer.innerHTML = ''; |
|
0 commit comments