2.6 盈利能力模块表格完善

This commit is contained in:
尚政杰
2026-02-06 18:01:05 +08:00
parent 890be2e3e9
commit 6dc7d00e6a
48 changed files with 2831 additions and 697 deletions

View File

@@ -3,10 +3,9 @@ const common_vendor = require("./common/vendor.js");
const _sfc_main = {
name: "WordCloud",
props: {
// 词云数据 [{text: '关键词', value: 100}, ...]
// 词云数据[{ text: '关键词', value: 100 }]
wordData: {
type: Array,
required: true,
default: () => []
},
// 画布宽度
@@ -19,149 +18,142 @@ const _sfc_main = {
type: Number,
default: 300
},
// 文字颜色列表(分层配色)
// 颜色:外 / 中 / 内
colorList: {
type: Array,
default: () => ["#60A5FA", "#FEC200", "#EF4444"]
// 外圈、中间、中心
},
// 新增:字号配置,让组件更灵活
fontSizeConfig: {
type: Object,
default: () => ({
minSize: 12,
// 最小字号
maxSize: 40,
// 最大字号
scaleFactor: 0.1
// 缩放因子,越大字号差异越明显
})
}
},
data() {
return {
canvasWidth: this.width,
canvasHeight: this.height,
ctx: null,
// canvas 2d 上下文
placedWords: []
// 已放置文字信息(用于碰撞检测)
placedWords: [],
// 已放置文字的包围盒(用于碰撞检测)
centerWords: [],
// value 最大的 3 个
otherWords: []
// 其余词
};
},
watch: {
// 词云数据变化时重绘
wordData: {
deep: true,
handler() {
this.drawWordCloud();
},
deep: true
}
}
},
mounted() {
this.initCanvas();
},
methods: {
// 初始化canvas
// 初始化 canvas
async initCanvas() {
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((r) => setTimeout(r, 50));
const query = common_vendor.index.createSelectorQuery().in(this);
query.select(".word-cloud-canvas").fields({ node: true, size: true }).exec(async (res) => {
if (!res || !res[0] || !res[0].node) {
common_vendor.index.__f__("error", "at components/WordCloud/WordCloud.vue:82", "获取canvas节点失败请检查canvas是否正确渲染");
query.select(".word-cloud-canvas").fields({ node: true }).exec((res) => {
if (!res || !res[0] || !res[0].node)
return;
}
const canvas = res[0].node;
let ctx = null;
try {
ctx = canvas.getContext("2d");
} catch (e) {
common_vendor.index.__f__("warn", "at components/WordCloud/WordCloud.vue:93", "获取2d上下文失败尝试兼容处理", e);
ctx = common_vendor.index.createCanvasContext("wordCloudCanvas", this);
}
if (!ctx) {
common_vendor.index.__f__("error", "at components/WordCloud/WordCloud.vue:99", "无法获取canvas 2d上下文");
return;
}
const ctx = canvas.getContext("2d");
const dpr = common_vendor.index.getSystemInfoSync().pixelRatio || 1;
canvas.width = this.canvasWidth * dpr;
canvas.height = this.canvasHeight * dpr;
canvas.width = this.width * dpr;
canvas.height = this.height * 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.ctx.clearRect(0, 0, this.width, this.height);
this.placedWords = [];
const sortedWords = [...this.wordData].sort((a, b) => b.value - a.value);
const values = sortedWords.map((item) => item.value);
this.valueMax = Math.max(...values);
this.valueMin = Math.min(...values);
sortedWords.forEach((word, index) => {
this.placeWord(word, index);
const sorted = [...this.wordData].sort((a, b) => b.value - a.value);
this.centerWords = sorted.slice(0, 3);
this.otherWords = sorted.slice(3);
this.centerWords.forEach((word, index) => {
this.placeWord(word, "center", index);
});
this.otherWords.forEach((word) => {
this.placeWord(word, "other");
});
this.$emit("rendered");
},
// 放置单个文字(核心:碰撞检测,确保不重叠)
placeWord(word, index) {
// 放置单个
placeWord(word, type, index = 0) {
const ctx = this.ctx;
const maxAttempts = 150;
const { minSize, maxSize, scaleFactor } = this.fontSizeConfig;
let normalizedValue = 1;
if (this.valueMax !== this.valueMin) {
normalizedValue = (word.value - this.valueMin) / (this.valueMax - this.valueMin);
const text = word.text || word.name;
const maxAttempts = 200;
let fontSize = 24;
let layer = "middle";
if (type === "center") {
fontSize = 32;
layer = "center";
} else {
layer = Math.random() > 0.5 ? "middle" : "outer";
fontSize = layer === "outer" ? 18 : 24;
}
const fontSize = Math.min(
minSize + (maxSize - minSize) * normalizedValue * scaleFactor,
maxSize
);
const rotateAngle = (Math.random() - 0.5) * 120 * Math.PI / 180;
ctx.font = `${fontSize}px sans-serif`;
const textWidth = ctx.measureText(word.text || word.name).width;
const angleLimit = Math.random() > 0.5 ? 60 * Math.PI / 180 : 30 * Math.PI / 180;
const angle = (Math.random() - 0.5) * 2 * angleLimit;
ctx.font = `bold ${fontSize}px sans-serif`;
const textWidth = ctx.measureText(text).width;
const textHeight = fontSize * 1.05;
for (let i = 0; i < maxAttempts; i++) {
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);
if (!isOverlap) {
const centerX = this.canvasWidth / 2;
const centerY = this.canvasHeight / 2;
const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
const maxDistance = Math.sqrt(Math.pow(centerX, 2) + Math.pow(centerY, 2));
let color;
if (distance > maxDistance * 0.66) {
color = this.colorList[0];
} else if (distance > maxDistance * 0.33) {
color = this.colorList[1];
} else {
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;
let x, y;
if (layer === "center") {
const centerX = this.width / 2;
const centerY = this.height / 2;
const offsets = [
{ x: 0, y: 0 },
// 最大的,正中
{ x: -80, y: 0 },
// 左
{ x: 80, y: 0 }
// 右
];
const pos = offsets[index] || offsets[0];
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;
}
const rect = this.getBoundingRect(
x,
y,
textWidth,
textHeight,
angle,
2
);
if (layer === "outer") {
if (rect.left < 0 || rect.right > this.width || rect.top < 0 || rect.bottom > this.height) {
continue;
}
}
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;
}
},
// 碰撞检测:检查当前文字是否与已放置的文字重叠(新增间距容差参数
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);
if (currentRect.left < placedRect.right && currentRect.right > placedRect.left && currentRect.top < placedRect.bottom && currentRect.bottom > placedRect.top) {
// 碰撞检测AABB
checkOverlapRect(current) {
for (const item of this.placedWords) {
const r = item.rect;
if (current.left < r.right && current.right > r.left && current.top < r.bottom && current.bottom > r.top) {
return true;
}
}
return false;
},
// 获取旋转后文字的包围盒(新增间距容差参数,缩小包围盒)
// 计算旋转后的包围盒
getBoundingRect(x, y, width, height, angle, gap = 2) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
@@ -169,22 +161,25 @@ const _sfc_main = {
const halfH = (height - gap) / 2;
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 }
];
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 };
const xs = [];
const ys = [];
points.forEach((p) => {
xs.push(x + p.x * cos - p.y * sin);
ys.push(y + p.x * sin + p.y * cos);
});
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;
ctx.save();
ctx.translate(x, y);
@@ -198,8 +193,8 @@ const _sfc_main = {
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: $data.canvasWidth + "px",
b: $data.canvasHeight + "px"
a: $props.width + "px",
b: $props.height + "px"
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-cab45d13"]]);