Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ web/node_modules/

# OS
.DS_Store
Thumbs.db
Thumbs.dbdocs/
50 changes: 50 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# DevBox 开发指南

DevBox 是 Go 后端 + Vue 3 前端的镜像代理控制台:代理 npm / pypi / docker / golang / conda 等 17 个镜像源,提供 GitHub 搜索、Release 下载中转、Git clone 加速,带暗色液态玻璃风格的 Web 控制台。

## 环境

- 平台:Windows 11;包管理统一用 `pnpm`(不用 npm)
- 前端技术栈:Vue 3.5(`<script setup>` + TS)+ Vue Router 4 + Vite 8 + Tailwind CSS 4
- **无 UI 组件库、无 icon 库、无动画库** —— 全部手写 SVG / Tailwind 原子类,禁止随意新增 npm 依赖

## 常用命令

```bash
# 前端构建验证(含 vue-tsc 类型检查,改完前端必跑)
cd web && pnpm run build

# 前端本地开发
cd web && pnpm run dev # http://localhost:5173,/api 代理到 :8080

# 后端本地启动(前端产物需先构建)
go run ./cmd/devbox/ -c configs/devbox.yaml -f web/dist # http://localhost:8080

# 后端测试
go test ./...
```

## 前端约定

- **设计系统**(`web/src/style.css`):
- 背景 `#05070a` + 网格渐变;玻璃卡片用 `glass` / `glass-hover` / `glass-inset`(Tailwind 4 `@utility` 定义,禁止改为普通类后再 `@apply`)
- 品牌色:紫 `#7c3aed` 系(logo、激活导航、渐变 kicker、sparkline 渐变),青 `cyan-400`(主操作、链接、焦点),语义色 emerald/amber/red
- 按钮 `btn` / `btn-primary` / `btn-danger` / `btn-brand`;表格用 `table-wrap` + `data-table`;标签 `tag` 系列
- **UI 文案一律使用中文**(按钮、表头、面板标题、状态标签、提示语),不混用英文;命令代码块内容保持原样
- **响应式 grid 必须声明移动端列**:任何带 `sm:/md:/lg:/xl:` 列声明的 grid 都要补 `grid-cols-1`,否则单列 auto 轨道会被代码块/表格的 nowrap 内容撑宽导致移动端横向溢出(历史教训)
- **API 契约以 Go 后端 JSON tag 为准**:如 `has_more`、`per_page`、`bytes_out`、`created_at`,前端不得改成 camelCase
- 通用组件位于 `web/src/components/`:`Panel`(卡片容器)、`CodeBlock`(命令代码块,自带复制)、`CopyButton`、`StatusDot`、`EmptyState`、`Banner`、`Logo`;跨页复用优先用这些组件,不要内联重复
- 新页面沿用:`page-header` + `page-kicker`(渐变紫青)+ `page-title` + `page-subtitle` 结构
- 页面切换过渡在 `App.vue`(`<transition name="page">`),新增交互过渡沿用同一风格(克制不过度)

## 后端约定

- 入口 `cmd/devbox/`,配置 `configs/devbox.yaml`(mirrors / gitproxy / cache / rate_limit 等),SQLite 数据库与缓存默认在 `/data`
- 鉴权:设置 `server.auth_token` 后需登录;`AUTH_TOKEN` 即登录密码,鉴权仅影响控制台,镜像代理与 Git 代理不受影响
- 主要 API:`/status`、`/stats/traffic`、`/stats/logs`、`/config/mirrors`、`/config/ratelimit`、`/config/public`、`/search`、`/release-sources`、`/release-download`

## 开发工作流

- 新功能按分支开发:`refactor/xxx` / `feature/xxx`,验证通过后提 PR,待用户审查后再合并(合并后清理分支)
- 前端视觉验证:`pnpm run dev` + 后端 `go run` 后,用 Edge headless 截图检查(`--headless --screenshot`,Windows 下注意 DPI 缩放会导致 window-size 失效,可用 CDP `Emulation.setDeviceMetricsOverride` 精确模拟 375px)
- 功能变化需同步更新 `README.md`(项目功能清单、Release 下载说明等在 README 有专门章节)
80 changes: 80 additions & 0 deletions docs/frontend-refactor-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# DevBox 前端重构任务提示词

> 用法:新开会话后,把本文件全文粘贴给 opencode 作为第一条指令(可附上"请先读一遍仓库里的 docs/frontend-refactor-prompt.md 再开始")。请全程使用中文交流。

## 角色与背景

你是 DevBox 项目的资深前端工程师。DevBox 是一个 Go 后端 + Vue 3 前端的**镜像代理控制台**:代理 npm / pypi / docker / golang / conda 等 17 个镜像源,提供 GitHub 搜索、Release 下载中转、Git clone 加速,带一个暗色极客风的 Web Dashboard。项目仓库根目录为 `E:\code\devbox`(Windows,包管理用 pnpm)。

当前任务:在**保持全部现有功能**的前提下,对前端做一次完整的 UI/UX 重构,让界面更精致、统一、有高级感。

## 技术栈与硬性约束

- Vue 3.5(`<script setup>` + TS)+ Vue Router 4 + Vite 8 + Tailwind CSS 4
- **无 UI 组件库、无 icon 库、无动画库** —— 全部手写 SVG / Tailwind 原子类
- 代码位置:`web/src/views/*.vue`(7 个页面)、`web/src/components/*.vue`、`web/src/style.css`、`web/src/api/client.ts`、`web/src/router/index.ts`、`web/src/App.vue`
- **不要改动后端 API 契约**;前端参数名须与后端 JSON tag 对齐(例如搜索接口是 `has_more`、`per_page`,不是 camelCase)
- 不新增任何 npm 依赖
- 构建验证:在 `web/` 目录下 `pnpm run build`(含 vue-tsc 类型检查)

## 现有设计资产(必读)

1. `web/public/favicon.svg` —— 新的品牌图标:等轴测紫渐变立方体 + 青色终端提示符 `>_`。重构时把它的视觉语言延伸到整个 UI
2. 品牌色系:紫 `#7c3aed / #9455f5 / #6528d4 / #bd8bff / #f2e4ff`,青色强调 `#67e8f9`
3. 根目录 `view.png` —— 旧版 Dashboard 全貌截图(只作**信息架构**参考,不作视觉参考)
4. 现有风格特征:`#05070a` 深色背景、slate 灰文字层级、cyan 强调、等宽字体(ui-monospace)、细边框(border-slate-800 类)、渐变网格背景

## 重构范围

| 文件 | 内容 | 优先级 |
| --------------------------- | -------------------------------------------------- | ------ |
| `App.vue` | 导航布局、header logo(用新 SVG 立方体)、页面过渡 | 高 |
| `views/Dashboard.vue` | 状态卡片、流量趋势图(自绘 sparkline)、访问日志 | 高 |
| `views/Mirrors.vue` | 17 镜像启停 / upstream / cache TTL 配置 | 高 |
| `views/Search.vue` | 7 源聚合搜索 + 安装命令生成/复制 | 中 |
| `views/Releases.vue` | GitHub Release 源持久化 + 中转下载 | 中 |
| `views/Settings.vue` | 运行信息、限流配置(rate/interval/白黑名单) | 中 |
| `views/GitProxy.vue` | git clone/archive/raw 命令生成与复制 | 低 |
| `views/Login.vue` | 登录(token 输入) | 低 |
| `components/StatusCard.vue` | 仪表盘统计卡 | 低 |

## 设计方向(必须遵守)

1. **目标风格为液态玻璃(liquid glass)**:在暗色终端极客基调上,采用半透明玻璃卡片(高光描边、顶部紫/青辉光、backdrop-blur)统一卡片、面板、表格、表单的视觉语言(spacing 刻度、圆角、边框、hover/active/disabled 状态一致)
2. **品牌色系统**:紫为主色、青为强调色、slate 做文字层级;交互元素(链接、激活导航、主按钮)统一用 cyan/紫渐变,保证 WCAG AA 对比度
3. **导航**:顶部 sticky header;移动端(375px)导航可用;logo 用新 SVG 立方体 + "devbox" 字标
4. **表格**:统一 `data-table` 样式(表头小号大写、行分隔、状态 tag、空态/加载态)
5. **微交互**:保留 App.vue 现有页面切换 `<transition>`;卡片/按钮 hover 反馈统一;列表重排/状态切换可加与主题一致的过渡(遵循流体动效原则,克制不过度)
6. **组件化**:跨页复用的视觉模式(Panel 卡片、Tag、StatusDot、EmptyState、CopyButton 等)提取到 `web/src/components/`,不要每个页面重复内联
7. **功能语义不变**:保留每个页面的全部功能与现有文案语义,不删减、不篡改行为
8. **渐进式**:先做 App.vue + Dashboard(确立设计系统),与用户确认视觉方向后再铺开其余页面,不要一次性全部改完再让用户看

## 推荐的设计 tokens(起点,可优化)

- 背景:`#05070a` 保持,面板 `slate-950/60` + `border-slate-800`
- 强调:主操作 `cyan-400` 系渐变,品牌点缀紫 `#7c3aed` 系渐变
- 圆角:卡片 10px(rounded-lg 偏大),标签/按钮 6px
- 文字:标题 slate-100,正文 slate-300,次要 slate-500,标签 uppercase tracking-widest text-[10px](沿用现有 feel)

## 工作流程

1. 先读代码:全部 views、components、style.css、api/client.ts、router/index.ts、index.html;用 `web/../view.png`(或 agent-vision 分析)了解信息架构
2. 拟定设计系统(色板/间距/圆角/组件清单)摘要,**先向用户确认方向**,再动手写代码
3. 逐页重构;每个页面完成后 `pnpm run build` 验证类型与构建
4. 全部完成后启动本地 dev(`nohup pnpm run dev > /tmp/dev.log 2>&1 &`,后台进程;该写法需在 Git Bash 中执行,PowerShell 用 `Start-Process` 或 `Start-Job`),用 Edge headless 截图 + agent-vision 逐页检查视觉效果并修复;完成后停掉后台进程
5. 最终 `pnpm run build` 通过后:新建分支 `refactor/frontend-redesign`,提交,推送,创建 PR(PR 描述写明设计变更点);PR 创建后等用户审查,**不要自行合并**
6. 若涉及 README 功能描述变化,同步更新 README.md

## 验收标准

- [ ] `pnpm run build` 通过(vue-tsc 无类型错误)
- [ ] 登录、Dashboard(状态/流量/日志)、Mirrors(启停/改 upstream/TTL)、Search(搜索/复制命令)、Releases(源管理/下载)、Settings(限流保存)全部可用
- [ ] 视觉统一、无样式冲突;375px 移动端导航可用
- [ ] 后端 API 契约未变(若前端字段名与后端不一致,以后端为准:`has_more`、`per_page` 等)
- [ ] 无新增依赖;无 console 报错

## 环境注意事项(重要)

- 本机安装了 opencode-vibeguard 插件:**读取文件时若看到 `__VG_IPV4_xxx__`、`__VG_EMAIL_xxx__` 等占位符是插件的显示层脱敏,磁盘文件是完好的**,不要据此"修复"文件
- 用 Edit 工具写 YAML/TS 文件可能被插件意外重排引号风格;若发现整个文件 diff 异常,改用 python 脚本写文件
- 本机 docker 命令不在 git-bash PATH 中;涉及 Docker 的操作在 PowerShell 中执行
Binary file modified view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 46 additions & 43 deletions web/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { isLoggedIn, logout } from './api/client'
import Logo from './components/Logo.vue'

const router = useRouter()

Expand All @@ -10,49 +11,26 @@ function handleLogout() {
}

const navItems = [
{ to: '/', label: 'Dashboard', code: 'dash' },
{ to: '/mirrors', label: 'Mirrors', code: 'mir' },
{ to: '/gitproxy', label: 'Git Proxy', code: 'git' },
{ to: '/search', label: 'Search', code: 'find' },
{ to: '/releases', label: 'Releases', code: 'rel' },
{ to: '/settings', label: 'Settings', code: 'cfg' },
{ to: '/', label: '仪表盘', code: 'dash' },
{ to: '/mirrors', label: '镜像源', code: 'mir' },
{ to: '/gitproxy', label: 'Git 代理', code: 'git' },
{ to: '/search', label: '搜索', code: 'find' },
{ to: '/releases', label: '发行版', code: 'rel' },
{ to: '/settings', label: '设置', code: 'cfg' },
]
</script>

<template>
<div class="min-h-screen text-slate-200">
<header class="sticky top-0 z-40 border-b border-slate-800 bg-slate-950/95 backdrop-blur-sm">
<header class="sticky top-0 z-40 border-b border-slate-800/80 bg-slate-950/70 backdrop-blur-md">
<div class="mx-auto flex max-w-7xl flex-col gap-3 px-4 py-3 lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center justify-between gap-4">
<router-link to="/" class="flex items-center gap-3 text-slate-100">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<defs>
<linearGradient id="dbTop" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f2e4ff"/>
<stop offset="100%" stop-color="#bd8bff"/>
</linearGradient>
<linearGradient id="dbLeft" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#7c3aed"/>
<stop offset="100%" stop-color="#9455f5"/>
</linearGradient>
<linearGradient id="dbRight" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#6528d4"/>
<stop offset="100%" stop-color="#43158f"/>
</linearGradient>
</defs>
<polygon points="12,2 19.75,6.75 12,11.5 4.25,6.75" fill="url(#dbTop)"/>
<polygon points="12,11.5 19.75,6.75 19.75,16.25 12,21" fill="url(#dbRight)"/>
<polygon points="12,11.5 12,21 4.25,16.25 4.25,6.75" fill="url(#dbLeft)"/>
<polygon points="12,4 16.5,6.4 12,8.8 7.5,6.4" fill="#ffffff" opacity="0.16"/>
<g stroke="#67e8f9" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">
<polyline points="13.25,13.75 15.25,15.25 13.25,16.75"/>
<line x1="16.25" y1="16.75" x2="17.75" y2="16.75"/>
</g>
</svg>
<router-link to="/" class="group flex items-center gap-3">
<Logo class="transition-[filter] duration-300 group-hover:drop-shadow-[0_0_10px_rgba(124,58,237,0.7)]" />
<span class="text-sm font-semibold tracking-wide text-slate-100">devbox</span>
<span class="hidden text-sm text-slate-500 sm:inline">mirror proxy console</span>
<span class="hidden text-sm text-slate-500 sm:inline">镜像代理控制台</span>
</router-link>
<button v-if="isLoggedIn()" @click="handleLogout" class="btn lg:hidden">logout</button>
<button v-if="isLoggedIn()" @click="handleLogout" class="btn lg:hidden">退出</button>
</div>

<nav class="flex items-center gap-1 overflow-x-auto pb-1 lg:pb-0">
Expand All @@ -66,7 +44,7 @@ const navItems = [
<span class="text-[10px] uppercase tracking-widest text-slate-600">{{ item.code }}</span>
<span>{{ item.label }}</span>
</router-link>
<button v-if="isLoggedIn()" @click="handleLogout" class="btn ml-2 hidden lg:inline-flex">logout</button>
<button v-if="isLoggedIn()" @click="handleLogout" class="btn ml-2 hidden lg:inline-flex">退出</button>
</nav>
</div>
</header>
Expand All @@ -83,38 +61,63 @@ const navItems = [

<style>
.nav-link {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.45rem;
border: 1px solid transparent;
border-radius: 0.375rem;
padding: 0.45rem 0.75rem;
color: #94a3b8;
font-size: 0.78rem;
line-height: 1rem;
text-decoration: none;
transition: border-color 150ms ease, color 150ms ease, background-color 150ms ease;
transition: border-color 150ms ease, color 150ms ease, background-color 150ms ease, box-shadow 150ms ease;
white-space: nowrap;
}

.nav-link:hover {
border-color: rgba(34, 211, 238, 0.35);
border-color: rgba(103, 232, 249, 0.28);
color: #cbd5e1;
background: rgba(15, 23, 42, 0.8);
background: linear-gradient(180deg, rgba(148, 163, 184, 0.09), rgba(2, 6, 23, 0.35));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

.nav-link.active {
border-color: rgba(34, 211, 238, 0.65);
color: #67e8f9;
background: rgba(8, 47, 73, 0.35);
border-color: rgba(189, 139, 255, 0.3);
color: var(--brand-light);
background: linear-gradient(180deg, color-mix(in srgb, var(--brand-1) 14%, transparent), rgba(2, 6, 23, 0.3));
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--brand-3) 12%, transparent), inset 0 -2px 0 0 color-mix(in srgb, var(--brand-1) 35%, transparent);
}

.nav-link.active::after {
content: "";
position: absolute;
left: 0.5rem;
right: 0.5rem;
bottom: 0.2rem;
height: 2px;
border-radius: 1px;
background: linear-gradient(90deg, var(--brand-1), var(--accent));
box-shadow: 0 0 10px color-mix(in srgb, var(--brand-1) 80%, transparent);
}

.nav-link.active .text-\[10px\] {
color: var(--brand-3);
}

.page-enter-active,
.page-leave-active {
transition: opacity 120ms ease;
transition: opacity 140ms ease, transform 140ms ease;
}

.page-enter-from {
opacity: 0;
transform: translateY(4px);
}

.page-enter-from,
.page-leave-to {
opacity: 0;
transform: translateY(-2px);
}
</style>
58 changes: 58 additions & 0 deletions web/src/components/Banner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<script setup lang="ts">
withDefaults(defineProps<{
message?: string
tone?: 'error' | 'success' | 'info'
}>(), {
tone: 'error',
})

defineSlots<{
default: () => unknown
}>()
</script>

<template>
<div
v-if="message || $slots.default"
role="status"
:aria-live="tone === 'error' ? 'assertive' : 'polite'"
class="mb-4 flex items-start gap-2.5 rounded-md border px-3 py-2 text-sm"
:class="{
'border-red-500/40 bg-red-950/25 text-red-300': tone === 'error',
'border-emerald-500/30 bg-emerald-950/20 text-emerald-300': tone === 'success',
'border-cyan-500/30 bg-cyan-950/20 text-cyan-300': tone === 'info',
}"
>
<svg
v-if="tone === 'error'"
class="mt-0.5 h-4 w-4 shrink-0"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linecap="round"
aria-hidden="true"
>
<circle cx="8" cy="8" r="6.5" />
<path d="M8 4.8v4" />
<circle cx="8" cy="11.4" r="0.6" fill="currentColor" stroke="none" />
</svg>
<svg
v-else-if="tone === 'success'"
class="mt-0.5 h-4 w-4 shrink-0"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.4"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="8" cy="8" r="6.5" />
<polyline points="5 8.2 7.2 10.3 11 6" />
</svg>
<span class="min-w-0">
<slot>{{ message }}</slot>
</span>
</div>
</template>
20 changes: 20 additions & 0 deletions web/src/components/CodeBlock.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup lang="ts">
import CopyButton from './CopyButton.vue'

withDefaults(defineProps<{
code: string
copyable?: boolean
}>(), {
copyable: true,
})
</script>

<template>
<div class="flex items-stretch overflow-hidden rounded-md border border-slate-800 bg-black/50 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
<div class="w-0.5 shrink-0 bg-gradient-to-b from-violet-500/70 via-cyan-400/50 to-transparent" aria-hidden="true" />
<code class="min-w-0 flex-1 whitespace-pre-wrap break-all px-3 py-2 font-mono text-xs text-emerald-300 md:overflow-x-auto md:whitespace-nowrap">{{ code }}</code>
<div v-if="copyable" class="flex shrink-0 items-center border-l border-slate-800/80 px-1.5">
<CopyButton :text="code" />
</div>
</div>
</template>
Loading
Loading