Files
lingji-work-fe/src/utils/tools.ts
rd 9dea979afc Merge remote-tracking branch 'origin/main' into feature/v1.2灵机空间-内容上传审核_rxd
# Conflicts:
#	pnpm-lock.yaml
#	src/components/_base/navbar/index.vue
#	src/components/text-over-tips/index.vue
#	src/layouts/Basic.vue
#	src/layouts/Page.vue
#	src/main.ts
#	src/router/constants.ts
#	src/router/index.ts
#	src/router/typeings.d.ts
#	src/utils/tools.ts
2025-08-14 15:06:46 +08:00

519 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* @Author: RenXiaoDong
* @Date: 2025-06-27 17:36:31
*/
import dayjs from 'dayjs';
export function toFixed(num: number | string, n: number): number {
return parseFloat(parseFloat(num.toString()).toFixed(n));
}
export function isNotData(n: number): boolean {
if (n === undefined) {
return true;
}
return n === -2147483648;
}
export function splitNumber(num: number): string | number {
if (!num) {
return num;
}
const parts = num.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
}
export function formatNumberShow(...args: any[]): string | number {
const [_args] = args;
const { value, len = 2, split = true, showExactValue = false } = typeof _args === 'object' ? _args : { value: _args };
const getNumber = (value: number) => {
return split ? splitNumber(value) : value;
};
if (isNotData(value)) {
return '-';
}
if (value < 0) {
return `-${formatNumberShow({
value: -value,
len,
split,
showExactValue,
})}`;
}
if (showExactValue) {
return getNumber(toFixed(value, len));
}
if (value < 10000) {
return getNumber(toFixed(value, len));
} else if (value < 100000000) {
const _n = Math.round((value / 10000) * 100) / 100;
return split ? `${splitNumber(_n)}w` : `${toFixed(_n, len)}w`;
} else {
const _n = Math.round((value / 100000000) * 100) / 100;
return split ? `${splitNumber(_n)}亿` : `${toFixed(_n, len)}亿`;
}
}
export function formatTableField(fieldItem: any, rowValue: any, showExactValue = false) {
// 获取嵌套属性值的函数
const getNestedValue = (obj: any, path: string) => {
if (!obj || !path) return undefined;
// 如果路径包含点号,说明是链式取值
if (path.includes('.')) {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
}
// 普通属性取值
return obj[path];
};
const _getValue = (value: any) => {
if (!isNumber(value)) return value || '-';
return formatNumberShow({ value, showExactValue });
};
// 使用链式取值获取数据
const rawValue = getNestedValue(rowValue, fieldItem.dataIndex);
const value = _getValue(rawValue ?? '-');
return `${fieldItem.prefix || ''}${value}${fieldItem.suffix || ''}`;
}
export function exactFormatTime(val: number, curYearFmt = 'MM-DD HH:mm:ss', otherYearFmt = 'YYYY-MM-DD HH:mm:ss') {
if (!val) return '-';
const year = dayjs(val * 1000).year();
const currYear = dayjs().year();
const diff = year - currYear;
const fmt = diff === 0 ? curYearFmt : otherYearFmt;
return dayjs(val * 1000).format(fmt);
}
// 导出文件
export function downloadByUrl(url: string, filename?: string) {
const a = document.createElement('a');
a.href = url;
if (filename) {
a.download = filename;
}
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
export function genRandomId() {
return `id_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
}
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 修改函数以同时获取视频时长和首帧
export function getVideoInfo(file: File): Promise<{ duration: number; firstFrame: string }> {
return new Promise((resolve) => {
const video = document.createElement('video');
video.preload = 'metadata';
video.crossOrigin = 'anonymous'; // 避免跨域问题
video.muted = true; // 静音,避免意外播放声音
video.style.position = 'fixed'; // 确保视频元素在DOM中
video.style.top = '-1000px'; // 但不可见
document.body.appendChild(video); // 添加到DOM
let hasResolved = false;
// 先获取元数据(时长)
video.onloadedmetadata = function () {
// 视频时长
const duration = video.duration;
// 尝试将视频定位到非常小的时间点,确保有帧可捕获
if (duration > 0) {
video.currentTime = Math.min(0.1, duration / 2);
}
};
// 当视频定位完成后尝试捕获首帧
video.onseeked = function () {
if (hasResolved) return;
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}
// 清理
window.URL.revokeObjectURL(video.src);
document.body.removeChild(video);
// 返回结果
hasResolved = true;
resolve({
duration: video.duration,
firstFrame: canvas.toDataURL('image/jpeg', 0.9), // 提高质量
});
};
// 作为备选方案监听loadeddata事件
video.onloadeddata = function () {
if (hasResolved) return;
// 尝试捕获帧
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}
// 检查是否捕获到有效帧(非全黑)
const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height);
if (imageData) {
let isAllBlack = true;
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i] > 10 || imageData.data[i + 1] > 10 || imageData.data[i + 2] > 10) {
isAllBlack = false;
break;
}
}
if (!isAllBlack) {
// 清理
window.URL.revokeObjectURL(video.src);
document.body.removeChild(video);
// 返回结果
hasResolved = true;
resolve({
duration: video.duration,
firstFrame: canvas.toDataURL('image/jpeg', 0.9),
});
return;
}
}
// 如果是全黑帧尝试定位到0.1秒
if (video.duration > 0) {
video.currentTime = 0.1;
}
};
// 设置视频源以触发加载
video.src = URL.createObjectURL(file);
// 设置超时,防止长时间无响应
setTimeout(() => {
if (!hasResolved) {
document.body.removeChild(video);
resolve({
duration: 0,
firstFrame: '',
});
}
}, 5000); // 5秒超时
});
}
export const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const remainingSecondsAfterHours = seconds % 3600;
const minutes = Math.floor(remainingSecondsAfterHours / 60);
const remainingSeconds = Math.floor(remainingSecondsAfterHours % 60);
if (hours > 0) {
if (remainingSecondsAfterHours === 0) {
return `${hours}小时`;
}
if (remainingSeconds === 0) {
return `${hours}小时${minutes}`;
}
return `${hours}小时${minutes}${remainingSeconds}`;
} else if (minutes > 0) {
if (remainingSeconds === 0) {
return `${minutes}`;
}
return `${minutes}${remainingSeconds}`;
} else {
return `${remainingSeconds}`;
}
};
export const formatUploadSpeed = (bytesPerSecond: number): string => {
if (bytesPerSecond < 1024) {
return `${bytesPerSecond.toFixed(2)} B/s`;
} else if (bytesPerSecond < 1024 * 1024) {
return `${(bytesPerSecond / 1024).toFixed(2)} KB/s`;
} else {
return `${(bytesPerSecond / (1024 * 1024)).toFixed(2)} MB/s`;
}
};
export function convertVideoUrlToCoverUrl(videoUrl: string): string {
if (!videoUrl || typeof videoUrl !== 'string') {
console.error('Invalid video URL');
return '';
}
const urlWithCovers = videoUrl.replace('/videos/', '/covers/');
const lastDotIndex = urlWithCovers.lastIndexOf('.');
if (lastDotIndex !== -1) {
return urlWithCovers.substring(0, lastDotIndex) + '.jpg';
}
return urlWithCovers + '.jpg';
}
/**
* 生成包含协议、域名和参数的完整URL
*/
export const generateFullUrl = (pathTemplate: string, params: Record<string, string | number> = {}): string => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
const port = window.location.port ? `:${window.location.port}` : '';
const baseUrl = `${protocol}//${hostname}${port}`;
let path = pathTemplate;
Object.entries(params).forEach(([key, value]) => {
path = path.replace(`:${key}`, String(value));
});
return `${baseUrl}${path}`;
};
/** 图片朝向类型 */
export type ImageOrientation = 'landscape' | 'portrait' | 'square';
/**
* 根据宽高判断图片是横图/竖图/正方形
*/
export const getImageOrientationBySize = (width: number, height: number): ImageOrientation => {
if (!width || !height) return 'square';
if (width > height) return 'landscape';
if (height > width) return 'portrait';
return 'square';
};
/**
* 加载图片自然尺寸(跨域下需服务端允许匿名访问)
*/
export const getImageNaturalSize = (url: string): Promise<{ width: number; height: number }> => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
resolve({ width: img.naturalWidth, height: img.naturalHeight });
};
img.onerror = () => {
resolve({ width: 0, height: 0 });
};
// 若是跨域资源并允许匿名访问,可尝试设置
try {
img.crossOrigin = 'anonymous';
} catch (_) {}
img.src = url;
});
};
/**
* 基于图片 URL 判断图片横竖
*/
export const getImageOrientationByUrl = async (url: string): Promise<ImageOrientation> => {
const { width, height } = await getImageNaturalSize(url);
return getImageOrientationBySize(width, height);
};
export function getImageMainColor(imageUrl: string): Promise<string> {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous'; // 处理跨域图片
img.src = imageUrl;
img.onload = () => {
// 创建画布缩小图片尺寸以提高性能
const canvas = document.createElement('canvas');
const maxDimension = 100; // 最大尺寸为100px
let width = img.width;
let height = img.height;
// 按比例缩小图片
if (width > height) {
if (width > maxDimension) {
height *= maxDimension / width;
width = maxDimension;
}
} else {
if (height > maxDimension) {
width *= maxDimension / height;
height = maxDimension;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Canvas context not available'));
return;
}
// 绘制缩小后的图片
ctx.drawImage(img, 0, 0, width, height);
// 获取图片数据
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// 使用中位数切分法提取主色调
const colorGroups = medianCut(data, 8); // 分成8组
// 找出最大的颜色组
let maxGroup = colorGroups[0];
for (let i = 1; i < colorGroups.length; i++) {
if (colorGroups[i].count > maxGroup.count) {
maxGroup = colorGroups[i];
}
}
// 计算组内平均颜色
const avgColor = {
r: Math.round(maxGroup.sumR / maxGroup.count),
g: Math.round(maxGroup.sumG / maxGroup.count),
b: Math.round(maxGroup.sumB / maxGroup.count)
};
resolve(`rgb(${avgColor.r},${avgColor.g},${avgColor.b})`);
};
img.onerror = () => {
reject(new Error('Failed to load image'));
};
});
}
/**
* 中位数切分法进行色彩量化
* @param data 图像数据
* @param levels 切分级别
* @returns 颜色组
*/
function medianCut(data: Uint8ClampedArray, levels: number): any[] {
const colors = [];
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
// 跳过透明像素
if (a < 128) continue;
colors.push({
r, g, b,
count: 1,
sumR: r, sumG: g, sumB: b
});
}
// 如果没有颜色数据,返回默认白色
if (colors.length === 0) {
return [{
count: 1,
sumR: 255, sumG: 255, sumB: 255
}];
}
// 开始中位数切分
let colorGroups = [{
colors,
count: colors.length,
sumR: colors.reduce((sum, c) => sum + c.r, 0),
sumG: colors.reduce((sum, c) => sum + c.g, 0),
sumB: colors.reduce((sum, c) => sum + c.b, 0)
}];
for (let i = 0; i < levels; i++) {
const newGroups = [];
for (const group of colorGroups) {
if (group.colors.length <= 1) {
newGroups.push(group);
continue;
}
// 找出颜色范围最大的通道
const rMin = Math.min(...group.colors.map(c => c.r));
const rMax = Math.max(...group.colors.map(c => c.r));
const gMin = Math.min(...group.colors.map(c => c.g));
const gMax = Math.max(...group.colors.map(c => c.g));
const bMin = Math.min(...group.colors.map(c => c.b));
const bMax = Math.max(...group.colors.map(c => c.b));
const rRange = rMax - rMin;
const gRange = gMax - gMin;
const bRange = bMax - bMin;
let sortChannel = 'r';
if (gRange > rRange && gRange > bRange) {
sortChannel = 'g';
} else if (bRange > rRange && bRange > gRange) {
sortChannel = 'b';
}
// 按最大范围通道排序
group.colors.sort((a, b) => {
if (sortChannel === 'r') {
return a.r - b.r;
} else if (sortChannel === 'g') {
return a.g - b.g;
} else {
return a.b - b.b;
}
});
// 切分中位数
const mid = Math.floor(group.colors.length / 2);
const group1 = group.colors.slice(0, mid);
const group2 = group.colors.slice(mid);
// 添加新组
newGroups.push({
colors: group1,
count: group1.length,
sumR: group1.reduce((sum, c) => sum + c.r, 0),
sumG: group1.reduce((sum, c) => sum + c.g, 0),
sumB: group1.reduce((sum, c) => sum + c.b, 0)
});
newGroups.push({
colors: group2,
count: group2.length,
sumR: group2.reduce((sum, c) => sum + c.r, 0),
sumG: group2.reduce((sum, c) => sum + c.g, 0),
sumB: group2.reduce((sum, c) => sum + c.b, 0)
});
}
colorGroups = newGroups;
}
return colorGroups;
}