2025-06-27 18:37:42 +08:00
|
|
|
|
/*
|
|
|
|
|
|
* @Author: RenXiaoDong
|
|
|
|
|
|
* @Date: 2025-06-27 17:36:31
|
|
|
|
|
|
*/
|
2025-06-28 15:28:54 +08:00
|
|
|
|
import dayjs from 'dayjs';
|
2025-07-16 10:14:04 +08:00
|
|
|
|
|
2025-06-27 18:37:42 +08:00
|
|
|
|
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)}亿`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-28 11:31:49 +08:00
|
|
|
|
|
|
|
|
|
|
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) => {
|
2025-07-02 17:55:20 +08:00
|
|
|
|
if (!isNumber(value)) return value || '-';
|
2025-06-28 11:31:49 +08:00
|
|
|
|
return formatNumberShow({ value, showExactValue });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 使用链式取值获取数据
|
|
|
|
|
|
const rawValue = getNestedValue(rowValue, fieldItem.dataIndex);
|
|
|
|
|
|
const value = _getValue(rawValue ?? '-');
|
|
|
|
|
|
|
|
|
|
|
|
return `${fieldItem.prefix || ''}${value}${fieldItem.suffix || ''}`;
|
|
|
|
|
|
}
|
2025-06-28 15:28:54 +08:00
|
|
|
|
|
2025-07-21 12:01:32 +08:00
|
|
|
|
export function exactFormatTime(val: number, curYearFmt = 'MM-DD HH:mm:ss', otherYearFmt = 'YYYY-MM-DD HH:mm:ss') {
|
2025-06-28 15:28:54 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-07-04 16:05:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 导出文件
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-07-17 17:23:40 +08:00
|
|
|
|
|
2025-07-21 15:10:26 +08:00
|
|
|
|
export function genRandomId() {
|
2025-08-28 14:58:58 +08:00
|
|
|
|
return `id_${dayjs().unix()}_${Math.floor(Math.random() * 10000)}`;
|
2025-07-31 18:18:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function formatFileSize(bytes: number): string {
|
|
|
|
|
|
if (bytes === 0) return '0 Bytes';
|
2025-08-01 11:49:15 +08:00
|
|
|
|
|
2025-07-31 18:18:50 +08:00
|
|
|
|
const k = 1024;
|
2025-08-23 16:14:05 +08:00
|
|
|
|
const sizes = ['Bytes', 'kb', 'mB', 'gB', 'tB'];
|
2025-07-31 18:18:50 +08:00
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
2025-08-01 11:49:15 +08:00
|
|
|
|
|
2025-07-31 18:18:50 +08:00
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 修改函数以同时获取视频时长和首帧
|
|
|
|
|
|
export function getVideoInfo(file: File): Promise<{ duration: number; firstFrame: string }> {
|
2025-07-31 18:18:50 +08:00
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
const video = document.createElement('video');
|
|
|
|
|
|
video.preload = 'metadata';
|
2025-08-01 11:49:15 +08:00
|
|
|
|
video.crossOrigin = 'anonymous'; // 避免跨域问题
|
|
|
|
|
|
video.muted = true; // 静音,避免意外播放声音
|
|
|
|
|
|
video.style.position = 'fixed'; // 确保视频元素在DOM中
|
|
|
|
|
|
video.style.top = '-1000px'; // 但不可见
|
|
|
|
|
|
document.body.appendChild(video); // 添加到DOM
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
let hasResolved = false;
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 先获取元数据(时长)
|
|
|
|
|
|
video.onloadedmetadata = function () {
|
|
|
|
|
|
// 视频时长
|
|
|
|
|
|
const duration = video.duration;
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 尝试将视频定位到非常小的时间点,确保有帧可捕获
|
|
|
|
|
|
if (duration > 0) {
|
|
|
|
|
|
video.currentTime = Math.min(0.1, duration / 2);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 当视频定位完成后尝试捕获首帧
|
|
|
|
|
|
video.onseeked = function () {
|
|
|
|
|
|
if (hasResolved) return;
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
|
canvas.width = video.videoWidth;
|
|
|
|
|
|
canvas.height = video.videoHeight;
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
if (ctx) {
|
|
|
|
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
}
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 清理
|
2025-07-31 18:18:50 +08:00
|
|
|
|
window.URL.revokeObjectURL(video.src);
|
2025-08-01 11:49:15 +08:00
|
|
|
|
document.body.removeChild(video);
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 返回结果
|
|
|
|
|
|
hasResolved = true;
|
|
|
|
|
|
resolve({
|
|
|
|
|
|
duration: video.duration,
|
2025-08-07 18:05:27 +08:00
|
|
|
|
firstFrame: canvas.toDataURL('image/jpeg', 0.9), // 提高质量
|
2025-08-01 11:49:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 作为备选方案,监听loadeddata事件
|
|
|
|
|
|
video.onloadeddata = function () {
|
|
|
|
|
|
if (hasResolved) return;
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 尝试捕获帧
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
|
canvas.width = video.videoWidth;
|
|
|
|
|
|
canvas.height = video.videoHeight;
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
if (ctx) {
|
|
|
|
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
}
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 检查是否捕获到有效帧(非全黑)
|
|
|
|
|
|
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) {
|
2025-08-07 18:05:27 +08:00
|
|
|
|
if (imageData.data[i] > 10 || imageData.data[i + 1] > 10 || imageData.data[i + 2] > 10) {
|
2025-08-01 11:49:15 +08:00
|
|
|
|
isAllBlack = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
if (!isAllBlack) {
|
|
|
|
|
|
// 清理
|
|
|
|
|
|
window.URL.revokeObjectURL(video.src);
|
|
|
|
|
|
document.body.removeChild(video);
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 返回结果
|
|
|
|
|
|
hasResolved = true;
|
|
|
|
|
|
resolve({
|
|
|
|
|
|
duration: video.duration,
|
2025-08-07 18:05:27 +08:00
|
|
|
|
firstFrame: canvas.toDataURL('image/jpeg', 0.9),
|
2025-08-01 11:49:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 如果是全黑帧,尝试定位到0.1秒
|
|
|
|
|
|
if (video.duration > 0) {
|
|
|
|
|
|
video.currentTime = 0.1;
|
|
|
|
|
|
}
|
2025-07-31 18:18:50 +08:00
|
|
|
|
};
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 设置视频源以触发加载
|
2025-07-31 18:18:50 +08:00
|
|
|
|
video.src = URL.createObjectURL(file);
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
2025-08-01 11:49:15 +08:00
|
|
|
|
// 设置超时,防止长时间无响应
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (!hasResolved) {
|
|
|
|
|
|
document.body.removeChild(video);
|
|
|
|
|
|
resolve({
|
|
|
|
|
|
duration: 0,
|
2025-08-07 18:05:27 +08:00
|
|
|
|
firstFrame: '',
|
2025-08-01 11:49:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000); // 5秒超时
|
2025-07-31 18:18:50 +08:00
|
|
|
|
});
|
2025-08-01 11:49:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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`;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-08-01 15:03:16 +08:00
|
|
|
|
|
2025-08-07 18:05:27 +08:00
|
|
|
|
export function convertVideoUrlToCoverUrl(videoUrl: string): string {
|
2025-08-01 15:03:16 +08:00
|
|
|
|
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';
|
|
|
|
|
|
}
|
2025-08-07 18:05:27 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成包含协议、域名和参数的完整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}`;
|
|
|
|
|
|
};
|
2025-08-11 22:40:54 +08:00
|
|
|
|
|
|
|
|
|
|
/** 图片朝向类型 */
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
2025-08-14 15:06:46 +08:00
|
|
|
|
|
2025-07-31 10:54:04 +08:00
|
|
|
|
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),
|
2025-08-28 14:58:58 +08:00
|
|
|
|
b: Math.round(maxGroup.sumB / maxGroup.count),
|
2025-07-31 10:54:04 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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({
|
2025-08-28 14:58:58 +08:00
|
|
|
|
r,
|
|
|
|
|
|
g,
|
|
|
|
|
|
b,
|
2025-07-31 10:54:04 +08:00
|
|
|
|
count: 1,
|
2025-08-28 14:58:58 +08:00
|
|
|
|
sumR: r,
|
|
|
|
|
|
sumG: g,
|
|
|
|
|
|
sumB: b,
|
2025-07-31 10:54:04 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有颜色数据,返回默认白色
|
|
|
|
|
|
if (colors.length === 0) {
|
2025-08-28 14:58:58 +08:00
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
count: 1,
|
|
|
|
|
|
sumR: 255,
|
|
|
|
|
|
sumG: 255,
|
|
|
|
|
|
sumB: 255,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
2025-07-31 10:54:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 开始中位数切分
|
2025-08-28 14:58:58 +08:00
|
|
|
|
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),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
2025-07-31 10:54:04 +08:00
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < levels; i++) {
|
|
|
|
|
|
const newGroups = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const group of colorGroups) {
|
|
|
|
|
|
if (group.colors.length <= 1) {
|
|
|
|
|
|
newGroups.push(group);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 找出颜色范围最大的通道
|
2025-08-28 14:58:58 +08:00
|
|
|
|
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));
|
2025-07-31 10:54:04 +08:00
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按最大范围通道排序
|
2025-08-14 15:06:46 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-07-31 10:54:04 +08:00
|
|
|
|
|
|
|
|
|
|
// 切分中位数
|
|
|
|
|
|
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),
|
2025-08-28 14:58:58 +08:00
|
|
|
|
sumB: group1.reduce((sum, c) => sum + c.b, 0),
|
2025-07-31 10:54:04 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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),
|
2025-08-28 14:58:58 +08:00
|
|
|
|
sumB: group2.reduce((sum, c) => sum + c.b, 0),
|
2025-07-31 10:54:04 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
colorGroups = newGroups;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return colorGroups;
|
2025-08-28 14:58:58 +08:00
|
|
|
|
}
|