Files
mes-ui-d2/docs/表格组件使用说明.md
sheng 3eaea3116d
Some checks failed
Release pipeline / publish (push) Has been cancelled
Release pipeline / Always run job (push) Has been cancelled
feat: 新增工厂区域管理页面,修复Sass废弃警告
1. 新增生产配置-工厂模型-工厂区域完整CRUD页面
2. 新增通用表格、弹窗表单、i18n工具组件
3. 升级sass-loader并修复Sass废弃警告
4. 添加文档记录Sass迁移修复细节
2026-05-26 18:32:57 +08:00

30 KiB
Raw Blame History

表格组件使用说明

基于 page-table + page-dialog-form 的新一ä»?CRUD 表格方案ã€? æº<C3A6>ç <C3A7>ä½<C3A4>置:src/components/page-table/ã€<EFBFBD>src/components/page-dialog-form/ã€<EFBFBD>src/composables/
完整å<C2B4>¯è¿<C3A8>行示ä¾ï¼šsrc/views/production-master-data/factory-model/factory-area/index.vue


目录

  1. 快速开始(三步走)
  2. 完整示ä¾ä»£ç <EFBFBD>
  3. [组件 API å<>考](#3-组件-api-å<>è€?
  4. [Composable API å<>考](#4-composable-api-å<>è€?
  5. [i18n 国际化方案](#5-i18n-国际化方�
  6. 常用场景速查
  7. 路由é…<EFBFBD>ç½®
  8. API 文件写法
  9. [旧代ç <C3A7>è¿<C3A8>移对照](#9-旧代ç <C3A7>è¿<C3A8>移对ç…?
  10. 常è§<EFBFBD>é—®é¢˜æŽæŸ¥

1. 快速开始(三步走)

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

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

export default {
  mixins: [i18nMixin('page.模å<C2A1>—å<E28094>?二级模å<C2A1>—.三级模å<C2A1>—')],

  created () {
    // --- 列定�---
    // å<>ªéœ€å£°æ˜Ž prop å’?label,idx 自动补é½<C3A9>
    // prop: '_actions' 约定为æ“<C3A6>作列,自动渲æŸ?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' }
    ])

    // --- 按钮定义 ---
    // ä¸<C3A4>å†<C3A5>分开å†?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) // â†?第二个å<C2AA>æ•°ä¼ å…¥æ<C2A5>ƒé™<C3A9>校验函æ•?
    this.toolbarButtons = btns.toolbarButtons
    this.rowButtons = btns.rowButtons
  }
}

第二步:写模æ<EFBFBD>¿ï¼ˆ<template> 中)

<template>
  <d2-container>
    <!-- æ<EFBFBD>œç´¢åŒ?-->
    <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"
      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="$t(key('confirm'))"
      :cancel-text="$t(key('cancel'))"
      @submit="onDialogSubmit"
      @close="onDialogClose"
    />
  </d2-container>
</template>

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

methods: {
  // 获å<C2B7>列表数æ<C2B0>®
  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 }
  },

  // æ<>œç´¢ / é‡<C3A9>ç½®
  onSearch () { this.pagination.current = 1; this.fetchData() },
  onReset () { this.search = { code: '', name: '' }; this.pagination.current = 1; this.fetchData() },

  // 分页å<C2B5>˜åŒ
  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
    })
  },

  // ç¼è¾ï¼šåžå¡«è¡¨å<C2A8>?  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
  },

  // æ<><C3A6>交表å<C2A8>  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. 完整示ä¾ä»£ç <C3A7>

ðŸ“<EFBFBD> src/views/production-master-data/factory-model/factory-area/index.vue
这是一ä¸?*å<>¯ç´æŽ¥è¿<C3A8>行的完整 CRUD 页é<C2B5>¢**,包å<E280A6>«æ<C2AB>œç´¢æ <C3A6>ã€<C3A3>表格ã€<C3A3>分页ã€<C3A3>æ°å¢?ç¼è¾å¼¹æ¡†ã€<C3A3>删除确认ã€?

模æ<EFBFBD>¿éƒ¨åˆ

<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"
      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="$t(key('confirm'))"
      :cancel-text="$t(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 () {
    const t = this.$t.bind(this)
    const k = (s) => t(this.key(s))
    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: k('enter_code'), trigger: 'blur' },
          { min: 1, max: 100, message: k('remark_length'), trigger: 'blur' }
        ],
        name: [
          { required: true, message: k('enter_name'), trigger: 'blur' },
          { min: 1, max: 100, message: k('remark_length'), trigger: 'blur' }
        ]
      },
      columns: [],
      toolbarButtons: [],
      rowButtons: [],
      formCols: [
        [
          { type: 'input', prop: 'code',
            label: k('code'), placeholder: k('enter_code'),
            clearable: true, style: { width: '90%' } }
        ],
        [
          { type: 'input', prop: 'name',
            label: k('name'), placeholder: k('enter_name'),
            clearable: true, style: { width: '90%' } }
        ],
        [
          { type: 'input', prop: 'remark', inputType: 'textarea',
            autosize: { minRows: 2, maxRows: 6 },
            label: k('remark'), placeholder: k('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) { /* å<>消删除 / 请æ±å¤±è´¥ä¸<C3A4>处ç<E2809E>?*/ }
    }
  }
}

3. 组件 API å<>è€?

3.1 page-table �表格 + 按钮�+ 分页

Props

Prop 类型 默认� 说明
columns Array [] 列定义,ç”?useTableColumns() 生æˆ<C3A6>
data Array [] 表格行数æ<EFBFBD>?
loading Boolean false 是å<EFBFBD>¦æ˜¾ç¤º loading é<>®ç½©
height String/Number â€? 表格高度ã€ä¸<EFBFBD>传则éš<EFBFBD>内容æå¼€ï¼ä¼ å…·ä½“数值åºå®šé«˜åº¦ï¼ä¼?'auto' å<>¯ç”¨è‡ªé€åº”
auto-height Boolean false å<EFBFBD>¯ç”¨é«˜åº¦è‡ªé€åº”,表格自动填满å<EFBFBD>¯ç”¨ç©ºé—?
border Boolean true 是å<EFBFBD>¦å¸¦è¾¹æ¡?
row-key String 'id' 行唯一 key
toolbar-buttons Array [] 顶部工具æ <EFBFBD>按é®ï¼Œç”?useTableButtons() 生æˆ<C3A6>
row-buttons Array [] 行内æ“<EFBFBD>作按é®ï¼Œç”± useTableButtons() 生æˆ<C3A6>
pagination Object null 分页å<EFBFBD>æ•° { current, size, total },传了æ‰<EFBFBD>显示分页
table-attrs Object {} é¢<EFBFBD>å¤é€<EFBFBD>ä¼ ç»?el-table 的属æ€?
table-listeners Object {} é¢<EFBFBD>å¤é€<EFBFBD>ä¼ ç»?el-table 的事ä»?
help-url String '' 帮助文档跳转 URLã€ä¼ äº†æ‰<C3A6>显示工具æ <C3A6>å<EFBFBD>³ä¾§çš„é—®å<C2AE>·æŒ‰é®ï¼Œç¹å‡»æ°çª—å<E28094>£æ‰“å¼€
help-text String '帮助' 帮助按钮文字,支�i18n key(组件自�$t() 翻译�

事件

äºä»¶å<EFBFBD>? å<EFBFBD>æ•° 说明
@page-change { current, size, total } 分页å<EFBFBD>˜åŒï¼ˆåˆ‡æ<EFBFBD>¢é¡µç ?æ<>¡æ•°ï¼?
@selection-change rows: Array 选中行å<EFBFBD>˜åŒ?
@sort-change é€<EFBFBD>ä¼  el-table 原生事件 æŽåº<EFBFBD>å<EFBFBD>˜åŒ

æ<EFBFBD>æ§½

æ<EFBFBD>æ§½å<EFBFBD>? 作用åŸ? 说明
#col-{prop} { row, index } 自定义列的渲染(列定义中 prop 需�slot: true�
#toolbar-extra â€? 工具æ <EFBFBD>区域追加自定义内容
#empty â€? 表格空数æ<EFBFBD>®æ—¶çš„å<EFBFBD> ä½?
#append â€? 表格最å<EFBFBD>Žä¸€è¡Œå<EFBFBD>Žè¿½åŠ 
#extra â€? 页é<EFBFBD>¢åº•部追加区域

列定义规�

// 普通列
{ prop: 'code', label: 'ç¼ç <C3A7>', minWidth: 120 }

// æ“<C3A6>作列(约定:prop === '_actions'ï¼?{ prop: '_actions', label: 'æ“<C3A6>作', width: 160, fixed: 'right' }

// 自定义æ<E280B0>槽列(slot: trueï¼?{ prop: 'status', label: '状æ€?, slot: true, width: 100 }

// å¤<C3A5>选框åˆ?+ åº<C3A5>å<EFBFBD>·åˆ—(通过 useTableColumns 第二个å<C2AA>数)
useTableColumns([...], { selectionWidth: 55, indexWidth: 60 })

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

Props

Prop 类型 默认� 说明
visible Boolean false 弹框显éš<EFBFBD>,使ç”?.sync 修饰ç¬?
title String '' 弹框标题,支�i18n key
width String '35%' 弹框宽度
form-cols Array [] 表å<EFBFBD>•字段结构(二维数组,è§<EFBFBD>䏿¹ï¼‰
form-data Object {} 表å<EFBFBD>•æ•°æ<EFBFBD>®å¯¹è±¡
rules Object {} 校验规则,与 el-form rules 一�
label-width String '100px' label 宽度
submitting Boolean false æ<EFBFBD><EFBFBD>交 loading 状æ€?
confirm-text String '确定' 确定按钮文字
cancel-text String <>消' å<EFBFBD>æ¶ˆæŒ‰é®æ‡å­

事件

äºä»¶å<EFBFBD>? 说明
@submit 表å<EFBFBD>•验è¯<EFBFBD>通过å<EFBFBD>Žè§¦å<EFBFBD>,父组件执行æ<EFBFBD><EFBFBD>交逻è¾
@close 弹框关闭å<EFBFBD>Žè§¦å<EFBFBD>?

方法(通过 ref 调用�

方法 说明
reset() é‡<EFBFBD>置表å<EFBFBD>
validate() æ‰åŠ¨éªŒè¯<EFBFBD>,返å?Promise<boolean>

formCols æ•°æ<C2B0>®ç»“æž„

**注æ„<C3A6>:需åœ?data() 中用 k(prop) æ<><C3A6>å‰<C3A5>完æˆ<C3A6> i18n 翻译**,ä¸<C3A4>è¦<C3A8>ä¼  raw keyã€?

// 例:两个普通输入框 + 一个多行文本框
formCols: [
  [
    { type: 'input', prop: 'code',
      label: k('code'), placeholder: k('enter_code'),
      clearable: true, style: { width: '90%' } }
  ],
  [
    { type: 'input', prop: 'name',
      label: k('name'), placeholder: k('enter_name'),
      clearable: true, style: { width: '90%' } }
  ],
  [
    { type: 'input', prop: 'remark', inputType: 'textarea',
      autosize: { minRows: 2, maxRows: 6 },
      label: k('remark'), placeholder: k('remark_required'),
      clearable: true, style: { width: '90%' } }
  ]
]

字段支æŒ<EFBFBD>的属性:

属� 适用类型 说明
type 全部 'input'(文本输入)/ 'select'(下拉)
prop 全部 绑定 formData 中的 key
label 全部 表å<EFBFBD>•项标ç­?
placeholder 全部 å<EFBFBD> ä½<EFBFBD>æ<EFBFBD><EFBFBD>示
inputType input 'textarea' 多行 / ä¸<C3A4>传为普é€?text
autosize input textarea çš?{ minRows, maxRows }
clearable 全部 是å<EFBFBD>¦å<EFBFBD>¯æ¸…空,默认 true
style 全部 æ ·å¼<EFBFBD>对象
options select 选项数组 [{ label, value }]
filterable select 是å<EFBFBD>¦å<EFBFBD>¯æ<EFBFBD>œç´¢ï¼Œé»˜è®¤ true

4. Composable API å<>è€?

4.1 useTableColumns(rawColumns, options?)

**作用**:消除手动分é…?idx åº<C3A5>å<EFBFBD>·ï¼Œè‡ªåŠ¨è¯†åˆ«æ“<C3A6>ä½œåˆ—åŒæ<C592>槽列ã€?

å<EFBFBD>æ•° 类型 默认å€? 说明
rawColumns Array � 列定�
options.selectionWidth number 55 å¤<EFBFBD>选框列宽,传 0 éš<C3A9>è—<C3A8>
options.indexWidth number 0 åº<EFBFBD>å<EFBFBD>·åˆ—宽,传 0 éš<C3A9>è—<C3A8>
const columns = useTableColumns([
  { prop: 'code', label: 'ç¼ç <C3A7>', width: 120 },
  { prop: 'name', label: <><C3A5>ç§°' },
  { prop: '_actions', label: 'æ“<C3A6>作', width: 160, fixed: 'right' }  // â†?æ“<C3A6>作åˆ?])

内部约定:prop === '_actions' 自动标记ä¸?slot: '_actions',由 page-table 渲染为æ“<C3A6>作列ã€?

4.2 useTableButtons(options, permissionCheck?)

**作用**:统一生æˆ<C3A6>工具æ <C3A6>按é®åŒè¡Œå†…æ“<C3A6>作按é®ï¼Œè‡ªåŠ¨æ³¨å…¥æ<C2A5>ƒé™<C3A9>校验结果ã€?

å<EFBFBD>æ•° 类型 说明
options.toolbar Array 顶部按钮列表
options.row Array 行内按钮列表
permissionCheck Function æ<EFBFBD>ƒé™<EFBFBD>函数,通常ä¼?this.$permission

**返回�*:{ toolbarButtons, rowButtons }

*按钮字段�

字段 toolbar row 说明
key � � 唯一标识
label � � 显示文本
icon � � Element UI 图标
type � � primary/success/warning/danger
color â€? âœ? 'danger' 使æ‡å­—å<E28094>˜çº?
auth âœ? âœ? æ<EFBFBD>ƒé™<EFBFBD> key
cssStyle � � 自定义样�
onClick � � 点击回调
hasPermission âœ? âœ? **自动注入**,由æ<C2B1>ƒé™<C3A9>函数计算
needSelection âœ? â€? 需选中行æ‰<EFBFBD>能ç¹å‡?
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) ä¸¤ä¸ªæ¹æ³•,消除æ¯<C3A6>个页é<C2B5>¢æ‰å†?T 常é‡<C3A9>å?tkey 方法ã€?

方法 返回� 说明
key(suffix) prefix.suffix 当å‰<EFBFBD>页é<EFBFBD>¢çš?i18n key
ckey(suffix) page.common.suffix 公共 i18n key(å¦"帮助"ã€?确定"ã€?å<>消"ï¼?
å<EFBFBD>æ•° 类型 说明
prefix String 该页é<EFBFBD>¢çš„ i18n key å‰<C3A5>ç¼€ï¼Œå¦ 'page.production_master_data.factory_model.factory_area'
import { i18nMixin } from '@/composables/useI18n'

export default {
  mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],
  // æ­¤å<C2A4>Žæ¨¡æ<C2A1>¿ä¸­ç”¨ $t(key('code')) 访问当å‰<C3A5>页é<C2B5>¢çš„ç¿»è¯?  // ç”?$t(ckey('help')) 访问公共翻译 'page.common.help'
  // JS 中用 this.$t(this.key('code')) / this.$t(this.ckey('help'))
}

**data() 中翻译文本的技�*�

data () {
  const t = this.$t.bind(this)       // 绑定 i18n 翻译函数
  const k = (s) => t(this.key(s))   // 当å‰<C3A5>页é<C2B5>¢ç¿»è¯ï¼škey + translate
  const ck = (s) => t(this.ckey(s)) // 公共翻è¯ï¼šckey + translate

  return {
    formCols: [
      [{ label: k('code'), placeholder: k('enter_code') }]  // 页é<C2B5>¢çº?key
    ],
    helpText: ck('help')  // 公共 key
  }
}

5. i18n 国际化方�

5.1 语言包文�

语言 文件路径
简体中� src/locales/zh-chs.json
ç¹<EFBFBD>体中æ src/locales/zh-cht.json
英文 src/locales/en.json
日文 src/locales/ja.json

5.2 语言包结�

æŒ?一级模å<EFBFBD>?â†?二级模å<C2A1>— â†?三级模å<C2A1> 三层嵌套ï¼?

{
  "page": {
    "production_master_data": {
      "factory_model": {
        "factory_area": {
          "search": "查询",
          "reset": "é‡<C3A9>ç½®",
          "code": "所区编�,
          "name": "所区å<EFBFBD><EFBFBD>ç§?,
          "add": "��,
          "edit": "ç¼?è¾?,
          "delete": "��,
          "enter_code": "请输入所区编�,
          "enter_name": "请输入所区å<C2BA><C3A5>ç§?,
          "add_title": "新增所�,
          "edit_title": "编辑所�,
          "confirm": "确定",
          "cancel": "å<EFBFBD>消",
          "operation_success": "æ“<EFBFBD>作æˆ<EFBFBD>功"
        }
      }
    }
  }
}

5.3 组件自动翻译

page-table �page-dialog-form 内置 $t()�

  • åˆ?label â†?自动翻译
  • 按钮 label â†?自动翻译
  • 表å<EFBFBD>• label / placeholder â†?自动翻译
  • 弹框 title / confirm / cancel â†?自动翻译

å æ­¤é¡µé<EFBFBD>¢å<EFBFBD>ªéœ€ä¼ å…¥ i18n key å­—ç¬¦ä¸²ï¼Œç»„ä»¶æ¸²æŸ“æ—¶è‡ªåŠ¨æ¿æ<C2BF>¢ä¸ºå½“å‰<C3A5>è¯­è¨€çš„æ‡æœ¬ã€?

5.4 页é<C2B5>¢ä¸­æ‰åŠ¨ç¿»è¯?

æ<EFBFBD>œç´¢åŒºã€<EFBFBD>校验消æ<EFBFBD>¯ã€<EFBFBD>$confirm ç­?UI 文字需手动ç”?$t()ï¼?

<!-- 模æ<EFBFBD>¿ä¸?â?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 }
])

模æ<EFBFBD>¿ä¸­ç”¨ #col-status æ<>æ§½ï¼?

<page-table :columns="columns" :data="tableData">
  <template #col-status="{ row }">
    <el-tag :type="row.status === 1 ? 'success' : 'danger'">
      {{ row.status === 1 ? <>¯ç”¨' : 'ç¦<C3A7>用' }}
    </el-tag>
  </template>
</page-table>

场景 2:工具æ <C3A6>追加自定义按é?

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

场景 3:å¤<C3A5>选框åˆ?+ åº<C3A5>å<EFBFBD>·åˆ?

useTableColumns(
  [
    { prop: 'code', label: 'ç¼ç <C3A7>' },
    { prop: 'name', label: <><C3A5>ç§°' }
  ],
  { selectionWidth: 55, indexWidth: 60 }
)

场景 4ï¼šå¸¦ä¸æ‰é€‰æ©çš„表å<C2A8>?

formCols: [
  [
    { type: 'select', prop: 'area_id',
      label: k('area'), placeholder: k('select_area'),
      options: [{ label: 'AåŽåŒº', value: 1 }, { label: 'BåŽåŒº', value: 2 }],
      clearable: true, style: { width: '90%' } }
  ]
]

场景 5:批é‡<C3A9>删除(工具æ ?+ 需è¦<C3A8>选中行)

useTableButtons({
  toolbar: [
    { key: 'batchDelete', label: '批é‡<C3A9>删除', icon: 'el-icon-delete',
      type: 'danger', auth: '/xxx/batch-delete',
      needSelection: true,  // â†?需è¦<C3A8>选中行æ‰<C3A6>能ç¹å‡?      onClick: this.batchDelete }
  ]
})

场景 6:表格自é€åº”高度

å<EFBFBD>ªéœ€åŠ?auto-height 属性:

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

表格高度会自动填æ»?d2-container çš?body 区域剩余空间,窗å<E28094>?resize / ä¾§æ <C3A6>折å<CB9C> æ—¶è‡ªåЍé‡<C3A9>ç®—ã€?

场景 7:帮助按é?

ç»?help-url 传值å<C2BC>³å<C2B3>¯åœ¨å·¥å…·æ <C3A6>最å<E282AC>³ä¾§æ˜¾ç¤ºé—®å<C2AE>·å¸®åŠ©æŒ‰é®ï¼Œç¹å‡»åœ¨æ°çª—å<E28094>£æ‰“å¼€å¸®åŠ©æ‡æ¡£ï¼?

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

按钮文字通过 help-text prop 支æŒ<C3A6> i18nã€æŽ¨è<C2A8><C3A8>使用公å…?key page.common.help(模æ<EFBFBD>¿ä¸­å†?$t(ckey('help'))),所有页é<EFBFBD>¢å…±äº«ï¼Œæ— éœ€æ¯<EFBFBD>个模å<EFBFBD>—é‡<EFBFBD>å¤<EFBFBD>定义ã€? ä¸<C3A4>ä¼  help-url æˆä¼ ç©ºå­—符串则ä¸<C3A4>显示帮助按é®ã€?

7. 路由é…<C3A9>ç½®

// 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-')
}

ä¹å<EFBFBD>Žåœ?src/router/routes.js 中引入该模å<C2A1>—并加å…?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. 旧代ç <C3A7>è¿<C3A8>移对ç…?

旧写� 新写�
手动构建 columns: [{ idx: 0, attrs: { prop, label } }] 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> 组件 需è¦<EFBFBD>时自行添加
分页需è¦<EFBFBD>é¢<EFBFBD>å¤?<page-footer> page-table 内置分页
æ¯<EFBFBD>个页é<EFBFBD>¢å®šä¹‰ T 常é‡<C3A9> + tkey() 方法 mixins: [i18nMixin(prefix)],使ç”?this.key('xxx')
表å<EFBFBD>•字段å­?i18n raw key,å­<C3A5>ç»„ä»¶ç¿»è¯ data() 中用 k(prop) æ<><C3A6>å‰<C3A5>ç¿»è¯

10. 常è§<C3A8>é—®é¢˜æŽæŸ¥

Q1:弹框打开å<EFBFBD>Žä¸<EFBFBD>显示内容ï¼?

确认 formCols 中的 label å’?placeholder 是å<C2AF>¦åœ?data() 阶段已翻è¯ã€å­<C3A5>组件 page-dialog-form 会调ç”?$t() 处ç<E2809E>† label,但建议åœ?data() 阶段就用 k() æ<><C3A6>å‰<C3A5>ç¿»è¯å¥½ï¼Œé<C592>¿å…<C3A5> webpack HMR 缓存问题ã€?

Q2:表格高度自é€åº”ä¸<EFBFBD>生效?

确认 d2-container çš?body 区域高度被正确约æ<C2A6>Ÿï¼ˆflex 填充)。如æž?d2-container 本身æœ?overflow: auto æˆå…¶ä»é«˜åº¦çº¦æ<C2A6>Ÿé—®é¢˜ï¼Œpage-table çš?height: 100% 会失效。给 d2-container çš?body 部分è®?overflow: hidden 通常å<C2B8>¯è§£å†³ã€?

Q3:æ<EFBFBD>ƒé™<EFBFBD>校验有按é®ä¸<EFBFBD>显示?

useTableButtons 的第二个å<C2AA>æ•°å¿…é¡»ä¼?this.$permissionã€å¦æžœé¡¹ç®æ²¡æœ‰æ³¨å†Œè¿™ä¸ªå…¨å±€æ¹æ³•,å<EFBFBD>¯ä»¥åœ¨ useTableButtons 中传入一个返å›?true çš„å<E2809E> ä½<C3A4>函数:() => trueã€?

Q4:æ°å¢žä¸€æ<EFBFBD>¡å<EFBFBD>Žé¡µç <EFBFBD>ä¸<EFBFBD>è·³åžç¬¬ä¸€é¡µï¼Ÿ

åœ?onDialogSubmit æˆ<C3A6>功å<C5B8>Žè°ƒç”?this.fetchData() å‰<C3A5>ï¼Œå¦æžœåˆ é™¤äº†å½“å‰<C3A5>页最å<E282AC>Žä¸€è¡Œå¯¼è‡?total - 1 超出范å´ï¼Œéœ€æ‰åŠ¨ä¿®æ­£é¡µç <C3A7>ï¼?

this.pagination.current = Math.min(
  this.pagination.current,
  Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
)

Q5:表å<EFBFBD>•验è¯<EFBFBD>ä¸<EFBFBD>æ<EFBFBD><EFBFBD>示错误æ‡å­—ï¼?

确认 page-dialog-form çš?<style> å<>—存在(默认 22px margin-bottom + position: absolute 错误æ‡å­—)ã€å¦æžœè¡¨å<C2A8>•项ä¹é—´è¢«å…¶ä»æ ·å¼<C3A5>覆ç,检查是å<C2AF>¦æœ‰å…¨å±€ CSS é‡<C3A9>ç½®äº?.el-form-item çš?marginã€?

Q6:å¦ä½•æ°å¢žä¸€ä¸?CRUD 页é<C2B5>¢æœ€å¿«ï¼Ÿ

  1. å¤<EFBFBD>åˆsrc/views/production-master-data/factory-model/factory-area/index.vue
  2. æ¿æ<EFBFBD>¢ API 引用(import { getList, create, edit, ... } from '@/api/xxx'ï¼?3. 修改 i18nMixin å<>æ•°ã€<C3A3>columnsã€<EFBFBD>formColsã€<EFBFBD>rules
  3. åœ?zh-chs.json å’?en.json 中添加语言åŒ?5. 添加路由é…<C3A9>ç½®

å¹³å<EFBFBD>‡ 10 分éŸå<C5B8>³å<C2B3>¯å®Œæˆ<C3A6>一个标å‡?CRUD 页é<C2B5>¢ã€?