提交 2ad8e183 authored 作者: 朱政's avatar 朱政

feat:智库中政令法案跳转,tip标签样式修改

上级 b46e0652
......@@ -77,6 +77,58 @@ function extractInterpretationFromLooseText(text) {
return String(m[1]).replace(/\\n/g, "\n").trim();
}
/**
* 从流式累积 buffer 中提取「解读」字符串的已生成部分(滤掉 ```json、[、{ 等外壳,避免界面出现 json\n[\n)
* 支持未闭合的字符串(流式进行中)
* @param {string} buffer
* @returns {string}
*/
function extractStreamingInterpretationFromBuffer(buffer) {
const s = String(buffer || "");
let rest = s.replace(/^\uFEFF/, "");
const fence = rest.match(/^```(?:json)?\s*/i);
if (fence) {
rest = rest.slice(fence[0].length);
}
const keyRe =
/["'](?:解读|interpretation|analysis|content)["']\s*:\s*"/i;
const m = rest.match(keyRe);
if (!m) {
return "";
}
let pos = m.index + m[0].length;
let out = "";
while (pos < rest.length) {
const ch = rest[pos];
if (ch === '"') {
break;
}
if (ch === "\\") {
pos += 1;
if (pos >= rest.length) {
break;
}
const esc = rest[pos];
if (esc === "n") {
out += "\n";
} else if (esc === "r") {
out += "\r";
} else if (esc === "t") {
out += "\t";
} else if (esc === '"' || esc === "\\") {
out += esc;
} else {
out += esc;
}
pos += 1;
continue;
}
out += ch;
pos += 1;
}
return out;
}
/**
* 图表解读(SSE 流式)
* @param {object} data - 请求体
......@@ -94,6 +146,8 @@ export function getChartAnalysis(data, options = {}) {
return new Promise((resolve, reject) => {
let buffer = "";
let latestInterpretation = "";
/** 已推给前端的「解读」正文长度,用于只增量回调 onChunk */
let lastStreamedInterpretationLen = 0;
let settled = false;
const abortController = new AbortController();
......@@ -175,9 +229,31 @@ export function getChartAnalysis(data, options = {}) {
return;
}
// 每收到一条消息即回调,用于流式渲染
// 流式渲染:不把 ```json、[、{ 等 markdown/JSON 外壳拼到界面
if (chunk && onDelta) {
onDelta(chunk);
let parsedMsg = null;
try {
parsedMsg = JSON.parse(raw);
} catch (_) {
parsedMsg = null;
}
const isReasoningChunk =
parsedMsg &&
typeof parsedMsg === "object" &&
parsedMsg.type === "reasoning" &&
typeof parsedMsg.chunk === "string";
if (isReasoningChunk) {
onDelta(parsedMsg.chunk);
} else {
const visible = extractStreamingInterpretationFromBuffer(buffer);
if (visible.length > lastStreamedInterpretationLen) {
onDelta(
visible.slice(lastStreamedInterpretationLen)
);
lastStreamedInterpretationLen = visible.length;
}
}
}
// 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
......
......@@ -286,8 +286,7 @@ export function getThinkPolicyIndustryChange(params) {
/**
* 获取智库政策(政策追踪列表)
* GET /api/thinkTankInfo/policy
* Query: thinkTankId, startDate, endDate, orgIds, domainIds(科技领域/智库领域,逗号分隔 id), pageNum, pageSize, sortField, sortOrder, sortFun, reportId 等
*/
export function getThinkPolicy(params) {
return request({
......
......@@ -103,17 +103,19 @@
</div>
<div class="right-footer-box">
<div class="relatedBills" v-for="(item, index) in box1DataItem.relatedBills" :key="index" v-show="item">
<div class="tag">{{ "政令" }}</div>
<div class="tag">{{ "法案" }}</div>
<div class="tag">{{ "科技领域相似" }}</div>
<div class="relatedBills-content">{{ item.name }}</div>
<div class="footer-image"> <img src="../images/image-right.png" alt="" /></div>
<div class="footer-image" @click.stop="handleBillMoreClick(item)"> <img src="../images/image-right.png"
alt="" /></div>
</div>
<div class="relatedAdministrativeOrders" v-for="(item, index) in box1DataItem.relatedAdministrativeOrders"
v-show="item" :key="index">
<div class="tag">{{ "政令" }}</div>
<div class="tag">{{ "科技领域相似" }}</div>
<div class="relatedAdministrativeOrders-content">{{ item.content }}</div>
<div class="footer-image"> <img src="../images/image-right.png" alt="" /></div>
<div class="footer-image" @click.stop="handleAdministrativeMoreClick(item)"> <img
src="../images/image-right.png" alt="" /></div>
</div>
</div>
</div>
......@@ -217,6 +219,29 @@ const handleTagClick = tag => {
activeItemIndex.value = 0;
updatePageData();
};
const handleBillMoreClick = (bill) => {
const billId = bill?.id;
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
/** 政策建议关联法案:新标签页打开政令介绍页*/
const handleAdministrativeMoreClick = (ad) => {
const id = ad?.bcId;
if (!id) {
return;
}
const route = router.resolve({
path: "/decreeLayout/overview/introduction",
query: { id: String(id) }
});
window.open(route.href, "_blank");
};
const handleSearchOpinions = () => {
// 搜索时默认切回“全部”标签
activeTag.value = "";
......
......@@ -74,11 +74,12 @@
</div>
<div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
</div>
<div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" />
</div>
......@@ -927,9 +928,18 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 20px;
left: 32px;
}
.ai-wrap {
position: absolute;
bottom: 18px;
right: 0;
cursor: pointer;
}
......@@ -942,14 +952,9 @@ onMounted(() => {
}
.ai-wrap {
position: relative;
cursor: pointer;
}
}
}
.box2 {
......
......@@ -252,7 +252,7 @@
<div class="file" v-for="(file, idxx) in item.relatedBills" :key="`${file.id}-${idxx}`">
<div class="type">法案</div>
<div class="title">{{ file.name }}</div>
<div class="more">
<div class="more" @click.stop="handleBillMoreClick(file)">
<img src="./images/arrow-right.png" alt="" />
</div>
</div>
......@@ -260,7 +260,7 @@
:key="file.bcId != null ? String(file.bcId) + '-' + idxx : idxx">
<div class="type">政令</div>
<div class="title">{{ file.content }}</div>
<div class="more">
<div class="more" @click.stop="handleAdministrativeMoreClick(file)">
<img src="./images/arrow-right.png" alt="" />
</div>
</div>
......@@ -458,6 +458,30 @@ const handleGetThinkPolicyIndustry = async () => {
}
};
/** 政策建议关联法案:新标签页打开法案介绍页,billId 随接口 id 变化 */
const handleBillMoreClick = (bill) => {
const billId = bill?.id;
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
/** 政策建议关联法案:新标签页打开政令介绍页*/
const handleAdministrativeMoreClick = (ad) => {
const id = ad?.bcId;
if (!id) {
return;
}
const route = router.resolve({
path: "/decreeLayout/overview/introduction",
query: { id: String(id) }
});
window.open(route.href, "_blank");
};
// 政策建议涉及部门分布(饼图)
const box2Data = ref([]);
......@@ -1423,9 +1447,8 @@ onMounted(() => {
.source {
position: absolute;
bottom: 21px;
left: 50%;
transform: translateX(-50%);
width: 350px;
left: 24px;
width: 420px;
height: 22px;
display: flex;
}
......@@ -1486,10 +1509,8 @@ onMounted(() => {
.source {
position: absolute;
bottom: 21px;
left: 50%;
transform: translateX(-50%);
width: 350px;
bottom: 24px;
width: 420px;
height: 22px;
display: flex;
......
......@@ -402,80 +402,8 @@ const handleSelectedFiltersUpdate = val => {
const author = ref('') // 作者
const curFooterList = ref([
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img1
// },
// {
// title: "中美对抗、竞争和合作跨越人工智能通用领域...",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img2
// },
// {
// title: "中国、智慧城市和中东:地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img3
// },
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img4
// },
// {
// title: "中美经济竞争:复杂经济和地缘政治关系中的...",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img5
// },
// {
// title: "中国、智慧城市和中东:留给地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img6
// },
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img7
// },
// {
// title: "中美对抗、竞争和合作跨越人工智能通用领域...",
// time: "2025年6月26日",
// from: "",
// img: Img8
// },
// {
// title: "中国、智慧城市和中东:地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img9
// },
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img10
// },
// {
// title: "中美对抗、竞争和合作跨越人工智能通用领域...",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img11
// },
// {
// title: "中国、智慧城市和中东:地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img12
// }
]);
/** 智库报告 / 调查项目共用列表:与资源库一致,初始为空,接口非成功时清空 */
const curFooterList = ref([]);
......@@ -579,11 +507,10 @@ const handleGetThinkDynamicsReport = async (payload) => {
params.keyword = keyword;
}
const res = await getThinkTankReport(params);
console.log("智库动态报告", res);
if (res.code === 200 && res.data) {
curFooterList.value = res.data.content;
total.value = res.data.totalElements;
} else if (res.code === 500 && res.message === "未找到对应筛选的报告") {
} else {
curFooterList.value = [];
total.value = 0;
}
......
......@@ -453,9 +453,9 @@ const handleGetThinkTankFundsSource = async () => {
alignTo: 'edge',
offset: [0, -LABEL_OFFSET_UP],
formatter(params) {
const valueYi = (params.data.value || 0) / 100000000
const valueYi = (params.data.value || 0) / 10000
const percent = params.percent || 0
const valueStr = `${valueYi.toFixed(3)}亿 ${percent}%`
const valueStr = `${valueYi} ${percent}%`
let cumulative = 0
for (let i = 0; i < params.dataIndex; i++) cumulative += dataList[i].value || 0
const centerAngle = 90 + ((cumulative + (params.data.value || 0) / 2) / total) * 360
......@@ -1096,10 +1096,10 @@ onMounted(() => {
}
.source {
margin: 0 auto;
margin-top: 10px;
margin-left: 240px;
/* 上下0,左右自动居中 */
width: 370px;
height: 22px;
display: flex;
......@@ -1285,7 +1285,7 @@ onMounted(() => {
}
.source {
margin: 0 auto;
margin-left: 35px;
margin-top: 10px;
/* 上下0,左右自动居中 */
......
......@@ -7,18 +7,11 @@
<div class="title">{{ "科技领域" }}</div>
</div>
<div class="select-main">
<el-checkbox-group
class="checkbox-group"
:model-value="selectedTypeIds"
@change="handleAreaGroupChange">
<el-checkbox-group class="checkbox-group" :model-value="selectedTypeIds" @change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox>
<el-checkbox
class="filter-checkbox"
v-for="t in (researchTypeList || [])"
:key="t.id"
:label="t.id">
<el-checkbox class="filter-checkbox" v-for="t in (researchTypeList || [])" :key="t.id" :label="t.id">
{{ t.name }}
</el-checkbox>
</el-checkbox-group>
......@@ -31,18 +24,11 @@
<div class="title">{{ "发布时间" }}</div>
</div>
<div class="select-main">
<el-checkbox-group
class="checkbox-group"
:model-value="selectedYearIds"
@change="handleYearGroupChange">
<el-checkbox-group class="checkbox-group" :model-value="selectedYearIds" @change="handleYearGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox>
<el-checkbox
class="filter-checkbox"
v-for="y in (researchTimeList || [])"
:key="y.id"
:label="y.id">
<el-checkbox class="filter-checkbox" v-for="y in (researchTimeList || [])" :key="y.id" :label="y.id">
{{ y.name }}
</el-checkbox>
</el-checkbox-group>
......@@ -71,17 +57,17 @@
</div>
<div class="file-box">
<div class="file">
<div class="file" v-for="sv in item.billInfoList" :key="sv.id || sv.name">
<div class="type">法案</div>
<div class="title">{{ item.billInfoList[0].name }}</div>
<div class="more">
<div class="title">{{ sv.name }}</div>
<div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
<div class="file">
<div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index">
<div class="type">政令</div>
<div class="title">{{ item.billInfoList[0].name }}</div>
<div class="more">
<div class="title">{{ sv.content }}</div>
<div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
......@@ -106,6 +92,7 @@
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import AreaTag from "@/components/base/AreaTag/index.vue";
import {
RESOURCE_FILTER_ALL_AREA,
......@@ -126,6 +113,32 @@ defineProps({
const emit = defineEmits(["filter-change", "page-change", "item-click"]);
const router = useRouter();
/** 政策建议关联法案:新标签页打开法案介绍页,billId 随接口 id 变化 */
const handleBillMoreClick = (bill) => {
const billId = bill?.id;
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
/** 政策建议关联法案:新标签页打开政令介绍页*/
const handleAdministrativeMoreClick = (ad) => {
const id = ad?.bcId;
if (!id) {
return;
}
const route = router.resolve({
path: "/decreeLayout/overview/introduction",
query: { id: String(id) }
});
window.open(route.href, "_blank");
};
const selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]);
......@@ -428,6 +441,7 @@ const handleYearGroupChange = (val) => {
height: 20px;
display: flex;
margin-top: 2px;
cursor: pointer;
.img {
width: 100%;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论