稀土掘金技术社区

一个前端工程师的年度作品:从零开发媲美商业级应用的后台管理系统!

Image

过去一年,我花了无数个夜晚,在一次次打磨与推翻中,完成了自己最满意的作品 —— 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

预览

主页仪表盘
Image
电子商务仪表盘
Image
卡片
Image
Image
横幅
Image
图表
Image
系统图标库
Image
富文本编辑器
Image
礼花效果
Image
全局搜索
Image
系统设置
Image
表格
Image

暗黑模式

Image
Image
Image

Image
Image

快速访问

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, // 防抖 300ms    maxCacheSize: 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>  <ArtSearchBar    ref="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>  <ArtForm    ref="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 的区别

特性
ArtSearchBar
ArtForm
使用场景
表格搜索
表单提交、弹窗
展开 / 收起
✅ 支持
❌ 不支持
按钮文案
搜索 / 重置
提交 / 重置
样式
卡片样式
无背景
默认布局
横向排列
横向排列

六、完整示例

让我们看一个完整的用户管理页面:

<template>  <divclass="user-page art-full-height">    <!-- 搜索栏 -->    <ArtSearchBar      v-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>
    <!-- 弹窗 -->    <UserDialog      v-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 = row    dialogVisible.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>  <ArtSearchBar    ref="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: number    userName: string    userPhone: string    userEmail: string    status: string    avatar: string  }
  interface UserSearchParams {    userName?: string    userPhone?: string    status?: 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 = true  try {    const res = await api.getUserList({      current: currentPage.value,      size: pageSize.value,      ...searchForm.value    })    data.value = res.records    total.value = res.total  } catch (error) {    console.error(error)  } finally {    loading.value = false  }}
const handleSizeChange = (val) => {  pageSize.value = val  currentPage.value = 1  fetchData()}
const handleCurrentChange = (val) => {  currentPage.value = val  fetchData()}
const handleSearch = () => {  currentPage.value = 1  fetchData()}
const handleReset = () => {  searchForm.value = {}  currentPage.value = 1  fetchData()}
// ... 还有更多代码

使用 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%,功能更强大!

总结

这套表格开发方案的核心优势:

  1. 「开发效率提升 10 倍」 - 从 200 行代码减少到 30 行
  2. 「功能更强大」 - 缓存、防抖、多种刷新策略、列配置等
  3. 「类型安全」 - 完整的 TypeScript 支持
  4. 「易于维护」 - 统一的代码风格和最佳实践
  5. 「用户体验好」 - 响应式设计、智能缓存、流畅交互

如果你也在开发后台管理系统,强烈建议尝试这套方案。它不仅能让你的代码更简洁,还能让你有更多时间专注于业务逻辑,而不是重复造轮子。

如果这篇文章对你有帮助,欢迎转发收藏!有任何问题也欢迎在评论区讨论 💬