提交 2bc44aa0 authored 作者: 刘宇琪's avatar 刘宇琪

刘宇琪 创新主题页面

上级 de9b61c0
<template>
<div class="analysis-box">
<div class="analysis-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v6l4 2"/>
</svg>
</div>
<div class="analysis-content">
<p class="analysis-text">{{ text }}</p>
</div>
<div v-if="showArrow" class="analysis-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
text: string
showArrow?: boolean
}>()
</script>
<style scoped>
.analysis-box {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px 20px;
background: linear-gradient(135deg, #F0F7FF 0%, #E8F4FD 100%);
border-radius: 8px;
border: 1px solid #D4E8FC;
}
.analysis-icon {
flex-shrink: 0;
width: 24px;
height: 24px;
color: #2D8CF0;
}
.analysis-icon svg {
width: 100%;
height: 100%;
}
.analysis-content {
flex: 1;
min-width: 0;
}
.analysis-text {
margin: 0;
font-size: 14px;
line-height: 1.8;
color: #2D8CF0;
}
.analysis-arrow {
flex-shrink: 0;
width: 20px;
height: 20px;
color: #2D8CF0;
cursor: pointer;
transition: transform 0.2s;
}
.analysis-arrow:hover {
transform: translateX(4px);
}
.analysis-arrow svg {
width: 100%;
height: 100%;
}
</style>
<template>
<div ref="selectRef" class="custom-select" :class="{ 'is-open': isOpen }">
<div class="select-trigger" @click="toggleDropdown">
<span class="select-value">{{ selectedLabel }}</span>
<svg class="select-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</div>
<Transition name="dropdown">
<div v-if="isOpen" class="select-dropdown">
<div
v-for="option in options"
:key="option.value"
class="select-option"
:class="{ 'is-selected': option.value === modelValue }"
@click="selectOption(option)"
>
{{ option.label }}
</div>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import type { SelectOption } from './types'
const props = defineProps<{
modelValue: string
options: SelectOption[]
placeholder?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const isOpen = ref(false)
const selectRef = ref<HTMLDivElement>()
const selectedLabel = computed(() => {
const option = props.options.find(opt => opt.value === props.modelValue)
return option?.label || props.placeholder || '请选择'
})
function toggleDropdown() {
isOpen.value = !isOpen.value
}
function selectOption(option: SelectOption) {
emit('update:modelValue', option.value)
isOpen.value = false
}
function handleClickOutside(event: MouseEvent) {
if (selectRef.value && !selectRef.value.contains(event.target as Node)) {
isOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.custom-select {
position: relative;
min-width: 120px;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
}
.select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
background-color: #fff;
border: 1px solid #E0E0E0;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.2s;
}
.select-trigger:hover {
border-color: #2D8CF0;
}
.is-open .select-trigger {
border-color: #2D8CF0;
}
.select-value {
font-size: 14px;
color: #333;
}
.select-arrow {
width: 16px;
height: 16px;
color: #999;
transition: transform 0.2s;
}
.is-open .select-arrow {
transform: rotate(180deg);
}
.select-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background-color: #fff;
border: 1px solid #E0E0E0;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 100;
overflow: hidden;
}
.select-option {
padding: 10px 12px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: background-color 0.2s;
}
.select-option:hover {
background-color: #F5F7FA;
}
.select-option.is-selected {
color: #2D8CF0;
background-color: #E8F4FD;
}
.dropdown-enter-active,
.dropdown-leave-active {
transition: opacity 0.2s, transform 0.2s;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-8px);
}
</style>
<template>
<div class="donut-chart-container">
<div v-if="loading" class="chart-loading">
<div class="loading-spinner"></div>
</div>
<div ref="chartRef" class="chart"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, shallowRef } from 'vue'
import * as echarts from 'echarts'
import { pieOption } from './bindEcharts'
// 接收 barOptionData: { names: string[], values: number[], total: number }
const props = defineProps<{
data: {
names: string[]
values: number[]
total: number
}
loading?: boolean
}>()
const chartRef = ref<HTMLDivElement>()
const chartInstance = shallowRef<echarts.ECharts>()
function initChart() {
if (!chartRef.value) return
chartInstance.value = echarts.init(chartRef.value)
updateChart()
}
function updateChart() {
if (!chartInstance.value || props.loading) return
if (!props.data?.names?.length) return
const option = pieOption(props.data)
chartInstance.value.setOption(option)
}
function handleResize() {
chartInstance.value?.resize()
}
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
chartInstance.value?.dispose()
})
watch(() => props.data, updateChart, { deep: true })
watch(() => props.loading, (loading) => {
if (!loading) {
updateChart()
}
})
</script>
<style scoped>
.donut-chart-container {
position: relative;
width: 100%;
height: 300px;
}
.chart {
width: 100%;
height: 100%;
}
.chart-loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.8);
z-index: 10;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #E8E8E8;
border-top-color: #2D8CF0;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
<template>
<div class="innovation-analysis">
<!-- 顶部导航栏 -->
<div class="header">
<div class="nav-tabs">
<button
class="nav-tab"
:class="{ active: currentView === 'ranking' }"
@click="switchView('ranking')"
>
创新主体排名
</button>
<button
class="nav-tab"
:class="{ active: currentView === 'layout' }"
@click="switchView('layout')"
>
研究布局
</button>
</div>
<div class="filters">
<CustomSelect
v-if="currentView === 'layout'"
v-model="selectedCategory"
:options="categoryOptions"
/>
<CustomSelect
v-model="releaseTime"
:options="releaseTimeList"
/>
</div>
</div>
<!-- 内容区域 -->
<div class="content">
<!-- 排名视图 -->
<template v-if="currentView === 'ranking'">
<div class="section-card">
<div class="section-header">
<div class="section-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<rect x="3" y="3" width="4" height="18" rx="1"/>
<rect x="10" y="8" width="4" height="13" rx="1"/>
<rect x="17" y="13" width="4" height="8" rx="1"/>
</svg>
</div>
<h3 class="section-title">创新主体综合排名</h3>
</div>
<RankingTable :data="innoItemList" :loading="loading" />
</div>
<div class="analysis-section">
<AnalysisText :text="rankingAnalysis" show-arrow />
</div>
</template>
<!-- 布局视图 -->
<template v-else>
<div class="section-card">
<div class="section-header">
<div class="section-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<rect x="3" y="3" width="4" height="18" rx="1"/>
<rect x="10" y="8" width="4" height="13" rx="1"/>
<rect x="17" y="13" width="4" height="8" rx="1"/>
</svg>
</div>
<h3 class="section-title">研究领域布局情况</h3>
</div>
<div class="charts-grid">
<div class="chart-item">
<DonutChart :data="barOptionData" :loading="loading" />
</div>
<div class="chart-item">
<RadarChart
:data="raderOptionData"
:loading="loading"
/>
</div>
</div>
</div>
<div class="analysis-section dual">
<AnalysisText :text="fieldAnalysis" show-arrow />
<AnalysisText :text="radarAnalysis" show-arrow />
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { getOverallRanking, getResearchField, getResearchFieldSubjectType } from '@/api/innovationSubject/overview.js'
import RankingTable from './RankingTable.vue'
import DonutChart from './DonutChart.vue'
import RadarChart from './RadarChart.vue'
import AnalysisText from './AnalysisText.vue'
import CustomSelect from './CustomSelect.vue'
// 接收父组件传入的 activeCate1(主体类型ID)
const props = defineProps<{
activeCate1: string | number
}>()
// 当前视图:'ranking' 创新主体排名, 'layout' 研究布局
const currentView = ref('ranking')
// 年份选择
const releaseTime = ref('2022')
const releaseTimeList = ref([
{ label: '2022', value: '2022' },
{ label: '2023', value: '2023' },
{ label: '2024', value: '2024' },
{ label: '2025', value: '2025' }
])
// 类别选择(研究布局视图)
const selectedCategory = ref('rd_output')
const categoryOptions = ref([
{ label: '研发产出投入', value: 'rd_output' },
{ label: '人才投入', value: 'talent' },
{ label: '专利产出', value: 'patent' }
])
// 数据状态
const loading = ref(false)
// 排名视图数据
const innoItemList = ref({
headerlist: [],
datalist: []
})
const rankingAnalysis = ref('研究型大学在综合排名中占据主导地位,特别是在论文和影响力指标上表现突出;科技企业在专利指数上领先,但在论文产出方面相对较弱;国家实验室和国防承包商在特定领域(如国家安全、能源)有显著优势,但在综合排名中相对靠后。')
// 布局视图数据 - 饼图
const barOptionData = ref({
names: [],
values: [],
total: 0
})
// 布局视图数据 - 雷达图
const raderOptionData = ref([])
// 分析文本
const fieldAnalysis = ref('人工智能与信息技术占据最大份额,反映了数字经济的核心地位;生物医学与健康是第二大研发领域,特别是在疫情后投入显著增加;能源与环境技术和先进制造与材料作为基础支撑领域保持稳定投入;国家安全与国防和航空航天作为战略领域保持专项投入。')
const radarAnalysis = ref('研究型大学在基础科学领域(生物医学、材料科学)占据主导地位;科技企业高度集中于人工智能和信息技术领域;国家实验室在能源技术和国家安全相关领域有显著优势;国防承包商的研究高度集中于航空航天和国家安全领域。')
// 切换视图
function switchView(view: string) {
console.log('[v0] switchView 被调用, view:', view, ', currentView:', currentView.value)
if (currentView.value !== view) {
currentView.value = view
if (view === 'ranking') {
console.log('[v0] 切换到排名视图,调用 handleGetOverallRanking')
handleGetOverallRanking()
} else {
console.log('[v0] 切换到研究布局视图,调用 handleGetResearchField 和 handleGetResearchFieldSubjectType')
handleGetResearchField()
handleGetResearchFieldSubjectType()
}
}
}
// 获取综合排名数据
const handleGetOverallRanking = async () => {
loading.value = true
try {
const params = {
subjectTypeId: props.activeCate1,
year: releaseTime.value
}
const res = await getOverallRanking(params)
console.log('[v0] 综合排名', res)
if (res.code === 200 && res.data) {
innoItemList.value = res.data
}
} catch (error) {
console.error('获取综合排名error', error)
} finally {
loading.value = false
}
}
// 获取研究领域布局情况(饼图数据)
const handleGetResearchField = async () => {
try {
const params = {
year: releaseTime.value
}
const res = await getResearchField(params)
console.log('[v0] 研究领域布局情况', res)
if (res.code === 200 && res.data) {
const names = res.data.map(item => item.areaName)
const values = res.data.map(item => item.amount)
const total = res.data.reduce((sum, item) => sum + item.amount, 0)
barOptionData.value = {
names,
values,
total
}
}
} catch (error) {
console.error('获取研究领域布局情况error', error)
}
}
// 获取研究领域主体类型(雷达图数据)
const handleGetResearchFieldSubjectType = async () => {
try {
const params = {
year: releaseTime.value
}
const res = await getResearchFieldSubjectType(params)
console.log('[v0] 研究领域主体类型', res)
if (res.code === 200 && res.data) {
raderOptionData.value = res.data
}
} catch (error) {
console.error('获取研究领域主体类型error', error)
}
}
// 监听年份变化
watch(releaseTime, () => {
if (currentView.value === 'ranking') {
handleGetOverallRanking()
} else {
handleGetResearchField()
handleGetResearchFieldSubjectType()
}
})
// 监听 activeCate1 变化(父组件切换主体类型时重新加载排名数据)
watch(() => props.activeCate1, () => {
if (currentView.value === 'ranking') {
handleGetOverallRanking()
}
})
// 初始化加载
onMounted(() => {
handleGetOverallRanking()
})
</script>
<style scoped>
.innovation-analysis {
width: 1600px;
margin: 0 auto;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 42px;
margin-top: 30px;
margin-bottom: 24px;
}
.nav-tabs {
display: flex;
align-items: center;
gap: 4px;
}
.nav-tab {
height: 42px;
padding: 4px 24px;
font-size: 18px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 400;
color: rgba(59, 65, 75, 1);
background: transparent;
border: none;
border-radius: 21px;
cursor: pointer;
transition: all 0.2s;
line-height: 24px;
}
.nav-tab:hover {
color: rgba(5, 95, 194, 1);
}
.nav-tab.active {
color: #fff;
background-color: rgba(5, 95, 194, 1);
font-weight: 700;
}
.filters {
display: flex;
align-items: center;
gap: 16px;
}
.content {
min-height: 528px;
background-color: #fff;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
overflow: hidden;
}
.section-card {
padding: 0;
}
.section-header {
display: flex;
align-items: center;
gap: 4px;
height: 48px;
padding: 0 24px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
}
.section-icon {
width: 24px;
height: 24px;
color: rgba(5, 95, 194, 1);
}
.section-icon svg {
width: 100%;
height: 100%;
}
.section-title {
margin: 0;
font-size: 20px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 700;
color: rgba(5, 95, 194, 1);
line-height: 26px;
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
padding: 24px;
position: relative;
}
.charts-grid::after {
content: '';
position: absolute;
left: 50%;
top: 24px;
bottom: 24px;
width: 1px;
background-color: rgba(234, 236, 238, 1);
transform: translateX(-50%);
}
.chart-item {
min-height: 300px;
}
.analysis-section {
padding: 0 24px 24px;
}
.analysis-section.dual {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 26px;
padding: 0 24px 24px;
}
</style>
<template>
<div class="radar-chart-container">
<div v-if="loading" class="chart-loading">
<div class="loading-spinner"></div>
</div>
<div ref="chartRef" class="chart"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, shallowRef } from 'vue'
import * as echarts from 'echarts'
import { raderOption } from './bindEcharts'
// 接收 raderOptionData: API 返回的原始数据
// 数据结构: [{ subjectTypeName: string, areaVoList: [{ areaName: string, amount: number }] }]
const props = defineProps<{
data: Array<{
subjectTypeName: string
areaVoList: Array<{
areaName: string
amount: number
}>
}>
loading?: boolean
}>()
const chartRef = ref<HTMLDivElement>()
const chartInstance = shallowRef<echarts.ECharts>()
function initChart() {
if (!chartRef.value) return
chartInstance.value = echarts.init(chartRef.value)
updateChart()
}
function updateChart() {
if (!chartInstance.value || props.loading) return
if (!props.data?.length) return
const option = raderOption(props.data)
chartInstance.value.setOption(option)
}
function handleResize() {
chartInstance.value?.resize()
}
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
chartInstance.value?.dispose()
})
watch(() => props.data, updateChart, { deep: true })
watch(() => props.loading, (loading) => {
if (!loading) {
updateChart()
}
})
</script>
<style scoped>
.radar-chart-container {
position: relative;
width: 100%;
height: 300px;
}
.chart {
width: 100%;
height: 100%;
}
.chart-loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.8);
z-index: 10;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #E8E8E8;
border-top-color: #2D8CF0;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
<template>
<div class="ranking-table">
<!-- 动态表头 -->
<div class="table-header">
<div
v-for="header in headerList"
:key="header.columnName"
class="col"
:class="getColumnClass(header.columnName)"
>
{{ header.columnLabel }}
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
<!-- 动态表格内容 -->
<div v-else class="table-body">
<div
v-for="(item, index) in dataList"
:key="item.recordId || index"
class="table-row"
>
<div
v-for="header in headerList"
:key="header.columnName"
class="col"
:class="getColumnClass(header.columnName)"
>
{{ formatCellValue(item.columnValues[header.columnName], header.columnName) }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
data: any
loading: boolean
}>()
// 表头列表(按 displayOrder 排序)
const headerList = computed(() => {
if (!props.data?.headerlist) return []
return [...props.data.headerlist].sort((a, b) => a.displayOrder - b.displayOrder)
})
// 数据列表
const dataList = computed(() => {
if (!props.data?.datalist) return []
return props.data.datalist
})
// 根据列名返回对应的 class
function getColumnClass(columnName: string): string {
const classMap: Record<string, string> = {
'ranking': 'col-rank',
'orgName': 'col-name',
'count': 'col-index',
'share': 'col-index'
}
return classMap[columnName] || 'col-index'
}
// 格式化单元格值
function formatCellValue(value: any, columnName: string): string {
if (value === undefined || value === null) return '--'
// 份额指数保留2位小数
if (columnName === 'share') {
return parseFloat(value).toFixed(2)
}
return value.toString()
}
</script>
<style scoped>
.ranking-table {
width: 100%;
box-sizing: border-box;
padding: 0 24px;
font-family: var(--font-family-base, 'Source Han Sans CN', 'Microsoft YaHei', sans-serif);
}
.table-header {
display: flex;
align-items: center;
padding: 0;
border-bottom: 1px solid #D9D9D9;
color: #666666;
font-size: 14px;
font-weight: 500;
background-color: transparent;
height: 60px;
box-sizing: border-box;
overflow: visible;
}
.table-body {
min-height: 200px;
}
.table-row {
display: flex;
align-items: center;
padding: 0;
border-bottom: 1px solid #F0F0F0;
transition: background-color 0.2s ease;
height: 60px;
box-sizing: border-box;
}
.table-row:hover {
background-color: #FAFAFA;
}
.col {
flex-shrink: 0;
font-size: 14px;
color: #333333;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.col-rank {
width: 60px;
}
.col-name {
width: 210px;
justify-content: flex-start;
padding-left: 29px;
}
.col-type {
width: 260px;
}
.col-index {
width: 150px;
}
.col-score {
width: 150px;
font-weight: 600;
color: #000;
}
.col-trend {
width: 122px;
flex: 1;
}
.trend-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 4px;
flex-shrink: 0;
}
.trend-icon svg {
width: 14px;
height: 14px;
}
.trend-up {
background-color: rgba(82, 196, 26, 0.15);
color: #52C41A;
}
.trend-down {
background-color: rgba(245, 34, 45, 0.15);
color: #F5222D;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
color: #999999;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #E8E8E8;
border-top-color: #2D8CF0;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
// // 模拟 API 服务
// import type { RankingApiResponse, LayoutApiResponse } from './types'
// // 模拟延迟
// const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// // 获取创新主体排名数据
// export async function fetchRankingData(year: string): Promise<RankingApiResponse> {
// await delay(500)
// return {
// list: [
// {
// rank: 1,
// name: '麻省理工学院(MIT)',
// type: '研究型大学',
// rdInvestIndex: 64,
// personnelIndex: 72,
// patentIndex: 87,
// paperIndex: 22,
// totalScore: 92,
// trend: 'up'
// },
// {
// rank: 2,
// name: '麻省理工学院',
// type: '国家实验室',
// rdInvestIndex: 66,
// personnelIndex: 60,
// patentIndex: 68,
// paperIndex: 69,
// totalScore: 85,
// trend: 'up'
// },
// {
// rank: 3,
// name: '斯坦福大学',
// type: '科技企业',
// rdInvestIndex: 76,
// personnelIndex: 11,
// patentIndex: 93,
// paperIndex: 51,
// totalScore: 81,
// trend: 'up'
// },
// {
// rank: 4,
// name: '谷歌(Alphabet)',
// type: '科技企业',
// rdInvestIndex: 7,
// personnelIndex: 2,
// patentIndex: 21,
// paperIndex: 4,
// totalScore: 76,
// trend: 'down'
// },
// {
// rank: 5,
// name: '耶鲁大学',
// type: '国防承包商',
// rdInvestIndex: 12,
// personnelIndex: 4,
// patentIndex: 63,
// paperIndex: 38,
// totalScore: 71,
// trend: 'down'
// }
// ],
// analysis: '研究型大学在综合排名中占据主导地位,特别是在论文和影响力指标上表现突出;科技企业在专利指数上领先,但在论文产出方面相对较弱;国家实验室和国防承包商在特定领域(如国家安全、能源)有显著优势,但在综合排名中相对靠后。'
// }
// }
// // 获取研究布局数据
// export async function fetchLayoutData(category: string, year: string): Promise<LayoutApiResponse> {
// await delay(500)
// return {
// fields: [
// { name: '人工智能与信息技术', value: 27, color: '#4A90D9' },
// { name: '生物医学与健康', value: 22, color: '#F5A623' },
// { name: '能源与环境技术', value: 18, color: '#7ED4BC' },
// { name: '先进制造与材料', value: 15, color: '#5BC0DE' },
// { name: '国家安全与国防', value: 12, color: '#F17C67' },
// { name: '航空航天', value: 8, color: '#B8D4E8' }
// ],
// radarIndicators: [
// { name: '集成电路', max: 100 },
// { name: '生物科技', max: 100 },
// { name: '人工智能', max: 100 },
// { name: '通信网络', max: 100 },
// { name: '量子科技', max: 100 },
// { name: '能源领域', max: 100 }
// ],
// radarData: [
// {
// name: '研究型大学',
// color: '#4A7DC9',
// values: [85, 90, 75, 60, 70, 65]
// },
// {
// name: '国家实验室',
// color: '#F5A623',
// values: [70, 65, 80, 75, 85, 80]
// },
// {
// name: '科技企业',
// color: '#B8A9C9',
// values: [90, 55, 95, 85, 50, 45]
// },
// {
// name: '国防承包商',
// color: '#4CAF50',
// values: [60, 40, 70, 65, 90, 85]
// }
// ],
// fieldAnalysis: '人工智能与信息技术占据最大份额,反映了数字经济的核心地位;生物医学与健康是第二大研发领域,特别是在疫情后投入显著增加;能源与环境技术和先进制造与材料作为基础支撑领域保持稳定投入;国家安全与国防和航空航天作为战略领域保持专项投入。',
// radarAnalysis: '研究型大学在基础科学领域(生物医学、材料科学)占据主导地位;科技企业高度集中于人工智能和信息技术领域;国家实验室在能源技术和国家安全相关领域有显著优势;国防承包商的研究高度集中于航空航天和国家安全领域。'
// }
// }
// // 获取年份选项
// export function getYearOptions(): { label: string; value: string }[] {
// return [
// { label: '2023', value: '2023' },
// { label: '2022', value: '2022' },
// { label: '2021', value: '2021' },
// { label: '2020', value: '2020' }
// ]
// }
// // 获取类别选项
// export function getCategoryOptions(): { label: string; value: string }[] {
// return [
// { label: '研发产出投入', value: 'rd_output' },
// { label: '人员投入', value: 'personnel' },
// { label: '专利产出', value: 'patent' },
// { label: '论文产出', value: 'paper' }
// ]
// }
import * as echarts from "echarts";
// 计算总和
function getTotal(data) {
return data.reduce((sum, item) => sum + item.value, 0);
};
export const pieOption = (data) => {
console.log(data, 'datadatadata')
// 颜色数组
const colors = [
'rgba(105, 177, 255, 1)',
'rgba(255, 236, 61, 1)',
'rgba(135, 232, 222, 1)',
'rgba(133, 165, 255, 1)',
'rgba(255, 120, 117, 1)',
'rgba(179, 127, 235, 1)',
'rgba(255, 187, 120, 1)',
'rgba(120, 255, 180, 1)',
'rgba(255, 150, 150, 1)'
];
const seriesData = data.names.map((name, index) => ({
name,
value: data.values[index],
itemStyle: { color: colors[index % colors.length] }
}));
const option = {
tooltip: {
trigger: 'item'
},
grid: {
left: '5%',
right: '5%',
top: '5%',
bottom: '5%',
containLabel: true
},
legend: {
orient: 'vertical',
x: 'right',
y: 'center',
align: 'left',
left: '52%',
data: data.names,
textStyle: {
color: "rgba(59, 65, 75, 1)",
fontSize: 14
},
formatter: function (name) {
const index = data.names.indexOf(name)
if (index === -1) return name
const value = data.values[index]
const total = data.total || data.values.reduce((sum, v) => sum + v, 0)
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0'
return `${name} ${percentage}%`
}
},
series: [
{
name: '频度',
type: 'pie',
center: ['28%', '50%'],
radius: ['35%', '55%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
labelLine: {
show: false
},
data: seriesData,
itemStyle: {
borderColor: '#fff',
borderWidth: 1
}
}
]
};
return option
}
var data1 = [
{
name: "捐赠基金",
value: 27
},
{
name: "政府拨款",
value: 22
},
{
name: "企业合作",
value: 18
},
{
name: "学费收入",
value: 15
},
{
name: "其他来源",
value: 12
}
];
export const pieOption1 = (data) => {
// 提取部门名称和对应的金额
const colors = [
'rgba(105, 177, 255, 1)',
'rgba(255, 236, 61, 1)',
'rgba(135, 232, 222, 1)',
'rgba(133, 165, 255, 1)',
'rgba(255, 120, 117, 1)'
];
// 提取部门名称和对应的金额
const departmentNames = data.map(item => item.departmentName);
const amounts = data.map(item => item.amount);
// 计算总金额
const getTotal = (data) => {
return data.reduce((total, item) => total + item.amount, 0);
};
// 饼图配置
const option = {
legend: {
orient: 'vertical',
x: 'right',
y: 'center',
align: 'left',
left: '60%',
data: departmentNames,
textStyle: { // 图例字体样式
color: "rgba(59, 65, 75, 1)",
fontSize: 16
},
formatter: function (name) {
var total = getTotal(data);
var item = data.find(item => item.departmentName === name);
var percentage = ((item.amount / total) * 100).toFixed(2);
return `${name} ${percentage}%`;
}
},
series: [
{
name: '频度',
type: 'pie',
center: ['30%', '50%'],
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
labelLine: {
show: false
},
data: data.map((item, index) => ({
name: item.departmentName,
value: item.amount,
itemStyle: { color: colors[index % colors.length] } // 使用颜色列表
}))
}
]
};
return option;
}
export const raderOption = (data) => {
// 固定颜色数组,避免随机色
const colors = [
{ line: 'rgba(91, 143, 249, 1)', area: 'rgba(91, 143, 249, 0.2)' },
{ line: 'rgba(255, 166, 77, 1)', area: 'rgba(255, 166, 77, 0.2)' },
{ line: 'rgba(100, 210, 180, 1)', area: 'rgba(100, 210, 180, 0.2)' },
{ line: 'rgba(168, 124, 255, 1)', area: 'rgba(168, 124, 255, 0.2)' },
{ line: 'rgba(255, 120, 117, 1)', area: 'rgba(255, 120, 117, 0.2)' }
];
// 提取所有可能的 areaName
const allAreaNames = new Set();
data.forEach(subject => {
subject.areaVoList.forEach(area => {
allAreaNames.add(area.areaName);
});
});
const indicatorNames = Array.from(allAreaNames);
// 保存原始数据用于 tooltip 显示
const originalData = {};
data.forEach(subject => {
originalData[subject.subjectTypeName] = {};
indicatorNames.forEach(indicator => {
const area = subject.areaVoList.find(a => a.areaName === indicator);
originalData[subject.subjectTypeName][indicator] = area ? area.amount : 0;
});
});
// 对数归一化函数:将原始值转换到 0-100 范围,保持相对差异可见
const logNormalize = (value, maxVal) => {
if (value <= 0) return 0;
if (maxVal <= 0) return 0;
// 使用对数变换:log(1+value) / log(1+max) * 100
return (Math.log(1 + value) / Math.log(1 + maxVal)) * 100;
};
// 计算每个维度的全局最大值
const globalMaxValues = {};
indicatorNames.forEach(name => {
let maxVal = 0;
data.forEach(subject => {
const area = subject.areaVoList.find(a => a.areaName === name);
if (area && area.amount > maxVal) {
maxVal = area.amount;
}
});
globalMaxValues[name] = maxVal;
});
// 生成雷达图的 indicator 配置(统一使用 100 作为 max)
const radarIndicators = indicatorNames.map(name => ({
name,
max: 100
}));
// 为每个 subjectTypeName 生成雷达图数据(归一化后的值)
const radarSeriesData = data.map((subject, index) => {
const values = indicatorNames.map(indicator => {
const area = subject.areaVoList.find(a => a.areaName === indicator);
const originalValue = area ? area.amount : 0;
const maxVal = globalMaxValues[indicator];
return logNormalize(originalValue, maxVal);
});
const colorIndex = index % colors.length;
return {
value: values,
name: subject.subjectTypeName,
lineStyle: {
color: colors[colorIndex].line,
width: 2
},
areaStyle: {
color: colors[colorIndex].area
},
itemStyle: {
color: colors[colorIndex].line
},
symbol: 'circle',
symbolSize: 6
};
});
// 雷达图配置
const option = {
tooltip: {
trigger: 'item',
confine: true,
formatter: function(params) {
if (!params.value) return '';
const name = params.name;
let html = `<div style="font-weight:bold;margin-bottom:8px;border-bottom:1px solid #eee;padding-bottom:4px;">${name}</div>`;
indicatorNames.forEach((indicator) => {
const originalValue = originalData[name] ? originalData[name][indicator] : 0;
html += `<div style="padding:2px 0;">${indicator}: <span style="font-weight:bold;color:#333;">${originalValue}</span></div>`;
});
return html;
}
},
legend: {
icon: 'circle',
orient: 'vertical',
right: 30,
top: 'center',
align: 'left',
itemWidth: 10,
itemHeight: 10,
itemGap: 12,
textStyle: {
color: "rgba(59, 65, 75, 1)",
fontSize: 14
}
},
radar: {
center: ['35%', '50%'],
radius: '55%',
indicator: radarIndicators,
axisName: {
formatter: '{value}',
color: 'rgba(59, 65, 75, 1)',
fontSize: 14,
fontWeight: 700
},
splitArea: {
areaStyle: {
color: ['#fff', '#F5F7FA', '#fff', '#F5F7FA', '#fff']
}
},
splitLine: {
lineStyle: {
color: '#E8E8E8'
}
},
axisLine: {
lineStyle: {
color: '#E8E8E8'
}
}
},
series: [
{
name: '研究领域分布',
type: 'radar',
data: radarSeriesData
}
]
};
return option;
}
export const barOption = (data) => {
// 提取年份和对应的专利数量
const years = data.map(item => item.year.toString());
const counts = data.map(item => item.countNum);
// 柱状图配置
const option = {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow"
}
},
grid: {
top: '3%',
right: '3%',
bottom: '1%',
left: '1%',
containLabel: true
},
xAxis: [{
axisLine: {
lineStyle: {
width: 1,
color: "rgba(231, 243, 255, 1)"
}
},
axisTick: { show: false },
type: "category",
boundaryGap: [100, 100],
axisLabel: {
color: "rgba(95, 101, 108, 1)",
// fontSize: 22,
// fontWeight: 400
},
data: years, // 动态设置 xAxis 数据
}],
yAxis: {
type: "value",
axisLine: {
lineStyle: {
type: "dashed"
}
},
axisLabel: {
color: "rgba(95, 101, 108, 1)",
// fontSize: 22,
// fontWeight: 400
},
splitNumber: 5,
splitLine: {
lineStyle: {
width: 1,
type: "dashed",
color: "rgba(231, 243, 255, 1)"
},
}
},
series: [
{
name: "专利数量",
data: counts, // 动态设置 series 数据
type: "bar",
barWidth: 20,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(46, 165, 255, 1)" },
{ offset: 1, color: "rgba(46, 165, 255, 0)" }
])
},
label: {
show: false,
position: 'top',
textStyle: {
fontSize: '20px',
fontWeight: 400,
color: 'rgba(255, 255, 255, 1)'
}
}
}
]
};
return option;
}
export const lineChart = (data) => {
// 提取年份和对应的专利数量
const years = data.map(item => item.year.toString());
const counts = data.map(item => item.countNum);
// 折线图配置
const option = {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow"
}
},
grid: {
left: '2%',
top: '8%',
right: '2%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
splitLine: {
show: false
},
axisLine: {
show: false
},
data: years, // 动态设置 xAxis 数据
},
yAxis: {
type: 'value',
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: "#E7F3FF"
}
},
axisLine: {
show: false
},
},
color: ['rgba(255, 149, 77, 1)'],
series: [
{
data: counts, // 动态设置 series 数据
type: 'line',
emphasis: {
focus: 'series'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 149, 77, 0.5)' }, // 起始颜色:深色
{ offset: 1, color: 'rgba(255, 149, 77, 0)' } // 结束颜色:浅色且透明度降低
])
},
}
]
};
return option;
}
export const lineChart1 = (data) => {
// 提取年份和对应的 fundAmount
const years = data.map(item => item.year.toString());
const fundAmounts = data.map(item => item.fundAmount);
// 折线图配置
const option = {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow"
}
},
grid: {
left: '2%',
top: '8%',
right: '2%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
splitLine: {
show: false
},
axisLine: {
show: false
},
data: years, // 动态设置 xAxis 数据
},
yAxis: {
type: 'value',
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: "#E7F3FF"
}
},
axisLine: {
show: false
},
},
color: ['rgba(33, 129, 57, 1)'],
series: [
{
data: fundAmounts, // 动态设置 series 数据
type: 'line',
emphasis: {
focus: 'series'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(33, 129, 57, 0.5)' }, // 起始颜色:深色
{ offset: 1, color: 'rgba(33, 129, 57, 0)' } // 结束颜色:浅色且透明度降低
])
},
}
]
};
return option;
}
const nameList = ["教育学院", "文理学院", "法学院", "商学院", "工程学院", "医学院"];
const valueList = [21, 21, 25, 79, 95, 109];
export const horizontalBaroption = (data) => {
// 提取部门名称和对应的金额
const departmentNames = data.map(item => item.departmentName);
const amounts = data.map(item => item.amount);
// 水平柱状图配置
const option = {
grid: {
top: '0',
right: '3%',
bottom: '1%',
left: '1%',
containLabel: true
},
color: ['#ce4f51', '#1778ff'],
xAxis: {
type: 'value',
splitLine: {
show: false
},
show: false
},
yAxis: {
type: 'category',
data: departmentNames, // 动态设置 yAxis 数据
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
show: true
}
},
series: [{
type: 'bar',
data: amounts.map((item, index) => {
return {
value: item,
label: {
textStyle: {
color: index < 3 ? '#1778ff' : '#ce4f51'
}
}
};
}),
label: {
show: true,
position: [650, -2]
},
barWidth: 8,
itemStyle: {
color: function (params) {
if (params.dataIndex < 3) {
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: 'rgba(22, 119, 255, 0)' },
{ offset: 1, color: '#1778ff' }
]);
} else {
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: 'rgba(206, 79, 81, 0)' },
{ offset: 1, color: '#ce4f51' }
]);
}
},
barBorderRadius: 4,
}
}]
};
return option;
}
export const raderOption1 = (data) => {
// 提取指标名称和对应的值
const indicatorNames = data.map(item => item.areaName);
const indicatorValues = data.map(item => item.areaValue);
// 动态生成雷���图的 indicator 配置
const indicators = indicatorNames.map((name, index) => ({
name,
max: Math.max(...indicatorValues) * 1.2 // 设置最大值为所有值的最大值的1.2倍
}));
let radarData = {
color:
"rgba(215, 27, 56, 0.2)",
name: '',
value: indicatorValues
}
console.log(indicators, 'indicators', radarData, 'radarDataradarData')
// 雷达图配置
const option = {
title: { text: '' },
legend: {
icon: 'circle',
orient: 'vertical',
right: 50,
top: 'center',
align: 'left',
textStyle: {
color: "rgba(59, 65, 75, 1)",
fontSize: "16px"
}
},
radar: {
radius: '60%',
indicator: indicators,
axisName: {
formatter: '{value}',
color: 'rgba(59, 65, 75, 1)',
fontSize: 16,
fontWeight: 700
}
},
series: [
{
name: 'Budget vs spending',
type: 'radar',
data: [radarData]
}
]
};
return option;
}
// 导出主组件和类型
export { default as InnovationAnalysis } from './InnovationAnalysis.vue'
export * from './types'
export * from './api'
// 类型定义
// 机构类型枚举
export type OrganizationType = '研究型大学' | '国家实验室' | '科技企业' | '国防承包商'
// 创新主体排名数据
export interface RankingItem {
rank: number
name: string
type: OrganizationType
rdInvestIndex: number
personnelIndex: number
patentIndex: number
paperIndex: number
totalScore: number
trend: 'up' | 'down'
}
// 环形图数据项
export interface DonutDataItem {
name: string
value: number
color: string
}
// 雷达图数据项
export interface RadarDataItem {
name: string
color: string
values: number[]
}
// 雷达图维度指标
export interface RadarIndicator {
name: string
max: number
color?: string
}
// 创新主体排名 API 响应
export interface RankingApiResponse {
data: RankingItem[]
analysisText: string
}
// 研究布局 API 响应
export interface ResearchLayoutApiResponse {
donutData: DonutDataItem[]
radarData: RadarDataItem[]
radarIndicators: RadarIndicator[]
donutAnalysisText: string
radarAnalysisText: string
}
// 请求参数
export interface RankingParams {
year: string
}
export interface ResearchLayoutParams {
year: string
type: string
}
<template>
<div class="resource-library">
<!-- 导航和搜索 -->
<div class="nav-bar">
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.value"
class="tab-btn"
:class="{ active: activeTab === tab.value }"
@click="switchTab(tab.value)"
>
{{ tab.label }}
</button>
</div>
<div class="search-box">
<input
v-model="searchKeyword"
type="text"
placeholder="搜索关键词"
@keyup.enter="handleSearch"
/>
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
</div>
</div>
<!-- 内容区域 -->
<div class="content">
<!-- 左侧筛选 -->
<aside class="filter-panel">
<div class="sf-section">
<div class="sf-title-row">
<span class="sf-title-bar"></span>
<span class="sf-title-text">科技领域</span>
</div>
<div class="sf-options">
<label
v-for="field in fields"
:key="field.value"
class="sf-label"
>
<span
class="sf-checkbox-icon"
:class="{ checked: selectedFields.includes(field.value) }"
@click="toggleField(field.value)"
>
<svg v-if="selectedFields.includes(field.value)" width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect width="14" height="14" rx="4" fill="#055FC2"/>
<path d="M3.5 7L6 9.5L10.5 5" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span v-else class="sf-checkbox-empty"></span>
</span>
<span class="sf-text" @click="toggleField(field.value)">{{ field.label }}</span>
</label>
</div>
</div>
</aside>
<!-- 右侧:卡片列表 + 分页 -->
<div class="right-panel">
<div class="card-list">
<div v-if="loading" class="loading-state">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
<template v-else>
<div
v-for="item in listData"
:key="item.id"
class="resource-card"
>
<div class="card-logo">
<img :src="item.logoUrl || defaultLogo" :alt="item.name" />
</div>
<div class="card-name">{{ item.name }}</div>
<div class="card-location">{{ item.address }}</div>
<div class="card-majors">{{ item.majors }}</div>
<div class="card-tags">
<span
v-for="tag in item.tags"
:key="tag"
class="tag"
>
{{ tag }}
</span>
</div>
</div>
</template>
</div>
<!-- 底部分页 -->
<div class="pagination-wrapper">
<div class="total-count">共{{ total }}所{{ currentTabLabel }}</div>
<div class="pagination">
<button
class="page-btn prev"
:disabled="currentPage === 1"
@click="goToPage(currentPage - 1)"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"/>
</svg>
</button>
<button
v-for="page in displayedPages"
:key="page"
class="page-btn"
:class="{ active: currentPage === page, ellipsis: page === '...' }"
:disabled="page === '...'"
@click="page !== '...' && goToPage(page)"
>
{{ page }}
</button>
<button
class="page-btn next"
:disabled="currentPage === totalPages"
@click="goToPage(currentPage + 1)"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 18l6-6-6-6"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { getIndustryKeyList } from '@/api/bill/billHome.js'
import { getSubjectList } from '@/api/characterPage/characterPage.js'
// Props
const props = defineProps<{
initialTab?: string
}>()
// 默认logo
const defaultLogo = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj48cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9IiNhMzE4MWIiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTggOGgxNnYxNkg4eiIvPjwvc3ZnPg=='
// 标签配置 - 添加 subjectTypeId
const tabs = ref([
{ label: '大学', value: 'university', subjectTypeId: 1 },
{ label: '实验室', value: 'laboratory', subjectTypeId: 2 },
{ label: '企业', value: 'enterprise', subjectTypeId: 3 }
])
// 生成最近五年的年份列表
const currentYear = new Date().getFullYear()
const yearOptions = ref(
Array.from({ length: 5 }, (_, i) => {
const year = currentYear - i
return { label: String(year), value: String(year) }
})
)
const selectedYear = ref(String(currentYear))
// 筛选字段(科技领域)- 从接口获取
const fields = ref<{ label: string; value: string }[]>([
{ label: '全部领域', value: 'all' }
])
const fieldsLoading = ref(false)
// 获取科技领域选项
async function fetchFieldOptions() {
fieldsLoading.value = true
try {
const res = await getIndustryKeyList()
if (res.code === 200 && res.data) {
const apiFields = res.data.map((item: any) => ({
label: item.name,
value: item.id
}))
fields.value = [{ label: '全部领域', value: 'all' }, ...apiFields]
}
} catch (error) {
console.error('获取科技领域失败:', error)
} finally {
fieldsLoading.value = false
}
}
// 状态
const activeTab = ref(props.initialTab || 'university')
const searchKeyword = ref('')
const selectedFields = ref<string[]>(['all'])
const loading = ref(false)
const listData = ref<any[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(15)
// 计算属性
const currentTabLabel = computed(() => {
const tab = tabs.value.find(t => t.value === activeTab.value)
return tab ? tab.label : ''
})
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
const displayedPages = computed(() => {
const pages: (number | string)[] = []
const tp = totalPages.value
const cp = currentPage.value
if (tp <= 7) {
for (let i = 1; i <= tp; i++) pages.push(i)
} else {
if (cp <= 4) {
for (let i = 1; i <= 5; i++) pages.push(i)
pages.push('...')
pages.push(tp)
} else if (cp >= tp - 3) {
pages.push(1)
pages.push('...')
for (let i = tp - 4; i <= tp; i++) pages.push(i)
} else {
pages.push(1)
pages.push('...')
for (let i = cp - 1; i <= cp + 1; i++) pages.push(i)
pages.push('...')
pages.push(tp)
}
}
return pages
})
// 方法
function switchTab(tab: string) {
if (activeTab.value !== tab) {
activeTab.value = tab
currentPage.value = 1
console.log('切换标签:', tab)
fetchData()
}
}
function handleSearch() {
currentPage.value = 1
console.log('搜索:', searchKeyword.value)
fetchData()
}
function toggleField(value: string) {
if (value === 'all') {
selectedFields.value = ['all']
} else {
const idx = selectedFields.value.indexOf(value)
if (idx > -1) {
selectedFields.value = selectedFields.value.filter(f => f !== value)
} else {
selectedFields.value = selectedFields.value.filter(f => f !== 'all')
selectedFields.value.push(value)
}
if (selectedFields.value.length === 0) {
selectedFields.value = ['all']
}
}
currentPage.value = 1
console.log('筛选变化:', selectedFields.value)
fetchData()
}
function goToPage(page: number | string) {
if (typeof page === 'number' && page >= 1 && page <= totalPages.value) {
currentPage.value = page
console.log('页码变化:', page)
fetchData()
}
}
// 模拟API获取数据
async function fetchData() {
loading.value = true
try {
// 获取当前 tab 的 subjectTypeId
const currentTab = tabs.value.find(t => t.value === activeTab.value)
const subjectTypeId = currentTab?.subjectTypeId || 1
// 获取筛选的领域 ID
const areaIds = selectedFields.value.includes('all')
? ''
: selectedFields.value.join(',')
// 调用真实接口
const res = await getSubjectList({
subjectTypeId,
arealist: areaIds,
keywords: searchKeyword.value,
currentPage: currentPage.value - 1, // API 的 currentPage 从 0 开始
pageSize: pageSize.value
})
if (res.code === 200 && res.data) {
// 映射 API 返回的数据到卡片显示格式
const mappedData = (res.data.content || []).map((item: any) => ({
id: item.id,
name: item.orgName,
location: item.countryName ? `${item.countryName}·${item.provinceName}` : item.provinceName || '--',
majors: item.fieldList?.join('、') || '--',
tags: item.areaList?.map((a: any) => a.areaName) || [],
logo: defaultLogo
}))
listData.value = mappedData
total.value = res.data.totalElements || 0
}
} catch (error) {
console.error('获取数据失败:', error)
listData.value = []
total.value = 0
} finally {
loading.value = false
}
}
// 初始化
onMounted(() => {
fetchFieldOptions()
fetchData()
})
</script>
<style scoped>
.resource-library {
width: 1600px;
margin: 0 auto;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
/* 导航和搜索 */
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-shrink: 0;
height: 50px;
}
.tabs {
display: flex;
align-items: center;
gap: 16px;
}
.tab-btn {
height: 42px;
padding: 4px 24px;
font-size: 18px;
font-weight: 400;
color: rgba(59, 65, 75, 1);
background: transparent;
border: none;
border-radius: 21px;
cursor: pointer;
transition: all 0.2s;
line-height: 24px;
}
.tab-btn:hover {
color: rgba(5, 95, 194, 1);
}
.tab-btn.active {
color: #fff;
background-color: rgba(5, 95, 194, 1);
font-weight: 700;
}
.search-box {
position: relative;
width: 200px;
}
.search-box input {
width: 100%;
height: 36px;
padding: 0 36px 0 12px;
font-size: 14px;
color: rgba(59, 65, 75, 1);
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
outline: none;
transition: border-color 0.2s;
}
.search-box input:focus {
border-color: rgba(5, 95, 194, 1);
}
.search-box input::placeholder {
color: rgba(132, 136, 142, 1);
}
.search-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: rgba(132, 136, 142, 1);
pointer-events: none;
}
/* 内容区域:横向排列筛选面板 + 右侧面板 */
.content {
display: flex;
gap: 16px;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* 筛选面板 */
.filter-panel {
width: 360px;
flex-shrink: 0;
background: #fff;
border-radius: 10px;
border: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
box-sizing: border-box;
padding: 16px 0 24px 0;
display: flex;
flex-direction: column;
gap: 16px;
align-items: flex-start;
overflow: hidden;
align-self: flex-start;
}
.sf-section {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.sf-title-row {
display: flex;
align-items: center;
gap: 17px;
width: 100%;
box-sizing: border-box;
}
.sf-title-bar {
display: inline-block;
width: 8px;
height: 16px;
background: rgba(5, 95, 194, 1);
border-radius: 0 2px 2px 0;
flex-shrink: 0;
}
.sf-title-text {
font-size: 16px;
font-weight: 700;
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
color: rgba(5, 95, 194, 1);
letter-spacing: 1px;
line-height: 24px;
}
.sf-options {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px 4px;
align-content: flex-start;
align-items: flex-start;
padding: 0 0 0 24px;
width: 100%;
box-sizing: border-box;
}
.sf-label {
width: 160px;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
flex-shrink: 0;
}
.sf-checkbox-icon {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex-shrink: 0;
cursor: pointer;
}
.sf-checkbox-empty {
display: block;
width: 14px;
height: 14px;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
box-sizing: border-box;
background: #fff;
}
.sf-text {
font-size: 16px;
font-weight: 400;
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
color: rgba(95, 101, 108, 1);
line-height: 24px;
cursor: pointer;
white-space: nowrap;
}
.sf-label:hover .sf-text {
color: rgba(59, 65, 75, 1);
}
/* 右侧面板:卡片 + 分页,独立 flex column */
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* 卡片列表 - 调整 height 控制高度 */
.card-list {
height: 700px;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-content: flex-start;
overflow-y: auto;
flex-shrink: 0;
padding: 0 16px;
}
.resource-card {
width: calc((100% - 32px) / 3);
padding: 12px 14px;
background-color: #fff;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
display: flex;
flex-direction: column;
gap: 6px;
transition: box-shadow 0.2s;
}
.resource-card:hover {
box-shadow: 0px 0px 30px 0px rgba(25, 69, 130, 0.15);
}
.card-logo {
width: 32px;
height: 32px;
}
.card-logo img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.card-name {
font-size: 18px;
font-weight: 700;
color: rgba(59, 65, 75, 1);
line-height: 24px;
}
.card-location {
font-size: 14px;
color: rgba(95, 101, 108, 1);
line-height: 22px;
}
.card-majors {
font-size: 14px;
color: rgba(95, 101, 108, 1);
line-height: 22px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 1px 8px;
font-size: 14px;
color: rgba(19, 168, 168, 1);
background-color: rgba(230, 255, 251, 1);
border: 1px solid rgba(135, 232, 222, 1);
border-radius: 4px;
line-height: 20px;
}
/* 分页 */
.pagination-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0 0 0;
flex-shrink: 0;
border-top: 1px solid rgba(234, 236, 238, 1);
}
.total-count {
font-size: 14px;
color: rgba(95, 101, 108, 1);
}
.pagination {
display: flex;
align-items: center;
gap: 8px;
}
.page-btn {
min-width: 32px;
height: 32px;
padding: 0 8px;
font-size: 14px;
color: rgba(59, 65, 75, 1);
background-color: #fff;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.page-btn:hover:not(:disabled):not(.active):not(.ellipsis) {
border-color: rgba(5, 95, 194, 1);
color: rgba(5, 95, 194, 1);
}
.page-btn.active {
background-color: rgba(5, 95, 194, 1);
border-color: rgba(5, 95, 194, 1);
color: #fff;
}
.page-btn.ellipsis {
border: none;
background: transparent;
cursor: default;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-btn.prev svg,
.page-btn.next svg {
width: 16px;
height: 16px;
}
/* 加载状态 */
.loading-state {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
color: rgba(132, 136, 142, 1);
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(230, 231, 232, 1);
border-top-color: rgba(5, 95, 194, 1);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
// 资源库组件导出
export { default as ResourceLibrary } from './ResourceLibrary.vue'
export * from './api'
...@@ -203,7 +203,8 @@ ...@@ -203,7 +203,8 @@
</div> --> </div> -->
</div> </div>
<DivideHeader id="position3" class="divide3" :titleText="'数据总览'"></DivideHeader> <DivideHeader id="position3" class="divide3" :titleText="'数据总览'"></DivideHeader>
<div v-if="activeCate === '创新主体排名'"> <InnovationAnalysis :active-cate1="activeCate1" />
<!-- <div v-if="activeCate === '创新主体排名'">
<div class="center-footer"> <div class="center-footer">
<div class="home-main-footer-header"> <div class="home-main-footer-header">
<div class="btn-box"> <div class="btn-box">
...@@ -236,31 +237,7 @@ ...@@ -236,31 +237,7 @@
{{ scope.row.columnValues[item.columnName] }} {{ scope.row.columnValues[item.columnName] }}
</template> </template>
</el-table-column> </el-table-column>
<!-- <el-table-column prop="name" label="机构名称" width="200"> </el-table-column>
<el-table-column label="类型" width="200">
<template #default="scope">
<div style="display: flex; align-items: center; gap: 5px">
<div v-for="tag in scopetype" :key="tag" :class="{
tag1: tag === '研究型大学',
tag2: tag === '国家实验室',
tag3: tag === '科技企业',
tag4: tag === '国防承包商'
}">
{{ tag }}
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="research" label="研发投入指数" width="200"> </el-table-column>
<el-table-column prop="people" label="人员投入指数" width="200"> </el-table-column>
<el-table-column prop="patent" label="专利产出指数" width="200"> </el-table-column>
<el-table-column prop="paper" label="论文产出指数" width="200"> </el-table-column>
<el-table-column prop="score" label="综合得分" width="150"> </el-table-column>
<el-table-column prop="trend" label="趋势" width="100">
<template #default="scope">
<div :class="{ up: scope.row.trend === 'up', down: scope.row.trend === 'down' }"></div>
</template>
</el-table-column> -->
</el-table> </el-table>
<div class="center-footer-elx-footer"> <div class="center-footer-elx-footer">
<img src="./assets/images/ai.png" alt="" /> <img src="./assets/images/ai.png" alt="" />
...@@ -321,52 +298,12 @@ ...@@ -321,52 +298,12 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div> -->
</div> </div>
<div class="home-main-footer"> <div class="home-main-footer">
<DivideHeader id="position4" class="divide4" :titleText="'资源库'"></DivideHeader> <DivideHeader id="position4" class="divide4" :titleText="'资源库'"></DivideHeader>
<div class="home-main-footer-header"> <ResourceLibrary :initial-tab="'university'" />
<div class="btn-box">
<div class="btn" :class="{ btnActive: activeCate1 === cate.id }" v-for="(cate, index) in categoryList1"
:key="index" @click="handleClickCate1(cate.id)">
{{ cate.typeName }}
</div>
</div>
<div class="select-box">
<el-select v-model="areaSelect" placeholder="全部领域" style="width: 120px"
@change="handleFindListBySubjectTypeId">
<el-option v-for="item in areaList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
</div>
<div class="home-main-footer-main">
<div style="width: 1600px">
<div class="footer-main-item" v-for="item in universityList" :key="item.name"
@click="handleClickToDetail(item.orgId)">
<img :src="item.logoUrl" style="height: 32px; width: 32px" />
<div class="item-text">{{ item.orgName }}</div>
<div class="item-text2">{{ item.address }}</div>
<div class="item-text2">{{ item.businessScope }}</div>
<div class="taglist">
<div class="tag1" v-for="(val, idx) in item.taglist" :key="idx" :class="{
tag2: val === '人工智能',
tag3: val === '新一代通信技术',
tag4: val === '生物科技',
tag5: val === '量子科技'
}">
{{ val }}
</div>
</div>
</div>
</div>
<div class="footer-box">
<div class="footer-right">
<el-pagination @current-change="handleCurrentChange" :pageSize="12" :current-page="currentPage" background
layout="prev, pager, next" :total="total" />
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -375,11 +312,10 @@ ...@@ -375,11 +312,10 @@
<script setup> <script setup>
import RiskSignal from "@/components/RiskSignal/RiskSignal.vue"; import RiskSignal from "@/components/RiskSignal/RiskSignal.vue";
import NewsList from "@/components/NewsList/NewsList.vue"; import NewsList from "@/components/base/newsList/index.vue";
import { onMounted, ref, computed } from "vue"; import { onMounted, ref, computed } from "vue";
import * as echarts from "echarts"; import * as echarts from "echarts";
import router from "@/router"; import router from "@/router";
import DivideHeader from "@/components/DivideHeader.vue"; import DivideHeader from "@/components/DivideHeader.vue";
import scrollToTop from "@/utils/scrollToTop"; import scrollToTop from "@/utils/scrollToTop";
import { useContainerScroll } from "@/hooks/useScrollShow"; import { useContainerScroll } from "@/hooks/useScrollShow";
...@@ -402,13 +338,13 @@ import { ...@@ -402,13 +338,13 @@ import {
getResearchFieldSubjectType getResearchFieldSubjectType
} from "@/api/innovationSubject/overview.js"; } from "@/api/innovationSubject/overview.js";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import InnovationAnalysis from './InnovationAnalysis/InnovationAnalysis.vue'
import News1 from "./assets/images/news1.png"; import News1 from "./assets/images/news1.png";
import News2 from "./assets/images/news2.png"; import News2 from "./assets/images/news2.png";
import News3 from "./assets/images/news3.png"; import News3 from "./assets/images/news3.png";
import News4 from "./assets/images/news4.png"; import News4 from "./assets/images/news4.png";
import News5 from "./assets/images/news5.png"; import News5 from "./assets/images/news5.png";
import ResourceLibrary from './ResourceLibrary/ResourceLibrary.vue'
import Message1 from "./assets/images/iconp9.png"; import Message1 from "./assets/images/iconp9.png";
import Message2 from "./assets/images/iconp8.png"; import Message2 from "./assets/images/iconp8.png";
import Message3 from "./assets/images/iconp5.png"; import Message3 from "./assets/images/iconp5.png";
...@@ -430,25 +366,29 @@ const distributionList = ref([ ...@@ -430,25 +366,29 @@ const distributionList = ref([
name: "研究型大学", name: "研究型大学",
num: 142, num: 142,
color: "rgba(231, 243, 255, 1)", color: "rgba(231, 243, 255, 1)",
textcolor: "rgba(22, 119, 255, 1)" textcolor: "rgba(22, 119, 255, 1)",
id:1
}, },
{ {
name: "国家实验室", name: "国家实验室",
num: 28, num: 28,
color: "rgba(255, 247, 230, 1)", color: "rgba(255, 247, 230, 1)",
textcolor: "rgba(255, 169, 64, 1)" textcolor: "rgba(255, 169, 64, 1)",
id:2
}, },
{ {
name: "科技企业", name: "科技企业",
num: 326, num: 326,
color: "rgba(230, 255, 251, 1)", color: "rgba(230, 255, 251, 1)",
textcolor: "rgba(19, 168, 168, 1)" textcolor: "rgba(19, 168, 168, 1)",
id:3
}, },
{ {
name: "国防承包商", name: "国防承包商",
num: 38, num: 38,
color: "rgba(249, 240, 255, 1)", color: "rgba(249, 240, 255, 1)",
textcolor: "rgba(114, 46, 209, 1)" textcolor: "rgba(114, 46, 209, 1)",
id:4
} }
]); ]);
const distributionColorList = ref([ const distributionColorList = ref([
...@@ -497,26 +437,7 @@ const zhouList = ref([ ...@@ -497,26 +437,7 @@ const zhouList = ref([
// name: "马萨诸塞州", // name: "马萨诸塞州",
// num: "85" // num: "85"
// }, // },
// {
// index: 2,
// name: "加利福尼亚州",
// num: "67"
// },
// {
// index: 3,
// name: "佐治亚州",
// num: "49"
// },
// {
// index: 4,
// name: "华盛顿州",
// num: "31"
// },
// {
// index: 5,
// name: "伊利诺伊州",
// num: "29"
// }
]); ]);
//统计不同地理对应的主体数量 //统计不同地理对应的主体数量
...@@ -541,7 +462,7 @@ const handleClickToDetail = university => { ...@@ -541,7 +462,7 @@ const handleClickToDetail = university => {
// 查看更多风险信号 // 查看更多风险信号
const handleToMoreRiskSignal = () => { const handleToMoreRiskSignal = () => {
const route = router.resolve("/viewRiskSignal"); const route = router.resolve("/riskSignal");
window.open(route.href, "_blank"); window.open(route.href, "_blank");
}; };
...@@ -2196,8 +2117,8 @@ onMounted(async () => { ...@@ -2196,8 +2117,8 @@ onMounted(async () => {
.home-main-footer { .home-main-footer {
margin-top: 34px; margin-top: 34px;
height: 910px; height: 1350px;
background: rgba(248, 249, 250, 1); background: rgb(255 255 255);
overflow: hidden; overflow: hidden;
.divide4 { .divide4 {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论