|
@@ -12,32 +12,41 @@
|
|
|
|
|
|
<script setup lang="ts" name="keywordFrequency">
|
|
|
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
|
|
+import { getKeywordData } from '/@/api/marketing/data';
|
|
|
+
|
|
|
import * as echarts from 'echarts';
|
|
|
+const emit = defineEmits(['success']);
|
|
|
|
|
|
-const chartData = ref([
|
|
|
- { name: '下载', value: 30, color: '#ff6384' },
|
|
|
- { name: '宅六', value: 10, color: '#4bc0c0' },
|
|
|
- { name: '张三', value: 20, color: '#36a2eb' },
|
|
|
- { name: '里斯', value: 25, color: '#cc65fe' },
|
|
|
- { name: '王五', value: 15, color: '#ffce56' },
|
|
|
-]);
|
|
|
+interface KeywordDataItem {
|
|
|
+ keyword: string;
|
|
|
+ count: number;
|
|
|
+}
|
|
|
|
|
|
- const value = ref('7');
|
|
|
- const options = [
|
|
|
- {
|
|
|
- value: '7',
|
|
|
- label: '7天',
|
|
|
- selected: true,
|
|
|
- },
|
|
|
- {
|
|
|
- value: '30',
|
|
|
- label: '30天',
|
|
|
- },
|
|
|
- ];
|
|
|
+const chartData = ref<KeywordDataItem[]>([
|
|
|
+
|
|
|
+]);
|
|
|
+const prop = defineProps({
|
|
|
+ flushed: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+const value = ref(7);
|
|
|
+const options = [
|
|
|
+ {
|
|
|
+ value: 7,
|
|
|
+ label: '7天',
|
|
|
+ selected: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 30,
|
|
|
+ label: '30天',
|
|
|
+ },
|
|
|
+];
|
|
|
|
|
|
const chartRef = ref(null);
|
|
|
let chartInstance: echarts.ECharts | null = null;
|
|
|
-console.log(chartInstance);
|
|
|
|
|
|
// 初始化图表
|
|
|
const initChart = () => {
|
|
@@ -54,74 +63,94 @@ const initChart = () => {
|
|
|
|
|
|
// 处理数据
|
|
|
const colorPalette = [
|
|
|
- '#ff6384', '#4bc0c0', '#36a2eb', '#cc65fe', '#ffce56',
|
|
|
- '#f87171', '#fbbf24', '#34d399', '#60a5fa', '#a78bfa',
|
|
|
- '#f472b6', '#facc15', '#2dd4bf', '#818cf8', '#f59e42',
|
|
|
- '#eab308', '#10b981', '#3b82f6', '#6366f1', '#f43f5e',
|
|
|
- '#a3e635', '#fcd34d', '#fca5a5', '#c084fc', '#f9fafb'
|
|
|
-];
|
|
|
-
|
|
|
-const realData = chartData.value;
|
|
|
-const totalBubbles = 25;
|
|
|
-const scatterData: any[] = [];
|
|
|
-const minRadius = 10;
|
|
|
-const maxRadius = 18;
|
|
|
-const padding = 2; // 气泡之间的最小间距
|
|
|
-const maxTry = 1000;
|
|
|
-
|
|
|
-function isOverlap(x: number, y: number, r: number, arr: any[]) {
|
|
|
- for (const b of arr) {
|
|
|
- const dx = x - b.value[0];
|
|
|
- const dy = y - b.value[1];
|
|
|
- const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
- if (dist < r + b.value[2] + padding) return true;
|
|
|
- }
|
|
|
- return false;
|
|
|
-}
|
|
|
+ '#ff6384',
|
|
|
+ '#4bc0c0',
|
|
|
+ '#36a2eb',
|
|
|
+ '#cc65fe',
|
|
|
+ '#ffce56',
|
|
|
+ '#f87171',
|
|
|
+ '#fbbf24',
|
|
|
+ '#34d399',
|
|
|
+ '#60a5fa',
|
|
|
+ '#a78bfa',
|
|
|
+ '#f472b6',
|
|
|
+ '#facc15',
|
|
|
+ '#2dd4bf',
|
|
|
+ '#818cf8',
|
|
|
+ '#f59e42',
|
|
|
+ '#eab308',
|
|
|
+ '#10b981',
|
|
|
+ '#3b82f6',
|
|
|
+ '#6366f1',
|
|
|
+ '#f43f5e',
|
|
|
+ '#a3e635',
|
|
|
+ '#fcd34d',
|
|
|
+ '#fca5a5',
|
|
|
+ '#c084fc',
|
|
|
+ '#f9fafb',
|
|
|
+ ];
|
|
|
|
|
|
-function placeBubble(radius: number, arr: any[]): [number, number] {
|
|
|
- let tryCount = 0;
|
|
|
- while (tryCount < maxTry) {
|
|
|
- const x = Math.random() * (100 - 2 * radius) + radius;
|
|
|
- const y = Math.random() * (100 - 2 * radius) + radius;
|
|
|
- if (!isOverlap(x, y, radius, arr)) return [x, y];
|
|
|
- tryCount++;
|
|
|
- }
|
|
|
- // fallback: 网格法兜底
|
|
|
- return [Math.random() * 100, Math.random() * 100];
|
|
|
-}
|
|
|
+ const realData = chartData.value;
|
|
|
+ const totalBubbles = 10;
|
|
|
+ const scatterData: any[] = [];
|
|
|
+ const minRadius = 10;
|
|
|
+ const maxRadius = 18;
|
|
|
+ const padding = 2; // 气泡之间的最小间距
|
|
|
+ const maxTry = 1000;
|
|
|
+
|
|
|
+ function isOverlap(x: number, y: number, r: number, arr: any[]) {
|
|
|
+ for (const b of arr) {
|
|
|
+ const dx = x - b.value[0];
|
|
|
+ const dy = y - b.value[1];
|
|
|
+ const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
+ if (dist < r + b.value[2] + padding) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
|
|
|
-// 先放真实数据
|
|
|
-realData.forEach((item, i) => {
|
|
|
- const radius = maxRadius;
|
|
|
- const [x, y] = placeBubble(radius, scatterData);
|
|
|
- scatterData.push({
|
|
|
- value: [x, y, radius],
|
|
|
- itemStyle: { color: colorPalette[i % colorPalette.length] },
|
|
|
- name: `${item.name}\n${item.value}`,
|
|
|
- });
|
|
|
-});
|
|
|
+ function placeBubble(radius: number, arr: any[]): [number, number] {
|
|
|
+ let tryCount = 0;
|
|
|
+ while (tryCount < maxTry) {
|
|
|
+ const x = Math.random() * (100 - 2 * radius) + radius;
|
|
|
+ const y = Math.random() * (100 - 2 * radius) + radius;
|
|
|
+ if (!isOverlap(x, y, radius, arr)) return [x, y];
|
|
|
+ tryCount++;
|
|
|
+ }
|
|
|
+ // fallback: 网格法兜底
|
|
|
+ return [Math.random() * 100, Math.random() * 100];
|
|
|
+ }
|
|
|
|
|
|
-// 填充空白气泡
|
|
|
-for (let j = realData.length; j < totalBubbles; j++) {
|
|
|
- const radius = minRadius + Math.random() * (maxRadius - minRadius) * 0.5;
|
|
|
- const [x, y] = placeBubble(radius, scatterData);
|
|
|
- scatterData.push({
|
|
|
- value: [x, y, radius],
|
|
|
- itemStyle: { color: colorPalette[j % colorPalette.length] },
|
|
|
- name: '',
|
|
|
- label: { show: false }
|
|
|
- });
|
|
|
-}
|
|
|
+ // 先放真实数据
|
|
|
+ realData.forEach((item, i) => {
|
|
|
+ const radius = maxRadius;
|
|
|
+ const [x, y] = placeBubble(radius, scatterData);
|
|
|
+ scatterData.push({
|
|
|
+ value: [x, y, radius],
|
|
|
+ itemStyle: { color: colorPalette[i % colorPalette.length] },
|
|
|
+ name: `${item.keyword}\n${item.count}`,
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 填充空白气泡
|
|
|
+ for (let j = realData.length; j < totalBubbles; j++) {
|
|
|
+ const radius = minRadius + Math.random() * (maxRadius - minRadius) * 0.5;
|
|
|
+ const [x, y] = placeBubble(radius, scatterData);
|
|
|
+ scatterData.push({
|
|
|
+ value: [x, y, radius],
|
|
|
+ itemStyle: { color: colorPalette[j % colorPalette.length] },
|
|
|
+ name: '',
|
|
|
+ label: { show: false },
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
// 配置项
|
|
|
const option = {
|
|
|
tooltip: {
|
|
|
trigger: 'item',
|
|
|
- formatter: '{b}',
|
|
|
+ formatter: '{b}次',
|
|
|
},
|
|
|
grid: {
|
|
|
- top: '40%',
|
|
|
+ top: '40%',
|
|
|
},
|
|
|
xAxis: { show: false },
|
|
|
yAxis: { show: false },
|
|
@@ -130,7 +159,7 @@ for (let j = realData.length; j < totalBubbles; j++) {
|
|
|
type: 'scatter',
|
|
|
data: scatterData,
|
|
|
symbolSize: (value: any) => {
|
|
|
- return value[2] * 3; // 根据值计算气泡大小
|
|
|
+ return value[2] * 5; // 根据值计算气泡大小
|
|
|
},
|
|
|
label: {
|
|
|
show: true,
|
|
@@ -159,10 +188,22 @@ const handleResize = () => {
|
|
|
chartInstance.resize();
|
|
|
}
|
|
|
};
|
|
|
+const getData = async () => {
|
|
|
+ const res = await getKeywordData({ days: value.value });
|
|
|
+ chartData.value = res.data;
|
|
|
+ initChart();
|
|
|
+ emit('success');
|
|
|
+};
|
|
|
|
|
|
// 生命周期钩子
|
|
|
onMounted(() => {
|
|
|
- initChart();
|
|
|
+ getData();
|
|
|
+});
|
|
|
+
|
|
|
+watch(() => prop.flushed, (val) => {
|
|
|
+ if(val){
|
|
|
+ getData();
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
onUnmounted(() => {
|