diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dd002f5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1.0.0] - 2026-01-26 + +### Added +- 实现前端国际化基础架构,包括: + - 引入 vue-i18n@9 核心库,支持 Vue 3 Composition API + - 建立标准化的资源目录结构(src/locales/) + - 创建中文和英文翻译文件(zh-CN.ts、en-US.ts) + - 封装通用的语言切换组件(LangSelect) + - 配置网络拦截器自动注入 Accept-Language 请求头 + - 集成 Element Plus 的国际化支持(ElConfigProvider) + +### Changed +- `src/store/modules/app.ts` - 新增 language 状态字段和 setLanguage action +- `src/layout/components/Navbar/index.vue` - 集成 LangSelect 组件到导航栏 +- `src/utils/request.ts` - 在请求拦截器中注入 Accept-Language 请求头 +- `src/App.vue` - 集成 ElConfigProvider 组件,支持 Element Plus 国际化 +- `src/main.ts` - 注册 i18n 插件 + +### Dependencies +- 新增 vue-i18n@9 diff --git a/docs/changelog/2026-01-26-002-01-changelog.md b/docs/changelog/2026-01-26-002-01-changelog.md new file mode 100644 index 0000000..cb4e2e5 --- /dev/null +++ b/docs/changelog/2026-01-26-002-01-changelog.md @@ -0,0 +1,57 @@ +# 变更日志 + +## 元数据 +- 需求编号:2026-01-26-002-01 +- 创建时间:2026-01-26 +- 创建人:SSOT 架构师 +- 版本号:v1.0.0 + +## 变更概述 +实现前端国际化基础架构,包括引入 vue-i18n@9 核心库、建立标准化的资源目录结构、封装通用的语言切换组件以及配置网络拦截器自动注入语言标识。此变更奠定了前端国际化的基础,为后续业务模块国际化提供底层支持。 + +## 变更内容 + +### 新增功能 +- 实现前端国际化基础架构,包括: + - 引入 vue-i18n@9 核心库,支持 Vue 3 Composition API + - 建立标准化的资源目录结构(src/locales/) + - 创建中文和英文翻译文件(zh-CN.ts、en-US.ts) + - 封装通用的语言切换组件(LangSelect) + - 配置网络拦截器自动注入 Accept-Language 请求头 + - 集成 Element Plus 的国际化支持(ElConfigProvider) + +### 新增文档 +- [需求文档](../requirements/2026-01-26-002-01-前端国际化-基础架构.md) +- [设计文档](../design/2026-01-26-002-01-前端国际化-基础架构-设计.md) +- [决策记录](../decisions/2026-01-26-002-01-ADR-前端国际化技术选型与架构决策.md) +- [提示词](../prompts/2026-01-26-002-01-prompt-前端国际化-基础架构.md) +- [会话记录](../sessions/2026-01-26-002-01-session.md) + +### 新增代码文件 +- `src/locales/index.ts` - i18n 核心配置文件 +- `src/locales/zh-CN.ts` - 中文翻译文件 +- `src/locales/en-US.ts` - 英文翻译文件 +- `src/components/LangSelect/index.vue` - 语言选择器组件 + +### 修改功能 +- `src/store/modules/app.ts` - 新增 language 状态字段和 setLanguage action +- `src/layout/components/Navbar/index.vue` - 集成 LangSelect 组件到导航栏 +- `src/utils/request.ts` - 在请求拦截器中注入 Accept-Language 请求头 +- `src/App.vue` - 集成 ElConfigProvider 组件,支持 Element Plus 国际化 +- `src/main.ts` - 注册 i18n 插件 + +### 新增依赖 +- vue-i18n@9 - Vue 3 国际化核心库 + +## 影响范围 +- 模块:前端国际化基础架构 +- 功能:语言切换、国际化资源管理、网络请求适配 +- 文件:5 个新文件,5 个修改文件 +- 依赖:新增 1 个 npm 包(vue-i18n@9) + +## 相关文档 +- [需求文档](../requirements/2026-01-26-002-01-前端国际化-基础架构.md) +- [设计文档](../design/2026-01-26-002-01-前端国际化-基础架构-设计.md) +- [决策记录](../decisions/2026-01-26-002-01-ADR-前端国际化技术选型与架构决策.md) +- [提示词](../prompts/2026-01-26-002-01-prompt-前端国际化-基础架构.md) +- [会话记录](../sessions/2026-01-26-002-01-session.md) diff --git a/docs/decisions/2026-01-26-002-01-ADR-前端国际化技术选型与架构决策.md b/docs/decisions/2026-01-26-002-01-ADR-前端国际化技术选型与架构决策.md new file mode 100644 index 0000000..7ea033e --- /dev/null +++ b/docs/decisions/2026-01-26-002-01-ADR-前端国际化技术选型与架构决策.md @@ -0,0 +1,101 @@ +# ADR-001: 前端国际化技术选型与架构决策 + +## 状态 +已接受 + +## 日期 +2026-01-26 + +## 背景 +前端国际化基础架构需要选择合适的技术方案,满足以下要求: +1. 与 Vue 3 项目技术栈深度集成 +2. 支持 Composition API 开发模式 +3. 与若依框架的 Pinia 状态管理和 Axios 请求拦截器无缝集成 +4. 提供良好的用户体验和开发体验 +5. 支持未来扩展(动态资源加载、多语言列表等) + +## 决策 + +### 决策 1:选择 vue-i18n@9 作为核心国际化库 +**选择方案**:vue-i18n@9(Composition API 模式) + +**选择理由**: +1. **官方推荐**:Vue 3 官方推荐的国际化解决方案,与 Vue 3 生态深度集成 +2. **Composition API 支持**:完美支持 Vue 3 的 Composition API,符合项目开发规范 +3. **TypeScript 支持**:提供完整的 TypeScript 类型定义,确保类型安全 +4. **社区活跃**:社区活跃度高,文档丰富,问题解决成本低 +5. **性能优秀**:支持懒加载和按需加载,性能表现优秀 +6. **Element Plus 兼容**:与 Element Plus 的国际化方案兼容良好 + +### 决策 2:使用 Pinia 进行状态管理 +**选择方案**:利用现有 Pinia 状态管理(在 app.ts 中新增 language 状态) + +**选择理由**: +1. **现有技术栈**:项目已使用 Pinia 作为状态管理方案,无需引入新的依赖 +2. **与若依框架一致**:若依框架使用 Pinia,保持技术栈一致性 +3. **开发效率高**:开发人员熟悉 Pinia,学习成本低 +4. **性能优秀**:Pinia 性能优于 Vuex,支持 TypeScript +5. **集成简单**:与 Vue 3 Composition API 集成良好 + +### 决策 3:采用静态资源为主、预留动态资源的混合策略 +**选择方案**:初期使用静态资源文件(zh-CN.ts、en-US.ts),预留动态资源加载接口 + +**选择理由**: +1. **快速交付**:静态资源实现简单,可快速完成基础架构搭建 +2. **性能优秀**:静态资源无需网络请求,加载速度快 +3. **稳定性高**:不依赖后端接口,稳定性更高 +4. **可扩展性**:预留动态资源加载接口,未来可无缝升级 +5. **渐进式增强**:符合渐进式增强原则,先实现基础功能,再逐步完善 + +### 决策 4:统一使用 zh-CN 格式(连字符) +**选择方案**:前端统一使用 `zh-CN` 格式(连字符),后端接口已确认支持 + +**选择理由**: +1. **国际标准**:符合 RFC 4646 语言标签标准(BCP 47) +2. **浏览器兼容**:浏览器原生支持 `zh-CN` 格式的 `Accept-Language` 请求头 +3. **后端确认**:后端 API 文档确认支持 `zh-CN` 格式 +4. **一致性**:与 Element Plus 的 locale 命名规范一致 +5. **可维护性**:统一格式减少混淆和错误 + +## 后果 + +### 正面影响 +1. **开发效率高**:与现有技术栈深度集成,开发人员熟悉,学习成本低 +2. **类型安全**:TypeScript 类型定义完整,减少运行时错误 +3. **性能优秀**:静态资源加载、Pinia 状态管理、懒加载等策略确保性能 +4. **可维护性强**:统一的技术栈和规范,代码可维护性强 +5. **可扩展性好**:预留动态资源加载接口,未来可无缝升级 +6. **用户体验好**:语言切换流畅,页面刷新后保持语言设置 +7. **社区支持好**:vue-i18n 和 Pinia 社区活跃,问题解决成本低 + +### 负面影响 +1. **初期工作量**:需要创建静态资源文件和封装组件,有一定初期工作量 +2. **格式转换**:后端接口可能返回 `zh_CN` 格式,需要在前端层进行格式转换 +3. **动态资源延迟**:未来接入动态资源时,需要额外的网络请求和缓存策略 +4. **Element Plus 适配**:需要维护 `langCode` 到 `ElementLocale` 的映射关系 + +## 替代方案 + +### 方案 2:使用 vue-i18n@8(Legacy 模式) +- **优点**:兼容 Vue 2,迁移成本低 +- **缺点**:不支持 Composition API,不符合项目开发规范,性能不如 v9 +- **适用场景**:Vue 2 项目迁移场景 + +### 方案 3:使用 Vuex 进行状态管理 +- **优点**:社区成熟,文档丰富 +- **缺点**:与项目现有技术栈不一致,性能不如 Pinia,不支持 TypeScript +- **适用场景**:Vue 2 项目或已有 Vuex 的项目 + +### 方案 4:完全使用动态资源加载 +- **优点**:资源集中管理,易于维护和更新 +- **缺点**:依赖后端接口,稳定性差,性能不如静态资源,开发复杂度高 +- **适用场景**:需要频繁更新翻译资源的大型项目 + +### 方案 5:使用 zh_CN 格式(下划线) +- **优点**:与后端接口返回格式一致,无需转换 +- **缺点**:不符合国际标准,浏览器兼容性差,与 Element Plus 不一致 +- **适用场景**:后端强制要求使用下划线格式的场景 + +## 相关文档 +- [需求文档](../requirements/2026-01-26-002-01-前端国际化-基础架构.md) +- [设计文档](../design/2026-01-26-002-01-前端国际化-基础架构-设计.md) diff --git a/docs/design/2026-01-26-002-01-前端国际化-基础架构-设计.md b/docs/design/2026-01-26-002-01-前端国际化-基础架构-设计.md new file mode 100644 index 0000000..0bd19e4 --- /dev/null +++ b/docs/design/2026-01-26-002-01-前端国际化-基础架构-设计.md @@ -0,0 +1,305 @@ +# 前端国际化-基础架构设计文档 + +## 元数据 +- **需求编号**:2026-01-26-002-01 +- **父需求**:2026-01-26-001-前端国际化总需求 +- **创建时间**:2026-01-26 +- **创建人**:SSOT 架构师 +- **状态**:已完成 +- **优先级**:P0 (最高) + +## 设计概述 +本设计方案负责搭建前端国际化的基础架构,包括引入 vue-i18n 核心库、建立标准化的资源目录结构、封装通用的语言切换组件以及配置网络拦截器自动注入语言标识。设计遵循 Vue 3 Composition API 规范,与若依框架的 Pinia 状态管理和 Axios 请求拦截器深度集成,为后续业务模块国际化提供底层支持。 + +## 架构设计 + +### 1. 系统架构图 +```mermaid +graph TB + A[用户界面] --> B[LangSelect 组件] + B --> C[Pinia Store - app.ts] + C --> D[LocalStorage - language] + C --> E[i18n 实例 - locales/index.ts] + E --> F[静态资源 - zh-CN.ts/en-US.ts] + C --> G[Axios 拦截器 - request.ts] + G --> H[后端 API - Accept-Language Header] + C --> I[DOM 属性 - document.documentElement.lang] + J[App.vue] --> K[ElConfigProvider] + C --> K +``` + +### 2. 模块架构 +``` +src/ +├── locales/ # 国际化资源目录(新增) +│ ├── index.ts # i18n 实例创建与导出 +│ ├── zh-CN.ts # 中文静态资源 +│ └── en-US.ts # 英文静态资源 +├── components/ +│ └── LangSelect/ # 语言切换组件(新增) +│ └── index.vue +├── store/ +│ └── modules/ +│ └── app.ts # 新增 language 状态和 setLanguage action +├── utils/ +│ └── request.ts # 修改拦截器注入 Accept-Language +├── layout/ +│ └── components/ +│ └── Navbar/ +│ └── index.vue # 集成 LangSelect 组件 +├── main.ts # 注册 i18n 插件 +└── App.vue # ElConfigProvider 包裹 +``` + +### 3. 数据流设计 +**初始化流程**: +``` +应用启动 -> main.ts -> 创建 i18n 实例 -> 读取 localStorage.language -> 设置默认语言 -> app.mount +``` + +**语言切换流程**: +``` +用户点击 LangSelect -> appStore.setLanguage(lang) -> 更新 state.language -> 更新 localStorage.language -> 更新 i18n.global.locale.value -> 更新 document.documentElement.lang +``` + +**API 请求流程**: +``` +发起请求 -> Axios 请求拦截器 -> 读取 appStore.language -> 注入 headers['Accept-Language'] -> 发送请求 +``` + +## 技术方案 + +### 1. 技术选型 +| 技术组件 | 版本要求 | 选择理由 | +| :--- | :--- | :--- | +| vue-i18n | ^9.x | Vue 3 官方国际化库,支持 Composition API | +| Pinia | 现有版本 | 项目现有状态管理方案,与若依框架集成 | +| Axios | 现有版本 | 项目现有 HTTP 客户端,拦截器机制成熟 | +| Element Plus | 现有版本 | 项目现有 UI 组件库,需配置 locale | +| TypeScript | 现有版本 | 项目现有类型系统,确保类型安全 | + +### 2. 核心技术实现 + +#### 2.1 vue-i18n 初始化配置 +在 `src/locales/index.ts` 中创建 i18n 实例: +- 默认语言:`zh-CN` +- 回退语言:`zh-CN` +- 模式:`legacy: false`(Composition API) +- 合并策略:静态资源作为基础,后续可扩展动态资源 + +#### 2.2 状态管理集成 +在 `src/store/modules/app.ts` 中: +- 新增 state:`language: string` +- 新增 action:`setLanguage(language: string)` +- 持久化:使用 `localStorage.setItem('language', language)` +- 副作用:同步更新 `i18n.global.locale.value` 和 `document.documentElement.lang` + +#### 2.3 网络拦截器增强 +在 `src/utils/request.ts` 的请求拦截器中: +- 引入 `useAppStore` +- 获取当前语言:`const lang = appStore.language` +- 注入 Header:`config.headers['Accept-Language'] = lang` +- 格式约束:统一使用 `zh-CN` 格式(连字符),后端已确认支持 + +#### 2.4 Element Plus 适配 +在 `src/App.vue` 中: +- 使用 `ElConfigProvider` 包裹 `router-view` +- 动态绑定 `locale` 属性 +- 维护 `langCode` 到 `ElementLocale` 的映射关系 + +### 3. 组件设计方案 + +#### 3.1 LangSelect 组件 +**组件路径**:`src/components/LangSelect/index.vue` + +**技术规范**: +- 使用 ` + + diff --git a/src/layout/components/Navbar/index.vue b/src/layout/components/Navbar/index.vue index 0e3432f..efb4708 100644 --- a/src/layout/components/Navbar/index.vue +++ b/src/layout/components/Navbar/index.vue @@ -8,6 +8,7 @@ import RuoYiDoc from './RuoYi/Doc/index.vue' import Hamburger from '@/components/Hamburger/index.vue' import Screenfull from '@/components/Screenfull/index.vue' import SizeSelect from '@/components/SizeSelect/index.vue' +import LangSelect from '@/components/LangSelect/index.vue' import HeaderSearch from '@/components/HeaderSearch/index.vue' import useAppStore from '@/store/modules/app' import useUserStore from '@/store/modules/user' @@ -70,6 +71,10 @@ function setLayout() { + + + + diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts new file mode 100644 index 0000000..86f9fe9 --- /dev/null +++ b/src/locales/en-US.ts @@ -0,0 +1,31 @@ +export default { + common: { + confirm: 'Confirm', + cancel: 'Cancel', + save: 'Save', + delete: 'Delete', + edit: 'Edit', + add: 'Add', + search: 'Search', + reset: 'Reset', + submit: 'Submit', + back: 'Back', + close: 'Close', + loading: 'Loading...', + success: 'Operation successful', + error: 'Operation failed', + warning: 'Warning', + info: 'Info' + }, + navbar: { + language: 'Language', + logout: 'Logout', + fullscreen: 'Fullscreen', + exitFullscreen: 'Exit Fullscreen', + size: 'Layout Size', + theme: 'Theme', + settings: 'Layout Settings', + dashboard: 'Dashboard', + profile: 'Profile' + } +} diff --git a/src/locales/index.ts b/src/locales/index.ts new file mode 100644 index 0000000..688270e --- /dev/null +++ b/src/locales/index.ts @@ -0,0 +1,15 @@ +import { createI18n } from 'vue-i18n' +import zhCN from './zh-CN' +import enUS from './en-US' + +const i18n = createI18n({ + legacy: false, + locale: localStorage.getItem('language') || 'zh-CN', + fallbackLocale: 'zh-CN', + messages: { + 'zh-CN': zhCN, + 'en-US': enUS + } +}) + +export default i18n diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts new file mode 100644 index 0000000..d025497 --- /dev/null +++ b/src/locales/zh-CN.ts @@ -0,0 +1,31 @@ +export default { + common: { + confirm: '确认', + cancel: '取消', + save: '保存', + delete: '删除', + edit: '编辑', + add: '新增', + search: '搜索', + reset: '重置', + submit: '提交', + back: '返回', + close: '关闭', + loading: '加载中...', + success: '操作成功', + error: '操作失败', + warning: '警告', + info: '提示' + }, + navbar: { + language: '语言选择', + logout: '退出登录', + fullscreen: '全屏', + exitFullscreen: '退出全屏', + size: '布局大小', + theme: '主题', + settings: '布局设置', + dashboard: '首页', + profile: '个人中心' + } +} diff --git a/src/main.ts b/src/main.ts index 0724c96..e5528eb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,7 +25,8 @@ import store from './store' // 引入状态管理 import plugins from './plugins' // 引入插件 import directive from './directive' // 引入指令 import compomemts from './components' // 引入全局组件 -app.use(router).use(store).use(plugins).use(directive).use(compomemts) +import i18n from './locales/index' // 引入国际化 +app.use(router).use(store).use(plugins).use(directive).use(compomemts).use(i18n) import VForm3 from '@lib/vform/designer.umd.js' //引入VForm 3库 import '@lib/vform/designer.style.css' //引入VForm3样式 diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts index 4761748..7890c41 100644 --- a/src/store/modules/app.ts +++ b/src/store/modules/app.ts @@ -1,4 +1,5 @@ import { defineStore } from 'pinia'; +import i18n from '@/locales/index'; const useAppStore = defineStore( 'app', @@ -10,7 +11,8 @@ const useAppStore = defineStore( hide: false }, device: 'desktop', - size: (localStorage.getItem('size') || 'default') as 'large' | 'default' | 'small' + size: (localStorage.getItem('size') || 'default') as 'large' | 'default' | 'small', + language: localStorage.getItem('language') || 'zh-CN' }), actions: { toggleSideBar(withoutAnimation: boolean) { @@ -39,6 +41,14 @@ const useAppStore = defineStore( }, toggleSideBarHide(status: boolean) { this.sidebar.hide = status + }, + setLanguage(language: string) { + this.language = language + localStorage.setItem('language', language) + if (i18n.global.locale.value !== language) { + i18n.global.locale.value = language + } + document.documentElement.lang = language } } }) diff --git a/src/utils/request.ts b/src/utils/request.ts index 5699464..8ecc276 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -6,6 +6,7 @@ import { tansParams, blobValidate } from '@/utils/ruoyi' import cache from '@/plugins/cache' import { saveAs } from 'file-saver' import useUserStore from '@/store/modules/user' +import useAppStore from '@/store/modules/app' import { router } from '@/router' let downloadLoadingInstance: any; @@ -33,6 +34,10 @@ service.interceptors.request.use((config) => { // 让每个请求携带自定义token 请根据实际情况自行修改 config.headers['Authorization'] = 'Bearer ' + getToken() } + // 设置语言标识 + const appStore = useAppStore() + const lang = appStore.language + config.headers['Accept-Language'] = lang // get请求映射params参数 if (config.method === 'get' && config.params) { let url = config.url + '?' + tansParams(config.params);