个股中心

This commit is contained in:
renzhijun
2026-01-31 09:22:58 +08:00
parent 5ffaac8fb2
commit 441f4c7360
6 changed files with 1292 additions and 562 deletions

View File

@@ -78,6 +78,8 @@
currentMonth)) + '-' + (currentDay > 9 ? currentDay : ('0' + currentDay))
// this.getYesterdayDateData()
this.generateMonthDateListData()
// 初始化时就派发一次当前日期事件
this.$emit('date-change', this.selectDateStr)
},
methods: {
/**
@@ -92,6 +94,8 @@
let selectDay = selectDate.getDate();
this.selectDateStr = selectYear + '-' + (selectMonth > 9 ? selectMonth : ('0' + selectMonth)) + '-' + (
selectDay > 9 ? selectDay : ('0' + selectDay))
// 派发日期变更事件
this.$emit('date-change', this.selectDateStr)
},
/**
* 生成日期数组
@@ -279,6 +283,7 @@
this.selectDateStr = item.date
this.chgStockData = item
console.log('点击某天');
this.$emit('date-change', this.selectDateStr)
}
}
}

View File

@@ -0,0 +1,294 @@
<template>
<view class="word-cloud-container">
<!-- canvas组件适配小程序 -->
<canvas
type="2d"
ref="wordCloudCanvas"
id="wordCloudCanvas"
class="word-cloud-canvas"
:style="{width: canvasWidth + 'px', height: canvasHeight + 'px'}"
></canvas>
</view>
</template>
<script>
export default {
name: "WordCloud",
props: {
// 词云数据 [{text: '关键词', value: 100}, ...]
wordData: {
type: Array,
required: true,
default: () => []
},
// 画布宽度
width: {
type: Number,
default: 300
},
// 画布高度
height: {
type: Number,
default: 300
},
// 文字颜色列表(分层配色)
colorList: {
type: Array,
default: () => ['#60A5FA', '#FEC200', '#EF4444'] // 外圈、中间、中心
}
},
data() {
return {
canvasWidth: this.width,
canvasHeight: this.height,
ctx: null, // canvas 2d 上下文
placedWords: [] // 已放置的文字信息(用于碰撞检测)
};
},
watch: {
wordData: {
handler() {
this.drawWordCloud();
},
deep: true
}
},
mounted() {
this.initCanvas();
},
methods: {
// 初始化canvas
async initCanvas() {
// 修复点1增加延迟确保canvas节点渲染完成兼容小程序渲染时机
await new Promise(resolve => setTimeout(resolve, 50));
// 适配小程序获取canvas上下文
const query = uni.createSelectorQuery().in(this);
// 修复点2使用ref选择器.ref-wordCloudCanvas替代ID选择器或给canvas加id
query.select('.word-cloud-canvas') // 改用class选择器与模板中canvas的class对应
.fields({ node: true, size: true })
.exec(async (res) => {
// 修复点3增加空值判断避免res[0]为null时报错
if (!res || !res[0] || !res[0].node) {
console.error('获取canvas节点失败请检查canvas是否正确渲染');
return;
}
const canvas = res[0].node;
let ctx = null;
// 修复点4兼容不同小程序平台的canvas 2d上下文获取
try {
ctx = canvas.getContext('2d');
} catch (e) {
console.warn('获取2d上下文失败尝试兼容处理', e);
// 部分小程序平台需要先初始化canvas 2d
ctx = uni.createCanvasContext('wordCloudCanvas', this);
}
if (!ctx) {
console.error('无法获取canvas 2d上下文');
return;
}
// 设置canvas尺寸
const dpr = uni.getSystemInfoSync().pixelRatio || 1;
canvas.width = this.canvasWidth * dpr;
canvas.height = this.canvasHeight * dpr;
ctx.scale(dpr, dpr);
this.ctx = ctx;
this.drawWordCloud();
});
},
// 绘制词云核心方法
drawWordCloud() {
if (!this.ctx || !this.wordData.length) return;
// 清空画布和已放置文字记录
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.placedWords = [];
// 按权重排序(权重越大,文字越大)
const sortedWords = [...this.wordData].sort((a, b) => b.value - a.value);
// 逐个绘制文字
sortedWords.forEach((word, index) => {
this.placeWord(word, index);
});
},
// 放置单个文字(核心:碰撞检测,确保不重叠)
placeWord(word, index) {
const ctx = this.ctx;
// 优化1提高最大尝试次数从50→150让更多文字能找到位置
const maxAttempts = 150;
const baseFontSize = 12; // 基础字体大小
// 分段计算分母适配不同value区间
const value = word.value;
const baseDenominator = 1000; // 基础分母
const interval = 500; // 区间步长
const step = 500; // 分母每次增加的步长
// 计算当前value所属区间动态确定分母
const intervalNum = Math.floor(value / interval);
let denominator = baseDenominator + (intervalNum * step);
// 兜底避免分母过小比如value为0时
denominator = Math.max(denominator, baseDenominator);
// 根据分段分母计算字体大小,限制最大值
const maxFontSize = 28; // 优化2适当减小最大字体从32→28节省空间
const fontSize = Math.min(baseFontSize + (value / denominator) * 16, maxFontSize);
// 旋转角度:-60° 到 60°
const rotateAngle = (Math.random() - 0.5) * 120 * Math.PI / 180;
// 设置字体样式
ctx.font = `${fontSize}px sans-serif`;
// 获取文字宽度和高度(用于碰撞检测)
const textWidth = ctx.measureText(word.name).width;
// 优化3更精准的文字高度估算从1.2→1.05),减少无效空间
const textHeight = fontSize * 1.05;
// 尝试放置文字,直到找到不重叠的位置
for (let i = 0; i < maxAttempts; i++) {
// 优化4扩大随机位置范围从0.2-0.8→0.05-0.95),利用边缘空间
const x = this.canvasWidth * 0.05 + Math.random() * this.canvasWidth * 0.9;
const y = this.canvasHeight * 0.05 + Math.random() * this.canvasHeight * 0.9;
// 碰撞检测:检查当前位置是否与已放置的文字重叠(传入间距容差)
const isOverlap = this.checkOverlap(x, y, textWidth, textHeight, rotateAngle, 2); // 间距容差2px
if (!isOverlap) {
// ===== 核心修改:按位置分层设置颜色 =====
// 1. 计算画布中心坐标
const centerX = this.canvasWidth / 2;
const centerY = this.canvasHeight / 2;
// 2. 计算当前文字位置到中心的距离(欧几里得距离)
const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
// 3. 计算画布最大半径(中心到角落的距离)
const maxDistance = Math.sqrt(Math.pow(centerX, 2) + Math.pow(centerY, 2));
// 4. 按距离分三层分配颜色
let color;
if (distance > maxDistance * 0.66) {
// 外圈(距离>2/3最大半径#60A5FA
color = this.colorList[0];
} else if (distance > maxDistance * 0.33) {
// 中间层(距离>1/3最大半径#FEC200
color = this.colorList[1];
} else {
// 中心层距离≤1/3最大半径#EF4444
color = this.colorList[2];
}
// =========================================
// 设置文字颜色
ctx.fillStyle = color;
// 无重叠,绘制文字
this.drawTextAtPosition(word.name, x, y, rotateAngle, fontSize);
// 记录已放置的文字信息(用于后续碰撞检测)
this.placedWords.push({
x, y, width: textWidth, height: textHeight, angle: rotateAngle
});
break;
}
}
},
// 碰撞检测:检查当前文字是否与已放置的文字重叠(新增间距容差参数)
checkOverlap(x, y, width, height, angle, gap = 2) {
// 简化碰撞检测:使用包围盒检测(旋转后的矩形包围盒)
const currentRect = this.getBoundingRect(x, y, width, height, angle, gap);
for (const placed of this.placedWords) {
const placedRect = this.getBoundingRect(placed.x, placed.y, placed.width, placed.height, placed.angle, gap);
// 轴对齐包围盒AABB碰撞检测
if (
currentRect.left < placedRect.right &&
currentRect.right > placedRect.left &&
currentRect.top < placedRect.bottom &&
currentRect.bottom > placedRect.top
) {
return true; // 重叠
}
}
return false; // 不重叠
},
// 获取旋转后文字的包围盒(新增间距容差参数,缩小包围盒)
getBoundingRect(x, y, width, height, angle, gap = 2) {
// 计算旋转后的四个顶点
const cos = Math.cos(angle);
const sin = Math.sin(angle);
// 优化5缩小包围盒减去间距容差让文字间距更小
const halfW = (width - gap) / 2;
const halfH = (height - gap) / 2;
// 四个顶点坐标(相对于中心)
const points = [
{ x: -halfW, y: -halfH },
{ x: halfW, y: -halfH },
{ x: halfW, y: halfH },
{ x: -halfW, y: halfH }
];
// 旋转并平移到实际位置
const rotatedPoints = points.map(point => ({
x: x + point.x * cos - point.y * sin,
y: y + point.x * sin + point.y * cos
}));
// 计算包围盒的最小/最大坐标
const left = Math.min(...rotatedPoints.map(p => p.x));
const right = Math.max(...rotatedPoints.map(p => p.x));
const top = Math.min(...rotatedPoints.map(p => p.y));
const bottom = Math.max(...rotatedPoints.map(p => p.y));
return { left, right, top, bottom };
},
// 在指定位置绘制旋转后的文字
drawTextAtPosition(text, x, y, angle, fontSize) {
const ctx = this.ctx;
// 保存当前画布状态
ctx.save();
// 平移到文字中心位置
ctx.translate(x, y);
// 旋转
ctx.rotate(angle);
// 绘制文字(居中对齐)
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 0, 0);
// 恢复画布状态
ctx.restore();
}
}
};
</script>
<style scoped>
.word-cloud-container {
width: 100%;
height: 100%;
}
.word-cloud-canvas {
width: 100%;
height: 100%;
}
</style>