1、animation.css
javascript
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
background: #f0f2f5;
color: #303133;
}
/* ========== 消息列表 transition-group 动画 ========== */
.list-enter {
opacity: 0;
transform: translateY(-12px);
}
.list-enter-active {
transition: all 0.35s ease;
}
.list-leave-active {
opacity: 0;
transform: translateX(40px);
transition: all 0.35s ease;
}
.list-move {
transition: transform 0.35s ease;
}
/* ========== 右侧抽屉动画 ========== */
.drawer-enter {
transform: translateX(100%);
}
.drawer-enter-active {
transition: transform 0.35s ease;
}
.drawer-leave-active {
transform: translateX(100%);
transition: transform 0.35s ease;
}
/* ========== 弹窗缩放动画 ========== */
.dialog-enter {
opacity: 0;
transform: scale(0.9);
}
.dialog-enter-active {
transition: all 0.25s ease;
}
.dialog-leave-active {
opacity: 0;
transform: scale(0.9);
transition: all 0.25s ease;
}
/* ========== Toast提示条动画【修复】 ========== */
.toast-enter {
opacity: 0;
transform: translateY(-20px);
}
.toast-enter-active {
transition: all 0.3s ease;
}
.toast-leave-active {
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s ease;
}
/* ========== 遮罩层 ========== */
.mask-enter {
opacity: 0;
}
.mask-enter-active {
opacity: 1;
transition: opacity 0.3s ease;
}
.mask-leave-active {
opacity: 0;
transition: opacity 0.3s ease;
}
2、ToastMessage.vue
javascript
<template>
<transition name="toast" appear>
<div v-if="showToast" class="toast">
{{ msg }}
</div>
</transition>
</template>
<script>
export default {
name: "ToastMessage",
props: {
showToast: {
type: Boolean,
default: false
},
msg: {
type: String,
default: "操作成功"
}
}
};
</script>
<style scoped>
.toast {
position: fixed;
top: 24px;
left: 50%;
transform: translateX(-50%);
min-width: 220px;
padding: 12px 20px;
background: #fff;
color: #303133;
border-left: 4px solid #67c23a;
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
z-index: 9999;
font-size: 14px;
text-align: center;
}
</style>
3、MessageList.vue
javascript
<template>
<div class="message-list">
<transition-group name="list" tag="ul">
<li v-for="item in msgList" :key="item.id" class="msg-item">
<div class="msg-info">
<span class="msg-title">{{ item.title }}</span>
<span class="msg-id">ID: {{ item.id }}</span>
</div>
<div class="msg-actions">
<button class="btn btn-primary btn-sm" @click="$emit('open-drawer', item)">
查看详情
</button>
<button class="btn btn-danger btn-sm" @click="$emit('del-msg', item.id)">
删除
</button>
</div>
</li>
</transition-group>
<div v-if="!msgList.length" class="empty-tip">
暂无消息
</div>
</div>
</template>
<script>
export default {
name: "MessageList",
props: {
msgList: {
type: Array,
default: () => []
}
}
};
</script>
<style scoped>
.message-list {
width: 100%;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
.msg-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
margin-bottom: 10px;
background: #f7fafc;
border: 1px solid #ebeef5;
border-radius: 10px;
transition: background 0.2s ease;
}
.msg-item:hover {
background: #eef4ff;
}
.msg-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.msg-title {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.msg-id {
font-size: 12px;
color: #909399;
}
.msg-actions {
display: flex;
gap: 8px;
}
.btn {
border: none;
border-radius: 8px;
padding: 7px 14px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background: #409eff;
color: #fff;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-danger {
background: #f56c6c;
color: #fff;
}
.btn-danger:hover {
background: #f78989;
}
.empty-tip {
padding: 30px 0;
text-align: center;
color: #909399;
font-size: 14px;
background: #f7fafc;
border-radius: 10px;
}
</style>
4、SlideDrawer.vue
javascript
<template>
<div>
<transition name="mask">
<div v-show="visible" class="drawer-mask" @click="$emit('close')"></div>
</transition>
<transition name="drawer">
<div v-show="visible" class="drawer">
<div class="drawer-header">
<h4>消息详情</h4>
<span class="close-icon" @click="$emit('close')">×</span>
</div>
<div class="drawer-body">
<div class="detail-item">
<label>标题</label>
<p>{{ detail.title }}</p>
</div>
<div class="detail-item">
<label>内容</label>
<p>{{ detail.content }}</p>
</div>
<div class="detail-item">
<label>消息ID</label>
<p>{{ detail.id }}</p>
</div>
</div>
<div class="drawer-footer">
<button class="btn btn-default" @click="$emit('close')">关闭</button>
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "SlideDrawer",
props: {
visible: {
type: Boolean,
default: false
},
detail: {
type: Object,
default: () => ({})
}
}
};
</script>
<style scoped>
.drawer-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 100;
}
.drawer {
position: fixed;
top: 0;
right: 0;
width: 380px;
height: 100vh;
background: #fff;
z-index: 200;
display: flex;
flex-direction: column;
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.08);
}
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #ebeef5;
}
.drawer-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2d3d;
}
.close-icon {
font-size: 24px;
color: #909399;
cursor: pointer;
user-select: none;
}
.close-icon:hover {
color: #303133;
}
.drawer-body {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.detail-item {
margin-bottom: 20px;
}
.detail-item label {
display: block;
margin-bottom: 8px;
font-size: 13px;
color: #909399;
}
.detail-item p {
margin: 0;
font-size: 14px;
color: #303133;
line-height: 1.6;
padding: 12px;
background: #f7fafc;
border-radius: 8px;
}
.drawer-footer {
padding: 16px 20px;
border-top: 1px solid #ebeef5;
text-align: right;
}
.btn {
border: none;
border-radius: 8px;
padding: 9px 18px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-default {
background: #e4e7ed;
color: #303133;
}
.btn-default:hover {
background: #c0c4cc;
}
</style>
5、ConfirmDialog.vue
javascript
<template>
<div>
<transition name="mask">
<div v-show="show" class="dialog-mask"></div>
</transition>
<transition name="dialog" mode="out-in">
<div v-show="show" class="dialog">
<div class="dialog-header">
<h4>确认删除</h4>
</div>
<div class="dialog-body">
<p>确定要删除全部消息吗?此操作无法恢复。</p>
</div>
<div class="dialog-footer">
<button class="btn btn-default" @click="$emit('cancel')">取消</button>
<button class="btn btn-danger" @click="$emit('confirm')">确认删除</button>
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "ConfirmDialog",
props: {
show: {
type: Boolean,
default: false
}
}
};
</script>
<style scoped>
.dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 300;
}
.dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 420px;
background: #fff;
border-radius: 12px;
z-index: 400;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
overflow: hidden;
}
.dialog-header {
padding: 18px 20px;
border-bottom: 1px solid #ebeef5;
}
.dialog-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2d3d;
}
.dialog-body {
padding: 24px 20px;
}
.dialog-body p {
margin: 0;
font-size: 14px;
color: #606266;
line-height: 1.6;
}
.dialog-footer {
padding: 14px 20px;
border-top: 1px solid #ebeef5;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn {
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-default {
background: #e4e7ed;
color: #303133;
}
.btn-default:hover {
background: #c0c4cc;
}
.btn-danger {
background: #f56c6c;
color: #fff;
}
.btn-danger:hover {
background: #f78989;
}
</style>
7、CountUp.vue
javascript
<template>
<div>
<transition :css="false" @before-enter="beforeEnter" @enter="enter">
<span v-if="show" class="num">{{ currentNum }}</span>
</transition>
</div>
</template>
<script>
export default {
name: "CountUp",
props: {
show: {
type: Boolean,
default: false
},
targetNum: {
type: Number,
default: 0
}
},
data() {
return {
currentNum: 0
};
},
methods: {
beforeEnter() {
this.currentNum = 0;
},
enter(el, done) {
const total = this.targetNum;
if (total === 0) {
done();
return;
}
const duration = 1200;
const startTime = performance.now();
const animate = () => {
const now = performance.now();
const progress = Math.min((now - startTime) / duration, 1);
this.currentNum = Math.floor(total * progress);
if (progress >= 1) {
this.currentNum = total;
done();
return;
}
requestAnimationFrame(animate);
};
animate();
}
}
};
</script>
<style scoped>
.num {
display: inline-block;
font-size: 28px;
font-weight: 700;
color: #f56c6c;
padding: 6px 14px;
background: #fef0f0;
border-radius: 10px;
margin-right: 8px;
}
</style>
8、App.vue
javascript
<template>
<div class="app-container">
<header class="app-header">
<h2>电商后台消息中心</h2>
<div class="header-actions">
<CountUp :show="showCount" :target-num="unreadTotal" />
<button class="btn btn-primary" @click="showCount = true">
开始数字计数
</button>
</div>
</header>
<main class="app-main">
<section class="card">
<div class="card-header">
<h3>消息列表</h3>
<button class="btn btn-success" @click="handleAddMsg">
+ 新增消息
</button>
</div>
<!-- 修复:移除错误 @add‑msg 事件,子组件不会派发该事件 -->
<MessageList :msg-list="msgList" @del-msg="handleDelMsg" @open-drawer="handleOpenDrawer" />
</section>
<section class="card">
<div class="card-header">
<h3>消息管理</h3>
</div>
<p class="tip-text">
点击"查看详情"打开右侧抽屉,点击"删除全部消息"弹出确认框。
</p>
<button class="btn btn-danger" @click="openDialog = true">
删除全部消息
</button>
</section>
</main>
<ConfirmDialog :show="openDialog" @cancel="openDialog = false" @confirm="delAllMsg" />
<SlideDrawer :visible="drawerShow" :detail="currentDetail" @close="drawerShow = false" />
<ToastMessage :show-toast="toastShow" :msg="toastText" />
</div>
</template>
<script>
import ToastMessage from "./components/ToastMessage.vue";
import MessageList from "./components/MessageList.vue";
import SlideDrawer from "./components/SlideDrawer.vue";
import ConfirmDialog from "./components/ConfirmDialog.vue";
import CountUp from "./components/CountUp.vue";
export default {
name: "App",
components: {
ToastMessage,
MessageList,
SlideDrawer,
ConfirmDialog,
CountUp
},
data() {
return {
msgId: 100,
msgList: [
{ id: 1, title: "系统通知", content: "您的订单已发货" },
{ id: 2, title: "活动提醒", content: "限时折扣活动开启" }
],
drawerShow: false,
currentDetail: {},
openDialog: false,
toastShow: false,
toastText: "",
showCount: false,
unreadTotal: 88
};
},
methods: {
handleAddMsg() {
this.msgId++;
this.msgList.unshift({
id: this.msgId,
title: `新消息 ${this.msgId}`,
content: "这是一条新推送消息"
});
this.showToast("新增消息成功!");
},
handleDelMsg(id) {
this.msgList = this.msgList.filter(item => item.id !== id);
this.showToast("删除消息成功!");
},
handleOpenDrawer(item) {
this.currentDetail = { ...item };
this.drawerShow = true;
},
delAllMsg() {
this.msgList = [];
this.openDialog = false;
this.showToast("全部消息已清空");
},
showToast(text) {
this.toastText = text;
this.toastShow = true;
setTimeout(() => {
this.toastShow = false;
}, 2500);
}
}
};
</script>
<style scoped>
.app-container {
min-height: 100vh;
padding: 24px;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px 24px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.app-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #1f2d3d;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.app-main {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
}
.card {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2d3d;
}
.tip-text {
margin: 0 0 16px;
font-size: 14px;
color: #909399;
line-height: 1.6;
}
.btn {
border: none;
border-radius: 8px;
padding: 9px 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background: #409eff;
color: #fff;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-success {
background: #67c23a;
color: #fff;
}
.btn-success:hover {
background: #85ce61;
}
.btn-danger {
background: #f56c6c;
color: #fff;
}
.btn-danger:hover {
background: #f78989;
}
</style>
9、main.js
javascript
import Vue from 'vue'
import App from './App.vue'
//引入全局动画css
import './assets/animation.css'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
10、

