提交 8fb13448 authored 作者: 张伊明's avatar 张伊明

fix 修复bug#20 #21

上级 f9cc4c39
import request from "@/api/request.js"; import request from "@/api/request.js";
// 根据行业领域id获取公司列表 // 获取实体列表(按行业/公司名筛选)
/** /**
* @param {id} * @param {Object} params
* @param {string} [params.id] - 行业领域id(全部领域不传)
* @param {string} [params.companyName] - 公司名称(搜索框为空不传)
*/ */
export function getCompanyList(params) { export function getCompanyList(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/billImpactAnalysis/industry/company/${params.id}`, url: `/api/billImpactAnalysis/industry/company`,
params, params,
}) })
} }
......
...@@ -32,19 +32,28 @@ ...@@ -32,19 +32,28 @@
</div> </div>
</div> --> </div> -->
<AnalysisBox title="典型阶段耗时"> <AnalysisBox title="典型阶段耗时">
<div class="box1-main" :class="{ 'box1-main--full': !timeFooterText }"> <div class="analysis-ai-wrapper analysis-ai-wrapper--box1">
<div class="box1-main-center" id="chart1"></div> <div class="box1-main" :class="{ 'box1-main--full': !timeFooterText }">
<div v-if="timeFooterText" class="box1-main-footer"> <div class="box1-main-center" id="chart1"></div>
<div class="box-footer-left"> <div v-if="timeFooterText" class="box1-main-footer">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" /> <div class="box-footer-left">
</div> <img src="@/assets/icons/box-footer-left-icon.png" alt="" />
<div class="box-footer-center"> </div>
{{ timeFooterText }} <div class="box-footer-center">
</div> {{ timeFooterText }}
<div class="box-footer-right"> </div>
<img src="../assets/icons/arrow-right.png" alt="" /> <div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
</div>
</div> </div>
</div> </div>
<div v-if="!aiPaneVisible.box1" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box1')" />
</div>
<div v-if="aiPaneVisible.box1" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box1')">
<AiPane :aiContent="overviewAiContent.box1" />
</div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -80,19 +89,28 @@ ...@@ -80,19 +89,28 @@
</div> </div>
</div> --> </div> -->
<AnalysisBox title="修正案次数分析"> <AnalysisBox title="修正案次数分析">
<div class="box2-main" :class="{ 'box2-main--full': !amendFooterText }"> <div class="analysis-ai-wrapper analysis-ai-wrapper--box2">
<div class="box2-main-center" id="chart2"></div> <div class="box2-main" :class="{ 'box2-main--full': !amendFooterText }">
<div v-if="amendFooterText" class="box2-main-footer"> <div class="box2-main-center" id="chart2"></div>
<div class="box-footer-left"> <div v-if="amendFooterText" class="box2-main-footer">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" /> <div class="box-footer-left">
</div> <img src="@/assets/icons/box-footer-left-icon.png" alt="" />
<div class="box-footer-center"> </div>
{{ amendFooterText }} <div class="box-footer-center">
</div> {{ amendFooterText }}
<div class="box-footer-right"> </div>
<img src="../assets/icons/arrow-right.png" alt="" /> <div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
</div>
</div> </div>
</div> </div>
<div v-if="!aiPaneVisible.box2" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box2')" />
</div>
<div v-if="aiPaneVisible.box2" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box2')">
<AiPane :aiContent="overviewAiContent.box2" />
</div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -366,7 +384,8 @@ ...@@ -366,7 +384,8 @@
</div> </div>
</div> --> </div> -->
<AnalysisBox title="投票分析"> <AnalysisBox title="投票分析">
<div class="vote-legend"> <div class="analysis-ai-wrapper analysis-ai-wrapper--box3">
<div class="vote-legend">
<div class="vote-legend-item"> <div class="vote-legend-item">
<span class="vote-legend-dot agree"></span> <span class="vote-legend-dot agree"></span>
<span>赞成票</span> <span>赞成票</span>
...@@ -375,7 +394,7 @@ ...@@ -375,7 +394,7 @@
<span class="vote-legend-dot against"></span> <span class="vote-legend-dot against"></span>
<span>反对票</span> <span>反对票</span>
</div> </div>
</div> </div>
<div class="box3-main" :class="{ 'box3-main--full': !voteFooterText }"> <div class="box3-main" :class="{ 'box3-main--full': !voteFooterText }">
<div class="box3-main-center"> <div class="box3-main-center">
<div class="box3-main-center-header"> <div class="box3-main-center-header">
...@@ -678,7 +697,15 @@ ...@@ -678,7 +697,15 @@
<img src="../assets/icons/arrow-right.png" alt="" /> <img src="../assets/icons/arrow-right.png" alt="" />
</div> </div>
</div> </div>
<div v-if="!aiPaneVisible.box3" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box3')" />
</div>
<div v-if="aiPaneVisible.box3" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box3')">
<AiPane :aiContent="overviewAiContent.box3" />
</div>
</div> </div>
</div>
</AnalysisBox> </AnalysisBox>
</div> </div>
</div> </div>
...@@ -690,6 +717,10 @@ import { ref, onMounted } from "vue"; ...@@ -690,6 +717,10 @@ import { ref, onMounted } from "vue";
import { getBillTimeAnalyze, getBillAmeAnalyzeCount, getBillTp } from "@/api/deepdig"; import { getBillTimeAnalyze, getBillAmeAnalyzeCount, getBillTp } from "@/api/deepdig";
import getBoxPlotChcart from "./utils/boxplot"; import getBoxPlotChcart from "./utils/boxplot";
import * as echarts from "echarts"; import * as echarts from "echarts";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import TipTab from "@/components/base/TipTab/index.vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import icon1 from "./assets/images/icon1.png"; import icon1 from "./assets/images/icon1.png";
import icon2 from "./assets/images/icon2.png"; import icon2 from "./assets/images/icon2.png";
...@@ -895,6 +926,31 @@ const timeFooterText = ref(""); ...@@ -895,6 +926,31 @@ const timeFooterText = ref("");
const amendFooterText = ref(""); const amendFooterText = ref("");
const voteFooterText = ref(""); const voteFooterText = ref("");
// AI面板显示状态(box1=典型阶段耗时,box2=修正案次数分析,box3=投票分析)
const aiPaneVisible = ref({
box1: false,
box2: false,
box3: false
});
const overviewAiContent = ref({
box1: "智能总结生成中...",
box2: "智能总结生成中...",
box3: "智能总结生成中..."
});
const aiPaneFetched = ref({
box1: false,
box2: false,
box3: false
});
const aiPaneLoading = ref({
box1: false,
box2: false,
box3: false
});
// 绘制echarts图表 // 绘制echarts图表
const setChart = (option, chartId) => { const setChart = (option, chartId) => {
let chartDom = document.getElementById(chartId); let chartDom = document.getElementById(chartId);
...@@ -991,6 +1047,96 @@ const handleGetBillVoteAnalyze = async () => { ...@@ -991,6 +1047,96 @@ const handleGetBillVoteAnalyze = async () => {
} }
}; };
const buildAiChartPayload = key => {
if (key === "box1") {
return {
type: "箱线图",
name: "典型阶段耗时",
data: {
categories: Array.isArray(chartData1.value?.dataX) ? chartData1.value.dataX : [],
samples: Array.isArray(chartData1.value?.dataY) ? chartData1.value.dataY : []
}
};
}
if (key === "box2") {
return {
type: "箱线图",
name: "修正案次数分析",
data: {
categories: Array.isArray(chartData2.value?.dataX) ? chartData2.value.dataX : [],
samples: Array.isArray(chartData2.value?.dataY) ? chartData2.value.dataY : []
}
};
}
if (key === "box3") {
return {
type: "投票分析",
name: "投票分析",
data: Array.isArray(voteAnalysisList.value) ? voteAnalysisList.value : []
};
}
return { type: "", name: "", data: [] };
};
const requestAiPaneContent = async key => {
if (!key || aiPaneLoading.value[key] || aiPaneFetched.value[key]) return;
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: true };
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成中..." };
try {
const payload = buildAiChartPayload(key);
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
overviewAiContent.value = {
...overviewAiContent.value,
[key]: base + chunk
};
}
}
);
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
const interpretation = first?.解读 || first?.["解读"];
if (interpretation) {
overviewAiContent.value = {
...overviewAiContent.value,
[key]: interpretation
};
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: false };
}
};
const handleShowAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: true
};
requestAiPaneContent(key);
};
const handleHideAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: false
};
};
onMounted(async () => { onMounted(async () => {
await handleGetBillTimeAnalyze(); await handleGetBillTimeAnalyze();
await handleGetBillAmeAnalyzeCount(); await handleGetBillAmeAnalyzeCount();
...@@ -1916,4 +2062,38 @@ onMounted(async () => { ...@@ -1916,4 +2062,38 @@ onMounted(async () => {
width: 200px; width: 200px;
margin-left: 10px; margin-left: 10px;
} }
.analysis-ai-wrapper {
position: relative;
height: 100%;
}
.analysis-ai-tip-row {
position: absolute;
left: 0;
bottom: 15px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.analysis-ai-tip-action {
position: absolute;
right: 0px;
}
.analysis-ai-pane {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
z-index: 5;
pointer-events: none;
:deep(.ai-pane-wrapper) {
pointer-events: auto;
}
}
</style> </style>
const resolveCssVarColor = (varName, fallback) => {
try {
if (typeof window === 'undefined' || typeof document === 'undefined') return fallback
const value = window.getComputedStyle(document.documentElement).getPropertyValue(varName)
const trimmed = value ? value.trim() : ''
return trimmed || fallback
} catch (e) {
return fallback
}
}
const getBoxPlotChcart = (data, unit, labelConfig = {}) => { const getBoxPlotChcart = (data, unit, labelConfig = {}) => {
const primary2 = resolveCssVarColor('--color-primary-2', '#F6FAFF')
const labels = { const labels = {
max: labelConfig.max || '最大耗时', max: labelConfig.max || '最大耗时',
q3: labelConfig.q3 || '平均耗时大', q3: labelConfig.q3 || '平均耗时大',
...@@ -16,6 +29,19 @@ const getBoxPlotChcart = (data, unit, labelConfig = {}) => { ...@@ -16,6 +29,19 @@ const getBoxPlotChcart = (data, unit, labelConfig = {}) => {
// left: 'center' // left: 'center'
// } // }
// ], // ],
graphic: [
{
type: 'text',
// 左上角只标注一次单位(y轴刻度不再逐行带单位)
left: '5%',
top: '0%',
style: {
text: unit,
fill: 'rgba(95, 101, 108, 1)',
font: '14px Microsoft YaHei'
}
}
],
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
axisPointer: { axisPointer: {
...@@ -61,10 +87,14 @@ const getBoxPlotChcart = (data, unit, labelConfig = {}) => { ...@@ -61,10 +87,14 @@ const getBoxPlotChcart = (data, unit, labelConfig = {}) => {
type: 'value', type: 'value',
name: '', name: '',
axisLabel: { axisLabel: {
formatter: (value) => `${value}${unit}` formatter: (value) => `${value}`
}, },
splitArea: { splitArea: {
show: true show: true,
// ECharts绘制到canvas,不能直接识别CSS变量字符串;这里取到真实颜色值后再配置交替背景
areaStyle: {
color: [primary2, '#ffffff']
}
} }
}, },
series: [ series: [
......
...@@ -4,187 +4,93 @@ ...@@ -4,187 +4,93 @@
<AnalysisBox title="涉及行业" :showAllBtn="false" width="100%" height="100%"> <AnalysisBox title="涉及行业" :showAllBtn="false" width="100%" height="100%">
<div class="left-main"> <div class="left-main">
<div class="left-center"> <div class="left-center">
<el-select <el-select v-model="curHylyId" placeholder="请选择领域" class="left-center-select"
v-model="curHylyId" @change="handleIndustryChange">
placeholder="请选择行业" <el-option v-for="item in industryList" :key="item.id" :label="item.name || item.hylymc"
class="left-center-select" :value="item.id" />
@change="handleIndustryChange"
>
<el-option
v-for="item in industryList"
:key="item.id"
:label="item.name || item.hylymc"
:value="item.id"
/>
</el-select> </el-select>
<el-input <el-input v-model="companySearchKeyword" placeholder="搜索实体" class="left-center-search"
v-model="companySearchKeyword" :suffix-icon="Search" clearable />
placeholder="搜索实体"
class="left-center-search"
:suffix-icon="Search"
clearable
/>
</div> </div>
<div class="left-footer"> <div class="left-list">
<div class="item-box"> <div class="left-list-title">实体名称</div>
<div class="item" :class="{ itemActive: companyActiveIndex === idx }" <div class="left-list-content">
@click="handleClickCompany(val, idx)" v-for="(val, idx) in curCompanyList" :key="val.id"> <el-empty v-if="!curCompanyList?.length" style="padding: 60px 0;" description="暂无数据"
<div class="id">{{ (currentPage - 1) * pageSize + idx + 1 }}</div> :image-size="100" />
<div class="title" <el-scrollbar v-else height="100%" always>
:class="{ titleActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }"> <div class="item-box">
{{ val.name }} <div class="item"
</div> :class="{ itemActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }"
<div class="icon"> @click="handleClickCompany(val, idx)" v-for="(val, idx) in curCompanyList"
<img v-if="val.status === 'up'" :src="upIcon" alt="" /> :key="val.id">
<img v-if="val.status === 'down'" :src="downIcon" alt="" /> <div class="item-icon">
<img :src="defaultIcon2" alt="" class="item-img" />
</div>
<div class="title"
:class="{ titleActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }">
{{ val.name }}
</div>
<div class="icon">
<img v-if="val.status === 'up'" :src="upIcon" alt="" />
<img v-if="val.status === 'down'" :src="downIcon" alt="" />
</div>
</div>
</div> </div>
</div> </el-scrollbar>
</div> </div>
<div class="footer-box"> </div>
<div class="left">{{ `共 ${filteredCompanyList.length} 项` }}</div> <div class="left-pagination">
<div class="right"> <div class="left-pagination-left">{{ `共 ${filteredCompanyList.length} 项` }}</div>
<el-pagination @current-change="handleCurrentChange" :pageSize="pageSize" <div class="left-pagination-right">
:current-page="currentPage" size="small" background layout="prev, pager, next" <el-pagination @current-change="handleCurrentChange" :pageSize="pageSize"
:total="filteredCompanyList.length" /> :current-page="currentPage" size="small" background layout="prev, pager, next"
</div> :total="filteredCompanyList.length" />
</div> </div>
</div> </div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="right"> <div class="box2">
<AnalysisBox title="产业链分析" :showAllBtn="false" width="100%" height="100%"> <AnalysisBox :showAllBtn="false">
<div class="right-main"> <template #custom-title>
<div class="right-main-content" id="chartGraph"> <div class="custom-title">
<div class="right-main-content-header"> <div class="title-left">
<div class="header-item1"> <div :class="['title-item', {'title-active': contentType==1}]" @click="headerContentType(1)">
<div class="header-item1-top">{{ "基础支撑" }}</div> <div class="title-icon">
<div class="header-item1-bottom"> <img :src="contentType==1 ? icon1620 : icon1621" alt="">
<div class="icon">
<img src="./assets/images/warning.png" alt="" />
</div>
<div class="text">
{{ "中国企业45家(51.00%),受制裁3家(7.00%)" }}
</div>
</div> </div>
<div>产业链</div>
</div> </div>
<div class="header-item2"> <div :class="['title-item', {'title-active': contentType==2}]" @click="headerContentType(2)">
<div class="header-item2-top">{{ "软件算法" }}</div> <div class="title-icon">
<div class="header-item2-bottom"> <img :src="contentType==2 ? icon422 : icon423" alt="">
<div class="icon">
<img src="./assets/images/warning.png" alt="" />
</div>
<div class="text">
{{ "中国企业45家(51.00%),受制裁3家(7.00%)" }}
</div>
</div>
</div>
<div class="header-item3">
<div class="header-item3-top">{{ "行业应用" }}</div>
<div class="header-item3-bottom">
<div class="icon">
<img src="./assets/images/warning.png" alt="" />
</div>
<div class="text">
{{ "中国企业45家(51.00%),受制裁3家(7.00%)" }}
</div>
</div> </div>
<div>实体关系</div>
</div> </div>
</div> </div>
<div class="right-main-content-main"> <div class="title-right" v-if="contentType==1">
<Fishbone /> <el-select v-model="industryChain.id" style="width: 100%" @change="onDecreeChainNodes">
<el-option v-for="item in industryChain.list" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div> </div>
</div> </div>
<div class="box-footer"> </template>
<div class="box-footer-left"> <div class="box2-main">
<img src="../assets/icons/right-icon1.png" alt="" /> <AiTips :tips="tips" />
</div> <div class="graph-box" v-if="contentType==1">
<div class="box-footer-center"> <ChartChain :listData="fishbone.list" :baseData="fishbone.base" />
法案以218:214​(众议院)和51:50​(副总统决胜票)微弱优势强行通过,暴露两党极端对立、党内倒戈频发的特点。
</div>
<div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
</div>
</div>
</div>
</AnalysisBox>
<div class="graph-dialog" v-if="isShowCompanyDialog">
<div class="tool-box">
<div class="tool" @click="handleChangeChart(0)">
<img src="./assets/images/tool1.png" alt="" />
</div>
<div class="tool1" @click="handleChangeChart(1)">
<img src="./assets/images/tool2.png" alt="" />
</div>
<div class="tool" @click="handleChangeChart(0)">
<img src="./assets/images/tool3.png" alt="" />
</div>
</div>
<div class="chart-box" id="graphChart"></div>
</div>
<div class="company-dialog" v-if="isShowCompanyDialog">
<div class="dialog-header">
<div class="logo">
<img :src="companyInfo.logo" alt="" />
</div>
<div class="company-title">{{ companyInfo.name }}</div>
<div class="status-icon">
<img v-if="companyInfo.status === 'up'" :src="upIcon" alt="" />
<img v-if="companyInfo.status === 'down'" :src="downIcon" alt="" />
</div>
<div class="status-rate">
{{ companyInfo.changeRate + "%" }}
</div> </div>
<div class="close" @click="isShowCompanyDialog = false"> <div class="graph-box" v-if="contentType==2 && graphInfo.nodes.length">
<img :src="closeIcon" alt="" /> <GraphChart :nodes="graphInfo.nodes" :links="graphInfo.links" layoutType="force" />
</div> </div>
</div> </div>
<div class="dialog-main"> </AnalysisBox>
<div class="dialog-box1">
<div class="dialog-box1-header">
<div class="icon">
<img :src="companyInfo.data1?.icon" alt="" />
</div>
<div class="dialog-box1-title">{{ companyInfo.data1?.title }}</div>
</div>
<div class="dialog-box1-main">
<div class="item" v-for="(val, idx) in companyInfo.data1?.list" :key="idx">
<div class="item-left">
<!-- <img :src="uncheckIcon" alt=""> -->
<img :src="checkedIcon" alt="" />
</div>
<div class="item-right">
<CommonPrompt :content="val">{{ val }}</CommonPrompt>
</div>
</div>
</div>
</div>
<div class="dialog-box2">
<div class="dialog-box2-header">
<div class="icon">
<img :src="companyInfo.data2?.icon" alt="" />
</div>
<div class="dialog-box2-title">{{ companyInfo.data2?.title }}</div>
</div>
<div class="dialog-box2-main" id="chart2"></div>
</div>
<div class="dialog-box3">
<div class="dialog-box3-header">
<div class="icon">
<img :src="companyInfo.data3?.icon" alt="" />
</div>
<div class="dialog-box3-title">{{ companyInfo.data3?.title }}</div>
</div>
<div class="dialog-box3-main" id="chart3"></div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick, computed, watch } from "vue"; import { ref, onMounted, onBeforeUnmount, nextTick, computed, watch, reactive } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import * as echarts from "echarts"; import * as echarts from "echarts";
...@@ -198,6 +104,7 @@ import getTreeChart from "./utils/treeChart"; ...@@ -198,6 +104,7 @@ import getTreeChart from "./utils/treeChart";
import downIcon from "./assets/images/down.png"; import downIcon from "./assets/images/down.png";
import upIcon from "./assets/images/up.png"; import upIcon from "./assets/images/up.png";
import defaultIcon2 from "@/assets/icons/default-icon2.png";
import CompanyLogo from "./assets/images/company-logo.png"; import CompanyLogo from "./assets/images/company-logo.png";
import icon1 from "./assets/images/icon1.png"; import icon1 from "./assets/images/icon1.png";
import icon2 from "./assets/images/icon2.png"; import icon2 from "./assets/images/icon2.png";
...@@ -211,8 +118,25 @@ import Fishbone from "./components/fishbone.vue"; ...@@ -211,8 +118,25 @@ import Fishbone from "./components/fishbone.vue";
import CommonPrompt from "../../commonPrompt/index.vue"; import CommonPrompt from "../../commonPrompt/index.vue";
import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue"; import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue";
import ChartChain from "@/views/decree/decreeLayout/influence/com/ChartChain.vue";
import AiTips from "@/views/decree/decreeLayout/influence/com/AiTips.vue";
import GraphChart from "@/components/base/GraphChart/index.vue";
import {
getDecreeRelatedChain,
getDecreeChainNodes,
getDecreeRelatedEntitie,
} from "@/api/decree/influence";
import icon422 from "@/views/decree/decreeLayout/influence/assets/images/icon422.png";
import icon423 from "@/views/decree/decreeLayout/influence/assets/images/icon423.png";
import icon1620 from "@/views/decree/decreeLayout/influence/assets/images/icon1620.png";
import icon1621 from "@/views/decree/decreeLayout/influence/assets/images/icon1621.png";
import company from "@/views/decree/decreeLayout/influence/assets/images/company.png";
import CompanyImg from "./assets/images/symbol.png"; import CompanyImg from "./assets/images/symbol.png";
const ALL_INDUSTRY_VALUE = "__all_industry__";
const isShowCompanyDialog = ref(false); const isShowCompanyDialog = ref(false);
const handleIndustryChange = () => { const handleIndustryChange = () => {
...@@ -237,12 +161,164 @@ const handleClickCompany = (val, index) => { ...@@ -237,12 +161,164 @@ const handleClickCompany = (val, index) => {
companyActiveIndex.value = (currentPage.value - 1) * pageSize.value + index; companyActiveIndex.value = (currentPage.value - 1) * pageSize.value + index;
if (val) { if (val) {
curCompanyId.value = val.id; curCompanyId.value = val.id;
handleGetCompanyDetail(); // handleGetCompanyDetail();
companyInfo.value.name = val.name; companyInfo.value.name = val.name;
companyInfo.value.status = val.status || companyInfo.value.status; companyInfo.value.status = val.status || companyInfo.value.status;
headerChartData(val);
} }
isShowCompanyDialog.value = true; // isShowCompanyDialog.value = true;
};
const tips =
"这项政令标志着中美AI竞争进入一个新阶段,其核心特征是 “精准封锁”与“体系输出”相结合。它短期内无疑会给中国AI产业链带来压力,但长期看,这场竞争更可能是一场围绕技术路线、生态系统和治理规则的持久战。";
const contentType = ref(1);
const entityInfo = reactive({
id: "",
node: { id: "", companyName: "" },
});
// 产业链
const industryChain = reactive({
list: [],
id: "",
});
// 产业链鱼骨图
const fishbone = reactive({
list: [],
base: [],
});
// 实体关系
const graphInfo = reactive({
nodes: [],
links: [],
});
const onDecreeRelatedChain = async (id) => {
try {
const res = await getDecreeRelatedChain({ id });
if (res.code === 200) {
industryChain.list = res.data || [];
if (industryChain.list.length) onDecreeChainNodes(industryChain.list[0].id);
}
} catch (error) {
console.log("获取产业链失败", error);
}
};
const onDecreeChainNodes = async (id) => {
industryChain.id = id;
try {
const res = await getDecreeChainNodes({ id });
if (res.code === 200) {
let obj = res.data.chains.reduce((result, item) => {
result["chain-" + item.id] = { ...item, children: [] };
return result;
}, {});
res.data.children.forEach((item) => {
if (item.companyId == entityInfo.id) {
obj["chain-" + item.chainId].children.push({ ...item, back: true });
} else if (obj["chain-" + item.chainId]?.children?.length < 10) {
obj["chain-" + item.chainId].children.push(item);
}
});
fishbone.list = Object.values(obj);
fishbone.base = (res.data.levelInfos || []).map((item, index) => {
return { ...item, name: ["上游", "中游", "下游"][index] };
});
}
} catch (error) {
console.log("获取产业链鱼骨图失败", error);
}
};
const onFormatLink = (item, index) => {
return {
id: `link-${index + 1}`,
source: item.id + "",
target: entityInfo.id + "",
label: {
show: true,
color: "#055fc2",
backgroundColor: "#eef7ff",
borderWidth: 0,
offset: [0, 15],
formatter: item.relation,
},
lineStyle: { color: "#B9DCFF", type: "solid", opacity: 1 },
};
};
const onWordWrap = (word, num) => {
const list = (word || "").split("");
let label = "";
for (let i = 0; i < list.length; i++) {
if (i % num === 0 && i !== 0) {
label += "\n";
}
label += list[i];
}
return label;
};
const onFormatNode = (item) => {
let leader = item.id == entityInfo.id;
return {
id: item.id + "",
name: onWordWrap(item.companyName || item.name || "", 7),
label: {
show: true,
color: "#3b414b",
fontSize: leader ? 18 : 14,
fontWeight: leader ? 700 : 400,
fontFamily: "Source Han Sans CN",
},
symbolSize: 40,
symbol: `image://${company}`,
};
};
const onDecreeRelatedEntitie = async (id) => {
try {
const res = await getDecreeRelatedEntitie({ id });
if (res.code === 200) {
graphInfo.links = (res.data || []).map(onFormatLink);
graphInfo.nodes = (res.data || []).map(onFormatNode);
if (entityInfo.node?.id) graphInfo.nodes.unshift(onFormatNode(entityInfo.node));
}
} catch (error) {
console.log("获取实体关系失败", error);
}
};
const headerChartData = (row) => {
entityInfo.id = row.id;
entityInfo.node = row;
industryChain.id = "";
fishbone.list = [];
fishbone.base = [];
graphInfo.nodes = [];
graphInfo.links = [];
switch (contentType.value) {
case 1:
onDecreeRelatedChain(row.id)
break;
case 2:
onDecreeRelatedEntitie(row.id)
break;
}
};
const headerContentType = (type) => {
contentType.value = type;
headerChartData(entityInfo.node);
}; };
const pageSize = ref(10); const pageSize = ref(10);
...@@ -250,6 +326,7 @@ const currentPage = ref(1); ...@@ -250,6 +326,7 @@ const currentPage = ref(1);
const companyList = ref([]); // 企业列表 const companyList = ref([]); // 企业列表
const companySearchKeyword = ref(""); // 企业搜索关键词 const companySearchKeyword = ref(""); // 企业搜索关键词
let companySearchTimer = null;
// 根据关键词筛选后的企业列表 // 根据关键词筛选后的企业列表
const filteredCompanyList = computed(() => { const filteredCompanyList = computed(() => {
...@@ -272,9 +349,21 @@ const handleCurrentChange = page => { ...@@ -272,9 +349,21 @@ const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
}; };
// 搜索关键词变化时重置到第一页 // 搜索关键词变化时重置到第一页,并通过接口重新拉取列表
watch(companySearchKeyword, () => { watch(companySearchKeyword, () => {
currentPage.value = 1; currentPage.value = 1;
if (companySearchTimer) {
clearTimeout(companySearchTimer);
}
companySearchTimer = setTimeout(() => {
handleGetCompanyListById();
}, 300);
});
onBeforeUnmount(() => {
if (companySearchTimer) {
clearTimeout(companySearchTimer);
}
}); });
const industryList = ref([ const industryList = ref([
...@@ -321,27 +410,36 @@ const companyInfo = ref({ ...@@ -321,27 +410,36 @@ const companyInfo = ref({
const handleGetHylyList = async () => { const handleGetHylyList = async () => {
try { try {
const res = await getHylyList(); const res = await getHylyList();
// console.log("行业领域字典列表", res); if (res.code === 200 && Array.isArray(res.data)) {
industryList.value = res.data industryList.value = [{ id: ALL_INDUSTRY_VALUE, name: "全部领域" }, ...res.data];
if (res.data && res.data.length > 0) { curHylyId.value = ALL_INDUSTRY_VALUE;
curHylyId.value = res.data[0].id; return;
} }
} catch (error) { } industryList.value = [];
curHylyId.value = "";
} catch (error) {
industryList.value = [];
curHylyId.value = "";
}
}; };
const curHylyId = ref(""); const curHylyId = ref("");
// 根据行业领域id获取公司列表 // 根据行业领域id获取公司列表
const handleGetCompanyListById = async () => { const handleGetCompanyListById = async () => {
const params = { const params = {};
id: curHylyId.value const trimmedCompanyName = companySearchKeyword.value.trim();
}; if (curHylyId.value && curHylyId.value !== ALL_INDUSTRY_VALUE) {
params.id = curHylyId.value;
}
if (trimmedCompanyName) {
params.companyName = trimmedCompanyName;
}
try { try {
const res = await getCompanyList(params); const res = await getCompanyList(params);
// console.log('根据行业id获取公司里列表', res); // console.log('根据行业id获取公司里列表', res);
if (res.code === 200 && res.data.length) { if (res.code === 200 && Array.isArray(res.data) && res.data.length) {
companyList.value = res.data; companyList.value = res.data;
companySearchKeyword.value = "";
currentPage.value = 1; currentPage.value = 1;
nextTick(() => { nextTick(() => {
handleClickCompany(companyList.value[0], 0); handleClickCompany(companyList.value[0], 0);
...@@ -349,7 +447,9 @@ const handleGetCompanyListById = async () => { ...@@ -349,7 +447,9 @@ const handleGetCompanyListById = async () => {
} else { } else {
companyList.value = []; companyList.value = [];
} }
} catch (error) { } } catch (error) {
companyList.value = [];
}
}; };
// 根据法案id,公司id,行业领域id获取公司的详情 // 根据法案id,公司id,行业领域id获取公司的详情
...@@ -357,7 +457,7 @@ const handleGetCompanyDetail = async () => { ...@@ -357,7 +457,7 @@ const handleGetCompanyDetail = async () => {
const params = { const params = {
billId: window.sessionStorage.getItem("billId"), billId: window.sessionStorage.getItem("billId"),
companyId: curCompanyId.value, companyId: curCompanyId.value,
id: curHylyId.value id: curHylyId.value === ALL_INDUSTRY_VALUE ? "" : curHylyId.value
}; };
try { try {
const res = await getCompanyDetail(params); const res = await getCompanyDetail(params);
...@@ -711,15 +811,28 @@ onMounted(async () => { ...@@ -711,15 +811,28 @@ onMounted(async () => {
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
padding: 10px 16px;
} }
.left-center { .left-center {
flex: 1; flex-shrink: 0;
min-height: 0; height: 32px;
margin: 12px 17px 0; min-height: 32px;
display: flex; display: flex;
align-items: flex-start; align-items: center;
gap: 12px; gap: 16px;
margin-bottom: 10px;
:deep(.el-input__wrapper),
:deep(.el-select__wrapper) {
height: 32px;
}
:deep(.el-input__inner),
:deep(.el-select__selection) {
height: 32px;
line-height: 32px;
}
.left-center-select { .left-center-select {
width: 150px; width: 150px;
...@@ -731,48 +844,76 @@ onMounted(async () => { ...@@ -731,48 +844,76 @@ onMounted(async () => {
min-width: 0; min-width: 0;
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
border: 1px solid var(--bg-black-10); background-color: var(--el-fill-color-blank);
box-shadow: none; border-radius: var(--el-border-radius-base);
box-shadow: 0 0 0 1px var(--el-border-color) inset;
border: none;
box-sizing: border-box;
} }
} }
} }
.left-footer { .left-list {
flex-shrink: 0; flex: 1;
margin: 0 auto; min-height: 0;
width: 446px;
height: 520px;
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
.left-list-title {
width: 100%;
text-align: left;
color: var(--text-primary-80-color);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
border-top: 1px solid rgba(240, 242, 244, 1);
padding: 12px 0;
}
.left-list-content {
flex: 1;
min-height: 0;
}
.item-box { .item-box {
height: 480px; width: 446px;
overflow: hidden; margin: 0 auto;
.item { .item {
width: 100%; width: 100%;
height: 48px; height: 48px;
border-radius: 4px; border-radius: 4px;
border-bottom: 1px solid rgba(243, 243, 244, 1); border-bottom: 1px solid rgba(240, 242, 244, 1);
border-top: 1px solid transparent;
display: flex; display: flex;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgba(246, 251, 255, 1); background-color: #f7f8fa;
}
&:first-child {
border-top-color: rgba(240, 242, 244, 1);
} }
.id { .item-icon {
margin-left: 12px; margin-left: 12px;
margin-top: 8px; margin-top: 8px;
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 12px; border-radius: 12px;
background: rgba(231, 241, 255); background: rgba(231, 241, 255);
text-align: center; display: flex;
line-height: 24px; align-items: center;
color: rgba(95, 101, 108, 1); justify-content: center;
font-family: Microsoft YaHei; overflow: hidden;
font-size: 12px;
font-weight: 400; .item-img {
width: 100%;
height: 100%;
object-fit: contain;
}
} }
.title { .title {
...@@ -804,31 +945,53 @@ onMounted(async () => { ...@@ -804,31 +945,53 @@ onMounted(async () => {
} }
.itemActive { .itemActive {
background: rgba(246, 251, 255, 1); background-color: rgba(5, 95, 194, 0.05);
border-top-color: rgba(174, 214, 255, 1);
border-bottom-color: rgba(174, 214, 255, 1);
.item-icon {
background: rgba(5, 95, 194, 0.1);
}
} }
.titleActive { .titleActive {
color: rgba(22, 119, 255, 1) !important; color: rgba(22, 119, 255, 1) !important;
} }
} }
}
.footer-box { .left-pagination {
display: flex; flex-shrink: 0;
justify-content: space-between; margin: 0 auto;
width: 446px;
height: 65px;
overflow: hidden;
display: flex;
align-items: flex-start;
.left { justify-content: space-between;
width: 100px; width: 100%;
height: 20px; box-sizing: border-box;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.right { .left-pagination-left {
flex: 300px; width: 100px;
} height: 18px;
margin-top: 25px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 18px;
overflow: hidden;
white-space: nowrap;
}
.left-pagination-right {
flex: 1;
min-width: 0;
display: flex;
justify-content: flex-end;
margin-top: 23px;
} }
} }
} }
...@@ -1362,5 +1525,78 @@ onMounted(async () => { ...@@ -1362,5 +1525,78 @@ onMounted(async () => {
} }
} }
} }
.box2 {
margin-top: 16px;
margin-left: 16px;
width: 1247px;
height: 847px;
position: relative;
.custom-title {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 100%;
height: 100%;
padding: 0 20px;
.title-left {
display: flex;
border: 1px solid rgb(5, 95, 194);
color: rgb(5, 95, 194);
border-radius: 16px;
width: 240px;
height: 32px;
overflow: hidden;
cursor: pointer;
.title-item {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 50%;
font-size: 16px;
line-height: 16px;
font-family: "Microsoft YaHei";
.title-icon {
width: 14px;
height: 14px;
font-size: 0;
img {
width: 100%;
height: 100%;
}
}
}
.title-active {
background-color: rgb(5, 95, 194);
color: white;
}
}
.title-right {
width: 180px;
}
}
.box2-main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 16px 20px;
.graph-box {
height: 20px;
flex: auto;
margin-top: 16px;
}
}
}
} }
</style> </style>
...@@ -90,10 +90,10 @@ ...@@ -90,10 +90,10 @@
<img class="person-avatar" :src="curPerson.imageUrl || defaultAvatar" alt="" <img class="person-avatar" :src="curPerson.imageUrl || defaultAvatar" alt=""
@click="handleClickAvatar(curPerson)" /> @click="handleClickAvatar(curPerson)" />
<div class="usr-icon1"> <div class="usr-icon1">
<img src="./assets/images/usr-icon1.png" alt="" /> <img :src="partyIconUrl" alt="" />
</div> </div>
<div class="usr-icon2"> <div class="usr-icon2">
<img src="./assets/images/usr-icon2.png" alt="" /> <img :src="congressIconUrl" alt="" />
</div> </div>
</div> </div>
<div class="info-right"> <div class="info-right">
...@@ -159,6 +159,12 @@ import { getPersonSummaryInfo } from "@/api/common/index"; ...@@ -159,6 +159,12 @@ import { getPersonSummaryInfo } from "@/api/common/index";
import defaultAvatar from "../assets/images/default-icon1.png"; import defaultAvatar from "../assets/images/default-icon1.png";
import defaultNew from "../assets/images/default-icon-news.png"; import defaultNew from "../assets/images/default-icon-news.png";
import defaultBill from "./assets/images/image1.png" import defaultBill from "./assets/images/image1.png"
import defaultUsrIcon1 from "./assets/images/usr-icon1.png";
import defaultUsrIcon2 from "./assets/images/usr-icon2.png";
import cyyIcon from "@/assets/icons/cyy.png";
import zyyIcon from "@/assets/icons/zyy.png";
import ghdIcon from "@/assets/icons/ghd.png";
import mzdIcon from "@/assets/icons/mzd.png";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
const route = useRoute(); const route = useRoute();
...@@ -229,6 +235,22 @@ const basicInfo = ref({}); ...@@ -229,6 +235,22 @@ const basicInfo = ref({});
const riskSignal = computed(() => basicInfo.value?.riskSignalVO || null); const riskSignal = computed(() => basicInfo.value?.riskSignalVO || null);
const hylyList = computed(() => (Array.isArray(basicInfo.value?.hylyList) ? basicInfo.value.hylyList : [])); const hylyList = computed(() => (Array.isArray(basicInfo.value?.hylyList) ? basicInfo.value.hylyList : []));
const reportList = computed(() => (Array.isArray(basicInfo.value?.reportList) ? basicInfo.value.reportList : [])); const reportList = computed(() => (Array.isArray(basicInfo.value?.reportList) ? basicInfo.value.reportList : []));
// 提出人头像下方标志:参/众议院 + 党派
const congressIconUrl = computed(() => {
const congress = curPerson.value?.congress;
if (congress === "参议院") return cyyIcon;
if (congress === "众议院") return zyyIcon;
return defaultUsrIcon1;
});
const partyIconUrl = computed(() => {
const dp = curPerson.value?.dp;
if (dp === "共和党") return ghdIcon;
if (dp === "民主党") return mzdIcon;
return defaultUsrIcon2;
});
const reversedStageList = computed(() => { const reversedStageList = computed(() => {
const list = Array.isArray(basicInfo.value?.stageList) ? basicInfo.value.stageList : []; const list = Array.isArray(basicInfo.value?.stageList) ? basicInfo.value.stageList : [];
return [...list].reverse(); return [...list].reverse();
...@@ -764,9 +786,10 @@ onMounted(() => { ...@@ -764,9 +786,10 @@ onMounted(() => {
.person-box { .person-box {
width: 500px; width: 500px;
overflow-x: auto; overflow-x: hidden;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
flex-wrap: wrap;
padding-bottom: 5px; padding-bottom: 5px;
&::-webkit-scrollbar { &::-webkit-scrollbar {
...@@ -783,7 +806,8 @@ onMounted(() => { ...@@ -783,7 +806,8 @@ onMounted(() => {
} }
.person-item { .person-item {
height: 28px; min-height: 28px;
height: auto;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid var(--btn-plain-border-color); border: 1px solid var(--btn-plain-border-color);
border-radius: 4px; border-radius: 4px;
...@@ -799,8 +823,12 @@ onMounted(() => { ...@@ -799,8 +823,12 @@ onMounted(() => {
margin-right: 8px; margin-right: 8px;
padding: 1px 12px; padding: 1px 12px;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: normal;
flex-shrink: 0; word-break: break-all;
line-height: 18px;
flex-shrink: 1;
max-width: 170px;
text-align: center;
} }
.nameItemActive { .nameItemActive {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论