diff --git a/frontend/src/adapters/gateways/EChartsOptionBuilder/wordcloudOptionBuilder.ts b/frontend/src/adapters/gateways/EChartsOptionBuilder/wordcloudOptionBuilder.ts index 34d0b39..4524b23 100644 --- a/frontend/src/adapters/gateways/EChartsOptionBuilder/wordcloudOptionBuilder.ts +++ b/frontend/src/adapters/gateways/EChartsOptionBuilder/wordcloudOptionBuilder.ts @@ -224,8 +224,25 @@ export function buildWordcloudOption( }; }); - // Build mask image for shape - const maskImage = getMask(wc.shape); + // Build mask: custom image or canvas shape + let maskImage: HTMLCanvasElement | HTMLImageElement | null = null; + + if (wc.shape === 'custom' && wc.customImageUrl && typeof window !== 'undefined') { + // Use custom uploaded image as mask + const cacheKey = 'custom-' + wc.customImageUrl.substring(0, 50); + const cached = maskCache.get(cacheKey); + if (cached) { + maskImage = cached as any; + } else { + const img = new Image(); + img.src = wc.customImageUrl; + maskImage = img; + // Cache after load + img.onload = () => { maskCache.set(cacheKey, img as any); }; + } + } else if (wc.shape !== 'rectangle') { + maskImage = getMask(wc.shape); + } const series: Record = { type: 'wordCloud', @@ -240,7 +257,7 @@ export function buildWordcloudOption( shrinkToFit: true, drawOutOfBound: false, layoutAnimation: true, - keepAspect: false, + keepAspect: wc.shape === 'custom', textStyle: { fontFamily: wc.fontFamily, fontWeight: wc.fontWeight, @@ -255,12 +272,8 @@ export function buildWordcloudOption( data: wordData, }; - // Apply mask for non-default shapes if (maskImage) { series.maskImage = maskImage; - series.shape = 'circle'; // base shape, mask overrides it - } else { - series.shape = 'circle'; } option.series = [series]; diff --git a/frontend/src/frameworks/components/configPanel/WordcloudConfig.tsx b/frontend/src/frameworks/components/configPanel/WordcloudConfig.tsx index 203d5c4..cfb93d7 100644 --- a/frontend/src/frameworks/components/configPanel/WordcloudConfig.tsx +++ b/frontend/src/frameworks/components/configPanel/WordcloudConfig.tsx @@ -1,7 +1,8 @@ 'use client'; -import React, { useCallback } from 'react'; -import { Select, Slider, InputNumber, Space, Typography, Radio } from 'antd'; +import React, { useCallback, useRef } from 'react'; +import { Select, Slider, InputNumber, Space, Typography, Radio, Button, Upload } from 'antd'; +import { UploadOutlined, DeleteOutlined } from '@ant-design/icons'; import { useChartConfig } from '@/frameworks/hooks/useChartConfig'; const { Text } = Typography; @@ -14,6 +15,7 @@ const SHAPES = [ { label: '三角形', value: 'triangle' }, { label: '五角星', value: 'star' }, { label: '五边形', value: 'pentagon' }, + { label: '自定义图片', value: 'custom' }, ]; const FONTS = [ @@ -63,13 +65,56 @@ export default function WordcloudConfig() { 形状