4162 lines
116 KiB
Vue
4162 lines
116 KiB
Vue
<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>
|