提交 ab0482ce authored 作者: yanpeng's avatar yanpeng

Merge branch 'master' into yp-dev

---
alwaysApply: true
---
# Overview
Insert overview text here. The agent will only see this should they choose to apply the rule.
```markdown
---
alwaysApply: true
---
# 前端开发(Cursor 专用执行要点)
## 1. HTML / 模板
- 属性命名:统一用小写-中线分割。动态 class 也用这种格式,使用单引号:`:class="{ 'xxx-xxx': isXxx }"`
- v-for 必须有唯一 key,`v-for + :key`(如`:key="item.id"`),禁止仅凭索引。示例:`<li v-for="item in list" :key="item.id"></li>`
- 禁止 v-if 与 v-for 同时在同一节点;如需筛选请用计算属性/方法过滤后再 v-for
- 无内容组件或标签必须自闭合,如 `<MyComp />` `<img />` `<input />`
- HTML 属性用双引号,动态属性外已双引号则用单引号
- 模板表达式保持极简,只写取值或很简单的显示;任何有数据处理(如数组/字符串操作、三元、函数执行等)必须抽到方法或计算属性完成
## 2. CSS
- 样式模块优先通用组件,确需定制严格按设计来写
- 全部样式变量统一来源于全局(如 `:root { --color-main-primary: #055fC2; ... }`)
- `<style scoped>` 默认加 scoped
- 修改子组件样式用 `:deep()`(Vue3 推荐写法),如:`.xxx :deep(.el-xxx) { ... }`
- 严禁页面大量内联 style,样式尽量写到 class 或 :style 绑定表达式
## 3. 文件系统
- 文件夹采用小驼峰,无数字/无关字符
- 每个业务模块 API 独立放模块对应文件夹
- 静态资源分全局 assets 与模块 assets:全局图片 assets/images,icon 建议 SVG,模块区分子文件夹
## 4. 组件
- 组件名大驼峰(PascalCase)。组件类型目录小驼峰,具体组件目录(如 AreaTag)大驼峰
- 单文件组件名 index.vue,多组件文件按具体功能如 LeftBtn.vue
- 优先用通用组件,业务组件只能放业务模块,不得放全局
## 5. JS/TS
- 命名统一规范
- 变量:小驼峰 userName、isVisible
- 常量:大写+下划线,如 MAX_COUNT
- 枚举:枚举名大驼峰,枚举值全大写+下划线 enum Status { SUCCESS = 'SUCCESS' }
- 普通函数:小驼峰+动词前缀 getUser/formatTime
- 事件函数:小驼峰+handle/on handleSubmit/onClose
- 布尔型函数:is/has/should + 大驼峰,如 isValid()
- 异步强制 async/await,catch 错误。禁止 Promise.then 链式嵌套(如遇回调 hell 必须拆分/抽象重写)
- 一律用 const/let 替换 var,优先 const
- 注释:
- 单行注释:后加空格,如 `// 注释内容`
- 方法/复杂模块加 /** JSDoc 注释 */
## 6. 其它
- 所有复杂渲染逻辑逻辑一律抽到计算属性或方法,模板结构保持简洁
......@@ -7,6 +7,10 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
*.rar
*.zip
*.7z
# Dependencies
node_modules
.pnpm
......@@ -14,6 +18,7 @@ node_modules
# Build outputs
dist
dist.rar
dist-ssr
*.local
......
......@@ -26,6 +26,7 @@
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"pdfjs-dist": "^5.4.449",
"pinia": "^3.0.4",
"vue": "^3.4.0",
"vue-router": "^4.2.5"
},
......@@ -1790,6 +1791,30 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.9",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.9",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.9",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.21.tgz",
......@@ -2220,6 +2245,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/birpc": {
"version": "2.9.0",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/birpc/-/birpc-2.9.0.tgz",
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
......@@ -2457,6 +2491,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/copy-anything": {
"version": "4.0.5",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/copy-anything/-/copy-anything-4.0.5.tgz",
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
"license": "MIT",
"dependencies": {
"is-what": "^5.2.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmmirror.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
......@@ -4049,6 +4098,12 @@
"node": ">=12.0.0"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
......@@ -4233,6 +4288,18 @@
"node": ">=0.10.0"
}
},
"node_modules/is-what": {
"version": "5.5.0",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/is-what/-/is-what-5.5.0.tgz",
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/is-windows/-/is-windows-1.0.2.tgz",
......@@ -5112,6 +5179,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mixin-deep": {
"version": "1.3.2",
"resolved": "https://registry.npmmirror.com/mixin-deep/-/mixin-deep-1.3.2.tgz",
......@@ -5414,6 +5487,12 @@
"@napi-rs/canvas": "^0.1.81"
}
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
......@@ -5442,6 +5521,36 @@
"node": ">=6"
}
},
"node_modules/pinia": {
"version": "3.0.4",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.7"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.5.0",
"vue": "^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.9",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.9"
}
},
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz",
......@@ -5689,6 +5798,12 @@
"node": ">=0.10.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/robust-predicates": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.2.tgz",
......@@ -6045,6 +6160,15 @@
"deprecated": "See https://github.com/lydell/source-map-url#deprecated",
"license": "MIT"
},
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/split-string/-/split-string-3.1.0.tgz",
......@@ -6154,6 +6278,18 @@
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
"node_modules/superjson": {
"version": "2.2.6",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/superjson/-/superjson-2.2.6.tgz",
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
"license": "MIT",
"dependencies": {
"copy-anything": "^4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz",
......
......@@ -35,6 +35,7 @@
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"pdfjs-dist": "^5.4.449",
"pinia": "^3.0.4",
"vue": "^3.4.0",
"vue-router": "^4.2.5"
},
......
......@@ -2,86 +2,12 @@
<div id="app">
<div class="pro-wrapper">
<div class="home-page">
<div class="navbar">
<div class="nav-left">
<div class="icon">
<img src="@/assets/icons/overview/logo.png" alt="" />
</div>
<div class="title-box">
<div
class="title"
:aria-disabled="item.disabled"
v-for="(item, index) in homeTitleList"
:key="index"
@mouseenter="handleShowMenu(index, true)"
@mouseleave="handleShowMenu(index, false)"
>
<div class="text" :class="{ textActive: homeActiveTitleIndex === index }">
{{ item.name }}
</div>
<div class="bottom-line" v-if="homeActiveTitleIndex === index"></div>
</div>
</div>
</div>
<div class="nav-right">
<div class="search-box">
<div class="input"><el-input type="text" v-model="searchText" @keyup.enter="handleSearch" /></div>
<div class="icon" @click="handleSearch">
<img src="@/assets/icons/overview/search.png" alt="" />
</div>
</div>
<div class="info-box" aria-disabled="true">
<div class="mail">
<img src="@/assets/icons/overview/mail.png" alt="" />
</div>
<div class="user">
<img src="@/assets/icons/overview/user.png" alt="" />
</div>
<div class="name">{{ "管理员" }}</div>
</div>
</div>
</div>
<div class="menu-box" v-if="isShowMenu" @mouseenter="handleHoverMenu(true)" @mouseleave="handleHoverMenu(false)">
<div class="menu-content">
<div class="menu-item" v-for="(item, index) in menuList" :key="index" @click="handleToModule(item)">
<div class="icon">
<img :src="item.icon" alt="" />
</div>
<div class="title">{{ item.title }}</div>
</div>
</div>
</div>
<ModuleHeader/>
<div class="main-container">
<router-view />
</div>
</div>
<!-- <div class="content-page" v-if="!isCurrentOverview">
<div class="navbar">
<div class="nav-brand">
<div class="brand-icon">
<img src="@/assets/icons/header-logo.png" alt="" />
</div>
<div class="brand-text" @click="handleToHome">
<div class="text-ch">某方向风险监测预警系统</div>
</div>
</div>
<div class="user-info">
<div class="email">
<img src="@/assets/icons/header-icon.png" alt="" />
</div>
<div class="avator">
<img src="@/assets/icons/header-avator.png" alt="" />
</div>
<span class="user">管理员</span>
</div>
</div>
<div class="main-container">
<router-view />
</div>
</div> -->
<div class="right-btn" aria-disabled="true">
<div class="right-btn" @click="handleClickToolBox">
<div class="item">
<div class="icon">
<img src="@/assets/icons/overview/domain.png" alt="" />
......@@ -96,7 +22,7 @@
</div>
</div>
<div class="tool-box" aria-disabled="true">
<div class="tool-box" @click="handleClickToolBox">
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon1.png" alt="" />
</div>
......@@ -127,10 +53,8 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import { Monitor, House, User, Location, Document, Bell, Message, ArrowDown } from "@element-plus/icons-vue";
import { useRouter } from "vue-router";
import { useRoute } from "vue-router";
import Breadcrumb from "@/components/BreadCrumb/index.vue";
import AiBox from "./components/AiBox.vue";
import { getPersonType } from "@/api/common/index";
// import { useDraggable } from "@vueuse/core";
......@@ -147,6 +71,7 @@ import Menu9 from "@/assets/icons/overview/menu9.png";
import Menu10 from "@/assets/icons/overview/menu10.png";
import Menu11 from "@/assets/icons/overview/menu11.png";
import Menu12 from "@/assets/icons/overview/menu12.png";
import { ElMessage } from "element-plus";
const router = useRouter();
const route = useRoute();
......@@ -196,7 +121,7 @@ const handleGetPersonType = async () => {
personTypeList.value = [];
}
window.sessionStorage.setItem("personTypeList", JSON.stringify(personTypeList.value));
} catch (error) {}
} catch (error) { }
};
const isCurrentOverview = computed(() => {
......@@ -321,6 +246,16 @@ const handleSearch = () => {
window.open(curRoute.href, "_blank");
};
const handleClickTitle = item => {
if (item.name === "主要国家科技动向感知" || item.name === "主要国家竞争科技安全") {
ElMessage.warning("当前功能正在开发中,敬请期待!");
}
};
const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
onMounted(() => {
handleGetPersonType();
});
......@@ -356,7 +291,7 @@ body {
text-align: justify;
}
.el-popper[data-popper-placement^="top"] > .el-popper__arrow:before {
.el-popper[data-popper-placement^="top"]>.el-popper__arrow:before {
display: none;
}
</style>
......@@ -371,42 +306,58 @@ body {
height: 100vh;
position: relative;
overflow: hidden;
.home-page {
width: 100%;
height: 100%;
position: relative;
.navbar {
width: 100%;
height: 64px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: linear-gradient(180deg, rgba(246, 250, 255, 0.8) 0%, rgba(255, 255, 255, 0.8) 100%);
.nav-content {
width: 1600px;
height: 64px;
margin: 0 auto;
display: flex;
justify-content: space-between;
position: relative;
.nav-left {
height: 64px;
margin-left: 161px;
display: flex;
.icon {
margin-top: 17px;
width: 29px;
height: 30px;
img {
width: 100%;
height: 100%;
}
}
.title-box {
display: flex;
height: 64px;
margin-left: 21px;
gap: 33px;
.title {
height: 64px;
cursor: pointer;
&:hover {
.text {
color: var(--color-main-active);
}
}
.text {
height: 39px;
margin-top: 12px;
......@@ -418,9 +369,11 @@ body {
line-height: 39px;
letter-spacing: 0px;
}
.textActive {
color: var(--color-main-active);
}
.bottom-line {
width: 50px;
height: 4px;
......@@ -431,11 +384,12 @@ body {
}
}
}
.nav-right {
display: flex;
justify-content: flex-end;
margin-right: 159px;
gap: 21px;
.search-box {
margin-top: 16px;
width: 300px;
......@@ -445,44 +399,55 @@ body {
border-radius: 10px;
background: rgba(231, 243, 255, 1);
display: flex;
.input {
width: 264px;
height: 36px;
}
.icon {
width: 18px;
height: 18px;
margin-left: 9px;
margin-top: 9px;
img {
width: 100%;
height: 100%;
}
}
}
.info-box {
height: 64px;
display: flex;
justify-content: flex-end;
align-items: center;
.mail {
width: 32px;
height: 32px;
margin-right: 14px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
.user {
width: 32px;
height: 32px;
margin-right: 11px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
.name {
width: 48px;
height: 30px;
......@@ -496,46 +461,54 @@ body {
}
}
}
}
.menu-box {
position: absolute;
z-index: 9999;
z-index: 999999;
width: 713px;
height: 413px;
top: 65px;
left: 140px;
top: 64px;
left: 0;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
backdrop-filter: blur(30px);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 0.8);
.menu-content {
width: 562px;
height: 348px;
margin-top: 20px;
margin-top: 8px;
margin-left: 72px;
display: flex;
flex-wrap: wrap;
.menu-item {
margin-top: 36px;
width: 280px;
height: 24px;
display: flex;
cursor: pointer;
&:hover {
.title {
color: var(--color-main-active);
font-size: 20px;
}
}
.icon {
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
}
}
.title {
margin-left: 16px;
height: 24px;
......@@ -551,6 +524,9 @@ body {
}
}
}
}
}
.main-container {
width: 100%;
height: calc(100vh - 64px);
......@@ -558,9 +534,11 @@ body {
overflow: hidden;
}
}
.content-page {
width: 100%;
height: 100%;
.navbar {
display: flex;
justify-content: center;
......@@ -572,6 +550,7 @@ body {
position: relative;
box-sizing: border-box;
height: 72px;
.nav-brand {
display: flex;
align-items: center;
......@@ -582,6 +561,7 @@ body {
.brand-icon {
width: 48px;
height: 48px;
img {
width: 100%;
height: 100%;
......@@ -590,6 +570,7 @@ body {
.brand-text {
cursor: pointer;
.text-ch {
height: 37px;
color: rgba(10, 18, 30, 1);
......@@ -648,6 +629,7 @@ body {
}
}
}
.main-container {
width: 100%;
height: calc(100vh - 72px);
......@@ -660,6 +642,8 @@ body {
position: absolute;
top: 132px;
right: 0;
z-index: 10000000000000;
.item {
width: 108px;
height: 40px;
......@@ -670,16 +654,19 @@ body {
display: flex;
margin-bottom: 8px;
cursor: pointer;
.icon {
width: 36px;
height: 36px;
margin-top: 2px;
margin-left: 2px;
img {
width: 100%;
height: 100%;
}
}
.text {
width: 36px;
height: 24px;
......@@ -714,9 +701,12 @@ body {
display: flex;
flex-direction: column;
justify-content: space-between;
.tool-item {
width: 24px;
height: 24px;
cursor: pointer;
img {
width: 100%;
height: 100%;
......@@ -752,6 +742,7 @@ body {
text-align: center;
}
}
.ai-dialog {
position: absolute;
right: 100px;
......@@ -766,6 +757,7 @@ body {
border-radius: 10px;
background: transparent;
}
:deep(.el-input__wrapper:hover) {
box-shadow: none !important;
}
......
......@@ -77,9 +77,34 @@ export function getUSGovernmentSanctionHistory(params) {
/**
* @header token
*/
export function getDepartmentList() {
export function getDepartmentList(params) {
return request({
method: 'GET',
url: `/api/organization/summaryDepartList`
url: `/api/organization/summaryDepartList`,
params
})
}
// 全政府-获取制裁手段数据
/**
* @params {orgId}
*/
export function getSanTypeList(params) {
return request({
method: 'GET',
url: `/api/rivalryIndex/sanTypeList`,
params
})
}
// 全政府-获取未来三个月内的打压遏制时间线
/**
* @params {field}
*/
export function getThreeMonthSanctionProcess(params) {
return request({
method: 'GET',
url: `/api/rivalryIndex/nextThreeMonthSanctionProcess`,
params
})
}
\ No newline at end of file
......@@ -104,22 +104,32 @@ export function getBillPostOrg(params) {
})
}
// 获取关键议员提案
// 获取涉华法案进展分布
/**
* @param {year}
*/
export function getMemberProposal(params) {
export function getBillProcess(params) {
return request({
method: 'GET',
url: `/api/BillOverview/memberProposal/${params.year}`,
url: `/bill/BillOverview/billsProcess/${params.year}`,
})
}
// 获取资源库
// 获取资源库法案
export function getBills(params, signal) {
return request({
method: 'GET',
url: `/api/BillOverview/bills`,
url: `/bill/BillOverview/bills`,
params,
signal
})
}
// 获取资源库国会议员
export function getBillsPerson(params, signal) {
return request({
method: 'GET',
url: `/bill/BillOverview/billsPerson`,
params,
signal
})
......
......@@ -64,6 +64,13 @@ export function getDecreeSummary(params) {
/**
* @param {id}
*/
// export function getDecreeReport(params) {
// return request({
// method: 'GET',
// url: `/api/administrativeOrderInfo/contentUrl/${params.id}`,
// })
// }
export function getDecreeReport(params) {
return request({
method: 'GET',
......
......@@ -20,7 +20,10 @@ export function getAllDomainCount(params) {
/**
* @header token
* @param {Object} params
* @param {String} params.byYOrM - 按月统计或按年统计
* @param {String} params.startDate - 开始日期
* @param {String} params.endDate - 结束日期
* @param {String} params.org - 机构
* @param {String} params.sanMeasures - 制裁手段
*/
export function getDomainContainmentTrend(params) {
return request({
......
......@@ -8,6 +8,6 @@
}
/* 全局一次性应用(可选) */
body {
/* body {
font-family: "YouSheBiaoTiHei", sans-serif;
}
} */
<template>
<div
class="card-title-custom"
:style="{ '--left-offset': leftOffset }"
>
{{ props.title }}
</div>
</template>
<script setup>
const props = defineProps({
title: {
type: String,
required: true
},
leftOffset: {
type: String,
default: '-22px'
}
})
</script>
<style scoped>
.card-title-custom {
font-size: 16px;
font-weight: 600;
color: #333;
position: relative;
}
.card-title-custom::before {
content: '';
position: absolute;
left: var(--left-offset, -22px);
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 16px;
background-color: #1459BB;
}
</style>
\ No newline at end of file
<template>
<div class="hello-world">
<el-card>
<template #header>
<div class="card-header">
<el-icon><Star /></el-icon>
<span>Hello World 组件</span>
</div>
</template>
<div class="content">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
<el-divider />
<div class="counter-section">
<h4>计数器示例</h4>
<div class="counter">
<el-button type="primary" :icon="Minus" @click="decrement" :disabled="count <= 0"></el-button>
<el-input-number v-model="count" :min="0" :max="100" style="margin: 0 10px;"></el-input-number>
<el-button type="primary" :icon="Plus" @click="increment" :disabled="count >= 100"></el-button>
</div>
<el-progress :percentage="count" :color="progressColor" style="margin-top: 20px;"></el-progress>
</div>
<el-divider />
<div class="theme-section">
<h4>主题切换</h4>
<el-radio-group v-model="theme" @change="handleThemeChange">
<el-radio-button label="light">浅色主题</el-radio-button>
<el-radio-button label="dark">深色主题</el-radio-button>
</el-radio-group>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Star, Plus, Minus } from '@element-plus/icons-vue'
// Props
const props = defineProps({
title: {
type: String,
default: '欢迎使用 Vue 3 + Element Plus'
},
description: {
type: String,
default: '这是一个可复用的组件示例,展示了 Vue 3 Composition API 的强大功能。'
}
})
// 响应式数据
const count = ref(0)
const theme = ref('light')
// 计算属性
const progressColor = computed(() => {
if (count.value < 30) return '#f56c6c'
if (count.value < 70) return '#e6a23c'
return '#67c23a'
})
// 方法
const increment = () => {
if (count.value < 100) {
count.value++
if (count.value === 100) {
ElMessage.success('恭喜!达到最大值!')
}
}
}
const decrement = () => {
if (count.value > 0) {
count.value--
if (count.value === 0) {
ElMessage.info('已归零')
}
}
}
const handleThemeChange = (value) => {
ElMessage.success(`已切换到${value === 'light' ? '浅色' : '深色'}主题`)
}
// 暴露给父组件的方法
defineExpose({
reset: () => {
count.value = 0
theme.value = 'light'
ElMessage.success('组件已重置')
}
})
</script>
<style scoped>
.hello-world {
margin: 20px 0;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: bold;
}
.content h3 {
color: #409eff;
margin-bottom: 12px;
}
.content p {
color: #606266;
line-height: 1.6;
}
.counter-section,
.theme-section {
text-align: center;
}
.counter-section h4,
.theme-section h4 {
color: #303133;
margin-bottom: 16px;
}
.counter {
display: flex;
align-items: center;
justify-content: center;
}
</style>
\ No newline at end of file
<template>
<div class="news-box">
<div class="box3-header">
<div class="box3-header-left">
<div class="box3-header-icon">
<img src="@/assets/images/box3-header-icon.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 list" :key="index" @click="handleClickToNewsDetail(news)">
<div class="left">
<img
:src="getProxyUrl(news.newsImage) || defaultImg"
alt=""
referrerpolicy="no-referrer"
@error="e => (e.target.src = errImg||News1)"
/>
</div>
<div class="right">
<div class="right-top">
<div class="title">{{ news.newsTitle||news.title }}</div>
<div class="time">
{{ news.newsDate ? news.newsDate.slice(5) : "" }} {{ news.newsOrg ? "-" + news.newsOrg : "" }}
</div>
</div>
<div class="right-footer">{{ news.newsContent||news.description }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import router from "@/router/index";
import News1 from "@/assets/images/news1.png"; // 错误图片
import defaultNew from "@/assets/images/default-icon-news.png"; // 默认图片
let { list,errImg,defaultImg } = defineProps({
list: {
type: Array,
default: () => []
},
defaultImg: {
type: String,
default: defaultNew
},
errImg: {
type: String,
default: ''
}
});
// 处理图片代理
const getProxyUrl = url => {
if (!url) return "";
const urlStr = String(url);
// 排除非 http 开头(相对路径)、已经是代理链接、或者是本地链接
if (
!urlStr.startsWith("http") ||
urlStr.includes("images.weserv.nl") ||
urlStr.includes("localhost") ||
urlStr.includes("127.0.0.1")
) {
return url;
}
// 移除协议头 http:// 或 https://
const cleanUrl = urlStr.replace(/^https?:\/\//i, "");
return `https://images.weserv.nl/?url=${encodeURIComponent(cleanUrl)}`;
};
const handleClickToNewsDetail = news => {
const route = router.resolve({
path: "/newsAnalysis",
query: {
newsId: news.newsId
}
});
window.open(route.href, "_blank");
};
// 查看更多新闻资讯
const handleToMoreNews = () => {
const route = router.resolve("/newsBrief");
window.open(route.href, "_blank");
};
</script>
<style lang="scss" scoped>
.news-box {
width: 792px;
height: 450px;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(25, 69, 130, 0.2);
background: rgba(255, 255, 255, 1);
.box3-header {
height: 48px;
border-bottom: 1px solid rgba(240, 242, 244, 1);
margin: 0 auto;
display: flex;
justify-content: space-between;
position: relative;
.box3-header-left {
display: flex;
.box3-header-icon {
margin-left: 21px;
margin-top: 16px;
width: 19px;
height: 19px;
img {
width: 100%;
height: 100%;
}
}
.box3-header-title {
margin-top: 11px;
margin-left: 20px;
height: 26px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
line-height: 26px;
}
}
.more {
width: 49px;
height: 24px;
position: absolute;
top: 14px;
right: 27px;
color: rgba(20, 89, 187, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
cursor: pointer;
}
}
.box3-main {
height: 402px;
overflow-y: auto;
overflow-x: hidden;
padding: 6px 0;
.box3-item {
display: flex;
height: 77px;
width: 749px;
margin-left: 21px;
border-bottom: 1px solid rgba(240, 242, 244, 1);
cursor: pointer;
&:hover {
background: var(--color-bg-hover);
}
.left {
width: 72px;
height: 48px;
margin-top: 15px;
img {
width: 100%;
height: 100%;
border-radius: 4px;
}
}
.right {
width: 657px;
margin-left: 20px;
.right-top {
width: 657px;
display: flex;
justify-content: space-between;
&:hover {
text-decoration: underline;
color: var(--color-main-active);
.title {
color: var(--color-main-active);
}
}
.title {
margin-top: 13px;
width: 500px;
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.time {
width: 157px;
text-align: right;
height: 22px;
margin-top: 19px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
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: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
</style>
<template>
<div class="policy-list">
<div
v-for="(item, index) in policyList"
:key="index"
class="policy-item"
>
<div class="item-left">
<div class="report-cover">
<img :src="$withFallbackImage(item.imageUrl, index)" alt="Report Cover" />
</div>
</div>
<div class="item-right">
<h3 class="item-title">
{{ item.content }}
</h3>
<div class="item-meta">
<span class="meta-date">{{ formatDate(item.times) }}</span>
<span class="meta-divider">·</span>
<span class="meta-source">
{{ item.name }}
<el-icon class="link-icon"><TopRight /></el-icon>
</span>
</div>
<div class="item-tags" v-if="item.tags && item.tags.length">
<span v-for="(tag, tIndex) in item.tags" :key="tIndex" class="tag-pill">
{{ tag }}
</span>
</div>
<div class="item-actions" v-if="item.statusRaw">
<div
v-for="(statusItem, sIndex) in parseStatus(item.statusRaw)"
:key="sIndex"
class="status-link"
>
<span class="status-type">{{ statusItem.type }}</span>
<span class="status-year">{{ statusItem.year }}</span>
<span class="status-name">{{ statusItem.name }}</span>
<el-icon class="arrow-icon"><Right /></el-icon>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { TopRight, Right } from '@element-plus/icons-vue'
interface PolicyItem {
content: string;
statusRaw: string; // 原始的长字符串
name: string;
times: string;
tags?: string[];
coverUrl?: string;
}
const props = defineProps<{
policyList: PolicyItem[]
}>()
// 格式化日期:2025-06-26 -> 2025年6月26日
const formatDate = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`;
}
// 解析状态字符串
// 输入: "法案 2024 《芯片科学法案》; 政令 2025 《推动美国...》"
// 输出: 数组对象
const parseStatus = (raw: string) => {
if (!raw) return [];
// 按分号分割多个条目
const items = raw.split(/[;;]/).map(s => s.trim()).filter(s => s);
return items.map(itemStr => {
// 简单正则匹配: "类型 年份 《名称》"
// 注意:这里假设数据格式比较规范,实际需根据后端数据调整
// 尝试移除书名号进行提取
const cleanStr = itemStr.replace(/[《》]/g, '');
const parts = cleanStr.split(' ');
return {
type: parts[0] || '政策',
year: parts[1] || '',
name: parts.slice(2).join(' ') || cleanStr // 剩余部分作为名称
}
});
}
</script>
<style scoped>
.policy-list {
display: flex;
flex-direction: column;
}
.policy-item {
display: flex;
padding: 20px 0;
border-bottom: 1px solid #ebeef5;
gap: 16px;
transition: background-color 0.2s;
}
.policy-item:last-child {
border-bottom: none;
}
/* 左侧封面 */
.item-left {
flex-shrink: 0;
}
.report-cover {
width: 60px;
height: 80px;
background-color: #f2f3f5;
border: 1px solid #e4e7ed;
border-radius: 2px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.report-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 右侧内容 */
.item-right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* 1. 标题 */
.item-title {
margin: 0 0 6px 0;
font-size: 16px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.4;
cursor: pointer;
}
.item-title:hover {
color: #409EFF;
}
/* 2. 元数据 */
.item-meta {
display: flex;
align-items: center;
font-size: 13px;
color: #606266;
margin-bottom: 8px;
}
.meta-divider {
margin: 0 8px;
font-weight: bold;
}
.meta-source {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
.meta-source:hover {
color: #409EFF;
}
.link-icon {
font-size: 12px;
}
/* 3. 标签 */
.item-tags {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.tag-pill {
background-color: #f2f3f5;
color: #5e6d82;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
/* 4. 底部状态链接 */
.item-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.status-link {
display: inline-flex;
align-items: center;
background-color: #ecf5ff; /* 浅蓝色背景 */
color: #409EFF; /* 蓝色文字 */
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.status-link:hover {
background-color: #d9ecff;
}
.status-type {
font-weight: bold;
margin-right: 4px;
}
.status-year {
margin-right: 4px;
}
.status-name {
margin-right: 4px;
}
.arrow-icon {
margin-left: 4px;
font-size: 12px;
}
</style>
\ No newline at end of file
<template>
<div class="policy-tracker-container">
<PolicyOverview />
<!-- 顶部搜索栏 -->
<div class="top-search-bar" v-if="props.showSearch">
<div class="search-left">
<el-input
v-model="searchQuery"
placeholder="搜索政策建议"
class="search-input"
size="default"
>
<template #suffix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div class="search-right">
<el-select v-model="dateRange" class="select-item" size="default">
<el-option label="近一年发布" value="last_year" />
<el-option label="近三年发布" value="last_three_years" />
</el-select>
<el-select v-model="sortOrder" class="select-item" size="default">
<el-option label="发布时间" value="time" />
<el-option label="热度" value="popularity" />
</el-select>
</div>
</div>
<el-row :gutter="24">
<el-col :span="6">
<aside class="filter-sidebar">
<div class="filter-group">
<CardTitle title="实施状态" style="margin-bottom: 10px"/>
<div class="checkbox-group">
<el-checkbox-group v-model="activeStatus">
<el-checkbox
v-for="status in statusFilters"
:key="status.id"
:label="status.id"
class="filter-checkbox"
>
<span
class="status-dot"
:style="`background-color: ${status.color}; border-color: ${status.borderColor || status.color || '#dcdfe6'}`"
></span>
{{ status.label }} ({{ status.count }})
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="filter-group">
<CardTitle title="科技领域" style="margin-bottom: 10px"/>
<div class="checkbox-group">
<el-checkbox-group v-model="activeTechField">
<el-checkbox
v-for="field in techFieldFilters"
:key="field.id"
:label="field.id"
class="filter-checkbox tech-checkbox"
>
{{ field.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</aside>
</el-col>
<el-col :span="18">
<div class="main-content">
<PolicyList :policyList="policies" />
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { Search } from '@element-plus/icons-vue';
import PolicyList from './PolicyList.vue';
import CardTitle from './CardTitle.vue';
import { getOverviewPolicy } from '@/api'
import PolicyOverview from '@/views/thinkTank/components/PolicyOverview.vue'
import { mockPolicyList } from '@/views/thinkTank/mockData';
const props = defineProps({
showSearch: {
type: Boolean,
default: true
}
})
// --- Sidebar Filters State & Data ---
const activeStatus = ref(['implemented']); // 改为数组支持多选
const activeTechField = ref(['ai']); // 改为数组支持多选
const statusFilters = ref([
{ id: 'implemented', label: '已实施', count: 18, color: '#e66657' },
{ id: 'partial', label: '部分实施', count: 12, color: '#e6a23c' },
{ id: 'unimplemented', label: '未实施', count: 12, color: '#909399' },
{ id: 'unknown', label: '未知状态', count: 4, color: 'transparent', borderColor: '#909399' },
]);
const techFieldFilters = ref([
{ id: 'ai', label: '人工智能' },
{ id: 'semiconductor', label: '半导体/芯片' },
{ id: 'energy', label: '能源与气候' },
{ id: 'international', label: '国际关系' },
{ id: 'economy', label: '经济决策' },
{ id: 'national_security', label: '国防与安全' },
]);
// --- Main Content State ---
const searchQuery = ref('');
const dateRange = ref('last_year');
const sortOrder = ref('time');
const policies = ref([]);
const getPolicies = async () => {
const { data } = await getOverviewPolicy({
researchTypeIds: activeTechField.value,
statusList: activeStatus.value,
})
// policies.value = data
policies.value = mockPolicyList
}
onMounted(() => {
getPolicies()
})
</script>
<style scoped>
.policy-tracker-container {
padding-top: 0;
}
/* 顶部搜索栏样式 */
.top-search-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-left {
flex: 1;
max-width: 400px;
}
.search-input {
width: 100%;
}
.search-input :deep(.el-input__wrapper) {
border-radius: 6px;
box-shadow: 0 0 0 1px #dcdfe6;
transition: box-shadow 0.3s ease;
}
.search-input :deep(.el-input__wrapper:hover) {
box-shadow: 0 0 0 1px #409EFF;
}
.search-input :deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px #409EFF;
}
.search-right {
display: flex;
align-items: center;
gap: 12px;
}
.select-item {
width: 140px;
}
.select-item :deep(.el-input__wrapper) {
border-radius: 6px;
box-shadow: 0 0 0 1px #dcdfe6;
transition: box-shadow 0.3s ease;
}
.select-item :deep(.el-input__wrapper:hover) {
box-shadow: 0 0 0 1px #409EFF;
}
.main-content {
background-color: #fff;
padding: 8px;
}
/* --- Sidebar Styles --- */
.filter-sidebar {
background-color: #fff;
padding: 20px;
border-radius: 4px;
}
.filter-group {
margin-bottom: 25px;
}
.filter-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin: 0 0 15px 0;
display: flex;
align-items: center;
}
.implementation-title {
padding-left: 12px;
position: relative;
}
.implementation-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #409EFF;
border-radius: 2px;
}
.icon-square {
display: inline-block;
width: 4px;
height: 16px;
background-color: #409EFF;
margin-right: 8px;
border-radius: 2px;
}
.filter-sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}
/* 复选框组样式 */
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-checkbox {
margin: 0 !important;
padding: 8px 12px;
border-radius: 4px;
transition: all 0.3s ease;
width: 100%;
display: flex;
align-items: center;
}
.filter-checkbox:hover {
background-color: #f0f2f5;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 0;
border: 1px solid transparent;
display: inline-block;
flex-shrink: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.top-search-bar {
flex-direction: column;
gap: 16px;
padding: 16px;
}
.search-left {
max-width: none;
width: 100%;
}
.search-right {
width: 100%;
justify-content: center;
gap: 8px;
}
.select-item {
width: 120px;
}
}
@media (max-width: 480px) {
.search-right {
flex-direction: column;
gap: 8px;
}
.select-item {
width: 100%;
max-width: 200px;
}
}
</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" v-infinite-scroll="loadMore" :infinite-scroll-disabled="allLoaded || loading"
:infinite-scroll-distance="10" v-loading="loading">
<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 {ref} from 'vue'
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')
};
const allLoaded = ref(false)
const loading = ref(false)
const currentPage = ref(0)
const pageSize = ref(7)
const loadMore = async () => {
if (allLoaded.value || loading.value) return;
// await fetchListData(true); // 传入 true 表示追加数据
ElMessage.success('追加数据')
}
// 添加初始化函数来获取数据
const fetchListData = async (append = false) => {
console.log("加载状态 =>", loading.value, allLoaded.value);
if (loading.value || allLoaded.value) return;
try {
loading.value = true;
// 调用接口获取数据
const response = await getStrategiesTopN(currentPage.value, pageSize.value, countryName.value);
const apiData = response.data || [];
// 转换数据格式
const newData =
apiData.content?.map(item => ({
...item,
img: item.countryImageUrl, // 使用默认图片或根据需要调整
title: item.titleZh,
content: item.contentZh,
time: formatTime(item.date), // 转换时间格式
tagList: item.domains
? item.domains.slice(0, 2).map(field => ({
name: field,
status: Math.floor(Math.random() * 3) + 1 // 随机分配 1-3 的状态值
}))
: []
})) || [];
if (newData.length > 0) {
if (append) {
// 追加数据
list.value = [...list.value, ...newData];
} else {
// 替换数据
list.value = [...newData];
}
// 检查是否还有更多数据(根据实际 API 响应调整判断逻辑)
if (newData.length < pageSize.value) {
allLoaded.value = true;
} else {
currentPage.value++;
}
} else {
allLoaded.value = true;
}
console.log("获取策略数据成功:", list.value);
} catch (error) {
console.error("获取策略数据失败:", error);
// 错误处理,停止加载状态但不重置数据
if (!append && list.value.length === 0) {
list.value = [];
}
} finally {
loading.value = false;
}
};
</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
<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" v-infinite-scroll="loadMore" :infinite-scroll-disabled="allLoaded || loading"
:infinite-scroll-distance="10" v-loading="loading">
<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 { ref } from 'vue'
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"
},
allLoaded: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
});
// 定义自定义事件,把点击事件传递给父组件
const emit = defineEmits(['item-click', 'more-click', 'loadMore']);
// 点击单条风险信号
const handleItemClick = (item, index) => {
emit('item-click', item, index)
};
// 点击“查看更多”
const handleMoreClick = () => {
emit('more-click')
};
// const allLoaded = ref(false)
// const loading = ref(false)
// const currentPage = ref(0)
// const pageSize = ref(7)
// const loadMore = async () => {
// if (allLoaded.value || loading.value) return;
// // await fetchListData(true); // 传入 true 表示追加数据
// ElMessage.success('追加数据')
// }
// // 添加初始化函数来获取数据
// const fetchListData = async (append = false) => {
// console.log("加载状态 =>", loading.value, allLoaded.value);
// if (loading.value || allLoaded.value) return;
// try {
// loading.value = true;
// // 调用接口获取数据
// const response = await getStrategiesTopN(currentPage.value, pageSize.value, countryName.value);
// const apiData = response.data || [];
// // 转换数据格式
// const newData =
// apiData.content?.map(item => ({
// ...item,
// img: item.countryImageUrl, // 使用默认图片或根据需要调整
// title: item.titleZh,
// content: item.contentZh,
// time: formatTime(item.date), // 转换时间格式
// tagList: item.domains
// ? item.domains.slice(0, 2).map(field => ({
// name: field,
// status: Math.floor(Math.random() * 3) + 1 // 随机分配 1-3 的状态值
// }))
// : []
// })) || [];
// if (newData.length > 0) {
// if (append) {
// // 追加数据
// list.value = [...list.value, ...newData];
// } else {
// // 替换数据
// list.value = [...newData];
// }
// // 检查是否还有更多数据(根据实际 API 响应调整判断逻辑)
// if (newData.length < pageSize.value) {
// allLoaded.value = true;
// } else {
// currentPage.value++;
// }
// } else {
// allLoaded.value = true;
// }
// console.log("获取策略数据成功:", list.value);
// } catch (error) {
// console.error("获取策略数据失败:", error);
// // 错误处理,停止加载状态但不重置数据
// if (!append && list.value.length === 0) {
// list.value = [];
// }
// } finally {
// loading.value = false;
// }
// };
const loadMore = () => {
emit('loadMore')
}
</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
<template>
<div class="search-container" v-show="!isShow">
<div class="search-type-tabs" v-if="enableBillTypeSwitch">
<div class="search-type-tab" :class="{ active: billSearchType === 'federal' }"
@click="handleChangeBillSearchType('federal')">
联邦议会
</div>
<div class="search-type-tab" :class="{ active: billSearchType === 'state' }"
@click="handleChangeBillSearchType('state')">
州议会
</div>
</div>
<div class="search-main" :class="{ 'search-main-with-tabs': enableBillTypeSwitch }">
<input v-model="store.searchBillText" :placeholder="placeholder" @keyup.enter="handleSearch"
class="search-input" />
<div class="search-btn" @click="handleSearch">
<img src="@/assets/icons/search-icon.png" alt />
搜索
</div>
</div>
<div class="search-center" v-if="countInfo.length">
<div class="search-item" v-for="info in countInfo">
<div class="search-item-num">{{ info.count }}</div>
<div class="search-item-name">{{ info.name }}</div>
</div>
</div>
<div class="search-bottom">
<div class="btn" @click="handleToPosi('position1')">
<div class="btn-text">最新动态</div>
<div class="btn-icon">
<img src="@/assets/icons/arrow-right-icon.png" alt />
</div>
</div>
<div class="btn" @click="handleToPosi('position2')">
<div class="btn-text">咨询要闻</div>
<div class="btn-icon">
<img src="@/assets/icons/arrow-right-icon.png" alt />
</div>
</div>
<div class="btn" @click="handleToPosi('position3')">
<div class="btn-text">数据总览</div>
<div class="btn-icon">
<img src="@/assets/icons/arrow-right-icon.png" alt />
</div>
</div>
<div class="btn" @click="handleToPosi('position4')">
<div class="btn-text">资源库</div>
<div class="btn-icon">
<img src="@/assets/icons/arrow-right-icon.png" alt />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, nextTick, watchEffect } from "vue";
import { useContainerScroll } from "@/hooks/useScrollShow";
import { useRouter } from "vue-router";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
const store = useWrittingAsstaintStore();
const router = useRouter();
const {
countInfo,
containerRef,
placeholder,
areaName,
enableBillTypeSwitch,
defaultBillSearchType
} = defineProps({
countInfo: {
type: Array,
default: () => []
},
containerRef: {
type: Object,
default: {}
},
placeholder: {
type: String,
default: ""
},
areaName: {
type: String,
default: "法案"
},
// 法案页专用:是否展示“联邦议会/州议会”搜索类型切换
// 其他页面默认 false,不受影响
enableBillTypeSwitch: {
type: Boolean,
default: false
},
// 法案页专用:默认搜索类型
// 可选值:'federal'(联邦议会)| 'state'(州议会)
defaultBillSearchType: {
type: String,
default: "federal"
}
});
// 法案搜索类型状态(仅在 enableBillTypeSwitch=true 时生效)
// 维护说明:
// - federal: 联邦议会
// - state: 州议会
const billSearchType = ref(defaultBillSearchType === "state" ? "state" : "federal");
const handleChangeBillSearchType = type => {
billSearchType.value = type;
};
const handleSearch = () => {
window.sessionStorage.setItem("curTabName", `搜索-${store.searchBillText}`);
if (!areaName) return;
const query = {
searchText: store.searchBillText,
areaName: areaName
};
// 法案页附带搜索类型参数,便于搜索结果页后续按类型处理
if (enableBillTypeSwitch) {
query.billSearchType = billSearchType.value;
}
const curRoute = router.resolve({
path: "/searchResults",
query
});
window.open(curRoute.href, "_blank");
};
let homeMainRef = ref(containerRef);
const { isShow } = useContainerScroll(homeMainRef);
watchEffect(() => {
if (isShow.value) {
homeMainRef.value.classList.add("scroll-main");
homeMainRef.value.classList.add("scrollHomeMain");
} else {
homeMainRef.value.classList.remove("scroll-main");
homeMainRef.value.classList.remove("scrollHomeMain");
}
store.changeIsShowSearchBar(isShow.value);
});
store.setSearchData({ placeholder, areaName, containerRef: homeMainRef });
// 锚点跳转
const handleToPosi = id => {
const element = document.getElementById(id);
if (element && homeMainRef.value) {
// 如果当前还未显示吸顶搜索栏,先强制切换状态以稳定布局
if (!isShow.value) {
isShow.value = true;
}
// 使用 nextTick 确保 DOM 状态更新(高度变化生效)后再计算
nextTick(() => {
const containerRect = homeMainRef.value.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
// 使用 getBoundingClientRect 计算元素相对于容器顶部的绝对距离,不受嵌套布局影响
const top = elementRect.top - containerRect.top + homeMainRef.value.scrollTop;
homeMainRef.value.scrollTo({
top: top,
behavior: "smooth"
});
});
}
};
</script>
<style lang="scss" scoped>
.search-container {
width: 960px;
height: 168px;
margin: 0 auto 68px auto;
.search-type-tabs {
display: flex;
align-items: flex-end;
height: 41px;
gap: 2px;
.search-type-tab {
width: 176px;
height: 41px;
line-height: 48px;
text-align: center;
border-radius: 10px 10px 0 0;
border: 1px solid rgb(255, 255, 255);
border-bottom: none;
background: rgba(255, 255, 255, 0.65);
color: rgb(95, 101, 108);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 700;
line-height: 41px;
cursor: pointer;
padding: 0 16px;
box-sizing: border-box;
}
.search-type-tab.active {
background: rgba(231, 243, 255, 1);
color: rgb(5, 95, 194);
border-color: rgb(255, 255, 255);
}
}
.search-main-with-tabs {
border-top-left-radius: 0 !important;
}
.search-center {
width: 688px;
height: 48px;
margin: 0 auto;
margin-top: 36px;
display: flex;
justify-content: space-between;
.search-item {
width: 130px;
height: 57px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
.search-item-num {
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 36px;
font-weight: 700;
line-height: 22px;
letter-spacing: 0px;
text-align: center;
color: rgba(5, 95, 194, 1);
}
.search-item-name {
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: center;
color: rgba(95, 101, 108, 1);
}
}
}
.search-main {
display: flex;
padding-right: 3px;
align-items: center;
justify-content: space-between;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
width: 960px;
height: 48px;
background-color: rgba(255, 255, 255, 0.65);
border-radius: 10px;
border: 1px solid #fff;
&:hover {
border: 1px solid var(--color-main-active);
}
.search-input {
border: none;
outline: none;
width: 838px;
height: 48px;
background-color: transparent;
font-size: 14px;
padding: 12px 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 22px;
color: rgba(59, 65, 75, 1);
&::placeholder {
color: #a8abb2;
}
}
.search-btn {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 120px;
height: 46px;
margin-right: -3px;
border-radius: 8px;
background-color: rgb(5, 95, 194);
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 22px;
color: #fff;
img {
width: 18px;
height: 18px;
margin-right: 8px;
}
}
}
.search-bottom {
width: 688px;
height: 48px;
margin: 0 auto;
margin-top: 36px;
display: flex;
justify-content: space-between;
// gap: 16px;
.btn {
display: flex;
align-items: center;
gap: 9px;
width: 160px;
height: 48px;
border: 1px solid #aed6ff;
box-sizing: border-box;
border-radius: 24px;
background: #e7f3ff;
cursor: pointer;
position: relative;
&:hover {
background: #cae3fc;
}
.btn-text {
width: 80px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 400;
line-height: 48px;
margin-left: 36px;
text-align: center;
}
.btn-icon {
position: absolute;
top: 16px;
right: 19px;
width: 6px;
height: 12px;
img {
width: 100%;
height: 100%;
}
}
}
}
}
</style>
<template>
<button class="action-button" :type="type">
{{ name }}
</button>
</template>
<script setup>
defineProps({
type: {
type: String,
default: 'normal'
},
name: {
type: String,
default: ''
}
})
</script>
<style scoped>
.action-button {
height: 28px;
padding: 0 8px;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
cursor: pointer;
line-height: 26px;
font-family: Source Han Sans CN;
font-size: 16px;
font-weight: 400;
transition: all 0.3s;
}
.action-button[type="normal"] {
background-color: rgba(255, 255, 255, 1);
color: rgba(59, 65, 75, 1);
}
.action-button[type="active"] {
background-color: rgba(231, 243, 255, 1);
color: var(--color-main-active);
border: 1px solid var(--color-main-active);
}
/* 悬停效果
.action-button[type="normal"]:hover {
background-color: #d9d9d9;
}
.action-button[type="active"]:hover {
background-color: #40a9ff;
} */
</style>
\ No newline at end of file
<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>
<button class="main-button" :type="type">
<div class="icon">
<slot name="button-icon"></slot>
</div>
<div class="button-text">{{ name }}</div>
</button>
</template>
<script setup>
defineProps({
type: {
type: String,
default: 'normal'
},
name: {
type: String,
default: ''
}
})
</script>
<style scoped>
.main-button {
height: 36px;
padding: 0 16px;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
cursor: pointer;
line-height: 34px;
font-family: var(--font-family-base);
font-size: var(--font-size-base);
font-weight: 400;
transition: all 0.3s;
}
.main-button[type="normal"] {
background-color: rgba(255, 255, 255, 1);
color: var(--color-main-primary);
}
.main-button[type="active"] {
background-color: var(--color-main-active);
color: #fff;
border: 1px solid var(--color-main-active);
}
/* 悬停效果
.main-button[type="normal"]:hover {
background-color: #d9d9d9;
}
.main-button[type="active"]:hover {
background-color: #40a9ff;
} */
</style>
\ No newline at end of file
<template>
<div class="source-tab-list-wrapper" :style="{ width: width }">
<div class="tab-item" :class="{ tabItemActive: activeSouceTabId === item.id }" v-for="item, index in sourceTabList"
:key="index" @click="handleClcikTab(item)">
{{ item.name }}
</div>
</div>
</template>
<script setup>
const props = defineProps({
width: {
type: String,
default: '1000px'
},
sourceTabList: {
type: Array,
default: [
]
},
activeSouceTabId: {
type: [String, Number],
default: ''
}
})
const emit = defineEmits('clickTab')
const handleClcikTab = (tab) => {
emit('clickTab', tab)
}
</script>
<style lang="scss" scoped>
.source-tab-list-wrapper {
height: 42px;
display: flex;
justify-content: flex-start;
gap: 12px;
.tab-item {
height: 42px;
line-height: 42px;
padding: 0 16px;
font-size: 20px;
font-weight: 400;
color: rgba(59, 65, 75, 1);
font-family: Source Han Sans CN;
text-align: center;
cursor: pointer;
&:hover {
color: var(--color-main-active);
}
}
.tabItemActive {
background: var(--color-main-active);
color: #fff;
font-weight: 700;
border-radius: 21px;
&:hover {
color: #fff;
}
}
}
</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="header-btn"></slot>
</div>
<div class="header-btn1" v-else>
<slot name="header-btn"></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-main-box-wrapper"
:style="{ width: width ? width : '1064px', height: height ? height : '450px' }">
<div class="overview-main-box-header">
<div class="header-left">
<div class="header-icon">
<slot name="header-icon"></slot>
</div>
<div class="header-title">{{ title }}</div>
</div>
<div class="header-right" @click="handleClickToDetail()">
{{ "查看详情 >" }}
</div>
</div>
<div class="wrapper-main">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const emit = defineEmits(['toDetail'])
const props = defineProps({
title: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
height: {
type: String,
default: ''
}
})
const handleClickToDetail = () => {
emit('toDetail')
}
</script>
<style lang="scss" scoped>
.overview-main-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);
position: relative;
.overview-main-box-header {
height: 48px;
border-bottom: 1px solid rgba(240, 242, 244, 1);
display: flex;
justify-content: space-between;
box-sizing: border-box;
.header-left {
display: flex;
.header-icon {
width: 18px;
height: 18px;
margin-top: 13.7px;
margin-left: 18.7px;
}
.header-title {
margin-left: 21px;
height: 48px;
padding: 0 16px;
background: var(--color-main-active);
color: #fff;
font-family: Source Han Sans CN;
font-size: 20px;
font-weight: 700;
line-height: 48px;
text-align: center;
}
}
.header-right {
margin-right: 27px;
margin-top: 12px;
height: 24px;
color: rgba(20, 89, 187, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
cursor: pointer;
}
}
.wrapper-main {
height: calc(100% - 48px);
overflow: hidden;
// position: relative;
}
}
</style>
<template>
<div class="overview-normal-box-wrapper"
:style="{ width: width ? width : '1064px', height: height ? height : '460px' }">
<div class="overview-normal-box-header">
<div class="header-left">
<div class="header-icon">
<slot name="header-icon"></slot>
</div>
<div class="header-title">{{ title }}</div>
</div>
<div class="header-right">
<slot name="header-right"></slot>
</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: ''
}
})
</script>
<style lang="scss" scoped>
.overview-normal-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);
position: relative;
.overview-normal-box-header {
height: 48px;
border-bottom: 1px solid rgba(240, 242, 244, 1);
display: flex;
justify-content: space-between;
box-sizing: border-box;
.header-left {
display: flex;
.header-icon {
width: 24px;
height: 24px;
margin-top: 12px;
margin-left: 20px;
}
.header-title {
margin-left: 16px;
height: 48px;
color: var(--color-main-active);
font-family: Source Han Sans CN;
font-size: 20px;
font-weight: 700;
line-height: 48px;
text-align: center;
}
}
.header-right {
height: 48px;
margin-right: 28px;
}
}
.wrapper-main {
height: calc(100% - 48px);
overflow: hidden;
}
}
</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 lang="scss" scoped>
.box4 {
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="module-header-wrapper">
<div class="nav-content">
<div class="nav-left" :class="{ 'flex-start': isShowSearchBar }">
<div class="icon">
<img v-show="!isShowSearchBar" src="@/assets/icons/overview/logo.png" alt="" />
<img v-show="isShowSearchBar" src="@/assets/icons/overview/logo-white.png" alt="" />
</div>
<SearchBar v-show="isShowSearchBar" />
<div class="title-box" v-show="!isShowSearchBar">
<!-- <div class="title-box" v-if="false"> -->
<div
class="title"
v-for="(item, index) in homeTitleList"
:key="index"
@mouseenter="handleShowMenu(index, true)"
@mouseleave="handleShowMenu(index, false)"
@click="handleClickTitle(item)"
>
<div class="text" :class="{ textActive: homeActiveTitleIndex === index }">
{{ item.name }}
</div>
<div class="bottom-line" v-if="homeActiveTitleIndex === index"></div>
</div>
</div>
</div>
<div class="nav-right">
<div class="info-box" @click="handleClickToolBox">
<div class="mail">
<img src="@/assets/icons/overview/mail.png" alt="" />
</div>
<div class="user">
<img src="@/assets/icons/overview/user.png" alt="" />
</div>
<div class="name">{{ "管理员" }}</div>
</div>
</div>
<div class="menu-box" v-if="isShowMenu" @mouseenter="handleHoverMenu(true)" @mouseleave="handleHoverMenu(false)">
<div class="menu-content">
<div class="menu-item" v-for="(item, index) in menuList" :key="index" @click="handleToModule(item)">
<div class="icon">
<img :src="item.icon" alt="" />
</div>
<div class="title">{{ item.title }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watchEffect } from "vue";
import { useRouter } from "vue-router";
import { useRoute } from "vue-router";
import { getPersonType } from "@/api/common/index";
import SearchBar from "@/components/layout/SearchBar.vue";
import Menu1 from "@/assets/icons/overview/menu1.png";
import Menu2 from "@/assets/icons/overview/menu2.png";
import Menu3 from "@/assets/icons/overview/menu3.png";
import Menu4 from "@/assets/icons/overview/menu4.png";
import Menu5 from "@/assets/icons/overview/menu5.png";
import Menu6 from "@/assets/icons/overview/menu6.png";
import Menu7 from "@/assets/icons/overview/menu7.png";
import Menu8 from "@/assets/icons/overview/menu8.png";
import Menu9 from "@/assets/icons/overview/menu9.png";
import Menu10 from "@/assets/icons/overview/menu10.png";
import Menu11 from "@/assets/icons/overview/menu11.png";
import Menu12 from "@/assets/icons/overview/menu12.png";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
const store = useWrittingAsstaintStore();
let isShowSearchBar = computed(() => {
return store.isShowSearchBar;
});
const router = useRouter();
const personTypeList = ref([]);
// 获取人物类别
const handleGetPersonType = async () => {
try {
const res = await getPersonType();
console.log("res", res);
if (res.code === 200) {
personTypeList.value = res.data;
} else {
personTypeList.value = [];
}
window.sessionStorage.setItem("personTypeList", JSON.stringify(personTypeList.value));
} catch (error) {}
};
// 概览页标题列表
const homeTitleList = ref([
{
name: "中美科技博弈",
path: "/ZMOverView",
disabled: false
},
{
name: "主要国家科技动向感知",
path: "",
disabled: true
},
{
name: "主要国家竞争科技安全",
path: "",
disabled: true
}
]);
const homeActiveTitleIndex = ref(0);
const isShowMenu = ref(false);
const handleShowMenu = (index, isShow) => {
if (index === 0) {
isShowMenu.value = isShow;
}
};
const handleHoverMenu = isShow => {
isShowMenu.value = isShow;
};
const menuList = ref([
{
title: "中美科技博弈概览",
icon: Menu1,
path: "/ZMOverView"
},
{
title: "科技法案",
icon: Menu2,
path: "/billHome"
},
{
title: "科技政令",
icon: Menu3,
path: "/decree"
},
{
title: "美国科技智库",
icon: Menu4,
path: "/thinkTank"
},
{
title: "出口管制",
icon: Menu5,
path: "/exportControl"
},
{
title: "科研合作限制",
icon: Menu6,
path: "/cooperationRestrictions"
},
{
title: "投融资限制",
icon: Menu7,
path: "/finance"
},
{
title: "市场准入限制",
icon: Menu8,
path: "/marketAccessRestrictions"
},
{
title: "规则限制",
icon: Menu9,
path: "/ruleRestrictions"
},
{
title: "美国科技人物观点",
icon: Menu10,
path: "/technologyFigures"
},
{
title: "美国主要创新主体动向",
icon: Menu11,
path: "/innovationSubject"
},
{
title: "美国科研资助体系",
icon: Menu12,
path: "/scientificFunding"
}
]);
const handleToModule = item => {
const curRoute = router.resolve({
path: item.path
});
window.open(curRoute.href, "_blank");
};
const searchText = ref("");
const handleSearch = () => {
const curRoute = router.resolve({
path: "/searchResults",
query: {
searchText: searchText.value
}
});
window.open(curRoute.href, "_blank");
};
const handleClickTitle = item => {
if (item.name === "主要国家科技动向感知" || item.name === "主要国家竞争科技安全") {
ElMessage.warning("当前功能正在开发中,敬请期待!");
}
};
const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
onMounted(() => {
handleGetPersonType();
});
</script>
<style lang="scss" scoped>
.module-header-wrapper {
width: 100%;
// height: 64px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: linear-gradient(180deg, rgba(246, 250, 255, 0.8) 0%, rgba(255, 255, 255, 0.8) 100%);
padding: 12px 0;
.nav-content {
width: 1600px;
margin: 0 auto;
display: flex;
justify-content: space-between;
position: relative;
align-items: flex-start;
.nav-left {
display: flex;
align-items: center;
&.flex-start {
align-items: flex-start;
.icon {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 50%;
background: rgb(5, 95, 194);
display: flex;
align-items: center;
justify-content: center;
img {
width: 29px;
height: 30px;
}
}
}
.icon {
width: 29px;
height: 30px;
margin-right: 17px;
img {
width: 100%;
height: 100%;
}
}
.title-box {
display: flex;
gap: 30px;
.title {
cursor: pointer;
position: relative;
&:hover {
.text {
color: var(--color-main-active);
}
}
.text {
color: rgba(59, 65, 75, 1);
font-family: YouSheBiaoTiHei;
font-style: Regular;
font-size: 30px;
font-weight: 400;
letter-spacing: 0px;
}
.textActive {
color: var(--color-main-active);
}
.bottom-line {
position: absolute;
bottom: -15px;
width: 90%;
height: 20px;
margin-top: 9px;
&::after {
display: block;
content: "";
width: 50%;
height: 4px;
background: var(--color-main-active);
position: absolute;
bottom: 2px;
left: 0;
right: 0;
margin: 0 auto;
}
}
}
}
}
.nav-right {
display: flex;
justify-content: flex-end;
gap: 21px;
.info-box {
display: flex;
justify-content: flex-end;
align-items: center;
.mail {
width: 32px;
height: 32px;
margin-right: 14px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
.user {
width: 32px;
height: 32px;
margin-right: 11px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
.name {
width: 48px;
height: 30px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
}
}
}
.menu-box {
position: absolute;
z-index: 999999999;
width: 713px;
height: 413px;
top: 52px;
left: 0;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
backdrop-filter: blur(30px);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 0.8);
.menu-content {
width: 562px;
height: 348px;
margin-top: 8px;
margin-left: 72px;
display: flex;
flex-wrap: wrap;
.menu-item {
margin-top: 36px;
width: 280px;
height: 24px;
display: flex;
cursor: pointer;
&:hover {
.title {
color: var(--color-main-active);
font-size: 20px;
}
}
.icon {
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
}
}
.title {
margin-left: 16px;
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 18px;
font-weight: 700;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
}
}
}
}
</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="box3-item" @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>
</template>
<script setup>
import DefaultIconNews from "@/assets/icons/default-icon-news.png";
const props = defineProps({
// 新闻列表数据
news: {
type: Object,
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 handleToNewsAnalysis = (item, index) => {
emit('item-click', item, index)
};
</script>
<style lang="scss" scoped>
.box3-item {
display: flex;
align-items: center;
height: 78px;
margin: 0px 21px;
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: 97px;
// flex-shrink: 0;
height: 72px;
img {
width: 100%;
height: 100%;
border-radius: 4px;
}
}
.right {
flex: 1;
min-width: 0;
margin-left: 20px;
.right-top {
display: flex;
justify-content: space-between;
.title {
// 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 {
text-align: right;
height: 22px;
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 {
height: 48px;
/* 调整为2行的高度:24px × 2 = 48px */
color: rgba(59, 65, 75, 1);
font-family: 'Source Han Sans CN';
font-size: 16px;
font-weight: 400;
line-height: 24px;
overflow: hidden;
display: -webkit-box;
/* 关键 */
-webkit-line-clamp: 2;
/* 显示2行 */
-webkit-box-orient: vertical;
/* 垂直方向排列 */
text-overflow: ellipsis;
/* 第二行省略号 */
white-space: normal;
/* 改为 normal */
word-break: break-word;
/* 允许单词换行 */
}
}
</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>
<el-space @click="onClick(person)">
<ElAvatar :size="48" :src="person[img]" alignment="center" />
<el-space :size="0" direction="vertical" alignment="flex-start">
<div class="text-header">{{ person[name] }}</div>
<div class="person-position">{{ person[position] }}</div>
</el-space>
</el-space>
</template>
<script setup>
import '@/styles/common.scss'
import { ElSpace } from 'element-plus';
const props = defineProps({
// 新闻列表数据
person: {
type: Object,
default: () => { }
},
img: {
type: String,
default: 'avatarUrl'
},
name: {
type: String,
default: "name"
},
position: {
type: String,
default: "position"
},
introduction: {
type: String,
default: "introduction"
},
});
const emit = defineEmits(['item-click', 'more-click']);
const onClick = (item) => {
emit('item-click', item)
console.log(item)
};
</script>
<style lang="scss" scoped>
.person-position {
font-size: 16px;
line-height: 24px;
color: var(--text-primary-65-color);
}
</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
<template>
<div class="header-wrapper">
<div class="icon"></div>
<div class="title"></div>
<div class="btn-box"></div>
</div>
</template>
\ No newline at end of file
<script setup>
import { ElSpace, ElButton } from 'element-plus';
import "@/styles/main.css"
import { copyToClipboardThenTip } from '@/utils/systemUtils'
const colors = [
{ title: "黑色90% / 主标题文字颜色", name: "--text-primary-90-color" },
{ title: "黑色80% / 小标题文字颜色", name: "--text-primary-80-color" },
{ title: "黑色65% / 正文颜色", name: "--text-primary-65-color" },
{ title: "黑色50%", name: "--text-primary-50-color" },
{ title: "黑色10%", name: "--bg-black-10" },
{ title: "黑色5% / 分割线颜色", name: "--bg-black-5" },
{ title: "黑色2% / 灰色背景色", name: "--bg-black-2" },
{ title: "白色主色", name: "--bg-white-100" },
{ title: "主色", name: "--color-primary-100" },
]
function copyColorVar(item) {
const color = `var(${item.name})`
copyToClipboardThenTip(color)
}
</script>
<template>
<el-space direction="vertical" alignment="flex-start">
<div class="text-title-2">颜色</div>
<el-space>
<el-button v-for="(item, index) in colors" :key="index" :color="`var(${item.name})`"
v-on:click="copyColorVar(item)">
{{ item.title }}
</el-button>
</el-space>
</el-space>
</template>
<style scoped></style>
\ No newline at end of file
<script setup lang="ts">
import "@/styles/container.scss"
import TextStyle from './textStyle.vue';
import ConstStyle from './constStyle.vue';
import { ElScrollbar, ElSpace } from "element-plus";
</script>
<template>
<el-scrollbar>
<div class="common-page">
<el-space direction="vertical" alignment="flex-start">
<div class="text-title-0-show">开发样式</div>
<div class="text-title-1-show">样式变量</div>
<const-style></const-style>
<div class="text-title-1-show">文字样式</div>
<text-style></text-style>
</el-space>
</div>
</el-scrollbar>
</template>
\ No newline at end of file
<template>
<table style="width: 100%; border-collapse: collapse; border: 1px solid #ebeef5;">
<!-- 表头 -->
<thead>
<tr class="text-title-2">
<th> 名称</th>
<th> 类型名称</th>
<th> 操作</th>
</tr>
</thead>
<!-- 表格内容 -->
<tbody>
<template v-for="(row, index) in tableData" :key="index">
<!-- 隔行变色效果 -->
<tr :style="{
height: '60px',
backgroundColor: index % 2 === 1 ? '#fafafa' : 'transparent'
}">
<!-- 名称列 -->
<td style="padding: 12px; border: 1px solid #ebeef5;">
<div :class="row.className">
{{ row.name }}
</div>
</td>
<!-- 类型名称列(带复制功能) -->
<td style="padding: 12px; border: 1px solid #ebeef5;">
<div @click="copyToClipboardThenTip(row.className)" style="cursor: pointer;">
<span>{{ row.className }}</span>
</div>
</td>
<!-- 操作列 -->
<td style="padding: 12px; border: 1px solid #ebeef5; text-align: center;">
<el-button type="primary" link @click="copyToClipboardThenTip(row.className)">
<el-icon>
<DocumentCopy />
</el-icon>
复制
</el-button>
</td>
</tr>
</template>
</tbody>
</table>
</template>
<script setup>
import { ref } from 'vue'
import { DocumentCopy } from '@element-plus/icons-vue'
import "@/styles/common.scss"
import { copyToClipboardThenTip } from '@/utils/systemUtils'
// 表格数据
const tableData = ref([
// { name: '0级标题', className: 'text-title-0' },
{ name: '0级标题-加粗', className: 'text-title-0-bold' },
{ name: '0级标题-综艺', className: 'text-title-0-show' },
// { name: '1级标题', className: 'text-title-1' },
{ name: '1级标题-加粗', className: 'text-title-1-bold' },
{ name: '1级标题-综艺', className: 'text-title-1-show' },
{ name: '2级标题', className: 'text-title-2' },
{ name: '2级标题-加粗', className: 'text-title-2-bold' },
{ name: '2级标题-综艺', className: 'text-title-2-show' },
{ name: '3级标题', className: 'text-title-3' },
{ name: '3级标题-加粗', className: 'text-title-3-bold' },
{ name: '3级标题-综艺', className: 'text-title-3-show' },
{ name: '正文', className: 'text-regular' },
{ name: '正文-加粗', className: 'text-bold' },
{ name: '正文-紧凑', className: 'text-compact' },
{ name: '正文-紧凑-加粗', className: 'text-compact-bold' },
{ name: '1级提示文字', className: 'text-tip-1' },
{ name: '1级提示文字-加粗', className: 'text-tip-1-bold' },
{ name: '2级提示文字', className: 'text-tip-2' },
{ name: '2级提示文字-加粗', className: 'text-tip-2-bold' },
{ name: '3级提示文字', className: 'text-tip-3' },
])
</script>
<style scoped></style>
\ No newline at end of file
<template>
<div class="message-bubble">
<div class="avatar-container" @click="handleClick">
<img :src="avatar || avatarUser" :alt="name" class="avatar" />
</div>
<div class="bubble-container">
<div class="bubble">
<div class="bubble-header">
<span class="name">{{ name }}</span>
<span class="meta">{{ time }} · {{ source }}</span>
</div>
<div class="bubble-content" @click="handleInfoClick">
{{ content }}
</div>
<div class="triangle"></div>
</div>
</div>
</div>
</template>
<script setup>
import avatarUser from "@/assets/images/avatar_user.png";
const emit = defineEmits(["click", "info-click"]);
defineProps({
avatar: {
type: String,
default: "https://via.placeholder.com/40x40/4A90E2/FFFFFF?text=T"
},
name: {
type: String,
default: "唐纳德·特朗普"
},
time: {
type: String,
default: "15:23"
},
source: {
type: String,
default: "发布于真实社交"
},
content: {
type: String,
default:
"埃隆·马斯克在强力支持我竞选总统之前,早就知道我强烈反对‘电动汽车强制令’。这太荒谬了,这一直是我竞选活动的主要部分。电动汽车没问题,但不应该强迫每个人都拥有一辆。埃隆获得的补贴可能远远超过历史上任何一个人。如果没有补贴,埃隆可能不得不关门大吉,回到南非老家。"
}
});
const handleClick = () => {
emit("click");
};
const handleInfoClick = () => {
emit("info-click");
};
</script>
<style scoped>
.message-bubble {
display: flex;
max-width: 720px;
margin-top: 5px;
margin-bottom: 16px;
}
.avatar-container {
flex-shrink: 0;
flex-grow: 0;
flex: 0;
margin-right: 12px;
cursor: pointer;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.bubble-container {
flex: 1;
position: relative;
}
.bubble {
background-color: rgba(246, 250, 255, 1);
border-radius: 12px;
padding: 10px 16px;
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;
font-family: "微软雅黑";
}
.name {
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
}
.meta {
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
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: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
font-family: "微软雅黑";
}
.triangle {
position: absolute;
left: -8px;
top: 15px;
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="info-wrapper">
<!-- <div class="header-item">国家科技安全</div>
<div class="header-item">></div> -->
<div class="header-item back-item" @click="handleBackHome">中美博弈概览</div>
<div class="header-item">></div>
<div class="header-item">{{ curTitleName }}</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import router from "@/router";
const props = defineProps({
curTitleName: {
type: String,
required: true
}
});
// 返回首页
const handleBackHome = () => {
router.push({
path: "/overview"
});
};
</script>
<style lang="scss" scoped>
.info-wrapper {
height: 64px;
line-height: 64px;
display: flex;
justify-content: flex-end;
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
color: #fff;
.header-item {
margin: 0 3px;
}
.back-item {
cursor: pointer;
&:hover {
color: #ccc;
}
}
}
</style>
\ No newline at end of file
<template>
<div class="menu-wrapper">
<div class="menu-item" @click="handleToOverview">
<div class="menu-item-icon">
<img src="@/assets/icons/home-header-icon1.png" alt="" />
</div>
<div class="menu-item-text">{{ "首页" }}</div>
</div>
<div
class="menu-item"
@click="handleToGjOverview"
@mouseenter="handleIsShowCountryMore(true)"
@mouseleave="handleIsShowCountryMore(false)"
>
<div class="menu-item-icon1">
<img src="@/assets/icons/home-header-icon2.png" alt="" />
</div>
<div class="menu-item-text">{{ "国家" }}</div>
</div>
<div class="menu-item">
<div class="menu-item-icon2">
<img src="@/assets/icons/home-header-icon3.png" alt="" />
</div>
<div class="menu-item-text">{{ "领域" }}</div>
</div>
<div class="menu-item">
<div class="menu-item-icon3">
<img src="@/assets/icons/home-header-icon4.png" alt="" />
</div>
<div class="menu-item-text">{{ "要素" }}</div>
</div>
<div class="menu-item">
<div class="menu-item-icon4">
<img src="@/assets/icons/home-header-icon5.png" alt="" />
</div>
<div class="menu-item-text">{{ "事件" }}</div>
</div>
</div>
<div
class="more-wrapper"
v-if="isShowCountryMore"
@mouseenter="handleIsShowCountryMore(true)"
@mouseleave="handleIsShowCountryMore(false)"
>
<div class="left">
<div class="left-header">
<div class="title">{{ "中美科技博弈概览" }}</div>
<div class="icon">
<img src="@/assets/icons/more.png" alt="" />
</div>
</div>
<div class="left-main">
<div class="item" v-for="(item, index) in leftList" :key="index" @click="handleClickItem(item)">
<div class="icon"></div>
<div class="text">{{ item.name }}</div>
</div>
</div>
</div>
<!-- <div class="right">
<div class="right-header">
<div class="title">{{ "风险监测" }}</div>
<div class="icon">
<img src="@/assets/icons/more.png" alt="" />
</div>
</div>
<div class="right-main">
<div class="item" v-for="(item, index) in rightList" :key="index">
<div class="icon"></div>
<div class="text">{{ item.name }}</div>
</div>
</div>
</div> -->
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import router from "@/router";
const isShowCountryMore = ref(false);
const handleIsShowCountryMore = isShow => {
isShowCountryMore.value = isShow;
};
const handleToOverview = () => {
router.push({
path: "/overview"
});
};
const handleToGjOverview = () => {
router.push({
path: "/gjOverview"
});
};
const leftList = ref([
{
name: "科技法案",
path: "/billHome"
},
{
name: "科技政令",
path: "/decree"
},
{
name: "美国科技智库",
path: "/thinkTank"
},
{
name: "出口管制",
path: "/exportControl"
},
{
name: "科研合作限制",
path: "/cooperationRestrictions"
},
{
name: "投融资限制",
path: "/finance"
},
{
name: "市场准入限制",
path: "/marketAccessRestrictions"
},
{
name: "规则限制",
path: "/ruleRestrictions"
},
{
name: "美国科技人物观点",
path: "/technologyFigures"
},
{
name: "美国主要创新主体动向",
path: "/innovationSubject"
},
{
name: "美国科研资助体系分析",
path: "/scientificFunding"
}
]);
const handleClickItem = item => {
const curRoute = router.resolve({
path: item.path
});
window.open(curRoute.href, "_blank");
};
const rightList = ref([
{
name: "科技战略布局",
path: ""
},
{
name: "创新体系位势分析",
path: ""
}
]);
</script>
<style lang="scss" scoped>
.menu-wrapper {
width: 644px;
height: 64px;
display: flex;
justify-content: space-between;
.menu-item {
display: flex;
gap: 11px;
width: 112px;
height: 64px;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
background: var(--color-main-active);
}
.menu-item-icon {
// margin-top: 2px;
width: 22px;
height: 22px;
img {
width: 100%;
height: 100%;
}
}
.menu-item-icon1 {
// margin-top: 4px;
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
}
}
.menu-item-icon2 {
// margin-top: 4px;
width: 24px;
height: 22px;
img {
width: 100%;
height: 100%;
}
}
.menu-item-icon3 {
// margin-top: 2px;
width: 20px;
height: 20px;
img {
width: 100%;
height: 100%;
}
}
.menu-item-icon4 {
width: 22px;
height: 20px;
img {
width: 100%;
height: 100%;
}
}
.menu-item-text {
// margin-top: 16px;
height: 32px;
color: rgba(255, 255, 255, 1);
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 24px;
font-weight: 700;
line-height: 32px;
letter-spacing: 0px;
text-align: left;
}
}
}
.more-wrapper {
position: absolute;
z-index: 99999;
top: 64px;
left: 0;
width: 100%;
height: 299px;
background: rgb(249, 249, 249);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.3);
display: flex;
.left {
margin-top: 35px;
margin-left: 179px;
width: 769px;
height: 218px;
.left-header {
display: flex;
height: 26px;
align-items: center;
.title {
width: 160px;
height: 26px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
}
.icon {
margin-top: -8px;
margin-left: 8px;
width: 10px;
height: 10px;
img {
width: 100%;
height: 100%;
}
}
}
.left-main {
height: 192px;
display: flex;
flex-wrap: wrap;
.item {
margin-top: 18px;
display: flex;
width: 256px;
height: 36px;
align-items: center;
border-radius: 4px;
cursor: pointer;
&:hover {
background: var(--color-bg-hover);
.icon {
background: var(--color-main-active) !important;
}
.text {
color: var(--color-main-active) !important;
font-weight: 700;
font-size: 20px;
}
}
.icon {
width: 6px;
height: 6px;
border-radius: 3px;
background: rgba(95, 101, 108, 1);
}
.text {
margin-left: 10px;
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 18px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
}
}
.right {
margin-top: 35px;
margin-left: 53px;
width: 192px;
height: 116px;
.right-header {
display: flex;
height: 26px;
align-items: center;
.title {
width: 80px;
height: 26px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
}
.icon {
margin-top: -8px;
margin-left: 8px;
width: 10px;
height: 10px;
img {
width: 100%;
height: 100%;
}
}
}
.right-main {
.item {
margin-top: 18px;
display: flex;
width: 256px;
height: 36px;
align-items: center;
border-radius: 4px;
cursor: pointer;
&:hover {
background: var(--color-bg-hover);
.icon {
background: var(--color-main-active) !important;
}
.text {
color: var(--color-main-active) !important;
font-weight: 700;
font-size: 20px;
}
}
.icon {
width: 6px;
height: 6px;
border-radius: 3px;
background: rgba(95, 101, 108, 1);
}
.text {
margin-left: 10px;
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 18px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="search-box">
<div class="search-header-container">
<div class="home-main-header-center">
<el-input
v-model="store.searchBillText"
@keyup.enter="handleSearch"
style="width: 680px; height: 100%"
:placeholder="store.searchData.placeholder"
/>
<div class="search">
<div class="search-icon">
<img src="@/assets/icons/search-icon.png" alt="" />
</div>
<div class="search-text" @click="handleSearch">搜索</div>
</div>
</div>
<div class="home-main-header-btn-box">
<div class="btn" @click="handleToPosi('position1')">
<div class="btn-text">{{ "最新动态" }}</div>
<div class="btn-icon">
<img src="@/assets/icons/arrow-right-icon.png" alt="" />
</div>
</div>
<div class="btn" @click="handleToPosi('position2')">
<div class="btn-text">{{ "资讯要闻" }}</div>
<div class="btn-icon">
<img src="@/assets/icons/arrow-right-icon.png" alt="" />
</div>
</div>
<div class="btn" @click="handleToPosi('position3')">
<div class="btn-text">{{ "数据总览" }}</div>
<div class="btn-icon">
<img src="@/assets/icons/arrow-right-icon.png" alt="" />
</div>
</div>
<div class="btn" @click="handleToPosi('position4')">
<div class="btn-text">{{ "资源库" }}</div>
<div class="btn-icon">
<img src="@/assets/icons/arrow-right-icon.png" alt="" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, computed, onUnmounted, nextTick, watch, watchEffect } from "vue";
import router from "@/router/index";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
const store = useWrittingAsstaintStore();
const handleSearch = () => {
window.sessionStorage.setItem("curTabName", `搜索-${store.searchBillText}`);
const curRoute = router.resolve({
path: "/searchResults",
query: {
searchText: store.searchBillText,
areaName: store.searchData.areaName
}
});
window.open(curRoute.href, "_blank");
};
let containerRef = computed(() => store.searchData.containerRef);
const handleToPosi = id => {
const element = document.getElementById(id);
if (element && containerRef.value) {
// 2. 使用 nextTick 等待 DOM 布局(如高度切换)完成后再进行坐标计算
nextTick(() => {
const containerRect = containerRef.value.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
// 使用 getBoundingClientRect 计算元素相对于容器顶部的绝对距离
const top = elementRect.top - containerRect.top + containerRef.value.scrollTop;
containerRef.value.scrollTo({
top: top,
behavior: "smooth"
});
});
}
};
</script>
<style lang="scss" scoped>
.search-box {
width: 100%;
// background: #fff;
// box-shadow: 0px 0px 15px 0px rgba(22, 119, 255, 0.1);
.search-header-container {
width: 100%;
margin: 0 auto;
// padding: 16px 0px 16px 0px;
}
.home-main-header-center {
// margin-top: 20px;
// margin-left: 200px;
width: 800px;
height: 48px;
border-radius: 10px;
// box-shadow: 0px 0px 15px 0px rgba(22, 119, 255, 0.1);
border: 1px solid var(--color-primary-35);
background: rgba(255, 255, 255, 1);
box-sizing: border-box;
padding: 1px;
position: relative;
&:hover {
border: 1px solid var(--color-main-active);
}
.search {
position: absolute;
right: -1px;
top: 0px;
width: 120px;
height: 46px;
border-radius: 10px;
background: var(--color-main-active);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
.search-icon {
width: 18px;
height: 18px;
img {
width: 100%;
height: 100%;
}
}
.search-text {
margin-left: 8px;
height: 22px;
color: #fff;
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 22px;
}
}
}
.home-main-header-btn-box {
margin-top: 16px;
// margin-left: 200px;
display: flex;
gap: 16px;
.btn {
display: flex;
align-items: center;
gap: 9px;
width: 140px;
height: 36px;
border: 1px solid #aed6ff;
box-sizing: border-box;
border-radius: 24px;
background: #e7f3ff;
cursor: pointer;
position: relative;
&:hover {
background: #cae3fc;
}
.btn-text {
width: 80px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 400;
line-height: 48px;
margin-left: 24px;
text-align: center;
}
.btn-icon {
position: absolute;
top: 10px;
right: 19px;
width: 6px;
height: 12px;
img {
width: 100%;
height: 100%;
}
}
}
}
}
</style>
......@@ -294,7 +294,15 @@ export function useMarkdownStream() {
const md = createMd()
// 预处理内容
// const processedContent = preprocessMarkdown(rawContent.value)
return md.render(rawContent.value)
let content = rawContent.value || ''
// 将 ==n== 转换为按钮样式的 HTML
// 使用正向预读和反向预读确保只匹配被 == 包裹的数字
content = content.replace(/==(\d+)==/g, (match, p1) => {
return `<button class="clause-ref-btn" data-clause="${p1}">${p1}</button>`
})
return md.render(content)
})
// 自动滚动
......
......@@ -28,6 +28,7 @@ export function useContainerScroll(containerRef, options = {}) {
lastScrollTop.value = currentScrollTop
ticking.value = false
}
const handleScroll = () => {
......@@ -42,6 +43,7 @@ export function useContainerScroll(containerRef, options = {}) {
}
onMounted(() => {
console.log('containerRef', containerRef)
const container = containerRef.value
if (container) {
container.addEventListener('scroll', handleScroll, { passive: true })
......
......@@ -4,13 +4,28 @@ import router from "./router";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import CardTitle from "./components/CardTitle.vue";
import { withFallbackImage } from "./utils";
import "./styles/scrollbar.css";
import "./styles/elui.css";
import "./styles/main.css";
import '@/assets/fonts/font.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
// import AreaTag from '@/components/base/AreaTag/index.vue'
// import LeftBtn from "@/components/base/PageBtn/LeftBtn.vue";
// import RightBtn from "@/components/base/PageBtn/RightBtn.vue";
// import OverviewMainBox from "@/components/base/BoxBackground/OverviewMainBox.vue";
// import OverviewNormalBox from "@/components/base/BoxBackground/OverviewNormalBox.vue";
// import AnalysisBox from '@/components/base/BoxBackground/AnalysisBox.vue'
// import NewsList from '@/components/base/NewsList/index.vue'
// import ModuleHeader from '@/components/base/ModuleHeader/index.vue'
// import RiskSignal from "@/components/base/RiskSignal/index.vue";
// import MessageBubble from "@/components/base/MessageBubble/index.vue";
// import SourceTabLsit from '@/components/base/SourceTabList/index.vue'
// import ActionButton from "@/components/base/ActionButton/index.vue"
// 引入 Pinia 实例
import pinia from './stores'
const app = createApp(App);
// 注册所有图标
......@@ -24,5 +39,19 @@ app.use(router);
app.use(ElementPlus, {
locale: zhCn,
})
app.component("CardTitle", CardTitle);
app.use(pinia) // 挂载 Pinia
// app.component('AreaTag', AreaTag) // 领域标签
// app.component('LeftBtn', LeftBtn) // 向左按钮
// app.component('RightBtn', RightBtn) // 向右按钮
// app.component('OverviewMainBox', OverviewMainBox) // 概览页最新动态背景盒子
// app.component('OverviewNormalBox', OverviewNormalBox) // 概览页一版模块背景盒子
// app.component('AnalysisBox', AnalysisBox) // 分析页模块背景
// app.component('ModuleHeader', ModuleHeader) // 模块头部
// app.component('RiskSignal', RiskSignal) // 风险信号
// app.component('NewsList', NewsList) // 新闻资讯
// app.component('MessageBubble', MessageBubble) // 社交媒体
// app.component('SourceTabLsit', SourceTabLsit) // 资源库tab列表
// app.component('ActionButton', ActionButton) // 普通按钮和激活按钮
app.mount("#app");
//创新主体
import InnovationSubject from "@/views/innovationSubject/index.vue";
import InnovationInstitution from "@/views/innovationSubject/innovativeInstitutions/index.vue";
const InnovationSubject = () => import('@/views/innovationSubject/index.vue')
const InnovationInstitution = () => import('@/views/innovationSubject/innovativeInstitutions/index.vue')
const innovationSubjectRoutes = [
//创新主体
......
//ZM博弈概览
import ZMGame from "@/views/ZMGame/index.vue";
const ZMGame = () => import('@/views/ZMGame/index.vue')
const ZMGameRoutes = [
{
......
//ZM博弈概览
import ZMOverview from "@/views/ZMOverView/index.vue";
const ZMOverview = () => import('@/views/ZMOverView/index.vue')
const ZMOverviewRoutes = [
{
......
// 法案相关
import BillHome from "@/views/bill/billHome/index.vue";
import BillLayoutContainer from "@/views/bill/billLayout/index.vue";
import BillLayout from "@/views/bill/index.vue";
import BillIntroduction from "@/views/bill/introdoction/index.vue";
import BillBackground from "@/views/bill/background/index.vue";
import BillTemplate from "@/views/bill/template/index.vue";
import BillDeepDigLayout from "@/views/bill/deepDig/index.vue";
import BillDeepDigProcessOverview from "@/views/bill/deepDig/processOverview/index.vue";
import BillDeepDigProcessAnalysis from "@/views/bill/deepDig/processAnalysis/index.vue";
import BillDeepDigPoliContribution from "@/views/bill/deepDig/poliContribution/index.vue";
import BillInfluenceLayout from "@/views/bill/influence/index.vue";
import BillInfluenceIndustry from "@/views/bill/influence/industry/index.vue";
import BillInfluenceScientificResearch from "@/views/bill/influence/scientificResearch/index.vue";
import BillRelevantCircumstance from "@/views/bill/relevantCircumstance/index.vue";
const BillHome = () => import('@/views/bill/billHome/index.vue')
const BillLayoutContainer = () => import('@/views/bill/billLayout/index.vue')
const BillLayout = () => import('@/views/bill/index.vue')
const BillIntroduction = () => import('@/views/bill/introdoction/index.vue')
const BillBackground = () => import('@/views/bill/background/index.vue')
const BillTemplate = () => import('@/views/bill/template/index.vue')
const BillDeepDigLayout = () => import('@/views/bill/deepDig/index.vue')
const BillDeepDigProcessOverview = () => import('@/views/bill/deepDig/processOverview/index.vue')
const BillDeepDigProcessAnalysis = () => import('@/views/bill/deepDig/processAnalysis/index.vue')
const BillDeepDigPoliContribution = () => import('@/views/bill/deepDig/poliContribution/index.vue')
const BillInfluenceLayout = () => import('@/views/bill/influence/index.vue')
const BillInfluenceIndustry = () => import('@/views/bill/influence/industry/index.vue')
const BillInfluenceScientificResearch = () => import('@/views/bill/influence/scientificResearch/index.vue')
const BillRelevantCircumstance = () => import('@/views/bill/relevantCircumstance/index.vue')
const billRoutes = [
......
// 人物主页
import CharacterPage from "@/views/characterPage/index.vue";
const CharacterPage = () => import('@/views/characterPage/index.vue')
const characterPageRoutes = [
// 人物主页
......
//企业主页
import companyPages from "@/views/companyPages/index.vue";
const companyPages = () => import('@/views/companyPages/index.vue')
const companyPagesRoutes = [
// 智库系统的主要路由
......
// 综合搜索
import ComprehensiveSearch from '@/views/comprehensiveSearch/index.vue'
import SearchResults from '@/views/comprehensiveSearch/searchResults/index.vue'
import Chat from '@/views/comprehensiveSearch/chat/index.vue'
const ComprehensiveSearch = () => import('@/views/comprehensiveSearch/index.vue')
const SearchResults = () => import('@/views/comprehensiveSearch/searchResults/index.vue')
const Chat = () => import('@/views/comprehensiveSearch/chat/index.vue')
const comprehensiveSearchRoutes = [
// 综合搜索
......
// 合作限制
import CooperationRestrictions from "@/views/coopRestriction/index.vue";
import CooperationRestrictionsDetail from "@/views/coopRestriction/detail/index.vue";
const CooperationRestrictions = () => import('@/views/coopRestriction/index.vue')
const CooperationRestrictionsDetail = () => import('@/views/coopRestriction/detail/index.vue')
const cooperationRestrictionsRoutes = [
// 合作限制
......
// 政令
import Decree from "@/views/decree/decreeHome/index.vue";
import DecreeLayoutContainer from "@/views/decree/decreeLayout/index.vue";
import DecreeOverviewLayout from "@/views/decree/decreeLayout/overview/index.vue";
import DecreeIntroduction from "@/views/decree/decreeLayout/overview/introduction/index.vue";
import DecreeBackground from "@/views/decree/decreeLayout/overview/background/index.vue";
import DecreeDeepDig from "@/views/decree/decreeLayout/deepdig/index.vue";
import DecreeInfluence from "@/views/decree/decreeLayout/influence/index.vue";
import Institution from "@/views/decree/institution/index.vue"
import DecreeOriginal from "@/views/decree/decreeOriginal/index.vue"
const Decree = () => import('@/views/decree/decreeHome/index.vue')
const DecreeLayoutContainer = () => import('@/views/decree/decreeLayout/index.vue')
const DecreeOverviewLayout = () => import('@/views/decree/decreeLayout/overview/index.vue')
const DecreeIntroduction = () => import('@/views/decree/decreeLayout/overview/introduction/index.vue')
const DecreeBackground = () => import('@/views/decree/decreeLayout/overview/background/index.vue')
const DecreeDeepDig = () => import('@/views/decree/decreeLayout/deepdig/index.vue')
const DecreeInfluence = () => import('@/views/decree/decreeLayout/influence/index.vue')
const Institution = () => import('@/views/decree/institution/index.vue')
const DecreeOriginal = () => import('@/views/decree/decreeOriginal/index.vue')
const decreeRoutes = [
// 政令首页
......
//样式主页
const StylePages = () => import("@/components/devStyle/components/index.vue");
const stylePagesRoutes = [
// 智库系统的主要路由
{
path: "/devStylePages",
name: "devStylePages",
component: StylePages,
meta: {
title: "开发样式",
dynamicTitle: true
}
},
]
export default stylePagesRoutes
\ No newline at end of file
// 出口管制
import ExportControl from "@/views/exportControl/index.vue";
const ExportControl = () => import('@/views/exportControl/index.vue')
const exportControlRoutes = [
// 出口管制首页
......
// 投融资限制
import Finance from "@/views/finance/index.vue";
const Finance = () => import('@/views/finance/index.vue')
const financeRoutes = [
// 投融资限制
......
// 市场准入限制
import MarketAccessRestrictions from "@/views/marketAccessRestrictions/marketAccessHome/index.vue";
import MarketAccessLayout from "@/views/marketAccessRestrictions/marketAccessLayout/index.vue";
import MarketAccessOverview from "@/views/marketAccessRestrictions/marketAccessLayout/overview/index.vue";
import MarketAccessCase from "@/views/marketAccessRestrictions/marketAccessLayout/case/index.vue";
import MarketSingleCaseLayout from "@/views/marketAccessRestrictions/singleCaseLayout/index.vue";
import MarketSingleCaseOverview from "@/views/marketAccessRestrictions/singleCaseLayout/overview/index.vue";
import MarketSingleCaseDeepdig from "@/views/marketAccessRestrictions/singleCaseLayout/deepdig/index.vue";
const MarketAccessRestrictions = () => import('@/views/marketAccessRestrictions/marketAccessHome/index.vue')
const MarketAccessLayout = () => import('@/views/marketAccessRestrictions/marketAccessLayout/index.vue')
const MarketAccessOverview = () => import('@/views/marketAccessRestrictions/marketAccessLayout/overview/index.vue')
const MarketAccessCase = () => import('@/views/marketAccessRestrictions/marketAccessLayout/case/index.vue')
const MarketSingleCaseLayout = () => import('@/views/marketAccessRestrictions/singleCaseLayout/index.vue')
const MarketSingleCaseOverview = () => import('@/views/marketAccessRestrictions/singleCaseLayout/overview/index.vue')
const MarketSingleCaseDeepdig = () => import('@/views/marketAccessRestrictions/singleCaseLayout/deepdig/index.vue')
const marketAccessRestrictionsRoutes = [
// 市场准入限制首页
{
path: "/marketAccessRestrictions",
......
//新闻速览
import newsBrief from "@/views/newsBrief/index.vue";
// 新闻事件分析
import NewsAnalysis from "@/views/newsAnalysis/index.vue";
const newsBrief = () => import('@/views/newsBrief/index.vue')
const NewsAnalysis = () => import('@/views/newsAnalysis/index.vue')
const newsRoutes = [
//新闻速览页面路由
......
......@@ -9,23 +9,23 @@ const overViewRoutes = [
// path: "/",
// redirect: "/overview"
// },
{
path: "/overview",
name: "overView",
component: overView,
meta: {
title: "中美科技博弈概览"
}
},
// GJ概览页面路由
{
path: "/gjOverview",
name: "gjOverView",
component: gjOverView,
meta: {
title: "国家概览"
}
},
// {
// path: "/overview",
// name: "overView",
// component: overView,
// meta: {
// title: "中美科技博弈概览"
// }
// },
// // GJ概览页面路由
// {
// path: "/gjOverview",
// name: "gjOverView",
// component: gjOverView,
// meta: {
// title: "国家概览"
// }
// },
]
......
import Portal from "@/views/portals/portal1/index.vue";
import Portal2 from "@/views/portals/portal2/index.vue";
const Portal = () => import('@/views/portals/portal1/index.vue')
const Portal2 = () => import('@/views/portals/portal2/index.vue')
const portalRoutes = [
// 门户
......
// 风险信号
import RiskSignal from "@/views/riskSignal/index.vue"
const RiskSignal = () => import('@/views/riskSignal/index.vue')
const riskSignalRoutes = [
//风险信号页面路由
......@@ -10,8 +10,7 @@ const riskSignalRoutes = [
meta: {
title: "风险信号"
}
},
}
]
export default riskSignalRoutes
\ No newline at end of file
// 规则限制
import RuleRestriction from "@/views/ruleRestriction/index.vue";
import RuleRestrictionDetail from "@/views/ruleRestriction/detail/index.vue";
const RuleRestriction = () => import('@/views/ruleRestriction/index.vue')
const RuleRestrictionDetail = () => import('@/views/ruleRestriction/detail/index.vue')
const ruleRestrictionsRoutes = [
// 规则限制
......
// 科研资助体系
import ScientificFunding from "@/views/scientificFunding/index.vue";
const ScientificFunding = () => import('@/views/scientificFunding/index.vue')
const scientificFundingRoutes = [
// 科研资助体系
......
//科技人物观点
import TechnologyFigures from "@/views/technologyFigures/index.vue";
const TechnologyFigures = () => import('@/views/technologyFigures/index.vue')
const technologyFiguresRoutes = [
{
......
// 智库相关
import thinkTank from "@/views/thinkTank/index.vue";
import ThinkTankDetail from "@/views/thinkTank/ThinkTankDetail/index.vue";
import ReportDetail from "@/views/thinkTank/ReportDetail/index.vue";
import ReportOriginal from "@/views/thinkTank/reportOriginal/index.vue"
const thinkTank = () => import('@/views/thinkTank/index.vue')
const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue')
const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue')
const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue')
const thinktankRoutes = [
// 智库系统的主要路由
{
......
// 智能写报
import WrittingAsstaint from "@/views/writtingAsstaint/index.vue";
const WrittingAsstaint = () => import('@/views/writtingAsstaint/index.vue')
const writtingRoutes = [
// 法案系统路由
// 智能写报路由
{
path: "/writtingAsstaint",
name: "writtingAsstaint",
......
// src/stores/index.js
import { createPinia } from 'pinia'
// 创建 Pinia 实例
const pinia = createPinia()
// 导出实例,供 main.js 挂载
export default pinia
\ No newline at end of file
import { defineStore } from 'pinia'
import { ElMessage, ElMessageBox } from 'element-plus'
export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
state: () => ({
// 基础状态
isGenerating: false,
isShowProcess: false, // 初始隐藏执行步骤面板
isShowSteps: false, // 步骤侧边栏显隐控制
isShowClauseTranslation: false, // 翻译侧边栏显隐控制
isEditMode: false,
writtingTitle: '',
descText: '',
reportContent: '', // 报文内容(核心)
curTempTitle: '政令',
tempActiveIndex: 0,
curAgentTool: '',
uploadFileList: [],
// 路由相关状态
_isDisableTemplate: false,
routeQuery: {},
// 步骤日志(用于流式更新)
processLog: '',
// 条款翻译相关状态
clauseTranslationMessages: [], // 存收到的条款翻译消息
highlightClauseId: null, // 当前高亮的条款 ID (clause_number)
isShowOriginal: true, // 是否显示原文内容
pdfMetadata: null, // PDF 解析元数据
// 静态数据
tabList: [
{ name: '写报', active: true },
{ name: '收藏', active: false },
{ name: '问答', active: false }
],
tempList: [
{ title: '政令', desc: '基于政令内容生成各维度的综合分析报告' },
{ title: '法案', desc: '基于政令内容生成各维度的综合分析报告' },
{ title: '智库', desc: '基于智库内容生成各维度的综合分析报告' },
{ title: '清单', desc: '基于清单内容生成各维度的综合分析报告' }
],
// 内部控制器
abortController: null,
isShowSearchBar: false,
searchBillText:'',
searchData:{
placeholder:'',
areaName:'',
containerRef:null
},
}),
getters: {
isDisableTemplate: (state) => state._isDisableTemplate,
formattedTime: () => {
const now = new Date()
const pad = n => n.toString().padStart(2, '0')
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
}
},
actions: {
// ========== 基础状态修改 ==========
toggleEditMode() {
this.isEditMode = !this.isEditMode
},
// 设置navbar是否显示搜索框
changeIsShowSearchBar(value){
this.isShowSearchBar = value
},
setSearchData({placeholder,areaName,containerRef}){
this.searchData.placeholder = placeholder
this.searchData.areaName = areaName
this.searchData.containerRef = containerRef
},
resetGenerateState() {
this.isGenerating = false;
this.isShowProcess = false;
this.isShowSteps = false;
this.curAgentTool = '';
this.processLog = '';
this.clauseTranslationMessages = [];
this.pdfMetadata = null;
this.isShowClauseTranslation = false;
this.highlightClauseId = null;
this.isShowOriginal = true;
this.abortController?.abort();
this.abortController = null;
},
backToInputAndClear() {
this.resetGenerateState();
this.reportContent = '';
this.writtingTitle = '';
this.descText = '';
this.uploadFileList = [];
this.routeQuery = {};
this._isDisableTemplate = false;
this.curTempTitle = '政令';
this.tempActiveIndex = 0;
},
updateTempActiveIndex(index, title) {
this.tempActiveIndex = index;
this.curTempTitle = title;
},
_keepStepsViewOnError() {
this.isGenerating = false;
this.isShowProcess = true;
this.isShowSteps = true;
this.abortController?.abort();
this.abortController = null;
},
async _showErrorDialog(message) {
await ElMessageBox.alert(message || '写报生成失败', '提示', {
confirmButtonText: '确认',
type: 'error'
});
},
async _handleGenerateError(message) {
this._keepStepsViewOnError();
await this._showErrorDialog(message);
},
// ========== 路由参数处理 ==========
async setRouteParams(query) {
this.routeQuery = { ...query };
this._isDisableTemplate = Object.keys(query).length > 0;
if (Object.keys(query).length > 0) {
const { topic, fileId } = query;
if (topic) {
this.curTempTitle = topic;
this.tempActiveIndex = this.tempList.findIndex(item => item.title === topic);
}
// 如果携带 fileId:外部跳转仅回填/锁定参数,不自动生成
if (fileId) {
this.curTempTitle = topic || '政令';
this.tempActiveIndex = this.tempList.findIndex(item => item.title === this.curTempTitle);
// 初始标题设为空,待点击生成后从接口获取并清洗
this.writtingTitle = '';
}
}
},
// ========== 内部工具函数 (抽取的公共逻辑) ==========
_cleanTitle(title) {
return (title || '').replace(/[^\u4e00-\u9fa5]/g, '');
},
async _fetchBusinessData({ url, payload, typeName }) {
this.isGenerating = true;
this.isShowProcess = true;
this.processLog = `${this.formattedTime}:正在获取${typeName}数据...\r\n`;
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.status === 'success' && data.result) {
this.processLog += `${this.formattedTime}:${typeName}数据获取成功,开始生成写报...\r\n`;
// 清洗标题
this.writtingTitle = this._cleanTitle(data.result['报告标题']);
// 触发 AI 生成
await this.fetchReportData({
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle,
result: data.result
});
} else {
throw new Error(data.error_log?.join(', ') || `获取${typeName}数据失败`);
}
} catch (error) {
console.error(`获取${typeName}数据异常:`, error);
await this._handleGenerateError(`获取${typeName}数据失败: ${error.message}`);
}
},
// ========== 获取政令基础信息 (翻译栏数据) ==========
async fetchOrderBaseInfo(orderId) {
try {
const response = await fetch('/reportData/get-order-base-info', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: Number(orderId) })
});
const data = await response.json();
if (data.status === 'success' && data.result) {
const res = data.result;
// 1. 填充元数据 (用于翻译栏顶部的标题卡片)
this.pdfMetadata = {
name: res.order_name_zh,
order_title: res.order_name,
signing_date: res.post_date
};
// 2. 填充条款翻译内容
this.clauseTranslationMessages = (res.clauses || []).map(item => ({
event: 'clause_translation',
payload: {
clause_number: item.section_num,
clause_section: item.section_num, // 这里根据结构复用 section_num
clause_content: item.section_text,
clause_content_zh: item.section_text_zh
}
}));
// 3. 自动展示翻译栏
this.isShowClauseTranslation = true;
this.isShowSteps = true;
console.log('政令基础信息获取并回填成功:', res);
}
} catch (error) {
console.error('获取政令基础信息失败:', error);
}
},
// ========== 获取业务数据并触发生成 (业务封装) ==========
async fetchOrderReportData(orderId) {
await this._fetchBusinessData({
url: '/reportData/get-order-report-data',
payload: { order_id: Number(orderId) },
typeName: '政令'
});
},
async fetchBillReportData(billId) {
await this._fetchBusinessData({
url: '/reportData/get-bill-report-data',
payload: { bill_id: billId },
typeName: '法案'
});
},
async fetchListReportData(sancDate) {
await this._fetchBusinessData({
url: '/reportData/get-sanc-list-report-data',
payload: { sanc_date: sancDate },
typeName: '实体清单'
});
},
// ========== 文件上传相关 ==========
handleExceed(files, uploadRef) {
if (uploadRef) {
uploadRef.clearFiles();
const file = files[0];
file.uid = Date.now();
uploadRef.handleStart(file);
}
},
handleFileChange(file, files) {
this.uploadFileList = files.length > 1 ? [file] : files;
},
// ========== PDF 解析 SSE(仅更新执行步骤) ==========
async fetchPdfData(selectedFile) {
if (this.abortController) this.abortController.abort();
this.abortController = new AbortController();
this.processLog = '';
try {
const formData = new FormData();
formData.append('pdf', selectedFile);
const { fetchEventSource } = await import('@microsoft/fetch-event-source');
await fetchEventSource('/pdfSse/api/v1/order/pdf/extract/report/sse', {
method: 'POST',
body: formData,
signal: this.abortController.signal,
headers: { Accept: 'text/event-stream', 'Cache-Control': 'no-cache' },
openWhenHidden: true,
retryDelay: 1000,
maxRetries: 3,
onopen: async (response) => {
if (response.ok && response.headers.get('content-type')?.includes('text/event-stream')) {
} else {
ElMessage.warning('SSE连接格式异常,即将断开');
this.abortController.abort();
}
},
onmessage: async (event) => {
if (!event || !event.data || event.data.trim() === '') return;
let jsonData = null;
try {
jsonData = JSON.parse(event.data.trim());
} catch (parseError) {
console.warn('SSE消息JSON解析失败', parseError, event.data);
return;
}
switch (event.event) {
case 'progress':
// 仅更新执行步骤
if (jsonData.message) {
this.processLog += `${this.formattedTime}:${jsonData.message}\r\n`;
}
break;
case 'metadata':
if (jsonData && jsonData.payload) {
this.pdfMetadata = jsonData.payload;
}
break;
case 'clause_translation':
// 保存条款翻译消息并显示侧边栏
if (jsonData) {
this.clauseTranslationMessages.push(jsonData);
this.isShowClauseTranslation = true;
this.isShowSteps = true; // 翻译出现时,步骤侧边栏也显示
}
break;
case 'result':
if (jsonData && Object.keys(jsonData).length) {
await this.fetchReportData({
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle,
result: jsonData
});
}
break;
case 'error':
await this._handleGenerateError(jsonData.message);
break;
default:
console.debug('未处理的SSE事件类型', event.event);
break;
}
},
onerror: async (error) => {
console.error('SSE连接错误', error);
if (error.name !== 'AbortError') {
await this._handleGenerateError('写报生成报错!');
return true;
}
this.resetGenerateState();
},
onclose: () => {
console.log('SSE连接正常关闭');
this.isGenerating = false;
}
});
} catch (error) {
if (error.name !== 'AbortError') {
console.error('PDF SSE请求异常', error);
await this._handleGenerateError(`PDF解析请求失败:${error.message}`);
throw error;
}
this.resetGenerateState();
throw error;
}
},
// ========== AI 生成报文 SSE(更新报文内容 + 执行步骤) ==========
async fetchReportData(params) {
if (this.abortController) this.abortController.abort();
this.abortController = new AbortController();
this.processLog = '';
// 用于把 SSE 的分片内容先聚合,再按“句子/段落边界”一次性提交到 reportContent
// 这样可以还原老版“一句完整再显示”的渲染效果,避免分片逐条渲染
let streamBuffer = '';
let lastFlushedIndex = 0;
const flushToReport = (force = false) => {
// 强制 flush 时,把剩余 buffer 全部提交;否则只提交到 lastFlushedIndex
const endIndex = force ? streamBuffer.length : lastFlushedIndex;
if (endIndex <= 0) return;
const chunk = streamBuffer.slice(0, endIndex);
if (!chunk) return;
this.reportContent += chunk;
streamBuffer = streamBuffer.slice(endIndex);
lastFlushedIndex = 0;
if (this.reportContent.includes('./out/img')) {
this.reportContent = this.reportContent.replaceAll('./out/img', 'http://172.19.21.9:8003/out/img');
}
};
// 尝试找到一个“合适的边界”再 flush:
// 1) 中文/英文句末标点 + 可选换行
// 2) 双换行(段落)
// 3) markdown 列表/标题等常见行结束
const updateFlushIndexByBoundary = () => {
// 优先按段落(双换行)
const paraIdx = streamBuffer.lastIndexOf('\n\n');
if (paraIdx !== -1) {
lastFlushedIndex = Math.max(lastFlushedIndex, paraIdx + 2);
}
// 其次按句末标点
// eslint-disable-next-line no-useless-escape
const sentenceRe = /[。!?!?;;](?:\s|\n|$)/g;
let m;
while ((m = sentenceRe.exec(streamBuffer)) !== null) {
lastFlushedIndex = Math.max(lastFlushedIndex, m.index + m[0].length);
}
// 再次按单行结束(避免长时间不刷新)
const lineIdx = streamBuffer.lastIndexOf('\n');
if (lineIdx !== -1 && streamBuffer.length > 200) {
lastFlushedIndex = Math.max(lastFlushedIndex, lineIdx + 1);
}
};
try {
const { fetchEventSource } = await import('@microsoft/fetch-event-source');
await fetchEventSource('/sseWrite/api/v1/workflow/invoke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
signal: this.abortController.signal,
openWhenHidden: true,
onopen: (res) => {
console.log('流式回答开始', res);
},
onmessage: (res) => {
if (!res.data) return;
let msgData;
try {
msgData = JSON.parse(res.data);
} catch (e) {
return;
}
const str = msgData.data || '';
if (msgData.event_type === 'stream_agent_out') {
if (str !== '[DONE]') {
// SSE 分片先进入 buffer(仅用于报文内容)
streamBuffer += str;
updateFlushIndexByBoundary();
flushToReport(false);
} else {
// 结束时把剩余内容强制 flush
flushToReport(true);
this.isGenerating = false;
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束');
// 这里不再调用 resetGenerateState,因为可能需要保留翻译内容
}
} else if (msgData.event_type === 'workflow_complete') {
// complete 也做一次强制 flush,避免尾巴丢失
flushToReport(true);
this.isGenerating = false;
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束');
} else if ((msgData.event_type || '').toLowerCase().includes('error')) {
// 优先从 data.error 获取详细错误描述
const errorMsg = msgData.data?.error || str || '生成失败';
this._handleGenerateError('生成失败:' + errorMsg);
} else {
// 老版 --index.vue 行为:步骤栏直接追加服务端发来的完整步骤内容,不加时间戳、不强行换行
// 这样可以避免 SSE 分片导致的“步骤破碎”(一条步骤被拆成多条显示)
if (str) {
this.processLog += str;
}
this.curAgentTool = msgData.tool || '无';
}
},
onerror: async (error) => {
await this._handleGenerateError('写报生成报错!');
throw new Error(error);
}
});
} catch (error) {
if (error.name !== 'AbortError') {
await this._handleGenerateError(error.message || '写报生成报错!');
} else {
this.resetGenerateState();
}
throw error;
}
},
// ========== 业务入口 ==========
async generateReport() {
// 路由参数优先
this.isGenerating = true;
this.isShowProcess = true;
if (Object.keys(this.routeQuery).length !== 0) {
const { fileId } = this.routeQuery;
// 外部跳转:根据 topic 决定调用哪种数据获取接口,再触发生成
if (fileId) {
if (this.curTempTitle === '法案') {
await this.fetchBillReportData(fileId);
} else if (this.curTempTitle === '清单') {
await this.fetchListReportData(fileId);
} else {
// 政令:先获取基础信息(用于翻译栏),再获取写报数据并生成
await this.fetchOrderBaseInfo(fileId);
await this.fetchOrderReportData(fileId);
}
return;
}
// 兼容:若仅有 routeQuery 但无 fileId,则直接走原生成接口
const params = {
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle,
report_id: fileId
};
await this.fetchReportData(params);
} else {
// 政令模板需要先解析PDF
if (this.curTempTitle === '政令') {
if (this.uploadFileList.length === 0) {
throw new Error('请上传政令文件');
}
const rawFile = this.uploadFileList[0].raw;
if (!rawFile) {
throw new Error('文件解析失败,请重新选择');
}
await this.fetchPdfData(rawFile);
} else {
const params = {
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle
};
await this.fetchReportData(params);
}
}
},
// ========== 导出Markdown ==========
exportContent() {
if (!this.reportContent) {
ElMessage.warning('暂无可导出的报文内容');
return;
}
const blob = new Blob([this.reportContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `markdown-${new Date().getTime()}.md`;
a.click();
URL.revokeObjectURL(url);
},
// ========== 清理 ==========
cleanup() {
this.resetGenerateState();
}
}
});
\ No newline at end of file
/***背景作为卡片***/
.background-as-card {
background-color: var(--color-primary-65);
border-radius: 10px;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
}
.flex-display{
display: flex;
}
/***文本样式***/
.text-base{
color: var(--color-primary-90);
}
//0级标题
.text-title-0{
@extend .text-base;
color: var(--color-primary-90);
font-size: 32px;
}
.text-title-0-bold{
@extend .text-title-0;
font-weight: Bold;
}
.text-title-0-show{
@extend .text-title-0;
font-size: 48px;
font-family: "YouSheBiaoTiHei";
}
//1级标题
.text-title-1{
@extend .text-base;
color: var(--color-primary-90);
font-size: 24px;
}
.text-title-1-bold{
@extend .text-title-1;
font-weight: Bold;
}
.text-title-1-show{
@extend .text-title-1;
font-size: 30px;
font-family: "YouSheBiaoTiHei";
}
//2级标题
.text-title-2{
@extend .text-base;
color: var(--color-primary-90);
font-size: 20px;
line-height:26px;
}
.text-title-2-bold{
@extend .text-title-2;
font-weight: Bold;
}
.text-title-2-show{
@extend .text-title-2;
font-size: 24px;
font-family: "YouSheBiaoTiHei";
line-height: normal !important; // 取消继承的行高
}
//3级标题
.text-title-3{
@extend .text-base;
color: var(--color-primary-90);
font-size: 18px;
line-height:24px;
}
.text-title-3-bold{
@extend .text-title-3;
font-weight: Bold;
}
.text-title-3-show{
@extend .text-title-3;
font-size: 20px;
font-family: "YouSheBiaoTiHei";
line-height: normal !important; // 取消继承的行高
}
//正文
.text-regular{
@extend .text-base;
font-size: 16px;
line-height:30px;
}
//正文-加粗
.text-bold{
@extend .text-base;
font-weight: Bold;
}
//正文-紧凑
.text-compact{
@extend .text-base;
font-size: 16px;
line-height:24px;
}
.text-compact-bold{
@extend .text-base;
font-size: 16px;
line-height:24px;
font-weight: Bold;
}
//1级提示文字
.text-tip-1{
@extend .text-base;
color: var(--color-primary-90);
font-size: 16px;
line-height:24px;
}
.text-tip-1-bold{
@extend .text-tip-1;
font-weight: Bold;
}
//2级提示文字
.text-tip-2{
@extend .text-base;
color: var(--color-primary-90);
font-size: 14px;
line-height:22px;
}
.text-tip-2-bold{
@extend .text-tip-2;
font-weight: Bold;
}
//3级提示文字
.text-tip-3{
@extend .text-base;
color: var(--color-primary-90);
font-size: 12px;
}
@use "common";
/***通用页面***/
.common-page {
font-family: "Microsoft Yahei", "PingFang SC", sans-serif;
margin: 16px auto;
width: 1600px;
align-items: center;
}
\ No newline at end of file
@use "common";
/***没有nav下划线***/
.common-descriptions .el-descriptions__label{
@extend .text-tip-1-bold;
}
.common-descriptions .el-descriptions__content{
@extend .text-tip-1;
color: var(--text-primary-80-color);
}
\ No newline at end of file
:root {
--color-main-active: rgba(5, 95, 194, 1);
--color-main-primay: rgba(59, 65, 75, 1);
--font-family-base: "Source Han Sans CN";
--font-size-base: 16px;
--color-bg-hover: #e7f3ff;
--color-bg-hover: #f6faff;
/* 普通按钮颜色 */
--btn-plain-border-color: rgba(230, 231, 232, 1);
......@@ -13,24 +17,32 @@
--btn-active-bg-color: rgba(231, 243, 255, 1);
--btn-active-text-color: var(--color-main-active);
/* 文字颜色 设计定义*/
--text-primary-color: #0a121e;
--text-primary-90-color: #222934;
--text-primary-80-color: #3b414b;
--text-primary-65-color: #5f656c;
--text-primary-50-color: #84888e;
/* 标签按钮颜色 */
--tag-btn1-bg-color: rgba(255, 241, 240, 1);
--tag-btn1-border-color: rgba(255, 204, 199, 1);
--tag-btn1-text-color: rgba(255, 77, 79, 1);
--tag-btn2-bg-color: rgba(255, 251, 230, 1);
--tag-btn2-border-color: rgba(255, 241, 184, 1);
--tag-btn2-text-color: rgba(250, 173, 20, 1);
--tag-btn2-bg-color: rgba(230, 244, 255, 1);
--tag-btn2-border-color: rgba(186, 224, 255, 1);
--tag-btn2-text-color: rgba(22, 119, 255, 1);
/* 背景颜色 设计定义*/
--bg-black-10: #E6E7E8;
--bg-black-5: #EAECEE;
--bg-black-2: #F7F8F9;
--bg-white-100: #FFFFFF;
--bg-white-65: rgba(255, 255, 255, 0.65);
--bg-white-50: rgba(255, 255, 255, 0.5);
--bg-white-35: rgba(255, 255, 255, 0.35);
--tag-btn3-bg-color: rgba(246, 255, 237, 1);
--tag-btn3-border-color: rgba(217, 247, 190, 1);
--tag-btn3-text-color: rgba(82, 196, 26, 1);
/* 描边颜色 设计定义*/
--border-black-10: #E6E7E8;
--border-black-5: #EAECEE;
/* 主色 设计定义*/
--color-primary-100: #055FC2;
--color-primary-50: #82AFE0;
--color-primary-35: #AED6FF;
--color-primary-10: #E7F3FF;
--color-primary-2: #F6FAFF;
}
.hover-dialog {
......
@use "common";
/***没有nav下划线***/
.tabs-nav-no-wrap .el-tabs__nav-wrap::after{
height: 0px !important;
width: 0px !important;
}
/***nav as card***/
.tabs-header-as-card .el-tabs__header:not(.disinheritance .el-tabs__header) {
@extend .background-as-card;
padding: 2px;
}
/***作为按钮样式的tabs-bar***/
/*选中无下划线*/
.tabs-bar-as-btn .el-tabs__active-bar:not(.disinheritance .el-tabs__active-bar) {
height: 0px;
}
/*定义字体*/
.tabs-bar-as-btn .el-tabs__item:not(.disinheritance .el-tabs__item) {
font-size: 20px;
font-family: "Source Han Sans CN-Regular";
font-weight: Regular;
line-height: 26px;
height: 50px;
}
/*激活时按钮样式*/
.tabs-bar-as-btn .el-tabs__item.is-active:not(.disinheritance .el-tabs__item){
font-size: 24px;
font-family: "Source Han Sans CN-Bold";
font-weight: Bold;
color: var(--color-primary-100);
border-width: 1px;
border-style: solid;
border-color: var(--color-primary-35);
border-radius: 10px;
background-color: var(--color-primary-2);
}
/***tabs-bar左边悬浮***/
.left-float-nav-tabs .el-tabs__item{
position:relative;
}
.left-float-nav-tabs .el-tabs__item.is-active{
background-color: aquamarine;
}
\ No newline at end of file
......@@ -3,7 +3,8 @@
$primary-color: var(--el-color-primary);
$base-color: rgba(5, 95, 194, 1);
$base-font-size: 16px;
$base-font-family: "微软雅黑";
// $base-font-family: "微软雅黑";
$base-font-family: "Source Han Sans CN";
// :root {
// --base-color: #{$base-color};
......
import { ElMessage } from "element-plus";
interface BaseReturn {
status: boolean;
message: string;
}
export async function copyToClipboard(txet: string): Promise<BaseReturn> {
try {
// 使用现代 Clipboard API
await navigator.clipboard.writeText(txet);
console.log("已复制:", txet);
return { status: true, message: "已复制:" + txet };
} catch (err) {
console.error("复制失败:", err);
// 降级方案:使用document.execCommand
const textArea = document.createElement("textarea");
textArea.value = txet;
document.body.appendChild(textArea);
textArea.select();
try {
const successful = document.execCommand("copy");
if (successful) {
return { status: true, message: "已复制:" + txet };
}
} catch (err) {}
document.body.removeChild(textArea);
return { status: false, message: "复制失败,请手动复制" };
}
}
// 复制类型名称到剪贴板
export async function copyToClipboardThenTip(text: string): Promise<void> {
const { status, message } = await copyToClipboard(text);
if (status) {
ElMessage.success(message);
} else {
ElMessage.error(message);
}
}
......@@ -3,7 +3,7 @@
<div class="home-wrapper">
<div class="home-header">
<div class="header-left">
<HeaderMenu></HeaderMenu>
<!-- <HeaderMenu></HeaderMenu> -->
</div>
<div class="header-right">
<input :value="input">
......@@ -26,7 +26,7 @@
<script setup>
import { onMounted, ref, computed } from "vue";
import HeaderMenu from "@/components/headerMenu.vue";
// import HeaderMenu from "@/components/headerMenu.vue";
import RiskToday from './component/riskToday/index.vue'
import ScienceNews from './component/scienceNews/index.vue'
const input = ref('')
......
<svg viewBox="0 0 177 31" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="177.000000" height="31.000000" fill="none" customFrame="#000000">
<path id="Text" d="M8.20898 18.0938L0.914062 18.0938L2.01562 10.3652L9.28711 10.3652L9.65039 7.8457L8.88281 6.9375L14.3496 6.9375L13.8457 10.3652L21.1699 10.3652L20.0625 18.0938L12.7676 18.0938L11.9062 24.2168L7.3418 24.2168L8.20898 18.0938ZM13.5352 12.6504L13.0781 15.8145L15.8379 15.8145L16.2715 12.6504L13.5352 12.6504ZM8.51953 15.8145L8.97656 12.6504L6.24023 12.6504L5.80664 15.8145L8.51953 15.8145ZM32.0175 11.0391L32.1347 10.1309L25.2968 10.1309L25.6073 7.8457L28.3202 7.8457L28.2968 7.77539L27.5761 6.9375L32.1347 6.9375L32.4218 7.8457L37.1034 7.8457L37.2499 7.58203L36.6933 6.9375L41.7148 6.9375L41.2109 7.8457L43.8476 7.8457L43.537 10.1309L36.6933 10.1309L36.5761 11.0391L42.9628 11.0391L42.623 13.3184L36.2421 13.3184L36.1191 14.2324L43.414 14.2324L43.1034 16.5117L23.9491 16.5117L24.2655 14.2324L31.5605 14.2324L31.6777 13.3184L25.2968 13.3184L25.6308 11.0391L32.0175 11.0391ZM27.0487 20.1621L23.8788 20.1621L24.2187 17.8828L29.662 17.8828L30.7226 16.9688L36.1894 16.9688L35.1347 17.8828L42.453 17.8828L42.1191 20.1621L38.4452 20.1621L42.453 24.2168L36.9863 24.2168L33.0488 20.1621L32.5214 20.1621L27.8632 24.2168L22.3905 24.2168L27.0487 20.1621ZM55.6795 9.59766L53.0428 9.81445L52.7733 11.7832L55.7498 11.7832L55.4159 14.2324L52.4159 14.2324L51.0213 24.2168L47.5174 24.2168L48.912 14.2324L45.9588 14.2324L46.2987 11.7832L49.2694 11.7832L49.5096 10.0781L46.703 10.2949L47.0428 7.8457L56.0135 7.1543L55.6795 9.59766ZM65.2538 19.3184L63.9354 19.5117L63.2616 24.2168L59.4705 24.2168L60.0506 20.0156L55.1287 20.6895L55.4862 18.1172L60.408 17.4492L61.7733 7.82227L61.0819 6.9375L65.7108 6.9375L64.2987 16.9453L65.617 16.752L65.2538 19.3184ZM44.6873 23.3496L46.2459 14.9062L48.6248 14.9062L47.0662 23.3496L44.6873 23.3496ZM51.7948 23.3496L52.5623 14.9062L54.9354 14.9062L54.1444 23.3496L51.7948 23.3496ZM59.8807 16.7051L56.3065 16.7051L55.8729 13.5352L59.4471 13.5352L59.8807 16.7051ZM60.6248 11.8828L57.0506 11.8828L56.5702 8.71289L60.1444 8.71289L60.6248 11.8828ZM79.0545 16.0078L81.0232 18.2168L83.3787 16.3418L83.4959 15.4102L75.6736 15.4102L75.9607 13.2949L79.5349 13.2949L79.7986 11.3496L76.0779 11.3496L76.3943 9.07031L80.1384 9.07031L80.3025 7.86914L79.7283 6.96094L84.8142 6.96094L84.5037 9.07031L88.2244 9.07031L87.9138 11.3496L84.1931 11.3496L83.906 13.2949L87.4099 13.2949L86.8298 17.3496L82.992 20.3789L86.4255 24.1699L81.8904 24.1699L80.3494 22.4414L78.0935 24.1934L73.5115 24.1934L78.4568 20.2559L75.5271 16.9219L75.6736 16.0078L79.0545 16.0078ZM68.4724 21.9844L68.9529 18.5039L67.0779 19.0078L67.4412 16.4883L69.3103 15.9844L69.8611 11.9531L68.0623 11.9531L68.4021 9.67383L70.2009 9.67383L70.488 7.63477L69.8904 6.9375L74.6658 6.9375L74.2791 9.67383L75.5974 9.67383L75.2634 11.9531L73.9451 11.9531L73.5584 14.8301L74.9295 14.4492L74.5662 16.9688L73.2009 17.3496L72.4568 22.6348C72.3748 23.0957 72.1541 23.4746 71.7947 23.7715C71.4353 24.0684 71.031 24.2168 70.5818 24.2168L66.3103 24.2168L68.4724 21.9844ZM107.685 24.1934L102.529 24.1934L99.4059 20.6426L94.3903 24.1934L88.7067 24.1934L95.4216 19.418L89.3512 19.418L89.6618 17.1855L98.7848 17.1855L99.5759 11.4727L104.064 11.4727L103.273 17.1855L108.793 17.1855L108.482 19.418L101.111 19.418L100.273 20.0156L104.041 20.0156L107.685 24.1934ZM103.103 8.30273L110.017 8.30273L109.49 11.9531L105.429 11.9531L105.646 10.5117L94.7067 10.5117L94.5602 11.4727L98.3981 11.4727L99.1891 13.8457L94.9938 13.8457L94.3434 11.9531L90.4059 11.9531L90.9333 8.30273L97.9938 8.30273L98.0641 7.79883L97.2966 6.9375L103.297 6.9375L103.103 8.30273ZM97.3903 16.6582L93.2184 16.6582L92.3278 14.2793L96.5055 14.2793L97.3903 16.6582ZM122.998 10.582L131.734 10.582L130.058 22.6348C129.976 23.0957 129.755 23.4746 129.396 23.7715C129.037 24.0684 128.632 24.2168 128.183 24.2168L123.431 24.2168L125.593 21.9844L126.865 12.8613L121.779 12.8613L115.679 24.2168L111.121 24.2168L117.214 12.8613L112.269 12.8613L112.586 10.582L118.439 10.582L120.05 7.58203L119.494 6.9375L124.966 6.9375L122.998 10.582ZM138.263 12L138.767 13.418L139.658 11.9062L139.898 10.1309L134.859 10.1309L135.169 7.8457L143.83 7.8457L143.138 12.8145L140.302 17.6895L142.681 24.2168L138.597 24.2168L137.806 22.0312L136.535 24.2168L132.433 24.2168L136.248 17.7129L134.496 12.8613L134.759 11.0391L138.41 11.0391L138.263 12ZM144.96 24.2168L147.123 21.9844L148.582 11.4961L143.806 11.4961L144.123 9.2168L148.898 9.2168L149.138 7.60547L148.558 6.9375L153.334 6.9375L153 9.2168L154.611 9.2168L154.294 11.4961L152.689 11.4961L151.13 22.6348C151.048 23.0957 150.832 23.4746 150.48 23.7715C150.128 24.0684 149.72 24.2168 149.255 24.2168L144.96 24.2168ZM146.759 19.7051L143.566 19.7051L143.542 13.3184L146.736 13.3184L146.759 19.7051ZM175.513 13.5352L170.831 14.3496L169.824 21.5273L173.925 21.5273L174.986 20.6133L174.458 24.2168L164.878 24.2168L167.181 7.8457L166.414 6.9375L171.886 6.9375L171.195 11.8535L175.871 11.0391L175.513 13.5352ZM161.591 11.9531L165.699 11.9531L165.382 14.2324L161.281 14.2324L160.249 21.5039L164.421 21.0703L164.111 23.3496L155.304 24.2168L157.607 7.82227L156.839 6.9375L162.312 6.9375L161.591 11.9531Z" fill="rgb(5,95,194)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 177 31" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="177.000000" height="31.000000" fill="none" customFrame="#000000">
<path id="Text" d="M8.20898 18.0938L0.914062 18.0938L2.01562 10.3652L9.28711 10.3652L9.65039 7.8457L8.88281 6.9375L14.3496 6.9375L13.8457 10.3652L21.1699 10.3652L20.0625 18.0938L12.7676 18.0938L11.9062 24.2168L7.3418 24.2168L8.20898 18.0938ZM13.5352 12.6504L13.0781 15.8145L15.8379 15.8145L16.2715 12.6504L13.5352 12.6504ZM8.51953 15.8145L8.97656 12.6504L6.24023 12.6504L5.80664 15.8145L8.51953 15.8145ZM32.0175 11.0391L32.1347 10.1309L25.2968 10.1309L25.6073 7.8457L28.3202 7.8457L28.2968 7.77539L27.5761 6.9375L32.1347 6.9375L32.4218 7.8457L37.1034 7.8457L37.2499 7.58203L36.6933 6.9375L41.7148 6.9375L41.2109 7.8457L43.8476 7.8457L43.537 10.1309L36.6933 10.1309L36.5761 11.0391L42.9628 11.0391L42.623 13.3184L36.2421 13.3184L36.1191 14.2324L43.414 14.2324L43.1034 16.5117L23.9491 16.5117L24.2655 14.2324L31.5605 14.2324L31.6777 13.3184L25.2968 13.3184L25.6308 11.0391L32.0175 11.0391ZM27.0487 20.1621L23.8788 20.1621L24.2187 17.8828L29.662 17.8828L30.7226 16.9688L36.1894 16.9688L35.1347 17.8828L42.453 17.8828L42.1191 20.1621L38.4452 20.1621L42.453 24.2168L36.9863 24.2168L33.0488 20.1621L32.5214 20.1621L27.8632 24.2168L22.3905 24.2168L27.0487 20.1621ZM55.6795 9.59766L53.0428 9.81445L52.7733 11.7832L55.7498 11.7832L55.4159 14.2324L52.4159 14.2324L51.0213 24.2168L47.5174 24.2168L48.912 14.2324L45.9588 14.2324L46.2987 11.7832L49.2694 11.7832L49.5096 10.0781L46.703 10.2949L47.0428 7.8457L56.0135 7.1543L55.6795 9.59766ZM65.2538 19.3184L63.9354 19.5117L63.2616 24.2168L59.4705 24.2168L60.0506 20.0156L55.1287 20.6895L55.4862 18.1172L60.408 17.4492L61.7733 7.82227L61.0819 6.9375L65.7108 6.9375L64.2987 16.9453L65.617 16.752L65.2538 19.3184ZM44.6873 23.3496L46.2459 14.9062L48.6248 14.9062L47.0662 23.3496L44.6873 23.3496ZM51.7948 23.3496L52.5623 14.9062L54.9354 14.9062L54.1444 23.3496L51.7948 23.3496ZM59.8807 16.7051L56.3065 16.7051L55.8729 13.5352L59.4471 13.5352L59.8807 16.7051ZM60.6248 11.8828L57.0506 11.8828L56.5702 8.71289L60.1444 8.71289L60.6248 11.8828ZM79.0545 16.0078L81.0232 18.2168L83.3787 16.3418L83.4959 15.4102L75.6736 15.4102L75.9607 13.2949L79.5349 13.2949L79.7986 11.3496L76.0779 11.3496L76.3943 9.07031L80.1384 9.07031L80.3025 7.86914L79.7283 6.96094L84.8142 6.96094L84.5037 9.07031L88.2244 9.07031L87.9138 11.3496L84.1931 11.3496L83.906 13.2949L87.4099 13.2949L86.8298 17.3496L82.992 20.3789L86.4255 24.1699L81.8904 24.1699L80.3494 22.4414L78.0935 24.1934L73.5115 24.1934L78.4568 20.2559L75.5271 16.9219L75.6736 16.0078L79.0545 16.0078ZM68.4724 21.9844L68.9529 18.5039L67.0779 19.0078L67.4412 16.4883L69.3103 15.9844L69.8611 11.9531L68.0623 11.9531L68.4021 9.67383L70.2009 9.67383L70.488 7.63477L69.8904 6.9375L74.6658 6.9375L74.2791 9.67383L75.5974 9.67383L75.2634 11.9531L73.9451 11.9531L73.5584 14.8301L74.9295 14.4492L74.5662 16.9688L73.2009 17.3496L72.4568 22.6348C72.3748 23.0957 72.1541 23.4746 71.7947 23.7715C71.4353 24.0684 71.031 24.2168 70.5818 24.2168L66.3103 24.2168L68.4724 21.9844ZM100.707 10.8223L100.824 9.91406L96.2419 9.91406L96.4821 8.0625L101.088 8.0625L101.134 7.70508L100.414 6.96094L105.265 6.96094L105.095 8.0625L105.793 8.0625L105.599 6.98438L108.722 6.98438L108.986 8.0625L109.894 8.0625L109.654 9.91406L104.855 9.91406L104.715 10.8223L109.297 10.8223L108.382 17.4023L105.072 17.4023L105.142 16.9219L103.847 16.9219L103.754 17.7129L99.7458 17.7129L99.8395 16.9219L98.5915 16.9219L98.5212 17.4258L95.2106 17.4258L96.1188 10.8223L100.707 10.8223ZM90.6462 10.9219L91.0329 8.32617L90.2419 7.41797L95.1813 7.41797L94.6774 10.9219L95.6149 10.9219L95.2809 13.248L94.3669 13.248L92.8317 24.1699L88.8005 24.1699L90.3356 13.248L89.4216 13.248L89.7614 10.9219L90.6462 10.9219ZM107.328 17.8828L107.257 18.334L108.67 18.334L108.406 20.2793L106.97 20.2793L106.631 22.7285C106.599 22.9355 106.515 23.1309 106.379 23.3145C106.242 23.498 106.07 23.6621 105.863 23.8066C105.656 23.9512 105.42 24.0664 105.154 24.1523C104.892 24.2422 104.625 24.2871 104.351 24.2871L100.174 24.2871L102.886 22.5352L103.203 20.2793L94.3434 20.2793L94.6071 18.334L103.466 18.334L103.537 17.8828L107.328 17.8828ZM99.863 23.2324L96.072 23.2324L95.902 20.7598L99.6989 20.7598L99.863 23.2324ZM99.0954 13.1543L100.367 13.1543L100.49 12.3867L99.2184 12.3867L99.0954 13.1543ZM104.498 12.3867L104.398 13.1543L105.646 13.1543L105.769 12.3867L104.498 12.3867ZM98.7848 15.4102L100.056 15.4102L100.174 14.666L98.902 14.666L98.7848 15.4102ZM104.064 15.4102L105.336 15.4102L105.459 14.666L104.181 14.666L104.064 15.4102ZM120.214 8.18555L120.261 7.8457L119.494 6.9375L125.423 6.9375L125.23 8.18555L132.05 8.18555L131.734 10.4414L126.935 10.4414L126.168 15.8906L121.416 15.8906L122.283 14.7832L122.88 10.4414L121.486 10.4414L120.718 15.8906L116.687 15.8906L117.455 10.4414L112.609 10.4414L112.92 8.18555L120.214 8.18555ZM128.617 16.6816L128.447 17.8535L131.183 17.8535L130.849 20.1855L128.113 20.1855L127.539 24.1934L122.974 24.1934L123.554 20.1855L117.742 20.1855L116.16 24.1934L111.531 24.1934L113.113 20.1855L110.834 20.1855L111.168 17.8535L114.05 17.8535L114.531 16.6816L119.16 16.6816L118.679 17.8535L123.888 17.8535L124.058 16.6816L128.617 16.6816ZM116.617 11.1094L115.298 15.2871L111.554 15.2871L112.873 11.1094L116.617 11.1094ZM131.283 11.1094L131.447 15.2871L127.726 15.2871L127.562 11.1094L131.283 11.1094ZM133.201 24.2168L137.425 16.752L135.169 16.752L135.503 14.4492L151.921 14.4492L150.767 22.6348C150.705 23.0801 150.492 23.4551 150.128 23.7598C149.769 24.0645 149.359 24.2168 148.898 24.2168L144.146 24.2168L146.302 21.9844L147.023 16.752L141.984 16.752L137.759 24.2168L133.201 24.2168ZM138.334 13.7754L133.775 13.7754L138.837 7.48828L138.386 6.9375L143.859 6.9375L138.334 13.7754ZM150.24 6.9375L153.837 13.7754L149.279 13.7754L145.681 6.9375L150.24 6.9375ZM165.962 8.30273L176.497 6.9375L176.158 9.33398L169.847 10.0312L169.296 13.0547L176.064 13.0547L175.73 15.334L174.335 15.334L173.087 24.1934L169.32 24.1934L170.568 15.334L168.886 15.334L167.328 24.1934L163.103 24.1934L165.962 8.30273ZM158.521 12.0234L156.218 12.0234L156.529 9.74414L158.855 9.74414L159.142 7.68164L158.615 6.98438L162.962 6.98438L162.576 9.74414L164.878 9.74414L164.568 12.0234L162.242 12.0234L160.537 24.2168L156.816 24.2168L158.521 12.0234ZM154.706 23.3496L156.622 12.5742L158.134 12.5742L156.218 23.3496L154.706 23.3496ZM161.615 21.9844L162.552 12.5742L164.064 12.5742L163.126 21.9844L161.615 21.9844Z" fill="rgb(5,95,194)" fill-rule="nonzero" />
</svg>
......@@ -39,13 +39,26 @@
</div>
<div class="title-right">
<el-select
v-model="select1"
placeholder="按月统计"
v-model="deptValue"
placeholder="全部部门"
class="custom-select"
@change="handleGetDomainContainmentTrend"
>
<el-option label="按月统计" value="按月统计" />
<el-option label="按年统计" value="按年统计" />
<el-option label="全部部门" value="" />
<el-option
v-for="item in departmentList"
:key="item.departId"
:label="item.departName"
:value="item.departId"
/>
</el-select>
<el-select
v-model="methodValue"
placeholder="全部制裁手段"
class="custom-select"
@change="handleGetDomainContainmentTrend"
>
<el-option v-for="item in methodOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</div>
......@@ -62,12 +75,13 @@
</div>
</div>
<div class="news-content">
<div v-for="value in newsList" class="news-item">
<div v-for="value,idx in newsList" :key="idx" class="news-item">
<div class="news-item-title">
<div class="tag-container">
<div v-for="tag in value.tags" :key="tag" :class="getTagClass(tag)">
<!-- <div v-for="tag in value.tags" :key="tag" :class="getTagClass(tag)">
{{ tag }}
</div>
</div> -->
<AreaTag v-for="tag,index in value.tags" :key="index" :tagName="tag"></AreaTag>
</div>
<div class="date">
......@@ -100,7 +114,7 @@
</div>
<div class="select-box">
<div class="rank-btns">
<!-- <div class="rank-btn" :class="{ active: rankType === 'institution' }" @click="rankType = 'institution'">
<div class="rank-btn" :class="{ active: rankType === 'institution' }" @click="rankType = 'institution'">
对我打压机构
</div>
<div class="rank-btn" :class="{ active: rankType === 'enterprise' }" @click="rankType = 'enterprise'">
......@@ -108,22 +122,62 @@
</div>
<div class="rank-btn" :class="{ active: rankType === 'school' }" @click="rankType = 'school'">
受打压院校
</div> -->
</div>
</div>
<el-select v-model="selectedField" placeholder="全部领域" class="field-select">
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="main-box">
<div class="main-box" v-loading="rankLoading" element-loading-background="rgba(255, 255, 255, 0.5)">
<!-- 机构排行的原有样式 -->
<template v-if="rankType === 'institution'">
<div v-for="(item, index) in rankList" :key="index" class="rank-item">
<div class="rank-num" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
<img :src="defaultImg" alt="" class="rank-icon" />
<div class="rank-name">{{ item.name }}</div>
<img :src="item.orgPicture ? item.orgPicture : defaultImg" alt="" class="rank-icon" />
<div class="rank-name" :title="item.name">{{ item.name }}</div>
<div class="rank-progress-container">
<div class="rank-progress-bar" :style="{ width: getProgressWidth(item.count) }"></div>
</div>
<div class="rank-count">{{ item.count }}</div>
</div>
</template>
<!-- 企业/院校排行的表格样式 -->
<template v-else>
<div class="table-header">
<div class="col-rank"></div>
<div class="col-name" style="color: rgb(59, 65, 75); font-weight: 700">
{{ rankType === "enterprise" ? "公司名称" : "院校名称" }}
</div>
<div class="col-domain" style="color: rgb(59, 65, 75); font-weight: 700">所属领域</div>
<div class="col-date" style="color: rgb(59, 65, 75); font-weight: 700">制裁时间</div>
<div class="col-member" v-if="rankType !== 'school'" style="color: rgb(59, 65, 75); font-weight: 700">
关键人物
</div>
</div>
<div class="table-list">
<div v-for="(item, index) in rankList" :key="index" class="table-row">
<div class="col-rank rank-num" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
<div class="col-name flex-align">
<img :src="item.orgPicture ? item.orgPicture : defaultImg" class="rank-icon" />
<span class="text-ellipsis" :title="item.name">{{ item.name }}</span>
</div>
<div class="col-domain">
<div class="domain-tags">
<span
v-for="(tag, tIndex) in item.domains.slice(0, rankType === 'enterprise' ? 2 : 3)"
:key="tIndex"
class="mini-tag"
:class="getTagClass(tag)"
>
{{ tag }}
</span>
</div>
</div>
<div class="col-date">{{ item.date }}</div>
<div class="col-member" v-if="rankType !== 'school'">{{ item.member }}</div>
</div>
</div>
</template>
</div>
</div>
</div>
......@@ -170,7 +224,7 @@
</foreignObject>
</g>
<g v-for="(node, index) in timelineNodes" :key="index">
<g v-for="(node, index) in timelineNodes" :key="index" @click="handleTimeLineNode(node)">
<line :x1="node.x" :y1="node.y" :x2="node.x" :y2="node.y + 150" stroke="#1677ff" stroke-width="1" />
<circle :cx="node.x" :cy="node.y" r="4" fill="#fff" stroke="#1677ff" stroke-width="3" />
......@@ -184,7 +238,8 @@
>
<div class="timeline-content-item">
<div class="item-tags">
<span v-for="tag in node.tags" :key="tag" :class="getTagClass(tag)">{{ tag }}</span>
<!-- <span v-for="tag in node.tags" :key="tag" :class="getTagClass(tag)">{{ tag }}</span> -->
<AreaTag v-for="tag,idx in node.tags" :key="idx" :tagName="tag"></AreaTag>
</div>
<div class="item-title">
<CommonPrompt :content="node.title" />
......@@ -205,6 +260,12 @@
</svg>
</div>
</div>
<div class="left-btn" @click="handleLeft">
<img src="@/assets/icons/card-btn-left.png" alt="">
</div>
<div class="right-btn" @click="handleRight">
<img src="@/assets/icons/card-btn-right.png" alt="">
</div>
</div>
</div>
</template>
......@@ -228,13 +289,82 @@ import {
getDomainContainmentRanking,
getDomainContainmentTimeline
} from "@/api/zmOverview/allDomains";
import { getUSGovernmentLatestDynamic } from "@/api/allGovernment.js";
import { getUSGovernmentLatestDynamic, getDepartmentList, getSanTypeList } from "@/api/allGovernment.js";
import { ElMessage } from "element-plus";
const router = useRouter();
const activeDate = inject("activeDate");
const select1 = ref("按月统计");
const deptValue = ref("");
const methodValue = ref("");
const departmentList = ref([]);
const methodOptions = ref([
// { label: "全部制裁手段", value: "" },
// { label: "法案", value: "-1" },
// { label: "政令", value: "-2" },
// { label: "实体清单", value: "1" },
// { label: "特别国民指定清单", value: "2" },
// { label: "涉军企业", value: "3" },
// { label: "行业制裁识别清单", value: "4" },
// { label: "无法核实清单", value: "5" },
// { label: "军事最终用户清单", value: "6" },
// { label: "非SDN中国军工企业名单", value: "7" },
// { label: "拒绝往来人员清单", value: "8" },
// { label: "军事最终用途与最终用户规则", value: "9" },
// { label: "欧盟合并制裁清单", value: "10" },
// { label: "英国制裁清单", value: "11" },
// { label: "加拿大合并自主制裁清单", value: "12" },
// { label: "商业管制清单", value: "13" }
]);
const handleGetSanList = async () => {
const params = {
orgId: deptValue.value
};
try {
const res = await getSanTypeList(params);
console.log("制裁手段列表", res);
if (res.code === 200 && res.data) {
methodOptions.value = res.data.map(item => {
return {
label: item.name,
value: item.id
};
});
}
} catch (error) {}
};
const getDepartmentListData = async () => {
const params = {
sanTypeId: methodValue.value
};
try {
const res = await getDepartmentList(params);
if (res.code === 200 && res.data) {
departmentList.value = res.data;
}
} catch (error) {
console.error("获取部门数据失败:", error);
}
};
watch(
() => methodValue.value,
val => {
getDepartmentListData();
}
);
watch(
() => deptValue.value,
val => {
handleGetSanList();
}
);
const rankType = ref("institution");
const rankLoading = ref(false);
const selectedField = ref("");
const selectedFieldTimeline = ref("");
const timelineContainerWidth = 1700;
......@@ -278,71 +408,7 @@ const handleClickTitle = item => {
window.open(href, "_blank");
};
const timelineList = ref([
{
date: "2025年 5月",
tags: ["人工智能", "航空航天"],
title: "《国家量子倡议再授权法案》发布",
content: "计划将国家量子倡议延长至2034年,新增研发中心、测试平台,并首次将NASA...",
info: "11月15日 · 参议院 · 科技法案"
},
{
date: "2025年 6月",
tags: ["人工智能"],
title: "《国家量子倡议再授权法案》发布",
content: "计划将国家量子倡议延长至2034年,新增研发中心、测试平台,并首次将NASA...",
info: "11月15日 · 参议院 · 科技法案"
},
{
date: "2025年 7月",
tags: ["集成电路"],
title: "《国家量子倡议再授权法案》发布",
content: "计划将国家量子倡议延长至2034年,新增研发中心、测试平台,并首次将NASA...",
info: "11月15日 · 参议院 · 科技法案"
},
{
date: "2025年 8月",
tags: ["新材料", "量子科技"],
title: "《国家量子倡议再授权法案》发布",
content: "计划将国家量子倡议延长至2034年,新增研发中心、测试平台,并首次将NASA...",
info: "11月15日 · 参议院 · 科技法案"
},
{
date: "2025年 9月",
tags: ["人工智能", "航空航天"],
title: "《国家量子倡议再授权法案》发布",
content: "计划将国家量子倡议延长至2034年,新增研发中心、测试平台,并首次将NASA...",
info: "11月15日 · 参议院 · 科技法案"
},
{
date: "2025年 10月",
tags: ["新材料", "能源"],
title: "《国家量子倡议再授权法案》发布",
content: "计划将国家量子倡议延长至2034年,新增研发中心、测试平台,并首次将NASA...",
info: "11月15日 · 参议院 · 科技法案"
},
{
date: "2025年 11月",
tags: ["人工智能", "航空航天"],
title: "《国家量子倡议再授权法案》发布",
content: "计划将国家量子倡议延长至2034年,新增研发中心、测试平台,并首次将NASA...",
info: "11月15日 · 参议院 · 科技法案"
},
{
date: "2025年 12月",
tags: ["新材料", "能源"],
title: "《国家量子倡议再授权法案》发布",
content: "计划将国家量子倡议延长至2034年,新增研发中心、测试平台,并首次将NASA...",
info: "11月15日 · 参议院 · 科技法案"
},
{
date: "2025年 12月",
tags: ["集成电路"],
title: "《国家量子倡议再授权法案》发布",
content: "计划将国家量子倡议延长至2034年,新增研发中心、测试平台,并首次将NASA...",
info: "11月15日 · 参议院 · 科技法案"
}
]);
const timelineList = ref([]);
// 处理时间线数据的方法
const processTimelineData = rawData => {
......@@ -395,6 +461,8 @@ const handleGetDomainContainmentTimeline = async () => {
// 处理返回的数据结构
const processedData = processTimelineData(res.data);
timelineList.value = processedData;
console.log('timelineList', timelineList.value);
}
} catch (error) {
console.error("获取美对我领域打压遏制时间线失败:", error);
......@@ -409,7 +477,10 @@ const rowHeight = 230;
const startX = 250;
const startY = 45;
const timeLineActiveIndex = ref(0)
const axisDates = computed(() => {
const dates = [];
if (timelineList.value.length > 0) {
dates.push({
......@@ -429,11 +500,40 @@ const axisDates = computed(() => {
y: lastNode.y - 25
});
}
return dates;
return dates
});
const handleLeft = () => {
if(timeLineActiveIndex.value === 0) {
ElMessage.warning('当前已经是第一组数据!')
} else {
timeLineActiveIndex.value --
}
}
const handleRight = () => {
if(timeLineActiveIndex.value === (timelineList.value.length % 9 )) {
ElMessage.warning('当前已经是最后一组数据!')
} else {
timeLineActiveIndex.value ++
}
console.log('axisDates',axisDates.value);
console.log('timelineNodes',timelineNodes.value);
}
const timelineNodes = computed(() => {
return timelineList.value.map((item, index) => {
// 计算起始索引:activeIndex * 9
const startIndex = timeLineActiveIndex.value * 9;
// 计算结束索引:起始索引 + 9(因为slice的第二个参数是结束索引,不包含)
const endIndex = timeLineActiveIndex.value + 9;
const showTimeLineList = timelineList.value.slice(startIndex, endIndex);
return showTimeLineList.map((item, index) => {
const row = Math.floor(index / maxPerRow);
const col = index % maxPerRow;
......@@ -460,7 +560,7 @@ const timelineNodes = computed(() => {
contentWidth: 320,
contentHeight: 180
};
});
})
});
const getColorName = tag => {
......@@ -746,100 +846,22 @@ const handleGetAllDomainCount = async () => {
} catch (error) {}
};
const box5Data = ref({
title: [
"2024-12",
"2025-1",
"2025-2",
"2025-3",
"2025-4",
"2025-5",
"2025-6",
"2025-7",
"2025-8",
"2025-9",
"2025-10",
"2025-11"
],
data: [
{
name: "集成电路",
color: "#0052D9",
value: [87, 78, 74, 67, 60, 59, 63, 66, 63, 58, 56, 62]
},
{
name: "生物科技",
color: "#00A79D",
value: [13, 13, 8, 13, 20, 37, 34, 25, 22, 20, 27, 18]
},
{
name: "量子科技",
color: "#7B61FF",
value: [18, 16, 12, 16, 16, 26, 30, 29, 25, 25, 33, 25]
},
{
name: "新一代信息技术",
color: "#FF9F1C",
value: [10, 22, 22, 34, 48, 51, 46, 55, 55, 60, 68, 70]
},
{
name: "人工智能",
color: "#E34D59",
value: [25, 34, 39, 45, 53, 54, 50, 47, 50, 54, 56, 51]
},
{
name: "通信网络",
color: "#0052D9",
value: [22, 26, 31, 31, 38, 33, 26, 38, 36, 40, 45, 47]
},
{
name: "新能源",
color: "#2BA471",
value: [53, 44, 43, 41, 34, 29, 57, 44, 61, 67, 61, 61]
},
{
name: "先进制造",
color: "#363B42",
value: [70, 75, 78, 75, 75, 80, 73, 51, 71, 77, 80, 89]
},
{
name: "航空航天",
color: "#3762F0",
value: [18, 16, 12, 16, 16, 26, 30, 29, 25, 25, 33, 25]
},
{
name: "海洋",
color: "#76D1FF",
value: [13, 13, 8, 13, 20, 37, 34, 25, 22, 20, 27, 18]
},
{
name: "新材料",
color: "#FFD900",
value: [10, 22, 22, 34, 48, 51, 46, 55, 55, 60, 68, 70]
},
{
name: "深海",
color: "#002060",
value: [22, 26, 31, 31, 38, 33, 26, 38, 36, 40, 45, 47]
},
{
name: "极地",
color: "#A6A6A6",
value: [53, 44, 43, 41, 34, 29, 57, 44, 61, 67, 61, 61]
},
{
name: "核",
color: "#FFB3B3",
value: [25, 34, 39, 45, 53, 54, 50, 47, 50, 54, 56, 51]
}
]
});
const box5Data = ref({});
const handleGetDomainContainmentTrend = async () => {
try {
const res = await getDomainContainmentTrend({
byYOrM: select1.value
});
const { startDate, endDate } = getCalculatedDate(activeDate.value);
const params = {
startDate,
endDate
};
if (deptValue.value) {
params.org = deptValue.value;
}
if (methodValue.value) {
params.sanMeasures = methodValue.value;
}
const res = await getDomainContainmentTrend(params);
console.log("美对华领域打压遏制数量趋势", res);
if (res.code === 200 && res.data) {
......@@ -910,60 +932,16 @@ const processDomainTrendData = rawData => {
};
};
const tagColors = [
{
text: "航空航天",
textColor: "#1677FF", // 蓝色文字
bgColor: "#E6F7FF" // 浅蓝色背景
},
{
text: "能源",
textColor: "#1677FF",
bgColor: "#E6F7FF"
},
{
text: "新材料",
textColor: "#5993EE",
bgColor: "#E6F7FF"
},
{
text: "生物科技",
textColor: "#5993EE",
bgColor: "#E6F7FF"
},
{
text: "人工智能",
textColor: "#D9001B",
bgColor: "#FFECEC"
},
{
text: "集成电路",
textColor: "#1677FF",
bgColor: "#E6F7FF"
}
];
const rankList = ref([]);
const getTagColor = tagName => {
const foundTag = tagColors.find(tag => tag.text === tagName);
return foundTag ? { textColor: foundTag.textColor, bgColor: foundTag.bgColor } : { textColor: "#000", bgColor: "#fff" };
};
const rankList = ref([
{ name: "美国商务部", count: 45 },
{ name: "美国财政部", count: 38 },
{ name: "美国白宫", count: 36 },
{ name: "美国国务院", count: 34 },
{ name: "美国战争部", count: 33 },
{ name: "联邦参议院", count: 31 },
{ name: "美国美国国土安全部", count: 28 },
{ name: "美国贸易代表办公室", count: 20 },
{ name: "联邦通信委员会", count: 16 },
{ name: "美国食品药品监督管理局", count: 12 }
]);
const maxCount = computed(() => {
if (!rankList.value || rankList.value.length === 0) return 0;
return Math.max(...rankList.value.map(item => item.count || 0));
});
const maxCount = Math.max(...rankList.value.map(item => item.count));
const getProgressWidth = count => {
return (count / maxCount) * 100 + "%";
if (!maxCount.value) return "0%";
return (count / maxCount.value) * 100 + "%";
};
// 处理排名数据的方法
......@@ -973,10 +951,25 @@ const processRankingData = rawData => {
}
return rawData.map(item => {
// 格式化日期:2025-10-08 -> 2025.10.8
let formattedDate = "";
if (item.sanctionDate) {
const date = new Date(item.sanctionDate);
if (!isNaN(date.getTime())) {
formattedDate = `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}`;
} else {
formattedDate = item.sanctionDate;
}
}
return {
name: item.orgName || "",
count: item.orgCount || 0,
orgPicture: item.orgPicture // 保留原始图片字段,以防后续需要使用
orgPicture: item.orgPicture,
// 新增字段
domains: item.domainList ? item.domainList.map(d => d.name) : [],
date: formattedDate,
member: item.keyMember || "-"
};
});
};
......@@ -989,16 +982,14 @@ const rankTypeMap = {
// 获取领域遏制排名数据
const handleGetDomainContainmentRanking = async () => {
rankLoading.value = true;
rankList.value = [];
try {
console.log("获取领域遏制排名数据", rankTypeMap[rankType.value], selectedField.value);
const res = await getDomainContainmentRanking(
rankTypeMap[rankType.value],
!!selectedField.value ? selectedField.value : ""
);
console.log("获取领域遏制排名数据", rankTypeMap[rankType.value], selectedField.value);
console.log("美对华领域打压遏制排行", res);
if (res.code === 200 && res.data) {
// 处理返回的数据结构
const processedData = processRankingData(res.data);
......@@ -1008,6 +999,8 @@ const handleGetDomainContainmentRanking = async () => {
console.error("获取美对华领域打压遏制排行失败:", error);
// 设置默认空数组
rankList.value = [];
} finally {
rankLoading.value = false;
}
};
......@@ -1059,11 +1052,26 @@ const prev = () => {
}
};
const handleTimeLineNode = node => {
// console.log("timeLineNode", node);
if (node.eventType === "科技法案") {
window.sessionStorage.setItem("curTabName", node.title);
const route = router.resolve({
path: "/billLayout",
query: {
billId: node.eventId
}
});
window.open(route.href, "_blank");
}
};
onMounted(() => {
// let Chart = getMultiLineChart(box5Data.value);
// setChart(Chart, "chartRef");
handleGetDomainContainmentTrend();
getDepartmentListData();
handleGetSanList();
handleGetAllDomainCount();
handleGetDomainContainmentRanking();
handleGetDomainContainmentTimeline();
......@@ -1077,6 +1085,7 @@ onUnmounted(() => {
watch(activeDate, () => {
handleGetAllDomainCount();
handleGetDomainContainmentTrend();
});
</script>
......@@ -1285,7 +1294,7 @@ watch(activeDate, () => {
.bottom-content {
display: flex;
gap: 17px;
background: rgba(255, 255, 255, 0.65);
.news-section {
width: 792px;
height: 700px;
......@@ -1377,7 +1386,7 @@ watch(activeDate, () => {
.select-box {
width: 691px;
height: 32px;
margin: 17px auto 36px auto;
margin: 10px auto 5px auto;
display: flex;
justify-content: space-between;
align-items: center;
......@@ -1432,17 +1441,18 @@ watch(activeDate, () => {
}
.main-box {
width: 100%;
height: 567px;
padding: 24px 51px 51px 27px;
height: 577px;
padding: 24px 30px 0px 27px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 21px;
gap: 28px;
overflow-y: auto;
.rank-item {
display: flex;
align-items: center;
height: 30px;
flex-shrink: 0;
.rank-num {
width: 24px;
......@@ -1515,6 +1525,167 @@ watch(activeDate, () => {
text-align: right;
}
}
// 新增表格样式
.table-header {
display: flex;
align-items: center;
padding-bottom: 12px;
// border-bottom: 1px solid #eee;
margin-bottom: 8px;
div {
font-family: "Microsoft YaHei";
font-size: 16px;
color: #5f656c;
font-weight: 400;
}
}
.table-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.table-row {
display: flex;
align-items: center;
height: 40px;
.rank-num {
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
text-align: center;
color: #3b414b;
&.rank-1 {
color: #d94b4b;
}
&.rank-2 {
color: #e3935d;
}
&.rank-3 {
color: #ebd348;
}
}
.flex-align {
display: flex;
align-items: center;
}
.rank-icon {
width: 30px;
height: 30px;
margin-right: 12px;
}
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: "Microsoft YaHei";
font-size: 16px;
font-weight: 700;
line-height: 30px;
color: rgb(59, 65, 75);
}
.domain-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
.mini-tag {
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-family: "Microsoft YaHei";
line-height: 1.2;
white-space: nowrap;
// 复用已有的tag颜色逻辑,需要配合 getTagClass
&.tag-item {
// 基础样式
}
&.tag-blue {
color: rgba(9, 88, 217, 1);
background: rgba(230, 244, 255, 1);
border-color: rgba(186, 224, 255, 1);
border: 1px solid rgba(186, 224, 255, 1);
}
&.tag-green {
color: rgba(56, 158, 13, 1);
background: rgba(246, 255, 237, 1);
border-color: rgba(217, 247, 190, 1);
border: 1px solid rgba(217, 247, 190, 1);
}
&.tag-red {
color: rgba(245, 34, 45, 1);
background: rgba(255, 241, 240, 1);
border-color: rgba(255, 163, 158, 1);
border: 1px solid rgba(255, 163, 158, 1);
}
&.tag-orange {
color: rgba(250, 140, 22, 1);
background: rgba(255, 247, 230, 1);
border-color: rgba(255, 213, 145, 1);
border: 1px solid rgba(255, 213, 145, 1);
}
&.tag-purple {
color: rgba(114, 46, 209, 1);
background: rgba(249, 240, 255, 1);
border-color: rgba(211, 173, 247, 1);
border: 1px solid rgba(211, 173, 247, 1);
}
&.tag-cyan {
color: rgba(19, 194, 194, 1);
background: rgba(230, 255, 251, 1);
border-color: rgba(135, 232, 222, 1);
border: 1px solid rgba(135, 232, 222, 1);
}
}
}
}
// 列宽控制
.col-rank {
width: 50px;
text-align: center;
flex-shrink: 0;
}
.col-name {
flex: 1.5;
min-width: 0;
margin-right: 16px;
}
.col-domain {
flex: 1.5;
min-width: 0;
margin-right: 16px;
}
.col-date {
width: 100px;
flex-shrink: 0;
font-family: "Microsoft YaHei";
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: rgb(59, 65, 75);
text-align: left;
}
.col-member {
width: 80px;
flex-shrink: 0;
font-family: "Microsoft YaHei";
font-size: 16px;
color: rgb(59, 65, 75);
text-align: center;
}
}
}
}
......@@ -1536,7 +1707,7 @@ watch(activeDate, () => {
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
background: rgba(255, 255, 255, 0.65);
background: transparent;
.news-item-title {
display: flex;
......@@ -1610,6 +1781,33 @@ watch(activeDate, () => {
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.65);
box-shadow: 0 0 20px rgba(25, 69, 130, 0.1);
position: relative;
.left-btn{
position: absolute;
z-index: 9999;
left: 0;
top: 345px;
width: 24px;
height: 48px;
img{
width: 100%;
height: 100%;
}
}
.right-btn{
position: absolute;
z-index: 9999;
right: 0;
top: 345px;
width: 24px;
height: 48px;
img{
width: 100%;
height: 100%;
}
}
.bottom-item {
width: 100%;
......@@ -1666,7 +1864,6 @@ watch(activeDate, () => {
width: 100%;
height: 652px;
position: relative;
.nav-btn {
position: absolute;
top: 50%;
......@@ -1688,8 +1885,7 @@ watch(activeDate, () => {
.svg-container {
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
overflow: hidden;
display: block;
padding: 0;
box-sizing: border-box;
......@@ -1727,7 +1923,8 @@ watch(activeDate, () => {
gap: 8px;
.item-tags {
width: 350px;
width: 355px;
height: 30px;
display: flex;
gap: 8px;
overflow: auto;
......
<template>
<div class="content-wrapper">
<div class="header" @mouseenter="stopAutoPlay" @mouseleave="startAutoPlay(true)">
<div class="header">
<div class="header-arrow-left" @click="prev">
<img src="../../assets/left-btn.png" alt="" />
</div>
......@@ -9,18 +9,13 @@
</div>
<div class="cards-mask">
<div class="item-box" :style="{ transform: `translateX(-${currentIndex * (307 + 16)}px)` }">
<div
class="header-item"
:class="{
<div class="header-item" :class="{
headerItem1: index % 5 === 0,
headerItem2: index % 5 === 1,
headerItem3: index % 5 === 2,
headerItem4: index % 5 === 3,
headerItem5: index % 5 === 4
}"
v-for="(item, index) in headerList"
:key="index"
>
}" v-for="(item, index) in headerList" :key="index">
<div class="name">{{ item.elementName }}</div>
<div class="num">{{ item.num }}</div>
</div>
......@@ -37,7 +32,8 @@
<div class="title">{{ "最新动态" }}</div>
</div>
<div class="box1-main">
<div class="box1-item" v-for="(item, index) in box1DataList" :key="index">
<div class="box1-item" v-for="(item, index) in box1DataList" :key="index"
@click="handleToDecreeDetail(item)">
<div class="box1-item-left">{{ index + 1 }}</div>
<div class="box1-item-right">
<div class="title">{{ item.name }}</div>
......@@ -48,18 +44,13 @@
<div class="box1-item-right-footer">
<div class="time">{{ item.postDate }}</div>
<div class="area-box">
<div
class="area"
:class="{
<div class="area" :class="{
area1: vall.status === '1',
area2: vall.status === '2',
area3: vall.status === '3',
area4: vall.status === '4',
area5: vall.status === '5'
}"
v-for="(vall, idxx) in item.areaList"
:key="idxx"
>
}" v-for="(vall, idxx) in item.areaList" :key="idxx">
{{ vall.industryName }}
</div>
</div>
......@@ -80,60 +71,50 @@
<div class="box2-main">
<div class="inner-box1">
<div class="left">
<div class="left-main">
<div
class="left-item"
:class="{ leftItemActive: box2LeftActiveIndex === index }"
v-for="(item, index) in box2DataList"
:key="index"
@click="handleClickBox2Item(index)"
>
<el-empty v-if="box2DataList.length === 0" style="padding-top: 80px" description="暂无数据"
:image-size="100" />
<div class="left-item" :class="{ leftItemActive: box2LeftActiveIndex === index }"
v-for="(item, index) in box2DataList" :key="index" @click="handleClickBox2Item(index)">
<div class="id">{{ index + 1 }}</div>
<div class="text">{{ item.name }}</div>
</div>
</div>
<div class="left-footer">
<el-pagination
background
layout="prev, pager, next"
:total="box2Total"
:page-size="box2PageSize"
v-model:current-page="box2CurrentPage"
@current-change="handleGetBox2DataList"
size="small"
:pager-count="6"
/>
<div class="left-footer" v-if="box2DataList.length !== 0">
<el-pagination background layout="prev, pager, next" :total="box2Total" :page-size="box2PageSize"
v-model:current-page="box2CurrentPage" @current-change="handleGetBox2DataList" size="small"
:pager-count="4" />
</div>
</div>
<div class="right">
<div class="title">{{ box2DetailInfo.name }}</div>
<div class="right" @click="handleToDecreeDetail(box2DetailInfo)">
<el-empty v-if="box2DataList.length === 0 || !isShowBox2Info" style="padding-top: 80px"
description="暂无数据" :image-size="100" />
<div v-else>
<div class="title">{{ box2DetailInfo?.name }}</div>
<div class="tag-box">
<div class="tag" v-for="(item, index) in box2DetailInfo.elemetList" :key="index">
{{ item }}
</div>
</div>
<div class="content">{{ box2DetailInfo.describe }}</div>
<div class="content">{{ box2DetailInfo?.describe }}</div>
<div class="area-box">
<div
class="area"
:class="{
<div class="area" :class="{
area1: item.status === '1',
area2: item.status === '2',
area3: item.status === '3',
area4: item.status === '4',
area5: item.status === '5'
}"
v-for="(item, index) in box2DetailInfo.areaList"
:key="index"
>
}" v-for="(item, index) in box2DetailInfo.areaList" :key="index">
{{ item.industryName }}
</div>
</div>
<div class="footer">
<div class="footer" v-show="box2DetailInfo?.postDate || box2DetailInfo?.orgNameList">
{{ `${box2DetailInfo?.postDate} · ${box2DetailInfo?.orgNameList?.toString()} · 行政令` }}
</div>
</div>
</div>
</div>
<div class="inner-box2">
<div class="chart-header">{{ "关键词云" }}</div>
<div class="box2Chart" id="box2Chart"></div>
......@@ -151,31 +132,24 @@
<div class="inner-box1">
<div class="left">
<div class="left-main">
<div
class="left-item"
:class="{ leftItemActive: box3LeftActiveIndex === index }"
v-for="(item, index) in box3DataList"
:key="index"
@click="handleClickBox3Item(index)"
>
<el-empty v-if="box3DataList.length === 0" style="padding-top: 80px" description="暂无数据"
:image-size="100" />
<div class="left-item" :class="{ leftItemActive: box3LeftActiveIndex === index }"
v-for="(item, index) in box3DataList" :key="index" @click="handleClickBox3Item(index)">
<div class="id">{{ index + 1 }}</div>
<div class="text">{{ item.name }}</div>
</div>
</div>
<div class="left-footer" v-if="box3DataList.length">
<el-pagination
background
layout="prev, pager, next"
:total="box3Total"
:page-size="box3PageSize"
v-model:current-page="box3CurrentPage"
@current-change="handleGetBox3DataList"
size="small"
:pager-count="6"
/>
<el-pagination background layout="prev, pager, next" :total="box3Total" :page-size="box3PageSize"
v-model:current-page="box3CurrentPage" @current-change="handleGetBox3DataList" size="small"
:pager-count="4" />
</div>
</div>
<div class="right">
<div class="right" @click="handleToDecreeDetail(box3DetailInfo)">
<el-empty v-if="box3DataList.length === 0 || !isShowBox3Info" style="padding-top: 80px"
description="暂无数据" :image-size="100" />
<div v-else>
<div class="title">{{ box3DetailInfo?.name }}</div>
<div class="tag-box">
<div class="tag" v-for="(item, index) in box3DetailInfo?.elemetList" :key="index">
......@@ -184,18 +158,13 @@
</div>
<div class="content">{{ box3DetailInfo?.describe }}</div>
<div class="area-box">
<div
class="area"
:class="{
<div class="area" :class="{
area1: item.status === '1',
area2: item.status === '2',
area3: item.status === '3',
area4: item.status === '4',
area5: item.status === '5'
}"
v-for="(item, index) in box3DetailInfo?.areaList"
:key="index"
>
}" v-for="(item, index) in box3DetailInfo?.areaList" :key="index">
{{ item.industryName }}
</div>
</div>
......@@ -204,7 +173,10 @@
</div>
</div>
</div>
</div>
<div class="inner-box2">
<el-empty v-if="box3ChartData.length === 0" style="padding-top: 80px" description="暂无数据"
:image-size="100" />
<div class="chart-header">{{ "关键词云" }}</div>
<div class="box3Chart" id="box3Chart"></div>
</div>
......@@ -217,6 +189,7 @@
<script setup>
import { onMounted, ref, computed, inject, watch, onUnmounted } from "vue";
import router from "@/router";
import setChart from "@/utils/setChart";
import getWordCloudChart from "./uitls/worldCloudChart";
import {
......@@ -231,6 +204,10 @@ import {
const activeDate = inject("activeDate");
const isShowBox2Info = ref(true)
const isShowBox3Info = ref(true)
const getCalculatedDate = type => {
const now = new Date();
const endDate = new Date();
......@@ -279,7 +256,7 @@ const handleGetHeaderList = async () => {
if (res.code === 200 && res.data) {
headerList.value = res.data;
}
} catch (error) {}
} catch (error) { }
};
// 最新动态
......@@ -306,7 +283,7 @@ const handleGetBox1Data = async () => {
if (res.code === 200 && res.data) {
box1DataList.value = res.data;
}
} catch (error) {}
} catch (error) { }
};
const box2DataList = ref([]);
......@@ -326,8 +303,12 @@ const handleGetOrderInfo = async id => {
console.log("根据id获取政令详细信息1", res);
if (res.code === 200 && res.data) {
box2DetailInfo.value = res.data;
} else {
isShowBox2Info.value = false
}
} catch (error) {
isShowBox2Info.value = false
}
} catch (error) {}
};
const handleGetBox2DataList = async () => {
......@@ -345,7 +326,7 @@ const handleGetBox2DataList = async () => {
box2Total.value = res.data.totalElements;
handleGetOrderInfo(box2DataList.value[box2LeftActiveIndex.value].id);
}
} catch (error) {}
} catch (error) { }
};
const handleClickBox2Item = index => {
......@@ -370,7 +351,7 @@ const handleGetBox2ChartData = async () => {
};
});
}
} catch (error) {}
} catch (error) { }
};
const handleBox2Chart = async () => {
await handleGetBox2ChartData();
......@@ -396,8 +377,12 @@ const handleGetOrderInfo1 = async id => {
console.log("根据id获取政令详细信息1", res);
if (res.code === 200 && res.data) {
box3DetailInfo.value = res.data;
} else {
isShowBox3Info.value = false
}
} catch (error) {
isShowBox3Info.value = false
}
} catch (error) {}
};
const handleGetBox3DataList = async () => {
......@@ -422,7 +407,7 @@ const handleGetBox3DataList = async () => {
} else {
box3DetailInfo.value = {};
}
} catch (error) {}
} catch (error) { }
};
const handleClickBox3Item = index => {
......@@ -448,7 +433,7 @@ const handleGetBox3ChartData = async () => {
};
});
}
} catch (error) {}
} catch (error) { }
};
const handleBox3Chart = async () => {
await handleGetBox3ChartData();
......@@ -459,9 +444,9 @@ const handleBox3Chart = async () => {
const currentIndex = ref(0);
let autoTimer = null;
const startAutoPlay = (isContiune) => {
if(!isContiune) {
currentIndex.value = 0
const startAutoPlay = isContiune => {
if (!isContiune) {
currentIndex.value = 0;
}
stopAutoPlay();
if (headerList.value.length >= 5) {
......@@ -497,13 +482,24 @@ const prev = () => {
}
};
const handleToDecreeDetail = item => {
window.sessionStorage.setItem("curTabName", item.name);
const route = router.resolve({
path: "/decreeLayout",
query: {
id: item.id
}
});
window.open(route.href, "_blank");
};
watch(activeDate, async () => {
handleGetBox2DataList(); // 美对我要素打压情况
handleGetBox3DataList(); // 美自身要素发展情况
handleBox2Chart(); // 关键词云-上
handleBox3Chart(); // 关键词云-下
await handleGetHeaderList(); // 全要素统计
startAutoPlay();
// startAutoPlay();
});
onMounted(async () => {
......@@ -513,7 +509,7 @@ onMounted(async () => {
handleBox2Chart(); // 关键词云-上
handleBox3Chart(); // 关键词云-下
await handleGetHeaderList(); // 全要素统计
startAutoPlay();
// startAutoPlay();
});
onUnmounted(() => {
......@@ -524,6 +520,7 @@ onUnmounted(() => {
<style lang="scss" scoped>
.content-wrapper {
width: 1600px;
// height: 2132px;
.header {
width: 1600px;
......@@ -534,6 +531,7 @@ onUnmounted(() => {
gap: 16px;
margin: 0 auto;
position: relative;
.header-arrow-left {
position: absolute;
left: -33px;
......@@ -541,11 +539,13 @@ onUnmounted(() => {
width: 24px;
height: 48px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
.header-arrow-right {
position: absolute;
right: -33px;
......@@ -553,6 +553,7 @@ onUnmounted(() => {
width: 24px;
height: 48px;
cursor: pointer;
img {
width: 100%;
height: 100%;
......@@ -577,6 +578,7 @@ onUnmounted(() => {
gap: 16px;
padding: 0;
transition: transform 0.5s ease; // 平滑过渡动画
.header-item {
width: 307px;
height: 178px;
......@@ -596,6 +598,7 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: left;
}
.num {
margin-top: -8px;
margin-left: 112px;
......@@ -611,23 +614,29 @@ onUnmounted(() => {
text-align: left;
}
}
.headerItem1 {
background: url("./assets/images/bg1.png");
}
.headerItem2 {
background: url("./assets/images/bg2.png");
}
.headerItem3 {
background: url("./assets/images/bg3.png");
}
.headerItem4 {
background: url("./assets/images/bg4.png");
}
.headerItem5 {
background: url("./assets/images/bg5.png");
}
}
}
.main {
width: 1598px;
height: 884px;
......@@ -635,6 +644,7 @@ onUnmounted(() => {
margin-top: 14px;
display: flex;
justify-content: space-between;
.box {
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 1);
......@@ -642,28 +652,34 @@ onUnmounted(() => {
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 0.65);
}
.left {
width: 472px;
.box1 {
width: 472px;
height: 884px;
overflow: hidden;
.box1-header {
height: 48px;
display: flex;
box-sizing: border-box;
border-bottom: 1px solid rgba(255, 255, 255, 1);
background: linear-gradient(180deg, rgba(231, 243, 255, 0.5), rgba(231, 243, 255, 0) 100%);
.icon {
width: 22px;
height: 18px;
margin-top: 15px;
margin-left: 16px;
img {
width: 100%;
height: 100%;
}
}
.title {
width: 89px;
height: 31px;
......@@ -679,19 +695,23 @@ onUnmounted(() => {
text-align: left;
}
}
.box1-main {
height: 836px;
overflow: hidden;
overflow-y: auto;
background: rgba(255, 255, 255, 0.65);
.box1-item {
width: 430px;
height: 167px;
padding: 12px 0;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
margin: 0 auto;
display: flex;
cursor: pointer;
.box1-item-left {
width: 30px;
height: 30px;
......@@ -707,11 +727,13 @@ onUnmounted(() => {
line-height: 30px;
letter-spacing: 0px;
}
.box1-item-right {
width: 388px;
margin-left: 12px;
.title {
// height: 30px;
height: 30px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-style: Bold;
......@@ -720,23 +742,35 @@ onUnmounted(() => {
line-height: 30px;
letter-spacing: 0px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
height: 48px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 16px;
font-weight: 400;
line-height: 30px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.tag-box {
margin-top: 4px;
// height: 28px;
height: 28px;
display: flex;
flex-wrap: wrap;
gap: 8px;
.tag {
width: 80px;
height: 28px;
......@@ -752,12 +786,14 @@ onUnmounted(() => {
letter-spacing: 0px;
}
}
.box1-item-right-footer {
height: 30px;
margin-top: 4px;
display: flex;
justify-content: space-between;
align-items: center;
.time {
width: 124px;
height: 30px;
......@@ -770,11 +806,13 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: left;
}
.area-box {
height: 24px;
display: flex;
gap: 8px;
justify-content: flex-end;
.area {
height: 24px;
padding: 0 8px;
......@@ -802,27 +840,33 @@ onUnmounted(() => {
}
}
}
.right {
width: 1110px;
.box2 {
width: 1110px;
height: 434px;
.box2-header {
height: 48px;
box-sizing: border-box;
border-bottom: 1px solid rgba(255, 255, 255, 1);
background: linear-gradient(180deg, rgba(231, 243, 255, 0.5), rgba(231, 243, 255, 0) 100%);
display: flex;
.icon {
width: 17px;
height: 16.5px;
margin-left: 19px;
margin-top: 16px;
img {
width: 100%;
height: 100%;
}
}
.title {
margin-top: 8px;
margin-left: 13px;
......@@ -838,12 +882,14 @@ onUnmounted(() => {
text-align: left;
}
}
.box2-main {
height: 386px;
background: rgba(255, 255, 255, 0.65);
display: flex;
justify-content: center;
gap: 16px;
.inner-box1 {
width: 640px;
height: 368px;
......@@ -852,21 +898,27 @@ onUnmounted(() => {
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
display: flex;
.left {
width: 320px;
border-right: 1px solid rgba(234, 236, 238, 1);
.left-main {
margin-top: 9px;
height: 270px;
.left-item {
height: 54px;
border: 1px solid transparent;
display: flex;
align-items: center;
&:hover {
background: rgba(246, 250, 255, 1);
}
cursor: pointer;
.id {
margin-left: 16px;
width: 24px;
......@@ -882,6 +934,7 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: center;
}
.text {
width: 260px;
height: 30px;
......@@ -899,6 +952,7 @@ onUnmounted(() => {
white-space: nowrap;
}
}
.leftItemActive {
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
......@@ -906,6 +960,7 @@ onUnmounted(() => {
background: rgba(246, 250, 255, 1);
}
}
.left-footer {
margin-top: 30px;
height: 60px;
......@@ -914,11 +969,14 @@ onUnmounted(() => {
align-items: center;
}
}
.right {
width: 320px;
height: 368px;
overflow: hidden;
overflow-y: auto;
cursor: pointer;
.title {
width: 283px;
min-height: 24px;
......@@ -934,6 +992,7 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: justify;
}
.tag-box {
width: 283px;
margin: 0 auto;
......@@ -941,6 +1000,7 @@ onUnmounted(() => {
display: flex;
flex-wrap: wrap;
gap: 8px;
.tag {
height: 28px;
padding: 0 8px;
......@@ -957,11 +1017,12 @@ onUnmounted(() => {
letter-spacing: 0px;
}
}
.content {
margin: 0 auto;
margin-top: 8px;
width: 283px;
max-height: 170px;
height: 150px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-style: Regular;
......@@ -971,17 +1032,22 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: justify;
overflow: hidden;
overflow-y: auto;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
}
.area-box {
width: 283px;
margin: 0 auto;
margin-top: 8px;
margin-top: 28px;
// height: 24px;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 8px;
.area {
height: 24px;
padding: 0 8px;
......@@ -999,6 +1065,7 @@ onUnmounted(() => {
color: rgba(245, 34, 45, 1);
}
}
.footer {
width: 283px;
margin: 0 auto;
......@@ -1015,6 +1082,7 @@ onUnmounted(() => {
}
}
}
.inner-box2 {
width: 412px;
height: 368px;
......@@ -1023,6 +1091,7 @@ onUnmounted(() => {
border-radius: 10px;
overflow: hidden;
position: relative;
.chart-header {
position: absolute;
top: 14px;
......@@ -1041,6 +1110,7 @@ onUnmounted(() => {
line-height: 26px;
letter-spacing: 0px;
}
.box2Chart {
width: 412px;
height: 368px;
......@@ -1048,26 +1118,31 @@ onUnmounted(() => {
}
}
}
.box3 {
margin-top: 16px;
width: 1110px;
height: 434px;
.box3-header {
height: 48px;
box-sizing: border-box;
border-bottom: 1px solid rgba(255, 255, 255, 1);
background: linear-gradient(180deg, rgba(231, 243, 255, 0.5), rgba(231, 243, 255, 0) 100%);
display: flex;
.icon {
width: 17px;
height: 16.5px;
margin-left: 19px;
margin-top: 16px;
img {
width: 100%;
height: 100%;
}
}
.title {
margin-top: 8px;
margin-left: 13px;
......@@ -1083,12 +1158,14 @@ onUnmounted(() => {
text-align: left;
}
}
.box3-main {
height: 386px;
background: rgba(255, 255, 255, 0.65);
display: flex;
justify-content: center;
gap: 16px;
.inner-box1 {
width: 640px;
height: 368px;
......@@ -1097,21 +1174,27 @@ onUnmounted(() => {
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
display: flex;
.left {
width: 320px;
border-right: 1px solid rgba(234, 236, 238, 1);
.left-main {
margin-top: 9px;
height: 270px;
.left-item {
height: 54px;
border: 1px solid transparent;
display: flex;
align-items: center;
&:hover {
background: rgba(246, 250, 255, 1);
}
cursor: pointer;
.id {
margin-left: 16px;
width: 24px;
......@@ -1127,6 +1210,7 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: center;
}
.text {
width: 260px;
height: 30px;
......@@ -1144,6 +1228,7 @@ onUnmounted(() => {
white-space: nowrap;
}
}
.leftItemActive {
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
......@@ -1151,6 +1236,7 @@ onUnmounted(() => {
background: rgba(246, 250, 255, 1);
}
}
.left-footer {
margin-top: 30px;
height: 60px;
......@@ -1159,11 +1245,14 @@ onUnmounted(() => {
align-items: center;
}
}
.right {
width: 320px;
height: 368px;
overflow: hidden;
overflow-y: auto;
cursor: pointer;
.title {
width: 283px;
min-height: 24px;
......@@ -1179,6 +1268,7 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: justify;
}
.tag-box {
width: 283px;
margin: 0 auto;
......@@ -1186,6 +1276,7 @@ onUnmounted(() => {
display: flex;
flex-wrap: wrap;
gap: 8px;
.tag {
height: 28px;
padding: 0 8px;
......@@ -1202,11 +1293,12 @@ onUnmounted(() => {
letter-spacing: 0px;
}
}
.content {
margin: 0 auto;
margin-top: 8px;
width: 283px;
max-height: 170px;
height: 150px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-style: Regular;
......@@ -1216,17 +1308,22 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: justify;
overflow: hidden;
overflow-y: auto;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
}
.area-box {
width: 283px;
margin: 0 auto;
margin-top: 8px;
margin-top: 28px;
// height: 24px;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 8px;
.area {
height: 24px;
padding: 0 8px;
......@@ -1244,6 +1341,7 @@ onUnmounted(() => {
color: rgba(245, 34, 45, 1);
}
}
.footer {
width: 283px;
margin: 0 auto;
......@@ -1260,6 +1358,7 @@ onUnmounted(() => {
}
}
}
.inner-box2 {
width: 412px;
height: 368px;
......@@ -1268,6 +1367,7 @@ onUnmounted(() => {
border-radius: 10px;
overflow: hidden;
position: relative;
.chart-header {
position: absolute;
top: 14px;
......@@ -1286,6 +1386,7 @@ onUnmounted(() => {
line-height: 26px;
letter-spacing: 0px;
}
.box3Chart {
width: 412px;
height: 368px;
......@@ -1296,26 +1397,31 @@ onUnmounted(() => {
}
}
}
.area1 {
border: 1px solid rgba(217, 247, 190, 1) !important;
background: rgba(246, 255, 237, 1) !important;
color: rgba(56, 158, 13, 1) !important;
}
.area2 {
border: 1px solid rgba(135, 232, 222, 1) !important;
background: rgba(230, 255, 251, 1) !important;
color: rgba(19, 168, 168, 1) !important;
}
.area3 {
border: 1px solid rgba(145, 202, 255, 1) !important;
background: rgba(230, 244, 255, 1) !important;
color: rgba(22, 119, 255, 1) !important;
}
.area4 {
border: 1px solid rgba(173, 198, 255, 1) !important;
background: rgba(240, 245, 255, 1) !important;
color: rgba(47, 84, 235, 1) !important;
}
.area5 {
border: 1px solid rgba(211, 173, 247, 1) !important;
background: rgba(249, 240, 255, 1) !important;
......
......@@ -60,6 +60,7 @@
<el-select
v-model="methodValue"
placeholder="全部制裁手段"
placement="bottom-end"
class="custom-select"
@change="getUSChinaSanctionTrendData"
>
......@@ -79,11 +80,11 @@
</div>
<div class="text-item-content">
<div v-for="(item, index) in dynamicList" :key="index" class="dynamic-item">
<img :src="defaultImg" alt="" class="item-icon" />
<img :src="item.logoUrl ? item.logoUrl : defaultImg" alt="" class="item-icon" />
<div class="item-right">
<div class="dynamic-item-header">
<span class="item-title" @click="handleNewsClick(item)">{{ item.title }}</span>
<span class="item-date">{{ item.date }} · {{ item.orgName }}</span>
<span class="item-date">{{ item.date }}</span>
</div>
<el-tooltip
effect="dark"
......@@ -164,12 +165,12 @@
<span>美政府部门对我打压遏制时间线</span>
</div>
<div class="bottom-item-select">
<div class="select-btn" :class="{ active: measureType === 'history' }" @click="measureType = 'history'">
<!-- <div class="select-btn" :class="{ active: measureType === 'history' }" @click="measureType = 'history'">
历史举措
</div>
<div class="select-btn" :class="{ active: measureType === 'future' }" @click="measureType = 'future'">
未来举措
</div>
</div> -->
<el-select v-model="selectedField" placeholder="全部领域" class="field-select">
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
......@@ -178,7 +179,7 @@
<div class="bottom-content">
<div class="timeline-container">
<div class="timeline-list">
<div v-for="(dept, index) in filteredTimelineList" :key="index" class="dept-row">
<div v-for="(dept, index) in timelineList" :key="index" class="dept-row">
<div class="dept-info">
<img :src="dept.icon || defaultImg" alt="" class="dept-icon" />
<div class="dept-text">
......@@ -187,7 +188,21 @@
</div>
</div>
<div class="events-container">
<div v-for="(event, eIndex) in dept.events" :key="eIndex" class="event-card" :class="getCardColorClass(event.level)">
<div
v-for="(event, eIndex) in dept.events"
:key="eIndex"
class="event-card"
:class="{
'card-red': event.level === 'red',
'card-yellow': event.level === 'yellow',
'card-green': event.level === 'green',
'card-white': event.level === 'white',
'card-default': !event.level
}"
:style="event.noData ? 'opacity:0;' : ''"
@click="handleClickEventCard(event)"
>
<div class="card-mask" v-if="activeSanctionId && activeSanctionId !== event.sanctionId"></div>
<div class="card-top-line" :class="getLineColorClass(event.level)"></div>
<div class="event-header">
<div class="event-date">{{ event.date }}</div>
......@@ -244,7 +259,9 @@ import {
getUSGovernmentLatestDynamic,
getUSGovernmentJointSanctionRank,
getUSGovernmentSanctionHistory,
getDepartmentList
getDepartmentList,
getSanTypeList,
getThreeMonthSanctionProcess
} from "@/api/allGovernment.js";
const router = useRouter();
......@@ -272,12 +289,18 @@ const getUSGovernmentSanctionHistoryData = async () => {
loadingHistory.value = true;
try {
const params = {
onlyHistory: measureType.value === "history",
onlyFuture: measureType.value === "future",
// onlyHistory: measureType.value === "history",
// onlyFuture: measureType.value === "future",
// onlyHistory: false,
// onlyFuture: true,
field: selectedField.value || null
// startDate: new Date(),
// endDate: new Date(new Date().setMonth(new Date().getMonth() + 3))
};
const res = await getUSGovernmentSanctionHistory(params);
const res = await getThreeMonthSanctionProcess(params);
console.log("未来三个月打压遏制时间线", res);
if (res.code === 200 && res.data) {
// 如果返回的是 content 数组(分页结构)或直接是数组
const rawList = res.data.content || res.data;
......@@ -305,13 +328,49 @@ const getUSGovernmentSanctionHistoryData = async () => {
else if (item.status === 4) level = "white";
grouped[deptName].events.push({
date: item.postDate ? item.postDate.replace(/^(\d{4})-(\d{2})-(\d{2})$/, "$1年$2月$3日") : "",
date: item.endDate ? item.endDate.replace(/^(\d{4})-(\d{2})-(\d{2})$/, "$1年$2月$3日") : "",
time: item.endDate,
content: item.name || item.summary,
tags: item.techDomainList ? item.techDomainList.slice(0, 2) : [],
level: level
level: level,
sanctionId: item.sanctionId
});
});
timelineList.value = Object.values(grouped);
//统计完成后生成新的数组
let fArr = []
rawList.forEach((item)=>{
fArr.push(item.endDate)
})
let timeLine = Object.values(grouped);
//重新遍历timeLine,根据日期数组额外增加数据
timeLine.forEach((item) => {
// 根据日期数组构建空数据
const data = []
for (let i = 0; i < fArr.length; i++) {
const option = {
date: fArr[i],
noData: true
}
data.push(option)
}
item.events.forEach((ele) => {
for (let m = 0; m < data.length; m++) {
if(ele.time == data[m].date && data[m].noData){
data[m] = ele
data[m].noData = false
break
}
}
})
item.events = data
})
// timelineList.value = Object.values(grouped);
timelineList.value = timeLine
console.log("timelineList", timelineList.value);
initSlider();
}
} catch (error) {
......@@ -356,6 +415,8 @@ const getUSGovernmentLatestDynamicData = async () => {
try {
const res = await getUSGovernmentLatestDynamic();
if (res.code === 200 && res.data) {
console.log("美政府部门打压遏制最新动态", res);
dynamicList.value = res.data.map(item => ({
title: item.orgName ? `${item.orgName}${item.title}` : item.title,
date: item.time ? item.time.replace(/^(\d{4})-(\d{2})-(\d{2})$/, "$1年$2月$3日") : "",
......@@ -363,7 +424,8 @@ const getUSGovernmentLatestDynamicData = async () => {
content: item.content || item.title,
tags: item.industrylist || [],
id: item.id,
orgName: item.orgName || ""
orgName: item.orgName || "",
logoUrl: item.logoUrl
}));
}
} catch (error) {
......@@ -455,18 +517,22 @@ const getUSChinaSanctionTrendData = async () => {
};
// 全政府-获取部门数据
const departmentList = ref([]);
const loadingDepartment = ref(false);
// const loadingDepartment = ref(false);
const getDepartmentListData = async () => {
loadingDepartment.value = true;
// loadingDepartment.value = true;
const params = {
sanTypeId: methodValue.value
};
try {
const res = await getDepartmentList();
const res = await getDepartmentList(params);
console.log("部门列表", res);
if (res.code === 200 && res.data) {
departmentList.value = res.data;
}
} catch (error) {
console.error("获取部门数据失败:", error);
} finally {
loadingDepartment.value = false;
// loadingDepartment.value = false;
}
};
......@@ -479,6 +545,7 @@ const getGovernmentList = async () => {
const res = await getAllGovernmentList({
monthNum: monthNum.value
});
console.log("全政府统计", res);
if (res.code === 200 && res.data) {
governmentList.value = res.data;
// 如果后端返回了数据,则更新 cardList
......@@ -517,7 +584,7 @@ const getGovernmentList = async () => {
return {
title: item.departName || "未知部门",
icon: defaultIcon, // 暂时使用默认图标
icon: item.departLogo, // 暂时使用默认图标
stats: stats,
departId: item.departId
};
......@@ -653,26 +720,26 @@ const fieldOptions = ref([
{ label: "军事", value: "23" },
{ label: "科技", value: "24" },
{ label: "安全", value: "25" },
{ label: "其他", value: "99" },
{ label: "其他", value: "99" }
]);
const methodOptions = ref([
{ label: "全部制裁手段", value: "" },
{ label: "法案", value: "-1" },
{ label: "政令", value: "-2" },
{ label: "实体清单", value: "1" },
{ label: "特别国民指定清单", value: "2" },
{ label: "涉军企业", value: "3" },
{ label: "行业制裁识别清单", value: "4" },
{ label: "无法核实清单", value: "5" },
{ label: "军事最终用户清单", value: "6" },
{ label: "非SDN中国军工企业名单", value: "7" },
{ label: "拒绝往来人员清单", value: "8" },
{ label: "军事最终用途与最终用户规则", value: "9" },
{ label: "欧盟合并制裁清单", value: "10" },
{ label: "英国制裁清单", value: "11" },
{ label: "加拿大合并自主制裁清单", value: "12" },
{ label: "商业管制清单", value: "13" },
// { label: "全部制裁手段", value: "" },
// { label: "法案", value: "-1" },
// { label: "政令", value: "-2" },
// { label: "实体清单", value: "1" },
// { label: "特别国民指定清单", value: "2" },
// { label: "涉军企业", value: "3" },
// { label: "行业制裁识别清单", value: "4" },
// { label: "无法核实清单", value: "5" },
// { label: "军事最终用户清单", value: "6" },
// { label: "非SDN中国军工企业名单", value: "7" },
// { label: "拒绝往来人员清单", value: "8" },
// { label: "军事最终用途与最终用户规则", value: "9" },
// { label: "欧盟合并制裁清单", value: "10" },
// { label: "英国制裁清单", value: "11" },
// { label: "加拿大合并自主制裁清单", value: "12" },
// { label: "商业管制清单", value: "13" }
// { label: "232调查", value: "14" },
// { label: "Capta List (CAP) - Treasury Department", value: "15" },
// { label: "ITAR Debarred (DTC) - State Department", value: "16" },
......@@ -682,6 +749,38 @@ const methodOptions = ref([
// { label: "经验证最终用户清单", value: "20" }
]);
const handleGetSanList = async () => {
const params = {
orgId: deptValue.value
};
try {
const res = await getSanTypeList(params);
console.log("制裁手段列表", res);
if (res.code === 200 && res.data) {
methodOptions.value = res.data.map(item => {
return {
label: item.name,
value: item.id
};
});
}
} catch (error) {}
};
watch(
() => methodValue.value,
val => {
getDepartmentListData();
}
);
watch(
() => deptValue.value,
val => {
handleGetSanList();
}
);
const dynamicList = ref([]);
const getColorName = tag => {
......@@ -771,12 +870,15 @@ const initSlider = () => {
// 设定总范围:2010年1月1日 - 当前年份年底
const currentYear = new Date().getFullYear();
const rangeMin = new Date(2018, 0, 1).getTime();
const rangeMax = new Date(currentYear, 11, 31).getTime();
const rangeMin = new Date().setHours(0, 0, 0, 0);
const rangeMax = new Date(new Date().setMonth(new Date().getMonth() + 12)).setHours(0, 0, 0, 0);
// 设定默认选中范围:2025-01-01 到 2025-04-25
const defaultStart = new Date(2025, 0, 1).getTime();
const defaultEnd = new Date(2025, 3, 25).getTime();
// const defaultStart = new Date(2025, 0, 1).getTime();
// const defaultEnd = new Date(2025, 3, 25).getTime();
const defaultStart = new Date().setHours(0, 0, 0, 0);
const defaultEnd = new Date(new Date().setMonth(new Date().getMonth() + 3)).setHours(0, 0, 0, 0);
// 初始化 dateRange 为默认选中范围
dateRange.value = [defaultStart, defaultEnd];
......@@ -955,6 +1057,16 @@ const initChart = (xAxisData = [], seriesData = []) => {
myChart.setOption(option);
};
const activeSanctionId = ref(null);
const handleClickEventCard = item => {
console.log("item", item);
if (activeSanctionId.value === item.sanctionId) {
activeSanctionId.value = null;
} else {
activeSanctionId.value = item.sanctionId;
}
};
// 监听时间范围变化,重新获取趋势数据
watch(
() => activeDate.value,
......@@ -970,6 +1082,8 @@ watch([() => measureType.value, () => selectedField.value], () => {
});
onMounted(() => {
getDepartmentListData();
handleGetSanList();
initChart();
initSlider();
getGovernmentList();
......@@ -977,7 +1091,7 @@ onMounted(() => {
getUSGovernmentLatestDynamicData();
getUSGovernmentJointSanctionRankData();
getUSGovernmentSanctionHistoryData();
getDepartmentListData();
startAutoPlay();
window.addEventListener("resize", () => {
myChart && myChart.resize();
......@@ -1054,10 +1168,10 @@ const prev = () => {
background-size: 358px 358px;
background-repeat: no-repeat;
background-position: center;
filter: blur(20px);
// filter: blur(20px);
z-index: 0;
opacity: 0.8;
background-color: rgba(5, 33, 77, 0.7); // 调整为更明显的深蓝色
// opacity: 0.8;
// background-color: rgba(5, 33, 77, 0.7); // 调整为更明显的深蓝色
&::after {
content: "";
......@@ -1066,7 +1180,10 @@ const prev = () => {
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg, rgba(5, 33, 77, 1) 0%, rgba(5, 33, 77, 0) 50%, rgba(5, 33, 77, 1) 100%);
// background: linear-gradient(90deg, rgba(5, 33, 77, 1) 0%, rgba(5, 33, 77, 0) 50%, rgba(5, 33, 77, 1) 100%);
// background: linear-gradient(180deg, rgba(34, 41, 52, 0.8) 0%, rgba(34, 41, 52, 0.4) 100%);
backdrop-filter: blur(100px);
background: linear-gradient(180deg, rgba(34, 41, 52, 0.8) 0%, rgba(34, 41, 52, 0.4) 100%);
}
}
......@@ -1090,6 +1207,8 @@ const prev = () => {
.card-icon {
width: 36px; // 假设大小
height: 36px;
border-radius: 50%;
overflow: hidden;
}
}
......@@ -1157,7 +1276,9 @@ const prev = () => {
.main-charts {
width: 1601px;
height: 500px;
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.65);
box-shadow: 0 0 20px rgba(25, 69, 130, 0.1);
display: flex;
......@@ -1173,6 +1294,7 @@ const prev = () => {
padding-left: 17px;
padding-right: 35px;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(231, 243, 255, 1) 0%, rgba(231, 243, 255, 0) 100%);
.title-left {
......@@ -1263,10 +1385,11 @@ const prev = () => {
.text-item {
width: 792px;
height: 700px;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.65);
box-shadow: 0 0 20px rgba(25, 69, 130, 0.1);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
overflow: hidden;
.text-item-title {
width: 100%;
height: 48px;
......@@ -1278,8 +1401,8 @@ const prev = () => {
background: linear-gradient(180deg, rgba(231, 243, 255, 1) 0%, rgba(231, 243, 255, 0) 100%);
img {
width: 18px;
height: 18px;
width: 24px;
height: 24px;
margin-right: 14px;
}
......@@ -1457,6 +1580,8 @@ const prev = () => {
.dept-icon {
width: 24px;
height: 24px;
border-radius: 12px;
overflow: hidden;
margin-right: -8px; // 图标叠加效果
&:last-child {
......@@ -1728,7 +1853,7 @@ const prev = () => {
display: flex;
align-items: center;
gap: 8px;
overflow-x: auto;
// overflow-x: auto;
// 启用滚动条
&::-webkit-scrollbar {
......@@ -1763,6 +1888,16 @@ const prev = () => {
position: relative;
overflow: hidden;
flex-shrink: 0;
cursor: pointer;
.card-mask {
width: 100%;
height: 100%;
left: 0;
top: 0;
position: absolute;
z-index: 99;
background: rgba(125, 125, 125, 0.8) !important;
}
&.card-red {
background: linear-gradient(180deg, rgba(206, 79, 81, 0.08) 0%, rgba(255, 255, 255, 1) 60%);
......@@ -1920,6 +2055,10 @@ const prev = () => {
}
}
}
.event-no-card{
width: 240px;
height: 130px;
}
}
}
}
......
No preview for this file type
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论