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

4162 lines
116 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>
<!-- <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">
<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">
<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">
<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">
<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"
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">
<!-- 1. 开源企业贡献榜-->
<div class="index-module__NV_5cW__chartCard list-card">
<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>
<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>
<!-- 2. 基础设施覆盖率多层环形图-->
<div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">基础设施覆盖率</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.infrastructure"></div>
<div v-else id="infrastructureChart" class="chart"></div>
</div>
</div>
<!-- 3. 高校开源社团/俱乐部数量-->
<div class="index-module__NV_5cW__chartCard">
<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>
<!-- 4. 社区治理健康度对比-->
<div class="index-module__NV_5cW__chartCard">
<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>
<!-- 5. 语言与技术竞争-->
<div class="index-module__NV_5cW__chartCard">
<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">
<div class="num-empty" v-if="isEmpty.languageTech"></div>
<div v-else id="languageTechChart" class="chart"></div>
</div>
</div>
<!-- 6. 明星开发者聚焦列表-->
<div class="index-module__NV_5cW__chartCard list-card">
<h3 class="chartCard__title">明星开发者聚焦</h3>
<!-- 新增ref + 鼠标事件 -->
<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>
<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>
<!-- 开发者活力波形图 -->
<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>
<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"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2v20M2 12h20"></path>
</svg>
<span>{{ showAllLabels ? '隐藏标签' : '显示全部' }}</span>
</div>
</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>
import { onMounted, ref, nextTick, watch, onUnmounted, computed } from 'vue';
import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize';
// 页面尺寸响应式
const { widthType } = usePageResize();
// 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
}));
});
// 2. 明星开发者数据基于GitHub中国区Top开发者
const excellentProjects2025 = ref([
{ 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({
// 基础设施覆盖率(来自 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%']
}
],
// 高校开源社团/俱乐部数据(来自 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]
},
// 语言与技术竞争(南丁格尔玫瑰图数据)
languageTech: {
// 主流语言数据基于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 }
]
}
});
// 处理高校社团数据
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({
infrastructure: false,
community: false,
communityHealth: false,
languageTech: false
});
// 语言图表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 }>({
infrastructureChart: null,
communityChart: null,
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;
}
});
});
// 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();
};
// ===================== 新增结束 =====================
// 初始化基础设施覆盖率图表(多层环形图)
const initInfrastructureChart = () => {
const el = document.getElementById('infrastructureChart');
if (!el) return;
if (chartInstances.value.infrastructureChart) {
chartInstances.value.infrastructureChart.dispose();
}
const myChart = echarts.init(el);
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',
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: {
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] // 文字与图标的间距
}
},
graphic: [
{
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'
}
}
],
series: seriesData,
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDelay: (idx: number) => idx * 100
});
};
// 初始化高校开源社团/俱乐部数量图表(纵向柱状图)
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;
const data = communityData.value;
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
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',
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',
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: [
{
name: '社团数量',
type: 'bar',
data: data.values,
barWidth: '60%',
barCategoryGap: '30%',
itemStyle: {
color: (params: any) => {
// 根据排名使用不同颜色
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)'
}
}
}
],
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDelay: (idx: number) => idx * 50
});
};
// 初始化社区治理健康度对比(纵向柱状图)
const initCommunityHealthChart = () => {
const el = document.getElementById('ossCompassChart');
if (!el) return;
if (chartInstances.value.communityHealthChart) {
chartInstances.value.communityHealthChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.communityHealthChart = myChart;
const data = chartData.value.communityHealth;
myChart.setOption({
tooltip: {
trigger: 'axis',
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) => {
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 `
<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>
`;
}
},
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: {
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: {
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: [
{
name: 'GitHub 社区',
type: 'bar',
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,
position: 'top',
formatter: '{c}',
fontSize: 11,
color: '#BF232A',
fontWeight: 'bold'
},
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)'
}
}
}
],
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDelay: (idx: number) => idx * 80
});
};
// 根据增长率动态生成颜色的函数
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;
if (chartInstances.value.languageTechChart) {
chartInstances.value.languageTechChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.languageTechChart = myChart;
const currentData = languageTab.value === 'mainstream'
? chartData.value.languageTech.mainstream
: chartData.value.languageTech.emerging;
myChart.setOption({
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: {
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>
`
},
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)'
},
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',
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: [
{
name: '语言占比',
type: 'bar',
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,
position: 'inside',
color: '#FFFFFF',
fontSize: 13,
fontWeight: 'bold',
formatter: '{c}%',
distance: 5
},
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
}
]
});
};
// 切换语言Tab
const changeLanguageTab = () => {
languageTab.value = languageTab.value === 'mainstream' ? 'emerging' : 'mainstream';
initLanguageTechChart();
};
// 初始化开发者活力波形图
const initVitalityChart = () => {
const el = document.getElementById('vitalityChart');
if (!el) return;
if (chartInstances.value.vitalityChart) {
chartInstances.value.vitalityChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.vitalityChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'axis',
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) => {
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: {
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',
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',
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
}
}
},
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
}
]
});
};
// 初始化中国开源人才密度地图
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 = {
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
};
// 初始化各图表
if (!isEmpty.value.infrastructure) nextTick(() => initInfrastructureChart());
if (!isEmpty.value.community) nextTick(() => initCommunityChart());
if (!isEmpty.value.communityHealth) nextTick(() => initCommunityHealthChart());
if (!isEmpty.value.languageTech) nextTick(() => initLanguageTechChart());
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;
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 {
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;
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;
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;
}
.index-module__NV_5cW__chartCard:hover .chartCard__title {
color: #3b82f6;
transform: scale(1.02);
transition: all 0.3s;
}
.chartCard__wide {
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;
margin: 0;
text-align: center;
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%;
height: 390px;
position: relative;
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;
}
/* 列表容器样式:隐藏原生滚动条(可选,视觉更美观) */
.list-container {
height: auto;
max-height: 300px;
/* 限制列表高度,超出滚动 */
overflow-y: auto;
padding: 0 0.5rem;
/* 隐藏滚动条 */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE/Edge */
position: relative;
}
/* 隐藏webkit内核浏览器的滚动条 */
.chartCard__container.list-container::-webkit-scrollbar {
display: none;
}
/* 滚动指示器 */
.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;
}
/* 企业列表特殊样式 */
.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: 400px;
margin-top: 20px;
}
.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%);
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;
}
.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 {
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;
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: black;
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 {
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 {
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;
}
/* 企业列表移动端调整 */
.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 {
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;
}
/* 地图小屏适配 */
.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>