摘要:学习Vue3学习vue3,学习使用vue3开发一个组件库,学习使用vue3开发一个项目开
前言
通过前面的准备, 我们已经有了基本的zdpui的框架, 接下来, 我们就使用实战的方式, 一边做一些实战项目, 一边积累的完善组件.
比如接下来, 我们就来做一个看板项目,这也是我一直想做的一个项目.
基本卡片
首先, 封装一个基本的卡片.
学习Vue3
学习vue3,学习使用vue3开发一个组件库,学习使用vue3开发一个项目
开始时间: 2025-01-01 12:33:33
状态:
未开始
卡片列表
初步封装了一个状态卡片组件:
import {computed} from"vue";
const props = defineProps({
title: {
type: String,
default: '这是一个卡片标题',
},
content: {
type: String,
default: '这是一个卡片内容',
},
date: {
type: String,
default: '2025-01-01 12:33:33',
},
status: {
type: String,
// 状态: init start finish delete
default: 'deleted',
}
})
const statusText = computed( => {
switch (props.status) {
case"init":
return"未开始"
case"start":
return"进行中"
case"finish":
return"已完成"
case"delete":
return"已删除"
}
})
{{ props.title }}
{{ props.content }}
开始时间: {{ props.date }}
{{ statusText }}
/*卡片的样式*/
.card {
background: linear-gradient(45deg, rgba(0, 123, 255, 0.7), rgba(0, 212, 255, 0.7)); /* 45度角的渐变色,从稍深的科技蓝到浅一点的蓝色,添加透明度以实现半透明效果 */
border-radius: 15px; /* 圆角边框 */
padding: 20px;
margin: 20px;
width: 300px;
box-shadow: 04px8pxrgba(0, 0, 0, 0.2); /* 初始阴影效果 */
transition: all 0.3s ease-in-out; /* 过渡效果,使动画更平滑 */
position: relative;
overflow: hidden; /* 隐藏溢出部分,用于后续的伪元素动画 */
cursor: pointer;
}
.card:hover {
transform: translateY(-10px); /* 悬停时向上移动 */
box-shadow: 08px16pxrgba(0, 0, 0, 0.3); /* 悬停时阴影加重 */
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 5px;
background: linear-gradient(to right, rgba(0, 255, 255, 0.8), rgba(0, 123, 255, 0.8)); /* 顶部的渐变线条,增加科技感 */
transform: scaleX(0); /* 初始状态为 0,用于动画效果 */
transform-origin: left;
transition: transform 0.3s ease-in-out;
}
.card:hover::before {
transform: scaleX(1); /* 悬停时展开顶部的渐变线条 */
}
.card::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 5px;
background: linear-gradient(to left, rgba(0, 255, 255, 0.8), rgba(0, 123, 255, 0.8)); /* 底部的渐变线条,与顶部对应 */
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s ease-in-out;
}
.card:hover::after {
transform: scaleX(1); /* 悬停时展开底部的渐变线条 */
}
/*标题的样式*/
.title {
font-size: 24px;
color: white;
margin-bottom: 10px;
}
/*内容的样式*/
.content {
font-size: 16px;
color: #fff; /* 将颜色修改为白色,提高对比度 */
margin-bottom: 10px;
line-height: 1.5;
animation: fadeIn 1s ease-in-out; /* 淡入动画 */
text-shadow: 1px1px2pxrgba(0, 0, 0, 0.3); /* 添加文字阴影,增强可读性 */
}
.content:hover {
transform: scale(1.05); /* 悬停时放大 */
transition: transform 0.3s ease-in-out;
color: #f2f2f2; /* 悬停时的颜色稍作调整,使其有变化 */
text-shadow: 2px2px3pxrgba(0, 0, 0, 0.4); /* 悬停时的文字阴影加深 */
}
/*日期的样式*/
.date {
font-size: 14px;
color: #d9d9d9;
margin-bottom: 10px;
font-style: italic;
animation: slideInLeft 1s ease-in-out; /* 从左滑入动画 */
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInLeft {
from {
transform: translateX(-50px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/*状态的样式*/
.statusspan {
display: inline-block;
padding: 5px10px;
border-radius: 5px;
color: white;
font-size: 14px;
}
/*未开始状态*/
.init {
display: inline-block;
padding: 5px10px;
background-color: #FFC107; /* 未开始状态的颜色,可根据喜好调整 */
border-radius: 5px;
color: white;
font-size: 14px;
animation: pulseNotStarted 1s infinite; /* 未开始状态的脉冲动画 */
}
/*进行中*/
.start {
display: inline-block;
padding: 5px10px;
background-color: #28A745; /* 进行中状态的颜色,可根据喜好调整 */
border-radius: 5px;
color: white;
font-size: 14px;
animation: blinkInProgress 1s infinite alternate; /* 进行中状态的闪烁动画 */
}
/*已完成*/
.finish {
display: inline-block;
padding: 5px10px;
background-color: #17A2B8; /* 已完成状态的颜色,可根据喜好调整 */
border-radius: 5px;
color: white;
font-size: 14px;
animation: bounceCompleted 1s ease-in-out; /* 已完成状态的弹动动画 */
}
/*删除了*/
.delete {
display: inline-block;
padding: 5px10px;
background-color: #6C757D; /* 已删除状态的颜色,可根据喜好调整 */
border-radius: 5px;
color: white;
font-size: 14px;
animation: fadeOutDeleted 1s ease-in-out; /* 已删除状态的淡出动画 */
}
/* 未开始状态的脉冲动画关键帧 */
@keyframes pulseNotStarted {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
/* 进行中状态的闪烁动画关键帧 */
@keyframes blinkInProgress {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
/* 已完成状态的弹动动画关键帧 */
@keyframes bounceCompleted {
0% {
transform: translateY(0);
}
25% {
transform: translateY(-5px);
}
50% {
transform: translateY(0);
}
75% {
transform: translateY(-3px);
}
100% {
transform: translateY(0);
}
}
/* 已删除状态的淡出动画关键帧 */
@keyframes fadeOutDeleted {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
然后封装了一个卡片列表组件.
import ZdpStatusCard1 from"@/ZdpStatusCard1.vue";
const props = defineProps({
title: {
type: String,
default: "计划看板"
},
// 任务列表 必须有 name, content, status, start_time 这几个属性
tasks: {
type: Array,
required: true,
}
})
{{ props.title }}
:status="v.status"
:content="v.content"
:date="v.start_time"
/>
.plan {
background: linear-gradient(-45deg, #007BFF, #00BFFF, #1E90FF, #4169E1);
background-size: 400%400%;
animation: gradientAnimation 15s ease infinite;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: 20px;
color: white;
}
@keyframes gradientAnimation {
0% {
background-position: 050%;
}
50% {
background-position: 100%50%;
}
100% {
background-position: 050%;
}
}
.title {
margin-bottom: 30px;
font-size: 2em;
text-shadow: 2px2px4pxrgba(0, 0, 0, 0.5);
animation: fadeIn 1s ease-in-out;
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 80%;
}
/* 如果你想为 ZdpStatusCard1 组件添加一些通用的样式,可以添加以下内容,假设 ZdpStatusCard1 组件的最外层元素是一个 div */
.cardsdiv {
margin: 20px;
transition: transform 0.3s ease-in-out;
}
.cardsdiv:hover {
transform: translateY(-10px);
}
最后在App.vue里面使用.
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: ,
status: 'start',
start_time: ,
})
tasks.push({
id: i + 2,
name: '学习Vue3',
content: ,
status: 'finish',
start_time: ,
})
tasks.push({
id: i + 3,
name: '学习Vue3',
content: ,
status: 'delete',
start_time: ,
})
}
/>
此时的页面效果如下.
在这里插入图片描述修改状态
当我们点击卡片的时候, 可以弹出一个对话框, 让用户修改状态.
在这里插入图片描述点击取消就是不修改, 点击确定则执行修改.
此时App.vue完整代码如下.
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: ,
status: 'start',
start_time: ,
})
tasks.push({
id: i + 2,
name: '学习Vue3',
content: ,
status: 'finish',
start_time: ,
})
tasks.push({
id: i + 3,
name: '学习Vue3',
content: ,
status: 'delete',
start_time: ,
})
}
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
}
@click="onTaskClick"
/>
@confirm="onChangeTaskStatus"
@close="modalShow = false"
>
v-model="formData.status"
:options="options"
/>
但是此时我们并没有真正的修改, 我们应该查找到要修改的任务, 修改其状态.
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,
}
}
exportdefault {
useTaskStatus,
}
此时App.vue代码已经很少了.
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: ,
status: 'start',
start_time: ,
})
tasks.value.push({
id: i + 2,
name: `学习Vue3${i + 2}`,
content: ,
status: 'finish',
start_time: ,
})
tasks.value.push({
id: i + 3,
name: `学习Vue3${i + 3}`,
content: ,
status: 'delete',
start_time: ,
})
}
const apiUpdate = null
const apiDelete = null
const {
formData,
onTaskClick,
modalShow,
options,
onChangeTaskStatus,
} =status.useTaskStatus(tasks, apiUpdate, apiDelete)
@click="onTaskClick"
/>
@confirm="onChangeTaskStatus"
@close="modalShow = false"
>
v-model="formData.status"
:options="options"
/>
不过我感觉还有优化空间, 就是这个任务列表. 任务列表的数据理论上应该是从后端动态生成的, 如果没有就用前端的模拟数据. 加载过程中应该也要有加载中的效果.
模拟任务列表接口
// 模拟获取所有的任务const mockStatusGetAll = =>{
returnnewPromise((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: ,
status: 'start',
start_time: ,
})
tasks.push({
id: i + 2,
name: `学习Vue3${i + 2}`,
content: ,
status: 'finish',
start_time: ,
})
tasks.push({
id: i + 3,
name: `学习Vue3${i + 3}`,
content: ,
status: 'delete',
start_time: ,
})
}
resolve(tasks);
} catch (error) {
reject(error);
}
}, 1000); // 模拟 1 秒的延迟
});
}
exportdefault {
mockStatusGetAll,
}
加载中状态
经过一番努力以后, 加载中的状态也实现了.
在这里插入图片描述最终App.vue的代码如下.
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
})
:loading="loading"
@click="onTaskClick"
/>
@confirm="onChangeTaskStatus"
@close="modalShow = false"
>
v-model="formData.status"
:options="options"
/>
此时整体效果看上去也还不错.
在这里插入图片描述总结
整体而言, 这个计划看板的案例比想象中简单了很多.
整体风格还算比较酷炫.
暂时能用就很棒了, 后面结合fastapi还有zdppy等其他框架打通前后端, 就能够成为一个不错的看板项目了.
宝子们,我在 Python 的世界里摸爬滚打十余载,积累了不少心得体会。如今想把这些年的经验和知识毫无保留地分享给有缘的小伙伴。要是你对 Python 学习感兴趣,欢迎来试听一二,也可以随时在评论区留言或者私信我,咱们一起探讨,共同进步,开启 Python 学习的奇妙之旅!
人生苦短, 我用Python, 坚持每天学习, 坚持每天进步一点点...
#深度好文计划#
来源:晓霞科技观