实验十三 Vue 过渡与动画—电商后台消息中心实现

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、

相关推荐
hunterandroid1 小时前
Navigation Component 工程落地:从单 Activity 架构到深度链接配置
android·前端
无责任此方_修行中1 小时前
如何不用括号调用 JS 的函数?
前端·javascript·node.js
kyriewen1 小时前
我数了 647 条大厂前端 JD:AI 不再只是加分项
前端·程序员·ai编程
无责任此方_修行中1 小时前
Deno 的八年:一场没逃出 Node 引力的运行时起义
前端·javascript·node.js
java1234_小锋1 小时前
【技术专题】HTML5 网页技术 - HTML5 语义化标签
前端·html·html5
涛涛ing2 小时前
2026年,你的JavaScript计数逻辑可以删了:CSS刚刚学会了自己数数
前端
广州华水科技2 小时前
单北斗GNSS变形监测系统在水库安全监测中的应用与优势
前端
PBitW2 小时前
行内编辑,如何防止后端未返回,直接全部更新了?
前端·面试
wjjzhbb2 小时前
前端监控:给用户体验装上仪表盘
前端·ux