From 54a6efb6f421887c3042969c2e41032154220d79 Mon Sep 17 00:00:00 2001 From: rd <1344903914@qq.com> Date: Thu, 31 Jul 2025 10:54:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=81=E8=A3=85=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E4=B8=BB=E8=89=B2=E8=B0=83=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/directives/getImageMainColor.ts | 26 +++++ src/directives/index.ts | 3 + src/main.ts | 8 +- src/utils/tools.ts | 172 +++++++++++++++++++++++++++- src/views/agent/index/index.vue | 9 +- 5 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 src/directives/getImageMainColor.ts diff --git a/src/directives/getImageMainColor.ts b/src/directives/getImageMainColor.ts new file mode 100644 index 0000000..246539f --- /dev/null +++ b/src/directives/getImageMainColor.ts @@ -0,0 +1,26 @@ +import { getImageMainColor } from '@/utils/tools'; + +// 创建图片主色调指令 +const imageMainColorDirective = { + mounted(el: HTMLElement, binding: any) { + const imageUrl = binding.value; + + if (!imageUrl) return; + + getImageMainColor(imageUrl) + .then(color => { + el.style.backgroundColor = color; + }) + .catch(error => { + console.error('获取图片主色调失败:', error); + // 设置默认背景色 + el.style.backgroundColor = '#E6E6E8'; + }); + } +}; + +export default { + install(app: any) { + app.directive('image-main-color', imageMainColorDirective); + } +}; diff --git a/src/directives/index.ts b/src/directives/index.ts index e69de29..484857d 100644 --- a/src/directives/index.ts +++ b/src/directives/index.ts @@ -0,0 +1,3 @@ +import getImageMainColor from './getImageMainColor'; + +export { getImageMainColor }; diff --git a/src/main.ts b/src/main.ts index bab185a..fff0b17 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,9 +5,10 @@ import App from './App.vue'; import router from './router'; import store from './stores'; +import * as directives from '@/directives'; import NoData from '@/components/no-data'; -import SvgIcon from "@/components/svg-icon"; +import SvgIcon from '@/components/svg-icon'; import '@/api/index'; import '@arco-design/web-vue/dist/arco.css'; // Arco 默认样式 @@ -15,7 +16,7 @@ import './core'; import 'normalize.css'; import 'uno.css'; -import 'virtual:svg-icons-register' +import 'virtual:svg-icons-register'; // import '@/styles/vars.css'; // 优先加载 @@ -26,4 +27,7 @@ app.component('SvgIcon', SvgIcon); app.use(store); app.use(router); + +Object.keys(directives).forEach((k) => app.use(directives[k])); // 注册指令 + app.mount('#app'); diff --git a/src/utils/tools.ts b/src/utils/tools.ts index 7fa94db..c0514fb 100644 --- a/src/utils/tools.ts +++ b/src/utils/tools.ts @@ -109,4 +109,174 @@ export function downloadByUrl(url: string, filename?: string) { export function genRandomId() { return `id_${Date.now()}_${Math.floor(Math.random() * 10000)}`; -} \ No newline at end of file +} + +export function getImageMainColor(imageUrl: string): Promise { + 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) => a[sortChannel] - b[sortChannel]); + + // 切分中位数 + 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; +} diff --git a/src/views/agent/index/index.vue b/src/views/agent/index/index.vue index 8333b84..c67ceae 100644 --- a/src/views/agent/index/index.vue +++ b/src/views/agent/index/index.vue @@ -1,5 +1,5 @@