vue3使用jparticles生成雪花飘落代码

vue3雪花代码

成果图:

视频:

2024-11-06 16-33-32

一,首先引入 JParticles

npm install jparticles --save

二,去阿里巴巴矢量图库下载雪花图片

多下载几种不同的雪花图片,效果会更好看些

下载完成之后我是用的七牛云的对象存储:对象存储 Kodo_云存储_海量安全高可靠云存储_oss - 七牛云

把图片上传到这个里面,然后生成临时访问的链接就行了:雪花 (3).png (200×200) (clouddn.com)也可以直接放到项目本地

三,完整代码

<template>
  <div ref="snowfallContainer" class="snowfall-container"></div>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";
import JParticles from "jparticles";

const snowfallContainer = ref(null);

onMounted(() => {
  if (snowfallContainer.value) {
    new JParticles.Snow(snowfallContainer.value, {
      // JParticles 的雪花配置项
      count: 100, // 数量
      color: "#FFF", // 颜色
      minSize: 1, // 最小尺寸
      maxSize: 5, // 最大尺寸
      shape: [
        "http://sle6k7baz.hd-bkt.clouddn.com/ax/other/%E9%9B%AA%E8%8A%B1%20%283%29.png?e=1730965992&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:CMOH2B52ah2hiyXSZXuO4gefZBQ=",
        "http://sle6k7baz.hd-bkt.clouddn.com/ax/other/%E9%9B%AA%E8%8A%B1%20%282%29.png?e=1730965785&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:h1ZJa-5GNWzorFLhI6THAehfQLM=",
        "http://sle6k7baz.hd-bkt.clouddn.com/ax/other/%E9%9B%AA%E8%8A%B1%20%281%29.png?e=1730965715&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:UdqK5uMD6NR6obkcchTjaCGO0xM=",
        "http://sle6k7baz.hd-bkt.clouddn.com/ax/other/%E9%9B%AA%E8%8A%B1.png?e=1730965649&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:5wdfCDrRzr3yYJC1TNo0bPxP7Q4=",
      ],//雪花图片地址,我这个链接一天后失效,请换成自己的图片链接
    });
  }
});
</script>

<style scoped>
.snowfall-container {
  width: 100%;
  height: 100%;
  background-image: url("http://sle6k7baz.hd-bkt.clouddn.com/ax/other/snow.jpg?e=1730967250&token=Fj4JTDTCX_isNmsjhWmTjkWWt-75_zWjjYIAv8Gq:HQEG7JqMyx7E3K9UnTNercJO6p8=");
  background-size: cover; /* 覆盖整个容器,可能会裁剪图片 */
  background-position: center; /* 居中背景图片 */
  background-repeat: no-repeat; /* 不重复背景图片 */
}
</style>

这样的话,咱们就是能实现上面展示的雪花的一个动态效果,上面我的图片地址一天后会失效,如果没有显示雪花的话请把图片地址换成自己的。

=======================================

后话

代码优化

昨天本来想把这个部署到服务器上,结果七牛云生成的链接是http协议的,我的网站是https协议的,导致图片加载不出来,本来想着那我就去配置一下ssl,结果这玩意竟然不能使用阿里云申请的ssl证书,oh my god,说实话不是很好用,貌似只能申请七牛云自己的ssl,我想着那我申请一个吧,结果这玩意还要填公司信息,还要填一个座机号,太麻烦了。

一怒之下我怒了一下,然后直接不用了,换成ftp把文件存服务器上

昨天那个代码我顺便优化了一下,把抖音上那个很火的爱心也加进去了

这是优化后的代码运行图及在线地址:AX

雪花和爱心都是动态的,视频我就不录制了,大家拿源码跑一下就行:

<template>
    <div ref="snowfallContainer" class="snowfall-container" style="position:relative">
        <div class="heart-container">
            <canvas ref="heartCanvas"></canvas>
        </div>
    </div>

    <div style="position: fixed ; width: 500px; height: 500px; top:0">
    <div
      style="
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
      "
    >
      <span :style="{ color: textColor }">
        {{ text }}
      </span>
    </div>
    <div style="position: absolute; height: 100%; width: 100%; top: 0; left: 0">
      <ButtonGroup style="left: 4px; top: 4px">
        
      </ButtonGroup>
      <canvas
        id="pinkboard"
        style="position: relative"
        ref="pinkboard"
        @click="changeText"
      ></canvas>
    </div>
  </div>
  
</template>

<script setup>
import { onMounted, ref, watch ,reactive } from "vue";
import JParticles from "jparticles";

const snowfallContainer = ref(null);

const text = ref("XJX");
const textColor = ref("#ea80b0");
const defaultTextColors = ref([
  "#ea80b0",
  "#2D8CF0",
  "#FF9900",
  "#19C919",
  "#9B1DEA",
  "#EA4CA3",
]);
const defaultHeartColors = ref([
  "#ea80b0",
  "#2D8CF0",
  "#FF9900",
  "#19C919",
  "#9B1DEA",
  "#EA4CA3",
]);
const drawerValue = ref(false);
const heartColor = ref("#ea80b0");
const pinkboard = ref(null);
let canvasEl;

onMounted(() => {
  // canvasEl=document.getElementById('pinkboard')
  canvasEl = pinkboard.value;
  puttingItAll(canvasEl);

  if (snowfallContainer.value) {
    new JParticles.Snow(snowfallContainer.value, {
      // JParticles 的雪花配置项
      count: 100, // 数量
      color: "#FFF", // 颜色
      minSize: 1, // 最小尺寸
      maxSize: 5, // 最大尺寸
      shape: [
        "https://wqjay.cn/api/axImg/getImgById/26e3657c6fe4ca96346b97aa0a1a3d51",
        "https://wqjay.cn/api/axImg/getImgById/2147acf9c77a397ed357ed7d5412ef4f",
        "https://wqjay.cn/api/axImg/getImgById/45266fd99bc611e3a3ba260744285052",
        "https://wqjay.cn/api/axImg/getImgById/bdc2e68f13260ee24f8fb2c7ba5ebcf2",
      ],
    });
  }
});
watch(heartColor, () => {
  puttingItAll(canvasEl, heartColor);
});

function getRndInteger(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
const textArr = ["许佳欣", "xjx", "XJX"];
let randomOld = 0;
function changeText() {
  let random = getRndInteger(0, textArr.length - 1);
  if (random == randomOld && random == textArr.length - 1) {
    random = random - 1;
  } else if (random == randomOld) {
    random = random + 1;
  }
  randomOld = random;
  text.value = textArr[random];
}
/*
 * Settings
 */
var settings = {
  particles: {
    length: 500, // maximum amount of particles
    duration: 2, // particle duration in sec
    velocity: 100, // particle velocity in pixels/sec
    effect: -0.75, // play with this for a nice effect
    size: 30, // particle size in pixels
  },
};

/*
 * RequestAnimationFrame polyfill by Erik Möller
 */
(function () {
  var b = 0;
  var c = ["ms", "moz", "webkit", "o"];
  for (var a = 0; a < c.length && !window.requestAnimationFrame; ++a) {
    window.requestAnimationFrame = window[c[a] + "RequestAnimationFrame"];
    window.cancelAnimationFrame =
      window[c[a] + "CancelAnimationFrame"] ||
      window[c[a] + "CancelRequestAnimationFrame"];
  }
  if (!window.requestAnimationFrame) {
    window.requestAnimationFrame = function (h, e) {
      var d = new Date().getTime();
      var f = Math.max(0, 16 - (d - b));
      var g = window.setTimeout(function () {
        h(d + f);
      }, f);
      b = d + f;
      return g;
    };
  }
  if (!window.cancelAnimationFrame) {
    window.cancelAnimationFrame = function (d) {
      clearTimeout(d);
    };
  }
})();

/*
 * Point class
 */
var Point = (function () {
  function Point(x, y) {
    this.x = typeof x !== "undefined" ? x : 0;
    this.y = typeof y !== "undefined" ? y : 0;
  }

  Point.prototype.clone = function () {
    return new Point(this.x, this.y);
  };
  Point.prototype.length = function (length) {
    if (typeof length == "undefined") return Math.sqrt(this.x * this.x + this.y * this.y);
    this.normalize();
    this.x *= length;
    this.y *= length;
    return this;
  };
  Point.prototype.normalize = function () {
    var length = this.length();
    this.x /= length;
    this.y /= length;
    return this;
  };
  return Point;
})();

/*
 * Particle class
 */
var Particle = (function () {
  function Particle() {
    this.position = new Point();
    this.velocity = new Point();
    this.acceleration = new Point();
    this.age = 0;
  }

  Particle.prototype.initialize = function (x, y, dx, dy) {
    this.position.x = x;
    this.position.y = y;
    this.velocity.x = dx;
    this.velocity.y = dy;
    this.acceleration.x = dx * settings.particles.effect;
    this.acceleration.y = dy * settings.particles.effect;
    this.age = 0;
  };
  Particle.prototype.update = function (deltaTime) {
    this.position.x += this.velocity.x * deltaTime;
    this.position.y += this.velocity.y * deltaTime;
    this.velocity.x += this.acceleration.x * deltaTime;
    this.velocity.y += this.acceleration.y * deltaTime;
    this.age += deltaTime;
  };
  Particle.prototype.draw = function (context, image) {
    function ease(t) {
      return --t * t * t + 1;
    }

    var size = image.width * ease(this.age / settings.particles.duration);
    context.globalAlpha = 1 - this.age / settings.particles.duration;
    context.drawImage(
      image,
      this.position.x - size / 2,
      this.position.y - size / 2,
      size,
      size
    );
  };
  return Particle;
})();

/*
 * ParticlePool class
 */

var ParticlePool = (function () {
  var particles,
    firstActive = 0,
    firstFree = 0,
    duration = settings.particles.duration;

  function ParticlePool(length) {
    // create and populate particle pool
    particles = new Array(length);
    for (var i = 0; i < particles.length; i++) particles[i] = new Particle();
  }

  ParticlePool.prototype.add = function (x, y, dx, dy) {
    particles[firstFree].initialize(x, y, dx, dy);

    // handle circular queue
    firstFree++;
    if (firstFree == particles.length) firstFree = 0;
    if (firstActive == firstFree) firstActive++;
    if (firstActive == particles.length) firstActive = 0;
  };
  ParticlePool.prototype.update = function (deltaTime) {
    let i;

    // update active particles
    if (firstActive < firstFree) {
      for (i = firstActive; i < firstFree; i++) particles[i].update(deltaTime);
    }
    if (firstFree < firstActive) {
      for (i = firstActive; i < particles.length; i++) particles[i].update(deltaTime);
      for (i = 0; i < firstFree; i++) particles[i].update(deltaTime);
    }

    // remove inactive particles
    while (particles[firstActive].age >= duration && firstActive != firstFree) {
      firstActive++;
      if (firstActive == particles.length) firstActive = 0;
    }
  };
  ParticlePool.prototype.draw = function (context, image) {
    let i;
    // draw active particles
    if (firstActive < firstFree) {
      for (i = firstActive; i < firstFree; i++) particles[i].draw(context, image);
    }
    if (firstFree < firstActive) {
      for (i = firstActive; i < particles.length; i++) particles[i].draw(context, image);
      for (i = 0; i < firstFree; i++) particles[i].draw(context, image);
    }
  };
  return ParticlePool;
})();

/*
 * Putting it all together
 */
function puttingItAll(canvas, heartColor) {
  var context = canvas.getContext("2d"),
    particles = new ParticlePool(settings.particles.length),
    particleRate = settings.particles.length / settings.particles.duration, // particles/sec
    time;

  // get point on heart with -PI <= t <= PI
  function pointOnHeart(t) {
    return new Point(
      160 * Math.pow(Math.sin(t), 3),
      130 * Math.cos(t) -
        50 * Math.cos(2 * t) -
        20 * Math.cos(3 * t) -
        10 * Math.cos(4 * t) +
        25
    );
  }

  // creating the particle image using a dummy canvas
  var image = (function () {
    var canvas = document.createElement("canvas"),
      context = canvas.getContext("2d");
    canvas.width = settings.particles.size;
    canvas.height = settings.particles.size;

    // helper function to create the path
    function to(t) {
      var point = pointOnHeart(t);
      point.x = settings.particles.size / 2 + (point.x * settings.particles.size) / 350;
      point.y = settings.particles.size / 2 - (point.y * settings.particles.size) / 350;
      return point;
    }

    // create the path
    context.beginPath();
    var t = -Math.PI;
    var point = to(t);
    context.moveTo(point.x, point.y);
    while (t < Math.PI) {
      t += 0.01; // baby steps!
      point = to(t);
      context.lineTo(point.x, point.y);
    }
    context.closePath();
    // create the fill
    context.fillStyle = heartColor ? heartColor.value : "#ea80b0";
    context.fill();
    // create the image
    var image = new Image();
    image.src = canvas.toDataURL();
    return image;
  })();

  // render that thing!
  function render() {
    // next animation frame
    requestAnimationFrame(render);

    // update time
    var newTime = new Date().getTime() / 1000,
      deltaTime = newTime - (time || newTime);
    time = newTime;

    // clear canvas
    context.clearRect(0, 0, canvas.width, canvas.height);

    // create new particles
    var amount = particleRate * deltaTime;
    for (var i = 0; i < amount; i++) {
      var pos = pointOnHeart(Math.PI - 2 * Math.PI * Math.random());
      var dir = pos.clone().length(settings.particles.velocity);
      particles.add(canvas.width / 2 + pos.x, canvas.height / 2 - pos.y, dir.x, -dir.y);
    }

    // update and draw particles
    particles.update(deltaTime);
    particles.draw(context, image);
  }

  // handle (re-)sizing of the canvas
  function onResize() {
    canvas.width = canvas.clientWidth;
    canvas.height = canvas.clientHeight;
  }

  window.onresize = onResize;

  // delay rendering bootstrap
  setTimeout(function () {
    onResize();
    render();
  }, 10);
}
</script>

<style scoped>
html,
body {
  height: 100%;
  padding: 0;
  margin: 0;
  background: #000;
}

.snowfall-container {
  width: 100%;
  height: 100%;
  background-image: url("https://wqjay.cn/api/axImg/getImgById/280b3c2eb17061a5f230c061630047e6");
  background-size: cover; /* 覆盖整个容器,可能会裁剪图片 */
  background-position: center; /* 居中背景图片 */
  background-repeat: no-repeat; /* 不重复背景图片 */
}

.heart-container {
  position: relative;
  width: 100%;
  height: 100%;
}

canvas {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}
</style>

ftp工具类

顺便把ftp工具类也给到大家:

package com.wq.ax.utils;


import com.wq.ax.common.exception.CustomException;
import com.wq.ax.common.exception.ErrorCode;
import jakarta.servlet.http.HttpServletResponse;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;

public class UploadUtil {


    //本地测试可以用服务器ip,就填192.168.1.1这种ip地址就行
    //假如我的ftp在服务器180.101.50.188上,而且我们的后端代码也部署在180.101.50.188,也就是同一台服务器上
    //那我们这里得用localhost,否则连不上
    private final static String server = "localhost";//ip地址

    private final static Integer port = 21;//端口

    private final static String username = "";//用户名

    private final static String password = "";//密码

    private final static String homePath = "/www/wwwroot/ftp_wq";//ftp文件夹所在目录

    /**
     * 上传文件到指定路径
     *
     * @param multipartFile 文件
     * @param dirPath 上传地址
     * @return
     * @throws Exception
     */
    public static String upload(MultipartFile multipartFile, String dirPath, Boolean masterDrawing) throws Exception {
        // 获取文件名
        String fileName = multipartFile.getOriginalFilename();
        if (fileName == null || fileName.isEmpty()) {
            throw new IllegalArgumentException("文件名不能为空");
        }

        // 创建FTPClient实例
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接到服务器
            ftpClient.connect(server, port);
            // 登录
            ftpClient.login(username, password); // 使用你的FTP用户名和密码
            // 设置文件类型为二进制(对于图片等文件)
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            // 设置编码为UTF-8
            ftpClient.setControlEncoding("UTF-8");

            // 转到目标目录,如果目录不存在则创建
            ftpClient.changeWorkingDirectory(dirPath);
            if (!ftpClient.changeWorkingDirectory(dirPath)) {
                // 创建目录
                if (!ftpClient.makeDirectory(dirPath)) {
                    throw new CustomException(ErrorCode.SERVER_ERROR, "创建目录失败");
                }
                // 切换到新创建的目录
                ftpClient.changeWorkingDirectory(dirPath);
            }

            // 检查文件是否存在
//            String[] fileNames = ftpClient.listNames();
//            boolean fileExists = false;
//            for (String name : fileNames) {
//                if (name.equals(fileName)) {
//                    fileExists = true;
//                    break;
//                }
//            }
//
//            if (fileExists) {
//                // 生成随机文件名
//                String randomFileName = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + "." + FilenameUtils.getExtension(fileName);
//                fileName = randomFileName;
//            }

            String substring = fileName.substring(fileName.lastIndexOf('.') + 1);
            // 生成随机文件名
            String randomFileName = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + "." + substring;
            fileName = randomFileName;

            // 根据是否上传原图去判断要不要调用压缩方法
            InputStream inputStream;
            if (masterDrawing) {
                inputStream = multipartFile.getInputStream();
            } else {
                inputStream = compressImage(multipartFile);
            }
            // 将文件保存到服务器
            boolean uploaded = ftpClient.storeFile(fileName, inputStream);
            inputStream.close();

            if (!uploaded) {
                throw new CustomException(ErrorCode.SERVER_ERROR, "文件上传失败");
            }

            // 返回文件上传后的完整路径
            return dirPath + "/" + fileName;
        } finally {
            // 关闭连接
            if (ftpClient.isConnected()) {
                ftpClient.logout();
                ftpClient.disconnect();
            }
        }
    }

    /**
     * 移动文件到指定路径
     * @param fromPath 原路径
     * @param toPath 新路径
     * @return
     * @throws Exception
     */
    public static String remove(String fromPath, String toPath) throws Exception {
        // 创建 FTPClient 实例
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接到 FTP 服务器
            ftpClient.connect(server, port);
            // 登录 FTP 服务器
            ftpClient.login(username, password);
            // 设置文件传输类型为二进制
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            // 设置被动模式
            ftpClient.enterLocalPassiveMode();

            String lastPathComponent = getLastPathComponent(fromPath);
            // 移动文件
            boolean success = ftpClient.rename(fromPath, toPath+lastPathComponent);
            if (!success) {
                throw new IOException("文件移动失败");
            } else {
                return toPath+lastPathComponent;
            }
        } finally {
            // 关闭连接
            if (ftpClient.isConnected()) {
                ftpClient.logout();
                ftpClient.disconnect();
            }
        }
    }

    /**
     * 根据id获取文件预览流
     * @param response
     * @param filename 文件完整地址
     * @throws IOException
     */
    public static void getFile(HttpServletResponse response, String filename) throws IOException {
        // 配置FTP客户端
        FTPClient ftpClient = new FTPClient();
        ftpClient.setControlEncoding("UTF-8");
        ftpClient.connect(server, port);
        ftpClient.login(username, password);

        // 设置文件类型为二进制
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);

        try (InputStream inputStream = ftpClient.retrieveFileStream(filename)) {
            // 设置输出的格式
            response.reset();
            // 设置Content-Type为图片类型
            response.setContentType("image/jpeg"); // 或者根据文件格式设置为 image/png 等
            response.setHeader("Content-Disposition", "inline; filename=\"" + getLastPart(filename) + "\"");

            // 循环取出流中的数据
            byte[] b = new byte[1024];
            int len;
            while ((len = inputStream.read(b)) > 0) {
                response.getOutputStream().write(b, 0, len);
            }
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
            // 返回错误状态码和消息
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().write("An error occurred while retrieving the file.");
            response.getWriter().flush();
        } finally {
            try {
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static String getFileUrl(String filename){
        return null;
    }

    /**
     * 删除文件
     * @param filePath 文件完整地址
     * @return
     * @throws Exception
     */
    public static boolean deleteFile(String filePath) throws Exception {
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接到FTP服务器
            ftpClient.connect(server, port);
            // 登录到FTP服务器
            ftpClient.login(username, password);
            // 设置文件类型为二进制,以确保文件内容不会改变
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            // 设置编码为UTF-8,防止中文乱码
            ftpClient.setControlEncoding("UTF-8");

            // 转换文件路径编码,防止中文乱码
            String remoteFilePath = new String(filePath.getBytes("UTF-8"), "ISO-8859-1");

            // 删除文件
            return ftpClient.deleteFile(remoteFilePath);
        } finally {
            // 关闭FTP连接
            if (ftpClient.isConnected()) {
                ftpClient.logout();
                ftpClient.disconnect();
            }
        }
    }

    public static String getLastPart(String str) {
        if (str == null || str.isEmpty()) {
            return "";
        }

        int lastSlashIndex = str.lastIndexOf('/');
        if (lastSlashIndex == -1) {
            return str; // 如果没有找到'/',返回整个字符串
        }

        return str.substring(lastSlashIndex + 1); // 返回最后一个'/'后面的内容
    }

    /**
     * 使用Thumbnailator压缩图片
     */
    private static ByteArrayInputStream compressImage(MultipartFile file) throws IOException {
        // 读取图片文件
        BufferedImage image = ImageIO.read(file.getInputStream());

        String contentType = file.getContentType();
        String outputFormat = "jpg"; // 默认格式

        if (contentType != null) {
            if (contentType.equalsIgnoreCase("image/png")) {
                outputFormat = "png";
            } else if (contentType.equalsIgnoreCase("image/gif")) {
                outputFormat = "gif";
            }
            // 可以根据需要添加更多格式
        }


        // 使用Thumbnailator压缩图片
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        Thumbnails.of(image)
                .scale(1.0) // 保持原始尺寸
                .outputQuality(0.75) // 设置压缩质量
                .outputFormat(outputFormat)
                .toOutputStream(outputStream);

        // 将压缩后的图片转换为InputStream
        return new ByteArrayInputStream(outputStream.toByteArray());
    }

    public static String getLastPathComponent(String path) {
        // 找到最后一个'/'的位置
        int lastSlashIndex = path.lastIndexOf('/');

        // 如果路径以'/'结尾,或者没有找到'/',则返回整个路径
        if (lastSlashIndex == path.length() - 1 || lastSlashIndex == -1) {
            return path;
        }

        // 返回最后一个'/'后面的字符串
        return path.substring(lastSlashIndex + 1);
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值