提交 143ff2ac authored 作者: coderBryanFu's avatar coderBryanFu

feat:数据资源库更新

流水线 #247 已通过 于阶段
in 1 分 48 秒
......@@ -143,6 +143,7 @@ export function getChartAnalysis(data, options = {}) {
: typeof options?.onInterpretationDelta === "function"
? options.onInterpretationDelta
: null;
const externalSignal = options?.signal;
return new Promise((resolve, reject) => {
let buffer = "";
let latestInterpretation = "";
......@@ -150,16 +151,32 @@ export function getChartAnalysis(data, options = {}) {
let lastStreamedInterpretationLen = 0;
let settled = false;
const abortController = new AbortController();
const externalAbortHandler = () => {
abortController.abort();
};
if (externalSignal) {
if (externalSignal.aborted) {
abortController.abort();
} else {
externalSignal.addEventListener("abort", externalAbortHandler, { once: true });
}
}
const safeResolve = value => {
if (settled) return;
settled = true;
if (externalSignal) {
externalSignal.removeEventListener("abort", externalAbortHandler);
}
resolve(value);
};
const safeReject = err => {
if (settled) return;
settled = true;
if (externalSignal) {
externalSignal.removeEventListener("abort", externalAbortHandler);
}
reject(err);
};
......
......@@ -107,11 +107,12 @@ export function getBillPersonAnalyzeDy(params) {
* @param {id}
* @header token
*/
export function getBillContentId(params) {
export function getBillContentId(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/contentId/${params.id}`,
params,
signal: config.signal
})
}
......@@ -120,11 +121,12 @@ export function getBillContentId(params) {
* @param {billId,id,cRelated,currentPage,pageSize,domainNameList,measuresNameList,content}
* @header token
*/
export function getBillContentTk(params) {
export function getBillContentTk(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/content/tk/${params.billId}/${params.id}`,
params,
signal: config.signal
})
}
......@@ -133,11 +135,12 @@ export function getBillContentTk(params) {
* @param {billId,versionId,cRelated}
* @header token
*/
export function getBillContentXzfs(params) {
export function getBillContentXzfs(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/content/xzfs/${params.billId}/${params.versionId}`,
params,
signal: config.signal
})
}
......@@ -146,11 +149,12 @@ export function getBillContentXzfs(params) {
* @param {billId,versionId,cRelated}
* @header token
*/
export function getBillHyly(params) {
export function getBillHyly(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/content/hyly/${params.billId}/${params.versionId}`,
params,
signal: config.signal
})
}
......
......@@ -3,8 +3,14 @@ import request from "@/api/request.js";
// 涉华法案领域分布
/**
* @param {Object} params
* @param {string} params.year - 年份
* @param {string} [params.status] - 状态:提出法案/通过法案
* @param {string} params.year - 年份(2022-2026)
* @param {string} [params.status] - 法案状态:
* - 提案
* - 众议院通过
* - 参议院通过
* - 分歧已解决
* - 呈交总统
* - 完成立法
*/
export function getBillIndustry(params) {
return request({
......
差异被折叠。
......@@ -82,6 +82,7 @@ const getGraphChart = (nodes, links, layoutType) => {
}
},
force: {
layoutAnimation: false, // 关闭初始化晃来晃去的动画
repulsion: 300,
gravity: 0,
edgeLength: 300
......
......@@ -17,7 +17,14 @@
</template>
<script setup>
import {ref} from 'vue'
import {onMounted, ref} from 'vue'
const props = defineProps({
activeTime: {
typeof: String,
default: '近一周'
}
})
const timeList = ref([
{
......@@ -34,6 +41,10 @@ const timeList = ref([
},
])
onMounted(() => {
timeList.value.forEach(item => { item.active = item.time === props.activeTime })
})
const handleTimeClick = (item, index) => {
timeList.value.forEach(time => {
time.active = false
......
<template>
<div class="intelligenceLeftTabBar">
<div class="navBox" :class="{navBoxShow:isNavMenuShow}">
<div class="navList" v-for="(item,index) in navList " :key="index" :class="{on:navPath==item.path}" @click="onNavListClick(item.path)">
<div class="icon" :style="{background:`url(${item.img})no-repeat`,backgroundSize:'24px 24px',backgroundPosition:'17px 17px'}"></div>
<span class="text-tip-1" style="white-space: nowrap; ">{{ item.name }}</span>
</div>
</div>
<img class="show" src="@/assets/icons/muenShow.png" :style="isNavMenuShow?'transform: scaleX(1)':''" alt="" @click="()=>{isNavMenuShow=!isNavMenuShow}">
</div>
</template>
<script setup>
import muen1 from '@/assets/icons/tool-item-icon1.png'
import muen2 from '@/assets/icons/tool-item-icon2.png'
import muen3 from '@/assets/icons/tool-item-icon3.png'
import muen4 from '@/assets/icons/tool-item-icon4.png'
import { onMounted, onUnmounted, ref, nextTick } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
const isNavMenuShow=ref(false)
const navList=ref([
{
img:muen1,
path:'/writtingAsstaint',
name:'智能写报'
},
{
img:muen2,
path:'/writtingAsstaint1',
name:'智能翻译'
},
{
img:muen3,
path:'/writtingAsstaint2',
name:'智能查询'
},
{
img:muen4,
path:'/writtingAsstaint3',
name:'智能对话'
},
])
const navPath=ref()
const route=useRoute()
if(route.path){
navPath.value=route.path
}
const onNavListClick=(path)=>{
if(path=='/writtingAsstaint'){
navPath.value=path
}else{
ElMessage.error('正在开发中')
}
}
</script>
<style lang="scss" scoped>
.intelligenceLeftTabBar{
padding: 5px 0;
border-right: 1px solid rgb(234, 236, 238);
position: relative;
.navBox{
height: 100%;
width: 65px;
transition: all 0.3s;
padding: 0 3px;
.navList{
display: flex;
align-items: center;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
height: 60px;
.icon{
width: 60px;
height: 60px;
flex-shrink: 0;
border-radius: 10px;
margin-right: 15px;
}
}
.on{
background-color: var(--color-primary-10);
color: var(--color-primary-100);
font-weight: Bold;
}
}
.navBoxShow{
width: 200px;
transition: all 0.3s;
}
.show{
position: absolute;
width: 24px;
height: 24px;
right: 21px;
bottom: 21px;
cursor: pointer;
transform: scaleX(-1)
}
}
</style>
\ No newline at end of file
......@@ -295,13 +295,16 @@ export function useMarkdownStream() {
// 预处理内容
// const processedContent = preprocessMarkdown(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>`
})
// content = content.replace(/==(\d+)、==/g, (match, p1) => {
// return `<button class="clause-ref-btn" data-clause="${p1}">${p1}</button>`
// })
console.log(content,11223)
content = content.replace(/==\s*(\d+)、.*?==/g, (match, p1) => {
return `<button class="clause-ref-btn" data-clause="${match.replace(/==/g, '') }">${p1}</button>`;
});
return md.render(content)
})
......
......@@ -69,7 +69,8 @@ router.beforeEach((to, from, next) => {
if (to.meta.title) {
if (to.meta.dynamicTitle) {
console.log('to', to);
document.title = window.sessionStorage.getItem("curTabName") || to.meta.title;
const storageKey = to.meta.titleStorageKey || "curTabName";
document.title = window.sessionStorage.getItem(storageKey) || to.meta.title;
} else {
document.title = to.meta.title
......
......@@ -20,7 +20,8 @@ const cooperationRestrictionsRoutes = [
component: CooperationRestrictionsDetail,
meta: {
title: "合作限制详情",
dynamicTitle: true
dynamicTitle: true,
titleStorageKey: "cooperationRestrictionsTabName"
}
},
......
......@@ -144,7 +144,7 @@
</el-select>
</template>
<div class="overview-card-body box5-main">
<div class="overview-chart-wrap" :class="{ 'is-empty': !box5HasData }">
<div class="overview-chart-wrap" v-loading="chartLoading.box5" :class="{ 'is-empty': !box5HasData }">
<el-empty v-if="!box5HasData" description="暂无数据" :image-size="100" />
<div v-else id="box5Chart" class="overview-chart"></div>
</div>
......@@ -169,7 +169,7 @@
</el-select>
</template>
<div class="overview-card-body box6-main">
<div class="overview-chart-wrap" :class="{ 'is-empty': !box9HasData }">
<div class="overview-chart-wrap" v-loading="chartLoading.box6" :class="{ 'is-empty': !box9HasData }">
<el-empty v-if="!box9HasData" description="暂无数据" :image-size="100" />
<div v-else id="box9Chart" class="overview-chart"></div>
</div>
......@@ -191,7 +191,7 @@
</el-select>
</template>
<div class="overview-card-body box7-main">
<div class="overview-chart-wrap" :class="{ 'is-empty': !box7HasData }">
<div class="overview-chart-wrap" v-loading="chartLoading.box7" :class="{ 'is-empty': !box7HasData }">
<el-empty v-if="!box7HasData" description="暂无数据" :image-size="100" />
<div v-else id="box7Chart" class="overview-chart"></div>
</div>
......@@ -211,10 +211,10 @@
</el-select>
</template>
<div class="overview-card-body box8-main">
<div class="overview-chart-wrap" :class="{ 'is-empty': !box8HasData }">
<div class="overview-chart-wrap" v-loading="chartLoading.box8" :class="{ 'is-empty': !box8HasData }">
<el-empty v-if="!box8HasData" description="暂无数据" :image-size="100" />
<template v-else>
<div class="box8-desc">通过涉华法案{{ box8Summary }}</div>
<div class="box8-desc">完成立法涉华法案{{ box8Summary }}</div>
<div id="box8Chart" class="overview-chart box8-chart"></div>
</template>
</div>
......@@ -229,7 +229,7 @@
</OverviewCard>
<OverviewCard class="overview-card--single box9" title="关键条款词云" :icon="box7HeaderIcon">
<div class="overview-card-body box9-main">
<div class="overview-chart-wrap">
<div class="overview-chart-wrap" v-loading="chartLoading.box9">
<el-empty v-if="!wordCloudHasData" description="暂无数据" :image-size="100" />
<WordCloundChart v-else class="overview-chart" width="100%" height="100%" :data="wordCloudData" />
</div>
......@@ -512,6 +512,14 @@ const aiPaneLoading = ref({
box9: false
});
const chartLoading = ref({
box5: false,
box6: false,
box7: false,
box8: false,
box9: false
});
const gotoNewsDetail = useGotoNewsDetail();
const handleClickNewsDetail = news => {
const newsId = news?.newsId || news?.id;
......@@ -767,7 +775,7 @@ const box5Data = ref({
value: [145, 52, 84, 99, 71, 96, 128, 144, 140, 168, 188, 172]
},
{
name: "通过法案",
name: "完成立法",
value: [6, 3, 4, 6, 11, 5, 2, 14, 16, 27, 28, 44]
},
{
......@@ -779,13 +787,14 @@ const box5Data = ref({
value: []
},
{
name: "双院通过",
name: "解决分歧",
value: []
}
]
});
const box5HasData = ref(true);
const handleGetBillCount = async () => {
chartLoading.value = { ...chartLoading.value, box5: true };
try {
const params = {
dateDesc: box5ProposalTime.value
......@@ -806,7 +815,7 @@ const handleGetBillCount = async () => {
value: sortedData.map(item => item.proposedCount)
},
{
name: "通过法案",
name: "完成立法",
value: sortedData.map(item => item.passCount)
},
{
......@@ -818,7 +827,7 @@ const handleGetBillCount = async () => {
value: sortedData.map(item => item.senateCount)
},
{
name: "双院通过",
name: "解决分歧",
value: sortedData.map(item => item.hscount)
}
],
......@@ -831,10 +840,10 @@ const handleGetBillCount = async () => {
title: [],
data: [
{ name: "提出法案", value: [] },
{ name: "通过法案", value: [] },
{ name: "完成立法", value: [] },
{ name: "众议院通过", value: [] },
{ name: "参议院通过", value: [] },
{ name: "双院通过", value: [] }
{ name: "解决分歧", value: [] }
],
percent: []
};
......@@ -847,13 +856,15 @@ const handleGetBillCount = async () => {
title: [],
data: [
{ name: "提出法案", value: [] },
{ name: "通过法案", value: [] },
{ name: "完成立法", value: [] },
{ name: "众议院通过", value: [] },
{ name: "参议院通过", value: [] },
{ name: "双院通过", value: [] }
{ name: "解决分歧", value: [] }
],
percent: []
};
} finally {
chartLoading.value = { ...chartLoading.value, box5: false };
}
};
......@@ -895,6 +906,7 @@ const handleBox5Change = () => {
const box7HasData = ref(true);
const box7AiData = ref({ inner: [], outer: [] });
const handleBox7Data = async () => {
chartLoading.value = { ...chartLoading.value, box7: true };
try {
const res = await getBillPostOrg({ year: box7selectetedTime.value });
console.log("法案提出部门", res);
......@@ -949,6 +961,8 @@ const handleBox7Data = async () => {
console.error("获取法案提出部门数据失败", error);
box7HasData.value = false;
box7AiData.value = { inner: [], outer: [] };
} finally {
chartLoading.value = { ...chartLoading.value, box7: false };
}
};
......@@ -975,6 +989,7 @@ const handleToSocialDetail = item => {
const wordCloudData = ref([]);
const wordCloudHasData = computed(() => Array.isArray(wordCloudData.value) && wordCloudData.value.length > 0);
const handleGetKeyTK = async () => {
chartLoading.value = { ...chartLoading.value, box9: true };
try {
const res = await getBillOverviewKeyTK();
console.log("关键条款", res);
......@@ -992,6 +1007,8 @@ const handleGetKeyTK = async () => {
}
} catch (error) {
console.error("获取关键条款error", error);
} finally {
chartLoading.value = { ...chartLoading.value, box9: false };
}
};
const handleBox6 = async () => {
......@@ -1001,10 +1018,15 @@ const handleBox6 = async () => {
// 涉华领域分布
const box9ChartData = ref([]);
const box9selectetedTime = ref("2025");
const box9LegislativeStatus = ref("提出法案");
// 立法状态下拉:提出法案、众议院通过、参议院通过、解决分歧、完成立法
// v-model 存储的是接口需要的 status 值
const box9LegislativeStatus = ref("提案");
const box9LegislativeStatusList = ref([
{ label: "提出法案", value: "提出法案" },
{ label: "通过法案", value: "通过法案" }
{ label: "提出法案", value: "提案" },
{ label: "众议院通过", value: "众议院通过" },
{ label: "参议院通过", value: "参议院通过" },
{ label: "解决分歧", value: "分歧已解决" },
{ label: "完成立法", value: "完成立法" }
]);
const box9YearList = ref([
{
......@@ -1031,6 +1053,7 @@ const box9YearList = ref([
const box9HasData = ref(true);
let box9ChartInstance = null;
const getBox9Data = async () => {
chartLoading.value = { ...chartLoading.value, box6: true };
const params = {
year: box9selectetedTime.value,
status: box9LegislativeStatus.value
......@@ -1048,6 +1071,8 @@ const getBox9Data = async () => {
} catch (error) {
box9HasData.value = false;
box9ChartData.value = [];
} finally {
chartLoading.value = { ...chartLoading.value, box6: false };
}
};
const handleBox9Data = async () => {
......@@ -1064,6 +1089,10 @@ const handleBox9Data = async () => {
null,
{ showCount: false }
);
// 记录埋点时,将当前选中的立法状态映射为序号(0-4)
const selectedIndex = box9LegislativeStatusList.value.findIndex(
item => item.value === box9LegislativeStatus.value
);
const selectParam = {
moduleType: '国会法案',
key: 2,
......@@ -1091,14 +1120,13 @@ const box8StageList = ref([]);
const box8MockDataByYear = {
"2025": {
passCount: 19,
// 从上到下顺序:提出法案、众议院通过、参议院通过、解决分歧、完成立法
stages: [
{ name: "已提案", count: 24 },
{ name: "提出法案", count: 24 },
{ name: "众议院通过", count: 20 },
{ name: "众议院不通过", count: 25 },
{ name: "解决分歧", count: 23 },
{ name: "参议院通过", count: 26 },
{ name: "总统否决", count: 48 },
{ name: "立法", count: 19 }
{ name: "解决分歧", count: 23 },
{ name: "完成立法", count: 19 }
]
}
};
......@@ -1222,15 +1250,15 @@ const getBox8ChartOption = stageList => {
};
const handleBox8Data = async () => {
const stageOrder = ["提案", "众议院通过", "众议院不通过", "分歧已解决", "参议院通过", "总统否决或未签署", "完成立法"];
chartLoading.value = { ...chartLoading.value, box8: true };
// 进展分布显示顺序:提出法案(对应进度“提案”)、众议院通过、参议院通过、分歧已解决(解决分歧)、完成立法
const stageOrder = ["提案", "众议院通过", "参议院通过", "分歧已解决", "完成立法"];
const stageNameMap = {
提案: "已提案",
提案: "提出法案",
众议院通过: "众议院通过",
众议院不通过: "众议院不通过",
分歧已解决: "解决分歧",
参议院通过: "参议院通过",
"总统否决或未签署": "总统否决",
完成立法: "立法"
分歧已解决: "解决分歧",
完成立法: "完成立法"
};
try {
......@@ -1289,6 +1317,8 @@ const handleBox8Data = async () => {
box8StageList.value = [];
setChart({}, "box8Chart", true, selectParam);
}
} finally {
chartLoading.value = { ...chartLoading.value, box8: false };
}
};
......
......@@ -49,7 +49,8 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
containLabel: true
},
legend: {
data: ['提出法案', '通过法案', '众议院通过', '参议院通过', '双院通过'],
// 图例顺序:提出法案、众议院通过、参议院通过、解决分歧、完成立法
data: ['提出法案', '众议院通过', '参议院通过', '解决分歧', '完成立法'],
show: true,
top: 10,
icon: 'circle',
......@@ -126,7 +127,8 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
data: proposedData
},
{
name: '通过法案',
// 众议院通过
name: '众议院通过',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -137,10 +139,11 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[1]
},
data: passData
data: houseData
},
{
name: '众议院通过',
// 参议院通过
name: '参议院通过',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -151,10 +154,11 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[2]
},
data: houseData
data: senateData
},
{
name: '参议院通过',
// 解决分歧
name: '解决分歧',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -165,10 +169,11 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[3]
},
data: senateData
data: hsData
},
{
name: '双院通过',
// 完成立法
name: '完成立法',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -179,7 +184,7 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[4]
},
data: hsData
data: passData
}
]
}
......
<svg viewBox="0 0 12 13" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.000000" height="13.000000" fill="none" customFrame="#000000">
<path id="矢量 1651" d="M8.05031 2.15239e-07C8.33835 2.69049e-07 8.61901 0.0965686 8.85205 0.275865C9.08508 0.455161 9.25853 0.70798 9.34755 0.998087C9.43656 1.28819 9.43656 1.6007 9.34755 1.8908C9.25853 2.18091 9.08508 2.43373 8.85205 2.61303C8.61901 2.79232 8.33835 2.88889 8.05031 2.88889L3.9583 2.88889C3.69558 2.88873 3.43848 2.80821 3.21793 2.65703C2.99737 2.50585 2.82274 2.29043 2.71502 2.03667C2.35984 2.08328 2.0252 2.23849 1.752 2.48332C1.4788 2.72814 1.27889 3.05197 1.17673 3.41519C1.07456 3.77842 1.07456 4.1653 1.17673 4.52853C1.27889 4.89176 1.4788 5.21558 1.752 5.46041C2.0252 5.70523 2.35984 5.86044 2.71502 5.90706C2.82284 5.65343 2.99751 5.43816 3.21806 5.28711C3.43861 5.13606 3.69565 5.05566 3.9583 5.05556L8.05031 5.05556C8.31755 5.05563 8.57888 5.13883 8.80185 5.29483C9.02483 5.45084 9.19961 5.67276 9.3045 5.93306C9.88944 5.98317 10.4467 6.21802 10.9042 6.60722C11.3618 6.99642 11.6985 7.52206 11.8708 8.11613C12.0431 8.7102 12.0431 9.34536 11.8708 9.93943C11.6985 10.5335 11.3618 11.0591 10.9042 11.4483C10.4467 11.8375 9.88944 12.0724 9.3045 12.1225C9.19961 12.3828 9.02483 12.6047 8.80185 12.7607C8.57888 12.9167 8.31755 12.9999 8.05031 13L3.9583 13C3.71881 13.0001 3.48352 12.9334 3.27608 12.8067C3.06865 12.6799 2.89638 12.4976 2.77661 12.2779C2.65684 12.0583 2.59378 11.8092 2.59378 11.5556C2.59378 11.3019 2.65684 11.0528 2.77661 10.8332C2.89638 10.6136 3.06865 10.4312 3.27608 10.3044C3.48352 10.1777 3.71881 10.111 3.9583 10.1111L8.05031 10.1111C8.60409 10.1111 9.07944 10.4607 9.29291 10.9626C9.64884 10.9171 9.98445 10.7624 10.2585 10.5177C10.5326 10.273 10.7333 9.94881 10.8358 9.58501C10.9384 9.22121 10.9384 8.83362 10.8358 8.46982C10.7333 8.10603 10.5326 7.78186 10.2585 7.53714C9.98445 7.29241 9.64884 7.13778 9.29291 7.09222C9.18531 7.34593 9.0108 7.56132 8.79036 7.71251C8.56992 7.86369 8.31294 7.94423 8.05031 7.94444L3.9583 7.94444C3.69087 7.94445 3.42933 7.86121 3.20622 7.70506C2.9831 7.54892 2.80826 7.32676 2.70342 7.06622C2.11732 7.01784 1.55849 6.78394 1.09952 6.39491C0.640554 6.00587 0.302655 5.47968 0.129709 4.88468C-0.0432364 4.28967 -0.0432363 3.65334 0.129709 3.05833C0.302654 2.46332 0.640554 1.93713 1.09952 1.54809C1.55849 1.15906 2.11732 0.925163 2.70342 0.876778C2.80847 0.616508 2.9834 0.394654 3.2065 0.238777C3.4296 0.0829001 3.69102 -0.000130273 3.9583 2.15239e-07L8.05031 2.15239e-07Z" fill="rgb(95,101,108)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 12 13.3145" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.000000" height="13.314453" fill="none" customFrame="#000000">
<path id="矢量 52" d="M2.47123 9.11862L8.566 3.02386C8.82631 2.76355 8.82631 2.3415 8.566 2.08119C8.30569 1.82088 7.88364 1.82088 7.62333 2.08119L1.52857 8.17595C1.40356 8.30096 1.33333 8.4705 1.33333 8.64729C1.33333 9.01542 1.63176 9.31385 1.9999 9.31385C2.17668 9.31385 2.34623 9.24363 2.47123 9.11862ZM2.82867 10.6472L1 10.6472C0.447715 10.6472 0 10.1995 0 9.64719L0 8.23273C0 7.96752 0.105357 7.71316 0.292894 7.52563L7.62333 0.195193C7.74833 0.0702133 7.91787 0 8.09467 0C8.27147 0 8.441 0.0702133 8.566 0.195193L10.452 2.08119C10.577 2.20621 10.6472 2.37575 10.6472 2.55253C10.6472 2.7293 10.577 2.89884 10.452 3.02386L2.82867 10.6472L2.82867 10.6472ZM0.666666 11.9805L11.3333 11.9805C11.7015 11.9805 12 12.279 12 12.6472C12 13.0154 11.7015 13.3139 11.3333 13.3139L0.666667 13.3139C0.298477 13.3139 0 13.0154 0 12.6472C0 12.279 0.298477 11.9805 0.666666 11.9805Z" fill="rgb(255,255,255)" fill-rule="evenodd" />
</svg>
......@@ -27,33 +27,17 @@
</div>
<div class="left-box-bottom" v-if="showTabs">
<template v-if="isLoading">
<div class="left-box-bottom-item is-skeleton" v-for="n in 4" :key="n">
<div class="icon">
<el-skeleton-item class="skeleton-tab-icon" variant="text" />
</div>
<div class="name">
<el-skeleton-item class="skeleton-tab-text" variant="text" />
</div>
<div class="left-box-bottom-item"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }" v-for="item in tabs"
:key="item.path" @click="emit('tab-click', item)">
<div class="icon">
<img v-if="activeTitle === item.name" :src="item.activeIcon" alt="" />
<img v-else :src="item.icon" alt="" />
</div>
</template>
<template v-else>
<div
class="left-box-bottom-item"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }"
v-for="item in tabs"
:key="item.path"
@click="emit('tab-click', item)"
>
<div class="icon">
<img v-if="activeTitle === item.name" :src="item.activeIcon" alt="" />
<img v-else :src="item.icon" alt="" />
</div>
<div class="name" :class="{ nameActive: activeTitle === item.name }">
{{ item.name }}
</div>
<div class="name" :class="{ nameActive: activeTitle === item.name }">
{{ item.name }}
</div>
</template>
</div>
</div>
</div>
......@@ -77,24 +61,18 @@
</div>
<div class="right-box-bottom" v-if="showActions">
<template v-if="isLoading">
<div class="btn3 is-skeleton">
<div class="icon">
<el-skeleton-item class="skeleton-action-icon" variant="text" />
</div>
<div class="text">
<el-skeleton-item class="skeleton-action-text" variant="text" />
</div>
<div class="btn2" @click="emit('open-analysis', 'forsee')">
<div class="icon">
<img :src="btnIconForsee" alt="" />
</div>
</template>
<template v-else>
<div class="btn3" @click="emit('open-analysis')">
<div class="icon">
<img :src="btnIconAnalysis" alt="" />
</div>
<div class="text">{{ "分析报告" }}</div>
<div class="text">{{ "进展预测" }}</div>
</div>
<div class="btn3" @click="emit('open-analysis', 'analysis')">
<div class="icon">
<img :src="btnIconAnalysis" alt="" />
</div>
</template>
<div class="text">{{ "分析报告" }}</div>
</div>
</div>
</div>
</div>
......@@ -103,7 +81,8 @@
<script setup>
import { computed } from "vue";
import btnIconAnalysis from "@/views/thinkTank/ReportDetail/images/btn-icon3.png";
import btnIconAnalysis from "@/views/bill/billLayout/assets/icons/writting-icon.svg";
import btnIconForsee from "@/views/bill/billLayout/assets/icons/forsee-icon.svg";
const props = defineProps({
billInfo: {
......@@ -347,6 +326,29 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
justify-content: flex-end;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.btn2 {
cursor: pointer;
width: 120px;
height: 36px;
border-radius: 6px;
background: var(--bg-white-100);
border: 1px solid var(--bg-black-10);
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.btn3 {
cursor: pointer;
width: 120px;
......@@ -358,16 +360,6 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
align-items: center;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 24px;
color: rgba(255, 255, 255, 1);
......@@ -387,4 +379,3 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
}
}
</style>
......@@ -2,16 +2,9 @@
<div class="layout-container">
<!-- 导航菜单 -->
<div class="layout-main">
<BillHeader
:billInfo="billInfoGlobal"
:defaultLogo="USALogo"
:tabs="mainHeaderBtnList"
:activeTitle="activeTitle"
:showTabs="showHeaderTabs"
:showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn"
@open-analysis="handleAnalysisClick"
/>
<BillHeader :billInfo="billInfoGlobal" :defaultLogo="USALogo" :tabs="mainHeaderBtnList"
:activeTitle="activeTitle" :showTabs="showHeaderTabs" :showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn" @open-analysis="handleAnalysisClick" />
<div class="layout-main-center">
<router-view />
......@@ -115,12 +108,24 @@ const handleClickMainHeaderBtn = item => {
});
};
const handleAnalysisClick = () => {
const handleAnalysisClick = analysisType => {
const billId = route.query.billId;
if (!billId) return;
// 进展预测 -> 法案简介页(法案进展)
if (analysisType === "forsee") {
router.push({
path: `/billLayout/ProgressForecast/${billId}`,
});
return;
}
// 分析报告 -> 写作助手
router.push({
path: "/writtingAsstaint",
query: {
topic: "法案",
fileId: route.query.billId
fileId: String(billId)
}
});
};
......@@ -149,11 +154,13 @@ watch(
// height: 1016px;
background: rgba(249, 250, 252, 1);
position: relative;
// margin: 0 auto;
.layout-main {
width: 100%;
height: calc(100vh - 72px);
height: 100vh;
overflow-y: auto;
.layout-main-center {
// height: calc(100% - 137px);
width: 1600px;
......
......@@ -150,8 +150,8 @@
</template>
<script setup>
import { ref, onMounted, computed, watch } from "vue";
import { useRoute } from "vue-router";
import { ref, onMounted, onBeforeUnmount, computed, watch } from "vue";
import { useRoute, onBeforeRouteLeave } from "vue-router";
import * as echarts from "echarts";
import { Search } from "@element-plus/icons-vue";
import getPieChart from "./utils/piechart";
......@@ -165,6 +165,31 @@ import { extractTextEntity } from "@/api/intelligent/index";
const route = useRoute();
const pageAbortController = new AbortController();
const isRequestCanceled = error => {
return (
error?.code === "ERR_CANCELED" ||
error?.name === "CanceledError" ||
error?.name === "AbortError" ||
(typeof error?.message === "string" && /canceled|aborted/i.test(error.message))
);
};
const getPageSignal = () => pageAbortController.signal;
const stopCurrentPageRequests = () => {
pageAbortController.abort();
// 让旧请求回调全部失效,避免离开页面后回写状态
tkRequestToken.value += 1;
xzfsRequestToken.value += 1;
hylyRequestToken.value += 1;
entityRequestToken.value += 1;
termsLoading.value = false;
limitLoading.value = false;
domainLoading.value = false;
aiPaneLoading.value = { domain: false, limit: false };
};
const curBill = ref("");
const curBillId = ref(null);
......@@ -294,7 +319,7 @@ const ensureEntitiesForTerms = async terms => {
try {
const results = await Promise.all(
tasks.map(async item => {
const res = await extractTextEntity(item.text);
const res = await extractTextEntity(item.text, { signal: getPageSignal() });
const entities = normalizeEntities(res?.result ?? res?.data?.result ?? res?.data ?? res);
return { key: item.key, entities };
})
......@@ -434,6 +459,7 @@ const requestAiPaneContent = async key => {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
signal: getPageSignal(),
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
......@@ -455,6 +481,7 @@ const requestAiPaneContent = async key => {
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
if (isRequestCanceled(error)) return;
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
......@@ -518,7 +545,7 @@ const handleGetBillList = async () => {
id: route.query.billId
};
try {
const res = await getBillContentId(params);
const res = await getBillContentId(params, { signal: getPageSignal() });
console.log("法案id列表", res);
const rawList = Array.isArray(res?.data) ? res.data : [];
const seen = new Set();
......@@ -579,7 +606,7 @@ const handleGetBillContentTk = async cRelated => {
params.content = searchKeyword.value.trim();
}
try {
const res = await getBillContentTk(params);
const res = await getBillContentTk(params, { signal: getPageSignal() });
if (currentToken !== tkRequestToken.value) {
return;
}
......@@ -627,6 +654,9 @@ const handleGetBillContentTk = async cRelated => {
total.value = 0;
}
} catch (error) {
if (isRequestCanceled(error)) {
return;
}
if (currentToken !== tkRequestToken.value) {
return;
}
......@@ -652,7 +682,7 @@ const handleGetBillContentXzfs = async () => {
};
try {
const res = await getBillContentXzfs(params);
const res = await getBillContentXzfs(params, { signal: getPageSignal() });
if (currentToken !== xzfsRequestToken.value) {
return;
}
......@@ -679,6 +709,9 @@ const handleGetBillContentXzfs = async () => {
let chart1 = getPieChart(chart1Data.value, chart1ColorList.value);
setChart(chart1, "chart1");
} catch (error) {
if (isRequestCanceled(error)) {
return;
}
if (currentToken !== xzfsRequestToken.value) {
return;
}
......@@ -701,7 +734,7 @@ const handleGetBillHyly = async () => {
};
try {
const res = await getBillHyly(params);
const res = await getBillHyly(params, { signal: getPageSignal() });
if (currentToken !== hylyRequestToken.value) {
return;
}
......@@ -729,6 +762,9 @@ const handleGetBillHyly = async () => {
let chart2 = getPieChart(chart2Data.value, chart2ColorList.value);
setChart(chart2, "chart2");
} catch (error) {
if (isRequestCanceled(error)) {
return;
}
if (currentToken !== hylyRequestToken.value) {
return;
}
......@@ -745,6 +781,14 @@ onMounted(async () => {
await handleGetBillContentXzfs();
await handleGetBillHyly();
});
onBeforeRouteLeave(() => {
stopCurrentPageRequests();
});
onBeforeUnmount(() => {
stopCurrentPageRequests();
});
</script>
<style lang="scss" scoped>
......
......@@ -6,7 +6,7 @@
<div class="left-top">
<img src="./assets/icon01.png" alt="" />
<div class="left-top-title">合作限制动态</div>
<div class="more" @click="handleClickToDetail">查看详情 ></div>
<div class="more" @click="handleClickToDetail">{{ "查看详情 >" }}</div>
</div>
<el-carousel ref="carouselRef" height="412px" direction="horizontal" :autoplay="true" :interval="5000"
......@@ -46,7 +46,7 @@
</div>
</div>
<div class="left-center-type" v-if="item.type">{{ item.type }}</div>
<div class="left-center-type" v-if="item.limitMeans">{{ item.limitMeans }}</div>
<!-- <div class="left-center-title">{{ item.LIMITTYPE }}</div> -->
</div>
<div class="left-bottom">
......@@ -188,12 +188,12 @@ const riskSignals = ref([]);
// 点击查看详情
const handleClickToDetail = item => {
const activeItem = item && item.ID ? item : mainTrend.value;
const id = activeItem?.ID;
const id = activeItem?.ID || activeItem?.id || activeItem?.limitId;
if (!id) return;
window.sessionStorage.setItem("curTabName", activeItem?.LIMITNAME);
window.sessionStorage.setItem("cooperationRestrictionsTabName", activeItem?.LIMITNAME || "");
const curRoute = router.resolve({
path: "/cooperationRestrictions/detail",
name: "CooperationRestrictionsDetail",
query: { id: id }
});
window.open(curRoute.href, "_blank");
......@@ -201,9 +201,11 @@ const handleClickToDetail = item => {
// 点击风险信号详情
const handleToRiskDetail = (item) => {
const id = item?.cooperationId || item?.ID || item?.id || item?.limitId;
if (!id) return;
const curRoute = router.resolve({
path: "/cooperationRestrictions/detail",
query: { id: item.cooperationId },
name: "CooperationRestrictionsDetail",
query: { id },
});
window.open(curRoute.href, "_blank");
};
......@@ -318,7 +320,7 @@ onMounted(() => {
width: 967px;
height: 208px;
margin-top: 33px;
margin-left: 62px;
margin-left: 57px;
border-bottom: 1px solid rgb(234, 236, 238);
position: relative;
......@@ -326,6 +328,7 @@ onMounted(() => {
width: 148px;
height: 148px;
margin-right: 21px;
margin-left: 5px;
}
display: flex;
......
......@@ -4,7 +4,8 @@
<div class="left-title">
<img src="./assets/icon01.png" alt="" />
<div class="tit">各类型合作限制政策对比</div>
<el-select v-model="value" placeholder="Select" class="select" @change="getCoopRestrictionCompareData">
<el-select v-model="value" placeholder="Select" class="select" popper-class="coop-select-dropdown"
@change="getCoopRestrictionCompareData">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
......@@ -34,7 +35,8 @@
<div class="right-title">
<img src="./assets/icon02.png" alt="" />
<div class="tit">各领域规则分布情况</div>
<el-select v-model="value1" placeholder="Select" class="select" @change="getCoopRestrictionDomainData">
<el-select v-model="value1" placeholder="Select" class="select" popper-class="coop-select-dropdown"
@change="getCoopRestrictionDomainData">
<el-option v-for="item in options1" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
......@@ -75,6 +77,22 @@ import AiPane from "@/components/base/Ai/AiPane/index.vue";
const COOP_LEFT_TIP_TEXT = "各类型合作限制政策对比,数据来源:美对华科技合作限制信息平台";
const COOP_RIGHT_TIP_TEXT = "各领域规则分布情况,数据来源:美对华科技合作限制信息平台";
// 临时展示 mock(不改样式):右侧“各领域规则分布情况”
// 用完把这个开关改回 false 即可恢复走接口
const USE_DOMAIN_MOCK = false;
const MOCK_COOP_RESTRICTION_DOMAIN = [
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 1, AREA: "人工智能" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 1, AREA: "生物科技" },
{ COOPERTYPE: "技术合作", COOPERTYPECOUNT: 2, AREA: "大数据" },
{ COOPERTYPE: "产学研合作", COOPERTYPECOUNT: 3, AREA: "新能源" },
{ COOPERTYPE: "项目合作", COOPERTYPECOUNT: 1, AREA: "智能制造" },
{ COOPERTYPE: "人才合作", COOPERTYPECOUNT: 2, AREA: "集成电路" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 2, AREA: "大数据" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 3, AREA: "新能源" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 1, AREA: "智能制造" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 2, AREA: "集成电路" }
];
const value = ref(10);
const value1 = ref("2025");
const options = [
......@@ -109,6 +127,10 @@ const options1 = [
const coopRestrictionDomain = ref([]);
const getCoopRestrictionDomainData = async () => {
if (USE_DOMAIN_MOCK) {
coopRestrictionDomain.value = MOCK_COOP_RESTRICTION_DOMAIN;
return;
}
try {
const res = await getCoopRestrictionDomain({
year: value1.value
......@@ -449,10 +471,18 @@ const initLeftChart = () => {
const option = {
color: colorMap.map((c) => c.line),
grid: { left: 40, right: 24, top: 46, bottom: 36 },
// 与智库概览「数量变化趋势」一致:预留图例空间,并用 containLabel 让轴文字不挤压绘图区
grid: {
top: "34%",
right: "3%",
bottom: "5%",
left: "2%",
containLabel: true
},
tooltip: { trigger: "axis", axisPointer: { type: "line" } },
legend: {
top: 8,
left: "center",
icon: "circle",
itemWidth: 12,
itemHeight: 12,
......@@ -572,6 +602,10 @@ const initRightChart = () => {
const domains = Array.from(domainsSet);
const types = Array.from(typesSet);
const legendSplitAt = Math.ceil(types.length / 2);
const legendFirstLine = types.slice(0, legendSplitAt);
const legendSecondLine = types.slice(legendSplitAt);
const indicators = domains.map((domain) => {
const domainData = rawData.filter((item) => item.AREA === domain);
const maxVal = Math.max(...domainData.map((d) => d.COOPERTYPECOUNT), 5);
......@@ -589,31 +623,56 @@ const initRightChart = () => {
name: type,
value: dataValues,
itemStyle: { color: colorMap[index % colorMap.length] },
// 不要填充多边形:让雷达图“圆里面是空的”
// areaStyle 不设置(或设为 0)可避免穿透同心圆的填充效果
// 雷达图围成区域填充:对应颜色 0.1 透明度
areaStyle: { color: colorMap[index % colorMap.length], opacity: 0.1 }
};
});
const option = {
color: colorMap,
legend: {
top: 8,
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
// 避免自动换行导致“第二行不居中”:拆成两行 legend,每行各自居中
legend: [
{
show: true,
type: "plain",
data: legendFirstLine,
top: 8,
left: "center",
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
}
},
data: types
},
{
show: legendSecondLine.length > 0,
type: "plain",
data: legendSecondLine,
top: 32,
left: "center",
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
}
}
],
radar: {
center: ["50%", "55%"],
radius: "65%",
// 对齐左侧折线图(grid top=34%)的“图例到图形”间距:下移雷达中心并略缩半径
center: ["50%", "62%"],
radius: "60%",
indicator: indicators,
axisName: {
color: "rgba(132, 136, 142, 1)",
......@@ -688,6 +747,11 @@ onBeforeUnmount(() => {
padding: 0;
}
/* 合作限制:下拉项内边距(teleport 到 body,用 :global 生效) */
:global(.coop-select-dropdown .el-select-dropdown__item) {
padding: 0 20px !important;
}
.datasub {
width: 1600px;
height: 460px;
......@@ -742,7 +806,7 @@ onBeforeUnmount(() => {
height: 412px;
box-sizing: border-box;
position: relative;
padding: 24px 24px 65px 24px;
padding: 0px 24px 64px 24px;
&.left-main--empty {
display: flex;
......@@ -770,7 +834,7 @@ onBeforeUnmount(() => {
.left-main-echarts {
width: 1015px;
height: 323px;
height: 348px;
}
.source {
......@@ -847,7 +911,7 @@ onBeforeUnmount(() => {
width: 521px;
height: 412px;
box-sizing: border-box;
padding: 24px 24px 64px 24px;
padding: 0px 24px 64px 24px;
position: relative;
&.right-main--empty {
......@@ -874,7 +938,7 @@ onBeforeUnmount(() => {
.right-main-echarts {
width: 473px;
height: 324px;
height: 348px;
}
.source {
......
......@@ -6,7 +6,8 @@
{{ item.name }}
</div>
</div>
<el-select v-model="sortModel" placeholder="发布时间" class="select" :teleported="true" placement="bottom-start"
<el-select v-model="sortModel" placeholder="发布时间" class="select" popper-class="coop-select-dropdown"
:teleported="true" placement="bottom-start"
:popper-options="sortPopperOptions" @change="handleSortChange">
<template #prefix>
<img v-if="sortModel !== true" src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png"
......@@ -120,8 +121,8 @@ const getMainDataList = async () => {
date: item.limitDate,
domain: item.limitArea || [],
type: item.limitMeans,
// 使用默认图片
img: defaultImg
// 优先使用接口返回的机构 logo(limitOrgLogo),空则回退默认图
img: item.limitOrgLogo || defaultImg
}));
total.value = res.data.totalElements || 0;
} else {
......@@ -138,10 +139,16 @@ const getMainDataList = async () => {
const router = useRouter();
const handleClick = item => {
const id = item?.id || item?.limitId || item?.ID;
if (!id) return;
window.sessionStorage.setItem(
"cooperationRestrictionsTabName",
item?.limitName || item?.title || item?.name || ""
);
const routeData = router.resolve({
path: "/cooperationRestrictions/detail",
name: "CooperationRestrictionsDetail",
query: {
id: item.id
id
}
});
window.open(routeData.href, "_blank");
......@@ -355,6 +362,11 @@ watch(currentPage, () => {
padding: 0;
}
/* 合作限制:下拉项内边距(teleport 到 body,用 :global 生效) */
:global(.coop-select-dropdown .el-select-dropdown__item) {
padding: 0 20px !important;
}
.reslib-page {
width: 1600px;
......
......@@ -45,9 +45,9 @@
<AnalysisBox title="相关实体" :showAllBtn="true">
<div class="left-bottom-main">
<div v-for="item in coopRelatedData" :key="item.id" class="main-box" @click="handleClickOnEntity(item)">
<img :src="item.img || defaultCom" alt="" />
<img :src="item.img || defaultCom" alt="" class="img-left-item" />
<div class="name">{{ item.ENTITYNAME }}</div>
<div class="type">{{ item.type }}</div>
<div class="type">{{ item.position }}</div>
</div>
</div>
</AnalysisBox>
......@@ -80,8 +80,15 @@
<span>{{ chineseNumbers[index] }}{{ item.TITLE }} </span>
<img src="./assets/打开按钮.png" alt="">
</div>
<div class="clause-item-content">
{{ item.CONTENT }}
<!-- contentList:单条按原样式展示;多条则逐条展示并加 1.2.3. 前缀 -->
<div v-if="Array.isArray(item.contentList) && item.contentList.length > 1" class="clause-item-content-list">
<div v-for="(row, i) in item.contentList" :key="i" class="clause-item-content-row">
<span class="row-index">{{ i + 1 }}.</span>
<span class="row-text">{{ row.CONTENT }}</span>
</div>
</div>
<div v-else class="clause-item-content-row">
<span class="row-text">{{ item.contentList?.[0]?.CONTENT || "" }}</span>
</div>
</div>
</div>
......@@ -163,19 +170,34 @@ const getcoopRelatedData = async () => {
limitId: route.query.id
});
if (res && res.code === 200) {
coopRelatedData.value = res.data || {};
// 展示图片:优先后端返回的 imageUrl,其次用已有 img 字段,再兜底默认图
coopRelatedData.value = (Array.isArray(res.data) ? res.data : []).map((row) => ({
...row,
img: row?.imageUrl || row?.img || row?.IMAGEURL || row?.image || ""
}));
} else {
coopRelatedData.value = {};
coopRelatedData.value = [];
}
} catch (error) {
console.error("获取合作限制相关实体数据失败:", error);
coopRelatedData.value = {};
coopRelatedData.value = [];
}
};
// 点击跳转关联实体详情
const handleClickOnEntity = (item) => {
if (!item.ENTITYID) return;
const path = `/companyPages/${item.ENTITYID}`;
const entityType = item?.ENTITYTYPE;
// ENTITYTYPE: 'O' 机构/公司;'P' 人物
if (entityType === "P") {
const personId = item?.PERSONID || item?.ENTITYID || item?.id;
if (!personId) return;
const url = `http://localhost:3000/characterPage?type=2&personId=${encodeURIComponent(personId)}`;
window.open(url, "_blank");
return;
}
// 默认按公司/机构跳转(含 ENTITYTYPE === 'O' 或字段缺失)
const companyId = item?.ENTITYID || item?.id;
if (!companyId) return;
const path = `/companyPages/${companyId}`;
const { href } = router.resolve({ path });
window.open(href, "_blank");
};
......@@ -260,11 +282,21 @@ const filteredBackgroundList = computed(() => {
const active2 = ref("涉华条款");
const chineseNumbers = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
const filteredClauseList = computed(() => {
const list = Array.isArray(limitClauseData.value) ? limitClauseData.value : [];
if (active2.value === "全部条款") {
return limitClauseData.value;
} else {
return limitClauseData.value.filter(item => item.ISCN === "Y");
// 展示全部条款及全部段落
return list.map((item) => ({
...item,
contentList: Array.isArray(item?.contentList) ? item.contentList : []
}));
}
// 涉华条款:仅展示 contentList 中 ISCN=Y 的段落;若过滤后为空则不展示该条款
return list
.map((item) => {
const contentList = (Array.isArray(item?.contentList) ? item.contentList : []).filter((row) => row?.ISCN === "Y");
return { ...item, contentList };
})
.filter((item) => Array.isArray(item?.contentList) && item.contentList.length > 0);
});
const dataList = ref([
......@@ -660,6 +692,12 @@ const dataList3 = ref([
img {
width: 24px;
height: 24px;
border-radius: 50%;
display: inline-block;
object-fit: cover;
object-position: center;
}
.name {
......@@ -923,7 +961,12 @@ const dataList3 = ref([
}
}
.clause-item-content {
.clause-item-content-list {
width: 1022px;
}
/* 每条段落的展示:padding / 描边 / 间距保持与旧版单条 CONTENT 一致 */
.clause-item-content-row {
width: 1022px;
padding: 12px 24px 12px 54px;
font-size: 16px;
......@@ -933,6 +976,17 @@ const dataList3 = ref([
color: rgb(59, 65, 75);
border-bottom: 1px solid rgb(234, 236, 238);
white-space: pre-line;
display: flex;
gap: 8px;
.row-index {
flex: 0 0 auto;
}
.row-text {
flex: 1;
min-width: 0;
}
}
}
}
......
......@@ -27,7 +27,7 @@
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="handleDateChange" />
<TimeTabPane @time-click="handleDateChange" activeTime="近一年" />
</div>
<div class="organization-list" ref="refOrganization" v-loading="organizationInfo.loading">
<div class="organization-item" v-for="(item, index) in organizationInfo.list" :key="index"
......@@ -77,7 +77,7 @@ const organizationInfo = reactive({
total: 0,
isSort: 1,
keyWord: "",
day: 7,
day: 365,
list: []
})
......
......@@ -34,7 +34,7 @@
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="onKeyOrganization" />
<TimeTabPane @time-click="onKeyOrganization" activeTime="近一年" />
</div>
<div class="home-main-header-item-box" v-if="keyOrganizationList.length">
<div class="organization-item" v-for="(item, index) in keyOrganizationList" :key="index" @click="handleToInstitution(item)">
......@@ -767,7 +767,7 @@ const handleGetDecreeYearOrder = async () => {
chart1Data.value.dataY = res.data.map(item => {
return item.count;
});
summarize1.value = await onChartInterpretation({type:"柱状图",name:"数量变化趋势",data:res.data})
onChartInterpretation({type:"柱状图",name:"数量变化趋势",data:res.data}, summarize1)
}
} catch (error) {
console.error("行政令发布频度error", error);
......@@ -775,38 +775,52 @@ const handleGetDecreeYearOrder = async () => {
box5Params.loading = false
};
// AI智能总结
const onChartInterpretation = async (text) => {
const response = await fetch('/aiAnalysis/chart_interpretation', {
method: 'POST',
headers: {
"X-API-Key": "aircasKEY19491001",
'Content-Type': 'application/json',
},
body: JSON.stringify({text}) // 把参数转为JSON字符串
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
const onChartInterpretation = async (text, param) => {
param.value = "正在生成..."
let buffer = '';
let summarize = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
// 👇 新增:超时 + 终止请求(只加这一段)
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10秒超时
try {
const response = await fetch('/aiAnalysis/chart_interpretation', {
method: 'POST',
headers: {
"X-API-Key": "aircasKEY19491001",
'Content-Type': 'application/json',
},
body: JSON.stringify({text}),
signal: controller.signal // 👇 新增:绑定中断信号
});
clearTimeout(timeout); // 👇 新增:请求成功清除定时器
if (!response.ok) throw new Error(`HTTP 错误 ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let summarize = '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const content = line.substring(6);
const textMatch = content.match(/"解读":\s*"([^"]*)"/);
if (textMatch && textMatch[1]) summarize = textMatch[1];
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const content = line.substring(6);
const textMatch = content.match(/"解读":\s*"([^"]*)"/);
if (textMatch && textMatch[1]) summarize = textMatch[1];
}
}
}
param.value = summarize
} catch (err) {
param.value = "系统异常,生成失败";
}
return summarize
}
const handleBox5 = async () => {
......@@ -870,7 +884,7 @@ const handleGetDecreeArea = async () => {
value: item.count
};
});
summarize2.value = await onChartInterpretation({type:"环形图",name:"领域分布情况",data:res.data})
onChartInterpretation({type:"环形图",name:"领域分布情况",data:res.data}, summarize2)
}
} catch (error) {
console.error("政令科技领域error", error);
......@@ -1194,7 +1208,7 @@ const handleSearch = () => {
// 关键机构
const keyOrganizationList = ref([]);
const onKeyOrganization = async (event) => {
let day = 7
let day = 365
if (event?.time === '近一周') day = 7
if (event?.time === '近一月') day = 30
if (event?.time === '近一年') day = 365
......
......@@ -79,8 +79,9 @@
<div class="graph-box" v-if="contentType==1">
<ChartChain :listData="fishbone.list" :baseData="fishbone.base" />
</div>
<div class="graph-box" v-if="contentType==2 && graphInfo.nodes.length">
<GraphChart :nodes="graphInfo.nodes" :links="graphInfo.links" layoutType="force" />
<div class="graph-box" v-if="contentType==2">
<GraphChart v-if="graphInfo.nodes?.length" :nodes="graphInfo.nodes" :links="graphInfo.links" layoutType="force" />
<el-empty v-else style="padding: 60px 0" description="暂无数据" :image-size="100" />
</div>
</div>
</AnalysisBox>
......@@ -168,6 +169,7 @@ const onDecreeEntities = async (page=1) => {
const contentType = ref(1);
const headerContentType = (type) => {
contentType.value = type;
if (!entityInfo.total) return;
headerChartData(entityInfo.node)
};
const headerChartData = (row) => {
......
......@@ -84,15 +84,12 @@ const router = useRouter();
const route = useRoute();
const isShowToolBox = computed(() => {
const isShow = route.fullPath.includes('dataLibrary') ? false : true
return isShow
})
const isShowHeader = computed(() => {
const isShow = route.meta.isShowHeader
return isShow? true : false
const isDataLibrary = route.fullPath.includes("dataLibrary");
const isWrittingAsstaint = route.path === "/writtingAsstaint";
return !isDataLibrary && !isWrittingAsstaint;
})
const isShowHeader = computed(() => !!route.meta.isShowHeader);
const isShowAiBox = ref(false);
......@@ -808,4 +805,7 @@ body {
cursor: not-allowed;
pointer-events: none;
}
</style>
<template>
<div class="view-box">
<div class="icon-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="">
</div>
<div class="tips-content">{{ props.tips }}</div>
<div class="icon-right">
<img src="@/assets/icons/box-footer-right-icon.png" alt="">
</div>
</div>
</template>
<script setup lang="ts" name="AiTips">
const props = defineProps({
tips: {
type: String,
default: ''
}
});
</script>
<style scoped lang="scss">
.view-box {
width: 100%;
display: flex;
align-items: center;
padding: 7px 12px;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 250, 255, 1);
.icon-left {
width: 20px;
height: 20px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.tips-content {
color: rgb(5, 95, 194);
font-size: 16px;
font-weight: 400;
line-height: 24px;
margin-left: 13px;
flex: 1;
}
.icon-right {
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
</style>
\ No newline at end of file
<template>
<AnalysisBox :title="props.title" :showAllBtn="false" height="auto">
<div class="box-main">
<div v-for="(item, index) in props.listData" :key="index" class="box-item">
<div class="item-tag">行政令</div>
<div class="item-right">
<div class="item-head">
<div class="item-name one-line-ellipsis">{{ item.name }}</div>
<div class="item-time">{{ item.time }}</div>
</div>
<div class="item-text one-line-ellipsis">{{ item.text }}</div>
</div>
</div>
</div>
</AnalysisBox>
</template>
<script setup lang="ts" name="RelatedEvent">
const props = defineProps({
listData: {
type: Array as any,
default: () => ([])
},
title: {
type: String,
default: ""
}
})
</script>
<style scoped lang="scss">
.box-main {
padding: 0 16px 16px;
.box-item {
border-top: 1px solid var(--bg-black-5);
padding: 6px;
display: flex;
font-size: 16px;
font-family: Source Han Sans CN;
.item-tag {
width: 80px;
height: 28px;
line-height: 28px;
border-radius: 14px;
text-align: center;
margin-right: 16px;
margin-top: 7px;
color: var(--color-yellow-100);
background-color: var(--color-yellow-10);
}
.item-right {
width: 20px;
flex: auto;
line-height: 30px;
.item-head {
display: flex;
.item-name {
width: 20px;
flex: auto;
font-weight: bold;
color: var(--text-primary-80-color);
}
.item-time {
margin-left: 100px;
flex: none;
color: var(--text-primary-65-color);
}
}
.item-text {
color: var(--text-primary-65-color);
}
}
}
.box-item:last-child {
border-bottom: 1px solid var(--bg-black-5);
}
}
</style>
\ No newline at end of file
<template>
<AnalysisBox :title="title" :showAllBtn="false" height="auto">
<el-empty v-if="!props.listData?.length" description="暂无数据" :image-size="200" />
<div v-else class="box-main">
<div class="data-list">
<div class="data-item" v-for="(item, index) in props.listData" :key="index">
<div class="item-head">
<div class="item-name">{{ item.title }}</div>
<div class="button-box">
<div class="button-icon">
<img src="../assets/icons/open.png" alt="" />
</div>
<div class="button-text">跳转原文</div>
</div>
</div>
<div class="item-down">
<div class="item-text" v-for="(text, num) in item.data" :key="num">{{ text }}</div>
</div>
</div>
</div>
<AiTips :tips="tips"></AiTips>
</div>
</AnalysisBox>
</template>
<script setup lang="ts" name="SurveyConclusion">
import AiTips from "@/views/marketAccessRestrictions/com/AiTips.vue";
const props = defineProps({
listData: {
type: Array as any,
default: () => ([])
},
title: {
type: String,
default: ""
},
tips: {
type: String,
default: ""
}
})
</script>
<style scoped lang="scss">
.box-main {
padding: 0 22px 20px;
.data-list {
margin-bottom: 16px;
border-top: 1px solid rgba(234, 236, 238, 1);
.data-item {
.item-head {
padding: 0 20px;
height: 48px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
background: rgba(247, 248, 249, 1);
display: flex;
align-items: center;
.item-name {
width: 20px;
flex: auto;
font-family: Source Han Sans CN;
font-size: 18px;
font-weight: bold;
line-height: 30px;
color: var(--text-primary-80-color);
}
.button-box {
display: flex;
align-items: center;
margin-left: 50px;
.button-icon {
width: 16px;
height: 16px;
font-size: 0;
margin-right: 4px;
img {
width: 100%;
height: 100%;
}
}
.button-text {
color: var(--color-primary-100);
font-family: Microsoft YaHei;
font-size: 12px;
font-weight: 400;
line-height: 12px;
}
}
}
.item-text {
letter-spacing: 1px;
padding: 12px 20px 12px 40px;
color: rgba(59, 65, 75, 1);
font-family: Source Han Sans CN;
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
border-bottom: 1px solid rgba(234, 236, 238, 1);
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="view-box">
<el-empty v-if="!props.surveyList?.length" description="当前条件下暂无数据" :image-size="200" />
<div class="timeline-item" v-for="(item, index) in props.surveyList" :key="item.searchid" @click="onNavigateToDetail(item)">
<div class="timeline-date">
<div class="date-text">{{ item.searchdatezh.slice(0,4) }}</div>
<div class="date-text">{{ item.searchdatezh.slice(5) }}</div>
</div>
<div class="timeline-line-box">
<div class="timeline-icon">
<img v-if="item.sortimageurl" :src="item.sortimageurl" alt="" />
<div v-else class="default-dot"></div>
</div>
<div class="timeline-line" v-if="index !== props.surveyList.length - 1"></div>
</div>
<div class="timeline-content-card">
<div class="item-head">
<div :class="`item-tag tag-${item.sortcode}`">{{ item.sortcode }}</div>
<div class="item-name">{{ item.searchname }}</div>
<div class="item-state">
<span class="dot"></span> {{ item.casestatus }}
</div>
</div>
<div class="card-body">
{{ item.content }}
</div>
<div class="card-footer">
<div class="footer-left-tags">
<AreaTag v-for="(name, num) in item.searchArea" :key="num" :tagName="name"></AreaTag>
</div>
<div class="footer-right-flags">
<div class="flag-icon" v-for="(name, num) in item.countryImage" :key="num">
<img :src="name" alt="" />
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup name="SurveyHistory">
import router from "@/router";
const props = defineProps({
surveyList: {
type: Array,
default: () => ([]),
},
})
const onNavigateToDetail = item => {
window.sessionStorage.setItem("curTabName", item.searchname);
const curRoute = router.resolve({
path: "/marketSingleCaseLayout/overview",
query: { id: item.sortcode, searchId: item.searchid }
});
window.open(curRoute.href, "_blank");
};
</script>
<style scoped lang="scss">
.view-box {
min-height: 600px;
width: 100%;
padding: 18px 27px 0 24px;
border-top: 1px solid rgba(234, 236, 238, 1);
border-bottom: 1px solid rgba(234, 236, 238, 1);
}
.timeline-item {
display: flex;
cursor: pointer;
.timeline-date {
width: 80px;
text-align: right;
margin-right: 16px;
.date-text {
height: 24px;
font-size: 16px;
font-weight: 700;
color: var(--color-main-active);
line-height: 24px;
letter-spacing: 1px;
}
}
.timeline-line-box {
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.timeline-icon {
width: 32px;
height: 32px;
z-index: 2;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 0;
img {
width: 100%;
height: 100%;
}
.default-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: orange;
}
}
.timeline-line {
width: 2px;
flex: 1;
background: #eaeeef;
margin: 4px 0;
}
}
.timeline-content-card {
width: 20px;
flex: auto;
padding: 2px 16px 0;
margin-bottom: 30px;
&:hover .item-head .item-name {
color: var(--color-main-active);
text-decoration: underline;
}
.item-head {
display: flex;
align-items: center;
margin-bottom: 10px;
.item-tag {
width: 48px;
height: 24px;
line-height: 24px;
text-align: center;
border-radius: 4px;
font-weight: bold;
font-size: 16px;
margin-right: 12px;
}
.tag-337 {
border: 1px solid #91caff;
background: #e6f4ff;
color: #055fc2;
}
.tag-232 {
border: 1px solid #b37feb;
background: #f9f0ff;
color: #722ed1;
}
.tag-301 {
border: 1px solid #ffd591;
background: #fff7e6;
color: #fa8c16;
}
.item-name {
font-size: 18px;
line-height: 18px;
font-weight: bold;
color: #3b414b;
width: 20px;
flex: auto;
}
.item-state {
font-size: 16px;
color: #84888e;
color: var(--color-main-active);
margin-left: 100px;
}
}
.card-body {
font-size: 16px;
color: #5f656c;
line-height: 30px;
margin-bottom: 8px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
.card-footer {
display: flex;
gap: 8px;
align-items: center;
.footer-left-tags {
display: flex;
gap: 8px;
.area-tag {
padding: 2px 12px;
background: #e6f7ff;
border: 1px solid #91d5ff;
color: #1890ff;
border-radius: 4px;
font-size: 14px;
}
}
.footer-right-flags {
display: flex;
gap: 4px;
.flag-icon {
width: 12px;
height: 24px;
img {
border-radius: 50%;
border: 1px solid #eee;
width: 24px;
height: 100%;
object-fit: cover;
}
}
}
}
}
}
</style>
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论