411 lines
11 KiB
TypeScript
411 lines
11 KiB
TypeScript
import moment from 'moment';
|
||
|
||
export function getToken(): string {
|
||
return window.localStorage.getItem('playedu-backend-token-' + getTenant()) || '';
|
||
}
|
||
|
||
export function setToken(token: string) {
|
||
window.localStorage.setItem('playedu-backend-token-' + getTenant(), token);
|
||
}
|
||
|
||
export function getTenant(): string {
|
||
return window.localStorage.getItem('tenantId') || '-1';
|
||
}
|
||
export function getTenantCode(): string {
|
||
return window.localStorage.getItem('tenantCode') || 'platform';
|
||
}
|
||
|
||
export function setTenant(tenantId: string, tenantCode: string) {
|
||
window.localStorage.setItem('tenantId', tenantId);
|
||
window.localStorage.setItem('tenantCode', tenantCode);
|
||
}
|
||
|
||
export function clearToken() {
|
||
window.localStorage.removeItem('playedu-backend-token-' + getTenant());
|
||
}
|
||
|
||
export function getLanguage(): string {
|
||
return window.localStorage.getItem('playedu-backend-language') || 'zh-CN';
|
||
}
|
||
|
||
export function setLanguage(text: string) {
|
||
window.localStorage.setItem('playedu-backend-language', text);
|
||
}
|
||
|
||
export function clearLanguage() {
|
||
window.localStorage.removeItem('playedu-backend-language');
|
||
}
|
||
|
||
export function dateFormat(dateStr: string) {
|
||
if (!dateStr) {
|
||
return '-';
|
||
}
|
||
return moment(dateStr).format('YYYY-MM-DD HH:mm');
|
||
}
|
||
export function dateFormatNoTime(dateStr: string) {
|
||
if (!dateStr) {
|
||
return '-';
|
||
}
|
||
return moment(dateStr).format('YYYY-MM-DD');
|
||
}
|
||
|
||
export function generateUUID(): string {
|
||
let guid = '';
|
||
for (let i = 1; i <= 32; i++) {
|
||
const n = Math.floor(Math.random() * 16.0).toString(16);
|
||
guid += n;
|
||
if (i === 8 || i === 12 || i === 16 || i === 20) guid += '-';
|
||
}
|
||
return guid;
|
||
}
|
||
|
||
export function transformBase64ToBlob(base64: string, mime: string, filename: string): File {
|
||
const arr = base64.split(',');
|
||
const bstr = atob(arr[1]);
|
||
let n = bstr.length;
|
||
const u8arr = new Uint8Array(n);
|
||
while (n--) {
|
||
u8arr[n] = bstr.charCodeAt(n);
|
||
}
|
||
return new File([u8arr], filename, { type: mime });
|
||
}
|
||
|
||
export function parseVideo(file: File): Promise<VideoParseInfo> {
|
||
return new Promise((resolve, reject) => {
|
||
const video = document.createElement('video');
|
||
video.muted = true;
|
||
video.setAttribute('src', URL.createObjectURL(file));
|
||
video.setAttribute('autoplay', 'autoplay');
|
||
video.setAttribute('crossOrigin', 'anonymous'); //设置跨域 否则toDataURL导出图片失败
|
||
video.setAttribute('width', '400'); //设置大小,如果不设置,下面的canvas就要按需设置
|
||
video.setAttribute('height', '300');
|
||
video.currentTime = 7; //视频时长,一定要设置,不然大概率白屏
|
||
video.addEventListener('loadeddata', function () {
|
||
const canvas = document.createElement('canvas'),
|
||
width = video.width, //canvas的尺寸和图片一样
|
||
height = video.height;
|
||
canvas.width = width; //画布大小,默认为视频宽高
|
||
canvas.height = height;
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) {
|
||
return reject('无法捕获视频帧');
|
||
}
|
||
ctx.drawImage(video, 0, 0, width, height); //绘制canvas
|
||
const dataURL = canvas.toDataURL('image/png'); //转换为base64
|
||
video.remove();
|
||
const info: VideoParseInfo = {
|
||
poster: dataURL,
|
||
duration: parseInt(video.duration + ''),
|
||
};
|
||
return resolve(info);
|
||
});
|
||
});
|
||
}
|
||
|
||
export function parseAudio(file: File): Promise<AudioParseInfo> {
|
||
return new Promise((resolve, reject) => {
|
||
const audio = document.createElement('audio');
|
||
audio.muted = true;
|
||
audio.setAttribute('src', URL.createObjectURL(file));
|
||
audio.setAttribute('autoplay', 'true');
|
||
audio.setAttribute('controls', 'true'); //设置设置
|
||
audio.currentTime = 7;
|
||
audio.addEventListener('loadeddata', function () {
|
||
const info: AudioParseInfo = {
|
||
duration: parseInt(audio.duration + ''),
|
||
};
|
||
audio.remove();
|
||
return resolve(info);
|
||
});
|
||
});
|
||
}
|
||
|
||
export function getHost() {
|
||
return window.location.protocol + '//' + window.location.host + '/';
|
||
}
|
||
|
||
export function inStrArray(array: string[], value: string): boolean {
|
||
for (let i = 0; i < array.length; i++) {
|
||
if (array[i] === value) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
export function ValidataCredentials(value: any) {
|
||
const regIdCard =
|
||
/^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/;
|
||
if (regIdCard.test(value)) {
|
||
if (value.length === 18) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
export function checkUrl(value: any) {
|
||
let url = value;
|
||
const str = url.substr(url.length - 1, 1);
|
||
if (str !== '/') {
|
||
url = url + '/';
|
||
}
|
||
return url;
|
||
}
|
||
|
||
export function dateWholeFormat(dateStr: string) {
|
||
if (!dateStr) {
|
||
return '';
|
||
}
|
||
return moment(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||
}
|
||
|
||
export function transUtcTime(value: string) {
|
||
const specifiedTime = value;
|
||
// 创建一个新的Date对象,传入指定时间
|
||
const specifiedDate = new Date(specifiedTime);
|
||
//将指定时间转换为UTC+0时间
|
||
const utcTime = specifiedDate.toISOString();
|
||
|
||
return utcTime;
|
||
}
|
||
|
||
export function transformPlayUrls(playUrl: any[]) {
|
||
const playDATA: any[] = [];
|
||
const userToken = encodeURIComponent(getToken());
|
||
|
||
playUrl.forEach((item) => {
|
||
playDATA.push({
|
||
name: item.name,
|
||
definition: item.name,
|
||
type: item.type,
|
||
url: item.url + '&token=' + userToken,
|
||
});
|
||
});
|
||
|
||
return playDATA;
|
||
}
|
||
|
||
export function returnAppUrl(url: string): string {
|
||
if (url.substring(0, 7) === 'base64:') {
|
||
const data: any = JSON.parse(atob(url.replace('base64:', '')));
|
||
return data.url as string;
|
||
}
|
||
return url;
|
||
}
|
||
|
||
// 验证是否为blob格式
|
||
export function blobValidate(data: any) {
|
||
return data.type !== 'application/json';
|
||
}
|
||
|
||
/**
|
||
* 从视频文件提取第一帧作为封面
|
||
* @param file 视频文件
|
||
* @param time 截取的时间点(秒),默认0.1秒
|
||
* @param quality 封面质量(0-1),默认0.8
|
||
* @returns Promise<string> base64格式的图片
|
||
*/
|
||
export const extractVideoThumbnail = (
|
||
file: File,
|
||
time: number = 0.1,
|
||
quality: number = 0.8
|
||
): Promise<string> => {
|
||
return new Promise((resolve, reject) => {
|
||
// 验证文件类型
|
||
if (!file.type.startsWith('video/')) {
|
||
reject(new Error('文件不是视频格式'));
|
||
return;
|
||
}
|
||
|
||
// 创建视频元素
|
||
const video = document.createElement('video');
|
||
const canvas = document.createElement('canvas');
|
||
const context = canvas.getContext('2d');
|
||
|
||
if (!context) {
|
||
reject(new Error('无法创建canvas上下文'));
|
||
return;
|
||
}
|
||
|
||
// 创建对象URL
|
||
const url = URL.createObjectURL(file);
|
||
|
||
// 配置视频
|
||
video.src = url;
|
||
video.muted = true; // 静音以避免自动播放限制
|
||
video.crossOrigin = 'anonymous';
|
||
video.preload = 'metadata';
|
||
|
||
// 事件监听器
|
||
const handleError = (error?: Error) => {
|
||
cleanup();
|
||
reject(error || new Error('无法读取视频文件'));
|
||
};
|
||
|
||
const handleSuccess = () => {
|
||
cleanup();
|
||
};
|
||
|
||
const cleanup = () => {
|
||
video.removeEventListener('loadeddata', onLoaded);
|
||
video.removeEventListener('seeked', onSeeked);
|
||
video.removeEventListener('error', onError);
|
||
URL.revokeObjectURL(url);
|
||
};
|
||
|
||
const onError = () => {
|
||
handleError(new Error('视频加载失败'));
|
||
};
|
||
|
||
const onLoaded = () => {
|
||
// 检查视频尺寸
|
||
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
||
handleError(new Error('视频尺寸无效'));
|
||
return;
|
||
}
|
||
|
||
// 设置canvas尺寸
|
||
// 限制最大尺寸,避免生成过大的图片
|
||
const maxWidth = 800;
|
||
const maxHeight = 450;
|
||
|
||
let width = video.videoWidth;
|
||
let height = video.videoHeight;
|
||
|
||
// 按比例缩放
|
||
if (width > maxWidth || height > maxHeight) {
|
||
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
||
width = Math.floor(width * ratio);
|
||
height = Math.floor(height * ratio);
|
||
}
|
||
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
|
||
// 尝试截取指定时间点的帧
|
||
video.currentTime = Math.min(time, video.duration || 1);
|
||
};
|
||
|
||
const onSeeked = () => {
|
||
try {
|
||
// 绘制视频帧到canvas
|
||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||
|
||
// 将canvas转为base64
|
||
const thumbnail = canvas.toDataURL('image/jpeg', quality);
|
||
|
||
// 检查生成的图片是否有效
|
||
if (!thumbnail || thumbnail.length < 100) {
|
||
handleError(new Error('生成封面失败'));
|
||
return;
|
||
}
|
||
|
||
resolve(thumbnail);
|
||
handleSuccess();
|
||
} catch (error) {
|
||
handleError(error instanceof Error ? error : new Error('生成封面失败'));
|
||
}
|
||
};
|
||
|
||
// 绑定事件
|
||
video.addEventListener('loadeddata', onLoaded);
|
||
video.addEventListener('seeked', onSeeked);
|
||
video.addEventListener('error', onError);
|
||
|
||
// 设置超时处理
|
||
const timeout = setTimeout(() => {
|
||
handleError(new Error('生成封面超时'));
|
||
}, 10000); // 10秒超时
|
||
|
||
// 清理超时
|
||
const originalResolve = resolve;
|
||
const originalReject = reject;
|
||
|
||
resolve = ((...args) => {
|
||
clearTimeout(timeout);
|
||
originalResolve(...args);
|
||
}) as typeof resolve;
|
||
|
||
reject = ((...args) => {
|
||
clearTimeout(timeout);
|
||
originalReject(...args);
|
||
}) as typeof reject;
|
||
|
||
// 开始加载视频
|
||
video.load();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 生成默认的视频封面(当提取失败时使用)
|
||
* @param width 封面宽度
|
||
* @param height 封面高度
|
||
* @param text 封面文字
|
||
* @returns base64格式的SVG图片
|
||
*/
|
||
export const generateDefaultVideoPoster = (
|
||
width: number = 800,
|
||
height: number = 450,
|
||
text: string = '视频预览'
|
||
): string => {
|
||
const colors = [
|
||
{ bg: '#1890ff', text: '#ffffff' }, // 蓝色
|
||
{ bg: '#52c41a', text: '#ffffff' }, // 绿色
|
||
{ bg: '#fa8c16', text: '#ffffff' }, // 橙色
|
||
{ bg: '#f5222d', text: '#ffffff' }, // 红色
|
||
{ bg: '#722ed1', text: '#ffffff' }, // 紫色
|
||
];
|
||
|
||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||
|
||
const svg = `
|
||
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||
<rect width="100%" height="100%" fill="${color.bg}"/>
|
||
<path d="M${width / 2 - 40} ${height / 2 - 40} L${width / 2 + 40} ${height / 2} L${width / 2 - 40} ${height / 2 + 40} Z"
|
||
fill="${color.text}" stroke="${color.text}" stroke-width="2"/>
|
||
<text x="${width / 2}" y="${height / 2 + 80}"
|
||
text-anchor="middle"
|
||
font-family="Arial, sans-serif"
|
||
font-size="18"
|
||
font-weight="bold"
|
||
fill="${color.text}">
|
||
${text}
|
||
</text>
|
||
<text x="${width / 2}" y="${height - 20}"
|
||
text-anchor="middle"
|
||
font-family="Arial, sans-serif"
|
||
font-size="12"
|
||
fill="rgba(255,255,255,0.8)">
|
||
点击播放
|
||
</text>
|
||
</svg>
|
||
`;
|
||
|
||
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
||
};
|
||
|
||
/**
|
||
* base64转Blob对象
|
||
*/
|
||
export const dataURLtoBlob = (dataurl: string): Blob => {
|
||
const arr = dataurl.split(',');
|
||
const mimeMatch = arr[0].match(/:(.*?);/);
|
||
const mime = mimeMatch ? mimeMatch[1] : 'image/jpeg';
|
||
const bstr = atob(arr[1]);
|
||
let n = bstr.length;
|
||
const u8arr = new Uint8Array(n);
|
||
|
||
while (n--) {
|
||
u8arr[n] = bstr.charCodeAt(n);
|
||
}
|
||
|
||
return new Blob([u8arr], { type: mime });
|
||
};
|
||
|
||
/**
|
||
* 获取文件扩展名
|
||
*/
|
||
export const getFileExtension = (filename: string): string => {
|
||
return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2);
|
||
};
|