当前位置: 首页 > news >正文

从 PHP 到 AI + Golang,程序员自救转型手记(十二):前端状态商店、多语言初始化

这是一个系列 Blog,作者将以一个 PHP 全栈工程师的身份,利用 AI 工具(claude code、codex、deepseek、豆包等):从零开始学习 golang 语言,并最终完成 ai-go-mall(github | gitee)开源项目的制作,全程记录分享。

在上一期,我们已经完成 “前端工程初始化”,本期将完成:前端状态商店、多语言初始化

一些代码可直接从 BuildAdmin/web 复制,部分代码就不需要 AI 再次生成了,这里只简单整理一遍,对于初学者可以跟着 git 提交顺序和文档去理解项目架构。

状态商店初始化

pinia 注册

首先于 stores 文件夹建立 index.ts 用于初始化 pinia,并注册它的持久化插件pinia-plugin-persistedstate

// src\stores\index.tsimport{createPinia}from'pinia'importpiniaPluginPersistedstatefrom'pinia-plugin-persistedstate'constpinia=createPinia()pinia.use(piniaPluginPersistedstate)exportdefaultpinia

在 main.ts 中,将 pinia 注册为 vue 插件:

// src\main.tsimportpiniafrom'/@/stores/index'constapp=createApp(App)app.use(pinia)

第一个状态商店

我们已经设计好了服务端的管理员模型,此时不如直接将它对应的状态商店建好,后续管理员信息包括 token,都是直接使用 pinia 实现前端的持久化存储:

定义 interface

PS:项目设计了 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}from'pinia'import{ADMIN_INFO}from'/@/stores/constant/cacheKey'importtype{AdminInfo}from'/@/stores/interface'exportconstuseAdminInfo=defineStore('adminInfo',{state:():AdminInfo=>{return{id:0,username:'',nickname:'',avatar:'',last_login_at:'',last_login_ip:'',token:'',super:false,}},actions:{/** * 状态批量填充 * @param state 新状态数据 * @param [exclude=true] 是否排除某些字段(忽略填充),默认值 true 排除 token,传递 false 则不排除,还可传递 string[] 指定排除字段列表 */dataFill(state:Partial<AdminInfo>,exclude:boolean|string[]=true){if(exclude===true){exclude=['token']}elseif(exclude===false){exclude=[]}if(Array.isArray(exclude)){exclude.forEach((item)=>{deletestate[itemaskeyofAdminInfo]})}this.$patch(state)},setToken(token:string){this.token=token},removeToken(){this.token=''},},persist:{key:ADMIN_INFO,},})

多语言初始化

由于 BuildAdmin/web 的多语言是根据路由按需加载的,目前本项目不需要此功能,所以让 ai 来实现多语言,提示词如下:

  1. 使用已安装的 vue-i18n 依赖实现多语言功能,所有的语言包放置于 src/lang 目录下,分为中文和英文
  2. 当前打包工具为 vite,多语言功能需要实现语言包的懒加载
  3. 载入 element plus 的中英文语言包
  4. 无需实现 legacy 模式的逻辑,语言包支持加载子级目录和文件

结果

  1. 由于项目目前还没有建立 config 状态商店,所以它给建立了一个 locale 状态商店专门存储当前语言,并且写了setLocale之类的函数来设置语言,暂时将这些都去掉,使用固定的 zh-cn,后续做了 config 再动态化
  2. 它还写了一个 deepMerge 函数,实际上使用 lodash-es 的 merge 函数即可
  3. 语言包按需加载的核心逻辑是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 后缀constrelativePath=filePath.replace(`./${locale}/`,'').replace('.ts','')// index 作为顶层,其余按目录层级拆分if(relativePath==='index'){return[]}returnrelativePath.split('/')}

路径转嵌套 key 之前自己写过,当时 AI 还没出生,算了,回头用自己的,这么多年了,稳;加上这哥们刚刚往我项目建了 30 多个文件用于测试和示例,给我气笑了,幸好立项就配好了 git,此时此刻放弃所有工作区的更改,毫无疑问是最合理的选择;多语言初始化最终由作者使用古法编程手搓,核心代码如下:

先建立了config状态商店,使用config.lang.active存储当前激活语言,然后于App.vue配置好了element plus的多语言:

<template> <el-config-provider :value-on-clear="() => null" :locale="elLocale"> <router-view></router-view> </el-config-provider> </template> <script setup lang="ts"> 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: Record<string, 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}from'lodash-es'importtype{App}from'vue'import{createI18n}from'vue-i18n'import{useConfig}from'/@/stores/config'/** * 支持的语言类型 */exporttypeLangKey='zh-cn'|'en'/** * 支持的语言列表 */exportconstlangs:LangKey[]=['zh-cn','en']/** * 语言显示名称 */exportconstlangNames:Record<LangKey,string>={en:'English','zh-cn':'简体中文',}/** * i18n 实例 */consti18n=createI18n({legacy:false,locale:'zh-cn',fallbackLocale:'zh-cn',messages:{},})// 使用 vite import.meta.glob 批量导入 lang 目录下所有 .ts 文件(包括子目录)constlangGlobs:Record<LangKey,Record<string,()=>Promise<{default:any}>>>={en:import.meta.glob('./en/**/*.ts')asRecord<string,()=>Promise<{default:any}>>,'zh-cn':import.meta.glob('./zh-cn/**/*.ts')asRecord<string,()=>Promise<{default:any}>>,}/** * 设置 i18n,并为 vue 安装 i18n 插件 */exportasyncfunctionsetupI18n(app:App):Promise<void>{constconfig=useConfig()i18n.global.fallbackLocale.value=config.lang.fallback// 初始化当前语言包awaitsetLang(config.lang.active)app.use(i18n)}/** * 设置语言 * @param lang 语言标识 */exportasyncfunctionsetLang(lang:LangKey):Promise<void>{awaitloadMessages(lang)constconfig=useConfig()i18n.global.locale.value=lang config.setLang(lang)}/** * 懒加载语言包 * @param lang 语言标识 */exportasyncfunctionloadMessages(lang:LangKey):Promise<void>{// 如果已加载则跳过if(i18n.global.availableLocales.includes(lang)){return}try{// 批量加载 lang 目录下所有 .ts 文件constglob=langGlobs[lang]constpromises=Object.entries(glob).map(async([path,loader])=>{constmodule=awaitloader()return{path,default:module.default}})constmodules=awaitPromise.all(promises)// 按文件路径构建嵌套的 messages 结构constmergedMessages:Record<string,any>={}for(const{path,default:moduleData}ofmodules){if(typeofmoduleData!=='object'||moduleData===null){continue}constkeys=filePathToKeys(lang,path)if(keys.length===0){// 合并到顶层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}`constpathName=path.slice(path.lastIndexOf(langPathPrefix)+(langPathPrefix.length+1),path.lastIndexOf('.'))constkeys=pathName.split('/')// index.ts 作为顶层,其余按目录层级拆分if(keys.length===1&&keys[0]==='index'){return[]}returnkeys}exportdefaulti18n
http://www.gsyq.cn/news/1601246.html

相关文章:

  • 3个关键问题:SMUDebugTool如何彻底改变AMD Ryzen处理器的硬件调试体验?
  • Fortran开发实战:在VS2019与oneAPI环境中高效集成MKL库
  • AI Agent Runtime 重构:Session 作为事件日志的工程实践
  • 如何在macOS上安装微信防撤回插件:3分钟快速指南
  • Stateless 应用里的锁,SAP Fiori Draft 为什么把锁从 ABAP Session 里搬了出来
  • PCB拼板工艺全解析:从V-CUT到邮票孔的设计实战
  • AMD Ryzen终极调试指南:5步掌握硬件监控与系统优化
  • 城通网盘解析器:三步获取高速直连下载地址的终极指南
  • RA8D2 I3C总线错误检测与恢复机制实战指南
  • 如何打破音乐平台枷锁:Unlock Music Electron让你的加密音乐重获自由
  • 从零到一:解锁微软、领英与讯飞联袂的AI Prompt工程师认证攻略
  • DS4Windows终极指南:5步将PlayStation手柄完美适配Windows游戏
  • 3个OneMore功能彻底改变你的OneNote笔记体验[特殊字符]
  • DS4Windows终极指南:3步让PlayStation手柄在Windows上完美重生
  • 3步解锁原神成就管理:YaeAchievement从新手到高手的完整攻略
  • Lenovo Legion Toolkit:终极指南 - 如何完全掌控联想拯救者笔记本性能
  • Windows窗口置顶神器:彻底告别多任务切换烦恼的终极解决方案
  • Windows部署自动化终极指南:5大功能让你轻松绕过硬件限制
  • Cadence Allegro PCB设计88问解析(二十八) 之 Allegro中dimension environment参数详解与标注标准化实践
  • EPSON RX8900SA/CE 时钟芯片I2C驱动实战与避坑指南
  • 从ShuffleNet V1/V2到移动端部署:PyTorch实现与四条黄金准则的实战解析
  • Linux环境下Milvus向量数据库的部署与配置实战
  • Linux系统库目录探秘:从/lib到/libexec,如何为不同架构与应用场景正确配置库文件
  • 5步掌握AMD Ryzen处理器SMU调试工具:从入门到精通
  • Minibalance For Arduino:从零搭建PID调试可视化平台
  • 3个实战秘籍:用SMUDebugTool突破AMD Ryzen处理器性能瓶颈
  • ChatGPT:从Generative Pre-trained Transformer到智能对话革命
  • ThinkPHP漏洞检测工具配置与实战:从JDK11环境搭建到安全测试
  • 技术视角下的《二十年后》:从代码注释到架构设计的承诺与背叛
  • 华为GaussDB数据类型实战指南:从基础到高阶应用场景解析