feat: 🚀 tabs

This commit is contained in:
2025-07-29 11:41:09 +08:00
parent 1a97f75546
commit 41da6b1914
10 changed files with 451 additions and 153 deletions

View File

@@ -1,32 +1,99 @@
import { Quill } from "@vueup/vue-quill";
// 源码中是import直接倒入这里要用Quill.import引入
const BlockEmbed = Quill.import("blots/block/embed");
const Link = Quill.import("formats/link");
const ATTRIBUTES = ["height", "width"];
const ATTRIBUTES = ["height", "width", "poster"];
class Video extends BlockEmbed {
static create(value) {
let node = super.create();
// 添加video标签所需的属性
// 基础视频属性
node.setAttribute("controls", "controls");
node.setAttribute("playsinline", "true");
node.setAttribute("webkit-playsinline", "true");
node.setAttribute("type", "video/mp4");
// poster 属性指定视频下载时显示的图像,或者在用户点击播放按钮前显示的图像。
// console.log(value.url, "= value.poster=");
// node.setAttribute("poster", this.sanitize(value.url));
node.setAttribute("src", this.sanitize(value.url));
// 处理视频URL添加时间片段定位到0.001秒(避开黑屏)
const baseUrl = this.sanitize(value.url);
const videoUrl = baseUrl.includes("#") ? `${baseUrl}&t=0.001` : `${baseUrl}#t=0.001`;
node.setAttribute("src", videoUrl);
// 临时封面(加载中显示)
node.setAttribute(
"poster",
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='300' viewBox='0 0 600 300'%3E%3Crect width='100%25' height='100%25' fill='%23f0f0f0'/%3E%3Ccircle cx='300' cy='150' r='40' fill='%23ccc'/%3E%3Cpolygon points='300,130 330,160 270,160' fill='white'/%3E%3C/svg%3E"
);
// 自动截取封面未提供poster时
if (!value.poster) {
this.captureFrameAsBlob(videoUrl, node);
} else {
node.setAttribute("poster", this.sanitize(value.poster));
}
// width: 600px;
// height: 300px;
// object-fit: contain;
// 视频样式
node.setAttribute(
"style",
`
width: 600px;
height: 300px;
`
);
return node;
}
/**
* 生成视频帧Blob核心方法返回Promise
* @param {string} videoUrl - 视频的本地Blob URL
* @returns {Promise<Blob|null>} - 视频帧Blob或null
*/
/**
* 将视频Blob URL转换为JPG图片Blob
* @param {string} videoBlobUrl - 本地视频Blob URLblob:xxx
* @returns {Promise<Blob|null>} - JPG格式图片Blob失败返回null
*/
static async captureVideoFrame(videoBlobUrl) {
return new Promise(resolve => {
// 1. 创建视频元素加载Blob
const video = document.createElement("video");
video.src = videoBlobUrl;
video.muted = true;
video.playsInline = true;
video.preload = "auto";
// 2. 视频可播放时开始转换
video.oncanplay = () => {
// 3. 创建Canvas绘制视频帧
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth || 640; // 使用视频实际宽度
canvas.height = video.videoHeight || 360; // 使用视频实际高度
const ctx = canvas.getContext("2d");
// 绘制视频当前帧到Canvas
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 4. 将Canvas转换为JPG Blob
canvas.toBlob(
blob => {
console.log(blob, "================>");
video.remove(); // 清理视频元素
resolve(blob || null); // 返回JPG Blob
},
"image/jpeg", // 强制JPG格式
0.8 // 图片质量0-1
);
};
// 错误处理
video.onerror = () => {
video.remove();
resolve(null);
};
});
}
// 以下方法保持不变
static formats(domNode) {
return ATTRIBUTES.reduce((formats, attribute) => {
if (domNode.hasAttribute(attribute)) {
@@ -41,15 +108,14 @@ class Video extends BlockEmbed {
}
static value(domNode) {
// 设置自定义的属性值
return {
url: domNode.getAttribute("src")
// poster: domNode.getAttribute("src")
url: domNode.getAttribute("src").split("#")[0],
poster: domNode.getAttribute("poster") || ""
};
}
format(name, value) {
if (ATTRIBUTES.indexOf(name) > -1) {
if (ATTRIBUTES.includes(name)) {
if (value) {
this.domNode.setAttribute(name, value);
} else {
@@ -61,12 +127,13 @@ class Video extends BlockEmbed {
}
html() {
const { video } = this.value();
return `<a href="${video}">${video}</a>`;
const { url, poster } = this.value();
return `<video src="${url}" ${poster ? `poster="${poster}"` : ""} controls playsinline webkit-playsinline ></video>`;
}
}
Video.blotName = "customVideo"; // 这里不用改不用iframe直接替换掉原来如果需要也可以保留原来的这里用个新的blot
Video.className = "ql-video"; // 可添加样式,看实际使用需要
Video.tagName = "video"; // 用video标签替换iframe
//style="width:600px;height:300px;"
Video.blotName = "customVideo";
// Video.className = "ql-video";
Video.tagName = "video";
export default Video;