提交 28ad9e83 authored 作者: 张伊明's avatar 张伊明

Merge branch 'master' of http://8.140.26.4:10003/caijian/risk-monitor into zym-dev

VITE_BASE_API= '/api' VITE_BASE_API= '/api'
# 图表解读等 /aiAnalysis 代理服务密钥(勿提交到公开仓库时可改为本地 .env.local)
VITE_AI_ANALYSIS_API_KEY=aircasKEY19491001
\ No newline at end of file
# 线上地址 # 线上地址
VITE_BASE_API= '/api' VITE_BASE_API= '/api'
# 图表解读等 AI 服务(与部署环境一致时填写)
VITE_AI_ANALYSIS_API_KEY=aircasKEY19491001
\ No newline at end of file
const baseUrl = `http://8.140.26.4:9085/` const baseUrl = `http://8.140.26.4:9085`
\ No newline at end of file \ No newline at end of file
...@@ -251,7 +251,7 @@ body { ...@@ -251,7 +251,7 @@ body {
/* 可点击文本 鼠标悬浮样式 */ /* 可点击文本 鼠标悬浮样式 */
#app .text-click-hover:hover { #app .text-click-hover:hover {
text-decoration: underline; text-decoration: underline;
color: rgb(5, 95, 194); color: var(--color-primary-100);
cursor: pointer; cursor: pointer;
} }
/* #endregion 公共样式类名 */ /* #endregion 公共样式类名 */
......
...@@ -5,6 +5,7 @@ export function getDepartmentList(params) { ...@@ -5,6 +5,7 @@ export function getDepartmentList(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeDict/department`, url: `/api/administrativeDict/department`,
params
}) })
} }
...@@ -27,34 +28,36 @@ export function getDecreeRiskSignal(params) { ...@@ -27,34 +28,36 @@ export function getDecreeRiskSignal(params) {
// 行政令发布频度 // 行政令发布频度
export function getDecreeYearOrder(params) { export function getDecreeYearOrder(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeOrderOverview/yearOrder/${params.year}`, url: `/api/administrativeOrderOverview/yearOrder`,
params data: params
}) })
} }
// 政令涉及领域 // 政令涉及领域
export function getDecreeArea(params) { export function getDecreeArea(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeOrderOverview/industry/${params.year}`, url: `/api/administrativeOrderOverview/industry`,
params data: params
}) })
} }
// 关键行政令 // 关键行政令
export function getKeyDecree(params) { export function getKeyDecree(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeOrderOverview/action?pageSize=${params.pageSize}&pageNum=${params.pageNum}`, url: `/api/administrativeOrderOverview/action`,
data: params
}) })
} }
// 政令重点条款 // 政令重点条款
export function getDecreeKeyInstruction() { export function getDecreeKeyInstruction(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeOrderOverview/instruction`, url: `/api/administrativeOrderOverview/instruction`,
data: params
}) })
} }
...@@ -86,3 +89,21 @@ export function getDecreeTypeList() { ...@@ -86,3 +89,21 @@ export function getDecreeTypeList() {
url: `/api/administrativeDict/type`, url: `/api/administrativeDict/type`,
}) })
} }
// 关键机构
export function getKeyOrganization() {
return request({
method: 'GET',
url: `/api/commonFeature/keyOrganization`,
})
}
// AI智能总结
export function getChartInterpretation(params) {
return request({
method: 'POST',
url: `/aiAnalysis/chart_interpretation`,
headers: {"X-API-Key": "aircasKEY19491001"},
data: params
})
}
\ No newline at end of file
...@@ -20,6 +20,39 @@ export function getDecreehylyList() { ...@@ -20,6 +20,39 @@ export function getDecreehylyList() {
}) })
} }
// 获取受影响实体列表
export function getDecreeEntities(params) {
return request({
method: 'POST',
url: `/api/administrativeOrderInfo/relatedEntities`,
data: params
})
}
// 获取实体产业链列表
export function getDecreeRelatedChain(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/relatedChain/${params.id}`,
})
}
// 获取产业链节点列表
export function getDecreeChainNodes(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/relatedChainNodes/${params.id}`,
})
}
// 获取实体关系节点列表
export function getDecreeRelatedEntitie(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/listRelatedEntitie/${params.id}`,
})
}
// 根据政行业领域ID获取公司列表 // 根据政行业领域ID获取公司列表
/** /**
* @param {cRelated, id} * @param {cRelated, id}
......
...@@ -78,3 +78,19 @@ export function getDecreeReport(params) { ...@@ -78,3 +78,19 @@ export function getDecreeReport(params) {
url: `/api/administrativeOrderInfo/contentUrl/${params.id}`, url: `/api/administrativeOrderInfo/contentUrl/${params.id}`,
}) })
} }
// 政令关键词云
export function getKeyWordUp() {
return request({
method: 'GET',
url: `/api/element/getKeyWordUp/2025-01-01`,
})
}
// 报告内容摘要
export function getOverview(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/overview/${params.id}`,
})
}
\ No newline at end of file
...@@ -51,6 +51,14 @@ service.interceptors.request.use(config => { ...@@ -51,6 +51,14 @@ service.interceptors.request.use(config => {
config.headers['token'] = token config.headers['token'] = token
// config.headers['Authorization'] = `Bearer ${token}` // 如果后端需要Bearer格式可以使用这个 // config.headers['Authorization'] = `Bearer ${token}` // 如果后端需要Bearer格式可以使用这个
} }
// 图表解读等 AI 分析服务(Vite 代理 /aiAnalysis)需要 X-API-Key
const reqUrl = String(config.url || '')
if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) {
config.headers['X-API-Key'] = aiApiKey
}
}
return config return config
}, error => { }, error => {
console.log(error) console.log(error)
...@@ -82,8 +90,14 @@ service.interceptors.response.use( ...@@ -82,8 +90,14 @@ service.interceptors.response.use(
// 重复请求触发的取消不提示错误 // 重复请求触发的取消不提示错误
if (isCanceledError) return Promise.reject(error) if (isCanceledError) return Promise.reject(error)
// 处理token过期或无效的情况 // 处理token过期或无效的情况(排除 AI 分析服务:其 401 多为 API Key 问题)
if (error.response && (error.response.status === 401 || error.response.status === 403)) { const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
if (
error.response &&
(error.response.status === 401 || error.response.status === 403) &&
!isAiAnalysisRequest
) {
ElMessage({ ElMessage({
message: 'Token已过期,请重新登录', message: 'Token已过期,请重新登录',
type: 'error', type: 'error',
......
...@@ -10,6 +10,13 @@ export function getThinkTankList() { ...@@ -10,6 +10,13 @@ export function getThinkTankList() {
}) })
} }
export function getAllThinkTankList(params) {
return request({
method: 'GET',
url: '/api/thinkTankOverview/thinkTanks/page',
params: params
})
}
//智库概览:获取智库发布 //智库概览:获取智库发布
export function getNewReport() { export function getNewReport() {
return request({ return request({
...@@ -28,13 +35,19 @@ export function getThinkTankRiskSignal() { ...@@ -28,13 +35,19 @@ export function getThinkTankRiskSignal() {
}) })
} }
// 政策建议趋势分布 /**
* 政策建议趋势分布(数量变化趋势)
* @param {{ startDate: string, endDate: string }} params - 如 2024-01-01 ~ 2024-12-31
*/
export function getThinkTankPolicyIndustryChange(params) { export function getThinkTankPolicyIndustryChange(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/thinkTankOverview/policyIndustryChange/${params}`, url: `/api/thinkTankOverview/policyIndustryChange`,
params: {
}) startDate: params.startDate,
endDate: params.endDate
}
});
} }
// 政策建议领域分布 // 政策建议领域分布
...@@ -113,10 +126,44 @@ export function getThinkDynamicsReportType() { ...@@ -113,10 +126,44 @@ export function getThinkDynamicsReportType() {
//智库动态:获取智库报告 //智库动态:获取智库报告
export function getThinkDynamicsReport(params) { export function getThinkDynamicsReport(params) {
const safe = params || {}
// 兼容两种调用方式:
// 1) { id, startDate, authorName, currentPage, pageSize, researchTypeIds, searchText, sortFun, years }
// 2) { id, startDate, parmas: { authorName, currentPage, pageSize, researchTypeIds, searchText, sortFun, years } }
const inner = safe.parmas && typeof safe.parmas === 'object' ? safe.parmas : {}
const id = safe.id
const startDate = safe.startDate
const authorName = inner.authorName ?? safe.authorName ?? ''
const currentPage = inner.currentPage ?? safe.currentPage ?? 1
const pageSize = inner.pageSize ?? safe.pageSize ?? 10
const researchTypeIds = inner.researchTypeIds ?? safe.researchTypeIds ?? ''
const searchText = inner.searchText ?? safe.searchText ?? ''
const sortFun = inner.sortFun ?? safe.sortFun ?? false
const years = inner.years ?? safe.years ?? null
const query = { currentPage, pageSize, sortFun }
// 仅在有值时才传,避免后端按空值筛选
if (authorName) query.authorName = authorName
if (researchTypeIds) query.researchTypeIds = researchTypeIds
if (searchText) query.searchText = searchText
if (years !== null && years !== undefined && years !== '') query.years = years
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankInfo/report/${params.id}/${params.startDate}`, url: `/api/thinkTankInfo/report/${id}/${startDate}`,
params: params.parmas params: query
})
}
// 智库领域观点分析(流式)
// [POST] 8.140.26.4:10029/report-domain-view-analysis
export function postReportDomainViewAnalysis(data) {
return request({
method: 'POST',
// 开发环境走 Vite 同源代理,避免浏览器跨域(见 vite.config.js:/intelligent-api -> 8.140.26.4:10029)
url: '/intelligent-api/report-domain-view-analysis',
data
}) })
} }
...@@ -152,15 +199,31 @@ export function getThinkPolicyIndustryChange(params) { ...@@ -152,15 +199,31 @@ export function getThinkPolicyIndustryChange(params) {
}) })
} }
//获取智库政策 /**
* 获取智库政策(政策追踪列表)
* GET /api/thinkTankInfo/policy
* Query: thinkTankId, startDate, endDate, orgIds, domainIds(科技领域/智库领域,逗号分隔 id), pageNum, pageSize, sortField, sortOrder, sortFun, reportId 等
*/
export function getThinkPolicy(params) { export function getThinkPolicy(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankInfo/policy/${params.id}/${params.startDate}`, url: '/api/thinkTankInfo/policy',
params params
}) })
} }
/**
* 政府机构字典(政策追踪-涉及部门筛选项)
* GET /api/commonDict/gov/agency
* @returns {Promise<{ code: number, data: Array<{ id: string, name: string }> }>}
*/
export function getGovAgencyDict() {
return request({
method: 'GET',
url: '/api/commonDict/gov/agency'
})
}
//智库百科基本信息 //智库百科基本信息
export function getThinkTankInfoBasic(params) { export function getThinkTankInfoBasic(params) {
return request({ return request({
...@@ -299,9 +362,26 @@ export function getThinkTankReportPolicy(params) { ...@@ -299,9 +362,26 @@ export function getThinkTankReportPolicy(params) {
//获取相关政策动态 //获取相关政策动态
export function getThinkTankReportPolicyAction(params) { export function getThinkTankReportPolicyAction(params) {
const {
reportId,
currentPage,
pageSize,
keyword = "",
orgIds = "",
// 新增:按科技领域 / 标签过滤
industryName = ""
} = params;
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankReport/policyAction/${params}`, url: `/api/thinkTankReport/policyDetail/${reportId}`,
params: {
currentPage,
pageSize,
keyword,
// 后端按标签过滤使用的字段
industryName,
orgIds,
}
}) })
} }
......
...@@ -326,9 +326,8 @@ watch(isTranslate, () => { ...@@ -326,9 +326,8 @@ watch(isTranslate, () => {
.report-main { .report-main {
flex: auto; flex: auto;
min-height: 0; height: 20px;
box-sizing: border-box; padding: 10px 0;
padding-top: 10px;
:deep(.el-scrollbar) { :deep(.el-scrollbar) {
height: 100%; height: 100%;
......
...@@ -5,10 +5,11 @@ ...@@ -5,10 +5,11 @@
</template> </template>
<script setup> <script setup>
import { onMounted, nextTick } from 'vue'; import { onMounted, onBeforeUnmount } from 'vue';
import setChart from '@/utils/setChart'; import setChart from '@/utils/setChart';
import getGraphChart from './graphChart'; import getGraphChart from './graphChart';
const emits = defineEmits(["handleClickNode"])
const props = defineProps({ const props = defineProps({
nodes: { nodes: {
type: Array, type: Array,
...@@ -32,11 +33,17 @@ const props = defineProps({ ...@@ -32,11 +33,17 @@ const props = defineProps({
} }
}) })
let chart = null
onMounted(() => { onMounted(() => {
const graph = getGraphChart(props.nodes, props.links, props.layoutType) const graph = getGraphChart(props.nodes, props.links, props.layoutType)
setChart(graph, 'graph') chart = setChart(graph, 'graph')
chart.on("click", (event) => { emits("handleClickNode", event) })
}) })
onBeforeUnmount(() => {
chart.off("click")
chart.dispose()
})
</script> </script>
......
...@@ -24,17 +24,19 @@ const props = defineProps({ ...@@ -24,17 +24,19 @@ const props = defineProps({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tip-wrapper{ .tip-wrapper {
width: 100%; width: 100%;
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 22px; height: 22px;
.icon{
.icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
img{
img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
......
...@@ -96,16 +96,16 @@ const homeTitleList = ref([ ...@@ -96,16 +96,16 @@ const homeTitleList = ref([
path: "/ZMOverView", path: "/ZMOverView",
disabled: false disabled: false
}, },
{ // {
name: "主要国家科技动向感知", // name: "主要国家科技动向感知",
path: "", // path: "",
disabled: true // disabled: true
}, // },
{ // {
name: "主要国家竞争科技安全", // name: "主要国家竞争科技安全",
path: "", // path: "",
disabled: true // disabled: true
} // }
]); ]);
const homeActiveTitleIndex = ref(0); const homeActiveTitleIndex = ref(0);
......
...@@ -3,6 +3,8 @@ const thinkTank = () => import('@/views/thinkTank/index.vue') ...@@ -3,6 +3,8 @@ const thinkTank = () => import('@/views/thinkTank/index.vue')
const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue') const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue')
const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue') const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue')
const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue') const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue')
const allThinkTank= () => import('@/views/thinkTank/allThinkTank/index.vue')
const MultiThinkTankViewAnalysis= () => import('@/views/thinkTank/MultiThinkTankViewAnalysis/index.vue')
const thinktankRoutes = [ const thinktankRoutes = [
// 智库系统的主要路由 // 智库系统的主要路由
...@@ -36,9 +38,19 @@ const thinktankRoutes = [ ...@@ -36,9 +38,19 @@ const thinktankRoutes = [
path: "/thinkTank/reportOriginal/:id", path: "/thinkTank/reportOriginal/:id",
name: "ReportOriginal", name: "ReportOriginal",
component: ReportOriginal, component: ReportOriginal,
// meta: {
// title: "报告原文" },
// } {
path: "/thinkTank/allThinkTank",
name: "allThinkTank",
component: allThinkTank,
},
{
path: "/thinkTank/MultiThinkTankViewAnalysis/:id",
name: "MultiThinkTankViewAnalysis",
component: MultiThinkTankViewAnalysis,
}, },
] ]
......
...@@ -9,6 +9,10 @@ const setChart = (option, chartId) => { ...@@ -9,6 +9,10 @@ const setChart = (option, chartId) => {
chartDom.removeAttribute("_echarts_instance_"); chartDom.removeAttribute("_echarts_instance_");
let chart = echarts.init(chartDom); let chart = echarts.init(chartDom);
chart.setOption(option); chart.setOption(option);
// 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致
setTimeout(() => {
chart.resize();
}, 0);
return chart; return chart;
}; };
......
...@@ -5,9 +5,8 @@ ...@@ -5,9 +5,8 @@
<div class="home-content"> <div class="home-content">
<div class="home-content-header"> <div class="home-content-header">
<SearchContainer style="margin-bottom: 0; height: fit-content" v-if="containerRef" <SearchContainer style="margin-bottom: 0; height: fit-content" v-if="containerRef" placeholder="搜索科技法案"
placeholder="搜索科技法案" :containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true" :containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true" defaultBillSearchType="federal" />
defaultBillSearchType="federal" />
</div> </div>
<div class="committee-cards-section"> <div class="committee-cards-section">
...@@ -55,8 +54,8 @@ ...@@ -55,8 +54,8 @@
</div> </div>
</div> </div>
<div class="box1-main" style="display: block"> <div class="box1-main" style="display: block">
<el-carousel ref="carouselRef" height="354px" :autoplay="true" :interval="3000" <el-carousel ref="carouselRef" height="354px" :autoplay="true" :interval="3000" arrow="never"
arrow="never" indicator-position="none" @change="handleCarouselChange"> indicator-position="none" @change="handleCarouselChange">
<el-carousel-item v-for="(bill, billIndex) in hotBillList" :key="billIndex"> <el-carousel-item v-for="(bill, billIndex) in hotBillList" :key="billIndex">
<div class="carousel-content" style="display: flex; height: 100%"> <div class="carousel-content" style="display: flex; height: 100%">
<div class="box1-main-left"> <div class="box1-main-left">
...@@ -64,8 +63,7 @@ ...@@ -64,8 +63,7 @@
{{ bill.billName }} {{ bill.billName }}
</div> </div>
<div class="box1-main-left-info"> <div class="box1-main-left-info">
<AreaTag v-for="(item, index) in bill.hylyList" :key="index" <AreaTag v-for="(item, index) in bill.hylyList" :key="index" :tagName="item.industryName">
:tagName="item.industryName">
</AreaTag> </AreaTag>
</div> </div>
<div class="box1-main-left-info1"> <div class="box1-main-left-info1">
...@@ -83,18 +81,15 @@ ...@@ -83,18 +81,15 @@
</div> </div>
</div> </div>
<div class="box1-main-left-info2"> <div class="box1-main-left-info2">
<div class="info2-item" v-for="(item, index) in bill.dyqkList" <div class="info2-item" v-for="(item, index) in bill.dyqkList" :key="index">
:key="index"> <div class="time-line" v-if="index !== bill.dyqkList.length - 1"></div>
<div class="time-line"
v-if="index !== bill.dyqkList.length - 1"></div>
<div class="item-icon"> <div class="item-icon">
<img src="./assets/images/info2-icon.png" alt="" /> <img src="./assets/images/info2-icon.png" alt="" />
</div> </div>
<div class="item-time" :class="{ itemTimeActive: index === 0 }"> <div class="item-time" :class="{ itemTimeActive: index === 0 }">
{{ item.actionDate }} {{ item.actionDate }}
</div> </div>
<div class="item-title" <div class="item-title" :class="{ itemTitleActive: index === 0 }">
:class="{ itemTitleActive: index === 0 }">
{{ item.actionContentCn }} {{ item.actionContentCn }}
</div> </div>
</div> </div>
...@@ -121,9 +116,8 @@ ...@@ -121,9 +116,8 @@
</el-carousel> </el-carousel>
</div> </div>
</overviewMainBox> </overviewMainBox>
<RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" <RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" @item-click="handleClickToDetailO"
@item-click="handleClickToDetailO" riskLevel="signalLevel" postDate="signalTime" riskLevel="signalLevel" postDate="signalTime" name="signalTitle" />
name="signalTitle" />
</div> </div>
<DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader> <DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div class="box1"> <div class="box1">
<AnalysisBox title="相关政令" :showAllBtn="false"> <AnalysisBox title="相关政令" :showAllBtn="false">
<div class="box1-main"> <div class="box1-main">
<el-empty v-if="!siderList?.length" style="padding-top: 40%;" description="暂无数据" :image-size="100" /> <el-empty v-if="!siderList?.length" style="padding: 60px 0;" description="暂无数据" :image-size="100" />
<el-scrollbar height="100%" always> <el-scrollbar height="100%" always>
<div class="left-item" :class="{ 'item-active': false }" v-for="(item, index) in siderList" :key="index" @click="handleClickDecree(item)"> <div class="left-item" :class="{ 'item-active': false }" v-for="(item, index) in siderList" :key="index" @click="handleClickDecree(item)">
<div class="item-head"> <div class="item-head">
...@@ -18,9 +18,9 @@ ...@@ -18,9 +18,9 @@
</div> </div>
<div class="box2"> <div class="box2">
<AnalysisBox title="政令关系挖掘" :showAllBtn="false"> <AnalysisBox title="政令关系挖掘" :showAllBtn="false">
<el-empty v-if="!siderList?.length" style="padding-top: 20%;" description="暂无数据" :image-size="100" /> <el-empty v-if="!siderList?.length" style="padding: 60px 0;" description="暂无数据" :image-size="100" />
<div class="box2-main"> <div class="box2-main" v-if="graphData.nodes?.length">
<div ref="containerRef" class="graph-container"></div> <GraphChart :nodes="graphData.nodes" :links="graphData.links" layoutType="force" @handleClickNode="handleClickNode" />
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -48,12 +48,13 @@ ...@@ -48,12 +48,13 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount } from "vue"; import { ref, onMounted, onBeforeUnmount, reactive } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import router from "@/router"; import router from "@/router";
import * as G6 from '@antv/g6'; import * as G6 from '@antv/g6';
import { getDecreeRelatedOrder } from "@/api/decree/deepdig"; import { getDecreeRelatedOrder } from "@/api/decree/deepdig";
import { getDecreeSummary } from "@/api/decree/introduction"; import { getDecreeSummary } from "@/api/decree/introduction";
import GraphChart from "@/components/base/GraphChart/index.vue";
import icon1628 from "./assets/icons/icon1628.png"; import icon1628 from "./assets/icons/icon1628.png";
import icon1629 from "./assets/icons/icon1629.png"; import icon1629 from "./assets/icons/icon1629.png";
...@@ -64,7 +65,7 @@ const route = useRoute(); ...@@ -64,7 +65,7 @@ const route = useRoute();
const dialogVisible = ref(false); const dialogVisible = ref(false);
// 基本信息 // 基本信息
const mainInfo = ref({}); const mainInfo = ref({ label: "", time: "", id: "" });
const nodeInfo = ref({}); const nodeInfo = ref({});
const onDecreeSummaryData = async () => { const onDecreeSummaryData = async () => {
try { try {
...@@ -74,10 +75,9 @@ const onDecreeSummaryData = async () => { ...@@ -74,10 +75,9 @@ const onDecreeSummaryData = async () => {
mainInfo.value.label = res.data.name; mainInfo.value.label = res.data.name;
mainInfo.value.time = res.data.postDate; mainInfo.value.time = res.data.postDate;
mainInfo.value.id = route.query.id; mainInfo.value.id = route.query.id;
mainInfo.value.isCenter = true
} }
} catch (error) { } catch (error) {
mainInfo.value = {}; mainInfo.value = { label: "", time: "", id: "" };
console.log("获取基本信息数据失败:", error); console.log("获取基本信息数据失败:", error);
} }
}; };
...@@ -109,145 +109,64 @@ const handleGetRelateOrder = async () => { ...@@ -109,145 +109,64 @@ const handleGetRelateOrder = async () => {
}; };
// 政令关系挖掘 // 政令关系挖掘
const containerRef = ref(); const graphData = reactive({
let graphInstance = null; nodes: [],
// 文本插入换行符 links: [],
const onWordWrap = (word, num) => { })
const list = word.split(''); // 节点点击处理
let label = ""; const handleClickNode = ({data}) => {
for (let i = 0; i < list.length; i++) { if (data.target) {
if (i % num === 0 && i !== 0) { let node = siderList.value.find(item => item.id==data.target)
label += "\n"; if (node) handleClickSider(node)
} else {
let node = siderList.value.find(item => item.id==data.id)
if (node) handleClickDecree(node)
} }
label += list[i]; }
const initGraphChart = () => {
Promise.all([onDecreeSummaryData(), handleGetRelateOrder()]).then(() => {
if (mainInfo.value.id && siderList.value.length) {
graphData.links = siderList.value.map(onFormatLink)
graphData.nodes = siderList.value.map(onFormatNode)
graphData.nodes.unshift(onFormatNode(mainInfo.value))
} }
return label; })
} }
const onFormatNode = (item) => { const onFormatLink = (item, index) => {
let isCenter = item.isCenter || false
return { return {
id: item.id+'', label:onWordWrap(item.label, 15), isCenter, id: `link-${index+1}`,
img: isCenter ? icon1628 : icon1629, source: route.query.id, target: item.id+'',
clipCfg: { r: isCenter ? 40 : 30 }, label: { show: true, color: "#055fc2", backgroundColor: "#eef7ff", borderWidth: 0, offset: [0, 15], formatter: item.relation },
labelCfg: { lineStyle: { color: '#B9DCFF', type: "solid", opacity: 1 }
style: {
fill: isCenter ? "#1459BB" : "#333333",
fontSize: isCenter ? 13 : 13,
fontWeight: isCenter ? "bold" : "normal"
}
}
} }
} }
const onFormatEdge = (item, index) => { const onFormatNode = (item) => {
let leader = item.id == mainInfo.value.id;
return { return {
id: `edge-${index+1}`, id: item.id+'',
target: item.id+'', name: onWordWrap(item.label, 8),
source: route.query.id, label: {
// label: ["", "相似", "继承", "冲突"][1], show: true,
label: item.relation, color: leader ? "#055fc2" : "#3b414b",
style: { fontSize: leader ? 16 : 14,
stroke: ["", "#B9DCFF", "#87E8DE", "#FFCCC7"][1], fontWeight: leader ? 700 : 400,
fontFamily: 'Source Han Sans CN',
}, },
labelCfg: { symbolSize: leader ? 60 : 40,
style: { symbol: `image://${leader ? icon1628 : icon1629}`
fill: ["", "#055FC2", "#13A8A8", "#CE4F51"][1],
background: {
fill: ["", "#E7F3FF", "#E6FFFB", "#FFE0E0"][1],
}
}
}
} }
} }
const initChart = () => { // 文本插入换行符
let edgeList = siderList.value.map(onFormatEdge) const onWordWrap = (word, num) => {
let nodeList = siderList.value.map(onFormatNode) const list = word.split('');
nodeList.unshift(onFormatNode(mainInfo.value)) let label = "";
console.log(nodeList) for (let i = 0; i < list.length; i++) {
if (i % num === 0 && i !== 0) {
const width = containerRef.value.offsetWidth || 800 label += "\n";
const height = containerRef.value.offsetHeight || 600
const centerX = width / 2
const centerY = height / 2
const radius = Math.min(width, height) / 2 - 120
const otherNodes = nodeList.filter(n => !n.isCenter)
const nodeCount = otherNodes.length
otherNodes.forEach((node, index) => {
const angle = (2 * Math.PI * index) / nodeCount - Math.PI / 2
node.x = centerX + radius * Math.cos(angle)
node.y = centerY + radius * Math.sin(angle)
})
const centerNode = nodeList.find(n => n.isCenter)
if (centerNode) {
centerNode.x = centerX
centerNode.y = centerY
centerNode.fx = centerX
centerNode.fy = centerY
}
graphInstance = new G6.Graph({
container: containerRef.value,
width,
height,
fitView: false,
fitCenter: false,
animate: true,
animateCfg: {
duration: 300,
easing: 'easeLinear'
},
minZoom: 0.1,
maxZoom: 10,
modes: {
default: [ 'drag-canvas', 'zoom-canvas', 'drag-node' ]
},
defaultNode: {
type: 'image',
size: 50,
style: { cursor: "pointer" },
clipCfg: { show: true, type: 'circle' },
labelCfg: {
position: "bottom", offset: 12,
style: {
fill: '#333',
fontSize: 11,
fontFamily: 'Microsoft YaHei',
textAlign: 'center',
background: {
fill: 'rgba(255, 255, 255, 0.95)',
padding: [4, 6, 4, 6],
radius: 4
}
}
}
},
defaultEdge: {
type: "line",
style: { lineWidth: 1, endArrow: true },
labelCfg: {
autoRotate: true,
style: {
cursor: "pointer",
fontSize: 12,
fontFamily: 'Microsoft YaHei',
background: { padding: [4, 4, 4, 4] }
}
} }
label += list[i];
} }
}) return label;
// 节点点击处理
graphInstance.on('node:click', (evt) => {
let node = siderList.value.find(item => item.id==evt.item._cfg.model.id)
if (node) handleClickDecree(node)
});
graphInstance.on('edge:click', (evt) => {
let node = siderList.value.find(item => item.id==evt.item._cfg.model.target)
if (node) handleClickSider(node)
});
graphInstance.data({nodes: nodeList, edges: edgeList})
graphInstance.render()
} }
const handleClickDecree = decree => { const handleClickDecree = decree => {
...@@ -293,9 +212,9 @@ const onRelationChart = () => { ...@@ -293,9 +212,9 @@ const onRelationChart = () => {
}, },
labelCfg: { labelCfg: {
style: { style: {
fill: ["", "#055FC2", "#13A8A8", "#CE4F51"][1], fill: ["", "#055fc2", "#13A8A8", "#CE4F51"][1],
background: { background: {
fill: ["", "#E7F3FF", "#E6FFFB", "#FFE0E0"][1], fill: ["", "#eef7ff", "#E6FFFB", "#FFE0E0"][1],
} }
} }
} }
...@@ -363,13 +282,10 @@ const onRelationChart = () => { ...@@ -363,13 +282,10 @@ const onRelationChart = () => {
} }
onMounted(() => { onMounted(() => {
Promise.all([onDecreeSummaryData(), handleGetRelateOrder()]).then(() => { initGraphChart()
if (mainInfo.value.id && siderList.value.length) initChart()
})
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
graphInstance?.destroy()
graph?.destroy() graph?.destroy()
}) })
</script> </script>
...@@ -462,10 +378,6 @@ onBeforeUnmount(() => { ...@@ -462,10 +378,6 @@ onBeforeUnmount(() => {
.box2-main { .box2-main {
height: 100%; height: 100%;
padding: 10px; padding: 10px;
.graph-container {
width: 100%;
height: 600px;
}
} }
} }
} }
......
...@@ -79,7 +79,7 @@ onMounted(() => { ...@@ -79,7 +79,7 @@ onMounted(() => {
.left { .left {
position: absolute; position: absolute;
left: -160px; left: -160px;
top: 0px; top: 25px;
width: 160px; width: 160px;
padding: 0px 16px; padding: 0px 16px;
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<WarnningPane :warnningLevel="riskInfo.riskLevel" :warnningContent="riskInfo.content" /> <WarnningPane :warnningLevel="riskInfo.riskLevel" :warnningContent="riskInfo.content" />
</div> </div>
<div class="introduction-wrap"> <div class="introduction-wrap">
<div class="left"> <div class="page-left">
<div class="box1"> <div class="box1">
<AnalysisBox title="基本信息" :showAllBtn="false"> <AnalysisBox title="基本信息" :showAllBtn="false">
<div class="box1-main"> <div class="box1-main">
...@@ -23,14 +23,10 @@ ...@@ -23,14 +23,10 @@
</div> </div>
<div class="item"> <div class="item">
<div class="item-left">{{ "英文全称:" }}</div> <div class="item-left">{{ "英文全称:" }}</div>
<div class="item-right text" v-if="basicInfo.eName?.length < 60"> <div class="item-right text" v-if="basicInfo.eName?.length < 60"> {{ basicInfo.eName }} </div>
{{ basicInfo.eName }}
</div>
<el-popover v-else effect="dark" :width="500" :content="basicInfo.eName" placement="top-start"> <el-popover v-else effect="dark" :width="500" :content="basicInfo.eName" placement="top-start">
<template #reference> <template #reference>
<div class="item-right text"> <div class="item-right text"> {{ basicInfo.eName }} </div>
{{ basicInfo.eName }}
</div>
</template> </template>
</el-popover> </el-popover>
</div> </div>
...@@ -66,7 +62,19 @@ ...@@ -66,7 +62,19 @@
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="box2"> <div>
<AnalysisBox title="报告内容摘要" :showAllBtn="false">
<el-empty v-if="false" style="margin: 20px 0;" description="暂无数据" :image-size="100" />
<div class="box5-main">
<AiSummary>
<template #summary-content>
<div class="box5-text">{{ box1Data }}</div>
</template>
</AiSummary>
</div>
</AnalysisBox>
</div>
<div>
<AnalysisBox title="相关事件" :showAllBtn="false"> <AnalysisBox title="相关事件" :showAllBtn="false">
<div class="box2-main"> <div class="box2-main">
<el-empty v-if="!relatedData.length" description="暂无数据" :image-size="100" /> <el-empty v-if="!relatedData.length" description="暂无数据" :image-size="100" />
...@@ -80,18 +88,21 @@ ...@@ -80,18 +88,21 @@
<span class="meta">{{ item.sjsj }} · {{ item.source }}</span> <span class="meta">{{ item.sjsj }} · {{ item.source }}</span>
</div> </div>
<div class="content">{{ item.sjnr }}</div> <div class="content">{{ item.sjnr }}</div>
<!-- <el-popover effect="dark" :width="1000" :content="item.content" placement="top-start">
<template #reference>
<div class="content">{{ item.content }}</div>
</template>
</el-popover> -->
</div> </div>
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
</div> </div>
<div class="right"> <div class="page-right">
<div class="box4">
<AnalysisBox title="政令关键词云" :showAllBtn="false">
<div class="box4-main">
<el-empty v-if="!wordCloudData.length" description="暂无数据" :image-size="100" />
<WordCloudChart v-if="wordCloudData.length" :data="wordCloudData" width="100%" height="100%" />
</div>
</AnalysisBox>
</div>
<div class="box3"> <div class="box3">
<AnalysisBox title="发布机构" :showAllBtn="false"> <AnalysisBox title="发布机构" :showAllBtn="false">
<div class="box3-top"> <div class="box3-top">
...@@ -151,12 +162,16 @@ import { ref, onMounted } from "vue"; ...@@ -151,12 +162,16 @@ import { ref, onMounted } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import router from "@/router"; import router from "@/router";
import WarnningPane from '@/components/base/WarningPane/index.vue' import WarnningPane from '@/components/base/WarningPane/index.vue'
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import { import {
getDecreeBasicInfo, getDecreeBasicInfo,
getDecreeRiskSignal, getDecreeRiskSignal,
getDecreeIssueOrganization getDecreeIssueOrganization,
getKeyWordUp,
getOverview,
} from "@/api/decree/introduction"; } from "@/api/decree/introduction";
import { getDecreeRelatedEvent } from "@/api/decree/background"; import { getDecreeRelatedEvent } from "@/api/decree/background";
import AiSummary from '@/components/base/Ai/AiSummary/index.vue'
import DefaultIcon1 from "@/assets/icons/default-icon1.png"; import DefaultIcon1 from "@/assets/icons/default-icon1.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png"; import DefaultIcon2 from "@/assets/icons/default-icon2.png";
...@@ -216,6 +231,30 @@ const handleGetBasicInfo = async () => { ...@@ -216,6 +231,30 @@ const handleGetBasicInfo = async () => {
}; };
handleGetBasicInfo(); handleGetBasicInfo();
// 政令关键词云
const wordCloudData = ref([])
const onKeyWordUp = async () => {
try {
const res = await getKeyWordUp();
console.log("政令关键词云", res);
wordCloudData.value = res.data.slice(0, 10).map(item => ({name: item.name, value: item.count}));
} catch (error) {
console.error("获取政令关键词云数据失败", error);
}
};
// 报告内容摘要
const box1Data = ref('');
const onOverview = async () => {
try {
const res = await getOverview({id: decreeId.value});
console.log("报告内容摘要", res);
box1Data.value = res.data;
} catch (error) {
console.error("获取报告内容摘要数据失败", error);
}
};
// 相关事件 // 相关事件
const relatedData = ref([]); const relatedData = ref([]);
const handleGetRelateEvents = async () => { const handleGetRelateEvents = async () => {
...@@ -304,6 +343,8 @@ const handleClickUser = item => { ...@@ -304,6 +343,8 @@ const handleClickUser = item => {
}; };
onMounted(() => { onMounted(() => {
onOverview()
onKeyWordUp()
onRiskSignalData() onRiskSignalData()
handleGetRelateEvents(); handleGetRelateEvents();
handleGetOrgnization(); handleGetOrgnization();
...@@ -322,15 +363,15 @@ onMounted(() => { ...@@ -322,15 +363,15 @@ onMounted(() => {
width: 100%; width: 100%;
display: flex; display: flex;
.left { .page-left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: auto; flex: auto;
margin-right: 16px; margin-right: 16px;
gap: 16px;
.box1 { .box1 {
height: 414px; height: 414px;
.box1-main { .box1-main {
display: flex; display: flex;
padding: 0 24px; padding: 0 24px;
...@@ -427,18 +468,20 @@ onMounted(() => { ...@@ -427,18 +468,20 @@ onMounted(() => {
} }
} }
.box2 { .box5-main {
margin-top: 16px; padding: 0px 22px 22px;
height: 420px; .box5-text {
padding-bottom: 20px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
min-height: 300px;
}
}
.box2-main { .box2-main {
margin-top: 3px; padding: 0 22px;
margin-left: 31px;
height: 100%;
width: 1004px;
overflow: hidden;
overflow-y: auto;
.box2-item { .box2-item {
display: flex; display: flex;
height: 60px; height: 60px;
...@@ -446,18 +489,16 @@ onMounted(() => { ...@@ -446,18 +489,16 @@ onMounted(() => {
margin-bottom: 2px; margin-bottom: 2px;
border-bottom: 1px solid rgba(240, 242, 244, 1); border-bottom: 1px solid rgba(240, 242, 244, 1);
margin-top: 10px; margin-top: 10px;
.item-left { .item-left {
width: 64px; width: 64px;
height: 48px; height: 48px;
border-radius: 2px; border-radius: 2px;
font-size: 0px;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
.item-center { .item-center {
flex: auto; flex: auto;
width: 20px; width: 20px;
...@@ -510,12 +551,10 @@ onMounted(() => { ...@@ -510,12 +551,10 @@ onMounted(() => {
} }
} }
} }
.box2-footer { .box2-footer {
margin: 20px 22px; margin: 20px 22px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
.box2-footer-left { .box2-footer-left {
height: 20px; height: 20px;
color: rgba(132, 136, 142, 1); color: rgba(132, 136, 142, 1);
...@@ -526,20 +565,26 @@ onMounted(() => { ...@@ -526,20 +565,26 @@ onMounted(() => {
} }
} }
} }
}
.right { .page-right {
display: flex;
flex-direction: column;
width: 520px; width: 520px;
flex: none;
gap: 16px;
.box3 { .box4 {
width: 520px; height: 414px;
.box4-main {
height: 100%; height: 100%;
}
}
.box3-top { .box3-top {
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
padding: 0 22px;
.box3-top-top { .box3-top-top {
width: 473px;
height: 88px; height: 88px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1); border: 1px solid rgba(234, 236, 238, 1);
...@@ -549,40 +594,13 @@ onMounted(() => { ...@@ -549,40 +594,13 @@ onMounted(() => {
align-items: center; align-items: center;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
padding: 0 16px;
cursor: pointer; cursor: pointer;
.more {
position: absolute;
right: 17px;
top: 17px;
display: flex;
gap: 3px;
.text {
height: 16px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 16px;
}
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
}
.left { .left {
width: 64px; width: 64px;
height: 64px; height: 64px;
margin-left: 17px; font-size: 0px;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -590,9 +608,9 @@ onMounted(() => { ...@@ -590,9 +608,9 @@ onMounted(() => {
} }
.right { .right {
width: 370px; width: 20px;
flex: auto;
margin-left: 15px; margin-left: 15px;
.name { .name {
height: 26px; height: 26px;
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
...@@ -601,7 +619,6 @@ onMounted(() => { ...@@ -601,7 +619,6 @@ onMounted(() => {
font-weight: 700; font-weight: 700;
line-height: 26px; line-height: 26px;
} }
.ename { .ename {
margin-top: 6px; margin-top: 6px;
height: 24px; height: 24px;
...@@ -613,9 +630,7 @@ onMounted(() => { ...@@ -613,9 +630,7 @@ onMounted(() => {
} }
} }
} }
.box3-top-bottom { .box3-top-bottom {
width: 473px;
height: 193px; height: 193px;
margin: 0 auto; margin: 0 auto;
...@@ -717,8 +732,8 @@ onMounted(() => { ...@@ -717,8 +732,8 @@ onMounted(() => {
} }
} }
} }
.box3-bottom { .box3-bottom {
padding-right: 22px;
.box3-bottom-header { .box3-bottom-header {
height: 59px; height: 59px;
display: flex; display: flex;
...@@ -748,11 +763,6 @@ onMounted(() => { ...@@ -748,11 +763,6 @@ onMounted(() => {
} }
.box3-bottom-main { .box3-bottom-main {
width: 510px;
height: 440px;
overflow: hidden;
overflow-y: auto;
:deep(.el-timeline) { :deep(.el-timeline) {
padding: 8px 0px 0px 25px !important; padding: 8px 0px 0px 25px !important;
} }
...@@ -780,12 +790,6 @@ onMounted(() => { ...@@ -780,12 +790,6 @@ onMounted(() => {
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 26px; line-height: 26px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
}
} }
} }
} }
......
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论