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 完整实现

相关推荐
爱滑雪的码农4 小时前
详细说说React大型项目结构以及日常开发核心语法
前端·javascript·react.js
@大迁世界5 小时前
43.HTML 事件处理和 React 事件处理有什么区别?
前端·javascript·react.js·html·ecmascript
ZC跨境爬虫5 小时前
跟着 MDN 学 HTML day_38:(DocumentFragment 文档片段接口详解)
前端·javascript·ui·html·音视频
@大迁世界6 小时前
41.ShadCN 是什么?它如何和 Tailwind CSS 集成,从而更容易构建可访问且可自定义的 React 组件?
前端·javascript·css·react.js·前端框架
xiangxiongfly9158 小时前
Vue3 根据角色权限动态加载路由
前端·javascript·vue.js·动态加载路由
Aolith9 小时前
我是怎么把个人论坛首页性能从80分优化到100分的(附踩坑全记录)
vue.js·性能优化
费曼学习法10 小时前
React 18 并发模式(Concurrent Mode):Fiber 架构的终极进化
javascript·react.js
_风满楼10 小时前
TDD 进阶:换个角度看会议室预约
前端·javascript·github
Amy_yang10 小时前
uni-app 安卓端纯前端预览 DOCX 的实现思路
前端·vue.js
子兮曰10 小时前
SuperSplat 深度解析:7.6K Stars 的浏览器端 3D 高斯泼溅编辑器 — 在 Web 上编辑现实
前端·javascript·webgl