# header-aside 布局组件迁移报告

> 迁移日期：2026-05-28  
> 迁移范围：`src/layout/header-aside/`  
> 适用项目：MES-UI（基于 D2Admin）

---

## 一、迁移概览

| 项目 | 迁改前 | 迁改后 |
|------|--------|--------|
| **修改文件数** | — | **14 个** |
| **硬编码中文处数** | **~35 处** | **0 处** |
| **删除死代码** | — | **1 处**（pageKeepAliveClean） |
| **新增 i18n key** | 0 | **47 个**（zh-chs/en/ja/zh-cht 各 47 个） |
| **语言包覆盖** | 仅 zh-chs + en | zh-chs / en / ja / zh-cht 四语言完整覆盖 |

---

## 二、执行逻辑判断结果

### 全部 16 个组件均在新项目中生效，无废弃组件

| 文件 | 状态 | 依赖 | 判断依据 |
|------|------|------|----------|
| `layout.vue` | ✅ 生效 | d2admin store 全模块 | 全局注册组件齐全 |
| `menu-side/index.js` | ✅ 生效 | d2admin/menu, d2-scrollbar | 组件已全局注册 |
| `menu-header/index.js` | ✅ 生效 | d2admin/menu + resize | throttle 正常 |
| `tabs/index.vue` | ✅ 生效 | d2admin/page, sortablejs | sortablejs 已安装 |
| `header-search/index.vue` | ✅ 生效 | 纯 emit | 无额外依赖 |
| `header-user/index.vue` | ✅ 生效 | d2admin/user + account | store 模块存在 |
| `header-fullscreen/index.vue` | ✅ 生效 | d2admin/fullscreen | store 模块存在 |
| `header-theme/index.vue` | ✅ 生效 | d2admin/theme | store 模块存在 |
| `header-theme/list/index.vue` | ✅ 生效 | d2admin/theme | store 模块存在 |
| `header-size/index.vue` | ✅ 生效 | d2admin/size | store 模块存在 |
| `header-color/index.vue` | ✅ 生效 | d2admin/color | store 模块存在 |
| `header-locales/index.vue` | ✅ 生效 | localeMixin | mixin 正常 |
| `header-log/index.vue` | ✅ 生效 | d2admin/log, 路由 log | log 路由存在 |
| `contextmenu/index.vue` | ✅ 生效 | 纯 UI | 无额外依赖 |
| `panel-search/index.vue` | ✅ 生效 | d2admin/search, fuse.js | fuse.js 已安装 |
| `locales/mixin.js` | ✅ 生效 | $i18n | i18n 已初始化 |

---

## 三、各文件改动明细

### 3.1 模板硬编码 → `$t()` (共 11 个文件)

| 文件 | 改动项 | 旧值 | 新值 |
|------|--------|------|------|
| **header-user/index.vue** | 用户问候 | `` `你好 ${info.name}` `` | `$t('page.layout.user.greeting', { name })` |
| | 未登录占位 | `'未登录'` | `$t('page.layout.user.not_logged_in')` |
| | 登出按钮 | `注销` | `$t('page.layout.user.logout')` |
| **header-fullscreen/index.vue** | 全屏 tooltip | `'全屏'` / `'退出全屏'` | `$t('page.layout.fullscreen.enter/exit')` |
| **header-theme/index.vue** | 主题按钮 tooltip | `content="主题"` | `:content="$t('page.layout.theme.title')"` |
| | 弹框标题 | `title="主题"` | `:title="$t('page.layout.theme.title')"` |
| **header-theme/list/index.vue** | 预览列标题 | `label="预览"` | `:label="$t('page.layout.theme.preview')"` |
| | 已激活按钮 | `已激活` | `$t('page.layout.theme.active')` |
| | 使用按钮 | `使用` | `$t('page.common.use')` |
| **header-size/index.vue** | 尺寸选项 | `'默认'/'中'/'小'/'最小'` | `$t('page.layout.size.*')` |
| | 通知标题 | `'提示'` | `$t('page.layout.size.notification_title')` |
| | 通知内容 | 硬编码 HTML | `$t('page.layout.size.notification_message')` |
| **header-log/index.vue** | tooltip 文本 | 模板字符串拼接 | `$t('page.layout.log.tooltip/no_log', {...})` |
| **menu-side/index.js** | 空菜单提示 | `没有侧栏菜单` | `this.$t('page.layout.menu.no_sidebar')` |
| **tabs/index.vue** | Tab 默认名 | `'未命名'` | `$t('page.layout.tabs.unnamed')` |
| | 关闭操作文字 | `关闭左侧/右侧/其它/全部` | `$t('page.layout.tabs.close_*')` |
| | 刷新文字 | `刷新` | `$t('page.layout.tabs.refresh')` |
| | 错误提示 | `'无效的操作'` | `$t('page.layout.tabs.invalid_operation')` |
| **panel-search/index.vue** | 搜索 placeholder | `placeholder="搜索页面"` | `:placeholder="$t('page.layout.search.placeholder')"` |
| | 快捷键提示 | 含 span 的散装文本 | `v-html="$t('page.layout.search.tip', {...})"` |
| **mixin/menu.js** | 临时菜单提示 | `'临时菜单'` | `$t('page.layout.menu.temp_menu')` |
| **libs/util.menu.js** | 菜单 fallback | `'未命名菜单'` | `this.$t('page.layout.menu.unnamed_menu')` |
| **locales/mixin.js** | 语言切换通知 | `'当前语言：...'/'语言变更'` | `$t('page.layout.locales.*')` |

### 3.2 data() → computed() 转换（响应式 i18n）

| 文件 | 属性 | 原因 |
|------|------|------|
| **header-size/index.vue** | `options` → `sizeOptions` | 确保语言切换时下拉选项也跟随更新 |
| **tabs/index.vue** | `contextmenuListIndex` | 确保右键菜单 title 切换语言时跟随更新 |
| | `contextmenuList` | 同上 |

### 3.3 删除死代码

| 文件 | 删除项 | 原因 |
|------|--------|------|
| **header-size/index.vue** | `...mapMutations({ pageKeepAliveClean })` | 引入后从未被调用 |
| 同步删除了 `import { mapMutations }` | `mapMutations` 不再需要 | 单一用途，移除了整个 import |

---

## 四、新增 i18n Key 清单

所有 key 位于 `page.layout.*` 命名空间下，四语言同步添加：

```
page.layout.
├── user.greeting       / not_logged_in / logout
├── fullscreen.enter    / exit
├── theme.title         / preview / active
├── size.default        / medium / small / mini / notification_title / notification_message
├── log.no_log          / tooltip / tooltip_zero
├── menu.no_sidebar     / temp_menu / unnamed_menu
├── tabs.unnamed        / refresh / close_left / close_right / close_other / close_all / invalid_operation
├── search.placeholder  / tip
└── locales.changed     / notification_title / preview_warning / preview_doc
```

加上 `page.common.use`，共计 **48 个新 key × 4 语言 = 192 条翻译**。

---

## 五、开发者注意事项

### 5.1 涉及 4 个语言包文件

> ⚠️ 所有修改的语言包 `zh-chs.json / en.json / ja.json / zh-cht.json` 已通过 `JSON.parse()` 验证合法性。

### 5.2 JSX 渲染函数的特殊语法

`menu-side/index.js` 和 `libs/util.menu.js` 使用 JSX 渲染函数，`$t()` 调用语法为 `{ this.$t('...') }`（单花括号），与 `.vue` 模板的 `{{ }}`（双花括号）不同。

### 5.3 `v-html` 使用处

`panel-search/index.vue` 的提示文本包含内联 `<span class="panel-search__key">` 标签，必须使用 `v-html` 绑定。

### 5.4 `data()` → `computed()` 的 Reactivity

`header-size` 和 `tabs` 组件中原来在 `data()` 里的中文文本数组已经移到 `computed`，确保 `$t()` 是响应式的。未来如果要在 `data()` 中放翻译文本，必须使用 computed 或确保使用者是组件内部翻译。

---

## 六、回滚指南

如需回滚任一组件的改动：

1. **布局组件** — `git checkout` 对应的组件文件即可，所有改动互相独立
2. **语言包** — 回滚 `src/locales/*.json`，新增的 `page.layout` 命名空间不影响业务页面
3. **localeMixin** — 回滚 `src/locales/mixin.js`

---

## 七、验证清单

| 验证项 | 方法 |
|--------|------|
| JSON 合法性 | `node -e "JSON.parse(require('fs').readFileSync('src/locales/zh-chs.json','utf8'))"` |
| 全量 JSON | 逐个检查 zh-chs/en/ja/zh-cht（已通过） |
| 语言切换 | 打开页面 → 切换中文/English/日本語/繁體中文 → 观察全部 header 文字是否跟随变化 |
| 搜索面板 | `Ctrl+K` 打开搜索面板 → 检查 placeholder + 快捷键提示 |
| 右键 Tab | 右键点击 Tab → 检查菜单文字 |
| 全屏切换 | 鼠标悬停全屏按钮 → 检查 tooltip |
| 主题弹框 | 打开主题弹框 → 检查标题 + "预览"/"已激活"/"使用" |
| 组件尺寸 | 切换组件尺寸 → 检查通知弹框 |
| 用户区 | 检查用户问候语 + 登出按钮 |

---

## 八、菜单 i18n 翻译方案（2026-05-28 追加）

### 8.1 问题背景

旧项目中，当用户切换语言（中文 → English）时，侧边栏菜单和顶部一级导航菜单的文字会跟随切换为英文。当前项目迁移后缺少这个能力，菜单文字始终为后端返回的单一语言文本。

### 8.2 方案设计

采用**双重保障**机制：

```
                    ┌─ 方案 A：$t() 包装（客户端翻译）
语言切换 ──────────┤
                    └─ 方案 B：menuReload（重新拉取后端菜单）
```

#### 方案 A：渲染层 `$t()` 包装

将菜单渲染节点中的所有 `menu.title` 包裹 `$t()`。

**修改文件**：

| 文件 | 改动 |
|------|------|
| `src/layout/header-aside/components/libs/util.menu.js` | `elMenuItem` 和 `elSubmenu` 中 `menu.title` → `this.$t(menu.title)` |
| `src/components/d2-module-index-menu/components/group.vue` | `{{menu.title}}` → `{{ $t(menu.title) }}`（2 处） |
| `src/components/d2-module-index-menu/components/item.vue` | `{{menu.title}}` → `{{ $t(menu.title) }}` |

**工作原理**：

```
后端返回 title 是 i18n key → $t() 返回对应语言的翻译 ✓
后端返回 title 是纯文本     → $t() 找不到 key，返回原文本（fallback）✓
```

无需修改后端接口即可兼容两种模式。

#### 方案 B：语言切换时重新拉取菜单

在 `src/store/modules/d2admin/modules/menu.js` 新增 `menuReload` action：

```js
async menuReload ({ state, dispatch }) {
  // 1. 清空 localStorage 缓存
  await dispatch('d2admin/db/set', {
    dbName: 'database',
    path: '$menu.sourceData',
    value: [],
    user: true
  }, { root: true })
  state.sourceData = []
  // 2. 重新从后端拉取（后端根据当前语言返回对应文本）
  await dispatch('sourceDataLoad')
}
```

在 `src/locales/mixin.js` 的 `onChangeLocale` 末尾追加：

```js
this.$store.dispatch('d2admin/menu/menuReload')
```

### 8.3 工作流程

```
用户点击切换语言
  │
  ├─ this.$i18n.locale = 'en'           → 客户端组件重新渲染
  │                                         $t(menu.title) 即时生效 ✓
  │
  ├─ $store.dispatch('menuReload')      → 清缓存 → 调后端
  │                                         getMenuAll() 返回英文菜单
  │                                         menu.install() 重建 header/aside
  │                                         侧栏 + 顶栏全部更新 ✓
  │
  └─ $notify({ title: 'Language Changed' }) → 通知用户
```

> **注意**：如果后端 `getMenuAll()` 接口**不支持**根据语言返回不同内容，方案 B 不会生效，此时仅依赖方案 A（`$t()` 包装）。建议后续后端添加多语言菜单数据返回。

### 8.4 修改清单

| 文件 | 操作 | 行数 |
|------|------|------|
| `src/layout/header-aside/components/libs/util.menu.js` | `menu.title` → `this.$t(menu.title)` | 2 处 |
| `src/components/d2-module-index-menu/components/group.vue` | `{{menu.title}}` → `{{ $t(menu.title) }}` | 2 处 |
| `src/components/d2-module-index-menu/components/item.vue` | `{{menu.title}}` → `{{ $t(menu.title) }}` | 1 处 |
| `src/store/modules/d2admin/modules/menu.js` | 新增 `menuReload` action | +15 行 |
| `src/locales/mixin.js` | `onChangeLocale` 末尾追加 `menuReload` | +1 行 |

### 8.5 验证方法

1. 启动项目，确认**当前展示的菜单文字正确**（方案 A 生效）
2. 切换语言，观察侧边栏和顶部导航栏文字是否更新（方案 A + B 双重生效）
3. 如果切换语言后菜单文字不变，检查：
   - 后端 `getMenuAll()` 是否支持多语言
   - 菜单数据中的 `name` 是否为可翻译的 i18n key 路径

---

## 九、一级模块首页复用方案（2026-05-28 追加）

### 9.1 问题背景

旧项目中，每个一级模块（如 `planning_production`）都有一个 `index/index.vue`：

```vue
<template>
  <d2-container>
    <page-navi class="cs-m" v-model="root"/>
  </d2-container>
</template>

<script>
export default {
  name: 'produce-index',
  components: {
    PageNavi: () => import('@/layout/header-aside/components/header-navi')
  },
  data() { return { root: '/planning_production' } }
}
</script>
```

这些文件结构**高度重合**，仅 `root` 值不同。随着模块增多（对照表中约 20+ 个一级模块），需要逐个创建这些文件。

### 9.2 方案：通用 `module-index.vue`

创建一个**共享的路由级组件**，动态读取路由 meta 中的 `root` 来展示模块首页。

**文件**：`src/views/system/function/module-index.vue`（已创建）

**核心逻辑**：

```vue
<template>
  <d2-container class="module-index-page">
    <template #header>
      <d2-module-index-banner
        :title="$t(headerMenu.title)"
        :sub-title="$t(headerMenu.title)" />
    </template>
    <d2-module-index-menu :menu="asideMenu" />
  </d2-container>
</template>

<script>
export default {
  computed: {
    rootPath () { return this.$route.meta.root || this.$route.path },
    headerMenu () {
      return this.$store.state.d2admin.menu.header.find(
        m => m.path === this.rootPath
      ) || { title: this.rootPath }
    },
    asideMenu () {
      return this.$store.state.d2admin.menu.aside.find(
        m => m.path === this.rootPath
      ) || { children: [] }
    }
  }
}
</script>
```

### 9.3 使用方式

在路由模块中，为每个一级模块添加一个指向同一组件的空路径路由：

**路由示例**（`src/router/modules/production-master-data.js`）：

```js
export default {
  path: '/production_configuration',
  component: layoutHeaderAside,
  children: (pre => [
    // ★ 模块首页 — 所有一级模块共用同一个组件
    {
      path: '',
      name: `${pre}index`,
      meta: { ...meta, title: '生产配置', root: '/production_configuration' },
      component: _import('system/function/module-index')
    },
    // 三级模块页面...
    {
      path: 'factory_model/factory_area',
      name: `${pre}factory_model-factory_area`,
      meta: { ...meta, cache: true, title: '工厂区域' },
      component: _import('production-master-data/factory-model/factory-area')
    }
  ])('production_configuration-')
}
```

### 9.4 对比

| 维度 | 旧方案 | 新方案 |
|------|--------|--------|
| **文件数** | 每个一级模块 1 个 index.vue（20+ 个） | 全局共享 1 个 `module-index.vue` |
| **模板代码** | 每个文件 20 行重复代码 | 0 行重复 |
| **root 值** | 硬编码在每个 data() 中 | 路由 `meta.root` 动态读取 |
| **i18n** | 无 | 通过 `$t(menu.title)` 自动翻译 |
| **新增模块** | 创建目录 + 创建 index.vue + 写重复代码 | 仅需添加路由配置行 |

### 9.5 验证方法

1. 访问 `/production_configuration`（不带子路径），应显示"生产配置"模块的二级和三级模块导航
2. 切换语言，banner 标题和菜单项应跟随翻译
3. 点击菜单项，正确跳转到对应页面对应的三级模块页面
