feat: 新增工厂区域管理页面,修复Sass废弃警告
1. 新增生产配置-工厂模型-工厂区域完整CRUD页面 2. 新增通用表格、弹窗表单、i18n工具组件 3. 升级sass-loader并修复Sass废弃警告 4. 添加文档记录Sass迁移修复细节
This commit is contained in:
43
src/api/production-master-data/factory-area.js
Normal file
43
src/api/production-master-data/factory-area.js
Normal file
@@ -0,0 +1,43 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@use 'sass:list';
|
||||
@import 'public';
|
||||
|
||||
// 补丁 base
|
||||
@@ -39,17 +40,17 @@
|
||||
$sizes: (0, 5, 10, 15, 20);
|
||||
|
||||
@for $index from 1 to 6 {
|
||||
.#{$prefix}-m-#{nth($sizes, $index)} { margin: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-mt-#{nth($sizes, $index)} { margin-top: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-mr-#{nth($sizes, $index)} { margin-right: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-mb-#{nth($sizes, $index)} { margin-bottom: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-ml-#{nth($sizes, $index)} { margin-left: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-m-#{list.nth($sizes, $index)} { margin: #{list.nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-mt-#{list.nth($sizes, $index)} { margin-top: #{list.nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-mr-#{list.nth($sizes, $index)} { margin-right: #{list.nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-mb-#{list.nth($sizes, $index)} { margin-bottom: #{list.nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-ml-#{list.nth($sizes, $index)} { margin-left: #{list.nth($sizes, $index)}px !important; }
|
||||
|
||||
.#{$prefix}-p-#{nth($sizes, $index)} { padding: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-pt-#{nth($sizes, $index)} { padding-top: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-pr-#{nth($sizes, $index)} { padding-right: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-pb-#{nth($sizes, $index)} { padding-bottom: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-pl-#{nth($sizes, $index)} { padding-left: #{nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-p-#{list.nth($sizes, $index)} { padding: #{list.nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-pt-#{list.nth($sizes, $index)} { padding-top: #{list.nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-pr-#{list.nth($sizes, $index)} { padding-right: #{list.nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-pb-#{list.nth($sizes, $index)} { padding-bottom: #{list.nth($sizes, $index)}px !important; }
|
||||
.#{$prefix}-pl-#{list.nth($sizes, $index)} { padding-left: #{list.nth($sizes, $index)}px !important; }
|
||||
}
|
||||
|
||||
// 快速使用
|
||||
|
||||
323
src/components/page-dialog-form/index.vue
Normal file
323
src/components/page-dialog-form/index.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<!--
|
||||
page-dialog-form — 增删改查弹框组件
|
||||
===================================
|
||||
这是一个「带表单验证的弹框」组件,配合 page-table 使用。
|
||||
负责:弹框显隐控制 + 表单渲染 + 表单验证 + 确定/取消按钮。
|
||||
|
||||
不支持复杂表单联动——如有需要,通过默认插槽自定义内容。
|
||||
|
||||
依赖:element-ui 的 <el-dialog> <el-form> <el-input> <el-select>
|
||||
i18n:title / label / placeholder / confirm/cancel 按钮 全部自动 $t() 翻译
|
||||
|
||||
@author 前端团队
|
||||
@since 2026-05
|
||||
-->
|
||||
|
||||
<!--
|
||||
el-dialog:
|
||||
visible.sync → 通过 .sync 修饰符双向绑定父组件的 visible prop
|
||||
destroy-on-close → 每次关闭销毁 DOM 重建,避免表单残留上次数据
|
||||
close-on-click-modal → 禁止点击遮罩关闭,防止误操作丢失填写数据
|
||||
-->
|
||||
<el-dialog
|
||||
:visible.sync="visibleProxy"
|
||||
:title="$t(title)"
|
||||
:width="width"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
@close="onClose"
|
||||
>
|
||||
<!-- ==================== 表单区 ==================== -->
|
||||
<!--
|
||||
el-form:
|
||||
rules 由父组件传入,字段名与 formData 的 key 一一对应
|
||||
label-width 默认 100px,可自定义
|
||||
-->
|
||||
<el-form
|
||||
ref="form"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-width="labelWidth || '100px'"
|
||||
>
|
||||
<!--
|
||||
遍历 formCols 中所有行 → 每行中的每个字段 → 渲染对应的表单项
|
||||
当前支持两种字段类型:
|
||||
- type='input' → <el-input>(支持 textarea、密码等)
|
||||
- type='select' → <el-select> + <el-option>
|
||||
|
||||
label / placeholder 会自动调用 $t() 翻译,传 i18n key 即可
|
||||
-->
|
||||
<el-form-item
|
||||
v-for="col in flatFormCols"
|
||||
:key="col.prop"
|
||||
:label="$t(col.label)"
|
||||
:prop="col.prop"
|
||||
>
|
||||
<!-- ===== 输入框类型 ===== -->
|
||||
<!--
|
||||
input:
|
||||
inputType='textarea' → 多行文本框
|
||||
不传 inputType → 普通文本输入框
|
||||
clearable → 默认 true,传 false 可关闭
|
||||
-->
|
||||
<el-input
|
||||
v-if="col.type === 'input'"
|
||||
v-model="formData[col.prop]"
|
||||
:placeholder="$t(col.placeholder)"
|
||||
:type="col.inputType || 'text'"
|
||||
:autosize="col.autosize"
|
||||
:clearable="col.clearable !== false"
|
||||
:style="col.style"
|
||||
/>
|
||||
<!-- ===== 下拉选择类型 ===== -->
|
||||
<!--
|
||||
select:
|
||||
options → [{ label: '苹果', value: 1 }]
|
||||
filterable → 默认 true,支持搜索过滤
|
||||
-->
|
||||
<el-select
|
||||
v-else-if="col.type === 'select'"
|
||||
v-model="formData[col.prop]"
|
||||
:placeholder="$t(col.placeholder)"
|
||||
:clearable="col.clearable !== false"
|
||||
:style="col.style"
|
||||
:filterable="col.filterable !== false"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in col.options"
|
||||
:key="opt.value"
|
||||
:label="$t(opt.label)"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- ===== 自定义表单内容插槽 ===== -->
|
||||
<!--
|
||||
如果需要超出 input/select 的复杂表单控件
|
||||
父组件可以用此插槽添加任意内容
|
||||
-->
|
||||
<slot />
|
||||
</el-form>
|
||||
|
||||
<!-- ==================== 底部按钮 ==================== -->
|
||||
<!--
|
||||
确定:type='primary' + submitting loading 状态
|
||||
取消:调用 onClose → 重置表单 + 关闭弹框
|
||||
按钮文字自动 $t() 翻译
|
||||
-->
|
||||
<template #footer>
|
||||
<el-button @click="onClose">{{ $t(cancelText) }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ $t(confirmText) }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* PageDialogForm — 增删改查弹框组件
|
||||
*
|
||||
* 【核心职责】
|
||||
* 1. 管理弹框显隐
|
||||
* 2. 根据 formCols 声明式渲染表单
|
||||
* 3. 表单验证 + 验证失败提示
|
||||
* 4. 提交/取消事件通知父组件
|
||||
*
|
||||
* 【典型用法】
|
||||
* <page-dialog-form
|
||||
* ref="dialogForm"
|
||||
* :visible.sync="dialogVisible"
|
||||
* :title="dialogTitle"
|
||||
* width="35%"
|
||||
* :form-cols="formCols"
|
||||
* :form-data="formData"
|
||||
* :rules="rules"
|
||||
* :submitting="submitting"
|
||||
* @submit="onDialogSubmit"
|
||||
* @close="onDialogClose"
|
||||
* />
|
||||
*
|
||||
* 【父组件调用的方法(通过 ref)】
|
||||
* this.$refs.dialogForm.reset() — 重置表单
|
||||
* this.$refs.dialogForm.validate() — 手动验证,返回 Promise<boolean>
|
||||
*
|
||||
* 【formCols 数据结构说明】
|
||||
* 二维数组:外层是行,里层是该行的字段。大多数场景每行一个字段即可。
|
||||
*
|
||||
* 例:
|
||||
* formCols: [
|
||||
* [ { type: 'input', prop: 'code', label: T+'.code', placeholder: T+'.enter_code' } ],
|
||||
* [ { type: 'input', prop: 'remark', label: T+'.remark', inputType: 'textarea', autosize: { minRows: 2 } } ],
|
||||
* [ { type: 'select', prop: 'area_id', label: T+'.area', options: [{ label: 'A区', value: 1 }] } ]
|
||||
* ]
|
||||
*
|
||||
* 【表单字段支持的属性】
|
||||
* 基础:type / prop / label / placeholder
|
||||
* input:inputType('textarea' | 'text')/ autosize / clearable
|
||||
* select:options / filterable
|
||||
* 通用:style(如 { width: '90%' })
|
||||
*
|
||||
* 【表单验证(rules)】
|
||||
* rules 与 el-form 的 rules 完全一致:
|
||||
* rules: {
|
||||
* code: [{ required: true, message: '请输入编码', trigger: 'blur' }]
|
||||
* }
|
||||
* 验证失败时,组件会用 $message.warning 显示第一条错误信息
|
||||
* message 字段支持 i18n key,父组件用 this.$t() 传值即可
|
||||
*/
|
||||
export default {
|
||||
name: 'PageDialogForm',
|
||||
props: {
|
||||
/**
|
||||
* 弹框显隐状态,父组件用 .sync 绑定
|
||||
* 例::visible.sync="dialogVisible"
|
||||
*/
|
||||
visible: { type: Boolean, default: false },
|
||||
|
||||
/**
|
||||
* 弹框标题,支持 i18n key(组件自动 $t() 翻译)
|
||||
*/
|
||||
title: { type: String, default: '' },
|
||||
|
||||
/**
|
||||
* 弹框宽度,可以是百分比或像素
|
||||
* 例:'35%' / '600px'
|
||||
*/
|
||||
width: { type: String, default: '35%' },
|
||||
|
||||
/**
|
||||
* 表单字段结构,二维数组
|
||||
* 外层数组每一个元素代表表单的一行
|
||||
* 内层数组每个元素代表该行中的一个字段
|
||||
*
|
||||
* 每个字段对象支持的属性见组件顶部的注释
|
||||
*/
|
||||
formCols: { type: Array, default: () => [] },
|
||||
|
||||
/**
|
||||
* 表单数据对象,key 与 formCols 中的 prop 一一对应
|
||||
* 编辑时父组件将 row 数据赋值给 formData
|
||||
*/
|
||||
formData: { type: Object, default: () => ({}) },
|
||||
|
||||
/**
|
||||
* 表单验证规则,与 el-form 的 rules 完全一致
|
||||
* 例:{ code: [{ required: true, message: '请输入编码', trigger: 'blur' }] }
|
||||
*/
|
||||
rules: { type: Object, default: () => ({}) },
|
||||
|
||||
/**
|
||||
* 表单 label 宽度,默认 '100px'
|
||||
*/
|
||||
labelWidth: { type: String, default: '100px' },
|
||||
|
||||
/**
|
||||
* 提交 loading 状态,提交期间显示转圈防止重复点击
|
||||
*/
|
||||
submitting: { type: Boolean, default: false },
|
||||
|
||||
/**
|
||||
* 确定按钮文字,默认 '确定',支持 i18n key
|
||||
*/
|
||||
confirmText: { type: String, default: '确定' },
|
||||
|
||||
/**
|
||||
* 取消按钮文字,默认 '取消',支持 i18n key
|
||||
*/
|
||||
cancelText: { type: String, default: '取消' }
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* visible 代理:用于 .sync 双向绑定
|
||||
* get → 返回父组件传入的 visible
|
||||
* set → emit update:visible 通知父组件
|
||||
*/
|
||||
visibleProxy: {
|
||||
get () { return this.visible },
|
||||
set (val) { this.$emit('update:visible', val) }
|
||||
},
|
||||
|
||||
/**
|
||||
* 将二维 formCols 打平为一维数组,方便 v-for 遍历
|
||||
* 二维数组 → 一维数组:[ [{a}], [{b},{c}] ] → [a, b, c]
|
||||
*/
|
||||
flatFormCols () {
|
||||
return this.formCols.flat()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 点击确定按钮
|
||||
* 1. 调用 el-form 的 validate 方法验证所有字段
|
||||
* 2. 验证通过 → emit('submit') 通知父组件执行提交逻辑
|
||||
* 3. 验证失败 → 用 $message.warning 显示第一条错误信息
|
||||
*
|
||||
* 注意:rules 中的 message 由父组件传入时已用 $t() 翻译
|
||||
* 这里只负责显示,翻译在父组件或 i18n 语言包中完成
|
||||
*/
|
||||
onSubmit () {
|
||||
this.$refs.form.validate((valid, invalidFields) => {
|
||||
if (!valid) {
|
||||
// 验证失败:取第一条错误信息提示用户
|
||||
const firstKey = Object.keys(invalidFields)[0]
|
||||
if (firstKey && invalidFields[firstKey].length) {
|
||||
const msg = invalidFields[firstKey][0].message
|
||||
this.$message.warning(this.$t(msg) || msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
this.$emit('submit')
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭弹框
|
||||
* 1. 重置表单清除验证状态和输入内容
|
||||
* 2. emit update:visible 关闭弹框(.sync 机制)
|
||||
* 3. emit close 通知父组件执行清理逻辑
|
||||
*/
|
||||
onClose () {
|
||||
this.$refs.form && this.$refs.form.resetFields()
|
||||
this.$emit('update:visible', false)
|
||||
this.$emit('close')
|
||||
},
|
||||
|
||||
/**
|
||||
* 手动验证表单,返回 Promise<boolean>
|
||||
* 主要用于父组件需要自行控制验证时机的场景
|
||||
*
|
||||
* 用法:const ok = await this.$refs.dialogForm.validate()
|
||||
*/
|
||||
validate () {
|
||||
return new Promise(resolve => {
|
||||
this.$refs.form.validate(valid => resolve(valid))
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置表单(清除验证状态和输入值)
|
||||
* 通常在打开新增弹框时调用
|
||||
*/
|
||||
reset () {
|
||||
this.$refs.form && this.$refs.form.resetFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 表单项间距固定 22px,不受 Element UI 状态覆盖 */
|
||||
/deep/ .el-form-item {
|
||||
margin-bottom: 22px !important;
|
||||
}
|
||||
/* 错误文字下沉到 margin 间隙中,不挤占表单项自身高度 */
|
||||
/deep/ .el-form-item__error {
|
||||
position: absolute;
|
||||
top: auto;
|
||||
bottom: -18px;
|
||||
}
|
||||
/* textarea 对齐顶部 */
|
||||
/deep/ .el-textarea {
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
520
src/components/page-table/index.vue
Normal file
520
src/components/page-table/index.vue
Normal file
@@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<!--
|
||||
page-table — CRUD 表格便捷组合体
|
||||
================================
|
||||
这是一个把「顶部按钮栏 + 数据表格 + 底部分页」打包在一起的高层组件。
|
||||
适合 80% 的增删改查页面,你只需要传列定义、按钮定义、数据和分页参数即可。
|
||||
|
||||
依赖:element-ui 的 <el-table> <el-button> <el-pagination>
|
||||
i18n:所有 label / 按钮文字 / 列标题 自动调用 $t() 翻译,传 i18n key 即可
|
||||
|
||||
@author 前端团队
|
||||
@since 2026-05
|
||||
-->
|
||||
<div
|
||||
ref="root"
|
||||
class="page-table"
|
||||
:class="{ 'page-table--auto': autoHeight || height === 'auto' }"
|
||||
v-loading="loading"
|
||||
>
|
||||
<!-- ==================== 顶部:工具栏按钮区 ==================== -->
|
||||
<!--
|
||||
toolbarButtons 由 useTableButtons({ toolbar: [...] }) 生成
|
||||
内部自动过滤 hasPermission === false 的按钮(无权限不显示)
|
||||
按钮被 disabled 只在前端计算(如 needSelection 需要选中行),实际权限由后端校验
|
||||
-->
|
||||
<div ref="toolbar" class="page-table__toolbar" v-if="toolbarButtons.length || helpUrl">
|
||||
<div class="page-table__toolbar-left">
|
||||
<el-button
|
||||
v-for="btn in visibleToolbarButtons"
|
||||
:key="btn.key"
|
||||
:type="btn.type"
|
||||
:size="btn.size"
|
||||
:icon="btn.icon"
|
||||
:style="btn.cssStyle"
|
||||
:disabled="btn.needSelection && !selectedCount"
|
||||
@click="btn.onClick"
|
||||
>
|
||||
{{ $t(btn.label) }}
|
||||
</el-button>
|
||||
<!-- 自定义工具栏内容:如打印按钮、列筛选器等 -->
|
||||
<slot name="toolbar-extra" />
|
||||
</div>
|
||||
<!-- 帮助按钮:始终显示在工具栏最右侧,点击新窗口打开帮助文档 -->
|
||||
<el-button
|
||||
v-if="helpUrl"
|
||||
class="page-table__help-btn"
|
||||
size="mini"
|
||||
icon="el-icon-question"
|
||||
@click="openHelp"
|
||||
>
|
||||
{{ $t(helpText) }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 中部:数据表格 ==================== -->
|
||||
<!--
|
||||
height 由外部传入或自动计算(autoHeight=true 时)
|
||||
v-bind="tableAttrs" / v-on="tableListeners" 透传 el-table 原生属性和事件
|
||||
-->
|
||||
<div ref="tableWrapper" class="page-table__body">
|
||||
<el-table
|
||||
ref="table"
|
||||
:data="tableData"
|
||||
:height="tableHeight"
|
||||
:border="border"
|
||||
:row-key="rowKey"
|
||||
:header-cell-style="headerCellStyle"
|
||||
v-bind="tableAttrs"
|
||||
v-on="tableListeners"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<!--
|
||||
遍历 columns,有五种列类型:
|
||||
1. type='selection' → 复选框列
|
||||
2. type='index' → 序号列
|
||||
3. slot='_actions' → 操作列(自动渲染 rowButtons)
|
||||
4. slot/headerSlot → 自定义插槽列
|
||||
5. 其他 → 普通数据列
|
||||
-->
|
||||
<template v-for="col in columns">
|
||||
<!-- 1. 复选框列 -->
|
||||
<el-table-column
|
||||
v-if="col.type === 'selection'"
|
||||
:key="'sel-' + col.idx"
|
||||
type="selection"
|
||||
:width="col.width"
|
||||
/>
|
||||
<!-- 2. 序号列 -->
|
||||
<el-table-column
|
||||
v-else-if="col.type === 'index'"
|
||||
:key="'idx-' + col.idx"
|
||||
type="index"
|
||||
:width="col.width"
|
||||
:label="$t(col.label || '#')"
|
||||
/>
|
||||
<!-- 3. 操作列:自动渲染 rowButtons(编辑/删除等行内按钮) -->
|
||||
<!--
|
||||
约定:columns 中 prop='_actions' 的列会被识别为操作列
|
||||
rowButtons 由 useTableButtons({ row: [...] }) 生成
|
||||
红色按钮通过 color='danger' 控制
|
||||
-->
|
||||
<el-table-column
|
||||
v-else-if="col.slot === '_actions' && rowButtons.length"
|
||||
:key="'act-' + col.idx"
|
||||
v-bind="colAttrs(col)"
|
||||
>
|
||||
<template #default="{ row, $index }">
|
||||
<span
|
||||
v-for="btn in visibleRowButtons"
|
||||
:key="btn.key"
|
||||
:style="btn.cssStyle"
|
||||
:class="{ 'action-btn--danger': btn.color === 'danger' }"
|
||||
class="action-btn"
|
||||
@click="btn.onClick(row, $index)"
|
||||
>
|
||||
<i v-if="btn.icon" :class="btn.icon" />
|
||||
{{ $t(btn.label) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 4. 自定义插槽列:列内容或表头可由父组件自定义 -->
|
||||
<!--
|
||||
使用方式:
|
||||
- col.slot='status' → 父组件用 <template #col-status="{ row }">
|
||||
- col.headerSlot='xyz' → 父组件用 <template #xyz> 自定义表头
|
||||
-->
|
||||
<el-table-column
|
||||
v-else-if="col.slot || col.headerSlot"
|
||||
:key="'slot-' + col.idx"
|
||||
v-bind="colAttrs(col)"
|
||||
>
|
||||
<template #header="{ column }">
|
||||
<slot v-if="col.headerSlot" :name="col.headerSlot" :data="col" :column="column">
|
||||
<span>{{ column.label }}</span>
|
||||
</slot>
|
||||
<span v-else>{{ column.label }}</span>
|
||||
</template>
|
||||
<template #default="{ row, $index }">
|
||||
<slot :name="'col-' + col.slot" :row="row" :index="$index">
|
||||
<span>{{ row[col.prop] }}</span>
|
||||
</slot>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 5. 普通数据列:直接按 prop 取 row 数据 -->
|
||||
<el-table-column
|
||||
v-else
|
||||
:key="'col-' + col.idx"
|
||||
v-bind="colAttrs(col)"
|
||||
/>
|
||||
</template>
|
||||
<!-- 表格无数据时显示的占位内容 -->
|
||||
<template #empty>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
<!-- 表格最后一行之后追加的内容 -->
|
||||
<template #append>
|
||||
<slot name="append" />
|
||||
</template>
|
||||
<slot />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 底部:分页组件 ==================== -->
|
||||
<!--
|
||||
只传 pagination 对象({ current, size, total })才显示分页
|
||||
page-change 事件回传 { current, size, total },父组件更新 pagination 后重新 fetchData
|
||||
-->
|
||||
<div ref="footer" class="page-table__footer" v-if="pagination">
|
||||
<el-pagination
|
||||
:current-page="pagination.current"
|
||||
:page-size="pagination.size || 10"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 25, 50, 100, 250, 500]"
|
||||
:disabled="loading"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 自定义尾部内容 -->
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* PageTable — CRUD 表格便捷组合体
|
||||
*
|
||||
* 【核心职责】
|
||||
* 1. 渲染顶部工具栏(增删改查等操作按钮,自动权限过滤)
|
||||
* 2. 渲染数据表格(5 种列类型,自动 i18n 翻译)
|
||||
* 3. 渲染底部分页
|
||||
* 4. 表格高度自适应(可选)
|
||||
*
|
||||
* 【典型用法】
|
||||
* <page-table
|
||||
* :columns="columns"
|
||||
* :data="tableData"
|
||||
* :loading="loading"
|
||||
* :toolbar-buttons="toolbarButtons"
|
||||
* :row-buttons="rowButtons"
|
||||
* :pagination="pagination"
|
||||
* auto-height
|
||||
* @page-change="onPageChange"
|
||||
* @selection-change="onSelect"
|
||||
* />
|
||||
*
|
||||
* 【内部事件流】
|
||||
* toolbarButtons[i].onClick() → 父组件方法 → fetchData → data 更新 → 表格刷新
|
||||
* el-pagination @change → emit('page-change') → 父组件更新 pagination → fetchData
|
||||
* el-table @selection-change → emit('selection-change') → 父组件获取选中行
|
||||
*
|
||||
* 【与 useTableColumns / useTableButtons 配合】
|
||||
* 详见文档:docs/sct-base-table-refactor-design.md
|
||||
*/
|
||||
export default {
|
||||
name: 'PageTable',
|
||||
props: {
|
||||
/**
|
||||
* 列定义,由 useTableColumns() 生成
|
||||
*
|
||||
* 普通列:{ prop: 'code', label: '编码', minWidth: 120 }
|
||||
* 操作列:{ prop: '_actions', label: '操作', width: 160, fixed: 'right' }
|
||||
* 自定义列:{ prop: 'status', label: '状态', slot: true }
|
||||
*
|
||||
* 组件内部会按 type/slot/prop 自动分类渲染不同的 <el-table-column>
|
||||
*/
|
||||
columns: { type: Array, default: () => [] },
|
||||
|
||||
/**
|
||||
* 表格行数据,直接传接口返回的数组
|
||||
*/
|
||||
data: { type: Array, default: () => [] },
|
||||
|
||||
/**
|
||||
* 是否显示 loading 遮罩,通常在 fetchData 期间为 true
|
||||
*/
|
||||
loading: { type: Boolean, default: false },
|
||||
|
||||
/**
|
||||
* 表格高度,不传则 el-table 按内容撑开
|
||||
* 传 'auto' 或同时传 auto-height 启用自动计算(窗口高度 - 搜索区 - 工具栏 - 分页)
|
||||
* 传数字/字符串(如 '500px')则固定高度
|
||||
*/
|
||||
height: { type: [String, Number], default: undefined },
|
||||
|
||||
/**
|
||||
* 启用表格高度自适应:根据容器可视区域自动计算高度
|
||||
* 启用后 height prop 被忽略,组件内部通过 ResizeObserver 持续计算
|
||||
*/
|
||||
autoHeight: { type: Boolean, default: false },
|
||||
|
||||
/**
|
||||
* 是否显示表格边框,默认 true
|
||||
*/
|
||||
border: { type: Boolean, default: true },
|
||||
|
||||
/**
|
||||
* 行数据的唯一 key,默认 'id',用于 el-table 的 row-key
|
||||
* 树形表格或需要保留选中状态时必需
|
||||
*/
|
||||
rowKey: { type: String, default: 'id' },
|
||||
|
||||
/**
|
||||
* 顶部工具栏按钮列表,由 useTableButtons({ toolbar: [...] }) 生成
|
||||
*
|
||||
* 每个按钮字段:
|
||||
* key - 唯一标识(必填)
|
||||
* label - 显示文本,支持 i18n key(组件自动 $t() 翻译)
|
||||
* icon - element-ui 图标名('el-icon-plus' 等)
|
||||
* type - 按钮样式类型('primary' | 'success' | 'warning' | 'danger')
|
||||
* auth - 权限 key,传给 useTableButtons 的第二个参数判断
|
||||
* cssStyle - 自定义样式对象
|
||||
* needSelection - true 的话需要表格有选中行才能点击
|
||||
* onClick - 点击回调(由父组件绑定方法)
|
||||
* hasPermission - 自动注入,由 useTableButtons 计算(不要手动设)
|
||||
*/
|
||||
toolbarButtons: { type: Array, default: () => [] },
|
||||
|
||||
/**
|
||||
* 行内操作按钮列表,由 useTableButtons({ row: [...] }) 生成
|
||||
*
|
||||
* 每个按钮字段:
|
||||
* key / label / icon / auth / onClick — 同 toolbarButtons
|
||||
* color - 'danger' 文字变红,用于删除等危险操作
|
||||
* cssStyle - 默认 { marginRight: '10px', cursor: 'pointer' }
|
||||
*/
|
||||
rowButtons: { type: Array, default: () => [] },
|
||||
|
||||
/**
|
||||
* 分页参数,传此 prop 才显示分页组件
|
||||
*
|
||||
* { current: 1, size: 10, total: 0 }
|
||||
*
|
||||
* total 通常在 getList 接口返回 res.count
|
||||
*/
|
||||
pagination: { type: Object, default: null },
|
||||
|
||||
/**
|
||||
* 额外透传给 el-table 的原生属性,如 { stripe: true, size: 'medium' }
|
||||
*/
|
||||
tableAttrs: { type: Object, default: () => ({}) },
|
||||
|
||||
/**
|
||||
* 额外透传给 el-table 的原生事件,如 { 'sort-change': handleSort }
|
||||
*/
|
||||
tableListeners: { type: Object, default: () => ({}) },
|
||||
|
||||
/**
|
||||
* 帮助文档的跳转 URL。传了才显示工具栏右侧的问号按钮,点击新窗口打开
|
||||
* 例:'/docs/factory-area-help.html'
|
||||
*/
|
||||
helpUrl: { type: String, default: '' },
|
||||
|
||||
/**
|
||||
* 帮助按钮的文字,支持 i18n key(组件自动 $t() 翻译)
|
||||
* 默认 '帮助'
|
||||
*/
|
||||
helpText: { type: String, default: '帮助' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tableSelected: [], // 当前选中的行数据
|
||||
computedHeight: null // autoHeight 时动态计算的高度
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 最终使用的表格高度:
|
||||
* - 用户明确传了 height(非 'auto')→ 直接用
|
||||
* - autoHeight=true 或 height='auto' → 用动态计算的 computedHeight
|
||||
* - 否则 undefined(el-table 按内容自适应)
|
||||
*/
|
||||
tableHeight () {
|
||||
if (this.height && this.height !== 'auto') return this.height
|
||||
if (this.autoHeight || this.height === 'auto') return this.computedHeight || undefined
|
||||
return undefined
|
||||
},
|
||||
|
||||
/**
|
||||
* 过滤后的工具栏按钮(排除无权限的)
|
||||
*/
|
||||
visibleToolbarButtons () {
|
||||
return this.toolbarButtons.filter(b => b.hasPermission !== false)
|
||||
},
|
||||
|
||||
/**
|
||||
* 过滤后的行内操作按钮(排除无权限的)
|
||||
*/
|
||||
visibleRowButtons () {
|
||||
return this.rowButtons.filter(b => b.hasPermission !== false)
|
||||
},
|
||||
|
||||
/**
|
||||
* 选中的行数
|
||||
*/
|
||||
selectedCount () {
|
||||
return this.tableSelected.length
|
||||
},
|
||||
|
||||
/**
|
||||
* 表头样式:浅灰背景 + 黑色加粗文字
|
||||
*/
|
||||
headerCellStyle () {
|
||||
return () => 'background-color: #F8F8F8; color: #000000; font-weight: 500;'
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.autoHeight || this.height === 'auto') {
|
||||
this.startAutoHeight()
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.stopAutoHeight()
|
||||
},
|
||||
methods: {
|
||||
/* ============ 列处理 ============ */
|
||||
|
||||
colAttrs (col) {
|
||||
const attrs = { ...col }
|
||||
delete attrs.idx
|
||||
delete attrs.slot
|
||||
delete attrs.headerSlot
|
||||
if (attrs.label) {
|
||||
attrs.label = this.$t(attrs.label)
|
||||
}
|
||||
return attrs
|
||||
},
|
||||
|
||||
/* ============ 表格事件 ============ */
|
||||
|
||||
onSelectionChange (val) {
|
||||
this.tableSelected = val
|
||||
this.$emit('selection-change', val)
|
||||
},
|
||||
|
||||
/* ============ 帮助按钮 ============ */
|
||||
|
||||
openHelp () {
|
||||
if (this.helpUrl) {
|
||||
window.open(this.helpUrl, '_blank')
|
||||
}
|
||||
},
|
||||
|
||||
/* ============ 分页事件 ============ */
|
||||
|
||||
onSizeChange (size) {
|
||||
this.$emit('page-change', {
|
||||
current: 1,
|
||||
size,
|
||||
total: this.pagination.total
|
||||
})
|
||||
},
|
||||
|
||||
onCurrentChange (current) {
|
||||
this.$emit('page-change', {
|
||||
current,
|
||||
size: this.pagination.size,
|
||||
total: this.pagination.total
|
||||
})
|
||||
},
|
||||
|
||||
/* ============ 自适应高度 ============ */
|
||||
|
||||
/**
|
||||
* 启动表格高度自适应
|
||||
*
|
||||
* 原理:
|
||||
* 1. CSS flex 布局让 page-table 占满父容器 100% 高度
|
||||
* 2. 工具栏和分页各占自然高度(flex-shrink: 0)
|
||||
* 3. tableWrapper 中间区域自动填充剩余空间(flex: 1 + min-height: 0)
|
||||
* 4. ResizeObserver 监听 tableWrapper 的实际渲染高度,赋给 el-table
|
||||
*
|
||||
* 这样 el-table 永远精确等于可用空间,不会产生外层滚动条
|
||||
*/
|
||||
startAutoHeight () {
|
||||
this.stopAutoHeight()
|
||||
|
||||
this._resizeObserver = new ResizeObserver(() => {
|
||||
this.$nextTick(() => {
|
||||
const wrapper = this.$refs.tableWrapper
|
||||
if (wrapper) {
|
||||
this.computedHeight = wrapper.clientHeight
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (this.$refs.tableWrapper) {
|
||||
this._resizeObserver.observe(this.$refs.tableWrapper)
|
||||
this.$nextTick(() => {
|
||||
const wrapper = this.$refs.tableWrapper
|
||||
if (wrapper) {
|
||||
this.computedHeight = wrapper.clientHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
stopAutoHeight () {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect()
|
||||
this._resizeObserver = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
/* 自适应高度模式:占满父容器 100%,禁止自身溢出 */
|
||||
.page-table--auto {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-table__toolbar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.page-table__toolbar-left {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.page-table__help-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
.page-table__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.page-table__footer {
|
||||
flex-shrink: 0;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.action-btn {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.action-btn--danger {
|
||||
color: #F56C6C;
|
||||
}
|
||||
</style>
|
||||
28
src/composables/useI18n.js
Normal file
28
src/composables/useI18n.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* i18n Mixin 工厂:消除每个页面重复写 tkey() 方法
|
||||
*
|
||||
* @param {string} prefix 页面的 i18n key 前缀,如 'page.production_master_data.factory_model.factory_area'
|
||||
* @returns {Object} Vue mixin
|
||||
*
|
||||
* @example
|
||||
* 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'
|
||||
* }
|
||||
*/
|
||||
export function i18nMixin (prefix) {
|
||||
return {
|
||||
methods: {
|
||||
key (suffix) {
|
||||
return prefix + '.' + suffix
|
||||
},
|
||||
ckey (suffix) {
|
||||
return 'page.common.' + suffix
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default i18nMixin
|
||||
48
src/composables/useTableButtons.js
Normal file
48
src/composables/useTableButtons.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 按钮定义工具:消除 buttonList / tableButtonList 重复定义
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Array} options.toolbar 顶部工具栏按钮
|
||||
* @param {Array} options.row 行内操作按钮
|
||||
* @param {Function} permissionCheck 权限校验函数,默认 $permission
|
||||
* @returns {{ toolbarButtons: Array, rowButtons: Array }}
|
||||
*
|
||||
* @example
|
||||
* import useTableButtons from '@/composables/useTableButtons'
|
||||
* // 在组件 data() 或 created() 中调用
|
||||
* const { toolbarButtons, rowButtons } = useTableButtons({
|
||||
* toolbar: [{ key: 'add', label: '新建', type: 'primary', auth: '/xxx/create', onClick: this.openDialog }],
|
||||
* row: [{ key: 'edit', label: '编辑', auth: '/xxx/edit', onClick: this.handleEdit }],
|
||||
* }, this.$permission)
|
||||
*/
|
||||
export function useTableButtons (options = {}, permissionCheck) {
|
||||
const check = permissionCheck || (() => true)
|
||||
|
||||
const toolbarButtons = (options.toolbar || []).map(btn => ({
|
||||
key: btn.key || btn.label,
|
||||
label: btn.label,
|
||||
icon: btn.icon,
|
||||
type: btn.type || '',
|
||||
size: btn.size || 'mini',
|
||||
auth: btn.auth,
|
||||
cssStyle: btn.cssStyle || {},
|
||||
onClick: btn.onClick,
|
||||
hasPermission: btn.auth ? check(btn.auth) : true
|
||||
}))
|
||||
|
||||
const rowButtons = (options.row || []).map(btn => ({
|
||||
key: btn.key || btn.label,
|
||||
label: btn.label,
|
||||
icon: btn.icon,
|
||||
color: btn.color || '',
|
||||
cssStyle: btn.cssStyle || { marginRight: '10px', cursor: 'pointer' },
|
||||
auth: btn.auth,
|
||||
confirm: btn.confirm || false,
|
||||
onClick: btn.onClick,
|
||||
hasPermission: btn.auth ? check(btn.auth) : true
|
||||
}))
|
||||
|
||||
return { toolbarButtons, rowButtons }
|
||||
}
|
||||
|
||||
export default useTableButtons
|
||||
46
src/composables/useTableColumns.js
Normal file
46
src/composables/useTableColumns.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 列定义工具:消除手动分配 idx,支持 _actions 约定
|
||||
*
|
||||
* @param {Array<Object>} rawColumns 列配置数组
|
||||
* @param {Object} options
|
||||
* @param {number} options.selectionWidth 复选框列宽度,默认 55,传 0 不显示
|
||||
* @param {number} options.indexWidth 序号列宽度,默认 60,传 0 不显示
|
||||
* @returns {Array<Object>} 补齐 idx 后的列数组
|
||||
*
|
||||
* @example
|
||||
* const columns = useTableColumns([
|
||||
* { prop: 'code', label: '编码', minWidth: 120 },
|
||||
* { prop: 'name', label: '名称' },
|
||||
* { prop: '_actions', label: '操作', width: 180, fixed: 'right' },
|
||||
* ])
|
||||
*/
|
||||
export function useTableColumns (rawColumns, options = {}) {
|
||||
const { selectionWidth = 55, indexWidth = 0 } = options
|
||||
|
||||
const result = []
|
||||
|
||||
if (selectionWidth > 0) {
|
||||
result.push({ idx: 0, type: 'selection', width: selectionWidth })
|
||||
}
|
||||
if (indexWidth > 0) {
|
||||
result.push({ idx: result.length, type: 'index', width: indexWidth, label: '#' })
|
||||
}
|
||||
|
||||
let slotIdx = 100
|
||||
rawColumns.forEach((col, i) => {
|
||||
const clean = { ...col }
|
||||
if (clean.prop === '_actions') {
|
||||
clean.slot = '_actions'
|
||||
clean.idx = slotIdx++
|
||||
} else if (clean.slot || clean.headerSlot) {
|
||||
clean.idx = slotIdx++
|
||||
} else {
|
||||
clean.idx = result.length
|
||||
}
|
||||
result.push(clean)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default useTableColumns
|
||||
@@ -2,12 +2,47 @@
|
||||
"_element": "en",
|
||||
"_name": "English",
|
||||
"page": {
|
||||
"common": {
|
||||
"help": "Help"
|
||||
},
|
||||
"demo": {
|
||||
"playground": {
|
||||
"locales": {
|
||||
"text": "D2Admin is a fully open source and free enterprise back-end product front-end integration solution, using the latest front-end technology stack, has prepared most of the project preparations, and with a lot of sample code to help the management system agile development."
|
||||
}
|
||||
}
|
||||
},
|
||||
"production_master_data": {
|
||||
"factory_model": {
|
||||
"factory_area": {
|
||||
"search": "Search",
|
||||
"reset": "Reset",
|
||||
"enter_code": "Please enter area code",
|
||||
"enter_name": "Please enter area name",
|
||||
"operation_success": "Operation succeeded",
|
||||
"create_success": "Created successfully",
|
||||
"edit_success": "Updated successfully",
|
||||
"delete_success": "Deleted successfully",
|
||||
"sort": "No.",
|
||||
"code": "Area Code",
|
||||
"name": "Area Name",
|
||||
"remark": "Remark",
|
||||
"operation": "Actions",
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"remark_required": "Please enter remark",
|
||||
"remark_length": "Length should be 1 to 100 characters",
|
||||
"add_title": "Add Area",
|
||||
"edit_title": "Edit Area",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"tip": "Tip",
|
||||
"confirm_delete": "Are you sure to delete?",
|
||||
"validation_fail": "Validation failed",
|
||||
"please_enter": "Please enter {name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,47 @@
|
||||
"_element": "zh-CN",
|
||||
"_name": "简体中文",
|
||||
"page": {
|
||||
"common": {
|
||||
"help": "帮 助"
|
||||
},
|
||||
"demo": {
|
||||
"playground": {
|
||||
"locales": {
|
||||
"text": "D2Admin 是一个完全 开源免费 的企业中后台产品前端集成方案,使用最新的前端技术栈,已经做好大部分项目前期准备工作,并且带有大量示例代码,助力管理系统敏捷开发。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"production_master_data": {
|
||||
"factory_model": {
|
||||
"factory_area": {
|
||||
"search": "查询",
|
||||
"reset": "重置",
|
||||
"enter_code": "请输入所区编码",
|
||||
"enter_name": "请输入所区名称",
|
||||
"operation_success": "操作成功",
|
||||
"create_success": "新增成功",
|
||||
"edit_success": "编辑成功",
|
||||
"delete_success": "删除成功",
|
||||
"sort": "序号",
|
||||
"code": "所区编码",
|
||||
"name": "所区名称",
|
||||
"remark": "备注",
|
||||
"operation": "操作",
|
||||
"add": "新 增",
|
||||
"edit": "编 辑",
|
||||
"delete": "删 除",
|
||||
"remark_required": "请输入备注",
|
||||
"remark_length": "长度在 1 到 100 个字符",
|
||||
"add_title": "新增所区",
|
||||
"edit_title": "编辑所区",
|
||||
"cancel": "取消",
|
||||
"confirm": "确定",
|
||||
"tip": "提示",
|
||||
"confirm_delete": "确定要执行该操作吗?",
|
||||
"validation_fail": "校验失败",
|
||||
"please_enter": "请输入{name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
src/router/modules/production-master-data.js
Normal file
18
src/router/modules/production-master-data.js
Normal file
@@ -0,0 +1,18 @@
|
||||
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-')
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import playground from './modules/playground'
|
||||
import plugins from './modules/plugins'
|
||||
import components from './modules/components'
|
||||
import productionMasterData from './modules/production-master-data'
|
||||
|
||||
import layoutHeaderAside from '@/layout/header-aside'
|
||||
|
||||
@@ -54,7 +55,8 @@ const frameIn = [
|
||||
},
|
||||
playground,
|
||||
plugins,
|
||||
components
|
||||
components,
|
||||
productionMasterData
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
<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"
|
||||
:label-width="'100px'"
|
||||
: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: 'production-master-data-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_configuration/factory_model/factory_area/create',
|
||||
onClick: this.openAdd
|
||||
}
|
||||
],
|
||||
row: [
|
||||
{
|
||||
key: 'edit',
|
||||
label: this.key('edit'),
|
||||
icon: 'el-icon-edit',
|
||||
auth: '/production_configuration/factory_model/factory_area/edit',
|
||||
onClick: this.openEdit
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: this.key('delete'),
|
||||
icon: 'el-icon-delete',
|
||||
color: 'danger',
|
||||
auth: '/production_configuration/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) {
|
||||
// 取消删除 / 请求失败时不处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-bar {
|
||||
padding: 10px 0;
|
||||
}
|
||||
/deep/ .el-form-item--mini.el-form-item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user