Encapsulating Forms and Tables in Vue 3 with Element‑Plus: Design and Implementation
This article demonstrates a comprehensive approach to building reusable form and table components in Vue 3 using Element‑Plus, covering the design rationale, code examples for services, dialogs, list pages, and utilities, and discusses the benefits for development efficiency, maintainability, and scalability.
The author introduces a modular solution for handling forms and tables in a Vue 3 + Element‑Plus project, aiming to standardize list‑page development, improve code reuse, and simplify maintenance across teams.
Service Layer (DemoService.ts) provides mock data and CRUD operations:
export function queryPlatformList() {
const platformList = [
{ name: "淘宝", code: "taobao" },
{ name: "京东", code: "jd" },
{ name: "抖音", code: "douyin" },
];
return platformList;
}
const dataList: any[] = [
{ id: 1, channelType: "sms", channelName: "阿里短信通知", platforms: queryPlatformList().filter(item => item.code !== "taobao"), status: 1, createTime: "2021-09-07 00:52:15", updateTime: "2021-11-07 00:52:15", createBy: "vshen", updateBy: "vshen", ext: { url: "https://sms.aliyun.com", account: "vshen", password: "vshen57", sign: "signVhsen123124" } },
{ id: 2, channelType: "dingtalk", channelName: "预警消息钉钉通知", platforms: queryPlatformList().filter(item => item.code !== "jingdong"), status: 1, createTime: "2021-11-10 00:52:15", updateTime: "2021-11-07 00:52:15", createBy: "vshen", updateBy: "vshen", ext: { accessType: "webhook", address: "https://dingtalk.aliyun.com" } },
{ id: 3, channelType: "email", channelName: "预警消息邮件通知", platforms: queryPlatformList().filter(item => item.code !== "douyin"), status: 0, ext: { host: "https://smpt.aliyun.com", account: "[email protected]", password: "[email protected]" }, createTime: "2021-11-07 00:52:15", updateTime: "2021-11-07 00:52:15", createBy: "vshen", updateBy: "vshen" },
];
export function queryPage({ form }: any, pagenation: any) {
return new Promise(resolve => {
let result: any[] = dataList;
Object.keys(form).forEach(key => {
const value = form[key];
result = dataList.filter(item => item[key] == value);
});
resolve({ success: true, data: { list: result } });
});
}
export function create(data: any = {}) {
return new Promise(resolve => {
setTimeout(() => {
dataList.push({ id: Date.now(), platform: [], ...data });
resolve({ success: true, message: "创建成功!" });
}, 500);
});
}
export function update(data: any) {
return new Promise(resolve => {
setTimeout(() => {
const index = dataList.findIndex(item => item.id == data.id);
const target = dataList[index];
Object.keys(data).forEach(key => { target[key] = data[key]; });
dataList.splice(index, 1, target);
resolve({ success: true, message: "更新成功!" });
}, 500);
});
}
export function remove(id: number) {
return new Promise(resolve => {
setTimeout(() => {
const index = dataList.findIndex(item => item.id == id);
dataList.splice(index, 1);
resolve({ success: true, message: "删除成功!" });
}, 500);
});
}Dynamic Form Dialog (FormDialog.ts) builds the add/edit form based on channel type, using visibility functions and a unified createFormItems generator to produce field configurations.
import { createFormDialog } from "@/components/Dialogs";
import { Toast } from "@/core/adaptor";
import * as DemoService from "@/api/demo-service";
export const ChannelEnum: any = { sms: "短信通知", dingtalk: "钉钉通知", email: "邮件通知" };
export const AccessTypeEnum: any = { webhook: "webhook", api: "api" };
const DingtalkVisiable = (formData: any) => formData.channelType == "dingtalk";
const DingtalkApiVisiable = (formData: any) => DingtalkVisiable(formData) && formData.accessType == AccessTypeEnum.api;
const DingtalkWebhookVisiable = (formData: any) => DingtalkVisiable(formData) && formData.accessType == AccessTypeEnum.webhook;
/* ... form item definitions for DingTalk, SMS, Email ... */
export function createFormItems(isEditMode: boolean, extJson: any = null) {
return [
{ label: "渠道名称", field: "channelName", uiType: "input", required: true },
{ label: "渠道类型", field: "channelType", required: true, uiType: "selector", disabled: isEditMode, props: { options: ChannelEnum } },
...DingTalkFormItems,
...SmsFormItems,
...EmailFormItems,
{ label: "应用于平台", field: "platforms", required: true, uiType: "selector", props: { multiple: true, options: () => DemoService.queryPlatformList() } }
];
}The demo‑list‑page.vue component ties everything together: it declares the table configuration, toolbar actions, filter items, and uses the createOrUpdateChannel helper to open the dialog.
<template>
<list-page v-bind="table">
<template #expand="{ row }">
<el-table :data="row.platforms" border stripe style="padding: 10px; width: 100%">
<el-table-column label="平台名称" prop="name"/>
<el-table-column label="平台编码" prop="code"/>
</el-table>
</template>
<template #status="{ row }">
<el-tag :type="row.status == 1 ? 'info' : 'danger'">{{ statusEnum[row.status] }}</el-tag>
</template>
</list-page>
</template>
<script setup lang="ts">
import { Toast, Dialog } from "@/core/adaptor";
import * as demoService from "@/api/demo-service";
import { createOrUpdateChannel, ChannelEnum } from "./formDialog";
const statusEnum: any = { 0: "禁用", 1: "启用" };
const table = reactive({
loader: (queryForm, pagenation) => demoService.queryPage(queryForm, pagenation),
filterItems: [{ label: "渠道类型", field: "channelType", uiType: "selector", props: { options: ChannelEnum } },{ label: "启用状态", field: "status", uiType: "selector", props: { options: statusEnum } }],
columns: [/* selection, index, expand, channelName, channelType, ... */],
toolbar: [{ text: "新增消息渠道", click: () => createOrUpdateChannel(null, table) }],
actions: [{ text: "编辑", click: (row) => createOrUpdateChannel(row, table) },{ text: row => row.status == 1 ? "禁用" : "启用", click: (row) => {/* toggle status */} }]
});
</script>Sub‑modules such as useTable.ts abstract data loading, handling both array and function loaders, while tableColumns.ts normalises column definitions, adds action columns, and supports different UI table libraries.
export function useTable(dataLoader: Function | any[], searchForm?: ISearchForm) {
const tableData = reactive({ list: [], total: 0, isLoading: false });
async function requestTableData(loader, form) { /* ... */ }
function refreshTableData(model = {}) { requestTableData(dataLoader, Object.assign({}, model, searchForm)); }
return { tableData, refreshTableData };
}Finally, the article presents the generic table.vue , action-button.vue , and tableSettingDrawer.vue** components that enable slot‑based rendering, action‑button confirmation, and user‑customisable column settings, respectively. The author concludes that this configuration‑driven pattern improves development speed, reduces code duplication, and makes the UI easier to maintain.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.