Files
Situation-Awareness-Platfor…/src/views/Jyh/ecosystem/index.vue

4172 lines
117 KiB
Vue
Raw Normal View History

<template>
<div class="index-module__NV_5cW__container">
<!-- 头部区域标题副标题武汉核心数据统计 -->
<div class="index-module__NV_5cW__header">
<div class="index-module__NV_5cW__floatingElements">
<div class="index-module__NV_5cW__floatingElement"></div>
<div class="index-module__NV_5cW__floatingElement"></div>
<div class="index-module__NV_5cW__floatingElement"></div>
</div>
<div class="index-module__NV_5cW__titleSection">
<div class="index-module__NV_5cW__titleContainer">
<h1 class="index-module__NV_5cW__title">开源生态与贡献价值全景</h1>
<div class="index-module__NV_5cW__titleBadge">WOS</div>
</div>
2025-12-18 14:09:34 +08:00
<!-- <div class="index-module__NV_5cW__subtitle">-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"-->
<!-- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"-->
<!-- class="lucide lucide-users" aria-hidden="true">-->
<!-- <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>-->
<!-- <path d="M16 3.128a4 4 0 0 1 0 7.744"></path>-->
<!-- <path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>-->
<!-- <circle cx="9" cy="7" r="4"></circle>-->
<!-- </svg>-->
<!-- <span>汇聚武汉开源力量共建城市开源生态</span>-->
<!-- </div>-->
<!-- 武汉4项核心开源数据统计 -->
<div class="index-module__NV_5cW__statsBar">
<div class="index-module__NV_5cW__stat">
2025-12-18 14:09:34 +08:00
<span class="index-module__NV_5cW__statNumber">200 +</span>
<span class="index-module__NV_5cW__statLabel">活跃开发者</span>
</div>
<div class="index-module__NV_5cW__statDivider"></div>
<div class="index-module__NV_5cW__stat">
2025-12-18 14:09:34 +08:00
<span class="index-module__NV_5cW__statNumber">300 +</span>
<span class="index-module__NV_5cW__statLabel">开源项目</span>
</div>
<div class="index-module__NV_5cW__statDivider"></div>
<div class="index-module__NV_5cW__stat">
2025-12-18 14:09:34 +08:00
<span class="index-module__NV_5cW__statNumber">500 +</span>
<span class="index-module__NV_5cW__statLabel">开源企业</span>
</div>
<div class="index-module__NV_5cW__statDivider"></div>
<div class="index-module__NV_5cW__stat">
2025-12-18 14:09:34 +08:00
<span class="index-module__NV_5cW__statNumber">2800亿</span>
<span class="index-module__NV_5cW__statLabel">产业估值</span>
</div>
</div>
</div>
</div>
<!-- 内容区域ECharts图表模块 + 项目列表 -->
<div class="index-module__NV_5cW__content">
<!-- ECharts图表区域 -->
<div class="index-module__NV_5cW__chartsSection">
<div class="index-module__NV_5cW__sectionHeader">
<h2 class="index-module__NV_5cW__sectionTitle">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
2025-12-18 14:09:34 +08:00
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-chart-pie" aria-hidden="true">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<span>开源生态与贡献价值全景数据可视化</span>
</h2>
</div>
<!-- 图表网格布局新增多个模块 -->
<div class="index-module__NV_5cW__chartsGrid">
2025-12-18 14:09:34 +08:00
<!-- 1. 开源企业贡献榜-->
<div class="index-module__NV_5cW__chartCard list-card">
2025-12-18 14:09:34 +08:00
<div class="chartCard__header">
<div class="title-with-stats">
<h3 class="chartCard__title">全球开源企业贡献榜 TOP 30</h3>
<div class="enterprise-stats" v-if="enterpriseFilter !== 'all'">
<span class="stat-badge">
平均分: {{ avgScore }}
</span>
<span class="stat-badge">
总仓库: {{ totalRepos }}
</span>
</div>
</div>
<div class="enterprise-filter">
<span :class="['filter-tag', { active: enterpriseFilter === 'all' }]"
@click="changeEnterpriseFilter('all')">
全部 ({{ allEnterprises.length }})
</span>
<span :class="['filter-tag', { active: enterpriseFilter === 'China' }]"
@click="changeEnterpriseFilter('China')">
🇨🇳 中国 ({{ chinaCount }})
</span>
<span :class="['filter-tag', { active: enterpriseFilter === 'USA' }]"
@click="changeEnterpriseFilter('USA')">
🇺🇸 美国 ({{ usaCount }})
</span>
</div>
</div>
<div class="chartCard__container list-container" ref="scrollContainer1" @mouseenter="pauseScroll(1)"
@mouseleave="resumeScroll(1)">
<div class="scroll-indicator">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M12 5v14M19 12l-7 7-7-7" />
</svg>
<span>悬停暂停滚动</span>
</div>
<div class="project-list enterprise-list">
<div class="project-item enterprise-item" v-for="(item, index) in excellentProjects" :key="index"
:title="`${item.name}\n全球排名: #${item.rank}\n综合得分: ${item.score}\n仓库数: ${item.repos}\n开发者: ${item.devs}\n国家: ${item.country}`">
<span :class="['enterprise-rank', getRankClass(item.displayRank || item.rank)]">
{{ getRankText(item.displayRank || item.rank) }}
</span>
<div class="enterprise-info">
<span class="enterprise-name">{{ item.name }}</span>
<div class="enterprise-meta">
<span class="meta-item">{{ getCountryFlag(item.country) }}</span>
<span class="meta-item">{{ item.repos }} 仓库</span>
<span class="meta-item meta-devs">{{ formatDevs(item.devs) }} 开发者</span>
</div>
</div>
2025-12-18 14:09:34 +08:00
<div class="score-bar-container">
<div :class="['score-bar', getScoreBarClass(item.score)]"
:style="{ width: (30 + (item.score - 60) / 36 * 70) + '%' }">
</div>
</div>
<span :class="['enterprise-score', getScoreClass(item.score)]">
{{ item.score }}
</span>
</div>
</div>
</div>
</div>
2025-12-18 14:09:34 +08:00
<!-- 2. 基础设施覆盖率多层环形图-->
<div class="index-module__NV_5cW__chartCard">
2025-12-18 14:09:34 +08:00
<h3 class="chartCard__title">基础设施覆盖率</h3>
<div class="chartCard__container">
2025-12-18 14:09:34 +08:00
<div class="num-empty" v-if="isEmpty.infrastructure"></div>
<div v-else id="infrastructureChart" class="chart"></div>
</div>
</div>
2025-12-18 14:09:34 +08:00
<!-- 3. 高校开源社团/俱乐部数量-->
<div class="index-module__NV_5cW__chartCard">
2025-12-18 14:09:34 +08:00
<h3 class="chartCard__title">高校开源社团/俱乐部数量</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.community"></div>
<div v-else id="communityChart" class="chart"></div>
</div>
</div>
2025-12-18 14:09:34 +08:00
<!-- 4. 社区治理健康度对比-->
<div class="index-module__NV_5cW__chartCard">
2025-12-18 14:09:34 +08:00
<h3 class="chartCard__title">社区治理健康度对比</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.ossCompass"></div>
<div v-else id="ossCompassChart" class="chart"></div>
</div>
</div>
2025-12-18 14:09:34 +08:00
<!-- 5. 语言与技术竞争-->
<div class="index-module__NV_5cW__chartCard">
2025-12-18 14:09:34 +08:00
<div class="chartCard__header">
<h3 class="chartCard__title">语言与技术竞争</h3>
<div class="language-tab" @click="changeLanguageTab">
<span :class="{ active: languageTab === 'mainstream' }">主流语言</span>
<span :class="{ active: languageTab === 'emerging' }">新兴语言</span>
</div>
</div>
<div class="chartCard__container">
2025-12-18 14:09:34 +08:00
<div class="num-empty" v-if="isEmpty.languageTech"></div>
<div v-else id="languageTechChart" class="chart"></div>
</div>
</div>
2025-12-18 14:09:34 +08:00
<!-- 6. 明星开发者聚焦列表-->
<div class="index-module__NV_5cW__chartCard list-card">
2025-12-18 14:09:34 +08:00
<h3 class="chartCard__title">明星开发者聚焦</h3>
<!-- 新增ref + 鼠标事件 -->
2025-12-18 14:09:34 +08:00
<div class="chartCard__container list-container developer-list-container" ref="scrollContainer2"
@mouseenter="pauseScroll(2)" @mouseleave="resumeScroll(2)">
<div class="project-list developer-list">
<div class="project-item developer-item" v-for="(item, index) in excellentProjects2025" :key="index">
<div class="dev-avatar">
<img :src="item.avatar" :alt="item.name" class="avatar-img" @error="handleImageError" />
</div>
<span class="project-rank dev-rank">{{ item.desc }}</span>
<div class="project-info dev-info">
<span class="project-name dev-name">{{ item.name }}</span>
<span class="project-desc dev-location">📍 {{ item.org }}</span>
</div>
2025-12-18 14:09:34 +08:00
<span class="project-org dev-followers">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
</svg>
{{ formatFollowers(item.followers) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
2025-12-18 14:09:34 +08:00
<!-- 开发者活力波形图 -->
<div class="index-module__NV_5cW__vitalitySection">
<h3 class="vitality__title">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
</svg>
<span>开发者活力波形图</span>
</h3>
<div class="vitality__container">
<div id="vitalityChart" class="chart"></div>
</div>
</div>
<!-- 开源项目热力图 -->
<div class="index-module__NV_5cW__vitalitySection heatmap-section">
<div class="heatmap-header">
<div class="title-row">
<h3 class="vitality__title">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z">
</path>
</svg>
2025-12-18 14:09:34 +08:00
<span>开源项目热力图</span>
</h3>
<div class="label-toggle" @click="toggleLabels">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
2025-12-18 14:09:34 +08:00
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2v20M2 12h20"></path>
</svg>
2025-12-18 14:09:34 +08:00
<span>{{ showAllLabels ? '隐藏标签' : '显示全部' }}</span>
</div>
2025-12-18 14:09:34 +08:00
</div>
<div class="heatmap-stats">
<div class="stat-item">
<span class="stat-value">{{ heatmapData.length }}</span>
<span class="stat-label">项目总数</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ topProject.name }}</span>
<span class="stat-label">领先项目</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<span class="stat-value">{{ totalDevs }}</span>
<span class="stat-label">总开发者数</span>
</div>
</div>
</div>
<div class="vitality__container heatmap-container">
<div class="heatmap-legend">
<div class="legend-item">
<div class="legend-icon bubble-small"></div>
<span>少量开发者</span>
</div>
<div class="legend-item">
<div class="legend-icon bubble-medium"></div>
<span>中等规模</span>
</div>
<div class="legend-item">
<div class="legend-icon bubble-large"></div>
<span>大量开发者</span>
</div>
</div>
<div id="heatmapChart" class="chart"></div>
</div>
</div>
<!-- 中国开源人才密度地图 -->
<div class="index-module__NV_5cW__vitalitySection map-section">
<h3 class="vitality__title">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<span>🌍 中国开源人才密度地图</span>
</h3>
<div class="map-container">
<div id="talentMapChart" class="chart"></div>
<!-- 右侧排行榜 -->
<div class="map-rank-panel">
<div class="panel-title">🏆 开源人才数量排行 TOP 5</div>
<div class="rank-list">
<div class="rank-item" v-for="(item, index) in topRegions" :key="index">
<span :class="['rank-num', 'rank-' + (index + 1)]">{{ index + 1 }}</span>
<span class="rank-name">{{ item.name }}</span>
<span class="rank-score">{{ item.score }} 万人</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
2025-12-18 14:09:34 +08:00
import { onMounted, ref, nextTick, watch, onUnmounted, computed } from 'vue';
import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize';
// 页面尺寸响应式
const { widthType } = usePageResize();
2025-12-18 14:09:34 +08:00
// 1. 全球开源企业贡献榜数据(来自 EnterpriseContributionRanking.vue
const rawEnterpriseData = [
{ name: 'Microsoft', openrank: 177654.43, repos: 9211, devs: 14954, country: 'USA' },
{ name: 'Huawei', openrank: 108690.66, repos: 11407, devs: 16413, country: 'China' },
{ name: 'Google', openrank: 64156.6, repos: 3426, devs: 64115, country: 'USA' },
{ name: 'Amazon', openrank: 54776.3, repos: 7850, devs: 35802, country: 'USA' },
{ name: 'Meta', openrank: 34811.3, repos: 1410, devs: 31912, country: 'USA' },
{ name: 'RedHat', openrank: 28004.37, repos: 736, devs: 3626, country: 'USA' },
{ name: 'Mozilla', openrank: 27085.14, repos: 1792, devs: 17242, country: 'USA' },
{ name: 'Alibaba', openrank: 24744.15, repos: 5570, devs: 18661, country: 'China' },
{ name: 'IBM', openrank: 21165.28, repos: 626, devs: 5658, country: 'USA' },
{ name: 'Elastic', openrank: 21472.45, repos: 2437, devs: 17735, country: 'USA' },
{ name: 'Nabu Casa', openrank: 18499.45, repos: 101, devs: 27413, country: 'USA' },
{ name: 'Grafana Labs', openrank: 18237.03, repos: 756, devs: 10925, country: 'USA' },
{ name: 'Ant Group', openrank: 17916.45, repos: 1251, devs: 13760, country: 'China' },
{ name: 'DataDog', openrank: 17795.41, repos: 935, devs: 5401, country: 'USA' },
{ name: 'Intel', openrank: 17725.74, repos: 1046, devs: 6142, country: 'USA' },
{ name: 'HashiCorp', openrank: 17229.81, repos: 745, devs: 17068, country: 'USA' },
{ name: 'Hugging Face', openrank: 16909.25, repos: 277, devs: 15021, country: 'USA' },
{ name: 'NVIDIA', openrank: 16082.9, repos: 850, devs: 12362, country: 'USA' },
{ name: 'Baidu', openrank: 15695.3, repos: 408, devs: 9386, country: 'China' },
{ name: 'VMWare', openrank: 14627.44, repos: 634, devs: 11794, country: 'USA' },
{ name: 'Odoo', openrank: 12959.59, repos: 27, devs: 2678, country: 'Belgium' },
{ name: 'Dio Academia', openrank: 11231.13, repos: 169, devs: 24070, country: 'Portugal' },
{ name: 'Oracle', openrank: 10799.13, repos: 528, devs: 37959, country: 'USA' },
{ name: 'Yandex', openrank: 10025.79, repos: 283, devs: 10112, country: 'Germany' },
{ name: 'NextCloud', openrank: 10029.67, repos: 229, devs: 39516, country: 'Russia' },
{ name: 'ByteDance', openrank: 8310.37, repos: 652, devs: 5989, country: 'China' },
{ name: 'WordPress', openrank: 8267.62, repos: 176, devs: 777, country: 'USA' },
{ name: 'Vercel', openrank: 8214.69, repos: 170, devs: 13420, country: 'USA' },
{ name: 'LangChain', openrank: 8108.64, repos: 135, devs: 10099, country: 'USA' },
{ name: 'ConsenSys', openrank: 8071.43, repos: 640, devs: 4686, country: 'USA' }
];
// 计算综合得分并排序
const calculateEnterpriseScore = () => {
const maxOpenrank = Math.max(...rawEnterpriseData.map(d => d.openrank));
const maxRepos = Math.max(...rawEnterpriseData.map(d => d.repos));
const maxDevs = Math.max(...rawEnterpriseData.map(d => d.devs));
const scored = rawEnterpriseData.map(item => {
const openrankScore = Math.sqrt(item.openrank / maxOpenrank) * 100;
const reposScore = Math.sqrt(item.repos / maxRepos) * 100;
const devsScore = Math.sqrt(item.devs / maxDevs) * 100;
const compositeScore = openrankScore * 0.4 + reposScore * 0.25 + devsScore * 0.35;
return { ...item, rawScore: compositeScore };
});
const sorted = scored.sort((a, b) => b.rawScore - a.rawScore);
const maxScore = sorted[0].rawScore;
const minScore = sorted[sorted.length - 1].rawScore;
return sorted.map((item, index) => {
let normalizedScore = ((item.rawScore - minScore) / (maxScore - minScore)) * 36 + 60;
normalizedScore = Math.max(60, Math.min(96, normalizedScore - index * 0.15));
return {
rank: index + 1,
name: item.name,
score: Math.round(normalizedScore * 10) / 10,
repos: item.repos,
country: item.country,
devs: item.devs
};
});
};
const allEnterprises = calculateEnterpriseScore();
const enterpriseFilter = ref<'all' | 'China' | 'USA'>('all');
// 计算各国企业数量
const chinaCount = computed(() => allEnterprises.filter(item => item.country === 'China').length);
const usaCount = computed(() => allEnterprises.filter(item => item.country === 'USA').length);
// 切换筛选并重启滚动
const changeEnterpriseFilter = (filter: 'all' | 'China' | 'USA') => {
enterpriseFilter.value = filter;
// 重启滚动
nextTick(() => {
pauseScroll(1);
setTimeout(() => autoScroll(1), 100);
});
};
// 计算筛选后的统计数据
const avgScore = computed(() => {
if (enterpriseFilter.value === 'all') return 0;
const filtered = excellentProjects.value;
if (filtered.length === 0) return 0;
const sum = filtered.reduce((acc, item) => acc + item.score, 0);
return (sum / filtered.length).toFixed(1);
});
const totalRepos = computed(() => {
if (enterpriseFilter.value === 'all') return 0;
return excellentProjects.value.reduce((acc, item) => acc + item.repos, 0).toLocaleString();
});
// 根据筛选条件过滤企业数据
const excellentProjects = computed(() => {
if (enterpriseFilter.value === 'all') {
return allEnterprises;
}
const filtered = allEnterprises.filter(item => item.country === enterpriseFilter.value);
// 重新计算排名
return filtered.map((item, index) => ({
...item,
displayRank: index + 1
}));
});
2025-12-18 14:09:34 +08:00
// 2. 明星开发者数据基于GitHub中国区Top开发者
const excellentProjects2025 = ref([
2025-12-18 14:09:34 +08:00
{ name: '稚晖', desc: 'TOP 1', org: 'Shanghai', followers: 82738, avatar: 'https://github.com/peng-zhihui.png' },
{ name: 'Crypto Michael', desc: 'TOP 2', org: 'Beijing, China', followers: 37622, avatar: 'https://github.com/michaelliao.png' },
{ name: '代码家', desc: 'TOP 3', org: 'Beijing, China', followers: 24743, avatar: 'https://github.com/daimajia.png' },
{ name: 'Jackson Tian', desc: 'TOP 4', org: 'Hangzhou, China', followers: 21267, avatar: 'https://github.com/JacksonTian.png' },
{ name: '云风', desc: 'TOP 5', org: 'China', followers: 21192, avatar: 'https://github.com/cloudwu.png' },
{ name: 'Fengda Huang', desc: 'TOP 6', org: 'Shanghai / Hangzhou', followers: 20229, avatar: 'https://github.com/phodal.png' },
{ name: '程序员鱼皮', desc: 'TOP 7', org: 'Shanghai', followers: 18461, avatar: 'https://github.com/liyupi.png' },
{ name: 'Trinea', desc: 'TOP 8', org: 'HangZhou', followers: 17019, avatar: 'https://github.com/Trinea.png' },
{ name: 'lifesinger', desc: 'TOP 9', org: 'Hangzhou, China', followers: 15894, avatar: 'https://github.com/lifesinger.png' },
{ name: 'stormzhang', desc: 'TOP 10', org: 'Shanghai, China', followers: 15889, avatar: 'https://github.com/stormzhang.png' },
{ name: 'Wenli Zhang', desc: 'TOP 11', org: 'Shanghai, China', followers: 15680, avatar: 'https://github.com/Ovilia.png' },
{ name: 'Wei Wang', desc: 'TOP 12', org: 'Yokohama / China', followers: 15532, avatar: 'https://github.com/onevcat.png' },
{ name: 'astaxie', desc: 'TOP 13', org: 'Shanghai, China', followers: 15022, avatar: 'https://github.com/astaxie.png' },
{ name: '司徒正美', desc: 'TOP 14', org: 'China', followers: 14742, avatar: 'https://github.com/RubyLouvre.png' },
{ name: '云谦', desc: 'TOP 15', org: 'HangZhou, China', followers: 14466, avatar: 'https://github.com/sorrycc.png' },
{ name: 'cangdu', desc: 'TOP 16', org: 'Shanghai, China', followers: 13728, avatar: 'https://github.com/bailicangdu.png' },
{ name: 'Huang Haiguang', desc: 'TOP 17', org: 'Qingdao, China', followers: 13422, avatar: 'https://github.com/fengdu78.png' },
{ name: '花裤衩', desc: 'TOP 18', org: 'Shanghai, China', followers: 13236, avatar: 'https://github.com/PanJiaChen.png' },
{ name: '破娃酱', desc: 'TOP 19', org: 'China', followers: 13203, avatar: 'https://github.com/breakwa11.png' },
{ name: 'Draven', desc: 'TOP 20', org: 'Beijing, China', followers: 13094, avatar: 'https://github.com/draveness.png' }
]);
// 3. 图表数据定义
const chartData = ref({
2025-12-18 14:09:34 +08:00
// 基础设施覆盖率(来自 InfrastructureCoverageRings.vue
infrastructure: [
{
name: '镜像站规模',
value: 85,
color: '#00F0FF',
radius: ['70%', '80%']
},
{
name: '代码托管平台规模',
value: 90,
color: '#FFAB00',
radius: ['50%', '60%']
},
{
name: '构建平台活跃度',
value: 40,
color: '#B20003',
radius: ['30%', '40%']
}
],
2025-12-18 14:09:34 +08:00
// 高校开源社团/俱乐部数据(来自 UniversityClubStatsChart.vue
universityClubs: [
// 华东地区
{ university: '上海交通大学', club: 'SJTU-LUG (Linux User Group)' },
{ university: '上海交通大学', club: '开放原子开源俱乐部' },
{ university: '复旦大学', club: 'Fudan LUG' },
{ university: '复旦大学', club: '开放原子开源俱乐部' },
{ university: '浙江大学', club: 'ZJU-LUG' },
{ university: '浙江大学', club: 'AAA (Azure Availability Association)' },
{ university: '浙江大学', club: '开放原子开源俱乐部' },
{ university: '南京大学', club: 'NJU-LUG' },
{ university: '南京大学', club: 'eScience 协会' },
{ university: '南京大学', club: '开放原子开源俱乐部' },
{ university: '中国科学技术大学', club: 'USTC-LUG' },
{ university: '中国科学技术大学', club: 'VLAB' },
{ university: '同济大学', club: 'Tongji LUG' },
{ university: '同济大学', club: '开放原子开源俱乐部' },
{ university: '华东师范大学', club: 'ECNU LUG' },
// 华北地区
{ university: '清华大学', club: 'TUNA (清华大学学生网络与开源软件协会)' },
{ university: '清华大学', club: '开放原子开源俱乐部' },
{ university: '北京大学', club: 'PKUOSC (北京大学开源软件协会)' },
{ university: '北京大学', club: 'PKU-LUG' },
{ university: '北京邮电大学', club: 'BYR Team' },
{ university: '北京邮电大学', club: '开放原子开源俱乐部' },
{ university: '北京航空航天大学', club: 'BUAA LUG' },
{ university: '北京航空航天大学', club: '开放原子开源俱乐部' },
{ university: '中国科学院大学', club: 'UCAS LUG' },
{ university: '天津大学', club: 'TJU LUG' },
{ university: '南开大学', club: 'NKU LUG' },
{ university: '哈尔滨工业大学', club: 'HIT LUG' },
{ university: '哈尔滨工业大学', club: '开放原子开源俱乐部' },
// 华中/华南地区
{ university: '华中科技大学', club: 'HUST LUG' },
{ university: '华中科技大学', club: '联创团队' },
{ university: '华中科技大学', club: '开放原子开源俱乐部' },
{ university: '武汉大学', club: 'WHU LUG' },
{ university: '武汉大学', club: '网络安全协会' },
{ university: '中山大学', club: 'SYSU LUG' },
{ university: '南方科技大学', club: 'SUSTech CRA' },
{ university: '南方科技大学', club: '开放原子开源俱乐部' },
{ university: '深圳大学', club: 'SZU LUG' },
{ university: '深圳大学', club: '开放原子开源俱乐部' },
// 西部/其他地区
{ university: '电子科技大学', club: 'UESTC LUG' },
{ university: '电子科技大学', club: '星辰工作室' },
{ university: '电子科技大学', club: '开放原子开源俱乐部' },
{ university: '四川大学', club: 'SCU LUG' },
{ university: '西安电子科技大学', club: 'XDU LUG' },
{ university: '西安电子科技大学', club: '开放原子开源俱乐部' },
{ university: '兰州大学', club: 'LZU LUG' },
{ university: '兰州大学', club: '开放原子开源俱乐部' },
{ university: '重庆邮电大学', club: 'Redrock 团队' },
{ university: '重庆邮电大学', club: 'LUG' },
{ university: '湖南大学', club: 'HNU LUG' },
{ university: '中南大学', club: 'CSU LUG' },
{ university: '山东大学', club: 'SDU LUG' },
{ university: '厦门大学', club: 'XMU LUG' }
],
// 社区治理健康度对比(来自 CommunityGovernanceHealth.vue
communityHealth: {
indicators: ['PR 合并率', 'Issue 响应时间', 'CI/CD 使用率', '平均代码质量得分', '安全审计频率', '贡献者多样性'],
github: [82, 88, 78, 85, 75, 80],
gitcode: [68, 75, 55, 72, 60, 65]
},
2025-12-18 14:09:34 +08:00
// 语言与技术竞争(南丁格尔玫瑰图数据)
languageTech: {
2025-12-18 14:09:34 +08:00
// 主流语言数据基于2025年数据增长率为2024→2025的变化
mainstream: [
{ name: 'JavaScript', value: 61, growth: 0 },
{ name: 'Python', value: 57, growth: 0 },
{ name: 'HTML/CSS', value: 52, growth: 1.96 },
{ name: 'Java', value: 49, growth: 6.52 },
{ name: 'SQL', value: 47, growth: -2.08 },
{ name: 'Shell', value: 36, growth: 0 }
],
// 新兴语言数据基于2025年数据增长率为2024→2025的变化
emerging: [
{ name: 'TypeScript', value: 42, growth: 13.51 },
{ name: 'C++', value: 25, growth: 0 },
{ name: 'C#', value: 21, growth: -4.55 },
{ name: 'Go', value: 20, growth: 11.11 },
{ name: 'Kotlin', value: 18, growth: 28.57 },
{ name: 'C', value: 18, growth: 0 }
]
}
});
2025-12-18 14:09:34 +08:00
// 处理高校社团数据
const processUniversityClubsData = () => {
const counts: { [key: string]: { count: number; clubs: string[] } } = {};
chartData.value.universityClubs.forEach(item => {
if (!counts[item.university]) {
counts[item.university] = { count: 0, clubs: [] };
}
counts[item.university].count++;
counts[item.university].clubs.push(item.club);
});
// 转换为数组并排序
const sorted = Object.entries(counts)
.map(([name, data]) => ({
name,
value: data.count,
clubs: data.clubs
}))
.sort((a, b) => b.value - a.value);
return {
universities: sorted.slice(0, 10).map(item => item.name), // 取前10名避免X轴过挤
values: sorted.slice(0, 10).map(item => item.value),
details: sorted.slice(0, 10).reduce((acc, item) => {
acc[item.name] = item.clubs;
return acc;
}, {} as { [key: string]: string[] })
};
};
const communityData = computed(() => processUniversityClubsData());
// 空数据标识
const isEmpty = ref({
2025-12-18 14:09:34 +08:00
infrastructure: false,
community: false,
2025-12-18 14:09:34 +08:00
communityHealth: false,
languageTech: false
});
2025-12-18 14:09:34 +08:00
// 语言图表Tab状态
const languageTab = ref<'mainstream' | 'emerging'>('mainstream');
// 开发者活力数据
const vitalityData = ref({
timeData: ["2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024"],
total: [20, 50, 100, 200, 300, 400, 600, 800, 1000, 1200, 1350],
new: [20, 30, 50, 100, 100, 100, 200, 200, 200, 200, 150]
});
// 开源项目热力图数据 [OpenRank, 活跃仓库数, 活跃开发者数, 项目名称, 所属组织]
const heatmapData = ref([
[67538.71, 29433, 1409, 'OpenHarmony', 'Huawei'],
[55000, 15000, 6000, 'Linux', 'Linux Foundation'],
[60000, 8000, 7000, 'Android', 'Google'],
[52000, 20000, 5500, 'Chromium', 'Google'],
[38000, 16000, 4500, 'TensorFlow', 'Google'],
[42000, 13000, 5000, 'React', 'Meta'],
[45417.67, 18000, 8629, 'Azure', 'Microsoft'],
[35000, 22000, 5000, 'Kubernetes', 'CNCF'],
[21844.62, 12340, 57, 'LLM', '发芽工具'],
[25241.13, 8000, 83, 'NixOS', '-'],
[22961.09, 4500, 9377, 'openEuler', 'Huawei'],
[17795.41, 2792, 975, 'DataDog', 'DataDog'],
[25598.21, 2082, 282, '.Net', '操作系统'],
[7405.46, 9639, 3, 'Expensify', 'Expensify'],
[8862.2, 4625, 120, 'PaddlePaddle', 'Baidu'],
[12959.59, 2363, 1, 'Odoo', 'Odoo'],
[12326.76, 881, 179, 'Rust', 'Rust Foundation'],
[6778.7, 400, 479, 'CloudHarmony', '企业应用']
]);
// 计算热力图统计数据
const topProject = computed(() => {
const sorted = [...heatmapData.value].sort((a, b) => b[0] - a[0]);
return { name: sorted[0][3], openrank: sorted[0][0] };
});
const totalDevs = computed(() => {
const total = heatmapData.value.reduce((sum, item) => sum + item[2], 0);
if (total >= 10000) {
return (total / 10000).toFixed(1) + '万';
}
return total.toLocaleString();
});
// 标签显示控制(默认显示全部)
const showAllLabels = ref(true);
const toggleLabels = () => {
showAllLabels.value = !showAllLabels.value;
initHeatmapChart();
};
// 中国开源人才密度地图数据
const talentMapData = ref([
{ name: '北京市', value: [116.4074, 39.9042, 141.15, 231.47] },
{ name: '上海市', value: [121.4737, 31.2304, 269.83, 169.99] },
{ name: '浙江省', value: [120.1551, 30.2794, 8.94, 90.98] },
{ name: '广东省', value: [113.2644, 23.1291, 7.5, 149.76] },
{ name: '台湾省', value: [121.5090, 25.0443, 32.75, 117.88] },
{ name: '四川省', value: [104.0657, 30.6595, 1.01, 48.7] },
{ name: '江苏省', value: [118.7780, 32.0472, 5.25, 56.28] },
{ name: '湖北省', value: [114.3053, 30.5928, 1.84, 34.09] },
{ name: '福建省', value: [119.3062, 26.0747, 1.39, 16.79] },
{ name: '陕西省', value: [108.9480, 34.2632, 1.04, 21.38] },
{ name: '重庆市', value: [106.5049, 29.5332, 1.61, 13.24] },
{ name: '山东省', value: [120.3331, 36.0670, 1.02, 16.02] },
{ name: '湖南省', value: [112.9822, 28.1944, 0.68, 14.25] },
{ name: '安徽省', value: [117.2830, 31.8611, 0.93, 12.92] },
{ name: '辽宁省', value: [123.4321, 41.8007, 0.77, 11.37] },
{ name: '云南省', value: [102.7123, 25.0406, 0.1, 3.68] },
{ name: '天津市', value: [117.1901, 39.1258, 7.79, 9.34] },
{ name: '河南省', value: [113.6654, 34.7579, 0.55, 9.07] },
{ name: '吉林省', value: [125.3245, 43.8171, 0.15, 2.7] },
{ name: '江西省', value: [115.8581, 28.6820, 0.25, 4.17] },
{ name: '广西壮族自治区', value: [108.3200, 22.8241, 0.14, 3.15] },
{ name: '黑龙江省', value: [126.5349, 45.8038, 0.11, 5.13] },
{ name: '澳门特别行政区', value: [113.5490, 22.1987, 737.5, 2.36] },
{ name: '河北省', value: [114.5024, 38.0454, 0.26, 4.74] },
{ name: '山西省', value: [112.5489, 37.8570, 0.25, 3.78] },
{ name: '贵州省', value: [106.7135, 26.5783, 0.11, 1.93] },
{ name: '内蒙古自治区', value: [111.6708, 40.8183, 0.01, 0.96] },
{ name: '香港特别行政区', value: [114.1095, 22.3964, 1797.7, 197.75] },
{ name: '新疆维吾尔自治区', value: [87.6168, 43.7928, 0.01, 0.77] },
{ name: '甘肃省', value: [103.8235, 36.0580, 0.04, 1.46] },
{ name: '海南省', value: [110.3312, 20.0319, 0.36, 1.25] },
{ name: '宁夏回族自治区', value: [106.2782, 38.4664, 0.05, 0.27] },
{ name: '西藏自治区', value: [91.1175, 29.6469, 0.01, 0.14] },
{ name: '青海省', value: [101.7782, 36.6172, 0.01, 0.18] }
]);
// 计算 TOP 5 地区
const topRegions = computed(() => {
const sorted = [...talentMapData.value]
.map(item => ({
name: item.name,
score: item.value[3],
density: item.value[2]
}))
.sort((a, b) => b.score - a.score);
return sorted.slice(0, 5);
});
// 获取人才等级
const getTalentLevel = (density: number) => {
if (density >= 500) return '🔥 核心枢纽';
if (density >= 100) return '⭐ 重要聚集地';
if (density >= 10) return '📈 潜力增长区';
return '🌱 发展中区域';
};
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
2025-12-18 14:09:34 +08:00
infrastructureChart: null,
communityChart: null,
2025-12-18 14:09:34 +08:00
communityHealthChart: null,
languageTechChart: null,
vitalityChart: null,
heatmapChart: null,
talentMapChart: null
});
// ===================== 新增:列表自动滚动逻辑 =====================
// 1. 定义滚动容器的ref
const scrollContainer1 = ref<HTMLDivElement | null>(null); // 第一个列表容器
const scrollContainer2 = ref<HTMLDivElement | null>(null); // 第二个列表容器
// 2. 存储定时器1/2定时器ID
const scrollTimers: { [key: number]: NodeJS.Timeout | null } = {
1: null,
2: null
};
// 3. 滚动配置
const scrollStep = 1; // 每次滚动的步长(像素,越小越流畅)
const scrollInterval = 20; // 滚动间隔(毫秒,越小越流畅)
// 4. 通用自动滚动函数
const autoScroll = (containerKey: number) => {
// 获取对应容器
const container = containerKey === 1 ? scrollContainer1.value : scrollContainer2.value;
if (!container) return;
// 获取列表内容元素project-list
const projectList = container.querySelector('.project-list') as HTMLDivElement;
if (!projectList) return;
// 若内容高度 ≤ 容器高度,不执行滚动
if (projectList.scrollHeight <= container.clientHeight) {
// 清除现有定时器
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
scrollTimers[containerKey] = null;
}
return;
}
// 清除现有定时器(避免重复)
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
}
// 启动定时器滚动
scrollTimers[containerKey] = setInterval(() => {
// 向下滚动步长
container.scrollTop += scrollStep;
// 滚动到底部时重置为0循环滚动
// 加1是为了兼容像素精度问题
if (projectList.scrollHeight - container.scrollTop <= container.clientHeight + 1) {
container.scrollTop = 0; // 直接重置也可以用平滑滚动container.scrollTo({ top: 0, behavior: 'smooth' })
}
}, scrollInterval);
};
// 5. 暂停滚动
const pauseScroll = (containerKey: number) => {
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
scrollTimers[containerKey] = null;
}
};
// 6. 恢复滚动
const resumeScroll = (containerKey: number) => {
autoScroll(containerKey);
};
// 7. 组件卸载时清除所有定时器
onUnmounted(() => {
Object.keys(scrollTimers).forEach(key => {
const k = Number(key);
if (scrollTimers[k]) {
clearInterval(scrollTimers[k]!);
scrollTimers[k] = null;
}
});
});
2025-12-18 14:09:34 +08:00
// 8. 格式化粉丝数82738 → 82.7K
const formatFollowers = (num: number) => {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
};
// 9. 图片加载失败处理
const handleImageError = (e: Event) => {
const target = e.target as HTMLImageElement;
const name = target.alt;
target.style.display = 'none';
if (target.parentElement) {
target.parentElement.innerHTML = `<span class="avatar-text">${name.slice(0, 1)}</span>`;
}
};
// 10. 企业排名相关方法
const getRankClass = (rank: number) => {
if (rank === 1) return 'gold';
if (rank === 2) return 'silver';
if (rank === 3) return 'bronze';
return 'normal';
};
const getRankText = (rank: number) => {
if (rank <= 3) return rank.toString();
return rank.toString().padStart(2, '0');
};
const getScoreClass = (score: number) => {
if (score >= 90) return 'score-blue'; // 90+ 蓝色
if (score >= 80) return 'score-light-blue'; // 80-89 浅蓝色
if (score >= 70) return 'score-green'; // 70-79 绿色
return 'score-red'; // 60-69 红色
};
const getScoreBarClass = (score: number) => {
if (score >= 90) return 'bar-blue'; // 90+ 蓝色
if (score >= 80) return 'bar-light-blue'; // 80-89 浅蓝色
if (score >= 70) return 'bar-green'; // 70-79 绿色
return 'bar-red'; // 60-69 红色
};
const getCountryFlag = (country: string) => {
const countryMap: { [key: string]: string } = {
'China': '🇨🇳',
'USA': '🇺🇸',
'Belgium': '🇧🇪',
'Portugal': '🇵🇹',
'Germany': '🇩🇪',
'Russia': '🇷🇺'
};
return countryMap[country] || country;
};
const formatDevs = (num: number) => {
if (num >= 10000) {
return (num / 1000).toFixed(1) + 'K';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
};
// ===================== 新增结束 =====================
2025-12-18 14:09:34 +08:00
// 初始化基础设施覆盖率图表(多层环形图)
const initInfrastructureChart = () => {
const el = document.getElementById('infrastructureChart');
if (!el) return;
2025-12-18 14:09:34 +08:00
if (chartInstances.value.infrastructureChart) {
chartInstances.value.infrastructureChart.dispose();
}
const myChart = echarts.init(el);
2025-12-18 14:09:34 +08:00
chartInstances.value.infrastructureChart = myChart;
const seriesData: any[] = [];
const center = ['50%', '50%'];
chartData.value.infrastructure.forEach(item => {
const completeValue = item.value;
const remainingValue = 100 - item.value;
seriesData.push({
name: item.name,
type: 'pie',
radius: item.radius,
center: center,
avoidLabelOverlap: false,
startAngle: 90,
hoverAnimation: true,
label: {
show: false
},
labelLine: {
show: false
},
emphasis: {
scale: true,
scaleSize: 5
},
data: [
{
value: completeValue,
name: item.name,
itemStyle: {
color: new echarts.graphic.LinearGradient(
0, 0, 0, 1,
[
{ offset: 0, color: item.color },
{ offset: 1, color: item.color + '80' }
]
),
shadowBlur: 10,
shadowColor: item.color + '40'
}
},
{
value: remainingValue,
name: '未覆盖',
itemStyle: {
color: 'rgba(255, 255, 255, 0.08)'
},
tooltip: {
show: false
},
emphasis: {
disabled: true
}
}
]
});
});
// 计算平均覆盖率
const avgCoverage = Math.round(
chartData.value.infrastructure.reduce((sum, d) => sum + d.value, 0) /
chartData.value.infrastructure.length
);
myChart.setOption({
tooltip: {
trigger: 'item',
2025-12-18 14:09:34 +08:00
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#3b82f6',
borderWidth: 1,
borderRadius: 6,
padding: 12,
textStyle: { color: '#333', fontSize: 13 },
formatter: (params: any) => {
if (params.name === '未覆盖') return '';
const remaining = 100 - params.value;
return `
<div style="line-height: 2;">
<div style="font-weight: bold; color: #3b82f6; margin-bottom: 6px; border-bottom: 2px solid #3b82f6; padding-bottom: 4px;">${params.name}</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 8px; height: 8px; background: #10b981; border-radius: 50%;"></span>
已覆盖<span style="color: #10b981; font-weight: 700; font-size: 15px;">${params.value}%</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 8px; height: 8px; background: #ef4444; border-radius: 50%;"></span>
未覆盖<span style="color: #ef4444; font-weight: 700; font-size: 15px;">${remaining}%</span>
</div>
</div>
`;
}
},
legend: {
2025-12-18 14:09:34 +08:00
data: chartData.value.infrastructure.map(d => d.name),
orient: 'vertical',
right: '5%',
top: 'center',
itemWidth: 12, // 图例方块宽度
itemHeight: 12, // 图例方块高度
itemGap: 15, // 图例项之间的间距
icon: 'circle', // 图例形状:'circle'圆形, 'rect'方形, 'roundRect'圆角方形
textStyle: {
color: '#333',
fontSize: 12,
padding: [0, 0, 0, 8] // 文字与图标的间距
}
},
2025-12-18 14:09:34 +08:00
graphic: [
{
2025-12-18 14:09:34 +08:00
type: 'text',
left: 'center',
top: '45%',
style: {
text: '总覆盖度',
fill: '#666',
fontSize: 13,
fontWeight: '500'
}
},
{
type: 'text',
left: 'center',
top: '52%',
style: {
text: `${avgCoverage}%`,
fill: '#3b82f6',
fontSize: 28,
fontWeight: 'bold'
}
}
],
2025-12-18 14:09:34 +08:00
series: seriesData,
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDelay: (idx: number) => idx * 100
});
};
2025-12-18 14:09:34 +08:00
// 初始化高校开源社团/俱乐部数量图表(纵向柱状图)
const initCommunityChart = () => {
const el = document.getElementById('communityChart');
if (!el) return;
if (chartInstances.value.communityChart) {
chartInstances.value.communityChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.communityChart = myChart;
2025-12-18 14:09:34 +08:00
const data = communityData.value;
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
2025-12-18 14:09:34 +08:00
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#3b82f6',
borderWidth: 1,
borderRadius: 6,
padding: 12,
textStyle: { color: '#333', fontSize: 13 },
formatter: (params: any) => {
const name = params[0].name;
const value = params[0].value;
const clubs = data.details[name] || [];
const clubsHtml = clubs.map(c => `${c}`).join('<br/>');
return `
<div style="line-height: 2;">
<div style="font-weight: bold; color: #3b82f6; margin-bottom: 6px; border-bottom: 2px solid #3b82f6; padding-bottom: 4px;">${name}</div>
<div>社团数量: <span style="color: #10b981; font-weight: 700; font-size: 16px;">${value}</span> </div>
<div style="margin-top: 8px; border-top: 1px solid rgba(0,0,0,0.1); padding-top: 6px; font-size: 11px; color: #666; max-height: 150px; overflow-y: auto;">
${clubsHtml}
</div>
</div>
`;
}
},
grid: {
left: '10%',
right: '10%',
bottom: '5%',
top: '20%',
containLabel: true
},
xAxis: {
type: 'category',
2025-12-18 14:09:34 +08:00
data: data.universities,
axisLabel: {
fontSize: 10,
rotate: 45,
interval: 0,
color: '#666',
margin: 15
},
axisTick: {
alignWithLabel: true,
length: 5
},
axisLine: {
lineStyle: { color: '#e5e7eb', width: 2 }
}
},
yAxis: {
type: 'value',
2025-12-18 14:09:34 +08:00
name: '社团数量',
nameTextStyle: { fontSize: 12, color: '#666' },
axisLabel: { fontSize: 12, color: '#666' },
minInterval: 1,
splitLine: {
lineStyle: { color: 'rgba(0, 0, 0, 0.06)', type: 'dashed' }
}
},
series: [
{
2025-12-18 14:09:34 +08:00
name: '社团数量',
type: 'bar',
2025-12-18 14:09:34 +08:00
data: data.values,
barWidth: '60%',
barCategoryGap: '30%',
itemStyle: {
color: (params: any) => {
2025-12-18 14:09:34 +08:00
// 根据排名使用不同颜色
const colors = [
['#3b82f6', '#60a5fa'], // 蓝色
['#8b5cf6', '#a78bfa'], // 紫色
['#ec4899', '#f472b6'], // 粉色
['#10b981', '#34d399'], // 绿色
['#f59e0b', '#fbbf24'] // 橙色
];
const colorPair = colors[params.dataIndex % colors.length];
return new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: colorPair[1] },
{ offset: 1, color: colorPair[0] }
]);
},
borderRadius: [4, 4, 0, 0],
shadowBlur: 8,
shadowColor: 'rgba(59, 130, 246, 0.3)'
},
label: {
show: true,
position: 'top',
formatter: '{c}',
fontSize: 11,
color: '#3b82f6',
fontWeight: 'bold'
},
emphasis: {
itemStyle: {
shadowBlur: 15,
shadowColor: 'rgba(59, 130, 246, 0.6)'
}
}
}
2025-12-18 14:09:34 +08:00
],
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDelay: (idx: number) => idx * 50
});
};
2025-12-18 14:09:34 +08:00
// 初始化社区治理健康度对比(纵向柱状图)
const initCommunityHealthChart = () => {
const el = document.getElementById('ossCompassChart');
if (!el) return;
2025-12-18 14:09:34 +08:00
if (chartInstances.value.communityHealthChart) {
chartInstances.value.communityHealthChart.dispose();
}
const myChart = echarts.init(el);
2025-12-18 14:09:34 +08:00
chartInstances.value.communityHealthChart = myChart;
const data = chartData.value.communityHealth;
myChart.setOption({
tooltip: {
trigger: 'axis',
2025-12-18 14:09:34 +08:00
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(59, 130, 246, 0.1)'
}
},
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: '#3b82f6',
borderWidth: 2,
borderRadius: 8,
padding: 16,
textStyle: { color: '#333', fontSize: 13 },
extraCssText: 'box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);',
formatter: (params: any) => {
2025-12-18 14:09:34 +08:00
const indicator = params[0].name;
const githubValue = params[0].value;
const gitcodeValue = params[1].value;
const diff = githubValue - gitcodeValue;
const diffPercent = ((diff / githubValue) * 100).toFixed(1);
return `
2025-12-18 14:09:34 +08:00
<div style="line-height: 2.2;">
<div style="font-weight: bold; color: #3b82f6; font-size: 15px; margin-bottom: 8px; border-bottom: 2px solid #3b82f6; padding-bottom: 4px;">${indicator}</div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 6px;">
<span style="display: inline-block; width: 10px; height: 10px; background: linear-gradient(135deg, #BF232A, #FF4900); border-radius: 50%; box-shadow: 0 2px 4px rgba(191, 35, 42, 0.3);"></span>
<span style="color: #666; min-width: 100px;">GitHub 社区</span>
<span style="color: #BF232A; font-weight: 700; font-size: 15px;">${githubValue}</span>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="display: inline-block; width: 10px; height: 10px; background: linear-gradient(135deg, #FF4900, #FFAB00); border-radius: 50%; box-shadow: 0 2px 4px rgba(255, 73, 0, 0.3);"></span>
<span style="color: #666; min-width: 100px;">GitCode 社区</span>
<span style="color: #FF4900; font-weight: 700; font-size: 15px;">${gitcodeValue}</span>
</div>
${diff > 0 ? `<div style="margin-top: 8px; padding: 4px 8px; background: rgba(59, 130, 246, 0.1); border-radius: 4px; color: #3b82f6; font-size: 11px;">📊 GitHub 领先 ${diffPercent}%</div>` : ''}
</div>
`;
}
},
2025-12-18 14:09:34 +08:00
legend: {
data: ['GitHub 社区', 'GitCode 社区'],
top: '2%',
right: '5%',
itemWidth: 14,
itemHeight: 14,
itemGap: 15,
textStyle: {
color: '#333',
fontSize: 12,
fontWeight: '600'
},
icon: 'roundRect'
},
grid: {
left: '5%',
right: '5%',
bottom: '8%',
top: '15%',
containLabel: true
},
xAxis: {
2025-12-18 14:09:34 +08:00
type: 'category',
data: data.indicators,
axisLabel: {
fontSize: 11,
color: '#666',
fontWeight: '500',
interval: 0,
rotate: 20,
margin: 12
},
axisTick: {
alignWithLabel: true,
length: 5
},
axisLine: {
lineStyle: { color: '#e5e7eb', width: 2 }
}
},
yAxis: {
2025-12-18 14:09:34 +08:00
type: 'value',
name: '健康度得分',
nameTextStyle: {
fontSize: 13,
color: '#666',
fontWeight: '600',
padding: [0, 0, 0, 10]
},
max: 100,
axisLabel: {
fontSize: 12,
color: '#666',
formatter: '{value}'
},
splitLine: {
lineStyle: {
color: 'rgba(0, 0, 0, 0.06)',
type: 'dashed'
}
}
},
series: [
{
2025-12-18 14:09:34 +08:00
name: 'GitHub 社区',
type: 'bar',
2025-12-18 14:09:34 +08:00
data: data.github,
barWidth: '35%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: '#FF4900' },
{ offset: 0.5, color: '#BF232A' },
{ offset: 1, color: '#BF232A' }
]),
borderRadius: [6, 6, 0, 0],
shadowBlur: 10,
shadowColor: 'rgba(191, 35, 42, 0.3)'
},
label: {
show: true,
2025-12-18 14:09:34 +08:00
position: 'top',
formatter: '{c}',
2025-12-18 14:09:34 +08:00
fontSize: 11,
color: '#BF232A',
fontWeight: 'bold'
},
2025-12-18 14:09:34 +08:00
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: 'rgba(191, 35, 42, 0.6)'
}
}
},
{
name: 'GitCode 社区',
type: 'bar',
data: data.gitcode,
barWidth: '35%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: '#FFAB00' },
{ offset: 0.5, color: '#FF4900' },
{ offset: 1, color: '#FF4900' }
]),
borderRadius: [6, 6, 0, 0],
shadowBlur: 10,
shadowColor: 'rgba(255, 73, 0, 0.3)'
},
label: {
show: true,
position: 'top',
formatter: '{c}',
fontSize: 11,
color: '#FF4900',
fontWeight: 'bold'
},
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: 'rgba(255, 73, 0, 0.6)'
}
}
}
2025-12-18 14:09:34 +08:00
],
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDelay: (idx: number) => idx * 80
});
};
2025-12-18 14:09:34 +08:00
// 根据增长率动态生成颜色的函数
const getColorByGrowth = (growth: number, tab: string) => {
const minGrowth = -5;
const maxGrowth = tab === 'mainstream' ? 10 : 30;
const normalized = Math.max(0, Math.min(1, (growth - minGrowth) / (maxGrowth - minGrowth)));
const colorStops = [
{ pos: 0, color: '#0063D0' },
{ pos: 0.2, color: '#0081FF' },
{ pos: 0.4, color: '#00A0FF' },
{ pos: 0.6, color: '#00C0FF' },
{ pos: 0.8, color: '#00E0FF' },
{ pos: 1, color: '#00F2FF' }
];
let startColor = colorStops[0].color;
let endColor = colorStops[0].color;
let localPos = 0;
for (let i = 0; i < colorStops.length - 1; i++) {
if (normalized >= colorStops[i].pos && normalized <= colorStops[i + 1].pos) {
startColor = colorStops[i].color;
endColor = colorStops[i + 1].color;
localPos = (normalized - colorStops[i].pos) / (colorStops[i + 1].pos - colorStops[i].pos);
break;
}
}
const interpolateColor = (color1: string, color2: string, factor: number) => {
const c1 = parseInt(color1.slice(1), 16);
const c2 = parseInt(color2.slice(1), 16);
const r1 = (c1 >> 16) & 0xff, g1 = (c1 >> 8) & 0xff, b1 = c1 & 0xff;
const r2 = (c2 >> 16) & 0xff, g2 = (c2 >> 8) & 0xff, b2 = c2 & 0xff;
const r = Math.round(r1 + (r2 - r1) * factor);
const g = Math.round(g1 + (g2 - g1) * factor);
const b = Math.round(b1 + (b2 - b1) * factor);
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
};
const topColor = interpolateColor(startColor, endColor, localPos);
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: topColor },
{ offset: 1, color: startColor }
]);
};
// 初始化语言与技术竞争(南丁格尔玫瑰图)
const initLanguageTechChart = () => {
const el = document.getElementById('languageTechChart');
if (!el) return;
2025-12-18 14:09:34 +08:00
if (chartInstances.value.languageTechChart) {
chartInstances.value.languageTechChart.dispose();
}
const myChart = echarts.init(el);
2025-12-18 14:09:34 +08:00
chartInstances.value.languageTechChart = myChart;
const currentData = languageTab.value === 'mainstream'
? chartData.value.languageTech.mainstream
: chartData.value.languageTech.emerging;
myChart.setOption({
2025-12-18 14:09:34 +08:00
graphic: {
elements: [
// 中心发光圆
{
type: 'circle',
z: 0,
shape: { cx: '48%', cy: '54%', r: '21%' },
style: {
fill: {
type: 'radial',
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{ offset: 0, color: 'rgba(59, 130, 246, 0.15)' },
{ offset: 0.7, color: 'rgba(59, 130, 246, 0.05)' },
{ offset: 1, color: 'rgba(59, 130, 246, 0)' }
]
},
stroke: 'rgba(59, 130, 246, 0.4)',
lineWidth: 2,
shadowBlur: 10,
shadowColor: 'rgba(59, 130, 246, 0.3)'
}
},
// 中间虚线圆
{
type: 'circle',
z: 0,
shape: { cx: '48%', cy: '54%', r: '50%' },
style: {
stroke: 'rgba(139, 92, 246, 0.2)',
lineWidth: 1.5,
lineDash: [8, 4]
}
},
// 外层虚线圆
{
type: 'circle',
z: 0,
shape: { cx: '48%', cy: '54%', r: '80%' },
style: {
stroke: 'rgba(236, 72, 153, 0.15)',
lineWidth: 1,
lineDash: [5, 5]
}
},
// 装饰性小圆点12点钟方向
{
type: 'circle',
z: 1,
shape: { cx: '48%', cy: '12%', r: 4 },
style: {
fill: '#3b82f6',
shadowBlur: 8,
shadowColor: 'rgba(59, 130, 246, 0.6)'
}
},
// 装饰性小圆点3点钟方向
{
type: 'circle',
z: 1,
shape: { cx: '88%', cy: '54%', r: 3 },
style: {
fill: '#8b5cf6',
shadowBlur: 6,
shadowColor: 'rgba(139, 92, 246, 0.6)'
}
},
// 装饰性小圆点6点钟方向
{
type: 'circle',
z: 1,
shape: { cx: '48%', cy: '96%', r: 3 },
style: {
fill: '#ec4899',
shadowBlur: 6,
shadowColor: 'rgba(236, 72, 153, 0.6)'
}
},
// 装饰性小圆点9点钟方向
{
type: 'circle',
z: 1,
shape: { cx: '8%', cy: '54%', r: 3 },
style: {
fill: '#10b981',
shadowBlur: 6,
shadowColor: 'rgba(16, 185, 129, 0.6)'
}
}
]
},
tooltip: {
2025-12-18 14:09:34 +08:00
trigger: 'item',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#3b82f6',
borderWidth: 1,
borderRadius: 6,
padding: 10,
textStyle: { color: '#333', fontSize: 13 },
formatter: (params: any) => `
<div style="line-height: 1.8;">
<div style="font-weight: bold; color: #3b82f6;">${params.name}</div>
<div>市场占比<span style="color: #10b981;">${params.value}%</span></div>
<div>增长率<span style="color: #f59e0b;">${params.data.growth}%</span></div>
</div>
`
},
2025-12-18 14:09:34 +08:00
visualMap: {
show: true,
type: 'continuous',
min: -5,
max: languageTab.value === 'mainstream' ? 10 : 30,
dimension: 2,
orient: 'vertical',
itemWidth: 14,
itemHeight: 90,
text: ['高增长', '低增长'],
textStyle: { color: '#333', fontSize: 11, fontWeight: '600' },
inRange: { color: ['#0063D0', '#00F2FF'] },
right: '2%',
top: '0%',
borderColor: 'rgba(59, 130, 246, 0.3)',
borderWidth: 2,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
padding: 8,
borderRadius: 8,
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.1)'
},
2025-12-18 14:09:34 +08:00
polar: {
radius: ['25%', '78%'],
center: ['48%', '54%'],
axisLine: { show: false },
splitLine: {
show: true,
lineStyle: { color: 'rgba(0,0,0,0.05)', type: 'dashed' }
}
},
angleAxis: {
type: 'category',
2025-12-18 14:09:34 +08:00
data: currentData.map(item => item.name),
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: '#333',
fontSize: 13,
fontWeight: '500',
margin: 18,
rotate: 0
}
},
radiusAxis: {
type: 'value',
min: 0,
max: Math.max(...currentData.map(item => item.value)) * 1.1,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
splitLine: {
show: true,
lineStyle: { color: 'rgba(0,0,0,0.08)', type: 'dashed' }
}
},
series: [
{
2025-12-18 14:09:34 +08:00
name: '语言占比',
type: 'bar',
2025-12-18 14:09:34 +08:00
coordinateSystem: 'polar',
data: currentData.map((item, index) => {
const baseColor = getColorByGrowth(item.growth, languageTab.value);
// 为每个花瓣添加独特的渐变效果
const gradientColor = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: baseColor.colorStops[0].color },
{ offset: 0.5, color: baseColor.colorStops[0].color },
{ offset: 1, color: baseColor.colorStops[1].color }
]);
return {
value: item.value,
growth: item.growth,
itemStyle: {
color: gradientColor,
borderRadius: [18, 18, 0, 0],
borderWidth: 2,
borderColor: 'rgba(255, 255, 255, 0.4)',
shadowBlur: 12,
shadowColor: 'rgba(59, 130, 246, 0.4)',
opacity: 0.95
}
};
}),
barWidth: '35%',
barGap: '-15%',
label: {
show: true,
2025-12-18 14:09:34 +08:00
position: 'inside',
color: '#FFFFFF',
fontSize: 13,
fontWeight: 'bold',
formatter: '{c}%',
distance: 5
},
2025-12-18 14:09:34 +08:00
emphasis: {
itemStyle: {
shadowBlur: 25,
shadowColor: 'rgba(59, 130, 246, 0.8)',
borderWidth: 3,
borderColor: 'rgba(255, 255, 255, 0.6)',
opacity: 1
},
label: {
fontSize: 15,
fontWeight: 'bold',
color: '#FFFFFF',
textShadowBlur: 4,
textShadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
// 添加动画效果
animationType: 'scale',
animationEasing: 'elasticOut',
animationDelay: (idx: number) => idx * 100
}
]
});
};
2025-12-18 14:09:34 +08:00
// 切换语言Tab
const changeLanguageTab = () => {
languageTab.value = languageTab.value === 'mainstream' ? 'emerging' : 'mainstream';
initLanguageTechChart();
};
// 初始化开发者活力波形图
const initVitalityChart = () => {
const el = document.getElementById('vitalityChart');
if (!el) return;
2025-12-18 14:09:34 +08:00
if (chartInstances.value.vitalityChart) {
chartInstances.value.vitalityChart.dispose();
}
const myChart = echarts.init(el);
2025-12-18 14:09:34 +08:00
chartInstances.value.vitalityChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'axis',
2025-12-18 14:09:34 +08:00
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999',
width: 1,
type: 'dashed'
},
lineStyle: {
color: '#3b82f6',
width: 1,
type: 'dashed'
}
},
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: '#10b981',
borderWidth: 2,
borderRadius: 8,
padding: 16,
textStyle: { color: '#333', fontSize: 14 },
extraCssText: 'box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);',
formatter: (params: any) => {
2025-12-18 14:09:34 +08:00
if (!Array.isArray(params) || params.length === 0) {
return '<div>暂无数据</div>';
}
const totalItem = params.find((item: any) => item.seriesName === '累计开发者');
const newItem = params.find((item: any) => item.seriesName === '年度新增');
const totalValue = totalItem?.value || 0;
const newValue = newItem?.value || 0;
const time = totalItem?.axisValue || newItem?.axisValue || '未知时间';
// 计算增长率
const prevIndex = vitalityData.value.timeData.indexOf(time) - 1;
let growthRate = 0;
if (prevIndex >= 0) {
const prevTotal = vitalityData.value.total[prevIndex];
growthRate = ((totalValue - prevTotal) / prevTotal * 100).toFixed(1);
}
return `
<div style="line-height: 2;">
<div style="font-weight: bold; color: #10b981; font-size: 16px; margin-bottom: 8px; border-bottom: 2px solid #10b981; padding-bottom: 4px;">📅 ${time}</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 8px; height: 8px; background: #10b981; border-radius: 50%;"></span>
累计开发者<span style="color: #10b981; font-weight: 700; font-size: 15px;">${totalValue}</span> 万人
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 8px; height: 8px; background: #f59e0b; border-radius: 50%;"></span>
年度新增<span style="color: #f59e0b; font-weight: 700; font-size: 15px;">${newValue}</span> 万人
</div>
${growthRate > 0 ? `<div style="margin-top: 6px; padding: 4px 8px; background: rgba(16, 185, 129, 0.1); border-radius: 4px; color: #10b981; font-size: 12px;">📈 同比增长 ${growthRate}%</div>` : ''}
</div>
`;
}
},
legend: {
2025-12-18 14:09:34 +08:00
left: 'center',
top: '0%',
itemWidth: 16,
itemHeight: 16,
itemGap: 20,
textStyle: { color: '#333', fontSize: 14, fontWeight: '600' },
data: ['累计开发者', '年度新增'],
icon: 'roundRect',
borderRadius: 4
},
grid: {
top: '18%',
left: '3%',
right: '4%',
bottom: '8%',
containLabel: true
},
xAxis: {
type: 'category',
2025-12-18 14:09:34 +08:00
boundaryGap: false,
axisTick: { show: false },
axisLine: {
lineStyle: { color: '#e5e7eb', width: 2 },
onZero: false
},
axisLabel: {
color: '#666',
fontSize: 13,
fontWeight: '500',
interval: 0,
margin: 12
},
data: vitalityData.value.timeData
},
yAxis: {
type: 'value',
name: '开发者数量(万人)',
nameTextStyle: { color: '#666', fontSize: 12 },
axisLabel: { color: '#666', fontSize: 12 },
splitLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.06)', type: 'dashed' }
}
},
series: [
{
name: '累计开发者',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 8,
lineStyle: { color: '#10b981', width: 3 },
itemStyle: {
color: '#10b981',
borderWidth: 2,
borderColor: '#fff'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(16, 185, 129, 0.4)' },
{ offset: 1, color: 'rgba(16, 185, 129, 0.05)' }
]),
shadowBlur: 10,
shadowColor: 'rgba(16, 185, 129, 0.2)'
},
data: vitalityData.value.total,
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: 'rgba(16, 185, 129, 0.6)',
scale: 1.2
},
label: {
show: true,
formatter: '{c} 万',
position: 'top',
color: '#10b981',
fontSize: 13,
fontWeight: 'bold'
}
},
markPoint: {
data: [
{
type: 'max', name: '最大值',
itemStyle: { color: '#10b981' },
label: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold'
}
}
],
symbolSize: 60
},
markLine: {
silent: true,
lineStyle: {
color: '#10b981',
type: 'dashed',
width: 1
},
label: {
color: '#10b981',
fontSize: 11
},
data: [{ type: 'average', name: '平均值' }]
}
},
{
name: '年度新增',
type: 'bar',
barWidth: '40%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#f59e0b' },
{ offset: 1, color: '#fbbf24' }
]),
borderRadius: [4, 4, 0, 0],
shadowBlur: 8,
shadowColor: 'rgba(245, 158, 11, 0.3)'
},
data: vitalityData.value.new,
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: 'rgba(245, 158, 11, 0.6)'
},
label: {
show: true,
formatter: '{c} 万',
position: 'top',
color: '#f59e0b',
fontSize: 12,
fontWeight: 'bold'
}
},
markPoint: {
data: [
{
type: 'max', name: '峰值',
itemStyle: { color: '#f59e0b' },
label: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold'
}
}
],
symbolSize: 60
},
animationDelay: (idx: number) => idx * 50
}
]
});
};
// 初始化开源项目热力图
const initHeatmapChart = () => {
const el = document.getElementById('heatmapChart');
if (!el) return;
if (chartInstances.value.heatmapChart) {
chartInstances.value.heatmapChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.heatmapChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: '#3b82f6',
borderWidth: 2,
borderRadius: 10,
padding: 18,
textStyle: { color: '#333', fontSize: 13 },
extraCssText: 'box-shadow: 0 8px 24px rgba(59, 130, 246, 0.25);',
formatter: (params: any) => {
const data = params.data.value;
const name = data[3];
const org = data[4];
const openrank = data[0].toLocaleString();
const repos = data[1].toLocaleString();
const devs = data[2].toLocaleString();
// 计算排名
const sorted = [...heatmapData.value].sort((a, b) => b[0] - a[0]);
const rank = sorted.findIndex(item => item[3] === name) + 1;
return `
<div style="line-height: 2.2;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<div style="font-weight: bold; color: #3b82f6; font-size: 16px;">${name}</div>
<div style="background: linear-gradient(135deg, #3b82f6, #8b5cf6); color: #fff; padding: 2px 10px; border-radius: 12px; font-size: 11px; font-weight: 600;">TOP ${rank}</div>
</div>
<div style="border-bottom: 2px solid #3b82f6; margin-bottom: 10px; opacity: 0.3;"></div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 6px;">
<span style="display: inline-block; width: 10px; height: 10px; background: linear-gradient(135deg, #f59e0b, #fbbf24); border-radius: 50%; box-shadow: 0 2px 4px rgba(245, 158, 11, 0.3);"></span>
<span style="color: #666; min-width: 80px;">所属组织</span>
<span style="color: #f59e0b; font-weight: 700;">${org}</span>
</div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 6px;">
<span style="display: inline-block; width: 10px; height: 10px; background: linear-gradient(135deg, #3b82f6, #60a5fa); border-radius: 50%; box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);"></span>
<span style="color: #666; min-width: 80px;">OpenRank</span>
<span style="color: #3b82f6; font-weight: 700; font-size: 14px;">${openrank}</span>
</div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 6px;">
<span style="display: inline-block; width: 10px; height: 10px; background: linear-gradient(135deg, #8b5cf6, #a78bfa); border-radius: 50%; box-shadow: 0 2px 4px rgba(139, 92, 246, 0.3);"></span>
<span style="color: #666; min-width: 80px;">活跃仓库</span>
<span style="color: #8b5cf6; font-weight: 700; font-size: 14px;">${repos}</span>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="display: inline-block; width: 10px; height: 10px; background: linear-gradient(135deg, #10b981, #34d399); border-radius: 50%; box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3);"></span>
<span style="color: #666; min-width: 80px;">活跃开发者</span>
<span style="color: #10b981; font-weight: 700; font-size: 14px;">${devs}</span>
</div>
</div>
`;
}
},
visualMap: {
min: 0,
max: 9000,
dimension: 2,
orient: 'vertical',
right: '2%',
top: 'center',
itemWidth: 18,
itemHeight: 140,
text: ['🔥 热门', '❄️ 冷门'],
calculable: true,
inRange: {
color: ['#FF4900', '#FFAB00', '#00B03E', '#00C0FF', '#00F0FF']
},
textStyle: {
color: '#333',
fontSize: 13,
fontWeight: '600'
},
borderColor: 'rgba(59, 130, 246, 0.3)',
borderWidth: 2,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
padding: 12,
borderRadius: 8,
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.1)'
},
grid: {
top: '8%',
left: '8%',
right: '16%',
bottom: '10%',
containLabel: true
},
xAxis: {
type: 'value',
name: 'OpenRank 影响力指数',
nameLocation: 'center',
nameGap: 35,
min: 1000,
max: 70000,
nameTextStyle: {
color: '#3b82f6',
fontSize: 14,
fontWeight: '700'
},
axisLine: {
lineStyle: {
color: '#3b82f6',
width: 2,
shadowBlur: 4,
shadowColor: 'rgba(59, 130, 246, 0.3)'
}
},
axisLabel: {
color: '#666',
fontSize: 12,
fontWeight: '600',
formatter: (value: number) => {
if (value >= 1000) {
return (value / 1000).toFixed(0) + 'k';
}
return value;
}
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(59, 130, 246, 0.1)',
type: 'dashed',
width: 1
}
}
},
yAxis: {
type: 'value',
2025-12-18 14:09:34 +08:00
name: '活跃仓库数量',
nameLocation: 'center',
nameGap: 45,
min: 0,
max: 30000,
nameTextStyle: {
color: '#8b5cf6',
fontSize: 14,
fontWeight: '700'
},
axisLine: {
lineStyle: {
color: '#8b5cf6',
width: 2,
shadowBlur: 4,
shadowColor: 'rgba(139, 92, 246, 0.3)'
}
},
axisLabel: {
color: '#666',
fontSize: 12,
fontWeight: '600',
formatter: (value: number) => {
if (value >= 1000) {
return (value / 1000).toFixed(0) + 'k';
}
return value;
}
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(139, 92, 246, 0.1)',
type: 'dashed',
width: 1
}
}
},
2025-12-18 14:09:34 +08:00
series: [
{
name: '开源项目',
type: 'scatter',
data: heatmapData.value.map(item => ({
value: item,
name: item[3]
})),
symbolSize: (data: any) => {
const devs = data[2];
return Math.log(devs + 1) * 4 + 10;
},
itemStyle: {
opacity: 0.9,
shadowBlur: 15,
shadowColor: 'rgba(59, 130, 246, 0.5)',
borderWidth: 2.5,
borderColor: 'rgba(255, 255, 255, 0.8)'
},
label: {
show: true,
formatter: (params: any) => {
const openrank = params.value[0];
const repos = params.value[1];
const devs = params.value[2];
// 如果开启显示全部,则显示所有标签
if (showAllLabels.value) {
return params.value[3];
}
// 否则只显示重要项目OpenRank > 15000 或 开发者数 > 3000 或 仓库数 > 10000
if (openrank > 15000 || devs > 3000 || repos > 10000) {
return params.value[3];
}
return '';
},
position: (pos: any, params: any, dom: any, rect: any, size: any) => {
// 智能定位:根据位置自动调整标签方向,避免重叠
const x = pos[0];
const y = pos[1];
const viewWidth = size.viewSize[0];
const viewHeight = size.viewSize[1];
// 右上区域:标签在上方
if (x > viewWidth * 0.6 && y < viewHeight * 0.4) {
return 'top';
}
// 右下区域:标签在右侧
if (x > viewWidth * 0.6 && y > viewHeight * 0.6) {
return 'right';
}
// 左上区域:标签在左侧
if (x < viewWidth * 0.4 && y < viewHeight * 0.4) {
return 'left';
}
// 左下区域:标签在下方
if (x < viewWidth * 0.4 && y > viewHeight * 0.6) {
return 'bottom';
}
// 默认在上方
return 'top';
},
color: '#1e293b',
fontSize: 10,
fontWeight: 'bold',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
padding: [3, 7],
borderRadius: 5,
borderWidth: 1.5,
borderColor: 'rgba(59, 130, 246, 0.4)',
shadowBlur: 6,
shadowColor: 'rgba(0, 0, 0, 0.1)',
distance: 8
},
labelLayout: {
// 标签避让配置
hideOverlap: true,
moveOverlap: 'shiftY',
draggable: false
},
emphasis: {
focus: 'self',
itemStyle: {
shadowBlur: 25,
shadowColor: 'rgba(59, 130, 246, 0.9)',
borderWidth: 4,
borderColor: '#fff',
opacity: 1
},
label: {
show: true,
fontSize: 12,
fontWeight: 'bold',
backgroundColor: 'rgba(59, 130, 246, 0.98)',
color: '#fff',
borderColor: '#3b82f6',
borderWidth: 2,
padding: [5, 9],
shadowBlur: 10,
shadowColor: 'rgba(59, 130, 246, 0.5)',
distance: 10
}
},
animationType: 'scale',
animationEasing: 'elasticOut',
animationDelay: (idx: number) => idx * 40
}
]
});
};
2025-12-18 14:09:34 +08:00
// 初始化中国开源人才密度地图
const initTalentMapChart = () => {
const el = document.getElementById('talentMapChart');
if (!el) return;
if (chartInstances.value.talentMapChart) {
chartInstances.value.talentMapChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.talentMapChart = myChart;
// 注册中国地图(科技蓝风格)
// 尝试导入本地地图文件,如果失败则从 CDN 加载
import('@/views/Jyh/ecosystem/json/china.js')
.then(() => {
console.log('本地地图文件加载成功');
// 延迟一下确保地图注册完成
setTimeout(() => {
renderMap();
}, 100);
})
.catch(err => {
console.warn('本地地图文件加载失败,尝试从 CDN 加载:', err);
// 从 CDN 加载地图数据
fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
.then(response => response.json())
.then(geoJson => {
echarts.registerMap('china', geoJson);
renderMap();
})
.catch(cdnErr => {
console.error('CDN 地图数据加载失败:', cdnErr);
el.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 14px;">地图数据加载失败,请刷新页面重试</div>';
});
});
// 渲染地图的函数
function renderMap() {
myChart.setOption({
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(17, 54, 122, 0.95)',
borderColor: '#00A0FF',
borderWidth: 1,
borderRadius: 8,
padding: 16,
textStyle: { color: '#6dd0e3', fontSize: 14 },
extraCssText: 'box-shadow: 0 4px 16px rgba(0, 160, 255, 0.4);',
formatter: (params: any) => {
if (params.componentSubType === 'scatter') {
const data = params.data.value;
const name = params.name;
const density = data[2];
const total = data[3];
const level = getTalentLevel(density);
return `
<div style="line-height: 2;">
<div style="font-weight: bold; color: #00F2FF; font-size: 16px; margin-bottom: 10px;">${name}</div>
<div style="margin-bottom: 6px;">
<span style="color: #6dd0e3;">开源人才密度</span>
<span style="color: #00C0FF; font-weight: 700; font-size: 15px;">${density}</span> /km²
</div>
<div style="margin-bottom: 6px;">
<span style="color: #6dd0e3;">开源人才数量</span>
<span style="color: #00C0FF; font-weight: 700; font-size: 15px;">${total}</span> 万人
</div>
<div style="margin-top: 8px; padding: 4px 10px; background: rgba(0, 242, 255, 0.2); border-radius: 6px; color: #00F2FF; font-size: 12px; font-weight: 600;">
${level}
</div>
</div>
`;
}
return '';
}
},
visualMap: {
show: false,
min: 0,
max: 500,
inRange: {
color: ['#00A0FF', '#0063D0', '#004190'],
symbolSize: [10, 60]
}
},
geo: {
map: 'china',
roam: true,
scaleLimit: {
min: 0.8,
max: 4
},
zoom: 2.1,
center: [105, 38],
label: {
show: true,
fontSize: 10,
color: '#CEF4FD',
emphasis: {
color: '#00F2FF',
fontSize: 11,
fontWeight: 'bold'
}
},
itemStyle: {
areaColor: 'rgba(0, 30, 80, 0.6)',
borderColor: 'rgba(0, 160, 255, 0.5)',
borderWidth: 1.5,
shadowBlur: 10,
shadowColor: 'rgba(0, 160, 255, 0.3)'
},
emphasis: {
itemStyle: {
areaColor: 'rgba(0, 50, 120, 0.8)',
borderColor: '#00C0FF',
borderWidth: 2,
shadowBlur: 15,
shadowColor: 'rgba(0, 192, 255, 0.6)'
}
}
},
series: [
{
name: '开源人才密度',
type: 'scatter',
coordinateSystem: 'geo',
data: talentMapData.value,
symbol: 'circle',
symbolSize: (val: any) => {
const density = val[2];
return Math.sqrt(density) * 1.5 + 8;
},
itemStyle: {
color: (params: any) => {
const density = params.value[2];
if (density >= 500) return '#FF4900';
if (density >= 100) return '#FFAB00';
if (density >= 10) return '#00C0FF';
return '#00A0FF';
},
shadowBlur: 25,
shadowColor: 'rgba(0, 242, 255, 0.6)',
opacity: 0.85
},
label: {
show: false
},
emphasis: {
itemStyle: {
shadowBlur: 35,
shadowColor: 'rgba(0, 242, 255, 1)',
scale: 1.3,
opacity: 1
},
label: {
show: true,
formatter: '{b}',
fontSize: 13,
fontWeight: 'bold',
backgroundColor: 'rgba(0, 192, 255, 0.95)',
color: '#fff',
borderColor: '#00F2FF',
borderWidth: 2,
padding: [5, 10],
borderRadius: 6
}
},
animationType: 'scale',
animationEasing: 'elasticOut',
animationDelay: (idx: number) => idx * 50
}
]
});
}
};
// 统一初始化图表
const initCharts = () => {
// 空数据判断
isEmpty.value = {
2025-12-18 14:09:34 +08:00
infrastructure: chartData.value.infrastructure.length === 0,
community: chartData.value.universityClubs.length === 0,
communityHealth: chartData.value.communityHealth.github.every(item => item === 0),
languageTech: chartData.value.languageTech.mainstream.length === 0 && chartData.value.languageTech.emerging.length === 0
};
// 初始化各图表
2025-12-18 14:09:34 +08:00
if (!isEmpty.value.infrastructure) nextTick(() => initInfrastructureChart());
if (!isEmpty.value.community) nextTick(() => initCommunityChart());
2025-12-18 14:09:34 +08:00
if (!isEmpty.value.communityHealth) nextTick(() => initCommunityHealthChart());
if (!isEmpty.value.languageTech) nextTick(() => initLanguageTechChart());
2025-12-18 14:09:34 +08:00
nextTick(() => initVitalityChart());
nextTick(() => initHeatmapChart());
nextTick(() => initTalentMapChart());
};
// 页面resize时重绘图表
watch(widthType, () => {
initCharts();
// 重新启动滚动防止resize后滚动失效
autoScroll(1);
autoScroll(2);
});
// 挂载时初始化
onMounted(() => {
setTimeout(() => {
initCharts();
// 启动列表自动滚动
autoScroll(1);
autoScroll(2);
}, 100);
});
</script>
<style lang="scss" scoped>
.index-module__NV_5cW__container {
background: linear-gradient(to bottom right, #faf5ff, #60a5fa);
min-height: 100vh;
padding: 2rem 1rem;
position: relative;
overflow-x: hidden;
2025-12-18 14:09:34 +08:00
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.index-module__NV_5cW__header {
text-align: center;
margin-bottom: 3rem;
padding: 2rem 1rem 1rem;
position: relative;
}
.index-module__NV_5cW__floatingElements {
pointer-events: none;
opacity: 0.3;
position: absolute;
inset: 0;
overflow: hidden;
}
.index-module__NV_5cW__floatingElement {
background: linear-gradient(135deg, #3b82f60d, #9333ea0d);
border-radius: 50%;
width: 120px;
height: 120px;
animation: 30s linear infinite index-module__NV_5cW__float;
position: absolute;
}
.index-module__NV_5cW__floatingElement:first-child {
animation-delay: 0s;
top: 10%;
left: 10%;
}
.index-module__NV_5cW__floatingElement:nth-child(2) {
animation-delay: -7s;
top: 60%;
right: 10%;
}
.index-module__NV_5cW__floatingElement:nth-child(3) {
animation-delay: -14s;
bottom: 20%;
left: 50%;
}
@keyframes index-module__NV_5cW__float {
0% {
transform: translateY(0) rotate(0);
}
33% {
transform: translateY(-20px) rotate(120deg);
}
66% {
transform: translateY(10px) rotate(240deg);
}
to {
transform: translateY(0) rotate(360deg);
}
}
.index-module__NV_5cW__titleSection {
color: #333;
z-index: 2;
flex-direction: column;
align-items: center;
gap: 1rem;
display: flex;
position: relative;
}
.index-module__NV_5cW__titleContainer {
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 1rem;
display: flex;
}
.index-module__NV_5cW__title {
background: #333;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
white-space: nowrap;
-webkit-background-clip: text;
background-clip: text;
margin: 0;
font-size: 2.8rem;
font-weight: 700;
}
.index-module__NV_5cW__titleBadge {
color: #fff;
letter-spacing: 0.05em;
white-space: nowrap;
background: #60a5fa;
border-radius: 1.5rem;
padding: 0.4rem 1rem;
font-size: 1rem;
font-weight: 600;
box-shadow: 0 2px 10px #3b82f64d;
}
.index-module__NV_5cW__subtitle {
color: #666;
text-align: center;
align-items: center;
gap: 0.5rem;
max-width: 500px;
font-size: 1.1rem;
font-weight: 400;
display: flex;
}
.index-module__NV_5cW__statsBar {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffff1a;
border: 1px solid #fff3;
border-radius: 1.5rem;
align-items: center;
gap: 1.5rem;
width: fit-content;
max-width: 90%;
margin-top: 0.75rem;
padding: 0.75rem 1.5rem;
display: flex;
}
.index-module__NV_5cW__stat {
flex-direction: column;
align-items: center;
gap: 0.25rem;
display: flex;
}
.index-module__NV_5cW__statNumber {
color: #60a5fa;
font-size: 1.5rem;
font-weight: 700;
}
.index-module__NV_5cW__statLabel {
color: #333;
font-size: 0.875rem;
font-weight: 500;
}
.index-module__NV_5cW__statDivider {
background: #fff3;
width: 1px;
height: 2rem;
}
.index-module__NV_5cW__content {
2025-12-18 14:09:34 +08:00
max-width: 1400px;
/* 加宽容器,适配更多图表 */
margin: 0 auto;
padding: 0 1rem;
}
/* 图表区域样式适配示例的num-empty */
.index-module__NV_5cW__chartsSection {
margin-bottom: 3rem;
}
.index-module__NV_5cW__chartsGrid {
display: grid;
2025-12-18 14:09:34 +08:00
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
/* 调整列宽,适配列表 */
gap: 1.5rem;
padding: 0;
}
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 16px #00000014;
2025-12-18 14:09:34 +08:00
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.index-module__NV_5cW__chartCard::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.05), transparent);
transition: left 0.6s;
}
.index-module__NV_5cW__chartCard:hover::before {
left: 100%;
}
/* 列表卡片特殊样式 */
.index-module__NV_5cW__chartCard.list-card {
padding: 1.5rem;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
2025-12-18 14:09:34 +08:00
.index-module__NV_5cW__chartCard:hover .chartCard__title {
color: #3b82f6;
transform: scale(1.02);
transition: all 0.3s;
}
.chartCard__wide {
2025-12-18 14:09:34 +08:00
grid-column: 1 / -1;
/* 跨列显示 */
}
.chartCard__header {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 1rem;
gap: 0.75rem;
}
.title-with-stats {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
width: 100%;
}
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
2025-12-18 14:09:34 +08:00
margin: 0;
text-align: center;
2025-12-18 14:09:34 +08:00
width: 100%;
}
.enterprise-stats {
display: flex;
gap: 0.5rem;
align-items: center;
animation: fadeInDown 0.5s ease-out;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stat-badge {
padding: 0.25rem 0.75rem;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 600;
color: #3b82f6;
white-space: nowrap;
}
.language-tab {
display: flex;
gap: 0.5rem;
background: linear-gradient(135deg, #f0f4ff, #faf5ff);
border-radius: 1rem;
padding: 0.3rem;
cursor: pointer;
user-select: none;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.1);
transition: all 0.3s;
}
.language-tab:hover {
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.2);
}
.language-tab span {
padding: 0.45rem 1.2rem;
border-radius: 0.75rem;
font-size: 0.875rem;
font-weight: 600;
color: #666;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.language-tab span::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.language-tab span:hover::before {
width: 100%;
height: 100%;
}
.language-tab span.active {
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
color: #fff;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
transform: translateY(-1px);
}
.language-tab span.active::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), transparent);
border-radius: 0.75rem;
}
/* 企业筛选器样式 */
.enterprise-filter {
display: flex;
gap: 0.4rem;
background: linear-gradient(135deg, #f0f4ff, #faf5ff);
border-radius: 1rem;
padding: 0.25rem;
cursor: pointer;
user-select: none;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.1);
transition: all 0.3s;
}
.enterprise-filter:hover {
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.2);
}
.filter-tag {
padding: 0.35rem 0.9rem;
border-radius: 0.75rem;
font-size: 0.8rem;
font-weight: 600;
color: #666;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
cursor: pointer;
}
.filter-tag::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.filter-tag:hover::before {
width: 100%;
height: 100%;
}
.filter-tag.active {
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
color: #fff;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
transform: translateY(-1px);
}
.filter-tag.active::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), transparent);
border-radius: 0.75rem;
}
.chartCard__container {
width: 100%;
2025-12-18 14:09:34 +08:00
height: 300px;
position: relative;
2025-12-18 14:09:34 +08:00
overflow: hidden;
}
/* 为语言图表添加特殊背景 */
.chartCard__wide .chartCard__container {
background: linear-gradient(135deg,
rgba(59, 130, 246, 0.02) 0%,
rgba(139, 92, 246, 0.02) 50%,
rgba(236, 72, 153, 0.02) 100%);
border-radius: 0.5rem;
}
/* 列表容器样式:隐藏原生滚动条(可选,视觉更美观) */
.chartCard__container.list-container {
height: auto;
2025-12-18 14:09:34 +08:00
max-height: 300px;
/* 限制列表高度,超出滚动 */
overflow-y: auto;
padding: 0 0.5rem;
/* 隐藏滚动条 */
2025-12-18 14:09:34 +08:00
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE/Edge */
position: relative;
}
/* 隐藏webkit内核浏览器的滚动条 */
.chartCard__container.list-container::-webkit-scrollbar {
display: none;
}
2025-12-18 14:09:34 +08:00
/* 滚动指示器 */
.scroll-indicator {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: rgba(59, 130, 246, 0.1);
border-radius: 0 0 0 0.5rem;
font-size: 0.7rem;
color: #3b82f6;
z-index: 10;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(3px);
}
}
.scroll-indicator svg {
width: 12px;
height: 12px;
}
/* 项目列表样式 */
.project-list {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.project-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.8rem;
background: #f8f9fa;
border-radius: 0.5rem;
border-left: 3px solid #2951E0;
transition: all 0.2s;
}
/* 鼠标悬停项目项时的样式(可选) */
.project-item:hover {
background: #f0f4ff;
transform: translateX(2px);
}
.project-rank {
width: 24px;
height: 24px;
border-radius: 50%;
background: #2951E0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.project-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.project-name {
font-size: 14px;
font-weight: 600;
color: #333;
}
.project-desc {
font-size: 12px;
color: #666;
font-style: italic;
}
.project-org {
font-size: 12px;
color: #888;
flex-shrink: 0;
white-space: nowrap;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
}
2025-12-18 14:09:34 +08:00
/* 企业列表特殊样式 */
.enterprise-list {
gap: 0.6rem;
}
.enterprise-item {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-left: 3px solid #3b82f6;
padding: 0.75rem 1rem;
gap: 0.75rem;
align-items: center;
position: relative;
overflow: hidden;
animation: slideInRight 0.3s ease-out backwards;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.enterprise-item:nth-child(1) {
animation-delay: 0.05s;
}
.enterprise-item:nth-child(2) {
animation-delay: 0.1s;
}
.enterprise-item:nth-child(3) {
animation-delay: 0.15s;
}
.enterprise-item:nth-child(4) {
animation-delay: 0.2s;
}
.enterprise-item:nth-child(5) {
animation-delay: 0.25s;
}
.enterprise-item::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.1), transparent);
transition: left 0.5s;
}
.enterprise-item:hover::before {
left: 100%;
}
.enterprise-item:hover {
background: linear-gradient(135deg, #f0f4ff 0%, #eff6ff 100%);
border-left-color: #2563eb;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);
transform: translateX(3px);
}
.enterprise-rank {
width: 32px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
flex-shrink: 0;
font-family: 'DIN Alternate', 'Arial', sans-serif;
}
.enterprise-rank.gold {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #2c3e50;
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.4);
}
.enterprise-rank.silver {
background: linear-gradient(135deg, #E8E8E8 0%, #C0C0C0 100%);
color: #2c3e50;
box-shadow: 0 2px 6px rgba(192, 192, 192, 0.4);
}
.enterprise-rank.bronze {
background: linear-gradient(135deg, #CD7F32 0%, #B8733C 100%);
color: #fff;
box-shadow: 0 2px 6px rgba(205, 127, 50, 0.4);
}
.enterprise-rank.normal {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #fff;
}
.enterprise-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.3rem;
min-width: 0;
}
.enterprise-name {
font-size: 14px;
font-weight: 700;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.enterprise-meta {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 11px;
color: #64748b;
}
.meta-item {
display: flex;
align-items: center;
gap: 2px;
}
.meta-devs {
color: #8b5cf6;
font-weight: 600;
}
.score-bar-container {
width: 80px;
height: 6px;
background: rgba(0, 0, 0, 0.08);
border-radius: 3px;
overflow: hidden;
flex-shrink: 0;
}
.score-bar {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease-out;
}
.score-bar.bar-blue {
background: linear-gradient(90deg, #2563eb 0%, #3b82f6 100%);
box-shadow: 0 0 4px rgba(37, 99, 235, 0.5);
}
.score-bar.bar-light-blue {
background: linear-gradient(90deg, #60a5fa 0%, #93c5fd 100%);
box-shadow: 0 0 4px rgba(96, 165, 250, 0.4);
}
.score-bar.bar-green {
background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
box-shadow: 0 0 4px rgba(16, 185, 129, 0.4);
}
.score-bar.bar-red {
background: linear-gradient(90deg, #ef4444 0%, #f87171 100%);
box-shadow: 0 0 4px rgba(239, 68, 68, 0.4);
}
.enterprise-score {
font-family: 'DIN Alternate', 'Arial', sans-serif;
font-size: 15px;
font-weight: 700;
width: 40px;
text-align: right;
flex-shrink: 0;
}
.enterprise-score.score-blue {
color: #2563eb;
text-shadow: 0 0 8px rgba(37, 99, 235, 0.3);
}
.enterprise-score.score-light-blue {
color: #60a5fa;
text-shadow: 0 0 6px rgba(96, 165, 250, 0.3);
}
.enterprise-score.score-green {
color: #10b981;
text-shadow: 0 0 6px rgba(16, 185, 129, 0.2);
}
.enterprise-score.score-red {
color: #ef4444;
text-shadow: 0 0 6px rgba(239, 68, 68, 0.3);
}
/* 明星开发者特殊样式 */
.developer-list-container {
max-height: 320px;
}
.developer-item {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-left: 3px solid #3b82f6;
padding: 0.75rem 1rem;
gap: 0.75rem;
}
.developer-item:hover {
background: linear-gradient(135deg, #eff6ff 0%, #f0f4ff 100%);
border-left-color: #8b5cf6;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);
}
.dev-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-text {
font-size: 16px;
font-weight: bold;
color: #fff;
}
.dev-rank {
width: auto;
height: auto;
border-radius: 12px;
background: linear-gradient(135deg, #ec4899 0%, #f59e0b 100%);
padding: 3px 10px;
font-size: 11px;
font-weight: 700;
box-shadow: 0 2px 6px rgba(236, 72, 153, 0.3);
}
.dev-info {
flex: 1;
gap: 0.3rem;
}
.dev-name {
font-size: 15px;
font-weight: 700;
color: #1e293b;
}
.dev-location {
font-size: 11px;
color: #64748b;
font-style: normal;
}
.dev-followers {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 12px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
color: #3b82f6;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
max-width: none;
}
.dev-followers svg {
flex-shrink: 0;
}
/* 与示例一致的空数据样式 */
.num-empty {
height: 10px;
border-top: 2px solid #2951E0;
width: 100%;
background: linear-gradient(180deg, rgba(41, 81, 224, 0.12) 0%, rgba(41, 81, 224, 0.04) 100%);
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
2025-12-18 14:09:34 +08:00
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.chart {
width: 100%;
height: 100%;
}
.index-module__NV_5cW__sectionHeader {
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
display: flex;
position: relative;
}
.index-module__NV_5cW__sectionTitle {
color: #333;
text-align: center;
justify-content: center;
align-items: center;
gap: 0.75rem;
width: 100%;
margin: 0;
font-size: 1.5rem;
font-weight: 600;
display: flex;
}
2025-12-18 14:09:34 +08:00
.index-module__NV_5cW__vitalitySection {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1.5rem;
margin-bottom: 2rem;
padding: 2rem;
box-shadow: 0 8px 24px #00000014;
transition: all 0.3s;
}
.index-module__NV_5cW__vitalitySection:hover {
border-color: #3b82f64d;
transform: translateY(-2px);
box-shadow: 0 12px 32px #0000001f;
}
.vitality__title {
color: #333;
font-size: 1.5rem;
font-weight: 600;
margin: 0 0 1.5rem 0;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
position: relative;
}
.vitality__title::before {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: linear-gradient(90deg, #10b981, #f59e0b);
border-radius: 2px;
}
.vitality__title svg {
color: #10b981;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.vitality__container {
width: 100%;
height: 400px;
position: relative;
background: linear-gradient(135deg,
rgba(16, 185, 129, 0.02) 0%,
rgba(245, 158, 11, 0.02) 100%);
border-radius: 0.75rem;
padding: 1rem;
}
/* 地图区域样式(科技蓝风格) */
.map-section {
background: linear-gradient(135deg,
rgba(0, 30, 80, 0.08) 0%,
rgba(0, 63, 208, 0.08) 100%);
position: relative;
overflow: hidden;
border: 2px solid rgba(0, 192, 255, 0.1);
box-shadow: 0 4px 16px rgba(0, 192, 255, 0.1);
}
.map-section::before {
content: '';
position: absolute;
top: -30%;
left: -30%;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(0, 192, 255, 0.1) 0%, transparent 70%);
animation: rotate 30s linear infinite;
pointer-events: none;
}
.map-container {
display: flex;
gap: 2rem;
align-items: stretch;
position: relative;
min-height: 600px;
}
.map-container .chart {
flex: 1;
min-height: 600px;
background: linear-gradient(135deg,
rgba(0, 20, 60, 0.4) 0%,
rgba(0, 40, 100, 0.3) 100%);
border-radius: 1rem;
border: 2px solid rgba(0, 192, 255, 0.3);
box-shadow: 0 8px 24px rgba(0, 192, 255, 0.2);
padding: 1rem;
}
.map-rank-panel {
width: 300px;
background: linear-gradient(135deg,
rgba(15, 23, 42, 0.9) 0%,
rgba(0, 30, 80, 0.9) 100%);
border-radius: 1rem;
border: 2px solid rgba(0, 192, 255, 0.3);
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 192, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
gap: 1rem;
}
.panel-title {
font-size: 1.2rem;
font-weight: 700;
color: #00F2FF;
text-align: center;
padding-bottom: 1rem;
margin-bottom: 0.5rem;
border-bottom: 2px solid rgba(0, 242, 255, 0.3);
}
.rank-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-height: 400px;
overflow-y: auto;
padding-right: 5px;
}
.rank-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 1rem;
background: rgba(0, 192, 255, 0.08);
border-radius: 0.75rem;
border: 1px solid rgba(0, 192, 255, 0.2);
border-left: 3px solid #00A0FF;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
margin-bottom: 0.75rem;
}
.rank-item::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 242, 255, 0.15), transparent);
transition: left 0.5s;
}
.rank-item:hover::before {
left: 100%;
}
.rank-item:hover {
background: rgba(0, 192, 255, 0.2);
border-color: #00F2FF;
border-left-color: #00F2FF;
box-shadow: 0 6px 16px rgba(0, 242, 255, 0.3);
transform: translateX(5px) scale(1.02);
}
.rank-num {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 700;
flex-shrink: 0;
font-family: 'DIN Alternate', 'Arial', sans-serif;
}
.rank-num.rank-1 {
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #1a1a1a;
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.rank-num.rank-2 {
background: linear-gradient(135deg, #E8E8E8, #C0C0C0);
color: #1a1a1a;
box-shadow: 0 4px 12px rgba(192, 192, 192, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.rank-num.rank-3 {
background: linear-gradient(135deg, #CD7F32, #B8733C);
color: white;
box-shadow: 0 4px 12px rgba(205, 127, 50, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.rank-num.rank-4,
.rank-num.rank-5 {
background: rgba(0, 192, 255, 0.2);
color: #00C0FF;
font-weight: bold;
border: 2px solid rgba(0, 192, 255, 0.4);
}
.rank-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #e2e8f0;
}
.rank-score {
font-size: 14px;
font-weight: 700;
color: #00F2FF;
font-family: 'DIN Alternate', 'Arial', sans-serif;
text-shadow: 0 0 8px rgba(0, 242, 255, 0.5);
}
/* 热力图特殊样式 */
.heatmap-section {
background: linear-gradient(135deg,
rgba(59, 130, 246, 0.03) 0%,
rgba(139, 92, 246, 0.03) 50%,
rgba(236, 72, 153, 0.03) 100%);
position: relative;
overflow: hidden;
}
.heatmap-section::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%);
animation: rotate 20s linear infinite;
pointer-events: none;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.heatmap-header {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.label-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(139, 92, 246, 0.1));
border: 2px solid rgba(59, 130, 246, 0.3);
border-radius: 1.5rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
font-size: 0.85rem;
font-weight: 600;
color: #3b82f6;
}
.label-toggle:hover {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2));
border-color: rgba(59, 130, 246, 0.5);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.label-toggle:active {
transform: translateY(0);
}
.label-toggle svg {
width: 16px;
height: 16px;
transition: transform 0.3s;
}
.label-toggle:hover svg {
transform: rotate(90deg);
}
.heatmap-stats {
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 1rem 2rem;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.9) 0%,
rgba(255, 255, 255, 0.7) 100%);
border-radius: 1rem;
border: 2px solid rgba(59, 130, 246, 0.2);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
}
.stat-value {
font-size: 1.3rem;
font-weight: 700;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
font-size: 0.8rem;
color: #666;
font-weight: 500;
}
.stat-divider {
width: 2px;
height: 2.5rem;
background: linear-gradient(to bottom,
transparent 0%,
rgba(59, 130, 246, 0.3) 50%,
transparent 100%);
}
.heatmap-container {
background: linear-gradient(135deg,
rgba(59, 130, 246, 0.02) 0%,
rgba(139, 92, 246, 0.02) 50%,
rgba(236, 72, 153, 0.02) 100%);
border: 1px solid rgba(59, 130, 246, 0.1);
position: relative;
}
.heatmap-legend {
position: absolute;
top: 1rem;
left: 1rem;
display: flex;
gap: 1.5rem;
padding: 0.75rem 1.25rem;
background: rgba(255, 255, 255, 0.95);
border-radius: 1rem;
border: 2px solid rgba(59, 130, 246, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: #666;
font-weight: 600;
}
.legend-icon {
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3);
}
.bubble-small {
width: 12px;
height: 12px;
background: linear-gradient(135deg, #FF4900, #FFAB00);
}
.bubble-medium {
width: 18px;
height: 18px;
background: linear-gradient(135deg, #00B03E, #00C0FF);
}
.bubble-large {
width: 24px;
height: 24px;
background: linear-gradient(135deg, #00C0FF, #00F0FF);
}
.index-module__NV_5cW__actionSection {
text-align: center;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1.5rem;
margin: 0;
padding: 2.5rem 2rem;
position: relative;
overflow: hidden;
box-shadow: 0 8px 24px #00000014;
}
.index-module__NV_5cW__actionBackground {
z-index: 1;
background: linear-gradient(135deg, #3b82f60d 0%, #9333ea0d 100%);
position: absolute;
inset: 0;
}
.index-module__NV_5cW__actionContent {
z-index: 2;
max-width: 600px;
margin: 0 auto;
position: relative;
}
.index-module__NV_5cW__actionIcon {
color: #fff;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
border-radius: 50%;
justify-content: center;
align-items: center;
width: 3rem;
height: 3rem;
margin: 0 auto 1.5rem;
display: flex;
box-shadow: 0 4px 12px #3b82f633;
}
.index-module__NV_5cW__actionTitle {
background: linear-gradient(135deg, #1e293b 0%, #3b82f6 100%);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
margin-bottom: 1rem;
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
}
.index-module__NV_5cW__actionDescription {
color: #64748b;
margin-bottom: 2rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
}
.index-module__NV_5cW__actionFeatures {
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
margin-bottom: 2rem;
display: flex;
}
.index-module__NV_5cW__feature {
color: #3b82f6;
background: #3b82f614;
border: 1px solid #3b82f626;
border-radius: 1.5rem;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
font-size: 0.85rem;
font-weight: 500;
display: flex;
}
.index-module__NV_5cW__createButton {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
border: none;
border-radius: 1.5rem;
align-items: center;
gap: 0.5rem;
height: auto;
margin: 0 auto;
padding: 0.75rem 2rem;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s;
display: flex;
box-shadow: 0 4px 12px #3b82f64d;
}
.index-module__NV_5cW__createButton:hover {
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
transform: translateY(-2px);
box-shadow: 0 6px 18px #3b82f666;
}
/* 响应式适配 */
@media (max-width: 768px) {
.index-module__NV_5cW__titleContainer {
text-align: center;
flex-direction: column;
gap: 0.5rem;
}
.index-module__NV_5cW__title {
font-size: 2.5rem;
}
.index-module__NV_5cW__titleBadge {
padding: 0.4rem 1.2rem;
font-size: 1rem;
}
.index-module__NV_5cW__container {
padding: 1rem;
}
.index-module__NV_5cW__header {
margin-bottom: 2rem;
padding: 1.5rem 1rem;
}
.index-module__NV_5cW__statsBar {
flex-wrap: nowrap;
justify-content: space-between;
gap: 0.8rem;
width: 100%;
max-width: 100%;
padding: 0.8rem 1.2rem;
}
.index-module__NV_5cW__stat {
text-align: center;
flex: 1;
min-width: 60px;
}
.index-module__NV_5cW__statNumber {
font-size: 1.1rem;
}
.index-module__NV_5cW__statLabel {
white-space: nowrap;
font-size: 0.7rem;
}
.index-module__NV_5cW__statDivider {
width: 1px;
height: 1.2rem;
}
.index-module__NV_5cW__sectionTitle {
text-align: left;
flex: 1;
justify-content: flex-start;
gap: 0.5rem;
font-size: 1.3rem;
}
.index-module__NV_5cW__actionSection {
border-radius: 1rem;
margin: 0;
padding: 2rem 1.5rem;
}
.index-module__NV_5cW__actionTitle {
font-size: 1.6rem;
}
.index-module__NV_5cW__actionFeatures {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.index-module__NV_5cW__createButton {
padding: 0.65rem 1.5rem;
font-size: 0.95rem;
}
.chartCard__container {
2025-12-18 14:09:34 +08:00
height: 250px;
}
.vitality__container {
height: 300px;
}
.heatmap-stats {
flex-wrap: wrap;
gap: 1rem;
padding: 0.8rem 1rem;
}
.stat-item {
flex: 1;
min-width: 80px;
}
.stat-value {
font-size: 1.1rem;
}
.stat-label {
font-size: 0.7rem;
}
.stat-divider {
height: 2rem;
}
.title-row {
flex-direction: column;
align-items: flex-start;
}
.label-toggle {
padding: 0.4rem 0.8rem;
font-size: 0.75rem;
}
.heatmap-legend {
flex-direction: column;
gap: 0.75rem;
padding: 0.6rem 0.9rem;
top: 0.5rem;
left: 0.5rem;
}
.legend-item {
font-size: 0.7rem;
}
.bubble-small {
width: 10px;
height: 10px;
}
.bubble-medium {
width: 14px;
height: 14px;
}
.bubble-large {
width: 18px;
height: 18px;
}
/* 地图移动端适配 */
.map-container {
flex-direction: column;
gap: 1rem;
}
.map-container .chart {
min-height: 350px;
}
.map-rank-panel {
width: 100%;
}
.rank-item {
padding: 0.6rem 0.8rem;
gap: 0.5rem;
}
.rank-num {
width: 28px;
height: 28px;
font-size: 12px;
}
.rank-name {
font-size: 13px;
}
.rank-score {
font-size: 13px;
}
/* 移动端列表样式调整 */
.project-item {
flex-wrap: wrap;
gap: 0.5rem;
}
.project-org {
width: 100%;
max-width: 100%;
margin-top: 0.2rem;
}
2025-12-18 14:09:34 +08:00
/* 企业列表移动端调整 */
.enterprise-item {
padding: 0.6rem 0.8rem;
gap: 0.5rem;
}
.enterprise-rank {
width: 28px;
height: 22px;
font-size: 12px;
}
.enterprise-name {
font-size: 13px;
}
.enterprise-meta {
font-size: 10px;
}
.score-bar-container {
width: 60px;
}
.enterprise-score {
font-size: 13px;
width: 35px;
}
}
@media (max-width: 480px) {
.index-module__NV_5cW__title {
font-size: 1.8rem;
}
.index-module__NV_5cW__actionSection {
padding: 1.5rem 1rem;
}
.index-module__NV_5cW__statsBar {
gap: 0.6rem;
padding: 0.6rem 0.8rem;
}
.index-module__NV_5cW__statNumber {
font-size: 1rem;
}
.index-module__NV_5cW__statLabel {
font-size: 0.65rem;
}
.index-module__NV_5cW__sectionTitle {
gap: 0.4rem;
font-size: 1.2rem;
}
.chartCard__container {
2025-12-18 14:09:34 +08:00
height: 200px;
}
.vitality__container {
height: 250px;
}
.heatmap-stats {
gap: 0.6rem;
padding: 0.6rem 0.8rem;
}
.stat-value {
font-size: 1rem;
}
.stat-label {
font-size: 0.65rem;
}
.heatmap-legend {
gap: 0.5rem;
padding: 0.5rem 0.7rem;
}
.legend-item {
font-size: 0.65rem;
gap: 0.3rem;
}
.vitality__title {
font-size: 1.2rem;
}
.index-module__NV_5cW__chartsGrid {
grid-template-columns: 1fr;
}
2025-12-18 14:09:34 +08:00
/* 地图小屏适配 */
.map-container .chart {
min-height: 300px;
padding: 0.5rem;
}
.map-rank-panel {
padding: 1rem;
}
.panel-title {
font-size: 1rem;
padding-bottom: 0.75rem;
}
.rank-item {
padding: 0.5rem 0.6rem;
}
.rank-num {
width: 26px;
height: 26px;
font-size: 11px;
}
.rank-name {
font-size: 12px;
}
.rank-score {
fo
/* 小屏列表高度调整 */
.chartCard__container.list-container {
max-height: 300px;
}
/* 企业列表小屏调整 */
.enterprise-item {
padding: 0.5rem 0.6rem;
}
.enterprise-rank {
width: 26px;
height: 20px;
font-size: 11px;
}
.enterprise-name {
font-size: 12px;
}
.score-bar-container {
width: 50px;
height: 5px;
}
.enterprise-score {
font-size: 12px;
width: 32px;
}
}
}
</style>