一个前端工程师的年度作品:从零开发媲美商业级应用的后台管理系统!
过去一年,我花了无数个夜晚,在一次次打磨与推翻中,完成了自己最满意的作品 —— Art Design Pro。
这不是一个普通的后台模板。
它是一场关于 「设计美学」与「工程化开发」 的融合实验——
希望让后台系统不再冰冷枯燥,而是像一件作品:优雅、流畅、有温度。
为什么要做这个项目?
在日常开发中,我几乎体验过所有主流后台模板。
它们的功能确实完整,但更多时候给人的感觉是——「工具」而非「产品」。
我希望做一个后台系统,
让人打开的第一眼就觉得舒服,
用起来像是在和它对话。
许多后台系统普遍存在这些问题 👇
「视觉疲劳」:灰白配色、生硬布局,难以长时间使用;
「体验割裂」:逻辑不统一、入口分散,操作效率低;
「复用困难」:组件风格不一致,二次开发成本高。
于是我决定从零开始,花一年的时间去打造一个真正属于自己的系统——
👉 一款 “既好看、又好用,还能直接用于商业项目” 的后台管理模板。
项目简介:Art Design Pro
Art Design Pro 是一款基于 Vue 3 + TypeScript + Vite + Element Plus 打造的现代化后台管理系统模板。
它的核心理念是:
❝让后台系统兼具设计美学与开发效率。
❞
这个项目有什么特别的呢?
「界面设计」:现代化 UI 设计,流畅交互,以用户体验与视觉设计为核心
「极速上手」:简洁架构 + 完整文档,后端开发者也能轻松使用
「丰富组件」:内置数据展示、表单等多种高质量组件,满足不同业务场景的需求
「丝滑交互」:按钮点击、主题切换、页面过渡、图表动画,体验媲美商业产品
「高效开发」:内置 useTable、ArtForm 等实用 API,显著提升开发效率
「精简脚本」:内置一键清理脚本,可快速清理演示数据,立即得到可开发的基础项目
技术栈
开发框架:Vue3、TypeScript、Vite、Element-Plus
代码规范:Eslint、Prettier、Stylelint、Husky、Lint-staged、cz-git
预览
主页仪表盘
电子商务仪表盘
卡片
横幅
图表
系统图标库
富文本编辑器
礼花效果
全局搜索
系统设置
表格
暗黑模式
快速访问
GitHub:GitHub - Daymychen/art-design-pro
演示地址:www.artd.pro
官方文档:www.artd.pro/docs
高效开发
❝在后台管理系统开发中,表格页面占据了 80% 的工作量。每次都要写分页、搜索、刷新、列配置... 这些重复的代码让人头疼。今天分享一套我们团队正在使用的表格开发方案,让你的开发效率提升 10 倍!
❞
痛点分析
在开发后台管理系统时,你是否遇到过这些问题:
每个表格页面都要写一遍分页逻辑 搜索、重置、刷新等功能重复实现 表格列配置、显示隐藏、拖拽排序需要手动处理 数据请求、loading 状态、错误处理代码冗余 缓存策略难以统一管理 移动端适配需要额外处理
如果你的答案是 "是",那这篇文章就是为你准备的。
解决方案概览
我们的方案包含以下核心部分:
「useTable」 - 强大的表格数据管理 Hook 「ArtTable」 - 增强的表格组件 「ArtTableHeader」 - 表格工具栏组件 「ArtSearchBar」 - 智能搜索栏组件 「ArtForm」 - 通用表单组件
一、useTable:表格数据管理的核心
1.1 基础用法
先看一个最简单的例子,只需要几行代码就能实现一个完整的表格:
const {data,columns,columnChecks,loading,pagination,refreshData,handleSizeChange,handleCurrentChange} = useTable({core: {apiFn: fetchGetUserList,apiParams: {current: 1,size: 20},columnsFactory: () => [{ prop: 'id', label: 'ID' },{ prop: 'userName', label: '用户名' },{ prop: 'userPhone', label: '手机号' }]}})
就这么简单!你已经拥有了:
✅ 自动的数据请求 ✅ 分页功能 ✅ Loading 状态 ✅ 列配置管理
1.2 核心特性
🚀 智能缓存机制
useTable({core: {/* ... */},performance: {enableCache: true, // 启用缓存cacheTime: 5 * 60 * 1000, // 缓存 5 分钟debounceTime: 300, // 防抖 300msmaxCacheSize: 50 // 最多缓存 50 条}})
缓存带来的好处:
相同参数的请求直接从缓存读取,秒开 减少服务器压力 提升用户体验
🎯 多种刷新策略
不同的业务场景需要不同的刷新策略:
// 新增数据后:回到第一页,清空分页缓存await refreshCreate()// 编辑数据后:保持当前页,只清空当前搜索缓存await refreshUpdate()// 删除数据后:智能处理页码,避免空页面await refreshRemove()// 手动刷新:清空所有缓存await refreshData()// 定时刷新:轻量刷新,保持分页状态await refreshSoft()
这些方法让你的代码更语义化,不用再纠结什么时候该清缓存。
🔄 数据转换器
有时候接口返回的数据需要处理一下才能用:
useTable({core: {/* ... */},transform: {dataTransformer: (records) => {return records.map((item, index) => ({...item,// 替换头像avatar: localAvatars[index % localAvatars.length].avatar,// 格式化日期createTime: dayjs(item.createTime).format('YYYY-MM-DD')}))}}})
📊 生命周期钩子
useTable({core: {/* ... */},hooks: {onSuccess: (data, response) => {console.log('数据加载成功', data)},onError: (error) => {ElMessage.error('加载失败:' + error.message)},onCacheHit: (data) => {console.log('从缓存读取', data)}}})
1.3 搜索功能
搜索是表格的核心功能,useTable 提供了完善的搜索支持:
// 定义搜索参数const searchParams = reactive({userName: '',userPhone: '',status: '1'})const { getData, resetSearchParams } = useTable({core: {apiFn: fetchGetUserList,apiParams: searchParams}})// 搜索const handleSearch = (params) => {Object.assign(searchParams, params)getData() // 自动回到第一页}// 重置const handleReset = () => {resetSearchParams() // 清空搜索条件并重新加载}
二、ArtTable:增强的表格组件
2.1 核心特性
ArtTable 基于 Element Plus 的 ElTable 封装,完全兼容原有 API,同时提供了更多增强功能:
<ArtTable:loading="loading":data="data":columns="columns":pagination="pagination"@selection-change="handleSelectionChange"@pagination:size-change="handleSizeChange"@pagination:current-change="handleCurrentChange"/>
✨ 自动高度计算
不用再手动计算表格高度了!ArtTable 会自动计算剩余空间:
<divclass="art-full-height"><UserSearch /><ElCardclass="art-table-card"><ArtTableHeader /><ArtTable /> <!-- 自动占满剩余高度 --></ElCard></div>
🎨 列配置灵活
支持多种列类型和自定义渲染:
columnsFactory: () => [{ type: 'selection' }, // 勾选列{ type: 'index', width: 60, label: '序号' }, // 序号列{ type: 'globalIndex' }, // 全局序号(跨页)// 自定义渲染{prop: 'avatar',label: '用户',formatter: (row) => {return h('div', { class: 'user-info' }, [h(ElImage, { src: row.avatar }),h('span', row.userName)])}},// 使用插槽{prop: 'status',label: '状态',useSlot: true // 在模板中使用 #status 插槽},// 操作列{prop: 'operation',label: '操作',fixed: 'right',formatter: (row) =>h('div', [h(ArtButtonTable, {type: 'edit',onClick: () => handleEdit(row)}),h(ArtButtonTable, {type: 'delete',onClick: () => handleDelete(row)})])}]
📱 响应式分页
自动适配移动端、平板、桌面端:
// 移动端:prev, pager, next, sizes, jumper, total// 平板:prev, pager, next, jumper, total// 桌面端:total, prev, pager, next, sizes, jumper
三、ArtTableHeader:强大的工具栏
3.1 开箱即用的功能
<ArtTableHeaderv-model:columns="columnChecks":loading="loading" @refresh="refreshData"><template #left><ElButton @click="handleAdd">新增用户</ElButton></template></ArtTableHeader>
一行代码,你就拥有了:
🔍 搜索栏切换 🔄 刷新按钮(带加载动画) 📏 表格尺寸切换(大、中、小) 🖥️ 全屏模式 📋 列显示 / 隐藏配置 🎨 斑马纹、边框、表头背景切换
3.2 列配置功能
用户可以:
✅ 勾选显示 / 隐藏列 ✅ 拖拽调整列顺序 ✅ 固定列不可拖动
这些配置会自动同步到表格显示。
3.3 自定义布局
<ArtTableHeader layout="refresh,size,fullscreen,columns" :show-zebra="false" :show-border="false" />通过 layout 属性控制显示哪些功能按钮。
四、ArtSearchBar:智能搜索栏
4.1 配置化搜索表单
不用再手写一堆 ElFormItem 了,用配置就能搞定:
<template><ArtSearchBarref="searchBarRef"v-model="formData":items="formItems":rules="rules"@reset="handleReset"@search="handleSearch"/></template><scriptsetuplang="ts">const formData = ref({userName: undefined,userPhone: undefined,status: '1'})const formItems = computed(() => [{label: '用户名',key: 'userName',type: 'input',placeholder: '请输入用户名',clearable: true},{label: '手机号',key: 'userPhone',type: 'input',props: { placeholder: '请输入手机号', maxlength: '11' }},{label: '状态',key: 'status',type: 'select',props: {placeholder: '请选择状态',options: [{ label: '在线', value: '1' },{ label: '离线', value: '2' }]}},{label: '性别',key: 'userGender',type: 'radiogroup',props: {options: [{ label: '男', value: '1' },{ label: '女', value: '2' }]}}])const handleSearch = async () => {await searchBarRef.value.validate()emit('search', formData.value)}const handleReset = () => {emit('reset')}</script>
4.2 核心特性
🎯 支持多种表单组件
开箱即用的组件类型:
input- 输入框select- 下拉选择date/datetime/daterange- 日期选择radiogroup/checkboxgroup- 单选 / 多选cascader- 级联选择treeselect- 树选择自定义组件 - 支持任意 Vue 组件
📦 自动展开 / 收起
当搜索项过多时,自动显示展开 / 收起按钮:
<ArtSearchBar :items="formItems" :show-expand="true" :default-expanded="false" :span="6" />默认只显示一行 超出部分自动隐藏 点击 "展开" 查看全部搜索项
🔄 动态选项加载
支持异步加载选项数据:
const statusOptions = ref([])onMounted(async () => {// 模拟接口请求statusOptions.value = await fetchStatusOptions()})const formItems = computed(() => [{label: '状态',key: 'status',type: 'select',props: {options: statusOptions.value // 动态选项}}])
🎨 响应式布局
自动适配不同屏幕尺寸:
// 通过 span 控制每行显示的表单项数量// span=6: 一行显示 4 个(24/6=4)// span=8: 一行显示 3 个(24/8=3)// span=12: 一行显示 2 个(24/12=2)
移动端自动调整为单列布局。
4.3 表单校验
支持完整的表单校验:
const rules = {userName: [{ required: true, message: '请输入用户名', trigger: 'blur' }],userPhone: [{ required: true, message: '请输入手机号', trigger: 'blur' },{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }]}const handleSearch = async () => {// 校验通过才执行搜索await searchBarRef.value.validate()emit('search', formData.value)}
五、ArtForm:通用表单组件
5.1 配置化表单
ArtForm 和 ArtSearchBar 使用相同的配置方式,但更适合弹窗、详情页等场景:
<template><ArtFormref="formRef"v-model="formData":items="formItems":rules="formRules":label-width="100":span="12"@reset="handleReset"@submit="handleSubmit"/></template><scriptsetuplang="ts">const formData = ref({userName: '',userPhone: '',userEmail: '',userGender: '1',status: true})const formItems = [{label: '用户名',key: 'userName',type: 'input',placeholder: '请输入用户名'},{label: '手机号',key: 'userPhone',type: 'input',props: { maxlength: 11 }},{label: '邮箱',key: 'userEmail',type: 'input',placeholder: '请输入邮箱'},{label: '性别',key: 'userGender',type: 'radiogroup',props: {options: [{ label: '男', value: '1' },{ label: '女', value: '2' }]}},{label: '是否启用',key: 'status',type: 'switch'},{label: '备注',key: 'remark',type: 'input',span: 24, // 占满整行props: {type: 'textarea',rows: 4}}]const formRules = {userName: [{ required: true, message: '请输入用户名', trigger: 'blur' }],userEmail: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }]}const handleSubmit = async () => {await formRef.value.validate()// 提交表单console.log('表单数据:', formData.value)}</script>
5.2 高级特性
🎨 自定义组件渲染
支持使用 h 函数渲染任意组件:
import ArtIconSelector from '@/components/core/base/art-icon-selector/index.vue'const formItems = [{label: '图标选择',key: 'icon',type: () =>h(ArtIconSelector, {iconType: IconTypeEnum.UNICODE,width: '100%'})},{label: '文件上传',key: 'files',type: () =>h(ElUpload,{action: '#',multiple: true,limit: 5,onChange: (file, fileList) => {formData.value.files = fileList}},{default: () => h(ElButton, { type: 'primary' }, () => '点击上传')})}]
🔌 插槽支持
支持为表单项添加插槽:
<ArtFormv-model="formData":items="formItems"><template #customField><divclass="custom-content"><!-- 自定义内容 --></div></template></ArtForm>
或者在配置中使用插槽:
const formItems = [{label: '网站',key: 'website',type: 'input',slots: {prepend: () => h('span', 'https://'),append: () => h('span', '.com')}}]
🎯 条件显示
根据条件动态显示 / 隐藏表单项:
const formItems = computed(() => [{label: '用户类型',key: 'userType',type: 'select',props: {options: [{ label: '个人', value: 'personal' },{ label: '企业', value: 'enterprise' }]}},{label: '企业名称',key: 'companyName',type: 'input',// 只有选择企业类型时才显示hidden: formData.value.userType !== 'enterprise'}])
📏 灵活布局
通过 span 控制表单项宽度:
const formItems = [{label: '用户名',key: 'userName',type: 'input',span: 12 // 占半行},{label: '手机号',key: 'userPhone',type: 'input',span: 12 // 占半行},{label: '地址',key: 'address',type: 'input',span: 24 // 占整行}]
5.3 与 ArtSearchBar 的区别
六、完整示例
让我们看一个完整的用户管理页面:
<template><divclass="user-page art-full-height"><!-- 搜索栏 --><ArtSearchBarv-model="searchForm":items="searchItems"@search="handleSearch"@reset="resetSearchParams"/><ElCardclass="art-table-card"shadow="never"><!-- 工具栏 --><ArtTableHeaderv-model:columns="columnChecks":loading="loading" @refresh="refreshData"><template #left><ElButton @click="handleAdd">新增用户</ElButton></template></ArtTableHeader><!-- 表格 --><ArtTable:loading="loading":data="data":columns="columns":pagination="pagination"@selection-change="handleSelectionChange"@pagination:size-change="handleSizeChange"@pagination:current-change="handleCurrentChange"/></ElCard><!-- 弹窗 --><UserDialogv-model:visible="dialogVisible":type="dialogType":user-data="currentUserData"@submit="handleDialogSubmit"/></div></template><scriptsetuplang="ts">import { useTable } from '@/composables/useTable'import { fetchGetUserList } from '@/api/system-manage'import UserDialog from './modules/user-dialog.vue'// 搜索表单const searchForm = ref({userName: undefined,userPhone: undefined,status: '1'})// 搜索项配置const searchItems = [{label: '用户名',key: 'userName',type: 'input',placeholder: '请输入用户名',clearable: true},{label: '手机号',key: 'userPhone',type: 'input',props: { placeholder: '请输入手机号', maxlength: '11' }},{label: '状态',key: 'status',type: 'select',props: {placeholder: '请选择状态',options: [{ label: '在线', value: '1' },{ label: '离线', value: '2' },{ label: '异常', value: '3' }]}}]// 表格配置const {data,columns,columnChecks,loading,pagination,getData,searchParams,resetSearchParams,handleSizeChange,handleCurrentChange,refreshData} = useTable({core: {apiFn: fetchGetUserList,apiParams: {current: 1,size: 20,...searchForm.value},columnsFactory: () => [{ type: 'selection' },{ type: 'index', width: 60, label: '序号' },{prop: 'avatar',label: '用户名',width: 280,formatter: (row) => {return h('div', { class: 'user-info' }, [h(ElImage, { src: row.avatar }),h('div', [h('p', row.userName), h('p', { class: 'email' }, row.userEmail)])])}},{ prop: 'userGender', label: '性别' },{ prop: 'userPhone', label: '手机号' },{prop: 'status',label: '状态',formatter: (row) => {const config = getStatusConfig(row.status)return h(ElTag, { type: config.type }, () => config.text)}},{prop: 'operation',label: '操作',width: 120,fixed: 'right',formatter: (row) =>h('div', [h(ArtButtonTable, {type: 'edit',onClick: () => handleEdit(row)}),h(ArtButtonTable, {type: 'delete',onClick: () => handleDelete(row)})])}]}})// 搜索处理const handleSearch = (params) => {Object.assign(searchParams, params)getData()}// 新增const handleAdd = () => {dialogType.value = 'add'dialogVisible.value = true}// 编辑const handleEdit = (row) => {dialogType.value = 'edit'currentUserData.value = rowdialogVisible.value = true}// 删除const handleDelete = (row) => {ElMessageBox.confirm('确定要删除该用户吗?', '提示', {type: 'warning'}).then(() => {// 调用删除接口// await deleteUser(row.id)ElMessage.success('删除成功')refreshData()})}</script>
七、性能优化
7.1 智能防抖
搜索时自动防抖,避免频繁请求:
const { getDataDebounced } = useTable({performance: {debounceTime: 300 // 300ms 防抖}})// 使用防抖搜索const handleSearch = () => {getDataDebounced()}
7.2 请求取消
切换页面或快速切换搜索条件时,自动取消上一个请求:
// useTable 内部实现let abortController = new AbortController()const fetchData = async () => {// 取消上一个请求if (abortController) {abortController.abort()}// 创建新的控制器abortController = new AbortController()// 发起请求await apiFn(params)}
7.3 缓存统计
实时查看缓存状态:
const { cacheInfo } = useTable({performance: { enableCache: true }})console.log(cacheInfo.value)// { total: 10, size: '45KB', hitRate: '8 avg hits' }
八、最佳实践
8.1 目录结构
views/system/user/index.vue # 主页面modules/user-search.vue # 搜索组件user-dialog.vue # 弹窗组件
8.2 搜索组件封装
使用 ArtSearchBar 封装搜索组件:
<!-- user-search.vue --><template><ArtSearchBarref="searchBarRef"v-model="formData":items="formItems"@reset="handleReset"@search="handleSearch"/></template><scriptsetuplang="ts">const formData = defineModel({ required: true })const emit = defineEmits(['search', 'reset'])const formItems = [{label: '用户名',key: 'userName',type: 'input',placeholder: '请输入用户名',clearable: true},{label: '手机号',key: 'userPhone',type: 'input',props: { placeholder: '请输入手机号', maxlength: '11' }}]const handleSearch = () => {emit('search', formData.value)}const handleReset = () => {emit('reset')}</script>
8.3 类型安全
充分利用 TypeScript 的类型推导:
// API 类型定义declare namespace Api.SystemManage {interface UserListItem {id: numberuserName: stringuserPhone: stringuserEmail: stringstatus: stringavatar: string}interface UserSearchParams {userName?: stringuserPhone?: stringstatus?: string}}// useTable 会自动推导类型const { data } = useTable({core: {apiFn: fetchGetUserList // 返回 Promise<UserListItem[]>// data 的类型自动推导为 UserListItem[]}})
九、对比传统方案
传统方案(约 200 行代码)
// 需要手动管理的状态const loading = ref(false)const data = ref([])const total = ref(0)const currentPage = ref(1)const pageSize = ref(20)const searchForm = ref({})// 需要手动实现的方法const fetchData = async () => {loading.value = truetry {const res = await api.getUserList({current: currentPage.value,size: pageSize.value,...searchForm.value})data.value = res.recordstotal.value = res.total} catch (error) {console.error(error)} finally {loading.value = false}}const handleSizeChange = (val) => {pageSize.value = valcurrentPage.value = 1fetchData()}const handleCurrentChange = (val) => {currentPage.value = valfetchData()}const handleSearch = () => {currentPage.value = 1fetchData()}const handleReset = () => {searchForm.value = {}currentPage.value = 1fetchData()}// ... 还有更多代码
使用 useTable(约 30 行代码)
const {data,columns,loading,pagination,searchParams,getData,resetSearchParams,handleSizeChange,handleCurrentChange} = useTable({core: {apiFn: fetchGetUserList,apiParams: { current: 1, size: 20 },columnsFactory: () => [/* 列配置 */]}})const handleSearch = (params) => {Object.assign(searchParams, params)getData()}
代码量减少 85%,功能更强大!
总结
这套表格开发方案的核心优势:
「开发效率提升 10 倍」 - 从 200 行代码减少到 30 行 「功能更强大」 - 缓存、防抖、多种刷新策略、列配置等 「类型安全」 - 完整的 TypeScript 支持 「易于维护」 - 统一的代码风格和最佳实践 「用户体验好」 - 响应式设计、智能缓存、流畅交互
如果你也在开发后台管理系统,强烈建议尝试这套方案。它不仅能让你的代码更简洁,还能让你有更多时间专注于业务逻辑,而不是重复造轮子。
如果这篇文章对你有帮助,欢迎转发收藏!有任何问题也欢迎在评论区讨论 💬