
HexaZen - EP08:UI 設計與國際化
3D 場景和音效引擎都搞定了,最後要讓整個應用的 UI 既好看又好用。
HexaZen 跑在 VitePress 的 whyframe iframe 裡,所以 vue-i18n 無法正常運作。
沒關係,那就自己做一個。ᕕ( ゚ ∀。)ᕗ
倒角邊框元件
HexaZen 的 UI 風格不用圓角,而是用「倒角(chamfer)」——像寶石一樣的多邊形切割。
content\aquarium\hexazen\components\chamfer-border-card.vue
<template>
<div class="chamfer-4 p-1" :style="frameStyle">
<div class="bg-white chamfer-3.5 p-3" :class="props.contentClass">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { computed } from 'vue'
interface Props {
contentClass?: string;
borderSize?: string;
borderColor?: string;
}
const props = withDefaults(defineProps<Props>(), {
contentClass: '',
borderSize: '4px',
borderColor: '#e5e7eb',
})
const frameStyle = computed<CSSProperties>(() => ({
padding: props.borderSize,
backgroundColor: props.borderColor,
}))
</script>外層一個帶色的 chamfer 框,內層一個白色的 chamfer 框,用 padding 差異製造邊框效果。
倒角按鈕
按鈕也用 clip-path 做出八邊形倒角:
const style = computed<CSSProperties>(() => {
const size = height.value / 2
const size2 = (size / 3) * 2
return {
paddingInline: `${size * 1.1}px`,
clipPath: `polygon(
${size2}px 0,
calc(100% - ${size2}px) 0,
100% ${size}px,
100% calc(100% - ${size}px),
calc(100% - ${size2}px) 100%,
${size2}px 100%,
0 calc(100% - ${size}px),
0 ${size}px
)`,
}
})用 useElementSize 動態取得按鈕高度,讓倒角比例自動適配不同大小的按鈕。( •̀ ω •́ )✧
聲景混音器
每個音效都可以獨立調整音量,透過 Modal 呈現:
<template>
<div class="space-y-4 p-2">
<div
v-for="item in playerList"
:key="item.id"
class="flex items-center gap-3"
>
<u-icon
:name="soundscapeIconMap[item.type] ?? 'i-mingcute:music-2-fill'"
class="text-xl text-gray-500 shrink-0"
/>
<span class="text-sm text-gray-600 flex-1 capitalize">
{{ t(item.player.title as any) }}
</span>
<u-slider
:model-value="volumeMap[item.id] ?? 1"
:min="0"
:max="2"
:step="0.01"
class="flex-1"
@update:model-value="(value) => handleVolumeChange(item.id, value ?? 1)"
/>
</div>
</div>
</template>每個音效類型都有專屬 icon:
const soundscapeIconMap: Record<SoundscapeType, string> = {
rustle: 'i-mingcute:leaf-fill',
insect: 'i-mingcute:bug-fill',
bird: 'i-material-symbols:raven',
frog: 'i-fa7-solid:frog',
beast: 'i-mingcute:cat-fill',
river: 'i-material-symbols:water',
building: 'i-solar:buildings-2-bold',
ocean: 'i-lucide-lab:waves-birds',
alpine: 'i-mynaui:mountain-snow-solid',
rain: 'i-material-symbols:rainy',
campfire: 'i-mingcute:campfire-fill',
}音量範圍 0~2,也就是最大可以放大到原始音量的 2 倍。
只顯示目前正在播放(未被靜音)的音效,避免混亂。ԅ(´∀` ԅ)
公告欄 Modal
首次開啟會顯示歡迎頁面,包含 Intro 和 Quick Start 兩個分頁。
Quick Start 使用輪播展示操作步驟,搭配影片和圖片:
const quickStartItems = computed(() => {
const descriptionList = t('start')
const imgList = t('startImg')
return descriptionList.map((text, index) => ({
img: imgList[index] || '',
description: text,
}))
})有趣的是文字內容和圖片路徑都放在 i18n 翻譯檔中,可以根據語言顯示不同的教學內容。
輕量 i18n:不用 vue-i18n 的方案
因為 whyframe 環境的限制,只好自己做了一個輕量版的 i18n composable。
content\aquarium\hexazen\composables\use-simple-i18n.ts
export function useSimpleI18n<
Messages extends Record<DefaultLang, MessageSchema>,
DefaultLang extends 'zh-hant' | 'en',
Data = Messages[DefaultLang],
>(
messages: Messages & StrictCheck<Messages>,
options?: {
defaultLocale?: DefaultLang;
},
) {
const currentLocale = ref(
options?.defaultLocale ?? Object.keys(messages)[0],
) as Ref<DefaultLang>
const languages = usePreferredLanguages()
watch(
languages,
([lang]) => {
currentLocale.value = (
lang?.includes('zh') ? 'zh-hant' : 'en'
) as DefaultLang
},
{ immediate: true },
)
function t<Key extends keyof Data>(
key: Key,
params?: Record<string, string | number>,
): Data[Key] extends string[] ? string[] : string {
const value = getMessage(key) as any
if (Array.isArray(value))
return value
if (typeof value === 'string' && params) {
let text = value
Object.entries(params).forEach(([k, v]) => {
text = text.replace(new RegExp(`{${k}}`, 'g'), String(v))
})
return text
}
return value
}
return { locale: currentLocale, setLocale, t }
}型別安全的嚴格檢查
最精華的部分是 TypeScript 型別層面的檢查:
type StrictCheck<Messages> = {
[L in keyof Messages]: {
[K in KeyUniverse<Messages>]: CommonType<Messages, K>;
};
}StrictCheck 確保所有語言都必須有完全相同的 key 集合。
如果中文多了一個 key 但英文沒有,TypeScript 編譯時就會報錯。不用等到運行時才發現遺漏翻譯。╰(*´︶`*)╯
特色
- 自動偵測瀏覽器語言(
usePreferredLanguages) - 支援字串插值(
{name}參數替換) - 支援字串陣列(用於教學步驟等清單)
- 編譯時期型別檢查,缺漏翻譯直接報錯
- 不到 100 行程式碼,零外部依賴
總結 🐟
以上程式碼可以在此取得
- 倒角邊框風格統一整個應用的視覺語言
- 聲景混音器提供精細的個別音量控制
- 公告欄兼具歡迎頁和操作教學
- 自製輕量 i18n,編譯時期保證翻譯完整性
- 自動偵測瀏覽器語言,零設定
下一章是最後一篇,讓我們來聊聊 WebGPU 與效能優化。♪( ◜ω◝و(و
TIP
因為篇幅問題,某些部分簡單帶過。
若想看到更詳細的解釋,請不吝留言或寫信給我喔!(*´∀`)~♥