|
@@ -2,7 +2,7 @@
|
|
* @Author: LiZhiWei
|
|
* @Author: LiZhiWei
|
|
* @Date: 2025-04-10 14:38:27
|
|
* @Date: 2025-04-10 14:38:27
|
|
* @LastEditors: LiZhiWei
|
|
* @LastEditors: LiZhiWei
|
|
- * @LastEditTime: 2025-04-25 09:32:40
|
|
|
|
|
|
+ * @LastEditTime: 2025-04-27 08:36:57
|
|
* @Description:
|
|
* @Description:
|
|
-->
|
|
-->
|
|
<template>
|
|
<template>
|
|
@@ -20,6 +20,7 @@ const pptxContainer = ref(null)
|
|
|
|
|
|
// 渲染幻灯片
|
|
// 渲染幻灯片
|
|
const renderSlides = () => {
|
|
const renderSlides = () => {
|
|
|
|
+ console.log("props.pptxJson", props.pptxJson)
|
|
if (!pptxContainer.value || !props.pptxJson) return
|
|
if (!pptxContainer.value || !props.pptxJson) return
|
|
|
|
|
|
pptxContainer.value.innerHTML = ""
|
|
pptxContainer.value.innerHTML = ""
|
|
@@ -1931,18 +1932,9 @@ const createShapeElement = (element) => {
|
|
const fillColor = element.fill.value || "transparent"
|
|
const fillColor = element.fill.value || "transparent"
|
|
topRect.setAttribute("fill", fillColor)
|
|
topRect.setAttribute("fill", fillColor)
|
|
topTrapezoid.setAttribute("fill", adjustBrightness(fillColor, 1.2))
|
|
topTrapezoid.setAttribute("fill", adjustBrightness(fillColor, 1.2))
|
|
- bottomTrapezoid.setAttribute(
|
|
|
|
- "fill",
|
|
|
|
- adjustBrightness(fillColor, 0.8)
|
|
|
|
- )
|
|
|
|
- leftTrapezoid.setAttribute(
|
|
|
|
- "fill",
|
|
|
|
- adjustBrightness(fillColor, 0.9)
|
|
|
|
- )
|
|
|
|
- rightTrapezoid.setAttribute(
|
|
|
|
- "fill",
|
|
|
|
- adjustBrightness(fillColor, 0.7)
|
|
|
|
- )
|
|
|
|
|
|
+ bottomTrapezoid.setAttribute("fill", adjustBrightness(fillColor, 0.8))
|
|
|
|
+ leftTrapezoid.setAttribute("fill", adjustBrightness(fillColor, 0.9))
|
|
|
|
+ rightTrapezoid.setAttribute("fill", adjustBrightness(fillColor, 0.7))
|
|
} else {
|
|
} else {
|
|
topRect.setAttribute("fill", "transparent")
|
|
topRect.setAttribute("fill", "transparent")
|
|
topTrapezoid.setAttribute("fill", "transparent")
|
|
topTrapezoid.setAttribute("fill", "transparent")
|
|
@@ -3817,7 +3809,7 @@ const createShapeElement = (element) => {
|
|
|
|
|
|
// 构建路径
|
|
// 构建路径
|
|
const path2 = `
|
|
const path2 = `
|
|
- M 0,${ry}
|
|
|
|
|
|
+ M 0,${ry}
|
|
A ${rx} ${ry} 0 1 1 ${element.width} ${ry}
|
|
A ${rx} ${ry} 0 1 1 ${element.width} ${ry}
|
|
L ${element.width - (rx - innerRx)},${ry}
|
|
L ${element.width - (rx - innerRx)},${ry}
|
|
A ${innerRx} ${innerRy} 0 1 0 ${rx - innerRx},${ry}
|
|
A ${innerRx} ${innerRy} 0 1 0 ${rx - innerRx},${ry}
|
|
@@ -4207,8 +4199,59 @@ const createTableElement = (element) => {
|
|
}
|
|
}
|
|
|
|
|
|
const createChartElement = (element) => {
|
|
const createChartElement = (element) => {
|
|
- // 实现图表元素创建逻辑
|
|
|
|
- return document.createElement("div") // 临时返回空div
|
|
|
|
|
|
+ // 1. 创建基础容器
|
|
|
|
+ const el = document.createElement("div")
|
|
|
|
+ el.style.position = "absolute"
|
|
|
|
+ el.style.top = element.top + "px"
|
|
|
|
+ el.style.left = element.left + "px"
|
|
|
|
+ el.style.width = element.width + "px"
|
|
|
|
+ el.style.height = element.height + "px"
|
|
|
|
+ el.style.zIndex = element.order
|
|
|
|
+ // 2. 创建SVG画布
|
|
|
|
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
|
|
|
+ svg.setAttribute("width", element.width)
|
|
|
|
+ svg.setAttribute("height", element.height)
|
|
|
|
+ svg.setAttribute("viewBox", `0 0 ${element.width} ${element.height}`)
|
|
|
|
+
|
|
|
|
+ // 3. 设置图表内边距
|
|
|
|
+ const padding = {
|
|
|
|
+ top: 60, // 为图例留出空间
|
|
|
|
+ right: 40, // 右侧边距
|
|
|
|
+ bottom: 60, // X轴标签空间
|
|
|
|
+ left: 60, // Y轴标签空间
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 4. 计算实际绘图区域
|
|
|
|
+ const chartWidth = element.width - padding.left - padding.right
|
|
|
|
+ const chartHeight = element.height - padding.top - padding.bottom
|
|
|
|
+ // 处理不同图表类型
|
|
|
|
+ switch (element.chartType) {
|
|
|
|
+ case "barChart":
|
|
|
|
+ // 绘制柱状图
|
|
|
|
+ drawBarChart(svg, element, {
|
|
|
|
+ padding,
|
|
|
|
+ chartWidth,
|
|
|
|
+ chartHeight,
|
|
|
|
+ barDir: element.barDir || "col",
|
|
|
|
+ grouping: element.grouping || "clustered",
|
|
|
|
+ })
|
|
|
|
+ break
|
|
|
|
+ case "pieChart":
|
|
|
|
+ case "doughnutChart":
|
|
|
|
+ drawDonutChart(svg, element, chartWidth, chartHeight)
|
|
|
|
+ break
|
|
|
|
+ default:
|
|
|
|
+ // 占位符
|
|
|
|
+ svg.innerHTML = `<text x="${element.width / 2}" y="${
|
|
|
|
+ element.height / 2
|
|
|
|
+ }" text-anchor="middle" fill="#999" font-size="12px">
|
|
|
|
+ 暂不支持该类型图表
|
|
|
|
+ </text>`
|
|
|
|
+ console.warn("Unsupported chart type:", element.chartType)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ el.appendChild(svg)
|
|
|
|
+ return el
|
|
}
|
|
}
|
|
|
|
|
|
// 创建图表元素
|
|
// 创建图表元素
|
|
@@ -4489,6 +4532,315 @@ const drawCategoryLabels = (svg, categories, padding, groupWidth, barDir) => {
|
|
return labelGroup
|
|
return labelGroup
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+/**
|
|
|
|
+ * 绘制甜甜圈图表
|
|
|
|
+ * @param {SVGElement} svg - SVG容器元素
|
|
|
|
+ * @param {Object} element - 图表配置对象
|
|
|
|
+ * @param {number} chartWidth - 图表宽度
|
|
|
|
+ * @param {number} chartHeight - 图表高度
|
|
|
|
+ */
|
|
|
|
+const drawDonutChart = (svg, element, chartWidth, chartHeight) => {
|
|
|
|
+ // 数据验证
|
|
|
|
+ if (!element?.data?.[0]?.values) return
|
|
|
|
+
|
|
|
|
+ const series = element.data[0]
|
|
|
|
+ const values = series.values.map((v) => Number(v.y) || 0)
|
|
|
|
+ const total = values.reduce((sum, v) => sum + v, 0)
|
|
|
|
+ if (total === 0) return
|
|
|
|
+
|
|
|
|
+ // 基础配置
|
|
|
|
+ const cx = chartWidth / 2
|
|
|
|
+ const cy = chartHeight / 2
|
|
|
|
+ // 保持较大的外半径
|
|
|
|
+ const outerR = (Math.min(chartWidth, chartHeight) / 2) * 0.98
|
|
|
|
+
|
|
|
|
+ // 处理空心大小
|
|
|
|
+ const holeSize = element.holeSize ? parseInt(element.holeSize) : 50
|
|
|
|
+ const innerR = outerR * (holeSize / 100)
|
|
|
|
+
|
|
|
|
+ // 默认颜色和自定义颜色处理
|
|
|
|
+ const defaultColors = [
|
|
|
|
+ "#4e79a7",
|
|
|
|
+ "#f28e2b",
|
|
|
|
+ "#e15759",
|
|
|
|
+ "#76b7b2",
|
|
|
|
+ "#59a14f",
|
|
|
|
+ "#edc949",
|
|
|
|
+ "#af7aa1",
|
|
|
|
+ "#ff9da7",
|
|
|
|
+ ]
|
|
|
|
+ const colors =
|
|
|
|
+ element.colors?.map(
|
|
|
|
+ (color, i) => color || defaultColors[i % defaultColors.length]
|
|
|
|
+ ) || defaultColors
|
|
|
|
+
|
|
|
|
+ // 标签数据
|
|
|
|
+ const labels = series.xlabels || {}
|
|
|
|
+
|
|
|
|
+ // 创建图表组
|
|
|
|
+ const chartGroup = document.createElementNS("http://www.w3.org/2000/svg", "g")
|
|
|
|
+ chartGroup.setAttribute("transform", `translate(${cx},${cy})`)
|
|
|
|
+
|
|
|
|
+ // 绘制扇形
|
|
|
|
+ let startAngle = -Math.PI / 2 // 从顶部开始绘制
|
|
|
|
+ values.forEach((value, index) => {
|
|
|
|
+ if (value <= 0) return
|
|
|
|
+
|
|
|
|
+ const percentage = value / total
|
|
|
|
+ const angle = percentage * Math.PI * 2
|
|
|
|
+ const endAngle = startAngle + angle
|
|
|
|
+
|
|
|
|
+ // 计算路径点 (相对于组中心 0,0)
|
|
|
|
+ const x1 = outerR * Math.cos(startAngle)
|
|
|
|
+ const y1 = outerR * Math.sin(startAngle)
|
|
|
|
+ const x2 = outerR * Math.cos(endAngle)
|
|
|
|
+ const y2 = outerR * Math.sin(endAngle)
|
|
|
|
+ const x3 = innerR * Math.cos(endAngle)
|
|
|
|
+ const y3 = innerR * Math.sin(endAngle)
|
|
|
|
+ const x4 = innerR * Math.cos(startAngle)
|
|
|
|
+ const y4 = innerR * Math.sin(startAngle)
|
|
|
|
+
|
|
|
|
+ // 构建路径
|
|
|
|
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
|
|
|
|
+ const largeArc = angle > Math.PI ? 1 : 0
|
|
|
|
+ const d = [
|
|
|
|
+ `M ${x1} ${y1}`,
|
|
|
|
+ `A ${outerR} ${outerR} 0 ${largeArc} 1 ${x2} ${y2}`,
|
|
|
|
+ `L ${x3} ${y3}`,
|
|
|
|
+ `A ${innerR} ${innerR} 0 ${largeArc} 0 ${x4} ${y4}`,
|
|
|
|
+ "Z",
|
|
|
|
+ ].join(" ")
|
|
|
|
+
|
|
|
|
+ path.setAttribute("d", d)
|
|
|
|
+ path.setAttribute("fill", colors[index % colors.length]) // 确保颜色循环使用
|
|
|
|
+
|
|
|
|
+ // 添加交互效果
|
|
|
|
+ path.style.transition = "transform 0.2s"
|
|
|
|
+ path.addEventListener("mouseover", () => {
|
|
|
|
+ path.style.transform = "scale(1.03)"
|
|
|
|
+ })
|
|
|
|
+ path.addEventListener("mouseout", () => {
|
|
|
|
+ path.style.transform = "scale(1)"
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ chartGroup.appendChild(path)
|
|
|
|
+
|
|
|
|
+ // 添加标签
|
|
|
|
+ const midAngle = startAngle + angle / 2
|
|
|
|
+ const labelR = outerR * 1.1
|
|
|
|
+ const labelX = labelR * Math.cos(midAngle)
|
|
|
|
+ const labelY = labelR * Math.sin(midAngle)
|
|
|
|
+
|
|
|
|
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text")
|
|
|
|
+ text.setAttribute("x", labelX)
|
|
|
|
+ text.setAttribute("y", labelY)
|
|
|
|
+ text.setAttribute("text-anchor", labelX < 0 ? "end" : "start")
|
|
|
|
+ text.setAttribute("dominant-baseline", "middle")
|
|
|
|
+ text.setAttribute("fill", "#555")
|
|
|
|
+ text.setAttribute("font-size", "9")
|
|
|
|
+ text.setAttribute("font-weight", "normal")
|
|
|
|
+ const tspanLabel = document.createElementNS(
|
|
|
|
+ "http://www.w3.org/2000/svg",
|
|
|
|
+ "tspan"
|
|
|
|
+ )
|
|
|
|
+ tspanLabel.setAttribute("x", labelX)
|
|
|
|
+ tspanLabel.textContent = labels[index] || `类别 ${index + 1}`
|
|
|
|
+ text.appendChild(tspanLabel)
|
|
|
|
+ const tspanPercent = document.createElementNS(
|
|
|
|
+ "http://www.w3.org/2000/svg",
|
|
|
|
+ "tspan"
|
|
|
|
+ )
|
|
|
|
+ tspanPercent.setAttribute("x", labelX)
|
|
|
|
+ tspanPercent.setAttribute("dy", "1.2em")
|
|
|
|
+ tspanPercent.textContent = `${(percentage * 100).toFixed(1)}%`
|
|
|
|
+ text.appendChild(tspanPercent)
|
|
|
|
+
|
|
|
|
+ chartGroup.appendChild(text)
|
|
|
|
+
|
|
|
|
+ startAngle = endAngle
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ svg.appendChild(chartGroup)
|
|
|
|
+ const padding = 50
|
|
|
|
+ svg.setAttribute(
|
|
|
|
+ "viewBox",
|
|
|
|
+ `${-cx / 2} ${-cy / 2} ${chartWidth + padding * 2} ${
|
|
|
|
+ chartHeight + padding * 2
|
|
|
|
+ }`
|
|
|
|
+ )
|
|
|
|
+ svg.style.overflow = "visible"
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const drawBarChart = (svg, element, options) => {
|
|
|
|
+ const { padding, chartWidth, chartHeight, barDir, grouping } = options
|
|
|
|
+ const series = element.data
|
|
|
|
+ const categories = series[0].xlabels
|
|
|
|
+ const categoryCount = Object.keys(categories).length
|
|
|
|
+
|
|
|
|
+ // 1. 计算最大值
|
|
|
|
+ let maxValue = 0
|
|
|
|
+ series.forEach((serie) => {
|
|
|
|
+ const seriesMax = Math.max(...serie.values.map((v) => v.y))
|
|
|
|
+ maxValue = Math.max(maxValue, seriesMax)
|
|
|
|
+ })
|
|
|
|
+ maxValue = maxValue * 1.2 // 增加20%空间
|
|
|
|
+
|
|
|
|
+ // 2. 计算柱子布局
|
|
|
|
+ const groupWidth = chartWidth / categoryCount
|
|
|
|
+ const barWidth =
|
|
|
|
+ grouping === "clustered"
|
|
|
|
+ ? (groupWidth * 0.6) / series.length // 分组模式
|
|
|
|
+ : groupWidth * 0.6 // 堆叠模式
|
|
|
|
+ const barSpacing =
|
|
|
|
+ (groupWidth * 0.4) / (grouping === "clustered" ? series.length + 1 : 2)
|
|
|
|
+
|
|
|
|
+ // 3. 绘制坐标轴
|
|
|
|
+ // X轴
|
|
|
|
+ const xAxisPath = document.createElementNS(
|
|
|
|
+ "http://www.w3.org/2000/svg",
|
|
|
|
+ "path"
|
|
|
|
+ )
|
|
|
|
+ xAxisPath.setAttribute(
|
|
|
|
+ "d",
|
|
|
|
+ `M${padding.left},${element.height - padding.bottom} L${
|
|
|
|
+ element.width - padding.right
|
|
|
|
+ },${element.height - padding.bottom}`
|
|
|
|
+ )
|
|
|
|
+ xAxisPath.setAttribute("stroke", "#000")
|
|
|
|
+ xAxisPath.setAttribute("stroke-width", "1")
|
|
|
|
+ svg.appendChild(xAxisPath)
|
|
|
|
+
|
|
|
|
+ // Y轴
|
|
|
|
+ const yAxisPath = document.createElementNS(
|
|
|
|
+ "http://www.w3.org/2000/svg",
|
|
|
|
+ "path"
|
|
|
|
+ )
|
|
|
|
+ yAxisPath.setAttribute(
|
|
|
|
+ "d",
|
|
|
|
+ `M${padding.left},${padding.top} L${padding.left},${
|
|
|
|
+ element.height - padding.bottom
|
|
|
|
+ }`
|
|
|
|
+ )
|
|
|
|
+ yAxisPath.setAttribute("stroke", "#000")
|
|
|
|
+ yAxisPath.setAttribute("stroke-width", "1")
|
|
|
|
+ svg.appendChild(yAxisPath)
|
|
|
|
+
|
|
|
|
+ // 4. 绘制Y轴刻度和网格线
|
|
|
|
+ const yTickCount = 5
|
|
|
|
+ for (let i = 0; i <= yTickCount; i++) {
|
|
|
|
+ const y = padding.top + (chartHeight * i) / yTickCount
|
|
|
|
+ const value = maxValue - (maxValue * i) / yTickCount
|
|
|
|
+
|
|
|
|
+ // 水平网格线
|
|
|
|
+ const gridLine = document.createElementNS(
|
|
|
|
+ "http://www.w3.org/2000/svg",
|
|
|
|
+ "line"
|
|
|
|
+ )
|
|
|
|
+ gridLine.setAttribute("x1", padding.left)
|
|
|
|
+ gridLine.setAttribute("y1", y)
|
|
|
|
+ gridLine.setAttribute("x2", padding.left + chartWidth)
|
|
|
|
+ gridLine.setAttribute("y2", y)
|
|
|
|
+ gridLine.setAttribute("stroke", "#eee")
|
|
|
|
+ gridLine.setAttribute("stroke-width", "1")
|
|
|
|
+ svg.appendChild(gridLine)
|
|
|
|
+
|
|
|
|
+ // 刻度线
|
|
|
|
+ const tick = document.createElementNS("http://www.w3.org/2000/svg", "line")
|
|
|
|
+ tick.setAttribute("x1", padding.left - 6)
|
|
|
|
+ tick.setAttribute("y1", y)
|
|
|
|
+ tick.setAttribute("x2", padding.left)
|
|
|
|
+ tick.setAttribute("y2", y)
|
|
|
|
+ tick.setAttribute("stroke", "#000")
|
|
|
|
+ tick.setAttribute("stroke-width", "1")
|
|
|
|
+ svg.appendChild(tick)
|
|
|
|
+
|
|
|
|
+ // 刻度值
|
|
|
|
+ const label = document.createElementNS("http://www.w3.org/2000/svg", "text")
|
|
|
|
+ label.setAttribute("x", padding.left - 10)
|
|
|
|
+ label.setAttribute("y", y + 4)
|
|
|
|
+ label.setAttribute("text-anchor", "end")
|
|
|
|
+ label.setAttribute("font-size", "12px")
|
|
|
|
+ label.textContent = value.toFixed(1)
|
|
|
|
+ svg.appendChild(label)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 5. 绘制数据条
|
|
|
|
+ series.forEach((serie, serieIndex) => {
|
|
|
|
+ serie.values.forEach((value, index) => {
|
|
|
|
+ const barHeight = (value.y / maxValue) * chartHeight
|
|
|
|
+ const x =
|
|
|
|
+ padding.left +
|
|
|
|
+ groupWidth * index +
|
|
|
|
+ (grouping === "clustered"
|
|
|
|
+ ? barSpacing * (serieIndex + 1) + barWidth * serieIndex
|
|
|
|
+ : barSpacing)
|
|
|
|
+ const y = element.height - padding.bottom - barHeight
|
|
|
|
+
|
|
|
|
+ // 绘制柱子
|
|
|
|
+ const bar = document.createElementNS("http://www.w3.org/2000/svg", "rect")
|
|
|
|
+ bar.setAttribute("x", x)
|
|
|
|
+ bar.setAttribute("y", y)
|
|
|
|
+ bar.setAttribute("width", barWidth)
|
|
|
|
+ bar.setAttribute("height", barHeight)
|
|
|
|
+ bar.setAttribute(
|
|
|
|
+ "fill",
|
|
|
|
+ element.colors[serieIndex] || `hsl(${serieIndex * 60}, 70%, 50%)`
|
|
|
|
+ )
|
|
|
|
+ svg.appendChild(bar)
|
|
|
|
+
|
|
|
|
+ // 数值标签
|
|
|
|
+ if (element.marker) {
|
|
|
|
+ const label = document.createElementNS(
|
|
|
|
+ "http://www.w3.org/2000/svg",
|
|
|
|
+ "text"
|
|
|
|
+ )
|
|
|
|
+ label.setAttribute("x", x + barWidth / 2)
|
|
|
|
+ label.setAttribute("y", y - 5)
|
|
|
|
+ label.setAttribute("text-anchor", "middle")
|
|
|
|
+ label.setAttribute("font-size", "12px")
|
|
|
|
+ label.textContent = value.y.toFixed(1)
|
|
|
|
+ svg.appendChild(label)
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 6. 绘制X轴类别标签
|
|
|
|
+ Object.values(categories).forEach((label, index) => {
|
|
|
|
+ const x = padding.left + groupWidth * (index + 0.5)
|
|
|
|
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text")
|
|
|
|
+ text.setAttribute("x", x)
|
|
|
|
+ text.setAttribute("y", element.height - padding.bottom + 20)
|
|
|
|
+ text.setAttribute("text-anchor", "middle")
|
|
|
|
+ text.setAttribute("font-size", "12px")
|
|
|
|
+ text.textContent = label
|
|
|
|
+ svg.appendChild(text)
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 7. 绘制图例
|
|
|
|
+ series.forEach((serie, index) => {
|
|
|
|
+ const legendX = padding.left + index * 120
|
|
|
|
+ const legendY = 20
|
|
|
|
+
|
|
|
|
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect")
|
|
|
|
+ rect.setAttribute("x", legendX)
|
|
|
|
+ rect.setAttribute("y", legendY)
|
|
|
|
+ rect.setAttribute("width", 15)
|
|
|
|
+ rect.setAttribute("height", 15)
|
|
|
|
+ rect.setAttribute(
|
|
|
|
+ "fill",
|
|
|
|
+ element.colors[index] || `hsl(${index * 60}, 70%, 50%)`
|
|
|
|
+ )
|
|
|
|
+ svg.appendChild(rect)
|
|
|
|
+
|
|
|
|
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text")
|
|
|
|
+ text.setAttribute("x", legendX + 25)
|
|
|
|
+ text.setAttribute("y", legendY + 12)
|
|
|
|
+ text.setAttribute("font-size", "12px")
|
|
|
|
+ text.textContent = serie.key
|
|
|
|
+ svg.appendChild(text)
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
// 绘制网格线
|
|
// 绘制网格线
|
|
const drawGrid = (svg, padding, width, height, maxValue) => {
|
|
const drawGrid = (svg, padding, width, height, maxValue) => {
|
|
// 创建网格组
|
|
// 创建网格组
|