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

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

Zym dev 查看合并请求 !200
流水线 #3 已失败
VITE_BASE_API= '/api' VITE_BASE_API= '/api'
\ No newline at end of file # 图表解读等 /aiAnalysis 代理服务密钥(勿提交到公开仓库时可改为本地 .env.local)
VITE_AI_ANALYSIS_API_KEY=aircasKEY19491001
\ No newline at end of file
# 线上地址 # 线上地址
VITE_BASE_API= '/api' VITE_BASE_API= '/api'
\ No newline at end of file # 图表解读等 AI 服务(与部署环境一致时填写)
VITE_AI_ANALYSIS_API_KEY=aircasKEY19491001
\ No newline at end of file
...@@ -14,6 +14,7 @@ lerna-debug.log* ...@@ -14,6 +14,7 @@ lerna-debug.log*
# Dependencies # Dependencies
node_modules node_modules
*node_modules
.pnpm .pnpm
.npm .npm
......
stages:
- build
- deploy
default:
tags:
- risk-monitor-frontend
cache:
key:
files:
- package-lock.json
paths:
- .npm/
policy: pull-push
build_pre:
stage: build
image: node:18-alpine
rules:
- if: '$CI_COMMIT_BRANCH == "pre"'
script:
- npm config set cache .npm --global
- npm ci --prefer-offline --no-audit --no-fund
- npm run build
artifacts:
expire_in: 1 hour
paths:
- dist/
deploy_pre:
stage: deploy
image: alpine:3.20
rules:
- if: '$CI_COMMIT_BRANCH == "pre"'
dependencies:
- build_pre
script:
- apk add --no-cache rsync
- rsync -av --delete dist/ /nas/kjb_service/zm/pre-project/html/
\ No newline at end of file
...@@ -9,5 +9,6 @@ ...@@ -9,5 +9,6 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
<script src="/js/config.js"></script>
</body> </body>
</html> </html>
\ No newline at end of file
差异被折叠。
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"d3": "^7.9.0", "d3": "^7.9.0",
"d3-cloud": "^1.2.7", "d3-cloud": "^1.2.7",
"dayjs": "^1.11.20",
"default-passive-events": "^4.0.0", "default-passive-events": "^4.0.0",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"echarts-liquidfill": "^3.1.0", "echarts-liquidfill": "^3.1.0",
...@@ -42,7 +43,10 @@ ...@@ -42,7 +43,10 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.0.0", "@vitejs/plugin-vue": "^5.0.0",
"autoprefixer": "^10.4.27",
"postcss": "^8.5.8",
"sass": "^1.93.3", "sass": "^1.93.3",
"tailwindcss": "^3.4.17",
"unplugin-auto-import": "^0.17.0", "unplugin-auto-import": "^0.17.0",
"unplugin-vue-components": "^0.26.0", "unplugin-vue-components": "^0.26.0",
"vite": "^5.0.0" "vite": "^5.0.0"
......
const baseUrl = `http://8.140.26.4:9085`
\ No newline at end of file
差异被折叠。
<template> <template>
<div id="app"> <div id="app">
<div class="pro-wrapper"> <router-view></router-view>
<div class="home-page">
<ModuleHeader />
<div class="main-container">
<router-view />
</div>
</div>
<div class="right-btn" @click="handleClickToolBox">
<div class="item">
<div class="icon">
<img src="@/assets/icons/overview/domain.png" alt="" />
</div>
<div class="text">{{ "领域" }}</div>
</div>
<div class="item">
<div class="icon">
<img src="@/assets/icons/overview/element.png" alt="" />
</div>
<div class="text">{{ "要素" }}</div>
</div>
</div>
<div class="tool-box">
<!-- <div class="tool-item">
<img src="@/assets/icons/tool-item-icon1.png" alt="" />
</div>
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon2.png" alt="" />
</div>
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon3.png" alt="" />
</div>
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon4.png" alt="" />
</div> -->
<el-tooltip content="智能写报" placement="left" :offset="10">
<div class="tool-item" @click="handleOpenPage('znxb')">
<img src="@/assets/icons/tool-item-icon1.png" alt="" />
</div>
</el-tooltip>
<el-tooltip content="智能翻译" placement="left" :offset="10">
<div class="tool-item" @click="handleClickToolBox">
<img src="@/assets/icons/tool-item-icon2.png" alt="" />
</div>
</el-tooltip>
<!-- <div class="tool-item">
<img src="@/assets/icons/tool-item-icon3.png" alt="" />
</div> -->
<el-tooltip content="智能问答" placement="left" :offset="10">
<div class="tool-item" @click="handleOpenPage('znwd')">
<img src="@/assets/icons/tool-item-icon4.png" alt="" />
</div>
</el-tooltip>
</div>
<!-- <div class="ai-btn" @click="openAiBox">
<div class="icon">
<img src="@/assets/icons/ai-icon.png" alt="" />
</div>
<div class="text">智能问答</div>
</div> -->
<div class="ai-dialog" v-if="isShowAiBox">
<AiBox @close="closeAiBox" />
</div>
</div>
</div> </div>
</template> </template>
...@@ -95,27 +29,6 @@ import { ElMessage } from "element-plus"; ...@@ -95,27 +29,6 @@ import { ElMessage } from "element-plus";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
// const target = ref(null);
// const { x, y, isDragging } = useDraggable(target, {
// initialValue: { x: 1770, y: 800 },
// onStart: () => console.log("开始拖动"),
// onEnd: () => console.log("结束拖动")
// });
// const style = computed(() => ({
// position: "absolute",
// left: `${x.value}px`,
// top: `${y.value}px`,
// cursor: isDragging.value ? "grabbing" : "grab"
// }));
const handleToHome = () => {
router.push({
path: "/ZMOverView"
});
isCurrentOverview.value = true;
};
const isShowAiBox = ref(false); const isShowAiBox = ref(false);
...@@ -322,12 +235,26 @@ body { ...@@ -322,12 +235,26 @@ body {
display: none; display: none;
} }
/* #region 公共样式类名 */
/* 单行文本溢出隐藏显示省略号 */ /* 单行文本溢出隐藏显示省略号 */
.one-line-ellipsis { .one-line-ellipsis {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
/* 多行文本两端对齐 最后一行正常显示 */
.text-align-justify {
text-align: justify;
text-align-last: left;
-webkit-text-align-last: left;
}
/* 可点击文本 鼠标悬浮样式 */
#app .text-click-hover:hover {
text-decoration: underline;
color: var(--color-primary-100);
cursor: pointer;
}
/* #endregion 公共样式类名 */
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
......
import request from "@/api/request.js"; import { getToken } from "@/api/request.js";
const CHART_INTERPRETATION_URL = "/aiAnalysis/chart_interpretation";
const API_KEY = "aircasKEY19491001";
/**
* 从模型返回的 markdown code fence 中提取数组 JSON
* 例如:```json\n[ ... ]\n```
* @param {string} buffer
* @returns {unknown[]}
*/
function parseChartInterpretationArray(buffer) {
// 可能带有前后空白、换行、以及 ```/```json 包裹
let cleaned = String(buffer || "").trim();
// 移除开头 fence
cleaned = cleaned.replace(/^```(?:json)?\s*/i, "");
// 移除结尾 fence(允许末尾有换行)
cleaned = cleaned.replace(/```[\s\r\n]*$/i, "").trim();
// 优先直接解析
try {
const parsed = JSON.parse(cleaned);
if (Array.isArray(parsed)) return parsed;
} catch (_) { }
// 兜底:从 first '[' 到 last ']' 截取
const start = cleaned.indexOf("[");
const end = cleaned.lastIndexOf("]");
if (start !== -1 && end !== -1 && end > start) {
const arrStr = cleaned.slice(start, end + 1);
const parsed = JSON.parse(arrStr);
if (Array.isArray(parsed)) return parsed;
}
throw new Error("无法解析图表解读 JSON 数组");
}
// 图表解读(流式)
/** /**
* @param {text} * 图表解读(SSE 流式)
* @param {object} data - 请求体
* @param {object} [options] - 可选配置
* @param {function(string): void} [options.onChunk] - 每收到一条 SSE 消息时回调,参数为当前 chunk 的 text
* @returns {Promise<{data: unknown[]}>}
*/ */
export function getChartAnalysis(data) { export function getChartAnalysis(data, options = {}) {
return request({ const { onChunk } = options;
method: 'POST', return new Promise((resolve, reject) => {
url: `/aiAnalysis/chart_interpretation`, let buffer = "";
data, let settled = false;
}) const abortController = new AbortController();
}
\ No newline at end of file const safeResolve = value => {
if (settled) return;
settled = true;
resolve(value);
};
const safeReject = err => {
if (settled) return;
settled = true;
reject(err);
};
(async () => {
try {
const { fetchEventSource } = await import("@microsoft/fetch-event-source");
await fetchEventSource(CHART_INTERPRETATION_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
"Cache-Control": "no-cache",
"X-API-Key": API_KEY,
// 后端同项目其它接口使用 token 字段名(axios 拦截器里就是这样注入的)
token: getToken()
},
body: JSON.stringify(data),
signal: abortController.signal,
openWhenHidden: true,
retryDelay: 1000,
maxRetries: 2,
onopen: response => {
const contentType = response.headers.get("content-type") || "";
if (!contentType.includes("text/event-stream")) {
throw new Error("SSE连接格式异常:content-type 不是 text/event-stream");
}
},
onmessage: event => {
const raw = (event?.data || "").trim();
if (!raw) return;
if (raw === "[DONE]") return;
let chunk = "";
// 兼容后端返回格式:
// - {"text":"```"} / {"text":"json\n[\n"}
// - {"type":"reasoning","chunk":"..."}(新格式)
try {
const msg = JSON.parse(raw);
if (Array.isArray(msg?.chunk)) {
safeResolve({ data: msg.chunk });
abortController.abort();
return;
}
if (msg && typeof msg === "object" && "chunk" in msg) {
chunk = typeof msg.chunk === "string" ? msg.chunk : "";
if (chunk) buffer += chunk;
} else if (msg && typeof msg === "object" && "text" in msg) {
chunk = String(msg.text ?? "");
if (chunk) buffer += chunk;
} else {
chunk = raw;
buffer += raw;
}
} catch (e) {
chunk = raw;
buffer += raw;
}
// 每收到一条消息即回调,用于流式渲染
if (chunk && typeof onChunk === "function") {
onChunk(chunk);
}
// 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
const trimmed = buffer.trim();
if (trimmed.endsWith("```")) {
try {
const arr = parseChartInterpretationArray(trimmed);
safeResolve({ data: arr });
abortController.abort();
} catch (_) { }
}
},
onclose: () => {
try {
const arr = parseChartInterpretationArray(buffer);
safeResolve({ data: arr });
} catch (e) {
safeReject(e);
}
},
onerror: error => {
if (error?.name === "AbortError") return true;
safeReject(error);
return true;
}
});
} catch (error) {
safeReject(error);
}
})();
});
}
...@@ -192,3 +192,45 @@ export function getBillFullText(params) { ...@@ -192,3 +192,45 @@ export function getBillFullText(params) {
params, params,
}) })
} }
// 条款对比-根据两版版本与筛选条件获取配对条款列表
/**
* @param {billId,oldVersionId,newVersionId,diffType,cRelated,keyword}
* @header token
* @returns { list: Array<{ oldTerm: object|null, newTerm: object|null }> }
*/
export function getBillTermsCompare(params) {
return request({
method: "GET",
url: "/api/billInfoBean/content/compare",
params,
});
}
// 版本对比-根据两版版本与筛选条件获取条款列表(分页)
/**
* @param {billId,content,currentPage,currentVersion,isCn,originalVersion,pageSize,status}
* @header token
*/
export function getBillVersionCompare(params, config = {}) {
return request({
method: "GET",
url: "/api/billInfoBean/versionCompare",
params,
signal: config.signal,
});
}
// 版本对比-根据筛选条件获取变更统计
/**
* @param {billId,content,currentVersion,isCn,originalVersion}
* @header token
*/
export function getBillVersionCompareStatistics(params, config = {}) {
return request({
method: "GET",
url: "/api/billInfoBean/versionCompareStatistics",
params,
signal: config.signal,
});
}
...@@ -2,17 +2,24 @@ import request from "@/api/request.js"; ...@@ -2,17 +2,24 @@ import request from "@/api/request.js";
// 涉华法案领域分布 // 涉华法案领域分布
/** /**
* @param {year} * @param {Object} params
* @param {string} params.year - 年份
* @param {string} [params.status] - 状态:提出法案/通过法案
*/ */
export function getBillIndustry(params) { export function getBillIndustry(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/BillOverview/billIndustry/${params.year}`, url: `/api/BillOverview/billIndustry/${params.year}`,
params, params: { status: params.status }
}) })
} }
// 涉华法案统计 // 涉华法案统计
/**
* @param {Object} params
* @param {string} [params.dateDesc] - 时间范围:近一年/近两年/近三年/全部
* @param {string} [params.industryId] - 行业领域 ID
*/
export function getBillCount(params) { export function getBillCount(params) {
return request({ return request({
method: 'GET', method: 'GET',
...@@ -21,6 +28,19 @@ export function getBillCount(params) { ...@@ -21,6 +28,19 @@ export function getBillCount(params) {
}) })
} }
// 近期美国国会各委员会涉华提案数量汇总
/**
* @param {Object} params
* @param {string} params.dateDesc - 时间范围:近一周/近一月/近一年
*/
export function getStatisticsBillCountByCommittee(params) {
return request({
method: 'GET',
url: `/api/BillOverview/statisticsBillCountByCommittee`,
params
})
}
// 获取关键条款 // 获取关键条款
export function getBillOverviewKeyTK() { export function getBillOverviewKeyTK() {
return request({ return request({
...@@ -233,6 +253,30 @@ export async function getHistoryBillListWithStage(personId, params = {}) { ...@@ -233,6 +253,30 @@ export async function getHistoryBillListWithStage(personId, params = {}) {
} }
} }
// /**
// * 获取潜在提案举措分析
// * GET /api/personHomepage/historyBill/clause/{personId}
// * @param {string} personId - 人物ID
// * @param {Object} params - 查询参数
// */
// export async function getPotentialClauseAnalysis(personId, params = {}) {
// const queryString = Object.entries(params)
// .filter(([, value]) => value !== undefined && value !== null && value !== '')
// .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
// .join('&')
// const url = queryString
// ? `/api/personHomepage/historyBill/clause/${personId}?${queryString}`
// : `/api/personHomepage/historyBill/clause/${personId}`
// return request(url, {
// method: 'GET',
// })
// }
/**
* 获取排序选项
*/
export function getSortOptions() { export function getSortOptions() {
return Promise.resolve({ return Promise.resolve({
......
...@@ -129,6 +129,18 @@ export function getCharacterRelation(params) { ...@@ -129,6 +129,18 @@ export function getCharacterRelation(params) {
params, params,
}) })
} }
// 获取人物教育履历
/**
* @param {personId}
* @header token
*/
export function getCharacterReducationResume(params) {
return request({
method: 'GET',
url: `/api/personHomepage/educationResume/${params.personId}`,
params,
})
}
// 获取相关实体 // 获取相关实体
/** /**
...@@ -142,18 +154,14 @@ export function getCharacterRelatedEntity(params) { ...@@ -142,18 +154,14 @@ export function getCharacterRelatedEntity(params) {
params, params,
}) })
} }
// 获取人物教育履历 export function getareaType(params) {
/**
* @param {personId}
* @header token
*/
export function getCharacterReducationResume(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/personHomepage/educationResume/${params.personId}`, url: `/api/commonDict/areaType`,
params, params
}) })
} }
export async function getFindingsReport(personId, params = {}) { export async function getFindingsReport(personId, params = {}) {
const queryParts = [] const queryParts = []
...@@ -192,11 +200,4 @@ export async function getSubjectList(params) { ...@@ -192,11 +200,4 @@ export async function getSubjectList(params) {
method: 'GET', method: 'GET',
params params
}) })
} }
export function getareaType(params) {
return request({
method: 'GET',
url: `/api/commonDict/areaType`,
params
})
}
\ No newline at end of file
import request from "@/api/request.js"; import request from "@/api/request.js";
// 最新科技政令 // 最新科技政令
export function getDepartmentList() { export function getDepartmentList(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeDict/department`, url: `/api/administrativeDict/department`,
params
}) })
} }
...@@ -27,34 +28,36 @@ export function getDecreeRiskSignal(params) { ...@@ -27,34 +28,36 @@ export function getDecreeRiskSignal(params) {
// 行政令发布频度 // 行政令发布频度
export function getDecreeYearOrder(params) { export function getDecreeYearOrder(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeOrderOverview/yearOrder/${params.year}`, url: `/api/administrativeOrderOverview/yearOrder`,
params data: params
}) })
} }
// 政令涉及领域 // 政令涉及领域
export function getDecreeArea(params) { export function getDecreeArea(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeOrderOverview/industry/${params.year}`, url: `/api/administrativeOrderOverview/industry`,
params data: params
}) })
} }
// 关键行政令 // 关键行政令
export function getKeyDecree() { export function getKeyDecree(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeOrderOverview/action`, url: `/api/administrativeOrderOverview/action`,
data: params
}) })
} }
// 政令重点条款 // 政令重点条款
export function getDecreeKeyInstruction() { export function getDecreeKeyInstruction(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeOrderOverview/instruction`, url: `/api/administrativeOrderOverview/instruction`,
data: params
}) })
} }
...@@ -85,4 +88,22 @@ export function getDecreeTypeList() { ...@@ -85,4 +88,22 @@ export function getDecreeTypeList() {
method: 'GET', method: 'GET',
url: `/api/administrativeDict/type`, url: `/api/administrativeDict/type`,
}) })
}
// 关键机构
export function getKeyOrganization() {
return request({
method: 'GET',
url: `/api/commonFeature/keyOrganization`,
})
}
// AI智能总结
export function getChartInterpretation(params) {
return request({
method: 'POST',
url: `/aiAnalysis/chart_interpretation`,
headers: {"X-API-Key": "aircasKEY19491001"},
data: params
})
} }
\ No newline at end of file
...@@ -20,6 +20,39 @@ export function getDecreehylyList() { ...@@ -20,6 +20,39 @@ export function getDecreehylyList() {
}) })
} }
// 获取受影响实体列表
export function getDecreeEntities(params) {
return request({
method: 'POST',
url: `/api/administrativeOrderInfo/relatedEntities`,
data: params
})
}
// 获取实体产业链列表
export function getDecreeRelatedChain(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/relatedChain/${params.id}`,
})
}
// 获取产业链节点列表
export function getDecreeChainNodes(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/relatedChainNodes/${params.id}`,
})
}
// 获取实体关系节点列表
export function getDecreeRelatedEntitie(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/listRelatedEntitie/${params.id}`,
})
}
// 根据政行业领域ID获取公司列表 // 根据政行业领域ID获取公司列表
/** /**
* @param {cRelated, id} * @param {cRelated, id}
......
...@@ -77,4 +77,20 @@ export function getDecreeReport(params) { ...@@ -77,4 +77,20 @@ export function getDecreeReport(params) {
method: 'GET', method: 'GET',
url: `/api/administrativeOrderInfo/contentUrl/${params.id}`, url: `/api/administrativeOrderInfo/contentUrl/${params.id}`,
}) })
}
// 政令关键词云
export function getKeyWordUp() {
return request({
method: 'GET',
url: `/api/element/getKeyWordUp/2025-01-01`,
})
}
// 报告内容摘要
export function getOverview(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/overview/${params.id}`,
})
} }
\ No newline at end of file
...@@ -4,6 +4,7 @@ import { ElMessage } from "element-plus"; ...@@ -4,6 +4,7 @@ import { ElMessage } from "element-plus";
const request200 = requestP => { const request200 = requestP => {
return requestP.then(data => { return requestP.then(data => {
if (data.code === 200) { if (data.code === 200) {
console.log('返回的数据结构 =>', data.data)
return data.data; return data.data;
} }
ElMessage({ ElMessage({
...@@ -122,13 +123,14 @@ export function getSanctionsInfoCount() { ...@@ -122,13 +123,14 @@ export function getSanctionsInfoCount() {
* sanReason: string * sanReason: string
* }[]>} * }[]>}
*/ */
export function getSanctionProcess(typeName = "实体清单", pageNum = 1, pageSize = 10, isCn = false) { export function getSanctionProcess(sanTypeIds = "1", pageNum = 1, pageSize = 10, isCn = false) {
return request200( return request200(
request({ request({
method: "POST", method: "POST",
url: "/api/entitiesDataCount/getSanctionProcess", url: "/api/entitiesDataCount/getSanctionProcess",
data: { data: {
typeName, sanTypeIds,
// typeName: tabMap[sanTypeId],
pageNum, pageNum,
pageSize, pageSize,
isCn isCn
...@@ -391,13 +393,13 @@ export function getDomainDistribution(sanctionDate = "2025-11-11") { ...@@ -391,13 +393,13 @@ export function getDomainDistribution(sanctionDate = "2025-11-11") {
* startTime: string * startTime: string
* }[]>} * }[]>}
*/ */
export function getEntitiesList(typeName = "实体清单", pageNum = 1, pageSize = 10, sanctionDate = "", isCn = false) { export function getEntitiesList(sanTypeId=1, pageNum = 1, pageSize = 10, sanctionDate = "", isCn = false) {
return request200( return request200(
request({ request({
method: "POST", method: "POST",
url: "/api/sanctionList/pageQuery", url: "/api/sanctionList/pageQuery",
data: { data: {
typeName, sanTypeId,
pageNum, pageNum,
pageSize, pageSize,
sanctionDate, sanctionDate,
......
import request from "@/api/request.js"; import request from "@/api/request.js";
// 实体清单-制裁概况-获取实体清单基本信息 // 实体清单-制裁概况-获取实体清单基本信息
export function getEntityInfo(sanType) { export function getEntityInfo(id) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/baseInfo/${sanType}` url: `/api/sanctionList/baseInfoById/${id}`
}); });
} }
...@@ -98,10 +98,10 @@ export function get50PercentEntityCount(data) { ...@@ -98,10 +98,10 @@ export function get50PercentEntityCount(data) {
} }
// 实体清单-数据统计-总量统计 // 实体清单-数据统计-总量统计
export function getTotalCount() { export function getTotalCount(id) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/total` url: `/api/sanctionList/statistics/total?sanTypeId=${id}`
}); });
} }
...@@ -113,7 +113,7 @@ export function getTotalCount() { ...@@ -113,7 +113,7 @@ export function getTotalCount() {
export function getSanctionCountChange(params) { export function getSanctionCountChange(params) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/num`, url: `/api/sanctionList/statistics/num`,
params params
}); });
} }
...@@ -128,7 +128,7 @@ export function getSanctionCountChange(params) { ...@@ -128,7 +128,7 @@ export function getSanctionCountChange(params) {
export function getRegionCount(params) { export function getRegionCount(params) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/region`, url: `/api/sanctionList/statistics/region`,
params params
}); });
} }
...@@ -143,7 +143,7 @@ export function getRegionCount(params) { ...@@ -143,7 +143,7 @@ export function getRegionCount(params) {
export function getTechDomainCount(params) { export function getTechDomainCount(params) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/domain`, url: `/api/sanctionList/statistics/domain`,
params params
}); });
} }
...@@ -158,7 +158,7 @@ export function getTechDomainCount(params) { ...@@ -158,7 +158,7 @@ export function getTechDomainCount(params) {
export function getEntityTypeCount(params) { export function getEntityTypeCount(params) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/entityType`, url: `/api/sanctionList/statistics/entityType`,
params params
}); });
} }
...@@ -247,7 +247,7 @@ export function getSingleSanctionOverview(params) { ...@@ -247,7 +247,7 @@ export function getSingleSanctionOverview(params) {
export function getSingleSanctionEntityCountry(params) { export function getSingleSanctionEntityCountry(params) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/countryRegion`, url: `/api/sanctionList/statistics/countryRegion`,
params params
}); });
} }
...@@ -292,11 +292,10 @@ export function getSingleSanctionOverviewList(data) { ...@@ -292,11 +292,10 @@ export function getSingleSanctionOverviewList(data) {
* @param {string} params.sanRecordId - 制裁记录ID * @param {string} params.sanRecordId - 制裁记录ID
* @header token * @header token
*/ */
export function getSingleSanctionTotalCount(params) { export function getSingleSanctionTotalCount(id) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/total`, url: `/api/sanctionList/statistics/total?sanTypeId=${id}`,
params
}); });
} }
...@@ -311,7 +310,7 @@ export function getSingleSanctionTotalCount(params) { ...@@ -311,7 +310,7 @@ export function getSingleSanctionTotalCount(params) {
export function getSingleSanctionDomainCount(params) { export function getSingleSanctionDomainCount(params) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/domain`, url: `/api/sanctionList/statistics/domain`,
params params
}); });
} }
...@@ -327,7 +326,7 @@ export function getSingleSanctionDomainCount(params) { ...@@ -327,7 +326,7 @@ export function getSingleSanctionDomainCount(params) {
export function getSingleSanctionEntityTypeCount(params) { export function getSingleSanctionEntityTypeCount(params) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/entityType`, url: `/api/sanctionList/statistics/entityType`,
params params
}); });
} }
...@@ -341,7 +340,7 @@ export function getSingleSanctionEntityTypeCount(params) { ...@@ -341,7 +340,7 @@ export function getSingleSanctionEntityTypeCount(params) {
export function getSingleSanctionEntityCountryCount(params) { export function getSingleSanctionEntityCountryCount(params) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/countryRegion`, url: `/api/sanctionList/statistics/countryRegion`,
params params
}); });
} }
...@@ -357,7 +356,7 @@ export function getSingleSanctionEntityCountryCount(params) { ...@@ -357,7 +356,7 @@ export function getSingleSanctionEntityCountryCount(params) {
export function getSingleSanctionEntityRegionCount(params) { export function getSingleSanctionEntityRegionCount(params) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/statistics/el/region`, url: `/api/sanctionList/statistics/region`,
params params
}); });
} }
...@@ -393,6 +392,14 @@ export function getSingleSanctionEntitySupplyChain(params) { ...@@ -393,6 +392,14 @@ export function getSingleSanctionEntitySupplyChain(params) {
}); });
} }
// 单次制裁-深度挖掘-制裁实体信息
export function getSingleSanctionEntityInfo(id) {
return request({
method: "GET",
url: `/api/organization/sanInfo?orgId=${id}`,
});
}
// 单次制裁-深度挖掘-制裁实体股权信息 // 单次制裁-深度挖掘-制裁实体股权信息
/** /**
* @param {Object} params * @param {Object} params
...@@ -551,10 +558,10 @@ export function getSingleSanctionEntityInternationalPaper(params) { ...@@ -551,10 +558,10 @@ export function getSingleSanctionEntityInternationalPaper(params) {
} }
// 商业管制清单-CCL清单简介-基本信息 // 商业管制清单-CCL清单简介-基本信息
export function getCCLInfo() { export function getCCLInfo(sanTypeId = 13) {
return request({ return request({
method: "GET", method: "GET",
url: `/api/sanctionList/baseInfo/ccl` url: `/api/sanctionList/baseInfoById/${sanTypeId}`
}); });
} }
...@@ -621,3 +628,11 @@ export function getCclQuery(data) { ...@@ -621,3 +628,11 @@ export function getCclQuery(data) {
data data
}); });
} }
// 商业管制清单-CCL清单列表-清单版本
export function getCCLVersionList() {
return request({
method: "GET",
url: `/api/ccl/version/dateList`
});
}
import request from "@/api/request.js"; import request from "@/api/request.js";
// 根据行业领域id获取公司列表 // 获取实体列表(按行业/公司名筛选)
/** /**
* @param {id} * @param {Object} params
* @param {string} [params.id] - 行业领域id(全部领域不传)
* @param {string} [params.companyName] - 公司名称(搜索框为空不传)
* @returns {Array<{id: string, name: string, marketChange: number|null, chainCompanyId: string|number}>}
* 说明:右侧详情查询当前建议优先使用 chainCompanyId;若缺失可回退 id,兼容后续接口调整
*/ */
export function getCompanyList(params) { export function getCompanyList(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/billImpactAnalysis/industry/company/${params.id}`, url: `/api/billImpactAnalysis/industry/company`,
params, params,
}) })
} }
......
...@@ -10,12 +10,16 @@ export class TextEntity { ...@@ -10,12 +10,16 @@ export class TextEntity {
type: string; type: string;
} }
// 智能化:提取文本实体 // 智能化:提取文本实体
export function extractTextEntity(text: string): Promise<IntelligentResultWrapper<TextEntity[]>> { export function extractTextEntity(
text: string,
config: { signal?: AbortSignal } = {}
): Promise<IntelligentResultWrapper<TextEntity[]>> {
return request({ return request({
url: `${INTELLECTUAL_API}/extract-entity`, url: `${INTELLECTUAL_API}/extract-entity`,
method: "POST", method: "POST",
data: { data: {
text text
} },
signal: config.signal
}); });
} }
...@@ -51,6 +51,14 @@ service.interceptors.request.use(config => { ...@@ -51,6 +51,14 @@ service.interceptors.request.use(config => {
config.headers['token'] = token config.headers['token'] = token
// config.headers['Authorization'] = `Bearer ${token}` // 如果后端需要Bearer格式可以使用这个 // config.headers['Authorization'] = `Bearer ${token}` // 如果后端需要Bearer格式可以使用这个
} }
// 图表解读等 AI 分析服务(Vite 代理 /aiAnalysis)需要 X-API-Key
const reqUrl = String(config.url || '')
if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) {
config.headers['X-API-Key'] = aiApiKey
}
}
return config return config
}, error => { }, error => {
console.log(error) console.log(error)
...@@ -82,8 +90,14 @@ service.interceptors.response.use( ...@@ -82,8 +90,14 @@ service.interceptors.response.use(
// 重复请求触发的取消不提示错误 // 重复请求触发的取消不提示错误
if (isCanceledError) return Promise.reject(error) if (isCanceledError) return Promise.reject(error)
// 处理token过期或无效的情况 // 处理token过期或无效的情况(排除 AI 分析服务:其 401 多为 API Key 问题)
if (error.response && (error.response.status === 401 || error.response.status === 403)) { const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
if (
error.response &&
(error.response.status === 401 || error.response.status === 403) &&
!isAiAnalysisRequest
) {
ElMessage({ ElMessage({
message: 'Token已过期,请重新登录', message: 'Token已过期,请重新登录',
type: 'error', type: 'error',
......
...@@ -125,8 +125,7 @@ export function getPersonSummaryInfo(params) { ...@@ -125,8 +125,7 @@ export function getPersonSummaryInfo(params) {
url: `/api/personHomepage/summaryInfo/${params.personId}`, url: `/api/personHomepage/summaryInfo/${params.personId}`,
}) })
} }
// 获取人物全局信息 通过personId 获取personType
export function getareaType(params) { export function getareaType(params) {
return request({ return request({
method: 'GET', method: 'GET',
......
...@@ -10,6 +10,13 @@ export function getThinkTankList() { ...@@ -10,6 +10,13 @@ export function getThinkTankList() {
}) })
} }
export function getAllThinkTankList(params) {
return request({
method: 'GET',
url: '/api/thinkTankOverview/thinkTanks/page',
params: params
})
}
//智库概览:获取智库发布 //智库概览:获取智库发布
export function getNewReport() { export function getNewReport() {
return request({ return request({
...@@ -28,13 +35,19 @@ export function getThinkTankRiskSignal() { ...@@ -28,13 +35,19 @@ export function getThinkTankRiskSignal() {
}) })
} }
// 政策建议趋势分布 /**
* 政策建议趋势分布(数量变化趋势)
* @param {{ startDate: string, endDate: string }} params - 如 2024-01-01 ~ 2024-12-31
*/
export function getThinkTankPolicyIndustryChange(params) { export function getThinkTankPolicyIndustryChange(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/thinkTankOverview/policyIndustryChange/${params}`, url: `/api/thinkTankOverview/policyIndustryChange`,
params: {
}) startDate: params.startDate,
endDate: params.endDate
}
});
} }
// 政策建议领域分布 // 政策建议领域分布
...@@ -113,10 +126,44 @@ export function getThinkDynamicsReportType() { ...@@ -113,10 +126,44 @@ export function getThinkDynamicsReportType() {
//智库动态:获取智库报告 //智库动态:获取智库报告
export function getThinkDynamicsReport(params) { export function getThinkDynamicsReport(params) {
const safe = params || {}
// 兼容两种调用方式:
// 1) { id, startDate, authorName, currentPage, pageSize, researchTypeIds, searchText, sortFun, years }
// 2) { id, startDate, parmas: { authorName, currentPage, pageSize, researchTypeIds, searchText, sortFun, years } }
const inner = safe.parmas && typeof safe.parmas === 'object' ? safe.parmas : {}
const id = safe.id
const startDate = safe.startDate
const authorName = inner.authorName ?? safe.authorName ?? ''
const currentPage = inner.currentPage ?? safe.currentPage ?? 1
const pageSize = inner.pageSize ?? safe.pageSize ?? 10
const researchTypeIds = inner.researchTypeIds ?? safe.researchTypeIds ?? ''
const searchText = inner.searchText ?? safe.searchText ?? ''
const sortFun = inner.sortFun ?? safe.sortFun ?? false
const years = inner.years ?? safe.years ?? null
const query = { currentPage, pageSize, sortFun }
// 仅在有值时才传,避免后端按空值筛选
if (authorName) query.authorName = authorName
if (researchTypeIds) query.researchTypeIds = researchTypeIds
if (searchText) query.searchText = searchText
if (years !== null && years !== undefined && years !== '') query.years = years
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankInfo/report/${params.id}/${params.startDate}`, url: `/api/thinkTankInfo/report/${id}/${startDate}`,
params: params.parmas params: query
})
}
// 智库领域观点分析(流式)
// [POST] 8.140.26.4:10029/report-domain-view-analysis
export function postReportDomainViewAnalysis(data) {
return request({
method: 'POST',
// 开发环境走 Vite 同源代理,避免浏览器跨域(见 vite.config.js:/intelligent-api -> 8.140.26.4:10029)
url: '/intelligent-api/report-domain-view-analysis',
data
}) })
} }
...@@ -152,15 +199,31 @@ export function getThinkPolicyIndustryChange(params) { ...@@ -152,15 +199,31 @@ export function getThinkPolicyIndustryChange(params) {
}) })
} }
//获取智库政策 /**
* 获取智库政策(政策追踪列表)
* GET /api/thinkTankInfo/policy
* Query: thinkTankId, startDate, endDate, orgIds, domainIds(科技领域/智库领域,逗号分隔 id), pageNum, pageSize, sortField, sortOrder, sortFun, reportId 等
*/
export function getThinkPolicy(params) { export function getThinkPolicy(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/thinkTankInfo/policy/${params.id}/${params.startDate}`, url: '/api/thinkTankInfo/policy',
params params
}) })
} }
/**
* 政府机构字典(政策追踪-涉及部门筛选项)
* GET /api/commonDict/gov/agency
* @returns {Promise<{ code: number, data: Array<{ id: string, name: string }> }>}
*/
export function getGovAgencyDict() {
return request({
method: 'GET',
url: '/api/commonDict/gov/agency'
})
}
//智库百科基本信息 //智库百科基本信息
export function getThinkTankInfoBasic(params) { export function getThinkTankInfoBasic(params) {
return request({ return request({
...@@ -299,9 +362,26 @@ export function getThinkTankReportPolicy(params) { ...@@ -299,9 +362,26 @@ export function getThinkTankReportPolicy(params) {
//获取相关政策动态 //获取相关政策动态
export function getThinkTankReportPolicyAction(params) { export function getThinkTankReportPolicyAction(params) {
return request({ const {
method: 'GET', reportId,
url: `/api/thinkTankReport/policyAction/${params}`, currentPage,
pageSize,
keyword = "",
orgIds = "",
// 新增:按科技领域 / 标签过滤
industryName = ""
} = params;
return request({
method: 'GET',
url: `/api/thinkTankReport/policyDetail/${reportId}`,
params: {
currentPage,
pageSize,
keyword,
// 后端按标签过滤使用的字段
industryName,
orgIds,
}
}) })
} }
......
...@@ -50,7 +50,7 @@ export const countryCoordMap = { ...@@ -50,7 +50,7 @@ export const countryCoordMap = {
// 欧洲 // 欧洲
俄罗斯: [37.6184, 55.7558], // 俄罗斯莫斯科 俄罗斯: [37.6184, 55.7558], // 俄罗斯莫斯科
德国: [10.4515, 51.1657], // 德国柏林 德国: [10.4515, 51.1657], // 德国柏林
英国: [-3.436, 55.3781], // 英国伦敦 英国: [-2, 54], // 英国伦敦
法国: [2.2137, 46.2276], // 法国巴黎 法国: [2.2137, 46.2276], // 法国巴黎
意大利: [12.5674, 41.8719], // 意大利罗马 意大利: [12.5674, 41.8719], // 意大利罗马
西班牙: [-3.7492, 40.4637], // 西班牙马德里 西班牙: [-3.7492, 40.4637], // 西班牙马德里
...@@ -218,12 +218,12 @@ export function convertAsiaCenterCoord(coord) { ...@@ -218,12 +218,12 @@ export function convertAsiaCenterCoord(coord) {
const [lng, lat] = coord; const [lng, lat] = coord;
// 将以本初子午线为基准的坐标转换为以亚洲为中心的坐标 // 将以本初子午线为基准的坐标转换为以亚洲为中心的坐标
// world-asia-center.json 是将标准坐标的经度减去了 180 度 // world-asia-center.json 是将标准坐标的经度加上了 180 度(地图向右平移)
let newLng = lng - 180; let newLng = lng + 180;
// 规范化到 [-180, 180] 范围 // 规范化到 [-180, 180] 范围
if (newLng < -180) { if (newLng > 180) {
newLng += 360; newLng -= 360;
} }
return [newLng, lat]; return [newLng, lat];
......
...@@ -135,8 +135,8 @@ const headerTitleClasses = computed(() => [ ...@@ -135,8 +135,8 @@ const headerTitleClasses = computed(() => [
.header-icon { .header-icon {
width: 22px; width: 22px;
height: 18px; height: 18px;
margin-left: 5px; margin-left: 0px;
margin-right: 14px; margin-right: 10px;
} }
.blue-title-block { .blue-title-block {
...@@ -155,6 +155,7 @@ const headerTitleClasses = computed(() => [ ...@@ -155,6 +155,7 @@ const headerTitleClasses = computed(() => [
/* color: var(--base-color); */ /* color: var(--base-color); */
color: $base-color; color: $base-color;
line-height: 48px; line-height: 48px;
padding: 0 5px;
// padding: 0 12px; // padding: 0 12px;
} }
......
...@@ -10,8 +10,7 @@ ...@@ -10,8 +10,7 @@
</div> </div>
<div <div
class="search-type-tab" class="search-type-tab"
:class="{ active: billSearchType === 'state' }" :class="{ active: billSearchType === 'state', 'is-disabled': true }"
@click="handleChangeBillSearchType('state')"
> >
州议会 州议会
</div> </div>
...@@ -206,6 +205,11 @@ const handleToPosi = id => { ...@@ -206,6 +205,11 @@ const handleToPosi = id => {
color: rgb(5, 95, 194); color: rgb(5, 95, 194);
border-color: rgb(255, 255, 255); border-color: rgb(255, 255, 255);
} }
.search-type-tab.is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
} }
.search-main-with-tabs { .search-main-with-tabs {
......
...@@ -17,7 +17,7 @@ defineProps({ ...@@ -17,7 +17,7 @@ defineProps({
}) })
</script> </script>
<style scoped> <style lang="scss" scoped>
.action-button { .action-button {
height: 28px; height: 28px;
padding: 0 8px; padding: 0 8px;
...@@ -29,11 +29,16 @@ defineProps({ ...@@ -29,11 +29,16 @@ defineProps({
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
transition: all 0.3s; transition: all 0.3s;
} }
.action-button[type="normal"] { .action-button[type="normal"] {
background-color: rgba(255, 255, 255, 1); background-color: rgba(255, 255, 255, 1);
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
&:hover {
background: var(--color-primary-2);
}
} }
.action-button[type="active"] { .action-button[type="active"] {
...@@ -52,6 +57,4 @@ defineProps({ ...@@ -52,6 +57,4 @@ defineProps({
.action-button[type="active"]:hover { .action-button[type="active"]:hover {
background-color: #40a9ff; background-color: #40a9ff;
} */ } */
</style> </style>
\ No newline at end of file
<svg viewBox="0 0 16 12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="12.000000" fill="none" customFrame="url(#clipPath_1)">
<defs>
<clipPath id="clipPath_1">
<rect width="16.000000" height="12.000000" x="0.000000" y="0.000000" rx="6.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="AI-logo" width="16.000000" height="12.000000" x="0.000000" y="0.000000" rx="6.000000" />
<path id="合并" d="M13.0238 1.46629C13.7681 0.916637 14.14 0 14.14 0C14.14 0 14.0472 0.916623 14.5122 1.46629C14.9772 2.01595 16 2.19866 16 2.19866C16 2.19866 14.8838 2.38224 14.1396 2.93288C13.3955 3.48351 13.0234 4.40121 13.0234 4.40121C13.0234 4.40121 13.1163 3.48353 12.6512 2.93288C12.1861 2.38222 11.163 2.1986 11.163 2.1986C11.163 2.1986 12.2795 2.01594 13.0238 1.46629ZM8.72107 2.20032L9.85481 10.5L7.29299 10.5L6.99502 8.24068L3.42302 8.24068L2.42736 10.5L0 10.5L3.28857 2.61208L2.85978 2.20032L8.72107 2.20032ZM5.4325 3.60747L4.00442 6.83353L6.86057 6.83353L6.43179 3.60747L5.4325 3.60747ZM13.9457 10.4999L14.9162 4.57493L11.475 4.57493L11.9074 5.13349L11.2386 10.4999L13.9457 10.4999Z" fill="rgb(5,95,194)" fill-rule="evenodd" />
</svg>
<template>
<div class="ai-button-wrapper">
<div class="icon">
<img src="./ai-icon.svg" alt="">
</div>
<div class="text text-tip-1">{{ '总结' }}</div>
</div>
</template>
<style lang="scss" scoped>
.ai-button-wrapper{
width: 74px;
height: 28px;
border-radius: 20px 0 0 20px;
background: var(--color-primary-10);
display: flex;
gap: 6px;
align-items: center;
justify-content: left;
box-sizing: border-box;
padding-left: 12px;
cursor: pointer;
.text{
color: var(--color-primary-100);
}
}
</style>
\ No newline at end of file
<svg viewBox="0 0 20 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20.000000" height="16.000000" fill="none" customFrame="url(#clipPath_2)">
<defs>
<clipPath id="clipPath_2">
<rect width="20.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="AI-logo" width="20.000000" height="16.000000" x="0.000000" y="0.000000" rx="8.000000" />
<path id="合并" d="M16.2798 1.95505C17.2101 1.22218 17.6751 0 17.6751 0C17.6751 0 17.559 1.22216 18.1403 1.95505C18.7215 2.68794 20 2.93155 20 2.93155C20 2.93155 18.6047 3.17632 17.6745 3.9105C16.7443 4.64468 16.2793 5.86828 16.2793 5.86828C16.2793 5.86828 16.3954 4.6447 15.814 3.9105C15.2326 3.1763 13.9537 2.93147 13.9537 2.93147C13.9537 2.93147 15.3494 2.68792 16.2798 1.95505ZM10.9013 2.93376L12.3185 14L9.11624 14L8.74378 10.9876L4.27877 10.9876L3.03421 14L0 14L4.11071 3.48277L3.57473 2.93376L10.9013 2.93376ZM6.79062 4.80996L5.00553 9.11138L8.57572 9.11138L8.03974 4.80996L6.79062 4.80996ZM17.4321 13.9999L18.6452 6.09991L14.3437 6.09991L14.8843 6.84466L14.0482 13.9999L17.4321 13.9999Z" fill="rgb(5,95,194)" fill-rule="evenodd" />
</svg>
<template>
<div class="ai-pane-wrapper">
<div class="header">
<div class="icon">
<img src="./ai-icon.svg" alt="">
</div>
<div class="title text-title-3-show">{{ '智能总结' }}</div>
</div>
<div class="content text-regular">{{ aiContent }}</div>
</div>
</template>
<script setup>
const props = defineProps({
aiContent: {
type: String,
default: ''
}
})
</script>
<style lang="scss">
.ai-pane-wrapper {
width: 100%;
min-height: 156px;
height: auto;
background: var(--color-primary-2);
box-sizing: border-box;
padding: 12px 16px;
.header {
width: 126px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
border-radius: 20px;
background: var(--color-primary-10);
.icon {
width: 20px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.title {
color: var(--color-primary-100);
}
}
.content {
margin-top: 8px;
width: 100%;
min-height: 90px;
height: auto;
box-sizing: border-box;
padding: 0 12px;
color: var(--color-primary-100);
display: block;
overflow: visible;
word-break: break-word;
white-space: pre-wrap;
}
}
</style>
\ No newline at end of file
<svg viewBox="0 0 1019 61" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1019.000000" height="61.000000" fill="none" customFrame="#000000">
<defs>
<linearGradient id="paint_linear_1" x1="7.60449219" x2="10.1398888" y1="-1.90734863e-06" y2="61.0955963" gradientUnits="userSpaceOnUse">
<stop stop-color="rgb(231,243,255)" offset="0" stop-opacity="1" />
<stop stop-color="rgb(231,243,255)" offset="1" stop-opacity="0" />
</linearGradient>
</defs>
<path id="矩形 5734" d="M0.000322558 0L1019 0L1019 31.5006L0 61.0008L0.000322558 0Z" fill="url(#paint_linear_1)" fill-rule="evenodd" />
</svg>
<svg viewBox="0 0 15 12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15.000000" height="12.000000" fill="none" customFrame="url(#clipPath_0)">
<defs>
<clipPath id="clipPath_0">
<rect width="15.000000" height="12.000000" x="0.000000" y="0.000000" rx="6.000000" fill="rgb(255,255,255)" />
</clipPath>
<linearGradient id="paint_linear_0" x1="7.49999952" x2="7.49999952" y1="0" y2="10.5" gradientUnits="userSpaceOnUse">
<stop stop-color="rgb(5,95,194)" offset="0" stop-opacity="1" />
<stop stop-color="rgb(137,193,255)" offset="1" stop-opacity="1" />
</linearGradient>
</defs>
<rect id="AI-logo" width="15.000000" height="12.000000" x="0.000000" y="0.000000" rx="6.000000" />
<path id="合并" d="M12.2098 1.46629C12.9076 0.916637 13.2563 0 13.2563 0C13.2563 0 13.1693 0.916623 13.6052 1.46629C14.0411 2.01595 15 2.19866 15 2.19866C15 2.19866 13.9535 2.38224 13.2559 2.93288C12.5583 3.48351 12.2095 4.40121 12.2095 4.40121C12.2095 4.40121 12.2966 3.48353 11.8605 2.93288C11.4245 2.38222 10.4653 2.1986 10.4653 2.1986C10.4653 2.1986 11.5121 2.01594 12.2098 1.46629ZM8.176 2.20032L9.23888 10.5L6.83718 10.5L6.55783 8.24068L3.20908 8.24068L2.27565 10.5L0 10.5L3.08303 2.61208L2.68105 2.20032L8.176 2.20032ZM5.09297 3.60747L3.75415 6.83353L6.43179 6.83353L6.0298 3.60747L5.09297 3.60747ZM13.0741 10.4999L13.9839 4.57493L10.7578 4.57493L11.1632 5.13349L10.5361 10.4999L13.0741 10.4999Z" fill="url(#paint_linear_0)" fill-rule="evenodd" />
</svg>
<svg viewBox="0 0 6.70703 13.4102" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="6.707031" height="13.410156" fill="none" customFrame="#000000">
<rect id="容器 848" width="6.000000" height="11.997072" x="0.353516" y="0.707031" />
<path id="矢量 626" d="M0 0L2.99854 2.99854L6 0" stroke="rgb(5,95,194)" stroke-width="1.000000" transform="matrix(1,0,0,-1,0.353516,3.70508)" />
<path id="矢量 627" d="M0.353516 9.70508L3.35205 12.7036L6.35352 9.70508" stroke="rgb(5,95,194)" stroke-width="1.000000" />
</svg>
<template>
<div class="summary-wrapper">
<div class="summary-header">
<div class="icon1">
<img src="./icon1.svg" alt="">
</div>
<div class="text text-tip-1">{{ '智库报告总结摘要' }}</div>
<div class="icon2">
<img src="./icon2.svg" alt="">
</div>
</div>
<div class="summary-main">
<slot name="summary-content"></slot>
</div>
</div>
</template>
<style lang="scss">
.summary-wrapper {
width: 100%;
height: 100%;
// background: linear-gradient(90deg, var(--color-primary-10) 0%, var(--bg-white-100) 100%) ;
background: url('./bg.svg') no-repeat;
overflow: hidden;
border-radius: 4px;
border: 1px solid #E7F3FF;
.summary-header {
margin-top: 16px;
margin-left: 24px;
width: 199px;
height: 32px;
border: 1px solid var(--color-primary-100);
border-radius: 16px;
background: var(--color-primary-10);
display: flex;
justify-content: center;
align-items: center;
gap: 9px;
.icon1 {
width: 15px;
height: 12px;
img {
width: 100%;
height: 100%;
}
}
.text {
color: var(--color-primary-100);
}
.icon2 {
width: 6px;
height: 12px;
img {
width: 100%;
height: 100%;
}
}
}
.summary-main {
margin: 0 auto;
margin-top: 24px;
width: calc(100% - 52px);
height: calc(100% - 92px);
}
}
</style>
\ No newline at end of file
...@@ -11,18 +11,16 @@ ...@@ -11,18 +11,16 @@
<el-switch v-model="isTranslate" /> <el-switch v-model="isTranslate" />
<div class="switch-label">原文显示</div> <div class="switch-label">原文显示</div>
<div class="btn" @click="emits('download')"> <div
<div class="icon icon-gap-4"> v-for="action in headerActions"
<img :src="defaultDownloadIcon" alt="" /> :key="action.key"
class="btn"
@click="action.onClick"
>
<div :class="['icon', action.iconGapClass]">
<img :src="action.icon" alt="" />
</div> </div>
<div class="text">下载</div> <div class="text">{{ action.text }}</div>
</div>
<div class="btn" @click="handleFindWord('open')">
<div class="icon icon-gap-6">
<img :src="defaultSearchIcon" alt="" />
</div>
<div class="text">查找</div>
</div> </div>
<div class="find-word-box" v-if="findWordBox"> <div class="find-word-box" v-if="findWordBox">
...@@ -47,15 +45,16 @@ ...@@ -47,15 +45,16 @@
</div> </div>
<div class="report-main"> <div class="report-main">
<div v-if="!displayReportData.length" class="noContent">{{ "暂无数据" }}</div> <div v-if="!displayReportData.length" class="no-content">暂无数据</div>
<el-scrollbar height="100%" v-else> <el-scrollbar v-else height="100%">
<div <div
v-for="item in displayReportData" v-for="item in displayReportData"
:key="item.num" :key="item.num"
:class="['content-row', { 'high-light': isHighlight }]" class="content-row"
:class="{ 'high-light': isHighlight }"
> >
<div :class="['content-cn', { 'translate-cn': !isTranslate }]" v-html="item.content"></div> <div class="content-cn" :class="{ 'translate-cn': !isTranslate }" v-html="item.content" />
<div class="content-en" v-html="item.contentEn" v-if="isTranslate"></div> <div v-if="isTranslate" class="content-en" v-html="item.contentEn" />
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
...@@ -70,8 +69,6 @@ import defaultSearchIcon from "./assets/icons/search.png"; ...@@ -70,8 +69,6 @@ import defaultSearchIcon from "./assets/icons/search.png";
import { nextTick, ref, watch } from "vue"; import { nextTick, ref, watch } from "vue";
import { debounce } from "lodash"; import { debounce } from "lodash";
// 该组件为公共展示组件:网络请求/下载等业务由父组件处理;查找、高亮、显示切换等通用交互在组件内部封装。
// 图标资源固定使用组件内置文件,保证组件资源自包含。
const props = defineProps({ const props = defineProps({
reportData: { type: Array, default: () => [] }, reportData: { type: Array, default: () => [] },
}); });
...@@ -88,6 +85,23 @@ const findWordMax = ref(0); ...@@ -88,6 +85,23 @@ const findWordMax = ref(0);
const originReportData = ref([]); const originReportData = ref([]);
const displayReportData = ref([]); const displayReportData = ref([]);
const headerActions = [
{
key: "download",
text: "下载",
icon: defaultDownloadIcon,
iconGapClass: "icon-gap-4",
onClick: () => emits("download"),
},
{
key: "search",
text: "查找",
icon: defaultSearchIcon,
iconGapClass: "icon-gap-6",
onClick: () => handleFindWord("open"),
},
];
function escapeRegExp(text) { function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
} }
...@@ -137,7 +151,9 @@ const doUpdateWord = () => { ...@@ -137,7 +151,9 @@ const doUpdateWord = () => {
displayReportData.value = originReportData.value.map((item) => { displayReportData.value = originReportData.value.map((item) => {
const cn = applyHighlightToText(item.content, term); const cn = applyHighlightToText(item.content, term);
const en = isTranslate.value ? applyHighlightToText(item.contentEn, term) : { html: item.contentEn, count: 0 }; const en = isTranslate.value
? applyHighlightToText(item.contentEn, term)
: { html: item.contentEn, count: 0 };
findWordMax.value += cn.count + en.count; findWordMax.value += cn.count + en.count;
return { return {
...item, ...item,
...@@ -222,8 +238,9 @@ watch(isTranslate, () => { ...@@ -222,8 +238,9 @@ watch(isTranslate, () => {
width: 1600px; width: 1600px;
background-color: white; background-color: white;
padding: 0 60px; padding: 0 60px;
height: 20px;
flex: auto; flex: auto;
height: 100%;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.report-header { .report-header {
...@@ -245,7 +262,6 @@ watch(isTranslate, () => { ...@@ -245,7 +262,6 @@ watch(isTranslate, () => {
display: flex; display: flex;
align-items: center; align-items: center;
.find-word-input { .find-word-input {
width: 20px;
flex: auto; flex: auto;
} }
.find-word-limit { .find-word-limit {
...@@ -265,8 +281,7 @@ watch(isTranslate, () => { ...@@ -265,8 +281,7 @@ watch(isTranslate, () => {
font-size: 20px; font-size: 20px;
line-height: 20px; line-height: 20px;
font-weight: 700; font-weight: 700;
width: 20px; flex: 1;
flex: auto;
} }
.btn { .btn {
margin-left: 10px; margin-left: 10px;
...@@ -310,25 +325,15 @@ watch(isTranslate, () => { ...@@ -310,25 +325,15 @@ watch(isTranslate, () => {
} }
.report-main { .report-main {
height: 20px;
flex: auto; flex: auto;
box-sizing: border-box; height: 20px;
padding-top: 10px; padding: 10px 0;
&::-webkit-scrollbar {
display: none;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
&::-webkit-scrollbar-track { :deep(.el-scrollbar) {
background: #f1f1f1; height: 100%;
} }
.noContent { .no-content {
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -347,10 +352,6 @@ watch(isTranslate, () => { ...@@ -347,10 +352,6 @@ watch(isTranslate, () => {
min-height: 100px; min-height: 100px;
gap: 80px; gap: 80px;
&:last-child {
border-bottom: none;
}
.content-en, .content-en,
.content-cn { .content-cn {
width: 50%; width: 50%;
......
...@@ -41,6 +41,12 @@ const getGraphChart = (nodes, links, layoutType) => { ...@@ -41,6 +41,12 @@ const getGraphChart = (nodes, links, layoutType) => {
itemStyle: { itemStyle: {
color: '#73C0DE' color: '#73C0DE'
}, },
// 方法1:通过 left/right/top/bottom 控制绘图区域
left: '5%',
right: '5%',
top: '5%',
bottom: '5%',
layout: layoutType, layout: layoutType,
data: nodes, data: nodes,
links: links, links: links,
......
...@@ -5,10 +5,11 @@ ...@@ -5,10 +5,11 @@
</template> </template>
<script setup> <script setup>
import { onMounted, nextTick } from 'vue'; import { onMounted, onBeforeUnmount } from 'vue';
import setChart from '@/utils/setChart'; import setChart from '@/utils/setChart';
import getGraphChart from './graphChart'; import getGraphChart from './graphChart';
const emits = defineEmits(["handleClickNode"])
const props = defineProps({ const props = defineProps({
nodes: { nodes: {
type: Array, type: Array,
...@@ -27,16 +28,22 @@ const props = defineProps({ ...@@ -27,16 +28,22 @@ const props = defineProps({
default: 'force' default: 'force'
}, },
height: { height: {
type: String, type: String,
default: 'force' default: 'force'
} }
}) })
let chart = null
onMounted(() => { onMounted(() => {
const graph = getGraphChart(props.nodes, props.links, props.layoutType) const graph = getGraphChart(props.nodes, props.links, props.layoutType)
setChart(graph, 'graph') chart = setChart(graph, 'graph')
chart.on("click", (event) => { emits("handleClickNode", event) })
}) })
onBeforeUnmount(() => {
chart.off("click")
chart.dispose()
})
</script> </script>
...@@ -45,5 +52,7 @@ onMounted(() => { ...@@ -45,5 +52,7 @@ onMounted(() => {
.graph-chart-wrapper { .graph-chart-wrapper {
width: 100%; width: 100%;
height: 100%; height: 100%;
// width: 800px;
// height: 500px;
} }
</style> </style>
\ No newline at end of file
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="#000000">
<rect id="Line/Calendar" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="形状" d="M11 3.08L13.52 3.08C13.7855 3.08 14 3.2945 14 3.56L14 13.52C14 13.7855 13.7855 14 13.52 14L2.48 14C2.2145 14 2 13.7855 2 13.52L2 3.56C2 3.2945 2.2145 3.08 2.48 3.08L5 3.08L5 2.12C5 2.054 5.054 2 5.12 2L5.96 2C6.026 2 6.08 2.054 6.08 2.12L6.08 3.08L9.92 3.08L9.92 2.12C9.92 2.054 9.974 2 10.04 2L10.88 2C10.946 2 11 2.054 11 2.12L11 3.08ZM3.08 4.16L3.08 6.2L12.92 6.2L12.92 4.16L11 4.16L11 4.88C11 4.946 10.946 5 10.88 5L10.04 5C9.974 5 9.92 4.946 9.92 4.88L9.92 4.16L6.08 4.16L6.08 4.88C6.08 4.946 6.026 5 5.96 5L5.12 5C5.054 5 5 4.946 5 4.88L5 4.16L3.08 4.16ZM12.92 12.92L3.08 12.92L3.08 7.22L12.92 7.22L12.92 12.92Z" fill="rgb(59,65,75)" fill-rule="evenodd" />
</svg>
<template>
<div class="time-tab-pane-wrapper">
<div
class="time-item"
:class="{'time-item-active': item.active}"
v-for="item,index in timeList"
:key="index"
@click="handleTimeClick(item,index)"
>
<div class="icon" v-if="item.active">
<img src="./calendat-icon.svg" alt="">
</div>
<div class="text text-tip-1" :class="{'text-active': item.active}">{{ item.time }}</div>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue'
const timeList = ref([
{
time: '近一周',
active: true
},
{
time: '近一月',
active: false
},
{
time: '近一年',
active: false
},
])
const handleTimeClick = (item, index) => {
timeList.value.forEach(time => {
time.active = false
})
timeList.value[index].active = true
emit('time-click', item)
}
const emit = defineEmits(['time-click'])
</script>
<style lang="scss" scoped>
.time-tab-pane-wrapper{
display: flex;
width: 248px;
height: 36px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50px;
border: 2px solid rgba(255,255,255,1);
.time-item{
height: 32px;
box-sizing: border-box;
padding: 4px 12px;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
cursor: pointer;
.icon{
width: 16px;
height: 16px;
img{
width: 100%;
height: 100%;
}
}
.text{
color: var(--text-primary-50-color);
}
.text-active{
color: var(--text-primary-80-color)
}
}
.time-item-active{
background: rgba(255, 255, 255, 0.65);
border-radius: 50px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
}
}
</style>
\ No newline at end of file
...@@ -3,13 +3,18 @@ ...@@ -3,13 +3,18 @@
<div class="icon"> <div class="icon">
<img src="./tip-icon.svg" alt=""> <img src="./tip-icon.svg" alt="">
</div> </div>
<div class="text text-tip-2 text-primary-50-clor">{{ `数据来源:${dataSource},数据时间:${dataTime}` }}</div> <div class="text text-tip-2 text-primary-50-clor">{{ tipText }}</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
text: {
type: String,
default: ''
},
dataSource: { dataSource: {
type: String, type: String,
default: '美国国会官网' default: '美国国会官网'
...@@ -21,20 +26,24 @@ const props = defineProps({ ...@@ -21,20 +26,24 @@ const props = defineProps({
}) })
const tipText = computed(() => props.text || `数据来源:${props.dataSource},数据时间:${props.dataTime}`)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tip-wrapper{ .tip-wrapper {
width: 100%; width: 100%;
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 22px; height: 22px;
.icon{
.icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
img{
img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
......
<template>
<div class="tree-chart-wrapper" id="tree-chart">
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import setChart from '@/utils/setChart';
import getTreeChart from './treeChart';
import CompanyImg from "@/assets/icons/symbol.png";
const props = defineProps({
treeData: {
type: Array,
default: [
{
id: 1,
name: 'a1',
symbolSize: 30,
value: 5,
symbol: `image://${CompanyImg}`,
children: [
{
id: 11,
name: 'b1',
symbolSize: 30,
value: 5,
symbol: `image://${CompanyImg}`,
},
{
id: 12,
name: 'b2',
symbolSize: 30,
value: 5,
symbol: `image://${CompanyImg}`,
},
{
id: 13,
name: 'b3',
symbolSize: 30,
value: 5,
symbol: `image://${CompanyImg}`,
},
{
id: 14,
name: 'b4',
symbolSize: 30,
value: 5,
symbol: `image://${CompanyImg}`,
}
]
}
]
}
})
onMounted(() => {
const treeChart = getTreeChart(props.treeData)
setChart(treeChart, 'tree-chart')
})
</script>
<style lang="scss" scoped>
.tree-chart-wrapper {
width: 100%;
height: 100%;
}
</style>
\ No newline at end of file
const getTreeChart = (treeData) => {
const option = {
series: [{
type: 'tree',
layout: 'orthogonal', // 从上到下布局
orient: 'TB', // Top to Bottom
data: treeData,
top: '10%',
bottom: '10%',
left: '3%',
right: '3%',
symbol: 'circle',
symbolSize: 40,
label: {
position: 'top',
verticalAlign: 'middle',
align: 'center',
fontSize: 20, // 字体大小
color: 'rgb(59, 65, 75)', // 字体颜色
fontWeight: 'normal', // 字体粗细
// formatter: '{b}', // 标签内容格式器
// rotate: 0, // 旋转角度
offset: [0, -6], // 偏移量
// lineHeight: 20, // 行高
// backgroundColor: 'transparent', // 背景色
// borderColor: 'transparent', // 边框颜色
// borderWidth: 0, // 边框宽度
// borderRadius: 0, // 圆角
// padding: 0, // 内边距
// shadowBlur: 0, // 阴影模糊
// shadowColor: 'transparent' // 阴影颜色
},
leaves: {
label: {
position: 'bottom',
verticalAlign: 'middle',
align: 'center',
offset: [0, 6],
}
},
lineStyle: {
color: '#ccc',
width: 2,
type: 'dashed', // 线条类型:'solid'(实线), 'dashed'(虚线), 'dotted'(点线)
curveness: 0.5, // 曲线弧度(0-1),仅当 edgeShape 为 'curve' 时有效
cap: 'round', // 线帽:'butt', 'round', 'square'
join: 'round', // 连接点:'bevel', 'round', 'miter'
shadowBlur: 0, // 阴影模糊大小
shadowColor: 'rgba(0,0,0,0.5)', // 阴影颜色
shadowOffsetX: 0, // 阴影水平偏移
shadowOffsetY: 0 // 阴影垂直偏移
},
emphasis: {
focus: 'descendant'
},
expandAndCollapse: false,
initialTreeDepth: 3
}]
};
return option
}
export default getTreeChart
\ No newline at end of file
<template> <template>
<div class="warnning-pane-wrapper" :style="{ width: width ? width : '1600px', height: height ? height : '116px' }" <div class="warnning-pane-wrapper" :style="{ width: width ? width : '1600px', height: height ? height : 'auto', minHeight: height ? undefined : '116px' }"
:class="{ :class="{
level1: warnningLevel === '特别重大风险', level1: warnningLevel === '特别重大风险',
level2: warnningLevel === '重大风险', level2: warnningLevel === '重大风险',
...@@ -146,15 +146,12 @@ const handleClickPane = () => { ...@@ -146,15 +146,12 @@ const handleClickPane = () => {
.warnning-pane-content{ .warnning-pane-content{
width: calc(100% - 40px); width: calc(100% - 40px);
margin: 0 auto; margin: 0 auto;
height: 60px; margin-bottom: 16px;
display: -webkit-box; min-height: 60px;
/* 2. 设置内部布局方向为垂直 */ height: auto;
-webkit-box-orient: vertical; display: block;
/* 3. 限制显示的行数为 2 行 */ overflow: visible;
-webkit-line-clamp: 2; white-space: pre-wrap;
/* 4. 隐藏超出部分 */ word-break: break-word;
overflow: hidden;
/* 5. 设置文本溢出显示省略号 */
text-overflow: ellipsis;
} }
</style> </style>
\ No newline at end of file
...@@ -21,9 +21,10 @@ const getWordCloudChart = data => { ...@@ -21,9 +21,10 @@ const getWordCloudChart = data => {
gridSize: 15, // 网格大小,影响词间距。 gridSize: 15, // 网格大小,影响词间距。
sizeRange: [16, 36], // 定义词云中文字大小的范围 sizeRange: [16, 36], // 定义词云中文字大小的范围
rotationRange: [0, 0], rotationRange: [0, 0],
rotationStep: 0, // rotationRange: [-90, 90],
drawOutOfBound: false, // 是否超出画布 // rotationStep: 10,
shrinkToFit: true, // 是否自动缩小以适应容器 drawOutOfBound: false, // 是否超出画布
shrinkToFit: true, // 是否自动缩小以适应容器
// 字体 // 字体
textStyle: { textStyle: {
color: function (params) { color: function (params) {
......
...@@ -133,11 +133,12 @@ const emit = defineEmits(['save', 'download', 'collect']) ...@@ -133,11 +133,12 @@ const emit = defineEmits(['save', 'download', 'collect'])
} }
} }
// .header-btn { .header-btn {
// display: flex; // display: flex;
// justify-content: flex-end; // justify-content: flex-end;
// gap: 8px; // gap: 8px;
// } margin-right: 10px;
}
// .header-btn1 { // .header-btn1 {
// position: absolute; // position: absolute;
......
...@@ -96,16 +96,16 @@ const homeTitleList = ref([ ...@@ -96,16 +96,16 @@ const homeTitleList = ref([
path: "/ZMOverView", path: "/ZMOverView",
disabled: false disabled: false
}, },
{ // {
name: "主要国家科技动向感知", // name: "主要国家科技动向感知",
path: "", // path: "",
disabled: true // disabled: true
}, // },
{ // {
name: "主要国家竞争科技安全", // name: "主要国家竞争科技安全",
path: "", // path: "",
disabled: true // disabled: true
} // }
]); ]);
const homeActiveTitleIndex = ref(0); const homeActiveTitleIndex = ref(0);
......
<template> <template>
<p class="p-regular-rereg"> <p class="p-regular-rereg">
<span class="text-regular" v-for="(segment, index) in processedText" :key="index"> <span class="text-regular" v-for="(segment, index) in processedText" :key="index">
<a v-if="segment.isEntity" :href="`https://cn.bing.com/search?q=${segment.entity?.text_span}`" <span v-if="segment.isEntity" @click="$emit('onEntityClick', segment.entity)" class="entity-link">
class="entity-link" target="_blank" rel="noopener noreferrer"> {{ segment.entity?.text_span }}
{{ segment.entity?.text_span }} <img :src="SearchIcon" :width="10" :height="10" alt="search" />
<img :src="SearchIcon" :width="10" :height="10" alt="search" /> </span>
</a> <span v-else>
<span v-else> {{ segment.text }}
{{ segment.text }} </span>
</span> </span>
</span> </p>
</p>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { TextEntity } from '@/api/intelligent'; import { TextEntity } from "@/api/intelligent";
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted } from "vue";
import SearchIcon from './images/search.png' import SearchIcon from "./images/search.png";
export interface ProcessedTextSegment { export interface ProcessedTextSegment {
text: string text: string;
isEntity: boolean isEntity: boolean;
entity?: TextEntity entity?: TextEntity;
} }
const props = defineProps({ const props = defineProps({
text: { text: {
type: String, type: String,
default: '' default: ""
}, },
entities: { entities: {
type: Array<TextEntity>, type: Array<TextEntity>,
default: () => [] default: () => []
} }
}) });
const emit = defineEmits(["onEntityClick"]);
// 处理后的文本段 // 处理后的文本段
const processedText = ref<ProcessedTextSegment[]>([]) const processedText = ref<ProcessedTextSegment[]>([]);
// 处理文本,识别并替换实体 // 处理文本,识别并替换实体
const processText = () => { const processText = () => {
console.log('props.entities.length', props.entities.length) console.log("props.entities.length", props.entities.length);
if (!props.text || !props.entities) { if (!props.text || !props.entities) {
// console.log('props.text', props.entities.length) // console.log('props.text', props.entities.length)
processedText.value = [{ text: '', isEntity: false }] processedText.value = [{ text: "", isEntity: false }];
return return;
} }
const result = [] const result = [];
let currentPosition = 0 let currentPosition = 0;
// 按实体文本长度排序,优先匹配长文本 // 按实体文本长度排序,优先匹配长文本
const sortedEntities = [...props.entities].sort((a, b) => const sortedEntities = [...props.entities].sort((a, b) => b.text_span.length - a.text_span.length);
b.text_span.length - a.text_span.length
)
while (currentPosition < props.text.length) { while (currentPosition < props.text.length) {
let matched = false let matched = false;
for (const entity of sortedEntities) { for (const entity of sortedEntities) {
const entityText = entity.text_span const entityText = entity.text_span;
const endPosition = currentPosition + entityText.length const endPosition = currentPosition + entityText.length;
if (props.text.substring(currentPosition, endPosition) === entityText) { if (props.text.substring(currentPosition, endPosition) === entityText) {
// 如果当前位置是实体,添加到结果 // 如果当前位置是实体,添加到结果
result.push({ result.push({
isEntity: true, isEntity: true,
entity: { ...entity } entity: { ...entity }
}) });
currentPosition = endPosition currentPosition = endPosition;
matched = true matched = true;
break break;
} }
} }
if (!matched) { if (!matched) {
// 如果不是实体,收集普通文本 // 如果不是实体,收集普通文本
let nextEntityStart = props.text.length let nextEntityStart = props.text.length;
for (const entity of sortedEntities) { for (const entity of sortedEntities) {
const pos = props.text.indexOf(entity.text_span, currentPosition) const pos = props.text.indexOf(entity.text_span, currentPosition);
if (pos !== -1 && pos < nextEntityStart) { if (pos !== -1 && pos < nextEntityStart) {
nextEntityStart = pos nextEntityStart = pos;
} }
} }
if (nextEntityStart > currentPosition) { if (nextEntityStart > currentPosition) {
const plainText = props.text.substring(currentPosition, nextEntityStart) const plainText = props.text.substring(currentPosition, nextEntityStart);
result.push({ result.push({
text: plainText, text: plainText,
isEntity: false isEntity: false
}) });
currentPosition = nextEntityStart currentPosition = nextEntityStart;
} else { } else {
// 没有更多实体,添加剩余文本 // 没有更多实体,添加剩余文本
const remainingText = props.text.substring(currentPosition) const remainingText = props.text.substring(currentPosition);
if (remainingText) { if (remainingText) {
result.push({ result.push({
text: remainingText, text: remainingText,
isEntity: false isEntity: false
}) });
} }
currentPosition = props.text.length currentPosition = props.text.length;
} }
} }
} }
processedText.value = result processedText.value = result;
} };
// 监听文本和实体变化 // 监听文本和实体变化
watch(() => props.text, processText) watch(() => props.text, processText);
watch(() => props.entities, processText, { deep: true }) watch(() => props.entities, processText, { deep: true });
// 初始化处理 // 初始化处理
onMounted(processText) onMounted(processText);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@use '@/styles/common.scss'; @use "@/styles/common.scss";
.entity-link { .entity-link {
color: var(--color-primary-100); color: var(--color-primary-100);
&:hover {
cursor: pointer;
}
} }
.p-regular-rereg { .p-regular-rereg {
text-indent: 2em; text-indent: 2em;
margin: 4px 0; margin: 4px 0;
} }
</style> </style>
\ No newline at end of file
<template> <template>
<div class="full-width"> <div class="full-width">
<div class="flex-display" style="align-items: center;"> <div class="flex-display" style="align-items: center">
<common-text class="text-title-3-bold" color="var(--text-primary-80-color)">{{ isOpenTranslation <common-text class="text-title-3-bold" color="var(--text-primary-80-color)">{{
? '中文' : '原文' }}</common-text> isOpenTranslation ? "中文" : "原文"
<div class="flex-fill" style="margin: 0 10px;"> }}</common-text>
<el-divider></el-divider> <div class="flex-fill" style="margin: 0 10px">
</div> <el-divider></el-divider>
<el-button v-if="showMoreVisible" @click="() => { showMore = !showMore; updateText() }"> </div>
{{ showMore ? '收起' : '展开' }} <el-button
<el-icon> v-if="showMoreVisible"
<arrow-up v-if="showMore" /> @click="
<arrow-down v-else /> () => {
</el-icon> showMore = !showMore;
</el-button> updateText();
</div> }
<el-row :gutter="32"> "
<el-col :span="textColSpan" v-for="(item, index) in allTexts" :key="index"> >
<!-- <p class="p-news-content"> {{ item }}</p> --> {{ showMore ? "收起" : "展开" }}
<intelligent-entity-text :text="item" <el-icon>
:entities="isHighlightEntity ? textEntities : []"></intelligent-entity-text> <arrow-up v-if="showMore" />
</el-col> <arrow-down v-else />
</el-row> </el-icon>
</div> </el-button>
</div>
<el-row :gutter="32">
<el-col :span="textColSpan" v-for="(item, index) in allTexts" :key="index">
<!-- <p class="p-news-content"> {{ item }}</p> -->
<intelligent-entity-text
:text="item"
@on-entity-click="e => $emit('onEntityClick', e)"
:entities="isHighlightEntity ? textEntities : []"
></intelligent-entity-text>
</el-col>
</el-row>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import '@/styles/container.scss'; import "@/styles/container.scss";
import '@/styles/common.scss'; import "@/styles/common.scss";
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted } from "vue";
import { TextEntity } from '@/api/intelligent'; import { TextEntity } from "@/api/intelligent";
import IntelligentEntityText from '@/components/base/texts/IntelligentEntityText.vue'; import IntelligentEntityText from "@/components/base/texts/IntelligentEntityText.vue";
import { ElIcon, ElButton, ElDivider, ElRow, ElCol } from 'element-plus'; import { ElIcon, ElButton, ElDivider, ElRow, ElCol } from "element-plus";
import CommonText from './CommonText.vue'; import CommonText from "./CommonText.vue";
const allTexts = ref([]); const allTexts = ref([]);
const textColSpan = ref(12); const textColSpan = ref(12);
...@@ -39,71 +51,73 @@ const hasTranslation = ref(false); ...@@ -39,71 +51,73 @@ const hasTranslation = ref(false);
const showMore = ref(false); const showMore = ref(false);
const showMoreVisible = ref(false); const showMoreVisible = ref(false);
const props = defineProps({ const props = defineProps({
//段落列表: 原始文本 //段落列表: 原始文本
textsRaw: { textsRaw: {
type: Array<String>, type: Array<String>,
default: () => [] default: () => []
}, },
//段落列表: 翻译文本 //段落列表: 翻译文本
textsTranslate: { textsTranslate: {
type: Array<String>, type: Array<String>,
default: () => [] default: () => []
}, },
//是否显示翻译 //是否显示翻译
isOpenTranslation: { isOpenTranslation: {
type: Boolean, type: Boolean,
default: true default: true
}, },
//是否高亮实体 //是否高亮实体
isHighlightEntity: { isHighlightEntity: {
type: Boolean, type: Boolean,
default: true default: true
}, },
//实体列表 //实体列表
textEntities: { textEntities: {
type: Array<TextEntity>, type: Array<TextEntity>,
default: () => [] default: () => []
} }
}) });
const emit = defineEmits(["onEntityClick"]);
function updateText() { function updateText() {
const tempTexts = [] const tempTexts = [];
const tempRaws = props.textsRaw ?? [] const tempRaws = props.textsRaw ?? [];
const tempTranslates = props.textsTranslate ?? [] const tempTranslates = props.textsTranslate ?? [];
hasTranslation.value = tempTranslates.length > 0 hasTranslation.value = tempTranslates.length > 0;
if (hasTranslation.value && props.isOpenTranslation) { if (hasTranslation.value && props.isOpenTranslation) {
// 遍历原始文本和翻译文本,将它们交替添加到 tempTexts 中,并保持原始文本和翻译文本的的数量一致 // 遍历原始文本和翻译文本,将它们交替添加到 tempTexts 中,并保持原始文本和翻译文本的的数量一致
const maxCount = Math.max(tempRaws.length, tempTranslates.length) const maxCount = Math.max(tempRaws.length, tempTranslates.length);
for (let i = 0; i < maxCount; i++) { for (let i = 0; i < maxCount; i++) {
if (i < tempTranslates.length) { if (i < tempTranslates.length) {
tempTexts.push(tempTranslates[i]); tempTexts.push(tempTranslates[i]);
} else { } else {
tempTexts.push(''); tempTexts.push("");
} }
if (i < tempRaws.length) { if (i < tempRaws.length) {
tempTexts.push(tempRaws[i]); tempTexts.push(tempRaws[i]);
} else { } else {
tempTexts.push(''); tempTexts.push("");
} }
} }
console.log(tempTexts.length) console.log(tempTexts.length);
textColSpan.value = 12; textColSpan.value = 12;
showMoreVisible.value = tempTexts.length > 6; showMoreVisible.value = tempTexts.length > 6;
allTexts.value = showMore.value ? tempTexts : tempTexts.slice(0, 6); allTexts.value = showMore.value ? tempTexts : tempTexts.slice(0, 6);
} else { } else {
textColSpan.value = 24; textColSpan.value = 24;
showMoreVisible.value = tempRaws.length > 3; showMoreVisible.value = tempRaws.length > 3;
allTexts.value = showMore.value ? tempRaws : tempRaws.slice(0, 3); allTexts.value = showMore.value ? tempRaws : tempRaws.slice(0, 3);
} }
} }
watch(() => [props.textsRaw, props.textsTranslate, props.isOpenTranslation], () => { watch(
updateText(); () => [props.textsRaw, props.textsTranslate, props.isOpenTranslation],
}) () => {
updateText();
}
);
onMounted(() => { onMounted(() => {
updateText(); updateText();
}) });
</script> </script>
\ No newline at end of file
// 法案资源库
const CountryBill = () => import('@/views/dataLibrary/bill/countryBill/index.vue')
const StateBill = () => import('@/views/dataLibrary/bill/stateBill/index.vue')
const dataBillRoutes = [
// 科技法案资源库路由
{
path: "/dataLibrary/countryBill",
name: "CountryBill",
component: CountryBill,
meta: {
title: '国会法案', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/stateBill",
name: "StateBill",
component: StateBill,
meta: {
title: '州法案', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataBillRoutes
\ No newline at end of file
// 法案资源库
const Decree = () => import('@/views/dataLibrary/decree/index.vue')
const dataDecreeRoutes = [
// 科技法案资源库路由
{
path: "/dataLibrary/dataDecree",
name: "DataDecree",
component: Decree,
meta: {
title: '科技政令', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataDecreeRoutes
\ No newline at end of file
// 法案资源库
const CommerceControlList = () => import('@/views/dataLibrary/exportControl/commerceControlList/index.vue')
const CommerceControlListEvent = () => import('@/views/dataLibrary/exportControl/commerceControlListEvent/index.vue')
const EntityList = () => import('@/views/dataLibrary/exportControl/entityList/index.vue')
const EntityListEvent = () => import('@/views/dataLibrary/exportControl/entityListEvent/index.vue')
const dataDecreeRoutes = [
// 科技法案资源库路由
{
path: "/dataLibrary/dataCommerceControlList",
name: "CommerceControlList",
component: CommerceControlList,
meta: {
title: '商业管制清单', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/dataCommerceControlListEvent",
name: "CommerceControlListEvent",
component: CommerceControlListEvent,
meta: {
title: '商业管制清单事件', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/dataEntityList",
name: "EntityList",
component: EntityList,
meta: {
title: '实体清单', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/dataEntityListEvent",
name: "EntityListEvent",
component: EntityListEvent,
meta: {
title: '实体清单事件', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataDecreeRoutes
\ No newline at end of file
// 法案资源库
const MREList = () => import('@/views/dataLibrary/financeControl/mREList/index.vue')
const MREListEvent = () => import('@/views/dataLibrary/financeControl/mREListEvent/index.vue')
const SDNList = () => import('@/views/dataLibrary/financeControl/sDNList/index.vue')
const SDNListEvent = () => import('@/views/dataLibrary/financeControl/sDNListEvent/index.vue')
const dataDecreeRoutes = [
// 科技法案资源库路由
{
path: "/dataLibrary/mREList",
name: "MREList",
component: MREList,
meta: {
title: '涉军企业清单', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/mREListEvent",
name: "MREListEvent",
component: MREListEvent,
meta: {
title: '涉军企业清单事件', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/sDNList",
name: "SDNList",
component: SDNList,
meta: {
title: 'SDN清单', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/sDNListEvent",
name: "SDNListEvent",
component: SDNListEvent,
meta: {
title: 'SDN清单事件', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataDecreeRoutes
\ No newline at end of file
// 法案资源库
const KeyLab = () => import('@/views/dataLibrary/innovationSubject/keyLab/index.vue')
const ResearchUniversity = () => import('@/views/dataLibrary/innovationSubject/researchUniversity/index.vue')
const TechnologyCompany = () => import('@/views/dataLibrary/innovationSubject/technologyCompany/index.vue')
const dataDecreeRoutes = [
// 科技法案资源库路由
{
path: "/dataLibrary/keyLab",
name: "KeyLab",
component: KeyLab,
meta: {
title: '重点实验室', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/researchUniversity",
name: "ResearchUniversity",
component: ResearchUniversity,
meta: {
title: '研究型大学', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/technologyCompany",
name: "TechnologyCompany",
component: TechnologyCompany,
meta: {
title: '科技企业', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataDecreeRoutes
\ No newline at end of file
// 法案资源库
const Case337 = () => import('@/views/dataLibrary/marketControl/case337/index.vue')
const Case232 = () => import('@/views/dataLibrary/marketControl/case232/index.vue')
const Case301 = () => import('@/views/dataLibrary/marketControl/case301/index.vue')
const dataDecreeRoutes = [
// 科技法案资源库路由
{
path: "/dataLibrary/case337",
name: "Case337",
component: Case337,
meta: {
title: '337调查', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/case232",
name: "Case232",
component: Case232,
meta: {
title: '232调查', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/case301",
name: "Case301",
component: Case301,
meta: {
title: '301调查', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataDecreeRoutes
\ No newline at end of file
// 法案资源库
const CongressMan = () => import('@/views/dataLibrary/technologyFigures/congressMan/index.vue')
const Minister = () => import('@/views/dataLibrary/technologyFigures/minister/index.vue')
const TechnologyLeader = () => import('@/views/dataLibrary/technologyFigures/technologyLeader/index.vue')
const ThinkTankResearcher = () => import('@/views/dataLibrary/technologyFigures/thinkTankResearcher/index.vue')
const dataDecreeRoutes = [
// 科技法案资源库路由
{
path: "/dataLibrary/congressMan",
name: "CongressMan",
component: CongressMan,
meta: {
title: '国会议员', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/minister",
name: "Minister",
component: Minister,
meta: {
title: '机构主官', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/technologyLeader",
name: "TechnologyLeader",
component: TechnologyLeader,
meta: {
title: '科技企业领袖', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/thinkTankResearcher",
name: "ThinkTankResearcher",
component: ThinkTankResearcher,
meta: {
title: '智库研究人员', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataDecreeRoutes
\ No newline at end of file
// 法案资源库
const ThinkTank = () => import('@/views/dataLibrary/thinkTank/index.vue')
const dataThinkTankRoutes = [
// 科技法案资源库路由
{
path: "/dataLibrary/dataThinkTank",
name: "DataThinkTank",
component: ThinkTank,
meta: {
title: '美国科技智库', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataThinkTankRoutes
\ No newline at end of file
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
const Home = () => import('@/views/home/index.vue')
const DataLibrary = () => import('@/views/dataLibrary/index.vue')
// 自动导入所有模块路由 // 自动导入所有模块路由
const modules = import.meta.glob('./modules/*.js', { eager: true }) const modules = import.meta.glob('./modules/*.js', { eager: true })
...@@ -9,8 +11,38 @@ const fileRoutes = Object.keys(modules).reduce((acc, path) => { ...@@ -9,8 +11,38 @@ const fileRoutes = Object.keys(modules).reduce((acc, path) => {
return acc return acc
}, []) }, [])
// 自动导入所有资源库模块路由
const datas = import.meta.glob('./dataLibrary/*.js', { eager: true })
const dataRoutes = Object.keys(datas).reduce((acc, path) => {
const module = datas[path].default
acc.push(...module)
return acc
}, [])
const routes = [ const routes = [
...fileRoutes, {
path: "/",
name: "Home",
component: Home,
children: [
...fileRoutes
]
},
{
path: "/dataLibrary",
name: "DataLibrary",
component: DataLibrary,
meta: {
title: '数据资源库'
},
children: [
...dataRoutes
]
},
]; ];
......
...@@ -14,7 +14,7 @@ const BillInfluenceIndustry = () => import('@/views/bill/influence/industry/inde ...@@ -14,7 +14,7 @@ const BillInfluenceIndustry = () => import('@/views/bill/influence/industry/inde
const BillProgressForecast = () => import('@/views/bill/influence/ProgressForecast/index.vue') const BillProgressForecast = () => import('@/views/bill/influence/ProgressForecast/index.vue')
const BillInfluenceScientificResearch = () => import('@/views/bill/influence/scientificResearch/index.vue') const BillInfluenceScientificResearch = () => import('@/views/bill/influence/scientificResearch/index.vue')
const BillRelevantCircumstance = () => import('@/views/bill/relevantCircumstance/index.vue') const BillRelevantCircumstance = () => import('@/views/bill/relevantCircumstance/index.vue')
const BillOriginalText = () => import('@/views/bill/billOriginalText/index.vue') const BillVersionCompare = () => import('@/views/bill/versionCompare/index.vue')
const billRoutes = [ const billRoutes = [
...@@ -37,14 +37,6 @@ const billRoutes = [ ...@@ -37,14 +37,6 @@ const billRoutes = [
dynamicTitle: true // 标记需要动态设置标题 dynamicTitle: true // 标记需要动态设置标题
}, },
children: [ children: [
{
path: "originalText",
name: "BillOriginalText",
component: BillOriginalText,
meta: {
title: "法案原文"
}
},
// 法案分析路由 // 法案分析路由
{ {
path: "bill", path: "bill",
...@@ -151,6 +143,14 @@ const billRoutes = [ ...@@ -151,6 +143,14 @@ const billRoutes = [
// meta: { // meta: {
// title: "相关情况" // title: "相关情况"
// } // }
},
{
path: "versionCompare",
name: "BillVersionCompare",
component: BillVersionCompare,
meta: {
title: "版本对比"
}
} }
] ]
}, },
......
// 综合搜索 // 综合搜索
const ComprehensiveSearch = () => import('@/views/comprehensiveSearch/index.vue') const ComprehensiveSearch = () => import("@/views/comprehensiveSearch/index.vue");
const SearchResults = () => import('@/views/comprehensiveSearch/searchResults/index.vue') const SearchResults = () => import("@/views/comprehensiveSearch/searchResults/index.vue");
const Chat = () => import('@/views/comprehensiveSearch/chat/index.vue') const Chat = () => import("@/views/comprehensiveSearch/chat/index.vue");
const comprehensiveSearchRoutes = [ const comprehensiveSearchRoutes = [
// 综合搜索 // 综合搜索
{ {
path: "/comprehensiveSearch", path: "/comprehensiveSearch",
name: "comprehensiveSearch", name: "comprehensiveSearch",
component: ComprehensiveSearch, component: ComprehensiveSearch,
meta: { meta: {
title: "搜索-科技安全" title: "搜索-科技安全"
} }
}, },
{ {
path: "/searchResults", path: "/searchResults",
name: "searchResults", name: "searchResults",
component: SearchResults, component: SearchResults,
meta: { meta: {
title: "搜索结果", title: "搜索结果",
dynamicTitle: true dynamicTitle: true
} }
}, },
{ {
path: "/chat", path: "/chat",
name: "chat", name: "chat",
component: Chat, component: Chat,
meta: { meta: {
title: "智能问答" title: "智能问答"
} }
}, }
];
]
import { useGotoPage } from "../common.js"; import { useGotoPage } from "../common.js";
export function useGotoComprehensiveSearch() { export function useGotoComprehensiveSearch() {
const gotoPage = useGotoPage(); const gotoPage = useGotoPage();
return (isNewTabs = true) => gotoPage("/comprehensiveSearch/", {}, isNewTabs) return (isNewTabs = true) => gotoPage("/comprehensiveSearch/", {}, isNewTabs);
} }
export function useGotoSearchResults() { export function useGotoSearchResults() {
const gotoPage = useGotoPage(); const gotoPage = useGotoPage();
return (isNewTabs = true) => gotoPage("/searchResults/", {searchText, areaName}, isNewTabs)
return (searchText, areaName, isNewTabs = true) => gotoPage("/searchResults/", { searchText, areaName }, isNewTabs);
} }
export default comprehensiveSearchRoutes export default comprehensiveSearchRoutes;
\ No newline at end of file
...@@ -18,7 +18,7 @@ const decreeRoutes = [ ...@@ -18,7 +18,7 @@ const decreeRoutes = [
name: "Decree", name: "Decree",
component: Decree, component: Decree,
meta: { meta: {
title: "政令概览" title: "科技政令概况"
} }
}, },
{ {
......
...@@ -120,7 +120,7 @@ const exportControlRoutes = [ ...@@ -120,7 +120,7 @@ const exportControlRoutes = [
name: "commercialControlList", name: "commercialControlList",
component: () => import("@/views/exportControl/v2.0CommercialControlList/index.vue"), component: () => import("@/views/exportControl/v2.0CommercialControlList/index.vue"),
meta: { meta: {
title: "全部实体清单" title: "商业管制清单"
} }
} }
] ]
......
...@@ -3,6 +3,8 @@ const thinkTank = () => import('@/views/thinkTank/index.vue') ...@@ -3,6 +3,8 @@ const thinkTank = () => import('@/views/thinkTank/index.vue')
const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue') const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue')
const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue') const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue')
const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue') const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue')
const allThinkTank= () => import('@/views/thinkTank/allThinkTank/index.vue')
const MultiThinkTankViewAnalysis= () => import('@/views/thinkTank/MultiThinkTankViewAnalysis/index.vue')
const thinktankRoutes = [ const thinktankRoutes = [
// 智库系统的主要路由 // 智库系统的主要路由
...@@ -36,9 +38,19 @@ const thinktankRoutes = [ ...@@ -36,9 +38,19 @@ const thinktankRoutes = [
path: "/thinkTank/reportOriginal/:id", path: "/thinkTank/reportOriginal/:id",
name: "ReportOriginal", name: "ReportOriginal",
component: ReportOriginal, component: ReportOriginal,
// meta: {
// title: "报告原文" },
// } {
path: "/thinkTank/allThinkTank",
name: "allThinkTank",
component: allThinkTank,
},
{
path: "/thinkTank/MultiThinkTankViewAnalysis/:id",
name: "MultiThinkTankViewAnalysis",
component: MultiThinkTankViewAnalysis,
}, },
] ]
......
import { defineStore } from 'pinia'
const useTagsViewStore = defineStore('tags-view', {
state: () => ({
visitedViews: [], // 存放打开的标签页列表 [{ path, title, name, affix }]
cachedViews: [] // 存放需要缓存的组件名称 (用于 keep-alive)
}),
actions: {
// 添加标签页
addView(view) {
this.addVisitedView(view)
this.addCachedView(view)
},
addVisitedView(view) {
this.visitedViews.forEach(item => {
item.active = false
})
// 防止重复添加
const isExists = this.visitedViews.some(v => v.path === view.path)
if (!isExists) {
// 可以给不同路由设置不同的标题,比如从 route.meta.title 获取
this.visitedViews.push({
...view,
title: view.meta?.title || '未命名'
})
} else {
this.visitedViews.forEach(v => {
if (v.path === view.path) {
v.active = true
}
})
}
},
addCachedView(view) {
// 只有配置了 keepAlive: true 的路由才加入缓存数组
if (view.meta?.keepAlive && !this.cachedViews.includes(view.name)) {
this.cachedViews.push(view.name)
}
},
// 关闭当前标签页
delView(view) {
return new Promise((resolve) => {
// 1. 先从缓存数组中移除(如果有)
this.delCachedView(view)
// 2. 再从访问数组中移除
const index = this.visitedViews.findIndex(v => v.path === view.path)
if (index !== -1) {
this.visitedViews.splice(index, 1)
}
resolve([...this.visitedViews])
})
},
// 从缓存中移除
delCachedView(view) {
if (view.meta?.keepAlive) {
const index = this.cachedViews.indexOf(view.name)
if (index !== -1) {
this.cachedViews.splice(index, 1)
}
}
},
// 关闭其他/右侧/全部
delOthersViews(view) {
// ...
}
}
})
export default useTagsViewStore
\ No newline at end of file
<template>
<el-row class="wrapper layout-grid-line">
<el-col :span="span">
<pre>
{{
`
import AiButton from '@/components/base/AiButton/index.vue'
import AiPane from '@/components/base/AiPane/index.vue'
<div class="chart-box">
<div class="btn-box" @mouseenter="handleSwitchAiContentShow(true)">
<AiButton />
</div>
<div class="content-box" v-if="isShowAiContent" @mouseleave="handleSwitchAiContentShow(false)">
<AiPane :aiContent="aiContent" />
</div>
</div>
`
}}
</pre>
<div class="chart-box">
<div class="btn-box" @mouseenter="handleSwitchAiContentShow(true)">
<AiButton />
</div>
<div class="content-box" v-if="isShowAiContent" @mouseleave="handleSwitchAiContentShow(false)">
<AiPane :aiContent="aiContent" />
</div>
</div>
</el-col>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
import '@/styles/common.scss'
import AiButton from '@/components/base/Ai/AiButton/index.vue'
import AiPane from '@/components/base/Ai/AiPane/index.vue'
const span = 12
const handleSwitchAiContentShow = (isShow) => {
isShowAiContent.value = isShow
}
const isShowAiContent = ref(false)
const aiContent = ref(`整个立法过程反映了美国政治的高度极化特点。民主党全员反对该法案,批评其"劫贫济富"。而共和党内部也有分歧,特别是在财政赤字问题上。马斯克多次公开批评该法案是"疯狂的支出计划"。整个立法过程反映了美国政治的高度极化特点。民主党全员反对该法案,批评其"劫贫济富"。而共和党内部也有分歧,特别是在财政赤字问题上。马斯克多次公开批评该法案是"疯狂的支出计划"。`)
</script>
<style lang="scss" scoped>
.chart-box {
width: 520px;
height: 300px;
border: 1px solid var(--bg-black-5);
position: relative;
.btn-box {
position: absolute;
right: 0;
bottom: 18px;
}
.content-box {
position: absolute;
left: 0;
bottom: 0;
}
}
</style>
\ No newline at end of file
<template>
<el-row class="wrapper layout-grid-line">
<el-col :span="span">
<pre>
{{
`
import AiSummary from '@/components/base/Ai/AiSummary/index.vue'
<div class="summary-box">
<AiSummary />
</div>
`
}}
</pre>
<div class="summary-box">
<AiSummary>
<template #summary-content>
<div class="content-box">
我是插槽内容区域(忽略背景色)
</div>
</template>
</AiSummary>
</div>
</el-col>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
import '@/styles/common.scss'
import AiSummary from '@/components/base/Ai/AiSummary/index.vue'
const span = 24
const aiContent = ref(`整个立法过程反映了美国政治的高度极化特点。民主党全员反对该法案,批评其"劫贫济富"。而共和党内部也有分歧,特别是在财政赤字问题上。马斯克多次公开批评该法案是"疯狂的支出计划"。整个立法过程反映了美国政治的高度极化特点。民主党全员反对该法案,批评其"劫贫济富"。而共和党内部也有分歧,特别是在财政赤字问题上。马斯克多次公开批评该法案是"疯狂的支出计划"。`)
</script>
<style lang="scss" scoped>
.summary-box {
margin-left: 10px;
margin-bottom: 10px;
width: 1019px;
height: 453px;
.content-box{
width: 100%;
height: 100%;
background: var(--color-primary-2);
}
}
</style>
\ No newline at end of file
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
`}} `}}
</pre> </pre>
<div class="chart-box"> <div class="chart-box">
<GraphChart :nodes="nodes" :links="links" layoutType="none"> <GraphChart :nodes="nodes" :links="links" layoutType="force">
</GraphChart> </GraphChart>
</div> </div>
</el-col> </el-col>
...@@ -207,6 +207,7 @@ const links = ref([ ...@@ -207,6 +207,7 @@ const links = ref([
{ source: 15, target: 7, label: { show: true, formatter: '合作', color: 'red', borderColor: 'red' } }, { source: 15, target: 7, label: { show: true, formatter: '合作', color: 'red', borderColor: 'red' } },
]); ]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -218,5 +219,4 @@ const links = ref([ ...@@ -218,5 +219,4 @@ const links = ref([
width: 800px; width: 800px;
height: 500px; height: 500px;
} }
</style> </style>
\ No newline at end of file
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论