自研国产零依赖前端UI框架实战010 表格加载中状态

131 阅读8分钟

前言

目前我们已经实现了用户管理系统的基础功能, 接下来就是想办法让它的开发变得更加的简单, 让里面的大部分逻辑能够被后面其他类似的功能进行复用.

这里我主要有两个想法, 一个是封装一个crud组件, 提供基础的增删改查的能力. 另一个是封装组合式API, 让逻辑代码能够被复用.

先封装组合式API

首先就是数据分页相关的逻辑, 按照我之前的开发经验, 这里可以抽离成一个通用的逻辑.

我这里先是创建了一个模拟获取用户后端接口的方法:

function apiGetPageUser(page = 1, size = 8, total = 888) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            try {
                resolve(getPageUser(page, size, total));
            } catch (error) {
                reject(error);
            }
        }, 1000); // 模拟 1 秒的延迟
    });
}

接着我把分页相关的代码封装到了crud.js文件中.

import {reffrom "vue";

// 用于crud的分页
const useCrudPage = (apiGetPage) => {
    // 第几页
    const page = ref(1);
    // 每页数据
    const size = ref(8);
    // 共多少条数据
    const total = ref(0);
    // 表格数据
    const data = ref([])
    // 监听分页变化
    const onChange = async (v) => {
        page.value = v
        await loadData()
    }
    // 加载数据
    const loadData = async () => {
        const newData = await apiGetPage(
            page.value,
            size.value,
        )
        data.value = newData.data
        total.value = newData.total
    }

    return {
        page,
        size,
        total,
        data,
        loadData,
        onChange,
    }
}

export default {
    useCrudPage,
}

最后再修改一下App.vue.

<script setup>
import zdp_table1 from "./zdpui/components/zdp_table1.vue";
import zdp_page1 from "./zdpui/components/zdp_page1.vue";
import random from "./zdpui/js/random.js";
import {onMounted, reactive, ref} from "vue";
import zdp_confirm1 from "./zdpui/components/zdp_confirm1.vue";
import zdp_modal1 from "./zdpui/components/zdp_modal1.vue";
import Zdp_input1 from "./zdpui/components/zdp_input1.vue";
import Zdp_button1 from "./zdpui/components/zdp_button1.vue";
import array from "./zdpui/js/array.js";
import crud from "./zdpui/js/crud.js";

const columns = [
  {
    title: "员工编号",
    key: "id",
    width: 80,
    align: "center"
  },
  {
    title: "姓名",
    key: "name",
    width: 100,
    align: "center"
  },
  {
    title: "年龄",
    key: "age",
    width: 100,
    align: "center"
  }
]
const {
  page,
  size,
  total,
  data,
  onChange,
  loadData,
} = crud.useCrudPage(random.apiGetPageUser)

const isEdit = ref(false);
const editId = ref(0);
const editIndex = ref(0);

// 点击编辑, 显示编辑对话框
const onShowEditDialog = (index, item) => {
  console.log("编辑", index, item)
  isEdit.value = true;
  isShowUserDialog.value = true;
  editId.value = item.id
  editIndex.value = index
  formDataUser.name = item.name
  formDataUser.age = item.age
  modalTitle.value = "编辑用户"
}

const onDelete = (index, item) => {
  console.log("删除", index, item)
  showDeleteDialog.value = true;
}
const showDeleteDialog = ref(false);
const onConfirmDelete = () => {
  console.log("确认删除");
  showDeleteDialog.value = false;
};
const onCloseDeleteDialog = () => {
  console.log("取消删除");
  showDeleteDialog.value = false;
};

const isShowUserDialog = ref(false);
const formDataUser = reactive({name: "张三", age: 23});
const modalTitle = ref("编辑用户");

// 点击对话框中的保存按钮
const onSave = () => {
  console.log("保存用户", formDataUser);
  isShowUserDialog.value = false;

  if (isEdit.value) {
    data.value[editIndex.value] = {
      id: editId.value,
      ...formDataUser,
    };
  } else {
    let newUser = {
      id: random.id1(),
      name: formDataUser.name,
      age: formDataUser.age,
    }
    array.insertFirst(data.value, newUser)
  }

  isEdit.value = false;
  formDataUser.name = ""
  formDataUser.age = 0
};

const handleCloseDialog = () => {
  isShowUserDialog.value = false;
};
const onShowAddUserModal = () => {
  isShowUserDialog.value = true;
  formDataUser.id = 0
  formDataUser.name = ""
  formDataUser.age = 0
  modalTitle.value = "新增用户"
};

onMounted(async ()=>{
  await loadData()
  console.log("data...", data.value)
})
</script>
<template>
  <div>
    <zdp_button1
        @click="onShowAddUserModal"
        text="新增"
    />
    <zdp_table1
        :columns="columns"
        :data="data"
        @edit="onShowEditDialog"
        @delete="onDelete"
    />
    <zdp_page1
        :page="page"
        :size="size"
        :total="total"
        @change="onChange"
    />
    <zdp_confirm1
        :show="showDeleteDialog"
        @confirm="onConfirmDelete"
        @close="onCloseDeleteDialog"
    />
    <zdp_modal1
        :show="isShowUserDialog"
        :title="modalTitle"
        @confirm="onSave"
        @close="handleCloseDialog"
    >
      <zdp_input1
          label="姓名"
          v-model="formDataUser.name"
          placeholder="请输入姓名"
      />
      <zdp_input1
          label="年龄"
          type="number"
          v-model="formDataUser.age"
          placeholder="请输入年龄"
      />
    </zdp_modal1>
  </div>
</template>

页面一切正常, 很完美, 这样我们就得到了第一个需要的组合式API.

在这里插入图片描述

在这里插入图片描述

表格加载状态

由于模拟接口有一秒钟的延迟, 在这一秒钟以内, 表格的内容是空的, 不太好.

所以我决定先封装一个通用的全局的加载中状态的组件, 然后把这个组件复用到表格组件中.

加载中组件的效果如:

在这里插入图片描述

在这里插入图片描述

组件代码如下:

<script setup>
const props = defineProps({
  loading: {
    typeBoolean,
    defaultfalse
  },
  text: {
    typeString,
    default'加载中...'
  }
})
</script>

<template>
  <div v-if="props.loading" class="loading-container">
    <div class="loading-spinner"></div>
    <div class="loading-text">{{ props.text }}</div>
  </div>
</template>

<style scoped>
.loading-container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  position: fixed;
  top0;
  left0;
  right0;
  bottom0;
  background-colorrgba(2402402400.8); /* 透明的灰白色背景 */
  z-index9999/* 确保在其他元素之上 */
}

.loading-spinner {
  border16px solid #f3f3f3/* 浅灰色 */
  border-top16px solid #3498db/* 蓝色 */
  border-radius50%;
  width88px;
  height88px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transformrotate(0deg);
  }
  100% {
    transformrotate(360deg);
  }
}

.loading-text {
  margin-top20px/* 加载文本在加载图标下面 */
  color#333/* 文本颜色 */
  font-size1.5em;
  font-family'Arial', sans-serif;
}
</style>

改造加载中组件

我接着对这个加载中的组件进行了一下改造, 现在, 它即只是普通的加载, 也支持全屏的加载, 通过type来控制.

默认是普通加载, type=full的时候是全屏加载.

完整代码如下:

<script setup>
const props = defineProps({
  loading: {
    typeBoolean,
    defaultfalse
  },
  text: {
    typeString,
    default'加载中...'
  },
  type: {
    typeString,
    // full, normal
    default'normal'
  }
})
</script>

<template>
  <div
      v-if="props.loading"
      :class="type"
  >
    <div class="spinner"></div>
    <div class="text">{{ props.text }}</div>
  </div>
</template>

<style scoped>
/*全屏加载*/
.full {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  position: fixed;
  top0;
  left0;
  right0;
  bottom0;
  background-colorrgba(2402402400.8); /* 透明的灰白色背景 */
  z-index9999/* 确保在其他元素之上 */
}

/*普通加载*/
.normal {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-colorrgba(2402402400.8); /* 透明的灰白色背景 */
  padding88px 0;
}

.spinner {
  border16px solid #f3f3f3/* 浅灰色 */
  border-top16px solid #3498db/* 蓝色 */
  border-radius50%;
  width88px;
  height88px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% {
    transformrotate(0deg);
  }
  100% {
    transformrotate(360deg);
  }
}

.text {
  margin-top20px/* 加载文本在加载图标下面 */
  color#333/* 文本颜色 */
  font-size1.5em;
  font-family'Arial', sans-serif;
}
</style>

普通加载状态如下:

在这里插入图片描述

在这里插入图片描述

让表格支持加载中状态

效果如下:

在这里插入图片描述

在这里插入图片描述

此时App.vue完整代码如下:

<script setup>
import zdp_table1 from "./zdpui/components/zdp_table1.vue";
import zdp_page1 from "./zdpui/components/zdp_page1.vue";
import random from "./zdpui/js/random.js";
import {onMounted, reactive, ref} from "vue";
import zdp_confirm1 from "./zdpui/components/zdp_confirm1.vue";
import zdp_modal1 from "./zdpui/components/zdp_modal1.vue";
import Zdp_input1 from "./zdpui/components/zdp_input1.vue";
import Zdp_button1 from "./zdpui/components/zdp_button1.vue";
import array from "./zdpui/js/array.js";
import crud from "./zdpui/js/crud.js";
import zdp_loading1 from "./zdpui/components/zdp_loading1.vue";

const columns = [
  {
    title: "员工编号",
    key: "id",
    width: 80,
    align: "center"
  },
  {
    title: "姓名",
    key: "name",
    width: 100,
    align: "center"
  },
  {
    title: "年龄",
    key: "age",
    width: 100,
    align: "center"
  }
]
const {
  page,
  size,
  total,
  data,
  loading,
  onChange,
  loadData,
} = crud.useCrudPage(random.apiGetPageUser)

const isEdit = ref(false);
const editId = ref(0);
const editIndex = ref(0);

// 点击编辑, 显示编辑对话框
const onShowEditDialog = (index, item) => {
  console.log("编辑", index, item)
  isEdit.value = true;
  isShowUserDialog.value = true;
  editId.value = item.id
  editIndex.value = index
  formDataUser.name = item.name
  formDataUser.age = item.age
  modalTitle.value = "编辑用户"
}

const onDelete = (index, item) => {
  console.log("删除", index, item)
  showDeleteDialog.value = true;
}
const showDeleteDialog = ref(false);
const onConfirmDelete = () => {
  console.log("确认删除");
  showDeleteDialog.value = false;
};
const onCloseDeleteDialog = () => {
  console.log("取消删除");
  showDeleteDialog.value = false;
};

const isShowUserDialog = ref(false);
const formDataUser = reactive({name: "张三", age: 23});
const modalTitle = ref("编辑用户");

// 点击对话框中的保存按钮
const onSave = () => {
  console.log("保存用户", formDataUser);
  isShowUserDialog.value = false;

  if (isEdit.value) {
    data.value[editIndex.value] = {
      id: editId.value,
      ...formDataUser,
    };
  } else {
    let newUser = {
      id: random.id1(),
      name: formDataUser.name,
      age: formDataUser.age,
    }
    array.insertFirst(data.value, newUser)
  }

  isEdit.value = false;
  formDataUser.name = ""
  formDataUser.age = 0
};

const handleCloseDialog = () => {
  isShowUserDialog.value = false;
};
const onShowAddUserModal = () => {
  isShowUserDialog.value = true;
  formDataUser.id = 0
  formDataUser.name = ""
  formDataUser.age = 0
  modalTitle.value = "新增用户"
};

onMounted(async ()=>{
  await loadData()
  console.log("data...", data.value)
})
</script>
<template>
  <div>
    <zdp_button1
        @click="onShowAddUserModal"
        text="新增"
    />
    <zdp_table1
        :columns="columns"
        :data="data"
        :loading="loading"
        @edit="onShowEditDialog"
        @delete="onDelete"
    />
    <zdp_page1
        :page="page"
        :size="size"
        :total="total"
        @change="onChange"
    />
    <zdp_confirm1
        :show="showDeleteDialog"
        @confirm="onConfirmDelete"
        @close="onCloseDeleteDialog"
    />
    <zdp_modal1
        :show="isShowUserDialog"
        :title="modalTitle"
        @confirm="onSave"
        @close="handleCloseDialog"
    >
      <zdp_input1
          label="姓名"
          v-model="formDataUser.name"
          placeholder="请输入姓名"
      />
      <zdp_input1
          label="年龄"
          type="number"
          v-model="formDataUser.age"
          placeholder="请输入年龄"
      />
    </zdp_modal1>
  </div>
</template>

这样的话, 我们的表格就支持加载中状态这个功能了, 是不是很强大, 比antd vue还要简单得多, 这个因为笔记里面是跟着一步一步走过来的, 可能稍微复杂点, 我后面出个使用教程, 那个时候就能够体会到这个组件的简单和强大了.

接下来做什么

表格优化完了, 但是我们真正的目标才走了一小步, 也就是封装组合式API, 封装crud组件.

接下来, 我们先继续封装组合式API, 等组合式API封装完毕以后, 我们再封装crud组件.

封装表格相关的操作

表格里面现在的操作也可以封装到一个组合式API中, 主要是表格的数据, 已经删除数据的接口.

当我点击删除的时候, 弹出删除窗口:

在这里插入图片描述

在这里插入图片描述

点击确认, 这个数据从表格中移除.

在这里插入图片描述

在这里插入图片描述

在表格相关的组合式API中, 主要封装了表格的columns相关的操作, 以及删除相关的操作.

// 用于表格
const useCrudTable = (data, _columns = null, apiDelete = null) => {
    // 表格数据
    const columns = ref(_columns ?? [
        {
            title: "编号",
            key: "id",
            width: 80,
            align: "center"
        },
        {
            title: "姓名",
            key: "name",
            width: 100,
            align: "center"
        },
        {
            title: "年龄",
            key: "age",
            width: 100,
            align: "center"
        }
    ])
    // 删除索引
    const deleteIndex = ref()
    // 删除数据
    const deleteData = ref()
    // 删除对话框
    const isShowDeleteDialog = ref(false);
    // 执行删除
    const onShowDeleteDialog = async (index, item) => {
        isShowDeleteDialog.value = true;
        deleteIndex.value = index
        deleteData.value = item
    }
    // 确认删除
    const onDelete = async () => {
        isShowDeleteDialog.value = false;
        array.remove(data.value, deleteIndex.value)
        if (apiDelete && deleteData.value.id) await apiDelete(deleteData.value.id)
    };
    const onHideDeleteDialog = () => {
        isShowDeleteDialog.value = false;
    };
    return {
        columns,
        deleteIndex,
        deleteData,
        isShowDeleteDialog,
        onShowDeleteDialog,
        onDelete,
        onHideDeleteDialog,
    }
}

此时App.vue完整代码如下.

<script setup>
import zdp_table1 from "./zdpui/components/zdp_table1.vue";
import zdp_page1 from "./zdpui/components/zdp_page1.vue";
import random from "./zdpui/js/random.js";
import {onMounted, reactive, ref} from "vue";
import zdp_confirm1 from "./zdpui/components/zdp_confirm1.vue";
import zdp_modal1 from "./zdpui/components/zdp_modal1.vue";
import Zdp_input1 from "./zdpui/components/zdp_input1.vue";
import Zdp_button1 from "./zdpui/components/zdp_button1.vue";
import array from "./zdpui/js/array.js";
import crud from "./zdpui/js/crud.js";

const {
  page,
  size,
  total,
  data,
  loading,
  onChange,
  loadData,
} = crud.useCrudPage(random.apiGetPageUser)
const {
  columns,
  isShowDeleteDialog,
  onDelete,
  onShowDeleteDialog,
  onHideDeleteDialog,
} = crud.useCrudTable(data)

const isEdit = ref(false);
const editId = ref(0);
const editIndex = ref(0);

// 点击编辑, 显示编辑对话框
const onShowEditDialog = (index, item) => {
  console.log("编辑", index, item)
  isEdit.value = true;
  isShowUserDialog.value = true;
  editId.value = item.id
  editIndex.value = index
  formDataUser.name = item.name
  formDataUser.age = item.age
  modalTitle.value = "编辑用户"
}

const isShowUserDialog = ref(false);
const formDataUser = reactive({name: "张三", age: 23});
const modalTitle = ref("编辑用户");

// 点击对话框中的保存按钮
const onSave = () => {
  console.log("保存用户", formDataUser);
  isShowUserDialog.value = false;

  if (isEdit.value) {
    data.value[editIndex.value] = {
      id: editId.value,
      ...formDataUser,
    };
  } else {
    let newUser = {
      id: random.id1(),
      name: formDataUser.name,
      age: formDataUser.age,
    }
    array.insertFirst(data.value, newUser)
  }

  isEdit.value = false;
  formDataUser.name = ""
  formDataUser.age = 0
};

const handleCloseDialog = () => {
  isShowUserDialog.value = false;
};
const onShowAddUserModal = () => {
  isShowUserDialog.value = true;
  formDataUser.id = 0
  formDataUser.name = ""
  formDataUser.age = 0
  modalTitle.value = "新增用户"
};

onMounted(async ()=>{
  await loadData()
  console.log("data...", data.value)
})
</script>
<template>
  <div>
    <zdp_button1
        @click="onShowAddUserModal"
        text="新增"
    />
    <zdp_table1
        :columns="columns"
        :data="data"
        :loading="loading"
        @edit="onShowEditDialog"
        @delete="onShowDeleteDialog"
    />
    <zdp_page1
        :page="page"
        :size="size"
        :total="total"
        @change="onChange"
    />
    <zdp_confirm1
        :show="isShowDeleteDialog"
        @confirm="onDelete"
        @close="onHideDeleteDialog"
    />
    <zdp_modal1
        :show="isShowUserDialog"
        :title="modalTitle"
        @confirm="onSave"
        @close="handleCloseDialog"
    >
      <zdp_input1
          label="姓名"
          v-model="formDataUser.name"
          placeholder="请输入姓名"
      />
      <zdp_input1
          label="年龄"
          type="number"
          v-model="formDataUser.age"
          placeholder="请输入年龄"
      />
    </zdp_modal1>
  </div>
</template>

总结

到目前为止, 我们以及实现了数据的新增, 编辑和删除功能, 还对表格的分页功能和删除功能做了组合式API的封装.

另外我们还新增了加载中状态组件, 实现当表格还在加载数据的时候, 显示加载中的动画, 这样更加的人性化.

不过, 我们还可以继续封装表单相关的东西, 所以我们还有很多的事儿要做.

那么, 我们继续往下学习吧!!!