Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
R
risk-monitor
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
1
合并请求
1
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
蔡建
risk-monitor
Commits
2bc44aa0
提交
2bc44aa0
authored
3月 12, 2026
作者:
刘宇琪
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
刘宇琪 创新主题页面
上级
de9b61c0
隐藏空白字符变更
内嵌
并排
正在显示
13 个修改的文件
包含
2634 行增加
和
101 行删除
+2634
-101
AnalysisText.vue
...ews/innovationSubject/InnovationAnalysis/AnalysisText.vue
+79
-0
CustomSelect.vue
...ews/innovationSubject/InnovationAnalysis/CustomSelect.vue
+155
-0
DonutChart.vue
...views/innovationSubject/InnovationAnalysis/DonutChart.vue
+101
-0
InnovationAnalysis.vue
...novationSubject/InnovationAnalysis/InnovationAnalysis.vue
+379
-0
RadarChart.vue
...views/innovationSubject/InnovationAnalysis/RadarChart.vue
+104
-0
RankingTable.vue
...ews/innovationSubject/InnovationAnalysis/RankingTable.vue
+211
-0
api.ts
src/views/innovationSubject/InnovationAnalysis/api.ts
+139
-0
bindEcharts.js
...views/innovationSubject/InnovationAnalysis/bindEcharts.js
+659
-0
index.ts
src/views/innovationSubject/InnovationAnalysis/index.ts
+4
-0
types.ts
src/views/innovationSubject/InnovationAnalysis/types.ts
+63
-0
ResourceLibrary.vue
...ews/innovationSubject/ResourceLibrary/ResourceLibrary.vue
+715
-0
index.ts
src/views/innovationSubject/ResourceLibrary/index.ts
+3
-0
index.vue
src/views/innovationSubject/index.vue
+22
-101
没有找到文件。
src/views/innovationSubject/InnovationAnalysis/AnalysisText.vue
0 → 100644
浏览文件 @
2bc44aa0
<
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
>
src/views/innovationSubject/InnovationAnalysis/CustomSelect.vue
0 → 100644
浏览文件 @
2bc44aa0
<
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
>
src/views/innovationSubject/InnovationAnalysis/DonutChart.vue
0 → 100644
浏览文件 @
2bc44aa0
<
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
>
src/views/innovationSubject/InnovationAnalysis/InnovationAnalysis.vue
0 → 100644
浏览文件 @
2bc44aa0
<
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
:
1
fr
1
fr
;
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
:
1
fr
1
fr
;
gap
:
26px
;
padding
:
0
24px
24px
;
}
</
style
>
src/views/innovationSubject/InnovationAnalysis/RadarChart.vue
0 → 100644
浏览文件 @
2bc44aa0
<
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
>
src/views/innovationSubject/InnovationAnalysis/RankingTable.vue
0 → 100644
浏览文件 @
2bc44aa0
<
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
>
src/views/innovationSubject/InnovationAnalysis/api.ts
0 → 100644
浏览文件 @
2bc44aa0
// // 模拟 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' }
// ]
// }
src/views/innovationSubject/InnovationAnalysis/bindEcharts.js
0 → 100644
浏览文件 @
2bc44aa0
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
;
}
src/views/innovationSubject/InnovationAnalysis/index.ts
0 → 100644
浏览文件 @
2bc44aa0
// 导出主组件和类型
export
{
default
as
InnovationAnalysis
}
from
'./InnovationAnalysis.vue'
export
*
from
'./types'
export
*
from
'./api'
src/views/innovationSubject/InnovationAnalysis/types.ts
0 → 100644
浏览文件 @
2bc44aa0
// 类型定义
// 机构类型枚举
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
}
src/views/innovationSubject/ResourceLibrary/ResourceLibrary.vue
0 → 100644
浏览文件 @
2bc44aa0
<
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
>
src/views/innovationSubject/ResourceLibrary/index.ts
0 → 100644
浏览文件 @
2bc44aa0
// 资源库组件导出
export
{
default
as
ResourceLibrary
}
from
'./ResourceLibrary.vue'
export
*
from
'./api'
src/views/innovationSubject/index.vue
浏览文件 @
2bc44aa0
...
...
@@ -203,7 +203,8 @@
</div>
-->
</div>
<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=
"home-main-footer-header"
>
<div
class=
"btn-box"
>
...
...
@@ -236,31 +237,7 @@
{{
scope
.
row
.
columnValues
[
item
.
columnName
]
}}
</
template
>
</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>
<div
class=
"center-footer-elx-footer"
>
<img
src=
"./assets/images/ai.png"
alt=
""
/>
...
...
@@ -321,52 +298,12 @@
</div>
</div>
</div>
</div>
</div>
-->
</div>
<div
class=
"home-main-footer"
>
<DivideHeader
id=
"position4"
class=
"divide4"
:titleText=
"'资源库'"
></DivideHeader>
<div
class=
"home-main-footer-header"
>
<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>
<ResourceLibrary
:initial-tab=
"'university'"
/>
</div>
</div>
</div>
...
...
@@ -375,11 +312,10 @@
<
script
setup
>
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
*
as
echarts
from
"echarts"
;
import
router
from
"@/router"
;
import
router
from
"@/router"
;
import
DivideHeader
from
"@/components/DivideHeader.vue"
;
import
scrollToTop
from
"@/utils/scrollToTop"
;
import
{
useContainerScroll
}
from
"@/hooks/useScrollShow"
;
...
...
@@ -402,13 +338,13 @@ import {
getResearchFieldSubjectType
}
from
"@/api/innovationSubject/overview.js"
;
import
setChart
from
"@/utils/setChart"
;
import
InnovationAnalysis
from
'./InnovationAnalysis/InnovationAnalysis.vue'
import
News1
from
"./assets/images/news1.png"
;
import
News2
from
"./assets/images/news2.png"
;
import
News3
from
"./assets/images/news3.png"
;
import
News4
from
"./assets/images/news4.png"
;
import
News5
from
"./assets/images/news5.png"
;
import
ResourceLibrary
from
'./ResourceLibrary/ResourceLibrary.vue'
import
Message1
from
"./assets/images/iconp9.png"
;
import
Message2
from
"./assets/images/iconp8.png"
;
import
Message3
from
"./assets/images/iconp5.png"
;
...
...
@@ -430,25 +366,29 @@ const distributionList = ref([
name
:
"研究型大学"
,
num
:
142
,
color
:
"rgba(231, 243, 255, 1)"
,
textcolor
:
"rgba(22, 119, 255, 1)"
textcolor
:
"rgba(22, 119, 255, 1)"
,
id
:
1
},
{
name
:
"国家实验室"
,
num
:
28
,
color
:
"rgba(255, 247, 230, 1)"
,
textcolor
:
"rgba(255, 169, 64, 1)"
textcolor
:
"rgba(255, 169, 64, 1)"
,
id
:
2
},
{
name
:
"科技企业"
,
num
:
326
,
color
:
"rgba(230, 255, 251, 1)"
,
textcolor
:
"rgba(19, 168, 168, 1)"
textcolor
:
"rgba(19, 168, 168, 1)"
,
id
:
3
},
{
name
:
"国防承包商"
,
num
:
38
,
color
:
"rgba(249, 240, 255, 1)"
,
textcolor
:
"rgba(114, 46, 209, 1)"
textcolor
:
"rgba(114, 46, 209, 1)"
,
id
:
4
}
]);
const
distributionColorList
=
ref
([
...
...
@@ -497,26 +437,7 @@ const zhouList = ref([
// name: "马萨诸塞州",
// 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 => {
// 查看更多风险信号
const
handleToMoreRiskSignal
=
()
=>
{
const
route
=
router
.
resolve
(
"/
viewR
iskSignal"
);
const
route
=
router
.
resolve
(
"/
r
iskSignal"
);
window
.
open
(
route
.
href
,
"_blank"
);
};
...
...
@@ -2196,8 +2117,8 @@ onMounted(async () => {
.home-main-footer
{
margin-top
:
34px
;
height
:
91
0px
;
background
:
rgb
a
(
248
,
249
,
250
,
1
);
height
:
135
0px
;
background
:
rgb
(
255
255
255
);
overflow
:
hidden
;
.divide4
{
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论