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

fix:法案边界测试 样式回归

上级 f8f89f13
流水线 #506 已通过 于阶段
in 1 分 54 秒
# characterPage 代码风险审查计划
## 审查范围
- `src/views/characterPage/index.vue` - 入口页
- `src/views/characterPage/components/unified/index.vue` - 统一布局
- `src/views/characterPage/components/thinkTankPerson/index.vue` - 智库人员
- `src/views/characterPage/components/techLeader/index.vue` - 科技领袖
- `src/views/characterPage/components/memberOfCongress/index.vue` - 国会议员
- `src/views/characterPage/components/*/components/characterRelationships/index.vue` - 人物关系
- `src/views/characterPage/components/*/components/relevantSituation/index.vue` - 相关情况
- `src/views/characterPage/components/thinkTankPerson/components/historicalProposal/components/NewsTracker.vue` - 新闻追踪(参考实现)
- `src/api/characterPage/characterPage.js` - API 层
---
## 一、样式溢出问题 (CRITICAL/HIGH)
### C1. 全模块固定 1600px 宽度 — 水平溢出 [CRITICAL]
**影响文件**: thinkTankPerson:467, techLeader, memberOfCongress, characterRelationships:467, relevantSituation:499
所有组件使用 `width: 1600px` 固定宽度,小于此宽度的屏幕会水平溢出。
```scss
// 当前代码
.think-tank-person {
width: 1600px; // 固定宽度
margin: 0 auto;
}
```
**修复方案**: 使用 `max-width: 1600px` + `width: 100%` 替代固定宽度。
```scss
// 修复后
.think-tank-person {
max-width: 1600px;
width: 100%;
margin: 0 auto;
padding: 0 16px;
box-sizing: border-box;
}
```
**涉及位置**:
| 文件 | 行号 | 当前值 |
|------|------|--------|
| thinkTankPerson/index.vue | 467, 472, 534, 574, 609 | width: 1600px / 1064px / 520px |
| techLeader/index.vue | 类似位置 | 同上 |
| memberOfCongress/index.vue | 类似位置 | 同上 |
| characterRelationships/index.vue | 467 | width: 1600px |
| relevantSituation/index.vue | 499, 504, 629 | width: 1600px / 1068px / 520px |
### C2. 固定高度容器导致内容溢出 [HIGH]
**影响文件**: thinkTankPerson/index.vue:576
```scss
.info-content {
height: 1933px; // 固定高度,内容多则溢出,内容少则大片空白
}
.main {
height: 1435px; // 固定高度
}
```
**修复方案**: 改为 `min-height`,让容器自适应内容高度。
### C3. .introduction/.description 无文本溢出处理 [MEDIUM]
**影响文件**: 所有三个类型视图的 header 区域
`characterInfo.description` 直接渲染,无 max-height 或 line-clamp 限制。超长描述会将下方内容推到不可见区域。
**修复方案**:
```scss
.introduction {
max-height: 72px; // 约3行
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
```
### C4. scoped 内 `* { margin:0; padding:0 }` 重置不可靠 [LOW]
**影响文件**: thinkTankPerson:459, characterRelationships:462, relevantSituation:493
Vue scoped styles 中的 `*` 选择器行为不符合预期,且可能影响全局样式。
**修复方案**: 移除这些全局重置,用具体选择器替代。
---
## 二、无数据 EMPTY 展示缺失 (HIGH)
### H1. 最新动态列表无空状态 [HIGH]
**影响文件**: thinkTankPerson/index.vue:41-71, techLeader, memberOfCongress
`CharacterLatestDynamic` 为空数组时,`.main` 容器直接显示为空,无任何提示。
**修复方案**: 参考 NewsTracker.vue:36 的实现
```html
<el-empty v-if="CharacterLatestDynamic.length === 0" description="暂无动态数据" :image-size="80" />
<template v-else>
<div v-for="item in CharacterLatestDynamic" ...>
<!-- 现有列表内容 -->
</div>
</template>
```
### H2. 词云图表无空状态 [HIGH]
**影响文件**: thinkTankPerson/index.vue:33, techLeader, memberOfCongress
`characterView` 无数据时,AnalysisBox 显示空白区域。
**修复方案**:
```html
<div class="echarts">
<el-empty v-if="characterView.length === 0" description="暂无观点数据" :image-size="80" />
<WordCloudChart v-else-if="wordLoading" :data="characterView" />
</div>
```
### H3. 人物关系图无空状态 [MEDIUM]
**影响文件**: characterRelationships/index.vue
`CharacterRelation` 为空时,仅显示中心节点,无关系提示。用户看到的是一张只有一个点的图。
**修复方案**:
```html
<el-empty v-if="nodes.length <= 1" description="暂无人物关系数据" :image-size="100" />
<div class="mainBox" v-else>
<div class="graph" id="relGraph"></div>
</div>
```
### H4. 相关情况子列表无空状态 [MEDIUM]
**影响文件**: relevantSituation/index.vue:61-103
`relationBillsList``relationAdList``relationThinkTankList` 三个列表无空状态。
**修复方案**: 每个列表添加:
```html
<el-empty v-if="relationBillsList.length === 0" description="暂无数据" :image-size="60" />
<template v-else>
<div v-for="item in relationBillsList" ...>...</div>
</template>
```
### H5. 人物基本信息卡片无空状态 [LOW]
**影响文件**: thinkTankPerson/index.vue:83
`v-if="characterBasicInfo"` 隐藏了整个卡片但无替代提示。
**修复方案**: 添加 `v-else` 显示加载中或空状态。
### H6. 履历卡片无空状态 [LOW]
**影响文件**: thinkTankPerson/index.vue:143, techLeader, memberOfCongress
`ResumeCard` 组件无论数据是否为空都显示,应添加空状态。
---
## 三、代码风险 (CRITICAL → LOW)
### R1. 空 catch 块吞噬错误 [CRITICAL]
**影响文件**: 几乎所有组件
```javascript
// index.vue:50-52
catch(error){
// 完全空的 catch 块!
}
```
空 catch 块会完全吞掉错误,导致问题无法排查。出现在:
- index.vue:50-52
- thinkTankPerson/index.vue:239-241, 287-289, 318-320, 410-413
- characterRelationships/index.vue:193-196, 355-357
- relevantSituation/index.vue:204-206
**修复方案**: 至少添加 `console.error(error)` 或使用统一错误处理。
### R2. ref 类型初始化错误 [HIGH]
**影响文件**: thinkTankPerson:343, relevantSituation:161-163
```javascript
// 错误:初始化为对象,实际用作数组
const CharacterLatestDynamic = ref({}); // 应为 ref([])
const relationBillsList = ref({}); // 应为 ref([])
const relationAdList = ref({}); // 应为 ref([])
const relationThinkTankList = ref({}); // 应为 ref([])
```
这会导致 `.length` 检查返回 undefined,v-for 遍历对象属性而非数组元素。
**修复方案**: 全部改为 `ref([])`
### R3. ECharts 双重初始化 — 内存泄漏 [HIGH]
**影响文件**: characterRelationships/index.vue
```javascript
// onMounted 中初始化一次 (line 394)
chart = echarts.init(el);
// getCharacterRelationFn 中又初始化一次 (line 296)
chart = echarts.init(el); // 覆盖了之前的实例,未 dispose
```
**修复方案**: 在重新 init 前先 dispose 旧实例,或提取为独立方法只调用一次。
### R4. v-for key 使用对象引用 [MEDIUM]
**影响文件**: unified/index.vue, thinkTankPerson:30
```html
<!-- key="tag" 传入的是整个对象,不是唯一标识 -->
<AreaTag v-for="tag in characterInfo.industryList" :key="tag" .../>
```
**修复方案**: 使用 `:key="tag.industryId"``:key="tag.industryName"`
### R5. 嵌套结构错误 [MEDIUM]
**影响文件**: thinkTankPerson/index.vue:106-118
```html
<div class="baseInfo-item"> <!-- 外层 -->
<div class="baseInfo-item"> <!-- 内层嵌套了同类型 -->
<div class="baseInfo-item-title">教育背景:</div>
<div class="baseInfo-item-content">...</div>
</div>
</div>
```
外层 `.baseInfo-item` 只包含一个子 `.baseInfo-item`,应该只有一层。
### R6. API 参数不匹配 [MEDIUM]
**影响文件**: thinkTankPerson/index.vue:364 vs characterPage.js:47-53
```javascript
// 调用时传了 signal
const res = await getCharacterLatestDynamic(params, abortController.value.signal);
// 但 API 定义不接收 signal
export function getCharacterLatestDynamic(params) {
return request({ method: "GET", url: `...`, params });
}
```
**修复方案**: 在 API 函数中接收并传递 signal 参数。
### R7. personId 独立读取 vs props 传递 [MEDIUM]
**影响文件**: 所有子组件
每个子组件都独立通过 `useRoute().query.personId` 获取 personId,而不是通过 props 接收。这导致:
- 组件与路由耦合
- 难以测试
- 可能出现不一致
**修复方案**: 父组件传递 personId 作为 prop。
### R8. getProxyUrl 函数重复 [LOW]
**影响文件**: thinkTankPerson:215-223, characterRelationships:94-104, memberOfCongress
同一函数在 3 个文件中重复。
**修复方案**: 提取到 `src/utils/imageProxy.js` 中统一导出。
### R9. console.log 残留 [LOW]
**影响文件**: 多处
- index.vue:34 — `console.log("人物全局信息111", res)`
- thinkTankPerson:234,276,334,365,405
**修复方案**: 移除所有调试日志。
### R10. 硬编码数据残留 [LOW]
**影响文件**: relevantSituation/index.vue:215-274
`billList``twoList``billList2` 三个 ref 定义了但从未在模板中使用(API 数据替代了它们)。
**修复方案**: 删除这些未使用的变量。
### R11. 年份列表硬编码 [LOW]
**影响文件**: thinkTankPerson/index.vue:431
```javascript
const num = ref(["2026","2025", "2024", "2023", "2022", "2021"]);
```
**修复方案**: 动态生成最近5-6年。
### R12. 无加载状态 [LOW]
**影响文件**: 所有列表组件
数据加载过程中无 skeleton 或 spinner 提示,用户看到空白后突然出现内容。
**修复方案**: 添加 `v-loading` 指令或 skeleton 组件。
---
## 四、实施步骤
### Step 1: 修复 ref 类型错误 (R2)
- 将所有 `ref({})` 改为 `ref([])` 用于数组数据
- 涉及: thinkTankPerson:343, relevantSituation:161-163
### Step 2: 修复空 catch 块 (R1)
- 所有空 catch 块添加 `console.error(error)`
- 涉及: 6+ 个文件中的 10+ 处
### Step 3: 修复 ECharts 双重初始化 (R3)
- characterRelationships/index.vue 统一初始化逻辑
### Step 4: 修复 v-for key 问题 (R4) 和嵌套结构 (R5)
- 使用 industryId/industryName 作为 key
- 修复 thinkTankPerson 中的嵌套 baseInfo-item
### Step 5: 添加 EMPTY 空状态展示 (H1-H6)
- 最新动态列表: thinkTankPerson/techLeader/memberOfCongress
- 词云图表: 同上三个组件
- 人物关系图: characterRelationships
- 相关情况列表: relevantSituation
- 基本信息和履历卡片
### Step 6: 修复样式溢出 (C1-C3)
- 1600px 固定宽度改为 max-width
- 固定高度改为 min-height
- .introduction 添加文本溢出限制
### Step 7: 清理代码 (R8-R11)
- 提取 getProxyUrl 到 utils
- 移除 console.log
- 删除硬编码未使用数据
- 动态生成年份列表
---
## 五、关键文件操作矩阵
| 文件 | 操作 | 优先级 | 描述 |
|------|------|--------|------|
| thinkTankPerson/index.vue | 修改 | CRITICAL | ref类型、空状态、样式、key、嵌套 |
| techLeader/index.vue | 修改 | HIGH | 空状态、样式 |
| memberOfCongress/index.vue | 修改 | HIGH | 空状态、样式 |
| characterRelationships/index.vue | 修改 | HIGH | ECharts双初始化、空状态、样式 |
| relevantSituation/index.vue | 修改 | MEDIUM | ref类型、空状态、清理 |
| unified/index.vue | 修改 | MEDIUM | v-for key |
| index.vue (主入口) | 修改 | LOW | catch块、console.log |
| src/utils/imageProxy.js | 新建 | LOW | 提取 getProxyUrl |
| src/api/characterPage/characterPage.js | 修改 | MEDIUM | signal 参数支持 |
---
## 六、风险评估
| 风险 | 缓解措施 |
|------|----------|
| 样式修改可能影响现有布局 | 逐步修改,每次改一个属性,测试验证 |
| 空状态添加需要确认设计规范 | 参考 NewsTracker.vue 已有的 el-empty 实现 |
| ECharts 重构可能影响关系图渲染 | 保留现有 setOption 逻辑,仅统一初始化入口 |
| getProxyUrl 提取需确认所有引用 | grep 确认所有使用点后一次性替换 |
---
## SESSION_ID (for /ccg:execute use)
- CODEX_SESSION: N/A (codex CLI not found in PATH)
- GEMINI_SESSION: N/A (gemini CLI not found in PATH)
{
"permissions": {
"allow": [
"mcp__plugin_ecc_playwright__browser_navigate"
]
}
}
...@@ -2,187 +2,186 @@ import request from "@/api/request.js"; ...@@ -2,187 +2,186 @@ import request from "@/api/request.js";
// 获取人物全局信息 // 获取人物全局信息
/** /**
* @param {personId} * @param {personId}
* @header token * @header token
*/ */
export function getCharacterGlobalInfo(params) { export function getCharacterGlobalInfo(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/summaryInfo/${params.personId}`, url: `/api/personHomepage/summaryInfo/${params.personId}`,
params, params
}) });
} }
// 获取人物基本信息 // 获取人物基本信息
/** /**
* @param {personId} * @param {personId}
* @header token * @header token
*/ */
export function getCharacterBasicInfo(params) { export function getCharacterBasicInfo(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/basicInfo/${params.personId}`, url: `/api/personHomepage/basicInfo/${params.personId}`,
params, params
}) });
} }
// 科技人物详情,获取领域观点 // 科技人物详情,获取领域观点
/** /**
* @param {areaId} * @param {areaId}
* @header token * @header token
*/ */
export function getCharacterFieldView(params) { export function getCharacterFieldView(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/areaOptions/${params.areaId}`, url: `/api/personHomepage/areaOptions/${params.areaId}`,
params, params
}) });
} }
// 获取最新动态 // 获取最新动态
/** /**
* @param {personId,cRelated,currentPage,pageSize} * @param {personId,cRelated,currentPage,pageSize}
* @header token * @header token
*/ */
export function getCharacterLatestDynamic(params) { export function getCharacterLatestDynamic(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/dynamics/${params.personId}`, url: `/api/personHomepage/dynamics/${params.personId}`,
params, params
}) });
} }
// 获取人物成果报告 // 获取人物成果报告
/** /**
* @param {personId,industryId,currentPage,pageSize,year} * @param {personId,industryId,currentPage,pageSize,year}
* @header token * @header token
*/ */
export function getCharacterAchievementReport(params) { export function getCharacterAchievementReport(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/findingsReport/${params.personId}/${params.year}`, url: `/api/personHomepage/findingsReport/${params.personId}/${params.year}`,
params, params
}) });
} }
// 获取人物历史提案 // 获取人物历史提案
/** /**
* @param {personId,industryId,currentPage,pageSize} * @param {personId,industryId,currentPage,pageSize}
* @header token * @header token
*/ */
export function getCharacterProposal(params) { export function getCharacterProposal(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/historyBill/${params.personId}`, url: `/api/personHomepage/historyBill/${params.personId}`,
params, params
}) });
} }
// 获取履历 // 获取履历
/** /**
* @param {personId} * @param {personId}
* @header token * @header token
*/ */
export function getCharacterResume(params) { export function getCharacterResume(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/jobResume/${params.personId}`, url: `/api/personHomepage/jobResume/${params.personId}`,
params, params
}) });
} }
// 获取观点 // 获取观点
/** /**
* @param {personId,year} * @param {personId,year}
* @header token * @header token
*/ */
export function getCharacterView(params) { export function getCharacterView(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/option/`, url: `/api/personHomepage/option/`,
params, params
}) });
} }
// 国会议员,获取资金来源 // 国会议员,获取资金来源
/** /**
* @param {personId,year} * @param {personId,year}
* @header token * @header token
*/ */
export function getCharacterFundSource(params) { export function getCharacterFundSource(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/personFunds/`, url: `/api/personHomepage/personFunds/`,
params, params
}) });
} }
// 获取人物关系 // 获取人物关系
/** /**
* @param {personId} * @param {personId}
* @header token * @header token
*/ */
export function getCharacterRelation(params) { export function getCharacterRelation(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/personRelation/${params.personId}`, url: `/api/personHomepage/personRelation/${params.personId}`,
params, params
}) });
} }
// 获取人物教育履历 // 获取人物教育履历
/** /**
* @param {personId} * @param {personId}
* @header token * @header token
*/ */
export function getCharacterReducationResume(params) { export function getCharacterReducationResume(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/educationResume/${params.personId}`, url: `/api/personHomepage/educationResume/${params.personId}`,
params, params
}) });
} }
// 获取相关实体 // 获取相关实体
/** /**
* @param {personId,startTime} * @param {personId,startTime}
* @header token * @header token
*/ */
export function getCharacterRelatedEntity(params) { export function getCharacterRelatedEntity(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/personHomepage/personRelation/${params.personId}/${params.startTime}`, url: `/api/personHomepage/personRelation/${params.personId}/${params.startTime}`,
params, params
}) });
} }
export function getareaType(params) { export function getareaType(params) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/commonDict/areaType`, url: `/api/commonDict/areaType`,
params params
}) });
} }
export async function getFindingsReport(personId, params = {}) { export async function getFindingsReport(personId, params = {}) {
const queryParts = [] const queryParts = [];
Object.entries(params).forEach(([key, value]) => { Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') return if (value === undefined || value === null || value === "") return;
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach(v => { value.forEach(v => {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`) queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
}) });
} else { } else {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`) queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
} }
}) });
const queryString = queryParts.join('&') const queryString = queryParts.join("&");
const url = queryString const url = queryString
? `/api/personHomepage/findingsReport/${personId}?${queryString}` ? `/api/personHomepage/findingsReport/${personId}?${queryString}`
: `/api/personHomepage/findingsReport/${personId}` : `/api/personHomepage/findingsReport/${personId}`;
return request(url, { method: 'GET' }) return request(url, { method: "GET" });
} }
/** /**
...@@ -196,21 +195,54 @@ export async function getFindingsReport(personId, params = {}) { ...@@ -196,21 +195,54 @@ export async function getFindingsReport(personId, params = {}) {
* @returns {Promise} 返回列表数据 * @returns {Promise} 返回列表数据
*/ */
export async function getSubjectList(params) { export async function getSubjectList(params) {
return request('/api/innovateSubject/findListBySubjectTypeId', { return request("/api/innovateSubject/findListBySubjectTypeId", {
method: 'GET', method: "GET",
params params
}) });
} }
/** /**
* 获取调研项目列表 * 获取调研项目列表
* @param {string} personId * @param {string} personId
* @param {Object} params * @param {Object} params
*/ */
export function getInvestigationProject(personId, params) { export async function getInvestigationProject(personId, params = {}) {
const queryParts = [];
return request({
method: 'GET', Object.entries(params).forEach(([key, value]) => {
url: `/api/personHomepage/investigationProject/${personId}`, if (value === undefined || value === null || value === "") return;
params, if (Array.isArray(value)) {
}) value.forEach(v => {
} queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
\ No newline at end of file });
} else {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
});
const queryString = queryParts.join("&");
const url = queryString
? `/api/personHomepage/investigationProject/${personId}?${queryString}`
: `/api/personHomepage/investigationProject/${personId}`;
return request(url, { method: "GET" });
}
export async function getPersonTestimonies(params = {}) {
const queryParts = [];
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null || value === "") return;
if (Array.isArray(value)) {
value.forEach(v => {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
});
} else {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
});
const queryString = queryParts.join("&");
const url = queryString ? `/api/think-tank/testimonies/person?${queryString}` : `/api/think-tank/testimonies/person`;
return request(url, { method: "GET" });
}
...@@ -39,18 +39,15 @@ const props = defineProps<{ ...@@ -39,18 +39,15 @@ const props = defineProps<{
<style scoped> <style scoped>
.page-header { .page-header {
width: 2548px; width: 100%;
height: 80px; height: 80px;
/* margin-left: 160px; */
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: -478px; justify-content: center;
padding-left: 478px; background: #ffffff;
background: #ffffff; box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); margin-bottom: 16px;
margin-bottom: 16px;
margin-left: 0px;
} }
.header-content { .header-content {
...@@ -85,7 +82,6 @@ const props = defineProps<{ ...@@ -85,7 +82,6 @@ const props = defineProps<{
color: var(--text-primary-65-color); color: var(--text-primary-65-color);
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.action-btn:hover { .action-btn:hover {
......
...@@ -282,11 +282,10 @@ const riskLabel = computed(() => { ...@@ -282,11 +282,10 @@ const riskLabel = computed(() => {
} }
.stat-card { .stat-card {
background-color: var(--color-primary-2); background-color: var(--color-primary-2);
border: 1px; border: 1px solid var(--color-primary-10);
padding: 12px; padding: 12px;
text-align: center; text-align: center;
border : 1px solid var(--color-primary-10);
} }
.stat-value { .stat-value {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<span class="text-tip-2 text-primary-50-clor">加载中...</span> <span class="text-tip-2 text-primary-50-clor">加载中...</span>
</div> </div>
<template v-else> <template v-else>
<div class="content-wrapper"> <div class="content-wrapper" v-if="currentProposalInfo">
<ProposalInfoSection v-if="currentProposalInfo" :info="currentProposalInfo" /> <ProposalInfoSection v-if="currentProposalInfo" :info="currentProposalInfo" />
<FilterSection <FilterSection
ref="filterSectionRef" ref="filterSectionRef"
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
:default-filters="defaultFilters" :default-filters="defaultFilters"
/> />
</div> </div>
<el-empty v-else description="暂无提案信息" :image-size="80" />
<ActionButtons <ActionButtons
show-set-as-current show-set-as-current
@reset="handleReset" @reset="handleReset"
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
</label> </label>
<span class="text-tip-1">已选择{{ selectedCount }}项法案</span> <span class="text-tip-1">已选择{{ selectedCount }}项法案</span>
</div> </div>
<div class="bills-list"> <div class="bills-list" v-if="bills.length > 0">
<BillCard <BillCard
v-for="bill in bills" v-for="bill in bills"
:key="bill.id" :key="bill.id"
...@@ -52,6 +52,7 @@ ...@@ -52,6 +52,7 @@
@toggle-select="toggleBillSelect" @toggle-select="toggleBillSelect"
/> />
</div> </div>
<el-empty v-else description="暂无匹配的相似法案" :image-size="80" />
</div> </div>
</div> </div>
<div class="action-footer"> <div class="action-footer">
...@@ -71,6 +72,7 @@ ...@@ -71,6 +72,7 @@
</button> </button>
</div> </div>
</template> </template>
<el-empty v-else description="暂无数据" :image-size="80" />
</div> </div>
</template> </template>
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
/> />
</div> </div>
</div> </div>
<el-empty v-else description="暂无预测数据" :image-size="80" />
<div class="action-footer flex-display-start"> <div class="action-footer flex-display-start">
<button class="btn-text flex-display-center" @click="handleViewHistory"> <button class="btn-text flex-display-center" @click="handleViewHistory">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
......
<template> <template>
<div class="app-container"> <div class="app-container">
<PageHeader /> <PageHeader :proposal-info="proposalInfo" />
<main class="layout-main-center"> <main class="layout-main-center">
<div class="content-card background-as-card"> <div class="content-card background-as-card">
<StepSidebar <StepSidebar
...@@ -287,22 +287,20 @@ function handleRepredict() { ...@@ -287,22 +287,20 @@ function handleRepredict() {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--bg-black-2); background-color: var(--bg-black-2);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
white-space: pre-line; white-space: pre-line;
max-height: calc(100vh - 80px); max-height: calc(100vh - 80px);
overflow-y: auto;
overflow-x: hidden;
white-space: pre-line;
box-sizing: border-box; box-sizing: border-box;
} }
.layout-main-center { .layout-main-center {
width: 1600px; width: 100%;
max-width: 1600px;
flex: 1; flex: 1;
padding-bottom: 32px; padding-bottom: 32px;
box-sizing: border-box; box-sizing: border-box;
margin-left: 475px; margin: 0 auto;
} }
.content-card { .content-card {
......
<template>
<div class="character-relationships" :style="{ height: containerHeight + 'px' }">
<div class="headerWrapper">
<div class="headerBox">
<span
v-for="(item, index) in list"
:key="index"
class="headerItem"
:class="{ active: item.value === activeIndex }"
@click="handleChangeLayout(item.value)"
>
<img :src="item.icon" :alt="item.label" />
</span>
</div>
<div class="headerBtnBox">
<img src="./assets/icon-expand.svg" alt="" class="btn-icon" />
<img src="./assets/icon-download.svg" alt="" class="btn-icon" />
<img src="./assets/icon-star.svg" alt="" class="btn-icon" />
</div>
</div>
<el-empty v-if="CharacterRelation.length === 0" description="暂无数据" :image-size="80" class="empty-placeholder" />
<template v-else>
<div class="mainBox" :style="{ paddingTop: (mainPaddingTop + 30) + 'px' }">
<div v-show="!showRelationChart || activeIndex !== thirdLayoutName" class="graph" id="relGraph"></div>
<RelationChart v-if="showRelationChart && activeIndex === thirdLayoutName && graphData.rootId" :graph-data="graphData" />
</div>
</template>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getCharacterGlobalInfo, getCharacterRelation } from '@/api/characterPage/characterPage.js'
import RelationChart from '@/components/base/RelationChart/index.vue'
const props = defineProps({
containerHeight: { type: Number, default: 738 },
mainPaddingTop: { type: Number, default: 0 },
showRelationChart: { type: Boolean, default: true },
thirdLayoutName: { type: String, default: '关系图' },
centerImageSize: { type: Number, default: 168 },
nodeImageSize: { type: Number, default: 108 },
centerSymbolSize: { type: Number, default: 84 },
nodeSymbolSize: { type: Number, default: 54 },
nodeHasDetailStyle: { type: Boolean, default: true },
})
import { useRoute } from 'vue-router'
const route = useRoute()
const personId = ref(route.query.personId || 'Y000064')
const getProxyUrl = (url) => {
if (!url) return ''
const urlStr = String(url)
if (
!urlStr.startsWith('http') ||
urlStr.includes('images.weserv.nl') ||
urlStr.includes('localhost') ||
urlStr.includes('127.0.0.1')
) {
return url
}
const cleanUrl = urlStr.replace(/^https?:\/\//i, '')
return `https://images.weserv.nl/?url=${encodeURIComponent(cleanUrl)}`
}
const createCircularImage = (imageUrl, size) => {
return new Promise((resolve) => {
if (!imageUrl) {
resolve('')
return
}
const img = new Image()
img.crossOrigin = 'anonymous'
const drawCircular = (imageEl) => {
try {
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const ctx = canvas.getContext('2d')
ctx.beginPath()
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2)
ctx.closePath()
ctx.clip()
ctx.drawImage(imageEl, 0, 0, size, size)
resolve(canvas.toDataURL('image/png'))
} catch (error) {
resolve(imageUrl)
}
}
img.onload = () => drawCircular(img)
img.onerror = () => {
const img2 = new Image()
img2.onload = () => drawCircular(img2)
img2.onerror = () => resolve(imageUrl)
img2.src = imageUrl
}
img.src = imageUrl
})
}
const nodes = ref([])
const links = ref([])
const characterInfo = ref({})
const CharacterRelation = ref([])
const graphData = ref({ rootId: '', nodes: [], lines: [] })
const list = ref([
{
value: '力导向布局',
label: '力导向布局',
icon: new URL('./assets/icon-force.svg', import.meta.url).href,
},
{
value: props.thirdLayoutName,
label: props.thirdLayoutName,
icon: new URL('./assets/icon-tree.svg', import.meta.url).href,
},
{
value: '圆形布局',
label: '圆形布局',
icon: new URL('./assets/icon-circle.svg', import.meta.url).href,
},
])
const activeIndex = ref('圆形布局')
let chart = null
const getCharacterGlobalInfoFn = async () => {
const params = { personId: personId.value }
try {
const res = await getCharacterGlobalInfo(params)
if (res.code === 200 && res.data) {
characterInfo.value = res.data
}
} catch (error) {}
}
const buildCenterNode = (centerCircularImage) => {
const base = {
id: 'c',
name: characterInfo.value.name || '',
category: 0,
symbolSize: props.centerSymbolSize,
symbol: `image://${centerCircularImage}`,
}
if (props.nodeHasDetailStyle) {
// techLeader style: detailed label with rich text
base.label = {
show: true,
position: 'bottom',
distance: 8,
formatter: '{b}',
color: 'rgba(5,95,194,1)',
fontSize: 24,
fontWeight: 700,
fontFamily: 'Microsoft YaHei',
}
} else {
// MOC / thinkTank style: rich formatter
base.label = {
show: true,
position: 'bottom',
formatter: '{n|{b}}',
rich: {
n: {
color: 'rgba(5,95,194,1)',
fontSize: 24,
fontWeight: 700,
fontFamily: 'Microsoft YaHei',
lineHeight: 36,
},
},
}
}
return base
}
const buildChildNode = (item, index, circularImage) => {
const base = {
id: index,
name: item.name,
category: 1,
symbolSize: props.nodeSymbolSize,
symbol: `image://${circularImage}`,
}
if (props.nodeHasDetailStyle) {
// techLeader style: with itemStyle and label
base.itemStyle = {
borderColor: 'rgba(174,214,255,1)',
borderWidth: 2,
}
base.label = {
show: true,
position: 'bottom',
distance: 5,
color: 'rgba(59,65,75,1)',
fontSize: 21,
fontWeight: 400,
fontFamily: 'Microsoft YaHei',
}
base.relationName = item.relation || ''
}
return base
}
const getCharacterRelationFn = async () => {
const params = { personId: personId.value }
try {
const res = await getCharacterRelation(params)
if (res.code === 200 && res.data) {
CharacterRelation.value = res.data
}
} catch (error) {}
const centerImageUrl = getProxyUrl(characterInfo.value.imageUrl)
const centerCircularImage = await createCircularImage(centerImageUrl, props.centerImageSize)
const centerNode = buildCenterNode(centerCircularImage)
if (CharacterRelation.value.length > 0) {
const circularImages = await Promise.all(
CharacterRelation.value.map((item) =>
createCircularImage(getProxyUrl(item.imageUrl), props.nodeImageSize)
)
)
const newNodes = CharacterRelation.value.map((item, index) =>
buildChildNode(item, index, circularImages[index])
)
newNodes.push(centerNode)
const newLinks = CharacterRelation.value.map((item, index) => ({
source: index,
target: 'c',
label: { show: true, formatter: item.relation },
...(props.nodeHasDetailStyle
? {
lineStyle: {
color: 'rgba(174,214,255,1)',
width: 1,
},
}
: {}),
}))
nodes.value = newNodes
links.value = newLinks
} else {
nodes.value = [centerNode]
links.value = []
}
// build RelationChart data
const gNodes = [{ id: 'center', text: characterInfo.value.name || '' }]
const gLines = []
CharacterRelation.value.forEach((item, index) => {
gNodes.push({ id: `node_${index}`, text: item.name })
gLines.push({ from: 'center', to: `node_${index}`, text: item.relation || '' })
})
graphData.value = { rootId: 'center', nodes: gNodes, lines: gLines }
}
const handleChangeLayout = (value) => {
activeIndex.value = value
renderChart()
}
const renderChart = () => {
const el = document.getElementById('relGraph')
if (!el) return
if (!chart) {
chart = echarts.init(el)
}
const rect = el.getBoundingClientRect()
const cx = rect.width / 2
const cy = rect.height / 2
const radius = Math.min(cx, cy) - 140
const dataNodes = nodes.value.map((n, i) => {
if (n.id === 'c') {
return { ...n, x: cx, y: cy, fixed: true }
}
const total = nodes.value.length - 1
const idx = nodes.value.slice(0, i).filter((nd) => nd.id !== 'c').length
const angle = (idx / (total || 1)) * Math.PI * 2
const rLocal = radius + (n.r || 0)
const x = cx + rLocal * Math.cos(angle)
const y = cy + rLocal * Math.sin(angle)
return { ...n, x, y }
})
// echarts option varies by nodeHasDetailStyle
let seriesBase
if (props.nodeHasDetailStyle) {
// techLeader variant
seriesBase = {
type: 'graph',
layout: activeIndex.value === '圆形布局' ? 'none' : 'force',
circular: { rotateLabel: false },
force: {
repulsion: activeIndex.value === '树形布局' ? 500 : 300,
edgeLength: activeIndex.value === '树形布局' ? 250 : 200,
gravity: activeIndex.value === '树形布局' ? 0.05 : 0.1,
},
roam: true,
draggable: true,
data: activeIndex.value === '圆形布局' ? dataNodes : nodes.value,
links: links.value,
categories: [{ name: '核心节点' }, { name: '关联节点' }],
edgeSymbol: ['none', 'arrow'],
edgeSymbolSize: [0, 8],
lineStyle: { color: 'rgba(174,214,255,1)', width: 1, curveness: 0 },
edgeLabel: {
show: true,
position: 'middle',
distance: -18,
formatter: ({ data }) => data?.label?.formatter || '',
color: 'rgba(5,95,194,1)',
fontSize: 14,
fontFamily: 'Microsoft YaHei',
backgroundColor: 'rgba(231, 243, 255, 1)',
borderRadius: 24,
padding: [0, 12],
},
label: { show: true },
emphasis: { focus: 'adjacency', lineStyle: { width: 2 } },
}
} else {
// MOC / thinkTank variant
seriesBase = {
type: 'graph',
layout: activeIndex.value === '圆形布局' ? 'none' : 'force',
circular: { rotateLabel: true },
force: { repulsion: 800, edgeLength: [80, 160] },
roam: true,
data: activeIndex.value === '圆形布局' ? dataNodes : nodes.value,
links: links.value,
edgeSymbol: ['none', 'arrow'],
edgeSymbolSize: [4, 10],
lineStyle: { color: 'rgba(174,214,255,1)', width: 2, opacity: 0.8 },
edgeLabel: {
show: true,
position: 'middle',
distance: -18,
formatter: ({ data }) => data?.label?.formatter || '',
color: 'rgb(5, 95, 194)',
fontSize: 12,
fontWeight: 400,
fontFamily: 'Microsoft YaHei',
lineHeight: 24,
backgroundColor: 'rgba(231, 243, 255, 1)',
borderRadius: 24,
padding: [0, 12],
},
label: { show: true, position: 'bottom', color: 'rgb(59,65,75)', fontSize: 16 },
itemStyle: { color: 'rgba(5,95,194,1)' },
emphasis: { focus: 'adjacency' },
}
}
chart.setOption(
{
backgroundColor: 'transparent',
tooltip: {},
series: [seriesBase],
},
true
)
}
onMounted(async () => {
await getCharacterGlobalInfoFn()
await getCharacterRelationFn()
await nextTick()
renderChart()
const onResize = () => chart && chart.resize()
window.addEventListener('resize', onResize)
watch(activeIndex, () => renderChart())
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
if (chart) {
chart.dispose()
chart = null
}
})
})
</script>
<style lang="scss" scoped>
* {
padding: 0;
margin: 0;
}
.character-relationships {
width: 1600px;
margin: 0 auto;
position: relative;
overflow: hidden;
background-color: transparent;
background-image: url('./assets/bg-grid.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.headerWrapper {
position: absolute;
top: 0;
left: 0;
width: 465px;
height: 54px;
background: #fff;
z-index: 10;
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
}
.headerBox {
position: absolute;
top: 11px;
left: 13px;
display: flex;
gap: 0;
width: 96px;
height: 32px;
background: #fff;
border-radius: 6px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
}
.headerItem {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
}
.headerItem img {
width: 18px;
height: 18px;
}
.headerItem.active {
background: rgba(231, 243, 255, 1);
border-color: #055ec2;
}
.headerBtnBox {
position: absolute;
top: 13px;
right: 12px;
display: flex;
gap: 12px;
}
.btn-icon {
width: 24px;
height: 24px;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.2s;
}
.btn-icon:hover {
opacity: 0.8;
}
.mainBox {
width: 100%;
height: 100%;
}
.graph {
width: 100%;
height: 100%;
}
.empty-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
:deep(canvas) {
background: transparent !important;
}
</style>
<template>
<div class="character-relationships">
<div class="headerBox">
<span
v-for="(item, index) in list"
:key="index"
class="headerItem"
:class="{ active: item.value === activeIndex }"
@click="handleChangeLayout(item.value)"
>
<img :src="item.icon" :alt="item.label" />
</span>
</div>
<div class="headerBtnBox">
<img src="./assets/icon-expand.svg" alt="" class="btn-icon" />
<img src="./assets/icon-download.svg" alt="" class="btn-icon" />
<img src="./assets/icon-star.svg" alt="" class="btn-icon" />
</div>
<div class="mainBox">
<div v-show="activeIndex !== '关系图'" class="graph" id="relGraph"></div>
<RelationChart v-if="activeIndex === '关系图' && graphData.rootId" :graph-data="graphData" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
import * as echarts from "echarts";
import { getCharacterGlobalInfo, getCharacterRelation } from "@/api/characterPage/characterPage.js";
import "default-passive-events";
import RelationChart from '@/components/base/RelationChart/index.vue';
import { useRoute } from "vue-router";
const route = useRoute();
const personId = ref(route.query.personId || "Y000064");
// ========== 图片代理 ==========
const getProxyUrl = (url) => {
if (!url) return "";
const urlStr = String(url);
if (
!urlStr.startsWith("http") ||
urlStr.includes("images.weserv.nl") ||
urlStr.includes("localhost") ||
urlStr.includes("127.0.0.1")
) {
return url;
}
const cleanUrl = urlStr.replace(/^https?:\/\//i, "");
return `https://images.weserv.nl/?url=${encodeURIComponent(cleanUrl)}`;
};
// ========== 创建圆形图片 ==========
const createCircularImage = (imageUrl, size, borderWidth = 2, borderColor = "rgba(174,214,255,1)") => {
return new Promise((resolve) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const canvas = document.createElement("canvas");
// 增加大小以容纳边框
const totalSize = size + borderWidth * 2;
canvas.width = totalSize;
canvas.height = totalSize;
const ctx = canvas.getContext("2d");
// 清空背景
ctx.clearRect(0, 0, totalSize, totalSize);
// 绘制圆形图片
ctx.beginPath();
ctx.arc(totalSize / 2, totalSize / 2, size / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
// 绘制图片到圆形区域
ctx.drawImage(img, borderWidth, borderWidth, size, size);
// 恢复上下文并绘制边框
ctx.restore();
ctx.beginPath();
ctx.arc(totalSize / 2, totalSize / 2, size / 2, 0, Math.PI * 2);
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.stroke();
resolve(canvas.toDataURL("image/png"));
};
img.onerror = () => {
resolve(imageUrl); // 失败时返回原图
};
img.src = imageUrl;
});
};
// ========== 数据 ==========
const nodes = ref([]);
const links = ref([]);
const characterInfo = ref({});
const CharacterRelation = ref([]);
const graphData = ref({ rootId: '', nodes: [], lines: [] });
const list = ref([
{
value: "圆形布局",
label: "圆形布局",
icon: new URL("./assets/icon-circle.svg", import.meta.url).href,
},
{
value: "力导向布局",
label: "力导向布局",
icon: new URL("./assets/icon-force.svg", import.meta.url).href,
},
{
value: "关系图",
label: "关系图",
icon: new URL("./assets/icon-tree.svg", import.meta.url).href,
},
]);
const activeIndex = ref("圆形布局");
let chart = null;
// ========== 切换布局 ==========
const handleChangeLayout = (value) => {
activeIndex.value = value;
renderChart();
};
// ========== 接口:人物全局信息 ==========
const getCharacterGlobalInfoFn = async () => {
const params = { personId: personId.value };
try {
const res = await getCharacterGlobalInfo(params);
if (res.code === 200 && res.data) {
characterInfo.value = res.data;
}
} catch (error) {
console.error("获取人物信息失败", error);
}
};
// ========== 接口:人物关系 ==========
const getCharacterRelationFn = async () => {
const params = { personId: personId.value };
try {
const res = await getCharacterRelation(params);
if (res.code === 200 && res.data) {
CharacterRelation.value = res.data;
}
} catch (error) {
console.error("获取人物关系失败", error);
}
// 创建圆形头像
const centerImageUrl = getProxyUrl(characterInfo.value.imageUrl);
const centerCircularImage = await createCircularImage(centerImageUrl, 76, 3, "rgba(174,214,255,1)");
// ---------- 组装节点和连线 ----------
const centerNode = {
id: "c",
name: characterInfo.value.name || "",
category: 0,
symbolSize: 80,
symbol: `image://${centerCircularImage}`,
label: {
show: true,
position: "bottom",
formatter: "{n|{b}}",
rich: {
n: {
color: "rgba(5,95,194,1)",
fontSize: 24,
fontWeight: 700,
fontFamily: "Microsoft YaHei",
lineHeight: 36,
},
},
},
};
if (CharacterRelation.value.length > 0) {
// 并行创建所有圆形头像
const circularImages = await Promise.all(
CharacterRelation.value.map((item) =>
createCircularImage(getProxyUrl(item.imageUrl), 76, 2, "rgba(174,214,255,1)")
)
);
const newNodes = CharacterRelation.value.map((item, index) => ({
id: index,
name: item.name,
category: 1,
symbolSize: 80,
symbol: `image://${circularImages[index]}`,
}));
newNodes.push(centerNode);
const newLinks = CharacterRelation.value.map((item, index) => ({
source: index,
target: "c",
label: { show: true, formatter: item.relation },
}));
nodes.value = newNodes;
links.value = newLinks;
} else {
nodes.value = [centerNode];
links.value = [];
}
// 构建关系图数据
const gNodes = [{ id: 'center', text: characterInfo.value.name || '' }];
const gLines = [];
CharacterRelation.value.forEach((item, index) => {
gNodes.push({ id: `node_${index}`, text: item.name });
gLines.push({ from: 'center', to: `node_${index}`, text: item.relation || '' });
});
graphData.value = { rootId: 'center', nodes: gNodes, lines: gLines };
};
// ========== 渲染图表 ==========
const renderChart = () => {
const el = document.getElementById("relGraph");
if (!el) return;
if (!chart) {
chart = echarts.init(el);
}
const rect = el.getBoundingClientRect();
const cx = rect.width / 2;
const cy = rect.height / 2;
const radius = Math.min(cx, cy) - 140;
const dataNodes = nodes.value.map((n, i) => {
if (n.id === "c") {
return { ...n, x: cx, y: cy, fixed: true };
}
const total = nodes.value.length - 1;
const idx = nodes.value.slice(0, i).filter((nd) => nd.id !== "c").length;
const angle = (idx / (total || 1)) * Math.PI * 2;
const rLocal = radius + (n.r || 0);
const x = cx + rLocal * Math.cos(angle);
const y = cy + rLocal * Math.sin(angle);
return { ...n, x, y };
});
chart.setOption(
{
tooltip: {},
series: [
{
type: "graph",
layout: activeIndex.value === "圆形布局" ? "none" : "force",
circular: { rotateLabel: true },
force: { repulsion: 800, edgeLength: [80, 160] },
roam: true,
data: activeIndex.value === "圆形布局" ? dataNodes : nodes.value,
links: links.value,
edgeSymbol: ["none", "arrow"],
edgeSymbolSize: [4, 10],
lineStyle: { color: "rgba(174,214,255,1)", width: 2, opacity: 0.8 },
edgeLabel: {
show: true,
position: "middle",
distance: 0,
formatter: ({ data }) => data?.label?.formatter || "",
color: "rgb(5, 95, 194)",
fontSize: 14,
fontWeight: 400,
fontFamily: "Microsoft YaHei",
backgroundColor: "#E7F3FF",
borderRadius: 14,
padding: [4, 12],
borderWidth: 1,
borderColor: "rgba(174,214,255,1)",
},
label: { show: true, position: "bottom", color: "rgb(59,65,75)", fontSize: 16 },
itemStyle: { color: "rgba(5,95,194,1)" },
emphasis: { focus: "adjacency" },
},
],
},
true
);
};
// ========== 生命周期 ==========
onMounted(async () => {
await getCharacterGlobalInfoFn();
await getCharacterRelationFn();
renderChart();
const onResize = () => chart && chart.resize();
window.addEventListener("resize", onResize);
watch(activeIndex, () => renderChart());
onBeforeUnmount(() => {
window.removeEventListener("resize", onResize);
if (chart) {
chart.dispose();
chart = null;
}
});
});
</script>
<style lang="scss" scoped>
* {
padding: 0;
margin: 0;
}
.character-relationships {
width: 1600px;
height: 738px;
background-color: #fff;
margin: 0 auto;
position: relative;
border-radius: 10px;
border: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
overflow: hidden;
background-image: url("./assets/bg-grid.png");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.headerBox {
position: absolute;
top: 14px;
left: 12px;
display: flex;
gap: 4px;
z-index: 10;
}
.headerItem {
width: 28px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 4px;
border: 1px solid rgba(234, 236, 238, 1);
background: rgba(255, 255, 255, 1);
}
.headerItem img {
width: 18px;
height: 18px;
}
.headerItem.active {
background: rgba(231, 243, 255, 1);
}
.headerBtnBox {
position: absolute;
top: 14px;
right: 12px;
display: flex;
gap: 4px;
z-index: 10;
}
.btn-icon {
width: 28px;
height: 28px;
cursor: pointer;
}
.mainBox {
width: 100%;
height: 100%;
}
.graph {
width: 100%;
height: 100%;
}
.headerBox {
position: absolute;
top: 14px;
left: 12px;
display: flex;
gap: 4px;
z-index: 10;
}
.headerItem {
width: 28px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 4px;
border: 1px solid rgba(234, 236, 238, 1);
background: rgba(255, 255, 255, 1);
}
.headerItem img {
width: 18px;
height: 18px;
}
.headerItem.active {
background: rgba(231, 243, 255, 1);
}
.headerBtnBox {
position: absolute;
top: 14px;
right: 12px;
display: flex;
gap: 4px;
z-index: 10;
}
.btn-icon {
width: 28px;
height: 28px;
cursor: pointer;
}
.mainBox {
width: 100%;
height: 100%;
}
.graph {
width: 100%;
height: 100%;
}
</style>
\ No newline at end of file
...@@ -80,7 +80,7 @@ export function useBills() { ...@@ -80,7 +80,7 @@ export function useBills() {
} }
if (filters.selectedDomains.length > 0 && !filters.selectedDomains.includes('all')) { if (filters.selectedDomains.length > 0 && !filters.selectedDomains.includes('all')) {
params.industryId = filters.selectedDomains params.industryIds = filters.selectedDomains
} }
if (filters.selectedTimeRanges.length > 0 && !filters.selectedTimeRanges.includes('all')) { if (filters.selectedTimeRanges.length > 0 && !filters.selectedTimeRanges.includes('all')) {
......
...@@ -255,7 +255,11 @@ ...@@ -255,7 +255,11 @@
<HistoricalProposal v-if="newsTab === 'history'" /> <HistoricalProposal v-if="newsTab === 'history'" />
<PotentialNews v-else /> <PotentialNews v-else />
</div> </div>
<CharacterRelationships v-if="infoActive === '人物关系'" /> <CharacterRelationships v-if="infoActive === '人物关系'"
:center-image-size="76" :node-image-size="76"
:center-symbol-size="80" :node-symbol-size="80"
:node-has-detail-style="false"
/>
<RelevantSituation v-if="infoActive === '相关情况'" /> <RelevantSituation v-if="infoActive === '相关情况'" />
<!-- 弹框 --> <!-- 弹框 -->
<el-dialog v-model="dialogVisible" width="761px" class="viewpoint-dialog" :modal="false" :draggable="true" <el-dialog v-model="dialogVisible" width="761px" class="viewpoint-dialog" :modal="false" :draggable="true"
...@@ -298,7 +302,7 @@ ...@@ -298,7 +302,7 @@
<script setup> <script setup>
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed } from "vue";
import CharacterRelationships from "./components/characterRelationships/index.vue"; import CharacterRelationships from "../CharacterRelationships/index.vue";
import RelevantSituation from "./components/relevantSituation/index.vue"; import RelevantSituation from "./components/relevantSituation/index.vue";
import HistoricalProposal from "./components/historicalProposal/components/BillTracker.vue"; import HistoricalProposal from "./components/historicalProposal/components/BillTracker.vue";
import PotentialNews from './components/historicalProposal/components/PotentialNews.vue' import PotentialNews from './components/historicalProposal/components/PotentialNews.vue'
......
<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">
<rect id="icon-圆形布局 1" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 423" d="M13.81 6.6C13.67 5.97 13.39 5.41 13.04 4.85C13.18 4.64 13.25 4.36 13.25 4.15C13.25 3.38 12.62 2.75 11.85 2.75C11.57 2.75 11.36 2.82 11.15 2.96C10.59 2.61 10.03 2.4 9.4 2.19C9.33 1.49 8.7 1 8 1C7.3 1 6.74 1.56 6.6 2.19C5.97 2.4 5.41 2.61 4.85 2.96C4.64 2.82 4.36 2.75 4.15 2.75C3.38 2.75 2.75 3.38 2.75 4.15C2.75 4.43 2.82 4.64 2.96 4.85C2.61 5.41 2.4 5.97 2.19 6.6C1.56 6.74 1 7.3 1 8C1 8.7 1.56 9.26 2.19 9.4C2.33 10.03 2.61 10.59 2.96 11.15C2.82 11.36 2.75 11.64 2.75 11.85C2.75 12.62 3.38 13.25 4.15 13.25C4.43 13.25 4.64 13.18 4.85 13.04C5.41 13.39 5.97 13.6 6.6 13.81C6.67 14.51 7.3 15 8 15C8.7 15 9.26 14.44 9.4 13.81C10.03 13.67 10.59 13.39 11.15 13.04C11.36 13.18 11.64 13.25 11.85 13.25C12.62 13.25 13.25 12.62 13.25 11.85C13.25 11.57 13.18 11.36 13.04 11.15C13.39 10.59 13.6 10.03 13.81 9.4C14.51 9.33 15 8.7 15 8C15 7.3 14.44 6.74 13.81 6.6L13.81 6.6ZM12.55 10.66C12.34 10.52 12.13 10.45 11.85 10.45C11.08 10.45 10.45 11.08 10.45 11.85C10.45 12.13 10.52 12.34 10.66 12.55C10.24 12.76 9.82 12.97 9.33 13.11C9.12 12.62 8.63 12.2 8 12.2C7.44 12.2 6.88 12.55 6.67 13.11C6.18 12.97 5.76 12.83 5.34 12.55C5.48 12.34 5.55 12.13 5.55 11.85C5.55 11.08 4.92 10.45 4.15 10.45C3.87 10.45 3.66 10.52 3.45 10.66C3.24 10.24 3.03 9.82 2.89 9.33C3.45 9.12 3.8 8.56 3.8 8C3.8 7.44 3.45 6.88 2.96 6.74C3.1 6.25 3.24 5.83 3.52 5.41C3.73 5.55 3.94 5.62 4.22 5.62C4.99 5.62 5.62 4.99 5.62 4.22C5.62 3.94 5.55 3.73 5.41 3.52C5.83 3.31 6.25 3.1 6.74 2.96C6.88 3.45 7.44 3.8 8 3.8C8.56 3.8 9.12 3.45 9.33 2.89C9.82 3.03 10.24 3.17 10.66 3.45C10.52 3.66 10.45 3.87 10.45 4.15C10.45 4.92 11.08 5.55 11.85 5.55C12.13 5.55 12.34 5.48 12.55 5.34C12.76 5.76 12.97 6.18 13.11 6.67C12.55 6.88 12.2 7.44 12.2 8C12.2 8.56 12.55 9.12 13.04 9.26C12.97 9.75 12.76 10.24 12.55 10.66L12.55 10.66Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none">
<defs>
<clipPath id="clipPath_1">
<rect width="18.000000" height="15.000000" x="5.000000" y="5.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="导出数据" width="28.000000" height="28.000000" x="0.000000" y="0.000000" />
<g id="容器 742" customFrame="url(#clipPath_1)">
<rect id="容器 742" width="18.000000" height="15.000000" x="5.000000" y="5.000000" />
<rect id="矩形 347" width="2.000000" height="6.000000" x="13.000000" y="5.000000" fill="rgb(132,136,142)" />
<path id="矢量 600" d="M18 11L10 11L14 16L18 11Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
<path id="矢量 601" d="M22 19.9996L22.9999 15.0012L19.9999 12.0011L18.9999 12L21.0003 15.001L17.9999 15.0015L16.9998 17.9987L14 17.9996L11.0001 17.9997L9.99998 15.002L7.00017 15.0028L8.99996 12.0008L8 12.0004L5 15.0023L6.00016 20L22 19.9996Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</g>
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none">
<defs>
<clipPath id="clipPath_0">
<rect width="20.000000" height="20.000000" x="4.000000" y="4.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="数据源" width="28.000000" height="28.000000" x="0.000000" y="0.000000" />
<g id="数据库 1" clip-path="url(#clipPath_0)" customFrame="url(#clipPath_0)">
<rect id="数据库 1" width="20.000000" height="20.000000" x="4.000000" y="4.000000" />
<path id="合并" d="M10.6426 6.48828C11.6719 6.28906 12.791 6.1875 14 6.1875C15.209 6.1875 16.3281 6.28906 17.3574 6.48828C18.3867 6.6875 19.2012 6.96094 19.7988 7.30469C20.3965 7.64844 20.6973 8.01953 20.6973 8.42188L20.6973 9.53906C20.6973 9.94141 20.3984 10.3125 19.7988 10.6563C19.1992 11 18.3867 11.2715 17.3574 11.4727C16.3281 11.6699 15.209 11.7695 14 11.7695C12.791 11.7695 11.6719 11.668 10.6426 11.4688C9.61328 11.2695 8.79883 10.9961 8.20117 10.6523C7.60156 10.3086 7.30273 9.9375 7.30273 9.53516L7.30273 8.41797C7.30273 8.01563 7.60156 7.64453 8.20117 7.30078C8.80078 6.96094 9.61328 6.68945 10.6426 6.48828ZM10.1387 12.5078C11.3359 12.7578 12.623 12.8828 14 12.8828C15.377 12.8828 16.6641 12.7578 17.8613 12.5078C19.0586 12.2578 20.0039 11.8887 20.6953 11.4004L20.6953 12.8828C20.6953 13.2852 20.3965 13.6563 19.7969 14C19.1973 14.3438 18.3848 14.6152 17.3555 14.8164C16.3281 15.0156 15.209 15.1152 14 15.1152C12.791 15.1152 11.6719 15.0137 10.6426 14.8145C9.61328 14.6152 8.79883 14.3418 8.20117 13.998C7.60156 13.6543 7.30273 13.2832 7.30273 12.8809L7.30273 11.3984C7.99609 11.8906 8.94141 12.2598 10.1387 12.5078ZM10.1387 15.8574C11.3359 16.1074 12.623 16.2324 14 16.2324C14.6624 16.2324 15.3041 16.2035 15.9249 16.1456C14.2088 16.4715 12.8443 17.3161 12.2805 18.3935C11.7114 18.3432 11.1654 18.2672 10.6426 18.166C9.61328 17.9668 8.80078 17.6934 8.20117 17.3496C7.60156 17.0059 7.30273 16.6348 7.30273 16.2324L7.30273 14.75C7.9961 15.2383 8.94141 15.6074 10.1387 15.8574ZM17.5 16C17.3281 16 17.1581 16.005 16.9902 16.0148C17.2857 15.9695 17.5761 15.917 17.8613 15.8574C19.0586 15.6074 20.0039 15.2383 20.6953 14.75L20.6953 16.2324C20.6953 16.3614 20.6646 16.4872 20.6031 16.6099C19.7199 16.2251 18.6512 16 17.5 16ZM13 19.5C13 18.1193 15.0147 17 17.5 17C19.9853 17 22 18.1193 22 19.5C22 20.8807 19.9853 22 17.5 22C15.0147 22 13 20.8807 13 19.5ZM17.5 18C18.3284 18 19 18.6716 19 19.5C19 20.3284 18.3284 21 17.5 21C16.6716 21 16 20.3284 16 19.5C16 18.6716 16.6716 18 17.5 18ZM12 19.5L12 19.4861C11.3572 19.4236 10.7367 19.33 10.1387 19.2051C8.94141 18.9551 7.99609 18.5859 7.30273 18.0957L7.30273 19.5781C7.30273 19.9805 7.60156 20.3516 8.20117 20.6953C8.79883 21.0391 9.61328 21.3125 10.6426 21.5117C11.4872 21.6752 12.3923 21.7729 13.3579 21.8027C12.5123 21.1873 12 20.3817 12 19.5Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</g>
</svg>
<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">
<rect id="拓扑 1" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 422" d="M12.7727 11.142C12.4273 11.142 12.107 11.2495 11.8433 11.4327L9.42154 9.6837C9.65586 9.26852 9.78974 8.7891 9.78974 8.27836C9.78974 8.18728 9.7853 8.09723 9.77697 8.00833L12.257 6.54325C12.6396 6.77576 13.1451 6.72689 13.4758 6.39622C13.8641 6.00791 13.8641 5.37835 13.4758 4.99004C13.0875 4.60173 12.4579 4.60173 12.0696 4.99004C11.7835 5.27612 11.7085 5.69309 11.844 6.04812L9.63655 7.35217C9.40869 6.68522 8.9416 6.12922 8.33762 5.78637L8.77881 3.58047C8.81046 3.5835 8.84253 3.58519 8.87497 3.58519C9.42413 3.58519 9.8693 3.14002 9.8693 2.59087C9.8693 2.04173 9.42413 1.59656 8.87497 1.59656C8.32582 1.59656 7.88065 2.04173 7.88065 2.59087C7.88065 2.87182 7.99736 3.12535 8.18469 3.30617L7.73951 5.53213C7.48164 5.45587 7.2087 5.41473 6.9261 5.41473C6.388 5.41473 5.88464 5.56327 5.45458 5.8214L4.08134 4.61294C4.32918 4.12086 4.24801 3.50548 3.83714 3.09461C3.32457 2.58205 2.49355 2.58205 1.98098 3.09461C1.46841 3.60717 1.46842 4.4382 1.98098 4.95078C2.43973 5.40952 3.15341 5.45735 3.66574 5.09492L4.93964 6.21596C4.39895 6.73684 4.06247 7.46826 4.06247 8.27836C4.06247 9.15881 4.45994 9.94631 5.0851 10.4716L3.95324 11.6792C3.74531 11.5408 3.49572 11.4602 3.22724 11.4602C2.50236 11.4602 1.91474 12.0478 1.91474 12.7727C1.91474 13.4975 2.50236 14.0852 3.22724 14.0852C3.95212 14.0852 4.53974 13.4975 4.53974 12.7727C4.53974 12.5477 4.48308 12.3359 4.38332 12.1508L5.62251 10.8288C6.01351 11.029 6.45662 11.142 6.9261 11.142C7.76819 11.142 8.52534 10.7785 9.04931 10.1999L11.3976 11.8958C11.2358 12.149 11.142 12.4499 11.142 12.7727C11.142 13.6733 11.8721 14.4034 12.7727 14.4034C13.6733 14.4034 14.4034 13.6733 14.4034 12.7727C14.4034 11.8721 13.6733 11.142 12.7727 11.142L12.7727 11.142Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none">
<rect id="收藏" width="28.000000" height="28.000000" x="0.000000" y="0.000000" />
<path id="星形 2" d="M15.9534 11.0113C15.9936 11.1349 16.1088 11.2186 16.2388 11.2186L21.6363 11.2188C21.9269 11.2188 22.0478 11.5907 21.8127 11.7615L17.446 14.9343C17.3409 15.0107 17.2969 15.1461 17.3371 15.2697L19.0048 20.4031C19.0946 20.6795 18.7783 20.9094 18.5432 20.7385L14.1763 17.5661C14.0712 17.4897 13.9288 17.4897 13.8237 17.5661L9.45683 20.7385C9.22171 20.9094 8.90539 20.6795 8.99518 20.4031L10.6629 15.2697C10.7031 15.1461 10.6591 15.0107 10.554 14.9343L6.18734 11.7615C5.95224 11.5907 6.07307 11.2188 6.36368 11.2188L11.7612 11.2186C11.8912 11.2186 12.0064 11.1349 12.0466 11.0113L13.7147 5.87799C13.8045 5.60161 14.1955 5.60161 14.2853 5.87799L15.9534 11.0113Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</svg>
<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">
<rect id="绿洲_拓扑图 1" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 422" d="M15.0147 9.88449L14.1867 9.88449L14.1867 8.65516C14.1867 7.53912 13.312 6.63116 12.2368 6.63116L8.37334 6.63116L8.37334 5.08449L9.2317 5.08449C9.42605 5.08449 9.58222 4.94394 9.58222 4.74957L9.58222 2.34115C9.58222 2.11642 9.40148 1.92004 9.17669 1.92004L6.76829 1.92004C6.57396 1.92004 6.41778 2.09179 6.41778 2.28616L6.41778 4.69459C6.41778 4.91931 6.59853 5.08449 6.82329 5.08449L7.62667 5.08449L7.62667 6.63116L3.59268 6.63116C2.62816 6.63116 1.81334 7.45731 1.81334 8.43519L1.81334 9.88449L0.930314 9.88449C0.735986 9.88449 0.58667 10.0572 0.58667 10.2515L0.58667 12.66C0.58667 12.8847 0.760554 13.0489 0.985319 13.0489L3.39371 13.0489C3.58806 13.0489 3.75111 12.9093 3.75111 12.7149L3.75111 10.3066C3.75111 10.0818 3.56351 9.88451 3.33874 9.88451L2.52445 9.88451L2.52445 8.43521C2.52445 7.84276 3.01364 7.34227 3.59268 7.34227L7.62667 7.34227L7.62667 9.88449L6.76829 9.88449C6.57396 9.88449 6.41778 10.0572 6.41778 10.2515L6.41778 12.66C6.41778 12.8847 6.59853 13.0489 6.82329 13.0489L9.2317 13.0489C9.42605 13.0489 9.58222 12.9093 9.58222 12.7149L9.58222 10.3066C9.58222 10.0818 9.40148 9.88451 9.1767 9.88451L8.37334 9.88451L8.37334 7.34227L12.2368 7.34227C12.9199 7.34227 13.4756 7.93123 13.4756 8.65516L13.4756 9.88449L12.6063 9.88449C12.4119 9.88449 12.2489 10.0572 12.2489 10.2515L12.2489 12.66C12.2489 12.8847 12.4365 13.0489 12.6613 13.0489L15.0697 13.0489C15.264 13.0489 15.4133 12.9093 15.4133 12.7149L15.4133 10.3066C15.4133 10.0818 15.2395 9.88449 15.0147 9.88449Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
<template>
<div class="character-relationships">
<div class="headerBox">
<span
v-for="(item, index) in list"
:key="index"
class="headerItem"
:class="{ active: item.value === activeIndex }"
@click="handleChangeLayout(item.value)"
>
<img :src="item.icon" :alt="item.label" />
</span>
</div>
<div class="headerBtnBox">
<img src="./assets/icon-expand.svg" alt="" class="btn-icon" />
<img src="./assets/icon-download.svg" alt="" class="btn-icon" />
<img src="./assets/icon-star.svg" alt="" class="btn-icon" />
</div>
<div class="mainBox">
<div v-show="activeIndex !== '关系图'" class="graph" id="relGraph"></div>
<RelationChart v-if="activeIndex === '关系图' && graphData.rootId" :graph-data="graphData" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import * as echarts from 'echarts'
import { getCharacterGlobalInfo, getCharacterRelation } from '@/api/characterPage/characterPage.js'
import { useRoute } from 'vue-router'
import RelationChart from '@/components/base/RelationChart/index.vue'
const route = useRoute()
const personId = ref(route.query.personId || 'Y000064')
const getProxyUrl = (url) => {
if (!url) return ''
const urlStr = String(url)
if (
!urlStr.startsWith('http') ||
urlStr.includes('images.weserv.nl') ||
urlStr.includes('localhost') ||
urlStr.includes('127.0.0.1')
) {
return url
}
const cleanUrl = urlStr.replace(/^https?:\/\//i, '')
return `https://images.weserv.nl/?url=${encodeURIComponent(cleanUrl)}`
}
// 创建圆形图片的函数
const createCircularImage = (imageUrl, size) => {
return new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const ctx = canvas.getContext('2d')
// 绘制圆形裁剪区域
ctx.beginPath()
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2)
ctx.closePath()
ctx.clip()
// 绘制图片
ctx.drawImage(img, 0, 0, size, size)
resolve(canvas.toDataURL('image/png'))
}
img.onerror = () => {
resolve(imageUrl) // 失败时返回原图
}
img.src = imageUrl
})
}
const nodes = ref([])
const links = ref([])
const characterInfo = ref({})
const CharacterRelation = ref([])
const graphData = ref({ rootId: '', nodes: [], lines: [] })
const list = ref([
{
value: '圆形布局',
label: '圆形布局',
icon: new URL('./assets/icon-circle.svg', import.meta.url).href,
},
{
value: '力导向布局',
label: '力导向布局',
icon: new URL('./assets/icon-force.svg', import.meta.url).href,
},
{
value: '关系图',
label: '关系图',
icon: new URL('./assets/icon-tree.svg', import.meta.url).href,
},
])
const activeIndex = ref('圆形布局')
let chart = null
const getCharacterGlobalInfoFn = async () => {
const params = { personId: personId.value }
try {
const res = await getCharacterGlobalInfo(params)
if (res.code === 200 && res.data) {
characterInfo.value = res.data
}
} catch (error) {}
}
const getCharacterRelationFn = async () => {
const params = { personId: personId.value }
try {
const res = await getCharacterRelation(params)
if (res.code === 200 && res.data) {
CharacterRelation.value = res.data
}
} catch (error) {}
// 创建圆形头像
const centerImageUrl = getProxyUrl(characterInfo.value.imageUrl)
const centerCircularImage = await createCircularImage(centerImageUrl, 168)
const centerNode = {
id: 'c',
name: characterInfo.value.name || '',
category: 0,
symbolSize: 84,
symbol: `image://${centerCircularImage}`,
itemStyle: {
borderColor: 'rgba(174,214,255,1)',
borderWidth: 3,
},
label: {
show: true,
position: 'bottom',
distance: 8,
formatter: '{b}',
color: 'rgba(5,95,194,1)',
fontSize: 24,
fontWeight: 700,
fontFamily: 'Microsoft YaHei',
},
}
if (CharacterRelation.value.length > 0) {
// 并行创建所有圆形头像
const circularImages = await Promise.all(
CharacterRelation.value.map((item) =>
createCircularImage(getProxyUrl(item.imageUrl), 108)
)
)
const newNodes = CharacterRelation.value.map((item, index) => ({
id: index,
name: item.name,
category: 1,
symbolSize: 54,
symbol: `image://${circularImages[index]}`,
itemStyle: {
borderColor: 'rgba(174,214,255,1)',
borderWidth: 2,
},
label: {
show: true,
position: 'bottom',
distance: 5,
color: 'rgba(59,65,75,1)',
fontSize: 21,
fontWeight: 400,
fontFamily: 'Microsoft YaHei',
},
relationName: item.relation || '',
}))
newNodes.push(centerNode)
const newLinks = CharacterRelation.value.map((item, index) => ({
source: index,
target: 'c',
label: { show: true, formatter: item.relation },
lineStyle: {
color: 'rgba(174,214,255,1)',
width: 1,
},
}))
nodes.value = newNodes
links.value = newLinks
} else {
nodes.value = [centerNode]
links.value = []
}
// 构建关系图数据
const gNodes = [{ id: 'center', text: characterInfo.value.name || '' }]
const gLines = []
CharacterRelation.value.forEach((item, index) => {
gNodes.push({ id: `node_${index}`, text: item.name })
gLines.push({ from: 'center', to: `node_${index}`, text: item.relation || '' })
})
graphData.value = { rootId: 'center', nodes: gNodes, lines: gLines }
}
const handleChangeLayout = (value) => {
activeIndex.value = value
renderChart()
}
const renderChart = () => {
const el = document.getElementById('relGraph')
if (!el) return
if (!chart) {
chart = echarts.init(el)
}
const rect = el.getBoundingClientRect()
const cx = rect.width / 2
const cy = rect.height / 2
const radius = Math.min(cx, cy) - 140
const dataNodes = nodes.value.map((n, i) => {
if (n.id === 'c') {
return { ...n, x: cx, y: cy, fixed: true }
}
const total = nodes.value.length - 1
const idx = nodes.value.slice(0, i).filter((nd) => nd.id !== 'c').length
const angle = (idx / (total || 1)) * Math.PI * 2
const rLocal = radius + (n.r || 0)
const x = cx + rLocal * Math.cos(angle)
const y = cy + rLocal * Math.sin(angle)
return { ...n, x, y }
})
chart.setOption(
{
backgroundColor: 'transparent',
tooltip: {},
series: [
{
type: 'graph',
layout: activeIndex.value === '圆形布局' ? 'none' : 'force',
circular: { rotateLabel: false },
force: {
repulsion: activeIndex.value === '树形布局' ? 500 : 300,
edgeLength: activeIndex.value === '树形布局' ? 250 : 200,
gravity: activeIndex.value === '树形布局' ? 0.05 : 0.1,
},
roam: true,
draggable: true,
data: activeIndex.value === '圆形布局' ? dataNodes : nodes.value,
links: links.value,
categories: [
{ name: '核心节点' },
{ name: '关联节点' },
],
edgeSymbol: ['none', 'arrow'],
edgeSymbolSize: [0, 8],
lineStyle: {
color: 'rgba(174,214,255,1)',
width: 1,
curveness: 0,
},
edgeLabel: {
show: true,
position: 'middle',
distance: -18,
formatter: ({ data }) => data?.label?.formatter || '',
color: 'rgba(5,95,194,1)',
fontSize: 14,
fontFamily: 'Microsoft YaHei',
backgroundColor: 'rgba(231, 243, 255, 1)',
borderRadius: 24,
padding: [0, 12],
},
label: { show: true },
emphasis: {
focus: 'adjacency',
lineStyle: { width: 2 },
},
},
],
},
true
)
}
onMounted(async () => {
await getCharacterGlobalInfoFn()
await getCharacterRelationFn()
renderChart()
const onResize = () => chart && chart.resize()
window.addEventListener('resize', onResize)
watch(activeIndex, () => renderChart())
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
if (chart) {
chart.dispose()
chart = null
}
})
})
</script>
<style scoped lang="scss">
.character-relationships {
width: 1600px;
height: 738px;
background-color: #fff;
margin: 0 auto;
position: relative;
border-radius: 10px;
border: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
overflow: hidden;
background-image: url('./assets/bg-grid.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.headerBox {
position: absolute;
top: 14px;
left: 12px;
display: flex;
gap: 4px;
z-index: 10;
}
.headerItem {
width: 28px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 4px;
border: 1px solid rgba(234, 236, 238, 1);
background: rgba(255, 255, 255, 1);
}
.headerItem img {
width: 18px;
height: 18px;
}
.headerItem.active {
background: rgba(231, 243, 255, 1);
}
.headerBtnBox {
position: absolute;
top: 14px;
right: 12px;
display: flex;
gap: 4px;
z-index: 10;
}
.btn-icon {
width: 28px;
height: 28px;
cursor: pointer;
}
.mainBox {
width: 100%;
height: 100%;
}
.graph {
width: 100%;
height: 100%;
}
.headerBox {
position: absolute;
top: 14px;
left: 12px;
display: flex;
gap: 4px;
z-index: 10;
}
.headerItem {
width: 28px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 4px;
border: 1px solid rgba(234, 236, 238, 1);
background: rgba(255, 255, 255, 1);
}
.headerItem img {
width: 18px;
height: 18px;
}
.headerItem.active {
background: rgba(231, 243, 255, 1);
}
.headerBtnBox {
position: absolute;
top: 14px;
right: 12px;
display: flex;
gap: 4px;
z-index: 10;
}
.btn-icon {
width: 28px;
height: 28px;
cursor: pointer;
}
.mainBox {
width: 100%;
height: 100%;
}
.graph {
width: 100%;
height: 100%;
}
</style>
\ No newline at end of file
...@@ -243,7 +243,7 @@ ...@@ -243,7 +243,7 @@
<script setup> <script setup>
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed } from "vue";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import CharacterRelationships from "./components/characterRelationships/index.vue"; import CharacterRelationships from "../CharacterRelationships/index.vue";
import RelevantSituation from "./components/relevantSituation/index.vue"; import RelevantSituation from "./components/relevantSituation/index.vue";
import getWordCloudChart from "../../utils/worldCloudChart"; import getWordCloudChart from "../../utils/worldCloudChart";
import { import {
......
<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">
<rect id="icon-圆形布局 1" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 423" d="M13.81 6.6C13.67 5.97 13.39 5.41 13.04 4.85C13.18 4.64 13.25 4.36 13.25 4.15C13.25 3.38 12.62 2.75 11.85 2.75C11.57 2.75 11.36 2.82 11.15 2.96C10.59 2.61 10.03 2.4 9.4 2.19C9.33 1.49 8.7 1 8 1C7.3 1 6.74 1.56 6.6 2.19C5.97 2.4 5.41 2.61 4.85 2.96C4.64 2.82 4.36 2.75 4.15 2.75C3.38 2.75 2.75 3.38 2.75 4.15C2.75 4.43 2.82 4.64 2.96 4.85C2.61 5.41 2.4 5.97 2.19 6.6C1.56 6.74 1 7.3 1 8C1 8.7 1.56 9.26 2.19 9.4C2.33 10.03 2.61 10.59 2.96 11.15C2.82 11.36 2.75 11.64 2.75 11.85C2.75 12.62 3.38 13.25 4.15 13.25C4.43 13.25 4.64 13.18 4.85 13.04C5.41 13.39 5.97 13.6 6.6 13.81C6.67 14.51 7.3 15 8 15C8.7 15 9.26 14.44 9.4 13.81C10.03 13.67 10.59 13.39 11.15 13.04C11.36 13.18 11.64 13.25 11.85 13.25C12.62 13.25 13.25 12.62 13.25 11.85C13.25 11.57 13.18 11.36 13.04 11.15C13.39 10.59 13.6 10.03 13.81 9.4C14.51 9.33 15 8.7 15 8C15 7.3 14.44 6.74 13.81 6.6L13.81 6.6ZM12.55 10.66C12.34 10.52 12.13 10.45 11.85 10.45C11.08 10.45 10.45 11.08 10.45 11.85C10.45 12.13 10.52 12.34 10.66 12.55C10.24 12.76 9.82 12.97 9.33 13.11C9.12 12.62 8.63 12.2 8 12.2C7.44 12.2 6.88 12.55 6.67 13.11C6.18 12.97 5.76 12.83 5.34 12.55C5.48 12.34 5.55 12.13 5.55 11.85C5.55 11.08 4.92 10.45 4.15 10.45C3.87 10.45 3.66 10.52 3.45 10.66C3.24 10.24 3.03 9.82 2.89 9.33C3.45 9.12 3.8 8.56 3.8 8C3.8 7.44 3.45 6.88 2.96 6.74C3.1 6.25 3.24 5.83 3.52 5.41C3.73 5.55 3.94 5.62 4.22 5.62C4.99 5.62 5.62 4.99 5.62 4.22C5.62 3.94 5.55 3.73 5.41 3.52C5.83 3.31 6.25 3.1 6.74 2.96C6.88 3.45 7.44 3.8 8 3.8C8.56 3.8 9.12 3.45 9.33 2.89C9.82 3.03 10.24 3.17 10.66 3.45C10.52 3.66 10.45 3.87 10.45 4.15C10.45 4.92 11.08 5.55 11.85 5.55C12.13 5.55 12.34 5.48 12.55 5.34C12.76 5.76 12.97 6.18 13.11 6.67C12.55 6.88 12.2 7.44 12.2 8C12.2 8.56 12.55 9.12 13.04 9.26C12.97 9.75 12.76 10.24 12.55 10.66L12.55 10.66Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none">
<defs>
<clipPath id="clipPath_1">
<rect width="18.000000" height="15.000000" x="5.000000" y="5.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="导出数据" width="28.000000" height="28.000000" x="0.000000" y="0.000000" />
<g id="容器 742" customFrame="url(#clipPath_1)">
<rect id="容器 742" width="18.000000" height="15.000000" x="5.000000" y="5.000000" />
<rect id="矩形 347" width="2.000000" height="6.000000" x="13.000000" y="5.000000" fill="rgb(132,136,142)" />
<path id="矢量 600" d="M18 11L10 11L14 16L18 11Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
<path id="矢量 601" d="M22 19.9996L22.9999 15.0012L19.9999 12.0011L18.9999 12L21.0003 15.001L17.9999 15.0015L16.9998 17.9987L14 17.9996L11.0001 17.9997L9.99998 15.002L7.00017 15.0028L8.99996 12.0008L8 12.0004L5 15.0023L6.00016 20L22 19.9996Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</g>
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none">
<defs>
<clipPath id="clipPath_0">
<rect width="20.000000" height="20.000000" x="4.000000" y="4.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="数据源" width="28.000000" height="28.000000" x="0.000000" y="0.000000" />
<g id="数据库 1" clip-path="url(#clipPath_0)" customFrame="url(#clipPath_0)">
<rect id="数据库 1" width="20.000000" height="20.000000" x="4.000000" y="4.000000" />
<path id="合并" d="M10.6426 6.48828C11.6719 6.28906 12.791 6.1875 14 6.1875C15.209 6.1875 16.3281 6.28906 17.3574 6.48828C18.3867 6.6875 19.2012 6.96094 19.7988 7.30469C20.3965 7.64844 20.6973 8.01953 20.6973 8.42188L20.6973 9.53906C20.6973 9.94141 20.3984 10.3125 19.7988 10.6563C19.1992 11 18.3867 11.2715 17.3574 11.4727C16.3281 11.6699 15.209 11.7695 14 11.7695C12.791 11.7695 11.6719 11.668 10.6426 11.4688C9.61328 11.2695 8.79883 10.9961 8.20117 10.6523C7.60156 10.3086 7.30273 9.9375 7.30273 9.53516L7.30273 8.41797C7.30273 8.01563 7.60156 7.64453 8.20117 7.30078C8.80078 6.96094 9.61328 6.68945 10.6426 6.48828ZM10.1387 12.5078C11.3359 12.7578 12.623 12.8828 14 12.8828C15.377 12.8828 16.6641 12.7578 17.8613 12.5078C19.0586 12.2578 20.0039 11.8887 20.6953 11.4004L20.6953 12.8828C20.6953 13.2852 20.3965 13.6563 19.7969 14C19.1973 14.3438 18.3848 14.6152 17.3555 14.8164C16.3281 15.0156 15.209 15.1152 14 15.1152C12.791 15.1152 11.6719 15.0137 10.6426 14.8145C9.61328 14.6152 8.79883 14.3418 8.20117 13.998C7.60156 13.6543 7.30273 13.2832 7.30273 12.8809L7.30273 11.3984C7.99609 11.8906 8.94141 12.2598 10.1387 12.5078ZM10.1387 15.8574C11.3359 16.1074 12.623 16.2324 14 16.2324C14.6624 16.2324 15.3041 16.2035 15.9249 16.1456C14.2088 16.4715 12.8443 17.3161 12.2805 18.3935C11.7114 18.3432 11.1654 18.2672 10.6426 18.166C9.61328 17.9668 8.80078 17.6934 8.20117 17.3496C7.60156 17.0059 7.30273 16.6348 7.30273 16.2324L7.30273 14.75C7.9961 15.2383 8.94141 15.6074 10.1387 15.8574ZM17.5 16C17.3281 16 17.1581 16.005 16.9902 16.0148C17.2857 15.9695 17.5761 15.917 17.8613 15.8574C19.0586 15.6074 20.0039 15.2383 20.6953 14.75L20.6953 16.2324C20.6953 16.3614 20.6646 16.4872 20.6031 16.6099C19.7199 16.2251 18.6512 16 17.5 16ZM13 19.5C13 18.1193 15.0147 17 17.5 17C19.9853 17 22 18.1193 22 19.5C22 20.8807 19.9853 22 17.5 22C15.0147 22 13 20.8807 13 19.5ZM17.5 18C18.3284 18 19 18.6716 19 19.5C19 20.3284 18.3284 21 17.5 21C16.6716 21 16 20.3284 16 19.5C16 18.6716 16.6716 18 17.5 18ZM12 19.5L12 19.4861C11.3572 19.4236 10.7367 19.33 10.1387 19.2051C8.94141 18.9551 7.99609 18.5859 7.30273 18.0957L7.30273 19.5781C7.30273 19.9805 7.60156 20.3516 8.20117 20.6953C8.79883 21.0391 9.61328 21.3125 10.6426 21.5117C11.4872 21.6752 12.3923 21.7729 13.3579 21.8027C12.5123 21.1873 12 20.3817 12 19.5Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</g>
</svg>
<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">
<rect id="拓扑 1" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 422" d="M12.7727 11.142C12.4273 11.142 12.107 11.2495 11.8433 11.4327L9.42154 9.6837C9.65586 9.26852 9.78974 8.7891 9.78974 8.27836C9.78974 8.18728 9.7853 8.09723 9.77697 8.00833L12.257 6.54325C12.6396 6.77576 13.1451 6.72689 13.4758 6.39622C13.8641 6.00791 13.8641 5.37835 13.4758 4.99004C13.0875 4.60173 12.4579 4.60173 12.0696 4.99004C11.7835 5.27612 11.7085 5.69309 11.844 6.04812L9.63655 7.35217C9.40869 6.68522 8.9416 6.12922 8.33762 5.78637L8.77881 3.58047C8.81046 3.5835 8.84253 3.58519 8.87497 3.58519C9.42413 3.58519 9.8693 3.14002 9.8693 2.59087C9.8693 2.04173 9.42413 1.59656 8.87497 1.59656C8.32582 1.59656 7.88065 2.04173 7.88065 2.59087C7.88065 2.87182 7.99736 3.12535 8.18469 3.30617L7.73951 5.53213C7.48164 5.45587 7.2087 5.41473 6.9261 5.41473C6.388 5.41473 5.88464 5.56327 5.45458 5.8214L4.08134 4.61294C4.32918 4.12086 4.24801 3.50548 3.83714 3.09461C3.32457 2.58205 2.49355 2.58205 1.98098 3.09461C1.46841 3.60717 1.46842 4.4382 1.98098 4.95078C2.43973 5.40952 3.15341 5.45735 3.66574 5.09492L4.93964 6.21596C4.39895 6.73684 4.06247 7.46826 4.06247 8.27836C4.06247 9.15881 4.45994 9.94631 5.0851 10.4716L3.95324 11.6792C3.74531 11.5408 3.49572 11.4602 3.22724 11.4602C2.50236 11.4602 1.91474 12.0478 1.91474 12.7727C1.91474 13.4975 2.50236 14.0852 3.22724 14.0852C3.95212 14.0852 4.53974 13.4975 4.53974 12.7727C4.53974 12.5477 4.48308 12.3359 4.38332 12.1508L5.62251 10.8288C6.01351 11.029 6.45662 11.142 6.9261 11.142C7.76819 11.142 8.52534 10.7785 9.04931 10.1999L11.3976 11.8958C11.2358 12.149 11.142 12.4499 11.142 12.7727C11.142 13.6733 11.8721 14.4034 12.7727 14.4034C13.6733 14.4034 14.4034 13.6733 14.4034 12.7727C14.4034 11.8721 13.6733 11.142 12.7727 11.142L12.7727 11.142Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none">
<rect id="收藏" width="28.000000" height="28.000000" x="0.000000" y="0.000000" />
<path id="星形 2" d="M15.9534 11.0113C15.9936 11.1349 16.1088 11.2186 16.2388 11.2186L21.6363 11.2188C21.9269 11.2188 22.0478 11.5907 21.8127 11.7615L17.446 14.9343C17.3409 15.0107 17.2969 15.1461 17.3371 15.2697L19.0048 20.4031C19.0946 20.6795 18.7783 20.9094 18.5432 20.7385L14.1763 17.5661C14.0712 17.4897 13.9288 17.4897 13.8237 17.5661L9.45683 20.7385C9.22171 20.9094 8.90539 20.6795 8.99518 20.4031L10.6629 15.2697C10.7031 15.1461 10.6591 15.0107 10.554 14.9343L6.18734 11.7615C5.95224 11.5907 6.07307 11.2188 6.36368 11.2188L11.7612 11.2186C11.8912 11.2186 12.0064 11.1349 12.0466 11.0113L13.7147 5.87799C13.8045 5.60161 14.1955 5.60161 14.2853 5.87799L15.9534 11.0113Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</svg>
<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">
<rect id="绿洲_拓扑图 1" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 422" d="M15.0147 9.88449L14.1867 9.88449L14.1867 8.65516C14.1867 7.53912 13.312 6.63116 12.2368 6.63116L8.37334 6.63116L8.37334 5.08449L9.2317 5.08449C9.42605 5.08449 9.58222 4.94394 9.58222 4.74957L9.58222 2.34115C9.58222 2.11642 9.40148 1.92004 9.17669 1.92004L6.76829 1.92004C6.57396 1.92004 6.41778 2.09179 6.41778 2.28616L6.41778 4.69459C6.41778 4.91931 6.59853 5.08449 6.82329 5.08449L7.62667 5.08449L7.62667 6.63116L3.59268 6.63116C2.62816 6.63116 1.81334 7.45731 1.81334 8.43519L1.81334 9.88449L0.930314 9.88449C0.735986 9.88449 0.58667 10.0572 0.58667 10.2515L0.58667 12.66C0.58667 12.8847 0.760554 13.0489 0.985319 13.0489L3.39371 13.0489C3.58806 13.0489 3.75111 12.9093 3.75111 12.7149L3.75111 10.3066C3.75111 10.0818 3.56351 9.88451 3.33874 9.88451L2.52445 9.88451L2.52445 8.43521C2.52445 7.84276 3.01364 7.34227 3.59268 7.34227L7.62667 7.34227L7.62667 9.88449L6.76829 9.88449C6.57396 9.88449 6.41778 10.0572 6.41778 10.2515L6.41778 12.66C6.41778 12.8847 6.59853 13.0489 6.82329 13.0489L9.2317 13.0489C9.42605 13.0489 9.58222 12.9093 9.58222 12.7149L9.58222 10.3066C9.58222 10.0818 9.40148 9.88451 9.1767 9.88451L8.37334 9.88451L8.37334 7.34227L12.2368 7.34227C12.9199 7.34227 13.4756 7.93123 13.4756 8.65516L13.4756 9.88449L12.6063 9.88449C12.4119 9.88449 12.2489 10.0572 12.2489 10.2515L12.2489 12.66C12.2489 12.8847 12.4365 13.0489 12.6613 13.0489L15.0697 13.0489C15.264 13.0489 15.4133 12.9093 15.4133 12.7149L15.4133 10.3066C15.4133 10.0818 15.2395 9.88449 15.0147 9.88449Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
<template>
<div class="character-relationships">
<div class="headerBox">
<span
v-for="(item, index) in list"
:key="index"
class="headerItem"
:class="{ active: item.value === activeIndex }"
@click="handleChangeLayout(item.value)"
>
<img :src="item.icon" :alt="item.label" />
</span>
</div>
<div class="headerBtnBox">
<img src="./assets/icon-expand.svg" alt="" class="btn-icon" />
<img src="./assets/icon-download.svg" alt="" class="btn-icon" />
<img src="./assets/icon-star.svg" alt="" class="btn-icon" />
</div>
<!-- 主要内容,人物关系图 -->
<div class="mainBox">
<div class="graph" id="relGraph"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
import * as echarts from "echarts";
import {getCharacterGlobalInfo, getCharacterRelation } from "@/api/characterPage/characterPage.js";
import 'default-passive-events';
import { useRoute } from 'vue-router';
const route = useRoute();
const personId = ref(route.query.personId || "Y000064");
// const nodes = [
// {
// id: "c",
// name: "埃隆·马斯克",
// category: 0,
// symbolSize: 80,
// symbol: `image://${Center}`,
// label: {
// show: true,
// position: "bottom",
// formatter: "{n|{b}}",
// rich: {
// n: {
// color: "rgba(5,95,194,1)",
// fontSize: 24,
// fontWeight: 700,
// fontFamily: "Microsoft YaHei",
// lineHeight: 36
// }
// }
// }
// },
// // 从三点钟方向顺时针排序
// { id: "n11", name: "贾斯汀・马斯克", category: 1, symbolSize: 80, symbol: `image://${P11}` },
// { id: "n7", name: "杰弗里·凯斯勒", category: 1, symbolSize: 80, symbol: `image://${P7}`, r: 80 },
// { id: "n6", name: "斯科特·贝森特", category: 1, symbolSize: 80, symbol: `image://${P6}` },
// { id: "n9", name: "道格·伯格姆", category: 1, symbolSize: 80, symbol: `image://${P9}` },
// { id: "n12", name: "史蒂夫・尤尔韦松", category: 1, symbolSize: 80, symbol: `image://${PS}` },
// { id: "n5", name: "拉里・埃里森", category: 1, symbolSize: 80, symbol: `image://${P5}`, r: 80 },
// { id: "n8", name: "马尔科·卢比奥", category: 1, symbolSize: 80, symbol: `image://${P8}` },
// { id: "n10", name: "艾拉・埃伦普里斯", category: 1, symbolSize: 80, symbol: `image://${P10}`, r: 80 },
// { id: "n2", name: "詹姆斯・默多克", category: 1, symbolSize: 80, symbol: `image://${P2}` },
// { id: "n1", name: "唐纳德・特朗普", category: 1, symbolSize: 80, symbol: `image://${P1}` },
// { id: "n4", name: "金博尔・马斯克", category: 1, symbolSize: 80, symbol: `image://${P4}` },
// { id: "n3", name: "格温・肖特韦尔", category: 1, symbolSize: 80, symbol: `image://${P3}`, r: 80 }
// ];
// const links = [
// { source: "n11", target: "c", label: { show: true, formatter: "第一任妻子" } },
// { source: "n7", target: "c", label: { show: true, formatter: "风险投资家" } },
// { source: "n6", target: "c", label: { show: true, formatter: "特斯拉董事" } },
// { source: "n9", target: "c", label: { show: true, formatter: "特斯拉董事" } },
// { source: "n12", target: "c", label: { show: true, formatter: "特斯拉董事" } },
// { source: "n5", target: "c", label: { show: true, formatter: "早期重要投资人" } },
// { source: "n8", target: "c", label: { show: true, formatter: "Boring Company 总裁" } },
// { source: "n10", target: "c", label: { show: true, formatter: "特斯拉独立董事" } },
// { source: "n2", target: "c", label: { show: true, formatter: "特斯拉董事" } },
// { source: "n1", target: "c", label: { show: true, formatter: "美国总统" } },
// { source: "n4", target: "c", label: { show: true, formatter: "马斯克弟弟" } },
// { source: "n3", target: "c", label: { show: true, formatter: "SpaceX 总裁" } }
// ];
// 处理图片代理
const getProxyUrl = (url) => {
if (!url) return "";
const urlStr = String(url);
// 排除非 http 开头(相对路径)、已经是代理链接、或者是本地链接
if (!urlStr.startsWith('http') || urlStr.includes('images.weserv.nl') || urlStr.includes('localhost') || urlStr.includes('127.0.0.1')) {
return url;
}
// 移除协议头 http:// 或 https://
const cleanUrl = urlStr.replace(/^https?:\/\//i, '');
return `https://images.weserv.nl/?url=${encodeURIComponent(cleanUrl)}`;
};
// 创建圆形图片的函数
const createCircularImage = (imageUrl, size) => {
return new Promise((resolve) => {
if (!imageUrl) {
resolve('');
return;
}
const img = new Image();
img.crossOrigin = 'anonymous';
const onLoadSuccess = () => {
try {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// 绘制圆形裁剪区域
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
// 绘制图片
ctx.drawImage(img, 0, 0, size, size);
resolve(canvas.toDataURL('image/png'));
} catch (error) {
resolve(imageUrl);
}
};
img.onload = onLoadSuccess;
img.onerror = () => {
// 如果加载失败,尝试不带 crossOrigin 的方式
const img2 = new Image();
img2.onload = () => {
try {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
ctx.drawImage(img2, 0, 0, size, size);
resolve(canvas.toDataURL('image/png'));
} catch (error) {
resolve(imageUrl);
}
};
img2.onerror = () => {
resolve(imageUrl);
};
img2.src = imageUrl;
};
img.src = imageUrl;
});
};
const nodes = ref([]);
const links = ref([]);
// 人物全局信息
const characterInfo = ref({});
// 人物关系
const CharacterRelation = ref([]);
const getCharacterGlobalInfoFn = async () => {
const params = {
personId: personId.value
};
try{
const res = await getCharacterGlobalInfo(params);
if (res.code === 200) {
if (res.data) {
characterInfo.value = res.data;
}
}
}catch(error){
}
};
const getCharacterRelationFn = async () => {
const params = {
personId: personId.value
};
try{
const res = await getCharacterRelation(params);
if (res.code === 200) {
if (res.data) {
CharacterRelation.value = res.data;
}
}
// 创建圆形头像
const centerImageUrl = getProxyUrl(characterInfo.value.imageUrl);
const centerCircularImage = await createCircularImage(centerImageUrl, 160);
if(CharacterRelation.value.length > 0){
// 并行创建所有圆形头像
const circularImages = await Promise.all(
CharacterRelation.value.map((item) =>
createCircularImage(getProxyUrl(item.imageUrl), 160)
)
);
const centerNode = {
id: "c",
name: characterInfo.value.name,
category: 0,
symbolSize: 80,
symbol: `image://${centerCircularImage}`,
label: {
show: true,
position: "bottom",
formatter: "{n|{b}}",
rich: {
n: {
color: "rgba(5,95,194,1)",
fontSize: 24,
fontWeight: 700,
fontFamily: "Microsoft YaHei",
lineHeight: 36
}
}
}
}
const newNodes = CharacterRelation.value.map((item,index) => {
return{
id: index,
name: item.name,
category: 1,
symbolSize: 80,
symbol: `image://${circularImages[index]}`
}
});
newNodes.push(centerNode);
const newLinks = CharacterRelation.value.map((item,index) =>({
source:index,
target:"c",
label: {
show: true,
formatter: item.relation }
}));
nodes.value = newNodes;
links.value = newLinks;
}else{
nodes.value = [{
id: "c",
name: characterInfo.value.name,
category: 0,
symbolSize: 80,
symbol: `image://${centerCircularImage}`,
label: {
show: true,
position: "bottom",
formatter: "{n|{b}}",
rich: {
n: {
color: "rgba(5,95,194,1)",
fontSize: 24,
fontWeight: 700,
fontFamily: "Microsoft YaHei",
lineHeight: 36
}
}
}
}];
links.value = [];
}
const el = document.getElementById("relGraph");
if (!el) return;
chart = echarts.init(el);
const setOption = () => {
const rect = el.getBoundingClientRect();
const cx = rect.width / 2;
const cy = rect.height / 2;
const radius = Math.min(cx, cy) - 140;
const dataNodes = nodes.value.map((n, i) => {
if (n.id === "c") {
return { ...n, x: cx, y: cy, fixed: true };
}
// 均匀环形分布
const idx = i - 1;
const angle = (idx / (nodes.value.length - 1)) * Math.PI * 2;
const rLocal = radius + (n.r || 0);
const x = cx + rLocal * Math.cos(angle);
const y = cy + rLocal * Math.sin(angle);
return { ...n, x, y };
});
chart.setOption({
tooltip: {},
series: [
{
type: "graph",
layout: activeIndex.value === "圆形布局" ? "none" : "force",
circular: { rotateLabel: true },
force: { repulsion: 800, edgeLength: [80, 160] },
roam: true,
data: activeIndex.value === "圆形布局" ? dataNodes : nodes.value,
links: links.value,
edgeSymbol: ["none", "arrow"],
edgeSymbolSize: [4, 10],
lineStyle: { color: "rgba(174,214,255,1)", width: 2, opacity: 0.8 },
edgeLabel: {
show: true,
position: "middle",
distance: -18,
formatter: ({ data }) => data?.label?.formatter || "",
color: "rgb(5, 95, 194)",
fontSize: 12,
fontWeight: 400,
fontFamily: "Microsoft YaHei",
lineHeight: 24,
backgroundColor: "rgba(231, 243, 255, 1)",
borderRadius: 24,
padding: [0, 12]
},
label: { show: true, position: "bottom", color: "rgb(59,65,75)", fontSize: 16 },
itemStyle: { color: "rgba(5,95,194,1)" },
emphasis: { focus: "adjacency" }
}
]
});
};
setOption();
}catch(error){
}
};
const list = ref([
{
value: "圆形布局",
label: "圆形布局",
icon: new URL('./assets/icon-circle.svg', import.meta.url).href,
},
{
value: "力导向布局",
label: "力导向布局",
icon: new URL('./assets/icon-force.svg', import.meta.url).href,
},
{
value: "树形布局",
label: "树形布局",
icon: new URL('./assets/icon-tree.svg', import.meta.url).href,
},
]);
const activeIndex = ref("圆形布局");
const handleChangeLayout = (layout) => {
activeIndex.value = layout;
};
let chart;
onMounted(async () => {
await getCharacterGlobalInfoFn();
getCharacterRelationFn();
const el = document.getElementById("relGraph");
if (!el) return;
chart = echarts.init(el);
const setOption = () => {
const rect = el.getBoundingClientRect();
const cx = rect.width / 2;
const cy = rect.height / 2;
const radius = Math.min(cx, cy) - 140;
const dataNodes = nodes.value.map((n, i) => {
if (n.id === "c") {
return { ...n, x: cx, y: cy, fixed: true };
}
// 均匀环形分布
const idx = i - 1;
const angle = (idx / (nodes.value.length - 1)) * Math.PI * 2;
const rLocal = radius + (n.r || 0);
const x = cx + rLocal * Math.cos(angle);
const y = cy + rLocal * Math.sin(angle);
return { ...n, x, y };
});
chart.setOption({
tooltip: {},
series: [
{
type: "graph",
layout: activeIndex.value === "圆形布局" ? "none" : "force",
circular: { rotateLabel: true },
force: { repulsion: 800, edgeLength: [80, 160] },
roam: true,
data: activeIndex.value === "圆形布局" ? dataNodes : nodes.value,
links: links.value,
edgeSymbol: ["none", "arrow"],
edgeSymbolSize: [4, 10],
lineStyle: { color: "rgba(174,214,255,1)", width: 2, opacity: 0.8 },
edgeLabel: {
show: true,
position: "middle",
distance: -18,
formatter: ({ data }) => data?.label?.formatter || "",
color: "rgb(5, 95, 194)",
fontSize: 12,
fontWeight: 400,
fontFamily: "Microsoft YaHei",
lineHeight: 24,
backgroundColor: "rgba(231, 243, 255, 1)",
borderRadius: 24,
padding: [0, 12]
},
label: { show: true, position: "bottom", color: "rgb(59,65,75)", fontSize: 16 },
itemStyle: { color: "rgba(5,95,194,1)" },
emphasis: { focus: "adjacency" }
}
]
});
};
setOption();
const onResize = () => chart && chart.resize();
window.addEventListener("resize", onResize);
watch(activeIndex, () => setOption());
onBeforeUnmount(() => {
window.removeEventListener("resize", onResize);
chart && chart.dispose();
});
});
</script>
<style lang="scss" scoped>
* {
padding: 0;
margin: 0;
}
.character-relationships {
width: 1600px;
height: 688px;
background-color: #fff;
margin: 0 auto;
position: relative;
border-radius: 4px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
.headerBox {
position: absolute;
top: 14px;
left: 12px;
display: flex;
gap: 8px;
.headerItem {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 4px;
cursor: pointer;
border: 1px solid rgb(230, 231, 232);
background-color: #fff;
transition: all 0.3s ease;
img {
width: 20px;
height: 20px;
}
}
.active {
background-color: rgba(246, 250, 255, 1);
border-color: rgb(5, 95, 194);
}
}
.headerBtnBox {
position: absolute;
top: 14px;
right: 12px;
display: flex;
gap: 4px;
.btn-icon {
width: 28px;
height: 28px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale(1.1);
}
}
}
.mainBox {
width: 100%;
height: 100%;
padding-top: 42px;
.graph {
width: 100%;
height: 100%;
}
}
}
</style>
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
@update:sort="updateSort" @update:sort="updateSort"
/> />
<!-- 智库报告 / 调查项目 --> <div class="news-body">
<div class="news-body" v-if="activeTab !== 'hearing'">
<NewsSidebar <NewsSidebar
:domain-options="domainOptions" :domain-options="domainOptions"
:time-options="timeOptions" :time-options="timeOptions"
...@@ -17,57 +16,28 @@ ...@@ -17,57 +16,28 @@
@toggle-domain="toggleDomain" @toggle-domain="toggleDomain"
@toggle-time="toggleTime" @toggle-time="toggleTime"
/> />
<div class="news-main">
<div class="news-grid">
<NewsCard
v-for="item in (activeTab === 'local' ? newsList : projectList)"
:key="item.reportId"
:item="item"
/>
</div>
<NewsPagination
:total="activeTab === 'local' ? totalNews : totalProjects"
v-model:current-page="currentPage"
:page-size="pageSize"
/>
</div>
</div>
<!-- 听证会 --> <!-- 智库报告 / 调查项目 -->
<div class="hearing-body" v-else> <div class="news-main" v-if="activeTab !== 'hearing'">
<div class="hearing-left"> <template v-if="(activeTab === 'local' ? newsList : projectList).length > 0">
<div class="hearing-filter-box"> <div class="news-grid">
<div class="hearing-filter-header"> <NewsCard
<div class="hearing-filter-icon"></div> v-for="item in (activeTab === 'local' ? newsList : projectList)"
<div class="hearing-filter-title">科技领域</div> :key="item.reportId"
</div> :item="item"
<div class="hearing-filter-main"> />
<el-checkbox-group class="hearing-checkbox-group" :model-value="hearingSelectedDomains" @change="handleHearingDomainChange">
<el-checkbox class="hearing-filter-checkbox" label="全部领域">全部领域</el-checkbox>
<el-checkbox v-for="item in domainOptions" :key="item.id" class="hearing-filter-checkbox" :label="item.label">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="hearing-filter-box">
<div class="hearing-filter-header">
<div class="hearing-filter-icon"></div>
<div class="hearing-filter-title">发布时间</div>
</div>
<div class="hearing-filter-main">
<el-checkbox-group class="hearing-checkbox-group" :model-value="hearingSelectedTimes" @change="handleHearingTimeChange">
<el-checkbox class="hearing-filter-checkbox" label="全部时间">全部时间</el-checkbox>
<el-checkbox v-for="item in timeOptions" :key="item.id" class="hearing-filter-checkbox" :label="item.label">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</div> </div>
</div> <NewsPagination
:total="activeTab === 'local' ? totalNews : totalProjects"
v-model:current-page="currentPage"
:page-size="pageSize"
/>
</template>
<el-empty v-else description="暂无数据" :image-size="80" class="empty-aligned" />
</div> </div>
<div class="hearing-right"> <!-- 听证会 -->
<div class="hearing-right" v-else>
<div class="hearing-card-box"> <div class="hearing-card-box">
<div class="hearing-card-content"> <div class="hearing-card-content">
<template v-if="hearingList.length > 0"> <template v-if="hearingList.length > 0">
...@@ -87,7 +57,7 @@ ...@@ -87,7 +57,7 @@
<div class="hearing-divider" v-if="index !== hearingList.length - 1"></div> <div class="hearing-divider" v-if="index !== hearingList.length - 1"></div>
</div> </div>
</template> </template>
<el-empty v-else description="暂无数据" :image-size="80" /> <el-empty v-else description="暂无数据" :image-size="80" class="empty-aligned" />
</div> </div>
</div> </div>
<div class="hearing-footer" v-if="hearingList.length > 0"> <div class="hearing-footer" v-if="hearingList.length > 0">
...@@ -108,17 +78,17 @@ ...@@ -108,17 +78,17 @@
<script setup> <script setup>
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import NewsTopBar from './NewsTopBar.vue' import NewsTopBar from './NewsTopBar.vue'
import NewsSidebar from './NewsSidebar.vue' import NewsSidebar from './NewsSidebar.vue'
import NewsCard from './NewsCard.vue' import NewsCard from './NewsCard.vue'
import NewsPagination from './NewsPagination.vue' import NewsPagination from './NewsPagination.vue'
import AreaTag from '@/components/base/AreaTag/index.vue' import AreaTag from '@/components/base/AreaTag/index.vue'
import { getIndustryKeyList } from '@/api/bill/billHome.js' import { getIndustryKeyList } from '@/api/bill/billHome.js'
import { getFindingsReport, getInvestigationProject } from '@/api/characterPage/characterPage.js' import { getFindingsReport, getInvestigationProject, getPersonTestimonies } from '@/api/characterPage/characterPage.js'
const route = useRoute() const props = defineProps({
const personId = computed(() => route.params.personId || route.query.personId || '') personId: { type: String, default: '' }
})
const searchText = ref('') const searchText = ref('')
const activeTab = ref('local') const activeTab = ref('local')
...@@ -137,54 +107,43 @@ const timeOptions = ref([]) ...@@ -137,54 +107,43 @@ const timeOptions = ref([])
const selectedDomains = ref(['all']) const selectedDomains = ref(['all'])
const selectedTimes = ref(['all']) const selectedTimes = ref(['all'])
// ====== 听证会 mock 数据 ====== // ====== 听证会数据 ======
const hearingSelectedDomains = ref(['全部领域'])
const hearingSelectedTimes = ref(['全部时间'])
const hearingCurrentPage = ref(1) const hearingCurrentPage = ref(1)
const hearingPageSize = 10 const hearingPageSize = 10
const hearingTotal = ref(0) const hearingTotal = ref(0)
const hearingList = ref([]) const hearingList = ref([])
const MOCK_HEARINGS = [ async function loadHearings() {
{ id: 1, titleZh: '人工智能在国防领域的应用前景与风险', testimonyDate: '2026-03-15', thinkTankName: '兰德公司', committeeZh: '众议院军事委员会', coverImgUrl: '', domains: ['人工智能', '国防安全'] }, if (!props.personId) return
{ id: 2, titleZh: '半导体供应链安全:对华出口管制的成效评估', testimonyDate: '2026-02-28', thinkTankName: '战略与国际研究中心', committeeZh: '参议院外交委员会', coverImgUrl: '', domains: ['半导体', '国际贸易'] }, hearingList.value = []
{ id: 3, titleZh: '量子计算技术发展与美国国家安全战略', testimonyDate: '2026-01-20', thinkTankName: '布鲁金斯学会', committeeZh: '众议院科学委员会', coverImgUrl: '', domains: ['量子计算', '国家安全'] }, hearingTotal.value = 0
{ id: 4, titleZh: '中美科技竞争背景下的新兴技术标准制定', testimonyDate: '2025-12-10', thinkTankName: '卡内基国际和平基金会', committeeZh: '参议院商务委员会', coverImgUrl: '', domains: ['技术标准', '中美关系'] }, try {
{ id: 5, titleZh: '生物技术发展与全球健康安全治理', testimonyDate: '2025-11-05', thinkTankName: '兰德公司', committeeZh: '众议院能源与商业委员会', coverImgUrl: '', domains: ['生物技术', '公共卫生'] }, const params = {
{ id: 6, titleZh: '太空领域军备控制的挑战与机遇', testimonyDate: '2025-10-18', thinkTankName: '战略与国际研究中心', committeeZh: '参议院军事委员会', coverImgUrl: '', domains: ['太空技术', '国防安全'] }, personId: props.personId,
{ id: 7, titleZh: '5G/6G网络基础设施安全与数据隐私保护', testimonyDate: '2025-09-22', thinkTankName: '布鲁金斯学会', committeeZh: '众议院能源与商业委员会', coverImgUrl: '', domains: ['通信技术', '数据安全'] }, pageNum: hearingCurrentPage.value,
{ id: 8, titleZh: '气候变化对国家安全的影响及应对策略', testimonyDate: '2025-08-14', thinkTankName: '卡内基国际和平基金会', committeeZh: '参议院环境与公共工程委员会', coverImgUrl: '', domains: ['气候变化', '能源技术'] }, pageSize: hearingPageSize,
] sortField: 'createTime',
sortOrder: 'desc',
function loadMockHearings() { }
hearingTotal.value = MOCK_HEARINGS.length if (!selectedDomains.value.includes('all')) {
const start = (hearingCurrentPage.value - 1) * hearingPageSize params.domainIds = selectedDomains.value
const end = start + hearingPageSize }
hearingList.value = MOCK_HEARINGS.slice(start, end).map(item => ({ if (!selectedTimes.value.includes('all')) {
...item, params.years = selectedTimes.value.map(Number)
coverImgUrl: item.coverImgUrl || 'https://via.placeholder.com/56x77?text=Hearing' }
})) const res = await getPersonTestimonies(params)
} if (res.code === 200 && res.data && res.data.content) {
hearingList.value = res.data.content
function handleHearingDomainChange(val) { hearingTotal.value = res.data.totalElements || 0
if (val.includes('全部领域')) { }
hearingSelectedDomains.value = ['全部领域'] } catch (error) {
} else { console.error('获取听证会数据失败:', error)
hearingSelectedDomains.value = val.length > 0 ? val : ['全部领域']
}
}
function handleHearingTimeChange(val) {
if (val.includes('全部时间')) {
hearingSelectedTimes.value = ['全部时间']
} else {
hearingSelectedTimes.value = val.length > 0 ? val : ['全部时间']
} }
} }
function handleHearingPageChange(page) { function handleHearingPageChange(page) {
hearingCurrentPage.value = page hearingCurrentPage.value = page
loadMockHearings() loadHearings()
} }
// ====== 原有逻辑 ====== // ====== 原有逻辑 ======
...@@ -231,17 +190,24 @@ function updateSort(value) { ...@@ -231,17 +190,24 @@ function updateSort(value) {
} }
async function loadNews() { async function loadNews() {
if (!personId.value) return if (!props.personId) return
newsList.value = []
totalNews.value = 0
loading.value = true loading.value = true
try { try {
const params = buildParams({ includeSearchText: true }) const params = buildParams({ includeSearchText: true })
const res = await getFindingsReport(personId.value, params) const res = await getFindingsReport(props.personId, params)
if (res.code === 200 && res.data) { if (res.code === 200 && res.data && res.data.content) {
newsList.value = res.data.content || [] newsList.value = res.data.content.map(item => ({
reportId: item.reportId || item.id,
name: item.name || item.reportName || item.title || '',
content: item.content || item.description || '',
imageUrl: item.imageUrl || '',
time: item.time || item.publishTime || item.publishDate || '',
sourceName: item.sourceName || '',
industryList: item.industryList || [],
}))
totalNews.value = res.data.totalElements || 0 totalNews.value = res.data.totalElements || 0
} else {
newsList.value = []
totalNews.value = 0
} }
} finally { } finally {
loading.value = false loading.value = false
...@@ -249,25 +215,24 @@ async function loadNews() { ...@@ -249,25 +215,24 @@ async function loadNews() {
} }
async function loadProjects() { async function loadProjects() {
if (!personId.value) return if (!props.personId) return
projectList.value = []
totalProjects.value = 0
loading.value = true loading.value = true
try { try {
const params = buildParams() const params = buildParams()
const res = await getInvestigationProject(personId.value, params) const res = await getInvestigationProject(props.personId, params)
if (res.code === 200 && res.data) { if (res.code === 200 && res.data && res.data.content) {
projectList.value = (res.data.content || []).map(item => ({ projectList.value = res.data.content.map(item => ({
reportId: item.projectId, reportId: item.projectId || item.id,
title: item.name, name: item.name || item.projectName || item.title || '',
content: item.description, content: item.description || item.content || '',
imageUrl: item.imageUrl, imageUrl: item.imageUrl || '',
time: item.time, time: item.time || item.publishTime || item.publishDate || '',
sourceName: item.sourceName, sourceName: item.sourceName || '',
industryList: item.industryList, industryList: item.industryList || [],
})) }))
totalProjects.value = res.data.totalElements || 0 totalProjects.value = res.data.totalElements || 0
} else {
projectList.value = []
totalProjects.value = 0
} }
} finally { } finally {
loading.value = false loading.value = false
...@@ -314,7 +279,7 @@ watch(activeTab, (val) => { ...@@ -314,7 +279,7 @@ watch(activeTab, (val) => {
} else if (val === 'capital') { } else if (val === 'capital') {
loadProjects() loadProjects()
} else if (val === 'hearing') { } else if (val === 'hearing') {
loadMockHearings() loadHearings()
} }
}) })
...@@ -322,10 +287,13 @@ watch( ...@@ -322,10 +287,13 @@ watch(
() => [selectedDomains.value, selectedTimes.value], () => [selectedDomains.value, selectedTimes.value],
() => { () => {
currentPage.value = 1 currentPage.value = 1
hearingCurrentPage.value = 1
if (activeTab.value === 'local') { if (activeTab.value === 'local') {
loadNews() loadNews()
} else { } else if (activeTab.value === 'capital') {
loadProjects() loadProjects()
} else if (activeTab.value === 'hearing') {
loadHearings()
} }
}, },
{ deep: true } { deep: true }
...@@ -374,70 +342,6 @@ onMounted(async () => { ...@@ -374,70 +342,6 @@ onMounted(async () => {
} }
/* ====== 听证会布局 ====== */ /* ====== 听证会布局 ====== */
.hearing-body {
display: flex;
gap: 16px;
align-items: flex-start;
}
.hearing-left {
width: 360px;
flex-shrink: 0;
padding-bottom: 24px;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
background: rgba(255, 255, 255, 1);
}
.hearing-filter-box {
margin-top: 16px;
}
.hearing-filter-header {
display: flex;
gap: 17px;
padding-left: 16px;
}
.hearing-filter-icon {
width: 8px;
height: 16px;
background: rgba(5, 95, 194, 1);
border-radius: 0 4px 4px 0;
margin-top: 4px;
}
.hearing-filter-title {
height: 24px;
color: rgba(5, 95, 194, 1);
font-size: 16px;
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
}
.hearing-filter-main {
margin-left: 24px;
margin-top: 12px;
}
.hearing-checkbox-group {
display: grid;
grid-template-columns: repeat(2, 160px);
gap: 8px 4px;
}
.hearing-filter-checkbox {
width: 160px;
height: 24px;
margin-right: 0 !important;
}
:deep(.hearing-filter-checkbox .el-checkbox__label) {
font-size: 16px;
}
.hearing-right { .hearing-right {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
...@@ -516,4 +420,21 @@ onMounted(async () => { ...@@ -516,4 +420,21 @@ onMounted(async () => {
font-weight: 400; font-weight: 400;
line-height: 18px; line-height: 18px;
} }
/* 成果分析 & 听证会 empty 对齐左侧筛选栏,预留底部翻页空间 */
.news-main .empty-aligned {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 33px;
height: calc(100% - 76px);
}
.hearing-card-content .empty-aligned {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 0;
}
</style> </style>
...@@ -158,9 +158,9 @@ function getDateYearsAgo(years) { ...@@ -158,9 +158,9 @@ function getDateYearsAgo(years) {
} }
//获取相关实体 //获取相关实体
const relationBillsList = ref({}); const relationBillsList = ref([]);
const relationAdList = ref({}); const relationAdList = ref([]);
const relationThinkTankList = ref({}); const relationThinkTankList = ref([]);
const getCharacterRelatedEntityFn = async () => { const getCharacterRelatedEntityFn = async () => {
const params = { const params = {
......
...@@ -144,7 +144,14 @@ ...@@ -144,7 +144,14 @@
</div> </div>
</div> </div>
<HistoricalProposal v-if="infoActive === '成果报告'" /> <HistoricalProposal v-if="infoActive === '成果报告'" />
<CharacterRelationships v-if="infoActive === '人物关系'" /> <CharacterRelationships v-if="infoActive === '人物关系'"
:container-height="688" :main-padding-top="42"
:show-relation-chart="false"
third-layout-name="树形布局"
:center-image-size="160" :node-image-size="160"
:center-symbol-size="80" :node-symbol-size="80"
:node-has-detail-style="false"
/>
<RelevantSituation v-if="infoActive === '相关情况'" /> <RelevantSituation v-if="infoActive === '相关情况'" />
<el-dialog v-model="dialogVisible" width="761px" class="viewpoint-dialog" :modal="false" :draggable="true" <el-dialog v-model="dialogVisible" width="761px" class="viewpoint-dialog" :modal="false" :draggable="true"
:lock-scroll="false"> :lock-scroll="false">
...@@ -171,7 +178,7 @@ ...@@ -171,7 +178,7 @@
<script setup> <script setup>
import { ref, onMounted,computed } from "vue"; import { ref, onMounted,computed } from "vue";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import CharacterRelationships from "./components/characterRelationships/index.vue"; import CharacterRelationships from "../CharacterRelationships/index.vue";
import RelevantSituation from "./components/relevantSituation/index.vue"; import RelevantSituation from "./components/relevantSituation/index.vue";
import HistoricalProposal from "./components/historicalProposal/components/NewsTracker.vue"; import HistoricalProposal from "./components/historicalProposal/components/NewsTracker.vue";
import getWordCloudChart from "../../utils/worldCloudChart"; import getWordCloudChart from "../../utils/worldCloudChart";
...@@ -237,7 +244,7 @@ const getCharacterGlobalInfoFn = async () => { ...@@ -237,7 +244,7 @@ const getCharacterGlobalInfoFn = async () => {
} }
} }
} catch (error) { } catch (error) {
console.error('获取人物全局信息失败:', error);
} }
}; };
...@@ -256,7 +263,7 @@ const getCharacterBasicInfoFn = async () => { ...@@ -256,7 +263,7 @@ const getCharacterBasicInfoFn = async () => {
} }
} }
} catch (error) { } catch (error) {
console.error(error); console.error('获取人物基本信息失败:', error);
} }
}; };
const wordLoading=ref(false) const wordLoading=ref(false)
...@@ -285,7 +292,7 @@ const getCharacterViewFn = async () => { ...@@ -285,7 +292,7 @@ const getCharacterViewFn = async () => {
wordLoading.value=true wordLoading.value=true
} }
} catch (error) { } catch (error) {
console.error('获取人物观点失败:', error);
} }
}; };
...@@ -340,7 +347,7 @@ const handleCurrentChange = page => { ...@@ -340,7 +347,7 @@ const handleCurrentChange = page => {
getCharacterLatestDynamicFn(); getCharacterLatestDynamicFn();
}; };
const CharacterLatestDynamic = ref({}); const CharacterLatestDynamic = ref([]);
const total = ref(0); const total = ref(0);
const pageSize = ref(7); const pageSize = ref(7);
const loading = ref(false); const loading = ref(false);
......
...@@ -96,7 +96,11 @@ ...@@ -96,7 +96,11 @@
}" :row-class-name="tableRowClassName" :row-style="{ height: '50px' }" size="large"> }" :row-class-name="tableRowClassName" :row-style="{ height: '50px' }" size="large">
<el-table-column prop="rank" label="排名" width="80" align="center" /> <el-table-column prop="rank" label="排名" width="80" align="center" />
<el-table-column prop="contributor" label="贡献者" min-width="200" /> <el-table-column prop="contributor" label="贡献者" min-width="200" />
<el-table-column prop="totalAmount" label="总捐款" width="150" align="right" /> <el-table-column prop="totalAmount" label="总捐款" width="150" align="right">
<template #default="{ row }">
{{ row.totalAmount }} 美元
</template>
</el-table-column>
<el-table-column prop="donationYear" label="捐款年份" width="150" align="right" /> <el-table-column prop="donationYear" label="捐款年份" width="150" align="right" />
</el-table> </el-table>
<div class="table-pagination"> <div class="table-pagination">
...@@ -171,25 +175,32 @@ ...@@ -171,25 +175,32 @@
<div class="baseInfo-item-title" :class="field.titleClass || ''">{{ field.label }}</div> <div class="baseInfo-item-title" :class="field.titleClass || ''">{{ field.label }}</div>
<div class="baseInfo-item-content" :class="field.contentClass || ''"> <div class="baseInfo-item-content" :class="field.contentClass || ''">
<template v-if="field.type === 'text'"> <template v-if="field.type === 'text'">
{{ characterBasicInfo[field.key] }} <template v-if="characterBasicInfo[field.key] != null && characterBasicInfo[field.key] !== ''">{{ characterBasicInfo[field.key] }}</template>
<span v-else class="undisclosed">未披露</span>
</template> </template>
<template v-else-if="field.type === 'education'"> <template v-else-if="field.type === 'education'">
<template v-if="field.format === 'school+major'"> <template v-if="characterBasicInfo.educationList && characterBasicInfo.educationList.length > 0">
<div v-for="item in characterBasicInfo.educationList" :key="item.school"> <template v-if="field.format === 'school+major'">
{{ item.school + item.major }} <div v-for="item in characterBasicInfo.educationList" :key="item.school">
</div> {{ item.school + item.major }}
</template> </div>
<template v-else> </template>
<span class="education-item" <template v-else>
v-for="value in characterBasicInfo.educationList" <span class="education-item"
:key="value.school"> v-for="value in characterBasicInfo.educationList"
{{ value.school + "(" + value.major + ")" }} :key="value.school">
</span> {{ value.school + "(" + value.major + ")" }}
</span>
</template>
</template> </template>
<span v-else class="undisclosed">未披露</span>
</template> </template>
<template v-else-if="field.type === 'industry'"> <template v-else-if="field.type === 'industry'">
<span class="span" v-for="item in characterBasicInfo[field.key]" <template v-if="characterBasicInfo[field.key] && characterBasicInfo[field.key].length > 0">
:key="item">{{ item.industryName }}</span> <span class="span" v-for="item in characterBasicInfo[field.key]"
:key="item">{{ item.industryName }}</span>
</template>
<template v-else><span class="undisclosed">未披露</span></template>
</template> </template>
</div> </div>
</div> </div>
...@@ -285,7 +296,7 @@ ...@@ -285,7 +296,7 @@
<!-- 成果报告 (智库) --> <!-- 成果报告 (智库) -->
<template v-if="config.historicalProposalType === 'news'"> <template v-if="config.historicalProposalType === 'news'">
<NewsTracker v-if="infoActive === '成果报告'" /> <NewsTracker v-if="infoActive === '成果报告'" :person-id="personIdRef" />
</template> </template>
<!-- 人物关系 - 传递 personId 确保 graph 能获取数据 --> <!-- 人物关系 - 传递 personId 确保 graph 能获取数据 -->
...@@ -420,7 +431,7 @@ const isSenate = (text) => { ...@@ -420,7 +431,7 @@ const isSenate = (text) => {
}; };
// 子组件 // 子组件
import CharacterRelationships from '../techLeader/components/characterRelationships/index.vue'; import CharacterRelationships from '../CharacterRelationships/index.vue';
import RelevantSituation from '../techLeader/components/relevantSituation/index.vue'; import RelevantSituation from '../techLeader/components/relevantSituation/index.vue';
import BillTracker from '../memberOfCongress/components/historicalProposal/components/BillTracker.vue'; import BillTracker from '../memberOfCongress/components/historicalProposal/components/BillTracker.vue';
import PotentialNews from '../memberOfCongress/components/historicalProposal/components/PotentialNews.vue'; import PotentialNews from '../memberOfCongress/components/historicalProposal/components/PotentialNews.vue';
...@@ -1256,6 +1267,9 @@ onMounted(() => { ...@@ -1256,6 +1267,9 @@ onMounted(() => {
font-family: "Microsoft YaHei"; font-family: "Microsoft YaHei";
line-height: 24px; line-height: 24px;
color: rgb(59, 65, 75); color: rgb(59, 65, 75);
.undisclosed {
color: rgb(184, 191, 199);
}
} }
.long { .long {
...@@ -1754,6 +1768,9 @@ onMounted(() => { ...@@ -1754,6 +1768,9 @@ onMounted(() => {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px 12px; gap: 8px 12px;
.undisclosed {
color: rgb(184, 191, 199);
}
} }
.education-item { .education-item {
......
...@@ -48,7 +48,7 @@ const getCharacterGlobalInfoFn = async () => { ...@@ -48,7 +48,7 @@ const getCharacterGlobalInfoFn = async () => {
} }
} }
}catch(error){ }catch(error){
console.error('获取人物全局信息失败:', error);
} }
}; };
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
</div> </div>
<!-- 表格内容 --> <!-- 表格内容 -->
<div class="table-body"> <div class="table-body" v-if="personList.length > 0">
<div class="table-row" v-for="(item, index) in personList" :key="index"> <div class="table-row" v-for="(item, index) in personList" :key="index">
<!-- 人物信息列 --> <!-- 人物信息列 -->
<div class="row-col col-person" @click="handleClickToCharacter(item.personId)"> <div class="row-col col-person" @click="handleClickToCharacter(item.personId)">
...@@ -51,6 +51,7 @@ ...@@ -51,6 +51,7 @@
</div> </div>
</div> </div>
</div> </div>
<el-empty v-else description="暂无数据" :image-size="80" style="height: 300px; display: flex; flex-direction: column; justify-content: center;" />
</div> </div>
</template> </template>
<script setup> <script setup>
......
<!-- SourceLibrary.vue --> <!-- SourceLibrary.vue -->
<template> <template>
<div class="source-library-container"> <div class="source-library-container">
<div class="source-library-grid"> <div class="source-library-grid" v-if="PersonResource.length > 0">
<div <div
v-for="(item, index) in PersonResource" v-for="(item, index) in PersonResource"
:key="index" :key="index"
class="source-library-card" class="source-library-card"
@click="handleClcikToCharacter(item.id)" @click="handleClcikToCharacter(item.id)"
> >
<div class="source-library-avatar-wrapper"> <div class="source-library-avatar-wrapper">
<img :src="item.avatar" alt="" class="source-library-avatar" /> <img :src="item.avatar" alt="" class="source-library-avatar" />
</div> </div>
<div class="source-library-text-content"> <div class="source-library-text-content">
<div class="card-main" "> <div class="card-main">
<h3 class="source-library-name">{{ item.name }}</h3> <h3 class="source-library-name">{{ item.name }}</h3>
<p class="source-library-title" v-if="item.title">{{ item.title }}</p> <p class="source-library-title" v-if="item.title">{{ item.title }}</p>
<div class="taglist"> <div class="taglist">
...@@ -26,7 +26,8 @@ ...@@ -26,7 +26,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="page"> <el-empty v-else description="暂无数据" :image-size="80" />
<div class="page" v-if="PersonResource.length > 0">
<div class="count">{{ `共 ${total} 项` }}</div> <div class="count">{{ `共 ${total} 项` }}</div>
<el-pagination <el-pagination
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
...@@ -200,10 +201,10 @@ const handlePageChange = p => { ...@@ -200,10 +201,10 @@ const handlePageChange = p => {
} }
.source-library-grid { .source-library-grid {
width: 1600px; width: 100%;
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(5, 1fr); grid-template-rows: repeat(5, 1fr);
gap: 16px 16px; gap: 16px 16px;
} }
...@@ -353,8 +354,7 @@ const handlePageChange = p => { ...@@ -353,8 +354,7 @@ const handlePageChange = p => {
} }
.page { .page {
/* width: 1221px; */ width: 100%;
width: 1600px;
height: 40px; height: 40px;
display: flex; display: flex;
align-items: center; align-items: center;
......
<!-- SpeechStance.vue --> <!-- SpeechStance.vue -->
<template> <template>
<div class="speech-stance-container"> <div class="speech-stance-container">
<div class="speech-stance-grid"> <div class="speech-stance-grid" v-if="PersonRelation.length > 0">
<div v-for="(item, index) in PersonRelation" :key="index" class="speech-stance-card"> <div v-for="(item, index) in PersonRelation" :key="index" class="speech-stance-card">
<div class="speech-stance-avatar-wrapper" @click="handleClcikToCharacter(item.personId)"> <div class="speech-stance-avatar-wrapper" @click="handleClcikToCharacter(item.personId)">
<img :src="item.personImage" alt="" class="speech-stance-avatar" /> <img :src="item.personImage" alt="" class="speech-stance-avatar" />
...@@ -14,19 +14,20 @@ ...@@ -14,19 +14,20 @@
<p class="speech-stance-content" :title="item.remarks"><span class="speech-stance-text">{{ item.remarks }}</span></p> <p class="speech-stance-content" :title="item.remarks"><span class="speech-stance-text">{{ item.remarks }}</span></p>
</div> </div>
</div> </div>
</div> </div>
<el-empty v-else description="暂无数据" :image-size="80" />
</div> </div>
</template> </template>
<script setup> <script setup>
// 导入数据 // 导入数据
import { onMounted, ref,defineProps,watch } from "vue"; import { onMounted, ref,defineProps,watch } from "vue";
import speechStance from '../json/speechStance.json';
import {getPersonRelation} from "@/api/technologyFigures/technologyFigures" import {getPersonRelation} from "@/api/technologyFigures/technologyFigures"
import { getPersonSummaryInfo } from "@/api/technologyFigures/technologyFigures"; import { getPersonSummaryInfo } from "@/api/technologyFigures/technologyFigures";
import { ElMessage } from "element-plus";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const props = defineProps({ const props = defineProps({
......
...@@ -86,7 +86,8 @@ ...@@ -86,7 +86,8 @@
</template> </template>
<!-- 主内容区域 --> <!-- 主内容区域 -->
<div class="box1-wrapper"> <el-empty v-if="newsDynamics.length === 0" description="暂无数据" :image-size="80" style="height: 352px; display: flex; flex-direction: column; justify-content: center;" />
<div class="box1-wrapper" v-else>
<div class="box1-left" @click="handleSwithCurBill('left')"> <div class="box1-left" @click="handleSwithCurBill('left')">
<img src="./assets/images/box1-left.png" alt="" /> <img src="./assets/images/box1-left.png" alt="" />
</div> </div>
...@@ -265,7 +266,8 @@ ...@@ -265,7 +266,8 @@
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="box6-main" id="box6Chart"></div> <div class="box6-main" id="box6Chart" v-if="box6Chart.data.length > 0"></div>
<el-empty v-else description="暂无数据" :image-size="80" style="height: 100%; display: flex; flex-direction: column; justify-content: center;" />
<div class="data-origin-box" style="margin-top: 18px;"> <div class="data-origin-box" style="margin-top: 18px;">
<div class="data-origin-icon"><img :src="tipsIcon" alt="" /></div> <div class="data-origin-icon"><img :src="tipsIcon" alt="" /></div>
<div class="data-origin-text">科技人物观点涉及领域变化趋势,数据来源:公开新闻报道及社交媒体数据</div> <div class="data-origin-text">科技人物观点涉及领域变化趋势,数据来源:公开新闻报道及社交媒体数据</div>
...@@ -282,7 +284,8 @@ ...@@ -282,7 +284,8 @@
<div class="box7-header-title">{{ "科技人物类型" }}</div> <div class="box7-header-title">{{ "科技人物类型" }}</div>
</div> </div>
</div> </div>
<div class="box7-main" id="box7Chart"></div> <div class="box7-main" id="box7Chart" v-if="chart7Data.name.length > 0"></div>
<el-empty v-else description="暂无数据" :image-size="80" style="height: 100%; display: flex; flex-direction: column; justify-content: center;" />
<div class="data-origin-box" style="padding-top: 3px;"> <div class="data-origin-box" style="padding-top: 3px;">
<div class="data-origin-icon"><img :src="tipsIcon" alt="" /></div> <div class="data-origin-icon"><img :src="tipsIcon" alt="" /></div>
<div class="data-origin-text">科技人物类型,数据来源:公开新闻报道及社交媒体数据</div> <div class="data-origin-text">科技人物类型,数据来源:公开新闻报道及社交媒体数据</div>
...@@ -882,14 +885,16 @@ const viewSelect = ref("国会议员"); ...@@ -882,14 +885,16 @@ const viewSelect = ref("国会议员");
watch(areaSelect, async (newVal) => { watch(areaSelect, async (newVal) => {
await handlegetOptionAreaChangeFn(newVal); await handlegetOptionAreaChangeFn(newVal);
const chart6 = getMultiLineChart(box6Chart.value.title, ...box6Chart.value.data.map(d => d.value)); if (box6Chart.value.data.length > 0) {
setChart(chart6, "box6Chart"); const chart6 = getMultiLineChart(box6Chart.value.title, box6Chart.value.data);
setChart(chart6, "box6Chart");
}
}); });
onMounted(async () => { onMounted(async () => {
await handlgetnewsDynamicsFn(); await handlgetnewsDynamicsFn();
curnews.value = newsDynamics.value[0]; curnews.value = newsDynamics.value?.[0] || curnews.value;
handleGetBillRiskSignalFn(); handleGetBillRiskSignalFn();
await handlegetCharacterOpinionWordCloudFn(); await handlegetCharacterOpinionWordCloudFn();
await handlegetOptionAreaChangeFn(areaSelect.value); await handlegetOptionAreaChangeFn(areaSelect.value);
...@@ -898,16 +903,22 @@ onMounted(async () => { ...@@ -898,16 +903,22 @@ onMounted(async () => {
await handlgetPersonTypeFn(); await handlgetPersonTypeFn();
// 词云 // 词云
const wordCloudChart = getWordCloudChart(CharacterOpinionWordCloud.value); if (CharacterOpinionWordCloud.value.length > 0) {
setChart(wordCloudChart, "box5Chart"); const wordCloudChart = getWordCloudChart(CharacterOpinionWordCloud.value);
setChart(wordCloudChart, "box5Chart");
}
// 领域趋势 // 领域趋势
const chart6 = getMultiLineChart(box6Chart.value.title, ...box6Chart.value.data.map(d => d.value)); if (box6Chart.value.data.length > 0) {
setChart(chart6, "box6Chart"); const chart6 = getMultiLineChart(box6Chart.value.title, box6Chart.value.data);
setChart(chart6, "box6Chart");
}
// 人物类型 // 人物类型
const chart7 = getDonutChart(chart7Data.value.name, chart7Data.value.value, true); if (chart7Data.value.name.length > 0) {
setChart(chart7, "box7Chart"); const chart7 = getDonutChart(chart7Data.value.name, chart7Data.value.value, true);
setChart(chart7, "box7Chart");
}
}); });
</script> </script>
......
import * as echarts from "echarts"; import * as echarts from "echarts";
const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6) => { // 预定义颜色池,循环使用
const colorPool = [
"rgba(10, 87, 166, 1)",
"rgba(64, 196, 181, 1)",
"rgba(114, 46, 209, 1)",
"rgba(245, 141, 46, 1)",
"rgba(111, 180, 255, 1)",
"rgba(196, 92, 68, 1)",
"rgba(82, 196, 26, 1)",
"rgba(250, 173, 20, 1)",
"rgba(19, 168, 168, 1)",
"rgba(245, 34, 45, 1)",
"rgba(47, 84, 235, 1)",
"rgba(141, 113, 196, 1)"
];
const getMultiLineChart = (dataX, seriesData) => {
const series = seriesData.map((item, index) => {
const baseColor = colorPool[index % colorPool.length] || "rgba(10, 87, 166, 1)";
// 提取 rgba 值用于渐变
const rgbaMatch = baseColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
const r = rgbaMatch?.[1] || "10";
const g = rgbaMatch?.[2] || "87";
const b = rgbaMatch?.[3] || "166";
return {
name: item.name,
type: "line",
lineStyle: { width: 1.5 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: `rgba(${r}, ${g}, ${b}, 0.2)` },
{ offset: 1, color: `rgba(${r}, ${g}, ${b}, 0)` }
])
},
emphasis: { focus: "series" },
data: item.value
};
});
return { return {
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
axisPointer: { axisPointer: {
type: "cross", type: "cross",
label: { label: { backgroundColor: "#6a7985" }
backgroundColor: "#6a7985"
}
} }
}, },
grid: { grid: {
...@@ -19,7 +56,8 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6 ...@@ -19,7 +56,8 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6
containLabel: true containLabel: true
}, },
legend: { legend: {
data: ["人工智能", "集成电路", "量子科技", "生物科技", "通信网络", "能源"], // data: ["人工智能", "集成电路", "量子科技", "生物科技", "通信网络", "能源"],
data: seriesData.map(item => item.name),
show: true, show: true,
top: "0px", top: "0px",
left: "center", left: "center",
...@@ -27,20 +65,21 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6 ...@@ -27,20 +65,21 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6
color: "rgba(95, 101, 108, 1)", color: "rgba(95, 101, 108, 1)",
fontFamily: "Microsoft YaHei", fontFamily: "Microsoft YaHei",
fontSize: "16px" fontSize: "16px"
}, // 设置图例标记为圆形 },
icon: "circle", icon: "circle",
itemWidth: 10, // 圆的直径 itemWidth: 10,
itemHeight: 10, // 圆的直径 itemHeight: 10,
itemGap: 20 // 图例项之间的间距 itemGap: 20
}, },
color: [ // color: [
"rgba(10, 87, 166, 1)", // 人工智能(深蓝色) // "rgba(10, 87, 166, 1)",
"rgba(64, 196, 181, 1)", // 集成电路(青绿色) // "rgba(64, 196, 181, 1)",
"rgba(114, 46, 209, 1)", // 量子科技(紫色) // "rgba(114, 46, 209, 1)",
"rgba(245, 141, 46, 1)", // 生物科技(橙色) // "rgba(245, 141, 46, 1)",
"rgba(111, 180, 255, 1)", // 通信网络(浅蓝色) // "rgba(111, 180, 255, 1)",
"rgba(196, 92, 68, 1)" // 能源(红褐色) // "rgba(196, 92, 68, 1)"
], // ],
color: colorPool,
xAxis: [ xAxis: [
{ {
type: "category", type: "category",
...@@ -51,9 +90,7 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6 ...@@ -51,9 +90,7 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6
color: "rgba(59, 65, 75, 1)" color: "rgba(59, 65, 75, 1)"
}, },
axisLine: { axisLine: {
lineStyle: { lineStyle: { color: "#E7F3FF" }
color: "#E7F3FF"
}
} }
} }
], ],
...@@ -67,151 +104,10 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6 ...@@ -67,151 +104,10 @@ const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6
color: "#E7F3FF" color: "#E7F3FF"
} }
}, },
axisLabel: { axisLabel: { fontSize: 14 }
fontSize: 14 // 设置X轴字体大小
}
} }
], ],
series: [ series
{
name: "人工智能",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(10, 87, 166, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(10, 87, 166, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY1
},
{
name: "集成电路",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(64, 196, 181, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(64, 196, 181, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY2
},
{
name: "量子科技",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(114, 46, 209, 1)" // 起始颜色
},
{
offset: 1,
color: "rgba(114, 46, 209, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY3
},
{
name: "生物科技",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(245, 141, 46, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(245, 141, 46, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY4
},
{
name: "通信网络",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(111, 180, 255, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(111, 180, 255, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY5
},
{
name: "能源",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(196, 92, 68, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(196, 92, 68, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY6
}
]
}; };
}; };
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论