# 页面开发规则

## 默认页面形态

新增业务页面优先使用：

- Vue 3 `script setup lang="ts"`。
- Element Plus 布局组件。
- `@gushen/gushen-common-components` 的表格、查询栏、上传等组件。
- `src/api/<module>/index.ts` 管理请求。
- `src/lang/modules/<module>/{zh-cn,en-us,zh-tw}.ts` 管理文案。

维护已有旧页面时，可以保留 Options API；新增页面不要主动复制旧页面里的 `console.log`、`@ts-nocheck`、硬编码文案和隐式 any。

## 列表页

推荐结构：

```text
src/views/<module>/<page>/
├── index.vue
├── components/
│   └── <可选子组件>.vue
└── config/
    └── gridConfig.ts
```

列表页默认使用 `GsCustomizeTable` / `<gs-table>`：

- 查询按钮放在表格 `#btn` 或页面顶部查询区。
- 列配置单独抽到 `config/gridConfig.ts`，复杂列通过 `slotArr` 开启插槽。
- 列表请求函数放在 `src/api/<module>/<page>/index.ts`。
- 成功判断用 `reqIsSucceed`，不要写死单一成功码，除非维护旧接口已有固定协议。

## 表单页

推荐结构：

```text
src/views/<module>/<page>/
├── index.vue
└── components/
    └── BasicForm.vue
```

表单规则：

- 使用 `el-form`、`el-row`、`el-col` 组织字段。
- 新增/编辑共用表单组件，通过 `disabled` 或 `readonly` 控制只读。
- 保存入参按后端要求包裹 `{ params: formData }`。
- 表单初始化、回显映射、保存映射拆成小函数。
- 复杂弹窗或选择器优先封装为独立子组件，不要在主页面堆大量弹窗逻辑。

## 流程页面

流程表单模板统一从 `page-template-index.md` 选择，不再以当前工程旧页面作为模板入口。

流程页面需要保留方法契约：

- `get_process_variables()`：返回流程变量。
- `ap_save()`：保存草稿，返回 Promise。
- 包装页通过 props 接收 `isSubmit`、`editStatus`、`bpm_businessKey`、`taskServiceParams`、`curTaskName`。

## 动态表单页

参考：`src/views/dynamic-form/index.vue`。

- 当前页面只挂载 `<FormRender ref="formRenderRef" />`。
- 使用 IntersectionObserver 在页面可见时向事件总线发送当前路由信息。
- 使用前确认 `@gushen/gushen-form-render` 插件是否已在 `src/core/initMain.ts` 注册。

## 路由与菜单

- 普通业务页默认由后台菜单动态加载，只需确保页面路径存在。
- 菜单 `menuUrl` / `originalAddr` 会在 `src/permission.ts` 中映射到 `@/views${componentPath}/index.vue`。
- 例如后台菜单路径 `/test-menu/demo1` 对应 `src/views/test-menu/demo1/index.vue`。
- 静态壳内页面才补 `src/router/index.ts`。
- 新增路由 meta 至少包含 `title`、`enTitle`、`noCache`、`hideInMenu`、`isBootstrap`。
- 动态表单菜单通过 `parameter.menu_type === 'DYNAMIC_FORM'` 进入固定 `FormRender` 容器，一般不新建手写业务页。

## 样式

- 页面根节点使用唯一 class，例如 `<section class="xxx-page">`。
- 页面级样式使用 `<style scoped lang="scss">`。
- 不要把布局高度写死到固定 px；优先使用 flex 和 `min-height: 0`。

## 生成后自检

- 页面能被路由加载。
- API 路径、请求方法、参数包装与现有接口一致。
- 表格远程刷新、分页、勾选、操作列能闭环。
- 用户可见文案进入语言包。
- 无新增 TypeScript / Vue 诊断。

## 常见问题与修复指南

以下是开发过程中已验证的高频问题及修复方案，生成页面时务必逐项检查。

### 1. gs-table 操作列不显示

**现象**：表格渲染正常，但操作列（`prop: 'action'`）和自定义内容列（如状态 Tag）显示空白。

**根因**：`GsTable` 组件的 `#cell` 插槽仅在列的 `editable: true` 时才渲染。源码逻辑：

```ts
// GsTable/index.vue
if (type === "cell") {
    const { editable } = column || {};
    return editable === true;
}
```

**修复**：需要自定义内容渲染的列必须加 `editable: true`：

```ts
{ prop: 'forecastStatus', label: '状态', editable: true },
{ prop: 'action', label: '操作', searchable: false, editable: true },
```

### 2. gs-table 数据请求方式选择

**原则**：优先使用项目中已验证的 `:columns` + `:options` + `:remote` 模式（参照 `mdmCustomer`），而非 `GsCustomizeTable` 的 `:grid-config` + `:query-params` 模式。

**原因**：两种组件的数据响应格式不同：
- `GsCustomizeTable` 内部期望 `res.data.data` 是行数组 + `res.data.count` 是总数
- `:remote` 模式由开发者自行处理响应格式转换，兼容性更好

```html
<gs-table
  ref="tableRef"
  :columns="columns"
  :options="tableOptions"
  :remote="remoteData"
>
  <template #header>
    <el-button type="primary" @click="openAddDialog">新增</el-button>
  </template>
  <template #cell="{ column, row }">
    <template v-if="column.prop === 'action'">
      <!-- 操作按钮 -->
    </template>
  </template>
</gs-table>
```

`remoteData` 函数必须返回 `{ data: rows[], total: number, number: pageIndex, size: pageRows }` 格式：

```ts
async function remoteData(otherParams: any = {}) {
  const { number, size, params } = otherParams || {}
  const res = await findPagination({ pageIndex: number, pageRows: size, params })
  if (res.code === 800) {
    const d = res.data || {}
    return { data: d.rows || [], number, size, total: Number(d.total) || 0 }
  }
  return { data: [], number, size, total: 0 }
}
```

### 3. Mock 模式下 GET 请求的查询参数丢失

**现象**：详情页打开后空白，控制台报 `Cannot read properties of undefined (reading 'data')`。

**根因**：axios 的 `params` 配置项在所有请求拦截器执行**之后**才拼接到 URL。Mock 拦截器运行时 URL 上没有查询参数，导致 mock router 提取不到 `id` 等参数。

```ts
// ❌ 错误：params 在拦截器之后拼接，mock 拿不到 id
export function getById(id: number) {
  return request({ url: '/api/forecast/get-id', method: 'get', params: { id } })
}

// ✅ 正确：参数直接拼在 URL 上
export function getById(id: number) {
  return request({ url: `/api/forecast/get-id?id=${id}`, method: 'get' })
}
```

**适用范围**：所有 GET 请求的 mock API 都必须将参数直接拼入 URL 字符串，不能依赖 axios 的 `params` 配置。

### 4. 语言包 key 结构与 i18n 引用不匹配

**现象**：页面上显示原始 key 名（如 `forecast-order.listTitle`）而非翻译文案。

**根因**：语言包模块通过 `import.meta.glob` 自动加载，文件夹名作为模块 key（如 `forecast-order`）。`$t('forecast-order.listTitle')` 按**嵌套对象路径**解析，而非扁平 key。语言包必须使用嵌套对象结构：

```ts
// ✅ 正确：嵌套对象，$t('forecast-order.status.DRAFT') 可以解析
export default {
  status: { DRAFT: '草稿', SUBMITTED: '已提交' },
  msg: { saveSuccess: '保存成功' },
}

// ❌ 错误：扁平 key，$t('forecast-order.status.DRAFT') 无法解析为 'forecast-order'['status']['DRAFT']
export default {
  'status.DRAFT': '草稿',
  'msg.saveSuccess': '保存成功',
}
```

**自检清单**：
- 所有 `$t('forecast-order.xxx')` 引用能在语言包中找到对应嵌套路径
- 特别注意模板字符串中的动态 key：`` $t(`forecast-order.status.${val}`) ``
- 语言包有 `{ immediate: true }` 的 watch 不能遗漏语言初始化时机

### 5. mock 菜单路由出现双斜杠 `//`

**现象**：点击左侧菜单跳转报错，URL 变成 `//forecast-order`。

**根因**：`permission.ts` 的 `generatePath` 函数拼接路由时 `parentPath=''`，结果为 `'' + '/' + '/forecast-order'` = `'//forecast-order'`。去双斜杠逻辑需要 `!item.originalAddr` 为 `true` 才生效。

**修复**：mock 菜单中 `originalAddr` 设为 `null`，不需要该字段：

```ts
export const mockMenus = [
  {
    menuId: 1001,
    menuCode: 'forecast-order',
    menuUrl: '/forecast-order',
    menuName: '销售预测',
    originalAddr: null,  // ← 关键：不能设为非空值
    children: [],
    // ...
  },
]
```

### 6. drawer/dialog 组件 v-model 循环同步

**现象**：弹窗打开后空白，或者关闭后无法再打开。

**根因**：手动用 `ref` + `watch` 同步 `props.visible ↔ internal visible` 加上 `el-drawer v-model` 内部也有自己的响应式逻辑，造成循环触发或事件丢失。

**修复**：直接用 `:model-value` + `@update:model-value` 模式，不创建内部 visible ref：

```html
<el-drawer
  :model-value="props.visible"
  @update:model-value="onVisibleChange"
>
```

```ts
function onVisibleChange(val: boolean) {
  if (!val) { /* 清理状态 */ }
  emit('update:visible', val)
}
```

### 7. 硬编码文案遗漏

**现象**：表单校验错误提示、ElMessage 弹窗消息、el-drawer 标题等始终显示中文，不随语言切换变化。

**检查范围**：
- `el-form` 的 `:rules` 中 `message` 字段
- `ElMessage.success/warning/error()` 调用
- `ElMessageBox.confirm()` 的 `title`、`message`
- 弹窗/抽屉的 `:title` 属性
- 表格列配置中 `placeholder`、`searchOptions.loop` 的 `label`
- `'是'/'否'`、`'确定'/'取消'` 等通用词

**修复**：以上所有位置统一使用 `t('module.key')` 或 `$t('module.key')`。需要先在语言包中定义对应 key。

