提交 63313ad6 authored 作者: 张伊明's avatar 张伊明

合并分支 'zym-dev' 到 'master'

Zym dev 查看合并请求 !183
import 'echarts-wordcloud';
import { MUTICHARTCOLORS } from '@/common/constant';
const getWordCloudChart = (data) => {
const option = {
const getWordCloudChart = data => {
const option = {
grid: {
left: 5,
top: 5,
......@@ -12,9 +12,9 @@ const getWordCloudChart = (data) => {
series: [
{
type: "wordCloud",
shape: 'circle',
width: '100%',
height: '100%',
shape: "circle",
width: "100%",
height: "100%",
// 其他形状你可以使用形状路径
// shape: 'circle', // 示例
// 或者自定义路径
......@@ -22,43 +22,29 @@ const getWordCloudChart = (data) => {
sizeRange: [16, 36], // 定义词云中文字大小的范围
rotationRange: [0, 0],
rotationStep: 0,
drawOutOfBound: false, // 是否超出画布
shrinkToFit: true, // 是否自动缩小以适应容器
drawOutOfBound: false, // 是否超出画布
shrinkToFit: true, // 是否自动缩小以适应容器
// 字体
textStyle: {
// normal: {
// color: function () {
// return 'rgb(' + [
// Math.round(Math.random() * 160),
// Math.round(Math.random() * 160),
// Math.round(Math.random() * 160)
// ].join(',') + ')';
// }
// },
color: function () {
// let colors = [
// "rgba(189, 33, 33, 1)",
// "rgba(232, 151, 21, 1)",
// "rgba(220, 190, 68, 1)",
// "rgba(96, 58, 186, 1)",
// "rgba(32, 121, 69, 1)",
// "rgba(22, 119, 255, 1)",
// ];
// let colors = MUTICHARTCOLORS
return MUTICHARTCOLORS[parseInt(Math.random() * MUTICHARTCOLORS.length)];
color: function (params) {
const colors = MUTICHARTCOLORS || [];
if (!colors.length) {
return "#69B1FF";
}
return colors[params.dataIndex % colors.length];
},
emphasis: {
shadowBlur: 5,
shadowColor: "#333",
},
shadowColor: "#333"
}
},
// 设置词云数据
data: data,
},
],
data
}
]
};
return option
}
return option;
};
export default getWordCloudChart
\ No newline at end of file
export default getWordCloudChart;
\ No newline at end of file
<template>
<div v-if="visible" ref="dialogRef" class="dialog-wrapper" :style="position">
<div class="dialog-box1" @mousedown="handleMouseDown">
<div class="icon">
<img v-if="detailItem.orgName === '参议院'" :src="logoSenate" alt="" />
<img v-else :src="logoHouse" alt="" />
</div>
<div class="title">
<div class="date">{{ detailItem.actionDate }}</div>
<div class="text">
{{ detailItem.actionTitle }}
</div>
</div>
<div class="close" @click="handleClose">
<img :src="closeIcon" alt="" />
</div>
</div>
<div class="dialog-box2" v-if="detailItem.agreeVote !== null || detailItem.disagreeVote !== null">
<div class="vote-bar">
<div class="agree-bar" :style="{ flex: detailItem.agreeVote || 1 }"></div>
<div class="disagree-bar" :style="{ flex: detailItem.disagreeVote || 1 }"></div>
</div>
<div class="vote-text">
<div class="agree-text">{{ (detailItem.agreeVote || 0) + "赞成" }}</div>
<div class="disagree-text">{{ (detailItem.disagreeVote || 0) + "反对" }}</div>
</div>
</div>
<template v-if="detailItem.fynrList && detailItem.fynrList.length">
<div class="dialog-box4">
<div class="box4-left">
<div class="icon">
<img :src="changeIcon" alt="" />
</div>
<div class="text">{{ "变更条款" }}</div>
</div>
</div>
<div class="dialog-box5">
<div class="box5-item" v-for="(sub, subIndex) in detailItem.fynrList" :key="subIndex + '-' + sub">
<div class="icon"></div>
<div class="text">{{ sub }}</div>
</div>
</div>
</template>
</div>
</template>
<script setup>
import { ref } from "vue";
defineProps({
visible: {
type: Boolean,
default: false,
},
detailItem: {
type: Object,
default: () => ({}),
},
position: {
type: Object,
default: () => ({ left: "0px", top: "0px" }),
},
});
const emit = defineEmits(["close"]);
const logoSenate = new URL("@/views/bill/deepDig/processOverview/assets/images/logo1.png", import.meta.url).href;
const logoHouse = new URL("@/views/bill/deepDig/processOverview/assets/images/logo2.png", import.meta.url).href;
const closeIcon = new URL("@/views/bill/deepDig/processOverview/assets/images/close.png", import.meta.url).href;
const changeIcon = new URL("@/views/bill/deepDig/processOverview/assets/images/dialog-box4-icon.png", import.meta.url).href;
const dialogRef = ref(null);
const handleClose = () => {
emit("close");
};
const handleMouseDown = (e) => {
const dialog = dialogRef.value;
if (!dialog) return;
const startX = e.clientX;
const startY = e.clientY;
const initialLeft = dialog.offsetLeft;
const initialTop = dialog.offsetTop;
const move = (moveEvent) => {
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
dialog.style.right = "auto";
dialog.style.left = `${initialLeft + deltaX}px`;
dialog.style.top = `${initialTop + deltaY}px`;
};
const stop = () => {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", stop);
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", stop);
};
</script>
<style lang="scss" scoped>
.dialog-wrapper {
position: absolute;
width: 480px;
padding-bottom: 20px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
background: rgba(255, 255, 255, 1);
z-index: 10000;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
.dialog-box1 {
display: flex;
min-height: 90px;
height: auto;
padding-bottom: 15px;
position: relative;
border-bottom: 1px solid rgba(240, 242, 244, 1);
cursor: move;
.icon {
width: 48px;
height: 48px;
margin-left: 18px;
margin-top: 20px;
img {
width: 100%;
height: 100%;
}
}
.title {
margin-top: 20px;
margin-left: 16px;
width: 350px;
.date {
height: 22px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 22px;
}
.text {
margin-top: 4px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 22px;
span {
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
}
}
}
.close {
position: absolute;
top: 14px;
right: 15px;
width: 16px;
height: 16px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
.dialog-box2 {
height: 59px;
padding: 0 23px;
.vote-bar {
display: flex;
height: 4px;
width: 100%;
margin-top: 20px;
border-radius: 2px;
overflow: hidden;
.agree-bar {
background: #52c41a;
margin-right: 2px;
}
.disagree-bar {
background: #f5222d;
}
}
.vote-text {
display: flex;
justify-content: space-between;
margin-top: 8px;
.agree-text {
color: #52c41a;
font-size: 14px;
}
.disagree-text {
color: #f5222d;
font-size: 14px;
}
}
}
.dialog-box4 {
width: 438px;
margin-left: 23px;
margin-top: 20px;
display: flex;
justify-content: space-between;
height: 30px;
.box4-left {
display: flex;
.icon {
margin-top: 6px;
width: 18px;
height: 18px;
img {
width: 100%;
height: 100%;
}
}
.text {
width: 64px;
height: 30px;
margin-left: 6px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 30px;
}
}
}
.dialog-box5 {
margin-top: 3px;
margin-left: 23px;
width: 438px;
.box5-item {
min-height: 30px;
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 1.5;
display: flex;
margin-bottom: 8px;
.icon {
flex-shrink: 0;
margin-left: 15px;
width: 6px;
height: 6px;
margin-top: 9px;
border-radius: 3px;
background: #84888e;
}
.text {
margin-left: 10px;
word-break: break-all;
}
}
}
}
</style>
......@@ -647,7 +647,7 @@ onMounted(() => {
.icon1 {
position: absolute;
left: 5px;
left: 8px;
bottom: -8px;
width: 16px;
height: 16px;
......@@ -663,7 +663,7 @@ onMounted(() => {
.icon2 {
position: absolute;
right: 5px;
right: 8px;
bottom: -8px;
width: 16px;
height: 16px;
......
......@@ -180,7 +180,8 @@
<OverviewCard class="overview-card--single box9" title="涉华法案关键条款" :icon="box7HeaderIcon">
<div class="overview-card-body box9-main">
<div class="overview-chart-wrap">
<div id="wordCloudChart" class="overview-chart"></div>
<el-empty v-if="!wordCloudHasData" description="暂无数据" :image-size="100" />
<WordCloundChart v-else class="overview-chart" width="100%" height="100%" :data="wordCloudData" />
</div>
<TipTab class="overview-tip" />
</div>
......@@ -197,8 +198,8 @@
</template>
<script setup>
import RiskSignal from "@/components/base/RiskSignal/index.vue";
import { onMounted, ref, onUnmounted, nextTick, watch } from "vue";
import RiskSignal from "@/components/base/riskSignal/index.vue";
import { onMounted, ref, onUnmounted, nextTick, watch, computed } from "vue";
import router from "@/router/index";
import setChart from "@/utils/setChart";
import {
......@@ -220,9 +221,9 @@ import OverviewCard from "./OverviewCard.vue";
import ResourceLibrarySection from "./ResourceLibrarySection.vue";
import { useContainerScroll } from "@/hooks/useScrollShow";
import TipTab from "@/components/base/TipTab/index.vue";
import WordCloundChart from "@/components/base/WordCloundChart/index.vue";
import getMultiLineChart from "./utils/multiLineChart";
import getWordCloudChart from "./utils/worldCloudChart";
import getPieChart from "./utils/piechart";
import getDoublePieChart from "./utils/doublePieChart";
......@@ -637,6 +638,7 @@ const handleToSocialDetail = item => {
};
// 关键条款
const wordCloudData = ref([]);
const wordCloudHasData = computed(() => Array.isArray(wordCloudData.value) && wordCloudData.value.length > 0);
const handleGetKeyTK = async () => {
try {
const res = await getBillOverviewKeyTK();
......@@ -655,8 +657,6 @@ const handleGetKeyTK = async () => {
};
const handleBox6 = async () => {
await handleGetKeyTK();
const wordCloudChart = getWordCloudChart(wordCloudData.value);
setChart(wordCloudChart, "wordCloudChart");
};
// 涉华领域分布
......
......@@ -111,48 +111,6 @@
</AnalysisBox>
</div>
<div class="box3">
<!-- <div class="box-header">
<div class="icon"></div>
<div class="title">{{ "政治献金领域分布" }}</div>
<div class="header-right">
<div class="right-icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="right-icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="right-icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box3-main">
<div class="box3-main-left" id="chart2"></div>
<div class="box3-main-right">
<el-empty v-if="!areaList.length" description="暂无数据" :image-size="100" />
<div class="box3-main-right-item" v-for="(item, index) in areaList" :key="index">
<div class="id">{{ index + 1 }}</div>
<div class="name">{{ item.name }}</div>
<div class="line">
<div class="inner-line" :style="{ width: (item.num / areaList[0].num) * 100 + '%' }"></div>
</div>
<div class="num">{{ item.numtext }}</div>
<div class="more">{{ `${item.insNum}家机构 >` }}</div>
</div>
</div>
</div>
<div class="box-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{ currentPersonName }}的政治资金主要依赖于一个由亿万富翁、特定行业利益集团及通过​“超级政治行动委员会”​​
运作的大额捐款网络。
</div>
<div class="box-footer-right">
<img src="@/assets/icons/box-footer-right-icon.png" alt="" />
</div>
</div> -->
<AnalysisBox title="政治献金领域分布">
<div class="box3-main" :class="{ 'box3-main-no-footer': !showHardcodedTips }">
<div class="box3-main-left" id="chart2"></div>
......@@ -196,6 +154,7 @@ import setChart from "@/utils/setChart";
import getPieChart from "./utils/piechart";
import getSankeyChart from "./utils/sankey";
import { MUTICHARTCOLORS } from "@/common/constant";
import Img1 from "./assets/images/1.png";
import Img2 from "./assets/images/2.png";
......@@ -434,23 +393,9 @@ const topAreaList = computed(() => {
return areaList.value.slice(0, 5);
});
const chart2ColorList = ref(["#4096FF", "#FFA39E", "#ADC6FF", "#FFC069", "#B5F5EC", "#B37FEB", "#D6E4FF"]);
const sankeyColors = [
"#5470c6",
"#91cc75",
"#fac858",
"#ee6666",
"#73c0de",
"#3ba272",
"#fc8452",
"#9a60b4",
"#ea7ccc",
"#a2c0f1",
"#f596aa",
"#e6b422",
"#4b2c20"
];
const chart2ColorList = ref([...MUTICHARTCOLORS]);
const sankeyColors = [...MUTICHARTCOLORS];
const partyContributionList = ref([
{
......
......@@ -100,11 +100,11 @@
<div class="term-main">
<div class="term-row term-row-cn">
<div class="term-no-cn">第{{ term.tkxh }}条.</div>
<div class="term-content-cn">{{ term.fynr }}</div>
<div class="term-content-cn" v-html="getTermContentHtml(term, 'cn')"></div>
</div>
<div class="term-row term-row-en" v-if="termsShowOriginal">
<div class="term-no-en">Sec.{{ term.tkxh }}</div>
<div class="term-content-en">{{ term.ywnr }}</div>
<div class="term-content-en" v-html="getTermContentHtml(term, 'en')"></div>
</div>
</div>
</div>
......@@ -136,6 +136,8 @@ import * as echarts from "echarts";
import { Search } from "@element-plus/icons-vue";
import getPieChart from "./utils/piechart";
import { getBillContentId, getBillContentTk, getBillContentXzfs, getBillHyly } from "@/api/bill";
import { MUTICHARTCOLORS } from "@/common/constant";
import { extractTextEntity } from "@/api/intelligent/index";
const route = useRoute();
......@@ -161,6 +163,134 @@ const domainLoading = ref(false);
const termsHighlight = ref(true);
const termsShowOriginal = ref(true);
const entityRequestToken = ref(0);
const termEntityCache = ref(new Map());
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 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) => {
const rawText = String(text ?? "");
if (!rawText) return "";
const safeText = escapeHtml(rawText).replace(/\n/g, "<br />");
if (!enableHighlight) return safeText;
const ranges = getEntityRanges(rawText, entities);
if (!ranges.length) return safeText;
let html = "";
let cursor = 0;
for (const r of ranges) {
if (cursor < r.start) {
html += escapeHtml(rawText.slice(cursor, r.start));
}
const spanText = rawText.slice(r.start, r.end);
const type = escapeHtml(r.ent?.type ?? "");
html += `<span class="term-entity" data-entity-type="${type}">${escapeHtml(spanText)}</span>`;
cursor = r.end;
}
if (cursor < rawText.length) {
html += escapeHtml(rawText.slice(cursor));
}
return html.replace(/\n/g, "<br />");
};
const getTermEntityKey = (term, lang) => {
const baseKey = getTermKey(term, -1);
return `${baseKey}__${lang}`;
};
const ensureEntitiesForTerms = async terms => {
if (!termsHighlight.value) return;
const list = Array.isArray(terms) ? terms : [];
if (!list.length) return;
const currentToken = ++entityRequestToken.value;
const tasks = [];
for (const term of list) {
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) 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);
};
const tkRequestToken = ref(0);
const xzfsRequestToken = ref(0);
const hylyRequestToken = ref(0);
......@@ -182,9 +312,9 @@ const getTermSerial = index => {
};
const chart1Data = ref([]);
const chart1ColorList = ref(["#4096ff", "#b37feb", "#ff7875", "#85a5ff", "#69b1ff", "#ffc069", "#87e8de"]);
const chart1ColorList = ref([...MUTICHARTCOLORS]);
const chart2ColorList = ref(["#ff7875", "#85a5ff", "#95de64", "#ffc069", "#85e5db"]);
const chart2ColorList = ref([...MUTICHARTCOLORS]);
const chart2Data = ref([]);
......@@ -218,6 +348,14 @@ watch([selectedDomain, selectedLimit], () => {
handleGetBillContentTk(checkedValue.value ? "Y" : "N");
});
watch(
[termsHighlight, termsShowOriginal],
() => {
ensureEntitiesForTerms(displayTermsList.value);
},
{ immediate: true }
);
const handleSearchSubmit = () => {
searchKeyword.value = searchValue.value;
currentPage.value = 1;
......@@ -362,6 +500,7 @@ const handleGetBillContentTk = async cRelated => {
return item;
});
total.value = res.data.totalElements || 0;
ensureEntitiesForTerms(mainTermsList.value);
} else {
mainTermsList.value = [];
total.value = 0;
......@@ -805,6 +944,14 @@ onMounted(async () => {
font-weight: 700;
line-height: 24px;
color: var(--text-primary-80-color);
:deep(.term-entity) {
display: inline;
padding: 0 2px;
border-radius: 4px;
background: rgba(255, 213, 79, 0.35);
box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.25);
}
}
.term-content-en {
......@@ -813,6 +960,14 @@ onMounted(async () => {
font-weight: 400;
line-height: 24px;
color: var(--text-primary-65-color);
:deep(.term-entity) {
display: inline;
padding: 0 2px;
border-radius: 4px;
background: rgba(255, 213, 79, 0.28);
box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.2);
}
}
.open {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论