PhraseFlow | 多場景短語工具
為日本旅遊新手打造的輕量網頁應用,幫助完全不懂日語的使用者,透過一鍵點選的方式,即時顯示實用的日語短句。
系統特色
- 零依賴:純 JavaScript + Alpine.js
- 離線使用:LocalStorage 儲存,無網路也能用
- AI 整合:Google Gemini API 生成個人專屬短句
- Material Design 3:乾淨的佈局
- 響應式設計:手機、平板、電腦均適配
系統架構
Alpine.js
↓
┌─────────────────┐
│ Material Web │ → 使用者介面
└─────────────────┘
↓
┌─────────────────┐
│ Google Gemini │ ← AI 短句生成
└─────────────────┘
↓
┌─────────────────┐
│ LocalStorage │ ← 資料儲存
└─────────────────┘
專案結構
phraseflow/
├── index.html # 主頁面
├── app.js # Alpine.js 應用邏輯
├── data.js # 短句資料庫
├── utils.js # 工具函數
├── styles.css # 主要樣式
└── package.json # 專案設定
核心技術實作
1. Alpine.js 狀態管理
function createApp() {
return {
view: "home",
data: [],
currentSceneName: "",
currentPhrases: [],
dialogPhrase: { native: "", target: "" },
num: 1,
place: "新宿",
food: "牛肉",
init() {
const cached = localStorage.getItem("qp_cachedData");
this.data = cached ? JSON.parse(cached) : defaultData;
this.placeOptions = [...getRecent(), ...presetPlaces].filter(unique);
this.foodOptions = [...getRecentFood(), ...presetFoods].filter(unique);
},
enterScene(id) {
const s = this.data.find((x) => x.id === id);
this.currentSceneName = s.scene;
this.currentPhrases = s.phrases;
this.view = "phrases";
}
}
}
2. Material Web Components 整合
<!-- 場景選擇按鈕 -->
<div class="scene-grid">
<template x-for="scene in data" :key="scene.id">
<md-filled-button class="scene-btn" @click="enterScene(scene.id)">
<span x-text="scene.scene"></span>
</md-filled-button>
</template>
</div>
<!-- 短句對話框 -->
<md-dialog x-ref="mainDlg">
<div slot="headline" x-text="dialogPhrase.native"></div>
<div slot="content" style="text-align:center">
<p id="targetText" class="big"></p>
<!-- 數量控制 -->
<div x-show="showNumCtrl" x-cloak class="num-wrap">
<md-filled-tonal-button class="num-btn" @click="dec()">
<span class="material-symbols-outlined">remove</span>
</md-filled-tonal-button>
<md-filled-tonal-button class="num-btn" @click="inc()">
<span class="material-symbols-outlined">add</span>
</md-filled-tonal-button>
</div>
</div>
</md-dialog>
3. Google Gemini AI 整合
async callGemini(plan) {
const apiKey = "AIzaSyCpL-uCIMgV4MMk9xGdTy3MShK1v2Vw5lQ";
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const promptText = "你是一個旅遊情境日語短句生成助手。" +
"請根據使用者提供的中文行程內容,提取3到5個常見旅遊情境。" +
"每個情境,請提供5到10組中日對照的短句。";
const requestData = {
contents: [
{ parts: [{ text: `${promptText}\n\n使用者行程:\n${plan}` }] }
]
};
const r = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestData)
});
const j = await r.json();
let resultText = j.candidates[0].content.parts[0].text;
resultText = resultText.replace(/```json/g, "").replace(/```/g, "").trim();
const newData = JSON.parse(resultText);
this.data = newData;
localStorage.setItem("qp_cachedData", resultText);
}
4. 自訂短句資料
編輯 data.js
檔案,添加你的短句:
export const defaultData = [
{
"id": 1,
"scene": "餐廳點餐",
"phrases": [
{
"pid": 1,
"native": "不好意思(用來招呼服務員)。",
"target": "すみません。"
},
{
"pid": 2,
"native": "可以不要放<食物>嗎?",
"target": "<食物>を入れないでください。"
}
]
}
];