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

feat: 优化法案概览与委员会统计展示

Made-with: Cursor
上级 8543633d
......@@ -25,7 +25,7 @@
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
<div class="item-dot" v-if="item.delta">+{{ item.delta }}</div>
<div class="item-dot" v-if="item.delta">{{ dotPrefix }}{{ item.delta }}</div>
</div>
<div v-if="shouldShowMoreCard" class="summary-item" @click="emit('more-click')">
......@@ -100,6 +100,10 @@ const props = defineProps({
loading: {
type: Boolean,
default: false
},
dotPrefix: {
type: String,
default: "+"
}
});
......
......@@ -35,7 +35,7 @@ const billRoutes = [
component: BillAllCommittee,
meta: {
title: "法案委员会列表",
isShowHeader: true
isShowHeader: false
}
},
{
......
......@@ -14,6 +14,12 @@
placeholder="搜索委员会"
/>
</div>
<div class="hard-select">
<el-select v-model="committeeInfo.metricType" @change="onAllCommittee()" placeholder="统计口径" style="width: 160px; margin-left: 8px">
<el-option label="政令数据总量" :value="1" />
<el-option label="政令新增数量" :value="2" />
</el-select>
</div>
</div>
<div class="date-box">
......@@ -30,7 +36,7 @@
<div class="item-name one-line-ellipsis">{{ item.name }}</div>
<div class="item-chamber one-line-ellipsis">{{ item.chamber }}</div>
</div>
<div class="item-total">{{ item.count }}</div>
<div class="item-total">{{ getDisplayCount(item) }}</div>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
......@@ -48,6 +54,12 @@
/>
</div>
</div>
<div class="back-bnt" @click="handleBack">
<el-icon>
<Back />
</el-icon>
<div class="back-text">返回</div>
</div>
</div>
</template>
......@@ -55,6 +67,7 @@
import { onMounted, reactive, ref } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ArrowRightBold } from "@element-plus/icons-vue";
import { Back } from "@element-plus/icons-vue";
import router from "@/router";
import TimeTabPane from "@/components/base/TimeTabPane/index.vue";
import { getStatisticsBillCountByCommittee } from "@/api/bill/billHome";
......@@ -66,6 +79,7 @@ const committeeInfo = reactive({
pageSize: 8,
total: 0,
keyWord: "",
metricType: 1,
dateDesc: "近一年",
list: []
});
......@@ -89,10 +103,11 @@ const onAllCommittee = async num => {
id: `${item.orgType || ""}-${item.orgName || ""}`,
name: item.orgName,
chamber: getChamberLabel(item.orgType),
count: Number(item.count || 0)
totalCount: Number(item.count || 0),
recentCount: Number(item.countRecent || item.totalRecent || item.recentCount || item.recent || item.newCount || 0)
}))
.filter(item => !committeeInfo.keyWord || item.name?.includes(committeeInfo.keyWord))
.sort((a, b) => (b.count || 0) - (a.count || 0));
.sort((a, b) => getSortValue(b) - getSortValue(a));
committeeInfo.total = source.length;
const start = (committeeInfo.pageNum - 1) * committeeInfo.pageSize;
......@@ -108,20 +123,36 @@ const onAllCommittee = async num => {
committeeInfo.loading = false;
};
const getSortValue = item => {
if (committeeInfo.metricType === 2) return Number(item?.recentCount || 0);
return Number(item?.totalCount || 0);
};
const getDisplayCount = item => {
return getSortValue(item);
};
const handleDateChange = event => {
committeeInfo.dateDesc = event?.time || "近一年";
onAllCommittee();
};
const handleToDataLibrary = item => {
const route = router.resolve({
router.push({
path: "/dataLibrary/countryBill",
query: {
selectedOrg: item.name,
selectedCongress: item.chamber
}
});
window.open(route.href, "_blank");
};
const handleBack = () => {
if (window.history.length > 1) {
router.back();
return;
}
router.push("/billHome");
};
const refCommittee = ref();
......@@ -143,6 +174,28 @@ onMounted(() => {
background-size: 100% 100%;
display: flex;
justify-content: center;
position: relative;
.back-bnt {
position: absolute;
top: 16px;
left: 30px;
width: 86px;
height: 38px;
background-color: white;
border-radius: 19px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary-65-color);
font-family: Source Han Sans CN;
font-size: 16px;
cursor: pointer;
}
.back-text {
margin-left: 6px;
}
.container-box {
width: 1600px;
......@@ -180,6 +233,11 @@ onMounted(() => {
width: 180px;
height: 32px;
}
.hard-select {
height: 42px;
padding: 5px 0;
}
}
.date-box {
......
......@@ -56,7 +56,31 @@
<AreaTag v-for="(item, index) in bill.hylyList" :key="index" :tagName="item.industryName">
</AreaTag>
</div>
<div class="box1-main-divider"></div>
<div class="box1-main-left-info1">
<div class="info1-box">
<div class="icon"></div>
<div class="info1-box-left">{{ "提出部门:" }}</div>
<div class="info1-box-right info1-box-right--committee">
<template v-if="getLimitedCommitteeList(bill.committeeList).length">
<div
v-for="committee in getLimitedCommitteeList(bill.committeeList)"
:key="committee.committeeId || committee.committeeName"
class="committee-item"
>
<img
class="committee-logo"
:src="committee.logoUrl || iconCommit"
:alt="committee.committeeName || '提案部门'"
/>
<div class="committee-name">
{{ committee.committeeName || "--" }}
</div>
</div>
</template>
<div v-else>--</div>
</div>
</div>
<div class="info1-box">
<div class="icon"></div>
<div class="info1-box-left">{{ "提案人:" }}</div>
......@@ -70,9 +94,11 @@
</div>
</div>
</div>
<div class="box1-main-divider box1-main-divider--before-list"></div>
<div class="box1-main-left-info2">
<div class="info2-item" v-for="(item, index) in bill.dyqkList" :key="index">
<div class="time-line" v-if="index !== bill.dyqkList.length - 1"></div>
<div class="info2-item" v-for="(item, index) in getLimitedDyqkList(bill.dyqkList)"
:key="index">
<div class="time-line" v-if="!isLastDyqkItem(bill.dyqkList, index)"></div>
<div class="item-icon">
<img src="./assets/images/info2-icon.png" alt="" />
</div>
......@@ -146,7 +172,7 @@
</div>
</div>
</OverviewCard>
<OverviewCard class="overview-card--single box6" title="领域分布情况" :icon="box6HeaderIcon">
<OverviewCard class="overview-card--single box6" title="领域分布情况" :icon="box7HeaderIcon">
<template #right>
<el-select v-model="box9selectetedTime" placeholder="选择时间" style="width: 90px">
<el-option v-for="item in box9YearList" :key="item.value" :label="item.label" :value="item.value" />
......@@ -185,7 +211,7 @@
<div v-else id="box7Chart" class="overview-chart"></div>
</div>
<div class="overview-tip-row">
<TipTab class="overview-tip" :text="'提出涉华科技法案委员会分布情况,数据来源:美国国会官网'" />
<TipTab class="overview-tip" :text="'涉华科技法案提案委员会分布情况,数据来源:美国国会官网'" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box7')" />
</div>
<div v-if="aiPaneVisible.box7" class="overview-ai-pane" @mouseleave="handleHideAiPane('box7')">
......@@ -193,7 +219,7 @@
</div>
</div>
</OverviewCard>
<OverviewCard class="overview-card--single box8" title="进展分布情况" :icon="box7HeaderIcon">
<OverviewCard class="overview-card--single box8" title="进展分布情况" :icon="box8HeaderIcon">
<template #right>
<el-select v-model="box8selectetedTime" placeholder="选择时间" style="width: 90px">
<el-option v-for="item in box8YearList" :key="item.value" :label="item.label" :value="item.value" />
......@@ -216,7 +242,7 @@
</div>
</div>
</OverviewCard>
<OverviewCard class="overview-card--single box9" title="关键条款词云" :icon="box7HeaderIcon">
<OverviewCard class="overview-card--single box9" title="关键条款词云" :icon="box6HeaderIcon">
<div class="overview-card-body box9-main">
<div class="overview-chart-wrap" v-loading="chartLoading.box9">
<el-empty v-if="!wordCloudHasData" description="暂无数据" :image-size="100" />
......@@ -289,6 +315,7 @@ import getDoublePieChart from "./utils/doublePieChart";
import box5HeaderIcon from "./assets/images/box5-header-icon.png";
import box6HeaderIcon from "./assets/images/box6-header-icon.png";
import box7HeaderIcon from "./assets/images/box7-header-icon.png";
import box8HeaderIcon from "./assets/images/box8-header-icon.png"
import iconCommit from "./assets/icons/icon-commit.png";
import iconILetter from "./assets/icons/icon-iLetter.png";
......@@ -385,7 +412,9 @@ const committeeCards = computed(() => {
orgId: item.orgId,
name: item.name,
subText: item.chamber,
count: item.count
count: item.total,
delta: item.count,
avatar: item.logoUrl
}));
});
......@@ -402,9 +431,11 @@ const handleGetCommitteeBillCount = async () => {
orgId: item.orgId,
name: item.orgName,
chamber: getChamberLabel(item.orgType),
count: Number(item.count || 0)
total: Number(item.total || 0),
count: Number(item.count || 0),
logoUrl: item.logoUrl || ""
}))
.sort((a, b) => (b.count || 0) - (a.count || 0));
.sort((a, b) => (b.total || 0) - (a.total || 0));
committeeTotalCount.value = mappedList.length;
committeeCardList.value = mappedList.slice(0, 3);
} else {
......@@ -430,6 +461,7 @@ const handleToCommitteeMore = () => {
const hotBillList = ref([]); // 热门法案列表
const carouselRef = ref(null);
const MAX_DYQK_DISPLAY_COUNT = 3;
const handleCarouselChange = index => {
if (hotBillList.value && hotBillList.value.length > 0) {
......@@ -437,6 +469,20 @@ const handleCarouselChange = index => {
}
};
const getLimitedDyqkList = dyqkList => {
if (!Array.isArray(dyqkList)) return [];
return dyqkList.slice(0, MAX_DYQK_DISPLAY_COUNT);
};
const getLimitedCommitteeList = committeeList => {
if (!Array.isArray(committeeList)) return [];
return committeeList.slice(0, 2);
};
const isLastDyqkItem = (dyqkList, index) => {
return index === getLimitedDyqkList(dyqkList).length - 1;
};
// 切换热门法案
const handleSwithCurBill = name => {
if (name === "left") {
......@@ -499,25 +545,17 @@ const handleToMoreNews = () => {
// 风险信号
const warningList = ref([]);
const box7selectetedTime = ref("2025");
const box7YearList = ref([
{
label: "2025",
value: "2025"
},
{
label: "2024",
value: "2024"
},
{
label: "2023",
value: "2023"
},
{
label: "2022",
value: "2022"
}
]);
const currentYear = new Date().getFullYear();
const recentFiveYearOptions = Array.from({ length: 5 }, (_, index) => {
const year = String(currentYear - index);
return {
label: year,
value: year
};
});
const box7selectetedTime = ref(String(currentYear));
const box7YearList = ref(recentFiveYearOptions);
const aiPaneVisible = ref({
box5: false,
......@@ -683,25 +721,8 @@ const handleHideAiPane = key => {
};
};
const box8selectetedTime = ref("2025");
const box8YearList = ref([
{
label: "2025",
value: "2025"
},
{
label: "2024",
value: "2024"
},
{
label: "2023",
value: "2023"
},
{
label: "2022",
value: "2022"
}
]);
const box8selectetedTime = ref(String(currentYear));
const box8YearList = ref(recentFiveYearOptions);
// 涉华法案数量使用的领域分类列表
const categoryList = ref([]);
......@@ -756,7 +777,7 @@ const handleGetNews = async () => {
newsList.value = res.data.map(item => {
return {
...item,
from: `${item.newsOrg} · ${item.newsDate ? item.newsDate.slice(5) : ""}`
from: `${item.newsDate ? item.newsDate : ""} · ${item.newsOrg || ""}`
};
});
} else {
......@@ -946,50 +967,70 @@ const box7HasData = ref(true);
const box7AiData = ref({ inner: [], outer: [] });
const handleBox7Data = async () => {
chartLoading.value = { ...chartLoading.value, box7: true };
const selectParam = {
moduleType: '国会法案',
key: 3,
selectedDate: box7selectetedTime.value ? JSON.stringify([box7selectetedTime.value + '-01-01', box7selectetedTime.value + '-12-31']) : '',
isInvolveCn: true
}
try {
const res = await getBillPostOrg({ year: box7selectetedTime.value });
console.log("法案提出部门", res);
let innerData = [];
let outerData = [];
if (Array.isArray(res?.data)) {
innerData = res.data
.map(item => ({
name: item?.orgName || item?.orgType || "",
value: Number(item?.count || 0)
}))
.filter(item => item.name && item.value > 0);
outerData = res.data.flatMap(item => {
const typeName = item?.orgName || item?.orgType || "";
const list = Array.isArray(item?.orgBillNumList) ? item.orgBillNumList : [];
return list
.map(child => ({
name: child?.orgName || "",
value: Number(child?.count || 0),
percent: typeof child?.percent === "number" ? child.percent : Number(child?.percent || 0),
type: child?.orgType || typeName
}))
.filter(child => child.name && child.value > 0);
});
} else {
const orgBillNumList = res?.data?.orgBillNumList || [];
const orgBillNumMap = res?.data?.orgBillNumMap || {};
if (res.code === 200 && Array.isArray(orgBillNumList) && orgBillNumList.length > 0) {
box7HasData.value = true;
// 必须等待DOM更新,因为v-if切换可能导致元素刚被创建
await nextTick();
const data1 = [];
const houseTotal = Number(orgBillNumMap?.House || 0);
const senateTotal = Number(orgBillNumMap?.Senate || 0);
if (houseTotal > 0) data1.push({ name: "众议院", value: houseTotal });
if (senateTotal > 0) data1.push({ name: "参议院", value: senateTotal });
const getOrgTypeLabel = orgType => (orgType === "Senate" ? "参议院" : "众议院");
const typeOrderMap = { 众议院: 0, 参议院: 1 };
const data2 = orgBillNumList
innerData = data1;
outerData = orgBillNumList
.map(item => ({
name: item.orgName,
value: Number(item.count || 0),
percent: typeof item.percent === "number" ? item.percent : Number(item.percent || 0),
type: getOrgTypeLabel(item.orgType)
type: item.orgType === "Senate" ? "参议院" : "众议院"
}))
// 关键:外环顺序必须按内环(众→参)分组,否则扇区角度会交错导致“不对应”
.sort((a, b) => {
const t1 = typeOrderMap[a.type] ?? 99;
const t2 = typeOrderMap[b.type] ?? 99;
.filter(item => item.name && item.value > 0);
}
if (res.code === 200 && innerData.length > 0 && outerData.length > 0) {
box7HasData.value = true;
// 必须等待DOM更新,因为v-if切换可能导致元素刚被创建
await nextTick();
const typeOrderMap = new Map(innerData.map((item, index) => [item.name, index]));
const data2 = outerData.sort((a, b) => {
const t1 = typeOrderMap.get(a.type) ?? 99;
const t2 = typeOrderMap.get(b.type) ?? 99;
if (t1 !== t2) return t1 - t2;
return (b.value ?? 0) - (a.value ?? 0);
});
const selectParam = {
moduleType: '国会法案',
key: 3,
selectedDate: box7selectetedTime.value ? JSON.stringify([box7selectetedTime.value + '-01-01', box7selectetedTime.value + '-12-31']) : '',
isInvolveCn: true
}
const box7Chart = getDoublePieChart(data1, data2);
const box7Chart = getDoublePieChart(innerData, data2);
setChart(box7Chart, "box7Chart", true, selectParam);
box7AiData.value = { inner: data1, outer: data2 };
box7AiData.value = { inner: innerData, outer: data2 };
} else {
// 接口异常(如500)时,清空图表数据以避免报错或显示错误信息
box7HasData.value = false;
......@@ -1056,7 +1097,7 @@ const handleBox6 = async () => {
// 涉华领域分布
const box9ChartData = ref([]);
const box9selectetedTime = ref("2025");
const box9selectetedTime = ref(String(currentYear));
// 立法状态下拉:提出法案、众议院通过、参议院通过、解决分歧、呈交总统、完成立法
// v-model 存储的是接口需要的 status 值(直接作为接口参数)
const box9LegislativeStatus = ref("提出法案");
......@@ -1068,28 +1109,7 @@ const box9LegislativeStatusList = ref([
{ label: "呈交总统", value: "呈交总统" },
{ label: "完成立法", value: "完成立法" }
]);
const box9YearList = ref([
{
label: "2026",
value: "2026"
},
{
label: "2025",
value: "2025"
},
{
label: "2024",
value: "2024"
},
{
label: "2023",
value: "2023"
},
{
label: "2022",
value: "2022"
}
]);
const box9YearList = ref(recentFiveYearOptions);
const box9HasData = ref(true);
let box9ChartInstance = null;
const BOX9_MAX_DOMAIN_COUNT = 7;
......@@ -1148,7 +1168,7 @@ const handleBox9Data = async () => {
};
}),
null,
{ showCount: false }
{ showCount: true, countUnit: "项" }
);
// 记录埋点时,将当前选中的立法状态映射为序号(0-4)
const selectedIndex = box9LegislativeStatusList.value.findIndex(
......@@ -2074,12 +2094,22 @@ onUnmounted(() => {
gap: 8px;
}
.box1-main-divider {
margin-top: 18px;
margin-bottom: 18px;
width: 468px;
height: 1px;
background: var(--bg-black-5);
}
.box1-main-left-info1 {
margin-top: 25px;
margin-top: 0;
margin-left: 4px;
.info1-box {
display: flex;
min-height: 30px;
align-items: flex-start;
.icon {
margin-top: 15px;
......@@ -2102,18 +2132,60 @@ onUnmounted(() => {
.info1-box-right {
margin-left: 40px;
height: 30px;
min-height: 30px;
color: var(--text-primary-65-color);
font-family: Microsoft YaHei;
font-size: var(--font-size-base);
font-weight: 400;
line-height: 30px;
}
.info1-box-right--committee {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 4px;
height: auto;
min-height: 30px;
line-height: 22px;
padding-top: 4px;
.committee-item {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.committee-logo {
width: 18px;
height: 18px;
min-width: 18px;
border-radius: 50%;
object-fit: cover;
background: var(--bg-black-5);
}
.committee-name {
color: var(--text-primary-65-color);
font-family: Microsoft YaHei;
font-size: var(--font-size-base);
font-weight: 400;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.box1-main-divider--before-list {
margin-top: 18px;
}
.box1-main-left-info2 {
margin-top: 21px;
margin-top: 0;
height: 200px;
width: 440px;
position: relative;
......@@ -2214,7 +2286,7 @@ onUnmounted(() => {
left: 0;
bottom: 0;
box-sizing: border-box;
padding: 9px 10px 12px 10px;
padding: 9px 20px 12px 20px;
.inner-box-header {
height: 30px;
......
......@@ -69,7 +69,7 @@ const getDoublePieChart = (data1, data2) => {
const name = truncateLabel(params?.name, 6)
const value = params?.value ?? 0
const percent = typeof params?.percent === 'number' ? params.percent : 0
return `{name|${name}}\n{time|${value} ${percent}%}`
return `{name|${name}}\n{time|${value} ${percent}%}`
},
minMargin: 5,
edgeDistance: 10,
......
......@@ -10,6 +10,7 @@ const truncateLabel = (value, maxLen = 6) => {
const getPieChart = (data, colorList, options = {}) => {
const showCount = options.showCount !== false
const countUnit = options.countUnit || '条'
const chartColors = Array.isArray(colorList) && colorList.length ? colorList : MUTICHARTCOLORS
let option = {
color: chartColors,
......@@ -38,7 +39,7 @@ const getPieChart = (data, colorList, options = {}) => {
const name = truncateLabel(params?.name, 6)
const value = params?.value ?? 0
const percent = typeof params?.percent === 'number' ? params.percent : 0
const labelText = showCount ? `${value} ${percent}%` : `${percent}%`
const labelText = showCount ? `${value}${countUnit} ${percent}%` : `${percent}%`
return `{name|${name}}\n{time|${labelText}}`
},
minMargin: 5,
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论