P18 | Element Plus 通用 CRUD 页面模板:一个模板覆盖 80% 管理页面

P18 | Element Plus 通用 CRUD 页面模板:一个模板覆盖 80% 管理页面

💰 付费文章 | 第三阶段:Web 管理台


管理台页面的共性

几乎所有管理台页面都是这个模式:

复制代码
┌─────────────────────────────────────┐
│ 搜索区域(表单筛选)                    │
│ [关键词] [城市▼] [搜索] [重置] [新增]    │
├─────────────────────────────────────┤
│ 数据表格                              │
│ ☐ | 名称 | 城市 | 状态 | 创建时间 | 操作 │
│ ☐ | 青岛海底世界 | 青岛 | 有效 | ... | 编辑 删除 │
│ ☐ | 金沙滩 | 青岛 | 有效 | ... | 编辑 删除 │
├─────────────────────────────────────┤
│ 分页器                                │
│ ← 1 2 3 ... 10 → 共100条              │
└─────────────────────────────────────┘

点击「新增」→ 弹出对话框 → 填写表单 → 提交保存
点击「编辑」→ 弹出对话框 → 回显数据 → 提交保存

80% 的页面都可以用同一个模板!


通用 CRUD 页面组件

复制代码
<template>
  <div class="crud-page">
    <!-- 搜索区域 -->
    <el-card class="search-card" shadow="never">
      <el-form :model="searchForm" inline>
        <slot name="search">
          <el-form-item label="关键词">
            <el-input v-model="searchForm.keyword" placeholder="请输入" clearable />
          </el-form-item>
        </slot>
        <el-form-item>
          <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
          <el-button @click="handleReset">重置</el-button>
          <el-button v-permission="addPermission" type="success" :icon="Plus" @click="handleAdd">
            新增
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 数据表格 -->
    <el-card shadow="never" class="table-card">
      <el-table :data="tableData" stripe v-loading="loading" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" />
        <slot name="columns">
          <el-table-column prop="id" label="ID" width="200" />
          <el-table-column prop="name" label="名称" />
        </slot>
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="{ row }">
            <el-button v-permission="editPermission" type="primary" link @click="handleEdit(row)">
              编辑
            </el-button>
            <el-popconfirm title="确定删除?" @confirm="handleDelete(row)">
              <template #reference>
                <el-button v-permission="deletePermission" type="danger" link>删除</el-button>
              </template>
            </el-popconfirm>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页 -->
      <el-pagination
        v-model:current-page="page"
        v-model:page-size="pageSize"
        :total="total"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next, jumper"
        class="mt-4 justify-end"
        @size-change="loadData"
        @current-change="loadData"
      />
    </el-card>

    <!-- 新增/编辑对话框 -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" @close="resetForm">
      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
        <slot name="form" :formData="formData">
          <el-form-item label="名称" prop="name">
            <el-input v-model="formData.name" placeholder="请输入" />
          </el-form-item>
        </slot>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { Search, Plus } from '@element-plus/icons-vue'
import { post } from '@/api/pikachuNetwork'

const props = defineProps({
  listApi: { type: String, required: true },    // 列表接口路径
  saveApi: { type: String, required: true },    // 保存接口路径
  removeApi: { type: String, required: true },  // 删除接口路径
  addPermission: { type: String, default: '' },
  editPermission: { type: String, default: '' },
  deletePermission: { type: String, default: '' },
})

const emit = defineEmits(['afterSave', 'beforeLoad'])

const searchForm = reactive({ keyword: '' })
const tableData = ref([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)

const dialogVisible = ref(false)
const dialogTitle = ref('新增')
const formData = reactive({})
const formRules = reactive({})
const formRef = ref(null)
const submitting = ref(false)

async function loadData() {
  loading.value = true
  try {
    const params = {
      page: page.value,
      pageSize: pageSize.value,
      ...searchForm
    }
    emit('beforeLoad', params)
    const result = await post(props.listApi, params)
    tableData.value = result.records || []
    total.value = result.total || 0
  } finally {
    loading.value = false
  }
}

function handleSearch() {
  page.value = 1
  loadData()
}

function handleReset() {
  Object.keys(searchForm).forEach(key => searchForm[key] = '')
  page.value = 1
  loadData()
}

function handleAdd() {
  dialogTitle.value = '新增'
  Object.keys(formData).forEach(key => formData[key] = null)
  dialogVisible.value = true
}

function handleEdit(row) {
  dialogTitle.value = '编辑'
  Object.assign(formData, { ...row })
  dialogVisible.value = true
}

async function handleSubmit() {
  await formRef.value?.validate()
  submitting.value = true
  try {
    await post(props.saveApi, { ...formData })
    ElMessage.success('保存成功')
    dialogVisible.value = false
    loadData()
    emit('afterSave', formData)
  } finally {
    submitting.value = false
  }
}

async function handleDelete(row) {
  await post(props.removeApi, { id: row.id })
  ElMessage.success('删除成功')
  loadData()
}

onMounted(loadData)

defineExpose({ loadData, searchForm })
</script>

使用模板:景点管理页面

复制代码
<!-- views/town/AttractionList.vue -->
<template>
  <CrudPage
    listApi="/plat/attraction/page"
    saveApi="/plat/attraction/save"
    removeApi="/plat/attraction/remove"
    addPermission="attraction:add"
    editPermission="attraction:edit"
    deletePermission="attraction:delete"
    @beforeLoad="addSearchParams"
  >
    <!-- 自定义搜索字段 -->
    <template #search>
      <el-form-item label="关键词">
        <el-input v-model="searchForm.keyword" placeholder="景点名称" clearable />
      </el-form-item>
      <el-form-item label="城市">
        <el-select v-model="searchForm.cityId" placeholder="全部" clearable>
          <el-option v-for="city in cities" :key="city.id" :label="city.name" :value="city.id" />
        </el-select>
      </el-form-item>
    </template>

    <!-- 自定义表格列 -->
    <template #columns>
      <el-table-column prop="attName" label="景点名称" min-width="200" />
      <el-table-column prop="cityName" label="城市" width="120" />
      <el-table-column label="封面" width="100">
        <template #default="{ row }">
          <el-image :src="row.coverImage" :preview-src-list="[row.coverImage]" style="width:60px;height:40px" fit="cover" />
        </template>
      </el-table-column>
      <el-table-column prop="ticketInfo" label="票价" width="120" />
      <el-table-column label="状态" width="80">
        <template #default="{ row }">
          <el-tag :type="row.flag === 1 ? 'success' : 'danger'">
            {{ row.flag === 1 ? '有效' : '已删' }}
          </el-tag>
        </template>
      </el-table-column>
    </template>
  </CrudPage>
</template>

下一篇

P19 → 加密通信层 pikachuNetwork.js 完整实现

相关推荐
胡萝卜术9 小时前
从API调用到手写LRU:力扣146「LRU缓存」的哈希表+双向链表终极进化之路
前端·javascript·面试
科技道人9 小时前
记录 默认置灰/禁用 app ‘Search Engine Selector‘ 的disable按钮
开发语言·前端·javascript
碎碎念_49213 小时前
SpringBoot + Vue 前后端分离从 0 到 1 完整环境配置流程
vue.js·spring boot·后端
shuaijie051814 小时前
强制修改调用接口的api地址。
javascript·vue.js·ecmascript
JerrySir14 小时前
恶意 npm 包只活了 17 分钟:技术面试里,为什么“升级依赖”还不算止血?
javascript·安全
白帽小阳15 小时前
2026前端面试题!(附答案及解析)
javascript·网络·python·安全·web安全·网络安全·护网行动
这是个栗子15 小时前
前端开发中的常用工具函数(八)
开发语言·前端·javascript
Hilaku15 小时前
Vue 和 React 真正的差距,不在语法,而在团队犯错成本
前端·javascript·程序员
研☆香15 小时前
为什么变量声明在赋值前也能使用?—— 深入理解 JavaScript 变量提升与作用域
开发语言·前端·javascript
柯克七七16 小时前
我把 bug 修得太干净了,测试组以为这个模块本来就没问题
前端·javascript·typescript