From 0a5b0c1914bd4fd03f5e07b7cecf25c59a91d226 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Wed, 15 Apr 2026 02:37:58 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20bootstrap=20=E2=86=92=20tailwind/sh?= =?UTF-8?q?adcn-vue=20infra=20+=20sonner=20toast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set up Tailwind v4 + shadcn-vue infrastructure and replace the first Bootstrap component (Toast) as a vertical slice that proves the approach. Infrastructure (refactor/01 phase A): - Add tailwindcss + @tailwindcss/vite (devDependencies) and wire the plugin in vite.config.js - Add shadcn-vue runtime deps: reka-ui, class-variance-authority, clsx, tailwind-merge, lucide-vue-next - Add components.json and frontend/lib/utils.js (cn helper) - Define design tokens via shadcn CSS variables (oklch, neutral baseColor) in frontend/style/style.css - Wire dark mode: store.setDarkMode now toggles .dark for the Tailwind dark: variant - Coexist with Bootstrap (no removal yet) Toast → vue-sonner (refactor/01 phase B item 1): - Replace internals of widgets/Toast.vue (Bootstrap Toast → vue-sonner) - Keep store.setAlert() public API unchanged so all 5 call sites stay put - Map alertStyle text-success/warning/danger/info to sonner severities Refactor planning: - Add AGENTS.md so future AI sessions know the project layout, JS-only preference, and where to find refactor progress - Add refactor/ with a 4-task plan, completion discipline, and per-task review checklists Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 121 ++++++++++++++++++++++ components.json | 19 ++++ frontend/components/ui/button/Button.vue | 23 ++++ frontend/components/ui/button/index.js | 2 + frontend/components/ui/button/variants.js | 27 +++++ frontend/components/ui/sonner/Sonner.vue | 21 ++++ frontend/components/ui/sonner/index.js | 2 + frontend/components/widgets/Toast.vue | 85 ++++----------- frontend/lib/utils.js | 9 ++ frontend/store.js | 4 + frontend/style/style.css | 78 ++++++++++++++ package.json | 8 ++ refactor/01-bootstrap-to-tailwind.md | 87 ++++++++++++++++ refactor/02-app-vue-split.md | 52 ++++++++++ refactor/03-add-tests.md | 82 +++++++++++++++ refactor/04-store-hardcoded.md | 49 +++++++++ refactor/README.md | 51 +++++++++ vite.config.js | 2 + 18 files changed, 657 insertions(+), 65 deletions(-) create mode 100644 AGENTS.md create mode 100644 components.json create mode 100644 frontend/components/ui/button/Button.vue create mode 100644 frontend/components/ui/button/index.js create mode 100644 frontend/components/ui/button/variants.js create mode 100644 frontend/components/ui/sonner/Sonner.vue create mode 100644 frontend/components/ui/sonner/index.js create mode 100644 frontend/lib/utils.js create mode 100644 refactor/01-bootstrap-to-tailwind.md create mode 100644 refactor/02-app-vue-split.md create mode 100644 refactor/03-add-tests.md create mode 100644 refactor/04-store-hardcoded.md create mode 100644 refactor/README.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..46d0691c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,121 @@ +# AGENTS.md + +> 本文件给 AI 协作者(Claude / Codex 等)阅读。说明项目结构、约定、当前重构状态。 + +--- + +## 项目简介 + +**MyIP** (IPCheck.ing) 是一个开源 IP 工具箱,提供 IP 查询、连通性测试、WebRTC 检测、DNS 泄漏检测、网速测试、Whois、MTR、安全检查等功能。前后端同仓,前端 SPA + Node 后端 API。 + +--- + +## 技术栈 + +| 层 | 技术 | +|---|---| +| 前端框架 | Vue 3 (Composition API, ` + + diff --git a/frontend/components/ui/button/index.js b/frontend/components/ui/button/index.js new file mode 100644 index 00000000..fff1a532 --- /dev/null +++ b/frontend/components/ui/button/index.js @@ -0,0 +1,2 @@ +export { default as Button } from './Button.vue'; +export { buttonVariants } from './variants.js'; diff --git a/frontend/components/ui/button/variants.js b/frontend/components/ui/button/variants.js new file mode 100644 index 00000000..a9c0ed95 --- /dev/null +++ b/frontend/components/ui/button/variants.js @@ -0,0 +1,27 @@ +import { cva } from 'class-variance-authority'; + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); diff --git a/frontend/components/ui/sonner/Sonner.vue b/frontend/components/ui/sonner/Sonner.vue new file mode 100644 index 00000000..d4aeab12 --- /dev/null +++ b/frontend/components/ui/sonner/Sonner.vue @@ -0,0 +1,21 @@ + + + diff --git a/frontend/components/ui/sonner/index.js b/frontend/components/ui/sonner/index.js new file mode 100644 index 00000000..be824543 --- /dev/null +++ b/frontend/components/ui/sonner/index.js @@ -0,0 +1,2 @@ +export { default as Sonner } from './Sonner.vue'; +export { toast } from 'vue-sonner'; diff --git a/frontend/components/widgets/Toast.vue b/frontend/components/widgets/Toast.vue index d21d6ae4..15a8e393 100644 --- a/frontend/components/widgets/Toast.vue +++ b/frontend/components/widgets/Toast.vue @@ -1,78 +1,33 @@ - - \ No newline at end of file diff --git a/frontend/lib/utils.js b/frontend/lib/utils.js new file mode 100644 index 00000000..dc581cb6 --- /dev/null +++ b/frontend/lib/utils.js @@ -0,0 +1,9 @@ +// shadcn-vue cn() 帮助函数:合并 class,处理 Tailwind 冲突 +// 见 refactor/01 + +import { clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs) { + return twMerge(clsx(inputs)); +} diff --git a/frontend/store.js b/frontend/store.js index 0b7afbc4..f96208be 100644 --- a/frontend/store.js +++ b/frontend/store.js @@ -134,8 +134,12 @@ export const useMainStore = defineStore('main', { this.shouldRefreshEveryThing = payload; }, // 设置黑暗模式 + // 同步 .dark class 到 ,让 Tailwind 的 dark: 变体生效(refactor/01) setDarkMode(value) { this.isDarkMode = value; + if (typeof document !== 'undefined') { + document.documentElement.classList.toggle('dark', !!value); + } }, // 设置 IP 数据库的使能状态 updateIPDBs({ id, enabled }) { diff --git a/frontend/style/style.css b/frontend/style/style.css index f77e98ec..a37dc44c 100644 --- a/frontend/style/style.css +++ b/frontend/style/style.css @@ -2,6 +2,84 @@ @import 'bootstrap-icons/font/bootstrap-icons.min.css'; @import 'flag-icons/css/flag-icons.min.css'; +/* === Tailwind v4 + shadcn-vue 基建(重构进行中 — 见 refactor/01) === */ +@import 'tailwindcss'; + +/* 暗色模式:用 .dark class 触发(由 store.isDarkMode 同步到 ) */ +@custom-variant dark (&:where(.dark, .dark *)); + +/* 设计 tokens — shadcn-vue 标准 CSS 变量 */ +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --radius: 0.5rem; +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(1 0 0 / 0.1); + --input: oklch(1 0 0 / 0.15); + --ring: oklch(0.556 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); +} +/* === 基建 end === */ + .jn-title { display: flex; justify-content: space-between; diff --git a/package.json b/package.json index ed0ab11c..ecc5f332 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "bootstrap-icons": "^1.13.1", "chart.js": "^4.5.1", "circle-progress.vue": "^3.4.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "concurrently": "^9.2.1", "country-code-lookup": "^0.1.5", "detect-gpu": "^5.0.70", @@ -31,23 +33,29 @@ "firebase": "^12.12.0", "flag-icons": "^7.5.0", "http-proxy-middleware": "^3.0.5", + "lucide-vue-next": "^1.0.0", "maxmind": "^5.0.6", "pinia": "^3.0.4", + "reka-ui": "^2.9.6", "svgmap": "2.19.3", + "tailwind-merge": "^3.5.0", "tar": "^7.5.13", "ua-parser-js": "^2.0.9", "vue": "^3.5.32", "vue-i18n": "^11.3.2", "vue-markdown-render": "^2.3.0", "vue-router": "^5.0.4", + "vue-sonner": "^2.0.9", "whoiser": "^1.18.0" }, "devDependencies": { "@serwist/vite": "^9.5.7", + "@tailwindcss/vite": "^4.2.2", "@vitejs/plugin-vue": "^6.0.6", "code-inspector-plugin": "^1.5.1", "nodemon": "^3.1.14", "serwist": "^9.5.7", + "tailwindcss": "^4.2.2", "vite": "^8.0.8" } } diff --git a/refactor/01-bootstrap-to-tailwind.md b/refactor/01-bootstrap-to-tailwind.md new file mode 100644 index 00000000..d3bfded8 --- /dev/null +++ b/refactor/01-bootstrap-to-tailwind.md @@ -0,0 +1,87 @@ +# 01 — Bootstrap → Tailwind + shadcn-vue + +**目标**:彻底移除 Bootstrap,UI 层完全迁移到 Tailwind v4 + shadcn-vue,bootstrap-icons 替换为 lucide-vue-next。最终 `package.json` 不再依赖 `bootstrap` / `bootstrap-icons`。 + +**状态**:🟢 进行中(阶段 A 已完成) + +**影响范围**:30+ Vue 文件、router/index.js 中的 DOM 操作、main.js 中的全局 tooltip 指令、style/style.css。 + +--- + +## 阶段 A — 基础设施搭建 ✅ + +- [x] 安装 Tailwind v4 (`tailwindcss@^4.2.2`、`@tailwindcss/vite@^4.2.2`),在 `vite.config.js` 注入 `tailwindcss()` 插件 +- [x] 安装 shadcn-vue 运行时依赖 (`reka-ui`、`class-variance-authority`、`clsx`、`tailwind-merge`、`lucide-vue-next`),建立 `components.json`,约定组件落在 `frontend/components/ui/`,工具函数在 `frontend/lib/utils.js` +- [x] 设计 design tokens(shadcn 标准 CSS 变量集 + neutral baseColor + oklch 色彩空间)写入 `style/style.css` 的 `@theme inline` 块 +- [x] 暗色模式:`store.setDarkMode()` 同步切换 `` 的 `.dark` class,`@custom-variant dark` 绑定该 class,Tailwind `dark:` 变体生效 +- [x] Tailwind 与 Bootstrap 共存验证:`npm run build` 通过、`npm test` 23 全绿、Vite HMR 正常、CSS bundle 含 `--background` 等 tokens 与 `.dark { }` 规则 +- [x] `tailwindcss` / `@tailwindcss/vite` 归入 devDependencies(保持 dev 分支 "chore: move dev tools out of production deps" 的约定) +- [x] 落地一个 shadcn `Button` 组件(`frontend/components/ui/button/`)作为后续替换的样板 + +## 阶段 B — 命令式 Bootstrap JS 替换(高 ROI 优先) + +> 共 13 处 `import ... from 'bootstrap'`,每替一处都直接简化代码。 + +- [x] **Toast** → shadcn `sonner`(vue-sonner):`components/widgets/Toast.vue`(保持 `store.setAlert()` 外部 API 不变) +- [ ] **Tooltip** → shadcn `Tooltip`:删除 `main.js` 里的 `app.directive('tooltip', ...)`,组件内改用 `` 包裹 +- [ ] **Modal** → shadcn `Dialog`: + - [ ] `components/widgets/Help.vue` + - [ ] `components/widgets/QueryIP.vue` + - [ ] `components/User.vue` + - [ ] `components/Additional.vue` +- [ ] **Offcanvas** → shadcn `Sheet`: + - [ ] `components/Nav.vue` + - [ ] `components/Achievements.vue` + - [ ] `components/Footer.vue` + - [ ] `components/Advanced.vue` + - [ ] `components/widgets/Patch.vue` +- [ ] **删除 `router/index.js` 里对 `document.getElementById('offcanvasTools')` 的命令式操作**,改为 store 状态驱动 Sheet 开合 +- [ ] 删除 `main.js` 里的 `import 'bootstrap'` + +## 阶段 C — 视觉层批量重写 + +按以下顺序,每个组件做完后单独 commit: + +- [ ] `components/Nav.vue`(含暗色模式样式收敛) +- [ ] `components/IpInfos.vue` + `components/ip-infos/` 子组件(IPCard / ASNInfo / DataPairBar) +- [ ] `components/ConnectivityTest.vue` +- [ ] `components/WebRtcTest.vue` +- [ ] `components/DnsLeaksTest.vue` +- [ ] `components/SpeedTest.vue` +- [ ] `components/Advanced.vue` +- [ ] `components/advanced-tools/` 全部 11 个组件 +- [ ] `components/Achievements.vue` +- [ ] `components/Additional.vue` +- [ ] `components/Footer.vue` +- [ ] `components/widgets/Preferences.vue` +- [ ] `components/widgets/InfoMask.vue` +- [ ] `components/widgets/PWA.vue` +- [ ] `App.vue` 模板部分(注意:App.vue 大改要协调 02 任务) + +## 阶段 D — 图标迁移 + +- [ ] 安装 `lucide-vue-next` +- [ ] 全仓库搜索 `bi bi-` 类名,逐一替换为对应的 lucide 组件 +- [ ] 删除 `style/style.css` 里的 `bootstrap-icons` import + +## 阶段 E — 卸载与清理 + +- [ ] 删除 `style/style.css` 里的 `@import 'bootstrap/dist/css/bootstrap.min.css'` +- [ ] 检查 `style/style.css` 自定义类是否还需要保留,能用 Tailwind 表达的删除 +- [ ] `npm uninstall bootstrap bootstrap-icons` +- [ ] `npm run build` 通过,体积对比记录一下 + +--- + +## 完成前回顾清单 + +将本任务标记为 ✅ 之前必须确认: + +- [ ] 全仓库 `grep -r "from 'bootstrap'"` 结果为 0 +- [ ] 全仓库 `grep -rE "data-bs-|class=.*\b(btn-|navbar-|offcanvas|modal-|toast|tooltip|dropdown-|card-|row|col-|d-flex|text-(white|dark|muted|success|danger|warning|primary)|bg-(white|dark|body))"` 中 Bootstrap 残留为 0(注意区分自定义类) +- [ ] 全仓库 `grep -r "bi bi-"` 结果为 0 +- [ ] `package.json` 不再含 `bootstrap` / `bootstrap-icons` +- [ ] `npm run build` 通过,`npm run dev` 启动后所有 Modal / Sheet / Tooltip / Toast 行为正常 +- [ ] 暗色模式在所有页面表现一致 +- [ ] 移动端布局未破坏(`store.isMobile` 路径手动验证一次) +- [ ] router 切换 advanced-tools 时 Sheet 正常开合 diff --git a/refactor/02-app-vue-split.md b/refactor/02-app-vue-split.md new file mode 100644 index 00000000..e73c6597 --- /dev/null +++ b/refactor/02-app-vue-split.md @@ -0,0 +1,52 @@ +# 02 — App.vue 上帝组件拆分 + +**目标**:将 `frontend/App.vue`(590 行,14 个 ref,混合了模板、加载编排、刷新调度、信息遮罩、快捷键注册、滚动控制)拆分为职责清晰的多个 composables 和子组件。最终 `App.vue` 只剩模板 + 少量编排代码(目标 < 150 行)。 + +**状态**:🟡 未开始 + +**前置依赖**:阶段 A 不强制,但**强烈建议在 01 阶段 B 完成后**再做,以避免拆分后又因 UI 框架替换而再次重构。 + +--- + +## 子任务 + +### 拆分 composables(新建 `frontend/composables/` 目录) + +- [ ] `useShortcuts.js` — 抽出 `ShortcutKeys()` 函数(约 270 行)和 `registerShortcutKeys` / `loadShortcuts` + - 入参:各组件 ref 集合 + configs + i18n t 函数 + - 出参:`registerAll()` / `keyMap` +- [ ] `useInfoMask.js` — 抽出 `infoMask` / `infoUnmask` / `toggleInfoMask` + `infoMaskLevel` 状态 + - 入参:各组件 ref(IPCheck / webRTC / dnsLeaks) + - 出参:`infoMaskLevel`、`toggleInfoMask`、`showMaskButton` +- [ ] `useRefreshOrchestrator.js` — 抽出 `refreshEverything` / `scheduleTimedTasks` / `loadingControl` + - 与 store 的 `mountingStatus` / `loadingStatus` / `shouldRefreshEveryThing` 协作 +- [ ] `useScrollTo.js` — 抽出 `scrollToElement`(小工具) + +### Store 改造 + +- [ ] 将刷新编排(refreshTasks)从 App.vue 移入 store action `refreshEverything()` +- [ ] App.vue 只 watch `shouldRefreshEveryThing` 并触发 store action + +### 模板与 ref 整理 + +- [ ] App.vue 顶层 14 个 ref 收敛:能用 store 状态替代的尽量替代(特别是 alert 相关的 4 个 ref,已经在 store 里有 `setAlert`) +- [ ] 删除 App.vue 中已废弃的 `app.config.globalProperties.$trackEvent`(如果确认 components 都已直接从 utils 导入 `trackEvent`) +- [ ] App.vue 顶部 `loadingControl` 的递归 setTimeout 改为基于 store getter 的 watch + +### 验收 + +- [ ] App.vue 总行数 < 150 +- [ ] 所有快捷键功能验证通过 +- [ ] 信息遮罩 3 级切换正常 +- [ ] 自动刷新 / 手动刷新行为不变 +- [ ] composables 文件每个都有头部注释说明输入输出 + +--- + +## 完成前回顾清单 + +- [ ] 全仓库 `grep` 确认 App.vue 中拆出的所有函数没有被其它组件直接引用过 +- [ ] `frontend/composables/` 下文件均为单一职责,没有"杂物 composable" +- [ ] App.vue 不再持有任何与 alert / refresh / mask / shortcut 相关的本地状态 +- [ ] 手动跑一遍:自动刷新模式 / 手动刷新模式 / 切换到任一 advanced-tool / 触发遮罩 1 / 2 / 解除 / 触发每个快捷键 +- [ ] 03 任务里有没有可以为这些新 composables 添加单测的位置(应该有,去补 03 的清单) diff --git a/refactor/03-add-tests.md b/refactor/03-add-tests.md new file mode 100644 index 00000000..b2eee4da --- /dev/null +++ b/refactor/03-add-tests.md @@ -0,0 +1,82 @@ +# 03 — 增加单元测试 + +**目标**:扩展现有 Node 内置 test runner 测试覆盖,给后续重构(特别是 01、02、04)提供安全网。 + +**状态**:🟢 进行中(基础设施 + 部分 utils + 部分 API smoke 已完成) + +**测试栈**:**Node 内置 `node --test`**(不引入 Vitest / Jest,避免增加依赖)。 +**位置**:根目录 `tests/`,文件命名 `xxx.test.js`,用 `node:test` + `node:assert/strict`。 + +--- + +## 已经完成(dev 分支已合入) + +- [x] `package.json` 加 scripts:`"test": "node --test tests/*.test.js"`、`"check": "npm run test && npm run build"` +- [x] 根目录建立 `tests/` 目录 +- [x] `common/valid-ip.js` + `frontend/utils/valid-ip.js` 测试(`tests/valid-ip.test.js`) +- [x] API smoke 测试(`tests/api-smoke.test.js`):覆盖 referer 检查 + configs / map / maxmind / dns 早期校验路径,无网络调用 + +> 现状参考:跑 `npm test` 应全绿。新增测试请遵循同样的 ESM + `node:test` 风格,避免引入新依赖。 + +--- + +## 待办子任务 + +### 阶段 A — utils 纯函数补齐 + +- [ ] `frontend/utils/transform-ip-data.js` — 各类 raw 输入到统一卡片数据的映射 / 边界 +- [ ] `frontend/utils/masked-info.js` — 遮罩输出格式(mock i18n t 函数) +- [ ] `frontend/utils/country-name.js` — 国家代码 → 名称映射 + 兜底 +- [ ] `frontend/utils/timestamp-to-date.js` — 时区与格式 +- [ ] `frontend/utils/system-detect.js` — UA 模拟测试 OS / Mobile 判定 +- [ ] `frontend/utils/speedtest-colos.js` — 数据查询函数 +- [ ] `frontend/utils/shortcut.js` — `mappingKeys` / `navigateCards` 行为(注意:依赖 DOM,可能需要轻量 mock 或拆纯函数后再测) + +### 阶段 B — API smoke 扩展 + +> 已覆盖 4 个 handler(configs / map / maxmind / dns),剩余 handler 用同样风格补齐: + +- [ ] `api/get-user-info.js` +- [ ] `api/get-whois.js` +- [ ] `api/cf-radar.js` +- [ ] `api/invisibility-test.js` +- [ ] `api/ip-sb.js` / `api/ipapi-com.js` / `api/ipapi-is.js` / `api/ipcheck-ing.js` / `api/ipinfo-io.js` / `api/ipapi-com.js` / `api/ip2location-io.js`(外部 IP 源,重点测 referer 校验和参数早期 reject 路径,**不要发真实请求**) +- [ ] `api/mac-checker.js` +- [ ] `api/update-user-achievement.js` +- [ ] `api/dns-resolver.js` 的更多分支(已有基础覆盖,可补错误路径) + +### 阶段 C — composables 测试(依赖 02 任务) + +> 02 拆出 composables 后回来补充: + +- [ ] `useShortcuts.js` +- [ ] `useInfoMask.js` +- [ ] `useRefreshOrchestrator.js` + +### 阶段 D — store 测试 + +- [ ] `store.js` 关键 action:`setAlert` / `setLoadingStatus` / `setRefreshEveryThing` / `setCurrentPath` +- [ ] 04 任务完成后,针对成就数据加载 / 解锁逻辑添加测试(`createInitialAchievementsState()` 等) + +### 阶段 E — 共享与后端 + +- [ ] `common/referer-check.js` — 已被 api-smoke 间接覆盖,可补独立测试覆盖更多组合 +- [ ] `common/maxmind-service.js` — 接口契约测试(mock fs / db) + +--- + +## Vue 组件测试(不在本阶段范围) + +Node test runner 测 Vue SFC 需要额外编译/jsdom 配置,会破坏"零依赖"原则。**本阶段不做组件测试**,需要时单独评估方案。 + +--- + +## 完成前回顾清单 + +- [ ] `npm run check` 通过 +- [ ] 所有新测试文件遵循 `tests/*.test.js` 命名 +- [ ] 测试不依赖网络(外部 API 测试只覆盖 referer / 参数早 reject 路径) +- [ ] 测试不引入新 npm 依赖 +- [ ] 02 任务完成后是否回头补了 composables 测试 +- [ ] 04 任务完成后是否回头补了成就解锁逻辑测试 +- [ ] 是否有"看上去是纯函数但其实有副作用"的 utils 被遗漏(`use-analytics.js` / `register-service-worker.js` 这类带副作用的需要明确决定测不测,并在本文档登记决定) diff --git a/refactor/04-store-hardcoded.md b/refactor/04-store-hardcoded.md new file mode 100644 index 00000000..a3453425 --- /dev/null +++ b/refactor/04-store-hardcoded.md @@ -0,0 +1,49 @@ +# 04 — store.js 硬编码数据治理 + +**目标**:将 `frontend/store.js` 中硬编码的数据(主要是 21 条 `userAchievements`、可能还有其它常量)抽离到独立数据文件,store 只持有可变状态和 actions,便于维护、便于测试、便于未来本地化与扩展。 + +**状态**:🟡 未开始 + +--- + +## 子任务 + +### 阶段 A — 现状盘点 + +- [ ] 通读 `frontend/store.js`,列出所有"硬编码常量数据"候选清单(不仅仅是 achievements,可能还有默认偏好、curl 域名映射等) +- [ ] 在本文件下方"盘点结果"小节登记发现项 + +### 阶段 B — achievements 抽离 + +- [ ] 新建 `frontend/data/achievements.js`,导出形如 `export const ACHIEVEMENTS_DEFINITIONS = [...]` 的数组(定义:`name` / `img`),不含状态字段(`achieved` / `achievedTime` / `showDetails`) +- [ ] 新建工厂函数 `createInitialAchievementsState()`,根据定义生成初始状态对象 +- [ ] store 的 `state.userAchievements` 改为调用工厂函数初始化 +- [ ] 校验所有读取 `userAchievements` 的地方(`Achievements.vue` / `App.vue` / 触发解锁的 utils)行为不变 +- [ ] 成就元数据(图片路径前缀不一致问题:`'achievements/iamhuman.webp'` vs `/achievements/...`)顺手统一 + +### 阶段 C — 其它硬编码 + +- [ ] 处理盘点中发现的其它项(具体子任务在阶段 A 完成后补齐到这里) + +### 阶段 D — 测试 + +- [ ] 在 03 的覆盖范围里补:`createInitialAchievementsState()` 单测、关键解锁逻辑回归测试 + +--- + +## 盘点结果 + +> 阶段 A 完成后填写。每项注明:所在行号、内容性质、建议归宿(data/ 目录、constants 文件、保留在 store)。 + +- (待填写) + +--- + +## 完成前回顾清单 + +- [ ] `frontend/store.js` 中已无大段静态对象字面量 +- [ ] `frontend/data/` 下文件均为纯数据 + 纯函数,无副作用、无 store 依赖 +- [ ] 全仓库 grep 确认所有 `userAchievements` 的读写仍走 store,没有组件直接 import 数据文件硬编码 +- [ ] 图片路径前缀问题已统一并验证渲染 +- [ ] 任何新增导出都在文件顶部注释说明用途 +- [ ] 已与 03 任务联动,补充必要的单测 diff --git a/refactor/README.md b/refactor/README.md new file mode 100644 index 00000000..28e419d6 --- /dev/null +++ b/refactor/README.md @@ -0,0 +1,51 @@ +# 重构计划索引 + +> 本目录管理 MyIP 当前阶段的重构任务。每个文件对应一个独立子项目。 + +启动日期:2026-04-15 + +--- + +## 子任务总览 + +| # | 文件 | 主题 | 状态 | +|---|---|---|---| +| 01 | [01-bootstrap-to-tailwind.md](./01-bootstrap-to-tailwind.md) | Bootstrap → Tailwind + shadcn-vue(彻底替换) | 🟢 进行中(阶段 A 完成) | +| 02 | [02-app-vue-split.md](./02-app-vue-split.md) | App.vue 上帝组件拆分 | 🟡 未开始 | +| 03 | [03-add-tests.md](./03-add-tests.md) | 增加单元测试(Node `node --test`) | 🟢 进行中(基建+部分用例已完成于 dev) | +| 04 | [04-store-hardcoded.md](./04-store-hardcoded.md) | store.js 硬编码数据治理 | 🟡 未开始 | + +状态图例:🟡 未开始 / 🟢 进行中 / ✅ 已完成 / ⚪ 已取消 + +--- + +## 推荐顺序 + +虽然顺序不强制,但建议这样推进,理由是后一项依赖前一项的成果: + +1. **先 01(Bootstrap → Tailwind)的基础设施部分**(装 Tailwind / shadcn-vue / Sheet 替换 Offcanvas),因为 02(App.vue 拆分)会动到大量模板和命令式 Bootstrap 调用,先有新 UI 基建可以避免做两次。 +2. **同时启动 03(测试基建)**——给 utils/ 加 Vitest,这是后续重构的安全网。 +3. **04(store 硬编码)** 可以独立穿插进行,影响面小。 +4. **02(App.vue 拆分)** 放到 01 推进过半再做,拆出的 composables 会更干净。 + +--- + +## 完成纪律(重要) + +**每次将子任务从 `[ ]` 改成 `[x]` 之前,必须执行以下检查:** + +1. **全局搜索回扫**:用 grep / Glob 搜索该子任务相关的关键词(如某个被替换的 Bootstrap class、被移除的 import、被抽离的函数名),确认全仓库没有遗漏的引用。 +2. **同类场景扫描**:思考"这个改动是否还有类似场景没处理"。比如替换了一个 Modal,确认其它 Modal 是否同样需要替换;抽出一个 composable,确认是否还有重复逻辑可以合并。 +3. **运行时验证**:能跑就跑(`npm run dev` / `npm run build` / `npm test`),看构建/测试是否仍通过。 +4. **更新本 README 的总览表**:状态从 🟡 / 🟢 改为 ✅。 +5. **如果发现遗漏**:不要勾选,回到计划文件补充新子项后再继续。 + +每个计划文件末尾都有一个"完成前回顾清单"小节,列出该任务特定的检查点。 + +--- + +## 范围约束 + +当前阶段**只做**这 4 件事。其它建议(TypeScript 化、Biome 接入、依赖瘦身、circle-progress 替换、firebase 动态加载、vue-router 版本核对等)暂不进入计划,避免范围蔓延。 + +完成 4 项后再评估下一阶段做什么。 diff --git a/vite.config.js b/vite.config.js index 5c8e4991..c707768f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,6 +1,7 @@ import dotenv, { parse } from 'dotenv'; import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' import { serwist } from '@serwist/vite' import { CodeInspectorPlugin } from 'code-inspector-plugin'; @@ -74,6 +75,7 @@ export default defineConfig({ } } }), + tailwindcss(), serwist({ swSrc: 'frontend/sw.js', swDest: 'sw.js',