Files
mes-ui-d2/docs/表格组件使用说明.md
sheng a61036e5dc
Some checks failed
Release pipeline / publish (push) Has been cancelled
Release pipeline / Always run job (push) Has been cancelled
feat: 新增角色管理模块,优化API与交互体验
1.  新增角色管理后台页面、路由与国际化文案
2.  重构API请求错误处理逻辑,统一拦截业务与HTTP错误
3.  新增确认弹窗组合式函数,区分取消与请求错误场景
4.  完善表格按钮权限与显示控制逻辑
5.  更新API参数规范与文档说明
6.  修复部分页面分页数据解析问题
2026-05-28 19:16:05 +08:00

42 KiB
Raw Blame History

表格组件使用说明

基于 page-table + page-dialog-form 的新一代 CRUD 表格方案。
源码位置:src/components/page-table/src/components/page-dialog-form/src/composables/
完整可运行示例:src/views/production-master-data/factory-model/factory-area/index.vue


目录

  1. 快速开始(三步走)
  2. 完整示例代码
  3. 组件 API 参考
  4. Composable API 参考
  5. i18n 国际化方案
  6. 常用场景速查
  7. 路由配置
  8. API 文件写法
  9. 旧代码迁移对照
  10. 接口请求错误处理规范
  11. 常见问题排查

1. 快速开始(三步走)

第一步:定义列和按钮(created() 中)

import { useTableColumns } from '@/composables/useTableColumns'
import { useTableButtons } from '@/composables/useTableButtons'
import { i18nMixin } from '@/composables/useI18n'

export default {
  mixins: [i18nMixin('page.模块名.二级模块.三级模块')],

  created () {
    // --- 列定义 ---
    // 只需声明 prop 和 labelidx 自动补齐
    // prop: '_actions' 约定为操作列,自动渲染 rowButtons
    this.columns = useTableColumns([
      { prop: 'sort', label: this.key('sort'), width: 80 },
      { prop: 'code', label: this.key('code'), minWidth: 120 },
      { prop: 'name', label: this.key('name') },
      { prop: '_actions', label: this.key('operation'), width: 160, fixed: 'right' }
    ])

    // --- 按钮定义 ---
    // 不再分开写 buttonList / tableButtonList
    const btns = useTableButtons({
      toolbar: [
        { key: 'add', label: this.key('add'), icon: 'el-icon-plus',
          type: 'primary', auth: '/xxx/create', onClick: this.openAdd }
      ],
      row: [
        { key: 'edit', label: this.key('edit'), icon: 'el-icon-edit',
          auth: '/xxx/edit', onClick: this.openEdit },
        { key: 'delete', label: this.key('delete'), icon: 'el-icon-delete',
          color: 'danger', auth: '/xxx/delete', onClick: this.handleDelete }
      ]
    }, this.$permission) // 第二个参数传入权限校验函数
    this.toolbarButtons = btns.toolbarButtons
    this.rowButtons = btns.rowButtons
  }
}

第二步:写模板(<template> 中)

<template>
  <d2-container>
    <!-- 搜索区 -->
    <template #header>
      <el-form :inline="true" size="mini">
        <el-form-item :label="$t(key('code'))">
          <el-input v-model="search.code" :placeholder="$t(key('enter_code'))"
                    clearable style="width:200px" @keyup.enter.native="onSearch" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-search" @click="onSearch">
            {{ $t(key('search')) }}
          </el-button>
          <el-button icon="el-icon-refresh" @click="onReset">
            {{ $t(key('reset')) }}
          </el-button>
        </el-form-item>
      </el-form>
    </template>

    <!-- 表格 + 按钮栏 + 分页 -->
    <page-table
      :columns="columns"
      :data="tableData"
      :loading="loading"
      :toolbar-buttons="toolbarButtons"
      :row-buttons="rowButtons"
      :pagination="pagination"
      help-url="/help/your-page"
      :help-text="$t(ckey('help'))"
      auto-height
      @page-change="onPageChange"
      @selection-change="onSelect"
    />

    <!-- 新增/编辑弹框 -->
    <page-dialog-form
      ref="dialogForm"
      :visible.sync="dialogVisible"
      :title="dialogTitle"
      width="35%"
      :form-cols="formCols"
      :form-data="formData"
      :rules="rules"
      :submitting="submitting"
      :confirm-text="key('confirm')"
      :cancel-text="key('cancel')"
      @submit="onDialogSubmit"
      @close="onDialogClose"
    />
  </d2-container>
</template>

第三步:写业务方法(增删改查)

methods: {
  // 获取列表数据
  async fetchData () {
    this.loading = true
    try {
      const res = await getList({
        ...this.search,
        page_no: this.pagination.current,
        page_size: this.pagination.size
      })
      this.tableData = res.data || []
      this.pagination.total = res.count || 0
    } finally { this.loading = false }
  },

  // 搜索 / 重置
  onSearch () { this.pagination.current = 1; this.fetchData() },
  onReset () { this.search = { code: '', name: '' }; this.pagination.current = 1; this.fetchData() },

  // 分页变化
  onPageChange (page) {
    this.pagination.current = page.current
    this.pagination.size = page.size
    this.fetchData()
  },

  // 新增:打开弹窗
  openAdd () {
    this.handleType = 'create'
    this.dialogTitle = this.key('add_title')
    this.$nextTick(() => {
      this.$refs.dialogForm.reset()
      this.resetForm()
      this.dialogVisible = true
    })
  },

  // 编辑:回填表单
  openEdit (row) {
    this.handleType = 'edit'
    this.dialogTitle = this.key('edit_title')
    this.editId = row.id
    this.formData = { code: row.code, name: row.name }
    this.dialogVisible = true
  },

  // 提交表单
  async onDialogSubmit () {
    this.submitting = true
    try {
      if (this.handleType === 'create') {
        await createApi(this.formData)
      } else {
        await editApi({ ...this.formData, id: this.editId })
      }
      this.$message.success(this.$t(this.key('operation_success')))
      this.dialogVisible = false
      this.fetchData()
    } finally { this.submitting = false }
  },

  // 关闭弹窗
  onDialogClose () { this.resetForm() },

  // 删除
  async handleDelete (row) {
    try {
      await this.$confirm(
        this.$t(this.key('confirm_delete')),
        this.$t(this.key('tip')),
        { confirmButtonText: this.$t(this.key('confirm')),
          cancelButtonText: this.$t(this.key('cancel')),
          type: 'warning', closeOnClickModal: false }
      )
      await deleteApi({ id: [row.id] })
      this.$message.success(this.$t(this.key('operation_success')))
      this.fetchData()
    } catch (e) { /* 取消或失败 */ }
  }
}

2. 完整示例代码

📁 src/views/production-master-data/factory-model/factory-area/index.vue
这是一个可直接运行的完整 CRUD 页面,包含搜索栏、表格、分页、新增/编辑弹框、删除确认。

模板部分

<template>
  <d2-container>
    <template #header>
      <div class="search-bar">
        <el-form :inline="true" size="mini">
          <el-form-item :label="$t(key('code'))">
            <el-input v-model="search.code" :placeholder="$t(key('enter_code'))"
                      clearable style="width:200px" @keyup.enter.native="onSearch" />
          </el-form-item>
          <el-form-item :label="$t(key('name'))">
            <el-input v-model="search.name" :placeholder="$t(key('enter_name'))"
                      clearable style="width:200px" @keyup.enter.native="onSearch" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" icon="el-icon-search" @click="onSearch">
              {{ $t(key('search')) }}
            </el-button>
            <el-button icon="el-icon-refresh" @click="onReset">
              {{ $t(key('reset')) }}
            </el-button>
          </el-form-item>
        </el-form>
      </div>
    </template>

    <page-table
      ref="pageTable"
      :columns="columns"
      :data="tableData"
      :loading="loading"
      :toolbar-buttons="toolbarButtons"
      :row-buttons="rowButtons"
      :pagination="pagination"
      help-url="/help/factory-area"
      :help-text="$t(ckey('help'))"
      auto-height
      @page-change="onPageChange"
      @selection-change="onSelect"
    />

    <page-dialog-form
      ref="dialogForm"
      :visible.sync="dialogVisible"
      :title="dialogTitle"
      width="35%"
      :form-cols="formCols"
      :form-data="formData"
      :rules="rules"
      :submitting="submitting"
      :confirm-text="key('confirm')"
      :cancel-text="key('cancel')"
      @submit="onDialogSubmit"
      @close="onDialogClose"
    />
  </d2-container>
</template>

Script 部分

import { useTableColumns } from '@/composables/useTableColumns'
import { useTableButtons } from '@/composables/useTableButtons'
import { i18nMixin } from '@/composables/useI18n'
import { getFactoryAreaList, createFactoryArea, editFactoryArea, deleteFactoryArea }
  from '@/api/production-master-data/factory-area'
import PageTable from '@/components/page-table'
import PageDialogForm from '@/components/page-dialog-form'

export default {
  name: 'factory-area',
  components: { PageTable, PageDialogForm },
  mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],

  data () {
    return {
      loading: false,
      submitting: false,
      tableData: [],
      selectedRows: [],
      dialogVisible: false,
      dialogTitle: '',
      editId: '',
      handleType: 'create',
      search: { code: '', name: '' },
      pagination: { current: 1, size: 10, total: 0 },
      formData: { code: '', name: '', remark: '' },
      rules: {
        code: [
          { required: true, message: this.key('enter_code'), trigger: 'blur' },
          { min: 1, max: 100, message: this.key('remark_length'), trigger: 'blur' }
        ],
        name: [
          { required: true, message: this.key('enter_name'), trigger: 'blur' },
          { min: 1, max: 100, message: this.key('remark_length'), trigger: 'blur' }
        ]
      },
      columns: [],
      toolbarButtons: [],
      rowButtons: [],
      formCols: [
        [
          { type: 'input', prop: 'code',
            label: this.key('code'), placeholder: this.key('enter_code'),
            clearable: true, style: { width: '90%' } }
        ],
        [
          { type: 'input', prop: 'name',
            label: this.key('name'), placeholder: this.key('enter_name'),
            clearable: true, style: { width: '90%' } }
        ],
        [
          { type: 'input', prop: 'remark', inputType: 'textarea',
            autosize: { minRows: 2, maxRows: 6 },
            label: this.key('remark'), placeholder: this.key('remark_required'),
            clearable: true, style: { width: '90%' } }
        ]
      ]
    }
  },

  created () {
    this.columns = useTableColumns([
      { prop: 'sort', label: this.key('sort'), width: 80 },
      { prop: 'code', label: this.key('code'), minWidth: 120 },
      { prop: 'name', label: this.key('name'), minWidth: 120 },
      { prop: 'remark', label: this.key('remark') },
      { prop: '_actions', label: this.key('operation'), width: 160, fixed: 'right' }
    ])
    const btns = useTableButtons({
      toolbar: [
        { key: 'add', label: this.key('add'), icon: 'el-icon-plus', type: 'primary',
          auth: '/production_master_data/factory_model/factory_area/create',
          onClick: this.openAdd }
      ],
      row: [
        { key: 'edit', label: this.key('edit'), icon: 'el-icon-edit',
          auth: '/production_master_data/factory_model/factory_area/edit',
          onClick: this.openEdit },
        { key: 'delete', label: this.key('delete'), icon: 'el-icon-delete',
          color: 'danger',
          auth: '/production_master_data/factory_model/factory_area/delete',
          onClick: this.handleDelete }
      ]
    }, this.$permission)
    this.toolbarButtons = btns.toolbarButtons
    this.rowButtons = btns.rowButtons
    this.fetchData()
  },

  methods: {
    async fetchData () {
      this.loading = true
      try {
        const res = await getFactoryAreaList({
          ...this.search, page_no: this.pagination.current, page_size: this.pagination.size
        })
        this.tableData = res.data || []
        this.pagination.total = res.count || 0
      } finally { this.loading = false }
    },

    onSearch () { this.pagination.current = 1; this.fetchData() },
    onReset () { this.search = { code: '', name: '' }; this.pagination.current = 1; this.fetchData() },
    onPageChange (page) {
      this.pagination.current = page.current
      this.pagination.size = page.size
      this.fetchData()
    },
    onSelect (rows) { this.selectedRows = rows },

    resetForm () { this.formData = { code: '', name: '', remark: '' }; this.editId = '' },

    openAdd () {
      this.handleType = 'create'
      this.dialogTitle = this.key('add_title')
      this.$nextTick(() => {
        this.$refs.dialogForm && this.$refs.dialogForm.reset()
        this.resetForm()
        this.dialogVisible = true
      })
    },

    openEdit (row) {
      this.handleType = 'edit'
      this.dialogTitle = this.key('edit_title')
      this.editId = row.id
      this.formData = { code: row.code, name: row.name, remark: row.remark || '' }
      this.dialogVisible = true
    },

    async onDialogSubmit () {
      this.submitting = true
      try {
        if (this.handleType === 'create') {
          await createFactoryArea(this.formData)
        } else {
          await editFactoryArea({ ...this.formData, id: this.editId })
        }
        this.$message.success(this.$t(this.key('operation_success')))
        this.dialogVisible = false
        this.fetchData()
      } finally { this.submitting = false }
    },

    onDialogClose () { this.resetForm() },

    async handleDelete (row) {
      try {
        await this.$confirm(
          this.$t(this.key('confirm_delete')),
          this.$t(this.key('tip')),
          { confirmButtonText: this.$t(this.key('confirm')),
            cancelButtonText: this.$t(this.key('cancel')),
            type: 'warning', closeOnClickModal: false }
        )
        await deleteFactoryArea({ id: [row.id] })
        this.$message.success(this.$t(this.key('operation_success')))
        this.pagination.current = Math.min(
          this.pagination.current,
          Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
        )
        this.fetchData()
      } catch (e) { /* 取消删除 / 请求失败不处理 */ }
    }
  }
}

3. 组件 API 参考

3.1 page-table — 表格 + 按钮栏 + 分页

Props

Prop 类型 默认值 说明
columns Array [] 列定义,由 useTableColumns() 生成
data Array [] 表格行数据
loading Boolean false 是否显示 loading 遮罩
height String/Number 表格高度,传 'auto' 启用自适应
auto-height Boolean false 启用高度自适应,填满可用空间
border Boolean true 是否带边框
row-key String 'id' 行唯一 key
toolbar-buttons Array [] 顶部工具栏按钮,由 useTableButtons() 生成
row-buttons Array [] 行内操作按钮,由 useTableButtons() 生成
pagination Object null 分页参数 { current, size, total }
table-attrs Object {} 额外透传给 el-table 的属性
table-listeners Object {} 额外透传给 el-table 的事件
help-url String '' 帮助文档 URL传了显示问号按钮
help-text String '帮助' 帮助按钮文字,支持 i18n key

事件

事件名 参数 说明
@page-change { current, size, total } 分页变化
@selection-change rows: Array 选中行变化

插槽

插槽名 作用域 说明
#col-{prop} { row, index } 自定义列渲染(列需设 slot: true
#toolbar-extra 工具栏追加内容
#empty 空数据占位
#append 表格末尾追加
#extra 页面底部追加

列定义规范

// 普通列
{ prop: 'code', label: '编码', minWidth: 120 }

// 操作列约定prop === '_actions'
{ prop: '_actions', label: '操作', width: 160, fixed: 'right' }

// 自定义插槽列slot: true
{ prop: 'status', label: '状态', slot: true, width: 100 }

// 复选框 + 序号列(通过 useTableColumns 第二个参数)
useTableColumns([...], { selectionWidth: 55, indexWidth: 60 })

3.2 page-dialog-form — 增删改查弹框

Props

Prop 类型 默认值 说明
visible Boolean false 弹框显隐,使用 .sync
title String '' 弹框标题,支持 i18n key
width String '35%' 弹框宽度
form-cols Array [] 表单字段结构(二维数组)
form-data Object {} 表单数据对象
rules Object {} 校验规则,与 el-form rules 一致
label-width String '100px' label 宽度
submitting Boolean false 提交 loading 状态
confirm-text String '确定' 确定按钮文字,支持 i18n key
cancel-text String '取消' 取消按钮文字,支持 i18n key

事件

事件名 说明
@submit 表单验证通过后触发
@close 弹框关闭后触发

方法(通过 ref 调用)

方法 说明
reset() 重置表单
validate() 手动验证,返回 Promise<boolean>

formCols 数据结构

传入 i18n keythis.key() 拼接完整 key组件内部自动翻译且跟随语言切换

formCols: [
  [
    { type: 'input', prop: 'code',
      label: this.key('code'), placeholder: this.key('enter_code'),
      clearable: true, style: { width: '90%' } }
  ],
  [
    { type: 'input', prop: 'name',
      label: this.key('name'), placeholder: this.key('enter_name'),
      clearable: true, style: { width: '90%' } }
  ],
  [
    { type: 'input', prop: 'remark', inputType: 'textarea',
      autosize: { minRows: 2, maxRows: 6 },
      label: this.key('remark'), placeholder: this.key('remark_required'),
      clearable: true, style: { width: '90%' } }
  ]
]

字段支持的属性:

属性 适用类型 说明
type 全部 'input'(文本) / 'select'(下拉)
prop 全部 绑定 formData 中的 key
label 全部 表单项标签,支持 i18n key
placeholder 全部 占位提示,支持 i18n key
inputType input 'textarea' / 不传为普通 text
autosize input textarea 自适应:{ minRows, maxRows }
clearable 全部 是否可清空,默认 true
style 全部 样式对象
options select 选项数组 [{ label, value }]
filterable select 是否可搜索,默认 true

4. Composable API 参考

4.1 useTableColumns(rawColumns, options?)

作用:消除手动分配 idx 序号,自动识别操作列和插槽列。

参数 类型 默认值 说明
rawColumns Array 列定义
options.selectionWidth number 55 复选框列宽,传 0 隐藏
options.indexWidth number 0 序号列宽,传 0 隐藏
const columns = useTableColumns([
  { prop: 'code', label: '编码', width: 120 },
  { prop: 'name', label: '名称' },
  { prop: '_actions', label: '操作', width: 160, fixed: 'right' }  // 操作列
])

内部约定prop === '_actions' 自动标记为 slot: '_actions',由 page-table 渲染为操作列。


4.2 useTableButtons(options, permissionCheck?)

作用:统一生成工具栏按钮和行内操作按钮,自动注入权限校验结果。

参数 类型 说明
options.toolbar Array 顶部按钮列表
options.row Array 行内按钮列表
permissionCheck Function 权限函数,通常为 this.$permission

返回值{ toolbarButtons, rowButtons }

按钮字段:

字段 toolbar row 说明
key 唯一标识
label 显示文本
icon Element UI 图标
type primary/success/warning/danger
color 'danger' 使文字变红
auth 权限 key
cssStyle 自定义样式
onClick 点击回调
hasPermission 自动注入,由权限函数计算
needSelection 需选中行才能点击
const btns = useTableButtons({
  toolbar: [
    { key: 'add', label: '新增', icon: 'el-icon-plus', type: 'primary',
      auth: '/xxx/create', onClick: this.openAdd }
  ],
  row: [
    { key: 'edit', label: '编辑', icon: 'el-icon-edit',
      auth: '/xxx/edit', onClick: this.openEdit },
    { key: 'delete', label: '删除', icon: 'el-icon-delete',
      color: 'danger', auth: '/xxx/delete', onClick: this.handleDelete }
  ]
}, this.$permission)

4.3 i18nMixin(prefix)

作用:注入 key(suffix)ckey(suffix) 两个方法,消除每个页面手动定义常量和 tkey 方法。

方法 返回值 说明
key(suffix) prefix.suffix 当前页面的 i18n key
ckey(suffix) page.common.suffix 公共 i18n key
参数 类型 说明
prefix String 该页面的 i18n key 前缀
import { i18nMixin } from '@/composables/useI18n'

export default {
  mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],
  // 模板中用 $t(key('code')) 访问当前页面翻译
  // 模板中用 $t(ckey('help')) 访问公共翻译 'page.common.help'
  // JS 中用 this.$t(this.key('code')) / this.$t(this.ckey('help'))
}

data() 中 i18n key 传参技巧

data () {
  return {
    // 传入完整 i18n key组件内部 $t() 翻译,切换语言自动响应
    formCols: [
      [{ label: this.key('code'), placeholder: this.key('enter_code') }]
    ],
    rules: {
      code: [{ required: true, message: this.key('enter_code'), trigger: 'blur' }]
    }
  }
}

原理page-table / page-dialog-form 内部使用 $t() 翻译传入的 key。$t() 是 Vue I18n 的响应式方法,切换语言时自动返回新的翻译结果,触发组件重新渲染。不要用 k() 等辅助函数提前翻译成静态字符串,否则语言切换后不会更新。


5. i18n 国际化方案

5.1 语言包文件

语言 文件路径
简体中文 src/locales/zh-chs.json
繁体中文 src/locales/zh-cht.json
英文 src/locales/en.json
日文 src/locales/ja.json

5.2 语言包结构

一级模块 > 二级模块 > 三级模块 三层嵌套:

{
  "page": {
    "production_master_data": {
      "factory_model": {
        "factory_area": {
          "search": "查询",
          "reset": "重置",
          "code": "所区编码",
          "name": "所区名称",
          "add": "新 增",
          "edit": "编 辑",
          "delete": "删 除",
          "enter_code": "请输入所区编码",
          "enter_name": "请输入所区名称",
          "add_title": "新增所区",
          "edit_title": "编辑所区",
          "confirm": "确定",
          "cancel": "取消",
          "operation_success": "操作成功"
        }
      }
    },
    "common": {
      "help": "帮 助"
    }
  }
}

5.3 组件自动翻译

page-tablepage-dialog-form 内部使用 $t() 翻译:

  • 列 label — 自动翻译
  • 按钮 label — 自动翻译
  • 表单 label / placeholder — 自动翻译
  • 弹框 title / confirm / cancel — 自动翻译
  • 验证规则 message — 自动翻译(translatedRules 计算属性)

页面只需传入 i18n key组件渲染时自动替换为当前语言的文本。

5.4 页面中手动翻译

搜索区、提示消息、$confirm 等 UI 文字需手动 $t()

<!-- 模板中key() 返回 i18n key -->
<el-form-item :label="$t(key('code'))">
  <el-input :placeholder="$t(key('enter_code'))" />
</el-form-item>
<el-button>{{ $t(key('search')) }}</el-button>
// JS 中
this.$message.success(this.$t(this.key('operation_success')))
this.$confirm(this.$t(this.key('confirm_delete')), this.$t(this.key('tip')), { ... })

6. 常用场景速查

场景 1自定义列渲染状态 tag

列定义加 slot: true

useTableColumns([
  { prop: 'status', label: '状态', slot: true, width: 100 }
])

模板中用 #col-status 插槽:

<page-table :columns="columns" :data="tableData">
  <template #col-status="{ row }">
    <el-tag :type="row.status === 1 ? 'success' : 'danger'">
      {{ row.status === 1 ? '启用' : '禁用' }}
    </el-tag>
  </template>
</page-table>

场景 2工具栏追加自定义按钮

<page-table :columns="columns" :toolbar-buttons="toolbarButtons">
  <template #toolbar-extra>
    <el-button size="mini" icon="el-icon-printer" @click="print">打印</el-button>
  </template>
</page-table>

场景 3复选框 + 序号列

useTableColumns(
  [
    { prop: 'code', label: '编码' },
    { prop: 'name', label: '名称' }
  ],
  { selectionWidth: 55, indexWidth: 60 }
)

场景 4带下拉选择的表单

formCols: [
  [
    { type: 'select', prop: 'area_id',
      label: this.key('area'), placeholder: this.key('select_area'),
      options: [{ label: 'A厂区', value: 1 }, { label: 'B厂区', value: 2 }],
      clearable: true, style: { width: '90%' } }
  ]
]

场景 5批量删除工具栏 + 需要选中行)

useTableButtons({
  toolbar: [
    { key: 'batchDelete', label: '批量删除', icon: 'el-icon-delete',
      type: 'danger', auth: '/xxx/batch-delete',
      needSelection: true,  // 需要选中行才能点击
      onClick: this.batchDelete }
  ]
})

场景 6表格自适应高度

只需加 auto-height 属性:

<page-table ... auto-height />

表格高度自动填满 d2-container 的 body 区域剩余空间,窗口 resize / 侧栏折叠时自动重算。

场景 7帮助按钮

help-url 即可在工具栏最右侧显示问号帮助按钮:

<page-table ... help-url="/help/factory-area" :help-text="$t(ckey('help'))" />

推荐使用公共 key page.common.help(模板中用 $t(ckey('help')))。不传 help-url 则不显示。


7. 路由配置

// src/router/modules/production-master-data.js
import layoutHeaderAside from '@/layout/header-aside'

const meta = { auth: true }
const _import = require('@/libs/util.import.' + process.env.NODE_ENV)

export default {
  path: '/production_master_data',
  component: layoutHeaderAside,
  children: (pre => [
    {
      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_master_data-')
}

之后在 src/router/routes.js 中引入该模块并加入 frameIn 数组:

import productionConfiguration from './modules/production-master-data'

const frameIn = [
  // ... 其他路由 ...
  productionConfiguration
]

8. API 文件写法

// src/api/production-master-data/factory-area.js
import { request } from '@/api/_service'

const BASE = 'production_master_data/factory_model/factory_area/'

function apiParams (method, data = {}) {
  return {
    method: `production_master_data_factory_model_factory_area_${method}`,
    platform: 'background',
    ...data
  }
}

export function getFactoryAreaList (data) {
  return request({
    url: BASE + 'list',
    method: 'get',
    params: apiParams('list', data)
  })
}

export function createFactoryArea (data) {
  return request({
    url: BASE + 'create',
    method: 'post',
    data: apiParams('create', data)
  })
}

export function editFactoryArea (data) {
  return request({
    url: BASE + 'edit',
    method: 'put',
    data: apiParams('edit', data)
  })
}

export function deleteFactoryArea (data) {
  return request({
    url: BASE + 'delete',
    method: 'delete',
    data: apiParams('delete', data)
  })
}

9. 旧代码迁移对照

旧写法 新写法
手动构建 columnsidx 序号 useTableColumns([{ prop, label }])
buttonList + tableButtonList 分开 useTableButtons({ toolbar: [...], row: [...] })
<sct-base-table> + <sct-base-dialog> + <SctBaseForm> <page-table> + <page-dialog-form>
<sct-form-search> 组件 原生 <el-form :inline>
<sct-back-to-top> 组件 需要时自行添加
额外 <page-footer> 分页 page-table 内置分页
每个页面定义 T 常量 + tkey() 方法 mixins: [i18nMixin(prefix)],使用 this.key('xxx')
data() 中用 k() 提前翻译 this.key() 传 key组件内部 $t() 翻译

10. 接口请求错误处理规范

错误处理采用 三层架构:拦截器全局兜底 → confirmMixin 区分取消/错误 → 页面标准模式。

10.1 错误处理总流程

                    ┌─────────────────────┐
                    │   页面发起 API 请求   │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │  是否需 confirm ?    │
                    └─────┬────────┬──────┘
                          │ 否     │ 是
                          │        │
                          │   ┌────▼───────────┐
                          │   │ $confirmAction() │
                          │   │  (confirmMixin)  │
                          │   └────┬──────┬─────┘
                          │    取消 │      │ 确认
                          │   ┌────▼──┐    │
                          │   │return │    │
                          │   │ true  │    │
                          │   └───────┘    │
                          │                │
               ┌──────────▼────────────────▼──┐
               │         axios 请求            │
               └──────────┬───────────────────┘
                          │
              ┌───────────▼────────────┐
              │   HTTP 状态码是什么?    │
              └──┬──────────────┬──────┘
           200  │              │ 非 200
                │              │
    ┌───────────▼──────┐  ┌───▼─────────────┐
    │  response.data.  │  │ HTTP 拦截器       │
    │  code 是什么?    │  │ error.message =  │
    └──┬───────────┬───┘  │ '未授权/超时/...' │
  0    │           │≠0    └───┬──────────────┘
       │           │          │
  ┌────▼──┐  ┌─────▼──────┐  │
  │resolve│  │handleError()│◄─┘
  │ data  │  │Message.error│
  └───────┘  └─────┬──────┘
                   │
              ┌────▼────┐
              │ throw   │
              │ error   │
              └────┬────┘
                   │
         ┌─────────▼─────────┐
         │ 页面层 catch 到了吗?│
         └────┬──────────┬───┘
         没写  │          │ 写了
              │          │
    ┌─────────▼──┐  ┌────▼─────────────┐
    │ 什么都不发生 │  │ finally 关锁      │
    │(依赖拦截器  │  │ loading/submitting│
    │  Message  │  │ = false           │
    └────────────┘  └──────────────────┘

10.2 三层架构详图

┌─────────────────────────────────────────────────┐
│              第一层:全局拦截器                     │
│           src/api/_service.js                     │
│                                                   │
│  拦截所有 axios 响应,统一处理两类错误:              │
│                                                   │
│  HTTP 错误 (4xx/5xx)                              │
│  ├─ 400 → error.message = '请求错误'               │
│  ├─ 401 → error.message = '未授权,请登录'          │
│  ├─ 403 → error.message = '拒绝访问'               │
│  ├─ 404 → error.message = '请求地址出错'            │
│  ├─ 408 → error.message = '请求超时'               │
│  ├─ 500 → error.message = '服务器内部错误'          │
│  ├─ 502 → error.message = '网关错误'               │
│  ├─ ... 其它状态码                                 │
│  │                                                │
│  └─ handleError(error)                            │
│     ├─ store.dispatch('d2admin/log/push', ...)    │
│     ├─ console.log(error)  (dev only)             │
│     └─ Message.error(error.message)  ← 弹红色提示  │
│                                                   │
│  业务错误 (code ≠ 0, 如 code=500 "参数错误")       │
│  ├─ 取出 response.data.msg 或 '请求失败'            │
│  ├─ const err = new Error(`${msg}: ${url}`)      │
│  ├─ handleError(err)                              │
│  └─ throw err  ← 继续向上抛给页面层                 │
└──────────────────────┬──────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────┐
│             第二层confirmMixin                   │
│        src/composables/useConfirmHandle.js        │
│                                                   │
│  $confirmAction(confirmOpts, action)              │
│  ├─ 第一层 try/catch                              │
│  │   await this.$confirm(...)                     │
│  │   ├─ 用户点"确定" → 继续                        │
│  │   └─ 用户点"取消" → catch → return true         │
│  │                                                │
│  └─ 第二层 try/catch                              │
│      await action()   ← 执行 API 调用              │
│      ├─ 成功 → return false                        │
│      └─ 失败 → catch → return false                │
│          (拦截器已弹出 Message此处静默吞掉        │
└──────────────────────┬──────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────┐
│              第三层:页面标准模式                    │
│                                                   │
│  场景A — 查询 (fetchData)                         │
│  ┌─────────────────────────────────────────────┐ │
│  │ async fetchData () {                         │ │
│  │   this.loading = true                        │ │
│  │   try {                                      │ │
│  │     const res = await getList(...)            │ │
│  │     this.tableData = res.data                 │ │
│  │   } finally { this.loading = false }          │ │
│  │ }                                            │ │
│  └─────────────────────────────────────────────┘ │
│  • try 内无 catch拦截器已弹错误提示               │
│  • finally 始终执行loading 无论成败都关闭          │
│                                                   │
│  场景B — 提交 (onDialogSubmit)                     │
│  ┌─────────────────────────────────────────────┐ │
│  │ async onDialogSubmit () {                    │ │
│  │   this.submitting = true                     │ │
│  │   try {                                      │ │
│  │     await createApi(this.formData)            │ │
│  │     this.$message.success(...)  ← 成功才执行  │ │
│  │     this.dialogVisible = false               │ │
│  │     this.fetchData()                         │ │
│  │   } finally { this.submitting = false }       │ │
│  │ }                                            │ │
│  └─────────────────────────────────────────────┘ │
│  • 出错时弹窗不关,用户可修改后重试                  │
│                                                   │
│  场景C — 删除 (handleDelete)                       │
│  ┌─────────────────────────────────────────────┐ │
│  │ async handleDelete (row) {                   │ │
│  │   const cancelled = await this.$confirmAction(│ │
│  │     { message: this.key('confirm_delete'),   │ │
│  │       title: this.key('tip') },              │ │
│  │     () => deleteApi({ id: row.id })          │ │
│  │   )                                          │ │
│  │   if (cancelled) return  ← 用户取消,静默       │ │
│  │   this.$message.success(...)  ← 成功才执行     │ │
│  │   this.fetchData()                           │ │
│  │ }                                            │ │
│  └─────────────────────────────────────────────┘ │
│  • $confirmAction 内已处理错误 Message不额外 catch│
│  • 没有 try/catch 包裹 → 代码更简洁直观             │
└─────────────────────────────────────────────────┘

10.3 关键原则

原则 说明
拦截器兜底 所有 API 错误HTTP + 业务)统一在 _service.js 中弹出 Message.error
finally 关锁 loading / submittingtry { ... } finally { lock = false },不论成败都关闭
只用 catch 不动 成功后的 $message.success / dialogVisible = false / fetchData 不放在 finally,只有成功才执行
$confirmAction 包装 删除/批量操作类的 confirm + API 用 $confirmAction() 区分取消和错误
不吞业务错误 fetchData / onDialogSubmit 的 try 只写 finally,不写 catch,错误由拦截器处理

10.4 confirmMixin 使用

页面中引入:

import { confirmMixin } from '@/composables/useConfirmHandle'

export default {
  mixins: [
    i18nMixin('page.xxx.xxx'),
    confirmMixin               // ← 新增
  ]
}

API

// $confirmAction(confirmOpts, actionFn) → Promise<boolean>
// 返回 true  = 用户点了取消
// 返回 false = action 已执行完成(无论成功失败)

const cancelled = await this.$confirmAction(
  {
    message: this.key('confirm_delete'),   // confirm 正文
    title: this.key('tip'),                // confirm 标题
    // type: 'warning',                    // 可选,默认 'warning'
    // confirmButtonText / cancelButtonText 可选,默认用 page.common 的
  },
  () => deleteApi({ id: row.id })          // 确认后执行的异步函数
)
if (cancelled) return
// 以下只在成功时执行
this.$message.success(...)
this.fetchData()

11. 常见问题排查

Q1弹框打开后不显示内容

确认 formCols 中的 labelplaceholder 是否传入了正确的 i18n key。应该用 this.key() 拼接完整 key不要用 k() 提前翻译成静态字符串。

Q2表格高度自适应不生效

确认 d2-container 的 body 区域高度被正确约束flex 填充)。给 d2-container 的 body 部分设置 overflow: hidden 通常可解决。

Q3权限校验有按钮不显示

useTableButtons 的第二个参数必须传入 this.$permission。如果项目没有注册这个全局方法,可以传入 () => true 占位。

Q4新增一条后页码不跳回第一页

onDialogSubmit 成功后调用 this.fetchData() 前,如果删除当前页最后一行导致 total - 1 超出范围,需手动修正页码。

Q5切换语言后弹框/表格内容不更新?

检查 data() 中是否使用了 k() 等辅助函数提前翻译。应该用 this.key() 传 i18n key由组件内部 $t() 处理翻译,这样切换语言时才能自动响应。

Q6表单验证错误提示显示为原始 i18n key

page-dialog-form 会通过 translatedRules 计算属性自动翻译验证规则的 message 字段。确认传入的 rules.message 使用了 this.key() 传入完整 key。