feat(studio-ds): P2 i18n 脚手架——vue-i18n@10 + zh/en locales + 防闪烁早设脚本 + useAppearance(主题/语言持久化)

P2 of HJ-FE-DS-001 金样板。后续 P3 组件库 / P4 Feed+Create 落地。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-16 15:50:35 +00:00
parent 2cd9bff52c
commit ecae9025a9
7 changed files with 140 additions and 3 deletions

View File

@ -1,10 +1,21 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>game-studio</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>造梦AI</title>
<!-- 防闪烁(FOUC):首屏渲染前按已持久化外观设 <html data-theme/lang>,避免暗→亮闪烁 -->
<script>
(function () {
try {
var t = localStorage.getItem('studio.theme');
if (t === 'light' || t === 'dim') document.documentElement.setAttribute('data-theme', t);
var l = localStorage.getItem('studio.lang');
if (l === 'en-US' || l === 'zh-CN') document.documentElement.setAttribute('lang', l);
} catch (e) { /* 隐私模式忽略 */ }
})();
</script>
</head>
<body>
<div id="app"></div>

View File

@ -13,6 +13,7 @@
"pinia": "^3.0.4",
"vant": "^4.9.24",
"vue": "^3.5.34",
"vue-i18n": "^10.0.0",
"vue-router": "^4.6.4"
},
"devDependencies": {

View File

@ -0,0 +1,64 @@
/**
* + + <html data-theme/lang> + i18n locale
*
* index.html initAppearance() i18n
*/
import { ref } from 'vue'
import { i18n } from '../i18n'
export type ThemeMode = 'dark' | 'light' | 'dim'
export type LangCode = 'zh-CN' | 'en-US'
const THEME_KEY = 'studio.theme'
const LANG_KEY = 'studio.lang'
// 读持久化主题(隐私模式 localStorage 抛错时回落暗色)
function readTheme(): ThemeMode {
try {
const v = localStorage.getItem(THEME_KEY)
if (v === 'dark' || v === 'light' || v === 'dim') return v
} catch { /* 忽略 */ }
return 'dark'
}
function readLang(): LangCode {
try {
const v = localStorage.getItem(LANG_KEY)
if (v === 'zh-CN' || v === 'en-US') return v
} catch { /* 忽略 */ }
return 'zh-CN'
}
// 全局单例(模块级,跨组件共享同一引用)
const theme = ref<ThemeMode>(readTheme())
const lang = ref<LangCode>(readLang())
// 暗色为默认(:root不写 data-theme浅/柔写属性触发语义层覆盖
function applyTheme(t: ThemeMode): void {
const el = document.documentElement
if (t === 'dark') el.removeAttribute('data-theme')
else el.setAttribute('data-theme', t)
}
function applyLang(l: LangCode): void {
document.documentElement.setAttribute('lang', l)
i18n.global.locale.value = l
}
export function useAppearance() {
function setTheme(t: ThemeMode): void {
theme.value = t
try { localStorage.setItem(THEME_KEY, t) } catch { /* 忽略 */ }
applyTheme(t)
}
function setLang(l: LangCode): void {
lang.value = l
try { localStorage.setItem(LANG_KEY, l) } catch { /* 忽略 */ }
applyLang(l)
}
return { theme, lang, setTheme, setLang }
}
// main.ts 挂载前调用一次:把持久化外观应用到 DOM + i18n
export function initAppearance(): void {
applyTheme(theme.value)
applyLang(lang.value)
}

View File

@ -0,0 +1,18 @@
/**
* vue-i18n APIlegacy:false
* zh-CN 线 key zh-CN useAppearance.initAppearance()
* index.html
*/
import { createI18n } from 'vue-i18n'
import zhCN from '../locales/zh-CN'
import enUS from '../locales/en-US'
export const i18n = createI18n({
legacy: false, // 组合式 APIuseI18n / $t关闭 legacy 选项式
globalInjection: true, // 模板内可直接用 $t
locale: 'zh-CN',
fallbackLocale: 'zh-CN',
messages: { 'zh-CN': zhCN, 'en-US': enUS },
})
export default i18n

View File

@ -0,0 +1,17 @@
/**
* zh-CN key i18n zh-CN
*/
export default {
common: {
brand: 'ZaoMeng AI',
nav: { feed: 'Feed', create: 'Create', message: 'Inbox', me: 'Me', discover: 'Discover' },
action: { play: 'Play now', share: 'Share', save: 'Save', submit: 'Submit', cancel: 'Cancel', confirm: 'Confirm', back: 'Back', retry: 'Retry', more: 'More' },
state: { loading: 'Loading', empty: 'Nothing here yet', saved: 'Saved', failed: 'Failed', success: 'Success' },
status: { draft: 'Draft', review: 'In review', published: 'Published', rejected: 'Rejected' },
},
appearance: {
title: 'Appearance',
theme: 'Theme', themeDark: 'Dark', themeLight: 'Light', themeDim: 'Dim',
language: 'Language', langZh: '中文', langEn: 'English',
},
}

View File

@ -0,0 +1,18 @@
/**
* 线common / appearance
* P4 feed / create / profile
*/
export default {
common: {
brand: '造梦AI',
nav: { feed: '游戏流', create: '创作', message: '消息', me: '我的', discover: '发现' },
action: { play: '立即玩', share: '分享', save: '保存', submit: '提交', cancel: '取消', confirm: '确定', back: '返回', retry: '重试', more: '更多' },
state: { loading: '加载中', empty: '这里还没有内容', saved: '已保存', failed: '失败', success: '成功' },
status: { draft: '草稿', review: '审核中', published: '已发布', rejected: '已驳回' },
},
appearance: {
title: '外观',
theme: '主题', themeDark: '暗色', themeLight: '浅色', themeDim: '柔和',
language: '语言', langZh: '中文', langEn: 'English',
},
}

View File

@ -16,11 +16,19 @@ import App from './App.vue'
import router from './router'
import { pinia } from './store'
// —— i18n中英双语+ 外观(主题/语言)初始化 ——
import i18n from './i18n'
import { initAppearance } from './composables/useAppearance'
const app = createApp(App)
// 注册顺序Pinia状态→ Router路由→ VantUI 组件 + 指令)
app.use(pinia)
app.use(router)
app.use(Vant)
app.use(i18n)
// 挂载前应用持久化的主题/语言到 DOM 与 i18n防首屏不一致/闪烁)
initAppearance()
app.mount('#app')