自研国产零依赖前端UI框架实战013 计划看板项目实战

100 阅读10分钟

前言

通过前面的准备, 我们已经有了基本的zdpui的框架, 接下来, 我们就使用实战的方式, 一边做一些实战项目, 一边积累的完善组件.

比如接下来, 我们就来做一个看板项目,这也是我一直想做的一个项目.

基本卡片

首先, 封装一个基本的卡片.

<template>
  <div class="card">
    <h3 class="title">学习Vue3</h3>
    <p class="content">学习vue3,学习使用vue3开发一个组件库,学习使用vue3开发一个项目</p>
    <p class="date">开始时间: 2025-01-01 12:33:33</p>
    <p class="status">
      状态:
      <span>未开始</span>
    </p>
  </div>
</template>

卡片列表

初步封装了一个状态卡片组件:

<script setup>
import {computed} from "vue";

const props = defineProps({
  title: {
    typeString,
    default'这是一个卡片标题',
  },
  content: {
    typeString,
    default'这是一个卡片内容',
  },
  date: {
    typeString,
    default'2025-01-01 12:33:33',
  },
  status: {
    typeString,
    // 状态: init start finish delete
    default'deleted',
  }
})

const statusText = computed(() => {
  switch (props.status) {
    case "init":
      return "未开始"
    case "start":
      return "进行中"
    case "finish":
      return "已完成"
    case "delete":
      return "已删除"
  }
})
</script>

<template>
  <div class="card">
    <h3 class="title">{{ props.title }}</h3>
    <p class="content">{{ props.content }}</p>
    <p class="date">开始时间: {{ props.date }}</p>
    <p class="status">
      <span :class="props.status">{{ statusText }}</span>
    </p>
  </div>
</template>


<style scoped>
/*卡片的样式*/
.card {
  backgroundlinear-gradient(45degrgba(01232550.7), rgba(02122550.7)); /* 45度角的渐变色,从稍深的科技蓝到浅一点的蓝色,添加透明度以实现半透明效果 */
  border-radius15px/* 圆角边框 */
  padding20px;
  margin20px;
  width300px;
  box-shadow0 4px 8px rgba(0000.2); /* 初始阴影效果 */
  transition: all 0.3s ease-in-out; /* 过渡效果,使动画更平滑 */
  position: relative;
  overflow: hidden; /* 隐藏溢出部分,用于后续的伪元素动画 */
  cursor: pointer;
}

.card:hover {
  transformtranslateY(-10px); /* 悬停时向上移动 */
  box-shadow0 8px 16px rgba(0000.3); /* 悬停时阴影加重 */
}

.card::before {
  content'';
  position: absolute;
  top0;
  left0;
  width100%;
  height5px;
  backgroundlinear-gradient(to right, rgba(02552550.8), rgba(01232550.8)); /* 顶部的渐变线条,增加科技感 */
  transformscaleX(0); /* 初始状态为 0,用于动画效果 */
  transform-origin: left;
  transition: transform 0.3s ease-in-out;
}

.card:hover::before {
  transformscaleX(1); /* 悬停时展开顶部的渐变线条 */
}

.card::after {
  content'';
  position: absolute;
  bottom0;
  left0;
  width100%;
  height5px;
  backgroundlinear-gradient(to left, rgba(02552550.8), rgba(01232550.8)); /* 底部的渐变线条,与顶部对应 */
  transformscaleX(0);
  transform-origin: right;
  transition: transform 0.3s ease-in-out;
}

.card:hover::after {
  transformscaleX(1); /* 悬停时展开底部的渐变线条 */
}

/*标题的样式*/
.title {
  font-size24px;
  color: white;
  margin-bottom10px;
}

/*内容的样式*/
.content {
  font-size16px;
  color#fff/* 将颜色修改为白色,提高对比度 */
  margin-bottom10px;
  line-height1.5;
  animation: fadeIn 1s ease-in-out; /* 淡入动画 */
  text-shadow1px 1px 2px rgba(0000.3); /* 添加文字阴影,增强可读性 */
}

.content:hover {
  transformscale(1.05); /* 悬停时放大 */
  transition: transform 0.3s ease-in-out;
  color#f2f2f2/* 悬停时的颜色稍作调整,使其有变化 */
  text-shadow2px 2px 3px rgba(0000.4); /* 悬停时的文字阴影加深 */
}

/*日期的样式*/
.date {
  font-size14px;
  color#d9d9d9;
  margin-bottom10px;
  font-style: italic;
  animation: slideInLeft 1s ease-in-out; /* 从左滑入动画 */
}

@keyframes fadeIn {
  from {
    opacity0;
  }
  to {
    opacity1;
  }
}

@keyframes slideInLeft {
  from {
    transformtranslateX(-50px);
    opacity0;
  }
  to {
    transformtranslateX(0);
    opacity1;
  }
}

/*状态的样式*/
.status span {
  display: inline-block;
  padding5px 10px;
  border-radius5px;
  color: white;
  font-size14px;
}

/*未开始状态*/
.init {
  display: inline-block;
  padding5px 10px;
  background-color#FFC107/* 未开始状态的颜色,可根据喜好调整 */
  border-radius5px;
  color: white;
  font-size14px;
  animation: pulseNotStarted 1s infinite; /* 未开始状态的脉冲动画 */
}

/*进行中*/
.start {
  display: inline-block;
  padding5px 10px;
  background-color#28A745/* 进行中状态的颜色,可根据喜好调整 */
  border-radius5px;
  color: white;
  font-size14px;
  animation: blinkInProgress 1s infinite alternate; /* 进行中状态的闪烁动画 */
}

/*已完成*/
.finish {
  display: inline-block;
  padding5px 10px;
  background-color#17A2B8/* 已完成状态的颜色,可根据喜好调整 */
  border-radius5px;
  color: white;
  font-size14px;
  animation: bounceCompleted 1s ease-in-out; /* 已完成状态的弹动动画 */
}

/*删除了*/
.delete {
  display: inline-block;
  padding5px 10px;
  background-color#6C757D/* 已删除状态的颜色,可根据喜好调整 */
  border-radius5px;
  color: white;
  font-size14px;
  animation: fadeOutDeleted 1s ease-in-out; /* 已删除状态的淡出动画 */
}


/* 未开始状态的脉冲动画关键帧 */
@keyframes pulseNotStarted {
  0% {
    transformscale(1);
  }
  50% {
    transformscale(1.1);
  }
  100% {
    transformscale(1);
  }
}

/* 进行中状态的闪烁动画关键帧 */
@keyframes blinkInProgress {
  0% {
    opacity1;
  }
  50% {
    opacity0.5;
  }
  100% {
    opacity1;
  }
}

/* 已完成状态的弹动动画关键帧 */
@keyframes bounceCompleted {
  0% {
    transformtranslateY(0);
  }
  25% {
    transformtranslateY(-5px);
  }
  50% {
    transformtranslateY(0);
  }
  75% {
    transformtranslateY(-3px);
  }
  100% {
    transformtranslateY(0);
  }
}

/* 已删除状态的淡出动画关键帧 */
@keyframes fadeOutDeleted {
  0% {
    opacity1;
  }
  100% {
    opacity0;
  }
}
</style>

然后封装了一个卡片列表组件.

<script setup>
import ZdpStatusCard1 from "@/ZdpStatusCard1.vue";

const props = defineProps({
  title: {
    typeString,
    default"计划看板"
  },
  // 任务列表 必须有 name, content, status, start_time 这几个属性
  tasks: {
    typeArray,
    requiredtrue,
  }
})
</script>

<template>
  <div class="plan">
    <h1 class="title">{{ props.title }}</h1>
    <div class="cards">
      <div v-for="(v,k) in tasks" :key="k">
        <ZdpStatusCard1
            :title="v.name"
            :status="v.status"
            :content="v.content"
            :date="v.start_time"
        />
      </div>
    </div>
  </div>
</template>

<style scoped>
.plan {
  backgroundlinear-gradient(-45deg#007BFF#00BFFF#1E90FF#4169E1);
  background-size400% 400%;
  animation: gradientAnimation 15s ease infinite;
  min-height100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding20px;
  color: white;
}

@keyframes gradientAnimation {
  0% {
    background-position0 50%;
  }
  50% {
    background-position100% 50%;
  }
  100% {
    background-position0 50%;
  }
}

.title {
  margin-bottom30px;
  font-size2em;
  text-shadow2px 2px 4px rgba(0000.5);
  animation: fadeIn 1s ease-in-out;
}

.cards {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  width80%;
}

/* 如果你想为 ZdpStatusCard1 组件添加一些通用的样式,可以添加以下内容,假设 ZdpStatusCard1 组件的最外层元素是一个 div */
.cards div {
  margin20px;
  transition: transform 0.3s ease-in-out;
}

.cards div:hover {
  transformtranslateY(-10px);
}
</style>

最后在App.vue里面使用.

<script setup>
import ZdpStatusCard1 from "@/ZdpStatusCard1.vue";
import ZdpStatusCardList1 from "@/ZdpStatusCardList1.vue";

const tasks = []
for (let i = 1; i < 60; i += 4) {
  tasks.push({
    id: i,
    name'学习Vue3',
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'init',
    start_time'2025-05-01 12:00:00',
  })
  tasks.push({
    id: i + 1,
    name'学习Vue3',
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'start',
    start_time'2025-05-01 12:00:00',
  })
  tasks.push({
    id: i + 2,
    name'学习Vue3',
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'finish',
    start_time'2025-05-01 12:00:00',
  })
  tasks.push({
    id: i + 3,
    name'学习Vue3',
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'delete',
    start_time'2025-05-01 12:00:00',
  })
}
</script>

<template>
  <ZdpStatusCardList1
      :tasks="tasks"
  />
</template>

此时的页面效果如下.

在这里插入图片描述

在这里插入图片描述

修改状态

当我们点击卡片的时候, 可以弹出一个对话框, 让用户修改状态.

在这里插入图片描述

在这里插入图片描述

点击取消就是不修改, 点击确定则执行修改.

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

<script setup>
import ZdpStatusCardList1 from "@/ZdpStatusCardList1.vue";
import ZdpModal1 from "@/zdpui/components/ZdpModal1.vue";
import {reactive, ref} from "vue";
import ZdpSelect1 from "@/zdpui/components/ZdpSelect1.vue";

const tasks = []
for (let i = 1; i < 60; i += 4) {
  tasks.push({
    id: i,
    name'学习Vue3',
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'init',
    start_time'2025-05-01 12:00:00',
  })
  tasks.push({
    id: i + 1,
    name'学习Vue3',
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'start',
    start_time'2025-05-01 12:00:00',
  })
  tasks.push({
    id: i + 2,
    name'学习Vue3',
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'finish',
    start_time'2025-05-01 12:00:00',
  })
  tasks.push({
    id: i + 3,
    name'学习Vue3',
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'delete',
    start_time'2025-05-01 12:00:00',
  })
}
const onTaskClick = (task) => {
  console.log(task)
  modalShow.value = true
}
const formData reactive({
  status"init",
});
const modalShow ref(false)
const options = [
  {label: "未开始", value: "init"},
  {label: "进行中", value: "start"},
  {label: "已完成", value: "finish"},
  {label: "已删除", value: "delete"},
  {label: "直接移除", value: "rel_delete"},
]
const onChangeTaskStatus = ()=>{
  console.log("修改任务状态", formData.status)
  modalShow.value = false
}
</script>

<template>
  <ZdpStatusCardList1
      :tasks="tasks"
      @click="onTaskClick"
  />
  <ZdpModal1
      :show="modalShow"
      @confirm="onChangeTaskStatus"
      @close="modalShow = false"
  >
    <ZdpSelect1
        label="状态"
        v-model="formData.status"
        :options="options"
    />
  </ZdpModal1>
</template>

但是此时我们并没有真正的修改, 我们应该查找到要修改的任务, 修改其状态.

const onChangeTaskStatus = ()=>{
  console.log("修改任务状态", formData.status)
  modalShow.value = false
  if(formData.status === "rel_delete"){
    array.remove(tasks.value, editIndex.value)
    return
  }
  tasks.value[editIndex.value].status = formData.status
  formData.status = "init"
}

封装组合式API

这个也可以封装成组合式API.

import {reactive, ref} from "vue";
import array from "@/zdpui/js/array.js";

const useTaskStatus = (tasks, apiUpdate = null, apiDelete = null) => {
    // 编辑任务索引
    const editIndex = ref(null)
    // 编辑任务
    const editTask = ref(null)
    // 表单数据
    const formData = reactive({
        status: "init", // init start finish delete rel_delete
    });
    // 点击任务
    const onTaskClick = (index, task) => {
        modalShow.value = true
        editIndex.value = index
        editTask.value = task
        formData.status = task.status
    }
    // 弹窗显示
    const modalShow = ref(false)
    // 下拉框选项列表
    const options = [
        {label: "未开始", value: "init"},
        {label: "进行中", value: "start"},
        {label: "已完成", value: "finish"},
        {label: "已删除", value: "delete"},
        {label: "直接移除", value: "rel_delete"},
    ]
    // 点击确认修改任务状态
    const onChangeTaskStatus = async () => {
        modalShow.value = false
        if (formData.status === "rel_delete") {
            array.remove(tasks.value, editIndex.value)
            if (apiDelete) await apiDelete(tasks.value[editIndex.value])
        } else {
            tasks.value[editIndex.value].status = formData.status
            if (apiUpdate) await apiUpdate(tasks.value[editIndex.value])
        }
        formData.status = "init"
    }

    return {
        editIndex,
        editTask,
        formData,
        onTaskClick,
        modalShow,
        options,
        onChangeTaskStatus,
    }
}

export default {
    useTaskStatus,
}

此时App.vue代码已经很少了.

<script setup>
import ZdpStatusCardList1 from "@/zdpui/components/ZdpStatusCardList1.vue";
import ZdpModal1 from "@/zdpui/components/ZdpModal1.vue";
import {reactive, ref} from "vue";
import ZdpSelect1 from "@/zdpui/components/ZdpSelect1.vue";
import status from "@/zdpui/compsable/status.js";

const tasks = ref([])
for (let i = 1; i < 60; i += 4) {
  tasks.value.push({
    id: i,
    name`学习Vue3${i}`,
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'init',
    start_time'2025-05-01 12:00:00',
  })
  tasks.value.push({
    id: i + 1,
    name`学习Vue3${i + 1}`,
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'start',
    start_time'2025-05-01 12:00:00',
  })
  tasks.value.push({
    id: i + 2,
    name`学习Vue3${i + 2}`,
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'finish',
    start_time'2025-05-01 12:00:00',
  })
  tasks.value.push({
    id: i + 3,
    name`学习Vue3${i + 3}`,
    content"学习Vue3基础语法, 包括组件、路由、状态管理等",
    status'delete',
    start_time'2025-05-01 12:00:00',
  })
}

const apiUpdate = null
const apiDelete = null
const {
  formData,
  onTaskClick,
  modalShow,
  options,
  onChangeTaskStatus,
} =status.useTaskStatus(tasks, apiUpdate, apiDelete)
</script>

<template>
  <ZdpStatusCardList1
      :tasks="tasks"
      @click="onTaskClick"
  />
  <ZdpModal1
      :show="modalShow"
      @confirm="onChangeTaskStatus"
      @close="modalShow = false"
  >
    <ZdpSelect1
        label="状态"
        v-model="formData.status"
        :options="options"
    />
  </ZdpModal1>
</template>

不过我感觉还有优化空间, 就是这个任务列表. 任务列表的数据理论上应该是从后端动态生成的, 如果没有就用前端的模拟数据. 加载过程中应该也要有加载中的效果.

模拟任务列表接口

// 模拟获取所有的任务
const mockStatusGetAll = ()=>{
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            try {
                let tasks = []
                for (let i = 1; i < 30; i += 4) {
                    tasks.push({
                        id: i,
                        name: `学习Vue3${i}`,
                        content"学习Vue3基础语法, 包括组件、路由、状态管理等",
                        status'init',
                        start_time'2025-05-01 12:00:00',
                    })
                    tasks.push({
                        id: i + 1,
                        name: `学习Vue3${i + 1}`,
                        content"学习Vue3基础语法, 包括组件、路由、状态管理等",
                        status'start',
                        start_time'2025-05-01 12:00:00',
                    })
                    tasks.push({
                        id: i + 2,
                        name: `学习Vue3${i + 2}`,
                        content"学习Vue3基础语法, 包括组件、路由、状态管理等",
                        status'finish',
                        start_time'2025-05-01 12:00:00',
                    })
                    tasks.push({
                        id: i + 3,
                        name: `学习Vue3${i + 3}`,
                        content"学习Vue3基础语法, 包括组件、路由、状态管理等",
                        status'delete',
                        start_time'2025-05-01 12:00:00',
                    })
                }
                resolve(tasks);
            } catch (error) {
                reject(error);
            }
        }, 1000); // 模拟 1 秒的延迟
    });
}

export default  {
    mockStatusGetAll,
}

加载中状态

经过一番努力以后, 加载中的状态也实现了.

在这里插入图片描述

在这里插入图片描述

最终App.vue的代码如下.

<script setup>
import ZdpStatusCardList1 from "@/zdpui/components/ZdpStatusCardList1.vue";
import ZdpModal1 from "@/zdpui/components/ZdpModal1.vue";
import {onMounted} from "vue";
import ZdpSelect1 from "@/zdpui/components/ZdpSelect1.vue";
import status from "@/zdpui/compsable/status.js";
import mockStatus from "@/zdpui/mock/status.js";

const apiGetAll = mockStatus.mockStatusGetAll
const apiUpdate = null
const apiDelete = null
const {
  tasks,
  loading,
  loadData,
} = status.useTaskList(apiGetAll)
const {
  formData,
  onTaskClick,
  modalShow,
  options,
  onChangeTaskStatus,
} = status.useTaskStatus(tasks, apiUpdate, apiDelete)

onMounted(async () => {
  await loadData()
})
</script>

<template>
  <ZdpStatusCardList1
      :tasks="tasks"
      :loading="loading"
      @click="onTaskClick"
  />
  <ZdpModal1
      :show="modalShow"
      @confirm="onChangeTaskStatus"
      @close="modalShow = false"
  >
    <ZdpSelect1
        label="状态"
        v-model="formData.status"
        :options="options"
    />
  </ZdpModal1>
</template>

此时整体效果看上去也还不错.

在这里插入图片描述

在这里插入图片描述

总结

整体而言, 这个计划看板的案例比想象中简单了很多.

整体风格还算比较酷炫.

暂时能用就很棒了, 后面结合fastapi还有zdppy等其他框架打通前后端, 就能够成为一个不错的看板项目了.

宝子们,我在 Python 的世界里摸爬滚打十余载,积累了不少心得体会。如今想把这些年的经验和知识毫无保留地分享给有缘的小伙伴。要是你对 Python 学习感兴趣,欢迎来试听一二,也可以随时在评论区留言或者私信我,咱们一起探讨,共同进步,开启 Python 学习的奇妙之旅!

人生苦短, 我用Python, 坚持每天学习, 坚持每天进步一点点...