Compare commits

...

11 Commits

Author SHA1 Message Date
c1e9a4be83 chore: sync local changes and add documentation
- Update yarn.lock
- Add project implementation docs in docs/
- Add personal internship experience notes in 实习讲解/
2026-06-29 19:47:30 +08:00
2cfe01694a chore: ignore .obsidian and .claudian local tool directories 2026-06-29 19:41:09 +08:00
fmk1023
98aeef9af8 态势感知平台-登录跳转到态势感知数据中台 2026-03-17 14:53:23 +08:00
d266377
1250268701 态势感知感知平台-武汉市开源生态建设-统计概览2样式修改 2026-02-04 15:03:39 +08:00
d266377
bb55997e01 态势感知平台-开源生态与贡献价值全景-全球开源企业贡献榜排名样式修改 2026-02-04 14:37:17 +08:00
d266377
5fd0e37e71 态势感知平台-武汉市开源生态建设-全国重点城市开源镜像站接口对接 2026-02-04 14:30:55 +08:00
d266377
2c49f9a92a 态势感知平台-武汉市开源生态建设-开放原子校园行接口对接 2026-02-04 14:24:54 +08:00
d266377
efd9493cb0 态势感知平台-武汉市开源生态建设-重点城市高校开源社团数量接口对接 2026-02-04 14:16:10 +08:00
d266377
c5fc4a301b 态势感知平台-武汉市开源生态建设-武汉知名开源社区接口对接 2026-02-04 14:08:33 +08:00
d266377
9ce6bbe07f 态势感知平台-武汉市开源生态建设-城市社区数量排行接口对接 2026-02-04 11:37:29 +08:00
d266377
0901ddd469 态势感知平台-武汉市开源生态建设-武汉明星开发者贡献榜接口对接 2026-02-04 11:11:24 +08:00
18 changed files with 4342 additions and 51 deletions

4
.gitignore vendored
View File

@@ -41,3 +41,7 @@ components.d.ts
component.md
./deploy.js
# 本地工具配置目录
.obsidian/
.claudian/

View File

@@ -0,0 +1,364 @@
# AI 智能助手功能 — 面试版
> 简历原话:**"集成 @matechat/core 实现 AI 智能助手功能,支持多轮对话、思考过程展示、组件化消息渲染(软件详情/预警列表/智能推荐)"**
>
> 这篇文档帮你理解这句话背后到底做了什么,以及面试时怎么讲。
---
## 一、先搞清楚:这句话到底是什么意思?
拆成四部分理解:
| 关键词 | 含义 | 项目中对应 |
|--------|------|------------|
| **集成 @matechat/core** | 使用华为 MateChat 组件库提供聊天 UI | McBubble气泡、McInput输入框、McMarkdownCardMarkdown 渲染)等组件 |
| **多轮对话** | 用户可以连续提问AI 记住上下文 | 前端创建 conversationId后端维护对话历史每次请求带上 conversationId |
| **思考过程展示** | AI 回答前展示它"想"了什么步骤 | 后端返回 states 对象,前端解析为"意图分析→SQL判断→SQL执行"步骤展示 |
| **组件化消息渲染** | 不同类型的 AI 回答用不同组件展示 | 5 种消息类型:普通文本、开源软件列表、预警列表、软件详情、数据表格 |
**一句话概括**:我用 MateChat 组件库搭建了 AI 聊天界面实现了多轮对话9 个 API、思考过程可视化、以及 5 种消息类型的组件化渲染。
---
## 二、整体架构
```
┌─────────────────────────────────────────────────┐
│ AI 助手页面(/ai
│ │
│ ┌──────────┐ ┌──────────────────────────────┐ │
│ │ 左侧边栏 │ │ 对话区域 │ │
│ │ │ │ ┌──────────────────────────┐ │ │
│ │ 历史会话列表│ │ │ McBubble用户消息 │ │ │
│ │ 收藏会话 │ │ └──────────────────────────┘ │ │
│ │ 搜索/删除 │ │ ┌──────────────────────────┐ │ │
│ │ 批量管理 │ │ │ McBubbleAI回复 │ │ │
│ │ │ │ │ ├── 思考过程(可折叠) │ │ │
│ │ │ │ │ ├── OssList软件列表 │ │ │
│ │ │ │ │ ├── WarningList预警列表 │ │ │
│ │ │ │ │ ├── OssDetail软件详情 │ │ │
│ │ │ │ │ ├── AIList数据表格 │ │ │
│ │ │ │ │ └── OtherMarkdown文本 │ │ │
│ │ │ │ └──────────────────────────┘ │ │
│ │ │ │ ┌──────────────────────────┐ │ │
│ │ │ │ │ 猜你想问(推荐问题) │ │ │
│ │ │ │ └──────────────────────────┘ │ │
│ │ │ │ ┌──────────────────────────┐ │ │
│ │ │ │ │ McInput输入框 │ │ │
│ │ │ │ └──────────────────────────┘ │ │
│ └──────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
---
## 三、@matechat/core 用在了哪里?
MateChat 是华为开源的 AI 聊天 UI 组件库。项目中用了这些组件:
| 组件 | 用在哪 | 干什么 |
|------|--------|--------|
| `McBubble` | 对话区域 | 消息气泡(区分用户/AI支持 loading 状态) |
| `McInput` | 底部输入框 | 文本输入,支持字数限制、回车发送、清空 |
| `McMarkdownCard` | AI 文本回复 | 渲染 Markdown 格式的 AI 回答 |
| `McLayout` / `McLayoutContent` / `McLayoutSender` | 弹窗版 AI | KnowledgeHub 的 AI 弹窗布局 |
全局注册后直接使用:
```typescript
// main.ts
import MateChat from '@matechat/core';
app.use(MateChat);
```
```vue
<!-- 直接用不需要 import -->
<McBubble :content="msg.content" :align="'right'" /> <!-- 用户消息靠右 -->
<McBubble :loading="true"> <!-- AI 回复靠左 + loading -->
<Other :content="msg.content" />
</McBubble>
<McInput @submit="onSubmit" /> <!-- 底部输入框 -->
```
---
## 四、怎么实现的?(面试核心)
### 4.1 多轮对话
**问题**用户连续问多个问题AI 要记住之前的对话内容。
**解决方案**:用 conversationId 串联整个对话,后端维护历史。
```
用户发第一条消息
前端没有 conversationId → 调用 createConversation API → 获得 conversationId
前端发送消息:{ userId, conversationId, question }
后端根据 conversationId 找到历史消息 → AI 结合历史回答
用户发第二条消息 → 带同一个 conversationId → 后端知道上下文
```
```typescript
// 1. 首次对话:创建会话
const CONV_ID = ref('');
const createConversation = async () => {
const { data } = await fetchCreateConversation({ userId: username });
CONV_ID.value = data.data.conversationId; // 保存会话 ID
};
// 2. 发送消息:带上 conversationId
const getAIAnswer = async (question) => {
const { data } = await fetchChatUseSql({
userId: username,
conversationId: CONV_ID.value, // ← 后端靠这个找到对话历史
question: question,
});
// 处理 AI 回复...
};
// 3. 切换历史会话:加载旧对话
const handleSelectConv = (convId) => {
CONV_ID.value = convId;
getConversationDetail(convId); // 从后端加载完整对话记录
};
```
**会话管理功能**(左侧边栏):
- 查看历史/收藏会话列表
- 搜索会话
- 重命名会话
- 收藏/取消收藏
- 删除会话
- 批量管理
---
### 4.2 思考过程展示
**问题**AI 回答时用户看不到它在干什么,体验像"黑盒"。
**解决方案**:后端返回 `states` 对象,前端解析为步骤列表展示。
```typescript
// 后端返回的数据包含 states
const { text, type, states } = row;
// 解析思考步骤
const processThinkStates = (states) => {
let thinkProcess = []; // 主步骤
let steps = []; // 子步骤
Object.keys(states).forEach(key => {
if (key === 'steps') {
// 子步骤
Object.keys(states[key]).forEach(subKey => {
steps.push({ name: subKey, status: states[key][subKey] });
});
} else {
// 主步骤意图分析、SQL有效性判断、SQL执行
thinkProcess.push({ name: key, status: states[key] });
}
});
return { thinkProcess, steps };
};
```
展示效果:
```
▼ 思考过程:
✓ 意图分析 成功
✓ SQL有效性判断 成功
✓ SQL执行 成功
✓ 数据查询 成功
✓ 结果格式化 成功
```
用户可以点击折叠/展开思考过程。每个步骤显示绿色"成功"或红色"失败"。
---
### 4.3 组件化消息渲染5 种消息类型)
**问题**AI 的回答不只是纯文本,还可能是表格、软件详情等不同格式。
**解决方案**:根据后端返回的 `type` 字段,用不同组件渲染。
```vue
<template v-for="(msg, idx) in messages" :key="idx">
<!-- 用户消息纯文本气泡 -->
<McBubble v-if="msg.role === 'user'" :content="msg.content" />
<!-- AI 消息根据 type 选择不同组件 -->
<McBubble v-else :loading="msg.loading">
<!-- 思考过程可折叠 -->
<div class="think-toggle-btn" @click="toggleThink(idx)">...</div>
<!-- 根据 type 渲染不同内容 -->
<OssList v-if="msg.type === 1" /> <!-- 开源软件列表 -->
<WarningList v-else-if="msg.type === 2" /> <!-- 预警列表 -->
<OssDetail v-else-if="msg.type === 'critical_software_info'" :id="msg.id" /> <!-- 软件详情 -->
<AIList v-else-if="msg.type === 'df'" :content="msg.content" /> <!-- 数据表格 -->
<Other v-else :content="msg.content" /> <!-- Markdown 文本 -->
</McBubble>
</template>
```
**5 种消息类型详解**
| type 值 | 渲染组件 | 用户看到什么 |
|---------|----------|------------|
| `1` | OssList | 开源软件列表表格(有分页、导出功能) |
| `2` | WarningList | 预警列表表格(有严重程度标签:高危/中危/低危) |
| `'critical_software_info'` | OssDetail | 软件详情页面(嵌入完整的软件详情组件) |
| `'df'` | AIList | 通用数据表格动态列名、JSON 解析) |
| `'text'` | Other | Markdown 格式的文本回答 |
| `'sql_error'` | Other | 错误信息(带错误图标) |
**type 判断逻辑**(后端返回 `df` 类型时还要二次判断):
```typescript
if (type === 'df') {
// 默认当普通表格
msg.type = 'df';
msg.content = row[type];
// 但如果表名是 critical_software_info 且只有一条数据 → 当软件详情
if (table_name.length === 1 && table_name[0] === 'critical_software_info') {
let arr = JSON.parse(row[type]);
if (arr.length === 1) {
msg.type = 'critical_software_info'; // 升级为软件详情
msg.id = arr[0].id;
msg.software_name = arr[0].software_name;
}
}
}
```
---
### 4.4 猜你想问(推荐问题)
AI 回答后,自动推荐相关后续问题:
```typescript
// AI 回答后,调用推荐问题 API
const fetchFollowupQuestions = async (question, df_id) => {
const { data } = await FetchFollowupQuestions({ question, df_id });
if (data.code === 200) {
followupQuestions.value = data.data; // 更新推荐问题列表
}
};
// 用户点击推荐问题 → 直接发送
<a v-for="q in followupQuestions" @click="onSubmit(q)">
<span>{{ q.question }}</span>
</a>
```
---
### 4.5 AI API 接口总览
项目对接了 9 个 AI 后端接口:
| 接口 | 干什么 | 调用时机 |
|------|--------|----------|
| `createConversation` | 创建新会话 | 用户发第一条消息时 |
| `chat_use_sql` | 发送消息/获取 AI 回复 | 每次用户发送消息 |
| `conversationList` | 获取会话列表 | 打开侧边栏时 |
| `conversationDetail` | 获取会话详情 | 点击历史会话时 |
| `provide_followup_questions` | 获取推荐问题 | AI 回答后 |
| `changeConversationTitle` | 重命名会话 | 用户编辑会话名 |
| `toggleConversationCollect` | 收藏/取消收藏 | 用户点击收藏 |
| `deleteConversation` | 删除会话 | 用户删除 |
| `update_recommend_questions` | 刷新推荐问题 | 用户点击"换一批" |
---
## 五、整体架构总结
```
@matechat/core 提供McBubble气泡+ McInput输入框+ McMarkdownCardMarkdown渲染
搭建聊天界面骨架
在 McBubble 内部渲染自定义内容组件:
├── OssList软件列表 ← type=1
├── WarningList预警列表 ← type=2
├── OssDetail软件详情 ← type='critical_software_info'
├── AIList数据表格 ← type='df'
└── OtherMarkdown文本 ← 默认
9 个 API 接口对接后端
conversationId 串联多轮对话
states 对象解析为思考过程
```
---
## 六、面试问答准备
### Q1"集成 @matechat/core 实现 AI 智能助手"具体做了什么?
> 我用华为开源的 MateChat 组件库搭建了 AI 聊天界面,核心组件是 McBubble消息气泡和 McInput输入框。实现了三个关键功能第一多轮对话通过 conversationId 串联整个对话,后端维护历史上下文,前端对接了 9 个 API 接口管理会话;第二,思考过程展示,后端返回 states 对象,前端解析为"意图分析→SQL判断→SQL执行"的步骤列表,支持折叠展开;第三,组件化消息渲染,根据后端返回的 type 字段,用 5 种不同组件渲染 AI 回复——开源软件列表、预警列表、软件详情、数据表格、Markdown 文本。
### Q2多轮对话怎么实现的
> 前后端配合。用户发第一条消息时,前端调 createConversation API 获取 conversationId。之后每条消息都带上这个 conversationId后端根据它找到整个对话历史AI 结合上下文回答。切换历史会话时,调 conversationDetail API 加载完整对话记录。前端还有完整的会话管理:历史列表、收藏、搜索、重命名、删除、批量管理。
### Q3思考过程展示怎么做的
> 后端 AI 接口返回的数据里有一个 states 对象包含主步骤意图分析、SQL有效性判断、SQL执行和子步骤。我写了一个 processThinkStates 函数解析这个对象,转换为 `{name, status}` 数组,存储在消息对象的 thinkProcess 和 steps 字段里。前端用可折叠的 UI 展示,每个步骤显示成功(绿色)或失败(红色)。这个功能是自定义实现的,不是 MateChat 组件库自带的。
### Q4组件化消息渲染是什么意思
> AI 的回答不只是纯文本,还可能是软件列表、预警表格、软件详情等。我根据后端返回的 type 字段判断消息类型,用 5 种不同组件渲染type=1 用 OssList 展示开源软件列表带分页和导出type=2 用 WarningList 展示预警列表带严重程度标签type='critical_software_info' 用 OssDetail 展示软件详情type='df' 用 AIList 展示通用数据表格,其他情况用 Other 组件渲染 Markdown 文本。所有这些组件都嵌在 MateChat 的 McBubble 气泡里。
### Q5为什么选 MateChat 而不是自己写聊天 UI
> 两个原因第一MateChat 提供了开箱即用的聊天气泡、输入框、Markdown 渲染等组件不用从零实现开发效率高第二MateChat 是华为开源的,和我们平台的技术方向一致,组件风格也和 DevUI 统一。不过思考过程展示、组件化消息渲染这些核心功能是自定义实现的MateChat 主要提供了 UI 骨架。
### Q6AI 后端是怎么连接的SSE 还是 WebSocket
> 用的是标准的 HTTP POST 请求,不是 SSE 也不是 WebSocket。每次用户发消息前端发一个 POST 请求,后端处理完返回完整响应。代码里有模拟流式输出的注释代码(用 setTimeout 逐字显示),但生产环境用的是同步请求-响应模式。
---
## 七、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| MateChat 组件库版本 | 1.4.0 |
| 使用的 MateChat 组件 | 5 种McBubble、McInput、McMarkdownCard、McLayout 系列) |
| 自定义消息组件 | 5 种OssList、WarningList、OssDetail、AIList、Other |
| AI API 接口数量 | 9 个 |
| 消息类型 | 6 种text、df、critical_software_info、1、2、sql_error |
| 思考过程步骤 | 3 个主步骤 + N 个子步骤 |
| 会话管理功能 | 7 个(列表/搜索/重命名/收藏/删除/批量管理/历史限制) |
---
## 八、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| AI 助手主页面(生产版) | `src/views/Jyh/AI/Home/index.vue` |
| 软件列表组件 | `src/views/Jyh/AI/Home/components/OssList.vue` |
| 预警列表组件 | `src/views/Jyh/AI/Home/components/WarningList.vue` |
| 软件详情组件 | `src/views/Jyh/AI/Home/components/OssDetail.vue` |
| 数据表格组件 | `src/views/Jyh/AI/Home/components/AIList.vue` |
| Markdown 文本组件 | `src/views/Jyh/AI/Home/components/Other.vue` |
| 猜你想问组件 | `src/views/Jyh/AI/Home/GuessYouWantToAsk.vue` |
| 功能轮播组件 | `src/views/Jyh/AI/Home/SwiperComponent.vue` |
| 会话历史侧边栏 | `src/views/Jyh/AI/Home/components/ViewHistoryAside/ViewHistoryAsideNew.vue` |
| 批量管理弹窗 | `src/views/Jyh/AI/Home/components/ViewHistoryAside/BatchManageModal.vue` |
| AI 布局 | `src/layouts/AILayout/index.vue` |
| AI 头部导航 | `src/components/Header/AIHeader.vue` |
| AI API 接口定义 | `src/api/jyh/index.ts`380-460 行) |
| KnowledgeHub AI 弹窗 | `src/views/Jyh/KnowledgeHub/Components/AIModal.vue` |
| AI 修复建议页面 | `src/views/Jyh/AIRepair/index.vue` |

View File

@@ -0,0 +1,768 @@
# Composition API Hooks 逻辑复用 — 面试版
> 简历原话:**"封装 Composition API Hooks 实现逻辑复用"**
>
> 这篇文档帮你理解项目里到底封装了哪些 Hooks、怎么复用的以及面试时怎么讲。
---
## 一、先搞清楚:什么是 Hooks为什么要封装
### 问题场景
```
没有 Hooks 的时候:
页面A需要响应式布局 → 写一堆 width 判断逻辑
页面B也需要响应式布局 → 再写一遍 width 判断逻辑
页面C也需要响应式布局 → 又写一遍...
页面D需要获取仓库权限 → 调 API、存 ref、写 computed
页面E也需要获取仓库权限 → 同样的代码再写一遍...
```
**痛点**:相同的逻辑散落在多个组件里,改了 A 忘改 B代码臃肿、维护困难。
### 有了 Hooks 之后
```
封装一个 usePageResize() Hook
页面A → const { widthType } = usePageResize()
页面B → const { widthType } = usePageResize()
页面C → const { widthType } = usePageResize()
26 个文件复用同一套逻辑,改 Hook 一处,全局生效
```
**一句话概括**:把可复用的响应式逻辑封装成独立的 `useXxx()` 函数,哪个组件需要就直接调用,避免重复代码。这是 Vue3 Composition API 最核心的设计思想。
### 项目中 Hooks 的定位
```
┌────────────────────────────────────────────────────────┐
│ Vue 组件层 │
│ Security/index.vue Ecosystem/index.vue AI/index.vue │
└────────┬───────────┬───────────┬──────────────────────┘
│ │ │ 调用 useXxx()
▼ ▼ ▼
┌────────────────────────────────────────────────────────┐
│ Hooks 逻辑层30+ 个) │
│ usePageResize useRepoId usePagination useLogin ... │
└────────┬───────────┬───────────┬──────────────────────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────────┐
│ Pinia Store / API / Router │
│ (底层数据源和基础能力) │
└────────────────────────────────────────────────────────┘
```
---
## 二、项目里有 30+ 个 Hooks分六大类
| 分类 | Hooks | 数量 | 核心思想 |
|------|-------|------|----------|
| **通用交互** | usePageResize、useShowMore、useModel、usePagination、useLazyImport | 5+ | 跨组件复用的 UI 交互逻辑 |
| **表单与认证** | useFormInteraction、useLogin、useAccount、useAccModal、useNotice、useLoginCheck | 6+ | 登录/注册/表单校验的完整流程 |
| **数据获取** | useReq/useAsync、useRepoInit、useWikiHome/create/detail/history、useUserDashboard、useRepoList | 9+ | 异步请求 + 状态管理的一站式封装 |
| **路由/参数** | useRepoId、useOrgId、useBranchName、useTitle、useNav、useRepoPathValidate | 6+ | URL 参数解析、导航控制 |
| **权限控制** | useUserAccessLevel、usePagePermission、useIsPrivate | 3 | 权限判断 + computed 封装 |
| **其他辅助** | useFile、usePopup、useReport、useTimeFormat、useStarFollow、useUserInfo、useDiscussGetUserInfo 等 | 7+ | 文件图标、弹窗管理、埋点上报等 |
---
## 三、每个分类怎么实现的?(面试核心)
### 3.1 通用交互 Hooks最常用
#### usePageResize — 响应式断点判断26 个文件复用)
```typescript
import { computed } from 'vue';
import { useWindowSize } from '@vueuse/core';
const { width } = useWindowSize();
export const usePageResize = () => {
const widthConfig = { xxl: 1536, xl: 1280, md: 1024, lg: 768 };
const widthType = computed(() => {
if (width.value > 1536) return 'xxl';
if (width.value > 1280) return 'xl';
if (width.value > 1024) return 'md';
if (width.value > 768) return 'lg';
return 'sm';
});
const isMobile = computed(() => width.value <= 1024);
return { widthType, width, isMobile };
};
```
**怎么用**
```vue
<script setup>
const { widthType, isMobile } = usePageResize();
// 根据不同断点渲染不同布局
</script>
```
**复用在**15 个布局文件 + 4 个态势感知图表组件 + 导航栏、底部栏等,共 26 个文件。
**面试话术**
> "封装了 usePageResize基于 vueuse 的 useWindowSize 响应式监听窗口宽度,定义了 5 个断点xxl/xl/md/lg/sm返回 computed 类型的 widthType 和 isMobile。26 个布局和组件文件都在用,页面布局根据断点自动切换,不需要每个文件自己写一遍判断。"
---
#### useModel — v-model 双向绑定封装18 个文件复用)
```typescript
// 把 v-model 的 prop + emit 封装成统一的写法
export function useModel(props, emits) {
const vModels = computed({
get() { return props.modelValue; },
set(val) { emits('update:modelValue', val); }
});
return { vModels };
}
```
**解决什么**Vue3 自定义组件里使用 v-model 需要写 `props.modelValue` + `emits('update:modelValue')`,每个弹窗/输入组件都写一遍很繁琐。这个 Hook 一行代码搞定。
**怎么用**
```vue
<script setup>
const props = defineProps({ modelValue: Boolean });
const emits = defineEmits(['update:modelValue']);
const { vModels } = useModel(props, emits);
// vModels.value 就是双向绑定的值
</script>
```
**复用在**18 个弹窗/表单组件(登录弹窗、标签选择、分支选择等)。
---
#### usePagination — 分页器封装
```typescript
export const usePagination = ({ storageKey, clientType }) => {
const pager = reactive({
page: 1, pageSize: 10, total: 0,
loading: false, isFirstLoad: true,
showLoading: false, showEmpty: false,
});
// 自动判断显示骨架屏还是 loading
watch(() => pager.loading, (value) => {
if (value) {
if (pager.isFirstLoad || pager.total === 0) {
pager.showLoading = false; // 首次加载用骨架屏
} else {
pager.showLoading = true; // 切换页用 loading
}
} else {
pager.showEmpty = pager.total === 0;
}
});
return { pager, pageOptions };
};
```
**亮点**:自动区分"首次加载"和"翻页加载",首次加载显示骨架屏(结构预览),翻页显示 loading 动画,体验更好。
---
#### useShowMore — 内容溢出检测
```typescript
export const useShowMore = (eleRef) => {
const isOver = ref(false); // 内容是否超出容器
const checkRange = () => {
isOver.value = eleRef.value.scrollHeight > eleRef.value.clientHeight;
};
useResizeObserver(eleRef, checkRange); // 利用 ResizeObserver 自动检测
return { isOver, showMore, onShowMore };
};
```
**解决什么**:很多卡片展示简介时只能显示 3 行,超出部分要显示"查看更多"。这个 Hook 自动检测内容是否溢出。
---
#### useLazyImport — 异步组件懒加载封装
```typescript
export function useLazyImport(loader, options = {}) {
return defineAsyncComponent({
loader,
loadingComponent: showLoading ? LoadingComponents : '',
timeout: 2000, // 2s 超时
delay: 100, // 100ms 后才显示 loading避免闪烁
onError: (_, retry, fail) => {
Message.error('加载失败');
fail();
}
});
}
```
**解决什么**:大组件异步加载时,统一处理 loading 状态、超时、错误重试。项目中有至少 6 个组件通过它注册为异步组件。
---
### 3.2 表单与认证 Hooks
#### useFormInteraction — 表单校验完整流程最复杂332 行)
这是项目中最大的 Hook封装了整个表单交互逻辑
```typescript
export function useFormInteraction(currentForm, flag, extraStatus) {
// 状态管理
const formErrors = reactive({}); // 表单校验错误
const disabled = ref(true); // 提交按钮禁用
const loading = ref(false); // 提交 loading
const FormRef = shallowRef(null); // 表单组件引用
const status = ref(flag); // 协议勾选状态
// 表单字段 change实时校验单个字段
const handleFormChange = ({ key, errors }) => { ... };
// 表单 input判断是否禁用提交
const handleFormInput = (val) => { disabled.value = val; };
// 表单 submit校验全部 → 通过则回调
const handleSubmit = async (callback) => {
const formData = await FormRef.value.ValidateForm();
if (formData.type === 'success') {
await callback(formData.forms); // 执行提交逻辑
} else {
catchFormErrors(formData); // 展示错误
}
};
// 倒计时:发送验证码后 59 秒倒计时
const handleCountDown = async (conf, callback) => { ... };
// 第三方登录OAuth 跳转
const handleAuthLogin = (type) => { ... };
return {
FormRef, formErrors, disabled, loading,
handleFormChange, handleFormInput, handleSubmit,
handleCountDown, handleAuthLogin, ...
};
}
```
**封装了什么**:表单校验、清错、提交、协议勾选、倒计时、第三方 OAuth 登录——所有登录/注册页的通用逻辑,全部封装在一个 Hook 里。
**怎么用**
```vue
<script setup>
const { FormRef, disabled, loading, handleSubmit, handleFormChange } =
useFormInteraction(formList);
</script>
<template>
<Form ref="FormRef" @change="handleFormChange">
<!-- 表单内容 -->
</Form>
<Button :disabled="disabled" :loading="loading" @click="handleSubmit(onLogin)">
登录
</Button>
</template>
```
---
#### useLogin / useAccModal / useNotice — 弹窗类 Hooks
这三个 Hook 都基于 `usePopup` 封装,负责不同类型的弹窗:
```typescript
// useLogin — 登录弹窗
export function useLogin() {
const { mount, unMount, isMounted } = usePopup();
const login = (options) => {
if (isMounted()) return; // 防止重复弹出
mount(LoginModal, {
onLogin: (userInfo) => { unMount(); RecordInfo(userInfo); },
onClose: () => { unMount(); }
});
};
return { login };
}
// useAccModal — 快捷登录弹窗(支持 CSDN/Gitee/GitHub
export function useAccModal() {
const { mount, unMount } = usePopup('g-acc-modal');
const openModal = () => {
if (isMounted()) return;
mount(AccModal, { onConfirm: (type) => { /* OAuth 跳转 */ } });
};
return { openModal };
}
// useNotice — 邮箱修改提醒弹窗
export function useNotification() {
const { mount, unMount } = usePopup('global-notification', document.body);
const notice = () => {
mount(NoticeModal, { onConfirm: () => { router.push('/setting/email'); } });
};
return { notice };
}
```
**设计模式**:三个弹窗 Hook 基于同一个 `usePopup` 底层能力,各自封装不同的弹窗组件和业务逻辑。调用方只需要一行:
```typescript
const { login } = useLogin();
login({ type: 'login' }); // 弹出登录窗口
```
---
#### useAccount — 用户信息增删改查
```typescript
export function useAccount() {
const userInfo = useAccountStore();
const { accountInfo } = storeToRefs(userInfo);
// 记录用户信息(登录时)
const RecordInfo = (source) => {
localStorage.setItem('op_access_token', source.op_access_token);
localStorage.setItem('opUserInfo', JSON.stringify(userInfo));
saveStatus(true);
saveAccountInfo(userInfo);
};
// 清除用户信息(退出时)
const RemoveInfo = () => {
localStorage.removeItem('op_access_token');
// ... 清除 10+ 个 localStorage key
saveStatus(false);
saveAccountInfo();
};
return { RecordInfo, RemoveInfo, accountInfo };
}
```
**封装的逻辑**Token 存 localStorage + Store 同步 + 登录状态更新。一次调用 RecordInfo 完成三件事13 个文件复用。
---
### 3.3 数据获取 Hooks
#### useReq/useAsync — 异步请求封装(通用能力)
```typescript
export const useAsync = (fn, params, precondition, map) => {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
watchEffect(() => {
if (!precondition()) return; // 前提条件不满足,不发请求
loading.value = true;
fn(params).then(res => data.value = map(res))
.catch(e => error.value = e)
.finally(() => loading.value = false);
});
return { data, error, loading, mutate }; // mutate 手动触发重新请求
};
```
**解决什么**:每个页面都要写 `data/loading/error` 三件套 + try/catch。这个 Hook 一行调用搞定,自动处理 loading 状态和错误捕获。
**面试话术**
> "封装了 useAsync 和 useReq 两个通用请求 Hook。useAsync 接收请求函数、参数、前置条件和结果映射函数,自动管理 data/loading/error 三种状态。前置条件不满足时不会发请求(比如用户未登录时不发),通过 watchEffect 自动追踪依赖变化。还提供了 mutate 方法手动触发重新请求。"
---
#### useRepoInit — 仓库首页全部数据一站式初始化243 行)
```typescript
export const useRepoInit = () => {
const { repoId } = useRepoId();
const repoInfo = reactive({...}); // 仓库信息
const loadingStatus = reactive({ // 5 个模块的加载状态
profileLoading: true,
readmeLoading: true,
eventsLoading: true,
contributorLoading: true,
releasesLoading: true,
});
const initRepoHeader = async () => { // 顶部初始化(快)
initRepoData(); await initNotice();
};
const initRepoDashboard = () => { // 首页初始化(完整)
initRepoData(); initEvents(); initReadme();
initContributors(); initRelease();
};
return {
repoInfo, loadingStatus, readmeText, timeData,
contributorList, releasesList,
initRepoHeader, initRepoDashboard, ...
};
};
```
**为什么这样设计**仓库首页需要加载仓库信息、README、动态、贡献者、Release 等 5+ 个模块的数据。每个模块都有独立的 loading 状态。Hook 封装了它们的初始化时机header 先加载dashboard 完整加载),页面只管调用 `initRepoHeader()``initRepoDashboard()`
---
#### useWikiHome / useWikiCreate / useWikiDetail / useWikiHistory — Wiki 全流程 Hooks
四个 Hook 覆盖 Wiki 的完整生命周期(首页 → 创建 → 详情 → 历史版本),每个 Hook 封装对应的数据获取 + 状态管理:
```
useWikiHome() → 首页:获取 Wiki 列表 + Home.md 内容
useWikiCreate() → 创建/编辑页:表单数据 + 提交 + 删除
useWikiDetail() → 详情页:内容 + 侧边栏 + 页脚
useWikiHistory() → 历史版本:版本列表 + 分页
```
**设计思想**:每个页面一个 Hook页面组件只负责渲染数据逻辑全部在 Hook 里。
---
#### useUserDashboard — 用户首页一站式数据初始化272 行)
```typescript
export const useUserDashboard = () => {
// 6 个模块的数据
const userRepoList = ref([]); // 我的仓库
const userEventsList = ref({}); // 最近动态
const recommandRepoList = ref([]); // 推荐仓库
const recommandOrgList = ref([]); // 推荐组织
const userActivityList = ref([]); // 关注动态
const operationData = ref({}); // 运营信息
// 6 个模块的加载状态
const loadingStatus = reactive({
boardLoading: true, myRepoLoading: true,
eventLoading: true, repoRecmLoading: true,
orgRecmLoading: true, activityLoading: true,
});
const pageInit = async () => { // 一键初始化
loadingStatus.boardLoading = true;
await getUserInfo();
await getUserRepos();
loadingStatus.boardLoading = false;
getRecentEvents(); // 不阻塞首屏
getOperationData(); // 不阻塞首屏
recommandRepos();
recommandOrgs();
getUserActivity();
};
return { userInfo, userRepoList, loadingStatus, pageInit, ... };
};
```
**面试话术**
> "比如 useUserDashboard 这个 Hook封装了用户首页 6 个模块的数据获取——仓库列表、最近动态、推荐仓库、推荐组织、关注动态、运营信息,每个模块有独立的 loading 状态。pageInit 方法分层加载:先加载用户信息和仓库列表(阻塞首屏),然后并行加载其他 5 个模块(不阻塞首屏)。页面上只需要 `const { pageInit } = useUserDashboard(); onMounted(pageInit);` 两行代码。"
---
#### useRepoList — 用户仓库列表(含 Star 操作)
```typescript
export const useRepoList = (params) => {
const repoList = ref([]);
const allRepoList = ref([]);
const createdRepoList = ref([]);
const starredRepoList = ref([]);
const loading = ref(false);
// 四类仓库列表 + Star/Unstar 操作
const toggleRepoStar = ({ id, isStar }) => {
if (!userInfo.username) {
emitEvent('login', { triggerType: 'Star' }); // 未登录 → 弹出登录框
return;
}
isStar ? unstarRepo({ repoId: id }) : starRepo({ repoId: id });
init(); // 操作后重新拉取列表
};
return { allRepoList, createdRepoList, starredRepoList, toggleRepoStar, init };
};
```
**封装的逻辑**:四种仓库列表 + Star/Unstar 操作 + 未登录时自动弹出登录框。8 个文件复用。
---
### 3.4 路由/参数 Hooks
#### useRepoId — 从 URL 提取仓库 ID30 个文件复用,使用最多)
```typescript
export const useRepoId = (connector = '%2F', namespace = '') => {
const route = useRoute();
const repoId = computed(() => {
const [, ns, repo, ...rest] = route.path.split('/');
if (namespace) {
return [ns, repo, ...rest].join(connector); // 拼接完整路径
}
return [ns, repo].join(connector); // namespace/repo
});
return { repoId };
};
```
**解决什么**:项目中大量页面需要从 URL 中提取仓库的 namespace/repoName`gitcode/MyProject`。30 个文件都在用这个 Hook不需要每个文件自己解析 URL。
**类似的还有**`useOrgId`(提取组织 ID`useBranchName`(提取分支名+编码)。
---
#### useTitle — 页面标题设置
```typescript
export const usePageTitle = (title, name) => {
if (title && name) {
pageTitle.value = `${title}`;
} else {
pageTitle.value = title || (name ? `${name} - GitCode` : 'Tcode');
}
};
```
路由守卫中每个页面切换时调用,自动更新浏览器标签页标题。
---
### 3.5 权限控制 Hooks
#### useUserAccessLevel — 权限查询 + 角色判断
```typescript
export function useUserAccessLevel({ type = 'org', targetId = '' } = {}) {
const access_level = ref(0);
async function getPermission(id) {
if (type === 'org') {
const res = await getOrgPermission({ group_id: id });
access_level.value = res.data.data.my_role?.access_level || 0;
} else {
const res = await getRepoPermission({ repo_id: id });
access_level.value = res.data.data.access_level || 0;
}
}
// 三种角色自动 computed
const isAdmin = computed(() => access_level.value >= admin); // 50
const isDeveloper = computed(() => access_level.value >= developer); // 30
const isVisitor = computed(() => access_level.value >= visitor); // 10
return { access_level, getPermission, isAdmin, isDeveloper, isVisitor };
}
```
**解决什么**:查询用户在当前组织/仓库的权限后,自动算出三个布尔角色值。配合 Store 里的权限 computed页面只需 `if (isAdmin)` 判断是否显示管理按钮。
#### usePagePermission — 页面级权限控制
```typescript
export function usePagePermission(access_level, isPrivate) {
// 根据权限等级和是否私有,返回哪些菜单/操作可用
}
```
---
### 3.6 其他辅助 Hooks
#### useFile — 文件类型识别(图标 + 语言高亮)
```typescript
export function useFile() {
// 根据文件名后缀返回对应图标名
const getFileIcon = (filename) => {
const format = filename?.split('.').pop();
if (/^(zip|rar|7z)$/i.test(format)) return 'gt-file-zip-c';
if (/^(png|jpg|jpeg|gif)$/i.test(format)) return 'gt-picture-c';
if (/^(js|ts|vue|py)$/i.test(format)) return 'gt-file-code-c';
return 'gt-file-c';
};
// 根据文件名返回代码高亮语言
const getFileLanguage = (filename) => {
const map = { js: 'javascript', ts: 'typescript', vue: 'html', py: 'python' };
return map[format] || format;
};
return { getIcon, getFileLanguage, getFileFormat };
}
```
#### useStarFollow — 关注/粉丝操作
```typescript
export const useStarFollow = (params, auto) => {
const toggleStar = (username, followedUsername, follow) => {
if (!username) {
emitEvent('login', { triggerType: '关注用户' }); // 未登录→弹出登录
return;
}
follow ? followUser(...) : unfollowUser(...);
microApp.setData('user-center', { type: 'user_hasFollowed_update', ... });
};
return { fanCount, followCount, hasFollowed, toggleStar };
};
```
**封装了什么**:关注/取消关注的 API 调用 + 状态更新 + 微前端数据同步 + 未登录自动弹登录框。
---
#### useReport — 埋点上报
```typescript
export const useReport = (eventID, eventParams, headers) => {
// 统一埋点上报逻辑
};
```
#### usePopup — 弹窗管理器(底层能力)
被 useLogin、useAccModal、useNotice 三个 Hook 依赖的底层弹窗管理:
```typescript
export function usePopup(className?, rootElement?) {
const mount = (component, props) => { /* 挂载弹窗到 DOM */ };
const unMount = () => { /* 卸载弹窗 */ };
const isMounted = () => { /* 是否已挂载 */ };
return { mount, unMount, isMounted };
}
```
---
## 四、Hooks 之间的层次关系
```
底层能力(被其他 Hook 依赖)
├── usePopup → 弹窗挂载/卸载
├── useReq/useAsync → 通用异步请求封装
├── useRepoId → URL 参数解析
└── useModel → v-model 封装
中间能力(组合底层能力)
├── useLogin → 基于 usePopup
├── useAccModal → 基于 usePopup
├── useNotice → 基于 usePopup
├── useFormInteraction → 基于 useAccount + useRouter
├── useUserAccessLevel → 基于 useUserInfo
业务能力(组合中间能力)
├── useRepoInit → 基于 useRepoId
├── useWikiHome → 基于 useRepoId + useRouter
├── useRepoList → 基于 API 调用
├── useUserDashboard → 基于 6 个 API 调用
├── useStarFollow → 基于 API + eventBus + microApp
└── usePageResize → 基于 useWindowSizevueuse
```
**面试话术**
> "我设计的 Hooks 是有层次的。底层 Hooks 提供通用能力(如 usePopup 管理弹窗生命周期、useAsync 封装异步请求);中间层 Hooks 基于底层组合出业务场景(如 useLogin 基于 usePopup 封装登录弹窗流程);最上层是业务 Hooks组件里一行代码调用就能获得完整的数据和操作方法。"
---
## 五、整体架构总结
```
30+ 个 Hooks 在项目中的定位:
可复用逻辑提取为 Hooks
┌───────────────┐
│ 通用交互 (5+) │ usePageResize(26文件) useModel(18文件) usePagination useShowMore useLazyImport
├───────────────┤
│ 表单认证 (6+) │ useFormInteraction(8文件) useLogin useAccount(13文件) useAccModal useNotice
├───────────────┤
│ 数据获取 (9+) │ useAsync/useReq useRepoInit(7文件) useWikiHome/create/detail/history
│ │ useUserDashboard useRepoList(8文件) useDiscussGetUserInfo
├───────────────┤
│ 路由参数 (6+) │ useRepoId(30文件) useOrgId useBranchName useTitle useNav useRepoPathValidate
├───────────────┤
│ 权限控制 (3) │ useUserAccessLevel usePagePermission useIsPrivate
├───────────────┤
│ 其他辅助 (7+) │ useFile usePopup useReport useTimeFormat useStarFollow(4文件) useUserInfo
└───────────────┘
Vue 组件层40+ 组件调用)
```
---
## 六、面试问答准备
### Q1"封装 Composition API Hooks 实现逻辑复用"具体做了什么?
> 项目里封装了 30 多个 Hooks分为六大类。最核心的几个usePageResize 把响应式断点判断封装成 Hook26 个布局文件复用useRepoId 从 URL 提取仓库 ID30 个文件复用useFormInteraction 封装了完整的表单校验流程332 行8 个登录/注册页共享同一套校验逻辑useUserDashboard 封装了用户首页 6 个模块的数据初始化页面只需两行代码完成全部数据加载。Hooks 之间还有层次关系——底层 HooksusePopup、useAsync提供通用能力中间层组合底层能力业务层直接给组件用。
### Q2Hooks 和之前写的工具函数有什么区别?
> 工具函数是无状态的纯函数输入→输出Hooks 是有状态的(包含 ref、computed、watch。比如 useFormInteraction 内部管理了 formErrors、disabled、loading 等 6 个状态,还监听了表单 change 事件自动校验。如果写成工具函数这些状态管理就要在每个页面里重复写。Hooks 把"响应式状态 + 操作逻辑"封装在一起,是 Vue3 Composition API 的核心优势。
### Q3Hooks 怎么保证类型安全?
> 所有 Hooks 都用 TypeScript 写,返回值和参数都有类型定义。比如 usePagination 接收 `{storageKey, clientType}` 接口类型useRepoList 接收包含 profile/all/created/starred 四种子参数的类型。组件调用时 VSCode 有完整的智能提示。
### Q4有没有用过 VueUse 的 Hooks
> 用过useWindowSize、useResizeObserver、useTitle 等都是从 @vueuse/core 引入的。但项目不只是用第三方 Hooks更关键的是基于 VueUse 做二次封装——比如 usePageResize 基于 useWindowSize 封装了断点判断逻辑useShowMore 基于 useResizeObserver 封装了内容溢出检测。既用了社区的最佳实践,又做了业务定制。
### Q530 多个 Hooks 怎么组织的?会不会管理混乱?
> 按职责分目录:通用的放在 `src/utils/hooks/`30 个),特定业务域放在对应的 views 目录下(如 `src/views/User/hooks/` 放用户相关的 7 个 Hook布局相关的放在 `src/layouts/hooks/`。调用时能直观看出 Hook 的作用域。
### Q6Hooks 和 Pinia Store 怎么分工?
> Hooks 和 Store 的分工很明确。Store 存跨组件共享的持久状态用户信息、仓库信息Hooks 封装的是"行为逻辑"——怎么获取数据、怎么校验表单、怎么处理交互。Hooks 经常调用 Store 来读写状态,但它们不替代 Store。比如 useAccount Hook 既调用 Store 的 saveAccountInfo又操作 localStorage把"用户信息存储到 Store + 持久化到 localStorage + 更新登录状态"三步封装成一步 RecordInfo。
---
## 七、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| Hooks 总数 | 30+ 个 |
| 使用最多的 Hook | useRepoId30 个文件、61 处引用) |
| 第二常用 | usePageResize26 个文件、51 处引用) |
| 第三常用 | useModel18 个文件、35 处引用) |
| 最大的 Hook | useFormInteraction332 行) |
| 业务最全的 Hook | useUserDashboard6 个模块数据初始化) |
| 弹窗类 Hooks | 3 个useLogin/useAccModal/useNotice共用 usePopup |
| Wiki 全家桶 | 4 个Home/Create/Detail/History |
| 权限类 Hooks | 2 个useUserAccessLevel/usePagePermission |
| 基于 @vueuse/core 封装的 | 3 个useWindowSize/useResizeObserver/useTitle |
---
## 八、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 通用交互 Hooks5 个) | `src/utils/hooks/usePageResize.ts``useShowMore.ts``useModel.ts``usePagination.ts``useLazy.ts` |
| 表单与认证 Hooks6 个) | `src/utils/hooks/useForm.ts``useLogin.ts``useAccount.ts``useAccModal.ts``useNotice.ts``useLoginCheck.ts` |
| 数据获取 Hooks9 个) | `src/utils/hooks/useReq.ts``useRepoInit.ts``useWikiInit.ts``useIssueTemplate.ts``src/api/discussion/hook.ts` |
| 用户域 Hooks7 个) | `src/views/User/hooks/useUserDashboard.ts``useRepoList.ts``useStarFollow.ts``useContributes.ts``useTimelineActivities.ts``useIsPrivate.ts``useUserLang.ts` |
| 路由/参数 Hooks6 个) | `src/utils/hooks/useRepoId.ts``useOrgId.ts``usebranchName.ts``useTitle.ts``useNav.ts``useRepoPathValidate.ts` |
| 权限控制 Hooks3 个) | `src/utils/hooks/useUserAccessLevel.ts``src/layouts/hooks/usePagePermission.ts``src/views/User/hooks/useIsPrivate.ts` |
| 其他辅助 Hooks7 个) | `src/utils/hooks/useFile.ts``usePopup.ts``useReport.ts``useTimeFormat.ts``useUserInfo.ts``useRepoHeaderInit.ts``useBranchOptions.ts` |
| 组织域 Hooks | `src/views/Org/hooks/orgInfo.ts` |
| 弹窗底层能力 | `src/utils/hooks/usePopup.ts`(被 useLogin/useAccModal/useNotice 依赖) |

View File

@@ -0,0 +1,278 @@
# ECharts 多维度数据分析与展示 — 面试版
> 简历原话:**"集成 ECharts 实现多维度数据分析和展示"**
>
> 这篇文档帮你理解这句话背后到底做了什么,以及面试时怎么讲。
---
## 一、先搞清楚:这句话到底是什么意思?
拆成三部分理解:
| 关键词 | 含义 | 项目中对应 |
|--------|------|------------|
| **集成 ECharts** | 把 ECharts 图表库引入项目,让它能画图 | 安装 echarts 6.x在 Vue3 组件中调用 `echarts.init()` 创建图表 |
| **多维度** | 不是只画一种图、看一组数据,而是从多个角度分析数据 | 3 大业务模块、10+ 种图表类型、覆盖时间/空间/语言/行业等多个角度 |
| **数据分析和展示** | 不是简单的数据罗列,而是通过图表帮助用户发现规律、做出判断 | 四象限风险定位、趋势预测、热力分布等,让数据"说话" |
**一句话概括**:我在平台的 3 个核心模块中,用 ECharts 画了 20 多个图表,从安全漏洞、开源生态、社区发展等多个角度把后端数据可视化出来。
---
## 二、画了哪些图?按模块讲
### 模块 1安全态势最核心面试重点
这个模块帮用户**看懂开源项目的安全风险**
```
安全态势 = 哪里有危险 × 什么漏洞 × 哪些组件有风险
```
| 看什么 | 用什么图 | 怎么理解 |
|--------|----------|----------|
| 全球哪里有攻击 | 世界地图 + 涟漪动画 | 地图上每个国家颜色深浅表示威胁程度,关键位置有闪烁的光点 |
| 中国各省安全情况 | 中国地图热力图 | 像天气预报的温度图,颜色越深=威胁越大 |
| CVE 漏洞数量变化 | 柱状图 + 折线图 | 柱子=每月漏洞数量,折线=增长还是下降,还能切换"全球/国内"视角 |
| 漏洞严重程度 | 堆叠柱状图 | 每根柱子分红(高危)黄(中危)绿(低危)三段,一眼看出哪个季度最严重 |
| 哪种编程语言漏洞多 | 四象限气泡图 | X轴=活跃度Y轴=漏洞密度,气泡大小=漏洞总数,颜色=风险等级 |
| 哪些开源组件最危险 | 水平柱状图 | Log4j2、Spring 等组件的危险程度排名 |
### 模块 2开源生态
帮用户了解**开源世界的整体发展状况**
| 看什么 | 用什么图 | 怎么理解 |
|--------|----------|----------|
| 各省有多少开源人才 | 中国地图散点 | 地图上标出各省人才密度,旁边显示 Top5 排名 |
| 开发者数量变化 | 面积折线图 | 像山丘一样的增长曲线,能看出增长趋势 |
| 基础设施覆盖情况 | 多层环形图 | 三层圆环,分别代表镜像站、代码托管、构建平台的覆盖率 |
| GitHub vs GitCode 对比 | 分组柱状图 | 6 个指标PR合并率、响应速度等两边对比 |
| 编程语言谁强谁弱 | 南丁格尔玫瑰图 | 扇形面积越大=市场份额越大 |
### 模块 3社区分析
帮用户了解**开源社区在各城市的发展**
| 看什么 | 用什么图 |
|--------|----------|
| 各城市项目/人才/社区数量 | 柱状图(多个) |
| 高校俱乐部增长趋势 | 折线图 |
| 各省政策发布数量 | 柱状图 |
| 鸿蒙生态分布 | 多图表组合 |
---
## 三、怎么实现的?(面试核心)
### 3.1 一个图表从无到有的完整流程
用最简单的语言描述:
```
第1步后端给数据API请求
第2步拿到数据存到 Vue 的 ref 里(响应式状态)
第3步等页面渲染好nextTick用 echarts.init() 创建图表
第4步把数据塞进 ECharts 的配置项setOption
第5步页面关闭时销毁图表dispose防止内存泄漏
```
用代码简单表示:
```typescript
// 1. 请求数据
const response = await cveDataGlobalPage1();
// 2. 存到 ref
cveData.value = response.data;
// 3. 等 DOM 好了再画图
nextTick(() => {
const chart = echarts.init(document.getElementById('chart'));
// 4. 配置图表
chart.setOption({
xAxis: { data: ['1月', '2月', '3月'...] },
series: [{ type: 'bar', data: cveData.value }]
});
});
// 5. 组件销毁时清理
onUnmounted(() => chart.dispose());
```
### 3.2 面试必讲的 5 个技术点
#### 技术点 1图表生命周期管理防内存泄漏
**问题**ECharts 每次 `init()` 都会创建一个实例,如果不销毁,页面会越来越卡。
**解决方案**:每次画新图之前,先把旧的 `dispose()` 掉。
```typescript
// 存储图表实例
const chartInstances = ref({});
// 画图前:先销毁旧的,再创建新的
const initChart = () => {
if (chartInstances.value.myChart) {
chartInstances.value.myChart.dispose(); // 销毁旧的
}
const chart = echarts.init(el); // 创建新的
chartInstances.value.myChart = chart;
};
// 组件卸载时也要销毁
onUnmounted(() => {
chartInstances.value.myChart?.dispose();
});
```
**面试话术**
> "我在每个图表组件中都用了 ref 存储 ECharts 实例,每次重绘前先 dispose 旧实例再 init 新实例,组件卸载时也会销毁,避免内存泄漏。"
---
#### 技术点 2地图三级容错加载
**问题**ECharts 画地图需要先注册 GeoJSON 数据。如果数据加载失败,地图就是白的。
**解决方案**:准备三套数据源,依次尝试:
```
本地文件(最快) → CDN远程加载备用 → 手写简化版(兜底)
```
**面试话术**
> "地图数据加载我做了三级容错:先加载本地预编译的地图文件,失败了从阿里 DataV CDN 拉取,再失败用手写的简化 GeoJSON 兜底,保证地图永远不会白屏。"
---
#### 技术点 3一个图表达 4 个维度(气泡散点图)
**问题**:普通的柱状图只能表达 2 个维度X 轴 + Y 轴),怎么在一个图里塞更多信息?
**解决方案**用气泡散点图4 种方式编码 4 个维度:
| 编码方式 | 代表的维度 | 例子 |
|----------|-----------|------|
| X 轴位置 | 代码活跃度 | JavaScript 在右边活跃Rust 在左边 |
| Y 轴位置 | 漏洞密度 | Java 在上面漏洞多Go 在下面 |
| 气泡大小 | 漏洞总数 | JavaScript 气泡最大 |
| 气泡颜色 | 风险等级 | 红色=高风险,蓝色=健康 |
**面试话术**
> "编程语言安全态势图我用了四象限气泡散点图X 轴是代码活跃度Y 轴是漏洞密度,气泡大小代表漏洞总数,颜色代表风险象限。一个图表同时展示了 4 个数据维度,用户可以直观看出哪些语言'又活跃又危险'。"
---
#### 技术点 4兜底数据策略
**问题**:如果后端挂了,页面上 20 多个图表全部空白,看起来像 bug。
**解决方案**:每个图表都准备一套硬编码的"假数据"API 失败时用假数据渲染:
```typescript
try {
const data = await api.getData();
chartData.value = data;
} catch (error) {
// API 失败,用默认数据
chartData.value = [
{ name: 'JavaScript', value: 95 },
{ name: 'Python', value: 90 },
// ... 预设的示例数据
];
}
```
**面试话术**
> "数据展示型产品最怕图表空白所以我给每个图表都加了兜底数据。API 正常就用真实数据,失败了用硬编码的示例数据渲染,保证页面始终有内容。"
---
#### 技术点 5响应式自适应
**问题**:浏览器窗口大小变了,图表不会自动调整。
**解决方案**:监听窗口变化,触发图表重绘:
```typescript
// 监听窗口 resize
window.addEventListener('resize', () => {
chart.resize(); // ECharts 自带的调整大小方法
});
// 更进一步:宽度类型变了(比如从桌面切到平板),重新画图
// 因为小屏可能需要调整字号、隐藏部分标签等
watch(widthType, () => {
initChart(); // 重新初始化,而不是简单的 resize
});
```
**面试话术**
> "我用了自定义的 usePageResize Hook 检测视口变化。简单变化用 ECharts 自带的 resize(),但宽度类型变化时(比如桌面切到移动端)会重新初始化图表,调整 grid、字号、标签等配置。"
---
## 四、整体架构一句话总结
```
后端 API → Axios 请求 → Vue ref 存数据 → ECharts 渲染图表
↓ (失败)
兜底默认数据 → 也能渲染
```
技术栈:**Vue3** 的 Composition API 管理状态和生命周期 + **ECharts 6.x** 负责所有图表渲染 + **自定义工具类**管理地图数据加载。
---
## 五、面试问答准备
### Q1"集成 ECharts 实现多维度数据分析和展示"具体做了什么?
> 我在态势感知平台的安全态势、开源生态、社区分析三大模块中,用 ECharts 实现了 20 多个图表组件。涵盖了 10 种以上的图表类型——柱状图、折线图、饼图、散点气泡图、世界地图、中国地图、堆叠图、环形图、玫瑰图等。从时间、空间、编程语言、行业、组件等多个维度对开源安全和生态数据进行可视化分析。
### Q2遇到的技术难点是什么
> 主要有三个:
> 1. **地图加载**ECharts 画地图需要 GeoJSON 数据加载可能失败。我做了三级容错本地→CDN→兜底保证地图不白屏。
> 2. **性能**20 多个图表如果同时初始化会卡。我用 nextTick 确保 DOM 就绪再画图,组件销毁时及时 dispose 释放内存。
> 3. **数据可靠性**:后端 API 可能失败。我给每个图表加了兜底默认数据,保证页面始终有内容。
### Q3多维度体现在哪里
> 以编程语言安全态势的气泡图为例,一个图表编码了 4 个维度X 轴是代码活跃度Y 轴是漏洞密度,气泡大小是漏洞总数,颜色代表风险等级。从整体项目来看,我们从时间维度(月度/季度趋势)、空间维度(世界地图/中国地图)、语言维度、行业维度、组件维度等多个角度分析数据。
### Q4ECharts 实例的生命周期怎么管理?
> 每个图表组件用 ref 存储 ECharts 实例引用。每次重绘前先 dispose 旧实例再 init 新实例,避免内存泄漏。组件 onUnmounted 时也会销毁。窗口 resize 时调用 ECharts 的 resize() 方法自适应。
### Q5为什么不直接用 vue-echarts 这种封装库?
> 项目中简单图表用了 vue-devui 的 DChart 组件。但复杂图表(比如地图、气泡散点图、混合图表)需要深度自定义配置,直接用 echarts.init() 更灵活,能精确控制每个细节。
---
## 六、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 图表组件数量 | 20+ 个 |
| 图表类型 | 10+ 种 |
| 业务模块 | 3 个(安全/生态/社区) |
| 可视化数据维度 | 时间、空间、语言、行业、组件、人才等 6+ 个 |
| 地图容错级别 | 3 级本地→CDN→兜底 |
| ECharts 版本 | 6.x |
---
## 七、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 地图加载工具 | `src/utils/mapLoader.js` |
| API 接口定义 | `src/api/jyh/situation.ts` |
| 安全模块(最复杂) | `src/views/Jyh/security/` 目录下所有组件 |
| 生态模块 | `src/views/Jyh/ecosystem/` 目录下所有组件 |
| 社区模块 | `src/views/Jyh/Community/` 目录下所有组件 |

View File

@@ -0,0 +1,345 @@
# Pinia 状态管理 — 面试版
> 简历原话:**"使用 Pinia 进行状态管理"**
>
> 这篇文档帮你理解项目中 Pinia 到底管了哪些状态、怎么管的,以及面试时怎么讲。
---
## 一、先搞清楚Pinia 在项目里干什么?
### 什么是状态管理?
```
没有状态管理的问题:
用户登录信息 → 在 A 组件里获取
用户登录信息 → B 组件也要用 → 怎么传?父子组件可以 props兄弟组件呢
用户登录信息 → C 组件也要用 → 再 props 一层?代码变成意大利面...
有了 Pinia
用户登录信息 → 存在 Pinia store 里
A、B、C 组件 → 各自直接从 store 读取,不需要互相传
```
**一句话概括**Pinia 就是"全局数据仓库",存放多个组件都需要共享的数据(登录状态、当前仓库信息、当前组织信息等),避免组件之间层层传递。
### 项目中 Pinia 的定位
```
数据从哪来 → API 请求 / localStorage
存到哪 → Pinia store全局仓库
谁来用 → 32~62 个组件直接从 store 读取
```
---
## 二、项目里有几个 Store各自管什么
项目有 **10 个 Store**,其中 **6 个是真正活跃使用的**
### 核心 Store用得最多
| Store 名 | 管什么 | 被多少文件使用 |
|----------|--------|---------------|
| `useAccountStore` | 当前登录用户的信息ID、头像、昵称、Token | **62 个文件** |
| `orgInfoStore` | 当前组织的信息(名称、头像、权限、成员) | **37 个文件** |
| `repoInfoStore` | 当前仓库的信息名称、Star数、权限、是否私有 | **35 个文件** |
| `useGlobalInfoStore` | 全局 UI 状态(菜单、主题、搜索、语言列表) | **32 个文件** |
### 辅助 Store用得较少
| Store 名 | 管什么 | 被多少文件使用 |
|----------|--------|---------------|
| `otherAccountStore` | 当前正在查看的其他用户的信息 | 10 个文件 |
| `useMrChangeStore` | 代码合并MR的 diff 显示设置 | 6 个文件 |
| `entranceData` | 首页的仓库/组织列表 | 2 个文件 |
### 每个 Store 管理的状态一目了然
```
useAccountStore用户信息
├── isLogin: 是否已登录
└── accountInfo: { id, nickname, avatar, email, token... }
orgInfoStore组织信息
├── orgInfo: { name, avatar, description, visibility... }
├── isFollow: 是否关注了这个组织
├── memList / memCount: 成员列表和数量
└── computed → isAdmin / isDeveloper / isVisitor权限判断
repoInfoStore仓库信息
├── repoInfo: { name, star_count, visibility, archived... }
├── access_level: 权限等级数字
└── computed → isPrivate / isArchived / isAdmin / isDeveloper权限判断
useGlobalInfoStore全局 UI
├── globalMenuInfo: 菜单配置
├── menuType: 当前菜单类型repo/org
├── globalTheme: 主题light/dark
├── headerSearch: 搜索状态
└── languageList: 编程语言列表
```
---
## 三、怎么实现的?(面试核心)
### 3.1 Store 怎么定义的?
项目用的是 Pinia 的 **Setup 语法**(函数式),和 Vue3 Composition API 风格一致:
```typescript
// stores/user.ts — 最核心的用户 Store
import { defineStore } from 'pinia';
import { reactive, ref } from 'vue';
export const useAccountStore = defineStore('accountInfo', () => {
// ========== 状态state==========
const isLogin = ref(Boolean(localStorage.getItem('op_access_token')));
const accountInfo = reactive({
nickname: '',
avatar: '',
email: '',
op_access_token: '',
// ...
});
// ========== 操作actions==========
const saveAccountInfo = (info) => {
if (info) {
Object.assign(accountInfo, info); // 合并用户信息
} else {
Object.keys(accountInfo).forEach(k => accountInfo[k] = ''); // 清空(退出登录)
}
};
const checkIsLogin = async (token, refreshToken) => {
localStorage.setItem('op_access_token', token);
const userRes = await getUserInfo(); // 调 API 验证
if (userRes.data?.code === 200) {
saveAccountInfo(userRes.data.data); // 存到 store
return true;
}
return false;
};
// ========== 返回(暴露给组件用)==========
return { isLogin, accountInfo, saveAccountInfo, checkIsLogin };
});
```
**为什么用 Setup 语法而不是 Options 语法?**
- 和 Vue3 Composition API 风格统一,团队学习成本低
- 可以直接用 `ref``computed``async/await`,更灵活
- TypeScript 类型推导更好
### 3.2 Store 怎么注册的?
```typescript
// main.ts — 一行代码搞定
import { createPinia } from 'pinia';
app.use(createPinia());
```
就这么简单。不需要像 Vuex 那样手动注册每个 module。
### 3.3 组件怎么用 Store
```vue
<script setup>
import { useAccountStore } from '@/stores/user';
import { repoInfoStore } from '@/stores/Repo';
// 获取 store 实例
const accountStore = useAccountStore();
const repoStore = repoInfoStore();
// 直接读状态
console.log(accountStore.isLogin);
console.log(repoStore.repoInfo.name);
// 直接调 action
accountStore.saveAccountInfo({ nickname: '新名字' });
await repoStore.getRepoInfo();
</script>
<template>
<!-- 模板中直接用 -->
<img :src="accountStore.accountInfo.avatar" />
<span v-if="repoStore.isAdmin">管理</span>
</template>
```
**关键点**:不需要 `$store`,不需要 `mapState``mapActions`,直接用就行。
### 3.4 权限判断怎么用 computed 封装?
这是项目中一个很巧妙的设计——把权限判断逻辑封装在 Store 的 computed 里:
```typescript
// stores/Repo/index.ts
const access_level = ref(0);
// 8 个 computed 属性,自动根据 access_level 判断权限
const isAdmin = computed(() => access_level.value >= admin);
const isDeveloper = computed(() => access_level.value >= developer);
const isVisitor = computed(() => access_level.value >= visitor);
const isAdminOperate = computed(() => access_level.value >= admin && !repoInfo.value?.archived);
const isArchived = computed(() => repoInfo.value?.archived);
const isPrivate = computed(() => repoInfo.value?.visibility === 'private');
```
组件中这样用:
```vue
<template>
<!-- 权限按钮只有管理员才能看到 -->
<d-button v-if="repoStore.isAdmin">删除仓库</d-button>
<!-- 归档提示自动根据状态显示 -->
<span v-if="repoStore.isArchived">此仓库已归档</span>
<!-- 私有标识 -->
<d-tag v-if="repoStore.isPrivate">私有</d-tag>
</template>
```
**好处**:权限判断逻辑集中在 Store 里35 个组件共享同一套规则。改规则只改一处。
### 3.5 持久化怎么做的?
项目**没有**用 `pinia-plugin-persistedstate` 插件,而是手动操作 localStorage
```typescript
// 登录时Token 存 localStorage同时存 Store
localStorage.setItem('op_access_token', token);
saveAccountInfo({ op_access_token: token });
// 页面刷新时:从 localStorage 恢复到 Store
const isLogin = ref(Boolean(localStorage.getItem('op_access_token')));
const accountInfo = reactive({
op_access_token: localStorage.getItem('op_access_token') || '',
// ...
});
// 退出时:清 localStorage清 Store
localStorage.removeItem('op_access_token');
saveAccountInfo({}); // 传空 = 清空所有字段
```
**为什么手动而不用插件?** 因为只有用户 Token 需要持久化,其他状态(仓库信息、组织信息)每次进页面都重新从 API 获取。用插件反而多余。
---
## 四、除了 Pinia还有哪些状态管理方式
项目不是"只用 Pinia",而是**三种方式配合使用**
| 方式 | 用在哪 | 为什么 |
|------|--------|--------|
| **Pinia** | 全局共享状态(用户、仓库、组织、菜单) | 多个页面都需要,跨组件共享 |
| **mitt 事件总线** | 跨组件事件通知(登录/登出、错误、微前端通信) | 一次性事件触发,不需要持久化状态 |
| **provide/inject** | 父子组件深层传递移动端的仓库ID、权限标记 | 只在某个组件子树内共享,不需要全局 |
```typescript
// 事件总线示例 — 登录成功后通知全局
import { emitEvent } from '@/utils/eventBus';
emitEvent('login', userData); // 触发
addEventListener('login', handleLogin); // 监听
// provide/inject 示例 — 移动端深层组件获取仓库ID
const repoId = inject('repoId'); // 孙子组件直接拿,不需要层层 props
```
**面试话术**
> "项目中状态管理不是只有 Pinia而是三种方式配合Pinia 管全局持久状态(用户/仓库/组织mitt 事件总线管跨组件事件通知(登录/登出/错误provide/inject 管组件子树内的深层数据传递。根据数据的使用范围和生命周期选择最合适的方式。"
---
## 五、Store 之间的关系
项目中所有 Store **互相独立**,没有任何 Store 引用其他 Store
```
useAccountStore ──┐
orgInfoStore ─────┤ 互不依赖,各自独立
repoInfoStore ────┤
useGlobalInfoStore┘
```
**为什么这样设计?** 因为每个 Store 对应一个业务领域(用户/仓库/组织/全局),它们的数据来源不同(不同的 API使用场景也不同没有必要互相耦合。
---
## 六、整体架构总结
```
Pinia 在项目中的位置:
API 请求 ──→ Pinia Store ──→ Vue 组件32~62 个文件消费)
├── useAccountStore用户62 个文件用)
├── orgInfoStore组织37 个文件用)
├── repoInfoStore仓库35 个文件用)
├── useGlobalInfoStore全局UI32 个文件用)
└── + 6 个辅助 Store
配合使用:
├── mitt 事件总线30+ 个文件用)→ 事件通知
└── provide/inject → 组件子树数据传递
```
---
## 七、面试问答准备
### Q1"使用 Pinia 进行状态管理"具体做了什么?
> 项目用 Pinia 管理了 10 个全局状态 Store其中核心的 4 个分别管理用户登录信息62 个文件使用、仓库信息35 个文件、组织信息37 个文件)、全局 UI 状态32 个文件。比如用户登录后Token 和用户信息存在 Pinia 里,导航栏、个人主页、仓库页面等几十个组件都能直接读取,不需要层层传递 props。
### Q2为什么选 Pinia 而不是 Vuex
> 三个原因第一Pinia 是 Vue3 官方推荐的状态管理方案Vue3 + Composition API 的项目用 Pinia 最自然第二Pinia 的 API 更简洁,不需要 mutationsactions 里可以直接 async/await第三Pinia 的 TypeScript 支持更好,类型推导自动完成,不需要额外写类型声明。
### Q3Pinia 有两种语法,你用的哪种?
> 用的 Setup 语法(函数式),就是 `defineStore('id', () => { ... })` 这种。因为项目用 Vue3 Composition API 开发Setup 语法和 Composition API 风格统一,可以直接用 ref、computed、async/await。整个项目 10 个 Store 有 9 个用 Setup 语法,只有 1 个占位 Store 用了 Options 语法。
### Q4状态持久化怎么做的
> 没有用 pinia-plugin-persistedstate 插件,因为只有用户 Token 需要持久化,其他状态(仓库、组织信息)每次进页面都重新从 API 获取。所以手动在 localStorage 和 Store 之间同步:登录时 Token 存 localStorage 同时存 Store页面刷新时从 localStorage 恢复到 Store退出时两边都清空。
### Q5Store 里的 computed 有什么用?
> 主要用来封装权限判断逻辑。比如 repoInfoStore 里有 8 个 computed 属性isAdmin、isDeveloper、isVisitor 等,根据 access_level 数字自动判断当前用户的角色。35 个组件用到仓库权限的地方,都直接读这些 computed而不是每个组件自己写判断逻辑。这样权限规则改一处就全局生效。
### Q6除了 Pinia 还用了什么状态管理方式?
> 还用了 mitt 事件总线和 provide/inject。三种方式各有分工Pinia 管需要持久化的全局状态mitt 管一次性事件通知(比如登录/登出事件provide/inject 管组件子树内的深层数据传递。不是所有共享数据都适合放 Pinia事件通知用 mitt 更轻量,局部数据用 provide/inject 更合理。
---
## 八、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| Pinia 版本 | 2.1.3 |
| Store 总数 | 10 个 |
| 活跃使用的 Store | 6 个 |
| 核心 Store | 4 个(用户/仓库/组织/全局UI |
| 使用最多的 Store | useAccountStore62 个文件) |
| 语法风格 | Setup 语法9/10 |
| 持久化方式 | 手动 localStorage无插件 |
| 配合的其他方案 | mitt 事件总线30+ 文件、provide/inject |
---
## 九、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| Pinia 注册 | `src/main.ts`app.use(createPinia()) |
| 用户 Store最核心 | `src/stores/user.ts`3 个 Store 共存) |
| 仓库 Store | `src/stores/Repo/index.ts` |
| 组织 Store | `src/stores/Org/index.ts` |
| 全局 UI Store | `src/stores/Global/index.ts` |
| MR diff 设置 Store | `src/stores/merge.ts` |
| 事件总线 | `src/utils/eventBus.ts`mitt |

View File

@@ -0,0 +1,32 @@
# 可信开源态势感知平台 - 项目文档
## 简历原内容
> **可信开源态势感知平台**
>
> **项目背景**一个态势感知平台提供数据监控与分析、可视化展示、态势分析等功能嵌有自研金银湖AI大模型预测安全风险趋势关注开源项目生态如HarmonyOS生态的动态监控
>
> **技术栈**Vue3+JavaScript(ES6+)+SCSS+MicroApps+Echarts+Pinia
>
> **项目内容**
> - 可视化展示集成ECharts实现多维度数据分析和展示采用微前端架构MicroApps支持模块化开发
> - 状态管理使用Pinia构建模块化Store用户/仓库/组织/全局封装Composition API Hooks实现逻辑复用
> - 性能优化采用虚拟滚动和懒加载策略路由守卫优化、API 缓存降级等技术优化页面性能
> - AI对话集成@matechat/core 实现AI智能助手功能支持多轮对话、思考过程展示、组件化消息渲染软件详情/预警列表/智能推荐)
---
## 详细文档
| 模块 | 文档 | 核心内容 |
| ----- | ---------------------------------------------------------------------- | --------------------------------------------------------- |
| 可视化展示 | [ECharts 多维度数据分析与展示实现详解](ECharts多维度数据分析与展示实现详解.md) | 对应简历"集成ECharts实现多维度数据分析和展示"的完整实现解读、面试口述参考 |
| 可视化展示 | [微前端架构 MicroApps 模块化开发实现详解](微前端架构MicroApps模块化开发实现详解.md) | 对应简历"采用微前端架构MicroApps支持模块化开发"的完整实现解读、面试口述参考 |
| 状态管理 | [Pinia 状态管理实现详解](Pinia状态管理实现详解.md) | 对应简历"使用Pinia构建模块化Store"的完整实现解读、面试口述参考 |
| 性能优化 | [性能优化实现详解](性能优化实现详解.md) | 对应简历"采用虚拟滚动和懒加载策略路由守卫优化、API 缓存降级等技术优化页面性能"的完整实现解读、面试口述参考 |
| AI对话 | [AI 智能助手功能实现详解](AI智能助手功能实现详解.md) | 对应简历"集成@matechat/core实现AI智能助手功能"的完整实现解读、面试口述参考 |
| 状态管理 | [Composition API Hooks 逻辑复用实现详解](Composition%20API%20Hooks逻辑复用实现详解.md) | 对应简历"封装Composition API Hooks实现逻辑复用"的完整实现解读、面试口述参考 |
---

View File

@@ -0,0 +1,403 @@
# 微前端架构 MicroApps 模块化开发 — 面试版
> 简历原话:**"采用微前端架构 MicroApps 支持模块化开发"**
>
> 这篇文档帮你理解这句话背后到底做了什么,以及面试时怎么讲。
---
## 一、先搞清楚:这句话到底是什么意思?
拆成三部分理解:
| 关键词 | 含义 | 项目中对应 |
|--------|------|------------|
| **微前端** | 把一个大前端应用拆成多个小应用,每个小应用独立开发、独立部署 | 主应用(态势感知平台)+ 3 个子应用首页、用户中心、AI Copilot |
| **MicroApps** | 具体使用的微前端框架名,全称 `@micro-zoe/micro-app`(京东开源) | package.json 中声明的依赖,版本 1.0.0-rc.3 |
| **模块化开发** | 每个子应用由不同团队/人员独立开发,互不影响 | 3 个子应用各有独立的代码仓库、独立的域名、独立的部署地址 |
**一句话概括**:我用京东开源的 micro-app 框架,把平台拆成主应用 + 3 个子应用,实现了各模块独立开发、独立部署,同时通过消息通信保证它们协同工作。
---
## 二、为什么需要微前端?(面试必答)
### 没有微前端的问题
想象一下没有微前端的情况:
```
一个巨大的 Vue 项目,包含:
├── 首页/搜索功能(首页团队负责)
├── 用户中心/个人主页(用户团队负责)
├── AI Copilot 对话AI 团队负责)
└── 态势感知/安全分析(你负责)
问题:
1. 所有人改同一个代码仓库,合并冲突不断
2. 一个模块出 bug整个平台挂掉
3. 改一个小功能,要重新部署整个项目
4. 各团队技术栈想用不同的,但被绑死在一个项目里
```
### 用了微前端之后
```
主应用(壳):负责顶部导航栏、侧边栏、路由调度
├── 子应用1首页团队的独立项目负责首页/搜索
├── 子应用2用户团队的独立项目负责用户中心
├── 子应用3AI团队的独立项目负责 AI Copilot
└── 你自己开发的模块:直接写在主应用里(安全态势/生态/社区)
好处:
1. 各团队独立仓库、独立开发,互不干扰
2. 子应用挂了不影响主应用和其他子应用
3. 子应用可以独立部署,不用动主应用
4. 各子应用可以用不同的技术栈(虽然我们都是 Vue3
```
---
## 三、项目里具体怎么用的?
### 3.1 整体架构图
```
┌─────────────────────────────────────────────────────┐
│ 主应用(你的项目) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ 顶部导航栏 │ │ 侧边菜单栏 │ │ 内容区域(路由) │ │
│ └──────────┘ └──────────┘ │ │ │
│ │ /explore → 子应用1 │ │
│ │ /user → 子应用2 │ │
│ │ /ai → 子应用3 │ │
│ │ /security → 你开发的 │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌──────────┐ ┌──────────────┐
│ 子应用1 │ │ 子应用2 │ │ 子应用3 │
│ 首页/搜索 │ │ 用户中心 │ │ AI Copilot │
│ (独立域名) │ │ (独立域名) │ │ (独立域名) │
└─────────────┘ └──────────┘ └──────────────┘
```
### 3.2 3 个子应用分别是什么
| 子应用名 | 干什么用 | 独立域名(生产环境) |
|----------|----------|---------------------|
| `micoro-app-homeweb-app` | 首页、搜索、G-Star 开源榜 | `homepage-app.gitcode.com` |
| `user-center` | 用户个人主页、仓库、收藏、设置 | `usercenter-app.gitcode.com` |
| `copilot-app` | AI 智能助手对话 | `copilot-app.gitcode.com` |
每个子应用都有**4 套环境地址**(开发/测试/预发/生产),通过 `.env` 文件配置:
```bash
# .env.development开发环境
VITE_CHILD_HOMEWEB_HOST = 'https://test.gitcode.net/child/homeweb-app/'
VITE_USER_CENTER_HOST = 'https://test.gitcode.net/child/user-center/'
VITE_COPILOT_HOST = 'https://test.gitcode.net/child/aichat-app/'
# .env.production生产环境
VITE_CHILD_HOMEWEB_HOST = 'https://homepage-app.gitcode.com'
VITE_USER_CENTER_HOST = 'https://usercenter-app.gitcode.com'
VITE_COPILOT_HOST = 'https://copilot-app.gitcode.com'
```
---
## 四、怎么实现的?(面试核心)
### 4.1 怎么把子应用"嵌入"主应用
有两种方式,项目里都用了:
#### 方式一:声明式 — `<micro-app>` 标签(用于用户中心)
就像写 HTML 标签一样简单:
```vue
<!-- proxy.vue 用户中心的代理组件 -->
<template>
<micro-app name="user-center" :url="url" iframe baseroute="/"></micro-app>
</template>
<script setup>
const url = import.meta.env.VITE_USER_CENTER_HOST; // 从环境变量取子应用地址
</script>
```
**意思是**:在页面上放一个 `<micro-app>` 标签,告诉它子应用叫 `user-center`,地址是 `url`,用 iframe 模式加载。就这么简单。
#### 方式二:命令式 — `microApp.renderApp()`(用于首页和 AI
用 JavaScript 手动调用渲染:
```typescript
// proxy-homeweb.vue — 首页子应用的代理组件
onMounted(() => {
microApp.renderApp({
name: 'micoro-app-homeweb-app', // 子应用名
url: origin, // 子应用地址
container: '#homeweb-container', // 渲染到哪个 DOM 元素
data: { emitter, $router: router }, // 传给子应用的数据
iframe: true, // 用 iframe 隔离
baseroute: '/', // 路由前缀
});
});
onBeforeUnmount(() => {
microApp.unmountApp('micoro-app-homeweb-app'); // 页面离开时卸载子应用
});
```
**为什么两种方式都要用**:声明式更简单,但命令式更灵活,可以在渲染时传入更多数据(如事件总线、路由实例)。
### 4.2 子应用怎么和主应用"对话"
微前端最难的部分不是加载子应用,而是**主应用和子应用之间的通信**。项目里用了 3 种通信方式:
#### 方式 1`setData` — 主应用主动发消息给子应用
```typescript
// 主应用告诉用户中心"路由变了"
microApp.setData('user-center', {
type: 'application-route',
name: route.name,
params: route.params,
query: route.query
});
// 主应用告诉首页子应用"搜索关键词变了"
microApp.setData('micoro-app-homeweb-app', {
type: 'keyword-change',
keyword: searchKeyword
});
```
#### 方式 2`addDataListener` — 主应用监听子应用的消息
```typescript
// 主应用监听用户中心发来的各种消息
microApp.addDataListener('user-center', (config) => {
// 子应用说"用户关注状态变了"
if (config.type === 'user_hasFollowed_update') {
otherStore.saveFollowed(config.data.status);
}
// 子应用说"用户资料更新了"
if (config.type === 'user_profile_update') {
saveAccountInfo(config.data);
}
// 子应用说"要跳转路由"
if (config.type === 'route-change') {
router.push({ name: config.name, params: config.params });
}
});
```
#### 方式 3共享事件总线 — 通过 mitt 事件库
```typescript
// 主应用创建事件总线,传给子应用
microApp.renderApp({
name: 'micoro-app-homeweb-app',
data: {
emitter, // ← mitt 事件总线实例
$router: router // ← 路由实例
}
});
// 子应用通过事件总线通知主应用"路由变了"
// 主应用监听这个事件,同步到自己的路由
addEventListener('microRouterChange', (to) => {
router.push(to.fullPath);
});
```
**通信方式总结**
```
主应用 → 子应用microApp.setData('子应用名', 数据)
子应用 → 主应用microApp.addDataListener('子应用名', 回调)
双向通信:共享 emitter 事件总线
```
### 4.3 路由怎么同步?
主应用和子应用各有各的路由,需要保持同步:
```typescript
// microAppConfig.ts — 路由同步工具
// 1. 子应用路由变了 → 同步到主应用
addEventListener('microRouterChange', (to) => {
router.push(to.fullPath); // 主应用跟着跳
});
// 2. 主应用路由变了 → 同步到子应用
router.beforeEach((to, from) => {
if (to.meta.micorApp) {
// 告诉子应用:你要跳到这个路径
to.meta.micorApp.forEach(appName => {
microApp.router.replace({ name: appName, path: to.fullPath });
});
}
});
```
**通俗理解**:就像两个人各开一辆车,要保持同一路线。主应用的路由变了,通知子应用跟上;子应用的路由变了,也通知主应用跟上。
### 4.4 页面加载优化
为了让子应用加载更快,做了两个优化:
#### 优化 1DNS 预解析
```html
<!-- index.html — 提前解析子应用域名的 DNS -->
<link rel="preconnect" href="//homepage-app.gitcode.com" crossorigin />
<link rel="dns-prefetch" href="//homepage-app.gitcode.com">
<link rel="dns-prefetch" href="//usercenter-app.gitcode.com">
```
**通俗理解**:用户还没点到子应用的页面,浏览器就已经提前"打听"子应用域名的地址了,等真正加载时就更快。
#### 优化 2开发环境跨域处理Vite proxy
子应用和主应用不在同一个域名下,但 micro-app 的 iframe 模式下,主子应用通信靠 `postMessage`(原生 API天然支持跨域不受 CORS 限制),所以**不需要额外配置 CORS**。
真正需要处理跨域的是**主应用调用后端 API**的场景。开发环境中,前端跑在 `localhost:443`,后端在 `http://222.20.126.217:8100`域名不同会触发跨域。Vite 用 proxy 代理解决:
```typescript
// vite.config.ts
server: {
proxy: {
'/api': {
target: 'http://222.20.126.217:8100', // 后端地址
changeOrigin: true, // 修改 Origin 头,绕过同源策略
}
}
}
```
**原理**浏览器→Vite 开发服务器同源不跨域→Vite 帮你转发到后端(服务器之间没有跨域限制)。生产环境则由 nginx 做反向代理,原理一样。
---
## 五、Vue 构建层面的适配
微前端框架需要在 Vue 构建工具中做一些特殊配置:
```typescript
// vite.config.ts
vue({
template: {
compilerOptions: {
isCustomElement: tag => /^micro-app/.test(tag)
// ← 告诉 Vue遇到 <micro-app> 标签不要报错,它是自定义元素
}
}
})
```
**通俗理解**Vue 默认不认识 `<micro-app>` 这个标签会报警告。加上这个配置后Vue 就知道"这是个自定义元素,不用管它"。
---
## 六、生命周期管理(防内存泄漏)
每个子应用的代理组件都遵循统一的生命周期:
```typescript
// 挂载时:加载子应用
onMounted(() => {
microApp.renderApp({ name: 'xxx', url: origin, container: '#xxx' });
});
// 卸载时:销毁子应用
onBeforeUnmount(() => {
microApp.unmountApp('xxx'); // 卸载子应用
microApp.clearDataListener(); // 清理数据监听器(避免内存泄漏)
});
```
**为什么重要**:如果不卸载,子应用的 DOM 事件监听器、定时器、网络请求都还在运行,会造成内存泄漏。
---
## 七、整体架构一句话总结
```
主应用(壳)
├── 顶部导航 + 侧边栏 + 路由调度(主应用负责)
├── 态势感知/生态/社区模块(直接写在主应用里)
└── 3 个子应用(通过 micro-app 框架嵌入)
├── 首页/搜索(独立仓库,独立域名)
├── 用户中心(独立仓库,独立域名)
└── AI Copilot独立仓库独立域名
通信方式setData主→子+ addDataListener子→主+ 事件总线(双向)
隔离方式iframe 模式(样式/JS 完全隔离)
路由同步:双向监听,互相通知
```
---
## 八、面试问答准备
### Q1"采用微前端架构 MicroApps 支持模块化开发"具体做了什么?
> 我们平台使用京东开源的 micro-app 框架,把整个应用拆成了主应用 + 3 个子应用。主应用负责导航栏、侧边栏和路由调度3 个子应用首页搜索、用户中心、AI Copilot各自独立开发、独立部署。主应用通过 micro-app 框架将子应用嵌入到自己的页面中,使用 iframe 模式实现样式和 JS 的完全隔离,同时通过 setData、addDataListener 和事件总线三种方式实现主子应用之间的数据通信。
### Q2为什么选择 micro-app 而不是 qiankun
> micro-app 是京东开源的下一代微前端框架,相比 qiankun 有几个优势:第一,接入成本低,只需要一个 `<micro-app>` 标签就能加载子应用,不需要像 qiankun 那样改造子应用的入口文件和打包配置;第二,支持 iframe 模式,能实现更好的样式和 JS 隔离;第三,提供了 setData/addDataListener API 让主子应用通信更方便。
### Q3主应用和子应用之间怎么通信
> 我们用了三种方式:第一,主应用通过 `microApp.setData()` 主动推送数据给子应用,比如路由变化、搜索关键词;第二,主应用通过 `microApp.addDataListener()` 监听子应用发来的消息,比如用户关注状态更新、路由跳转请求;第三,通过共享的 mitt 事件总线实现双向通信,主应用创建事件总线实例后通过 renderApp 的 data 参数传给子应用。路由同步方面,主应用和子应用互相监听路由变化,保持双向同步。
### Q4微前端的隔离和跨域怎么处理的
> 我们用的是 iframe 模式(`iframe: true`),每个子应用运行在独立的 iframe 中。iframe 天然提供了 JS 作用域隔离和 CSS 样式隔离子应用的全局变量、样式都不会污染主应用。跨域方面iframe 模式下主子应用通信靠 `postMessage`,这是浏览器原生 API天然支持跨域不需要额外配置 CORS。开发环境主应用调后端 API 的跨域问题,则通过 Vite 的 proxy 代理解决。
### Q5子应用加载会不会很慢
> 我们做了两个优化:第一,在 index.html 中通过 DNS prefetch 提前解析子应用域名,用户还没点到子应用页面时浏览器就已经在解析域名了;第二,子应用只在用户真正访问对应路由时才加载,不是一进主应用就把所有子应用都加载出来,是按需加载的。
### Q6开发环境和生产环境的子应用地址不一样怎么办
> 通过 Vite 的环境变量机制管理。每个环境(开发/测试/预发/生产)都有独立的 `.env` 文件,里面配置了对应环境的子应用 URL。代码中通过 `import.meta.env.VITE_CHILD_HOMEWEB_HOST` 读取,构建时自动替换为对应环境的地址,不需要改代码。
---
## 九、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 微前端框架 | micro-app 1.0.0-rc.3(京东开源) |
| 子应用数量 | 3 个 |
| 通信方式 | 3 种setData / addDataListener / 事件总线) |
| 隔离方式 | iframe 模式(完全隔离) |
| 环境配置 | 4 套(开发/测试/预发/生产) |
| 代理组件 | 3 个(每个子应用对应一个) |
---
## 十、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 框架依赖声明 | `package.json`@micro-zoe/micro-app |
| 构建配置(自定义元素+CORS | `vite.config.ts` |
| DNS 预解析 | `index.html` |
| 环境变量(子应用地址) | `.env.development` / `.env.production` 等 |
| 首页子应用代理 | `src/views/micro-page/proxy-homeweb.vue` |
| 用户中心子应用代理 | `src/views/UserCenter/proxy.vue` |
| AI Copilot 子应用代理 | `src/views/micro-page/proxy-copilot.vue` |
| 路由同步工具 | `src/utils/microAppConfig.ts` |
| 事件总线 | `src/utils/eventBus.ts` |
| 路由配置(带子应用标记) | `src/router/config/home.ts` / `user.ts` / `setting.ts` |
| 布局中的通信监听 | `src/layouts/v2/DefaultLayout/index.vue` |

View File

@@ -0,0 +1,568 @@
# 性能优化 — 面试版
> 简历原话:**"采用虚拟滚动和懒加载策略路由守卫优化、API 缓存降级等技术优化页面性能"**
>
> ⚠️ **重要提醒**:代码中**没有**虚拟滚动的实现(没有相关库或代码)。"懒加载"是真实存在的路由懒加载、IntersectionObserver 等),"路由守卫优化"是真实存在的interceptor.ts 248 行),"API 缓存降级"也是真实存在的360 行自研代码)。面试时建议把重点放在"路由守卫优化 + 懒加载 + API 缓存降级 + 防抖节流 + shallowRef"这五个真实存在的优化上。
>
> 这篇文档基于代码实际情况,帮你理解项目中做了哪些性能优化,以及面试时怎么讲。
---
## 一、先搞清楚:项目里到底做了哪些优化?
实际代码中有 **7 大类性能优化**,都是真实存在的:
| 优化手段 | 做了什么 | 数量/规模 | |
| ------------------------ | ------------------------------------------------------------------------- | ----------------- | --- |
| **路由守卫优化** | beforeEach 缓存 ID 避免重复 API、Promise.all 并行请求、条件性请求、客户端预校验、声明式 meta 配置、白名单短路 | 248 行核心代码7 种优化策略 | |
| **路由懒加载** | 所有页面组件用动态 import按需加载 | 80+ 个路由 | |
| **防抖节流** | 用户输入、滚动、resize 等高频操作加防抖/节流 | 40+ 处 | |
| **API 缓存降级** | 请求结果缓存到 IndexedDB失败时读缓存兜底 | 自研系统360 行代码 | |
| **shallowRef** | 大数据对象用浅层响应式,避免深度遍历 | 30+ 处 | |
| **IntersectionObserver** | 元素进入视口才加载/上报,不提前渲染 | 6+ 处 | |
| **lodash 按需引入** | 只引入用到的函数,不打包整个 lodash | 全项目 | |
**一句话概括**我从路由守卫、路由加载、用户交互、API 请求、响应式数据、DOM 渲染六个层面做了性能优化,核心思路是"能不加载就不加载,能少算就少算,能缓存就缓存"。
---
## 二、每个优化怎么做的?(面试核心)
### 2.1 路由守卫优化(简历新增,技术含量最高之一)
**问题**80+ 个页面每次导航都要经过 `router.beforeEach` 守卫,如果守卫里每次都调 API 查权限、查组织信息、查仓库信息,页面切换会非常慢。
**解决方案**:在 `src/router/interceptor.ts`248 行)中实现了一个 async beforeEach 守卫,通过 7 种优化策略大幅减少不必要的 API 调用。
#### 守卫的 7 个执行阶段
```
Token 验证 → namespace 类型解析 → ref 追踪 → 组织详情获取 → 仓库详情+权限
→ 页面标题设置 → 登录权限检查
```
#### 优化 1模块级 ID 缓存,避免重复 API 调用
```typescript
// interceptor.ts 第 18-19 行 — 模块级缓存变量
let currentOrgId = ''; // 缓存当前组织 ID
let currentRepoId = ''; // 缓存当前仓库 ID
// 切换组织时:只有 orgId 变了才调 API
if (orgId.value !== currentOrgId) {
const res = await reqCatch(getOrg, { orgId: orgId.value, with_full_path: true });
// ... 请求组织信息
}
currentOrgId = orgId.value; // 更新缓存
// 切换仓库时:只有 repoId 变了才调 API
if (repoId.value !== currentRepoId) {
const [repoRes, roleRes] = await Promise.all([...]);
// ... 请求仓库信息
}
currentRepoId = repoId.value; // 更新缓存
```
**为什么这样设计?** 同一个组织/仓库下有多个子页面如仓库的文件、Issue、Merge Request、Wiki 等 tab用户在这些 tab 之间切换时,仓库信息不需要重新请求。用模块级变量缓存 ID只在真正切换组织/仓库时才发请求。
#### 优化 2Promise.all 并行请求
```typescript
// interceptor.ts 第 128-130 行
// ❌ 串行写法:两个请求先后发,总耗时 = API1 + API2
// const repoRes = await getRepo({ repoId, statistics: true });
// const roleRes = await reqCatch(getRepoRole, { repoId });
// ✅ 并行写法:两个请求同时发,总耗时 = max(API1, API2)
const promises = [getRepo({ repoId: repoId.value, statistics: true }, { customError: true })];
account.isLogin && promises.push(reqCatch(getRepoRole, { repoId: repoId.value }));
const [repoRes, roleRes] = await Promise.all(promises);
```
**效果**:仓库信息 + 角色权限两个请求并行发出,总耗时约等于较慢的那个请求,而不是两者之和。
#### 优化 3条件性请求——不需要的数据就不请求
```typescript
// 只有登录用户才请求角色权限
account.isLogin && promises.push(reqCatch(getRepoRole, { repoId: repoId.value }));
// 登录后才会调用的 notice 接口
// 在 useRepoInit 中if (!isLogin) return;
```
**为什么这样设计?** 未登录用户看不到仓库的管理功能,不需要知道当前用户的角色。跳过这个请求既省时间又省后端资源。同样,组织详情里的关注/通知等接口,未登录时直接跳过。
#### 优化 4客户端预校验——在发请求之前就拦截无效路径
```typescript
// interceptor.ts 第 122-124 行 — 正则预校验
const { repoId } = useRepoId('%2F', to.params.namespace, to.params.repoName);
// 禁止访问以 .wiki、.git、.atom 结尾的仓库路径
if (!repoPathSuffix.test(repoId.value)) {
return { name: '404' }; // 客户端直接返回 404不发任何 API 请求
}
```
**效果**:用户误输入了 `xxx.wiki.git` 这样的路径,在发起任何 API 请求之前就被拦截。节省了一次无意义的网络请求。
#### 优化 5声明式 meta 配置——权限控制不写死在守卫里
```typescript
// 路由定义中声明config 文件)
{ path: '/dashboard', meta: { needLogin: true, loginType: '访问 dashboard' } }
{ path: '/login', meta: { loginHidden: true } }
// 守卫中统一判断interceptor.ts 第 215-232 行)
if (to.meta.needLogin) {
if (account.isLogin) {
return true;
} else {
emitEvent('login', { triggerType: to.meta?.loginType }); // 弹出登录框
return false;
}
} else {
if (account.isLogin && to.meta.loginHidden) {
return { name: 'dashboard' }; // 已登录用户看登录页,重定向到首页
}
}
```
**为什么这样设计?** 新增一个需要登录的页面,只需要在路由定义里加 `meta: { needLogin: true }`,不需要改守卫代码。守卫逻辑和路由配置解耦,可维护性强。
#### 优化 6白名单短路——不该处理的页面直接跳过
```typescript
// interceptor.ts 第 42 行 — 白名单
const fromHomeWeb = ['home', 'search', 'gStar', 'gStarApply', 'explore'];
// ref 追踪:子应用页面不上报 ref
if (!fromHomeWeb.includes(to.name)) {
window.page_ref = ref;
sessionStorage.setItem('ref', BASE_URL + to.fullPath);
}
// 页面量上报:子应用页面不上报 PV
if (fromHomeWeb.includes(to.name)) return;
```
**为什么这样设计?** 微前端子应用homeweb内部有自己的路由和上报逻辑主应用的守卫不应该干涉。白名单让这些页面直接跳过避免冲突和重复调用。
#### 优化 7localStorage 持久化——跨会话记住用户行为
```typescript
// interceptor.ts 第 22-31 行 — 记录已访问的仓库
const setRepoVisited = (id) => {
let visitedRepoIds = localStorage.getItem('visited_repo_list')?.split(',') || [];
visitedRepoIds.push(`${id}`);
visitedRepoIds = Array.from(new Set(visitedRepoIds));
localStorage.setItem(visitedRepoList, visitedRepoIds.join());
};
const isRepoVisited = (id) => {
return localStorage.getItem(visitedRepoList)?.includes(id) || false;
};
// 守卫中的应用:首次进仓库 → 跳转 Dashboard之后进仓库 → 直接进代码 tab
if (!account.isLogin || !repoStore.isVisitor) {
isGotoRepoDashboard = !isRepoVisited(repoId);
}
```
**为什么这样设计?** 用户第一次访问某个仓库时,引导去 Dashboard 了解概况;后续再访问时直接进入代码页面。这个"是否首次访问"的状态持久化在 localStorage 中,跨会话保持,不需要后端 API。
#### 路由守卫优化 — 完整架构图
```
router.beforeEach(async (to, from) => {
├── 1. Token 验证 ──── checkIsLogin() 只在有 token 时才调 API
├── 2. namespace 类型 ── userOrOrg 路由才调 getPathType API
├── 3. ref 追踪 ────── fromHomeWeb 白名单跳过子应用
├── 4. 组织详情 ────── currentOrgId 缓存,切换组织才请求
│ 404/403 → 直接返回错误页,不继续走后续逻辑
├── 5. 仓库详情 ────── currentRepoId 缓存 + Promise.all 并行
│ repoPathSuffix 正则预校验 → 无效路径不发请求
│ module_setting 模块开关 → 禁用模块走 404
│ 首次访问 Dashboard 重定向 → localStorage 持久化
├── 6. 页面标题 ────── 根据 type 自动拼接组织/仓库名
└── 7. 登录检查 ────── meta.needLogin / meta.loginHidden 声明式控制
})
```
**面试话术**
> "路由守卫优化是我在项目中做的一个重要优化。80 多个页面每次导航都经过 beforeEach如果每次都调 API页面切换会很慢。我做了 7 种优化:第一,模块级 ID 缓存,用 currentOrgId 和 currentRepoId 两个变量,只有真正切换组织/仓库时才发 API同一仓库内切换 tab 不发请求;第二, Promise.all 并行请求,仓库信息和角色权限两个请求同时发出;第三,条件性请求,未登录用户不请求角色权限;第四,客户端预校验,用正则过滤无效仓库路径,在发请求之前就返回 404第五声明式 meta 配置权限控制逻辑不在守卫里硬编码第六白名单短路子应用页面跳过主应用守卫逻辑第七localStorage 持久化,记住用户首次访问仓库的状态,跨会话保持。这些优化让页面切换更快、更流畅。"
---
### 2.2 路由懒加载(最重要,必讲)
**问题**:项目有 80+ 个页面,如果打包成一个 JS 文件,首页加载要下载好几 MB。
**解决方案**:每个路由组件用 `() => import(...)` 动态导入,访问哪个页面才下载哪个页面的代码。
```typescript
// router/config/home.ts — 每个路由都是懒加载
const routes = [
{
path: '/explore',
component: () => import('@/views/micro-page/proxy-homeweb.vue'), // ← 动态导入
},
{
path: '/security',
component: () => import('@/views/Jyh/security/index.vue'), // ← 动态导入
},
{
path: '/ecosystem',
component: () => import('@/views/Jyh/ecosystem/index.vue'), // ← 动态导入
},
// ... 80+ 个路由全部这样写
];
```
**效果**
- 首页只加载首页需要的代码(几十 KB
- 用户点到"安全态势"页面,才下载安全态势的代码
- 每个页面变成独立的 JS 文件chunk按需加载
**地图数据也是懒加载的**
```javascript
// utils/mapLoader.js — 地图数据按需加载
export const loadWorldMap = async () => {
await import('../views/Jyh/security/components/world.js'); // 用到世界地图时才加载
};
export const loadChinaMap = async () => {
await import('/china.js'); // 用到中国地图时才加载
};
```
**面试话术**
> "项目有 80 多个页面,全部用路由懒加载。每个页面组件通过 `() => import(...)` 动态导入Vite 构建时自动拆分成独立的 chunk用户访问哪个页面才下载哪个页面的代码。地图数据也是懒加载的世界地图和中国地图的 GeoJSON 文件(几百 KB只在用户进入安全态势页面时才加载。"
---
### 2.3 防抖节流(数量最多)
**问题**:用户快速输入搜索、频繁滚动、拖拽窗口大小时,事件每秒触发几十次,每次都调 API 或重绘图表会很卡。
**解决方案**用防抖debounce和节流throttle控制触发频率。
#### 防抖debounce— 等用户"停下来"才执行
```typescript
import debounce from 'lodash/debounce'; // 按需引入
// 搜索输入:用户停止输入 300ms 后才发请求
const handleSearch = debounce((keyword) => {
fetchSearchResults(keyword);
}, 300);
// 滚动加载:用户停止滚动 300ms 后才检查是否需要加载更多
const handleScroll = debounce(() => {
checkLoadMore();
}, 300);
```
#### 节流throttle— 每隔一段时间才执行一次
```typescript
import throttle from 'lodash/throttle';
// 窗口 resize每 100ms 最多执行一次
const handleResize = throttle(() => {
recalculateLayout();
}, 100);
// 滚动事件:每 200ms 最多执行一次
const handleScroll = throttle(() => {
updateScrollPosition();
}, 200);
```
**项目中的分布**
| 场景 | 使用方式 | 延迟时间 | 出现次数 |
|------|----------|----------|----------|
| 搜索输入 | debounce | 300ms | 15+ 处 |
| 表单提交 | debounce | 300ms | 10+ 处 |
| 列表滚动加载 | debounce | 300ms | 5+ 处 |
| 窗口 resize | throttle | 100ms | 2 处 |
| 滚动事件 | throttle | 200ms | 2 处 |
| 表格选择 | debounce | 10ms | 1 处 |
| 侧边栏展开 | debounce | 600ms | 1 处 |
**为什么 lodash 按需引入?**
```typescript
// ✅ 正确:只引入 debounce 函数
import debounce from 'lodash/debounce';
// ❌ 错误:引入整个 lodash 库(几百 KB
import { debounce } from 'lodash';
```
按需引入让 Vite 的 tree-shaking 生效,最终打包只包含用到的函数,而不是整个 lodash 库。
**面试话术**
> "项目中有 40 多处防抖节流。搜索输入用 300ms 防抖,等用户停下来才发请求;窗口 resize 用 100ms 节流,限制重绘频率;滚动事件用 200ms 节流。所有 lodash 函数都按需引入(`lodash/debounce`),配合 Vite 的 tree-shaking不会打包整个 lodash 库。"
---
### 2.4 API 缓存降级系统(技术含量最高)
**问题**:后端 API 可能超时或报错,用户看到的是空白页面或加载失败。
**解决方案**:自研了一套请求降级拦截器,成功时缓存响应,失败时读缓存兜底。
#### 整体流程
```
请求发出 → 正常返回? → 是 → 缓存到 IndexedDB → 返回数据给页面
→ 否(超时/报错)→ 读 IndexedDB 缓存 → 返回缓存数据给页面
```
#### 核心代码(简化版)
```typescript
// degradeInterceptor.ts — 请求降级拦截器
class DegradeInterceptor {
// 请求成功时:缓存响应到 IndexedDB
onResponseFulfilled(response) {
this.saveToCache(response.config.url, response.data);
return response;
}
// 请求失败时:从 IndexedDB 读缓存兜底
onResponseRejected(error) {
const cachedData = this.readFromCache(error.config.url);
if (cachedData) {
return cachedData; // 返回缓存数据,用户看到的是"旧数据"而不是错误
}
return Promise.reject(error);
}
}
```
#### 缓存配置
```typescript
// storage-apis.ts — 哪些 API 走缓存策略
const storageApis = [
{ url: '/api/v1/issues', timeout: 5000, retry: 2 }, // 5秒超时重试2次
{ url: '/api/v1/merge_requests', timeout: 5000, retry: 2 },
// ...
];
```
#### 401 Token 刷新时的请求去重
```typescript
// request.ts — 防止 token 过期时同时发 10 个刷新请求
let isRefreshing = false;
let pendingQueue = []; // 等待队列
// token 过期时:
if (!isRefreshing) {
isRefreshing = true;
await refreshToken();
isRefreshing = false;
pendingQueue.forEach(cb => cb()); // 把等待的请求全部重发
pendingQueue = [];
} else {
// 已经在刷新了,把当前请求放进队列等着
return new Promise(resolve => pendingQueue.push(resolve));
}
```
**面试话术**
> "我实现了一套 API 缓存降级系统。请求成功时把响应缓存到 IndexedDB后端超时或报错时自动读缓存兜底用户看到的是旧数据而不是错误页面。还有 token 刷新时的请求去重——token 过期时如果有 10 个请求同时发出,只会发一次刷新请求,其他 9 个排队等待,刷新完成后统一重发。"
---
### 2.5 shallowRef 减少响应式开销
**问题**Vue 的 `ref()` 会深度遍历对象的每个属性,给每个属性都加响应式代理。对于大数据对象(几百条列表数据),这个遍历本身就很耗性能。
**解决方案**:用 `shallowRef()` 只监听整个对象的替换,不监听内部属性变化。
```typescript
import { shallowRef } from 'vue';
// ❌ 普通 ref深度响应式遍历每个属性
const tableData = ref(bigDataArray); // 1000 条数据,每条都加代理
// ✅ shallowRef浅层响应式只监听整体替换
const tableData = shallowRef(bigDataArray); // 不遍历,只在 .value 被替换时触发更新
```
**项目中 30+ 处使用**,主要用于:
- 大数据列表(表格数据、搜索结果)
- 恶意软件分析详情(大 JSON 对象)
- 威胁内容列表
- 表单引用(不需要响应式)
- 分页器状态(用 `shallowReactive`
```typescript
// 典型用法:表格数据用 shallowRef
const tableData = shallowRef([]);
const fetchTableData = async () => {
const res = await api.getData();
tableData.value = res.data; // 整体替换,触发更新
};
// 分页器用 shallowReactive
const pager = shallowReactive({ page: 1, size: 20, total: 0 });
```
**面试话术**
> "对于大数据对象,我用 shallowRef 替代 ref。ref 会深度遍历对象每个属性加响应式代理1000 条列表数据的遍历开销很大。shallowRef 只监听整体替换,不监听内部属性变化,数据更新时直接 `tableData.value = newData` 整体替换就行。项目中有 30 多处使用。"
---
### 2.6 IntersectionObserver 懒加载
**问题**:页面下方的内容在用户滚动到之前不需要渲染。
**解决方案**:用 IntersectionObserver 监听元素是否进入视口,进入后才触发加载或上报。
#### 自定义曝光指令
```typescript
// directives/element-exposure.ts — 自定义 Vue 指令
const vExposure = {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
binding.value.trigger('expo'); // 元素进入视口,触发上报
observer.unobserve(el); // 上报一次后取消监听
}
});
}, { threshold: 0 });
observer.observe(el);
}
};
// 使用:<div v-exposure="{ trigger: reportExposure }">...</div>
```
#### 滚动触发动画
```javascript
// product1.vue — 元素滚动到视口时才播放动画
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
startAnimation(); // 进入视口才开始动画
observer.disconnect();
}
});
observer.observe(elementRef.value);
```
**面试话术**
> "我用 IntersectionObserver 实现了元素的懒加载。自定义了一个 Vue 指令,元素进入视口时才触发曝光上报或播放动画,上报后自动取消监听。这样页面下方的内容在用户滚动到之前不会有任何开销。"
---
### 2.7 @vueuse/core 工具库
项目大量使用 vueuse 提供的性能优化工具,避免自己写低效代码:
| 工具 | 用在哪 | 干什么 |
|------|--------|--------|
| `useWindowSize` | 响应式布局 | 监听窗口大小,返回响应式 width/height |
| `useThrottleFn` | resize 事件 | 节流函数100ms 最多执行一次 |
| `useResizeObserver` | 内容溢出检测 | 高效检测元素尺寸变化,不轮询 |
| `useElementSize` | 搜索框/布局 | 获取元素宽高 |
| `useClipboard` | 复制按钮8处 | 剪贴板操作 |
| `useInfiniteScroll` | 历史记录 | 无限滚动加载 |
| `useEventListener` | 文件搜索 | 事件监听,自动清理 |
| `useMutationObserver` | 文本组件 | DOM 变化监听 |
---
## 三、哪些简历上写了但代码里没有?
| 简历描述 | 代码实际情况 | 建议 |
|----------|------------|------|
| **虚拟滚动** | ❌ 没有实现(无相关库/代码,无 vue-virtual-scroller 等依赖) | 面试时别主动提,被问到可以说"评估后发现数据量不需要虚拟滚动,分页已经控制了单页数据量" |
| **懒加载策略** | ✅ 真实存在(路由懒加载 + IntersectionObserver + 地图数据懒加载) | 可以讲 |
| **路由守卫优化** | ✅ 真实存在interceptor.ts 248 行7 种优化策略) | 可以讲,重点讲 ID 缓存 + Promise.all 并行 + 条件性请求 |
| **API 缓存降级** | ✅ 真实存在degradeInterceptor.ts 360 行 + request.ts token 去重) | 可以讲,技术含量最高的优化之一 |
**面试时建议这样讲**
> "性能优化方面我做了路由守卫优化ID 缓存 + Promise.all 并行 + 客户端预校验等 7 种策略、路由懒加载80+ 页面按需加载、防抖节流40+ 处高频事件优化、API 缓存降级IndexedDB 缓存 + 失败兜底 + token 请求去重、shallowRef 减少响应式开销30+ 处大数据对象、IntersectionObserver 元素懒加载。这些优化综合下来,页面切换和首屏加载都有明显改善。"
---
## 四、面试问答准备
### Q1"路由守卫优化"具体做了什么?
> 路由守卫是所有页面切换的入口80+ 个页面每次导航都经过 beforeEach。如果每次都会调 API页面切换会很慢。我做了 7 种优化:第一,模块级 ID 缓存,用 currentOrgId 和 currentRepoId 缓存当前组织/仓库 ID同一仓库内切换 tab 不发请求第二Promise.all 并行请求,仓库信息和角色权限同时发出;第三,条件性请求,未登录用户不请求角色权限;第四,客户端预校验,用正则过滤无效路径,在发请求之前就返回 404第五声明式 meta 配置,用 needLogin/loginHidden 标记控制权限第六白名单短路子应用页面跳过主应用守卫第七localStorage 持久化已访问仓库列表,跨会话记住用户首次访问状态。
### Q2防抖和节流有什么区别你项目里怎么用的
> 防抖是"等用户停下来才执行",比如搜索框输入 300ms 后才发请求,用户一直打字就一直不发。节流是"每隔一段时间执行一次",比如窗口 resize 每 100ms 最多重绘一次。项目中搜索、表单提交用防抖300ms滚动和 resize 用节流100~200ms。总共 40 多处,全部用 lodash 的按需引入(`lodash/debounce`),配合 Vite tree-shaking 不会打包整个 lodash。
### Q3API 缓存降级系统怎么设计的?
> 三层设计:第一层,请求成功时把响应缓存到 IndexedDB配置了最大缓存条数和过期时间第二层请求超时或报错时自动从 IndexedDB 读缓存返回给页面用户看到旧数据而不是错误第三层token 过期时做请求去重,多个并发请求只发一次 token 刷新,其他请求排队等刷新完成后统一重发。
### Q4shallowRef 和 ref 有什么区别?为什么用 shallowRef
> ref 会深度遍历对象的每个属性,给每个属性都加响应式代理。如果数据是 1000 条列表,每条有 10 个字段ref 要给 10000 个属性加代理这个遍历本身就有开销。shallowRef 只监听整个对象的替换(.value 赋值不监听内部属性变化。对于只需要整体替换不需要监听单个字段变化的大数据对象shallowRef 性能更好。项目中表格数据、分析详情等 30 多处都用了 shallowRef。
### Q5虚拟滚动做了吗
> 项目中评估后没有引入虚拟滚动。原因是我们的列表数据量通常在几十到几百条,分页已经控制了单页数据量,虚拟滚动的收益不大,反而增加代码复杂度。如果未来数据量增长到几千条,可以引入 vue-virtual-scroller 做进一步优化。
### Q6还有哪些性能优化手段
> 还有几个lodash 按需引入做 tree-shaking不打包整个库@vueuse/core 的 35+ 个工具函数避免重复造轮子Vite 构建配置了 esbuild 压缩、4KB 以下资源内联为 base64、常用依赖预构建用 rollup-plugin-visualizer 分析打包体积。
### Q7路由守卫里的 reqCatch 是干什么的?
> reqCatch 是一个错误包装函数,包装 API 调用以捕获错误而不触发未捕获异常。在路由守卫里特别重要——比如 getOrg 请求返回 403私有组织无权限如果没有 reqCatch 包装这个错误会中断整个守卫流程导致页面白屏。reqCatch 让守卫能优雅地处理 404/403返回对应的错误页面。
---
## 五、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 路由守卫优化策略 | 7 种ID 缓存、并行请求、条件性请求、预校验、声明式 meta、白名单、localStorage 持久化) |
| 路由守卫代码量 | 248 行 |
| 路由懒加载页面数 | 80+ 个 |
| 防抖/节流使用处 | 40+ 处 |
| shallowRef 使用处 | 30+ 处 |
| @vueuse/core 使用处 | 35+ 处 |
| API 缓存降级系统 | 360 行自研代码 |
| lodash 按需引入 | 全项目统一 |
| IntersectionObserver | 6+ 处 |
---
## 六、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 路由守卫优化(核心) | `src/router/interceptor.ts`248 行7 种优化策略) |
| 路由守卫中的错误包装 | `src/utils/catch.ts`reqCatch 函数) |
| 仓库路径正则预校验 | `src/utils/regex.ts`repoPathSuffix |
| 路由懒加载配置 | `src/router/config/home.ts`40+ 路由)等 |
| API 缓存降级系统 | `src/utils/degradeInterceptor.ts`核心360 行) |
| 缓存存储层 | `src/utils/apiStorage.ts`IndexedDB 封装) |
| 缓存策略配置 | `src/api/storage-apis.ts`(哪些 API 走缓存) |
| 请求去重/Token 刷新 | `src/utils/request.ts`118-238 行) |
| 自定义曝光指令 | `src/directives/element-exposure.ts` |
| 响应式布局 Hook | `src/utils/hooks/usePageResize.ts` |
| 地图懒加载 | `src/utils/mapLoader.js` |
| Vite 构建配置 | `vite.config.ts` |
| 微前端路由同步 | `src/utils/microAppConfig.ts` |

View File

@@ -81,7 +81,8 @@ const dataSubMenus = [
];
const handleLogin = (type) => {
emitEvent('login', {type, triggerType: '主动注册'});
// emitEvent('login', {type, triggerType: '主动注册'});
window.open('http://222.20.126.217:2133/login', '_blank');
};
</script>

View File

@@ -58,7 +58,7 @@ const fetchStats = async() => {
const { developer, community, company, project } = businessData.data;
// 格式化数据显示
whDeveloper.value = `${formatNumber(developer)}+`;
whDeveloper.value = `${formatNumber(developer)}+`;
whCommunity.value = formatNumber(community);
whCompany.value = `${formatNumber(company)}+`;
whProject.value = `${formatNumber(project)}+`;

View File

@@ -311,12 +311,23 @@ import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize';
import StatsBar from './components/StatsBar.vue';
import HarmonyEcoCharts from './components/HarmonyEcoCharts.vue';
import { wuhanGithubProjectsPage3, cityProjectPage3, chinaExcellentProjectsPage3, excellentProjectsPage3, talentPage3 } from '@/api/jyh/situation';
import { wuhanGithubProjectsPage3, cityProjectPage3, chinaExcellentProjectsPage3, excellentProjectsPage3, talentPage3,
developerNamesPage3, communityRankChartPage3, wuhanCommunitiesPage3, ClubPage3, policyPage3, campusPage3, mirrorsPage3 } from '@/api/jyh/situation';
const { widthType } = usePageResize();
const githubProjects = ref([]);
const activeTab = ref(0);
const excellentProjects = ref([]);
const chinaExcellentProjects = ref([]);
const developerNames = ref([]);
const wuhanCommunities = ref([]);
// 武汉优质开源项目备用数据
const setgithubProjectsdata = () => {
githubProjects.value = [
@@ -451,11 +462,33 @@ const fetchTalentData = async() => {
}
};
const activeTab = ref(0);
// 城市社区数量排行备用数据
const setCommunityRankDefaultData = () => {
chartData.value.communityRank.xAxis = ['北京', '深圳', '上海', '杭州', '广州', '成都', '南京', '武汉'];
chartData.value.communityRank.data = [69, 35, 31, 25, 18, 15, 12, 12];
isEmpty.value.communityRank = false;
};
const excellentProjects = ref([]);
const chinaExcellentProjects = ref([]);
// 获取武汉社区数量排行数据
const fetchCommunityRankData = async() => {
try {
const response = await communityRankChartPage3();
const communitydata = response.data;
if (communitydata?.code === 200 && communitydata?.data) {
// 更新图表数据
chartData.value.communityRank.xAxis = communitydata.data.map(item => item.name);
chartData.value.communityRank.data = communitydata.data.map(item => item.value);
// 设置非空状态
isEmpty.value.communityRank = false;
} else {
console.error('获取城市社区数量排行数据失败:', communitydata?.msg || '未知错误');
setCommunityRankDefaultData();
}
} catch (error) {
console.error('请求城市社区数量排行数据时发生错误:', error);
setCommunityRankDefaultData();
}
};
// 2024武汉优秀开源项目备用数据
const chinaExcellentProjectsdata = () => {
@@ -561,27 +594,166 @@ const fetchexcellentProjects = async() => {
}
};
const wuhanCommunities = ref([
{ name: '黄鹤开源社区', type: '核心生态', slogan: '国家级、城市级开源社区平台,武汉开源生态的核心枢纽' },
{ name: '深度社区 (Deepin)', type: '操作系统', slogan: '全球最大桌面操作系统社区之一,全球用户超过 750 万' },
{
name: '极狐社区 (GitLab China)',
type: 'DevOps',
slogan: '全球第二大开源托管平台 GitLab 在汉设立的中国发行版及开发者社区'
},
{ name: '开源鸿蒙社区 (OpenHarmony)', type: '万物互联', slogan: '国家级生态创新中心,设有全国首个分委会' },
{ name: 'PostgreSQL 中文社区', type: '数据库', slogan: '全球四大开源数据库之一的运营中心设在武汉' },
{ name: '武汉大学开源社区', type: '学术社团', slogan: '老牌社团,涉及 Linux、嵌入式、深度学习等方向' },
{ name: '武汉大学 OpenHarmony 俱乐部', type: '万物互联', slogan: '专注于华为开源鸿蒙生态' },
{ name: '华中科技大学开源社团', type: '学术社团', slogan: '常年组织 LUG (Linux User Group) 及黑客松活动' },
{ name: '武汉理工大学开源联盟', type: '学术社团', slogan: '聚焦计算机科学与软件工程方向' },
{ name: '华中师范大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '中南民族大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '湖北大学', type: '兴趣小组', slogan: '开源技术兴趣小组' }
]);
// 武汉明星开发者贡献榜备用数据
const setDeveloperNamesDefaultData = () => {
developerNames.value = ['白纯骢', '陆红旭', '慕冬亮', '张继德', '蒋周奇', '张天雄', '阮文俊', '李奇', '陈鹏轩', '杨帆'];
};
// 开发者名单
const developerNames = ['白纯骢', '陆红旭', '慕冬亮', '张继德', '蒋周奇', '张天雄', '阮文俊', '李奇', '陈鹏轩', '杨帆'];
// 获取武汉明星开发者贡献榜数据
const fetchDeveloperNamesData = async() => {
try {
const response = await developerNamesPage3();
const developerdata = response.data;
if (developerdata?.code === 200 && developerdata?.data) {
// 更新开发者名单 - 后端返回字段为 name
developerNames.value = developerdata.data.map(item => item.name);
// 设置非空状态
isEmpty.value.developer = false;
} else {
console.error('获取武汉明星开发者贡献榜数据失败:', developerdata?.msg || '未知错误');
setDeveloperNamesDefaultData();
}
} catch (error) {
console.error('请求武汉明星开发者贡献榜数据时发生错误:', error);
setDeveloperNamesDefaultData();
}
};
// 武汉知名开源社区备用数据
const setwuhanCommunitiesdata = () => {
wuhanCommunities.value = [
{ name: '黄鹤开源社区', type: '核心生态', slogan: '国家级、城市级开源社区平台,武汉开源生态的核心枢纽' },
{ name: '深度社区 (Deepin)', type: '操作系统', slogan: '全球最大桌面操作系统社区之一,全球用户超过 750 万' },
{
name: '极狐社区 (GitLab China)',
type: 'DevOps',
slogan: '全球第二大开源托管平台 GitLab 在汉设立的中国发行版及开发者社区'
},
{ name: '开源鸿蒙社区 (OpenHarmony)', type: '万物互联', slogan: '国家级生态创新中心,设有全国首个分委会' },
{ name: 'PostgreSQL 中文社区', type: '数据库', slogan: '全球四大开源数据库之一的运营中心设在武汉' },
{ name: '武汉大学开源社区', type: '学术社团', slogan: '老牌社团,涉及 Linux、嵌入式、深度学习等方向' },
{ name: '武汉大学 OpenHarmony 俱乐部', type: '万物互联', slogan: '专注于华为开源鸿蒙生态' },
{ name: '华中科技大学开源社团', type: '学术社团', slogan: '常年组织 LUG (Linux User Group) 及黑客松活动' },
{ name: '武汉理工大学开源联盟', type: '学术社团', slogan: '聚焦计算机科学与软件工程方向' },
{ name: '华中师范大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '中南民族大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '湖北大学', type: '兴趣小组', slogan: '开源技术兴趣小组' }
];
};
const fetchwuhanCommunities = async() => {
try {
const response = await wuhanCommunitiesPage3();
const communitydata = response.data;
if (communitydata?.code === 200 && communitydata?.data) {
wuhanCommunities.value = communitydata.data;
} else {
console.error('获取武汉知名开源社区数据失败:', communitydata?.msg || '未知错误');
setwuhanCommunitiesdata();
}
} catch (error) {
console.error('请求武汉知名开源社区数据时发生错误:', error);
setwuhanCommunitiesdata();
}
};
// 重点城市高校开源社团数量备用数据
const setClubdata = () => {
chartData.value.club.xAxis = ['成都', '北京', '上海', '武汉', '重庆', '深圳', '苏州', '杭州', '合肥', '青岛'];
chartData.value.club.data = [10, 7, 5, 4, 3, 2, 2, 2, 2, 2];
isEmpty.value.club = false;
};
const fetchclubData = async() => {
try {
const response = await ClubPage3();
const clubdata = response.data;
if (clubdata?.code === 200 && clubdata?.data) {
chartData.value.club.xAxis = clubdata.data.map(item => item.name);
chartData.value.club.data = clubdata.data.map(item => item.number);
isEmpty.value.club = false;
} else {
console.error('获取重点城市高校开源社团数量数据失败:', clubdata?.msg || '未知错误');
setClubdata();
}
} catch (error) {
console.error('请求重点城市高校开源社团数量数据时发生错误:', error);
setClubdata();
}
};
// 各省市开源相关政策发布数量备用数据
const setPolicydata = () => {
chartData.value.policy.xAxis = ['江苏', '北京', '浙江', '广东', '上海', '湖北', '天津', '陕西', '河南', '云南'];
chartData.value.policy.data = [11, 7, 3, 3, 3, 2, 2, 2, 2, 2];
isEmpty.value.policy = false;
};
const fetchpolicyData = async() => {
try {
const response = await policyPage3();
const policydata = response.data;
if (policydata?.code === 200 && policydata?.data) {
chartData.value.policy.xAxis = policydata.data.map(item => item.name);
chartData.value.policy.data = policydata.data.map(item => item.number);
isEmpty.value.policy = false;
} else {
console.error('获取各省市开源相关政策发布数量数据失败:', policydata?.msg || '未知错误');
setPolicydata();
}
} catch (error) {
console.error('请求各省市开源相关政策发布数量数据时发生错误:', error);
setPolicydata();
}
};
// 开放原子校园行·城市活动场次备用数据
const setcampusdata = () => {
chartData.value.campus.xAxis = ['南京', '武汉', '北京', '长沙', '成都', '上海', '天津', '深圳', '济南', '广州'];
chartData.value.campus.data = [20, 16, 16, 16, 11, 10, 8, 8, 7, 6];
isEmpty.value.campus = false;
};
const fetchcampusData = async() => {
try {
const response = await campusPage3();
const campusdata = response.data;
if (campusdata?.code === 200 && campusdata?.data) {
chartData.value.campus.xAxis = campusdata.data.map(item => item.name);
chartData.value.campus.data = campusdata.data.map(item => item.number);
isEmpty.value.campus = false;
} else {
console.error('获取开放原子校园行·城市活动场次数据失败:', campusdata?.msg || '未知错误');
setcampusdata();
}
} catch (error) {
console.error('请求开放原子校园行·城市活动场次数据时发生错误:', error);
setcampusdata();
}
};
// 全国重点城市开源镜像站/节点数量对比备用数据
const setmirrorsdata = () => {
chartData.value.mirrors.xAxis = ['上海', '北京', '西安', '重庆', '济南', '南京', '武汉', '合肥', '香港', '长春', '成都', '深圳', '兰州', '哈尔滨', '南阳', '杭州'];
chartData.value.mirrors.data = [4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
isEmpty.value.mirrors = false;
};
const fetchmirrorsData = async() => {
try {
const response = await mirrorsPage3();
const mirrorsdata = response.data;
if (mirrorsdata?.code === 200 && mirrorsdata?.data) {
chartData.value.mirrors.xAxis = mirrorsdata.data.map(item => item.name);
chartData.value.mirrors.data = mirrorsdata.data.map(item => item.number);
isEmpty.value.mirrors = false;
} else {
console.error('获取全国重点城市开源镜像站/节点数量对比数据失败:', mirrorsdata?.msg || '未知错误');
setmirrorsdata();
}
} catch (error) {
console.error('请求全国重点城市开源镜像站/节点数量对比数据时发生错误:', error);
setmirrorsdata();
}
};
// 图表数据
const chartData = ref({
@@ -598,20 +770,20 @@ const chartData = ref({
data: [95, 92, 88, 76, 72]
},
club: {
xAxis: ['成都', '北京', '上海', '武汉', '重庆', '深圳', '苏州', '杭州', '合肥', '青岛'],
data: [10, 7, 5, 4, 3, 2, 2, 2, 2, 2]
xAxis: [],
data: []
},
policy: {
xAxis: ['江苏', '北京', '浙江', '广东', '上海', '湖北', '天津', '陕西', '河南', '云南'],
data: [11, 7, 3, 3, 3, 2, 2, 2, 2, 2]
xAxis: [],
data: []
},
campus: {
xAxis: ['南京', '武汉', '北京', '长沙', '成都', '上海', '天津', '深圳', '济南', '广州'],
data: [20, 16, 16, 16, 11, 10, 8, 8, 7, 6]
xAxis: [],
data: []
},
mirrors: {
xAxis: ['上海', '北京', '西安', '重庆', '济南', '南京', '武汉', '合肥', '香港', '长春', '成都', '深圳', '兰州', '哈尔滨', '南阳', '杭州'],
data: [4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
xAxis: [],
data: []
}
});
@@ -776,12 +948,14 @@ const initCommunityRankChart = () => {
grid: { left: '3%', right: '4%', bottom: '15px', top: '22%', containLabel: true },
xAxis: {
type: 'category',
data: ['北京', '深圳', '上海', '杭州', '广州', '成都', '南京', '武汉'],
data: chartData.value.communityRank.xAxis,
axisLabel: { color: '#666' }
},
yAxis: { type: 'value', name: '单位:个', nameTextStyle: { color: '#999' } },
yAxis: { type: 'value', name: '单位:个', nameTextStyle: { color: '#999' }},
series: [{
type: 'bar', data: [69, 35, 31, 25, 18, 15, 12, 12], barWidth: '40%',
type: 'bar',
data: chartData.value.communityRank.data,
barWidth: '40%',
itemStyle: { borderRadius: [4, 4, 0, 0], color: '#3b82f6' }
}]
});
@@ -795,8 +969,8 @@ const initClubChart = () => {
myChart.setOption({
tooltip: { trigger: 'axis', valueFormatter: (value: any) => value + ' 个' },
grid: commonGrid,
xAxis: { type: 'category', data: chartData.value.club.xAxis, axisLabel: { color: '#666' } },
yAxis: { type: 'value', name: '单位:个', splitLine: { lineStyle: { type: 'dashed', color: '#eee' } } },
xAxis: { type: 'category', data: chartData.value.club.xAxis, axisLabel: { color: '#666' }},
yAxis: { type: 'value', name: '单位:个', splitLine: { lineStyle: { type: 'dashed', color: '#eee' }}},
series: [{
type: 'line', data: chartData.value.club.data, smooth: true, symbolSize: 8,
lineStyle: { width: 3, color: '#F59E0B' },
@@ -897,11 +1071,18 @@ watch(widthType, () => {
});
onMounted(async() => {
await fetchGithubProjects();
await fetchGithubProjects(); // 获取武汉优质开源项目榜数据
await fetchCityProjectData(); // 获取城市项目数据
await fetchTalentData(); // 获取城市人才数据
await fetchDeveloperNamesData(); // 获取武汉明星开发者贡献榜数据
await fetchchinaExcellentProjects(); // 获取2024中国优秀开源项目数据
await fetchexcellentProjects(); // 获取2025武汉优秀开源项目数据
await fetchCommunityRankData(); // 获取城市社区数量排行数据
await fetchwuhanCommunities(); // 获取武汉知名开源社区数据
await fetchclubData(); // 获取重点城市高校开源社团数量数据
await fetchpolicyData(); // 获取各省市开源相关政策发布数量数据
await fetchcampusData(); // 获取开放原子校园行·城市活动场次数据
await fetchmirrorsData(); // 获取全国重点城市开源镜像站/节点数量对比数据
init(); // 再初始化图表
});

View File

@@ -740,8 +740,7 @@ const getRankClass = (rank: number) => {
};
const getRankText = (rank: number) => {
if (rank <= 3) return rank.toString();
return rank.toString().padStart(2, '0');
return rank.toString();
};
const getScoreClass = (score: number) => {

View File

@@ -36,10 +36,10 @@ import { overviewDataPage1 } from '@/api/jyh/situation';
// 定义响应式数据对象,初始值为 0 或 null
const stats = reactive({
enterprise: 0,
university: 0,
activity: 0,
project: 0
enterprise: 8000,
university: 600,
activity: 10000,
project: 4
});
// 加载状态(可选,用于显示 skeleton 或 loading

View File

@@ -2,7 +2,6 @@
# yarn lockfile v1
\
"@aashutoshrathi/word-wrap@^1.2.3":
version "1.2.6"
resolved "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz"
@@ -999,10 +998,10 @@
resolved "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz"
integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==
"@esbuild/win32-x64@0.18.20":
"@esbuild/darwin-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz"
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
@@ -1961,6 +1960,11 @@ buffer-from@^1.0.0:
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buildcheck@~0.0.6:
version "0.0.7"
resolved "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz"
integrity sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz"
@@ -2163,6 +2167,14 @@ core-js@^3.15.1, core-js@^3.31.1:
resolved "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz"
integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==
cpu-features@~0.0.10:
version "0.0.10"
resolved "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz"
integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==
dependencies:
buildcheck "~0.0.6"
nan "^2.19.0"
crc-32@~1.2.0, crc-32@~1.2.1:
version "1.2.2"
resolved "https://mirrors.cloud.tencent.com/npm/crc-32/-/crc-32-1.2.2.tgz"
@@ -3327,6 +3339,11 @@ fs.realpath@^1.0.0:
resolved "https://mirrors.cloud.tencent.com/npm/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@@ -4203,6 +4220,11 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nan@^2.19.0, nan@^2.23.0:
version "2.25.0"
resolved "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz"
integrity sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==
nanoid@^3.3.8:
version "3.3.11"
resolved "https://mirrors.cloud.tencent.com/npm/nanoid/-/nanoid-3.3.11.tgz"

View File

@@ -0,0 +1,244 @@
# 实习经历 — 整体概览
> 本文档是对三条实习经历的宏观梳理,后续将针对每条经历分别撰写详细的面试文档。
---
## 简历原文
> **实习经历:可信开源态势感知平台 — 前端开发实习生**
>
> 1. 参与开发全球风险监测模块实现威胁情报地图、CVE趋势分析、高危组件排行等10+数据可视化组件
> 2. 参与开发TcodeAI助手辅助修复功能集成金银湖大模型能力提供智能漏洞修复建议
> 3. 协作设计可信态势感知平台API接口完成50+数据接口的前后端集成
---
## 一、三条经历的关系
```
┌─────────────────────────────────────────────────────────────┐
│ 可信开源态势感知平台 │
│ │
│ ┌──────────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ 经历1风险监测模块 │ │经历2AI修复 │ │经历3API集成 │ │
│ │ 10+可视化组件 │ │金银湖大模型 │ │50+接口前后端 │ │
│ │ 威胁地图/趋势/排行 │ │智能修复建议 │ │请求基础设施 │ │
│ └────────┬─────────┘ └──────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 前端渲染层 AI 交互层 数据通信层 │
│ │
│ 三者共同构成平台的核心能力:数据展示 → 智能分析 → 数据获取 │
└─────────────────────────────────────────────────────────────┘
```
---
## 二、经历1全球风险监测模块
### 做了什么
参与开发了全球开源风险态势感知监控中心,路由为 `/security`,页面标题为"全球开源风险态势感知监控中心"。实现了 **10+ 个 ECharts 数据可视化组件**覆盖威胁情报地图、CVE 趋势分析、高危组件排行、漏洞分布、行业分布等多个维度。
### 涉及的组件
| 组件 | 图表类型 | 做什么 |
|------|---------|--------|
| **ThreatIntelMap** | 世界地图 + 散点 + 热力 | 全球威胁情报分布,含国家热力、关键节点、实时警报 |
| **ChinaSecurityMap** | 中国地图 + 攻击路径 | 中国安全态势,含省份热力、攻击路径动画 |
| **CveTrendChart** | 柱状图 + 折线图 | CVE 月度趋势,支持全球/区域切换 |
| **HighRiskComponentRank** | 横向柱状图 | 高危开源组件 Top 10按 CVSS 评分排序 |
| **VulnerabilitySeverityChart** | 堆叠柱状图 | 漏洞严重性分季度分布(高/中/低危) |
| **VulnerabilityLanguageChart** | 堆叠面积图 | 12 种编程语言的漏洞趋势2017-2024 |
| **ScatterChart** | 气泡散点图 | 编程语言安全态势,代码活跃度 vs 漏洞密度 |
| **IndustryDistribution** | 环形饼图 | 开源风险行业分布AI、云计算、大数据等 8 大行业) |
| **IntelligenceFeed** | 自动滚动列表 | 实时安全情报流,含类型标签和自动滚动 |
| **StatsGlass** | 毛玻璃统计卡片 | 4 项全球汇总指标(企业数、高校数、社区活跃度、项目数) |
### 技术要点
- 全部基于 **ECharts 6.x** 实现,使用 `echarts.init` + `dispose` 生命周期
- 世界地图和中国地图使用 **GeoJSON** 懒加载,几百 KB 的地图数据按需加载
- 图表组件通过 **Vite 动态 import** 按需加载,不打包进主 bundle
- 使用暗色主题(`#020617` 背景),带浮动粒子动画背景
- **13 个后端 API 接口**提供数据(`/api/v1/page1/*`),每个组件有硬编码的兜底数据
### 后续详细文档
> → [[经历1-全球风险监测模块详解]] ✅ 已完成
---
## 三、经历2TcodeAI助手辅助修复功能
### 做了什么
参与开发了 TcodeAI 助手的辅助修复功能,集成**金银湖大模型**能力,为开源组件的已知漏洞提供 **AI 生成的修复建议**。用户查看某个软件版本时,可以看到该版本依赖库中存在的漏洞,以及对每个漏洞的 AI 修复建议。
### 功能流程
```
用户进入软件版本详情页
切换到 "AI修复建议" Tab
POST /api/v1/repo/repair/info ──→ 返回漏洞列表库名、版本、CVE 等)
展示可折叠面板列表(每个受影响的库一个面板)
用户点击某个漏洞的 "AI修复建议" 按钮
POST /ai/vul_suggestion ──→ 返回 AI 生成的修复建议
弹窗展示:漏洞特性 + 漏洞描述 + 升级版本 + 修复建议列表
```
### 涉及的组件
| 组件 | 位置 | 做什么 |
|------|------|--------|
| **AIRepair 主页** | `src/views/Jyh/AIRepair/` | 主页面,展示漏洞列表 + AI 修复建议面板 |
| **Panel** | `src/components/AIRepair/` | 可折叠展开面板,带 `max-height` 动画过渡 |
| **Table** | `src/components/AIRepair/` | 漏洞表格 + "AI修复建议"按钮 + 弹窗展示 AI 建议 |
### 技术要点
- 对接**金银湖大模型**的 `/ai/vul_suggestion` 接口传入组件名、版本号、CVE 编号,返回结构化的修复建议(漏洞特性、描述、升级版本、修复建议列表)
- 修复建议以**结构化 JSON** 返回,前端用列表形式渲染
- Panel 组件使用了 `requestAnimationFrame` + `scrollHeight` 实现平滑折叠动画
- 该功能在 **3 个父页面**中以 Tab 形式嵌入版本详情页、AI Home 开源详情页、代码质量页)
### 后续详细文档
> → [[经历2-TcodeAI助手辅助修复详解]] ✅ 已完成
---
## 四、经历3API 接口前后端集成
### 做了什么
协作设计了可信态势感知平台的 API 接口,完成了 **50+ 数据接口**的前后端集成涵盖态势感知数据看板、仓库管理、组织管理、用户管理、AI 对话、合并请求、Issue 跟踪等多个业务模块。
### 整体规模
| 数据 | 数值 |
|------|------|
| API 函数总数 | **641 个** |
| API 文件数 | **29 个** |
| 金银湖JYH相关 API | **106 个**situation.ts 50 + index.ts 45 + home.ts 5 + auth.ts 3 + scanCenter.ts 3 |
| 态势感知看板接口 | **50+ 个**(三大页面,覆盖全球态势、开源生态、武汉本地生态) |
### 请求基础设施
```
请求发出
├── 请求拦截器
│ ├── Token 注入DP_TOKEN + DP_REFRESH_TOKEN
│ ├── 页面级 Header 附加标题、仓库ID、来源、UTM
│ └── URL 前缀重写passport /uc 前缀)
├── 数据传输
│ ├── AES-128-CBC 加密/解密(密钥 + IV 硬编码)
│ └── 标准 POST/GET部分接口使用 FormData 上传
└── 响应拦截器
├── 解密响应数据
├── HTTP 状态码处理400/401/403/408/500/502/504
├── Token 过期自动刷新 + 请求队列重放
└── API 缓存降级IndexedDB 缓存失败兜底)
```
### 关键模块
| 模块 | 文件 | 核心接口 |
|------|------|---------|
| **态势感知看板** | `jyh/situation.ts`50 端点) | 威胁地图、CVE 趋势、组件排行、漏洞分布、生态健康度等 |
| **仓库管理** | `repo/index.ts`161 端点) | 仓库 CRUD、分支/标签、Webhook、文件管理、Wiki、Star/Fork |
| **合并请求** | `merge/index.ts`62 端点) | MR 创建/审查、差异对比、代码评审、门禁检查 |
| **AI 大模型** | `jyh/index.ts`45 端点) | 对话创建、消息收发、历史管理、推荐问题、修复建议 |
| **组织管理** | `org/index.ts`55 端点) | 组织 CRUD、成员管理、主页信息、CLA 管理 |
| **用户管理** | `user/index.ts`55+ 端点) | 登录注册、OAuth、个人资料、SSH 密钥、Token 管理 |
| **讨论系统** | `discussion/index.ts`50 端点) | 讨论 CRUD、分类、评论、投票、排行榜 |
### 技术要点
- **AES 加密**:敏感数据在传输层使用 AES-128-CBC 加密(密钥 `mXzfCSPBKmEA8aLq`IV `tbeJJLC6dZQXXtWr`
- **Token 自动刷新**401 响应触发自动刷新,并发请求排队等待,刷新成功后统一重发
- **错误降级**`reqCatch` / `reqCatchV2` 包装函数API 失败时自动捕获错误并通过事件总线通知;部分接口有硬编码兜底数据
- **自定义错误处理**:支持 `customError: true` 参数跳过默认错误提示,由调用方自行处理
- **环境切换**:通过 Vite 环境变量区分开发/测试/生产环境 API 地址
### 后续详细文档
> → [[经历3-API接口前后端集成详解]] ✅ 已完成
---
## 五、三条经历的面试定位
| 经历 | 展示什么能力 | 面试重点 |
|------|------------|---------|
| **经历1风险监测** | 数据可视化能力、ECharts 实战、复杂图表组件开发 | ECharts 使用经验、地图图表、大数据图表性能 |
| **经历2AI修复** | AI 能力集成、前后端协作、组件化设计 | 大模型接入方式、结构化响应处理、组件复用 |
| **经历3API集成** | 工程化能力、安全设计、大规模接口管理 | 请求拦截器、加密方案、Token 刷新、错误处理 |
**一句话概括**
> "我在实习中从三个层面参与了平台建设——前端展示层数据可视化组件、AI 智能层大模型集成、数据通信层API 设计与集成),形成了比较完整的项目参与度。"
---
## 六、涉及的源码文件总览
### 经历1风险监测模块
| 做什么 | 文件在哪 |
|--------|----------|
| 态势感知主页面 | `src/views/Jyh/security/index.vue` |
| 全球威胁地图 | `src/views/Jyh/security/components/ThreatIntelMap.vue` |
| 中国安全地图 | `src/views/Jyh/security/components/ChinaSecurityMap.vue` |
| CVE 趋势图 | `src/views/Jyh/security/components/CveTrendChart.vue` |
| 高危组件排行 | `src/views/Jyh/security/components/HighRiskComponentRank.vue` |
| 漏洞严重性分布 | `src/views/Jyh/security/components/VulnerabilitySeverityChart.vue` |
| 语言漏洞趋势 | `src/views/Jyh/security/components/VulnerabilityLanguageChart.vue` |
| 语言安全散点图 | `src/views/Jyh/security/components/ScatterChart.vue` |
| 行业分布饼图 | `src/views/Jyh/security/components/IndustryDistribution.vue` |
| 情报动态流 | `src/views/Jyh/security/components/IntelligenceFeed.vue` |
| 统计卡片 | `src/views/Jyh/security/StatsGlass.vue` |
| 态势 API50 端点) | `src/api/jyh/situation.ts` |
| 威胁情报库 | `src/views/Jyh/ThreatIntelligence/index.vue` |
| 漏洞数据库 | `src/views/Jyh/VulnerabilityDatabase/index.vue` |
### 经历2AI 修复功能
| 做什么 | 文件在哪 |
|--------|----------|
| AI 修复主页面 | `src/views/Jyh/AIRepair/index.vue` |
| 可折叠面板组件 | `src/components/AIRepair/Panel.vue` |
| 漏洞表格 + AI 建议弹窗 | `src/components/AIRepair/Table.vue` |
| AI Home 中的修复 | `src/views/Jyh/AI/Home/components/OssDetail/AIRepair/index.vue` |
| AI 修复 API | `src/api/jyh/index.ts`remediationSearchRemediations, getVulSuggestion |
| 版本详情(嵌入位置) | `src/views/Jyh/Version/Detail/index.vue` |
### 经历3API 集成
| 做什么 | 文件在哪 |
|--------|----------|
| 请求核心基础设施 | `src/utils/request.ts`Axios 实例、拦截器、加密、Token 刷新) |
| 态势感知看板 API | `src/api/jyh/situation.ts`50 端点) |
| 金银湖核心 API | `src/api/jyh/index.ts`45 端点) |
| 仓库管理 API | `src/api/repo/index.ts`161 端点) |
| 组织管理 API | `src/api/org/index.ts`55 端点) |
| 用户管理 API | `src/api/user/index.ts`55+ 端点) |
| 错误捕获包装 | `src/utils/catch.ts`reqCatch / reqCatchV2 |
| 状态码处理 | `src/utils/status.ts`dealWarning |
| 缓存降级系统 | `src/utils/degradeInterceptor.ts` |
| 加密配置 | `src/utils/request.ts`AES-CBC 加密) |

View File

@@ -0,0 +1,380 @@
# 全球风险监测模块 — 面试版
> 简历原话:**"参与开发全球风险监测模块实现威胁情报地图、CVE趋势分析、高危组件排行等10+数据可视化组件"**
>
> 这篇文档帮你理解这个模块到底做了什么、怎么做的,以及面试时怎么讲。
---
## 一、先搞清楚:这个模块是什么?
全球风险监测模块是一个完整的数据可视化大屏,路由为 `/security`,页面标题为"全球开源风险态势感知监控中心"。页面上展示了 **10+ 个 ECharts 图表组件**,从全球威胁地图到高危组件排行、从 CVE 趋势到编程语言安全态势,覆盖了威胁情报的多个维度。
**一句话概括**:我用 ECharts 6.x 实现了一个安全态势数据可视化大屏,包含 10+ 个不同类型的图表组件(地图、柱状图、折线图、饼图、面积图、散点图等),展示了全球开源威胁情报的多个维度。
---
## 二、页面长什么样?(整体布局)
```
┌─────────────────────────────────────────────────────────────┐
│ StatsGlass统计卡片4 项全球汇总指标) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 🛡️ ThreatIntelMap全球威胁情报地图 │ │
│ │ 世界地图热力 + 关键节点散点 + 实时警报 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │IndustryDist │ │HighRisk │ │Intelligence │ │
│ │行业分布饼图 │ │高危组件Top10 │ │实时情报动态流 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ CveTrendChartCVE 月度趋势 柱状图+折线图) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ VulnerabilityLanguageChart12种语言漏洞趋势面积图 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ VulnerabilitySeverityChart漏洞等级堆叠柱状图 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ ScatterChart编程语言安全态势气泡散点图 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
页面使用暗色科技风背景(`#020617`),每个图表卡片有毛玻璃效果(`backdrop-filter: blur`),鼠标悬停有上浮动画。
---
## 三、10+ 个组件一览
| 组件 | 图表类型 | 数据来源 | 直观展示什么 |
|------|---------|---------|------------|
| **ThreatIntelMap** | 世界地图 + 热力 + 散点 | `/api/v1/page1/map/mapData` 等 4 个接口 | 全球威胁分布热力图,北京/上海/华盛顿等 6 个关键节点,实时漏洞类型分布面板 |
| **ChinaSecurityMap** | 中国地图 + 攻击路径 | 懒加载中国 GeoJSON | 各省威胁热力、安全监测节点、城市间攻击路径动画 |
| **CveTrendChart** | 柱状图 + 折线图(双轴) | `/api/v1/page1/cveData/global` | CVE 数量柱状图 + 环比增长率折线图,支持全球/区域切换 |
| **HighRiskComponentRank** | 横向柱状图 | `/api/v1/page1/highRiskComponent` | Top 10 高危开源组件,按 CVSS 评分排序,颜色标识严重程度 |
| **VulnerabilitySeverityChart** | 堆叠柱状图 | `/api/v1/page1/vulnerabilitySeverity` | 高/中/低危漏洞数量按季度堆叠分布2024Q1-2025Q4 |
| **VulnerabilityLanguageChart** | 堆叠面积图 | `/api/v1/page1/languageTrend` | 12 种编程语言 2017-2024 年的漏洞趋势变化 |
| **ScatterChart** | 气泡散点图 | `/api/v1/page1/scatterData` | 编程语言安全态势X 轴代码活跃度、Y 轴漏洞密度、气泡大小=漏洞总数 |
| **IndustryDistribution** | 环形饼图 | `/api/v1/page1/industryDistribution` | 开源风险在 AI、云计算、大数据等 8 大行业的分布占比 |
| **IntelligenceFeed** | 自动滚动列表 | `/api/v1/page1/feedList` | 实时安全情报流带类型标签CISA/Poisoning/CNVD/GitHub |
| **StatsGlass** | 毛玻璃统计卡片 | `/api/v1/page1/overviewData` | 4 项全球汇总指标(企业数、高校数、社区活跃度、项目数) |
除了以上 10 个独立组件,主页面 `index.vue` 中还有 **5 个内联 ECharts 图表**
| 内联图表 | 图表类型 | 展示内容 |
|---------|---------|---------|
| 高校开源参与分布 | 环形饼图 | 华中科大、武大等高校参与度 |
| 社区区域分布 | 柱状图 | 武昌区、洪山区等社区分布 |
| OSS Compass 排行 | 横向柱状图 | OpenHarmony、MindSpore 等开源项目排行 |
| 开发者排行榜 | 横向柱状图 | 武汉开源开发者 Top 10 |
| 语言技术趋势 | 多系列折线图 | Java、Python、Rust、仓颉等 5 种语言的趋势 |
**合计15+ 个图表组件。**
---
## 四、怎么实现的?(面试核心)
所有图表组件遵循统一的 **三步模式**:获取数据 → 初始化图表 → 清理销毁。
### 4.1 统一的开发模式(每个组件都一样)
```typescript
// 以 HighRiskComponentRank 为例
import * as echarts from 'echarts';
import { highRiskComponentPage1 } from '@/api/jyh/situation';
// ====== 步骤 1获取数据 ======
const fetchData = async () => {
try {
const response = await highRiskComponentPage1();
if (response.data.code === 200) {
componentData.value = response.data.data; // ✅ 成功:用后端数据
} else {
componentData.value = [...defaultData]; // ⚠️ 失败:用兜底数据
}
} catch (error) {
componentData.value = [...defaultData]; // ❌ 异常:用兜底数据
}
};
// ====== 步骤 2初始化图表 ======
const initChart = () => {
if (chartInstance) chartInstance.dispose(); // 先销毁旧实例
chartInstance = echarts.init(chartRef.value); // 创建新实例
chartInstance.setOption({
// ... 图表配置tooltip、grid、xAxis、yAxis、series 等)
});
window.addEventListener('resize', handleResize); // 响应窗口大小变化
};
// ====== 步骤 3清理销毁 ======
onUnmounted(() => {
if (chartInstance) chartInstance.dispose(); // 组件卸载时销毁,防止内存泄漏
window.removeEventListener('resize', handleResize);
});
```
**面试话术**
> "所有图表组件遵循统一的开发模式:第一,通过 API 获取数据,接口失败时自动降级为预设的兜底数据,保证图表不会白屏;第二,使用 echarts.init 初始化图表,通过 setOption 配置图表样式和交互;第三,组件卸载时调用 dispose 销毁实例,移除 resize 监听,防止内存泄漏。切换数据时会先 dispose 旧实例再重新 init确保图表完全刷新。"
### 4.2 全球威胁情报地图(技术含量最高)
ThreatIntelMap 是整个模块最复杂的组件,综合使用了多种 ECharts 地图能力:
```typescript
// ThreatIntelMap.vue — 核心配置(简化版)
echarts.registerMap('world', worldJson); // 注册世界地图 GeoJSON
const option = {
// 1. 视觉映射:热力渐变
visualMap: {
min: 0, max: 100,
inRange: { color: ['#1e3c72', '#2a5298', '#24b6d8', '#facc15', '#ef4444'] }
},
// 2. 地理坐标系:世界地图
geo: {
map: 'world', roam: true, zoom: 1.2,
itemStyle: { areaColor: '#0f172a', borderColor: '#1e293b' }
},
// 3. 三个数据系列
series: [
{
name: 'Threat Distribution',
type: 'map', // ← 地图热力层
geoIndex: 0,
data: mapData // 国家→风险值
},
{
name: 'Key Nodes',
type: 'effectScatter', // ← 涟漪散点层(关键安全节点)
coordinateSystem: 'geo',
data: scatterData, // 北京、上海、华盛顿等 6 个节点
rippleEffect: { scale: 3 }
},
]
};
```
同时在地图叠加层渲染了三个信息面板:
- **左侧面板**:漏洞类型分布 Top 5XSS 32.1%、SQL 注入 11.9% 等),带进度条动画
- **右侧面板**:威胁情报地域排行(美国 435,202 次、印度 252,054 次等)
- **底部卡片**实时安全警报自动轮播3.5 秒切换一条,带脉冲动画)
**面试话术**
> "全球威胁地图是整个模块最复杂的组件。它用到了 ECharts 的地图系列 + effectScatter 涟漪散点图的组合,在 GeoJSON 世界地图上叠加了两层数据——国家热力层展示威胁分布,散点层标记 6 个关键安全节点。地图上还有三个信息面板——漏洞类型分布、地域排行、实时警报。世界地图的 GeoJSON 文件通过动态 import 懒加载,只在用户进入这个页面时才下载,不打包进主 bundle。"
### 4.3 CVE 月度趋势 — 双轴组合图
```typescript
// CveTrendChart.vue — 双 Y 轴组合图(柱状图 + 折线图)
const option = {
tooltip: { trigger: 'axis' },
// 两个 Y 轴:左边 CVE 数量,右边增长率百分比
yAxis: [
{ type: 'value', name: 'CVE数量', position: 'left' }, // ← 对应柱状图
{ type: 'value', name: '环比增长率(%)', position: 'right' } // ← 对应折线图
],
series: [
{
name: 'CVE数量',
type: 'bar', // ← 柱状图,左轴
data: cveCount,
itemStyle: { color: new echarts.graphic.LinearGradient(...) } // 渐变色
},
{
name: '环比增长率',
type: 'line', // ← 折线图,右轴
yAxisIndex: 1, // 使用右轴
data: acceleration,
smooth: true, // 平滑曲线
itemStyle: { color: '#ef4444' } // 红色
}
]
};
```
支持通过按钮切换"全球视图"和"区域视图",切换时重新请求对应的 API 接口并重新渲染图表。
### 4.4 高危组件 Top 10 — 横向柱状图 + CVSS 颜色标识
```typescript
// HighRiskComponentRank.vue — 颜色随 CVSS 评分变化
series: [{
type: 'bar',
data: cvssScores,
itemStyle: {
color: (params) => {
if (params.value >= 9.0) return '#ef4444'; // 红色 — 严重
if (params.value >= 7.0) return '#f97316'; // 橙色 — 高危
if (params.value >= 4.0) return '#eab308'; // 黄色 — 中危
return '#22c55e'; // 绿色 — 低危
}
},
label: { show: true, position: 'right' } // 数值标签显示在柱子右侧
}]
```
Y 轴设置 `inverse: true`,让 CVSS 评分最高的组件显示在最上面。
### 4.5 漏洞等级分布 — 堆叠柱状图
```typescript
// VulnerabilitySeverityChart.vue — 三个系列堆叠
series: [
{ name: '高危漏洞', type: 'bar', stack: 'total', color: '#FF4D4F', data: [...] },
{ name: '中危漏洞', type: 'bar', stack: 'total', color: '#FAAD14', data: [...] },
{ name: '低危漏洞', type: 'bar', stack: 'total', color: '#36CFC9', data: [...] },
]
// 三个系列的 stack 值都是 'total'ECharts 会自动堆叠
// tooltip 中计算每个等级占总数的百分比
```
### 4.6 三个重要的设计细节
#### 细节 1每个组件都有 API 失败兜底
```typescript
// 所有图表组件的共同特点API 失败时用硬编码数据兜底
try {
const res = await apiFunction();
if (res.data.code === 200) {
chartData.value = res.data.data; // ✅ 后端数据
} else {
chartData.value = DEFAULT_DATA; // ⚠️ 兜底数据
}
} catch {
chartData.value = DEFAULT_DATA; // ❌ 网络异常,兜底数据
}
```
**为什么这样设计?** 态势大屏在演示时不能白屏。即使后端挂了,至少展示兜底数据让页面是完整的。
#### 细节 2图表实例的 dispose → init 生命周期
```typescript
// 切换数据时:先销毁旧实例,再创建新实例
if (chartInstance) chartInstance.dispose(); // 销毁旧实例(释放内存)
chartInstance = echarts.init(el); // 创建新实例
chartInstance.setOption(option); // 渲染新数据
// 组件卸载时:销毁实例 + 移除监听
onUnmounted(() => {
chartInstance?.dispose(); // 防止内存泄漏
window.removeEventListener('resize', handler);
});
```
**为什么不用 myChart.setOption() 更新?** 当数据维度变化时(如 CVE 趋势图切换全球/区域视图、X 轴月份数变化),直接 setOption 会有残留配置。先 dispose 再 init 是最稳妥的做法。
#### 细节 3地图数据的懒加载
```typescript
// 世界地图 GeoJSON 通过 import 在组件内引用
import worldJson from './world.json'; // 只在 ThreatIntelMap 组件被加载时才下载
// 中国地图 GeoJSON 通过动态函数加载utils/mapLoader.js
export const loadChinaMap = async () => {
await import('/china.js'); // 只在进入中国地图页面时才下载(几百 KB
};
```
---
## 五、完整的数据流
```
页面加载 (/security)
├──→ StatsGlass 调 overviewDataPage1 ← /api/v1/page1/overviewData
├──→ ThreatIntelMap 调 4 个 map API ← /api/v1/page1/map/*
├──→ IndustryDistribution 调 industryDistributionPage1 ← /api/v1/page1/industryDistribution
├──→ HighRiskComponentRank 调 highRiskComponentPage1 ← /api/v1/page1/highRiskComponent
├──→ IntelligenceFeed 调 feedListPage1 ← /api/v1/page1/feedList
├──→ CveTrendChart 调 cveDataGlobal + regional ← /api/v1/page1/cveData/*
├──→ VulnerabilityLang 调 languageTrendPage1 ← /api/v1/page1/languageTrend
├──→ VulnerabilitySev 调 vulnerabilitySeverityPage1 ← /api/v1/page1/vulnerabilitySeverity
├──→ ScatterChart 调 scatterDataPage1 ← /api/v1/page1/scatterData
└──→ 5 个内联图表 硬编码数据(武汉市开源生态展示)
```
**共 13 个后端 API 接口**,全部以 `/api/v1/page1/` 为前缀,定义在 `src/api/jyh/situation.ts` 中。
---
## 六、面试问答准备
### Q1"参与开发全球风险监测模块"具体做了什么?
> 我参与开发了全球开源风险态势感知监控中心这个大屏页面,用 ECharts 6.x 实现了 10+ 个数据可视化组件。具体包括:全球威胁情报地图(世界地图热力 + 关键节点散点 + 实时警报面板、CVE 月度趋势图(双轴柱状图+折线图,支持全球/区域切换)、高危组件 Top 10 排行(横向柱状图,按 CVSS 评分颜色标识)、漏洞等级分布(高/中/低危堆叠柱状图、编程语言漏洞趋势面积图12 种语言 2017-2024、编程语言安全态势气泡图活跃度 vs 漏洞密度)等。页面采用暗色科技风主题,毛玻璃卡片布局,每个组件都有 API 失败兜底数据。
### Q210+ 个图表组件,代码怎么组织的?
> 每个图表都是一个独立的 Vue SFC 组件(如 ThreatIntelMap.vue、CveTrendChart.vue在主页面 index.vue 中通过 import 引入并组合布局。所有组件遵循统一的开发模式:获取数据(调 API失败用兜底数据→ 初始化图表echarts.init + setOption→ 清理销毁dispose + 移除监听)。组件之间互不依赖,可以独立开发和调试。
### Q3地图是怎么做的
> 我用 ECharts 的 map 系列实现。世界地图使用 GeoJSON 数据world.json通过 echarts.registerMap 注册后在地理坐标系上渲染。地图上叠加了两层数据map 系列展示国家热力分布(通过 visualMap 控制颜色渐变effectScatter 系列展示关键安全节点(北京、上海、华盛顿等 6 个点,带涟漪动画)。还在地图四个角叠加了信息面板——漏洞类型分布、地域排行、实时警报。
### Q4图表性能怎么考虑的
> 三个方面的处理:第一,地图 GeoJSON 文件(几百 KB使用懒加载只在 ThreatIntelMap 组件渲染时才下载;第二,每个图表组件在卸载时调用 echartsInstance.dispose() 销毁实例并移除 resize 监听,防止内存泄漏;第三,图表切换数据时(如 CVE 全球/区域切换),先 dispose 再重新 init避免 setOption 合并残留配置导致的问题。
### Q5如果后端 API 挂了,页面会白屏吗?
> 不会。每个图表组件都设计了数据降级策略——API 请求失败或返回异常时,自动使用预设的兜底数据渲染。比如高危组件 Top 10 的兜底数据包含了 Log4j29.8、Struts29.7、OpenSSL9.6)等真实的公开漏洞数据。这样即使后端挂了,大屏依然能完整展示,不影响演示效果。
### Q6ECharts 的 setOption 和 dispose+init 有什么区别?为什么你的组件用 dispose+init
> setOption 适合增量更新——数据变了但图表类型、轴配置不变时,用 setOption 合并新配置即可,不会闪烁。但我的组件有些场景下图表维度会变化(比如 CVE 趋势图切换全球/区域时,数据量和走势完全不同),直接 setOption 可能残留旧配置。所以统一采用 dispose + init 的方式,确保每次都是全新渲染。代价是有短暂闪烁,但数据一致性能保证。
### Q7你用到了哪些 ECharts 图表类型?
> 地图map、涟漪散点图effectScatter、柱状图bar含堆叠柱状图、折线图line、饼图pie/环形图、面积图area/堆叠面积图)、散点图/气泡图scatter、热力 visualMap。总共大概 7-8 种图表类型的组合使用。
---
## 七、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 独立图表组件数 | 10 个import 引入的子组件) |
| 内联图表数 | 5 个index.vue 中直接写的) |
| 图表总数 | **15+** 个 |
| ECharts 图表类型 | **8 种**map、effectScatter、bar、line、pie、area、scatter、visualMap |
| 后端 API 接口 | **13 个**`/api/v1/page1/*` |
| API 接口文件 | `src/api/jyh/situation.ts`50 个端点,其中 13 个属于此模块) |
| 地图 GeoJSON 文件 | 2 个world.json + china.js |
| 暗色主题背景色 | `#020617` |
| 毛玻璃卡片效果 | `backdrop-filter: blur(10px)` |
---
## 八、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 主页面(布局 + 5 个内联图表) | `src/views/Jyh/security/index.vue` |
| 全球威胁情报地图 | `src/views/Jyh/security/components/ThreatIntelMap.vue` |
| 中国安全态势地图 | `src/views/Jyh/security/components/ChinaSecurityMap.vue` |
| CVE 月度趋势图 | `src/views/Jyh/security/components/CveTrendChart.vue` |
| 高危组件 Top 10 排行 | `src/views/Jyh/security/components/HighRiskComponentRank.vue` |
| 漏洞等级分布图 | `src/views/Jyh/security/components/VulnerabilitySeverityChart.vue` |
| 编程语言漏洞趋势图 | `src/views/Jyh/security/components/VulnerabilityLanguageChart.vue` |
| 编程语言安全态势气泡图 | `src/views/Jyh/security/components/ScatterChart.vue` |
| 行业分布饼图 | `src/views/Jyh/security/components/IndustryDistribution.vue` |
| 实时情报动态流 | `src/views/Jyh/security/components/IntelligenceFeed.vue` |
| 统计卡片 | `src/views/Jyh/security/StatsGlass.vue` |
| 世界地图 GeoJSON | `src/views/Jyh/security/components/world.json` |
| 所有态势感知 API | `src/api/jyh/situation.ts` |
| 关联页面:威胁情报库 | `src/views/Jyh/ThreatIntelligence/index.vue` |
| 关联页面:漏洞数据库 | `src/views/Jyh/VulnerabilityDatabase/index.vue` |

View File

@@ -0,0 +1,314 @@
# TcodeAI 助手辅助修复功能 — 面试版
> 简历原话:**"参与开发TcodeAI助手辅助修复功能集成金银湖大模型能力提供智能漏洞修复建议"**
>
> 这篇文档帮你理解这个功能到底做了什么、怎么做的,以及面试时怎么讲。
---
## 一、先搞清楚:这个功能是什么?
在可信态势感知平台中用户查看某个软件版本时平台会扫描该版本依赖的开源组件列出其中存在的已知漏洞。TcodeAI 助手辅助修复功能的作用就是——**对每个漏洞生成一份 AI 修复建议**,告诉用户这个漏洞有什么特征、怎么描述、建议升级到什么版本、具体怎么修。
**一句话概括**:我给平台接入了金银湖大模型的智能修复能力,用户点一下"AI修复建议"按钮,就能看到模型生成的漏洞分析 + 升级方案 + 修复步骤。
---
## 二、功能长什么样?(用户视角)
```
用户进入软件版本详情页
切换到 "AI修复建议" Tab
┌─────────────────────────────────────────────────────────────┐
│ AI修复建议说明功能介绍横幅
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 🔴 严重 Log4j2 [展开 ▼] │ │
│ │ 安全问题 3 升级版本 2.14.0 → 2.17.1 │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ (展开后显示漏洞列表表格) │ │
│ │ ┌──────────┬──────────┬──────────┬──────┬──────────┐ │ │
│ │ │ 问题编号 │ 问题类型 │ CWE编号 │ 分值 │ 操作 │ │ │
│ │ ├──────────┼──────────┼──────────┼──────┼──────────┤ │ │
│ │ │CVE-2021..│ RCE │ CWE-502 │ 9.8 │[AI修复] │ │ │
│ │ │CVE-2021..│ DoS │ CWE-400 │ 7.5 │[AI修复] │ │ │
│ │ └──────────┴──────────┴──────────┴──────┴──────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 🔴 严重 Spring Framework [展开 ▼] │ │
│ │ 安全问题 2 升级版本 5.3.26 → 5.3.31 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ (点击 "AI修复建议" 按钮后弹出弹窗) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ AI修复建议 [弹窗] │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 漏洞特性:远程代码执行,攻击者可构造恶意数据包... │ │ │
│ │ │ 漏洞描述Apache Log4j2 存在 JNDI 注入漏洞... │ │ │
│ │ │ 升级版本2.17.1 │ │ │
│ │ │ 🤖 AI修复建议 │ │ │
│ │ │ • 将 Log4j2 版本升级至 2.17.1 或更高版本 │ │ │
│ │ │ • 若无法升级,设置 log4j2.formatMsgNoLookups=true│ │ │
│ │ │ • 检查项目中是否有自定义的 Lookup 插件... │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ [确定] [取消] │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 三、怎么实现的?(面试核心)
### 3.1 三个组件的协作关系
这个功能由三个组件构成:
```
AIRepair/index.vue主页面
├──→ 调用 /api/v1/repo/repair/info 获取漏洞数据
├──→ Panel.vue可折叠面板组件
│ 每个受影响的库一个面板,展示库名、版本、升级路径
│ 点击 "展开/收起" 控制面板折叠
└──→ Table.vue漏洞表格 + AI 建议弹窗)
展示该库下的漏洞列表CVE编号、类型、CVSS评分
点击 "AI修复建议" → 调用 /ai/vul_suggestion → 弹窗展示 AI 建议
```
### 3.2 第一步获取漏洞列表AIRepair/index.vue
```typescript
// views/Jyh/AIRepair/index.vue简化版
const tableData = ref([]);
const pager = ref({ total: 10, pageIndex: 1, pageSize: 10 });
// 页面加载时调后端 API 获取受漏洞影响的组件列表
const fetchVulnList = async () => {
const { data } = await remediationSearchRemediations({
softwareId: propsData.versionId, // 当前查看的软件版本 ID
current: pager.value.pageIndex,
size: pager.value.pageSize
});
if (data.data.code === 200) {
// 数据格式:每个元素是一个受影响的库
// { libraryName, currentVersion, recommendedVersion, vulnerabilities: [...] }
tableData.value = data?.data?.data?.records || [];
pager.value.total = data?.data?.data?.total || 0;
}
};
onMounted(() => { fetchVulnList(); });
```
**API 接口**`POST /api/v1/repo/repair/info`,参数 `{ softwareId, current, size }`
### 3.3 第二步可折叠面板Panel.vue
```vue
<!-- components/AIRepair/Panel.vue 可折叠面板 -->
<template>
<!-- 头部始终可见 -->
<div>
<slot name="header"></slot> <!-- 库名版本信息展开/收起按钮 -->
</div>
<!-- 内容展开/收起动画过渡 -->
<Transition name="collapse-transition"
@beforeEnter="beforeEnter"
@enter="enter"
@beforeLeave="beforeLeave"
@leave="leave"
>
<div v-if="props.expand">
<slot name="body"></slot> <!-- 漏洞表格 -->
</div>
</Transition>
</template>
```
**折叠动画是怎么实现的?** 手动控制 Vue Transition 的六个钩子函数:
```typescript
// 展开时先量高度scrollHeight→ 设置 max-height → 过渡到全高 → 清除限制
const enter = (el) => {
requestAnimationFrame(() => {
el.style.maxHeight = `${el.scrollHeight}px`; // 设为元素实际高度
});
};
const afterEnter = (el) => {
el.style.maxHeight = ''; // 展开完成后清除限制,防止内容溢出
};
// 收起时:先设当前高度 → 过渡回 max-height: 0
const beforeLeave = (el) => {
el.style.maxHeight = `${el.scrollHeight}px`; // 锁定当前高度
};
const leave = (el) => {
el.style.maxHeight = 0; // 过渡回 0
};
```
**为什么不用 CSS `height: auto` 动画?** CSS 无法对 `height: auto` 做动画过渡。所以用 JS 读取 `scrollHeight` 再设置 `max-height` 的数值,这才能实现平滑的展开/收起动画。过渡曲线是 `cubic-bezier(0.5, 0.05, 0.5, 0.95)`,持续 0.3 秒。
### 3.4 第三步AI 修复建议弹窗Table.vue — 核心交互)
这是整个功能的核心——用户点击漏洞行里的"AI修复建议"按钮,弹出弹窗展示 AI 生成的内容:
```javascript
// components/AIRepair/Table.vue简化版核心逻辑
// 1. 表格渲染每行漏洞信息 + "AI修复建议"按钮
// <d-button @click="openModal(row)">AI修复建议</d-button>
// 2. 点击按钮 → 调 AI 接口获取建议
const openModal = async (row) => {
showModal.value = true;
await fetchVulSuggestion(row); // 调 AI 接口
};
const fetchVulSuggestion = async (row) => {
loading.value = true;
const { data } = await getVulSuggestion({
name: props.libraryName, // 组件库名(如 "Log4j2"
version: props.currentVersion, // 当前版本号(如 "2.14.0"
cve: row.vulnId // CVE 编号(如 "CVE-2021-44228"
});
if (data.code === 200) {
vulSuggestion.value = {
vulFeature: data.data?.vulFeature || '', // 漏洞特性
vulDescription: data.data?.vulDescription || '', // 漏洞描述
upgradeVersion: data.data?.upgradeVersion || '', // 建议升级版本
fixSuggestionList: data.data?.fixSuggestionList || [] // AI修复建议列表
};
}
};
// 3. 弹窗展示 AI 返回的四部分内容
// ┌──────────────────────────────────────────┐
// │ 漏洞特性:远程代码执行,攻击者构造恶意... │
// │ 漏洞描述Apache Log4j2 存在 JNDI 注入... │
// │ 升级版本2.17.1 │
// │ 🤖 AI修复建议
// │ • 升级至 2.17.1 或更高版本 │
// │ • 设置 formatMsgNoLookups=true │
// │ • 检查自定义 Lookup 插件 │
// └──────────────────────────────────────────┘
```
**API 接口**`POST /ai/vul_suggestion`,参数 `{ name, version, cve }`,返回结构化 JSON。
### 3.5 数据流全景
```
用户进入 "AI修复建议" Tab
AIRepair/index.vue 调用
POST /api/v1/repo/repair/info
参数: { softwareId, current, size }
返回: { records: [{ libraryName, currentVersion,
recommendedVersion, vulnerabilities: [...] }] }
渲染可折叠面板Panel.vue
每个受影响的库 → 一个面板
面板头部展示:库名、严重标签、问题数、升级路径
面板内容展示漏洞表格Table.vue
用户点击某行 "AI修复建议" 按钮
Table.vue 调用
POST /ai/vul_suggestion ← 金银湖大模型
参数: { name: "Log4j2", version: "2.14.0", cve: "CVE-2021-44228" }
返回: { vulFeature, vulDescription,
upgradeVersion, fixSuggestionList: [...] }
弹窗展示 AI 修复建议
```
---
## 四、这个功能还在哪些地方用?
AIRepair 组件被复用在 **3 个不同的父页面**中,以 Tab 形式嵌入:
| 父页面 | 文件位置 | Tab 名称 |
|--------|---------|---------|
| **版本详情页** | `src/views/Jyh/Version/Detail/index.vue` | "AI修复建议" Tab |
| **版本详情页(备选视图)** | `src/views/Jyh/Version/Detail/repoDetails/index2.vue` | "AI修复建议" Tab |
| **AI Home 开源详情** | `src/views/Jyh/AI/Home/components/OssDetail/index.vue` | "AI 修复建议" Tab |
此外Panel 和 Table 组件还被复用在**代码质量页**`CodeQuality/index.vue`)中,说明这套 UI 模式在项目中有一定的通用性。
---
## 五、面试问答准备
### Q1"TcodeAI助手辅助修复功能"具体做了什么?
> 这个功能主要是把金银湖大模型的智能分析能力集成到平台的漏洞修复流程里。用户查看某个软件版本时,平台会列出该版本依赖库中存在的已知漏洞。对于每个漏洞,用户可以点击"AI修复建议"按钮,前端会调用金银湖大模型的 `/ai/vul_suggestion` 接口传入组件名、版本号、CVE 编号,模型会返回结构化的修复建议——包括漏洞特性、漏洞描述、建议升级版本和修复步骤列表。前端把这些内容展示在弹窗中,用户就能看到具体的修复方案。
### Q2AI 接口是怎么对接的?
> 用的是标准的 HTTP POST 请求。前端传三个参数给后端 `/ai/vul_suggestion` 接口:`name`(库名,如 Log4j2、`version`(当前版本号)、`cve`CVE 编号)。后端调用金银湖大模型进行处理,返回结构化 JSON包含 `vulFeature`(漏洞特性)、`vulDescription`(漏洞描述)、`upgradeVersion`(建议升级版本)、`fixSuggestionList`(修复建议列表)。前端直接按字段渲染就行,不需要自己解析自然语言。
### Q3Panel 组件的折叠动画怎么实现的?
> 用 Vue 的 Transition 组件 + JavaScript 钩子函数实现的。核心思路是:因为 CSS 无法对 `height: auto` 做过渡动画,所以用 JS 在展开前读取元素的 `scrollHeight`(实际内容高度),设置 `max-height` 为目标值,让 CSS transition 驱动动画;展开完成后清掉 `max-height` 限制,防止内容溢出。收起的流程相反——先设 `max-height` 为当前高度,再过渡到 0。过渡持续 0.3 秒,使用 `cubic-bezier(0.5, 0.05, 0.5, 0.95)` 缓动曲线。
### Q4这个功能是怎么嵌入到平台里的
> AIRepair 组件以 Tab 的形式嵌入到三个不同的父页面中——软件版本详情页的两个视图,和 AI Home 的开源软件详情页。父页面通过 `versionId` prop 把当前查看的软件版本 ID 传给 AIRepairAIRepair 根据这个 ID 去请求对应版本的安全漏洞信息。这种组件化的设计使得同一个功能可以在多个页面中复用,不需要重复开发。
### Q5AI 返回的数据有哪些字段?怎么展示的?
> AI 返回四个字段:`vulFeature`(漏洞特性——描述漏洞的技术特征,如"远程代码执行,攻击者可通过构造恶意数据包触发")、`vulDescription`(漏洞描述——更详细的漏洞说明)、`upgradeVersion`(建议升级到的安全版本号)、`fixSuggestionList`(修复建议列表——一个字符串数组,每条是一条具体的修复措施)。前端用弹窗展示,前三个字段是键值对形式,修复建议用列表形式渲染。所有字段都用了 `||` 兜底为 `'--'`,防止某个字段缺失导致空白。
### Q6AI 修复建议和漏洞列表是两个不同的接口,为什么要分开?
> 两个接口的职责不同。漏洞列表接口(`/api/v1/repo/repair/info`返回的是批量数据——当前软件版本所有受影响依赖库的漏洞概况数据量较大但字段较少页面加载时一次性请求。AI 修复建议接口(`/ai/vul_suggestion`)是点对点请求——用户点击某个具体漏洞后才请求,每次只传一个 CVE 编号去查询。这种设计避免了首页加载时对每个漏洞都调一次 AI 接口,既不浪费 AI 调用额度,也保证了页面加载速度。
---
## 六、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 组件文件数 | 3 个index.vue + Panel.vue + Table.vue |
| AI API 接口数 | 2 个(`/api/v1/repo/repair/info` + `/ai/vul_suggestion` |
| AI 返回字段数 | 4 个(漏洞特性、漏洞描述、升级版本、修复建议列表) |
| 嵌入父页面数 | 3+ 个(版本详情页 ×2 + AI Home详情 + 代码质量页) |
| Panel 折叠动画过渡 | 0.3 秒cubic-bezier 缓动 |
---
## 七、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| AI 修复主页面 | `src/views/Jyh/AIRepair/index.vue` |
| 可折叠面板组件 | `src/components/AIRepair/Panel.vue` |
| 漏洞表格 + AI 建议弹窗 | `src/components/AIRepair/Table.vue` |
| AI Home 中的修复副本 | `src/views/Jyh/AI/Home/components/OssDetail/AIRepair/index.vue` |
| AI 修复相关 API | `src/api/jyh/index.ts`remediationSearchRemediations、getVulSuggestion |
| 版本详情页(嵌入位置 1 | `src/views/Jyh/Version/Detail/index.vue` |
| 版本详情页(嵌入位置 2 | `src/views/Jyh/Version/Detail/repoDetails/index2.vue` |
| AI Home 详情(嵌入位置 3 | `src/views/Jyh/AI/Home/components/OssDetail/index.vue` |

View File

@@ -0,0 +1,388 @@
# API 接口前后端集成 — 面试版
> 简历原话:**"协作设计可信态势感知平台API接口完成50+数据接口的前后端集成"**
>
> 这篇文档帮你理解 API 集成层到底做了什么、怎么做的,以及面试时怎么讲。
---
## 一、先搞清楚API 集成层是什么?
API 集成层是前端和后端之间的"翻译官"和"交通管制员"。它不是简单的 `axios.get('/xxx')`,而是一整套基础设施:
```
页面组件Vue 组件)
│ 调用 API 函数
API 函数层29 个文件641 个函数)
→ 定义接口 URL、method、参数格式
│ 调用 request()
请求基础设施request.ts273 行)
→ Axios 实例、请求拦截器、响应拦截器
→ AES 加密/解密、Token 注入、Token 过期自动刷新
→ 错误统一处理、缓存降级
│ HTTP 请求
后端服务器
```
**一句话概括**:我参与设计和集成了平台的 API 通信层,包括定义了 50+ 个态势感知相关的数据接口并搭建了完整的请求基础设施加密、Token 刷新、错误处理、缓存降级),覆盖全平台 641 个接口。
---
## 二、API 整体规模
| 数据 | 数值 |
|------|------|
| API 函数总数 | **641 个** |
| API 文件数 | **29 个** |
| 金银湖态势感知相关 | **106 个**situation.ts 50 + index.ts 45 + home.ts 5 + auth.ts 3 + scanCenter.ts 3 |
| 最大的文件 | `repo/index.ts`161 个函数) |
### 按模块分布
| 模块 | 文件 | 函数数 | 负责什么 |
|------|------|--------|---------|
| **态势感知看板** | `jyh/situation.ts` | 50 | 威胁地图、CVE 趋势、组件排行、漏洞分布等 |
| **金银湖核心** | `jyh/index.ts` | 45 | AI 对话、版本详情、SBOM、许可证、修复建议 |
| **仓库管理** | `repo/index.ts` | 161 | 仓库 CRUD、分支、标签、Wiki、文件、Webhook |
| **组织管理** | `org/index.ts` | 55 | 组织 CRUD、成员、首页、CLA、开发者门户 |
| **用户管理** | `user/index.ts` | 55+ | 登录注册、OAuth、个人资料、SSH 密钥、Token |
| **合并请求** | `merge/index.ts` | 62 | MR 创建/审查、差异对比、代码评审、门禁 |
| **讨论系统** | `discussion/index.ts` | 50 | 讨论 CRUD、分类、评论、投票、排行榜 |
| **其他 20+ 个文件** | issue、commit、branch 等 | ~120 | Issue、提交、分支、标签、发布、通知等 |
---
## 三、请求基础设施(面试核心 — request.ts
`src/utils/request.ts`273 行)是整个平台 API 通信的中枢,每一行都在解决实际问题。
### 3.1 整体架构
```
组件调用 API 函数
proxyService(params) ← 创建一个 Axios 实例
├── 请求拦截器 #1注入 Header
│ ├── DP_TOKEN / DP_REFRESH_TOKENToken 头)
│ ├── Authorization: Bearer xxx权限头
│ ├── page-title / page-repo-id / page-ref页面监控头
│ ├── gitcode-utm-source来源追踪头
│ └── URL 前缀重写setPassportPrefix /uc
├── 请求拦截器 #2缓存降级检查
│ └── degradeInterceptor 决定是否读缓存
├── ===== 发出 HTTP 请求 =====
├── 响应拦截器 #1成功处理
│ ├── code===500 → 检查登录状态
│ └── AES 解密响应数据($Decrypt
├── 响应拦截器 #1失败处理
│ ├── 401 → Token 自动刷新refreshing 锁 + 请求队列)
│ ├── 其他 4xx/5xx → dealWarning防抖 200ms
│ └── 网络超时 → 静默处理
└── 响应拦截器 #2缓存降级存储
└── 成功时存缓存,失败时读缓存返回
```
### 3.2 AES 加密解密
```typescript
// request.ts — 所有响应数据经过 AES-128-CBC 解密
import CryptoJS from 'crypto-js';
const sKey = CryptoJS.enc.Utf8.parse('mXzfCSPBKmEA8aLq');
const iv = CryptoJS.enc.Utf8.parse('tbeJJLC6dZQXXtWr');
// 解密:响应拦截器中自动调用
const $Decrypt = (text) => {
let src = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Base64.parse(text));
let bytes = CryptoJS.AES.decrypt(src, sKey, {
iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7
});
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
};
// 响应拦截器中response.data.data = $Decrypt(response.data.data)
```
**为什么用加密?** 态势感知平台涉及敏感的安全数据AES-128-CBC 加密保证了传输过程中即使被截获也无法直接读取。
### 3.3 Token 自动刷新 + 请求队列
这是整个基础设施中设计最巧妙的部分——**当 Token 过期时,多个并发请求只触发一次刷新**
```typescript
// request.ts — Token 刷新逻辑(简化版)
let refreshing = false; // 刷新锁
const queue = []; // 请求等待队列
// 响应拦截器的错误处理分支
async (error) => {
if (response?.status === 401 && Token && ) {
if (refreshing) {
// 情况 A已经有刷新在进行 → 当前请求排队等待
return new Promise((resolve) => {
queue.unshift({ config, resolve });
});
}
// 情况 B第一个遇到 401 的请求 → 执行刷新
refreshing = true;
const res = await refreshToken(); // 调刷新接口
refreshing = false;
if (res.status === 200) {
// 刷新成功 → 更新 Token → 重放所有排队的请求
localStorage.setItem('op_access_token', newToken);
queue.forEach(({ config, resolve }) => {
config.headers.Authorization = `Bearer ${newToken}`;
resolve(axios(config)); // 用新 Token 重发
});
queue.splice(0); // 清空队列
return axios(config); // 重发当前请求
}
}
}
```
**场景**:用户打开一个有 5 个图表的页面5 个请求同时发出,全部返回 401。第一个请求触发 `refreshToken`,其余 4 个进入队列等待。刷新成功后5 个请求用新 Token 全部重发。**不会出现 5 个请求各自刷新一次 Token 的情况。**
### 3.4 错误统一处理status.ts
```typescript
// status.ts — HTTP 状态码 → 用户提示 + 事件通知
export function showMessage(res) {
switch (res.status) {
case 400: return res.data.error_message; // 参数错误
case 401: emitEvent('logout', ...); break; // 未授权 → 触发登出
case 403: emitEvent('forbiddenRefresh'); // 无权限 → 刷新页面
case 404: break; // 不存在 → 不提示
case 408: return '请求超时';
case 500: return '服务器错误';
case 502: return '网络错误';
case 504: return '网络超时';
default: return res.data.error_message || '连接出错';
}
}
// 用防抖包裹200ms防止短时间内多次弹错误提示
export function dealWarning() {
return debounce((res) => {
const msg = showMessage(res);
if (msg) Message.error(msg);
}, 200);
}
```
**面试话术**
> "请求基础设施有四个关键设计。第一AES-128-CBC 加密传输敏感数据在传输过程中全程加密。第二Token 自动刷新 + 请求队列——用 refreshing 锁和 Promise 队列机制,保证多个并发的 401 请求只触发一次 Token 刷新,其余排队等待,刷新成功后统一重发。第三,错误统一处理——所有 HTTP 错误状态码映射为用户友好的提示信息,并用 200ms 防抖避免短时间弹多次错误。第四API 缓存降级——请求成功时缓存到 IndexedDB失败时自动读缓存兜底。"
---
## 四、API 函数层 — 50+ 态势感知接口怎么组织的?
### 4.1 统一的 API 函数格式
每个 API 函数遵循统一的格式:
```typescript
// src/api/jyh/situation.ts — 示例:态势感知看板 API 函数
import request from '@/utils/request';
// 格式export function 函数名(参数): 返回类型
// ↓ 语义化命名 ↓ TypeScript 泛型
// 全球威胁地图数据
export function mapDataPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/map/mapData`, // ← 接口路径
method: 'get', // ← 请求方法
params // ← GET 方式用 params
});
}
// AI 对话POST 方式)
export function fetchChatUseSql(data): Promise<any> {
return request({
url: '/ai/v1/chat/use_sql', // ← AI 服务接口
method: 'POST', // ← POST 请求
data // ← POST 方式用 data 传 JSON body
});
}
// 文件上传FormData 方式)
export function uploadImage(data): Promise<any> {
const formData = new FormData();
formData.append('file', data.file);
return request({
url: '/api/v1/upload/image',
method: 'POST',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
});
}
```
### 4.2 态势感知相关的 50+ 接口
态势感知模块的接口集中在 `src/api/jyh/situation.ts`50 个函数),按三个页面组织:
| 页面 | 接口前缀 | 端点数 | 干什么 |
|------|---------|--------|--------|
| **第1页全球风险态势** | `/api/v1/page1/*` | 13 个 | 威胁地图、CVE 趋势、组件排行、漏洞分布、行业饼图 |
| **第2页开源生态与贡献** | `/api/v1/page2/*` | ~18 个 | 企业榜单、基础设施覆盖率、高校俱乐部、社区健康、语言竞争力、开发者、热力图 |
| **第3页武汉市开源生态** | `/api/v1/page3/*` | ~19 个 | HarmonyOS 统计、项目分布、开发者、企业、高校、社区、政策、镜像站点 |
加上 `jyh/index.ts` 中的 45 个通用接口AI 对话、版本详情、SBOM、许可证、预警订阅、修复建议、质量任务等金银湖模块共有 **106 个 API**
---
## 五、两个错误处理工具函数
### 5.1 reqCatch — 安全包装 API 调用
```typescript
// src/utils/catch.ts
// 把 try/catch 包装成一个通用函数,避免每个组件都写 try/catch
export async function reqCatch(req, params) {
try {
const data = await req(params);
return { data, error: null }; // 成功 → 返回数据
} catch (e) {
return { data: null, error: e }; // 失败 → 返回错误对象,不会 throw
}
}
// 用法:
const res = await reqCatch(getOrg, { orgId: 'xxx' });
if (!res.error) {
// 成功处理
} else if (res.error.error_code === 404) {
// 404 处理
}
```
### 5.2 reqCatchV2 — 升级版,自动发事件
```typescript
export async function reqCatchV2(req) {
try {
const data = await req();
return { data, error: null };
} catch (e) {
emitEvent('responseError', e); // ← 自动通过事件总线通知全局
return { data: null, error: e };
}
}
```
**为什么有两版?** `reqCatch` 是早期版本,静默捕获错误让调用方自己处理。`reqCatchV2` 增加了自动事件通知——在路由守卫里用 V2错误会被 `eventBus` 监听到,触发统一的 403/404 页面跳转。
---
## 六、API 缓存降级系统degradeInterceptor.ts
这是一个 360 行的自研系统,核心思想是:**API 成功时缓存结果,失败时读缓存兜底**。
```
请求发出
├── 请求拦截器
│ ├── 是否匹配缓存策略URL 正则匹配)
│ ├── 是否处于降级状态?(上次请求失败,且在熔断时间内)
│ │ → 是:直接读 IndexedDB 缓存返回(不发请求)
│ │ → 否:正常发请求(带重试增强)
├── 请求成功 → 响应拦截器
│ └── 缓存结果到 IndexedDB + 更新状态为 success
└── 请求失败 → 响应拦截器
├── 记录失败状态 + 时间戳
└── 读 IndexedDB 缓存返回(用户看到旧数据而不是错误)
```
**关键配置**`storage-apis.ts`
```typescript
const storageApis = [
{ url: '/api/v1/issues', timeout: 5000, retry: 2 },
{ url: '/api/v1/merge_requests', timeout: 5000, retry: 2 },
// ...
];
```
**面试话术**
> "API 缓存降级系统的思路是'请求成功时缓存,失败时读缓存兜底'。在请求拦截器里判断是否命中缓存策略——如果上次请求失败且在熔断时间内,就直接读 IndexedDB 返回缓存数据,不发网络请求。响应成功时自动更新缓存,失败时记录状态并返回历史缓存。还有缓存条数上限控制和过期清理机制。"
---
## 七、面试问答准备
### Q1"完成50+数据接口的前后端集成"具体做了什么?
> 我参与了两方面的工作。第一是接口定义——和 backend 协作设计并集成了 50+ 个态势感知相关的数据接口,按三个页面组织(全球态势、开源生态、武汉生态),定义了接口路径、请求方法、参数格式和返回结构。第二是搭建了完整的请求基础设施——包括 Axios 实例封装、AES-128-CBC 加密传输、Token 自动刷新 + 请求队列、HTTP 状态码统一处理、API 缓存降级系统等。全平台共集成了 641 个 API 函数,分布在 29 个文件中。
### Q2Token 过期自动刷新是怎么实现的?
> 用了一个 `refreshing` 锁 + Promise 请求队列的机制。当请求返回 401 时,先检查 `refreshing` 是否为 true——如果是说明已经有请求在刷新 Token 了,当前请求就包装成一个 Promise 放进等待队列;如果不是,就执行刷新并设 `refreshing = true`。刷新成功后更新 Token然后遍历队列用新 Token 重发所有等待的请求。这样无论同时有多少个请求返回 401都只会发一次刷新请求。
### Q3为什么需要 AES 加密?
> 这个平台涉及安全态势数据,有一定的敏感性。后端返回的数据在传输层用 AES-128-CBC 加密,前端在响应拦截器中自动解密。加密密钥和 IV 是硬编码在前端代码中的生产环境会用更安全的方式管理。加密流程对业务代码完全透明——API 函数只是调用 `request()`,加密解密都在拦截器里自动完成。
### Q4reqCatch 和 reqCatchV2 有什么区别?
> 都是 try/catch 的通用封装,避免每个页面都写 try/catch。区别在于`reqCatch` 静默捕获错误,返回 `{ data, error }` 结构,让调用方自己判断。`reqCatchV2` 在捕获错误后还会通过事件总线 `emitEvent('responseError', e)` 通知全局——这主要用于路由守卫里,发生 403/404 时通过事件触发全局的页面跳转。新代码基本都用 V2。
### Q5API 缓存降级是怎么运作的?
> 这是一套自研的请求降级系统。基于 IndexedDB 做持久化缓存。请求成功时自动缓存响应数据;请求失败时判断是否在熔断时间内——如果是,直接读缓存返回,用户看到的是旧数据而不是错误页面。还有最大缓存条数限制和过期清理机制。通过 `storage-apis.ts` 配置哪些接口走缓存策略,可以设置超时、重试次数、熔断时间等参数。
### Q6HTTP 错误怎么统一处理的?
> 在响应拦截器里统一处理。根据 HTTP 状态码映射不同的行为400 返回参数错误提示、401 触发 Token 刷新或登出、403 触发页面刷新、404 静默不提示、5xx 显示服务端错误提示。错误提示用 `dealWarning` 函数包裹了 200ms 防抖,避免短时间内多个请求同时失败时弹出多条错误消息。还有 `customError: true` 参数,允许特定请求跳过默认错误处理,由调用方自行处理。
---
## 八、关键数字(面试时用)
| 数据 | 数值 |
|------|------|
| 全平台 API 函数总数 | **641 个** |
| API 文件数 | **29 个** |
| 态势感知金银湖API 数 | **106 个** |
| 态势感知看板接口situation.ts | **50 个** |
| 请求基础设施代码量 | `request.ts` 273 行 + `degradeInterceptor.ts` 360 行 + `catch.ts` 46 行 + `status.ts` 76 行 |
| Token 刷新机制 | refreshing 锁 + Promise 队列,确保多并发 401 只刷新一次 |
| 加密方式 | AES-128-CBC密钥 mXzfCSPBKmEA8aLqIV tbeJJLC6dZQXXtWr |
| 请求超时 | 30 秒 |
| 错误提示防抖 | 200ms |
---
## 九、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 请求核心基础设施 | `src/utils/request.ts`273 行Axios、拦截器、加密、Token 刷新) |
| API 缓存降级系统 | `src/utils/degradeInterceptor.ts`360 行IndexedDB 缓存、熔断、重试) |
| 错误捕获包装函数 | `src/utils/catch.ts`reqCatch + reqCatchV2 |
| HTTP 状态码处理 | `src/utils/status.ts`dealWarning + showMessage |
| 缓存配置 | `src/api/storage-apis.ts`(哪些接口走缓存策略) |
| 缓存存储层 | `src/utils/apiStorage.ts`IndexedDB 封装) |
| 态势感知看板 API50 端点) | `src/api/jyh/situation.ts` |
| 金银湖核心 API45 端点) | `src/api/jyh/index.ts` |
| 金银湖首页 API5 端点) | `src/api/jyh/home.ts` |
| 金银湖认证 API3 端点) | `src/api/jyh/auth.ts` |
| 仓库管理 API161 端点) | `src/api/repo/index.ts` |
| 组织管理 API55 端点) | `src/api/org/index.ts` |
| 用户管理 API55+ 端点) | `src/api/user/index.ts` |
| 合并请求 API62 端点) | `src/api/merge/index.ts` |
| 讨论系统 API50 端点) | `src/api/discussion/index.ts` |