import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material
Page {
id: root
// 统计数据和配置
property var collegeStats: [
{college: "计算机学院", count: 120, color: "#1e88e5"},
{college: "经济管理学院", count: 85, color: "#0d47a1"},
{college: "外语学院", count: 65, color: "#64b5f6"},
{college: "艺术学院", count: 45, color: "#90caf9"}
]
property var hometownStats: [
{hometown: "北京", count: 50, color: "#1e88e5"},
{hometown: "上海", count: 45, color: "#0d47a1"},
{hometown: "广州", count: 38, color: "#64b5f6"},
{hometown: "深圳", count: 35, color: "#90caf9"},
{hometown: "杭州", count: 28, color: "#42a5f5"}
]
property var genderStats: {
"male": {count: 210, color: "#64b5f6"},
"female": {count: 125, color: "#f06292"}
}
property int maxCount: hometownStats.reduce((max, item) => Math.max(max, item.count), 0)
property int activeTab: 0
property var hoveredItem: null
// 卡片式布局
ColumnLayout {
anchors.fill: parent
anchors.margins: 24
spacing: 20
// 标题区域
Rectangle {
Layout.fillWidth: true
height: 80
radius: 12
gradient: Gradient {
GradientStop { position: 0.0; color: "#6a5acd" }
GradientStop { position: 1.0; color: "#4b0082" }
}
Label {
anchors.centerIn: parent
text: "📊 数据统计"
font {
bold: true
pixelSize: 32
family: "Microsoft YaHei"
}
color: "white"
}
}
// 选项卡区域
RowLayout {
Layout.fillWidth: true
spacing: 2
Repeater {
model: ["学院分布", "籍贯分布", "性别比例"]
delegate: Rectangle {
Layout.fillWidth: true
height: 48
radius: 6
color: activeTab === index ? "#6a5acd" : "#3a3a6a"
border.color: "#4a4a7a"
Behavior on color {
ColorAnimation { duration: 300 }
}
Label {
anchors.centerIn: parent
text: modelData
font.bold: activeTab === index
font.pixelSize: 16
color: "white"
}
MouseArea {
anchors.fill: parent
onClicked: activeTab = index
}
// 底部指示器
Rectangle {
visible: activeTab === index
anchors.bottom: parent.bottom
width: parent.width
height: 3
color: "#ffcc00"
}
}
}
}
// 图表区域
Rectangle {
id: chartContainer
Layout.fillWidth: true
Layout.fillHeight: true
radius: 12
color: "#1e1e3a"
border.color: "#4a4a7a"
// 图表加载动画
Rectangle {
id: loadingIndicator
width: 40
height: 40
anchors.centerIn: parent
radius: 20
color: "#6a5acd"
visible: false
RotationAnimation on rotation {
from: 0; to: 360
duration: 1000
loops: Animation.Infinite
}
}
// 学院分布饼图
Canvas {
id: collegeCanvas
anchors.fill: parent
anchors.margins: 20
visible: activeTab === 0
onPaint: drawCollegeChart()
Component.onCompleted: requestPaint()
// 悬停交互
MouseArea {
anchors.fill: parent
hoverEnabled: true
onPositionChanged: {
hoveredItem = findHoveredItem(mouseX, mouseY)
collegeCanvas.requestPaint()
}
onExited: {
hoveredItem = null
collegeCanvas.requestPaint()
}
}
}
// 籍贯分布柱状图
Canvas {
id: hometownCanvas
anchors.fill: parent
anchors.margins: 20
visible: activeTab === 1
onPaint: drawHometownChart()
Component.onCompleted: requestPaint()
// 悬停交互
MouseArea {
anchors.fill: parent
hoverEnabled: true
onPositionChanged: {
hoveredItem = findHoveredBar(mouseX, mouseY)
hometownCanvas.requestPaint()
}
onExited: {
hoveredItem = null
hometownCanvas.requestPaint()
}
}
}
// 性别比例饼图
Canvas {
id: genderCanvas
anchors.fill: parent
anchors.margins: 20
visible: activeTab === 2
onPaint: drawGenderChart()
Component.onCompleted: requestPaint()
// 悬停交互
MouseArea {
anchors.fill: parent
hoverEnabled: true
onPositionChanged: {
hoveredItem = findHoveredGender(mouseX, mouseY)
genderCanvas.requestPaint()
}
onExited: {
hoveredItem = null
genderCanvas.requestPaint()
}
}
}
// 图表标题
Label {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 16
text: {
if (activeTab === 0) return "各学院学生分布";
if (activeTab === 1) return "学生籍贯分布";
return "性别比例";
}
font.bold: true
font.pixelSize: 20
color: "#e0e0ff"
}
// 悬停提示框
Rectangle {
visible: hoveredItem !== null
x: hoveredItem ? hoveredItem.x - width/2 : 0
y: hoveredItem ? hoveredItem.y - height - 10 : 0
width: content.width + 20
height: content.height + 12
radius: 6
color: "#4a4a7a"
border.color: "#6a5acd"
Column {
id: content
anchors.centerIn: parent
spacing: 4
Label {
text: hoveredItem ? hoveredItem.label : ""
font.bold: true
color: "white"
}
Label {
text: hoveredItem ? hoveredItem.value : ""
color: "#ffcc00"
}
}
}
}
// 数据卡片容器
RowLayout {
Layout.fillWidth: true
spacing: 16
StatsCard {
title: "学生总数"
value: "335"
icon: "👥"
color: "#1e88e5"
}
StatsCard {
title: "学院数量"
value: "12"
icon: "🏫"
color: "#0d47a1"
}
StatsCard {
title: "班级数量"
value: "86"
icon: "📚"
color: "#64b5f6"
}
}
}
// 图表绘制函数
function drawCollegeChart() {
var ctx = collegeCanvas.getContext('2d');
ctx.reset();
const centerX = collegeCanvas.width / 2;
const centerY = collegeCanvas.height / 2;
const radius = Math.min(collegeCanvas.width, collegeCanvas.height) * 0.35;
let total = collegeStats.reduce((sum, item) => sum + item.count, 0);
let startAngle = 0;
let hoveredAngle = -1;
// 绘制饼图
collegeStats.forEach((item, index) => {
const sliceAngle = (item.count / total) * 2 * Math.PI;
const isHovered = hoveredItem && hoveredItem.type === "college" && hoveredItem.index === index;
// 绘制扇形
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, isHovered ? radius * 1.05 : radius,
startAngle, startAngle + sliceAngle);
ctx.closePath();
// 填充颜色
ctx.fillStyle = item.color;
ctx.fill();
// 添加光泽效果
if (!isHovered) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
ctx.fill();
}
// 绘制边框
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 1;
ctx.stroke();
// 绘制标签
const midAngle = startAngle + sliceAngle / 2;
const labelRadius = isHovered ? radius * 0.95 : radius * 0.85;
const labelX = centerX + Math.cos(midAngle) * labelRadius;
const labelY = centerY + Math.sin(midAngle) * labelRadius;
const percent = (item.count / total * 100).toFixed(1);
ctx.font = "14px 'Microsoft YaHei'";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "white";
ctx.fillText(`${item.college}\n${percent}%`, labelX, labelY);
startAngle += sliceAngle;
});
// 绘制中心圆
ctx.beginPath();
ctx.arc(centerX, centerY, radius * 0.4, 0, Math.PI * 2);
ctx.fillStyle = "#1a1a2e";
ctx.fill();
ctx.strokeStyle = "#4a4a7a";
ctx.lineWidth = 1;
ctx.stroke();
// 绘制总人数
ctx.font = "bold 18px 'Microsoft YaHei'";
ctx.fillStyle = "#ffcc00";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(total.toString(), centerX, centerY - 10);
ctx.font = "12px 'Microsoft YaHei'";
ctx.fillStyle = "white";
ctx.fillText("总人数", centerX, centerY + 12);
}
function drawHometownChart() {
var ctx = hometownCanvas.getContext('2d');
ctx.reset();
const barAreaHeight = hometownCanvas.height * 0.7;
const barAreaWidth = hometownCanvas.width * 0.8;
const barAreaX = (hometownCanvas.width - barAreaWidth) / 2;
const barAreaY = hometownCanvas.height * 0.15;
const barWidth = barAreaWidth / hometownStats.length;
const barMaxHeight = barAreaHeight * 0.8;
const scaleY = barMaxHeight / maxCount;
// 绘制背景网格
ctx.strokeStyle = "#3a3a5a";
ctx.lineWidth = 0.5;
// 水平网格线
for (let i = 1; i <= 5; i++) {
const y = barAreaY + barAreaHeight - (barAreaHeight / 5) * i;
ctx.beginPath();
ctx.moveTo(barAreaX, y);
ctx.lineTo(barAreaX + barAreaWidth, y);
ctx.stroke();
// 刻度标签
ctx.fillStyle = "#a0a0c0";
ctx.font = "12px 'Microsoft YaHei'";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillText(Math.round(maxCount * i / 5).toString(), barAreaX - 8, y);
}
// 绘制柱状图
hometownStats.forEach((item, index) => {
const barHeight = item.count * scaleY;
const x = barAreaX + index * barWidth + barWidth * 0.1;
const y = barAreaY + barAreaHeight - barHeight;
const isHovered = hoveredItem && hoveredItem.type === "hometown" && hoveredItem.index === index;
// 绘制柱体
const gradient = ctx.createLinearGradient(x, y, x, y + barHeight);
gradient.addColorStop(0, Qt.lighter(item.color, 1.2));
gradient.addColorStop(1, item.color);
ctx.fillStyle = gradient;
// 使用手动绘制圆角矩形替代 roundRect
const cornerRadius = 4;
const barWidthAdjusted = barWidth * 0.8;
ctx.beginPath();
ctx.moveTo(x + cornerRadius, y);
ctx.lineTo(x + barWidthAdjusted - cornerRadius, y);
ctx.quadraticCurveTo(x + barWidthAdjusted, y, x + barWidthAdjusted, y + cornerRadius);
ctx.lineTo(x + barWidthAdjusted, y + barHeight - cornerRadius);
ctx.quadraticCurveTo(x + barWidthAdjusted, y + barHeight, x + barWidthAdjusted - cornerRadius, y + barHeight);
ctx.lineTo(x + cornerRadius, y + barHeight);
ctx.quadraticCurveTo(x, y + barHeight, x, y + barHeight - cornerRadius);
ctx.lineTo(x, y + cornerRadius);
ctx.quadraticCurveTo(x, y, x + cornerRadius, y);
ctx.closePath();
ctx.fill();
// 添加光泽效果
if (isHovered) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.beginPath();
ctx.moveTo(x + cornerRadius, y);
ctx.lineTo(x + barWidthAdjusted - cornerRadius, y);
ctx.quadraticCurveTo(x + barWidthAdjusted, y, x + barWidthAdjusted, y + cornerRadius);
ctx.lineTo(x + barWidthAdjusted, y + barHeight * 0.3);
ctx.lineTo(x, y + barHeight * 0.3);
ctx.lineTo(x, y + cornerRadius);
ctx.quadraticCurveTo(x, y, x + cornerRadius, y);
ctx.closePath();
ctx.fill();
}
// 绘制边框
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 1;
ctx.stroke();
// 绘制数值
ctx.fillStyle = "#ffcc00";
ctx.font = "bold 14px 'Microsoft YaHei'";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText(item.count.toString(), x + barWidthAdjusted / 2, y - 5);
// 绘制标签
ctx.fillStyle = "#e0e0ff";
ctx.font = "12px 'Microsoft YaHei'";
ctx.textBaseline = "top";
ctx.fillText(item.hometown, x + barWidthAdjusted / 2, barAreaY + barAreaHeight + 5);
});
}
function drawGenderChart() {
var ctx = genderCanvas.getContext('2d');
ctx.reset();
const centerX = genderCanvas.width / 2;
const centerY = genderCanvas.height / 2;
const radius = Math.min(genderCanvas.width, genderCanvas.height) * 0.35;
const total = genderStats.male.count + genderStats.female.count;
// 绘制男性部分
const maleAngle = (genderStats.male.count / total) * Math.PI * 2;
const isMaleHovered = hoveredItem && hoveredItem.type === "gender" && hoveredItem.gender === "male";
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, isMaleHovered ? radius * 1.05 : radius, 0, maleAngle);
ctx.closePath();
ctx.fillStyle = genderStats.male.color;
ctx.fill();
// 绘制女性部分
const isFemaleHovered = hoveredItem && hoveredItem.type === "gender" && hoveredItem.gender === "female";
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, isFemaleHovered ? radius * 1.05 : radius, maleAngle, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = genderStats.female.color;
ctx.fill();
// 添加光泽效果
ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
ctx.beginPath();
ctx.arc(centerX, centerY, radius * 0.8, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
// 绘制中心分隔线
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(centerX + Math.cos(0) * radius, centerY + Math.sin(0) * radius);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(centerX + Math.cos(maleAngle) * radius, centerY + Math.sin(maleAngle) * radius);
ctx.stroke();
// 绘制标签
const malePercent = (genderStats.male.count / total * 100).toFixed(1);
const maleLabelX = centerX + Math.cos(maleAngle / 2) * radius * 0.7;
const maleLabelY = centerY + Math.sin(maleAngle / 2) * radius * 0.7;
ctx.fillStyle = "white";
ctx.font = "bold 16px 'Microsoft YaHei'";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(`男生\n${malePercent}%`, maleLabelX, maleLabelY);
const femalePercent = (genderStats.female.count / total * 100).toFixed(1);
const femaleAngle = maleAngle + (Math.PI * 2 - maleAngle) / 2;
const femaleLabelX = centerX + Math.cos(femaleAngle) * radius * 0.7;
const femaleLabelY = centerY + Math.sin(femaleAngle) * radius * 0.7;
ctx.fillText(`女生\n${femalePercent}%`, femaleLabelX, femaleLabelY);
}
// 悬停检测函数
function findHoveredItem(x, y) {
const centerX = collegeCanvas.width / 2;
const centerY = collegeCanvas.height / 2;
const radius = Math.min(collegeCanvas.width, collegeCanvas.height) * 0.35;
// 计算鼠标相对于圆心的角度
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > radius * 1.05) return null;
const angle = Math.atan2(dy, dx);
const normalizedAngle = angle < 0 ? angle + Math.PI * 2 : angle;
let currentAngle = 0;
const total = collegeStats.reduce((sum, item) => sum + item.count, 0);
for (let i = 0; i < collegeStats.length; i++) {
const item = collegeStats[i];
const sliceAngle = (item.count / total) * 2 * Math.PI;
if (normalizedAngle >= currentAngle && normalizedAngle < currentAngle + sliceAngle) {
const midAngle = currentAngle + sliceAngle / 2;
const labelX = centerX + Math.cos(midAngle) * radius * 0.8;
const labelY = centerY + Math.sin(midAngle) * radius * 0.8;
return {
type: "college",
index: i,
label: item.college,
value: item.count,
x: labelX,
y: labelY
};
}
currentAngle += sliceAngle;
}
return null;
}
function findHoveredBar(x, y) {
const barAreaHeight = hometownCanvas.height * 0.7;
const barAreaWidth = hometownCanvas.width * 0.8;
const barAreaX = (hometownCanvas.width - barAreaWidth) / 2;
const barAreaY = hometownCanvas.height * 0.15;
const barWidth = barAreaWidth / hometownStats.length;
for (let i = 0; i < hometownStats.length; i++) {
const barX = barAreaX + i * barWidth;
if (x >= barX && x <= barX + barWidth) {
const barY = barAreaY;
return {
type: "hometown",
index: i,
label: hometownStats[i].hometown,
value: hometownStats[i].count,
x: barX + barWidth / 2,
y: barY
};
}
}
return null;
}
function findHoveredGender(x, y) {
const centerX = genderCanvas.width / 2;
const centerY = genderCanvas.height / 2;
const radius = Math.min(genderCanvas.width, genderCanvas.height) * 0.35;
// 计算鼠标相对于圆心的角度
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > radius * 1.05) return null;
const angle = Math.atan2(dy, dx);
const normalizedAngle = angle < 0 ? angle + Math.PI * 2 : angle;
const total = genderStats.male.count + genderStats.female.count;
const maleAngle = (genderStats.male.count / total) * Math.PI * 2;
if (normalizedAngle <= maleAngle) {
return {
type: "gender",
gender: "male",
label: "男生",
value: genderStats.male.count,
x: centerX + Math.cos(maleAngle / 2) * radius * 0.7,
y: centerY + Math.sin(maleAngle / 2) * radius * 0.7
};
} else {
return {
type: "gender",
gender: "female",
label: "女生",
value: genderStats.female.count,
x: centerX + Math.cos(maleAngle + (Math.PI * 2 - maleAngle) / 2) * radius * 0.7,
y: centerY + Math.sin(maleAngle + (Math.PI * 2 - maleAngle) / 2) * radius * 0.7
};
}
}
// 卡片组件
component StatsCard: Rectangle {
property string title
property string value
property string icon
property color color
Layout.preferredWidth: 180
Layout.preferredHeight: 120
radius: 12
color: "#252541"
border.color: "#4a4a7a"
// 悬停效果
Behavior on scale {
NumberAnimation { duration: 200 }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: parent.scale = 1.05
onExited: parent.scale = 1.0
}
// 装饰边框
Rectangle {
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Qt.lighter(parent.color, 1.3)
border.width: 1
}
// 内部布局
Column {
anchors.centerIn: parent
spacing: 10
// 图标
Rectangle {
width: 50
height: 50
radius: 25
color: Qt.lighter(parent.parent.color, 1.3)
anchors.horizontalCenter: parent.horizontalCenter
Label {
anchors.centerIn: parent
text: parent.parent.parent.icon
font.pixelSize: 24
}
}
// 标题
Label {
text: parent.parent.parent.title
font.pixelSize: 14
color: "#a0a0c0"
anchors.horizontalCenter: parent.horizontalCenter
}
// 数值
Label {
text: parent.parent.parent.value
font.bold: true
font.pixelSize: 24
color: "#ffcc00"
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}