提交 5466ba6a authored 作者: coderBryanFu's avatar coderBryanFu

feat:新增通用组件AiSummary

...@@ -235,12 +235,26 @@ body { ...@@ -235,12 +235,26 @@ body {
display: none; display: none;
} }
/* #region 公共样式类名 */
/* 单行文本溢出隐藏显示省略号 */ /* 单行文本溢出隐藏显示省略号 */
.one-line-ellipsis { .one-line-ellipsis {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
/* 多行文本两端对齐 最后一行正常显示 */
.text-align-justify {
text-align: justify;
text-align-last: left;
-webkit-text-align-last: left;
}
/* 可点击文本 鼠标悬浮样式 */
#app .text-click-hover:hover {
text-decoration: underline;
color: rgb(5, 95, 194);
cursor: pointer;
}
/* #endregion 公共样式类名 */
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -192,3 +192,30 @@ export function getBillFullText(params) { ...@@ -192,3 +192,30 @@ export function getBillFullText(params) {
params, params,
}) })
} }
// 条款对比-根据两版版本与筛选条件获取配对条款列表
/**
* @param {billId,oldVersionId,newVersionId,diffType,cRelated,keyword}
* @header token
* @returns { list: Array<{ oldTerm: object|null, newTerm: object|null }> }
*/
export function getBillTermsCompare(params) {
return request({
method: "GET",
url: "/api/billInfoBean/content/compare",
params,
});
}
// 版本对比-根据两版版本与筛选条件获取条款列表(分页)
/**
* @param {billId,content,currentPage,currentVersion,isCn,originalVersion,pageSize,status}
* @header token
*/
export function getBillVersionCompare(params) {
return request({
method: "GET",
url: "/api/billInfoBean/versionCompare",
params,
});
}
import request from "@/api/request.js"; import request from "@/api/request.js";
// 最新科技政令 // 最新科技政令
export function getDepartmentList() { export function getDepartmentList(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeDict/department`, url: `/api/administrativeDict/department`,
...@@ -43,10 +43,10 @@ export function getDecreeArea(params) { ...@@ -43,10 +43,10 @@ export function getDecreeArea(params) {
} }
// 关键行政令 // 关键行政令
export function getKeyDecree() { export function getKeyDecree(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeOrderOverview/action`, url: `/api/administrativeOrderOverview/action?pageSize=${params.pageSize}&pageNum=${params.pageNum}`,
}) })
} }
......
...@@ -239,6 +239,8 @@ watch(isTranslate, () => { ...@@ -239,6 +239,8 @@ watch(isTranslate, () => {
background-color: white; background-color: white;
padding: 0 60px; padding: 0 60px;
flex: auto; flex: auto;
height: 100%;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.report-header { .report-header {
...@@ -324,9 +326,14 @@ watch(isTranslate, () => { ...@@ -324,9 +326,14 @@ watch(isTranslate, () => {
.report-main { .report-main {
flex: auto; flex: auto;
min-height: 0;
box-sizing: border-box; box-sizing: border-box;
padding-top: 10px; padding-top: 10px;
:deep(.el-scrollbar) {
height: 100%;
}
.no-content { .no-content {
height: 100%; height: 100%;
display: flex; display: flex;
......
...@@ -133,11 +133,12 @@ const emit = defineEmits(['save', 'download', 'collect']) ...@@ -133,11 +133,12 @@ const emit = defineEmits(['save', 'download', 'collect'])
} }
} }
// .header-btn { .header-btn {
// display: flex; // display: flex;
// justify-content: flex-end; // justify-content: flex-end;
// gap: 8px; // gap: 8px;
// } margin-right: 10px;
}
// .header-btn1 { // .header-btn1 {
// position: absolute; // position: absolute;
......
...@@ -13,7 +13,7 @@ const BillInfluenceLayout = () => import('@/views/bill/influence/index.vue') ...@@ -13,7 +13,7 @@ const BillInfluenceLayout = () => import('@/views/bill/influence/index.vue')
const BillInfluenceIndustry = () => import('@/views/bill/influence/industry/index.vue') const BillInfluenceIndustry = () => import('@/views/bill/influence/industry/index.vue')
const BillInfluenceScientificResearch = () => import('@/views/bill/influence/scientificResearch/index.vue') const BillInfluenceScientificResearch = () => import('@/views/bill/influence/scientificResearch/index.vue')
const BillRelevantCircumstance = () => import('@/views/bill/relevantCircumstance/index.vue') const BillRelevantCircumstance = () => import('@/views/bill/relevantCircumstance/index.vue')
const BillOriginalText = () => import('@/views/bill/billOriginalText/index.vue') const BillVersionCompare = () => import('@/views/bill/versionCompare/index.vue')
const billRoutes = [ const billRoutes = [
...@@ -36,14 +36,6 @@ const billRoutes = [ ...@@ -36,14 +36,6 @@ const billRoutes = [
dynamicTitle: true // 标记需要动态设置标题 dynamicTitle: true // 标记需要动态设置标题
}, },
children: [ children: [
{
path: "originalText",
name: "BillOriginalText",
component: BillOriginalText,
meta: {
title: "法案原文"
}
},
// 法案分析路由 // 法案分析路由
{ {
path: "bill", path: "bill",
...@@ -138,6 +130,14 @@ const billRoutes = [ ...@@ -138,6 +130,14 @@ const billRoutes = [
// meta: { // meta: {
// title: "相关情况" // title: "相关情况"
// } // }
},
{
path: "versionCompare",
name: "BillVersionCompare",
component: BillVersionCompare,
meta: {
title: "版本对比"
}
} }
] ]
}, },
......
...@@ -18,7 +18,7 @@ const decreeRoutes = [ ...@@ -18,7 +18,7 @@ const decreeRoutes = [
name: "Decree", name: "Decree",
component: Decree, component: Decree,
meta: { meta: {
title: "政令概览" title: "科技政令概况"
} }
}, },
{ {
......
...@@ -204,16 +204,20 @@ ...@@ -204,16 +204,20 @@
<div class="coop-members"> <div class="coop-members">
<div class="coop-member"> <div class="coop-member">
<img class="coop-avatar" :src="item.avatar || defaultAvatar" alt="committee-avatar" /> <img class="coop-avatar" :src="item.avatar || defaultAvatar" alt="committee-avatar" />
<div class="coop-member-info">
<div class="coop-member-name" :title="item.name">{{ item.name }}</div> <div class="coop-member-name" :title="item.name">{{ item.name }}</div>
<div v-if="item.nameEn" class="coop-member-name-en" :title="item.nameEn">
{{ item.nameEn }}
</div>
</div> </div>
</div> </div>
<div class="coop-summary" :title="item.desc">
{{ item.desc }}
</div> </div>
<!-- <div class="coop-summary" :title="item.desc">
{{ item.desc }}
</div> -->
<div class="coop-count"> <div class="coop-count">
{{ `${item.proposalSize ?? (item.bills || []).length}项重点法案` }} {{ `${item.proposalSize ?? (item.bills || []).length}项重点法案` }}
</div> </div>
<slot name="committee-extra" :committee="item" />
</div> </div>
<div class="coop-proposals"> <div class="coop-proposals">
<div <div
...@@ -447,6 +451,7 @@ const handleGetCommitteeList = async () => { ...@@ -447,6 +451,7 @@ const handleGetCommitteeList = async () => {
const descText = billInfoPage[0]?.originDepart || ""; const descText = billInfoPage[0]?.originDepart || "";
return { return {
id: item.id, id: item.id,
nameEn:item.nameEn || "",
avatar: "", avatar: "",
name: item.name || "-", name: item.name || "-",
desc: descText, desc: descText,
...@@ -1232,6 +1237,12 @@ onMounted(() => { ...@@ -1232,6 +1237,12 @@ onMounted(() => {
min-width: 0; min-width: 0;
} }
.coop-member-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.coop-avatar { .coop-avatar {
width: 40px; width: 40px;
height: 40px; height: 40px;
...@@ -1273,6 +1284,17 @@ onMounted(() => { ...@@ -1273,6 +1284,17 @@ onMounted(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.coop-member-name-en {
color: var(--text-primary-65-color);
font-family: "Microsoft YaHei";
font-size: 14px;
font-weight: 400;
line-height: 22px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.coop-count { .coop-count {
flex-shrink: 0; flex-shrink: 0;
color: #1459bb; color: #1459bb;
...@@ -1284,7 +1306,6 @@ onMounted(() => { ...@@ -1284,7 +1306,6 @@ onMounted(() => {
} }
.coop-proposals { .coop-proposals {
margin-top: 10px;
padding-top: 10px; padding-top: 10px;
border-top: 1px solid #eaeced; border-top: 1px solid #eaeced;
display: grid; display: grid;
......
...@@ -127,7 +127,13 @@ ...@@ -127,7 +127,13 @@
<el-empty v-if="!box5HasData" description="暂无数据" :image-size="100" /> <el-empty v-if="!box5HasData" description="暂无数据" :image-size="100" />
<div v-else id="box5Chart" class="overview-chart"></div> <div v-else id="box5Chart" class="overview-chart"></div>
</div> </div>
<div class="overview-tip-row">
<TipTab class="overview-tip" /> <TipTab class="overview-tip" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box5')" />
</div>
<div v-if="aiPaneVisible.box5" class="overview-ai-pane" @mouseleave="handleHideAiPane('box5')">
<AiPane :aiContent="overviewAiContent.box5" />
</div>
</div> </div>
</OverviewCard> </OverviewCard>
<OverviewCard class="overview-card--single box6" title="涉华法案领域分布" :icon="box6HeaderIcon"> <OverviewCard class="overview-card--single box6" title="涉华法案领域分布" :icon="box6HeaderIcon">
...@@ -141,7 +147,13 @@ ...@@ -141,7 +147,13 @@
<el-empty v-if="!box9HasData" description="暂无数据" :image-size="100" /> <el-empty v-if="!box9HasData" description="暂无数据" :image-size="100" />
<div v-else id="box9Chart" class="overview-chart"></div> <div v-else id="box9Chart" class="overview-chart"></div>
</div> </div>
<div class="overview-tip-row">
<TipTab class="overview-tip" /> <TipTab class="overview-tip" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box6')" />
</div>
<div v-if="aiPaneVisible.box6" class="overview-ai-pane" @mouseleave="handleHideAiPane('box6')">
<AiPane :aiContent="overviewAiContent.box6" />
</div>
</div> </div>
</OverviewCard> </OverviewCard>
</div> </div>
...@@ -157,7 +169,13 @@ ...@@ -157,7 +169,13 @@
<el-empty v-if="!box7HasData" description="暂无数据" :image-size="100" /> <el-empty v-if="!box7HasData" description="暂无数据" :image-size="100" />
<div v-else id="box7Chart" class="overview-chart"></div> <div v-else id="box7Chart" class="overview-chart"></div>
</div> </div>
<div class="overview-tip-row">
<TipTab class="overview-tip" /> <TipTab class="overview-tip" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box7')" />
</div>
<div v-if="aiPaneVisible.box7" class="overview-ai-pane" @mouseleave="handleHideAiPane('box7')">
<AiPane :aiContent="overviewAiContent.box7" />
</div>
</div> </div>
</OverviewCard> </OverviewCard>
<OverviewCard class="overview-card--single box8" title="涉华法案进展分布" :icon="box7HeaderIcon"> <OverviewCard class="overview-card--single box8" title="涉华法案进展分布" :icon="box7HeaderIcon">
...@@ -174,7 +192,13 @@ ...@@ -174,7 +192,13 @@
<div id="box8Chart" class="overview-chart box8-chart"></div> <div id="box8Chart" class="overview-chart box8-chart"></div>
</template> </template>
</div> </div>
<div class="overview-tip-row">
<TipTab class="overview-tip" /> <TipTab class="overview-tip" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box8')" />
</div>
<div v-if="aiPaneVisible.box8" class="overview-ai-pane" @mouseleave="handleHideAiPane('box8')">
<AiPane :aiContent="overviewAiContent.box8" />
</div>
</div> </div>
</OverviewCard> </OverviewCard>
<OverviewCard class="overview-card--single box9" title="涉华法案关键条款" :icon="box7HeaderIcon"> <OverviewCard class="overview-card--single box9" title="涉华法案关键条款" :icon="box7HeaderIcon">
...@@ -183,7 +207,13 @@ ...@@ -183,7 +207,13 @@
<el-empty v-if="!wordCloudHasData" description="暂无数据" :image-size="100" /> <el-empty v-if="!wordCloudHasData" description="暂无数据" :image-size="100" />
<WordCloundChart v-else class="overview-chart" width="100%" height="100%" :data="wordCloudData" /> <WordCloundChart v-else class="overview-chart" width="100%" height="100%" :data="wordCloudData" />
</div> </div>
<div class="overview-tip-row">
<TipTab class="overview-tip" /> <TipTab class="overview-tip" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box9')" />
</div>
<div v-if="aiPaneVisible.box9" class="overview-ai-pane" @mouseleave="handleHideAiPane('box9')" >
<AiPane :aiContent="overviewAiContent.box9" />
</div>
</div> </div>
</OverviewCard> </OverviewCard>
</div> </div>
...@@ -221,6 +251,8 @@ import OverviewCard from "./OverviewCard.vue"; ...@@ -221,6 +251,8 @@ import OverviewCard from "./OverviewCard.vue";
import ResourceLibrarySection from "./ResourceLibrarySection.vue"; import ResourceLibrarySection from "./ResourceLibrarySection.vue";
import { useContainerScroll } from "@/hooks/useScrollShow"; import { useContainerScroll } from "@/hooks/useScrollShow";
import TipTab from "@/components/base/TipTab/index.vue"; import TipTab from "@/components/base/TipTab/index.vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import WordCloundChart from "@/components/base/WordCloundChart/index.vue"; import WordCloundChart from "@/components/base/WordCloundChart/index.vue";
import getMultiLineChart from "./utils/multiLineChart"; import getMultiLineChart from "./utils/multiLineChart";
...@@ -370,6 +402,36 @@ const box7YearList = ref([ ...@@ -370,6 +402,36 @@ const box7YearList = ref([
} }
]); ]);
const aiPaneVisible = ref({
box5: false,
box6: false,
box7: false,
box8: false,
box9: false
});
const overviewAiContent = ref({
box5: "智能总结生成中...",
box6: "智能总结生成中...",
box7: "智能总结生成中...",
box8: "智能总结生成中...",
box9: "智能总结生成中..."
});
const handleShowAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: true
};
};
const handleHideAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: false
};
};
const box8selectetedTime = ref("2025"); const box8selectetedTime = ref("2025");
const box8YearList = ref([ const box8YearList = ref([
{ {
...@@ -2109,6 +2171,20 @@ onUnmounted(() => { ...@@ -2109,6 +2171,20 @@ onUnmounted(() => {
.overview-card-body { .overview-card-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
}
.overview-ai-pane {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
z-index: 3;
pointer-events: none;
.ai-pane-wrapper {
pointer-events: auto;
}
} }
.overview-chart-wrap { .overview-chart-wrap {
...@@ -2131,8 +2207,17 @@ onUnmounted(() => { ...@@ -2131,8 +2207,17 @@ onUnmounted(() => {
min-height: 0; min-height: 0;
} }
.overview-tip { .overview-tip-row {
margin-top: 10px; margin-top: 10px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.overview-tip-action {
position: absolute;
right: -30px;
} }
} }
} }
......
...@@ -78,14 +78,6 @@ ...@@ -78,14 +78,6 @@
<div class="right-box-bottom" v-if="showActions"> <div class="right-box-bottom" v-if="showActions">
<template v-if="isLoading"> <template v-if="isLoading">
<div class="btn1 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>
<div class="btn3 is-skeleton"> <div class="btn3 is-skeleton">
<div class="icon"> <div class="icon">
<el-skeleton-item class="skeleton-action-icon" variant="text" /> <el-skeleton-item class="skeleton-action-icon" variant="text" />
...@@ -96,13 +88,6 @@ ...@@ -96,13 +88,6 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="btn1" @click="emit('open-original-text')">
<div class="icon">
<img :src="btnIconOriginalText" alt="" />
</div>
<div class="text">{{ "法案原文" }}</div>
</div>
<div class="btn3" @click="emit('open-analysis')"> <div class="btn3" @click="emit('open-analysis')">
<div class="icon"> <div class="icon">
<img :src="btnIconAnalysis" alt="" /> <img :src="btnIconAnalysis" alt="" />
...@@ -118,7 +103,6 @@ ...@@ -118,7 +103,6 @@
<script setup> <script setup>
import { computed } from "vue"; import { computed } from "vue";
import btnIconOriginalText from "@/views/thinkTank/ReportDetail/images/btn-icon1.png";
import btnIconAnalysis from "@/views/thinkTank/ReportDetail/images/btn-icon3.png"; import btnIconAnalysis from "@/views/thinkTank/ReportDetail/images/btn-icon3.png";
const props = defineProps({ const props = defineProps({
...@@ -150,7 +134,7 @@ const props = defineProps({ ...@@ -150,7 +134,7 @@ const props = defineProps({
const isLoading = computed(() => !props.billInfo || !props.billInfo.billName); const isLoading = computed(() => !props.billInfo || !props.billInfo.billName);
const emit = defineEmits(["tab-click", "open-original-text", "open-analysis"]); const emit = defineEmits(["tab-click", "open-analysis"]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -363,41 +347,6 @@ const emit = defineEmits(["tab-click", "open-original-text", "open-analysis"]); ...@@ -363,41 +347,6 @@ const emit = defineEmits(["tab-click", "open-original-text", "open-analysis"]);
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 8px;
.btn1 {
cursor: pointer;
width: 120px;
height: 36px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
.btn3 { .btn3 {
cursor: pointer; cursor: pointer;
width: 120px; width: 120px;
......
...@@ -7,10 +7,9 @@ ...@@ -7,10 +7,9 @@
:defaultLogo="USALogo" :defaultLogo="USALogo"
:tabs="mainHeaderBtnList" :tabs="mainHeaderBtnList"
:activeTitle="activeTitle" :activeTitle="activeTitle"
:showTabs="!isBillOriginalTextPage" :showTabs="showHeaderTabs"
:showActions="!isBillOriginalTextPage" :showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn" @tab-click="handleClickMainHeaderBtn"
@open-original-text="handleOpenBillOriginalText"
@open-analysis="handleAnalysisClick" @open-analysis="handleAnalysisClick"
/> />
...@@ -22,14 +21,13 @@ ...@@ -22,14 +21,13 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed, watch } from "vue"; import { ref, onMounted, watch } from "vue";
import router from "@/router"; import router from "@/router";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { getBillInfoGlobal } from "@/api/bill"; import { getBillInfoGlobal } from "@/api/bill";
import BillHeader from "./components/BillHeader.vue"; import BillHeader from "./components/BillHeader.vue";
const route = useRoute(); const route = useRoute();
const isBillOriginalTextPage = computed(() => route.path === "/billLayout/originalText");
import icon1 from "./assets/icons/icon1.svg"; import icon1 from "./assets/icons/icon1.svg";
import icon1Active from "./assets/icons/icon1_active.svg"; import icon1Active from "./assets/icons/icon1_active.svg";
...@@ -55,16 +53,6 @@ const getBillInfoGlobalFn = async () => { ...@@ -55,16 +53,6 @@ const getBillInfoGlobalFn = async () => {
} }
}; };
const handleOpenBillOriginalText = () => {
const targetRoute = router.resolve({
path: "/billLayout/originalText",
query: {
billId: route.query.billId
}
});
window.open(targetRoute.href, "_blank");
};
const mainHeaderBtnList = ref([ const mainHeaderBtnList = ref([
{ {
icon: icon1, icon: icon1,
...@@ -93,6 +81,8 @@ const mainHeaderBtnList = ref([ ...@@ -93,6 +81,8 @@ const mainHeaderBtnList = ref([
]); ]);
const activeTitle = ref("法案概况"); const activeTitle = ref("法案概况");
const showHeaderTabs = ref(true);
const showHeaderActions = ref(true);
const getActiveTitleByRoutePath = path => { const getActiveTitleByRoutePath = path => {
if (path.startsWith("/billLayout/deepDig")) return "深度挖掘"; if (path.startsWith("/billLayout/deepDig")) return "深度挖掘";
...@@ -102,8 +92,12 @@ const getActiveTitleByRoutePath = path => { ...@@ -102,8 +92,12 @@ const getActiveTitleByRoutePath = path => {
return "法案概况"; return "法案概况";
}; };
const syncActiveTitleFromRoute = () => { const syncHeaderStateFromRoute = () => {
const currentPath = route.path || "";
activeTitle.value = getActiveTitleByRoutePath(route.path); activeTitle.value = getActiveTitleByRoutePath(route.path);
const isVersionCompare = currentPath.startsWith("/billLayout/versionCompare");
showHeaderTabs.value = !isVersionCompare;
showHeaderActions.value = !isVersionCompare;
}; };
const handleClickMainHeaderBtn = item => { const handleClickMainHeaderBtn = item => {
...@@ -134,7 +128,7 @@ const handleAnalysisClick = () => { ...@@ -134,7 +128,7 @@ const handleAnalysisClick = () => {
onMounted(() => { onMounted(() => {
getBillInfoGlobalFn(); getBillInfoGlobalFn();
// 以当前路由为准,避免 sessionStorage 造成高亮错乱 // 以当前路由为准,避免 sessionStorage 造成高亮错乱
syncActiveTitleFromRoute(); syncHeaderStateFromRoute();
// 兜底:如果未来出现未知路由且有缓存,再用缓存 // 兜底:如果未来出现未知路由且有缓存,再用缓存
const cachedTitle = window.sessionStorage.getItem("activeTitle"); const cachedTitle = window.sessionStorage.getItem("activeTitle");
if (!activeTitle.value && cachedTitle) activeTitle.value = cachedTitle; if (!activeTitle.value && cachedTitle) activeTitle.value = cachedTitle;
...@@ -143,7 +137,7 @@ onMounted(() => { ...@@ -143,7 +137,7 @@ onMounted(() => {
watch( watch(
() => route.path, () => route.path,
() => { () => {
syncActiveTitleFromRoute(); syncHeaderStateFromRoute();
}, },
{ immediate: true } { immediate: true }
); );
......
...@@ -697,6 +697,7 @@ onMounted(() => { ...@@ -697,6 +697,7 @@ onMounted(() => {
right: 84px; right: 84px;
top: 15px; top: 15px;
.btn { .btn {
height: 28px; height: 28px;
padding: 0 8px; padding: 0 8px;
......
<template> <template>
<div class="process-overview-wrap"> <div class="process-overview-wrap">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">流程概要</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="main">
<div class="left" :style="{ width: (maxLineWidth + 250) + 'px' }">
<div class="top">
<div class="top-line" :style="{ width: lineWidth }">
<div class="top-line1"></div>
</div>
<div class="start">
<div class="icon">
<img src="./assets/images/logo1.png" alt="" />
</div>
<div class="name">{{ "参议院" }}</div>
</div>
<div class="content-box" :style="senateBoxStyle">
<div class="item-box" v-for="(item, index) in senateList" :key="item.id" style="width: 280px; flex-shrink: 0;">
<div class="item-box-dot">
<img src="./assets/images/top-line-dot.png" alt="" />
</div>
<div class="item-content">
<div class="item-header">
<div class="item-title" :title="item.actionTitle">
{{ item.actionTitle }} <span v-if="item.versionId">({{ item.versionId }})</span>
</div>
<div class="item-header-icon" @click="handleClickDetail(true, item, $event)">
<img src="./assets/images/item-header-icon.png" alt="" />
</div>
</div>
<div class="item-info" v-if="item.agreeVote !== null || item.disagreeVote !== null">
{{ (item.agreeVote || 0) + "赞成:" + (item.disagreeVote || 0) + "反对" }}
</div>
<div class="item-main" v-if="item.fynrList && item.fynrList.length">
<div class="item-main-item" v-for="(sub, subIndex) in item.fynrList" :key="subIndex">
<div class="icon"></div>
<CommonPrompt :content="sub">
<div class="text">{{ sub }}</div>
</CommonPrompt>
</div>
</div>
</div>
<div class="item-time">
{{ item.actionDate }}
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="bottom-line" :style="{ width: lineWidth }">
<div class="bottom-line1"></div>
</div>
<div class="start">
<div class="name">{{ "众议院" }}</div>
<div class="icon">
<img src="./assets/images/logo2.png" alt="" />
</div>
</div>
<div class="content-box" :style="houseBoxStyle">
<div class="item-box" v-for="(item, index) in houseList" :key="item.id" style="width: 280px; flex-shrink: 0;">
<div class="item-time">
{{ item.actionDate }}
</div>
<div class="item-box-dot">
<img src="./assets/images/bottom-line-dot.png" alt="" />
</div>
<div class="item-content">
<div class="item-header">
<div class="item-title" :title="item.actionTitle">
{{ item.actionTitle }} <span v-if="item.versionId">({{ item.versionId }})</span>
</div>
<div class="item-header-icon" @click="handleClickDetail(true, item, $event)">
<img src="./assets/images/item-header-icon.png" alt="" />
</div>
</div>
<div class="item-info" v-if="item.agreeVote !== null || item.disagreeVote !== null">
{{ (item.agreeVote || 0) + "赞成:" + (item.disagreeVote || 0) + "反对" }}
</div>
<div class="item-main" v-if="item.fynrList && item.fynrList.length">
<div class="item-main-item" v-for="(sub, subIndex) in item.fynrList" :key="subIndex">
<div class="icon"></div>
<CommonPrompt :content="sub">
<div class="text">{{ sub }}</div>
</CommonPrompt>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="right" :style="{ left: rightPos }">
<div class="junction-dot">
<div class="inner-dot"></div>
</div>
<div class="right-line"></div>
</div>
</div>
</div> -->
<AnalysisBox title="流程概要" :showAllBtn="false"> <AnalysisBox title="流程概要" :showAllBtn="false">
<div class="main"> <div class="main">
<div class="left" :style="{ width: (maxLineWidth + 250) + 'px' }"> <div class="left" :style="{ width: (maxLineWidth + 250) + 'px' }">
...@@ -207,17 +99,6 @@ ...@@ -207,17 +99,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="footer">
<div class="footer-left">
<img src="./assets/icons/right-icon1.png" alt="" />
</div>
<div class="footer-center">
立法过程始于2025年2月共和党启动预算框架谈判,5月22日众议院以215:214的1票优势通过初始版本;随后参议院历经16小时全文朗读和马拉松式辩论,于7月1日以51:50的票数通过修订版(副总统万斯投关键票);因参议院版本大改(如永久化减税、提高债限至5万亿美元),法案重返众议院审议,经程序性投票(219:213)和最终表决(218:214),于7月3日深夜通过;特朗普在7月4日独立日签署生效,全程凸显两党对立、党内分歧及程序博弈。
</div>
<div class="footer-right">
<img src="./assets/icons/arrow-right.png" alt="" />
</div>
</div> -->
</AnalysisBox> </AnalysisBox>
<ProcessOverviewDetailDialog <ProcessOverviewDetailDialog
......
...@@ -29,7 +29,7 @@ const siderBtnList = ref([ ...@@ -29,7 +29,7 @@ const siderBtnList = ref([
path: '/billLayout/bill/background' path: '/billLayout/bill/background'
}, },
{ {
name: '内容概要', name: '法案原文',
path: '/billLayout/bill/template' path: '/billLayout/bill/template'
}, },
]) ])
...@@ -38,7 +38,7 @@ const siderBtnActive = ref("法案简介"); ...@@ -38,7 +38,7 @@ const siderBtnActive = ref("法案简介");
const getSiderActiveByRoutePath = path => { const getSiderActiveByRoutePath = path => {
if (path.includes("/billLayout/bill/background")) return "法案背景"; if (path.includes("/billLayout/bill/background")) return "法案背景";
if (path.includes("/billLayout/bill/template")) return "内容概要"; if (path.includes("/billLayout/bill/template")) return "法案原文";
return "法案简介"; return "法案简介";
}; };
......
<template> <template>
<div class="introduction-wrap"> <div class="introduction-wrap">
<WarningPane
v-if="riskSignal"
class="risk-signal-pane-top"
:warnningLevel="riskSignal.riskLevel"
:warnningContent="riskSignal.riskContent"
/>
<div class="introduction-wrap-content">
<div class="introduction-wrap-left"> <div class="introduction-wrap-left">
<div class="introduction-wrap-left-box1"> <div class="introduction-wrap-left-box1">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">基本信息</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box1-main">
<div class="box1-left">
<img :src=" basicInfo.imageUrl || defaultBill" alt="" />
</div>
<div class="box1-right">
<div class="box1-right-item">
<div class="item-left">提案人:</div>
<div class="item-right">{{ basicInfo.tarName }}</div>
</div>
<div class="box1-right-item">
<div class="item-left">提出时间:</div>
<div class="item-right">{{ basicInfo.introductionDate }}</div>
</div>
<div class="box1-right-item">
<div class="item-left">相关领域:</div>
<div class="item-right1">
<div class="right1-item" v-for="item in basicInfo.hylyList" :key="item">{{ item }}</div>
</div>
</div>
<div class="box1-right-item">
<div class="item-left">委员会报告:</div>
<div class="item-right2" v-if="basicInfo.reportList">
<div class="right2-item" v-for="(item, index) in basicInfo.reportList" :key="index">
{{ item }}
</div>
</div>
</div>
<div class="box1-right-item">
<div class="item-left">表决记录:</div>
<div class="item-right3">{{ `全程共进行${basicInfo.votetotal}次唱名表决` }}</div>
</div>
<div class="box1-right-item">
<div class="item-left">最近状态:</div>
<div class="item-right3">{{ basicInfo.status }}</div>
</div>
<div class="box1-right-item">
<div class="item-left">立案流程:</div>
<div class="item-right4">
<div
class="step"
v-for="(item, index) in basicInfo.stageList ? [...basicInfo.stageList].reverse() : []"
:key="index"
:style="{ zIndex: (basicInfo.stageList?.length || 0) - index }"
>
<div
class="step-box"
:class="{ 'step-box-active': index === (basicInfo.stageList?.length || 0) - 1 }"
>
{{ item }}
</div>
</div>
</div>
</div>
</div>
</div> -->
<AnalysisBox title="基本信息" :showAllBtn="false"> <AnalysisBox title="基本信息" :showAllBtn="false">
<div class="box1-main"> <div class="box1-main">
<div class="box1-left"> <div class="box1-left">
...@@ -88,14 +28,13 @@ ...@@ -88,14 +28,13 @@
<div class="box1-right-item"> <div class="box1-right-item">
<div class="item-left">相关领域:</div> <div class="item-left">相关领域:</div>
<div class="item-right1"> <div class="item-right1">
<!-- <div class="right1-item" v-for="item in basicInfo.hylyList" :key="item">{{ item }}</div> --> <AreaTag v-for="item in hylyList" :key="item" :tagName="item" />
<AreaTag v-for="item, index in basicInfo.hylyList" :key="index" :tagName="item"/>
</div> </div>
</div> </div>
<div class="box1-right-item"> <div class="box1-right-item">
<div class="item-left">委员会报告:</div> <div class="item-left">委员会报告:</div>
<div class="item-right2" v-if="basicInfo.reportList"> <div class="item-right2" v-if="reportList.length">
<div class="right2-item" v-for="(item, index) in basicInfo.reportList" :key="index"> <div class="right2-item" v-for="(item, index) in reportList" :key="getReportKey(item, index)">
{{ item }} {{ item }}
</div> </div>
</div> </div>
...@@ -111,11 +50,14 @@ ...@@ -111,11 +50,14 @@
<div class="box1-right-item"> <div class="box1-right-item">
<div class="item-left">立案流程:</div> <div class="item-left">立案流程:</div>
<div class="item-right4"> <div class="item-right4">
<div class="step" <div
v-for="(item, index) in basicInfo.stageList ? [...basicInfo.stageList].reverse() : []" :key="index" v-for="(item, index) in reversedStageList"
:style="{ zIndex: (basicInfo.stageList?.length || 0) - index }"> :key="getStageKey(item, index)"
class="step"
:style="{ zIndex: getStageZIndex(index) }"
>
<div class="step-box" <div class="step-box"
:class="{ 'step-box-active': index === (basicInfo.stageList?.length || 0) - 1 }"> :class="{ 'step-box-active': index === stageActiveIndex }">
{{ item }} {{ item }}
</div> </div>
</div> </div>
...@@ -126,33 +68,8 @@ ...@@ -126,33 +68,8 @@
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="introduction-wrap-left-box2"> <div class="introduction-wrap-left-box2">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">法案进展</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box2-main">
<div class="box2-main-center">
<STimeline :dataList="timelineData" />
</div>
</div> -->
<AnalysisBox title="法案进展" :showAllBtn="false"> <AnalysisBox title="法案进展" :showAllBtn="false">
<template #header-btn> <template #header-btn>
<!-- <div class="progress-header-btns">
<div class="btn" :class="{ btnActive: progressMode === 'latest' }" @click="handleSwitchProgressMode('latest')">
最新进展
</div>
<div class="btn" :class="{ btnActive: progressMode === 'early' }" @click="handleSwitchProgressMode('early')">
前期进程
</div>
</div> -->
</template> </template>
<div class="box2-main"> <div class="box2-main">
<div class="box2-main-center"> <div class="box2-main-center">
...@@ -163,105 +80,10 @@ ...@@ -163,105 +80,10 @@
</div> </div>
</div> </div>
<div class="introduction-wrap-right"> <div class="introduction-wrap-right">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">提出人</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="introduction-wrap-right-main">
<div class="right-main-box1">
<div class="name-box">
<div class="person-box">
<div class="person-item" :class="{ nameItemActive: box3BtnActive === item.name }"
@click="handleClcikBox3Btn(item.name, index)" v-for="(item, index) in personList" :key="index">
{{ item.name }}
</div>
</div>
</div>
<div class="info-box">
<div class="info-left">
<img :src="defaultAvatar" alt="" @click="handleClickAvatar(curPerson)" />
<div class="usr-icon1">
<img src="./assets/images/usr-icon1.png" alt="" />
</div>
<div class="usr-icon2">
<img src="./assets/images/usr-icon2.png" alt="" />
</div>
</div>
<div class="info-right">
<div class="info-right-title" @click="handleClickAvatar(curPerson)">{{ curPerson.name }}</div>
<div class="info-right-item">
<div class="item-left">英文名:</div>
<div class="item-right">{{ curPerson.ename }}</div>
</div>
<div class="info-right-item">
<div class="item-left">党派:</div>
<div class="item-right">{{ curPerson.dp }}</div>
</div>
<div class="info-right-item">
<div class="item-left">选区:</div>
<div class="item-right">{{ curPerson.xq }}</div>
</div>
<div class="info-right-item">
<div class="item-left">职位:</div>
<div class="item-right">{{ curPerson.zw }}</div>
</div>
</div>
</div>
</div>
<div class="right-main-box2" v-if="curPerson.tagList && curPerson.tagList.length">
<div class="tag-box" :class="{
status0: index % 4 === 0,
status1: index % 4 === 1,
status2: index % 4 === 2,
status3: index % 4 === 3
}" v-for="(tag, index) in curPerson.tagList" :key="index">
{{ tag.industryName }}
</div>
</div>
<div class="right-main-box3">
<div class="right-main-box3-header">
<div class="icon">
<img src="./assets/images/right-main-box3-header-icon.svg" alt="" />
</div>
<div class="title">人物动态</div>
</div>
<div class="right-main-box3-main">
<el-timeline style="max-width: 500px">
<el-timeline-item :timestamp="item.newsDate" placement="top"
v-for="(item, index) in curPerson.newsList?.slice(0, 3)" :key="index">
<div class="timeline-content">
{{ item.newsContent }}
</div>
</el-timeline-item>
</el-timeline>
</div>
<div class="right-main-box3-footer">
<div class="btn-more" @click="handleClickMore2">
<img src="../assets/images/btn-more.png" alt="" />
</div>
</div>
</div>
</div> -->
<AnalysisBox title="提出人" :showAllBtn="false"> <AnalysisBox title="提出人" :showAllBtn="false">
<div class="introduction-wrap-right-main"> <div class="introduction-wrap-right-main">
<div class="right-main-box1"> <div class="right-main-box1">
<div class="name-box"> <div class="name-box">
<!-- <el-select
v-model="selectValue"
placeholder="请选择"
style="width: 180px; margin: 0 10px"
@change="handleChangeFaId"
>
<el-option v-for="item in faList" :key="item.value" :label="item.label" :value="item.id" />
</el-select> -->
<div class="person-box"> <div class="person-box">
<div class="person-item" :class="{ nameItemActive: box3BtnActive === item.name }" <div class="person-item" :class="{ nameItemActive: box3BtnActive === item.name }"
@click="handleClcikBox3Btn(item.name, index)" v-for="(item, index) in personList" :key="index"> @click="handleClcikBox3Btn(item.name, index)" v-for="(item, index) in personList" :key="index">
...@@ -301,7 +123,6 @@ ...@@ -301,7 +123,6 @@
</div> </div>
</div> </div>
<div class="right-main-box2" v-if="curPerson.tagList && curPerson.tagList.length"> <div class="right-main-box2" v-if="curPerson.tagList && curPerson.tagList.length">
<!-- <WordCloudMap :data="wordCloudData" :shape="circle" /> -->
<div class="tag-box status"v-for="(tag, index) in curPerson.tagList" :key="index"> <div class="tag-box status"v-for="(tag, index) in curPerson.tagList" :key="index">
{{ tag }} {{ tag }}
</div> </div>
...@@ -321,17 +142,6 @@ ...@@ -321,17 +142,6 @@
{{ item.newsContent }} {{ item.newsContent }}
</div> </div>
</el-timeline-item> </el-timeline-item>
<!-- <el-timeline-item timestamp="2018/4/3" placement="top">
<div class="timeline-content">
OBBBA通过势头,在得州巩固军工、能源集团支持,为2026年连任铺路,同时协调党内资源争夺关键摇摆选区。
</div>
</el-timeline-item>
<el-timeline-item timestamp="2018/4/2" placement="top">
<div class="timeline-content">
特朗普力挺阿灵顿,白宫声明强调法案“美丽且必要”,双方矛盾凸显共和党内
​商业资本与传统能源势力裂痕。
</div>
</el-timeline-item> -->
</el-timeline> </el-timeline>
</div> </div>
</div> </div>
...@@ -339,13 +149,15 @@ ...@@ -339,13 +149,15 @@
</AnalysisBox> </AnalysisBox>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import WordCloudMap from "./WordCloudMap.vue"; import WordCloudMap from "./WordCloudMap.vue";
import STimeline from "./STimeline.vue"; import STimeline from "./STimeline.vue";
import WarningPane from "@/components/base/WarningPane/index.vue";
import { getBillInfo, getBillPerson, getBillEvent, getBillDyqk } from "@/api/bill"; import { getBillInfo, getBillPerson, getBillEvent, getBillDyqk } from "@/api/bill";
import defaultAvatar from "../assets/images/default-icon1.png"; import defaultAvatar from "../assets/images/default-icon1.png";
import defaultNew from "../assets/images/default-icon-news.png"; import defaultNew from "../assets/images/default-icon-news.png";
...@@ -383,6 +195,19 @@ const handleSwitchProgressMode = mode => { ...@@ -383,6 +195,19 @@ const handleSwitchProgressMode = mode => {
const basicInfo = ref({}); const basicInfo = ref({});
const riskSignal = computed(() => basicInfo.value?.riskSignalVO || null);
const hylyList = computed(() => (Array.isArray(basicInfo.value?.hylyList) ? basicInfo.value.hylyList : []));
const reportList = computed(() => (Array.isArray(basicInfo.value?.reportList) ? basicInfo.value.reportList : []));
const reversedStageList = computed(() => {
const list = Array.isArray(basicInfo.value?.stageList) ? basicInfo.value.stageList : [];
return [...list].reverse();
});
const stageListLength = computed(() => (Array.isArray(basicInfo.value?.stageList) ? basicInfo.value.stageList.length : 0));
const stageActiveIndex = computed(() => stageListLength.value - 1);
const getStageZIndex = index => stageListLength.value - index;
const getStageKey = (item, index) => `${item}-${index}`;
const getReportKey = (item, index) => `${item}-${index}`;
const handleGetBasicInfo = async () => { const handleGetBasicInfo = async () => {
const params = { const params = {
id: billId.value id: billId.value
...@@ -452,8 +277,10 @@ onMounted(() => { ...@@ -452,8 +277,10 @@ onMounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.introduction-wrap { .introduction-wrap {
width: 100%; width: 100%;
height: 880px; min-height: 880px;
height: auto;
display: flex; display: flex;
flex-direction: column;
.progress-header-btns { .progress-header-btns {
display: flex; display: flex;
...@@ -536,6 +363,19 @@ onMounted(() => { ...@@ -536,6 +363,19 @@ onMounted(() => {
} }
} }
.risk-signal-pane-top {
width: 1600px;
margin-top: 16px;
margin-right: 18px;
height: 116px;
min-height: 116px;
flex-shrink: 0;
}
.introduction-wrap-content {
display: flex;
}
.introduction-wrap-left { .introduction-wrap-left {
width: 1064px; width: 1064px;
margin-top: 16px; margin-top: 16px;
...@@ -1358,6 +1198,7 @@ onMounted(() => { ...@@ -1358,6 +1198,7 @@ onMounted(() => {
line-height: 26px; line-height: 26px;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
line-clamp: 3;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
......
...@@ -387,7 +387,7 @@ const handleChangeBill = val => { ...@@ -387,7 +387,7 @@ const handleChangeBill = val => {
}; };
const handleOpenVersionCompare = () => { const handleOpenVersionCompare = () => {
const targetUrl = `/billLayout/deepDig/processOverview?billId=${route.query.billId}`; const targetUrl = `/billLayout/versionCompare?billId=${route.query.billId}`;
window.open(targetUrl, "_blank"); window.open(targetUrl, "_blank");
}; };
...@@ -404,8 +404,8 @@ const handleGetBillList = async () => { ...@@ -404,8 +404,8 @@ const handleGetBillList = async () => {
billList.value = rawList billList.value = rawList
.map(item => { .map(item => {
return { return {
label: item.bbmc, label: item.contentZh,
value: item.bbmc value: item.contentZh
}; };
}) })
.filter(item => { .filter(item => {
......
<svg viewBox="0 0 13.5996 12.666" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13.599609" height="12.666016" fill="none" customFrame="#000000">
<path id="矢量 455" d="M2 8.66667L2 10C1.99999 10.0416 2.00192 10.0831 2.00578 10.1245C2.00965 10.1659 2.01545 10.207 2.02317 10.2479C2.03089 10.2888 2.0405 10.3292 2.052 10.3691C2.0635 10.4091 2.07684 10.4484 2.09202 10.4872C2.1072 10.5259 2.12416 10.5638 2.14289 10.6009C2.16162 10.6381 2.18204 10.6742 2.20416 10.7095C2.22627 10.7447 2.24998 10.7788 2.27529 10.8118C2.3006 10.8448 2.32739 10.8765 2.35566 10.907C2.38394 10.9375 2.41358 10.9666 2.44458 10.9944C2.47558 11.0221 2.50781 11.0483 2.54126 11.073C2.57472 11.0977 2.60925 11.1208 2.64487 11.1423C2.68048 11.1637 2.71702 11.1835 2.75448 11.2016C2.79195 11.2196 2.83017 11.2359 2.86916 11.2503C2.90814 11.2648 2.94772 11.2775 2.98788 11.2882C3.02805 11.299 3.06863 11.3079 3.10963 11.3149C3.15063 11.3218 3.19186 11.3269 3.23333 11.33L3.33333 11.3333L5.33333 11.3333L5.33333 12.6667L3.33333 12.6667C3.246 12.6667 3.15887 12.6624 3.07195 12.6538C2.98504 12.6453 2.89875 12.6325 2.81309 12.6154C2.72743 12.5984 2.64282 12.5772 2.55924 12.5518C2.47566 12.5265 2.39353 12.4971 2.31284 12.4637C2.23216 12.4303 2.1533 12.393 2.07627 12.3518C1.99925 12.3106 1.92443 12.2658 1.85181 12.2173C1.77919 12.1687 1.70913 12.1168 1.64162 12.0614C1.57411 12.006 1.50947 11.9474 1.44771 11.8856C1.38596 11.8239 1.32738 11.7592 1.27197 11.6917C1.21657 11.6242 1.1646 11.5541 1.11608 11.4815C1.06756 11.4089 1.02271 11.3341 0.981543 11.2571C0.940373 11.18 0.903077 11.1012 0.869654 11.0205C0.836232 10.9398 0.806845 10.8577 0.781492 10.7741C0.75614 10.6905 0.734944 10.6059 0.717906 10.5202C0.700867 10.4346 0.688068 10.3483 0.679507 10.2614C0.670947 10.1745 0.666666 10.0873 0.666666 10L0.666666 8.66667L2 8.66667L2 8.66667ZM10.6667 5.33333L13.6 12.6667L12.1633 12.6667L11.3627 10.6667L8.636 10.6667L7.83667 12.6667L6.40067 12.6667L9.33333 5.33333L10.6667 5.33333L10.6667 5.33333ZM10 7.25667L9.16867 9.33333L10.83 9.33333L10 7.25667ZM4 0L4 1.33333L6.66667 1.33333L6.66667 6L4 6L4 8L2.66667 8L2.66667 6L0 6L0 1.33333L2.66667 1.33333L2.66667 0L4 0ZM10 0.666667C10.0873 0.666667 10.1745 0.670947 10.2614 0.679507C10.3483 0.688068 10.4346 0.700867 10.5202 0.717906C10.6059 0.734944 10.6905 0.75614 10.7741 0.781492C10.8577 0.806845 10.9398 0.836232 11.0205 0.869654C11.1012 0.903077 11.18 0.940373 11.2571 0.981543C11.3341 1.02271 11.4089 1.06756 11.4815 1.11608C11.5541 1.1646 11.6242 1.21657 11.6917 1.27197C11.7592 1.32738 11.8239 1.38596 11.8856 1.44772C11.9474 1.50947 12.006 1.57411 12.0614 1.64162C12.1168 1.70913 12.1687 1.77919 12.2173 1.85181C12.2658 1.92443 12.3106 1.99925 12.3518 2.07628C12.393 2.1533 12.4303 2.23216 12.4637 2.31284C12.4971 2.39353 12.5265 2.47566 12.5518 2.55924C12.5772 2.64282 12.5984 2.72743 12.6154 2.81309C12.6325 2.89875 12.6453 2.98504 12.6538 3.07195C12.6624 3.15887 12.6667 3.246 12.6667 3.33333L12.6667 4.66667L11.3333 4.66667L11.3333 3.33333C11.3333 3.28966 11.3312 3.2461 11.3269 3.20264C11.3226 3.15919 11.3162 3.11604 11.3077 3.07321C11.2992 3.03038 11.2886 2.98807 11.2759 2.94629C11.2632 2.9045 11.2486 2.86343 11.2318 2.82309C11.2151 2.78274 11.1965 2.74332 11.1759 2.7048C11.1553 2.66629 11.1329 2.62888 11.1086 2.59257C11.0844 2.55626 11.0584 2.52123 11.0307 2.48748C11.003 2.45372 10.9737 2.4214 10.9428 2.39052C10.9119 2.35965 10.8796 2.33036 10.8459 2.30265C10.8121 2.27495 10.7771 2.24897 10.7408 2.22471C10.7045 2.20045 10.667 2.17802 10.6285 2.15744C10.59 2.13685 10.5506 2.1182 10.5102 2.10149C10.4699 2.08478 10.4288 2.07009 10.387 2.05741C10.3453 2.04474 10.303 2.03414 10.2601 2.02562C10.2173 2.0171 10.1741 2.0107 10.1307 2.00642C10.0872 2.00214 10.0437 2 10 2L8 2L8 0.666667L10 0.666667L10 0.666667ZM2.66667 2.66667L1.33333 2.66667L1.33333 4.66667L2.66667 4.66667L2.66667 2.66667ZM5.33333 2.66667L4 2.66667L4 4.66667L5.33333 4.66667L5.33333 2.66667Z" fill="rgb(95,101,108)" fill-rule="nonzero" />
</svg>
<template>
<div class="version-compare-wrap">
<div class="compare-top">
<div class="compare-top-col">
<div class="compare-top-label">原版本:</div>
<el-select v-model="oldVersionId" placeholder="请选择版本" class="compare-top-select" clearable>
<el-option
v-for="item in versionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.value === newVersionId"
/>
</el-select>
</div>
<div class="compare-top-col">
<div class="compare-top-label">现版本:</div>
<el-select v-model="newVersionId" placeholder="请选择版本" class="compare-top-select" clearable>
<el-option
v-for="item in versionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.value === oldVersionId"
/>
</el-select>
</div>
</div>
<div class="compare-tools">
<div class="compare-tools-tabs" ref="tabsWrapRef">
<button
v-for="(tab, index) in diffTabs"
:key="tab.value"
class="compare-tools-tab"
:class="{ 'is-active': diffType === tab.value }"
type="button"
@click="handleDiffTabClick(tab.value)"
:ref="el => setTabRef(el, index)"
>
<span class="label">{{ tab.label }}</span>
<span
class="count"
v-if="diffCounts[tab.value] !== null && diffCounts[tab.value] !== undefined"
>
{{ diffCounts[tab.value] }}
</span>
</button>
<span class="compare-tools-tabs-active" :style="activeBarStyle"></span>
</div>
<div class="compare-tools-actions">
<el-checkbox v-model="onlyChinaRelated" label="只看涉华条款" size="large" />
<div class="compare-tools-switches">
<div class="compare-tools-switch">
<el-switch v-model="termsHighlight" inline-prompt/>
<span class="label">高亮实体</span>
</div>
<div class="compare-tools-switch">
<el-switch v-model="termsShowOriginal" inline-prompt/>
<span class="label">
<img class="label-icon" :src="translateIcon" alt="" />
显示原文
</span>
</div>
</div>
<div class="find-word-wrap">
<div class="find-word-box" v-if="findWordBox">
<div class="find-word-input">
<el-input ref="findWordInputRef" v-model="findWordTxt" placeholder="查找条款内容" clearable
@input="handleUpdateWord" />
</div>
<div class="find-word-limit">{{ findWordNum }}/{{ findWordMax }}</div>
<div class="find-word-icon" @click="handleFindWord('last')">
<el-icon><ArrowUp /></el-icon>
</div>
<div class="find-word-icon" @click="handleFindWord('next')">
<el-icon><ArrowDown /></el-icon>
</div>
<div class="find-word-icon" @click="handleFindWord('close')">
<el-icon><Close /></el-icon>
</div>
</div>
<el-button type="primary" plain class="find-word-open-btn"
@click="handleFindWord('open')">
<el-icon class="find-word-open-icon"><Search /></el-icon>
查找
</el-button>
</div>
</div>
</div>
<div class="compare-columns" v-loading="isLoading" @click="handleEntityClick">
<div class="compare-row" v-for="(pair, index) in comparePairs" :key="getPairKey(pair, index)">
<div class="compare-cell">
<template v-if="pair?.oldTerm">
<div class="term-body">
<div class="term-main">
<div class="term-row term-row-cn">
<div class="term-no-cn">{{ pair.oldTerm.tkxh }}条.</div>
<div class="term-content-cn" v-html="getTermContentHtml(pair.oldTerm, 'cn')"></div>
</div>
<div class="term-row term-row-en" v-if="termsShowOriginal">
<div class="term-no-en">Sec.{{ pair.oldTerm.tkxh }}</div>
<div class="term-content-en" v-html="getTermContentHtml(pair.oldTerm, 'en')"></div>
</div>
</div>
</div>
</template>
<div v-else class="term-empty"></div>
</div>
<div class="compare-cell">
<template v-if="pair?.newTerm">
<div class="term-body">
<div class="term-main">
<div class="term-row term-row-cn">
<div class="term-no-cn">{{ pair.newTerm.tkxh }}条.</div>
<div class="term-content-cn" v-html="getTermContentHtml(pair.newTerm, 'cn')"></div>
</div>
<div class="term-row term-row-en" v-if="termsShowOriginal">
<div class="term-no-en">Sec.{{ pair.newTerm.tkxh }}</div>
<div class="term-content-en" v-html="getTermContentHtml(pair.newTerm, 'en')"></div>
</div>
</div>
</div>
</template>
<div v-else class="term-empty"></div>
</div>
</div>
</div>
<div class="compare-footer">
<div class="compare-footer-text">{{ `共 ${total} 项` }}</div>
<div class="compare-footer-right">
<el-pagination
background
layout="prev, pager, next"
:total="total"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { getBillContentId, getBillVersionCompare } from "@/api/bill";
import { extractTextEntity } from "@/api/intelligent/index";
import { ArrowDown, ArrowUp, Close, Search } from "@element-plus/icons-vue";
import translateIcon from "./assert/icons/translate-icons.svg";
const route = useRoute();
const billId = computed(() => route.query.billId);
const versionOptions = ref([]);
const oldVersionId = ref("");
const newVersionId = ref("");
const diffType = ref("CHANGE");
const onlyChinaRelated = ref(false);
const keyword = ref("");
const termsHighlight = ref(true);
const termsShowOriginal = ref(true);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const diffTabs = [
{ label: "变更", value: "CHANGE" },
{ label: "新增", value: "ADD" },
{ label: "删除", value: "DELETE" }
];
const diffCounts = ref({ CHANGE: 0, ADD: 0, DELETE: 0 });
const tabsWrapRef = ref(null);
const tabItemRefs = ref([]);
const activeBarStyle = ref({});
const findWordBox = ref(false);
const findWordInputRef = ref();
const findWordTxt = ref("");
const findWordKeyword = ref("");
const findWordNum = ref(0);
const findWordMax = ref(0);
const findWordTimer = ref(null);
const isLoading = ref(false);
const comparePairs = ref([]);
const compareRequestToken = ref(0);
const handleLoadVersionOptions = async () => {
if (!billId.value) {
versionOptions.value = [];
oldVersionId.value = "";
newVersionId.value = "";
return;
}
const res = await getBillContentId({ id: billId.value });
const rawList = Array.isArray(res?.data) ? res.data : [];
const seen = new Set();
versionOptions.value = rawList
.map(item => {
return {
label: item?.contentZh,
value: item?.contentZh
};
})
.filter(item => item.value)
.filter(item => {
if (seen.has(item.value)) return false;
seen.add(item.value);
return true;
});
if (!versionOptions.value.length) {
oldVersionId.value = "";
newVersionId.value = "";
return;
}
if (versionOptions.value.length === 1) {
oldVersionId.value = versionOptions.value[0].value;
newVersionId.value = "";
return;
}
oldVersionId.value = versionOptions.value[0].value;
newVersionId.value = versionOptions.value[versionOptions.value.length - 1].value;
};
const normalizeDiffType = value => {
if (value === "ADD") return "ADD";
if (value === "DELETE") return "DELETE";
return "CHANGE";
};
const setTabRef = (el, index) => {
if (el) tabItemRefs.value[index] = el;
};
const updateActiveBar = () => {
const index = diffTabs.findIndex(tab => tab.value === diffType.value);
const target = tabItemRefs.value[index];
const wrap = tabsWrapRef.value;
if (!target || !wrap) {
activeBarStyle.value = {};
return;
}
const wrapRect = wrap.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
activeBarStyle.value = {
width: `${targetRect.width}px`,
transform: `translateX(${targetRect.left - wrapRect.left}px)`
};
};
const handleDiffTabClick = value => {
if (diffType.value === value) return;
diffType.value = value;
nextTick(() => {
updateActiveBar();
});
};
const updateDiffCounts = list => {
const counts = { CHANGE: 0, ADD: 0, DELETE: 0 };
for (const pair of list) {
const isOld = Boolean(pair?.oldTerm);
const isNew = Boolean(pair?.newTerm);
if (isOld && isNew) {
counts.CHANGE += 1;
} else if (isNew && !isOld) {
counts.ADD += 1;
} else if (isOld && !isNew) {
counts.DELETE += 1;
}
}
diffCounts.value = counts;
};
const mapDiffTypeToStatus = value => {
const diff = normalizeDiffType(value);
if (diff === "ADD") return "add";
if (diff === "DELETE") return "del";
return "update";
};
const mapStatusToDiffType = value => {
if (value === "add") return "ADD";
if (value === "del") return "DELETE";
return "CHANGE";
};
const mapVersionCompareItemToPair = item => {
const oldTerm = item?.originalClauseMainId
? {
id: item.originalClauseMainId,
ywid: item.originalClauseMainId,
tkxh: item?.originalClauseNumber ?? "",
fynr: item?.originalContentZh ?? "",
ywnr: item?.originalContent ?? ""
}
: null;
const newTerm = item?.currentClauseMainId
? {
id: item.currentClauseMainId,
ywid: item.currentClauseMainId,
tkxh: item?.currentClauseNumber ?? "",
fynr: item?.currentContentZh ?? "",
ywnr: item?.currentContent ?? ""
}
: null;
return { oldTerm, newTerm };
};
const fetchComparePage = async ({ diff, page, size }) => {
const params = {
billId: billId.value,
content: keyword.value,
currentPage: Math.max(1, Number(page) || 1),
currentVersion: newVersionId.value,
isCn: onlyChinaRelated.value ? "Y" : "N",
originalVersion: oldVersionId.value,
pageSize: Math.max(1, Number(size) || 10),
status: mapDiffTypeToStatus(diff)
};
const res = await getBillVersionCompare(params);
const data = res?.data?.data ?? res?.data ?? {};
const raw = Array.isArray(data?.content) ? data.content : [];
const countType = Array.isArray(data?.countType) ? data.countType : [];
return {
list: raw.map(mapVersionCompareItemToPair),
total: Number(data?.totalElements ?? 0) || 0,
countType
};
};
const getCountByChangeType = (countTypeList, changeTypeLabel) => {
const list = Array.isArray(countTypeList) ? countTypeList : [];
const target = list.find(item => String(item?.changeType ?? "") === changeTypeLabel);
return Number(target?.count ?? 0) || 0;
};
const loadComparePairs = async () => {
if (!billId.value || !oldVersionId.value || !newVersionId.value) {
comparePairs.value = [];
updateDiffCounts([]);
total.value = 0;
return;
}
const currentToken = ++compareRequestToken.value;
isLoading.value = true;
try {
const currentRes = await fetchComparePage({
diff: diffType.value,
page: currentPage.value,
size: pageSize.value
});
if (currentToken !== compareRequestToken.value) return;
comparePairs.value = currentRes.list;
total.value = currentRes.total;
diffCounts.value = {
CHANGE: getCountByChangeType(currentRes.countType, "更新"),
ADD: getCountByChangeType(currentRes.countType, "新增"),
DELETE: getCountByChangeType(currentRes.countType, "删除")
};
await ensureEntitiesForPairs(comparePairs.value);
} catch (error) {
if (currentToken !== compareRequestToken.value) return;
comparePairs.value = [];
updateDiffCounts([]);
total.value = 0;
} finally {
if (currentToken === compareRequestToken.value) {
isLoading.value = false;
}
}
};
const handleCurrentChange = page => {
currentPage.value = Number(page) || 1;
};
const getPairKey = (pair, index) => {
const oldKey = pair?.oldTerm?.ywid ?? pair?.oldTerm?.id ?? pair?.oldTerm?.tkxh ?? "";
const newKey = pair?.newTerm?.ywid ?? pair?.newTerm?.id ?? pair?.newTerm?.tkxh ?? "";
return `${oldKey || "old"}__${newKey || "new"}__${index}`;
};
const escapeHtml = value => {
const str = String(value ?? "");
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
};
const escapeRegExp = text => {
return String(text || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const getSearchRanges = (text, searchTerm) => {
const rawText = String(text ?? "");
const term = String(searchTerm ?? "").trim();
if (!rawText || !term) return [];
const ranges = [];
const reg = new RegExp(escapeRegExp(term), "g");
let match;
while ((match = reg.exec(rawText)) !== null) {
ranges.push({ start: match.index, end: match.index + match[0].length });
if (match[0].length === 0) reg.lastIndex += 1;
}
return ranges;
};
const normalizeEntities = entities => {
const list = Array.isArray(entities) ? entities : [];
return list
.map(item => {
return {
text_span: String(item?.text_span ?? "").trim(),
type: String(item?.type ?? "").trim()
};
})
.filter(item => item.text_span);
};
const getEntityRanges = (text, entities) => {
const ranges = [];
const rawText = String(text ?? "");
if (!rawText) return ranges;
const list = normalizeEntities(entities).sort((a, b) => b.text_span.length - a.text_span.length);
for (const ent of list) {
let startIndex = 0;
while (startIndex < rawText.length) {
const idx = rawText.indexOf(ent.text_span, startIndex);
if (idx === -1) break;
ranges.push({ start: idx, end: idx + ent.text_span.length, ent });
startIndex = idx + ent.text_span.length;
}
}
ranges.sort((a, b) => a.start - b.start || b.end - a.end);
const merged = [];
let lastEnd = 0;
for (const r of ranges) {
if (r.start < lastEnd) continue;
merged.push(r);
lastEnd = r.end;
}
return merged;
};
const buildHighlightedHtml = (text, entities, enableHighlight, searchTerm) => {
const rawText = String(text ?? "");
if (!rawText) return "";
const safeText = escapeHtml(rawText).replace(/\n/g, "<br />");
const term = String(searchTerm ?? "").trim();
const enableSearch = Boolean(term);
if (!enableHighlight && !enableSearch) return safeText;
const ranges = getEntityRanges(rawText, entities);
const searchRanges = enableSearch ? getSearchRanges(rawText, term) : [];
if (!ranges.length && !searchRanges.length) return safeText;
const merged = [];
for (const r of ranges) {
merged.push({ start: r.start, end: r.end, type: "entity", ent: r.ent });
}
for (const r of searchRanges) {
const overlapsEntity = ranges.some(er => r.start < er.end && r.end > er.start);
if (overlapsEntity) continue;
merged.push({ start: r.start, end: r.end, type: "search" });
}
merged.sort((a, b) => a.start - b.start || b.end - a.end);
let html = "";
let cursor = 0;
for (const r of merged) {
if (cursor < r.start) {
html += escapeHtml(rawText.slice(cursor, r.start));
}
const spanText = rawText.slice(r.start, r.end);
if (r.type === "entity") {
const type = escapeHtml(r.ent?.type ?? "");
const text = escapeHtml(spanText);
html += `<span class="term-entity" data-entity-type="${type}" data-entity-text="${text}">${text}</span>`;
} else {
html += `<span class="term-find-highlight">${escapeHtml(spanText)}</span>`;
}
cursor = r.end;
}
if (cursor < rawText.length) {
html += escapeHtml(rawText.slice(cursor));
}
return html.replace(/\n/g, "<br />");
};
const handleEntityClick = event => {
const el = event?.target?.closest?.("span.term-entity");
if (!el) return;
const q = String(el.getAttribute("data-entity-text") ?? "").trim();
if (!q) return;
const url = `https://www.bing.com/search?q=${encodeURIComponent(q)}`;
window.open(url, "_blank", "noopener");
};
const termEntityCache = ref(new Map());
const entityRequestToken = ref(0);
const getTermEntityKey = (term, lang) => {
const baseKey = term?.ywid ?? term?.id ?? term?.tkxh ?? "";
return `${baseKey}__${lang}`;
};
const ensureEntitiesForPairs = async pairs => {
if (!termsHighlight.value) return;
const list = Array.isArray(pairs) ? pairs : [];
if (!list.length) return;
const currentToken = ++entityRequestToken.value;
const tasks = [];
const maxTasks = 40;
for (const pair of list) {
const terms = [pair?.oldTerm, pair?.newTerm].filter(Boolean);
for (const term of terms) {
const cnKey = getTermEntityKey(term, "cn");
const enKey = getTermEntityKey(term, "en");
if (!termEntityCache.value.has(cnKey) && String(term?.fynr ?? "").trim()) {
tasks.push({ key: cnKey, text: term.fynr });
}
if (!termEntityCache.value.has(enKey) && String(term?.ywnr ?? "").trim()) {
tasks.push({ key: enKey, text: term.ywnr });
}
if (tasks.length >= maxTasks) break;
}
if (tasks.length >= maxTasks) break;
}
if (!tasks.length) return;
try {
const results = await Promise.all(
tasks.map(async item => {
const res = await extractTextEntity(item.text);
const entities = normalizeEntities(res?.result ?? res?.data?.result ?? res?.data ?? res);
return { key: item.key, entities };
})
);
if (currentToken !== entityRequestToken.value) return;
for (const r of results) {
termEntityCache.value.set(r.key, r.entities);
}
} catch (error) {
if (currentToken !== entityRequestToken.value) return;
}
};
const getTermContentHtml = (term, lang) => {
const raw = lang === "en" ? term?.ywnr : term?.fynr;
const key = getTermEntityKey(term, lang);
const entities = termEntityCache.value.get(key) || [];
return buildHighlightedHtml(raw, entities, termsHighlight.value, findWordKeyword.value);
};
const countOccurrences = (text, searchTerm) => {
const rawText = String(text ?? "");
const term = String(searchTerm ?? "").trim();
if (!rawText || !term) return 0;
const reg = new RegExp(escapeRegExp(term), "g");
const matches = rawText.match(reg);
return matches ? matches.length : 0;
};
const updateActiveFindHighlight = () => {
const spans = document.querySelectorAll("span.term-find-highlight");
spans.forEach((span, index) => {
if (index + 1 === findWordNum.value) {
span.classList.add("is-active");
span.scrollIntoView({ block: "center" });
} else {
span.classList.remove("is-active");
}
});
};
const doUpdateFindWord = async () => {
findWordNum.value = 0;
findWordMax.value = 0;
const term = String(findWordTxt.value || "").trim();
findWordKeyword.value = term;
if (!term) {
await nextTick();
return;
}
const list = Array.isArray(comparePairs.value) ? comparePairs.value : [];
for (const pair of list) {
const terms = [pair?.oldTerm, pair?.newTerm].filter(Boolean);
for (const t of terms) {
findWordMax.value += countOccurrences(t?.fynr, term);
if (termsShowOriginal.value) {
findWordMax.value += countOccurrences(t?.ywnr, term);
}
}
}
if (findWordMax.value > 0) {
await nextTick();
findWordNum.value = 1;
updateActiveFindHighlight();
}
};
const handleUpdateWord = () => {
if (findWordTimer.value) {
clearTimeout(findWordTimer.value);
findWordTimer.value = null;
}
findWordTimer.value = setTimeout(() => {
doUpdateFindWord();
}, 300);
};
const handleFindWord = event => {
switch (event) {
case "open":
findWordBox.value = true;
nextTick(() => {
findWordInputRef.value?.focus?.();
});
break;
case "last":
if (findWordMax.value > 1) {
findWordNum.value = findWordNum.value === 1 ? findWordMax.value : findWordNum.value - 1;
updateActiveFindHighlight();
}
break;
case "next":
if (findWordMax.value > 1) {
findWordNum.value = findWordNum.value === findWordMax.value ? 1 : findWordNum.value + 1;
updateActiveFindHighlight();
}
break;
case "close":
findWordBox.value = false;
findWordTxt.value = "";
findWordKeyword.value = "";
findWordNum.value = 0;
findWordMax.value = 0;
break;
}
};
watch(
[billId, oldVersionId, newVersionId, diffType, onlyChinaRelated, keyword, currentPage, pageSize],
() => {
loadComparePairs();
},
{ immediate: true }
);
watch(diffType, () => {
currentPage.value = 1;
nextTick(() => {
updateActiveBar();
});
});
watch([oldVersionId, newVersionId, onlyChinaRelated, keyword], () => {
currentPage.value = 1;
});
watch(termsHighlight, () => {
ensureEntitiesForPairs(comparePairs.value);
});
watch(
[comparePairs, termsShowOriginal],
() => {
if (!findWordBox.value) return;
doUpdateFindWord();
},
{ deep: true }
);
onMounted(async () => {
await handleLoadVersionOptions();
nextTick(() => {
updateActiveBar();
});
});
</script>
<style lang="scss" scoped>
.version-compare-wrap {
display: flex;
flex-direction: column;
row-gap: 16px;
width: 100%;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px 75px;
box-sizing: border-box;
}
.compare-top {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 37px;
margin-top: 0;
}
.compare-top-col {
display: flex;
align-items: center;
gap: 22px;
background: transparent;
border-radius: 0;
padding: 0;
box-shadow: none;
}
.compare-top-label {
font-size: 16px;
font-weight: 400;
color: var(--text-primary-65-color);
white-space: nowrap;
}
.compare-top-select {
flex: 1;
:deep(.el-select__wrapper) {
background-color: rgb(246, 250, 255);
box-shadow: 0 0 0 1px var(--color-primary-35) inset;
}
:deep(.el-select__wrapper.is-hovering) {
box-shadow: 0 0 0 1px var(--color-primary-35) inset;
}
:deep(.el-select__wrapper.is-focused) {
box-shadow: 0 0 0 1px var(--color-primary-35) inset;
}
:deep(.el-select__selected-item),
:deep(.el-select__placeholder),
:deep(.el-select__input),
:deep(.el-select__caret),
:deep(.el-select__suffix),
:deep(.el-select__icon) {
color: var(--color-primary-100);
}
}
.compare-tools {
height: 66px;
border-bottom: 1px solid var(--border-black-5);
border-top: 1px solid var(--border-black-5);
background: transparent;
display: flex;
align-items: center;
gap: 24px;
flex-wrap: nowrap;
white-space: nowrap;
}
.compare-tools-tabs {
flex: 0 0 auto;
position: relative;
display: flex;
align-items: center;
gap: 28px;
height: 100%;
}
.compare-tools-tab {
background: transparent;
border: none;
padding: 0;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 400;
color: var(--text-primary-65-color);
position: relative;
&.is-active {
color: var(--color-primary-100);
}
&:focus-visible {
outline: 2px solid rgba(66, 133, 244, 0.5);
outline-offset: 4px;
border-radius: 6px;
}
.label {
line-height: 1;
}
.count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 6px;
border-radius: 999px;
background: var(--color-primary-100);
color: var(--bg-white-100);
font-size: 12px;
font-weight: 600;
}
}
.compare-tools-tabs-active {
position: absolute;
left: 0;
bottom: -1px;
height: 2px;
background: var(--color-primary-100);
border-radius: 2px;
transition: transform 0.2s ease, width 0.2s ease;
}
.compare-tools-actions {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
margin-left: auto;
justify-content: flex-end;
font-size: 16px;
font-weight: 400;
color: var(--text-primary-65-color);
:deep(.el-checkbox__label) {
font-size: 16px;
font-weight: 400;
color: var(--text-primary-65-color);
}
}
.compare-tools-switches {
display: flex;
align-items: center;
gap: 16px;
}
.compare-tools-switch {
display: inline-flex;
align-items: center;
gap: 8px;
.label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 16px;
font-weight: 400;
color: var(--text-primary-65-color);
}
}
.label-icon {
width: 16px;
height: 16px;
}
.compare-tools-search {
display: inline-flex;
align-items: center;
gap: 8px;
}
.find-word-box {
width: 430px;
height: 60px;
border: 1px solid rgba(230, 231, 232, 1);
background-color: #ffffff;
border-radius: 6px;
display: flex;
align-items: center;
position: absolute;
right: 0;
top: -68px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.06);
.find-word-input {
flex: 1;
min-width: 0;
padding-left: 8px;
}
.find-word-limit {
border-right: solid 1px rgba(230, 231, 232, 1);
color: #5f656c;
padding: 0 16px 0 8px;
white-space: nowrap;
}
.find-word-icon {
padding: 6px 10px;
margin: 0 2px;
cursor: pointer;
display: inline-flex;
align-items: center;
}
}
.find-word-wrap {
position: relative;
display: inline-flex;
align-items: center;
}
.find-word-open-btn {
height: 32px;
background-color: var(--bg-white-100);
border-color: var(--bg-black-10);
color: var(--text-primary-80-color);
&:hover {
background-color: var(--bg-black-5);
border-color: var(--bg-black-10);
color: var(--text-primary-80-color);
}
&:active {
background-color: var(--bg-black-10);
border-color: var(--bg-black-10);
color: var(--text-primary-80-color);
}
}
.find-word-open-icon {
margin-right: 4px;
}
:deep(span.term-find-highlight) {
background-color: #ffff00;
}
:deep(span.term-find-highlight.is-active) {
background-color: #ff9632;
}
.compare-columns {
margin-top: 16px;
display: flex;
flex-direction: column;
}
.compare-row {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
}
.compare-cell {
width: 100%;
box-sizing: border-box;
border-radius: 2px;
background: var(--bg-white-100);
display: flex;
align-items: flex-start;
position: relative;
padding: 16px;
}
.compare-row:nth-child(2n-1) .compare-cell {
background: var(--bg-black-2);
}
.compare-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 0 0;
}
.compare-footer-text {
font-size: 14px;
color: var(--text-primary-65-color);
white-space: nowrap;
}
.compare-footer-right {
display: flex;
justify-content: flex-end;
flex: 1;
min-width: 0;
}
.term-empty {
width: 100%;
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(132, 136, 142, 1);
}
.term-body {
display: flex;
column-gap: 18px;
flex: 1;
min-width: 0;
width: 100%;
}
.term-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
row-gap: 6px;
}
.term-row {
display: flex;
align-items: flex-start;
column-gap: 18px;
}
.term-no-cn {
font-size: 16px;
font-weight: 700;
line-height: 24px;
color: var(--color-primary-100);
white-space: nowrap;
width: 90px;
text-align: center;
flex: 0 0 90px;
}
.term-no-en {
font-size: 14px;
font-weight: 400;
line-height: 24px;
color: var(--color-primary-100);
white-space: nowrap;
width: 90px;
text-align: center;
flex: 0 0 90px;
}
.term-content-cn {
flex: 1;
font-size: 16px;
font-weight: 700;
line-height: 24px;
color: var(--text-primary-80-color);
:deep(.term-entity) {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0 6px 0 4px;
background: var(--color-primary-10);
cursor: pointer;
transition: background-color 0.15s ease, transform 0.15s ease;
}
:deep(.term-entity:hover) {
background: var(--color-primary-35);
transform: translateY(-1px);
}
:deep(.term-entity::before) {
content: "";
display: inline-block;
width: 14px;
height: 14px;
flex: 0 0 14px;
background-color: var(--color-primary-100);
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M448 768c176.736 0 320-143.264 320-320S624.736 128 448 128 128 271.264 128 448s143.264 320 320 320zm0 64C236.288 832 64 659.712 64 448S236.288 64 448 64s384 172.288 384 384-172.288 384-384 384z'/%3E%3Cpath d='M832 832a32 32 0 0 1-22.624-9.376l-160-160a32 32 0 0 1 45.248-45.248l160 160A32 32 0 0 1 832 832z'/%3E%3C/svg%3E");
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
}
.term-content-en {
flex: 1;
font-size: 14px;
font-weight: 400;
line-height: 24px;
color: var(--text-primary-65-color);
:deep(.term-entity) {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0 6px 0 4px;
background: var(--color-primary-10);
cursor: pointer;
transition: background-color 0.15s ease, transform 0.15s ease;
}
:deep(.term-entity:hover) {
background: var(--color-primary-35);
transform: translateY(-1px);
}
:deep(.term-entity::before) {
content: "";
display: inline-block;
width: 14px;
height: 14px;
flex: 0 0 14px;
background-color: var(--color-primary-100);
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M448 768c176.736 0 320-143.264 320-320S624.736 128 448 128 128 271.264 128 448s143.264 320 320 320zm0 64C236.288 832 64 659.712 64 448S236.288 64 448 64s384 172.288 384 384-172.288 384-384 384z'/%3E%3Cpath d='M832 832a32 32 0 0 1-22.624-9.376l-160-160a32 32 0 0 1 45.248-45.248l160 160A32 32 0 0 1 832 832z'/%3E%3C/svg%3E");
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
}
@media (max-width: 1680px) {
.compare-row,
.compare-top {
grid-template-columns: 1fr;
}
.compare-tools-actions {
width: 100%;
}
.compare-tools-search {
width: 100%;
}
.find-word-box {
width: 100%;
}
}
</style>
...@@ -63,12 +63,18 @@ ...@@ -63,12 +63,18 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<div class="home-main-header-item-box"> <div class="home-main-header-item-box" v-if="govInsList.length">
<div class="item" v-for="(item, index) in govInsList" :key="index" @click="handleToInstitution(item)"> <div class="item" v-for="(item, index) in govInsList.slice(0, 7)" :key="index" @click="handleToInstitution(item)">
<div class="item-left"> <div class="item-left">
<img :src="item.img ? item.img : DefaultIcon2" alt="" /> <img :src="item.orgImage || DefaultIcon2" alt="" />
</div>
<div class="item-right one-line-ellipsis">{{ item.orgName }}</div>
<div class="item-num">{{ item.total }}</div>
<el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
</div> </div>
<div class="item-right">{{ item.name }}</div> <div class="item">
<div class="item-num item-more">查看全部机构 ({{govInsList.length+1}}家)</div>
<el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
</div> </div>
</div> </div>
</div> </div>
...@@ -115,15 +121,7 @@ ...@@ -115,15 +121,7 @@
{{ item.name }} {{ item.name }}
</div> </div>
<div class="box1-main-right-info"> <div class="box1-main-right-info">
<!-- <div class="tag" :class="{ <AreaTag v-for="(tag, index) in item.industryList" :key="index" :tagName="tag.industryName" />
tag1: tag.status == 1,
tag2: tag.status == 2,
tag3: tag.status == 3
}" v-for="(tag, index) in item.industryList" :key="index">
{{ tag.industryName }}
</div> -->
<AreaTag v-for="(tag, index) in item.industryList" :key="index" :tagName="tag.industryName">
</AreaTag>
</div> </div>
<div class="box1-main-right-center"> <div class="box1-main-right-center">
{{ item.describe }} {{ item.describe }}
...@@ -243,6 +241,7 @@ ...@@ -243,6 +241,7 @@
<div class="header-title">{{ "关键行政令" }}</div> <div class="header-title">{{ "关键行政令" }}</div>
</div> </div>
<div class="box7-main"> <div class="box7-main">
<div class="box7-list">
<div class="box7-item" v-for="(item, index) in keyDecreeList" :key="index" @click="handleKeyDecree(item)"> <div class="box7-item" v-for="(item, index) in keyDecreeList" :key="index" @click="handleKeyDecree(item)">
<div class="icon"> <div class="icon">
<img src="./assets/images/warning.png" alt="" /> <img src="./assets/images/warning.png" alt="" />
...@@ -253,14 +252,11 @@ ...@@ -253,14 +252,11 @@
<div class="time">{{ item.time }}</div> <div class="time">{{ item.time }}</div>
</div> </div>
<div class="info-content">{{ item.content ? item.content : "暂无数据" }}</div> <div class="info-content">{{ item.content ? item.content : "暂无数据" }}</div>
<!-- <el-popover effect="dark" :width="800" :content="item.content" placement="top-start">
<template #reference>
<div class="info-content">{{ item.content ? item.content : "暂无数据" }}</div>
</template>
</el-popover> -->
</div> </div>
</div> </div>
</div> </div>
<SimplePagination v-model:current-page="keyDecreeInfo.page" :page-size="keyDecreeInfo.size" :total="keyDecreeInfo.total" @page-change="handleGetKeyDecree" />
</div>
</div> </div>
<div class="box8"> <div class="box8">
<div class="box8-header"> <div class="box8-header">
...@@ -269,17 +265,19 @@ ...@@ -269,17 +265,19 @@
</div> </div>
<div class="header-title">{{ "政令重点条款" }}</div> <div class="header-title">{{ "政令重点条款" }}</div>
</div> </div>
<div class="box8-main" id="wordCloudChart"></div> <div class="box8-content" v-if="wordCloudData?.length">
<WordCloudChart :data="wordCloudData" width="100%" height="100%" />
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="home-main-footer"> <div class="home-main-footer">
<DivideHeader id="position4" class="divide4" :titleText="'资源库'"></DivideHeader> <DivideHeader id="position4" class="divide4" :titleText="'科技政令库'"></DivideHeader>
<div class="home-main-footer-header"> <div class="home-main-footer-header">
<div class="search-box"> <div class="search-box">
<el-select v-model="searchType" :empty-values="[null, undefined]" style="width: 100%"> <el-select v-model="searchType" :empty-values="[null, undefined]" style="width: 100%" filterable>
<el-option label="全部政府部门" value="" /> <el-option label="全部政府部门" value="" />
<el-option v-for="item in govInsList" :key="item.id" :label="item.name" :value="item.id" /> <el-option v-for="item in govInsList" :key="item.orgId" :label="item.orgName" :value="item.orgId" />
</el-select> </el-select>
</div> </div>
<div style="flex: auto;"></div> <div style="flex: auto;"></div>
...@@ -306,38 +304,23 @@ ...@@ -306,38 +304,23 @@
</div> </div>
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <div class="checkbox-group">
<el-checkbox v-for="type in decreeTypeList" :key="type.id" v-model="checkedDecreeType" <el-checkbox v-for="type in decreeTypeList" :key="type.id" v-model="checkedDecreeType" :label="type.typeId"
:label="type.typeId" style="width: 180px" class="filter-checkbox" style="width: 180px" class="filter-checkbox"
@change="handleChangeCheckedDecreeType"> @change="handleChangeCheckedDecreeType">
{{ type.typeName }} {{ type.typeName }}
</el-checkbox> </el-checkbox>
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="select-box">
<div class="select-box-header">
<div class="icon"></div>
<div class="title">{{ "发布机构" }}</div>
</div>
<div class="select-main">
<div class="checkbox-group">
<el-checkbox v-for="cate in govInsList" :key="cate.id" v-model="checkedGovIns"
:label="cate.id" style="width: 180px" class="filter-checkbox"
@change="handleChangeCheckedGovIns">
{{ cate.name }}
</el-checkbox>
</div>
</div>
</div> -->
<div class="select-box"> <div class="select-box">
<div class="select-box-header"> <div class="select-box-header">
<div class="icon"></div> <div class="icon"></div>
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main select-main1"> <div class="select-main">
<div class="checkbox-group"> <div class="checkbox-group">
<el-checkbox v-for="area in areaList" :key="area.id" v-model="activeAreaList" :label="area.id" <el-checkbox v-for="area in areaList" :key="area.id" v-model="activeAreaList" :label="area.id"
style="width: 100px" @change="checked => handleAreaChange(area.id, checked)"> style="width: 100px" class="filter-checkbox" @change="checked => handleAreaChange(area.id, checked)">
{{ area.name }} {{ area.name }}
</el-checkbox> </el-checkbox>
</div> </div>
...@@ -351,28 +334,12 @@ ...@@ -351,28 +334,12 @@
<div class="select-main"> <div class="select-main">
<div class="checkbox-group"> <div class="checkbox-group">
<el-checkbox v-for="time in pubTime" :key="time.id" v-model="activePubTime" :label="time.id" <el-checkbox v-for="time in pubTime" :key="time.id" v-model="activePubTime" :label="time.id"
style="width: 100px" class="filter-checkbox" style="width: 100px" class="filter-checkbox" @change="checked => handlePubTimeChange(time.id, checked)">
@change="checked => handlePubTimeChange(time.id, checked)">
{{ time.name }} {{ time.name }}
</el-checkbox> </el-checkbox>
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="select-box">
<div class="select-box-header">
<div class="icon"></div>
<div class="title">{{ "涉及领域" }}</div>
</div>
<div class="select-main select-main1">
<div class="checkbox-group">
<el-checkbox v-for="area in areaList" :key="area.id" v-model="activeAreaList"
:label="area.id" style="width: 100px"
@change="checked => handleAreaChange(area.id, checked)">
{{ area.name }}
</el-checkbox>
</div>
</div>
</div> -->
</div> </div>
<div class="right"> <div class="right">
<div class="content-header"> <div class="content-header">
...@@ -403,9 +370,7 @@ ...@@ -403,9 +370,7 @@
</div> </div>
<div class="desc">{{ item.desc }}</div> <div class="desc">{{ item.desc }}</div>
<div class="tag-box"> <div class="tag-box">
<div class="tag" v-for="(val, idx) in item.tagList" :key="idx"> <AreaTag v-for="(tag, index) in item.tagList" :key="index" :tagName="tag.industryName" />
{{ val.industryName }}
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -427,9 +392,10 @@ ...@@ -427,9 +392,10 @@
</template> </template>
<script setup> <script setup>
import NewsList from "@/components/base/NewsList/index.vue"; import { onMounted, ref, watch, nextTick, reactive } from "vue";
import { onMounted, ref, watch, nextTick } from "vue";
import router from "@/router"; import router from "@/router";
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import SimplePagination from "@/components/SimplePagination.vue";
import { import {
getDepartmentList, getDepartmentList,
getLatestDecree, getLatestDecree,
...@@ -448,7 +414,6 @@ import DivideHeader from "@/components/DivideHeader.vue"; ...@@ -448,7 +414,6 @@ import DivideHeader from "@/components/DivideHeader.vue";
import { useContainerScroll } from "@/hooks/useScrollShow"; import { useContainerScroll } from "@/hooks/useScrollShow";
import getBarChart from "./utils/barChart"; import getBarChart from "./utils/barChart";
import getPieChart from "./utils/piechart"; import getPieChart from "./utils/piechart";
import getWordCloudChart from "./utils/wordCloudChart";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
...@@ -458,11 +423,11 @@ import { ElMessage } from "element-plus"; ...@@ -458,11 +423,11 @@ import { ElMessage } from "element-plus";
// 跳转行政机构主页 // 跳转行政机构主页
const handleToInstitution = item => { const handleToInstitution = item => {
window.sessionStorage.setItem("curTabName", item.name); window.sessionStorage.setItem("curTabName", item.orgName);
const curRoute = router.resolve({ const curRoute = router.resolve({
path: "/institution", path: "/institution",
query: { query: {
id: item.id id: item.orgId
} }
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
...@@ -480,32 +445,15 @@ const handleCurrentChange = page => { ...@@ -480,32 +445,15 @@ const handleCurrentChange = page => {
}; };
// 页面 header // 页面 header
const govInsList = ref([ const govInsList = ref([]);
// {
// img: Gov1,
// name: "美国白宫"
// },
// {
// img: Gov2,
// name: "美国财政部"
// },
]);
const checkedGovIns = ref([]); const checkedGovIns = ref([]);
const handleChangeCheckedGovIns = val => { };
const handleGetDepartmentList = async () => { const handleGetDepartmentList = async () => {
try { try {
const res = await getDepartmentList(); const res = await getDepartmentList();
console.log("机构列表", res); console.log("机构列表", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
govInsList.value = res.data.map(item => { govInsList.value = res.data;
return {
id: item.orgId,
name: item.orgName,
img: item.orgImage
};
});
} }
} catch (error) { } catch (error) {
console.error("获取机构列表error", error); console.error("获取机构列表error", error);
...@@ -649,11 +597,8 @@ const newsList = ref([ ...@@ -649,11 +597,8 @@ const newsList = ref([
// } // }
]); ]);
const handleGetNews = async () => { const handleGetNews = async () => {
const params = {
moduleId: "0101"
};
try { try {
const res = await getNews(params); const res = await getNews({moduleId: "0101"});
console.log("新闻资讯", res); console.log("新闻资讯", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
// newsList.value = res.data || [] // newsList.value = res.data || []
...@@ -671,7 +616,6 @@ const handleGetNews = async () => { ...@@ -671,7 +616,6 @@ const handleGetNews = async () => {
console.error("新闻资讯error", error); console.error("新闻资讯error", error);
} }
}; };
handleGetNews();
// 点击新闻条目,跳转到新闻分析页 // 点击新闻条目,跳转到新闻分析页
const handleToNewsAnalysis = news => { const handleToNewsAnalysis = news => {
const route = router.resolve({ const route = router.resolve({
...@@ -914,13 +858,19 @@ const handleBox6YearChange = () => { ...@@ -914,13 +858,19 @@ const handleBox6YearChange = () => {
// 关键行政令 // 关键行政令
const keyDecreeList = ref([]); const keyDecreeList = ref([]);
const keyDecreeInfo = reactive({
total: 0,
page: 1,
size: 3,
})
const handleGetKeyDecree = async () => { const handleGetKeyDecree = async () => {
try { try {
const res = await getKeyDecree(); const res = await getKeyDecree({pageSize:keyDecreeInfo.size, pageNum:keyDecreeInfo.page-1});
console.log("关键行政令", res); console.log("关键行政令", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data?.total) {
keyDecreeList.value = res.data.map(item => { keyDecreeInfo.total = res.data.total || 0;
keyDecreeList.value = res.data.list.map(item => {
return { return {
title: item.name, title: item.name,
content: item.describe, content: item.describe,
...@@ -935,38 +885,17 @@ const handleGetKeyDecree = async () => { ...@@ -935,38 +885,17 @@ const handleGetKeyDecree = async () => {
handleGetKeyDecree(); handleGetKeyDecree();
// 政令重点条款 // 政令重点条款
const wordCloudData = [ const wordCloudData = ref([]);
// { name: "与马斯克公开冲突", value: 100 },
// { name: "传统能源", value: 5 },
// { name: "共和党财政鹰派", value: 77 },
// { name: "未实现赤字控制目标", value: 35 },
// { name: "得克萨斯州", value: 88 },
// { name: "选举压力", value: 57 },
// { name: "主张财政紧缩", value: 72 },
// { name: "财政保守", value: 18 },
];
const handleGetDecreeKeyInstruction = async () => { const handleGetDecreeKeyInstruction = async () => {
try { try {
const res = await getDecreeKeyInstruction(); const res = await getDecreeKeyInstruction();
console.log("政令重点条款", res); console.log("政令重点条款", res);
wordCloudData.value = res.data.map(item => { wordCloudData.value = res.data.map(item => ({name: item.clause, value: item.count}));
return {
name: item.clause,
value: item.count
};
});
} catch (error) { } catch (error) {
console.error("政令重点条款error", error); console.error("政令重点条款error", error);
} }
}; };
const handleBox8 = async () => {
await handleGetDecreeKeyInstruction();
let chart3 = getWordCloudChart(wordCloudData.value);
setChart(chart3, "wordCloudChart");
};
// 资源库 // 资源库
const searchType = ref(""); const searchType = ref("");
const isChina = ref(false); const isChina = ref(false);
...@@ -995,22 +924,6 @@ const handleToPosi = id => { ...@@ -995,22 +924,6 @@ const handleToPosi = id => {
} }
}; };
// const handleGetAreaList = async () => {
// try {
// const res = await getDecreehylyList();
// console.log("行业领域列表", res);
// if (res.code === 200 && res.data) {
// areaList.value = res.data.map(item => {
// return {
// name: item.name,
// id: item.id
// };
// });
// console.log("areaList", areaList.value);
// }
// } catch (error) { }
// };
// 政令类型 // 政令类型
const decreeTypeList = ref([]); const decreeTypeList = ref([]);
const checkedDecreeType = ref([]); const checkedDecreeType = ref([]);
...@@ -1101,7 +1014,7 @@ const handleGetAreaList = async () => { ...@@ -1101,7 +1014,7 @@ const handleGetAreaList = async () => {
console.log("行业领域列表", res); console.log("行业领域列表", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
areaList.value = [ areaList.value = [
{ name: "全", id: "all" }, { name: "全部领域", id: "all" },
...res.data.map(item => { ...res.data.map(item => {
return { return {
name: item.name, name: item.name,
...@@ -1218,13 +1131,14 @@ const handleSearch = () => { ...@@ -1218,13 +1131,14 @@ const handleSearch = () => {
}; };
onMounted(async () => { onMounted(async () => {
handleGetNews();
handleGetDecreeTypeList(); handleGetDecreeTypeList();
handleGetAreaList(); handleGetAreaList();
handleGetDecreeOrderList(); handleGetDecreeOrderList();
handleBox1(); // 最新科技政令 handleBox1(); // 最新科技政令
handleBox5(); handleBox5();
handleBox6(); handleBox6();
handleBox8(); handleGetDecreeKeyInstruction();
}); });
</script> </script>
...@@ -1476,15 +1390,17 @@ onMounted(async () => { ...@@ -1476,15 +1390,17 @@ onMounted(async () => {
} }
.home-main-header-item-box { .home-main-header-item-box {
margin-top: 48px; margin: 48px 0 64px;
margin-bottom: 64px;
width: 1600px; width: 1600px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px;
.item { .item {
width: 254px; width: 20%;
height: 72px; flex: auto;
height: 80px;
padding: 0 16px;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
background: rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.65);
...@@ -1492,8 +1408,7 @@ onMounted(async () => { ...@@ -1492,8 +1408,7 @@ onMounted(async () => {
border-radius: 10px; border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
align-items: center; align-items: center;
gap: 17px; justify-content: center;
margin: 0 6px 16px 6px;
cursor: pointer; cursor: pointer;
transition: transition:
transform 0.3s ease, transform 0.3s ease,
...@@ -1505,10 +1420,9 @@ onMounted(async () => { ...@@ -1505,10 +1420,9 @@ onMounted(async () => {
} }
.item-left { .item-left {
margin-left: 24px;
width: 48px; width: 48px;
height: 48px; height: 48px;
font-size: 0px;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -1516,12 +1430,28 @@ onMounted(async () => { ...@@ -1516,12 +1430,28 @@ onMounted(async () => {
} }
.item-right { .item-right {
width: 140px; width: 20px;
flex: auto;
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
line-height: 24px; line-height: 20px;
margin: 0 16px;
}
.item-num {
white-space: nowrap;
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
line-height: 20px;
margin-right: 2px;
color: var(--color-primary-100);
}
.item-more {
margin-right: 12px;
font-size: 16px;
} }
} }
} }
...@@ -1708,15 +1638,10 @@ onMounted(async () => { ...@@ -1708,15 +1638,10 @@ onMounted(async () => {
line-height: 26px; line-height: 26px;
display: -webkit-box; display: -webkit-box;
/* 将元素设置为弹性盒模型 */
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
/* 限制文本显示的行数 */
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
/* 设置弹性盒子的子元素垂直排列 */
overflow: hidden; overflow: hidden;
/* 隐藏溢出的内容 */
text-overflow: ellipsis; text-overflow: ellipsis;
/* 文本溢出时显示省略号 */
} }
.box1-main-right-info { .box1-main-right-info {
...@@ -1724,58 +1649,21 @@ onMounted(async () => { ...@@ -1724,58 +1649,21 @@ onMounted(async () => {
display: flex; display: flex;
height: 24px; height: 24px;
gap: 8px; gap: 8px;
.tag {
height: 24px;
line-height: 24px;
padding: 0 8px;
box-sizing: border-box;
border-radius: 4px;
margin-right: 5px;
border: 1px solid rgba(255, 163, 158, 1);
color: rgba(245, 34, 45, 1);
background: rgba(255, 241, 240, 1);
}
.tag1 {
border: 1px solid rgba(135, 232, 222, 1);
color: rgba(19, 168, 168, 1);
background: rgba(230, 255, 251, 1);
}
.tag2 {
border: 1px solid rgba(186, 224, 255, 1);
background: rgba(230, 244, 255, 1);
color: rgba(22, 119, 255, 1);
}
.tag3 {
border: 1px solid rgba(255, 229, 143, 1);
color: rgba(250, 173, 20, 1);
background: rgba(255, 251, 230, 1);
}
.tag4 {
border: 1px solid rgba(255, 163, 158, 1);
color: rgba(245, 34, 45, 1);
background: rgba(255, 241, 240, 1);
}
} }
.box1-main-right-center { .box1-main-right-center {
margin-top: 10px; margin-top: 10px;
height: 200px; height: 180px;
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 30px; line-height: 30px;
display: -webkit-box; display: -webkit-box;
/* 将元素设置为弹性盒模型 */ -webkit-line-clamp: 6;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
/* 隐藏溢出的内容 */
text-overflow: ellipsis; text-overflow: ellipsis;
/* 文本溢出时显示省略号 */
} }
.box1-main-right-footer { .box1-main-right-footer {
...@@ -2730,15 +2618,6 @@ onMounted(async () => { ...@@ -2730,15 +2618,6 @@ onMounted(async () => {
font-weight: 400; font-weight: 400;
} }
} }
.info-content {
margin-top: 3px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 30px;
}
} }
} }
} }
...@@ -2912,10 +2791,9 @@ onMounted(async () => { ...@@ -2912,10 +2791,9 @@ onMounted(async () => {
.box7-main { .box7-main {
margin-top: 10px; margin-top: 10px;
height: 380px;
box-sizing: border-box; .box7-list {
overflow-y: auto; height: 310px;
overflow-x: hidden;
.box7-item { .box7-item {
width: 730px; width: 730px;
...@@ -2985,7 +2863,12 @@ onMounted(async () => { ...@@ -2985,7 +2863,12 @@ onMounted(async () => {
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 24px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
}
} }
} }
} }
...@@ -2999,9 +2882,11 @@ onMounted(async () => { ...@@ -2999,9 +2882,11 @@ onMounted(async () => {
border-radius: 10px; border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2); box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
display: flex;
flex-direction: column;
.box8-header { .box8-header {
width: 792px; width: 100%;
height: 48px; height: 48px;
display: flex; display: flex;
border-bottom: 1px solid rgba(240, 242, 244, 1); border-bottom: 1px solid rgba(240, 242, 244, 1);
...@@ -3030,6 +2915,12 @@ onMounted(async () => { ...@@ -3030,6 +2915,12 @@ onMounted(async () => {
} }
} }
.box8-content {
width: 100%;
height: 20px;
flex: auto;
}
.box8-main { .box8-main {
height: 401px; height: 401px;
} }
...@@ -3039,7 +2930,6 @@ onMounted(async () => { ...@@ -3039,7 +2930,6 @@ onMounted(async () => {
.home-main-footer { .home-main-footer {
margin-top: 34px; margin-top: 34px;
max-height: 1860px;
padding-bottom: 160px; padding-bottom: 160px;
background: rgba(248, 249, 250, 1); background: rgba(248, 249, 250, 1);
overflow: hidden; overflow: hidden;
...@@ -3133,16 +3023,21 @@ onMounted(async () => { ...@@ -3133,16 +3023,21 @@ onMounted(async () => {
.left { .left {
width: 360px; width: 360px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2); height: 100%;
background: rgba(255, 255, 255, 1); padding-bottom: 24px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px; border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
background: rgba(255, 255, 255, 1);
position: relative;
.select-box { .select-box {
margin-top: 16px; margin-top: 16px;
.select-box-header { .select-box-header {
display: flex; display: flex;
gap: 16px; gap: 17px;
.icon { .icon {
margin-top: 4px; margin-top: 4px;
...@@ -3155,7 +3050,7 @@ onMounted(async () => { ...@@ -3155,7 +3050,7 @@ onMounted(async () => {
.title { .title {
height: 24px; height: 24px;
color: var(--color-main-active); color: var(--color-main-active);
font-family: Microsoft YaHei; font-family: "Source Han Sans CN";
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
line-height: 24px; line-height: 24px;
...@@ -3165,11 +3060,20 @@ onMounted(async () => { ...@@ -3165,11 +3060,20 @@ onMounted(async () => {
} }
.select-main { .select-main {
margin-left: 25px; margin-left: 24px;
} margin-top: 12px;
.select-main1 { .checkbox-group {
width: 260px; display: grid;
grid-template-columns: repeat(2, 160px);
gap: 8px 4px;
.filter-checkbox {
width: 160px;
height: 24px;
margin-right: 0 !important;
}
}
} }
} }
} }
...@@ -3177,7 +3081,6 @@ onMounted(async () => { ...@@ -3177,7 +3081,6 @@ onMounted(async () => {
.right { .right {
width: 20px; width: 20px;
flex: auto; flex: auto;
max-height: 1489px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2); box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
box-sizing: border-box; box-sizing: border-box;
...@@ -3213,12 +3116,9 @@ onMounted(async () => { ...@@ -3213,12 +3116,9 @@ onMounted(async () => {
} }
.content-box { .content-box {
max-height: 1367px;
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
overflow: hidden; overflow: hidden;
min-height: 790px; min-height: 790px;
overflow: hidden;
overflow-y: auto;
box-sizing: border-box; box-sizing: border-box;
.main-item { .main-item {
...@@ -3349,20 +3249,6 @@ onMounted(async () => { ...@@ -3349,20 +3249,6 @@ onMounted(async () => {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
.tag {
height: 28px;
line-height: 28px;
text-align: center;
padding: 0 8px;
border-radius: 4px;
background: rgba(231, 243, 255, 1);
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
letter-spacing: 0px;
}
} }
} }
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div class="box1"> <div class="box1">
<AnalysisBox title="相关政令" :showAllBtn="false"> <AnalysisBox title="相关政令" :showAllBtn="false">
<div class="box1-main"> <div class="box1-main">
<el-empty v-if="siderList.length===0" style="padding-top: 30%" description="暂无数据" :image-size="100" /> <el-empty v-if="!siderList?.length" style="padding-top: 40%;" description="暂无数据" :image-size="100" />
<el-scrollbar height="100%" always> <el-scrollbar height="100%" always>
<div class="left-item" :class="{ 'item-active': false }" v-for="(item, index) in siderList" :key="index" @click="handleClickDecree(item)"> <div class="left-item" :class="{ 'item-active': false }" v-for="(item, index) in siderList" :key="index" @click="handleClickDecree(item)">
<div class="item-head"> <div class="item-head">
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
</div> </div>
<div class="box2"> <div class="box2">
<AnalysisBox title="政令关系挖掘" :showAllBtn="false"> <AnalysisBox title="政令关系挖掘" :showAllBtn="false">
<el-empty v-if="!siderList?.length" style="padding-top: 20%;" description="暂无数据" :image-size="100" />
<div class="box2-main"> <div class="box2-main">
<div ref="containerRef" class="graph-container"></div> <div ref="containerRef" class="graph-container"></div>
</div> </div>
......
...@@ -8,22 +8,16 @@ ...@@ -8,22 +8,16 @@
<div class="layout-main-header-left-box"> <div class="layout-main-header-left-box">
<div class="left-box-top"> <div class="left-box-top">
<div class="icon"> <div class="icon">
<img <img :src="summaryInfo.imageUrl || USALogo" alt="" />
v-if="summaryInfo.imageUrl"
:src="summaryInfo.imageUrl"
alt=""
style="height: 40px; margin-top: 12px"
/>
<img v-else :src="USALogo" alt="" />
</div> </div>
<div class="info"> <div class="info">
<div class="info-box1">{{ summaryInfo.name }}</div> <div class="info-box1 one-line-ellipsis">{{ summaryInfo.name || "--" }}</div>
<div class="info-box2"> <div class="info-box2">
<div class="info-box2-item item1">{{ summaryInfo.postDate }}</div> <div class="info-box2-item">{{ summaryInfo.postDate || "--" }}</div>
| |
<div class="info-box2-item item2">{{ summaryInfo.orgName }}</div> <div class="info-box2-item">{{ summaryInfo.orgName || "--" }}</div>
| |
<div class="info-box2-item item3">{{ summaryInfo.ename }}</div> <div class="info-box2-item one-line-ellipsis">{{ summaryInfo.ename || "--" }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -47,8 +41,8 @@ ...@@ -47,8 +41,8 @@
</div> </div>
<div class="layout-main-header-right-box"> <div class="layout-main-header-right-box">
<div class="right-box-top"> <div class="right-box-top">
<div class="time">{{ summaryInfo.postDate }}</div> <div class="time">{{ summaryInfo.postDate || "--" }}</div>
<div class="name">{{ summaryInfo.orgName }}</div> <div class="name">{{ summaryInfo.orgName || "--" }}</div>
</div> </div>
<div class="right-box-bottom"> <div class="right-box-bottom">
<div class="btn" @click="handleShowReport"> <div class="btn" @click="handleShowReport">
...@@ -203,12 +197,12 @@ const mainHeaderBtnList = ref([ ...@@ -203,12 +197,12 @@ const mainHeaderBtnList = ref([
name: "深度挖掘", name: "深度挖掘",
path: "/decreeLayout/deepDig" path: "/decreeLayout/deepDig"
}, },
// { {
// icon: icon3, icon: icon3,
// activeIcon: icon3Active, activeIcon: icon3Active,
// name: "影响分析", name: "影响分析",
// path: "/decreeLayout/influence" path: "/decreeLayout/influence"
// }, },
]); ]);
const activeTitle = ref("政令概况"); const activeTitle = ref("政令概况");
...@@ -377,9 +371,9 @@ onMounted(() => { ...@@ -377,9 +371,9 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
.header-main { .header-main {
width: 100%; width: 100%;
background-color: #fff; border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.05); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
overflow: hidden; background: rgba(255, 255, 255, 1);
} }
.layout-main-header { .layout-main-header {
width: 1600px; width: 1600px;
...@@ -396,30 +390,32 @@ onMounted(() => { ...@@ -396,30 +390,32 @@ onMounted(() => {
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
} }
.layout-main-header-left-box { .layout-main-header-left-box {
width: 1100px; width: 20px;
margin-top: 13px; flex: auto;
margin-top: 12px;
.left-box-top { .left-box-top {
height: 64px; height: 64px;
display: flex; display: flex;
align-items: center; align-items: center;
.icon { .icon {
width: 64px; width: 64px;
height: 64px; height: 40px;
border-radius: 4px;
overflow: hidden; overflow: hidden;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: fill;
} }
} }
.info { .info {
width: 700px; margin-left: 10px;
margin-left: 9px; margin-right: 40px;
width: 20px;
flex: auto;
.info-box1 { .info-box1 {
width: 700px; width: 100%;
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
font-size: 20px; font-size: 20px;
...@@ -428,9 +424,6 @@ onMounted(() => { ...@@ -428,9 +424,6 @@ onMounted(() => {
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
margin-top: 5px; margin-top: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.info-box2 { .info-box2 {
margin-top: 5px; margin-top: 5px;
...@@ -444,15 +437,13 @@ onMounted(() => { ...@@ -444,15 +437,13 @@ onMounted(() => {
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
display: flex; display: flex;
margin-left: -10px;
.info-box2-item { .info-box2-item {
white-space: nowrap;
padding: 0 10px; padding: 0 10px;
} }
.item3 { .info-box2-item:first-child {
max-width: 420px; padding-left: 0px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
} }
...@@ -498,9 +489,9 @@ onMounted(() => { ...@@ -498,9 +489,9 @@ onMounted(() => {
} }
} }
.layout-main-header-right-box { .layout-main-header-right-box {
width: 450px;
margin-top: 19px;
.right-box-top { .right-box-top {
white-space: nowrap;
padding-top: 11px;
.time { .time {
height: 24px; height: 24px;
line-height: 24px; line-height: 24px;
...@@ -710,49 +701,5 @@ onMounted(() => { ...@@ -710,49 +701,5 @@ onMounted(() => {
} }
} }
} }
// .tool-box {
// position: fixed;
// z-index: 10000;
// bottom: 80px;
// left: 0;
// width: 48px;
// height: 144px;
// border-radius: 0px 10px 10px 0px;
// box-shadow: 0px 0px 15px 0px rgba(22, 119, 255, 0.1);
// background: rgba(255, 255, 255, 1);
// .tool1 {
// width: 17px;
// height: 18px;
// margin-top: 17px;
// margin-left: 16px;
// cursor: pointer;
// img {
// width: 100%;
// height: 100%;
// }
// }
// .tool2 {
// width: 22px;
// height: 20px;
// margin-top: 26px;
// margin-left: 14px;
// cursor: pointer;
// img {
// width: 100%;
// height: 100%;
// }
// }
// .tool3 {
// width: 20px;
// height: 20px;
// margin-top: 25px;
// margin-left: 15px;
// cursor: pointer;
// img {
// width: 100%;
// height: 100%;
// }
// }
// }
} }
</style> </style>
\ No newline at end of file
<template>
<div class="relation-graph-wrapper">
<div class="graph-controls">
<!-- 这项政令标志着中美AI竞争进入一个新阶段,其核心特征是 “精准封锁”与“体系输出”相结合。它短期内无疑会给中国AI产业链带来压力,但长期看,这场竞争更可能是一场围绕技术路线、生态系统和治理规则的持久战。 -->
<div v-for="item in controlBtns" :key="item.type" :class="['control-btn', { 'control-btn-active': currentLayoutType === item.type }]" @click="handleClickControlBtn(item.type)">
<img :src="item.icon" alt="" />
</div>
</div>
<div ref="containerRef" class="graph-container"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import G6 from '@antv/g6'
import { Close } from '@element-plus/icons-vue'
import echartsIcon01 from './assets/images/echartsicon01.png'
import echartsIcon02 from './assets/images/echartsicon02.png'
import echartsIcon03 from './assets/images/echartsicon03.png'
const props = defineProps({
graphData: {
type: Object,
default: () => ({ nodes: [], links: [] })
},
treeData: {
type: Object,
default: () => null
},
controlActive: {
type: Number,
default: 1
}
})
const emit = defineEmits(['nodeClick', 'layoutChange'])
const containerRef = ref(null)
const graphInstance = ref(null)
const currentLayoutType = ref(1)
const controlBtns = [
{ type: 1, icon: echartsIcon01, name: '力导向布局' },
{ type: 2, icon: echartsIcon02, name: '树布局' },
{ type: 3, icon: echartsIcon03, name: '环状布局' }
]
const initGraph = (layoutType = 1) => {
if (!containerRef.value) return
destroyGraph()
nextTick(() => {
const width = containerRef.value.offsetWidth || 800
const height = containerRef.value.offsetHeight || 600
switch (layoutType) {
case 1:
initNormalGraph(layoutType, width, height)
break
case 2:
initTreeGraph(width, height)
break
case 3:
initCircularGraph(width, height)
break
}
})
}
const initNormalGraph = (layoutType, width, height) => {
const data = processGraphData(props.graphData)
if (!data.nodes || data.nodes.length === 0) return
const layout = {
type: 'force',
center: [width / 2, height / 2],
preventOverlap: true,
nodeSpacing: 80,
linkDistance: 250,
nodeStrength: -800,
edgeStrength: 0.1,
collideStrength: 0.8,
alphaDecay: 0.01,
alphaMin: 0.001
}
graphInstance.value = new G6.Graph({
container: containerRef.value,
width,
height,
fitView: true,
fitViewPadding: 100,
fitCenter: true,
animate: true,
animateCfg: {
duration: 300,
easing: 'easeLinear'
},
minZoom: 0.1,
maxZoom: 10,
modes: {
default: [
'drag-canvas',
'zoom-canvas',
'drag-node',
{
type: 'activate-relations',
trigger: 'mouseenter',
resetSelected: true
}
]
},
layout,
defaultNode: {
type: 'image',
size: 40,
clipCfg: {
show: true,
type: 'circle',
r: 20
},
labelCfg: {
position: 'bottom',
offset: 10,
style: {
fill: '#333',
fontSize: 11,
fontFamily: 'Microsoft YaHei',
textAlign: 'center',
background: {
fill: 'rgba(255, 255, 255, 0.95)',
padding: [4, 6, 4, 6],
radius: 4
}
}
}
},
defaultEdge: {
type: 'quadratic',
style: {
stroke: '#5B8FF9',
lineWidth: 3,
opacity: 0.9,
endArrow: {
path: 'M 0,0 L 12,6 L 12,-6 Z',
fill: '#5B8FF9'
}
},
labelCfg: {
autoRotate: true,
style: {
fill: '#333',
fontSize: 10,
fontFamily: 'Microsoft YaHei',
background: {
fill: '#fff',
padding: [2, 4, 2, 4],
radius: 2
}
}
}
},
nodeStateStyles: {
active: {
shadowColor: '#1459BB',
shadowBlur: 15,
stroke: '#1459BB',
lineWidth: 3
},
inactive: {
opacity: 0.3
}
},
edgeStateStyles: {
active: {
stroke: '#1459BB',
lineWidth: 4
},
inactive: {
opacity: 0.15
}
}
})
graphInstance.value.data(data)
graphInstance.value.render()
bindGraphEvents()
}
const initCircularGraph = (width, height) => {
const data = processGraphData(props.graphData)
if (!data.nodes || data.nodes.length === 0) return
const centerX = width / 2
const centerY = height / 2
const radius = Math.min(width, height) / 2 - 120
const otherNodes = data.nodes.filter(n => !n.isCenter)
const nodeCount = otherNodes.length
otherNodes.forEach((node, index) => {
const angle = (2 * Math.PI * index) / nodeCount - Math.PI / 2
node.x = centerX + radius * Math.cos(angle)
node.y = centerY + radius * Math.sin(angle)
})
const centerNode = data.nodes.find(n => n.isCenter)
if (centerNode) {
centerNode.x = centerX
centerNode.y = centerY
centerNode.fx = centerX
centerNode.fy = centerY
}
graphInstance.value = new G6.Graph({
container: containerRef.value,
width,
height,
fitView: false,
fitCenter: false,
animate: true,
animateCfg: {
duration: 300,
easing: 'easeLinear'
},
minZoom: 0.1,
maxZoom: 10,
modes: {
default: [
'drag-canvas',
'zoom-canvas',
'drag-node',
{
type: 'activate-relations',
trigger: 'mouseenter',
resetSelected: true
}
]
},
defaultNode: {
type: 'image',
size: 40,
clipCfg: {
show: true,
type: 'circle',
r: 20
},
labelCfg: {
position: 'bottom',
offset: 10,
style: {
fill: '#333',
fontSize: 11,
fontFamily: 'Microsoft YaHei',
textAlign: 'center',
background: {
fill: 'rgba(255, 255, 255, 0.95)',
padding: [4, 6, 4, 6],
radius: 4
}
}
}
},
defaultEdge: {
type: 'quadratic',
style: {
stroke: '#5B8FF9',
lineWidth: 3,
opacity: 0.9,
endArrow: {
path: 'M 0,0 L 12,6 L 12,-6 Z',
fill: '#5B8FF9'
}
},
labelCfg: {
autoRotate: true,
style: {
fill: '#333',
fontSize: 10,
fontFamily: 'Microsoft YaHei',
background: {
fill: '#fff',
padding: [2, 4, 2, 4],
radius: 2
}
}
}
},
nodeStateStyles: {
active: {
shadowColor: '#1459BB',
shadowBlur: 15,
stroke: '#1459BB',
lineWidth: 3
},
inactive: {
opacity: 0.3
}
},
edgeStateStyles: {
active: {
stroke: '#1459BB',
lineWidth: 4
},
inactive: {
opacity: 0.15
}
}
})
graphInstance.value.data(data)
graphInstance.value.render()
bindGraphEvents()
}
const initTreeGraph = (width, height) => {
const treeDataSource = convertGraphToTree(props.graphData)
if (!treeDataSource) return
graphInstance.value = new G6.TreeGraph({
container: containerRef.value,
width,
height,
fitView: true,
fitViewPadding: 80,
animate: true,
animateCfg: {
duration: 300,
easing: 'easeLinear'
},
minZoom: 0.1,
maxZoom: 10,
modes: {
default: [
'drag-canvas',
'zoom-canvas',
'drag-node',
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const data = item.getModel()
data.collapsed = collapsed
return true
}
}
]
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d) {
return d.id
},
getHeight: function getHeight() {
return 16
},
getWidth: function getWidth() {
return 16
},
getVGap: function getVGap() {
return 30
},
getHGap: function getHGap() {
return 120
}
},
defaultNode: {
type: 'image',
size: 40,
clipCfg: {
show: true,
type: 'circle',
r: 20
},
labelCfg: {
position: 'right',
offset: 10,
style: {
fill: '#333',
fontSize: 11,
fontFamily: 'Microsoft YaHei',
background: {
fill: 'rgba(255, 255, 255, 0.95)',
padding: [4, 6, 4, 6],
radius: 4
}
}
}
},
defaultEdge: {
type: 'cubic-horizontal',
style: {
stroke: '#5B8FF9',
lineWidth: 3
}
},
nodeStateStyles: {
active: {
shadowColor: '#1459BB',
shadowBlur: 15,
stroke: '#1459BB',
lineWidth: 3
}
}
})
graphInstance.value.data(treeDataSource)
graphInstance.value.render()
graphInstance.value.fitView()
bindGraphEvents()
}
const convertGraphToTree = (graphData) => {
if (!graphData || !graphData.nodes || graphData.nodes.length === 0) {
return null
}
const nodes = graphData.nodes
const links = graphData.links || graphData.edges || []
const centerNode = nodes[0]
const centerId = String(centerNode.id || '0')
const childIdSet = new Set()
const childrenNodes = []
links.forEach((link) => {
const source = String(link.source)
const target = String(link.target)
if (source === centerId && !childIdSet.has(target)) {
const node = nodes.find(n => String(n.id) === target)
if (node) {
childIdSet.add(target)
childrenNodes.push({
id: target,
label: node.name || '',
img: node.image || echartsIcon03,
size: node.symbolSize || 40,
name: node.name,
image: node.image,
isSanctioned: node.isSanctioned
})
}
} else if (target === centerId && !childIdSet.has(source)) {
const node = nodes.find(n => String(n.id) === source)
if (node) {
childIdSet.add(source)
childrenNodes.push({
id: source,
label: node.name || '',
img: node.image || echartsIcon03,
size: node.symbolSize || 40,
name: node.name,
image: node.image,
isSanctioned: node.isSanctioned
})
}
}
})
if (childrenNodes.length === 0) {
nodes.slice(1).forEach((node) => {
const nodeId = String(node.id)
if (!childIdSet.has(nodeId)) {
childIdSet.add(nodeId)
childrenNodes.push({
id: nodeId,
label: node.name || '',
img: node.image || echartsIcon03,
size: node.symbolSize || 40,
name: node.name,
image: node.image,
isSanctioned: node.isSanctioned
})
}
})
}
return {
id: centerId,
label: centerNode.name || '',
img: centerNode.image || echartsIcon03,
size: centerNode.symbolSize || 60,
name: centerNode.name,
image: centerNode.image,
isSanctioned: centerNode.isSanctioned,
children: childrenNodes
}
}
const processGraphData = (rawData) => {
if (!rawData || !rawData.nodes || rawData.nodes.length === 0) {
return { nodes: [], edges: [] }
}
const nodeMap = new Map()
const nodes = []
rawData.nodes.forEach((node, index) => {
const nodeId = String(node.id || index)
if (nodeMap.has(nodeId)) {
return
}
nodeMap.set(nodeId, true)
const isCenter = index === 0
const size = node.symbolSize || (isCenter ? 60 : 40)
nodes.push({
id: nodeId,
label: node.name || '',
img: node.image || echartsIcon03,
size,
isCenter,
clipCfg: {
show: true,
type: 'circle',
r: size / 2
},
style: {
cursor: 'pointer'
},
labelCfg: {
position: 'bottom',
offset: 12,
style: {
fill: isCenter ? '#1459BB' : '#333',
fontSize: isCenter ? 13 : 11,
fontWeight: isCenter ? 'bold' : 'normal',
fontFamily: 'Microsoft YaHei',
textAlign: 'center'
}
},
...node,
id: nodeId
})
})
const edgeMap = new Map()
const edges = []
const rawEdges = rawData.links || rawData.edges || []
rawEdges.forEach((edge, index) => {
const source = String(edge.source)
const target = String(edge.target)
const edgeKey = `${source}-${target}`
if (edgeMap.has(edgeKey)) {
return
}
if (!nodeMap.has(source) || !nodeMap.has(target)) {
return
}
edgeMap.set(edgeKey, true)
edges.push({
id: `edge-${index}`,
source,
target,
label: edge.name || ''
})
})
return { nodes, edges }
}
const bindGraphEvents = () => {
if (!graphInstance.value) return
graphInstance.value.on('node:click', (evt) => {
const node = evt.item
const model = node.getModel()
emit('nodeClick', model)
})
graphInstance.value.on('canvas:click', () => {
})
}
const handleClickControlBtn = (btn) => {
currentLayoutType.value = btn
emit('layoutChange', btn)
initGraph(btn)
}
const destroyGraph = () => {
if (graphInstance.value) {
graphInstance.value.destroy()
graphInstance.value = null
}
}
const handleResize = () => {
if (graphInstance.value && containerRef.value) {
const width = containerRef.value.offsetWidth
const height = containerRef.value.offsetHeight
graphInstance.value.changeSize(width, height)
graphInstance.value.fitView()
}
}
watch(
() => props.graphData,
() => {
initGraph(currentLayoutType.value)
}
)
watch(
() => props.treeData,
() => {
if (currentLayoutType.value === 2) {
initGraph(2)
}
}
)
watch(
() => props.controlActive,
(newVal) => {
if (newVal !== currentLayoutType.value) {
handleClickControlBtn(newVal)
}
}
)
onMounted(() => {
initGraph(1)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
destroyGraph()
})
defineExpose({
refresh: () => initGraph(currentLayoutType.value),
changeLayout: (type) => handleClickControlBtn(type),
getGraph: () => graphInstance.value
})
</script>
<style lang="scss" scoped>
.relation-graph-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.graph-container {
width: 100%;
height: 100%;
}
.graph-controls {
position: absolute;
top: 16px;
right: 16px;
display: flex;
gap: 8px;
z-index: 10;
.control-btn {
width: 32px;
height: 32px;
border-radius: 4px;
border: 1px solid rgba(234, 236, 238, 1);
background: rgba(255, 255, 255, 1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
img {
width: 16px;
height: 16px;
}
&:hover {
border-color: rgba(5, 95, 194, 0.5);
}
}
.control-btn-active {
border-color: rgba(5, 95, 194, 1);
background: rgba(231, 243, 255, 1);
}
}
</style>
\ No newline at end of file
<template>
<div class="view-box">
<div class="icon-left">
<img src="../../assets/icons/ai.png" alt="">
</div>
<div class="tips-content">{{ props.tips }}</div>
<div class="icon-right">
<img src="../../assets/icons/right.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> <template>
<div class="view-box"> <div class="view-box">
<div class="right-main"> <el-empty v-if="!dataList?.length" style="padding-top: 15%;" description="暂无数据" :image-size="100" />
<div class="right-main-content"> <div v-if="dataList.length" class="main-content-main">
<div class="hintWrap"> <div class="main-mask"
<div class="icon1"></div> @wheel.prevent="handleWheel"
<div class="title"> @mousedown="handleMouseDown"
这项政令标志着中美AI竞争进入一个新阶段,其核心特征是 “精准封锁”与“体系输出”相结合。它短期内无疑会给中国AI产业链带来压力,但长期看,这场竞争更可能是一场围绕技术路线、生态系统和治理规则的持久战。 @mouseup="handleMouseUp"
</div> @mouseleave="handleMouseUp"
<div class="icon2Wrap"> @mousemove="handleMouseMove"
<div class="icon2"></div> ></div>
</div> <div class="fishbone-container" :style="{ transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`, transformOrigin: 'center center' }">
</div>
<div class="right-main-content-main">
<div class="fishbone-wrapper">
<div class="fishbone-scroll-container" ref="scrollContainerRef">
<div class="fishbone" v-if="dataList.length > 0">
<div class="main-line" :style="{ width: dataList.length * 200 + 300 + 'px' }">
<!-- 主轴上的标签 --> <!-- 主轴上的标签 -->
<div class="main-line" :style="{ width: dataList.length * 200 + 300 + 'px' }">
<div class="main-line-text" v-for="(item, index) in dataList" :key="'label-' + index" <div class="main-line-text" v-for="(item, index) in dataList" :key="'label-' + index"
:class="{ :class="{
'blue-theme': index < 2, 'blue-theme': index < 2,
...@@ -27,36 +22,35 @@ ...@@ -27,36 +22,35 @@
</div> </div>
</div> </div>
<!-- 奇数索引的数据组放在上方 --> <!-- 奇数索引的数据组放在上方 -->
<div v-for="(causeGroup, groupIndex) in onFilterData(1)" :key="'top-' + groupIndex" <div v-for="(causeGroup, groupIndex) in onFilterData(1)" :key="groupIndex"
:class="getTopBoneClass(groupIndex)" :style="{ left: groupIndex * 400 + 420 + 'px' }"> class="top-bone" :style="{ left: groupIndex * 400 + 510 + 'px', height: (causeGroup.causes?.length) * 22 + 100 + 'px' }">
<div class="left-bone"> <div class="left-bone">
<div class="left-bone-item" v-for="(item, index) in getLeftItems(causeGroup.causes)" :key="'left-' + index"> <div class="left-bone-item" v-for="item in getLeftItems(causeGroup.causes)" :key="item.id">
<img :src="defaultIcon2 || item.picture" alt="" class="company-icon" /> <img :src="defaultIcon2 || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div> <div class="text" :title="item.name">{{ item.name }}</div>
<div class="line"></div> <div class="line"></div>
</div> </div>
</div> </div>
<div class="right-bone"> <div class="right-bone">
<div class="right-bone-item" v-for="(item, index) in getRightItems(causeGroup.causes)" :key="'right-' + index"> <div class="right-bone-item" v-for="item in getRightItems(causeGroup.causes)" :key="item.id">
<div class="line"></div> <div class="line"></div>
<img :src="defaultIcon2 || item.picture" alt="" class="company-icon" /> <img :src="defaultIcon2 || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div> <div class="text" :title="item.name">{{ item.name }}</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 偶数索引的数据组放在下方 --> <!-- 偶数索引的数据组放在下方 -->
<div v-for="(causeGroup, groupIndex) in onFilterData(0)" :key="'bottom-' + groupIndex" <div v-for="(causeGroup, groupIndex) in onFilterData(0)" :key="groupIndex"
:class="getBottomBoneClass(groupIndex)" :style="{ left: groupIndex * 400 + 220 + 'px' }"> class="bottom-bone" :style="{ left: groupIndex * 400 + 310 + 'px', height: (causeGroup.causes?.length) * 22 + 100 + 'px' }">
<div class="left-bone"> <div class="left-bone">
<div class="left-bone-item" v-for="(item, index) in getLeftItems(causeGroup.causes)" :key="'left-bottom-' + index"> <div class="left-bone-item" v-for="item in getRightItems(causeGroup.causes)" :key="item.id">
<img :src="defaultIcon2 || item.picture" alt="" class="company-icon" /> <img :src="defaultIcon2 || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div> <div class="text" :title="item.name">{{ item.name }}</div>
<div class="line"></div> <div class="line"></div>
</div> </div>
</div> </div>
<div class="right-bone"> <div class="right-bone">
<div class="right-bone-item" v-for="(item, index) in getRightItems(causeGroup.causes)" :key="'right-bottom-' + index"> <div class="right-bone-item" v-for="item in getLeftItems(causeGroup.causes)" :key="item.id">
<div class="line"></div> <div class="line"></div>
<img :src="defaultIcon2 || item.picture" alt="" class="company-icon" /> <img :src="defaultIcon2 || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div> <div class="text" :title="item.name">{{ item.name }}</div>
...@@ -64,14 +58,8 @@ ...@@ -64,14 +58,8 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else
style="display: flex; justify-content: center; align-items: center; height: 200px; width: 100%">
<el-empty description="暂无相关数据" />
</div>
</div> </div>
</div> <div v-if="dataList.length" class="main-content-footer">
</div>
<div class="right-main-content-footer">
<div class="footer-item footer-item1"> <div class="footer-item footer-item1">
<div class="footer-item-bottom"> <div class="footer-item-bottom">
<div class="icon"> <div class="icon">
...@@ -119,16 +107,49 @@ ...@@ -119,16 +107,49 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</template> </template>
<script setup name="ChartChain"> <script setup name="ChartChain">
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import defaultIcon2 from "@/assets/icons/default-icon2.png"; import defaultIcon2 from "@/assets/icons/default-icon2.png";
import noticeIcon from "./assets/images/notice-icon.png"; import noticeIcon from "../assets/images/notice-icon.png";
import { getDeepMiningSelect, getDeepMiningIndustry, getDeepMiningIndustryFishbone, getDeepMiningIndustryEntity } from "@/api/exportControlV2.0"; import { getDeepMiningSelect, getDeepMiningIndustry, getDeepMiningIndustryFishbone, getDeepMiningIndustryEntity } from "@/api/exportControlV2.0";
// 缩放功能处理
const scale = ref(1)
const minScale = 0.1
const maxScale = 10
const handleWheel = (e) => {
if (e.deltaY < 0) {
// 放大:不超过最大值
scale.value = Math.min(scale.value + 0.1, maxScale)
} else {
// 缩小:不低于最小值
scale.value = Math.max(scale.value - 0.1, minScale)
}
}
// 移动功能处理
const translateX = ref(0) // X轴位移
const translateY = ref(0) // Y轴位移
let isDragging = false
let startX = 0
let startY = 0
const handleMouseMove = (e) => {
if (!isDragging) return
translateX.value = e.clientX - startX
translateY.value = e.clientY - startY
}
const handleMouseDown = (e) => {
// 排除右键/中键,只响应左键(e.button=0为左键)
if (e.button !== 0) return
isDragging = true
startX = e.clientX - translateX.value
startY = e.clientY - translateY.value
}
const handleMouseUp = () => {
isDragging = false
}
// 实体清单-深度挖掘-产业链中国企业实体信息查询 // 实体清单-深度挖掘-产业链中国企业实体信息查询
const cnEntityOnChainData = ref({}); const cnEntityOnChainData = ref({});
const getCnEntityOnChainData = async () => { const getCnEntityOnChainData = async () => {
...@@ -173,16 +194,6 @@ const getRightItems = items => { ...@@ -173,16 +194,6 @@ const getRightItems = items => {
const midpoint = Math.ceil(items.length / 2); const midpoint = Math.ceil(items.length / 2);
return items.slice(midpoint); return items.slice(midpoint);
}; };
// 获取上方鱼骨图位置类名
const getTopBoneClass = index => {
const positions = ["top-bone", "top-bone1", "top-bone2"];
return positions[index % 3] || "top-bone";
};
// 获取下方鱼骨图位置类名
const getBottomBoneClass = index => {
const positions = ["bottom-bone", "bottom-bone1", "bottom-bone2"];
return positions[index % 3] || "bottom-bone";
};
const getFishboneData = async () => { const getFishboneData = async () => {
const currentSanction = sanctionList.value.find(item => item.id === currentSanctionId.value); const currentSanction = sanctionList.value.find(item => item.id === currentSanctionId.value);
const date = currentSanction ? currentSanction.date : ''; const date = currentSanction ? currentSanction.date : '';
...@@ -305,126 +316,35 @@ onMounted(() => { ...@@ -305,126 +316,35 @@ onMounted(() => {
.view-box { .view-box {
width: 100%; width: 100%;
height: 100%; height: 100%;
}
.right-main {
height: 100%;
padding: 11px 16px 20px;
.right-main-content {
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.hintWrap { .main-content-main {
display: flex; position: relative;
align-items: center;
padding: 7px 12px;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 250, 255, 1);
margin-bottom: 9px;
.icon1 {
width: 19px;
height: 20px; height: 20px;
background-image: url("../assets/icons/ai.png"); flex: auto;
background-size: 100% 100%;
flex-shrink: 0;
}
.title {
color: rgb(5, 95, 194);
font-size: 16px;
font-weight: 400;
line-height: 24px;
margin-left: 13px;
flex: 1;
}
.icon2Wrap {
width: 24px;
height: 24px;
background-color: rgba(231, 243, 255, 1);
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
border-radius: 12px; justify-content: center;
margin-left: 20px;
flex-shrink: 0;
.icon2 {
width: 24px;
height: 24px;
background-image: url("../assets/icons/right.png");
background-size: 100% 100%;
}
}
}
.right-main-content-main {
flex: 1;
position: relative;
overflow: hidden; overflow: hidden;
.main-mask {
.fishbone-wrapper { position: absolute;
position: relative; top: 0;
width: 100%; left: 0;
height: 100%;
}
.fishbone-scroll-container {
display: flex;
align-items: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-x: auto; z-index: 3;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(144, 202, 249, 0.5) transparent;
&::-webkit-scrollbar {
height: 6px;
} }
&::-webkit-scrollbar-track { .fishbone-container {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(144, 202, 249, 0.5);
border-radius: 3px;
}
}
.fishbone {
position: relative; position: relative;
width: fit-content;
height: 100%;
margin-top: 40px;
min-width: 100%;
padding-left: 275px;
margin-left: 40px;
.main-line { .main-line {
margin-top: 280px;
width: 1888px;
height: 3px; height: 3px;
background: rgb(230, 231, 232); background: rgb(230, 231, 232);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 100px;
// 虚线
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
// 添加中间的文字块 // 添加中间的文字块
.main-line-text { .main-line-text {
...@@ -472,28 +392,28 @@ onMounted(() => { ...@@ -472,28 +392,28 @@ onMounted(() => {
.top-bone { .top-bone {
position: absolute; position: absolute;
top: 20px; bottom: 0px;
right: 200px;
width: 3px; width: 3px;
height: 260px;
background: rgb(230, 231, 232); background: rgb(230, 231, 232);
transform-origin: bottom center;
transform: skew(30deg); transform: skew(30deg);
z-index: 1; z-index: 1;
.left-bone { .left-bone {
color: #777; color: #777;
position: absolute; position: absolute;
top: 0; top: -20px;
left: -150px; right: 0;
width: 150px; width: 180px;
height: 50px; height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
// overflow: hidden;
.left-bone-item { .left-bone-item {
transform: skew(-30deg); transform: skew(-30deg);
height: 45px; height: 40px;
margin-bottom: 2px; margin: 4px 0;
margin-top: 2px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
...@@ -519,17 +439,18 @@ onMounted(() => { ...@@ -519,17 +439,18 @@ onMounted(() => {
.right-bone { .right-bone {
color: #777; color: #777;
position: absolute; position: absolute;
top: 0; top: -44px;
right: -150px; left: 0;
width: 150px; width: 180px;
height: 210px; height: 100%;
overflow: hidden; display: flex;
flex-direction: column;
justify-content: flex-end;
.right-bone-item { .right-bone-item {
transform: skew(-30deg); transform: skew(-30deg);
height: 39px; height: 40px;
margin-bottom: 2px; margin: 4px 0;
margin-top: 2px;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
...@@ -554,39 +475,30 @@ onMounted(() => { ...@@ -554,39 +475,30 @@ onMounted(() => {
} }
} }
.top-bone1 {
@extend .top-bone;
right: 500px;
}
.top-bone2 {
@extend .top-bone;
right: 800px;
}
.bottom-bone { .bottom-bone {
position: absolute; position: absolute;
top: 280px; top: 0px;
right: 360px;
width: 3px; width: 3px;
height: 260px;
background: rgb(230, 231, 232); background: rgb(230, 231, 232);
transform-origin: top center;
transform: skew(-30deg); transform: skew(-30deg);
z-index: 1; z-index: 1;
.left-bone { .left-bone {
color: #777; color: #777;
position: absolute; position: absolute;
top: 50px; bottom: -44px;
left: -150px; right: 0;
width: 150px; width: 180px;
height: 260px; height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
.left-bone-item { .left-bone-item {
transform: skew(30deg); transform: skew(30deg);
height: 39px; height: 40px;
margin-bottom: 2px; margin: 4px 0;
margin-top: 2px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
...@@ -613,16 +525,18 @@ onMounted(() => { ...@@ -613,16 +525,18 @@ onMounted(() => {
.right-bone { .right-bone {
color: #777; color: #777;
position: absolute; position: absolute;
top: 50px; bottom: -20px;
right: -150px; left: 0;
width: 150px; width: 180px;
height: 260px; height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
.right-bone-item { .right-bone-item {
transform: skew(30deg); transform: skew(30deg);
height: 35px; height: 40px;
margin-bottom: 2px; margin: 4px 0;
margin-top: 2px;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
...@@ -646,19 +560,9 @@ onMounted(() => { ...@@ -646,19 +560,9 @@ onMounted(() => {
} }
} }
} }
.bottom-bone1 {
@extend .bottom-bone;
right: 660px;
}
.bottom-bone2 {
@extend .bottom-bone;
right: 960px;
}
} }
.right-main-content-footer { .main-content-footer {
margin-top: 16px; margin-top: 16px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
...@@ -725,6 +629,5 @@ onMounted(() => { ...@@ -725,6 +629,5 @@ onMounted(() => {
} }
} }
} }
}
} }
</style> </style>
\ No newline at end of file
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
</div> </div>
<div class="data-title">实体名称</div> <div class="data-title">实体名称</div>
<div style="height: 20px; flex: auto;"> <div style="height: 20px; flex: auto;">
<el-empty v-if="showCompanyList.length === 0" style="padding-top: 30%" description="暂无数据" :image-size="100" /> <el-empty v-if="!showCompanyList?.length" style="padding-top: 35%;" description="暂无数据" :image-size="100" />
<el-scrollbar height="100%" always> <el-scrollbar height="100%" always>
<div class="list-data"> <div class="list-data">
<div class="list-item" v-for="item in showCompanyList" :key="item.id" :class="{ 'item-active': activeEntityId === item.id }" @click="handleToCompanyDetail(item)"> <div class="list-item" v-for="item in showCompanyList" :key="item.id" :class="{ 'item-active': activeEntityId === item.id }" @click="handleToCompanyDetail(item)">
...@@ -75,19 +75,15 @@ ...@@ -75,19 +75,15 @@
</div> </div>
</div> </div>
</template> </template>
<div class="box2-main" v-if="contentType==1"> <div class="box2-main">
<AiTips :tips="tips" />
<div class="graph-box" v-if="contentType==1">
<ChartChain /> <ChartChain />
</div> </div>
<div class="box2-main" v-if="contentType==2"> <div class="graph-box" v-if="contentType==2">
<!-- <ChartRelation
:graph-data="graphData"
:tree-data="treeData"
:control-active="1"
@node-click="handleNodeClick"
@layout-change="handleLayoutChange"
/> -->
<GraphChart :nodes="testData.nodes" :links="testData.links" layoutType="force" /> <GraphChart :nodes="testData.nodes" :links="testData.links" layoutType="force" />
</div> </div>
</div>
</AnalysisBox> </AnalysisBox>
</div> </div>
</div> </div>
...@@ -101,9 +97,9 @@ import getBarChart from "./utils/barChart"; ...@@ -101,9 +97,9 @@ import getBarChart from "./utils/barChart";
import { getDecreeIndustry, getDecreehylyList, getDecreeCompany } from "@/api/decree/influence"; import { getDecreeIndustry, getDecreehylyList, getDecreeCompany } from "@/api/decree/influence";
import { getCnEntityOnChain, getChainInfoByDomainId } from "@/api/exportControl"; import { getCnEntityOnChain, getChainInfoByDomainId } from "@/api/exportControl";
import { getSingleSanctionEntitySupplyChain } from "@/api/exportControlV2.0"; import { getSingleSanctionEntitySupplyChain } from "@/api/exportControlV2.0";
import ChartChain from "./ChartChain.vue"; import ChartChain from "./com/ChartChain.vue";
import AiTips from "./com/AiTips.vue";
import GraphChart from "@/components/base/GraphChart/index.vue"; import GraphChart from "@/components/base/GraphChart/index.vue";
import ChartRelation from "./ChartRelation.vue";
import defaultIcon2 from "@/assets/icons/default-icon2.png"; import defaultIcon2 from "@/assets/icons/default-icon2.png";
import noticeIcon from "./assets/images/notice-icon.png"; import noticeIcon from "./assets/images/notice-icon.png";
import icon422 from "./assets/images/icon422.png"; import icon422 from "./assets/images/icon422.png";
...@@ -113,191 +109,65 @@ import icon1621 from "./assets/images/icon1621.png"; ...@@ -113,191 +109,65 @@ import icon1621 from "./assets/images/icon1621.png";
import company from "./assets/images/company.png"; import company from "./assets/images/company.png";
import companyActive from "./assets/images/company-active.png"; import companyActive from "./assets/images/company-active.png";
const tips = "这项政令标志着中美AI竞争进入一个新阶段,其核心特征是 “精准封锁”与“体系输出”相结合。它短期内无疑会给中国AI产业链带来压力,但长期看,这场竞争更可能是一场围绕技术路线、生态系统和治理规则的持久战。"
// 关系图数据 // 关系图数据
const testData = { const testData = {
// 节点数据 // 节点数据
nodes: [ nodes: [
{ id: 0, name: "泰丰先行", symbolSize: 60, symbol: `image://${company}`, x:0, y:0 },
{ id: 1, name: "国轩高科", symbolSize: 40, symbol: `image://${company}` },
{ id: 2, name: "智方纳米", symbolSize: 40, symbol: `image://${company}` },
{ id: 3, name: "香百科技", symbolSize: 40, symbol: `image://${company}` },
{ id: 4, name: "格林滨", symbolSize: 40, symbol: `image://${company}` },
{ id: 5, name: "江西紫宸", symbolSize: 40, symbol: `image://${company}` },
{ id: 6, name: "紫江企业", symbolSize: 40, symbol: `image://${company}` },
{ id: 7, name: "大而美法案", symbolSize: 40, symbol: `image://${company}` },
{ id: 8, name: "比亚迪", symbolSize: 40, symbol: `image://${company}` },
],
// 关系数据
links: [
{ {
id: 0, source: 1, target: 0,
name: "泰丰先行", label: { show: true, color: "#055FC2", backgroundColor: "#E7F3FF", borderWidth: 0, offset: [0, 15], formatter: '持股' },
// category: 0, lineStyle: { color: '#B9DCFF', type: "solid" }
symbolSize: 30,
value: 8,
symbol: `image://${company}`,
x: 50,
y: 10
},
{
id: 1,
name: "国轩高科",
// category: 0,
symbolSize: 30,
value: 9,
symbol: `image://${company}`,
x: 150,
y: 10
},
{
id: 2,
name: "智方纳米",
// category: 2,
symbolSize: 30,
value: 7,
symbol: `image://${company}`,
x: 250,
y: 10
},
{
id: 3,
name: "香百科技",
// category: 1,
symbolSize: 30,
value: 6,
symbol: `image://${company}`,
x: 350,
y: 10
},
{
id: 4,
name: "格林滨",
// category: 2,
symbolSize: 30,
value: 6,
symbol: `image://${company}`,
x: 450,
y: 10
},
{
id: 5,
name: "江西紫宸",
// category: 2,
symbolSize: 30,
value: 7,
symbol: `image://${company}`,
x: 550,
y: 10
},
{
id: 6,
name: "紫江企业",
// category: 4,
symbolSize: 30,
value: 6,
symbol: `image://${company}`,
x: 650,
y: 10
},
{
id: 7,
name: "大而美法案",
// category: 4,
symbolSize: 50,
value: 5,
symbol: `image://${company}`,
x: 300,
y: 200
},
{
id: 8,
name: "比亚迪",
// category: 0,
symbolSize: 30,
value: 10,
symbol: `image://${company}`,
x: 50,
y: 400
}, },
{ {
id: 9, source: 2, target: 0,
name: "铜陵有色", label: { show: true, color: "#055FC2", backgroundColor: "#E7F3FF", borderWidth: 0, offset: [0, 15], formatter: '持股' },
// category: 3, lineStyle: { color: '#B9DCFF', type: "solid" }
symbolSize: 30,
value: 8,
symbol: `image://${company}`,
x: 150,
y: 400
}, },
{ {
id: 10, source: 3, target: 0,
name: "长盛精密", label: { show: true, color: "#055FC2", backgroundColor: "#E7F3FF", borderWidth: 0, offset: [0, 15], formatter: '合作' },
// category: 1, lineStyle: { color: '#B9DCFF', type: "solid" }
symbolSize: 30,
value: 7,
symbol: `image://${company}`,
x: 250,
y: 400
}, },
{ {
id: 11, source: 4, target: 0,
name: "天合光能", label: { show: true, color: "#055FC2", backgroundColor: "#E7F3FF", borderWidth: 0, offset: [0, 15], formatter: '从属' },
// category: 0, lineStyle: { color: '#B9DCFF', type: "solid" }
symbolSize: 30,
value: 8,
symbol: `image://${company}`,
x: 350,
y: 400
}, },
{ {
id: 12, source: 5, target: 0,
name: "昆仑化学", label: { show: true, color: "#055FC2", backgroundColor: "#E7F3FF", borderWidth: 0, offset: [0, 15], formatter: '合作' },
// category: 2, lineStyle: { color: '#B9DCFF', type: "solid" }
symbolSize: 30,
value: 6,
symbol: `image://${company}`,
x: 250,
y: 400
}, },
{ {
id: 13, source: 6, target: 0,
name: "嘉源科技", label: { show: true, color: "#055FC2", backgroundColor: "#E7F3FF", borderWidth: 0, offset: [0, 15], formatter: '持股' },
// category: 1, lineStyle: { color: '#B9DCFF', type: "solid" }
symbolSize: 30,
value: 6,
symbol: `image://${company}`,
x: 450,
y: 400
}, },
{ {
id: 14, source: 7, target: 0,
name: "华阳集团", label: { show: true, color: "#055FC2", backgroundColor: "#E7F3FF", borderWidth: 0, offset: [0, 15], formatter: '合作' },
// category: 4, lineStyle: { color: '#B9DCFF', type: "solid" }
symbolSize: 30,
value: 7,
symbol: `image://${company}`,
x: 550,
y: 400
}, },
{ {
id: 15, source: 8, target: 0,
name: "海辰智能", label: { show: true, color: "#055FC2", backgroundColor: "#E7F3FF", borderWidth: 0, offset: [0, 15], formatter: '合作' },
// category: 1, lineStyle: { color: '#B9DCFF', type: "solid" }
symbolSize: 30,
value: 7,
symbol: `image://${company}`,
x: 650,
y: 400
}, },
], ],
// 关系数据
links: [
{ source: 1, target: 7, label: { show: true, formatter: '合作' } },
{ source: 2, target: 7, label: { show: true, formatter: '持股' } },
{ source: 3, target: 7, label: { show: true, formatter: '合作' } },
{ source: 4, target: 7, lineStyle: { type: 'dashed', color: '#d32f2f' }, label: { show: true, formatter: '从属' } },
{ source: 5, target: 7, label: { show: true, formatter: '合作' } },
{ source: 6, target: 7, label: { show: true, formatter: '持股' } },
{ source: 0, target: 7, label: { show: true, formatter: '持股' } },
{ source: 8, target: 7, label: { show: true, formatter: '合作' } },
{ source: 9, target: 7, lineStyle: { type: 'dashed', color: '#d32f2f' }, label: { show: true, formatter: '从属' } },
{ source: 10, target: 7, lineStyle: { type: 'dashed', color: '#d32f2f' }, label: { show: true, formatter: '合作' } },
{ source: 11, target: 7, label: { show: true, formatter: '合作' } },
{ source: 12, target: 7, label: { show: true, formatter: '合作' } },
{ source: 13, target: 7, label: { show: true, formatter: '合作' } },
{ source: 14, target: 7, label: { show: true, formatter: '合作' } },
{ source: 15, target: 7, label: { show: true, formatter: '合作', color: 'red', borderColor: 'red' } },
],
}; };
// 受影响实体 // 受影响实体
...@@ -374,7 +244,7 @@ const handleGetHylyList = async () => { ...@@ -374,7 +244,7 @@ const handleGetHylyList = async () => {
}; };
// 产业链/实体关系 // 产业链/实体关系
const contentType = ref(2); const contentType = ref(1);
const headerContentType = (type) => { const headerContentType = (type) => {
contentType.value = type; contentType.value = type;
}; };
...@@ -693,7 +563,7 @@ onMounted(() => { ...@@ -693,7 +563,7 @@ onMounted(() => {
align-items: flex-end; align-items: flex-end;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0 16px; padding: 0 20px;
.title-left { .title-left {
display: flex; display: flex;
border: 1px solid rgb(5, 95, 194); border: 1px solid rgb(5, 95, 194);
...@@ -734,6 +604,14 @@ onMounted(() => { ...@@ -734,6 +604,14 @@ onMounted(() => {
.box2-main { .box2-main {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex;
flex-direction: column;
padding: 16px 20px;
.graph-box {
height: 20px;
flex: auto;
margin-top: 16px;
}
} }
} }
} }
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<div class="box1-main"> <div class="box1-main">
<div class="box1-item" v-for="(item, index) in backgroundList" :key="index"> <div class="box1-item" v-for="(item, index) in backgroundList" :key="index">
<div class="id">{{ index + 1 }}</div> <div class="id">{{ index + 1 }}</div>
<div class="title">{{ item.content }}</div> <div class="title text-align-justify">{{ item.content }}</div>
<div class="open"> <div class="open">
<img src="./assets/images/open-icon.png" alt="" /> <img src="./assets/images/open-icon.png" alt="" />
</div> </div>
...@@ -37,14 +37,17 @@ ...@@ -37,14 +37,17 @@
<div class="box2-main"> <div class="box2-main">
<div class="custom-collapse"> <div class="custom-collapse">
<el-collapse v-model="dependActive"> <el-collapse v-model="dependActive">
<el-collapse-item v-for="(item, index) in dependList" :key="item.billId" title="Consistency" :name="item.billId"> <el-collapse-item v-for="(item, index) in dependList" :key="item.billId" :name="item.billId">
<template #icon> <template #icon>
<el-icon><ArrowDownBold /></el-icon> <el-icon v-if="dependActive.includes(item.billId)"><ArrowDownBold /></el-icon>
<el-icon v-else><ArrowUpBold /></el-icon>
</template> </template>
<template #title> <template #title>
<div class="custom-collapse-title"> <div class="custom-collapse-title">
<div class="custom-collapse-index">{{ index + 1 }}</div> <div class="custom-collapse-index">{{ index + 1 }}</div>
<div class="custom-collapse-name one-line-ellipsis">{{ item.title }}</div> <div class="custom-collapse-name one-line-ellipsis">
<span class="text-click-hover" @click.stop="handleClickDecree(item)">{{ item.title }}</span>
</div>
</div> </div>
</template> </template>
<div class="custom-collapse-content"> <div class="custom-collapse-content">
...@@ -192,6 +195,20 @@ const handleGetLaws = async () => { ...@@ -192,6 +195,20 @@ const handleGetLaws = async () => {
console.error("获取法律依据数据失败", error); console.error("获取法律依据数据失败", error);
} }
}; };
// 跳转科技法案详情页
const handleClickDecree = decree => {
window.sessionStorage.setItem("billId", decree.billId);
window.sessionStorage.setItem("curTabName", decree.title);
const route = router.resolve({
path: "/billLayout",
query: {
billId: decree.billId
}
});
console.log(route);
window.open(route.href, "_blank");
};
onMounted(() => { onMounted(() => {
handleGetBackground(); handleGetBackground();
...@@ -261,29 +278,31 @@ onMounted(() => { ...@@ -261,29 +278,31 @@ onMounted(() => {
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px 0; padding: 18px 0;
.id { .id {
margin-right: 16px;
margin-left: 15px; margin-left: 15px;
width: 24px; width: 24px;
height: 24px; height: 24px;
text-align: center; text-align: center;
line-height: 30px;
border-radius: 12px;
background: #e7f3ff; background: #e7f3ff;
color: #0a57a6; color: #0a57a6;
font-size: 15px;
line-height: 24px;
border-radius: 50%;
} }
.title { .title {
width: 914px; width: 20px;
line-height: 24px; flex: auto;
margin-left: 13px; line-height: 30px;
} }
.open { .open {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-left: 16px; margin: 0 16px;
img { img {
width: 100%; width: 100%;
...@@ -332,7 +351,8 @@ onMounted(() => { ...@@ -332,7 +351,8 @@ onMounted(() => {
.custom-collapse-title { .custom-collapse-title {
position: relative; position: relative;
.custom-collapse-index { .custom-collapse-index {
font-size: 15px; font-family: Microsoft YaHei;
font-size: var(--font-size-base);
position: absolute; position: absolute;
top: 12px; top: 12px;
left: -32px; left: -32px;
......
...@@ -37,9 +37,6 @@ ...@@ -37,9 +37,6 @@
<div class="item"> <div class="item">
<div class="item-left">{{ "相关领域:" }}</div> <div class="item-left">{{ "相关领域:" }}</div>
<div class="item-right tag-box"> <div class="item-right tag-box">
<!-- <div class="tag" v-for="(area, index) in basicInfo.areaList" :key="index">
{{ area.industryName }}
</div> -->
<AreaTag v-for="(area, index) in basicInfo.areaList" :key="index" :tagName="area.industryName"></AreaTag> <AreaTag v-for="(area, index) in basicInfo.areaList" :key="index" :tagName="area.industryName"></AreaTag>
</div> </div>
</div> </div>
...@@ -120,7 +117,7 @@ ...@@ -120,7 +117,7 @@
<img :src="item.avatar ? item.avatar : DefaultIcon1" alt="" /> <img :src="item.avatar ? item.avatar : DefaultIcon1" alt="" />
</div> </div>
<div class="box3-top-bottom-item-right"> <div class="box3-top-bottom-item-right">
<div class="name" @click="handleClickUser(item)">{{ item.name }}</div> <div class="name text-click-hover one-line-ellipsis" @click="handleClickUser(item)">{{ item.name }}</div>
<div class="position">{{ item.job }}</div> <div class="position">{{ item.job }}</div>
</div> </div>
</div> </div>
...@@ -336,22 +333,20 @@ onMounted(() => { ...@@ -336,22 +333,20 @@ onMounted(() => {
.box1-main { .box1-main {
display: flex; display: flex;
padding: 0 24px;
.box1-main-left { .box1-main-left {
width: 395px; width: 395px;
height: 332px; height: 332px;
margin-left: 24px;
img { img {
width: 100%; width: 100%;
// height: 100%;
} }
} }
.box1-main-left-img-mock { .box1-main-left-img-mock {
width: 240px; width: 240px;
height: 332px; height: 332px;
margin-left: 24px;
background-color: #0b1932; background-color: #0b1932;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -378,7 +373,8 @@ onMounted(() => { ...@@ -378,7 +373,8 @@ onMounted(() => {
} }
.box1-main-right { .box1-main-right {
width: 590px; width: 20px;
flex: auto;
margin-left: 20px; margin-left: 20px;
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
...@@ -387,24 +383,22 @@ onMounted(() => { ...@@ -387,24 +383,22 @@ onMounted(() => {
line-height: 24px; line-height: 24px;
.item { .item {
height: 30px;
display: flex; display: flex;
margin-bottom: 17px; margin-bottom: 22px;
.item-left { .item-left {
width: 100px; width: 100px;
} }
.item-right { .item-right {
width: 470px; width: 20px;
overflow: hidden; flex: auto;
text-overflow: ellipsis;
white-space: nowrap;
} }
.tag-box { .tag-box {
display: flex; display: flex;
gap: 8px; gap: 8px;
flex-wrap: wrap;
.tag { .tag {
height: 24px; height: 24px;
...@@ -423,6 +417,9 @@ onMounted(() => { ...@@ -423,6 +417,9 @@ onMounted(() => {
} }
.text { .text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: normal !important; font-weight: normal !important;
} }
} }
...@@ -697,10 +694,6 @@ onMounted(() => { ...@@ -697,10 +694,6 @@ onMounted(() => {
line-height: 24px; line-height: 24px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
} }
.position { .position {
......
<template> <template>
<div class="introduction-wrap"> <div class="introduction-wrap">
<div class="left"> <div class="page-left">
<div class="box1"> <div class="box1">
<AnalysisBox title="主要指令" :showAllBtn="false"> <AnalysisBox title="主要指令" :showAllBtn="false">
<div class="analysis-box"> <div class="analysis-box">
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<el-empty v-if="!contentList?.length" style="padding: 60px 0;" description="暂无数据" :image-size="100" /> <el-empty v-if="!contentList?.length" style="padding: 60px 0;" description="暂无数据" :image-size="100" />
<div v-for="(section, index) in contentList" :key="index" class="section"> <div v-for="(section, index) in contentList" :key="index" class="section">
<div class="section-header"> <div class="section-header">
<div class="section-title">({{ simpleNumToChinese(index+1) }}) {{ section.content }}</div> <div class="section-title text-align-justify" v-html="section.content"></div>
<div class="section-icon"> <div class="section-icon">
<img src="./assets/images/open-icon.png" alt="" /> <img src="./assets/images/open-icon.png" alt="" />
</div> </div>
...@@ -30,19 +30,19 @@ ...@@ -30,19 +30,19 @@
<div class="numbered-list"> <div class="numbered-list">
<div v-for="(item, itemIndex) in section.slaver" :key="itemIndex" class="list-item"> <div v-for="(item, itemIndex) in section.slaver" :key="itemIndex" class="list-item">
<div class="list-item-dot">{{itemIndex+1}}.</div> <div class="list-item-dot">{{itemIndex+1}}.</div>
<div class="list-item-word">{{ item.content }}</div> <div class="list-item-word" v-html="item.content"></div>
<!-- 渲染二级列表 --> <!-- 渲染二级列表 -->
<div v-if="item.slaver" class="sub-list"> <div v-if="item.slaver" class="sub-list">
<div v-for="(subItem, subIndex) in item.slaver" :key="subIndex" class="sub-item"> <div v-for="(subItem, subIndex) in item.slaver" :key="subIndex" class="sub-item">
<div class="sub-item-dot">({{subIndex+1}})</div> <div class="sub-item-dot">({{subIndex+1}})</div>
<div class="sub-item-word">{{ subItem.content }}</div> <div class="sub-item-word" v-html="subItem.content"></div>
<!-- 渲染三级列表 --> <!-- 渲染三级列表 -->
<div v-if="subItem.slaver" class="sub-sub-list"> <div v-if="subItem.slaver" class="sub-sub-list">
<div v-for="(subSubItem, subSubIndex) in subItem.slaver" :key="subSubIndex" class="sub-sub-item"> <div v-for="(subSubItem, subSubIndex) in subItem.slaver" :key="subSubIndex" class="sub-sub-item">
<div class="sub-sub-item-dot">{{ALPHABET[subSubIndex%26]}}.</div> <div class="sub-sub-item-dot">{{ALPHABET[subSubIndex%26]}}.</div>
<div class="sub-sub-item-word">{{ subSubItem.content }}</div> <div class="sub-sub-item-word" v-html="subItem.content"></div>
</div> </div>
</div> </div>
</div> </div>
...@@ -55,17 +55,22 @@ ...@@ -55,17 +55,22 @@
</AnalysisBox> </AnalysisBox>
</div> </div>
</div> </div>
<div class="right"> <div class="page-right">
<div class="box3"> <div class="box3">
<AnalysisBox title="发布机构" :showAllBtn="false"> <AnalysisBox title="执行机构" :showAllBtn="false">
<div class="box3-top"> <div class="box3-top">
<div class="box3-top-top" @click="handleToInstitution(box3TopTopData)"> <div class="organization-list">
<div class="organization-item" v-for="item in organizationInfo.list" :key="item.id">
<ActionButton @click="handleOrganization(item)" :name="item.obb" :type="item.id==organizationInfo.node.id?'active':'normal'" />
</div>
</div>
<div class="box3-top-top" @click="handleToInstitution()">
<div class="left"> <div class="left">
<img :src="box3TopTopData.logo ? box3TopTopData.logo : DefaultIcon2" alt="" /> <img :src="organizationInfo.node.logo || DefaultIcon2" alt="" />
</div> </div>
<div class="right"> <div class="right">
<div class="name">{{ box3TopTopData.name + " >" }}</div> <div class="name">{{ organizationInfo.node.name + " >" }}</div>
<div class="ename">{{ box3TopTopData.eName }}</div> <div class="ename">{{ organizationInfo.node.ename }}</div>
</div> </div>
</div> </div>
<div class="box3-top-bottom"> <div class="box3-top-bottom">
...@@ -76,17 +81,21 @@ ...@@ -76,17 +81,21 @@
<div class="text">{{ "关键人物" }}</div> <div class="text">{{ "关键人物" }}</div>
</div> </div>
<div class="box3-top-bottom-main"> <div class="box3-top-bottom-main">
<div class="box3-top-bottom-item" v-for="(item, index) in box3TopBottomData" :key="index"> <div class="box3-top-bottom-item" v-for="(item, index) in organizationInfo.node.leaders" :key="index">
<div class="box3-top-bottom-item-left"> <div class="box3-top-bottom-item-left">
<img :src="item.avatar ? item.avatar : DefaultIcon1" alt="" /> <img :src="item.avatar || DefaultIcon1" alt="" />
</div> </div>
<div class="box3-top-bottom-item-right"> <div class="box3-top-bottom-item-right">
<div class="name" @click="handleClickUser(item)">{{ item.name }}</div> <div class="name one-line-ellipsis text-click-hover" @click="handleClickUser(item)">{{ item.name }}</div>
<div class="position">{{ item.job }}</div> <div class="position">{{ item.job }}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="organization-button">
<div class="button-text">查看政令执行情况</div>
<el-icon size="16"><Right /></el-icon>
</div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -109,14 +118,14 @@ ...@@ -109,14 +118,14 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, onMounted, reactive } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import router from "@/router"; import router from "@/router";
import { Search } from '@element-plus/icons-vue' import { Search } from '@element-plus/icons-vue'
import { getDecreeIssueOrganization } from "@/api/decree/introduction"; import { getDecreeOrganization } from "@/api/decree/introduction";
import { getDecreeRelatedEntity, getDecreeMainContent } from "@/api/decree/background"; import { getDecreeRelatedEntity, getDecreeMainContent } from "@/api/decree/background";
import { getDecreehylyList } from "@/api/decree/home"; import { getDecreehylyList } from "@/api/decree/home";
import ActionButton from '@/components/base/ActionButton/index.vue'
import DefaultIcon1 from "@/assets/icons/default-icon1.png"; import DefaultIcon1 from "@/assets/icons/default-icon1.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png"; import DefaultIcon2 from "@/assets/icons/default-icon2.png";
import defaultCom from "@/views/coopRestriction/assets/images/default-icon2.png" import defaultCom from "@/views/coopRestriction/assets/images/default-icon2.png"
...@@ -211,14 +220,16 @@ const contentList = ref([ ...@@ -211,14 +220,16 @@ const contentList = ref([
const ALPHABET = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]; const ALPHABET = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
const onMainContentData = async () => { const onMainContentData = async () => {
try { try {
const res = await getDecreeMainContent({ const keyword = commandWord.value;
id: route.query.id, const res = await getDecreeMainContent({ id: route.query.id, keyword, domainId: areaType.value });
keyword: commandWord.value,
domainId: areaType.value
});
console.log("主要指令", res); console.log("主要指令", res);
if (res && res.code === 200) { if (res && res.code === 200) {
contentList.value = res.data; contentList.value = res.data || [];
contentList.value.forEach((item, index) => { item.content = `(${simpleNumToChinese(index+1)}) ${item.content}` })
if (keyword) {
let word = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
contentList.value.forEach(item => {onHighlight(word, item)})
}
} else { } else {
contentList.value = [] contentList.value = []
} }
...@@ -227,12 +238,19 @@ const onMainContentData = async () => { ...@@ -227,12 +238,19 @@ const onMainContentData = async () => {
console.error("获取主要指令数据失败:", error); console.error("获取主要指令数据失败:", error);
} }
}; };
// 搜索高亮效果
const onHighlight = (word, row) => {
row.content = String(row.content).replace(new RegExp(word, "gi"), (match) => {
return `<span class="highlight">${match}</span>`;
});
if (row.slaver?.length) {
row.slaver.forEach(item => { onHighlight(word, item) })
}
}
// 数字转中文(支持 0-99 整数) // 数字转中文(支持 0-99 整数)
const simpleNumToChinese = (num) => { const simpleNumToChinese = (num) => {
// 1. 基础校验:只处理 0-99 的整数 // 1. 基础校验:只处理 0-99 的整数
if (!Number.isInteger(num) || num < 0 || num > 99) { if (!Number.isInteger(num) || num < 0 || num > 99) return '100';
return '仅支持 0-99 之间的整数';
}
// 2. 定义基础字符 // 2. 定义基础字符
const singleChars = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']; const singleChars = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
const tenChar = '十'; const tenChar = '十';
...@@ -271,20 +289,34 @@ const onRelatedEntityData = async () => { ...@@ -271,20 +289,34 @@ const onRelatedEntityData = async () => {
} }
}; };
// 发布机构 // 执行机构
const box3TopTopData = ref({ const organizationInfo = reactive({
id: "", list : [],
logo: "", node: {id: "", obb: "", logo: "", name: "", ename: "", leaders: []},
name: "", })
eName: "" const handleGetOrgnization = async () => {
}); try {
const box3TopBottomData = ref([]); const res = await getDecreeOrganization({id: route.query.id});
// 跳转行政机构主页 console.log("执行机构", res);
const handleToInstitution = item => { if (res.code === 200 && res.data?.length) {
organizationInfo.list = res.data;
organizationInfo.node = res.data[0];
}
} catch (error) {
organizationInfo.node = {id: "", obb: "", logo: "", name: "", ename: "", leaders: []};
console.error("获取执行机构数据失败", error);
}
};
// 切换执行机构
const handleOrganization = (node) => {
organizationInfo.node = node;
};
// 跳转机构主页
const handleToInstitution = () => {
const curRoute = router.resolve({ const curRoute = router.resolve({
path: "/institution", path: "/institution",
query: { query: {
id: item.id id: organizationInfo.node.id
} }
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
...@@ -300,21 +332,6 @@ const handleClickUser = item => { ...@@ -300,21 +332,6 @@ const handleClickUser = item => {
}); });
window.open(routeData.href, "_blank"); window.open(routeData.href, "_blank");
}; };
const handleGetOrgnization = async () => {
try {
const res = await getDecreeIssueOrganization({id: route.query.id});
console.log("发布机构", res);
if (res.code === 200 && res.data) {
let { id, image, name, ename } = res.data
Object.assign(box3TopTopData.value, { id, logo: image, name, eName: ename });
box3TopBottomData.value = res.data.personList;
}
} catch (error) {
box3TopTopData.value = { id: "", logo: "", name: "", eName: "" };
box3TopBottomData.value = [];
console.error("执行机构error", error);
}
};
onMounted(() => { onMounted(() => {
handleGetAreaList(); handleGetAreaList();
...@@ -325,13 +342,19 @@ onMounted(() => { ...@@ -325,13 +342,19 @@ onMounted(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.analysis-content {
:deep(span.highlight) {
background-color: #ffff00;
}
}
.introduction-wrap { .introduction-wrap {
display: flex; display: flex;
width: 1600px; width: 1600px;
padding: 16px 0; padding: 16px 0;
gap: 16px; gap: 16px;
.left { .page-left {
width: 20px; width: 20px;
flex: auto; flex: auto;
...@@ -387,7 +410,6 @@ onMounted(() => { ...@@ -387,7 +410,6 @@ onMounted(() => {
.section-title { .section-title {
font-size: 18px; font-size: 18px;
line-height: 30px; line-height: 30px;
font-weight: 600;
letter-spacing: 1px; letter-spacing: 1px;
width: 20px; width: 20px;
flex: auto; flex: auto;
...@@ -457,7 +479,7 @@ onMounted(() => { ...@@ -457,7 +479,7 @@ onMounted(() => {
} }
} }
.right { .page-right {
width: 520px; width: 520px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -466,10 +488,34 @@ onMounted(() => { ...@@ -466,10 +488,34 @@ onMounted(() => {
.box3 { .box3 {
.box3-top { .box3-top {
margin-top: 2px; margin-top: 2px;
padding: 0 22px 20px;
border-bottom: 1px solid rgba(234, 236, 238, 1); border-bottom: 1px solid rgba(234, 236, 238, 1);
.organization-list {
display: flex;
flex-wrap: wrap;
margin-bottom: 16px;
gap: 8px 16px;
}
.organization-button {
height: 36px;
background-color: var(--color-primary-100);
color: var(--bg-white-100);
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
.button-text {
margin-right: 8px;
font-family: Microsoft YaHei;
font-size: 16px;
line-height: 16px;
}
}
.box3-top-top { .box3-top-top {
width: 473px;
height: 88px; height: 88px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1); border: 1px solid rgba(234, 236, 238, 1);
...@@ -477,42 +523,13 @@ onMounted(() => { ...@@ -477,42 +523,13 @@ onMounted(() => {
background: rgba(247, 248, 249, 1); background: rgba(247, 248, 249, 1);
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0 auto;
position: relative;
cursor: pointer; cursor: pointer;
.more {
position: absolute;
right: 17px;
top: 17px;
display: flex;
gap: 3px;
.text {
height: 16px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 16px;
}
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
}
.left { .left {
width: 64px; width: 64px;
height: 64px; height: 64px;
margin-left: 17px; margin-left: 17px;
font-size: 0px;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -520,7 +537,8 @@ onMounted(() => { ...@@ -520,7 +537,8 @@ onMounted(() => {
} }
.right { .right {
width: 370px; width: 20px;
flex: auto;
margin-left: 15px; margin-left: 15px;
.name { .name {
...@@ -545,9 +563,7 @@ onMounted(() => { ...@@ -545,9 +563,7 @@ onMounted(() => {
} }
.box3-top-bottom { .box3-top-bottom {
width: 473px;
height: 193px; height: 193px;
margin: 0 auto;
.box3-top-bottom-header { .box3-top-bottom-header {
height: 40px; height: 40px;
...@@ -624,10 +640,6 @@ onMounted(() => { ...@@ -624,10 +640,6 @@ onMounted(() => {
line-height: 24px; line-height: 24px;
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
} }
.position { .position {
......
...@@ -135,7 +135,6 @@ onMounted(() => { ...@@ -135,7 +135,6 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
.header-main { .header-main {
padding: 17px 0; padding: 17px 0;
width: 100%; width: 100%;
......
...@@ -53,7 +53,7 @@ export default defineConfig({ ...@@ -53,7 +53,7 @@ export default defineConfig({
'/api': { '/api': {
target: 'http://8.140.26.4:9085/', target: 'http://8.140.26.4:9085/',
// target: 'http://192.168.0.5:28080/', // target: 'http://192.168.0.6:28080/',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '')
}, },
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论