提交 6b4028a9 authored 作者: 朱政's avatar 朱政

Merge branch 'pre' into zz-dev

流水线 #310 已通过 于阶段
in 1 分 28 秒
......@@ -17,4 +17,11 @@ export function getThinkTankList() {
method: 'GET',
url: `/temporarySearch/search-info/all-organization-names`,
})
}
export function getStatusList() {
return request({
method: 'GET',
url: `/temporarySearch/search-info/dBillStage`,
})
}
\ No newline at end of file
<template>
<div class="layout-container">
<div class="layout-main">
<div class="layout-main-center">
<div class="report-header">
<div class="report-title">政令原文</div>
<el-switch v-model="isHighlight" @change="handleHighlight" />
<div class="switch-label switch-label-left">高亮实体</div>
<el-switch v-model="isTranslate" @change="handleTranslate" />
<div class="switch-label">译文显示</div>
<div class="btn" @click="emits('download')">
<div class="icon icon-gap-4">
<img src="./assets/icons/download.png" alt="" />
</div>
<div class="text">下载</div>
</div>
<div class="btn" @click="handleFindWord('open')">
<div class="icon icon-gap-6">
<img src="./assets/icons/search.png" alt="" />
</div>
<div class="text">查找</div>
</div>
<div class="find-word-box" v-if="findWordBox">
<div class="find-word-input">
<el-input v-model="findWordTxt" placeholder="查找原文内容" @input="handleUpdateWord" />
</div>
<div class="find-word-limit">{{ findWordNum }}/{{ findWordMax }}</div>
<div class="find-word-icon" @click="handleFindWord('last')">
<el-icon><ArrowUp /></el-icon>
</div>
<div class="find-word-icon" @click="handleFindWord('next')">
<el-icon><ArrowDown /></el-icon>
</div>
<div class="find-word-icon" @click="handleFindWord('close')">
<el-icon><Close /></el-icon>
</div>
</div>
</div>
<div :class="['report-main', {'is-highlight': isHighlight}]">
<div v-if="!formatData.length" class="no-content">暂无数据</div>
<el-scrollbar v-else height="100%">
<div v-for="(item, index) in formatData" :key="index" class="content-row">
<div class="content-en" :class="{ 'only-show': !isTranslate }">
<template v-for="(node, num) in item.contentEn" :key="num">
<el-icon v-if="node.icon && isHighlight" color="#508fd4" size="14"><Search /></el-icon>
<span v-else-if="node.point" class="find-light-point"></span>
<span v-else-if="node.fill" :class="node.class" @click="handleHighLight(node)">{{ node.txt }}</span>
<span v-else :class="node.class">{{ node.txt }}</span>
</template>
</div>
<div class="content-ch" v-if="isTranslate">
<template v-for="(node, num) in item.contentCh" :key="num">
<el-icon v-if="node.icon && isHighlight" color="#508fd4" size="14"><Search /></el-icon>
<span v-else-if="node.point" class="find-light-point"></span>
<span v-else-if="node.fill" :class="node.class" @click="handleHighLight(node)">{{ node.txt }}</span>
<span v-else :class="node.class">{{ node.txt }}</span>
</template>
</div>
</div>
</el-scrollbar>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { nextTick, ref } from "vue";
import { debounce } from "lodash";
import { extractTextEntity } from "@/api/intelligent";
const emits = defineEmits(["download"]);
const isHighlight = ref(true); // 是否高亮实体
const isTranslate = ref(true); // 是否显示译文
const handleTranslate = () => {
if (findWordTxt.value?.trim()) handleUpdateWord()
}
const findWordTxt = ref("");
const findWordBox = ref(false);
const findWordNum = ref(0);
const findWordMax = ref(0);
const formatData = ref([])
const handleUpdateWord = debounce(() => {
findWordNum.value = 0;
findWordMax.value = 0;
formatData.value = originData.map(item => {
return {
contentEn: onHighlightedText(item.contentEn, keywordEn),
contentCh: isTranslate.value ? onHighlightedText(item.contentCh, keywordCh) : [],
}
})
if (findWordMax.value) {
findWordNum.value = 1;
nextTick(updateActiveHighlight)
}
}, 300);
function handleFindWord(event) {
switch (event) {
case "open":
findWordBox.value = true;
break;
case "last":
if (findWordMax.value > 1) {
findWordNum.value = findWordNum.value === 1 ? findWordMax.value : findWordNum.value - 1;
updateActiveHighlight();
}
break;
case "next":
if (findWordMax.value > 1) {
findWordNum.value = findWordNum.value === findWordMax.value ? 1 : findWordNum.value + 1;
updateActiveHighlight();
}
break;
case "close":
findWordBox.value = false;
findWordTxt.value = "";
handleUpdateWord();
break;
}
}
const updateActiveHighlight = () => {
if (!findWordNum.value) return
const spans = document.querySelectorAll("span.find-light-point");
spans.forEach((span, index) => {
if (index + 1 === findWordNum.value) span.scrollIntoView({});
});
}
// 政令原文信息
let originData = []
let keywordCh = []
let keywordEn = []
const setOriginalData = (data) => {
originData = []
let num = Math.max(data.content.length, data.contentEn.length)
for (let i = 0; i < num; i++) {
let obj = {
contentCh: data.content[i] || "",
contentEn: data.contentEn[i] || "",
}
originData.push(obj);
}
let allEn = ""
let allCh = ""
formatData.value = originData.map(item => {
allEn += item.contentEn + "。"
allCh += item.contentCh + "。"
return {
contentEn: [{txt: item.contentEn, class: ""}],
contentCh: [{txt: item.contentCh, class: ""}]
}
})
extractTextEntity(allEn || "").then(res => {
if (res?.result?.length) keywordEn = res.result || []
originData.forEach((item, index) => {
formatData.value[index].contentEn = onHighlightedText(item.contentEn, keywordEn);
})
})
extractTextEntity(allCh || "").then(res => {
if (res?.result?.length) keywordCh = res.result || []
if (isTranslate.value) {
originData.forEach((item, index) => {
formatData.value[index].contentCh = onHighlightedText(item.contentCh, keywordCh);
})
}
})
}
defineExpose({ setOriginalData })
// 文本分割
const onHighlightedText = (text, nounList) => {
if (text === "") return []
// 获取区间(合并相邻/重叠),并自动过滤空关键词
const getIntervals = (str, keywords) => {
const intervals = [];
for (const kw of keywords) {
if (!kw) continue; // 跳过空字符串,防止无限循环
let idx = 0;
while ((idx = str.indexOf(kw, idx)) !== -1) {
intervals.push({ start: idx, end: idx + kw.length });
idx += kw.length;
}
}
if (!intervals.length) return [];
intervals.sort((a, b) => a.start - b.start);
const merged = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const prev = merged[merged.length - 1];
const curr = intervals[i];
if (curr.start <= prev.end) {
prev.end = Math.max(prev.end, curr.end);
} else {
merged.push(curr);
}
}
return merged;
};
// 处理空搜索文本
let textTrim = findWordTxt.value.trim() || "";
const searchIntervals = textTrim ? getIntervals(text, [textTrim]) : [];
const nounIntervals = getIntervals(text, nounList.map(n => n.text_span));
// 收集分割点
const points = new Set([0, text.length]);
[...searchIntervals, ...nounIntervals].forEach(({ start, end }) => {
points.add(start);
points.add(end);
});
const sortedPoints = Array.from(points).sort((a, b) => a - b);
const list = [];
for (let i = 0; i < sortedPoints.length - 1; i++) {
const start = sortedPoints[i];
const end = sortedPoints[i + 1];
if (start === end) continue;
const fragment = text.slice(start, end);
const findLight = searchIntervals.some(s => start >= s.start && end <= s.end);
const highLight = nounIntervals.some(n => start >= n.start && end <= n.end);
const classes = [];
if (findLight) classes.push("find-light");
if (highLight) classes.push("high-light");
const textItem = { txt: fragment, class: classes.join(" ") };
if (highLight) {
const nounInterval = nounIntervals.find(n => start >= n.start && end <= n.end);
if (nounInterval) {
textItem.fill = text.slice(nounInterval.start, nounInterval.end);
}
}
list.push(textItem);
// 实体区间结束后添加图标
const iconAdded = new Set();
for (const nounInterval of nounIntervals) {
const key = `${nounInterval.start}-${nounInterval.end}`;
if (end === nounInterval.end && !iconAdded.has(key)) {
list.push({ icon: true, class: "" });
iconAdded.add(key);
break;
}
}
// 搜索区间结束后添加锚点
const pointAdded = new Set();
for (const searchInterval of searchIntervals) {
const key = `${searchInterval.start}-${searchInterval.end}`;
if (end === searchInterval.end && !pointAdded.has(key)) {
findWordMax.value++;
list.push({ point: true, class: "" });
pointAdded.add(key);
break;
}
}
}
return list;
};
// 点击高亮实体
import { useGotoSearchResults } from "@/router/modules/comprehensiveSearch";
const gotoSearchResults = useGotoSearchResults();
const handleHighLight = (node) => {
gotoSearchResults(node.fill, '')
}
</script>
<style lang="scss" scoped>
.is-highlight .high-light {
color: var(--color-primary-100);
cursor: pointer;
}
.find-light {
background-color: #ffff00;
}
.layout-container {
width: 100%;
height: 100%;
background-color: #f7f8f9;
.layout-main {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
.layout-main-center {
width: 1600px;
background-color: white;
padding: 0 60px;
flex: auto;
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
.report-header {
height: 80px;
display: flex;
align-items: center;
border-bottom: solid 1px rgba(234, 236, 238, 1);
margin: 0 20px 10px;
position: relative;
.find-word-box {
position: absolute;
top: -50px;
right: 0px;
width: 430px;
height: 60px;
border: 1px solid rgba(230, 231, 232, 1);
background-color: white;
border-radius: 6px;
display: flex;
align-items: center;
.find-word-input {
flex: auto;
}
.find-word-limit {
border-right: solid 1px rgba(230, 231, 232, 1);
color: #5f656c;
padding-right: 16px;
}
.find-word-icon {
padding: 10px 12px;
margin: 0 2px;
cursor: pointer;
}
}
.report-title {
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 20px;
line-height: 20px;
font-weight: 700;
flex: 1;
}
.btn {
margin-left: 10px;
width: 88px;
height: 32px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
.text {
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
.icon {
width: 16px;
height: 16px;
font-size: 0px;
img {
width: 100%;
height: 100%;
}
}
.icon-gap-4 {
margin-right: 4px;
}
.icon-gap-6 {
margin-right: 6px;
}
}
}
.report-main {
flex: auto;
height: 20px;
padding: 10px 0;
:deep(.el-scrollbar) {
height: 100%;
}
.no-content {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 20px;
font-weight: 400;
}
.content-row {
display: flex;
width: 100%;
padding: 0 20px;
min-height: 100px;
gap: 80px;
.content-en,
.content-ch {
width: 50%;
flex: auto;
padding-bottom: 40px;
box-sizing: border-box;
font-size: 16px;
line-height: 1.8;
color: #3b414b;
font-family: Microsoft YaHei;
text-align: justify;
white-space: pre-wrap;
}
.only-show {
padding-bottom: 10px;
}
}
}
}
}
}
.switch-label {
margin-left: 6px;
}
.switch-label-left {
margin-right: 10px;
}
:deep(.el-scrollbar__bar.is-vertical) {
right: 0px;
width: 4px;
background: transparent;
border-radius: 2px;
& > div {
background: #c5c7c9;
opacity: 1;
}
& > div:hover {
background: #505357;
}
}
</style>
// Bill layout & visual tokens
// 统一用于 bill 板块布局改造,后续可并入全局设计变量体系
// spacing
$bill-space-8: 8px;
$bill-space-12: 12px;
$bill-space-16: 16px;
$bill-space-24: 24px;
$bill-space-30: 30px;
// layout
$bill-column-gap: 16px;
$bill-left-column-width: 1064px;
$bill-right-column-width: 520px;
// typography
$bill-title-font-size: 16px;
$bill-title-font-weight: 600;
$bill-body-font-size: 14px;
$bill-body-font-weight: 400;
// surface
$bill-panel-radius: 8px;
$bill-panel-border-color: #e8ecf3;
$bill-panel-shadow: 0 2px 8px rgba(15, 35, 95, 0.06);
$bill-panel-bg: #ffffff;
// text & color
$bill-text-primary: #1f2a44;
$bill-text-secondary: #5b6b88;
$bill-text-muted: #8b97ad;
$bill-color-accent: #2f66ff;
$bill-color-success: #19be6b;
$bill-color-warning: #ff9f43;
$bill-color-danger: #f56c6c;
# Bill 板块布局统一改造计划
> 目标:统一 `@src/views/bill` 下页面布局与视觉风格,解决“底部 30px 留白不稳定”问题,并沉淀可复用布局组件,便于后续新会话直接接续执行。
## 1. 背景与问题
当前 `bill` 板块存在以下共性问题:
1. 页面布局实现不统一(外层滚动、内层滚动、固定高度混用)。
2. 多数页面为“左右两列 + 多卡片”结构,但容器命名、间距、宽高策略不一致。
3. `30px` 底部留白在不同页面生效机制不同,容易出现:
- 某页不生效
- 增加占位后挤压布局
- Header(Tab/操作按钮)受影响
4. 样式硬编码较多(固定宽高、局部 margin/padding),复用性低。
## 2. 总体改造策略
采用“**分层统一 + 渐进迁移**”策略,避免一次性大改风险:
- **第一层:板块承载层统一**
- 统一滚动、内容宽度、底部安全留白机制。
- **第二层:页面布局骨架统一**
- 抽象“页面壳 + 双列布局 + 卡片容器”组件。
- **第三层:业务页迁移**
- 先迁移典型页,再批量替换。
## 3. 分阶段计划
### 阶段 A:基线确认与止血(进行中)
- 修复当前试错引入的结构问题(例如模板闭合错误、布局挤压)。
- 保证关键页面可用:
- `background`
- `processAnalysis`
- `introdoction`
### 阶段 B:新增通用布局组件(进行中)
#### 完成备注(2026-04-02)
- [x] 第一步已完成并通过验收:完成布局组件目录与基础文件脚手架
- `src/views/bill/components/layout/BillPageShell.vue`
- `src/views/bill/components/layout/BillTwoColumn.vue`
- `src/views/bill/components/layout/BillPanel.vue`
- `src/views/bill/components/layout/index.js`
- [x] 第二步已完成并通过验收:实现 `BillPageShell` 滚动容器与底部 `30px` 安全留白机制
- [x] 第三步已完成并通过验收:实现 `BillTwoColumn` 统一双列布局(1064/520 + gap 16 + 响应式堆叠)
- [x] 第四步已完成并通过验收:实现 `BillPanel` 统一卡片结构与基础样式(header/body/footer + 可配置边框/圆角/阴影)
- [x] 第五步已完成并通过验收:`bill-tokens.scss` 接入 `BillPageShell/BillTwoColumn/BillPanel` 并替换关键样式硬编码
- [x] 第六步已完成并通过验收:试点页 `background/index.vue` 已迁移到统一布局骨架(`BillPageShell + BillTwoColumn`
- [x] 第六步修正项(验收反馈已落实):
- 修正 1:移除 `.right-panel``margin-left/margin-right`,避免右列偏移与原样式不一致
- 修正 2:`BillPageShell` 不再使用内部滚动容器;页面滚动回到外层 `layout-main`,确保 Header 与内容一起滚动
- [x] 第七步已完成并通过验收:`introdoction/index.vue` 迁移到统一布局骨架(`BillPageShell + BillTwoColumn`
- [x] 第七步修正项(验收反馈已落实):
- 修正 1:移除 `.introduction-wrap-right``margin-left/margin-right`,消除右列偏移
- 修正 2:在 `.introduction-wrap-content` 增加 `margin-bottom: 30px`,确保外层滚动时可见底部留白
- [x] 第八步已完成并通过验收:`template/index.vue` 迁移到统一布局骨架(`BillPageShell + BillTwoColumn`
- [x] 第八步注意点(已沉淀):
- 删除 `page-bottom-gap` 占位节点,避免与统一留白策略重复
- 在双列容器 `.content-row` 增加 `margin-bottom: 30px`,适配“外层 `layout-main` 滚动”模式
- 迁移后需检查未使用变量并清理,避免 lint 噪音影响后续页面迁移
- [x] 第九步已完成并通过验收:`deepDig/processOverview/index.vue` 迁移到统一页面壳(`BillPageShell`
- [x] 第九步注意点(已沉淀):
- 保留页面内部横向滚动容器(`.main`),仅替换页面根壳,避免时间轴布局回归风险
- 删除 `page-bottom-gap`,改用页面容器底部留白(`margin-bottom: 30px`)适配外层滚动
- [x] 第十步已完成并通过验收:`deepDig/processAnalysis/index.vue` 迁移到统一页面壳(`BillPageShell`
- [x] 第十步注意点(已沉淀):
- 保持原双列业务结构与内部滚动策略不变,仅替换页面根壳
- 页面底部留白统一为容器 `margin-bottom: 30px`,与外层 `layout-main` 滚动机制一致
新增组件建议放在:`src/views/bill/components/layout/`
#### 计划新增组件索引
1. `src/views/bill/components/layout/BillPageShell.vue`
- 作用:统一页面根容器、内容区、底部留白(30px)机制。
2. `src/views/bill/components/layout/BillTwoColumn.vue`
- 作用:统一左右列布局(默认 1064 / 520)、列间距、响应式策略。
3. `src/views/bill/components/layout/BillPanel.vue`
- 作用:统一卡片容器(header/body/footer)结构、间距、基础样式。
4. `src/views/bill/components/layout/index.js`
- 作用:导出布局组件,便于页面统一引入。
> 可选(后续):
> - `BillPanelGroup.vue`(同列多卡片统一间距)
> - `BillPageSection.vue`(统一页面内 section 标题与辅助动作区)
### 阶段 C:样式 Token 统一(计划)
新增:`src/styles/bill-tokens.scss`(或接入现有变量体系)
- 间距:`8/12/16/24/30`
- 列间距:`16`
- 标题字号/字重
- 卡片边框、阴影、圆角
- 统一颜色 token(主文字、次文字、强调色)
### 阶段 D:页面迁移(计划)
按“先典型、后批量”执行:
1. 试点页(先迁移)
- `background/index.vue`(典型双列 + 卡片 + 多交互)
2. 法案概况组
- `introdoction/index.vue`
- `template/index.vue`
- `background/index.vue`
3. 深度挖掘组
- `deepDig/processOverview/index.vue`
- `deepDig/processAnalysis/index.vue`
- `deepDig/poliContribution/index.vue`
4. 影响分析组
- `influence/industry/index.vue`
- `influence/scientificResearch/index.vue`
- `influence/index.vue`(承载层)
### 阶段 E:收口与回归(计划)
- 清理临时样式补丁(历史试错 `gap/margin-bottom` 等)。
- 回归检查:
1. 所有页面底部留白可见
2. 无横向滚动条
3. Header 不丢失
4. 双列不挤压
## 4. 现有页面索引(改造范围)
### 4.1 板块承载与导航层
- `src/views/bill/billLayout/index.vue`
- `src/views/bill/billLayout/components/BillHeader.vue`
- `src/views/bill/index.vue`
- `src/views/bill/deepDig/index.vue`
- `src/views/bill/influence/index.vue`
### 4.2 法案概况(Bill)
- `src/views/bill/introdoction/index.vue`
- `src/views/bill/background/index.vue`
- `src/views/bill/template/index.vue`
- `src/views/bill/billOriginalText/index.vue`
### 4.3 深度挖掘(DeepDig)
- `src/views/bill/deepDig/processOverview/index.vue`
- `src/views/bill/deepDig/processAnalysis/index.vue`
- `src/views/bill/deepDig/poliContribution/index.vue`
### 4.4 影响分析(Influence)
- `src/views/bill/influence/industry/index.vue`
- `src/views/bill/influence/scientificResearch/index.vue`
- `src/views/bill/influence/ProgressForecast/index.vue`
### 4.5 其他相关页面
- `src/views/bill/relevantCircumstance/index.vue`
- `src/views/bill/versionCompare/index.vue`
- `src/views/bill/billHome/index.vue`
## 5. 迁移原则(执行时必须遵守)
1. 不改变业务逻辑(API、状态、路由行为)
2. 先替换布局骨架,再替换样式细节
3. 每次只迁移少量页面,保证可回滚
4. 对“固定高度 + 内部滚动”页面优先保守改造
5. 若页面是横向 flex 主体,禁止直接插入会改变横向布局的占位节点
## 6. 验收标准
1. `@bill` 目标页面滚动到底统一可见 `30px` 留白
2. 页面核心布局(左右列、卡片)不变形
3. `BillHeader` 的 Tab 与功能按钮稳定显示
4. 不新增横向滚动条
5. 关键路由刷新后视觉一致
## 7. 新会话接续建议
新会话可直接按以下顺序继续:
1. 先读取本文件:
- `src/views/bill/BILL_LAYOUT_UNIFICATION_PLAN.md`
2. 先完成组件脚手架:
- `BillPageShell.vue`
- `BillTwoColumn.vue`
- `BillPanel.vue`
3. 首个迁移页:
- `src/views/bill/background/index.vue`
4. 迁移完成后逐页扩展到 `introdoction/template/processOverview/processAnalysis`
---
最后更新:2026-04-02
<template>
<div class="background-wrap">
<div class="background-wrap-left">
<AnalysisBox class="left-box left-box--background" title="立法背景" :showAllBtn="false" :devTip="true">
<template #header-btn>
<div class="header-btn-box">
<el-button :type="box1Btn1Type" plain @click="handleClickBox1Btn(1)">涉华背景</el-button>
<el-button :type="box1Btn2Type" plain @click="handleClickBox1Btn(2)">全部背景</el-button>
</div>
</template>
<div class="box1-main" v-loading="backgroundLoading">
<div class="box1-main-center">
<div class="box1-main-item" v-for="item in backgroundDisplayList" :key="item.id">
<div class="id">{{ item.displayIndex }}</div>
<div class="title">{{ item.backgroundTitle }}</div>
<div class="share">
<img src="./assets/icons/open.png" alt="打开" />
<BillPageShell class="background-page">
<BillTwoColumn class="background-content" :stack-on-narrow="false">
<template #left>
<div class="background-wrap-left">
<AnalysisBox class="left-box left-box--background" title="立法背景" :showAllBtn="false" :devTip="true">
<template #header-btn>
<div class="header-btn-box">
<el-button :type="box1Btn1Type" plain @click="handleClickBox1Btn(1)">涉华背景</el-button>
<el-button :type="box1Btn2Type" plain @click="handleClickBox1Btn(2)">全部背景</el-button>
</div>
</template>
<div class="box1-main" v-loading="backgroundLoading">
<div class="box1-main-center">
<div class="box1-main-item" v-for="item in backgroundDisplayList" :key="item.id">
<div class="id">{{ item.displayIndex }}</div>
<div class="title">{{ item.backgroundTitle }}</div>
<div class="share">
<img src="./assets/icons/open.png" alt="打开" />
</div>
</div>
</div>
<div class="box1-main-footer">
<div class="info">
{{ totalText }}
</div>
<el-pagination
background
layout="prev, pager, next"
:total="total"
v-model:current-page="currentPage"
@current-change="handleGetBillBackground"
/>
</div>
</div>
</div>
<div class="box1-main-footer">
<div class="info">
{{ totalText }}
</div>
<el-pagination background layout="prev, pager, next" :total="total" v-model:current-page="currentPage"
@current-change="handleGetBillBackground" />
</div>
</div>
</AnalysisBox>
</AnalysisBox>
<AnalysisBox class="left-box left-box--event" title="相关事件" :showAllBtn="false" :devTip="true">
<div class="box2-main">
<div class="box2-main-item" v-for="item in eventDisplayList" :key="item.id" @click="handleClickEvent(item)">
<div class="left">
<img :src="item.imgSrc" @error="handleNewsImgError" alt="" />
</div>
<div class="center">
<CommonPrompt :content="item.sjbt">
<div class="title">{{ item.sjbt }}</div>
</CommonPrompt>
<CommonPrompt :content="item.sjnr">
<div class="content">{{ item.sjnr }}</div>
</CommonPrompt>
<AnalysisBox class="left-box left-box--event" title="相关事件" :showAllBtn="false" :devTip="true">
<div class="box2-main">
<div class="box2-main-item" v-for="item in eventDisplayList" :key="item.id" @click="handleClickEvent(item)">
<div class="left">
<img :src="item.imgSrc" @error="handleNewsImgError" alt="" />
</div>
<div class="center">
<CommonPrompt :content="item.sjbt">
<div class="title">{{ item.sjbt }}</div>
</CommonPrompt>
<CommonPrompt :content="item.sjnr">
<div class="content">{{ item.sjnr }}</div>
</CommonPrompt>
</div>
<div class="right">{{ item.sjsj }}</div>
</div>
</div>
<div class="right">{{ item.sjsj }}</div>
</div>
</div>
</AnalysisBox>
</div>
<AnalysisBox class="right-panel" title="议员相关性分析" :showAllBtn="false" :devTip="true">
<template #header-btn>
<div class="header-btn-box">
<el-button :type="box2Btn1Type" plain @click="handleClickBox2Btn(1)">赞成议员</el-button>
<el-button :type="box2Btn2Type" plain @click="handleClickBox2Btn(2)">反对议员</el-button>
</AnalysisBox>
</div>
</template>
<div class="background-wrap-right-main">
<div class="right-box1">
<div class="right-box1-main-top">
<el-icon class="nav-icon" size="20" :color="prevIconColor" @click="handlePrev">
<CaretLeft />
</el-icon>
<div class="user-list-container">
<div class="user-list-wrapper" :style="userListWrapperStyle">
<div class="user-box" v-for="item in personList" :key="item.id" @click="handleClickUser(item)">
<div class="img-box">
<img :src="item.image" alt="" />
<div class="icon1">
<img :src="item.icon" alt="" />
</div>
<div class="icon2">
<img :src="item.icon1" alt="" />
<template #right>
<AnalysisBox class="right-panel" title="议员相关性分析" :showAllBtn="false" :devTip="true">
<template #header-btn>
<div class="header-btn-box">
<el-button :type="box2Btn1Type" plain @click="handleClickBox2Btn(1)">赞成议员</el-button>
<el-button :type="box2Btn2Type" plain @click="handleClickBox2Btn(2)">反对议员</el-button>
</div>
</template>
<div class="background-wrap-right-main">
<div class="right-box1">
<div class="right-box1-main-top">
<el-icon class="nav-icon" size="20" :color="prevIconColor" @click="handlePrev">
<CaretLeft />
</el-icon>
<div class="user-list-container">
<div class="user-list-wrapper" :style="userListWrapperStyle">
<div class="user-box" v-for="item in personList" :key="item.id" @click="handleClickUser(item)">
<div class="img-box">
<img :src="item.image" alt="" />
<div class="icon1">
<img :src="item.icon" alt="" />
</div>
<div class="icon2">
<img :src="item.icon1" alt="" />
</div>
</div>
<CommonPrompt :content="item.name">
<div class="name">{{ item.name }}</div>
</CommonPrompt>
</div>
</div>
<CommonPrompt :content="item.name">
<div class="name">{{ item.name }}</div>
</CommonPrompt>
</div>
<el-icon class="nav-icon" size="20" :color="nextIconColor" @click="handleNext">
<CaretRight />
</el-icon>
</div>
<div class="right-box1-main-bottom">
<WordCloudMap :data="wordCloudData" :selectedName="selectedIndustryName" shape="circle" @wordClick="handleWordClick" />
</div>
</div>
<el-icon class="nav-icon" size="20" :color="nextIconColor" @click="handleNext">
<CaretRight />
</el-icon>
</div>
<div class="right-box1-main-bottom">
<WordCloudMap :data="wordCloudData" :selectedName="selectedIndustryName" shape="circle"
@wordClick="handleWordClick" />
</div>
</div>
<div class="right-box2">
<div class="right-box2-header">
<div class="title">
<span class="title-active">"{{ selectedIndustryName }}"</span>涉及议员动态 >
</div>
</div>
<div class="right-box2-center">
<div class="user-box" v-for="item in aboutUserList" :key="item.id" @click="handleClickUser(item)">
<div class="user-left">
<div class="img-box">
<img :src="item.img" alt="" />
<div class="right-box2">
<div class="right-box2-header">
<div class="title">
<span class="title-active">"{{ selectedIndustryName }}"</span>涉及议员动态 >
</div>
</div>
<div class="user-right">
<div class="name">{{ item.name }}</div>
<CommonPrompt :content="item.content">
<div class="content">{{ item.content }}</div>
</CommonPrompt>
<div class="right-box2-center">
<div class="user-box" v-for="item in aboutUserList" :key="item.id" @click="handleClickUser(item)">
<div class="user-left">
<div class="img-box">
<img :src="item.img" alt="" />
</div>
</div>
<div class="user-right">
<div class="name">{{ item.name }}</div>
<CommonPrompt :content="item.content">
<div class="content">{{ item.content }}</div>
</CommonPrompt>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
</AnalysisBox>
</template>
</BillTwoColumn>
</BillPageShell>
</template>
<script setup>
......@@ -119,6 +130,7 @@ import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import WordCloudMap from "./WordCloudMap.vue";
import CommonPrompt from "../commonPrompt/index.vue";
import { BillPageShell, BillTwoColumn } from "../components/layout";
import userIcon from "./assets/icons/user-icon.png";
import userIcon1 from "./assets/icons/user-icon1.png";
import userIcon2 from "./assets/icons/user-icon2.png";
......@@ -400,16 +412,31 @@ onMounted(() => {
}
}
.background-wrap {
.background-page {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
}
.background-content {
width: 100%;
height: auto;
box-sizing: border-box;
align-items: flex-start;
:deep(.bill-two-column__left) {
width: 1064px;
}
:deep(.bill-two-column__right) {
width: 520px;
margin-right: 18px;
}
.background-wrap-left {
width: 1064px;
margin-top: 16px;
margin-bottom: 30px;
.left-box {
width: 1064px;
......@@ -600,9 +627,8 @@ onMounted(() => {
}
}
.right-panel {
margin-right: 18px;
margin-left: 16px;
margin-top: 16px;
width: 520px;
max-height: 824px;
......
......@@ -61,7 +61,7 @@
</div>
<div class="right-box-bottom" v-if="showActions">
<div class="btn2" @click="emit('open-analysis', 'forsee')">
<div class="btn2" v-if="showForsee" @click="emit('open-analysis', 'forsee')">
<div class="icon">
<img :src="btnIconForsee" alt="" />
</div>
......@@ -108,6 +108,10 @@ const props = defineProps({
showActions: {
type: Boolean,
default: true
},
showForsee: {
type: Boolean,
default: true
}
});
......@@ -154,9 +158,7 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
}
.header-main {
position: sticky;
top: 0;
z-index: 1000;
position: relative;
width: 100%;
background-color: #fff;
box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.05);
......
......@@ -4,7 +4,8 @@
<div class="layout-main">
<BillHeader :billInfo="billInfoGlobal" :defaultLogo="USALogo" :tabs="mainHeaderBtnList"
:activeTitle="activeTitle" :showTabs="showHeaderTabs" :showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn" @open-analysis="handleAnalysisClick" />
:showForsee="showForseeAction" @tab-click="handleClickMainHeaderBtn"
@open-analysis="handleAnalysisClick" />
<div class="layout-main-center">
<router-view />
......@@ -14,10 +15,10 @@
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import { ref, onMounted, watch, computed } from "vue";
import router from "@/router";
import { useRoute } from "vue-router";
import { getBillInfoGlobal } from "@/api/bill";
import { getBillInfoGlobal, getBillInfo } from "@/api/bill";
import BillHeader from "./components/BillHeader.vue";
const route = useRoute();
......@@ -77,6 +78,25 @@ const activeTitle = ref("法案概况");
const showHeaderTabs = ref(true);
const showHeaderActions = ref(true);
const billBasicInfo = ref({});
const getBillBasicInfoFn = async () => {
const billId = route.query.billId;
if (!billId) return;
try {
const res = await getBillInfo({ id: billId });
billBasicInfo.value = res?.data || {};
} catch (error) {
console.error(error);
billBasicInfo.value = {};
}
};
const showForseeAction = computed(() => {
const stageList = Array.isArray(billBasicInfo.value?.stageList) ? billBasicInfo.value.stageList : [];
const latestStage = stageList.length ? String(stageList[stageList.length - 1] || "").trim() : "";
return !latestStage.includes("完成立法");
});
const getActiveTitleByRoutePath = path => {
if (path.startsWith("/billLayout/deepDig")) return "深度挖掘";
if (path.startsWith("/billLayout/influence")) return "影响分析";
......@@ -132,6 +152,7 @@ const handleAnalysisClick = analysisType => {
onMounted(() => {
getBillInfoGlobalFn();
getBillBasicInfoFn();
// 以当前路由为准,避免 sessionStorage 造成高亮错乱
syncHeaderStateFromRoute();
// 兜底:如果未来出现未知路由且有缓存,再用缓存
......@@ -143,6 +164,14 @@ watch(
() => route.path,
() => {
syncHeaderStateFromRoute();
}
);
watch(
() => route.query.billId,
() => {
getBillInfoGlobalFn();
getBillBasicInfoFn();
},
{ immediate: true }
);
......@@ -160,12 +189,12 @@ watch(
width: 100%;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
.layout-main-center {
// height: calc(100% - 137px);
width: 1600px;
max-width: 100%;
margin: 0 auto;
padding-bottom: 20px;
}
}
}
......
<template>
<div class="bill-page-shell" :class="shellClass" :style="shellStyle">
<div class="bill-page-shell__content" :class="contentClass">
<slot />
</div>
</div>
</template>
<script>
const BILL_DEFAULT_BOTTOM_SAFE_SPACE = 30
export default {
name: 'BillPageShell',
props: {
bottomSafeSpace: {
type: Number,
default: BILL_DEFAULT_BOTTOM_SAFE_SPACE
},
shellClass: {
type: [String, Array, Object],
default: ''
},
contentClass: {
type: [String, Array, Object],
default: ''
}
},
computed: {
shellStyle() {
return {
paddingBottom: `${this.bottomSafeSpace}px`
}
}
}
}
</script>
<style scoped lang="scss">
@import '@/styles/bill-tokens.scss';
.bill-page-shell {
width: 100%;
min-height: 0;
box-sizing: border-box;
}
.bill-page-shell__content {
width: 100%;
color: $bill-text-primary;
}
</style>
<template>
<section class="bill-panel" :class="panelClass" :style="panelStyle">
<header
v-if="$slots.header"
class="bill-panel__header"
:class="headerClass"
:style="sectionStyle"
>
<slot name="header" />
</header>
<div class="bill-panel__body" :class="bodyClass" :style="sectionStyle">
<slot />
</div>
<footer
v-if="$slots.footer"
class="bill-panel__footer"
:class="footerClass"
:style="sectionStyle"
>
<slot name="footer" />
</footer>
</section>
</template>
<script>
const BILL_DEFAULT_PANEL_PADDING = 16
const BILL_DEFAULT_PANEL_RADIUS = 8
export default {
name: 'BillPanel',
props: {
panelClass: {
type: [String, Array, Object],
default: ''
},
headerClass: {
type: [String, Array, Object],
default: ''
},
bodyClass: {
type: [String, Array, Object],
default: ''
},
footerClass: {
type: [String, Array, Object],
default: ''
},
padding: {
type: Number,
default: BILL_DEFAULT_PANEL_PADDING
},
borderRadius: {
type: Number,
default: BILL_DEFAULT_PANEL_RADIUS
},
bordered: {
type: Boolean,
default: true
},
shadow: {
type: Boolean,
default: false
},
background: {
type: String,
default: '#fff'
}
},
computed: {
panelStyle() {
return {
background: this.background,
borderRadius: `${this.borderRadius}px`,
border: this.bordered ? '1px solid #e8ecf3' : 'none',
boxShadow: this.shadow ? '0 2px 8px rgba(15, 35, 95, 0.06)' : 'none'
}
},
sectionStyle() {
return {
padding: `${this.padding}px`
}
}
}
}
</script>
<style scoped lang="scss">
@import '@/styles/bill-tokens.scss';
.bill-panel {
width: 100%;
box-sizing: border-box;
color: $bill-text-primary;
}
.bill-panel__header,
.bill-panel__body,
.bill-panel__footer {
width: 100%;
box-sizing: border-box;
}
.bill-panel__header {
padding-bottom: $bill-space-12;
}
.bill-panel__footer {
padding-top: $bill-space-12;
}
.bill-panel__header:empty,
.bill-panel__footer:empty {
display: none;
}
</style>
<template>
<div class="bill-two-column" :class="wrapperClass" :style="wrapperStyle">
<div class="bill-two-column__left" :style="leftStyle">
<slot name="left" />
</div>
<div class="bill-two-column__right" :style="rightStyle">
<slot name="right" />
</div>
</div>
</template>
<script>
const BILL_DEFAULT_LEFT_WIDTH = 1064
const BILL_DEFAULT_RIGHT_WIDTH = 520
const BILL_DEFAULT_COLUMN_GAP = 16
const BILL_DEFAULT_MIN_STACK_WIDTH = 1660
export default {
name: 'BillTwoColumn',
props: {
leftWidth: {
type: Number,
default: BILL_DEFAULT_LEFT_WIDTH
},
rightWidth: {
type: Number,
default: BILL_DEFAULT_RIGHT_WIDTH
},
gap: {
type: Number,
default: BILL_DEFAULT_COLUMN_GAP
},
minStackWidth: {
type: Number,
default: BILL_DEFAULT_MIN_STACK_WIDTH
},
stackOnNarrow: {
type: Boolean,
default: true
},
wrapperClass: {
type: [String, Array, Object],
default: ''
}
},
computed: {
shouldStack() {
if (!this.stackOnNarrow || typeof window === 'undefined') {
return false
}
return window.innerWidth < this.minStackWidth
},
wrapperStyle() {
if (this.shouldStack) {
return {
display: 'flex',
flexDirection: 'column',
gap: `${this.gap}px`
}
}
return {
display: 'grid',
gridTemplateColumns: `${this.leftWidth}px ${this.rightWidth}px`,
columnGap: `${this.gap}px`,
alignItems: 'start'
}
},
leftStyle() {
if (this.shouldStack) {
return {
width: '100%'
}
}
return {
minWidth: '0'
}
},
rightStyle() {
if (this.shouldStack) {
return {
width: '100%'
}
}
return {
minWidth: '0'
}
}
}
}
</script>
<style scoped lang="scss">
@import '@/styles/bill-tokens.scss';
.bill-two-column {
width: 100%;
min-width: 0;
color: $bill-text-primary;
}
.bill-two-column__left,
.bill-two-column__right {
min-width: 0;
}
</style>
import BillPageShell from './BillPageShell.vue'
import BillTwoColumn from './BillTwoColumn.vue'
import BillPanel from './BillPanel.vue'
export {
BillPageShell,
BillTwoColumn,
BillPanel
}
<template>
<div class="wrap">
<BillPageShell class="wrap">
<div class="left">
<div class="box1">
<!-- <div class="box-header">
......@@ -31,7 +31,7 @@
</div>
</div>
</div> -->
<AnalysisBox title="典型阶段耗时">
<AnalysisBox title="典型阶段耗时分析">
<div class="analysis-ai-wrapper analysis-ai-wrapper--box1">
<div class="box1-main" :class="{ 'box1-main--full': !timeFooterText }">
<div class="box1-main-center" id="chart1"></div>
......@@ -48,7 +48,7 @@
</div>
</div>
<div v-if="!aiPaneVisible.box1" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" />
<TipTab class="analysis-ai-tip" :text="'与历史同类法案的典型阶段耗时对比分析,数据来源:美国国会官网'"/>
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box1')" />
</div>
<div v-if="aiPaneVisible.box1" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box1')">
......@@ -105,7 +105,7 @@
</div>
</div>
<div v-if="!aiPaneVisible.box2" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" />
<TipTab class="analysis-ai-tip" :text="'与历史同类法案的修正案次数对比分析,数据来源:美国国会官网'"/>
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box2')" />
</div>
<div v-if="aiPaneVisible.box2" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box2')">
......@@ -716,7 +716,7 @@
</AnalysisBox>
</div>
</div>
</div>
</BillPageShell>
</template>
<script setup>
......@@ -728,6 +728,7 @@ 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 { BillPageShell } from "../../components/layout";
import icon1 from "./assets/images/icon1.png";
import icon2 from "./assets/images/icon2.png";
......@@ -1160,7 +1161,7 @@ onMounted(async () => {
min: "最小次数",
current: "该法案修正案数量"
};
let chart2 = getBoxPlotChcart(chartData2.value, "", countLabels);
let chart2 = getBoxPlotChcart(chartData2.value, "", countLabels);
setChart(chart2, "chart2");
});
</script>
......@@ -1168,6 +1169,8 @@ onMounted(async () => {
<style lang="scss" scoped>
.wrap {
display: flex;
margin-bottom: 30px;
.box-header {
height: 56px;
......
<template>
<div class="process-overview-wrap">
<BillPageShell class="process-overview-wrap">
<AnalysisBox title="流程概要" :showAllBtn="false">
<div class="main">
<div class="left" :style="{ width: boardWidth + 'px' }">
......@@ -194,7 +194,7 @@
:position="dialogPos"
@close="handleClickDetail(false)"
/>
</div>
</BillPageShell>
</template>
<script setup>
......@@ -202,6 +202,7 @@ import { ref, onMounted, computed, nextTick } from "vue";
import { getBillDyqkSummary } from "@/api/bill";
import CommonPrompt from "../../commonPrompt/index.vue";
import ProcessOverviewDetailDialog from "../../ProcessOverviewDetailDialog.vue";
import { BillPageShell } from "../../components/layout";
const actionList = ref([]);
const isShowDetailDialog = ref(false);
......@@ -499,6 +500,7 @@ const updateRightTop = () => {
width: 1600px;
height: 848px;
margin-top: 16px;
margin-bottom: 30px;
position: relative;
.main {
......@@ -799,20 +801,13 @@ const updateRightTop = () => {
.text {
width: 240px;
max-width: 100%;
color: rgb(59, 65, 75);
font-size: 16px;
line-height: 24px;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: 48px;
}
:deep(.text-ellipsis) {
white-space: normal !important;
}
}
}
......
<template>
<div class="introduction-wrap">
<BillPageShell class="introduction-wrap">
<WarningPane v-if="riskSignal" class="risk-signal-pane-top" :warnningLevel="riskSignal.riskLevel"
:warnningContent="riskSignal.riskContent" />
<div class="introduction-wrap-content">
<div class="introduction-wrap-left">
<BillTwoColumn class="introduction-wrap-content" :stack-on-narrow="false">
<template #left>
<div class="introduction-wrap-left">
<div class="introduction-wrap-left-box1">
<AnalysisBox title="基本信息" :showAllBtn="false">
......@@ -69,10 +70,12 @@
</div>
</AnalysisBox>
</div>
</div>
<div class="introduction-wrap-right">
<AnalysisBox title="提出人" :showAllBtn="false">
<div class="introduction-wrap-right-main">
</div>
</template>
<template #right>
<div class="introduction-wrap-right">
<AnalysisBox title="提出人" :showAllBtn="false">
<div class="introduction-wrap-right-main">
<div class="right-main-box1">
<div class="name-box">
<div class="person-box">
......@@ -139,10 +142,11 @@
</div>
</div>
</div>
</AnalysisBox>
</div>
</div>
</div>
</AnalysisBox>
</div>
</template>
</BillTwoColumn>
</BillPageShell>
</template>
<script setup>
......@@ -151,6 +155,7 @@ import { useRoute, useRouter } from "vue-router";
import WordCloudMap from "./WordCloudMap.vue";
import STimeline from "./STimeline.vue";
import WarningPane from "@/components/base/WarningPane/index.vue";
import { BillPageShell, BillTwoColumn } from "../components/layout";
import { getBillInfo, getBillPerson, getBillEvent, getBillDyqk } from "@/api/bill";
import { getPersonSummaryInfo } from "@/api/common/index";
import defaultAvatar from "../assets/images/default-icon1.png";
......@@ -338,6 +343,14 @@ onMounted(() => {
height: auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
.page-bottom-gap {
height: 30px;
width: 100%;
flex-shrink: 0;
pointer-events: none;
}
.progress-header-btns {
display: flex;
......@@ -432,6 +445,7 @@ onMounted(() => {
.introduction-wrap-content {
display: flex;
margin-bottom: 30px;
}
.introduction-wrap-left {
......@@ -497,7 +511,6 @@ onMounted(() => {
flex-wrap: wrap;
align-items: center;
width: 700px;
min-height: 40px;
gap: 8px;
.right1-item {
......@@ -769,8 +782,6 @@ onMounted(() => {
.introduction-wrap-right {
margin-top: 16px;
margin-left: 16px;
margin-right: 18px;
width: 520px;
height: 845px;
......
<template>
<div class="temp-wrap">
<BillPageShell class="temp-wrap">
<div class="tools-row">
<div class="tools-row-left">
<el-select v-model="curBill" placeholder="请选择版本" @change="handleChangeBill"
......@@ -39,8 +39,9 @@
</div>
</div>
</div>
<div class="content-row">
<div class="side">
<BillTwoColumn class="content-row" :left-width="520" :right-width="1064" :stack-on-narrow="false">
<template #left>
<div class="side">
<div class="side-box side-box-domain">
<AnalysisBox title="涉及领域" width="520px" height="415px" v-loading="domainLoading">
<div class="chart-ai-wrap">
......@@ -93,8 +94,10 @@
</div>
</AnalysisBox>
</div>
</div>
<div class="terms">
</div>
</template>
<template #right>
<div class="terms">
<div class="terms-switch">
<span class="terms-switch-label">高亮实体</span>
<el-switch
......@@ -103,6 +106,10 @@
active-text="开"
inactive-text="关"
/>
<span v-if="termsEntityLoading" class="terms-entity-loading">
<el-icon class="is-loading"><Loading /></el-icon>
实体识别中(剩余 {{ termsEntityPendingCount }} 条)
</span>
<span class="terms-switch-divider"></span>
<span class="terms-switch-label">显示原文</span>
<el-switch
......@@ -112,7 +119,7 @@
inactive-text="关"
/>
</div>
<AnalysisBox title="主要条款" :showAllBtn="false" v-loading="termsLoading">
<AnalysisBox title="主要条款" :showAllBtn="false" height="auto" v-loading="termsLoading">
<div class="left-main">
<div class="left-main-item" v-for="(term, index) in displayTermsList" :key="getTermKey(term, index)">
<div class="term-body">
......@@ -120,11 +127,23 @@
<div class="term-main">
<div class="term-row term-row-cn">
<div class="term-no-cn">{{ term.tkxh }}条.</div>
<div class="term-content-cn" v-html="getTermContentHtml(term, 'cn')"></div>
<div class="term-content-cn">
<IntelligentEntityText
:text="term?.fynr || ''"
:entities="termsHighlight ? getTermEntities(term, 'cn') : []"
@on-entity-click="e => gotoSearchResults(e.text_span, '')"
/>
</div>
</div>
<div class="term-row term-row-en" v-if="termsShowOriginal">
<div class="term-no-en">Sec.{{ term.tkxh }}</div>
<div class="term-content-en" v-html="getTermContentHtml(term, 'en')"></div>
<div class="term-content-en">
<IntelligentEntityText
:text="term?.ywnr || ''"
:entities="termsHighlight ? getTermEntities(term, 'en') : []"
@on-entity-click="e => gotoSearchResults(e.text_span, '')"
/>
</div>
</div>
</div>
</div>
......@@ -144,16 +163,17 @@
</div>
</div>
</AnalysisBox>
</div>
</div>
</div>
</div>
</template>
</BillTwoColumn>
</BillPageShell>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed, watch } from "vue";
import { useRoute, onBeforeRouteLeave } from "vue-router";
import * as echarts from "echarts";
import { Search } from "@element-plus/icons-vue";
import { Search, Loading } from "@element-plus/icons-vue";
import getPieChart from "./utils/piechart";
import { getBillContentId, getBillContentTk, getBillContentXzfs, getBillHyly } from "@/api/bill";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
......@@ -162,9 +182,12 @@ 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";
import { useGotoSearchResults } from "@/router/modules/comprehensiveSearch";
import IntelligentEntityText from "@/components/base/texts/IntelligentEntityText.vue";
import { BillPageShell, BillTwoColumn } from "../components/layout";
const route = useRoute();
const gotoSearchResults = useGotoSearchResults();
const pageAbortController = new AbortController();
const isRequestCanceled = error => {
......@@ -186,6 +209,8 @@ const stopCurrentPageRequests = () => {
hylyRequestToken.value += 1;
entityRequestToken.value += 1;
termsLoading.value = false;
termsEntityLoading.value = false;
termsEntityPendingCount.value = 0;
limitLoading.value = false;
domainLoading.value = false;
aiPaneLoading.value = { domain: false, limit: false };
......@@ -214,16 +239,8 @@ const termsShowOriginal = ref(true);
const entityRequestToken = ref(0);
const termEntityCache = ref(new Map());
const escapeHtml = value => {
const str = String(value ?? "");
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
};
const termsEntityLoading = ref(false);
const termsEntityPendingCount = ref(0);
const normalizeEntities = entities => {
const list = Array.isArray(entities) ? entities : [];
......@@ -237,69 +254,23 @@ const normalizeEntities = entities => {
.filter(item => item.text_span);
};
const getEntityRanges = (text, entities) => {
const ranges = [];
const rawText = String(text ?? "");
if (!rawText) return ranges;
const list = normalizeEntities(entities).sort((a, b) => b.text_span.length - a.text_span.length);
for (const ent of list) {
let startIndex = 0;
while (startIndex < rawText.length) {
const idx = rawText.indexOf(ent.text_span, startIndex);
if (idx === -1) break;
ranges.push({ start: idx, end: idx + ent.text_span.length, ent });
startIndex = idx + ent.text_span.length;
}
}
ranges.sort((a, b) => a.start - b.start || b.end - a.end);
const merged = [];
let lastEnd = 0;
for (const r of ranges) {
if (r.start < lastEnd) continue;
merged.push(r);
lastEnd = r.end;
}
return merged;
};
const buildHighlightedHtml = (text, entities, enableHighlight) => {
const rawText = String(text ?? "");
if (!rawText) return "";
const safeText = escapeHtml(rawText).replace(/\n/g, "<br />");
if (!enableHighlight) return safeText;
const ranges = getEntityRanges(rawText, entities);
if (!ranges.length) return safeText;
let html = "";
let cursor = 0;
for (const r of ranges) {
if (cursor < r.start) {
html += escapeHtml(rawText.slice(cursor, r.start));
}
const spanText = rawText.slice(r.start, r.end);
const type = escapeHtml(r.ent?.type ?? "");
html += `<span class="term-entity" data-entity-type="${type}">${escapeHtml(spanText)}</span>`;
cursor = r.end;
}
if (cursor < rawText.length) {
html += escapeHtml(rawText.slice(cursor));
}
return html.replace(/\n/g, "<br />");
};
const getTermEntityKey = (term, lang) => {
const baseKey = getTermKey(term, -1);
return `${baseKey}__${lang}`;
};
const ensureEntitiesForTerms = async terms => {
if (!termsHighlight.value) return;
if (!termsHighlight.value) {
termsEntityLoading.value = false;
termsEntityPendingCount.value = 0;
return;
}
const list = Array.isArray(terms) ? terms : [];
if (!list.length) return;
if (!list.length) {
termsEntityLoading.value = false;
termsEntityPendingCount.value = 0;
return;
}
const currentToken = ++entityRequestToken.value;
......@@ -314,30 +285,40 @@ const ensureEntitiesForTerms = async terms => {
tasks.push({ key: enKey, text: term.ywnr });
}
}
if (!tasks.length) return;
if (!tasks.length) {
termsEntityLoading.value = false;
termsEntityPendingCount.value = 0;
return;
}
try {
const results = await Promise.all(
tasks.map(async item => {
const res = await extractTextEntity(item.text, { signal: getPageSignal() });
const entities = normalizeEntities(res?.result ?? res?.data?.result ?? res?.data ?? res);
return { key: item.key, entities };
})
);
if (currentToken !== entityRequestToken.value) return;
for (const r of results) {
termEntityCache.value.set(r.key, r.entities);
termsEntityLoading.value = true;
termsEntityPendingCount.value = tasks.length;
const fetchOne = async item => {
try {
const res = await extractTextEntity(item.text, { signal: getPageSignal() });
if (currentToken !== entityRequestToken.value) return;
const entities = normalizeEntities(res?.result ?? res?.data?.result ?? res?.data ?? res);
const nextCache = new Map(termEntityCache.value);
nextCache.set(item.key, entities);
termEntityCache.value = nextCache;
} catch (error) {
if (currentToken !== entityRequestToken.value) return;
} finally {
if (currentToken !== entityRequestToken.value) return;
termsEntityPendingCount.value = Math.max(0, termsEntityPendingCount.value - 1);
if (termsEntityPendingCount.value === 0) {
termsEntityLoading.value = false;
}
}
} catch (error) {
if (currentToken !== entityRequestToken.value) return;
}
};
await Promise.all(tasks.map(item => fetchOne(item)));
};
const getTermContentHtml = (term, lang) => {
const raw = lang === "en" ? term?.ywnr : term?.fynr;
const getTermEntities = (term, lang) => {
const key = getTermEntityKey(term, lang);
const entities = termEntityCache.value.get(key) || [];
return buildHighlightedHtml(raw, entities, termsHighlight.value);
return termEntityCache.value.get(key) || [];
};
const tkRequestToken = ref(0);
......@@ -347,10 +328,6 @@ const hylyRequestToken = ref(0);
const mainTermsList = ref([]);
const domainFooterText = ref("");
const limitFooterText = ref("");
const btnActiveIndex = ref(1);
const handleSelectBtn = index => {
btnActiveIndex.value = index;
};
const getTermKey = (term, index) => {
return term?.ywid ?? term?.id ?? term?.tkxh ?? index;
......@@ -576,7 +553,16 @@ const handleChangeCheckbox = val => {
handleGetBillHyly();
};
/** 法案布局主区域可滚动容器(.layout-main)滚回顶部 */
const scrollBillLayoutMainToTop = () => {
const mainEl = document.querySelector(".layout-main");
if (mainEl) {
mainEl.scrollTo({ top: 0, behavior: "smooth" });
}
};
const handleCurrentChange = val => {
scrollBillLayoutMainToTop();
currentPage.value = val;
handleGetBillContentTk(checkedValue.value ? "Y" : "N");
};
......@@ -925,8 +911,11 @@ onBeforeUnmount(() => {
.content-row {
display: flex;
align-items: flex-start;
margin-bottom: 30px;
}
.box-header {
display: flex;
position: relative;
......@@ -988,9 +977,7 @@ onBeforeUnmount(() => {
.terms {
margin-top: 16px;
margin-left: 16px;
width: 1064px;
height: 1232px;
position: relative;
.terms-switch {
......@@ -1007,6 +994,14 @@ onBeforeUnmount(() => {
color: var(--text-primary-65-color);
}
.terms-entity-loading {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--text-primary-65-color);
}
.terms-switch-divider {
display: inline-block;
width: 1px;
......@@ -1022,17 +1017,21 @@ onBeforeUnmount(() => {
}
}
:deep(.analysis-box-wrapper) {
height: auto;
}
:deep(.wrapper-main) {
display: flex;
flex-direction: column;
height: calc(100% - 45px);
gap:16px;
height: auto;
min-height: 0;
overflow: visible;
gap: 16px;
}
.left-main {
flex: 1;
min-height: 0;
overflow-y: auto;
flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 12px;
......@@ -1112,12 +1111,12 @@ onBeforeUnmount(() => {
line-height: 24px;
color: var(--text-primary-80-color);
:deep(.term-entity) {
display: inline;
padding: 0 2px;
border-radius: 4px;
background: rgba(255, 213, 79, 0.35);
box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.25);
:deep(.entity-link) {
color: var(--color-primary-100);
}
:deep(.entity-link:hover) {
cursor: pointer;
}
}
......@@ -1128,12 +1127,12 @@ onBeforeUnmount(() => {
line-height: 24px;
color: var(--text-primary-65-color);
:deep(.term-entity) {
display: inline;
padding: 0 2px;
border-radius: 4px;
background: rgba(255, 213, 79, 0.28);
box-shadow: inset 0 0 0 1px rgba(255, 193, 7, 0.2);
:deep(.entity-link) {
color: var(--color-primary-100);
}
:deep(.entity-link:hover) {
cursor: pointer;
}
}
......@@ -1196,6 +1195,7 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
justify-content: center;
padding-left: 20px;
}
.overview-tip-action {
......
......@@ -165,7 +165,7 @@ import { useRoute } from "vue-router";
import router from '@/router'
import { getPostOrgList, getPostMemberList } from '@/api/bill/billHome'
import { search } from '@/api/comprehensiveSearch'
import { search, getStatusList } from '@/api/comprehensiveSearch'
import { ElMessage } from 'element-plus'
import getDateRange from '@/utils/getDateRange'
......@@ -345,10 +345,13 @@ const activeTagList = computed(() => {
)
}
if (selectedStatus.value && selectedStatus.value !== '全部阶段') {
const statusName = statusList.value.filter(item => {
return item.id === selectedStatus.value
})[0].name
arr.push(
{
tag: '所处阶段',
name: selectedStatus.value
name: statusName
}
)
}
......@@ -711,6 +714,31 @@ const statusList = ref([
},
])
const handleGetStatusList = async () => {
try {
const res = await getStatusList()
console.log('获取立法阶段列表', res);
if (res.code === 200) {
const arr = res.data.map(item => {
return {
name: item.name,
id: item.number
}
})
statusList.value = [
{
name: '全部阶段',
id: '全部阶段'
},
...arr
]
}
} catch (error) {
}
}
const selectedStatus = ref('全部阶段')
const statusPlaceHolder = ref('请选择立法阶段')
const handleSelectStauts = value => {
......@@ -737,6 +765,7 @@ const handleClear = () => {
// 确定
const handleConfirm = () => {
currentPage.value = 1
fetchTableData()
}
......@@ -785,6 +814,9 @@ const fetchTableData = async () => {
// isSelectedAll.value = false
// selectedMap.value.clear()
// 调用接口获取数据...
loading.value = true
const params = {
page: currentPage.value,
size: pageSize.value,
......@@ -843,12 +875,10 @@ const fetchTableData = async () => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
loading.value = false
} catch (error) {
loading.value = false
}
// tableData.value = res.data
// total.value = res.total
......@@ -867,6 +897,12 @@ const allData = ref([])
// 获取筛选条件下全部表格数据
const fetchAllData = async () => {
let statusParam = null
if (selectedStatus.value !== '全部阶段') {
statusParam = statusList.value.filter(item => {
return item.name === selectedStatus.value
})[0].id
}
const params = {
page: 1,
size: 9999,
......@@ -879,7 +915,7 @@ const fetchAllData = async () => {
originChamber: selectedCongress.value === '全部议院' ? null : selectedCongress.value,
originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value,
sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value,
status: selectedStatus.value === '通过' ? 1 : 0,
status: selectedStatus.value === '全部阶段' ? null : selectedStatus.value,
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
......@@ -1038,8 +1074,10 @@ const initParam = () => {
customTime.value = JSON.parse(route.query.selectedDate)
}
isInvolveCn.value = route.query.isInvolveCn ? true : false
if (route.query.selectedStatus) {
selectedStatus.value = route.query.selectedStatus
if (route.query.selectedStatus && route.query.selectedStatus !== '全部阶段') {
selectedStatus.value = statusList.value.filter(item => {
return item.name === route.query.selectedStatus
})[0].id
} else {
selectedStatus.value = '全部阶段'
}
......@@ -1054,7 +1092,7 @@ const initParam = () => {
} else {
const savedQuery = JSON.parse(sessionStorage.getItem('routeQuery') || '{}');
selectedArea.value = savedQuery.domains ? savedQuery.domains : '全部领域'
if (Array.isArray(JSON.parse(savedQuery.selectedDate)) && JSON.parse(savedQuery.selectedDate).length) {
if (savedQuery.selectedDate && Array.isArray(JSON.parse(savedQuery.selectedDate)) && JSON.parse(savedQuery.selectedDate).length) {
selectedDate.value = '自定义'
customTime.value = JSON.parse(savedQuery.selectedDate)
}
......@@ -1123,9 +1161,11 @@ const handleExport = () => {
onMounted(async () => {
handleGetOrgList()
handleGetMemberList()
initParam()
await handleGetStatusList()
// 初始化
await fetchTableData()
initParam()
fetchTableData()
})
onBeforeUnmount(() => {
......
......@@ -124,13 +124,13 @@
</div>
</div>
</div>
<div class="data-main-box-main-content" v-loading="loading" element-loading-text="全部数据加载中,请稍候...">
<div class="data-main-box-main-content" v-loading="loading" element-loading-text="数据加载中,请稍候...">
<el-table ref="tableRef" :data="tableData" row-key="id" @selection-change="handleSelectionChange"
@select="handleSelect" @select-all="handleSelectAll" style="width: 100%" :row-style="{ height: '52px' }">
<el-table-column type="selection" width="40" />
<el-table-column label="政令名称" width="720">
<template #default="scope">
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{ scope.row.title
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{ scope.row.originalTitle
}}</span>
</template>
</el-table-column>
......@@ -139,7 +139,7 @@
</el-table-column>
<el-table-column label="发布机构">
<template #default="scope">
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.organizationName
<span class="person-item text-compact" @click="handleOrgClick(scope.row)">{{ scope.row.organizationName
}}</span>
</template>
</el-table-column>
......@@ -675,6 +675,7 @@ const handleClear = () => {
// 确定
const handleConfirm = () => {
currentPage.value = 1
fetchTableData()
}
......@@ -718,6 +719,7 @@ const selectedCount = computed(() => selectedMap.value.size)
const fetchTableData = async () => {
// isSelectedAll.value = false
// selectedMap.value.clear()
loading.value = true
// 调用接口获取数据...
const params = {
page: currentPage.value,
......@@ -772,8 +774,9 @@ const fetchTableData = async () => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
loading.value = false
} catch (error) {
loading.value = false
}
// tableData.value = res.data
// total.value = res.total
......@@ -1003,17 +1006,18 @@ const handleClickToDetail = (curDecree) => {
window.open(route.href, "_blank");
};
// 跳转人物详情
const handlePerClick = item => {
window.sessionStorage.setItem("curTabName", item.sponsorPersonName);
const route = router.resolve({
path: "/characterPage",
query: {
type: 2,
personId: item.personId
}
});
window.open(route.href, "_blank");
// 跳转机构详情
const handleOrgClick = item => {
console.log('item', item);
window.sessionStorage.setItem("curTabName", item.organizationName);
const route = router.resolve({
path: "/institution",
query: {
id: item.organizationId
}
});
window.open(route.href, "_blank");
};
// 导出
......@@ -1216,6 +1220,8 @@ onMounted(async () => {
.header-left-item2 {
color: var(--color-primary-100);
display: flex;
gap: 8px;
}
......
......@@ -15,18 +15,18 @@
placeholder-name="请输入智库报告名称" />
<div class="check-box">
<div class="check-box-left text-tip-1">
{{ '是否涉华:' }}
{{ "是否涉华:" }}
</div>
<div class="check-box-right">
<el-checkbox v-model="isInvolveCn" class="involve-checkbox">
{{ '只看涉华智库报告' }}
{{ "只看涉华智库报告" }}
</el-checkbox>
</div>
</div>
</div>
<div class="header-footer">
<div class="header-footer-left">
<ActiveTag v-for="tag, index in activeTagList" :key="index" :tagName="tag.name"
<ActiveTag v-for="(tag, index) in activeTagList" :key="index" :tagName="tag.name"
@close="handleCloseCurTag(tag, index)" />
</div>
<div class="header-footer-right">
......@@ -38,7 +38,7 @@
<div class="chart-main-box" v-if="isShowChart">
<div class="info-box">
<div class="switch-box" @click="handleSwitchChartData">
<img src="@/views/dataLibrary/assets/icons/chart-active.svg" alt="">
<img src="@/views/dataLibrary/assets/icons/chart-active.svg" alt="" />
</div>
<div class="num-box text-title-3-bold">
{{ `共 ${totalNum} 条数据` }}
......@@ -52,7 +52,6 @@
@change="handleChangeTime">
<el-option v-for="item in timeList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
</ChartHeader>
</div>
......@@ -66,14 +65,13 @@
<RaderChart v-if="activeChart === '雷达图'" :radarChartData="curChartData" />
</template>
</ChartContainer>
</div>
</div>
</div>
<div class="data-main-box" v-else>
<div class="data-main-box-header">
<div class="switch-box" @click="handleSwitchChartData">
<img src="@/views/dataLibrary/assets/icons/data-active.svg" alt="">
<img src="@/views/dataLibrary/assets/icons/data-active.svg" alt="" />
</div>
<div class="num-box text-title-3-bold">
{{ `共 ${totalNum} 条数据` }}
......@@ -86,17 +84,19 @@
<el-checkbox v-model="isSelectedAll" label="全选" @change="handleSelectAllChange" size="large" />
</div>
<div class="header-left-item2 text-tip-1">{{ `已选择${selectedCount}项` }}</div>
<div class="header-left-item2 text-tip-1 cancel" @click="handleClearAll" v-show="selectedCount">{{ '取消' }}
<div class="header-left-item3 text-tip-1" v-if="isShowAllDataMaxLengthTip">{{ `(当前最大选择不能超过1万条!)` }}</div>
<div class="header-left-item2 text-tip-1 cancel" @click="handleClearAll" v-show="selectedCount">
{{ "取消" }}
<div class="header-left-item3 text-tip-1" v-if="isShowAllDataMaxLengthTip">
{{ `(当前最大选择不能超过1万条!)` }}
</div>
</div>
</div>
<div class="header-right">
<div class="header-right-item item1">
<div class="icon">
<img src="../assets/icons/download.svg" alt="">
<img src="../assets/icons/download.svg" alt="" />
</div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
<div class="text text-tip-1" @click="handleExport">{{ "导出" }}</div>
</div>
<div class="header-right-item2 item2">
<el-select v-model="curOperation" placeholder="批量操作" style="width: 120px">
......@@ -121,7 +121,8 @@
<el-table-column type="selection" width="40" />
<el-table-column label="报告名称" width="720">
<template #default="scope">
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{ scope.row.originalTitle
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{
scope.row.originalTitle
}}</span>
</template>
</el-table-column>
......@@ -130,16 +131,16 @@
</el-table-column>
<el-table-column label="发布智库">
<template #default="scope">
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.organizationName
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{
scope.row.organizationName
}}</span>
</template>
</el-table-column>
<el-table-column label="涉及领域" width="350" class-name="date-column">
<template #default="scope">
<div class="tag-box">
<AreaTag v-for="tag, index in scope.row.domains" :key="index" :tagName="tag" />
<AreaTag v-for="(tag, index) in scope.row.domains" :key="index" :tagName="tag" />
</div>
</template>
</el-table-column>
<el-table-column label="类型" width="100">
......@@ -157,64 +158,64 @@
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import ChartContainer from '../components/ChartContainer/index.vue'
import ChartHeader from '../components/ChartHeader/index.vue'
import ActiveTag from '../components/ActiveTag/index.vue'
import HeaderBtnBox from '../components/HeaderBtnBox/index.vue'
import LineChart from '../components/LineChart/index.vue'
import PieChart from '../components/PieChart/index.vue'
import BarChart from '../components/BarChart/index.vue'
import RaderChart from '../components/RadarChart/idnex.vue'
import SelectBox from '../components/SelectBox/index.vue'
import InputBox from '../components/InputBox/index.vue'
import { ref, computed, watch, onMounted, nextTick } from "vue";
import ChartContainer from "../components/ChartContainer/index.vue";
import ChartHeader from "../components/ChartHeader/index.vue";
import ActiveTag from "../components/ActiveTag/index.vue";
import HeaderBtnBox from "../components/HeaderBtnBox/index.vue";
import LineChart from "../components/LineChart/index.vue";
import PieChart from "../components/PieChart/index.vue";
import BarChart from "../components/BarChart/index.vue";
import RaderChart from "../components/RadarChart/idnex.vue";
import SelectBox from "../components/SelectBox/index.vue";
import InputBox from "../components/InputBox/index.vue";
import { useRoute } from "vue-router";
import router from '@/router'
import router from "@/router";
import { search, getThinkTankList } from '@/api/comprehensiveSearch'
import { ElMessage } from 'element-plus'
import getDateRange from '@/utils/getDateRange'
import { search, getThinkTankList } from "@/api/comprehensiveSearch";
import { ElMessage } from "element-plus";
import getDateRange from "@/utils/getDateRange";
import { getDepartmentList } from "@/api/decree/home";
const route = useRoute();
// 图表/数据
const isShowChart = ref(false)
const isShowChart = ref(false);
// 点击切换数据/图表
const handleSwitchChartData = () => {
isShowChart.value = !isShowChart.value
isShowChart.value = !isShowChart.value;
if (isShowChart.value) {
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
return item.name === curDemension.value;
})[0];
setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
if (curDemension.value === '发布时间') {
if (selectedTime.value === '按月度统计') {
curChartData.value = curDemensionItem.data
} else if (selectedTime.value === '按季度统计') {
curChartData.value = curDemensionItem.quatarData
activeChart.value = curDemensionItem.chartTypeList[0];
if (curDemension.value === "发布时间") {
if (selectedTime.value === "按月度统计") {
curChartData.value = curDemensionItem.data;
} else if (selectedTime.value === "按季度统计") {
curChartData.value = curDemensionItem.quatarData;
} else {
curChartData.value = curDemensionItem.yearData
curChartData.value = curDemensionItem.yearData;
}
} else {
curChartData.value = curDemensionItem.data
curChartData.value = curDemensionItem.data;
}
})
});
}
}
};
// 总计数据
const totalNum = ref(0)
const totalNum = ref(0);
// 统计维度列表
const staticsDemensionList = ref([
{
name: '发布时间',
name: "发布时间",
active: true,
chartTypeList: ['折线图', '柱状图'],
chartTitle: '美国智库报告数量随时间变化趋势',
chartTypeList: ["折线图", "柱状图"],
chartTitle: "美国智库报告数量随时间变化趋势",
data: {
dataX: [],
dataY: []
......@@ -229,207 +230,192 @@ const staticsDemensionList = ref([
}
},
{
name: '科技领域',
name: "科技领域",
active: false,
chartTypeList: ['饼状图'],
chartTitle: '美国智库报告领域分布',
chartTypeList: ["饼状图"],
chartTitle: "美国智库报告领域分布",
data: []
},
{
name: '发布智库',
name: "发布智库",
active: false,
chartTypeList: ['饼状图'],
chartTitle: '美国智库报告发布智库分布',
chartTypeList: ["饼状图"],
chartTitle: "美国智库报告发布智库分布",
data: []
}
])
]);
// 当前维度下的图表列表
const curChartTypeList = computed(() => {
let arr = staticsDemensionList.value.filter(item => item.active)
return arr[0].chartTypeList
})
let arr = staticsDemensionList.value.filter(item => item.active);
return arr[0].chartTypeList;
});
// 当前图表标题
const curChartTitle = computed(() => {
let arr = staticsDemensionList.value.filter(item => item.active)
return arr[0].chartTitle
})
let arr = staticsDemensionList.value.filter(item => item.active);
return arr[0].chartTitle;
});
// 当前维度
const curDemension = ref('发布时间')
const curDemension = ref("发布时间");
// 点击维度item
const handleClickDemensionItem = (val) => {
activeChart.value = ''
const handleClickDemensionItem = val => {
activeChart.value = "";
staticsDemensionList.value.forEach(item => {
item.active = false
})
val.active = true
curDemension.value = val.name
item.active = false;
});
val.active = true;
curDemension.value = val.name;
setTimeout(() => {
activeChart.value = val.chartTypeList[0]
if (curDemension.value === '发布时间' && selectedTime.value === '按年度统计') {
curChartData.value = val.yearData
} else if (curDemension.value === '发布时间' && selectedTime.value === '按季度统计') {
curChartData.value = val.quatarData
activeChart.value = val.chartTypeList[0];
if (curDemension.value === "发布时间" && selectedTime.value === "按年度统计") {
curChartData.value = val.yearData;
} else if (curDemension.value === "发布时间" && selectedTime.value === "按季度统计") {
curChartData.value = val.quatarData;
} else {
curChartData.value = val.data
curChartData.value = val.data;
}
})
}
});
};
// 时间图表 当前选择时间
const selectedTime = ref('按月统计')
const selectedTime = ref("按月统计");
// 时间图表-时间列表
const timeList = ref([
{
label: '按年度统计',
value: '按年度统计'
label: "按年度统计",
value: "按年度统计"
},
{
label: '按季度统计',
value: '按季度统计'
label: "按季度统计",
value: "按季度统计"
},
{
label: '按月度统计',
value: '按月度统计'
},
])
label: "按月度统计",
value: "按月度统计"
}
]);
const handleChangeTime = value => {
let curChart = activeChart.value
activeChart.value = ''
if (value === '按月度统计') {
let curChart = activeChart.value;
activeChart.value = "";
if (value === "按月度统计") {
setTimeout(() => {
activeChart.value = curChart
curChartData.value = staticsDemensionList.value[0].data
})
} else if (value === '按季度统计') {
activeChart.value = curChart;
curChartData.value = staticsDemensionList.value[0].data;
});
} else if (value === "按季度统计") {
setTimeout(() => {
activeChart.value = curChart
curChartData.value = staticsDemensionList.value[0].quatarData
})
activeChart.value = curChart;
curChartData.value = staticsDemensionList.value[0].quatarData;
});
} else {
setTimeout(() => {
activeChart.value = curChart
curChartData.value = staticsDemensionList.value[0].yearData
})
activeChart.value = curChart;
curChartData.value = staticsDemensionList.value[0].yearData;
});
}
}
};
// 激活的标签列表
const activeTagList = computed(() => {
const arr = []
if (selectedArea.value && selectedArea.value !== '全部领域') {
arr.push(
{
tag: '科技领域',
name: selectedArea.value
}
)
const arr = [];
if (selectedArea.value && selectedArea.value !== "全部领域") {
arr.push({
tag: "科技领域",
name: selectedArea.value
});
}
if (selectedDate.value === '自定义') {
arr.push(
{
tag: '发布时间',
name: customTime.value.join('至')
}
)
if (selectedDate.value === "自定义") {
arr.push({
tag: "发布时间",
name: customTime.value.join("至")
});
}
if (selectedThinkTank.value && selectedThinkTank.value !== '全部智库') {
arr.push(
{
tag: '发布智库',
name: selectedThinkTank.value
}
)
if (selectedThinkTank.value && selectedThinkTank.value !== "全部智库") {
arr.push({
tag: "发布智库",
name: selectedThinkTank.value
});
}
if (reportName.value) {
arr.push(
{
tag: '报告名称',
name: reportName.value
}
)
arr.push({
tag: "报告名称",
name: reportName.value
});
}
if (isInvolveCn.value) {
const involveStr = '涉华'
arr.push(
{
tag: '是否涉华',
name: involveStr
}
)
const involveStr = "涉华";
arr.push({
tag: "是否涉华",
name: involveStr
});
}
return arr
})
return arr;
});
// 关闭当前标签
const handleCloseCurTag = (tag, index) => {
switch (tag.tag) {
case '科技领域':
selectedArea.value = '全部领域'
break
case '发布时间':
selectedDate.value = ''
customTime.value = []
break
case '发布智库':
selectedThinkTank.value = '全部智库'
break
case '报告名称':
reportName.value = ''
break
case '是否涉华':
isInvolveCn.value = false
break
case "科技领域":
selectedArea.value = "全部领域";
break;
case "发布时间":
selectedDate.value = "";
customTime.value = [];
break;
case "发布智库":
selectedThinkTank.value = "全部智库";
break;
case "报告名称":
reportName.value = "";
break;
case "是否涉华":
isInvolveCn.value = false;
break;
}
};
}
const activeChart = ref('') // 当前激活的图表
const activeChart = ref(""); // 当前激活的图表
// 切换当前图表
const handleSwitchActiveChart = val => {
activeChart.value = val.name
}
activeChart.value = val.name;
};
// 雷达图数据
const radarChartData = ref({
title: [
{
name: '航空航天',
name: "航空航天",
max: 10
},
{
name: '先进制造',
name: "先进制造",
max: 10
},
{
name: '量子科技',
name: "量子科技",
max: 10
},
{
name: '人工智能',
name: "人工智能",
max: 10
},
{
name: '新材料',
name: "新材料",
max: 10
},
{
name: '集成电路',
name: "集成电路",
max: 10
},
}
],
data: [
{
name: "337调查",
value: [10, 5, 2, 8, 5, 7
]
value: [10, 5, 2, 8, 5, 7]
},
{
name: "232调查",
......@@ -440,151 +426,148 @@ const radarChartData = ref({
value: [5, 8, 2, 9, 1, 5]
}
]
}
)
});
// 数据- 是否全选
const isSelectedAll = ref(false)
const isSelectedAll = ref(false);
// 批量操作-当前操作
const curOperation = ref('')
const curOperation = ref("");
const operationList = ref([
{
name: 'aaa',
id: 'aaa'
name: "aaa",
id: "aaa"
},
{
name: 'bbb',
id: 'bbb'
name: "bbb",
id: "bbb"
},
{
name: 'ccc',
id: 'ccc'
},
])
name: "ccc",
id: "ccc"
}
]);
// 科技领域
const areaPlaceHolder = ref('请选择领域')
const selectedArea = ref('全部领域')
const areaPlaceHolder = ref("请选择领域");
const selectedArea = ref("全部领域");
const areaList = ref([
{
name: '全部领域',
id: '全部领域'
name: "全部领域",
id: "全部领域"
},
{
name: '人工智能',
id: '人工智能'
name: "人工智能",
id: "人工智能"
},
{
name: '生物科技',
id: '生物科技'
name: "生物科技",
id: "生物科技"
},
{
name: '新一代通信网络',
id: '新一代通信网络'
name: "新一代通信网络",
id: "新一代通信网络"
},
{
name: '量子科技',
id: '量子科技'
name: "量子科技",
id: "量子科技"
},
{
name: '新能源',
id: '新能源'
name: "新能源",
id: "新能源"
},
{
name: '集成电路',
id: '集成电路'
name: "集成电路",
id: "集成电路"
},
{
name: '海洋',
id: '海洋'
name: "海洋",
id: "海洋"
},
{
name: '先进制造',
id: '先进制造'
name: "先进制造",
id: "先进制造"
},
{
name: '新材料',
id: '新材料'
name: "新材料",
id: "新材料"
},
{
name: '航空航天',
id: '航空航天'
name: "航空航天",
id: "航空航天"
},
{
name: '太空',
id: '太空'
name: "太空",
id: "太空"
},
{
name: '深海',
id: '深海'
name: "深海",
id: "深海"
},
{
name: '极地',
id: '极地'
name: "极地",
id: "极地"
},
{
name: '核',
id: '核'
name: "核",
id: "核"
},
{
name: '其他',
id: '其他'
},
])
const handleSelectArea = (value) => {
selectedArea.value = value
}
name: "其他",
id: "其他"
}
]);
const handleSelectArea = value => {
selectedArea.value = value;
};
// 提出时间
const DatePlaceHolder = ref('请选择时间')
const selectedDate = ref('')
const DatePlaceHolder = ref("请选择时间");
const selectedDate = ref("");
const dateList = ref([
{
name: '自定义',
id: '自定义'
name: "自定义",
id: "自定义"
},
{
name: '近一年',
id: '近一年'
name: "近一年",
id: "近一年"
},
{
name: '近半年',
id: '近半年'
name: "近半年",
id: "近半年"
},
{
name: '近三月',
id: '近三月'
name: "近三月",
id: "近三月"
},
{
name: '近一月',
id: '近一月'
name: "近一月",
id: "近一月"
}
])
const customTime = ref([]) // 自定义时间
]);
const customTime = ref([]); // 自定义时间
const handleCustomDate = value => {
customTime.value = value
}
const handleSelectDate = (value) => {
selectedDate.value = value
if (selectedDate.value !== '自定义') {
customTime.value = getDateRange(selectedDate.value)
customTime.value = value;
};
const handleSelectDate = value => {
selectedDate.value = value;
if (selectedDate.value !== "自定义") {
customTime.value = getDateRange(selectedDate.value);
}
}
};
// 发布智库
const thinkTankList = ref([
{
name: '全部智库',
id: '全部智库'
},
])
const selectedThinkTank = ref('全部智库')
const thinkTankPlaceHolder = ref('请选择发布智库')
name: "全部智库",
id: "全部智库"
}
]);
const selectedThinkTank = ref("全部智库");
const thinkTankPlaceHolder = ref("请选择发布智库");
const handleSelectThinkTank = value => {
selectedThinkTank.value = value
}
selectedThinkTank.value = value;
};
const handleGetThinkTankList = async () => {
try {
......@@ -594,65 +577,60 @@ const handleGetThinkTankList = async () => {
return {
name: item,
id: item
}
})
thinkTankList.value = [...thinkTankList.value, ...arr]
} catch (error) {
}
}
};
});
thinkTankList.value = [...thinkTankList.value, ...arr];
} catch (error) { }
};
// 作者
const authorList = ref([
{
name: '全部作者',
id: '全部作者'
},
])
const selectedAuthor = ref('暂无作者信息')
const authorPlaceHolder = ref('请选择发布智库')
name: "全部作者",
id: "全部作者"
}
]);
const selectedAuthor = ref("暂无作者信息");
const authorPlaceHolder = ref("请选择发布智库");
const handleSelectAuthor = value => {
selectedAuthor.value = value
}
selectedAuthor.value = value;
};
// 报告名称列表
const reportName = ref('')
const handleInputReportName = (value) => {
reportName.value = value
}
const reportName = ref("");
const handleInputReportName = value => {
reportName.value = value;
};
// 是否涉华
const isInvolveCn = ref(false)
const isInvolveCn = ref(false);
// 清空条件
const handleClear = () => {
selectedArea.value = '全部领域'
selectedDate.value = ''
customTime.value = []
selectedThinkTank.value = '全部智库'
reportName.value = ''
isInvolveCn.value = false
ElMessage.success('已清空条件!')
}
selectedArea.value = "全部领域";
selectedDate.value = "";
customTime.value = [];
selectedThinkTank.value = "全部智库";
reportName.value = "";
isInvolveCn.value = false;
ElMessage.success("已清空条件!");
};
// 确定
const handleConfirm = () => {
fetchTableData()
}
fetchTableData();
};
// 展开全部 / 收起
const isFolderAll = ref(false)
const isFolderAll = ref(false);
const handleSwitchFolderAll = () => {
isFolderAll.value = !isFolderAll.value
}
isFolderAll.value = !isFolderAll.value;
};
const tableRef = ref(null)
const tableRef = ref(null);
// 表格数据
const tableData = ref([
])
const tableData = ref([]);
const releaseTimeList = ref([
{
......@@ -666,17 +644,17 @@ const releaseTimeList = ref([
]);
const isSort = ref(true); // true 倒序 false 升序
const handlePxChange = val => {
fetchTableData()
fetchTableData();
};
const currentPage = ref(1);
const pageSize = ref(10)
const pageSize = ref(10);
// 存储选中的数据(跨页)[citation:3][citation:8]
const selectedMap = ref(new Map()) // 使用 Map 存储,key 为唯一 id
const selectedMap = ref(new Map()); // 使用 Map 存储,key 为唯一 id
// 计算已选中的条数
const selectedCount = computed(() => selectedMap.value.size)
const selectedCount = computed(() => selectedMap.value.size);
// 获取当前页表格数据(示例)
const fetchTableData = async () => {
......@@ -686,326 +664,318 @@ const fetchTableData = async () => {
const params = {
page: currentPage.value,
size: pageSize.value,
keyword: '',
type: 4, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单 6= 人物 7= 机构 8=新闻 9= 社媒
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value], // 领域
proposedDateStart: customTime.value[0] ? customTime.value[0] : null, // 开始日期
proposedDateEnd: customTime.value[1] ? customTime.value[1] : null, // 结束日期
organizationName: selectedThinkTank.value === '全部智库' ? null : selectedThinkTank.value, // 智库名称
reportNameZh: reportName.value? reportName.value : null, // 报告名称
isInvolveCn: isInvolveCn.value ? 'Y' : null, // 是否涉华
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
// keyword: '',
type: 4, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单 6= 人物 7= 机构 8=新闻 9= 社媒
domains: selectedArea.value === "全部领域" ? null : [selectedArea.value], // 领域
proposedDateStart: customTime.value[0] ? customTime.value[0] : null, // 开始日期
proposedDateEnd: customTime.value[1] ? customTime.value[1] : null, // 结束日期
organizationName: selectedThinkTank.value === "全部智库" ? null : selectedThinkTank.value, // 智库名称
reportNameZh: reportName.value ? reportName.value : null, // 报告名称
isInvolveCn: isInvolveCn.value ? "Y" : null, // 是否涉华
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
};
try {
const res = await search(params)
console.log('搜索结果', res);
const res = await search(params);
console.log("搜索结果", res);
if (res.code === 200 && res.data) {
tableData.value = res.data.records
totalNum.value = res.data.total
tableData.value = res.data.records;
totalNum.value = res.data.total;
staticsDemensionList.value[0].data = {
dataX: Object.keys(res.data.aggregationsDate),
dataY: Object.values(res.data.aggregationsDate).map(value => Number(value))
}
};
staticsDemensionList.value[0].quatarData = {
dataX: Object.keys(res.data.aggregationsQuarter),
dataY: Object.values(res.data.aggregationsQuarter).map(value => Number(value))
}
};
staticsDemensionList.value[0].yearData = {
dataX: Object.keys(res.data.aggregationsYear),
dataY: Object.values(res.data.aggregationsYear).map(value => Number(value))
}
};
staticsDemensionList.value[1].data = Object.entries(res.data.aggregationsDomain).map(([key, value]) => ({
name: key,
value: Number(value)
}))
}));
staticsDemensionList.value[2].data = Object.entries(res.data.aggregationsorganizationName).map(([key, value]) => ({
name: key,
value: Number(value)
}))
}));
}
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
return item.name === curDemension.value;
})[0];
activeChart.value = ''
activeChart.value = "";
setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
} catch (error) {
}
activeChart.value = curDemensionItem.chartTypeList[0];
curChartData.value = curDemensionItem.data;
});
} catch (error) { }
// tableData.value = res.data
// total.value = res.total
// 数据加载后,回显已选中的行
// 数据加载后,回显已选中的行
nextTick(() => {
tableData.value.forEach(row => {
if (selectedMap.value.has(row.id)) {
tableRef.value?.toggleRowSelection(row, true)
tableRef.value?.toggleRowSelection(row, true);
}
})
})
}
const allData = ref([])
});
});
};
const allData = ref([]);
// 获取筛选条件下全部表格数据
const fetchAllData = async () => {
const params = {
page: 1,
size: 9999,
keyword: '',
type: 4, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单【制裁记录】 6= 人物 7= 机构 8=新闻 9= 社媒
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value],
// keyword: '',
type: 4, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单【制裁记录】 6= 人物 7= 机构 8=新闻 9= 社媒
domains: selectedArea.value === "全部领域" ? null : [selectedArea.value],
proposedDateStart: customTime.value[0],
proposedDateEnd: customTime.value[1],
organizationName: selectedThinkTank.value === '全部智库' ? null : selectedThinkTank.value,
reportNameZh: reportName.value? reportName.value : null,
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
organizationName: selectedThinkTank.value === "全部智库" ? null : selectedThinkTank.value,
reportNameZh: reportName.value ? reportName.value : null,
isInvolveCn: isInvolveCn.value ? "Y" : "N",
sort: isSort.value ? 0 : 1
}
};
try {
const res = await search(params)
console.log('搜索结果', res);
const res = await search(params);
console.log("搜索结果", res);
if (res.code === 200 && res.data) {
allData.value = res.data.records
allData.value = res.data.records;
}
} catch (error) {
ElMessage.error('加载全部数据出错!')
ElMessage.error("加载全部数据出错!");
}
}
};
// 单选事件
// 单选事件
const handleSelect = (selection, row) => {
if (selection.some(item => item.id === row.id)) {
// 选中:添加到 Map
selectedMap.value.set(row.id, row)
selectedMap.value.set(row.id, row);
} else {
// 取消选中:从 Map 移除
selectedMap.value.delete(row.id)
selectedMap.value.delete(row.id);
}
}
};
// 表格自带 当前页 全选/全不选事件
const handleSelectAll = (selection) => {
// 表格自带 当前页 全选/全不选事件
const handleSelectAll = selection => {
if (selection.length > 0) {
// 全选:将当前页所有数据添加到 Map
tableData.value.forEach(row => {
if (!selectedMap.value.has(row.id)) {
selectedMap.value.set(row.id, row)
selectedMap.value.set(row.id, row);
}
})
});
} else {
// 全不选:从 Map 中移除当前页的所有数据
tableData.value.forEach(row => {
selectedMap.value.delete(row.id)
})
selectedMap.value.delete(row.id);
});
}
}
};
// 处理选择变化(用于统计)
const handleSelectionChange = (val) => {
const handleSelectionChange = val => {
// 这里可以做一些额外的处理,但主要统计使用 selectedMap
console.log('当前页选中数量:', val.length)
}
console.log("当前页选中数量:", val.length);
};
// 全选当前页按钮
const handleSelectAllPage = () => {
if (tableData.value.length === 0) return
if (tableData.value.length === 0) return;
// 检查当前页是否已全选
const currentPageSelected = tableData.value.every(row =>
selectedMap.value.has(row.id)
)
const currentPageSelected = tableData.value.every(row => selectedMap.value.has(row.id));
if (currentPageSelected) {
// 已全选,则不动当前页的全选
tableData.value.forEach(row => {
tableRef.value.toggleRowSelection(row, false)
tableRef.value.toggleRowSelection(row, false);
// selectedMap.value.delete(row.id)
})
});
} else {
// 未全选,则全选当前页
tableData.value.forEach(row => {
tableRef.value.toggleRowSelection(row, true)
tableRef.value.toggleRowSelection(row, true);
if (!selectedMap.value.has(row.id)) {
selectedMap.value.set(row.id, row)
selectedMap.value.set(row.id, row);
}
})
});
}
}
};
// 全选最大1万条提示
const isShowAllDataMaxLengthTip = ref(false)
const loading = ref(false) // 加载数据loading
const isShowAllDataMaxLengthTip = ref(false);
const loading = ref(false); // 加载数据loading
// 处理 全选(全部数据)
const handleSelectAllChange = async () => {
if (isSelectedAll.value) {
if (totalNum.value > 10000) {
isShowAllDataMaxLengthTip.value = true
isShowAllDataMaxLengthTip.value = true;
}
loading.value = true
loading.value = true;
await fetchAllData()
handleSelectAllPage()
await fetchAllData();
handleSelectAllPage();
allData.value.forEach(row => {
if (!selectedMap.value.has(row.id)) {
selectedMap.value.set(row.id, row)
selectedMap.value.set(row.id, row);
}
})
loading.value = false
});
loading.value = false;
} else {
handleClearAll()
handleClearAll();
}
}
};
// 清空所有选择
const handleClearAll = () => {
isSelectedAll.value = false
selectedMap.value.clear()
tableRef.value?.clearSelection()
}
isSelectedAll.value = false;
selectedMap.value.clear();
tableRef.value?.clearSelection();
};
// 翻页
const handleCurrentChange = async (val) => {
currentPage.value = val
await fetchTableData()
const handleCurrentChange = async val => {
currentPage.value = val;
await fetchTableData();
if (isSelectedAll.value) {
handleSelectAllPage()
handleSelectAllPage();
}
}
};
// 监听数据变化,回显选中状态 [citation:4][citation:8]
watch(tableData, () => {
nextTick(() => {
tableData.value.forEach(row => {
if (selectedMap.value.has(row.id)) {
tableRef.value?.toggleRowSelection(row, true)
tableRef.value?.toggleRowSelection(row, true);
}
})
})
})
});
});
});
// 当前图表数据
const curChartData = ref(null)
const curChartData = ref(null);
// 下载当前图表数据
const handleDownloadCurChartData = () => {
const jsonStr = JSON.stringify(curChartData.value, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const blob = new Blob([jsonStr], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.download = 'chartData.json';
link.download = "chartData.json";
link.click();
URL.revokeObjectURL(url);
}
};
// 跳转到当前页 初始化筛选条件
const initParam = () => {
const hasQuery = Object.keys(route.query).length > 0;
if (hasQuery) {
selectedArea.value = route.query.domains ? route.query.domains : '全部领域'
if (route.query.selectedDate && Array.isArray(JSON.parse(route.query.selectedDate)) && JSON.parse(route.query.selectedDate).length) {
selectedDate.value = '自定义'
customTime.value = JSON.parse(route.query.selectedDate)
selectedArea.value = route.query.domains ? route.query.domains : "全部领域";
if (
route.query.selectedDate &&
Array.isArray(JSON.parse(route.query.selectedDate)) &&
JSON.parse(route.query.selectedDate).length
) {
selectedDate.value = "自定义";
customTime.value = JSON.parse(route.query.selectedDate);
}
selectedThinkTank.value = route.query.orgnizationName ? route.query.orgnizationName : '全部智库'
selectedThinkTank.value = route.query.orgnizationName ? route.query.orgnizationName : "全部智库";
isInvolveCn.value = route.query.isInvolveCn ? true : false
isInvolveCn.value = route.query.isInvolveCn ? true : false;
reportName.value = route.query.reportName ? route.query.reportName : ''
reportName.value = route.query.reportName ? route.query.reportName : "";
const query = route.query;
if (Object.keys(query).length > 0) {
sessionStorage.setItem('thinktankRouteQuery', JSON.stringify(query));
sessionStorage.setItem("thinktankRouteQuery", JSON.stringify(query));
}
} else {
const savedQuery = JSON.parse(sessionStorage.getItem('thinktankRouteQuery') || '{}');
selectedArea.value = savedQuery.domains ? savedQuery.domains : '全部领域'
if (savedQuery.selectedDate && Array.isArray(JSON.parse(savedQuery.selectedDate)) && JSON.parse(savedQuery.selectedDate).length) {
selectedDate.value = '自定义'
customTime.value = JSON.parse(savedQuery.selectedDate)
const savedQuery = JSON.parse(sessionStorage.getItem("thinktankRouteQuery") || "{}");
selectedArea.value = savedQuery.domains ? savedQuery.domains : "全部领域";
if (
savedQuery.selectedDate &&
Array.isArray(JSON.parse(savedQuery.selectedDate)) &&
JSON.parse(savedQuery.selectedDate).length
) {
selectedDate.value = "自定义";
customTime.value = JSON.parse(savedQuery.selectedDate);
}
isInvolveCn.value = savedQuery.isInvolveCn ? true : false
isInvolveCn.value = savedQuery.isInvolveCn ? true : false;
reportName.value = savedQuery.reportName ? savedQuery.reportName : ''
reportName.value = savedQuery.reportName ? savedQuery.reportName : "";
}
}
};
// 跳转政令详情
const handleClickToDetail = (curDecree) => {
console.log('curDecree', curDecree);
const handleClickToDetail = curReport => {
console.log("curReport", curReport);
window.sessionStorage.setItem("billId", curDecree.id);
window.sessionStorage.setItem("curTabName", curDecree.title);
window.sessionStorage.setItem("billId", curReport.id);
window.sessionStorage.setItem("curTabName", curReport.title);
const route = router.resolve({
path: "/decreeLayout",
query: {
billId: curDecree.id
name: "ReportDetail",
params: {
id: curReport.id
}
});
window.open(route.href, "_blank");
};
// 跳转人物详情
// 跳转智库详情
const handlePerClick = item => {
window.sessionStorage.setItem("curTabName", item.sponsorPersonName);
const route = router.resolve({
path: "/characterPage",
query: {
type: 2,
personId: item.personId
const curRoute = router.resolve({
name: "ThinkTankDetail",
params: {
id: item.organizationId,
name: item.organizationName
}
});
window.open(route.href, "_blank");
window.open(curRoute.href, "_blank");
};
// 导出
const handleExport = () => {
if (!selectedCount.value) {
ElMessage.warning('请选择至少一项数据!')
return
ElMessage.warning("请选择至少一项数据!");
return;
}
console.log(selectedMap.value);
const arr = Array.from(selectedMap.value);
const jsonStr = JSON.stringify(arr, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const blob = new Blob([jsonStr], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.download = 'export.json';
link.download = "export.json";
link.click();
URL.revokeObjectURL(url);
};
onMounted(async () => {
handleGetThinkTankList()
initParam()
handleGetThinkTankList();
initParam();
// 初始化
await fetchTableData()
})
await fetchTableData();
});
</script>
<style lang="scss" scoped>
......@@ -1055,7 +1025,6 @@ onMounted(async () => {
}
}
.header-footer {
margin-top: 16px;
display: flex;
......@@ -1176,7 +1145,6 @@ onMounted(async () => {
.header-left-item2 {
color: var(--color-primary-100);
}
.header-left-item3 {
......@@ -1256,7 +1224,6 @@ onMounted(async () => {
flex-wrap: wrap;
gap: 8px;
width: 340px;
}
}
......@@ -1285,12 +1252,9 @@ onMounted(async () => {
}
}
:deep(.el-table__header-wrapper) {
// background-color: #f5f7fa;
height: 48px;
}
:deep(.el-table__header th) {
......
......@@ -42,7 +42,12 @@
<img :src="item.imgUrl || DefaultIcon2" alt="" />
</div>
<div class="item-right one-line-ellipsis" @click="handleToInstitution(item)">{{ item.orgName }}</div>
<div class="item-total" @click="handleToDataLibrary(item)">{{ item.totalOrderNum }}</div>
<el-popover content="跳转至数据资源库" placement="top">
<template #reference>
<div class="item-total" @click="handleToDataLibrary(item)">{{ item.totalOrderNum }}</div>
</template>
</el-popover>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
......
......@@ -26,7 +26,7 @@
</div>
<div class="layout-main-center">
<BaseDecreeOriginal :report-data="reportData" @download="handleDownload" />
<newOriginal ref="refNewOriginal" @download="handleDownload"></newOriginal>
</div>
</div>
</div>
......@@ -38,7 +38,7 @@ import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { getDecreeSummary } from "@/api/decree/introduction";
import { getDecreeReport } from "@/api/decree/introduction";
import BaseDecreeOriginal from "@/components/base/DecreeOriginal/index.vue";
import newOriginal from "@/components/base/DecreeOriginal/newOriginal.vue";
const route = useRoute();
let pdfUrl = "";
......@@ -98,26 +98,16 @@ const handleGetSummary = async () => {
};
// 获取报告原文 - 修改为获取分段数组
const reportData = ref([]);
const refNewOriginal = ref(null);
const handleGetReport = async () => {
try {
const res = await getDecreeReport({id: route.query.id});
console.log("报告原文", res);
if (res.code === 200 && res.data) {
if (res.code === 200) {
pdfUrl = res.data.pdfUrl;
const originData = [];
let num = Math.max(res.data.content.length, res.data.contentEn.length)
for (let i = 0; i < num; i++) {
let obj = {
content: res.data.content[i] || "",
contentEn: res.data.contentEn[i] || "",
num: i + 1,
}
originData.push(obj);
}
reportData.value = JSON.parse(JSON.stringify(originData));
refNewOriginal.value.setOriginalData({...res.data});
}
} catch (error) { }
} catch (error) {}
};
onMounted(() => {
......
......@@ -16,14 +16,12 @@
<div class="timeline-content-card">
<div class="item-head">
<div :class="`item-tag tag-${item.sortcode}`">{{ item.sortcode }}</div>
<div class="item-name">{{ item.searchname }}</div>
<div class="item-name one-line-ellipsis">{{ item.searchname }}</div>
<div class="item-state">
<span class="dot"></span> {{ item.casestatus }}
</div>
</div>
<div class="card-body">
{{ item.content }}
</div>
<div class="card-body">{{ item.content }}</div>
<div class="card-footer">
<div class="footer-left-tags">
<AreaTag v-for="(name, num) in item.searchArea" :key="num" :tagName="name"></AreaTag>
......@@ -129,7 +127,7 @@ const onNavigateToDetail = item => {
.timeline-content-card {
width: 20px;
flex: auto;
padding: 2px 16px 0;
padding: 4px 16px 0;
margin-bottom: 30px;
&:hover .item-head .item-name {
......
......@@ -32,39 +32,26 @@
<div class="wrapper-main">
<div class="left">
<!-- 科技领域 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "科技领域" }}</div>
</div>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedAreaList" @change="handleCheckedAreasChange">
<el-checkbox class="filter-checkbox" label="全部领域"> 全部领域 </el-checkbox>
<el-checkbox v-for="area in surveyAreaList" :key="area.id" :label="area.id" class="filter-checkbox">
{{ area.name }}
</el-checkbox>
</el-checkbox-group>
<div class="check-box">
<div class="check-head">
<div class="head-name">{{ "科技领域" }}</div>
</div>
<el-checkbox-group class="check-list" v-model="checkedAreaList" @change="handleCheckedAreasChange">
<el-checkbox class="check-item" v-for="item in surveyAreaList" :key="item.id" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 发布时间 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "发布时间" }}</div>
<div class="check-box">
<div class="check-head">
<div class="head-name">{{ "发布时间" }}</div>
</div>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedSurveyYears" @change="handleCheckedYearsChange">
<el-checkbox class="filter-checkbox" label="全部时间"> 全部时间 </el-checkbox>
<el-checkbox v-for="year in displayedYearList" :key="year.id" :label="year.id" class="filter-checkbox">
<el-checkbox-group class="check-list" v-model="checkedYearList" @change="handleCheckedYearsChange">
<el-checkbox class="check-item" v-for="year in surveyYearList" :key="year.id" :label="year.id">
{{ year.name }}
</el-checkbox>
<div v-if="surveyYearList.length > 6" class="expand-btn" @click="isYearExpanded = !isYearExpanded">
{{ isYearExpanded ? "收起" : "更早" }}
<el-icon>
<ArrowUp v-if="isYearExpanded" />
<ArrowDown v-else />
</el-icon>
</div>
</el-checkbox-group>
</div>
</div>
......@@ -98,8 +85,8 @@
</template>
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import { Search, ArrowDown, ArrowUp } from "@element-plus/icons-vue";
import { ref, onMounted, watch } from "vue";
import { Search } from "@element-plus/icons-vue";
import { getSearchAllArea, getSearchAllYear, getSurveyList } from "@/api/marketAccessRestrictions";
import SurveyHistory from "@/views/marketAccessRestrictions/com/SurveyHistory.vue"
......@@ -116,42 +103,49 @@ const handleSwithSort = () => {
// 科技领域过滤
const surveyAreaList = ref([]);
const checkedAreaList = ref([]);
const checkAllAreas = ref(true);
const isIndeterminateAreas = ref(false);
const handleCheckAllAreasChange = val => {
checkedAreaList.value = val ? surveyAreaList.value.map(a => a.id) : [];
isIndeterminateAreas.value = false;
const checkedAreaList = ref(['']);
const handleGetSearchAllArea = async () => {
try {
const res = await getSearchAllArea({ sortCode: "232" });
if (res.code === 200) {
surveyAreaList.value = res.data.map(item => ({ name: item.AREANAME, id: item.AREACODE }));
}
} catch (error) {}
surveyAreaList.value.unshift({ name: "全部领域", id: "" });
};
const handleCheckedAreasChange = value => {
const checkedCount = value.length;
checkAllAreas.value = checkedCount === surveyAreaList.value.length;
isIndeterminateAreas.value = checkedCount > 0 && checkedCount < surveyAreaList.value.length;
const handleCheckedAreasChange = event => {
if (event.length && event[event.length-1] !== "") {
checkedAreaList.value = event.filter(item => item !== "");
} else {
checkedAreaList.value = [""];
}
currentPage.value = 1;
handleFetchSurveyList();
};
// 发布时间过滤
const surveyYearList = ref([]);
const checkedSurveyYears = ref([]);
const checkAllYears = ref(true);
const isIndeterminateYears = ref(false);
const isYearExpanded = ref(false);
const displayedYearList = computed(() => {
if (isYearExpanded.value) return surveyYearList.value;
return surveyYearList.value.slice(0, 6);
});
const handleCheckAllYearsChange = val => {
checkedSurveyYears.value = val ? surveyYearList.value.map(y => y.id) : [];
isIndeterminateYears.value = false;
const checkedYearList = ref(['']);
const handleGetSearchAllYear = async () => {
try {
const res = await getSearchAllYear({ sortCode: "232" });
if (res.code === 200) {
let allYear = res.data.sort((a, b) => (b-a));
let beforeYear = allYear.slice(6).join(',');
surveyYearList.value = allYear.slice(0, 6).map(item => ({ name: item + "年", id: item }));
if (beforeYear) surveyYearList.value.push({ name: "更早", id: beforeYear });
}
} catch (error) {}
surveyYearList.value.unshift({ name: "全部时间", id: "" });
};
const handleCheckedYearsChange = value => {
const checkedCount = value.length;
checkAllYears.value = checkedCount === surveyYearList.value.length;
isIndeterminateYears.value = checkedCount > 0 && checkedCount < surveyYearList.value.length;
const handleCheckedYearsChange = event => {
if (event.length && event[event.length-1] !== "") {
checkedYearList.value = event.filter(item => item !== "");
} else {
checkedYearList.value = [""];
}
currentPage.value = 1;
handleFetchSurveyList();
};
// 数据列表
......@@ -169,8 +163,8 @@ const handleFetchSurveyList = async () => {
currentPage: currentPage.value - 1,
pageSize: pageSize.value,
sortCode: "232",
publishYear: checkAllYears.value ? "" : checkedSurveyYears.value.toString(),
Area: checkAllAreas.value ? "" : checkedAreaList.value.toString(),
publishYear: checkedYearList.value.join(',') || null,
Area: checkedAreaList.value.join(',') || null,
caseStatus: filterStage.value,
keywords: searchText.value,
sortField: "date",
......@@ -199,39 +193,12 @@ const handleSearch = () => {
};
// 监听过滤条件
watch([checkedSurveyYears, checkedAreaList, isSort, filterStage, filterParty, filterReason], () => {
watch([isSort, filterStage, filterParty, filterReason], () => {
if (isInitializing.value) return;
currentPage.value = 1;
handleFetchSurveyList();
});
const handleGetSearchAllArea = async () => {
try {
const res = await getSearchAllArea({ sortCode: "232" });
if (res.code === 200 && res.data) {
surveyAreaList.value = res.data.map(item => ({
name: item.AREANAME,
id: item.AREACODE
}));
handleCheckAllAreasChange(true);
}
} catch (error) {}
};
const handleGetSearchAllYear = async () => {
try {
const res = await getSearchAllYear({ sortCode: "232" });
if (res.code === 200 && res.data) {
const sortedYears = res.data.sort((a, b) => b - a);
surveyYearList.value = sortedYears.map(item => ({
name: item + "年",
id: item
}));
handleCheckAllYearsChange(true);
}
} catch (error) {}
};
onMounted(async () => {
await Promise.all([handleGetSearchAllArea(), handleGetSearchAllYear()]);
isInitializing.value = false;
......@@ -306,58 +273,50 @@ onMounted(async () => {
.left {
width: 360px;
min-height: 560px;
min-height: 300px;
height: fit-content;
padding-bottom: 20px;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: #fff;
.left-box {
margin-top: 17px;
.left-header {
display: flex;
align-items: center;
.icon {
.check-box {
margin-top: 18px;
.check-head {
position: relative;
margin-bottom: 12px;
&::before {
content: "";
position: absolute;
top: 0px;
left: 0px;
width: 8px;
height: 16px;
background: var(--color-main-active);
height: 100%;
background: var(--color-primary-100);
border-radius: 0 2px 2px 0;
}
.title {
margin-left: 17px;
color: var(--color-main-active);
.head-name {
margin-left: 25px;
color: var(--color-primary-100);
font-size: 16px;
font-weight: 700;
line-height: 16px;
font-family: Source Han Sans CN;
font-weight: bold;
}
}
}
.checkbox-group {
padding: 10px 0 0 25px;
.filter-checkbox {
width: 130px;
margin-bottom: 8px;
height: 32px;
:deep(.el-checkbox__label) {
font-size: 16px;
color: #5f656c;
}
}
.expand-btn {
color: var(--color-main-active);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
margin-top: 4px;
.el-icon {
margin-left: 4px;
.check-list {
padding: 0 10px 0 25px;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 0 12px;
.check-item {
width: 100%;
height: 32px;
:deep(.el-checkbox__label) {
font-family: Source Han Sans CN;
font-size: 16px;
color: var(--text-primary-65-color);
}
}
}
}
......
......@@ -21,39 +21,26 @@
<div class="wrapper-main">
<div class="left">
<!-- 科技领域 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "科技领域" }}</div>
</div>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedAreaList" @change="handleCheckedAreasChange">
<el-checkbox class="filter-checkbox" label="全部领域"> 全部领域 </el-checkbox>
<el-checkbox v-for="area in surveyAreaList" :key="area.id" :label="area.id" class="filter-checkbox">
{{ area.name }}
</el-checkbox>
</el-checkbox-group>
<div class="check-box">
<div class="check-head">
<div class="head-name">{{ "科技领域" }}</div>
</div>
<el-checkbox-group class="check-list" v-model="checkedAreaList" @change="handleCheckedAreasChange">
<el-checkbox class="check-item" v-for="item in surveyAreaList" :key="item.id" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 发布时间 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "发布时间" }}</div>
<div class="check-box">
<div class="check-head">
<div class="head-name">{{ "发布时间" }}</div>
</div>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedSurveyYears" @change="handleCheckedYearsChange">
<el-checkbox class="filter-checkbox" label="全部时间"> 全部时间 </el-checkbox>
<el-checkbox v-for="year in displayedYearList" :key="year.id" :label="year.id" class="filter-checkbox">
<el-checkbox-group class="check-list" v-model="checkedYearList" @change="handleCheckedYearsChange">
<el-checkbox class="check-item" v-for="year in surveyYearList" :key="year.id" :label="year.id">
{{ year.name }}
</el-checkbox>
<div v-if="surveyYearList.length > 6" class="expand-btn" @click="isYearExpanded = !isYearExpanded">
{{ isYearExpanded ? "收起" : "更早" }}
<el-icon>
<ArrowUp v-if="isYearExpanded" />
<ArrowDown v-else />
</el-icon>
</div>
</el-checkbox-group>
</div>
</div>
......@@ -78,8 +65,8 @@
</template>
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import { Search, ArrowDown, ArrowUp } from "@element-plus/icons-vue";
import { ref, onMounted, watch } from "vue";
import { Search } from "@element-plus/icons-vue";
import { getSearchAllArea, getSearchAllYear, getSurveyList } from "@/api/marketAccessRestrictions";
import SurveyHistory from "@/views/marketAccessRestrictions/com/SurveyHistory.vue"
......@@ -88,46 +75,51 @@ const handleSwithSort = () => {
isSort.value = !isSort.value;
};
const surveyYearList = ref([]);
const checkedSurveyYears = ref([]);
const isYearExpanded = ref(false);
const displayedYearList = computed(() => {
if (isYearExpanded.value) {
return surveyYearList.value;
}
return surveyYearList.value.slice(0, 6);
});
const checkAllYears = ref(false);
const isIndeterminateYears = ref(false);
const handleCheckAllYearsChange = (val) => {
checkedSurveyYears.value = val ? surveyYearList.value.map(y => y.id) : [];
isIndeterminateYears.value = false;
// 科技领域过滤
const surveyAreaList = ref([]);
const checkedAreaList = ref(['']);
const handleGetSearchAllArea = async () => {
try {
const res = await getSearchAllArea({ sortCode: "301" });
if (res.code === 200) {
surveyAreaList.value = res.data.map(item => ({ name: item.AREANAME, id: item.AREACODE }));
}
} catch (error) {}
surveyAreaList.value.unshift({ name: "全部领域", id: "" });
};
const handleCheckedYearsChange = (value) => {
const checkedCount = value.length;
checkAllYears.value = checkedCount === surveyYearList.value.length;
isIndeterminateYears.value = checkedCount > 0 && checkedCount < surveyYearList.value.length;
const handleCheckedAreasChange = event => {
if (event.length && event[event.length-1] !== "") {
checkedAreaList.value = event.filter(item => item !== "");
} else {
checkedAreaList.value = [""];
}
currentPage.value = 1;
handleFetchSurveyList();
};
const surveyAreaList = ref([]);
const checkedAreaList = ref([]);
const checkAllAreas = ref(false);
const isIndeterminateAreas = ref(false);
const handleCheckAllAreasChange = (val) => {
checkedAreaList.value = val ? surveyAreaList.value.map(a => a.id) : [];
isIndeterminateAreas.value = false;
// 发布时间过滤
const surveyYearList = ref([]);
const checkedYearList = ref(['']);
const handleGetSearchAllYear = async () => {
try {
const res = await getSearchAllYear({ sortCode: "301" });
if (res.code === 200) {
let allYear = res.data.sort((a, b) => (b-a));
let beforeYear = allYear.slice(6).join(',');
surveyYearList.value = allYear.slice(0, 6).map(item => ({ name: item + "年", id: item }));
if (beforeYear) surveyYearList.value.push({ name: "更早", id: beforeYear });
}
} catch (error) {}
surveyYearList.value.unshift({ name: "全部时间", id: "" });
};
const handleCheckedAreasChange = (value) => {
const checkedCount = value.length;
checkAllAreas.value = checkedCount === surveyAreaList.value.length;
isIndeterminateAreas.value = checkedCount > 0 && checkedCount < surveyAreaList.value.length;
const handleCheckedYearsChange = event => {
if (event.length && event[event.length-1] !== "") {
checkedYearList.value = event.filter(item => item !== "");
} else {
checkedYearList.value = [""];
}
currentPage.value = 1;
handleFetchSurveyList();
};
const totalDiscussNum = ref(0);
......@@ -145,8 +137,8 @@ const handleFetchSurveyList = async () => {
currentPage: currentPage.value - 1,
pageSize: pageSize.value,
sortCode: "301",
publishYear: checkAllYears.value ? "" : checkedSurveyYears.value.toString(),
Area: checkAllAreas.value ? "" : checkedAreaList.value.toString(),
publishYear: checkedYearList.value.join(',') || null,
Area: checkedAreaList.value.join(',') || null,
// keywords: searchText.value,
sortField: "date",
sortOrder: isSort.value ? "asc" : "desc"
......@@ -173,60 +165,11 @@ const handleSearch = () => {
handleFetchSurveyList();
};
watch(
[checkedSurveyYears, checkedAreaList, isSort],
() => {
if (isInitializing.value) return;
currentPage.value = 1;
handleFetchSurveyList();
},
{
deep: true
}
);
const handleGetSearchAllArea = async () => {
try {
const res = await getSearchAllArea({
sortCode: '301'
});
if(res.code === 200 && res.data) {
surveyAreaList.value = res.data.map(item => {
return {
name: item.AREANAME,
id: item.AREACODE
};
});
// 默认选中全部
checkAllAreas.value = true;
handleCheckAllAreasChange(true);
}
} catch (error) {
}
}
const handleGetSearchAllYear = async () => {
try {
const res = await getSearchAllYear({
sortCode: '301'
});
if(res.code === 200 && res.data) {
// 排序并格式化
const sortedYears = res.data.sort((a, b) => b - a);
surveyYearList.value = sortedYears.map(item => {
return {
name: item + '年',
id: item
};
});
// 默认选中全部
checkAllYears.value = true;
handleCheckAllYearsChange(true);
}
} catch (error) {
}
}
watch([isSort], () => {
if (isInitializing.value) return;
currentPage.value = 1;
handleFetchSurveyList();
});
onMounted(async () => {
await Promise.all([handleGetSearchAllArea(), handleGetSearchAllYear()]);
......@@ -309,58 +252,50 @@ onMounted(async () => {
.left {
width: 360px;
min-height: 560px;
min-height: 300px;
height: fit-content;
padding-bottom: 20px;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: #fff;
.left-box {
margin-top: 17px;
.left-header {
display: flex;
align-items: center;
.icon {
.check-box {
margin-top: 18px;
.check-head {
position: relative;
margin-bottom: 12px;
&::before {
content: "";
position: absolute;
top: 0px;
left: 0px;
width: 8px;
height: 16px;
background: var(--color-main-active);
height: 100%;
background: var(--color-primary-100);
border-radius: 0 2px 2px 0;
}
.title {
margin-left: 17px;
color: var(--color-main-active);
.head-name {
margin-left: 25px;
color: var(--color-primary-100);
font-size: 16px;
font-weight: 700;
line-height: 16px;
font-family: Source Han Sans CN;
font-weight: bold;
}
}
}
.checkbox-group {
padding: 10px 0 0 25px;
.filter-checkbox {
width: 130px;
margin-bottom: 8px;
height: 32px;
:deep(.el-checkbox__label) {
font-size: 16px;
color: #5f656c;
}
}
.expand-btn {
color: var(--color-main-active);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
margin-top: 4px;
.el-icon {
margin-left: 4px;
.check-list {
padding: 0 10px 0 25px;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 0 12px;
.check-item {
width: 100%;
height: 32px;
:deep(.el-checkbox__label) {
font-family: Source Han Sans CN;
font-size: 16px;
color: var(--text-primary-65-color);
}
}
}
}
......
......@@ -33,53 +33,37 @@
<div class="wrapper-main">
<div class="left">
<!-- 科技领域 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "科技领域" }}</div>
</div>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedAreaList" @change="handleCheckedAreasChange">
<el-checkbox class="filter-checkbox" label="全部领域"> 全部领域 </el-checkbox>
<el-checkbox v-for="area in surveyAreaList" :key="area.id" :label="area.id" class="filter-checkbox">
{{ area.name }}
</el-checkbox>
</el-checkbox-group>
<div class="check-box">
<div class="check-head">
<div class="head-name">{{ "科技领域" }}</div>
</div>
<el-checkbox-group class="check-list" v-model="checkedAreaList" @change="handleCheckedAreasChange">
<el-checkbox class="check-item" v-for="item in surveyAreaList" :key="item.id" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 发布时间 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "发布时间" }}</div>
<div class="check-box">
<div class="check-head">
<div class="head-name">{{ "发布时间" }}</div>
</div>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedSurveyYears" @change="handleCheckedYearsChange">
<el-checkbox class="filter-checkbox" label="全部时间"> 全部时间 </el-checkbox>
<el-checkbox v-for="year in displayedYearList" :key="year.id" :label="year.id" class="filter-checkbox">
<el-checkbox-group class="check-list" v-model="checkedYearList" @change="handleCheckedYearsChange">
<el-checkbox class="check-item" v-for="year in surveyYearList" :key="year.id" :label="year.id">
{{ year.name }}
</el-checkbox>
<div v-if="surveyYearList.length > 6" class="expand-btn" @click="isYearExpanded = !isYearExpanded">
{{ isYearExpanded ? "收起" : "更早" }}
<el-icon>
<ArrowUp v-if="isYearExpanded" />
<ArrowDown v-else />
</el-icon>
</div>
</el-checkbox-group>
</div>
</div>
<!-- 受调查国家/地区 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "受调查国家/地区" }}</div>
<div class="check-box">
<div class="check-head">
<div class="head-name">{{ "受调查国家/地区" }}</div>
</div>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedCountryList"
@change="handleCheckedCountriesChange">
<el-checkbox class="filter-checkbox" label="全部"> 全部 </el-checkbox>
<el-checkbox v-for="area in surveyCountryList" :key="area.id" :label="area.id" class="filter-checkbox">
<el-checkbox-group class="check-list" v-model="checkedCountryList" @change="handleCheckedCountriesChange">
<el-checkbox class="check-item" v-for="area in surveyCountryList" :key="area.id" :label="area.id">
{{ area.name }}
</el-checkbox>
</el-checkbox-group>
......@@ -103,8 +87,8 @@
</template>
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import { Search, ArrowDown, ArrowUp } from "@element-plus/icons-vue";
import { ref, onMounted, watch } from "vue";
import { Search } from "@element-plus/icons-vue";
import { getSearchAllArea, getSearchAllYear, getSurveyList, getSearchAllCountry } from "@/api/marketAccessRestrictions";
import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue"
......@@ -131,61 +115,71 @@ const releaseTimeList = ref([
// 科技领域过滤
const surveyAreaList = ref([]);
const checkedAreaList = ref(['全部领域']);
const handleCheckedAreasChange = val => {
if (val.includes("全部领域") && val.length > 1) {
if (val[val.length - 1] === "全部领域") {
checkedAreaList.value = ["全部领域"];
} else {
checkedAreaList.value = val.filter(item => item !== "全部领域");
const checkedAreaList = ref(['']);
const handleGetSearchAllArea = async () => {
try {
const res = await getSearchAllArea({ sortCode: "337" });
if (res.code === 200) {
surveyAreaList.value = res.data.map(item => ({ name: item.AREANAME, id: item.AREACODE }));
}
} else if (val.length === 0) {
checkedAreaList.value = ["全部领域"];
} catch (error) {}
surveyAreaList.value.unshift({ name: "全部领域", id: "" });
};
const handleCheckedAreasChange = event => {
if (event.length && event[event.length-1] !== "") {
checkedAreaList.value = event.filter(item => item !== "");
} else {
checkedAreaList.value = [""];
}
currentPage.value = 1;
handleFetchSurveyList();
};
// 发布时间过滤
const surveyYearList = ref([]);
const checkedSurveyYears = ref(['全部时间']);
const isYearExpanded = ref(false);
const displayedYearList = computed(() => {
if (isYearExpanded.value) return surveyYearList.value;
return surveyYearList.value.slice(0, 6);
});
const handleCheckedYearsChange = value => {
// const checkedCount = value.length;
// checkAllYears.value = checkedCount === surveyYearList.value.length;
// isIndeterminateYears.value = checkedCount > 0 && checkedCount < surveyYearList.value.length;
if (val.includes("全部时间") && val.length > 1) {
if (val[val.length - 1] === "全部时间") {
checkedSurveyYears.value = ["全部时间"];
} else {
checkedSurveyYears.value = val.filter(item => item !== "全部时间");
const checkedYearList = ref(['']);
const handleGetSearchAllYear = async () => {
try {
const res = await getSearchAllYear({ sortCode: "337" });
if (res.code === 200) {
let allYear = res.data.sort((a, b) => (b-a));
let beforeYear = allYear.slice(6).join(',');
surveyYearList.value = allYear.slice(0, 6).map(item => ({ name: item + "年", id: item }));
if (beforeYear) surveyYearList.value.push({ name: "更早", id: beforeYear });
}
} else if (val.length === 0) {
checkedSurveyYears.value = ["全部时间"];
} catch (error) {}
surveyYearList.value.unshift({ name: "全部时间", id: "" });
};
const handleCheckedYearsChange = event => {
if (event.length && event[event.length-1] !== "") {
checkedYearList.value = event.filter(item => item !== "");
} else {
checkedYearList.value = [""];
}
currentPage.value = 1;
handleFetchSurveyList();
};
// 受调查国家/地区过滤
const surveyCountryList = ref([]);
const checkedCountryList = ref(['全部']);
const handleCheckedCountriesChange = val => {
if (val.includes("全部") && val.length > 1) {
if (val[val.length - 1] === "全部") {
checkedCountryList.value = ["全部"];
} else {
checkedCountryList.value = val.filter(item => item !== "全部");
const checkedCountryList = ref(['']);
const handleGetSearchAllCountry = async () => {
try {
const res = await getSearchAllCountry();
if (res.code === 200) {
surveyCountryList.value = res.data.map(item => ({ name: item.COUNTRYNAME, id: item.COUNTRYID }));
}
} else if (val.length === 0) {
checkedCountryList.value = ["全部"];
} catch (error) {}
surveyCountryList.value.unshift({ name: "全部", id: "" });
};
const handleCheckedCountriesChange = event => {
if (event.length && event[event.length-1] !== "") {
checkedCountryList.value = event.filter(item => item !== "");
} else {
checkedCountryList.value = [""];
}
currentPage.value = 1;
handleFetchSurveyList();
};
// 数据列表
......@@ -203,9 +197,9 @@ const handleFetchSurveyList = async () => {
currentPage: currentPage.value - 1,
pageSize: pageSize.value,
sortCode: "337",
publishYear: checkedSurveyYears.value[0] === '全部时间' ? null : checkedSurveyYears.value.toString(),
Area: checkedAreaList.value[0] === '全部领域' ? null : checkedAreaList.value.toString(),
searchCountry: checkedCountryList.value[0] === '全部' ? null : checkedCountryList.value.toString(),
publishYear: checkedYearList.value.join(',') || null,
Area: checkedAreaList.value.join(',') || null,
searchCountry: checkedCountryList.value.join(',') || null,
caseStatus: filterStage.value ? filterStage.value : null,
keywords: searchText.value ? searchText.value : null,
sortField: "date",
......@@ -234,52 +228,11 @@ const handleSearch = () => {
};
// 监听过滤条件
watch(
[checkedSurveyYears, checkedAreaList, checkedCountryList, isSort, filterStage, filterParty, filterReason],
() => {
if (isInitializing.value) return;
currentPage.value = 1;
handleFetchSurveyList();
}
);
const handleGetSearchAllArea = async () => {
try {
const res = await getSearchAllArea({ sortCode: "337" });
if (res.code === 200 && res.data) {
surveyAreaList.value = res.data.map(item => ({
name: item.AREANAME,
id: item.AREACODE
}));
handleCheckAllAreasChange(true);
}
} catch (error) { }
};
const handleGetSearchAllYear = async () => {
try {
const res = await getSearchAllYear({ sortCode: "337" });
if (res.code === 200 && res.data) {
const sortedYears = res.data.sort((a, b) => b - a);
surveyYearList.value = sortedYears.map(item => ({
name: item + "年",
id: item
}));
}
} catch (error) { }
};
const handleGetSearchAllCountry = async () => {
try {
const res = await getSearchAllCountry();
if (res.code === 200 && res.data) {
surveyCountryList.value = res.data.map(item => ({
name: item.COUNTRYNAME,
id: item.COUNTRYID
}));
}
} catch (error) { }
};
watch([isSort, filterStage, filterParty, filterReason], () => {
if (isInitializing.value) return;
currentPage.value = 1;
handleFetchSurveyList();
});
onMounted(async () => {
await Promise.all([handleGetSearchAllArea(), handleGetSearchAllYear(), handleGetSearchAllCountry()]);
......@@ -360,58 +313,50 @@ onMounted(async () => {
.left {
width: 360px;
min-height: 560px;
min-height: 300px;
height: fit-content;
padding-bottom: 20px;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: #fff;
.left-box {
margin-top: 17px;
.left-header {
display: flex;
align-items: center;
.icon {
.check-box {
margin-top: 18px;
.check-head {
position: relative;
margin-bottom: 12px;
&::before {
content: "";
position: absolute;
top: 0px;
left: 0px;
width: 8px;
height: 16px;
background: var(--color-main-active);
height: 100%;
background: var(--color-primary-100);
border-radius: 0 2px 2px 0;
}
.title {
margin-left: 17px;
color: var(--color-main-active);
font-size: 16px;
font-weight: 700;
}
}
}
.checkbox-group {
padding: 10px 0 0 25px;
.filter-checkbox {
width: 130px;
margin-bottom: 8px;
height: 32px;
:deep(.el-checkbox__label) {
.head-name {
margin-left: 25px;
color: var(--color-primary-100);
font-size: 16px;
color: #5f656c;
line-height: 16px;
font-family: Source Han Sans CN;
font-weight: bold;
}
}
.expand-btn {
color: var(--color-main-active);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
margin-top: 4px;
.el-icon {
margin-left: 4px;
.check-list {
padding: 0 10px 0 25px;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 0 12px;
.check-item {
width: 100%;
height: 32px;
:deep(.el-checkbox__label) {
font-family: Source Han Sans CN;
font-size: 16px;
color: var(--text-primary-65-color);
}
}
}
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论