mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
[重构] 前端改造v1
This commit is contained in:
@@ -42,7 +42,7 @@ jobs:
|
||||
if: steps.version.outputs.should_run == 'true'
|
||||
uses: https://github.com/actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Build Frontend
|
||||
if: steps.version.outputs.should_run == 'true'
|
||||
@@ -51,8 +51,9 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
cd atsf_server/web
|
||||
npm install
|
||||
REACT_APP_VERSION="$VERSION" npm run build
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
NEXT_PUBLIC_APP_VERSION="$VERSION" pnpm build
|
||||
|
||||
- name: Set up Go
|
||||
if: steps.version.outputs.should_run == 'true'
|
||||
|
||||
@@ -18,12 +18,13 @@ jobs:
|
||||
CI: ""
|
||||
run: |
|
||||
cd atsf_server/web
|
||||
npm install
|
||||
npm run build
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@releases/v3
|
||||
with:
|
||||
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
BRANCH: gh-pages # The branch the action should deploy to.
|
||||
FOLDER: web/build # The folder the action should deploy.
|
||||
FOLDER: atsf_server/web/build # The folder the action should deploy.
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Build Frontend
|
||||
env:
|
||||
@@ -64,8 +64,9 @@ jobs:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
cd atsf_server/web
|
||||
npm install
|
||||
REACT_APP_VERSION="$VERSION" npm run build
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
NEXT_PUBLIC_APP_VERSION="$VERSION" pnpm build
|
||||
|
||||
- name: Upload Frontend Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
+7
-1
@@ -134,4 +134,10 @@ See:
|
||||
1. [docs/deployment.md](./docs/deployment.md)
|
||||
2. [docs/design.md](./docs/design.md)
|
||||
3. [docs/development-guidelines.md](./docs/development-guidelines.md)
|
||||
4. [docs/development-plan.md](./docs/development-plan.md)
|
||||
4. [docs/development-plan.md](./docs/development-plan.md)
|
||||
|
||||
Frontend notes:
|
||||
|
||||
* The new admin frontend lives in `atsf_server/web`
|
||||
* `pnpm` is the default package manager
|
||||
* `pnpm build` exports static assets into `atsf_server/web/build`
|
||||
@@ -137,3 +137,9 @@ curl -fsSL https://raw.githubusercontent.com/Rain-kl/ATSFlare/main/scripts/insta
|
||||
2. [docs/development-guidelines.md](./docs/development-guidelines.md)
|
||||
3. [docs/development-plan.md](./docs/development-plan.md)
|
||||
|
||||
前端开发补充:
|
||||
|
||||
* 新版管理端位于 `atsf_server/web`
|
||||
* 前端包管理器统一使用 `pnpm`
|
||||
* 构建命令为 `pnpm build`,产物输出到 `atsf_server/web/build`
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
ARG VERSION=dev
|
||||
|
||||
FROM node:18 AS builder
|
||||
FROM node:20 AS builder
|
||||
|
||||
ARG VERSION
|
||||
|
||||
WORKDIR /build
|
||||
COPY ./web/package*.json ./
|
||||
RUN npm install
|
||||
COPY ./web/package.json ./
|
||||
COPY ./web/pnpm-lock.yaml ./
|
||||
RUN corepack enable && pnpm install --frozen-lockfile
|
||||
COPY ./web ./
|
||||
RUN REACT_APP_VERSION="$VERSION" npm run build
|
||||
RUN NEXT_PUBLIC_APP_VERSION="$VERSION" pnpm build
|
||||
|
||||
FROM golang:1.22 AS builder2
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
||||
"ignorePatterns": [
|
||||
".next",
|
||||
"build",
|
||||
"coverage",
|
||||
"playwright-report",
|
||||
"test-results",
|
||||
"src"
|
||||
]
|
||||
}
|
||||
+29
-27
@@ -1,28 +1,30 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.idea
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# build outputs
|
||||
/.next
|
||||
/out
|
||||
/build
|
||||
/coverage
|
||||
/playwright-report
|
||||
/test-results
|
||||
|
||||
# local env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.idea
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
*.db
|
||||
+40
-21
@@ -1,21 +1,40 @@
|
||||
# React Template
|
||||
|
||||
## Basic Usages
|
||||
|
||||
```shell
|
||||
# Runs the app in the development mode
|
||||
npm start
|
||||
|
||||
# Builds the app for production to the `build` folder
|
||||
npm run build
|
||||
```
|
||||
|
||||
If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build,
|
||||
for example: `REACT_APP_SERVER=http://your.domain.com`.
|
||||
|
||||
Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled.
|
||||
|
||||
## Reference
|
||||
|
||||
1. https://github.com/OIerDb-ng/OIerDb
|
||||
2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
|
||||
# ATSFlare Web
|
||||
|
||||
ATSFlare 管理端新版前端已切换为 Next.js App Router + TypeScript + Tailwind CSS 工程。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```shell
|
||||
corepack enable
|
||||
pnpm install
|
||||
|
||||
# 本地开发
|
||||
pnpm dev
|
||||
|
||||
# 类型检查
|
||||
pnpm typecheck
|
||||
|
||||
# 代码检查
|
||||
pnpm lint
|
||||
|
||||
# 单元测试
|
||||
pnpm test
|
||||
|
||||
# 生成静态构建产物到 build/
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 构建说明
|
||||
|
||||
* 构建采用 Next.js 静态导出模式。
|
||||
* `pnpm build` 会先生成 `out/`,随后自动复制为 Go Server 兼容的 `build/` 目录。
|
||||
* 默认 API Base URL 为 `/api`,如需覆盖可在构建时设置 `NEXT_PUBLIC_API_BASE_URL`。
|
||||
* 构建版本号可通过 `NEXT_PUBLIC_APP_VERSION` 注入,例如 `NEXT_PUBLIC_APP_VERSION=v0.4.0 pnpm build`。
|
||||
|
||||
## 目录约定
|
||||
|
||||
* `app/`:路由与布局
|
||||
* `features/`:业务模块
|
||||
* `components/`:复用组件
|
||||
* `lib/`:请求、环境变量、工具与常量
|
||||
* `tests/`:测试代码
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModulePageSkeleton } from '@/features/shared/components/module-page-skeleton';
|
||||
|
||||
export default function ApplyLogsPage() {
|
||||
return (
|
||||
<ModulePageSkeleton
|
||||
title='应用记录'
|
||||
description='阶段 3 将在此接入过滤器、结果状态可视化与分页详情。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModulePageSkeleton } from '@/features/shared/components/module-page-skeleton';
|
||||
|
||||
export default function ConfigVersionsPage() {
|
||||
return (
|
||||
<ModulePageSkeleton
|
||||
title='配置版本'
|
||||
description='阶段 3 将在此接入版本列表、diff 预览、激活与回滚流程。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModulePageSkeleton } from '@/features/shared/components/module-page-skeleton';
|
||||
|
||||
export default function FilesPage() {
|
||||
return (
|
||||
<ModulePageSkeleton
|
||||
title='文件管理'
|
||||
description='阶段 4 将在此接入文件列表、下载与管理交互。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { DashboardShell } from '@/components/layout/dashboard-shell';
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
return <DashboardShell>{children}</DashboardShell>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModulePageSkeleton } from '@/features/shared/components/module-page-skeleton';
|
||||
|
||||
export default function ManagedDomainsPage() {
|
||||
return (
|
||||
<ModulePageSkeleton
|
||||
title='域名管理'
|
||||
description='阶段 3 将在此接入域名列表、证书绑定状态与启用切换。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModulePageSkeleton } from '@/features/shared/components/module-page-skeleton';
|
||||
|
||||
export default function NodesPage() {
|
||||
return (
|
||||
<ModulePageSkeleton
|
||||
title='节点管理'
|
||||
description='阶段 3 将在此接入节点状态标签、部署命令、心跳与更新动作。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DashboardOverview } from '@/features/dashboard/components/dashboard-overview';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<PageHeader
|
||||
title='ATSFlare 管理端工程初始化'
|
||||
description='新版前端已切换到 Next.js 工程骨架,当前页面用于展示阶段 1 交付结果与模块入口。'
|
||||
/>
|
||||
<DashboardOverview />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModulePageSkeleton } from '@/features/shared/components/module-page-skeleton';
|
||||
|
||||
export default function ProxyRoutesPage() {
|
||||
return (
|
||||
<ModulePageSkeleton
|
||||
title='反代规则'
|
||||
description='阶段 3 将在此接入规则列表、创建编辑抽屉与发布动作。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModulePageSkeleton } from '@/features/shared/components/module-page-skeleton';
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<ModulePageSkeleton
|
||||
title='设置'
|
||||
description='阶段 4 将在此接入系统设置、运维设置与个人设置重组。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModulePageSkeleton } from '@/features/shared/components/module-page-skeleton';
|
||||
|
||||
export default function TlsCertificatesPage() {
|
||||
return (
|
||||
<ModulePageSkeleton
|
||||
title='TLS 证书'
|
||||
description='阶段 3 将在此接入证书上传、导入与有效期展示。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModulePageSkeleton } from '@/features/shared/components/module-page-skeleton';
|
||||
|
||||
export default function UsersPage() {
|
||||
return (
|
||||
<ModulePageSkeleton
|
||||
title='用户管理'
|
||||
description='阶段 4 将在此接入用户列表、角色管理与搜索体验。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { PublicShell } from '@/components/layout/public-shell';
|
||||
|
||||
interface PublicLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function PublicLayout({ children }: PublicLayoutProps) {
|
||||
return <PublicShell>{children}</PublicShell>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PublicRoutePlaceholder } from '@/features/auth/components/public-route-placeholder';
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<PublicRoutePlaceholder
|
||||
title='登录页占位'
|
||||
description='阶段 2 将在此接入真实登录表单、Session 兼容与登录后跳转。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PublicRoutePlaceholder } from '@/features/auth/components/public-route-placeholder';
|
||||
|
||||
export default function GithubOAuthPage() {
|
||||
return (
|
||||
<PublicRoutePlaceholder
|
||||
title='GitHub OAuth 回调占位'
|
||||
description='阶段 2 将在此接入授权回调处理、状态提示与跳转逻辑。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PublicRoutePlaceholder } from '@/features/auth/components/public-route-placeholder';
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<PublicRoutePlaceholder
|
||||
title='注册页占位'
|
||||
description='阶段 2 将在此接入注册表单、字段校验与提交流程。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PublicRoutePlaceholder } from '@/features/auth/components/public-route-placeholder';
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<PublicRoutePlaceholder
|
||||
title='重置密码页占位'
|
||||
description='阶段 2 将在此接入找回密码与重置流程。'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@import '../styles/tokens.css';
|
||||
@import 'tailwindcss';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(56, 189, 248, 0.08), transparent 30%),
|
||||
linear-gradient(180deg, rgba(8, 17, 31, 0.98), rgba(8, 17, 31, 1));
|
||||
color: var(--foreground-primary);
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(56, 189, 248, 0.32);
|
||||
color: #f8fafc;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Metadata } from 'next';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { AppProviders } from '@/components/providers/app-providers';
|
||||
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'ATSFlare 控制台',
|
||||
template: '%s | ATSFlare',
|
||||
},
|
||||
description: 'ATSFlare 管理端新版工程骨架',
|
||||
applicationName: 'ATSFlare',
|
||||
};
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: RootLayoutProps) {
|
||||
return (
|
||||
<html lang='zh-CN' suppressHydrationWarning>
|
||||
<body>
|
||||
<AppProviders>{children}</AppProviders>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { AppCard } from '@/components/ui/app-card';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className='flex min-h-screen items-center justify-center px-4'>
|
||||
<AppCard title='页面不存在' description='当前请求的页面尚未接入新版管理端路由。'>
|
||||
<div className='space-y-4 text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
<p>请返回总览页,或继续通过侧边导航访问已初始化的模块入口。</p>
|
||||
<Link href='/' className='inline-flex rounded-full bg-sky-400 px-4 py-2 font-medium text-slate-950 transition hover:bg-sky-300'>
|
||||
返回总览
|
||||
</Link>
|
||||
</div>
|
||||
</AppCard>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function EmptyState({ title, description }: EmptyStateProps) {
|
||||
return (
|
||||
<div className='rounded-2xl border border-dashed border-white/10 bg-white/5 px-5 py-6 text-sm'>
|
||||
<p className='text-base font-semibold text-white'>{title}</p>
|
||||
<p className='mt-2 leading-6 text-[var(--foreground-secondary)]'>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
interface ErrorStateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function ErrorState({ title, description }: ErrorStateProps) {
|
||||
return (
|
||||
<div className='rounded-2xl border border-rose-400/20 bg-rose-500/10 px-5 py-6 text-sm'>
|
||||
<p className='text-base font-semibold text-rose-100'>{title}</p>
|
||||
<p className='mt-2 leading-6 text-rose-100/80'>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AppCard } from '@/components/ui/app-card';
|
||||
import { StatusBadge } from '@/components/ui/status-badge';
|
||||
|
||||
interface FeaturePlaceholderProps {
|
||||
title: string;
|
||||
description: string;
|
||||
milestones: string[];
|
||||
}
|
||||
|
||||
export function FeaturePlaceholder({
|
||||
title,
|
||||
description,
|
||||
milestones,
|
||||
}: FeaturePlaceholderProps) {
|
||||
return (
|
||||
<AppCard
|
||||
title={title}
|
||||
description={description}
|
||||
action={<StatusBadge label='阶段 1 骨架完成' variant='success' />}
|
||||
>
|
||||
<ul className='space-y-3 text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
{milestones.map((item) => (
|
||||
<li key={item} className='flex gap-3'>
|
||||
<span className='mt-2 h-2 w-2 shrink-0 rounded-full bg-sky-400' />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</AppCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
interface LoadingStateProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LoadingState({ className }: LoadingStateProps) {
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div className='h-4 w-32 animate-pulse rounded bg-white/10' />
|
||||
<div className='h-24 animate-pulse rounded-2xl bg-white/5' />
|
||||
<div className='h-24 animate-pulse rounded-2xl bg-white/5' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { DashboardSidebar } from '@/components/layout/dashboard-sidebar';
|
||||
import { DashboardTopbar } from '@/components/layout/dashboard-topbar';
|
||||
|
||||
interface DashboardShellProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function DashboardShell({ children }: DashboardShellProps) {
|
||||
return (
|
||||
<div className='flex min-h-screen bg-transparent text-[var(--foreground-primary)]'>
|
||||
<DashboardSidebar />
|
||||
<div className='flex min-w-0 flex-1 flex-col'>
|
||||
<DashboardTopbar />
|
||||
<main className='flex-1 px-4 py-6 md:px-8 md:py-8'>{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { dashboardNavigation } from '@/lib/constants/navigation';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { isPathActive } from '@/lib/utils/navigation';
|
||||
import { useAppShellStore } from '@/store/app-shell';
|
||||
|
||||
export function DashboardSidebar() {
|
||||
const pathname = usePathname();
|
||||
const currentPath = pathname ?? '/';
|
||||
const isSidebarCollapsed = useAppShellStore((state) => state.isSidebarCollapsed);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'sticky top-0 hidden h-screen shrink-0 border-r border-[var(--border-default)] bg-[var(--surface-panel)]/95 px-3 py-6 backdrop-blur lg:block',
|
||||
isSidebarCollapsed ? 'w-24' : 'w-72',
|
||||
)}
|
||||
>
|
||||
<div className='flex h-full flex-col gap-6'>
|
||||
<div className='flex items-center gap-3 rounded-2xl border border-[var(--border-default)] bg-white/5 px-3 py-3'>
|
||||
<div className='flex h-11 w-11 items-center justify-center rounded-2xl bg-sky-400/15 text-lg font-semibold text-sky-200'>
|
||||
AF
|
||||
</div>
|
||||
{!isSidebarCollapsed ? (
|
||||
<div>
|
||||
<p className='text-sm font-semibold text-white'>ATSFlare</p>
|
||||
<p className='text-xs text-[var(--foreground-secondary)]'>控制面新版工程</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<nav className='flex-1 space-y-2'>
|
||||
{dashboardNavigation.map((item) => {
|
||||
const active = isPathActive(currentPath, item.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-start gap-3 rounded-2xl border px-3 py-3 transition-colors',
|
||||
active
|
||||
? 'border-sky-300/30 bg-sky-400/10 text-white'
|
||||
: 'border-transparent text-[var(--foreground-secondary)] hover:border-[var(--border-default)] hover:bg-white/5 hover:text-white',
|
||||
)}
|
||||
>
|
||||
<span className='mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-white/5 text-xs font-semibold'>
|
||||
{item.shortLabel.slice(0, 1)}
|
||||
</span>
|
||||
{!isSidebarCollapsed ? (
|
||||
<span className='min-w-0'>
|
||||
<span className='block text-sm font-medium'>{item.label}</span>
|
||||
<span className='mt-1 block text-xs leading-5 text-inherit/80'>
|
||||
{item.description}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{!isSidebarCollapsed ? (
|
||||
<div className='rounded-2xl border border-emerald-400/15 bg-emerald-500/10 px-4 py-4 text-xs leading-6 text-emerald-100/90'>
|
||||
阶段 1 已完成目录骨架、静态导出与基础布局初始化。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { getCurrentNavigationItem } from '@/lib/utils/navigation';
|
||||
import { publicEnv } from '@/lib/env/public-env';
|
||||
import { useAppShellStore } from '@/store/app-shell';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function DashboardTopbar() {
|
||||
const pathname = usePathname();
|
||||
const currentPath = pathname ?? '/';
|
||||
const toggleSidebar = useAppShellStore((state) => state.toggleSidebar);
|
||||
const currentItem = getCurrentNavigationItem(currentPath);
|
||||
|
||||
return (
|
||||
<header className='sticky top-0 z-10 border-b border-[var(--border-default)] bg-[var(--surface-panel)]/75 px-4 py-4 backdrop-blur md:px-8'>
|
||||
<div className='flex flex-col gap-4 md:flex-row md:items-center md:justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={toggleSidebar}
|
||||
className='inline-flex h-11 w-11 items-center justify-center rounded-2xl border border-[var(--border-default)] bg-white/5 text-lg text-white transition hover:bg-white/10'
|
||||
aria-label='切换侧边栏'
|
||||
>
|
||||
≡
|
||||
</button>
|
||||
<div>
|
||||
<p className='text-xs uppercase tracking-[0.24em] text-[var(--foreground-secondary)]'>当前模块</p>
|
||||
<h2 className='text-lg font-semibold text-white'>
|
||||
{currentItem?.label ?? 'ATSFlare 控制台'}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-3 text-sm text-[var(--foreground-secondary)]'>
|
||||
<span className='rounded-full border border-[var(--border-default)] px-3 py-1.5'>
|
||||
静态导出模式
|
||||
</span>
|
||||
<span className='rounded-full border border-[var(--border-default)] px-3 py-1.5'>
|
||||
版本 {publicEnv.appVersion}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, description }: PageHeaderProps) {
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<p className='text-sm font-medium uppercase tracking-[0.24em] text-sky-300'>ATSFlare</p>
|
||||
<div className='space-y-2'>
|
||||
<h1 className='text-3xl font-semibold tracking-tight text-white'>{title}</h1>
|
||||
<p className='max-w-3xl text-sm leading-7 text-[var(--foreground-secondary)]'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Link from 'next/link';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { publicEnv } from '@/lib/env/public-env';
|
||||
|
||||
interface PublicShellProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function PublicShell({ children }: PublicShellProps) {
|
||||
return (
|
||||
<div className='flex min-h-screen items-center justify-center px-4 py-12'>
|
||||
<div className='w-full max-w-3xl rounded-[var(--radius-xl)] border border-[var(--border-default)] bg-[var(--surface-panel)]/85 p-8 shadow-[var(--shadow-soft)] backdrop-blur'>
|
||||
<div className='mb-8 flex flex-col gap-4 border-b border-[var(--border-default)] pb-6 md:flex-row md:items-end md:justify-between'>
|
||||
<div>
|
||||
<p className='text-sm font-medium uppercase tracking-[0.24em] text-sky-300'>ATSFlare</p>
|
||||
<h1 className='mt-2 text-3xl font-semibold text-white'>管理端改造进行中</h1>
|
||||
<p className='mt-2 text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
认证流程将在阶段 2 接入。当前页面用于承载入口骨架与路由占位。
|
||||
</p>
|
||||
</div>
|
||||
<div className='text-sm text-[var(--foreground-secondary)]'>
|
||||
<span className='rounded-full border border-[var(--border-default)] px-3 py-1.5'>
|
||||
{publicEnv.appVersion}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-6'>{children}</div>
|
||||
|
||||
<div className='mt-8 border-t border-[var(--border-default)] pt-6 text-sm text-[var(--foreground-secondary)]'>
|
||||
<Link href='/' className='text-sky-300 transition hover:text-sky-200'>
|
||||
返回新版总览
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
interface AppProvidersProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AppProviders({ children }: AppProvidersProps) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
staleTime: 30_000,
|
||||
},
|
||||
mutations: {
|
||||
retry: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
interface AppCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
title?: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
export function AppCard({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: AppCardProps) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'rounded-[var(--radius-xl)] border border-[var(--border-default)] bg-[var(--surface-card)]/90 shadow-[var(--shadow-soft)] backdrop-blur',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{(title || description || action) && (
|
||||
<header className='flex flex-col gap-3 border-b border-[var(--border-default)] px-6 py-5 md:flex-row md:items-start md:justify-between'>
|
||||
<div className='space-y-1'>
|
||||
{title ? <h2 className='text-lg font-semibold text-white'>{title}</h2> : null}
|
||||
{description ? (
|
||||
<p className='text-sm leading-6 text-[var(--foreground-secondary)]'>{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{action ? <div className='shrink-0'>{action}</div> : null}
|
||||
</header>
|
||||
)}
|
||||
<div className='px-6 py-5'>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
const variantClasses = {
|
||||
success: 'border-emerald-400/25 bg-emerald-500/15 text-emerald-200',
|
||||
warning: 'border-amber-400/25 bg-amber-500/15 text-amber-200',
|
||||
danger: 'border-rose-400/25 bg-rose-500/15 text-rose-200',
|
||||
info: 'border-sky-400/25 bg-sky-500/15 text-sky-200',
|
||||
} as const;
|
||||
|
||||
interface StatusBadgeProps {
|
||||
label: string;
|
||||
variant?: keyof typeof variantClasses;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StatusBadge({
|
||||
label,
|
||||
variant = 'info',
|
||||
className,
|
||||
}: StatusBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium tracking-wide',
|
||||
variantClasses[variant],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FeaturePlaceholder } from '@/components/feedback/feature-placeholder';
|
||||
|
||||
interface PublicRoutePlaceholderProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function PublicRoutePlaceholder({
|
||||
title,
|
||||
description,
|
||||
}: PublicRoutePlaceholderProps) {
|
||||
return (
|
||||
<FeaturePlaceholder
|
||||
title={title}
|
||||
description={description}
|
||||
milestones={[
|
||||
'认证页路由入口已建立,便于阶段 2 接入真实表单。',
|
||||
'公共布局已统一,可复用品牌区、说明区与回跳入口。',
|
||||
'后续将在此处补齐表单校验、Session 兼容与 OAuth 回调处理。',
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { AppCard } from '@/components/ui/app-card';
|
||||
import { StatusBadge } from '@/components/ui/status-badge';
|
||||
import { dashboardNavigation } from '@/lib/constants/navigation';
|
||||
|
||||
const readinessItems = [
|
||||
{
|
||||
title: '工程底座',
|
||||
description: 'Next.js App Router、TypeScript strict、Tailwind CSS 与静态导出链路已建立。',
|
||||
},
|
||||
{
|
||||
title: '质量工具',
|
||||
description: 'ESLint、Prettier、Vitest、Playwright 配置已就位,可继续补充模块测试。',
|
||||
},
|
||||
{
|
||||
title: '目录分层',
|
||||
description: '已拆分 app、components、features、lib、store、tests 等基础结构。',
|
||||
},
|
||||
];
|
||||
|
||||
export function DashboardOverview() {
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<AppCard
|
||||
title='阶段 1 已启动'
|
||||
description='当前已完成新版管理端基础工程初始化,可继续推进认证迁移与框架层骨架。'
|
||||
action={<StatusBadge label='可继续阶段 2' variant='success' />}
|
||||
>
|
||||
<div className='grid gap-4 lg:grid-cols-3'>
|
||||
{readinessItems.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className='rounded-2xl border border-[var(--border-default)] bg-white/5 p-4'
|
||||
>
|
||||
<p className='text-base font-semibold text-white'>{item.title}</p>
|
||||
<p className='mt-2 text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<div className='grid gap-6 xl:grid-cols-[1.3fr_0.9fr]'>
|
||||
<AppCard title='模块入口' description='各业务页面已建立路由占位,后续按阶段逐步接入真实数据与交互。'>
|
||||
<div className='grid gap-3 md:grid-cols-2'>
|
||||
{dashboardNavigation.slice(1).map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className='rounded-2xl border border-[var(--border-default)] bg-white/5 p-4 transition hover:border-sky-300/30 hover:bg-sky-400/10'
|
||||
>
|
||||
<p className='text-sm font-semibold text-white'>{item.label}</p>
|
||||
<p className='mt-2 text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
{item.description}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<AppCard title='下一步建议' description='按前端改造计划,后续优先进入认证与框架层迁移。'>
|
||||
<ol className='space-y-3 text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
<li>1. 接入登录、注册、重置密码与 OAuth 回调页面。</li>
|
||||
<li>2. 增加统一鉴权守卫、未登录跳转与消息反馈容器。</li>
|
||||
<li>3. 在业务模块中逐步接入 Query 与 API 资源层。</li>
|
||||
</ol>
|
||||
</AppCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { EmptyState } from '@/components/feedback/empty-state';
|
||||
import { ErrorState } from '@/components/feedback/error-state';
|
||||
import { FeaturePlaceholder } from '@/components/feedback/feature-placeholder';
|
||||
import { LoadingState } from '@/components/feedback/loading-state';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
|
||||
interface ModulePageSkeletonProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function ModulePageSkeleton({
|
||||
title,
|
||||
description,
|
||||
}: ModulePageSkeletonProps) {
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<PageHeader title={title} description={description} />
|
||||
|
||||
<FeaturePlaceholder
|
||||
title={`${title} 页面骨架`}
|
||||
description='当前阶段先完成页面入口、统一头部和反馈组件接线,真实列表、表单与联动能力将在后续业务迁移阶段接入。'
|
||||
milestones={[
|
||||
'路由入口已创建,可纳入统一导航。',
|
||||
'页面标题区、说明区与内容容器已统一。',
|
||||
'加载态、空态、错误态组件已建立,可在真实查询接入后直接复用。',
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className='grid gap-4 xl:grid-cols-3'>
|
||||
<LoadingState />
|
||||
<EmptyState title='空态骨架' description='后续接入查询后,可将资源为空时的提示统一沉淀为此组件。' />
|
||||
<ErrorState title='错误态骨架' description='后续请求失败、鉴权失效与重试提示可在此基础上扩展。' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { publicEnv } from '@/lib/env/public-env';
|
||||
import type { ApiEnvelope } from '@/types/api';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
export function getApiUrl(path: string) {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${publicEnv.apiBaseUrl}${normalizedPath}`;
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(path: string, init?: RequestInit) {
|
||||
const response = await fetch(getApiUrl(path), {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
...init,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(`请求失败(${response.status})`, response.status);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as ApiEnvelope<T>;
|
||||
|
||||
if (!payload.success) {
|
||||
throw new ApiError(payload.message || '请求失败', response.status);
|
||||
}
|
||||
|
||||
return payload.data;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { NavigationItem } from '@/types/navigation';
|
||||
|
||||
export const dashboardNavigation: NavigationItem[] = [
|
||||
{
|
||||
href: '/',
|
||||
label: '总览',
|
||||
shortLabel: '总览',
|
||||
description: '查看阶段推进情况、系统入口与模块骨架状态。',
|
||||
},
|
||||
{
|
||||
href: '/proxy-routes',
|
||||
label: '反代规则',
|
||||
shortLabel: '规则',
|
||||
description: '阶段 3 接入列表、表单与发布动作。',
|
||||
},
|
||||
{
|
||||
href: '/config-versions',
|
||||
label: '配置版本',
|
||||
shortLabel: '版本',
|
||||
description: '阶段 3 接入版本预览、激活与回滚体验。',
|
||||
},
|
||||
{
|
||||
href: '/nodes',
|
||||
label: '节点管理',
|
||||
shortLabel: '节点',
|
||||
description: '阶段 3 接入状态标签、部署命令与更新动作。',
|
||||
},
|
||||
{
|
||||
href: '/apply-logs',
|
||||
label: '应用记录',
|
||||
shortLabel: '记录',
|
||||
description: '阶段 3 接入筛选、分页与详情展示。',
|
||||
},
|
||||
{
|
||||
href: '/managed-domains',
|
||||
label: '域名管理',
|
||||
shortLabel: '域名',
|
||||
description: '阶段 3 接入证书绑定与启用状态切换。',
|
||||
},
|
||||
{
|
||||
href: '/tls-certificates',
|
||||
label: 'TLS 证书',
|
||||
shortLabel: '证书',
|
||||
description: '阶段 3 接入导入、上传与有效期展示。',
|
||||
},
|
||||
{
|
||||
href: '/files',
|
||||
label: '文件管理',
|
||||
shortLabel: '文件',
|
||||
description: '阶段 4 接入列表、下载与管理动作。',
|
||||
},
|
||||
{
|
||||
href: '/users',
|
||||
label: '用户管理',
|
||||
shortLabel: '用户',
|
||||
description: '阶段 4 接入用户列表、角色与搜索体验。',
|
||||
},
|
||||
{
|
||||
href: '/settings',
|
||||
label: '设置',
|
||||
shortLabel: '设置',
|
||||
description: '阶段 4 接入系统设置、运维设置与个人设置。',
|
||||
},
|
||||
];
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const publicEnvSchema = z.object({
|
||||
NEXT_PUBLIC_API_BASE_URL: z.string().default('/api'),
|
||||
NEXT_PUBLIC_APP_VERSION: z.string().default('dev'),
|
||||
});
|
||||
|
||||
const rawEnv = {
|
||||
NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
|
||||
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
};
|
||||
|
||||
const parsedEnv = publicEnvSchema.parse(rawEnv);
|
||||
|
||||
function normalizeBaseUrl(value: string) {
|
||||
if (value === '/') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export const publicEnv = {
|
||||
apiBaseUrl: normalizeBaseUrl(parsedEnv.NEXT_PUBLIC_API_BASE_URL),
|
||||
appVersion: parsedEnv.NEXT_PUBLIC_APP_VERSION,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { dashboardNavigation } from '@/lib/constants/navigation';
|
||||
import type { NavigationItem } from '@/types/navigation';
|
||||
|
||||
export function isPathActive(pathname: string, href: string) {
|
||||
if (href === '/') {
|
||||
return pathname === '/';
|
||||
}
|
||||
|
||||
return pathname === href || pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
export function getCurrentNavigationItem(pathname: string): NavigationItem | undefined {
|
||||
return dashboardNavigation.find((item) => isPathActive(pathname, item.href));
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,13 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
pageExtensions: ['ts', 'tsx'],
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
eslint: {
|
||||
dirs: ['app', 'components', 'features', 'hooks', 'lib', 'store', 'tests', 'types'],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,51 +1,51 @@
|
||||
{
|
||||
"name": "react-template",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"history": "^5.3.0",
|
||||
"marked": "^4.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-toastify": "^9.0.8",
|
||||
"react-turnstile": "^1.0.5",
|
||||
"semantic-ui-css": "^2.5.0",
|
||||
"semantic-ui-react": "^2.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.7.1"
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true
|
||||
},
|
||||
"proxy": "http://localhost:3000"
|
||||
}
|
||||
{
|
||||
"name": "atsflare-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.6.2",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"postbuild": "node scripts/prepare-static-export.mjs",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@tanstack/react-query": "^5.66.8",
|
||||
"clsx": "^2.1.1",
|
||||
"next": "15.2.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@tailwindcss/postcss": "^4.0.9",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "15.2.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"prettier": "^3.5.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.0.9",
|
||||
"typescript": "^5.8.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:3000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
Generated
+2324
-8646
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
const config = {
|
||||
singleQuote: true,
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
plugins: ['prettier-plugin-tailwindcss'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<title>ATSFlare</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const root = process.cwd();
|
||||
const outDir = resolve(root, 'out');
|
||||
const buildDir = resolve(root, 'build');
|
||||
|
||||
if (!existsSync(outDir)) {
|
||||
throw new Error('Missing Next.js export output: out/');
|
||||
}
|
||||
|
||||
rmSync(buildDir, { recursive: true, force: true });
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
cpSync(outDir, buildDir, { recursive: true });
|
||||
@@ -0,0 +1,16 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface AppShellState {
|
||||
isSidebarCollapsed: boolean;
|
||||
toggleSidebar: () => void;
|
||||
setSidebarCollapsed: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const useAppShellStore = create<AppShellState>((set) => ({
|
||||
isSidebarCollapsed: false,
|
||||
toggleSidebar: () =>
|
||||
set((state) => ({
|
||||
isSidebarCollapsed: !state.isSidebarCollapsed,
|
||||
})),
|
||||
setSidebarCollapsed: (value) => set({ isSidebarCollapsed: value }),
|
||||
}));
|
||||
@@ -0,0 +1,19 @@
|
||||
:root {
|
||||
--surface-canvas: #08111f;
|
||||
--surface-panel: #0f172a;
|
||||
--surface-card: #111c33;
|
||||
--surface-muted: #16243d;
|
||||
--border-default: rgba(148, 163, 184, 0.18);
|
||||
--border-strong: rgba(125, 211, 252, 0.28);
|
||||
--foreground-primary: #e2e8f0;
|
||||
--foreground-secondary: #94a3b8;
|
||||
--foreground-muted: #64748b;
|
||||
--brand-primary: #38bdf8;
|
||||
--brand-secondary: #818cf8;
|
||||
--status-success: #22c55e;
|
||||
--status-warning: #f59e0b;
|
||||
--status-danger: #ef4444;
|
||||
--status-info: #38bdf8;
|
||||
--shadow-soft: 0 16px 40px rgba(8, 15, 31, 0.35);
|
||||
--radius-xl: 1.25rem;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
@@ -0,0 +1,14 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { DashboardOverview } from '@/features/dashboard/components/dashboard-overview';
|
||||
|
||||
describe('DashboardOverview', () => {
|
||||
it('renders stage one heading and readiness items', () => {
|
||||
render(<DashboardOverview />);
|
||||
|
||||
expect(screen.getByText('阶段 1 已启动')).toBeInTheDocument();
|
||||
expect(screen.getByText('工程底座')).toBeInTheDocument();
|
||||
expect(screen.getByText('质量工具')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getCurrentNavigationItem, isPathActive } from '@/lib/utils/navigation';
|
||||
|
||||
describe('navigation utils', () => {
|
||||
it('marks root path as active only for home', () => {
|
||||
expect(isPathActive('/', '/')).toBe(true);
|
||||
expect(isPathActive('/nodes', '/')).toBe(false);
|
||||
});
|
||||
|
||||
it('resolves current navigation item for nested paths', () => {
|
||||
expect(getCurrentNavigationItem('/nodes/abc')?.label).toBe('节点管理');
|
||||
expect(getCurrentNavigationItem('/settings')?.label).toBe('设置');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"types": ["node", "vitest/globals"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "build", "src"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface ApiEnvelope<T> {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface NavigationItem {
|
||||
href: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
description: string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./tests/setup/vitest.setup.ts'],
|
||||
include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
|
||||
css: true,
|
||||
},
|
||||
});
|
||||
@@ -94,6 +94,20 @@ volumes:
|
||||
* `REDIS_CONN_STRING` 未配置时,相关能力将回退为进程内实现
|
||||
* `UPLOAD_PATH` 目录在启动时若不存在会自动创建
|
||||
|
||||
### 1.3 前端构建环境变量
|
||||
|
||||
新版管理端位于 `atsf_server/web`,构建时支持以下公开环境变量:
|
||||
|
||||
| 环境变量 | 作用 | 默认值 | 示例 |
|
||||
| --- | --- | --- | --- |
|
||||
| `NEXT_PUBLIC_API_BASE_URL` | 前端请求后端 API 的基础路径;默认走同源 `/api` | `/api` | `NEXT_PUBLIC_API_BASE_URL=https://demo.example.com/api` |
|
||||
| `NEXT_PUBLIC_APP_VERSION` | 构建时注入前端展示版本号 | `dev` | `NEXT_PUBLIC_APP_VERSION=v0.4.0` |
|
||||
|
||||
说明:
|
||||
|
||||
* 以上变量在前端构建阶段读取,并会被打包进静态资源
|
||||
* 推荐生产环境继续使用同源部署,优先保持 `NEXT_PUBLIC_API_BASE_URL=/api`
|
||||
|
||||
---
|
||||
|
||||
## 2. Agent 配置
|
||||
|
||||
+10
-3
@@ -27,10 +27,17 @@
|
||||
|
||||
```bash
|
||||
cd atsf_server/web
|
||||
npm install
|
||||
npm run build
|
||||
corepack enable
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
* 前端使用 Next.js 静态导出模式构建
|
||||
* `pnpm build` 会生成供 Go Server 托管的 `atsf_server/web/build` 目录
|
||||
* 如需覆盖默认接口地址,可在构建前设置 `NEXT_PUBLIC_API_BASE_URL`
|
||||
|
||||
### 2.2 启动服务
|
||||
|
||||
```bash
|
||||
@@ -226,7 +233,7 @@ GOCACHE=/tmp/atsflare-go-cache go test ./...
|
||||
|
||||
```bash
|
||||
cd atsf_server/web
|
||||
npm run build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 6.4 发布工作流
|
||||
|
||||
@@ -43,11 +43,11 @@
|
||||
|
||||
### 2.3 前端
|
||||
|
||||
前端继续基于现有 React 管理端:
|
||||
前端改造专项以 `atsf_server/web` 新版工程为基线:
|
||||
|
||||
* 保持现有目录结构
|
||||
* 继续复用现有 UI 与 helper 组织方式
|
||||
* 不为第三版提前引入新的大型框架或状态管理方案
|
||||
* 使用 Next.js App Router + TypeScript + Tailwind CSS
|
||||
* 按 [docs/frontend-development-guidelines.md](./frontend-development-guidelines.md) 执行目录分层与组件规范
|
||||
* 首期仍以静态导出产物交由 Go Server 托管为前提
|
||||
|
||||
---
|
||||
|
||||
@@ -216,15 +216,15 @@ V3 新增行为:
|
||||
|
||||
要求:
|
||||
|
||||
* 只为当前版本主链路增加页面与交互
|
||||
* API 请求统一放在已有 helper 体系
|
||||
* 页面状态优先保持简单
|
||||
* 沿用现有组件与样式体系,不大规模重构后台 UI
|
||||
* 新前端页面、组件与请求层统一遵循 [docs/frontend-development-guidelines.md](./frontend-development-guidelines.md)
|
||||
* API 请求统一收敛到 `atsf_server/web/lib/api/`
|
||||
* 页面路由与布局放在 `app/`,业务逻辑放在 `features/`
|
||||
* 构建产物必须保持可被 Go Server 静态托管
|
||||
|
||||
如果第三版要新增页面,优先原则:
|
||||
|
||||
* 能复用现有页面结构就不新增一套页面框架
|
||||
* 能复用已有表单模式就不自造 DSL 编辑器
|
||||
* 能复用现有 feature 结构就不平行再造一套页面逻辑
|
||||
* 能复用统一表单、反馈与布局组件就不在页面中重复实现
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -145,10 +145,27 @@
|
||||
* 节点状态有颜色区分:在线(绿色)、离线(红色)、待接入(黄色)
|
||||
* curl 部署命令支持一键复制
|
||||
|
||||
### 3.6 并行专项:管理端前端工程改造
|
||||
|
||||
目标:按 [docs/frontend-revamp-plan.md](./frontend-revamp-plan.md) 推进新版管理端重建,不阻塞 Server/Agent 主链路。
|
||||
|
||||
实施约束:
|
||||
|
||||
1. 前端专项按独立阶段推进,当前从“阶段 1:工程初始化”开始执行
|
||||
2. 首期产物必须保持静态导出,并继续由 `atsf_server` 托管
|
||||
3. 认证迁移、业务模块迁移与最终切换按 `docs/frontend-revamp-plan.md` 的阶段顺序执行
|
||||
|
||||
验收标准:
|
||||
|
||||
* 新前端可本地开发、可静态构建
|
||||
* Go Server 可继续托管新版构建产物
|
||||
* 前端专项与现有第三版主链路互不破坏
|
||||
|
||||
---
|
||||
|
||||
## 4. 阶段执行原则
|
||||
|
||||
* 每个阶段完成后验证验收标准,再进入下一阶段
|
||||
* 阶段间的代码不相互依赖时可并行
|
||||
* 每个阶段完成后运行全量测试
|
||||
* 每个阶段完成后运行全量测试
|
||||
* 管理端前端改造按 [docs/frontend-revamp-plan.md](./frontend-revamp-plan.md) 单独跟踪阶段状态
|
||||
Reference in New Issue
Block a user