从 PHP 到 AI + Golang,程序员自救转型手记(十二):前端状态商店、多语言初始化 这是一个系列 Blog作者将以一个 PHP 全栈工程师的身份利用 AI 工具claude code、codex、deepseek、豆包等从零开始学习 golang 语言并最终完成 ai-go-mallgithub | gitee开源项目的制作全程记录分享。在上一期我们已经完成 “前端工程初始化”本期将完成前端状态商店、多语言初始化一些代码可直接从 BuildAdmin/web 复制部分代码就不需要 AI 再次生成了这里只简单整理一遍对于初学者可以跟着 git 提交顺序和文档去理解项目架构。状态商店初始化pinia 注册首先于 stores 文件夹建立 index.ts 用于初始化 pinia并注册它的持久化插件pinia-plugin-persistedstate// src\stores\index.tsimport{createPinia}frompiniaimportpiniaPluginPersistedstatefrompinia-plugin-persistedstateconstpiniacreatePinia()pinia.use(piniaPluginPersistedstate)exportdefaultpinia在 main.ts 中将 pinia 注册为 vue 插件// src\main.tsimportpiniafrom//stores/indexconstappcreateApp(App)app.use(pinia)第一个状态商店我们已经设计好了服务端的管理员模型此时不如直接将它对应的状态商店建好后续管理员信息包括 token都是直接使用 pinia 实现前端的持久化存储定义 interfacePS项目设计了 src\stores\interface 文件夹专门存放状态商店的所有 interface为了避免文件数量过多项目自带的 interface都尽量按分类放在已有的 ts 文件中而不是建立很多文件开发者自己的可以单独建立 ts 文件存放比如自建 src\stores\interface\cms.ts 存放 cms 相关的 interface如下的 adminInfo 接口是管理员模型定义的精简版后续还需要额外的字段可以再加// src\stores\interface\index.tsexportinterfaceAdminInfo{id:numberusername:stringnickname:stringavatar:stringlast_login_at:stringlast_login_ip:stringtoken:string// 是否是 superAdmin用于判定是否显示超管级按钮不做任何权限判断super:boolean}adminInfo 状态商店// src\stores\adminInfo.tsimport{defineStore}frompiniaimport{ADMIN_INFO}from//stores/constant/cacheKeyimporttype{AdminInfo}from//stores/interfaceexportconstuseAdminInfodefineStore(adminInfo,{state:():AdminInfo{return{id:0,username:,nickname:,avatar:,last_login_at:,last_login_ip:,token:,super:false,}},actions:{/** * 状态批量填充 * param state 新状态数据 * param [excludetrue] 是否排除某些字段忽略填充默认值 true 排除 token传递 false 则不排除还可传递 string[] 指定排除字段列表 */dataFill(state:PartialAdminInfo,exclude:boolean|string[]true){if(excludetrue){exclude[token]}elseif(excludefalse){exclude[]}if(Array.isArray(exclude)){exclude.forEach((item){deletestate[itemaskeyofAdminInfo]})}this.$patch(state)},setToken(token:string){this.tokentoken},removeToken(){this.token},},persist:{key:ADMIN_INFO,},})多语言初始化由于 BuildAdmin/web 的多语言是根据路由按需加载的目前本项目不需要此功能所以让 ai 来实现多语言提示词如下使用已安装的 vue-i18n 依赖实现多语言功能所有的语言包放置于 src/lang 目录下分为中文和英文当前打包工具为 vite多语言功能需要实现语言包的懒加载载入 element plus 的中英文语言包无需实现 legacy 模式的逻辑语言包支持加载子级目录和文件结果由于项目目前还没有建立 config 状态商店所以它给建立了一个 locale 状态商店专门存储当前语言并且写了setLocale之类的函数来设置语言暂时将这些都去掉使用固定的 zh-cn后续做了 config 再动态化它还写了一个 deepMerge 函数实际上使用 lodash-es 的 merge 函数即可语言包按需加载的核心逻辑是en: () import(./en)然后await en()据我所知在 Vite 中这种方式是支持不了子目录的只把en文件夹里边的所有 ts 文件加载到了比如en/test.ts但en/test/test.ts加载不到经过测试也确实如此将整理出来的结果直接作为提示词发给 cc新的一轮中语言包加载改为了import.meta.glob(./zh-cn/**/*.ts)看起来没问题了然后它又写了一个将文件路径转换为嵌套的 key 路径函数如下/** * 将文件路径转换为嵌套的 key 路径 * 例如./zh-cn/test/test1.ts → [test, test1] * ./zh-cn/common.ts → [common] * ./zh-cn/index.ts → []顶层合并 */functionfilePathToKeys(locale:AppLocale,filePath:string):string[]{// 去掉语言目录前缀和 .ts 后缀constrelativePathfilePath.replace(./${locale}/,).replace(.ts,)// index 作为顶层其余按目录层级拆分if(relativePathindex){return[]}returnrelativePath.split(/)}路径转嵌套 key 之前自己写过当时 AI 还没出生算了回头用自己的这么多年了稳加上这哥们刚刚往我项目建了 30 多个文件用于测试和示例给我气笑了幸好立项就配好了 git此时此刻放弃所有工作区的更改毫无疑问是最合理的选择多语言初始化最终由作者使用古法编程手搓核心代码如下先建立了config状态商店使用config.lang.active存储当前激活语言然后于App.vue配置好了element plus的多语言template el-config-provider :value-on-clear() null :localeelLocale router-view/router-view /el-config-provider /template script setup langts import elEn from element-plus/es/locale/lang/en import elZhCn from element-plus/es/locale/lang/zh-cn import { computed } from vue import { useConfig } from //stores/config const config useConfig() const elLocales: Recordstring, typeof elEn { en: elEn, zh-cn: elZhCn, } const elLocale computed(() elLocales[config.lang.active] || elLocales[config.lang.fallback]) /script项目的其他多语言加载核心逻辑如下// src\lang\index.ts// 实现了语言包懒加载、子目录语言包加载、当前语言动态修改函数import{merge,set}fromlodash-esimporttype{App}fromvueimport{createI18n}fromvue-i18nimport{useConfig}from//stores/config/** * 支持的语言类型 */exporttypeLangKeyzh-cn|en/** * 支持的语言列表 */exportconstlangs:LangKey[][zh-cn,en]/** * 语言显示名称 */exportconstlangNames:RecordLangKey,string{en:English,zh-cn:简体中文,}/** * i18n 实例 */consti18ncreateI18n({legacy:false,locale:zh-cn,fallbackLocale:zh-cn,messages:{},})// 使用 vite import.meta.glob 批量导入 lang 目录下所有 .ts 文件包括子目录constlangGlobs:RecordLangKey,Recordstring,()Promise{default:any}{en:import.meta.glob(./en/**/*.ts)asRecordstring,()Promise{default:any},zh-cn:import.meta.glob(./zh-cn/**/*.ts)asRecordstring,()Promise{default:any},}/** * 设置 i18n并为 vue 安装 i18n 插件 */exportasyncfunctionsetupI18n(app:App):Promisevoid{constconfiguseConfig()i18n.global.fallbackLocale.valueconfig.lang.fallback// 初始化当前语言包awaitsetLang(config.lang.active)app.use(i18n)}/** * 设置语言 * param lang 语言标识 */exportasyncfunctionsetLang(lang:LangKey):Promisevoid{awaitloadMessages(lang)constconfiguseConfig()i18n.global.locale.valuelang config.setLang(lang)}/** * 懒加载语言包 * param lang 语言标识 */exportasyncfunctionloadMessages(lang:LangKey):Promisevoid{// 如果已加载则跳过if(i18n.global.availableLocales.includes(lang)){return}try{// 批量加载 lang 目录下所有 .ts 文件constgloblangGlobs[lang]constpromisesObject.entries(glob).map(async([path,loader]){constmoduleawaitloader()return{path,default:module.default}})constmodulesawaitPromise.all(promises)// 按文件路径构建嵌套的 messages 结构constmergedMessages:Recordstring,any{}for(const{path,default:moduleData}ofmodules){if(typeofmoduleData!object||moduleDatanull){continue}constkeysfilePathToKeys(lang,path)if(keys.length0){// 合并到顶层merge(mergedMessages,moduleData)}else{// 子模块 — 按路径嵌套merge(mergedMessages,set({},keys,moduleData))}}i18n.global.setLocaleMessage(lang,mergedMessages)}catch(error){console.error(Failed to load lang:${lang},error)}}constfilePathToKeys(lang:LangKey,path:string){constlangPathPrefix/${lang}constpathNamepath.slice(path.lastIndexOf(langPathPrefix)(langPathPrefix.length1),path.lastIndexOf(.))constkeyspathName.split(/)// index.ts 作为顶层其余按目录层级拆分if(keys.length1keys[0]index){return[]}returnkeys}exportdefaulti18n