涨停分析,异常数据对接,词云优化
This commit is contained in:
@@ -1,24 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="word-cloud-container">
|
<view class="word-cloud-container">
|
||||||
<!-- canvas组件,适配小程序 -->
|
<!-- uni-app / 小程序 canvas,2d 模式 -->
|
||||||
<canvas
|
<canvas
|
||||||
type="2d"
|
type="2d"
|
||||||
ref="wordCloudCanvas"
|
class="word-cloud-canvas"
|
||||||
id="wordCloudCanvas"
|
:style="{ width: width + 'px', height: height + 'px' }"
|
||||||
class="word-cloud-canvas"
|
></canvas>
|
||||||
:style="{width: canvasWidth + 'px', height: canvasHeight + 'px'}"
|
|
||||||
></canvas>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "WordCloud",
|
name: 'WordCloud',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
// 词云数据 [{text: '关键词', value: 100}, ...]
|
// 词云数据:[{ text: '关键词', value: 100 }]
|
||||||
wordData: {
|
wordData: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
// 画布宽度
|
// 画布宽度
|
||||||
@@ -31,79 +29,56 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 300
|
default: 300
|
||||||
},
|
},
|
||||||
// 文字颜色列表(分层配色)
|
// 颜色:外 / 中 / 内
|
||||||
colorList: {
|
colorList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => ['#60A5FA', '#FEC200', '#EF4444'] // 外圈、中间、中心
|
default: () => ['#60A5FA', '#FEC200', '#EF4444']
|
||||||
},
|
|
||||||
// 新增:字号配置,让组件更灵活
|
|
||||||
fontSizeConfig: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
minSize: 12, // 最小字号
|
|
||||||
maxSize: 40, // 最大字号
|
|
||||||
scaleFactor: 0.1 // 缩放因子,越大字号差异越明显
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
canvasWidth: this.width,
|
ctx: null, // canvas 2d 上下文
|
||||||
canvasHeight: this.height,
|
placedWords: [], // 已放置文字的包围盒(用于碰撞检测)
|
||||||
ctx: null, // canvas 2d 上下文
|
centerWords: [], // value 最大的 3 个
|
||||||
placedWords: [] // 已放置的文字信息(用于碰撞检测)
|
otherWords: [] // 其余词
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
// 词云数据变化时重绘
|
||||||
wordData: {
|
wordData: {
|
||||||
|
deep: true,
|
||||||
handler() {
|
handler() {
|
||||||
this.drawWordCloud();
|
this.drawWordCloud();
|
||||||
},
|
}
|
||||||
deep: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initCanvas();
|
this.initCanvas();
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
// 初始化canvas
|
|
||||||
async initCanvas() {
|
|
||||||
// 修复点1:增加延迟,确保canvas节点渲染完成(兼容小程序渲染时机)
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
|
|
||||||
// 适配小程序获取canvas上下文
|
methods: {
|
||||||
|
// 初始化 canvas
|
||||||
|
async initCanvas() {
|
||||||
|
// 延迟,确保 canvas 已渲染(小程序必需)
|
||||||
|
await new Promise(r => setTimeout(r, 50));
|
||||||
|
|
||||||
const query = uni.createSelectorQuery().in(this);
|
const query = uni.createSelectorQuery().in(this);
|
||||||
// 修复点2:使用ref选择器(.ref-wordCloudCanvas)替代ID选择器,或给canvas加id
|
query
|
||||||
query.select('.word-cloud-canvas') // 改用class选择器,与模板中canvas的class对应
|
.select('.word-cloud-canvas')
|
||||||
.fields({ node: true, size: true })
|
.fields({ node: true })
|
||||||
.exec(async (res) => {
|
.exec(res => {
|
||||||
// 修复点3:增加空值判断,避免res[0]为null时报错
|
if (!res || !res[0] || !res[0].node) return;
|
||||||
if (!res || !res[0] || !res[0].node) {
|
|
||||||
console.error('获取canvas节点失败,请检查canvas是否正确渲染');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = res[0].node;
|
const canvas = res[0].node;
|
||||||
let ctx = null;
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
// 修复点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;
|
const dpr = uni.getSystemInfoSync().pixelRatio || 1;
|
||||||
canvas.width = this.canvasWidth * dpr;
|
canvas.width = this.width * dpr;
|
||||||
canvas.height = this.canvasHeight * dpr;
|
canvas.height = this.height * dpr;
|
||||||
ctx.scale(dpr, dpr);
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
@@ -111,183 +86,189 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 绘制词云核心方法
|
// 绘制词云
|
||||||
drawWordCloud() {
|
drawWordCloud() {
|
||||||
if (!this.ctx || !this.wordData.length) return;
|
if (!this.ctx || !this.wordData.length) return;
|
||||||
|
|
||||||
// 清空画布和已放置文字记录
|
// 清空画布
|
||||||
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
|
this.ctx.clearRect(0, 0, this.width, this.height);
|
||||||
this.placedWords = [];
|
this.placedWords = [];
|
||||||
|
|
||||||
// 按权重排序(权重越大,文字越大)
|
// 按 value 从大到小排序
|
||||||
const sortedWords = [...this.wordData].sort((a, b) => b.value - a.value);
|
const sorted = [...this.wordData].sort((a, b) => b.value - a.value);
|
||||||
|
|
||||||
// 计算value的最大值和最小值,用于归一化
|
// 中心层:取前 3 个
|
||||||
const values = sortedWords.map(item => item.value);
|
this.centerWords = sorted.slice(0, 3);
|
||||||
this.valueMax = Math.max(...values);
|
this.otherWords = sorted.slice(3);
|
||||||
this.valueMin = Math.min(...values);
|
|
||||||
|
|
||||||
// 逐个绘制文字
|
// 先画中心层(优先级最高)
|
||||||
sortedWords.forEach((word, index) => {
|
this.centerWords.forEach((word, index) => {
|
||||||
this.placeWord(word, index);
|
this.placeWord(word, 'center', index);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 再画中 / 外层
|
||||||
|
this.otherWords.forEach(word => {
|
||||||
|
this.placeWord(word, 'other');
|
||||||
|
});
|
||||||
|
// ⭐ 通知父组件:canvas 绘制完成
|
||||||
|
this.$emit('rendered');
|
||||||
},
|
},
|
||||||
|
|
||||||
// 放置单个文字(核心:碰撞检测,确保不重叠)
|
// 放置单个词
|
||||||
placeWord(word, index) {
|
placeWord(word, type, index = 0) {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
// 优化1:提高最大尝试次数(从50→150),让更多文字能找到位置
|
const text = word.text || word.name;
|
||||||
const maxAttempts = 150;
|
const maxAttempts = 200;
|
||||||
const { minSize, maxSize, scaleFactor } = this.fontSizeConfig;
|
|
||||||
|
|
||||||
// ===== 核心优化:重新设计字号计算逻辑 =====
|
let fontSize = 24;
|
||||||
// 1. 归一化value(0-1区间)
|
let layer = 'middle';
|
||||||
let normalizedValue = 1;
|
|
||||||
if (this.valueMax !== this.valueMin) {
|
// 根据层级设置字号
|
||||||
normalizedValue = (word.value - this.valueMin) / (this.valueMax - this.valueMin);
|
if (type === 'center') {
|
||||||
|
fontSize = 32; // ⭐ 最中间三个:32px
|
||||||
|
layer = 'center';
|
||||||
|
} else {
|
||||||
|
layer = Math.random() > 0.5 ? 'middle' : 'outer';
|
||||||
|
fontSize = layer === 'outer' ? 18 : 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 计算字号:基于归一化值,确保最小到最大的平滑过渡
|
// 随机两档旋转角度
|
||||||
// 公式:最小字号 + (最大字号 - 最小字号) * 归一化值 * 缩放因子
|
const angleLimit =
|
||||||
const fontSize = Math.min(
|
Math.random() > 0.5
|
||||||
minSize + (maxSize - minSize) * normalizedValue * scaleFactor,
|
? 60 * Math.PI / 180
|
||||||
maxSize
|
: 30 * Math.PI / 180;
|
||||||
);
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
// 旋转角度:-60° 到 60°
|
const angle = (Math.random() - 0.5) * 2 * angleLimit;
|
||||||
const rotateAngle = (Math.random() - 0.5) * 120 * Math.PI / 180;
|
|
||||||
|
|
||||||
// 设置字体样式
|
// 设置字体(全部加粗)
|
||||||
ctx.font = `${fontSize}px sans-serif`;
|
ctx.font = `bold ${fontSize}px sans-serif`;
|
||||||
|
|
||||||
// 获取文字宽度和高度(用于碰撞检测)
|
// 测量文字尺寸
|
||||||
const textWidth = ctx.measureText(word.text || word.name).width; // 兼容text/name字段
|
const textWidth = ctx.measureText(text).width;
|
||||||
// 优化3:更精准的文字高度估算(从1.2→1.05),减少无效空间
|
|
||||||
const textHeight = fontSize * 1.05;
|
const textHeight = fontSize * 1.05;
|
||||||
|
|
||||||
// 尝试放置文字,直到找到不重叠的位置
|
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
// 优化4:扩大随机位置范围(从0.2-0.8→0.05-0.95),利用边缘空间
|
let x, y;
|
||||||
const x = this.canvasWidth * 0.05 + Math.random() * this.canvasWidth * 0.9;
|
|
||||||
const y = this.canvasHeight * 0.05 + Math.random() * this.canvasHeight * 0.9;
|
|
||||||
|
|
||||||
// 碰撞检测:检查当前位置是否与已放置的文字重叠(传入间距容差)
|
// ===== 中心层:固定布局,保证 3 个一定能放下 =====
|
||||||
const isOverlap = this.checkOverlap(x, y, textWidth, textHeight, rotateAngle, 2); // 间距容差2px
|
if (layer === 'center') {
|
||||||
|
const centerX = this.width / 2;
|
||||||
|
const centerY = this.height / 2;
|
||||||
|
|
||||||
if (!isOverlap) {
|
// 中心三个固定位置
|
||||||
// ===== 核心修改:按位置分层设置颜色 =====
|
const offsets = [
|
||||||
// 1. 计算画布中心坐标
|
{ x: 0, y: 0 }, // 最大的,正中
|
||||||
const centerX = this.canvasWidth / 2;
|
{ x: -80, y: 0 }, // 左
|
||||||
const centerY = this.canvasHeight / 2;
|
{ x: 80, y: 0 } // 右
|
||||||
|
];
|
||||||
|
|
||||||
// 2. 计算当前文字位置到中心的距离(欧几里得距离)
|
const pos = offsets[index] || offsets[0];
|
||||||
const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
|
x = centerX + pos.x;
|
||||||
|
y = centerY + pos.y;
|
||||||
|
} else {
|
||||||
|
// 中 / 外层:随机位置
|
||||||
|
x = this.width * 0.05 + Math.random() * this.width * 0.9;
|
||||||
|
y = this.height * 0.05 + Math.random() * this.height * 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 计算画布最大半径(中心到角落的距离)
|
// 计算旋转后的包围盒
|
||||||
const maxDistance = Math.sqrt(Math.pow(centerX, 2) + Math.pow(centerY, 2));
|
const rect = this.getBoundingRect(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
textWidth,
|
||||||
|
textHeight,
|
||||||
|
angle,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
// 4. 按距离分三层分配颜色
|
// 外层:超出画布直接跳过,继续尝试
|
||||||
let color;
|
if (layer === 'outer') {
|
||||||
if (distance > maxDistance * 0.66) {
|
if (
|
||||||
// 外圈(距离>2/3最大半径):#60A5FA
|
rect.left < 0 ||
|
||||||
color = this.colorList[0];
|
rect.right > this.width ||
|
||||||
} else if (distance > maxDistance * 0.33) {
|
rect.top < 0 ||
|
||||||
// 中间层(距离>1/3最大半径):#FEC200
|
rect.bottom > this.height
|
||||||
color = this.colorList[1];
|
) {
|
||||||
} else {
|
continue;
|
||||||
// 中心层(距离≤1/3最大半径):#EF4444
|
|
||||||
color = this.colorList[2];
|
|
||||||
}
|
}
|
||||||
// =========================================
|
|
||||||
|
|
||||||
// 设置文字颜色
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
|
|
||||||
// 无重叠,绘制文字
|
|
||||||
this.drawTextAtPosition(word.text || word.name, x, y, rotateAngle, fontSize);
|
|
||||||
|
|
||||||
// 记录已放置的文字信息(用于后续碰撞检测)
|
|
||||||
this.placedWords.push({
|
|
||||||
x, y, width: textWidth, height: textHeight, angle: rotateAngle
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 碰撞检测
|
||||||
|
if (this.checkOverlapRect(rect)) continue;
|
||||||
|
|
||||||
|
// 根据层级设置颜色
|
||||||
|
ctx.fillStyle =
|
||||||
|
layer === 'center'
|
||||||
|
? this.colorList[2]
|
||||||
|
: layer === 'middle'
|
||||||
|
? this.colorList[1]
|
||||||
|
: this.colorList[0];
|
||||||
|
|
||||||
|
// 绘制文字
|
||||||
|
this.drawText(text, x, y, angle);
|
||||||
|
|
||||||
|
// 保存包围盒
|
||||||
|
this.placedWords.push({ rect });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 碰撞检测:检查当前文字是否与已放置的文字重叠(新增间距容差参数)
|
// 碰撞检测(AABB)
|
||||||
checkOverlap(x, y, width, height, angle, gap = 2) {
|
checkOverlapRect(current) {
|
||||||
// 简化碰撞检测:使用包围盒检测(旋转后的矩形包围盒)
|
for (const item of this.placedWords) {
|
||||||
const currentRect = this.getBoundingRect(x, y, width, height, angle, gap);
|
const r = item.rect;
|
||||||
|
|
||||||
for (const placed of this.placedWords) {
|
|
||||||
const placedRect = this.getBoundingRect(placed.x, placed.y, placed.width, placed.height, placed.angle, gap);
|
|
||||||
|
|
||||||
// 轴对齐包围盒(AABB)碰撞检测
|
|
||||||
if (
|
if (
|
||||||
currentRect.left < placedRect.right &&
|
current.left < r.right &&
|
||||||
currentRect.right > placedRect.left &&
|
current.right > r.left &&
|
||||||
currentRect.top < placedRect.bottom &&
|
current.top < r.bottom &&
|
||||||
currentRect.bottom > placedRect.top
|
current.bottom > r.top
|
||||||
) {
|
) {
|
||||||
return true; // 重叠
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false; // 不重叠
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取旋转后文字的包围盒(新增间距容差参数,缩小包围盒)
|
// 计算旋转后的包围盒
|
||||||
getBoundingRect(x, y, width, height, angle, gap = 2) {
|
getBoundingRect(x, y, width, height, angle, gap = 2) {
|
||||||
// 计算旋转后的四个顶点
|
|
||||||
const cos = Math.cos(angle);
|
const cos = Math.cos(angle);
|
||||||
const sin = Math.sin(angle);
|
const sin = Math.sin(angle);
|
||||||
|
|
||||||
// 优化5:缩小包围盒(减去间距容差),让文字间距更小
|
|
||||||
const halfW = (width - gap) / 2;
|
const halfW = (width - gap) / 2;
|
||||||
const halfH = (height - gap) / 2;
|
const halfH = (height - gap) / 2;
|
||||||
|
|
||||||
// 四个顶点坐标(相对于中心)
|
|
||||||
const points = [
|
const points = [
|
||||||
{ x: -halfW, y: -halfH },
|
{ x: -halfW, y: -halfH },
|
||||||
{ x: -halfW, y: halfH },
|
{ x: halfW, y: -halfH },
|
||||||
{ x: halfW, y: halfH },
|
{ x: halfW, y: halfH },
|
||||||
{ x: halfW, y: -halfH }
|
{ x: -halfW, y: halfH }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 旋转并平移到实际位置
|
const xs = [];
|
||||||
const rotatedPoints = points.map(point => ({
|
const ys = [];
|
||||||
x: x + point.x * cos - point.y * sin,
|
|
||||||
y: y + point.x * sin + point.y * cos
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 计算包围盒的最小/最大坐标
|
points.forEach(p => {
|
||||||
const left = Math.min(...rotatedPoints.map(p => p.x));
|
xs.push(x + p.x * cos - p.y * sin);
|
||||||
const right = Math.max(...rotatedPoints.map(p => p.x));
|
ys.push(y + p.x * sin + p.y * cos);
|
||||||
const top = Math.min(...rotatedPoints.map(p => p.y));
|
});
|
||||||
const bottom = Math.max(...rotatedPoints.map(p => p.y));
|
|
||||||
|
|
||||||
return { left, right, top, bottom };
|
return {
|
||||||
|
left: Math.min(...xs),
|
||||||
|
right: Math.max(...xs),
|
||||||
|
top: Math.min(...ys),
|
||||||
|
bottom: Math.max(...ys)
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// 在指定位置绘制旋转后的文字
|
// 绘制文字
|
||||||
drawTextAtPosition(text, x, y, angle, fontSize) {
|
drawText(text, x, y, angle) {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
|
|
||||||
// 保存当前画布状态
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
// 平移到文字中心位置
|
|
||||||
ctx.translate(x, y);
|
ctx.translate(x, y);
|
||||||
// 旋转
|
|
||||||
ctx.rotate(angle);
|
ctx.rotate(angle);
|
||||||
|
|
||||||
// 绘制文字(居中对齐)
|
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.fillText(text, 0, 0);
|
ctx.fillText(text, 0, 0);
|
||||||
|
|
||||||
// 恢复画布状态
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,6 +280,7 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.word-cloud-canvas {
|
.word-cloud-canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: '24rpx',
|
fontSize: '24rpx',
|
||||||
fontWeight: '500'
|
fontWeight: '500'
|
||||||
}" >
|
}" @click="bkydAction(index)" >
|
||||||
<view class="single-line-ellipsis">{{item.title}}</view>
|
<view class="single-line-ellipsis">{{item.title}}</view>
|
||||||
<view class="count-text">{{item.count}}只</view>
|
<view class="count-text">{{item.count}}只</view>
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
<view v-show="activeType === 0" style="width: 100%; height: 500rpx;">
|
<view v-show="activeType === 0" style="width: 100%; height: 500rpx;">
|
||||||
<l-echart ref="chartRef"></l-echart>
|
<l-echart ref="chartRef"></l-echart>
|
||||||
</view>
|
</view>
|
||||||
<WordCloud v-show="activeType === 1" :wordData="wordData" :width="330" :height="330" />
|
<WordCloud v-show="activeType === 1" :wordData="wordData" :width="330" :height="330" @rendered="onWordCloudRendered" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
||||||
@@ -556,6 +556,10 @@
|
|||||||
//}
|
//}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 词云绘制完成
|
||||||
|
onWordCloudRendered() {
|
||||||
|
uni.hideLoading();
|
||||||
|
},
|
||||||
getStockHeatType(stock) {
|
getStockHeatType(stock) {
|
||||||
// 假设通过连板数计算热度(可根据实际业务逻辑调整)
|
// 假设通过连板数计算热度(可根据实际业务逻辑调整)
|
||||||
const days = stock.continuous_days_num || 0;
|
const days = stock.continuous_days_num || 0;
|
||||||
@@ -725,9 +729,19 @@ getStockHeatType(stock) {
|
|||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
//this.$refs.chartRef && this.initPieChart(); // 增加存在性判断
|
//this.$refs.chartRef && this.initPieChart(); // 增加存在性判断
|
||||||
|
uni.showLoading({
|
||||||
|
title: '词云生成中...',
|
||||||
|
mask: true // 防止用户误触
|
||||||
|
});
|
||||||
|
|
||||||
this.initWordCloud();
|
this.initWordCloud();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
|
uni.showLoading({
|
||||||
|
title: '词云生成中...',
|
||||||
|
mask: true // 防止用户误触
|
||||||
|
});
|
||||||
|
|
||||||
this.initWordCloud();
|
this.initWordCloud();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -916,7 +930,9 @@ getStockHeatType(stock) {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading();
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
//console.log('父页面设置词云数据:', JSON.stringify(this.wordData));
|
//console.log('父页面设置词云数据:', JSON.stringify(this.wordData));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -188,48 +188,40 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
// 筛选后的股票列表
|
// 筛选后的股票列表:按板块codes匹配 + 连板排序/筛选
|
||||||
filteredStocks() {
|
filteredStocks() {
|
||||||
if (!this.allStocks.length) return [];
|
if (!this.originData?.stocks || !this.bkList.length) return [];
|
||||||
|
|
||||||
let stocks = [...this.allStocks];
|
// 1. 获取当前选中板块的股票代码集合
|
||||||
|
const currentBk = this.bkList[this.activeIndex];
|
||||||
|
if (!currentBk?.codes || currentBk.codes.length === 0) return [];
|
||||||
|
const targetCodes = new Set(currentBk.codes); // 转Set提升匹配效率
|
||||||
|
|
||||||
// 先筛选当前选中板块的股票
|
// 2. 从stocks中筛选出scode在targetCodes中的股票
|
||||||
if (this.activeIndex >= 0 && this.bkList.length) {
|
let stocks = this.originData.stocks.filter(stock => targetCodes.has(stock.scode));
|
||||||
const currentSector = this.bkList[this.activeIndex]?.title;
|
|
||||||
if (currentSector) {
|
|
||||||
stocks = stocks.filter(stock => {
|
|
||||||
// 匹配核心板块或板块分类
|
|
||||||
const sectorMatch = stock.core_sectors.some(s => s.includes(currentSector)) ||
|
|
||||||
(Array.isArray(stock.sector_category) ? stock.sector_category.includes(currentSector) : stock.sector_category === currentSector);
|
|
||||||
return sectorMatch;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据筛选类型排序/过滤
|
// 3. 保留原有筛选/排序逻辑
|
||||||
switch (this.filterIndex) {
|
switch (this.filterIndex) {
|
||||||
case 0: // 按连板数
|
case 0: // 按连板数从高到低排序
|
||||||
stocks.sort((a, b) => {
|
stocks.sort((a, b) => {
|
||||||
const aDays = this.parseContinuousDays(a.continuous_days);
|
const aDays = this.parseContinuousDays(a.continuous_days);
|
||||||
const bDays = this.parseContinuousDays(b.continuous_days);
|
const bDays = this.parseContinuousDays(b.continuous_days);
|
||||||
return bDays - aDays;
|
return bDays - aDays;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 1: // 只看龙头(≥2连板)
|
case 1: // 只看龙头(≥2连板),并按连板数从高到低排序
|
||||||
stocks = stocks.filter(stock => this.parseContinuousDays(stock.continuous_days) >= 2);
|
stocks = stocks.filter(stock => this.parseContinuousDays(stock.continuous_days) >= 2);
|
||||||
stocks.sort((a, b) => {
|
stocks.sort((a, b) => {
|
||||||
const aDays = this.parseContinuousDays(a.continuous_days);
|
const aDays = this.parseContinuousDays(a.continuous_days);
|
||||||
const bDays = this.parseContinuousDays(b.continuous_days);
|
const bDays = this.parseContinuousDays(b.continuous_days);
|
||||||
return bDays - aDays;
|
return bDays - aDays;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
return stocks;
|
||||||
|
}
|
||||||
return stocks;
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onLoad(e) {
|
onLoad(e) {
|
||||||
this.activeIndex = e.index
|
this.activeIndex = e.index
|
||||||
@@ -351,99 +343,85 @@ computed: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// 为所有股票添加角色标签
|
|
||||||
setStockRoles() {
|
|
||||||
if (!this.originData || !this.originData.stocks || !this.bkList.length) return;
|
|
||||||
console.log("setStockRoles",JSON.stringify(this.originData.stocks))
|
|
||||||
this.allStocks = this.originData.stocks.map(stock => {
|
|
||||||
// 找到股票所属板块的热度排名
|
|
||||||
let sectorIndex = -1;
|
|
||||||
const stockSectors = Array.isArray(stock.sector_category) ? stock.sector_category : [stock.sector_category];
|
|
||||||
|
|
||||||
// 匹配板块列表中的位置
|
// 为所有股票添加角色标签
|
||||||
this.bkList.some((bk, idx) => {
|
setStockRoles() {
|
||||||
const match = stockSectors.some(s => s.includes(bk.title));
|
if (!this.originData || !this.originData.stocks || !this.bkList.length) return;
|
||||||
if (match) {
|
this.allStocks = this.originData.stocks.map(stock => {
|
||||||
sectorIndex = idx;
|
// 找到股票所属板块的热度排名
|
||||||
return true;
|
let sectorIndex = -1;
|
||||||
}
|
const stockSectors = Array.isArray(stock.sector_category) ? stock.sector_category : [stock.sector_category];
|
||||||
return false;
|
// 匹配板块列表中的位置
|
||||||
});
|
this.bkList.some((bk, idx) => {
|
||||||
|
const match = stockSectors.some(s => s.includes(bk.title));
|
||||||
|
if (match) {
|
||||||
|
sectorIndex = idx;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
// 获取同板块的所有股票
|
||||||
|
const sectorStocks = this.originData.stocks.filter(s => {
|
||||||
|
const sSectors = Array.isArray(s.sector_category) ? s.sector_category : [s.sector_category];
|
||||||
|
return sSectors.some(ss => stockSectors.includes(ss));
|
||||||
|
});
|
||||||
|
// 获取股票角色
|
||||||
|
const stockRole = this.getStockRole(stock, sectorStocks, sectorIndex);
|
||||||
|
return {
|
||||||
|
...stock,
|
||||||
|
stockRole: stockRole.text ? stockRole : null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// 获取同板块的所有股票
|
|
||||||
const sectorStocks = this.originData.stocks.filter(s => {
|
|
||||||
const sSectors = Array.isArray(s.sector_category) ? s.sector_category : [s.sector_category];
|
|
||||||
return sSectors.some(ss => stockSectors.includes(ss));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取股票角色
|
|
||||||
const stockRole = this.getStockRole(stock, sectorStocks, sectorIndex);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...stock,
|
|
||||||
stockRole: stockRole.text ? stockRole : null
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求接口数据(优化:动态日期+自动时间戳)
|
* 请求接口数据(优化:动态日期+自动时间戳)
|
||||||
*/
|
*/
|
||||||
// 请求接口数据
|
// 请求接口数据
|
||||||
async fetchData() {
|
async fetchData() {
|
||||||
try {
|
try {
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = new Date().getTime();
|
||||||
const formattedDate = this.selectedFullDate;
|
const formattedDate = this.selectedFullDate;
|
||||||
const baseURL = getBaseURL1();
|
const baseURL = getBaseURL1();
|
||||||
const requestUrl = `${baseURL}/data/zt/daily/${formattedDate}.json?t=${timestamp}`;
|
const requestUrl = `${baseURL}/data/zt/daily/${formattedDate}.json?t=${timestamp}`;
|
||||||
|
|
||||||
console.log('请求URL:', requestUrl);
|
console.log('请求URL:', requestUrl);
|
||||||
|
const res = await uni.request({
|
||||||
|
url: requestUrl,
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
const res = await uni.request({
|
if (res.statusCode === 200 && res.data) {
|
||||||
url: requestUrl,
|
this.originData = res.data;
|
||||||
method: 'GET'
|
const { sector_data } = this.originData;
|
||||||
});
|
|
||||||
|
|
||||||
if (res.statusCode === 200 && res.data) {
|
// 解析sector_data生成板块列表:剔除「其他」,格式[{title: 板块名, codes: 股票代码数组}]
|
||||||
this.originData = res.data;
|
this.bkList = Object.entries(sector_data)
|
||||||
|
.filter(([sectorName]) => sectorName !== '其他') // 去掉其他板块
|
||||||
|
.map(([sectorName, sectorInfo]) => ({
|
||||||
|
title: sectorName,
|
||||||
|
codes: sectorInfo.stock_codes || [] // 取板块对应的股票代码
|
||||||
|
}));
|
||||||
|
|
||||||
// 处理板块列表
|
console.log('生成板块列表:', this.bkList);
|
||||||
const chartData = this.originData.chart_data || {};
|
// 为股票添加角色标签
|
||||||
const labels = chartData.labels || [];
|
this.setStockRoles();
|
||||||
const counts = chartData.counts || [];
|
} else {
|
||||||
|
uni.showToast({
|
||||||
const maxCount = counts.length > 0 ? Math.max(...counts) : 0;
|
title: '数据请求失败',
|
||||||
const maxLen = Math.min(labels.length, counts.length);
|
icon: 'none'
|
||||||
let bkList = [];
|
});
|
||||||
|
}
|
||||||
for (let i = 0; i < maxLen; i++) {
|
} catch (error) {
|
||||||
const title = labels[i];
|
console.error('请求异常:', error);
|
||||||
const count = counts[i] || 0;
|
uni.showToast({
|
||||||
bkList.push({
|
title: '网络异常',
|
||||||
title,
|
icon: 'none'
|
||||||
count
|
});
|
||||||
});
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
this.bkList = bkList;
|
|
||||||
|
|
||||||
// 为股票添加角色标签
|
|
||||||
this.setStockRoles();
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: '数据请求失败',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('请求异常:', error);
|
|
||||||
uni.showToast({
|
|
||||||
title: '网络异常',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user