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

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

Zym dev 查看合并请求 !192
import request from "@/api/request.js";
import { getToken } from "@/api/request.js";
const CHART_INTERPRETATION_URL = "/aiAnalysis/chart_interpretation";
const API_KEY = "aircasKEY19491001";
/**
* 从模型返回的 markdown code fence 中提取数组 JSON
* 例如:```json\n[ ... ]\n```
* @param {string} buffer
* @returns {unknown[]}
*/
function parseChartInterpretationArray(buffer) {
// 可能带有前后空白、换行、以及 ```/```json 包裹
let cleaned = String(buffer || "").trim();
// 移除开头 fence
cleaned = cleaned.replace(/^```(?:json)?\s*/i, "");
// 移除结尾 fence(允许末尾有换行)
cleaned = cleaned.replace(/```[\s\r\n]*$/i, "").trim();
// 优先直接解析
try {
const parsed = JSON.parse(cleaned);
if (Array.isArray(parsed)) return parsed;
} catch (_) { }
// 兜底:从 first '[' 到 last ']' 截取
const start = cleaned.indexOf("[");
const end = cleaned.lastIndexOf("]");
if (start !== -1 && end !== -1 && end > start) {
const arrStr = cleaned.slice(start, end + 1);
const parsed = JSON.parse(arrStr);
if (Array.isArray(parsed)) return parsed;
}
throw new Error("无法解析图表解读 JSON 数组");
}
// 图表解读(流式)
/**
* @param {text}
* 图表解读(SSE 流式)
* @param {object} data - 请求体
* @param {object} [options] - 可选配置
* @param {function(string): void} [options.onChunk] - 每收到一条 SSE 消息时回调,参数为当前 chunk 的 text
* @returns {Promise<{data: unknown[]}>}
*/
export function getChartAnalysis(data) {
return request({
method: 'POST',
url: `/aiAnalysis/chart_interpretation`,
data,
})
export function getChartAnalysis(data, options = {}) {
const { onChunk } = options;
return new Promise((resolve, reject) => {
let buffer = "";
let settled = false;
const abortController = new AbortController();
const safeResolve = value => {
if (settled) return;
settled = true;
resolve(value);
};
const safeReject = err => {
if (settled) return;
settled = true;
reject(err);
};
(async () => {
try {
const { fetchEventSource } = await import("@microsoft/fetch-event-source");
await fetchEventSource(CHART_INTERPRETATION_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
"Cache-Control": "no-cache",
"X-API-Key": API_KEY,
// 后端同项目其它接口使用 token 字段名(axios 拦截器里就是这样注入的)
token: getToken()
},
body: JSON.stringify(data),
signal: abortController.signal,
openWhenHidden: true,
retryDelay: 1000,
maxRetries: 2,
onopen: response => {
const contentType = response.headers.get("content-type") || "";
if (!contentType.includes("text/event-stream")) {
throw new Error("SSE连接格式异常:content-type 不是 text/event-stream");
}
},
onmessage: event => {
const raw = (event?.data || "").trim();
if (!raw) return;
if (raw === "[DONE]") return;
let chunk = "";
// 后端返回格式示例:{"text":"```"} / {"text":"json\n[\n"}
try {
const msg = JSON.parse(raw);
if (msg && typeof msg === "object" && "text" in msg) {
chunk = String(msg.text ?? "");
buffer += chunk;
} else {
chunk = raw;
buffer += raw;
}
} catch (e) {
chunk = raw;
buffer += raw;
}
// 每收到一条消息即回调,用于流式渲染
if (chunk && typeof onChunk === "function") {
onChunk(chunk);
}
// 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
const trimmed = buffer.trim();
if (trimmed.endsWith("```")) {
try {
const arr = parseChartInterpretationArray(trimmed);
safeResolve({ data: arr });
abortController.abort();
} catch (_) { }
}
},
onclose: () => {
try {
const arr = parseChartInterpretationArray(buffer);
safeResolve({ data: arr });
} catch (e) {
safeReject(e);
}
},
onerror: error => {
if (error?.name === "AbortError") return true;
safeReject(error);
return true;
}
});
} catch (error) {
safeReject(error);
}
})();
});
}
\ No newline at end of file
......@@ -212,10 +212,25 @@ export function getBillTermsCompare(params) {
* @param {billId,content,currentPage,currentVersion,isCn,originalVersion,pageSize,status}
* @header token
*/
export function getBillVersionCompare(params) {
export function getBillVersionCompare(params, config = {}) {
return request({
method: "GET",
url: "/api/billInfoBean/versionCompare",
params,
signal: config.signal,
});
}
// 版本对比-根据筛选条件获取变更统计
/**
* @param {billId,content,currentVersion,isCn,originalVersion}
* @header token
*/
export function getBillVersionCompareStatistics(params, config = {}) {
return request({
method: "GET",
url: "/api/billInfoBean/versionCompareStatistics",
params,
signal: config.signal,
});
}
......@@ -2,17 +2,24 @@ import request from "@/api/request.js";
// 涉华法案领域分布
/**
* @param {year}
* @param {Object} params
* @param {string} params.year - 年份
* @param {string} [params.status] - 状态:提出法案/通过法案
*/
export function getBillIndustry(params) {
return request({
method: 'GET',
url: `/api/BillOverview/billIndustry/${params.year}`,
params,
params: { status: params.status }
})
}
// 涉华法案统计
/**
* @param {Object} params
* @param {string} [params.dateDesc] - 时间范围:近一年/近两年/近三年/全部
* @param {string} [params.industryId] - 行业领域 ID
*/
export function getBillCount(params) {
return request({
method: 'GET',
......
......@@ -10,12 +10,16 @@ export class TextEntity {
type: string;
}
// 智能化:提取文本实体
export function extractTextEntity(text: string): Promise<IntelligentResultWrapper<TextEntity[]>> {
export function extractTextEntity(
text: string,
config: { signal?: AbortSignal } = {}
): Promise<IntelligentResultWrapper<TextEntity[]>> {
return request({
url: `${INTELLECTUAL_API}/extract-entity`,
method: "POST",
data: {
text
}
},
signal: config.signal
});
}
......@@ -10,12 +10,39 @@
defaultBillSearchType="federal" />
</div>
<div class="committee-cards-section">
<div class="committee-cards-filter">
<span class="committee-cards-desc">近期美国国会各委员会涉华提案数量汇总</span>
<el-radio-group v-model="committeeTimeRange" class="committee-time-switch" size="default">
<el-radio-button v-for="item in committeeTimeOptions" :key="item.value" :label="item.value">
<span class="committee-time-switch-inner">
<el-icon v-if="committeeTimeRange === item.value" class="committee-time-switch-icon">
<Calendar />
</el-icon>
{{ item.label }}
</span>
</el-radio-button>
</el-radio-group>
</div>
<div class="committee-cards-row">
<div v-for="item in committeeCardList" :key="item.id" class="committee-card">
<div class="committee-card-icon"></div>
<div class="committee-card-content">
<div class="committee-card-name">{{ item.name }}</div>
<div class="committee-card-chamber">{{ item.chamber }}</div>
</div>
<div class="committee-card-count">{{ item.count }}&gt;</div>
</div>
</div>
</div>
<DivideHeader id="position1" class="divide1" :titleText="'最新动态'"></DivideHeader>
<div class="home-content-center">
<div class="center-top">
<overviewMainBox class="box1" title="热门法案" @toDetail="handleClickToDetail">
<template #headerIcon>
<img style="width: 100%; height: 100%" src="./assets/images/box1-header-icon.png" alt="" />
<img style="width: 100%; height: 100%" src="./assets/images/box1-header-icon.png"
alt="" />
</template>
<div class="box1-left" @click="handleSwithCurBill('left')">
<div class="icon">
......@@ -101,25 +128,27 @@
<DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader>
<div class="center-center">
<NewsList
:newsList="newsList"
img="newsImage"
title="newsTitle"
from="from"
content="newsContent"
/>
<NewsList :newsList="newsList" img="newsImage" title="newsTitle" from="from"
content="newsContent" />
<MessageBubble :messageList="messageList" imageUrl="personImage"
@more-click="handleToSocialDetail" @person-click="handleClickToCharacter" name="personName"
content="remarks" source="orgName" />
</div>
<DivideHeader id="position3" class="divide3" :titleText="'数据总览'"></DivideHeader>
<div class="center-footer">
<OverviewCard class="overview-card--double box5" title="涉华法案数量变化趋势" :icon="box5HeaderIcon">
<template #right>
<el-select v-model="box5Select" placeholder="选择领域" @change="handleBox5Change" style="width: 150px">
<el-select v-model="box5Select" placeholder="选择领域" @change="handleBox5Change"
style="width: 150px">
<el-option label="全部领域" value="全部领域" />
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
<el-option v-for="item in categoryList" :key="item.id" :label="item.name"
:value="item.id" />
</el-select>
<el-select v-model="box5ProposalTime" placeholder="提案时间" @change="handleBox5Change"
style="width: 120px; margin-left: 8px">
<el-option v-for="item in box5ProposalTimeList" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
</template>
<div class="overview-card-body box5-main">
......@@ -131,7 +160,8 @@
<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')">
<div v-if="aiPaneVisible.box5" class="overview-ai-pane"
@mouseleave="handleHideAiPane('box5')">
<AiPane :aiContent="overviewAiContent.box5" />
</div>
</div>
......@@ -139,7 +169,13 @@
<OverviewCard class="overview-card--single box6" title="涉华法案领域分布" :icon="box6HeaderIcon">
<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" />
<el-option v-for="item in box9YearList" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
<el-select v-model="box9LegislativeStatus" placeholder="立法状态" @change="handleBox9StatusChange"
style="width: 110px; margin-left: 8px">
<el-option v-for="item in box9LegislativeStatusList" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
</template>
<div class="overview-card-body box6-main">
......@@ -151,7 +187,8 @@
<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')">
<div v-if="aiPaneVisible.box6" class="overview-ai-pane"
@mouseleave="handleHideAiPane('box6')">
<AiPane :aiContent="overviewAiContent.box6" />
</div>
</div>
......@@ -161,7 +198,8 @@
<OverviewCard class="overview-card--single box7" title="涉华法案提出部门" :icon="box7HeaderIcon">
<template #right>
<el-select v-model="box7selectetedTime" placeholder="选择时间" style="width: 90px">
<el-option v-for="item in box7YearList" :key="item.value" :label="item.label" :value="item.value" />
<el-option v-for="item in box7YearList" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</template>
<div class="overview-card-body box7-main">
......@@ -173,7 +211,8 @@
<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')">
<div v-if="aiPaneVisible.box7" class="overview-ai-pane"
@mouseleave="handleHideAiPane('box7')">
<AiPane :aiContent="overviewAiContent.box7" />
</div>
</div>
......@@ -181,7 +220,8 @@
<OverviewCard class="overview-card--single box8" title="涉华法案进展分布" :icon="box7HeaderIcon">
<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" />
<el-option v-for="item in box8YearList" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</template>
<div class="overview-card-body box8-main">
......@@ -196,7 +236,8 @@
<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')">
<div v-if="aiPaneVisible.box8" class="overview-ai-pane"
@mouseleave="handleHideAiPane('box8')">
<AiPane :aiContent="overviewAiContent.box8" />
</div>
</div>
......@@ -205,13 +246,15 @@
<div class="overview-card-body box9-main">
<div class="overview-chart-wrap">
<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 class="overview-tip-row">
<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')" >
<div v-if="aiPaneVisible.box9" class="overview-ai-pane"
@mouseleave="handleHideAiPane('box9')">
<AiPane :aiContent="overviewAiContent.box9" />
</div>
</div>
......@@ -221,7 +264,8 @@
</div>
<div class="home-content-footer">
<DivideHeader id="position4" class="divide4" :titleText="'资源库'"></DivideHeader>
<ResourceLibrarySection :on-click-to-detail="handleClickToDetailO" :on-after-page-change="handlePageChange" />
<ResourceLibrarySection :on-click-to-detail="handleClickToDetailO"
:on-after-page-change="handlePageChange" />
</div>
</div>
</div>
......@@ -245,6 +289,7 @@ import {
getRemarks
} from "@/api/bill/billHome";
import { getPersonSummaryInfo } from "@/api/common/index";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import DivideHeader from "@/components/DivideHeader.vue";
import overviewMainBox from "@/components/base/boxBackground/overviewMainBox.vue";
import OverviewCard from "./OverviewCard.vue";
......@@ -265,6 +310,7 @@ import box7HeaderIcon from "./assets/images/box7-header-icon.png";
import { ElMessage } from "element-plus";
import { Calendar } from "@element-plus/icons-vue";
// 跳转人物主页
const handleClickToCharacter = async (id, name) => {
......@@ -324,6 +370,19 @@ const handleClickToCharacter = async (id, name) => {
const containerRef = ref(null);
const { isShow } = useContainerScroll(containerRef);
// 委员会卡片占位数据
const committeeTimeRange = ref("近一月");
const committeeTimeOptions = [
{ label: "近一周", value: "近一周" },
{ label: "近一月", value: "近一月" },
{ label: "近一年", value: "近一年" }
];
const committeeCardList = ref([
{ id: 1, name: "众议院外交委员会", chamber: "众议院", count: 42 },
{ id: 2, name: "参议院军事委员会", chamber: "参议院", count: 28 },
{ id: 3, name: "众议院情报委员会", chamber: "众议院", count: 19 }
]);
const hotBillList = ref([]); // 热门法案列表
const carouselRef = ref(null);
......@@ -418,11 +477,132 @@ const overviewAiContent = ref({
box9: "智能总结生成中..."
});
const aiPaneFetched = ref({
box5: false,
box6: false,
box7: false,
box8: false,
box9: false
});
const aiPaneLoading = ref({
box5: false,
box6: false,
box7: false,
box8: false,
box9: false
});
const buildAiChartPayload = key => {
if (key === "box5") {
const title = Array.isArray(box5Data.value.title) ? box5Data.value.title : [];
const proposed = box5Data.value?.data?.[0]?.value || [];
const passed = box5Data.value?.data?.[1]?.value || [];
const rate =
box5Data.value.percent ||
title.map((_, i) => {
const p = Number(proposed[i] || 0);
const pass = Number(passed[i] || 0);
return p ? Number(((pass / p) * 100).toFixed(2)) : 0;
});
return {
type: "折线图",
name: "涉华法案数量变化趋势",
data: title.map((month, i) => ({
month,
proposed: Number(proposed[i] || 0),
passed: Number(passed[i] || 0),
pass_rate: Number(rate[i] || 0)
}))
};
}
if (key === "box6") {
return {
type: "饼图",
name: "涉华法案领域分布",
data: (Array.isArray(box9ChartData.value) ? box9ChartData.value : []).map(item => ({
industry_name: item.industryName,
count_bill: Number(item.countBill || 0)
}))
};
}
if (key === "box7") {
return {
type: "环形图",
name: "涉华法案提出部门",
data: {
inner: box7AiData.value.inner,
outer: box7AiData.value.outer
}
};
}
if (key === "box8") {
return {
type: "柱状图",
name: "涉华法案进展分布",
data: box8StageList.value
};
}
if (key === "box9") {
return {
type: "词云图",
name: "涉华法案关键条款",
data: Array.isArray(wordCloudData.value) ? wordCloudData.value : []
};
}
return { type: "", name: "", data: [] };
};
const requestAiPaneContent = async key => {
if (!key || aiPaneLoading.value[key] || aiPaneFetched.value[key]) return;
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: true };
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成中..." };
try {
const payload = buildAiChartPayload(key);
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
overviewAiContent.value = {
...overviewAiContent.value,
[key]: base + chunk
};
}
}
);
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
const interpretation = first?.解读 || first?.["解读"];
// 流式已渲染过内容,最终用解析出的解读覆盖(保证显示格式统一)
if (interpretation) {
overviewAiContent.value = {
...overviewAiContent.value,
[key]: interpretation
};
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: false };
}
};
const handleShowAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: true
};
requestAiPaneContent(key);
};
const handleHideAiPane = key => {
......@@ -535,6 +715,13 @@ const handleGetRemarks = async () => {
// 涉华法案数量
const box5Select = ref("全部领域");
const box5ProposalTime = ref("近一年");
const box5ProposalTimeList = ref([
{ label: "近一年", value: "近一年" },
{ label: "近两年", value: "近两年" },
{ label: "近三年", value: "近三年" },
{ label: "全部", value: "全部" }
]);
const box5Data = ref({
title: [
"2024-09",
......@@ -564,7 +751,9 @@ const box5Data = ref({
const box5HasData = ref(true);
const handleGetBillCount = async () => {
try {
let params = {};
const params = {
dateDesc: box5ProposalTime.value
};
if (box5Select.value !== "全部领域") {
params.industryId = box5Select.value;
}
......@@ -626,7 +815,7 @@ const handleBox5 = async () => {
const pass = passed[i] || 0;
return p ? ((pass / p) * 100).toFixed(2) : 0;
});
let box5Chart = getMultiLineChart(box5Data.value.title, proposed, passed, rate);
const box5Chart = getMultiLineChart(box5Data.value.title, proposed, passed, rate);
setChart(box5Chart, "box5Chart");
}
};
......@@ -637,6 +826,7 @@ const handleBox5Change = () => {
// 法案提出部门
const box7HasData = ref(true);
const box7AiData = ref({ inner: [], outer: [] });
const handleBox7Data = async () => {
try {
const res = await getBillPostOrg({ year: box7selectetedTime.value });
......@@ -674,14 +864,17 @@ const handleBox7Data = async () => {
const box7Chart = getDoublePieChart(data1, data2);
setChart(box7Chart, "box7Chart");
box7AiData.value = { inner: data1, outer: data2 };
} else {
// 接口异常(如500)时,清空图表数据以避免报错或显示错误信息
box7HasData.value = false;
setChart({}, "box7Chart");
box7AiData.value = { inner: [], outer: [] };
}
} catch (error) {
console.error("获取法案提出部门数据失败", error);
box7HasData.value = false;
box7AiData.value = { inner: [], outer: [] };
}
};
......@@ -724,7 +917,16 @@ const handleBox6 = async () => {
// 涉华领域分布
const box9ChartData = ref([]);
const box9selectetedTime = ref("2025");
const box9LegislativeStatus = ref("提出法案");
const box9LegislativeStatusList = ref([
{ label: "提出法案", value: "提出法案" },
{ label: "通过法案", value: "通过法案" }
]);
const box9YearList = ref([
{
label: "2026",
value: "2026"
},
{
label: "2025",
value: "2025"
......@@ -746,7 +948,8 @@ const box9HasData = ref(true);
let box9ChartInstance = null;
const getBox9Data = async () => {
const params = {
year: box9selectetedTime.value
year: box9selectetedTime.value,
status: box9LegislativeStatus.value
};
try {
const res = await getBillIndustry(params);
......@@ -773,7 +976,9 @@ const handleBox9Data = async () => {
name: item.industryName,
value: item.countBill
};
})
}),
null,
{ showCount: false }
);
box9ChartInstance = setChart(box9Chart, "box9Chart");
}
......@@ -783,9 +988,14 @@ watch(box9selectetedTime, () => {
handleBox9Data();
});
const handleBox9StatusChange = () => {
handleBox9Data();
};
const box8HasData = ref(true);
const box8Summary = ref(0);
let box8ChartInstance = null;
const box8StageList = ref([]);
const box8MockDataByYear = {
"2025": {
......@@ -944,6 +1154,7 @@ const handleBox8Data = async () => {
box8HasData.value = true;
box8Summary.value = countMap.get("完成立法") || 0;
box8StageList.value = stages;
await nextTick();
const box8Chart = getBox8ChartOption(stages);
box8ChartInstance = setChart(box8Chart, "box8Chart");
......@@ -952,12 +1163,14 @@ const handleBox8Data = async () => {
if (data && data.stages && data.stages.length > 0) {
box8HasData.value = true;
box8Summary.value = data.passCount || 0;
box8StageList.value = data.stages;
await nextTick();
const box8Chart = getBox8ChartOption(data.stages);
box8ChartInstance = setChart(box8Chart, "box8Chart");
} else {
box8HasData.value = false;
box8Summary.value = 0;
box8StageList.value = [];
setChart({}, "box8Chart");
}
}
......@@ -967,12 +1180,14 @@ const handleBox8Data = async () => {
if (data && data.stages && data.stages.length > 0) {
box8HasData.value = true;
box8Summary.value = data.passCount || 0;
box8StageList.value = data.stages;
await nextTick();
const box8Chart = getBox8ChartOption(data.stages);
box8ChartInstance = setChart(box8Chart, "box8Chart");
} else {
box8HasData.value = false;
box8Summary.value = 0;
box8StageList.value = [];
setChart({}, "box8Chart");
}
}
......@@ -1329,6 +1544,150 @@ onUnmounted(() => {
}
}
.committee-cards-section {
width: 100%;
margin-top: 64px;
margin-bottom: 0;
.committee-cards-filter {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.committee-cards-desc {
color: var(--text-primary-65-color);
font-family: Microsoft YaHei;
font-size: var(--font-size-base);
font-weight: 400;
line-height: 22px;
}
.committee-time-switch {
:deep(.el-radio-group) {
display: inline-flex;
align-items: stretch;
padding: 4px;
border-radius: 50px;
background: var(--bg-black-10);
gap: 0;
flex-wrap: nowrap;
}
:deep(.el-radio-button) {
margin: 0;
.el-radio-button__inner {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 100%;
padding: 8px 20px;
border: none;
border-radius: 50px;
background: transparent;
box-shadow: none;
color: var(--text-primary-50-color);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 22px;
}
&:first-child .el-radio-button__inner {
border-radius: 50px;
}
&:last-child .el-radio-button__inner {
border-radius: 50px;
}
}
:deep(.el-radio-button.is-active) {
.el-radio-button__inner {
background: rgba(255, 255, 255, 0.65);
color: var(--text-primary-80-color);
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.12);
}
}
.committee-time-switch-inner {
display: inline-flex;
align-items: center;
gap: 6px;
}
.committee-time-switch-icon {
font-size: 16px;
}
}
}
.committee-cards-row {
display: flex;
gap: 16px;
width: 100%;
.committee-card {
flex: 1;
min-height: 100px;
display: flex;
align-items: center;
gap: 16px;
padding: 21px 23px;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: var(--bg-white-100);
box-sizing: border-box;
.committee-card-icon {
width: 54px;
height: 54px;
min-width: 54px;
border-radius: 50%;
background: var(--color-primary-10);
flex-shrink: 0;
}
.committee-card-content {
flex: 1;
min-width: 0;
text-align: left;
.committee-card-name {
color: var(--text-primary-80-color);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
line-height: 26px;
margin-bottom: 4px;
}
.committee-card-chamber {
color: var(--text-primary-50-color);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 22px;
}
}
.committee-card-count {
flex-shrink: 0;
align-self: center;
text-align: right;
color: var(--color-primary-100);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
line-height: 26px;
white-space: nowrap;
}
}
}
}
.divide1 {
margin-top: 64px;
margin-bottom: 36px;
......@@ -1845,7 +2204,7 @@ onUnmounted(() => {
margin-top: 21px;
height: 450px;
display: flex;
gap:16px;
gap: 16px;
.box3 {
width: 792px;
......@@ -2136,6 +2495,7 @@ onUnmounted(() => {
.overview-card--double {
width: calc(#{$overview-single-width} * 2 + #{$overview-card-gap});
}
.box5-main,
.box6-main,
.box7-main,
......
......@@ -65,9 +65,22 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
{
type: 'value',
position: 'left',
// 纵轴单位只在纵轴上方显示一次(通过 axis.name),避免每个刻度重复显示
name: '项',
nameLocation: 'end',
nameGap: 12,
nameTextStyle: {
color: '#666',
fontSize: 14,
fontWeight: 400,
// 给单位一点点下移空间,使其更贴近顶部刻度数字的视觉基线
padding: [0, 0, 6, -20]
},
axisLabel: {
formatter: '{value}项',
color: '#666'
formatter: '{value}',
color: '#666',
fontSize: 14,
fontWeight: 400
},
splitLine: {
show: true,
......@@ -83,9 +96,21 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
min: 0,
max: 100,
interval: 20,
// 通过率单位仅展示一次
name: '%',
nameLocation: 'end',
nameGap: 12,
nameTextStyle: {
color: '#666',
fontSize: 14,
fontWeight: 400,
padding: [0, 0, 6, 20]
},
axisLabel: {
formatter: '{value}%',
color: '#666'
formatter: '{value}',
color: '#666',
fontSize: 14,
fontWeight: 400
},
splitLine: {
show: true,
......
......@@ -6,9 +6,18 @@ const truncateLabel = (value, maxLen = 6) => {
return `${chars.slice(0, maxLen).join('')}...`
}
const getPieChart = (data, colorList) => {
const getPieChart = (data, colorList, options = {}) => {
const showCount = options.showCount !== false
let option = {
// color: colorList,
tooltip: showCount
? undefined
: {
formatter: params => {
const percent = typeof params.percent === 'number' ? params.percent : 0
return `${params.name}: ${percent}%`
}
},
series: [
{
type: 'pie',
......@@ -26,7 +35,8 @@ const getPieChart = (data, colorList) => {
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}%}`
const labelText = showCount ? `${value}${percent}%` : `${percent}%`
return `{name|${name}}\n{time|${labelText}}`
},
minMargin: 5,
edgeDistance: 10,
......
......@@ -107,214 +107,6 @@
:position="dialogPos"
@close="handleClickDetail(false)"
/>
<div class="dialog-wrapper1" v-if="isShowCompareDialog">
<div class="dialog-header">
<div class="header-left">
<div class="icon">
<img src="./assets/images/dialog-box4-icon.png" alt="" />
</div>
<div class="title">{{ "法案版本对比" }}</div>
</div>
<div class="header-right" @click="handleSwitchCompareDialog(false)">
<img src="./assets/images/close.png" alt="" />
</div>
</div>
<div class="dialog-info">
<div class="info-box1">
<span>原版本:</span>
<el-select v-model="preVersion" placeholder="请选择原版本" style="width: 300px">
<el-option v-for="item in preVersionList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="info-box2">
<span>现版本:</span>
<el-select v-model="curVersion" placeholder="请选择现版本" style="width: 300px">
<el-option v-for="item in curVersionList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="info-box3">
<div class="icon1" v-if="!isOnlyShowChinesePolicy" @click="handleSwitchPolicy(true)"></div>
<div class="icon2" v-else @click="handleSwitchPolicy(false)">
<img src="./assets/images/info-box3-selected.png" alt="" />
</div>
<div class="text">{{ "只看涉华条款" }}</div>
</div>
<div class="info-box4">
<el-input v-model="input" style="width: 160px" placeholder="搜索条款" />
<div class="icon">
<img src="./assets/images/search.png" alt="" />
</div>
</div>
</div>
<div class="dialog-main">
<div class="dialog-main-header">
<div class="dialog-main-header-left">
{{ " H.R.1(众议院)" }}
</div>
<div class="dialog-main-header-right">
{{ "H.R.1(参议院)" }}
</div>
</div>
<div class="dialog-main-item">
<div class="dialog-main-left">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(a)." }}</div>
<div class="title-right">
{{
"废止1930年《关税法》第321(a)(2)(C) 条规定的最低免税门槛。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Repeal the de minimis threshold under section 321(a)(2)(C) of the
Tariff Act of 1930.
</div>
</div>
<div class="dialog-main-right">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(b)." }}</div>
<div class="title-right">
{{
"自2027年1月1日起,对依据《1930年关税法》第321(a)(2)(C)条申报入境的货物,征收30%从价税或每件25美元关税(以较高者为准)。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Effective January 1, 2027, impose a duty of 30% ad valorem or $25
per package (whichever is higher) on articles entered under
section 321(a)(2)(C) of the Tariff Act of 1930.
</div>
</div>
</div>
<div class="dialog-main-item">
<div class="dialog-main-left">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(a)." }}</div>
<div class="title-right">
{{
"废止1930年《关税法》第321(a)(2)(C) 条规定的最低免税门槛。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Repeal the de minimis threshold under section 321(a)(2)(C) of the
Tariff Act of 1930.
</div>
</div>
<div class="dialog-main-right">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(b)." }}</div>
<div class="title-right">
{{
"自2027年1月1日起,对依据《1930年关税法》第321(a)(2)(C)条申报入境的货物,征收30%从价税或每件25美元关税(以较高者为准)。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Effective January 1, 2027, impose a duty of 30% ad valorem or $25
per package (whichever is higher) on articles entered under
section 321(a)(2)(C) of the Tariff Act of 1930.
</div>
</div>
</div>
<div class="dialog-main-item">
<div class="dialog-main-left">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(a)." }}</div>
<div class="title-right">
{{
"废止1930年《关税法》第321(a)(2)(C) 条规定的最低免税门槛。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Repeal the de minimis threshold under section 321(a)(2)(C) of the
Tariff Act of 1930.
</div>
</div>
<div class="dialog-main-right">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(b)." }}</div>
<div class="title-right">
{{
"自2027年1月1日起,对依据《1930年关税法》第321(a)(2)(C)条申报入境的货物,征收30%从价税或每件25美元关税(以较高者为准)。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Effective January 1, 2027, impose a duty of 30% ad valorem or $25
per package (whichever is higher) on articles entered under
section 321(a)(2)(C) of the Tariff Act of 1930.
</div>
</div>
</div>
<div class="dialog-main-item">
<div class="dialog-main-left">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(a)." }}</div>
<div class="title-right">
{{
"废止1930年《关税法》第321(a)(2)(C) 条规定的最低免税门槛。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Repeal the de minimis threshold under section 321(a)(2)(C) of the
Tariff Act of 1930.
</div>
</div>
<div class="dialog-main-right">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(b)." }}</div>
<div class="title-right">
{{
"自2027年1月1日起,对依据《1930年关税法》第321(a)(2)(C)条申报入境的货物,征收30%从价税或每件25美元关税(以较高者为准)。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Effective January 1, 2027, impose a duty of 30% ad valorem or $25
per package (whichever is higher) on articles entered under
section 321(a)(2)(C) of the Tariff Act of 1930.
</div>
</div>
</div>
<div class="dialog-main-item">
<div class="dialog-main-left">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(a)." }}</div>
<div class="title-right">
{{
"废止1930年《关税法》第321(a)(2)(C) 条规定的最低免税门槛。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Repeal the de minimis threshold under section 321(a)(2)(C) of the
Tariff Act of 1930.
</div>
</div>
<div class="dialog-main-right">
<div class="dialog-main-item-title">
<div class="title-left">{{ "§1102(b)." }}</div>
<div class="title-right">
{{
"自2027年1月1日起,对依据《1930年关税法》第321(a)(2)(C)条申报入境的货物,征收30%从价税或每件25美元关税(以较高者为准)。"
}}
</div>
</div>
<div class="dialog-main-item-content">
Effective January 1, 2027, impose a duty of 30% ad valorem or $25
per package (whichever is higher) on articles entered under
section 321(a)(2)(C) of the Tariff Act of 1930.
</div>
</div>
</div>
</div>
<div class="dialog-footer">
<el-pagination background layout="prev, pager, next" :total="1000" />
</div>
</div>
</div>
</template>
......@@ -336,14 +128,12 @@ const getBillDyqkSummaryList = async () => {
};
const res = await getBillDyqkSummary(params);
if (res && res.code === 200) {
console.log("法案流程概要:", res);
// 确保赋值的是数组,如果 data 为 null 则给空数组
actionList.value = Array.isArray(res.data) ? res.data : [];
} else {
actionList.value = [];
}
} catch (error) {
console.log(error);
actionList.value = []; // 出错时也重置为空数组
}
};
......@@ -400,35 +190,6 @@ const rightPos = computed(() => {
return (maxLineWidth.value + 90) + 'px';
});
const isShowCompareDialog = ref(false)
const handleSwitchCompareDialog = (isShow) => {
isShowCompareDialog.value = isShow
}
const isOnlyShowChinesePolicy = ref(false);
const handleSwitchPolicy = (isShow) => {
isOnlyShowChinesePolicy.value = isShow;
};
const preVersion = ref("委员会初始版本(2025年5月20日)");
const preVersionList = ref([
{
label: "委员会初始版本(2025年5月20日)",
value: "委员会初始版本(2025年5月20日)",
},
]);
const curVersion = ref("公法(2025年7月4日)");
const curVersionList = ref([
{
label: "公法(2025年7月4日)",
value: "公法(2025年7月4日)",
},
]);
const isShowDetailDialog = ref(false);
const currentDetailItem = ref({});
const dialogPos = ref({ left: '0px', top: '0px' });
......
......@@ -43,7 +43,17 @@
<div class="side">
<div class="side-box side-box-domain">
<AnalysisBox title="涉及领域" width="520px" height="415px" v-loading="domainLoading">
<div :class="['right-box2-main', { 'right-box-main--full': !domainFooterText }]" id="chart2"></div>
<div class="chart-ai-wrap">
<div :class="['right-box2-main', { 'right-box-main--full': !domainFooterText }]" id="chart2"></div>
<div class="overview-tip-row">
<TipTab class="overview-tip" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('domain')" />
</div>
<div v-if="aiPaneVisible.domain" class="overview-ai-pane"
@mouseleave="handleHideAiPane('domain')">
<AiPane :aiContent="overviewAiContent.domain" />
</div>
</div>
<div v-if="domainFooterText" class="right-box2-footer">
<div class="right-box2-footer-left">
<img src="./assets/icons/right-icon1.png" alt="" />
......@@ -59,7 +69,17 @@
</div>
<div class="side-box side-box-limit">
<AnalysisBox title="限制手段" width="520px" height="415px" v-loading="limitLoading">
<div :class="['right-box1-main', { 'right-box-main--full': !limitFooterText }]" id="chart1"></div>
<div class="chart-ai-wrap">
<div :class="['right-box1-main', { 'right-box-main--full': !limitFooterText }]" id="chart1"></div>
<div class="overview-tip-row">
<TipTab class="overview-tip" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('limit')" />
</div>
<div v-if="aiPaneVisible.limit" class="overview-ai-pane"
@mouseleave="handleHideAiPane('limit')">
<AiPane :aiContent="overviewAiContent.limit" />
</div>
</div>
<div v-if="limitFooterText" class="right-box1-footer">
<div class="right-box1-footer-left">
<img src="./assets/icons/right-icon1.png" alt="" />
......@@ -136,6 +156,10 @@ 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 { getChartAnalysis } from "@/api/aiAnalysis/index";
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 { MUTICHARTCOLORS } from "@/common/constant";
import { extractTextEntity } from "@/api/intelligent/index";
......@@ -318,6 +342,26 @@ const chart2ColorList = ref([...MUTICHARTCOLORS]);
const chart2Data = ref([]);
const aiPaneVisible = ref({
domain: false,
limit: false
});
const overviewAiContent = ref({
domain: "智能总结生成中...",
limit: "智能总结生成中..."
});
const aiPaneFetched = ref({
domain: false,
limit: false
});
const aiPaneLoading = ref({
domain: false,
limit: false
});
const displayTermsList = computed(() => {
const keyword = (searchKeyword.value || "").trim().toLowerCase();
const domain = selectedDomain.value;
......@@ -356,6 +400,83 @@ watch(
{ immediate: true }
);
const buildAiChartPayload = key => {
if (key === "domain") {
return {
type: "饼图",
name: "涉及领域",
data: (chart2Data.value || []).map(item => ({
industry_name: item.name,
count_bill: Number(item.value || 0)
}))
};
}
if (key === "limit") {
return {
type: "饼图",
name: "限制手段",
data: (chart1Data.value || []).map(item => ({
industry_name: item.name,
count_bill: Number(item.value || 0)
}))
};
}
return { type: "", name: "", data: [] };
};
const requestAiPaneContent = async key => {
if (!key || aiPaneLoading.value[key] || aiPaneFetched.value[key]) return;
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: true };
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成中..." };
try {
const payload = buildAiChartPayload(key);
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
overviewAiContent.value = {
...overviewAiContent.value,
[key]: base + chunk
};
}
}
);
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
const interpretation = first?.解读 || first?.["解读"];
if (interpretation) {
overviewAiContent.value = {
...overviewAiContent.value,
[key]: interpretation
};
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: false };
}
};
const handleShowAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: true
};
requestAiPaneContent(key);
};
const handleHideAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: false
};
};
const handleSearchSubmit = () => {
searchKeyword.value = searchValue.value;
currentPage.value = 1;
......@@ -554,6 +675,7 @@ const handleGetBillContentXzfs = async () => {
value: item.countTk
};
});
aiPaneFetched.value = { ...aiPaneFetched.value, limit: false };
let chart1 = getPieChart(chart1Data.value, chart1ColorList.value);
setChart(chart1, "chart1");
} catch (error) {
......@@ -602,6 +724,7 @@ const handleGetBillHyly = async () => {
value: item.countTk
};
});
aiPaneFetched.value = { ...aiPaneFetched.value, domain: false };
let chart2 = getPieChart(chart2Data.value, chart2ColorList.value);
setChart(chart2, "chart2");
......@@ -1017,6 +1140,38 @@ onMounted(async () => {
width: 520px;
}
.chart-ai-wrap {
position: relative;
display: flex;
flex-direction: column;
}
.overview-tip-row {
margin-top: 10px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.overview-tip-action {
position: absolute;
right: 0px;
}
.overview-ai-pane {
position: absolute;
left: 0;
bottom: -22px;
width: 100%;
z-index: 3;
pointer-events: none;
:deep(.ai-pane-wrapper) {
pointer-events: auto;
}
}
.side-box-limit {
margin-top: 15px;
width: 520px;
......@@ -1024,12 +1179,12 @@ onMounted(async () => {
.right-box1-main {
width: 520px;
height: 315px;
height: 275px;
padding: 16px;
}
.right-box-main--full {
height: 375px;
height: 315px;
}
.right-box1-footer {
......@@ -1091,12 +1246,12 @@ onMounted(async () => {
.right-box2-main {
width: 520px;
height: 315px;
height: 275px;
padding: 16px;
}
.right-box-main--full {
height: 375px;
height: 315px;
}
.right-box2-footer {
......
......@@ -151,7 +151,7 @@
<script setup>
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { getBillContentId, getBillVersionCompare } from "@/api/bill";
import { getBillContentId, getBillVersionCompare, getBillVersionCompareStatistics } 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";
......@@ -197,6 +197,22 @@ const isLoading = ref(false);
const comparePairs = ref([]);
const compareRequestToken = ref(0);
const queryAbortController = ref(null);
const resetAbortController = () => {
queryAbortController.value?.abort?.();
queryAbortController.value = new AbortController();
return queryAbortController.value;
};
const isAbortError = error => {
return (
error?.code === "ERR_CANCELED" ||
error?.name === "CanceledError" ||
error?.name === "AbortError" ||
(typeof error?.message === "string" && /canceled|aborted/i.test(error.message))
);
};
const handleLoadVersionOptions = async () => {
if (!billId.value) {
......@@ -273,21 +289,6 @@ const handleDiffTabClick = value => {
});
};
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);
......@@ -296,12 +297,6 @@ const mapDiffTypeToStatus = value => {
return "update";
};
const mapStatusToDiffType = value => {
if (value === "add") return "ADD";
if (value === "del") return "DELETE";
return "CHANGE";
};
const mapVersionCompareItemToPair = item => {
const oldTerm = item?.originalClauseMainId
? {
......@@ -324,63 +319,78 @@ const mapVersionCompareItemToPair = item => {
return { oldTerm, newTerm };
};
const fetchComparePage = async ({ diff, page, size }) => {
const getVersionCompareBaseParams = () => ({
billId: billId.value,
content: keyword.value,
currentVersion: newVersionId.value,
isCn: onlyChinaRelated.value ? "Y" : "N",
originalVersion: oldVersionId.value
});
const fetchComparePage = async ({ diff, page, size, signal }) => {
const params = {
billId: billId.value,
content: keyword.value,
...getVersionCompareBaseParams(),
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 res = await getBillVersionCompare(params, { signal });
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
total: Number(data?.totalElements ?? 0) || 0
};
};
const getCountByChangeType = (countTypeList, changeTypeLabel) => {
const list = Array.isArray(countTypeList) ? countTypeList : [];
const getCountByChangeType = (statisticsList, changeTypeLabel) => {
const list = Array.isArray(statisticsList) ? statisticsList : [];
const target = list.find(item => String(item?.changeType ?? "") === changeTypeLabel);
return Number(target?.count ?? 0) || 0;
};
const loadCompareStatistics = async (currentToken, signal) => {
debugger
const res = await getBillVersionCompareStatistics(getVersionCompareBaseParams(), { signal });
if (currentToken !== compareRequestToken.value) return;
const list = res.data
diffCounts.value = {
CHANGE: getCountByChangeType(list, "更新"),
ADD: getCountByChangeType(list, "新增"),
DELETE: getCountByChangeType(list, "删除")
};
};
const loadComparePairs = async () => {
if (!billId.value || !oldVersionId.value || !newVersionId.value) {
queryAbortController.value?.abort?.();
comparePairs.value = [];
updateDiffCounts([]);
diffCounts.value = { CHANGE: 0, ADD: 0, DELETE: 0 };
total.value = 0;
return;
}
const controller = resetAbortController();
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;
const [currentRes] = await Promise.all([
fetchComparePage({
diff: diffType.value,
page: currentPage.value,
size: pageSize.value,
signal: controller.signal
}),
loadCompareStatistics(currentToken, controller.signal)
]);
if (currentToken !== compareRequestToken.value || controller.signal.aborted) 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);
await ensureEntitiesForPairs(comparePairs.value, controller.signal);
} catch (error) {
if (currentToken !== compareRequestToken.value) return;
if (currentToken !== compareRequestToken.value || isAbortError(error)) return;
comparePairs.value = [];
updateDiffCounts([]);
diffCounts.value = { CHANGE: 0, ADD: 0, DELETE: 0 };
total.value = 0;
} finally {
if (currentToken === compareRequestToken.value) {
......@@ -532,8 +542,9 @@ const getTermEntityKey = (term, lang) => {
return `${baseKey}__${lang}`;
};
const ensureEntitiesForPairs = async pairs => {
const ensureEntitiesForPairs = async (pairs, signal = queryAbortController.value?.signal) => {
if (!termsHighlight.value) return;
if (signal?.aborted) return;
const list = Array.isArray(pairs) ? pairs : [];
if (!list.length) return;
......@@ -563,18 +574,19 @@ const ensureEntitiesForPairs = async pairs => {
try {
const results = await Promise.all(
tasks.map(async item => {
const res = await extractTextEntity(item.text);
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
const res = await extractTextEntity(item.text, { signal });
const entities = normalizeEntities(res?.result ?? res?.data?.result ?? res?.data ?? res);
return { key: item.key, entities };
})
);
if (currentToken !== entityRequestToken.value) return;
if (currentToken !== entityRequestToken.value || signal?.aborted) return;
for (const r of results) {
termEntityCache.value.set(r.key, r.entities);
}
} catch (error) {
if (currentToken !== entityRequestToken.value) return;
if (currentToken !== entityRequestToken.value || isAbortError(error)) return;
}
};
......@@ -721,6 +733,8 @@ onMounted(async () => {
flex-direction: column;
row-gap: 16px;
width: 100%;
height: 848px;
overflow: hidden;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px 75px;
......@@ -974,6 +988,10 @@ onMounted(async () => {
.compare-columns {
margin-top: 16px;
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论