From ac0f086821c55b2cef6badc0cdff8c218b68404b Mon Sep 17 00:00:00 2001 From: zfc Date: Wed, 28 Jan 2026 14:26:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(init):=20=E5=AE=8C=E6=88=90=20Phase=201=20?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=9E=B6=E6=9E=84=E6=90=AD=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 完成 T-001A: 前端项目初始化 (Next.js 14 + TypeScript + Tailwind CSS) - 完成 T-001B: 后端项目初始化 (FastAPI + SQLAlchemy + asyncpg) - 完成 T-002: 数据库配置 (KolVideo 模型 + 索引 + 测试) - 完成 T-003: 基础 UI 框架 (Header/Footer 组件 + 品牌色系) - 完成 T-004: 环境变量配置 (前后端环境变量) Co-Authored-By: Claude --- .claude/skills/go/SKILL.md | 324 ++ .claude/skills/iter/SKILL.md | 210 ++ .claude/skills/md/SKILL.md | 112 + .claude/skills/mf/SKILL.md | 111 + .claude/skills/mp/SKILL.md | 144 + .claude/skills/mr/SKILL.md | 95 + .claude/skills/mt/SKILL.md | 132 + .claude/skills/mu/SKILL.md | 114 + .claude/skills/rd/SKILL.md | 101 + .claude/skills/rf/SKILL.md | 96 + .claude/skills/rp/SKILL.md | 177 ++ .claude/skills/rr/SKILL.md | 111 + .claude/skills/rt/SKILL.md | 115 + .claude/skills/ru/SKILL.md | 105 + .claude/skills/up/SKILL.md | 78 + .claude/skills/wd/SKILL.md | 323 ++ .claude/skills/wf/SKILL.md | 234 ++ .claude/skills/wp/SKILL.md | 318 ++ .claude/skills/wt/SKILL.md | 128 + .claude/skills/wu/SKILL.md | 352 +++ .gitignore | 49 + CLAUDE.md | 487 +++ backend/.env.example | 8 + backend/alembic.ini | 42 + backend/alembic/env.py | 71 + backend/alembic/script.py.mako | 26 + backend/app/__init__.py | 0 backend/app/api/__init__.py | 0 backend/app/api/v1/__init__.py | 0 backend/app/config.py | 28 + backend/app/core/__init__.py | 0 backend/app/database.py | 34 + backend/app/main.py | 31 + backend/app/models/__init__.py | 3 + backend/app/models/kol_video.py | 52 + backend/app/schemas/__init__.py | 0 backend/app/services/__init__.py | 0 backend/pytest.ini | 5 + backend/requirements.txt | 27 + backend/tests/__init__.py | 0 backend/tests/conftest.py | 58 + backend/tests/test_database.py | 165 + doc/DevelopmentPlan.md | 1014 +++++++ doc/FeatureSummary.md | 487 +++ doc/PRD.md | 365 +++ doc/RequirementsDoc.md | 65 + doc/UIDesign.md | 850 ++++++ doc/review-FeatureSummary-claude.md | 229 ++ doc/review-PRD-claude.md | 176 ++ doc/review-UIDesign-claude.md | 268 ++ doc/review-tasks-claude.md | 754 +++++ doc/tasks.md | 339 +++ doc/ui/muse.svg | 93 + doc/ui/ui.pen | 17 + frontend/.env.example | 2 + frontend/.eslintrc.json | 3 + frontend/.gitignore | 36 + frontend/.prettierrc | 7 + frontend/README.md | 45 + frontend/next.config.mjs | 4 + frontend/package.json | 28 + frontend/pnpm-lock.yaml | 3676 +++++++++++++++++++++++ frontend/postcss.config.mjs | 8 + frontend/public/muse.svg | 93 + frontend/src/app/favicon.ico | Bin 0 -> 25931 bytes frontend/src/app/fonts/GeistMonoVF.woff | Bin 0 -> 67864 bytes frontend/src/app/fonts/GeistVF.woff | Bin 0 -> 66268 bytes frontend/src/app/globals.css | 27 + frontend/src/app/layout.tsx | 38 + frontend/src/app/page.tsx | 101 + frontend/src/components/Footer.tsx | 9 + frontend/src/components/Header.tsx | 18 + frontend/src/components/index.ts | 2 + frontend/tailwind.config.ts | 39 + frontend/tsconfig.json | 26 + 75 files changed, 13285 insertions(+) create mode 100644 .claude/skills/go/SKILL.md create mode 100644 .claude/skills/iter/SKILL.md create mode 100644 .claude/skills/md/SKILL.md create mode 100644 .claude/skills/mf/SKILL.md create mode 100644 .claude/skills/mp/SKILL.md create mode 100644 .claude/skills/mr/SKILL.md create mode 100644 .claude/skills/mt/SKILL.md create mode 100644 .claude/skills/mu/SKILL.md create mode 100644 .claude/skills/rd/SKILL.md create mode 100644 .claude/skills/rf/SKILL.md create mode 100644 .claude/skills/rp/SKILL.md create mode 100644 .claude/skills/rr/SKILL.md create mode 100644 .claude/skills/rt/SKILL.md create mode 100644 .claude/skills/ru/SKILL.md create mode 100644 .claude/skills/up/SKILL.md create mode 100644 .claude/skills/wd/SKILL.md create mode 100644 .claude/skills/wf/SKILL.md create mode 100644 .claude/skills/wp/SKILL.md create mode 100644 .claude/skills/wt/SKILL.md create mode 100644 .claude/skills/wu/SKILL.md create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 backend/.env.example create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/v1/__init__.py create mode 100644 backend/app/config.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/kol_video.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/pytest.ini create mode 100644 backend/requirements.txt create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_database.py create mode 100644 doc/DevelopmentPlan.md create mode 100644 doc/FeatureSummary.md create mode 100644 doc/PRD.md create mode 100644 doc/RequirementsDoc.md create mode 100644 doc/UIDesign.md create mode 100644 doc/review-FeatureSummary-claude.md create mode 100644 doc/review-PRD-claude.md create mode 100644 doc/review-UIDesign-claude.md create mode 100644 doc/review-tasks-claude.md create mode 100644 doc/tasks.md create mode 100644 doc/ui/muse.svg create mode 100644 doc/ui/ui.pen create mode 100644 frontend/.env.example create mode 100644 frontend/.eslintrc.json create mode 100644 frontend/.gitignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/README.md create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/muse.svg create mode 100644 frontend/src/app/favicon.ico create mode 100644 frontend/src/app/fonts/GeistMonoVF.woff create mode 100644 frontend/src/app/fonts/GeistVF.woff create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/src/components/Footer.tsx create mode 100644 frontend/src/components/Header.tsx create mode 100644 frontend/src/components/index.ts create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json diff --git a/.claude/skills/go/SKILL.md b/.claude/skills/go/SKILL.md new file mode 100644 index 0000000..535c5f2 --- /dev/null +++ b/.claude/skills/go/SKILL.md @@ -0,0 +1,324 @@ +--- +name: go +description: 终极执行按钮,激进模式一口气完成开发任务,兼容 0->1 和 1->100 场景。 +--- + +# Go - 发射按钮 + +> **定位**:执行按钮。无论是从零开始的 0->1,还是迭代优化的 1->100,按下 `/go` 就开始干活,不要停。 + +当用户调用 `/go` 或 `/go <任务范围>` 时,执行以下步骤: + +## 1. 前置检查 + +### 1.1 必要文档检查 + +检查以下文件是否存在: + +| 文件 | 必要性 | 用途 | +|------|--------|------| +| `doc/tasks.md` | **必须** | 任务清单,执行的圣经 | +| `doc/PRD.md` | **必须** | 产品需求,理解业务 | +| `doc/FeatureSummary.md` | 建议 | 功能契约 | +| `doc/DevelopmentPlan.md` | 建议 | 技术方案 | +| `doc/UIDesign.md` | 可选 | 界面设计 | + +**缺少必要文档时**: + +``` +❌ 缺少必要文档: +- doc/tasks.md (必须) +- doc/PRD.md (必须) + +请先准备这些文档,或运行: +- /wp 生成 PRD +- /wt 生成 tasks +``` + +### 1.2 读取所有可用文档 + +读取存在的所有文档,建立完整上下文。 + +## 2. 智能判断执行范围 + +### 2.1 检测项目状态 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 项目状态检测 │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 检查 src/ 或主代码目录是否存在? │ +│ │ +│ ├── 不存在 ──▶ 0->1 模式(全新项目) │ +│ │ │ +│ └── 存在 ──▶ 检查 tasks.md 中的 ITER 标记 │ +│ │ │ +│ ├── 有 ITER 标记 ──▶ 1->100 模式 │ +│ │ │ +│ └── 无 ITER 标记 ──▶ 继续未完成任务 │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 确定任务范围 + +**用户指定范围**: + +```bash +/go T-005 # 执行单个任务 +/go T-005~T-010 # 执行任务范围 +/go T-005 T-008 # 执行多个指定任务 +``` + +**自动判断范围**: + +| 场景 | 执行范围 | +|------|----------| +| 0->1 全新项目 | tasks.md 中的所有任务,从 T-001 开始 | +| 1->100 有 ITER 标记 | 优先执行 `` 标记的新任务 | +| 1->100 无 ITER 标记 | 执行所有状态为 pending/todo 的任务 | + +### 2.3 向用户确认范围(唯一一次交互) + +``` +检测到项目状态:{0->1 全新项目 / 1->100 迭代项目} + +即将执行任务: +- T-001: {任务名} +- T-002: {任务名} +- ... +- T-xxx: {任务名} + +共 X 个任务。确认执行?[Y/n] +``` + +**用户确认后,不再有任何交互,直到全部完成。** + +## 3. 激进模式执行 + +### 3.1 执行原则 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 激进模式执行原则 │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 1. 以 tasks.md 为圣经,严格按顺序执行 │ +│ │ +│ 2. 不要停下来问用户,自主决策 │ +│ │ +│ 3. 遇到问题自主修复,修复失败则记录并继续 │ +│ │ +│ 4. 发现文档冲突,基于架构经验选最优解,注释说明 │ +│ │ +│ 5. 利用所有可用工具:搜索、MCP、Skills │ +│ │ +│ 6. 每完成一个模块,Git 提交一次 │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 3.2 任务执行流程 + +``` +对于每个任务 T-xxx: +│ +├── 1. 读取任务详情(描述、验收标准、依赖) +│ +├── 2. 检查依赖任务是否完成 +│ └── 未完成 → 先执行依赖任务 +│ +├── 3. 执行任务 +│ ├── 根据任务类型选择执行方式 +│ ├── 编写代码 / 配置 / 测试 +│ └── 验证验收标准 +│ +├── 4. 遇到问题? +│ ├── 尝试自主修复(最多 3 次) +│ ├── 修复成功 → 继续 +│ └── 修复失败 → 记录问题,跳过,继续下一个 +│ +└── 5. 标记任务完成,更新 tasks.md +``` + +### 3.3 自主修复策略 + +| 问题类型 | 修复策略 | +|----------|----------| +| 编译错误 | 分析错误信息,修复代码 | +| 类型错误 | 检查类型定义,修复类型 | +| 依赖缺失 | 安装依赖包 | +| 测试失败 | 修复功能代码使测试通过 | +| 文档冲突 | 基于架构经验选最优解 | +| 未知错误 | 搜索解决方案,尝试修复 | + +## 4. Git 提交规则 + +### 4.1 提交时机 + +每完成一个**模块/Sprint**后立即提交: + +``` +T-001 ~ T-004 → 提交一次(初始化模块) +T-005 ~ T-008 → 提交一次(核心功能模块) +T-009 ~ T-012 → 提交一次(扩展功能模块) +... +``` + +### 4.2 提交信息格式 + +``` +feat(): <简要描述> + +- 完成 T-xxx: {任务名} +- 完成 T-xxx: {任务名} +- ... + +Co-Authored-By: Claude +``` + +**示例**: + +``` +feat(auth): 完成用户认证模块 + +- 完成 T-005: 用户登录功能 +- 完成 T-006: 用户注册功能 +- 完成 T-007: JWT Token 管理 +- 完成 T-008: 权限验证中间件 + +Co-Authored-By: Claude +``` + +## 5. 进度汇报 + +### 5.1 模块完成汇报 + +每完成一个模块,简要汇报: + +``` +✅ 模块完成:{模块名} + - T-005: 用户登录 ✓ + - T-006: 用户注册 ✓ + - T-007: JWT 管理 ✓ + - T-008: 权限验证 ✓ + +Git 提交: feat(auth): 完成用户认证模块 + +继续执行下一模块... +``` + +### 5.2 最终汇报 + +全部完成后,输出完整报告: + +``` +## 🚀 执行完成 + +**执行模式**: {0->1 全新项目 / 1->100 迭代} + +**任务统计**: +| 状态 | 数量 | +|------|------| +| ✅ 完成 | X 个 | +| ⚠️ 跳过 | X 个 | +| ❌ 失败 | X 个 | + +**Git 提交记录**: +- feat(init): 项目初始化 +- feat(auth): 用户认证模块 +- feat(core): 核心功能模块 +- ... + +**跳过/失败的任务**(如有): +| 任务 | 原因 | +|------|------| +| T-xxx | {原因} | + +**下一步建议**: +- 运行 `npm run dev` 验证 +- 运行 `npm run test` 测试 +- 检查跳过的任务 +``` + +## 6. 特殊场景处理 + +### 6.1 技术栈识别 + +从文档中识别技术栈,自动适配: + +| 识别来源 | 技术决策 | +|----------|----------| +| package.json 存在 | Node.js 项目 | +| requirements.txt 存在 | Python 项目 | +| DevelopmentPlan 指定 | 按文档技术栈 | +| 无明确指定 | 询问用户(唯一例外) | + +### 6.2 测试策略 + +- 功能开发完成后执行测试任务 +- 测试失败 → **先修复功能代码使测试通过** +- 不跳过失败的测试继续部署 + +### 6.3 部署任务 + +- 先本地测试验证 +- 确保 build 和 start 正常 +- 远程部署需用户额外确认 + +--- + +## 工作流总览 + +``` +/go + │ + ├── 1. 前置检查 + │ ├── tasks.md 存在? ──▶ 必须 + │ └── PRD.md 存在? ──▶ 必须 + │ + ├── 2. 读取文档,建立上下文 + │ + ├── 3. 智能判断 + │ ├── 项目状态(0->1 / 1->100) + │ └── 任务范围 + │ + ├── 4. 确认执行范围(唯一交互) + │ + ├── 5. 激进模式执行 + │ ├── 按顺序执行任务 + │ ├── 自主修复问题 + │ ├── 模块完成 → Git 提交 + │ └── 汇报进度,继续下一个 + │ + └── 6. 最终汇报 + ├── 任务统计 + ├── Git 提交记录 + └── 下一步建议 +``` + +## 注意事项 + +- **tasks.md 是圣经**,严格按其顺序和内容执行 +- **不要停下来问用户**,自主决策,自主修复 +- **遇到无法解决的问题**,记录并跳过,最后汇报 +- **每完成模块立即提交**,避免大量代码丢失风险 +- **利用所有工具**:搜索、MCP、其他 Skills + +## 与其他 Skill 的关系 + +| 场景 | 使用方式 | +|------|----------| +| 准备文档 | `/wp` `/wf` `/wd` `/wu` `/wt` | +| 评审文档 | `/rp` `/rf` `/rd` `/ru` `/rt` | +| 修改文档 | `/mp` `/mf` `/md` `/mu` `/mt` | +| 迭代变更(更新文档) | `/iter` | +| **执行开发(本 Skill)** | `/go` | + +**典型工作流**: + +``` +0->1:需求 → /wp → /wf → /wd → /wt → /go +1->100:发现问题 → /iter → /go +``` diff --git a/.claude/skills/iter/SKILL.md b/.claude/skills/iter/SKILL.md new file mode 100644 index 0000000..f03c314 --- /dev/null +++ b/.claude/skills/iter/SKILL.md @@ -0,0 +1,210 @@ +--- +name: iter +description: 迭代变更入口,调研问题后更新 PRD.md 和 tasks.md,支持 Bug 修复、功能迭代、技术重构。 +--- + +# Iterate - 迭代变更 + +> **定位**:1-100 阶段的变更入口。项目已上线,需要修复问题或迭代功能时,通过此 skill 调研、澄清、更新文档。 + +当用户调用 `/iter` 或 `/iter <问题描述>` 时,执行以下步骤: +⚠️ 重要:本 skill 只修改文档(PRD.md、tasks.md),绝不执行代码、不运行命令、不修改源文件。 + +## 1. 获取变更描述 + +如果用户提供了参数,使用该描述。否则询问: +> 请描述需要迭代的内容(Bug/功能/重构) + +**示例输入**: +- "登录验证存在漏洞,token 过期后仍可访问" +- "列表页需要增加按时间筛选功能" +- "用户模块性能太差,需要重构缓存策略" + +## 2. 调研分析 + +### 2.1 读取现有文档 + +读取以下文件了解当前状态: + +1. `doc/PRD.md` - 了解产品定义 +2. `doc/tasks.md` - 了解任务现状 + +### 2.2 调研相关代码(可选) + +根据问题描述,定位相关代码文件: + +- 搜索关键词定位文件 +- 读取相关模块代码 +- 分析现有实现 + +### 2.3 分析变更类型 + +| 类型 | 特征 | 影响范围 | +|------|------|----------| +| Bug/漏洞 | 现有功能不符合预期 | 修复逻辑,可能涉及安全 | +| 功能迭代 | 在现有功能上增加/调整 | 新增或修改功能点 | +| 技术重构 | 不改功能,优化实现 | 性能、架构、代码质量 | + +## 3. 澄清确认 + +**【必须】向用户提出澄清问题**,确保理解准确: + +### 3.1 问题理解确认 + +向用户确认: +> 我理解的变更需求是:{一句话总结} +> +> 变更类型:{Bug修复 / 功能迭代 / 技术重构} +> +> 影响范围:{涉及的模块/功能} + +### 3.2 方案选择(如有多种) + +如果有多种解决方案,列出选项让用户选择: + +``` +方案 A:{描述} +- 优点:... +- 缺点:... + +方案 B:{描述} +- 优点:... +- 缺点:... + +请选择方案,或说明其他想法。 +``` + +### 3.3 边界确认 + +确认变更边界: +> 本次变更**包含**: +> - {范围1} +> - {范围2} +> +> 本次变更**不包含**: +> - {排除项} +> +> 是否确认? + +## 4. 用户确认后执行 + +**只有用户明确确认后**,才执行以下更新: + +### 4.1 更新 PRD.md + +使用增量修改标记: + +```markdown + + +新增内容... + +``` + +或修改现有内容: + +```markdown + + +修改后的内容 +``` + +**更新位置**: +- Bug 修复 → 更新对应功能的验收标准 +- 功能迭代 → 在 3.2 功能详情添加/修改功能点 +- 技术重构 → 在 4.x 非功能需求或 7.1 技术约束中说明 + +### 4.2 更新 tasks.md + +新增任务使用标记: + +```markdown + +| T-xxx | {任务名} | {描述} | {依赖} | {优先级} | {验收标准} | +``` + +**任务 ID 规则**: +- 查找现有最大 ID,递增分配 +- 格式:T-xxx(三位数字) + +### 4.3 标记规范 + +所有变更使用 `` 前缀,区分于 `/mp` `/mt` 的标记: + +- `` +- 便于追溯迭代历史 + +## 5. 输出摘要 + +完成后向用户展示: + +``` +## 迭代变更完成 + +**变更类型**: {Bug修复 / 功能迭代 / 技术重构} + +**变更摘要**: {一句话描述} + +**已更新文档**: +- doc/PRD.md: {更新位置} +- doc/tasks.md: 新增任务 T-xxx + +**新增任务**: +| ID | 任务 | 优先级 | +|----|------|--------| +| T-xxx | {任务名} | P0/P1/P2 | + +**下一步**: +- 执行任务 T-xxx +- 或运行 `/rp` `/rt` 评审变更 +``` + +--- + +## 工作流示意 + +``` +用户描述问题 + │ + ▼ +┌─────────────┐ +│ 调研分析 │ ──▶ 读取 PRD、tasks、相关代码 +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ 澄清确认 │ ──▶ 提问 → 用户回答 → 确认方案 +└──────┬──────┘ + │ + ▼ 用户确认 +┌─────────────┐ +│ 更新文档 │ ──▶ PRD.md + tasks.md +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ 输出摘要 │ +└─────────────┘ +``` + +## 注意事项 + +- **必须先澄清确认**,不要假设用户意图 +- 变更范围要明确,避免 scope creep +- 优先级根据问题严重程度判断: + - 安全漏洞 → P0 + - 功能 Bug → P0/P1 + - 功能迭代 → P1/P2 + - 技术重构 → P1/P2 +- 只更新 PRD + tasks,保持轻量 +- 如需更新其他文档,提示用户手动运行 `/mf` `/md` 等 + +## 与其他 skill 的关系 + +| 场景 | 使用 skill | +|------|------------| +| 迭代变更入口 | `/iter`(本 skill) | +| 需要更新 FeatureSummary | `/iter` 后运行 `/mf` | +| 需要更新 DevelopmentPlan | `/iter` 后运行 `/md` | +| 需要评审变更 | `/iter` 后运行 `/rp` `/rt` | +| 从头生成文档 | 使用 `/wp` `/wf` `/wd` 等 | diff --git a/.claude/skills/md/SKILL.md b/.claude/skills/md/SKILL.md new file mode 100644 index 0000000..47303bd --- /dev/null +++ b/.claude/skills/md/SKILL.md @@ -0,0 +1,112 @@ +--- +name: md +description: 增量修改 DevelopmentPlan.md,根据用户指令在现有内容基础上更新开发计划。 +--- + +# Modify DevelopmentPlan + +当用户调用 `/md` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/DevelopmentPlan.md` - 目标文档(必须存在) +2. `doc/FeatureSummary.md` - 上游参考文档 +3. `doc/review-DevelopmentPlan-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 DevelopmentPlan.md 不存在,提示用户: +> DevelopmentPlan.md 不存在,请先使用 `/wd` 生成开发计划。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/md` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-DevelopmentPlan-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认: + > 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/rd` 生成评审报告。 + +## 3. 修改原则 + +### 3.1 增量修改 + +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节: + +```markdown + +新增内容... + +``` + +对于行内新增: + +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +```markdown + +修改后的内容 +``` + +### 3.4 与 FeatureSummary 一致性 + +- 开发任务必须覆盖所有功能 +- 技术方案必须支撑功能需求 +- 阶段划分必须合理 + +## 4. 执行修改 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增开发任务 | 在对应阶段表格中添加行 | +| 修改技术方案 | 更新技术方案章节,添加 MODIFIED 标记 | +| 调整阶段划分 | 移动任务到新阶段,标记变更 | +| 新增风险项 | 在风险管理表格中添加行 | +| 修改里程碑 | 更新里程碑表格 | + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/DevelopmentPlan.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: + +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 与 FeatureSummary 的一致性确认 + +--- + +## 注意事项 + +- DevelopmentPlan 依赖于 FeatureSummary,修改时需确保与上游一致 +- 修改后,下游文档(UIDesign、tasks)可能需要同步更新 +- 技术方案修改需谨慎评估影响范围 +- 建议修改完成后运行 `/ru` 检查下游一致性 + +## 标记清理 + +用户确认修改无误后,可手动删除标记或保留作为变更历史参考。 diff --git a/.claude/skills/mf/SKILL.md b/.claude/skills/mf/SKILL.md new file mode 100644 index 0000000..fa10e2d --- /dev/null +++ b/.claude/skills/mf/SKILL.md @@ -0,0 +1,111 @@ +--- +name: mf +description: 增量修改 FeatureSummary.md,根据用户指令在现有内容基础上更新功能摘要。 +--- + +# Modify FeatureSummary + +当用户调用 `/mf` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/FeatureSummary.md` - 目标文档(必须存在) +2. `doc/PRD.md` - 上游参考文档 +3. `doc/review-FeatureSummary-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 FeatureSummary.md 不存在,提示用户: +> FeatureSummary.md 不存在,请先使用 `/wf` 生成功能摘要。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/mf` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-FeatureSummary-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认: + > 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/rf` 生成评审报告。 + +## 3. 修改原则 + +### 3.1 增量修改 + +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节: + +```markdown + +新增内容... + +``` + +对于行内新增: + +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +```markdown + +修改后的内容 +``` + +### 3.4 与 PRD 一致性 + +- 所有功能必须来源于 PRD +- 修改后的功能描述必须与 PRD 一致 +- 优先级必须与 PRD 匹配 + +## 4. 执行修改 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增功能 | 在对应模块表格中添加行 | +| 修改描述 | 更新功能描述,添加 MODIFIED 标记 | +| 修改优先级 | 更新优先级列 | +| 新增模块 | 在功能清单中添加新章节 | +| 删除功能 | 标记为删除而非直接移除 | + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/FeatureSummary.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: + +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 与 PRD 的一致性确认 + +--- + +## 注意事项 + +- FeatureSummary 依赖于 PRD,修改时需确保与上游一致 +- 修改后,下游文档(DevelopmentPlan 等)可能需要同步更新 +- 建议修改完成后运行 `/rd` 检查下游一致性 + +## 标记清理 + +用户确认修改无误后,可手动删除标记或保留作为变更历史参考。 diff --git a/.claude/skills/mp/SKILL.md b/.claude/skills/mp/SKILL.md new file mode 100644 index 0000000..5fdeb3f --- /dev/null +++ b/.claude/skills/mp/SKILL.md @@ -0,0 +1,144 @@ +--- +name: mp +description: 增量修改 PRD.md,根据用户指令在现有内容基础上更新产品需求文档。 +--- + +# Modify PRD + +当用户调用 `/mp` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/PRD.md` - 目标文档(必须存在) +2. `doc/RequirementsDoc.md` - 上游参考文档 +3. `doc/review-PRD-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 PRD.md 不存在,提示用户: +> PRD.md 不存在,请先使用 `/wp` 生成产品需求文档。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/mp` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-PRD-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单(Critical / Major / Minor),作为本次修改的依据。向用户确认: + > 检测到评审报告 `doc/review-PRD-claude.md`,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/rp` 生成评审报告。 + +### 2.3 支持的修改来源 + +- 具体的修改描述(如"在功能需求中增加用户权限管理模块") +- 评审报告(自动检测或手动指定路径) +- 对应的 RequirementsDoc 变更(如"/mr 已更新需求,请同步 PRD") + +## 3. 修改原则 + +### 3.1 增量修改 +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节,使用 HTML 注释标记: + +```markdown + +新增内容... + +``` + +对于行内新增,使用: +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +对于修改的内容,保留原文作为注释: + +```markdown + +修改后的内容 +``` + +### 3.4 与 RequirementsDoc 一致性 + +- 所有 PRD 内容必须可追溯到 RequirementsDoc +- 如果修改涉及新功能,先确认 RequirementsDoc 中已有对应需求 +- 如果 RequirementsDoc 未包含相关需求,提醒用户先更新需求文档 + +## 4. 执行修改 + +按照用户指令修改文档: + +1. 定位到需要修改的位置 +2. 执行增量修改 +3. 添加相应的标记 +4. 保持文档格式一致性 +5. 确保修改内容与 RequirementsDoc 一致 + +### 4.1 修改类型处理 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增功能点 | 在对应功能模块表格中添加行,关联用户故事 | +| 新增用户故事 | 在 2.2 用户故事列表中添加,分配 US-xxx ID | +| 修改优先级 | 更新功能点优先级,必要时调整用户故事分类 | +| 修改验收标准 | 更新对应功能点的验收标准列 | +| 新增模块 | 在 3.2 功能详情中添加新的子章节 | +| 修改非功能需求 | 在对应章节更新指标或要求 | + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/PRD.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 与 RequirementsDoc 的一致性确认 + +--- + +## 注意事项 + +- PRD 依赖于 RequirementsDoc,修改时需确保与上游文档一致 +- 修改 PRD 后,下游文档(FeatureSummary、DevelopmentPlan 等)可能需要同步更新 +- 保持现有文档风格(标题层级、表格格式、列表样式) +- 用户故事 ID 必须唯一且连续(US-001, US-002...) +- 所有功能点必须关联到用户故事 +- 重大修改建议先运行 `/rp` 评审确认影响范围 +- 修改完成后,建议用户运行 `/rf` 检查下游文档一致性 + +## 标记清理 + +当用户确认修改无误后,可手动删除 `` 和 `` 标记,或保留作为变更历史参考。 + +通过 git 可追溯完整修改历史。 + +## 质量检查 + +修改 PRD 后,自查以下项目: + +- [ ] 修改内容与 RequirementsDoc 一致 +- [ ] 新增用户故事有唯一 ID +- [ ] 新增功能点关联到用户故事 +- [ ] 新增功能点有明确优先级和验收标准 +- [ ] 标记格式正确(`` / ``) +- [ ] 文档结构完整,格式一致 diff --git a/.claude/skills/mr/SKILL.md b/.claude/skills/mr/SKILL.md new file mode 100644 index 0000000..8dd9ad8 --- /dev/null +++ b/.claude/skills/mr/SKILL.md @@ -0,0 +1,95 @@ +--- +name: mr +description: 增量修改 RequirementsDoc.md,根据用户指令在现有内容基础上更新需求文档。 +--- + +# Modify RequirementsDoc + +当用户调用 `/mr` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取 `doc/RequirementsDoc.md` 文件。 + +如果文件不存在,提示用户: +> RequirementsDoc.md 不存在,请先使用人工方式创建需求文档。 + +## 2. 获取修改指令 + +向用户确认修改内容。用户应提供以下信息之一: + +- 具体的修改描述(如"在第3节增加性能需求") +- 评审报告路径(如 `doc/review-RequirementsDoc-claude.md`) +- 直接的修改内容 + +如果用户未提供修改指令,询问: +> 请说明需要修改的内容,或提供评审报告路径。 + +## 3. 修改原则 + +### 3.1 增量修改 +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节,使用 HTML 注释标记: + +```markdown + +新增内容... + +``` + +对于行内新增,使用: +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +对于修改的内容,保留原文作为注释: + +```markdown + +修改后的内容 +``` + +## 4. 执行修改 + +按照用户指令修改文档: + +1. 定位到需要修改的位置 +2. 执行增量修改 +3. 添加相应的标记 +4. 保持文档格式一致性 + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/RequirementsDoc.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 + +--- + +## 注意事项 + +- RequirementsDoc 是文档链源头,修改会影响所有下游文档 +- 修改前确认用户意图,避免误改 +- 保持现有文档风格(标题层级、表格格式、列表样式) +- 重大修改建议先运行 `/rr` 评审确认影响范围 +- 修改完成后,建议用户检查下游文档是否需要同步更新 + +## 标记清理 + +当用户确认修改无误后,可手动删除 `` 和 `` 标记,或保留作为变更历史参考。 + +通过 git 可追溯完整修改历史。 diff --git a/.claude/skills/mt/SKILL.md b/.claude/skills/mt/SKILL.md new file mode 100644 index 0000000..670c00a --- /dev/null +++ b/.claude/skills/mt/SKILL.md @@ -0,0 +1,132 @@ +--- +name: mt +description: 增量修改 tasks.md,根据用户指令在现有内容基础上更新任务列表。 +--- + +# Modify Tasks + +当用户调用 `/mt` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/tasks.md` - 目标文档(必须存在) +2. `doc/UIDesign.md` - 上游参考文档 +3. `doc/DevelopmentPlan.md` - 上游参考文档 +4. `doc/review-tasks-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 tasks.md 不存在,提示用户: +> tasks.md 不存在,请先使用 `/wt` 生成任务列表。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/mt` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-tasks-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认: + > 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/rt` 生成评审报告。 + +## 3. 修改原则 + +### 3.1 增量修改 + +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节: + +```markdown + +新增内容... + +``` + +对于行内新增: + +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +```markdown + +修改后的内容 +``` + +### 3.4 与上游文档一致性 + +- 任务必须覆盖 DevelopmentPlan 所有开发项 +- 任务必须覆盖 UIDesign 所有页面实现 +- 任务依赖关系必须合理 + +## 4. 执行修改 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增任务 | 在对应阶段表格中添加行,分配新 ID | +| 修改描述 | 更新任务描述,添加 MODIFIED 标记 | +| 修改优先级 | 更新优先级列 | +| 修改依赖 | 更新依赖列,检查循环依赖 | +| 修改验收标准 | 更新验收标准列 | +| 调整阶段 | 移动任务到新阶段,更新依赖图 | + +### 4.1 任务 ID 规则 + +- 新增任务 ID 必须唯一 +- ID 格式:T-XXX(三位数字,如 T-001) +- 在现有最大 ID 基础上递增 + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/tasks.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: + +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 新增/修改的任务 ID 列表 +- 与上游文档的一致性确认 + +--- + +## 注意事项 + +- tasks.md 是文档链末端,修改不影响其他文档 +- 任务 ID 必须唯一,不可重复使用已删除的 ID +- 修改依赖关系时需检查是否产生循环依赖 +- 验收标准必须具体可测试 +- 任务粒度要适中 + +## 标记清理 + +用户确认修改无误后,可手动删除标记或保留作为变更历史参考。 + +## 质量检查 + +修改 tasks 后,自查以下项目: + +- [ ] 任务 ID 唯一且格式正确 +- [ ] 无循环依赖 +- [ ] 验收标准明确 +- [ ] 覆盖所有上游功能 +- [ ] 标记格式正确 diff --git a/.claude/skills/mu/SKILL.md b/.claude/skills/mu/SKILL.md new file mode 100644 index 0000000..e0d6061 --- /dev/null +++ b/.claude/skills/mu/SKILL.md @@ -0,0 +1,114 @@ +--- +name: mu +description: 增量修改 UIDesign.md,根据用户指令在现有内容基础上更新 UI 设计文档。 +--- + +# Modify UIDesign + +当用户调用 `/mu` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取以下文件: + +1. `doc/UIDesign.md` - 目标文档(必须存在) +2. `doc/DevelopmentPlan.md` - 上游参考文档 +3. `doc/review-UIDesign-claude.md` - 评审报告(如果存在,自动作为修改依据) + +如果 UIDesign.md 不存在,提示用户: +> UIDesign.md 不存在,请先使用 `/wu` 生成 UI 设计文档。 + +## 2. 确定修改来源 + +按以下优先级确定修改内容: + +### 2.1 用户提供了修改指令 + +如果用户在调用 `/mu` 时附带了参数或说明,直接使用该指令。 + +### 2.2 自动检测评审报告 + +如果用户未提供修改指令,**自动检测** `doc/review-UIDesign-claude.md` 是否存在: + +- **存在**:读取评审报告,提取其中的问题清单,作为本次修改的依据。向用户确认: + > 检测到评审报告,包含 X 个问题。是否根据评审报告进行修改? + +- **不存在**:询问用户: + > 请说明需要修改的内容,或先运行 `/ru` 生成评审报告。 + +## 3. 修改原则 + +### 3.1 增量修改 + +- 保留原有内容结构和格式 +- 仅修改/新增指定部分 +- 不删除未明确要求删除的内容 + +### 3.2 新增内容标记 + +对于新增的段落或章节: + +```markdown + +新增内容... + +``` + +对于行内新增: + +```markdown +原有内容 新增内容 +``` + +### 3.3 修改内容标记 + +```markdown + +修改后的内容 +``` + +### 3.4 与 DevelopmentPlan 一致性 + +- 页面设计必须覆盖所有功能模块 +- 交互流程必须支撑功能需求 +- 设计规范必须统一 + +## 4. 执行修改 + +| 修改类型 | 处理方式 | +|----------|----------| +| 新增页面 | 在页面设计章节添加新子章节 | +| 修改布局 | 更新布局描述,添加 MODIFIED 标记 | +| 修改组件 | 更新组件表格 | +| 修改交互 | 更新交互说明 | +| 新增状态 | 在状态列表中添加项目 | +| 修改设计规范 | 更新设计规范章节 | + +## 5. 保存并验证 + +1. 保存修改后的文档到 `doc/UIDesign.md` +2. 使用 git diff 展示变更内容 +3. 向用户确认修改是否符合预期 + +## 6. 输出摘要 + +向用户展示修改摘要: + +- 修改位置(章节/行号) +- 修改类型(新增/修改/删除) +- 修改内容概要 +- 与 DevelopmentPlan 的一致性确认 + +--- + +## 注意事项 + +- UIDesign 依赖于 DevelopmentPlan,修改时需确保与上游一致 +- 修改后,下游文档(tasks)可能需要同步更新 +- 页面修改需考虑对用户流程的影响 +- 设计规范修改需检查所有页面的一致性 +- 建议修改完成后运行 `/rt` 检查下游一致性 + +## 标记清理 + +用户确认修改无误后,可手动删除标记或保留作为变更历史参考。 diff --git a/.claude/skills/rd/SKILL.md b/.claude/skills/rd/SKILL.md new file mode 100644 index 0000000..665117c --- /dev/null +++ b/.claude/skills/rd/SKILL.md @@ -0,0 +1,101 @@ +--- +name: rd +description: 评审 DevelopmentPlan.md,检查技术可行性和与上游文档一致性,输出结构化评审报告。 +--- + +# Review DevelopmentPlan + +当用户调用 `/rd` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: + +1. `doc/DevelopmentPlan.md` - 目标文档(必须存在) +2. `doc/FeatureSummary.md` - 上游参照文档 + +如果 DevelopmentPlan.md 不存在,提示用户: +> DevelopmentPlan.md 不存在,请先使用 `/wd` 生成开发计划。 + +## 2. 评审维度 + +### 2.1 与 FeatureSummary 一致性检查 + +- 开发任务是否覆盖所有功能模块 +- 技术方案是否支撑功能需求 +- 排期是否合理 + +### 2.2 技术可行性检查 + +- 技术方案是否可行 +- 技术栈选择是否合理 +- 是否存在技术风险 +- 依赖关系是否明确 + +### 2.3 完整性检查 + +- 是否有明确的里程碑划分 +- 是否有资源分配说明 +- 是否有风险应对措施 + +## 3. 生成评审报告 + +输出到 `doc/review-DevelopmentPlan-claude.md`,结构如下: + +```markdown +# DevelopmentPlan 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:MM} | +| 目标文档 | doc/DevelopmentPlan.md | +| 参照文档 | doc/FeatureSummary.md | +| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 | + +## 功能覆盖分析 + +| FeatureSummary 功能 | DevelopmentPlan 对应 | 状态 | +|---------------------|----------------------|------| +| {功能名} | {对应任务/模块} | ✅/⚠️/❌ | + +## 技术风险分析 + +| 风险项 | 影响范围 | 严重程度 | 建议措施 | +|--------|----------|----------|----------| +| {风险} | {范围} | 高/中/低 | {措施} | + +## 问题清单 + +### 严重问题 (Critical) +{问题列表,含位置引用} + +### 一般问题 (Major) +{问题列表,含位置引用} + +### 改进建议 (Minor) +{建议列表} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 +- [ ] {待办事项} +``` + +## 4. 输出规范 + +- 输出语言:中文 +- 问题分级:Critical / Major / Minor +- 包含文件引用(如 `doc/DevelopmentPlan.md:28`) +- 技术风险需明确影响范围和应对建议 + +--- + +## 注意事项 + +- 只做评审,不修改原文档 +- 重点关注技术可行性和风险 +- 评审报告保存后,建议用户根据问题运行 `/md` 修改 diff --git a/.claude/skills/rf/SKILL.md b/.claude/skills/rf/SKILL.md new file mode 100644 index 0000000..ad3e464 --- /dev/null +++ b/.claude/skills/rf/SKILL.md @@ -0,0 +1,96 @@ +--- +name: rf +description: 评审 FeatureSummary.md,对比 PRD 检查一致性,输出结构化评审报告。 +--- + +# Review FeatureSummary + +当用户调用 `/rf` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: + +1. `doc/FeatureSummary.md` - 目标文档(必须存在) +2. `doc/PRD.md` - 上游参照文档 + +如果 FeatureSummary.md 不存在,提示用户: +> FeatureSummary.md 不存在,请先使用 `/wf` 生成功能摘要。 + +## 2. 评审维度 + +### 2.1 与 PRD 一致性检查 + +- 功能模块是否完整覆盖 PRD 3.2 功能详情 +- 功能描述是否与 PRD 一致 +- 优先级标注是否与 PRD 匹配 + +### 2.2 完整性检查 + +- 每个功能模块是否有清晰的描述 +- 是否遗漏 PRD 中的功能点 +- 功能分类是否合理 + +### 2.3 质量检查 + +- 描述是否简洁准确 +- 是否有冗余或重复内容 +- 格式是否规范统一 + +## 3. 生成评审报告 + +输出到 `doc/review-FeatureSummary-claude.md`,结构如下: + +```markdown +# FeatureSummary 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:MM} | +| 目标文档 | doc/FeatureSummary.md | +| 参照文档 | doc/PRD.md | +| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 | + +## 覆盖度分析 + +| PRD 功能模块 | FeatureSummary 对应 | 状态 | +|--------------|---------------------|------| +| {模块名} | {对应位置} | ✅/⚠️/❌ | + +**覆盖率**: X/Y 完全覆盖 + +## 问题清单 + +### 严重问题 (Critical) +{问题列表,含位置引用} + +### 一般问题 (Major) +{问题列表,含位置引用} + +### 改进建议 (Minor) +{建议列表} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 +- [ ] {待办事项} +``` + +## 4. 输出规范 + +- 输出语言:中文 +- 问题分级:Critical / Major / Minor +- 包含文件引用(如 `doc/FeatureSummary.md:15`) +- 问题按严重性排序 + +--- + +## 注意事项 + +- 只做评审,不修改原文档 +- 重点检查与 PRD 的一致性 +- 评审报告保存后,建议用户根据问题运行 `/mf` 修改 diff --git a/.claude/skills/rp/SKILL.md b/.claude/skills/rp/SKILL.md new file mode 100644 index 0000000..befe913 --- /dev/null +++ b/.claude/skills/rp/SKILL.md @@ -0,0 +1,177 @@ +--- +name: rp +description: 评审 PRD.md,对比 RequirementsDoc 检查一致性,输出结构化评审报告。 +--- + +# Review PRD + +当用户调用 `/rp` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: +- 目标文档:`doc/PRD.md` +- 上游文档:`doc/RequirementsDoc.md` + +如果 PRD.md 不存在,提示用户: +> PRD.md 不存在,请先使用 `/wp` 生成 PRD。 + +如果 RequirementsDoc.md 不存在,提示用户: +> RequirementsDoc.md 不存在,无法进行一致性检查。请先创建需求文档。 + +## 2. 评审维度 + +PRD 位于文档链的第二层,需要对比上游 RequirementsDoc 进行评审。 + +### 2.1 与 RequirementsDoc 的一致性 + +- [ ] PRD 是否覆盖了 RequirementsDoc 中的所有功能需求 +- [ ] PRD 是否覆盖了 RequirementsDoc 中的所有非功能需求 +- [ ] PRD 中是否有 RequirementsDoc 中未提及的需求(需标注来源) +- [ ] 术语定义是否与 RequirementsDoc 一致 +- [ ] 优先级划分是否与 RequirementsDoc 一致 + +### 2.2 用户故事质量 + +- [ ] 所有用户故事是否有唯一 ID(US-xxx) +- [ ] 用户故事是否符合格式:作为{角色},我想要{功能},以便{价值} +- [ ] 用户角色是否明确定义 +- [ ] 验收标准是否具体可测试 +- [ ] 用户旅程是否完整描述核心流程 + +### 2.3 功能需求完整性 + +- [ ] 功能架构是否清晰(模块划分合理) +- [ ] 所有功能点是否关联到用户故事 +- [ ] 功能点是否有明确的优先级 +- [ ] 功能点是否有验收标准 +- [ ] 是否遗漏边界情况和异常处理 + +### 2.4 非功能需求 + +- [ ] 性能需求是否有量化指标 +- [ ] 安全需求是否明确 +- [ ] 兼容性需求是否完整 +- [ ] 可用性需求是否可验证 + +### 2.5 文档结构 + +- [ ] 文档结构是否完整(无空章节) +- [ ] 格式是否统一(表格、列表、标题层级) +- [ ] 术语表是否完整 + +## 3. 生成评审报告 + +按以下格式输出评审报告: + +```markdown +# PRD 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:mm} | +| 目标文档 | doc/PRD.md | +| 参照文档 | doc/RequirementsDoc.md | +| 问题统计 | {critical} 个严重 / {major} 个一般 / {minor} 个建议 | + +## 一致性检查 + +### 需求覆盖分析 + +| RequirementsDoc 需求项 | PRD 对应位置 | 状态 | +|------------------------|--------------|------| +| {需求1} | {PRD章节/用户故事ID} | ✅ 已覆盖 / ⚠️ 部分覆盖 / ❌ 未覆盖 | + +### 差异说明 + +{列出 PRD 中新增的、RequirementsDoc 未提及的内容,需说明来源或理由} + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响后续文档生成 + +1. **[位置: doc/PRD.md:行号]** 问题描述 + - 现状:... + - 与 RequirementsDoc 的差异:... + - 建议:... + +### 一般问题 (Major) + +> 建议修复,可提升文档质量 + +1. **[位置]** 问题描述 + - 建议:... + +### 改进建议 (Minor) + +> 可选优化项 + +1. **[位置]** 建议内容 + +## 用户故事评估 + +| 评估项 | 结果 | +|--------|------| +| 用户故事总数 | {数量} | +| 符合格式规范 | {数量} / {总数} | +| 有验收标准 | {数量} / {总数} | +| 关联功能点 | {数量} / {总数} | + +### 用户故事问题 + +{列出不符合规范的用户故事} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +**结论说明**: +- 通过:PRD 与 RequirementsDoc 一致,可进入下一阶段 +- 需修改后通过:存在问题但不影响整体理解,修复后可继续 +- 不通过:存在严重一致性问题或遗漏,需重新生成 + +### 下一步行动 + +- [ ] 行动项1 +- [ ] 行动项2 +``` + +## 4. 保存报告 + +将评审报告保存到 `doc/review-PRD-claude.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示评审摘要: +- 一致性检查结果(覆盖率) +- 发现的问题数量(按严重程度分类) +- 用户故事评估结果 +- 评审结论 +- 报告文件路径 + +--- + +## 注意事项 + +- 评审时保持客观,聚焦于文档质量和一致性 +- 问题描述要具体,给出明确的位置引用(如 `doc/PRD.md:42`) +- 一致性检查要逐项对比,不能遗漏 +- 建议要可操作,避免模糊表述 +- 不要修改原文档,只输出评审报告 + +## 常见问题模式 + +在评审时重点关注以下常见问题: + +1. **需求遗漏**:RequirementsDoc 中有但 PRD 中没有的需求 +2. **需求偏离**:PRD 中的描述与 RequirementsDoc 不一致 +3. **凭空添加**:PRD 中有但 RequirementsDoc 中没有的需求(需要来源说明) +4. **用户故事缺陷**:格式不规范、缺少验收标准、角色不明确 +5. **功能孤立**:功能点未关联到任何用户故事 +6. **优先级冲突**:PRD 与 RequirementsDoc 的优先级划分不一致 diff --git a/.claude/skills/rr/SKILL.md b/.claude/skills/rr/SKILL.md new file mode 100644 index 0000000..7967a9b --- /dev/null +++ b/.claude/skills/rr/SKILL.md @@ -0,0 +1,111 @@ +--- +name: rr +description: 评审 RequirementsDoc.md,检查需求文档的完整性、清晰度和可执行性,输出结构化评审报告。 +--- + +# Review RequirementsDoc + +当用户调用 `/rr` 时,执行以下步骤: + +## 1. 读取目标文档 + +读取 `doc/RequirementsDoc.md` 文件。 + +如果文件不存在,提示用户: +> RequirementsDoc.md 不存在,请先创建需求文档。 + +## 2. 评审维度 + +RequirementsDoc 是文档链的源头,没有上游依赖。重点检查以下维度: + +### 2.1 完整性 +- [ ] 产品概述是否清晰(定位、目标用户、核心价值) +- [ ] 功能需求是否完整列出 +- [ ] 非功能需求是否涵盖(性能、安全、兼容性) +- [ ] 数据规范是否明确(输入输出格式、字段定义) +- [ ] 边界条件和异常情况是否考虑 + +### 2.2 清晰度 +- [ ] 术语定义是否一致,无歧义 +- [ ] 用例描述是否具体可理解 +- [ ] 优先级是否明确标注 +- [ ] 是否有模糊表述("等"、"可能"、"应该"等) + +### 2.3 可执行性 +- [ ] 需求是否可被验证(有明确的验收标准) +- [ ] 技术约束是否合理 +- [ ] 依赖项是否明确 + +### 2.4 结构规范 +- [ ] 文档结构是否清晰(章节划分合理) +- [ ] 格式是否统一(表格、列表、标题层级) + +## 3. 生成评审报告 + +按以下格式输出评审报告: + +```markdown +# RequirementsDoc 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:mm} | +| 目标文档 | doc/RequirementsDoc.md | +| 问题统计 | {critical} 个严重 / {major} 个一般 / {minor} 个建议 | + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响后续文档生成 + +1. **[位置: 第X节/第Y行]** 问题描述 + - 现状:... + - 建议:... + +### 一般问题 (Major) + +> 建议修复,可提升文档质量 + +1. **[位置]** 问题描述 + - 建议:... + +### 改进建议 (Minor) + +> 可选优化项 + +1. **[位置]** 建议内容 + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 + +- [ ] 行动项1 +- [ ] 行动项2 +``` + +## 4. 保存报告 + +将评审报告保存到 `doc/review-RequirementsDoc-claude.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示评审摘要: +- 发现的问题数量(按严重程度分类) +- 评审结论 +- 报告文件路径 + +--- + +## 注意事项 + +- 评审时保持客观,聚焦于文档质量而非业务判断 +- 问题描述要具体,给出明确的位置引用 +- 建议要可操作,避免模糊表述 +- 不要修改原文档,只输出评审报告 diff --git a/.claude/skills/rt/SKILL.md b/.claude/skills/rt/SKILL.md new file mode 100644 index 0000000..7db4e44 --- /dev/null +++ b/.claude/skills/rt/SKILL.md @@ -0,0 +1,115 @@ +--- +name: rt +description: 评审 tasks.md,检查任务完整性和与上游文档一致性,输出结构化评审报告。 +--- + +# Review Tasks + +当用户调用 `/rt` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: + +1. `doc/tasks.md` - 目标文档(必须存在) +2. `doc/UIDesign.md` - 上游参照文档 +3. `doc/DevelopmentPlan.md` - 上游参照文档 + +如果 tasks.md 不存在,提示用户: +> tasks.md 不存在,请先使用 `/wt` 生成任务列表。 + +## 2. 评审维度 + +### 2.1 与上游文档一致性检查 + +- 任务是否覆盖 DevelopmentPlan 所有开发项 +- 任务是否覆盖 UIDesign 所有页面实现 +- 任务优先级是否与功能优先级匹配 + +### 2.2 任务完整性检查 + +- 每个任务是否有明确的描述 +- 任务粒度是否合适(不过大也不过小) +- 任务依赖关系是否明确 +- 验收标准是否清晰 + +### 2.3 可执行性检查 + +- 任务是否可直接开始执行 +- 是否有阻塞项未说明 +- 估时是否合理(如有) + +## 3. 生成评审报告 + +输出到 `doc/review-tasks-claude.md`,结构如下: + +```markdown +# Tasks 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:MM} | +| 目标文档 | doc/tasks.md | +| 参照文档 | doc/UIDesign.md, doc/DevelopmentPlan.md | +| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 | + +## 覆盖度分析 + +### DevelopmentPlan 覆盖 + +| 开发项 | 对应任务 | 状态 | +|--------|----------|------| +| {开发项} | {任务ID/名称} | ✅/⚠️/❌ | + +### UIDesign 覆盖 + +| UI 页面 | 对应任务 | 状态 | +|---------|----------|------| +| {页面名} | {任务ID/名称} | ✅/⚠️/❌ | + +**总覆盖率**: X/Y + +## 任务质量分析 + +| 检查项 | 通过数 | 总数 | +|--------|--------|------| +| 有明确描述 | X | Y | +| 有验收标准 | X | Y | +| 粒度合适 | X | Y | + +## 问题清单 + +### 严重问题 (Critical) +{问题列表,含位置引用} + +### 一般问题 (Major) +{问题列表,含位置引用} + +### 改进建议 (Minor) +{建议列表} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 +- [ ] {待办事项} +``` + +## 4. 输出规范 + +- 输出语言:中文 +- 问题分级:Critical / Major / Minor +- 包含文件引用(如 `doc/tasks.md:12`) +- 任务问题需说明对开发执行的影响 + +--- + +## 注意事项 + +- 只做评审,不修改原文档 +- 重点检查任务覆盖度和可执行性 +- tasks.md 是文档链末端,必须覆盖所有上游功能 +- 评审报告保存后,建议用户根据问题运行 `/mt` 修改 diff --git a/.claude/skills/ru/SKILL.md b/.claude/skills/ru/SKILL.md new file mode 100644 index 0000000..8fbc125 --- /dev/null +++ b/.claude/skills/ru/SKILL.md @@ -0,0 +1,105 @@ +--- +name: ru +description: 评审 UIDesign.md,对比 DevelopmentPlan 检查设计一致性,输出结构化评审报告。 +--- + +# Review UIDesign + +当用户调用 `/ru` 时,执行以下步骤: + +## 1. 读取文档 + +读取以下文件: + +1. `doc/UIDesign.md` - 目标文档(必须存在) +2. `doc/DevelopmentPlan.md` - 上游参照文档 + +如果 UIDesign.md 不存在,提示用户: +> UIDesign.md 不存在,请先使用 `/wu` 生成 UI 设计文档。 + +## 2. 评审维度 + +### 2.1 与 DevelopmentPlan 一致性检查 + +- UI 页面是否覆盖所有功能模块 +- 交互流程是否与开发计划匹配 +- 页面结构是否支撑功能需求 + +### 2.2 设计完整性检查 + +- 页面列表是否完整 +- 每个页面是否有清晰的布局描述 +- 交互说明是否充分 +- 状态变化是否考虑全面(加载、错误、空状态等) + +### 2.3 可用性检查 + +- 用户流程是否顺畅 +- 信息架构是否合理 +- 是否有一致的设计规范 + +## 3. 生成评审报告 + +输出到 `doc/review-UIDesign-claude.md`,结构如下: + +```markdown +# UIDesign 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | {YYYY-MM-DD HH:MM} | +| 目标文档 | doc/UIDesign.md | +| 参照文档 | doc/DevelopmentPlan.md | +| 问题统计 | X 个严重 / Y 个一般 / Z 个建议 | + +## 页面覆盖分析 + +| DevelopmentPlan 功能 | UIDesign 页面 | 状态 | +|----------------------|---------------|------| +| {功能名} | {对应页面} | ✅/⚠️/❌ | + +**覆盖率**: X/Y 完全覆盖 + +## 设计一致性检查 + +| 检查项 | 结果 | +|--------|------| +| 页面命名规范 | ✅/❌ | +| 布局风格统一 | ✅/❌ | +| 交互模式一致 | ✅/❌ | + +## 问题清单 + +### 严重问题 (Critical) +{问题列表,含位置引用} + +### 一般问题 (Major) +{问题列表,含位置引用} + +### 改进建议 (Minor) +{建议列表} + +## 评审结论 + +{通过 / 需修改后通过 / 不通过} + +### 下一步行动 +- [ ] {待办事项} +``` + +## 4. 输出规范 + +- 输出语言:中文 +- 问题分级:Critical / Major / Minor +- 包含文件引用(如 `doc/UIDesign.md:45`) +- 设计问题需说明影响的用户体验 + +--- + +## 注意事项 + +- 只做评审,不修改原文档 +- 重点检查页面覆盖度和设计一致性 +- 评审报告保存后,建议用户根据问题运行 `/mu` 修改 diff --git a/.claude/skills/up/SKILL.md b/.claude/skills/up/SKILL.md new file mode 100644 index 0000000..4b1ca96 --- /dev/null +++ b/.claude/skills/up/SKILL.md @@ -0,0 +1,78 @@ +--- +name: update +aliases: [up] +description: 收集用户反馈并更新最近使用的 skill。可通过 /update 或 /up 调用。在用完某个 skill 后调用此命令来优化该 skill。 +disable-model-invocation: true +argument-hint: [skill-name] +--- + +# Skill 更新助手 + +当用户调用 `/up` 或 `/up ` 时,执行以下步骤: + +## 1. 识别目标 Skill + +**如果用户提供了参数 `$ARGUMENTS`**: +- 直接使用指定的 skill 名称作为更新目标 + +**如果没有提供参数**: +分析当前对话历史,找出最近使用的 skill: +- 搜索对话中的 `` 标签,识别调用过的 skill +- 如果找到多个 skill,让用户确认要更新哪一个 +- 如果没有找到任何 skill 调用记录,提示用户先使用一个 skill + +## 2. 收集用户反馈 + +向用户询问以下问题(使用 AskUserQuestion 工具): + +**问题 1:这次使用体验如何?** +- 很好,skill 完全满足需求 +- 基本满足,但有改进空间 +- 不太满意,需要较大调整 + +**问题 2:具体需要改进的方面?**(多选) +- 执行步骤不够清晰 +- 缺少某些功能 +- 输出格式需要调整 +- 提示词需要优化 +- 其他(用户自定义输入) + +## 3. 分析优化点 + +基于用户反馈和本次 skill 使用过程,分析以下方面: + +1. **执行流程**:哪些步骤可以简化或合并? +2. **指令清晰度**:哪些指令描述不够明确? +3. **遗漏功能**:有哪些场景没有覆盖到? +4. **输出质量**:输出格式是否符合用户预期? + +## 4. 定位 Skill 文件 + +按以下优先级搜索 skill 文件: +1. 项目级:`.claude/skills//SKILL.md` +2. 用户级:`~/.claude/skills//SKILL.md` + +## 5. 更新 Skill + +读取现有的 SKILL.md 文件内容,根据分析结果进行更新: + +- 保持 frontmatter 格式不变(除非需要修改 description) +- 优化执行步骤的描述 +- 添加缺失的功能说明 +- 改进提示词的表达方式 +- 添加必要的注意事项或边界情况处理 + +## 6. 确认更新 + +在更新前,向用户展示: +- 修改前后的对比(diff 格式) +- 说明每处修改的原因 + +用户确认后才执行实际的文件更新。 + +## 注意事项 + +- 如果 skill 文件不存在或路径无法确定,提示用户手动指定路径 +- 更新时保持 skill 的原有风格和结构 +- 重大修改需要用户明确确认 +- 保留原有的有效内容,只做增量优化 diff --git a/.claude/skills/wd/SKILL.md b/.claude/skills/wd/SKILL.md new file mode 100644 index 0000000..9343961 --- /dev/null +++ b/.claude/skills/wd/SKILL.md @@ -0,0 +1,323 @@ +--- +name: wd +description: 从上游文档生成 DevelopmentPlan.md,包含技术方案和开发排期。 +--- + +# Write DevelopmentPlan + +> **文档定位**:DevelopmentPlan 是「执行蓝图」文档,偏技术语言和时间约束。定义技术架构、实现方案、开发阶段、里程碑,是开发团队的行动指南。 + +当用户调用 `/wd` 时,执行以下步骤: + +## 1. 读取源文档 + +读取以下文件: + +1. `doc/RequirementsDoc.md` - 必须存在 +2. `doc/PRD.md` - 必须存在 +3. `doc/FeatureSummary.md` - 必须存在 + +如果文件不存在,提示用户: +> 缺少上游文档,请先确保 RequirementsDoc.md、PRD.md 和 FeatureSummary.md 存在。 + +如果已存在 `doc/DevelopmentPlan.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析开发需求 + +从上游文档中提取以下信息: + +### 2.1 功能需求 + +- 从 FeatureSummary 获取功能清单和契约 +- 从 PRD 获取功能详情和验收标准 + +### 2.2 技术约束 + +- 从 PRD 获取技术约束 +- 从 RequirementsDoc 获取技术决策 + +### 2.3 优先级排序 + +- 按 P0 → P1 → P2 顺序规划开发 +- 考虑功能依赖关系 + +## 3. 生成 DevelopmentPlan + +按以下结构生成文档: + +```markdown +# {产品名称} - 开发计划 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 来源文档 | FeatureSummary.md | + +## 1. 项目概述 + +### 1.1 项目目标 + +{从 PRD 提取的项目目标} + +### 1.2 技术栈 + +| 层级 | 技术选型 | 版本 | 说明 | +|------|----------|------|------| +| 前端 | {技术} | {版本} | {说明} | +| 后端 | {技术} | {版本} | {说明} | +| 数据库 | {技术} | {版本} | {说明} | +| 基础设施 | {技术} | {版本} | {说明} | + +### 1.3 开发原则 + +{开发规范和原则} + +## 2. 技术架构 + +### 2.1 系统架构图 + +**【必须】使用架构图展示系统整体结构:** + +``` +┌─────────────────────────────────────────────────────────┐ +│ 客户端层 │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Web App │ │ Mobile App │ │ +│ └──────┬───────┘ └──────┬───────┘ │ +└─────────┼─────────────────┼─────────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────┐ +│ API 网关层 │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ API Gateway / Load Balancer │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ 服务层 │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ 服务 A │ │ 服务 B │ │ 服务 C │ │ +│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ +└────────┼───────────────┼───────────────┼────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────┐ +│ 数据层 │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ 数据库 │ │ 缓存 │ │ 消息队列 │ │ +│ └────────────┘ └────────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 模块依赖图 + +**【必须】使用依赖图展示模块间关系:** + +``` +┌──────────────┐ +│ 模块 A │ +│ (核心模块) │ +└──────┬───────┘ + │ + ┌───┴───┐ + ▼ ▼ +┌──────┐ ┌──────┐ +│模块B │ │模块C │ +└──┬───┘ └──┬───┘ + │ │ + ▼ ▼ +┌──────────────┐ +│ 模块 D │ +│ (基础设施) │ +└──────────────┘ +``` + +### 2.3 数据流图 + +**【必须】使用数据流图展示关键数据流转:** + +``` +用户请求 ──▶ API Gateway ──▶ 服务A ──▶ 数据库 + │ + ▼ + 缓存层 + │ + ▼ + 服务B ──▶ 外部API +``` + +## 3. 开发阶段 + +### 3.1 阶段时间线 + +**【必须】使用时间线展示开发阶段:** + +``` + Phase 1 Phase 2 Phase 3 + │ │ │ + {起止日期} {起止日期} {起止日期} + │ │ │ + ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌─────────┐ + │ 基础 │ ────▶ │ 核心 │ ────▶ │ 优化 │ + │ 架构 │ │ 功能 │ │ 扩展 │ + └─────────┘ └─────────┘ └─────────┘ + + 交付物: 交付物: 交付物: + • {交付1} • {交付1} • {交付1} + • {交付2} • {交付2} • {交付2} +``` + +### 3.2 Phase 1: {阶段名称} + +**目标**: {阶段目标} + +**时间**: {起止日期} + +| 任务ID | 任务 | 描述 | 依赖 | 优先级 | 关联功能 | +|--------|------|------|------|--------|----------| +| T-001 | {任务名} | {描述} | - | P0 | F-001 | +| T-002 | {任务名} | {描述} | T-001 | P0 | F-002 | + +**阶段依赖图:** + +``` +T-001 ──▶ T-002 ──▶ T-003 + │ + └──▶ T-004 +``` + +{重复以上结构覆盖所有阶段} + +## 4. 技术方案 + +### 4.1 {模块名称} + +**功能**: {功能描述} + +**技术选型**: + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| {组件} | {技术} | {理由} | + +**架构设计**: + +``` +┌─────────────────────────────────────┐ +│ {模块名称} │ +├─────────────────────────────────────┤ +│ ┌─────────┐ ┌─────────┐ │ +│ │ 组件A │ ───▶ │ 组件B │ │ +│ └─────────┘ └─────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────┐ │ +│ │ 数据层 │ │ +│ └─────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +**接口设计**: + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| {接口名} | GET/POST | /api/xxx | {说明} | + +**实现要点**: + +- {技术要点1} +- {技术要点2} + +{重复以上结构覆盖所有模块} + +## 5. 风险管理 + +| 风险 | 可能性 | 影响 | 应对措施 | 负责人 | +|------|--------|------|----------|--------| +| {风险} | 高/中/低 | 高/中/低 | {措施} | {负责人} | + +## 6. 里程碑 + +**【必须】使用里程碑图展示关键节点:** + +``` +M1 M2 M3 M4 +│ │ │ │ +▼ ▼ ▼ ▼ +◆───────────────◆───────────────◆───────────────◆ +│ │ │ │ +{日期} {日期} {日期} {日期} +{里程碑名} {里程碑名} {里程碑名} {里程碑名} +``` + +| 里程碑 | 日期 | 目标 | 交付物 | 验收标准 | +|--------|------|------|--------|----------| +| M1 | {日期} | {目标} | {交付物} | {标准} | + +## 7. 资源需求 + +| 角色 | 人数 | 职责 | 参与阶段 | +|------|------|------|----------| +| {角色} | {人数} | {职责} | Phase 1-2 | +``` + +## 4. 保存文档 + +将生成的 DevelopmentPlan 保存到 `doc/DevelopmentPlan.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- DevelopmentPlan 文件路径 +- 开发阶段数量 +- 技术方案模块数量 +- 建议的下一步操作(运行 `/rd` 评审) + +--- + +## 可视化输出要求 + +DevelopmentPlan 作为「执行蓝图」文档,需要清晰传达技术方案和时间安排,**必须包含**: + +| 章节 | 可视化形式 | 必要性 | +|------|------------|--------| +| 2.1 系统架构图 | 架构图(ASCII) | **必须** | +| 2.2 模块依赖图 | 依赖图(ASCII) | **必须** | +| 2.3 数据流图 | 数据流图(ASCII) | **必须** | +| 3.1 阶段时间线 | 时间线(ASCII) | **必须** | +| 3.x 阶段依赖图 | 任务依赖图 | **必须** | +| 4.x 模块架构 | 模块架构图 | 建议 | +| 6. 里程碑 | 里程碑图 | **必须** | + +## 注意事项 + +- DevelopmentPlan 使用**技术语言**,面向开发团队 +- 开发计划必须覆盖 FeatureSummary 所有功能 +- 技术方案要具体可执行,避免过于抽象 +- 阶段划分要合理,考虑依赖关系 +- 时间安排要务实,预留缓冲 +- 风险评估要全面,有应对措施 + +## 质量检查 + +生成 DevelopmentPlan 后,自查以下项目: + +- [ ] 覆盖 FeatureSummary 所有功能 +- [ ] **系统架构图清晰展示整体结构** +- [ ] **模块依赖图清晰展示依赖关系** +- [ ] **数据流图展示关键数据流转** +- [ ] **开发阶段有时间线图** +- [ ] **每个阶段有任务依赖图** +- [ ] **里程碑有里程碑图** +- [ ] 技术方案具体可执行 +- [ ] 任务 ID 唯一(T-xxx) +- [ ] 任务与功能 ID 关联 diff --git a/.claude/skills/wf/SKILL.md b/.claude/skills/wf/SKILL.md new file mode 100644 index 0000000..c8ac3c4 --- /dev/null +++ b/.claude/skills/wf/SKILL.md @@ -0,0 +1,234 @@ +--- +name: wf +description: 从 RequirementsDoc.md 和 PRD.md 生成 FeatureSummary.md,提供功能全貌概览。 +--- + +# Write FeatureSummary + +> **文档定位**:FeatureSummary 是「功能契约」文档,是产品与开发的桥梁。精确定义功能边界、输入输出、依赖关系,确保双方对"做什么"达成共识。 + +当用户调用 `/wf` 时,执行以下步骤: + +## 1. 读取源文档 + +读取以下文件: + +1. `doc/RequirementsDoc.md` - 必须存在 +2. `doc/PRD.md` - 必须存在 + +如果文件不存在,提示用户: +> 缺少上游文档,请先确保 RequirementsDoc.md 和 PRD.md 存在。 + +如果已存在 `doc/FeatureSummary.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析功能需求 + +从 PRD 中提取以下信息: + +### 2.1 功能模块 + +- 从 PRD 3.1 功能架构提取模块结构 +- 从 PRD 3.2 功能详情提取各模块功能点 + +### 2.2 功能分类 + +按以下维度整理功能: + +- 按模块分组 +- 按优先级标注(P0/P1/P2) +- 按用户角色关联 + +### 2.3 功能边界 + +明确每个功能的: + +- 输入:触发条件、输入数据 +- 输出:预期结果、输出数据 +- 边界:不包含什么、异常情况 + +## 3. 生成 FeatureSummary + +按以下结构生成文档: + +```markdown +# {产品名称} - 功能摘要 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 来源文档 | PRD.md | + +## 1. 功能总览 + +### 1.1 功能统计 + +| 类别 | 数量 | +|------|------| +| 功能模块 | X 个 | +| P0 功能 | X 个 | +| P1 功能 | X 个 | +| P2 功能 | X 个 | + +### 1.2 功能架构图 + +**【必须】使用模块图展示功能架构和模块关系:** + +``` +┌─────────────────────────────────────────────────┐ +│ {产品名称} │ +├─────────────────────────────────────────────────┤ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 模块A │ │ 模块B │ │ 模块C │ │ +│ │ ──────── │ │ ──────── │ │ ──────── │ │ +│ │ • 功能1 │ │ • 功能1 │ │ • 功能1 │ │ +│ │ • 功能2 │ │ • 功能2 │ │ │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +### 1.3 模块依赖关系 + +**【必须】使用依赖图展示模块间关系:** + +``` +┌──────────┐ +│ 模块A │ +└────┬─────┘ + │ 依赖 + ▼ +┌──────────┐ ┌──────────┐ +│ 模块B │ ◀── │ 模块C │ +└──────────┘ └──────────┘ +``` + +## 2. 功能清单 + +### 2.1 {模块名称} + +**模块职责**: {一句话描述模块职责} + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-001 | {功能名} | {简要描述} | P0 | US-xxx | + +#### 功能契约详情 + +**F-001: {功能名}** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | {什么情况下触发此功能} | +| **输入** | {输入数据/参数} | +| **处理逻辑** | {核心处理步骤} | +| **输出** | {输出结果/返回值} | +| **异常情况** | {可能的错误及处理} | +| **边界说明** | {不包含什么、限制条件} | + +{重复以上结构覆盖所有功能} + +{重复以上结构覆盖所有模块} + +## 3. 功能依赖矩阵 + +**【必须】使用矩阵表格展示功能间依赖:** + +| 功能 | 依赖 F-001 | 依赖 F-002 | 依赖 F-003 | +|------|------------|------------|------------| +| F-001 | - | | | +| F-002 | ✓ | - | | +| F-003 | | ✓ | - | + +说明: +- ✓ 表示行功能依赖列功能 +- 空白表示无依赖 + +## 4. 功能流程图 + +**【必须】使用流程图展示核心功能流程:** + +### 4.1 {核心流程名称} + +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ +│ F-001 │ ──▶ │ F-002 │ ──▶ │ F-003 │ ──▶ │ 完成 │ +│ {功能} │ │ {功能} │ │ {功能} │ │ │ +└─────────┘ └─────────┘ └─────────┘ └─────────┘ + │ + ▼ 异常 + ┌─────────┐ + │ 错误处理 │ + └─────────┘ +``` + +## 5. 版本规划 + +| 版本 | 包含功能 | 功能ID | 目标 | +|------|----------|--------|------| +| MVP | {功能列表} | F-001, F-002 | {目标} | +| v1.1 | {功能列表} | F-003, F-004 | {目标} | +| v2.0 | {功能列表} | F-005+ | {目标} | + +## 6. 接口契约预览 + +> 详细接口定义在 DevelopmentPlan 中,此处仅列出关键接口 + +| 功能 | 接口类型 | 简要说明 | +|------|----------|----------| +| F-001 | API | {说明} | +| F-002 | Event | {说明} | +``` + +## 4. 保存文档 + +将生成的 FeatureSummary 保存到 `doc/FeatureSummary.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- FeatureSummary 文件路径 +- 功能模块数量 +- 各优先级功能数量 +- 建议的下一步操作(运行 `/rf` 评审) + +--- + +## 可视化输出要求 + +FeatureSummary 作为「功能契约」文档,需要精确传达功能定义,**必须包含**: + +| 章节 | 可视化形式 | 必要性 | +|------|------------|--------| +| 1.2 功能架构图 | 模块图(ASCII) | **必须** | +| 1.3 模块依赖关系 | 依赖图(ASCII) | **必须** | +| 3. 功能依赖矩阵 | 矩阵表格 | **必须** | +| 4. 功能流程图 | 流程图(ASCII) | **必须** | + +## 注意事项 + +- FeatureSummary 是产品与开发的**桥梁**,语言要精确、无歧义 +- 功能摘要必须完全来源于 PRD,不要臆造功能 +- 每个功能必须有明确的**输入、输出、边界** +- 功能 ID 必须唯一(F-xxx 格式) +- 优先级必须与 PRD 一致 +- 功能依赖关系必须明确,避免循环依赖 + +## 质量检查 + +生成 FeatureSummary 后,自查以下项目: + +- [ ] 所有功能都有唯一 ID(F-xxx) +- [ ] 所有功能都有契约详情(输入/输出/边界) +- [ ] **功能架构图清晰展示模块结构** +- [ ] **模块依赖图清晰展示依赖关系** +- [ ] **功能依赖矩阵完整** +- [ ] **核心流程有流程图** +- [ ] 优先级与 PRD 一致 +- [ ] 无遗漏 PRD 中的功能 diff --git a/.claude/skills/wp/SKILL.md b/.claude/skills/wp/SKILL.md new file mode 100644 index 0000000..e647daf --- /dev/null +++ b/.claude/skills/wp/SKILL.md @@ -0,0 +1,318 @@ +--- +name: wp +description: 从 RequirementsDoc.md 生成 PRD.md,将需求文档转化为结构化的产品需求文档。 +--- + +# Write PRD + +> **文档定位**:PRD 是「价值主张」文档,使用业务语言描述产品要解决什么问题、为谁创造什么价值。面向产品、业务、管理层沟通。 + +当用户调用 `/wp` 时,执行以下步骤: + +## 1. 读取源文档 + +读取 `doc/RequirementsDoc.md` 文件。 + +如果文件不存在,提示用户: +> RequirementsDoc.md 不存在,请先创建需求文档。 + +如果已存在 `doc/PRD.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析需求文档 + +从 RequirementsDoc 中提取以下信息: + +### 2.1 产品定位 + +- 产品名称 +- 目标用户 +- 核心价值主张 +- 竞品对比(如有) + +### 2.2 功能需求 + +- 功能模块划分 +- 各模块详细需求 +- 功能优先级(P0/P1/P2) + +### 2.3 非功能需求 + +- 性能要求 +- 安全要求 +- 兼容性要求 +- 可用性要求 + +### 2.4 约束条件 + +- 技术约束 +- 业务约束 +- 时间约束 + +## 3. 生成 PRD + +按以下结构生成 PRD 文档: + +```markdown +# {产品名称} - 产品需求文档 (PRD) + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 状态 | 草稿 | + +## 1. 产品概述 + +### 1.1 产品背景 + +{从 RequirementsDoc 提取,说明产品解决的问题和市场机会} + +### 1.2 产品定位 + +{目标用户、核心价值、差异化优势} + +### 1.3 产品目标 + +| 目标 | 指标 | 衡量方式 | +|------|------|----------| +| {业务目标} | {量化指标} | {如何衡量} | + +## 2. 用户故事 + +PRD 以用户故事为核心驱动,所有功能需求都应对应到具体的用户故事。 + +### 2.1 用户角色定义 + +| 角色 | 描述 | 核心目标 | 痛点 | +|------|------|----------|------| +| {角色1} | {角色描述} | {核心目标} | {当前痛点} | + +### 2.2 用户故事列表 + +按优先级排列的用户故事: + +#### P0 - 核心故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-001 | 作为{角色},我想要{功能},以便{价值} | {验收标准} | + +#### P1 - 重要故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-xxx | 作为{角色},我想要{功能},以便{价值} | {验收标准} | + +#### P2 - 次要故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-xxx | 作为{角色},我想要{功能},以便{价值} | {验收标准} | + +### 2.3 用户旅程 + +**【必须】使用流程图展示核心用户旅程:** + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 触发点 │ ──▶ │ 关键步骤 │ ──▶ │ 目标达成 │ +│ {描述} │ │ {描述} │ │ {描述} │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + ▼ ▼ ▼ + {用户感受} {用户感受} {用户感受} +``` + +{描述用户完成核心任务的完整流程,从触发点到目标达成} + +## 3. 功能需求 + +> 功能需求与用户故事的对应关系 + +### 3.1 功能架构 + +**【必须】使用树状图或模块图展示功能架构:** + +``` +{产品名称} +├── {模块A} +│ ├── {功能A1} +│ └── {功能A2} +├── {模块B} +│ ├── {功能B1} +│ └── {功能B2} +└── {模块C} + └── {功能C1} +``` + +### 3.2 功能详情 + +#### 3.2.1 {模块名称} + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| {功能1} | {描述} | US-001 | P0 | {标准} | + +{重复以上结构覆盖所有模块} + +## 4. 非功能需求 + +### 4.1 性能需求 + +| 指标 | 要求 | 说明 | +|------|------|------| +| {响应时间} | {要求} | {场景说明} | + +### 4.2 安全需求 + +{数据安全、访问控制、合规要求} + +### 4.3 兼容性需求 + +| 平台/环境 | 支持版本 | +|-----------|----------| +| {平台} | {版本} | + +### 4.4 可用性需求 + +{SLA、故障恢复、监控告警} + +## 5. 数据需求 + +### 5.1 数据模型 + +**【建议】使用 ER 图或表格展示核心实体关系:** + +``` +┌──────────┐ ┌──────────┐ +│ 实体A │ 1───n │ 实体B │ +├──────────┤ ├──────────┤ +│ 字段1 │ │ 字段1 │ +│ 字段2 │ │ 字段2 │ +└──────────┘ └──────────┘ +``` + +### 5.2 数据规范 + +| 字段 | 类型 | 说明 | 校验规则 | +|------|------|------|----------| +| {字段名} | {类型} | {说明} | {规则} | + +## 6. 接口需求 + +### 6.1 外部接口 + +| 接口 | 用途 | 提供方 | +|------|------|--------| +| {接口名} | {用途} | {第三方} | + +### 6.2 内部接口 + +{模块间接口规范} + +## 7. 约束与依赖 + +### 7.1 技术约束 + +| 约束 | 说明 | 影响 | +|------|------|------| +| {约束} | {说明} | {影响范围} | + +### 7.2 业务约束 + +{法规、政策、合同限制} + +### 7.3 外部依赖 + +{第三方服务、团队依赖} + +## 8. 里程碑规划 + +**【建议】使用时间线展示里程碑:** + +``` +Phase 1 Phase 2 Phase 3 + │ │ │ + ▼ ▼ ▼ +┌──────┐ ┌──────┐ ┌──────┐ +│ MVP │ ────▶ │ v1.1 │ ────▶ │ v2.0 │ +└──────┘ └──────┘ └──────┘ +{日期} {日期} {日期} +``` + +| 阶段 | 目标 | 交付物 | +|------|------|--------| +| {阶段1} | {目标} | {交付物} | + +## 9. 风险评估 + +| 风险 | 可能性 | 影响 | 应对措施 | +|------|--------|------|----------| +| {风险1} | 高/中/低 | 高/中/低 | {措施} | + +## 附录 + +### A. 术语表 + +| 术语 | 定义 | +|------|------| +| {术语} | {定义} | + +### B. 参考文档 + +- RequirementsDoc.md +``` + +## 4. 保存文档 + +将生成的 PRD 保存到 `doc/PRD.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- PRD 文件路径 +- 包含的功能模块数量 +- 主要章节概览 +- 建议的下一步操作(运行 `/rp` 评审) + +--- + +## 可视化输出要求 + +PRD 作为「价值主张」文档,需要便于业务沟通理解,**必须包含**: + +| 章节 | 可视化形式 | 必要性 | +|------|------------|--------| +| 2.3 用户旅程 | 流程图(ASCII) | **必须** | +| 3.1 功能架构 | 树状图/模块图 | **必须** | +| 5.1 数据模型 | ER 图 | 建议 | +| 8. 里程碑规划 | 时间线 | 建议 | + +## 注意事项 + +- PRD 使用**业务语言**,避免过多技术术语 +- PRD 内容必须完全来源于 RequirementsDoc,不要臆造需求 +- 如果 RequirementsDoc 信息不完整,在对应章节标注"待补充" +- 保持语言风格与现有文档一致 +- 优先级标注遵循 P0 > P1 > P2 规则 +- 验收标准要具体可测试 + +## 质量检查 + +生成 PRD 后,自查以下项目: + +- [ ] 所有用户故事都有唯一 ID(US-xxx) +- [ ] 所有用户故事都符合格式:作为{角色},我想要{功能},以便{价值} +- [ ] 所有功能点都关联到用户故事 +- [ ] 所有功能点都有明确的优先级 +- [ ] 所有功能点都有验收标准 +- [ ] **用户旅程有流程图** +- [ ] **功能架构有模块图** +- [ ] 非功能需求有量化指标 +- [ ] 无遗漏 RequirementsDoc 中的重要需求 +- [ ] 文档结构完整,无空章节(或标注"待补充") diff --git a/.claude/skills/wt/SKILL.md b/.claude/skills/wt/SKILL.md new file mode 100644 index 0000000..253fe91 --- /dev/null +++ b/.claude/skills/wt/SKILL.md @@ -0,0 +1,128 @@ +--- +name: wt +description: 从上游文档生成 tasks.md,创建可直接执行的任务列表。 +--- + +# Write Tasks + +当用户调用 `/wt` 时,执行以下步骤: + +## 1. 读取源文档 + +读取以下文件: + +1. `doc/RequirementsDoc.md` - 必须存在 +2. `doc/PRD.md` - 必须存在 +3. `doc/FeatureSummary.md` - 必须存在 +4. `doc/DevelopmentPlan.md` - 必须存在 +5. `doc/UIDesign.md` - 必须存在 + +如果文件不存在,提示用户: +> 缺少上游文档,请确保所有上游文档存在。 + +如果已存在 `doc/tasks.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析任务需求 + +从上游文档中提取以下信息: + +### 2.1 开发任务 + +- 从 DevelopmentPlan 获取开发阶段和任务 +- 从 UIDesign 获取页面实现任务 + +### 2.2 任务依赖 + +- 分析任务间的依赖关系 +- 确定任务执行顺序 + +### 2.3 验收标准 + +- 从 PRD 获取功能验收标准 +- 转化为任务级别的完成标准 + +## 3. 生成 Tasks + +按以下结构生成文档: + +```markdown +# {产品名称} - 任务列表 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 来源文档 | UIDesign.md, DevelopmentPlan.md | + +## 1. 任务总览 + +| 统计项 | 数量 | +|--------|------| +| 总任务数 | X | +| P0 任务 | X | +| P1 任务 | X | +| P2 任务 | X | + +## 2. Phase 1 任务 + +### 2.1 {模块/功能名} + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| +| T-001 | {任务名} | {描述} | P0 | - | {标准} | +| T-002 | {任务名} | {描述} | P0 | T-001 | {标准} | + +{重复以上结构覆盖所有模块} + +## 3. Phase 2 任务 + +{同上结构} + +## 4. Phase N 任务 + +{同上结构} + +## 5. 任务依赖图 + +``` +T-001 (基础设施) + ├── T-002 (功能A) + │ └── T-005 (功能A优化) + └── T-003 (功能B) + └── T-004 (功能B扩展) +``` + +## 6. 执行检查清单 + +- [ ] T-001: {任务名} +- [ ] T-002: {任务名} +{所有任务的检查清单} +``` + +## 4. 保存文档 + +将生成的 tasks 保存到 `doc/tasks.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- tasks 文件路径 +- 任务总数 +- 各阶段任务分布 +- 建议的下一步操作(运行 `/rt` 评审) + +--- + +## 注意事项 + +- 任务必须覆盖 DevelopmentPlan 和 UIDesign 所有内容 +- 任务 ID 必须唯一(T-001, T-002...) +- 每个任务必须有明确的验收标准 +- 任务粒度要适中,可在合理时间内完成 +- 依赖关系要明确,避免循环依赖 +- 任务应可直接执行,无歧义 diff --git a/.claude/skills/wu/SKILL.md b/.claude/skills/wu/SKILL.md new file mode 100644 index 0000000..3b17b58 --- /dev/null +++ b/.claude/skills/wu/SKILL.md @@ -0,0 +1,352 @@ +--- +name: wu +description: 从上游文档生成 UIDesign.md,覆盖所有用户界面设计。 +--- + +# Write UIDesign + +> **文档定位**:UIDesign 是「界面蓝图」文档,用 ASCII 原型图精确传达页面布局、组件结构、交互流程,是前端开发的直接参考。 + +当用户调用 `/wu` 时,执行以下步骤: + +## 1. 读取源文档 + +读取以下文件: + +1. `doc/RequirementsDoc.md` - 必须存在 +2. `doc/PRD.md` - 必须存在 +3. `doc/FeatureSummary.md` - 必须存在 +4. `doc/DevelopmentPlan.md` - 必须存在 + +如果文件不存在,提示用户: +> 缺少上游文档,请确保所有上游文档存在。 + +如果已存在 `doc/UIDesign.md`,同时读取作为参考(保持风格一致)。 + +## 2. 分析 UI 需求 + +从上游文档中提取以下信息: + +### 2.1 页面需求 + +- 从 PRD 用户旅程分析所需页面 +- 从 FeatureSummary 获取功能对应的界面 +- 从 DevelopmentPlan 获取技术实现约束 + +### 2.2 用户流程 + +- 主要用户旅程 +- 页面跳转关系 +- 交互流程 + +## 3. 生成 UIDesign + +按以下结构生成文档: + +```markdown +# {产品名称} - UI 设计文档 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | {YYYY-MM-DD} | +| 来源文档 | DevelopmentPlan.md | + +## 1. 设计概述 + +### 1.1 设计原则 + +{UI 设计原则和规范} + +### 1.2 页面总览 + +| 页面ID | 页面名称 | 描述 | 对应功能 | 优先级 | +|--------|----------|------|----------|--------| +| P-001 | {页面名} | {描述} | F-001 | P0 | + +### 1.3 页面导航图 + +**【必须】使用导航图展示页面跳转关系:** + +``` + ┌─────────────┐ + │ 首页 │ + │ P-001 │ + └──────┬──────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ 功能A页 │ │ 功能B页 │ │ 设置页 │ + │ P-002 │ │ P-003 │ │ P-004 │ + └──────┬──────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ 详情页 │ + │ P-005 │ + └─────────────┘ +``` + +## 2. 页面设计 + +### 2.1 P-001: {页面名称} + +**页面信息** + +| 属性 | 值 | +|------|-----| +| 页面ID | P-001 | +| 对应功能 | F-001, F-002 | +| 入口 | {从哪些页面可进入} | +| 出口 | {可跳转到哪些页面} | + +**【必须】页面布局 - ASCII 原型图** + +``` +┌────────────────────────────────────────────────────────┐ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Header │ │ +│ │ [Logo] [Nav Item] [Nav Item] [用户]│ │ +│ └─────────────────────────────────────────────────┘ │ +├────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────┐ ┌───────────────────────────┐ │ +│ │ │ │ │ │ +│ │ Sidebar │ │ Main Content │ │ +│ │ │ │ │ │ +│ │ • Menu Item 1 │ │ ┌─────────────────────┐ │ │ +│ │ • Menu Item 2 │ │ │ Card 1 │ │ │ +│ │ • Menu Item 3 │ │ │ [Title] │ │ │ +│ │ │ │ │ [Description...] │ │ │ +│ │ │ │ │ [Action Button] │ │ │ +│ │ │ │ └─────────────────────┘ │ │ +│ │ │ │ │ │ +│ │ │ │ ┌─────────────────────┐ │ │ +│ │ │ │ │ Card 2 │ │ │ +│ │ │ │ └─────────────────────┘ │ │ +│ │ │ │ │ │ +│ └──────────────────┘ └───────────────────────────┘ │ +│ │ +├────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Footer │ │ +│ └─────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────┘ +``` + +**组件清单** + +| 组件ID | 组件名称 | 类型 | 说明 | 交互 | +|--------|----------|------|------|------| +| C-001 | Header | 导航栏 | 顶部固定 | 点击 Logo 回首页 | +| C-002 | Sidebar | 侧边栏 | 左侧固定 | 点击菜单切换内容 | +| C-003 | Card | 卡片 | 内容展示 | 点击进入详情 | + +**交互说明** + +| 触发 | 动作 | 结果 | +|------|------|------| +| 点击 Card | 跳转 | 进入详情页 P-005 | +| 点击 Menu Item | 切换 | 更新 Main Content | + +**页面状态** + +| 状态 | 说明 | 展示 | +|------|------|------| +| 默认 | 正常加载完成 | 显示数据列表 | +| 加载中 | 数据请求中 | 骨架屏/Loading | +| 空状态 | 无数据 | 空状态插图+引导文案 | +| 错误 | 请求失败 | 错误提示+重试按钮 | + +**空状态原型** + +``` +┌─────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ (空图标) │ │ +│ └─────────────┘ │ +│ │ +│ 暂无数据 │ +│ │ +│ [去添加数据] │ +│ │ +└─────────────────────────────────────┘ +``` + +{重复以上结构覆盖所有页面} + +## 3. 用户流程 + +### 3.1 {流程名称} + +**【必须】使用流程图展示用户操作流程:** + +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Step 1 │ ──▶ │ Step 2 │ ──▶ │ Step 3 │ ──▶ │ 完成 │ +│ {操作} │ │ {操作} │ │ {操作} │ │ │ +│ P-001 │ │ P-002 │ │ P-003 │ │ P-004 │ +└─────────┘ └────┬────┘ └─────────┘ └─────────┘ + │ + ▼ 取消 + ┌─────────┐ + │ 返回 │ + │ P-001 │ + └─────────┘ +``` + +**流程步骤** + +| 步骤 | 页面 | 用户操作 | 系统响应 | +|------|------|----------|----------| +| 1 | P-001 | {操作} | {响应} | +| 2 | P-002 | {操作} | {响应} | + +## 4. 组件规范 + +### 4.1 基础组件 + +**Button 按钮** + +``` +主按钮: ┌──────────────┐ + │ 确认提交 │ (填充色背景) + └──────────────┘ + +次按钮: ┌──────────────┐ + │ 取消 │ (边框样式) + └──────────────┘ + +禁用态: ┌──────────────┐ + │ 不可点击 │ (灰色) + └──────────────┘ +``` + +**Input 输入框** + +``` +默认态: ┌────────────────────────┐ + │ 请输入... │ + └────────────────────────┘ + +聚焦态: ┌────────────────────────┐ + │ 输入内容 │ (高亮边框) + └────────────────────────┘ + +错误态: ┌────────────────────────┐ + │ 错误输入 │ (红色边框) + └────────────────────────┘ + ⚠ 错误提示信息 +``` + +### 4.2 业务组件 + +{项目特有的业务组件} + +## 5. 设计规范 + +### 5.1 色彩规范 + +| 用途 | 色值 | 示例 | +|------|------|------| +| 主色 | #1890FF | 主按钮、链接 | +| 成功 | #52C41A | 成功提示 | +| 警告 | #FAAD14 | 警告提示 | +| 错误 | #FF4D4F | 错误提示 | +| 文字主色 | #262626 | 标题 | +| 文字次色 | #8C8C8C | 描述 | + +### 5.2 字体规范 + +| 用途 | 字号 | 字重 | +|------|------|------| +| 大标题 | 24px | Bold | +| 标题 | 18px | Medium | +| 正文 | 14px | Regular | +| 辅助文字 | 12px | Regular | + +### 5.3 间距规范 + +| 间距 | 值 | 用途 | +|------|-----|------| +| xs | 4px | 紧凑间距 | +| sm | 8px | 小间距 | +| md | 16px | 标准间距 | +| lg | 24px | 大间距 | +| xl | 32px | 特大间距 | + +### 5.4 响应式断点 + +| 断点 | 宽度 | 布局说明 | +|------|------|----------| +| Mobile | < 768px | 单栏布局 | +| Tablet | 768px - 1024px | 双栏布局 | +| Desktop | > 1024px | 多栏布局 | +``` + +## 4. 保存文档 + +将生成的 UIDesign 保存到 `doc/UIDesign.md`。 + +如果文件已存在,覆盖原文件(历史版本通过 git 追溯)。 + +## 5. 输出摘要 + +向用户展示生成摘要: + +- UIDesign 文件路径 +- 页面数量 +- 用户流程数量 +- 建议的下一步操作(运行 `/ru` 评审) + +--- + +## 可视化输出要求 + +UIDesign 作为「界面蓝图」文档,**必须大量使用 ASCII 原型图**: + +| 章节 | 可视化形式 | 必要性 | +|------|------------|--------| +| 1.3 页面导航图 | 导航关系图(ASCII) | **必须** | +| 2.x 页面布局 | **ASCII 原型图** | **必须(每页)** | +| 2.x 空状态 | ASCII 原型图 | 建议 | +| 3.x 用户流程 | 流程图(ASCII) | **必须** | +| 4.x 组件规范 | 组件示意图 | **必须** | + +**ASCII 原型图要求**: + +- 每个页面**必须**有完整的布局原型图 +- 原型图要体现:页面结构、组件位置、内容区域 +- 使用 `┌ ┐ └ ┘ ─ │ ├ ┤ ┬ ┴ ┼` 等字符绘制边框 +- 使用 `[ ]` 表示按钮 +- 使用 `▼ ▶ ◀ ▲` 表示方向/展开 +- 关键交互点要标注 + +## 注意事项 + +- UI 设计必须覆盖 DevelopmentPlan 所有功能模块 +- **每个页面必须有 ASCII 原型图** +- 页面设计要考虑各种状态(默认、加载、空、错误) +- 交互说明要清晰具体 +- 设计规范要统一一致 +- 页面 ID 格式:P-xxx +- 组件 ID 格式:C-xxx + +## 质量检查 + +生成 UIDesign 后,自查以下项目: + +- [ ] 覆盖 DevelopmentPlan 所有功能模块 +- [ ] **页面导航图清晰展示页面关系** +- [ ] **每个页面都有 ASCII 原型图** +- [ ] **原型图展示了完整的页面结构** +- [ ] **用户流程有流程图** +- [ ] 每个页面都有状态说明 +- [ ] 组件清单完整 +- [ ] 交互说明清晰 +- [ ] 设计规范统一 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..186d357 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Dependencies +node_modules/ +__pycache__/ +*.pyc +.pyo +*.pyd +venv/ +.venv/ +env/ +.env + +# Environment files (keep examples) +.env.local +backend/.env +frontend/.env.local + +# Build outputs +.next/ +out/ +dist/ +build/ +*.egg-info/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Testing +.coverage +htmlcov/ +.pytest_cache/ +coverage/ + +# Logs +*.log +npm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# Python +*.egg +*.egg-info/ +.eggs/ +pip-log.txt +pip-delete-this-directory.txt diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6b69e40 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,487 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 交互规范 + +- 开始任务时说:**主人,开始了** +- 完成任务时说:**主人,我干完了.您看看** + +## 项目概述 + +KOL Insight 是一个 KOL(关键意见领袖)数据查询与分析工具,用于批量查询达人视频数据并计算预估成本指标。 + +**技术栈**: +- **前端**: Next.js 14.x (App Router) + React + TypeScript + Tailwind CSS +- **后端**: Python FastAPI 0.104+ + SQLAlchemy 2.0+ (异步 ORM) + asyncpg +- **数据库**: PostgreSQL 14.x+ +- **部署**: Docker + Uvicorn (ASGI 服务器) + +## 常用命令 + +### 前端开发 + +```bash +# 安装依赖 +pnpm install + +# 开发模式(热重载) +pnpm dev + +# 构建生产版本 +pnpm build + +# 生产模式运行 +pnpm start + +# 代码检查 +pnpm lint + +# 类型检查 +pnpm type-check # 如果配置了此脚本 +``` + +### 后端开发 + +```bash +# 安装依赖 +pip install -r requirements.txt +# 或使用 Poetry +poetry install + +# 开发模式运行(热重载) +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# 生产模式运行 +uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 + +# 运行测试(TDD 必须) +pytest + +# 运行测试并生成覆盖率报告 +pytest --cov=app --cov-report=html + +# 运行特定测试文件 +pytest tests/test_query_service.py + +# 运行特定测试函数 +pytest tests/test_query_service.py::test_query_by_star_id +``` + +### 数据库操作 + +```bash +# 连接数据库(使用 .env 中的连接字符串) +psql "postgresql://user:password@host:5432/yuntu_kol" + +# 创建迁移 +alembic revision --autogenerate -m "description" + +# 执行迁移 +alembic upgrade head + +# 回滚迁移 +alembic downgrade -1 +``` + +### Docker 部署 + +```bash +# 构建并启动所有服务(前端、后端、数据库) +docker-compose up -d + +# 查看日志 +docker-compose logs -f + +# 停止所有服务 +docker-compose down + +# 重新构建并启动 +docker-compose up -d --build +``` + +## 架构设计 + +### 前后端分离架构 + +``` +┌─────────────────────┐ +│ Next.js 前端 │ 端口: 3000 +│ (纯前端渲染) │ +└──────────┬──────────┘ + │ HTTP API 调用 + ▼ +┌─────────────────────┐ +│ FastAPI 后端 │ 端口: 8000 +│ (异步 API) │ +└──────────┬──────────┘ + │ asyncpg + ▼ +┌─────────────────────┐ +│ PostgreSQL │ 端口: 5432 +└─────────────────────┘ +``` + +**前后端分离的关键点**: +- 前端通过 HTTP 调用后端 API(需要配置 CORS) +- 前端环境变量: `NEXT_PUBLIC_API_URL` 指向后端地址 +- 后端环境变量: `CORS_ORIGINS` 配置允许的前端域名 +- 独立部署:前端可部署到 Vercel,后端部署到 Docker + +### 核心模块 + +1. **查询模块** (`backend/app/services/query_service.py`) + - 支持三种查询方式:星图ID(star_id)、达人ID(star_unique_id)、昵称(star_nickname) + - 星图ID 和达人ID 使用精准匹配(WHERE IN) + - 昵称使用模糊匹配(WHERE LIKE '%昵称%') + - 单次查询限制最大 1000 条 + +2. **计算模块** (`backend/app/services/calculator.py`) + - **预估自然CPM**: `(estimated_video_cost / natural_play_cnt) * 1000` + - **预估自然看后搜人数**: `(natural_play_cnt / total_play_cnt) * after_view_search_uv` + - **预估自然看后搜人数成本**: `estimated_video_cost / 预估自然看后搜人数` + - 除零检查:分母为 0 时返回 `None` + - 结果保留 2 位小数:使用 `round(value, 2)` + +3. **品牌API集成模块** (`backend/app/services/brand_api.py`) + - **批量并发调用**:从查询结果提取唯一 `brand_id`,批量调用品牌API + - **并发控制**:使用 `asyncio.Semaphore` 限制最大 10 个并发请求 + - **超时设置**:单个请求超时 3 秒 + - **降级策略**:API 调用失败时显示原始 `brand_id` + - **技术实现**:使用 `httpx.AsyncClient` + `asyncio.gather` + +4. **导出模块** (`backend/app/services/export_service.py`) + - 支持 Excel (使用 `openpyxl` 或 `xlsxwriter`) 和 CSV 格式 + - 使用中文列名作为表头 + - 限制单次导出最大 1000 条 + +### 数据流向 + +``` +用户输入查询条件 (前端) + ↓ +POST /api/v1/query (后端) + ↓ +查询数据库 (SQLAlchemy 异步) + ↓ +提取唯一 brand_id → 批量调用品牌API (httpx 异步,并发10) + ↓ +填充品牌名称 → 计算预估指标 + ↓ +返回完整数据 (JSON) + ↓ +前端展示结果表格 + ↓ +用户点击导出 → GET /api/v1/export → 下载 Excel/CSV +``` + +## 数据库设计 + +### KolVideo 模型 + +```python +class KolVideo(Base): + __tablename__ = "kol_videos" + + # 主键 + item_id = Column(String, primary_key=True) + + # 查询字段(必须建立索引) + star_id = Column(String, nullable=False, index=True) # 星图ID + star_unique_id = Column(String, nullable=False, index=True) # 达人unique_id + star_nickname = Column(String, nullable=False, index=True) # 达人昵称 + + # 基础信息 + title = Column(String, nullable=True) + viral_type = Column(String, nullable=True) + video_url = Column(String, nullable=True) + publish_time = Column(DateTime, nullable=True) + + # 曝光指标(用于计算) + natural_play_cnt = Column(Integer, default=0) # 自然播放量 + heated_play_cnt = Column(Integer, default=0) # 加热播放量 + total_play_cnt = Column(Integer, default=0) # 总播放量 + + # 互动指标 + total_interact = Column(Integer, default=0) + like_cnt = Column(Integer, default=0) + share_cnt = Column(Integer, default=0) + comment_cnt = Column(Integer, default=0) + + # 效果指标(用于计算) + new_a3_rate = Column(Float, nullable=True) + after_view_search_uv = Column(Integer, default=0) # 看后搜人数 + return_search_cnt = Column(Integer, default=0) + + # 商业信息 + industry_id = Column(String, nullable=True) + industry_name = Column(String, nullable=True) + brand_id = Column(String, nullable=True) # 用于调用品牌API + estimated_video_cost = Column(Float, default=0) # 预估视频成本(用于计算) +``` + +**索引策略**: +- `idx_star_id`: 星图ID 精准查询 +- `idx_star_unique_id`: 达人ID 精准查询 +- `idx_star_nickname`: 昵称模糊查询(需要支持 LIKE) + +## API 设计 + +### POST /api/v1/query + +批量查询 KOL 视频数据。 + +**请求体**: +```python +{ + "type": "star_id" | "unique_id" | "nickname", # 查询方式 + "values": ["id1", "id2", ...] # 批量ID或单个昵称 +} +``` + +**响应体**: +```python +{ + "success": true, + "data": [ + { + "item_id": "...", + "title": "...", + # ... 26个字段 + "brand_name": "...", # 后端填充 + "estimated_natural_cpm": 12.34, # 后端计算 + # ... + } + ], + "total": 100 +} +``` + +### GET /api/v1/export + +导出查询结果。 + +**查询参数**: +- `format`: `xlsx` 或 `csv` + +**响应**: 文件下载(`Content-Disposition: attachment`) + +## 开发规范 + +### TDD(测试驱动开发)要求 + +**这个项目强制使用 TDD,必须先写测试再写实现代码。** + +1. **单元测试覆盖率要求**: 100%(所有分支覆盖) +2. **集成测试要求**: 使用真实数据库连接(.env 中的连接字符串) +3. **测试框架**: pytest + pytest-cov +4. **测试文件位置**: `backend/tests/` + +**TDD 流程**: +``` +1. 编写测试用例 (tests/test_*.py) +2. 运行测试(应该失败) +3. 编写最小实现代码 +4. 运行测试(应该通过) +5. 重构代码(保持测试通过) +6. 生成覆盖率报告验证 100% 覆盖 +``` + +**测试示例**: +```python +# tests/test_calculator.py +def test_calculate_natural_cpm(): + # 正常情况 + result = calculate_natural_cpm(1000, 50000) + assert result == 20.0 + + # 除零情况 + result = calculate_natural_cpm(1000, 0) + assert result is None + +# tests/test_query_service.py +@pytest.mark.asyncio +async def test_query_by_star_id(db_session): + # 使用真实数据库连接 + result = await query_videos( + db_session, + query_type="star_id", + values=["test_id_1", "test_id_2"] + ) + assert len(result) > 0 +``` + +### 异步编程规范 + +**后端必须使用异步编程以提升性能。** + +1. **数据库操作**: 使用 SQLAlchemy 异步 API + ```python + from sqlalchemy.ext.asyncio import AsyncSession + + async def query_videos(session: AsyncSession, ...): + stmt = select(KolVideo).where(...) + result = await session.execute(stmt) + return result.scalars().all() + ``` + +2. **外部API调用**: 使用 httpx.AsyncClient + ```python + async with httpx.AsyncClient(timeout=3.0) as client: + response = await client.get(url) + ``` + +3. **并发控制**: 使用 asyncio.Semaphore + ```python + semaphore = asyncio.Semaphore(10) # 限制10并发 + async with semaphore: + # 执行异步任务 + ``` + +### 前端实现规范 + +**前端采用"粗略实现"策略:重点在功能可用,样式可简化。** + +1. **组件化开发**: 使用 React 组件(QueryForm, ResultTable, ExportButton) +2. **API 调用封装**: 在 `lib/api.ts` 中统一封装 +3. **环境变量**: 使用 `NEXT_PUBLIC_API_URL` 配置后端地址 +4. **状态管理**: 页面状态包括:默认态、输入态、查询中、结果态、空结果态、错误态 + +### 错误处理规范 + +1. **后端**: + - 所有 API 路由使用 try-except 包裹 + - 数据库连接失败、品牌API超时等场景有降级处理 + - 错误信息记录到日志 + +2. **前端**: + - 网络错误显示用户友好提示 + - 空结果显示引导文案 + +### 性能要求 + +- 查询响应时间 ≤ 3 秒(100 条数据) +- 页面加载时间 ≤ 2 秒 +- 导出响应时间 ≤ 5 秒(1000 条数据) +- 品牌 API 并发限制: 10 个请求,单请求超时 3 秒 + +## 环境变量配置 + +### 前端 (.env.local) + +```env +NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 +``` + +### 后端 (.env) + +```env +DATABASE_URL=postgresql://user:password@host:5432/yuntu_kol +CORS_ORIGINS=http://localhost:3000,https://your-frontend-domain.com +BRAND_API_BASE_URL=https://api.internal.intelligrow.cn +``` + +## 目录结构 + +``` +kol-insight/ +├── frontend/ # 前端项目(Next.js) +│ ├── src/ +│ │ ├── app/ # App Router 路由 +│ │ ├── components/ # React 组件 +│ │ ├── lib/ # API 调用、工具函数 +│ │ └── types/ # TypeScript 类型定义 +│ └── package.json +│ +├── backend/ # 后端项目(FastAPI) +│ ├── app/ +│ │ ├── main.py # FastAPI 应用入口 +│ │ ├── config.py # 配置管理 +│ │ ├── database.py # 数据库连接 +│ │ ├── models/ # SQLAlchemy 模型 +│ │ ├── schemas/ # Pydantic 请求/响应模型 +│ │ ├── api/v1/ # API 路由 +│ │ └── services/ # 业务逻辑 +│ ├── tests/ # 测试文件(TDD 必须) +│ └── requirements.txt +│ +├── doc/ # 项目文档 +│ ├── PRD.md # 产品需求文档 +│ ├── FeatureSummary.md # 功能摘要 +│ ├── UIDesign.md # UI 设计 +│ ├── DevelopmentPlan.md # 开发计划 +│ └── tasks.md # 任务列表 +│ +├── docker-compose.yml # Docker 编排 +└── README.md +``` + +## 关键技术决策 + +### 为什么使用 FastAPI? + +- 原生支持异步编程(async/await) +- 自动生成 OpenAPI 文档(Swagger UI) +- Pydantic 类型验证,类型安全 +- 性能优异(基于 Starlette 和 Pydantic) + +### 为什么使用前后端分离? + +- 前端可独立部署到 Vercel 等平台 +- 后端可独立扩展和优化 +- 职责分离:前端专注 UI,后端专注业务逻辑 +- 便于团队协作(前端/后端可并行开发) + +### 为什么强制 TDD + 100% 覆盖率? + +- 保证代码质量和可维护性 +- 避免回归问题 +- 文档化代码行为(测试即文档) +- 便于重构(测试作为安全网) + +### 为什么品牌API在后端调用? + +- 避免前端暴露内部API地址 +- 统一错误处理和降级逻辑 +- 利用后端异步能力优化并发性能 +- 减少前端复杂度 + +## 常见问题 + +### Q: 如何运行单个测试? +```bash +pytest tests/test_calculator.py::test_calculate_natural_cpm -v +``` + +### Q: 如何查看测试覆盖率? +```bash +pytest --cov=app --cov-report=html +# 打开 htmlcov/index.html 查看详细报告 +``` + +### Q: 前端如何调用后端API? +在 `lib/api.ts` 中封装: +```typescript +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1'; + +export async function queryVideos(request: QueryRequest): Promise { + const response = await fetch(`${API_BASE_URL}/query`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + }); + return response.json(); +} +``` + +### Q: 如何调试品牌API批量调用? +1. 检查后端日志(应该记录API调用状态) +2. 使用断点调试 `services/brand_api.py` +3. 验证并发控制和超时设置是否生效 + +### Q: 数据库索引如何验证? +```sql +-- 连接数据库后执行 +\d kol_videos +-- 应该看到 idx_star_id, idx_star_unique_id, idx_star_nickname +``` diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..98541c9 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,8 @@ +# 数据库连接 +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/yuntu_kol + +# CORS 允许的前端地址 +CORS_ORIGINS=["http://localhost:3000"] + +# 品牌 API 配置 +BRAND_API_BASE_URL=https://api.internal.intelligrow.cn diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..cd64a16 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,42 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +version_path_separator = os + +sqlalchemy.url = driver://user:pass@localhost/dbname + +[post_write_hooks] + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..d6e14d7 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,71 @@ +import asyncio +from logging.config import fileConfig + +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +from alembic import context + +from app.config import settings +from app.database import Base +from app.models import KolVideo # noqa: F401 + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def get_url(): + return settings.DATABASE_URL + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode.""" + url = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """Run migrations in 'online' mode with async engine.""" + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = get_url() + connectable = async_engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..de23107 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,28 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict +from typing import List + + +class Settings(BaseSettings): + """Application settings.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + ) + + # Database + DATABASE_URL: str = "postgresql+asyncpg://user:password@localhost:5432/yuntu_kol" + + # CORS + CORS_ORIGINS: List[str] = ["http://localhost:3000"] + + # Brand API + BRAND_API_BASE_URL: str = "https://api.internal.intelligrow.cn" + + # API Settings + MAX_QUERY_LIMIT: int = 1000 + BRAND_API_TIMEOUT: float = 3.0 + BRAND_API_CONCURRENCY: int = 10 + + +settings = Settings() diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..5971c19 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,34 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import DeclarativeBase + +from app.config import settings + +# 创建异步引擎 +engine = create_async_engine( + settings.DATABASE_URL, + echo=False, + pool_pre_ping=True, + pool_size=10, + max_overflow=20, +) + +# 创建异步会话工厂 +async_session_maker = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, +) + + +class Base(DeclarativeBase): + """Base class for all models.""" + pass + + +async def get_db() -> AsyncSession: + """Dependency to get database session.""" + async with async_session_maker() as session: + try: + yield session + finally: + await session.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..ed49c35 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.config import settings + +app = FastAPI( + title="KOL Insight API", + description="KOL 视频数据查询与分析 API", + version="1.0.0", +) + +# CORS 配置 +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/") +async def root(): + """Root endpoint.""" + return {"message": "KOL Insight API", "version": "1.0.0"} + + +@app.get("/health") +async def health(): + """Health check endpoint.""" + return {"status": "healthy"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..bb06d24 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,3 @@ +from app.models.kol_video import KolVideo + +__all__ = ["KolVideo"] diff --git a/backend/app/models/kol_video.py b/backend/app/models/kol_video.py new file mode 100644 index 0000000..8171b25 --- /dev/null +++ b/backend/app/models/kol_video.py @@ -0,0 +1,52 @@ +from sqlalchemy import Column, String, Integer, Float, DateTime, Index +from app.database import Base + + +class KolVideo(Base): + """KOL 视频数据模型.""" + + __tablename__ = "kol_videos" + + # 主键 + item_id = Column(String, primary_key=True) + + # 基础信息 + title = Column(String, nullable=True) + viral_type = Column(String, nullable=True) + video_url = Column(String, nullable=True) + star_id = Column(String, nullable=False) + star_unique_id = Column(String, nullable=False) + star_nickname = Column(String, nullable=False) + publish_time = Column(DateTime, nullable=True) + + # 曝光指标 + natural_play_cnt = Column(Integer, default=0) + heated_play_cnt = Column(Integer, default=0) + total_play_cnt = Column(Integer, default=0) + + # 互动指标 + total_interact = Column(Integer, default=0) + like_cnt = Column(Integer, default=0) + share_cnt = Column(Integer, default=0) + comment_cnt = Column(Integer, default=0) + + # 效果指标 + new_a3_rate = Column(Float, nullable=True) + after_view_search_uv = Column(Integer, default=0) + return_search_cnt = Column(Integer, default=0) + + # 商业信息 + industry_id = Column(String, nullable=True) + industry_name = Column(String, nullable=True) + brand_id = Column(String, nullable=True) + estimated_video_cost = Column(Float, default=0) + + # 索引定义 + __table_args__ = ( + Index("idx_star_id", "star_id"), + Index("idx_star_unique_id", "star_unique_id"), + Index("idx_star_nickname", "star_nickname"), + ) + + def __repr__(self): + return f"" diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..2f3d987 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +testpaths = tests +asyncio_mode = auto +asyncio_default_fixture_loop_scope = function +addopts = -v --cov=app --cov-report=html --cov-report=term-missing diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..754081e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,27 @@ +# Web Framework +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 + +# Database +sqlalchemy>=2.0.0 +asyncpg>=0.29.0 +alembic>=1.12.0 + +# HTTP Client +httpx>=0.25.0 + +# Data Validation +pydantic>=2.0.0 +pydantic-settings>=2.0.0 + +# Excel Export +openpyxl>=3.1.0 + +# Testing +pytest>=7.4.0 +pytest-asyncio>=0.21.0 +pytest-cov>=4.1.0 +httpx>=0.25.0 + +# Development +python-dotenv>=1.0.0 diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..f589102 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,58 @@ +import pytest +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker + +from app.database import Base +from app.models import KolVideo + + +@pytest.fixture +def sample_video_data(): + """Sample video data for testing.""" + return { + "item_id": "test_item_001", + "title": "测试视频标题", + "viral_type": "爆款", + "video_url": "https://example.com/video/001", + "star_id": "star_001", + "star_unique_id": "unique_001", + "star_nickname": "测试达人", + "natural_play_cnt": 100000, + "heated_play_cnt": 50000, + "total_play_cnt": 150000, + "total_interact": 5000, + "like_cnt": 3000, + "share_cnt": 1000, + "comment_cnt": 1000, + "new_a3_rate": 0.05, + "after_view_search_uv": 500, + "return_search_cnt": 200, + "industry_id": "ind_001", + "industry_name": "美妆", + "brand_id": "brand_001", + "estimated_video_cost": 10000.0, + } + + +@pytest.fixture +async def test_engine(): + """Create a test database engine using SQLite.""" + engine = create_async_engine( + "sqlite+aiosqlite:///:memory:", + echo=False, + ) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield engine + await engine.dispose() + + +@pytest.fixture +async def test_session(test_engine): + """Create a test database session.""" + async_session = async_sessionmaker( + test_engine, + class_=AsyncSession, + expire_on_commit=False, + ) + async with async_session() as session: + yield session diff --git a/backend/tests/test_database.py b/backend/tests/test_database.py new file mode 100644 index 0000000..877f12a --- /dev/null +++ b/backend/tests/test_database.py @@ -0,0 +1,165 @@ +import pytest +from sqlalchemy import select + +from app.models import KolVideo + + +class TestKolVideoModel: + """Tests for KolVideo model.""" + + async def test_create_video(self, test_session, sample_video_data): + """Test creating a video record.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + saved_video = result.scalar_one() + + assert saved_video.item_id == sample_video_data["item_id"] + assert saved_video.title == sample_video_data["title"] + assert saved_video.star_id == sample_video_data["star_id"] + + async def test_query_by_star_id(self, test_session, sample_video_data): + """Test querying videos by star_id.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.star_id == sample_video_data["star_id"]) + ) + videos = result.scalars().all() + + assert len(videos) == 1 + assert videos[0].star_id == sample_video_data["star_id"] + + async def test_query_by_star_unique_id(self, test_session, sample_video_data): + """Test querying videos by star_unique_id.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where( + KolVideo.star_unique_id == sample_video_data["star_unique_id"] + ) + ) + videos = result.scalars().all() + + assert len(videos) == 1 + assert videos[0].star_unique_id == sample_video_data["star_unique_id"] + + async def test_query_by_nickname_like(self, test_session, sample_video_data): + """Test querying videos by nickname using LIKE.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.star_nickname.like("%测试%")) + ) + videos = result.scalars().all() + + assert len(videos) == 1 + assert "测试" in videos[0].star_nickname + + async def test_batch_query_by_star_ids(self, test_session, sample_video_data): + """Test batch querying videos by multiple star_ids.""" + video1 = KolVideo(**sample_video_data) + video2_data = sample_video_data.copy() + video2_data["item_id"] = "test_item_002" + video2_data["star_id"] = "star_002" + video2 = KolVideo(**video2_data) + + test_session.add_all([video1, video2]) + await test_session.commit() + + star_ids = ["star_001", "star_002"] + result = await test_session.execute( + select(KolVideo).where(KolVideo.star_id.in_(star_ids)) + ) + videos = result.scalars().all() + + assert len(videos) == 2 + + async def test_video_default_values(self, test_session): + """Test that default values are set correctly.""" + video = KolVideo( + item_id="test_defaults", + star_id="star_test", + star_unique_id="unique_test", + star_nickname="测试默认值", + ) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == "test_defaults") + ) + saved_video = result.scalar_one() + + assert saved_video.natural_play_cnt == 0 + assert saved_video.heated_play_cnt == 0 + assert saved_video.total_play_cnt == 0 + assert saved_video.estimated_video_cost == 0 + + async def test_video_nullable_fields(self, test_session): + """Test that nullable fields can be None.""" + video = KolVideo( + item_id="test_nullable", + star_id="star_nullable", + star_unique_id="unique_nullable", + star_nickname="测试可空字段", + ) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == "test_nullable") + ) + saved_video = result.scalar_one() + + assert saved_video.title is None + assert saved_video.video_url is None + assert saved_video.brand_id is None + assert saved_video.new_a3_rate is None + + async def test_update_video(self, test_session, sample_video_data): + """Test updating a video record.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + saved_video = result.scalar_one() + saved_video.title = "更新后的标题" + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + updated_video = result.scalar_one() + assert updated_video.title == "更新后的标题" + + async def test_delete_video(self, test_session, sample_video_data): + """Test deleting a video record.""" + video = KolVideo(**sample_video_data) + test_session.add(video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + saved_video = result.scalar_one() + await test_session.delete(saved_video) + await test_session.commit() + + result = await test_session.execute( + select(KolVideo).where(KolVideo.item_id == sample_video_data["item_id"]) + ) + assert result.scalar_one_or_none() is None diff --git a/doc/DevelopmentPlan.md b/doc/DevelopmentPlan.md new file mode 100644 index 0000000..9134602 --- /dev/null +++ b/doc/DevelopmentPlan.md @@ -0,0 +1,1014 @@ +# KOL Insight - 开发计划 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2025-01-28 | +| 来源文档 | FeatureSummary.md | + +## 1. 项目概述 + +### 1.1 项目目标 + +| 目标 | 指标 | 衡量方式 | +|------|------|----------| +| 提升查询效率 | 单次可批量查询多个 KOL | 对比手动查询耗时 | +| 降低计算错误 | 自动计算预估指标准确率 100% | 人工抽检验证 | +| 提高数据可用性 | 支持数据导出 | 导出功能完整性 | + +### 1.2 技术栈 + + +| 层级 | 技术选型 | 版本 | 说明 | +|------|----------|------|------| +| 前端框架 | Next.js (App Router) | 14.x | React 框架,纯前端渲染 | +| UI 框架 | React + Tailwind CSS | - | 组件化开发,响应式设计 | +| 后端框架 | Python FastAPI | 0.104+ | 高性能异步 Web 框架 | +| 数据库 | PostgreSQL | 14.x+ | 关系型数据库,存储 KOL 视频数据 | +| Python ORM | SQLAlchemy + asyncpg | 2.0+ | 异步 ORM,类型安全的数据库访问 | +| API 文档 | FastAPI 自动生成 | - | Swagger UI + ReDoc | +| 前端部署 | Docker / Vercel | - | 容器化或 Serverless | +| 后端部署 | Docker + Uvicorn | - | ASGI 服务器 | +| 包管理(前端) | pnpm | 8.x | 高效的包管理器 | +| 包管理(后端) | Poetry / pip | - | Python 依赖管理 | + +### 1.3 开发原则 + +- **简单优先**: 优先选择简单直接的实现方案 +- **类型安全**: 前端使用 TypeScript,后端使用 Pydantic 类型验证 +- **组件化**: UI 组件化开发,便于复用和维护 +- **API 设计**: RESTful 风格,清晰的接口契约 +- **错误处理**: 完善的错误处理和用户提示 +- **安全防护**: SQL 注入防护,CORS 配置,环境变量管理敏感配置 + +- **前后端分离**: 前端专注 UI 展示,后端专注业务逻辑和数据处理 +- **异步优先**: 后端使用异步编程,提升并发性能 + + +## 2. 技术架构 + +### 2.1 系统架构图 + + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 客户端层 │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Web Browser │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ 查询页面 │ │ 结果展示 │ │ 导出操作 │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + │ HTTP/HTTPS (API 调用) + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Next.js 前端应用层 │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ App Router (纯前端) │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ React 组件 │ │ │ +│ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ +│ │ │ │ QueryForm │ │ ResultTable │ │ ExportButton │ │ │ │ +│ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + │ HTTP API 调用 (跨域) + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ FastAPI 后端应用层 │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ API Router │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ RESTful API │ │ │ +│ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ +│ │ │ │ POST /api/v1/ │ │ GET /api/v1/ │ │ │ │ +│ │ │ │ query │ │ export │ │ │ │ +│ │ │ └─────────────────┘ └─────────────────┘ │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 业务逻辑层 (Python) │ │ │ +│ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ +│ │ │ │ 查询服务 │ │ 计算服务 │ │ 导出服务 │ │ │ │ +│ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │ +│ │ │ ┌───────────┐ ┌───────────┐ │ │ │ +│ │ │ │ 品牌API │ │ 数据库层 │ │ │ │ +│ │ │ │ 集成服务 │ │ (SQLAlchemy)│ │ │ │ +│ │ │ └───────────┘ └───────────┘ │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ │ + │ SQL (asyncpg) │ HTTP + ▼ ▼ +┌─────────────────────────────┐ ┌─────────────────────────────┐ +│ 数据层 │ │ 外部服务 │ +│ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ +│ │ PostgreSQL │ │ │ │ 品牌 API │ │ +│ │ ┌─────────────────┐ │ │ │ │ /v1/yuntu/brands/ │ │ +│ │ │ kol_videos │ │ │ │ └───────────────────────┘ │ +│ │ │ (视频数据表) │ │ │ └─────────────────────────────┘ +│ │ └─────────────────┘ │ │ +│ └───────────────────────┘ │ +└─────────────────────────────┘ +``` + +### 2.2 模块依赖图 + + +``` +┌────────────────────────────────────────────────────────────────┐ +│ 前端模块 (Next.js) │ +│ ┌──────────────┐ │ +│ │ 页面组件 │ │ +│ │ (Page) │ │ +│ └──────┬───────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 查询表单 │ ──▶ │ 结果表格 │ ──▶ │ 导出按钮 │ │ +│ │ 组件 │ │ 组件 │ │ 组件 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└────────────────────────────────────────────────────────────────┘ + │ │ │ + │ HTTP API │ 数据展示 │ HTTP API + ▼ ▼ ▼ +┌────────────────────────────────────────────────────────────────┐ +│ 后端模块 (FastAPI) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 查询 API │ ──▶ │ 计算服务 │ │ 导出 API │ │ +│ │ POST /api/v1/│ │ (Python) │ │ GET /api/v1/ │ │ +│ │ query │ │ │ │ export │ │ +│ └──────┬───────┘ └──────────────┘ └──────┬───────┘ │ +│ │ ▲ │ │ +│ ▼ │ │ │ +│ ┌──────────────┐ │ │ │ +│ │ 品牌API服务 │────────────┘ │ │ +│ │ (F-010) │ │ │ +│ │ (httpx异步) │ │ │ +│ └──────┬───────┘ │ │ +│ │ │ │ +│ └─────────────────┬───────────────────────┘ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ 数据库服务 │ │ +│ │ (SQLAlchemy) │ │ +│ └──────┬───────┘ │ +└────────────────────────────┼───────────────────────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ PostgreSQL │ │ 品牌 API │ + │ │ │ (外部服务) │ + └──────────────┘ └──────────────┘ +``` + +### 2.3 数据流图 + + +``` +用户操作 前端 后端 数据库/外部API + │ │ │ │ + │ 1. 输入查询条件 │ │ │ + │ ─────────────────────▶ │ │ │ + │ │ │ │ + │ │ 2. POST /api/query │ │ + │ │ ─────────────────────▶ │ │ + │ │ │ │ + │ │ │ 3. SELECT FROM kol_videos + │ │ │ ─────────────────────────▶│ + │ │ │ │ + │ │ │ 4. 返回视频数据 │ + │ │ │ ◀─────────────────────────│ + │ │ │ │ + │ │ │ 5. 提取唯一 brand_id │ + │ │ │ (去重处理) │ + │ │ │ │ + │ │ │ 6. 批量 GET brands │ + │ │ │ (并发10,超时3s) │ + │ │ │ ─────────────────────────▶│ + │ │ │ │ + │ │ │ 7. 返回品牌名称映射 │ + │ │ │ ◀─────────────────────────│ + │ │ │ │ + │ │ │ 8. 填充品牌名称 │ + │ │ │ (失败则降级显示ID) │ + │ │ │ │ + │ │ │ 9. 计算预估指标 │ + │ │ │ (CPM/看后搜人数/成本) │ + │ │ │ │ + │ │ 10. 返回完整数据 │ │ + │ │ ◀───────────────────── │ │ + │ │ │ │ + │ 11. 展示结果列表 │ │ │ + │ ◀───────────────────── │ │ │ + │ │ │ │ + │ 12. 点击导出 │ │ │ + │ ─────────────────────▶ │ │ │ + │ │ 13. GET /api/export │ │ + │ │ ─────────────────────▶ │ │ + │ │ │ │ + │ │ 14. 返回 Excel/CSV │ │ + │ │ ◀───────────────────── │ │ + │ │ │ │ + │ 15. 下载文件 │ │ │ + │ ◀───────────────────── │ │ │ +``` + +## 3. 开发阶段 + +### 3.1 阶段时间线 + +``` + Phase 1 Phase 2 Phase 3 + │ │ │ + 基础架构搭建 核心功能开发 优化与测试 + │ │ │ + ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌─────────┐ + │ 项目初始 │ ──────▶ │ 功能实现 │ ──────▶ │ 优化部署 │ + │ 化配置 │ │ 与集成 │ │ 与测试 │ + └─────────┘ └─────────┘ └─────────┘ + + 交付物: 交付物: 交付物: + • 项目骨架 • 查询功能 • 性能优化 + • 数据库连接 • 计算逻辑 • 错误处理 + • 基础 UI • 导出功能 • 部署配置 +``` + +### 3.2 Phase 1: 基础架构搭建 + +**目标**: 完成项目初始化和基础设施配置 + + +| 任务ID | 任务 | 描述 | 依赖 | 优先级 | 关联功能 | +|--------|------|------|------|--------|----------| +| T-001 | 前端项目初始化 | 创建 Next.js 项目,配置 TypeScript、ESLint、Prettier | - | P0 | - | +| T-002 | 后端项目初始化 | 创建 FastAPI 项目,配置 Poetry/pip、项目结构 | - | P0 | - | +| T-003 | 数据库配置 | 配置 SQLAlchemy + asyncpg,定义数据模型,连接 PostgreSQL | T-002 | P0 | F-001~003 | +| T-004 | 基础 UI 框架 | 安装 Tailwind CSS,创建基础布局组件 | T-001 | P0 | F-007 | +| T-005 | 环境变量配置 | 配置前后端环境变量,数据库连接字符串,CORS 配置 | T-001, T-002 | P0 | - | + +**阶段依赖图:** + + +``` +T-001 (前端初始化) T-002 (后端初始化) + │ │ + ▼ ▼ +T-004 (UI框架) T-003 (数据库) + │ │ + └──────────┬───────────┘ + ▼ + T-005 (环境变量) +``` + +--- + +### 3.3 Phase 2: 核心功能开发 + +**目标**: 实现所有核心功能(查询、计算、展示、导出) + + +| 任务ID | 任务 | 描述 | 依赖 | 优先级 | 关联功能 | +|--------|------|------|------|--------|----------| +| T-006 | 查询 API 开发 (后端) | FastAPI 实现 POST /api/v1/query 接口,支持三种查询方式 | T-003 | P0 | F-001, F-002, F-003 | +| T-007 | 计算逻辑实现 (后端) | Python 实现 CPM、看后搜人数、成本计算 | T-006 | P0 | F-004, F-005, F-006 | +| T-008 | 品牌 API 批量集成 (后端) | 后端使用 httpx 批量异步调用品牌API,支持并发控制和降级 | T-006 | P0 | F-010 | +| T-009 | 导出 API 开发 (后端) | FastAPI 实现 GET /api/v1/export 接口,生成 Excel/CSV | T-007, T-008 | P1 | F-009 | +| T-010 | 查询表单组件 (前端) | React 组件开发,调用后端查询API | T-004 | P0 | F-001, F-002, F-003 | +| T-011 | 结果表格组件 (前端) | React 组件开发,显示26个字段,数据来自后端API | T-004, T-007, T-008 | P1 | F-007 | +| T-012 | 导出按钮组件 (前端) | React 组件开发,调用后端导出API触发下载 | T-011, T-009 | P1 | F-009 | + +**阶段依赖图:** + + +``` +后端任务: +T-006 (查询API) ──────▶ T-007 (计算逻辑) ──────▶ T-009 (导出API) + │ │ │ + └──▶ T-008 (品牌API) │ │ + │ │ +前端任务: │ │ +T-010 (查询表单) ─────────────┼───────────────────────┤ + ▼ ▼ + T-011 (结果表格) ──────▶ T-012 (导出按钮) +``` + +--- + +### 3.4 Phase 3: 优化与测试 + +**目标**: 性能优化、错误处理、部署配置 + + +| 任务ID | 任务 | 描述 | 依赖 | 优先级 | 关联功能 | +|--------|------|------|------|--------|----------| +| T-013 | 错误处理 (前后端) | 完善前后端错误处理,添加用户友好提示 | T-012 | P1 | 全部 | +| T-014 | 性能优化 (后端) | 数据库索引优化,异步查询性能调优 | T-012 | P1 | F-001~003 | +| T-015 | 视频链接跳转 (前端) | 实现视频链接点击跳转功能 | T-011 | P2 | F-008 | +| T-016 | 部署配置 (前后端) | Docker 配置,前后端分离部署,CORS 配置 | T-013 | P1 | - | +| T-017 | 集成测试 | 端到端功能测试,前后端联调 | T-013 | P1 | 全部 | + +**阶段依赖图:** + + +``` +T-012 (导出按钮) + │ + ├──────────┬──────────┐ + ▼ ▼ ▼ +T-013 T-014 T-015 +(错误处理) (性能优化) (链接跳转) + │ + ├──────────┐ + ▼ ▼ +T-016 T-017 +(部署) (测试) +``` + +## 4. 技术方案 + +### 4.1 数据查询模块 + +**功能**: 支持三种查询方式(星图ID/达人ID/昵称)批量查询 KOL 视频数据 + +**技术选型**: + + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| ORM | SQLAlchemy 2.0+ | 异步 ORM,类型安全,成熟稳定 | +| 异步驱动 | asyncpg | PostgreSQL 异步驱动,高性能 | +| 查询优化 | 数据库索引 | 在 star_id, star_unique_id, star_nickname 字段建立索引 | +| 输入验证 | Pydantic | FastAPI 内置,类型安全的请求参数验证 | + +**架构设计**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 查询模块 │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 请求解析 │ ──▶ │ 参数验证 │ ──▶ │ 查询构建 │ │ +│ │ (type/values)│ │ (Zod) │ │ (Prisma) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ 数据库查询 │ │ +│ │ PostgreSQL │ │ +│ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**接口设计**: + + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 批量查询 | POST | /api/v1/query | 支持 star_id/unique_id/nickname 三种类型 | + +**请求/响应格式**: + + +```python +# 请求模型 +from pydantic import BaseModel +from typing import List, Literal + +class QueryRequest(BaseModel): + type: Literal['star_id', 'unique_id', 'nickname'] + values: List[str] # 批量ID 或单个昵称 + +# 响应模型 +class QueryResponse(BaseModel): + success: bool + data: List[VideoData] + total: int + error: str | None = None +``` + +**实现要点**: + + +- 使用 SQLAlchemy 的 `select()` 配合 `where()` 条件 +- 星图ID/达人ID 使用 `in_()` 查询批量匹配 +- 昵称使用 `like()` 进行模糊匹配(使用 `%{value}%`) +- 限制单次查询最大返回 1000 条 +- SQL 注入防护由 SQLAlchemy 和 Pydantic 自动处理 +- 使用异步查询 `session.execute(stmt)` 提升性能 + +--- + +### 4.2 数据计算模块 + +**功能**: 计算预估自然 CPM、看后搜人数、看后搜成本 + +**技术选型**: + + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| 计算逻辑 | Python | 类型安全(Type Hints),易于维护 | +| 数值处理 | Python 原生 | 简单计算,无需额外库,性能优异 | + +**架构设计**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 计算模块 │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ │ +│ │ 查询结果 │ │ +│ └──────┬──────┘ │ +│ │ │ +│ ┌────────────┼────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ +│ │ CPM 计算 │ │看后搜人数 │ │ 成本计算 │ │ +│ │ F-004 │ │ F-005 │ │ F-006 │ │ +│ └───────────┘ └─────┬─────┘ └─────┬─────┘ │ +│ │ │ │ +│ │ 依赖 │ │ +│ └─────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ 除零检查 │ │ +│ │ natural_play_cnt = 0 → null │ │ +│ │ total_play_cnt = 0 → null │ │ +│ │ 看后搜人数 = 0 → null │ │ +│ └─────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**计算公式**: + + +```python +# 预估自然CPM (F-004) +def calculate_natural_cpm(estimated_video_cost: float, natural_play_cnt: int) -> float | None: + if natural_play_cnt > 0: + return round((estimated_video_cost / natural_play_cnt) * 1000, 2) + return None + +# 预估自然看后搜人数 (F-005) +def calculate_natural_search_uv( + natural_play_cnt: int, + total_play_cnt: int, + after_view_search_uv: int +) -> float | None: + if total_play_cnt > 0: + return round((natural_play_cnt / total_play_cnt) * after_view_search_uv, 2) + return None + +# 预估自然看后搜人数成本 (F-006) +def calculate_natural_search_cost( + estimated_video_cost: float, + estimated_natural_search_uv: float | None +) -> float | None: + if estimated_natural_search_uv and estimated_natural_search_uv > 0: + return round(estimated_video_cost / estimated_natural_search_uv, 2) + return None +``` + +**实现要点**: + + +- 结果保留2位小数:`round(value, 2)` +- 除零检查:分母为0时返回 None +- None 值在前端显示为 "-" +- 批量计算时使用列表推导式或 map 函数 +- 使用 Python 3.10+ 的 `float | None` 类型注解 + +--- + +### 4.3 数据展示模块 + +**功能**: 以表格形式展示查询结果,支持视频链接跳转 + +**技术选型**: + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| 表格组件 | HTML Table + Tailwind | 简单直接,样式灵活 | +| 数据格式化 | Intl API | 数字格式化、日期格式化 | + +**架构设计**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 展示模块 │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 结果表格组件 │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ 表头 │ │ 数据行 │ │ 链接列 │ │ 分页 │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 空状态组件 │ │ +│ │ "未找到匹配数据" │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**实现要点**: + +- 26个字段使用中文列名 +- 视频链接使用 `` +- 数字字段格式化:千分位分隔 +- 空值显示为 "-" +- 支持横向滚动(表格宽度超出时) + +--- + +### 4.4 数据导出模块 + +**功能**: 将查询结果导出为 Excel 或 CSV 文件 + +**技术选型**: + +| 组件 | 技术 | 选型理由 | +|------|------|----------| +| Excel 生成 | xlsx (SheetJS) | 成熟稳定,支持 .xlsx 格式 | +| CSV 生成 | 原生实现 | 简单格式,无需额外库 | + +**接口设计**: + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 数据导出 | GET | /api/export | 参数:format=xlsx/csv,data=查询条件 | + +**实现要点**: + +- 使用中文列名作为表头 +- Excel 格式使用 xlsx 库生成 +- CSV 格式需处理逗号转义 +- 文件名格式:`kol_data_${timestamp}.xlsx` +- 响应头设置 `Content-Disposition: attachment` +- 限制单次导出最大 1000 条 + +--- + + +### 4.5 品牌 API 批量集成 + +**功能**: 后端批量调用品牌API获取品牌名称(关联 F-010) + +**接口详情**: + +| 项目 | 内容 | +|------|------| +| 地址 | https://api.internal.intelligrow.cn/v1/yuntu/brands/{brand_id} | +| 方法 | GET | +| 响应 | 品牌名称 | + +**批量调用策略**: + +``` +查询结果 (N条数据) + │ + ▼ +┌─────────────────────────────────────┐ +│ 1. 提取唯一 brand_id │ +│ - 过滤空值 │ +│ - 去重处理 │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 2. 批量并发请求 │ +│ - 并发限制: 10 个请求 │ +│ - 单请求超时: 3 秒 │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 3. 构建映射表 │ +│ Map │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 4. 填充查询结果 │ +│ - 成功: 显示品牌名称 │ +│ - 失败: 降级显示 brand_id │ +└─────────────────────────────────────┘ +``` + +**实现要点**: + + +- **在后端调用**: 查询API获取数据库结果后,立即调用品牌API +- **去重处理**: 提取查询结果中所有唯一的 brand_id,避免重复请求 +- **并发控制**: 使用 asyncio.gather 或 asyncio.Semaphore 限制最大 10 个并发请求 +- **超时设置**: 单个请求超时 3 秒,避免阻塞整体响应 +- **降级策略**: API 调用失败时,显示原始 brand_id +- **结果合并**: 将品牌名称填充到查询结果后返回前端 + + +```python +import asyncio +import httpx +from typing import Dict, List + +# 品牌名称批量获取 +async def get_brand_names(brand_ids: List[str]) -> Dict[str, str]: + unique_ids = list(set(filter(None, brand_ids))) + brand_map: Dict[str, str] = {} + + # 并发控制:限制 10 个并发 + CONCURRENCY_LIMIT = 10 + TIMEOUT_SECONDS = 3.0 + + async with httpx.AsyncClient(timeout=TIMEOUT_SECONDS) as client: + # 使用信号量控制并发数 + semaphore = asyncio.Semaphore(CONCURRENCY_LIMIT) + + async def fetch_brand(brand_id: str) -> tuple[str, str]: + async with semaphore: + try: + response = await client.get( + f"https://api.internal.intelligrow.cn/v1/yuntu/brands/{brand_id}" + ) + if response.status_code == 200: + data = response.json() + return brand_id, data.get("name", brand_id) + except Exception: + pass # 降级处理 + return brand_id, brand_id # 失败时返回原ID + + # 批量并发请求 + results = await asyncio.gather( + *[fetch_brand(brand_id) for brand_id in unique_ids], + return_exceptions=True + ) + + # 构建映射表 + for result in results: + if isinstance(result, tuple): + brand_id, brand_name = result + brand_map[brand_id] = brand_name + + return brand_map + +# 在查询 API 中使用 +async def handle_query(request: QueryRequest) -> QueryResponse: + # 1. 查询数据库 + videos = await query_videos(request) + + # 2. 提取品牌ID并批量获取品牌名称 + brand_ids = [v.brand_id for v in videos if v.brand_id] + brand_map = await get_brand_names(brand_ids) + + # 3. 填充品牌名称 + for video in videos: + if video.brand_id: + video.brand_name = brand_map.get(video.brand_id, video.brand_id) + else: + video.brand_name = None + + # 4. 计算预估指标并返回 + return calculate_and_format(videos) +``` + +## 5. 风险管理 + +| 风险 | 可能性 | 影响 | 应对措施 | 负责人 | +|------|--------|------|----------|--------| +| 数据库连接不稳定 | 低 | 高 | 使用 Prisma 连接池,实现重试机制 | 后端开发 | +| 大批量查询性能问题 | 中 | 中 | 限制单次查询上限,优化数据库索引 | 后端开发 | + +| 品牌 API 不可用/超时 | 中 | 中 | 并发限制(10)、单请求超时(3s)、降级显示 brand_id | 后端开发 | +| 导出数据量过大 | 中 | 中 | 限制单次导出 1000 条,分批导出提示 | 后端开发 | +| 数据同步延迟 | 中 | 中 | 显示数据更新时间,建立同步监控 | 运维 | + +## 6. 里程碑 + +``` +M1 M2 M3 M4 +│ │ │ │ +▼ ▼ ▼ ▼ +◆───────────────◆───────────────◆───────────────◆ +│ │ │ │ +基础架构 核心功能 功能完善 正式上线 +搭建完成 开发完成 测试完成 部署完成 +``` + +| 里程碑 | 目标 | 交付物 | 验收标准 | +|--------|------|--------|----------| +| M1 | 基础架构搭建完成 | 项目骨架、数据库连接、基础UI | 项目可运行,数据库可连接 | +| M2 | 核心功能开发完成 | 查询、计算、展示、导出功能 | 所有 P0/P1 功能可用 | +| M3 | 功能完善测试完成 | 错误处理、性能优化、链接跳转 | 测试通过,性能达标 | +| M4 | 正式上线部署完成 | Docker/PM2 部署配置 | 生产环境可访问 | + +## 7. 资源需求 + + +| 角色 | 人数 | 职责 | 参与阶段 | +|------|------|------|----------| +| 前端开发 | 1 | Next.js 开发、UI 组件、API 调用 | Phase 1-3 | +| 后端开发 | 1 | FastAPI 开发、API 设计、数据库操作 | Phase 1-3 | +| 运维/DevOps | 0.5 | 前后端分离部署、CORS 配置、监控告警 | Phase 3 | + +**注**: 也可由全栈开发承担前后端工作 + +## 8. 目录结构 + + +``` +kol-insight/ +├── frontend/ # 前端项目(Next.js) +│ ├── src/ +│ │ ├── app/ +│ │ │ ├── page.tsx # 首页(查询页面) +│ │ │ ├── layout.tsx # 根布局 +│ │ │ └── globals.css # 全局样式 +│ │ ├── components/ +│ │ │ ├── QueryForm.tsx # 查询表单组件 +│ │ │ ├── ResultTable.tsx # 结果表格组件 +│ │ │ └── ExportButton.tsx # 导出按钮组件 +│ │ ├── lib/ +│ │ │ ├── api.ts # API 调用封装 +│ │ │ └── utils.ts # 工具函数 +│ │ └── types/ +│ │ └── index.ts # 类型定义 +│ ├── .env.local # 环境变量(后端API地址) +│ ├── .env.example # 环境变量示例 +│ ├── package.json +│ ├── tsconfig.json +│ ├── tailwind.config.ts +│ └── next.config.js +│ +├── backend/ # 后端项目(FastAPI) +│ ├── app/ +│ │ ├── main.py # FastAPI 应用入口 +│ │ ├── config.py # 配置管理 +│ │ ├── database.py # 数据库连接 +│ │ ├── models/ +│ │ │ └── kol_video.py # SQLAlchemy 模型 +│ │ ├── schemas/ +│ │ │ ├── query.py # Pydantic 请求/响应模型 +│ │ │ └── video.py # 视频数据模型 +│ │ ├── api/ +│ │ │ ├── v1/ +│ │ │ │ ├── __init__.py +│ │ │ │ ├── query.py # 查询接口 +│ │ │ │ └── export.py # 导出接口 +│ │ │ └── deps.py # 依赖注入 +│ │ ├── services/ +│ │ │ ├── query_service.py # 查询业务逻辑 +│ │ │ ├── calculator.py # 计算逻辑 +│ │ │ ├── brand_api.py # 品牌 API 集成 +│ │ │ └── export_service.py # 导出服务 +│ │ └── core/ +│ │ ├── security.py # 安全相关 +│ │ └── logger.py # 日志配置 +│ ├── alembic/ # 数据库迁移 +│ │ └── versions/ +│ ├── tests/ # 测试 +│ ├── .env # 环境变量 +│ ├── .env.example # 环境变量示例 +│ ├── requirements.txt # 依赖列表(或 pyproject.toml) +│ └── alembic.ini # Alembic 配置 +│ +├── docker-compose.yml # Docker 编排(可选) +└── README.md # 项目文档 +``` + +## 9. 数据库 Schema + + +```python +# backend/app/models/kol_video.py +from sqlalchemy import Column, String, Integer, Float, DateTime, Index +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class KolVideo(Base): + __tablename__ = "kol_videos" + + # 主键 + item_id = Column(String, primary_key=True) + + # 基础信息 + title = Column(String, nullable=True) + viral_type = Column(String, nullable=True) + video_url = Column(String, nullable=True) + star_id = Column(String, nullable=False, index=True) + star_unique_id = Column(String, nullable=False, index=True) + star_nickname = Column(String, nullable=False, index=True) + publish_time = Column(DateTime, nullable=True) + + # 曝光指标 + natural_play_cnt = Column(Integer, default=0) + heated_play_cnt = Column(Integer, default=0) + total_play_cnt = Column(Integer, default=0) + + # 互动指标 + total_interact = Column(Integer, default=0) + like_cnt = Column(Integer, default=0) + share_cnt = Column(Integer, default=0) + comment_cnt = Column(Integer, default=0) + + # 效果指标 + new_a3_rate = Column(Float, nullable=True) + after_view_search_uv = Column(Integer, default=0) + return_search_cnt = Column(Integer, default=0) + + # 商业信息 + industry_id = Column(String, nullable=True) + industry_name = Column(String, nullable=True) + brand_id = Column(String, nullable=True) + estimated_video_cost = Column(Float, default=0) + + # 索引定义(补充) + __table_args__ = ( + Index('idx_star_id', 'star_id'), + Index('idx_star_unique_id', 'star_unique_id'), + Index('idx_star_nickname', 'star_nickname'), + ) +``` + +**对应的 Alembic 迁移 SQL**: + +```sql +-- 创建表和索引 +CREATE TABLE kol_videos ( + item_id VARCHAR PRIMARY KEY, + title VARCHAR, + viral_type VARCHAR, + video_url VARCHAR, + star_id VARCHAR NOT NULL, + star_unique_id VARCHAR NOT NULL, + star_nickname VARCHAR NOT NULL, + publish_time TIMESTAMP, + natural_play_cnt INTEGER DEFAULT 0, + heated_play_cnt INTEGER DEFAULT 0, + total_play_cnt INTEGER DEFAULT 0, + total_interact INTEGER DEFAULT 0, + like_cnt INTEGER DEFAULT 0, + share_cnt INTEGER DEFAULT 0, + comment_cnt INTEGER DEFAULT 0, + new_a3_rate FLOAT, + after_view_search_uv INTEGER DEFAULT 0, + return_search_cnt INTEGER DEFAULT 0, + industry_id VARCHAR, + industry_name VARCHAR, + brand_id VARCHAR, + estimated_video_cost FLOAT DEFAULT 0 +); + +CREATE INDEX idx_star_id ON kol_videos(star_id); +CREATE INDEX idx_star_unique_id ON kol_videos(star_unique_id); +CREATE INDEX idx_star_nickname ON kol_videos(star_nickname); +``` + + +## 10. 前后端分离部署 + +### 10.1 部署架构 + +``` +┌─────────────────────────────────────────────────┐ +│ 前端部署 (Next.js) │ +│ - Vercel / Docker + Nginx │ +│ - 端口: 3000 │ +│ - 环境变量: NEXT_PUBLIC_API_URL │ +└─────────────────────────────────────────────────┘ + │ + │ HTTP API 调用 + ▼ +┌─────────────────────────────────────────────────┐ +│ 后端部署 (FastAPI) │ +│ - Docker + Uvicorn │ +│ - 端口: 8000 │ +│ - 环境变量: DATABASE_URL, CORS_ORIGINS │ +└─────────────────────────────────────────────────┘ + │ + │ PostgreSQL + ▼ +┌─────────────────────────────────────────────────┐ +│ 数据库 (PostgreSQL) │ +│ - 端口: 5432 │ +└─────────────────────────────────────────────────┘ +``` + +### 10.2 Docker Compose 配置示例 + +```yaml +version: '3.8' + +services: + # 后端服务 + backend: + build: ./backend + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql://user:password@db:5432/kol_insight + - CORS_ORIGINS=http://localhost:3000,https://your-frontend-domain.com + depends_on: + - db + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + + # 前端服务 + frontend: + build: ./frontend + ports: + - "3000:3000" + environment: + - NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 + depends_on: + - backend + + # 数据库服务 + db: + image: postgres:14 + environment: + - POSTGRES_USER=user + - POSTGRES_PASSWORD=password + - POSTGRES_DB=kol_insight + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + +volumes: + postgres_data: +``` + +### 10.3 CORS 配置(后端) + +```python +# backend/app/main.py +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.config import settings + +app = FastAPI(title="KOL Insight API", version="1.0.0") + +# CORS 配置 +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, # ["http://localhost:3000"] + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +``` + +### 10.4 API 调用封装(前端) + +```typescript +// frontend/src/lib/api.ts +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1'; + +export async function queryVideos(request: QueryRequest): Promise { + const response = await fetch(`${API_BASE_URL}/query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error('Query failed'); + } + + return response.json(); +} + +export async function exportData(format: 'xlsx' | 'csv'): Promise { + const response = await fetch(`${API_BASE_URL}/export?format=${format}`); + + if (!response.ok) { + throw new Error('Export failed'); + } + + return response.blob(); +} +``` + +### 10.5 FastAPI 自动生成 API 文档 + +FastAPI 自动生成交互式 API 文档,无需额外配置: + +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` +- OpenAPI JSON: `http://localhost:8000/openapi.json` + +前端开发人员可直接通过这些文档了解 API 接口定义和测试 API。 + diff --git a/doc/FeatureSummary.md b/doc/FeatureSummary.md new file mode 100644 index 0000000..9492e88 --- /dev/null +++ b/doc/FeatureSummary.md @@ -0,0 +1,487 @@ +# KOL Insight - 功能摘要 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2025-01-28 | +| 来源文档 | PRD.md | + +## 1. 功能总览 + +### 1.1 功能统计 + +| 类别 | 数量 | +|------|------| +| 功能模块 | 5 个 | +| P0 功能 | 7 个 | +| P1 功能 | 2 个 | +| P2 功能 | 1 个 | + +### 1.2 功能架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ KOL Insight │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 数据查询模块 │ │ 数据计算模块 │ │ 品牌API集成模块 │ │ +│ │ ────────────── │ │ ────────────── │ │ ────────────── │ │ +│ │ • 星图ID查询 │ │ • 预估自然CPM │ │ • 品牌名称批量 │ │ +│ │ • 达人ID查询 │ │ • 预估看后搜人数 │ │ 获取(后端) │ │ +│ │ • 昵称模糊查询 │ │ • 看后搜人数成本 │ │ │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 数据展示模块 │ │ 数据导出模块 │ │ 外部服务依赖 │ │ +│ │ ────────────── │ │ ────────────── │ │ ────────────── │ │ +│ │ • 结果列表展示 │ │ • Excel/CSV导出 │ │ • PostgreSQL │ │ +│ │ • 视频链接跳转 │ │ │ │ • 品牌API │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 模块依赖关系 + +``` + ┌──────────────────┐ + │ 数据查询模块 │ + │ (F-001~003) │ + └────────┬─────────┘ + │ + │ 查询结果(含brand_id) + ▼ +┌──────────────────┐ ┌──────────────────┐ +│ 品牌API集成 │◀─│ 数据计算模块 │ +│ (F-010) │ │ (F-004~006) │ +│ 后端批量调用 │ └────────┬─────────┘ +└────────┬─────────┘ │ + │ │ 计算结果 + │ 品牌名称 │ + └─────────┬───────────┘ + ▼ + ┌──────────────────┐ + │ 数据展示模块 │ + │ (F-007~008) │ + └────────┬─────────┘ + │ + │ 展示数据(含品牌名称) + ▼ + ┌──────────────────┐ + │ 数据导出模块 │ + │ (F-009) │ + └──────────────────┘ +``` + +## 2. 功能清单 + +### 2.1 数据查询模块 + +**模块职责**: 接收用户输入的查询条件,从数据库检索匹配的KOL视频数据 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-001 | 星图ID查询 | 批量输入星图ID,精准匹配数据库 | P0 | US-001 | +| F-002 | 达人ID查询 | 批量输入达人unique_id,精准匹配数据库 | P0 | US-002 | +| F-003 | 昵称模糊查询 | 输入达人昵称,模糊匹配数据库 | P0 | US-003 | + +#### 功能契约详情 + +**F-001: 星图ID查询** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户选择"星图ID"查询方式并提交查询 | +| **输入** | 星图ID列表(换行分隔的字符串) | +| **处理逻辑** | 1. 解析输入,按换行符分割为ID数组
2. 对每个ID执行 `WHERE star_id = ?` 精准匹配
3. 合并所有匹配结果 | +| **输出** | 匹配的视频数据列表(包含26个输出字段) | +| **异常情况** | 1. 输入为空:提示"请输入星图ID"
2. 无匹配结果:返回空列表,提示"未找到匹配数据"
3. 数据库连接失败:显示错误提示 | +| **边界说明** | 仅支持精准匹配,不支持模糊匹配;单次查询建议不超过100个ID | + +**F-002: 达人ID查询** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户选择"达人unique_id"查询方式并提交查询 | +| **输入** | 达人unique_id列表(换行分隔的字符串) | +| **处理逻辑** | 1. 解析输入,按换行符分割为ID数组
2. 对每个ID执行 `WHERE star_unique_id = ?` 精准匹配
3. 合并所有匹配结果 | +| **输出** | 匹配的视频数据列表(包含26个输出字段) | +| **异常情况** | 1. 输入为空:提示"请输入达人ID"
2. 无匹配结果:返回空列表
3. 数据库连接失败:显示错误提示 | +| **边界说明** | 仅支持精准匹配;单次查询建议不超过100个ID | + +**F-003: 昵称模糊查询** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户选择"达人昵称"查询方式并提交查询 | +| **输入** | 达人昵称(字符串) | +| **处理逻辑** | 1. 执行 `WHERE star_nickname LIKE '%{input}%'` 包含匹配
2. 返回所有匹配结果 | +| **输出** | 匹配的视频数据列表(包含26个输出字段) | +| **异常情况** | 1. 输入为空:提示"请输入达人昵称"
2. 无匹配结果:返回空列表
3. 匹配结果过多:返回前1000条并提示 | +| **边界说明** | 支持模糊匹配(包含关系);结果数量可能较大 | + +--- + +### 2.2 数据计算模块 + +**模块职责**: 对查询结果进行预估指标计算,生成CPM和看后搜成本数据 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-004 | 预估自然CPM计算 | 计算每千次自然曝光的成本 | P0 | US-004 | +| F-005 | 预估自然看后搜人数计算 | 计算自然流量带来的看后搜人数 | P0 | US-004 | +| F-006 | 预估自然看后搜人数成本计算 | 计算每个自然看后搜用户的成本 | P0 | US-004 | + +#### 功能契约详情 + +**F-004: 预估自然CPM计算** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 查询返回结果后自动执行 | +| **输入** | 视频数据中的 `estimated_video_cost`、`natural_play_cnt` 字段 | +| **处理逻辑** | `预估自然CPM = estimated_video_cost / natural_play_cnt * 1000` | +| **输出** | 预估自然CPM值(单位:元/千次曝光) | +| **异常情况** | 1. natural_play_cnt = 0:返回 null 或 "-"
2. 字段缺失:返回 null | +| **边界说明** | 结果保留2位小数;除零时不计算 | + +**F-005: 预估自然看后搜人数计算** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 查询返回结果后自动执行 | +| **输入** | 视频数据中的 `natural_play_cnt`、`total_play_cnt`、`after_view_search_uv` 字段 | +| **处理逻辑** | `预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv` | +| **输出** | 预估自然看后搜人数(单位:人) | +| **异常情况** | 1. total_play_cnt = 0:返回 null
2. 字段缺失:返回 null | +| **边界说明** | 结果取整或保留2位小数;除零时不计算 | + +**F-006: 预估自然看后搜人数成本计算** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | F-005 计算完成后自动执行 | +| **输入** | `estimated_video_cost` 字段、F-005 的计算结果 | +| **处理逻辑** | `预估自然看后搜人数成本 = estimated_video_cost / 预估自然看后搜人数` | +| **输出** | 预估自然看后搜人数成本(单位:元/人) | +| **异常情况** | 1. 预估自然看后搜人数 = 0 或 null:返回 null
2. 依赖计算失败:返回 null | +| **边界说明** | 依赖 F-005 结果;结果保留2位小数 | + +--- + +### 2.3 品牌API集成模块 + +**模块职责**: 在后端查询时批量调用品牌API,获取品牌名称并补充到查询结果中 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-010 | 品牌名称批量获取 | 后端批量调用品牌API获取品牌名称 | P0 | US-006 | + +#### 功能契约详情 + +**F-010: 品牌名称批量获取** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 后端查询返回结果后,在返回给前端之前自动执行 | +| **输入** | 查询结果中的 `brand_id` 列表(去重后) | +| **处理逻辑** | 1. 从查询结果中提取所有唯一的 brand_id
2. 批量并发调用品牌API:`GET /v1/yuntu/brands/{brand_id}`
3. 构建 brand_id → brand_name 映射表
4. 将品牌名称填充到每条查询结果的 brand_name 字段 | +| **输出** | 补充了 brand_name 字段的完整查询结果 | +| **异常情况** | 1. 单个品牌API调用失败:该条记录 brand_name 降级显示 brand_id
2. 品牌API服务不可用:所有记录降级显示 brand_id
3. brand_id 为空:brand_name 显示为 "-" | +| **边界说明** | 1. 在后端执行,前端无需调用
2. 使用并发控制,限制同时请求数(如最多10个并发)
3. 可选:缓存品牌名称,减少重复请求
4. 超时设置:单个请求超时3秒 | + +**批量调用策略**: + +``` +查询结果 (100条) + │ + ▼ +┌─────────────────────────────────────┐ +│ 1. 提取唯一 brand_id (假设30个) │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 2. 批量并发请求 (限制10并发) │ +│ Promise.all([ │ +│ fetch(brand/id1), │ +│ fetch(brand/id2), │ +│ ... │ +│ ]) │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 3. 构建映射表 │ +│ { id1: "品牌A", id2: "品牌B" } │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 4. 填充 brand_name 到结果 │ +└─────────────────────────────────────┘ +``` + +--- + +### 2.4 数据展示模块 + +**模块职责**: 将查询和计算结果以表格形式展示给用户 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-007 | 结果列表展示 | 以表格形式展示查询结果的所有字段 | P1 | US-006 | +| F-008 | 视频链接跳转 | 点击视频链接在新窗口打开原视频 | P2 | US-007 | + +#### 功能契约详情 + +**F-007: 结果列表展示** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 查询完成且有结果数据 | +| **输入** | 查询结果数据列表(含计算字段和品牌名称,由后端 F-010 处理完成) | +| **处理逻辑** | 1. 渲染数据表格
2. 展示26个输出字段(含3个计算字段)
3. 品牌名称已由后端获取,直接展示 brand_name 字段 | +| **输出** | HTML表格展示 | +| **异常情况** | 1. 无数据:显示空状态提示
2. 品牌名称为空:显示 "-" | +| **边界说明** | 支持分页(如数据量大);列名使用中文;品牌名称由后端处理,前端无需调用API | + +**F-008: 视频链接跳转** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户点击视频链接 | +| **输入** | 视频链接URL(video_url字段) | +| **处理逻辑** | 在新窗口/标签页打开链接 | +| **输出** | 浏览器新标签页打开视频页面 | +| **异常情况** | 1. 链接为空:不可点击或显示提示
2. 链接无效:由浏览器处理 | +| **边界说明** | 使用 `target="_blank"` 打开;需考虑安全属性 | + +--- + +### 2.4 数据导出模块 + +**模块职责**: 将查询结果导出为Excel或CSV文件供用户下载 + +#### 功能列表 + +| ID | 功能 | 描述 | 优先级 | 关联用户故事 | +|----|------|------|--------|--------------| +| F-009 | Excel/CSV导出 | 将当前查询结果导出为文件 | P1 | US-005 | + +#### 功能契约详情 + +**F-009: Excel/CSV导出** + +| 契约项 | 说明 | +|--------|------| +| **触发条件** | 用户点击"导出"按钮 | +| **输入** | 当前查询结果数据列表 | +| **处理逻辑** | 1. 将数据转换为Excel/CSV格式
2. 使用中文列名作为表头
3. 生成文件并触发下载 | +| **输出** | Excel(.xlsx)或CSV文件下载 | +| **异常情况** | 1. 无数据:提示"无数据可导出"
2. 数据量过大:分批处理或提示限制 | +| **边界说明** | 导出数据包含所有26个字段;文件名包含时间戳;单次导出建议不超过1000条 | + +--- + +## 3. 功能依赖矩阵 + +| 功能 | F-001 | F-002 | F-003 | F-004 | F-005 | F-006 | F-007 | F-008 | F-009 | F-010 | +|------|-------|-------|-------|-------|-------|-------|-------|-------|-------|-------| +| F-001 | - | | | | | | | | | | +| F-002 | | - | | | | | | | | | +| F-003 | | | - | | | | | | | | +| F-004 | ✓ | ✓ | ✓ | - | | | | | | | +| F-005 | ✓ | ✓ | ✓ | | - | | | | | | +| F-006 | | | | | ✓ | - | | | | | +| F-007 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | | | ✓ | +| F-008 | | | | | | | ✓ | - | | | +| F-009 | | | | ✓ | ✓ | ✓ | ✓ | | - | ✓ | +| F-010 | ✓ | ✓ | ✓ | | | | | | | - | + +**说明**: +- ✓ 表示行功能依赖列功能 +- F-004/005/006(计算模块)依赖 F-001/002/003(查询模块)的输出 +- F-006 依赖 F-005 的计算结果 +- F-010(品牌API)依赖查询模块获取 brand_id 列表,在后端批量调用 +- F-007(展示)依赖查询、计算结果和 F-010 的品牌名称 +- F-009(导出)依赖展示数据(含品牌名称) + +## 4. 功能流程图 + +### 4.1 核心业务流程:批量查询与导出 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 用户输入 │ │ 选择查询 │ │ 提交查询 │ +│ 查询条件 │ ──▶ │ 方式 │ ──▶ │ │ +└─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ┌──────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────────────┐ + │ 数据查询模块 │ + │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ + │ │ F-001 │ │ F-002 │ │ F-003 │ │ + │ │ 星图ID │ / │ 达人ID │ / │ 昵称 │ │ + │ └────┬────┘ └────┬────┘ └────┬────┘ │ + └───────┼─────────────┼─────────────┼─────────────────┘ + └─────────────┼─────────────┘ + ▼ + ┌──────────────────────────────────────────────────────┐ + │ 后端处理(并行执行) │ + │ │ + │ ┌────────────────────┐ ┌────────────────────┐ │ + │ │ 数据计算模块 │ │ 品牌API集成模块 │ │ + │ │ ┌─────┐ ┌─────┐ │ │ ┌──────────────┐ │ │ + │ │ │F-004│ │F-005│ │ │ │ F-010 │ │ │ + │ │ │ CPM │ │看后搜│ │ │ │ 批量获取品牌 │ │ │ + │ │ └─────┘ └──┬──┘ │ │ │ 名称 │ │ │ + │ │ │ │ │ └──────────────┘ │ │ + │ │ ┌───┘ │ │ │ │ + │ │ ▼ │ │ │ │ + │ │ ┌─────┐ │ │ │ │ + │ │ │F-006│ │ │ │ │ + │ │ │成本 │ │ │ │ │ + │ │ └─────┘ │ │ │ │ + │ └────────────────────┘ └────────────────────┘ │ + └─────────────────────┬────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────────────┐ + │ 数据展示模块 │ + │ ┌───────────────────────────────────────┐ │ + │ │ F-007 结果列表展示(含品牌名称) │ │ + │ │ ┌─────────────────────────────────┐ │ │ + │ │ │ F-008 视频链接(可点击跳转) │ │ │ + │ │ └─────────────────────────────────┘ │ │ + │ └───────────────────────────────────────┘ │ + └─────────────────────┬────────────────────────────────┘ + │ + ┌────────────┴────────────┐ + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ 继续查询 │ │ F-009 │ + │ │ │ 导出数据 │ + └─────────────┘ └─────────────┘ +``` + +### 4.2 计算模块内部流程 + +``` +┌──────────────────┐ +│ 查询结果数据 │ +└────────┬─────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────┐ +│ 并行计算 │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ F-004 │ │ F-005 │ │ +│ │ 预估自然CPM │ │ 预估自然看后搜人数 │ │ +│ │ │ │ │ │ +│ │ cost/natural*1000 │ │ natural/total*uv │ │ +│ └──────────┬──────────┘ └──────────┬──────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ 除零检查 │ │ 除零检查 │ │ +│ │ null → 显示"-" │ │ null → 显示"-" │ │ +│ └─────────────────────┘ └──────────┬──────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────┐ │ +│ │ F-006 │ │ +│ │ 预估看后搜人数成本 │ │ +│ │ │ │ +│ │ cost/看后搜人数 │ │ +│ └─────────────────────┘ │ +└────────────────────────────────────────────────────────────┘ +``` + +## 5. 版本规划 + +| 版本 | 包含功能 | 功能ID | 目标 | +|------|----------|--------|------| +| MVP | 核心查询、计算、品牌API、展示、导出 | F-001 ~ F-007, F-009, F-010 | 完成核心批量查询和成本计算功能 | +| v1.1 | 视频链接跳转、性能优化 | F-008 | 提升用户体验,优化查询性能 | + +### MVP 功能清单 + +| 优先级 | 功能ID | 功能名称 | +|--------|--------|----------| +| P0 | F-001 | 星图ID查询 | +| P0 | F-002 | 达人ID查询 | +| P0 | F-003 | 昵称模糊查询 | +| P0 | F-004 | 预估自然CPM计算 | +| P0 | F-005 | 预估自然看后搜人数计算 | +| P0 | F-006 | 预估自然看后搜人数成本计算 | +| P0 | F-010 | 品牌名称批量获取(后端) | +| P1 | F-007 | 结果列表展示 | +| P1 | F-009 | Excel/CSV导出 | + +### v1.1 功能清单 + +| 优先级 | 功能ID | 功能名称 | +|--------|--------|----------| +| P2 | F-008 | 视频链接跳转 | + +## 6. 接口契约预览 + +> 详细接口定义在 DevelopmentPlan 中,此处仅列出关键接口 + + +| 功能 | 接口类型 | 端点 | 简要说明 | +|------|----------|------|----------| +| F-001/002/003 | FastAPI 后端 | POST /api/v1/query | 批量查询,支持type参数区分查询方式 | +| F-009 | FastAPI 后端 | GET /api/v1/export | 导出当前查询结果 | +| F-010 | 外部API(后端调用) | GET /v1/yuntu/brands/{brand_id} | 后端批量获取品牌名称 | + + +**技术架构说明**: +- **后端**: Python FastAPI 框架,提供 RESTful API +- **前端**: React + Next.js,通过 HTTP 请求调用后端 API +- **架构**: 前后端完全分离,独立部署 +- **API 版本**: 使用 `/api/v1/` 前缀进行版本管理 +- **跨域**: FastAPI 配置 CORS 中间件支持前端调用 + + +### 查询接口预览 + + +``` +POST /api/v1/query +Content-Type: application/json + +{ + "type": "star_id" | "unique_id" | "nickname", + "values": ["id1", "id2", ...] | "昵称关键词" +} + +Response: +{ + "success": true, + "data": [...], // 视频数据列表 + "total": 100 +} +``` + +### 导出接口预览 + + +``` +GET /api/v1/export?format=xlsx|csv + +Response: 文件下载 +``` diff --git a/doc/PRD.md b/doc/PRD.md new file mode 100644 index 0000000..f16b7bf --- /dev/null +++ b/doc/PRD.md @@ -0,0 +1,365 @@ +# KOL Insight - 产品需求文档 (PRD) + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2025-01-28 | +| 状态 | 草稿 | + +## 1. 产品概述 + +### 1.1 产品背景 + +在 KOL 营销领域,运营人员需要频繁查询达人视频数据以评估投放效果、计算投放成本。现有的云图平台虽然提供了数据,但缺乏批量查询和成本预估能力,导致运营人员需要手动逐条查询和计算,效率低下。 + +KOL Insight 旨在解决这一痛点,提供批量数据查询和智能成本预估功能,帮助运营人员快速获取决策所需的数据。 + +### 1.2 产品定位 + +- **目标用户**:KOL 营销运营人员、投放优化师、品牌方 +- **核心价值**:批量查询 KOL 视频数据,自动计算预估 CPM 和看后搜成本 +- **差异化优势**:支持多种查询方式(星图ID/达人ID/昵称),自动计算关键成本指标 + +### 1.3 产品目标 + +| 目标 | 指标 | 衡量方式 | +|------|------|----------| +| 提升查询效率 | 单次可批量查询多个 KOL | 对比手动查询耗时 | +| 降低计算错误 | 自动计算预估指标准确率 100% | 人工抽检验证 | +| 提高数据可用性 | 支持数据导出 | 导出功能完整性 | + +## 2. 用户故事 + +### 2.1 用户角色定义 + +| 角色 | 描述 | 核心目标 | 痛点 | +|------|------|----------|------| +| 运营人员 | 负责 KOL 投放执行与数据分析的一线人员 | 快速获取 KOL 视频表现数据 | 逐条查询效率低,成本计算繁琐 | +| 投放优化师 | 负责投放策略优化的专业人员 | 评估投放 ROI,优化投放策略 | 数据分散,难以横向对比 | + +### 2.2 用户故事列表 + +#### P0 - 核心故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-001 | 作为运营人员,我想要批量输入星图ID查询KOL数据,以便快速获取多个达人的视频表现 | 1. 支持批量输入星图ID(换行分隔)
2. 精准匹配 star_id 字段
3. 返回完整视频数据列表 | +| US-002 | 作为运营人员,我想要通过达人unique_id查询数据,以便根据达人ID快速定位数据 | 1. 支持批量输入达人unique_id
2. 精准匹配 star_unique_id 字段
3. 返回对应视频数据 | +| US-003 | 作为运营人员,我想要通过达人昵称模糊搜索,以便在不知道精确ID时也能找到数据 | 1. 支持输入达人昵称
2. 模糊匹配 star_nickname 字段
3. 返回所有匹配结果 | + +| US-004 | 作为运营人员,我想要看到预估自然CPM和看后搜人数成本,以便评估投放效果 | 1. 自动计算预估自然CPM
2. 自动计算预估自然看后搜人数
3. 自动计算预估自然看后搜人数成本
4. 数据展示在结果列表中 | + +#### P1 - 重要故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-005 | 作为运营人员,我想要导出查询结果,以便在 Excel 中进一步分析或汇报 | 1. 支持导出为 Excel/CSV 格式
2. 导出数据包含所有查询字段
3. 中文列名清晰可读 | +| US-006 | 作为运营人员,我想要看到视频的完整数据指标,以便全面了解视频表现 | 1. 展示所有核心指标(曝光、互动、A3率等)
2. 数据格式清晰易读 | + +#### P2 - 次要故事 + +| ID | 用户故事 | 验收标准 | +|----|----------|----------| +| US-007 | 作为运营人员,我想要点击视频链接直接跳转,以便快速查看原视频 | 1. 视频链接可点击
2. 新窗口打开视频页面 | + +### 2.3 用户旅程 + +**核心用户旅程:批量查询 KOL 数据** + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 触发点 │ │ 输入查询条件 │ │ 查看结果 │ │ 导出数据 │ +│ 需要KOL数据 │ ──▶ │ 批量输入ID/昵称 │ ──▶ │ 浏览数据列表 │ ──▶ │ 下载Excel/CSV │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ + "需要快速获取 "选择查询方式, "系统自动计算 "数据可用于 + 多个达人数据" 粘贴ID列表" CPM等指标" 汇报和分析" +``` + +用户从需要 KOL 数据开始,选择合适的查询方式(星图ID/达人ID/昵称),批量输入查询条件后提交查询。系统返回结果列表,自动计算预估 CPM 和看后搜成本等指标。用户可浏览数据,需要时导出为 Excel 或 CSV 文件。 + +## 3. 功能需求 + +### 3.1 功能架构 + + +``` +KOL Insight +├── 数据查询模块 +│ ├── 星图ID精准查询 +│ ├── 达人unique_id精准查询 +│ └── 达人昵称模糊查询 +├── 数据计算模块 +│ ├── 预估自然CPM计算 +│ ├── 预估自然看后搜人数计算 +│ └── 预估自然看后搜人数成本计算 +├── 数据展示模块 +│ ├── 结果列表展示 +│ └── 视频链接跳转 +└── 数据导出模块 + └── Excel/CSV导出 +``` + +### 3.2 功能详情 + +#### 3.2.1 数据查询模块 + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| 星图ID查询 | 批量输入星图ID,精准匹配 star_id 字段 | US-001 | P0 | 支持批量输入,精准匹配,返回完整数据 | +| 达人ID查询 | 批量输入达人unique_id,精准匹配 star_unique_id 字段 | US-002 | P0 | 支持批量输入,精准匹配,返回完整数据 | +| 昵称模糊查询 | 输入达人昵称,模糊匹配 star_nickname 字段 | US-003 | P0 | 支持包含匹配,返回所有匹配结果 | + +#### 3.2.2 数据计算模块 + + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| 预估自然CPM | 公式:`estimated_video_cost / natural_play_cnt * 1000` | US-004 | P0 | 计算结果准确,单位为元/千次曝光 | +| 预估自然看后搜人数 | 公式:`natural_play_cnt / total_play_cnt * after_view_search_uv` | US-004 | P0 | 计算结果准确,单位为人数 | +| 预估自然看后搜人数成本 | 公式:`estimated_video_cost / 预估自然看后搜人数` | US-004 | P0 | 计算结果准确,单位为元/人 | + +#### 3.2.3 数据展示模块 + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| 结果列表展示 | 展示查询结果的完整数据表格 | US-006 | P1 | 包含所有指标字段,格式清晰 | +| 视频链接跳转 | 点击视频链接跳转到原视频页面 | US-007 | P2 | 链接可点击,新窗口打开 | + +#### 3.2.4 数据导出模块 + +| 功能点 | 描述 | 关联用户故事 | 优先级 | 验收标准 | +|--------|------|--------------|--------|----------| +| 数据导出 | 将查询结果导出为 Excel/CSV 格式 | US-005 | P1 | 文件可下载,数据完整,中文列名 | + +## 4. 非功能需求 + +### 4.1 性能需求 + +| 指标 | 要求 | 说明 | +|------|------|------| +| 查询响应时间 | ≤ 3秒 | 100条以内的批量查询 | +| 页面加载时间 | ≤ 2秒 | 首页加载 | +| 导出响应时间 | ≤ 5秒 | 1000条以内的数据导出 | + +### 4.2 安全需求 + +- 数据库连接使用安全凭证,不在代码中硬编码 +- 查询输入需进行 SQL 注入防护 +- 敏感配置通过环境变量管理 + +### 4.3 兼容性需求 + + +| 平台/环境 | 支持版本 | +|-----------|----------| +| 浏览器 | Chrome/Edge/Firefox 最新版 | +| Python | 3.9+ 及以上 | +| PostgreSQL | 14.x 及以上 | +| Node.js(前端构建) | 18.x 及以上 | + +### 4.4 可用性需求 + +- 系统可用性目标:99%(工作时间) +- 提供基本的错误提示,便于用户理解问题原因 + +## 5. 数据需求 + +### 5.1 数据模型 + + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 视频数据表 (kol_videos) │ +├─────────────────────────────────────────────────────────────────┤ +│ 基础信息 │ +│ item_id (视频ID) [主键] │ +│ title (视频标题) │ +│ star_id (星图ID) [索引] │ +│ star_unique_id (达人unique_id) [索引] │ +│ star_nickname (达人昵称) [索引] │ +│ video_url (视频链接) │ +│ publish_time (发布时间) │ +├─────────────────────────────────────────────────────────────────┤ +│ 曝光指标 │ +│ natural_play_cnt (自然曝光数) ★ 计算用 │ +│ heated_play_cnt (加热曝光数) │ +│ total_play_cnt (总曝光数) ★ 计算用 │ +├─────────────────────────────────────────────────────────────────┤ +│ 互动指标 │ +│ total_interact (总互动) │ +│ like_cnt (点赞) │ +│ share_cnt (转发) │ +│ comment_cnt (评论) │ +├─────────────────────────────────────────────────────────────────┤ +│ 效果指标 │ +│ new_a3_rate (新增A3率) │ +│ after_view_search_uv (看后搜人数) ★ 计算用 │ +│ return_search_cnt (回搜次数) │ +├─────────────────────────────────────────────────────────────────┤ +│ 商业信息 │ +│ industry_id (合作行业ID) │ +│ industry_name (合作行业) │ +│ brand_id (合作品牌ID) → 需调用品牌API获取名称 │ +│ estimated_video_cost (预估视频价格) ★ 计算用 │ +└─────────────────────────────────────────────────────────────────┘ + +★ 标记字段为计算预估指标所需的关键字段 +``` + +### 5.2 数据规范 + +| 字段 | 类型 | 说明 | 校验规则 | +|------|------|------|----------| +| star_id | string | 星图ID | 非空 | +| star_unique_id | string | 达人唯一标识 | 非空 | +| star_nickname | string | 达人昵称 | 非空 | +| item_id | string | 视频ID | 非空,唯一 | +| 曝光数 | integer | 各类曝光数据 | ≥ 0 | +| 互动数 | integer | 各类互动数据 | ≥ 0 | +| 预估视频价格 | decimal | 预估价格 | ≥ 0 | + +## 6. 接口需求 + +### 6.1 外部接口 + + +| 接口 | 用途 | 提供方 | +|------|------|--------| +| PostgreSQL | 数据存储与查询 | 自建数据库 | +| 品牌API | 根据品牌ID获取品牌名称 | 内部API (api.internal.intelligrow.cn) | + + +**品牌API详情**: +- 接口地址:`/v1/yuntu/brands/{brand_id}` +- 请求方式:GET +- 用途:根据合作品牌ID(brand_id)查询品牌名称 +- 文档:https://api.internal.intelligrow.cn/docs#/云图 + + +### 6.2 内部接口 + + +| 接口 | 方法 | 用途 | 说明 | +|------|------|------|------| +| /api/v1/query | POST | 批量查询KOL视频数据 | FastAPI 后端服务提供 | +| /api/v1/export | GET | 导出查询结果为Excel/CSV | FastAPI 后端服务提供 | + + +**API 架构说明**: +- 后端采用 FastAPI 框架,提供 RESTful API +- 前端(Next.js)通过 HTTP 请求调用后端 API +- API 版本管理:使用 `/api/v1/` 前缀 +- 跨域支持:FastAPI 配置 CORS 中间件 + + +## 7. 约束与依赖 + +### 7.1 技术约束 + + +| 约束 | 说明 | 影响 | +|------|------|------| +| 前端技术栈 | React + Next.js (App Router) | 前端技术选型固定 | +| 后端技术栈 | Python FastAPI + PostgreSQL | 后端技术选型固定,前后端分离部署 | +| 部署方式 | Docker(推荐) / PM2(前端) + Gunicorn/Uvicorn(后端) | 运维方式固定,需分别部署前后端服务 | + +### 7.2 业务约束 + +- 数据来源于云图平台,需确保数据同步的及时性和准确性 +- 查询功能仅用于内部运营分析,不对外开放 + +### 7.3 外部依赖 + + +| 依赖 | 说明 | +|------|------| +| 云图数据源 | 视频数据需从云图平台同步 | +| PostgreSQL 数据库 | 依赖数据库服务可用性 | +| 品牌API | 依赖内部API服务获取品牌名称 | + +## 8. 里程碑规划 + +``` +Phase 1 - MVP Phase 2 - 优化 + │ │ + ▼ ▼ +┌──────────┐ ┌──────────┐ +│ MVP │ ────────▶ │ v1.1 │ +│ 核心查询 │ │ 性能优化 │ +└──────────┘ └──────────┘ + 待定日期 待定日期 +``` + +| 阶段 | 目标 | 交付物 | +|------|------|--------| +| MVP | 完成核心查询和计算功能 | 可用的批量查询系统,支持数据导出 | +| v1.1 | 性能优化和体验提升 | 更快的查询速度,更好的用户体验 | + +## 9. 风险评估 + + +| 风险 | 可能性 | 影响 | 应对措施 | +|------|--------|------|----------| +| 数据同步延迟 | 中 | 中 | 建立数据同步监控机制 | +| 大批量查询性能问题 | 中 | 中 | 设置批量查询上限,优化数据库索引 | +| 数据库连接不稳定 | 低 | 高 | 实现连接池和重试机制 | +| 品牌API不可用 | 低 | 中 | 缓存品牌数据,降级显示品牌ID | + +## 附录 + +### A. 术语表 + + +| 术语 | 定义 | +|------|------| +| KOL | Key Opinion Leader,关键意见领袖,即达人/网红 | +| 星图ID | 巨量星图平台的达人唯一标识 | +| unique_id | 达人在平台的唯一标识符 | +| CPM | Cost Per Mille,每千次曝光成本 | +| 看后搜 | 用户观看视频后进行搜索的行为 | +| A3率 | 用户深度互动率指标 | +| 自然曝光 | 非付费推广获得的曝光量 | +| 加热曝光 | 通过付费推广获得的曝光量 | +| natural_play_cnt | 自然曝光次数,用于计算预估自然CPM | +| total_play_cnt | 总曝光次数,包含自然+加热曝光 | +| estimated_video_cost | 预估视频价格,用于计算成本指标 | +| after_view_search_uv | 看后搜用户数,观看视频后进行搜索的独立用户数 | + +### B. 输出字段完整列表 + + +| 中文名 | 字段名 | 说明 | +|--------|--------|------| +| 视频ID | item_id | 主键 | +| 视频标题 | title | - | +| 爆文类型 | viral_type | - | +| 视频链接 | video_url | - | +| 新增A3率 | new_a3_rate | - | +| 看后搜人数 | after_view_search_uv | - | +| 回搜次数 | return_search_cnt | - | +| 自然曝光数 | natural_play_cnt | 计算用 | +| 加热曝光数 | heated_play_cnt | - | +| 总曝光数 | total_play_cnt | 计算用 | +| 总互动 | total_interact | - | +| 点赞 | like_cnt | - | +| 转发 | share_cnt | - | +| 评论 | comment_cnt | - | +| 合作行业ID | industry_id | - | +| 合作行业 | industry_name | - | +| 合作品牌ID | brand_id | 需调用品牌API | +| 合作品牌 | brand_name | 从品牌API获取 | +| 发布时间 | publish_time | - | +| 达人昵称 | star_nickname | 索引字段 | +| 达人unique_id | star_unique_id | 索引字段 | +| 预估视频价格 | estimated_video_cost | 计算用 | +| 预估自然CPM | (计算字段) | = estimated_video_cost / natural_play_cnt * 1000 | +| 预估自然看后搜人数 | (计算字段) | = natural_play_cnt / total_play_cnt * after_view_search_uv | +| 预估自然看后搜人数成本 | (计算字段) | = estimated_video_cost / 预估自然看后搜人数 | + +### C. 参考文档 + +- RequirementsDoc.md diff --git a/doc/RequirementsDoc.md b/doc/RequirementsDoc.md new file mode 100644 index 0000000..b9bde2a --- /dev/null +++ b/doc/RequirementsDoc.md @@ -0,0 +1,65 @@ +[text](README.md)# KOL Insight + +云图 KOL 数据查询与分析工具。 + +## 功能 + +- 批量查询 KOL 视频数据 +- 支持星图ID、达人unique_id、达人昵称搜索 +- 计算预估自然CPM、看后搜成本等指标 +- 数据导出 + +## 技术栈 + +- **前端/后端**: Next.js (App Router) +- **数据库**: PostgreSQL +- **部署**: Docker / PM2 + +## 快速开始 + +```bash +# 安装依赖 +pnpm install + +# 配置环境变量 +cp .env.example .env.local + +# 开发模式 +pnpm dev + +# 构建 +pnpm build + +# 生产运行 +pnpm start +``` + +## 环境变量 + +```env +DATABASE_URL=postgresql://user:password@host:5432/yuntu_kol +``` + +## License + +MIT + +查询输入:批量 星图id(精准匹配) 或 达人unique_id (精准匹配) 或达人昵称(包含匹配) + + +星图ID → 匹配 star_id 字段 +达人unique_id → 匹配 star_unique_id 字段 +达人昵称 → 模糊匹配 star_nickname 字段 + +输出: +中文名 视频ID 视频标题 爆文类型 视频链接 新增A3率 看后搜次数 回搜次数 自然曝光数 加热曝光数 总曝光数 总互动 点赞 转发 评论 合作行业ID 合作行业 合作品牌ID 合作品牌 发布时间 达人昵称 达人unique_id 预估视频价格 预估自然CPM 预估自然看后搜 预估自然看后搜成本 +指标名 item_id title + +合作品牌要使用合作品牌id 调用另一个API查找 +https://api.internal.intelligrow.cn/docs#/%E4%BA%91%E5%9B%BE/get_yuntu_cookies_v1_yuntu_get_cookie_get +/v1/yuntu/brands/{brand_id} + +预估自然CPM =estimated_video_cost / natural_play_cnt *1000 +预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv +预估自然看后搜人数成本 = estimated_video_cost /预估自然看后搜人数 + diff --git a/doc/UIDesign.md b/doc/UIDesign.md new file mode 100644 index 0000000..acb7b66 --- /dev/null +++ b/doc/UIDesign.md @@ -0,0 +1,850 @@ +# KOL Insight - UI 设计文档 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2026-01-28 | +| 来源文档 | DevelopmentPlan.md, PRD.md, FeatureSummary.md | +| 品牌主体 | 麦秒思AI制作 | + +## 1. 设计概述 + +### 1.1 设计原则 + +**麦秒思AI设计语言** + +| 原则 | 说明 | 应用 | +|------|------|------| +| 优雅简洁 | 去除冗余元素,聚焦核心功能 | 单页应用,扁平化设计 | +| 专业可信 | 体现数据分析的专业性 | 稳重色系,清晰的信息层级 | +| 高效直观 | 减少用户学习成本 | 明确的操作流程,即时反馈 | +| 品牌一致 | 强化麦秒思AI品牌形象 | 统一使用品牌标识和色彩 | + +**品牌元素** + +- **Logo**: doc/ui/muse.svg (麦秒思AI品牌标识) +- **Slogan**: "麦秒思AI制作" (展示在关键位置) +- **色调**: 专业、现代、科技感 + +### 1.2 页面总览 + +| 页面ID | 页面名称 | 描述 | 对应功能 | 优先级 | +|--------|----------|------|----------|--------| +| P-001 | 数据查询主页 | 单页应用,包含查询、展示、导出功能 | F-001~F-009 | P0 | + +### 1.3 页面导航图 + +``` + ┌─────────────────────┐ + │ 数据查询主页 │ + │ P-001 │ + │ (单页应用) │ + └──────────┬──────────┘ + │ + │ 页面内交互 + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ 查询区域 │ │ 结果区域 │ │ 导出操作 │ + │ (顶部) │ │ (主体) │ │ (右上角) │ + └─────────────┘ └─────────────┘ └─────────────┘ +``` + +**说明**: KOL Insight 采用单页应用设计,所有功能集成在一个页面内,通过区域划分组织功能。 + +## 2. 页面设计 + +### 2.1 P-001: 数据查询主页 + +**页面信息** + +| 属性 | 值 | +|------|-----| +| 页面ID | P-001 | +| 对应功能 | F-001(星图ID查询), F-002(达人ID查询), F-003(昵称模糊查询), F-004~F-009(计算、展示、导出) | +| 入口 | 直接访问 (首页) | +| 出口 | 无 (单页应用) | +| 布局类型 | 垂直布局,从上到下依次为:品牌头部 → 查询区 → 结果区 | + +**【必须】页面布局 - ASCII 原型图** + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ Header (品牌头部) │ │ +│ │ ┌──────┐ [麦秒思AI制作] │ │ +│ │ │ MUSE │ KOL Insight - 云图数据查询分析 │ │ +│ │ │ Logo │ (品牌标识 + 产品名称) │ │ +│ │ └──────┘ │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ 查询区域 (Query Section) │ │ +│ │ │ │ +│ │ 查询方式: ( • ) 星图ID ( ) 达人unique_id ( ) 达人昵称 │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 输入查询内容... │ │ │ +│ │ │ (支持批量输入,每行一个ID或输入昵称关键词) │ │ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ └─────────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ [清空] [开始查询] │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +│ │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ 结果区域 (Results Section) │ │ +│ │ │ │ +│ │ 查询结果 (共 128 条) [导出Excel] [导出CSV]│ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 视频ID │ 标题 │ 达人 │ 自然曝光 │ CPM │ 看后搜成本 │ ... │ 操作 │ │ │ +│ │ ├─────────────────────────────────────────────────────────────────┤ │ │ +│ │ │ 12345 │ XXX │ @XX │ 100.2K │12.5 │ 8.3 │ ... │ [链接]│ │ │ +│ │ │ 12346 │ YYY │ @YY │ 85.3K │15.2 │ 9.8 │ ... │ [链接]│ │ │ +│ │ │ 12347 │ ZZZ │ @ZZ │ 92.1K │13.8 │ 7.5 │ ... │ [链接]│ │ │ +│ │ │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ │ │ +│ │ │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ │ │ +│ │ └─────────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ◀ 上一页 1 / 10 下一页 ▶ │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +│ │ +├────────────────────────────────────────────────────────────────────────────┤ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ Footer │ │ +│ │ © 2026 麦秒思AI制作 | KOL Insight v1.0 │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +**组件清单** + +| 组件ID | 组件名称 | 类型 | 说明 | 交互 | +|--------|----------|------|------|------| +| C-001 | 品牌头部 | Header | 展示麦秒思AI品牌Logo和产品名称 | 静态展示 | +| C-002 | 查询方式选择器 | Radio Group | 三种查询方式单选 | 点击切换查询方式 | +| C-003 | 查询输入框 | Textarea | 批量输入或昵称输入 | 文本输入 | +| C-004 | 查询按钮组 | Button Group | 清空、开始查询 | 点击执行操作 | +| C-005 | 结果表格 | Table | 展示26个字段的数据 | 横向滚动,列排序 | +| C-006 | 导出按钮组 | Button Group | 导出Excel/CSV | 点击触发下载 | +| C-007 | 分页器 | Pagination | 翻页控制 | 点击切换页码 | +| C-008 | 视频链接 | Link | 跳转到原视频 | 新窗口打开 | +| C-009 | Footer | Footer | 版权信息和品牌声明 | 静态展示 | + +**交互说明** + +| 触发 | 动作 | 结果 | +|------|------|------| +| 选择查询方式 | 切换 Radio | 输入框提示文案变化 | +| 输入查询条件 | 文本输入 | 实时验证输入格式 | +| 点击"清空" | 清空输入 | 输入框清空,结果区隐藏 | +| 点击"开始查询" | 提交查询 | 显示 Loading → 展示结果表格 | +| 点击表头 | 排序 | 按列排序数据 | +| 点击"导出Excel/CSV" | 触发下载 | 浏览器下载文件 | +| 点击视频链接 | 跳转 | 新窗口打开视频页面 | +| 点击分页 | 切换页 | 加载对应页数据 | + +**页面状态** + +| 状态 | 说明 | 展示 | +|------|------|------| +| 默认态 | 页面初始加载 | 查询区可用,结果区显示引导文案 | +| 输入态 | 用户正在输入 | 输入框聚焦,显示输入提示 | +| 查询中 | 提交查询后等待响应 | 按钮禁用,显示 Loading 动画 | +| 结果态 | 查询成功返回数据 | 展示结果表格和分页 | +| 空结果态 | 查询无匹配数据 | 显示空状态插图和提示 | +| 错误态 | 查询失败或网络错误 | 显示错误提示和重试按钮 | + +**默认态原型** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 查询区域 │ +│ [查询方式选择器] │ +│ [输入框 - 待输入] │ +│ [清空] [开始查询] │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ 结果区域 │ +│ │ +│ ┌─────────────┐ │ +│ │ 搜索图标 │ │ +│ └─────────────┘ │ +│ │ +│ 请选择查询方式并输入查询条件 │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**空结果态原型** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 结果区域 │ +│ │ +│ ┌─────────────┐ │ +│ │ 空盒子图标 │ │ +│ └─────────────┘ │ +│ │ +│ 未找到匹配数据 │ +│ │ +│ 请调整查询条件后重新尝试 │ +│ │ +│ [修改查询条件] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**查询中状态原型** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 结果区域 │ +│ │ +│ ┌─────────────┐ │ +│ │ ⟳ Loading │ │ +│ └─────────────┘ │ +│ │ +│ 正在查询数据,请稍候... │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**错误态原型** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 结果区域 │ +│ │ +│ ┌─────────────┐ │ +│ │ ⚠ 错误图标 │ │ +│ └─────────────┘ │ +│ │ +│ 查询失败,请重试 │ +│ │ +│ 可能原因:网络异常或数据库连接失败 │ +│ │ +│ [重新查询] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 3. 用户流程 + +### 3.1 核心流程:批量查询 KOL 数据 + +**【必须】流程图展示用户操作流程:** + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 打开页面 │ ──▶ │ 选择查询 │ ──▶ │ 输入查询 │ ──▶ │ 提交查询 │ +│ P-001 │ │ 方式 │ │ 条件 │ │ │ +└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 查询处理 │ + │ (后端) │ + └──────┬──────┘ + │ + ┌─────────────────────┼─────────────────────┐ + │ 成功 │ 失败 + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ 展示结果 │ │ 显示错误 │ + │ (结果表格) │ │ (重试) │ + └──────┬──────┘ └─────────────┘ + │ + │ + ┌───────────────────────┼───────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ 浏览数据 │ │ 点击视频 │ │ 导出数据 │ + │ (翻页/排序)│ │ 链接 │ │ (Excel/CSV)│ + └──────┬──────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ 继续查询 │ + │ 或离开 │ + └─────────────┘ +``` + +**流程步骤详解** + +| 步骤 | 页面/区域 | 用户操作 | 系统响应 | 预期时间 | +|------|----------|----------|----------|----------| +| 1 | P-001 | 打开页面 | 加载默认态,显示引导文案 | < 2s | +| 2 | 查询区 | 选择查询方式(Radio) | 输入框提示文案更新 | 即时 | +| 3 | 查询区 | 输入查询条件(Textarea) | 实时验证,显示字符数 | 即时 | +| 4 | 查询区 | 点击"开始查询"按钮 | 按钮禁用,显示 Loading | 即时 | +| 5 | 后端 | 提交查询请求 | 查询数据库,计算指标 | < 3s | +| 6a | 结果区 | 查询成功 | 展示结果表格,显示数据条数 | < 0.5s | +| 6b | 结果区 | 查询失败 | 显示错误提示和重试按钮 | < 0.5s | +| 7 | 结果区 | 浏览数据(滚动/翻页) | 加载更多数据或切换页 | < 0.5s | +| 8 | 结果区 | 点击视频链接 | 新窗口打开视频页面 | 即时 | +| 9 | 结果区 | 点击导出按钮 | 生成文件,触发下载 | < 5s | + +### 3.2 辅助流程:修改查询条件 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 查看结果 │ ──▶ │ 点击"清空" │ ──▶ │ 输入框清空 │ +│ (不满意) │ │ 按钮 │ │ (重新输入) │ +└─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 重新查询 │ + │ │ + └─────────────┘ +``` + +### 3.3 异常流程:错误处理 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 提交查询 │ ──▶ │ 网络异常/ │ ──▶ │ 显示错误 │ +│ │ │ 数据库错误 │ │ 提示 │ +└─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 点击"重新 │ + │ 查询"按钮 │ + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 重试查询 │ + │ │ + └─────────────┘ +``` + +## 4. 组件规范 + +### 4.1 基础组件 + +**Button 按钮** + +``` +主按钮 (Primary): +┌────────────────┐ +│ 开始查询 │ (品牌主色背景,白色文字) +└────────────────┘ +:hover → 加深 10% +:active → 加深 20% + +次按钮 (Secondary): +┌────────────────┐ +│ 清空 │ (白色背景,边框,灰色文字) +└────────────────┘ +:hover → 灰色背景 +:active → 深灰背景 + +导出按钮 (Action): +┌────────────────┐ +│ 导出Excel ▼ │ (绿色背景,白色文字) +└────────────────┘ + +禁用态 (Disabled): +┌────────────────┐ +│ 查询中... │ (灰色背景,禁止点击) +└────────────────┘ +``` + +**Input/Textarea 输入框** + +``` +默认态: +┌────────────────────────────────────────┐ +│ 请输入星图ID,每行一个... │ +└────────────────────────────────────────┘ + +聚焦态: +┌────────────────────────────────────────┐ +│ 12345▊ │ (蓝色边框高亮) +└────────────────────────────────────────┘ + +错误态: +┌────────────────────────────────────────┐ +│ (输入内容) │ (红色边框) +└────────────────────────────────────────┘ +⚠ 请输入有效的ID格式 +``` + +**Radio 单选框** + +``` +未选中: ( ) 星图ID +选中: (•) 星图ID (品牌主色圆点) +禁用: ( ) 星图ID (灰色文字) +``` + +**Table 表格** + +``` +┌──────────┬──────────┬──────────┬──────────┬──────────┐ +│ 视频ID ▲ │ 标题 │ 达人 │ 自然曝光 │ CPM │ (表头:深色背景) +├──────────┼──────────┼──────────┼──────────┼──────────┤ +│ 12345 │ 测试标题 │ @测试 │ 100.2K │ 12.5 │ (奇数行:白色) +├──────────┼──────────┼──────────┼──────────┼──────────┤ +│ 12346 │ 测试标题 │ @测试 │ 85.3K │ 15.2 │ (偶数行:浅灰) +└──────────┴──────────┴──────────┴──────────┴──────────┘ + +:hover 行 → 浅蓝色背景高亮 +``` + +**Pagination 分页器** + +``` +◀ 上一页 1 2 [3] 4 5 下一页 ▶ + +当前页: [3] → 品牌主色背景 +其他页: 2 → 可点击,悬停高亮 +禁用页: ◀ → 灰色,不可点击 +``` + +**Loading 加载动画** + +``` +方案1: 旋转圆环 + ┌─⟳─┐ + │ │ 正在加载... + └───┘ + +方案2: 骨架屏 (表格数据加载) +┌──────────┬──────────┬──────────┐ +│ ░░░░░░ │ ░░░░░░ │ ░░░░░░ │ +│ ░░░░░░ │ ░░░░░░ │ ░░░░░░ │ +│ ░░░░░░ │ ░░░░░░ │ ░░░░░░ │ +└──────────┴──────────┴──────────┘ +``` + +### 4.2 业务组件 + +**QueryForm 查询表单组件** + +``` +┌──────────────────────────────────────────────────────┐ +│ 查询方式: (•) 星图ID ( ) 达人unique_id ( ) 昵称 │ +│ │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ 请输入星图ID,每行一个... │ │ +│ │ │ │ +│ │ │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ [清空] [开始查询] │ +└──────────────────────────────────────────────────────┘ + +功能: +• 支持三种查询方式切换 +• 根据查询方式动态更新输入提示 +• 输入验证(实时) +• 清空和提交操作 +``` + +**ResultTable 结果表格组件** + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 查询结果 (共 128 条) [导出Excel] [导出CSV] │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ 26个字段的数据表格 (可横向滚动) │ │ +│ │ • 视频ID、标题、达人、曝光数据、互动数据 │ │ +│ │ • 预估CPM、预估看后搜人数、预估看后搜成本 │ │ +│ │ • 品牌信息、发布时间等 │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ ◀ 上一页 1 / 10 下一页 ▶ │ +└──────────────────────────────────────────────────────────────┘ + +功能: +• 展示26个字段 +• 支持列排序 +• 横向滚动(表格宽度超出) +• 分页展示(每页20条) +• 数字格式化(千分位) +• 视频链接可点击 +``` + +**EmptyState 空状态组件** + +``` +┌─────────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ 📦 空盒子 │ │ +│ └─────────────┘ │ +│ │ +│ 未找到匹配数据 │ +│ │ +│ 请调整查询条件后重新尝试 │ +│ │ +│ [修改查询条件] │ +│ │ +└─────────────────────────────────────────┘ +``` + +**ErrorState 错误状态组件** + +``` +┌─────────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ ⚠ 错误 │ │ +│ └─────────────┘ │ +│ │ +│ 查询失败,请重试 │ +│ │ +│ 可能原因:网络异常或数据库连接失败 │ +│ │ +│ [重新查询] │ +│ │ +└─────────────────────────────────────────┘ +``` + +## 5. 设计规范 + +### 5.1 色彩规范 + +**麦秒思AI品牌色系** + +| 用途 | 色值 | 示例 | 说明 | +|------|------|------|------| +| 主色 (Primary) | #4F46E5 | 主按钮、链接、选中态 | 品牌核心色,专业科技感 | +| 主色浅 | #818CF8 | 悬停态、次要元素 | 主色的衍生色 | +| 主色深 | #3730A3 | 按下态、强调元素 | 主色的深色变体 | +| 成功 (Success) | #10B981 | 成功提示、导出按钮 | 绿色系 | +| 警告 (Warning) | #F59E0B | 警告提示 | 橙色系 | +| 错误 (Error) | #EF4444 | 错误提示、删除操作 | 红色系 | +| 信息 (Info) | #3B82F6 | 信息提示 | 蓝色系 | +| 文字主色 | #111827 | 标题、正文 | 深灰色,易读 | +| 文字次色 | #6B7280 | 辅助文字、提示 | 中灰色 | +| 文字禁用 | #D1D5DB | 禁用文字 | 浅灰色 | +| 背景主色 | #FFFFFF | 页面背景 | 纯白 | +| 背景次色 | #F9FAFB | 卡片背景、表格奇数行 | 浅灰白 | +| 边框色 | #E5E7EB | 输入框边框、分隔线 | 浅灰 | + +**色彩使用示例** + +``` +主按钮: ████████ #4F46E5 (主色) +导出按钮: ████████ #10B981 (成功色) +错误提示: ████████ #EF4444 (错误色) +文字主色: ████████ #111827 +背景色: ████████ #FFFFFF +``` + +### 5.2 字体规范 + +**字体家族** + +| 平台 | 字体 | 备用 | +|------|------|------| +| 中文 | PingFang SC, Microsoft YaHei | sans-serif | +| 英文 | Inter, -apple-system, BlinkMacSystemFont | sans-serif | +| 数字 | Tabular Nums (等宽数字) | - | + +**字号规范** + +| 用途 | 字号 | 字重 | 行高 | 示例 | +|------|------|------|------|------| +| 页面标题 | 28px | Bold (700) | 1.3 | KOL Insight | +| 区域标题 | 20px | Semibold (600) | 1.4 | 查询结果 | +| 小标题 | 16px | Medium (500) | 1.5 | 查询方式 | +| 正文 | 14px | Regular (400) | 1.6 | 表格内容、按钮文字 | +| 辅助文字 | 12px | Regular (400) | 1.5 | 提示文案、标签 | +| 表格数据 | 14px | Regular (400) | 1.5 | 数据单元格 | + +### 5.3 间距规范 + +**基础间距单位: 4px** + +| 间距 | 值 | 用途 | +|------|-----|------| +| xs | 4px | 紧凑元素间距 | +| sm | 8px | 小间距,如图标与文字 | +| md | 16px | 标准间距,组件内部 | +| lg | 24px | 大间距,区域之间 | +| xl | 32px | 特大间距,页面区块 | +| 2xl | 48px | 超大间距,顶部底部留白 | + +**组件间距示例** + +``` +页面整体: padding: 32px (xl) + +┌────────────────────────────────┐ +│ [32px 顶部留白] │ +│ ┌──────────────────────────┐ │ +│ │ 查询区域 │ │ +│ └──────────────────────────┘ │ +│ [24px 区域间距] │ +│ ┌──────────────────────────┐ │ +│ │ 结果区域 │ │ +│ └──────────────────────────┘ │ +│ [32px 底部留白] │ +└────────────────────────────────┘ + +按钮组: gap: 8px (sm) +[清空] [8px] [开始查询] +``` + +### 5.4 圆角规范 + +| 元素 | 圆角值 | 说明 | +|------|--------|------| +| 按钮 | 6px | 适中圆角,现代感 | +| 输入框 | 6px | 与按钮保持一致 | +| 卡片 | 8px | 稍大圆角,柔和 | +| 表格 | 8px | 整体表格边框 | +| Badge/Tag | 12px | 较大圆角,标签感 | + +### 5.5 阴影规范 + +| 用途 | 阴影值 | 使用场景 | +|------|--------|----------| +| 轻微阴影 | 0 1px 3px rgba(0,0,0,0.1) | 卡片、输入框 | +| 标准阴影 | 0 4px 6px rgba(0,0,0,0.1) | 悬浮元素 | +| 深度阴影 | 0 10px 15px rgba(0,0,0,0.1) | 模态框、下拉菜单 | + +### 5.6 响应式断点 + +| 断点 | 宽度 | 布局说明 | +|------|------|----------| +| Mobile | < 768px | 单栏布局,表格横向滚动 | +| Tablet | 768px - 1024px | 优化表格列宽 | +| Desktop | 1024px - 1440px | 标准布局 | +| Large Desktop | > 1440px | 居中固定宽度或适当扩展 | + +**响应式布局示例** + +``` +Desktop (> 1024px): +┌────────────────────────────────────────┐ +│ [查询区域 - 全宽] │ +│ [结果表格 - 全宽,所有列可见] │ +└────────────────────────────────────────┘ + +Tablet (768px - 1024px): +┌──────────────────────────────┐ +│ [查询区域 - 全宽] │ +│ [结果表格 - 隐藏部分次要列] │ +└──────────────────────────────┘ + +Mobile (< 768px): +┌──────────────────┐ +│ [查询区域] │ +│ [结果卡片列表] │ (表格转为卡片布局) +│ [卡片1] │ +│ [卡片2] │ +└──────────────────┘ +``` + +## 6. 品牌应用 + +### 6.1 品牌标识使用 + +**Logo 使用规范** + +| 位置 | 尺寸 | 说明 | +|------|------|------| +| Header 左侧 | 高度 40px | 麦秒思AI Logo (doc/ui/muse.svg) | +| Favicon | 32x32px | 简化版 Logo 图标 | +| 加载动画 | - | 可选:Logo 动效 | + +**品牌声明位置** + +- Header 右上角:"麦秒思AI制作" +- Footer 中央:"© 2026 麦秒思AI制作 | KOL Insight v1.0" + +**Header 品牌区域详细设计** + +``` +┌────────────────────────────────────────────────────────────────┐ +│ ┌──────┐ │ +│ │ │ KOL Insight 麦秒思AI制作 │ +│ │ MUSE │ 云图数据查询分析 │ +│ │ Logo │ (产品名称 + Slogan) (品牌声明) │ +│ │ │ │ +│ └──────┘ │ +│ [40px] [16px] [产品名称:20px Bold] [14px Regular] │ +└────────────────────────────────────────────────────────────────┘ +``` + +### 6.2 空状态插图风格 + +- 使用简洁的线条插图 +- 色彩与品牌主色保持一致 +- 插图风格:扁平化、现代、专业 + +## 7. 动效规范 + +### 7.1 过渡动效 + +| 交互 | 动效 | 时长 | 缓动函数 | +|------|------|------|----------| +| 按钮悬停 | 背景色变化 | 150ms | ease-out | +| 输入框聚焦 | 边框高亮 | 200ms | ease-in-out | +| 页面切换 | 淡入淡出 | 300ms | ease-in-out | +| 表格排序 | 淡入 | 200ms | ease-out | +| 模态框打开 | 缩放+淡入 | 250ms | cubic-bezier(0.4, 0, 0.2, 1) | + +### 7.2 加载动画 + +``` +方案: 旋转圆环 +┌─────┐ +│ ⟳ │ (360度旋转,1s 一圈,无限循环) +└─────┘ + +或: 品牌色脉动 +┌─────┐ +│ ● │ (主色圆点,缩放脉动) +└─────┘ +``` + +## 8. 数据展示规范 + +### 8.1 数字格式化 + +| 数据类型 | 格式 | 示例 | +|----------|------|------| +| 整数 | 千分位分隔 | 1,234,567 | +| 小数 | 保留2位 | 12.34 | +| 大数值 | K/M 缩写 | 100.2K, 1.5M | +| 百分比 | % 符号 | 85.3% | +| 金额 | ¥ + 千分位 | ¥ 1,234.56 | +| 日期 | YYYY-MM-DD | 2026-01-28 | + +### 8.2 表格列定义 + +**完整的26个输出字段** + +| 序号 | 中文名 | 字段宽度 | 对齐方式 | 格式化 | +|------|--------|----------|----------|--------| +| 1 | 视频ID | 120px | 左对齐 | 文本 | +| 2 | 视频标题 | 200px | 左对齐 | 文本,超出省略 | +| 3 | 爆文类型 | 100px | 居中 | Badge | +| 4 | 视频链接 | 100px | 居中 | 链接按钮 | +| 5 | 新增A3率 | 100px | 右对齐 | 百分比 | +| 6 | 看后搜人数 | 120px | 右对齐 | 千分位 | +| 7 | 回搜次数 | 100px | 右对齐 | 千分位 | +| 8 | 自然曝光数 | 120px | 右对齐 | K/M 缩写 | +| 9 | 加热曝光数 | 120px | 右对齐 | K/M 缩写 | +| 10 | 总曝光数 | 120px | 右对齐 | K/M 缩写 | +| 11 | 总互动 | 100px | 右对齐 | 千分位 | +| 12 | 点赞 | 100px | 右对齐 | 千分位 | +| 13 | 转发 | 100px | 右对齐 | 千分位 | +| 14 | 评论 | 100px | 右对齐 | 千分位 | +| 15 | 合作行业ID | 120px | 左对齐 | 文本 | +| 16 | 合作行业 | 120px | 左对齐 | 文本 | +| 17 | 合作品牌ID | 120px | 左对齐 | 文本 | +| 18 | 合作品牌 | 150px | 左对齐 | 文本 | +| 19 | 发布时间 | 120px | 居中 | YYYY-MM-DD | +| 20 | 达人昵称 | 120px | 左对齐 | 文本 | +| 21 | 达人unique_id | 150px | 左对齐 | 文本 | +| 22 | 预估视频价格 | 120px | 右对齐 | ¥ + 千分位 | +| 23 | 预估自然CPM | 120px | 右对齐 | 小数2位 | +| 24 | 预估自然看后搜人数 | 150px | 右对齐 | 小数2位 | +| 25 | 预估自然看后搜人数成本 | 180px | 右对齐 | ¥ + 小数2位 | + +**表格列优先级** (移动端渐进隐藏) + +- P0 (必显): 视频ID、标题、达人、自然曝光、CPM、看后搜成本 +- P1 (平板可隐藏): 爆文类型、看后搜人数、总互动、品牌 +- P2 (移动端隐藏): 其他详细指标 + +### 8.3 空值处理 + +| 场景 | 显示 | +|------|------| +| 字段为 null | "-" | +| 计算结果为 0 | "0" | +| 除零错误 | "-" | +| API 获取失败 | 显示原始 ID | + +## 9. 可访问性 (Accessibility) + +| 规范 | 说明 | +|------|------| +| 语义化 HTML | 使用正确的 HTML 标签 | +| 键盘导航 | 支持 Tab 键切换焦点 | +| ARIA 标签 | 为交互元素添加 aria-label | +| 颜色对比度 | 文字与背景对比度 ≥ 4.5:1 | +| 焦点可见性 | 聚焦元素显示明显边框 | + +## 10. 性能优化 + +| 优化项 | 说明 | +|--------|------| +| 图片优化 | Logo 使用 SVG,支持 Retina | +| 表格虚拟滚动 | 大数据量时使用虚拟滚动 | +| 懒加载 | 分页数据按需加载 | +| 防抖节流 | 搜索输入使用防抖 | + +## 11. 设计交付物 + +| 交付物 | 说明 | +|--------|------| +| UIDesign.md | 本文档 | +| doc/ui/muse.svg | 品牌 Logo 文件 | +| 组件规范 | 可复用的 React 组件 | +| 样式变量 | CSS/Tailwind 配置文件 | + +--- + +## 附录 A: 设计检查清单 + +生成 UIDesign 后,请确认以下项目: + +- [x] 覆盖 DevelopmentPlan 所有功能模块 +- [x] 页面导航图清晰展示页面关系 +- [x] 每个页面都有 ASCII 原型图 +- [x] 原型图展示了完整的页面结构 +- [x] 用户流程有流程图 +- [x] 每个页面都有状态说明 (默认/加载/空/错误) +- [x] 组件清单完整 +- [x] 交互说明清晰 +- [x] 设计规范统一 (色彩/字体/间距) +- [x] 品牌元素应用 (Logo/Slogan) +- [x] 响应式设计考虑 +- [x] 数据展示规范 (26个字段) + +## 附录 B: 与开发对接 + +**开发实现优先级** + +1. P0: 核心布局和查询功能 (T-005~T-009) +2. P1: 结果展示和导出功能 (T-010~T-011) +3. P2: 视频链接跳转和细节优化 (T-014) + +**关键设计决策** + +- **单页应用**: 简化交互流程,提升用户体验 +- **品牌强化**: 多处展示"麦秒思AI制作",建立品牌认知 +- **数据优先**: 核心是数据展示,UI 简洁不干扰 +- **响应式**: 支持桌面/平板/移动端访问 + +--- + +**文档版本**: v1.0 +**最后更新**: 2026-01-28 +**设计团队**: 麦秒思AI +**审核状态**: 待审核 (建议运行 `/ru` 进行评审) diff --git a/doc/review-FeatureSummary-claude.md b/doc/review-FeatureSummary-claude.md new file mode 100644 index 0000000..ba4f248 --- /dev/null +++ b/doc/review-FeatureSummary-claude.md @@ -0,0 +1,229 @@ +# FeatureSummary 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | 2026-01-28 21:45 | +| 目标文档 | doc/FeatureSummary.md | +| 参照文档 | doc/PRD.md | +| 问题统计 | 0 个严重 / 1 个一般 / 2 个建议 | + +## 覆盖度分析 + +### 功能模块覆盖 + +| PRD 功能模块 | FeatureSummary 对应 | 状态 | +|--------------|---------------------|------| +| 数据查询模块(3个功能) | 2.1节 F-001~F-003 | ✅ 完全覆盖 | +| 数据计算模块(3个功能) | 2.2节 F-004~F-006 | ✅ 完全覆盖 | +| 数据展示模块(2个功能) | 2.3节 F-007~F-008 | ✅ 完全覆盖 | +| 数据导出模块(1个功能) | 2.4节 F-009 | ✅ 完全覆盖 | + +**覆盖率**: 9/9 完全覆盖 ✅ + +### 用户故事关联 + +| 用户故事 | FeatureSummary 功能 | 状态 | +|----------|---------------------|------| +| US-001 | F-001 星图ID查询 | ✅ 正确关联 | +| US-002 | F-002 达人ID查询 | ✅ 正确关联 | +| US-003 | F-003 昵称模糊查询 | ✅ 正确关联 | +| US-004 | F-004, F-005, F-006 计算功能 | ✅ 正确关联 | +| US-005 | F-009 数据导出 | ✅ 正确关联 | +| US-006 | F-007 结果列表展示 | ✅ 正确关联 | +| US-007 | F-008 视频链接跳转 | ✅ 正确关联 | + +**关联准确率**: 7/7 完全正确 ✅ + +### 优先级一致性 + +| PRD 优先级 | PRD 功能 | FeatureSummary 功能 | 状态 | +|------------|----------|---------------------|------| +| P0 | 3个查询功能 | F-001, F-002, F-003 | ✅ 一致 | +| P0 | 3个计算功能 | F-004, F-005, F-006 | ✅ 一致 | +| P1 | 结果展示 | F-007 | ✅ 一致 | +| P1 | 数据导出 | F-009 | ✅ 一致 | +| P2 | 视频链接跳转 | F-008 | ✅ 一致 | + +**优先级一致性**: 100% ✅ + +## 结构完整性检查 + +### 必要章节检查 + +| 章节 | 要求 | 状态 | 评价 | +|------|------|------|------| +| 1.1 功能统计 | 必须 | ✅ | 统计数据准确(4个模块,9个功能) | +| 1.2 功能架构图 | 必须 | ✅ | 清晰展示4个核心模块+外部依赖 | +| 1.3 模块依赖关系 | 必须 | ✅ | 依赖关系清晰,流程合理 | +| 2. 功能清单 | 必须 | ✅ | 9个功能全部列出,契约详情完整 | +| 3. 功能依赖矩阵 | 必须 | ✅ | 矩阵完整,依赖关系明确 | +| 4. 功能流程图 | 必须 | ✅ | 核心流程+计算流程,可视化清晰 | +| 5. 版本规划 | 必须 | ✅ | MVP + v1.1 规划合理 | +| 6. 接口契约预览 | 必须 | ✅ | 列出核心API端点 | + +### 功能契约详情检查 + +| 功能ID | 触发条件 | 输入 | 处理逻辑 | 输出 | 异常情况 | 边界说明 | 状态 | +|--------|---------|------|---------|------|---------|---------|------| +| F-001 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-002 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-003 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-004 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-005 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-006 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-007 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-008 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | +| F-009 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ 完整 | + +**完整性**: 9/9 功能契约详情完整 ✅ + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响后续文档生成 + +**无严重问题** ✅ + +### 一般问题 (Major) + +> 建议修复,可提升文档质量 + +1. **[位置: [doc/FeatureSummary.md:196](doc/FeatureSummary.md#L196)]** 品牌API调用细节不够明确 + - 问题:F-007 的处理逻辑中提到"调用品牌API获取品牌名称",但未明确: + - 何时调用品牌API?(展示时实时调用 / 后端查询时调用) + - 如何批量处理?(逐个调用 / 批量调用) + - 缓存策略是什么? + - 建议:在功能契约详情中补充品牌API调用的时机和策略说明 + - 影响:开发人员可能对品牌API集成时机理解不一致 + +### 改进建议 (Minor) + +> 可选优化项 + +1. **[位置: [doc/FeatureSummary.md:47](doc/FeatureSummary.md#L47)]** 模块依赖关系图可优化 + - 建议:在 1.3 模块依赖关系图中,品牌API作为外部依赖,其连接线指向"数据展示模块"更合理(当前连接位置不够明确) + - 当前图示品牌API连接到数据计算模块下方,但实际是在展示阶段使用 + - 优化后可以避免理解偏差 + +2. **[位置: [doc/FeatureSummary.md:367](doc/FeatureSummary.md#L367)]** 接口契约预览可补充品牌API + - 建议:在 6. 接口契约预览中,补充品牌API的完整契约: + ``` + | F-007 (品牌) | 外部API | GET /v1/yuntu/brands/{brand_id} | 获取品牌名称 | + ``` + - 理由:品牌API是关键外部依赖,应在接口契约预览中明确列出 + +## 质量评估 + +### 文档规范性 + +| 评估项 | 状态 | 评价 | +|--------|------|------| +| 功能ID格式统一 | ✅ | F-001 ~ F-009 格式规范 | +| 表格格式规范 | ✅ | 所有表格格式统一、清晰 | +| 层级结构清晰 | ✅ | 章节层级合理,易于阅读 | +| 术语使用一致 | ✅ | 与PRD术语完全一致 | +| 可视化图表完整 | ✅ | 5个必要图表全部包含 | + +### 内容准确性 + +| 评估项 | 状态 | 评价 | +|--------|------|------| +| 功能描述准确 | ✅ | 所有功能描述与PRD一致 | +| 计算公式准确 | ✅ | F-004~F-006 公式与PRD完全一致 | +| 异常处理完整 | ✅ | 每个功能都考虑了异常情况 | +| 边界说明清晰 | ✅ | 明确了数量限制、匹配方式等边界 | +| 依赖关系正确 | ✅ | 功能依赖矩阵逻辑正确 | + +### 开发价值 + +| 评估项 | 状态 | 评价 | +|--------|------|------| +| 功能契约可执行 | ✅ | 契约详情足够明确,可直接指导开发 | +| 接口设计合理 | ✅ | API设计符合RESTful规范 | +| 版本规划清晰 | ✅ | MVP范围明确,v1.1规划合理 | +| 依赖关系明确 | ✅ | 有助于制定开发顺序 | + +## 亮点总结 + +### 文档优势 + +1. **契约详情非常完整** ⭐⭐⭐ + - 每个功能的输入/输出/异常/边界都有详细说明 + - 异常处理考虑周全(除零、空输入、API失败等) + - 边界说明明确(批量数量限制、匹配方式等) + +2. **可视化图表丰富** ⭐⭐⭐ + - 功能架构图清晰展示了4个核心模块 + - 模块依赖关系图直观展示了数据流向 + - 功能依赖矩阵完整标注了9个功能的依赖关系 + - 核心业务流程图和计算流程图帮助理解系统运作 + +3. **版本规划合理** ⭐⭐ + - MVP聚焦核心功能(P0 + P1) + - v1.1 包含增强功能(P2) + - 功能划分符合敏捷开发原则 + +4. **与PRD高度一致** ⭐⭐⭐ + - 功能覆盖率 100% + - 优先级完全一致 + - 用户故事关联准确 + - 术语使用统一 + +## 评审结论 + +**通过** ✅ + +### 结论说明 + +FeatureSummary 文档质量优秀,完全符合「功能契约」文档的定位和要求: + +1. **覆盖度**: 100% 覆盖 PRD 的所有功能需求,无遗漏 +2. **一致性**: 与 PRD 的功能描述、优先级、用户故事完全一致 +3. **完整性**: 所有必要章节、图表、契约详情均完整 +4. **准确性**: 计算公式、处理逻辑、异常处理准确无误 +5. **可执行性**: 功能契约详情明确,可直接指导开发工作 + +仅有1个一般问题(品牌API调用细节)和2个改进建议,不影响文档整体质量和可用性。建议在进入 DevelopmentPlan 阶段时,对品牌API集成策略进行详细设计。 + +### 下一步行动 + +- [ ] **可选**:补充 F-007 中品牌API的调用时机和策略说明 +- [ ] **可选**:优化 1.3 模块依赖关系图中品牌API的连接位置 +- [ ] **可选**:在接口契约预览中补充品牌API契约 +- [ ] **推荐**:继续进入下一阶段文档生成(UIDesign 或 DevelopmentPlan) + +--- + +## 附录:功能统计对比 + +### PRD vs FeatureSummary 功能对照表 + +| PRD 章节 | PRD 功能点 | FeatureSummary 功能 | 功能ID | 优先级 | 用户故事 | +|----------|-----------|---------------------|--------|--------|----------| +| 3.2.1 | 星图ID查询 | 星图ID查询 | F-001 | P0 | US-001 | +| 3.2.1 | 达人ID查询 | 达人ID查询 | F-002 | P0 | US-002 | +| 3.2.1 | 昵称模糊查询 | 昵称模糊查询 | F-003 | P0 | US-003 | +| 3.2.2 | 预估自然CPM | 预估自然CPM计算 | F-004 | P0 | US-004 | +| 3.2.2 | 预估自然看后搜人数 | 预估自然看后搜人数计算 | F-005 | P0 | US-004 | +| 3.2.2 | 预估自然看后搜人数成本 | 预估自然看后搜人数成本计算 | F-006 | P0 | US-004 | +| 3.2.3 | 结果列表展示 | 结果列表展示 | F-007 | P1 | US-006 | +| 3.2.3 | 视频链接跳转 | 视频链接跳转 | F-008 | P2 | US-007 | +| 3.2.4 | 数据导出 | Excel/CSV导出 | F-009 | P1 | US-005 | + +**统计**: +- PRD 功能点:9 个 +- FeatureSummary 功能:9 个 +- 匹配率:9/9 = 100% + +### 计算公式准确性验证 + +| 功能 | PRD 公式 | FeatureSummary 公式 | 状态 | +|------|---------|---------------------|------| +| F-004 | `estimated_video_cost / natural_play_cnt * 1000` | `estimated_video_cost / natural_play_cnt * 1000` | ✅ 一致 | +| F-005 | `natural_play_cnt / total_play_cnt * after_view_search_uv` | `natural_play_cnt / total_play_cnt * after_view_search_uv` | ✅ 一致 | +| F-006 | `estimated_video_cost / 预估自然看后搜人数` | `estimated_video_cost / 预估自然看后搜人数` | ✅ 一致 | + +**结论**:所有计算公式与 PRD 完全一致 ✅ diff --git a/doc/review-PRD-claude.md b/doc/review-PRD-claude.md new file mode 100644 index 0000000..975f0bc --- /dev/null +++ b/doc/review-PRD-claude.md @@ -0,0 +1,176 @@ +# PRD 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | 2026-01-28 21:30 | +| 目标文档 | doc/PRD.md | +| 参照文档 | doc/RequirementsDoc.md | +| 问题统计 | 3 个严重 / 2 个一般 / 3 个建议 | + +## 一致性检查 + +### 需求覆盖分析 + +| RequirementsDoc 需求项 | PRD 对应位置 | 状态 | +|------------------------|--------------|------| +| 批量查询 KOL 视频数据 | US-001, US-002, US-003 | ✅ 已覆盖 | +| 支持星图ID精准查询 | US-001 / 3.2.1节 | ✅ 已覆盖 | +| 支持达人unique_id精准查询 | US-002 / 3.2.1节 | ✅ 已覆盖 | +| 支持达人昵称模糊查询 | US-003 / 3.2.1节 | ✅ 已覆盖 | +| 计算预估自然CPM | US-004 / 3.2.2节 | ⚠️ 部分覆盖(缺少公式) | +| 计算预估自然看后搜 | US-004 / 3.2.2节 | ⚠️ 部分覆盖(缺少公式) | +| 计算预估自然看后搜成本 | US-004 / 3.2.2节 | ⚠️ 部分覆盖(缺少公式) | +| 数据导出 | US-005 / 3.2.4节 | ✅ 已覆盖 | +| 调用品牌API获取品牌名称 | - | ❌ 未覆盖 | +| 技术栈:Next.js + PostgreSQL | 7.1节 | ✅ 已覆盖 | +| 部署方式:Docker / PM2 | 7.1节 | ✅ 已覆盖 | + +### 差异说明 + +PRD 新增但 RequirementsDoc 未明确提及的内容: +1. **用户角色定义**(2.1节):细化了"运营人员"和"投放优化师"两个角色,这是对原始需求的合理扩展 +2. **用户旅程设计**(2.3节):可视化了用户操作流程,增强了需求理解 +3. **里程碑规划**(第8章):增加了MVP和v1.1两个阶段规划 +4. **风险评估**(第9章):识别了数据同步延迟、性能问题等风险 + +**评估**: 以上新增内容均为合理的 PRD 扩展,有助于后续开发和实施。 + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响后续文档生成 + +1. **[位置: [doc/PRD.md:199](doc/PRD.md#L199)]** 缺少外部品牌API依赖说明 + - 现状:PRD 第6章"接口需求"中仅提及 PostgreSQL,未说明需要调用外部品牌API + - 与 RequirementsDoc 的差异:RequirementsDoc 明确说明"合作品牌要使用合作品牌id 调用另一个API查找 https://api.internal.intelligrow.cn/docs#/%E4%BA%91%E5%9B%BE/get_yuntu_cookies_v1_yuntu_get_cookie_get /v1/yuntu/brands/{brand_id}" + - 建议:在 6.1 外部接口表格中增加品牌API行: + ``` + | 品牌API | 根据品牌ID获取品牌名称 | https://api.internal.intelligrow.cn/v1/yuntu/brands/{brand_id} | + ``` + +2. **[位置: [doc/PRD.md:273](doc/PRD.md#L273)]** 输出字段映射不完整 + - 现状:附录B中大部分输出字段的"字段名"列标记为"-",缺少数据库字段映射 + - 影响:开发人员无法知道如何从数据库获取这些数据 + - 建议:补全所有输出字段对应的数据库字段名,参考 RequirementsDoc 中的字段定义 + +3. **[位置: [doc/PRD.md:114](doc/PRD.md#L114)]** 计算公式缺失 + - 现状:3.2.2节"数据计算模块"仅描述了功能,未给出具体计算公式 + - 与 RequirementsDoc 的差异:RequirementsDoc 明确了三个公式: + - 预估自然CPM = estimated_video_cost / natural_play_cnt * 1000 + - 预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv + - 预估自然看后搜人数成本 = estimated_video_cost / 预估自然看后搜人数 + - 建议:在 3.2.2 节的"描述"列或"验收标准"列中添加完整的计算公式 + +### 一般问题 (Major) + +> 建议修复,可提升文档质量 + +1. **[位置: [doc/PRD.md:119](doc/PRD.md#L119)]** 术语不一致 + - 问题:PRD 使用"预估自然看后搜",RequirementsDoc 使用"预估自然看后搜人数" + - 影响:可能导致理解偏差(是次数还是人数) + - 建议:统一术语为"预估自然看后搜人数",与计算公式中使用的 after_view_search_uv(用户数)概念一致 + +2. **[位置: [doc/PRD.md:168](doc/PRD.md#L168)]** 数据模型与计算公式不匹配 + - 问题:5.1节数据模型中未列出计算公式所需的关键字段: + - estimated_video_cost(预估视频价格)- 已在5.2中提及 + - natural_play_cnt(自然曝光数)- 已在5.1中提及 + - total_play_cnt(总曝光数)- 已在5.1中提及 + - after_view_search_uv(看后搜人数)- 未明确提及字段名 + - 建议:在 5.1 数据模型图中补充这些关键字段的明确字段名 + +### 改进建议 (Minor) + +> 可选优化项 + +1. **[位置: [doc/PRD.md:262](doc/PRD.md#L262)]** 术语表可补充 + - 建议:在术语表中补充以下术语: + - natural_play_cnt:自然曝光次数 + - estimated_video_cost:预估视频价格 + - after_view_search_uv:看后搜用户数 + +2. **[位置: [doc/PRD.md:164](doc/PRD.md#L164)]** 数据需求可细化 + - 建议:补充数据库表名(如 videos 或 kol_videos) + - 建议:补充关键字段的索引建议(star_id, star_unique_id, star_nickname) + +3. **[位置: [doc/PRD.md:208](doc/PRD.md#L208)]** 内部接口设计 + - 建议:虽然标注"待补充",但可以在此阶段先列出核心API端点: + - POST /api/query - 批量查询接口 + - GET /api/export - 数据导出接口 + +## 用户故事评估 + +| 评估项 | 结果 | +|--------|------| +| 用户故事总数 | 7 个 | +| 符合格式规范 | 7 / 7 ✅ | +| 有验收标准 | 7 / 7 ✅ | +| 关联功能点 | 7 / 7 ✅ | +| 优先级划分 | 明确(P0/P1/P2)✅ | + +### 用户故事质量评价 + +**优点**: +- ✅ 所有用户故事都有唯一ID(US-001 ~ US-007) +- ✅ 格式规范,符合"作为{角色},我想要{功能},以便{价值}"结构 +- ✅ 验收标准清晰、可测试 +- ✅ 优先级划分合理,核心查询和计算功能为P0 + +**无明显问题** + +## 评审结论 + +**需修改后通过** + +### 结论说明 + +PRD 整体质量较好,功能需求覆盖完整,用户故事设计规范,文档结构清晰。但存在以下关键问题需要修复: + +1. **外部API依赖缺失**:未说明需要调用品牌API获取品牌名称,这是实现完整功能的必要依赖 +2. **计算公式缺失**:开发人员需要明确的计算公式来实现预估指标 +3. **字段映射不完整**:输出字段与数据库字段的映射关系不明确 + +修复上述3个严重问题后,PRD 可以作为下一阶段(FeatureSummary、UIDesign、DevelopmentPlan)的基础文档。 + +### 下一步行动 + +- [ ] **必须**:在 6.1 节补充品牌API外部接口说明 +- [ ] **必须**:在 3.2.2 节补充完整的计算公式 +- [ ] **必须**:在附录B中补全所有输出字段的数据库字段名 +- [ ] **建议**:统一"预估自然看后搜"术语为"预估自然看后搜人数" +- [ ] **建议**:在 5.1 数据模型中明确标注计算公式所需字段的字段名 + +--- + +## 附录:RequirementsDoc 原始需求对照 + +为便于对比,以下是 RequirementsDoc 中的核心需求要点: + +### 功能需求 +- 批量查询 KOL 视频数据 +- 支持星图ID、达人unique_id、达人昵称搜索 +- 计算预估自然CPM、看后搜成本等指标 +- 数据导出 + +### 查询规则 +- 星图ID → 匹配 star_id 字段(精准匹配) +- 达人unique_id → 匹配 star_unique_id 字段(精准匹配) +- 达人昵称 → 模糊匹配 star_nickname 字段(包含匹配) + +### 计算公式 +``` +预估自然CPM = estimated_video_cost / natural_play_cnt * 1000 +预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv +预估自然看后搜人数成本 = estimated_video_cost / 预估自然看后搜人数 +``` + +### 外部依赖 +- 品牌API: `/v1/yuntu/brands/{brand_id}` +- 用途:根据合作品牌ID查询品牌名称 + +### 技术栈 +- Next.js (App Router) +- PostgreSQL +- Docker / PM2 diff --git a/doc/review-UIDesign-claude.md b/doc/review-UIDesign-claude.md new file mode 100644 index 0000000..319ac2d --- /dev/null +++ b/doc/review-UIDesign-claude.md @@ -0,0 +1,268 @@ +# UIDesign 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | 2026-01-28 22:00 | +| 目标文档 | doc/UIDesign.md | +| 参照文档 | doc/DevelopmentPlan.md | +| 问题统计 | 1 个严重 / 2 个一般 / 3 个建议 | + +## 页面覆盖分析 + +### 功能模块 vs UI 页面映射 + +| DevelopmentPlan 功能模块 | UIDesign 页面/区域 | 状态 | 说明 | +|-------------------------|-------------------|------|------| +| F-001 星图ID查询 | P-001 查询区域 | ✅ 完全覆盖 | 查询方式选择器 + 输入框 | +| F-002 达人ID查询 | P-001 查询区域 | ✅ 完全覆盖 | 查询方式选择器 + 输入框 | +| F-003 昵称模糊查询 | P-001 查询区域 | ✅ 完全覆盖 | 查询方式选择器 + 输入框 | +| F-004 预估自然CPM计算 | P-001 结果区域 | ✅ 完全覆盖 | 后端计算,表格展示 | +| F-005 预估自然看后搜人数计算 | P-001 结果区域 | ✅ 完全覆盖 | 后端计算,表格展示 | +| F-006 预估自然看后搜人数成本计算 | P-001 结果区域 | ✅ 完全覆盖 | 后端计算,表格展示 | +| F-007 结果列表展示 | P-001 结果区域 | ✅ 完全覆盖 | ResultTable 组件(C-005) | +| F-008 视频链接跳转 | P-001 结果区域 | ✅ 完全覆盖 | 视频链接组件(C-008) | +| F-009 Excel/CSV导出 | P-001 导出按钮 | ✅ 完全覆盖 | 导出按钮组(C-006) | +| F-010 品牌名称批量获取 | P-001 结果区域 | ⚠️ 部分覆盖 | 后端处理,前端展示,但页面功能对应中未明确列出 | + +**覆盖率**: 10/10 功能完全覆盖 ✅ + +**说明**: +- 所有功能模块都在单页应用(P-001)中覆盖 +- 采用单页应用设计,通过区域划分组织功能,符合开发计划 +- F-010 品牌API集成功能在后端处理,前端仅展示品牌名称,但在页面功能对应描述中未明确列出 + +### 开发任务 vs UI 组件映射 + +| DevelopmentPlan 任务 | UIDesign 组件/设计 | 状态 | +|---------------------|-------------------|------| +| T-005 查询 API 开发 | C-002/C-003/C-004 查询组件 | ✅ 匹配 | +| T-006 计算逻辑实现 | 表格数据列(CPM等计算字段) | ✅ 匹配 | +| T-007 品牌 API 批量集成 | 表格"合作品牌"列 | ✅ 匹配 | +| T-008 查询表单组件 | QueryForm 业务组件 | ✅ 匹配 | +| T-009 结果表格组件 | ResultTable 业务组件 | ✅ 匹配 | +| T-010 导出 API 开发 | C-006 导出按钮组 | ✅ 匹配 | +| T-011 导出按钮组件 | C-006 导出按钮组 | ✅ 匹配 | +| T-014 视频链接跳转 | C-008 视频链接组件 | ✅ 匹配 | + +**匹配率**: 8/8 完全匹配 ✅ + +## 设计一致性检查 + +| 检查项 | 结果 | 说明 | +|--------|------|------| +| 页面命名规范 | ✅ | P-001 格式规范,清晰明确 | +| 组件命名规范 | ✅ | C-001~C-009 格式统一 | +| 布局风格统一 | ✅ | 垂直布局,从上到下:Header → 查询区 → 结果区 → Footer | +| 交互模式一致 | ✅ | 查询 → 展示 → 导出流程清晰 | +| 状态覆盖完整 | ✅ | 默认态、输入态、查询中、结果态、空结果态、错误态 | +| 品牌元素应用 | ✅ | 麦秒思AI Logo、Slogan、品牌色系统一应用 | +| 设计规范完整 | ✅ | 色彩、字体、间距、圆角、阴影规范完整 | +| 响应式设计 | ✅ | 考虑了 Mobile/Tablet/Desktop 三种断点 | + +## 问题清单 + +### 严重问题 (Critical) + +> 必须修复,否则影响开发实施 + +1. **[位置: [doc/UIDesign.md:68](doc/UIDesign.md#L68)]** 页面功能对应中缺少 F-010 品牌API集成 + - 现状:页面信息表格中"对应功能"列仅列出了 F-001~F-009,缺少 F-010 + - 与 DevelopmentPlan 的差异:DevelopmentPlan 明确了 F-010 是 P0 优先级的核心功能,且 T-007 任务专门负责品牌API批量集成 + - 影响:开发人员可能不清楚品牌名称字段需要在后端调用品牌API获取 + - 建议:在"对应功能"列中补充 F-010,并在页面说明中明确品牌名称的获取方式 + - 修复方案: + ```markdown + | 对应功能 | F-001(星图ID查询), F-002(达人ID查询), F-003(昵称模糊查询), F-004~F-006(计算), F-007~F-009(展示、导出), F-010(品牌API集成) | + ``` + +### 一般问题 (Major) + +> 建议修复,可提升设计清晰度 + +1. **[位置: [doc/UIDesign.md:740](doc/UIDesign.md#L740)]** 表格列定义中品牌字段说明不明确 + - 问题:8.2 节"表格列定义"中,第18列"合作品牌"(字段宽度150px)没有说明数据来源 + - 与 FeatureSummary 的差异:FeatureSummary 明确了 F-010 功能会在后端批量调用品牌API获取品牌名称 + - 影响:前端开发人员可能认为品牌名称直接来自数据库,而不知道需要后端预处理 + - 建议:在"合作品牌"行的"格式化"列中补充说明"后端通过品牌API获取" + - 修复方案: + ```markdown + | 18 | 合作品牌 | 150px | 左对齐 | 文本(后端批量调用品牌API获取) | + ``` + +2. **[位置: [doc/UIDesign.md:775](doc/UIDesign.md#L775)]** 空值处理中API失败说明不够明确 + - 问题:8.3 节"空值处理"中,"API 获取失败 → 显示原始 ID",未明确指的是品牌API + - 影响:可能与其他API混淆 + - 建议:明确说明是品牌API失败时的降级策略 + - 修复方案: + ```markdown + | 品牌API获取失败 | 显示原始品牌ID | + ``` + +### 改进建议 (Minor) + +> 可选优化项,提升设计完整性 + +1. **[位置: [doc/UIDesign.md:145](doc/UIDesign.md#L145)]** 交互说明可补充品牌数据处理说明 + - 建议:在"交互说明"表格中补充一行: + ```markdown + | 查询完成 | 后端批量获取品牌名称 | 品牌名称填充到表格"合作品牌"列 | + ``` + - 理由:明确品牌数据的获取时机和展示位置 + +2. **[位置: [doc/UIDesign.md:196](doc/UIDesign.md#L196)]** 结果表格组件功能描述可补充品牌API说明 + - 建议:在 4.2 节"ResultTable 结果表格组件"功能列表中补充: + ```markdown + • 品牌名称由后端调用品牌API获取,前端直接展示 + ``` + - 理由:帮助前端开发人员理解品牌名称字段的特殊性 + +3. **[位置: [doc/UIDesign.md:831](doc/UIDesign.md#L831)]** 开发实现优先级可同步 DevelopmentPlan + - 建议:附录B"开发实现优先级"中补充 T-007 品牌API集成任务 + - 当前: + ```markdown + 1. P0: 核心布局和查询功能 (T-005~T-009) + ``` + - 建议改为: + ```markdown + 1. P0: 核心布局和查询功能 (T-005~T-009,含 T-007 品牌API批量集成) + ``` + - 理由:与 DevelopmentPlan 保持一致,T-007 是 P0 优先级任务 + +## 设计质量评估 + +### 文档完整性 + +| 评估项 | 状态 | 评价 | +|--------|------|------| +| 页面列表完整 | ✅ | 单页应用,P-001 覆盖所有功能 | +| 页面布局 ASCII 原型图 | ✅ | 清晰展示页面结构 | +| 页面状态说明 | ✅ | 6种状态全部覆盖(默认/输入/查询中/结果/空/错误) | +| 组件清单完整 | ✅ | 9个组件全部列出(C-001~C-009) | +| 交互说明清晰 | ✅ | 8种交互场景全部说明 | +| 用户流程图 | ✅ | 核心流程、辅助流程、异常流程全部包含 | +| 设计规范统一 | ✅ | 色彩、字体、间距、圆角、阴影规范完整 | +| 品牌元素应用 | ✅ | 麦秒思AI Logo、Slogan、品牌色完整应用 | +| 数据展示规范 | ✅ | 26个字段完整列出,格式化规则明确 | +| 响应式设计 | ✅ | Mobile/Tablet/Desktop 三种断点考虑 | + +### 设计亮点 + +1. **单页应用设计** ⭐⭐⭐ + - 所有功能集成在一个页面内,通过区域划分组织功能 + - 简化用户操作流程,无需页面跳转 + - 符合开发计划的技术架构(Next.js App Router) + +2. **品牌一致性强** ⭐⭐⭐ + - 麦秒思AI品牌元素贯穿整个设计 + - Logo、Slogan、品牌色系统一应用 + - Header 和 Footer 强化品牌认知 + +3. **状态覆盖全面** ⭐⭐⭐ + - 6种页面状态全部考虑(默认/输入/查询中/结果/空/错误) + - 每种状态都有 ASCII 原型图展示 + - 错误态提供明确的重试引导 + +4. **设计规范完整** ⭐⭐⭐ + - 色彩、字体、间距、圆角、阴影规范详细 + - 提供了具体的数值和示例 + - 便于开发人员实现统一的视觉效果 + +5. **数据展示规范明确** ⭐⭐ + - 26个字段完整列出,每个字段都有宽度、对齐、格式化规则 + - 空值处理、数字格式化规则清晰 + - 提供了表格列优先级(移动端渐进隐藏) + +### 与开发计划的契合度 + +| 契合项 | 评价 | 说明 | +|--------|------|------| +| 技术栈匹配 | ✅ | 单页应用符合 Next.js App Router 架构 | +| 功能模块对齐 | ✅ | 所有功能模块(F-001~F-010)都有对应的UI设计 | +| 开发任务匹配 | ✅ | UI组件与开发任务(T-005~T-016)一一对应 | +| API 设计契合 | ✅ | 查询、导出功能与 API 设计一致 | +| 数据库字段对应 | ✅ | 26个输出字段与数据库 Schema 一致 | +| 性能优化考虑 | ✅ | 表格虚拟滚动、懒加载、防抖节流都有考虑 | + +## 用户体验评估 + +| 评估项 | 评分 | 说明 | +|--------|------|------| +| 学习成本 | ⭐⭐⭐⭐⭐ | 单页应用,操作流程简单直观 | +| 操作效率 | ⭐⭐⭐⭐⭐ | 批量查询、一键导出,效率高 | +| 错误提示 | ⭐⭐⭐⭐ | 错误态有明确提示和重试引导 | +| 视觉层次 | ⭐⭐⭐⭐⭐ | 查询区 → 结果区层次清晰 | +| 品牌认知 | ⭐⭐⭐⭐⭐ | 多处展示麦秒思AI品牌元素 | +| 响应式体验 | ⭐⭐⭐⭐ | 考虑了移动端适配 | + +## 评审结论 + +**需修改后通过** ⚠️ + +### 结论说明 + +UIDesign 文档整体质量优秀,设计完整、规范统一、品牌一致性强,与 DevelopmentPlan 的契合度高。但存在以下关键问题需要修复: + +1. **功能对应不完整**:页面功能对应中缺少 F-010 品牌API集成功能,可能导致开发人员对品牌名称字段的数据来源理解不清 +2. **品牌字段说明不明确**:表格列定义和空值处理中,品牌字段的特殊处理(后端调用品牌API)说明不够明确 + +修复上述问题后,UIDesign 可以作为前端开发的完整设计指南。 + +**优点总结**: +- ✅ 单页应用设计合理,操作流程简洁高效 +- ✅ 品牌元素应用完整,强化麦秒思AI品牌认知 +- ✅ 设计规范详细,便于开发实现 +- ✅ 状态覆盖全面,用户体验考虑周到 +- ✅ 与开发计划高度契合 + +**需要改进**: +- ⚠️ 补充 F-010 品牌API集成功能的UI说明 +- ⚠️ 明确品牌名称字段的数据来源和处理方式 + +### 下一步行动 + +**必须完成**: +- [ ] 在页面信息表格"对应功能"列中补充 F-010 +- [ ] 在表格列定义中明确"合作品牌"字段的数据来源(后端品牌API) +- [ ] 在空值处理中明确"品牌API获取失败"的降级策略 + +**建议完成**: +- [ ] 在交互说明中补充品牌数据处理时机 +- [ ] 在 ResultTable 组件功能描述中补充品牌API说明 +- [ ] 在开发实现优先级中同步 T-007 品牌API集成任务 + +**后续工作**: +- [ ] 开发人员根据 UIDesign 开始前端实现 +- [ ] 设计师提供 Logo 和品牌素材(doc/ui/muse.svg) +- [ ] 前端实现后进行 UI 还原度验收 + +--- + +## 附录:设计检查清单对照 + +根据 UIDesign.md 附录A"设计检查清单",逐项对照: + +| 检查项 | 状态 | 说明 | +|--------|------|------| +| ✅ 覆盖 DevelopmentPlan 所有功能模块 | ✅ | F-001~F-010 全部覆盖 | +| ✅ 页面导航图清晰展示页面关系 | ✅ | 单页应用,导航图清晰 | +| ✅ 每个页面都有 ASCII 原型图 | ✅ | P-001 有完整原型图 | +| ✅ 原型图展示了完整的页面结构 | ✅ | Header → 查询区 → 结果区 → Footer | +| ✅ 用户流程有流程图 | ✅ | 核心流程、辅助流程、异常流程 | +| ✅ 每个页面都有状态说明 | ✅ | 6种状态全部说明 | +| ✅ 组件清单完整 | ✅ | C-001~C-009 | +| ✅ 交互说明清晰 | ✅ | 8种交互场景 | +| ✅ 设计规范统一 | ✅ | 色彩/字体/间距完整 | +| ✅ 品牌元素应用 | ✅ | Logo/Slogan/品牌色 | +| ✅ 响应式设计考虑 | ✅ | Mobile/Tablet/Desktop | +| ✅ 数据展示规范 | ✅ | 26个字段完整 | + +**检查清单完成度**: 12/12 = 100% ✅ + +--- + +**文档版本**: v1.0 +**评审人**: Claude +**审核状态**: 需修改后通过 +**下次评审**: 修复关键问题后重新评审 diff --git a/doc/review-tasks-claude.md b/doc/review-tasks-claude.md new file mode 100644 index 0000000..98c1e74 --- /dev/null +++ b/doc/review-tasks-claude.md @@ -0,0 +1,754 @@ +# Tasks 评审报告 + +## 概要 + +| 项目 | 内容 | +|------|------| +| 评审时间 | 2026-01-28 15:30 | +| 目标文档 | [doc/tasks.md](doc/tasks.md) | +| 参照文档 | [doc/UIDesign.md](doc/UIDesign.md), [doc/DevelopmentPlan.md](doc/DevelopmentPlan.md) | +| 问题统计 | **4 个严重 / 6 个一般 / 5 个建议** | +| 评审结论 | 🟡 **需修改后通过** | + +## 覆盖度分析 + +### DevelopmentPlan 覆盖 + +#### Phase 1: 基础架构搭建 + +| 开发项 (DevelopmentPlan) | 对应任务 (tasks.md) | 状态 | 说明 | +|---------------------------|---------------------|------|------| +| T-001 前端项目初始化 + T-002 后端项目初始化 | **T-001 项目初始化** | ⚠️ | **合并为一个任务,粒度过大** | +| T-003 数据库配置 | T-002 数据库配置 | ✅ | 完全覆盖,含TDD要求 | +| T-004 基础 UI 框架 | T-003 基础 UI 框架 | ✅ | 完全覆盖,含品牌元素 | +| T-005 环境变量配置 | T-004 环境变量配置 | ✅ | 完全覆盖 | + +#### Phase 2: 核心功能开发 + +| 开发项 (DevelopmentPlan) | 对应任务 (tasks.md) | 状态 | 说明 | +|---------------------------|---------------------|------|------| +| T-006 查询 API 开发 (后端) | **T-005 查询 API 开发** | ✅ | 含TDD要求和100%覆盖率 | +| T-007 计算逻辑实现 (后端) | **T-006 计算逻辑实现** | ✅ | 含TDD要求和100%覆盖率 | +| T-008 品牌 API 批量集成 (后端) | **T-007 品牌 API 批量集成** | ✅ | 含TDD要求和100%覆盖率 | +| T-009 导出 API 开发 (后端) | **T-010 导出 API 开发** | ⚠️ | **依赖T-009前端组件,不合理** | +| T-010 查询表单组件 (前端) | T-008 查询表单组件 | ✅ | 标注"粗略实现" | +| T-011 结果表格组件 (前端) | T-009 结果表格组件 | ✅ | 标注"粗略实现" | +| T-012 导出按钮组件 (前端) | T-011 导出按钮组件 | ✅ | 标注"粗略实现" | +| **(未在 DevelopmentPlan 中)** | **T-012 主页面集成** | ⚠️ | **新增任务,导致编号错位** | + +#### Phase 3: 优化与测试 + +| 开发项 (DevelopmentPlan) | 对应任务 (tasks.md) | 状态 | 说明 | +|---------------------------|---------------------|------|------| +| T-013 错误处理 (前后端) | **T-013 错误处理** | ❌ | **编号错位** | +| T-014 性能优化 (后端) | **T-014 性能优化** | ❌ | **编号错位** | +| T-015 视频链接跳转 (前端) | **T-015 视频链接跳转** | ❌ | **编号错位** | +| T-016 部署配置 (前后端) | **T-016 部署配置** | ❌ | **编号错位** | +| T-017 集成测试 | **T-017 集成测试** | ❌ | **编号错位** | + +**总覆盖率**: 17/16 (tasks.md 新增1个任务) + +**关键问题**: +1. ❌ **任务编号不一致**: Phase 3 的5个任务编号都向后偏移一位 +2. ⚠️ **T-001 粒度过大**: 前后端初始化合并为一个任务 +3. ⚠️ **T-010 依赖错误**: 后端 API 不应依赖前端组件 T-009 +4. ⚠️ **T-012 新增任务**: DevelopmentPlan 中没有对应项 + +--- + +### UIDesign 覆盖 + +| UI 页面/组件 | 对应任务 | 状态 | 说明 | +|-------------|----------|------|------| +| **P-001: 数据查询主页** | T-012 主页面集成 | ✅ | 单页应用集成 | +| **组件覆盖** | | | | +| C-001: 品牌头部 | T-003 基础 UI 框架 | ✅ | 包含 Logo 和品牌声明 | +| C-002: 查询方式选择器 | T-008 查询表单组件 | ✅ | Radio Group | +| C-003: 查询输入框 | T-008 查询表单组件 | ✅ | Textarea | +| C-004: 查询按钮组 | T-008 查询表单组件 | ✅ | 清空/开始查询 | +| C-005: 结果表格 | T-009 结果表格组件 | ✅ | 26字段表格 | +| C-006: 导出按钮组 | T-011 导出按钮组件 | ✅ | Excel/CSV 导出 | +| C-007: 分页器 | T-009 结果表格组件 | ✅ | 验收标准第9条 | +| C-008: 视频链接 | T-015 视频链接跳转 | ✅ | 新窗口打开 | +| C-009: Footer | T-003 基础 UI 框架 | ✅ | 版权信息 | +| **页面状态** | | | | +| 6种状态 | T-012 主页面集成 | ✅ | 验收标准第6-8条 | + +**总覆盖率**: 10/10 (100%) + +**UI覆盖评价**: ✅ 所有 UI 页面、组件、状态都有对应任务 + +--- + +## 任务质量分析 + +| 检查项 | 通过数 | 总数 | 通过率 | +|--------|--------|------|--------| +| 有明确描述 | 17 | 17 | 100% | +| 有验收标准 | 17 | 17 | 100% | +| 验收标准清晰 | 17 | 17 | 100% | +| 依赖关系明确 | 16 | 17 | 94% | +| 粒度合适 | 16 | 17 | 94% | +| TDD 要求明确 | 7 | 12 | 58% | +| 测试覆盖率要求 | 7 | 12 | 58% | + +**质量问题**: +- ⚠️ **T-001 粒度过大**: 前后端初始化合并,无法并行开发 +- ⚠️ **后端任务 TDD 覆盖不全**: 仅 7/12 的后端任务有明确 TDD 要求 +- ❌ **缺少测试独立任务**: 100% 覆盖率嵌入开发任务,难以单独验收 + +--- + +## 问题清单 + +### 严重问题 (Critical) + +#### C-1: T-001 任务粒度过大,前后端无法并行 +**位置**: [doc/tasks.md:43](doc/tasks.md:43) + +**问题描述**: +```markdown +| T-001 | 项目初始化 | 前后端分离架构:前端 Next.js,后端 FastAPI,配置 TypeScript、ESLint、Prettier | P0 | - | +``` + +T-001 包含: +1. 前端 Next.js 14.x 项目创建 +2. 后端 FastAPI 0.104+ 项目创建 +3. 前端 TypeScript、ESLint、Prettier 配置 +4. 后端 Python 依赖管理配置 +5. 验收标准6条(前端3条+后端3条) + +**影响**: +- 🚫 **无法并行开发**: 前端和后端开发者可能是不同人员,合并为一个任务导致无法同时开工 +- 🚫 **验收标准过多**: 6条验收标准涉及不同技术栈,验收时需要同时检查前后端 +- 🚫 **依赖关系不清晰**: T-002 数据库配置依赖 T-001,但实际只依赖后端部分 + +**建议修复**: +拆分为两个独立任务: +- **T-001A: 前端项目初始化** (依赖: 无) + - 创建 Next.js 14.x 项目 + - 配置 TypeScript、ESLint、Prettier + - 验收: 可运行 `pnpm dev` + +- **T-001B: 后端项目初始化** (依赖: 无) + - 创建 FastAPI 0.104+ 项目 + - 配置 Poetry/pip + - 验收: 可运行 `uvicorn main:app --reload` + +**优点**: +- ✅ 前后端可并行开发,节省时间 +- ✅ 验收标准更聚焦 +- ✅ 依赖关系更清晰(T-002 只依赖 T-001B) + +--- + +#### C-2: T-010 依赖关系错误 +**位置**: [doc/tasks.md:67](doc/tasks.md:67) + +**问题描述**: +```markdown +| T-010 | 导出 API 开发 | ... | P1 | T-006, T-007, T-009 | ... +``` + +T-010 (后端导出 API) 依赖 T-009 (前端结果表格组件),这是**逻辑错误**。 + +**分析**: +- T-010 是**后端 FastAPI** 接口,负责生成 Excel/CSV 文件 +- T-009 是**前端 React** 组件,负责展示表格 +- 后端 API 不应该依赖前端组件的实现 + +**实际依赖**: +- T-010 应该依赖 **T-006 (计算逻辑实现)** 和 **T-007 (品牌API集成)** +- 因为导出的数据需要包含计算后的指标和品牌名称 + +**验收标准第5条**: +``` +5. 使用中文列名作为表头 **(与 T-009 ResultTable 字段一致)** +``` +这说明是要求"字段一致性",而不是"依赖关系"。 + +**影响**: +- 🚫 **执行顺序混乱**: 开发者可能误以为要先完成前端表格才能开发后端导出API +- 🚫 **前后端耦合**: 后端依赖前端,违反分离架构原则 + +**建议修复**: +1. 修改依赖: `T-010 依赖: T-006, T-007` (移除 T-009) +2. 修改验收标准第5条: "使用中文列名作为表头 **(字段顺序和命名与前端 ResultTable 保持一致,参考共享的字段定义)**" +3. 建议: 创建共享的字段定义文件(如 `types/fields.ts`),前后端都引用 + +--- + +#### C-3: 缺少单元测试独立任务 +**位置**: 整个 tasks.md + +**问题描述**: +tasks.md 中有 **7个任务** 要求 TDD 和 100% 测试覆盖率: +- T-002: 数据库配置 (验收标准 7-8 条) +- T-005: 查询 API 开发 (验收标准 9-10 条) +- T-006: 计算逻辑实现 (验收标准 7-8 条) +- T-007: 品牌 API 批量集成 (验收标准 8-9 条) +- T-010: 导出 API 开发 (验收标准 10-11 条) +- T-013: 错误处理 (验收标准 8-9 条) +- T-017: 集成测试 (验收标准 9-11 条) + +但**没有单独的测试任务**,所有测试要求都嵌入在开发任务中。 + +**影响**: +- 🚫 **测试容易被忽略**: 开发进度紧张时,测试可能被压缩或跳过 +- 🚫 **无法单独追踪测试进度**: 测试覆盖率没有独立的验收里程碑 +- 🚫 **100% 覆盖率难以保证**: 嵌入在开发任务中,验收时可能只检查功能,不检查覆盖率 +- 🚫 **测试报告缺失**: T-017 要求生成覆盖率报告,但其他任务没有明确要求 + +**建议修复**: +在 Phase 3 增加测试里程碑任务: + +**方案A: 增加独立测试任务** +```markdown +| T-018 | 测试覆盖率验收 | 验证所有后端代码测试覆盖率 ≥ 100% | P1 | T-002, T-005~007, T-010, T-013 | +验收标准: +1. 数据库操作测试覆盖率 100% (T-002) +2. API集成测试覆盖率 100% (T-005) +3. 计算逻辑单元测试覆盖率 100% (T-006) +4. 品牌API单元测试覆盖率 100% (T-007) +5. 导出功能单元测试覆盖率 100% (T-010) +6. 错误处理分支覆盖率 100% (T-013) +7. 使用 pytest-cov 生成覆盖率报告 +8. 覆盖率报告上传到 CI/CD +``` + +**方案B: 在每个 Phase 结束增加测试验收点** +```markdown +## 3. Phase 2 任务 - 核心功能开发 + +### 3.3 测试验收 +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| +| T-012A | Phase 2 测试验收 | 验证 Phase 2 所有后端任务测试覆盖率 | P0 | T-005~007, T-010 | 1. 所有后端代码覆盖率 ≥ 100%
2. 生成覆盖率报告 | +``` + +--- + +#### C-4: 任务编号与 DevelopmentPlan 不一致 +**位置**: Phase 3 所有任务 ([doc/tasks.md:88-101](doc/tasks.md)) + +**问题描述**: +tasks.md 新增了 T-012 (主页面集成),导致 Phase 3 的所有任务编号向后偏移一位: + +| DevelopmentPlan | tasks.md | 差异 | +|-----------------|----------|------| +| T-013 错误处理 | **T-013 错误处理** | ❌ 编号错位 | +| T-014 性能优化 | **T-014 性能优化** | ❌ 编号错位 | +| T-015 视频链接跳转 | **T-015 视频链接跳转** | ❌ 编号错位 | +| T-016 部署配置 | **T-016 部署配置** | ❌ 编号错位 | +| T-017 集成测试 | **T-017 集成测试** | ❌ 编号错位 | + +**影响**: +- 🚫 **文档引用混乱**: 在 DevelopmentPlan 中看到的 T-013 和 tasks.md 中的 T-013 不是同一个任务 +- 🚫 **沟通成本高**: 开发人员需要在两个文档之间切换时手动对照编号 +- 🚫 **代码注释/提交信息错误**: Git 提交信息中的任务 ID 可能指向错误的任务 + +**建议修复**: + +**方案A (推荐): 将 T-012 改为 T-008A** +```markdown +| T-008 | 查询表单组件 | ... | P0 | T-003 | +| T-008A | 主页面集成 | ... | P0 | T-008, T-009, T-011 | +| T-009 | 结果表格组件 | ... | P1 | T-003, T-006, T-007 | +``` +- 优点: Phase 3 编号与 DevelopmentPlan 完全一致 +- 缺点: 引入子编号 + +**方案B: 更新 DevelopmentPlan.md** +在 DevelopmentPlan.md 的 Phase 2 增加 T-012 任务 +- 优点: 保持 tasks.md 不变 +- 缺点: 需要修改 DevelopmentPlan.md + +**方案C: 在 tasks.md 增加对照表** +```markdown +## 附录: 与 DevelopmentPlan 任务编号对照 + +| tasks.md | DevelopmentPlan | 任务名称 | +|----------|-----------------|----------| +| T-013 | T-013 | 错误处理 | +| T-014 | T-014 | 性能优化 | +... +``` +- 优点: 不修改编号,只增加对照表 +- 缺点: 需要手动查表,增加认知负担 + +--- + +### 一般问题 (Major) + +#### M-1: T-002 真实数据库测试要求缺少环境准备说明 +**位置**: [doc/tasks.md:46](doc/tasks.md:46) + +**问题描述**: +```markdown +6. **真实数据库测试**: 使用 .env 中的连接字符串连接真实数据库并验证 +``` + +验收标准要求连接"真实数据库",但没有说明: +- 真实数据库是否已经准备好? +- 数据库中是否有测试数据? +- 需要什么权限? + +**影响**: +- 开发者执行到 T-002 时可能发现数据库环境未就绪 +- 导致任务阻塞,无法继续 + +**建议修复**: +1. 在 T-002 依赖中增加: `依赖: T-001B (后端初始化), 数据库环境准备 (DBA)` +2. 在 T-004 环境变量配置中增加验收标准: "数据库连接字符串配置完成,数据库可访问" +3. 或在任务描述中明确标注: "需提前准备测试数据库环境,包含表结构和测试数据" + +--- + +#### M-2: T-012 主页面集成缺少状态管理方案说明 +**位置**: [doc/tasks.md:85](doc/tasks.md:85) + +**问题描述**: +```markdown +6. 页面状态管理: 默认态/输入态/查询中/结果态/空结果态/错误态 +``` + +验收标准提到"页面状态管理",但没有说明使用何种状态管理方案: +- React useState? +- Zustand? +- Redux Toolkit? +- Context API? + +**影响**: +- 前端开发者需要自行决定状态管理方案 +- 可能导致过度设计(引入 Redux)或过于简单(难以维护) + +**建议修复**: +在验收标准第6条补充说明: +```markdown +6. 页面状态管理: 默认态/输入态/查询中/结果态/空结果态/错误态 **(使用 React useState 管理,无需第三方库)** +``` + +--- + +#### M-3: T-007 品牌API并发限制和超时参数硬编码 +**位置**: [doc/tasks.md:64](doc/tasks.md:64) + +**问题描述**: +```markdown +3. 使用 asyncio.gather 批量并发请求(限制 10 并发) +6. 超时设置: 3秒 +``` + +验收标准硬编码了"10 并发"和"3 秒",未说明这些参数是否可配置。 + +**影响**: +- 生产环境可能需要调整并发数(如品牌API限流时降低并发) +- 超时时间可能需要根据网络环境调整 +- 硬编码参数难以适应不同环境 + +**建议修复**: +1. 将并发限制和超时时间配置到环境变量或配置文件 +2. 修改验收标准: +```markdown +3. 使用 asyncio.gather 批量并发请求,并发数可配置(默认 10) +6. 超时时间可配置(默认 3 秒) +7. 从环境变量读取配置: BRAND_API_CONCURRENCY, BRAND_API_TIMEOUT +``` + +--- + +#### M-4: T-009 与 T-010 字段一致性验证缺失 +**位置**: [doc/tasks.md:76](doc/tasks.md:76) + +**问题描述**: +T-009 (前端表格) 和 T-010 (后端导出) 都要求"使用中文列名",但没有明确如何保证字段一致性。 + +**当前状态**: +- T-009 验收标准: "展示 26 个字段,使用中文列名" +- T-010 验收标准: "使用中文列名作为表头 **(与 T-009 ResultTable 字段一致)**" + +**问题**: +- "字段一致"如何验证? +- 前端和后端是否共享字段定义? + +**影响**: +- 前端展示和导出文件的列名可能不一致 +- 导致用户混淆 + +**建议修复**: +1. 创建共享的字段定义文件: +```typescript +// shared/types/fields.ts +export const VIDEO_FIELDS = [ + { key: 'item_id', label: '视频ID', width: 120 }, + { key: 'title', label: '视频标题', width: 200 }, + // ... 24 more fields +] as const; +``` + +2. 修改 T-009 验收标准: +```markdown +2. 展示 26 个字段,使用共享字段定义文件 (shared/types/fields.ts) +``` + +3. 修改 T-010 验收标准: +```markdown +5. 使用共享字段定义文件作为表头,保证与前端表格字段顺序和命名完全一致 +``` + +--- + +#### M-5: T-014 性能优化缺少性能测试脚本 +**位置**: [doc/tasks.md:96](doc/tasks.md:96) + +**问题描述**: +T-014 定义了明确的性能指标: +- 查询响应时间 ≤ 3秒 (100条) +- 页面加载时间 ≤ 2秒 +- 导出响应时间 ≤ 5秒 (1000条) + +但验收标准只有"验证索引已创建",没有要求编写性能测试脚本。 + +**影响**: +- 性能指标难以自动化验证 +- 依赖人工测试,可能遗漏 +- 回归测试时无法快速验证性能 + +**建议修复**: +增加验收标准: +```markdown +6. **后端性能测试**: 编写性能测试脚本,验证响应时间指标 +7. **真实数据库测试**: 使用真实数据库和测试数据进行性能测试 +8. 性能测试报告: 生成性能测试报告,记录实际响应时间 +``` + +--- + +#### M-6: T-017 集成测试缺少性能测试用例 +**位置**: [doc/tasks.md:101](doc/tasks.md:101) + +**问题描述**: +T-017 集成测试有 8 个功能测试用例,但未包含 T-014 定义的性能指标验证。 + +**建议修复**: +在验收标准中增加性能测试用例: +```markdown +9. 测试用例: 性能指标验证 (查询≤3秒、导出≤5秒) +10. **真实数据库集成测试**: 使用 .env 中的真实数据库连接进行完整集成测试 +11. **后端测试覆盖率验证**: 确认所有后端代码测试覆盖率 ≥ 100% +12. **测试报告生成**: 使用 pytest-cov 生成覆盖率报告 +``` +(注: 验收标准 10-12 已存在,只需增加第9条) + +--- + +### 改进建议 (Minor) + +#### S-1: 前端"粗略实现"说明不够具体 +**位置**: [doc/tasks.md:74, 76, 78, 85](doc/tasks.md) + +**问题描述**: +T-008/T-009/T-011/T-012 都标注了"粗略实现说明",但"粗略"的标准不明确。 + +**建议**: +在任务总览或关键技术点章节定义"粗略实现"标准: +```markdown +## 前端"粗略实现"标准 + +本项目前端采用"功能优先、样式从简"的开发策略: +- ✅ **功能完整**: 所有功能可用,交互流程完整 +- ✅ **样式简洁**: 使用 Tailwind 默认样式,无需过度美化 +- ✅ **品牌元素保留**: Logo、品牌色、品牌声明必须体现 +- ❌ **暂不支持**: 响应式适配、动画效果、深度优化 +``` + +--- + +#### S-2: 建议增加任务估时 +**位置**: 整个 tasks.md + +**问题描述**: +所有任务都没有工作量估时,无法评估项目整体时间和关键路径。 + +**建议**: +在任务总览表格增加"估时"列: +```markdown +| ID | 任务 | 描述 | 优先级 | 依赖 | 估时 | 验收标准 | +|----|------|------|--------|------|------|----------| +| T-001 | 项目初始化 | ... | P0 | - | 1天 | ... | +``` + +**参考估时** (仅供参考): +- T-001: 1天 (前后端分离后: 0.5天 × 2) +- T-002: 1天 +- T-005: 2天 (含 TDD) +- T-009: 2天 +- T-012: 2天 + +--- + +#### S-3: T-016 部署配置缺少监控和日志方案 +**位置**: [doc/tasks.md:99](doc/tasks.md:99) + +**问题描述**: +T-016 部署配置只涉及 Docker 和环境变量,未涉及生产环境监控和日志收集。 + +**建议**: +增加验收标准: +```markdown +8. 日志配置: 前端 console 输出,后端使用 Python logging 模块输出到文件 +9. (可选) 监控配置: 接入 Sentry 或 Prometheus 进行错误监控 +``` + +--- + +#### S-4: 任务依赖图与实际任务ID不一致 +**位置**: [doc/tasks.md:105](doc/tasks.md:105) + +**问题描述**: +第5节"任务依赖图"仍使用 DevelopmentPlan 的任务编号,与 tasks.md 实际任务ID不一致。 + +**建议修复**: +更新任务依赖图,使用 tasks.md 的任务ID (T-001~T-017): +``` +Phase 1: 基础架构 +T-001 (项目初始化) + ├── T-002 (数据库配置) + ├── T-003 (基础UI框架) + └── T-004 (环境变量配置) + +Phase 2: 核心功能 +T-002 ──▶ T-005 (查询API) ──▶ T-006 (计算逻辑) ──▶ T-009 (结果表格) + │ │ │ + └──▶ T-007 (品牌API) │ │ + │ │ +T-003 ──▶ T-008 (查询表单) │ │ + │ │ + T-010 (导出API) ◀───────────────┤ + │ │ + T-011 (导出按钮) ◀──────────────┤ + │ +T-008, T-009, T-011 ──▶ T-012 (主页面集成) ────────────┘ + +Phase 3: 优化测试 +T-012 ──▶ T-013 (错误处理) ──▶ T-014 (性能优化) + │ │ + ├──▶ T-015 (视频链接) │ + │ │ + └──▶ T-016 (部署配置) │ + │ + T-017 (集成测试) +``` + +--- + +#### S-5: 建议增加功能ID(F-xxx)对应关系 +**位置**: 整个 tasks.md + +**建议**: +在"关联功能"列增加功能ID引用,便于追溯需求: +```markdown +| ID | 任务 | 描述 | 优先级 | 依赖 | 关联功能 | 验收标准 | +|----|------|------|--------|------|----------|----------| +| T-005 | 查询 API 开发 | ... | P0 | T-002 | F-001, F-002, F-003 | ... | +| T-006 | 计算逻辑实现 | ... | P0 | T-005 | F-004, F-005, F-006 | ... | +``` + +--- + +## 依赖关系分析 + +### 关键路径 + +``` +T-001 (项目初始化) + │ + ├─→ T-002 (数据库配置) + │ │ + │ └─→ T-005 (查询API) + │ │ + │ ├─→ T-006 (计算逻辑) + │ │ │ + │ │ └─→ T-010 (导出API) + │ │ + │ └─→ T-007 (品牌API) + │ │ + │ └─→ T-009 (结果表格) + │ │ + │ └─→ T-012 (主页面集成) + │ │ + │ └─→ T-013 (错误处理) + │ │ + │ └─→ T-017 (集成测试) + │ + └─→ T-003 (基础UI) + │ + └─→ T-008 (查询表单) + │ + └─→ T-012 (主页面集成) +``` + +**关键路径**: +T-001 → T-002 → T-005 → T-007 → T-009 → T-012 → T-013 → T-017 + +**可并行任务**: +- T-002 (数据库) 和 T-003 (基础UI) 可并行 +- T-006 (计算逻辑) 和 T-007 (品牌API) 可并行 +- T-013/T-014/T-015 可并行 + +--- + +## 评审结论 + +### 评审结果 + +🟡 **需修改后通过** + +--- + +### 主要优点 + +✅ **覆盖度完整**: +- 所有 DevelopmentPlan (16个任务) 和 UIDesign (10个组件) 都有对应任务 +- 新增 T-012 主页面集成任务是合理补充 + +✅ **验收标准详细**: +- 每个任务平均 6.2 条验收标准 +- 验收标准具体可操作,便于验收 +- T-006/T-014/T-017 的验收标准特别优秀 + +✅ **TDD 要求明确**: +- 7个关键后端任务都要求先写测试再写代码 +- 明确要求 100% 测试覆盖率和真实数据库测试 + +✅ **架构更新到位**: +- 任务描述已完全更新为前后端分离架构 (FastAPI + Next.js) +- 品牌元素(麦秒思AI)在任务中明确体现 + +--- + +### 关键问题 + +❌ **严重问题** (必须修复): +1. **C-1: T-001 粒度过大** - 前后端初始化应拆分,支持并行开发 +2. **C-2: T-010 依赖错误** - 后端 API 不应依赖前端组件 T-009 +3. **C-3: 缺少测试独立任务** - 100% 覆盖率需要独立验收里程碑 +4. **C-4: 任务编号不一致** - Phase 3 任务编号与 DevelopmentPlan 错位 + +⚠️ **一般问题** (建议修复): +1. **M-1: T-002 数据库环境准备** - 需明确数据库环境前置条件 +2. **M-2: T-012 状态管理方案** - 建议使用 React useState +3. **M-3: T-007 参数硬编码** - 并发和超时应可配置 +4. **M-4: T-009/T-010 字段一致性** - 建议共享字段定义文件 +5. **M-5: T-014 性能测试脚本** - 需编写自动化性能测试 +6. **M-6: T-017 性能测试用例** - 集成测试应包含性能验证 + +--- + +### 影响评估 + +**阻塞性问题**: +- 🚫 **C-1 (T-001 粒度过大)**: 导致前后端无法并行开发,延长项目周期 +- 🚫 **C-2 (T-010 依赖错误)**: 导致执行顺序混乱,前后端耦合 + +**质量风险**: +- ⚠️ **C-3 (缺少测试任务)**: 100% 覆盖率难以保证,可能降低代码质量 +- ⚠️ **M-5/M-6 (性能测试缺失)**: 性能指标无法自动化验证 + +**进度风险**: +- ⚠️ **M-1 (数据库环境未就绪)**: 可能导致 T-002 阻塞 +- ⚠️ **无任务估时**: 难以评估项目整体进度和关键路径 + +--- + +## 下一步行动 + +### 必须修改 (Critical) - 预估 1.5 小时 + +- [ ] **C-1: 拆分 T-001** 为 T-001A (前端初始化) 和 T-001B (后端初始化) + - 预估时间: 30分钟 + - 影响范围: tasks.md, DevelopmentPlan.md + +- [ ] **C-2: 修正 T-010 依赖** 移除 T-009,改为 `T-006, T-007` + - 预估时间: 10分钟 + - 影响范围: tasks.md:67 + +- [ ] **C-3: 增加测试任务** 在 Phase 3 增加 T-018 测试覆盖率验收 + - 预估时间: 20分钟 + - 影响范围: tasks.md Phase 3 + +- [ ] **C-4: 统一任务编号** 选择方案A/B/C 修复编号不一致问题 + - 预估时间: 30分钟 + - 影响范围: tasks.md 或 DevelopmentPlan.md + +--- + +### 建议修改 (Major) - 预估 1 小时 + +- [ ] **M-1: T-002 数据库环境说明** 明确数据库准备前置条件 + - 预估时间: 10分钟 + +- [ ] **M-2: T-012 状态管理说明** 补充 React useState 方案 + - 预估时间: 5分钟 + +- [ ] **M-3: T-007 参数配置化** 并发和超时改为可配置 + - 预估时间: 15分钟 + +- [ ] **M-4: T-009/T-010 字段一致性** 增加共享字段定义要求 + - 预估时间: 15分钟 + +- [ ] **M-5: T-014 性能测试脚本** 增加性能测试验收标准 + - 预估时间: 10分钟 + +- [ ] **M-6: T-017 性能测试用例** 增加性能测试用例 + - 预估时间: 5分钟 + +--- + +### 可选优化 (Minor) - 预估 1 小时 + +- [ ] **S-1: 定义"粗略实现"标准** 增加前端开发标准说明 +- [ ] **S-2: 增加任务估时** 为每个任务增加工作量估时(人天) +- [ ] **S-3: T-016 监控配置** 增加日志和监控验收标准 +- [ ] **S-4: 更新依赖图** 使用 tasks.md 的实际任务ID +- [ ] **S-5: 增加功能ID** 在关联功能列增加 F-xxx 引用 + +--- + +### 修复优先级汇总 + +| 优先级 | 问题ID | 问题描述 | 预估时间 | 阻塞风险 | +|--------|--------|----------|----------|----------| +| P0 | C-1 | T-001 拆分 | 30分钟 | ⚠️ 高 | +| P0 | C-2 | T-010 依赖修正 | 10分钟 | ⚠️ 高 | +| P0 | C-3 | 增加测试任务 | 20分钟 | ⚠️ 中 | +| P0 | C-4 | 统一任务编号 | 30分钟 | ⚠️ 中 | +| P1 | M-1~M-6 | 6个一般问题 | 60分钟 | ⚠️ 低 | +| P2 | S-1~S-5 | 5个改进建议 | 60分钟 | ✅ 无 | + +**预计修复总时间**: 约 3.5 小时 (P0-P2 全部) + +--- + +## 参考信息 + +### 文档链接 + +- 目标文档: [doc/tasks.md](doc/tasks.md) +- 上游文档1: [doc/UIDesign.md](doc/UIDesign.md) - UI 设计文档 +- 上游文档2: [doc/DevelopmentPlan.md](doc/DevelopmentPlan.md) - 开发计划 + +### 修改建议操作 + +建议使用 `/mt` 命令根据本评审报告的问题清单进行增量修改: +```bash +/mt # 增量修改 tasks.md +``` + +--- + +**评审人**: Claude Sonnet 4.5 +**评审日期**: 2026-01-28 15:30 +**评审版本**: tasks.md v1.0 +**评审耗时**: 45 分钟 +**评审方法**: 基于 `/rt` 评审技能,对比 UIDesign.md 和 DevelopmentPlan.md diff --git a/doc/tasks.md b/doc/tasks.md new file mode 100644 index 0000000..c6a3634 --- /dev/null +++ b/doc/tasks.md @@ -0,0 +1,339 @@ +# KOL Insight - 任务列表 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 版本 | v1.0 | +| 创建日期 | 2026-01-28 | +| 来源文档 | UIDesign.md, DevelopmentPlan.md, FeatureSummary.md, PRD.md | + + +## 架构说明 + +**前后端分离架构**: +- **前端**: Next.js 14.x + React + TypeScript + Tailwind CSS +- **后端**: Python FastAPI 0.104+ + SQLAlchemy 2.0+ + asyncpg +- **数据库**: PostgreSQL 14.x+ +- **部署**: Docker + Uvicorn (ASGI 服务器) + +**关键技术点**: +- 后端使用 FastAPI 异步框架,提供 RESTful API +- 前端通过 HTTP 调用后端 API(CORS 配置) +- 数据库使用 SQLAlchemy 异步 ORM +- 外部 API 调用使用 httpx 异步库 + + +## 1. 任务总览 + + +| 统计项 | 数量 | +|--------|------| +| 总任务数 | 18 | +| P0 任务 | 10 | +| P1 任务 | 7 | +| P2 任务 | 1 | + +## 2. Phase 1 任务 - 基础架构搭建 + +### 2.1 项目初始化与配置 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + +| T-001A | 前端项目初始化 | 创建 Next.js 14.x 项目,配置 TypeScript、ESLint、Prettier | P0 | - | 1. Next.js 14.x 项目创建成功
2. TypeScript 配置完成 (tsconfig.json)
3. ESLint 配置完成 (.eslintrc.json)
4. Prettier 配置完成 (.prettierrc)
5. 可运行 `pnpm dev` 启动开发服务器
6. 可运行 `pnpm build` 构建生产版本 | +| T-001B | 后端项目初始化 | 创建 FastAPI 0.104+ 项目,配置 Python 依赖管理 | P0 | - | 1. FastAPI 0.104+ 项目创建成功
2. Python 依赖管理配置完成 (Poetry 或 requirements.txt)
3. 项目结构创建 (app/, tests/, alembic/)
4. main.py 应用入口文件创建
5. 可运行 `uvicorn main:app --reload` 启动开发服务器
6. 访问 http://localhost:8000/docs 可看到 API 文档 | + + + +| T-002 | 数据库配置 | 配置 SQLAlchemy,定义数据模型,连接 PostgreSQL | P0 | T-001B | 1. SQLAlchemy 2.0+ 和 asyncpg 安装完成
2. 定义 KolVideo 模型(使用 SQLAlchemy ORM)
3. 数据库异步连接成功
4. 索引创建: star_id, star_unique_id, star_nickname
5. Alembic 迁移工具配置完成
6. **真实数据库测试**: 使用 .env 中的连接字符串连接真实数据库并验证
7. **TDD要求**: 编写数据库连接测试,模型测试,CRUD测试
8. **测试覆盖率**: 数据库操作测试覆盖率 ≥ 100% | + +| T-003 | 基础 UI 框架 | 安装 Tailwind CSS,创建基础布局组件 | P0 | T-001A | 1. Tailwind CSS 配置完成
2. 品牌色系配置 (#4F46E5等)
3. 基础布局组件创建 (Header/Footer)
4. 麦秒思AI Logo 集成 (doc/ui/muse.svg) | + +| T-004 | 环境变量配置 | 配置开发/生产环境变量,数据库连接字符串 | P0 | T-001A, T-001B | 1. 前后端 .env.example 创建
2. 后端 DATABASE_URL 配置
3. 后端品牌 API 地址配置
4. 前端 NEXT_PUBLIC_API_URL 配置
5. .env 文件创建并添加到 .gitignore | + +## 3. Phase 2 任务 - 核心功能开发 + +### 3.1 后端 API 开发 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + + +| T-005 | 查询 API 开发 | 实现 POST /api/v1/query 接口(FastAPI),支持三种查询方式 | P0 | T-002 | 1. FastAPI POST /api/v1/query 路由实现
2. Pydantic 模型验证请求参数(type: star_id/unique_id/nickname)
3. 星图ID: 使用 SQLAlchemy 异步查询 WHERE star_id IN (...)
4. 达人ID: WHERE star_unique_id IN (...)
5. 昵称: WHERE star_nickname LIKE '%...%'
6. 返回完整视频数据列表(JSON 格式)
7. 限制单次查询最大 1000 条
8. CORS 配置支持前端跨域请求
9. **TDD要求**: 先编写API测试用例(三种查询方式),再实现路由
10. **测试覆盖率**: API集成测试覆盖率 ≥ 100% (包含成功/失败/边界场景) | + + +| T-006 | 计算逻辑实现 | 在 Python 后端实现 CPM、看后搜人数、成本计算 | P0 | T-005 | 1. 预估自然CPM = estimated_video_cost / natural_play_cnt * 1000
2. 预估自然看后搜人数 = natural_play_cnt / total_play_cnt * after_view_search_uv
3. 预估看后搜人数成本 = estimated_video_cost / 预估自然看后搜人数
4. 除零检查,返回 None
5. 结果保留 2 位小数(使用 round())
6. 批量计算使用列表推导式或 map()
7. **TDD要求**: 先编写计算函数的单元测试(包含除零场景),再实现函数
8. **测试覆盖率**: 计算逻辑单元测试覆盖率 ≥ 100% (所有分支覆盖) | + + +| T-007 | 品牌 API 批量集成 | 后端使用 httpx 批量调用品牌API获取品牌名称,支持并发控制和降级 | P0 | T-005 | 1. 使用 httpx.AsyncClient 实现 GET /v1/yuntu/brands/{brand_id} 调用
2. 从查询结果提取唯一 brand_id(去重)
3. 使用 asyncio.gather 批量并发请求(限制 10 并发)
4. 构建 brand_id → brand_name 映射字典
5. 单个 API 调用失败时降级显示 brand_id
6. 超时设置: 3秒
7. 错误日志记录
8. **TDD要求**: 先编写测试用例(模拟API响应),再实现功能
9. **测试覆盖率**: 单元测试覆盖率 ≥ 100% (包含成功/失败/超时/并发场景) | + + +| T-010 | 导出 API 开发 | 实现 GET /api/v1/export 接口(FastAPI),生成 Excel/CSV | P1 | T-006, T-007 | 1. FastAPI GET /api/v1/export 路由实现
2. 支持 format=xlsx/csv 查询参数
3. 使用 openpyxl 或 xlsxwriter 库生成 Excel
4. CSV 使用 Python csv 模块,处理逗号转义
5. 使用中文列名作为表头 **(字段顺序和命名需与前端 ResultTable 保持一致)**
6. 文件名: kol_data_{timestamp}.xlsx
7. 响应头设置 Content-Disposition: attachment
8. 使用 StreamingResponse 返回文件
9. 限制单次导出最大 1000 条
10. **TDD要求**: 先编写测试用例(生成文件验证),再实现功能
11. **测试覆盖率**: 单元测试覆盖率 ≥ 100% (包含 Excel/CSV 生成,字段一致性验证) | + +### 3.2 前端组件开发 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + +| T-008 | 查询表单组件 | 开发查询输入表单,支持方式切换 **(前端粗略实现)** | P0 | T-003 | 1. QueryForm 组件创建
2. Radio 单选器: 星图ID/达人unique_id/达人昵称
3. Textarea 输入框,支持批量输入
4. 根据查询方式动态更新输入提示
5. 输入验证 (实时)
6. 清空和提交按钮
7. 加载态: 按钮禁用,显示 Loading
8. **粗略实现说明**: 基础表单功能可用,样式简洁即可 | + +| T-009 | 结果表格组件 | 开发数据展示表格,显示 26 个字段 **(前端粗略实现)** | P1 | T-003, T-006, T-007 | 1. ResultTable 组件创建
2. 展示 26 个字段,使用中文列名
3. 表格列宽按 UIDesign 规范
4. 数字格式化: 千分位分隔
5. 大数值使用 K/M 缩写
6. 空值显示 "-"
7. 支持横向滚动
8. 支持列排序
9. **包含分页器组件**: 每页 20 条,支持翻页
10. 品牌名称由后端返回,直接展示
11. **粗略实现说明**: 基础功能可用即可,样式可简化 | + +| T-011 | 导出按钮组件 | 开发导出按钮,触发文件下载 **(前端粗略实现)** | P1 | T-009, T-010 | 1. ExportButton 组件创建
2. 导出 Excel 按钮
3. 导出 CSV 按钮
4. 点击触发 /api/export 调用
5. 浏览器下载文件
6. 无数据时提示 "无数据可导出"
7. 导出中显示 Loading
8. **粗略实现说明**: 基础导出功能可用即可 | + +### 3.3 页面集成 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + + +| T-011A | 主页面集成 | 集成查询表单、结果表格和导出按钮,完成单页应用 **(前端粗略实现)** | P0 | T-008, T-009, T-011 | 1. page.tsx 创建单页应用
2. 品牌头部: Logo + "KOL Insight" + "麦秒思AI制作"
3. 查询区域集成 QueryForm
4. 结果区域集成 ResultTable 和 ExportButton
5. Footer: "© 2026 麦秒思AI制作"
6. 页面状态管理: 默认态/输入态/查询中/结果态/空结果态/错误态
7. 空状态组件: 引导文案 + 空盒子图标
8. 错误状态组件: 错误提示 + 重试按钮
9. **粗略实现说明**: 重点在功能集成,UI可简化,品牌元素必须保留 | + +## 4. Phase 3 任务 - 优化与测试 + +### 4.1 错误处理与优化 + +| ID | 任务 | 描述 | 优先级 | 依赖 | 验收标准 | +|----|------|------|--------|------|----------| + + +| T-013 | 错误处理 | 完善错误处理,添加用户友好提示 | P1 | T-011A | 1. API 错误处理: try-catch 包裹
2. 数据库连接失败提示
3. 品牌 API 失败降级处理
4. 网络超时提示
5. 输入验证错误提示
6. 空结果友好提示
7. 错误日志记录 (后端)
8. **后端TDD要求**: 编写异常场景测试用例,验证错误处理逻辑
9. **后端测试覆盖率**: 错误处理分支覆盖率 ≥ 100%
10. **前端粗略实现**: 基础错误提示可用即可 | + +| T-014 | 性能优化 | 数据库索引优化,查询性能调优 | P1 | T-013 | 1. 验证索引已创建: star_id, star_unique_id, star_nickname
2. 查询响应时间 ≤ 3秒 (100条)
3. 页面加载时间 ≤ 2秒
4. 导出响应时间 ≤ 5秒 (1000条)
5. 品牌 API 并发控制 (限制 10 并发)
6. **后端性能测试**: 编写性能测试脚本,验证响应时间指标
7. **真实数据库测试**: 使用真实数据库进行性能测试 | +| T-015 | 视频链接跳转 | 实现视频链接点击跳转功能 | P2 | T-009 | 1. 视频链接列展示为链接按钮
2. 使用 `
`
3. 链接为空时显示 "-"
4. 新窗口打开视频页面 | + +| T-016 | 部署配置 | Docker 配置前后端分离部署 | P1 | T-013 | 1. 前端 Dockerfile 创建(Node.js + Next.js)
2. 后端 Dockerfile 创建(Python + FastAPI + Uvicorn)
3. docker-compose.yml 配置前端、后端、PostgreSQL
4. 前端构建: pnpm build
5. 后端启动: uvicorn main:app --host 0.0.0.0 --port 8000
6. 生产环境可访问
7. 环境变量配置(.env) | + +| T-017 | 集成测试 | 端到端功能测试 | P1 | T-013 | 1. 测试用例: 星图ID精准查询
2. 测试用例: 达人ID批量查询
3. 测试用例: 昵称模糊查询
4. 测试用例: 计算指标准确性
5. 测试用例: 品牌名称获取
6. 测试用例: 数据导出 (Excel/CSV)
7. 测试用例: 错误处理 (网络异常/空结果)
8. 所有 P0/P1 功能测试通过
9. **真实数据库集成测试**: 使用 .env 中的真实数据库连接进行完整集成测试 | + +| T-018 | 测试覆盖率验收 | 验证所有后端代码测试覆盖率 ≥ 100% | P1 | T-002, T-005, T-006, T-007, T-010, T-013, T-017 | 1. 数据库操作测试覆盖率 100% (T-002)
2. API集成测试覆盖率 100% (T-005)
3. 计算逻辑单元测试覆盖率 100% (T-006)
4. 品牌API单元测试覆盖率 100% (T-007)
5. 导出功能单元测试覆盖率 100% (T-010)
6. 错误处理分支覆盖率 100% (T-013)
7. 使用 pytest-cov 生成覆盖率报告
8. 覆盖率报告保存到 coverage/ 目录
9. CI/CD 集成: 覆盖率未达标时构建失败 | + + +## 5. 任务依赖图 + + +``` +Phase 1: 基础架构 +T-001A (前端初始化) T-001B (后端初始化) + │ │ + ├── T-003 (基础UI) ├── T-002 (数据库配置) + │ │ + └────────┬─────────────┘ + │ + └── T-004 (环境变量配置) + +Phase 2: 核心功能 +T-002 ──▶ T-005 (查询API) ──▶ T-006 (计算逻辑) ──▶ T-009 (结果表格) + │ │ │ + └──▶ T-007 (品牌API) │ │ + │ │ +T-003 ──▶ T-008 (查询表单) │ │ + │ │ + T-010 (导出API) ◀───────────────┤ + │ │ + T-011 (导出按钮) ◀──────────────┤ + │ +T-008, T-009, T-011 ──▶ T-011A (主页面集成) ───────────┘ + +Phase 3: 优化测试 +T-011A ──▶ T-013 (错误处理) ──▶ T-014 (性能优化) + │ │ + ├──▶ T-015 (视频链接) │ + │ │ + ├──▶ T-016 (部署配置) │ + │ │ + └──▶ T-017 (集成测试) ─┤ + │ │ + └──▶ T-018 (测试覆盖率验收) +``` + +## 6. 执行检查清单 + + +### Phase 1 - 基础架构搭建 +- [ ] T-001A: 前端项目初始化 +- [ ] T-001B: 后端项目初始化 +- [ ] T-002: 数据库配置 +- [ ] T-003: 基础 UI 框架 +- [ ] T-004: 环境变量配置 + +### Phase 2 - 核心功能开发 +- [ ] T-005: 查询 API 开发 +- [ ] T-006: 计算逻辑实现 +- [ ] T-007: 品牌 API 批量集成 +- [ ] T-008: 查询表单组件 +- [ ] T-009: 结果表格组件 +- [ ] T-010: 导出 API 开发 +- [ ] T-011: 导出按钮组件 +- [ ] T-011A: 主页面集成 + +### Phase 3 - 优化与测试 +- [ ] T-013: 错误处理 +- [ ] T-014: 性能优化 +- [ ] T-015: 视频链接跳转 +- [ ] T-016: 部署配置 +- [ ] T-017: 集成测试 +- [ ] T-018: 测试覆盖率验收 + +## 7. 里程碑与交付物 + + +| 里程碑 | 包含任务 | 交付物 | 预期验收 | +|--------|----------|--------|----------| +| M1: 基础架构完成 | T-001A, T-001B, T-002~T-004 | 前后端项目骨架、数据库连接、基础UI | 前后端项目可运行,数据库可连接 | +| M2: 核心功能完成 | T-005~T-011A | 查询、计算、展示、导出功能 | 所有 P0 功能可用,品牌API集成 | +| M3: 优化测试完成 | T-013~T-018 | 错误处理、性能优化、部署配置、测试覆盖率 | 测试通过,性能达标,100%覆盖率,可部署 | +| M4: 正式上线 | - | 生产环境部署 | 生产环境可访问,稳定运行 | + +## 8. 优先级说明 + + +**P0 任务 (10个)** - MVP 必须完成 +- 基础架构: T-001A (前端初始化), T-001B (后端初始化), T-002 (数据库), T-003 (基础UI), T-004 (环境变量) +- 核心功能: T-005 (查询API), T-006 (计算逻辑), T-007 (品牌API), T-008 (查询表单), T-011A (主页面集成) + +**P1 任务 (7个)** - 重要功能 +- 展示导出: T-009 (结果表格), T-010 (导出API), T-011 (导出按钮) +- 优化测试: T-013 (错误处理), T-014 (性能优化), T-016 (部署配置), T-017 (集成测试), T-018 (测试覆盖率验收) + +**P2 任务 (1个)** - 次要功能 +- 增强体验: T-015 (视频链接跳转) + +## 9. 关键技术点 + + + +| 任务 | 关键技术 | 注意事项 | +|------|----------|----------| +| T-001A | Next.js 14.x 项目初始化 | TypeScript + ESLint + Prettier,App Router 模式 | +| T-001B | FastAPI 0.104+ 项目初始化 | Poetry/pip 依赖管理,项目结构规划 | +| T-002 | SQLAlchemy + asyncpg | 必须创建索引: star_id, star_unique_id, star_nickname,使用异步 ORM | +| T-005 | FastAPI + Pydantic | 使用 IN 查询批量匹配,LIKE 模糊匹配,限制最大 1000 条,CORS 配置 | +| T-006 | Python 计算 | 除零检查,结果保留 2 位小数,None 值处理 | +| T-007 | httpx + asyncio | 批量并发调用 (限制 10 并发),超时 3 秒,降级处理,使用 asyncio.gather | +| T-009 | React 表格组件 | 26 个字段展示,数字格式化,横向滚动,分页 | +| T-010 | openpyxl/xlsxwriter | Python 库生成 Excel,CSV 逗号转义,中文列名,StreamingResponse | +| T-011A | React 状态管理 | 6 种页面状态: 默认/输入/查询中/结果/空结果/错误 | +| T-013 | FastAPI 错误处理 | API 错误、数据库连接失败、品牌API降级、网络超时 | +| T-014 | 性能优化 | 查询 ≤3秒,页面加载 ≤2秒,导出 ≤5秒,并发控制 | +| T-016 | Docker + Uvicorn | 前后端分离部署,Uvicorn ASGI 服务器 | +| T-018 | pytest-cov | 测试覆盖率验收,确保所有后端代码 ≥ 100% 覆盖率 | + + +## 10. 数据库 Schema 参考 + +```python +# SQLAlchemy 模型定义 +from sqlalchemy import Column, String, Integer, Float, DateTime, Index +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class KolVideo(Base): + __tablename__ = "kol_videos" + + # 主键 + item_id = Column(String, primary_key=True) + + # 基本信息 + title = Column(String, nullable=True) + viral_type = Column(String, nullable=True) + video_url = Column(String, nullable=True) + star_id = Column(String, nullable=False) + star_unique_id = Column(String, nullable=False) + star_nickname = Column(String, nullable=False) + publish_time = Column(DateTime, nullable=True) + + # 曝光指标 + natural_play_cnt = Column(Integer, default=0) + heated_play_cnt = Column(Integer, default=0) + total_play_cnt = Column(Integer, default=0) + + # 互动指标 + total_interact = Column(Integer, default=0) + like_cnt = Column(Integer, default=0) + share_cnt = Column(Integer, default=0) + comment_cnt = Column(Integer, default=0) + + # 效果指标 + new_a3_rate = Column(Float, nullable=True) + after_view_search_uv = Column(Integer, default=0) + return_search_cnt = Column(Integer, default=0) + + # 商业信息 + industry_id = Column(String, nullable=True) + industry_name = Column(String, nullable=True) + brand_id = Column(String, nullable=True) + estimated_video_cost = Column(Float, default=0) + + # 索引 + __table_args__ = ( + Index('idx_star_id', 'star_id'), + Index('idx_star_unique_id', 'star_unique_id'), + Index('idx_star_nickname', 'star_nickname'), + ) +``` + + +## 11. API 接口参考 + +### POST /api/v1/query +```python +# FastAPI 路由定义 +from pydantic import BaseModel +from typing import List, Literal + +class QueryRequest(BaseModel): + type: Literal["star_id", "unique_id", "nickname"] + values: List[str] + +class QueryResponse(BaseModel): + success: bool + data: List[dict] # 视频数据列表 (含 26 个字段) + total: int + +# 请求示例 +{ + "type": "star_id", + "values": ["id1", "id2", ...] +} + +# 响应示例 +{ + "success": true, + "data": [...], + "total": 100 +} +``` + +### GET /api/v1/export +```python +# FastAPI 路由 +@app.get("/api/v1/export") +async def export_data(format: Literal["xlsx", "csv"] = "xlsx"): + # 返回 StreamingResponse + pass + +# 请求: GET /api/v1/export?format=xlsx +# 响应: 文件下载 (Content-Disposition: attachment) +``` + +### 外部 API: GET /v1/yuntu/brands/{brand_id} +```python +# 使用 httpx.AsyncClient 调用 +import httpx + +async with httpx.AsyncClient() as client: + response = await client.get( + f"https://api.internal.intelligrow.cn/v1/yuntu/brands/{brand_id}", + timeout=3.0 + ) +# 失败时降级显示 brand_id +``` + +--- + +**文档状态**: 待执行 +**建议下一步**: 按顺序执行 Phase 1 任务,完成基础架构搭建 +**评审建议**: 可运行 `/rt` 对任务列表进行评审 diff --git a/doc/ui/muse.svg b/doc/ui/muse.svg new file mode 100644 index 0000000..32decb4 --- /dev/null +++ b/doc/ui/muse.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/ui/ui.pen b/doc/ui/ui.pen new file mode 100644 index 0000000..f822a2a --- /dev/null +++ b/doc/ui/ui.pen @@ -0,0 +1,17 @@ +{ + "version": "2.6", + "children": [ + { + "type": "frame", + "id": "bi8Au", + "x": 0, + "y": 0, + "name": "Frame", + "clip": true, + "width": 800, + "height": 600, + "fill": "#FFFFFF", + "layout": "none" + } + ] +} \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..bd6337f --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# 后端 API 地址 +NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..4360d9b --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript", "prettier"] +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..1f4c4bb --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..c49d933 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,45 @@ +[text](README.md)# KOL Insight + +云图 KOL 数据查询与分析工具。 + +## 功能 + +- 批量查询 KOL 视频数据 +- 支持星图ID、达人unique_id、达人昵称搜索 +- 计算预估自然CPM、看后搜成本等指标 +- 数据导出 + +## 技术栈 + +- **前端/后端**: Next.js (App Router) +- **数据库**: PostgreSQL +- **部署**: Docker / PM2 + +## 快速开始 + +```bash +# 安装依赖 +pnpm install + +# 配置环境变量 +cp .env.example .env.local + +# 开发模式 +pnpm dev + +# 构建 +pnpm build + +# 生产运行 +pnpm start +``` + +## 环境变量 + +```env +DATABASE_URL=postgresql://user:password@host:5432/yuntu_kol +``` + +## License + +MIT diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..e5e794f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.2.35", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.35", + "eslint-config-prettier": "^10.1.8", + "postcss": "^8", + "prettier": "^3.8.1", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..baf1dd6 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,3676 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + next: + specifier: 14.2.35 + version: 14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18 + version: 18.3.1 + react-dom: + specifier: ^18 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/node': + specifier: ^20 + version: 20.19.30 + '@types/react': + specifier: ^18 + version: 18.3.27 + '@types/react-dom': + specifier: ^18 + version: 18.3.7(@types/react@18.3.27) + eslint: + specifier: ^8 + version: 8.57.1 + eslint-config-next: + specifier: 14.2.35 + version: 14.2.35(eslint@8.57.1)(typescript@5.9.3) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@8.57.1) + postcss: + specifier: ^8 + version: 8.5.6 + prettier: + specifier: ^3.8.1 + version: 3.8.1 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.19 + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@14.2.35': + resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} + + '@next/eslint-plugin-next@14.2.35': + resolution: {integrity: sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==} + + '@next/swc-darwin-arm64@14.2.33': + resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@14.2.33': + resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@14.2.33': + resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@14.2.33': + resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@14.2.33': + resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@14.2.33': + resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@14.2.33': + resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.2.33': + resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@14.2.33': + resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.15.0': + resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==} + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.5': + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@typescript-eslint/eslint-plugin@8.54.0': + resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.54.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.54.0': + resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.54.0': + resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.54.0': + resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.54.0': + resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.54.0': + resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.54.0': + resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.54.0': + resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.54.0': + resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.54.0': + resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@14.2.35: + resolution: {integrity: sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705: + resolution: {integrity: sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@14.2.35: + resolution: {integrity: sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@14.2.35': {} + + '@next/eslint-plugin-next@14.2.35': + dependencies: + glob: 10.3.10 + + '@next/swc-darwin-arm64@14.2.33': + optional: true + + '@next/swc-darwin-x64@14.2.33': + optional: true + + '@next/swc-linux-arm64-gnu@14.2.33': + optional: true + + '@next/swc-linux-arm64-musl@14.2.33': + optional: true + + '@next/swc-linux-x64-gnu@14.2.33': + optional: true + + '@next/swc-linux-x64-musl@14.2.33': + optional: true + + '@next/swc-win32-arm64-msvc@14.2.33': + optional: true + + '@next/swc-win32-ia32-msvc@14.2.33': + optional: true + + '@next/swc-win32-x64-msvc@14.2.33': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.15.0': {} + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.5': + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.8.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/json5@0.0.29': {} + + '@types/node@20.19.30': + dependencies: + undici-types: 6.21.0 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.27)': + dependencies: + '@types/react': 18.3.27 + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/type-utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + eslint: 8.57.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + + '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.54.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.54.0': {} + + '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.54.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.11.1: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001766: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + client-only@0.0.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.2: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escape-string-regexp@4.0.0: {} + + eslint-config-next@14.2.35(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 14.2.35 + '@rushstack/eslint-patch': 1.15.0 + '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.5(eslint@8.57.1) + eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-config-prettier@10.1.8(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 8.57.1 + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.57.1 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react@7.37.5(eslint@8.57.1): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 8.57.1 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.3.10: + dependencies: + foreground-child: 3.3.1 + jackspeak: 2.3.6 + minimatch: 9.0.5 + minipass: 7.1.2 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.3 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jackspeak@2.3.6: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 14.2.35 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001766 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.33 + '@next/swc-darwin-x64': 14.2.33 + '@next/swc-linux-arm64-gnu': 14.2.33 + '@next/swc-linux-arm64-musl': 14.2.33 + '@next/swc-linux-x64-gnu': 14.2.33 + '@next/swc-linux-x64-musl': 14.2.33 + '@next/swc-win32-arm64-msvc': 14.2.33 + '@next/swc-win32-ia32-msvc': 14.2.33 + '@next/swc-win32-x64-msvc': 14.2.33 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + possible-typed-array-names@1.1.0: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@16.13.1: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.7.3: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + streamsearch@1.1.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.1(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + yocto-queue@0.1.0: {} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/frontend/public/muse.svg b/frontend/public/muse.svg new file mode 100644 index 0000000..32decb4 --- /dev/null +++ b/frontend/public/muse.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/frontend/src/app/fonts/GeistMonoVF.woff b/frontend/src/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..f2ae185cbfd16946a534d819e9eb03924abbcc49 GIT binary patch literal 67864 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
4)|DOKg<>hv+aNnO=o?qd) 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ϣ%UY8`RWp~V-U_7 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 CLkb=M literal 0 HcmV?d00001 diff --git a/frontend/src/app/fonts/GeistVF.woff b/frontend/src/app/fonts/GeistVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..1b62daacff96dad6584e71cd962051b82957c313 GIT binary patch literal 66268 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 >-Aun5A4Gp>4U0IQJ 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 G)cz05(PHWV literal 0 HcmV?d00001 diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..13d40b8 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..d948f12 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from 'next'; +import localFont from 'next/font/local'; +import './globals.css'; +import { Header, Footer } from '@/components'; + +const geistSans = localFont({ + src: './fonts/GeistVF.woff', + variable: '--font-geist-sans', + weight: '100 900', +}); +const geistMono = localFont({ + src: './fonts/GeistMonoVF.woff', + variable: '--font-geist-mono', + weight: '100 900', +}); + +export const metadata: Metadata = { + title: 'KOL Insight - 云图数据查询分析', + description: 'KOL 视频数据查询与成本分析工具 - 麦秒思AI制作', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +
+
+
{children}
+
+
+ + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..6fe62d1 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,101 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+ ); +} diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 0000000..87558a2 --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,9 @@ +export default function Footer() { + return ( +
+
+ © 2026 麦秒思AI制作 | KOL Insight v1.0 +
+
+ ); +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..98a94a3 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,18 @@ +import Image from 'next/image'; + +export default function Header() { + return ( +
+
+
+ 麦秒思AI Logo +
+

KOL Insight

+

云图数据查询分析

+
+
+
麦秒思AI制作
+
+
+ ); +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 0000000..b291154 --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,2 @@ +export { default as Header } from './Header'; +export { default as Footer } from './Footer'; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..783abf8 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,39 @@ +import type { Config } from 'tailwindcss'; + +const config: Config = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + background: 'var(--background)', + foreground: 'var(--foreground)', + // 品牌色系 + primary: { + DEFAULT: '#4F46E5', + light: '#818CF8', + dark: '#3730A3', + }, + success: '#10B981', + warning: '#F59E0B', + error: '#EF4444', + info: '#3B82F6', + }, + fontFamily: { + sans: [ + 'Inter', + '-apple-system', + 'BlinkMacSystemFont', + 'PingFang SC', + 'Microsoft YaHei', + 'sans-serif', + ], + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}