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

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

上级 b46e0652
...@@ -77,6 +77,58 @@ function extractInterpretationFromLooseText(text) { ...@@ -77,6 +77,58 @@ function extractInterpretationFromLooseText(text) {
return String(m[1]).replace(/\\n/g, "\n").trim(); 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 流式) * 图表解读(SSE 流式)
* @param {object} data - 请求体 * @param {object} data - 请求体
...@@ -94,6 +146,8 @@ export function getChartAnalysis(data, options = {}) { ...@@ -94,6 +146,8 @@ export function getChartAnalysis(data, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let buffer = ""; let buffer = "";
let latestInterpretation = ""; let latestInterpretation = "";
/** 已推给前端的「解读」正文长度,用于只增量回调 onChunk */
let lastStreamedInterpretationLen = 0;
let settled = false; let settled = false;
const abortController = new AbortController(); const abortController = new AbortController();
...@@ -175,9 +229,31 @@ export function getChartAnalysis(data, options = {}) { ...@@ -175,9 +229,31 @@ export function getChartAnalysis(data, options = {}) {
return; return;
} }
// 每收到一条消息即回调,用于流式渲染 // 流式渲染:不把 ```json、[、{ 等 markdown/JSON 外壳拼到界面
if (chunk && onDelta) { 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,则提前解析并中断连接 // 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
......
...@@ -286,8 +286,7 @@ export function getThinkPolicyIndustryChange(params) { ...@@ -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) { export function getThinkPolicy(params) {
return request({ return request({
......
...@@ -103,17 +103,19 @@ ...@@ -103,17 +103,19 @@
</div> </div>
<div class="right-footer-box"> <div class="right-footer-box">
<div class="relatedBills" v-for="(item, index) in box1DataItem.relatedBills" :key="index" v-show="item"> <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="tag">{{ "科技领域相似" }}</div>
<div class="relatedBills-content">{{ item.name }}</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>
<div class="relatedAdministrativeOrders" v-for="(item, index) in box1DataItem.relatedAdministrativeOrders" <div class="relatedAdministrativeOrders" v-for="(item, index) in box1DataItem.relatedAdministrativeOrders"
v-show="item" :key="index"> v-show="item" :key="index">
<div class="tag">{{ "政令" }}</div> <div class="tag">{{ "政令" }}</div>
<div class="tag">{{ "科技领域相似" }}</div> <div class="tag">{{ "科技领域相似" }}</div>
<div class="relatedAdministrativeOrders-content">{{ item.content }}</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> </div>
</div> </div>
...@@ -217,6 +219,29 @@ const handleTagClick = tag => { ...@@ -217,6 +219,29 @@ const handleTagClick = tag => {
activeItemIndex.value = 0; activeItemIndex.value = 0;
updatePageData(); 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 = () => { const handleSearchOpinions = () => {
// 搜索时默认切回“全部”标签 // 搜索时默认切回“全部”标签
activeTag.value = ""; activeTag.value = "";
......
...@@ -74,11 +74,12 @@ ...@@ -74,11 +74,12 @@
</div> </div>
<div class="box5-footer"> <div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" /> <TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton /> <AiButton />
</div> </div>
</div>
<div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)"> <div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" /> <AiPane :aiContent="aiContentBox5" />
</div> </div>
...@@ -927,9 +928,18 @@ onMounted(() => { ...@@ -927,9 +928,18 @@ onMounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
position: absolute;
bottom: 20px;
left: 32px;
}
.ai-wrap {
position: absolute; position: absolute;
bottom: 18px; bottom: 18px;
right: 0; right: 0;
cursor: pointer;
} }
...@@ -942,14 +952,9 @@ onMounted(() => { ...@@ -942,14 +952,9 @@ onMounted(() => {
} }
.ai-wrap {
position: relative;
cursor: pointer;
} }
} }
}
.box2 { .box2 {
......
...@@ -252,7 +252,7 @@ ...@@ -252,7 +252,7 @@
<div class="file" v-for="(file, idxx) in item.relatedBills" :key="`${file.id}-${idxx}`"> <div class="file" v-for="(file, idxx) in item.relatedBills" :key="`${file.id}-${idxx}`">
<div class="type">法案</div> <div class="type">法案</div>
<div class="title">{{ file.name }}</div> <div class="title">{{ file.name }}</div>
<div class="more"> <div class="more" @click.stop="handleBillMoreClick(file)">
<img src="./images/arrow-right.png" alt="" /> <img src="./images/arrow-right.png" alt="" />
</div> </div>
</div> </div>
...@@ -260,7 +260,7 @@ ...@@ -260,7 +260,7 @@
:key="file.bcId != null ? String(file.bcId) + '-' + idxx : idxx"> :key="file.bcId != null ? String(file.bcId) + '-' + idxx : idxx">
<div class="type">政令</div> <div class="type">政令</div>
<div class="title">{{ file.content }}</div> <div class="title">{{ file.content }}</div>
<div class="more"> <div class="more" @click.stop="handleAdministrativeMoreClick(file)">
<img src="./images/arrow-right.png" alt="" /> <img src="./images/arrow-right.png" alt="" />
</div> </div>
</div> </div>
...@@ -458,6 +458,30 @@ const handleGetThinkPolicyIndustry = async () => { ...@@ -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([]); const box2Data = ref([]);
...@@ -1423,9 +1447,8 @@ onMounted(() => { ...@@ -1423,9 +1447,8 @@ onMounted(() => {
.source { .source {
position: absolute; position: absolute;
bottom: 21px; bottom: 21px;
left: 50%; left: 24px;
transform: translateX(-50%); width: 420px;
width: 350px;
height: 22px; height: 22px;
display: flex; display: flex;
} }
...@@ -1486,10 +1509,8 @@ onMounted(() => { ...@@ -1486,10 +1509,8 @@ onMounted(() => {
.source { .source {
position: absolute; position: absolute;
bottom: 21px; bottom: 24px;
left: 50%; width: 420px;
transform: translateX(-50%);
width: 350px;
height: 22px; height: 22px;
display: flex; display: flex;
......
...@@ -402,80 +402,8 @@ const handleSelectedFiltersUpdate = val => { ...@@ -402,80 +402,8 @@ const handleSelectedFiltersUpdate = val => {
const author = ref('') // 作者 const author = ref('') // 作者
const curFooterList = 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
// }
]);
...@@ -579,11 +507,10 @@ const handleGetThinkDynamicsReport = async (payload) => { ...@@ -579,11 +507,10 @@ const handleGetThinkDynamicsReport = async (payload) => {
params.keyword = keyword; params.keyword = keyword;
} }
const res = await getThinkTankReport(params); const res = await getThinkTankReport(params);
console.log("智库动态报告", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
curFooterList.value = res.data.content; curFooterList.value = res.data.content;
total.value = res.data.totalElements; total.value = res.data.totalElements;
} else if (res.code === 500 && res.message === "未找到对应筛选的报告") { } else {
curFooterList.value = []; curFooterList.value = [];
total.value = 0; total.value = 0;
} }
......
...@@ -453,9 +453,9 @@ const handleGetThinkTankFundsSource = async () => { ...@@ -453,9 +453,9 @@ const handleGetThinkTankFundsSource = async () => {
alignTo: 'edge', alignTo: 'edge',
offset: [0, -LABEL_OFFSET_UP], offset: [0, -LABEL_OFFSET_UP],
formatter(params) { formatter(params) {
const valueYi = (params.data.value || 0) / 100000000 const valueYi = (params.data.value || 0) / 10000
const percent = params.percent || 0 const percent = params.percent || 0
const valueStr = `${valueYi.toFixed(3)}亿 ${percent}%` const valueStr = `${valueYi} ${percent}%`
let cumulative = 0 let cumulative = 0
for (let i = 0; i < params.dataIndex; i++) cumulative += dataList[i].value || 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 const centerAngle = 90 + ((cumulative + (params.data.value || 0) / 2) / total) * 360
...@@ -1096,10 +1096,10 @@ onMounted(() => { ...@@ -1096,10 +1096,10 @@ onMounted(() => {
} }
.source { .source {
margin: 0 auto;
margin-top: 10px; margin-top: 10px;
margin-left: 240px;
/* 上下0,左右自动居中 */
width: 370px; width: 370px;
height: 22px; height: 22px;
display: flex; display: flex;
...@@ -1285,7 +1285,7 @@ onMounted(() => { ...@@ -1285,7 +1285,7 @@ onMounted(() => {
} }
.source { .source {
margin: 0 auto; margin-left: 35px;
margin-top: 10px; margin-top: 10px;
/* 上下0,左右自动居中 */ /* 上下0,左右自动居中 */
......
...@@ -7,18 +7,11 @@ ...@@ -7,18 +7,11 @@
<div class="title">{{ "科技领域" }}</div> <div class="title">{{ "科技领域" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<el-checkbox-group <el-checkbox-group class="checkbox-group" :model-value="selectedTypeIds" @change="handleAreaGroupChange">
class="checkbox-group"
:model-value="selectedTypeIds"
@change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA"> <el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }} {{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox> </el-checkbox>
<el-checkbox <el-checkbox class="filter-checkbox" v-for="t in (researchTypeList || [])" :key="t.id" :label="t.id">
class="filter-checkbox"
v-for="t in (researchTypeList || [])"
:key="t.id"
:label="t.id">
{{ t.name }} {{ t.name }}
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>
...@@ -31,18 +24,11 @@ ...@@ -31,18 +24,11 @@
<div class="title">{{ "发布时间" }}</div> <div class="title">{{ "发布时间" }}</div>
</div> </div>
<div class="select-main"> <div class="select-main">
<el-checkbox-group <el-checkbox-group class="checkbox-group" :model-value="selectedYearIds" @change="handleYearGroupChange">
class="checkbox-group"
:model-value="selectedYearIds"
@change="handleYearGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME"> <el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }} {{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox> </el-checkbox>
<el-checkbox <el-checkbox class="filter-checkbox" v-for="y in (researchTimeList || [])" :key="y.id" :label="y.id">
class="filter-checkbox"
v-for="y in (researchTimeList || [])"
:key="y.id"
:label="y.id">
{{ y.name }} {{ y.name }}
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>
...@@ -71,17 +57,17 @@ ...@@ -71,17 +57,17 @@
</div> </div>
<div class="file-box"> <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="type">法案</div>
<div class="title">{{ item.billInfoList[0].name }}</div> <div class="title">{{ sv.name }}</div>
<div class="more"> <div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" /> <img src="../assets/images/image-right.png" alt="" />
</div> </div>
</div> </div>
<div class="file"> <div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index">
<div class="type">政令</div> <div class="type">政令</div>
<div class="title">{{ item.billInfoList[0].name }}</div> <div class="title">{{ sv.content }}</div>
<div class="more"> <div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" /> <img src="../assets/images/image-right.png" alt="" />
</div> </div>
</div> </div>
...@@ -106,6 +92,7 @@ ...@@ -106,6 +92,7 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router";
import AreaTag from "@/components/base/AreaTag/index.vue"; import AreaTag from "@/components/base/AreaTag/index.vue";
import { import {
RESOURCE_FILTER_ALL_AREA, RESOURCE_FILTER_ALL_AREA,
...@@ -126,6 +113,32 @@ defineProps({ ...@@ -126,6 +113,32 @@ defineProps({
const emit = defineEmits(["filter-change", "page-change", "item-click"]); 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 selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]); const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]);
...@@ -428,6 +441,7 @@ const handleYearGroupChange = (val) => { ...@@ -428,6 +441,7 @@ const handleYearGroupChange = (val) => {
height: 20px; height: 20px;
display: flex; display: flex;
margin-top: 2px; margin-top: 2px;
cursor: pointer;
.img { .img {
width: 100%; width: 100%;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论