提交 ed1f2324 authored 作者: 刘宇琪's avatar 刘宇琪

merge pre

......@@ -14,6 +14,7 @@ lerna-debug.log*
# Dependencies
node_modules
*node_modules
.pnpm
.npm
......
stages:
- build
- deploy
# cache:
# key: "$CI_COMMIT_REF_SLUG"
# paths:
# - .npm/
build_pre:
stage: build
image: node:20-bullseye
tags:
- risk-monitor-frontend
only:
- pre
script:
- node -v
- npm -v
- npm config set cache .npm --global
- npm ci --prefer-offline --no-audit --no-fund
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
deploy_pre:
stage: deploy
image: alpine:3.20
tags:
- risk-monitor-frontend
only:
- 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
差异被折叠。
......@@ -26,6 +26,7 @@
"axios": "^1.12.2",
"d3": "^7.9.0",
"d3-cloud": "^1.2.7",
"dayjs": "^1.11.20",
"default-passive-events": "^4.0.0",
"echarts": "^5.4.3",
"echarts-liquidfill": "^3.1.0",
......
......@@ -29,6 +29,25 @@ import { ElMessage } from "element-plus";
const router = useRouter();
const route = useRoute();
import useTagsViewStore from '@/stores/tagsView.js'
const tagsViewStore = useTagsViewStore()
// 在路由全局守卫中处理
router.beforeEach((to, from, next) => {
// 路由允许添加标签(排除掉隐藏的布局页如 /404, /login 等)
if (to.path.includes('dataLibrary')) {
tagsViewStore.addView({
path: to.path,
name: to.name, // 对应组件的 name,用于缓存
meta: { ...to.meta },
active: true
})
}
next()
})
const isShowAiBox = ref(false);
......
......@@ -36,6 +36,47 @@ function parseChartInterpretationArray(buffer) {
throw new Error("无法解析图表解读 JSON 数组");
}
/**
* 从数组结果中提取可展示的解读文本
* @param {unknown[]} arr
* @returns {string}
*/
function pickInterpretationText(arr) {
if (!Array.isArray(arr) || arr.length === 0) {
return "";
}
const first = arr[0] || {};
return (
first["解读"] ||
first["interpretation"] ||
first["analysis"] ||
first["content"] ||
""
);
}
/**
* 从非标准 JSON 文本中兜底提取“解读”字段(兼容单引号/双引号)
* 示例:
* [{'图表标题': '数量变化趋势', '解读': 'xxx'}]
* [{"图表标题":"数量变化趋势","解读":"xxx"}]
* @param {string} text
* @returns {string}
*/
function extractInterpretationFromLooseText(text) {
const raw = String(text || "");
if (!raw) {
return "";
}
const reg =
/["']解读["']\s*:\s*["']([\s\S]*?)["']\s*(?:[,}\]])/;
const m = raw.match(reg);
if (!m || !m[1]) {
return "";
}
return String(m[1]).replace(/\\n/g, "\n").trim();
}
/**
* 图表解读(SSE 流式)
* @param {object} data - 请求体
......@@ -44,9 +85,15 @@ function parseChartInterpretationArray(buffer) {
* @returns {Promise<{data: unknown[]}>}
*/
export function getChartAnalysis(data, options = {}) {
const { onChunk } = options;
const onDelta =
typeof options?.onChunk === "function"
? options.onChunk
: typeof options?.onInterpretationDelta === "function"
? options.onInterpretationDelta
: null;
return new Promise((resolve, reject) => {
let buffer = "";
let latestInterpretation = "";
let settled = false;
const abortController = new AbortController();
......@@ -93,12 +140,23 @@ export function getChartAnalysis(data, options = {}) {
if (raw === "[DONE]") return;
let chunk = "";
// 后端返回格式示例:{"text":"```"} / {"text":"json\n[\n"}
// 兼容后端返回格式:
// - {"text":"```"} / {"text":"json\n[\n"}
// - {"type":"reasoning","chunk":"..."}(新格式)
try {
const msg = JSON.parse(raw);
if (msg && typeof msg === "object" && "text" in msg) {
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 ?? "");
buffer += chunk;
if (chunk) buffer += chunk;
} else {
chunk = raw;
buffer += raw;
......@@ -108,9 +166,18 @@ export function getChartAnalysis(data, options = {}) {
buffer += raw;
}
// 兜底:非标准 JSON(如单引号 Python 风格)时,尝试直接从文本提取“解读”
const looseInterpretation = extractInterpretationFromLooseText(raw);
if (looseInterpretation) {
latestInterpretation = looseInterpretation;
safeResolve({ data: [{ 解读: looseInterpretation }] });
abortController.abort();
return;
}
// 每收到一条消息即回调,用于流式渲染
if (chunk && typeof onChunk === "function") {
onChunk(chunk);
if (chunk && onDelta) {
onDelta(chunk);
}
// 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
......@@ -118,6 +185,10 @@ export function getChartAnalysis(data, options = {}) {
if (trimmed.endsWith("```")) {
try {
const arr = parseChartInterpretationArray(trimmed);
const interpretation = pickInterpretationText(arr);
if (interpretation) {
latestInterpretation = interpretation;
}
safeResolve({ data: arr });
abortController.abort();
} catch (_) { }
......@@ -126,8 +197,22 @@ export function getChartAnalysis(data, options = {}) {
onclose: () => {
try {
const arr = parseChartInterpretationArray(buffer);
const interpretation = pickInterpretationText(arr);
if (interpretation) {
latestInterpretation = interpretation;
}
safeResolve({ data: arr });
} catch (e) {
// 兜底:整体 buffer 不是标准 JSON(如单引号)时直接提取“解读”
const looseInterpretation = extractInterpretationFromLooseText(buffer);
if (looseInterpretation) {
safeResolve({ data: [{ 解读: looseInterpretation }] });
return;
}
if (latestInterpretation) {
safeResolve({ data: [{ 解读: latestInterpretation }] });
return;
}
safeReject(e);
}
},
......
......@@ -1156,6 +1156,15 @@
| -------- | -------- | ----- | -------- | -------- | ------ |
|areas|区域名称列表|query|false|array|string|
|researchTypeIds|研究类型ID列表|query|false|array|string|
|domainIds|科技领域 ID 列表(逗号分隔)|query|false|string||
|startDate|发布时间起 YYYY-MM-DD(与政策追踪发布时间逻辑一致)|query|false|string||
|endDate|发布时间止 YYYY-MM-DD|query|false|string||
|category|分类(如调查项目)|query|false|string||
|pageNum|页码|query|false|integer||
|pageSize|每页条数|query|false|integer||
|sortFun|排序|query|false|boolean||
|thinkTankId|智库 ID(详情页动态列表限定当前智库)|query|false|string||
|keyword|关键词搜索(智库动态)|query|false|string||
|token|Token Request Header|header|false|string||
......
......@@ -28,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() {
return request({
......
......@@ -393,13 +393,13 @@ export function getDomainDistribution(sanctionDate = "2025-11-11") {
* 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(
request({
method: "POST",
url: "/api/sanctionList/pageQuery",
data: {
typeName,
sanTypeId,
pageNum,
pageSize,
sanctionDate,
......
......@@ -392,6 +392,14 @@ export function getSingleSanctionEntitySupplyChain(params) {
});
}
// 单次制裁-深度挖掘-制裁实体信息
export function getSingleSanctionEntityInfo(id) {
return request({
method: "GET",
url: `/api/organization/sanInfo?orgId=${id}`,
});
}
// 单次制裁-深度挖掘-制裁实体股权信息
/**
* @param {Object} params
......@@ -550,10 +558,10 @@ export function getSingleSanctionEntityInternationalPaper(params) {
}
// 商业管制清单-CCL清单简介-基本信息
export function getCCLInfo() {
export function getCCLInfo(sanTypeId = 13) {
return request({
method: "GET",
url: `/api/sanctionList/baseInfo/ccl`
url: `/api/sanctionList/baseInfoById/${sanTypeId}`
});
}
......@@ -620,3 +628,11 @@ export function getCclQuery(data) {
data
});
}
// 商业管制清单-CCL清单列表-清单版本
export function getCCLVersionList() {
return request({
method: "GET",
url: `/api/ccl/version/dateList`
});
}
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) {
return request({
method: 'GET',
url: `/api/billImpactAnalysis/industry/company/${params.id}`,
url: `/api/billImpactAnalysis/industry/company`,
params,
})
}
......
// 智库概览信息
import request from "@/api/request.js";
import request, { getToken } from "@/api/request.js";
// 智库列表
export function getThinkTankList() {
......@@ -87,7 +87,11 @@ export function getHylyList() {
}
//获取智库报告
/**
* 智库概览/智库动态-智库报告、调查项目
* GET /api/thinkTankOverview/report
* 常用 query:pageNum, pageSize, sortFun, domainIds, startDate, endDate, category(调查项目), thinkTankId(详情页), keyword(动态搜索)
*/
export function getThinkTankReport(params) {
return request({
method: 'GET',
......@@ -158,6 +162,7 @@ export function getThinkDynamicsReport(params) {
// 智库领域观点分析(流式)
// [POST] 8.140.26.4:10029/report-domain-view-analysis
// 每次请求体:{ domain, report_view_list }(一个 domain);多领域由前端按领域循环多次调用
export function postReportDomainViewAnalysis(data) {
return request({
method: 'POST',
......@@ -167,6 +172,86 @@ export function postReportDomainViewAnalysis(data) {
})
}
/**
* 智库领域观点分析(真正流式,逐 chunk 回调)
* @param {object} data
* @param {{ onReasoningChunk?: (chunk: string) => void, onMessage?: (msg: any) => void }} handlers
*/
export async function postReportDomainViewAnalysisStream(data, handlers = {}) {
const { onReasoningChunk, onMessage } = handlers
const token = getToken()
const response = await fetch('/intelligent-api/report-domain-view-analysis', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { token } : {})
},
body: JSON.stringify(data)
})
if (!response.ok) {
throw new Error(`流式分析请求失败: ${response.status}`)
}
// 兜底:非流式返回时仍可读取文本继续后续解析
if (!response.body) {
return await response.text()
}
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let done = false
let pending = ''
let fullText = ''
while (!done) {
const result = await reader.read()
done = result.done
if (result.value) {
const chunkText = decoder.decode(result.value, { stream: !done })
fullText += chunkText
pending += chunkText
const lines = pending.split(/\r?\n/)
pending = lines.pop() ?? ''
for (const rawLine of lines) {
const line = String(rawLine || '').trim()
if (!line || !line.startsWith('data:')) continue
const jsonText = line.slice(5).trim()
if (!jsonText || jsonText === '[DONE]') continue
try {
const msg = JSON.parse(jsonText)
if (typeof onMessage === 'function') onMessage(msg)
if (msg?.type === 'reasoning' && msg?.chunk != null && typeof onReasoningChunk === 'function') {
const c = String(msg.chunk)
if (c) onReasoningChunk(c)
}
} catch (e) {
// 忽略非 JSON 数据行
}
}
}
}
// 处理最后一行残留
const last = String(pending || '').trim()
if (last.startsWith('data:')) {
const jsonText = last.slice(5).trim()
if (jsonText && jsonText !== '[DONE]') {
try {
const msg = JSON.parse(jsonText)
if (typeof onMessage === 'function') onMessage(msg)
if (msg?.type === 'reasoning' && msg?.chunk != null && typeof onReasoningChunk === 'function') {
const c = String(msg.chunk)
if (c) onReasoningChunk(c)
}
} catch (e) {
// ignore
}
}
}
return fullText
}
//提出建议领域分布
export function getThinkPolicyIndustry(params) {
......@@ -240,18 +325,12 @@ export function getThinkTankInfoBranch(params) {
})
}
//获取经费来源统计
export function getThinkTankFundsTotal(params) {
return request({
method: 'GET',
url: `/api/thinkTankInfo/fundsTotal/${params}`,
})
}
//获取经费来源
export function getThinkTankFundsSource(params) {
return request({
method: 'GET',
url: `/api/thinkTankInfo/fundsSource/${params}`,
url: `/api/thinkTankInfo/fundsStatistics/${params}`,
})
}
......@@ -265,9 +344,17 @@ export function getThinkTankResearchAreae(params) {
//获取核心研究人员
export function getThinkTankPerson(params) {
const { thinkTankId, currentPage, pageSize } = params
return request({
method: 'GET',
url: `/api/thinkTankInfo/person/${params}`,
url: `/api/thinkTankInfo/person/page`,
params: {
currentPage,
pageNum: currentPage,
page: currentPage,
pageSize,
thinkTankId
}
})
}
......
......@@ -10,8 +10,7 @@
</div>
<div
class="search-type-tab"
:class="{ active: billSearchType === 'state' }"
@click="handleChangeBillSearchType('state')"
:class="{ active: billSearchType === 'state', 'is-disabled': true }"
>
州议会
</div>
......@@ -206,6 +205,11 @@ const handleToPosi = id => {
color: rgb(5, 95, 194);
border-color: rgb(255, 255, 255);
}
.search-type-tab.is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
.search-main-with-tabs {
......
......@@ -24,7 +24,8 @@ const props = defineProps({
<style lang="scss">
.ai-pane-wrapper {
width: 100%;
height: 156px;
min-height: 156px;
height: auto;
background: var(--color-primary-2);
box-sizing: border-box;
padding: 12px 16px;
......@@ -57,18 +58,15 @@ const props = defineProps({
.content {
margin-top: 8px;
width: 100%;
height: 90px;
min-height: 90px;
height: auto;
box-sizing: border-box;
padding: 0 12px;
color: var(--color-primary-100);
display: -webkit-box;
-webkit-line-clamp: 3;
/* 控制显示的行数 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
display: block;
overflow: visible;
word-break: break-word;
/* 防止长单词溢出 */
white-space: pre-wrap;
}
}
</style>
\ No newline at end of file
......@@ -41,6 +41,12 @@ const getGraphChart = (nodes, links, layoutType) => {
itemStyle: {
color: '#73C0DE'
},
// 方法1:通过 left/right/top/bottom 控制绘图区域
left: '5%',
right: '5%',
top: '5%',
bottom: '5%',
layout: layoutType,
data: nodes,
links: links,
......
......@@ -52,5 +52,7 @@ onBeforeUnmount(() => {
.graph-chart-wrapper {
width: 100%;
height: 100%;
// width: 800px;
// height: 500px;
}
</style>
\ No newline at end of file
......@@ -3,13 +3,18 @@
<div class="icon">
<img src="./tip-icon.svg" alt="">
</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>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
text: {
type: String,
default: ''
},
dataSource: {
type: String,
default: '美国国会官网'
......@@ -21,6 +26,8 @@ const props = defineProps({
})
const tipText = computed(() => props.text || `数据来源:${props.dataSource},数据时间:${props.dataTime}`)
</script>
<style lang="scss" scoped>
......
<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' }"
:class="{
level1: warnningLevel === '特别重大风险',
level2: warnningLevel === '重大风险',
......@@ -27,12 +27,22 @@
</div>
</div>
<div class="warnning-pane-content text-regular">
{{ warnningContent }}
{{ showContent }}
<div class="show-all-btn" v-if="props.warnningContent.length > 185" @click.stop="handleClickShowAll">
<div class="text text-tip-2">
{{ isShowAllContent? '收起' : '展开' }}
</div>
<div class="icon">
<img v-if="!isShowAllContent" src="./icons/down.png" alt="">
<img v-else src="./icons/up.png" alt="">
</div>
</div>
</div>
</div>
</template>
<script setup>
import {ref, computed} from 'vue'
import { ElMessage } from 'element-plus'
......@@ -50,6 +60,14 @@ const props = defineProps(
}
)
const showContent = computed(() => {
if(!isShowAllContent.value) {
return props.warnningContent.length > 185 ? props.warnningContent.slice(0,185) + '...' : props.warnningContent
} else {
return props.warnningContent
}
})
const emit = defineEmits(['clickPane'])
const handleClickPane = () => {
......@@ -57,6 +75,12 @@ const handleClickPane = () => {
emit('clickPane')
}
const isShowAllContent = ref(false)
const handleClickShowAll = () => {
isShowAllContent.value = !isShowAllContent.value
}
</script>
<style lang="scss" scoped>
......@@ -64,7 +88,8 @@ const handleClickPane = () => {
border-radius: 10px;
border: 1px solid var(--color-primary-100);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
cursor: pointer;
// cursor: pointer;
height: max-content;
}
.level1 {
......@@ -146,15 +171,40 @@ const handleClickPane = () => {
.warnning-pane-content{
width: calc(100% - 40px);
margin: 0 auto;
height: 60px;
display: -webkit-box;
/* 2. 设置内部布局方向为垂直 */
-webkit-box-orient: vertical;
/* 3. 限制显示的行数为 2 行 */
-webkit-line-clamp: 2;
/* 4. 隐藏超出部分 */
overflow: hidden;
/* 5. 设置文本溢出显示省略号 */
text-overflow: ellipsis;
// height: 60px;
// display: -webkit-box;
// /* 2. 设置内部布局方向为垂直 */
// -webkit-box-orient: vertical;
// /* 3. 限制显示的行数为 2 行 */
// -webkit-line-clamp: 2;
// /* 4. 隐藏超出部分 */
// overflow: hidden;
// /* 5. 设置文本溢出显示省略号 */
// text-overflow: ellipsis;
position: relative;
.show-all-btn {
position: absolute;
right: 0px;
bottom: 4px;
width: 48px;
height: 22px;
display: flex;
gap: 4px;
cursor: pointer;
.text{
color: var(--text-primary-50-color);
&:hover{
color: var(--color-primary-100);
}
}
.icon{
width: 16px;
height: 16px;
img{
width: 100%;
height: 100%;
}
}
}
}
</style>
\ No newline at end of file
<template>
<p class="p-regular-rereg">
<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}`"
class="entity-link" target="_blank" rel="noopener noreferrer">
{{ segment.entity?.text_span }}
<img :src="SearchIcon" :width="10" :height="10" alt="search" />
</a>
<span v-else>
{{ segment.text }}
</span>
</span>
</p>
<p class="p-regular-rereg">
<span class="text-regular" v-for="(segment, index) in processedText" :key="index">
<span v-if="segment.isEntity" @click="$emit('onEntityClick', segment.entity)" class="entity-link">
{{ segment.entity?.text_span }}
<img :src="SearchIcon" :width="10" :height="10" alt="search" />
</span>
<span v-else>
{{ segment.text }}
</span>
</span>
</p>
</template>
<script lang="ts" setup>
import { TextEntity } from '@/api/intelligent';
import { ref, watch, onMounted } from 'vue';
import SearchIcon from './images/search.png'
import { TextEntity } from "@/api/intelligent";
import { ref, watch, onMounted } from "vue";
import SearchIcon from "./images/search.png";
export interface ProcessedTextSegment {
text: string
isEntity: boolean
entity?: TextEntity
text: string;
isEntity: boolean;
entity?: TextEntity;
}
const props = defineProps({
text: {
type: String,
default: ''
},
entities: {
type: Array<TextEntity>,
default: () => []
}
})
text: {
type: String,
default: ""
},
entities: {
type: Array<TextEntity>,
default: () => []
}
});
const emit = defineEmits(["onEntityClick"]);
// 处理后的文本段
const processedText = ref<ProcessedTextSegment[]>([])
const processedText = ref<ProcessedTextSegment[]>([]);
// 处理文本,识别并替换实体
const processText = () => {
console.log('props.entities.length', props.entities.length)
if (!props.text || !props.entities) {
// console.log('props.text', props.entities.length)
processedText.value = [{ text: '', isEntity: false }]
return
}
console.log("props.entities.length", props.entities.length);
if (!props.text || !props.entities) {
// console.log('props.text', props.entities.length)
processedText.value = [{ text: "", isEntity: false }];
return;
}
const result = []
let currentPosition = 0
const result = [];
let currentPosition = 0;
// 按实体文本长度排序,优先匹配长文本
const sortedEntities = [...props.entities].sort((a, b) =>
b.text_span.length - a.text_span.length
)
// 按实体文本长度排序,优先匹配长文本
const sortedEntities = [...props.entities].sort((a, b) => b.text_span.length - a.text_span.length);
while (currentPosition < props.text.length) {
let matched = false
while (currentPosition < props.text.length) {
let matched = false;
for (const entity of sortedEntities) {
const entityText = entity.text_span
const endPosition = currentPosition + entityText.length
for (const entity of sortedEntities) {
const entityText = entity.text_span;
const endPosition = currentPosition + entityText.length;
if (props.text.substring(currentPosition, endPosition) === entityText) {
// 如果当前位置是实体,添加到结果
result.push({
isEntity: true,
entity: { ...entity }
})
currentPosition = endPosition
matched = true
break
}
}
if (props.text.substring(currentPosition, endPosition) === entityText) {
// 如果当前位置是实体,添加到结果
result.push({
isEntity: true,
entity: { ...entity }
});
currentPosition = endPosition;
matched = true;
break;
}
}
if (!matched) {
// 如果不是实体,收集普通文本
let nextEntityStart = props.text.length
if (!matched) {
// 如果不是实体,收集普通文本
let nextEntityStart = props.text.length;
for (const entity of sortedEntities) {
const pos = props.text.indexOf(entity.text_span, currentPosition)
if (pos !== -1 && pos < nextEntityStart) {
nextEntityStart = pos
}
}
for (const entity of sortedEntities) {
const pos = props.text.indexOf(entity.text_span, currentPosition);
if (pos !== -1 && pos < nextEntityStart) {
nextEntityStart = pos;
}
}
if (nextEntityStart > currentPosition) {
const plainText = props.text.substring(currentPosition, nextEntityStart)
result.push({
text: plainText,
isEntity: false
})
currentPosition = nextEntityStart
} else {
// 没有更多实体,添加剩余文本
const remainingText = props.text.substring(currentPosition)
if (remainingText) {
result.push({
text: remainingText,
isEntity: false
})
}
currentPosition = props.text.length
}
}
}
if (nextEntityStart > currentPosition) {
const plainText = props.text.substring(currentPosition, nextEntityStart);
result.push({
text: plainText,
isEntity: false
});
currentPosition = nextEntityStart;
} else {
// 没有更多实体,添加剩余文本
const remainingText = props.text.substring(currentPosition);
if (remainingText) {
result.push({
text: remainingText,
isEntity: false
});
}
currentPosition = props.text.length;
}
}
}
processedText.value = result
}
processedText.value = result;
};
// 监听文本和实体变化
watch(() => props.text, processText)
watch(() => props.entities, processText, { deep: true })
watch(() => props.text, processText);
watch(() => props.entities, processText, { deep: true });
// 初始化处理
onMounted(processText)
onMounted(processText);
</script>
<style lang="scss" scoped>
@use '@/styles/common.scss';
@use "@/styles/common.scss";
.entity-link {
color: var(--color-primary-100);
color: var(--color-primary-100);
&:hover {
cursor: pointer;
}
}
.p-regular-rereg {
text-indent: 2em;
margin: 4px 0;
text-indent: 2em;
margin: 4px 0;
}
</style>
\ No newline at end of file
</style>
<template>
<div class="full-width">
<div class="flex-display" style="align-items: center;">
<common-text class="text-title-3-bold" color="var(--text-primary-80-color)">{{ isOpenTranslation
? '中文' : '原文' }}</common-text>
<div class="flex-fill" style="margin: 0 10px;">
<el-divider></el-divider>
</div>
<el-button v-if="showMoreVisible" @click="() => { showMore = !showMore; updateText() }">
{{ showMore ? '收起' : '展开' }}
<el-icon>
<arrow-up v-if="showMore" />
<arrow-down v-else />
</el-icon>
</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"
:entities="isHighlightEntity ? textEntities : []"></intelligent-entity-text>
</el-col>
</el-row>
</div>
<div class="full-width">
<div class="flex-display" style="align-items: center">
<common-text class="text-title-3-bold" color="var(--text-primary-80-color)">{{
isOpenTranslation ? "中文" : "原文"
}}</common-text>
<div class="flex-fill" style="margin: 0 10px">
<el-divider></el-divider>
</div>
<el-button
v-if="showMoreVisible"
@click="
() => {
showMore = !showMore;
updateText();
}
"
>
{{ showMore ? "收起" : "展开" }}
<el-icon>
<arrow-up v-if="showMore" />
<arrow-down v-else />
</el-icon>
</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>
<script lang="ts" setup>
import '@/styles/container.scss';
import '@/styles/common.scss';
import "@/styles/container.scss";
import "@/styles/common.scss";
import { ref, watch, onMounted } from 'vue';
import { TextEntity } from '@/api/intelligent';
import IntelligentEntityText from '@/components/base/texts/IntelligentEntityText.vue';
import { ElIcon, ElButton, ElDivider, ElRow, ElCol } from 'element-plus';
import CommonText from './CommonText.vue';
import { ref, watch, onMounted } from "vue";
import { TextEntity } from "@/api/intelligent";
import IntelligentEntityText from "@/components/base/texts/IntelligentEntityText.vue";
import { ElIcon, ElButton, ElDivider, ElRow, ElCol } from "element-plus";
import CommonText from "./CommonText.vue";
const allTexts = ref([]);
const textColSpan = ref(12);
......@@ -39,71 +51,73 @@ const hasTranslation = ref(false);
const showMore = ref(false);
const showMoreVisible = ref(false);
const props = defineProps({
//段落列表: 原始文本
textsRaw: {
type: Array<String>,
default: () => []
},
//段落列表: 翻译文本
textsTranslate: {
type: Array<String>,
default: () => []
},
//是否显示翻译
isOpenTranslation: {
type: Boolean,
default: true
},
//是否高亮实体
isHighlightEntity: {
type: Boolean,
default: true
},
//实体列表
textEntities: {
type: Array<TextEntity>,
default: () => []
}
})
//段落列表: 原始文本
textsRaw: {
type: Array<String>,
default: () => []
},
//段落列表: 翻译文本
textsTranslate: {
type: Array<String>,
default: () => []
},
//是否显示翻译
isOpenTranslation: {
type: Boolean,
default: true
},
//是否高亮实体
isHighlightEntity: {
type: Boolean,
default: true
},
//实体列表
textEntities: {
type: Array<TextEntity>,
default: () => []
}
});
const emit = defineEmits(["onEntityClick"]);
function updateText() {
const tempTexts = []
const tempRaws = props.textsRaw ?? []
const tempTranslates = props.textsTranslate ?? []
hasTranslation.value = tempTranslates.length > 0
const tempTexts = [];
const tempRaws = props.textsRaw ?? [];
const tempTranslates = props.textsTranslate ?? [];
hasTranslation.value = tempTranslates.length > 0;
if (hasTranslation.value && props.isOpenTranslation) {
// 遍历原始文本和翻译文本,将它们交替添加到 tempTexts 中,并保持原始文本和翻译文本的的数量一致
const maxCount = Math.max(tempRaws.length, tempTranslates.length)
for (let i = 0; i < maxCount; i++) {
if (i < tempTranslates.length) {
tempTexts.push(tempTranslates[i]);
} else {
tempTexts.push('');
}
if (i < tempRaws.length) {
tempTexts.push(tempRaws[i]);
} else {
tempTexts.push('');
}
}
console.log(tempTexts.length)
textColSpan.value = 12;
showMoreVisible.value = tempTexts.length > 6;
allTexts.value = showMore.value ? tempTexts : tempTexts.slice(0, 6);
} else {
textColSpan.value = 24;
showMoreVisible.value = tempRaws.length > 3;
allTexts.value = showMore.value ? tempRaws : tempRaws.slice(0, 3);
}
if (hasTranslation.value && props.isOpenTranslation) {
// 遍历原始文本和翻译文本,将它们交替添加到 tempTexts 中,并保持原始文本和翻译文本的的数量一致
const maxCount = Math.max(tempRaws.length, tempTranslates.length);
for (let i = 0; i < maxCount; i++) {
if (i < tempTranslates.length) {
tempTexts.push(tempTranslates[i]);
} else {
tempTexts.push("");
}
if (i < tempRaws.length) {
tempTexts.push(tempRaws[i]);
} else {
tempTexts.push("");
}
}
console.log(tempTexts.length);
textColSpan.value = 12;
showMoreVisible.value = tempTexts.length > 6;
allTexts.value = showMore.value ? tempTexts : tempTexts.slice(0, 6);
} else {
textColSpan.value = 24;
showMoreVisible.value = tempRaws.length > 3;
allTexts.value = showMore.value ? tempRaws : tempRaws.slice(0, 3);
}
}
watch(() => [props.textsRaw, props.textsTranslate, props.isOpenTranslation], () => {
updateText();
})
watch(
() => [props.textsRaw, props.textsTranslate, props.isOpenTranslation],
() => {
updateText();
}
);
onMounted(() => {
updateText();
})
</script>
\ No newline at end of file
updateText();
});
</script>
......@@ -13,15 +13,15 @@ import '@/assets/fonts/font.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
// import AreaTag from '@/components/base/AreaTag/index.vue'
// import LeftBtn from "@/components/base/PageBtn/LeftBtn.vue";
// import RightBtn from "@/components/base/PageBtn/RightBtn.vue";
// import OverviewMainBox from "@/components/base/BoxBackground/OverviewMainBox.vue";
// import OverviewNormalBox from "@/components/base/BoxBackground/OverviewNormalBox.vue";
// import AnalysisBox from '@/components/base/BoxBackground/AnalysisBox.vue'
// import NewsList from '@/components/base/NewsList/index.vue'
// import LeftBtn from "@/components/base/pageBtn/leftBtn.vue";
// import RightBtn from "@/components/base/pageBtn/rightBtn.vue";
// import OverviewMainBox from "@/components/base/boxBackground/overviewMainBox.vue";
// import OverviewNormalBox from "@/components/base/boxBackground/overviewNormalBox.vue";
// import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'
// import NewsList from '@/components/base/newsList/index.vue'
// import ModuleHeader from '@/components/base/ModuleHeader/index.vue'
// import RiskSignal from "@/components/base/RiskSignal/index.vue";
// import MessageBubble from "@/components/base/MessageBubble/index.vue";
// import RiskSignal from "@/components/base/riskSignal/index.vue";
// import MessageBubble from "@/components/base/messageBubble/index.vue";
// import SourceTabLsit from '@/components/base/SourceTabList/index.vue'
// import ActionButton from "@/components/base/ActionButton/index.vue"
......
......@@ -65,4 +65,5 @@ router.beforeEach((to, from, next) => {
next();
});
export default router;
//企业主页
import { useGotoPage } from "../common.js";
const companyPages = () => import('@/views/companyPages/index.vue')
const companyPages2 = () => import('@/views/companyPages2/index.vue')
......@@ -28,4 +29,9 @@ const companyPagesRoutes = [
]
export function useGotoCompanyPages() {
const gotoPage = useGotoPage();
return (id, isNewTabs = true) => gotoPage("/companyPages/" + id, {}, isNewTabs)
}
export default companyPagesRoutes
\ No newline at end of file
// 综合搜索
const ComprehensiveSearch = () => import('@/views/comprehensiveSearch/index.vue')
const SearchResults = () => import('@/views/comprehensiveSearch/searchResults/index.vue')
const Chat = () => import('@/views/comprehensiveSearch/chat/index.vue')
const ComprehensiveSearch = () => import("@/views/comprehensiveSearch/index.vue");
const SearchResults = () => import("@/views/comprehensiveSearch/searchResults/index.vue");
const Chat = () => import("@/views/comprehensiveSearch/chat/index.vue");
const comprehensiveSearchRoutes = [
// 综合搜索
{
path: "/comprehensiveSearch",
name: "comprehensiveSearch",
component: ComprehensiveSearch,
meta: {
title: "搜索-科技安全"
}
},
{
path: "/searchResults",
name: "searchResults",
component: SearchResults,
meta: {
title: "搜索结果",
dynamicTitle: true
}
},
{
path: "/chat",
name: "chat",
component: Chat,
meta: {
title: "智能问答"
}
},
]
// 综合搜索
{
path: "/comprehensiveSearch",
name: "comprehensiveSearch",
component: ComprehensiveSearch,
meta: {
title: "搜索-科技安全"
}
},
{
path: "/searchResults",
name: "searchResults",
component: SearchResults,
meta: {
title: "搜索结果",
dynamicTitle: true
}
},
{
path: "/chat",
name: "chat",
component: Chat,
meta: {
title: "智能问答"
}
}
];
import { useGotoPage } from "../common.js";
export function useGotoComprehensiveSearch() {
const gotoPage = useGotoPage();
return (isNewTabs = true) => gotoPage("/comprehensiveSearch/", {}, isNewTabs)
const gotoPage = useGotoPage();
return (isNewTabs = true) => gotoPage("/comprehensiveSearch/", {}, isNewTabs);
}
export function useGotoSearchResults() {
const gotoPage = useGotoPage();
return (isNewTabs = true) => gotoPage("/searchResults/", {searchText, areaName}, isNewTabs)
const gotoPage = useGotoPage();
return (searchText, areaName, isNewTabs = true) => gotoPage("/searchResults/", { searchText, areaName }, isNewTabs);
}
export default comprehensiveSearchRoutes
\ No newline at end of file
export default comprehensiveSearchRoutes;
......@@ -120,7 +120,7 @@ const exportControlRoutes = [
name: "commercialControlList",
component: () => import("@/views/exportControl/v2.0CommercialControlList/index.vue"),
meta: {
title: "全部实体清单"
title: "商业管制清单"
}
}
]
......
......@@ -27,8 +27,8 @@
<pre>
{{
`
import LeftBtn from '@/components/base/PageBtn/LeftBtn.vue'
import RightBtn from '@/components/base/PageBtn/RightBtn.vue'
import LeftBtn from '@/components/base/pageBtn/leftBtn.vue'
import RightBtn from '@/components/base/pageBtn/rightBtn.vue'
<LeftBtn />
<RightBtn />
......
......@@ -11,7 +11,7 @@
`}}
</pre>
<div class="chart-box">
<GraphChart :nodes="nodes" :links="links" layoutType="none">
<GraphChart :nodes="nodes" :links="links" layoutType="force">
</GraphChart>
</div>
</el-col>
......@@ -207,6 +207,7 @@ const links = ref([
{ source: 15, target: 7, label: { show: true, formatter: '合作', color: 'red', borderColor: 'red' } },
]);
</script>
<style lang="scss" scoped>
......@@ -218,5 +219,4 @@ const links = ref([
width: 800px;
height: 500px;
}
</style>
\ No newline at end of file
// 使用 day.js(推荐,轻量级)
import dayjs from 'dayjs';
// 获取日期范围
const getDateRange = (type) => {
const endDate = dayjs(); // 当前日期
let startDate;
switch(type) {
case '近一月':
startDate = dayjs().subtract(1, 'month');
break;
case '近三月':
startDate = dayjs().subtract(3, 'month');
break;
case '近半年':
startDate = dayjs().subtract(6, 'month');
break;
case '近一年':
startDate = dayjs().subtract(1, 'year');
break;
default:
return null;
}
return [
startDate.format('YYYY-MM-DD'),
endDate.format('YYYY-MM-DD')
];
}
export default getDateRange
// 使用示例
// console.log(getDateRange('近一个月')); // ['2026-02-25', '2026-03-25']
// console.log(getDateRange('近三个月')); // ['2025-12-25', '2026-03-25']
// console.log(getDateRange('近半年')); // ['2025-09-25', '2026-03-25']
// console.log(getDateRange('近一年')); // ['2025-03-25', '2026-03-25']
\ No newline at end of file
const getMonthRange = (yearMonth) => {
// yearMonth 格式:'2026-03'
const [year, month] = yearMonth.split('-');
// 当月第一天
const firstDay = `${year}-${month}-01`;
// 当月最后一天:先获取下个月的第0天,即当月的最后一天
const lastDay = new Date(Number(year), Number(month), 0);
const lastDayStr = `${year}-${month}-${lastDay.getDate()}`;
return [firstDay, lastDayStr];
}
export default getMonthRange
// 绘制echarts图表
import getMonthRange from './getMonthRange'
import * as echarts from 'echarts'
import 'echarts-wordcloud';
const setChart = (option, chartId) => {
import router from '@/router/index'
const setChart = (option, chartId, allowClick, selectParam) => {
let chartDom = document.getElementById(chartId);
if (!chartDom) {
return null;
......@@ -9,6 +11,65 @@ const setChart = (option, chartId) => {
chartDom.removeAttribute("_echarts_instance_");
let chart = echarts.init(chartDom);
chart.setOption(option);
if (allowClick) {
chart.on('click', function (params) {
switch (selectParam.moduleType) {
case '国会法案':
if(selectParam.selectedDate && selectParam.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate+'-01-01',selectParam.selectedDate+'-12-31'])
}
// 判断点击的是否为饼图的数据项
if (params.componentType === 'series' && params.seriesType === 'pie') {
console.log('点击的扇形名称:', params.name);
if (selectParam.key === '领域') {
selectParam.domains = params.name
} else if (selectParam.key === '议院委员会') {
if (params.name === '众议院' || params.name === '参议院') {
selectParam.selectedCongress = params.name
selectParam.selectedOrg = ''
} else {
selectParam.selectedOrg = params.name
selectParam.selectedCongress = ''
}
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') {
if (params.name === '已立法') {
selectParam.selectedStauts = 1
} else {
selectParam.selectedStauts = 0
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
} else {
console.log('当前点击', selectParam, params.seriesName, params.name);
if (params.seriesName !== '通过率') {
selectParam = {
selectedDate: JSON.stringify(getMonthRange(params.name)),
status: params.seriesName === '通过法案' ? 1 : 0,
...selectParam
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
}
}
}
});
}
// 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致
setTimeout(() => {
chart.resize();
......
......@@ -88,7 +88,7 @@
<div class="main">
<div class="item"><div class="item-left">提案人:</div><div class="item-right">{{ item.tcr }}</div></div>
<div class="item"><div class="item-left">委员会:</div><div class="item-right">{{ item.wyh }}</div></div>
<div class="item"><div class="item-left">相关领域:</div><div class="item-right1"><div class="tag" v-for="(val, idx) in item.areaList" :key="`${item.billId}-${val}-${idx}`">{{ val }}</div></div></div>
<div class="item"><div class="item-left">相关领域:</div><div class="item-right1"><AreaTag v-for="(val, idx) in item.areaList" :key="`${item.billId}-${val}-${idx}`" :tagName="val" /></div></div>
<div class="item"><div class="item-left">最新动议:</div><div class="item-right"><CommonPrompt :content="item.zxdy" /></div></div>
<div class="item">
<div class="item-left">法案进展:</div>
......@@ -260,6 +260,7 @@
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { getHylyList, getPostOrgList, getPostMemberList, getBills, getBillsPerson, getBillsPersonRel, getBillsIsCnCommittee } from "@/api/bill/billHome";
import { getPersonSummaryInfo } from "@/api/common/index";
import CommonPrompt from "../commonPrompt/index.vue";
import desc from "./assets/icons/icon-desc.png";
import defaultAvatar from "@/assets/icons/default-icon1.png";
......@@ -268,6 +269,7 @@ import zyyIcon from "@/assets/icons/zyy.png";
import cyyIcon from "@/assets/icons/cyy.png";
import ghdIcon from "@/assets/icons/ghd.png";
import mzdIcon from "@/assets/icons/mzd.png";
import { ElMessage } from "element-plus";
const router = useRouter();
......@@ -306,7 +308,7 @@ const handleClickTab = tab => {
};
// sortFun: true 正序 / false 倒序(法案接口字段)
const releaseTime = ref(true);
const releaseTime = ref(false);
const releaseTimeList = ref([
{ label: "发布时间正序", value: true },
{ label: "发布时间倒序", value: false }
......@@ -405,16 +407,48 @@ const handleBillImageError = e => {
img.src = defaultBill;
};
const handleClickAvatar = member => {
const handleClickAvatar = async member => {
if (!member?.id) return;
window.sessionStorage.setItem("curTabName", member.name || "");
const routeData = router.resolve({
path: "/characterPage",
query: {
personId: member.id
const personTypeList = JSON.parse(window.sessionStorage.getItem("personTypeList") || "[]");
let type = 0;
let personTypeName = "";
const params = {
personId: member.id
};
try {
const res = await getPersonSummaryInfo(params);
if (res.code === 200 && res.data) {
const arr = personTypeList.filter(item => item.typeId === res.data.personType);
if (arr && arr.length > 0) {
personTypeName = arr[0].typeName;
if (personTypeName === "科技企业领袖") {
type = 1;
} else if (personTypeName === "国会议员") {
type = 2;
} else if (personTypeName === "智库研究人员") {
type = 3;
} else {
personTypeName = "";
ElMessage.warning("找不到当前人员的类型值!");
return;
}
window.sessionStorage.setItem("curTabName", member.name || "");
const routeData = router.resolve({
path: "/characterPage",
query: {
type,
personId: member.id
}
});
window.open(routeData.href, "_blank");
} else {
personTypeName = "";
ElMessage.warning("找不到当前人员的类型值!");
}
} else {
ElMessage.warning("找不到当前人员的类型值!");
}
});
window.open(routeData.href, "_blank");
} catch (error) {}
};
const getReversedProgress = progress => (Array.isArray(progress) ? [...progress].reverse() : []);
......@@ -628,10 +662,10 @@ const handleGetBillsPerson = async () => {
sortFun: !memberSortFun.value
};
if (footerSelect1.value !== "全部委员会") params.committeeId = footerSelect1.value;
if (!activeYyList.value.includes("全部议院")) params.congressIds = activeYyList.value;
if (!activeDpList.value.includes("全部党派")) params.partyIds = activeDpList.value;
if (!activeAreaList.value.includes("全部领域")) params.domainIds = activeAreaList.value;
if (footerSelect1.value !== "全部委员会") params.committeeIds = footerSelect1.value;
if (!activeYyList.value.includes("全部议院")) params.congressIds = activeYyList.value.join(",");
if (!activeDpList.value.includes("全部党派")) params.partyIds = activeDpList.value.join(",");
if (!activeAreaList.value.includes("全部领域")) params.domainIds = activeAreaList.value.join(",");
const formatDateYm = dateStr => {
if (!dateStr) return "";
......
import { MUTICHARTCOLORS } from '../../../../common/constant'
const truncateLabel = (value, maxLen = 6) => {
if (value === null || value === undefined) return ''
const str = String(value)
......@@ -8,8 +10,9 @@ const truncateLabel = (value, maxLen = 6) => {
const getPieChart = (data, colorList, options = {}) => {
const showCount = options.showCount !== false
const chartColors = Array.isArray(colorList) && colorList.length ? colorList : MUTICHARTCOLORS
let option = {
// color: colorList,
color: chartColors,
tooltip: showCount
? undefined
: {
......
......@@ -72,12 +72,12 @@ const mainHeaderBtnList = ref([
name: "影响分析",
path: "/billLayout/influence"
},
{
icon: icon4,
activeIcon: icon4Active,
name: "相关情况",
path: "/billLayout/relevantCircumstance"
}
// {
// icon: icon4,
// activeIcon: icon4Active,
// name: "相关情况",
// path: "/billLayout/relevantCircumstance"
// }
]);
const activeTitle = ref("法案概况");
......
......@@ -32,19 +32,28 @@
</div>
</div> -->
<AnalysisBox title="典型阶段耗时">
<div class="box1-main" :class="{ 'box1-main--full': !timeFooterText }">
<div class="box1-main-center" id="chart1"></div>
<div v-if="timeFooterText" class="box1-main-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{ timeFooterText }}
</div>
<div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
<div class="analysis-ai-wrapper analysis-ai-wrapper--box1">
<div class="box1-main" :class="{ 'box1-main--full': !timeFooterText }">
<div class="box1-main-center" id="chart1"></div>
<div v-if="timeFooterText" class="box1-main-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{ timeFooterText }}
</div>
<div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
</div>
</div>
</div>
<div v-if="!aiPaneVisible.box1" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box1')" />
</div>
<div v-if="aiPaneVisible.box1" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box1')">
<AiPane :aiContent="overviewAiContent.box1" />
</div>
</div>
</AnalysisBox>
</div>
......@@ -80,19 +89,28 @@
</div>
</div> -->
<AnalysisBox title="修正案次数分析">
<div class="box2-main" :class="{ 'box2-main--full': !amendFooterText }">
<div class="box2-main-center" id="chart2"></div>
<div v-if="amendFooterText" class="box2-main-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{ amendFooterText }}
</div>
<div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
<div class="analysis-ai-wrapper analysis-ai-wrapper--box2">
<div class="box2-main" :class="{ 'box2-main--full': !amendFooterText }">
<div class="box2-main-center" id="chart2"></div>
<div v-if="amendFooterText" class="box2-main-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{ amendFooterText }}
</div>
<div class="box-footer-right">
<img src="../assets/icons/arrow-right.png" alt="" />
</div>
</div>
</div>
<div v-if="!aiPaneVisible.box2" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box2')" />
</div>
<div v-if="aiPaneVisible.box2" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box2')">
<AiPane :aiContent="overviewAiContent.box2" />
</div>
</div>
</AnalysisBox>
</div>
......@@ -366,7 +384,8 @@
</div>
</div> -->
<AnalysisBox title="投票分析">
<div class="vote-legend">
<div class="analysis-ai-wrapper analysis-ai-wrapper--box3">
<div class="vote-legend">
<div class="vote-legend-item">
<span class="vote-legend-dot agree"></span>
<span>赞成票</span>
......@@ -375,7 +394,7 @@
<span class="vote-legend-dot against"></span>
<span>反对票</span>
</div>
</div>
</div>
<div class="box3-main" :class="{ 'box3-main--full': !voteFooterText }">
<div class="box3-main-center">
<div class="box3-main-center-header">
......@@ -678,7 +697,15 @@
<img src="../assets/icons/arrow-right.png" alt="" />
</div>
</div>
<div v-if="!aiPaneVisible.box3" class="analysis-ai-tip-row">
<TipTab class="analysis-ai-tip" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box3')" />
</div>
<div v-if="aiPaneVisible.box3" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box3')">
<AiPane :aiContent="overviewAiContent.box3" />
</div>
</div>
</div>
</AnalysisBox>
</div>
</div>
......@@ -690,6 +717,10 @@ import { ref, onMounted } from "vue";
import { getBillTimeAnalyze, getBillAmeAnalyzeCount, getBillTp } from "@/api/deepdig";
import getBoxPlotChcart from "./utils/boxplot";
import * as echarts from "echarts";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import TipTab from "@/components/base/TipTab/index.vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import icon1 from "./assets/images/icon1.png";
import icon2 from "./assets/images/icon2.png";
......@@ -895,6 +926,31 @@ const timeFooterText = ref("");
const amendFooterText = ref("");
const voteFooterText = ref("");
// AI面板显示状态(box1=典型阶段耗时,box2=修正案次数分析,box3=投票分析)
const aiPaneVisible = ref({
box1: false,
box2: false,
box3: false
});
const overviewAiContent = ref({
box1: "智能总结生成中...",
box2: "智能总结生成中...",
box3: "智能总结生成中..."
});
const aiPaneFetched = ref({
box1: false,
box2: false,
box3: false
});
const aiPaneLoading = ref({
box1: false,
box2: false,
box3: false
});
// 绘制echarts图表
const setChart = (option, chartId) => {
let chartDom = document.getElementById(chartId);
......@@ -991,6 +1047,96 @@ const handleGetBillVoteAnalyze = async () => {
}
};
const buildAiChartPayload = key => {
if (key === "box1") {
return {
type: "箱线图",
name: "典型阶段耗时",
data: {
categories: Array.isArray(chartData1.value?.dataX) ? chartData1.value.dataX : [],
samples: Array.isArray(chartData1.value?.dataY) ? chartData1.value.dataY : []
}
};
}
if (key === "box2") {
return {
type: "箱线图",
name: "修正案次数分析",
data: {
categories: Array.isArray(chartData2.value?.dataX) ? chartData2.value.dataX : [],
samples: Array.isArray(chartData2.value?.dataY) ? chartData2.value.dataY : []
}
};
}
if (key === "box3") {
return {
type: "投票分析",
name: "投票分析",
data: Array.isArray(voteAnalysisList.value) ? voteAnalysisList.value : []
};
}
return { type: "", name: "", data: [] };
};
const requestAiPaneContent = async key => {
if (!key || aiPaneLoading.value[key] || aiPaneFetched.value[key]) return;
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: true };
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成中..." };
try {
const payload = buildAiChartPayload(key);
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
overviewAiContent.value = {
...overviewAiContent.value,
[key]: base + chunk
};
}
}
);
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
const interpretation = first?.解读 || first?.["解读"];
if (interpretation) {
overviewAiContent.value = {
...overviewAiContent.value,
[key]: interpretation
};
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
aiPaneLoading.value = { ...aiPaneLoading.value, [key]: false };
}
};
const handleShowAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: true
};
requestAiPaneContent(key);
};
const handleHideAiPane = key => {
aiPaneVisible.value = {
...aiPaneVisible.value,
[key]: false
};
};
onMounted(async () => {
await handleGetBillTimeAnalyze();
await handleGetBillAmeAnalyzeCount();
......@@ -1916,4 +2062,38 @@ onMounted(async () => {
width: 200px;
margin-left: 10px;
}
.analysis-ai-wrapper {
position: relative;
height: 100%;
}
.analysis-ai-tip-row {
position: absolute;
left: 0;
bottom: 15px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.analysis-ai-tip-action {
position: absolute;
right: 0px;
}
.analysis-ai-pane {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
z-index: 5;
pointer-events: none;
:deep(.ai-pane-wrapper) {
pointer-events: auto;
}
}
</style>
const resolveCssVarColor = (varName, fallback) => {
try {
if (typeof window === 'undefined' || typeof document === 'undefined') return fallback
const value = window.getComputedStyle(document.documentElement).getPropertyValue(varName)
const trimmed = value ? value.trim() : ''
return trimmed || fallback
} catch (e) {
return fallback
}
}
const getBoxPlotChcart = (data, unit, labelConfig = {}) => {
const primary2 = resolveCssVarColor('--color-primary-2', '#F6FAFF')
const labels = {
max: labelConfig.max || '最大耗时',
q3: labelConfig.q3 || '平均耗时大',
......@@ -16,6 +29,19 @@ const getBoxPlotChcart = (data, unit, labelConfig = {}) => {
// left: 'center'
// }
// ],
graphic: [
{
type: 'text',
// 左上角只标注一次单位(y轴刻度不再逐行带单位)
left: '5%',
top: '0%',
style: {
text: unit,
fill: 'rgba(95, 101, 108, 1)',
font: '14px Microsoft YaHei'
}
}
],
tooltip: {
trigger: 'item',
axisPointer: {
......@@ -61,10 +87,14 @@ const getBoxPlotChcart = (data, unit, labelConfig = {}) => {
type: 'value',
name: '',
axisLabel: {
formatter: (value) => `${value}${unit}`
formatter: (value) => `${value}`
},
splitArea: {
show: true
show: true,
// ECharts绘制到canvas,不能直接识别CSS变量字符串;这里取到真实颜色值后再配置交替背景
areaStyle: {
color: [primary2, '#ffffff']
}
}
},
series: [
......
......@@ -16,6 +16,12 @@ import { onMounted, ref, computed, watch } from "vue";
import { useRoute } from "vue-router";
import router from '@/router'
import SiderTabs from "@/components/base/SiderTabs/index.vue";
//分支pre自动部署测试:
//第一次测试
//第二次测试
//第三次测试
//第四次测试
//第五次测试
const route = useRoute();
......
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论