Compare commits
3 Commits
abbf3d6421
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fc6cd3a2d | |||
| 17063cbad0 | |||
| 360317fba8 |
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.cursor/
|
||||
# docs/
|
||||
181
AGENTS.md
Normal file
181
AGENTS.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# AGENTS.md
|
||||
|
||||
## 基本要求
|
||||
|
||||
- 交流使用中文。
|
||||
- 代码整洁、高效、易读,优先遵循现有目录结构和命名风格。
|
||||
- 不使用 Linux 指令;需要命令示例时统一使用 Windows PowerShell。
|
||||
- 修改前先阅读相关文档和现有代码,避免凭空新增不一致的结构。
|
||||
- 不回滚或覆盖用户已有改动,除非用户明确要求。
|
||||
|
||||
## 项目定位
|
||||
|
||||
OPS 是“开发运维一体化平台”。首期交付重点是可验证的后端、前端和验收证据,而不是只停留在模板或文档层面。
|
||||
|
||||
统一命名:
|
||||
|
||||
- 项目名称统一使用 `OPS`。
|
||||
- 旧称 `DOIP` 不再使用;新增文档、代码包名、页面文案、配置示例和注释不要再引入 `DOIP`。
|
||||
- `OPS-001` 至 `OPS-033` 是需求编号,必须保留。
|
||||
|
||||
优先参考资料:
|
||||
|
||||
- `README.md`:项目一句话说明。
|
||||
- `TODOS.md`:当前交付任务优先级。
|
||||
- `docs/首期验收矩阵.md`:OPS 需求到演示路径、数据准备、通过标准和证据的映射。
|
||||
- `docs/首期数据模型与状态机.md`:第 1 阶段核心模型、状态机和存储分工。
|
||||
- `docs/首期UI状态覆盖.md`:第 1 阶段页面状态覆盖要求。
|
||||
- `docs/P1故障救援策略.md`:采集、解析、通知、派单失败的救援策略。
|
||||
- `docs/P1测试计划.md`:第 1 阶段后端、前端和端到端测试计划。
|
||||
- `docs/国产时序数据库选型验证.md`:TDengine 开源版选型结论和适配层要求。
|
||||
|
||||
当前决策:
|
||||
|
||||
- P0 和 P1 文档任务已完成,后续重点应转向可运行的 `server/`、`web/` 和验收证据。
|
||||
- 时序数据库采用 TDengine 开源版。正式部署前仍需验证 AGPL 合规、麒麟兼容性、备份恢复和是否需要企业版支持。
|
||||
- 第 1 阶段优先打通资源纳管、采集、原始事件、告警、通知、工单、报表/大屏和审计闭环。
|
||||
- H3C/华三为首批网络设备适配重点,但具体型号、SNMP 版本、Trap/Syslog 样例和账号权限必须由现场确认,不能凭空编写。
|
||||
- 3D 机房前端已外包,OPS 侧只提供后端接口、样例数据、权限控制和告警状态。
|
||||
|
||||
## 目录职责
|
||||
|
||||
- `docs/`:项目需求、架构、验收和计划文档。默认可读可改;改动需保持内容严谨,不做无关润色。
|
||||
- `templates/front_sample/standard`:前端模板,基于 Arco Design Pro Vue 3、Vite、Pinia、TypeScript。默认只读。
|
||||
- `templates/server_sample/`:后端模板,基于 Go、Gin、GORM、BSM-SDK Core。默认只读。
|
||||
- `web/`:实际前端工程目录。不存在时,按任务需要从 `templates/front_sample/standard` 初始化后再开发。
|
||||
- `server/`:实际后端工程目录。不存在时,按任务需要从 `templates/server_sample/` 初始化后再开发。
|
||||
- `deploy/`:部署、迁移、发布脚本目录。不存在时按 TODO 创建。
|
||||
|
||||
## 边界规则
|
||||
|
||||
- 未经用户明确要求,绝不直接修改 `templates/front_sample/standard`。
|
||||
- 未经用户明确要求,绝不直接修改 `templates/server_sample/`。
|
||||
- 不把模板目录当作最终交付目录;实际业务代码应落在 `web/`、`server/`、`deploy/` 等交付目录。
|
||||
- 不提交真实密钥、Token、数据库密码、私有仓库凭据或带凭据的 remote URL。
|
||||
- 配置文件提供 `.example` 示例,真实本地配置不应纳入版本控制。
|
||||
- 不引入与当前技术栈无关的大型框架或重构,除非任务明确要求。
|
||||
|
||||
## 常用 Windows PowerShell 命令
|
||||
|
||||
查看目录:
|
||||
|
||||
```powershell
|
||||
Get-ChildItem -Force
|
||||
```
|
||||
|
||||
查看文件:
|
||||
|
||||
```powershell
|
||||
Get-Content -LiteralPath .\README.md -Encoding UTF8
|
||||
```
|
||||
|
||||
复制模板初始化实际工程:
|
||||
|
||||
```powershell
|
||||
Copy-Item -LiteralPath .\templates\server_sample -Destination .\server -Recurse
|
||||
Copy-Item -LiteralPath .\templates\front_sample\standard -Destination .\web -Recurse
|
||||
```
|
||||
|
||||
后端常用命令:
|
||||
|
||||
```powershell
|
||||
Set-Location .\server
|
||||
go mod tidy
|
||||
go test ./...
|
||||
go vet ./...
|
||||
gofmt -w .
|
||||
go run .\cmd\main\main.go
|
||||
go run .\cmd\cli\main.go migrate
|
||||
```
|
||||
|
||||
前端常用命令:
|
||||
|
||||
```powershell
|
||||
Set-Location .\web
|
||||
pnpm install
|
||||
pnpm dev
|
||||
pnpm type:check
|
||||
pnpm lint
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Git 状态检查:
|
||||
|
||||
```powershell
|
||||
git status --short
|
||||
git diff -- .\AGENTS.md
|
||||
```
|
||||
|
||||
检查旧项目名残留:
|
||||
|
||||
```powershell
|
||||
Select-String -Path .\README.md,.\TODOS.md,.\AGENTS.md,.\docs\*.md,.\deploy\README.md -Encoding UTF8 -Pattern 'DOIP','doip'
|
||||
```
|
||||
|
||||
## 后端规范
|
||||
|
||||
- 使用 Go、Gin、GORM,延续 `templates/server_sample/` 的分层:`cmd/`、`internal/config/`、`internal/impl/`、`internal/logic/`、`internal/models/`、`internal/routers/`。
|
||||
- API 需要统一响应结构、统一错误码和 `traceId`。
|
||||
- 数据库设计优先面向 PostgreSQL,不继续扩散 MySQL 风格类型。
|
||||
- 时序数据进入 TDengine 开源版;业务代码必须通过时序库适配层访问,不直接在业务逻辑中散落 TDengine 专有 SQL。
|
||||
- 危险操作必须有审计日志、人工确认或可恢复方案。
|
||||
- 配置通过 `etc/*.example.yaml` 描述结构,不把本地真实配置提交进仓库。
|
||||
- 业务逻辑应写在 `internal/logic/`,模型和持久化写在 `internal/models/`,路由注册写在 `internal/routers/`。
|
||||
- 首期核心模型优先落地资源、指标定义、采集任务、原始事件、告警、通知、工单、审计。
|
||||
- 采集失败、Trap/Syslog 解析失败、通知失败、派单失败必须有错误码、重试或降级、用户提示、日志和审计。
|
||||
- 告警、事件、工单状态机必须在后端集中定义和校验,前端不能自行拼接非法状态。
|
||||
|
||||
## 前端规范
|
||||
|
||||
- 使用 Vue 3、TypeScript、Pinia、Vue Router、Arco Design Vue,延续模板现有风格。
|
||||
- 页面优先实现真实可用流程,不新增纯展示型占位页。
|
||||
- API 定义按业务域放入 `src/api/`,页面放入 `src/views/`,复用组件放入 `src/components/`。
|
||||
- 状态需要覆盖 loading、empty、error、success、partial、forbidden、stale、operating、operation_failed。
|
||||
- 开发期 mock 只能用于前端独立调试;验收和联调必须连接真实后端 API。
|
||||
- 文案和菜单要按模板 i18n 方式维护,不在组件中散落重复字符串。
|
||||
- 首页、大屏、报表不能使用静态假数据作为验收依据;暂未接通真实接口时要明确显示空态、错误或数据过期。
|
||||
- 错误状态必须显示 `traceId`,便于从页面追到后端日志。
|
||||
|
||||
## 测试规范
|
||||
|
||||
- 禁止 mock 数据库;后端测试必须使用真实 SQLite 内存库或任务指定的真实测试数据库。
|
||||
- 新增核心业务逻辑时补充对应单元测试或接口测试。
|
||||
- 状态机、统一响应、错误救援、权限边界和审计记录属于必须测试范围。
|
||||
- TDengine 适配层至少要覆盖批量写入、时间范围查询、聚合查询、保留策略校验和不可用降级。
|
||||
- 后端改动至少运行:
|
||||
|
||||
```powershell
|
||||
go test ./...
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
- 前端改动至少运行:
|
||||
|
||||
```powershell
|
||||
pnpm type:check
|
||||
pnpm lint
|
||||
pnpm build
|
||||
```
|
||||
|
||||
- 如果因私有依赖、网络、凭据或环境限制无法运行测试,需要在交付说明中明确写出原因和风险。
|
||||
|
||||
## 文档规范
|
||||
|
||||
- 文档使用中文,标题层级清晰,避免空泛描述。
|
||||
- 修改需求、架构或验收相关内容时,同步检查 `TODOS.md` 和 `docs/首期验收矩阵.md` 是否需要更新。
|
||||
- 新增命令示例必须使用 Windows PowerShell,不写 Bash、`cp`、`rm -rf`、`export` 等 Linux 写法。
|
||||
- 文档中的路径使用项目相对路径,例如 `server/etc/app.example.yaml`。
|
||||
- 涉及部署 Linux/麒麟时,可以写执行步骤、配置项和检查点,但命令示例仍保持 Windows PowerShell。
|
||||
- 现场未知信息必须标为“待现场确认”,不要伪造设备型号、账号、OID、Trap/Syslog 样例、短信平台参数或生产地址。
|
||||
- 修改 OPS 命名、时序数据库选型、H3C 接入、验收矩阵相关内容时,要同步检查 README、TODOS、首期验收矩阵和相关 P1 文档。
|
||||
|
||||
|
||||
|
||||
## 已知坑
|
||||
|
||||
- `templates/front_sample/standard` 和 `templates/server_sample/` 是模板目录,不是最终交付目录。
|
||||
- 测试不能用 mock 数据库替代真实数据库行为。
|
||||
- 当前仓库存在私有 Go 依赖,执行 `go mod tidy`、`go test` 可能需要私有仓库访问权限。
|
||||
- 处理中文文档时使用 UTF-8 读取和写入,避免产生乱码。
|
||||
- TDengine 开源版无时间使用限制,但 AGPL-3.0 合规和麒麟目标版本支持需要正式确认。
|
||||
- 旧项目名 `DOIP` 已替换为 `OPS`,后续新增内容不要重新引入旧称。
|
||||
18
README.md
18
README.md
@@ -1,2 +1,18 @@
|
||||
# ops
|
||||
# OPS 开发运维一体化平台
|
||||
|
||||
OPS 是面向医院信息化场景的开发运维一体化平台,首期交付重点是打通“资源纳管 -> 指标/日志/Trap -> 告警 -> 通知 -> 工单 -> 报表/大屏/审计”的可验证闭环。
|
||||
|
||||
## 文档入口
|
||||
|
||||
| 文档 | 说明 |
|
||||
| --- | --- |
|
||||
| `TODOS.md` | 当前交付优先级和完成状态。 |
|
||||
| `docs/integrated-ops-platform-requirements.md` | 招标文件和菜单规划提取的 33 条 OPS 需求。 |
|
||||
| `docs/integrated-ops-platform-blueprint-design.md` | 平台蓝图、分期路线和审查结论。 |
|
||||
| `docs/首期验收矩阵.md` | OPS 需求到演示路径、数据准备、通过标准和证据的映射。 |
|
||||
| `docs/首期数据模型与状态机.md` | 第 1 阶段核心模型、状态机和存储分工。 |
|
||||
| `docs/首期UI状态覆盖.md` | 第 1 阶段页面状态覆盖要求。 |
|
||||
| `docs/本地开发与验收部署说明.md` | 本地开发、联调验收和 Linux/麒麟部署边界。 |
|
||||
| `docs/H3C华三首批接入调研.md` | H3C/华三首批接入基线和现场确认表。 |
|
||||
| `docs/国产时序数据库选型验证.md` | TDengine、Apache IoTDB、openGemini 选型验证。 |
|
||||
| `deploy/README.md` | 验收部署、迁移、回滚和烟测说明。 |
|
||||
|
||||
74
TODOS.md
Normal file
74
TODOS.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# OPS 首期交付 TODO
|
||||
|
||||
来源:`/autoplan` 审查 `docs/integrated-ops-platform-blueprint-design.md`。
|
||||
|
||||
## P0
|
||||
|
||||
- [x] 处理 `.gitignore` 中忽略 `docs/` 的规则。
|
||||
- 原因:当前需求、蓝图和后续验收矩阵都在 `docs/`,但 `.gitignore` 忽略该目录,交付文档无法正常纳入版本控制。
|
||||
- 验收:`git status --short --ignored` 能清楚区分应跟踪文档与不应跟踪文件;必要时移除 `docs/` 忽略规则或显式强制添加交付文档。
|
||||
|
||||
- [x] 忽略 Git 远程地址明文凭据整改,不作为当前交付阻塞项。
|
||||
- 原因:用户已明确要求忽略该 P0;该项不再阻塞首期文档、设计和后续实现准备。
|
||||
- 风险:远程地址中如果保留账号、密码或 Token,仍存在泄露风险;后续推送、共享仓库或交付前建议重新评估。
|
||||
- 验收:本文档已记录用户决策;不在当前任务中修改 Git 远程地址。
|
||||
|
||||
- [x] 编写 `docs/首期验收矩阵.md`。
|
||||
- 原因:当前蓝图已经映射 33 条 OPS 需求,但缺少逐项演示路径、数据准备、通过标准和证据要求。
|
||||
- 验收:每个 OPS 编号都有阶段归属、演示脚本、截图/录像证据、通过标准。
|
||||
- 产物:`docs/首期验收矩阵.md`。
|
||||
|
||||
- [x] 明确第 1 阶段的数据模型与状态机。
|
||||
- 原因:资源、指标、原始事件、告警、事件、工单、通知、审计是闭环主干,必须先稳定。
|
||||
- 验收:文档中包含 ER 关系、状态枚举、非法状态迁移、审计字段。
|
||||
- 产物:`docs/首期数据模型与状态机.md`。
|
||||
|
||||
- [x] 补齐第 1 阶段 UI 信息架构和状态覆盖。
|
||||
- 原因:当前蓝图列出了页面,但未指定每个页面的加载、空态、错误、成功、部分成功、无权限状态。
|
||||
- 验收:首页、综合监控、告警中心、工单、报表、大屏、权限页都有状态表。
|
||||
- 产物:`docs/首期UI状态覆盖.md`。
|
||||
|
||||
## P1
|
||||
|
||||
- [] 定义采集失败、Trap/Syslog 解析失败、通知失败、派单失败的错误与救援策略。
|
||||
- 原因:一体化运维平台自身故障不能静默,否则验收时无法证明平台可靠性。
|
||||
- 验收:每类失败都有重试、降级、用户提示、日志、审计和测试要求。
|
||||
|
||||
- [] 为第 1 阶段建立后端与前端测试计划。
|
||||
- 原因:当前仓库尚无实际 `server/`、`web/` 工程,测试策略需要先指导实现。
|
||||
- 验收:后端包含单元、接口、SQLite 内存库测试;前端包含类型检查、状态渲染和核心 E2E。
|
||||
|
||||
- [] 使用 gstack 工程评审口径梳理首期架构、数据模型和接口设计。
|
||||
- 原因:P0/P1 文档已形成验收、状态机、UI 状态、救援和测试计划,但后续初始化 `server/`、`web/` 前还需要一份可编码的模块边界和 REST API 规格。
|
||||
- 验收:文档包含后端/前端模块划分、核心数据流、数据模型、统一响应、错误码、API 路由、前端状态映射和实施顺序。
|
||||
|
||||
- [] 区分本地开发命令和验收部署说明。
|
||||
- 原因:本地开发和调试使用 Windows PowerShell;会议已确认验收部署需要面向 Linux 与麒麟系统,模板 README 不能直接成为交付文档。
|
||||
- 验收:实际 `server/`、`web/` 文档提供 Windows PowerShell 本地开发路径;`deploy/` 提供 Linux/麒麟部署、迁移、回滚和烟测说明,且不包含真实凭据。
|
||||
- 产物:`docs/本地开发与验收部署说明.md`、`deploy/README.md`。
|
||||
|
||||
- [] 完成 H3C/华三设备首批接入调研。
|
||||
- 原因:会议确认现场设备以 H3C/华三为主,第 1 阶段应优先适配该品牌常见指标、接口状态和 Trap/Syslog 样例。
|
||||
- 验收:明确首批设备型号、SNMP 版本、OID 清单、Trap 字典、Syslog 样例、账号权限和网络连通性。
|
||||
- 产物:`docs/H3C华三首批接入调研.md`。
|
||||
- 风险:仓库现有资料未提供现场具体型号、SNMP 版本、真实 Trap/Syslog 样例和账号权限;文档已提供首批接入基线和现场确认表,正式验收前必须由院方或现场工程师补齐。
|
||||
|
||||
- [] 完成国产/国内生态时序数据库选型验证。
|
||||
- 原因:高频指标和采集样本不应全部压入 PostgreSQL,需要选型 TDengine、Apache IoTDB、openGemini 或其他合适产品。
|
||||
- 验收:形成选型记录,覆盖 Linux/麒麟部署、Go 连接方式、批量写入、范围查询、聚合、降采样、保留策略、备份恢复和授权风险。
|
||||
- 产物:`docs/国产时序数据库选型验证.md`。
|
||||
- 决策:采用 TDengine 开源版;正式部署前继续验证 AGPL 合规、麒麟兼容性、备份恢复和是否需要企业版支持。
|
||||
|
||||
## P2
|
||||
|
||||
- [ ] 规划拓扑、IPAM、机柜、动环进入第 2 阶段的导入和联动策略。
|
||||
- 原因:这些能力是完整蓝图必备,但不应阻塞第 1 阶段告警闭环。
|
||||
- 验收:每个能力明确数据来源、导入方式、与资源/告警的关联点。
|
||||
|
||||
- [ ] 设计外包 3D 机房前端所需后端接口。
|
||||
- 原因:会议确认 3D 机房前端已外包,OPS 侧只需要提供数据中心、机房、机柜、U 位、设备和告警状态等后端接口。
|
||||
- 验收:接口文档明确字段、刷新频率、权限控制、状态编码、样例响应和错误码。
|
||||
|
||||
- [ ] 设计告警质量评分和规则优化建议的第 3 阶段路线。
|
||||
- 原因:第 3 阶段要从可处置走向可优化,需要沉淀告警噪声、误报、处理时长等指标。
|
||||
- 验收:定义告警质量指标、统计口径、规则建议来源和人工确认边界。
|
||||
80
deploy/README.md
Normal file
80
deploy/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# OPS 验收部署说明
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本文定义 OPS 在 Linux/麒麟验收环境中的部署、迁移、回滚和烟测步骤。受项目规则约束,本文不提供 Linux 命令示例,只提供执行顺序、配置项和验收检查点。
|
||||
|
||||
## 2. 部署前确认
|
||||
|
||||
| 项目 | 必填内容 |
|
||||
| --- | --- |
|
||||
| 操作系统 | 发行版、版本、CPU 架构、补丁状态 |
|
||||
| 网络区域 | Web 访问区、后端 API 区、数据库区、采集区 |
|
||||
| 域名或访问地址 | 前端访问地址、API 地址、回调地址 |
|
||||
| PostgreSQL | 版本、地址、端口、库名、账号来源 |
|
||||
| 时序数据库 | 产品、版本、部署形态、保留策略 |
|
||||
| 通知渠道 | 站内消息、短信、邮件测试账号和发送限制 |
|
||||
| 采集入口 | SNMP、Trap、Syslog、URL/API 探测网络策略 |
|
||||
| 备份目录 | 数据库备份、时序库备份、配置备份、日志归档 |
|
||||
|
||||
真实密码、Token、短信密钥和邮件密码只能写入现场配置,不进入仓库。
|
||||
|
||||
## 3. 部署顺序
|
||||
|
||||
| 顺序 | 步骤 | 通过标准 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 准备运行用户、目录、端口和防火墙策略 | 服务账号权限最小化,端口策略已审批。 |
|
||||
| 2 | 部署 PostgreSQL 并初始化数据库 | 可连接,字符集和时区正确。 |
|
||||
| 3 | 部署选定时序数据库 | 可写入样本,可按时间范围查询。 |
|
||||
| 4 | 放置后端配置文件 | 配置中无明文提交凭据,凭据来自现场安全渠道。 |
|
||||
| 5 | 执行数据库迁移 | 表结构与当前版本匹配,迁移日志保留。 |
|
||||
| 6 | 启动后端服务 | 健康检查成功,日志可查看。 |
|
||||
| 7 | 部署前端静态包 | 页面可访问,API 地址指向验收后端。 |
|
||||
| 8 | 配置采集和通知通道 | 采集任务、短信、邮件测试通过。 |
|
||||
| 9 | 执行烟测 | 登录、资源、告警、通知、工单、报表主路径通过。 |
|
||||
|
||||
## 4. 迁移要求
|
||||
|
||||
| 迁移对象 | 要求 |
|
||||
| --- | --- |
|
||||
| PostgreSQL 表结构 | 每个迁移文件必须可追踪版本、执行时间和执行结果。 |
|
||||
| 初始化字典 | 告警级别、资源类型、通知渠道、权限码必须可重复执行。 |
|
||||
| 时序库 schema | 指标命名、标签、保留策略必须与 `docs/首期数据模型与状态机.md` 一致。 |
|
||||
| 样例数据 | 验收样例必须可清理,不与生产数据混淆。 |
|
||||
|
||||
迁移失败时不得继续执行后续部署步骤。
|
||||
|
||||
## 5. 回滚要求
|
||||
|
||||
| 对象 | 回滚策略 |
|
||||
| --- | --- |
|
||||
| 后端服务 | 保留上一版本二进制或镜像,配置兼容性检查通过后回退。 |
|
||||
| 前端静态包 | 保留上一版本静态文件,切回后清理浏览器缓存影响。 |
|
||||
| PostgreSQL | 迁移前备份,迁移失败按备份恢复或执行成对回滚脚本。 |
|
||||
| 时序数据库 | 变更保留策略、降采样规则前备份元数据和关键样本。 |
|
||||
| 配置文件 | 每次变更前保留上一份配置,敏感字段仍按现场安全要求保存。 |
|
||||
|
||||
## 6. 烟测清单
|
||||
|
||||
| 编号 | 检查项 | 通过标准 |
|
||||
| --- | --- | --- |
|
||||
| S-001 | 登录和权限 | 管理员可登录,普通用户不能访问无权限菜单。 |
|
||||
| S-002 | 首页总览 | 显示资源健康、告警趋势、待处理告警,数据来自后端。 |
|
||||
| S-003 | 资源列表 | 可查看主机、H3C/华三网络设备、数据库、URL/API 样例资源。 |
|
||||
| S-004 | 采集状态 | 最近采集时间、失败原因、数据过期状态可见。 |
|
||||
| S-005 | 原始事件 | Trap/Syslog 样例可入库,未解析事件可见。 |
|
||||
| S-006 | 告警中心 | 可触发、确认、忽略、恢复和派单。 |
|
||||
| S-007 | 通知记录 | 站内消息、短信、邮件记录可查,失败原因可见。 |
|
||||
| S-008 | 工单管理 | 可创建、接单、转交、挂起、重启、关闭。 |
|
||||
| S-009 | 报表大屏 | 基础报表可生成,大屏组件局部失败可降级。 |
|
||||
| S-010 | 审计日志 | 权限变更、告警处理、工单流转可按 `traceId` 查询。 |
|
||||
|
||||
## 7. 验收输出
|
||||
|
||||
- 部署环境确认表。
|
||||
- 配置项脱敏清单。
|
||||
- 数据库迁移记录。
|
||||
- 后端和前端版本号。
|
||||
- 烟测截图、接口响应和日志。
|
||||
- 回滚演练记录或回滚步骤确认单。
|
||||
|
||||
126
docs/H3C华三首批接入调研.md
Normal file
126
docs/H3C华三首批接入调研.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# OPS H3C/华三设备首批接入调研
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文定义第 1 阶段 H3C/华三网络设备接入的首批调研范围、字段清单、采集指标、Trap/Syslog 样例要求和验收条件。
|
||||
|
||||
当前仓库中的需求和蓝图只确认“现场设备以 H3C/华三为主”,尚未提供具体型号、SNMP 版本、账号权限、Trap 字典和 Syslog 样例。本文不伪造现场事实,先固定可开发的接入基线,并列出必须现场确认的数据。
|
||||
|
||||
## 2. 首批接入范围
|
||||
|
||||
| 类型 | 首期目标 | 验收口径 |
|
||||
| --- | --- | --- |
|
||||
| H3C/华三交换机 | 优先接入 | 展示设备基本信息、接口状态、接口流量、接口错误、Trap/Syslog 告警。 |
|
||||
| H3C/华三路由或三层设备 | 可选接入 | 若现场提供账号,展示路由、ARP 或转发表样例。 |
|
||||
| H3C/华三安全设备 | 第 2 阶段优先 | 首期只保留资源类型和接口状态样例。 |
|
||||
| 非 H3C 网络设备 | 后续扩展 | 第 1 阶段不追求多厂商全覆盖。 |
|
||||
|
||||
## 3. 现场确认表
|
||||
|
||||
| 字段 | 必填 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 设备名称 | 是 | 核心交换机-1 | 与资源名称一致。 |
|
||||
| 厂商 | 是 | H3C | 固定为 H3C/华三或现场实际厂商。 |
|
||||
| 型号 | 是 | 待现场提供 | 不能凭空填写。 |
|
||||
| 管理 IP | 是 | 待现场提供 | 用于 SNMP 轮询和连通性测试。 |
|
||||
| SNMP 版本 | 是 | v2c 或 v3 | v3 需确认认证和加密方式。 |
|
||||
| SNMP 端口 | 是 | 161 | 按现场安全策略确认。 |
|
||||
| Trap 目标端口 | 是 | 162 | 需要网络策略放通。 |
|
||||
| Syslog 目标端口 | 是 | 514 或现场指定 | 需要确认 UDP/TCP。 |
|
||||
| 只读凭据 | 是 | 凭据引用 | 不记录真实 community、用户名或密码。 |
|
||||
| 采集频率 | 是 | 60 秒或 300 秒 | 高频接口指标进入时序库。 |
|
||||
| 维护窗口 | 否 | 每周日 00:00-02:00 | 维护期默认抑制告警。 |
|
||||
| 所属业务 | 否 | HIS 网络域 | 用于业务系统视图。 |
|
||||
| 所属机房/机柜 | 否 | 待导入 | 支持后续 3D 机房联动。 |
|
||||
|
||||
## 4. SNMP 采集指标基线
|
||||
|
||||
| 指标编码 | 指标名称 | 维度 | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `device.uptime` | 设备运行时长 | 设备 | 识别重启和稳定性。 |
|
||||
| `device.cpu.usage` | CPU 使用率 | 设备 | 触发性能告警。 |
|
||||
| `device.memory.usage` | 内存使用率 | 设备 | 触发性能告警。 |
|
||||
| `interface.oper_status` | 接口运行状态 | 接口 | 识别接口 down/up。 |
|
||||
| `interface.admin_status` | 接口管理状态 | 接口 | 区分人为关闭和异常 down。 |
|
||||
| `interface.in_bps` | 入方向速率 | 接口 | 趋势图、报表、大屏。 |
|
||||
| `interface.out_bps` | 出方向速率 | 接口 | 趋势图、报表、大屏。 |
|
||||
| `interface.in_errors` | 入方向错误包 | 接口 | 识别链路质量问题。 |
|
||||
| `interface.out_errors` | 出方向错误包 | 接口 | 识别链路质量问题。 |
|
||||
| `interface.discards` | 丢弃包 | 接口 | 流量拥塞分析。 |
|
||||
|
||||
OID 不在本文写死。实现时应把 OID 放入资源类型模板和 H3C 指标模板,支持按型号覆盖。
|
||||
|
||||
## 5. Trap 字典基线
|
||||
|
||||
| 事件类型 | 期望字段 | 默认级别 | 转换规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| 设备重启 | 设备标识、发生时间、重启原因 | 高 | 生成设备重启告警。 |
|
||||
| 接口 down | 设备标识、接口索引、接口名称、状态 | 高 | 关联接口资源,生成接口故障告警。 |
|
||||
| 接口 up | 设备标识、接口索引、接口名称、状态 | 信息 | 匹配未恢复接口告警并恢复。 |
|
||||
| 电源异常 | 设备标识、电源槽位、状态 | 高 | 生成硬件告警。 |
|
||||
| 风扇异常 | 设备标识、风扇槽位、状态 | 中 | 生成硬件告警。 |
|
||||
| 温度异常 | 设备标识、传感器、温度、阈值 | 中 | 生成环境或硬件告警。 |
|
||||
| 认证失败 | 来源、用户名或协议摘要 | 中 | 生成安全类告警或审计事件。 |
|
||||
|
||||
未识别 Trap 必须进入 `raw_events`,状态为 `unparsed`,允许补字典后重放。
|
||||
|
||||
## 6. Syslog 样例要求
|
||||
|
||||
现场至少提供以下样例,每类不少于 3 条原始文本:
|
||||
|
||||
| 类别 | 用途 | 解析目标 |
|
||||
| --- | --- | --- |
|
||||
| 接口状态变化 | 验证链路故障和恢复 | 设备、接口、状态、发生时间。 |
|
||||
| 设备重启或板卡变化 | 验证硬件事件 | 设备、模块、原因、级别。 |
|
||||
| 登录或认证失败 | 验证安全事件 | 来源 IP、账号、失败原因。 |
|
||||
| 配置变更 | 验证审计关联 | 操作人、变更对象、时间。 |
|
||||
| 协议邻居变化 | 验证网络事件 | 协议、邻居、状态。 |
|
||||
|
||||
Syslog 解析失败时按 `docs/P1故障救援策略.md` 的未解析事件流程处理。
|
||||
|
||||
## 7. 采集与告警规则
|
||||
|
||||
| 规则 | 默认条件 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| 设备不可达 | 连续 3 次 SNMP 失败 | 生成采集失败内部事件,资源详情显示失败原因。 |
|
||||
| CPU 高 | 连续 5 分钟超过阈值 | 生成性能告警。 |
|
||||
| 内存高 | 连续 5 分钟超过阈值 | 生成性能告警。 |
|
||||
| 接口 down | 管理状态 up 且运行状态 down | 生成接口故障告警。 |
|
||||
| 接口错误包突增 | 错误包增量超过阈值 | 生成链路质量告警。 |
|
||||
| Trap 未解析 | 字典未命中 | 进入未解析池,不直接告警。 |
|
||||
| Syslog 命中屏蔽 | 命中维护窗口或屏蔽策略 | 记录抑制事件,不通知。 |
|
||||
|
||||
阈值必须支持按设备、接口和业务系统覆盖。
|
||||
|
||||
## 8. 数据模型映射
|
||||
|
||||
| OPS 对象 | H3C 数据来源 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `resources` | 设备基本信息、管理 IP、型号 | 厂商固定为 H3C/华三或现场实际值。 |
|
||||
| `metric_definitions` | 指标模板 | 不同型号可覆盖 OID。 |
|
||||
| `metric_series` | 接口和设备指标 | 标签包含接口名、接口索引。 |
|
||||
| `raw_events` | Trap、Syslog、采集失败 | 保留原始报文和解析状态。 |
|
||||
| `alerts` | 规则命中结果 | 关联资源、接口、业务系统。 |
|
||||
| `audit_logs` | 规则变更、字典变更、重放 | 支撑验收追溯。 |
|
||||
|
||||
## 9. 验收脚本
|
||||
|
||||
1. 录入一台 H3C/华三网络设备资源,绑定只读凭据引用。
|
||||
2. 执行 SNMP 连通性测试,记录成功或失败原因。
|
||||
3. 展示设备基本信息、CPU、内存、接口状态和接口流量。
|
||||
4. 投递或接收一条接口 down Trap/Syslog 样例。
|
||||
5. 在原始事件池查看原文、解析结果和规则命中。
|
||||
6. 在告警中心查看接口故障告警。
|
||||
7. 投递或接收恢复样例,确认告警恢复。
|
||||
8. 查看资源详情、报表、大屏和审计日志中的证据。
|
||||
|
||||
## 10. 未决项
|
||||
|
||||
| 未决项 | 当前状态 | 影响 | 处理建议 |
|
||||
| --- | --- | --- | --- |
|
||||
| 具体设备型号 | 待现场提供 | 影响 OID 模板和接口面板 | 验收前必须填入现场确认表。 |
|
||||
| SNMP v2c 或 v3 | 待现场确认 | 影响凭据模型和安全配置 | 优先使用只读权限,v3 优先。 |
|
||||
| Trap/Syslog 样例 | 待现场提供 | 影响解析规则 | 至少提供可解析、未解析、恢复样例。 |
|
||||
| 模拟事件是否可验收 | 待院方确认 | 影响正式演示方式 | 会前确认可控样例边界。 |
|
||||
| 网络策略 | 待现场确认 | 影响采集连通性 | 明确 161、162、Syslog 端口策略。 |
|
||||
|
||||
172
docs/P1故障救援策略.md
Normal file
172
docs/P1故障救援策略.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# OPS P1 故障救援策略
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文定义第 1 阶段必须覆盖的平台自身故障处理策略,重点解决采集失败、Trap/Syslog 解析失败、通知失败和派单失败不能静默的问题。
|
||||
|
||||
本文用于指导 `server/internal/logic/`、`server/internal/models/`、`web/src/views/` 和验收脚本实现。相关状态机以 `docs/首期数据模型与状态机.md` 为准。
|
||||
|
||||
## 2. 通用原则
|
||||
|
||||
| 原则 | 要求 |
|
||||
| --- | --- |
|
||||
| 错误可见 | 平台内部失败必须进入可查询记录,不允许只写进控制台日志。 |
|
||||
| 业务不中断 | 通知失败、自动派单失败不能阻断告警确认、人工派单和工单处理。 |
|
||||
| 可重试 | 外部依赖、网络抖动、临时超时类错误必须支持自动重试和人工重试。 |
|
||||
| 可降级 | 解析失败、通知失败、派单失败要进入待处理队列或未解析池,而不是丢弃。 |
|
||||
| 可审计 | 状态变化、重试、人工修正、规则重放必须写审计日志并带 `traceId`。 |
|
||||
| 可测试 | 每一类救援路径都要有接口测试或端到端测试覆盖。 |
|
||||
|
||||
## 3. 统一错误分类
|
||||
|
||||
| 错误码 | 场景 | 用户提示 | 默认处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| `COLLECT_UNREACHABLE` | 设备不可达、端口不通 | 设备不可达,保留最近成功采集时间 | 按策略重试,连续失败后生成平台内部事件 |
|
||||
| `COLLECT_AUTH_FAILED` | 凭据错误、权限不足 | 凭据不可用,请检查凭据引用和设备权限 | 停止高频重试,提示人工修正 |
|
||||
| `COLLECT_TIMEOUT` | SNMP、Agent、数据库、自定义 SQL 超时 | 采集超时,已进入重试队列 | 指数退避重试,标记采集延迟 |
|
||||
| `EVENT_PARSE_FAILED` | Trap/Syslog 字典缺失或格式不匹配 | 事件未解析,可补充规则后重放 | 进入未解析事件池 |
|
||||
| `NOTIFY_CHANNEL_FAILED` | 站内消息、短信、邮件发送失败 | 通知发送失败,不影响告警处理 | 按渠道重试,允许人工补发 |
|
||||
| `TICKET_ASSIGN_FAILED` | 无匹配处理人、权限不足、重复派单 | 自动派单失败,请人工选择处理人 | 回退待分派状态 |
|
||||
| `STATE_CONFLICT` | 并发确认、关闭、转交 | 状态已变化,请刷新后重试 | 拒绝本次操作并返回当前状态 |
|
||||
| `RATE_LIMITED` | 告警风暴、通知过载 | 已限流,部分通知合并发送 | 启用限流和摘要通知 |
|
||||
|
||||
所有 API 错误响应必须包含 `code`、`message`、`traceId` 和 `suggestion`。
|
||||
|
||||
## 4. 采集失败救援
|
||||
|
||||
### 4.1 失败来源
|
||||
|
||||
| 来源 | 典型原因 | 是否自动重试 | 是否生成内部事件 |
|
||||
| --- | --- | --- | --- |
|
||||
| SNMP 轮询 | 设备不可达、团体字错误、OID 不支持 | 是 | 连续失败达到阈值后生成 |
|
||||
| H3C/华三接口指标 | 接口索引变化、接口 down、设备重启 | 是 | 是 |
|
||||
| 主机 Agent | Agent 离线、版本不兼容、主机防火墙 | 是 | 是 |
|
||||
| 数据库监控 | 账号权限不足、SQL 超时、连接池耗尽 | 按错误类型 | 是 |
|
||||
| URL/API 探测 | 5xx、超时、DNS 失败、证书失败 | 是 | 是 |
|
||||
|
||||
### 4.2 重试策略
|
||||
|
||||
| 错误类型 | 自动重试 | 退避策略 | 人工动作 |
|
||||
| --- | --- | --- | --- |
|
||||
| 网络不可达 | 3 次 | 30 秒、1 分钟、5 分钟 | 检查网络和防火墙 |
|
||||
| 协议超时 | 3 次 | 10 秒、30 秒、1 分钟 | 调整超时或采集周期 |
|
||||
| 凭据错误 | 0 次或 1 次确认 | 不持续重试 | 修改凭据引用 |
|
||||
| 指标部分缺失 | 不重试整个任务 | 标记缺失指标 | 检查模板与设备型号 |
|
||||
| 采集器异常 | 2 次 | 10 秒、30 秒 | 查看采集器日志 |
|
||||
|
||||
### 4.3 降级表现
|
||||
|
||||
- 资源列表显示 `collect_status=failed` 或 `partial_success`。
|
||||
- 资源详情显示最近成功采集时间、失败原因和失败次数。
|
||||
- 时序图只展示已有样本,并标记缺口时间段。
|
||||
- 首页、大屏和报表显示“数据过期”或“部分指标不可用”,不使用假数据填充。
|
||||
|
||||
### 4.4 日志与审计
|
||||
|
||||
每次失败至少记录:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `collector_task_id` | 采集任务 ID。 |
|
||||
| `resource_id` | 资源 ID。 |
|
||||
| `error_code` | 统一错误码。 |
|
||||
| `error_message` | 原始失败摘要,敏感信息脱敏。 |
|
||||
| `retry_count` | 当前重试次数。 |
|
||||
| `last_success_at` | 最近成功采集时间。 |
|
||||
| `trace_id` | 请求或任务链路 ID。 |
|
||||
|
||||
## 5. Trap/Syslog 解析失败救援
|
||||
|
||||
### 5.1 处理流程
|
||||
|
||||
```text
|
||||
+-----------+ +-------------+ +----------------+
|
||||
| 接收原文 | ---> | 写 raw_events | ---> | 尝试字典/规则解析 |
|
||||
+-----------+ +-------------+ +----------------+
|
||||
|
|
||||
+-------------------+-------------------+
|
||||
v v
|
||||
+-------------+ +--------------+
|
||||
| parsed | | unparsed |
|
||||
| 转告警或抑制 | | 待补规则重放 |
|
||||
+-------------+ +--------------+
|
||||
```
|
||||
|
||||
### 5.2 未解析事件要求
|
||||
|
||||
| 项目 | 要求 |
|
||||
| --- | --- |
|
||||
| 入库 | 原始报文必须先写入 `raw_events.payload_json` 或原文存储字段。 |
|
||||
| 状态 | `parse_status=unparsed`,不能直接删除。 |
|
||||
| 可见性 | 告警中心或事件池显示未解析数量、来源、接收时间。 |
|
||||
| 重放 | 补充 Trap 字典、OID 描述或 Syslog 规则后,可对选定事件重放解析。 |
|
||||
| 审计 | 规则新增、规则修改、重放动作必须写审计。 |
|
||||
|
||||
### 5.3 用户提示
|
||||
|
||||
| 场景 | 页面提示 | 建议动作 |
|
||||
| --- | --- | --- |
|
||||
| OID 字典缺失 | 未识别 Trap OID | 补充 OID 描述和级别映射 |
|
||||
| Syslog 格式不匹配 | 日志规则未命中 | 创建或调整解析规则 |
|
||||
| 资源无法匹配 | 事件来源未绑定资源 | 绑定来源 IP、主机名或设备标识 |
|
||||
| 恢复事件不完整 | 无法判断恢复关系 | 补充恢复规则或人工关闭 |
|
||||
|
||||
## 6. 通知失败救援
|
||||
|
||||
### 6.1 渠道策略
|
||||
|
||||
| 渠道 | 首期要求 | 失败处理 |
|
||||
| --- | --- | --- |
|
||||
| 站内消息 | 必须支持 | 写失败记录,允许重新发送 |
|
||||
| 短信 | 必须支持测试账号 | 记录第三方返回码,按渠道限流 |
|
||||
| 邮件 | 必须支持测试 SMTP | 记录 SMTP 错误摘要,支持人工补发 |
|
||||
|
||||
通知失败不改变告警和工单主状态。告警详情必须展示通知记录和失败原因。
|
||||
|
||||
### 6.2 重试与限流
|
||||
|
||||
| 场景 | 策略 |
|
||||
| --- | --- |
|
||||
| 临时网络失败 | 自动重试 3 次,间隔 1 分钟、5 分钟、15 分钟。 |
|
||||
| 第三方限流 | 暂停该渠道,改为站内消息和摘要通知。 |
|
||||
| 收件人无效 | 不自动重试,提示维护接收人配置。 |
|
||||
| 告警风暴 | 同一资源同一规则在去重窗口内合并通知。 |
|
||||
| 高级别告警 | 可突破普通限流,但仍要记录发送频率和接收人。 |
|
||||
|
||||
## 7. 派单失败救援
|
||||
|
||||
### 7.1 自动派单失败
|
||||
|
||||
| 失败原因 | 救援动作 | 用户可见表现 |
|
||||
| --- | --- | --- |
|
||||
| 无匹配处理人 | 回退为待分派,显示推荐处理组为空 | 告警详情显示“请选择处理人” |
|
||||
| 处理人无权限 | 回退为待分派,提示权限不匹配 | 派单失败记录可见 |
|
||||
| 重复派单 | 拒绝创建新未关闭工单,返回已有关联工单 | 告警详情跳转已有工单 |
|
||||
| 工单状态冲突 | 拒绝更新,返回当前工单状态 | 页面提示刷新 |
|
||||
| 规则配置错误 | 禁用该自动派单规则或标记异常 | 策略页显示异常规则 |
|
||||
|
||||
### 7.2 人工救援
|
||||
|
||||
- 告警详情提供“重新派单”和“手动创建工单”入口。
|
||||
- 工单列表支持筛选 `source=dispatch_failed`。
|
||||
- 派单规则异常必须出现在系统管理或策略页,不只存在于日志。
|
||||
|
||||
## 8. 测试要求
|
||||
|
||||
| 测试项 | 类型 | 通过标准 |
|
||||
| --- | --- | --- |
|
||||
| 采集凭据错误 | 后端接口测试 | 返回 `COLLECT_AUTH_FAILED`,资源采集状态失败,写审计。 |
|
||||
| SNMP 超时 | 后端集成测试 | 触发重试,连续失败后生成内部事件。 |
|
||||
| Trap 未解析 | 接口测试 | `raw_events` 保留原文,状态为 `unparsed`,补规则后可重放。 |
|
||||
| 短信发送失败 | 集成测试 | 告警状态不变,通知记录失败,可人工补发。 |
|
||||
| 自动派单无人匹配 | 端到端测试 | 告警保留待分派提示,不创建重复工单。 |
|
||||
| 并发关闭工单 | 后端接口测试 | 只有一个请求成功,其余返回 `STATE_CONFLICT`。 |
|
||||
|
||||
## 9. 验收证据
|
||||
|
||||
| 证据 | 要求 |
|
||||
| --- | --- |
|
||||
| 接口响应 | 保留失败请求、错误码、`traceId` 和建议动作。 |
|
||||
| 页面截图 | 资源详情、未解析事件池、通知记录、派单失败提示。 |
|
||||
| 日志 | 能按 `traceId` 查询采集、解析、通知、派单链路。 |
|
||||
| 数据库记录 | `collector_runs`、`raw_events`、`notification_records`、`ticket_transitions` 有对应记录。 |
|
||||
113
docs/P1测试计划.md
Normal file
113
docs/P1测试计划.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# OPS 第 1 阶段测试计划
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文定义 OPS 第 1 阶段后端、前端和端到端测试范围。当前仓库尚未保留实际 `server/`、`web/` 工程;本文作为编码和联调前测试规格,后续实现必须按本文补齐测试。
|
||||
|
||||
## 2. 测试分层
|
||||
|
||||
| 层级 | 目标 | 工具建议 | 数据要求 |
|
||||
| --- | --- | --- | --- |
|
||||
| 后端单元测试 | 验证状态机、规则匹配、错误救援、权限判断 | Go `testing` | 不访问外部服务 |
|
||||
| 后端接口测试 | 验证 REST API、统一响应、鉴权、审计 | Go `httptest` | SQLite 内存库或任务指定测试库 |
|
||||
| 后端集成测试 | 验证 PostgreSQL 事务语义、时序库适配、通知适配 | Go 测试套件 | 真实 SQLite 内存库或真实测试服务,不 mock 数据库 |
|
||||
| 前端类型检查 | 验证 TypeScript、API 类型和状态分支 | `pnpm type:check` | 不依赖真实后端 |
|
||||
| 前端组件/页面测试 | 验证 loading、empty、error、success、partial、forbidden 状态 | 项目模板现有测试工具 | 可使用固定响应夹具 |
|
||||
| 端到端测试 | 验证资源到告警、通知、工单、报表闭环 | Playwright 或等价工具 | 连接真实后端 API |
|
||||
|
||||
开发期 mock 只能用于前端独立调试。联调、验收和端到端测试必须连接真实后端 API。
|
||||
|
||||
## 3. 后端测试矩阵
|
||||
|
||||
| 能力域 | 必测场景 | 测试类型 | 通过标准 |
|
||||
| --- | --- | --- | --- |
|
||||
| 统一响应 | 成功、业务错误、权限错误、系统错误 | 接口测试 | 响应包含 `code`、`message`、`traceId`,错误含建议动作。 |
|
||||
| 资源模型 | 创建、编辑、停用、退役、非法状态恢复 | 单元 + 接口 | 状态机合法,非法流转拒绝并写审计。 |
|
||||
| 采集任务 | 成功、部分成功、超时、凭据错误、连续失败 | 单元 + 集成 | 失败原因可见,连续失败生成内部事件。 |
|
||||
| 时序适配 | 批量写入、范围查询、聚合、降采样、保留策略 | 集成 | 可按 `resource_id + metric_code + 时间范围` 查询。 |
|
||||
| Trap/Syslog | 解析成功、未解析、补规则重放、屏蔽策略 | 接口 + 集成 | 未解析事件保留原文,重放后状态正确。 |
|
||||
| 告警规则 | 阈值触发、恢复、去重、压缩、抑制、升级 | 单元 + 接口 | 告警状态机合法,策略命中可追踪。 |
|
||||
| 通知 | 站内消息、短信、邮件成功和失败 | 集成 | 通知失败不阻塞告警和工单,记录失败原因。 |
|
||||
| 工单 | 创建、接单、转交、挂起、重启、关闭、并发关闭 | 单元 + 接口 | 非法流转拒绝,并发冲突返回当前状态。 |
|
||||
| 权限 | 功能权限、数据权限、越权访问 | 接口 | 越权返回 forbidden,不泄露敏感数据。 |
|
||||
| 报表 | 空范围、大范围、导出、无权限 | 接口 + 集成 | 大范围查询受控,导出写审计。 |
|
||||
|
||||
## 4. 前端测试矩阵
|
||||
|
||||
| 页面 | 状态覆盖 | 必测动作 |
|
||||
| --- | --- | --- |
|
||||
| 首页总览 | loading、empty、error、success、partial、forbidden、stale | 模块配置保存、局部组件失败、无权限模块隐藏。 |
|
||||
| 综合监控 | loading、empty、error、success、partial、forbidden、stale | 资源筛选、查看详情、采集失败提示、手动刷新。 |
|
||||
| 告警中心 | loading、empty、error、success、partial、forbidden、operating、operation_failed | 确认、忽略、派单、筛选、导出、查看通知记录。 |
|
||||
| 策略与模板 | empty、error、success、forbidden、operation_failed | 创建规则、字段校验、禁用策略、模板变量校验。 |
|
||||
| 通知中心 | loading、empty、error、success、partial、operation_failed | 查看三类渠道记录、失败重发。 |
|
||||
| 工单管理 | loading、empty、error、success、partial、forbidden、operation_failed | 接单、转交、挂起、重启、关闭、并发冲突提示。 |
|
||||
| 报表管理 | loading、empty、error、success、partial、forbidden、stale | 生成、导出、空范围、大范围失败提示。 |
|
||||
| 可视化大屏 | loading、empty、error、success、partial、forbidden、stale | 轮播配置、组件局部失败、刷新时间展示。 |
|
||||
| 权限管理 | empty、error、success、forbidden、operation_failed | 角色授权、数据权限、越权验证。 |
|
||||
|
||||
## 5. 端到端验收测试
|
||||
|
||||
### 5.1 主闭环脚本
|
||||
|
||||
1. 创建或导入主机、H3C/华三网络设备、数据库、URL/API 样例资源。
|
||||
2. 配置采集任务和指标阈值。
|
||||
3. 写入或触发一条可控异常。
|
||||
4. 在原始事件池查看接收、解析、规则命中记录。
|
||||
5. 在告警中心确认告警生成、去重、级别和业务上下文。
|
||||
6. 验证站内消息、短信、邮件三类通知记录。
|
||||
7. 对告警执行确认并派单。
|
||||
8. 工单完成接单、处理、关闭。
|
||||
9. 回到告警详情查看关联工单、处理记录和审计日志。
|
||||
10. 在首页、大屏、报表查看该故障的统计证据。
|
||||
|
||||
### 5.2 失败救援脚本
|
||||
|
||||
| 脚本 | 操作 | 通过标准 |
|
||||
| --- | --- | --- |
|
||||
| 采集失败 | 使用错误凭据触发采集 | 资源详情显示失败原因,写 `collector_runs` 和审计。 |
|
||||
| 未解析 Trap | 投递未知 OID 样例 | `raw_events` 状态为 `unparsed`,补规则后可重放。 |
|
||||
| 通知失败 | 配置不可用短信或邮件测试通道 | 告警可继续处理,通知记录失败,可重试。 |
|
||||
| 自动派单失败 | 创建无匹配处理人的派单规则 | 告警停留待分派,不创建重复工单。 |
|
||||
| 权限拒绝 | 普通账号尝试关闭无权工单 | 返回 forbidden,页面不乐观更新。 |
|
||||
|
||||
## 6. 本地验证命令
|
||||
|
||||
后端至少运行:
|
||||
|
||||
```powershell
|
||||
Set-Location .\server
|
||||
go test ./...
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
前端初始化后至少运行:
|
||||
|
||||
```powershell
|
||||
Set-Location .\web
|
||||
pnpm type:check
|
||||
pnpm lint
|
||||
pnpm build
|
||||
```
|
||||
|
||||
如果因为私有依赖、网络、凭据或环境限制无法运行,需要在交付说明中记录原因、影响范围和替代验证证据。
|
||||
|
||||
## 7. 测试数据要求
|
||||
|
||||
| 数据 | 最小要求 |
|
||||
| --- | --- |
|
||||
| 资源 | 主机、H3C/华三网络设备、数据库、虚拟化、URL/API 各至少 1 类样例。 |
|
||||
| 指标 | CPU、内存、磁盘、接口状态、接口流量、URL 可用性、响应时间。 |
|
||||
| Trap/Syslog | 至少 1 条可解析样例、1 条未解析样例、1 条恢复样例。 |
|
||||
| 告警 | 不同级别至少 3 条,覆盖触发、确认、忽略、恢复、派单。 |
|
||||
| 通知 | 站内消息、短信、邮件成功和失败各至少 1 条记录。 |
|
||||
| 工单 | 创建、接单、转交、挂起、重启、关闭全路径样例。 |
|
||||
| 权限 | 管理员、值班人员、普通只读用户各 1 个。 |
|
||||
|
||||
## 8. 质量门禁
|
||||
|
||||
- 后端核心状态机和规则逻辑必须有单元测试。
|
||||
- 所有核心接口必须覆盖成功、失败、无权限、非法状态。
|
||||
- 前端核心页面必须覆盖 `docs/首期UI状态覆盖.md` 中列出的状态。
|
||||
- 端到端测试必须能证明主闭环,不得只校验静态页面存在。
|
||||
- 测试不能使用 mock 数据库替代真实数据库行为。
|
||||
842
docs/integrated-ops-platform-blueprint-design.md
Normal file
842
docs/integrated-ops-platform-blueprint-design.md
Normal file
@@ -0,0 +1,842 @@
|
||||
<!-- /autoplan restore point: C:\Users\27105\.gstack\projects\ops\main-autoplan-restore-20260621-181451.md -->
|
||||
<!-- /autoplan restore point: C:\Users\27105\.gstack\projects\ops\main-autoplan-restore-20260621-180424.md -->
|
||||
<!-- /autoplan restore point: C:\Users\27105\.gstack\projects\ops\main-autoplan-restore-20260621-151741.md -->
|
||||
# 一体化运维平台完整蓝图与分期落地方案
|
||||
|
||||
生成来源:`/office-hours`
|
||||
生成日期:2026-06-21
|
||||
分支:`main`
|
||||
仓库:`ops`
|
||||
状态:草案
|
||||
模式:企业内部项目交付
|
||||
|
||||
## 问题陈述
|
||||
|
||||
当前项目已经从招标文件和菜单规划表中提取出 33 条一体化运维平台需求,覆盖资源监控、告警、工单、拓扑、IP 地址管理、数据中心、资产、机柜、知识库、报表、大屏、权限和系统管理。
|
||||
|
||||
真正的问题不是缺少功能清单,而是需求按资源类型和菜单组织,容易形成“页面很多、闭环很弱”的平台。医院当前高概率存在多工具监控、手工台账、微信群或人工派单混用的状态。平台需要把分散的运维信号、处理动作和验收证据统一到同一条运维事件链路中。
|
||||
|
||||
## 当前现状
|
||||
|
||||
基于本次需求讨论输入,当前假设的现场状态为:
|
||||
|
||||
- 多套监控或厂商工具并存,告警来源分散。
|
||||
- Excel 或人工方式维护设备、IP、机柜、业务系统等台账。
|
||||
- 告警通知和故障处理依赖人工沟通,缺少统一受理、派单、升级、关闭和复盘记录。
|
||||
- 大屏和报表容易变成展示层,若没有真实事件链路支撑,验收时会被追问数据来源、处理过程和审计证据。
|
||||
|
||||
该假设需要在现场调研阶段确认,但足以指导蓝图设计:平台必须围绕“资源上下文 + 事件生命周期 + 处理闭环证据”组织。
|
||||
|
||||
## 会议结论补充
|
||||
|
||||
本轮会议对部分前期待确认项给出了更明确边界:
|
||||
|
||||
- 现场设备以 H3C/华三为主,厂商适配优先级应向 H3C 设备倾斜;第 1 阶段不追求全厂商覆盖。
|
||||
- 3D 机房前端已经外包,OPS 侧不承担 3D 前端实现,只需要提供数据中心、机房、机柜、U 位、设备和告警状态等后端接口。
|
||||
- Windows PowerShell 主要用于本地开发、调试和文档命令示例;验收部署环境需要面向 Linux 与麒麟系统。
|
||||
- 医院当前有老运维平台,但首期不做迁移或集成;后续可能需要做历史数据迁移。
|
||||
- 告警渠道中,平台站内消息、短信、邮件需要优先打通。
|
||||
- 暂不考虑独立移动端;前端页面需要具备移动端适配能力。
|
||||
- 数据中心、机柜、设备等已有现有数据,但短期内拿不到;首期需要支持后续导入和接口预留,不能依赖这些数据才能完成验收。
|
||||
- 验收是否允许使用模拟 Trap、模拟 Syslog 和可控 URL/API 故障仍需确认。
|
||||
- 建议引入国产或国内生态成熟的时序数据库,用于承载高频指标、采集样本和后续趋势分析。
|
||||
|
||||
## 目标用户与最小可验收闭环
|
||||
|
||||
### 核心用户
|
||||
|
||||
- 一线值班运维人员:需要及时知道哪些告警要处理、归属谁、下一步怎么做。
|
||||
- 网络、主机、数据库、虚拟化等专项管理员:需要看到所属资源指标、拓扑关系、历史趋势和故障上下文。
|
||||
- 运维负责人:需要看到告警处理效率、未恢复风险、资源健康、报表和大屏。
|
||||
- 系统管理员:需要管理用户、角色、权限、字典、系统参数和日志审计。
|
||||
|
||||
### 最窄可验收闭环
|
||||
|
||||
以一个关键业务系统或关键资源组为样例,完成:
|
||||
|
||||
1. 纳管主机、网络设备、数据库、虚拟化或 URL/API 等关键资源。
|
||||
2. 采集指标、Syslog、SNMP Trap 或探测结果。
|
||||
3. 触发告警并进入统一事件池。
|
||||
4. 执行去重、压缩、屏蔽、抑制、级别判断和通知路由。
|
||||
5. 值班人员确认、忽略、派单或升级。
|
||||
6. 工单处理并关闭,沉淀处理记录和关联知识。
|
||||
7. 首页、大屏、报表、历史告警和审计日志能证明完整过程。
|
||||
|
||||
## 约束条件
|
||||
|
||||
- 蓝图要覆盖招标需求,但落地路线不能把 33 条需求全部压进同一期。
|
||||
- 现场设备以 H3C/华三为主,但仍需确认具体型号、协议开放情况和账号权限。
|
||||
- 平台事务数据仍面向 PostgreSQL 设计;高频指标和采集样本需要引入时序数据库选型,不把所有时序数据压入 PostgreSQL。
|
||||
- 开发期可使用 SQLite 内存库做真实测试,不以 mock 数据库代替行为验证。
|
||||
- 模板目录只能作为初始化来源,实际业务代码应落在 `server/`、`web/`、`deploy/`。
|
||||
- 需求、架构和验收文档需要中文维护;本地开发命令示例使用 Windows PowerShell,验收部署需另行提供 Linux/麒麟适配说明。
|
||||
|
||||
## 设计前提
|
||||
|
||||
1. 平台蓝图采用“双主线”:资源监控覆盖 + 告警事件闭环,两者并行。
|
||||
2. 告警事件生命周期是业务主轴,资源、资产、拓扑、机柜、IPAM 是上下文,不是孤立菜单。
|
||||
3. 资源监控蓝图完整覆盖,落地路线从主机、H3C/华三网络设备、数据库、虚拟化、URL/业务可用性等关键样例开始。
|
||||
4. 告警能力必须包含去重、压缩或归并、屏蔽、抑制、升级、确认、忽略、派单、历史和报表证据。
|
||||
5. 工单、知识库、报表、大屏围绕同一条事件链路产生证据。
|
||||
6. 拓扑、IPAM、机柜、资产、动环属于完整蓝图必备能力,但按资源上下文和影响分析能力分期接入;3D 机房前端由外部团队负责,OPS 侧负责后端接口和数据模型。
|
||||
7. 验收设计从“菜单是否存在”升级为“故障从发现到关闭是否可追踪、可审计、可报表”。
|
||||
|
||||
## 外部产品校准
|
||||
|
||||
外部校准显示,同类 ITOM/AIOps 平台的核心价值正在从“展示更多指标”转向“服务上下文、告警降噪、事件归并、工单处置和持续复盘”。
|
||||
|
||||
- Grafana 告警最佳实践强调:告警应面向一线响应人,必须可理解、可行动;基础设施信号适合辅助诊断,不应全部作为高优先级打扰;需要分组、抑制抖动和定期复盘。参考:<https://grafana.com/docs/grafana/latest/alerting/guides/best-practices/>
|
||||
- ServiceNow ITOM 强调跨监控工具聚合事件、服务映射、去重、关联和突出可行动告警。参考:<https://www.servicenow.com/products/it-operations-management.html>
|
||||
- Atlassian 事故管理强调可重复的记录、诊断、解决和活动留痕。参考:<https://www.atlassian.com/incident-management>
|
||||
- PagerDuty Event Orchestration 把动态路由、事件规则和事件编排作为告警治理核心能力。参考:<https://support.pagerduty.com/main/docs/event-orchestration>
|
||||
- TDengine TSDB 官方文档说明其面向物联网、工业互联网、金融、IT 运维等时序场景,提供 SQL、连接器、集群、流式计算和数据订阅能力。参考:<https://docs.taosdata.com/>
|
||||
- Apache IoTDB 官方介绍其定位为工业物联网时序数据库,支持边云协同、多协议兼容、高压缩比、高吞吐读写和 Grafana 等生态集成。参考:<https://iotdb.apache.org/>
|
||||
- openGemini 官方介绍其聚焦可观测性海量数据存储与分析,支持指标、日志、链路数据存储,具备分布式集群、高基数和压缩能力。参考:<https://opengemini.org/>
|
||||
|
||||
结论:招标需求应按事件生命周期重组,而不是按菜单平铺;指标和采集样本需要单独进行国产/国内生态时序数据库选型,避免 PostgreSQL 同时承担事务数据和高频时序数据压力。
|
||||
|
||||
## 备选方案
|
||||
|
||||
### 方案 A:事件生命周期主轴平台
|
||||
|
||||
以“资源纳管 -> 指标/日志/Trap -> 告警事件 -> 降噪归并 -> 通知/升级 -> 工单处理 -> 知识沉淀 -> 报表/大屏”为平台骨架。
|
||||
|
||||
优点:
|
||||
|
||||
- 同时满足资源监控覆盖和告警闭环。
|
||||
- 验收故事强,能演示故障从产生到关闭的完整证据链。
|
||||
- 33 条需求可以归入统一架构,而不是散成菜单清单。
|
||||
|
||||
缺点:
|
||||
|
||||
- 对领域模型要求高,资源、指标、事件、告警、策略、工单要一次设计清楚。
|
||||
- 前期接口、状态机和数据模型设计工作较重。
|
||||
- 需要额外维护招标需求映射矩阵,向非技术干系人解释每个需求落点。
|
||||
|
||||
### 方案 B:模块矩阵型平台
|
||||
|
||||
按需求文档中的模块直接建设:综合监控、告警管理、工单、拓扑、IPAM、资产、知识库、报表、大屏、权限、系统管理独立推进。
|
||||
|
||||
优点:
|
||||
|
||||
- 和招标清单结构一致,容易解释“每个模块都有”。
|
||||
- 适合做投标响应和逐项验收映射。
|
||||
- 模块可以表面并行拆分。
|
||||
|
||||
缺点:
|
||||
|
||||
- 容易形成模块孤岛。
|
||||
- 后续补事件链路时会返工。
|
||||
- 开发优先级不清晰,容易先完成页面而不是闭环。
|
||||
|
||||
### 方案 C:分层能力底座平台
|
||||
|
||||
按采集接入层、资源/资产模型层、事件治理层、应用体验层建立底座,再把监控、告警、工单、拓扑、大屏、报表作为上层应用。
|
||||
|
||||
优点:
|
||||
|
||||
- 长期架构更稳,适合扩展多厂商、多协议、多资源类型。
|
||||
- 能统一 SNMP、Syslog、Trap、Agent、URL 探测、数据库脚本等接入方式。
|
||||
- 适合完整蓝图和多期演进。
|
||||
|
||||
缺点:
|
||||
|
||||
- 单独作为交付路线过重,容易离验收演示太远。
|
||||
- 如果没有事件生命周期牵引,可能先做底座但缺少业务价值证明。
|
||||
|
||||
## 推荐方案
|
||||
|
||||
采用方案 A,并吸收方案 C 作为内部架构原则。
|
||||
|
||||
工程和产品表达上,平台以事件生命周期为主轴。内部架构上,按能力分层建设,保证后续多资源、多协议、多厂商扩展不推倒重来。方案 B 只作为需求映射和验收矩阵,不作为工程主结构。
|
||||
|
||||
## 蓝图架构
|
||||
|
||||
### 1. 采集接入层
|
||||
|
||||
职责:把不同来源的指标、日志、Trap、探测结果和手工录入数据接入平台。
|
||||
|
||||
能力范围:
|
||||
|
||||
- 主机 Agent 或无代理采集。
|
||||
- SNMP 轮询和 SNMP Trap 接收,第 1 阶段优先覆盖 H3C/华三设备的常见指标、接口状态和 Trap 样例。
|
||||
- Syslog 接收和规则解析。
|
||||
- URL/API、端口、服务、进程、Webservice 探测。
|
||||
- 数据库连接和自定义 SQL 脚本监控。
|
||||
- 中间件、虚拟化、存储、安全设备、动环设备适配。
|
||||
- 跨网代理和主动/被动数据推送。
|
||||
|
||||
关键设计:
|
||||
|
||||
- 所有采集结果先归一为 `MetricSample`、`LogEvent`、`TrapEvent`、`ProbeResult`、`DiscoveryResult`。
|
||||
- 采集配置、凭据引用、调度周期、失败重试和最近采集状态必须可审计。
|
||||
- 采集失败本身也应能产生平台内部告警。
|
||||
|
||||
### 1.5. 指标与时序数据存储层
|
||||
|
||||
职责:承载高频指标样本、探测结果、采集状态和趋势分析数据,避免事务库承担所有时序写入压力。
|
||||
|
||||
存储分工:
|
||||
|
||||
- PostgreSQL:保存资源、资产、告警、事件、工单、策略、权限、审计、报表配置等事务数据。
|
||||
- 时序数据库:保存指标样本、探测样本、接口流量、采集健康度、后续容量趋势和性能趋势。
|
||||
- 对象存储或文件存储:保存导出报表、附件、截图、录像和大屏快照等非结构化证据。
|
||||
|
||||
候选方向:
|
||||
|
||||
- TDengine TSDB:适合 IT 运维、物联网和工业互联网等时序场景,需验证麒麟部署、Go 连接器、集群运维和授权模式。
|
||||
- Apache IoTDB:适合工业物联网和设备树结构数据,需验证资源模型映射、SQL 能力、Grafana 集成和运维复杂度。
|
||||
- openGemini:适合可观测性海量指标、日志和链路数据,需验证项目成熟度、麒麟适配、社区活跃度和长期维护风险。
|
||||
|
||||
选型原则:
|
||||
|
||||
- 必须支持 Linux 与麒麟部署,能在验收环境稳定运行。
|
||||
- 必须提供 Go 后端可用的连接方式,支持批量写入、范围查询、聚合、降采样和保留策略。
|
||||
- 必须支持与 PostgreSQL 中的资源 ID、指标编码、时间范围关联查询。
|
||||
- 首期不把时序库能力暴露为复杂产品卖点,只作为监控指标和报表性能底座。
|
||||
|
||||
### 2. 资源与资产模型层
|
||||
|
||||
职责:提供统一上下文,回答“这个告警来自什么资源、归属哪个业务、影响哪些对象”。
|
||||
|
||||
核心对象:
|
||||
|
||||
- `Resource`:主机、网络设备、安全设备、存储、数据库、中间件、虚拟化对象、URL/API、动环设备等统一资源。
|
||||
- `ResourceType`:资源类型和指标模板。
|
||||
- `MetricDefinition`:指标编码、单位、阈值建议、采集方式。
|
||||
- `MetricSeries`:时序数据中的指标序列映射,关联资源、指标定义、采集任务和时序库存储位置。
|
||||
- `BusinessSystem`:HIS、LIS、PACS、EMR 等业务系统。
|
||||
- `TopologyNode`、`TopologyLink`:网络拓扑和业务拓扑。
|
||||
- `Asset`、`DataCenter`、`Room`、`Rack`、`RackUnit`:资产、数据中心、机房、机柜和 U 位。
|
||||
- `IpSubnet`、`IpAddress`、`IpLease`、`IpConflict`:IP 地址管理。
|
||||
|
||||
关键设计:
|
||||
|
||||
- 资源必须能绑定业务系统、组织、责任人、位置、资产和权限范围。
|
||||
- 资产台账和监控资源要允许先分离后关联,避免现场台账不准时卡住监控接入。
|
||||
- 拓扑不只用于展示,还要为告警影响分析和归并提供上下文。
|
||||
- 数据中心、机柜、设备等现有数据短期拿不到时,系统必须允许先纳管监控资源,后续再通过导入或接口补齐资产位置关系。
|
||||
|
||||
### 3. 事件治理层
|
||||
|
||||
职责:把原始异常转为可处理、可路由、可归并、可关闭的运维事件。
|
||||
|
||||
核心对象:
|
||||
|
||||
- `RawEvent`:来自指标阈值、Syslog、Trap、探测失败、采集失败的原始事件。
|
||||
- `AlertRule`:阈值、匹配条件、持续时间、恢复条件、作用范围。
|
||||
- `Alert`:平台告警实例。
|
||||
- `Incident`:归并后的事件或故障单元,可关联多个告警。
|
||||
- `SilencePolicy`:屏蔽策略和屏蔽时间段。
|
||||
- `DedupRule`:去重规则。
|
||||
- `CorrelationRule`:压缩、依赖、抑制和关联规则。
|
||||
- `EscalationPolicy`:升级策略。
|
||||
- `NotificationPolicy`:通知路由。
|
||||
|
||||
状态建议:
|
||||
|
||||
- `Alert`: firing、acknowledged、ignored、recovered、expired。
|
||||
- `Incident`: open、assigned、in_progress、suspended、resolved、closed。
|
||||
- `Ticket`: created、accepted、transferred、withdrawn、suspended、restarted、closed。
|
||||
|
||||
关键设计:
|
||||
|
||||
- 告警不等于工单。告警是信号,事件是归并后的处置对象,工单是执行记录。
|
||||
- 所有降噪策略必须留下规则命中和处理轨迹,便于验收和追责。
|
||||
- 高级别告警优先、升级、通知失败重试、通知历史都要可查;第 1 阶段优先实现平台站内消息、短信、邮件三类渠道。
|
||||
|
||||
### 4. 流程闭环层
|
||||
|
||||
职责:把事件处理从“人工沟通”变成平台内可追踪流程。
|
||||
|
||||
能力范围:
|
||||
|
||||
- 告警确认、忽略、恢复、失效、导出。
|
||||
- 手动创建工单、告警直接派单、策略自动派单。
|
||||
- 接单、转交、撤回、挂起、重启、关闭。
|
||||
- 知识库分类、发布、审核、附件、检测点关联。
|
||||
- 处理记录、通知记录、审核日志、操作日志。
|
||||
|
||||
关键设计:
|
||||
|
||||
- 工单必须能回链告警、事件、资源、业务系统、知识条目和处理人。
|
||||
- 知识库不是独立文档仓库,应优先服务告警检测点和故障类型。
|
||||
- 危险操作或策略变更需要审计日志。
|
||||
|
||||
### 5. 应用体验层
|
||||
|
||||
职责:向不同角色提供可操作的页面和验收证据。
|
||||
|
||||
核心应用:
|
||||
|
||||
- 首页总览:待处理告警、资源健康、告警趋势、网络状态、模块化配置。
|
||||
- 综合监控:资源列表、资源详情、指标趋势、采集状态、模板绑定。
|
||||
- 告警中心:实时告警、历史告警、降噪策略、模板、通知、升级、派单。
|
||||
- 工单管理:事件工单流转和处理记录。
|
||||
- 业务系统视图:业务健康、业务拓扑、影响范围、时间轴、笔记和文档。
|
||||
- 网络拓扑:设备、链路、布局、告警图标、统计、下载。
|
||||
- IP 地址管理:子网、地址状态、扫描、冲突、租约、变更、报表。
|
||||
- 数据中心与资产:数据中心层级、机房、机柜、U 位、资产变更和容量。
|
||||
- 知识库:分类、审核、关联检测点、附件下载。
|
||||
- 报表管理:TopN、故障、服务器、网络设备、流量、统计报告和导出。
|
||||
- 可视化大屏:分组、个人配置、轮播、拓扑、实时告警、接口流量、业务状态。
|
||||
- 用户权限与系统管理:组织、用户、角色、数据权限、字典、参数、消息、日志。
|
||||
|
||||
## 需求映射
|
||||
|
||||
| 能力域 | 覆盖需求 |
|
||||
| --- | --- |
|
||||
| 首页与大屏 | OPS-001、OPS-028 |
|
||||
| 资源监控 | OPS-002 至 OPS-012、OPS-031、OPS-032 |
|
||||
| 网络拓扑与流量 | OPS-013、OPS-014、OPS-015 |
|
||||
| IP 地址管理 | OPS-016、OPS-017 |
|
||||
| 告警治理 | OPS-018、OPS-019、OPS-020、OPS-021 |
|
||||
| 工单闭环 | OPS-022 |
|
||||
| 数据中心、机柜、资产 | OPS-023、OPS-024、OPS-025 |
|
||||
| 知识库 | OPS-026 |
|
||||
| 报表 | OPS-027 |
|
||||
| 用户权限、系统管理、日志 | OPS-029、OPS-030 |
|
||||
| 业务系统视图 | OPS-011、OPS-033 |
|
||||
|
||||
## 分期路线
|
||||
|
||||
### 第 0 阶段:现场确认与验收基线
|
||||
|
||||
目标:把蓝图变成可执行范围。
|
||||
|
||||
交付物:
|
||||
|
||||
- 现场资源清单和样例设备清单。
|
||||
- 通知渠道确认:平台站内消息、短信、邮件为第 1 阶段优先渠道;企业微信、钉钉、电话等作为后续扩展。
|
||||
- 账号、协议、网络连通性和安全边界确认。
|
||||
- 验收矩阵:每个 OPS 编号对应演示路径、数据准备、通过标准和截图/录像证据。
|
||||
- Linux 与麒麟验收部署环境确认,包括发行版、CPU 架构、数据库部署方式和网络策略。
|
||||
|
||||
### 第 1 阶段:双主线核心闭环
|
||||
|
||||
目标:完成资源监控和告警事件闭环的最小可验收主干。
|
||||
|
||||
范围:
|
||||
|
||||
- 资源模型、资源类型、指标定义、采集任务、采集状态。
|
||||
- 主机、H3C/华三网络设备、数据库、虚拟化、URL/API 至少各一类样例接入或现场可替代样例。
|
||||
- 时序数据库完成选型验证并接入指标样本写入、查询和保留策略。
|
||||
- 指标阈值告警、Syslog/Trap 样例告警、URL/API 探测告警。
|
||||
- 告警去重、压缩或归并、屏蔽、抑制、级别、升级、通知历史。
|
||||
- 平台站内消息、短信、邮件三类通知优先打通。
|
||||
- 告警确认、忽略、恢复、派单和历史查询。
|
||||
- 工单基础流转:创建、接单、转交、挂起、关闭。
|
||||
- 首页总览、告警大屏、基础报表、权限和操作日志。
|
||||
- 3D 机房前端所需后端接口草案,包括数据中心、机房、机柜、U 位、设备坐标/位置、资源健康和告警状态。
|
||||
|
||||
不承诺:
|
||||
|
||||
- 所有厂商全量适配。
|
||||
- 除平台站内消息、短信、邮件之外的所有通知渠道全部打通。
|
||||
- 3D 机房前端实现、复杂 IPAM、全量知识审核流程。
|
||||
- 老运维平台迁移或集成。
|
||||
|
||||
### 第 2 阶段:资源上下文增强
|
||||
|
||||
目标:让告警更容易定位影响范围和责任边界。
|
||||
|
||||
范围:
|
||||
|
||||
- 网络拓扑、业务拓扑和资源关系。
|
||||
- IP 地址管理、自动扫描、冲突、租约、变更记录。
|
||||
- 数据中心、机房、机柜、U 位和资产关联;在现场数据可获得后支持批量导入、校验和资源关联。
|
||||
- 面向外包 3D 机房前端的稳定后端接口和权限控制。
|
||||
- 知识库检测点关联和审核日志。
|
||||
- 更多资源类型和厂商适配。
|
||||
- 老运维平台历史数据迁移评估和一次性导入方案。
|
||||
|
||||
### 第 3 阶段:运营治理与智能化
|
||||
|
||||
目标:从“可处置”升级为“可优化”。
|
||||
|
||||
范围:
|
||||
|
||||
- 告警质量评分、噪声分析、规则优化建议。
|
||||
- 业务健康度、SLA/SLO、容量趋势和风险预测。
|
||||
- 自动化处置建议、脚本执行审批、回滚和审计。
|
||||
- 更完整的大屏编排、专题报表和领导视图。
|
||||
|
||||
## 成功标准
|
||||
|
||||
### 蓝图成功标准
|
||||
|
||||
- 33 条 OPS 需求均能映射到能力域和分期路线。
|
||||
- 每个核心对象都有清晰归属:资源、指标、原始事件、告警、事件、工单、知识、报表、审计。
|
||||
- 平台能解释“为什么这不是菜单堆叠,而是运维闭环”。
|
||||
|
||||
### 第 1 阶段验收标准
|
||||
|
||||
- 至少完成一个端到端故障演示:资源异常触发告警,告警被归并和通知,值班人员确认并派单,工单处理关闭,报表和大屏可追踪全过程。
|
||||
- 至少覆盖主机、H3C/华三网络设备、数据库、虚拟化、URL/API 中的关键样例。
|
||||
- 告警策略、模板、级别、平台站内消息、短信、邮件、升级、历史、导出可演示。
|
||||
- 指标样本进入时序数据库,资源详情、趋势图、报表能从真实采集或可控样例数据读取。
|
||||
- 3D 机房后端接口可返回数据中心、机房、机柜、设备和告警状态样例数据,但不要求 OPS 交付 3D 前端。
|
||||
- 权限、数据权限和操作日志能证明平台安全边界。
|
||||
- 报表和大屏数据来自真实后端记录,不使用静态展示数据作为验收依据。
|
||||
|
||||
## 交付与部署计划
|
||||
|
||||
平台作为内网部署 Web 服务交付。本地开发和调试以 Windows PowerShell 为主,验收部署目标面向 Linux 与麒麟系统。
|
||||
|
||||
建议交付形态:
|
||||
|
||||
- `server/`:Go、Gin、GORM 后端服务,提供 REST API、后台任务、采集调度和事件处理。
|
||||
- `web/`:Vue 3、TypeScript、Pinia、Vue Router、Arco Design Vue 前端。
|
||||
- `deploy/`:数据库迁移、本地初始化脚本、Linux/麒麟部署说明、服务配置示例、烟测脚本。
|
||||
- `server/etc/*.example.yaml`:配置结构示例,不提交真实凭据。
|
||||
|
||||
建议发布流程:
|
||||
|
||||
- 开发环境:SQLite 内存库用于测试,PostgreSQL 作为事务数据目标数据库;时序库可先通过适配层和本地实例验证。
|
||||
- 验收环境:使用 PostgreSQL、选定时序数据库、可控样例设备或现场授权设备,部署到 Linux/麒麟系统。
|
||||
- 每次交付附带版本说明、迁移脚本、验收矩阵、演示脚本和截图/录像证据。
|
||||
|
||||
## 外部依赖
|
||||
|
||||
- 现场设备清单、IP 段、账号权限和协议开放情况。
|
||||
- H3C/华三设备型号、SNMP OID、Trap 字典、Syslog 格式和账号权限。
|
||||
- 是否允许 SNMP、Syslog、Trap、Agent、数据库连接和 URL/API 探测。
|
||||
- 医院短信平台、邮件服务、站内消息要求和第三方平台接入方式。
|
||||
- 是否需要对接现有 ITSM、短信平台、统一身份认证或日志审计平台;老运维平台首期不迁移不集成,仅保留后续数据迁移可能性。
|
||||
- 现场网络隔离、代理部署和安全审查要求。
|
||||
- Linux 与麒麟系统版本、CPU 架构、服务管理方式、数据库部署边界和国产化合规要求。
|
||||
- 选定时序数据库在 Linux/麒麟环境下的部署、授权、运维、备份和恢复要求。
|
||||
- 真实验收样例:至少一个业务系统、一个网络设备、一个主机、一个数据库或 URL/API。
|
||||
|
||||
## 待确认问题
|
||||
|
||||
1. 验收是否允许使用模拟 Trap、模拟 Syslog 和可控 URL/API 故障?如果允许,需要明确哪些场景可模拟、哪些必须来自现场设备。
|
||||
2. H3C/华三首批接入设备的型号、SNMP 版本、Trap/Syslog 格式、账号权限和网络连通性是什么?
|
||||
3. 短信平台和邮件服务的接入方式、测试账号、发送限制和验收口径是什么?
|
||||
4. Linux 与麒麟验收环境的系统版本、CPU 架构、服务管理方式和网络安全限制是什么?
|
||||
5. 时序数据库已决策采用 TDengine 开源版;现场仍需确认 AGPL 合规、Linux/麒麟目标版本、部署形态、备份恢复和是否需要企业版支持。
|
||||
6. 3D 机房外包前端需要 OPS 提供哪些接口字段、刷新频率、权限控制和告警状态编码?
|
||||
7. 数据中心、机柜、设备现有数据何时可获得?如果短期拿不到,是否接受使用样例数据完成接口和验收演示?
|
||||
|
||||
## 下一步任务
|
||||
|
||||
下一步不要直接开发。先组织一次 60-90 分钟现场确认会,拿这份蓝图逐项确认第 0 阶段的输入。
|
||||
|
||||
会前准备一张确认表,至少包含:
|
||||
|
||||
- 首批资源样例:主机、网络设备、数据库、虚拟化、URL/API。
|
||||
- 每类资源的接入方式、账号、网络连通性和负责人。
|
||||
- H3C/华三设备的 SNMP、Trap、Syslog 接入样例。
|
||||
- 第 1 阶段必须打通的平台站内消息、短信、邮件渠道。
|
||||
- Linux/麒麟验收部署环境和时序数据库选型。
|
||||
- 外包 3D 机房前端需要的后端接口清单。
|
||||
- 一条验收故障演示脚本:故障如何触发、谁接收、谁确认、谁派单、谁关闭、怎么出报表。
|
||||
- 每个 OPS 编号归属到第 1 阶段、第 2 阶段或第 3 阶段。
|
||||
|
||||
会议结论必须形成 `docs/首期验收矩阵.md` 和 `TODOS.md`,否则后续开发会回到范围不清。
|
||||
|
||||
## 关键观察
|
||||
|
||||
- 你坚持 “A 与 B 都很重要”,这是正确压力。资源覆盖和告警闭环不能互相牺牲,但必须分清底座和主线。
|
||||
- 你选择“时间未定,先做完整蓝图”,说明当前目标不是快速样机,而是要先把平台结构讲清楚。这个选择要求文档必须分期,否则完整蓝图会变成全量承诺。
|
||||
- 你最终选择“事件生命周期主轴平台,并把分层底座作为内部原则”,这是本次最关键决策。它保留长期扩展能力,同时不丢掉验收闭环。
|
||||
|
||||
## /autoplan 审查记录
|
||||
|
||||
### 输入概况
|
||||
|
||||
- 计划文件:`docs/integrated-ops-platform-blueprint-design.md`
|
||||
- 基准分支:`main`
|
||||
- 界面范围:包含首页、综合监控、告警中心、工单、大屏、报表、权限等应用界面。
|
||||
- 开发体验范围:包含 REST API、部署、配置示例、迁移脚本、开发测试命令和工程初始化。
|
||||
- 外部审查声部:本轮未启用。后续如 Codex CLI 因权限被拒绝,需要按用户要求先申请权限再执行。
|
||||
- 高优先级仓库问题:Git remote URL 含明文凭据。报告中不复述凭据;后续用户已明确要求忽略该项,不作为当前交付阻塞。
|
||||
- 高优先级仓库问题:`.gitignore` 忽略 `docs/`,会导致需求、蓝图和验收矩阵默认不进版本控制。
|
||||
- 前提检查:通过。用户已确认事件生命周期主轴,并明确选择“方案 A,方案 C 作为方案 A 的内部架构原则”。
|
||||
|
||||
### CEO 视角审查
|
||||
|
||||
#### 0A. 前提挑战
|
||||
|
||||
| 前提 | 结论 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 双主线:资源监控覆盖 + 告警事件闭环 | 通过 | 用户明确指出 A 与 B 都重要;蓝图把资源监控作为底座、告警闭环作为主线,方向正确。 |
|
||||
| 事件生命周期是业务主轴 | 通过 | 招标清单按菜单组织,但验收价值来自故障从发现到关闭的证据链。 |
|
||||
| 第 1 阶段从关键样例开始而非全厂商覆盖 | 有风险但可接受 | 这是唯一可落地方式,但需要验收矩阵明确“不等于全量厂商适配”。 |
|
||||
| 工单、知识库、报表、大屏围绕事件链路 | 通过 | 能避免展示层空转。 |
|
||||
| 第 0 阶段现场确认优先 | 通过 | 当前最大未知是现场设备、协议、账号、通知渠道和是否允许模拟故障。 |
|
||||
|
||||
#### 0B. 现有资产复用
|
||||
|
||||
| 子问题 | 现有资产 | 复用决策 |
|
||||
| --- | --- | --- |
|
||||
| 后端分层、配置、DB、Redis、JWT、路由 | `templates/server_sample/` | 作为初始化来源,实际代码必须落到 `server/`。 |
|
||||
| 前端 Vue 3、Arco、Vite、Pinia、i18n 风格 | `templates/front_sample/standard` | 作为初始化来源,实际代码必须落到 `web/`。 |
|
||||
| 需求和 OPS 编号 | `docs/integrated-ops-platform-requirements.md` | 作为需求源,不直接等同开发范围。 |
|
||||
| 验收矩阵 | 缺失 | 必须新增 `docs/首期验收矩阵.md`。 |
|
||||
| 交付待办 | 缺失 | 已新增 `TODOS.md`。 |
|
||||
|
||||
#### 0C. 目标状态
|
||||
|
||||
```text
|
||||
当前状态
|
||||
需求清单完整,但仓库只有文档和模板,尚无实际交付工程。
|
||||
运维现状假设为多工具、手工台账、人工派单。
|
||||
|
|
||||
v
|
||||
当前计划
|
||||
用事件生命周期组织平台:资源上下文 -> 原始信号 -> 告警 -> 事件 -> 工单 -> 知识/报表/大屏。
|
||||
用分层能力底座保证后续多协议、多厂商、多资源扩展。
|
||||
|
|
||||
v
|
||||
12 个月理想状态
|
||||
医院关键业务系统、资源、告警、工单、资产、拓扑、报表形成统一运维事实库。
|
||||
告警质量可度量,规则可优化,自动化处置有审批、回滚和审计。
|
||||
```
|
||||
|
||||
#### 0C 补充:替代方案对比
|
||||
|
||||
| 方案 | 完整度 | 工作量 | 风险 | 结论 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 最小可行:只做告警闭环 + 少量资源样例 | 7/10 | 中 | 中 | 太窄,无法满足用户强调的资源覆盖主线。 |
|
||||
| 事件生命周期 + 分层底座 | 10/10 | 大 | 中 | 采用。与用户选择一致。 |
|
||||
| 完整模块矩阵 | 7/10 | 很大 | 高 | 只保留为需求映射,不作为工程主结构。 |
|
||||
|
||||
#### 审查项 1-11
|
||||
|
||||
| 审查项 | 结果 |
|
||||
| --- | --- |
|
||||
| 架构 | 方向正确,但实现前需要补 ER 模型、状态机和部署顺序。 |
|
||||
| 错误与救援 | 当前计划列出了失败概念,但缺完整救援登记表;每类失败都需要重试、降级、用户提示、日志和审计。 |
|
||||
| 安全 | 计划之外发现 P0:Git 远程地址含明文凭据;计划内还需定义凭据存储、数据权限和策略变更审计。 |
|
||||
| 数据流与交互 | 采集、告警、通知、派单、报表、大屏都需要明确空值、空列表、错误、部分成功的处理方式。 |
|
||||
| 代码质量 | 当前尚无业务代码,主要风险是把模板命令和部署说明不加区分地复制进实际交付;后续需要明确本地开发使用 Windows PowerShell,验收部署面向 Linux/麒麟。 |
|
||||
| 测试 | 已有测试意图,但缺覆盖矩阵;工程测试计划产物已生成。 |
|
||||
| 性能 | 需要补 p99 目标、背压、索引、分页、告警风暴处理和报表范围限制。 |
|
||||
| 可观测性 | 概念较强,但平台自监控不足:采集失败、队列积压、通知失败、规则命中率都要有指标。 |
|
||||
| 部署 | 已有交付形态,但缺本地初始化、Linux/麒麟部署、迁移回滚和烟测脚本。 |
|
||||
| 长期演进 | 第 1/2/3 阶段路线合理;主要技术债风险是第 1 阶段跳过 ER 和状态定义导致模型反复变动。 |
|
||||
| 设计与体验 | 界面范围真实,但需要页面层级、状态表、响应式和无障碍细节。 |
|
||||
|
||||
#### 错误与救援登记表
|
||||
|
||||
| 路径 | 可能问题 | 救援状态 | 救援动作 | 用户可见表现 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 采集任务执行 | 设备不可达、凭据错误、协议超时 | 已规划但需细化 | 重试、标记采集失败、生成平台内部告警 | 资源详情显示采集失败原因和最近成功时间 |
|
||||
| Trap/Syslog 接收 | 格式未知、OID 字典缺失、规则不匹配 | 已规划但需细化 | 入原始事件池,标记未解析,允许补规则重放 | 告警中心显示未解析事件数量 |
|
||||
| 告警规则执行 | 阈值错误、规则过宽、重复触发 | 已规划但需细化 | 规则校验、去重、抑制、规则命中审计 | 策略页显示命中和降噪效果 |
|
||||
| 通知发送 | 站内消息、短信、邮件失败 | 缺口 | 按渠道重试,记录失败原因,允许人工补发 | 通知历史显示失败和重试状态 |
|
||||
| 自动派单 | 无匹配处理人、权限不足、重复派单 | 缺口 | 回退待分派队列,阻止重复工单 | 告警详情显示派单失败和建议动作 |
|
||||
| 报表生成 | 数据量过大、时间范围无数据、导出失败 | 缺口 | 限制范围、异步生成、失败可重试 | 报表页显示进度、空态或失败原因 |
|
||||
|
||||
#### 失败模式登记表
|
||||
|
||||
| 路径 | 失败模式 | 救援状态 | 是否测试 | 用户是否可见 | 是否记录日志 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 采集任务 | 代理离线 | 部分覆盖 | 必须测试 | 是 | 必须记录 |
|
||||
| 告警治理 | 告警风暴导致通知轰炸 | 部分覆盖 | 必须测试 | 是 | 必须记录 |
|
||||
| 通知 | 第三方渠道不可用 | 缺口 | 必须测试 | 是 | 必须记录 |
|
||||
| 工单 | 并发关闭同一工单 | 缺口 | 必须测试 | 是 | 必须记录 |
|
||||
| 权限 | 越权查看其他科室资源 | 缺口 | 必须测试 | 拒绝访问 | 必须记录 |
|
||||
| 报表 | 大范围查询拖慢数据库 | 缺口 | 必须测试 | 是 | 必须记录 |
|
||||
|
||||
#### CEO 审查小结
|
||||
|
||||
```text
|
||||
+====================================================================+
|
||||
| CEO 计划审查小结 |
|
||||
+====================================================================+
|
||||
| 审查模式 | 选择性扩展 |
|
||||
| 系统审计 | 文档完整,工程目录缺失,Git 远程凭据风险 |
|
||||
| 步骤 0 | 保留事件生命周期主轴,吸收分层底座 |
|
||||
| 第 1 项(架构) | 发现 3 个问题 |
|
||||
| 第 2 项(错误) | 映射 6 条错误路径,存在 4 个缺口 |
|
||||
| 第 3 项(安全) | 发现 2 个问题,其中 1 个高严重度 |
|
||||
| 第 4 项(数据/体验) | 映射 6 个边界场景,5 个尚未处理 |
|
||||
| 第 5 项(质量) | 发现 1 个问题 |
|
||||
| 第 6 项(测试) | 已生成覆盖图,存在 8 个缺口 |
|
||||
| 第 7 项(性能) | 发现 4 个问题 |
|
||||
| 第 8 项(可观测) | 发现 5 个缺口 |
|
||||
| 第 9 项(部署) | 标记 4 个风险 |
|
||||
| 第 10 项(未来演进) | 可逆性 4/5,技术债 3 项 |
|
||||
| 第 11 项(设计) | 发现 5 个问题 |
|
||||
+--------------------------------------------------------------------+
|
||||
| 非范围项 | 已写入 |
|
||||
| 已有资产 | 已写入 |
|
||||
| 目标状态差距 | 已写入 |
|
||||
| 错误/救援登记表 | 6 条路径,4 个关键缺口 |
|
||||
| 失败模式 | 共 6 项,4 个关键缺口 |
|
||||
| TODOS.md 更新 | 已写入 9 项 |
|
||||
| 范围提案 | 无需用户额外挑战 |
|
||||
| 外部声部 | 本轮未启用 |
|
||||
| 完整性选择 | 9/9 项选择完整方案 |
|
||||
| 已产出图示 | 架构、数据流、状态、回滚 |
|
||||
| 未决决策 | 0 |
|
||||
+====================================================================+
|
||||
```
|
||||
|
||||
### 设计视角审查
|
||||
|
||||
#### 设计评分
|
||||
|
||||
| 维度 | 分数 | 发现 | 自动决策 |
|
||||
| --- | --- | --- | --- |
|
||||
| 信息架构 | 6/10 | 页面清单清楚,但每页首要/次要/三级信息未定义。 | 实现前补信息架构表。 |
|
||||
| 交互状态 | 4/10 | 缺加载、空态、错误、成功、部分成功、无权限状态表。 | 写入 P0 待办。 |
|
||||
| 用户旅程 | 6/10 | 端到端故障故事存在,但一线值班、专项管理员、负责人视角未拆。 | 补用户旅程故事板。 |
|
||||
| 模板化风险 | 7/10 | 蓝图偏功能性,风险较低;但大屏/首页容易做成模板化卡片堆叠。 | 要求密集、可扫描的运维界面,避免装饰性卡片堆叠。 |
|
||||
| 设计系统一致性 | 5/10 | 无 `DESIGN.md`,只能依赖 Arco 模板风格。 | 等 `web/` 初始化后再补设计系统文档。 |
|
||||
| 响应式与无障碍 | 3/10 | 不做独立移动端,但仍需定义移动端适配、键盘、屏幕阅读器、颜色对比。 | 增加 P0 UI 状态、响应式和无障碍规格。 |
|
||||
| 未决设计决策 | 5/10 | 告警中心、工单、拓扑、大屏的布局和状态细节待定。 | 保留为设计任务,不阻塞蓝图批准。 |
|
||||
|
||||
#### 页面结构草图
|
||||
|
||||
```text
|
||||
运维入口
|
||||
├─ 首页总览
|
||||
│ ├─ 待处理告警
|
||||
│ ├─ 资源健康
|
||||
│ ├─ 告警趋势
|
||||
│ └─ 快捷入口
|
||||
├─ 综合监控
|
||||
│ ├─ 资源列表
|
||||
│ └─ 资源详情 -> 指标趋势 / 采集状态 / 关联告警
|
||||
├─ 告警中心
|
||||
│ ├─ 实时告警
|
||||
│ ├─ 事件归并
|
||||
│ ├─ 策略与通知
|
||||
│ └─ 历史与导出
|
||||
├─ 工单管理
|
||||
├─ 业务系统视图
|
||||
├─ 拓扑/IPAM/资产
|
||||
├─ 报表与大屏
|
||||
└─ 权限与系统管理
|
||||
```
|
||||
|
||||
#### 必需交互状态表
|
||||
|
||||
| 功能 | 加载 | 空态 | 错误 | 成功 | 部分成功 | 无权限 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 首页总览 | 骨架屏 | 引导接入资源 | 数据源失败提示 | 展示核心指标 | 部分组件降级 | 隐藏无权限模块 |
|
||||
| 综合监控 | 表格加载 | 引导新增/导入 | 采集失败摘要 | 资源健康展示 | 部分指标缺失 | 数据权限提示 |
|
||||
| 告警中心 | 告警流加载 | 无待处理告警 | 规则/查询失败 | 可确认/派单 | 部分渠道失败 | 操作按钮禁用 |
|
||||
| 工单管理 | 列表加载 | 无工单 | 流转失败 | 状态更新 | 通知失败但工单保留 | 无权转交/关闭 |
|
||||
| 报表大屏 | 图表加载 | 无统计数据 | 生成失败 | 可查看/导出 | 部分组件无数据 | 不显示敏感数据 |
|
||||
|
||||
#### 设计审查小结
|
||||
|
||||
```text
|
||||
+====================================================================+
|
||||
| 设计计划审查小结 |
|
||||
+====================================================================+
|
||||
| 系统审计 | 无 DESIGN.md,界面范围已确认 |
|
||||
| 步骤 0 | 初始评分 5/10 |
|
||||
| 第 1 轮(信息架构) | 6/10 -> 8/10,已补层级草图 |
|
||||
| 第 2 轮(状态) | 4/10 -> 7/10,已补状态表 |
|
||||
| 第 3 轮(旅程) | 6/10 -> 7/10,仍需拆分用户角色 |
|
||||
| 第 4 轮(模板化) | 7/10 -> 8/10 |
|
||||
| 第 5 轮(设计系统) | 5/10 -> 5/10,等待 web/DESIGN.md |
|
||||
| 第 6 轮(响应式) | 3/10 -> 5/10,仍需具体规格 |
|
||||
| 第 7 轮(决策) | 4 项已解决,3 项延后 |
|
||||
+--------------------------------------------------------------------+
|
||||
| 总体设计评分 | 5/10 -> 7/10 |
|
||||
+====================================================================+
|
||||
```
|
||||
|
||||
### 工程视角审查
|
||||
|
||||
#### 架构图
|
||||
|
||||
```text
|
||||
+----------------------+
|
||||
| web/ Vue 界面 |
|
||||
+----------+-----------+
|
||||
|
|
||||
v
|
||||
+----------------------+
|
||||
| server/ REST API |
|
||||
| 认证 / RBAC / 审计 |
|
||||
+----+----------+------+
|
||||
| |
|
||||
+--------------+ +----------------+
|
||||
v v
|
||||
+-------------+ +----------------+ +-------------+
|
||||
| 资源台账 |<---->| 事件治理 |-->| 流程闭环 |
|
||||
| 资源模型 | | 原始/告警/事件 | | 工单/知识库 |
|
||||
+------+------+ +-------+--------+ +------+------+
|
||||
| | |
|
||||
v v v
|
||||
+-------------+ +----------------+ +-------------+
|
||||
| 采集器 | | 通知渠道 | | 报表 |
|
||||
| H3C/SNMP | | 站内/短信/邮件 | | 大屏 |
|
||||
+------+------+ +-------+--------+ +------+------+
|
||||
| | |
|
||||
v v v
|
||||
外部设备 外部通知渠道 PostgreSQL + 时序库
|
||||
```
|
||||
|
||||
#### 状态机
|
||||
|
||||
```text
|
||||
告警
|
||||
触发中 -> 已确认 -> 已恢复
|
||||
-> 已忽略
|
||||
-> 已失效
|
||||
非法:已恢复 -> 触发中,除非有新的原始事件
|
||||
|
||||
事件
|
||||
待处理 -> 已分派 -> 处理中 -> 已解决 -> 已关闭
|
||||
-> 已挂起 -> 处理中
|
||||
非法:已关闭 -> 处理中
|
||||
|
||||
工单
|
||||
已创建 -> 已接单 -> 已关闭
|
||||
-> 已转交 -> 已接单
|
||||
-> 已挂起 -> 已重启 -> 已接单
|
||||
-> 已撤回
|
||||
非法:已关闭 -> 已转交
|
||||
```
|
||||
|
||||
#### 测试覆盖图
|
||||
|
||||
```text
|
||||
代码路径 / 数据流 测试要求
|
||||
[+] 资源纳管 接口集成测试 + API 测试
|
||||
├─ 正常:创建资源并绑定类型 必须覆盖
|
||||
├─ 空态:暂无资源 必须覆盖
|
||||
└─ 错误:重复身份 / 非法 IP 必须覆盖
|
||||
[+] 采集任务执行 单元测试 + 集成测试
|
||||
├─ 正常:指标样本入库 必须覆盖
|
||||
├─ 错误:超时 / 凭据失败 必须覆盖
|
||||
└─ 部分成功:部分指标缺失 必须覆盖
|
||||
[+] 原始事件 -> 告警 -> 事件 单元测试 + 集成测试
|
||||
├─ 去重 / 压缩 / 抑制 必须覆盖
|
||||
├─ 抖动 / 告警风暴 必须覆盖
|
||||
└─ 恢复 必须覆盖
|
||||
[+] 通知路由 集成测试
|
||||
├─ 成功 必须覆盖
|
||||
└─ 第三方失败 / 重试 必须覆盖
|
||||
[+] 告警 -> 工单流程 端到端测试
|
||||
├─ 分派 / 接单 / 转交 / 关闭 必须覆盖
|
||||
└─ 并发关闭 必须覆盖
|
||||
[+] 报表和大屏 集成测试 + 端到端测试
|
||||
├─ 聚合准确 必须覆盖
|
||||
├─ 查询范围无数据 必须覆盖
|
||||
└─ 大范围查询保护 必须覆盖
|
||||
```
|
||||
|
||||
测试计划产物:`C:\Users\27105\.gstack\projects\ops\27105-main-eng-review-test-plan-20260621-152008.md`
|
||||
|
||||
#### 工程发现
|
||||
|
||||
| 领域 | 发现 | 严重度 | 决策 |
|
||||
| --- | --- | --- | --- |
|
||||
| 安全 | Git 远程地址含嵌入式凭据。 | 已记录风险 | 用户已明确要求忽略该项,不作为当前交付阻塞;后续推送、共享仓库或交付前建议重新评估。 |
|
||||
| 仓库整洁 | `.gitignore` 忽略 `docs/`,导致核心交付文档不出现在 Git 状态中。 | P0 | 决定移除忽略规则,或显式强制添加指定文档。 |
|
||||
| 架构 | 数据模型和状态机只是隐含存在,尚不足以指导实现。 | P1 | 编码前补 ER 和状态模型。 |
|
||||
| 测试 | 计划强调测试,但缺具体测试矩阵。 | P1 | 已生成测试计划产物,并写入 TODO。 |
|
||||
| 部署 | 本地开发路径、Linux/麒麟验收部署、迁移回滚和烟测脚本尚未成文。 | P1 | 文档需要区分本地 Windows PowerShell 命令和验收环境 Linux/麒麟部署说明。 |
|
||||
| 性能 | 缺告警风暴处理和报表查询范围限制。 | P1 | 补背压、分页、索引和异步导出。 |
|
||||
|
||||
#### 并行实施策略
|
||||
|
||||
| 线路 | 工作流 | 模块 | 依赖 |
|
||||
| --- | --- | --- | --- |
|
||||
| A | 资源模型 + 指标 | `server/internal/models`、`server/internal/logic/monitoring` | 无 |
|
||||
| B | 前端壳层 + 路由 | `web/src/router`、`web/src/views`、i18n | 无 |
|
||||
| C | 事件/告警引擎 | `server/internal/logic/alerting` | A |
|
||||
| D | 工单/流程 | `server/internal/logic/ticketing` | C |
|
||||
| E | 报表/大屏 | `server/internal/logic/reports`、`web/src/views/dashboard` | A + C |
|
||||
| F | 部署/测试产物 | `deploy/`、docs、CI | A + B 骨架 |
|
||||
|
||||
先启动 A 和 B。C 等待 A。D 和 E 等待 C。F 可以在初始服务和前端骨架存在后开始。
|
||||
|
||||
### 开发体验审查
|
||||
|
||||
#### 开发者画像
|
||||
|
||||
| 字段 | 内容 |
|
||||
| --- | --- |
|
||||
| 角色 | 项目交付工程师,负责从模板初始化实际 `server/`、`web/`、`deploy/` 并完成验收材料。 |
|
||||
| 场景 | 拿到招标需求和蓝图后,需要快速搭出可运行系统、可测试 API、可演示 UI。 |
|
||||
| 可接受等待 | 30-60 分钟内应能跑通后端健康检查和前端开发服务。 |
|
||||
| 期望 | Windows PowerShell 命令、配置示例、迁移命令、测试命令、演示数据准备脚本。 |
|
||||
|
||||
#### 开发者体验叙述
|
||||
|
||||
我打开仓库,`README.md` 只有 `ops`。我能在 `docs/` 找到需求和蓝图,但不知道下一步该初始化 `server/` 还是先写验收矩阵。模板 README 有不少可用信息,但里面混有 Linux/macOS 命令和 systemd 部署示例,和本项目“Windows PowerShell”要求冲突。作为交付工程师,我最需要的是一条清晰路径:复制哪个模板、改哪些服务名、配置哪个 example、执行哪些 PowerShell 命令、如何跑测试、如何准备一条告警闭环演示数据。现在这些都散在模板和蓝图里,还没有形成可复制的入门路径。
|
||||
|
||||
#### 开发体验基准
|
||||
|
||||
| 工具/流程 | 首次跑通时间 | 关键开发体验选择 | 来源 |
|
||||
| --- | --- | --- | --- |
|
||||
| 当前 OPS 仓库 | >30 分钟 | README 缺少启动路径 | 当前仓库 |
|
||||
| 第 1 阶段目标 | <15 分钟 | 一条 PowerShell 初始化脚本 + 示例配置 + 演示数据 | `/autoplan` 目标 |
|
||||
| 同类内部交付流程 | 5-15 分钟 | 模板复制后即可健康检查 | 参考基准 |
|
||||
|
||||
#### 开发体验评分
|
||||
|
||||
| 维度 | 分数 | 发现 |
|
||||
| --- | --- | --- |
|
||||
| 入门路径 | 3/10 | README 不足,实际工程目录未初始化。 |
|
||||
| API/CLI | 5/10 | 模板有 CLI,但 OPS 命令未定义。 |
|
||||
| 错误信息 | 4/10 | 蓝图要求统一响应,但未定义错误码、原因和修复建议。 |
|
||||
| 文档 | 5/10 | 需求和蓝图强,开发手册缺失。 |
|
||||
| 升级路径 | 3/10 | 版本、迁移、回滚策略未定义。 |
|
||||
| 开发环境 | 4/10 | Windows PowerShell 路径未成文。 |
|
||||
| 社区/生态 | 不适用 | 内部交付项目,不作为核心评分。 |
|
||||
| 开发体验度量 | 4/10 | 尚未定义首次跑通时间、验收脚本运行时间、失败率。 |
|
||||
|
||||
开发体验总体评分:4/10。目标:第 1 阶段开发者从获取代码到后端健康检查 + 前端启动 <15 分钟。
|
||||
|
||||
#### 开发体验实施清单
|
||||
|
||||
- [ ] README 增加“从零开始”的 PowerShell 路径。
|
||||
- [ ] `server/etc/app.example.yaml` 和本地配置说明。
|
||||
- [ ] `deploy/` 提供初始化、迁移、演示数据、烟测脚本。
|
||||
- [ ] 错误响应包含 `code`、`message`、`traceId`、`cause`、`suggestion`。
|
||||
- [ ] 每个核心 API 提供请求/响应示例。
|
||||
- [ ] 测试命令和验收命令可复制执行。
|
||||
|
||||
### 决策审计轨迹
|
||||
|
||||
| # | 阶段 | 决策 | 分类 | 原则 | 理由 | 被拒绝方案 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | 输入 | 使用当前蓝图作为计划文件 | 机械决策 | 倾向行动 | 用户在 office-hours 计划后立即调用 `/autoplan`。 | 等待 D10 再审查 |
|
||||
| 2 | CEO | 保留事件生命周期作为主轴 | 机械决策 | 选择完整性 | 同时保留 A+B,并避免菜单孤岛。 | 模块矩阵作为主架构 |
|
||||
| 3 | CEO | 将分层底座作为内部架构原则 | 机械决策 | 显式优先 | 与用户明确选择一致。 | 单独走底座优先交付 |
|
||||
| 4 | CEO | 为验收矩阵和模型/状态规格增加 TODO | 机械决策 | 限定范围 | 这些事项在当前影响范围内,并能解除实现阻塞。 | 延后所有规划债 |
|
||||
| 5 | 设计 | UI 实现前必须补状态表 | 机械决策 | 选择完整性 | 空态、错误、部分成功、无权限是运维界面的核心状态。 | 让实现者自行推断状态 |
|
||||
| 6 | 工程 | 生成测试计划产物 | 机械决策 | 选择完整性 | 测试必须在代码实现前规划。 | 等实现后再补测试 |
|
||||
| 7 | 工程 | 将含凭据 Git 远程地址标记为 P0 | 机械决策 | 安全 | 远程地址中的凭据虽不属于产品范围,但会危害仓库协作。 | 当作环境细节忽略 |
|
||||
| 8 | 开发体验 | 要求本地 PowerShell 入门路径,并补 Linux/麒麟验收部署说明 | 机械决策 | 显式优先 | 本地开发按项目规则使用 Windows PowerShell;验收部署按会议结论面向 Linux/麒麟。 | 直接复用模板 README |
|
||||
|
||||
### 跨阶段主题
|
||||
|
||||
**主题:显式运维证据。** CEO、工程、设计和开发体验审查都指向同一个缺口:平台方向已经成立,但每个阶段都需要证据产物。验收矩阵、状态表、测试计划、烟测脚本和演示数据不是文档负担,而是产品可验收性的证明。
|
||||
|
||||
**主题:不要让模板痕迹泄漏进交付。** 工程和开发体验审查都指出了这一点。模板有价值,但实际 `server/`、`web/`、`deploy/` 文档必须符合 Windows PowerShell、UTF-8 中文文档、密钥卫生和项目专属服务命名要求。
|
||||
|
||||
### 不在当前范围
|
||||
|
||||
- 第 1 阶段不承诺全厂商覆盖,需等现场设备清单确认后再扩展。
|
||||
- 第 1 阶段不承诺完整拓扑、IPAM、机柜和深度资产套件,这些属于第 2 阶段资源上下文增强。
|
||||
- AI 规则优化和自动化处置属于第 3 阶段治理与智能化能力。
|
||||
- 本轮为文本审查,未生成视觉稿;视觉稿应在 `web/` 初始化和界面范围明确后再做。
|
||||
|
||||
## GSTACK 审查报告
|
||||
|
||||
| 审查 | 触发 | 目的 | 次数 | 状态 | 发现 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| CEO 审查 | `/autoplan` | 范围与策略 | 1 | 存在待处理问题 | 方向成立;需要补验收矩阵、模型/状态规格、失败登记表,并修复文档版本管理问题。 |
|
||||
| 设计审查 | `/autoplan` | UI/UX 缺口 | 1 | 存在待处理问题 | 5/10 -> 7/10;缺详细状态、响应式、无障碍和 `DESIGN.md`。 |
|
||||
| 工程审查 | `/autoplan` | 架构与测试 | 1 | 存在待处理问题 | 发现 5 个高价值缺口;测试计划产物已写入;含凭据 Git 远程地址风险已记录,用户已确认不作为当前阻塞项。 |
|
||||
| 开发体验审查 | `/autoplan` | 开发体验缺口 | 1 | 存在待处理问题 | 总体 4/10;缺 README 入门路径、PowerShell 文档和部署文档。 |
|
||||
| 外部审查声部 | `/autoplan` | 独立审查 | 0 | 本轮未启用 | 后续如需运行 Codex CLI 且遇到权限拒绝,应先向用户申请权限。 |
|
||||
|
||||
- **结论:** 架构方向已批准。编码前必须完成的文档规格已补齐:`docs/首期验收矩阵.md`、第 1 阶段数据模型和状态机、UI 状态覆盖。含凭据 Git 远程地址风险已按用户要求忽略,不作为当前交付阻塞项。
|
||||
- **Codex CLI:** 本轮未作为独立声部运行;后续如果因权限问题被拒绝,将先向用户申请权限。
|
||||
- **跨模型审查:** 本轮未运行,仅保留主审查结论。
|
||||
**审批结论:**
|
||||
- 用户已选择 A:按当前方案批准蓝图方向。后续进入 P1 待办处理与第 1 阶段实施准备;Git 远程凭据风险已按用户要求忽略,不作为当前交付阻塞项。
|
||||
|
||||
93
docs/integrated-ops-platform-requirements.md
Normal file
93
docs/integrated-ops-platform-requirements.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 一体化运维平台 - 需求与功能清单
|
||||
|
||||
来源文件:西藏自治区人民医院信息系统硬件支撑平台升级改造项目_招标文件.pdf;菜单规划表3.0.xlsx
|
||||
|
||||
范围说明:本文件只提取“一体化运维平台”相关内容,不包含机房装修、网络交换、安全设备、服务器、存储、光纤布线等硬件交付清单。硬件、安全、网络设备仅在“被监控对象”维度出现。
|
||||
|
||||
## 1. 平台定位
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 平台名称 | 一体化运维管理平台 |
|
||||
| 数量 | 1 套 |
|
||||
| 建设目标 | 对网络设备、安全设备、服务器硬件、虚拟化系统、存储设备等 IT 资源进行集中监控管理与运维信息收集,提升 IT 运维效率 |
|
||||
| 来源依据 | 第 77 页“6.一体化运维管理平台建设”;第 107-116 页“二、一体化运维平台” |
|
||||
| 主要模块 | 首页总览、可视化大屏、综合监控、网络架构管理、告警管理、工单管理、数据中心管理、资产管理、知识库、报表管理、用户权限管理、日志管理、系统管理、快速开发/低代码能力 |
|
||||
| 验收方式 | 逐项功能演示、监控对象接入验证、告警测试、报表/大屏输出、权限与日志检查、移动端验证 |
|
||||
|
||||
## 2. 核心需求清单
|
||||
|
||||
| 编号 | 需求类型 | 强制性 | 优先级 | 需求描述 | 原文依据 | 来源位置 | 验收/证明方式 | 备注 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| OPS-001 | 首页总览 | 强制 | P0 | 首页需以折线图、拓扑图等方式展示设备状态、告警状态、网络状态,提供待处理告警、设备总览、告警趋势、网络监控等入口,且支持总览页面模块化配置。 | 菜单规划表3.0.xlsx“总览”备注 | Sheet1 第 6 行 | 查看首页总览,调整模块显示内容并验证保存生效 | 菜单列不作为需求依据,功能点来自描述与备注 |
|
||||
| OPS-002 | 操作系统监控 | 强制 | P0 | 平台需统一监控内存利用率、磁盘、CPU 利用率、硬盘利用率、网卡状态、接收和发送的流量及包数、日志、Syslog、异常进程、目录和文件数量及大小等基础指标。 | “操作系统监控的指标包含...” | 招标文件第 107 页;Sheet1 第 11 行 | 接入主机并展示指标,模拟阈值触发告警 | 基础监控能力 |
|
||||
| OPS-003 | 服务器硬件监控 | 强制 | P1 | 支持 IBM、HP、联想、浪潮、华为、EMC、H3C 等厂商服务器硬件监控,采集电压、电流、温度、风扇及其他硬件状态。 | “服务器硬件运行指标,包括电压、电流、温度、风扇...” | 招标文件第 110 页;Sheet1 第 12 行 | 接入物理服务器并展示硬件健康与性能指标 | 含多品牌适配 |
|
||||
| OPS-004 | 网络设备监控 | 强制 | P1 | 支持不同品牌网络设备自动发现、拓扑生成、逻辑面板图、接口状态可视化、接口表、路由表、转发表、ARP 表查看,并可通过 Syslog、SNMP Trap 接收网络设备告警事件。 | “逻辑面板图...接口表、路由表、转发表以及ARP表...” | 招标文件第 107-110 页;Sheet1 第 13 行 | SNMP/Trap 接入、逻辑面板、接口状态和告警规则演示 | 被监控对象含交换机、防火墙、安全设备 |
|
||||
| OPS-005 | 安全设备监控 | 强制 | P1 | 支持安全设备状态、型号、版本、CPU、内存、接口状态、接口信息等监控,并提供历史数据记录管理和按时间间隔生成历史数据分析报表能力。 | “安全设备监控...历史数据分析报表” | Sheet1 第 14 行 | 接入安全设备并输出状态、接口与历史分析报表 | 与网络设备监控关联 |
|
||||
| OPS-006 | 存储监控 | 强制 | P1 | 支持惠普、日立、EMC、群晖、IBM、华为等存储设备监控,采集 CPU 使用率、内存及虚拟内存使用率、硬盘空间、磁盘 IO 吞吐、可用率、控制器状态、存储池、物理硬盘、网口状态、速率、流量、背板、节点等信息。 | “存储设备监控...CPU使用率、内存...控制器状态” | 招标文件第 110-111 页;Sheet1 第 15 行 | 接入存储并展示容量、硬盘、网口、节点和控制器状态 | 需适配本项目存储设备 |
|
||||
| OPS-007 | 数据库监控 | 强制 | P1 | 支持 Oracle、MySQL、国产数据库等监控,采集表空间、死锁数、用户连接、请求、内存、缓存、连通性、SQL 执行耗时 TOP5、SQL 耗 CPU TOP5、SQL 耗内存 TOP5;支持 Oracle RAC 运行状态、Cluster、ASM、数据库实例及其他集群资源状态;支持自定义 SQL 脚本监控。 | “监控指标包含:表空间...支持自定义SQL脚本监控” | 招标文件第 107 页;Sheet1 第 16 行 | 接入数据库实例,展示性能、RAC/集群、SQL TOP 和自定义 SQL 监控结果 | 医院国产化环境重点 |
|
||||
| OPS-008 | 中间件监控 | 强制 | P1 | 支持 WebLogic、Tomcat、MQ、国产中间件及 ActiveMQ、RocketMQ、Kafka、IBM WebSphere 等中间件监控,采集服务可用性、JVM 总大小/已用大小、应用可用性、连接池可用性、连接池大小、活动连接数、等待连接数、创建连接数、活动线程数量、会话创建数、无效会话数等指标。 | “监控指标包括:服务可用性、JVM...” | 招标文件第 107 页;Sheet1 第 17 行 | 接入至少一种院方实际中间件并展示运行状态和指标 | 中间件种类需按现场确认 |
|
||||
| OPS-009 | 虚拟化监控 | 强制 | P1 | 支持 VMware、华为云、私有云及国产虚拟化平台监控,展示物理机、虚拟机、网络、存储资源、数据库等元素关系并动态更新,同时展示宿主机、集群、虚拟机、资源池、CPU、内存、磁盘、开关机状态等。 | “清晰展现各元素之间的关系,并且动态更新” | 招标文件第 108、110 页;Sheet1 第 18 行 | 接入虚拟化平台并展示主机、虚拟机、资源关系视图 | 与现有虚拟化平台兼容 |
|
||||
| OPS-010 | 日志与 Trap 监控 | 强制 | P1 | 支持接收交换机、路由器、防火墙、Unix/Linux 等设备生成的 Syslog 消息,基于规则识别重要日志并关联告警;支持 SNMP Trap 规则、Trap 字典、OID 含义和描述自定义、告警级别和恢复信息设置、Trap 屏蔽策略和屏蔽时间段配置。 | “支持Trap字典...支持Trap屏蔽” | 招标文件第 111 页;Sheet1 第 19、80 行 | 采集 Syslog/Trap,配置字典、规则、屏蔽策略并验证告警关联 | 与日志审计设备不是同一范围 |
|
||||
| OPS-011 | URL 与业务可用性监控 | 强制 | P1 | 支持定期检测网页、网站、网址、URL 和 Web 业务流程是否可正常访问,及时发现 Web 业务异常或网页变化;采集可用性、响应时间、返回状态代码等指标,并支持端口、服务、进程、Webservice、业务调用链、业务日志监控。 | “用于定期检测指定的网页、网站、网址或URL...” | 招标文件第 107、111 页;Sheet1 第 20 行 | 配置 URL/API/端口/服务监控,模拟异常并触发告警 | 面向 HIS/LIS/PACS/EMR 等业务 |
|
||||
| OPS-012 | 动环与安全环境监控 | 强制 | P2 | 支持电力、UPS、空调、温湿度等电器相关监控,以及消防、门禁、漏水、有害气体等消防安全相关监控。 | “电力/UPS/空调/温湿度”“消防/门禁/漏水/有害气体” | Sheet1 第 21-22 行 | 接入或模拟动环设备,展示状态、指标和告警 | 与数据中心/机房管理联动 |
|
||||
| OPS-013 | 网络拓扑管理 | 强制 | P1 | 支持可视化拓扑展示、全景图、告警图标点击查看、拓扑统计查看设备名称、设备 IP、链路流量;支持环形、层次、同心圆、网络布局、拓扑图下载、添加设备/文本/子拓扑/区域、导入设备、添加链路、分组和多级分组管理、拓扑缩放/纵览/移动/手动刷新。 | “支持可视化展示拓扑视图、查看全景图...” | 招标文件第 114 页;Sheet1 第 24-26 行 | 创建拓扑分组,导入设备与链路,验证布局、告警查看、统计、下载 | 与 OPS-004 关联 |
|
||||
| OPS-014 | 网络流量分析 | 强制 | P1 | 支持从应用、协议、会话、流向等维度分析网络流量状态和构成,识别占用网络资源最多的应用、协议、设备和流向,支持流量异常检测、WAN 流量监测、实时预警、网络优化参考、链路新增/编辑/删除/批量删除/禁止/允许、流量监控分析。 | “从应用、协议、会话、流向等维度了解网络流量状态和构成” | Sheet1 第 29 行 | 新增链路并展示应用/协议/会话/流向分析,触发流量异常预警 | 支撑网络带宽规划 |
|
||||
| OPS-015 | 流量参数配置 | 强制 | P2 | 支持按实际应用场景或自定义业务类型设置需要监测的应用、端口、协议,并支持设置监测数据保存时间。 | “用户自己设置需要监测流量的应用端口、协议...” | Sheet1 第 33 行 | 配置应用/端口/协议和保存周期,验证流量采集策略生效 | 与流量分析联动 |
|
||||
| OPS-016 | IP 地址管理 | 强制 | P1 | 支持添加子网络、网段、IP 地址,展示 IP 与 MAC 对应关系,查看在用、未用、分配、保留状态;支持分组分层管理,从规划、分配、监控、回收维度对 IP 地址进行全生命周期管理。 | “IP地址管理支持添加子网络、网段、IP地址...” | 招标文件第 112-113 页;Sheet1 第 34-36、38 行 | 创建分组、导入/新增子网,查看 IP/MAC、分配和使用状态 | 包含一键/自动扫描能力 |
|
||||
| OPS-017 | IP 自动扫描与报表 | 强制 | P2 | 支持按扫描规则和周期自动扫描 IP 网段、IP 地址并将新发现地址加入系统;支持 IP 概览、IP 地址冲突记录、子网 IP 使用占比 TOP10、IP 状态统计、子网统计、DHCP 地址租约、IP 变更记录、IP 异常记录等报表。 | “系统会根据设置的扫描规则、周期自动扫描...” | Sheet1 第 36、38 行 | 配置扫描任务并生成 IP 使用、冲突、租约、变更、异常报表 | “一件扫描”统一理解为“一键扫描” |
|
||||
| OPS-018 | 告警降噪与策略 | 强制 | P0 | 支持告警去重、告警压缩、告警屏蔽、告警依赖、告警抑制、告警策略规则匹配和条件设置,可按设备、监测点、监测指标选择策略范围,减少告警泛滥、误报和重复告警。 | “告警去重、告警压缩、告警屏蔽...” | 招标文件第 111 页;Sheet1 第 44、50 行 | 触发重复/依赖告警,验证去重、压缩、屏蔽、抑制和策略范围 | 运维闭环核心功能 |
|
||||
| OPS-019 | 告警模板与通知 | 强制 | P0 | 支持操作系统、服务器硬件、网络设备、安全设备、存储、数据库、中间件、虚拟化、动环、消防等告警模板;模板可自定义告警内容并使用系统变量;支持颜色、网页弹窗、邮件、手机短信、声音、脚本、微信公众号、企业微信、钉钉、电话、工单等告警方式,邮件支持服务器、发件人、收件人、临时收件人配置,短信支持无线 Modem、短消息平台和第三方短信平台。 | “用户可以自定义告警内容...支持多种告警方式” | 招标文件第 111 页;Sheet1 第 45-46 行 | 配置模板、变量和多渠道通知,触发测试告警 | 需确认医院实际通信渠道 |
|
||||
| OPS-020 | 告警级别与升级 | 强制 | P1 | 支持最多七级告警级别,自定义级别名称和显示颜色;同一监控数据同时触发高低级别规则时只发送高级别告警;支持告警发生后在设置时间内未处理时自动升级并转发,通知方式可自由选择。 | “最多可达七级...支持告警升级” | Sheet1 第 47 行 | 配置多级告警、颜色和升级规则,验证高级别优先与超时升级 | 提升值班响应效率 |
|
||||
| OPS-021 | 告警受理与历史 | 强制 | P0 | 支持受理、确认、忽略、查看、搜索、导出告警;支持直接将告警分派为工单处理,特定发送方式为工单时自动生成工单;记录通知或邮件发送历史,支持查看发送详情、告警来源、历史告警、已恢复、已忽略、已失效告警。 | “受理告警、确认告警、忽略告警...” | 招标文件第 111 页;Sheet1 第 48-49 行 | 触发告警并完成确认、忽略、派单、导出、历史查询 | 与工单管理联动 |
|
||||
| OPS-022 | 工单管理 | 强制 | P1 | 支持轻量化运维工单管理,可手动或自动创建事件处理工单,支持接单、转交、撤回、挂起、重启、关闭;支持告警联动自动生成事件工单、告警发送策略自动派单、在告警视图中直接创建工单并分派人员。 | “工单管理可与告警联动,实现告警发生后自动生成事件工单” | Sheet1 第 51 行 | 创建手工工单和告警自动工单,验证流转、派单、通知和关闭 | 规范运维任务处理 |
|
||||
| OPS-023 | 数据中心与机房管理 | 强制 | P2 | 支持多层级机房和数据中心管理,可按“省份-城市-数据中心-楼层”模式管理;机房管理可集成动环设备并添加机柜,实现设备集中监控、集中告警、集中展示和数据中心能力管理。 | “支持多层级机房和数据中心管理...” | Sheet1 第 54 行 | 建立数据中心/楼层/机房层级,关联动环设备和机柜 | 与 3D 机房/资产数据相关 |
|
||||
| OPS-024 | 机柜与 U 位管理 | 强制 | P2 | 支持机柜设计工具和机柜布局图,展示机柜内设备位置、占用 U 位、状态信息;关联已监控设备后实时显示设备状态和告警信息;支持按可容纳机柜数、实际使用机柜 U 位数统计物理空间使用量和剩余物理空间。 | “创建机柜布局图...统计出各机房的物理空间使用量” | 招标文件第 113 页;Sheet1 第 56-57 行 | 创建机柜布局,绑定设备,查看 U 位占用、状态告警和剩余空间 | 支撑上架规划 |
|
||||
| OPS-025 | 资产管理 | 强制 | P2 | 支持机房设备发生变化时手动添加或删除设备,系统根据变更计算最新可用物理空间,为设备上架提供规划参考。 | “手动添加或删除设备...计算出最新的可用物理空间” | Sheet1 第 60 行 | 新增/删除设备并验证空间容量重新计算 | 与机柜/U 位管理联动 |
|
||||
| OPS-026 | 知识库管理 | 强制 | P2 | 支持知识与设备异常检测点关联,设备异常时可查看相关知识;支持知识分类创建、分类展示、新增、编辑、删除;支持知识发布、编辑、删除、浏览、附件下载;支持知识审核和审核日志记录。 | “知识库支持和设备异常的检测点关联...” | Sheet1 第 66、68、70 行 | 创建分类和知识,关联检测点,触发异常后查看知识并完成审核 | 提升故障处理复用能力 |
|
||||
| OPS-027 | 报表管理 | 强制 | P1 | 支持 TopN 报表、统计报告、流量统计、故障报告、服务器报表、网络设备报表;可对指定单台或多台设备、任意监测指标、任意时段进行统计并导出;故障报告展示故障设备、检测点、IP、类型、次数、百分比、开始/结束时间、持续时间;服务器报表展示可用性、响应时间、CPU、物理/虚拟内存、磁盘 I/O、磁盘使用率、今日告警次数;网络设备报表展示可用性、服务成功率、平均响应时间、抖动、CPU、内存、运行时间、今日告警。 | “TopN报表...统计报告...故障报告...” | 招标文件第 112 页;Sheet1 第 72-77 行 | 生成并导出 TopN、统计、流量、故障、服务器、网络设备报表 | 建议定义标准报表模板 |
|
||||
| OPS-028 | 可视化大屏管理 | 强制 | P1 | 支持用户更换大屏展示样式和内容,深度个性化配置;支持大屏分组、不同用户使用各自配置、多自定义大屏轮播、轮播时间间隔设置;视图可显示状态报告、拓扑图、实时告警、接口流量图、业务状态图表等并自由组合。 | “大屏展示支持分组展示...多个自定义的可视化大屏进行轮播展示” | 招标文件第 114 页;Sheet1 第 7 行 | 配置个人/分组大屏、轮播、数据组件和自由组合页面 | 可作为可视化验收重点 |
|
||||
| OPS-029 | 用户权限管理 | 强制 | P1 | 支持用户分组、角色、用户权限、数据权限,保障用户在权限内使用系统和管理对象;支持用户新增、编辑、删除、停用、启用、关联到组;支持用户组新增、编辑、删除、关联用户、关联角色、关联数据权限;支持角色新增、编辑、删除、关联组、关联操作权限。 | “用户权限支持管理用户的分组、角色、用户权限...” | 招标文件第 114-115 页;Sheet1 第 79 行 | 创建用户、用户组、角色,验证功能权限和数据权限隔离 | 保障系统安全性 |
|
||||
| OPS-030 | 系统管理 | 强制 | P1 | 支持部门、用户、角色、数据字典、群组、岗位、参数配置、第三方账户、分类字典、Logo/登录页背景、常用语、消息列表、消息模板、系统日志等系统基础数据和配置管理。 | “系统基础数据,维护部门信息...” | 招标文件第 114-115 页 | 权限配置、用户角色、日志下载、消息模板配置 | 与用户权限管理配套 |
|
||||
| OPS-031 | 采集管理 | 强制 | P1 | 支持定时任务、监控面板自定义、模板导入导出、主机添加和导入、主机群组、监控模板、指标维护、自动发现、自动分组、自动发现策略。 | “支持定时任务...监控面板...模板...主机管理...自动发现” | 招标文件第 113 页 | 新增主机、配置模板、自动发现、策略验证 | 平台运维配置能力 |
|
||||
| OPS-032 | 代理管理 | 强制 | P2 | 支持跨网、跨地区部署代理,监控数据统一汇总,支持主动式和被动式数据推送。 | “支持跨网、跨地区部署代理,监控数据统一汇总” | 招标文件第 114 页 | 部署代理并验证数据汇聚 | 适合内外网隔离场景 |
|
||||
| OPS-033 | 业务系统视图与业务拓扑 | 强制 | P1 | 支持按业务系统分类、等保级别、网络类型等多维度树状展示,展示业务系统关联资源告警、健康度、影响范围、业务拓扑、时间轴、运维笔记和文档;支持手动创建业务拓扑架构图,显示资源实时状态,支持分块组合和不同品牌型号设备图标自定义。 | “支持按业务系统分类...提供手动方式创建业务拓扑架构图” | 招标文件第 112 页 | 建立业务系统视图并绘制业务拓扑,验证资源、告警、健康度联动 | 建议围绕 HIS/LIS/PACS/EMR 建模 |
|
||||
|
||||
|
||||
## 3. 模块化功能清单
|
||||
|
||||
| 模块 | 功能 | 对应需求编号 | 用户/角色 | 输入 | 处理 | 输出 | 验收点 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 首页总览 | 待处理告警、设备总览、告警趋势、网络监控、模块化总览 | OPS-001 | 运维人员/管理者 | 设备状态、告警状态、网络状态 | 汇总、趋势分析、模块化展示 | 首页总览视图 | 总览数据准确,模块可配置 |
|
||||
| 综合监控 | 操作系统、服务器硬件、网络设备、安全设备、存储、数据库、中间件、虚拟化统一监控 | OPS-002 至 OPS-009 | 运维中心/专项管理员 | 主机、设备、数据库、中间件、虚拟化平台连接和指标 | 纳管、指标采集、状态判断、历史记录 | 全域资源监控视图 | 多品牌、多类型资源接入并展示指标 |
|
||||
| 日志与 Trap | Syslog、SNMP Trap、Trap 字典、OID 解析、告警关联、Trap 屏蔽 | OPS-010 | 运维/网络管理员 | Syslog、Trap、规则、字典、屏蔽策略 | 采集、解析、规则匹配、关联告警 | 日志与 Trap 告警视图 | 日志可读、规则生效、屏蔽可验证 |
|
||||
| URL 与业务可用性 | URL、网页、Web 业务流程、端口、服务、进程、Webservice、调用链、业务日志 | OPS-011, OPS-033 | 应用运维 | URL/API、业务系统、端口、服务、日志 | 可用性探测、链路分析、业务建模 | 业务可用性和业务健康视图 | 模拟故障可告警,业务拓扑可联动 |
|
||||
| 动环环境监控 | 电力、UPS、空调、温湿度、消防、门禁、漏水、有害气体 | OPS-012, OPS-023 | 机房运维 | 动环设备、传感器、阈值 | 状态采集、阈值判断、机房关联 | 动环状态和告警 | 动环设备状态与机房视图联动 |
|
||||
| 网络拓扑 | 拓扑展示、全景图、告警点击、设备和链路统计、布局、下载、子拓扑、区域、链路管理 | OPS-013 | 网络管理员 | 设备、链路、区域、拓扑分组 | 可视化编排、布局、刷新、统计 | 网络拓扑图 | 拓扑可编辑、可查看告警和链路流量 |
|
||||
| 流量分析 | 应用、协议、会话、流向分析,异常检测,WAN 监测,实时预警,链路管理,参数配置 | OPS-014, OPS-015 | 网络管理员 | 链路、流量数据、应用端口、协议、保存周期 | 流量解析、统计、异常识别、策略配置 | 流量分析报表和预警 | 能定位高占用应用/协议/设备/流向 |
|
||||
| IP 地址管理 | 子网、网段、IP、MAC、分配/使用/保留状态、分组分层、自动扫描、IP 报表 | OPS-016, OPS-017 | 网络管理员 | IP 段、扫描规则、DHCP/地址状态 | 扫描、分组、统计、生命周期管理 | IP 台账和 IP 报表 | 新 IP 自动发现,冲突和变更可追踪 |
|
||||
| 告警中心 | 告警降噪、策略、模板、通知、级别、升级、受理、历史、导出 | OPS-018 至 OPS-021 | 值班人员/负责人 | 指标异常、事件、阈值、通知配置、策略 | 去重、压缩、屏蔽、升级、派单、记录 | 告警状态、通知记录、历史告警 | 告警闭环、升级和多渠道通知可演示 |
|
||||
| 工单管理 | 手动/自动工单、接单、转交、撤回、挂起、重启、关闭、告警自动派单 | OPS-022 | 运维人员/负责人 | 告警事件、运维任务、处理人 | 创建、派单、流转、通知、关闭 | 工单列表和处理记录 | 告警自动转工单,工单流转完整 |
|
||||
| 数据中心管理 | 数据中心层级、机房、楼层、动环集成、机柜接入、集中展示 | OPS-023 | 机房运维 | 省份、城市、数据中心、楼层、机房、动环设备 | 层级建模、设备关联、能力展示 | 数据中心/机房统一视图 | 多层级数据中心结构可维护 |
|
||||
| 机柜与 U 位 | 机柜设计、机柜布局、设备位置、U 位占用、设备状态、告警、空间容量 | OPS-024 | 机房运维 | 机柜、设备、U 位、监控状态 | 可视化布局、状态绑定、容量计算 | 机柜图和空间容量视图 | 设备位置、U 位和剩余空间准确 |
|
||||
| 资产管理 | 设备新增、删除、空间容量重算、上架规划参考 | OPS-025 | 资产/机房运维 | 设备资产、机柜空间、变更信息 | 资产变更、容量计算 | 资产台账和空间规划结果 | 设备变更后容量自动更新 |
|
||||
| 知识库管理 | 分类、知识发布、浏览、附件下载、异常检测点关联、审核、审核日志 | OPS-026 | 运维人员/知识管理员 | 知识条目、附件、分类、检测点、审核意见 | 分类管理、发布审核、关联检索 | 知识库和故障处理参考 | 异常时可查看关联知识 |
|
||||
| 报表管理 | TopN、统计报告、流量统计、故障报告、服务器报表、网络设备报表、导出 | OPS-027 | 管理者/运维 | 历史指标、告警、设备、时间范围 | 统计、排序、汇总、导出 | 运维报表 | 多类报表可生成并导出 |
|
||||
| 可视化大屏 | 大屏分组、个人配置、组件组合、状态报告、拓扑、实时告警、接口流量、业务图表、轮播 | OPS-028 | 运维中心/领导 | 设备、链路、指标、告警、业务状态 | 可视化编排、动态绑定、轮播展示 | 运维驾驶舱和专题大屏 | 多用户配置和轮播生效 |
|
||||
| 用户权限管理 | 用户、用户组、角色、功能权限、数据权限、停启用、关联组/角色/权限 | OPS-029 | 系统管理员 | 组织、账号、角色、权限范围 | 授权、分权、停启用、关联 | 权限配置结果 | 功能权限和数据权限隔离 |
|
||||
| 系统管理 | 部门、用户、角色、字典、群组、岗位、参数、第三方账户、Logo、消息、日志 | OPS-030 | 系统管理员 | 基础数据、系统参数、消息模板、日志 | 基础数据维护、参数配置、日志管理 | 管理后台 | 权限、日志、消息模板可配置 |
|
||||
| 采集与代理 | 定时任务、面板模板、主机管理、群组、指标、自动发现、跨网代理、主动/被动推送 | OPS-031, OPS-032 | 平台管理员 | 主机、模板、策略、代理节点、采集数据 | 配置、发现、分组、采集、汇总 | 采集配置和汇总监控数据 | 自动发现与跨网数据汇聚可验证 |
|
||||
| 业务系统管理 | 业务分类、等保级别、网络类型、健康度、影响范围、业务拓扑、时间轴、笔记、文档 | OPS-033 | 应用负责人 | 业务系统、关联资源、告警、文档 | 建模、关联、健康评估、拓扑编排 | 业务健康视图和业务拓扑 | HIS/LIS/PACS/EMR 可建模 |
|
||||
|
||||
<!-- ## 4. 验收建议清单
|
||||
|
||||
| 验收项 | 建议验收动作 | 通过标准 |
|
||||
| --- | --- | --- |
|
||||
| 资源接入 | 接入服务器、数据库、虚拟化、网络设备、安全设备、存储设备各至少 1 类样例 | 能展示实时指标、状态、历史趋势 |
|
||||
| 业务建模 | 建立 HIS、LIS、PACS、EMR、医疗质量管理系统中的至少 1-2 个业务视图样例 | 能关联主机、数据库、网络、告警和健康度 |
|
||||
| 告警闭环 | 模拟 CPU/端口/API/数据库异常 | 能生成告警、分派、通知、移动端查看和处理 |
|
||||
| 报表 | 生成日报、月报、资源分析、未恢复告警统计 | 能在线查看并导出 |
|
||||
| 拓扑 | 生成网络拓扑和业务拓扑 | 能展示链路状态、资源实时状态、缩放和自定义图标 |
|
||||
| 大屏 | 配置运维驾驶舱和业务大屏 | 支持拖拽、动态数据绑定、跳转切换 |
|
||||
| 3D 机房 | 建立机柜和设备位置,绑定告警 | 告警能定位到机柜/设备 |
|
||||
| 权限与日志 | 创建部门、用户、角色并操作系统 | 数据权限隔离,操作日志可查询/下载 | -->
|
||||
152
docs/国产时序数据库选型验证.md
Normal file
152
docs/国产时序数据库选型验证.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# OPS 国产/国内生态时序数据库选型验证
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文用于记录 P1 时序数据库选型验证和最终决策。OPS 第 1 阶段指标样本、探测样本、接口流量和采集健康度采用 TDengine 开源版作为时序数据存储底座。
|
||||
|
||||
本文不是商业采购建议。TDengine 开源版无时间使用限制,但采用 AGPL-3.0 许可证;最终部署版本、AGPL 合规、麒麟兼容性和是否需要企业版支持仍需在现场确认。
|
||||
|
||||
## 2. 选型约束
|
||||
|
||||
| 约束 | 要求 |
|
||||
| --- | --- |
|
||||
| 部署环境 | 必须支持 Linux/麒麟验收环境,优先支持 x86_64 和 ARM64。 |
|
||||
| 后端语言 | 必须提供 Go 后端可用连接方式。 |
|
||||
| 写入能力 | 支持批量写入高频指标样本。 |
|
||||
| 查询能力 | 支持时间范围查询、聚合、降采样或等效能力。 |
|
||||
| 保留策略 | 支持数据保留周期或 TTL。 |
|
||||
| 运维 | 具备备份恢复、权限控制、日志和基础监控能力。 |
|
||||
| 风险 | TDengine 开源版的 AGPL 合规、国产化适配、社区活跃度和长期维护风险可评估。 |
|
||||
|
||||
## 3. 官方资料依据
|
||||
|
||||
| 产品 | 官方资料 | 本文采用的信息 |
|
||||
| --- | --- | --- |
|
||||
| TDengine | https://docs.tdengine.com/tdengine-reference/supported-platforms/ | 官方平台矩阵列出 Linux、Windows、Galaxy Kirin V10、NeoKylin 等支持情况,并说明 Go 连接器平台支持。 |
|
||||
| TDengine | https://docs.tdengine.com/tdengine-reference/client-libraries/go/ | Go 驱动实现 `database/sql`,WebSocket 连接为迁移方向,原生和 REST 连接计划在 2027-01-01 停用。 |
|
||||
| Apache IoTDB | https://iotdb.apache.org/UserGuide/latest/API/Programming-Go-Native-API.html | Go Native API 支持 `Session` 和 `SessionPool`,并提供批量写入、范围查询、聚合查询等接口。 |
|
||||
| openGemini | https://docs.opengemini.org/guide/quick_start/get_started.html | openGemini 支持 x86-64、ARM-64 和主流 Linux,单节点默认 HTTP 端口为 8086,并提供基础写入查询示例。 |
|
||||
|
||||
## 4. 候选产品验证
|
||||
|
||||
### 4.1 TDengine
|
||||
|
||||
| 项目 | 验证结论 |
|
||||
| --- | --- |
|
||||
| Linux/麒麟 | 官方平台矩阵覆盖 Galaxy Kirin V10、NeoKylin 等,但部分能力标注与企业版相关,需确认 OSS/企业版边界。 |
|
||||
| CPU 架构 | 服务端和连接器支持 x64、ARM64;Go 连接器覆盖 Linux、Windows、macOS。 |
|
||||
| Go 连接 | 官方 Go 驱动实现 `database/sql`;建议采用 WebSocket 连接路径。 |
|
||||
| 批量写入 | 支持 SQL 和 schemaless 写入,适合指标样本。 |
|
||||
| 查询 | 支持时间范围查询和聚合查询。 |
|
||||
| 保留策略 | 支持 TTL 或库表级保留策略,需在样例库验证。 |
|
||||
| 风险 | Go 原生连接和 REST 连接计划停用,首期必须避免新代码依赖将废弃路径。 |
|
||||
|
||||
### 4.2 Apache IoTDB
|
||||
|
||||
| 项目 | 验证结论 |
|
||||
| --- | --- |
|
||||
| Linux/麒麟 | Apache 项目,需在目标麒麟版本实测安装、服务管理和性能。 |
|
||||
| CPU 架构 | 需按现场系统包和 JVM 环境确认。 |
|
||||
| Go 连接 | Go Native API 提供 `SessionPool`,并建议多线程场景使用连接池。 |
|
||||
| 批量写入 | 提供 `InsertTablet`、`InsertRecords`、`InsertTablets` 等批量写入接口。 |
|
||||
| 查询 | 提供原始数据查询、聚合查询、SQL 查询等接口。 |
|
||||
| 保留策略 | 支持 TTL 方向,需结合版本验证。 |
|
||||
| 风险 | 设备树模型适合设备数据,但 OPS 的资源、指标、标签模型需要设计映射规则。 |
|
||||
|
||||
### 4.3 openGemini
|
||||
|
||||
| 项目 | 验证结论 |
|
||||
| --- | --- |
|
||||
| Linux/麒麟 | 官方文档列出主流 Linux、openEuler 等支持;麒麟需实测。 |
|
||||
| CPU 架构 | 官方快速开始列出 x86-64 和 ARM-64 支持。 |
|
||||
| Go 连接 | 内核使用 Go,生态接近 InfluxDB 协议;OPS 需验证 Go 客户端选型。 |
|
||||
| 批量写入 | 支持 line protocol 风格写入,适合指标。 |
|
||||
| 查询 | 支持类 InfluxQL 查询,适合时间范围和聚合。 |
|
||||
| 保留策略 | 支持数据库、保留策略方向,需在版本中验证配置项。 |
|
||||
| 风险 | 相比 TDengine 和 IoTDB,项目成熟度、企业支持和麒麟适配需要更谨慎验证。 |
|
||||
|
||||
## 5. 评分矩阵
|
||||
|
||||
分数为当前文档验证分,不替代现场压测。
|
||||
|
||||
| 维度 | 权重 | TDengine | Apache IoTDB | openGemini |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Linux/麒麟适配 | 20 | 18 | 14 | 14 |
|
||||
| Go 接入成熟度 | 15 | 14 | 13 | 10 |
|
||||
| 批量写入 | 15 | 14 | 13 | 12 |
|
||||
| 范围查询与聚合 | 15 | 14 | 14 | 12 |
|
||||
| 保留策略/降采样 | 10 | 8 | 8 | 8 |
|
||||
| 运维和备份恢复 | 10 | 8 | 8 | 7 |
|
||||
| 授权和交付风险 | 10 | 7 | 9 | 8 |
|
||||
| 团队学习成本 | 5 | 4 | 3 | 4 |
|
||||
| 总分 | 100 | 87 | 82 | 75 |
|
||||
|
||||
## 6. 选型结论
|
||||
|
||||
第 1 阶段已决策采用 TDengine 开源版。Apache IoTDB 和 openGemini 不作为首期实现目标,仅保留为后续替换或风险备选。
|
||||
|
||||
理由:
|
||||
|
||||
- TDengine 对 Linux、麒麟和多 CPU 架构的官方说明最贴近当前验收约束。
|
||||
- TDengine Go 驱动与 `database/sql` 兼容,接入成本较低。
|
||||
- Apache IoTDB 的设备树模型适合设备时序数据,但 OPS 需要额外设计资源和标签映射。
|
||||
- openGemini 适合可观测性指标场景,但首期交付要降低成熟度和现场适配风险。
|
||||
|
||||
TDengine 开源版使用边界:
|
||||
|
||||
| 项目 | 结论 |
|
||||
| --- | --- |
|
||||
| 使用期限 | 开源版无固定到期时间;不是试用版授权。 |
|
||||
| 许可证 | AGPL-3.0,需要进行开源合规确认。 |
|
||||
| 商业支持 | 若院方要求原厂支持、国产系统认证或企业特性,需要另行评估企业版或商业支持。 |
|
||||
| 技术路线 | 后端优先使用 TDengine Go WebSocket 连接路径,避免依赖官方已标记未来停用的原生或 REST 路径。 |
|
||||
| 部署验证 | 必须在目标 Linux/麒麟版本上完成安装、写入、查询、保留策略、备份恢复和故障降级验证。 |
|
||||
|
||||
首期实现仍必须通过时序库适配层隔离 TDengine,避免业务逻辑直接依赖 TDengine SQL 方言,保留后续替换空间。
|
||||
|
||||
## 7. 适配层接口
|
||||
|
||||
后端建议定义最小接口:
|
||||
|
||||
| 方法 | 输入 | 输出 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `WriteSamples` | 样本数组 | 写入结果 | 支持批量写入。 |
|
||||
| `QueryRange` | 资源、指标、时间范围、标签 | 样本序列 | 用于资源详情趋势图。 |
|
||||
| `QueryAggregate` | 资源、指标、时间范围、聚合窗口 | 聚合序列 | 用于报表和大屏。 |
|
||||
| `CheckHealth` | 无 | 健康状态 | 用于部署烟测和自监控。 |
|
||||
| `EnsureRetention` | 保留策略配置 | 执行结果 | 初始化或校验保留策略。 |
|
||||
|
||||
业务代码只依赖适配层,不直接拼接 TDengine 专有查询。
|
||||
|
||||
## 8. 验证计划
|
||||
|
||||
| 步骤 | 验证内容 | 通过标准 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 在目标 Linux/麒麟环境安装 TDengine 开源版 | 服务可启动,端口和日志正常。 |
|
||||
| 2 | 后端使用 Go 写入 10 万条样本 | 无明显错误,写入耗时可记录。 |
|
||||
| 3 | 按 `resource_id + metric_code + 时间范围` 查询 | 返回正确序列,能映射回资源。 |
|
||||
| 4 | 执行 1 分钟、5 分钟、1 小时聚合 | 聚合结果正确。 |
|
||||
| 5 | 配置保留策略或 TTL | 过期策略可查询和验证。 |
|
||||
| 6 | 执行备份恢复演练 | 恢复后样本可查。 |
|
||||
| 7 | 权限验证 | 只读账号不能写入,写入账号不能管理系统配置。 |
|
||||
| 8 | 故障验证 | 时序库不可用时,后端返回局部失败并记录 `traceId`。 |
|
||||
|
||||
## 9. 验收影响
|
||||
|
||||
| 功能 | 依赖时序库 | 降级策略 |
|
||||
| --- | --- | --- |
|
||||
| 资源详情趋势图 | 是 | 基本信息可见,趋势区显示局部错误。 |
|
||||
| 首页资源健康 | 部分依赖 | 显示最近成功采集时间和数据过期提示。 |
|
||||
| 告警阈值 | 是 | 采集失败转内部事件,不伪造阈值结果。 |
|
||||
| 报表 | 是 | 异步失败可重试,保留失败原因。 |
|
||||
| 大屏 | 是 | 单组件失败不影响整屏。 |
|
||||
|
||||
## 10. 未决项
|
||||
|
||||
| 未决项 | 当前处理 |
|
||||
| --- | --- |
|
||||
| AGPL 合规 | 需要项目、院方或法务确认开源版使用方式是否满足 AGPL-3.0 要求。 |
|
||||
| 企业版需求 | 若验收要求原厂支持、认证或企业特性,需要商务和现场运维确认。 |
|
||||
| 麒麟具体版本 | 验收环境确定后实测。 |
|
||||
| 高可用部署 | 第 1 阶段可先单节点或小集群验证,正式上线前补 HA 方案。 |
|
||||
| 压测指标 | `server/` 初始化后补写入吞吐、查询 p95/p99 和资源占用基线。 |
|
||||
137
docs/本地开发与验收部署说明.md
Normal file
137
docs/本地开发与验收部署说明.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# OPS 本地开发与验收部署说明
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文区分本地开发、联调验收和 Linux/麒麟部署三类场景,避免把模板 README 直接作为交付部署手册。
|
||||
|
||||
当前仓库尚未保留实际 `server/` 和 `web/` 工程。后续实际工程创建后,应在对应目录补充更细的运行说明。
|
||||
|
||||
## 2. 场景边界
|
||||
|
||||
| 场景 | 目标 | 主要使用者 | 命令口径 |
|
||||
| --- | --- | --- | --- |
|
||||
| 本地开发 | 开发、调试、单元测试、接口联调 | 开发工程师 | Windows PowerShell |
|
||||
| 联调验收 | 连接真实后端 API,准备演示数据和证据 | 交付工程师、测试人员 | Windows PowerShell 为主 |
|
||||
| 生产或验收部署 | 在 Linux/麒麟内网环境部署服务 | 部署工程师 | 本文只写步骤和检查项,不写 Linux 命令示例 |
|
||||
|
||||
## 3. 本地开发目录
|
||||
|
||||
| 目录 | 职责 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| `server/` | 实际后端工程,基于 `templates/server_sample/` 初始化后继续改造 | 尚未创建 |
|
||||
| `web/` | 实际前端工程,基于 `templates/front_sample/standard` 初始化 | 尚未创建 |
|
||||
| `deploy/` | 部署、迁移、回滚、烟测说明和配置示例 | 已创建 |
|
||||
| `docs/` | 需求、架构、验收和测试文档 | 已存在 |
|
||||
|
||||
模板目录默认只读,不作为最终交付目录。
|
||||
|
||||
## 4. 初始化实际工程
|
||||
|
||||
后端已初始化。若在干净仓库中重新初始化,可参考:
|
||||
|
||||
```powershell
|
||||
Copy-Item -LiteralPath .\templates\server_sample -Destination .\server -Recurse
|
||||
```
|
||||
|
||||
前端初始化:
|
||||
|
||||
```powershell
|
||||
Copy-Item -LiteralPath .\templates\front_sample\standard -Destination .\web -Recurse
|
||||
```
|
||||
|
||||
初始化后需要检查:
|
||||
|
||||
| 检查项 | 要求 |
|
||||
| --- | --- |
|
||||
| 配置示例 | 只提交 `etc/*.example.yaml`、`*.example.yaml` 或 `.env.example`,不提交真实密码。 |
|
||||
| 目录结构 | 后端延续 `cmd/`、`internal/config/`、`internal/logic/`、`internal/models/`、`internal/routers/`。 |
|
||||
| 前端结构 | API 放入 `src/api/`,页面放入 `src/views/`,状态放入 Pinia。 |
|
||||
| 模板引用 | 业务代码不直接写回 `templates/`。 |
|
||||
|
||||
## 5. 本地后端开发
|
||||
|
||||
常用命令:
|
||||
|
||||
```powershell
|
||||
Set-Location .\server
|
||||
go mod tidy
|
||||
go test ./...
|
||||
go vet ./...
|
||||
gofmt -w .
|
||||
go run .\cmd\main\main.go
|
||||
```
|
||||
|
||||
迁移命令按实际 CLI 保留:
|
||||
|
||||
```powershell
|
||||
Set-Location .\server
|
||||
go run .\cmd\cli\main.go migrate
|
||||
```
|
||||
|
||||
后端重新初始化后,应只提交 `server/etc/*.example.yaml` 或等价 `.example` 配置文件。本地真实配置应复制为 `server/etc/ops_dev.yaml` 等文件后再填写,并在 `server/.gitignore` 中排除。
|
||||
|
||||
后端本地配置要求:
|
||||
|
||||
| 配置 | 要求 |
|
||||
| --- | --- |
|
||||
| PostgreSQL | 本地可使用开发库;测试不能用 mock 数据库。 |
|
||||
| SQLite 内存库 | 仅用于后端测试,验证真实 SQL 行为。 |
|
||||
| 时序数据库 | 使用 TDengine 开源版;本地和验收环境均通过适配层配置连接信息,不在代码中写死地址或凭据。 |
|
||||
| 通知渠道 | 本地使用测试账号或受控沙箱,不能提交真实凭据。 |
|
||||
| traceId | 所有接口和任务日志必须可关联。 |
|
||||
|
||||
## 6. 本地前端开发
|
||||
|
||||
常用命令:
|
||||
|
||||
```powershell
|
||||
Set-Location .\web
|
||||
pnpm install
|
||||
pnpm dev
|
||||
pnpm type:check
|
||||
pnpm lint
|
||||
pnpm build
|
||||
```
|
||||
|
||||
前端联调要求:
|
||||
|
||||
| 项目 | 要求 |
|
||||
| --- | --- |
|
||||
| API 地址 | 通过本地配置读取,不写死到组件中。 |
|
||||
| 状态覆盖 | 页面按 `docs/首期UI状态覆盖.md` 实现。 |
|
||||
| mock 数据 | 只用于独立调试,验收必须连接真实后端 API。 |
|
||||
| 文案 | 按模板 i18n 方式维护,不在组件中散落重复字符串。 |
|
||||
|
||||
## 7. Linux/麒麟验收部署检查项
|
||||
|
||||
部署说明放入 `deploy/README.md`。验收前必须确认:
|
||||
|
||||
| 检查项 | 要求 |
|
||||
| --- | --- |
|
||||
| 操作系统 | 明确 Linux 或麒麟版本、CPU 架构、补丁状态。 |
|
||||
| 运行用户 | 后端、前端静态服务、数据库、时序库使用独立低权限账号。 |
|
||||
| 网络策略 | 明确 Web 端口、API 端口、数据库端口、Trap/Syslog 接收端口和防火墙策略。 |
|
||||
| 数据库 | PostgreSQL 版本、初始化库、备份路径、恢复演练方式。 |
|
||||
| 时序库 | 选定产品版本、部署形态、保留策略、备份恢复方式。 |
|
||||
| 配置 | 只使用现场配置文件,不把真实凭据提交进仓库。 |
|
||||
| 日志 | 应用日志、审计日志、采集日志、通知日志有保存周期。 |
|
||||
| 回滚 | 后端二进制、前端静态包、数据库迁移均有回滚方案。 |
|
||||
| 烟测 | 登录、资源列表、告警列表、通知记录、工单流转、报表查询可验证。 |
|
||||
|
||||
## 8. 验收材料目录
|
||||
|
||||
```text
|
||||
验收证据/
|
||||
01-首页总览/
|
||||
02-资源监控/
|
||||
03-H3C网络设备/
|
||||
04-告警闭环/
|
||||
05-通知记录/
|
||||
06-工单闭环/
|
||||
07-报表大屏/
|
||||
08-权限审计/
|
||||
09-3D机房接口/
|
||||
10-部署与烟测/
|
||||
```
|
||||
|
||||
证据目录不要求当前仓库立即创建。正式验收时应保存截图、录像、接口响应、日志和数据库查询结果。
|
||||
BIN
docs/源文件/菜单规划表3.0.xlsx
Normal file
BIN
docs/源文件/菜单规划表3.0.xlsx
Normal file
Binary file not shown.
BIN
docs/源文件/西藏自治区人民医院信息系统硬件支撑平台升级改造项目_招标文件.pdf
Normal file
BIN
docs/源文件/西藏自治区人民医院信息系统硬件支撑平台升级改造项目_招标文件.pdf
Normal file
Binary file not shown.
189
docs/首期UI状态覆盖.md
Normal file
189
docs/首期UI状态覆盖.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# OPS 首期 UI 状态覆盖
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文定义第 1 阶段前端页面必须覆盖的状态,避免只实现“有数据时的页面”。所有页面至少需要考虑加载、空态、错误、成功、部分成功、无权限、操作中、操作失败和数据过期状态。
|
||||
|
||||
本文用于指导 `web/src/views/`、`web/src/components/`、`web/src/api/` 和验收矩阵编写。
|
||||
|
||||
## 2. 通用状态定义
|
||||
|
||||
| 状态 | 含义 | 页面要求 |
|
||||
| --- | --- | --- |
|
||||
| 加载中 | 首次进入或刷新数据时接口未返回 | 使用骨架屏、表格加载或局部加载,不阻塞整个系统导航。 |
|
||||
| 空态 | 接口成功但无数据 | 给出下一步动作,如新增、导入、配置采集或调整筛选。 |
|
||||
| 错误 | 接口失败或后端返回错误码 | 显示失败原因、traceId 和重试入口。 |
|
||||
| 成功 | 数据完整返回 | 显示主要数据、关键操作和更新时间。 |
|
||||
| 部分成功 | 页面部分组件或部分接口失败 | 成功部分正常显示,失败组件显示局部错误,不整页崩溃。 |
|
||||
| 无权限 | 用户无功能权限或数据权限 | 隐藏敏感数据,操作按钮禁用或不展示,保留必要说明。 |
|
||||
| 操作中 | 用户提交确认、派单、关闭、导出等动作 | 按钮进入 loading,防止重复提交。 |
|
||||
| 操作失败 | 用户动作失败 | 保留当前页面状态,显示失败原因和可恢复动作。 |
|
||||
| 数据过期 | 数据不是最新或采集延迟 | 显示最近更新时间、最近成功采集时间和刷新入口。 |
|
||||
|
||||
通用交互要求:
|
||||
|
||||
- 所有错误提示必须带 `traceId`,便于后端排查。
|
||||
- 删除、关闭、忽略、屏蔽、策略变更等动作必须二次确认。
|
||||
- 表格页必须支持筛选条件保留、分页、刷新和空结果提示。
|
||||
- 无权限不是错误;应区分“没有权限”和“接口失败”。
|
||||
- 部分成功不能伪装成成功,必须提示哪些数据不可用。
|
||||
|
||||
## 3. 首页总览
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 加载中 | 顶部指标、告警趋势、资源健康、网络状态分别显示骨架屏 | 首屏无明显闪烁,组件独立加载。 |
|
||||
| 空态 | 未接入资源时显示接入资源、配置采集、导入样例数据入口 | 不显示假数据。 |
|
||||
| 错误 | 总览接口失败时显示错误摘要和重试按钮 | 可复制 traceId。 |
|
||||
| 成功 | 显示待处理告警、资源总数、健康度、告警趋势、网络状态 | 数据来自真实接口。 |
|
||||
| 部分成功 | 某一组件失败时仅该组件显示失败,其余组件正常 | 例如告警趋势失败不影响资源健康。 |
|
||||
| 无权限 | 隐藏无权限模块,显示可访问模块 | 数据权限生效。 |
|
||||
| 数据过期 | 显示最近刷新时间和采集延迟提示 | 采集失败不被隐藏。 |
|
||||
|
||||
## 4. 综合监控
|
||||
|
||||
### 4.1 资源列表
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 加载中 | 表格加载,筛选区可见但禁用提交 | 避免重复请求。 |
|
||||
| 空态 | 显示新增资源、导入资源、自动发现入口 | 空态能引导首批资源接入。 |
|
||||
| 错误 | 查询失败时保留筛选条件并允许重试 | 不清空用户筛选。 |
|
||||
| 成功 | 显示资源名称、类型、IP、业务系统、采集状态、最高告警级别 | H3C/华三设备可识别厂商和型号。 |
|
||||
| 部分成功 | 资源信息可见但实时指标缺失时标注“指标暂不可用” | 不把指标缺失当资源不存在。 |
|
||||
| 无权限 | 只展示数据权限内资源 | 越权资源不可见。 |
|
||||
| 数据过期 | 显示最近成功采集时间和失败原因 | 采集失败可追踪。 |
|
||||
|
||||
### 4.2 资源详情
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 加载中 | 基本信息、指标趋势、告警、采集任务分区加载 | 局部加载不阻塞页面。 |
|
||||
| 空态 | 无指标时提示绑定模板或配置采集任务 | 有明确下一步。 |
|
||||
| 错误 | 某类指标查询失败时显示局部错误 | 基本信息仍可查看。 |
|
||||
| 成功 | 展示指标趋势、采集任务、关联告警、业务系统、资产位置 | 时序指标来自时序数据库。 |
|
||||
| 部分成功 | 部分指标或接口流量缺失时标记缺失项 | 不影响其他指标。 |
|
||||
| 无权限 | 无权查看敏感指标时隐藏具体值 | 权限提示清楚。 |
|
||||
| 数据过期 | 显示最近采集成功时间、当前采集状态 | 支持手动刷新。 |
|
||||
|
||||
## 5. 告警中心
|
||||
|
||||
### 5.1 实时告警
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 加载中 | 告警列表 loading,筛选条件可见 | 不阻塞导航。 |
|
||||
| 空态 | 显示“暂无待处理告警”,提供查看历史告警入口 | 不用假告警填充。 |
|
||||
| 错误 | 查询失败时显示错误、traceId、重试 | 可定位后端问题。 |
|
||||
| 成功 | 显示级别、资源、业务系统、摘要、首次触发、最近触发、状态、处理动作 | 告警与资源、业务系统连通。 |
|
||||
| 部分成功 | 告警可见但资源详情缺失时标注“资源上下文不可用” | 告警不因上下文失败而消失。 |
|
||||
| 无权限 | 无权确认、忽略、派单时禁用按钮 | 权限边界清楚。 |
|
||||
| 操作中 | 确认、忽略、派单按钮 loading | 防止重复提交。 |
|
||||
| 操作失败 | 保留告警原状态,显示失败原因 | 不乐观修改为成功。 |
|
||||
|
||||
### 5.2 告警详情
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 成功 | 展示原始事件、规则命中、降噪命中、通知记录、关联事件、工单、审计 | 能证明告警链路。 |
|
||||
| 部分成功 | 通知记录或工单记录加载失败时局部提示 | 主告警仍可处理。 |
|
||||
| 数据过期 | 提示告警最近更新时间和恢复判断时间 | 避免误判当前状态。 |
|
||||
|
||||
### 5.3 策略与模板
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 空态 | 无策略时提供创建阈值规则、Trap 规则、通知策略入口 | 能从空系统开始配置。 |
|
||||
| 错误 | 规则校验失败时定位到字段 | 不允许保存无效规则。 |
|
||||
| 成功 | 策略列表显示作用范围、状态、最近命中次数 | 规则可管理。 |
|
||||
| 无权限 | 无权编辑策略时只读展示 | 变更权限受控。 |
|
||||
|
||||
## 6. 通知中心
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 加载中 | 通知记录列表加载 | 保留筛选条件。 |
|
||||
| 空态 | 无通知记录时提示先配置告警通知策略 | 引导明确。 |
|
||||
| 错误 | 查询发送记录失败时显示重试 | 可查看 traceId。 |
|
||||
| 成功 | 显示站内消息、短信、邮件的发送状态、接收人、失败原因 | 三类优先渠道可验证。 |
|
||||
| 部分成功 | 某渠道失败时显示局部失败,不影响其他渠道 | 通知失败不阻塞告警处理。 |
|
||||
| 操作失败 | 手动重发失败时显示渠道返回原因 | 可再次重试。 |
|
||||
|
||||
## 7. 工单管理
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 加载中 | 工单列表 loading | 可保留筛选条件。 |
|
||||
| 空态 | 无工单时提供手动创建工单入口 | 不影响告警列表。 |
|
||||
| 错误 | 查询失败或流转失败显示原因 | 工单状态不被错误更新。 |
|
||||
| 成功 | 展示工单编号、来源、资源、处理人、状态、优先级、更新时间 | 告警工单能回链告警。 |
|
||||
| 部分成功 | 关联告警或资源加载失败时局部提示 | 工单主体仍可处理。 |
|
||||
| 无权限 | 无权接单、转交、关闭时按钮禁用 | 数据权限和功能权限生效。 |
|
||||
| 操作中 | 接单、转交、挂起、关闭按钮 loading | 防重复提交。 |
|
||||
| 操作失败 | 并发关闭、无权限、状态非法时提示具体原因 | 状态机约束可见。 |
|
||||
|
||||
## 8. 报表管理
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 加载中 | 报表生成进度或图表加载 | 大范围查询不阻塞页面。 |
|
||||
| 空态 | 时间范围无数据时提示调整条件 | 不显示 0 值假图。 |
|
||||
| 错误 | 生成失败时显示原因和重试 | 支持 traceId。 |
|
||||
| 成功 | 可查看并导出 TopN、故障、服务器、网络设备报表 | 数据来自指标、告警、工单真实记录。 |
|
||||
| 部分成功 | 部分图表无数据或失败时局部提示 | 报表整体可读。 |
|
||||
| 无权限 | 不展示无权限资源和敏感字段 | 导出也受权限控制。 |
|
||||
| 数据过期 | 显示报表生成时间和数据截止时间 | 避免误解为实时数据。 |
|
||||
|
||||
## 9. 可视化大屏
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 加载中 | 大屏组件分区加载 | 不整屏空白。 |
|
||||
| 空态 | 未配置大屏时提供创建或选择模板入口 | 首次使用可继续。 |
|
||||
| 错误 | 某组件失败显示局部错误 | 不影响其他组件轮播。 |
|
||||
| 成功 | 展示资源健康、实时告警、接口流量、业务状态、拓扑入口 | 数据来自后端。 |
|
||||
| 部分成功 | 某数据源失败时显示降级提示 | 不用静态图冒充。 |
|
||||
| 无权限 | 隐藏无权限业务或资源 | 大屏权限生效。 |
|
||||
| 数据过期 | 显示最后刷新时间 | 可手动刷新。 |
|
||||
|
||||
## 10. 业务系统视图
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 空态 | 无业务系统时提供创建业务系统入口 | 支持 HIS/LIS/PACS/EMR 建模。 |
|
||||
| 成功 | 展示业务健康、关联资源、告警、影响范围、时间轴、文档 | 业务视图与资源/告警连通。 |
|
||||
| 部分成功 | 拓扑或时间轴失败时局部提示 | 业务基础信息可见。 |
|
||||
| 无权限 | 无权业务系统不可见 | 数据权限隔离。 |
|
||||
|
||||
## 11. 3D 机房后端接口页面
|
||||
|
||||
第 1 阶段 OPS 不交付 3D 前端,但需要提供后端接口和接口验证页面或接口文档。
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 空态 | 未导入真实机柜数据时返回样例数据或空层级提示 | 不阻塞接口联调。 |
|
||||
| 成功 | 返回数据中心、机房、机柜、U 位、设备、告警状态 | 外包前端可消费。 |
|
||||
| 部分成功 | 设备资产未绑定监控资源时标注未绑定 | 不影响机柜结构展示。 |
|
||||
| 无权限 | 无权机房或机柜不返回 | 权限生效。 |
|
||||
| 数据过期 | 返回数据更新时间 | 外包前端可展示刷新状态。 |
|
||||
|
||||
## 12. 用户权限与系统管理
|
||||
|
||||
| 状态 | 页面表现 | 验收点 |
|
||||
| --- | --- | --- |
|
||||
| 空态 | 无角色、无用户组时提供创建入口 | 可从空系统初始化。 |
|
||||
| 错误 | 保存权限失败时显示字段级错误 | 不保存半成品。 |
|
||||
| 成功 | 用户、角色、数据权限、字典、参数可维护 | 权限变更写审计。 |
|
||||
| 无权限 | 非管理员不可访问或只读 | 管理入口受控。 |
|
||||
| 操作失败 | 删除被引用角色、停用当前用户等操作被拦截 | 规则清晰。 |
|
||||
|
||||
## 13. 全局验收清单
|
||||
|
||||
- [ ] 每个页面都有加载、空态、错误、成功、部分成功、无权限状态。
|
||||
- [ ] 告警、工单、策略等关键操作有操作中、操作成功、操作失败反馈。
|
||||
- [ ] 所有错误反馈包含 `traceId`。
|
||||
- [ ] 部分成功不会导致整页不可用。
|
||||
- [ ] 无权限状态不泄露敏感资源名称、IP、业务系统信息。
|
||||
- [ ] 数据过期时显示最近更新时间或最近成功采集时间。
|
||||
- [ ] 报表和大屏不使用静态展示数据作为验收依据。
|
||||
- [ ] 移动端适配至少保证核心列表、详情、确认/派单动作可用;不要求独立移动端应用。
|
||||
404
docs/首期数据模型与状态机.md
Normal file
404
docs/首期数据模型与状态机.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# OPS 首期数据模型与状态机
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文用于固定第 1 阶段编码前的核心后端规格,覆盖资源监控、采集、时序指标、原始事件、告警、事件、通知、工单、审计和 3D 机房后端接口所需的最小数据模型与状态机。
|
||||
|
||||
本文不替代数据库迁移脚本。后续实现时,应以本文作为 `server/internal/models/`、`server/internal/logic/` 和接口设计的依据。
|
||||
|
||||
## 2. 范围边界
|
||||
|
||||
第 1 阶段必须覆盖:
|
||||
|
||||
- 主机、H3C/华三网络设备、数据库、虚拟化、URL/API 至少各一类样例资源。
|
||||
- 指标采集、Syslog、SNMP Trap、URL/API 探测、采集失败事件。
|
||||
- 告警去重、压缩或归并、屏蔽、抑制、确认、忽略、恢复、派单。
|
||||
- 平台站内消息、短信、邮件三类通知。
|
||||
- 工单创建、接单、转交、撤回、挂起、重启、关闭。
|
||||
- 报表、大屏、审计能追踪完整故障闭环。
|
||||
- 外包 3D 机房前端所需的数据中心、机房、机柜、U 位、设备和告警状态接口模型。
|
||||
|
||||
第 1 阶段不覆盖:
|
||||
|
||||
- 所有厂商全量适配。
|
||||
- 老运维平台迁移或集成。
|
||||
- 3D 机房前端实现。
|
||||
- 完整 IPAM、复杂拓扑编辑、全量资产审核流程。
|
||||
|
||||
## 3. 存储分工
|
||||
|
||||
| 存储 | 数据范围 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| PostgreSQL | 资源、业务系统、资产、采集任务、规则、告警、事件、工单、通知、权限、审计、报表配置 | 事务数据主库,保证一致性和审计。 |
|
||||
| TDengine 开源版 | 指标样本、探测样本、接口流量、采集健康度、容量和性能趋势 | 第 1 阶段选定的时序数据库。后端通过适配层访问,避免业务逻辑直接依赖 TDengine SQL 方言。 |
|
||||
| 文件或对象存储 | 报表导出、附件、截图、录像、大屏快照 | 第 1 阶段可先使用本地文件存储,接口保留替换能力。 |
|
||||
|
||||
设计原则:
|
||||
|
||||
- PostgreSQL 保存资源和指标定义,时序数据库保存样本值。
|
||||
- 所有时序数据必须能通过 `resource_id`、`metric_code`、时间范围回查到资源上下文。
|
||||
- 告警、事件、工单状态变化必须写审计日志。
|
||||
- 真实凭据不落业务表明文,只保存凭据引用和脱敏描述。
|
||||
|
||||
## 4. 通用字段
|
||||
|
||||
核心业务表建议统一包含:
|
||||
|
||||
| 字段 | 类型建议 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `id` | UUID 或雪花 ID | 主键。 |
|
||||
| `tenant_id` | string | 单院区可固定,保留多租户或多院区扩展。 |
|
||||
| `created_at` | timestamp | 创建时间。 |
|
||||
| `updated_at` | timestamp | 更新时间。 |
|
||||
| `created_by` | string | 创建人。 |
|
||||
| `updated_by` | string | 更新人。 |
|
||||
| `deleted_at` | timestamp nullable | 软删除时间,需要保留审计的表不做物理删除。 |
|
||||
| `version` | integer | 乐观锁版本,用于工单、规则等并发修改。 |
|
||||
|
||||
审计相关表必须额外保留:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `trace_id` | 请求链路 ID。 |
|
||||
| `operator_id` | 操作人。 |
|
||||
| `operator_name` | 操作人快照。 |
|
||||
| `source_ip` | 操作来源 IP。 |
|
||||
| `before_json` | 变更前快照,敏感字段脱敏。 |
|
||||
| `after_json` | 变更后快照,敏感字段脱敏。 |
|
||||
|
||||
## 5. 资源与资产模型
|
||||
|
||||
### 5.1 核心表
|
||||
|
||||
| 表名 | 职责 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `business_systems` | 业务系统,如 HIS、LIS、PACS、EMR | `name`、`code`、`level`、`network_zone`、`owner_org_id`、`owner_user_id`、`description` |
|
||||
| `resource_types` | 资源类型和默认模板 | `code`、`name`、`category`、`vendor`、`default_metric_template_id`、`icon` |
|
||||
| `resources` | 统一监控资源 | `name`、`resource_type_id`、`vendor`、`model`、`ip`、`hostname`、`business_system_id`、`owner_org_id`、`owner_user_id`、`asset_id`、`status`、`collect_status` |
|
||||
| `resource_relations` | 资源依赖关系 | `source_resource_id`、`target_resource_id`、`relation_type`、`description` |
|
||||
| `metric_definitions` | 指标定义 | `resource_type_id`、`metric_code`、`metric_name`、`unit`、`value_type`、`default_interval_seconds`、`threshold_hint` |
|
||||
| `metric_series` | 时序指标序列映射 | `resource_id`、`metric_definition_id`、`metric_code`、`tsdb_name`、`series_key`、`labels_json`、`retention_policy` |
|
||||
| `credentials` | 凭据引用 | `name`、`type`、`secret_ref`、`masked_summary`、`owner_org_id`、`status` |
|
||||
|
||||
### 5.2 资产与 3D 机房接口模型
|
||||
|
||||
| 表名 | 职责 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `data_centers` | 数据中心 | `name`、`province`、`city`、`address`、`status` |
|
||||
| `rooms` | 机房 | `data_center_id`、`name`、`floor`、`status` |
|
||||
| `racks` | 机柜 | `room_id`、`name`、`code`、`total_u`、`row_no`、`column_no`、`x`、`y`、`z`、`status` |
|
||||
| `rack_units` | U 位占用 | `rack_id`、`u_no`、`resource_id`、`asset_id`、`occupy_height`、`status` |
|
||||
| `assets` | 资产台账 | `asset_no`、`name`、`vendor`、`model`、`serial_no`、`resource_id`、`rack_id`、`status` |
|
||||
|
||||
3D 机房前端接口至少需要返回:
|
||||
|
||||
- 数据中心、机房、机柜层级。
|
||||
- 机柜坐标、容量、已占用 U 位、剩余 U 位。
|
||||
- 设备与资源绑定关系。
|
||||
- 设备健康状态、最高告警级别、未恢复告警数量。
|
||||
- 数据更新时间和权限过滤结果。
|
||||
|
||||
### 5.3 资源状态
|
||||
|
||||
| 状态码 | 中文名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `draft` | 草稿 | 已录入但未启用采集。 |
|
||||
| `active` | 运行中 | 正常纳管,可采集、可触发告警。 |
|
||||
| `disabled` | 已停用 | 暂停采集,不产生新告警。 |
|
||||
| `maintenance` | 维护中 | 可采集,但默认抑制或降级告警。 |
|
||||
| `decommissioned` | 已退役 | 不再采集,仅保留历史和审计。 |
|
||||
|
||||
合法流转:
|
||||
|
||||
```text
|
||||
草稿 -> 运行中
|
||||
运行中 -> 维护中 -> 运行中
|
||||
运行中 -> 已停用 -> 运行中
|
||||
运行中 -> 已退役
|
||||
已停用 -> 已退役
|
||||
```
|
||||
|
||||
非法流转:
|
||||
|
||||
- 已退役资源不能直接恢复为运行中,必须重新建档或走恢复审批。
|
||||
- 已停用资源不能生成新的业务告警,但可以生成配置或审计事件。
|
||||
|
||||
## 6. 采集与时序模型
|
||||
|
||||
### 6.1 采集任务表
|
||||
|
||||
| 表名 | 职责 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `collector_tasks` | 采集任务定义 | `resource_id`、`collector_type`、`protocol`、`credential_id`、`schedule_cron`、`interval_seconds`、`timeout_seconds`、`retry_limit`、`status` |
|
||||
| `collector_runs` | 采集执行记录 | `task_id`、`started_at`、`finished_at`、`run_status`、`success_count`、`failed_count`、`error_code`、`error_message` |
|
||||
| `discovery_tasks` | 自动发现任务 | `name`、`scan_range`、`protocols`、`credential_id`、`schedule_cron`、`status` |
|
||||
| `probe_targets` | URL/API/端口探测目标 | `resource_id`、`target_url`、`method`、`expected_status`、`timeout_seconds`、`status` |
|
||||
|
||||
### 6.2 时序样本逻辑结构
|
||||
|
||||
时序库中的指标样本必须至少包含:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `ts` | 样本时间。 |
|
||||
| `resource_id` | 资源 ID。 |
|
||||
| `resource_type` | 资源类型编码。 |
|
||||
| `metric_code` | 指标编码。 |
|
||||
| `value` | 指标值。 |
|
||||
| `unit` | 单位。 |
|
||||
| `labels` | 标签,如接口名、磁盘分区、数据库实例名。 |
|
||||
| `quality` | 样本质量:正常、缺失、估算、异常。 |
|
||||
| `collector_task_id` | 采集任务 ID。 |
|
||||
|
||||
### 6.3 采集任务状态
|
||||
|
||||
任务配置状态:
|
||||
|
||||
```text
|
||||
草稿 -> 已启用 -> 已停用
|
||||
已启用 -> 已停用 -> 已启用
|
||||
已启用 -> 已删除
|
||||
已停用 -> 已删除
|
||||
```
|
||||
|
||||
单次执行状态:
|
||||
|
||||
```text
|
||||
待执行 -> 执行中 -> 成功
|
||||
待执行 -> 执行中 -> 部分成功
|
||||
待执行 -> 执行中 -> 失败
|
||||
执行中 -> 超时
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 连续失败达到阈值时,必须生成平台内部原始事件。
|
||||
- 部分成功必须记录缺失指标,不允许只显示成功。
|
||||
- 凭据错误、设备不可达、协议超时需要区分错误码。
|
||||
|
||||
## 7. 事件与告警模型
|
||||
|
||||
### 7.1 核心表
|
||||
|
||||
| 表名 | 职责 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `raw_events` | 原始事件 | `source_type`、`source_id`、`resource_id`、`event_key`、`occurred_at`、`severity`、`title`、`message`、`payload_json`、`parse_status` |
|
||||
| `alert_rules` | 告警规则 | `name`、`scope_type`、`scope_id`、`metric_code`、`condition_expr`、`duration_seconds`、`recover_expr`、`severity`、`status` |
|
||||
| `alerts` | 告警实例 | `alert_key`、`resource_id`、`rule_id`、`first_seen_at`、`last_seen_at`、`recovered_at`、`severity`、`status`、`summary` |
|
||||
| `incidents` | 归并事件 | `incident_no`、`title`、`severity`、`status`、`owner_user_id`、`business_system_id`、`opened_at`、`closed_at` |
|
||||
| `incident_alerts` | 事件与告警关联 | `incident_id`、`alert_id`、`relation_type` |
|
||||
| `silence_policies` | 屏蔽策略 | `name`、`scope_type`、`scope_id`、`start_at`、`end_at`、`reason`、`status` |
|
||||
| `dedup_rules` | 去重规则 | `name`、`match_expr`、`dedup_window_seconds`、`status` |
|
||||
| `correlation_rules` | 关联/压缩/抑制规则 | `name`、`match_expr`、`action`、`priority`、`status` |
|
||||
| `escalation_policies` | 升级策略 | `name`、`severity`、`timeout_minutes`、`next_target_type`、`next_target_id`、`status` |
|
||||
|
||||
### 7.2 原始事件状态
|
||||
|
||||
| 状态码 | 中文名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `received` | 已接收 | 原始数据已入库。 |
|
||||
| `parsed` | 已解析 | 已识别资源、级别、事件键。 |
|
||||
| `unparsed` | 未解析 | 字典或规则不匹配。 |
|
||||
| `suppressed` | 已抑制 | 命中屏蔽、依赖或抑制规则。 |
|
||||
| `converted` | 已转告警 | 已生成或更新告警。 |
|
||||
| `archived` | 已归档 | 不再参与处理。 |
|
||||
|
||||
合法流转:
|
||||
|
||||
```text
|
||||
已接收 -> 已解析 -> 已转告警
|
||||
已接收 -> 未解析 -> 已解析 -> 已转告警
|
||||
已解析 -> 已抑制 -> 已归档
|
||||
已转告警 -> 已归档
|
||||
未解析 -> 已归档
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 未解析 Trap/Syslog 不能直接丢弃,必须可查询、可补规则重放。
|
||||
- 已抑制事件必须记录命中的策略 ID。
|
||||
|
||||
### 7.3 告警状态机
|
||||
|
||||
| 状态码 | 中文名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `firing` | 触发中 | 当前异常仍存在。 |
|
||||
| `acknowledged` | 已确认 | 值班人员已接手,但异常未恢复。 |
|
||||
| `ignored` | 已忽略 | 经人工判断无需处理。 |
|
||||
| `recovered` | 已恢复 | 恢复条件满足或收到恢复事件。 |
|
||||
| `expired` | 已失效 | 长时间未刷新且无法确认恢复。 |
|
||||
|
||||
合法流转:
|
||||
|
||||
```text
|
||||
触发中 -> 已确认 -> 已恢复
|
||||
触发中 -> 已忽略
|
||||
触发中 -> 已恢复
|
||||
触发中 -> 已失效
|
||||
已确认 -> 已恢复
|
||||
已确认 -> 已忽略
|
||||
已确认 -> 已失效
|
||||
```
|
||||
|
||||
非法流转:
|
||||
|
||||
- 已恢复告警不能重新变为触发中;同一资源同一规则再次异常时,应创建或激活新的告警周期。
|
||||
- 已忽略告警不能自动派单。
|
||||
- 已失效告警不能再确认,只能作为历史查看。
|
||||
|
||||
### 7.4 事件状态机
|
||||
|
||||
| 状态码 | 中文名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `open` | 待处理 | 已创建事件,尚未分派。 |
|
||||
| `assigned` | 已分派 | 已指定处理人或处理组。 |
|
||||
| `in_progress` | 处理中 | 处理人已开始处理。 |
|
||||
| `suspended` | 已挂起 | 等待外部条件或人工决策。 |
|
||||
| `resolved` | 已解决 | 技术处理完成,等待关闭确认。 |
|
||||
| `closed` | 已关闭 | 闭环完成。 |
|
||||
|
||||
合法流转:
|
||||
|
||||
```text
|
||||
待处理 -> 已分派 -> 处理中 -> 已解决 -> 已关闭
|
||||
处理中 -> 已挂起 -> 处理中
|
||||
已分派 -> 待处理
|
||||
已解决 -> 处理中
|
||||
```
|
||||
|
||||
非法流转:
|
||||
|
||||
- 已关闭事件不能回到处理中。
|
||||
- 未关联告警或资源的事件不能进入已关闭。
|
||||
- 已解决事件若关联告警再次触发,应退回处理中或新建事件,不能静默覆盖。
|
||||
|
||||
## 8. 通知模型
|
||||
|
||||
### 8.1 核心表
|
||||
|
||||
| 表名 | 职责 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `notification_policies` | 通知策略 | `name`、`scope_type`、`severity`、`channels`、`receiver_expr`、`status` |
|
||||
| `notification_templates` | 通知模板 | `channel`、`title_template`、`content_template`、`variables_json`、`status` |
|
||||
| `notification_records` | 通知发送记录 | `channel`、`alert_id`、`incident_id`、`receiver`、`send_status`、`retry_count`、`error_message`、`sent_at` |
|
||||
|
||||
第 1 阶段通知渠道:
|
||||
|
||||
- 平台站内消息。
|
||||
- 短信。
|
||||
- 邮件。
|
||||
|
||||
### 8.2 通知状态机
|
||||
|
||||
```text
|
||||
待发送 -> 发送中 -> 已发送
|
||||
待发送 -> 发送中 -> 失败 -> 待重试 -> 发送中
|
||||
失败 -> 已取消
|
||||
待重试 -> 已取消
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 任何通知失败都不能阻断告警和工单状态流转。
|
||||
- 通知失败必须可见、可重试、可审计。
|
||||
- 短信和邮件必须记录第三方返回码或失败原因。
|
||||
|
||||
## 9. 工单模型
|
||||
|
||||
### 9.1 核心表
|
||||
|
||||
| 表名 | 职责 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `tickets` | 工单 | `ticket_no`、`title`、`source_type`、`source_id`、`alert_id`、`incident_id`、`resource_id`、`assignee_id`、`status`、`priority`、`closed_at` |
|
||||
| `ticket_transitions` | 工单流转记录 | `ticket_id`、`from_status`、`to_status`、`operator_id`、`reason`、`created_at` |
|
||||
| `ticket_comments` | 工单处理记录 | `ticket_id`、`comment_type`、`content`、`attachments_json`、`created_by` |
|
||||
| `ticket_sla_records` | 工单时效记录 | `ticket_id`、`accepted_at`、`first_response_at`、`resolved_at`、`closed_at`、`overdue_flag` |
|
||||
|
||||
### 9.2 工单状态机
|
||||
|
||||
| 状态码 | 中文名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `created` | 已创建 | 工单已生成,等待接单。 |
|
||||
| `accepted` | 已接单 | 处理人已接收。 |
|
||||
| `in_progress` | 处理中 | 正在处理。 |
|
||||
| `transferred` | 已转交 | 转给其他处理人或处理组。 |
|
||||
| `suspended` | 已挂起 | 暂停处理,等待外部条件。 |
|
||||
| `restarted` | 已重启 | 从挂起恢复。 |
|
||||
| `withdrawn` | 已撤回 | 创建方撤回或确认无需处理。 |
|
||||
| `closed` | 已关闭 | 处理完成并关闭。 |
|
||||
|
||||
合法流转:
|
||||
|
||||
```text
|
||||
已创建 -> 已接单 -> 处理中 -> 已关闭
|
||||
已创建 -> 已撤回
|
||||
已接单 -> 已转交 -> 已接单
|
||||
处理中 -> 已转交 -> 已接单
|
||||
处理中 -> 已挂起 -> 已重启 -> 处理中
|
||||
已接单 -> 已关闭
|
||||
```
|
||||
|
||||
非法流转:
|
||||
|
||||
- 已关闭工单不能转交、撤回或挂起。
|
||||
- 已撤回工单不能重新接单,只能新建工单。
|
||||
- 同一告警不能重复自动创建多个未关闭工单。
|
||||
- 无权限用户不能关闭、转交或撤回工单。
|
||||
|
||||
## 10. 权限与审计模型
|
||||
|
||||
| 表名 | 职责 | 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `organizations` | 组织部门 | `name`、`parent_id`、`status` |
|
||||
| `users` | 用户 | `username`、`display_name`、`org_id`、`status` |
|
||||
| `roles` | 角色 | `name`、`code`、`status` |
|
||||
| `user_roles` | 用户角色 | `user_id`、`role_id` |
|
||||
| `role_permissions` | 功能权限 | `role_id`、`permission_code` |
|
||||
| `data_scopes` | 数据权限 | `subject_type`、`subject_id`、`scope_type`、`scope_expr` |
|
||||
| `audit_logs` | 审计日志 | `action`、`object_type`、`object_id`、`operator_id`、`trace_id`、`before_json`、`after_json` |
|
||||
|
||||
必须审计的动作:
|
||||
|
||||
- 资源新增、修改、停用、退役。
|
||||
- 凭据引用变更。
|
||||
- 告警确认、忽略、派单、恢复、失效。
|
||||
- 屏蔽、抑制、升级、通知策略变更。
|
||||
- 工单接单、转交、撤回、挂起、重启、关闭。
|
||||
- 权限、角色、数据权限变更。
|
||||
- 报表导出和敏感数据查看。
|
||||
|
||||
## 11. 核心关系
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
BUSINESS_SYSTEMS ||--o{ RESOURCES : contains
|
||||
RESOURCE_TYPES ||--o{ RESOURCES : classifies
|
||||
RESOURCES ||--o{ METRIC_SERIES : emits
|
||||
METRIC_DEFINITIONS ||--o{ METRIC_SERIES : defines
|
||||
RESOURCES ||--o{ COLLECTOR_TASKS : collected_by
|
||||
COLLECTOR_TASKS ||--o{ COLLECTOR_RUNS : runs
|
||||
RESOURCES ||--o{ RAW_EVENTS : produces
|
||||
RAW_EVENTS ||--o{ ALERTS : converts_to
|
||||
ALERT_RULES ||--o{ ALERTS : triggers
|
||||
INCIDENTS ||--o{ INCIDENT_ALERTS : groups
|
||||
ALERTS ||--o{ INCIDENT_ALERTS : grouped_by
|
||||
INCIDENTS ||--o{ TICKETS : handled_by
|
||||
ALERTS ||--o{ TICKETS : may_create
|
||||
ALERTS ||--o{ NOTIFICATION_RECORDS : notifies
|
||||
INCIDENTS ||--o{ NOTIFICATION_RECORDS : notifies
|
||||
DATA_CENTERS ||--o{ ROOMS : contains
|
||||
ROOMS ||--o{ RACKS : contains
|
||||
RACKS ||--o{ RACK_UNITS : contains
|
||||
RESOURCES ||--o| ASSETS : binds
|
||||
ASSETS ||--o{ RACK_UNITS : occupies
|
||||
```
|
||||
|
||||
## 12. 编码前检查清单
|
||||
|
||||
- [ ] PostgreSQL 表结构与本文核心表一一对应。
|
||||
- [ ] 时序数据库选型完成,并定义 `resource_id + metric_code + ts` 查询方式。
|
||||
- [ ] 告警、事件、工单状态机在后端集中定义,不在前端自行拼状态。
|
||||
- [ ] 所有状态变化写入 `audit_logs` 或对应流转记录。
|
||||
- [ ] 采集失败、通知失败、自动派单失败均能产生可见错误和审计记录。
|
||||
- [ ] 3D 机房接口不依赖真实现场台账即可返回样例数据。
|
||||
- [ ] API 响应统一包含 `code`、`message`、`traceId`,错误场景包含可操作建议。
|
||||
144
docs/首期验收矩阵.md
Normal file
144
docs/首期验收矩阵.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# OPS 首期验收矩阵
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文根据 `docs/integrated-ops-platform-requirements.md`、`docs/integrated-ops-platform-blueprint-design.md`、`docs/首期数据模型与状态机.md` 和 `docs/首期UI状态覆盖.md` 编写,用于把 OPS-001 至 OPS-033 的需求落实到验收阶段、演示路径、数据准备、通过标准和证据要求。
|
||||
|
||||
首期验收重点不是证明所有模块已经全量完成,而是证明平台主线已经跑通:
|
||||
|
||||
```text
|
||||
资源纳管 -> 指标 / Syslog / SNMP Trap / URL/API 探测 -> 原始事件 -> 告警 -> 通知 -> 确认 / 派单 -> 工单关闭 -> 报表 / 大屏 / 审计追踪
|
||||
```
|
||||
|
||||
## 2. 阶段口径
|
||||
|
||||
| 阶段 | 验收口径 |
|
||||
| --- | --- |
|
||||
| 第 0 阶段 | 现场确认、样例资源、账号权限、部署环境、通知渠道、验收数据准备。 |
|
||||
| 第 1 阶段 | 首期必须可演示、可闭环、可出证据的核心范围。 |
|
||||
| 第 2 阶段 | 蓝图必备但不阻塞首期闭环;首期只验收接口预留、数据模型或样例能力。 |
|
||||
| 第 3 阶段 | 运营治理和智能化增强;首期只验收规划口径,不作为功能通过项。 |
|
||||
|
||||
## 3. 通用验收前提
|
||||
|
||||
- 现场设备优先按 H3C/华三网络设备准备样例。
|
||||
- 站内消息、短信、邮件为首期优先打通通知渠道。
|
||||
- 3D 机房前端已外包,OPS 首期只验收后端接口和样例数据。
|
||||
- 老运维平台首期不迁移、不集成,只保留后续迁移评估入口。
|
||||
- 本地开发命令使用 Windows PowerShell;验收部署目标为 Linux/麒麟。
|
||||
- 指标样本应进入选定时序数据库或其适配层,事务数据进入 PostgreSQL。
|
||||
- “模拟 Trap、模拟 Syslog、可控 URL/API 故障”是否可作为正式验收手段仍需院方确认;确认前在矩阵中按“可控样例/待确认”处理。
|
||||
|
||||
## 4. 证据要求
|
||||
|
||||
每个验收项至少保留以下证据之一:
|
||||
|
||||
| 证据类型 | 要求 |
|
||||
| --- | --- |
|
||||
| 截图 | 页面全屏截图,包含时间、资源名称、状态或操作结果。 |
|
||||
| 录像 | 端到端闭环建议录屏,覆盖触发、通知、确认、派单、关闭、报表。 |
|
||||
| 导出文件 | 报表、告警导出、工单导出需保留原始文件。 |
|
||||
| 接口响应 | 后端接口验收需保留请求参数、响应 JSON、HTTP 状态码和 `traceId`。 |
|
||||
| 日志 | 采集、告警、通知、工单、权限、审计相关日志需能按 `traceId` 查询。 |
|
||||
| 数据库记录 | 必要时保留 PostgreSQL 关键记录和时序库查询结果截图或导出。 |
|
||||
|
||||
## 5. OPS 验收矩阵
|
||||
|
||||
| 编号 | 能力 | 阶段 | 演示路径 | 数据准备 | 通过标准 | 证据 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| OPS-001 | 首页总览 | 第 1 阶段 | 登录首页,查看待处理告警、资源总览、告警趋势、网络状态;调整总览模块配置并保存。 | 至少准备主机、H3C/华三网络设备、数据库、URL/API 样例资源;准备 1 条未恢复告警。 | 首页数据来自后端真实记录;模块配置保存后刷新仍生效;空态、加载、错误、无权限状态可演示。 | 首页截图、模块配置前后截图、接口响应。 |
|
||||
| OPS-002 | 操作系统监控 | 第 1 阶段 | 纳管一台主机,查看 CPU、内存、磁盘、网卡、流量、日志或 Syslog;触发阈值告警。 | 主机账号或采集代理;CPU/磁盘阈值规则;时序库写入样例。 | 指标趋势可查;采集状态可见;异常能生成告警并进入告警中心。 | 资源详情截图、时序查询结果、告警截图。 |
|
||||
| OPS-003 | 服务器硬件监控 | 第 2 阶段,首期样例或预留 | 接入可用物理服务器或提供硬件健康样例接口,展示电压、电流、温度、风扇等字段。 | 现场服务器如短期不可接入,准备样例数据和资源类型模板。 | 首期至少完成模型、接口和页面位置预留;若现场设备可用,则展示真实硬件健康数据。 | 接口响应、资源类型模板截图、样例页面截图。 |
|
||||
| OPS-004 | 网络设备监控 | 第 1 阶段 | 接入 H3C/华三网络设备,展示接口状态、接口流量、ARP/路由/转发表样例;接收 Trap 或 Syslog 生成告警。 | H3C/华三设备型号、SNMP 版本、OID、Trap 字典、Syslog 样例、账号权限。 | H3C/华三样例设备可被识别;接口状态和流量可查;Trap/Syslog 能关联资源并生成告警。 | 设备详情截图、Trap/Syslog 原始事件、告警详情、接口响应。 |
|
||||
| OPS-005 | 安全设备监控 | 第 2 阶段,首期样例或预留 | 展示安全设备资源类型、CPU、内存、接口状态和历史报表样例。 | 安全设备样例或模拟资源;指标定义。 | 首期完成资源类型、指标定义和接口预留;实际接入按现场设备顺延。 | 指标定义截图、接口响应、样例报表。 |
|
||||
| OPS-006 | 存储监控 | 第 2 阶段,首期样例或预留 | 展示存储资源类型、容量、控制器、磁盘、端口状态样例。 | 存储设备型号或样例数据;指标定义。 | 首期完成模型与页面入口预留;若现场存储可接入,则展示真实容量和健康状态。 | 资源类型截图、样例趋势图、接口响应。 |
|
||||
| OPS-007 | 数据库监控 | 第 1 阶段 | 接入一个数据库实例,展示连通性、表空间或连接数、SQL TOP 样例、自定义 SQL 监控结果。 | 测试数据库账号;自定义 SQL;阈值规则。 | 数据库状态可采集;自定义 SQL 可执行并入库;异常可触发告警。 | 数据库详情截图、自定义 SQL 配置、告警截图。 |
|
||||
| OPS-008 | 中间件监控 | 第 2 阶段,首期样例或预留 | 展示 Tomcat、WebLogic、MQ 或国产中间件监控样例。 | 现场中间件类型待确认;可准备 Tomcat 样例。 | 首期至少完成资源类型、指标模板和接口预留;现场类型确认后再接入。 | 模板截图、样例接口响应、页面入口截图。 |
|
||||
| OPS-009 | 虚拟化监控 | 第 1 阶段样例 | 接入或配置虚拟化样例,展示宿主机、虚拟机、CPU、内存、磁盘、开关机状态。 | 虚拟化平台测试账号或可控样例数据。 | 虚拟化资源可在综合监控中查看;虚拟机状态变化可刷新;异常可关联告警。 | 虚拟化资源截图、接口响应、告警截图。 |
|
||||
| OPS-010 | 日志与 Trap 监控 | 第 1 阶段 | 接收 H3C/华三或样例设备 Syslog、SNMP Trap;配置 Trap 字典、OID 描述、规则、屏蔽策略。 | H3C/华三 Trap/Syslog 样例;模拟方式需院方确认。 | 原始事件可查;解析成功和未解析状态可见;规则命中后生成告警;屏蔽策略生效并留审计。 | 原始事件列表、规则配置截图、告警详情、审计日志。 |
|
||||
| OPS-011 | URL 与业务可用性监控 | 第 1 阶段 | 配置 URL/API 探测,模拟 5xx、超时或断连,触发可用性告警。 | 可控 URL/API;探测周期;响应码规则。 | 可用性、响应时间、状态码进入时序库;异常能生成告警并恢复;待确认是否允许模拟故障作为正式验收。 | 探测配置截图、趋势图、告警和恢复记录。 |
|
||||
| OPS-012 | 动环与安全环境监控 | 第 2 阶段,首期预留 | 展示动环资源类型、温湿度、UPS、门禁等模型与接口预留。 | 动环设备短期不作为首期硬依赖;准备样例资源。 | 首期不要求真实动环接入;需证明数据模型可关联数据中心、机房、告警。 | 模型文档、接口响应、样例页面。 |
|
||||
| OPS-013 | 网络拓扑管理 | 第 2 阶段,首期基础联动 | 展示基础拓扑或资源关系视图,点击资源查看告警和链路流量样例。 | H3C/华三设备、接口关系或手工拓扑样例。 | 首期不要求完整拓扑编辑器;需证明资源、链路、告警关系可查询。 | 拓扑样例截图、资源告警联动截图。 |
|
||||
| OPS-014 | 网络流量分析 | 第 2 阶段,首期样例 | 查看 H3C/华三接口流量趋势,展示应用/协议/会话分析预留。 | 接口流量指标;如无流量分析源,准备接口流量样例。 | 首期至少展示接口流量趋势和异常告警;深度流量分析进入后续阶段。 | 流量趋势图、时序查询、告警截图。 |
|
||||
| OPS-015 | 流量参数配置 | 第 2 阶段 | 配置应用、端口、协议和数据保存周期样例。 | 应用/端口/协议字典;保存周期策略。 | 首期可作为配置模型和接口预留;不阻塞核心闭环。 | 配置接口响应、策略页面截图。 |
|
||||
| OPS-016 | IP 地址管理 | 第 2 阶段,首期预留 | 创建子网、IP 地址样例,展示 IP 与资源绑定入口。 | 样例子网、IP/MAC 数据;真实台账短期不可得。 | 首期不依赖真实 IP 台账;需支持后续导入和资源关联。 | 样例列表截图、导入接口说明。 |
|
||||
| OPS-017 | IP 自动扫描与报表 | 第 2 阶段 | 配置扫描规则样例,展示 IP 统计报表接口预留。 | 扫描网段需现场授权;无授权时使用样例数据。 | 未获授权不得主动扫描生产网段;首期只验证模型和接口预留。 | 扫描配置截图、样例报表。 |
|
||||
| OPS-018 | 告警降噪与策略 | 第 1 阶段 | 触发重复告警、依赖告警或维护窗口告警,验证去重、压缩、屏蔽、抑制。 | 主机或 H3C/华三设备告警样例;屏蔽策略;去重窗口。 | 告警风暴被降噪;策略命中可追踪;被抑制事件不丢失审计。 | 告警列表前后对比、策略命中记录、审计日志。 |
|
||||
| OPS-019 | 告警模板与通知 | 第 1 阶段,渠道裁剪 | 配置告警模板变量,触发测试告警,发送站内消息、短信、邮件。 | 站内消息配置;短信平台测试账号;邮件服务配置。 | 首期必须打通站内消息、短信、邮件;微信、企业微信、钉钉、电话等作为后续扩展。 | 模板截图、三类通知记录、收件截图或发送回执。 |
|
||||
| OPS-020 | 告警级别与升级 | 第 1 阶段 | 配置多级告警和升级策略,高低级别同时命中时只发送高级别;超时未确认自动升级。 | 至少 3 个告警级别;升级超时规则;接收人。 | 高级别优先生效;升级产生通知记录;升级过程可审计。 | 规则截图、告警详情、通知记录、审计日志。 |
|
||||
| OPS-021 | 告警受理与历史 | 第 1 阶段 | 触发告警后完成确认、忽略、恢复、派单、搜索、导出和历史查询。 | 至少 3 条不同状态告警;导出权限。 | 告警状态机合法;历史可查;导出文件准确;确认、忽略、派单都有审计。 | 告警列表、详情、导出文件、审计日志。 |
|
||||
| OPS-022 | 工单管理 | 第 1 阶段 | 从告警创建工单,执行接单、转交、挂起、重启、关闭;尝试非法流转。 | 告警样例;处理人和处理组;工单权限。 | 工单状态机合法;非法流转被拒绝;工单能回链告警、资源、事件。 | 工单流转截图、非法流转错误、审计日志。 |
|
||||
| OPS-023 | 数据中心与机房管理 | 第 2 阶段,首期接口 | 返回数据中心、机房、机柜层级样例,供外包 3D 机房前端联调。 | 样例数据中心、机房、机柜;真实数据短期不可得。 | 首期不要求真实台账;后端接口字段稳定,能返回告警状态聚合。 | 接口文档、JSON 响应、联调截图。 |
|
||||
| OPS-024 | 机柜与 U 位管理 | 第 2 阶段,首期接口 | 返回机柜、U 位、设备占用、资源健康、最高告警级别样例。 | 样例机柜、U 位、设备绑定关系。 | 支持外包 3D 前端展示;真实机柜数据后续导入。 | 接口响应、样例数据截图。 |
|
||||
| OPS-025 | 资产管理 | 第 2 阶段,首期预留 | 录入或导入样例资产,绑定监控资源和机柜位置。 | 样例资产编号、型号、序列号、位置。 | 首期支持资产与资源分离后关联;不因资产台账缺失阻塞监控接入。 | 资产样例截图、绑定关系接口响应。 |
|
||||
| OPS-026 | 知识库管理 | 第 2 阶段,首期可选 | 创建知识分类和处理说明,关联一个告警检测点。 | 样例知识条目;附件可选。 | 首期可作为告警详情的关联入口;完整审核流程后续实现。 | 知识条目截图、告警详情关联截图。 |
|
||||
| OPS-027 | 报表管理 | 第 1 阶段 | 生成 TopN、故障、服务器、网络设备基础报表并导出。 | 指标样本、告警、工单历史记录;时间范围。 | 报表数据来自真实后端记录和时序数据;空范围、超大范围、无权限状态可处理。 | 报表截图、导出文件、接口响应。 |
|
||||
| OPS-028 | 可视化大屏管理 | 第 1 阶段 | 配置基础大屏,展示资源健康、实时告警、接口流量、业务状态并轮播。 | 样例资源、告警、接口流量、业务系统。 | 大屏组件数据来自后端;轮播配置生效;部分组件失败时局部降级。 | 大屏截图、配置截图、刷新记录。 |
|
||||
| OPS-029 | 用户权限管理 | 第 1 阶段 | 创建用户、用户组、角色,配置功能权限和数据权限;验证越权访问。 | 管理员账号;普通运维账号;不同组织资源。 | 功能权限和数据权限隔离;无权限操作被拒绝;权限变更写审计。 | 权限配置截图、越权错误响应、审计日志。 |
|
||||
| OPS-030 | 系统管理 | 第 1 阶段 | 配置部门、字典、参数、消息模板、系统日志查询。 | 部门样例、字典项、消息模板。 | 基础配置可维护;消息模板支撑站内消息;系统日志可按时间和操作人查询。 | 配置截图、日志查询截图、审计记录。 |
|
||||
| OPS-031 | 采集管理 | 第 1 阶段 | 新增主机或 H3C/华三设备采集任务,配置模板、指标、自动发现样例。 | 采集凭据引用、指标模板、发现范围。 | 采集任务可启停;执行记录可查;失败能生成平台内部事件。 | 采集配置、执行记录、失败事件截图。 |
|
||||
| OPS-032 | 代理管理 | 第 2 阶段 | 展示跨网代理模型和主动/被动数据推送接口预留。 | 代理节点样例;网络隔离方案待确认。 | 首期不要求真实跨网代理部署;需保留代理节点模型和接入接口。 | 接口文档、代理节点样例。 |
|
||||
| OPS-033 | 业务系统视图与业务拓扑 | 第 1 阶段样例 | 建立一个业务系统视图,关联主机、数据库、URL/API、H3C/华三设备和告警。 | HIS/LIS/PACS/EMR 中至少一个样例业务;关联资源和告警。 | 业务健康、关联资源、未恢复告警、影响范围可见;业务视图能跳转资源和告警详情。 | 业务视图截图、关联关系截图、告警联动截图。 |
|
||||
|
||||
## 6. 首期端到端验收脚本
|
||||
|
||||
建议首期至少执行一条完整演示脚本:
|
||||
|
||||
1. 在综合监控中确认主机、H3C/华三网络设备、数据库、URL/API 样例资源已纳管。
|
||||
2. 查看资源详情中的指标趋势,确认指标样本已进入时序数据库或适配层。
|
||||
3. 触发一个可控异常:CPU 阈值、H3C/华三 Trap/Syslog、URL/API 超时三选一。
|
||||
4. 在原始事件池查看事件接收、解析、规则命中记录。
|
||||
5. 在告警中心查看告警生成、级别、资源上下文、业务系统、降噪命中记录。
|
||||
6. 验证站内消息、短信、邮件三类通知记录。
|
||||
7. 值班人员确认告警,并从告警派生工单。
|
||||
8. 工单完成接单、处理、关闭。
|
||||
9. 回到告警详情查看关联工单、处理记录和审计日志。
|
||||
10. 在首页、大屏和报表中查看该故障的统计和闭环证据。
|
||||
|
||||
通过标准:
|
||||
|
||||
- 端到端链路中每一步都有后端记录,不使用纯静态展示数据。
|
||||
- 每一次状态变化都有操作人、时间、traceId 或审计记录。
|
||||
- 任一通知渠道失败时,不阻塞告警确认和工单处理,但必须展示失败原因。
|
||||
- 若使用模拟 Trap、模拟 Syslog 或可控 URL/API 故障,需要在验收前取得院方确认。
|
||||
|
||||
## 7. 待确认事项
|
||||
|
||||
| 事项 | 影响 | 建议处理 |
|
||||
| --- | --- | --- |
|
||||
| 模拟 Trap、模拟 Syslog、可控 URL/API 故障是否允许作为验收手段 | 影响 OPS-010、OPS-011、端到端脚本 | 会前明确允许范围,区分“正式验收可用”和“内部演示可用”。 |
|
||||
| H3C/华三首批设备型号、SNMP 版本、OID、Trap 字典 | 影响 OPS-004、OPS-010、OPS-014 | 已建立 `docs/H3C华三首批接入调研.md`,现场确认后固化到采集模板和验收样例。 |
|
||||
| 短信平台和邮件服务接入方式 | 影响 OPS-019、OPS-020、OPS-021 | 先获取测试账号和发送限制,再写验收脚本。 |
|
||||
| Linux/麒麟部署环境 | 影响所有验收部署 | 已建立 `docs/本地开发与验收部署说明.md` 和 `deploy/README.md`,现场仍需明确系统版本、CPU 架构、服务管理方式、网络策略。 |
|
||||
| TDengine 开源版现场验证 | 影响 OPS-002、OPS-004、OPS-007、OPS-011、OPS-014、OPS-027 | 时序数据库已决策采用 TDengine 开源版;现场仍需验证 Linux/麒麟部署、AGPL 合规、Go WebSocket 连接、保留策略、备份恢复和故障降级。 |
|
||||
| 3D 机房外包前端接口字段 | 影响 OPS-023、OPS-024、OPS-025 | 与外包方确认字段、刷新频率、状态编码、权限边界。 |
|
||||
|
||||
## 8. P1 支撑文档
|
||||
|
||||
| 文档 | 覆盖范围 |
|
||||
| --- | --- |
|
||||
| `docs/P1故障救援策略.md` | 采集失败、Trap/Syslog 解析失败、通知失败、派单失败的重试、降级、提示、日志、审计和测试要求。 |
|
||||
| `docs/P1测试计划.md` | 第 1 阶段后端、前端、端到端测试矩阵和验收脚本。 |
|
||||
| `docs/本地开发与验收部署说明.md` | Windows PowerShell 本地开发、联调验收和 Linux/麒麟部署边界。 |
|
||||
| `docs/H3C华三首批接入调研.md` | H3C/华三首批接入范围、指标基线、Trap/Syslog 样例要求和现场确认表。 |
|
||||
| `docs/国产时序数据库选型验证.md` | TDengine 开源版选型结论、AGPL 合规边界、现场验证计划和适配层要求。 |
|
||||
|
||||
## 9. 验收材料目录建议
|
||||
|
||||
```text
|
||||
docs/
|
||||
首期验收矩阵.md
|
||||
验收证据/
|
||||
01-首页总览/
|
||||
02-资源监控/
|
||||
03-H3C网络设备/
|
||||
04-告警闭环/
|
||||
05-通知记录/
|
||||
06-工单闭环/
|
||||
07-报表大屏/
|
||||
08-权限审计/
|
||||
09-3D机房接口/
|
||||
10-部署与烟测/
|
||||
```
|
||||
|
||||
证据目录可以在实际验收阶段创建;本文只定义结构,不强制当前仓库立即新增截图或录像文件。
|
||||
5
templates/front_sample/standard/.env.development
Normal file
5
templates/front_sample/standard/.env.development
Normal file
@@ -0,0 +1,5 @@
|
||||
# API base URL (optional; mock is used in dev when unset)
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
|
||||
# Error reporting endpoint (optional)
|
||||
# VITE_ERROR_REPORT_URL=http://localhost:8080
|
||||
0
templates/front_sample/standard/.env.production
Normal file
0
templates/front_sample/standard/.env.production
Normal file
68
templates/front_sample/standard/README.md
Normal file
68
templates/front_sample/standard/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Arco Design Pro Vite
|
||||
|
||||
基于 [Arco Design Pro](https://arco.design/pro/) 的 Vue 3 中后台模板,使用 Vite 8 + Pinia + TypeScript 构建。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js >= 20.19.0
|
||||
- pnpm
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
pnpm install # 安装依赖
|
||||
pnpm dev # 开发服务器
|
||||
pnpm build # 生产构建
|
||||
pnpm report # 构建并生成 bundle 分析报告
|
||||
pnpm type:check # TypeScript 检查
|
||||
pnpm lint # Biome 代码检查
|
||||
pnpm lint:fix # 自动修复
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # 接口定义(按业务域)
|
||||
├── assets/ # 静态资源与全局样式
|
||||
├── components/ # 全局 / 布局级组件
|
||||
├── directive/ # 自定义指令
|
||||
├── hooks/ # 组合式函数
|
||||
├── layout/ # 页面布局
|
||||
├── locale/ # i18n 入口与全局文案
|
||||
├── mocks/ # Mock 数据(开发环境)
|
||||
│ ├── handlers/ # 全局 mock 处理器
|
||||
│ └── setup.ts # mock 启用与响应包装
|
||||
├── plugins/ # 应用插件(如 HTTP 拦截器)
|
||||
├── router/ # 路由与守卫
|
||||
├── store/ # Pinia 状态
|
||||
├── types/ # 全局类型
|
||||
├── utils/ # 工具函数
|
||||
└── views/ # 页面(每页可含 components/、locale/、mock.ts)
|
||||
config/
|
||||
└── vite.config.ts # Vite 配置
|
||||
public/ # 静态公共资源
|
||||
```
|
||||
|
||||
## Mock 说明
|
||||
|
||||
仅在开发环境(`import.meta.env.DEV`)下,`main.ts` 会动态加载 `src/mocks/index.ts`;生产构建不会打入 mockjs。
|
||||
|
||||
- 全局 handler 位于 `mocks/handlers/`
|
||||
- 页面级 mock 保留在 `views/**/mock.ts`,由 `import.meta.glob` 自动注册
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `VITE_API_BASE_URL` | 后端 API 地址(见 `.env.development`) |
|
||||
| `VITE_ERROR_REPORT_URL` | 可选,配置后启用前端错误上报(`utils/error-report.ts`) |
|
||||
|
||||
## i18n 说明
|
||||
|
||||
- 菜单等全局文案:`locale/zh-CN.ts`、`locale/en-US.ts`
|
||||
- 页面文案:`views/**/locale/` 与 `components/**/locale/`,通过 `import.meta.glob` 自动聚合
|
||||
|
||||
## 模板标记
|
||||
|
||||
路由与部分功能块带有 `/** simple */` … `/** simple end */` 注释,表示 Arco Pro「精简版 / 完整版」的可选模块边界。
|
||||
BIN
templates/front_sample/standard/biome-report.json
Normal file
BIN
templates/front_sample/standard/biome-report.json
Normal file
Binary file not shown.
51
templates/front_sample/standard/biome.json
Normal file
51
templates/front_sample/standard/biome.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": [
|
||||
"src/**",
|
||||
"config/**",
|
||||
"*.ts",
|
||||
"*.js",
|
||||
"*.vue",
|
||||
"components.d.ts"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "always",
|
||||
"quoteProperties": "asNeeded"
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"preset": "recommended",
|
||||
"correctness": {
|
||||
"noUnusedVariables": "warn",
|
||||
"useExhaustiveDependencies": "off"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"noSvgWithoutTitle": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
templates/front_sample/standard/components.d.ts
vendored
Normal file
14
templates/front_sample/standard/components.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
113
templates/front_sample/standard/config/vite.config.ts
Normal file
113
templates/front_sample/standard/config/vite.config.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { vitePluginForArco } from '@arco-plugins/vite-vue';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import { resolve } from 'path';
|
||||
import visualizer from 'rollup-plugin-visualizer';
|
||||
import { ArcoResolver } from 'unplugin-vue-components/resolvers';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { defineConfig, type PluginOption } from 'vite';
|
||||
import compressPlugin from 'vite-plugin-compression';
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
||||
import svgLoader from 'vite-svg-loader';
|
||||
|
||||
const manualChunkGroups: Record<string, string[]> = {
|
||||
arco: ['@arco-design/web-vue'],
|
||||
chart: ['echarts', 'vue-echarts'],
|
||||
vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
|
||||
};
|
||||
|
||||
function manualChunks(id: string) {
|
||||
if (!id.includes('node_modules')) return;
|
||||
for (const [chunkName, packages] of Object.entries(manualChunkGroups)) {
|
||||
for (const pkg of packages) {
|
||||
if (id.includes(`node_modules/${pkg}`)) {
|
||||
return chunkName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
const isDev = command === 'serve';
|
||||
const isReport = mode === 'report';
|
||||
|
||||
const plugins: PluginOption[] = [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
svgLoader({ svgoConfig: {} }),
|
||||
vitePluginForArco({}),
|
||||
];
|
||||
|
||||
if (!isDev) {
|
||||
plugins.push(
|
||||
Components({
|
||||
dirs: [],
|
||||
deep: false,
|
||||
resolvers: [ArcoResolver()],
|
||||
}),
|
||||
compressPlugin({ ext: '.gz' }),
|
||||
ViteImageOptimizer({
|
||||
png: { quality: 80 },
|
||||
jpeg: { quality: 80 },
|
||||
jpg: { quality: 80 },
|
||||
webp: { quality: 80 },
|
||||
}),
|
||||
);
|
||||
|
||||
if (isReport) {
|
||||
plugins.push(
|
||||
visualizer({
|
||||
filename: './node_modules/.cache/visualizer/stats.html',
|
||||
open: true,
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
plugins,
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: '@', replacement: resolve(__dirname, '../src') },
|
||||
{ find: 'assets', replacement: resolve(__dirname, '../src/assets') },
|
||||
{
|
||||
find: 'vue-i18n',
|
||||
replacement: 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js',
|
||||
},
|
||||
{
|
||||
find: 'vue',
|
||||
replacement: 'vue/dist/vue.esm-bundler.js',
|
||||
},
|
||||
],
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
modifyVars: {
|
||||
hack: `true; @import (reference) "${resolve(
|
||||
'src/assets/style/breakpoint.less',
|
||||
)}";`,
|
||||
},
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
server: isDev
|
||||
? {
|
||||
open: true,
|
||||
fs: { strict: true },
|
||||
}
|
||||
: undefined,
|
||||
build: isDev
|
||||
? undefined
|
||||
: {
|
||||
rollupOptions: {
|
||||
output: { manualChunks },
|
||||
},
|
||||
chunkSizeWarningLimit: 2000,
|
||||
},
|
||||
};
|
||||
});
|
||||
13
templates/front_sample/standard/index.html
Normal file
13
templates/front_sample/standard/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Arco Design Pro - 开箱即用的中台前端/设计解决方案</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/app/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
58
templates/front_sample/standard/package.json
Normal file
58
templates/front_sample/standard/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "arco-design-pro-vue",
|
||||
"description": "Arco Design Pro for Vue",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"author": "ArcoDesign Team",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite --config ./config/vite.config.ts",
|
||||
"build": "vue-tsc --noEmit && vite build --config ./config/vite.config.ts",
|
||||
"report": "vite build --config ./config/vite.config.ts --mode report",
|
||||
"preview": "pnpm run build && vite preview --host",
|
||||
"type:check": "vue-tsc --noEmit --skipLibCheck",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --write .",
|
||||
"format": "biome format --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.58.0",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"axios": "^1.8.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^6.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.1",
|
||||
"sortablejs": "^1.15.6",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^8.0.1",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arco-plugins/vite-vue": "^1.4.6",
|
||||
"@biomejs/biome": "^2.5.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mockjs": "^1.0.10",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.0",
|
||||
"less": "^4.2.2",
|
||||
"mockjs": "^1.1.0",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"sharp": "^0.34.1",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-image-optimizer": "^2.0.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
}
|
||||
3399
templates/front_sample/standard/pnpm-lock.yaml
generated
Normal file
3399
templates/front_sample/standard/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
templates/front_sample/standard/public/avatar-default.svg
Normal file
10
templates/front_sample/standard/public/avatar-default.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" aria-hidden="true">
|
||||
<circle cx="24" cy="24" r="24" fill="#F2F3F5"/>
|
||||
<circle cx="24" cy="19" r="7" stroke="#86909C" stroke-width="2"/>
|
||||
<path
|
||||
d="M10 40c0-7.732 6.268-14 14-14s14 6.268 14 14"
|
||||
stroke="#86909C"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 352 B |
12
templates/front_sample/standard/public/favicon.svg
Normal file
12
templates/front_sample/standard/public/favicon.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37754 16.9795L12.7498 9.43027C14.7163 7.41663 17.9428 7.37837 19.9564 9.34482C19.9852 9.37297 20.0137 9.40145 20.0418 9.43027L20.1221 9.51243C22.1049 11.5429 22.1049 14.7847 20.1221 16.8152L12.7498 24.3644C10.7834 26.378 7.55686 26.4163 5.54322 24.4498C5.5144 24.4217 5.48592 24.3932 5.45777 24.3644L5.37754 24.2822C3.39468 22.2518 3.39468 19.0099 5.37754 16.9795Z" fill="#12D2AC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0479 9.43034L27.3399 16.8974C29.3674 18.9735 29.3674 22.2883 27.3399 24.3644C25.3735 26.3781 22.147 26.4163 20.1333 24.4499C20.1045 24.4217 20.076 24.3933 20.0479 24.3644L12.7558 16.8974C10.7284 14.8213 10.7284 11.5065 12.7558 9.43034C14.7223 7.4167 17.9488 7.37844 19.9624 9.34489C19.9912 9.37304 20.0197 9.40152 20.0479 9.43034Z" fill="#307AF2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1321 9.52163L23.6851 13.1599L16.3931 20.627L9.10103 13.1599L12.6541 9.52163C14.6707 7.45664 17.9794 7.4174 20.0444 9.434C20.074 9.46286 20.1032 9.49207 20.1321 9.52163Z" fill="#0057FE"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="26" height="19" fill="white" transform="translate(3.5 7)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
51
templates/front_sample/standard/scripts/audit-check.mjs
Normal file
51
templates/front_sample/standard/scripts/audit-check.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
function walk(dir, acc = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
||||
walk(full, acc);
|
||||
} else if (/\.(ts|vue)$/.test(entry.name)) {
|
||||
acc.push(full);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
const srcFiles = walk('src');
|
||||
const badPlaceholder = [];
|
||||
const zhLocales = srcFiles.filter((f) => f.includes(`${path.sep}locale${path.sep}zh-CN.ts`));
|
||||
|
||||
for (const file of srcFiles) {
|
||||
const text = fs.readFileSync(file, 'utf8');
|
||||
if (text.includes("'???'") || /'(\?\?[^']*)'/.test(text)) {
|
||||
badPlaceholder.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
let zhOk = 0;
|
||||
for (const file of zhLocales) {
|
||||
if (/[\u4e00-\u9fff]/.test(fs.readFileSync(file, 'utf8'))) zhOk += 1;
|
||||
}
|
||||
|
||||
let mockInDist = false;
|
||||
if (fs.existsSync('dist/assets')) {
|
||||
for (const name of fs.readdirSync('dist/assets')) {
|
||||
if (!name.endsWith('.js')) continue;
|
||||
const chunk = fs.readFileSync(path.join('dist/assets', name), 'utf8');
|
||||
if (chunk.includes('mockjs') || chunk.includes('Mock.mock')) {
|
||||
mockInDist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
badPlaceholder: badPlaceholder.length,
|
||||
zhLocales: `${zhOk}/${zhLocales.length}`,
|
||||
mockInDist,
|
||||
hasGit: fs.existsSync('.env.development') && fs.readFileSync('.env.development', 'utf8').includes('VITE_API_BASE_URL=http'),
|
||||
settingsHttp: fs.readFileSync('src/locale/zh-CN/settings.ts', 'utf8').includes('http.logout.title'),
|
||||
rootMenu: fs.readFileSync('src/locale/zh-CN.ts', 'utf8').includes('仪表盘'),
|
||||
}, null, 2));
|
||||
67
templates/front_sample/standard/scripts/restore-p0-vue.mjs
Normal file
67
templates/front_sample/standard/scripts/restore-p0-vue.mjs
Normal file
@@ -0,0 +1,67 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const BASE =
|
||||
'https://raw.githubusercontent.com/arco-design/arco-design-pro-vue/main/arco-design-pro-vite/src';
|
||||
|
||||
const vueFiles = [
|
||||
'views/visualization/multi-dimension-data-analysis/components/content-publishing-source.vue',
|
||||
'views/user/info/components/my-project.vue',
|
||||
'views/user/info/components/my-team.vue',
|
||||
'views/user/setting/components/enterprise-certification.vue',
|
||||
];
|
||||
|
||||
async function download(relPath) {
|
||||
const res = await fetch(`${BASE}/${relPath}`);
|
||||
if (!res.ok) throw new Error(`${relPath}: HTTP ${res.status}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
function patchForProject(content, relPath) {
|
||||
let text = content;
|
||||
|
||||
if (relPath.includes('my-project.vue')) {
|
||||
text = text.replace(
|
||||
"import { queryMyProjectList, MyProjectRecord } from '@/api/user-center';",
|
||||
"import { type MyProjectRecord, queryMyProjectList } from '@/api/user';",
|
||||
);
|
||||
text = text.replace(/\{\{ project\.contributors \}\}\s*/g, '');
|
||||
}
|
||||
|
||||
if (relPath.includes('my-team.vue')) {
|
||||
text = text.replace(
|
||||
"import { queryMyTeamList, MyTeamRecord } from '@/api/user-center';",
|
||||
"import { type MyTeamRecord, queryMyTeamList } from '@/api/user';",
|
||||
);
|
||||
}
|
||||
|
||||
if (relPath.includes('enterprise-certification.vue')) {
|
||||
text = text.replace(
|
||||
"import { EnterpriseCertificationModel } from '@/api/user-center';",
|
||||
"import type { EnterpriseCertificationModel } from '@/api/user';",
|
||||
);
|
||||
text = text.replace(
|
||||
/type: Object as PropType<EnterpriseCertificationModel>/,
|
||||
'type: Object as PropType<EnterpriseCertificationModel>,',
|
||||
);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const relPath of vueFiles) {
|
||||
let content = await download(relPath);
|
||||
content = patchForProject(content, relPath);
|
||||
const fullPath = path.join('src', relPath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
const hasCn = /[\u4e00-\u9fff]/.test(content);
|
||||
console.log(`OK ${relPath} (cn=${hasCn})`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const BASE =
|
||||
'https://raw.githubusercontent.com/arco-design/arco-design-pro-vue/main/arco-design-pro-vite/src';
|
||||
|
||||
const localeFiles = [
|
||||
'locale/zh-CN/settings.ts',
|
||||
'views/login/locale/zh-CN.ts',
|
||||
'views/form/group/locale/zh-CN.ts',
|
||||
'views/form/step/locale/zh-CN.ts',
|
||||
'views/dashboard/workplace/locale/zh-CN.ts',
|
||||
'views/dashboard/monitor/locale/zh-CN.ts',
|
||||
'views/list/card/locale/zh-CN.ts',
|
||||
'views/list/search-table/locale/zh-CN.ts',
|
||||
'views/profile/basic/locale/zh-CN.ts',
|
||||
'views/result/success/locale/zh-CN.ts',
|
||||
'views/result/error/locale/zh-CN.ts',
|
||||
'views/exception/403/locale/zh-CN.ts',
|
||||
'views/exception/404/locale/zh-CN.ts',
|
||||
'views/user/info/locale/zh-CN.ts',
|
||||
'views/user/setting/locale/zh-CN.ts',
|
||||
'views/visualization/data-analysis/locale/zh-CN.ts',
|
||||
'views/visualization/multi-dimension-data-analysis/locale/zh-CN.ts',
|
||||
];
|
||||
|
||||
const rootZhCN = `import { mergeLocaleModules } from './merge-locales';
|
||||
import localeSettings from './zh-CN/settings';
|
||||
|
||||
const componentLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/components/**/locale/zh-CN.ts', { eager: true }),
|
||||
);
|
||||
const viewLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/views/**/locale/zh-CN.ts', { eager: true }),
|
||||
);
|
||||
|
||||
export default {
|
||||
'menu.dashboard': '仪表盘',
|
||||
'menu.server.dashboard': '仪表盘-服务端',
|
||||
'menu.server.workplace': '工作台-服务端',
|
||||
'menu.server.monitor': '实时监控-服务端',
|
||||
'menu.list': '列表页',
|
||||
'menu.result': '结果页',
|
||||
'menu.exception': '异常页',
|
||||
'menu.form': '表单页',
|
||||
'menu.profile': '详情页',
|
||||
'menu.visualization': '数据可视化',
|
||||
'menu.user': '个人中心',
|
||||
'menu.arcoWebsite': 'Arco Design',
|
||||
'menu.faq': '常见问题',
|
||||
'navbar.docs': '文档中心',
|
||||
'navbar.action.locale': '切换为中文',
|
||||
...localeSettings,
|
||||
...componentLocales,
|
||||
...viewLocales,
|
||||
};
|
||||
`;
|
||||
|
||||
async function download(relPath) {
|
||||
const url = `${BASE}/${relPath}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`${relPath}: HTTP ${res.status}`);
|
||||
}
|
||||
return res.text();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const relPath of localeFiles) {
|
||||
const content = await download(relPath);
|
||||
const dest = path.join('src', relPath.replace(/^locale\//, 'locale/'));
|
||||
const fullPath = path.join('src', relPath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
const hasCn = /[\u4e00-\u9fff]/.test(content);
|
||||
console.log(`OK ${relPath} (cn=${hasCn})`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join('src', 'locale/zh-CN.ts'), rootZhCN, 'utf8');
|
||||
console.log('OK locale/zh-CN.ts (cn=true)');
|
||||
|
||||
// search-table column setting label
|
||||
const stPath = path.join('src', 'views/list/search-table/index.vue');
|
||||
let st = fs.readFileSync(stPath, 'utf8');
|
||||
st = st.replace(
|
||||
"{{ item.title === '#' ? '???' : item.title }}",
|
||||
"{{ item.title === '#' ? '序列号' : item.title }}",
|
||||
);
|
||||
fs.writeFileSync(stPath, st, 'utf8');
|
||||
console.log('OK search-table/index.vue');
|
||||
|
||||
// verify
|
||||
let bad = 0;
|
||||
for (const relPath of ['locale/zh-CN.ts', ...localeFiles]) {
|
||||
const fullPath = path.join('src', relPath);
|
||||
const text = fs.readFileSync(fullPath, 'utf8');
|
||||
if (text.includes("'???'") || text.includes("'??'")) {
|
||||
console.error('STILL BAD:', relPath);
|
||||
bad += 1;
|
||||
}
|
||||
}
|
||||
if (bad) process.exit(1);
|
||||
console.log('All locale files verified');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
22
templates/front_sample/standard/src/api/dashboard.ts
Normal file
22
templates/front_sample/standard/src/api/dashboard.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||
import axios from 'axios';
|
||||
|
||||
export interface ContentDataRecord {
|
||||
x: string;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export function queryContentData() {
|
||||
return axios.get<ContentDataRecord[]>('/api/content-data');
|
||||
}
|
||||
|
||||
export interface PopularRecord {
|
||||
key: number;
|
||||
clickNumber: string;
|
||||
title: string;
|
||||
increases: number;
|
||||
}
|
||||
|
||||
export function queryPopularList(params: { type: string }) {
|
||||
return axios.get<TableData[]>('/api/popular/list', { params });
|
||||
}
|
||||
21
templates/front_sample/standard/src/api/form.ts
Normal file
21
templates/front_sample/standard/src/api/form.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export interface BaseInfoModel {
|
||||
activityName: string;
|
||||
channelType: string;
|
||||
promotionTime: string[];
|
||||
promoteLink: string;
|
||||
}
|
||||
export interface ChannelInfoModel {
|
||||
advertisingSource: string;
|
||||
advertisingMedia: string;
|
||||
keyword: string[];
|
||||
pushNotify: boolean;
|
||||
advertisingContent: string;
|
||||
}
|
||||
|
||||
export type UnitChannelModel = BaseInfoModel & ChannelInfoModel;
|
||||
|
||||
export function submitChannelForm(data: UnitChannelModel) {
|
||||
return axios.post('/api/channel-form/submit', { data });
|
||||
}
|
||||
50
templates/front_sample/standard/src/api/list.ts
Normal file
50
templates/front_sample/standard/src/api/list.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface';
|
||||
import axios from 'axios';
|
||||
|
||||
export interface PolicyRecord {
|
||||
id: string;
|
||||
number: number;
|
||||
name: string;
|
||||
contentType: 'img' | 'horizontalVideo' | 'verticalVideo';
|
||||
filterType: 'artificial' | 'rules';
|
||||
count: number;
|
||||
status: 'online' | 'offline';
|
||||
createdTime: string;
|
||||
}
|
||||
|
||||
export interface PolicyParams extends Partial<PolicyRecord> {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface PolicyListRes {
|
||||
list: PolicyRecord[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export function queryPolicyList(params: PolicyParams) {
|
||||
return axios.get<PolicyListRes>('/api/list/policy', { params });
|
||||
}
|
||||
|
||||
export interface ServiceRecord {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
name?: string;
|
||||
actionType?: string;
|
||||
icon?: string;
|
||||
data?: DescData[];
|
||||
enable?: boolean;
|
||||
expires?: boolean;
|
||||
}
|
||||
export function queryInspectionList() {
|
||||
return axios.get('/api/list/quality-inspection');
|
||||
}
|
||||
|
||||
export function queryTheServiceList() {
|
||||
return axios.get('/api/list/the-service');
|
||||
}
|
||||
|
||||
export function queryRulesPresetList() {
|
||||
return axios.get('/api/list/rules-preset');
|
||||
}
|
||||
38
templates/front_sample/standard/src/api/message.ts
Normal file
38
templates/front_sample/standard/src/api/message.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export interface MessageRecord {
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
subTitle: string;
|
||||
avatar?: string;
|
||||
content: string;
|
||||
time: string;
|
||||
status: 0 | 1;
|
||||
messageType?: number;
|
||||
}
|
||||
export type MessageListType = MessageRecord[];
|
||||
|
||||
export function queryMessageList() {
|
||||
return axios.post<MessageListType>('/api/message/list');
|
||||
}
|
||||
|
||||
interface MessageStatus {
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
export function setMessageStatus(data: MessageStatus) {
|
||||
return axios.post<MessageListType>('/api/message/read', data);
|
||||
}
|
||||
|
||||
export interface ChatRecord {
|
||||
id: number;
|
||||
username: string;
|
||||
content: string;
|
||||
time: string;
|
||||
isCollect: boolean;
|
||||
}
|
||||
|
||||
export function queryChatList() {
|
||||
return axios.post<ChatRecord[]>('/api/chat/list');
|
||||
}
|
||||
49
templates/front_sample/standard/src/api/profile.ts
Normal file
49
templates/front_sample/standard/src/api/profile.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export interface ProfileBasicRes {
|
||||
status: number;
|
||||
video: {
|
||||
mode: string;
|
||||
acquisition: {
|
||||
resolution: string;
|
||||
frameRate: number;
|
||||
};
|
||||
encoding: {
|
||||
resolution: string;
|
||||
rate: {
|
||||
min: number;
|
||||
max: number;
|
||||
default: number;
|
||||
};
|
||||
frameRate: number;
|
||||
profile: string;
|
||||
};
|
||||
};
|
||||
audio: {
|
||||
mode: string;
|
||||
acquisition: {
|
||||
channels: number;
|
||||
};
|
||||
encoding: {
|
||||
channels: number;
|
||||
rate: number;
|
||||
profile: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function queryProfileBasic() {
|
||||
return axios.get<ProfileBasicRes>('/api/profile/basic');
|
||||
}
|
||||
|
||||
export type operationLogRes = Array<{
|
||||
key: string;
|
||||
contentNumber: string;
|
||||
updateContent: string;
|
||||
status: number;
|
||||
updateTime: string;
|
||||
}>;
|
||||
|
||||
export function queryOperationLog() {
|
||||
return axios.get<operationLogRes>('/api/operation/log');
|
||||
}
|
||||
117
templates/front_sample/standard/src/api/user.ts
Normal file
117
templates/front_sample/standard/src/api/user.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import axios, { type AxiosProgressEvent } from 'axios';
|
||||
import type { RouteRecordNormalized } from 'vue-router';
|
||||
import type { UserState } from '@/store/modules/user/types';
|
||||
|
||||
export interface LoginData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginRes {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export function login(data: LoginData) {
|
||||
return axios.post<LoginRes>('/api/user/login', data);
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return axios.post<LoginRes>('/api/user/logout');
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return axios.post<UserState>('/api/user/info');
|
||||
}
|
||||
|
||||
export function getMenuList() {
|
||||
return axios.post<RouteRecordNormalized[]>('/api/user/menu');
|
||||
}
|
||||
|
||||
export interface MyProjectRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
peopleNumber: number;
|
||||
contributors: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function queryMyProjectList() {
|
||||
return axios.post('/api/user/my-project/list');
|
||||
}
|
||||
|
||||
export interface MyTeamRecord {
|
||||
id: number;
|
||||
avatar: string;
|
||||
name: string;
|
||||
peopleNumber: number;
|
||||
}
|
||||
|
||||
export function queryMyTeamList() {
|
||||
return axios.post('/api/user/my-team/list');
|
||||
}
|
||||
|
||||
export interface LatestActivity {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export function queryLatestActivity() {
|
||||
return axios.post<LatestActivity[]>('/api/user/latest-activity');
|
||||
}
|
||||
|
||||
export function saveUserInfo() {
|
||||
return axios.post('/api/user/save-info');
|
||||
}
|
||||
|
||||
export interface BasicInfoModel {
|
||||
email: string;
|
||||
nickname: string;
|
||||
countryRegion: string;
|
||||
area: string;
|
||||
address: string;
|
||||
profile: string;
|
||||
}
|
||||
|
||||
export interface EnterpriseCertificationModel {
|
||||
accountType: number;
|
||||
status: number;
|
||||
time: string;
|
||||
legalPerson: string;
|
||||
certificateType: string;
|
||||
authenticationNumber: string;
|
||||
enterpriseName: string;
|
||||
enterpriseCertificateType: string;
|
||||
organizationCode: string;
|
||||
}
|
||||
|
||||
export type CertificationRecord = Array<{
|
||||
certificationType: number;
|
||||
certificationContent: string;
|
||||
status: number;
|
||||
time: string;
|
||||
}>;
|
||||
|
||||
export interface UnitCertification {
|
||||
enterpriseInfo: EnterpriseCertificationModel;
|
||||
record: CertificationRecord;
|
||||
}
|
||||
|
||||
export function queryCertification() {
|
||||
return axios.post<UnitCertification>('/api/user/certification');
|
||||
}
|
||||
|
||||
export function userUploadApi(
|
||||
data: FormData,
|
||||
config: {
|
||||
controller: AbortController;
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||
},
|
||||
) {
|
||||
return axios.post('/api/user/upload', data, config);
|
||||
}
|
||||
73
templates/front_sample/standard/src/api/visualization.ts
Normal file
73
templates/front_sample/standard/src/api/visualization.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import axios from 'axios';
|
||||
import type { GeneralChart } from '@/types/global';
|
||||
|
||||
export interface ChartDataRecord {
|
||||
x: string;
|
||||
y: number;
|
||||
name: string;
|
||||
}
|
||||
export interface DataChainGrowth {
|
||||
quota: string;
|
||||
}
|
||||
|
||||
export interface DataChainGrowthRes {
|
||||
count: number;
|
||||
growth: number;
|
||||
chartData: {
|
||||
xAxis: string[];
|
||||
data: { name: string; value: number[] };
|
||||
};
|
||||
}
|
||||
export function queryDataChainGrowth(data: DataChainGrowth) {
|
||||
return axios.post<DataChainGrowthRes>('/api/data-chain-growth', data);
|
||||
}
|
||||
|
||||
export interface PopularAuthorRes {
|
||||
list: {
|
||||
ranking: number;
|
||||
author: string;
|
||||
contentCount: number;
|
||||
clickCount: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function queryPopularAuthor() {
|
||||
return axios.get<PopularAuthorRes>('/api/popular-author/list');
|
||||
}
|
||||
|
||||
export interface ContentPublishRecord {
|
||||
x: string[];
|
||||
y: number[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function queryContentPublish() {
|
||||
return axios.get<ContentPublishRecord[]>('/api/content-publish');
|
||||
}
|
||||
|
||||
export function queryContentPeriodAnalysis() {
|
||||
return axios.post<GeneralChart>('/api/content-period-analysis');
|
||||
}
|
||||
|
||||
export interface PublicOpinionAnalysis {
|
||||
quota: string;
|
||||
}
|
||||
export interface PublicOpinionAnalysisRes {
|
||||
count: number;
|
||||
growth: number;
|
||||
chartData: ChartDataRecord[];
|
||||
}
|
||||
export function queryPublicOpinionAnalysis(data: DataChainGrowth) {
|
||||
return axios.post<PublicOpinionAnalysisRes>(
|
||||
'/api/public-opinion-analysis',
|
||||
data,
|
||||
);
|
||||
}
|
||||
export interface DataOverviewRes {
|
||||
xAxis: string[];
|
||||
data: Array<{ name: string; value: number[]; count: number }>;
|
||||
}
|
||||
|
||||
export function queryDataOverview() {
|
||||
return axios.post<DataOverviewRes>('/api/data-overview');
|
||||
}
|
||||
24
templates/front_sample/standard/src/app/App.vue
Normal file
24
templates/front_sample/standard/src/app/App.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<a-config-provider :locale="locale">
|
||||
<router-view />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
|
||||
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
||||
import { computed } from 'vue';
|
||||
import useLocale from '@/hooks/locale';
|
||||
|
||||
const { currentLocale } = useLocale();
|
||||
const locale = computed(() => {
|
||||
switch (currentLocale.value) {
|
||||
case 'zh-CN':
|
||||
return zhCN;
|
||||
case 'en-US':
|
||||
return enUS;
|
||||
default:
|
||||
return enUS;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
22
templates/front_sample/standard/src/app/env.d.ts
vendored
Normal file
22
templates/front_sample/standard/src/app/env.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vue/jsx" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
|
||||
const component: DefineComponent<
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>,
|
||||
unknown
|
||||
>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string;
|
||||
readonly VITE_ERROR_REPORT_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
38
templates/front_sample/standard/src/app/main.ts
Normal file
38
templates/front_sample/standard/src/app/main.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import ArcoVue from '@arco-design/web-vue';
|
||||
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
|
||||
import { createApp } from 'vue';
|
||||
import globalComponents from '@/components';
|
||||
import '@/assets/style/global.less';
|
||||
import { setupHttp } from '@/plugins/http';
|
||||
import directive from '@/directive';
|
||||
import i18n from '@/locale';
|
||||
import router from '@/router';
|
||||
import store from '@/store';
|
||||
import setupErrorReport from '@/utils/error-report';
|
||||
import App from './App.vue';
|
||||
|
||||
async function bootstrap() {
|
||||
if (import.meta.env.DEV) {
|
||||
await import('@/mocks');
|
||||
}
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(ArcoVue, {});
|
||||
app.use(ArcoVueIcon);
|
||||
app.use(router);
|
||||
app.use(store);
|
||||
app.use(i18n);
|
||||
app.use(globalComponents);
|
||||
app.use(directive);
|
||||
|
||||
setupHttp();
|
||||
setupErrorReport(
|
||||
app,
|
||||
import.meta.env.VITE_ERROR_REPORT_URL?.trim() ?? '',
|
||||
);
|
||||
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
12
templates/front_sample/standard/src/assets/logo.svg
Normal file
12
templates/front_sample/standard/src/assets/logo.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37754 16.9795L12.7498 9.43027C14.7163 7.41663 17.9428 7.37837 19.9564 9.34482C19.9852 9.37297 20.0137 9.40145 20.0418 9.43027L20.1221 9.51243C22.1049 11.5429 22.1049 14.7847 20.1221 16.8152L12.7498 24.3644C10.7834 26.378 7.55686 26.4163 5.54322 24.4498C5.5144 24.4217 5.48592 24.3932 5.45777 24.3644L5.37754 24.2822C3.39468 22.2518 3.39468 19.0099 5.37754 16.9795Z" fill="#12D2AC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0479 9.43034L27.3399 16.8974C29.3674 18.9735 29.3674 22.2883 27.3399 24.3644C25.3735 26.3781 22.147 26.4163 20.1333 24.4499C20.1045 24.4217 20.076 24.3933 20.0479 24.3644L12.7558 16.8974C10.7284 14.8213 10.7284 11.5065 12.7558 9.43034C14.7223 7.4167 17.9488 7.37844 19.9624 9.34489C19.9912 9.37304 20.0197 9.40152 20.0479 9.43034Z" fill="#307AF2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1321 9.52163L23.6851 13.1599L16.3931 20.627L9.10103 13.1599L12.6541 9.52163C14.6707 7.45664 17.9794 7.4174 20.0444 9.434C20.074 9.46286 20.1032 9.49207 20.1321 9.52163Z" fill="#0057FE"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="26" height="19" fill="white" transform="translate(3.5 7)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,19 @@
|
||||
// ==============breakpoint============
|
||||
|
||||
// Extra small screen / phone
|
||||
@screen-xs: 480px;
|
||||
|
||||
// Small screen / tablet
|
||||
@screen-sm: 576px;
|
||||
|
||||
// Medium screen / desktop
|
||||
@screen-md: 768px;
|
||||
|
||||
// Large screen / wide desktop
|
||||
@screen-lg: 992px;
|
||||
|
||||
// Extra large screen / full hd
|
||||
@screen-xl: 1200px;
|
||||
|
||||
// Extra extra large screen / large desktop
|
||||
@screen-xxl: 1600px;
|
||||
94
templates/front_sample/standard/src/assets/style/global.less
Normal file
94
templates/front_sample/standard/src/assets/style/global.less
Normal file
@@ -0,0 +1,94 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
background-color: var(--color-bg-1);
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.echarts-tooltip-diy {
|
||||
background: linear-gradient(
|
||||
304.17deg,
|
||||
rgba(253, 254, 255, 0.6) -6.04%,
|
||||
rgba(244, 247, 252, 0.6) 85.2%
|
||||
) !important;
|
||||
border: none !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
/* Note: backdrop-filter has minimal browser support */
|
||||
|
||||
border-radius: 6px !important;
|
||||
.content-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 9px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
width: 164px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
box-shadow: 6px 0px 20px rgba(34, 87, 188, 0.1);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.tooltip-title {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
.tooltip-title,
|
||||
.tooltip-value {
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: right;
|
||||
color: #1d2129;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tooltip-item-icon {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.general-card {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
& > .arco-card-header {
|
||||
height: auto;
|
||||
padding: 20px;
|
||||
border: none;
|
||||
}
|
||||
& > .arco-card-body {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.split-line {
|
||||
border-color: rgb(var(--gray-2));
|
||||
}
|
||||
|
||||
.arco-table-cell {
|
||||
.circle {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: rgb(var(--blue-6));
|
||||
&.pass {
|
||||
background-color: rgb(var(--green-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
13343
templates/front_sample/standard/src/assets/world.json
Normal file
13343
templates/front_sample/standard/src/assets/world.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<a-breadcrumb class="container-breadcrumb">
|
||||
<a-breadcrumb-item>
|
||||
<icon-apps />
|
||||
</a-breadcrumb-item>
|
||||
<a-breadcrumb-item v-for="item in items" :key="item">
|
||||
{{ $t(item) }}
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array as PropType<string[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container-breadcrumb {
|
||||
margin: 16px 0;
|
||||
:deep(.arco-breadcrumb-item) {
|
||||
color: rgb(var(--gray-6));
|
||||
&:last-child {
|
||||
color: rgb(var(--gray-8));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<VChart
|
||||
v-if="renderChart"
|
||||
:option="option"
|
||||
autoresize
|
||||
:style="{ width, height }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import VChart from 'vue-echarts';
|
||||
|
||||
defineProps({
|
||||
option: {
|
||||
type: Object as () => EChartsOption,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const renderChart = ref(false);
|
||||
nextTick(() => {
|
||||
renderChart.value = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<a-layout-footer class="footer">Arco Pro</a-layout-footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
color: var(--color-text-2);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
35
templates/front_sample/standard/src/components/index.ts
Normal file
35
templates/front_sample/standard/src/components/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
|
||||
import {
|
||||
DataZoomComponent,
|
||||
GraphicComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
TooltipComponent,
|
||||
} from 'echarts/components';
|
||||
import { use } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import type { App } from 'vue';
|
||||
import Breadcrumb from './breadcrumb/index.vue';
|
||||
import Chart from './chart/index.vue';
|
||||
|
||||
// Manually introduce ECharts modules to reduce packing size
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
RadarChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
DataZoomComponent,
|
||||
GraphicComponent,
|
||||
]);
|
||||
|
||||
export default {
|
||||
install(Vue: App) {
|
||||
Vue.component('Chart', Chart);
|
||||
Vue.component('Breadcrumb', Breadcrumb);
|
||||
},
|
||||
};
|
||||
160
templates/front_sample/standard/src/components/menu/index.vue
Normal file
160
templates/front_sample/standard/src/components/menu/index.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script lang="tsx">
|
||||
import { compile, computed, defineComponent, h, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { RouteMeta } from 'vue-router';
|
||||
import { type RouteRecordRaw, useRoute, useRouter } from 'vue-router';
|
||||
import { useAppStore } from '@/store';
|
||||
import { openWindow, regexUrl } from '@/utils';
|
||||
import { listenerRouteChange } from '@/utils/route-listener';
|
||||
import useMenuTree from './use-menu-tree';
|
||||
|
||||
export default defineComponent({
|
||||
emit: ['collapse'],
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { menuTree } = useMenuTree();
|
||||
const collapsed = computed({
|
||||
get() {
|
||||
if (appStore.device === 'desktop') return appStore.menuCollapse;
|
||||
return false;
|
||||
},
|
||||
set(value: boolean) {
|
||||
appStore.updateSettings({ menuCollapse: value });
|
||||
},
|
||||
});
|
||||
|
||||
const topMenu = computed(() => appStore.topMenu);
|
||||
const openKeys = ref<string[]>([]);
|
||||
const selectedKey = ref<string[]>([]);
|
||||
|
||||
const goto = (item: RouteRecordRaw) => {
|
||||
// Open external link
|
||||
if (regexUrl.test(item.path)) {
|
||||
openWindow(item.path);
|
||||
selectedKey.value = [item.name as string];
|
||||
return;
|
||||
}
|
||||
// Eliminate external link side effects
|
||||
const { hideInMenu, activeMenu } = item.meta as RouteMeta;
|
||||
if (route.name === item.name && !hideInMenu && !activeMenu) {
|
||||
selectedKey.value = [item.name as string];
|
||||
return;
|
||||
}
|
||||
// Trigger router change
|
||||
router.push({
|
||||
name: item.name,
|
||||
});
|
||||
};
|
||||
const findMenuOpenKeys = (target: string) => {
|
||||
const result: string[] = [];
|
||||
let isFind = false;
|
||||
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
|
||||
if (item.name === target) {
|
||||
isFind = true;
|
||||
result.push(...keys);
|
||||
return;
|
||||
}
|
||||
if (item.children?.length) {
|
||||
item.children.forEach((el) => {
|
||||
backtrack(el, [...keys, el.name as string]);
|
||||
});
|
||||
}
|
||||
};
|
||||
menuTree.value.forEach((el: RouteRecordRaw) => {
|
||||
if (isFind) return; // Performance optimization
|
||||
backtrack(el, [el.name as string]);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
listenerRouteChange((newRoute) => {
|
||||
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
|
||||
if (requiresAuth && (!hideInMenu || activeMenu)) {
|
||||
const menuOpenKeys = findMenuOpenKeys(
|
||||
(activeMenu || newRoute.name) as string,
|
||||
);
|
||||
|
||||
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
|
||||
openKeys.value = [...keySet];
|
||||
|
||||
selectedKey.value = [
|
||||
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
|
||||
];
|
||||
}
|
||||
}, true);
|
||||
const setCollapse = (val: boolean) => {
|
||||
if (appStore.device === 'desktop')
|
||||
appStore.updateSettings({ menuCollapse: val });
|
||||
};
|
||||
|
||||
const renderSubMenu = () => {
|
||||
function travel(_route: RouteRecordRaw[], nodes = []) {
|
||||
if (_route) {
|
||||
_route.forEach((element) => {
|
||||
// This is demo, modify nodes as needed
|
||||
const icon = element?.meta?.icon
|
||||
? () => h(compile(`<${element?.meta?.icon}/>`))
|
||||
: null;
|
||||
const node =
|
||||
element?.children && element?.children.length !== 0 ? (
|
||||
<a-sub-menu
|
||||
key={element?.name}
|
||||
v-slots={{
|
||||
icon,
|
||||
title: () => h(compile(t(element?.meta?.locale || ''))),
|
||||
}}
|
||||
>
|
||||
{travel(element?.children)}
|
||||
</a-sub-menu>
|
||||
) : (
|
||||
<a-menu-item
|
||||
key={element?.name}
|
||||
v-slots={{ icon }}
|
||||
onClick={() => goto(element)}
|
||||
>
|
||||
{t(element?.meta?.locale || '')}
|
||||
</a-menu-item>
|
||||
);
|
||||
nodes.push(node as never);
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
return travel(menuTree.value);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<a-menu
|
||||
mode={topMenu.value ? 'horizontal' : 'vertical'}
|
||||
v-model:collapsed={collapsed.value}
|
||||
v-model:open-keys={openKeys.value}
|
||||
show-collapse-button={appStore.device !== 'mobile'}
|
||||
auto-open={false}
|
||||
selected-keys={selectedKey.value}
|
||||
auto-open-selected={true}
|
||||
level-indent={34}
|
||||
style="height: 100%;width:100%;"
|
||||
onCollapse={setCollapse}
|
||||
>
|
||||
{renderSubMenu()}
|
||||
</a-menu>
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-menu-inner) {
|
||||
.arco-menu-inline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.arco-icon {
|
||||
&:not(.arco-icon-down) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { computed } from 'vue';
|
||||
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
||||
import usePermission from '@/hooks/permission';
|
||||
import appClientMenus from '@/router/app-menus';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
export default function useMenuTree() {
|
||||
const permission = usePermission();
|
||||
const appStore = useAppStore();
|
||||
const appRoute = computed(() => {
|
||||
if (appStore.menuFromServer) {
|
||||
return appStore.appAsyncMenus;
|
||||
}
|
||||
return appClientMenus;
|
||||
});
|
||||
const menuTree = computed(() => {
|
||||
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
|
||||
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
||||
return (a.meta.order || 0) - (b.meta.order || 0);
|
||||
});
|
||||
function travel(_routes: RouteRecordRaw[], layer: number) {
|
||||
if (!_routes) return null;
|
||||
|
||||
const collector: any = _routes.map((element) => {
|
||||
// no access
|
||||
if (!permission.accessRouter(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// leaf node
|
||||
if (element.meta?.hideChildrenInMenu || !element.children) {
|
||||
element.children = [];
|
||||
return element;
|
||||
}
|
||||
|
||||
// route filter hideInMenu true
|
||||
element.children = element.children.filter(
|
||||
(x) => x.meta?.hideInMenu !== true,
|
||||
);
|
||||
|
||||
// Associated child node
|
||||
const subItem = travel(element.children, layer + 1);
|
||||
|
||||
if (subItem.length) {
|
||||
element.children = subItem;
|
||||
return element;
|
||||
}
|
||||
// the else logic
|
||||
if (layer > 1) {
|
||||
element.children = subItem;
|
||||
return element;
|
||||
}
|
||||
|
||||
if (element.meta?.hideInMenu === false) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
return collector.filter(Boolean);
|
||||
}
|
||||
return travel(copyRouter, 0);
|
||||
});
|
||||
|
||||
return {
|
||||
menuTree,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<a-spin style="display: block" :loading="loading">
|
||||
<a-tabs v-model:activeKey="messageType" type="rounded" destroy-on-hide>
|
||||
<a-tab-pane v-for="item in tabList" :key="item.key">
|
||||
<template #title>
|
||||
<span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span>
|
||||
</template>
|
||||
<a-result v-if="!renderList.length" status="404">
|
||||
<template #subtitle> {{ $t('messageBox.noContent') }} </template>
|
||||
</a-result>
|
||||
<List
|
||||
:render-list="renderList"
|
||||
:unread-count="unreadCount"
|
||||
@item-click="handleItemClick"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<template #extra>
|
||||
<a-button type="text" @click="emptyList">
|
||||
{{ $t('messageBox.tab.button') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tabs>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
type MessageListType,
|
||||
type MessageRecord,
|
||||
queryMessageList,
|
||||
setMessageStatus,
|
||||
} from '@/api/message';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import List from './list.vue';
|
||||
|
||||
interface TabItem {
|
||||
key: string;
|
||||
title: string;
|
||||
avatar?: string;
|
||||
}
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
const messageType = ref('message');
|
||||
const { t } = useI18n();
|
||||
const messageData = reactive<{
|
||||
renderList: MessageRecord[];
|
||||
messageList: MessageRecord[];
|
||||
}>({
|
||||
renderList: [],
|
||||
messageList: [],
|
||||
});
|
||||
toRefs(messageData);
|
||||
const tabList: TabItem[] = [
|
||||
{
|
||||
key: 'message',
|
||||
title: t('messageBox.tab.title.message'),
|
||||
},
|
||||
{
|
||||
key: 'notice',
|
||||
title: t('messageBox.tab.title.notice'),
|
||||
},
|
||||
{
|
||||
key: 'todo',
|
||||
title: t('messageBox.tab.title.todo'),
|
||||
},
|
||||
];
|
||||
async function fetchSourceData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await queryMessageList();
|
||||
messageData.messageList = data;
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
async function readMessage(data: MessageListType) {
|
||||
const ids = data.map((item) => item.id);
|
||||
await setMessageStatus({ ids });
|
||||
fetchSourceData();
|
||||
}
|
||||
const renderList = computed(() => {
|
||||
return messageData.messageList.filter(
|
||||
(item) => messageType.value === item.type,
|
||||
);
|
||||
});
|
||||
const unreadCount = computed(() => {
|
||||
return renderList.value.filter((item) => !item.status).length;
|
||||
});
|
||||
const getUnreadList = (type: string) => {
|
||||
const list = messageData.messageList.filter(
|
||||
(item) => item.type === type && !item.status,
|
||||
);
|
||||
return list;
|
||||
};
|
||||
const formatUnreadLength = (type: string) => {
|
||||
const list = getUnreadList(type);
|
||||
return list.length ? `(${list.length})` : ``;
|
||||
};
|
||||
const handleItemClick = (items: MessageListType) => {
|
||||
if (renderList.value.length) readMessage([...items]);
|
||||
};
|
||||
const emptyList = () => {
|
||||
messageData.messageList = [];
|
||||
};
|
||||
fetchSourceData();
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-popover-popup-content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-list-item-meta) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
:deep(.arco-tabs-nav) {
|
||||
padding: 14px 0 12px 16px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 0;
|
||||
.arco-result-subtitle {
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<a-list :bordered="false">
|
||||
<a-list-item
|
||||
v-for="item in renderList"
|
||||
:key="item.id"
|
||||
action-layout="vertical"
|
||||
:style="{
|
||||
opacity: item.status ? 0.5 : 1,
|
||||
}"
|
||||
>
|
||||
<template #extra>
|
||||
<a-tag v-if="item.messageType === 0" color="gray">未开始</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 1" color="green">已开通</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 2" color="blue">进行中</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 3" color="red">即将到期</a-tag>
|
||||
</template>
|
||||
<div class="item-wrap" @click="onItemClick(item)">
|
||||
<a-list-item-meta>
|
||||
<template v-if="item.avatar" #avatar>
|
||||
<a-avatar shape="circle">
|
||||
<img v-if="item.avatar" :src="item.avatar" />
|
||||
<icon-desktop v-else />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<a-space :size="4">
|
||||
<span>{{ item.title }}</span>
|
||||
<a-typography-text type="secondary">
|
||||
{{ item.subTitle }}
|
||||
</a-typography-text>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>
|
||||
<a-typography-paragraph
|
||||
:ellipsis="{
|
||||
rows: 1,
|
||||
}"
|
||||
>{{ item.content }}</a-typography-paragraph
|
||||
>
|
||||
<a-typography-text
|
||||
v-if="item.type === 'message'"
|
||||
class="time-text"
|
||||
>
|
||||
{{ item.time }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</div>
|
||||
</a-list-item>
|
||||
<template #footer>
|
||||
<a-space
|
||||
fill
|
||||
:size="0"
|
||||
:class="{ 'add-border-top': renderList.length < showMax }"
|
||||
>
|
||||
<div class="footer-wrap">
|
||||
<a-link @click="allRead">{{ $t('messageBox.allRead') }}</a-link>
|
||||
</div>
|
||||
<div class="footer-wrap">
|
||||
<a-link>{{ $t('messageBox.viewMore') }}</a-link>
|
||||
</div>
|
||||
</a-space>
|
||||
</template>
|
||||
<div
|
||||
v-if="renderList.length && renderList.length < 3"
|
||||
:style="{ height: (showMax - renderList.length) * 86 + 'px' }"
|
||||
></div>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
import type { MessageListType, MessageRecord } from '@/api/message';
|
||||
|
||||
const props = defineProps({
|
||||
renderList: {
|
||||
type: Array as PropType<MessageListType>,
|
||||
required: true,
|
||||
},
|
||||
unreadCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['itemClick']);
|
||||
const allRead = () => {
|
||||
emit('itemClick', [...props.renderList]);
|
||||
};
|
||||
|
||||
const onItemClick = (item: MessageRecord) => {
|
||||
if (!item.status) {
|
||||
emit('itemClick', [item]);
|
||||
}
|
||||
};
|
||||
const showMax = 3;
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-list) {
|
||||
.arco-list-item {
|
||||
min-height: 86px;
|
||||
border-bottom: 1px solid rgb(var(--gray-3));
|
||||
}
|
||||
.arco-list-item-extra {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
.arco-list-item-meta-content {
|
||||
flex: 1;
|
||||
}
|
||||
.item-wrap {
|
||||
cursor: pointer;
|
||||
}
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
.arco-empty {
|
||||
display: none;
|
||||
}
|
||||
.arco-list-footer {
|
||||
padding: 0;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
border-top: none;
|
||||
.arco-space-item {
|
||||
width: 100%;
|
||||
border-right: 1px solid rgb(var(--gray-3));
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
.add-border-top {
|
||||
border-top: 1px solid rgb(var(--gray-3));
|
||||
}
|
||||
}
|
||||
.footer-wrap {
|
||||
text-align: center;
|
||||
}
|
||||
.arco-typography {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.add-border {
|
||||
border-top: 1px solid rgb(var(--gray-3));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
'messageBox.tab.title.message': 'Message',
|
||||
'messageBox.tab.title.notice': 'Notice',
|
||||
'messageBox.tab.title.todo': 'Todo',
|
||||
'messageBox.tab.button': 'empty',
|
||||
'messageBox.allRead': 'All Read',
|
||||
'messageBox.viewMore': 'View More',
|
||||
'messageBox.noContent': 'No Content',
|
||||
'messageBox.switchRoles': 'Switch Roles',
|
||||
'messageBox.userCenter': 'User Center',
|
||||
'messageBox.userSettings': 'User Settings',
|
||||
'messageBox.logout': 'Logout',
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
'messageBox.tab.title.message': '消息',
|
||||
'messageBox.tab.title.notice': '通知',
|
||||
'messageBox.tab.title.todo': '待办',
|
||||
'messageBox.tab.button': '清空',
|
||||
'messageBox.allRead': '全部已读',
|
||||
'messageBox.viewMore': '查看更多',
|
||||
'messageBox.noContent': '暂无内容',
|
||||
'messageBox.switchRoles': '切换角色',
|
||||
'messageBox.userCenter': '用户中心',
|
||||
'messageBox.userSettings': '用户设置',
|
||||
'messageBox.logout': '登出登录',
|
||||
};
|
||||
368
templates/front_sample/standard/src/components/navbar/index.vue
Normal file
368
templates/front_sample/standard/src/components/navbar/index.vue
Normal file
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<div class="left-side">
|
||||
<a-space>
|
||||
<img
|
||||
alt="logo"
|
||||
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
|
||||
/>
|
||||
<a-typography-title
|
||||
:style="{ margin: 0, fontSize: '18px' }"
|
||||
:heading="5"
|
||||
>
|
||||
Arco Pro
|
||||
</a-typography-title>
|
||||
<icon-menu-fold
|
||||
v-if="!topMenu && appStore.device === 'mobile'"
|
||||
style="font-size: 22px; cursor: pointer"
|
||||
@click="toggleDrawerMenu"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="center-side">
|
||||
<Menu v-if="topMenu" class="center-menu" />
|
||||
<div class="navbar-search">
|
||||
<a-input
|
||||
v-model="searchKeyword"
|
||||
:placeholder="$t('settings.searchPlaceholder')"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="navbar-search-btn"
|
||||
@click="handleSearch"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
{{ $t('settings.search') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="right-side">
|
||||
<li>
|
||||
<a-tooltip :content="$t('settings.language')">
|
||||
<a-button
|
||||
class="nav-btn"
|
||||
type="outline"
|
||||
:shape="'circle'"
|
||||
@click="setDropDownVisible"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-language />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-dropdown trigger="click" @select="changeLocale as any">
|
||||
<div ref="triggerBtn" class="trigger-btn"></div>
|
||||
<template #content>
|
||||
<a-doption
|
||||
v-for="item in locales"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-check v-show="item.value === currentLocale" />
|
||||
</template>
|
||||
{{ item.label }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip
|
||||
:content="
|
||||
theme === 'light'
|
||||
? $t('settings.navbar.theme.toDark')
|
||||
: $t('settings.navbar.theme.toLight')
|
||||
"
|
||||
>
|
||||
<a-button
|
||||
class="nav-btn"
|
||||
type="outline"
|
||||
:shape="'circle'"
|
||||
@click="handleToggleTheme"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-moon-fill v-if="theme === 'dark'" />
|
||||
<icon-sun-fill v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip :content="$t('settings.navbar.alerts')">
|
||||
<div class="message-box-trigger">
|
||||
<a-badge :count="9" dot>
|
||||
<a-button
|
||||
class="nav-btn"
|
||||
type="outline"
|
||||
:shape="'circle'"
|
||||
@click="setPopoverVisible"
|
||||
>
|
||||
<icon-notification />
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-popover
|
||||
trigger="click"
|
||||
:arrow-style="{ display: 'none' }"
|
||||
:content-style="{ padding: 0, minWidth: '400px' }"
|
||||
content-class="message-popover"
|
||||
>
|
||||
<div ref="refBtn" class="ref-btn"></div>
|
||||
<template #content>
|
||||
<message-box />
|
||||
</template>
|
||||
</a-popover>
|
||||
</li>
|
||||
<li>
|
||||
<a-tooltip
|
||||
:content="
|
||||
isFullscreen
|
||||
? $t('settings.navbar.screen.toExit')
|
||||
: $t('settings.navbar.screen.toFull')
|
||||
"
|
||||
>
|
||||
<a-button
|
||||
class="nav-btn"
|
||||
type="outline"
|
||||
:shape="'circle'"
|
||||
@click="toggleFullScreen"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-fullscreen-exit v-if="isFullscreen" />
|
||||
<icon-fullscreen v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a-dropdown trigger="click">
|
||||
<a-avatar
|
||||
:size="32"
|
||||
:style="{ marginRight: '8px', cursor: 'pointer' }"
|
||||
>
|
||||
<img alt="avatar" :src="avatar" />
|
||||
</a-avatar>
|
||||
<template #content>
|
||||
<a-doption>
|
||||
<a-space @click="switchRoles">
|
||||
<icon-tag />
|
||||
<span>
|
||||
{{ $t('messageBox.switchRoles') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'Info' })">
|
||||
<icon-user />
|
||||
<span>
|
||||
{{ $t('messageBox.userCenter') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'Setting' })">
|
||||
<icon-settings />
|
||||
<span>
|
||||
{{ $t('messageBox.userSettings') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
<a-doption>
|
||||
<a-space @click="handleLogout">
|
||||
<icon-export />
|
||||
<span>
|
||||
{{ $t('messageBox.logout') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useDark, useFullscreen, useToggle } from '@vueuse/core';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import Menu from '@/components/menu/index.vue';
|
||||
import { resolveAvatarUrl } from '@/constants/avatar';
|
||||
import useLocale from '@/hooks/locale';
|
||||
import useUser from '@/hooks/user';
|
||||
import { LOCALE_OPTIONS } from '@/locale';
|
||||
import { useAppStore, useUserStore } from '@/store';
|
||||
import MessageBox from '../message-box/index.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const { logout } = useUser();
|
||||
const { changeLocale, currentLocale } = useLocale();
|
||||
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||
const locales = [...LOCALE_OPTIONS];
|
||||
const searchKeyword = ref('');
|
||||
const avatar = computed(() => resolveAvatarUrl(userStore.avatar));
|
||||
const theme = computed(() => {
|
||||
return appStore.theme;
|
||||
});
|
||||
const topMenu = computed(() => appStore.topMenu && appStore.menu);
|
||||
const isDark = useDark({
|
||||
selector: 'body',
|
||||
attribute: 'arco-theme',
|
||||
valueDark: 'dark',
|
||||
valueLight: 'light',
|
||||
storageKey: 'arco-theme',
|
||||
onChanged(dark: boolean) {
|
||||
// overridden default behavior
|
||||
appStore.toggleTheme(dark);
|
||||
},
|
||||
});
|
||||
const toggleTheme = useToggle(isDark);
|
||||
const handleToggleTheme = () => {
|
||||
toggleTheme();
|
||||
};
|
||||
const handleSearch = () => {
|
||||
const keyword = searchKeyword.value.trim();
|
||||
if (!keyword) {
|
||||
return;
|
||||
}
|
||||
Message.info(`${keyword}`);
|
||||
};
|
||||
const refBtn = ref();
|
||||
const triggerBtn = ref();
|
||||
const setPopoverVisible = () => {
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
refBtn.value.dispatchEvent(event);
|
||||
};
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
};
|
||||
const setDropDownVisible = () => {
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
triggerBtn.value.dispatchEvent(event);
|
||||
};
|
||||
const switchRoles = async () => {
|
||||
const res = await userStore.switchRoles();
|
||||
Message.success(res as string);
|
||||
};
|
||||
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.left-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.center-side {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.center-menu {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navbar-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 4px 4px 4px 16px;
|
||||
background-color: var(--color-fill-2);
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:focus-within {
|
||||
border-color: rgb(var(--primary-6));
|
||||
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.12);
|
||||
}
|
||||
|
||||
:deep(.arco-input-outer) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-input-wrapper) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.navbar-search-btn {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
padding-right: 20px;
|
||||
list-style: none;
|
||||
:deep(.locale-select) {
|
||||
border-radius: 20px;
|
||||
}
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-text-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-btn {
|
||||
border-color: rgb(var(--gray-2));
|
||||
color: rgb(var(--gray-8));
|
||||
font-size: 16px;
|
||||
}
|
||||
.trigger-btn,
|
||||
.ref-btn {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
}
|
||||
.trigger-btn {
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.message-popover {
|
||||
.arco-popover-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
101
templates/front_sample/standard/src/components/tab-bar/index.vue
Normal file
101
templates/front_sample/standard/src/components/tab-bar/index.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="tab-bar-container">
|
||||
<a-affix ref="affixRef" :offset-top="offsetTop">
|
||||
<div class="tab-bar-box">
|
||||
<div class="tab-bar-scroll">
|
||||
<div class="tags-wrap">
|
||||
<tab-item
|
||||
v-for="(tag, index) in tagList"
|
||||
:key="tag.fullPath"
|
||||
:index="index"
|
||||
:item-data="tag"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-bar-operation"></div>
|
||||
</div>
|
||||
</a-affix>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
import { useAppStore, useTabBarStore } from '@/store';
|
||||
import {
|
||||
listenerRouteChange,
|
||||
removeRouteListener,
|
||||
} from '@/utils/route-listener';
|
||||
import tabItem from './tab-item.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const tabBarStore = useTabBarStore();
|
||||
|
||||
const affixRef = ref();
|
||||
const tagList = computed(() => {
|
||||
return tabBarStore.getTabList;
|
||||
});
|
||||
const offsetTop = computed(() => {
|
||||
return appStore.navbar ? 60 : 0;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => appStore.navbar,
|
||||
() => {
|
||||
affixRef.value.updatePosition();
|
||||
},
|
||||
);
|
||||
listenerRouteChange((route: RouteLocationNormalized) => {
|
||||
if (
|
||||
!route.meta.noAffix &&
|
||||
!tagList.value.some((tag) => tag.fullPath === route.fullPath)
|
||||
) {
|
||||
tabBarStore.updateTabList(route);
|
||||
}
|
||||
}, true);
|
||||
|
||||
onUnmounted(() => {
|
||||
removeRouteListener();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tab-bar-container {
|
||||
position: relative;
|
||||
background-color: var(--color-bg-2);
|
||||
.tab-bar-box {
|
||||
display: flex;
|
||||
padding: 0 0 0 20px;
|
||||
background-color: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
.tab-bar-scroll {
|
||||
height: 32px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.tags-wrap {
|
||||
padding: 4px 0;
|
||||
height: 48px;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
|
||||
:deep(.arco-tag) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
&:first-child {
|
||||
.arco-tag-close-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-bar-operation {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
## 组件说明
|
||||
|
||||
该组件非官方最终设计规范,以单独组件存在。
|
||||
|
||||
同时仅仅提供最基本的功能,后续进行优化及更改。
|
||||
|
||||
|
||||
## Component description
|
||||
|
||||
The component unofficial final design specification exists as a separate component.
|
||||
|
||||
At the same time, only the most basic functions are provided, and subsequent optimizations and changes will be made.
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<a-dropdown
|
||||
trigger="contextMenu"
|
||||
:popup-max-height="false"
|
||||
@select="actionSelect"
|
||||
>
|
||||
<span
|
||||
class="arco-tag arco-tag-size-medium arco-tag-checked"
|
||||
:class="{ 'link-activated': itemData.fullPath === $route.fullPath }"
|
||||
@click="goto(itemData)"
|
||||
>
|
||||
<span class="tag-link">
|
||||
{{ $t(itemData.title) }}
|
||||
</span>
|
||||
<span
|
||||
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
|
||||
@click.stop="tagClose(itemData, index)"
|
||||
>
|
||||
<icon-close />
|
||||
</span>
|
||||
</span>
|
||||
<template #content>
|
||||
<a-doption :disabled="disabledReload" :value="TabAction.reload">
|
||||
<icon-refresh />
|
||||
<span>刷新</span>
|
||||
</a-doption>
|
||||
<a-doption
|
||||
class="sperate-line"
|
||||
:disabled="disabledCurrent"
|
||||
:value="TabAction.current"
|
||||
>
|
||||
<icon-close />
|
||||
<span>关闭</span>
|
||||
</a-doption>
|
||||
<a-doption :disabled="disabledLeft" :value="TabAction.left">
|
||||
<icon-to-left />
|
||||
<span>向左靠</span>
|
||||
</a-doption>
|
||||
<a-doption
|
||||
class="sperate-line"
|
||||
:disabled="disabledRight"
|
||||
:value="TabAction.right"
|
||||
>
|
||||
<icon-to-right />
|
||||
<span>向右靠</span>
|
||||
</a-doption>
|
||||
<a-doption :value="TabAction.others">
|
||||
<icon-swap />
|
||||
<span>其它</span>
|
||||
</a-doption>
|
||||
<a-doption :value="TabAction.all">
|
||||
<icon-folder-delete />
|
||||
<span>删除</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, type PropType } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME } from '@/router/constants';
|
||||
import { useTabBarStore } from '@/store';
|
||||
import type { TagProps } from '@/store/modules/tab-bar/types';
|
||||
|
||||
enum TabAction {
|
||||
reload = 'reload',
|
||||
current = 'current',
|
||||
left = 'left',
|
||||
right = 'right',
|
||||
others = 'others',
|
||||
all = 'all',
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
itemData: {
|
||||
type: Object as PropType<TagProps>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tabBarStore = useTabBarStore();
|
||||
|
||||
const goto = (tag: TagProps) => {
|
||||
router.push({ ...tag });
|
||||
};
|
||||
const tagList = computed(() => {
|
||||
return tabBarStore.getTabList;
|
||||
});
|
||||
|
||||
const disabledReload = computed(() => {
|
||||
return props.itemData.fullPath !== route.fullPath;
|
||||
});
|
||||
|
||||
const disabledCurrent = computed(() => {
|
||||
return props.index === 0;
|
||||
});
|
||||
|
||||
const disabledLeft = computed(() => {
|
||||
return [0, 1].includes(props.index);
|
||||
});
|
||||
|
||||
const disabledRight = computed(() => {
|
||||
return props.index === tagList.value.length - 1;
|
||||
});
|
||||
|
||||
const tagClose = (tag: TagProps, idx: number) => {
|
||||
tabBarStore.deleteTag(idx, tag);
|
||||
if (props.itemData.fullPath === route.fullPath) {
|
||||
const latest = tagList.value[idx - 1]; // 获取队列的前一个tab
|
||||
router.push({ name: latest.name });
|
||||
}
|
||||
};
|
||||
|
||||
const findCurrentRouteIndex = () => {
|
||||
return tagList.value.findIndex((el) => el.fullPath === route.fullPath);
|
||||
};
|
||||
const actionSelect = async (value: any) => {
|
||||
const { itemData, index } = props;
|
||||
const copyTagList = [...tagList.value];
|
||||
if (value === TabAction.current) {
|
||||
tagClose(itemData, index);
|
||||
} else if (value === TabAction.left) {
|
||||
const currentRouteIdx = findCurrentRouteIndex();
|
||||
copyTagList.splice(1, props.index - 1);
|
||||
|
||||
tabBarStore.freshTabList(copyTagList);
|
||||
if (currentRouteIdx < index) {
|
||||
router.push({ name: itemData.name });
|
||||
}
|
||||
} else if (value === TabAction.right) {
|
||||
const currentRouteIdx = findCurrentRouteIndex();
|
||||
copyTagList.splice(props.index + 1);
|
||||
|
||||
tabBarStore.freshTabList(copyTagList);
|
||||
if (currentRouteIdx > index) {
|
||||
router.push({ name: itemData.name });
|
||||
}
|
||||
} else if (value === TabAction.others) {
|
||||
const filterList = tagList.value.filter((el, idx) => {
|
||||
return idx === 0 || idx === props.index;
|
||||
});
|
||||
tabBarStore.freshTabList(filterList);
|
||||
router.push({ name: itemData.name });
|
||||
} else if (value === TabAction.reload) {
|
||||
tabBarStore.deleteCache(itemData);
|
||||
await router.push({
|
||||
name: REDIRECT_ROUTE_NAME,
|
||||
params: {
|
||||
path: route.fullPath,
|
||||
},
|
||||
});
|
||||
tabBarStore.addCache(itemData.name);
|
||||
} else {
|
||||
tabBarStore.resetTabList();
|
||||
router.push({ name: DEFAULT_ROUTE_NAME });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tag-link {
|
||||
color: var(--color-text-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
.link-activated {
|
||||
color: rgb(var(--link-6));
|
||||
.tag-link {
|
||||
color: rgb(var(--link-6));
|
||||
}
|
||||
& + .arco-tag-close-btn {
|
||||
color: rgb(var(--link-6));
|
||||
}
|
||||
}
|
||||
:deep(.arco-dropdown-option-content) {
|
||||
span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.arco-dropdown-open {
|
||||
.tag-link {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
.arco-tag-close-btn {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
.sperate-line {
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
</style>
|
||||
16
templates/front_sample/standard/src/config/settings.json
Normal file
16
templates/front_sample/standard/src/config/settings.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"theme": "light",
|
||||
"colorWeak": false,
|
||||
"navbar": true,
|
||||
"menu": true,
|
||||
"topMenu": false,
|
||||
"hideMenu": false,
|
||||
"menuCollapse": false,
|
||||
"footer": true,
|
||||
"themeColor": "#165DFF",
|
||||
"menuWidth": 220,
|
||||
"device": "desktop",
|
||||
"tabBar": false,
|
||||
"menuFromServer": false,
|
||||
"serverMenu": []
|
||||
}
|
||||
14
templates/front_sample/standard/src/constants/avatar.ts
Normal file
14
templates/front_sample/standard/src/constants/avatar.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/** Local default avatar: linear neutral user icon. */
|
||||
export const DEFAULT_USER_AVATAR = '/avatar-default.svg';
|
||||
|
||||
const BROKEN_AVATAR_PATTERN = /pstatp\.com|vcloud\/vadmin/;
|
||||
|
||||
export function resolveAvatarUrl(url?: string) {
|
||||
if (!url || BROKEN_AVATAR_PATTERN.test(url)) {
|
||||
return DEFAULT_USER_AVATAR;
|
||||
}
|
||||
if (url.startsWith('//')) {
|
||||
return `https:${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
8
templates/front_sample/standard/src/directive/index.ts
Normal file
8
templates/front_sample/standard/src/directive/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { App } from 'vue';
|
||||
import permission from './permission';
|
||||
|
||||
export default {
|
||||
install(Vue: App) {
|
||||
Vue.directive('permission', permission);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { DirectiveBinding } from 'vue';
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const { value } = binding;
|
||||
const userStore = useUserStore();
|
||||
const { role } = userStore;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
const permissionValues = value;
|
||||
|
||||
const hasPermission = permissionValues.includes(role);
|
||||
if (!hasPermission && el.parentNode) {
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(`need roles! Like v-permission="['admin','user']"`);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
checkPermission(el, binding);
|
||||
},
|
||||
updated(el: HTMLElement, binding: DirectiveBinding) {
|
||||
checkPermission(el, binding);
|
||||
},
|
||||
};
|
||||
25
templates/front_sample/standard/src/hooks/chart-option.ts
Normal file
25
templates/front_sample/standard/src/hooks/chart-option.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import { computed } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
// for code hints
|
||||
// import { SeriesOption } from 'echarts';
|
||||
// Because there are so many configuration items, this provides a relatively convenient code hint.
|
||||
// When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, TypeScript does not report errors, and code writing is convenient.
|
||||
type optionsFn = (isDark: boolean) => EChartsOption;
|
||||
|
||||
export default function useChartOption(sourceOption: optionsFn) {
|
||||
const appStore = useAppStore();
|
||||
const isDark = computed(() => {
|
||||
return appStore.theme === 'dark';
|
||||
});
|
||||
// echarts support https://echarts.apache.org/zh/theme-builder.html
|
||||
// It's not used here
|
||||
// TODO echarts themes
|
||||
const chartOption = computed<EChartsOption>(() => {
|
||||
return sourceOption(isDark.value);
|
||||
});
|
||||
return {
|
||||
chartOption,
|
||||
};
|
||||
}
|
||||
16
templates/front_sample/standard/src/hooks/loading.ts
Normal file
16
templates/front_sample/standard/src/hooks/loading.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default function useLoading(initValue = false) {
|
||||
const loading = ref(initValue);
|
||||
const setLoading = (value: boolean) => {
|
||||
loading.value = value;
|
||||
};
|
||||
const toggle = () => {
|
||||
loading.value = !loading.value;
|
||||
};
|
||||
return {
|
||||
loading,
|
||||
setLoading,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
22
templates/front_sample/standard/src/hooks/locale.ts
Normal file
22
templates/front_sample/standard/src/hooks/locale.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default function useLocale() {
|
||||
const i18 = useI18n();
|
||||
const currentLocale = computed(() => {
|
||||
return i18.locale.value;
|
||||
});
|
||||
const changeLocale = (value: string) => {
|
||||
if (i18.locale.value === value) {
|
||||
return;
|
||||
}
|
||||
i18.locale.value = value;
|
||||
localStorage.setItem('arco-locale', value);
|
||||
Message.success(i18.t('navbar.action.locale'));
|
||||
};
|
||||
return {
|
||||
currentLocale,
|
||||
changeLocale,
|
||||
};
|
||||
}
|
||||
33
templates/front_sample/standard/src/hooks/permission.ts
Normal file
33
templates/front_sample/standard/src/hooks/permission.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
export default function usePermission() {
|
||||
const userStore = useUserStore();
|
||||
return {
|
||||
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
|
||||
return (
|
||||
!route.meta?.requiresAuth ||
|
||||
!route.meta?.roles ||
|
||||
route.meta?.roles?.includes('*') ||
|
||||
route.meta?.roles?.includes(userStore.role)
|
||||
);
|
||||
},
|
||||
findFirstPermissionRoute(_routers: any, role = 'admin') {
|
||||
const cloneRouters = [..._routers];
|
||||
while (cloneRouters.length) {
|
||||
const firstElement = cloneRouters.shift();
|
||||
if (
|
||||
firstElement?.meta?.roles?.find((el: string[]) => {
|
||||
return el.includes('*') || el.includes(role);
|
||||
})
|
||||
)
|
||||
return { name: firstElement.name };
|
||||
if (firstElement?.children) {
|
||||
cloneRouters.push(...firstElement.children);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// You can add any rules you want
|
||||
};
|
||||
}
|
||||
26
templates/front_sample/standard/src/hooks/request.ts
Normal file
26
templates/front_sample/standard/src/hooks/request.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { ref, type UnwrapRef } from 'vue';
|
||||
import type { HttpResponse } from '@/plugins/http';
|
||||
import useLoading from './loading';
|
||||
|
||||
// use to fetch list
|
||||
// Don't use async function. It doesn't work in async function.
|
||||
// Use the bind function to add parameters
|
||||
// example: useRequest(api.bind(null, {}))
|
||||
|
||||
export default function useRequest<T>(
|
||||
api: () => Promise<AxiosResponse<HttpResponse>>,
|
||||
defaultValue = [] as unknown as T,
|
||||
isLoading = true,
|
||||
) {
|
||||
const { loading, setLoading } = useLoading(isLoading);
|
||||
const response = ref<T>(defaultValue);
|
||||
api()
|
||||
.then((res) => {
|
||||
response.value = res.data as unknown as UnwrapRef<T>;
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
return { loading, response };
|
||||
}
|
||||
32
templates/front_sample/standard/src/hooks/responsive.ts
Normal file
32
templates/front_sample/standard/src/hooks/responsive.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { onBeforeMount, onBeforeUnmount, onMounted } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
import { addEventListen, removeEventListen } from '@/utils/event';
|
||||
|
||||
const WIDTH = 992; // https://arco.design/vue/component/grid#responsivevalue
|
||||
|
||||
function queryDevice() {
|
||||
const rect = document.body.getBoundingClientRect();
|
||||
return rect.width - 1 < WIDTH;
|
||||
}
|
||||
|
||||
export default function useResponsive(immediate?: boolean) {
|
||||
const appStore = useAppStore();
|
||||
function resizeHandler() {
|
||||
if (!document.hidden) {
|
||||
const isMobile = queryDevice();
|
||||
appStore.toggleDevice(isMobile ? 'mobile' : 'desktop');
|
||||
appStore.toggleMenu(isMobile);
|
||||
}
|
||||
}
|
||||
const debounceFn = useDebounceFn(resizeHandler, 100);
|
||||
onMounted(() => {
|
||||
if (immediate) debounceFn();
|
||||
});
|
||||
onBeforeMount(() => {
|
||||
addEventListen(window, 'resize', debounceFn);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
removeEventListen(window, 'resize', debounceFn);
|
||||
});
|
||||
}
|
||||
12
templates/front_sample/standard/src/hooks/themes.ts
Normal file
12
templates/front_sample/standard/src/hooks/themes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { computed } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
export default function useThemes() {
|
||||
const appStore = useAppStore();
|
||||
const isDark = computed(() => {
|
||||
return appStore.theme === 'dark';
|
||||
});
|
||||
return {
|
||||
isDark,
|
||||
};
|
||||
}
|
||||
24
templates/front_sample/standard/src/hooks/user.ts
Normal file
24
templates/front_sample/standard/src/hooks/user.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
export default function useUser() {
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const logout = async (logoutTo?: string) => {
|
||||
await userStore.logout();
|
||||
const currentRoute = router.currentRoute.value;
|
||||
Message.success('登出成功');
|
||||
router.push({
|
||||
name: logoutTo && typeof logoutTo === 'string' ? logoutTo : 'login',
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
redirect: currentRoute.name as string,
|
||||
},
|
||||
});
|
||||
};
|
||||
return {
|
||||
logout,
|
||||
};
|
||||
}
|
||||
178
templates/front_sample/standard/src/layout/default-layout.vue
Normal file
178
templates/front_sample/standard/src/layout/default-layout.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||
<div v-if="navbar" class="layout-navbar">
|
||||
<NavBar />
|
||||
</div>
|
||||
<a-layout>
|
||||
<a-layout>
|
||||
<a-layout-sider
|
||||
v-if="renderMenu"
|
||||
v-show="!hideMenu"
|
||||
class="layout-sider"
|
||||
breakpoint="xl"
|
||||
:collapsed="collapsed"
|
||||
:collapsible="true"
|
||||
:width="menuWidth"
|
||||
:style="{ paddingTop: navbar ? '60px' : '' }"
|
||||
:hide-trigger="true"
|
||||
@collapse="setCollapsed"
|
||||
>
|
||||
<div class="menu-wrapper">
|
||||
<Menu />
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
<a-drawer
|
||||
v-if="hideMenu"
|
||||
:visible="drawerVisible"
|
||||
placement="left"
|
||||
:footer="false"
|
||||
mask-closable
|
||||
:closable="false"
|
||||
@cancel="drawerCancel"
|
||||
>
|
||||
<Menu />
|
||||
</a-drawer>
|
||||
<a-layout class="layout-content" :style="paddingStyle">
|
||||
<TabBar v-if="appStore.tabBar" />
|
||||
<a-layout-content>
|
||||
<PageLayout />
|
||||
</a-layout-content>
|
||||
<Footer v-if="footer" />
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, provide, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Footer from '@/components/footer/index.vue';
|
||||
import Menu from '@/components/menu/index.vue';
|
||||
import NavBar from '@/components/navbar/index.vue';
|
||||
import TabBar from '@/components/tab-bar/index.vue';
|
||||
import usePermission from '@/hooks/permission';
|
||||
import useResponsive from '@/hooks/responsive';
|
||||
import { useAppStore, useUserStore } from '@/store';
|
||||
import PageLayout from './page-layout.vue';
|
||||
|
||||
const isInit = ref(false);
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const permission = usePermission();
|
||||
useResponsive(true);
|
||||
const navbarHeight = `60px`;
|
||||
const navbar = computed(() => appStore.navbar);
|
||||
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
|
||||
const hideMenu = computed(() => appStore.hideMenu);
|
||||
const footer = computed(() => appStore.footer);
|
||||
const menuWidth = computed(() => {
|
||||
return appStore.menuCollapse ? 48 : appStore.menuWidth;
|
||||
});
|
||||
const collapsed = computed(() => {
|
||||
return appStore.menuCollapse;
|
||||
});
|
||||
const paddingStyle = computed(() => {
|
||||
const paddingLeft =
|
||||
renderMenu.value && !hideMenu.value
|
||||
? { paddingLeft: `${menuWidth.value}px` }
|
||||
: {};
|
||||
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {};
|
||||
return { ...paddingLeft, ...paddingTop };
|
||||
});
|
||||
const setCollapsed = (val: boolean) => {
|
||||
if (!isInit.value) return; // for page initialization menu state problem
|
||||
appStore.updateSettings({ menuCollapse: val });
|
||||
};
|
||||
watch(
|
||||
() => userStore.role,
|
||||
(roleValue) => {
|
||||
if (roleValue && !permission.accessRouter(route))
|
||||
router.push({ name: 'notFound' });
|
||||
},
|
||||
);
|
||||
const drawerVisible = ref(false);
|
||||
const drawerCancel = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
provide('toggleDrawerMenu', () => {
|
||||
drawerVisible.value = !drawerVisible.value;
|
||||
});
|
||||
onMounted(() => {
|
||||
isInit.value = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@nav-size-height: 60px;
|
||||
@layout-max-width: 1100px;
|
||||
|
||||
.layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
height: @nav-size-height;
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
height: 100%;
|
||||
transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--color-border);
|
||||
content: '';
|
||||
}
|
||||
|
||||
> :deep(.arco-layout-sider-children) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
:deep(.arco-menu) {
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 4px solid transparent;
|
||||
background-clip: padding-box;
|
||||
border-radius: 7px;
|
||||
background-color: var(--color-text-4);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
min-height: 100vh;
|
||||
overflow-y: hidden;
|
||||
background-color: var(--color-fill-2);
|
||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
}
|
||||
</style>
|
||||
25
templates/front_sample/standard/src/layout/page-layout.vue
Normal file
25
templates/front_sample/standard/src/layout/page-layout.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade" mode="out-in" appear>
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="route.meta.ignoreCache"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
<keep-alive v-else :include="cacheList">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useTabBarStore } from '@/store';
|
||||
|
||||
const tabBarStore = useTabBarStore();
|
||||
|
||||
const cacheList = computed(() => tabBarStore.getCacheList);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
30
templates/front_sample/standard/src/locale/en-US.ts
Normal file
30
templates/front_sample/standard/src/locale/en-US.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { mergeLocaleModules } from './merge-locales';
|
||||
import localeSettings from './en-US/settings';
|
||||
|
||||
const componentLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/components/**/locale/en-US.ts', { eager: true }),
|
||||
);
|
||||
const viewLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/views/**/locale/en-US.ts', { eager: true }),
|
||||
);
|
||||
|
||||
export default {
|
||||
'menu.dashboard': 'Dashboard',
|
||||
'menu.server.dashboard': 'Dashboard-Server',
|
||||
'menu.server.workplace': 'Workplace-Server',
|
||||
'menu.server.monitor': 'Monitor-Server',
|
||||
'menu.list': 'List',
|
||||
'menu.result': 'Result',
|
||||
'menu.exception': 'Exception',
|
||||
'menu.form': 'Form',
|
||||
'menu.profile': 'Profile',
|
||||
'menu.visualization': 'Data Visualization',
|
||||
'menu.user': 'User Center',
|
||||
'menu.arcoWebsite': 'Arco Design',
|
||||
'menu.faq': 'FAQ',
|
||||
'navbar.docs': 'Docs',
|
||||
'navbar.action.locale': 'Switch to English',
|
||||
...localeSettings,
|
||||
...componentLocales,
|
||||
...viewLocales,
|
||||
};
|
||||
16
templates/front_sample/standard/src/locale/en-US/settings.ts
Normal file
16
templates/front_sample/standard/src/locale/en-US/settings.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
'settings.search': 'Search',
|
||||
'settings.searchPlaceholder': 'Search menus, pages, features...',
|
||||
'settings.language': 'Language',
|
||||
'settings.navbar.theme.toLight': 'Click to use light mode',
|
||||
'settings.navbar.theme.toDark': 'Click to use dark mode',
|
||||
'settings.navbar.screen.toFull': 'Click to switch to full screen mode',
|
||||
'settings.navbar.screen.toExit': 'Click to exit the full screen mode',
|
||||
'settings.navbar.alerts': 'alerts',
|
||||
'http.error.default': 'Error',
|
||||
'http.error.request': 'Request Error',
|
||||
'http.logout.title': 'Confirm logout',
|
||||
'http.logout.content':
|
||||
'You have been logged out, you can cancel to stay on this page, or log in again',
|
||||
'http.logout.okText': 'Re-Login',
|
||||
};
|
||||
21
templates/front_sample/standard/src/locale/index.ts
Normal file
21
templates/front_sample/standard/src/locale/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import en from './en-US';
|
||||
import cn from './zh-CN';
|
||||
|
||||
export const LOCALE_OPTIONS = [
|
||||
{ label: '中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' },
|
||||
];
|
||||
const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN';
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: defaultLocale,
|
||||
fallbackLocale: 'en-US',
|
||||
legacy: false,
|
||||
messages: {
|
||||
'en-US': en,
|
||||
'zh-CN': cn,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
10
templates/front_sample/standard/src/locale/merge-locales.ts
Normal file
10
templates/front_sample/standard/src/locale/merge-locales.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
type LocaleModule = { default: Record<string, string> };
|
||||
|
||||
export function mergeLocaleModules(
|
||||
modules: Record<string, LocaleModule>,
|
||||
): Record<string, string> {
|
||||
return Object.values(modules).reduce(
|
||||
(messages, module) => ({ ...messages, ...module.default }),
|
||||
{},
|
||||
);
|
||||
}
|
||||
30
templates/front_sample/standard/src/locale/zh-CN.ts
Normal file
30
templates/front_sample/standard/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { mergeLocaleModules } from './merge-locales';
|
||||
import localeSettings from './zh-CN/settings';
|
||||
|
||||
const componentLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/components/**/locale/zh-CN.ts', { eager: true }),
|
||||
);
|
||||
const viewLocales = mergeLocaleModules(
|
||||
import.meta.glob('@/views/**/locale/zh-CN.ts', { eager: true }),
|
||||
);
|
||||
|
||||
export default {
|
||||
'menu.dashboard': '仪表盘',
|
||||
'menu.server.dashboard': '仪表盘-服务端',
|
||||
'menu.server.workplace': '工作台-服务端',
|
||||
'menu.server.monitor': '实时监控-服务端',
|
||||
'menu.list': '列表页',
|
||||
'menu.result': '结果页',
|
||||
'menu.exception': '异常页',
|
||||
'menu.form': '表单页',
|
||||
'menu.profile': '详情页',
|
||||
'menu.visualization': '数据可视化',
|
||||
'menu.user': '个人中心',
|
||||
'menu.arcoWebsite': 'Arco Design',
|
||||
'menu.faq': '常见问题',
|
||||
'navbar.docs': '文档中心',
|
||||
'navbar.action.locale': '切换为中文',
|
||||
...localeSettings,
|
||||
...componentLocales,
|
||||
...viewLocales,
|
||||
};
|
||||
16
templates/front_sample/standard/src/locale/zh-CN/settings.ts
Normal file
16
templates/front_sample/standard/src/locale/zh-CN/settings.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
'settings.search': '搜索',
|
||||
'settings.searchPlaceholder': '搜索菜单、页面、功能...',
|
||||
'settings.language': '语言',
|
||||
'settings.navbar.theme.toLight': '点击切换为亮色模式',
|
||||
'settings.navbar.theme.toDark': '点击切换为暗黑模式',
|
||||
'settings.navbar.screen.toFull': '点击切换全屏模式',
|
||||
'settings.navbar.screen.toExit': '点击退出全屏模式',
|
||||
'settings.navbar.alerts': '消息通知',
|
||||
'http.error.default': '错误',
|
||||
'http.error.request': '请求错误',
|
||||
'http.logout.title': '确认登出',
|
||||
'http.logout.content':
|
||||
'您已被登出,您可以取消以停留在此页面,或重新登录',
|
||||
'http.logout.okText': '重新登录',
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import Mock from 'mockjs';
|
||||
import setupMock, { successResponseWrap } from '@/mocks/setup';
|
||||
|
||||
const haveReadIds: number[] = [];
|
||||
const getMessageList = () => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
type: 'message',
|
||||
title: '\u90d1\u6666\u6708',
|
||||
subTitle: '\u7684\u79c1\u4fe1',
|
||||
avatar:
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp',
|
||||
content: '\u5ba1\u6279\u8bf7\u6c42\u5df2\u53d1\u9001\uff0c\u8bf7\u67e5\u6536',
|
||||
time: '\u4eca\u5929 12:30:01',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'message',
|
||||
title: '\u5b81\u6ce2',
|
||||
subTitle: '\u7684\u56de\u590d',
|
||||
avatar:
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
content: '\u6b64\u5904 bug \u5df2\u7ecf\u4fee\u590d',
|
||||
time: '\u4eca\u5929 12:30:01',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'message',
|
||||
title: '\u5b81\u6ce2',
|
||||
subTitle: '\u7684\u56de\u590d',
|
||||
avatar:
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
content: '\u6b64\u5904 bug \u5df2\u7ecf\u4fee\u590d',
|
||||
time: '\u4eca\u5929 12:20:01',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'notice',
|
||||
title: '\u7eed\u8d39\u901a\u77e5',
|
||||
subTitle: '',
|
||||
avatar: '',
|
||||
content:
|
||||
'\u60a8\u7684\u4ea7\u54c1\u4f7f\u7528\u671f\u9650\u5373\u5c06\u622a\u6b62\uff0c\u5982\u9700\u7ee7\u7eed\u4f7f\u7528\u4ea7\u54c1\u8bf7\u524d\u5f80\u8d2d\u4e70',
|
||||
time: '\u4eca\u5929 12:20:01',
|
||||
messageType: 3,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'notice',
|
||||
title: '\u89c4\u5219\u5f00\u901a\u6210\u529f',
|
||||
subTitle: '',
|
||||
avatar: '',
|
||||
content:
|
||||
'\u5185\u5bb9\u5c4f\u853d\u89c4\u5219\u4e8e 2021-12-01 \u5f00\u901a\u6210\u529f\u5e76\u751f\u6548',
|
||||
time: '\u4eca\u5929 12:20:01',
|
||||
messageType: 1,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
type: 'todo',
|
||||
title: '\u8d28\u68c0\u961f\u5217\u53d8\u66f4',
|
||||
subTitle: '',
|
||||
avatar: '',
|
||||
content:
|
||||
'\u5185\u5bb9\u8d28\u68c0\u961f\u5217\u4e8e 2021-12-01 19:50:23 \u8fdb\u884c\u53d8\u66f4\uff0c\u8bf7\u91cd\u65b0\u63d0\u4ea4',
|
||||
time: '\u4eca\u5929 12:20:01',
|
||||
messageType: 0,
|
||||
},
|
||||
].map((item) => ({
|
||||
...item,
|
||||
status: haveReadIds.indexOf(item.id) === -1 ? 0 : 1,
|
||||
}));
|
||||
};
|
||||
|
||||
setupMock({
|
||||
setup: () => {
|
||||
Mock.mock(/\/api\/message\/list/, () => {
|
||||
return successResponseWrap(getMessageList());
|
||||
});
|
||||
|
||||
Mock.mock(/\/api\/message\/read/, (params: { body: string }) => {
|
||||
const { ids } = JSON.parse(params.body);
|
||||
haveReadIds.push(...(ids || []));
|
||||
return successResponseWrap(true);
|
||||
});
|
||||
},
|
||||
});
|
||||
104
templates/front_sample/standard/src/mocks/handlers/user.ts
Normal file
104
templates/front_sample/standard/src/mocks/handlers/user.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import Mock from 'mockjs';
|
||||
import { DEFAULT_USER_AVATAR } from '@/constants/avatar';
|
||||
import { isLogin } from '@/utils/auth';
|
||||
import setupMock, {
|
||||
failResponseWrap,
|
||||
successResponseWrap,
|
||||
} from '@/mocks/setup';
|
||||
|
||||
export interface MockParams {
|
||||
url: string;
|
||||
type: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
setupMock({
|
||||
setup() {
|
||||
Mock.mock(/\/api\/user\/info/, () => {
|
||||
if (isLogin()) {
|
||||
const role = window.localStorage.getItem('userRole') || 'admin';
|
||||
return successResponseWrap({
|
||||
name: 'admin',
|
||||
avatar: DEFAULT_USER_AVATAR,
|
||||
email: 'wangliqun@email.com',
|
||||
job: 'frontend',
|
||||
jobName: '\u524d\u7aef\u827a\u672f\u5bb6',
|
||||
organization: 'Frontend',
|
||||
organizationName: '\u524d\u7aef',
|
||||
location: 'beijing',
|
||||
locationName: '\u5317\u4eac',
|
||||
introduction: '\u4eba\u7206\u723d\uff0c\u6027\u6e29\u7eaf',
|
||||
personalWebsite: 'https://www.arco.design',
|
||||
phone: '150****0000',
|
||||
registrationDate: '2013-05-10 12:10:00',
|
||||
accountId: '15012312300',
|
||||
certification: 1,
|
||||
role,
|
||||
});
|
||||
}
|
||||
return failResponseWrap(null, '\u672a\u767b\u5f55', 50008);
|
||||
});
|
||||
|
||||
Mock.mock(/\/api\/user\/login/, (params: MockParams) => {
|
||||
const { username, password } = JSON.parse(params.body);
|
||||
if (!username) {
|
||||
return failResponseWrap(
|
||||
null,
|
||||
'\u7528\u6237\u540d\u4e0d\u80fd\u4e3a\u7a7a',
|
||||
50000,
|
||||
);
|
||||
}
|
||||
if (!password) {
|
||||
return failResponseWrap(null, '\u5bc6\u7801\u4e0d\u80fd\u4e3a\u7a7a', 50000);
|
||||
}
|
||||
if (username === 'admin' && password === 'admin') {
|
||||
window.localStorage.setItem('userRole', 'admin');
|
||||
return successResponseWrap({ token: '12345' });
|
||||
}
|
||||
if (username === 'user' && password === 'user') {
|
||||
window.localStorage.setItem('userRole', 'user');
|
||||
return successResponseWrap({ token: '54321' });
|
||||
}
|
||||
return failResponseWrap(
|
||||
null,
|
||||
'\u8d26\u53f7\u6216\u8005\u5bc6\u7801\u9519\u8bef',
|
||||
50000,
|
||||
);
|
||||
});
|
||||
|
||||
Mock.mock(/\/api\/user\/logout/, () => successResponseWrap(null));
|
||||
Mock.mock(/\/api\/user\/menu/, () => {
|
||||
const menuList = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
meta: {
|
||||
locale: 'menu.server.dashboard',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-dashboard',
|
||||
order: 1,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'workplace',
|
||||
name: 'Workplace',
|
||||
meta: {
|
||||
locale: 'menu.server.workplace',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'https://arco.design',
|
||||
name: 'arcoWebsite',
|
||||
meta: {
|
||||
locale: 'menu.arcoWebsite',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
return successResponseWrap(menuList);
|
||||
});
|
||||
},
|
||||
});
|
||||
9
templates/front_sample/standard/src/mocks/index.ts
Normal file
9
templates/front_sample/standard/src/mocks/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import Mock from 'mockjs';
|
||||
import './handlers/user';
|
||||
import './handlers/message-box';
|
||||
|
||||
import.meta.glob('@/views/**/mock.ts', { eager: true });
|
||||
|
||||
Mock.setup({
|
||||
timeout: '600-1000',
|
||||
});
|
||||
27
templates/front_sample/standard/src/mocks/setup.ts
Normal file
27
templates/front_sample/standard/src/mocks/setup.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import debug from '@/utils/env';
|
||||
|
||||
export default ({ mock, setup }: { mock?: boolean; setup: () => void }) => {
|
||||
if (mock !== false && debug) setup();
|
||||
};
|
||||
|
||||
export const successResponseWrap = (data: unknown) => {
|
||||
return {
|
||||
data,
|
||||
status: 'ok',
|
||||
msg: '请求成功',
|
||||
code: 20000,
|
||||
};
|
||||
};
|
||||
|
||||
export const failResponseWrap = (
|
||||
data: unknown,
|
||||
msg: string,
|
||||
code = 50000,
|
||||
) => {
|
||||
return {
|
||||
data,
|
||||
status: 'fail',
|
||||
msg,
|
||||
code,
|
||||
};
|
||||
};
|
||||
78
templates/front_sample/standard/src/plugins/http.ts
Normal file
78
templates/front_sample/standard/src/plugins/http.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
import i18n from '@/locale';
|
||||
import { useUserStore } from '@/store';
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
export interface HttpResponse<T = unknown> {
|
||||
status: number;
|
||||
msg: string;
|
||||
code: number;
|
||||
data: T;
|
||||
}
|
||||
|
||||
let initialized = false;
|
||||
|
||||
function t(key: string) {
|
||||
return i18n.global.t(key);
|
||||
}
|
||||
|
||||
export function setupHttp() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL?.trim();
|
||||
if (baseURL) {
|
||||
axios.defaults.baseURL = baseURL;
|
||||
}
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response: AxiosResponse<HttpResponse>) => {
|
||||
const res = response.data;
|
||||
if (res.code !== 20000) {
|
||||
Message.error({
|
||||
content: res.msg || t('http.error.default'),
|
||||
duration: 5 * 1000,
|
||||
});
|
||||
if (
|
||||
[50008, 50012, 50014].includes(res.code) &&
|
||||
response.config.url !== '/api/user/info'
|
||||
) {
|
||||
Modal.error({
|
||||
title: t('http.logout.title'),
|
||||
content: t('http.logout.content'),
|
||||
okText: t('http.logout.okText'),
|
||||
async onOk() {
|
||||
const userStore = useUserStore();
|
||||
await userStore.logout();
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(res.msg || t('http.error.default')));
|
||||
}
|
||||
return res as unknown as AxiosResponse<HttpResponse>;
|
||||
},
|
||||
(error) => {
|
||||
Message.error({
|
||||
content: error.msg || t('http.error.request'),
|
||||
duration: 5 * 1000,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { appExternalRoutes, appRoutes } from '../routes';
|
||||
|
||||
const mixinRoutes = [...appRoutes, ...appExternalRoutes];
|
||||
|
||||
const appClientMenus = mixinRoutes.map((el) => {
|
||||
const { name, path, meta, redirect, children } = el;
|
||||
return {
|
||||
name,
|
||||
path,
|
||||
meta,
|
||||
redirect,
|
||||
children,
|
||||
};
|
||||
});
|
||||
|
||||
export default appClientMenus;
|
||||
18
templates/front_sample/standard/src/router/constants.ts
Normal file
18
templates/front_sample/standard/src/router/constants.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const WHITE_LIST = [
|
||||
{ name: 'notFound', children: [] },
|
||||
{ name: 'login', children: [] },
|
||||
];
|
||||
|
||||
export const NOT_FOUND = {
|
||||
name: 'notFound',
|
||||
};
|
||||
|
||||
export const REDIRECT_ROUTE_NAME = 'Redirect';
|
||||
|
||||
export const DEFAULT_ROUTE_NAME = 'Workplace';
|
||||
|
||||
export const DEFAULT_ROUTE = {
|
||||
title: 'menu.dashboard.workplace',
|
||||
name: DEFAULT_ROUTE_NAME,
|
||||
fullPath: '/dashboard/workplace',
|
||||
};
|
||||
17
templates/front_sample/standard/src/router/guard/index.ts
Normal file
17
templates/front_sample/standard/src/router/guard/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Router } from 'vue-router';
|
||||
import { setRouteEmitter } from '@/utils/route-listener';
|
||||
import setupPermissionGuard from './permission';
|
||||
import setupUserLoginInfoGuard from './userLoginInfo';
|
||||
|
||||
function setupPageGuard(router: Router) {
|
||||
router.beforeEach(async (to) => {
|
||||
// emit route change
|
||||
setRouteEmitter(to);
|
||||
});
|
||||
}
|
||||
|
||||
export default function createRouteGuard(router: Router) {
|
||||
setupPageGuard(router);
|
||||
setupUserLoginInfoGuard(router);
|
||||
setupPermissionGuard(router);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import NProgress from 'nprogress'; // progress bar
|
||||
import type { RouteRecordNormalized, Router } from 'vue-router';
|
||||
|
||||
import usePermission from '@/hooks/permission';
|
||||
import { useAppStore, useUserStore } from '@/store';
|
||||
import { NOT_FOUND, WHITE_LIST } from '../constants';
|
||||
import { appRoutes } from '../routes';
|
||||
|
||||
export default function setupPermissionGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const Permission = usePermission();
|
||||
const permissionsAllow = Permission.accessRouter(to);
|
||||
if (appStore.menuFromServer) {
|
||||
// 针对来自服务端的菜单配置进行处理
|
||||
// Handle routing configuration from the server
|
||||
|
||||
// 根据需要自行完善来源于服务端的菜单配置的permission逻辑
|
||||
// Refine the permission logic from the server's menu configuration as needed
|
||||
if (
|
||||
!appStore.appAsyncMenus.length &&
|
||||
!WHITE_LIST.find((el) => el.name === to.name)
|
||||
) {
|
||||
await appStore.fetchServerMenuConfig();
|
||||
}
|
||||
const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST];
|
||||
|
||||
let exist = false;
|
||||
while (serverMenuConfig.length && !exist) {
|
||||
const element = serverMenuConfig.shift();
|
||||
if (element?.name === to.name) exist = true;
|
||||
|
||||
if (element?.children) {
|
||||
serverMenuConfig.push(
|
||||
...(element.children as unknown as RouteRecordNormalized[]),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (exist && permissionsAllow) {
|
||||
next();
|
||||
} else next(NOT_FOUND);
|
||||
} else if (permissionsAllow) {
|
||||
next();
|
||||
} else {
|
||||
const destination =
|
||||
Permission.findFirstPermissionRoute(appRoutes, userStore.role) ||
|
||||
NOT_FOUND;
|
||||
next(destination);
|
||||
}
|
||||
NProgress.done();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import NProgress from 'nprogress'; // progress bar
|
||||
import type { LocationQueryRaw, Router } from 'vue-router';
|
||||
|
||||
import { useUserStore } from '@/store';
|
||||
import { isLogin } from '@/utils/auth';
|
||||
|
||||
export default function setupUserLoginInfoGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start();
|
||||
const userStore = useUserStore();
|
||||
if (isLogin()) {
|
||||
if (userStore.role) {
|
||||
next();
|
||||
} else {
|
||||
try {
|
||||
await userStore.info();
|
||||
next();
|
||||
} catch (error) {
|
||||
await userStore.logout();
|
||||
next({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.name,
|
||||
...to.query,
|
||||
} as LocationQueryRaw,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (to.name === 'login') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
next({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.name,
|
||||
...to.query,
|
||||
} as LocationQueryRaw,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
37
templates/front_sample/standard/src/router/index.ts
Normal file
37
templates/front_sample/standard/src/router/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import NProgress from 'nprogress'; // progress bar
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import 'nprogress/nprogress.css';
|
||||
|
||||
import createRouteGuard from './guard';
|
||||
import { appRoutes } from './routes';
|
||||
import { NOT_FOUND_ROUTE, REDIRECT_MAIN } from './routes/base';
|
||||
|
||||
NProgress.configure({ showSpinner: false }); // NProgress Configuration
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: 'login',
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
...appRoutes,
|
||||
REDIRECT_MAIN,
|
||||
NOT_FOUND_ROUTE,
|
||||
],
|
||||
scrollBehavior() {
|
||||
return { top: 0 };
|
||||
},
|
||||
});
|
||||
|
||||
createRouteGuard(router);
|
||||
|
||||
export default router;
|
||||
31
templates/front_sample/standard/src/router/routes/base.ts
Normal file
31
templates/front_sample/standard/src/router/routes/base.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { REDIRECT_ROUTE_NAME } from '@/router/constants';
|
||||
|
||||
export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue');
|
||||
|
||||
export const REDIRECT_MAIN: RouteRecordRaw = {
|
||||
path: '/redirect',
|
||||
name: 'redirectWrapper',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/redirect/:path',
|
||||
name: REDIRECT_ROUTE_NAME,
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const NOT_FOUND_ROUTE: RouteRecordRaw = {
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'notFound',
|
||||
component: () => import('@/views/not-found/index.vue'),
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
path: 'https://arco.design',
|
||||
name: 'arcoWebsite',
|
||||
meta: {
|
||||
locale: 'menu.arcoWebsite',
|
||||
icon: 'icon-link',
|
||||
requiresAuth: true,
|
||||
order: 8,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
path: 'https://arco.design/vue/docs/pro/faq',
|
||||
name: 'faq',
|
||||
meta: {
|
||||
locale: 'menu.faq',
|
||||
icon: 'icon-question-circle',
|
||||
requiresAuth: true,
|
||||
order: 9,
|
||||
},
|
||||
};
|
||||
25
templates/front_sample/standard/src/router/routes/index.ts
Normal file
25
templates/front_sample/standard/src/router/routes/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { RouteRecordNormalized } from 'vue-router';
|
||||
|
||||
const modules = import.meta.glob('./modules/*.ts', { eager: true });
|
||||
const externalModules = import.meta.glob('./externalModules/*.ts', {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
function formatModules(_modules: any, result: RouteRecordNormalized[]) {
|
||||
Object.keys(_modules).forEach((key) => {
|
||||
const defaultModule = _modules[key].default;
|
||||
if (!defaultModule) return;
|
||||
const moduleList = Array.isArray(defaultModule)
|
||||
? [...defaultModule]
|
||||
: [defaultModule];
|
||||
result.push(...moduleList);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export const appRoutes: RouteRecordNormalized[] = formatModules(modules, []);
|
||||
|
||||
export const appExternalRoutes: RouteRecordNormalized[] = formatModules(
|
||||
externalModules,
|
||||
[],
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
|
||||
const DASHBOARD: AppRouteRecordRaw = {
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.dashboard',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-dashboard',
|
||||
order: 0,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'workplace',
|
||||
name: 'Workplace',
|
||||
component: () => import('@/views/dashboard/workplace/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.dashboard.workplace',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
/** simple */
|
||||
{
|
||||
path: 'monitor',
|
||||
name: 'Monitor',
|
||||
component: () => import('@/views/dashboard/monitor/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.dashboard.monitor',
|
||||
requiresAuth: true,
|
||||
roles: ['admin'],
|
||||
},
|
||||
},
|
||||
/** simple end */
|
||||
],
|
||||
};
|
||||
|
||||
export default DASHBOARD;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
|
||||
const EXCEPTION: AppRouteRecordRaw = {
|
||||
path: '/exception',
|
||||
name: 'exception',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.exception',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-exclamation-circle',
|
||||
order: 6,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '403',
|
||||
name: '403',
|
||||
component: () => import('@/views/exception/403/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.exception.403',
|
||||
requiresAuth: true,
|
||||
roles: ['admin'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '404',
|
||||
name: '404',
|
||||
component: () => import('@/views/exception/404/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.exception.404',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '500',
|
||||
name: '500',
|
||||
component: () => import('@/views/exception/500/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.exception.500',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default EXCEPTION;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
|
||||
const FORM: AppRouteRecordRaw = {
|
||||
path: '/form',
|
||||
name: 'form',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.form',
|
||||
icon: 'icon-settings',
|
||||
requiresAuth: true,
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'step',
|
||||
name: 'Step',
|
||||
component: () => import('@/views/form/step/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.form.step',
|
||||
requiresAuth: true,
|
||||
roles: ['admin'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'group',
|
||||
name: 'Group',
|
||||
component: () => import('@/views/form/group/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.form.group',
|
||||
requiresAuth: true,
|
||||
roles: ['admin'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default FORM;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
|
||||
const LIST: AppRouteRecordRaw = {
|
||||
path: '/list',
|
||||
name: 'list',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.list',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-list',
|
||||
order: 2,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'search-table', // The midline path complies with SEO specifications
|
||||
name: 'SearchTable',
|
||||
component: () => import('@/views/list/search-table/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.list.searchTable',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'card',
|
||||
name: 'Card',
|
||||
component: () => import('@/views/list/card/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.list.cardList',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default LIST;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user