提交 8e94a51d authored 作者: yanpeng's avatar yanpeng

Merge branch 'pre' of http://8.140.26.4:10003/caijian/risk-monitor into yp-dev

......@@ -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({
......
......@@ -10,4 +10,11 @@ export function search(data) {
url: `/temporarySearch/search-info/es/page`,
data:data
})
}
export function getThinkTankList() {
return request({
method: 'GET',
url: `/temporarySearch/search-info/all-organization-names`,
})
}
\ No newline at end of file
......@@ -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
......
......@@ -246,6 +246,9 @@ const handleToModule = (item, index) => {
window.sessionStorage.setItem('homeActiveTitleIndex', index)
if (index === 1) {
homeActiveTitleIndex.value = index
menuList.value.forEach(val => {
val.active = false
})
item.active = true
router.push({
path: item.path
......
......@@ -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"
}
},
......
......@@ -11,7 +11,7 @@
`}}
</pre>
<div class="chart-box">
<GraphChart :nodes="nodes" :links="links" layoutType="force">
<GraphChart :nodes="nodes" :links="links" layoutType="none">
</GraphChart>
</div>
</el-col>
......
const getQuarterRange = (quatarNum) => {
const getQuarterRange = (year, quatarNum) => {
const quarters = {
1: ['2025-01-01', '2025-03-31'],
2: ['2025-04-01', '2025-06-30'],
3: ['2025-07-01', '2025-09-30'],
4: ['2025-10-01', '2025-12-31']
1: [year+ '-01-01', year+ '-03-31'],
2: [year+ '-04-01', year+ '-06-30'],
3: [year+ '-07-01', year+ '-09-30'],
4: [year+ '-10-01', year+ '-12-31']
};
return quarters[quatarNum];
......
......@@ -16,67 +16,51 @@ const setChart = (option, chartId, allowClick, selectParam) => {
chart.on('click', function (params) {
switch (selectParam.moduleType) {
case '国会法案':
// 判断点击的是否为饼图的数据项
if (params.componentType === 'series' && params.seriesType === 'pie') {
console.log('点击的扇形名称:', params.name);
if (selectParam.key === '领域') {
selectParam.domains = params.name
if (selectParam.key === 1) {
// console.log('当前点击', selectParam, params.seriesName, params.name);
selectParam.selectedStatus = params.seriesName
selectParam.selectedDate = JSON.stringify(getMonthRange(params.name))
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
return
} else if (selectParam.key === 2) {
selectParam.domains = params.name
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
return
} else if (selectParam.key === 3) {
if (params.name === '众议院' || params.name === '参议院') {
selectParam.selectedCongress = params.name
selectParam.selectedOrg = '全部委员会'
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} else if (selectParam.key === '议院委员会') {
if (params.name === '众议院' || params.name === '参议院') {
selectParam.selectedCongress = params.name
selectParam.selectedOrg = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} else {
selectParam.selectedOrg = params.name
selectParam.selectedCongress = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} else {
selectParam.selectedOrg = params.name
selectParam.selectedCongress = '全部议院'
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') {
if (params.name === '已立法') {
selectParam.selectedStatus = 1
} else {
selectParam.selectedStatus = 0
}
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
return
} else {
selectParam.selectedStatus = params.name
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
} else {
console.log('当前点击', selectParam, params.seriesName, params.name);
if (params.seriesName !== '通过率') {
selectParam.selectedDate = JSON.stringify(getMonthRange(params.name))
if (params.seriesName === '通过法案') {
selectParam.selectedStatus = 1
} else {
selectParam.selectedStatus = null
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
}
}
break
......@@ -89,16 +73,37 @@ const setChart = (option, chartId, allowClick, selectParam) => {
});
window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') {
const quatarNum = Number(params.name[params.name.length - 1])
selectParam.selectedDate = JSON.stringify(getQuarterRange(quatarNum))
selectParam.selectedDate = JSON.stringify(getQuarterRange(selectParam.selectedDate, quatarNum))
const route = router.resolve({
path: "/dataLibrary/dataDecree",
query: selectParam
});
window.open(route.href, "_blank");
}
break
case '科技智库报告':
if (selectParam.key === 1) {
selectParam.domains = params.seriesName
const year = params.name.slice(0,4)
const quatarNum = Number(params.name[params.name.length - 1])
selectParam.selectedDate = JSON.stringify(getQuarterRange(year, quatarNum))
const route = router.resolve({
path: "/dataLibrary/dataThinkTank",
query: selectParam
});
window.open(route.href, "_blank");
return
} else if (selectParam.key === 2) {
selectParam.domains = params.name
const route = router.resolve({
path: "/dataLibrary/dataThinkTank",
query: selectParam
});
window.open(route.href, "_blank");
return
}
}
});
......
......@@ -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
}
]
}
......
......@@ -27,29 +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>
......@@ -73,30 +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="btn2" @click="emit('open-analysis', 'forsee')">
<div class="icon">
<img :src="btnIconForsee" alt="" />
</div>
<div class="text">{{ "进展预测" }}</div>
</div>
<div class="btn3" @click="emit('open-analysis', '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>
......
......@@ -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>
......
......@@ -316,12 +316,12 @@ const handleSearch = async () => {
console.log("综合搜索结果", res);
if (res.code === 200 && res.data) {
if (!selectedDomains.value.length) {
domains.value = Object.entries(res.data.aggregations).map(([name, id]) => ({
domains.value = Object.entries(res.data.aggregationsDomain).map(([name, id]) => ({
name,
id,
selected: false
}));
}
}
searchResults.value = res.data.records;
searchResults.value.forEach(item => {
item.originalTitle = highlightText(item.originalTitle, keyword.value);
......
......@@ -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;
}
}
}
}
......
......@@ -94,11 +94,11 @@
</div>
</div>
<div class="header-right">
<div class="header-right-item item1">
<div class="header-right-item item1" @click="handleExport">
<div class="icon">
<img src="../../assets/icons/download.svg" alt="">
</div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
<div class="text text-tip-1">{{ '导出' }}</div>
</div>
<div class="header-right-item2 item2">
<el-select v-model="curOperation" placeholder="批量操作" style="width: 120px">
......@@ -833,13 +833,12 @@ const fetchTableData = async () => {
}))
}
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
activeChart.value = ''
timer3.value = setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
......@@ -1040,7 +1039,7 @@ const initParam = () => {
}
isInvolveCn.value = route.query.isInvolveCn ? true : false
if (route.query.selectedStatus) {
selectedStatus.value = route.query.selectedStatus === '1' ? '通过' : '提出'
selectedStatus.value = route.query.selectedStatus
} else {
selectedStatus.value = '全部阶段'
}
......@@ -1061,7 +1060,7 @@ const initParam = () => {
}
isInvolveCn.value = savedQuery.isInvolveCn ? true : false
if (savedQuery.selectedStatus) {
selectedStatus.value = savedQuery.selectedStatus === '1' ? '通过' : '提出'
selectedStatus.value = savedQuery.selectedStatus
} else {
selectedStatus.value = '全部阶段'
}
......@@ -1102,6 +1101,10 @@ const handlePerClick = item => {
// 导出
const handleExport = () => {
if (!selectedCount.value) {
ElMessage.warning('请至少选择一项!')
return
}
console.log(selectedMap.value);
const arr = Array.from(selectedMap.value);
......@@ -1111,7 +1114,7 @@ const handleExport = () => {
const link = document.createElement('a');
link.href = url;
link.download = 'export.json';
link.download = 'bill.json';
link.click();
URL.revokeObjectURL(url);
......
<template>
<div class="input-wrapper">
<div class="input-left text-tip-1">{{ inputTitle + ':' }}</div>
<div class="input-right">
<el-input v-model="inputValue" :placeholder="placeholderName" style="width: 240px" />
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
inputTitle: {
type: String,
default: ''
},
placeholderName: {
type: String,
default: ''
},
inputName: {
type: String,
default: ''
},
})
const emit = defineEmits(['update:inputText', 'update:customTime'])
const inputValue = computed({
get: () => props.inputName,
set: (value) => emit('update:inputText', value)
})
</script>
<style lang="scss" scoped>
.input-wrapper {
width: 348px;
height: 28px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
.input-left {
width: 100px;
height: 24px;
color: var(--text-primary-65-color);
}
.input-right {
width: 240px;
display: flex;
gap: 8px;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 4px;
border: 1px solid var(--bg-black-10);
}
}
}
</style>
\ No newline at end of file
......@@ -101,11 +101,11 @@
</div>
</div>
<div class="header-right">
<div class="header-right-item item1">
<div class="header-right-item item1" @click="handleExport">
<div class="icon">
<img src="../assets/icons/download.svg" alt="">
</div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
<div class="text text-tip-1">{{ '导出' }}</div>
</div>
<div class="header-right-item2 item2">
<el-select v-model="curOperation" placeholder="批量操作" style="width: 120px">
......@@ -137,7 +137,7 @@
<el-table-column label="发布时间" width="120" class-name="date-column">
<template #default="scope">{{ scope.row.date }}</template>
</el-table-column>
<el-table-column label="发布机构" width="180">
<el-table-column label="发布机构">
<template #default="scope">
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.organizationName
}}</span>
......@@ -396,6 +396,7 @@ const handleCloseCurTag = (tag, index) => {
break
case '发布时间':
selectedDate.value = ''
customTime.value = []
break
case '发布机构':
selectedIns.value = '全部机构'
......@@ -765,6 +766,8 @@ const fetchTableData = async () => {
return item.name === curDemension.value
})[0]
activeChart.value = ''
setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
......@@ -989,12 +992,12 @@ const initParam = () => {
const handleClickToDetail = (curDecree) => {
console.log('curDecree', curDecree);
window.sessionStorage.setItem("billId", curDecree.id);
window.sessionStorage.setItem("decreeId", curDecree.id);
window.sessionStorage.setItem("curTabName", curDecree.title);
const route = router.resolve({
path: "/decreeLayout",
query: {
billId: curDecree.id
id: curDecree.id
}
});
window.open(route.href, "_blank");
......@@ -1340,4 +1343,6 @@ onMounted(async () => {
// :deep(.el-table__header th:first-child) {
// background-color: #e6f7ff;
// color: #1890ff;
// }</style>
\ No newline at end of file
// }
</style>
\ No newline at end of file
......@@ -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) => {
......
<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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论