提交 1363ec3d authored 作者: coderBryanFu's avatar coderBryanFu

ui通用组件统一全局base文件夹

上级 ebc89c2c
...@@ -14,6 +14,7 @@ node_modules ...@@ -14,6 +14,7 @@ node_modules
# Build outputs # Build outputs
dist dist
dist.rar
dist-ssr dist-ssr
*.local *.local
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<NavBarV2/> <ModuleHeader/>
<div class="main-container"> <div class="main-container">
<router-view /> <router-view />
</div> </div>
......
<template>
<div class="tag-wrapper" :class="classObject">
{{ tagName }}
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const props = defineProps({
tagName: {
type: String,
default: '标签名称'
}
})
const classObject = computed(() => ({
'tag1': props.tagName === '人工智能',
'tag2': props.tagName === '生物科技',
'tag3': props.tagName === '新一代通信网络',
'tag4': props.tagName === '量子科技',
'tag5': props.tagName === '新能源',
'tag6': props.tagName === '集成电路',
'tag7': props.tagName === '海洋',
'tag8': props.tagName === '先进制造',
'tag9': props.tagName === '新材料',
'tag10': props.tagName === '航空航天',
'tag11': props.tagName === '太空',
'tag12': props.tagName === '深海',
'tag13': props.tagName === '极地',
'tag14': props.tagName === '核',
'tag15': props.tagName === '其他',
}))
</script>
<style lang="scss" scoped>
.tag-wrapper {
height: 24px;
padding: 0 8px;
line-height: 24px;
text-align: center;
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 14px;
font-weight: 400;
letter-spacing: 0px;
box-sizing: border-box;
border-radius: 4px;
border: 1px solid rgba(183, 235, 143, 1);
background: rgba(246, 255, 237, 1);
color: rgba(82, 196, 26, 1);
}
.tag1 {
border: 1px solid rgba(255, 163, 158, 1);
background: rgba(255, 241, 240, 1);
color: rgba(245, 34, 45, 1);
}
.tag2 {
border: 1px solid rgba(135, 232, 222, 1);
background: rgba(230, 255, 251, 1);
color: rgba(19, 168, 168, 1);
}
.tag3 {
border: 1px solid rgba(174, 214, 255, 1);
background: rgba(246, 250, 255, 1);
color: rgba(5, 95, 194, 1);
}
.tag4 {
border: 1px solid rgba(211, 173, 247, 1);
background: rgba(249, 240, 255, 1);
color: rgba(114, 46, 209, 1);
}
.tag5 {
border: 1px solid rgba(183, 235, 143, 1);
background: rgba(246, 255, 237, 1);
color: rgba(82, 196, 26, 1);
}
.tag6 {
border: 1px solid rgba(145, 202, 255, 1);
background: rgba(230, 244, 255, 1);
color: rgba(22, 119, 255, 1);
}
.tag7 {
border: 1px solid rgba(156, 207, 245, 1);
background: rgba(241, 247, 250, 1);
color: rgba(15, 120, 199, 1);
}
.tag8 {
border: 1px solid rgba(255, 229, 143, 1);
background: rgba(255, 251, 230, 1);
color: rgba(250, 173, 20, 1);
}
.tag9 {
border: 1px solid rgba(255, 213, 145, 1);
background: rgba(255, 247, 230, 1);
color: rgba(250, 140, 22, 1);
}
.tag10 {
border: 1px solid rgba(173, 198, 255, 1);
background: rgba(240, 245, 255, 1);
color: rgba(47, 84, 235, 1);
}
.tag11 {
border: 1px solid rgba(173, 198, 255, 1);
background: rgba(240, 245, 255, 1);
color: rgba(47, 84, 235, 1);
}
.tag12 {
border: 1px solid rgba(116, 146, 203, 1);
background: rgba(230, 244, 255, 1);
color: rgba(73, 104, 161, 1);
}
.tag13 {
border: 1px solid rgba(214, 228, 255, 1);
background: rgba(240, 245, 255, 1);
color: rgba(133, 165, 255, 1);
}
.tag14 {
border: 1px solid rgba(255, 187, 150, 1);
background: rgba(255, 242, 232, 1);
color: rgba(250, 84, 28, 1);
}
.tag15 {
border: 1px solid rgba(183, 235, 143, 1);
background: rgba(246, 255, 237, 1);
color: rgba(82, 196, 26, 1);
}
</style>
\ No newline at end of file
<template>
<div class="analysis-box-wrapper" :style="{ width: width ? width : '100%', height: height ? height : '100%' }">
<div class="wrapper-header">
<div class="header-icon"></div>
<div class="header-title">{{ title }}</div>
<div class="header-btn" v-if="!showAllBtn">
<slot name="headerBtn"></slot>
</div>
<div class="header-btn1" v-else>
<slot name="headerBtn"></slot>
</div>
<div class="header-right">
<div class="header-right-btn" @click="handleSave" v-if="showAllBtn">
<img src="@/assets/icons/box-header-icon1.png" alt="">
</div>
<div class="header-right-btn" @click="handleDownload">
<img src="@/assets/icons/box-header-icon2.png" alt="">
</div>
<div class="header-right-btn" @click="handleCollect">
<img src="@/assets/icons/box-header-icon3.png" alt="">
</div>
</div>
</div>
<div class="wrapper-main">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { ref, computed } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
height: {
type: String,
default: ''
},
showAllBtn: {
type: Boolean,
default: true
}
})
const handleSave = () => {
ElMessage.success('保存当前内容')
// emit('save')
}
const handleDownload = () => {
ElMessage.success('下载当前内容')
// emit('download')
}
const handleCollect = () => {
ElMessage.success('收藏当前内容')
// emit('collect')
}
const emit = defineEmits(['save','download','collect'])
</script>
<style lang="scss" scoped>
.analysis-box-wrapper {
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
.wrapper-header {
height: 45px;
display: flex;
box-sizing: border-box;
position: relative;
.header-icon {
margin-top: 18px;
width: 8px;
height: 20px;
background: var(--color-main-active);
border-radius: 0 4px 4px 0;
}
.header-title {
margin-left: 14px;
margin-top: 14px;
height: 26px;
// color: var(--color-main-active);
// font-family: Source Han Sans CN;
// font-size: 20px;
// font-weight: 700;
// line-height: 26px;
// letter-spacing: 0px;
height: 26px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
line-height: 26px;
}
.header-btn {
position: absolute;
top: 14px;
right: 84px;
// display: flex;
// justify-content: flex-end;
// gap: 8px;
}
.header-btn1 {
position: absolute;
top: 14px;
right: 104px;
}
.header-right {
position: absolute;
top: 14px;
right: 14px;
height: 28px;
display: flex;
justify-content: flex-end;
gap: 4px;
.header-right-btn {
width: 28px;
height: 28px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
}
.wrapper-main {
height: calc(100% - 45px);
overflow: hidden;
// overflow-y: auto;
padding: 5px auto;
}
}
</style>
<template>
<div class="overview-box-wrapper" :style="{ width: width ? width : '640px', height: height ? height : '415px' }">
<div class="wrapper-header">
<div class="header-icon"></div>
<div class="header-title">{{ title }}</div>
<div class="header-right">
<div class="header-right-btn" @click="handleSave">
<img src="@/assets/icons/box-header-icon1.png" alt="">
</div>
<div class="header-right-btn" @click="handleDownload">
<img src="@/assets/icons/box-header-icon2.png" alt="">
</div>
<div class="header-right-btn" @click="handleCollect">
<img src="@/assets/icons/box-header-icon3.png" alt="">
</div>
</div>
</div>
<div class="wrapper-main">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
height: {
type: String,
default: ''
}
})
const handleSave = () => {
alert('save')
}
const handleDownload = () => {
alert('download')
}
const handleCollect = () => {
alert('collect')
}
</script>
<style lang="scss" scoped>
.overview-box-wrapper {
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
.wrapper-header {
height: 45px;
display: flex;
box-sizing: border-box;
.header-icon {
margin-top: 18px;
width: 8px;
height: 20px;
background: var(--color-main-active);
border-radius: 0 4px 4px 0;
}
.header-title {
margin-left: 14px;
margin-top: 14px;
height: 26px;
color: var(--color-main-active);
font-family: Source Han Sans CN;
font-style: Bold;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
}
.header-right {
position: absolute;
top: 14px;
right: 12px;
height: 28px;
display: flex;
justify-content: flex-end;
gap: 4px;
.header-right-btn {
width: 28px;
height: 28px;
img {
width: 100%;
height: 100%;
}
}
}
}
.wrapper-main {
height: calc(100% - 45px);
overflow: hidden;
overflow-y: auto;
box-sizing: border-box;
padding: 5px 10px;
}
}
</style>
<template>
<div class="box4">
<div class="box4-header">
<div class="header-icon">
<img src="./image1.png" alt="" />
</div>
<div class="header-title">{{ "社交媒体" }}</div>
<div class="more" @click="handleToMoreNews">{{ "更多 +" }}</div>
</div>
<div class="box4-main">
<div class="message-bubble" v-for="(item, index) in messageList" :key="index" @click="handleClickPerson(item)">
<div class="avatar-container">
<img :src="item[props.imageUrl] || avatarUser" :alt="item[props.name]" class="avatar" />
<div class="avatar-containerOne" v-if="isRepublicanParty"><img src="./image2.png" alt=""
class="avatar-imageOne" /></div>
<div class="avatar-containerTwo" v-if="isUnitedStatesSenate"><img src="./image3.png" alt=""
class="avatar-imageTwo" /></div>
</div>
<div class="bubble-container">
<div class="bubble">
<div class="bubble-header">
<span class="name">{{ item[props.name] }}</span>
<span class="meta">{{ item[props.time] }} · {{ item[props.source] }}</span>
</div>
<div class="bubble-content">
{{ item[props.content] }}
</div>
<div class="triangle"></div>
</div>
</div>
</div>
<!-- <MessageBubble v-for="(item, index) in messageList" @click="handleClickPsserson(item)"
@info-click="handleMediaClick(item)" :key="index" :avatar="item.img ? item.img : DefaultIcon1" :name="item.name"
:time="item.time" :source="item.source" :content="item.content" /> -->
<!-- <div class="box4-main-item" v-for="(item, index) in messageList" :key="index">
<div class="left" @click="handleClickPerson(item)">
<img :src="item.img ? item.img : DefaultIcon1" alt="" />
</div>
<div class="right">
<div class="right-top">
<div class="name">{{ item.name }}</div>
<div class="time">{{ item.time }}</div>
</div>
<div class="content">{{ item.content }}</div>
</div>
</div> -->
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import avatarUser from "@/assets/images/avatar_user.png";
const emit = defineEmits(["click", "info-click"]);
const props = defineProps({
isRepublicanParty: {
type: Boolean,
default: false
},
isUnitedStatesSenate: {
type: Boolean,
default: false
},
messageList: {
type: Array,
default: () => []
},
imageUrl: {
type: String,
default: "imageUrl"
},
name: {
type: String,
default: "name"
},
time: {
type: String,
default: "time"
},
source: {
type: String,
default: "source"
},
content: {
type: String,
default:
"content"
}
});
const formattedTime = computed((index) => {
const date = new Date(index);
if (isNaN(date.getTime())) {
return index; // 如果不是有效日期,返回原值
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate());
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
// return `${year}年${month}月${day}日 ${hours}:${minutes}:${seconds}`;
return `${month}${day}${hours}:${minutes}`;
});
const handleClickPerson = (item) => {
emit("person-click", item);
};
const handleInfoClick = (item) => {
emit("info-click", item);
};
const handleToMoreNews = (item) => {
emit("more-click", item);
};
</script>
<style scoped>
.box4 {
margin-left: 20px;
width: 792px;
height: 450px;
border-radius: 10px;
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
border: 1px solid rgb(234, 236, 238);
.box4-header {
width: 792px;
height: 48px;
border-bottom: 1px solid rgb(234, 236, 238);
display: flex;
box-sizing: border-box;
position: relative;
.header-icon {
margin-left: 18px;
margin-top: 14px;
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
}
}
.header-title {
width: 80px;
margin-top: 11px;
margin-left: 18px;
height: 26px;
color: rgb(5, 95, 194);
font-family: "Source Han Sans CN";
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
}
.more {
width: 45px;
height: 24px;
position: absolute;
top: 12px;
right: 27px;
color: rgb(5, 95, 194);
font-family: 'Source Han Sans CN';
font-size: 16px;
font-weight: 400;
line-height: 24px;
cursor: pointer;
text-align: right;
}
}
.box4-main {
height: 402px;
overflow-y: auto;
box-sizing: border-box;
padding-bottom: 8px;
padding-left: 21px;
padding-top: 23px;
.message-bubble {
display: flex;
max-width: 740px;
margin-bottom: 15px;
.avatar-container {
flex-shrink: 0;
flex-grow: 0;
flex: 0;
width: 42px;
height: 42px;
margin-right: 14.5px;
cursor: pointer;
position: relative;
.avatar-containerOne,
.avatar-containerTwo {
display: inline-block;
position: absolute;
}
.avatar-containerOne {
left: 2px;
top: 29px;
.avatar-imageOne {
width: 20px;
height: 20px;
}
}
.avatar-containerTwo {
right: 2px;
top: 29px;
.avatar-imageTwo {
width: 20px;
height: 20px;
}
}
.avatar {
width: 42px;
height: 42px;
border-radius: 50%;
object-fit: cover;
}
}
.bubble-container {
flex: 1;
position: relative;
.bubble {
background-color: rgba(246, 250, 255, 1);
border-radius: 12px;
padding: 12px 12px;
position: relative;
border: 1px solid rgba(231, 243, 255, 1);
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
.bubble-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
.name {
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-size: 16px;
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
}
.meta {
color: rgb(95, 101, 108);
font-family: "Source Han Sans CN";
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
text-align: right;
}
}
.bubble-content {
color: rgba(59, 65, 75, 1);
font-family: "Source Han Sans CN";
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.triangle {
position: absolute;
left: -9px;
/* 向左偏移1px,给描边留出空间 */
top: 15px;
width: 0;
height: 0;
/* 外层:描边颜色的三角形(比内层大1px) */
border-top: 9px solid transparent;
border-bottom: 9px solid transparent;
border-right: 9px solid rgb(231, 243, 255);
}
/* 内层:原有颜色的三角形,覆盖在外层上面,模拟描边效果 */
.triangle::after {
content: '';
position: absolute;
top: -8px;
/* 向上偏移1px,对齐中心 */
left: 1px;
/* 向右偏移1px,露出外层的描边 */
width: 0;
height: 0;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid rgba(246, 250, 255, 1);
}
}
}
}
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.message-bubble {
max-width: 100%;
}
.bubble-header {
flex-direction: column;
align-items: flex-start;
}
.meta {
margin-top: 4px;
}
}
</style>
<template>
<div class="box3">
<div class="box3-header">
<div class="box3-header-left">
<div class="box3-header-icon">
<img src="./image1.png" alt="" />
</div>
<div class="box3-header-title">{{ "新闻资讯" }}</div>
<div class="more" @click="handleToMoreNews">{{ "更多 +" }}</div>
</div>
</div>
<div class="box3-main">
<div class="box3-item" v-for="(news, index) in newsList" :key="index" @click="handleToNewsAnalysis(news)">
<div class="left">
<img :src="news[props.img] ? news[props.img] : DefaultIconNews" alt="" />
</div>
<div class="right">
<div class="right-top">
<div class="title"><span class="text-inner">{{ news[props.title] }}</span></div>
<div class="time">{{ news[props.from] }}</div>
</div>
<div class="right-footer">{{ news[props.content] }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import DefaultIconNews from "@/assets/icons/default-icon-news.png";
const props = defineProps({
// 新闻列表数据
newsList: {
type: Array,
default: () => []
},
img: {
type: String,
default: 'img'
},
title: {
type: String,
default: "title"
},
from: {
type: String,
default: "from"
},
content: {
type: String,
default: "content"
},
});
const emit = defineEmits(['item-click', 'more-click']);
const handleToMoreNews = () => {
emit('more-click')
};
const handleToNewsAnalysis = (item, index) => {
emit('item-click', item, index)
};
</script>
<style lang="scss" scoped>
.box3 {
width: 792px !important;
height: 450px !important;
border-radius: 10px !important;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1) !important;
background: rgba(255, 255, 255, 1) !important;
display: flex !important;
flex-direction: column;
gap: 0 !important;
overflow: hidden;
.box3-header {
height: 48px !important;
border-bottom: 1px solid rgba(234, 236, 238, 1) !important;
margin: 0 !important;
display: flex !important;
justify-content: space-between !important;
position: relative !important;
width: 100%;
box-sizing: border-box;
.box3-header-left {
display: flex !important;
.box3-header-icon {
margin-left: 19px !important;
margin-top: 14px !important;
width: 24px !important;
height: 24px !important;
img {
width: 100% !important;
height: 100% !important;
}
}
.box3-header-title {
margin-top: 11px !important;
margin-left: 17px !important;
height: 26px !important;
color: var(--color-main-active) !important;
font-family: 'Source Han Sans CN' !important;
font-size: 20px !important;
font-weight: 700 !important;
line-height: 26px !important;
}
}
.more {
width: 45px;
height: 24px;
position: absolute;
top: 12px;
right: 27px;
color: rgba(20, 89, 187, 1);
font-family: 'Source Han Sans CN';
font-size: 16px;
font-weight: 400;
line-height: 24px;
cursor: pointer;
}
}
.box3-main {
height: 401px;
overflow-y: auto;
overflow-x: hidden;
padding: 6px 0;
.box3-item {
display: flex;
height: 78px;
width: 749px;
margin-left: 21px;
border-bottom: 1px solid rgba(240, 242, 244, 1);
cursor: pointer;
&:hover {
.right-top .title {
color: rgb(5, 95, 194) !important;
font-weight: 700;
}
.right-top .text-inner {
border-bottom-color: rgb(5, 95, 194) !important;
}
}
.left {
width: 72px;
height: 48px;
margin-top: 15px;
img {
width: 100%;
height: 100%;
}
}
.right {
width: 657px;
margin-left: 20px;
.right-top {
width: 657px;
display: flex;
justify-content: space-between;
.title {
margin-top: 14px;
width: 500px;
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: 'Source Han Sans CN';
font-size: 16px;
font-weight: 700;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.text-inner {
border-bottom: 1px solid transparent;
}
}
.time {
width: 157px;
text-align: right;
height: 22px;
margin-top: 14px;
color: rgba(95, 101, 108, 1);
font-family: 'Source Han Sans CN';
font-size: 14px;
font-weight: 400;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.right-footer {
width: 657px;
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: 'Source Han Sans CN';
font-size: 16px;
font-weight: 400;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="left-btn-wrapper">
<img src="@/assets/images/icon/card-btn-left.png" alt="">
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.left-btn-wrapper {
width: 24px;
height: 48px;
img {
width: 100%;
height: 100%;
}
}
</style>
\ No newline at end of file
<template>
<div class="right-btn-wrapper">
<img src="@/assets/images/icon/card-btn-right.png" alt="">
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.right-btn-wrapper {
width: 24px;
height: 48px;
img {
width: 100%;
height: 100%;
}
}
</style>
\ No newline at end of file
<template>
<div class="box2">
<div class="box2-header">
<div class="icon">
<img src="./image1.png" alt="" />
</div>
<div class="title">
<div class="text">{{ title }}</div>
<div class="num">{{ list.length }}</div>
</div>
</div>
<div class="box2-main">
<div class="box2-main-item" v-for="(item, index) in list" :key="index" @click="handleItemClick(item, index)">
<div :class="{
itemLeftStatus1: item[props.riskLevel] === '特别重大',
itemLeftStatus2: item[props.riskLevel] === '重大风险',
itemLeftStatus3: item[props.riskLevel] === '较大风险',
itemLeftStatus4: item[props.riskLevel] === '一般风险' || !item[props.riskLevel],
itemLeftStatus5: item[props.riskLevel] === '低风险',
}">
{{ item[props.riskLevel] || "暂无数据" }}
</div>
<div class="item-right">
<div class="text"> <span class="text-inner">{{ item[props.name] }}</span></div>
<div class="time">{{ item[props.postDate] }}</div>
</div>
</div>
</div>
<div class="box2-footer" @click="handleMoreClick" v-if="showMore">
<div class="icon">
<img src="./image2.png" alt="" />
</div>
<div class="text">{{ moreText }}</div>
</div>
</div>
</template>
<script setup>
import { ElMessage } from "element-plus";
// 接收父组件传递的参数
const props = defineProps({
// 标题(默认“风险信号”)
title: {
type: String,
default: "风险信号"
},
// 风险信号列表数据
list: {
type: Array,
default: () => []
},
// “查看更多”文本(默认“查看更多”)
moreText: {
type: String,
default: "查看更多"
},
//控制“查看更多”是否显示,默认显示
showMore: {
type: Boolean,
default: true
},
name: {
type: String,
default: "name"
},
postDate: {
type: String,
default: "postDate"
},
riskLevel: {
type: String,
default: "riskLevel"
},
});
// 定义自定义事件,把点击事件传递给父组件
const emit = defineEmits(['item-click', 'more-click']);
// 点击单条风险信号
const handleItemClick = (item, index) => {
emit('item-click', item, index)
};
// 点击“查看更多”
const handleMoreClick = () => {
emit('more-click')
};
</script>
<style scoped lang="scss">
.risk-status-base {
width: 40px;
height: 40px;
border-radius: 20px;
font-size: 12px;
font-weight: 400;
line-height: 14px;
box-sizing: border-box;
padding: 6px 4px;
text-align: center;
}
.itemLeftStatus1 {
color: rgb(206, 79, 81) !important;
background: rgba(255, 241, 240, 1) !important;
@extend .risk-status-base
}
.itemLeftStatus2 {
color: rgba(250, 140, 22, 1) !important;
background: rgba(255, 247, 230, 1) !important;
@extend .risk-status-base
}
.itemLeftStatus3 {
color: rgba(212, 177, 6, 1) !important;
background: rgba(254, 255, 230, 1) !important;
@extend .risk-status-base
}
.itemLeftStatus4 {
color: rgba(82, 196, 26, 1) !important;
background: rgba(246, 255, 237, 1) !important;
@extend .risk-status-base
}
.itemLeftStatus5 {
color: rgba(22, 119, 255, 1) !important;
background: rgba(230, 244, 255, 1) !important;
@extend .risk-status-base
}
.box2 {
width: 520px;
height: 450px;
border-radius: 10px;
position: relative;
background: rgba(255, 255, 255, 1);
padding: 0;
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
border: 1px solid rgba(234, 236, 238, 1);
box-sizing: border-box;
overflow: hidden;
.box2-header {
height: 48px;
display: flex;
border-bottom: 1px solid rgba(240, 242, 244, 1);
.icon {
width: 24px;
height: 24px;
margin-left: 18px;
margin-top: 14px;
margin-bottom: 10px;
img {
width: 100%;
height: 100%;
}
}
.title {
display: flex;
width: 148px;
background: rgb(206, 79, 81);
margin-left: 18px;
.text {
margin-left: 16px;
height: 48px;
color: rgba(255, 255, 255, 1);
font-family: 'Source Han Sans CN';
font-size: 20px;
font-weight: 700;
line-height: 48px;
}
.num {
width: 24px;
height: 20px;
line-height: 20px;
text-align: center;
color: rgba(255, 255, 255, 1);
font-family: Microsoft YaHei;
font-size: 12px;
margin-left: 15px;
margin-top: 15px;
border-radius: 100px;
background: rgba(255, 255, 255, 0.3);
}
}
}
.box2-main {
box-sizing: border-box;
padding-left: 23px;
padding-right: 30px;
overflow-y: auto;
width: 520px;
height: calc(100% - 160px);
border-radius: 4px;
.box2-main-item {
width: 463px;
height: 48px;
border-radius: 2px;
position: relative;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
.item-right .text {
color: rgb(5, 95, 194) !important;
font-weight: 700;
}
.item-right .text-inner {
border-bottom-color: rgb(5, 95, 194) !important;
}
}
.item-left {
margin-top: 4px;
margin-left: 0px;
margin-bottom: 4px;
width: 40px;
height: 40px;
border-radius: 20px;
color: rgba(82, 196, 26, 1);
background: rgba(246, 255, 237, 1);
font-family: Microsoft YaHei;
font-size: 12px;
font-weight: 400;
line-height: 14px;
box-sizing: border-box;
padding: 6px 4px;
text-align: center;
flex-shrink: 0;
}
.item-right {
margin-left: 12px;
height: 46px;
display: flex;
align-items: center;
flex: 1;
background: transparent;
padding: 0;
border-bottom: 1px solid #EAECEE;
box-sizing: border-box;
overflow: hidden; // 保证右侧不会溢出
.text {
padding-top: 8px;
padding-bottom: 8px;
flex: 1 1 auto;
min-width: 0;
height: 100%;
background: transparent;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
letter-spacing: 0px;
text-align: left;
color: rgb(59, 65, 75);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
flex-shrink: 1;
.text-inner {
border-bottom: 1px solid transparent;
}
}
.time {
flex: 0 0 auto;
margin-left: 12px;
padding-top: 11px;
padding-bottom: 11px;
height: 100%;
flex-shrink: 0;
background: transparent;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
box-sizing: border-box;
color: rgb(132, 136, 142);
white-space: nowrap;
}
}
}
}
.box2-footer {
position: absolute;
left: 26px;
right: 20px;
bottom: 20px;
width: 460px;
height: 42px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-radius: 6px;
background: var(--color-main-active);
cursor: pointer;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
margin-left: 8px;
color: rgba(255, 255, 255, 1);
font-family: "Source Han Sans CN";
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
}
}
</style>
\ No newline at end of file
...@@ -12,11 +12,15 @@ import "./styles/main.css"; ...@@ -12,11 +12,15 @@ import "./styles/main.css";
import '@/assets/fonts/font.css' import '@/assets/fonts/font.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs' import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import AreaTag from '@/components/areaTag.vue' import AreaTag from '@/components/base/areaTag/AreaTag.vue'
import leftBtn from "@/components/pageBtn/leftBtn.vue"; import leftBtn from "@/components/base/pageBtn/leftBtn.vue";
import rightBtn from "@/components/pageBtn/rightBtn.vue"; import rightBtn from "@/components/base/pageBtn/rightBtn.vue";
import OverviewBox from '@/components/BoxBackground/overviewBox.vue' import OverviewBox from '@/components/base/boxBackground/overviewBox.vue'
import AnalysisBox from '@/components/BoxBackground/analysisBox.vue' import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'
import NewsList from '@/components/base/newsList/NewsList.vue'
import ModuleHeader from '@/components/base/moduleHeader/index.vue'
import RiskSignal from "@/components/base/riskSignal/RiskSignal.vue";
import MessageBubble from "@/components/base/messageBubble/MessageBubble.vue";
// 引入 Pinia 实例 // 引入 Pinia 实例
import pinia from './stores' import pinia from './stores'
...@@ -36,8 +40,13 @@ app.use(ElementPlus, { ...@@ -36,8 +40,13 @@ app.use(ElementPlus, {
app.use(pinia) // 挂载 Pinia app.use(pinia) // 挂载 Pinia
app.component("CardTitle", CardTitle); app.component("CardTitle", CardTitle);
app.component('AreaTag', AreaTag) // 领域标签 app.component('AreaTag', AreaTag) // 领域标签
app.component('leftBtn', leftBtn) app.component('leftBtn', leftBtn) // 向左按钮
app.component('rightBtn', rightBtn) app.component('rightBtn', rightBtn) // 向右按钮
app.component('OverviewBox', OverviewBox) // 概览页模块背景 app.component('OverviewBox', OverviewBox) // 概览页模块背景
app.component('AnalysisBox', AnalysisBox) // 分析页模块背景 app.component('AnalysisBox', AnalysisBox) // 分析页模块背景
app.component('ModuleHeader', ModuleHeader) // 模块头部
app.component('RiskSignal', RiskSignal) // 风险信号
app.component('NewsList', NewsList) // 新闻资讯
app.component('MessageBubble', MessageBubble) // 社交媒体
app.mount("#app"); app.mount("#app");
...@@ -709,7 +709,7 @@ import Ghd from "@/assets/icons/ghd.png"; ...@@ -709,7 +709,7 @@ import Ghd from "@/assets/icons/ghd.png";
import Mzd from "@/assets/icons/mzd.png"; import Mzd from "@/assets/icons/mzd.png";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import AreaTag from "@/components/areaTag.vue"; import AreaTag from "@/components/AreaTag.vue";
const searchBillText = ref(""); const searchBillText = ref("");
......
...@@ -262,7 +262,7 @@ ...@@ -262,7 +262,7 @@
<template #reference> <template #reference>
<div class="info-content">{{ item.content ? item.content : "暂无数据" }}</div> <div class="info-content">{{ item.content ? item.content : "暂无数据" }}</div>
</template> </template>
</el-popover> --> </el-popover> -->
</div> </div>
</div> </div>
</div> </div>
...@@ -1409,16 +1409,13 @@ onMounted(async () => { ...@@ -1409,16 +1409,13 @@ onMounted(async () => {
} }
.home-main { .home-main {
position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
// background: url("./assets/images/background.png");
// background-size: 100% 100%;
.home-top-bg { .home-top-bg {
background: background: url("./assets/images/background.png"), linear-gradient(180deg, rgba(229, 241, 254, 1) 0%, rgba(246, 251, 255, 0) 30%);
url("./assets/images/background.png"),
linear-gradient(180deg, rgba(229, 241, 254, 1) 0%, rgba(246, 251, 255, 0) 30%);
background-size: 100% 100%; background-size: 100% 100%;
position: absolute; position: absolute;
width: 100%; width: 100%;
......
<template> <template>
<div class="home-wrapper"> <div class="home-wrapper">
<!-- <div class="search-header" v-show="isShow">
=======
<div class="search-header" v-show="isShow">
<div class="home-main-header-center"> <div class="home-main-header-center">
<el-input v-model="searchExportControlText" @keyup.enter="handleSearch" style="width: 800px; height: 100%" <el-input v-model="searchExportControlText" @keyup.enter="handleSearch" style="width: 800px; height: 100%"
placeholder="搜索出口管制" /> placeholder="搜索出口管制" />
...@@ -40,7 +37,7 @@ ...@@ -40,7 +37,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div> -->
<!-- <div class="home-header" v-show="!isShow"> <!-- <div class="home-header" v-show="!isShow">
<div class="header-left"> <div class="header-left">
......
<template>
<div class="effect-analysis">
<div class="placeholder-card">
<div class="card-header">
<CardTitle title="影响分析" />
<el-icon class="expand-icon"><TrendCharts /></el-icon>
</div>
<div class="card-body">
<div class="placeholder-content">
<h3 class="placeholder-title">影响分析功能开发中</h3>
<el-empty description="暂无数据" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { TrendCharts } from '@element-plus/icons-vue'
import CardTitle from '@/components/CardTitle.vue'
</script>
<style scoped>
.effect-analysis {
/* padding: 20px 0; */
}
.placeholder-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
min-height: 500px;
}
.card-header {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 40px 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.expand-icon {
color: #999;
}
.placeholder-content {
display: flex;
flex-direction: column;
align-items: center;
max-width: 400px;
}
.placeholder-icon {
margin-bottom: 24px;
color: #d3d3d3;
}
.placeholder-title {
font-size: 20px;
color: #333;
margin: 0 0 16px 0;
}
.placeholder-description {
font-size: 14px;
color: #666;
margin-bottom: 20px;
line-height: 1.6;
}
.feature-list {
list-style: none;
padding: 0;
margin: 0 0 30px 0;
text-align: left;
}
.feature-list li {
padding: 8px 0;
color: #666;
font-size: 14px;
position: relative;
padding-left: 20px;
}
.feature-list li::before {
content: "•";
color: #409eff;
font-weight: bold;
position: absolute;
left: 0;
}
</style>
<template>
<div class="overview-container">
<el-row :gutter="20">
<!-- 左侧内容 -->
<el-col :span="8">
<!-- 内容摘要 -->
<div class="content-card">
<div class="card-header">
<CardTitle title="内容摘要" />
<el-icon class="expand-icon"><ArrowRight /></el-icon>
</div>
<div class="card-body">
<div class="summary-content">
<div v-html="props.reportSummary.summary"></div>
<el-button type="text" class="more-btn">查看更多 ></el-button>
</div>
</div>
</div>
<!-- 涉及科技领域 -->
<div class="content-card tech-fields" style="margin-top: 20px;">
<div class="card-header">
<CardTitle title="涉及科技领域" />
<el-icon class="expand-icon"><ArrowRight /></el-icon>
</div>
<div class="card-body">
<div class="tech-tags">
<el-tag type="primary" size="small" v-for="tag in props.reportSummary.researchTypes" :key="tag">{{ tag }}</el-tag>
</div>
<WordCloud :words="props.reportSummary.researchTypes" />
</div>
</div>
</el-col>
<!-- 右侧主要内容 -->
<el-col :span="16">
<div class="content-card main-content-card">
<div class="card-header">
<CardTitle title="主要内容" />
<div class="content-actions">
<el-button type="plain" size="small">核心发现</el-button>
<el-button type="plain" size="small">政策建议</el-button>
<el-icon class="expand-icon"><Paperclip /></el-icon>
</div>
</div>
<div class="card-body">
<div class="main-content-list">
<div v-for="(item, index) in mainContentList" :key="item.id" class="content-item">
<!-- 左侧:序号 -->
<div class="item-number">{{ item.serialNum }}</div>
<!-- 中间:标题和内容 -->
<div class="item-main">
<h4 class="item-title">{{ item.content }}</h4>
<p class="item-content">{{ item.econtent }}</p>
</div>
<!-- 右侧:标签 -->
<div class="item-tags">
<el-tag
v-for="tag in item.tags"
:key="tag.name"
:type="tag.type"
size="small"
class="content-tag"
>
{{ tag.name }}
</el-tag>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="totalElements"
layout="prev, pager, next"
:pager-count="5"
small
/>
<span class="total-info">{{ totalElements }}</span>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Paperclip } from '@element-plus/icons-vue'
import CardTitle from '@/components/CardTitle.vue'
import { mockMainContentList } from '../mockData'
import WordCloud from './WordCloud.vue'
const props = defineProps({
reportSummary: {
type: Object,
required: true
},
content: {
type: Object,
required: true
}
})
const currentPage = ref(1)
const totalElements = ref(6)
const pageSize = ref(10)
// 标签类型映射 - 根据限制方式设置不同的标签颜色
const getTagType = (tagName) => {
const tagTypeMap = {
'出口管制': 'danger',
'资本管制': 'warning',
'技术封锁': 'danger',
'金融制裁': 'danger',
'对台军售': 'warning',
'关税贸易': 'primary',
'供应链打击': 'warning'
}
return tagTypeMap[tagName] || 'info'
}
// 处理接口数据的方法
const processApiData = (apiResponse) => {
if (!apiResponse || !apiResponse.content) return []
return apiResponse.content.map(item => ({
id: item.id,
title: item.content, // 使用content作为标题
content: item.content, // 内容
econtent: item.econtent,
serialNum: item.serialNum,
tags: item.xzfsList.map(tag => ({
name: tag,
type: getTagType(tag)
}))
}))
}
// 更新分页信息的方法
const updatePagination = (apiResponse) => {
if (apiResponse) {
totalElements.value = apiResponse.totalElements || 0
pageSize.value = apiResponse.size || 10
currentPage.value = (apiResponse.number || 0) + 1 // API返回的是0基础的页码
}
}
const mainContentList = ref(mockMainContentList)
</script>
<style scoped>
.overview-container {
padding: 0;
}
.content-card {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.card-header {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px;
}
.expand-icon {
color: #999;
cursor: pointer;
margin-left: 10px;
}
.summary-content {
line-height: 1.8;
color: #666;
}
.summary-content p {
margin-bottom: 16px;
text-align: justify;
}
.more-btn {
color: #409eff;
padding: 0;
}
.tech-tags {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.tag-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tech-tag {
background: #f5f7fa;
border: 1px solid #dcdfe6;
color: #606266;
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
}
.tech-tag.ai-chip {
background: #fff7e6;
border-color: #ffd591;
color: #fa8c16;
}
.content-actions {
display: flex;
align-items: center;
}
.main-content-list {
margin-bottom: 20px;
}
.content-item {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.content-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.item-number {
width: 28px;
height: 28px;
background: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
flex-shrink: 0;
margin-top: 2px;
}
.item-main {
flex: 1;
min-width: 0;
}
.item-title {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
line-height: 1.4;
}
.item-content {
margin: 0;
line-height: 1.6;
color: #4b5563;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-tags {
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
min-width: 120px;
align-items: flex-end;
}
.content-tag {
font-size: 12px;
height: 22px;
line-height: 20px;
padding: 0 8px;
white-space: nowrap;
}
.pagination-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.total-info {
font-size: 12px;
color: #999;
}
.pagination-wrapper :deep(.el-pagination) {
--el-pagination-font-size: 12px;
}
.pagination-wrapper :deep(.el-pagination .el-pager li) {
min-width: 28px;
height: 28px;
line-height: 28px;
}
.pagination-wrapper :deep(.el-pagination .btn-prev),
.pagination-wrapper :deep(.el-pagination .btn-next) {
height: 28px;
line-height: 28px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.content-item {
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 20px;
}
.item-number {
align-self: flex-start;
}
.item-main {
order: 2;
}
.item-tags {
order: 3;
flex-direction: row;
align-items: flex-start;
flex-wrap: wrap;
min-width: auto;
}
.item-title {
font-size: 15px;
}
.item-content {
font-size: 13px;
}
}
</style>
<template>
<div class="word-cloud-container">
<svg :width="width" :height="height" ref="svgRef">
</svg>
<div v-if="!words || words.length === 0" class="no-data-message">
{{ emptyText }}
</div>
</div>
</template>
<script>
import { defineComponent, ref, watch, onMounted, nextTick } from 'vue';
import * as d3 from 'd3';
import cloud from 'd3-cloud';
// 默认颜色数组,您可以根据需要扩展
const defaultColors = [
'#f56c6c', // 红色
'#e6a23c', // 橙色
'#67c23a', // 绿色
'#409eff', // 蓝色
'#909399', // 灰色
'#b3a2c7', // 紫色
'#49a37e', // 军绿
];
export default defineComponent({
name: 'WordCloud',
props: {
// 词汇数组,例如:['通用人工智能', 'AI芯片', '出口管制']
words: {
type: Array,
required: true,
default: () => [],
},
// 词云容器的宽度
width: {
type: [Number, String],
default: 600,
},
// 词云容器的高度
height: {
type: [Number, String],
default: 400,
},
// 词汇的最小字号
minFontSize: {
type: Number,
default: 16,
},
// 词汇的最大字号
maxFontSize: {
type: Number,
default: 50,
},
// 词汇颜色数组
colors: {
type: Array,
default: () => defaultColors,
},
// 词汇为空时的提示文本
emptyText: {
type: String,
default: '暂无词云数据',
},
},
setup(props) {
const svgRef = ref(null);
/**
* @description 根据词汇数组,计算词频,并转换成d3-cloud所需的格式
* @param {Array<string>} rawWords 原始词汇数组
* @returns {Array<{text: string, size: number}>} 转换后的数据
*/
const prepareData = (rawWords) => {
if (!rawWords || rawWords.length === 0) return [];
// 1. 计算词频
const wordCounts = rawWords.reduce((acc, word) => {
acc[word] = (acc[word] || 0) + 1;
return acc;
}, {});
// 2. 转换为d3-cloud所需的格式,并找出最大/最小词频
let minCount = Infinity;
let maxCount = -Infinity;
const data = Object.entries(wordCounts).map(([text, count]) => {
if (count < minCount) minCount = count;
if (count > maxCount) maxCount = count;
return { text, count };
});
// 3. 将词频映射到字号范围
const sizeScale = d3
.scaleLinear()
.domain([minCount, maxCount])
.range([props.minFontSize, props.maxFontSize]);
return data.map((d) => ({
...d,
size: sizeScale(d.count), // size就是字号
}));
};
/**
* @description 绘制词云
* @param {Array<{text: string, size: number, x: number, y: number, rotate: number}>} words 布局后的词汇数据
*/
const drawCloud = (words) => {
const svg = d3.select(svgRef.value);
// 清空SVG内容
svg.selectAll('*').remove();
// 创建一个<g>元素并平移到中心
const g = svg
.append('g')
.attr(
'transform',
`translate(${props.width / 2}, ${props.height / 2})`
);
// 绑定数据并创建<text>元素
const wordElements = g.selectAll('text').data(words, (d) => d.text);
// 进入(Enter)阶段:添加新的<text>元素
wordElements
.enter()
.append('text')
.style('font-size', (d) => `${d.size}px`)
.style('fill', (_, i) => props.colors[i % props.colors.length]) // 循环使用颜色
.attr('text-anchor', 'middle') // 文本居中
// *********** 关键修改:移除悬浮样式和交互,保持水平布局 ***********
.attr('transform', (d) => `translate(${d.x}, ${d.y})`) // 移除rotate属性
.text((d) => d.text);
// ************************************************************
// 退出(Exit)阶段:移除多余的<text>元素(如果更新时词汇减少)
wordElements.exit().remove();
};
/**
* @description 启动d3-cloud布局引擎
* @param {Array<{text: string, count: number, size: number}>} data 待布局的词汇数据
*/
const generateLayout = (data) => {
if (!data || data.length === 0) {
d3.select(svgRef.value).selectAll('*').remove();
return;
}
const layout = cloud()
.size([props.width, props.height]) // 词云的尺寸
.words(data) // 传入词汇数据
.padding(5) // 词汇之间的最小间距
// *********** 关键修改:固定旋转角度为 0,实现水平布局 ***********
.rotate(0)
// ************************************************************
.font('Impact') // 字体
.fontSize((d) => d.size) // 使用计算出的字号
.on('end', drawCloud); // 布局计算完成后调用drawCloud
layout.start(); // 启动布局
};
/**
* @description 核心渲染函数,处理数据并启动布局
*/
const renderWordCloud = () => {
// 保证在DOM更新后执行
nextTick(() => {
const processedData = prepareData(props.words);
generateLayout(processedData);
});
};
onMounted(renderWordCloud);
// 监听props变化,重新渲染词云
watch(() => [props.words, props.width, props.height, props.minFontSize, props.maxFontSize], () => {
renderWordCloud();
}, { deep: true });
return {
svgRef,
renderWordCloud,
};
},
});
</script>
<style lang="scss">
.word-cloud-container {
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: v-bind(width + 'px'); // 动态绑定宽度
height: v-bind(height + 'px'); // 动态绑定高度
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
// 词云SVG的样式
svg {
display: block; // 移除底部空白
}
// 词汇文本的通用样式
text {
font-weight: bold;
// *********** 关键修改:移除transition和cursor,避免交互 ***********
}
.no-data-message {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
color: #909399;
background-color: #f5f7fa;
}
}
</style>
\ No newline at end of file
<template>
<div class="funding-source-container">
<div class="chart-header">
<CardTitle title="经费来源" />
<div class="header-icons">
<el-icon><Coin /></el-icon>
<el-icon><Download /></el-icon>
<el-icon><Star /></el-icon>
</div>
</div>
<div class="chart-body">
<div class="stats-panel">
<div class="stat-card total-card">
<div class="label">总计</div>
<div class="value">4.358亿美元</div>
</div>
<div class="stat-card govt-card">
<div class="label">政府部门</div>
<div class="value">3.271亿美元</div>
</div>
<div class="stat-card other-card">
<div class="label">其他机构</div>
<div class="value">1.087亿美元</div>
</div>
</div>
<div class="chart-panel" ref="chartRef"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts';
import { Coin, Download, Star } from '@element-plus/icons-vue';
import CardTitle from '@/components/CardTitle.vue';
// 1. 模拟数据
// 注意:为了还原图表,这里的数据是凑出来的近似值,确保比例看起来像截图
const chartData = [
// 右侧数据 (通常从12点顺时针开始)
{ value: 7830, name: '美国国土安全部', percent: '21%' },
{ value: 7290, name: '美国办公室国防部长和...', percent: '21%' },
{ value: 6740, name: '美国卫生与公众服务部...', percent: '18%' },
{ value: 4840, name: '美国空军', percent: '18%' },
{ value: 3880, name: '美国陆军', percent: '16%' },
{ value: 3520, name: '捐款', percent: '16%' },
// 左侧数据
{ value: 3110, name: '基金', percent: '14%' },
{ value: 2905, name: '大学', percent: '12%' },
{ value: 2840, name: '私营部门', percent: '12%' },
{ value: 2400, name: '州和地方政府机构', percent: '12%' },
{ value: 2130, name: '其他非营利组织', percent: '11%' },
{ value: 2060, name: '非美国政府机构和国际...', percent: '8%' },
{ value: 1850, name: '其他联邦机构', percent: '8%' },
{ value: 1200, name: '其他', percent: '8%' },
];
// 颜色盘 (从截图吸取的近似色)
const colorPalette = [
'#8cbbf1', // 浅蓝
'#a5d67d', // 浅绿
'#f6c469', // 橙黄
'#fdf27e', // 黄色
'#94e6d6', // 青绿
'#6b85ef', // 深蓝紫
'#d3d7fd', // 极浅蓝
'#d9f3b2', // 极浅绿
'#eb7d7d', // 红
'#a28ee3', // 紫
'#f4a678', // 橙
'#6ba7f5', // 蓝
'#f5a8a8', // 浅红
];
const chartRef = ref(null);
let myChart = null;
const initChart = () => {
if (!chartRef.value) return;
myChart = echarts.init(chartRef.value);
const option = {
color: colorPalette,
tooltip: {
trigger: 'item',
formatter: '{b}: {c}万 ({d}%)'
},
series: [
{
name: '经费来源',
type: 'pie',
radius: ['45%', '60%'], // 环形图半径
center: ['50%', '50%'], // 居中
data: chartData,
// 标签配置
label: {
show: true,
position: 'outside',
formatter: function (params) {
// 这里的逻辑是为了模仿截图:右边的文字名字在右侧,左边的文字名字在左侧
// 简单的判断逻辑:基于 ECharts 内部计算的 label 角度,或者根据数据索引
// 这里我们构建一个富文本结构
return `{name|${params.name}}\n{val|${params.value}万} {pct|${params.data.percent}}`;
},
// 关键配置:使用 edge 对齐方式让标签像表格一样排列在两侧
alignTo: 'edge',
edgeDistance: 10, // 距离容器边缘的距离
minMargin: 5,
lineHeight: 20,
rich: {
name: {
fontSize: 13,
fontWeight: 'bold',
color: '#333',
padding: [0, 5]
},
val: {
fontSize: 12,
color: '#666'
},
pct: {
fontSize: 12,
color: '#666',
padding: [0, 5]
}
}
},
// 引导线配置
labelLine: {
length: 15,
length2: 60, // 第二段线长一点,以便连接到边缘
maxSurfaceAngle: 80
},
// 每一项的样式
itemStyle: {
borderColor: '#fff',
borderWidth: 2
}
}
]
};
myChart.setOption(option);
};
// 响应式处理
const resizeHandler = () => {
if (myChart) {
myChart.resize();
}
};
onMounted(() => {
nextTick(() => {
initChart();
window.addEventListener('resize', resizeHandler);
});
});
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
if (myChart) {
myChart.dispose();
}
});
</script>
<style lang="scss" scoped>
.funding-source-container {
width: 100%;
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
// 头部样式
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.title-wrapper {
display: flex;
align-items: center;
.blue-bar {
width: 4px;
height: 18px;
background-color: #409eff; // Element Plus Primary Blue
margin-right: 8px;
border-radius: 2px;
}
.title-text {
font-size: 18px;
font-weight: 700;
color: #303133;
}
}
.header-icons {
display: flex;
gap: 15px;
color: #909399;
cursor: pointer;
.el-icon {
font-size: 18px;
&:hover {
color: #409eff;
}
}
}
}
// 主体布局
.chart-body {
display: flex;
flex-direction: row;
height: 450px; // 固定一个高度给图表展示
// 左侧统计面板
.stats-panel {
width: 200px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 20px;
flex-shrink: 0;
.stat-card {
padding: 15px;
border-radius: 6px;
display: flex;
flex-direction: column;
gap: 8px;
.label {
font-size: 14px;
}
.value {
font-size: 20px;
font-weight: 800;
}
// 不同卡片的特定样式
&.total-card {
background-color: #eef6ff; // 浅蓝背景
.label { color: #409eff; }
.value { color: #185ebd; }
}
&.govt-card {
background-color: #fff2f2; // 浅红背景
.label { color: #f56c6c; }
.value { color: #c43e3e; }
}
&.other-card {
background-color: #f0f9eb; // 浅绿背景
.label { color: #67c23a; }
.value { color: #3a8e1e; }
}
}
}
// 右侧图表区域
.chart-panel {
flex: 1;
min-width: 0; // 防止 flex 子项溢出
height: 100%;
}
}
}
// 移动端适配微调
@media (max-width: 768px) {
.chart-body {
flex-direction: column !important;
height: auto !important;
.stats-panel {
width: 100% !important;
flex-direction: row !important;
overflow-x: auto;
padding-bottom: 10px;
}
.chart-panel {
height: 400px !important;
}
}
}
</style>
\ No newline at end of file
<template>
<el-row :gutter="24">
<el-col :span="6">
<aside class="filter-sidebar">
<div class="filter-group">
<CardTitle title="报告类型" style="margin-bottom: 10px"/>
<el-checkbox-group v-model="selectedReportTypes">
<el-checkbox label="研究报告" />
<el-checkbox label="简报" />
<el-checkbox label="会议记录" />
<el-checkbox label="期刊文章" />
</el-checkbox-group>
</div>
<div class="filter-group">
<CardTitle title="研究类型" style="margin-bottom: 10px"/>
<el-checkbox-group v-model="selectedResearchTypes">
<el-checkbox label="人工智能" />
<el-checkbox label="半导体/芯片" />
<el-checkbox label="能源与气候" />
<el-checkbox label="国际关系" />
<el-checkbox label="经济决策" />
<el-checkbox label="国防与安全" />
</el-checkbox-group>
</div>
<div class="filter-group">
<CardTitle title="作者" style="margin-bottom: 10px"/>
<el-input v-model="authorName" placeholder="输入作者名称" />
</div>
</aside>
</el-col>
<el-col :span="18">
<div class="report-content">
<div class="content-toolbar">
<el-input
v-model="searchQuery"
placeholder="按报告标题检索"
:prefix-icon="Search"
style="width: 240px;"
/>
<div class="sort-options">
<el-select v-model="sourceType" placeholder="来源类型" style="width: 120px; margin-right: 10px;">
<el-option label="全部" value="all"></el-option>
<el-option label="原创" value="original"></el-option>
</el-select>
<el-select v-model="publishDateSort" placeholder="发布时间" style="width: 120px;">
<el-option label="最新" value="desc"></el-option>
<el-option label="最早" value="asc"></el-option>
</el-select>
</div>
</div>
<el-row :gutter="20">
<el-col :span="8" v-for="report in reportList" :key="report.id" class="card-col">
<el-card shadow="hover" class="report-card" :body-style="{ padding: '0px' }">
<el-image :src="$withFallbackImage(report.imageUrl, report.id) " fit="cover" class="card-image" lazy></el-image>
<div class="card-content">
<h4 class="card-title">{{ report.name }}</h4>
<div class="card-meta">
<span class="card-date">{{ report.times }}</span>
<span class="card-source">{{ props.thinkTankName }}</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { Search } from '@element-plus/icons-vue';
import { getThinkTankReport } from '@/api';
import { useRoute } from 'vue-router';
import { mockReportList } from '../mockData';
const route = useRoute();
const props = defineProps({
thinkTankName: {
type: String,
required: true
}
});
// Filters State
const selectedReportTypes = ref(['研究报告', '简报']);
const selectedResearchTypes = ref(['人工智能']);
const authorName = ref('');
// Toolbar State
const searchQuery = ref('');
const sourceType = ref('all');
const publishDateSort = ref('desc');
// Mock Data for Report Cards
const reportList = ref([
{ id: 1, title: '中国对AI的转型产业政策', date: '2025年6月26日', source: '兰德科技智库', imageUrl: 'https://images.unsplash.com/photo-1611162617474-5b21e879e113?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600' },
{ id: 2, title: '中美对抗、竞争和合作将跨越人工智能通用领域的五个...', date: '2025年6月26日', source: '兰德科技智库', imageUrl: 'https://images.unsplash.com/photo-1574015974293-817f0e62f0f3?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600' },
{ id: 3, title: '中国、智慧城市和中东:地区和美国的选择', date: '2025年6月26日', source: '兰德科技智库', imageUrl: 'https://images.unsplash.com/photo-1593989931843-3f17374312b3?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600' },
{ id: 4, title: '中国对AI的转型产业政策', date: '2025年6月26日', source: '兰德科技智库', imageUrl: 'https://images.unsplash.com/photo-1695423635441-89d968a528f4?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600' },
{ id: 5, title: '中美经济竞争:复杂经济和地缘政治关系中的收益...', date: '2025年6月26日', source: '兰德科技智库', imageUrl: 'https://images.unsplash.com/photo-1616121213037-1439247c6178?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600' },
{ id: 6, title: '中国、智慧城市和中东:留给地区和美国的选择', date: '2025年6月26日', source: '兰德科技智库', imageUrl: 'https://images.unsplash.com/photo-1542372147-7a2846215392?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600' },
]);
onMounted(() => {
getThinkTankReport({ id: route.params.id }).then(res => {
// reportList.value = res.data;
reportList.value = mockReportList;
});
})
</script>
<style scoped>
.filter-sidebar .filter-group {
margin-bottom: 24px;
}
.filter-sidebar .filter-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin: 0 0 12px 0;
}
.filter-sidebar .el-checkbox-group {
display: flex;
flex-direction: column;
}
.filter-sidebar .el-checkbox {
margin-bottom: 10px;
}
.filter-sidebar .el-checkbox:last-child {
margin-bottom: 0;
}
/* Right Content Grid Styles */
.content-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-col {
margin-bottom: 20px;
}
.report-card {
cursor: pointer;
border-radius: 10px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
border: 1px solid #e4e7ed;
padding: 10px;
}
.report-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.card-image {
width: 100%;
height: 160px;
display: block;
}
.card-content {
padding: 16px;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin: 0 0 10px 0;
line-height: 1.4;
/* overflow: hidden; */
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
overflow: hidden;
-webkit-box-orient: vertical;
}
.card-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #909399;
}
</style>
\ No newline at end of file
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论