昨天我們完成了頂部雙行即時預覽與連續運算鏈狀態機,徹底解決了長算式盲打與運算中斷的體驗痛點。然而,當我們正式進入第四階段「全球即時匯率中心與智慧記帳」時,另一個真實痛點隨之而來:當使用者身處國內進行純本幣計算(例如在台灣計算新台幣開銷、在日本計算日圓帳目)時,頂部巨大的外幣換算面板與底部的換算數值列顯得毫無用武之地,反而壓縮了主數字螢幕的高度,讓日常記帳視野變得擁擠侷促。
為了兼顧出國跨國換算的強大能力與國內在地運算的簡潔視野,今天我們實作 Local Mode 本地模式智慧偵測與頂部動態膠囊收合機制。系統透過網路通訊協定與瀏覽器底層時區雙重自動定位,當偵測到來源幣別與目標幣別一致時,即刻啟動本地模式將頂部膠囊收合為精緻輕巧的狀態;更重要的是,系統具備智慧防抖狀態感知,當算式仍在輸入中或選單開啟時絕不擅自收合,杜絕版面突兀跳動。
線上體驗 Live Demo:https://410355-collab.github.io/Premium-Business-Calculator/
(強烈建議使用 iPhone Safari 或 Android 手機開啟體驗!)
GitHub 開源倉庫:https://github.com/410355-collab/Premium-Business-Calculator
歡迎提出建議與指導
(開發環境:Google Antigravity IDE + Google Gemini 協同開發)
每日更新中...
在整合跨國匯率的計算機工具中,多數產品往往陷入兩難的設計誤區:
第一個問題是介面缺乏場景感知。許多匯率型計算機無論使用者在何種情境下,始終將外幣換算列、國旗切換器與匯率更新時間硬性固定於螢幕頂端。然而在使用者高達八成的日常情境中,大家只是在計算當地的薪資、水電或購物發票。此時兩排幣別選擇器與底部的等值換算螢幕不但毫無意義,更壓縮了主數字螢幕的高度,造成數字過早縮小的擁擠視覺。
第二個問題是自動化行為缺乏防呆時序。部分嘗試做自動收合或切換的應用,常常採用生硬的定時器強制收起,導致使用者剛點擊了計算機鍵盤、長算式正輸入到一半,頂部面板突然向上滑動,帶動整個數字螢幕上下跳動,瞬間打斷使用者的視覺焦點與思維節奏。要做到無感的在地優化,系統必須兼具精準的地理定位、雙幣同源判定與細膩的鍵盤輸入保護機制。
為了實現優雅純粹的本地模式體驗,我們建立了三位一體的智慧收合架構:
網路端點與底層時區雙重定位管線:
當初次開啟應用或重設幣別時,系統首先透過輕量地理介面嘗試解析使用者的當前所在地幣別代碼;若遇離線或網路攔截,立即無縫降級至瀏覽器原生的高精度時區物件進行備用推斷。無論使用者身處台北、東京、首爾還是雪梨,系統皆能在第一時間辨識出當地法幣。
同幣別本地模式切換邏輯:
當系統判定來源幣別與目標幣別一致時,立即為頂部導覽區域與外層容器掛載同幣別樣式標記。此時頂部的大型換算面板進入收合態,僅保留稅率切換按鍵與極簡狀態膠囊,並於主螢幕右側呈現微型本地模式指示;而底部的匯率換算副螢幕亦同步隱藏,將完整的縱向高度全數歸還給主計算螢幕。
輸入狀態感知與防跳動定時器:
我們為介面導入了閒置感知器。當算式處於輸入狀態且尚未求值完成,抑或幣別下拉選單、側邊設定抽屜正在開啟時,收合定時器即刻被靜默攔截並鎖定;唯有當使用者求值完畢且處於真正閒置時,才允許在十五秒後平順收納。此外,點擊頂部微型膠囊可隨時主動在展開與收合之間自由切換,給予使用者百分之百的掌控權。
以下為雙重定位檢測、同幣別狀態標記與防干擾收合定時器的關鍵邏輯:
// 雙重地理定位檢測:網路 API 優先,時區物件降級備援
async function detectLocationCurrency(isFirstTimeUser) {
let detectedCurrency = null;
try {
const res = await fetch('https://ipwho.is/');
if (res.ok) {
const data = await res.json();
if (data.currency && data.currency.code) {
detectedCurrency = data.currency.code;
}
}
} catch (e) {
console.warn("API detection failed, trying Local Timezone fallback...");
}
// 網路不可用時,利用 Intl API 時區字串進行精確推斷
if (!detectedCurrency) {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (tz.includes('Seoul')) detectedCurrency = 'KRW';
else if (tz.includes('Taipei')) detectedCurrency = 'TWD';
else if (tz.includes('Tokyo')) detectedCurrency = 'JPY';
else if (tz.includes('Shanghai') || tz.includes('Chongqing')) detectedCurrency = 'CNY';
else if (tz.includes('Kuala_Lumpur') || tz.includes('Kuching')) detectedCurrency = 'MYR';
else if (tz.includes('Singapore')) detectedCurrency = 'SGD';
else if (tz.includes('Sydney') || tz.includes('Melbourne') || tz.startsWith('Australia/')) detectedCurrency = 'AUD';
else if (tz.includes('Ho_Chi_Minh') || tz.includes('Hanoi')) detectedCurrency = 'VND';
else if (tz.includes('Bangkok')) detectedCurrency = 'THB';
else if (tz.startsWith('America/')) detectedCurrency = 'USD';
else if (tz.startsWith('Europe/')) detectedCurrency = 'EUR';
} catch (e) {}
}
// 命中支援幣別時套用設定,若首發為本地本幣則立即預設收合膠囊
if (detectedCurrency && flags[detectedCurrency]) {
if (isFirstTimeUser || detectedCurrency === 'TWD') {
currentFrom = detectedCurrency;
selectCurrency('from', currentFrom, flags[detectedCurrency]);
if (detectedCurrency === 'TWD') {
currentTo = 'TWD';
selectCurrency('to', 'TWD', flags['TWD']);
const headerArea = document.getElementById('header-area');
if (headerArea) setCapsuleCollapsed(true, { animate: false });
}
}
const currName = I18N[currentLang].currencies[detectedCurrency] || detectedCurrency;
showToastMsg(`${I18N[currentLang].toastLocationSuccess}${currName}`);
} else {
showToastMsg(I18N[currentLang].toastLocationFailed);
}
}
接著是同幣別狀態切換與防止算式中途跳動的收合狀態控制:
// 同幣別樣式標記判斷
function checkSameCurrency() {
const headerArea = document.getElementById('header-area');
const phoneContainer = document.querySelector('.phone-container');
if (!headerArea || !phoneContainer) return;
if (currentFrom === currentTo) {
headerArea.classList.add('same-currency');
phoneContainer.classList.add('same-currency');
} else {
headerArea.classList.remove('same-currency');
phoneContainer.classList.remove('same-currency');
}
}
// 智慧防抖收合定時器:算式輸入中或選單開啟時絕對維持展開
function resetInactivityTimer() {
if (inactivityTimer) clearTimeout(inactivityTimer);
const headerArea = document.getElementById('header-area');
if (!headerArea || headerArea.classList.contains('collapsed')) return;
// 核心防護:算式未完結(非初始零且尚未求值)或選單展開時,禁止收合
const isFormulaActive = (currentInput !== "0" && !isEvaluated);
const isAnyMenuOpen = document.querySelector('.overlay-menu.show') ||
document.querySelector('.dropdown-options.show');
if (isFormulaActive || isAnyMenuOpen) return;
// 閒置達標後執行平滑收合
inactivityTimer = setTimeout(() => {
setCapsuleCollapsed(true);
}, 15000);
}
在實作動態膠囊與本地模式的互動過程中,我們投入了大量心力在細節體驗上的打磨:
零延遲降級雙重保險:
許多外幣應用在離線或飛航模式下開啟時,常因網路端點超時而卡頓數秒。我們的定位架構將網路請求設置為非同步背景執行,一旦失敗立刻無縫呼叫現代標準時區解析介面。時區資訊儲存於本機記憶體,完全不耗費任何網路頻寬,在數毫秒內即可完成推導,徹底告別白屏等待。
絕對安全的視覺防跳機制:
若使用者正在輸入一條由括號與乘加組合而成的長算式,最忌諱介面元素突然改變大小造成視線偏移。透過嚴謹的運算中判定條件,只要當前算式不是初始狀態且尚未敲下等於,系統的閒置收合計時器會立刻自我凍結;直到使用者按下等於得出最終總額,且目光轉移離開鍵盤時,介面才會在安靜的片刻緩緩將頂部收納。
立體無重排流暢過渡:
動態膠囊在收合與展開時,並非單純藉由粗暴的屬性切換達成,而是搭配動畫樣式進行尺寸預判。當膠囊收合為小巧圓角狀態時,稅率按鈕自動緊縮至頂部角落,騰出的空間立刻被下方的數字螢幕平滑填補;點擊收合態的頂部區域又能隨時呼叫完整國旗與雙幣對調器,兼顧了單純運算的舒適度與需要出國換算時的隨手可得性。
今天我們成功建立了 Local Mode 本地模式智慧偵測與頂部動態膠囊收合系統。藉由網路通訊協定與時區雙軌推斷,計算機能夠主動感知使用者所處的地理環境;配合精確的同幣別標記與智慧防抖定時器,為日常計算騰出了乾淨專注的視野空間。
明天我們將進一步探討跨國換算的靈魂機制:九國幣別雙向對調連動、自訂匯率手動覆寫面板,以及面對複雜網路環境時的節流防抖與斷網降級快照機制。
出國在地下街逛街收不到網路訊號,難道計算機的匯率換算就徹底停擺了嗎?面對銀行高昂的跨國刷卡手續費,又該如何擺脫公定匯率的束縛,精準掌握真實開銷?明天我們將帶來極致流暢的跨國換匯體驗,敬請期待!