HaloLight https://halolight.docs.h7ml.cn HaloLight 多框架管理后台解决方案文档 - 一套设计规范,多框架实现的企业级 Admin Dashboard,支持 Next.js、Vue、Angular 等 12 种框架 Fri, 19 Dec 2025 04:57:55 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed zh-CN Copyright © 2025 h7ml & HaloLight <![CDATA[HaloLight Docs]]> https://halolight.docs.h7ml.cn/README.en https://halolight.docs.h7ml.cn/README.en Fri, 19 Dec 2025 04:56:41 GMT HaloLight Docs

简体中文 | English

Deploy License VitePress Node pnpm Website

HaloLight multi-framework admin dashboard documentation site, built with VitePress, supporting bilingual (Chinese & English).

Project Relations

  • halolight/docs: The single source of truth for documentation and specifications, defining cross-framework design, interfaces, and best practices
  • halolight/halolight: Next.js 14 reference implementation, validating the React path
  • halolight/halolight-vue: Vue 3.5 reference implementation, validating the Vue path

Specification updates land here first, then sync to corresponding implementation repos to ensure documentation and code consistency.

Project Overview

HaloLight is an enterprise-grade admin dashboard solution with multi-framework implementations. Reference implementations:

All 14 frontend frameworks, 7 backend APIs, and 8 deployment solutions have been implemented and deployed. See each repo's README for preview URLs.

Frontend Frameworks

Framework Status Preview Repo Docs
🟦 Next.js 14 ✅ Deployed Preview GitHub Guide
⚛️ React (Vite) ✅ Deployed Preview GitHub Guide
💚 Vue 3.5 ✅ Deployed Preview GitHub Guide
🔺 Angular 21 ✅ Deployed Preview GitHub Guide
🌿 Nuxt 4 ✅ Deployed Preview GitHub Guide
🧡 SvelteKit 2 ✅ Deployed Preview GitHub Guide
🪐 Astro 5 ✅ Deployed Preview GitHub Guide
💠 Solid.js ✅ Deployed Preview GitHub Guide
⚡ Qwik ✅ Deployed Preview GitHub Guide
🎸 Remix ✅ Deployed Preview GitHub Guide
🪶 Preact ✅ Deployed Preview GitHub Guide
🔥 Lit ✅ Deployed Preview GitHub Guide
🦖 Fresh (Deno) ✅ Deployed Preview GitHub Guide
🦕 Deno ✅ Deployed Preview GitHub Guide

Backend APIs

Backend Tech Status Preview Repo Docs
🦜 NestJS 11 ✅ Deployed API Docs GitHub Guide
🐍 Python FastAPI ✅ Deployed API Docs GitHub Guide
☕ Java Spring Boot ✅ Deployed API Docs GitHub Guide
🐹 Go Fiber ✅ Deployed API Docs GitHub Guide
🟩 Node.js Express ✅ Deployed - GitHub Guide
🐘 PHP Laravel ✅ Deployed - GitHub Guide
🍞 Bun + Hono ✅ Deployed - GitHub Guide

Middleware / Full-stack

Project Status Description Repo Docs
🔗 tRPC BFF ✅ Deployed Type-safe API Gateway GitHub Guide
⚡ Next.js Action ✅ Deployed Server Actions full-stack GitHub Guide

Deployment Options

Platform Status Preview Repo Docs
Cloudflare Deployed Preview GitHub Guide
Vercel Deployed Preview GitHub Guide
Netlify Deployed Preview GitHub Guide
Docker Deployed - GitHub Guide
Railway Deployed Preview GitHub Guide
Fly.io Deployed Preview GitHub Guide
Azure ✅ Deployed Preview GitHub Guide
AWS ✅ Deployed Preview GitHub Guide

Extension Projects

Project Status Description Repo Docs
🎨 UI Components ✅ Deployed Stencil Web Components GitHub Guide
🤖 AI Assistant 🚧 In Development RAG + Action Execution GitHub Guide
₿ Web3 Integration 🚧 In Development Wallet Login + On-chain Data GitHub Guide

Core Features

  • Draggable Dashboard - Customizable layout Dashboard system
  • Permission Control - RBAC permission management with wildcard support
  • Theme System - 11 skin presets + light/dark mode
  • Mock Data - Complete data simulation for development
  • Component Library - 30+ components based on shadcn/ui

Development

bash
# Install dependencies
pnpm install

# Start development server
pnpm dev

# Build for production
pnpm build

# Preview build
pnpm preview

Documentation Structure

docs/
├── .vitepress/              # VitePress configuration
│   ├── config.ts           # Main config
│   ├── nav.ts              # Navigation config
│   ├── sidebar.ts          # Sidebar config
│   ├── head.ts             # HTML head config
│   └── pwa.ts              # PWA config
├── guide/                   # User guides (Chinese)
├── en/guide/                # User guides (English)
├── development/             # Development docs (Chinese)
├── en/development/          # Development docs (English)
├── public/                  # Static assets
├── index.md                 # Homepage (Chinese)
└── en/index.md              # Homepage (English)

Tech Stack

Contributing

Contributions are welcome! Feel free to submit Issues and Pull Requests.

  1. Fork this repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Create a Pull Request

License

MIT © 2025 h7ml & HaloLight

]]>
<![CDATA[HaloLight Docs]]> https://halolight.docs.h7ml.cn/README https://halolight.docs.h7ml.cn/README Fri, 19 Dec 2025 04:56:41 GMT HaloLight Docs

English | 简体中文

Deploy License VitePress Node pnpm Website

HaloLight 多框架管理后台项目文档站点,基于 VitePress 构建,支持中英文双语。

项目关系

  • halolight/docs:文档与规范的唯一来源,定义跨框架的设计、接口和最佳实践
  • halolight/halolight:Next.js 14 参考实现,验证规范的 React 路径
  • halolight/halolight-vue:Vue 3.5 参考实现,验证规范的 Vue 路径

规范更新优先在本仓库落地,再同步到对应实现仓库,确保文档与代码一致。

项目概述

HaloLight 是一套多框架实现的企业级管理后台解决方案。参考实现:

所有 14 个前端框架、7 个后端 API、8 个部署方案均已实现并部署,预览地址见各仓库 README。

框架版本

框架 状态 预览 仓库 文档
🟦 Next.js 14 ✅ 已部署 预览 GitHub 指南
⚛️ React (Vite) ✅ 已部署 预览 GitHub 指南
💚 Vue 3.5 ✅ 已部署 预览 GitHub 指南
🔺 Angular 21 ✅ 已部署 预览 GitHub 指南
🌿 Nuxt 4 ✅ 已部署 预览 GitHub 指南
🧡 SvelteKit 2 ✅ 已部署 预览 GitHub 指南
🪐 Astro 5 ✅ 已部署 预览 GitHub 指南
💠 Solid.js ✅ 已部署 预览 GitHub 指南
⚡ Qwik ✅ 已部署 预览 GitHub 指南
🎸 Remix ✅ 已部署 预览 GitHub 指南
🪶 Preact ✅ 已部署 预览 GitHub 指南
🔥 Lit ✅ 已部署 预览 GitHub 指南
🦖 Fresh (Deno) ✅ 已部署 预览 GitHub 指南
🦕 Deno ✅ 已部署 预览 GitHub 指南

后端 API

后端技术 状态 预览 仓库 文档
🦜 NestJS 11 ✅ 已部署 API Docs GitHub 指南
🐍 Python FastAPI ✅ 已部署 API Docs GitHub 指南
☕ Java Spring Boot ✅ 已部署 API Docs GitHub 指南
🐹 Go Fiber ✅ 已部署 API Docs GitHub 指南
🟩 Node.js Express ✅ 已部署 - GitHub 指南
🐘 PHP Laravel ✅ 已部署 - GitHub 指南
🍞 Bun + Hono ✅ 已部署 - GitHub 指南

中间层 / 全栈

项目 状态 说明 仓库 文档
🔗 tRPC BFF ✅ 已部署 类型安全 API 网关 GitHub 指南
⚡ Next.js Action ✅ 已部署 Server Actions 全栈方案 GitHub 指南

部署方案

平台 状态 预览 仓库 文档
☁️ Cloudflare ✅ 已部署 预览 GitHub 指南
▲ Vercel ✅ 已部署 预览 GitHub 指南
🔷 Netlify ✅ 已部署 预览 GitHub 指南
🐳 Docker ✅ 已部署 - GitHub 指南
🚂 Railway ✅ 已部署 预览 GitHub 指南
✈️ Fly.io ✅ 已部署 预览 GitHub 指南
☁️ Azure ✅ 已部署 预览 GitHub 指南
🟠 AWS ✅ 已部署 预览 GitHub 指南

扩展项目

项目 状态 说明 仓库 文档
🎨 UI 组件库 ✅ 已部署 Stencil Web Components GitHub 指南
🤖 AI 助理 🚧 开发中 RAG + 动作执行 GitHub 指南
₿ Web3 集成 🚧 开发中 钱包登录 + 链上数据 GitHub 指南

核心特性

  • 可拖拽仪表盘 - 自定义布局的 Dashboard 系统
  • 权限控制 - RBAC 权限管理,支持通配符
  • 主题系统 - 11 种皮肤预设 + 明暗模式
  • Mock 数据 - 开发环境完整数据模拟
  • 组件库 - 基于 shadcn/ui 30+ 组件

开发

bash
# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

# 构建生产版本
pnpm build

# 预览构建结果
pnpm preview

文档结构

docs/
├── .vitepress/              # VitePress 配置
│   ├── config.ts           # 主配置
│   ├── nav.ts              # 导航栏配置
│   ├── sidebar.ts          # 侧边栏配置
│   ├── head.ts             # HTML head 配置
│   └── pwa.ts              # PWA 配置
├── guide/                   # 使用指南
│   ├── index.md            # 简介
│   ├── getting-started.md  # 快速开始
│   ├── nextjs.md           # 🟦 Next.js
│   ├── vue.md              # 💚 Vue
│   ├── angular.md          # 🔺 Angular
│   ├── nuxt.md             # 🌿 Nuxt
│   ├── sveltekit.md        # 🧡 SvelteKit
│   ├── astro.md            # 🪐 Astro
│   ├── solidjs.md          # 💠 Solid.js
│   ├── qwik.md             # ⚡ Qwik
│   ├── remix.md            # 🎸 Remix
│   ├── preact.md           # 🪶 Preact
│   ├── lit.md              # 🔥 Lit
│   ├── fresh.md            # 🦖 Fresh (Deno)
│   ├── deno.md             # 🦕 Deno + Hono
│   ├── api-go.md           # 🐹 Go API
│   ├── api-node.md         # 🟩 Node.js API
│   ├── admin.md            # 🛠️ Admin 面板
│   ├── cloudflare.md       # ☁️ Cloudflare
│   ├── vercel.md           # ▲ Vercel
│   ├── netlify.md          # 🔷 Netlify
│   ├── docker.md           # 🐳 Docker
│   ├── railway.md          # 🚂 Railway
│   ├── fly.md              # ✈️ Fly.io
│   ├── azure.md            # ☁️ Azure
│   └── aws.md              # 🟠 AWS
├── development/             # 开发文档
│   ├── index.md            # 开发概览
│   ├── architecture.md     # 整体架构
│   ├── components.md       # 组件规范
│   ├── state-management.md # 状态管理
│   ├── api-patterns.md     # API 设计
│   ├── authentication.md   # 认证系统
│   ├── dashboard.md        # 仪表盘
│   ├── theming.md          # 主题系统
│   └── implementation-guide.md # 实现指南
├── public/                  # 静态资源
└── index.md                 # 首页

技术栈

贡献指南

欢迎提交 Issue 和 Pull Request!

  1. Fork 本仓库
  2. 创建功能分支 (git checkout -b feature/amazing-feature)
  3. 提交更改 (git commit -m 'feat: add amazing feature')
  4. 推送分支 (git push origin feature/amazing-feature)
  5. 创建 Pull Request

License

MIT © 2025 h7ml & HaloLight

]]>
<![CDATA[API 层设计]]> https://halolight.docs.h7ml.cn/development/api-patterns https://halolight.docs.h7ml.cn/development/api-patterns Fri, 19 Dec 2025 04:56:41 GMT API 层设计

本文档描述 HaloLight 项目的 API 服务层架构和数据获取策略。

技术选型

框架 HTTP 客户端 缓存层
React/Next.js Axios TanStack Query
Vue 3 Axios TanStack Query
Svelte Fetch TanStack Query
Angular HttpClient RxJS

服务层结构

services/
├── api.ts              # Axios 实例配置
├── auth.ts             # 认证服务
├── users.ts            # 用户服务
├── roles.ts            # 角色服务
├── permissions.ts      # 权限服务
├── dashboard.ts        # 仪表盘服务
├── settings.ts         # 设置服务
└── index.ts            # 统一导出

Axios 实例配置

ts
import axios from 'axios'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 30000,
  headers: { 'Content-Type': 'application/json' },
})

// 请求拦截器
api.interceptors.request.use((config) => {
  const token = useAuthStore.getState().token
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器
api.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    if (error.response?.status === 401) {
      useAuthStore.getState().logout()
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

服务定义规范

ts
// services/users.ts
export const userService = {
  getList: (params: UserQueryParams) =>
    api.get<PaginatedResponse<User>>('/users', { params }),

  getById: (id: string) =>
    api.get<User>(`/users/${id}`),

  create: (data: CreateUserDto) =>
    api.post<User>('/users', data),

  update: (id: string, data: UpdateUserDto) =>
    api.put<User>(`/users/${id}`, data),

  delete: (id: string) =>
    api.delete(`/users/${id}`),
}

TanStack Query Hooks

Query Hook 模式

ts
// hooks/useUsers.ts
export function useUsers(params: UserQueryParams) {
  return useQuery({
    queryKey: ['users', params],
    queryFn: () => userService.getList(params),
    staleTime: 5 * 60 * 1000, // 5分钟
  })
}

export function useUser(id: string) {
  return useQuery({
    queryKey: ['users', id],
    queryFn: () => userService.getById(id),
    enabled: !!id,
  })
}

Mutation Hook 模式

ts
export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: userService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
      toast.success('用户创建成功')
    },
  })
}

Mock 数据系统

Mock.js 配置

ts
// mocks/index.ts
import Mock from 'mockjs'
import './modules/auth'
import './modules/users'
import './modules/dashboard'

Mock.setup({ timeout: '200-600' })

Mock 模块示例

ts
// mocks/modules/users.ts
Mock.mock(/\/api\/users(\?.*)?$/, 'get', (options) => {
  const params = parseQuery(options.url)
  return {
    code: 200,
    data: {
      list: Mock.mock({
        [`list|${params.pageSize || 10}`]: [{
          'id': '@guid',
          'name': '@cname',
          'email': '@email',
          'status|1': ['active', 'inactive'],
          'createdAt': '@datetime',
        }]
      }).list,
      total: 100,
    }
  }
})

错误处理

ts
interface ApiError {
  code: string
  message: string
  details?: Record<string, string[]>
}

// 统一错误处理
function handleApiError(error: ApiError) {
  switch (error.code) {
    case 'VALIDATION_ERROR':
      // 表单验证错误
      break
    case 'UNAUTHORIZED':
      // 未授权
      break
    case 'FORBIDDEN':
      // 无权限
      break
    default:
      toast.error(error.message)
  }
}

分页规范

ts
interface PaginatedResponse<T> {
  list: T[]
  total: number
  page: number
  pageSize: number
  totalPages: number
}

interface PaginationParams {
  page?: number
  pageSize?: number
  sortBy?: string
  sortOrder?: 'asc' | 'desc'
}
]]>
<![CDATA[整体架构]]> https://halolight.docs.h7ml.cn/development/architecture https://halolight.docs.h7ml.cn/development/architecture Fri, 19 Dec 2025 04:56:41 GMT 整体架构

本文档描述 HaloLight 项目的整体架构设计,包括目录结构、分层设计和核心模式。

目录结构规范

标准目录布局

src/
├── app/                    # 页面路由 (Next.js) 或 views/ (Vue)
│   ├── (admin)/           # 管理后台路由组
│   │   ├── dashboard/     # 仪表盘
│   │   ├── users/         # 用户管理
│   │   ├── roles/         # 角色管理
│   │   ├── permissions/   # 权限管理
│   │   ├── settings/      # 系统设置
│   │   └── profile/       # 个人中心
│   └── (auth)/            # 认证路由组
│       ├── login/         # 登录
│       ├── register/      # 注册
│       ├── forgot-password/
│       └── reset-password/
├── components/            # 可复用组件
│   ├── ui/               # 基础 UI 组件 (shadcn/ui)
│   ├── layout/           # 布局组件
│   ├── dashboard/        # 仪表盘组件
│   ├── charts/           # 图表组件
│   └── shared/           # 通用业务组件
├── hooks/ (composables/)  # 可复用逻辑
├── stores/               # 状态管理
├── services/             # API 服务层
├── lib/                  # 工具库
├── types/                # TypeScript 类型定义
├── styles/               # 全局样式
└── mocks/                # Mock 数据

分层架构

四层架构设计

┌─────────────────────────────────────────────────┐
│                   视图层 (Views)                  │
│        Pages / Views / Routes                    │
├─────────────────────────────────────────────────┤
│                  组件层 (Components)              │
│     UI Components / Layout / Dashboard          │
├─────────────────────────────────────────────────┤
│                  状态层 (State)                   │
│     Stores / Composables / Hooks                │
├─────────────────────────────────────────────────┤
│                  服务层 (Services)                │
│     API Services / Data Fetching / Cache        │
└─────────────────────────────────────────────────┘

各层职责

层级 职责 框架实现
视图层 页面路由、布局组装 Next.js Pages / Vue Views
组件层 UI 渲染、用户交互 React/Vue/Svelte Components
状态层 应用状态、业务逻辑 Zustand / Pinia / Stores
服务层 API 调用、数据缓存 TanStack Query / Axios

Provider 层次结构

React/Next.js Provider 链

tsx
<ThemeProvider>
  <MockProvider>
    <QueryClientProvider>
      <AuthProvider>
        <PermissionProvider>
          <WebSocketProvider>
            <ErrorProvider>
              <ToastProvider>
                {children}
              </ToastProvider>
            </ErrorProvider>
          </WebSocketProvider>
        </PermissionProvider>
      </AuthProvider>
    </QueryClientProvider>
  </MockProvider>
</ThemeProvider>

Vue Plugin 注册顺序

ts
app.use(pinia)
app.use(router)
app.use(i18n)
app.use(mockPlugin)
app.use(queryPlugin)
app.use(permissionPlugin)

布局系统

AdminLayout 结构

┌──────────────────────────────────────────────────────┐
│                     Header                            │
│  [Logo] [Breadcrumb]           [Search] [User] [Settings]
├────────────┬─────────────────────────────────────────┤
│            │                                          │
│  Sidebar   │              Content                     │
│            │                                          │
│  - Menu    │   ┌──────────────────────────────────┐  │
│  - Nav     │   │        Page Content               │  │
│            │   │                                   │  │
│            │   └──────────────────────────────────┘  │
│            │                                          │
├────────────┴─────────────────────────────────────────┤
│                     Footer                            │
└──────────────────────────────────────────────────────┘

AuthLayout 结构

┌──────────────────────────────────────────────────────┐
│                                                       │
│                    ┌────────────┐                    │
│                    │            │                    │
│                    │  Auth Form │                    │
│                    │            │                    │
│                    └────────────┘                    │
│                                                       │
└──────────────────────────────────────────────────────┘

路由配置规范

路由元信息

ts
interface RouteMeta {
  title: string           // 页面标题
  icon?: string          // 菜单图标
  permission?: string    // 所需权限
  hidden?: boolean       // 是否在菜单中隐藏
  keepAlive?: boolean    // 是否缓存组件
  breadcrumb?: boolean   // 是否显示面包屑
}

标准路由表

路径 页面 权限
/dashboard 仪表盘 dashboard:view
/users 用户列表 users:list
/users/create 创建用户 users:create
/users/:id 用户详情 users:view
/users/:id/edit 编辑用户 users:update
/roles 角色列表 roles:list
/permissions 权限管理 permissions:list
/settings 系统设置 settings:view
/profile 个人中心 - (登录即可)

环境变量规范

变量命名约定

前缀 框架 说明
NEXT_PUBLIC_ Next.js 客户端可见
VITE_ Vue/Vite 客户端可见
PUBLIC_ SvelteKit 客户端可见
无前缀 所有 仅服务端可见

必需环境变量

bash
# API 配置
*_API_BASE_URL=http://localhost:3000/api
*_API_TIMEOUT=30000

# 认证配置
*_AUTH_SECRET=your-secret-key
*_TOKEN_EXPIRES=7d

# Mock 开关
*_ENABLE_MOCK=true

# 功能开关
*_ENABLE_WEBSOCKET=true
*_ENABLE_ANALYTICS=false

代码组织原则

1。功能优先

按功能模块组织代码,而非按文件类型:

# 推荐 ✅
features/
├── users/
│   ├── components/
│   ├── hooks/
│   ├── services/
│   └── types/

# 避免 ❌
components/
├── UserList.tsx
├── UserForm.tsx
hooks/
├── useUsers.ts
services/
├── userService.ts

2。就近原则

组件专用的样式、类型、工具放在组件目录下:

components/
└── UserCard/
    ├── index.tsx
    ├── UserCard.module.css
    ├── UserCard.types.ts
    └── useUserCard.ts

3。公共提取

多处使用的代码提取到共享位置:

# 2个以上组件使用 → 提取到 components/shared/
# 3个以上地方使用 → 提取到 lib/ 或 utils/

2。规范先行

新增或调整功能时,先在 halolight/docs 明确接口、约束和目录,再同步到 halolighthalolight-vue,避免各实现分叉。

]]>
<![CDATA[认证系统]]> https://halolight.docs.h7ml.cn/development/authentication https://halolight.docs.h7ml.cn/development/authentication Fri, 19 Dec 2025 04:56:41 GMT 认证系统

本文档描述 HaloLight 项目的用户认证和权限控制实现。

认证流程

┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
│  登录   │ ──► │ 验证    │ ──► │ 获取    │ ──► │ 存储    │
│  表单   │     │ 凭证    │     │ Token   │     │ 状态    │
└─────────┘     └─────────┘     └─────────┘     └─────────┘

认证页面

页面 路径 功能
登录 /login 用户名/密码登录
注册 /register 新用户注册
忘记密码 /forgot-password 发送重置邮件
重置密码 /reset-password 设置新密码

Token 管理

双 Token 机制

ts
interface TokenPair {
  accessToken: string   // 短期有效 (15分钟)
  refreshToken: string  // 长期有效 (7天)
}

Token 刷新

ts
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401 && !error.config._retry) {
      error.config._retry = true
      try {
        const { refreshToken } = useAuthStore.getState()
        const newTokens = await authService.refresh(refreshToken)
        useAuthStore.getState().setTokens(newTokens)
        error.config.headers.Authorization = `Bearer ${newTokens.accessToken}`
        return api(error.config)
      } catch {
        useAuthStore.getState().logout()
      }
    }
    return Promise.reject(error)
  }
)

权限系统

权限格式

ts
// 格式: resource:action
const permissions = [
  'users:list',      // 查看用户列表
  'users:create',    // 创建用户
  'users:update',    // 更新用户
  'users:delete',    // 删除用户
  'users:*',         // 用户所有权限
  '*',               // 超级管理员
]

权限检查

ts
function hasPermission(userPerms: string[], required: string): boolean {
  return userPerms.some((p) =>
    p === '*' ||
    p === required ||
    (p.endsWith(':*') && required.startsWith(p.slice(0, -1)))
  )
}

权限组件

tsx
// React
function PermissionGuard({ permission, children, fallback }) {
  const hasPermission = useAuthStore((s) => s.hasPermission)
  return hasPermission(permission) ? children : fallback
}

// 使用
<PermissionGuard permission="users:delete" fallback={null}>
  <DeleteButton />
</PermissionGuard>
vue
<!-- Vue -->
<template>
  <slot v-if="hasPermission(permission)" />
  <slot v-else name="fallback" />
</template>

权限指令 (Vue)

ts
// v-permission 指令
app.directive('permission', {
  mounted(el, binding) {
    const authStore = useAuthStore()
    if (!authStore.hasPermission(binding.value)) {
      el.parentNode?.removeChild(el)
    }
  },
})

// 使用
<button v-permission="'users:delete'">删除</button>

路由守卫

Next.js Middleware

ts
// middleware.ts
export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')
  const isAuthPage = request.nextUrl.pathname.startsWith('/login')

  if (!token && !isAuthPage) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  if (token && isAuthPage) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }
}

Vue Router Guard

ts
router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()

  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next({ name: 'login', query: { redirect: to.fullPath } })
    return
  }

  if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) {
    next({ name: '403' })
    return
  }

  next()
})

标准权限列表

ts
const PERMISSIONS = {
  // 仪表盘
  'dashboard:view': '查看仪表盘',
  'dashboard:edit': '编辑仪表盘',

  // 用户管理
  'users:list': '查看用户列表',
  'users:view': '查看用户详情',
  'users:create': '创建用户',
  'users:update': '更新用户',
  'users:delete': '删除用户',

  // 角色管理
  'roles:list': '查看角色列表',
  'roles:create': '创建角色',
  'roles:update': '更新角色',
  'roles:delete': '删除角色',

  // 权限管理
  'permissions:list': '查看权限列表',
  'permissions:assign': '分配权限',

  // 系统设置
  'settings:view': '查看设置',
  'settings:update': '更新设置',
}

角色预设

ts
const ROLES = {
  admin: {
    name: '管理员',
    permissions: ['*'],
  },
  manager: {
    name: '经理',
    permissions: ['dashboard:*', 'users:list', 'users:view'],
  },
  user: {
    name: '普通用户',
    permissions: ['dashboard:view'],
  },
}
]]>
<![CDATA[组件规范]]> https://halolight.docs.h7ml.cn/development/components https://halolight.docs.h7ml.cn/development/components Fri, 19 Dec 2025 04:56:41 GMT 组件规范

本文档定义 HaloLight 项目的 UI 组件库规范,基于 shadcn/ui 设计系统。

组件库概览

基于 shadcn/ui

所有框架版本都使用对应的 shadcn/ui 实现:

框架 组件库 仓库
React/Next.js shadcn/ui shadcn/ui
Vue 3 shadcn-vue shadcn-vue
Svelte shadcn-svelte shadcn-svelte
Angular spartan/ui spartan
Solid.js solid-ui solid-ui

必需组件清单

基础 UI 组件 (30+)

ui/
├── accordion.tsx        # 折叠面板
├── alert-dialog.tsx     # 确认对话框
├── alert.tsx           # 警告提示
├── avatar.tsx          # 头像
├── badge.tsx           # 徽章
├── breadcrumb.tsx      # 面包屑
├── button.tsx          # 按钮
├── calendar.tsx        # 日历
├── card.tsx            # 卡片
├── checkbox.tsx        # 复选框
├── collapsible.tsx     # 可折叠区域
├── command.tsx         # 命令面板
├── data-table.tsx      # 数据表格
├── date-picker.tsx     # 日期选择器
├── dialog.tsx          # 对话框
├── dropdown-menu.tsx   # 下拉菜单
├── form.tsx            # 表单
├── input.tsx           # 输入框
├── label.tsx           # 标签
├── pagination.tsx      # 分页
├── popover.tsx         # 弹出层
├── progress.tsx        # 进度条
├── radio-group.tsx     # 单选组
├── scroll-area.tsx     # 滚动区域
├── select.tsx          # 选择器
├── separator.tsx       # 分隔线
├── sheet.tsx           # 侧边抽屉
├── skeleton.tsx        # 骨架屏
├── slider.tsx          # 滑块
├── switch.tsx          # 开关
├── table.tsx           # 表格
├── tabs.tsx            # 标签页
├── textarea.tsx        # 文本域
├── toast.tsx           # 轻提示
├── tooltip.tsx         # 工具提示
└── sonner.tsx          # Toast 通知

布局组件

layout/
├── AdminLayout.tsx      # 管理后台主布局
├── AuthLayout.tsx       # 认证页面布局
├── Sidebar.tsx          # 侧边栏
├── Header.tsx           # 顶部栏
├── Footer.tsx           # 底部栏
├── Breadcrumb.tsx       # 面包屑导航
├── TabsNav.tsx          # 标签页导航
└── PageContainer.tsx    # 页面容器

仪表盘组件

dashboard/
├── DashboardGrid.tsx    # 可拖拽网格容器
├── WidgetWrapper.tsx    # Widget 包装器
├── StatsWidget.tsx      # 统计数据卡片
├── ChartWidget.tsx      # 图表 Widget
├── TableWidget.tsx      # 表格 Widget
├── CalendarWidget.tsx   # 日历 Widget
├── TasksWidget.tsx      # 任务列表
└── QuickActionsWidget.tsx # 快捷操作

图表组件

charts/
├── LineChart.tsx        # 折线图
├── BarChart.tsx         # 柱状图
├── PieChart.tsx         # 饼图
├── AreaChart.tsx        # 面积图
├── RadarChart.tsx       # 雷达图
└── GaugeChart.tsx       # 仪表盘

组件设计规范

1。Props 接口设计

tsx
// 基础 Props 结构
interface ComponentProps {
  // 必需属性在前
  children: React.ReactNode

  // 可选属性按字母排序
  className?: string
  disabled?: boolean
  loading?: boolean
  size?: 'sm' | 'md' | 'lg'
  variant?: 'default' | 'outline' | 'ghost'

  // 事件处理函数
  onChange?: (value: T) => void
  onClick?: () => void
}

2。样式变体

使用 cva (class-variance-authority) 定义变体:

tsx
import { cva, type VariantProps } from 'class-variance-authority'

const buttonVariants = cva(
  // 基础样式
  'inline-flex items-center justify-center rounded-md font-medium transition-colors',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        outline: 'border border-input bg-background hover:bg-accent',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
      },
      size: {
        sm: 'h-8 px-3 text-xs',
        md: 'h-9 px-4 text-sm',
        lg: 'h-10 px-6 text-base',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  loading?: boolean
}

3。可访问性 (a11y)

所有组件必须支持:

tsx
// ARIA 属性
<button
  role="button"
  aria-label={ariaLabel}
  aria-disabled={disabled}
  aria-busy={loading}
>

// 键盘导航
const handleKeyDown = (e: KeyboardEvent) => {
  if (e.key === 'Enter' || e.key === ' ') {
    onClick?.()
  }
  if (e.key === 'Escape') {
    onClose?.()
  }
}

// 焦点管理
const focusRef = useRef<HTMLElement>(null)
useEffect(() => {
  if (open) {
    focusRef.current?.focus()
  }
}, [open])

4。响应式设计

tsx
// Tailwind 断点
const breakpoints = {
  sm: '640px',   // 手机横屏
  md: '768px',   // 平板
  lg: '1024px',  // 小型桌面
  xl: '1280px',  // 桌面
  '2xl': '1536px' // 大屏
}

// 响应式类名示例
<div className="
  grid
  grid-cols-1
  sm:grid-cols-2
  md:grid-cols-3
  lg:grid-cols-4
  gap-4
">

布局组件规范

AdminLayout

tsx
interface AdminLayoutProps {
  children: React.ReactNode
}

// 布局结构
<div className="min-h-screen bg-background">
  <Sidebar />
  <div className="flex flex-col lg:ml-64">
    <Header />
    <main className="flex-1 p-6">
      <PageContainer>
        {children}
      </PageContainer>
    </main>
    <Footer />
  </div>
</div>
tsx
interface SidebarState {
  collapsed: boolean      // 是否折叠
  mobileOpen: boolean    // 移动端是否展开
  activeMenu: string     // 当前激活菜单
  openMenus: string[]    // 展开的子菜单
}

// 折叠宽度
const SIDEBAR_WIDTH = 256        // 展开时 16rem
const SIDEBAR_COLLAPSED = 64     // 折叠时 4rem

Header 组件

tsx
interface HeaderProps {
  showBreadcrumb?: boolean
  showSearch?: boolean
  showNotifications?: boolean
  showUserMenu?: boolean
}

// 组成部分
<header className="h-16 border-b bg-background/95 backdrop-blur">
  <div className="flex items-center justify-between px-4 h-full">
    {/* 左侧 */}
    <div className="flex items-center gap-4">
      <SidebarTrigger />
      <Breadcrumb />
    </div>

    {/* 右侧 */}
    <div className="flex items-center gap-2">
      <GlobalSearch />
      <ThemeToggle />
      <NotificationDropdown />
      <UserDropdown />
    </div>
  </div>
</header>

表单组件规范

表单字段结构

tsx
<FormField
  control={form.control}
  name="email"
  render={({ field }) => (
    <FormItem>
      <FormLabel>邮箱</FormLabel>
      <FormControl>
        <Input placeholder="请输入邮箱" {...field} />
      </FormControl>
      <FormDescription>
        我们不会分享你的邮箱
      </FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

表单验证

tsx
// Zod Schema
const formSchema = z.object({
  username: z.string().min(2, '用户名至少2个字符').max(50),
  email: z.string().email('请输入有效的邮箱地址'),
  password: z.string().min(8, '密码至少8个字符'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: '两次密码输入不一致',
  path: ['confirmPassword'],
})

数据表格规范

DataTable 功能

tsx
interface DataTableProps<T> {
  columns: ColumnDef<T>[]
  data: T[]

  // 分页
  pagination?: boolean
  pageSize?: number

  // 排序
  sorting?: boolean
  defaultSort?: { id: string; desc: boolean }

  // 筛选
  filtering?: boolean
  globalFilter?: boolean

  // 选择
  selection?: boolean
  onSelectionChange?: (rows: T[]) => void

  // 操作
  actions?: (row: T) => React.ReactNode
}

列定义示例

tsx
const columns: ColumnDef<User>[] = [
  {
    id: 'select',
    header: ({ table }) => (
      <Checkbox
        checked={table.getIsAllPageRowsSelected()}
        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
      />
    ),
  },
  {
    accessorKey: 'name',
    header: '姓名',
    cell: ({ row }) => <span className="font-medium">{row.getValue('name')}</span>,
  },
  {
    accessorKey: 'email',
    header: ({ column }) => (
      <Button variant="ghost" onClick={() => column.toggleSorting()}>
        邮箱
        <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  {
    accessorKey: 'status',
    header: '状态',
    cell: ({ row }) => (
      <Badge variant={row.getValue('status') === 'active' ? 'default' : 'secondary'}>
        {row.getValue('status')}
      </Badge>
    ),
    filterFn: (row, id, value) => value.includes(row.getValue(id)),
  },
  {
    id: 'actions',
    cell: ({ row }) => <RowActions row={row.original} />,
  },
]
]]>
<![CDATA[仪表盘]]> https://halolight.docs.h7ml.cn/development/dashboard https://halolight.docs.h7ml.cn/development/dashboard Fri, 19 Dec 2025 04:56:41 GMT 仪表盘

本文档描述 HaloLight 可拖拽仪表盘的实现规范。

技术选型

框架 拖拽库
React/Next.js react-grid-layout
Vue 3 grid-layout-plus
Svelte svelte-grid
Angular angular-gridster2

Widget 类型

ID 类型 默认尺寸 描述
stats 统计卡片 3x2 数字统计展示
chart-line 折线图 6x4 趋势数据
chart-bar 柱状图 6x4 对比数据
chart-pie 饼图 4x4 占比数据
recent-users 最近用户 4x4 用户列表
notifications 通知 4x4 消息列表
tasks 任务 4x4 待办事项
calendar 日历 4x4 日程安排
quick-actions 快捷操作 3x2 常用功能

布局配置

响应式断点

ts
const breakpoints = { lg: 1200, md: 996, sm: 768 }
const cols = { lg: 12, md: 8, sm: 4 }

布局数据结构

ts
interface GridLayout {
  i: string      // Widget ID
  x: number      // 列位置 (0-based)
  y: number      // 行位置
  w: number      // 宽度 (列数)
  h: number      // 高度 (行数)
  minW?: number  // 最小宽度
  minH?: number  // 最小高度
  static?: boolean // 是否固定
}

默认布局

ts
const defaultLayouts = {
  lg: [
    { i: 'stats-1', x: 0, y: 0, w: 3, h: 2 },
    { i: 'stats-2', x: 3, y: 0, w: 3, h: 2 },
    { i: 'stats-3', x: 6, y: 0, w: 3, h: 2 },
    { i: 'stats-4', x: 9, y: 0, w: 3, h: 2 },
    { i: 'chart-line', x: 0, y: 2, w: 8, h: 4 },
    { i: 'chart-pie', x: 8, y: 2, w: 4, h: 4 },
    { i: 'recent-users', x: 0, y: 6, w: 4, h: 4 },
    { i: 'tasks', x: 4, y: 6, w: 4, h: 4 },
    { i: 'notifications', x: 8, y: 6, w: 4, h: 4 },
  ],
}

React 实现

tsx
import GridLayout, { Responsive, WidthProvider } from 'react-grid-layout'

const ResponsiveGridLayout = WidthProvider(Responsive)

function Dashboard() {
  const { layouts, updateLayout, isEditing } = useDashboardStore()

  return (
    <ResponsiveGridLayout
      layouts={layouts}
      breakpoints={{ lg: 1200, md: 996, sm: 768 }}
      cols={{ lg: 12, md: 8, sm: 4 }}
      rowHeight={80}
      isDraggable={isEditing}
      isResizable={isEditing}
      onLayoutChange={(layout, allLayouts) => {
        updateLayout(allLayouts)
      }}
    >
      {widgets.map((widget) => (
        <div key={widget.id}>
          <WidgetWrapper widget={widget} />
        </div>
      ))}
    </ResponsiveGridLayout>
  )
}

Vue 实现

vue
<template>
  <GridLayout
    v-model:layout="layout"
    :col-num="12"
    :row-height="80"
    :is-draggable="isEditing"
    :is-resizable="isEditing"
  >
    <GridItem
      v-for="item in layout"
      :key="item.i"
      :x="item.x"
      :y="item.y"
      :w="item.w"
      :h="item.h"
    >
      <WidgetWrapper :widget="getWidget(item.i)" />
    </GridItem>
  </GridLayout>
</template>

<script setup>
import { GridLayout, GridItem } from 'grid-layout-plus'
</script>

Widget 组件

WidgetWrapper

tsx
interface WidgetWrapperProps {
  widget: WidgetConfig
  onRemove?: () => void
}

function WidgetWrapper({ widget, onRemove }: WidgetWrapperProps) {
  const { isEditing } = useDashboardStore()

  return (
    <Card className="h-full flex flex-col">
      <CardHeader className="flex-row items-center justify-between py-2">
        <CardTitle className="text-sm">{widget.title}</CardTitle>
        {isEditing && (
          <Button variant="ghost" size="icon" onClick={onRemove}>
            <X className="h-4 w-4" />
          </Button>
        )}
      </CardHeader>
      <CardContent className="flex-1 overflow-hidden">
        <WidgetContent type={widget.type} settings={widget.settings} />
      </CardContent>
    </Card>
  )
}

StatsWidget

tsx
function StatsWidget({ title, value, change, icon: Icon }) {
  return (
    <div className="flex items-center justify-between">
      <div>
        <p className="text-sm text-muted-foreground">{title}</p>
        <p className="text-2xl font-bold">{value}</p>
        <p className={cn('text-xs', change > 0 ? 'text-green-500' : 'text-red-500')}>
          {change > 0 ? '+' : ''}{change}%
        </p>
      </div>
      <Icon className="h-8 w-8 text-muted-foreground" />
    </div>
  )
}

ECharts 集成

主题适配

ts
const getChartTheme = (isDark: boolean) => ({
  backgroundColor: 'transparent',
  textStyle: { color: isDark ? '#e5e5e5' : '#333' },
  axisLine: { lineStyle: { color: isDark ? '#444' : '#ccc' } },
  splitLine: { lineStyle: { color: isDark ? '#333' : '#eee' } },
})

响应式尺寸

tsx
function ChartWidget({ option }) {
  const chartRef = useRef<EChartsInstance>()

  useEffect(() => {
    const observer = new ResizeObserver(() => {
      chartRef.current?.resize()
    })
    observer.observe(containerRef.current)
    return () => observer.disconnect()
  }, [])

  return <ReactECharts ref={chartRef} option={option} />
}

编辑模式

工具栏

tsx
function DashboardToolbar() {
  const { isEditing, toggleEditing, resetLayout } = useDashboardStore()

  return (
    <div className="flex gap-2">
      <Button variant="outline" onClick={toggleEditing}>
        {isEditing ? <Check /> : <Edit />}
        {isEditing ? '完成' : '编辑'}
      </Button>
      {isEditing && (
        <>
          <AddWidgetButton />
          <Button variant="outline" onClick={resetLayout}>
            <RotateCcw /> 重置
          </Button>
        </>
      )}
    </div>
  )
}

持久化

ts
// 布局保存到 localStorage
const useDashboardStore = create(
  persist(
    (set) => ({
      layouts: defaultLayouts,
      updateLayout: (layouts) => set({ layouts }),
    }),
    { name: 'dashboard-layout' }
  )
)
]]>
<![CDATA[HaloLight 生态系统]]> https://halolight.docs.h7ml.cn/development/ecosystem https://halolight.docs.h7ml.cn/development/ecosystem Fri, 19 Dec 2025 04:56:41 GMT HaloLight 生态系统

HaloLight 是一个多框架、多平台的后台管理系统解决方案。本文档列出所有项目及其状态。

项目总览

前端框架实现

项目 框架 状态 说明
halolight Next.js 14 + React 18 ✅ 已发布 参考实现
halolight-react React + Vite ✅ 已发布 纯 SPA 版本
halolight-vue Vue 3.5 + Vite ✅ 已发布 Vue 参考实现
halolight-angular Angular 21 ✅ 已发布 Angular 实现
halolight-nuxt Nuxt 3 ✅ 已发布 Vue SSR 版本
halolight-svelte SvelteKit ✅ 已发布 Svelte 实现
halolight-astro Astro ✅ 已发布 静态优先
halolight-solid SolidJS ✅ 已发布 高性能响应式
halolight-qwik Qwik ✅ 已发布 可恢复式
halolight-remix Remix ✅ 已发布 全栈 React
halolight-preact Preact ✅ 已发布 轻量级 React
halolight-lit Lit ✅ 已发布 Web Components
halolight-fresh Fresh (Deno) ✅ 已发布 Deno 原生
halolight-deno Fresh (Deno) ✅ 已发布 Deno 实现

部署平台

项目 平台 状态 特性
halolight-cloudflare Cloudflare Pages/Workers ✅ 已发布 边缘运行时, Next.js 15
halolight-vercel Vercel ✅ 已发布 Edge Functions
halolight-netlify Netlify ✅ 已发布 Edge Functions
halolight-aws AWS Amplify ✅ 已发布 Lambda@Edge
halolight-azure Azure Static Web Apps ✅ 已发布 Azure Functions
halolight-fly Fly.io ✅ 已发布 全球部署
halolight-railway Railway ✅ 已发布 一键部署
halolight-docker Docker 自托管 ✅ 已发布 Traefik 反向代理

后端 API

项目 技术栈 状态 特性
halolight-api-nestjs NestJS + Prisma + TypeScript ✅ 已发布 Node.js 企业级
halolight-api-node Express + Prisma + TypeScript ✅ 已发布 Node.js 参考实现
halolight-api-go Gin + GORM ✅ 已发布 高性能
halolight-api-python FastAPI + SQLAlchemy + Alembic ✅ 已发布 Python 生态
halolight-api-bun Hono + Drizzle ORM ✅ 已发布 Bun 运行时
halolight-api-java Spring Boot 3.4 + JPA ✅ 已发布 企业级 Java
halolight-api-php Laravel + Eloquent ✅ 已发布 PHP 生态

基础设施

项目 用途 状态 特性
halolight-bff tRPC 网关 ✅ 已发布 类型安全 API
halolight-action Next.js 全栈 ✅ 已发布 Server Actions
halolight-ui Stencil Web Components ✅ 已发布 跨框架组件库

智能化 & Web3

项目 用途 状态 特性
halolight-ai AI 智能助理 ✅ 已发布 RAG + 动作执行
halolight-web3 Web3 集成 ✅ 已发布 EVM + Solana + IPFS

技术栈对比

前端框架

React 系:     Next.js → Remix → Preact → React (Vite)
Vue 系:       Vue 3.5 → Nuxt 3
其他:         Angular → SvelteKit → SolidJS → Qwik → Lit → Astro → Fresh → Deno

后端语言

Node.js:      NestJS (Prisma) → Express (Prisma) → Hono (Drizzle)
Go:           Gin (GORM)
Python:       FastAPI (SQLAlchemy)
Java:         Spring Boot (JPA)
PHP:          Laravel (Eloquent)

部署平台

边缘运行时:    Cloudflare → Vercel → Netlify
云平台:        AWS Amplify → Azure SWA → Fly.io → Railway
自托管:        Docker + Traefik

快速开始

选择前端框架

bash
# React (推荐)
git clone https://github.com/halolight/halolight
cd halolight && pnpm install && pnpm dev

# Vue
git clone https://github.com/halolight/halolight-vue
cd halolight-vue && pnpm install && pnpm dev

# Angular
git clone https://github.com/halolight/halolight-angular
cd halolight-angular && pnpm install && pnpm start

选择后端 API

bash
# Node.js (推荐)
git clone https://github.com/halolight/halolight-api-node
cd halolight-api-node && pnpm install && pnpm dev

# Go
git clone https://github.com/halolight/halolight-api-go
cd halolight-api-go && go run main.go

# Python
git clone https://github.com/halolight/halolight-api-python
cd halolight-api-python && pip install -r requirements.txt && uvicorn main:app --reload

# Java
git clone https://github.com/halolight/halolight-api-java
cd halolight-api-java && mvn spring-boot:run

选择部署平台

bash
# Cloudflare (推荐边缘)
git clone https://github.com/halolight/halolight-cloudflare
cd halolight-cloudflare && pnpm install && pnpm deploy

# Docker (推荐自托管)
git clone https://github.com/halolight/halolight-docker
cd halolight-docker && docker-compose up -d

组件库使用

安装 halolight-ui

bash
npm install @halolight/ui

在 HTML 中使用

html
<script type="module" src="https://unpkg.com/@halolight/ui/dist/halolight-ui.esm.js"></script>

<hl-button variant="primary">Click me</hl-button>
<hl-input label="Email" type="email"></hl-input>
<hl-card>
  <h3 slot="header">Card Title</h3>
  <p>Card content</p>
</hl-card>

在 React 中使用

tsx
import { defineCustomElements } from '@halolight/ui/loader';
defineCustomElements();

function App() {
  return <hl-button variant="primary">Click me</hl-button>;
}

在 Vue 中使用

vue
<script setup>
import { defineCustomElements } from '@halolight/ui/loader';
defineCustomElements();
</script>

<template>
  <hl-button variant="primary">Click me</hl-button>
</template>

AI 助理集成

部署 halolight-ai

bash
git clone https://github.com/halolight/halolight-ai
cd halolight-ai
cp .env.example .env
# 配置 OPENAI_API_KEY 或其他 LLM 密钥
docker-compose up -d

API 调用

bash
# 发送消息
curl -X POST http://localhost:3000/api/ai/chat \
  -H "Content-Type: application/json" \
  -H "X-Tenant-ID: default" \
  -H "X-User-ID: user1" \
  -d '{"message": "帮我分析今日数据"}'

# 执行动作
curl -X POST http://localhost:3000/api/ai/actions/execute \
  -H "Content-Type: application/json" \
  -d '{"action": "query_users", "params": {"role": "admin"}}'

Web3 集成

安装依赖

bash
# 核心包
npm install @halolight/web3-core

# React 组件
npm install @halolight/web3-react

# Vue 组件
npm install @halolight/web3-vue

React 示例

tsx
import { Web3Provider, WalletButton, TokenBalance } from '@halolight/web3-react';

function App() {
  return (
    <Web3Provider>
      <WalletButton />
      <TokenBalance />
    </Web3Provider>
  );
}

贡献指南

  1. Fork 对应仓库
  2. 创建功能分支:git checkout -b feature/xxx
  3. 提交更改:git commit -m 'feat: xxx'
  4. 推送分支:git push origin feature/xxx
  5. 提交 Pull Request

许可证

所有 HaloLight 项目均采用 MIT 许可证。

]]>
<![CDATA[实现指南]]> https://halolight.docs.h7ml.cn/development/implementation-guide https://halolight.docs.h7ml.cn/development/implementation-guide Fri, 19 Dec 2025 04:56:41 GMT 实现指南

本指南帮助开发者为 HaloLight 创建新的框架版本实现。

共用约束

  • 数据模拟:使用 Mock.js 结合 fetch 拦截,行为与 halolight/src/mock 对齐 (响应格式、延时、错误码)
  • 认证流程:登录/注册/忘记密码/重置密码参考 halolight/src/app/(auth) 的页面流与校验逻辑
  • 环境变量:各框架复用同一变量命名 (如 *_API_URL*_USE_MOCK*_DEMO_**_BRAND_NAME),保持文档一致
  • CI/CD 与测试:沿用通用工作流 (lint + type-check + test + build + security),保留单元测试与覆盖率任务

实现清单

第一阶段:项目初始化

  • [ ] 使用框架 CLI 创建项目
  • [ ] 配置 TypeScript
  • [ ] 安装 Tailwind CSS
  • [ ] 安装 shadcn/ui 对应版本
  • [ ] 配置环境变量
  • [ ] 设置路径别名 (@/)

第二阶段:基础架构

  • [ ] 创建目录结构
  • [ ] 实现 API 服务层
  • [ ] 配置 TanStack Query
  • [ ] 设置 Mock.js fetch 拦截 (复用 halolight/src/mock 数据结构)
  • [ ] 实现状态管理 Store

第三阶段:认证系统

  • [ ] 登录/注册/忘记密码/重置密码页面 (参考 Next.js 版本交互)
  • [ ] Auth Store (含 Token 持久化)
  • [ ] 路由守卫
  • [ ] 权限检查 (页面级与按钮级)

第四阶段:布局组件

  • [ ] AdminLayout
  • [ ] AuthLayout
  • [ ] Sidebar
  • [ ] Header
  • [ ] Footer
  • [ ] Breadcrumb
  • [ ] TabsNav

第五阶段:核心页面

  • [ ] Dashboard (可拖拽仪表盘)
  • [ ] 用户管理 (CRUD)
  • [ ] 角色管理
  • [ ] 权限管理
  • [ ] 系统设置
  • [ ] 个人中心

第六阶段:增强功能

  • [ ] 主题切换
  • [ ] 皮肤预设
  • [ ] 多语言 (可选)
  • [ ] 响应式适配
  • [ ] 错误边界
  • [ ] CI/CD (lint、type-check、test、build、security) 与覆盖率报告

快速开始

1。创建项目

bash
# Next.js
npx create-next-app@latest halolight --typescript --tailwind --app

# Vue
npm create vue@latest halolight-vue

# SvelteKit
npx sv create halolight-svelte

# Angular
ng new halolight-angular --routing --style=scss

2。安装依赖

bash
# 通用依赖
npm install axios @tanstack/react-query zustand
npm install -D tailwindcss postcss autoprefixer

# shadcn/ui
npx shadcn@latest init

3。目录结构

src/
├── app/              # 页面
├── components/
│   ├── ui/          # shadcn 组件
│   ├── layout/      # 布局组件
│   └── dashboard/   # 仪表盘组件
├── hooks/           # 自定义 hooks
├── stores/          # 状态管理
├── services/        # API 服务
├── lib/             # 工具函数
├── types/           # 类型定义
└── mocks/           # Mock 数据

框架对照表

组件语法

概念 React Vue Svelte
组件 function Component() <script setup> <script>
Props props: Props defineProps<Props>() export let prop
State useState() ref() let state = $state()
计算 useMemo() computed() $derived()
副作用 useEffect() watch() $effect()
上下文 useContext() provide/inject setContext()

路由

概念 Next.js Vue Router SvelteKit
文件路由 app/page.tsx - routes/+page.svelte
动态路由 [id]/page.tsx :id [id]/+page.svelte
布局 layout.tsx - +layout.svelte
守卫 middleware.ts beforeEach +page.server.ts

状态管理

框架 推荐方案 持久化
React Zustand zustand/middleware
Vue Pinia pinia-plugin-persistedstate
Svelte Svelte Stores -
Angular Signals localStorage

API 模块模板

ts
// services/users.ts
import { api } from './api'

export const userService = {
  getList: (params) => api.get('/users', { params }),
  getById: (id) => api.get(`/users/${id}`),
  create: (data) => api.post('/users', data),
  update: (id, data) => api.put(`/users/${id}`, data),
  delete: (id) => api.delete(`/users/${id}`),
}

Query Hook 模板

ts
// hooks/useUsers.ts
export function useUsers(params) {
  return useQuery({
    queryKey: ['users', params],
    queryFn: () => userService.getList(params),
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: userService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}

Store 模板

ts
// stores/auth.ts
export const useAuthStore = create(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: async (credentials) => {
        const response = await authService.login(credentials)
        set({
          user: response.user,
          token: response.token,
          isAuthenticated: true,
        })
      },

      logout: () => {
        set({ user: null, token: null, isAuthenticated: false })
      },

      hasPermission: (permission) => {
        const { permissions } = get().user || {}
        return permissions?.includes(permission) || permissions?.includes('*')
      },
    }),
    { name: 'auth' }
  )
)

测试清单

功能测试

  • [ ] 登录/登出流程
  • [ ] 权限控制生效
  • [ ] 仪表盘拖拽保存
  • [ ] 主题切换正常
  • [ ] 皮肤切换正常
  • [ ] 响应式布局
  • [ ] 数据表格操作
  • [ ] 表单提交验证

兼容性测试

  • [ ] Chrome 最新版
  • [ ] Firefox 最新版
  • [ ] Safari 最新版
  • [ ] Edge 最新版
  • [ ] 移动端浏览器

代码规范

命名约定

类型 规范 示例
组件 PascalCase UserCard.tsx
Hook camelCase + use useUsers.ts
Store camelCase + use useAuthStore.ts
服务 camelCase + Service userService.ts
类型 PascalCase User, UserQueryParams
常量 UPPER_SNAKE API_BASE_URL

文件组织

# 组件目录
components/
└── UserCard/
    ├── index.tsx          # 主组件
    ├── UserCard.types.ts  # 类型定义
    └── useUserCard.ts     # 组件逻辑

# 页面目录
app/users/
├── page.tsx               # 列表页
├── [id]/
│   └── page.tsx          # 详情页
└── create/
    └── page.tsx          # 创建页

参考资源

]]>
<![CDATA[HaloLight 开发文档]]> https://halolight.docs.h7ml.cn/development/ https://halolight.docs.h7ml.cn/development/ Fri, 19 Dec 2025 04:56:41 GMT HaloLight 开发文档

本文档集合了 HaloLight 多框架管理后台项目的共有模式和实现规范,用于指导各框架版本的开发。

文档目录

架构设计

功能模块

开发指南

框架状态

全部框架版本均已实现并部署 (预览地址见各仓库 README)。参考实现 (用于规范校验):

其他框架:Angular · Nuxt · SvelteKit · Astro · Solid.js · Qwik · Remix · Preact · Lit · Fresh (Deno)。

框架 状态 预览 仓库
Next.js 14 ✅ 已部署 预览 GitHub
Vue 3.5 ✅ 已部署 预览 GitHub
Angular 21 ✅ 已部署 预览 GitHub
Nuxt 4 ✅ 已部署 预览 GitHub
SvelteKit 2 ✅ 已部署 预览 GitHub
Astro 5 ✅ 已部署 预览 GitHub
Solid.js ✅ 已部署 预览 GitHub
Qwik ✅ 已部署 预览 GitHub
Remix ✅ 已部署 预览 GitHub
Preact ✅ 已部署 预览 GitHub
Lit ✅ 已部署 预览 GitHub
Fresh (Deno) ✅ 已部署 预览 GitHub

技术栈概览

通用技术栈

  • TypeScript - 类型安全
  • Tailwind CSS - 样式系统
  • shadcn/ui - UI 组件库 (各框架对应版本)
  • Mock.js - 开发环境数据模拟
  • ECharts - 图表可视化

框架特定依赖

功能 React/Next.js Vue 3 Angular Svelte
状态管理 Zustand Pinia Signals/RxJS Svelte Stores
数据获取 TanStack Query TanStack Query RxJS TanStack Query
路由 Next.js App Router Vue Router Angular Router SvelteKit
表单 React Hook Form VeeValidate Reactive Forms Superforms
拖拽布局 react-grid-layout grid-layout-plus angular-gridster2 svelte-grid
]]>
<![CDATA[状态管理]]> https://halolight.docs.h7ml.cn/development/state-management https://halolight.docs.h7ml.cn/development/state-management Fri, 19 Dec 2025 04:56:41 GMT 状态管理

本文档描述 HaloLight 项目的状态管理模式,涵盖不同框架的实现方案。

状态管理方案对照

框架 状态库 特点
React/Next.js Zustand 简洁、无样板代码
Vue 3 Pinia 官方推荐、类型安全
Svelte Svelte Stores 原生响应式
Angular Signals + RxJS 细粒度响应式
Solid.js createStore 细粒度响应式

Store 模块划分

stores/
├── auth.ts           # 认证状态
├── ui-settings.ts    # UI 设置
├── dashboard.ts      # 仪表盘布局
├── navigation.ts     # 导航菜单
├── tabs.ts           # 多标签页
└── error.ts          # 错误状态

Auth Store

状态定义

ts
interface AuthState {
  user: User | null
  token: string | null
  refreshToken: string | null
  permissions: string[]
  roles: string[]
  isAuthenticated: boolean
  isLoading: boolean
}

Zustand 实现 (React)

ts
export const useAuthStore = create<AuthStore>()(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      permissions: [],
      isAuthenticated: false,

      login: async (credentials) => {
        const response = await authService.login(credentials)
        set({
          user: response.user,
          token: response.token,
          permissions: response.permissions,
          isAuthenticated: true,
        })
      },

      hasPermission: (permission) => {
        const { permissions } = get()
        return permissions.some((p) =>
          p === '*' || p === permission ||
          (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
        )
      },
    }),
    { name: 'auth-storage' }
  )
)

Pinia 实现 (Vue)

ts
export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)
  const permissions = ref<string[]>([])

  const isAuthenticated = computed(() => !!token.value)

  function hasPermission(permission: string): boolean {
    return permissions.value.some((p) =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  }

  return { user, token, permissions, isAuthenticated, hasPermission }
}, { persist: { paths: ['token', 'user'] } })

Dashboard Store

布局状态

ts
interface DashboardState {
  layouts: { lg: GridLayout[]; md: GridLayout[]; sm: GridLayout[] }
  widgets: WidgetConfig[]
  isEditing: boolean
}

interface GridLayout {
  i: string; x: number; y: number; w: number; h: number
}

持久化策略

数据类型 存储位置 示例
用户偏好 localStorage 主题、语言
UI 状态 localStorage 侧边栏、布局
临时数据 sessionStorage 表单草稿
服务端数据 TanStack Query API 响应
]]>
<![CDATA[主题系统]]> https://halolight.docs.h7ml.cn/development/theming https://halolight.docs.h7ml.cn/development/theming Fri, 19 Dec 2025 04:56:41 GMT 主题系统

本文档描述 HaloLight 的主题切换和皮肤预设系统。

主题模式

模式 描述
light 浅色主题
dark 深色主题
system 跟随系统

皮肤预设

共 11 种颜色皮肤:

皮肤 Primary 适用场景
default 蓝色 通用
zinc 灰色 简约
slate 蓝灰 专业
stone 棕灰 温暖
gray 中性灰 通用
neutral 黑白 极简
red 红色 警示
rose 玫红 时尚
orange 橙色 活力
green 绿色 自然
blue 蓝色 科技
yellow 黄色 明亮
violet 紫色 优雅

CSS 变量

颜色变量定义

css
:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --card: 0 0% 100%;
  --card-foreground: 222.2 84% 4.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --secondary-foreground: 222.2 47.4% 11.2%;
  --muted: 210 40% 96.1%;
  --muted-foreground: 215.4 16.3% 46.9%;
  --accent: 210 40% 96.1%;
  --accent-foreground: 222.2 47.4% 11.2%;
  --destructive: 0 84.2% 60.2%;
  --destructive-foreground: 210 40% 98%;
  --border: 214.3 31.8% 91.4%;
  --input: 214.3 31.8% 91.4%;
  --ring: 222.2 84% 4.9%;
  --radius: 0.5rem;
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  /* ... */
}

皮肤变量

css
[data-skin="rose"] {
  --primary: 346.8 77.2% 49.8%;
  --primary-foreground: 355.7 100% 97.3%;
}

[data-skin="green"] {
  --primary: 142.1 76.2% 36.3%;
  --primary-foreground: 355.7 100% 97.3%;
}

主题切换实现

React Context

tsx
interface ThemeContextValue {
  theme: 'light' | 'dark' | 'system'
  skin: SkinPreset
  setTheme: (theme: string) => void
  setSkin: (skin: SkinPreset) => void
}

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState('system')
  const [skin, setSkin] = useState<SkinPreset>('default')

  useEffect(() => {
    const root = document.documentElement
    root.classList.remove('light', 'dark')

    if (theme === 'system') {
      const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light'
      root.classList.add(systemTheme)
    } else {
      root.classList.add(theme)
    }

    root.setAttribute('data-skin', skin)
  }, [theme, skin])

  return (
    <ThemeContext.Provider value={{ theme, skin, setTheme, setSkin }}>
      {children}
    </ThemeContext.Provider>
  )
}

Vue Composable

ts
export function useTheme() {
  const theme = ref<'light' | 'dark' | 'system'>('system')
  const skin = ref<SkinPreset>('default')

  const actualTheme = computed(() => {
    if (theme.value === 'system') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light'
    }
    return theme.value
  })

  watch([theme, skin], () => {
    document.documentElement.classList.remove('light', 'dark')
    document.documentElement.classList.add(actualTheme.value)
    document.documentElement.setAttribute('data-skin', skin.value)
  }, { immediate: true })

  return { theme, skin, actualTheme }
}

View Transitions API

主题切换动画 (Vue)

ts
async function toggleTheme() {
  if (!document.startViewTransition) {
    theme.value = theme.value === 'dark' ? 'light' : 'dark'
    return
  }

  await document.startViewTransition(() => {
    theme.value = theme.value === 'dark' ? 'light' : 'dark'
  }).ready

  // 圆形展开动画
  const { clientX, clientY } = event
  const radius = Math.hypot(
    Math.max(clientX, window.innerWidth - clientX),
    Math.max(clientY, window.innerHeight - clientY)
  )

  document.documentElement.animate(
    {
      clipPath: [
        `circle(0px at ${clientX}px ${clientY}px)`,
        `circle(${radius}px at ${clientX}px ${clientY}px)`,
      ],
    },
    {
      duration: 500,
      easing: 'ease-in-out',
      pseudoElement: '::view-transition-new(root)',
    }
  )
}

CSS 配置

css
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

::view-transition-old(root) {
  z-index: 1;
}

::view-transition-new(root) {
  z-index: 9999;
}

主题选择器组件

tsx
function ThemeSelector() {
  const { theme, setTheme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon">
          <Sun className="h-4 w-4 rotate-0 scale-100 dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-4 w-4 rotate-90 scale-0 dark:rotate-0 dark:scale-100" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem onClick={() => setTheme('light')}>
          <Sun className="mr-2 h-4 w-4" /> 浅色
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('dark')}>
          <Moon className="mr-2 h-4 w-4" /> 深色
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('system')}>
          <Monitor className="mr-2 h-4 w-4" /> 系统
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

皮肤选择器

tsx
function SkinSelector() {
  const { skin, setSkin } = useTheme()

  const skins: SkinPreset[] = [
    'default', 'zinc', 'slate', 'stone', 'gray',
    'neutral', 'red', 'rose', 'orange', 'green',
    'blue', 'yellow', 'violet'
  ]

  return (
    <div className="grid grid-cols-5 gap-2">
      {skins.map((s) => (
        <button
          key={s}
          onClick={() => setSkin(s)}
          className={cn(
            'h-8 w-8 rounded-full border-2',
            skin === s ? 'border-primary' : 'border-transparent'
          )}
          style={{ backgroundColor: `hsl(var(--skin-${s}))` }}
        />
      ))}
    </div>
  )
}

ECharts 主题适配

ts
const echartTheme = computed(() => ({
  backgroundColor: 'transparent',
  textStyle: {
    color: actualTheme.value === 'dark' ? '#e5e5e5' : '#333',
  },
  title: {
    textStyle: {
      color: actualTheme.value === 'dark' ? '#fff' : '#333',
    },
  },
  legend: {
    textStyle: {
      color: actualTheme.value === 'dark' ? '#e5e5e5' : '#333',
    },
  },
  xAxis: {
    axisLine: { lineStyle: { color: actualTheme.value === 'dark' ? '#444' : '#ccc' } },
    splitLine: { lineStyle: { color: actualTheme.value === 'dark' ? '#333' : '#eee' } },
  },
  yAxis: {
    axisLine: { lineStyle: { color: actualTheme.value === 'dark' ? '#444' : '#ccc' } },
    splitLine: { lineStyle: { color: actualTheme.value === 'dark' ? '#333' : '#eee' } },
  },
}))
]]>
<![CDATA[API Layer Design]]> https://halolight.docs.h7ml.cn/en/development/api-patterns https://halolight.docs.h7ml.cn/en/development/api-patterns Fri, 19 Dec 2025 04:56:41 GMT API Layer Design

This document describes the API service layer architecture and data fetching strategies for the HaloLight project.

Technology Stack

Framework HTTP Client Cache Layer
React/Next.js Axios TanStack Query
Vue 3 Axios TanStack Query
Svelte Fetch TanStack Query
Angular HttpClient RxJS

Service Layer Structure

services/
├── api.ts              # Axios instance configuration
├── auth.ts             # Authentication service
├── users.ts            # User service
├── roles.ts            # Role service
├── permissions.ts      # Permission service
├── dashboard.ts        # Dashboard service
├── settings.ts         # Settings service
└── index.ts            # Unified export

Axios Instance Configuration

ts
import axios from 'axios'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 30000,
  headers: { 'Content-Type': 'application/json' },
})

// Request interceptor
api.interceptors.request.use((config) => {
  const token = useAuthStore.getState().token
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// Response interceptor
api.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    if (error.response?.status === 401) {
      useAuthStore.getState().logout()
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

Service Definition Specification

ts
// services/users.ts
export const userService = {
  getList: (params: UserQueryParams) =>
    api.get<PaginatedResponse<User>>('/users', { params }),

  getById: (id: string) =>
    api.get<User>(`/users/${id}`),

  create: (data: CreateUserDto) =>
    api.post<User>('/users', data),

  update: (id: string, data: UpdateUserDto) =>
    api.put<User>(`/users/${id}`, data),

  delete: (id: string) =>
    api.delete(`/users/${id}`),
}

TanStack Query Hooks

Query Hook Pattern

ts
// hooks/useUsers.ts
export function useUsers(params: UserQueryParams) {
  return useQuery({
    queryKey: ['users', params],
    queryFn: () => userService.getList(params),
    staleTime: 5 * 60 * 1000, // 5 minutes
  })
}

export function useUser(id: string) {
  return useQuery({
    queryKey: ['users', id],
    queryFn: () => userService.getById(id),
    enabled: !!id,
  })
}

Mutation Hook Pattern

ts
export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: userService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
      toast.success('User created successfully')
    },
  })
}

Mock Data System

Mock.js Configuration

ts
// mocks/index.ts
import Mock from 'mockjs'
import './modules/auth'
import './modules/users'
import './modules/dashboard'

Mock.setup({ timeout: '200-600' })

Mock Module Example

ts
// mocks/modules/users.ts
Mock.mock(/\/api\/users(\?.*)?$/, 'get', (options) => {
  const params = parseQuery(options.url)
  return {
    code: 200,
    data: {
      list: Mock.mock({
        [`list|${params.pageSize || 10}`]: [{
          'id': '@guid',
          'name': '@name',
          'email': '@email',
          'status|1': ['active', 'inactive'],
          'createdAt': '@datetime',
        }]
      }).list,
      total: 100,
    }
  }
})

Error Handling

ts
interface ApiError {
  code: string
  message: string
  details?: Record<string, string[]>
}

// Unified error handling
function handleApiError(error: ApiError) {
  switch (error.code) {
    case 'VALIDATION_ERROR':
      // Form validation error
      break
    case 'UNAUTHORIZED':
      // Unauthorized
      break
    case 'FORBIDDEN':
      // Forbidden
      break
    default:
      toast.error(error.message)
  }
}

Pagination Specification

ts
interface PaginatedResponse<T> {
  list: T[]
  total: number
  page: number
  pageSize: number
  totalPages: number
}

interface PaginationParams {
  page?: number
  pageSize?: number
  sortBy?: string
  sortOrder?: 'asc' | 'desc'
}
]]>
<![CDATA[Overall Architecture]]> https://halolight.docs.h7ml.cn/en/development/architecture https://halolight.docs.h7ml.cn/en/development/architecture Fri, 19 Dec 2025 04:56:41 GMT Overall Architecture

This document describes the overall architecture design of the HaloLight project, including directory structure, layered design, and core patterns.

Directory Structure Specification

Standard Directory Layout

src/
├── app/                    # Page routes (Next.js) or views/ (Vue)
│   ├── (admin)/           # Admin route group
│   │   ├── dashboard/     # Dashboard
│   │   ├── users/         # User management
│   │   ├── roles/         # Role management
│   │   ├── permissions/   # Permission management
│   │   ├── settings/      # System settings
│   │   └── profile/       # User profile
│   └── (auth)/            # Auth route group
│       ├── login/         # Login
│       ├── register/      # Register
│       ├── forgot-password/
│       └── reset-password/
├── components/            # Reusable components
│   ├── ui/               # Basic UI components (shadcn/ui)
│   ├── layout/           # Layout components
│   ├── dashboard/        # Dashboard components
│   ├── charts/           # Chart components
│   └── shared/           # Shared business components
├── hooks/ (composables/)  # Reusable logic
├── stores/               # State management
├── services/             # API service layer
├── lib/                  # Utility library
├── types/                # TypeScript type definitions
├── styles/               # Global styles
└── mocks/                # Mock data

Layered Architecture

Four-Layer Architecture Design

┌─────────────────────────────────────────────────┐
│                   View Layer                     │
│        Pages / Views / Routes                    │
├─────────────────────────────────────────────────┤
│                Component Layer                   │
│     UI Components / Layout / Dashboard          │
├─────────────────────────────────────────────────┤
│                  State Layer                     │
│     Stores / Composables / Hooks                │
├─────────────────────────────────────────────────┤
│                 Service Layer                    │
│     API Services / Data Fetching / Cache        │
└─────────────────────────────────────────────────┘

Layer Responsibilities

Layer Responsibility Framework Implementation
View Layer Page routing, layout assembly Next.js Pages / Vue Views
Component Layer UI rendering, user interaction React/Vue/Svelte Components
State Layer Application state, business logic Zustand / Pinia / Stores
Service Layer API calls, data caching TanStack Query / Axios

Provider Hierarchy

React/Next.js Provider Chain

tsx
<ThemeProvider>
  <MockProvider>
    <QueryClientProvider>
      <AuthProvider>
        <PermissionProvider>
          <WebSocketProvider>
            <ErrorProvider>
              <ToastProvider>
                {children}
              </ToastProvider>
            </ErrorProvider>
          </WebSocketProvider>
        </PermissionProvider>
      </AuthProvider>
    </QueryClientProvider>
  </MockProvider>
</ThemeProvider>

Vue Plugin Registration Order

ts
app.use(pinia)
app.use(router)
app.use(i18n)
app.use(mockPlugin)
app.use(queryPlugin)
app.use(permissionPlugin)

Layout System

AdminLayout Structure

┌──────────────────────────────────────────────────────┐
│                     Header                            │
│  [Logo] [Breadcrumb]           [Search] [User] [Settings]
├────────────┬─────────────────────────────────────────┤
│            │                                          │
│  Sidebar   │              Content                     │
│            │                                          │
│  - Menu    │   ┌──────────────────────────────────┐  │
│  - Nav     │   │        Page Content               │  │
│            │   │                                   │  │
│            │   └──────────────────────────────────┘  │
│            │                                          │
├────────────┴─────────────────────────────────────────┤
│                     Footer                            │
└──────────────────────────────────────────────────────┘

AuthLayout Structure

┌──────────────────────────────────────────────────────┐
│                                                       │
│                    ┌────────────┐                    │
│                    │            │                    │
│                    │  Auth Form │                    │
│                    │            │                    │
│                    └────────────┘                    │
│                                                       │
└──────────────────────────────────────────────────────┘

Route Configuration Specification

Route Meta Information

ts
interface RouteMeta {
  title: string           // Page title
  icon?: string          // Menu icon
  permission?: string    // Required permission
  hidden?: boolean       // Hidden in menu
  keepAlive?: boolean    // Cache component
  breadcrumb?: boolean   // Show breadcrumb
}

Standard Route Table

Path Page Permission
/dashboard Dashboard dashboard:view
/users User list users:list
/users/create Create user users:create
/users/:id User detail users:view
/users/:id/edit Edit user users:update
/roles Role list roles:list
/permissions Permission management permissions:list
/settings System settings settings:view
/profile User profile - (authenticated)

Environment Variable Specification

Variable Naming Convention

Prefix Framework Description
NEXT_PUBLIC_ Next.js Client-side visible
VITE_ Vue/Vite Client-side visible
PUBLIC_ SvelteKit Client-side visible
No prefix All Server-side only

Required Environment Variables

bash
# API Configuration
*_API_BASE_URL=http://localhost:3000/api
*_API_TIMEOUT=30000

# Authentication Configuration
*_AUTH_SECRET=your-secret-key
*_TOKEN_EXPIRES=7d

# Mock Toggle
*_ENABLE_MOCK=true

# Feature Toggles
*_ENABLE_WEBSOCKET=true
*_ENABLE_ANALYTICS=false

Code Organization Principles

1. Feature First

Organize code by feature modules, not by file types:

# Recommended ✅
features/
├── users/
│   ├── components/
│   ├── hooks/
│   ├── services/
│   └── types/

# Avoid ❌
components/
├── UserList.tsx
├── UserForm.tsx
hooks/
├── useUsers.ts
services/
├── userService.ts

2. Proximity Principle

Place component-specific styles, types, and utilities in the component directory:

components/
└── UserCard/
    ├── index.tsx
    ├── UserCard.module.css
    ├── UserCard.types.ts
    └── useUserCard.ts

3. Shared Extraction

Extract code used in multiple places to shared locations:

# Used by 2+ components → Extract to components/shared/
# Used in 3+ places → Extract to lib/ or utils/

4. Specification First

When adding or modifying features, first clarify interfaces, constraints, and directory structure in halolight/docs, then sync to halolight and halolight-vue to avoid implementation divergence.

]]>
<![CDATA[Authentication System]]> https://halolight.docs.h7ml.cn/en/development/authentication https://halolight.docs.h7ml.cn/en/development/authentication Fri, 19 Dec 2025 04:56:41 GMT Authentication System

This document describes the user authentication and permission control implementation for the HaloLight project.

Authentication Flow

┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
│  Login  │ ──► │ Verify  │ ──► │  Get    │ ──► │  Store  │
│  Form   │     │Credentials│     │ Token   │     │  State  │
└─────────┘     └─────────┘     └─────────┘     └─────────┘

Authentication Pages

Page Path Function
Login /login Username/password login
Register /register New user registration
Forgot Password /forgot-password Send reset email
Reset Password /reset-password Set new password

Token Management

Dual Token Mechanism

ts
interface TokenPair {
  accessToken: string   // Short-lived (15 minutes)
  refreshToken: string  // Long-lived (7 days)
}

Token Refresh

ts
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401 && !error.config._retry) {
      error.config._retry = true
      try {
        const { refreshToken } = useAuthStore.getState()
        const newTokens = await authService.refresh(refreshToken)
        useAuthStore.getState().setTokens(newTokens)
        error.config.headers.Authorization = `Bearer ${newTokens.accessToken}`
        return api(error.config)
      } catch {
        useAuthStore.getState().logout()
      }
    }
    return Promise.reject(error)
  }
)

Permission System

Permission Format

ts
// Format: resource:action
const permissions = [
  'users:list',      // View user list
  'users:create',    // Create user
  'users:update',    // Update user
  'users:delete',    // Delete user
  'users:*',         // All user permissions
  '*',               // Super admin
]

Permission Check

ts
function hasPermission(userPerms: string[], required: string): boolean {
  return userPerms.some((p) =>
    p === '*' ||
    p === required ||
    (p.endsWith(':*') && required.startsWith(p.slice(0, -1)))
  )
}

Permission Component

tsx
// React
function PermissionGuard({ permission, children, fallback }) {
  const hasPermission = useAuthStore((s) => s.hasPermission)
  return hasPermission(permission) ? children : fallback
}

// Usage
<PermissionGuard permission="users:delete" fallback={null}>
  <DeleteButton />
</PermissionGuard>
vue
<!-- Vue -->
<template>
  <slot v-if="hasPermission(permission)" />
  <slot v-else name="fallback" />
</template>

Permission Directive (Vue)

ts
// v-permission directive
app.directive('permission', {
  mounted(el, binding) {
    const authStore = useAuthStore()
    if (!authStore.hasPermission(binding.value)) {
      el.parentNode?.removeChild(el)
    }
  },
})

// Usage
<button v-permission="'users:delete'">Delete</button>

Route Guards

Next.js Middleware

ts
// middleware.ts
export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')
  const isAuthPage = request.nextUrl.pathname.startsWith('/login')

  if (!token && !isAuthPage) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  if (token && isAuthPage) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }
}

Vue Router Guard

ts
router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()

  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next({ name: 'login', query: { redirect: to.fullPath } })
    return
  }

  if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) {
    next({ name: '403' })
    return
  }

  next()
})

Standard Permission List

ts
const PERMISSIONS = {
  // Dashboard
  'dashboard:view': 'View dashboard',
  'dashboard:edit': 'Edit dashboard',

  // User Management
  'users:list': 'View user list',
  'users:view': 'View user details',
  'users:create': 'Create user',
  'users:update': 'Update user',
  'users:delete': 'Delete user',

  // Role Management
  'roles:list': 'View role list',
  'roles:create': 'Create role',
  'roles:update': 'Update role',
  'roles:delete': 'Delete role',

  // Permission Management
  'permissions:list': 'View permission list',
  'permissions:assign': 'Assign permissions',

  // System Settings
  'settings:view': 'View settings',
  'settings:update': 'Update settings',
}

Role Presets

ts
const ROLES = {
  admin: {
    name: 'Administrator',
    permissions: ['*'],
  },
  manager: {
    name: 'Manager',
    permissions: ['dashboard:*', 'users:list', 'users:view'],
  },
  user: {
    name: 'User',
    permissions: ['dashboard:view'],
  },
}
]]>
<![CDATA[Component Specification]]> https://halolight.docs.h7ml.cn/en/development/components https://halolight.docs.h7ml.cn/en/development/components Fri, 19 Dec 2025 04:56:41 GMT Component Specification

This document defines the UI component library specification for the HaloLight project, based on the shadcn/ui design system.

Component Library Overview

Based on shadcn/ui

All framework versions use the corresponding shadcn/ui implementation:

Framework Component Library Repository
React/Next.js shadcn/ui shadcn/ui
Vue 3 shadcn-vue shadcn-vue
Svelte shadcn-svelte shadcn-svelte
Angular spartan/ui spartan
Solid.js solid-ui solid-ui

Required Component Checklist

Basic UI Components (30+)

ui/
├── accordion.tsx        # Accordion
├── alert-dialog.tsx     # Alert dialog
├── alert.tsx           # Alert
├── avatar.tsx          # Avatar
├── badge.tsx           # Badge
├── breadcrumb.tsx      # Breadcrumb
├── button.tsx          # Button
├── calendar.tsx        # Calendar
├── card.tsx            # Card
├── checkbox.tsx        # Checkbox
├── collapsible.tsx     # Collapsible
├── command.tsx         # Command palette
├── data-table.tsx      # Data table
├── date-picker.tsx     # Date picker
├── dialog.tsx          # Dialog
├── dropdown-menu.tsx   # Dropdown menu
├── form.tsx            # Form
├── input.tsx           # Input
├── label.tsx           # Label
├── pagination.tsx      # Pagination
├── popover.tsx         # Popover
├── progress.tsx        # Progress
├── radio-group.tsx     # Radio group
├── scroll-area.tsx     # Scroll area
├── select.tsx          # Select
├── separator.tsx       # Separator
├── sheet.tsx           # Sheet
├── skeleton.tsx        # Skeleton
├── slider.tsx          # Slider
├── switch.tsx          # Switch
├── table.tsx           # Table
├── tabs.tsx            # Tabs
├── textarea.tsx        # Textarea
├── toast.tsx           # Toast
├── tooltip.tsx         # Tooltip
└── sonner.tsx          # Toast notification

Layout Components

layout/
├── AdminLayout.tsx      # Admin main layout
├── AuthLayout.tsx       # Auth page layout
├── Sidebar.tsx          # Sidebar
├── Header.tsx           # Header
├── Footer.tsx           # Footer
├── Breadcrumb.tsx       # Breadcrumb navigation
├── TabsNav.tsx          # Tabs navigation
└── PageContainer.tsx    # Page container

Dashboard Components

dashboard/
├── DashboardGrid.tsx    # Draggable grid container
├── WidgetWrapper.tsx    # Widget wrapper
├── StatsWidget.tsx      # Stats card
├── ChartWidget.tsx      # Chart widget
├── TableWidget.tsx      # Table widget
├── CalendarWidget.tsx   # Calendar widget
├── TasksWidget.tsx      # Task list
└── QuickActionsWidget.tsx # Quick actions

Chart Components

charts/
├── LineChart.tsx        # Line chart
├── BarChart.tsx         # Bar chart
├── PieChart.tsx         # Pie chart
├── AreaChart.tsx        # Area chart
├── RadarChart.tsx       # Radar chart
└── GaugeChart.tsx       # Gauge

Component Design Specification

1. Props Interface Design

tsx
// Basic Props structure
interface ComponentProps {
  // Required props first
  children: React.ReactNode

  // Optional props in alphabetical order
  className?: string
  disabled?: boolean
  loading?: boolean
  size?: 'sm' | 'md' | 'lg'
  variant?: 'default' | 'outline' | 'ghost'

  // Event handlers
  onChange?: (value: T) => void
  onClick?: () => void
}

2. Style Variants

Use cva (class-variance-authority) to define variants:

tsx
import { cva, type VariantProps } from 'class-variance-authority'

const buttonVariants = cva(
  // Base styles
  'inline-flex items-center justify-center rounded-md font-medium transition-colors',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        outline: 'border border-input bg-background hover:bg-accent',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
      },
      size: {
        sm: 'h-8 px-3 text-xs',
        md: 'h-9 px-4 text-sm',
        lg: 'h-10 px-6 text-base',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  loading?: boolean
}

3. Accessibility (a11y)

All components must support:

tsx
// ARIA attributes
<button
  role="button"
  aria-label={ariaLabel}
  aria-disabled={disabled}
  aria-busy={loading}
>

// Keyboard navigation
const handleKeyDown = (e: KeyboardEvent) => {
  if (e.key === 'Enter' || e.key === ' ') {
    onClick?.()
  }
  if (e.key === 'Escape') {
    onClose?.()
  }
}

// Focus management
const focusRef = useRef<HTMLElement>(null)
useEffect(() => {
  if (open) {
    focusRef.current?.focus()
  }
}, [open])

4. Responsive Design

tsx
// Tailwind breakpoints
const breakpoints = {
  sm: '640px',   // Mobile landscape
  md: '768px',   // Tablet
  lg: '1024px',  // Small desktop
  xl: '1280px',  // Desktop
  '2xl': '1536px' // Large screen
}

// Responsive class example
<div className="
  grid
  grid-cols-1
  sm:grid-cols-2
  md:grid-cols-3
  lg:grid-cols-4
  gap-4
">

Layout Component Specification

AdminLayout

tsx
interface AdminLayoutProps {
  children: React.ReactNode
}

// Layout structure
<div className="min-h-screen bg-background">
  <Sidebar />
  <div className="flex flex-col lg:ml-64">
    <Header />
    <main className="flex-1 p-6">
      <PageContainer>
        {children}
      </PageContainer>
    </main>
    <Footer />
  </div>
</div>
tsx
interface SidebarState {
  collapsed: boolean      // Is collapsed
  mobileOpen: boolean    // Mobile open state
  activeMenu: string     // Current active menu
  openMenus: string[]    // Expanded submenus
}

// Collapse widths
const SIDEBAR_WIDTH = 256        // Expanded 16rem
const SIDEBAR_COLLAPSED = 64     // Collapsed 4rem

Header Component

tsx
interface HeaderProps {
  showBreadcrumb?: boolean
  showSearch?: boolean
  showNotifications?: boolean
  showUserMenu?: boolean
}

// Component parts
<header className="h-16 border-b bg-background/95 backdrop-blur">
  <div className="flex items-center justify-between px-4 h-full">
    {/* Left side */}
    <div className="flex items-center gap-4">
      <SidebarTrigger />
      <Breadcrumb />
    </div>

    {/* Right side */}
    <div className="flex items-center gap-2">
      <GlobalSearch />
      <ThemeToggle />
      <NotificationDropdown />
      <UserDropdown />
    </div>
  </div>
</header>

Form Component Specification

Form Field Structure

tsx
<FormField
  control={form.control}
  name="email"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Email</FormLabel>
      <FormControl>
        <Input placeholder="Enter email" {...field} />
      </FormControl>
      <FormDescription>
        We won't share your email
      </FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

Form Validation

tsx
// Zod Schema
const formSchema = z.object({
  username: z.string().min(2, 'Username must be at least 2 characters').max(50),
  email: z.string().email('Please enter a valid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword'],
})

Data Table Specification

DataTable Features

tsx
interface DataTableProps<T> {
  columns: ColumnDef<T>[]
  data: T[]

  // Pagination
  pagination?: boolean
  pageSize?: number

  // Sorting
  sorting?: boolean
  defaultSort?: { id: string; desc: boolean }

  // Filtering
  filtering?: boolean
  globalFilter?: boolean

  // Selection
  selection?: boolean
  onSelectionChange?: (rows: T[]) => void

  // Actions
  actions?: (row: T) => React.ReactNode
}

Column Definition Example

tsx
const columns: ColumnDef<User>[] = [
  {
    id: 'select',
    header: ({ table }) => (
      <Checkbox
        checked={table.getIsAllPageRowsSelected()}
        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
      />
    ),
  },
  {
    accessorKey: 'name',
    header: 'Name',
    cell: ({ row }) => <span className="font-medium">{row.getValue('name')}</span>,
  },
  {
    accessorKey: 'email',
    header: ({ column }) => (
      <Button variant="ghost" onClick={() => column.toggleSorting()}>
        Email
        <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => (
      <Badge variant={row.getValue('status') === 'active' ? 'default' : 'secondary'}>
        {row.getValue('status')}
      </Badge>
    ),
    filterFn: (row, id, value) => value.includes(row.getValue(id)),
  },
  {
    id: 'actions',
    cell: ({ row }) => <RowActions row={row.original} />,
  },
]
]]>
<![CDATA[Dashboard]]> https://halolight.docs.h7ml.cn/en/development/dashboard https://halolight.docs.h7ml.cn/en/development/dashboard Fri, 19 Dec 2025 04:56:41 GMT Dashboard

This document describes the implementation specification for the HaloLight draggable dashboard.

Technology Stack

Framework Drag Library
React/Next.js react-grid-layout
Vue 3 grid-layout-plus
Svelte svelte-grid
Angular angular-gridster2

Widget Types

ID Type Default Size Description
stats Stats card 3x2 Numeric statistics
chart-line Line chart 6x4 Trend data
chart-bar Bar chart 6x4 Comparison data
chart-pie Pie chart 4x4 Proportion data
recent-users Recent users 4x4 User list
notifications Notifications 4x4 Message list
tasks Tasks 4x4 Todo items
calendar Calendar 4x4 Schedule
quick-actions Quick actions 3x2 Common functions

Layout Configuration

Responsive Breakpoints

ts
const breakpoints = { lg: 1200, md: 996, sm: 768 }
const cols = { lg: 12, md: 8, sm: 4 }

Layout Data Structure

ts
interface GridLayout {
  i: string      // Widget ID
  x: number      // Column position (0-based)
  y: number      // Row position
  w: number      // Width (columns)
  h: number      // Height (rows)
  minW?: number  // Minimum width
  minH?: number  // Minimum height
  static?: boolean // Is static
}

Default Layout

ts
const defaultLayouts = {
  lg: [
    { i: 'stats-1', x: 0, y: 0, w: 3, h: 2 },
    { i: 'stats-2', x: 3, y: 0, w: 3, h: 2 },
    { i: 'stats-3', x: 6, y: 0, w: 3, h: 2 },
    { i: 'stats-4', x: 9, y: 0, w: 3, h: 2 },
    { i: 'chart-line', x: 0, y: 2, w: 8, h: 4 },
    { i: 'chart-pie', x: 8, y: 2, w: 4, h: 4 },
    { i: 'recent-users', x: 0, y: 6, w: 4, h: 4 },
    { i: 'tasks', x: 4, y: 6, w: 4, h: 4 },
    { i: 'notifications', x: 8, y: 6, w: 4, h: 4 },
  ],
}

React Implementation

tsx
import GridLayout, { Responsive, WidthProvider } from 'react-grid-layout'

const ResponsiveGridLayout = WidthProvider(Responsive)

function Dashboard() {
  const { layouts, updateLayout, isEditing } = useDashboardStore()

  return (
    <ResponsiveGridLayout
      layouts={layouts}
      breakpoints={{ lg: 1200, md: 996, sm: 768 }}
      cols={{ lg: 12, md: 8, sm: 4 }}
      rowHeight={80}
      isDraggable={isEditing}
      isResizable={isEditing}
      onLayoutChange={(layout, allLayouts) => {
        updateLayout(allLayouts)
      }}
    >
      {widgets.map((widget) => (
        <div key={widget.id}>
          <WidgetWrapper widget={widget} />
        </div>
      ))}
    </ResponsiveGridLayout>
  )
}

Vue Implementation

vue
<template>
  <GridLayout
    v-model:layout="layout"
    :col-num="12"
    :row-height="80"
    :is-draggable="isEditing"
    :is-resizable="isEditing"
  >
    <GridItem
      v-for="item in layout"
      :key="item.i"
      :x="item.x"
      :y="item.y"
      :w="item.w"
      :h="item.h"
    >
      <WidgetWrapper :widget="getWidget(item.i)" />
    </GridItem>
  </GridLayout>
</template>

<script setup>
import { GridLayout, GridItem } from 'grid-layout-plus'
</script>

Widget Components

WidgetWrapper

tsx
interface WidgetWrapperProps {
  widget: WidgetConfig
  onRemove?: () => void
}

function WidgetWrapper({ widget, onRemove }: WidgetWrapperProps) {
  const { isEditing } = useDashboardStore()

  return (
    <Card className="h-full flex flex-col">
      <CardHeader className="flex-row items-center justify-between py-2">
        <CardTitle className="text-sm">{widget.title}</CardTitle>
        {isEditing && (
          <Button variant="ghost" size="icon" onClick={onRemove}>
            <X className="h-4 w-4" />
          </Button>
        )}
      </CardHeader>
      <CardContent className="flex-1 overflow-hidden">
        <WidgetContent type={widget.type} settings={widget.settings} />
      </CardContent>
    </Card>
  )
}

StatsWidget

tsx
function StatsWidget({ title, value, change, icon: Icon }) {
  return (
    <div className="flex items-center justify-between">
      <div>
        <p className="text-sm text-muted-foreground">{title}</p>
        <p className="text-2xl font-bold">{value}</p>
        <p className={cn('text-xs', change > 0 ? 'text-green-500' : 'text-red-500')}>
          {change > 0 ? '+' : ''}{change}%
        </p>
      </div>
      <Icon className="h-8 w-8 text-muted-foreground" />
    </div>
  )
}

ECharts Integration

Theme Adaptation

ts
const getChartTheme = (isDark: boolean) => ({
  backgroundColor: 'transparent',
  textStyle: { color: isDark ? '#e5e5e5' : '#333' },
  axisLine: { lineStyle: { color: isDark ? '#444' : '#ccc' } },
  splitLine: { lineStyle: { color: isDark ? '#333' : '#eee' } },
})

Responsive Sizing

tsx
function ChartWidget({ option }) {
  const chartRef = useRef<EChartsInstance>()

  useEffect(() => {
    const observer = new ResizeObserver(() => {
      chartRef.current?.resize()
    })
    observer.observe(containerRef.current)
    return () => observer.disconnect()
  }, [])

  return <ReactECharts ref={chartRef} option={option} />
}

Edit Mode

Toolbar

tsx
function DashboardToolbar() {
  const { isEditing, toggleEditing, resetLayout } = useDashboardStore()

  return (
    <div className="flex gap-2">
      <Button variant="outline" onClick={toggleEditing}>
        {isEditing ? <Check /> : <Edit />}
        {isEditing ? 'Done' : 'Edit'}
      </Button>
      {isEditing && (
        <>
          <AddWidgetButton />
          <Button variant="outline" onClick={resetLayout}>
            <RotateCcw /> Reset
          </Button>
        </>
      )}
    </div>
  )
}

Persistence

ts
// Save layout to localStorage
const useDashboardStore = create(
  persist(
    (set) => ({
      layouts: defaultLayouts,
      updateLayout: (layouts) => set({ layouts }),
    }),
    { name: 'dashboard-layout' }
  )
)
]]>
<![CDATA[HaloLight Ecosystem]]> https://halolight.docs.h7ml.cn/en/development/ecosystem https://halolight.docs.h7ml.cn/en/development/ecosystem Fri, 19 Dec 2025 04:56:41 GMT HaloLight Ecosystem

HaloLight is a multi-framework, multi-platform admin dashboard solution. This document lists all projects and their status.

Project Overview

Frontend Framework Implementations

Project Framework Status Description
halolight Next.js 14 + React 18 ✅ Released Reference implementation
halolight-react React + Vite ✅ Released Pure SPA version
halolight-vue Vue 3.5 + Vite ✅ Released Vue reference implementation
halolight-angular Angular 21 ✅ Released Angular implementation
halolight-nuxt Nuxt 3 ✅ Released Vue SSR version
halolight-svelte SvelteKit ✅ Released Svelte implementation
halolight-astro Astro ✅ Released Static-first
halolight-solid SolidJS ✅ Released High-performance reactive
halolight-qwik Qwik ✅ Released Resumable
halolight-remix Remix ✅ Released Full-stack React
halolight-preact Preact ✅ Released Lightweight React
halolight-lit Lit ✅ Released Web Components
halolight-fresh Fresh (Deno) ✅ Released Deno native
halolight-deno Fresh (Deno) ✅ Released Deno implementation

Deployment Platforms

Project Platform Status Features
halolight-cloudflare Cloudflare Pages/Workers ✅ Released Edge runtime, Next.js 15
halolight-vercel Vercel ✅ Released Edge Functions
halolight-netlify Netlify ✅ Released Edge Functions
halolight-aws AWS Amplify ✅ Released Lambda@Edge
halolight-azure Azure Static Web Apps ✅ Released Azure Functions
halolight-fly Fly.io ✅ Released Global deployment
halolight-railway Railway ✅ Released One-click deploy
halolight-docker Docker self-hosted ✅ Released Traefik reverse proxy

Backend APIs

Project Tech Stack Status Features
halolight-api-nestjs NestJS + Prisma + TypeScript ✅ Released Node.js enterprise
halolight-api-node Express + Prisma + TypeScript ✅ Released Node.js reference implementation
halolight-api-go Gin + GORM ✅ Released High performance
halolight-api-python FastAPI + SQLAlchemy + Alembic ✅ Released Python ecosystem
halolight-api-bun Hono + Drizzle ORM ✅ Released Bun runtime
halolight-api-java Spring Boot 3.4 + JPA ✅ Released Enterprise Java
halolight-api-php Laravel + Eloquent ✅ Released PHP ecosystem

Infrastructure

Project Purpose Status Features
halolight-bff tRPC Gateway ✅ Released Type-safe API
halolight-action Next.js Full-stack ✅ Released Server Actions
halolight-ui Stencil Web Components ✅ Released Cross-framework component library

AI & Web3

Project Purpose Status Features
halolight-ai AI Assistant ✅ Released RAG + Action execution
halolight-web3 Web3 Integration ✅ Released EVM + Solana + IPFS

Tech Stack Comparison

Frontend Frameworks

React family:     Next.js → Remix → Preact → React (Vite)
Vue family:       Vue 3.5 → Nuxt 3
Others:           Angular → SvelteKit → SolidJS → Qwik → Lit → Astro → Fresh → Deno

Backend Languages

Node.js:          NestJS (Prisma) → Express (Prisma) → Hono (Drizzle)
Go:               Gin (GORM)
Python:           FastAPI (SQLAlchemy)
Java:             Spring Boot (JPA)
PHP:              Laravel (Eloquent)

Deployment Platforms

Edge runtime:     Cloudflare → Vercel → Netlify
Cloud platforms:  AWS Amplify → Azure SWA → Fly.io → Railway
Self-hosted:      Docker + Traefik

Quick Start

Choose Frontend Framework

bash
# React (Recommended)
git clone https://github.com/halolight/halolight
cd halolight && pnpm install && pnpm dev

# Vue
git clone https://github.com/halolight/halolight-vue
cd halolight-vue && pnpm install && pnpm dev

# Angular
git clone https://github.com/halolight/halolight-angular
cd halolight-angular && pnpm install && pnpm start

Choose Backend API

bash
# Node.js (Recommended)
git clone https://github.com/halolight/halolight-api-node
cd halolight-api-node && pnpm install && pnpm dev

# Go
git clone https://github.com/halolight/halolight-api-go
cd halolight-api-go && go run main.go

# Python
git clone https://github.com/halolight/halolight-api-python
cd halolight-api-python && pip install -r requirements.txt && uvicorn main:app --reload

# Java
git clone https://github.com/halolight/halolight-api-java
cd halolight-api-java && mvn spring-boot:run

Choose Deployment Platform

bash
# Cloudflare (Recommended for edge)
git clone https://github.com/halolight/halolight-cloudflare
cd halolight-cloudflare && pnpm install && pnpm deploy

# Docker (Recommended for self-hosted)
git clone https://github.com/halolight/halolight-docker
cd halolight-docker && docker-compose up -d

Component Library Usage

Install halolight-ui

bash
npm install @halolight/ui

Use in HTML

html
<script type="module" src="https://unpkg.com/@halolight/ui/dist/halolight-ui.esm.js"></script>

<hl-button variant="primary">Click me</hl-button>
<hl-input label="Email" type="email"></hl-input>
<hl-card>
  <h3 slot="header">Card Title</h3>
  <p>Card content</p>
</hl-card>

Use in React

tsx
import { defineCustomElements } from '@halolight/ui/loader';
defineCustomElements();

function App() {
  return <hl-button variant="primary">Click me</hl-button>;
}

Use in Vue

vue
<script setup>
import { defineCustomElements } from '@halolight/ui/loader';
defineCustomElements();
</script>

<template>
  <hl-button variant="primary">Click me</hl-button>
</template>

AI Assistant Integration

Deploy halolight-ai

bash
git clone https://github.com/halolight/halolight-ai
cd halolight-ai
cp .env.example .env
# Configure OPENAI_API_KEY or other LLM keys
docker-compose up -d

API Calls

bash
# Send message
curl -X POST http://localhost:3000/api/ai/chat \
  -H "Content-Type: application/json" \
  -H "X-Tenant-ID: default" \
  -H "X-User-ID: user1" \
  -d '{"message": "Help me analyze today'\''s data"}'

# Execute action
curl -X POST http://localhost:3000/api/ai/actions/execute \
  -H "Content-Type: application/json" \
  -d '{"action": "query_users", "params": {"role": "admin"}}'

Web3 Integration

Install Dependencies

bash
# Core package
npm install @halolight/web3-core

# React components
npm install @halolight/web3-react

# Vue components
npm install @halolight/web3-vue

React Example

tsx
import { Web3Provider, WalletButton, TokenBalance } from '@halolight/web3-react';

function App() {
  return (
    <Web3Provider>
      <WalletButton />
      <TokenBalance />
    </Web3Provider>
  );
}

Contributing

  1. Fork the repository
  2. Create feature branch: git checkout -b feature/xxx
  3. Commit changes: git commit -m 'feat: xxx'
  4. Push branch: git push origin feature/xxx
  5. Submit Pull Request

License

All HaloLight projects are licensed under MIT License.

]]>
<![CDATA[Implementation Guide]]> https://halolight.docs.h7ml.cn/en/development/implementation-guide https://halolight.docs.h7ml.cn/en/development/implementation-guide Fri, 19 Dec 2025 04:56:41 GMT Implementation Guide

This guide helps developers create new framework version implementations for HaloLight.

Shared Constraints

  • Data Mocking: Use Mock.js with fetch interception, aligned with halolight/src/mock behavior (response format, delay, error codes)
  • Authentication Flow: Login/register/forgot password/reset password reference halolight/src/app/(auth) page flows and validation logic
  • Environment Variables: All frameworks reuse the same variable naming (e.g. *_API_URL, *_USE_MOCK, *_DEMO_*, *_BRAND_NAME) for documentation consistency
  • CI/CD & Testing: Use common workflow (lint + type-check + test + build + security), retain unit tests and coverage tasks

Implementation Checklist

Phase 1: Project Initialization

  • [ ] Create project using framework CLI
  • [ ] Configure TypeScript
  • [ ] Install Tailwind CSS
  • [ ] Install corresponding shadcn/ui version
  • [ ] Configure environment variables
  • [ ] Setup path alias (@/)

Phase 2: Basic Architecture

  • [ ] Create directory structure
  • [ ] Implement API service layer
  • [ ] Configure TanStack Query
  • [ ] Setup Mock.js fetch interception (reuse halolight/src/mock data structure)
  • [ ] Implement state management Store

Phase 3: Authentication System

  • [ ] Login/register/forgot password/reset password pages (reference Next.js version interaction)
  • [ ] Auth Store (with Token persistence)
  • [ ] Route guards
  • [ ] Permission checks (page-level and button-level)

Phase 4: Layout Components

  • [ ] AdminLayout
  • [ ] AuthLayout
  • [ ] Sidebar
  • [ ] Header
  • [ ] Footer
  • [ ] Breadcrumb
  • [ ] TabsNav

Phase 5: Core Pages

  • [ ] Dashboard (draggable dashboard)
  • [ ] User Management (CRUD)
  • [ ] Role Management
  • [ ] Permission Management
  • [ ] System Settings
  • [ ] User Profile

Phase 6: Enhanced Features

  • [ ] Theme switching
  • [ ] Skin presets
  • [ ] Internationalization (optional)
  • [ ] Responsive adaptation
  • [ ] Error boundaries
  • [ ] CI/CD (lint, type-check, test, build, security) and coverage reports

Quick Start

1. Create Project

bash
# Next.js
npx create-next-app@latest halolight --typescript --tailwind --app

# Vue
npm create vue@latest halolight-vue

# SvelteKit
npx sv create halolight-svelte

# Angular
ng new halolight-angular --routing --style=scss

2. Install Dependencies

bash
# Common dependencies
npm install axios @tanstack/react-query zustand
npm install -D tailwindcss postcss autoprefixer

# shadcn/ui
npx shadcn@latest init

3. Directory Structure

src/
├── app/              # Pages
├── components/
│   ├── ui/          # shadcn components
│   ├── layout/      # Layout components
│   └── dashboard/   # Dashboard components
├── hooks/           # Custom hooks
├── stores/          # State management
├── services/        # API services
├── lib/             # Utility functions
├── types/           # Type definitions
└── mocks/           # Mock data

Framework Comparison Table

Component Syntax

Concept React Vue Svelte
Component function Component() <script setup> <script>
Props props: Props defineProps<Props>() export let prop
State useState() ref() let state = $state()
Computed useMemo() computed() $derived()
Side Effect useEffect() watch() $effect()
Context useContext() provide/inject setContext()

Routing

Concept Next.js Vue Router SvelteKit
File Routing app/page.tsx - routes/+page.svelte
Dynamic Route [id]/page.tsx :id [id]/+page.svelte
Layout layout.tsx - +layout.svelte
Guard middleware.ts beforeEach +page.server.ts

State Management

Framework Recommended Solution Persistence
React Zustand zustand/middleware
Vue Pinia pinia-plugin-persistedstate
Svelte Svelte Stores -
Angular Signals localStorage

API Module Template

ts
// services/users.ts
import { api } from './api'

export const userService = {
  getList: (params) => api.get('/users', { params }),
  getById: (id) => api.get(`/users/${id}`),
  create: (data) => api.post('/users', data),
  update: (id, data) => api.put(`/users/${id}`, data),
  delete: (id) => api.delete(`/users/${id}`),
}

Query Hook Template

ts
// hooks/useUsers.ts
export function useUsers(params) {
  return useQuery({
    queryKey: ['users', params],
    queryFn: () => userService.getList(params),
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: userService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}

Store Template

ts
// stores/auth.ts
export const useAuthStore = create(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: async (credentials) => {
        const response = await authService.login(credentials)
        set({
          user: response.user,
          token: response.token,
          isAuthenticated: true,
        })
      },

      logout: () => {
        set({ user: null, token: null, isAuthenticated: false })
      },

      hasPermission: (permission) => {
        const { permissions } = get().user || {}
        return permissions?.includes(permission) || permissions?.includes('*')
      },
    }),
    { name: 'auth' }
  )
)

Testing Checklist

Functional Testing

  • [ ] Login/logout flow
  • [ ] Permission control effective
  • [ ] Dashboard drag and save
  • [ ] Theme switching works
  • [ ] Skin switching works
  • [ ] Responsive layout
  • [ ] Data table operations
  • [ ] Form submission validation

Compatibility Testing

  • [ ] Chrome latest
  • [ ] Firefox latest
  • [ ] Safari latest
  • [ ] Edge latest
  • [ ] Mobile browsers

Code Standards

Naming Conventions

Type Standard Example
Component PascalCase UserCard.tsx
Hook camelCase + use useUsers.ts
Store camelCase + use useAuthStore.ts
Service camelCase + Service userService.ts
Type PascalCase User, UserQueryParams
Constant UPPER_SNAKE API_BASE_URL

File Organization

# Component directory
components/
└── UserCard/
    ├── index.tsx          # Main component
    ├── UserCard.types.ts  # Type definitions
    └── useUserCard.ts     # Component logic

# Page directory
app/users/
├── page.tsx               # List page
├── [id]/
│   └── page.tsx          # Detail page
└── create/
    └── page.tsx          # Create page

Reference Resources

]]>
<![CDATA[HaloLight Development Documentation]]> https://halolight.docs.h7ml.cn/en/development/ https://halolight.docs.h7ml.cn/en/development/ Fri, 19 Dec 2025 04:56:41 GMT HaloLight Development Documentation

This documentation collection covers the shared patterns and implementation specifications for the HaloLight multi-framework admin dashboard project, guiding the development of each framework version.

Table of Contents

Architecture Design

  • Architecture - Overall project architecture and directory structure specifications
  • Components - UI component library design specifications
  • State Management - State management patterns and best practices

Feature Modules

  • API Patterns - API service layer architecture and data fetching strategies
  • Authentication - User authentication and permission control implementation
  • Dashboard - Draggable dashboard implementation specifications
  • Theming - Theme switching and skin preset system

Development Guide

Framework Status

All framework versions have been implemented and deployed (preview links available in respective repository READMEs). Reference implementations (for specification validation):

Other frameworks:Angular · Nuxt · SvelteKit · Astro · Solid.js · Qwik · Remix · Preact · Lit · Fresh (Deno)。

Framework Status Preview Repository
Next.js 14 ✅ Deployed Preview GitHub
Vue 3.5 ✅ Deployed Preview GitHub
Angular 21 ✅ Deployed Preview GitHub
Nuxt 4 ✅ Deployed Preview GitHub
SvelteKit 2 ✅ Deployed Preview GitHub
Astro 5 ✅ Deployed Preview GitHub
Solid.js ✅ Deployed Preview GitHub
Qwik ✅ Deployed Preview GitHub
Remix ✅ Deployed Preview GitHub
Preact ✅ Deployed Preview GitHub
Lit ✅ Deployed Preview GitHub
Fresh (Deno) ✅ Deployed Preview GitHub

Tech Stack Overview

Common Tech Stack

  • TypeScript - Type safety
  • Tailwind CSS - Style system
  • shadcn/ui - UI component library (framework-specific versions)
  • Mock.js - Development environment data simulation
  • ECharts - Chart visualization

Framework-Specific Dependencies

Feature React/Next.js Vue 3 Angular Svelte
State Management Zustand Pinia Signals/RxJS Svelte Stores
Data Fetching TanStack Query TanStack Query RxJS TanStack Query
Routing Next.js App Router Vue Router Angular Router SvelteKit
Forms React Hook Form VeeValidate Reactive Forms Superforms
Drag Layout react-grid-layout grid-layout-plus angular-gridster2 svelte-grid
]]>
<![CDATA[State Management]]> https://halolight.docs.h7ml.cn/en/development/state-management https://halolight.docs.h7ml.cn/en/development/state-management Fri, 19 Dec 2025 04:56:41 GMT State Management

This document describes the state management patterns for the HaloLight project, covering implementation solutions for different frameworks.

State Management Solution Comparison

Framework State Library Features
React/Next.js Zustand Simple, no boilerplate
Vue 3 Pinia Official recommendation, type-safe
Svelte Svelte Stores Native reactivity
Angular Signals + RxJS Fine-grained reactivity
Solid.js createStore Fine-grained reactivity

Store Module Division

stores/
├── auth.ts           # Authentication state
├── ui-settings.ts    # UI settings
├── dashboard.ts      # Dashboard layout
├── navigation.ts     # Navigation menu
├── tabs.ts           # Multi-tab page
└── error.ts          # Error state

Auth Store

State Definition

ts
interface AuthState {
  user: User | null
  token: string | null
  refreshToken: string | null
  permissions: string[]
  roles: string[]
  isAuthenticated: boolean
  isLoading: boolean
}

Zustand Implementation (React)

ts
export const useAuthStore = create<AuthStore>()(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      permissions: [],
      isAuthenticated: false,

      login: async (credentials) => {
        const response = await authService.login(credentials)
        set({
          user: response.user,
          token: response.token,
          permissions: response.permissions,
          isAuthenticated: true,
        })
      },

      hasPermission: (permission) => {
        const { permissions } = get()
        return permissions.some((p) =>
          p === '*' || p === permission ||
          (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
        )
      },
    }),
    { name: 'auth-storage' }
  )
)

Pinia Implementation (Vue)

ts
export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)
  const permissions = ref<string[]>([])

  const isAuthenticated = computed(() => !!token.value)

  function hasPermission(permission: string): boolean {
    return permissions.value.some((p) =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  }

  return { user, token, permissions, isAuthenticated, hasPermission }
}, { persist: { paths: ['token', 'user'] } })

Dashboard Store

Layout State

ts
interface DashboardState {
  layouts: { lg: GridLayout[]; md: GridLayout[]; sm: GridLayout[] }
  widgets: WidgetConfig[]
  isEditing: boolean
}

interface GridLayout {
  i: string; x: number; y: number; w: number; h: number
}

Persistence Strategy

Data Type Storage Location Example
User Preferences localStorage Theme, language
UI State localStorage Sidebar, layout
Temporary Data sessionStorage Form drafts
Server Data TanStack Query API responses
]]>
<![CDATA[Theme System]]> https://halolight.docs.h7ml.cn/en/development/theming https://halolight.docs.h7ml.cn/en/development/theming Fri, 19 Dec 2025 04:56:41 GMT Theme System

This document describes the theme switching and skin preset system for HaloLight.

Theme Modes

Mode Description
light Light theme
dark Dark theme
system Follow system

Skin Presets

11 color skins available:

Skin Primary Use Case
default Blue General
zinc Gray Minimalist
slate Blue-gray Professional
stone Brown-gray Warm
gray Neutral gray General
neutral Black & white Minimal
red Red Alert
rose Rose Fashion
orange Orange Energetic
green Green Natural
blue Blue Tech
yellow Yellow Bright
violet Violet Elegant

CSS Variables

Color Variable Definition

css
:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --card: 0 0% 100%;
  --card-foreground: 222.2 84% 4.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --secondary-foreground: 222.2 47.4% 11.2%;
  --muted: 210 40% 96.1%;
  --muted-foreground: 215.4 16.3% 46.9%;
  --accent: 210 40% 96.1%;
  --accent-foreground: 222.2 47.4% 11.2%;
  --destructive: 0 84.2% 60.2%;
  --destructive-foreground: 210 40% 98%;
  --border: 214.3 31.8% 91.4%;
  --input: 214.3 31.8% 91.4%;
  --ring: 222.2 84% 4.9%;
  --radius: 0.5rem;
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  /* ... */
}

Skin Variables

css
[data-skin="rose"] {
  --primary: 346.8 77.2% 49.8%;
  --primary-foreground: 355.7 100% 97.3%;
}

[data-skin="green"] {
  --primary: 142.1 76.2% 36.3%;
  --primary-foreground: 355.7 100% 97.3%;
}

Theme Switching Implementation

React Context

tsx
interface ThemeContextValue {
  theme: 'light' | 'dark' | 'system'
  skin: SkinPreset
  setTheme: (theme: string) => void
  setSkin: (skin: SkinPreset) => void
}

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState('system')
  const [skin, setSkin] = useState<SkinPreset>('default')

  useEffect(() => {
    const root = document.documentElement
    root.classList.remove('light', 'dark')

    if (theme === 'system') {
      const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light'
      root.classList.add(systemTheme)
    } else {
      root.classList.add(theme)
    }

    root.setAttribute('data-skin', skin)
  }, [theme, skin])

  return (
    <ThemeContext.Provider value={{ theme, skin, setTheme, setSkin }}>
      {children}
    </ThemeContext.Provider>
  )
}

Vue Composable

ts
export function useTheme() {
  const theme = ref<'light' | 'dark' | 'system'>('system')
  const skin = ref<SkinPreset>('default')

  const actualTheme = computed(() => {
    if (theme.value === 'system') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light'
    }
    return theme.value
  })

  watch([theme, skin], () => {
    document.documentElement.classList.remove('light', 'dark')
    document.documentElement.classList.add(actualTheme.value)
    document.documentElement.setAttribute('data-skin', skin.value)
  }, { immediate: true })

  return { theme, skin, actualTheme }
}

View Transitions API

Theme Switch Animation (Vue)

ts
async function toggleTheme() {
  if (!document.startViewTransition) {
    theme.value = theme.value === 'dark' ? 'light' : 'dark'
    return
  }

  await document.startViewTransition(() => {
    theme.value = theme.value === 'dark' ? 'light' : 'dark'
  }).ready

  // Circular expand animation
  const { clientX, clientY } = event
  const radius = Math.hypot(
    Math.max(clientX, window.innerWidth - clientX),
    Math.max(clientY, window.innerHeight - clientY)
  )

  document.documentElement.animate(
    {
      clipPath: [
        `circle(0px at ${clientX}px ${clientY}px)`,
        `circle(${radius}px at ${clientX}px ${clientY}px)`,
      ],
    },
    {
      duration: 500,
      easing: 'ease-in-out',
      pseudoElement: '::view-transition-new(root)',
    }
  )
}

CSS Configuration

css
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

::view-transition-old(root) {
  z-index: 1;
}

::view-transition-new(root) {
  z-index: 9999;
}

Theme Selector Component

tsx
function ThemeSelector() {
  const { theme, setTheme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon">
          <Sun className="h-4 w-4 rotate-0 scale-100 dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-4 w-4 rotate-90 scale-0 dark:rotate-0 dark:scale-100" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem onClick={() => setTheme('light')}>
          <Sun className="mr-2 h-4 w-4" /> Light
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('dark')}>
          <Moon className="mr-2 h-4 w-4" /> Dark
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('system')}>
          <Monitor className="mr-2 h-4 w-4" /> System
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Skin Selector

tsx
function SkinSelector() {
  const { skin, setSkin } = useTheme()

  const skins: SkinPreset[] = [
    'default', 'zinc', 'slate', 'stone', 'gray',
    'neutral', 'red', 'rose', 'orange', 'green',
    'blue', 'yellow', 'violet'
  ]

  return (
    <div className="grid grid-cols-5 gap-2">
      {skins.map((s) => (
        <button
          key={s}
          onClick={() => setSkin(s)}
          className={cn(
            'h-8 w-8 rounded-full border-2',
            skin === s ? 'border-primary' : 'border-transparent'
          )}
          style={{ backgroundColor: `hsl(var(--skin-${s}))` }}
        />
      ))}
    </div>
  )
}

ECharts Theme Adaptation

ts
const echartTheme = computed(() => ({
  backgroundColor: 'transparent',
  textStyle: {
    color: actualTheme.value === 'dark' ? '#e5e5e5' : '#333',
  },
  title: {
    textStyle: {
      color: actualTheme.value === 'dark' ? '#fff' : '#333',
    },
  },
  legend: {
    textStyle: {
      color: actualTheme.value === 'dark' ? '#e5e5e5' : '#333',
    },
  },
  xAxis: {
    axisLine: { lineStyle: { color: actualTheme.value === 'dark' ? '#444' : '#ccc' } },
    splitLine: { lineStyle: { color: actualTheme.value === 'dark' ? '#333' : '#eee' } },
  },
  yAxis: {
    axisLine: { lineStyle: { color: actualTheme.value === 'dark' ? '#444' : '#ccc' } },
    splitLine: { lineStyle: { color: actualTheme.value === 'dark' ? '#333' : '#eee' } },
  },
}))
]]>
<![CDATA[Check-in Scheduler Platform]]> https://halolight.docs.h7ml.cn/en/guide/action https://halolight.docs.h7ml.cn/en/guide/action Fri, 19 Dec 2025 04:56:41 GMT Check-in Scheduler Platform

HaloLight Action is a modern check-in scheduler platform built with Next.js 14 App Router and Supabase, supporting multi-platform automatic check-ins, task scheduling, execution tracking, and push notifications.

Live Preview: https://halolight-action.h7ml.cn

GitHub: https://github.com/halolight/halolight-action

Tech Stack

Technology Version Description
Next.js 14.2 App Router architecture
TypeScript 5.7 Type safety
Supabase latest PostgreSQL + Auth + RLS
Tailwind CSS 3.4 Utility-first CSS
shadcn/ui latest Radix UI component library
TanStack Query 5.x Server state management
Zustand 5.x Client state management
Framer Motion latest Smooth animations
Vitest latest Unit testing

Core Features

Automated Check-in Tasks

  • Multi-platform Support: V2EX, GitHub, Juejin, CSDN, Bilibili, etc.
  • Cron Scheduling: Flexible cron expressions
  • Priority Queue: Task priority management
  • Manual Trigger: Support instant execution
  • Enable/Disable: Flexible task state control

Data Monitoring & Analytics

  • Check-in Records: Complete execution history, success rate statistics
  • Execution Analysis: Duration analysis, error tracking
  • Push Logs: Multi-channel push history, status monitoring
  • Backend Pagination: Element UI style, supports large datasets

Multi-channel Push Notifications

  • 12 Push Services: ServerChan, DingTalk, WeCom, Feishu, Telegram, Discord, Bark, etc.
  • Integrated push-all-in-one: Unified push interface
  • Push Testing: Instant configuration verification
  • Default Channel: Flexible notification routing

Complete Permission System

  • Supabase RLS: Database-level row-level security
  • Role Management: super_admin / admin / user / guest
  • API Tokens: Fine-grained API access control
  • Audit Logs: Operation tracking

Directory Structure

halolight-action/
├── app/                    # Next.js 14 App Router
│   ├── (auth)/             # Authentication pages
│   │   ├── login/
│   │   ├── register/
│   │   ├── forgot-password/
│   │   └── reset-password/
│   ├── (dashboard)/        # Dashboard pages (15 feature pages)
│   │   ├── dashboard/      # Dashboard home
│   │   ├── signin-tasks/   # Check-in task management
│   │   ├── signin-records/ # Check-in records
│   │   ├── push-logs/      # Push logs
│   │   ├── scheduled-tasks/# Scheduled tasks
│   │   ├── notifications/  # Notification center
│   │   ├── data-dictionary/# Data dictionary
│   │   ├── users/          # User management
│   │   └── settings/       # Settings
│   │       ├── profile/
│   │       ├── appearance/
│   │       ├── push-channels/
│   │       └── api-proxy/
│   └── api/                # API routes
│       ├── cron/           # Cron job execution
│       ├── signin/         # Check-in execution
│       └── push/test/      # Push testing
├── components/             # React components
│   ├── ui/                 # shadcn/ui base components
│   ├── layout/             # Layout components
│   └── auth/               # Auth components
├── hooks/                  # React Query Hooks
├── lib/                    # Utilities
│   ├── supabase/           # Supabase client
│   ├── dal/                # Data access layer
│   ├── cron/               # Cron executor
│   └── push/               # Push service wrapper
├── providers/              # Context Providers
├── stores/                 # Zustand state management
├── types/                  # TypeScript type definitions
└── supabase/migrations/    # Database migration scripts

Quick Start

Requirements

  • Node.js >= 24.0.0
  • pnpm >= 10.x
  • Supabase account (required)

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-action.git
cd halolight-action

# Install dependencies
pnpm install

Environment Variables

bash
cat > .env.local <<'EOF'
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_APP_NAME=HaloLight Action
NEXT_PUBLIC_APP_URL=http://localhost:3000
EOF

Supabase Setup

  1. Create a project in Supabase Dashboard
  2. Execute supabase/migrations/init.sql in SQL Editor
  3. Copy Project URL and anon public key to .env.local
  4. (Optional) Run pnpm generate:types to generate type definitions

Start Development Server

bash
pnpm dev
# Visit http://localhost:3000

Default Test Account

Database Schema

Core Tables

Table Description Key Features
signin_tasks Check-in task config Cron scheduling, priority, credentials
signin_records Check-in execution records History tracking, success rate stats
push_channels Push channel config 12 push services, testing & default channel
push_logs Push execution logs Push history, error tracking
data_dictionary Data dictionary config System config, multi-type support
notifications System notifications User messages, notification center
cron_jobs Cron job config HTTP request scheduling
users User info Auth, role management
user_tokens API tokens Access control, token management

Row Level Security (RLS)

  • User Isolation: Users can only access their own data
  • Role Permissions: Admins have higher permissions
  • System Protection: System configs are protected, cannot be deleted
  • Audit Trail: All operations are traceable

Common Commands

bash
# Development
pnpm dev                # Start development server
pnpm build              # Production build
pnpm start              # Start production server

# Quality Checks
pnpm lint               # ESLint check
pnpm type-check         # TypeScript type check
pnpm format             # Prettier format
pnpm ci                 # Full CI check

# Testing
pnpm test               # Run tests (watch)
pnpm test:run           # Run tests (once)
pnpm test:coverage      # Test coverage report

# Supabase
pnpm generate:types     # Generate types from Supabase

Deployment

One-click deploy:

Deploy with Vercel

Manual Deployment

bash
# Build
pnpm build

# Start production server
pnpm start

Vercel Cron Jobs

Add in Vercel project settings:

  • Schedule: 0 8 * * * (daily at 8 AM)
  • Path: /api/cron

Security Notes

Important Reminders

  1. Don't commit secrets:

    • Don't commit .env.local to Git
    • Don't expose Supabase keys in Issues/PRs
    • Don't hardcode sensitive info in code
  2. Use correct keys:

    • Frontend uses anon public key (safe)
    • Frontend should NOT use service_role key (dangerous)
  3. Credential storage:

    • Check-in task credentials should be encrypted
    • Consider Supabase Vault or external key management for production
  4. RLS policies:

    • Regularly review RLS policies
    • Test unauthorized access scenarios
    • Always enable RLS for new tables

Relationship with HaloLight Series

Project Backend Features
halolight Mock.js Frontend demo, no backend needed
halolight-action Supabase Check-in scheduler, real backend
halolight-vue Mock.js Vue 3.5 implementation
halolight-angular Mock.js Angular implementation
]]>
<![CDATA[Super Admin Panel]]> https://halolight.docs.h7ml.cn/en/guide/admin https://halolight.docs.h7ml.cn/en/guide/admin Fri, 19 Dec 2025 04:56:41 GMT Super Admin Panel

HaloLight Admin is a powerful super admin panel for managing multiple HaloLight instances and tenants.

Features

  • 🏢 Multi-Tenant Management - Support for multi-tenant isolation and management
  • ⚙️ Global Configuration - Centralized configuration management for all instances
  • 📊 System Monitoring - Real-time system status and performance monitoring
  • 📝 Audit Logs - Complete operation audit records
  • 👥 User Management - Cross-tenant user management
  • 🔐 Permission Control - Fine-grained permission management

Quick Start

bash
# Clone repository
git clone https://github.com/halolight/halolight-admin.git
cd halolight-admin

# Install dependencies
pnpm install

# Run development server
pnpm dev

Feature Modules

Tenant Management

  • Create/edit/delete tenants
  • Tenant quota management
  • Tenant data isolation

System Monitoring

  • CPU/memory usage
  • API request statistics
  • Error log monitoring
  • Performance metrics analysis

Audit Logs

  • User operation records
  • System change tracking
  • Security event alerts
  • Log export functionality
]]>
<![CDATA[AI Assistant]]> https://halolight.docs.h7ml.cn/en/guide/ai https://halolight.docs.h7ml.cn/en/guide/ai Fri, 19 Dec 2025 04:56:41 GMT AI Assistant

HaloLight AI service is built with Hono + LangChain.js, providing RAG retrieval-augmented generation, action execution, and multi-model automatic fallback.

Live Preview: Internal API service (no standalone demo)

GitHub: https://github.com/halolight/halolight-ai

Tech Stack

Technology Version Description
Hono latest Lightweight HTTP framework
LangChain.js latest LLM orchestration + RAG pipeline
pgvector latest PostgreSQL vector search extension
Drizzle ORM latest TypeScript ORM & migrations
PostgreSQL 14+ Persistence and vector storage
Node.js 22+ Runtime
Zod latest Data validation

Directory Structure

halolight-ai/
├── src/
│   ├── index.ts                    # Application entry
│   ├── routes/                     # API routes
│   │   ├── chat.ts                 # Chat interface
│   │   ├── actions.ts              # Action execution
│   │   ├── history.ts              # History records
│   │   └── knowledge.ts            # Knowledge base management
│   ├── services/
│   │   ├── llm/                    # LLM service layer
│   │   │   ├── openai.ts
│   │   │   ├── anthropic.ts
│   │   │   └── factory.ts          # Model factory (auto fallback)
│   │   ├── rag/                    # RAG pipeline
│   │   │   ├── embeddings.ts       # Document chunking & embedding
│   │   │   ├── retriever.ts        # Vector retrieval
│   │   │   └── pipeline.ts         # RAG pipeline
│   │   ├── actions/                # Action execution system
│   │   │   ├── executor.ts         # Action executor
│   │   │   ├── registry.ts         # Action registry
│   │   │   └── permissions.ts      # Permission validation
│   │   └── memory/
│   │       └── conversation.ts     # Conversation memory management
│   ├── db/
│   │   ├── schema.ts               # Drizzle Schema (with pgvector)
│   │   └── client.ts               # Database client
│   ├── middleware/
│   │   ├── auth.ts                 # Authentication middleware
│   │   └── tenant.ts               # Tenant isolation
│   └── types/
│       └── index.ts                # TypeScript type definitions
├── drizzle/                        # Migration files
├── drizzle.config.ts
├── Dockerfile
├── docker-compose.yml
└── package.json

Quick Start

Prerequisites

  • Node.js >= 22.0.0
  • PostgreSQL >= 14 (with pgvector extension)
  • At least one LLM provider API Key configured

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-ai.git
cd halolight-ai

# Install dependencies
pnpm install

Environment Variables

bash
cp .env.example .env

Key configurations:

env
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/halolight_ai

# LLM Providers (configure at least one)
OPENAI_API_KEY=sk-...
# or
ANTHROPIC_API_KEY=sk-ant-...
# or
AZURE_OPENAI_API_KEY=...

# RAG Configuration
CHUNK_SIZE=1000              # Document chunk size
CHUNK_OVERLAP=200            # Chunk overlap
RETRIEVAL_TOP_K=5            # Retrieval count

# Conversation Configuration
MAX_CONVERSATION_HISTORY=20  # Conversation history length
ENABLE_STREAMING=true        # Enable streaming response
ENABLE_AUDIT_LOG=true        # Enable audit logging

# Production
JWT_SECRET=your-secret-key
CORS_ORIGINS=https://your-domain.com

Initialize Database

bash
# Generate migration files
pnpm db:generate

# Run migrations
pnpm db:migrate

# Or push schema directly (development)
pnpm db:push

# Open Drizzle Studio
pnpm db:studio

Start Development Server

bash
pnpm dev

Service will start at http://localhost:3000.

Production Build

bash
pnpm build
pnpm start

Core Features

1. Multi-Model Support

The system automatically detects available LLM providers and falls back by priority:

Azure OpenAI (1) → OpenAI (2) → Anthropic (3) → Ollama (4)

2. RAG Knowledge Base

Step Description Configuration
Document Chunking RecursiveCharacterTextSplitter 1000 chars, 200 overlap
Vector Embedding OpenAI Embeddings text-embedding-3-small
Vector Storage pgvector 1536 dimensions
Retrieval Cosine similarity Top-K (default 5)
Context Injection Inject retrieval results into LLM prompt -

3. Streaming Response

Enable SSE streaming output to reduce first-token latency:

bash
POST /api/ai/chat/stream
Content-Type: application/json

{
  "message": "Hello",
  "streaming": true
}

4. Permission Control

Role-based access control (RBAC):

Role Permission Level
super_admin Highest permission
admin Admin permission
user Regular user
guest Guest

Sensitive operations require confirmation (_confirmed: true).

5. Conversation Memory

  • Stored in conversations and messages tables
  • Retains last 20 messages by default
  • Supports multi-turn conversation context

6. Tenant Isolation

All data operations are based on TenantContext:

typescript
interface TenantContext {
  tenantId: string;
  userId: string;
  role: UserRole;
}

Extracted from request headers:

  • X-Tenant-ID
  • X-User-ID
  • X-User-Role

API Endpoints

Health Check

bash
GET /health
GET /health/ready
GET /api/ai/info

Chat

bash
# Send message
POST /api/ai/chat
Content-Type: application/json

{
  "message": "Hello, introduce HaloLight",
  "conversationId": "uuid",    // optional
  "includeContext": true,
  "maxContextDocs": 5
}

# Streaming response
POST /api/ai/chat/stream

Action Execution

bash
# Execute action
POST /api/ai/actions/execute
Content-Type: application/json

{
  "action": "query_users",
  "params": {
    "role": "admin",
    "limit": 10
  }
}

# Get available actions
GET /api/ai/actions/available

# Get action details
GET /api/ai/actions/:name

History

bash
GET /api/ai/history?limit=10
GET /api/ai/history/:id
DELETE /api/ai/history/:id
PATCH /api/ai/history/:id

Knowledge Base

bash
# Import document
POST /api/ai/knowledge/ingest
Content-Type: application/json

{
  "content": "Document content...",
  "metadata": {
    "title": "Document Title",
    "category": "Technical Documentation"
  },
  "source": "manual",
  "sourceId": "doc-001"
}

# Batch import
POST /api/ai/knowledge/batch-ingest

# List documents
GET /api/ai/knowledge?limit=50&offset=0

# Delete document
DELETE /api/ai/knowledge/:id

Authentication

All /api/* endpoints require the following headers (optional in development):

X-Tenant-ID: your-tenant-id
X-User-ID: your-user-id
X-User-Role: admin | user | guest

Deployment

Docker

bash
# Build image
docker build -t halolight-ai .

# Run container
docker run -p 3000:3000 \
  -e DATABASE_URL=postgresql://... \
  -e OPENAI_API_KEY=sk-... \
  halolight-ai

Docker Compose

bash
docker-compose up -d

Production Requirements

  • DATABASE_URL: PostgreSQL connection string
  • NODE_ENV=production
  • At least one LLM provider API Key
  • JWT_SECRET: Secret key for authentication
  • CORS_ORIGINS: Allowed cross-origin sources

Troubleshooting

Database Connection Failed

bash
# Check if PostgreSQL is running
psql $DATABASE_URL -c "SELECT 1"

# Check pgvector extension
psql $DATABASE_URL -c "CREATE EXTENSION IF NOT EXISTS vector"

LLM Provider Errors

bash
# Check available providers
curl http://localhost:3000/api/ai/info

Inaccurate Vector Retrieval

  • Increase RETRIEVAL_TOP_K value
  • Adjust CHUNK_SIZE and CHUNK_OVERLAP
  • Use hybrid retrieval (vector + keyword)
]]>
<![CDATA[Angular Version]]> https://halolight.docs.h7ml.cn/en/guide/angular https://halolight.docs.h7ml.cn/en/guide/angular Fri, 19 Dec 2025 04:56:41 GMT Angular Version

HaloLight Angular version is built on Angular 21 with Signals + Standalone Components + TypeScript.

Live Preview: https://halolight-angular.h7ml.cn/

GitHub: https://github.com/halolight/halolight-angular

Features

  • 🏗️ Angular 21 - Latest enterprise framework with Signals + Standalone Components
  • NgRx Signals - Lightweight reactive state management
  • 🎨 Theme System - 11 skins, light/dark mode, View Transitions
  • 🔐 Authentication - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization and business management
  • 🛡️ Permission Control - RBAC fine-grained permission management
  • 📑 Multi-tabs - Tab bar management
  • Command Palette - Keyboard shortcuts navigation

Tech Stack

Technology Version Description
Angular 21.x Enterprise framework
TypeScript 5.x Type safety
Tailwind CSS 4.x Atomic CSS
spartan/ui latest UI component library (Radix-style)
NgRx Signals 21.x Reactive state management
TanStack Query 5.x Server state
Mock.js 1.x Data mocking

Core Features

  • Configurable Dashboard - 9 widgets, drag-and-drop layout, responsive design
  • Multi-tab Navigation - Browser-style tabs, context menu, state caching
  • Permission System - RBAC permission control, route guards, permission directives/components
  • Theme System - 11 skins, light/dark mode, View Transitions
  • Multi-account Switching - Quick account switching, remember login state
  • Command Palette - Keyboard shortcuts (⌘K), global search
  • Real-time Notifications - WebSocket push, notification center

Directory Structure

halolight-angular/
├── src/
│   ├── app/
│   │   ├── pages/                  # Page components
│   │   │   ├── admin/             # Admin pages
│   │   │   │   ├── dashboard/     # Dashboard
│   │   │   │   ├── users/         # User management
│   │   │   │   ├── roles/         # Role management
│   │   │   │   ├── permissions/   # Permission management
│   │   │   │   ├── settings/      # System settings
│   │   │   │   └── profile/       # User profile
│   │   │   └── auth/              # Auth pages
│   │   │       ├── login/
│   │   │       ├── register/
│   │   │       ├── forgot-password/
│   │   │       └── reset-password/
│   │   ├── components/
│   │   │   ├── ui/                # spartan/ui components
│   │   │   ├── layout/            # Layout components
│   │   │   ├── dashboard/         # Dashboard components
│   │   │   └── shared/            # Shared components
│   │   ├── services/              # Service layer
│   │   ├── stores/                # NgRx Signals Stores
│   │   ├── guards/                # Route guards
│   │   ├── interceptors/          # HTTP interceptors
│   │   ├── directives/            # Directives
│   │   ├── pipes/                 # Pipes
│   │   ├── lib/                   # Utility library
│   │   ├── types/                 # Type definitions
│   │   ├── mocks/                 # Mock data
│   │   ├── app.routes.ts          # Route configuration
│   │   ├── app.config.ts          # App configuration
│   │   └── app.component.ts       # Root component
│   ├── environments/              # Environment configuration
│   └── styles.css                 # Global styles
├── public/                        # Static assets
├── angular.json
├── tailwind.config.js
├── tsconfig.json
└── package.json

Quick Start

Environment Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-angular.git
cd halolight-angular
pnpm install

Environment Variables

bash
cp src/environments/environment.example.ts src/environments/environment.development.ts
ts
// src/environments/environment.development.ts
export const environment = {
  production: false,
  apiUrl: '/api',
  useMock: true,
  appTitle: 'Admin Pro',
  brandName: 'Halolight',
  demoEmail: '[email protected]',
  demoPassword: '123456',
  showDemoHint: true,
};

Start Development

bash
pnpm start

Visit http://localhost:4200

Build for Production

bash
pnpm build
ng build --configuration production

Demo Account

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Core Functionality

State Management (NgRx Signals)

ts
// stores/auth.store.ts
import { signalStore, withState, withMethods, withComputed, patchState } from '@ngrx/signals';
import { computed, inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

interface AuthState {
  user: User | null;
  token: string | null;
  loading: boolean;
}

const initialState: AuthState = {
  user: null,
  token: null,
  loading: false,
};

export const AuthStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withComputed((store) => ({
    isAuthenticated: computed(() => !!store.token() && !!store.user()),
    permissions: computed(() => store.user()?.permissions ?? []),
  })),
  withMethods((store, authService = inject(AuthService)) => ({
    async login(credentials: LoginCredentials) {
      patchState(store, { loading: true });
      try {
        const response = await authService.login(credentials);
        patchState(store, {
          user: response.user,
          token: response.token,
          loading: false,
        });
      } catch (error) {
        patchState(store, { loading: false });
        throw error;
      }
    },

    logout() {
      patchState(store, { user: null, token: null });
    },

    hasPermission(permission: string): boolean {
      const permissions = store.permissions();
      return permissions.some(p =>
        p === '*' || p === permission ||
        (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
      );
    },
  }))
);

Data Fetching (TanStack Query)

ts
// services/users.service.ts
import { Injectable, inject } from '@angular/core';
import { injectQuery, injectMutation, injectQueryClient } from '@tanstack/angular-query-experimental';
import { ApiService } from './api.service';

@Injectable({ providedIn: 'root' })
export class UsersService {
  private api = inject(ApiService);
  private queryClient = injectQueryClient();

  getUsers(params?: UserQueryParams) {
    return injectQuery(() => ({
      queryKey: ['users', params],
      queryFn: () => this.api.get<UserListResponse>('/users', { params }),
    }));
  }

  createUser() {
    return injectMutation(() => ({
      mutationFn: (data: CreateUserDto) => this.api.post<User>('/users', data),
      onSuccess: () => {
        this.queryClient.invalidateQueries({ queryKey: ['users'] });
      },
    }));
  }
}

Permission Control

ts
// directives/permission.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef, inject, effect } from '@angular/core';
import { AuthStore } from '../stores/auth.store';

@Directive({
  selector: '[appPermission]',
  standalone: true,
})
export class PermissionDirective {
  private templateRef = inject(TemplateRef<unknown>);
  private viewContainer = inject(ViewContainerRef);
  private authStore = inject(AuthStore);

  @Input() set appPermission(permission: string) {
    effect(() => {
      const hasPermission = this.authStore.hasPermission(permission);
      this.viewContainer.clear();
      if (hasPermission) {
        this.viewContainer.createEmbeddedView(this.templateRef);
      }
    });
  }
}
html
<!-- Using directive -->
<button *appPermission="'users:delete'">Delete</button>
ts
// components/permission-guard.component.ts
import { Component, Input, inject, computed } from '@angular/core';
import { AuthStore } from '../../stores/auth.store';

@Component({
  selector: 'app-permission-guard',
  standalone: true,
  template: `
    @if (hasPermission()) {
      <ng-content />
    } @else {
      <ng-content select="[fallback]" />
    }
  `,
})
export class PermissionGuardComponent {
  @Input({ required: true }) permission!: string;

  private authStore = inject(AuthStore);
  hasPermission = computed(() => this.authStore.hasPermission(this.permission));
}
html
<!-- Using component -->
<app-permission-guard permission="users:delete">
  <app-delete-button />
  <span fallback>No permission</span>
</app-permission-guard>

Draggable Dashboard

ts
// components/dashboard/dashboard-grid.component.ts
import { Component, inject, computed } from '@angular/core';
import { GridsterModule, GridsterConfig, GridsterItem } from 'angular-gridster2';
import { DashboardStore } from '../../stores/dashboard.store';

@Component({
  selector: 'app-dashboard-grid',
  standalone: true,
  imports: [GridsterModule, WidgetWrapperComponent],
  template: `
    <gridster [options]="options()">
      @for (widget of widgets(); track widget.id) {
        <gridster-item [item]="widget">
          <app-widget-wrapper [widget]="widget" />
        </gridster-item>
      }
    </gridster>
  `,
})
export class DashboardGridComponent {
  private dashboardStore = inject(DashboardStore);

  widgets = this.dashboardStore.widgets;
  isEditing = this.dashboardStore.isEditing;

  options = computed<GridsterConfig>(() => ({
    gridType: 'fit',
    displayGrid: this.isEditing() ? 'always' : 'none',
    draggable: { enabled: this.isEditing() },
    resizable: { enabled: this.isEditing() },
    pushItems: true,
    minCols: 12,
    maxCols: 12,
    minRows: 4,
    defaultItemCols: 3,
    defaultItemRows: 2,
    itemChangeCallback: (item) => this.dashboardStore.updateWidget(item),
  }));
}

Theme System

Skin Presets

Supports 11 preset skins, switch via the quick settings panel:

Skin Primary Color CSS Variable
Default Purple --primary: 51.1% 0.262 276.97
Blue Blue --primary: 54.8% 0.243 264.05
Emerald Emerald --primary: 64.6% 0.178 142.49
Rose Rose --primary: 59.3% 0.214 12.76
Orange Orange --primary: 65.4% 0.194 35.76
Amber Amber --primary: 74.2% 0.167 83.25
Yellow Yellow --primary: 84.5% 0.181 99.58
Lime Lime --primary: 76.5% 0.165 128.35
Teal Teal --primary: 59.8% 0.134 179.61
Cyan Cyan --primary: 68.3% 0.148 192.18
Sky Sky --primary: 68.5% 0.171 227.08

CSS Variables (OKLch)

css
/* Example variable definitions */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 98% 0 0;
  --secondary: 96.1% 0.002 286.08;
  --secondary-foreground: 14.9% 0.017 285.75;
  --muted: 96.1% 0.002 286.08;
  --muted-foreground: 55.4% 0.009 285.82;
  --accent: 96.1% 0.002 286.08;
  --accent-foreground: 14.9% 0.017 285.75;
  --border: 92.2% 0.004 285.86;
  --input: 92.2% 0.004 285.86;
  --ring: 51.1% 0.262 276.97;
}

.dark {
  --background: 14.9% 0.017 285.75;
  --foreground: 98% 0 0;
  --primary: 56.1% 0.287 277.04;
  /* ... */
}

Theme Switching

ts
// Toggle theme
const uiSettingsStore = inject(UiSettingsStore);
uiSettingsStore.setTheme('dark'); // 'light' | 'dark' | 'system'

// Change skin
uiSettingsStore.setSkin('rose'); // 11 skin presets

Page Routes

Path Page Permission
/ Redirect to /dashboard -
/login Login Public
/register Register Public
/forgot-password Forgot password Public
/reset-password Reset password Public
/dashboard Dashboard dashboard:view
/users User list users:list
/users/create Create user users:create
/users/:id User details users:view
/users/:id/edit Edit user users:update
/roles Role management roles:list
/permissions Permission management permissions:list
/settings System settings settings:view
/profile User profile Authenticated

Environment Variables

Configuration Example

ts
// src/environments/environment.development.ts
export const environment = {
  production: false,
  apiUrl: '/api',
  useMock: true,
  appTitle: 'Admin Pro',
  brandName: 'Halolight',
  demoEmail: '[email protected]',
  demoPassword: '123456',
  showDemoHint: true,
};

Variable Description

Variable Description Default Value
production Production environment false
apiUrl API base path /api
useMock Use Mock data true
appTitle Application title Admin Pro
brandName Brand name Halolight
demoEmail Demo account email [email protected]
demoPassword Demo account password 123456
showDemoHint Show demo hint true

Usage

ts
import { inject } from '@angular/core';
import { environment } from '../environments/environment';

// Use in components or services
export class ApiService {
  private apiUrl = environment.apiUrl;
  private useMock = environment.useMock;

  // ...
}

Common Commands

bash
pnpm start          # Start development server
pnpm build          # Production build
pnpm lint           # Lint code
pnpm lint:fix       # Auto fix
pnpm type-check     # Type check
pnpm test           # Run tests
pnpm test:coverage  # Test coverage

Testing

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Single run
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI

Test Example

ts
// auth.store.spec.ts
import { TestBed } from '@angular/core/testing';
import { AuthStore } from './auth.store';
import { AuthService } from '../services/auth.service';

describe('AuthStore', () => {
  let store: InstanceType<typeof AuthStore>;
  let authService: jasmine.SpyObj<AuthService>;

  beforeEach(() => {
    const authServiceSpy = jasmine.createSpyObj('AuthService', ['login', 'logout']);

    TestBed.configureTestingModule({
      providers: [
        AuthStore,
        { provide: AuthService, useValue: authServiceSpy },
      ],
    });

    store = TestBed.inject(AuthStore);
    authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
  });

  it('should initialize with default state', () => {
    expect(store.user()).toBeNull();
    expect(store.token()).toBeNull();
    expect(store.isAuthenticated()).toBe(false);
  });

  it('should login successfully', async () => {
    const mockResponse = {
      user: { id: '1', email: '[email protected]', permissions: ['users:view'] },
      token: 'mock-token',
    };
    authService.login.and.returnValue(Promise.resolve(mockResponse));

    await store.login({ email: '[email protected]', password: '123456' });

    expect(store.user()).toEqual(mockResponse.user);
    expect(store.token()).toBe('mock-token');
    expect(store.isAuthenticated()).toBe(true);
  });

  it('should check permissions correctly', async () => {
    const mockResponse = {
      user: { id: '1', email: '[email protected]', permissions: ['users:*', 'dashboard:view'] },
      token: 'mock-token',
    };
    authService.login.and.returnValue(Promise.resolve(mockResponse));
    await store.login({ email: '[email protected]', password: '123456' });

    expect(store.hasPermission('users:view')).toBe(true);
    expect(store.hasPermission('users:delete')).toBe(true);
    expect(store.hasPermission('dashboard:view')).toBe(true);
    expect(store.hasPermission('settings:view')).toBe(false);
  });
});

Configuration

Angular Configuration

ts
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideQueryClient } from '@tanstack/angular-query-experimental';
import { QueryClient } from '@tanstack/query-core';

import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
    provideAnimations(),
    provideQueryClient(new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 1000 * 60 * 5, // 5 minutes
          gcTime: 1000 * 60 * 10, // 10 minutes
        },
      },
    })),
  ],
};

Tailwind Configuration

js
// tailwind.config.js
import { fontFamily } from 'tailwindcss/defaultTheme';

export default {
  darkMode: ['class'],
  content: ['./src/**/*.{html,ts}'],
  theme: {
    container: {
      center: true,
      padding: '2rem',
      screens: {
        '2xl': '1400px',
      },
    },
    extend: {
      fontFamily: {
        sans: ['Inter var', ...fontFamily.sans],
      },
      colors: {
        border: 'oklch(var(--border))',
        input: 'oklch(var(--input))',
        ring: 'oklch(var(--ring))',
        background: 'oklch(var(--background))',
        foreground: 'oklch(var(--foreground))',
        primary: {
          DEFAULT: 'oklch(var(--primary))',
          foreground: 'oklch(var(--primary-foreground))',
        },
        secondary: {
          DEFAULT: 'oklch(var(--secondary))',
          foreground: 'oklch(var(--secondary-foreground))',
        },
        // ... more color definitions
      },
      keyframes: {
        'accordion-down': {
          from: { height: '0' },
          to: { height: 'var(--radix-accordion-content-height)' },
        },
        'accordion-up': {
          from: { height: 'var(--radix-accordion-content-height)' },
          to: { height: '0' },
        },
      },
      animation: {
        'accordion-down': 'accordion-down 0.2s ease-out',
        'accordion-up': 'accordion-up 0.2s ease-out',
      },
    },
  },
  plugins: [require('tailwindcss-animate')],
};

Deployment

Deploy with Vercel

bash
vercel

Docker

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
bash
docker build -t halolight-angular .
docker run -p 3000:80 halolight-angular

Other Platforms

CI/CD

The project is configured with a complete GitHub Actions CI workflow:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

Route Guards

ts
// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthStore } from '../stores/auth.store';

export const authGuard: CanActivateFn = (route, state) => {
  const authStore = inject(AuthStore);
  const router = inject(Router);

  if (!authStore.isAuthenticated()) {
    router.navigate(['/login'], { queryParams: { redirect: state.url } });
    return false;
  }

  return true;
};

// guards/permission.guard.ts
export const permissionGuard: CanActivateFn = (route) => {
  const authStore = inject(AuthStore);
  const router = inject(Router);
  const permission = route.data['permission'] as string;

  if (permission && !authStore.hasPermission(permission)) {
    router.navigate(['/403']);
    return false;
  }

  return true;
};

HTTP Interceptors

ts
// interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthStore } from '../stores/auth.store';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authStore = inject(AuthStore);
  const token = authStore.token();

  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
  }

  return next(req);
};

// interceptors/error.interceptor.ts
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);
  const authStore = inject(AuthStore);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        authStore.logout();
        router.navigate(['/login']);
      }
      return throwError(() => error);
    })
  );
};

Signals Computed Properties

ts
// stores/ui-settings.store.ts
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { computed } from '@angular/core';

interface UiSettingsState {
  theme: 'light' | 'dark' | 'system';
  skin: string;
  sidebarCollapsed: boolean;
}

const initialState: UiSettingsState = {
  theme: 'system',
  skin: 'default',
  sidebarCollapsed: false,
};

export const UiSettingsStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withComputed((store) => ({
    effectiveTheme: computed(() => {
      if (store.theme() === 'system') {
        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
      }
      return store.theme();
    }),
    isDarkMode: computed(() => store.effectiveTheme() === 'dark'),
  })),
  withMethods((store) => ({
    setTheme(theme: 'light' | 'dark' | 'system') {
      patchState(store, { theme });
      document.documentElement.classList.toggle('dark', store.isDarkMode());
    },
    setSkin(skin: string) {
      patchState(store, { skin });
      document.documentElement.setAttribute('data-theme', skin);
    },
    toggleSidebar() {
      patchState(store, { sidebarCollapsed: !store.sidebarCollapsed() });
    },
  }))
);

Performance Optimization

Image Optimization

ts
// Using NgOptimizedImage
import { NgOptimizedImage } from '@angular/common';

@Component({
  imports: [NgOptimizedImage],
  template: `
    <img
      ngSrc="proxy.php?url=https%3A%2F%2Fhalolight.docs.h7ml.cn%2Fassets%2Fimages%2Fhero.jpg"
      width="1200"
      height="600"
      priority
      alt="Hero image"
    />
  `,
})
export class HeroComponent {}

Lazy Loading Components

ts
// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./pages/admin/dashboard/dashboard.component')
      .then(m => m.DashboardComponent),
  },
  {
    path: 'users',
    loadChildren: () => import('./pages/admin/users/users.routes')
      .then(m => m.USERS_ROUTES),
  },
];

Preloading Strategy

ts
// app.config.ts
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules),
      withComponentInputBinding()
    ),
  ],
};

OnPush Change Detection

ts
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-user-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (user of users(); track user.id) {
      <app-user-card [user]="user" />
    }
  `,
})
export class UserListComponent {
  users = signal<User[]>([]);
}

FAQ

Q: How to configure Mock data?

A: Set useMock: true in environment.ts and define Mock data in the src/mocks directory:

ts
// mocks/users.mock.ts
import Mock from 'mockjs';

Mock.mock('/api/users', 'get', {
  'data|10-20': [{
    'id|+1': 1,
    'name': '@cname',
    'email': '@email',
    'avatar': '@image(100x100)',
    'role': '@pick(["admin", "user", "guest"])',
    'status': '@pick(["active", "inactive"])',
    'createdAt': '@datetime',
  }],
  total: '@integer(10, 100)',
});

Q: How to implement route permission control?

A: Use permissionGuard and specify the required permission in the route configuration:

ts
// app.routes.ts
{
  path: 'users',
  loadComponent: () => import('./pages/admin/users/users.component'),
  data: { permission: 'users:view' },
  canActivate: [authGuard, permissionGuard],
}

Q: How to customize theme colors?

A: Override CSS variables in styles.css:

css
:root {
  --primary: 51.1% 0.262 276.97; /* Custom primary color */
  --primary-foreground: 98% 0 0;
}

.dark {
  --primary: 56.1% 0.287 277.04;
  --primary-foreground: 98% 0 0;
}

Q: How to integrate third-party UI component libraries?

A: spartan/ui is already integrated. To add other components, extend with Angular CDK:

ts
import { CdkDrag, CdkDropList } from '@angular/cdk/drag-drop';

@Component({
  imports: [CdkDrag, CdkDropList],
  template: `
    <div cdkDropList (cdkDropListDropped)="drop($event)">
      @for (item of items(); track item.id) {
        <div cdkDrag>{{ item.name }}</div>
      }
    </div>
  `,
})
export class DraggableListComponent {}

Comparison with Other Versions

Feature Angular Version Next.js Vue
SSR/SSG ✅ Angular SSR ✅ (Nuxt)
State Management NgRx Signals Zustand Pinia
Routing Angular Router App Router Vue Router
Build Tool Angular CLI + esbuild Next.js Vite
Type Safety TypeScript (enforced) TypeScript TypeScript
Enterprise Support Google Vercel Community
]]>
<![CDATA[Bun Hono Backend API]]> https://halolight.docs.h7ml.cn/en/guide/api-bun https://halolight.docs.h7ml.cn/en/guide/api-bun Fri, 19 Dec 2025 04:56:41 GMT Bun Hono Backend API

HaloLight Bun Backend API is built on Bun + Hono + Drizzle ORM, providing ultra-high-performance backend service.

API Documentation: https://halolight-api-bun.h7ml.cn/docs

GitHub: https://github.com/halolight/halolight-api-bun

Features

  • 🔐 JWT Dual Token - Access Token + Refresh Token, automatic renewal
  • 🛡️ RBAC Authorization - Role-based access control with wildcard matching
  • 📡 RESTful API - Standardized interface design, OpenAPI documentation
  • 🗄️ Drizzle ORM - Type-safe database operations
  • Data Validation - Request parameter validation, error handling
  • 📊 Logging System - Request logging, error tracking
  • 🐳 Docker Support - Containerized deployment
  • Extreme Performance - 4x faster than Node.js

Tech Stack

Technology Version Description
Bun 1.1+ Runtime
Hono 4.x Web Framework
Drizzle ORM 0.36+ Database ORM
PostgreSQL 15+ Data Storage
Zod 3.x Data Validation
JWT - Authentication
Swagger - API Documentation

Quick Start

Requirements

  • Bun >= 1.1
  • pnpm >= 8.0
  • PostgreSQL (optional, defaults to SQLite)

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-api-bun.git
cd halolight-api-bun

# Install dependencies
pnpm install

Environment Variables

bash
cp .env.example .env
env
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/halolight

# JWT Secrets
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# Service Config
PORT=3002
NODE_ENV=development
CORS_ORIGIN=http://localhost:3000
API_PREFIX=/api

Database Initialization

bash
bun run db:push
bun run db:seed

Start Service

bash
# Development mode
bun run dev

# Production mode
bun run build
bun run start

Visit http://localhost:3002

Project Structure

halolight-api-bun/
├── src/
│   ├── routes/          # Controllers/Route handlers
│   ├── services/        # Business logic layer
│   ├── db/              # Data models
│   ├── middleware/      # Middleware
│   ├── utils/           # Utility functions
│   └── index.ts         # Application entry
├── test/                # Test files
├── Dockerfile           # Docker configuration
├── docker-compose.yml
└── package.json

API Modules

Authentication Endpoints

Method Path Description Auth
POST /api/auth/login User login Public
POST /api/auth/register User registration Public
POST /api/auth/refresh Refresh token Public
POST /api/auth/logout User logout Required
POST /api/auth/forgot-password Forgot password Public
POST /api/auth/reset-password Reset password Public

User Management Endpoints

Method Path Description Auth
GET /api/users Get user list users:view
GET /api/users/:id Get user details users:view
POST /api/users Create user users:create
PUT /api/users/:id Update user users:update
DELETE /api/users/:id Delete user users:delete
GET /api/users/me Get current user Required

Complete Endpoint List

Document Management (Documents) - 5 endpoints

Method Path Description
GET /api/documents Get document list
GET /api/documents/:id Get document details
POST /api/documents Create document
PUT /api/documents/:id Update document
DELETE /api/documents/:id Delete document

File Management (Files) - 5 endpoints

Method Path Description
GET /api/files Get file list
GET /api/files/:id Get file details
POST /api/files/upload Upload file
PUT /api/files/:id Update file info
DELETE /api/files/:id Delete file

Message Management (Messages) - 5 endpoints

Method Path Description
GET /api/messages Get message list
GET /api/messages/:id Get message details
POST /api/messages Send message
PUT /api/messages/:id/read Mark as read
DELETE /api/messages/:id Delete message

Notification Management (Notifications) - 4 endpoints

Method Path Description
GET /api/notifications Get notification list
PUT /api/notifications/:id/read Mark as read
PUT /api/notifications/read-all Mark all as read
DELETE /api/notifications/:id Delete notification

Calendar Management (Calendar) - 5 endpoints

Method Path Description
GET /api/calendar/events Get event list
GET /api/calendar/events/:id Get event details
POST /api/calendar/events Create event
PUT /api/calendar/events/:id Update event
DELETE /api/calendar/events/:id Delete event

Dashboard (Dashboard) - 6 endpoints

Method Path Description
GET /api/dashboard/stats Statistics data
GET /api/dashboard/visits Visit trends
GET /api/dashboard/sales Sales data
GET /api/dashboard/pie Pie chart data
GET /api/dashboard/tasks Pending tasks
GET /api/dashboard/calendar Today's events

Authentication Mechanism

JWT Dual Token

Access Token:  15 minutes validity, for API requests
Refresh Token: 7 days validity, for refreshing Access Token

Request Header

http
Authorization: Bearer <access_token>

Refresh Flow

typescript
// Refresh token example
const response = await fetch('/api/auth/refresh', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    refreshToken: 'your_refresh_token'
  })
});

const { accessToken, refreshToken } = await response.json();

Authorization System

Role Definitions

Role Description Permissions
super_admin Super Administrator * (all permissions)
admin Administrator users:*, documents:*, ...
user Regular User documents:view, files:view, ...
guest Guest dashboard:view

Permission Format

{resource}:{action}

Examples:
- users:view      # View users
- users:create    # Create users
- users:*         # All user operations
- *               # All permissions

Error Handling

Error Response Format

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" }
    ]
  }
}

Error Codes

Status Error Code Description
400 VALIDATION_ERROR Validation failed
401 UNAUTHORIZED Unauthorized
403 FORBIDDEN Forbidden
404 NOT_FOUND Resource not found
409 CONFLICT Resource conflict
500 INTERNAL_ERROR Server error

Common Commands

bash
# Development
bun run dev                 # Start development server
bun run build               # Production build
bun run start               # Run production build

# Build
bun run build               # Build production version

# Testing
bun test                    # Run unit tests
bun test --coverage         # Generate coverage report

# Database
bun run db:push             # Push schema to database
bun run db:generate         # Generate migration files
bun run db:migrate          # Run database migrations
bun run db:seed             # Seed test data
bun run db:studio           # Open Drizzle Studio

# Code Quality
bun run lint                # ESLint check
bun run lint:fix            # ESLint auto-fix
bun run type-check          # TypeScript type check

Deployment

Docker

bash
docker build -t halolight-api-bun .
docker run -p 3002:3002 halolight-api-bun

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3002:3002"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Production Configuration

env
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=your-production-secret

Testing

Running Tests

bash
bun test                    # Run all tests
bun test --coverage         # Generate coverage report

Test Examples

typescript
// Authentication test example
import { describe, test, expect } from 'bun:test';

describe('Auth API', () => {
  test('should login successfully', async () => {
    const response = await fetch('http://localhost:3002/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: '[email protected]',
        password: 'admin123'
      })
    });

    const data = await response.json();
    expect(data.success).toBe(true);
    expect(data.data.accessToken).toBeDefined();
  });
});

Performance Metrics

Benchmarks

Metric Value Condition
Request Throughput ~50,000 req/s Single core, simple route
Average Response Time <5ms Local database
Memory Usage ~30MB Cold start
CPU Usage <10% Idle state

Observability

Logging System

typescript
// Logging configuration example
import { logger } from './utils/logger';

logger.info('User logged in', { userId: user.id });
logger.error('Database error', { error: err.message });

Health Check

typescript
// GET /health
app.get('/health', (c) => {
  return c.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

Monitoring Metrics

typescript
// Prometheus metrics endpoint
app.get('/metrics', async (c) => {
  return c.text(await register.metrics());
});

FAQ

Q: How to configure database connection?

A: Set DATABASE_URL in .env file:

env
DATABASE_URL=postgresql://user:password@localhost:5432/halolight

Q: How to use Bun's built-in password hashing?

A: Use Bun.password API:

typescript
// Hash password
const hash = await Bun.password.hash(password, {
  algorithm: 'bcrypt',
  cost: 10
});

// Verify password
const isValid = await Bun.password.verify(password, hash, 'bcrypt');

Development Tools

  • Drizzle Studio - Visual database management tool
  • Hoppscotch/Postman - API testing tools
  • ESLint + Prettier - Code formatting
  • Bun VSCode Extension - Bun syntax support

Comparison with Other Backends

Feature Bun + Hono NestJS FastAPI Spring Boot
Language TypeScript TypeScript Python Java
ORM Drizzle Prisma SQLAlchemy JPA
Performance ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Learning Curve ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
]]>
<![CDATA[Go Fiber Backend API]]> https://halolight.docs.h7ml.cn/en/guide/api-go https://halolight.docs.h7ml.cn/en/guide/api-go Fri, 19 Dec 2025 04:56:41 GMT Go Fiber Backend API

HaloLight Go Fiber backend API is built on Fiber 3.0, providing high-performance Go backend service with complete JWT dual-token authentication.

API Documentation: https://halolight-api-go.h7ml.cn/docs

GitHub: https://github.com/halolight/halolight-api-go

Features

  • 🔐 JWT Dual Tokens - Access Token + Refresh Token with auto-renewal
  • 🛡️ RBAC Permissions - Role-based access control with wildcard matching
  • 📡 RESTful API - Standardized interface design with OpenAPI documentation
  • 🗄️ GORM 2 - Type-safe database operations
  • Data Validation - Request parameter validation and error handling
  • 📊 Logging System - Request logging and error tracking
  • 🐳 Docker Support - Containerized deployment

Tech Stack

Technology Version Description
Go 1.22+ Runtime
Fiber 3.0 Web Framework
GORM 2.0 Database ORM
PostgreSQL 16 Data Storage
go-playground/validator v10 Data Validation
JWT golang-jwt/jwt/v5 Authentication
Swagger UI - API Documentation

Quick Start

Environment Requirements

  • Go >= 1.22
  • PostgreSQL 16 (optional, defaults to SQLite)

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-api-go.git
cd halolight-api-go

# Install dependencies
go mod download

Environment Variables

bash
cp .env.example .env
env
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/halolight?sslmode=disable

# JWT Secret
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=7d
JWT_REFRESH_EXPIRES=30d

# Service Configuration
PORT=8080
APP_ENV=development

Database Initialization

bash
# GORM auto-migration
go run cmd/server/main.go

# Or use Makefile
make migrate

Start Service

bash
# Development mode
go run cmd/server/main.go

# Production mode
make build
./bin/server

Visit http://localhost:8080

Project Structure

halolight-api-go/
├── cmd/
│   └── server/
│       └── main.go              # Application entry
├── internal/
│   ├── handlers/                # Controllers/route handlers
│   │   ├── auth_handler.go      # Authentication endpoints
│   │   ├── user_handler.go      # User management
│   │   ├── role_handler.go      # Role management
│   │   ├── permission_handler.go # Permission management
│   │   ├── team_handler.go      # Team management
│   │   ├── document_handler.go  # Document management
│   │   ├── file_handler.go      # File management
│   │   ├── folder_handler.go    # Folder management
│   │   ├── calendar_handler.go  # Calendar events
│   │   ├── notification_handler.go # Notification management
│   │   ├── message_handler.go   # Message management
│   │   ├── dashboard_handler.go # Dashboard statistics
│   │   └── home_handler.go      # Homepage + health check
│   ├── services/                # Business logic layer
│   │   ├── auth_service.go
│   │   ├── user_service.go
│   │   └── ...
│   ├── models/                  # Data models
│   │   ├── user.go
│   │   ├── role.go
│   │   └── ...
│   ├── middleware/              # Middleware
│   │   ├── auth.go
│   │   └── cors.go
│   └── routes/                  # Route configuration
│       └── router.go
├── pkg/
│   ├── config/                  # Configuration management
│   ├── database/                # Database connection
│   └── utils/                   # Utility functions
├── docs/                        # Documentation
├── .github/workflows/           # GitHub Actions
├── Dockerfile                   # Docker configuration
├── docker-compose.yml
└── go.mod

API Modules

Authentication Endpoints

Method Path Description Permission
POST /api/auth/login User login Public
POST /api/auth/register User registration Public
POST /api/auth/refresh Refresh token Public
POST /api/auth/logout Logout Authenticated
POST /api/auth/forgot-password Forgot password Public
POST /api/auth/reset-password Reset password Public
GET /api/auth/me Get current user Authenticated

User Management Endpoints

Method Path Description Permission
GET /api/users List users users:view
GET /api/users/:id Get user details users:view
POST /api/users Create user users:create
PUT /api/users/:id Update user users:update
DELETE /api/users/:id Delete user users:delete
PATCH /api/users/:id/status Update user status users:update
POST /api/users/batch-delete Batch delete users users:delete

Complete Endpoint List

Role Management (Roles) - 6 endpoints

Method Path Description
GET /api/roles List roles
GET /api/roles/:id Get role details
POST /api/roles Create role
PUT /api/roles/:id Update role
POST /api/roles/:id/permissions Assign permissions
DELETE /api/roles/:id Delete role

Permission Management (Permissions) - 4 endpoints

Method Path Description
GET /api/permissions List permissions
GET /api/permissions/:id Get permission details
POST /api/permissions Create permission
DELETE /api/permissions/:id Delete permission

Team Management (Teams) - 7 endpoints

Method Path Description
GET /api/teams List teams
GET /api/teams/:id Get team details
POST /api/teams Create team
PATCH /api/teams/:id Update team
DELETE /api/teams/:id Delete team
POST /api/teams/:id/members Add member
DELETE /api/teams/:id/members/:userId Remove member

Document Management (Documents) - 11 endpoints

Method Path Description
GET /api/documents List documents
GET /api/documents/:id Get document details
POST /api/documents Create document
PUT /api/documents/:id Update document
PATCH /api/documents/:id/rename Rename document
POST /api/documents/:id/move Move document
POST /api/documents/:id/tags Update tags
POST /api/documents/:id/share Share document
POST /api/documents/:id/unshare Unshare document
POST /api/documents/batch-delete Batch delete
DELETE /api/documents/:id Delete document

File Management (Files) - 14 endpoints

Method Path Description
POST /api/files/upload Upload file
POST /api/files/folder Create folder
GET /api/files List files
GET /api/files/storage Get storage info
GET /api/files/:id Get file details
GET /api/files/:id/download-url Get download URL
PATCH /api/files/:id/rename Rename file
POST /api/files/:id/move Move file
POST /api/files/:id/copy Copy file
PATCH /api/files/:id/favorite Toggle favorite
POST /api/files/:id/share Share file
POST /api/files/batch-delete Batch delete
DELETE /api/files/:id Delete file

Folder Management (Folders) - 5 endpoints

Method Path Description
GET /api/folders List folders
GET /api/folders/tree Get folder tree
GET /api/folders/:id Get folder details
POST /api/folders Create folder
DELETE /api/folders/:id Delete folder

Message Management (Messages) - 5 endpoints

Method Path Description
GET /api/messages/conversations List conversations
GET /api/messages/conversations/:id Get conversation details
POST /api/messages Send message
PUT /api/messages/:id/read Mark as read
DELETE /api/messages/:id Delete message

Notification Management (Notifications) - 5 endpoints

Method Path Description
GET /api/notifications List notifications
GET /api/notifications/unread-count Get unread count
PUT /api/notifications/:id/read Mark as read
PUT /api/notifications/read-all Mark all as read
DELETE /api/notifications/:id Delete notification

Calendar Management (Calendar) - 9 endpoints

Method Path Description
GET /api/calendar/events List events
GET /api/calendar/events/:id Get event details
POST /api/calendar/events Create event
PUT /api/calendar/events/:id Update event
PATCH /api/calendar/events/:id/reschedule Reschedule event
POST /api/calendar/events/:id/attendees Add attendee
DELETE /api/calendar/events/:id/attendees/:attendeeId Remove attendee
POST /api/calendar/events/batch-delete Batch delete
DELETE /api/calendar/events/:id Delete event

Dashboard (Dashboard) - 9 endpoints

Method Path Description
GET /api/dashboard/stats Get statistics
GET /api/dashboard/visits Get visit data
GET /api/dashboard/sales Get sales data
GET /api/dashboard/products Get product data
GET /api/dashboard/orders Get order data
GET /api/dashboard/activities Get activity data
GET /api/dashboard/pie Get pie chart data
GET /api/dashboard/tasks Get task data
GET /api/dashboard/overview Get overview data

Authentication Mechanism

JWT Dual Tokens

Access Token:  7 days validity, used for API requests
Refresh Token: 30 days validity, used to refresh Access Token

Request Headers

http
Authorization: Bearer <access_token>

Refresh Flow

go
// Token refresh example
func RefreshToken(c *fiber.Ctx) error {
    type RefreshRequest struct {
        RefreshToken string `json:"refreshToken"`
    }

    var req RefreshRequest
    if err := c.BodyParser(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "message": "Invalid request",
        })
    }

    // Validate refresh token
    claims, err := utils.ValidateToken(req.RefreshToken)
    if err != nil {
        return c.Status(401).JSON(fiber.Map{
            "success": false,
            "message": "Invalid refresh token",
        })
    }

    // Generate new access token
    accessToken, err := utils.GenerateAccessToken(claims.UserID)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "success": false,
            "message": "Failed to generate token",
        })
    }

    return c.JSON(fiber.Map{
        "success": true,
        "accessToken": accessToken,
    })
}

Permission System

Role Definitions

Role Description Permissions
super_admin Super Administrator * (all permissions)
admin Administrator users:*, documents:*, files:*, teams:*
user Regular User documents:view, documents:create, files:*
guest Guest dashboard:view

Permission Format

{resource}:{action}

Examples:
- users:view      # View users
- users:create    # Create users
- users:*         # All user operations
- *               # All permissions

Error Handling

Error Response Format

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request parameter validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" }
    ]
  }
}

Error Codes

Status Code Error Code Description
400 VALIDATION_ERROR Parameter validation failed
401 UNAUTHORIZED Unauthorized
403 FORBIDDEN Forbidden
404 NOT_FOUND Resource not found
409 CONFLICT Resource conflict
500 INTERNAL_ERROR Internal server error

Common Commands

bash
# Development
go run cmd/server/main.go

# Build
go build -o bin/server cmd/server/main.go

# Testing
go test ./...

# Database
make migrate

# Code Quality
go vet ./...
golangci-lint run

Deployment

Docker

bash
docker build -t halolight-api-go .
docker run -p 8080:8080 halolight-api-go

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - APP_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Production Configuration

env
APP_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=your-production-secret

Testing

Run Tests

bash
# Unit tests
go test ./...

# Test coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

Test Example

go
func TestUserLogin(t *testing.T) {
    app := fiber.New()

    // Setup routes
    app.Post("/api/auth/login", handlers.Login)

    // Prepare test data
    reqBody := `{"email":"[email protected]","password":"password123"}`
    req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(reqBody))
    req.Header.Set("Content-Type", "application/json")

    // Execute request
    resp, _ := app.Test(req)

    // Verify response
    assert.Equal(t, 200, resp.StatusCode)
}

Performance Metrics

Benchmarks

Metric Value Conditions
Request Throughput 10,000+ QPS Single machine, 8-core CPU
Average Response Time < 10ms Simple queries
Memory Usage ~50MB Idle state
CPU Usage < 10% Idle state

Observability

Logging System

go
// Logging configuration
logger := log.New(os.Stdout, "API: ", log.LstdFlags)

app.Use(func(c *fiber.Ctx) error {
    start := time.Now()
    err := c.Next()
    logger.Printf("%s %s %s %v",
        c.Method(),
        c.Path(),
        c.IP(),
        time.Since(start),
    )
    return err
})

Health Check

go
// Health check endpoint
app.Get("/health", func(c *fiber.Ctx) error {
    return c.JSON(fiber.Map{
        "status": "healthy",
        "timestamp": time.Now(),
        "database": db.Ping() == nil,
    })
})

Monitoring Metrics

go
// Prometheus metrics
import "github.com/prometheus/client_golang/prometheus"

var (
    requestCounter = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "api_requests_total",
            Help: "Total API requests",
        },
        []string{"method", "path", "status"},
    )
)

FAQ

Q: JWT secret key length requirements?

A: JWT secret must be at least 32 characters. Recommend using 64+ character random strings.

bash
# Generate secure key
openssl rand -base64 64

Q: Database connection failed?

A: Check database configuration and network connection.

bash
# Check PostgreSQL status
docker-compose ps postgres

# Test connection
psql -h localhost -U postgres -d halolight

Development Tools

  • Air - Go hot reload tool
  • golangci-lint - Go code linter
  • goose - Database migration tool
  • mockery - Mock generation tool

Comparison with Other Backends

Feature Go Fiber NestJS FastAPI Spring Boot
Language Go TypeScript Python Java
ORM GORM Prisma SQLAlchemy JPA
Performance ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Learning Curve ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
]]>
<![CDATA[Java Spring Boot Backend API]]> https://halolight.docs.h7ml.cn/en/guide/api-java https://halolight.docs.h7ml.cn/en/guide/api-java Fri, 19 Dec 2025 04:56:41 GMT Java Spring Boot Backend API

HaloLight Spring Boot backend API is built on Spring Boot 3.4.1, providing enterprise-grade backend service with complete JWT dual-token authentication.

API Documentation: https://halolight-api-java.h7ml.cn/api/swagger-ui

GitHub: https://github.com/halolight/halolight-api-java

Features

  • 🔐 JWT Dual-Token - Access Token + Refresh Token, automatic renewal
  • 🛡️ RBAC Permissions - Role-based access control, wildcard matching
  • 📡 RESTful API - Standardized interface design, OpenAPI documentation
  • 🗄️ Spring Data JPA - Type-safe database operations
  • Data Validation - Bean Validation request parameter validation
  • 📊 Logging System - Request logs, error tracking
  • 🐳 Docker Support - Multi-stage build, containerized deployment

Tech Stack

Technology Version Description
Java 23 Runtime
Spring Boot 3.4.1 Web Framework
Spring Data JPA 3.4.1 Database ORM
PostgreSQL 16 Data Storage
Bean Validation jakarta.validation Data Validation
JWT JJWT Authentication
Springdoc OpenAPI 2.7.0 API Documentation

Quick Start

Requirements

  • Java >= 17
  • Maven >= 3.9
  • PostgreSQL 16 (optional, defaults to H2)

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-api-java.git
cd halolight-api-java

# Install dependencies
./mvnw clean install

Environment Variables

bash
cp .env.example .env
env
# Database
DATABASE_URL=jdbc:postgresql://localhost:5432/halolight_db
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=your-password

# JWT Secret
JWT_SECRET=your-super-secret-jwt-key-change-in-production-min-32-chars
JWT_EXPIRATION=86400000
JWT_REFRESH_EXPIRATION=604800000

# Service Configuration
PORT=8080
SPRING_PROFILES_ACTIVE=production

Database Initialization

bash
# Auto-create tables (first run)
./mvnw spring-boot:run

# Run seed data (optional)
./mvnw exec:java -Dexec.mainClass="com.halolight.seed.DataSeeder"

Start Service

bash
# Development mode
./mvnw spring-boot:run

# Production mode
./mvnw clean package -DskipTests
java -jar target/halolight-api-java-1.0.0.jar

Visit http://localhost:8080

Directory Structure

halolight-api-java/
├── src/main/java/com/halolight/
│   ├── controller/              # Controllers/Route handlers
│   │   ├── AuthController.java
│   │   ├── UserController.java
│   │   └── ...
│   ├── service/                 # Business logic layer
│   │   ├── AuthService.java
│   │   └── ...
│   ├── domain/                  # Data models
│   │   ├── entity/              # JPA Entities
│   │   └── repository/          # Repository interfaces
│   ├── config/                  # Middleware/Configuration
│   │   ├── SecurityConfig.java
│   │   └── ...
│   ├── web/dto/                 # Request validation DTOs
│   ├── security/                # Security components
│   └── HalolightApplication.java  # Application entry
├── src/main/resources/          # Resource files
│   ├── application.yml
│   └── application-*.yml
├── src/test/                    # Test files
├── Dockerfile                   # Docker configuration
├── docker-compose.yml
└── pom.xml                      # Maven configuration

API Modules

Authentication Endpoints

Method Path Description Permission
POST /api/auth/login User login Public
POST /api/auth/register User registration Public
POST /api/auth/refresh Refresh token Public
POST /api/auth/logout Logout Authenticated
POST /api/auth/forgot-password Forgot password Public
POST /api/auth/reset-password Reset password Public
GET /api/auth/me Get current user Authenticated

User Management Endpoints

Method Path Description Permission
GET /api/users Get user list users:view
GET /api/users/{id} Get user details users:view
POST /api/users Create user users:create
PUT /api/users/{id} Update user users:update
PUT /api/users/{id}/status Update user status users:update
DELETE /api/users/{id} Delete user users:delete

Complete Endpoint List

Role Management (Roles) - 6 Endpoints

Method Path Description
GET /api/roles Get role list
GET /api/roles/{id} Get role details
POST /api/roles Create role
PUT /api/roles/{id} Update role
POST /api/roles/{id}/permissions Assign permissions
DELETE /api/roles/{id} Delete role

Permission Management (Permissions) - 4 Endpoints

Method Path Description
GET /api/permissions Get permission list
POST /api/permissions Create permission
PUT /api/permissions/{id} Update permission
DELETE /api/permissions/{id} Delete permission

Document Management (Documents) - 10 Endpoints

Method Path Description
GET /api/documents Get document list
GET /api/documents/{id} Get document details
POST /api/documents Create document
PUT /api/documents/{id} Update document
PUT /api/documents/{id}/rename Rename document
POST /api/documents/{id}/move Move document
POST /api/documents/{id}/tags Update tags
POST /api/documents/{id}/share Share document
POST /api/documents/{id}/unshare Unshare document
DELETE /api/documents/{id} Delete document

File Management (Files) - 10 Endpoints

Method Path Description
POST /api/files/upload Upload file
GET /api/files Get file list
GET /api/files/storage Get storage quota
GET /api/files/{id} Get file details
GET /api/files/{id}/download Download file
PUT /api/files/{id}/rename Rename file
POST /api/files/{id}/move Move file
PUT /api/files/{id}/favorite Toggle favorite
POST /api/files/{id}/share Share file
DELETE /api/files/{id} Delete file

Team Management (Teams) - 6 Endpoints

Method Path Description
GET /api/teams Get team list
GET /api/teams/{id} Get team details
POST /api/teams Create team
PUT /api/teams/{id} Update team
POST /api/teams/{id}/members Add member
DELETE /api/teams/{id}/members/{userId} Remove member

Message Management (Messages) - 5 Endpoints

Method Path Description
GET /api/messages/conversations Get conversation list
GET /api/messages/conversations/{userId} Get conversation messages
POST /api/messages Send message
PUT /api/messages/{id}/read Mark as read
DELETE /api/messages/{id} Delete message

Notification Management (Notifications) - 5 Endpoints

Method Path Description
GET /api/notifications Get notification list
GET /api/notifications/unread-count Get unread count
PUT /api/notifications/{id}/read Mark single as read
PUT /api/notifications/read-all Mark all as read
DELETE /api/notifications/{id} Delete notification

Calendar Management (Calendar) - 8 Endpoints

Method Path Description
GET /api/calendar/events Get event list
GET /api/calendar/events/{id} Get event details
POST /api/calendar/events Create event
PUT /api/calendar/events/{id} Update event
PUT /api/calendar/events/{id}/reschedule Reschedule
POST /api/calendar/events/{id}/attendees Add attendee
DELETE /api/calendar/events/{id}/attendees/{attendeeId} Remove attendee
DELETE /api/calendar/events/{id} Delete event

Dashboard (Dashboard) - 5 Endpoints

Method Path Description
GET /api/dashboard/stats Statistics data
GET /api/dashboard/visits Visit trends
GET /api/dashboard/sales Sales data
GET /api/dashboard/pie Pie chart data
GET /api/dashboard/tasks Pending tasks

Authentication Mechanism

JWT Dual-Token

Access Token:  24-hour validity, used for API requests
Refresh Token: 7-day validity, used to refresh Access Token

Request Header

http
Authorization: Bearer <access_token>

Refresh Flow

java
// Frontend auto-refresh example
@Component
public class JwtTokenInterceptor {
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);

        // 401 auto refresh
        if (response.code() == 401) {
            String newToken = refreshToken(refreshToken);
            Request newRequest = request.newBuilder()
                .header("Authorization", "Bearer " + newToken)
                .build();
            return chain.proceed(newRequest);
        }

        return response;
    }
}

Permission System

Role Definitions

Role Description Permissions
super_admin Super Administrator * (all permissions)
admin Administrator users:*, documents:*, roles:*
user Regular User documents:view, files:view
guest Guest dashboard:view

Permission Format

{resource}:{action}

Examples:
- users:view      # View users
- users:create    # Create users
- users:*         # All user operations
- *               # All permissions

Permission Check

java
@RestController
@RequestMapping("/api/users")
public class UserController {

    @PreAuthorize("hasPermission('users:view')")
    @GetMapping
    public Page<UserDTO> getUsers(Pageable pageable) {
        return userService.findAll(pageable);
    }

    @PreAuthorize("hasPermission('users:create')")
    @PostMapping
    public UserDTO createUser(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }
}

Error Handling

Error Response Format

json
{
  "timestamp": "2025-12-04T12:00:00.000Z",
  "status": 400,
  "error": "Bad Request",
  "message": "Validation failed",
  "path": "/api/users",
  "details": [
    { "field": "email", "message": "must be a valid email address" }
  ]
}

Error Codes

Status Code Error Code Description
400 Bad Request Parameter validation failed
401 Unauthorized Unauthorized
403 Forbidden No permission
404 Not Found Resource not found
409 Conflict Resource conflict
422 Unprocessable Entity Business logic error
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server error

Database Models

Spring Data JPA entities include 17 models:

java
// User Entity
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String name;

    private String password;
    private String avatar;

    @Enumerated(EnumType.STRING)
    private UserStatus status = UserStatus.ACTIVE;

    @ManyToMany
    @JoinTable(name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles;

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

// Role Entity
@Entity
@Table(name = "roles")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String name;

    private String description;

    @ManyToMany
    @JoinTable(name = "role_permissions")
    private Set<Permission> permissions;
}

// Permission Entity
@Entity
@Table(name = "permissions")
public class Permission {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String name;  // Format: "users:create", "users:*", "*"

    private String description;
}

Complete Entity List:

  • User, Role, Permission (RBAC core)
  • Team, TeamMember (team management)
  • Document, File, Folder (document/file)
  • CalendarEvent, EventAttendee (calendar)
  • Notification, Message, Conversation (notification/message)
  • Dashboard, Visit, Sale (dashboard statistics)

Environment Variables

Variable Name Description Default Value
SPRING_PROFILES_ACTIVE Runtime environment development
PORT Service port 8080
DATABASE_URL Database connection jdbc:postgresql://localhost:5432/halolight_db
DATABASE_USERNAME Database username postgres
DATABASE_PASSWORD Database password -
JWT_SECRET JWT secret (min 32 chars) -
JWT_EXPIRATION AccessToken expiration (ms) 86400000 (24h)
JWT_REFRESH_EXPIRATION RefreshToken expiration (ms) 604800000 (7d)
CORS_ALLOWED_ORIGINS CORS allowed origins http://localhost:3000

Usage

yaml
# application.yml
spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}

jwt:
  secret: ${JWT_SECRET}
  expiration: ${JWT_EXPIRATION:86400000}
  refreshExpiration: ${JWT_REFRESH_EXPIRATION:604800000}

Common Commands

bash
# Development
./mvnw spring-boot:run                # Start development server
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev  # Specify environment

# Build
./mvnw clean package                  # Build JAR
./mvnw clean package -DskipTests      # Build without tests
./mvnw clean install                  # Install to local repository

# Testing
./mvnw test                           # Run all tests
./mvnw test -Dtest=UserServiceTest    # Run specific test
./mvnw verify                         # Run integration tests
./mvnw test jacoco:report             # Generate coverage report

# Database
./mvnw flyway:migrate                 # Run migrations (if using Flyway)
./mvnw liquibase:update               # Update schema (if using Liquibase)

# Code Quality
./mvnw checkstyle:check               # Code style check
./mvnw spotbugs:check                 # Static analysis

Deployment

Docker

bash
docker build -t halolight-api-java .
docker run -p 8080:8080 --env-file .env halolight-api-java

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=production
      - DATABASE_URL=jdbc:postgresql://db:5432/halolight
      - DATABASE_USERNAME=postgres
      - DATABASE_PASSWORD=${DB_PASSWORD}
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

Production Environment Configuration

env
SPRING_PROFILES_ACTIVE=production
DATABASE_URL=jdbc:postgresql://prod-db.example.com:5432/halolight
DATABASE_USERNAME=halolight_user
DATABASE_PASSWORD=your-production-password
JWT_SECRET=your-production-secret-min-32-chars
CORS_ALLOWED_ORIGINS=https://halolight.h7ml.cn

Testing

Run Tests

bash
./mvnw test                           # Run unit tests
./mvnw test jacoco:report             # Generate coverage report
./mvnw verify                         # Run integration tests

Test Example

java
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void testLogin() throws Exception {
        LoginRequest request = new LoginRequest();
        request.setEmail("[email protected]");
        request.setPassword("123456");

        mockMvc.perform(post("/api/auth/login")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.accessToken").exists())
                .andExpect(jsonPath("$.refreshToken").exists());
    }

    @Test
    @WithMockUser(authorities = {"users:view"})
    public void testGetUsers() throws Exception {
        mockMvc.perform(get("/api/users")
                .param("page", "0")
                .param("size", "10"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content").isArray());
    }
}

Performance Metrics

Benchmark

Metric Value Description
Request Throughput ~3000 QPS Simple queries, 4 cores 8GB
Average Response Time 15-30ms P50, database queries
P95 Response Time 50-100ms Including complex queries
Memory Usage 256-512 MB Stable running state
CPU Usage 10-30% Medium load

Performance Testing

bash
# Using Apache Bench
ab -n 10000 -c 100 -H "Authorization: Bearer TOKEN" \
  http://localhost:8080/api/users

# Using wrk
wrk -t4 -c100 -d30s -H "Authorization: Bearer TOKEN" \
  http://localhost:8080/api/users

Observability

Logging System

java
// Logback configuration
@Slf4j
@RestController
public class UserController {

    @GetMapping("/api/users/{id}")
    public UserDTO getUser(@PathVariable Long id) {
        log.info("Fetching user with id: {}", id);
        try {
            return userService.findById(id);
        } catch (Exception e) {
            log.error("Error fetching user {}: {}", id, e.getMessage(), e);
            throw e;
        }
    }
}

Health Check

java
@Component
public class CustomHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        // Check database connection
        boolean dbUp = checkDatabase();

        if (dbUp) {
            return Health.up()
                .withDetail("database", "Available")
                .build();
        }

        return Health.down()
            .withDetail("database", "Unavailable")
            .build();
    }
}

Endpoint: GET /actuator/health

json
{
  "status": "UP",
  "components": {
    "db": { "status": "UP" },
    "diskSpace": { "status": "UP" }
  }
}

Monitoring Metrics

yaml
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

Prometheus Endpoint: GET /actuator/prometheus

FAQ

Q: How to modify JWT expiration time?

A: Configure in .env or application.yml:

env
JWT_EXPIRATION=3600000          # 1 hour (milliseconds)
JWT_REFRESH_EXPIRATION=86400000  # 1 day (milliseconds)

Q: How to enable HTTPS?

A: Generate certificate and configure Spring Boot:

yaml
# application.yml
server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: your-password
    key-store-type: PKCS12
bash
# Generate self-signed certificate (development)
keytool -genkeypair -alias halolight -keyalg RSA -keysize 2048 \
  -storetype PKCS12 -keystore keystore.p12 -validity 365

Q: How to configure database connection pool?

A: Use HikariCP (Spring Boot default):

yaml
spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

Q: How to implement pagination and sorting?

A: Use Spring Data JPA Pageable:

java
@GetMapping("/api/users")
public Page<UserDTO> getUsers(
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "10") int size,
    @RequestParam(defaultValue = "id,desc") String sort
) {
    String[] sortParams = sort.split(",");
    Sort.Direction direction = sortParams.length > 1 &&
        sortParams[1].equals("desc") ? Sort.Direction.DESC : Sort.Direction.ASC;

    Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortParams[0]));
    return userService.findAll(pageable);
}

Development Tools

  • IntelliJ IDEA - Official recommended IDE with Spring Boot support
  • Spring Boot DevTools - Hot reload, automatic restart
  • Lombok - Reduce boilerplate code
  • MapStruct - DTO mapping generation
  • JaCoCo - Code coverage tool
  • Postman/Insomnia - API testing tools

Comparison with Other Backends

Feature Spring Boot NestJS FastAPI Go Fiber
Language Java TypeScript Python Go
ORM JPA/Hibernate Prisma SQLAlchemy GORM
Performance ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Learning Curve ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Enterprise-Grade ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Ecosystem ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Community Support ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
]]>
<![CDATA[TypeScript NestJS Backend API]]> https://halolight.docs.h7ml.cn/en/guide/api-nestjs https://halolight.docs.h7ml.cn/en/guide/api-nestjs Fri, 19 Dec 2025 04:56:41 GMT TypeScript NestJS Backend API

HaloLight NestJS backend API is built on NestJS 11, providing enterprise-grade backend services with complete RBAC permission system.

API Documentation: https://halolight-api-nestjs.h7ml.cn/docs

GitHub: https://github.com/halolight/halolight-api-nestjs

Features

  • 🔐 JWT Dual Tokens - Access Token + Refresh Token with auto-renewal
  • 🛡️ RBAC Permissions - Role-Based Access Control with wildcard matching
  • 📡 RESTful API - Standardized interface design with OpenAPI documentation
  • 🗄️ Prisma ORM - Type-safe database operations
  • Data Validation - Request parameter validation and error handling
  • 📊 Logging System - Request logs and error tracking
  • 🐳 Docker Support - Containerized deployment

Tech Stack

Technology Version Description
TypeScript 5.7 Runtime
NestJS 11 Web Framework
Prisma 5 Database ORM
PostgreSQL 16 Data Storage
class-validator - Data Validation
JWT - Authentication
Swagger - API Documentation

Quick Start

Requirements

  • Node.js >= 18
  • pnpm >= 8
  • PostgreSQL (optional, defaults to SQLite)

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-api-nestjs.git
cd halolight-api-nestjs

# Install dependencies
pnpm install

Environment Variables

bash
cp .env.example .env
env
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/halolight_db

# JWT Keys
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# Service Configuration
PORT=3000
NODE_ENV=development

Database Initialization

bash
pnpm prisma:generate
pnpm prisma:migrate
pnpm prisma:seed

Start Service

bash
# Development mode
pnpm dev

# Production mode
pnpm build
pnpm start:prod

Visit http://localhost:3000

Project Structure

halolight-api-nestjs/
├── src/
│   ├── common/                  # Shared modules
│   ├── configs/                 # Configuration modules
│   ├── infrastructure/          # Infrastructure layer
│   ├── modules/                 # Business modules (12 modules)
│   │   ├── auth/                # Authentication
│   │   ├── users/               # User management
│   │   ├── roles/               # Role management
│   │   ├── permissions/         # Permission management
│   │   ├── teams/               # Team management
│   │   ├── documents/           # Document management
│   │   ├── files/               # File management
│   │   ├── folders/             # Folder management
│   │   ├── calendar/            # Calendar management
│   │   ├── notifications/       # Notification management
│   │   ├── messages/            # Message management
│   │   └── dashboard/           # Dashboard statistics
│   ├── app.controller.ts        # Root controller
│   ├── app.service.ts           # Root service
│   ├── app.module.ts            # Root module
│   └── main.ts                  # Application entry
├── prisma/
│   ├── schema.prisma            # Database model definitions
│   └── migrations/              # Database migration history
├── test/                        # Test files
├── Dockerfile                   # Docker configuration
├── docker-compose.yml
└── package.json

API Modules

Authentication Endpoints

Method Path Description Permission
POST /api/auth/login User login Public
POST /api/auth/register User registration Public
POST /api/auth/refresh Refresh token Public
POST /api/auth/logout User logout Required
POST /api/auth/forgot-password Forgot password Public
POST /api/auth/reset-password Reset password Public

User Management Endpoints

Method Path Description Permission
GET /api/users Get user list users:view
GET /api/users/:id Get user details users:view
POST /api/users Create user users:create
PUT /api/users/:id Update user users:update
DELETE /api/users/:id Delete user users:delete
GET /api/users/me Get current user Required

Complete Endpoint List

Document Management (Documents) - 11 endpoints

Method Path Description
GET /api/documents Get document list
GET /api/documents/:id Get document details
POST /api/documents Create document
PUT /api/documents/:id Update document
DELETE /api/documents/:id Delete document

File Management (Files) - 14 endpoints

Method Path Description
GET /api/files Get file list
GET /api/files/:id Get file details
POST /api/files/upload Upload file
PUT /api/files/:id Update file info
DELETE /api/files/:id Delete file

Message Management (Messages) - 5 endpoints

Method Path Description
GET /api/messages Get message list
GET /api/messages/:id Get message details
POST /api/messages Send message
PUT /api/messages/:id/read Mark as read
DELETE /api/messages/:id Delete message

Notification Management (Notifications) - 5 endpoints

Method Path Description
GET /api/notifications Get notification list
PUT /api/notifications/:id/read Mark as read
PUT /api/notifications/read-all Mark all as read
DELETE /api/notifications/:id Delete notification

Calendar Management (Calendar) - 9 endpoints

Method Path Description
GET /api/calendar/events Get event list
GET /api/calendar/events/:id Get event details
POST /api/calendar/events Create event
PUT /api/calendar/events/:id Update event
DELETE /api/calendar/events/:id Delete event

Dashboard (Dashboard) - 9 endpoints

Method Path Description
GET /api/dashboard/stats Statistics data
GET /api/dashboard/visits Visit trends
GET /api/dashboard/sales Sales data
GET /api/dashboard/pie Pie chart data
GET /api/dashboard/tasks Task list
GET /api/dashboard/overview System overview

Authentication Mechanism

JWT Dual Tokens

Access Token:  15 minutes validity, used for API requests
Refresh Token: 7 days validity, used to refresh Access Token

Request Headers

http
Authorization: Bearer <access_token>

Refresh Flow

typescript
// Token refresh example
const response = await fetch('/api/auth/refresh', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ refreshToken })
});

const { accessToken, refreshToken: newRefreshToken } = await response.json();

Permission System

Role Definitions

Role Description Permissions
super_admin Super Administrator * (all permissions)
admin Administrator users:*, documents:*, ...
user Regular User documents:view, files:view, ...
guest Guest dashboard:view

Permission Format

{resource}:{action}

Examples:
- users:view      # View users
- users:create    # Create users
- users:*         # All user operations
- *               # All permissions

Error Handling

Error Response Format

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" }
    ]
  }
}

Error Codes

Status Code Error Code Description
400 VALIDATION_ERROR Validation failed
401 UNAUTHORIZED Unauthorized
403 FORBIDDEN No permission
404 NOT_FOUND Resource not found
409 CONFLICT Resource conflict
500 INTERNAL_ERROR Server error

Common Commands

bash
# Development
pnpm dev              # Start development server
pnpm start:debug      # Debug mode

# Build
pnpm build            # Build for production
pnpm start:prod       # Run production build

# Testing
pnpm test             # Run unit tests
pnpm test:e2e         # Run E2E tests
pnpm test:cov         # Generate coverage report

# Database
pnpm prisma:generate  # Generate Prisma Client
pnpm prisma:migrate   # Run migrations
pnpm prisma:studio    # Prisma Studio GUI
pnpm prisma:seed      # Run seed data

# Code Quality
pnpm lint             # ESLint check
pnpm lint:fix         # Auto-fix issues
pnpm format           # Prettier formatting

Deployment

Docker

bash
docker build -t halolight-api-nestjs .
docker run -p 3000:3000 halolight-api-nestjs

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Production Configuration

env
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=your-production-secret

Testing

Run Tests

bash
pnpm test             # Unit tests
pnpm test:e2e         # E2E tests
pnpm test:cov         # Coverage report

Test Example

typescript
describe('AuthController', () => {
  it('should login user', async () => {
    const response = await request(app.getHttpServer())
      .post('/api/auth/login')
      .send({ email: '[email protected]', password: 'password' })
      .expect(200);

    expect(response.body).toHaveProperty('accessToken');
  });
});

Performance Metrics

Benchmarks

Metric Value Conditions
Request Throughput 5000+ QPS Single CPU core
Average Response Time <50ms Simple queries
Memory Usage ~150MB After startup
CPU Usage <30% Normal load

Observability

Logging System

typescript
// Logging configuration
import { WinstonModule } from 'nest-winston';

WinstonModule.forRoot({
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

Health Check

typescript
// GET /health
{
  "status": "ok",
  "info": {
    "database": { "status": "up" },
    "redis": { "status": "up" }
  }
}

Monitoring Metrics

typescript
// Prometheus metrics endpoint
// GET /metrics
http_requests_total{method="GET",status="200"} 1234
http_request_duration_seconds{quantile="0.99"} 0.052

FAQ

Q: How to configure database connection?

A: Set DATABASE_URL in .env file

env
DATABASE_URL="postgresql://user:password@localhost:5432/halolight"

Q: How to handle file uploads?

A: Use FileInterceptor from @nestjs/platform-express

typescript
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
  return { filename: file.originalname };
}

Development Tools

  • Prisma Studio - Database visualization and management
  • Swagger UI - API documentation and testing
  • Postman - API debugging tool
  • NestJS CLI - Code generation tool

Backend Comparison

Feature NestJS FastAPI Spring Boot Laravel
Language TypeScript Python Java PHP
ORM Prisma SQLAlchemy JPA Eloquent
Performance ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Learning Curve ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
]]>
<![CDATA[Node.js Express Backend API]]> https://halolight.docs.h7ml.cn/en/guide/api-node https://halolight.docs.h7ml.cn/en/guide/api-node Fri, 19 Dec 2025 04:56:41 GMT Node.js Express Backend API

HaloLight Node.js backend API is built on Express 5 + TypeScript + Prisma, providing enterprise-grade RESTful API service.

API Documentation: https://halolight-api-node.h7ml.cn/docs

GitHub: https://github.com/halolight/halolight-api-node

Features

  • 🔐 Dual JWT Tokens - Access Token + Refresh Token with auto-renewal
  • 🛡️ RBAC Permissions - Role-based access control with wildcard matching
  • 📡 RESTful API - Standardized interface design with OpenAPI documentation
  • 🗄️ Prisma ORM - Type-safe database operations
  • Data Validation - Zod request validation and error handling
  • 📊 Logging System - Pino logging with request tracking
  • 🐳 Docker Support - Containerized deployment
  • 🎯 12 Business Modules - 90+ RESTful endpoints

Tech Stack

Technology Version Description
Node.js 20+ JavaScript runtime
Express 5.x Web framework
Prisma 6.x Database ORM
PostgreSQL 16 Data storage
Zod 3.x Data validation
JWT 9.x Authentication
Pino 9.x Logging system
Swagger UI 5.x API documentation

Quick Start

Requirements

  • Node.js >= 20
  • pnpm >= 9
  • PostgreSQL (optional, defaults to SQLite)

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-api-node.git
cd halolight-api-node

# Install dependencies
pnpm install

Environment Variables

bash
cp .env.example .env
env
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/halolight?schema=public"

# JWT Secrets (must be ≥32 characters)
JWT_SECRET="your-super-secret-jwt-key-minimum-32-characters-long"
JWT_EXPIRES_IN=7d
REFRESH_TOKEN_SECRET="your-refresh-secret-key-minimum-32-characters-long"
REFRESH_TOKEN_EXPIRES_IN=30d

# Service Configuration
PORT=3001
NODE_ENV=development

# CORS
CORS_ORIGIN="http://localhost:3000"

Database Initialization

bash
# Generate Prisma Client
pnpm db:generate

# Push database changes
pnpm db:push

# Seed database (optional)
pnpm db:seed

Start Service

bash
# Development mode
pnpm dev

# Production mode
pnpm build
pnpm start

Visit http://localhost:3001

Project Structure

halolight-api-node/
├── src/
│   ├── routes/               # Controllers/Route handlers
│   │   ├── auth.ts          # Authentication routes
│   │   ├── users.ts         # User management
│   │   ├── roles.ts         # Role management
│   │   ├── permissions.ts   # Permission management
│   │   ├── teams.ts         # Team management
│   │   ├── documents.ts     # Document management
│   │   ├── files.ts         # File management
│   │   ├── folders.ts       # Folder management
│   │   ├── calendar.ts      # Calendar events
│   │   ├── notifications.ts # Notification management
│   │   ├── messages.ts      # Message management
│   │   └── dashboard.ts     # Dashboard statistics
│   ├── services/            # Business logic layer
│   ├── middleware/          # Middleware
│   │   ├── auth.ts          # JWT auth + RBAC
│   │   ├── validate.ts      # Zod request validation
│   │   └── error.ts         # Global error handling
│   ├── utils/               # Utility functions
│   ├── config/              # Configuration files
│   │   ├── env.ts           # Environment variables
│   │   └── swagger.ts       # Swagger configuration
│   └── index.ts             # Application entry
├── prisma/                  # Database migrations/Schema
│   └── schema.prisma        # Database models (17+ models)
├── tests/                   # Test files
├── Dockerfile               # Docker configuration
├── docker-compose.yml
└── package.json

API Modules

Authentication Endpoints

Method Path Description Permission
POST /api/auth/login User login Public
POST /api/auth/register User registration Public
POST /api/auth/refresh Refresh token Public
POST /api/auth/logout User logout Authenticated
POST /api/auth/forgot-password Forgot password Public
POST /api/auth/reset-password Reset password Public

User Management Endpoints

Method Path Description Permission
GET /api/users Get user list users:view
GET /api/users/:id Get user details users:view
POST /api/users Create user users:create
PUT /api/users/:id Update user users:update
DELETE /api/users/:id Delete user users:delete
GET /api/users/me Get current user Authenticated

Complete Endpoint List

Role Management (Roles) - 6 endpoints

Method Path Description
GET /api/roles Get role list
GET /api/roles/:id Get role details
POST /api/roles Create role
PUT /api/roles/:id Update role
DELETE /api/roles/:id Delete role
PUT /api/roles/:id/permissions Assign permissions

Permission Management (Permissions) - 4 endpoints

Method Path Description
GET /api/permissions Get permission list
GET /api/permissions/:id Get permission details
POST /api/permissions Create permission
DELETE /api/permissions/:id Delete permission

Team Management (Teams) - 7 endpoints

Method Path Description
GET /api/teams Get team list
GET /api/teams/:id Get team details
POST /api/teams Create team
PUT /api/teams/:id Update team
DELETE /api/teams/:id Delete team
POST /api/teams/:id/members Add member
DELETE /api/teams/:id/members/:userId Remove member

Document Management (Documents) - 11 endpoints

Method Path Description
GET /api/documents Get document list
GET /api/documents/:id Get document details
POST /api/documents Create document
PUT /api/documents/:id Update document
DELETE /api/documents/:id Delete document
POST /api/documents/:id/share Share document
DELETE /api/documents/:id/share/:shareId Unshare document
POST /api/documents/:id/tags Add tag
DELETE /api/documents/:id/tags/:tagId Remove tag
POST /api/documents/:id/move Move document
POST /api/documents/:id/copy Copy document

File Management (Files) - 14 endpoints

Method Path Description
GET /api/files Get file list
GET /api/files/:id Get file details
POST /api/files/upload Upload file
GET /api/files/:id/download Download file
PUT /api/files/:id Update file info
DELETE /api/files/:id Delete file
POST /api/files/:id/move Move file
POST /api/files/:id/copy Copy file
POST /api/files/:id/share Share file
GET /api/files/:id/versions Get version history
GET /api/files/storage Get storage info
POST /api/files/batch-delete Batch delete
POST /api/files/batch-move Batch move
POST /api/files/search Search files

Folder Management (Folders) - 5 endpoints

Method Path Description
GET /api/folders Get folder tree
POST /api/folders Create folder
PUT /api/folders/:id Update folder
DELETE /api/folders/:id Delete folder
POST /api/folders/:id/move Move folder

Calendar Management (Calendar) - 9 endpoints

Method Path Description
GET /api/calendar/events Get event list
GET /api/calendar/events/:id Get event details
POST /api/calendar/events Create event
PUT /api/calendar/events/:id Update event
DELETE /api/calendar/events/:id Delete event
POST /api/calendar/events/:id/attendees Add attendee
DELETE /api/calendar/events/:id/attendees/:userId Remove attendee
POST /api/calendar/events/:id/reminder Set reminder
GET /api/calendar/events/upcoming Get upcoming events

Notification Management (Notifications) - 5 endpoints

Method Path Description
GET /api/notifications Get notification list
GET /api/notifications/:id Get notification details
PUT /api/notifications/:id/read Mark as read
PUT /api/notifications/read-all Mark all as read
DELETE /api/notifications/:id Delete notification

Message Management (Messages) - 5 endpoints

Method Path Description
GET /api/messages/conversations Get conversation list
GET /api/messages/conversations/:id Get conversation details
POST /api/messages Send message
PUT /api/messages/:id/read Mark as read
DELETE /api/messages/:id Delete message

Dashboard (Dashboard) - 9 endpoints

Method Path Description
GET /api/dashboard/stats Statistics data
GET /api/dashboard/visits Visit trends
GET /api/dashboard/sales Sales data
GET /api/dashboard/pie Pie chart data
GET /api/dashboard/tasks To-do tasks
GET /api/dashboard/calendar Today's events
GET /api/dashboard/activities Recent activities
GET /api/dashboard/notifications Unread notifications
GET /api/dashboard/users User statistics

Authentication Mechanism

Dual JWT Tokens

Access Token:  7 days validity, used for API requests
Refresh Token: 30 days validity, used to refresh Access Token

Request Headers

http
Authorization: Bearer <access_token>

Refresh Flow

typescript
// POST /api/auth/refresh
const response = await fetch('/api/auth/refresh', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ refreshToken: 'your_refresh_token' })
});

const { accessToken, refreshToken } = await response.json();
// Update locally stored tokens
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);

Permission System

Role Definitions

Role Description Permissions
super_admin Super Administrator * (all permissions)
admin Administrator users:*, documents:*, files:*, teams:*
user Regular User documents:view, files:view, calendar:*
guest Guest dashboard:view

Permission Format

{resource}:{action}

Examples:
- users:view      # View users
- users:create    # Create users
- users:*         # All user operations
- *               # All permissions

Permission Validation Example

typescript
// Using permission middleware in routes
import { requireAuth, requirePermission } from './middleware/auth';

// Requires authentication
router.get('/api/users/me', requireAuth, getUserProfile);

// Requires specific permission
router.post('/api/users', requireAuth, requirePermission('users:create'), createUser);

// Requires one of multiple permissions
router.put('/api/users/:id', requireAuth, requirePermission(['users:update', 'users:*']), updateUser);

Error Handling

Error Response Format

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" }
    ]
  }
}

Error Codes

Status Code Error Code Description
400 VALIDATION_ERROR Validation failed
401 UNAUTHORIZED Unauthorized
403 FORBIDDEN Forbidden
404 NOT_FOUND Resource not found
409 CONFLICT Resource conflict
500 INTERNAL_ERROR Internal server error

Common Commands

bash
# Development
pnpm dev              # Start dev server (hot reload)
pnpm build            # TypeScript compilation
pnpm start            # Start production server

# Testing
pnpm test             # Run tests
pnpm test:coverage    # Test coverage

# Database
pnpm db:generate      # Generate Prisma Client
pnpm db:push          # Push database changes
pnpm db:migrate       # Run migrations
pnpm db:studio        # Prisma Studio (database GUI)
pnpm db:seed          # Seed database

# Code Quality
pnpm lint             # ESLint check
pnpm lint:fix         # Auto fix
pnpm format           # Prettier formatting

Deployment

Docker

bash
docker build -t halolight-api-node .
docker run -p 3001:3001 halolight-api-node

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3001:3001"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
      - REFRESH_TOKEN_SECRET=${REFRESH_TOKEN_SECRET}
    restart: unless-stopped
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

Production Configuration

env
NODE_ENV=production
PORT=3001
DATABASE_URL=postgresql://user:pass@host:5432/halolight
JWT_SECRET=your-production-secret-minimum-32-characters
REFRESH_TOKEN_SECRET=your-refresh-secret-minimum-32-characters
CORS_ORIGIN=https://yourdomain.com

Testing

Running Tests

bash
# Run all tests
pnpm test

# Run test coverage
pnpm test:coverage

# Watch mode
pnpm test:watch

Test Examples

typescript
// tests/auth.test.ts
import request from 'supertest';
import app from '../src/index';

describe('Authentication', () => {
  it('should login successfully', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({
        email: '[email protected]',
        password: 'password123'
      });

    expect(response.status).toBe(200);
    expect(response.body.success).toBe(true);
    expect(response.body.data).toHaveProperty('accessToken');
    expect(response.body.data).toHaveProperty('refreshToken');
  });

  it('should reject invalid credentials', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({
        email: '[email protected]',
        password: 'wrongpassword'
      });

    expect(response.status).toBe(401);
    expect(response.body.success).toBe(false);
  });
});

Performance Metrics

Benchmarks

Metric Value Condition
Request Throughput ~8,000 req/s Single core, simple queries
Average Response Time <10ms Local database, no complex queries
Memory Usage ~80MB Base memory after startup
CPU Usage <5% Idle state

Performance Optimization Tips

  • Use connection pooling for database connections
  • Enable database indexes to optimize queries
  • Implement caching strategy (Redis)
  • Use CDN for static assets
  • Enable Gzip compression

Observability

Logging System

typescript
// Using Pino logging
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'yyyy-mm-dd HH:MM:ss',
      ignore: 'pid,hostname'
    }
  }
});

// Log requests
app.use((req, res, next) => {
  logger.info({
    method: req.method,
    url: req.url,
    ip: req.ip
  }, 'Incoming request');
  next();
});

Health Check

typescript
// GET /health
app.get('/health', async (req, res) => {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    database: 'connected'
  };

  try {
    // Check database connection
    await prisma.$queryRaw`SELECT 1`;
  } catch (error) {
    health.status = 'unhealthy';
    health.database = 'disconnected';
    return res.status(503).json(health);
  }

  res.json(health);
});

Monitoring Metrics

typescript
// Prometheus metrics integration
import promClient from 'prom-client';

const register = new promClient.Registry();

// Default metrics
promClient.collectDefaultMetrics({ register });

// Custom metrics
const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register]
});

// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.send(await register.metrics());
});

FAQ

Q: How to share database across multiple services?

A: Configure the same DATABASE_URL and ensure identical Prisma Schema.

env
# All services use the same database connection
DATABASE_URL="postgresql://user:pass@shared-db:5432/halolight"
bash
# Ensure Schema consistency
pnpm db:push

Q: How to auto-refresh JWT tokens when expired?

A: Detect 401 errors in frontend interceptor and automatically call refresh endpoint.

typescript
// Axios interceptor example
axios.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const { data } = await axios.post('/api/auth/refresh', { refreshToken });

        localStorage.setItem('accessToken', data.accessToken);
        localStorage.setItem('refreshToken', data.refreshToken);

        originalRequest.headers['Authorization'] = `Bearer ${data.accessToken}`;
        return axios(originalRequest);
      } catch (err) {
        // Refresh failed, redirect to login
        window.location.href = '/login';
        return Promise.reject(err);
      }
    }

    return Promise.reject(error);
  }
);

Q: How to implement file upload restrictions?

A: Use multer middleware to configure file size and type limits.

typescript
import multer from 'multer';

const upload = multer({
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
  },
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Unsupported file type'));
    }
  }
});

router.post('/api/files/upload', upload.single('file'), uploadFile);

Q: How to enable HTTPS?

A: Use Nginx reverse proxy or configure SSL certificates in Express.

typescript
// Enable HTTPS in Express
import https from 'https';
import fs from 'fs';

const options = {
  key: fs.readFileSync('path/to/private-key.pem'),
  cert: fs.readFileSync('path/to/certificate.pem')
};

https.createServer(options, app).listen(443, () => {
  console.log('HTTPS server running on port 443');
});

Development Tools

  • Prisma Studio - Visual database management tool (pnpm db:studio)
  • Postman - API testing tool (can import Swagger docs)
  • VSCode Extension Pack - ESLint + Prettier + TypeScript
  • Docker Desktop - Container management
  • pgAdmin - PostgreSQL database management

VSCode Configuration

json
// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.tsdk": "node_modules/typescript/lib"
}

Comparison with Other Backends

Feature Express NestJS Fastify Koa
Language JavaScript/TypeScript TypeScript JavaScript/TypeScript JavaScript/TypeScript
Performance ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
Learning Curve ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
Ecosystem ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Built-in Features ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
Community Support ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐

Why Choose Express?

  • Mature and Stable - Over 10 years of production validation
  • High Flexibility - Minimalist framework with free middleware composition
  • Rich Ecosystem - Massive third-party plugins and tools
  • Low Learning Curve - Simple and easy to understand for quick start
  • Active Community - Abundant documentation and Q&A resources
]]>
<![CDATA[PHP Laravel Backend API]]> https://halolight.docs.h7ml.cn/en/guide/api-php https://halolight.docs.h7ml.cn/en/guide/api-php Fri, 19 Dec 2025 04:56:41 GMT PHP Laravel Backend API

HaloLight PHP enterprise-grade backend API service, built with Laravel 11 + PostgreSQL + Redis, providing a complete RESTful API.

API Documentation: https://halolight-api-php.h7ml.cn/docs

GitHub: https://github.com/halolight/halolight-api-php

Features

  • 🔐 JWT Dual Token - Access Token + Refresh Token, automatic renewal
  • 🛡️ RBAC Permissions - Role-based access control, wildcard matching
  • 📡 RESTful API - Standardized API design, OpenAPI documentation
  • 🗄️ Eloquent ORM - Elegant ActiveRecord database operations
  • Data Validation - Form Request parameter validation
  • 📊 Logging System - Request logging, error tracking
  • 🐳 Docker Support - Containerized deployment

Tech Stack

Technology Version Description
PHP 8.2+ Runtime
Laravel 11.x Web Framework
Eloquent 11.x Database ORM
PostgreSQL 16 Data Storage
Redis 7 Cache/Queue
Form Request 11.x Data Validation
JWT tymon/jwt-auth Authentication
L5-Swagger 8.x API Documentation

Quick Start

Requirements

  • PHP >= 8.2
  • Composer >= 2.0
  • PostgreSQL 16 (optional, defaults to SQLite)

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-api-php.git
cd halolight-api-php

# Install dependencies
composer install

Environment Variables

bash
cp .env.example .env
env
# Database
DB_CONNECTION=pgsql
DB_HOST=localhost
DB_PORT=5432
DB_DATABASE=halolight
DB_USERNAME=postgres
DB_PASSWORD=your_password

# JWT Secret
JWT_SECRET=your-super-secret-key-min-32-chars
JWT_TTL=10080  # 7 days, in minutes

# Service Config
APP_PORT=8080
APP_ENV=development
APP_DEBUG=true

Database Initialization

bash
# Generate application key
php artisan key:generate

# Run migrations
php artisan migrate

# Seed data
php artisan db:seed

Start Service

bash
# Development mode
php artisan serve --port=8080

# Production mode
php artisan optimize
php artisan serve --port=8080 --env=production

Visit http://localhost:8080

Project Structure

halolight-api-php/
├── app/
│   ├── Http/
│   │   ├── Controllers/     # Controllers/Route handlers
│   │   ├── Middleware/      # Middleware
│   │   └── Requests/        # Request validation
│   ├── Services/            # Business logic layer
│   ├── Models/              # Data models
│   ├── Enums/               # Enum types
│   └── Providers/           # Service providers
├── database/
│   ├── migrations/          # Database migrations
│   └── seeders/             # Seed data
├── tests/                   # Test files
├── Dockerfile               # Docker configuration
├── docker-compose.yml
└── composer.json

API Modules

Authentication Endpoints

Method Path Description Permission
POST /api/auth/login User login Public
POST /api/auth/register User registration Public
POST /api/auth/refresh Refresh token Public
POST /api/auth/logout Logout Authenticated
POST /api/auth/forgot-password Forgot password Public
POST /api/auth/reset-password Reset password Public
GET /api/auth/me Get current user Authenticated

User Management Endpoints

Method Path Description Permission
GET /api/users Get user list users:view
GET /api/users/:id Get user details users:view
POST /api/users Create user users:create
PUT /api/users/:id Update user users:update
DELETE /api/users/:id Delete user users:delete
GET /api/users/me Get current user Authenticated
PATCH /api/users/:id/status Update user status users:update

Complete Endpoint List

Role Management (Roles) - 6 endpoints

Method Path Description
GET /api/roles Get role list
GET /api/roles/:id Get role details
POST /api/roles Create role
PUT /api/roles/:id Update role
DELETE /api/roles/:id Delete role
POST /api/roles/:id/permissions Assign permissions

Permission Management (Permissions) - 4 endpoints

Method Path Description
GET /api/permissions Get permission list
GET /api/permissions/:id Get permission details
POST /api/permissions Create permission
DELETE /api/permissions/:id Delete permission

Team Management (Teams) - 7 endpoints

Method Path Description
GET /api/teams Get team list
GET /api/teams/:id Get team details
POST /api/teams Create team
PUT /api/teams/:id Update team
DELETE /api/teams/:id Delete team
POST /api/teams/:id/members Add member
DELETE /api/teams/:id/members/:userId Remove member

Document Management (Documents) - 9 endpoints

Method Path Description
GET /api/documents Get document list
GET /api/documents/:id Get document details
POST /api/documents Create document
PUT /api/documents/:id Update document
DELETE /api/documents/:id Delete document
POST /api/documents/:id/share Share document
GET /api/documents/:id/versions Get version history
POST /api/documents/:id/restore Restore version
POST /api/documents/:id/duplicate Duplicate document

File Management (Files) - 9 endpoints

Method Path Description
GET /api/files Get file list
GET /api/files/:id Get file details
POST /api/files/upload Upload file
PUT /api/files/:id Update file info
DELETE /api/files/:id Delete file
GET /api/files/:id/download Download file
POST /api/files/:id/move Move file
POST /api/files/:id/copy Copy file
GET /api/files/:id/preview Preview file

Folder Management (Folders) - 5 endpoints

Method Path Description
GET /api/folders Get folder list
GET /api/folders/:id Get folder details
POST /api/folders Create folder
PUT /api/folders/:id Update folder
DELETE /api/folders/:id Delete folder

Message Management (Messages) - 5 endpoints

Method Path Description
GET /api/messages Get message list
GET /api/messages/:id Get message details
POST /api/messages Send message
PUT /api/messages/:id/read Mark as read
DELETE /api/messages/:id Delete message

Notification Management (Notifications) - 5 endpoints

Method Path Description
GET /api/notifications Get notification list
GET /api/notifications/:id Get notification details
PUT /api/notifications/:id/read Mark as read
PUT /api/notifications/read-all Mark all as read
DELETE /api/notifications/:id Delete notification

Calendar Management (Calendar) - 8 endpoints

Method Path Description
GET /api/calendar/events Get event list
GET /api/calendar/events/:id Get event details
POST /api/calendar/events Create event
PUT /api/calendar/events/:id Update event
DELETE /api/calendar/events/:id Delete event
POST /api/calendar/events/:id/attendees Add attendee
DELETE /api/calendar/events/:id/attendees/:userId Remove attendee
GET /api/calendar/availability Check availability

Dashboard (Dashboard) - 9 endpoints

Method Path Description
GET /api/dashboard/stats Statistics data
GET /api/dashboard/visits Visit trends
GET /api/dashboard/sales Sales data
GET /api/dashboard/pie Pie chart data
GET /api/dashboard/tasks Todo tasks
GET /api/dashboard/calendar Today's schedule
GET /api/dashboard/notifications Latest notifications
GET /api/dashboard/activity Activity log
GET /api/dashboard/overview Overview data

Authentication

JWT Dual Token

Access Token:  7 days validity, for API requests
Refresh Token: 30 days validity, for refreshing Access Token

Request Header

http
Authorization: Bearer <access_token>

Refresh Flow

php
<?php

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    public function refresh(Request $request)
    {
        $refreshToken = $request->input('refreshToken');

        // Validate Refresh Token
        try {
            auth()->setToken($refreshToken)->authenticate();

            // Generate new Access Token
            $newAccessToken = auth()->refresh();

            return response()->json([
                'accessToken' => $newAccessToken,
                'refreshToken' => $refreshToken, // Optional: can also generate new Refresh Token
                'expiresIn' => auth()->factory()->getTTL() * 60
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'error' => 'Invalid refresh token'
            ], 401);
        }
    }
}

Permission System

Role Definitions

Role Description Permissions
super_admin Super Administrator * (all permissions)
admin Administrator users:*, documents:*, files:*, teams:*
user Regular User documents:view, files:view, calendar:*
guest Guest dashboard:view

Permission Format

{resource}:{action}

Examples:
- users:view      # View users
- users:create    # Create user
- users:*         # All user operations
- *               # All permissions

Error Handling

Error Response Format

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request parameter validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" }
    ]
  }
}

Error Codes

Status Error Code Description
400 VALIDATION_ERROR Parameter validation failed
401 UNAUTHORIZED Unauthorized
403 FORBIDDEN No permission
404 NOT_FOUND Resource not found
409 CONFLICT Resource conflict
422 UNPROCESSABLE_ENTITY Unprocessable entity
500 INTERNAL_ERROR Server error

Common Commands

bash
# Development
php artisan serve --port=8080    # Start development server
php artisan tinker                # Enter REPL environment

# Build
php artisan optimize              # Optimize application
php artisan config:cache          # Cache configuration
php artisan route:cache           # Cache routes

# Testing
php artisan test                  # Run tests
php artisan test --coverage       # Run tests with coverage report

# Database
php artisan migrate               # Run migrations
php artisan migrate:fresh --seed  # Reset and seed data
php artisan db:seed               # Seed data

# Code quality
composer lint                     # Laravel Pint code style check
composer analyse                  # PHPStan static analysis

Deployment

Docker

bash
docker build -t halolight-api-php .
docker run -p 8080:8080 halolight-api-php

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - APP_ENV=production
      - APP_DEBUG=false
      - DB_CONNECTION=pgsql
      - DB_HOST=db
      - DB_DATABASE=halolight
      - DB_USERNAME=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - REDIS_HOST=redis
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - db
      - redis
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

Production Configuration

env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://halolight-api-php.h7ml.cn

DB_CONNECTION=pgsql
DB_HOST=your-db-host
DB_DATABASE=halolight
DB_USERNAME=your-db-user
DB_PASSWORD=your-db-password

REDIS_HOST=your-redis-host
REDIS_PASSWORD=your-redis-password

JWT_SECRET=your-production-secret-min-32-chars
JWT_TTL=10080

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

Testing

Run Tests

bash
php artisan test
php artisan test --coverage

Test Example

php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class AuthTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_login_with_correct_credentials()
    {
        $user = User::factory()->create([
            'email' => '[email protected]',
            'password' => bcrypt('password123')
        ]);

        $response = $this->postJson('/api/auth/login', [
            'email' => '[email protected]',
            'password' => 'password123'
        ]);

        $response->assertStatus(200)
                 ->assertJsonStructure([
                     'accessToken',
                     'refreshToken',
                     'user'
                 ]);
    }

    public function test_user_cannot_login_with_incorrect_password()
    {
        $user = User::factory()->create([
            'email' => '[email protected]',
            'password' => bcrypt('password123')
        ]);

        $response = $this->postJson('/api/auth/login', [
            'email' => '[email protected]',
            'password' => 'wrong-password'
        ]);

        $response->assertStatus(401)
                 ->assertJson([
                     'error' => 'Unauthorized'
                 ]);
    }
}

Performance Metrics

Benchmark

Metric Value Description
Request Throughput ~1,500 QPS Single core, using Swoole/Octane
Average Response Time ~15ms Simple query, PostgreSQL
Memory Usage ~50MB Single process, no cache
CPU Usage ~40% High load, 4 cores

Observability

Logging System

php
<?php

use Illuminate\Support\Facades\Log;

// Configure log channels
// config/logging.php
return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['single', 'daily'],
        ],
        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'days' => 14,
        ],
    ],
];

// Use logging
Log::info('User logged in', ['user_id' => $user->id]);
Log::error('Payment failed', ['error' => $exception->getMessage()]);

Health Check

php
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;

class HealthController extends Controller
{
    public function check()
    {
        $status = [
            'status' => 'healthy',
            'timestamp' => now()->toIso8601String(),
            'services' => []
        ];

        // Check database
        try {
            DB::connection()->getPdo();
            $status['services']['database'] = 'healthy';
        } catch (\Exception $e) {
            $status['status'] = 'unhealthy';
            $status['services']['database'] = 'unhealthy';
        }

        // Check Redis
        try {
            Redis::ping();
            $status['services']['redis'] = 'healthy';
        } catch (\Exception $e) {
            $status['status'] = 'unhealthy';
            $status['services']['redis'] = 'unhealthy';
        }

        return response()->json($status);
    }
}

Monitoring Metrics

php
<?php

namespace App\Http\Middleware;

use Illuminate\Support\Facades\Cache;

class MetricsMiddleware
{
    public function handle($request, $next)
    {
        $start = microtime(true);

        $response = $next($request);

        $duration = microtime(true) - $start;

        // Record request metrics
        Cache::increment('metrics:requests_total');
        Cache::increment("metrics:requests_{$response->status()}");

        // Record response time (can use Prometheus or other tools)
        $this->recordDuration($request->path(), $duration);

        return $response;
    }

    private function recordDuration($path, $duration)
    {
        // Implement metrics recording logic
    }
}

FAQ

Q: How to handle file uploads?

A: Laravel provides convenient file upload handling:

php
<?php

namespace App\Http\Controllers\Api\V1;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
    public function upload(Request $request)
    {
        $request->validate([
            'file' => 'required|file|max:10240', // 10MB
        ]);

        $file = $request->file('file');
        $path = $file->store('uploads', 'public');

        return response()->json([
            'path' => Storage::url($path),
            'name' => $file->getClientOriginalName(),
            'size' => $file->getSize(),
            'mime' => $file->getMimeType()
        ]);
    }
}

Q: How to implement database transactions?

A: Use Laravel's DB facade or Eloquent models:

php
<?php

use Illuminate\Support\Facades\DB;
use App\Models\Order;
use App\Models\Payment;

// Method 1: DB facade
DB::transaction(function () {
    $order = Order::create([...]);
    $payment = Payment::create([
        'order_id' => $order->id,
        ...
    ]);
});

// Method 2: Manual control
DB::beginTransaction();
try {
    $order = Order::create([...]);
    $payment = Payment::create([...]);
    DB::commit();
} catch (\Exception $e) {
    DB::rollback();
    throw $e;
}

Development Tools

  • Laravel Idea - Laravel enhancement plugin for PhpStorm
  • Laravel Telescope - Local development debugging tool
  • Laravel Debugbar - Development environment performance analysis
  • PHPStan - Static analysis tool
  • Laravel Pint - Code style formatter

Backend Framework Comparison

Feature Laravel NestJS FastAPI Spring Boot
Language PHP TypeScript Python Java
ORM Eloquent Prisma SQLAlchemy JPA
Performance ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
Learning Curve ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
Ecosystem ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
Development Speed ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
]]>
<![CDATA[Python FastAPI Backend API]]> https://halolight.docs.h7ml.cn/en/guide/api-python https://halolight.docs.h7ml.cn/en/guide/api-python Fri, 19 Dec 2025 04:56:41 GMT Python FastAPI Backend API

HaloLight FastAPI backend API is built on FastAPI 0.115+, providing modern asynchronous Python backend service.

API Documentation: https://halolight-api-python.h7ml.cn/api/docs

GitHub: https://github.com/halolight/halolight-api-python

Features

  • 🔐 JWT Dual-Token - Access Token + Refresh Token, auto renewal
  • 🛡️ RBAC Permissions - Role-based access control, wildcard matching
  • 📡 RESTful API - Standardized interface design, OpenAPI docs
  • 🗄️ SQLAlchemy 2.0 - Type-safe database operations
  • Data Validation - Request parameter validation, error handling
  • 📊 Logging System - Request logging, error tracking
  • 🐳 Docker Support - Containerized deployment

Tech Stack

Technology Version Description
Python 3.11+ Runtime
FastAPI 0.115+ Web Framework
SQLAlchemy 2.0+ Database ORM
PostgreSQL 16 Data Storage
Pydantic v2 Data Validation
JWT python-jose Authentication
Swagger UI - API Documentation

Quick Start

Requirements

  • Python >= 3.11
  • pip >= 23.0
  • PostgreSQL 16 (optional, defaults to SQLite)

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-api-python.git
cd halolight-api-python

# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate

# Install dependencies
pip install -e .

Environment Variables

bash
cp .env.example .env
env
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/halolight_db

# JWT Secret
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# Service Config
PORT=8000
NODE_ENV=development

Database Initialization

bash
alembic upgrade head           # Run migrations
python scripts/seed.py         # Seed data

Start Service

bash
# Development mode
uvicorn app.main:app --reload --port 8000

# Production mode
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4

Visit http://localhost:8000

Project Structure

halolight-api-python/
├── app/
│   ├── api/              # Controllers/Route handlers
│   │   ├── auth.py       # Auth endpoints
│   │   ├── users.py      # User management
│   │   └── ...
│   ├── services/         # Business logic layer
│   ├── models/           # Data models
│   ├── schemas/          # Request validation
│   ├── core/             # Utility functions
│   └── main.py           # App entry
├── alembic/              # Database migrations/Schema
├── tests/                # Test files
├── Dockerfile            # Docker config
├── docker-compose.yml
└── pyproject.toml

API Modules

Authentication Endpoints

Method Path Description Permission
POST /api/auth/login User login Public
POST /api/auth/register User registration Public
POST /api/auth/refresh Refresh token Public
POST /api/auth/logout Logout Authenticated
POST /api/auth/forgot-password Forgot password Public
POST /api/auth/reset-password Reset password Public

User Management Endpoints

Method Path Description Permission
GET /api/users Get user list users:view
GET /api/users/:id Get user details users:view
POST /api/users Create user users:create
PUT /api/users/:id Update user users:update
DELETE /api/users/:id Delete user users:delete
GET /api/users/me Get current user Authenticated

Complete Endpoint List

Document Management (Documents) - 5 endpoints

Method Path Description
GET /api/documents Get document list
GET /api/documents/:id Get document details
POST /api/documents Create document
PUT /api/documents/:id Update document
DELETE /api/documents/:id Delete document

File Management (Files) - 5 endpoints

Method Path Description
GET /api/files Get file list
GET /api/files/:id Get file details
POST /api/files/upload Upload file
PUT /api/files/:id Update file info
DELETE /api/files/:id Delete file

Message Management (Messages) - 5 endpoints

Method Path Description
GET /api/messages Get message list
GET /api/messages/:id Get message details
POST /api/messages Send message
PUT /api/messages/:id/read Mark as read
DELETE /api/messages/:id Delete message

Notification Management (Notifications) - 4 endpoints

Method Path Description
GET /api/notifications Get notification list
PUT /api/notifications/:id/read Mark as read
PUT /api/notifications/read-all Mark all as read
DELETE /api/notifications/:id Delete notification

Calendar Management (Calendar) - 5 endpoints

Method Path Description
GET /api/calendar/events Get event list
GET /api/calendar/events/:id Get event details
POST /api/calendar/events Create event
PUT /api/calendar/events/:id Update event
DELETE /api/calendar/events/:id Delete event

Dashboard (Dashboard) - 6 endpoints

Method Path Description
GET /api/dashboard/stats Statistics data
GET /api/dashboard/visits Visit trends
GET /api/dashboard/sales Sales data
GET /api/dashboard/pie Pie chart data
GET /api/dashboard/tasks Task list
GET /api/dashboard/calendar Today's schedule

Authentication Mechanism

JWT Dual-Token

Access Token:  15 minutes validity, used for API requests
Refresh Token: 7 days validity, used to refresh Access Token

Request Header

http
Authorization: Bearer <access_token>

Refresh Flow

python
# Token refresh example
import requests

response = requests.post(
    'http://localhost:8000/api/auth/refresh',
    json={'refreshToken': refresh_token}
)
new_tokens = response.json()

Permission System

Role Definitions

Role Description Permissions
super_admin Super Admin * (all permissions)
admin Administrator users:*, documents:*, ...
user Regular User documents:view, files:view, ...
guest Guest dashboard:view

Permission Format

{resource}:{action}

Examples:
- users:view      # View users
- users:create    # Create user
- users:*         # All user operations
- *               # All permissions

Error Handling

Error Response Format

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request parameter validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" }
    ]
  }
}

Error Codes

Status Code Error Code Description
400 VALIDATION_ERROR Parameter validation failed
401 UNAUTHORIZED Unauthorized
403 FORBIDDEN Forbidden
404 NOT_FOUND Resource not found
409 CONFLICT Resource conflict
500 INTERNAL_ERROR Server error

Database Models

User Model

python
# app/models/user.py
from sqlalchemy import Column, Integer, String, DateTime
from app.core.database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    username = Column(String, unique=True, nullable=False)
    hashed_password = Column(String, nullable=False)
    role = Column(String, default="user")
    created_at = Column(DateTime, server_default=func.now())
    updated_at = Column(DateTime, onupdate=func.now())

Document Model

python
# app/models/document.py
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import relationship
from app.core.database import Base

class Document(Base):
    __tablename__ = "documents"

    id = Column(Integer, primary_key=True)
    title = Column(String, nullable=False)
    content = Column(Text)
    author_id = Column(Integer, ForeignKey("users.id"))
    author = relationship("User", back_populates="documents")

Environment Variables

Variable Description Default
DATABASE_URL Database connection string sqlite:///./halolight.db
JWT_SECRET JWT signing key -
JWT_ACCESS_EXPIRES Access Token expiry 15m
JWT_REFRESH_EXPIRES Refresh Token expiry 7d
PORT Service port 8000
NODE_ENV Runtime environment development
CORS_ORIGINS CORS allowed origins ["http://localhost:3000"]

Unified Response Format

Success Response

json
{
  "success": true,
  "data": {
    "id": 1,
    "name": "Example data"
  },
  "message": "Operation successful"
}

Paginated Response

json
{
  "success": true,
  "data": {
    "items": [...],
    "total": 100,
    "page": 1,
    "pageSize": 10,
    "totalPages": 10
  }
}

Error Response

json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Error description",
    "details": []
  }
}

Deployment

Docker

bash
docker build -t halolight-api-python .
docker run -p 8000:8000 halolight-api-python

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Production Configuration

env
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=your-production-secret

Testing

Running Tests

bash
pytest
pytest --cov=app tests/

Test Examples

python
def test_login_success(client):
    response = client.post(
        "/api/auth/login",
        json={"email": "[email protected]", "password": "123456"}
    )
    assert response.status_code == 200
    assert "accessToken" in response.json()

def test_get_users_with_permission(client, admin_token):
    response = client.get(
        "/api/users",
        headers={"Authorization": f"Bearer {admin_token}"}
    )
    assert response.status_code == 200
    assert isinstance(response.json()["data"], list)

Performance Metrics

Benchmarks

Metric Value Description
Request Throughput 5000+ QPS Single-core uvicorn
Avg Response Time < 10ms Simple queries
Memory Usage ~100MB Base runtime
CPU Usage 30-50% High load

Observability

Logging System

python
import logging

logger = logging.getLogger(__name__)
logger.info("User logged in", extra={"user_id": user.id})

Health Check

python
@app.get("/health")
async def health_check():
    return {"status": "ok", "timestamp": datetime.now()}

Monitoring Metrics

python
# Prometheus metrics endpoint
from prometheus_fastapi_instrumentator import Instrumentator

Instrumentator().instrument(app).expose(app)

Common Commands

bash
# Development
uvicorn app.main:app --reload --port 8000

# Build
pip install -e .

# Testing
pytest
pytest --cov=app tests/

# Database
alembic upgrade head
alembic revision --autogenerate -m "description"

# Code Quality
black app tests
ruff check app tests --fix

FAQ

Q: How to configure database connection pool?

A: Configure SQLAlchemy connection pool in core/database.py

python
engine = create_engine(
    DATABASE_URL,
    pool_size=10,
    max_overflow=20,
    pool_timeout=30
)

Q: How to enable CORS?

A: Configure CORS middleware in main.py

python
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Q: How to implement file upload?

A: Use FastAPI's UploadFile type

python
from fastapi import UploadFile, File

@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...)):
    contents = await file.read()
    # Process file contents
    return {"filename": file.filename}

Development Tools

  • Black - Python code formatter
  • Ruff - Fast linter
  • mypy - Type checker
  • pytest - Testing framework

Architecture Features

Async Advantages

FastAPI is based on Python's asyncio, supporting high-concurrency asynchronous operations:

python
@app.get("/api/async-example")
async def async_endpoint():
    result = await async_database_query()
    return result

Automatic Documentation

FastAPI automatically generates OpenAPI (Swagger) documentation with no extra configuration:

  • Swagger UI: /docs
  • ReDoc: /redoc
  • OpenAPI Schema: /openapi.json

Dependency Injection System

python
from fastapi import Depends

def get_current_user(token: str = Depends(oauth2_scheme)):
    return verify_token(token)

@app.get("/api/protected")
async def protected_route(user = Depends(get_current_user)):
    return {"user": user}

Backend Comparison

Feature FastAPI NestJS Go Fiber Spring Boot
Language Python TypeScript Go Java
ORM SQLAlchemy Prisma GORM JPA
Performance ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
Learning Curve ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
]]>
<![CDATA[Astro Version]]> https://halolight.docs.h7ml.cn/en/guide/astro https://halolight.docs.h7ml.cn/en/guide/astro Fri, 19 Dec 2025 04:56:41 GMT Astro Version

HaloLight Astro version is built on Astro 5, featuring Islands architecture with zero JS initial load and ultimate performance, supporting multi-framework component integration.

Live Preview: https://halolight-astro.h7ml.cn/

GitHub: https://github.com/halolight/halolight-astro

Features

  • 🏝️ Islands Architecture - Zero JS by default, hydrate interactive components on demand
  • Ultimate Performance - Zero JavaScript on initial load, Lighthouse 100 score
  • 🔀 Multi-framework Integration - Support React, Vue, Svelte, Solid components in one project
  • 📄 Content-first - Native Markdown/MDX support, content collections
  • 🔄 View Transitions - Native View Transitions API support
  • 🚀 SSR/SSG/Hybrid - Flexible rendering modes
  • 📦 API Endpoints - Native REST API endpoint support
  • 🎨 Theme System - Light/dark theme switching
  • 🔐 Authentication - Complete login/register/forgot password flow
  • 📊 Dashboard - Data visualization and business management

Tech Stack

Technology Version Description
Astro 5.x Islands architecture framework
TypeScript 5.x Type safety
Tailwind CSS 3.x Atomic CSS
Vite Built-in Build tool
@astrojs/node 9.x Node.js adapter
Vitest 4.x Unit testing

Core Features

  • Islands Architecture - Zero JS by default, hydrate interactive components on demand
  • Multi-framework Support - Use React, Vue, Svelte components in the same project
  • Content-first - Static-first, ultimate initial load performance
  • SSR Support - Server-side rendering via @astrojs/node adapter
  • File-based Routing - Automatic routing based on file system
  • API Endpoints - Native support for REST API endpoints

Directory Structure

halolight-astro/
├── src/
│   ├── pages/                    # File-based routing
│   │   ├── index.astro          # Home
│   │   ├── privacy.astro        # Privacy Policy
│   │   ├── terms.astro          # Terms of Service
│   │   ├── auth/                # Auth pages
│   │   │   ├── login.astro
│   │   │   ├── register.astro
│   │   │   ├── forgot-password.astro
│   │   │   └── reset-password.astro
│   │   ├── dashboard/           # Dashboard pages
│   │   │   ├── index.astro      # Dashboard home
│   │   │   ├── analytics.astro  # Analytics
│   │   │   ├── users.astro      # User management
│   │   │   ├── accounts.astro   # Account management
│   │   │   ├── documents.astro  # Document management
│   │   │   ├── files.astro      # File management
│   │   │   ├── messages.astro   # Message center
│   │   │   ├── notifications.astro
│   │   │   ├── calendar.astro   # Calendar
│   │   │   ├── profile.astro    # Profile
│   │   │   └── settings/        # Settings
│   │   └── api/                 # API endpoints
│   │       └── auth/
│   │           ├── login.ts
│   │           ├── register.ts
│   │           ├── forgot-password.ts
│   │           └── reset-password.ts
│   ├── layouts/                 # Layout components
│   │   ├── Layout.astro         # Base layout
│   │   ├── AuthLayout.astro     # Auth layout
│   │   ├── DashboardLayout.astro # Dashboard layout
│   │   └── LegalLayout.astro    # Legal pages layout
│   ├── components/              # UI components
│   │   └── dashboard/
│   │       ├── Sidebar.astro    # Sidebar
│   │       └── Header.astro     # Top navigation
│   ├── styles/                  # Global styles
│   │   └── globals.css
│   └── assets/                  # Static assets
├── public/                      # Public assets
├── tests/                       # Test files
├── astro.config.mjs            # Astro config
├── tailwind.config.mjs         # Tailwind config
├── vitest.config.ts            # Test config
└── package.json

Quick Start

Environment Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-astro.git
cd halolight-astro
pnpm install

Environment Variables

bash
cp .env.example .env.local
env
# .env.local example
PUBLIC_API_URL=/api
PUBLIC_MOCK=true
[email protected]
PUBLIC_DEMO_PASSWORD=123456
PUBLIC_SHOW_DEMO_HINT=true
PUBLIC_APP_TITLE=Admin Pro
PUBLIC_BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:4321

Build for Production

bash
pnpm build
pnpm preview

Demo Account

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Core Functionality

Islands Architecture

Astro's Islands architecture allows pages to be static HTML by default, with JavaScript only added to interactive components:

astro
---
// Static import, no JS
import StaticCard from '../components/StaticCard.astro';
// Interactive component (can be from React/Vue/Svelte)
import Counter from '../components/Counter.tsx';
---

<!-- Pure static, zero JS -->
<StaticCard title="Statistics" />

<!-- Hydrate on page load -->
<Counter client:load />

<!-- Hydrate when visible (lazy load) -->
<Counter client:visible />

<!-- Hydrate when browser is idle -->
<Counter client:idle />

Client Directives:

Directive Behavior Use Case
client:load Hydrate immediately on page load Critical interactions
client:idle Hydrate when browser is idle Non-critical interactions
client:visible Hydrate when element is visible Lazy-loaded components
client:only Client-side rendering only Browser API dependent
client:media Hydrate when media query matches Responsive components

Layout System

astro
---
// layouts/DashboardLayout.astro
import Layout from './Layout.astro';
import Sidebar from '../components/dashboard/Sidebar.astro';
import Header from '../components/dashboard/Header.astro';

interface Props {
  title: string;
  description?: string;
}

const { title, description } = Astro.props;
const currentPath = Astro.url.pathname;
---

<Layout title={title} description={description}>
  <div class="min-h-screen bg-gray-50 dark:bg-gray-900">
    <Sidebar currentPath={currentPath} />
    <div class="lg:pl-64">
      <Header title={title} />
      <main class="p-4 lg:p-6">
        <slot />
      </main>
    </div>
  </div>
</Layout>

API Endpoints

Astro natively supports creating API endpoints:

typescript
// src/pages/api/auth/login.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();
  const { email, password } = body;

  // Validation logic
  if (!email || !password) {
    return new Response(
      JSON.stringify({ success: false, message: 'Email and password are required' }),
      { status: 400, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // Authentication logic...

  return new Response(
    JSON.stringify({
      success: true,
      message: 'Login successful',
      user: { id: 1, name: 'Admin', role: 'admin' },
      token: 'mock_token',
    }),
    { status: 200, headers: { 'Content-Type': 'application/json' } }
  );
};

File-based Routing

File Path URL Description
src/pages/index.astro / Home
src/pages/auth/login.astro /auth/login Login
src/pages/dashboard/index.astro /dashboard Dashboard
src/pages/dashboard/[id].astro /dashboard/:id Dynamic route
src/pages/api/auth/login.ts /api/auth/login API endpoint

Page Routes

Path Page Permission
/ Home Public
/auth/login Login Public
/auth/register Register Public
/auth/forgot-password Forgot Password Public
/auth/reset-password Reset Password Public
/dashboard Dashboard dashboard:view
/dashboard/analytics Analytics analytics:view
/dashboard/users User Management users:view
/dashboard/accounts Account Management accounts:view
/dashboard/documents Document Management documents:view
/dashboard/files File Management files:view
/dashboard/messages Message Center messages:view
/dashboard/notifications Notification Center notifications:view
/dashboard/calendar Calendar calendar:view
/dashboard/profile Profile settings:view
/dashboard/settings Settings settings:view
/privacy Privacy Policy Public
/terms Terms of Service Public

Environment Variables

Configuration

bash
# .env
PUBLIC_API_URL=/api
PUBLIC_MOCK=true
PUBLIC_DEMO_EMAIL=[email protected]
PUBLIC_DEMO_PASSWORD=123456
PUBLIC_SHOW_DEMO_HINT=true
PUBLIC_APP_TITLE=Admin Pro
PUBLIC_BRAND_NAME=Halolight

Variable Reference

Variable Description Default
PUBLIC_API_URL API base URL /api
PUBLIC_MOCK Enable mock data true
PUBLIC_APP_TITLE App title Admin Pro
PUBLIC_BRAND_NAME Brand name Halolight
PUBLIC_DEMO_EMAIL Demo account email -
PUBLIC_DEMO_PASSWORD Demo account password -
PUBLIC_SHOW_DEMO_HINT Show demo hint false

Usage

astro
---
// In .astro files
const apiUrl = import.meta.env.PUBLIC_API_URL;
const isMock = import.meta.env.PUBLIC_MOCK === 'true';
---
typescript
// In .ts files
const apiUrl = import.meta.env.PUBLIC_API_URL;

Common Commands

bash
# Development
pnpm dev              # Start dev server (default port 4321)
pnpm dev --port 3000  # Specify port

# Build
pnpm build            # Production build
pnpm preview          # Preview production build

# Checks
pnpm astro check      # Type check
pnpm lint             # ESLint check
pnpm lint:fix         # ESLint autofix

# Tests
pnpm test             # Run tests
pnpm test:run         # Single run
pnpm test:coverage    # Coverage report

# Astro CLI
pnpm astro add react     # Add React integration
pnpm astro add vue       # Add Vue integration
pnpm astro add tailwind  # Add Tailwind
pnpm astro add mdx       # Add MDX support

Testing

bash
# Run tests
pnpm test

# Generate coverage report
pnpm test --coverage

Testing Examples

typescript
// tests/components/Counter.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from '../../src/components/Counter';

describe('Counter', () => {
  it('renders with initial count', () => {
    render(<Counter />);
    expect(screen.getByText('0')).toBeInTheDocument();
  });

  it('increments count on button click', () => {
    render(<Counter />);
    const button = screen.getByRole('button');
    fireEvent.click(button);
    expect(screen.getByText('1')).toBeInTheDocument();
  });
});

Configuration

Astro Configuration

javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import node from '@astrojs/node';

export default defineConfig({
  integrations: [tailwind()],
  output: 'server',  // SSR mode
  adapter: node({
    mode: 'standalone',
  }),
  server: {
    port: 4321,
    host: true,
  },
});

Output Modes

Mode Description Use Case
static Static site generation (SSG) Blogs, documentation
server Server-side rendering (SSR) Dynamic applications
hybrid Hybrid mode Partially dynamic

Deployment

Deploy with Vercel

bash
# Install adapter
pnpm add @astrojs/vercel

# astro.config.mjs
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  output: 'server',
  adapter: vercel(),
});

Docker

dockerfile
FROM node:20-alpine AS builder

RUN npm install -g pnpm

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

ENV HOST=0.0.0.0
ENV PORT=4321

EXPOSE 4321

CMD ["node", "./dist/server/entry.mjs"]

Other Platforms

CI/CD

Complete GitHub Actions CI workflow configuration:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm astro check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

Content Collections

Astro's built-in content management system with type-safe Markdown/MDX content.

typescript
// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    author: z.string(),
    tags: z.array(z.string()).optional(),
    image: z.string().optional(),
  }),
});

export const collections = {
  blog: blogCollection,
};
astro
---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---

<BlogLayout title={post.data.title}>
  <article>
    <h1>{post.data.title}</h1>
    <time>{post.data.pubDate.toLocaleDateString()}</time>
    <Content />
  </article>
</BlogLayout>

View Transitions

Native View Transitions API support for smooth page animations.

astro
---
// src/layouts/Layout.astro
import { ViewTransitions } from 'astro:transitions';
---

<html>
  <head>
    <ViewTransitions />
  </head>
  <body>
    <slot />
  </body>
</html>
astro
---
// Custom transition animations
---
<div transition:name="hero">
  <h1 transition:animate="slide">Welcome</h1>
</div>

<style>
  /* Custom animations */
  @keyframes slide-in {
    from { transform: translateX(-100%); opacity: 0; }
    to { transform: translateX(0); opacity: 1; }
  }

  ::view-transition-old(hero) {
    animation: slide-out 0.3s ease-out;
  }

  ::view-transition-new(hero) {
    animation: slide-in 0.3s ease-out;
  }
</style>

Middleware

Request interception and processing.

typescript
// src/middleware.ts
import { defineMiddleware, sequence } from 'astro:middleware';

// Authentication middleware
const auth = defineMiddleware(async (context, next) => {
  const token = context.cookies.get('token')?.value;

  // Protected routes
  const protectedPaths = ['/dashboard', '/profile', '/settings'];
  const isProtected = protectedPaths.some(path =>
    context.url.pathname.startsWith(path)
  );

  if (isProtected && !token) {
    return context.redirect('/auth/login');
  }

  // Pass user info to pages
  if (token) {
    context.locals.user = await verifyToken(token);
  }

  return next();
});

// Logger middleware
const logger = defineMiddleware(async (context, next) => {
  const start = Date.now();
  const response = await next();
  const duration = Date.now() - start;

  console.log(`${context.request.method} ${context.url.pathname} - ${duration}ms`);

  return response;
});

// Compose middleware
export const onRequest = sequence(logger, auth);

Performance Optimization

Image Optimization

astro
---
import { Image } from 'astro:assets';
import myImage from '../assets/hero.png';
---

<!-- Auto-optimized images -->
<Image src={myImage} alt="Hero" width={800} height={600} />

<!-- Remote images -->
<Image
  src="https://example.com/image.jpg"
  alt="Remote"
  width={400}
  height={300}
  inferSize
/>

Lazy Loading Components

astro
---
// Use client:visible for lazy loading
import HeavyComponent from '../components/HeavyComponent';
---

<!-- Load only when element is visible -->
<HeavyComponent client:visible />

Preload

astro
---
// Preload critical resources
---
<head>
  <link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin />
  <link rel="preconnect" href="https://api.example.com" />
  <link rel="dns-prefetch" href="https://cdn.example.com" />
</head>

Code Splitting

astro
---
// Dynamically import heavy components
const Chart = await import('../components/Chart.tsx');
---

<Chart.default client:visible data={data} />

FAQ

Q: How do I share state between Islands?

A: Use nanostores or Zustand:

bash
pnpm add nanostores @nanostores/react
typescript
// src/stores/counter.ts
import { atom } from 'nanostores';

export const $counter = atom(0);

export function increment() {
  $counter.set($counter.get() + 1);
}
tsx
// React component
import { useStore } from '@nanostores/react';
import { $counter, increment } from '../stores/counter';

export function Counter() {
  const count = useStore($counter);
  return <button onClick={increment}>{count}</button>;
}

Q: How do I handle form submissions?

A: Use API endpoints:

astro
---
// src/pages/contact.astro
---
<form method="POST" action="/api/contact">
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button type="submit">Submit</button>
</form>
typescript
// src/pages/api/contact.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) => {
  const data = await request.formData();
  const email = data.get('email');
  const message = data.get('message');

  // Handle form data
  await sendEmail({ email, message });

  return new Response(JSON.stringify({ success: true }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  });
};

Q: How do I implement authentication?

A: Use middleware + Cookies:

typescript
// src/middleware.ts
export const onRequest = defineMiddleware(async (context, next) => {
  const token = context.cookies.get('auth-token')?.value;

  if (context.url.pathname.startsWith('/dashboard') && !token) {
    return context.redirect('/auth/login');
  }

  if (token) {
    try {
      const user = await verifyToken(token);
      context.locals.user = user;
    } catch {
      context.cookies.delete('auth-token');
      return context.redirect('/auth/login');
    }
  }

  return next();
});

Q: What if the bundle size is too large?

A: Optimization suggestions:

  1. Check client: directive usage, prefer client:visible or client:idle
  2. Use dynamic imports
  3. Remove unused integrations
  4. Use @playform/compress to compress output
bash
pnpm add @playform/compress
javascript
// astro.config.mjs
import compress from '@playform/compress';

export default defineConfig({
  integrations: [compress()],
});

Comparison with Other Versions

Feature Astro Next.js Vue
Default JS Size 0 KB ~80 KB ~70 KB
Islands Architecture Native support Not supported Not supported (Nuxt)
Multi-framework Components Supported Not supported Not supported
SSG/SSR Supported Supported Supported (Nuxt)
Learning Curve Low Medium Medium
]]>
<![CDATA[AWS Deployment]]> https://halolight.docs.h7ml.cn/en/guide/aws https://halolight.docs.h7ml.cn/en/guide/aws Fri, 19 Dec 2025 04:56:41 GMT AWS Deployment

HaloLight AWS deployment version, enterprise-grade deployment solution for AWS ecosystem.

Features

  • 🟠 AWS Amplify - Fully managed frontend deployment
  • 📦 S3 - Static asset storage
  • 🌐 CloudFront - Global CDN distribution
  • Lambda@Edge - Edge computing
  • 🔐 IAM - Identity and access management
  • 📊 CloudWatch - Monitoring and logging

Quick Start

Method 1: Amplify Console Deploy

  1. Login to AWS Amplify Console
  2. Click "Host web app"
  3. Connect GitHub repository
  4. Configure build settings
  5. Deploy

Method 2: Amplify CLI Deploy

bash
# Install Amplify CLI
npm install -g @aws-amplify/cli

# Configure AWS credentials
amplify configure

# Initialize project
amplify init

# Add hosting
amplify add hosting

# Deploy
amplify publish

Configuration File

amplify.yml

yaml
version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*
      - .next/cache/**/*

Lambda@Edge Functions

typescript
// edge-functions/auth.ts
export async function handler(event: any) {
  const request = event.Records[0].cf.request
  const headers = request.headers

  // Verify authentication
  if (!headers.authorization) {
    return {
      status: '401',
      statusDescription: 'Unauthorized',
      body: 'Unauthorized',
    }
  }

  return request
}

CloudFront Configuration

json
{
  "Origins": {
    "Items": [
      {
        "DomainName": "halolight.s3.amazonaws.com",
        "S3OriginConfig": {
          "OriginAccessIdentity": ""
        }
      }
    ]
  },
  "DefaultCacheBehavior": {
    "ViewerProtocolPolicy": "redirect-to-https",
    "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6"
  }
}

Environment Variables

Set in Amplify Console:

bash
NEXT_PUBLIC_API_URL=https://api.example.com
AWS_REGION=ap-northeast-1

IAM Policy

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "amplify:*",
        "s3:*",
        "cloudfront:*",
        "lambda:*"
      ],
      "Resource": "*"
    }
  ]
}
]]>
<![CDATA[Azure Deployment]]> https://halolight.docs.h7ml.cn/en/guide/azure https://halolight.docs.h7ml.cn/en/guide/azure Fri, 19 Dec 2025 04:56:41 GMT Azure Deployment

HaloLight Azure deployment version, enterprise-grade deployment solution for Microsoft ecosystem.

Features

  • ☁️ Azure Static Web Apps - Static site hosting
  • Azure Functions - Serverless functions
  • 🔐 Azure AD - Enterprise identity authentication
  • 🌐 Azure CDN - Global CDN acceleration
  • 📊 Application Insights - Application monitoring
  • 🔒 Enterprise Security - Microsoft security compliance

Quick Start

Method 1: GitHub Actions Deploy

  1. Fork repository to your GitHub
  2. Create Static Web App in Azure Portal
  3. Connect GitHub repository
  4. Automatically generate GitHub Actions workflow

Method 2: Azure CLI Deploy

bash
# Login to Azure
az login

# Create resource group
az group create --name halolight-rg --location eastasia

# Create Static Web App
az staticwebapp create \
  --name halolight \
  --resource-group halolight-rg \
  --source https://github.com/halolight/halolight-azure \
  --branch main \
  --app-location "/" \
  --output-location ".next" \
  --login-with-github

Configuration File

staticwebapp.config.json

json
{
  "navigationFallback": {
    "rewrite": "/index.html",
    "exclude": ["/api/*", "/_next/*", "/static/*"]
  },
  "routes": [
    {
      "route": "/api/*",
      "allowedRoles": ["authenticated"]
    }
  ],
  "globalHeaders": {
    "X-Frame-Options": "DENY",
    "X-Content-Type-Options": "nosniff"
  },
  "mimeTypes": {
    ".json": "application/json"
  }
}

Azure Functions

typescript
// api/hello/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  context.res = {
    status: 200,
    body: { message: "Hello from Azure Functions!" }
  }
}

export default httpTrigger

Azure AD Integration

typescript
// Configure Azure AD authentication
const msalConfig = {
  auth: {
    clientId: process.env.AZURE_AD_CLIENT_ID,
    authority: `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}`,
    redirectUri: process.env.AZURE_AD_REDIRECT_URI,
  },
}

Environment Variables

Set in Azure Portal:

bash
NEXT_PUBLIC_API_URL=https://your-app.azurestaticapps.net
AZURE_AD_CLIENT_ID=your-client-id
AZURE_AD_TENANT_ID=your-tenant-id
]]>
<![CDATA[TypeScript tRPC Gateway API]]> https://halolight.docs.h7ml.cn/en/guide/bff https://halolight.docs.h7ml.cn/en/guide/bff Fri, 19 Dec 2025 04:56:41 GMT TypeScript tRPC Gateway API

A type-safe API gateway built on tRPC 11 + Express 5, providing unified end-to-end type-safe interface layer for frontend applications.

API Documentation: https://halolight-bff.h7ml.cn

GitHub: https://github.com/halolight/halolight-bff

Features

  • 🎯 End-to-End Type Safety - tRPC provides complete type inference from server to client with zero runtime overhead
  • 🔐 JWT Dual Token Auth - Access Token + Refresh Token auto-renewal with RBAC permission control
  • 📡 Service Gateway Aggregation - Unified aggregation of multiple backend services (Python/Java/Go/Bun) with automatic failover
  • Zod Data Validation - Automatic input validation, type-safe with detailed error messages
  • 🔄 SuperJSON Serialization - Automatic handling of Date, Map, Set, BigInt, RegExp and other complex types
  • 🎭 Request Batching - Automatic batch processing of multiple requests to reduce network overhead
  • 📊 Distributed Tracing - Automatic Trace ID propagation with complete request chain logging
  • 🐳 Docker Support - Containerized deployment with production-grade configuration

Tech Stack

Technology Version Description
TypeScript 5.9 Programming Language
tRPC 11 RPC Framework
Zod - Data Validation
Express 5 Web Server
SuperJSON - Serialization
JWT - Authentication
Pino - Logging System

Quick Start

Requirements

  • Node.js >= 20.0
  • pnpm >= 8.0
  • At least one backend service (Python/Java/Go/Bun)

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-bff.git
cd halolight-bff

# Install dependencies
pnpm install

Environment Variables

bash
cp .env.example .env
env
# Server configuration
PORT=3002
HOST=0.0.0.0
NODE_ENV=development

# JWT secret (must change in production)
JWT_SECRET=your-super-secret-key-at-least-32-characters-long
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# CORS configuration
CORS_ORIGIN=*

# Log level
LOG_LEVEL=info

# Backend service registry (configure at least one)
HALOLIGHT_API_PYTHON_URL=http://localhost:8000
HALOLIGHT_API_BUN_URL=http://localhost:3000
HALOLIGHT_API_JAVA_URL=http://localhost:8080
HALOLIGHT_API_NESTJS_URL=http://localhost:3001
HALOLIGHT_API_NODE_URL=http://localhost:3003
HALOLIGHT_API_GO_URL=http://localhost:8081

Database Initialization

No database required (API gateway does not directly access database).

Start Service

bash
# Development mode (hot reload)
pnpm dev

# Production mode
pnpm build
pnpm start

Visit http://localhost:3002

Project Structure

halolight-bff/
├── src/
│   ├── index.ts              # Application entry
│   ├── server.ts             # Express server + tRPC adapter
│   ├── trpc.ts               # tRPC instance and procedure definitions
│   ├── context.ts            # Context creation (user, tracing, services)
│   ├── routers/
│   │   ├── index.ts          # Root router (combining all modules)
│   │   ├── auth.ts           # Authentication module (8 endpoints)
│   │   ├── users.ts          # User management (8 endpoints)
│   │   ├── dashboard.ts      # Dashboard statistics (9 endpoints)
│   │   ├── permissions.ts    # Permission management (7 endpoints)
│   │   ├── roles.ts          # Role management (8 endpoints)
│   │   ├── teams.ts          # Team management (9 endpoints)
│   │   ├── folders.ts        # Folder management (8 endpoints)
│   │   ├── files.ts          # File management (9 endpoints)
│   │   ├── documents.ts      # Document management (10 endpoints)
│   │   ├── calendar.ts       # Calendar events (10 endpoints)
│   │   ├── notifications.ts  # Notifications (7 endpoints)
│   │   └── messages.ts       # Messaging/chat (9 endpoints)
│   ├── middleware/
│   │   └── auth.ts           # JWT authentication/authorization middleware
│   ├── services/
│   │   ├── httpClient.ts     # HTTP client (backend communication)
│   │   └── serviceRegistry.ts # Backend service registry
│   └── schemas/
│       ├── index.ts          # Schema exports
│       └── common.ts         # Common Zod schemas (pagination, sorting, response)
├── .env.example              # Environment variables template
├── .github/workflows/        # CI/CD configuration
├── Dockerfile                # Docker image build
├── docker-compose.yml        # Docker Compose configuration
├── package.json              # Dependencies configuration
└── tsconfig.json             # TypeScript configuration

API Modules

HaloLight BFF provides 12 core business modules covering 100+ tRPC endpoints:

Module Endpoints Description
auth 8 Login, register, token refresh, logout, password management
users 8 User CRUD, role/status management, profile
dashboard 9 Statistics, visit trends, sales data, tasks, calendar
permissions 7 Permission CRUD, tree structure, module permissions, batch operations
roles 8 Role CRUD, permission assignment, user association
teams 9 Team CRUD, member management, invitations, permissions
folders 8 Folder CRUD, tree structure, move, breadcrumb
files 9 File CRUD, upload, download, move, copy, share
documents 10 Document CRUD, version control, collaboration, sharing
calendar 10 Event CRUD, attendee management, RSVP, reminders
notifications 7 Notification list, unread count, mark read, batch delete
messages 9 Conversation management, message CRUD, send, read status

Authentication Endpoints

Procedure Type Description Permission
auth.login mutation User login Public
auth.register mutation User registration Public
auth.refresh mutation Refresh token Public
auth.logout mutation Logout Authenticated
auth.forgotPassword mutation Forgot password Public
auth.resetPassword mutation Reset password Public
auth.verifyEmail mutation Verify email Public
auth.changePassword mutation Change password Authenticated

User Management Endpoints

Procedure Type Description Permission
users.list query Get user list users:view
users.byId query Get user details users:view
users.me query Get current user Authenticated
users.create mutation Create user users:create
users.update mutation Update user users:update
users.delete mutation Delete user users:delete
users.updateRole mutation Update user role users:update
users.updateStatus mutation Update user status users:update

Complete Endpoint List

Dashboard - 9 Endpoints

Procedure Type Description
dashboard.getStats query Statistics (users, documents, files, tasks)
dashboard.getVisits query Visit trends (7/30 days)
dashboard.getSales query Sales data (line chart)
dashboard.getPieData query Pie chart data (category distribution)
dashboard.getTasks query Todo task list
dashboard.getCalendar query Today's calendar
dashboard.getActivities query Recent activities
dashboard.getNotifications query Latest notifications
dashboard.getProgress query Project progress

Permissions - 7 Endpoints

Procedure Type Description
permissions.list query Get permission list
permissions.tree query Get permission tree
permissions.byId query Get permission details
permissions.create mutation Create permission
permissions.update mutation Update permission
permissions.delete mutation Delete permission
permissions.modules query Get permission modules

Roles - 8 Endpoints

Procedure Type Description
roles.list query Get role list
roles.byId query Get role details
roles.create mutation Create role
roles.update mutation Update role
roles.delete mutation Delete role
roles.assignPermissions mutation Assign permissions
roles.removePermissions mutation Remove permissions
roles.users query Get users in role

Teams - 9 Endpoints

Procedure Type Description
teams.list query Get team list
teams.byId query Get team details
teams.create mutation Create team
teams.update mutation Update team
teams.delete mutation Delete team
teams.addMember mutation Add member
teams.removeMember mutation Remove member
teams.updateMemberRole mutation Update member role
teams.members query Get team members

Folders - 8 Endpoints

Procedure Type Description
folders.list query Get folder list
folders.tree query Get folder tree
folders.byId query Get folder details
folders.create mutation Create folder
folders.update mutation Update folder
folders.delete mutation Delete folder
folders.move mutation Move folder
folders.breadcrumb query Get breadcrumb path

Files - 9 Endpoints

Procedure Type Description
files.list query Get file list
files.byId query Get file details
files.upload mutation Upload file
files.update mutation Update file info
files.delete mutation Delete file
files.move mutation Move file
files.copy mutation Copy file
files.download query Get download link
files.share mutation Share file

Documents - 10 Endpoints

Procedure Type Description
documents.list query Get document list
documents.byId query Get document details
documents.create mutation Create document
documents.update mutation Update document
documents.delete mutation Delete document
documents.versions query Get version history
documents.restore mutation Restore version
documents.share mutation Share document
documents.unshare mutation Unshare document
documents.collaborators query Get collaborators

Calendar - 10 Endpoints

Procedure Type Description
calendar.events query Get event list
calendar.byId query Get event details
calendar.create mutation Create event
calendar.update mutation Update event
calendar.delete mutation Delete event
calendar.addAttendee mutation Add attendee
calendar.removeAttendee mutation Remove attendee
calendar.rsvp mutation RSVP response
calendar.setReminder mutation Set reminder
calendar.byMonth query Get events by month

Notifications - 7 Endpoints

Procedure Type Description
notifications.list query Get notification list
notifications.unreadCount query Get unread count
notifications.markRead mutation Mark as read
notifications.markAllRead mutation Mark all as read
notifications.delete mutation Delete notification
notifications.deleteAll mutation Delete all
notifications.preferences query Get notification preferences

Messages - 9 Endpoints

Procedure Type Description
messages.conversations query Get conversation list
messages.byConversation query Get conversation messages
messages.send mutation Send message
messages.markRead mutation Mark as read
messages.delete mutation Delete message
messages.createConversation mutation Create conversation
messages.deleteConversation mutation Delete conversation
messages.search query Search messages
messages.unreadCount query Get unread count

Core Concepts

tRPC Procedures

tRPC provides three procedure types:

typescript
// Public endpoint - no authentication required
export const publicProcedure = t.procedure;

// Protected endpoint - requires valid JWT
export const protectedProcedure = t.procedure.use(isAuthenticated);

// Admin endpoint - requires admin role
export const adminProcedure = t.procedure.use(isAdmin);

Usage Example:

typescript
export const usersRouter = router({
  // Query - fetch data
  list: protectedProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().default(10),
      keyword: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      // ctx.user contains authenticated user info
      const client = ctx.services.getDefault();
      const data = await client.get('/api/users', { query: input });
      return { code: 200, message: 'success', data };
    }),

  // Mutation - modify data
  create: adminProcedure
    .input(z.object({
      name: z.string().min(2),
      email: z.string().email(),
      role: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.post('/api/users', { body: input });
      return { code: 201, message: 'Created', data };
    }),
});

Context

Each request creates an independent context:

typescript
interface Context {
  req: Request;                    // Express request object
  res: Response;                   // Express response object
  user: JWTPayload | null;         // Authenticated user (via JWT)
  traceId: string;                 // Distributed tracing ID (UUID)
  services: ServiceRegistry;       // Backend service registry
}

Context Creation Flow:

  1. Parse JWT Token from Authorization header
  2. Verify token validity and extract user information
  3. Generate unique traceId (for distributed tracing)
  4. Inject ServiceRegistry (backend service collection)

JWT Token Structure

typescript
interface JWTPayload {
  id: string;                      // User ID
  name: string;                    // Username
  email: string;                   // Email
  role: {
    id: string;                    // Role ID
    name: string;                  // Role name (e.g., admin, user)
    label: string;                 // Role display name
    permissions: string[];         // Permission list (e.g., ["users:*", "documents:view"])
  };
}

Token Usage:

typescript
// Client sends request
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// Server automatically parses and injects into ctx.user
const userId = ctx.user.id;
const userPermissions = ctx.user.role.permissions;

Permission System

Supports flexible wildcard permission matching:

Permission Format Description Example
* All permissions (super admin) Can perform any operation
{resource}:* All operations on module users:* = all user module permissions
{resource}:{action} Specific operation users:view = view users only

Permission Check Example:

typescript
// Check permission in middleware
export const requirePermission = (permission: string) => {
  return t.middleware(({ ctx, next }) => {
    if (!ctx.user) {
      throw new TRPCError({ code: 'UNAUTHORIZED' });
    }

    const hasPermission = ctx.user.role.permissions.some(p =>
      p === '*' ||
      p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    );

    if (!hasPermission) {
      throw new TRPCError({ code: 'FORBIDDEN' });
    }

    return next();
  });
};

// Usage
export const deleteUser = protectedProcedure
  .use(requirePermission('users:delete'))
  .input(z.object({ id: z.string() }))
  .mutation(async ({ input, ctx }) => {
    // Only users with users:delete permission can execute
  });

Service Registry and Discovery

Configure multiple backend services via environment variables:

bash
# Python FastAPI
HALOLIGHT_API_PYTHON_URL=http://api-python:8000

# Bun Hono
HALOLIGHT_API_BUN_URL=http://api-bun:3000

# Java Spring Boot
HALOLIGHT_API_JAVA_URL=http://api-java:8080

# Go Fiber
HALOLIGHT_API_GO_URL=http://api-go:8081

Service Priority: By configuration order, the first available service is used as default.

Usage Example:

typescript
// Use default service (highest priority)
const client = ctx.services.getDefault();
const data = await client.get('/api/users');

// Use specific service
const pythonClient = ctx.services.get('python');
const stats = await pythonClient.get('/api/dashboard/stats');

// Failover: automatically switch to next service if default is unavailable
try {
  const data = await ctx.services.getDefault().get('/api/users');
} catch (error) {
  // ServiceRegistry automatically retries other services
}

Response Format

All APIs follow a unified response structure:

typescript
// Standard response
interface APIResponse<T> {
  code: number;        // HTTP status code (200, 201, 400, 500...)
  message: string;     // Human-readable message (success, error, ...)
  data: T | null;      // Response data (on success) or null (on failure)
}

// Paginated response
interface PaginatedResponse<T> {
  code: number;
  message: string;
  data: {
    list: T[];         // Data list
    total: number;     // Total record count
    page: number;      // Current page number
    limit: number;     // Items per page
    totalPages?: number; // Total pages (optional)
  };
}

Examples:

typescript
// Success response
{
  "code": 200,
  "message": "success",
  "data": {
    "id": "1",
    "name": "John Doe",
    "email": "[email protected]"
  }
}

// Paginated response
{
  "code": 200,
  "message": "success",
  "data": {
    "list": [{ "id": "1", "name": "User 1" }],
    "total": 100,
    "page": 1,
    "limit": 10,
    "totalPages": 10
  }
}

// Error response (tRPC auto-formatted)
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Not authenticated"
  }
}

Authentication

JWT Dual Token

Access Token:  15 minutes validity, used for API requests
Refresh Token: 7 days validity, used to refresh Access Token

Request Header

http
Authorization: Bearer <access_token>

Refresh Flow

typescript
// Client example
const refreshToken = async () => {
  const refreshToken = localStorage.getItem('refreshToken');
  const result = await trpc.auth.refresh.mutate({ refreshToken });

  localStorage.setItem('accessToken', result.data.accessToken);
  localStorage.setItem('refreshToken', result.data.refreshToken);

  return result.data.accessToken;
};

// tRPC client configuration - auto refresh
const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      async headers() {
        let token = localStorage.getItem('accessToken');

        // Auto refresh if token expired
        if (isTokenExpired(token)) {
          token = await refreshToken();
        }

        return {
          authorization: `Bearer ${token}`,
        };
      },
    }),
  ],
});

Error Handling

tRPC Error Types

typescript
import { TRPCError } from '@trpc/server';

// 400 - Bad request
throw new TRPCError({
  code: 'BAD_REQUEST',
  message: 'Invalid input',
});

// 401 - Unauthorized
throw new TRPCError({
  code: 'UNAUTHORIZED',
  message: 'Not authenticated',
});

// 403 - Forbidden
throw new TRPCError({
  code: 'FORBIDDEN',
  message: 'Insufficient permissions',
});

// 404 - Not found
throw new TRPCError({
  code: 'NOT_FOUND',
  message: 'Resource not found',
});

// 409 - Conflict
throw new TRPCError({
  code: 'CONFLICT',
  message: 'Email already exists',
});

// 500 - Internal server error
throw new TRPCError({
  code: 'INTERNAL_SERVER_ERROR',
  message: 'Something went wrong',
});

Error Response Format

json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Not authenticated",
    "data": {
      "code": "UNAUTHORIZED",
      "httpStatus": 401,
      "path": "auth.login"
    }
  }
}

Client Usage

React + @tanstack/react-query

typescript
import { createTRPCReact } from '@trpc/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from 'halolight-bff';

// Create tRPC React hooks
const trpc = createTRPCReact<AppRouter>();

// Create tRPC client
const trpcClient = trpc.createClient({
  transformer: superjson, // Support Date, Map, Set, etc.
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      headers() {
        return {
          authorization: `Bearer ${localStorage.getItem('token')}`,
        };
      },
    }),
  ],
});

// Create React Query client
const queryClient = new QueryClient();

// Root component
function App() {
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <UserList />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

// Use tRPC hooks
function UserList() {
  // Query - auto-managed loading state, caching, refetching
  const { data, isLoading, error } = trpc.users.list.useQuery({
    page: 1,
    limit: 10,
  });

  // Mutation - auto-managed loading state, error handling
  const createUser = trpc.users.create.useMutation({
    onSuccess: () => {
      // Auto refresh user list
      trpc.users.list.invalidate();
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <button onClick={() => createUser.mutate({
        name: 'New User',
        email: '[email protected]',
        role: 'user',
      })}>
        Create User
      </button>

      {data?.data.list.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Next.js App Router

typescript
// app/api/trpc/[trpc]/route.ts - tRPC API route
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  });

export { handler as GET, handler as POST };

// app/providers.tsx - tRPC Provider
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import superjson from 'superjson';
import type { AppRouter } from '@/server/routers';

const trpc = createTRPCReact<AppRouter>();

export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      transformer: superjson,
      links: [
        httpBatchLink({
          url: '/api/trpc',
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

// app/page.tsx - Server Component
import { createCaller } from '@/server/routers';

export default async function Page() {
  const caller = createCaller({ req: {}, res: {}, user: null });
  const stats = await caller.dashboard.getStats();

  return <div>Total Users: {stats.data.totalUsers}</div>;
}

Vue 3 + TanStack Query

typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query';
import superjson from 'superjson';
import type { AppRouter } from 'halolight-bff';

// Create tRPC client
const trpc = createTRPCProxyClient<AppRouter>({
  transformer: superjson,
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      headers() {
        return {
          authorization: `Bearer ${localStorage.getItem('token')}`,
        };
      },
    }),
  ],
});

// Use in component
export default {
  setup() {
    const queryClient = useQueryClient();

    // Query
    const { data, isLoading } = useQuery({
      queryKey: ['users', { page: 1 }],
      queryFn: () => trpc.users.list.query({ page: 1, limit: 10 }),
    });

    // Mutation
    const createUser = useMutation({
      mutationFn: (user) => trpc.users.create.mutate(user),
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ['users'] });
      },
    });

    return { data, isLoading, createUser };
  },
};

Vanilla TypeScript

typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from 'halolight-bff';
import superjson from 'superjson';

const client = createTRPCProxyClient<AppRouter>({
  transformer: superjson,
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      headers() {
        return {
          authorization: `Bearer ${localStorage.getItem('token')}`,
        };
      },
    }),
  ],
});

// Usage (full type inference)
const users = await client.users.list.query({ page: 1 });
console.log(users.data.list); // TS automatically infers type

const newUser = await client.users.create.mutate({
  name: 'John',
  email: '[email protected]',
  role: 'user',
});

Development Guide

Adding New Router

  1. Create new router file:
typescript
// src/routers/products.ts
import { z } from 'zod';
import { router, protectedProcedure, adminProcedure } from '../trpc';

export const productsRouter = router({
  // Query - get product list
  list: protectedProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().default(10),
      category: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.get('/api/products', { query: input });
      return { code: 200, message: 'success', data };
    }),

  // Query - get product details
  byId: protectedProcedure
    .input(z.object({
      id: z.string(),
    }))
    .query(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.get(`/api/products/${input.id}`);
      return { code: 200, message: 'success', data };
    }),

  // Mutation - create product (requires admin permission)
  create: adminProcedure
    .input(z.object({
      name: z.string().min(2),
      price: z.number().positive(),
      category: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.post('/api/products', { body: input });
      return { code: 201, message: 'Created', data };
    }),

  // Mutation - update product
  update: adminProcedure
    .input(z.object({
      id: z.string(),
      name: z.string().min(2).optional(),
      price: z.number().positive().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { id, ...updateData } = input;
      const client = ctx.services.getDefault();
      const data = await client.put(`/api/products/${id}`, { body: updateData });
      return { code: 200, message: 'Updated', data };
    }),

  // Mutation - delete product
  delete: adminProcedure
    .input(z.object({
      id: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      await client.delete(`/api/products/${input.id}`);
      return { code: 200, message: 'Deleted', data: null };
    }),
});
  1. Register in root router:
typescript
// src/routers/index.ts
import { router } from '../trpc';
import { authRouter } from './auth';
import { usersRouter } from './users';
import { productsRouter } from './products'; // Import new router

export const appRouter = router({
  auth: authRouter,
  users: usersRouter,
  products: productsRouter, // Register new router
  // ... other routers
});

export type AppRouter = typeof appRouter;
  1. Client usage:
typescript
// Type auto-inference, no manual definition needed
const products = await trpc.products.list.query({ page: 1 });
const product = await trpc.products.byId.query({ id: '1' });
const newProduct = await trpc.products.create.mutate({
  name: 'iPhone 15',
  price: 999,
  category: 'electronics',
});

Adding Custom Middleware

typescript
// src/middleware/rateLimit.ts
import { TRPCError } from '@trpc/server';
import { t } from '../trpc';

const rateLimitMap = new Map<string, { count: number; resetAt: number }>();

export const rateLimit = (maxRequests: number, windowMs: number) => {
  return t.middleware(({ ctx, next }) => {
    const key = ctx.user?.id || ctx.req.ip;
    const now = Date.now();

    const record = rateLimitMap.get(key);

    if (!record || now > record.resetAt) {
      rateLimitMap.set(key, { count: 1, resetAt: now + windowMs });
      return next();
    }

    if (record.count >= maxRequests) {
      throw new TRPCError({
        code: 'TOO_MANY_REQUESTS',
        message: 'Rate limit exceeded',
      });
    }

    record.count++;
    return next();
  });
};

// Usage
export const limitedProcedure = protectedProcedure.use(
  rateLimit(10, 60000) // Max 10 requests per minute
);

Adding Schema Validation

typescript
// src/schemas/product.ts
import { z } from 'zod';

export const productSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(100),
  price: z.number().positive(),
  category: z.enum(['electronics', 'clothing', 'books']),
  stock: z.number().int().nonnegative(),
  createdAt: z.date(),
  updatedAt: z.date(),
});

export const createProductSchema = productSchema.omit({
  id: true,
  createdAt: true,
  updatedAt: true,
});

export const updateProductSchema = createProductSchema.partial();

// Use in router
export const productsRouter = router({
  create: adminProcedure
    .input(createProductSchema)
    .mutation(async ({ input, ctx }) => {
      // input is already validated by Zod, type-safe
    }),

  update: adminProcedure
    .input(z.object({
      id: z.string(),
      data: updateProductSchema,
    }))
    .mutation(async ({ input, ctx }) => {
      // ...
    }),
});

Common Commands

bash
# Development
pnpm dev              # Start dev server (hot reload)
pnpm dev:watch        # Start dev server (file watch)

# Build
pnpm build            # Build for production
pnpm start            # Start production server

# Testing
pnpm test             # Run tests
pnpm test:watch       # Run tests in watch mode
pnpm test:coverage    # Generate test coverage

# Code quality
pnpm lint             # Run ESLint
pnpm lint:fix         # Auto-fix lint errors
pnpm type-check       # TypeScript type checking
pnpm format           # Prettier code formatting

Deployment

Docker

bash
# Build image
docker build -t halolight-bff .

# Run container
docker run -p 3002:3002 \
  -e JWT_SECRET=your-secret-key \
  -e HALOLIGHT_API_PYTHON_URL=http://api-python:8000 \
  halolight-bff

Docker Compose

yaml
# docker-compose.yml
version: '3.8'

services:
  bff:
    build: .
    ports:
      - "3002:3002"
    environment:
      - NODE_ENV=production
      - JWT_SECRET=${JWT_SECRET}
      - HALOLIGHT_API_PYTHON_URL=http://api-python:8000
      - HALOLIGHT_API_BUN_URL=http://api-bun:3000
      - HALOLIGHT_API_JAVA_URL=http://api-java:8080
    depends_on:
      - api-python
      - api-bun
      - api-java
    restart: unless-stopped

  api-python:
    image: halolight-api-python
    ports:
      - "8000:8000"

  api-bun:
    image: halolight-api-bun
    ports:
      - "3000:3000"

  api-java:
    image: halolight-api-java
    ports:
      - "8080:8080"
bash
docker-compose up -d

Production Configuration

env
NODE_ENV=production
PORT=3002
HOST=0.0.0.0

# Strong secret (at least 32 characters)
JWT_SECRET=your-production-secret-key-with-at-least-32-characters
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# Restrict CORS
CORS_ORIGIN=https://your-frontend.com

# Production logging
LOG_LEVEL=warn

# Backend services
HALOLIGHT_API_PYTHON_URL=https://api-python.production.com
HALOLIGHT_API_BUN_URL=https://api-bun.production.com
HALOLIGHT_API_JAVA_URL=https://api-java.production.com

Performance Optimization

1. Enable Request Batching

tRPC automatically batches multiple concurrent requests to reduce network overhead:

typescript
// Client configuration
const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      maxURLLength: 2083, // Max URL length
    }),
  ],
});

// These three requests are automatically batched into one HTTP request
const [users, stats, notifications] = await Promise.all([
  trpc.users.list.query({ page: 1 }),
  trpc.dashboard.getStats.query(),
  trpc.notifications.unreadCount.query(),
]);

2. Use DataLoader to Avoid N+1 Queries

typescript
import DataLoader from 'dataloader';

// Create DataLoader
const userLoader = new DataLoader(async (ids: string[]) => {
  const users = await db.user.findMany({
    where: { id: { in: ids } },
  });
  return ids.map(id => users.find(u => u.id === id));
});

// Inject in context
export const createContext = (opts: CreateExpressContextOptions) => {
  return {
    ...opts,
    loaders: {
      user: userLoader,
    },
  };
};

// Use in router
export const postsRouter = router({
  list: protectedProcedure.query(async ({ ctx }) => {
    const posts = await db.post.findMany();

    // Batch load author info, avoid N+1 queries
    const postsWithAuthors = await Promise.all(
      posts.map(async (post) => ({
        ...post,
        author: await ctx.loaders.user.load(post.authorId),
      }))
    );

    return postsWithAuthors;
  }),
});

3. Caching Strategy

typescript
// Use Redis caching
import Redis from 'ioredis';

const redis = new Redis();

export const dashboardRouter = router({
  getStats: protectedProcedure.query(async ({ ctx }) => {
    const cacheKey = `dashboard:stats:${ctx.user.id}`;

    // Try to get from cache
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // Fetch from backend service
    const client = ctx.services.getDefault();
    const data = await client.get('/api/dashboard/stats');

    // Cache for 5 minutes
    await redis.setex(cacheKey, 300, JSON.stringify(data));

    return data;
  }),
});

4. Rate Limiting

typescript
import rateLimit from 'express-rate-limit';

// Configure global rate limiting in Express
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Max 100 requests
  message: 'Too many requests from this IP',
});

app.use('/trpc', limiter);

Security Best Practices

1. Use Strong JWT Secret

bash
# Generate strong secret (at least 32 characters)
openssl rand -base64 32

# Configure in .env
JWT_SECRET=your-generated-secret-key-with-at-least-32-characters

2. Enable HTTPS

typescript
// Force HTTPS in production
if (process.env.NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (!req.secure) {
      return res.redirect(`https://${req.headers.host}${req.url}`);
    }
    next();
  });
}

3. Restrict CORS

bash
# Allow only specific origin
CORS_ORIGIN=https://your-frontend.com

# Or multiple origins (comma-separated)
CORS_ORIGIN=https://app1.com,https://app2.com

4. Input Validation

typescript
// Strictly validate all inputs with Zod
export const createUser = protectedProcedure
  .input(z.object({
    name: z.string().min(2).max(50),
    email: z.string().email(),
    age: z.number().int().positive().max(150),
    role: z.enum(['admin', 'user']),
  }))
  .mutation(async ({ input, ctx }) => {
    // input is strictly validated
  });

5. Log Sanitization

typescript
// Use Pino redact configuration
const logger = pino({
  redact: {
    paths: [
      'req.headers.authorization',
      'req.body.password',
      'req.body.token',
      'res.headers["set-cookie"]',
    ],
    remove: true, // Completely remove sensitive fields
  },
});

Observability

Logging System

typescript
// Use Pino structured logging
import pino from 'pino';
import pinoHttp from 'pino-http';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'SYS:standard',
      ignore: 'pid,hostname',
    },
  },
});

// HTTP request logging
app.use(pinoHttp({ logger }));

// Use in router
export const usersRouter = router({
  create: adminProcedure.mutation(async ({ input, ctx }) => {
    logger.info({ userId: ctx.user.id, input }, 'Creating user');

    try {
      const data = await createUser(input);
      logger.info({ userId: ctx.user.id, data }, 'User created');
      return data;
    } catch (error) {
      logger.error({ userId: ctx.user.id, error }, 'Failed to create user');
      throw error;
    }
  }),
});

Health Check

typescript
// Health check endpoint
app.get('/health', async (req, res) => {
  try {
    // Check backend service connections
    const services = await Promise.all([
      fetch(`${process.env.HALOLIGHT_API_PYTHON_URL}/health`),
      fetch(`${process.env.HALOLIGHT_API_BUN_URL}/health`),
    ]);

    const allHealthy = services.every(s => s.ok);

    res.status(allHealthy ? 200 : 503).json({
      status: allHealthy ? 'healthy' : 'unhealthy',
      timestamp: new Date().toISOString(),
      services: {
        python: services[0].ok,
        bun: services[1].ok,
      },
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message,
    });
  }
});

Monitoring Metrics

typescript
// Prometheus metrics
import promClient from 'prom-client';

// Create registry
const register = new promClient.Registry();

// Collect default metrics
promClient.collectDefaultMetrics({ register });

// Custom metrics
const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

Troubleshooting

Q: Port Already in Use

A: Change PORT in .env or terminate the process using the port:

bash
# Find process using port
lsof -i :3002

# Kill process
kill -9 <PID>

# Or change port
echo "PORT=3003" >> .env

Q: CORS Errors

A: Update CORS_ORIGIN in .env to allow your origin:

bash
# Development - allow all origins
CORS_ORIGIN=*

# Production - specify origin
CORS_ORIGIN=https://your-frontend.com

Q: Token Verification Fails

A: Ensure JWT_SECRET is consistent across all environments:

bash
# Check JWT_SECRET consistency
echo $JWT_SECRET

# Regenerate token
curl -X POST http://localhost:3002/trpc/auth.login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password"}'

Q: Backend Service Connection Failed

A: Check if backend services are running and URLs are configured correctly:

bash
# Check service health
curl http://localhost:8000/health
curl http://localhost:3000/health

# Check environment variables
echo $HALOLIGHT_API_PYTHON_URL
echo $HALOLIGHT_API_BUN_URL

# Test connection
curl http://localhost:3002/health

Q: Type Inference Not Working

A: Ensure AppRouter type is correctly exported and imported in client:

typescript
// Server - src/routers/index.ts
export const appRouter = router({
  // ... routers
});

export type AppRouter = typeof appRouter; // Must export type

// Client - ensure importing from correct path
import type { AppRouter } from 'halolight-bff'; // NPM package
// or
import type { AppRouter } from '@/server/routers'; // Monorepo

Comparison with Other Gateways

Feature tRPC BFF GraphQL REST API gRPC
Type Safety ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
Developer Experience ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐
Performance ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Learning Curve ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
Ecosystem ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
Documentation ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
]]>
<![CDATA[Cloudflare Version]]> https://halolight.docs.h7ml.cn/en/guide/cloudflare https://halolight.docs.h7ml.cn/en/guide/cloudflare Fri, 19 Dec 2025 04:56:41 GMT Cloudflare Version

HaloLight Cloudflare version is built on Next.js 15 App Router + React 19, using @opennextjs/cloudflare adapter for Cloudflare Workers/Pages edge runtime, enabling global low-latency access.

Live Preview: https://halolight-cloudflare.h7ml.cn/

GitHub: https://github.com/halolight/halolight-cloudflare

Differences from Original

Feature Original (Next.js) Cloudflare Version
Next.js 14.x 15.5.x
React 18.x 19.x
Runtime Node.js (Vercel) Cloudflare Workers (Edge)
Deployment Platform Vercel / Docker Cloudflare Pages
Development Tools webpack Turbopack
Deployment Command pnpm build && pnpm start pnpm deploy
SSR Location Server/Serverless Global edge nodes
Cold Start Platform-dependent < 50ms

Tech Stack

Technology Version Description
Next.js 15.5.x React full-stack framework
React 19.x UI library
TypeScript 5.x Type safety
Tailwind CSS 4.x Atomic CSS
@opennextjs/cloudflare 1.x Cloudflare adapter layer
Wrangler 4.x Cloudflare CLI
shadcn/ui latest UI component library
Zustand 5.x State management
TanStack Query 5.x Server state
Vitest 4.x Unit testing
Mock.js 1.x Data mocking

Directory Structure

halolight-cloudflare/
├── src/
│   ├── app/                      # App Router pages
│   │   ├── (dashboard)/          # Dashboard route group
│   │   ├── (auth)/               # Auth route group
│   │   ├── (legal)/              # Legal terms route group
│   │   ├── layout.tsx            # Root layout
│   │   └── page.tsx              # Homepage
│   ├── components/               # React components
│   │   ├── ui/                   # shadcn/ui components
│   │   ├── layout/               # Layout components
│   │   └── dashboard/            # Dashboard components
│   ├── hooks/                    # React Hooks
│   ├── stores/                   # Zustand Stores
│   ├── lib/                      # Utilities
│   ├── mock/                     # Mock data
│   ├── providers/                # Context Providers
│   ├── config/                   # Configuration files
│   └── __tests__/                # Unit tests
├── public/                       # Static assets
├── .github/workflows/            # GitHub Actions CI
├── .open-next/                   # OpenNext build output (auto-generated)
├── coverage/                     # Test coverage (auto-generated)
├── cloudflare-env.d.ts           # Cloudflare environment types
├── vitest.config.ts              # Vitest test configuration
├── open-next.config.ts           # OpenNext configuration
├── wrangler.jsonc                # Wrangler configuration
├── next.config.ts                # Next.js configuration
└── package.json

Quick Start

Requirements

  • Node.js >= 18
  • pnpm >= 8
  • Wrangler CLI (requires Cloudflare login)

Installation

bash
git clone https://github.com/halolight/halolight-cloudflare.git
cd halolight-cloudflare
pnpm install

Environment Variables

bash
cp .dev.vars.example .dev.vars
bash
# .dev.vars example
NEXT_PUBLIC_API_URL=/api
NEXT_PUBLIC_MOCK=true
NEXT_PUBLIC_APP_TITLE=HaloLight
NEXT_PUBLIC_BRAND_NAME=HaloLight
NEXT_PUBLIC_DEMO_EMAIL=[email protected]
NEXT_PUBLIC_DEMO_PASSWORD=Admin@123

Start Development

bash
pnpm dev

Visit http://localhost:3000

Local Preview (Edge Environment)

bash
pnpm preview

Simulates Cloudflare Workers environment to detect Edge Runtime compatibility issues.

Deploy to Cloudflare

bash
wrangler login   # Login required for first time
pnpm deploy      # Build and deploy

Common Scripts

bash
pnpm dev          # Start development server (Turbopack, Node.js environment)
pnpm build        # Next.js production build
pnpm preview      # Local preview of Cloudflare environment
pnpm deploy       # Deploy to Cloudflare
pnpm upload       # Upload only without deployment
pnpm lint         # ESLint check
pnpm type-check   # TypeScript type check
pnpm test         # Run unit tests (watch mode)
pnpm test:run     # Run unit tests (single run)
pnpm test:coverage # Run tests and generate coverage report
pnpm cf-typegen   # Generate Cloudflare environment types

Edge Runtime Constraints

Cloudflare Workers is an edge runtime, some Node.js APIs are unavailable:

Unavailable APIs:

  • fs - File system operations
  • child_process - Child processes
  • net, dgram - Native network sockets
  • crypto.createCipher and other legacy crypto APIs

Partially Available (via nodejs_compat):

  • Buffer - Binary data processing
  • process.env - Environment variables
  • crypto partial APIs - such as randomUUID()

Note

When using @opennextjs/cloudflare, the entire application automatically runs in edge environment, no need to manually declare export const runtime = 'edge'.

Cloudflare Services Integration

Available Services

Service Purpose Description
KV Key-value storage Globally distributed cache
D1 SQLite database Edge SQL database
R2 Object storage S3-compatible storage
Queues Message queues Async task processing
Durable Objects Stateful objects Real-time collaboration
Workers AI AI inference Edge AI models

Usage Example

ts
import { getRequestContext } from '@opennextjs/cloudflare';

export async function GET() {
  const { env } = getRequestContext();
  const value = await env.MY_KV.get('key');
  return Response.json({ value });
}

Configure KV Storage

jsonc
// wrangler.jsonc
{
  "kv_namespaces": [
    { "binding": "MY_KV", "id": "xxx" }
  ]
}

Configure D1 Database

jsonc
// wrangler.jsonc
{
  "d1_databases": [
    { "binding": "MY_DB", "database_id": "xxx" }
  ]
}

SSR/SSG/ISR Support

Rendering Mode Support Status Description
SSR ✅ Supported Each request renders at the edge
SSG ✅ Supported Static pages generated at build time
ISR ⚠️ Partial Requires R2 cache configuration

Enable ISR

ts
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";

export default defineCloudflareConfig({
  incrementalCache: r2IncrementalCache,
});

CI/CD

Project has complete GitHub Actions CI workflow configured:

Job Description
lint ESLint + TypeScript type check
test Vitest unit tests + Codecov coverage
build OpenNext Cloudflare production build
security Dependency security audit
dependency-review PR dependency change review

Deployment Workflow Example

yaml
# .github/workflows/deploy.yml
name: Deploy to Cloudflare

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install
      - run: pnpm deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}

Deployment Architecture

User Request → Cloudflare CDN → Workers (Edge) → KV/D1/R2/External API

          300+ global nodes
          Nearby response < 50ms

Quota Limits

Limit Free Tier Paid Tier
Worker script size 1MB (compressed) 10MB
CPU time 10-50ms Seconds
Memory 128MB 128MB
Subrequests 50 1000
Request duration 30s 30s

Reference

For actual limits, check Cloudflare official documentation.

Version Rollback

Cloudflare Pages retains deployment history, supporting the following rollback methods:

  1. Dashboard Rollback:

    • Cloudflare Dashboard → Workers & Pages → Project → Deployments
    • Select historical version → "Rollback to this deployment"
  2. Redeploy Specific Commit:

    bash
    git checkout <commit-hash>
    pnpm deploy

Common Issues

"Cannot find module 'fs'" Error

Edge Runtime doesn't support Node.js built-in modules. Use Web APIs instead or ensure the code only runs on the client side.

Build Size Too Large

  • Check if dependencies have Node.js-specific code
  • Use dynamic imports to split code
  • Remove unused dependencies

Slow Cold Start

  • Reduce Worker script size
  • Use Smart Placement for nearby deployment
  • Warm up critical paths
]]>
<![CDATA[Architecture Combination Guide]]> https://halolight.docs.h7ml.cn/en/guide/combinations https://halolight.docs.h7ml.cn/en/guide/combinations Fri, 19 Dec 2025 04:56:41 GMT B{Team Size?} B -->|Small <5| C{SEO Required?} B -->|Medium 5-20| D{Tech Preference?} B -->|Large >20| E[Angular + Spring Boot] C -->|Yes| F[Nuxt + FastAPI] C -->|No| G[Vue + FastAPI] D -->|Node.js| H[Next.js + NestJS] D -->|Python| I[Vue + FastAPI] D -->|Java| J[Angular + Spring Boot] D -->|Go| K[SvelteKit + Go Fiber] ``` ## 📊 Combination Evaluation Matrix Rating mainstream combinations across dimensions (max ⭐⭐⭐⭐⭐): ### Next.js + NestJS | Dimension | Rating | Description | |]]> Architecture Combination Guide

The core advantage of HaloLight lies in complete frontend-backend decoupling, supporting any combination. This document helps you choose the most suitable tech stack combination.

🎯 Quick Decision Flowchart

mermaid
graph TD
    A[Start Selection] --> B{Team Size?}
    B -->|Small <5| C{SEO Required?}
    B -->|Medium 5-20| D{Tech Preference?}
    B -->|Large >20| E[Angular + Spring Boot]

    C -->|Yes| F[Nuxt + FastAPI]
    C -->|No| G[Vue + FastAPI]

    D -->|Node.js| H[Next.js + NestJS]
    D -->|Python| I[Vue + FastAPI]
    D -->|Java| J[Angular + Spring Boot]
    D -->|Go| K[SvelteKit + Go Fiber]

📊 Combination Evaluation Matrix

Rating mainstream combinations across dimensions (max ⭐⭐⭐⭐⭐):

Next.js + NestJS

Dimension Rating Description
Development Efficiency ⭐⭐⭐⭐⭐ TypeScript full-stack unification, type sharing
Performance ⭐⭐⭐⭐ SSR + edge caching optimization
Learning Curve ⭐⭐⭐ Need to understand React and NestJS architecture
Ecosystem Maturity ⭐⭐⭐⭐⭐ npm ecosystem extremely rich
Deployment ⭐⭐⭐⭐ Vercel + Railway/Fly.io one-click deploy
Overall ⭐⭐⭐⭐ First choice for multi-tenant SaaS, enterprise backends

Vue + FastAPI

Dimension Rating Description
Development Efficiency ⭐⭐⭐⭐⭐ Vue smooth learning curve, FastAPI fast dev
Performance ⭐⭐⭐⭐ Vue 3 compilation optimized, Python async efficient
Learning Curve ⭐⭐⭐⭐⭐ Both relatively easy to pick up
Data Processing ⭐⭐⭐⭐⭐ Python data science ecosystem unbeatable
Deployment ⭐⭐⭐⭐ Frontend CDN, backend containerized
Overall ⭐⭐⭐⭐⭐ First choice for data/AI-driven apps

Angular + Spring Boot

Dimension Rating Description
Development Efficiency ⭐⭐⭐ Rigorous architecture, high initial investment
Performance ⭐⭐⭐⭐ Enterprise optimization mature
Learning Curve ⭐⭐ Both have some complexity
Enterprise Maturity ⭐⭐⭐⭐⭐ Large enterprise first choice
Long-term Maintenance ⭐⭐⭐⭐⭐ Clear architecture, highly maintainable
Overall ⭐⭐⭐⭐ Best for large enterprises, long-term projects

SvelteKit + Go Fiber

Dimension Rating Description
Development Efficiency ⭐⭐⭐⭐ Concise code, great dev experience
Performance ⭐⭐⭐⭐⭐ Both are performance benchmarks
Learning Curve ⭐⭐⭐ Svelte unique syntax, need to learn Go
Resource Usage ⭐⭐⭐⭐⭐ Extremely low memory and CPU usage
Deployment ⭐⭐⭐⭐⭐ Tiny container images, edge deployment ready
Overall ⭐⭐⭐⭐⭐ Best for high-performance real-time apps

🎨 Combination Matrix

Below shows all possible frontend-backend combinations. Each cell represents a viable tech stack pairing.

Frontend Frameworks (Horizontal) × Backend APIs (Vertical)

Frontend \ Backend NestJS Node.js FastAPI Spring Boot Go PHP Bun tRPC BFF
Next.js ⭐ Best ⭐ Best ⭐ Best
Nuxt ⭐ Best
Vue ⭐ Best
Angular ⭐ Best ⭐ Best
SvelteKit ⭐ Best
Astro ⭐ Best
Solid.js ⭐ Best ⭐ Best
Qwik ⭐ Best ⭐ Best
Remix ⭐ Best ⭐ Best ⭐ Best ⭐ Best
Preact ⭐ Best ⭐ Best
Lit ⭐ Best
Fresh ⭐ Best ⭐ Best

Legend:

  • ⭐ Best: Significant advantages in specific scenarios
  • ✅ Available: Fully compatible, ready to use

💡 Selection Recommendations

By Team Size

Small Teams (< 5 people)

  • Vue + FastAPI - Quick to learn, high efficiency
  • Preact + Bun - Lightweight, good performance
  • Astro + Node.js - Content-focused scenarios

Medium Teams (5-20 people)

  • Next.js + NestJS - TypeScript unified stack
  • Vue + Spring Boot - Balance usability and enterprise features
  • SvelteKit + FastAPI - Balance performance and efficiency

Large Teams (> 20 people)

  • Angular + Spring Boot - Architectural standards, maintainability
  • Next.js + NestJS + tRPC BFF - Micro-frontend + BFF architecture
  • Any Frontend + GraphQL Gateway + Microservices

By Tech Stack Preference

TypeScript Full-Stack

  • Next.js / Nuxt / Remix + NestJS + tRPC BFF
  • Solid.js / Qwik + Bun + Hono

Python Ecosystem

  • Vue / React / Astro + FastAPI
  • SvelteKit + FastAPI

Java Ecosystem

  • Angular / Vue + Spring Boot

Go Ecosystem

  • SvelteKit / Solid / Qwik + Go Fiber

By Deployment Environment

Serverless / Edge-First

  • Next.js + NestJS (Vercel + Vercel Functions)
  • Nuxt + Bun (Cloudflare Workers)
  • Astro + Deno Deploy

Traditional Servers

  • Any Frontend (Nginx static hosting) + Any Backend (PM2/Systemd)

Containerized (Kubernetes)

  • Any Combination (Docker images + K8s Deployment)

Hybrid Cloud

  • Frontend (CDN) + Backend (Private Cloud) + tRPC BFF (Edge nodes)

🔧 Tech Stack Comparison

Frontend Framework Features

Feature Next.js Vue Angular SvelteKit Solid Qwik
Learning Curve Medium Low High Low Medium Medium
TypeScript ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
SSR/SSG ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
Performance ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Ecosystem ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Bundle Size Medium Small Large Minimal Minimal Small

Backend Tech Features

Feature NestJS FastAPI Spring Boot Go Fiber
Dev Efficiency ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
Performance ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
TypeScript ⭐⭐⭐⭐⭐ - - -
Enterprise Maturity ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
Data Science ⭐⭐⭐⭐⭐ ⭐⭐
Resource Usage Medium Small Large Minimal

🚀 Quick Setup

After choosing your combination, follow these steps:

Step 1: Start Frontend

bash
# Example with Vue
git clone https://github.com/halolight/halolight-vue.git
cd halolight-vue
pnpm install
pnpm dev

Step 2: Start Backend API

bash
# Example with FastAPI
git clone https://github.com/halolight/halolight-api-python.git
cd halolight-api-python
pip install -e ".[dev]"
uvicorn app.main:app --reload

Step 3: Configure Frontend to Connect Backend

bash
# Frontend project's .env.local
VITE_API_URL=http://localhost:8000/api
VITE_USE_MOCK=false  # Disable Mock, use real API

]]>
<![CDATA[Deno Fresh Backend API]]> https://halolight.docs.h7ml.cn/en/guide/deno https://halolight.docs.h7ml.cn/en/guide/deno Fri, 19 Dec 2025 04:56:41 GMT Deno Fresh Backend API

HaloLight Deno backend API is built on Fresh framework and Deno KV, using the native Deno runtime to provide high-performance RESTful API services.

API Documentation: https://halolight-deno.h7ml.cn/docs

GitHub: https://github.com/halolight/halolight-deno

Features

  • 🔐 JWT Dual Tokens - Access Token + Refresh Token with auto-renewal
  • 🛡️ RBAC Permissions - Role-based access control with wildcard matching
  • 📡 RESTful API - Standardized interface design with OpenAPI documentation
  • 💾 Deno KV - Built-in key-value storage, no external database needed
  • Islands Architecture - Partial hydration for extreme performance
  • Data Validation - Request parameter validation and error handling
  • 📊 Logging System - Request logs and error tracking
  • 🐳 Docker Support - Containerized deployment

Tech Stack

Technology Version Description
Deno 2.x Runtime (built-in TypeScript)
Fresh 1.x Deno-native web framework
Preact 10.x Lightweight UI library
Deno KV - Built-in key-value storage (database)
Hono 4.x API routing framework (optional)
JWT - Authentication
Tailwind CSS 3.x Atomic CSS

Quick Start

Requirements

  • Deno >= 2.0.0

Installation

bash
# Clone repository
git clone https://github.com/halolight/halolight-deno.git
cd halolight-deno

# No dependency install needed, Deno manages automatically

Environment Variables

bash
cp .env.example .env
env
# API Configuration
API_URL=/api
USE_MOCK=true

# JWT Secret
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# Deno KV (optional, local by default)
DENO_KV_PATH=./data/kv.db

# Service Configuration
PORT=8000
NODE_ENV=development

# Demo Account
[email protected]
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=false

# Brand Configuration
APP_TITLE=Admin Pro
BRAND_NAME=Halolight

Database Initialization

bash
# Deno KV requires no migration, auto-creates
# If seed data needed
deno task seed

Start Service

bash
# Development mode
deno task dev

# Production mode
deno task build
deno task start

Visit http://localhost:8000

Project Structure

halolight-deno/
├── routes/                  # Route handlers
│   ├── api/                 # API endpoints
│   │   ├── auth/            # Auth routes
│   │   ├── users/           # User routes
│   │   ├── dashboard/       # Dashboard routes
│   │   ├── documents/       # Document routes
│   │   ├── files/           # File routes
│   │   ├── messages/        # Message routes
│   │   ├── notifications/   # Notification routes
│   │   └── calendar/        # Calendar routes
│   ├── (auth)/              # Auth pages
│   │   ├── login.tsx
│   │   ├── register.tsx
│   │   └── forgot-password.tsx
│   └── (dashboard)/         # Dashboard pages
│       ├── index.tsx
│       ├── users.tsx
│       └── settings.tsx
├── utils/                   # Utilities
│   ├── auth.ts              # Auth utilities
│   ├── kv.ts                # Deno KV wrapper
│   ├── permissions.ts       # Permission checks
│   ├── jwt.ts               # JWT utilities
│   └── validation.ts        # Data validation
├── islands/                 # Interactive components (client-side)
│   ├── AuthProvider.tsx
│   ├── ThemeToggle.tsx
│   └── Dashboard.tsx
├── components/              # UI components
│   ├── ui/
│   ├── layout/
│   └── dashboard/
├── static/                  # Static assets
├── fresh.gen.ts             # Fresh generated file
├── fresh.config.ts          # Fresh config
├── deno.json                # Deno config
└── import_map.json          # Import map

API Modules

Authentication Endpoints

Method Path Description Permission
POST /api/auth/login User login Public
POST /api/auth/register User registration Public
POST /api/auth/refresh Refresh token Public
POST /api/auth/logout User logout Authenticated
POST /api/auth/forgot-password Forgot password Public
POST /api/auth/reset-password Reset password Public

User Management Endpoints

Method Path Description Permission
GET /api/users Get user list users:view
GET /api/users/:id Get user details users:view
POST /api/users Create user users:create
PUT /api/users/:id Update user users:update
DELETE /api/users/:id Delete user users:delete
GET /api/users/me Get current user Authenticated

Complete Endpoint List

Document Management (Documents) - 5 endpoints

Method Path Description
GET /api/documents Get document list
GET /api/documents/:id Get document details
POST /api/documents Create document
PUT /api/documents/:id Update document
DELETE /api/documents/:id Delete document

File Management (Files) - 5 endpoints

Method Path Description
GET /api/files Get file list
GET /api/files/:id Get file details
POST /api/files/upload Upload file
PUT /api/files/:id Update file info
DELETE /api/files/:id Delete file

Message Management (Messages) - 5 endpoints

Method Path Description
GET /api/messages Get message list
GET /api/messages/:id Get message details
POST /api/messages Send message
PUT /api/messages/:id/read Mark as read
DELETE /api/messages/:id Delete message

Notification Management (Notifications) - 4 endpoints

Method Path Description
GET /api/notifications Get notification list
PUT /api/notifications/:id/read Mark as read
PUT /api/notifications/read-all Mark all as read
DELETE /api/notifications/:id Delete notification

Calendar Management (Calendar) - 5 endpoints

Method Path Description
GET /api/calendar/events Get event list
GET /api/calendar/events/:id Get event details
POST /api/calendar/events Create event
PUT /api/calendar/events/:id Update event
DELETE /api/calendar/events/:id Delete event

Dashboard - 6 endpoints

Method Path Description
GET /api/dashboard/stats Statistics data
GET /api/dashboard/visits Visit trends
GET /api/dashboard/sales Sales data
GET /api/dashboard/pie Pie chart data
GET /api/dashboard/tasks Todo tasks
GET /api/dashboard/calendar Today's schedule

Authentication

JWT Dual Tokens

Access Token:  15-minute validity for API requests
Refresh Token: 7-day validity for refreshing Access Token

Request Headers

http
Authorization: Bearer <access_token>

Refresh Flow

typescript
// utils/jwt.ts
import { create, verify } from "https://deno.land/x/[email protected]/mod.ts";

const key = await crypto.subtle.generateKey(
  { name: "HMAC", hash: "SHA-256" },
  true,
  ["sign", "verify"],
);

export async function createAccessToken(userId: string): Promise<string> {
  return await create(
    { alg: "HS256", typ: "JWT" },
    { userId, exp: Date.now() + 15 * 60 * 1000 }, // 15 minutes
    key,
  );
}

export async function refreshToken(refreshToken: string): Promise<string> {
  const payload = await verify(refreshToken, key);
  return await createAccessToken(payload.userId as string);
}

Permission System

Role Definitions

Role Description Permissions
super_admin Super Administrator * (all permissions)
admin Administrator users:*, documents:*, files:*, messages:*, calendar:*
user Regular User documents:view, files:view, messages:view, calendar:view
guest Guest dashboard:view

Permission Format

{resource}:{action}

Examples:
- users:view      # View users
- users:create    # Create users
- users:*         # All user operations
- *               # All permissions

Permission Check Implementation

typescript
// utils/permissions.ts
export function hasPermission(user: User, permission: string): boolean {
  const userPermissions = user.permissions || [];

  return userPermissions.some((p) => {
    if (p === "*") return true;
    if (p.endsWith(":*")) {
      const resource = p.slice(0, -2);
      return permission.startsWith(resource + ":");
    }
    return p === permission;
  });
}

// Route middleware
export function requirePermission(permission: string) {
  return async (req: Request, ctx: any) => {
    const user = ctx.state.user;
    if (!hasPermission(user, permission)) {
      return new Response("Forbidden", { status: 403 });
    }
    return await ctx.next();
  };
}

Error Handling

Error Response Format

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request parameter validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" }
    ]
  }
}

Error Codes

Status Error Code Description
400 VALIDATION_ERROR Parameter validation failed
401 UNAUTHORIZED Unauthorized
403 FORBIDDEN Forbidden
404 NOT_FOUND Resource not found
409 CONFLICT Resource conflict
500 INTERNAL_ERROR Server error

Common Commands

bash
# Development
deno task dev              # Start dev server (hot reload)

# Build
deno task build            # Build production bundle

# Testing
deno test                  # Run tests
deno test --coverage       # Test coverage

# Database
deno task seed             # Initialize seed data

# Code Quality
deno lint                  # Lint
deno fmt                   # Format
deno check **/*.ts         # Type check

Deployment

Docker

bash
docker build -t halolight-deno .
docker run -p 8000:8000 halolight-deno

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - NODE_ENV=production
      - JWT_SECRET=${JWT_SECRET}
      - DENO_KV_PATH=/data/kv.db
    volumes:
      - deno_kv_data:/data
    restart: unless-stopped

volumes:
  deno_kv_data:
bash
# Install deployctl
deno install -Arf jsr:@deno/deployctl

# Deploy to Deno Deploy
deployctl deploy --project=halolight-deno main.ts

Production Configuration

env
NODE_ENV=production
JWT_SECRET=your-production-secret-key-min-32-chars
DENO_KV_PATH=/data/kv.db
PORT=8000

Testing

Run Tests

bash
deno test                  # Run all tests
deno test --coverage       # Generate coverage report
deno test --watch          # Watch mode

Test Examples

typescript
// routes/api/auth/_test.ts
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";

Deno.test("POST /api/auth/login - success", async () => {
  const response = await fetch("http://localhost:8000/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      email: "[email protected]",
      password: "123456",
    }),
  });

  assertEquals(response.status, 200);
  const data = await response.json();
  assertEquals(data.success, true);
  assertEquals(typeof data.data.accessToken, "string");
});

Deno.test("POST /api/auth/login - invalid credentials", async () => {
  const response = await fetch("http://localhost:8000/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      email: "[email protected]",
      password: "wrong",
    }),
  });

  assertEquals(response.status, 401);
  const data = await response.json();
  assertEquals(data.success, false);
});

Performance Metrics

Benchmarks

Metric Value Notes
Request Throughput ~45,000 req/s Deno 2.0, single core, wrk test
Average Response Time <5ms Local Deno KV, no external deps
Memory Usage ~30MB Base memory after startup
CPU Usage <10% Idle state

Observability

Logging System

typescript
// utils/logger.ts
import { Logger } from "https://deno.land/[email protected]/log/mod.ts";

const logger = new Logger("app");

export function logRequest(req: Request, res: Response, duration: number) {
  logger.info(
    `${req.method} ${new URL(req.url).pathname} ${res.status} ${duration}ms`,
  );
}

export function logError(error: Error, context?: any) {
  logger.error(`Error: ${error.message}`, { error, context });
}

Health Check

typescript
// routes/api/health.ts
export const handler = async (req: Request): Promise<Response> => {
  try {
    // Check Deno KV connection
    const kv = await Deno.openKv();
    await kv.get(["health"]);
    await kv.close();

    return Response.json({
      status: "healthy",
      timestamp: new Date().toISOString(),
      uptime: Deno.memoryUsage(),
    });
  } catch (error) {
    return Response.json(
      { status: "unhealthy", error: error.message },
      { status: 503 },
    );
  }
};

Monitoring Metrics

typescript
// utils/metrics.ts
const metrics = {
  requests: 0,
  errors: 0,
  responseTime: [] as number[],
};

export function recordRequest(duration: number) {
  metrics.requests++;
  metrics.responseTime.push(duration);
}

export function recordError() {
  metrics.errors++;
}

export function getMetrics() {
  const avg = metrics.responseTime.reduce((a, b) => a + b, 0) /
    metrics.responseTime.length;
  return {
    totalRequests: metrics.requests,
    totalErrors: metrics.errors,
    avgResponseTime: avg || 0,
  };
}

FAQ

Q: How to persist Deno KV data?

A: Configure the DENO_KV_PATH environment variable to specify the data file path.

bash
# .env
DENO_KV_PATH=./data/kv.db
typescript
// Use custom path
const kv = await Deno.openKv(Deno.env.get("DENO_KV_PATH"));

Q: How to enable remote Deno KV (Deno Deploy)?

A: When deploying on Deno Deploy, using Deno.openKv() automatically connects to the hosted distributed KV.

typescript
// Production environment automatically uses remote KV
const kv = await Deno.openKv();

Q: How to handle file uploads?

A: Use Fresh's FormData API to handle file uploads.

typescript
// routes/api/files/upload.ts
export const handler = async (req: Request): Promise<Response> => {
  const formData = await req.formData();
  const file = formData.get("file") as File;

  if (!file) {
    return Response.json({ error: "No file uploaded" }, { status: 400 });
  }

  // Save file to Deno KV or cloud storage
  const bytes = await file.arrayBuffer();
  const kv = await Deno.openKv();
  await kv.set(["files", crypto.randomUUID()], {
    name: file.name,
    type: file.type,
    size: file.size,
    data: new Uint8Array(bytes),
  });

  return Response.json({ success: true });
};

Q: How does Islands architecture integrate with APIs?

A: Islands are client-side interactive components that call backend APIs via fetch.

typescript
// islands/UserList.tsx
import { useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";

export default function UserList() {
  const users = useSignal([]);

  useEffect(() => {
    fetch("/api/users")
      .then((res) => res.json())
      .then((data) => users.value = data);
  }, []);

  return (
    <div>
      {users.value.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Development Tools

  • Deno VSCode Extension - Official VS Code plugin with IntelliSense and debugging
  • deployctl - Deno Deploy command-line tool
  • wrk / autocannon - HTTP stress testing tools
  • Deno Lint - Built-in code linting tool

Comparison with Other Backends

Feature Deno Fresh NestJS FastAPI Spring Boot
Language TypeScript (Deno) TypeScript Python Java
ORM Deno KV Prisma SQLAlchemy JPA
Performance ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Learning Curve ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
Startup Time <100ms ~2s ~1s ~5s
Memory Usage 30MB 80MB 50MB 150MB
Deployment Deno Deploy Docker/Cloud Docker/Cloud Docker/Cloud
]]>
<![CDATA[Docker Deployment]]> https://halolight.docs.h7ml.cn/en/guide/docker https://halolight.docs.h7ml.cn/en/guide/docker Fri, 19 Dec 2025 04:56:41 GMT Docker Deployment

HaloLight Docker containerized deployment solution, supporting multi-stage builds and Kubernetes deployment.

Features

  • 🐳 Docker Containerization - Standardized container deployment
  • 🏗️ Multi-Stage Build - Optimized image size
  • 📦 Docker Compose - Multi-service orchestration
  • 🔄 Nginx Reverse Proxy - High-performance reverse proxy
  • Health Checks - Container health monitoring
  • ☸️ K8s Ready - Kubernetes deployment support

Quick Start

bash
# Clone repository
git clone https://github.com/halolight/halolight-docker.git
cd halolight-docker

# Start with Docker Compose
docker-compose up -d

# View logs
docker-compose logs -f

Dockerfile

dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:3000/api/health || exit 1

CMD ["node", "server.js"]

Docker Compose

yaml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:password@db:5432/halolight
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - app

volumes:
  postgres_data:
  redis_data:

Kubernetes Deployment

yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: halolight
spec:
  replicas: 3
  selector:
    matchLabels:
      app: halolight
  template:
    metadata:
      labels:
        app: halolight
    spec:
      containers:
      - name: halolight
        image: halolight/halolight:latest
        ports:
        - containerPort: 3000
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"
]]>
<![CDATA[Fly.io Deployment]]> https://halolight.docs.h7ml.cn/en/guide/fly https://halolight.docs.h7ml.cn/en/guide/fly Fri, 19 Dec 2025 04:56:41 GMT Fly.io Deployment

HaloLight Fly.io deployment version, a global edge deployment solution that supports multi-region distributed deployments.

Live Preview: https://halolight-fly.h7ml.cn

GitHub: https://github.com/halolight/halolight-fly

Features

  • ✈️ Global Edge - Deploy to 30+ regions worldwide
  • 📈 Auto Scaling - Scale instances automatically on demand
  • 💾 Volumes - Persistent storage volume support
  • 🔒 Private Network - Built-in WireGuard private network
  • 📊 Monitoring Metrics - Prometheus/Grafana integration
  • 🔄 Blue-Green Deployment - Zero-downtime rolling deployments
  • 🐘 Managed Databases - One-click PostgreSQL/Redis creation
  • 🖥️ Machines API - Fine-grained instance control

Quick Start

bash
# Install Fly CLI
# macOS
brew install flyctl

# Linux
curl -L https://fly.io/install.sh | sh

# Windows
powershell -Command "iwr https://fly.io/install.ps1 -useb | iex"

# Log in to Fly.io
fly auth login

# Clone the project
git clone https://github.com/halolight/halolight-fly.git
cd halolight-fly

# Initialize the app (creates fly.toml)
fly launch

# Deploy
fly deploy

Option 2: From Dockerfile

bash
# Clone the project
git clone https://github.com/halolight/halolight-fly.git
cd halolight-fly

# Log in
fly auth login

# Create the app
fly apps create halolight

# Deploy using Dockerfile
fly deploy --dockerfile Dockerfile

Option 3: GitHub Actions

yaml
# .github/workflows/fly.yml
name: Fly Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: superfly/flyctl-actions/setup-flyctl@master

      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: \${{ secrets.FLY_API_TOKEN }}

Configuration Files

fly.toml

toml
# App name
app = "halolight"

# Primary region (Hong Kong)
primary_region = "hkg"

# Build configuration
[build]
  dockerfile = "Dockerfile"

# Environment variables
[env]
  NODE_ENV = "production"
  PORT = "3000"
  NEXT_PUBLIC_API_URL = "/api"
  NEXT_PUBLIC_MOCK = "false"

# HTTP service configuration
[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = true      # Automatically stop when idle
  auto_start_machines = true     # Automatically start on request
  min_machines_running = 1       # Keep at least 1 instance running
  processes = ["app"]

# TCP service port mapping
[[services]]
  protocol = "tcp"
  internal_port = 3000

  [[services.ports]]
    port = 80
    handlers = ["http"]

  [[services.ports]]
    port = 443
    handlers = ["tls", "http"]

  # Health checks
  [[services.http_checks]]
    interval = "10s"
    timeout = "2s"
    grace_period = "5s"
    method = "GET"
    path = "/api/health"
    protocol = "http"
    tls_skip_verify = false

  # TCP checks
  [[services.tcp_checks]]
    interval = "15s"
    timeout = "2s"
    grace_period = "1s"

# VM configuration
[[vm]]
  cpu_kind = "shared"
  cpus = 1
  memory_mb = 512

# Process groups (multi-process optional)
[processes]
  app = "pnpm start"
  # worker = "pnpm run worker"

# Mount volumes
[mounts]
  source = "halolight_data"
  destination = "/data"
  initial_size = "1gb"

# Deployment strategy
[deploy]
  strategy = "rolling"
  max_unavailable = 0.33

# Static assets
[[statics]]
  guest_path = "/app/public"
  url_prefix = "/static/"

Dockerfile

dockerfile
# Build stage
FROM node:20-alpine AS builder

RUN npm install -g pnpm

WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

# Production stage
FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production

# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

Environment Variables

How to Set

bash
# Set a single variable via CLI
fly secrets set DATABASE_URL="postgresql://user:pass@host:5432/db"

# Set multiple variables via CLI
fly secrets set \\
  JWT_SECRET="your-secret" \\
  REDIS_URL="redis://..."

# Import from .env file
fly secrets import < .env.production

# View existing variables
fly secrets list

# Delete a variable
fly secrets unset DATABASE_URL

Common Variables

Variable Description Example
`NODE_ENV` Runtime environment `production`
`PORT` Server port `3000`
`NEXT_PUBLIC_API_URL` API base URL `/api`
`NEXT_PUBLIC_MOCK` Enable mock data `false`
`DATABASE_URL` PostgreSQL connection `postgresql://...`
`REDIS_URL` Redis connection `redis://...`
`JWT_SECRET` JWT secret `your-secret-key`

Built-in Variables

Fly.io automatically injects the following environment variables:

bash
FLY_APP_NAME        # App name
FLY_REGION          # Current region code (e.g., hkg)
FLY_ALLOC_ID        # Instance allocation ID
FLY_PUBLIC_IP       # Public IP
FLY_PRIVATE_IP      # Private network IP
PRIMARY_REGION      # Primary region

Persistent Storage

Create a Volume

bash
# Create a Volume in the specified region
fly volumes create halolight_data \\
  --region hkg \\
  --size 10 \\
  --count 1

# View Volumes
fly volumes list

# Expand size
fly volumes extend vol_xxx --size 20

# Delete Volume
fly volumes destroy vol_xxx

Mount Configuration

toml
# fly.toml
[mounts]
  source = "halolight_data"
  destination = "/data"

Using SQLite

typescript
// lib/db.ts
import Database from 'better-sqlite3';

const db = new Database('/data/app.db');

// Initialize table schema
db.exec(\`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT
  )
\`);

Managed Databases

PostgreSQL

bash
# Create a PostgreSQL cluster
fly postgres create --name halolight-db

# Optional configuration
fly postgres create \\
  --name halolight-db \\
  --region hkg \\
  --vm-size shared-cpu-1x \\
  --volume-size 10 \\
  --initial-cluster-size 1

# Attach to the app
fly postgres attach halolight-db --app halolight

# This automatically sets the DATABASE_URL environment variable

# Connect to the database (interactive)
fly postgres connect -a halolight-db

# Use psql
fly proxy 5432 -a halolight-db
# Then in another terminal
psql "postgresql://postgres:xxx@localhost:5432/halolight"

Redis

bash
# Create Redis (Upstash)
fly redis create

# Or use Fly-managed Redis
fly apps create halolight-redis
fly deploy --config redis.toml

# Get connection info
fly redis status halolight-redis

Redis Configuration File

toml
# redis.toml
app = "halolight-redis"
primary_region = "hkg"

[build]
  image = "flyio/redis:6.2.6"

[env]
  REDIS_PASSWORD = ""

[[mounts]]
  source = "redis_data"
  destination = "/data"

[[services]]
  internal_port = 6379
  protocol = "tcp"

  [[services.ports]]
    port = 6379

Multi-Region Deployment

Add Regions

bash
# View available regions
fly platform regions

# Add regions
fly regions add sin nrt syd

# View current regions
fly regions list

# Remove regions
fly regions remove syd

Region Code Reference

Code Location Latency (Asia)
hkg Hong Kong ~5ms
sin Singapore ~30ms
nrt Tokyo ~50ms
syd Sydney ~100ms
lax Los Angeles ~150ms
iad Washington ~200ms
lhr London ~200ms
fra Frankfurt ~180ms

Scale Instances

bash
# Set instance count
fly scale count 3

# Set instance count per region
fly scale count hkg=2 sin=1 nrt=1

# View current instances
fly scale show

# Adjust instance size
fly scale vm shared-cpu-2x
fly scale memory 1024

# Or configure in fly.toml
[[vm]]
  cpu_kind = "shared"
  cpus = 2
  memory_mb = 1024

Private Network

Service-to-Service Communication

bash
# View private IPs
fly ips private

# Use the .internal domain for app-to-app communication
# Format: <app-name>.internal

# Example for connecting to the database
DATABASE_URL=postgres://user:[email protected]:5432/db

WireGuard Tunnel

bash
# Create WireGuard configuration
fly wireguard create

# View configurations
fly wireguard list

# Import into the WireGuard client to access directly
# Internal service: http://halolight.internal:3000

Common Commands

bash
# App management
fly apps list                 # List all apps
fly apps create <name>        # Create an app
fly apps destroy <name>       # Delete an app

# Deployment
fly deploy                    # Deploy
fly deploy --remote-only      # Remote build only
fly deploy --local-only       # Local build only
fly deploy --strategy rolling # Rolling deployment

# Status and logs
fly status                    # View status
fly logs                      # View logs
fly logs -a halolight         # Logs for a specific app

# Instance management
fly machines list             # List instances
fly machines start <id>       # Start an instance
fly machines stop <id>        # Stop an instance
fly machines destroy <id>     # Destroy an instance

# SSH access
fly ssh console               # SSH into an instance
fly ssh issue                 # Generate SSH certificate

# Proxy
fly proxy 5432 -a halolight-db  # Proxy port

# Monitoring
fly dashboard                 # Open dashboard
fly metrics                   # View metrics

# Release management
fly releases                  # View release history
fly releases rollback         # Roll back to the previous version

Monitoring and Alerts

Prometheus Metrics

Fly.io automatically exposes Prometheus metrics:

bash
# Access metrics endpoint
curl https://halolight.fly.dev/_metrics

# Configure Prometheus scraping
scrape_configs:
  - job_name: 'fly'
    static_configs:
      - targets: ['halolight.fly.dev']
    metrics_path: '/_metrics'

Grafana Integration

bash
# Deploy Grafana
fly apps create halolight-grafana
fly deploy --config grafana.toml

# Configure data source to connect to Prometheus

Custom Health Checks

typescript
// app/api/health/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const checks = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    region: process.env.FLY_REGION,
    checks: {
      database: await checkDatabase(),
      redis: await checkRedis(),
    },
  };

  const allHealthy = Object.values(checks.checks).every(c => c === 'ok');

  return NextResponse.json(checks, {
    status: allHealthy ? 200 : 503,
  });
}

Custom Domains

Add a Domain

bash
# Add a custom domain
fly certs create halolight-fly.h7ml.cn

# View certificate status
fly certs show halolight-fly.h7ml.cn

# List all certificates
fly certs list

DNS Configuration

# A record
Type: A
Name: halolight-fly
Value: <fly-app-ipv4>

# AAAA record (IPv6)
Type: AAAA
Name: halolight-fly
Value: <fly-app-ipv6>

# Or use CNAME
Type: CNAME
Name: halolight-fly
Value: halolight.fly.dev

Get IPs

bash
# View app IPs
fly ips list

# Allocate dedicated IPv4 (paid)
fly ips allocate-v4

# Allocate IPv6 (free)
fly ips allocate-v6

FAQ

Q: What if deployment fails?

A: Check these items:

  1. Inspect build logs: `fly logs --build`
  2. Confirm the Dockerfile is correct
  3. Verify fly.toml is correctly configured
  4. Check if memory is sufficient

Q: How to roll back a deployment?

A: Use the following commands:

bash
# View release history
fly releases

# Roll back to the previous version
fly releases rollback

# Roll back to a specific version
fly releases rollback v5

Q: Cold starts are too slow?

A: Optimization tips:

  1. Keep `min_machines_running = 1`
  2. Increase the number of instances
  3. Use `auto_start_machines = true`
  4. Optimize Docker image size

Q: How to debug the app?

A: Use SSH access:

bash
# SSH into an instance
fly ssh console

# Run a command
fly ssh console -C "ls -la"

# View processes
fly ssh console -C "ps aux"

Q: Database connection issues?

A: Check the following:

  1. Confirm DATABASE_URL is correct
  2. Use the `.internal` domain for internal connections
  3. Ensure PostgreSQL is on the same private network

Pricing

Resource Free Tier Price Beyond
Shared CPU 3 instances $1.94/mo/instance
Memory 256MB/instance $0.01/GB/hour
Bandwidth 160GB/month $0.02/GB
IPv4 - $2/month
IPv6 Unlimited Free
Volumes 3GB $0.15/GB/month
PostgreSQL - From $6.44/month
toml
# Development/Test
[[vm]]
  cpu_kind = "shared"
  cpus = 1
  memory_mb = 256

# Production
[[vm]]
  cpu_kind = "shared"
  cpus = 2
  memory_mb = 1024

Comparison With Other Platforms

Feature Fly.io Railway Render
Global Regions 30+ 2 4
Private Network ✅ WireGuard
Managed Databases ✅ PostgreSQL
Auto Scaling ✅ Pro
Free Tier 3 instances $5/month 750 hours
Docker Support ✅ Native
Edge Computing
]]>
<![CDATA[Fresh (Deno) Version]]> https://halolight.docs.h7ml.cn/en/guide/fresh https://halolight.docs.h7ml.cn/en/guide/fresh Fri, 19 Dec 2025 04:56:41 GMT Fresh (Deno) Version

HaloLight Fresh version is built on Fresh 2 + Deno, using Islands architecture + Preact to deliver a zero-config, ultra-fast admin dashboard.

Live Preview: https://halolight-fresh.h7ml.cn

GitHub: https://github.com/halolight/halolight-fresh

Features

  • 🏗️ Islands Architecture - Zero JS by default, hydrate on demand, ultimate performance
  • Zero Config - Works out of the box, no build step required
  • 🎨 Theme System - 11 skins, dark mode, View Transitions
  • 🔐 Authentication - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization and business management
  • 🛡️ Permission Control - RBAC fine-grained permission management
  • 🔒 Secure by Default - Deno sandbox security model
  • 🌐 Edge First - Native support for Deno Deploy edge deployment

Tech Stack

Technology Version Description
Fresh 2.x Deno full-stack framework
Deno 2.x Modern JavaScript runtime
Preact 10.x Lightweight UI library
@preact/signals 2.x Reactive state management
TypeScript Built-in Type safety
Tailwind CSS Built-in Atomic CSS
Zod 3.x Data validation
Chart.js 4.x Chart visualization

Core Features

  • Islands Architecture - Zero JS by default, only interactive components hydrate, ultimate performance
  • Zero Config Development - JIT rendering, no build step, instant startup
  • Permission System - RBAC permission control, route guards, permission components
  • Theme System - 11 skins, dark mode, View Transitions
  • Edge Deployment - Native support for Deno Deploy edge runtime
  • Type Safety - Built-in TypeScript, no configuration needed
  • Security Model - Deno sandbox, explicit permissions, secure by default

Directory Structure

halolight-fresh/
├── routes/                        # File-based routing
│   ├── _app.tsx                  # Root layout
│   ├── _layout.tsx               # Default layout
│   ├── _middleware.ts            # Global middleware
│   ├── index.tsx                 # Homepage
│   ├── auth/                     # Auth pages
│   │   ├── login.tsx
│   │   ├── register.tsx
│   │   ├── forgot-password.tsx
│   │   └── reset-password.tsx
│   ├── dashboard/                # Dashboard pages
│   │   ├── _layout.tsx           # Dashboard layout
│   │   ├── _middleware.ts        # Auth middleware
│   │   ├── index.tsx
│   │   ├── users/
│   │   │   ├── index.tsx
│   │   │   ├── create.tsx
│   │   │   └── [id].tsx
│   │   ├── roles.tsx
│   │   ├── permissions.tsx
│   │   ├── settings.tsx
│   │   └── profile.tsx
│   └── api/                      # API routes
│       └── auth/
│           ├── login.ts
│           ├── register.ts
│           └── me.ts
├── islands/                      # Interactive Islands
│   ├── LoginForm.tsx
│   ├── UserTable.tsx
│   ├── DashboardGrid.tsx
│   ├── ThemeToggle.tsx
│   └── Sidebar.tsx
├── components/                   # Static components
│   ├── ui/                       # UI components
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   ├── Card.tsx
│   │   └── ...
│   ├── layout/                   # Layout components
│   │   ├── AdminLayout.tsx
│   │   ├── AuthLayout.tsx
│   │   └── Header.tsx
│   └── shared/                   # Shared components
│       └── PermissionGuard.tsx
├── lib/                          # Utilities
│   ├── auth.ts
│   ├── permission.ts
│   ├── session.ts
│   └── cn.ts
├── signals/                      # State management
│   ├── auth.ts
│   ├── ui-settings.ts
│   └── dashboard.ts
├── static/                       # Static assets
├── fresh.config.ts              # Fresh config
├── deno.json                    # Deno config
└── tailwind.config.ts           # Tailwind config

Quick Start

Requirements

  • Deno >= 2.x

Install Deno

bash
# macOS/Linux
curl -fsSL https://deno.land/install.sh | sh

# Windows
irm https://deno.land/install.ps1 | iex

Installation

bash
git clone https://github.com/halolight/halolight-fresh.git
cd halolight-fresh

Environment Variables

bash
cp .env.example .env
env
# .env
API_URL=/api
USE_MOCK=true
[email protected]
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=true
APP_TITLE=Admin Pro
BRAND_NAME=Halolight
SESSION_SECRET=your-secret-key

Start Development

bash
deno task dev

Visit http://localhost:8000

Production Build

bash
deno task build
deno task start

Core Features

State Management (@preact/signals)

ts
// signals/auth.ts
import { signal, computed, effect } from '@preact/signals'
import { IS_BROWSER } from '$fresh/runtime.ts'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

export const user = signal<User | null>(null)
export const token = signal<string | null>(null)
export const loading = signal(false)

export const isAuthenticated = computed(() => !!token.value && !!user.value)
export const permissions = computed(() => user.value?.permissions ?? [])

// Only persist in browser
if (IS_BROWSER) {
  const saved = localStorage.getItem('auth')
  if (saved) {
    const { user: savedUser, token: savedToken } = JSON.parse(saved)
    user.value = savedUser
    token.value = savedToken
  }

  effect(() => {
    if (user.value && token.value) {
      localStorage.setItem('auth', JSON.stringify({
        user: user.value,
        token: token.value,
      }))
    }
  })
}

export async function login(credentials: { email: string; password: string }) {
  loading.value = true
  try {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
      headers: { 'Content-Type': 'application/json' },
    })
    const data = await response.json()

    user.value = data.user
    token.value = data.token
  } finally {
    loading.value = false
  }
}

export function logout() {
  user.value = null
  token.value = null
  if (IS_BROWSER) {
    localStorage.removeItem('auth')
  }
}

export function hasPermission(permission: string): boolean {
  const perms = permissions.value
  return perms.some((p) =>
    p === '*' || p === permission ||
    (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
  )
}

Data Fetching (Handlers)

ts
// routes/api/auth/login.ts
import { Handlers } from '$fresh/server.ts'
import { z } from 'zod'
import { setCookie } from '$std/http/cookie.ts'
import { createToken } from '../../../lib/auth.ts'

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6),
})

export const handler: Handlers = {
  async POST(req) {
    try {
      const body = await req.json()
      const { email, password } = loginSchema.parse(body)

      // Authenticate user (example)
      const user = await authenticateUser(email, password)
      if (!user) {
        return new Response(
          JSON.stringify({ error: 'Invalid email or password' }),
          { status: 401, headers: { 'Content-Type': 'application/json' } }
        )
      }

      const token = await createToken({ userId: user.id })

      const response = new Response(
        JSON.stringify({ user, token }),
        { headers: { 'Content-Type': 'application/json' } }
      )

      setCookie(response.headers, {
        name: 'token',
        value: token,
        path: '/',
        httpOnly: true,
        sameSite: 'Lax',
        maxAge: 60 * 60 * 24 * 7,
      })

      return response
    } catch (e) {
      if (e instanceof z.ZodError) {
        return new Response(
          JSON.stringify({ error: 'Validation failed', details: e.errors }),
          { status: 400, headers: { 'Content-Type': 'application/json' } }
        )
      }
      return new Response(
        JSON.stringify({ error: 'Server error' }),
        { status: 500, headers: { 'Content-Type': 'application/json' } }
      )
    }
  },
}

Permission Control

tsx
// components/shared/PermissionGuard.tsx
import { ComponentChildren } from 'preact'

interface Props {
  permission: string
  userPermissions: string[]
  children: ComponentChildren
  fallback?: ComponentChildren
}

function checkPermission(
  userPermissions: string[],
  permission: string
): boolean {
  return userPermissions.some((p) =>
    p === '*' || p === permission ||
    (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
  )
}

export function PermissionGuard({
  permission,
  userPermissions,
  children,
  fallback,
}: Props) {
  if (!checkPermission(userPermissions, permission)) {
    return fallback ?? null
  }

  return <>{children}</>
}
tsx
// Usage (in server-side rendering)
<PermissionGuard
  permission="users:delete"
  userPermissions={ctx.state.user.permissions}
  fallback={<span class="text-muted-foreground">No permission</span>}
>
  <Button variant="destructive">Delete</Button>
</PermissionGuard>

Islands Architecture

tsx
// islands/LoginForm.tsx
import { useSignal } from '@preact/signals'
import { login, loading } from '../signals/auth.ts'
import { Button } from '../components/ui/Button.tsx'
import { Input } from '../components/ui/Input.tsx'

interface Props {
  redirectTo?: string
}

export default function LoginForm({ redirectTo = '/dashboard' }: Props) {
  const email = useSignal('')
  const password = useSignal('')
  const error = useSignal('')

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    error.value = ''

    try {
      await login({
        email: email.value,
        password: password.value,
      })
      globalThis.location.href = redirectTo
    } catch (e) {
      error.value = 'Invalid email or password'
    }
  }

  return (
    <form onSubmit={handleSubmit} class="space-y-4">
      {error.value && (
        <div class="text-destructive text-sm">{error.value}</div>
      )}

      <Input
        type="email"
        label="Email"
        value={email.value}
        onInput={(e) => email.value = e.currentTarget.value}
        required
      />

      <Input
        type="password"
        label="Password"
        value={password.value}
        onInput={(e) => password.value = e.currentTarget.value}
        required
      />

      <Button type="submit" class="w-full" disabled={loading.value}>
        {loading.value ? 'Logging in...' : 'Login'}
      </Button>
    </form>
  )
}
tsx
// routes/auth/login.tsx
import { Handlers, PageProps } from '$fresh/server.ts'
import { AuthLayout } from '../../components/layout/AuthLayout.tsx'
import LoginForm from '../../islands/LoginForm.tsx'

export const handler: Handlers = {
  GET(req, ctx) {
    const url = new URL(req.url)
    const redirect = url.searchParams.get('redirect') || '/dashboard'
    return ctx.render({ redirect })
  },
}

export default function LoginPage({ data }: PageProps<{ redirect: string }>) {
  return (
    <AuthLayout>
      <div class="max-w-md mx-auto">
        <h1 class="text-2xl font-bold text-center mb-8">Login</h1>
        <LoginForm redirectTo={data.redirect} />
      </div>
    </AuthLayout>
  )
}

Theme System

Skin Presets

Supports 11 preset skins, switch via quick settings panel:

Skin Color CSS Variable
Default Purple --primary: 51.1% 0.262 276.97
Blue Blue --primary: 54.8% 0.243 264.05
Emerald Green --primary: 64.6% 0.178 142.49
Orange Orange --primary: 69.7% 0.186 37.37
Rose Rose --primary: 62.8% 0.241 12.48
Teal Teal --primary: 66.7% 0.151 193.65
Amber Amber --primary: 77.5% 0.166 69.76
Cyan Cyan --primary: 75.1% 0.146 204.66
Pink Pink --primary: 65.7% 0.255 347.69
Indigo Indigo --primary: 51.9% 0.235 272.75
Lime Lime --primary: 78.1% 0.167 136.29

CSS Variables (OKLch)

css
/* Theme variable definitions */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 96.1% 0.006 285.75;
  --secondary-foreground: 14.9% 0.017 285.75;
  --muted: 96.1% 0.006 285.75;
  --muted-foreground: 44.7% 0.025 285.75;
  --accent: 96.1% 0.006 285.75;
  --accent-foreground: 14.9% 0.017 285.75;
  --destructive: 62.8% 0.241 12.48;
  --destructive-foreground: 100% 0 0;
  --border: 89.8% 0.011 285.75;
  --input: 89.8% 0.011 285.75;
  --ring: 51.1% 0.262 276.97;
  --radius: 0.5rem;
}

Page Routes

Path Page Permission
/ Homepage Public
/auth/login Login Public
/auth/register Register Public
/auth/forgot-password Forgot Password Public
/auth/reset-password Reset Password Public
/dashboard Dashboard dashboard:view
/dashboard/users User List users:list
/dashboard/users/create Create User users:create
/dashboard/users/[id] User Detail users:view
/dashboard/roles Role Management roles:list
/dashboard/permissions Permission Management permissions:list
/dashboard/settings System Settings settings:view
/dashboard/profile Profile Logged in

Common Commands

bash
deno task dev            # Start development server
deno task build          # Production build
deno task start          # Start production server
deno task check          # Format and type check
deno task fmt            # Format code
deno task fmt:check      # Check code format
deno task lint           # Lint code
deno task test           # Run tests
deno task test:watch     # Test watch mode
deno task test:coverage  # Test coverage
deno task ci             # Run full CI check

Deployment

bash
# Install deployctl
deno install -A --no-check -r -f https://deno.land/x/deploy/deployctl.ts

# Deploy
deployctl deploy --project=halolight-fresh main.ts

Docker

dockerfile
FROM denoland/deno:2.0.0

WORKDIR /app
COPY . .

RUN deno cache main.ts

EXPOSE 8000
CMD ["run", "-A", "main.ts"]
bash
docker build -t halolight-fresh .
docker run -p 8000:8000 halolight-fresh

Other Platforms

Demo Accounts

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Testing

The project uses Deno's built-in testing framework, test files are located in the tests/ directory.

Test Structure

tests/
├── setup.ts              # Test environment setup
│   ├── localStorage mock
│   ├── sessionStorage mock
│   ├── matchMedia mock
│   └── Helper functions (createMockUser, mockAuthenticatedState, etc.)
└── lib/
    ├── utils.test.ts     # Utility function tests
    ├── config.test.ts    # Config tests
    └── stores.test.ts    # State management tests

Run Tests

bash
# Run all tests
deno task test

# Watch mode
deno task test:watch

# Test coverage
deno task test:coverage

# Coverage report output to coverage/lcov.info

Test Example

ts
// tests/lib/config.test.ts
import { assertEquals, assertExists } from "$std/assert/mod.ts";
import "../setup.ts";

import { hasPermission } from "../../lib/config.ts";
import type { Permission } from "../../lib/types.ts";

Deno.test("hasPermission - permission check", async (t) => {
  const userPermissions: Permission[] = ["dashboard:view", "users:view"];

  await t.step("should return true when user has permission", () => {
    const result = hasPermission(userPermissions, "dashboard:view");
    assertEquals(result, true);
  });

  await t.step("should support wildcard permissions", () => {
    const adminPermissions: Permission[] = ["*"];
    const result = hasPermission(adminPermissions, "dashboard:view");
    assertEquals(result, true);
  });
});

Configuration

Fresh Configuration

ts
// fresh.config.ts
import { defineConfig } from '$fresh/server.ts'
import tailwind from '$fresh/plugins/tailwind.ts'

export default defineConfig({
  plugins: [tailwind()],
})

Deno Configuration

json
// deno.json
{
  "lock": false,
  "tasks": {
    "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
    "dev": "deno run -A --watch=static/,routes/ dev.ts",
    "build": "deno run -A dev.ts build",
    "start": "deno run -A main.ts",
    "update": "deno run -A -r https://fresh.deno.dev/update ."
  },
  "imports": {
    "$fresh/": "https://deno.land/x/[email protected]/",
    "$std/": "https://deno.land/[email protected]/",
    "preact": "https://esm.sh/[email protected]",
    "preact/": "https://esm.sh/[email protected]/",
    "@preact/signals": "https://esm.sh/@preact/[email protected]",
    "zod": "https://deno.land/x/[email protected]/mod.ts"
  },
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

CI/CD

The project uses GitHub Actions for continuous integration, configuration file is located at .github/workflows/ci.yml.

Workflow Tasks

Task Description Trigger
lint Format check, code check, type check push/PR
test Run tests and upload coverage push/PR
build Production build verification After lint/test pass
security Deno security audit push/PR
dependency-review Dependency security review PR only

Code Quality Configuration

json
// deno.json
{
  "lint": {
    "rules": {
      "tags": ["recommended"],
      "exclude": [
        "no-explicit-any",
        "explicit-function-return-type",
        "explicit-module-boundary-types",
        "jsx-button-has-type",
        "no-unused-vars"
      ]
    }
  },
  "fmt": {
    "lineWidth": 100,
    "indentWidth": 2,
    "singleQuote": false,
    "semiColons": true
  }
}

Advanced Features

Middleware System

ts
// routes/dashboard/_middleware.ts
import { FreshContext } from '$fresh/server.ts'
import { getCookies } from '$std/http/cookie.ts'
import { verifyToken, getUser } from '../../lib/auth.ts'

export async function handler(req: Request, ctx: FreshContext) {
  const cookies = getCookies(req.headers)
  const token = cookies.token

  if (!token) {
    const url = new URL(req.url)
    return new Response(null, {
      status: 302,
      headers: { Location: `/auth/login?redirect=${url.pathname}` },
    })
  }

  try {
    const payload = await verifyToken(token)
    const user = await getUser(payload.userId)
    ctx.state.user = user
    ctx.state.token = token
  } catch {
    return new Response(null, {
      status: 302,
      headers: { Location: '/auth/login' },
    })
  }

  return ctx.next()
}

Nested Layouts

tsx
// routes/dashboard/_layout.tsx
import { PageProps } from '$fresh/server.ts'
import { AdminLayout } from '../../components/layout/AdminLayout.tsx'
import Sidebar from '../../islands/Sidebar.tsx'

export default function DashboardLayout({ Component, state }: PageProps) {
  return (
    <AdminLayout>
      <div class="flex min-h-screen">
        <Sidebar user={state.user} />
        <main class="flex-1 p-6">
          <Component />
        </main>
      </div>
    </AdminLayout>
  )
}

Performance Optimization

Islands Architecture Optimization

Fresh defaults to zero JS, only interactive components need hydration:

tsx
// Static component (components/) - Zero JS
export function Card({ title, content }) {
  return (
    <div class="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  )
}

// Interactive Island (islands/) - Hydrate on demand
export default function Counter() {
  const count = useSignal(0)
  return (
    <button onClick={() => count.value++}>
      Count: {count.value}
    </button>
  )
}

Edge Deployment Optimization

ts
// Leverage Deno Deploy edge runtime
export const handler: Handlers = {
  async GET(req) {
    // Execute at edge nodes, reduce latency
    const data = await fetchFromDatabase()
    return new Response(JSON.stringify(data))
  }
}

Preloading

tsx
// Preload critical resources
<link rel="preload" href="/api/auth/me" as="fetch" crossOrigin="anonymous" />

FAQ

Q: How to share state between Islands and server components?

A: Use @preact/signals, which works on both server and client:

ts
// signals/auth.ts
export const user = signal<User | null>(null)

// islands/UserProfile.tsx (client-side)
import { user } from '../signals/auth.ts'
export default function UserProfile() {
  return <div>{user.value?.name}</div>
}

// routes/dashboard/index.tsx (server-side)
import { user } from '../signals/auth.ts'
export default function Dashboard({ data }: PageProps) {
  return <div>Welcome {data.user.name}</div>
}

Q: How to handle environment variables?

A: Fresh uses Deno's environment variable system:

ts
// Read environment variable
const apiUrl = Deno.env.get('API_URL') || '/api'

// .env file (development)
// Automatically loaded with deno task dev

Q: How to implement data persistence?

A: Use Deno KV (built-in key-value database):

ts
// lib/db.ts
const kv = await Deno.openKv()

export async function saveUser(user: User) {
  await kv.set(['users', user.id], user)
}

export async function getUser(id: number) {
  const result = await kv.get(['users', id])
  return result.value as User
}

Comparison with Other Versions

Feature Fresh Version Astro Version Next.js Version
Runtime Deno Node.js Node.js
State Management @preact/signals - Zustand
Data Fetching Handlers Load functions TanStack Query
Form Validation Zod Zod React Hook Form + Zod
Server-side Built-in @astrojs/node API Routes
Component Library Custom - shadcn/ui
Islands Architecture
Zero Config
Edge Deployment Deno Deploy Cloudflare Vercel Edge
Build Step Optional Required Required
]]>
<![CDATA[Getting Started]]> https://halolight.docs.h7ml.cn/en/guide/getting-started https://halolight.docs.h7ml.cn/en/guide/getting-started Fri, 19 Dec 2025 04:56:41 GMT Getting Started

Choose your preferred frontend framework and match it with a backend API to quickly start with HaloLight.

🎯 Choose Your Combination

HaloLight uses a fully decoupled frontend-backend architecture, supporting 12 Frontends × 8 Backends = 96 Combinations.

Step 1: Choose Frontend Framework (Pick 1 of 12)

Framework Use Cases Characteristics
Next.js / Nuxt Multi-tenant SaaS, SEO needs SSR + Edge rendering friendly
Vue Quick delivery for small-medium teams Lightweight, smooth learning curve
Angular Large-scale, long-term projects Strong typing, clear architecture
SvelteKit / Solid / Qwik High interaction, real-time scenarios Ultimate performance & reactivity
Remix / Preact / Lit Progressive enhancement, lightweight Web Components, small bundle size
Astro Content-focused admin panels Islands architecture, zero JS by default

Step 2: Choose Backend API (Pick 1 of 8)

Backend Tech Use Cases Characteristics
NestJS / Express Node.js ecosystem teams High compatibility with frontend TS
FastAPI Data/AI-driven applications Python ecosystem, rapid iteration
Spring Boot Enterprise, financial industry Mature middleware ecosystem
Go Fiber High performance, high concurrency Low resource usage
PHP Laravel Traditional web teams Complete ecosystem, easy to learn
Bun + Hono Ultimate performance pursuit Next-gen runtime
tRPC BFF Mobile/Desktop multi-platform Type sharing, aggregation & simplification
📊 Multi-tenant SaaS / Enterprise Admin

Recommended: Next.js + NestJS

Advantages:

  • SSR + TypeScript end-to-end unification
  • Code sharing (types, utilities)
  • Mature deployment ecosystem (Vercel + Railway/Fly.io)
🤖 Data-Intensive / AI-Driven Apps

Recommended: Vue + FastAPI or React + FastAPI

Advantages:

  • Python data science ecosystem (Pandas, NumPy, scikit-learn)
  • Fast API development (auto docs, dependency injection)
  • Lightweight frontend, easy chart library integration
🏢 Large Enterprise / Long-term Projects

Recommended: Angular + Spring Boot

Advantages:

  • Strong typing, modular architecture
  • Mature enterprise middleware (auth, cache, message queue)
  • Long-term support and stability
⚡ High-Performance Real-time Apps

Recommended: SvelteKit + Go Fiber

Advantages:

  • Frontend compilation optimization, minimal bundle
  • Backend high throughput, low latency
  • Low resource usage, cost optimization
📱 Mobile/Desktop Multi-platform Unified

Recommended: Any Frontend + tRPC BFF + Any Backend

Advantages:

  • BFF aggregates and tailors APIs for multiple platforms
  • TypeScript end-to-end type safety
  • Reduces frontend complexity

Requirements

  • Node.js 18.17 or higher
  • pnpm 8+ (recommended) / npm / yarn

Next.js Version

bash
# Clone repository
git clone https://github.com/halolight/halolight.git
cd halolight

# Install dependencies
pnpm install

# Start development server
pnpm dev

Visit http://localhost:3000 to see the result.

Detailed documentation: Next.js Version Guide

Vue Version

bash
# Clone repository
git clone https://github.com/halolight/halolight-vue.git
cd halolight-vue

# Install dependencies
pnpm install

# Start development server
pnpm dev

Visit http://localhost:5173 to see the result.

Detailed documentation: Vue Version Guide

Default Credentials

All versions use the same Mock credentials:

Role Email Password
Admin [email protected] 123456

Directory Structure

project-root/
├── src/
│   ├── app/              # Page routes
│   ├── components/       # Components
│   │   ├── ui/          # shadcn/ui components
│   │   ├── layout/      # Layout components
│   │   └── dashboard/   # Dashboard components
│   ├── hooks/           # Custom hooks
│   ├── stores/          # State management
│   ├── services/        # API services
│   ├── lib/             # Utility library
│   ├── types/           # Type definitions
│   └── mocks/           # Mock data
├── public/              # Static assets
└── package.json

🚀 Quick Examples

Option 1: Next.js + NestJS

bash
# Terminal 1: Start frontend
git clone https://github.com/halolight/halolight.git && cd halolight
pnpm install && pnpm dev
bash
# Terminal 2: Start backend
git clone https://github.com/halolight/halolight-api-nestjs.git && cd halolight-api-nestjs
pnpm install && pnpm dev

Option 2: Vue + FastAPI

bash
# Terminal 1: Start frontend
git clone https://github.com/halolight/halolight-vue.git && cd halolight-vue
pnpm install && pnpm dev
bash
# Terminal 2: Start backend
git clone https://github.com/halolight/halolight-api-python.git && cd halolight-api-python
pip install -e ".[dev]" && uvicorn app.main:app --reload

Next Steps

]]>
<![CDATA[Introduction]]> https://halolight.docs.h7ml.cn/en/guide/ https://halolight.docs.h7ml.cn/en/guide/ Fri, 19 Dec 2025 04:56:41 GMT Introduction

HaloLight is a multi-framework enterprise-level admin dashboard solution.

What is HaloLight

HaloLight follows the philosophy of "one design specification, multiple framework implementations", providing developers with a unified Admin Dashboard experience. Whether you use React, Vue, Angular, or other modern frameworks, you'll get consistent functionality and design.

Core Features

Draggable Dashboard

Custom Dashboard system based on Grid Layout, supporting:

  • Widget drag and drop arrangement
  • Responsive layout adaptation
  • Layout state persistence

Permission Control

Complete RBAC permission management system:

  • Fine-grained permission control (page/button level)
  • Wildcard permission matching (users:*, *)
  • Dynamic menu rendering

Theme System

Rich visual customization capabilities:

  • 11 skin presets
  • Light/Dark mode switching
  • View Transitions animation effects

Component Library

Based on shadcn/ui design system:

  • 30+ beautiful UI components
  • Complete form/table solutions
  • Highly customizable

Frontend-Backend Any Combination

  • 98 Combination Options: 14 frontend frameworks × 7 backend APIs, choose freely based on team tech stack or business scenarios
  • BFF/Gateway Decoupling: Optional tRPC, GraphQL Gateway for aggregation, authentication & simplification
  • Upgrade Without Lock-in: Replace any frontend or backend while maintaining contract compatibility
  • Independent Evolution: Frontend can choose SSR/SSG/SPA, backend can choose monolith/microservices/serverless

Framework Implementations

All framework versions have been implemented and deployed (preview links available in respective repository READMEs). Current reference implementations:

Framework Status Preview Repository
Next.js 14 ✅ Deployed Preview GitHub
React (Vite) ✅ Deployed Preview GitHub
Vue 3.5 ✅ Deployed Preview GitHub
Angular 21 ✅ Deployed Preview GitHub
Nuxt 4 ✅ Deployed Preview GitHub
SvelteKit 2 ✅ Deployed Preview GitHub
Astro 5 ✅ Deployed Preview GitHub
Solid.js ✅ Deployed Preview GitHub
Qwik ✅ Deployed Preview GitHub
Remix ✅ Deployed Preview GitHub
Preact ✅ Deployed Preview GitHub
Lit ✅ Deployed Preview GitHub
Fresh (Deno) ✅ Deployed Preview GitHub
Deno ✅ Deployed Preview GitHub

💡 Flexible Combinations: Frontend mainline supports 14 frameworks, any frontend can combine with 7 backend APIs, forming 98+ combination options.

Backend API Implementations

Backend Tech Status Preview Repository
NestJS 11 ✅ Deployed API Docs GitHub
Python FastAPI ✅ Deployed API Docs GitHub
Java Spring Boot ✅ Deployed API Docs GitHub
Go Fiber ✅ Deployed API Docs GitHub
Node.js Express ✅ Deployed - GitHub
PHP Laravel ✅ Deployed - GitHub
Bun + Hono ✅ Deployed - GitHub

Middleware / Full-Stack

Project Status Description Repository
tRPC BFF ✅ Deployed Type-safe gateway GitHub
Next.js Action ✅ Deployed Server Actions full-stack GitHub

Tech Stack

All framework versions share the following tech stack:

  • TypeScript - Type safety
  • Tailwind CSS - Atomic CSS
  • shadcn/ui - UI component library
  • TanStack Query - Server state management
  • ECharts - Chart visualization
  • Mock.js - Data simulation

Next Steps

]]>
<![CDATA[Lit Version]]> https://halolight.docs.h7ml.cn/en/guide/lit https://halolight.docs.h7ml.cn/en/guide/lit Fri, 19 Dec 2025 04:56:41 GMT Lit Version

HaloLight Lit version is built on Lit 3 with Web Components standards + TypeScript, providing cross-framework reusable Web Components library.

Live Preview: https://halolight-lit.h7ml.cn

GitHub: https://github.com/halolight/halolight-lit

Features

  • 🎯 Web Components Standard - Native browser support, no framework lock-in
  • Cross-framework Reusable - Components work in React/Vue/Angular
  • 🎨 Theme System - 11 skins, light/dark mode, View Transitions
  • 🔐 Authentication - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization and business management
  • 🛡️ Permission Control - RBAC fine-grained permission management
  • 🪶 Lightweight - Core library ~5KB gzip
  • 🌓 Shadow DOM - Style isolation, avoid conflicts

Tech Stack

Technology Version Description
Lit 3.x Web Components framework
TypeScript 5.x Type safety
Tailwind CSS 4.x Atomic CSS
@lit-labs/router 0.1.x Client-side routing
@lit-labs/context 1.x Context state
Shoelace 2.x Web Components UI library
Zod 3.x Data validation
ECharts 5.x Chart visualization
Vite 6.x Build tool
Mock.js 1.x Data mocking

Core Features

  • Configurable Dashboard - 9 widgets, drag & drop layout, responsive design
  • Permission System - RBAC permission control, route guards, permission components
  • Theme System - 11 skins, light/dark mode, View Transitions
  • Reactive Properties - @property decorator for reactivity
  • Shadow DOM Isolation - Style encapsulation, avoid global conflicts
  • Native Support - Based on Web standards, compatible with all modern browsers

Directory Structure

halolight-lit/
├── src/
│   ├── pages/                     # Page components
│   │   ├── hl-home.ts            # Homepage
│   │   ├── auth/                 # Auth pages
│   │   │   ├── hl-login.ts
│   │   │   ├── hl-register.ts
│   │   │   ├── hl-forgot-password.ts
│   │   │   └── hl-reset-password.ts
│   │   └── dashboard/            # Dashboard pages
│   │       ├── hl-dashboard.ts
│   │       ├── hl-users.ts
│   │       ├── hl-user-detail.ts
│   │       ├── hl-user-create.ts
│   │       ├── hl-roles.ts
│   │       ├── hl-permissions.ts
│   │       ├── hl-settings.ts
│   │       └── hl-profile.ts
│   ├── components/               # Component library
│   │   ├── ui/                   # UI components
│   │   │   ├── hl-button.ts
│   │   │   ├── hl-input.ts
│   │   │   ├── hl-card.ts
│   │   │   └── hl-dialog.ts
│   │   ├── layout/               # Layout components
│   │   │   ├── hl-admin-layout.ts
│   │   │   ├── hl-auth-layout.ts
│   │   │   ├── hl-sidebar.ts
│   │   │   └── hl-header.ts
│   │   ├── dashboard/            # Dashboard components
│   │   │   ├── hl-dashboard-grid.ts
│   │   │   ├── hl-widget-wrapper.ts
│   │   │   └── hl-stats-widget.ts
│   │   └── shared/               # Shared components
│   │       └── hl-permission-guard.ts
│   ├── stores/                   # State management
│   │   ├── auth-context.ts
│   │   ├── ui-settings-context.ts
│   │   └── dashboard-context.ts
│   ├── lib/                      # Utilities
│   │   ├── api.ts
│   │   ├── permission.ts
│   │   └── styles.ts
│   ├── mock/                     # Mock data
│   ├── types/                    # Type definitions
│   ├── hl-app.ts                 # Root component
│   ├── router.ts                 # Route config
│   └── main.ts                   # Entry file
├── public/                       # Static assets
├── vite.config.ts               # Vite config
├── tailwind.config.ts           # Tailwind config
└── package.json

Quick Start

Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-lit.git
cd halolight-lit
pnpm install

Environment Variables

bash
cp .env.example .env
env
# .env
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:5173

Production Build

bash
pnpm build
pnpm preview

Core Features

State Management (@lit-labs/context)

ts
// stores/auth-context.ts
import { createContext } from '@lit-labs/context'
import { html, LitElement } from 'lit'
import { customElement, state } from 'lit/decorators.js'
import { provide } from '@lit-labs/context'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

interface AuthState {
  user: User | null
  token: string | null
  loading: boolean
  login: (credentials: { email: string; password: string }) => Promise<void>
  logout: () => void
  hasPermission: (permission: string) => boolean
}

export const authContext = createContext<AuthState>('auth')

@customElement('hl-auth-provider')
export class AuthProvider extends LitElement {
  @state() private user: User | null = null
  @state() private token: string | null = null
  @state() private loading = false

  @provide({ context: authContext })
  authState: AuthState = {
    user: null,
    token: null,
    loading: false,
    login: this.login.bind(this),
    logout: this.logout.bind(this),
    hasPermission: this.hasPermission.bind(this),
  }

  connectedCallback() {
    super.connectedCallback()
    this.loadFromStorage()
  }

  private loadFromStorage() {
    const saved = localStorage.getItem('auth')
    if (saved) {
      const { user, token } = JSON.parse(saved)
      this.user = user
      this.token = token
      this.updateContext()
    }
  }

  private updateContext() {
    this.authState = {
      ...this.authState,
      user: this.user,
      token: this.token,
      loading: this.loading,
    }
  }

  async login(credentials: { email: string; password: string }) {
    this.loading = true
    this.updateContext()

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: { 'Content-Type': 'application/json' },
      })
      const data = await response.json()

      this.user = data.user
      this.token = data.token
      localStorage.setItem('auth', JSON.stringify({
        user: this.user,
        token: this.token,
      }))
    } finally {
      this.loading = false
      this.updateContext()
    }
  }

  logout() {
    this.user = null
    this.token = null
    localStorage.removeItem('auth')
    this.updateContext()
  }

  hasPermission(permission: string): boolean {
    const perms = this.user?.permissions ?? []
    return perms.some(p =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  }

  render() {
    return html`<slot></slot>`
  }
}

Base Component

ts
// components/ui/hl-button.ts
import { LitElement, html, css } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { classMap } from 'lit/directives/class-map.js'

@customElement('hl-button')
export class HlButton extends LitElement {
  static styles = css`
    :host {
      display: inline-block;
    }

    button {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border-radius: 0.375rem;
      font-weight: 500;
      cursor: pointer;
      transition: all 0.2s;
    }

    .default {
      background-color: var(--primary);
      color: var(--primary-foreground);
    }

    .default:hover {
      opacity: 0.9;
    }

    .destructive {
      background-color: var(--destructive);
      color: var(--destructive-foreground);
    }

    .outline {
      border: 1px solid var(--border);
      background: transparent;
    }

    .sm { height: 2rem; padding: 0 0.75rem; font-size: 0.875rem; }
    .md { height: 2.5rem; padding: 0 1rem; }
    .lg { height: 3rem; padding: 0 1.5rem; font-size: 1.125rem; }

    .disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
  `

  @property() variant: 'default' | 'destructive' | 'outline' | 'ghost' = 'default'
  @property() size: 'sm' | 'md' | 'lg' = 'md'
  @property({ type: Boolean }) disabled = false

  render() {
    const classes = {
      [this.variant]: true,
      [this.size]: true,
      disabled: this.disabled,
    }

    return html`
      <button class=${classMap(classes)} ?disabled=${this.disabled}>
        <slot></slot>
      </button>
    `
  }
}

Route Configuration

ts
// router.ts
import { Router } from '@lit-labs/router'
import { html } from 'lit'

// Lazy load page components
const routes = [
  {
    path: '/',
    render: () => html`<hl-home></hl-home>`,
    enter: async () => {
      await import('./pages/hl-home.js')
      return true
    },
  },
  {
    path: '/login',
    render: () => html`<hl-login></hl-login>`,
    enter: async () => {
      await import('./pages/auth/hl-login.js')
      return true
    },
  },
  {
    path: '/dashboard',
    render: () => html`<hl-dashboard></hl-dashboard>`,
    enter: async ({ router }) => {
      // Route guard
      const authState = document.querySelector('hl-auth-provider')?.authState
      if (!authState?.token) {
        router.goto('/login?redirect=/dashboard')
        return false
      }
      await import('./pages/dashboard/hl-dashboard.js')
      return true
    },
  },
  {
    path: '/users',
    render: () => html`<hl-users></hl-users>`,
    enter: async ({ router }) => {
      const authState = document.querySelector('hl-auth-provider')?.authState
      if (!authState?.hasPermission('users:list')) {
        return false
      }
      await import('./pages/dashboard/hl-users.js')
      return true
    },
  },
  // More routes...
]

export function createRouter(host: HTMLElement) {
  return new Router(host, routes)
}

Permission Control

ts
// components/shared/hl-permission-guard.ts
import { LitElement, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { consume } from '@lit-labs/context'
import { authContext, type AuthState } from '../../stores/auth-context'

@customElement('hl-permission-guard')
export class HlPermissionGuard extends LitElement {
  @property() permission = ''

  @consume({ context: authContext, subscribe: true })
  authState!: AuthState

  render() {
    const hasPermission = this.authState?.hasPermission(this.permission)

    if (!hasPermission) {
      return html`<slot name="fallback"></slot>`
    }

    return html`<slot></slot>`
  }
}

Usage Example:

html
<hl-permission-guard permission="users:delete">
  <hl-button variant="destructive">Delete</hl-button>
  <span slot="fallback" class="text-muted-foreground">No permission</span>
</hl-permission-guard>

Draggable Dashboard

ts
// components/dashboard/hl-dashboard-grid.ts
import { LitElement, html, css } from 'lit'
import { customElement, state } from 'lit/decorators.js'
import Sortable from 'sortablejs'

@customElement('hl-dashboard-grid')
export class HlDashboardGrid extends LitElement {
  static styles = css`
    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
      gap: 1rem;
    }

    .widget {
      background: var(--card);
      border-radius: 0.5rem;
      padding: 1rem;
      cursor: move;
    }
  `

  @state() private widgets = [
    { id: 'stats', type: 'stats' },
    { id: 'chart', type: 'chart' },
    { id: 'table', type: 'table' },
  ]

  firstUpdated() {
    const grid = this.shadowRoot?.querySelector('.grid')
    if (grid) {
      new Sortable(grid as HTMLElement, {
        animation: 150,
        onEnd: (evt) => {
          const { oldIndex, newIndex } = evt
          if (oldIndex !== undefined && newIndex !== undefined) {
            const item = this.widgets.splice(oldIndex, 1)[0]
            this.widgets.splice(newIndex, 0, item)
            this.requestUpdate()
          }
        },
      })
    }
  }

  render() {
    return html`
      <div class="grid">
        ${this.widgets.map(widget => html`
          <div class="widget" data-id=${widget.id}>
            <hl-widget-wrapper type=${widget.type}></hl-widget-wrapper>
          </div>
        `)}
      </div>
    `
  }
}

Theme System

Skin Presets

Supports 11 preset skins, switch via quick settings panel:

Skin Primary Color CSS Variable
Default Purple --primary: 51.1% 0.262 276.97
Blue Blue --primary: 54.8% 0.243 264.05
Emerald Emerald --primary: 64.6% 0.178 142.49
Rose Rose --primary: 58.5% 0.217 12.53
Orange Orange --primary: 68.4% 0.197 41.73

CSS Variables (OKLch)

css
/* Theme variable definition */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 98% 0 0;
  --card: 100% 0 0;
  --card-foreground: 14.9% 0.017 285.75;
  --border: 93.3% 0.011 285.88;
  --radius: 0.5rem;
}

.dark {
  --background: 14.9% 0.017 285.75;
  --foreground: 98% 0 0;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 98% 0 0;
  --card: 14.9% 0.017 285.75;
  --card-foreground: 98% 0 0;
  --border: 25.1% 0.025 285.82;
}

Page Routes

Path Page Permission
/ Homepage Public
/login Login Public
/register Register Public
/forgot-password Forgot Password Public
/reset-password Reset Password Public
/dashboard Dashboard dashboard:view
/users User Management users:view
/users/create Create User users:create
/users/:id User Detail users:view
/roles Role Management roles:view
/permissions Permission Management permissions:view
/settings System Settings settings:view
/profile Profile settings:view

Using in Other Frameworks

React

tsx
import '@halolight/lit/hl-button'

function App() {
  return (
    <hl-button variant="default" onClick={() => console.log('clicked')}>
      Click
    </hl-button>
  )
}

Vue

vue
<template>
  <hl-button variant="default" @click="handleClick">
    Click
  </hl-button>
</template>

<script setup>
import '@halolight/lit/hl-button'

function handleClick() {
  console.log('clicked')
}
</script>

Angular

ts
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'
import '@halolight/lit/hl-button'

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
html
<hl-button variant="default" (click)="handleClick()">
  Click
</hl-button>

Common Commands

bash
pnpm dev            # Start dev server
pnpm build          # Production build
pnpm preview        # Preview production build
pnpm lint           # Code linting
pnpm lint:fix       # Auto fix
pnpm type-check     # Type checking
pnpm test           # Run tests
pnpm test:coverage  # Test coverage

Deployment

Deploy with Vercel

Docker

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Other Platforms

Demo Accounts

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Testing

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Single run
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI

Test Example

ts
// __tests__/hl-button.test.ts
import { expect, fixture, html } from '@open-wc/testing'
import '../src/components/ui/hl-button'

describe('hl-button', () => {
  it('renders with default variant', async () => {
    const el = await fixture(html`<hl-button>Click me</hl-button>`)
    const button = el.shadowRoot?.querySelector('button')
    expect(button).to.exist
    expect(button?.textContent?.trim()).to.equal('Click me')
  })

  it('applies variant classes', async () => {
    const el = await fixture(html`<hl-button variant="destructive">Delete</hl-button>`)
    const button = el.shadowRoot?.querySelector('button')
    expect(button?.classList.contains('destructive')).to.be.true
  })

  it('handles disabled state', async () => {
    const el = await fixture(html`<hl-button disabled>Disabled</hl-button>`)
    const button = el.shadowRoot?.querySelector('button')
    expect(button?.disabled).to.be.true
  })
})

Configuration

Vite Configuration

ts
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: 'src/main.ts',
      formats: ['es'],
    },
    rollupOptions: {
      external: /^lit/,
    },
  },
  server: {
    port: 5173,
  },
})

Tailwind Configuration

ts
// tailwind.config.ts
import type { Config } from 'tailwindcss'

export default {
  content: ['./index.html', './src/**/*.{ts,js}'],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        border: 'oklch(var(--border))',
        background: 'oklch(var(--background))',
        foreground: 'oklch(var(--foreground))',
        primary: {
          DEFAULT: 'oklch(var(--primary))',
          foreground: 'oklch(var(--primary-foreground))',
        },
      },
    },
  },
} satisfies Config

CI/CD

Complete GitHub Actions CI workflow configured:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

Lifecycle Hooks

ts
// Component lifecycle
@customElement('my-component')
export class MyComponent extends LitElement {
  // First connected to DOM
  connectedCallback() {
    super.connectedCallback()
    console.log('Component connected')
  }

  // First update complete
  firstUpdated(changedProperties: PropertyValues) {
    super.firstUpdated(changedProperties)
    console.log('First render complete')
  }

  // Each update complete
  updated(changedProperties: PropertyValues) {
    super.updated(changedProperties)
    if (changedProperties.has('value')) {
      console.log('Value changed:', this.value)
    }
  }

  // Removed from DOM
  disconnectedCallback() {
    super.disconnectedCallback()
    console.log('Component disconnected')
  }
}

Custom Directives

ts
// lib/directives/tooltip.ts
import { directive, Directive } from 'lit/directive.js'
import { AsyncDirective } from 'lit/async-directive.js'

class TooltipDirective extends AsyncDirective {
  render(text: string) {
    return text
  }

  update(part: any, [text]: [string]) {
    const element = part.element
    element.setAttribute('title', text)
    element.style.cursor = 'help'
    return this.render(text)
  }
}

export const tooltip = directive(TooltipDirective)
ts
// Usage
import { tooltip } from './lib/directives/tooltip'

render() {
  return html`
    <span ${tooltip('Tooltip message')}>Hover to see tooltip</span>
  `
}

Performance Optimization

Virtual Scrolling

ts
// components/ui/hl-virtual-list.ts
import { LitElement, html } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { repeat } from 'lit/directives/repeat.js'

@customElement('hl-virtual-list')
export class HlVirtualList extends LitElement {
  @property({ type: Array }) items: any[] = []
  @property({ type: Number }) itemHeight = 50
  @state() private visibleStart = 0
  @state() private visibleEnd = 20

  private handleScroll(e: Event) {
    const target = e.target as HTMLElement
    const scrollTop = target.scrollTop
    this.visibleStart = Math.floor(scrollTop / this.itemHeight)
    this.visibleEnd = this.visibleStart + 20
  }

  render() {
    const visibleItems = this.items.slice(this.visibleStart, this.visibleEnd)

    return html`
      <div class="container" @scroll=${this.handleScroll}>
        <div style="height: ${this.items.length * this.itemHeight}px">
          <div style="transform: translateY(${this.visibleStart * this.itemHeight}px)">
            ${repeat(
              visibleItems,
              item => item.id,
              item => html`<div class="item">${item.name}</div>`
            )}
          </div>
        </div>
      </div>
    `
  }
}

Lazy Loading Components

ts
// Route lazy loading
{
  path: '/dashboard',
  enter: async () => {
    await import('./pages/dashboard/hl-dashboard.js')
    return true
  },
}

// Dynamic import
async loadWidget(type: string) {
  const module = await import(`./widgets/hl-${type}-widget.js`)
  return module.default
}

Preloading

ts
// Preload critical routes
const preloadRoutes = ['/dashboard', '/users']

preloadRoutes.forEach(async (route) => {
  const link = document.createElement('link')
  link.rel = 'modulepreload'
  link.href = `./pages${route}.js`
  document.head.appendChild(link)
})

FAQ

Q: How to use global styles in Shadow DOM?

A: Use CSS custom properties or @import global styles:

ts
static styles = css`
  @import url('/global.css');

  :host {
    color: var(--foreground);
    background: var(--background);
  }
`

Q: How to handle form data two-way binding?

A: Use @input event and @state decorator:

ts
@customElement('hl-form')
export class HlForm extends LitElement {
  @state() private formData = { name: '', email: '' }

  private handleInput(field: string, value: string) {
    this.formData = { ...this.formData, [field]: value }
  }

  render() {
    return html`
      <input
        .value=${this.formData.name}
        @input=${(e: Event) =>
          this.handleInput('name', (e.target as HTMLInputElement).value)}
      />
    `
  }
}

Q: How to communicate between components?

A: Use custom events or Context API:

ts
// Dispatch event
this.dispatchEvent(new CustomEvent('data-changed', {
  detail: { data: this.data },
  bubbles: true,
  composed: true, // Penetrate Shadow DOM
}))

// Listen event
@customElement('parent-component')
export class ParentComponent extends LitElement {
  render() {
    return html`
      <child-component @data-changed=${this.handleDataChanged}></child-component>
    `
  }

  private handleDataChanged(e: CustomEvent) {
    console.log('Data:', e.detail.data)
  }
}

Comparison with Other Versions

Feature Lit Version Next.js Version Vue Version
SSR/SSG ✅ (Experimental) ✅ (Nuxt)
State Management @lit-labs/context Zustand Pinia
Routing @lit-labs/router App Router Vue Router
Build Tool Vite Next.js Vite
Cross-framework Reusable ✅ Native Support
Shadow DOM
Bundle Size 5KB (gzip) ~90KB ~60KB
]]>
<![CDATA[Netlify Deployment]]> https://halolight.docs.h7ml.cn/en/guide/netlify https://halolight.docs.h7ml.cn/en/guide/netlify Fri, 19 Dec 2025 04:56:41 GMT Netlify Deployment

HaloLight Netlify deployment edition, a one-click deployment solution optimized for the Netlify platform.

Live Preview: https://halolight-netlify.h7ml.cn

GitHub: https://github.com/halolight/halolight-netlify

Features

  • 🔷 One-Click Deploy - Deploy to Netlify button for fast launch
  • Global CDN - 300+ edge nodes for lightning-fast delivery
  • 🔄 Automatic CI/CD - Git push triggers auto build and deploy
  • 📝 Form Handling - Backend-free form submissions
  • 🔐 Identity - Built-in user authentication service
  • 🌐 Functions - Serverless functions (AWS Lambda)
  • 🔗 Split Testing - A/B testing and traffic splitting
  • 📊 Analytics - Server-side analytics (paid)

Quick Start

Deploy to Netlify

After clicking the button:

  1. Sign in to Netlify (supports GitHub/GitLab/Bitbucket)
  2. Authorize repository access
  3. Configure environment variables
  4. Auto-clone and deploy

Method 2: CLI Deploy

```bash

Install Netlify CLI

npm install -g netlify-cli

Sign in to Netlify

netlify login

Clone project

git clone https://github.com/halolight/halolight-netlify.git cd halolight-netlify

Install dependencies

pnpm install

Initialize Netlify site

netlify init

Local development (with Functions)

netlify dev

Deploy to production

netlify deploy --prod ```

Method 3: GitHub Integration

  1. Fork the halolight-netlify repository
  2. In the Netlify console click "Add new site" → "Import an existing project"
  3. Choose GitHub and authorize
  4. Select your Fork
  5. Configure build settings and deploy

Configuration Files

netlify.toml

```toml [build] command = "pnpm build" publish = ".next"

[build.environment] NODE_VERSION = "20" PNPM_VERSION = "9"

Next.js plugin (auto handles SSR/ISR)

[[plugins]] package = "@netlify/plugin-nextjs"

Production

[context.production] command = "pnpm build"

[context.production.environment] NEXT_PUBLIC_MOCK = "false"

Preview (branch deploy)

[context.deploy-preview] command = "pnpm build"

[context.deploy-preview.environment] NEXT_PUBLIC_MOCK = "true"

Branch deploys

[context.branch-deploy] command = "pnpm build"

Redirect rules

[[redirects]] from = "/api/*" to = "/.netlify/functions/:splat" status = 200

SPA fallback

[[redirects]] from = "/*" to = "/index.html" status = 200 conditions =

Custom headers

[[headers]] for = "/*" [headers.values] X-Frame-Options = "DENY" X-Content-Type-Options = "nosniff" X-XSS-Protection = "1; mode=block" Referrer-Policy = "strict-origin-when-cross-origin"

[[headers]] for = "/_next/static/*" [headers.values] Cache-Control = "public, max-age=31536000, immutable" ```

package.json Scripts

```json { "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "netlify:dev": "netlify dev", "netlify:build": "netlify build", "netlify:deploy": "netlify deploy --prod" } } ```

Environment Variables

In Netlify console → Site settings → Environment variables:

Variable Description Example
`NODE_ENV` Runtime environment `production`
`NEXT_PUBLIC_API_URL` Base API URL `/api`
`NEXT_PUBLIC_MOCK` Enable mock data `false`
`NEXT_PUBLIC_APP_TITLE` App title `Admin Pro`
`DATABASE_URL` Database connection `postgresql://...`

Environment Variable Scopes

Netlify supports per-context variables:

``` Production - Production environment variables Deploy Preview - PR preview variables Branch Deploy - Branch deploy variables All - Shared across all environments ```

Serverless Functions

Basic Function

```typescript // netlify/functions/hello.ts import type { Handler, HandlerEvent, HandlerContext } from "@netlify/functions";

export const handler: Handler = async ( event: HandlerEvent, context: HandlerContext ) => { const { httpMethod, body, queryStringParameters } = event;

return { statusCode: 200, headers: { "Content-Type": "application/json", }, body: JSON.stringify({ message: "Hello from Netlify Functions!", method: httpMethod, query: queryStringParameters, }), }; }; ```

Background Function (Long Running)

```typescript // netlify/functions/background-task.ts import type { BackgroundHandler } from "@netlify/functions";

export const handler: BackgroundHandler = async (event) => { // Maximum runtime 15 minutes console.log("Processing background task...");

// Perform time-consuming operations await processLongRunningTask(event.body);

// Background functions don't return a response };

// Configure as background function export const config = { type: "background", }; ```

Scheduled Functions

```typescript // netlify/functions/daily-report.ts import type { Handler } from "@netlify/functions";

export const handler: Handler = async () => { console.log("Generating daily report...");

await generateReport();

return { statusCode: 200, body: "Report generated", }; };

// Run daily at 9:00 UTC export const config = { schedule: "0 9 * * *", }; ```

Edge Functions

```typescript // netlify/edge-functions/geolocation.ts import type { Context } from "@netlify/edge-functions";

export default async (request: Request, context: Context) => { const { country, city } = context.geo;

// Geo-based response return new Response( JSON.stringify({ country, city, message: `Hello from ${city}, ${country}!`, }), { headers: { "Content-Type": "application/json" }, } ); };

export const config = { path: "/api/geo", }; ```

Netlify Identity

Configure Authentication

```typescript // lib/netlify-identity.ts import netlifyIdentity from "netlify-identity-widget";

// Initialize netlifyIdentity.init({ container: "#netlify-modal", locale: "zh", });

// Login export function login() { netlifyIdentity.open("login"); }

// Signup export function signup() { netlifyIdentity.open("signup"); }

// Logout export function logout() { netlifyIdentity.logout(); }

// Get current user export function getCurrentUser() { return netlifyIdentity.currentUser(); }

// Listen for auth state netlifyIdentity.on("login", (user) => { console.log("User logged in:", user); netlifyIdentity.close(); });

netlifyIdentity.on("logout", () => { console.log("User logged out"); }); ```

Protected Function

```typescript // netlify/functions/protected.ts import type { Handler } from "@netlify/functions";

export const handler: Handler = async (event) => { const { user } = event.context.clientContext || {};

if (!user) { return { statusCode: 401, body: JSON.stringify({ error: "Unauthorized" }), }; }

return { statusCode: 200, body: JSON.stringify({ message: Hello \${user.email}!, roles: user.app_metadata?.roles || [], }), }; };


## Form Handling

### HTML Form

\`\`\`html
<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field">
  <input type="hidden" name="form-name" value="contact" />
  <p class="hidden">
    <label>Don't fill this out: <input name="bot-field" /></label>
  </p>
  <p>
    <label>Email: <input type="email" name="email" required /></label>
  </p>
  <p>
    <label>Message: <textarea name="message" required></textarea></label>
  </p>
  <p>
    <button type="submit">Send</button>
  </p>
</form>
\`\`\`

### React Form

\`\`\`tsx
// components/ContactForm.tsx
"use client";

import { useState } from "react";

export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setStatus("loading");

    const formData = new FormData(e.currentTarget);

    try {
      const response = await fetch("/", {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams(formData as any).toString(),
      });

      if (response.ok) {
        setStatus("success");
      } else {
        setStatus("error");
      }
    } catch {
      setStatus("error");
    }
  };

  return (
    <form
      name="contact"
      method="POST"
      data-netlify="true"
      onSubmit={handleSubmit}
    >
      <input type="hidden" name="form-name" value="contact" />
      {/* Form fields */}
      <button type="submit" disabled={status === "loading"}>
        {status === "loading" ? "Sending..." : "Send"}
      </button>
      {status === "success" && <p>Message sent!</p>}
      {status === "error" && <p>Error sending message</p>}
    </form>
  );
}
\`\`\`

## Common Commands

\`\`\`bash
# Sign in
netlify login

# View site status
netlify status

# Local development
netlify dev

# Build
netlify build

# Deploy preview
netlify deploy

# Deploy to production
netlify deploy --prod

# Open site
netlify open

# Open dashboard
netlify open:admin

# View logs
netlify logs

# Environment variables
netlify env:list
netlify env:set KEY value
netlify env:unset KEY

# Link to site
netlify link

# Unlink
netlify unlink
\`\`\`

## Monitoring and Logs

### View Logs

\`\`\`bash
# CLI view logs
netlify logs

# Live log stream
netlify logs --live

# Function logs
netlify logs:function hello
\`\`\`

### Build Plugins

\`\`\`toml
# netlify.toml

# Performance analysis
[[plugins]]
  package = "netlify-plugin-lighthouse"

# Cache optimization
[[plugins]]
  package = "netlify-plugin-cache"
  [plugins.inputs]
    paths = [".next/cache", "node_modules/.cache"]

# Commit status notifications
[[plugins]]
  package = "netlify-plugin-checklinks"
\`\`\`

## Custom Domains

### Add Domain

1. Go to Site settings → Domain management
2. Click "Add custom domain"
3. Enter your domain

### DNS Configuration

\`\`\`
# A record (root)
Type: A
Name: @
Value: 75.2.60.5

# CNAME record (subdomain)
Type: CNAME
Name: www
Value: your-site.netlify.app
\`\`\`

### HTTPS

Netlify configures HTTPS automatically:
- Automatically requests Let's Encrypt certificates
- Auto renewal
- Enforces HTTPS redirects

## Branch Deploys and Previews

### Deploy Contexts

| Context | Trigger | URL format |
|--------|----------|----------|
| Production | main branch push | \`your-site.netlify.app\` |
| Deploy Preview | PR created/updated | \`deploy-preview-123--your-site.netlify.app\` |
| Branch Deploy | Other branch push | \`branch-name--your-site.netlify.app\` |

### Lock Deploys

\`\`\`bash
# Lock current deploy (stop auto deploys)
netlify deploy:lock

# Unlock
netlify deploy:unlock
\`\`\`

## FAQ

### Q: What if the build fails?

A: Check the following:
1. Review build logs and ensure dependencies install correctly
2. Confirm \`pnpm-lock.yaml\` is committed
3. Check Node.js version (build.environment.NODE_VERSION)
4. Verify the build command is correct

### Q: How to roll back a deployment?

A: On the Deploys page:
1. Find a previous successful deploy
2. Click "Publish deploy"
3. Or use CLI: \`netlify rollback\`

### Q: Functions cold starts are slow?

A: Optimization tips:
1. Reduce function bundle size
2. Use Edge Functions (no cold start)
3. Use Background Functions for heavy tasks

### Q: How to set redirects?

A: Configure in \`netlify.toml\` or \`_redirects\`:

\`\`\`toml
# netlify.toml
[[redirects]]
  from = "/old-path"
  to = "/new-path"
  status = 301

# Proxy
[[redirects]]
  from = "/api/*"
  to = "https://api.example.com/:splat"
  status = 200
\`\`\`

## Pricing

| Plan | Price | Features |
|------|------|------|
| Starter | Free | 100GB bandwidth, 300 build minutes |
| Pro | $19/member/month | 1TB bandwidth, 25000 build minutes |
| Business | $99/member/month | Custom SLA, SSO |
| Enterprise | Contact sales | Dedicated support, compliance |

### Functions Quotas

| Plan | Invocations | Runtime |
|------|----------|----------|
| Starter | 125K/month | 100 hours |
| Pro | Unlimited | 1000 hours |

## Comparison With Other Platforms

| Feature | Netlify | Vercel | Cloudflare |
|------|---------|--------|------------|
| One-click deploy | ✅ | ✅ | ✅ |
| Edge Functions | ✅ | ✅ | ✅ |
| Form handling | ✅ Built-in | ❌ External | ❌ External |
| Identity | ✅ Built-in | ❌ External | ✅ Access |
| Free bandwidth | 100GB | 100GB | Unlimited |
| Free builds | 300 minutes | 6000 minutes | 500 runs |
| Split Testing | ✅ | ⚠️ Limited | ❌ |

## Related Links

- [Live Preview](https://halolight-netlify.h7ml.cn)
- [GitHub Repository](https://github.com/halolight/halolight-netlify)
- [Netlify Docs](https://docs.netlify.com)
- [Netlify CLI Docs](https://cli.netlify.com)
- [Netlify Functions Docs](https://docs.netlify.com/functions/overview)
- [HaloLight Docs](https://docs.halolight.h7ml.cn)
]]>
<![CDATA[Next.js Version]]> https://halolight.docs.h7ml.cn/en/guide/nextjs https://halolight.docs.h7ml.cn/en/guide/nextjs Fri, 19 Dec 2025 04:56:41 GMT Next.js Version

HaloLight Next.js version is built on Next.js 14 App Router with React 18 + TypeScript.

Live Preview: https://halolight.h7ml.cn/

GitHub: https://github.com/halolight/halolight

Features

  • 🏗️ Next.js 14 App Router - Server components & streaming rendering
  • Zustand State Management - Lightweight state management solution
  • 🎨 Theme System - 11 skins, dark/light mode, View Transitions
  • 🔐 Authentication System - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization & business management
  • 🛡️ Permission Control - RBAC fine-grained permission management
  • 📑 Multi-Tab Navigation - Tab bar management
  • Command Palette - Keyboard shortcut navigation

Tech Stack

Technology Version Description
Next.js 14.x React full-stack framework (App Router)
React 18.x UI library
TypeScript 5.x Type safety
Tailwind CSS 4.x Atomic CSS
shadcn/ui latest UI component library (28 components)
Zustand 5.x State management (6 Stores)
TanStack Query 5.x Server state
React Hook Form 7.x Form handling
Zod 4.x Data validation
react-grid-layout 1.x Drag-and-drop layout
Recharts 3.x Chart visualization
Framer Motion 12.x Animation effects
Mock.js 1.x Data mocking
next-pwa 5.x PWA support

Core Features

  • Configurable Dashboard - 9 widget types, drag-and-drop layout, responsive adaptation
  • Multi-Tab Navigation - Browser-style tabs, context menu, state caching
  • Permission System - RBAC permission control, route guards, permission components
  • Theme System - 11 skins, dark/light mode, View Transitions
  • Multi-Account Switching - Quick account switch, remember login state
  • Command Palette - Keyboard shortcuts (⌘K), global search
  • Real-time Notifications - WebSocket push, notification center

Directory Structure

halolight/
├── src/
│   ├── app/                      # App Router pages
│   │   ├── (auth)/              # Auth route group
│   │   │   ├── login/           # Login
│   │   │   ├── register/        # Register
│   │   │   ├── forgot-password/ # Forgot password
│   │   │   ├── reset-password/  # Reset password
│   │   │   └── layout.tsx       # Auth layout
│   │   ├── (dashboard)/         # Dashboard route group
│   │   │   ├── page.tsx         # Dashboard home (configurable)
│   │   │   ├── accounts/        # Account & permissions
│   │   │   ├── analytics/       # Analytics
│   │   │   ├── calendar/        # Calendar
│   │   │   ├── docs/            # Help docs
│   │   │   ├── documents/       # Document management
│   │   │   ├── files/           # File storage
│   │   │   ├── messages/        # Message center
│   │   │   ├── notifications/   # Notification center
│   │   │   ├── profile/         # User profile
│   │   │   ├── users/           # User management
│   │   │   ├── settings/        # System settings
│   │   │   │   └── teams/       # Team management
│   │   │   │       └── roles/   # Role management
│   │   │   └── layout.tsx       # Dashboard layout
│   │   ├── (legal)/             # Legal route group
│   │   │   ├── privacy/         # Privacy policy
│   │   │   ├── terms/           # Terms of service
│   │   │   └── layout.tsx
│   │   ├── layout.tsx           # Root layout
│   │   ├── error.tsx            # Error page
│   │   └── not-found.tsx        # 404 page
│   ├── components/
│   │   ├── ui/                  # shadcn/ui components (28)
│   │   ├── layout/              # Layout components (11)
│   │   │   ├── admin-layout.tsx # Admin layout
│   │   │   ├── sidebar.tsx      # Collapsible sidebar
│   │   │   ├── header.tsx       # Header (notifications/errors/user menu)
│   │   │   ├── footer.tsx       # Footer
│   │   │   ├── tab-bar.tsx      # Multi-tab navigation
│   │   │   ├── command-menu.tsx # Command palette (⌘K)
│   │   │   ├── quick-settings.tsx # UI settings panel
│   │   │   ├── theme-toggle.tsx # Theme toggle
│   │   │   └── pending-overlay.tsx # Loading overlay
│   │   ├── dashboard/           # Dashboard components
│   │   │   ├── configurable-dashboard.tsx # Configurable dashboard
│   │   │   ├── charts.tsx       # Chart components
│   │   │   ├── stats-card.tsx   # Stats card
│   │   │   └── recent-activity.tsx # Recent activity
│   │   └── shared/              # Shared components
│   ├── hooks/                   # React Hooks (15)
│   │   ├── use-users.ts         # User CRUD
│   │   ├── use-teams.ts         # Team management
│   │   ├── use-messages.ts      # Message management
│   │   ├── use-notifications.ts # Notification management
│   │   ├── use-calendar.ts      # Calendar data
│   │   ├── use-documents.ts     # Document management
│   │   ├── use-files.ts         # File management
│   │   ├── use-dashboard.ts     # Dashboard state
│   │   ├── use-dashboard-data.ts # Dashboard data Hook collection
│   │   ├── use-chart-palette.ts # Chart palette (theme-aware)
│   │   ├── use-action-mutation.ts # Server Action wrapper
│   │   ├── use-keep-alive.tsx   # Page state caching
│   │   ├── use-tdk.ts           # TDK management
│   │   └── use-title.ts         # Page title
│   ├── stores/                  # Zustand Stores (6)
│   │   ├── auth-store.ts        # Auth state (with multi-account)
│   │   ├── ui-settings-store.ts # UI settings
│   │   ├── dashboard-store.ts   # Dashboard state
│   │   ├── navigation-store.ts  # Navigation state
│   │   ├── tabs-store.ts        # Tab state
│   │   └── error-store.ts       # Error collection
│   ├── providers/               # React Providers (8)
│   │   ├── app-providers.tsx    # Provider aggregation
│   │   ├── auth-provider.tsx    # Auth Provider
│   │   ├── theme-provider.tsx   # Theme Provider
│   │   ├── query-provider.tsx   # TanStack Query
│   │   ├── error-provider.tsx   # Error handling
│   │   ├── permission-provider.tsx # Permission check
│   │   ├── websocket-provider.tsx # WebSocket real-time notifications
│   │   └── keep-alive-provider.tsx # Page keep-alive
│   ├── actions/                 # Server Actions
│   ├── config/                  # Configuration
│   │   ├── routes.ts            # Routes & permissions config
│   │   └── tdk.ts               # TDK config
│   ├── lib/                     # Utility library
│   │   └── api/                 # API client
│   ├── mock/                    # Mock data (9 modules)
│   └── middleware.ts            # Middleware (auth + security headers)
├── public/
│   ├── manifest.json            # PWA manifest
│   ├── sw.js                    # Service Worker
│   ├── icons/                   # PWA icons (8 sizes)
│   ├── screenshots/             # PWA screenshots
│   └── fonts/                   # Self-hosted fonts
├── next.config.mjs              # Next.js + PWA config
├── tailwind.config.js
├── tsconfig.json
└── package.json

Quick Start

Environment Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight.git
cd halolight
pnpm install

Environment Variables

bash
cp .env.example .env.local
env
# .env.local example
NEXT_PUBLIC_API_URL=/api
NEXT_PUBLIC_MOCK=true              # Enable Mock data
[email protected]
NEXT_PUBLIC_DEMO_PASSWORD=123456
NEXT_PUBLIC_SHOW_DEMO_HINT=false
NEXT_PUBLIC_WS_URL=                # WebSocket URL
NEXT_PUBLIC_APP_TITLE=Admin Pro
NEXT_PUBLIC_BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:3000

Build for Production

bash
pnpm build
pnpm start

Demo Account

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Core Functionality

1. State Management (Zustand)

tsx
// stores/auth-store.ts
interface AuthState {
  user: AccountWithToken | null
  accounts: AccountWithToken[]        // Multi-account list
  activeAccountId: string | null      // Current account

  login: (data: LoginRequest) => Promise<void>
  register: (data: RegisterRequest) => Promise<void>
  logout: () => Promise<void>
  switchAccount: (accountId: string) => Promise<void>  // Quick account switch
  forgotPassword: (email: string) => Promise<void>
  resetPassword: (token: string, password: string) => Promise<void>
  checkAuth: () => Promise<void>
}

// Cookie-based token storage with "Remember me" support (7 days / 1 day)
Cookies.set("token", response.token, {
  expires: data.remember ? 7 : 1,
  secure: process.env.NODE_ENV === "production",
  sameSite: "strict",
})

2. Data Fetching (TanStack Query)

tsx
// hooks/use-users.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

export function useUsers() {
  const queryClient = useQueryClient()

  // Query user list
  const { data, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  // Create user
  const createUser = useMutation({
    mutationFn: createUserApi,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })

  return { data, isLoading, createUser }
}

3. Permission Control

tsx
// Route permission config
export const ROUTE_PERMISSIONS: Record<string, Permission> = {
  "/": "dashboard:view",
  "/users": "users:view",
  "/analytics": "analytics:view",
  // ...
}

// Permission check
const { hasPermission } = usePermission()
if (hasPermission("users:delete")) {
  // Show delete button
}
tsx
// Permission guard component
<PermissionGuard permission="users:delete" fallback={<Disabled />}>
  <DeleteButton />
</PermissionGuard>

4. Draggable Dashboard

tsx
// Dashboard edit mode
const { isEditing, setIsEditing, addWidget, removeWidget, resetToDefault } = useDashboardStore()

// Responsive layout (columns auto-adapt)
// lg: 12 cols, md: 8 cols, sm: 4 cols, xs: 2 cols, mobile: 1 col

Supports 9 widget types:

Widget Type Description Data Source
stats Statistics card (4 metrics) useDashboardStats
chart-line Line chart (visit trends) useDashboardVisits
chart-bar Bar chart (sales statistics) useDashboardSales
chart-pie Pie chart (traffic distribution) useDashboardPie
recent-users Recent users list useDashboardUsers
notifications Notifications list useDashboardNotifications
tasks Todo tasks useDashboardTasks
calendar Today's schedule useDashboardCalendar
quick-actions Quick action shortcuts Static config

Theme System

Skin Presets

Supports 11 skin presets with live preview and smooth transition animations:

Preset Name Description
default Shadcn · Neutral Official default neutral colors
blue Shadcn · Blue Blue primary + cool-toned charts
emerald Shadcn · Emerald Fresh green, ideal for data display
amber Shadcn · Amber Amber/orange, warm and vibrant
violet Shadcn · Violet High-saturation purple, tech feel
rose Shadcn · Rose Rose primary, contrasting charts
teal Shadcn · Teal Teal primary, modern feel
slate Shadcn · Slate Low-saturation gray-blue, utilitarian
ocean Legacy · Ocean Blue Blue-green gradient
sunset Legacy · Sunset Orange Orange-pink contrast
aurora Legacy · Aurora Green Cyan-green + purple

CSS Variables (OKLch)

css
/* Example variable definitions */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --muted: 96.4% 0.004 285.75;
  --accent: 96.4% 0.004 285.75;
  /* ... */
}

Page Routes

Path Page Permission
/ Redirect to dashboard -
/login Login Public
/register Register Public
/forgot-password Forgot password Public
/reset-password Reset password Public
/dashboard Configurable dashboard dashboard:view
/accounts Account & permissions settings:view
/analytics Analytics analytics:view
/calendar Calendar calendar:view
/documents Document management documents:view
/files File storage files:view
/messages Message center messages:view
/notifications Notification center notifications:view
/users User management users:view
/settings System settings settings:view
/settings/teams Team settings settings:view
/settings/teams/roles Role management settings:view
/profile User profile settings:view
/docs Help docs documents:view
/privacy Privacy policy Public
/terms Terms of service Public

Environment Variables

Configuration Example

bash
cp .env.example .env.local
env
# .env.local
NEXT_PUBLIC_API_URL=/api
NEXT_PUBLIC_MOCK=true
[email protected]
NEXT_PUBLIC_DEMO_PASSWORD=123456
NEXT_PUBLIC_SHOW_DEMO_HINT=false
NEXT_PUBLIC_WS_URL=
NEXT_PUBLIC_APP_TITLE=Admin Pro
NEXT_PUBLIC_BRAND_NAME=Halolight

Variable Description

Variable Description Default
NEXT_PUBLIC_API_URL API base path /api
NEXT_PUBLIC_MOCK Enable Mock data true
NEXT_PUBLIC_DEMO_EMAIL Demo account email [email protected]
NEXT_PUBLIC_DEMO_PASSWORD Demo account password 123456
NEXT_PUBLIC_SHOW_DEMO_HINT Show demo hint false
NEXT_PUBLIC_WS_URL WebSocket URL -
NEXT_PUBLIC_APP_TITLE Application title Admin Pro
NEXT_PUBLIC_BRAND_NAME Brand name Halolight

Usage

tsx
// Use in client components
const apiUrl = process.env.NEXT_PUBLIC_API_URL
const isMock = process.env.NEXT_PUBLIC_MOCK === 'true'

Common Commands

bash
pnpm dev            # Start development server
pnpm build          # Production build
pnpm start          # Preview production build
pnpm lint           # Code linting
pnpm lint:fix       # Auto fix
pnpm type-check     # Type checking
pnpm test           # Run tests
pnpm test:coverage  # Test coverage

Testing

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Single run
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI interface

Test Example

tsx
// __tests__/components/button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '@/components/ui/button'

describe('Button Component', () => {
  it('renders correctly', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('handles click events', () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

Configuration

Next.js Configuration

js
// next.config.mjs
import withPWA from "next-pwa"

const nextConfig = {
  // Package import optimization - reduce bundle size
  experimental: {
    optimizePackageImports: [
      "@radix-ui/react-*",
      "lucide-react",
      "framer-motion",
      "@tanstack/react-query",
      "recharts",
      "zustand",
    ],
  },

  // Remove console in production
  compiler: {
    removeConsole: { exclude: ["error", "warn"] },
  },

  // Disable source maps
  productionBrowserSourceMaps: false,

  // Image optimization
  images: {
    formats: ["image/avif", "image/webp"],
  },
}

const pwaConfig = withPWA({
  dest: "public",
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === "development",
})

export default pwaConfig(nextConfig)

Deployment

Deploy with Vercel

bash
vercel

Docker

dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]
bash
docker build -t halolight-nextjs .
docker run -p 3000:3000 halolight-nextjs

Other Platforms

CI/CD

The project has a complete GitHub Actions CI workflow configured:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

Multi-Tab Navigation

tsx
// stores/tabs-store.ts
interface Tab {
  id: string
  title: string
  path: string
  icon?: string
  closable?: boolean  // Home tab not closable
}

// Context menu features
- Refresh page
- Close tab
- Close others
- Close right tabs
- Close all

Page State Caching (Keep-Alive)

tsx
// hooks/use-keep-alive.tsx

// Auto save/restore scroll position
useScrollRestore()

// Save form state
const [values, saveValues, clearCache] = useFormCache('filter-form', initialValues)

// Save custom state
const [state, setState] = useStateCache('my-key', initialValue)

Command Palette (⌘K)

tsx
// components/layout/command-menu.tsx
// Supports keyboard quick navigation, theme switching, account switching, logout, etc.

Shortcuts:
-K / Ctrl+K - Open command palette
- Search pages - Quick navigation to any page
- Switch theme - Toggle dark/light mode
- Switch account - Quick account switching

Real-time Notifications (WebSocket)

tsx
// providers/websocket-provider.tsx
const { status, lastMessage, sendMessage, reconnect } = useWebSocket()

// Listen for new notifications
useRealtimeNotifications((notification) => {
  console.log('New notification:', notification)
})

// Connection status
status === 'Open' // Connected
status === 'Connecting' // Connecting
status === 'Closed' // Disconnected

PWA Support

js
// next.config.mjs
const pwaConfig = withPWA({
  dest: "public",
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === "development",
  runtimeCaching: [
    // Font caching (1 year)
    { urlPattern: /\.(?:woff|woff2|ttf)$/i, handler: "CacheFirst" },
    // Image caching (24 hours)
    { urlPattern: /\.(?:jpg|png|svg|webp)$/i, handler: "StaleWhileRevalidate" },
    // Next.js static assets (1 year)
    { urlPattern: /\/_next\/static\/.+\.(js|css)$/i, handler: "CacheFirst" },
    // Page data (1 hour)
    { urlPattern: /\/_next\/data\/.+\.json$/i, handler: "NetworkFirst" },
  ],
})

Features:

  • Offline Access - Service Worker caches static assets
  • Install to Desktop - Supports Add to Home Screen
  • Self-hosted Fonts - Inter + JetBrains Mono
  • Icon Support - 8 sizes (72x72 ~ 512x512)

Performance Optimization

Image Optimization

tsx
// Use Next.js Image component
import Image from 'next/image'

<Image
  src="/images/hero.png"
  alt="Hero"
  width={800}
  height={600}
  priority // Priority loading
  placeholder="blur" // Blur placeholder
/>

// next.config.mjs
images: {
  formats: ["image/avif", "image/webp"],
}

Lazy Loading Components

tsx
// Dynamic import components
import dynamic from 'next/dynamic'

const DashboardChart = dynamic(
  () => import('@/components/dashboard/chart'),
  {
    loading: () => <Skeleton />,
    ssr: false // Disable SSR
  }
)

Preloading

tsx
// Route preloading
import Link from 'next/link'

<Link href="/dashboard" prefetch>
  Dashboard
</Link>

// Data preloading
queryClient.prefetchQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})

Package Import Optimization

js
// next.config.mjs
experimental: {
  optimizePackageImports: [
    "@radix-ui/react-*",
    "lucide-react",
    "framer-motion",
    "@tanstack/react-query",
    "recharts",
    "zustand",
  ],
}

FAQ

Q: How to disable Mock data?

A: Set NEXT_PUBLIC_MOCK=false in .env.local and configure real API address.

env
NEXT_PUBLIC_MOCK=false
NEXT_PUBLIC_API_URL=https://api.example.com

Q: How to add a new page?

A: Create a new directory and page.tsx file under src/app/(dashboard).

tsx
// src/app/(dashboard)/my-page/page.tsx
export default function MyPage() {
  return <div>My Page</div>
}

// Add route permission
// src/config/routes.ts
export const ROUTE_PERMISSIONS = {
  // ...
  "/my-page": "my-page:view",
}

Q: How to customize theme colors?

A: Modify CSS variables in tailwind.config.js.

js
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: 'oklch(var(--primary))',
          foreground: 'oklch(var(--primary-foreground))',
        },
      },
    },
  },
}
css
/* app/globals.css */
:root {
  --primary: 51.1% 0.262 276.97; /* Change to your color */
}

Q: How to disable PWA?

A: Set disable: true in next.config.mjs.

js
const pwaConfig = withPWA({
  dest: "public",
  disable: true, // Disable PWA
})

Q: How to deploy to static hosting platforms?

A: Configure static export mode.

js
// next.config.mjs
export default {
  output: 'export',
  images: {
    unoptimized: true, // Need to disable image optimization for static export
  },
}
bash
pnpm build
# Output to out/ directory

Comparison with Other Versions

Feature Next.js Vue Angular
SSR/SSG ✅ (Nuxt) ✅ (Universal)
State Management Zustand Pinia Services + RxJS
Router App Router Vue Router Angular Router
Build Tool Next.js Vite esbuild + Vite
Component Library shadcn/ui shadcn-vue Angular Material
Learning Curve Medium Low High
Performance Excellent Excellent Excellent
]]>
<![CDATA[Nuxt Version]]> https://halolight.docs.h7ml.cn/en/guide/nuxt https://halolight.docs.h7ml.cn/en/guide/nuxt Fri, 19 Dec 2025 04:56:41 GMT Nuxt Version

Halolight Nuxt version is built on Nuxt 3 with Vue 3.5 + Composition API + TypeScript, providing an out-of-the-box full-stack development experience.

Live Preview: https://halolight-nuxt.h7ml.cn/

GitHub: https://github.com/halolight/halolight-nuxt

Features

  • 🔄 Auto Import - Components, composables, and APIs auto-imported with zero config
  • 📁 File-Based Routing - Automatic routing from the file system
  • 🌐 Full-Stack Development - Built-in Nitro server, unified frontend and backend
  • 🚀 SSR/SSG/SPA - Flexible rendering modes
  • Vite Powered - Lightning-fast HMR
  • 🔌 Module Ecosystem - Rich Nuxt module extensions
  • 🎨 Theme System - Light/dark theme toggle
  • 🔐 Authentication - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization and business management
  • 🛡️ Access Control - Fine-grained RBAC permission management
  • Command Palette - ⌘/Ctrl + K quick navigation

Tech Stack

Technology Version Description
Nuxt 3.10 Vue full-stack framework
Vue 3.5+ Progressive framework
TypeScript 5.7 Type safety
Tailwind CSS 3.x (CDN) Atomic CSS
Pinia 0.5 State management
VueUse 10.x Composables utility library
Mock.js 1.x Data mocking

Core Features

  • Full-Stack Development: Built-in Nitro server for unified frontend and backend development
  • Auto Import: Automatic import of components, composables, and APIs
  • File-Based Routing: Automatic routing based on file system
  • SSR/SSG: Optional server-side rendering and static generation
  • Command Palette: ⌘/Ctrl + K for quick navigation
  • Hot Reload: Excellent HMR development experience
  • Module Ecosystem: Rich Nuxt module extensions

Directory Structure

halolight-nuxt/
├── nuxt.config.ts              # Nuxt configuration
├── app.vue                     # App root component
├── pages/                      # File-based routing
│   ├── index.vue              # Home page
│   ├── login.vue              # Login
│   ├── register.vue           # Register
│   ├── forgot-password.vue    # Forgot password
│   ├── reset-password.vue     # Reset password
│   ├── terms.vue              # Terms of service
│   ├── privacy.vue            # Privacy policy
│   ├── dashboard/             # Dashboard
│   │   └── index.vue
│   ├── users/                 # User management
│   │   └── index.vue
│   ├── messages/              # Messages
│   │   └── index.vue
│   ├── analytics/             # Analytics
│   │   └── index.vue
│   ├── profile/               # User profile
│   │   └── index.vue
│   └── settings/              # System settings
│       └── index.vue
├── components/                 # Auto-imported components
│   └── common/                # Common components
│       ├── AppHeader.vue
│       ├── AppSidebar.vue
│       ├── AppFooter.vue
│       ├── AppTabs.vue
│       ├── CommandMenu.vue
│       └── ToastContainer.vue
├── composables/                # Composable functions
│   ├── useTheme.ts
│   ├── useToast.ts
│   └── useCommandMenu.ts
├── layouts/                    # Layouts
│   ├── default.vue            # Admin layout
│   └── auth.vue               # Auth layout
├── middleware/                 # Middleware
│   └── auth.global.ts
├── plugins/                    # Plugins
│   └── pinia-persist.client.ts
├── stores/                     # Pinia stores
│   ├── auth.ts
│   ├── ui-settings.ts
│   ├── dashboard.ts
│   ├── layout.ts
│   └── tabs.ts
├── utils/                      # Utility functions
│   ├── index.ts
│   └── mock.ts
├── assets/css/                 # Style files
│   ├── main.css
│   └── tailwind.css
├── tests/                      # Test files
│   └── unit/
├── .github/                    # GitHub Actions
│   └── workflows/
│       ├── ci.yml
│       └── deploy.yml
└── public/                     # Static assets

Quick Start

Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-nuxt.git
cd halolight-nuxt
pnpm install

Environment Variables

bash
cp .env.example .env.local
env
# .env.local
NUXT_PUBLIC_API_BASE=/api
NUXT_PUBLIC_MOCK=true
[email protected]
NUXT_PUBLIC_DEMO_PASSWORD=123456
NUXT_PUBLIC_SHOW_DEMO_HINT=true
NUXT_PUBLIC_APP_TITLE=Admin Pro
NUXT_PUBLIC_BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:3000

Build for Production

bash
pnpm build
pnpm preview

Demo Account

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Core Functionality

State Management (Pinia)

ts
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null)
  const token = ref('')
  const loading = ref(false)
  const error = ref('')

  const isAuthenticated = computed(() => !!token.value && !!user.value)

  async function login(credentials: LoginCredentials) {
    loading.value = true
    error.value = ''
    try {
      // Login logic
      const result = await mockLogin(credentials)
      user.value = result.user
      token.value = result.token
    } catch (e) {
      error.value = e.message
      throw e
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
    token.value = ''
    navigateTo('/login')
  }

  return { user, token, loading, error, isAuthenticated, login, logout }
})

Data Fetching (useFetch)

vue
<script setup lang="ts">
// Using useFetch with automatic SSR handling
const { data: users, pending, error, refresh } = await useFetch('/api/users', {
  query: { page: 1, limit: 10 },
})

// Using useAsyncData with custom key
const { data: stats } = await useAsyncData('dashboard-stats', () =>
  $fetch('/api/dashboard/stats')
)
</script>

Access Control

ts
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
  const authStore = useAuthStore()

  // Public pages list
  const publicPages = ['/login', '/register', '/forgot-password', '/reset-password']

  if (publicPages.includes(to.path)) {
    return
  }

  if (!authStore.isAuthenticated) {
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath },
    })
  }
})

Draggable Dashboard

vue
<script setup lang="ts">
// Dashboard configuration
const dashboardStore = useDashboardStore()
const widgets = computed(() => dashboardStore.widgets)

// Drag implementation
function handleDragEnd(event) {
  dashboardStore.updateLayout(event.newLayout)
}
</script>

Page Routes

Path Page Permission
/ Home Public
/login Login Public
/register Register Public
/forgot-password Forgot password Public
/reset-password Reset password Public
/terms Terms of service Public
/privacy Privacy policy Public
/dashboard Dashboard dashboard:view
/users User management users:view
/messages Messages messages:view
/analytics Analytics analytics:view
/profile User profile settings:view
/settings System settings settings:view

Environment Variables

Configuration Example

bash
# .env
NUXT_PUBLIC_API_BASE=/api
NUXT_PUBLIC_MOCK=true
NUXT_PUBLIC_DEMO_EMAIL=[email protected]
NUXT_PUBLIC_DEMO_PASSWORD=123456
NUXT_PUBLIC_SHOW_DEMO_HINT=true
NUXT_PUBLIC_APP_TITLE=Admin Pro
NUXT_PUBLIC_BRAND_NAME=Halolight

# Server private variables
NUXT_JWT_SECRET=your-jwt-secret
NUXT_DATABASE_URL=postgresql://localhost:5432/halolight

Variable Description

Variable Name Description Default
NUXT_PUBLIC_API_BASE API base URL /api
NUXT_PUBLIC_MOCK Enable mock data true
NUXT_PUBLIC_APP_TITLE App title Admin Pro
NUXT_PUBLIC_BRAND_NAME Brand name Halolight
NUXT_PUBLIC_DEMO_EMAIL Demo account email -
NUXT_PUBLIC_DEMO_PASSWORD Demo account password -
NUXT_JWT_SECRET JWT secret (server) -
NUXT_DATABASE_URL Database connection (server) -

Usage

vue
<script setup lang="ts">
// In components
const config = useRuntimeConfig();

// Public variables
const apiBase = config.public.apiBase;

// Private variables (server only)
// const jwtSecret = config.jwtSecret; // Not accessible on client
</script>
typescript
// In server/api
export default defineEventHandler((event) => {
  const config = useRuntimeConfig();
  const jwtSecret = config.jwtSecret; // Can access private variables
});

Common Commands

bash
# Development
pnpm dev              # Start dev server
pnpm dev -o           # Start and open browser

# Build
pnpm build            # Production build
pnpm preview          # Preview production build
pnpm generate         # Static site generation (SSG)

# Checks
pnpm typecheck        # TypeScript type check
pnpm lint             # ESLint check
pnpm lint:fix         # ESLint autofix

# Tests
pnpm test             # Run tests
pnpm test:run         # Single run
pnpm test:coverage    # Coverage report

# Nuxt CLI
npx nuxi add page <name>       # Add page
npx nuxi add component <name>  # Add component
npx nuxi add composable <name> # Add composable
npx nuxi add plugin <name>     # Add plugin
npx nuxi add middleware <name> # Add middleware
npx nuxi add api <name>        # Add API route
npx nuxi upgrade               # Upgrade Nuxt

Testing

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Single run
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI

Test Example

typescript
// tests/unit/stores/auth.spec.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useAuthStore } from '~/stores/auth'

describe('Auth Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('should have correct initial state', () => {
    const store = useAuthStore()
    expect(store.user).toBeNull()
    expect(store.token).toBe('')
    expect(store.isAuthenticated).toBe(false)
  })

  it('should login successfully', async () => {
    const store = useAuthStore()
    await store.login({ email: '[email protected]', password: '123456' })
    expect(store.isAuthenticated).toBe(true)
  })
})

Configuration

Nuxt Configuration

ts
// nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2025-11-30',
  devtools: { enabled: false },

  modules: [
    '@pinia/nuxt',
    '@vueuse/nuxt',
  ],

  css: ['~/assets/css/main.css'],

  runtimeConfig: {
    public: {
      apiBase: '/api',
      mock: true,
      demoEmail: '[email protected]',
      demoPassword: '123456',
      showDemoHint: true,
      appTitle: 'Admin Pro',
      brandName: 'Halolight',
    },
  },

  app: {
    head: {
      title: 'Admin Pro',
      script: [
        { src: 'https://cdn.tailwindcss.com' },
      ],
    },
  },
})

Deployment

bash
npx vercel

Or use the Vercel button for one-click deployment:

Deploy with Vercel

Docker

dockerfile
FROM node:20-alpine AS builder

RUN npm install -g pnpm

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/.output ./.output

ENV HOST=0.0.0.0
ENV PORT=3000

EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]

Other Platforms

  • Cloudflare Pages: Configure nitro.preset: 'cloudflare-pages'
  • Netlify: Configure nitro.preset: 'netlify'
  • Node.js Server: pnpm build && node .output/server/index.mjs

CI/CD

Complete GitHub Actions CI workflow configured:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

Server Routes (API Endpoints)

Nuxt 3 has a built-in Nitro server for creating server-side APIs.

typescript
// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const { page = 1, limit = 10 } = query;

  // Mock database query
  const users = await getUsersFromDB({ page: Number(page), limit: Number(limit) });

  return {
    success: true,
    data: users,
    pagination: {
      page: Number(page),
      limit: Number(limit),
      total: 100,
    },
  };
});

Server Middleware

typescript
// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const url = getRequestURL(event);

  // Protect API routes
  if (url.pathname.startsWith('/api/admin')) {
    const token = getHeader(event, 'authorization')?.replace('Bearer ', '');

    if (!token) {
      throw createError({
        statusCode: 401,
        message: 'Unauthorized',
      });
    }

    try {
      const user = verifyToken(token);
      event.context.user = user;
    } catch {
      throw createError({
        statusCode: 401,
        message: 'Invalid or expired token',
      });
    }
  }
});

Plugins

typescript
// plugins/api.ts
export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig();

  const api = $fetch.create({
    baseURL: config.public.apiBase,
    onRequest({ options }) {
      const authStore = useAuthStore();
      if (authStore.token) {
        options.headers = {
          ...options.headers,
          Authorization: `Bearer ${authStore.token}`,
        };
      }
    },
    onResponseError({ response }) {
      if (response.status === 401) {
        const authStore = useAuthStore();
        authStore.logout();
        navigateTo('/login');
      }
    },
  });

  return {
    provide: {
      api,
    },
  };
});

Composables

typescript
// composables/useUsers.ts
export function useUsers() {
  const { $api } = useNuxtApp();

  const users = ref<User[]>([]);
  const loading = ref(false);
  const error = ref<string | null>(null);

  async function fetchUsers(params?: { page?: number; limit?: number }) {
    loading.value = true;
    error.value = null;

    try {
      const response = await $api<ApiResponse<User[]>>('/api/users', {
        params,
      });
      users.value = response.data;
      return response;
    } catch (e: any) {
      error.value = e.message;
      throw e;
    } finally {
      loading.value = false;
    }
  }

  return {
    users,
    loading,
    error,
    fetchUsers,
  };
}

Performance Optimization

Image Optimization

vue
<script setup lang="ts">
// Using @nuxt/image
</script>

<template>
  <NuxtImg
    src="/hero.jpg"
    alt="Hero"
    width="800"
    height="600"
    loading="lazy"
    format="webp"
  />

  <NuxtPicture
    src="/hero.jpg"
    alt="Hero"
    width="800"
    height="600"
    sizes="sm:100vw md:50vw lg:33vw"
  />
</template>

Lazy-Load Components

vue
<script setup lang="ts">
// Lazy-load heavy components
const Chart = defineAsyncComponent(() => import('~/components/Chart.vue'));
</script>

<template>
  <ClientOnly>
    <Chart :data="chartData" />
    <template #fallback>
      <div class="h-80 animate-pulse bg-gray-200" />
    </template>
  </ClientOnly>
</template>

Prefetch

vue
<script setup lang="ts">
// Prefetch critical data
const { data } = await useFetch('/api/critical-data', {
  key: 'critical',
  lazy: false,
});
</script>

Frequently Asked Questions

Q: How do I configure SSG (static generation)?

A: Update nuxt.config.ts:

typescript
export default defineNuxtConfig({
  ssr: true,
  nitro: {
    prerender: {
      routes: ['/', '/about', '/contact'],
      crawlLinks: true,
    },
  },
});

Run pnpm generate to create the static site.

Q: How do I configure SPA mode?

A: Disable SSR:

typescript
export default defineNuxtConfig({
  ssr: false,
});

Q: What is the difference between useFetch and $fetch?

A:

  • useFetch is a composable that automatically handles SSR data syncing
  • $fetch is the low-level method and does not handle SSR
vue
<script setup lang="ts">
// Recommended: handles SSR automatically
const { data } = await useFetch('/api/users');

// Manual call
const fetchData = async () => {
  const data = await $fetch('/api/users');
};
</script>

Q: How do I add global CSS?

A: Configure it in nuxt.config.ts:

typescript
export default defineNuxtConfig({
  css: ['~/assets/css/main.css'],
});

Q: How do I configure a proxy?

A: Use nitro.routeRules:

typescript
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/api/external/**': {
        proxy: 'https://api.example.com/**',
      },
    },
  },
});

Q: How do I implement authentication?

A: Use middleware + Pinia:

typescript
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
  const authStore = useAuthStore();
  const publicPages = ['/login', '/register', '/forgot-password'];

  if (!publicPages.includes(to.path) && !authStore.isAuthenticated) {
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath },
    });
  }
});

Q: How do I optimize bundle size?

A: Optimization suggestions:

typescript
export default defineNuxtConfig({
  // On-demand component import
  components: {
    dirs: ['~/components'],
    global: false,
  },

  // Experimental features
  experimental: {
    treeshakeClientOnly: true,
  },

  // Vite optimization
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            'vue-vendor': ['vue', 'vue-router', 'pinia'],
          },
        },
      },
    },
  },
});

Comparison with Other Versions

Feature Nuxt Version Vue Version Next.js Version
State Management Pinia Pinia Zustand
Data Fetching useFetch Axios TanStack Query
Form Validation Native VeeValidate + Zod React Hook Form + Zod
Server Built-in Nitro Separate backend API Routes
Styling Tailwind CDN Tailwind Tailwind
Routing File-based routing Vue Router App Router
SSR Built-in support Requires configuration Built-in support
]]>
<![CDATA[Preact Version]]> https://halolight.docs.h7ml.cn/en/guide/preact https://halolight.docs.h7ml.cn/en/guide/preact Fri, 19 Dec 2025 04:56:41 GMT Preact Version

HaloLight Preact version is built on Preact + Vite, using Signals + TypeScript to deliver a lightweight, high-performance admin dashboard.

Live Preview: https://halolight-preact.h7ml.cn

GitHub: https://github.com/halolight/halolight-preact

Features

  • 🪶 Ultra Lightweight - Core library only 3KB gzip
  • High-Performance Signals - Reactive state management with automatic dependency tracking
  • 🎨 Theme System - 11 skins, dark/light mode, View Transitions
  • 🔐 Authentication System - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization and business management
  • 🛡️ Permission Control - RBAC fine-grained permission management
  • ⚛️ React Compatible - Can directly use most React ecosystem libraries
  • 🚀 Fast Startup - Vite provides ultra-fast dev experience

Tech Stack

Technology Version Description
Preact 10.x Lightweight React alternative
@preact/signals 2.x Reactive state management
TypeScript 5.9 Type safety
Tailwind CSS 4.x Atomic CSS
shadcn/ui latest UI component library (compat layer)
Vite 7.2 Build tool
preact-router 4.x Client-side routing
TanStack Query 5.x Server state
Mock.js 1.x Data mocking

Core Features

  • Signals State Management - High-performance reactive, automatic dependency tracking, fine-grained updates
  • Permission System - RBAC permission control, route guards, permission components
  • Theme System - 11 skins, dark/light mode, View Transitions
  • Data Mocking - Mock.js + Fetch interception, complete backend simulation
  • React Compatible - Use React ecosystem libraries through preact/compat

Directory Structure

halolight-preact/
├── src/
│   ├── pages/                     # Page components
│   │   ├── Home.tsx              # Homepage
│   │   ├── auth/                 # Auth pages
│   │   │   ├── Login.tsx
│   │   │   ├── Register.tsx
│   │   │   ├── ForgotPassword.tsx
│   │   │   └── ResetPassword.tsx
│   │   └── dashboard/            # Dashboard pages
│   │       ├── Dashboard.tsx
│   │       ├── Users.tsx
│   │       ├── UserDetail.tsx
│   │       ├── UserCreate.tsx
│   │       ├── Roles.tsx
│   │       ├── Permissions.tsx
│   │       ├── Settings.tsx
│   │       └── Profile.tsx
│   ├── components/               # Component library
│   │   ├── ui/                   # UI components
│   │   │   ├── Button.tsx
│   │   │   ├── Input.tsx
│   │   │   ├── Card.tsx
│   │   │   └── Dialog.tsx
│   │   ├── layout/               # Layout components
│   │   │   ├── AdminLayout.tsx
│   │   │   ├── AuthLayout.tsx
│   │   │   ├── Sidebar.tsx
│   │   │   └── Header.tsx
│   │   ├── dashboard/            # Dashboard components
│   │   │   ├── DashboardGrid.tsx
│   │   │   ├── WidgetWrapper.tsx
│   │   │   └── StatsWidget.tsx
│   │   └── shared/               # Shared components
│   │       └── PermissionGuard.tsx
│   ├── stores/                   # State management
│   │   ├── auth.ts
│   │   ├── ui-settings.ts
│   │   └── dashboard.ts
│   ├── hooks/                    # Custom Hooks
│   │   ├── useAuth.ts
│   │   └── usePermission.ts
│   ├── lib/                      # Utilities
│   │   ├── api.ts
│   │   ├── permission.ts
│   │   └── cn.ts
│   ├── mock/                     # Mock data
│   │   ├── index.ts
│   │   └── handlers/
│   ├── types/                    # Type definitions
│   ├── App.tsx                   # Root component
│   ├── routes.tsx                # Route config
│   └── main.tsx                  # Entry file
├── public/                       # Static assets
├── vite.config.ts               # Vite config
├── tailwind.config.ts           # Tailwind config
└── package.json

Quick Start

Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-preact.git
cd halolight-preact
pnpm install

Environment Variables

bash
cp .env.example .env
env
# .env
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:5173

Production Build

bash
pnpm build
pnpm preview

Core Features

State Management (@preact/signals)

tsx
// stores/auth.ts
import { signal, computed, effect } from '@preact/signals'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

// Reactive state
export const user = signal<User | null>(null)
export const token = signal<string | null>(null)
export const loading = signal(false)

// Computed properties
export const isAuthenticated = computed(() => !!token.value && !!user.value)
export const permissions = computed(() => user.value?.permissions ?? [])

// Persistence
effect(() => {
  if (user.value && token.value) {
    localStorage.setItem('auth', JSON.stringify({
      user: user.value,
      token: token.value,
    }))
  }
})

// Initialization
const saved = localStorage.getItem('auth')
if (saved) {
  const { user: savedUser, token: savedToken } = JSON.parse(saved)
  user.value = savedUser
  token.value = savedToken
}

// Methods
export async function login(credentials: { email: string; password: string }) {
  loading.value = true
  try {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
      headers: { 'Content-Type': 'application/json' },
    })
    const data = await response.json()

    user.value = data.user
    token.value = data.token
  } finally {
    loading.value = false
  }
}

export function logout() {
  user.value = null
  token.value = null
  localStorage.removeItem('auth')
}

export function hasPermission(permission: string): boolean {
  const perms = permissions.value
  return perms.some(p =>
    p === '*' || p === permission ||
    (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
  )
}

Signals Features:

  • Fine-grained Updates - Only updates components that depend on the Signal
  • Automatic Dependency Tracking - No need to manually declare dependencies
  • No Memoization Needed - Computed properties are automatically cached
  • Cross-component Communication - Global state automatically syncs

Data Fetching (TanStack Query)

tsx
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { token } from '../stores/auth'

export function useUsers(page = 1) {
  return useQuery({
    queryKey: ['users', page],
    queryFn: async () => {
      const response = await fetch(`/api/users?page=${page}`, {
        headers: { Authorization: `Bearer ${token.value}` },
      })
      return response.json()
    },
    enabled: !!token.value,
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async (data: CreateUserDto) => {
      const response = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(data),
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${token.value}`,
        },
      })
      return response.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}
tsx
// Usage
import { useUsers } from '../hooks/useUsers'

export function UsersPage() {
  const { data, isLoading, error } = useUsers(1)

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Failed to load</div>

  return (
    <ul>
      {data.data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Permission Control

tsx
// hooks/usePermission.ts
import { hasPermission } from '../stores/auth'

export function usePermission() {
  return {
    hasPermission,
    can: (permission: string) => hasPermission(permission),
  }
}
tsx
// components/shared/PermissionGuard.tsx
import { ComponentChildren } from 'preact'
import { hasPermission } from '../../stores/auth'

interface Props {
  permission: string
  children: ComponentChildren
  fallback?: ComponentChildren
}

export function PermissionGuard({ permission, children, fallback }: Props) {
  if (!hasPermission(permission)) {
    return fallback ?? null
  }

  return children
}
tsx
// Usage
<PermissionGuard
  permission="users:delete"
  fallback={<span class="text-muted-foreground">No permission</span>}
>
  <Button variant="destructive">Delete</Button>
</PermissionGuard>

Route Configuration

tsx
// routes.tsx
import Router, { Route } from 'preact-router'
import { isAuthenticated, hasPermission } from './stores/auth'

// Page components
import Home from './pages/Home'
import Login from './pages/auth/Login'
import Register from './pages/auth/Register'
import Dashboard from './pages/dashboard/Dashboard'
import Users from './pages/dashboard/Users'

// Route guard HOC
function ProtectedRoute({ component: Component, permission, ...rest }) {
  if (!isAuthenticated.value) {
    route('/login?redirect=' + rest.path)
    return null
  }

  if (permission && !hasPermission(permission)) {
    return <div>No permission</div>
  }

  return <Component {...rest} />
}

export function AppRouter() {
  return (
    <Router>
      <Route path="/" component={Home} />
      <Route path="/login" component={Login} />
      <Route path="/register" component={Register} />
      <ProtectedRoute
        path="/dashboard"
        component={Dashboard}
        permission="dashboard:view"
      />
      <ProtectedRoute
        path="/users"
        component={Users}
        permission="users:list"
      />
    </Router>
  )
}

Theme System

Skin Presets

Support 11 preset skins, switch via quick settings panel:

Skin Color CSS Variable
Default Purple --primary: 51.1% 0.262 276.97
Blue Blue --primary: 54.8% 0.243 264.05
Emerald Emerald --primary: 64.6% 0.178 142.49
Amber Amber --primary: 78.3% 0.177 74.21
Rose Rose --primary: 62.8% 0.243 12.48
Slate Slate --primary: 51.4% 0.032 257.42
Zinc Zinc --primary: 50.7% 0.017 285.96
Stone Stone --primary: 53.4% 0.015 69.82
Neutral Neutral --primary: 50.9% 0.016 286.13
Red Red --primary: 55.5% 0.238 25.33
Orange Orange --primary: 72.3% 0.187 56.24

CSS Variables (OKLch)

css
/* Light mode */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 96.5% 0.006 286.32;
  --secondary-foreground: 21.7% 0.026 285.88;
  --accent: 96.5% 0.006 286.32;
  --accent-foreground: 21.7% 0.026 285.88;
}

/* Dark mode */
.dark {
  --background: 15.5% 0.018 285.88;
  --foreground: 98.3% 0.006 286.32;
  --primary: 74.1% 0.196 275.74;
  --primary-foreground: 21.7% 0.043 286.07;
  --secondary: 20.7% 0.021 286.05;
  --secondary-foreground: 98.3% 0.006 286.32;
}

Theme Switching

tsx
// stores/ui-settings.ts
import { signal, effect } from '@preact/signals'

export const theme = signal<'light' | 'dark'>('light')
export const skin = signal<string>('default')

// Persistence
effect(() => {
  localStorage.setItem('theme', theme.value)
  document.documentElement.classList.toggle('dark', theme.value === 'dark')
})

effect(() => {
  localStorage.setItem('skin', skin.value)
  document.documentElement.dataset.skin = skin.value
})

// Initialization
theme.value = (localStorage.getItem('theme') as any) || 'light'
skin.value = localStorage.getItem('skin') || 'default'

export function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

export function setSkin(newSkin: string) {
  skin.value = newSkin
}

Page Routes

Path Page Permission
/ Homepage Public
/login Login Public
/register Register Public
/forgot-password Forgot Password Public
/reset-password Reset Password Public
/dashboard Dashboard dashboard:view
/users User List users:list
/users/create Create User users:create
/users/:id User Detail users:view
/roles Role Management roles:list
/permissions Permission Management permissions:list
/settings System Settings settings:view
/profile Profile Logged in

Common Commands

bash
pnpm dev            # Start dev server
pnpm build          # Production build
pnpm preview        # Preview production build
pnpm lint           # Code linting
pnpm lint:fix       # Auto fix
pnpm type-check     # Type checking
pnpm test           # Run tests
pnpm test:coverage  # Test coverage

Deployment

Deploy with Vercel

Docker

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
bash
docker build -t halolight-preact .
docker run -p 80:80 halolight-preact

Other Platforms

Demo Accounts

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Testing

Test Commands

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Single run
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI

Test File Organization

Test files are placed together with source files, using .test.ts or .test.tsx suffix:

src/components/ui/
├── Button.tsx
├── Button.test.tsx     # Button component test
├── Input.tsx
└── Input.test.tsx      # Input component test

Test Example

tsx
// src/components/ui/Button.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/preact'
import { Button } from './Button'

describe('Button', () => {
  it('renders default button', () => {
    render(<Button>Click</Button>)
    expect(screen.getByRole('button')).toHaveTextContent('Click')
  })

  it('renders different variants', () => {
    render(<Button variant="destructive">Delete</Button>)
    expect(screen.getByRole('button')).toHaveClass('bg-destructive')
  })

  it('handles disabled state', () => {
    render(<Button disabled>Disabled</Button>)
    expect(screen.getByRole('button')).toBeDisabled()
  })
})

Configuration

Vite Configuration

ts
// vite.config.ts
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
import path from 'path'

export default defineConfig({
  plugins: [preact()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      // React compatibility
      react: 'preact/compat',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
  build: {
    target: 'esnext',
    minify: 'terser',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['preact', 'preact/hooks'],
          router: ['preact-router'],
          query: ['@tanstack/react-query'],
        },
      },
    },
  },
})

Tailwind Configuration

ts
// tailwind.config.ts
import type { Config } from 'tailwindcss'

export default {
  darkMode: ['class'],
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        border: 'oklch(var(--border) / <alpha-value>)',
        input: 'oklch(var(--input) / <alpha-value>)',
        ring: 'oklch(var(--ring) / <alpha-value>)',
        background: 'oklch(var(--background) / <alpha-value>)',
        foreground: 'oklch(var(--foreground) / <alpha-value>)',
        primary: {
          DEFAULT: 'oklch(var(--primary) / <alpha-value>)',
          foreground: 'oklch(var(--primary-foreground) / <alpha-value>)',
        },
      },
    },
  },
  plugins: [require('tailwindcss-animate')],
} satisfies Config

CI/CD

The project is configured with complete GitHub Actions CI workflow:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

Component Example

tsx
// components/ui/Button.tsx
import { ComponentChildren } from 'preact'
import { cn } from '../../lib/cn'

interface Props {
  variant?: 'default' | 'destructive' | 'outline' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  class?: string
  children: ComponentChildren
  onClick?: () => void
}

export function Button({
  variant = 'default',
  size = 'md',
  disabled,
  class: className,
  children,
  onClick,
}: Props) {
  return (
    <button
      class={cn(
        'inline-flex items-center justify-center rounded-md font-medium transition-colors',
        {
          'bg-primary text-primary-foreground hover:bg-primary/90':
            variant === 'default',
          'bg-destructive text-destructive-foreground hover:bg-destructive/90':
            variant === 'destructive',
          'border border-input bg-background hover:bg-accent':
            variant === 'outline',
          'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
          'h-8 px-3 text-sm': size === 'sm',
          'h-10 px-4': size === 'md',
          'h-12 px-6 text-lg': size === 'lg',
          'opacity-50 cursor-not-allowed': disabled,
        },
        className
      )}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

Form Handling

tsx
// pages/auth/Login.tsx
import { useState } from 'preact/hooks'
import { route } from 'preact-router'
import { login, loading } from '../../stores/auth'

export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState('')

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    setError('')

    try {
      await login({ email, password })
      const params = new URLSearchParams(location.search)
      route(params.get('redirect') || '/dashboard')
    } catch (e) {
      setError('Invalid email or password')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <div class="text-destructive">{error}</div>}

      <input
        type="email"
        value={email}
        onInput={(e) => setEmail(e.currentTarget.value)}
        placeholder="Email"
        required
      />

      <input
        type="password"
        value={password}
        onInput={(e) => setPassword(e.currentTarget.value)}
        placeholder="Password"
        required
      />

      <button type="submit" disabled={loading.value}>
        {loading.value ? 'Logging in...' : 'Login'}
      </button>
    </form>
  )
}

Performance Optimization

Lazy Loading Components

tsx
// App.tsx
import { lazy, Suspense } from 'preact/compat'

const Dashboard = lazy(() => import('./pages/dashboard/Dashboard'))
const Users = lazy(() => import('./pages/dashboard/Users'))

export function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Router>
        <Route path="/dashboard" component={Dashboard} />
        <Route path="/users" component={Users} />
      </Router>
    </Suspense>
  )
}

Code Splitting

ts
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['preact', 'preact/hooks'],
          router: ['preact-router'],
          query: ['@tanstack/react-query'],
        },
      },
    },
  },
})

Signals Optimization

tsx
// Use computed to avoid redundant calculations
import { signal, computed } from '@preact/signals'

const items = signal([1, 2, 3, 4, 5])
const filter = signal('all')

// Computed properties are automatically cached
const filteredItems = computed(() => {
  if (filter.value === 'all') return items.value
  return items.value.filter(item => item > 2)
})

// Usage in component
function ItemList() {
  return (
    <ul>
      {filteredItems.value.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  )
}

FAQ

Q: How to use React ecosystem libraries?

A: Preact provides React compatibility through preact/compat, most React libraries can be used directly:

ts
// vite.config.ts
export default defineConfig({
  resolve: {
    alias: {
      react: 'preact/compat',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
})

Q: How do Signals work with React Hooks?

A: Signals can be used directly in components without useState:

tsx
import { signal } from '@preact/signals'

const count = signal(0)

function Counter() {
  // Use signal.value directly
  return (
    <button onClick={() => count.value++}>
      Count: {count.value}
    </button>
  )
}

Q: How to optimize first screen loading?

A: Use code splitting and lazy loading:

tsx
import { lazy, Suspense } from 'preact/compat'

const HeavyComponent = lazy(() => import('./HeavyComponent'))

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  )
}

Comparison with Other Versions

Feature Preact Next.js Vue
SSR/SSG ❌ (SPA) ✅ (Nuxt)
State Management Signals Zustand Pinia
Routing preact-router App Router Vue Router
Build Tool Vite Next.js Vite
Bundle Size ~3KB ~85KB ~33KB
React Compatibility -
Learning Curve Low Medium Medium
]]>
<![CDATA[Qwik Version]]> https://halolight.docs.h7ml.cn/en/guide/qwik https://halolight.docs.h7ml.cn/en/guide/qwik Fri, 19 Dec 2025 04:56:41 GMT Qwik Version

HaloLight Qwik version is built on Qwik City, featuring Qwik resumability architecture + TypeScript for zero hydration and ultimate performance.

Live Preview: https://halolight-qwik.h7ml.cn

GitHub: https://github.com/halolight/halolight-qwik

Features

  • 🔄 Resumability - No hydration needed, server state directly resumed
  • Lazy Load Everything - Code loaded on demand, minimal initial JS (~1KB)
  • 🎨 Theme System - 11 skins, dark mode, View Transitions
  • 🔐 Authentication - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization and business management
  • 🛡️ Permission Control - RBAC fine-grained permission management
  • 📡 Signals - Fine-grained reactive system
  • 🌐 Edge Deployment - Native support for Cloudflare Workers and other edge platforms

Tech Stack

Technology Version Description
Qwik 2.x Resumable framework
Qwik City 2.x Full-stack framework
TypeScript 5.x Type safety
Tailwind CSS 4.x Atomic CSS
Qwik UI latest UI component library
Modular Forms latest Form handling
Zod 3.x Data validation
ECharts 5.x Chart visualization
Mock.js 1.x Data mocking

Core Features

  • Configurable Dashboard - 9 widgets, drag & drop layout, responsive adaptation
  • Permission System - RBAC permission control, route guards, permission components
  • Theme System - 11 skins, dark mode, View Transitions
  • Server-side Rendering - Built-in SSR support, SEO optimization
  • File-based Routing - Directory-based routing system
  • Real-time Notifications - WebSocket push, notification center

Directory Structure

halolight-qwik/
├── src/
│   ├── routes/                    # File-based routing
│   │   ├── index.tsx            # Home
│   │   ├── layout.tsx           # Root layout
│   │   ├── (auth)/              # Auth route group
│   │   │   ├── layout.tsx
│   │   │   ├── login/
│   │   │   │   └── index.tsx
│   │   │   ├── register/
│   │   │   ├── forgot-password/
│   │   │   └── reset-password/
│   │   ├── (dashboard)/         # Dashboard route group
│   │   │   ├── layout.tsx
│   │   │   ├── dashboard/
│   │   │   │   └── index.tsx
│   │   │   ├── users/
│   │   │   │   ├── index.tsx
│   │   │   │   ├── create/
│   │   │   │   └── [id]/
│   │   │   ├── roles/
│   │   │   ├── permissions/
│   │   │   ├── settings/
│   │   │   └── profile/
│   │   └── api/                 # API endpoints
│   │       └── auth/
│   │           └── login/
│   │               └── index.ts
│   ├── components/              # Component library
│   │   ├── ui/                  # Qwik UI components
│   │   ├── layout/              # Layout components
│   │   │   ├── admin-layout/
│   │   │   ├── auth-layout/
│   │   │   ├── sidebar/
│   │   │   └── header/
│   │   ├── dashboard/           # Dashboard components
│   │   │   ├── dashboard-grid/
│   │   │   ├── widget-wrapper/
│   │   │   └── stats-widget/
│   │   └── shared/              # Shared components
│   │       └── permission-guard/
│   ├── stores/                  # State management
│   │   ├── auth.ts
│   │   ├── ui-settings.ts
│   │   └── dashboard.ts
│   ├── lib/                     # Utilities
│   │   ├── api.ts
│   │   ├── permission.ts
│   │   └── cn.ts
│   ├── mock/                    # Mock data
│   └── types/                   # Type definitions
├── public/                      # Static assets
├── vite.config.ts              # Vite config
├── tailwind.config.ts          # Tailwind config
└── package.json

Quick Start

Environment Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-qwik.git
cd halolight-qwik
pnpm install

Environment Variables

bash
cp .env.example .env
env
# .env example
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:5173

Build for Production

bash
pnpm build
pnpm serve

Core Features

State Management (Context + Signals)

tsx
// stores/auth.ts
import {
  createContextId,
  useContext,
  useStore,
  useComputed$,
  $,
  type Signal,
} from '@builder.io/qwik'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

interface AuthState {
  user: User | null
  token: string | null
  loading: boolean
}

export const AuthContext = createContextId<AuthState>('auth')

export function useAuth() {
  const state = useContext(AuthContext)

  const isAuthenticated = useComputed$(() => !!state.token && !!state.user)
  const permissions = useComputed$(() => state.user?.permissions ?? [])

  const login = $(async (credentials: { email: string; password: string }) => {
    state.loading = true
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: { 'Content-Type': 'application/json' },
      })
      const data = await response.json()

      state.user = data.user
      state.token = data.token
    } finally {
      state.loading = false
    }
  })

  const logout = $(() => {
    state.user = null
    state.token = null
  })

  const hasPermission = $((permission: string) => {
    const perms = state.user?.permissions ?? []
    return perms.some(p =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  })

  return {
    state,
    isAuthenticated,
    permissions,
    login,
    logout,
    hasPermission,
  }
}

Data Fetching (routeLoader$)

tsx
// routes/(dashboard)/users/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'

export const useUsers = routeLoader$(async ({ query, cookie, status }) => {
  const token = cookie.get('token')?.value
  const page = Number(query.get('page')) || 1

  // Permission check
  const user = await validateToken(token)
  if (!hasPermission(user, 'users:list')) {
    status(403)
    return { error: 'No permission to access' }
  }

  const response = await fetch(`/api/users?page=${page}`, {
    headers: { Authorization: `Bearer ${token}` },
  })

  return response.json()
})

export default component$(() => {
  const users = useUsers()

  return (
    <div>
      <h1>User List</h1>

      {users.value.error ? (
        <div class="text-destructive">{users.value.error}</div>
      ) : (
        <ul>
          {users.value.data.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  )
})

Permission Control

Route Guards

tsx
// routes/(dashboard)/layout.tsx
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
import { useAuth } from '~/stores/auth'
import { AdminLayout } from '~/components/layout/admin-layout'

export const useAuthGuard = routeLoader$(async ({ cookie, redirect, url }) => {
  const token = cookie.get('token')?.value

  if (!token) {
    throw redirect(302, `/login?redirect=${url.pathname}`)
  }

  // Validate token and return user info
  return {
    user: await validateToken(token),
  }
})

export default component$(() => {
  const data = useAuthGuard()

  return (
    <AdminLayout user={data.value.user}>
      <Slot />
    </AdminLayout>
  )
})

Permission Component

tsx
// components/shared/permission-guard/index.tsx
import { component$, Slot, useComputed$ } from '@builder.io/qwik'
import { useAuth } from '~/stores/auth'

interface Props {
  permission: string
}

export const PermissionGuard = component$<Props>(({ permission }) => {
  const { hasPermission } = useAuth()

  const allowed = useComputed$(async () => {
    return await hasPermission(permission)
  })

  return (
    <>
      {allowed.value ? (
        <Slot />
      ) : (
        <Slot name="fallback" />
      )}
    </>
  )
})

// Usage
<PermissionGuard permission="users:delete">
  <Button variant="destructive" q:slot="">Delete</Button>
  <span q:slot="fallback" class="text-muted-foreground">No Permission</span>
</PermissionGuard>

Form Submission (routeAction$)

tsx
// routes/(auth)/login/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, zod$, z, Form } from '@builder.io/qwik-city'

export const useLogin = routeAction$(
  async (data, { cookie, redirect, fail }) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' },
      })

      if (!response.ok) {
        return fail(401, { message: 'Invalid email or password' })
      }

      const result = await response.json()

      cookie.set('token', result.token, {
        path: '/',
        httpOnly: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 24 * 7,
      })

      throw redirect(302, '/dashboard')
    } catch (e) {
      return fail(500, { message: 'Server error' })
    }
  },
  zod$({
    email: z.string().email('Please enter a valid email'),
    password: z.string().min(6, 'Password must be at least 6 characters'),
  })
)

export default component$(() => {
  const action = useLogin()

  return (
    <Form action={action}>
      {action.value?.failed && (
        <div class="text-destructive">{action.value.message}</div>
      )}

      <input type="email" name="email" placeholder="Email" />
      {action.value?.fieldErrors?.email && (
        <span class="text-destructive">{action.value.fieldErrors.email}</span>
      )}

      <input type="password" name="password" placeholder="Password" />
      {action.value?.fieldErrors?.password && (
        <span class="text-destructive">{action.value.fieldErrors.password}</span>
      )}

      <button type="submit" disabled={action.isRunning}>
        {action.isRunning ? 'Logging in...' : 'Login'}
      </button>
    </Form>
  )
})

API Routes

ts
// routes/api/auth/login/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'

export const onPost: RequestHandler = async ({ json, parseBody }) => {
  const body = await parseBody()
  const { email, password } = body as { email: string; password: string }

  // Validation logic
  if (!email || !password) {
    json(400, { success: false, message: 'Email and password are required' })
    return
  }

  // Authentication logic...

  json(200, {
    success: true,
    user: { id: 1, name: 'Admin', email },
    token: 'mock_token',
  })
}

Draggable Dashboard

tsx
// components/dashboard/dashboard-grid/index.tsx
import { component$, useSignal, useStore, $ } from '@builder.io/qwik'

interface Widget {
  id: string
  type: string
  x: number
  y: number
  w: number
  h: number
}

export const DashboardGrid = component$(() => {
  const widgets = useStore<Widget[]>([
    { id: '1', type: 'stats', x: 0, y: 0, w: 3, h: 2 },
    { id: '2', type: 'chart', x: 3, y: 0, w: 6, h: 4 },
  ])

  const handleLayoutChange = $((newLayout: Widget[]) => {
    widgets.splice(0, widgets.length, ...newLayout)
  })

  return (
    <div class="dashboard-grid">
      {widgets.map((widget) => (
        <div
          key={widget.id}
          class="widget"
          style={{
            gridColumn: `${widget.x + 1} / span ${widget.w}`,
            gridRow: `${widget.y + 1} / span ${widget.h}`,
          }}
        >
          {/* Widget content */}
        </div>
      ))}
    </div>
  )
})

Theme System

Skin Presets

Supports 11 preset skins, switchable through quick settings panel:

Skin Primary Color CSS Variable
Default Purple --primary: 51.1% 0.262 276.97
Blue Blue --primary: 54.8% 0.243 264.05
Emerald Emerald --primary: 64.6% 0.178 142.49
Rose Rose --primary: 61.8% 0.238 12.57
Orange Orange --primary: 68.3% 0.199 36.35
Yellow Yellow --primary: 88.1% 0.197 95.45
Violet Violet --primary: 57.8% 0.24 305.4
Cyan Cyan --primary: 73.8% 0.139 196.85
Pink Pink --primary: 72.2% 0.218 345.82
Lime Lime --primary: 79.2% 0.183 123.7
Amber Amber --primary: 82.5% 0.157 62.24

CSS Variables (OKLch)

css
/* Global variable definitions */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 98% 0 0;
  --secondary: 96.1% 0 0;
  --secondary-foreground: 9.8% 0 0;
  --muted: 95.1% 0.01 286.38;
  --muted-foreground: 45.1% 0.009 285.88;
  --accent: 95.1% 0.01 286.38;
  --accent-foreground: 9.8% 0 0;
  --destructive: 54.3% 0.227 25.78;
  --destructive-foreground: 98% 0 0;
  --border: 89.8% 0.006 286.32;
  --input: 89.8% 0.006 286.32;
  --ring: 51.1% 0.262 276.97;
  --radius: 0.5rem;
}

.dark {
  --background: 0% 0 0;
  --foreground: 98% 0 0;
  --primary: 59.6% 0.262 276.97;
  --primary-foreground: 14.9% 0.017 285.75;
  /* ... */
}

Page Routes

Path Page Permission
/ Home Public
/login Login Public
/register Register Public
/forgot-password Forgot Password Public
/reset-password Reset Password Public
/dashboard Dashboard dashboard:view
/users User List users:list
/users/create Create User users:create
/users/[id] User Details users:view
/roles Role Management roles:list
/permissions Permission Management permissions:list
/settings System Settings settings:view
/profile Profile Authenticated

Common Commands

bash
pnpm dev            # Start development server
pnpm build          # Production build
pnpm serve          # Preview production build
pnpm lint           # Code linting
pnpm lint:fix       # Auto-fix linting issues
pnpm type-check     # Type checking
pnpm test           # Run tests
pnpm test:e2e       # E2E tests

Deployment

Deploy with Vercel

bash
# Use Vercel Edge adapter
pnpm add -D @builder.io/qwik-city/adapters/vercel-edge

Docker

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server ./server
COPY --from=builder /app/package.json .
RUN npm install --production
EXPOSE 3000
CMD ["node", "server/entry.express.js"]

Other Platforms

  • Node.js Server

    bash
    pnpm build
    node server/entry.express.js
  • Cloudflare Pages

    bash
    # Use Cloudflare Pages adapter
    pnpm add -D @builder.io/qwik-city/adapters/cloudflare-pages
  • Netlify

  • AWS Amplify

  • Azure Static Web Apps

Demo Accounts

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Testing

bash
pnpm test           # Run tests
pnpm test:e2e       # E2E tests
pnpm test:coverage  # Coverage report

Test Examples

tsx
// src/components/permission-guard/permission-guard.spec.tsx
import { describe, it, expect } from 'vitest'
import { createDOM } from '@builder.io/qwik/testing'
import { PermissionGuard } from './permission-guard'

describe('PermissionGuard', () => {
  it('should render content when has permission', async () => {
    const { screen, render } = await createDOM()

    await render(
      <PermissionGuard permission="users:view">
        <div>Protected Content</div>
      </PermissionGuard>
    )

    expect(screen.innerHTML).toContain('Protected Content')
  })

  it('should render fallback when no permission', async () => {
    const { screen, render } = await createDOM()

    await render(
      <PermissionGuard permission="admin:*">
        <div>Protected Content</div>
        <div q:slot="fallback">No Permission</div>
      </PermissionGuard>
    )

    expect(screen.innerHTML).toContain('No Permission')
  })
})

Configuration

Vite Configuration

ts
// vite.config.ts
import { defineConfig } from 'vite'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { qwikCity } from '@builder.io/qwik-city/vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig(() => {
  return {
    plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
    preview: {
      headers: {
        'Cache-Control': 'public, max-age=600',
      },
    },
  }
})

CI/CD

Project is configured with complete GitHub Actions CI workflow:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

Qwik Resumability Principle

Qwik's core innovation is "resumability" rather than "hydration":

tsx
// Traditional frameworks (React/Vue): Need to re-execute all code to rebuild state
// Qwik: Directly resume state from HTML without re-execution

// Server serializes state
export default component$(() => {
  const count = useSignal(0)

  // Qwik serializes state into HTML
  return <div>Count: {count.value}</div>
})

// Client directly resumes state from HTML without executing component code
// Code is loaded and executed only on interaction

Lazy Loading Strategy

Qwik implements the most aggressive code splitting:

tsx
// Each event handler is an independent lazy loading unit
export default component$(() => {
  const count = useSignal(0)

  // This function won't be downloaded until clicked
  const handleClick = $(() => {
    count.value++
  })

  return (
    <button onClick$={handleClick}>
      Count: {count.value}
    </button>
  )
})

Preload Optimization

tsx
// routes/(dashboard)/layout.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$, Link } from '@builder.io/qwik-city'

// Preload data
export const usePreloadData = routeLoader$(async () => {
  return {
    navigation: await fetchNavigation(),
  }
})

export default component$(() => {
  const data = usePreloadData()

  return (
    <nav>
      {data.value.navigation.map((item) => (
        // Link automatically preloads target page
        <Link href={item.path} prefetch>
          {item.title}
        </Link>
      ))}
    </nav>
  )
})

Performance Optimization

Image Optimization

tsx
import { component$ } from '@builder.io/qwik'
import { Image } from '@unpic/qwik'

export default component$(() => {
  return (
    <Image
      src="https://example.com/image.jpg"
      layout="constrained"
      width={800}
      height={600}
      alt="Optimized image"
    />
  )
})

Lazy Loading Components

tsx
// Component-level lazy loading
import { component$ } from '@builder.io/qwik'

export default component$(() => {
  return (
    <div>
      {/* Use resource$ for component lazy loading */}
      <Resource
        value={heavyComponentResource}
        onPending={() => <div>Loading...</div>}
        onResolved={(HeavyComponent) => <HeavyComponent />}
      />
    </div>
  )
})

Preload Critical Resources

tsx
// routes/layout.tsx
import { component$, useVisibleTask$ } from '@builder.io/qwik'

export default component$(() => {
  useVisibleTask$(() => {
    // Preload critical fonts
    const link = document.createElement('link')
    link.rel = 'preload'
    link.as = 'font'
    link.href = '/fonts/main.woff2'
    link.type = 'font/woff2'
    link.crossOrigin = 'anonymous'
    document.head.appendChild(link)
  })

  return <Slot />
})

FAQ

Q: How to handle client-side state?

A: Use useSignal and useStore to create reactive state:

tsx
import { component$, useSignal, useStore } from '@builder.io/qwik'

export default component$(() => {
  // Use useSignal for simple values
  const count = useSignal(0)

  // Use useStore for complex objects
  const state = useStore({
    user: null,
    loading: false,
  })

  return (
    <div>
      <p>Count: {count.value}</p>
      <button onClick$={() => count.value++}>Increment</button>
    </div>
  )
})

Q: How to integrate third-party libraries?

A: Use useVisibleTask$ to execute code on the client:

tsx
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'

export default component$(() => {
  const chartRef = useSignal<Element>()

  useVisibleTask$(({ cleanup }) => {
    // Initialize third-party library on the client
    import('chart.js').then(({ Chart }) => {
      const chart = new Chart(chartRef.value, {
        // Configuration...
      })

      cleanup(() => chart.destroy())
    })
  })

  return <canvas ref={chartRef} />
})

Q: How to optimize initial load time?

A: Qwik optimizes automatically, but you can further:

  1. Use SSR: Enabled by default
  2. Preload critical routes:
    tsx
    <Link href="/dashboard" prefetch>Dashboard</Link>
  3. Defer non-critical resources:
    tsx
    useVisibleTask$(({ track }) => {
      // Load only when component is visible
      track(() => isVisible.value)
      if (isVisible.value) {
        loadAnalytics()
      }
    })

Q: How to handle form submission?

A: Use routeAction$ for server-side processing:

tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'

export const useAddUser = routeAction$(
  async (data) => {
    // Server-side processing
    const user = await createUser(data)
    return { success: true, user }
  },
  zod$({
    name: z.string().min(2),
    email: z.string().email(),
  })
)

export default component$(() => {
  const action = useAddUser()

  return (
    <Form action={action}>
      <input name="name" />
      <input name="email" type="email" />
      <button type="submit">
        {action.isRunning ? 'Submitting...' : 'Submit'}
      </button>
    </Form>
  )
})

Comparison with Other Versions

Feature Qwik Version Next.js Vue
SSR/SSG ✅ Built-in ✅ (Nuxt)
State Management Context + Signals Zustand Pinia
Data Fetching routeLoader$ TanStack Query TanStack Query
Form Validation Modular Forms + Zod React Hook Form + Zod VeeValidate + Zod
Routing File-based Routing App Router Vue Router
Build Tool Vite Next.js Vite
Hydration Resumable (Zero Hydration) Traditional Hydration Traditional Hydration
Initial JS ~1KB ~85KB ~33KB
Server-side Built-in Full-stack API Routes Separate Backend
Component Library Qwik UI shadcn/ui shadcn-vue
]]>
<![CDATA[Railway Deployment]]> https://halolight.docs.h7ml.cn/en/guide/railway https://halolight.docs.h7ml.cn/en/guide/railway Fri, 19 Dec 2025 04:56:41 GMT Railway Deployment

HaloLight Railway deployment edition, an optimized one-click deployment solution tailored for the Railway platform.

Live Preview: https://halolight-railway.h7ml.cn

GitHub: https://github.com/halolight/halolight-railway

Features

  • 🚂 One-Click Deploy - Templated fast deployment, live in 30 seconds
  • 📈 Auto Scaling - On-demand auto scaling, zero-downtime deploys
  • 🐘 PostgreSQL - One-click managed database
  • 🔴 Redis - Built-in cache service support
  • 🌐 Custom Domains - Free HTTPS with auto renewal
  • ⚙️ Environment Variables - Convenient config management with references
  • 📊 Monitoring Panel - Real-time resource monitoring and log aggregation
  • 🔄 Auto Deployments - Git push triggers automatic deploys

Quick Start

Deploy on Railway

After clicking the button:

  1. Sign in to your Railway account
  2. Choose the GitHub repository
  3. Configure environment variables
  4. Deployment completes automatically

Method 2: CLI Deployment

bash
# Install Railway CLI
npm install -g @railway/cli

# Sign in to Railway
railway login

# Clone the project
git clone https://github.com/halolight/halolight-railway.git
cd halolight-railway

# Initialize Railway project
railway init

# Link to an existing project (optional)
railway link

# Deploy
railway up

Method 3: GitHub Integration

  1. Fork the halolight-railway repository
  2. In the Railway console select "Deploy from GitHub repo"
  3. Choose your forked repo
  4. Configure environment variables and deploy

Configuration Files

railway.json

json
{
  "$schema": "https://railway.app/railway.schema.json",
  "build": {
    "builder": "NIXPACKS",
    "buildCommand": "pnpm install && pnpm build"
  },
  "deploy": {
    "startCommand": "pnpm start",
    "healthcheckPath": "/api/health",
    "healthcheckTimeout": 300,
    "restartPolicyType": "ON_FAILURE",
    "restartPolicyMaxRetries": 10,
    "numReplicas": 1
  }
}

nixpacks.toml (Optional)

toml
[phases.setup]
nixPkgs = ["nodejs_20", "pnpm"]

[phases.install]
cmds = ["pnpm install --frozen-lockfile"]

[phases.build]
cmds = ["pnpm build"]

[start]
cmd = "pnpm start"

Environment Variables

App Configuration

Variable Description Example
NODE_ENV Runtime environment production
PORT Server port 3000 (Railway sets automatically)
NEXT_PUBLIC_API_URL API base URL /api
NEXT_PUBLIC_MOCK Enable mock data false
NEXT_PUBLIC_APP_TITLE App title Admin Pro

Railway Variable References

Railway lets you reference other services in env vars:

bash
# Reference the auto-generated domain
NEXT_PUBLIC_API_URL=https://${{RAILWAY_PUBLIC_DOMAIN}}/api

# Reference PostgreSQL service
DATABASE_URL=${{Postgres.DATABASE_URL}}

# Reference Redis service
REDIS_URL=${{Redis.REDIS_URL}}

# Reference project variables
JWT_SECRET=${{shared.JWT_SECRET}}

Add Services

PostgreSQL Database

bash
# Via CLI
railway add --database postgres

# Or in the console
# 1. Click "New Service"
# 2. Select "Database" → "PostgreSQL"
# 3. DATABASE_URL is generated automatically

Generated environment variables:

  • DATABASE_URL - Full connection string
  • PGHOST - Host
  • PGPORT - Port
  • PGUSER - Username
  • PGPASSWORD - Password
  • PGDATABASE - Database name

Redis Cache

bash
# Via CLI
railway add --database redis

# Or in the console
# 1. Click "New Service"
# 2. Select "Database" → "Redis"
# 3. REDIS_URL is generated automatically

Generated environment variables:

  • REDIS_URL - Full connection string
  • REDISHOST - Host
  • REDISPORT - Port
  • REDISPASSWORD - Password

Custom Domains

Add a Domain

  1. In service settings click "Settings"
  2. Find the "Domains" section
  3. Click "Generate Domain" (free Railway domain)
  4. Or click "Add Custom Domain" (custom domain)

DNS Configuration

Type: CNAME
Name: your-subdomain
Value: <your-app>.up.railway.app

HTTPS

Railway automatically configures HTTPS for all domains:

  • Automatically requests Let's Encrypt certificates
  • Auto renews
  • Forces HTTPS redirects

Common Commands

bash
# Sign in
railway login

# View status
railway status

# Deploy
railway up

# View logs
railway logs

# Open console
railway open

# Run remote command
railway run <command>

# Connect to database
railway connect postgres

# Environment variables
railway variables
railway variables set KEY=value

Monitoring and Logs

Real-Time Logs

bash
# View logs via CLI
railway logs -f

# Or in the console
# Service → Deployments → click a deployment → View Logs

Resource Monitoring

Railway console provides:

  • CPU usage
  • Memory usage
  • Network traffic
  • Request count/response time
  • Error rate

Alerting

  1. Go to project settings
  2. Configure webhook notifications
  3. Supports Slack, Discord, Email

Scaling

Manual Scaling

json
// railway.json
{
  "deploy": {
    "numReplicas": 3
  }
}

Auto Scaling (Pro Plan)

Railway Pro supports metric-based autoscaling:

  • CPU threshold
  • Memory threshold
  • Request queue depth

Pricing

Plan Price Features
Hobby $5/mo 500 execution hours, 1GB RAM
Pro $20/mo+ Unlimited hours, more resources
Enterprise Contact sales Dedicated support, SLA guarantees

FAQ

Q: What if deployment fails?

A: Check these items:

  1. Inspect build logs to confirm dependencies installed correctly
  2. Make sure pnpm-lock.yaml is committed
  3. Verify environment variables are set correctly
  4. Confirm the start command is correct

Q: How to roll back a deployment?

A: In the Deployments page:

  1. Find a previous successful deployment
  2. Click "Redeploy"
  3. Or use CLI: railway rollback

Q: How to configure private networking?

A: Railway services communicate over the internal network:

bash
# Use internal DNS
DATABASE_URL=postgres://user:[email protected]:5432/db

Comparison with Other Platforms

Feature Railway Vercel Fly.io
One-Click Deploy ⚠️ CLI required
Managed Database ✅ Built-in ❌ External needed ✅ Built-in
Free Tier/Credit $5/mo credit 100GB 3 shared VMs
Auto Scaling ✅ Pro
Private Network ⚠️ Limited
]]>
<![CDATA[React Version]]> https://halolight.docs.h7ml.cn/en/guide/react https://halolight.docs.h7ml.cn/en/guide/react Fri, 19 Dec 2025 04:56:41 GMT React Version

HaloLight React version is built on React 19 + Vite 6, a pure Client-Side Rendering (CSR) Single Page Application (SPA).

Live Preview: https://halolight-react.h7ml.cn/

GitHub: https://github.com/halolight/halolight-react

Features

  • 🏗️ React 19 - Latest React features and performance optimizations
  • Vite 6 - Lightning-fast cold start and HMR
  • 🎨 Theme System - 11 skins, dark mode, View Transitions
  • 🔐 Authentication - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization and business management
  • 🛡️ Permission Control - RBAC fine-grained permission management
  • 📑 Multi-tab - Browser-style tab management
  • Command Palette - Keyboard shortcuts navigation (⌘K)

Tech Stack

Technology Version Description
React 19.x UI framework
Vite 6.x Build tool
TypeScript 5.x Type safety
React Router 6.x Client-side routing
Zustand 5.x State management
TanStack Query 5.x Server state
React Hook Form 7.x Form handling
Zod 4.x Data validation
Tailwind CSS 4.x Atomic CSS
shadcn/ui latest UI component library
react-grid-layout 1.5.x Drag-and-drop layout
Recharts 3.x Chart visualization
Framer Motion 12.x Animation effects
Mock.js 1.x Data mocking

Core Features

  • Configurable Dashboard - 9 widgets, drag-and-drop layout, responsive design
  • Multi-tab Navigation - Browser-style tabs, context menu, state caching
  • Permission System - RBAC permission control, route guards, permission components
  • Theme System - 11 skins, dark mode, View Transitions
  • Multi-account Switching - Quick account switching, remember login state
  • Command Palette - Keyboard shortcuts (⌘K), global search
  • Real-time Notifications - WebSocket push, notification center

Directory Structure

halolight-react/
├── src/
│   ├── pages/                     # Page components
│   │   ├── auth/                  # Auth pages
│   │   │   ├── login/
│   │   │   ├── register/
│   │   │   ├── forgot-password/
│   │   │   └── reset-password/
│   │   ├── dashboard/             # Dashboard
│   │   └── legal/                 # Legal pages
│   ├── components/
│   │   ├── ui/                    # shadcn/ui components (20+)
│   │   ├── layout/                # Layout components
│   │   │   ├── admin-layout.tsx
│   │   │   ├── auth-layout.tsx
│   │   │   ├── sidebar.tsx
│   │   │   ├── header.tsx
│   │   │   └── footer.tsx
│   │   ├── dashboard/             # Dashboard components
│   │   │   ├── configurable-dashboard.tsx
│   │   │   ├── widget-wrapper.tsx
│   │   │   ├── stats-widget.tsx
│   │   │   ├── chart-widget.tsx
│   │   │   └── ...
│   │   └── shared/                # Shared components
│   ├── hooks/                     # Custom Hooks
│   │   ├── use-users.ts
│   │   ├── use-auth.ts
│   │   ├── use-theme.ts
│   │   └── ...
│   ├── stores/                    # Zustand Stores
│   │   ├── auth.ts
│   │   ├── ui-settings.ts
│   │   ├── dashboard-layout.ts
│   │   └── tabs.ts
│   ├── lib/
│   │   ├── api/                   # API services
│   │   ├── auth/                  # Auth logic
│   │   ├── validations/           # Zod schemas
│   │   └── utils.ts               # Utility functions
│   ├── routes/                    # Route configuration
│   │   └── index.tsx
│   ├── config/                    # Configuration files
│   │   ├── routes.ts
│   │   └── tdk.ts
│   ├── types/                     # Type definitions
│   ├── mock/                      # Mock data
│   ├── providers/                 # Context Providers
│   ├── App.tsx
│   └── main.tsx
├── public/                        # Static assets
├── vite.config.ts
├── tsconfig.json
└── package.json

Quick Start

Environment Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-react.git
cd halolight-react
pnpm install

Environment Variables

bash
cp .env.example .env.development
env
# .env.development example
VITE_API_URL=/api
VITE_MOCK=true
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=true

Start Development

bash
pnpm dev

Visit http://localhost:5173

Build for Production

bash
pnpm build
pnpm preview

Demo Account

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Core Functionality

State Management (Zustand)

tsx
// stores/auth.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface AuthState {
  user: User | null
  token: string | null
  isAuthenticated: boolean
  login: (credentials: LoginCredentials) => Promise<void>
  logout: () => void
  hasPermission: (permission: string) => boolean
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: async (credentials) => {
        const response = await authApi.login(credentials)
        set({
          user: response.user,
          token: response.token,
          isAuthenticated: true,
        })
      },

      logout: () => {
        set({ user: null, token: null, isAuthenticated: false })
      },

      hasPermission: (permission) => {
        const { user } = get()
        if (!user) return false
        return user.permissions.some(p =>
          p === '*' || p === permission ||
          (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
        )
      },
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({ token: state.token, user: state.user }),
    }
  )
)

Data Fetching (TanStack Query)

tsx
// hooks/use-users.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { usersApi } from '@/lib/api'

export function useUsers(params?: UserQueryParams) {
  return useQuery({
    queryKey: ['users', params],
    queryFn: () => usersApi.getList(params),
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: usersApi.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}

// Usage in component
function UsersPage() {
  const { data: users, isLoading, error } = useUsers()

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return <div>{/* Render user list */}</div>
}

Permission Control

tsx
// hooks/use-permission.ts
import { useAuthStore } from '@/stores/auth'

export function usePermission(permission: string): boolean {
  const hasPermission = useAuthStore((state) => state.hasPermission)
  return hasPermission(permission)
}

export function usePermissions(permissions: string[]): boolean {
  const hasPermission = useAuthStore((state) => state.hasPermission)
  return permissions.every(p => hasPermission(p))
}
tsx
// Usage
function DeleteButton() {
  const canDelete = usePermission('users:delete')

  if (!canDelete) return null

  return <Button variant="destructive">Delete</Button>
}
tsx
// components/permission-guard.tsx
import { usePermission } from '@/hooks/use-permission'

interface PermissionGuardProps {
  permission: string
  children: React.ReactNode
  fallback?: React.ReactNode
}

export function PermissionGuard({
  permission,
  children,
  fallback = null,
}: PermissionGuardProps) {
  const hasPermission = usePermission(permission)

  if (!hasPermission) return fallback

  return <>{children}</>
}
tsx
<!-- Usage -->
<PermissionGuard permission="users:delete" fallback={<span>No permission</span>}>
  <DeleteButton />
</PermissionGuard>

Draggable Dashboard

tsx
// components/dashboard/configurable-dashboard.tsx
import GridLayout from 'react-grid-layout'
import { useDashboardStore } from '@/stores/dashboard-layout'

export function ConfigurableDashboard() {
  const { layout, setLayout, isEditing } = useDashboardStore()

  return (
    <GridLayout
      layout={layout}
      onLayoutChange={setLayout}
      cols={12}
      rowHeight={80}
      isDraggable={isEditing}
      isResizable={isEditing}
      margin={[16, 16]}
    >
      {layout.map((item) => (
        <div key={item.i}>
          <WidgetWrapper widget={getWidget(item.i)} />
        </div>
      ))}
    </GridLayout>
  )
}

Theme System

Skin Presets

Supports 11 preset skins, switchable via quick settings panel:

Skin Main Color CSS Variable
Default Purple --primary: 51.1% 0.262 276.97
Blue Blue --primary: 54.8% 0.243 264.05
Emerald Emerald --primary: 64.6% 0.178 142.49
Orange Orange --primary: 65.7% 0.198 45.13
Rose Rose --primary: 58.9% 0.238 11.26
Cyan Cyan --primary: 75.6% 0.146 191.68
Yellow Yellow --primary: 85.1% 0.184 98.08
Violet Violet --primary: 55.3% 0.264 293.49
Slate Slate --primary: 47.9% 0.017 256.71
Zinc Zinc --primary: 48.3% 0 0
Neutral Neutral --primary: 48.5% 0 0

CSS Variables (OKLch)

css
/* Example variable definitions */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 96.1% 0.004 286.41;
  --secondary-foreground: 14.9% 0.017 285.75;
  --muted: 96.1% 0.004 286.41;
  --muted-foreground: 45.8% 0.009 285.77;
  --accent: 96.1% 0.004 286.41;
  --accent-foreground: 14.9% 0.017 285.75;
  --destructive: 59.3% 0.246 27.33;
  --destructive-foreground: 100% 0 0;
  --border: 89.8% 0.006 286.32;
  --input: 89.8% 0.006 286.32;
  --ring: 51.1% 0.262 276.97;
  --radius: 0.5rem;
}

.dark {
  --background: 0% 0 0;
  --foreground: 98.3% 0 0;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  /* ... */
}

Page Routes

Path Page Permission
/ Redirect to /dashboard -
/login Login Public
/register Register Public
/forgot-password Forgot password Public
/reset-password Reset password Public
/dashboard Dashboard dashboard:view
/users User list users:list
/users/create Create user users:create
/users/:id User details users:view
/users/:id/edit Edit user users:update
/roles Role management roles:list
/permissions Permission management permissions:list
/settings System settings settings:view
/profile User profile Authenticated

Environment Variables

Configuration Example

bash
cp .env.example .env.development
env
# .env.development example
VITE_API_URL=/api
VITE_MOCK=true
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=true

Variable Descriptions

Variable Name Description Default Value
VITE_API_URL API base path /api
VITE_MOCK Enable Mock data true
VITE_APP_TITLE Application title Admin Pro
VITE_BRAND_NAME Brand name Halolight
VITE_DEMO_EMAIL Demo account email [email protected]
VITE_DEMO_PASSWORD Demo account password 123456
VITE_SHOW_DEMO_HINT Show demo hint true

Usage

tsx
// Access environment variables in code
const apiUrl = import.meta.env.VITE_API_URL
const isMock = import.meta.env.VITE_MOCK === 'true'

Common Commands

bash
pnpm dev            # Start development server
pnpm build          # Production build
pnpm preview        # Preview production build
pnpm lint           # Code linting
pnpm lint:fix       # Auto fix
pnpm type-check     # Type checking
pnpm test           # Run tests
pnpm test:coverage  # Test coverage

Testing

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Single run
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI interface

Test Examples

tsx
// __tests__/components/Button.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from '@/components/ui/button'

describe('Button', () => {
  it('renders button with text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button')).toHaveTextContent('Click me')
  })

  it('handles click events', async () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)

    await userEvent.click(screen.getByRole('button'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('disables button when disabled prop is true', () => {
    render(<Button disabled>Click me</Button>)
    expect(screen.getByRole('button')).toBeDisabled()
  })
})

Configuration

Vite Configuration

ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: 'dist',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
          'chart-vendor': ['recharts'],
        },
      },
    },
  },
})

Deployment

Deploy with Vercel

bash
vercel

Docker

dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
bash
docker build -t halolight-react .
docker run -p 3000:80 halolight-react

Other Platforms

CI/CD

Complete GitHub Actions CI workflow configuration:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

PWA Support

Built-in PWA support including:

  • Service Worker registration
  • Offline caching
  • App manifest (manifest.json)
  • Multiple icon sizes
json
// public/manifest.json
{
  "name": "Admin Pro",
  "short_name": "Admin",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

React Router Configuration

tsx
// routes/index.tsx
import { createBrowserRouter, Navigate } from 'react-router-dom'
import { DashboardLayout } from '@/layouts/dashboard-layout'
import { AuthLayout } from '@/layouts/auth-layout'

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Navigate to="/dashboard" replace />,
  },
  {
    path: '/login',
    element: <AuthLayout />,
    children: [
      { index: true, element: <LoginPage /> },
    ],
  },
  {
    path: '/',
    element: <DashboardLayout />,
    children: [
      { path: 'dashboard', element: <HomePage /> },
      { path: 'users', element: <UsersPage /> },
      { path: 'settings', element: <SettingsPage /> },
      // More routes...
    ],
  },
])

Route Guards

tsx
// components/auth-guard.tsx
import { Navigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/stores/auth'

interface AuthGuardProps {
  children: React.ReactNode
  permission?: string
}

export function AuthGuard({ children, permission }: AuthGuardProps) {
  const location = useLocation()
  const { isAuthenticated, hasPermission } = useAuthStore()

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />
  }

  if (permission && !hasPermission(permission)) {
    return <Navigate to="/403" replace />
  }

  return <>{children}</>
}

Performance Optimization

Image Optimization

tsx
// Lazy load images
import { useState } from 'react'

function LazyImage({ src, alt }: { src: string; alt: string }) {
  const [loaded, setLoaded] = useState(false)

  return (
    <div className="relative">
      {!loaded && <div className="skeleton" />}
      <img
        src={src}
        alt={alt}
        loading="lazy"
        onLoad={() => setLoaded(true)}
        className={loaded ? 'opacity-100' : 'opacity-0'}
      />
    </div>
  )
}

Lazy Loading Components

tsx
// Route-level code splitting
import { lazy, Suspense } from 'react'

const Dashboard = lazy(() => import('@/pages/dashboard'))
const Users = lazy(() => import('@/pages/users'))

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/users" element={<Users />} />
      </Routes>
    </Suspense>
  )
}

Preloading

tsx
// Preload component on hover
import { lazy } from 'react'

const UserDetails = lazy(() => import('@/pages/user-details'))

function UserList() {
  const preloadUserDetails = () => {
    // Trigger preload
    import('@/pages/user-details')
  }

  return (
    <Link
      to="/users/1"
      onMouseEnter={preloadUserDetails}
    >
      View Details
    </Link>
  )
}

Memo Optimization

tsx
import { memo } from 'react'

// Prevent unnecessary re-renders
const ExpensiveComponent = memo(({ data }: { data: any }) => {
  return <div>{/* Complex rendering logic */}</div>
})

Frequently Asked Questions

Q: How to add a new route?

A: Add route configuration in src/routes/index.tsx:

tsx
{
  path: '/new-page',
  element: <NewPage />,
}

Q: How to customize theme colors?

A: Modify CSS variables or use theme switching feature:

css
:root {
  --primary: 51.1% 0.262 276.97; /* Modify primary color */
}

Q: How to integrate real API?

A: Set VITE_MOCK to false and configure VITE_API_URL:

env
VITE_MOCK=false
VITE_API_URL=https://api.example.com

Q: How to add new permissions?

A: Add permission string to user's permissions array and use usePermission Hook:

tsx
const canEdit = usePermission('users:edit')

Comparison with Other Versions

Feature React Version Next.js Vue
SSR/SSG ✅ (Nuxt)
State Management Zustand Zustand Pinia
Routing React Router App Router Vue Router
Build Tool Vite Next.js Vite
]]>
<![CDATA[Remix Version]]> https://halolight.docs.h7ml.cn/en/guide/remix https://halolight.docs.h7ml.cn/en/guide/remix Fri, 19 Dec 2025 04:56:41 GMT Remix Version

HaloLight Remix version is built on React Router 7 (the original Remix team has merged into React Router), featuring TypeScript + Web standards-first full-stack development experience.

Live Preview: https://halolight-remix.h7ml.cn/

GitHub: https://github.com/halolight/halolight-remix

Features

  • 🌐 Web Standards First - Based on native APIs like Fetch API, FormData, Response
  • 🔄 Loader/Action - Elegant server-side data patterns with progressive enhancement
  • 📁 File-based Routing - Intuitive nested routes and layout system
  • Progressive Enhancement - Forms work without JavaScript
  • 🎯 Type Safety - Auto-generated route types (+types/)
  • 🎨 Theme System - 11 skin presets + OKLch color space
  • 📑 Multi-tabs - Tab bar + right-click menu management
  • 🚀 Vite Powered - Lightning fast HMR
  • 🌍 Edge Deployment - One-click deploy to Cloudflare Pages
  • 📊 Data Visualization - Recharts integration
  • 🔐 Authentication System - Complete login/register/password reset flow
  • 🛡️ Permission Control - RBAC fine-grained permission management

Tech Stack

Technology Version Description
React Router 7.x Full-stack routing framework (formerly Remix)
React 19.x UI framework
TypeScript 5.9 Type safety
Vite 7.x Build tool
Tailwind CSS 4.x Atomic CSS + OKLch
Radix UI latest Accessible UI primitives
Zustand 5.x Lightweight state management
Recharts 3.x Chart visualization
Vitest 4.x Unit testing
Cloudflare Pages - Edge deployment

Core Features

  • Web Standards First - Based on Fetch API, FormData, Response and other native APIs
  • Loader/Action Pattern - Elegant server-side data loading and form handling
  • File-based Routing - Intuitive nested routes and layout system
  • Progressive Enhancement - Forms work without JavaScript
  • Type Safety - Auto-generated route type definitions (+types/)
  • Theme System - 11 skin presets + OKLch color space + dark mode
  • Multi-tab Management - Tab bar + right-click menu + state persistence

Directory Structure

halolight-remix/
├── app/
│   ├── routes/                    # File-based routing
│   │   ├── _index.tsx            # Homepage (dashboard)
│   │   ├── login.tsx             # Login
│   │   ├── register.tsx          # Register
│   │   ├── forgot-password.tsx   # Forgot password
│   │   ├── reset-password.tsx    # Reset password
│   │   ├── users.tsx             # User management
│   │   ├── users.$id.tsx         # User details (dynamic route)
│   │   ├── settings.tsx          # System settings
│   │   ├── profile.tsx           # Profile
│   │   ├── security.tsx          # Security settings
│   │   ├── analytics.tsx         # Data analytics
│   │   ├── notifications.tsx     # Notification center
│   │   ├── documents.tsx         # Document management
│   │   ├── calendar.tsx          # Calendar
│   │   ├── api.users.ts          # API endpoint
│   │   ├── api.auth.login.ts     # Login API
│   │   ├── api.auth.logout.ts    # Logout API
│   │   └── +types/               # Auto-generated types
│   ├── components/               # Component library
│   │   ├── ui/                   # Base UI components
│   │   │   ├── button.tsx
│   │   │   ├── card.tsx
│   │   │   ├── dialog.tsx
│   │   │   ├── dropdown-menu.tsx
│   │   │   ├── input.tsx
│   │   │   ├── select.tsx
│   │   │   ├── table.tsx
│   │   │   ├── toast.tsx
│   │   │   └── ...
│   │   ├── layout/               # Layout components
│   │   │   ├── header.tsx
│   │   │   ├── sidebar.tsx
│   │   │   ├── footer.tsx
│   │   │   ├── tab-bar.tsx
│   │   │   └── quick-settings.tsx
│   │   ├── auth/                 # Auth components
│   │   │   └── auth-shell.tsx
│   │   ├── dashboard/            # Dashboard components
│   │   │   ├── stats-card.tsx
│   │   │   └── chart-widget.tsx
│   │   ├── admin-layout.tsx      # Admin layout
│   │   └── theme-provider.tsx    # Theme provider
│   ├── hooks/                    # React Hooks
│   │   ├── use-chart-palette.ts
│   │   ├── use-toast.ts
│   │   └── use-media-query.ts
│   ├── lib/                      # Utilities
│   │   ├── utils.ts              # cn() className utility
│   │   ├── meta.ts               # TDK meta info
│   │   ├── session.server.ts     # Session management
│   │   ├── auth.server.ts        # Auth logic
│   │   └── project-info.ts       # Project info
│   ├── stores/                   # Zustand state
│   │   ├── tabs-store.ts         # Tabs state
│   │   └── ui-settings-store.ts  # UI settings state
│   ├── types/                    # TypeScript types
│   │   ├── user.ts
│   │   └── api.ts
│   ├── root.tsx                  # Root component
│   ├── routes.ts                 # Route config
│   └── app.css                   # Global styles
├── tests/                        # Test files
│   ├── setup.ts
│   ├── lib/
│   ├── stores/
│   └── components/
├── public/                       # Static assets
├── .github/workflows/ci.yml      # CI config
├── wrangler.json                 # Cloudflare config
├── vitest.config.ts              # Vitest config
├── eslint.config.js              # ESLint config
├── vite.config.ts                # Vite config
└── package.json

Quick Start

Environment Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-remix.git
cd halolight-remix
pnpm install

Environment Variables

bash
cp .env.example .env
bash
# .env example
SESSION_SECRET=your-super-secret-session-key
API_BASE_URL=https://api.halolight.h7ml.cn
MOCK_ENABLED=true
DEMO_EMAIL=[email protected]
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=true
APP_TITLE=Admin Pro
BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:5173

Production Build

bash
pnpm build
pnpm start

Demo Account

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Core Functionality

Loader/Action Data Pattern

Route File Conventions

React Router 7 uses file-system routing, where filenames determine URL paths:

app/routes/
├── _index.tsx            → /              (index route)
├── about.tsx             → /about         (static route)
├── users.tsx             → /users         (static route)
├── users.$id.tsx         → /users/:id     (dynamic route)
├── users.$id_.edit.tsx   → /users/:id/edit (nested route)
├── _layout.tsx           → layout route (no URL segment)
├── _layout.dashboard.tsx → /dashboard      (with layout)
├── $.tsx                 → /*             (splat route)
├── api.users.ts          → /api/users     (resource route)
└── [...slug].tsx         → /* optional catch-all

Special File Conventions

Filename Description
_index.tsx Index route, matches parent route exact path
_layout.tsx Pathless layout, child routes share layout
$param.tsx Dynamic route parameter
$.tsx Splat route, catches all sub-paths
api.*.ts Resource route (loader/action only, no UI)
+types/ Auto-generated type definitions

Loader (Data Loading)

Loader executes on the server for page data fetching:

tsx
// app/routes/users.tsx
import type { Route } from "./+types/users";

// Server-side data loading
export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const page = Number(url.searchParams.get("page")) || 1;
  const limit = Number(url.searchParams.get("limit")) || 10;
  const search = url.searchParams.get("search") || "";

  // Check authentication
  const session = await getSession(request.headers.get("Cookie"));
  if (!session.has("userId")) {
    throw redirect("/login");
  }

  // Fetch data
  const response = await fetch(
    `${process.env.API_BASE_URL}/users?page=${page}&limit=${limit}&search=${search}`,
    {
      headers: {
        Authorization: `Bearer ${session.get("token")}`,
      },
    }
  );

  if (!response.ok) {
    throw new Response("Failed to fetch user list", { status: response.status });
  }

  const { data, total } = await response.json();

  return {
    users: data,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  };
}

// Page component receives loaderData
export default function UsersPage({ loaderData }: Route.ComponentProps) {
  const { users, pagination } = loaderData;

  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">User Management</h1>

      <div className="rounded-md border">
        <table className="w-full">
          <thead>
            <tr className="border-b bg-muted/50">
              <th className="p-4 text-left">Name</th>
              <th className="p-4 text-left">Email</th>
              <th className="p-4 text-left">Role</th>
              <th className="p-4 text-left">Actions</th>
            </tr>
          </thead>
          <tbody>
            {users.map((user) => (
              <tr key={user.id} className="border-b">
                <td className="p-4">{user.name}</td>
                <td className="p-4">{user.email}</td>
                <td className="p-4">{user.role}</td>
                <td className="p-4">
                  <Link to={`/users/${user.id}`}>View</Link>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <Pagination {...pagination} />
    </div>
  );
}

Action (Form Handling)

Action handles form submissions with progressive enhancement:

tsx
// app/routes/login.tsx
import type { Route } from "./+types/login";
import { Form, useActionData, useNavigation, redirect } from "react-router";
import { commitSession, getSession } from "~/lib/session.server";

// Server-side form handling
export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;
  const redirectTo = formData.get("redirectTo") as string || "/";

  // Validation
  const errors: Record<string, string> = {};

  if (!email) {
    errors.email = "Please enter email";
  } else if (!email.includes("@")) {
    errors.email = "Please enter a valid email address";
  }

  if (!password) {
    errors.password = "Please enter password";
  } else if (password.length < 6) {
    errors.password = "Password must be at least 6 characters";
  }

  if (Object.keys(errors).length > 0) {
    return { errors, values: { email } };
  }

  // Call login API
  const response = await fetch(`${process.env.API_BASE_URL}/auth/login`, {
    method: "POST",
    body: JSON.stringify({ email, password }),
    headers: { "Content-Type": "application/json" },
  });

  if (!response.ok) {
    const data = await response.json();
    return { errors: { form: data.message || "Invalid email or password" } };
  }

  const { user, token } = await response.json();

  // Create session
  const session = await getSession(request.headers.get("Cookie"));
  session.set("userId", user.id);
  session.set("token", token);
  session.set("user", user);

  // Redirect and set Cookie
  return redirect(redirectTo, {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

// Meta info
export function meta(): Route.MetaDescriptors {
  return [
    { title: "Login - Admin Pro" },
    { name: "description", content: "Login to Admin Pro management system" },
  ];
}

// Page component
export default function LoginPage() {
  const actionData = useActionData<typeof action>();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="w-full max-w-md space-y-8 p-8">
        <div className="text-center">
          <h1 className="text-2xl font-bold">Welcome Back</h1>
          <p className="text-muted-foreground">Login to your account</p>
        </div>

        {/* Form error message */}
        {actionData?.errors?.form && (
          <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
            {actionData.errors.form}
          </div>
        )}

        {/* Progressive enhancement form - works without JS */}
        <Form method="post" className="space-y-4">
          <div className="space-y-2">
            <label htmlFor="email" className="text-sm font-medium">
              Email
            </label>
            <input
              id="email"
              name="email"
              type="email"
              autoComplete="email"
              required
              defaultValue={actionData?.values?.email}
              className="w-full rounded-md border px-3 py-2"
              placeholder="[email protected]"
            />
            {actionData?.errors?.email && (
              <p className="text-sm text-destructive">{actionData.errors.email}</p>
            )}
          </div>

          <div className="space-y-2">
            <label htmlFor="password" className="text-sm font-medium">
              Password
            </label>
            <input
              id="password"
              name="password"
              type="password"
              autoComplete="current-password"
              required
              className="w-full rounded-md border px-3 py-2"
              placeholder="••••••••"
            />
            {actionData?.errors?.password && (
              <p className="text-sm text-destructive">{actionData.errors.password}</p>
            )}
          </div>

          <button
            type="submit"
            disabled={isSubmitting}
            className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
          >
            {isSubmitting ? "Logging in..." : "Login"}
          </button>
        </Form>

        <p className="text-center text-sm text-muted-foreground">
          Don't have an account?{" "}
          <Link to="/register" className="text-primary hover:underline">
            Register now
          </Link>
        </p>
      </div>
    </div>
  );
}

Meta (TDK Meta Info)

tsx
// app/routes/users.tsx
import type { Route } from "./+types/users";
import { generateMeta } from "~/lib/meta";

export function meta(): Route.MetaDescriptors {
  return generateMeta("/users");
}
ts
// app/lib/meta.ts
export const pageMetas: Record<string, PageMeta> = {
  "/users": {
    title: "User Management",
    description: "Manage system user accounts, including creation, editing, and permission configuration",
    keywords: ["user management", "account management", "permission configuration"],
  },
  // ...
};

export function generateMeta(path: string, overrides?: Partial<PageMeta>) {
  const meta = pageMetas[path] || { title: "Page", description: "" };
  // Return complete meta tag array
}

State Management (Zustand)

Tabs Store (Tab Management)

tsx
// app/stores/tabs-store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

interface TabsState {
  tabs: Tab[];
  activeTabId: string | null;
  addTab: (tab: Omit<Tab, "id">) => string;
  removeTab: (id: string) => void;
  setActiveTab: (id: string) => void;
  clearTabs: () => void;
}

export const useTabsStore = create<TabsState>()(
  persist(
    (set, get) => ({
      tabs: [homeTab],
      activeTabId: "home",
      addTab: (tab) => { /* ... */ },
      removeTab: (id) => { /* ... */ },
      // ...
    }),
    { name: "tabs-storage" }
  )
);

UI Settings Store (Skin/Layout)

tsx
// app/stores/ui-settings-store.ts
export type SkinPreset =
  | "default" | "blue" | "emerald" | "amber" | "violet"
  | "rose" | "teal" | "slate" | "ocean" | "sunset" | "aurora";

export const useUiSettingsStore = create<UiSettingsState>()(
  persist(
    (set) => ({
      skin: "default",
      showFooter: true,
      showTabBar: true,
      setSkin: (skin) => set({ skin }),
      setShowFooter: (visible) => set({ showFooter: visible }),
      // ...
    }),
    { name: "ui-settings-storage" }
  )
);

Theme System

Skin Presets

Supports 11 preset skins, switch via Quick Settings panel:

Skin Primary Color
Default Purple
Blue Blue
Emerald Emerald
Amber Amber
Violet Violet
Rose Rose
Teal Teal
Slate Slate
Ocean Ocean Blue
Sunset Sunset Orange
Aurora Aurora

CSS Variables (OKLch)

css
/* app/app.css */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  /* ... */
}

[data-skin="ocean"] {
  --primary: 54.3% 0.195 240.03;
}

.dark {
  --background: 14.9% 0.017 285.75;
  --foreground: 98.5% 0 0;
  /* ... */
}

Page Routes

Path Page Permission
/ Dashboard dashboard:view
/login Login Public
/register Register Public
/forgot-password Forgot Password Public
/reset-password Reset Password Public
/users User Management users:view
/settings System Settings settings:view
/profile Profile settings:view
/security Security Settings settings:view
/analytics Data Analytics analytics:view
/notifications Notification Center notifications:view
/documents Document Management documents:view
/calendar Calendar calendar:view

Environment Variables

Configuration Example

bash
# .env
SESSION_SECRET=your-super-secret-session-key
API_BASE_URL=https://api.halolight.h7ml.cn
MOCK_ENABLED=true
DEMO_EMAIL=[email protected]
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=true
APP_TITLE=Admin Pro
BRAND_NAME=Halolight

Variable Description

Variable Name Description Default Value
SESSION_SECRET Session secret key (required) (required)
API_BASE_URL API base URL /api
MOCK_ENABLED Enable Mock data false
DEMO_EMAIL Demo account email -
DEMO_PASSWORD Demo account password -
SHOW_DEMO_HINT Show demo hint false
APP_TITLE Application title Admin Pro
BRAND_NAME Brand name Halolight

Usage

ts
// app/routes/users.tsx
export async function loader({ request }: Route.LoaderArgs) {
  const apiUrl = process.env.API_BASE_URL;
  const response = await fetch(`${apiUrl}/users`);
  return response.json();
}

Common Commands

bash
# Development
pnpm dev              # Start dev server
pnpm dev --host       # Allow LAN access

# Build
pnpm build            # Production build
pnpm start            # Start production server

# Code Quality
pnpm typecheck        # TypeScript type check
pnpm lint             # ESLint check
pnpm lint:fix         # ESLint auto-fix
pnpm format           # Prettier format

# Testing
pnpm test             # Watch mode
pnpm test:run         # Single run
pnpm test:coverage    # Coverage report
pnpm test:ui          # Vitest UI interface

# Deployment
pnpm preview          # Cloudflare local preview
pnpm deploy           # Deploy to Cloudflare Pages

Testing

Run Tests

pnpm test:coverage # Coverage report


### Test Example

```tsx
// tests/stores/tabs-store.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { useTabsStore } from "~/stores/tabs-store";

describe("useTabsStore", () => {
  beforeEach(() => {
    useTabsStore.getState().clearTabs();
  });

  it("should add new tab", () => {
    const { addTab } = useTabsStore.getState();
    addTab({ title: "User Management", path: "/users" });

    const { tabs } = useTabsStore.getState();
    expect(tabs).toHaveLength(2);
  });
});

Configuration

React Router Configuration

ts
// vite.config.ts
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";

export default defineConfig({
  plugins: [reactRouter()],
});

Wrangler Configuration

json
// wrangler.json
{
  "name": "halolight-remix",
  "compatibility_date": "2024-12-01",
  "compatibility_flags": ["nodejs_compat"],
  "pages_build_output_dir": "./build/client"
}

ESLint Configuration

js
// eslint.config.js
import js from "@eslint/js";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import tseslint from "typescript-eslint";

export default tseslint.config(
  { ignores: ["build", ".react-router"] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      "react-hooks": reactHooks,
      "react-refresh": reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      "react-refresh/only-export-components": [
        "warn",
        { allowConstantExport: true },
      ],
    },
  }
);

Deployment

bash
# Install Wrangler CLI
npm install -g wrangler

# Login
wrangler login

# Deploy
pnpm deploy

Cloudflare Configuration

json
// wrangler.json
{
  "name": "halolight-remix",
  "compatibility_date": "2024-12-01",
  "compatibility_flags": ["nodejs_compat"],
  "pages_build_output_dir": "./build/client"
}

GitHub Actions Deployment

yaml
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install --frozen-lockfile
      - run: pnpm build

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: halolight-remix
          directory: build/client

Node.js Server

bash
pnpm build
pnpm start

Docker

dockerfile
# Dockerfile
FROM node:20-alpine AS builder

RUN corepack enable && corepack prepare pnpm@latest --activate

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

FROM node:20-alpine AS runner

WORKDIR /app

RUN corepack enable && corepack prepare pnpm@latest --activate

COPY --from=builder /app/build ./build
COPY --from=builder /app/package.json .
COPY --from=builder /app/pnpm-lock.yaml .

RUN pnpm install --prod --frozen-lockfile

ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

CMD ["pnpm", "start"]
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - SESSION_SECRET=${SESSION_SECRET}
      - API_BASE_URL=${API_BASE_URL}
    restart: unless-stopped

Vercel

bash
# Install Vercel CLI
npm install -g vercel

# Deploy
vercel

Other Platforms

CI/CD

The project is configured with complete GitHub Actions CI workflow:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Using Cookie for session management:

ts
// app/lib/session.server.ts
import { createCookieSessionStorage, redirect } from "react-router";

// Create session storage
export const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "__session",
    httpOnly: true,
    maxAge: 60 * 60 * 24 * 7, // 7 days
    path: "/",
    sameSite: "lax",
    secrets: [process.env.SESSION_SECRET!],
    secure: process.env.NODE_ENV === "production",
  },
});

export const { getSession, commitSession, destroySession } = sessionStorage;

// Get current user
export async function getUser(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  const user = session.get("user");
  return user || null;
}

// Require login
export async function requireUser(request: Request) {
  const user = await getUser(request);
  if (!user) {
    const url = new URL(request.url);
    throw redirect(`/login?redirectTo=${encodeURIComponent(url.pathname)}`);
  }
  return user;
}

// Logout
export async function logout(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  return redirect("/login", {
    headers: {
      "Set-Cookie": await destroySession(session),
    },
  });
}

Error Handling (ErrorBoundary)

Global and route-level error handling:

tsx
// app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse, useRouteError } from "react-router";

export function ErrorBoundary() {
  const error = useRouteError();

  // Route errors (like 404, 401)
  if (isRouteErrorResponse(error)) {
    return (
      <html lang="en">
        <head>
          <meta charSet="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          <title>Error {error.status}</title>
          <Meta />
          <Links />
        </head>
        <body>
          <div className="flex min-h-screen items-center justify-center">
            <div className="text-center">
              <h1 className="text-9xl font-bold text-muted-foreground">
                {error.status}
              </h1>
              <p className="mt-4 text-xl">{error.statusText}</p>
              <p className="mt-2 text-muted-foreground">{error.data}</p>
              <a href="/" className="mt-8 inline-block text-primary hover:underline">
                Back to Home
              </a>
            </div>
          </div>
          <Scripts />
        </body>
      </html>
    );
  }

  // Unknown errors
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Error Occurred</title>
        <Meta />
        <Links />
      </head>
      <body>
        <div className="flex min-h-screen items-center justify-center">
          <div className="text-center">
            <h1 className="text-2xl font-bold text-destructive">Error Occurred</h1>
            <p className="mt-2 text-muted-foreground">
              {error instanceof Error ? error.message : "Unknown error"}
            </p>
            <a href="/" className="mt-8 inline-block text-primary hover:underline">
              Back to Home
            </a>
          </div>
        </div>
        <Scripts />
      </body>
    </html>
  );
}
tsx
// app/routes/users.$id.tsx - Route-level error boundary
export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error) && error.status === 404) {
    return (
      <div className="flex h-96 items-center justify-center">
        <div className="text-center">
          <h2 className="text-xl font-semibold">User Not Found</h2>
          <p className="text-muted-foreground">Please check if the user ID is correct</p>
          <Link to="/users" className="mt-4 inline-block text-primary">
            Back to User List
          </Link>
        </div>
      </div>
    );
  }

  throw error; // Throw other errors upward
}

Resource Routes (API Endpoints)

Resource routes have no UI components, only export loader/action:

ts
// app/routes/api.users.ts
import type { Route } from "./+types/api.users";
import { getSession } from "~/lib/session.server";

// GET /api/users
export async function loader({ request }: Route.LoaderArgs) {
  const session = await getSession(request.headers.get("Cookie"));

  if (!session.has("userId")) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const url = new URL(request.url);
  const page = Number(url.searchParams.get("page")) || 1;
  const limit = Number(url.searchParams.get("limit")) || 10;

  const response = await fetch(
    `${process.env.API_BASE_URL}/users?page=${page}&limit=${limit}`,
    {
      headers: {
        Authorization: `Bearer ${session.get("token")}`,
      },
    }
  );

  const data = await response.json();
  return Response.json(data);
}

// POST /api/users
export async function action({ request }: Route.ActionArgs) {
  const session = await getSession(request.headers.get("Cookie"));

  if (!session.has("userId")) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const body = await request.json();

  const response = await fetch(`${process.env.API_BASE_URL}/users`, {
    method: "POST",
    body: JSON.stringify(body),
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${session.get("token")}`,
    },
  });

  const data = await response.json();
  return Response.json(data, { status: response.status });
}
ts
// app/routes/api.users.$id.ts
import type { Route } from "./+types/api.users.$id";

// GET /api/users/:id
export async function loader({ params, request }: Route.LoaderArgs) {
  const { id } = params;
  const session = await getSession(request.headers.get("Cookie"));

  const response = await fetch(`${process.env.API_BASE_URL}/users/${id}`, {
    headers: {
      Authorization: `Bearer ${session.get("token")}`,
    },
  });

  if (!response.ok) {
    throw new Response("User not found", { status: 404 });
  }

  return Response.json(await response.json());
}

// PUT /api/users/:id
export async function action({ params, request }: Route.ActionArgs) {
  const { id } = params;
  const session = await getSession(request.headers.get("Cookie"));
  const body = await request.json();

  const response = await fetch(`${process.env.API_BASE_URL}/users/${id}`, {
    method: request.method,
    body: JSON.stringify(body),
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${session.get("token")}`,
    },
  });

  return Response.json(await response.json(), { status: response.status });
}

Advanced Features

useFetcher (No-Navigation Data Fetching)

tsx
// app/routes/users.tsx
import { useFetcher } from "react-router";

export default function UsersPage() {
  const fetcher = useFetcher();

  const handleDelete = (userId: string) => {
    if (confirm("Are you sure you want to delete this user?")) {
      fetcher.submit(
        { userId },
        { method: "delete", action: "/api/users" }
      );
    }
  };

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>
          <span>{user.name}</span>
          <button
            onClick={() => handleDelete(user.id)}
            disabled={fetcher.state === "submitting"}
          >
            {fetcher.state === "submitting" ? "Deleting..." : "Delete"}
          </button>
        </div>
      ))}
    </div>
  );
}

Optimistic UI Updates

tsx
// app/routes/notifications.tsx
import { useFetcher } from "react-router";

function NotificationItem({ notification }) {
  const fetcher = useFetcher();

  // Optimistic UI: immediately show read status
  const isRead = fetcher.formData
    ? fetcher.formData.get("read") === "true"
    : notification.read;

  return (
    <div className={isRead ? "opacity-50" : ""}>
      <p>{notification.message}</p>
      {!isRead && (
        <fetcher.Form method="post" action="/api/notifications/mark-read">
          <input type="hidden" name="id" value={notification.id} />
          <input type="hidden" name="read" value="true" />
          <button type="submit">Mark as Read</button>
        </fetcher.Form>
      )}
    </div>
  );
}

defer and Suspense

tsx
// app/routes/analytics.tsx
import type { Route } from "./+types/analytics";
import { Await, defer } from "react-router";
import { Suspense } from "react";

export async function loader({ request }: Route.LoaderArgs) {
  // Fast data returns immediately
  const summary = await getSummary();

  // Slow data loads deferred
  const chartDataPromise = getChartData();
  const reportPromise = generateReport();

  return defer({
    summary,
    chartData: chartDataPromise,
    report: reportPromise,
  });
}

export default function AnalyticsPage({ loaderData }: Route.ComponentProps) {
  const { summary, chartData, report } = loaderData;

  return (
    <div className="space-y-6">
      {/* Show immediately */}
      <SummaryCard data={summary} />

      {/* Deferred chart loading */}
      <Suspense fallback={<ChartSkeleton />}>
        <Await resolve={chartData}>
          {(data) => <Chart data={data} />}
        </Await>
      </Suspense>

      {/* Deferred report loading */}
      <Suspense fallback={<ReportSkeleton />}>
        <Await resolve={report}>
          {(data) => <Report data={data} />}
        </Await>
      </Suspense>
    </div>
  );
}

Parallel Data Loading

tsx
// app/routes/dashboard.tsx
export async function loader({ request }: Route.LoaderArgs) {
  // Parallel requests to multiple data sources
  const [stats, recentUsers, notifications, activities] = await Promise.all([
    getStats(),
    getRecentUsers(),
    getNotifications(),
    getActivities(),
  ]);

  return { stats, recentUsers, notifications, activities };
}

Middleware Pattern

ts
// app/lib/middleware.ts
import { redirect } from "react-router";
import { getSession } from "./session.server";

type LoaderFunction = (args: LoaderArgs) => Promise<any>;

// Authentication middleware
export function withAuth(loader: LoaderFunction): LoaderFunction {
  return async (args) => {
    const session = await getSession(args.request.headers.get("Cookie"));

    if (!session.has("userId")) {
      const url = new URL(args.request.url);
      throw redirect(`/login?redirectTo=${encodeURIComponent(url.pathname)}`);
    }

    // Inject user info
    const user = session.get("user");
    return loader({ ...args, user });
  };
}

// Role check middleware
export function withRole(role: string, loader: LoaderFunction): LoaderFunction {
  return withAuth(async (args) => {
    const { user } = args as any;

    if (user.role !== role) {
      throw new Response("Insufficient permissions", { status: 403 });
    }

    return loader(args);
  });
}

// Usage example
// app/routes/admin.tsx
export const loader = withRole("admin", async ({ request }) => {
  // Only admin role can access
  return getAdminData();
});

Performance Optimization

Code Splitting

tsx
// Use React.lazy for dynamic imports
import { lazy, Suspense } from "react";

const Chart = lazy(() => import("~/components/dashboard/chart"));

export default function Dashboard() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <Chart data={data} />
    </Suspense>
  );
}

Prefetching

tsx
// Link prefetching
import { Link, prefetchRouteModule } from "react-router";

function NavLink({ to, children }) {
  return (
    <Link
      to={to}
      onMouseEnter={() => prefetchRouteModule(to)}
      onFocus={() => prefetchRouteModule(to)}
    >
      {children}
    </Link>
  );
}

Caching Strategy

tsx
// app/routes/api.static-data.ts
export async function loader() {
  const data = await getStaticData();

  return Response.json(data, {
    headers: {
      "Cache-Control": "public, max-age=3600, s-maxage=86400",
    },
  });
}

Frequently Asked Questions

Q: How to handle form validation?

A: Combine server-side and client-side validation:

tsx
// app/routes/register.tsx
import { z } from "zod";

const registerSchema = z.object({
  email: z.string().email("Please enter a valid email"),
  password: z.string().min(6, "Password must be at least 6 characters"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords do not match",
  path: ["confirmPassword"],
});

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const data = Object.fromEntries(formData);

  // Server-side validation
  const result = registerSchema.safeParse(data);
  if (!result.success) {
    return { errors: result.error.flatten().fieldErrors };
  }

  // Create user...
}

Q: How to implement file uploads?

A: Use FormData to handle files:

tsx
// app/routes/upload.tsx
export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const file = formData.get("file") as File;

  if (!file || file.size === 0) {
    return { error: "Please select a file" };
  }

  // Upload to storage service
  const buffer = await file.arrayBuffer();
  const url = await uploadToStorage(buffer, file.name, file.type);

  return { url };
}

export default function UploadPage() {
  const actionData = useActionData<typeof action>();

  return (
    <Form method="post" encType="multipart/form-data">
      <input type="file" name="file" required />
      <button type="submit">Upload</button>
      {actionData?.url && <p>Upload successful: {actionData.url}</p>}
      {actionData?.error && <p className="text-destructive">{actionData.error}</p>}
    </Form>
  );
}

Q: How to handle internationalization?

A: Use Cookie or URL prefix:

tsx
// app/lib/i18n.ts
export const locales = ["zh-CN", "en-US"] as const;
export type Locale = typeof locales[number];

export function getLocale(request: Request): Locale {
  const url = new URL(request.url);
  const cookie = request.headers.get("Cookie");

  // 1. Check URL parameter
  const urlLocale = url.searchParams.get("locale");
  if (urlLocale && locales.includes(urlLocale as Locale)) {
    return urlLocale as Locale;
  }

  // 2. Check Cookie
  const cookieLocale = getCookie(cookie, "locale");
  if (cookieLocale && locales.includes(cookieLocale as Locale)) {
    return cookieLocale as Locale;
  }

  // 3. Check Accept-Language
  const acceptLanguage = request.headers.get("Accept-Language");
  if (acceptLanguage?.includes("zh")) {
    return "zh-CN";
  }

  return "en-US";
}

Q: How to implement real-time updates?

A: Use SSE (Server-Sent Events):

ts
// app/routes/api.events.ts
export async function loader({ request }: Route.LoaderArgs) {
  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder();

      const sendEvent = (data: any) => {
        controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
      };

      // Send events periodically
      const interval = setInterval(() => {
        sendEvent({ type: "ping", timestamp: Date.now() });
      }, 5000);

      // Cleanup
      request.signal.addEventListener("abort", () => {
        clearInterval(interval);
        controller.close();
      });
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}
tsx
// Client-side usage
useEffect(() => {
  const eventSource = new EventSource("/api/events");

  eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    // Handle event
  };

  return () => eventSource.close();
}, []);

Performance Optimization

Code Splitting

tsx
// Use React.lazy for dynamic imports
import { lazy, Suspense } from "react";

const Chart = lazy(() => import("~/components/dashboard/chart"));

export default function Dashboard() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <Chart data={data} />
    </Suspense>
  );
}

Prefetching

tsx
// Link prefetching
import { Link, prefetchRouteModule } from "react-router";

function NavLink({ to, children }) {
  return (
    <Link
      to={to}
      onMouseEnter={() => prefetchRouteModule(to)}
      onFocus={() => prefetchRouteModule(to)}
    >
      {children}
    </Link>
  );
}

Caching Strategy

tsx
// app/routes/api.static-data.ts
export async function loader() {
  const data = await getStaticData();

  return Response.json(data, {
    headers: {
      "Cache-Control": "public, max-age=3600, s-maxage=86400",
    },
  });
}

Environment Variables

Variable Name Description Default Value
SESSION_SECRET Session secret key (required)
API_BASE_URL API base URL /api
MOCK_ENABLED Enable Mock data false
DEMO_EMAIL Demo account email -
DEMO_PASSWORD Demo account password -
SHOW_DEMO_HINT Show demo hint false
APP_TITLE Application title Admin Pro
BRAND_NAME Brand name Halolight

Common Commands

bash
# Development
pnpm dev              # Start dev server
pnpm dev --host       # Allow LAN access

# Build
pnpm build            # Production build
pnpm start            # Start production server

# Code Quality
pnpm typecheck        # TypeScript type check
pnpm lint             # ESLint check
pnpm lint:fix         # ESLint auto-fix
pnpm format           # Prettier format

# Testing
pnpm test             # Watch mode
pnpm test:run         # Single run
pnpm test:coverage    # Coverage report
pnpm test:ui          # Vitest UI interface

# Deployment
pnpm preview          # Cloudflare local preview
pnpm deploy           # Deploy to Cloudflare Pages

Comparison with Other Versions

Feature Remix Version Vue Version Next.js Version
State Management Zustand Pinia Zustand
Data Fetching Loader/Action TanStack Query TanStack Query
Form Handling Progressive Enhancement Form VeeValidate React Hook Form
Server-side Built-in SSR Nuxt App Router
Component Library Radix UI shadcn-vue shadcn/ui
Routing File-based routing Vue Router App Router
Theme OKLch CSS Variables OKLch CSS Variables OKLch CSS Variables
Testing Vitest Vitest Vitest
Build Tool Vite Vite Turbopack
]]>
<![CDATA[Solid.js Version]]> https://halolight.docs.h7ml.cn/en/guide/solidjs https://halolight.docs.h7ml.cn/en/guide/solidjs Fri, 19 Dec 2025 04:56:41 GMT Solid.js Version

HaloLight Solid.js version is built on SolidStart 1.0, featuring Solid.js fine-grained reactivity + TypeScript for high-performance admin dashboard. No virtual DOM, compile-time optimization, minimal bundle size.

Live Preview: https://halolight-solidjs.h7ml.cn/

GitHub: https://github.com/halolight/halolight-solidjs

Features

  • Fine-grained Reactivity - No virtual DOM, precise dependency tracking, millisecond-level response
  • 🔧 Compile-time Optimization - JSX compiled to efficient DOM operations, zero runtime overhead
  • 📦 Minimal Bundle - Core ~7KB gzip, 10x+ smaller than React
  • 🎯 Signals Primitives - Simple and elegant reactive state management
  • 🌐 SolidStart Full-stack - Built-in SSR/SSG, file routing, RPC
  • 🔄 Server Functions - "use server" seamless server-side logic calls
  • 🎨 Theme System - 11 skin presets + OKLch color space
  • 📑 Multi-tab Navigation - Tab bar + context menu management
  • 🛡️ Permission Control - Fine-grained permission validation and component guards
  • 📊 Data Visualization - solid-charts integration

Tech Stack

Technology Version Description
SolidStart 1.x Solid full-stack framework
Solid.js 1.9+ Fine-grained reactive framework
TypeScript 5.x Type safety
Tailwind CSS 4.x Atomic CSS + OKLch
Kobalte 0.13+ Accessible UI primitives
solid-primitives latest Reactive utilities library
Zod 3.x Data validation
@solid-primitives/storage latest Persistent storage
solid-charts latest Chart visualization
Vitest 4.x Unit testing
Mock.js 1.x Data mocking

Core Features

  • Fine-grained Reactivity: No virtual DOM, precise dependency tracking and updates
  • Compile-time Optimization: JSX compiled to efficient DOM operations
  • Signals: Simple reactive primitives
  • Server-side Rendering: SolidStart built-in SSR support
  • File-based Routing: File system-based routing
  • RPC: Seamless server function calls

Directory Structure

halolight-solidjs/
├── src/
│   ├── routes/                    # File-based routing
│   │   ├── index.tsx             # Home (Dashboard)
│   │   ├── (auth)/               # Auth route group (no layout path)
│   │   │   ├── login.tsx         # Login
│   │   │   ├── register.tsx      # Register
│   │   │   ├── forgot-password.tsx # Forgot password
│   │   │   └── reset-password.tsx  # Reset password
│   │   ├── (dashboard)/          # Dashboard route group (with AdminLayout)
│   │   │   ├── dashboard.tsx     # Dashboard home
│   │   │   ├── analytics.tsx     # Data analytics
│   │   │   ├── users/            # User management
│   │   │   │   ├── index.tsx     # User list
│   │   │   │   ├── create.tsx    # Create user
│   │   │   │   └── [id].tsx      # User details (dynamic route)
│   │   │   ├── roles.tsx         # Role management
│   │   │   ├── permissions.tsx   # Permission management
│   │   │   ├── messages.tsx      # Message center
│   │   │   ├── notifications.tsx # Notifications
│   │   │   ├── documents.tsx     # Document management
│   │   │   ├── calendar.tsx      # Calendar
│   │   │   ├── settings.tsx      # System settings
│   │   │   └── profile.tsx       # User profile
│   │   ├── privacy.tsx           # Privacy policy
│   │   ├── terms.tsx             # Terms of service
│   │   └── api/                  # API routes
│   │       ├── auth/
│   │       │   ├── login.ts      # POST /api/auth/login
│   │       │   ├── register.ts   # POST /api/auth/register
│   │       │   └── logout.ts     # POST /api/auth/logout
│   │       └── users/
│   │           ├── index.ts      # GET/POST /api/users
│   │           └── [id].ts       # GET/PUT/DELETE /api/users/:id
│   ├── components/               # Component library
│   │   ├── ui/                   # Kobalte wrapped components
│   │   │   ├── Button.tsx
│   │   │   ├── Card.tsx
│   │   │   ├── Dialog.tsx
│   │   │   ├── DropdownMenu.tsx
│   │   │   ├── Input.tsx
│   │   │   ├── Select.tsx
│   │   │   ├── Table.tsx
│   │   │   ├── Toast.tsx
│   │   │   └── ...
│   │   ├── layout/               # Layout components
│   │   │   ├── AdminLayout.tsx   # Admin main layout
│   │   │   ├── AuthLayout.tsx    # Auth page layout
│   │   │   ├── Sidebar.tsx       # Sidebar
│   │   │   ├── Header.tsx        # Top navigation
│   │   │   ├── Footer.tsx        # Footer
│   │   │   ├── TabBar.tsx        # Tab bar
│   │   │   └── QuickSettings.tsx # Quick settings panel
│   │   ├── dashboard/            # Dashboard components
│   │   │   ├── DashboardGrid.tsx # Draggable grid
│   │   │   ├── WidgetWrapper.tsx # Widget wrapper
│   │   │   ├── StatsWidget.tsx   # Stats card
│   │   │   └── ChartWidget.tsx   # Chart widget
│   │   ├── auth/                 # Auth components
│   │   │   └── AuthShell.tsx     # Auth shell
│   │   └── shared/               # Shared components
│   │       ├── PermissionGuard.tsx # Permission guard
│   │       └── ErrorBoundary.tsx   # Error boundary
│   ├── stores/                   # State management (Signals + Store)
│   │   ├── auth.ts               # Auth state
│   │   ├── ui-settings.ts        # UI settings state
│   │   ├── tabs.ts               # Tab state
│   │   └── dashboard.ts          # Dashboard layout state
│   ├── lib/                      # Utilities
│   │   ├── api.ts                # API client
│   │   ├── permission.ts         # Permission utils
│   │   ├── meta.ts               # TDK meta info
│   │   └── cn.ts                 # Class name utils
│   ├── server/                   # Server code
│   │   ├── auth.ts               # Auth logic
│   │   ├── session.ts            # Session management
│   │   └── middleware.ts         # Middleware
│   ├── hooks/                    # Custom hooks
│   │   ├── createUsers.ts        # User data
│   │   └── createToast.ts        # Toast notifications
│   └── types/                    # TypeScript types
│       ├── user.ts
│       └── api.ts
├── tests/                        # Test files
│   ├── setup.ts
│   ├── stores/
│   └── components/
├── public/                       # Static assets
├── .github/workflows/ci.yml      # CI configuration
├── app.config.ts                 # SolidStart configuration
├── tailwind.config.ts            # Tailwind configuration
├── vitest.config.ts              # Vitest configuration
└── package.json

Quick Start

Installation

bash
git clone https://github.com/halolight/halolight-solidjs.git
cd halolight-solidjs
pnpm install

Environment Variables

bash
cp .env.example .env
bash
# .env example
VITE_API_URL=/api
VITE_USE_MOCK=true
VITE_DEMO_EMAIL=[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=true
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:3000

Build for Production

bash
pnpm build
pnpm start

Demo Account

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Core Functionality

Signals - Fine-grained Reactivity

Solid.js core is Signals, providing the most fine-grained reactive updates:

tsx
import { createSignal, createEffect, createMemo } from 'solid-js';

// Create signal - reactive state
const [count, setCount] = createSignal(0);

// Create derived value - auto track dependencies
const doubled = createMemo(() => count() * 2);

// Create side effect - auto respond to changes
createEffect(() => {
  console.log('count changed:', count());
});

// Update state
setCount(1);        // Set new value
setCount(c => c + 1); // Functional update

Store - Nested Reactive Objects

For complex nested data, use Store:

tsx
import { createStore, produce } from 'solid-js/store';

interface User {
  id: number;
  name: string;
  profile: {
    avatar: string;
    bio: string;
  };
}

const [user, setUser] = createStore<User>({
  id: 1,
  name: 'Admin',
  profile: {
    avatar: '/avatar.png',
    bio: '',
  },
});

// Access - auto track
console.log(user.name);
console.log(user.profile.avatar);

// Update - path-based
setUser('name', 'New Name');
setUser('profile', 'bio', 'This is my bio');

// Update - functional (Immer style)
setUser(
  produce((draft) => {
    draft.name = 'New Name';
    draft.profile.bio = 'This is my bio';
  })
);

State Management (Signals + Store)

tsx
// stores/auth.ts
import { createSignal, createMemo } from 'solid-js'
import { createStore } from 'solid-js/store'
import { makePersisted } from '@solid-primitives/storage'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

interface AuthState {
  user: User | null
  token: string | null
}

const [state, setState] = makePersisted(
  createStore<AuthState>({
    user: null,
    token: null,
  }),
  { name: 'auth' }
)

const [loading, setLoading] = createSignal(false)

export const authStore = {
  get user() { return state.user },
  get token() { return state.token },
  get loading() { return loading() },

  isAuthenticated: createMemo(() => !!state.token && !!state.user),
  permissions: createMemo(() => state.user?.permissions ?? []),

  async login(credentials: { email: string; password: string }) {
    setLoading(true)
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: { 'Content-Type': 'application/json' },
      })
      const data = await response.json()

      setState({
        user: data.user,
        token: data.token,
      })
    } finally {
      setLoading(false)
    }
  },

  logout() {
    setState({ user: null, token: null })
  },

  hasPermission(permission: string): boolean {
    const perms = state.user?.permissions ?? []
    return perms.some(p =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  },
}

UI Settings Store (Skin/Layout)

tsx
// stores/ui-settings.ts
import { createStore } from 'solid-js/store';
import { makePersisted } from '@solid-primitives/storage';

export type SkinPreset =
  | 'default'
  | 'blue'
  | 'emerald'
  | 'amber'
  | 'violet'
  | 'rose'
  | 'teal'
  | 'slate'
  | 'ocean'
  | 'sunset'
  | 'aurora';

export type ThemeMode = 'light' | 'dark' | 'system';

interface UiSettingsState {
  skin: SkinPreset;
  theme: ThemeMode;
  showFooter: boolean;
  showTabBar: boolean;
  sidebarCollapsed: boolean;
}

const [state, setState] = makePersisted(
  createStore<UiSettingsState>({
    skin: 'default',
    theme: 'system',
    showFooter: true,
    showTabBar: true,
    sidebarCollapsed: false,
  }),
  { name: 'ui-settings-storage' }
);

export const uiSettingsStore = {
  get skin() {
    return state.skin;
  },
  get theme() {
    return state.theme;
  },
  get showFooter() {
    return state.showFooter;
  },
  get showTabBar() {
    return state.showTabBar;
  },
  get sidebarCollapsed() {
    return state.sidebarCollapsed;
  },

  setSkin(skin: SkinPreset) {
    document.documentElement.setAttribute('data-skin', skin);
    setState('skin', skin);
  },

  setTheme(theme: ThemeMode) {
    if (theme === 'system') {
      const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      document.documentElement.classList.toggle('dark', isDark);
    } else {
      document.documentElement.classList.toggle('dark', theme === 'dark');
    }
    setState('theme', theme);
  },

  setShowFooter(visible: boolean) {
    setState('showFooter', visible);
  },

  setShowTabBar(visible: boolean) {
    setState('showTabBar', visible);
  },

  toggleSidebar() {
    setState('sidebarCollapsed', (c) => !c);
  },
};

Tabs Store (Tab Management)

tsx
// stores/tabs.ts
import { createStore, produce } from 'solid-js/store';
import { makePersisted } from '@solid-primitives/storage';

interface Tab {
  id: string;
  title: string;
  path: string;
  closable: boolean;
}

const homeTab: Tab = {
  id: 'home',
  title: 'Home',
  path: '/',
  closable: false,
};

interface TabsState {
  tabs: Tab[];
  activeTabId: string;
}

const [state, setState] = makePersisted(
  createStore<TabsState>({
    tabs: [homeTab],
    activeTabId: 'home',
  }),
  { name: 'tabs-storage' }
);

export const tabsStore = {
  get tabs() {
    return state.tabs;
  },
  get activeTabId() {
    return state.activeTabId;
  },
  get activeTab() {
    return state.tabs.find((t) => t.id === state.activeTabId);
  },

  addTab(tab: Omit<Tab, 'id' | 'closable'>): string {
    // Check if already exists
    const existing = state.tabs.find((t) => t.path === tab.path);
    if (existing) {
      setState('activeTabId', existing.id);
      return existing.id;
    }

    const id = crypto.randomUUID();
    const newTab: Tab = { ...tab, id, closable: true };

    setState(
      produce((draft) => {
        draft.tabs.push(newTab);
        draft.activeTabId = id;
      })
    );

    return id;
  },

  removeTab(id: string) {
    const tab = state.tabs.find((t) => t.id === id);
    if (!tab?.closable) return;

    const index = state.tabs.findIndex((t) => t.id === id);
    const newTabs = state.tabs.filter((t) => t.id !== id);

    let newActiveId = state.activeTabId;
    if (state.activeTabId === id) {
      // Switch to adjacent tab
      newActiveId = newTabs[Math.min(index, newTabs.length - 1)]?.id || 'home';
    }

    setState({
      tabs: newTabs,
      activeTabId: newActiveId,
    });
  },

  setActiveTab(id: string) {
    setState('activeTabId', id);
  },

  closeOthers(id: string) {
    setState(
      produce((draft) => {
        draft.tabs = draft.tabs.filter((t) => t.id === id || !t.closable);
        draft.activeTabId = id;
      })
    );
  },

  closeRight(id: string) {
    const index = state.tabs.findIndex((t) => t.id === id);
    setState('tabs', (tabs) => tabs.filter((t, i) => i <= index || !t.closable));
  },

  clearTabs() {
    setState({
      tabs: [homeTab],
      activeTabId: 'home',
    });
  },
};

Route Middleware

tsx
// src/middleware.ts
import { createMiddleware } from '@solidjs/start/middleware';

export default createMiddleware({
  onRequest: [
    // Logging middleware
    async (event) => {
      const start = Date.now();
      const response = await event.next();
      const duration = Date.now() - start;
      console.log(`${event.request.method} ${event.request.url} - ${duration}ms`);
      return response;
    },

    // Auth middleware
    async (event) => {
      const url = new URL(event.request.url);

      // Public paths
      const publicPaths = ['/login', '/register', '/forgot-password', '/reset-password', '/api/auth'];
      const isPublic = publicPaths.some((path) => url.pathname.startsWith(path));

      if (isPublic) {
        return;
      }

      // Protect dashboard routes
      if (url.pathname.startsWith('/dashboard') || url.pathname.startsWith('/api/')) {
        const cookies = event.request.headers.get('cookie') || '';
        const token = cookies.match(/token=([^;]+)/)?.[1];

        if (!token) {
          // API routes return 401
          if (url.pathname.startsWith('/api/')) {
            return new Response(JSON.stringify({ error: 'Unauthorized' }), {
              status: 401,
              headers: { 'Content-Type': 'application/json' },
            });
          }

          // Page routes redirect
          return new Response(null, {
            status: 302,
            headers: { Location: `/login?redirect=${encodeURIComponent(url.pathname)}` },
          });
        }

        // Verify token and inject user info
        try {
          const user = await verifyToken(token);
          event.locals.user = user;
        } catch {
          // Invalid token, clear cookie and redirect
          return new Response(null, {
            status: 302,
            headers: {
              Location: '/login',
              'Set-Cookie': 'token=; Max-Age=0; Path=/',
            },
          });
        }
      }
    },
  ],
});

async function verifyToken(token: string) {
  // In production, verify JWT
  return { id: 1, name: 'Admin', permissions: ['*'] };
}

Server Functions (RPC)

SolidStart supports "use server" marked server functions:

tsx
// server/auth.ts
'use server';
import { z } from 'zod';
import { useSession } from 'vinxi/http';

const loginSchema = z.object({
  email: z.string().email('Please enter a valid email'),
  password: z.string().min(6, 'Password must be at least 6 characters'),
});

const registerSchema = loginSchema.extend({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword'],
});

export async function login(credentials: z.infer<typeof loginSchema>) {
  const validated = loginSchema.parse(credentials);

  // Mock validation
  if (validated.email !== '[email protected]' || validated.password !== '123456') {
    throw new Error('Invalid email or password');
  }

  const user = {
    id: 1,
    name: 'Admin',
    email: validated.email,
    role: 'admin',
    permissions: ['*'],
  };

  const token = `mock_token_${Date.now()}`;

  // Set session
  const session = await useSession({
    password: process.env.SESSION_SECRET!,
  });
  await session.update({ userId: user.id, token });

  return {
    success: true,
    user,
    token,
  };
}

export async function register(data: z.infer<typeof registerSchema>) {
  const validated = registerSchema.parse(data);

  // Check if email already exists
  const existing = await db.users.findByEmail(validated.email);
  if (existing) {
    throw new Error('Email already registered');
  }

  // Create user
  const user = await db.users.create({
    email: validated.email,
    name: validated.name,
    password: await hashPassword(validated.password),
  });

  return { success: true, user };
}

export async function getCurrentUser() {
  const session = await useSession({
    password: process.env.SESSION_SECRET!,
  });

  if (!session.data.userId) {
    return null;
  }

  const user = await db.users.findById(session.data.userId);
  return user;
}

export async function logout() {
  const session = await useSession({
    password: process.env.SESSION_SECRET!,
  });

  await session.clear();
  return { success: true };
}

API Routes

tsx
// routes/api/users/index.ts
import type { APIEvent } from '@solidjs/start/server';
import { json } from '@solidjs/router';

// GET /api/users
export async function GET(event: APIEvent) {
  const url = new URL(event.request.url);
  const page = Number(url.searchParams.get('page')) || 1;
  const limit = Number(url.searchParams.get('limit')) || 10;
  const search = url.searchParams.get('search') || '';

  // Mock data
  const users = generateMockUsers(page, limit, search);
  const total = 100;

  return json({
    success: true,
    data: users,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  });
}

// POST /api/users
export async function POST(event: APIEvent) {
  const body = await event.request.json();
  const { email, name, role } = body;

  // Validation
  if (!email || !name) {
    return json({ success: false, message: 'Email and name are required' }, { status: 400 });
  }

  // Create user
  const user = {
    id: Date.now(),
    email,
    name,
    role: role || 'user',
    createdAt: new Date().toISOString(),
  };

  return json({
    success: true,
    data: user,
    message: 'User created successfully',
  });
}
tsx
// routes/api/users/[id].ts
import type { APIEvent } from '@solidjs/start/server';
import { json } from '@solidjs/router';

// GET /api/users/:id
export async function GET(event: APIEvent) {
  const id = event.params.id;

  const user = await db.users.findById(id);
  if (!user) {
    return json({ success: false, message: 'User not found' }, { status: 404 });
  }

  return json({ success: true, data: user });
}

// PUT /api/users/:id
export async function PUT(event: APIEvent) {
  const id = event.params.id;
  const body = await event.request.json();

  const user = await db.users.update(id, body);
  return json({
    success: true,
    data: user,
    message: 'User updated successfully',
  });
}

// DELETE /api/users/:id
export async function DELETE(event: APIEvent) {
  const id = event.params.id;

  await db.users.delete(id);
  return json({
    success: true,
    message: 'User deleted successfully',
  });
}

// src/middleware.ts import { createMiddleware } from '@solidjs/start/middleware'

export default createMiddleware({ onRequest: [ async (event) => { const url = new URL(event.request.url)

  // Protect dashboard routes
  if (url.pathname.startsWith('/dashboard')) {
    const token = event.request.headers.get('cookie')?.match(/token=([^;]+)/)?.[1]

    if (!token) {
      return new Response(null, {
        status: 302,
        headers: { Location: `/login?redirect=${url.pathname}` },
      })
    }
  }
},

], })


### Server Functions (RPC)

```tsx
// server/auth.ts
'use server'
import { z } from 'zod'

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6),
})

export async function login(credentials: z.infer<typeof loginSchema>) {
  const validated = loginSchema.parse(credentials)

  // Validation logic...

  return {
    success: true,
    user: { id: 1, name: 'Admin', email: validated.email },
    token: 'mock_token',
  }
}

export async function getCurrentUser(token: string) {
  // Validate token and return user
  return {
    id: 1,
    name: 'Admin',
    permissions: ['*'],
  }
}

Permission Component

tsx
// components/shared/PermissionGuard.tsx
import { Show, type ParentComponent, type JSX } from 'solid-js'
import { authStore } from '~/stores/auth'

interface Props {
  permission: string
  fallback?: JSX.Element
}

export const PermissionGuard: ParentComponent<Props> = (props) => {
  const hasPermission = () => authStore.hasPermission(props.permission)

  return (
    <Show when={hasPermission()} fallback={props.fallback}>
      {props.children}
    </Show>
  )
}
tsx
// Usage
<PermissionGuard
  permission="users:delete"
  fallback={<span class="text-muted-foreground">No Permission</span>}
>
  <Button variant="destructive">Delete</Button>
</PermissionGuard>

Data Fetching

tsx
// routes/(dashboard)/users/index.tsx
import { createAsync, cache } from '@solidjs/router'

const getUsers = cache(async (params: { page: number }) => {
  'use server'
  const users = await db.users.findMany({
    skip: (params.page - 1) * 10,
    take: 10,
  })
  return users
}, 'users')

export const route = {
  load: ({ location }) => {
    const page = Number(location.query.page) || 1
    void getUsers({ page })
  },
}

export default function UsersPage() {
  const users = createAsync(() => getUsers({ page: 1 }))

  return (
    <div>
      <h1>User List</h1>
      <Show when={users()}>
        {(data) => (
          <For each={data()}>
            {(user) => <UserCard user={user} />}
          </For>
        )}
      </Show>
    </div>
  )
}

Form Handling

tsx
// routes/(auth)/login.tsx
import { createSignal } from 'solid-js'
import { useNavigate, useSearchParams } from '@solidjs/router'
import { authStore } from '~/stores/auth'

export default function LoginPage() {
  const navigate = useNavigate()
  const [searchParams] = useSearchParams()
  const [email, setEmail] = createSignal('')
  const [password, setPassword] = createSignal('')
  const [error, setError] = createSignal('')

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    setError('')

    try {
      await authStore.login({
        email: email(),
        password: password(),
      })
      navigate(searchParams.redirect || '/dashboard')
    } catch (e) {
      setError('Invalid email or password')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <Show when={error()}>
        <div class="text-destructive">{error()}</div>
      </Show>

      <input
        type="email"
        value={email()}
        onInput={(e) => setEmail(e.currentTarget.value)}
        placeholder="Email"
      />

      <input
        type="password"
        value={password()}
        onInput={(e) => setPassword(e.currentTarget.value)}
        placeholder="Password"
      />

      <button type="submit" disabled={authStore.loading}>
        {authStore.loading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  )
}

Permission Components

tsx
// components/shared/PermissionGuard.tsx
import { Show, type ParentComponent, type JSX, createMemo } from 'solid-js';
import { authStore } from '~/stores/auth';

interface Props {
  permission?: string;
  permissions?: string[];
  mode?: 'any' | 'all';
  fallback?: JSX.Element;
}

export const PermissionGuard: ParentComponent<Props> = (props) => {
  const hasPermission = createMemo(() => {
    // Single permission check
    if (props.permission) {
      return authStore.hasPermission(props.permission);
    }

    // Multiple permissions check
    if (props.permissions) {
      return props.mode === 'all'
        ? authStore.hasAllPermissions(props.permissions)
        : authStore.hasAnyPermission(props.permissions);
    }

    return true;
  });

  return (
    <Show when={hasPermission()} fallback={props.fallback}>
      {props.children}
    </Show>
  );
};

// Usage example
<PermissionGuard
  permission="users:delete"
  fallback={<span class="text-muted-foreground">No Permission</span>}
>
  <Button variant="destructive" onClick={handleDelete}>
    Delete User
  </Button>
</PermissionGuard>

// Multiple permissions check
<PermissionGuard
  permissions={['users:edit', 'users:delete']}
  mode="any"
>
  <DropdownMenu>
    <DropdownMenuItem>Edit</DropdownMenuItem>
    <DropdownMenuItem>Delete</DropdownMenuItem>
  </DropdownMenu>
</PermissionGuard>

Data Fetching

Using createAsync and cache for data fetching:

tsx
// routes/(dashboard)/users/index.tsx
import { createAsync, cache, useSearchParams } from '@solidjs/router';
import { Show, For, Suspense } from 'solid-js';
import { AdminLayout } from '~/components/layout/AdminLayout';
import { Table, Pagination, Button, Input } from '~/components/ui';

// Define cache function
const getUsers = cache(async (params: { page: number; limit: number; search?: string }) => {
  'use server';

  const response = await fetch(
    `${process.env.API_BASE_URL}/users?page=${params.page}&limit=${params.limit}&search=${params.search || ''}`
  );

  if (!response.ok) {
    throw new Error('Failed to fetch users');
  }

  return response.json();
}, 'users');

// Preload
export const route = {
  load: ({ location }) => {
    const page = Number(new URLSearchParams(location.search).get('page')) || 1;
    void getUsers({ page, limit: 10 });
  },
};

export default function UsersPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  const page = () => Number(searchParams.page) || 1;
  const search = () => searchParams.search || '';

  const users = createAsync(() =>
    getUsers({ page: page(), limit: 10, search: search() })
  );

  const handleSearch = (value: string) => {
    setSearchParams({ search: value, page: '1' });
  };

  const handlePageChange = (newPage: number) => {
    setSearchParams({ page: String(newPage) });
  };

  return (
    <AdminLayout title="User Management">
      <div class="space-y-6">
        {/* Search bar */}
        <div class="flex items-center justify-between">
          <Input
            type="search"
            placeholder="Search users..."
            value={search()}
            onInput={(e) => handleSearch(e.currentTarget.value)}
            class="max-w-sm"
          />
          <Button>
            <PlusIcon class="mr-2 h-4 w-4" />
            Add User
          </Button>
        </div>

        {/* Table */}
        <Suspense fallback={<TableSkeleton />}>
          <Show when={users()}>
            {(data) => (
              <>
                <Table>
                  <Table.Header>
                    <Table.Row>
                      <Table.Head>Name</Table.Head>
                      <Table.Head>Email</Table.Head>
                      <Table.Head>Role</Table.Head>
                      <Table.Head>Status</Table.Head>
                      <Table.Head class="text-right">Actions</Table.Head>
                    </Table.Row>
                  </Table.Header>
                  <Table.Body>
                    <For each={data().data}>
                      {(user) => (
                        <Table.Row>
                          <Table.Cell>{user.name}</Table.Cell>
                          <Table.Cell>{user.email}</Table.Cell>
                          <Table.Cell>
                            <Badge>{user.role}</Badge>
                          </Table.Cell>
                          <Table.Cell>
                            <StatusBadge status={user.status} />
                          </Table.Cell>
                          <Table.Cell class="text-right">
                            <UserActions user={user} />
                          </Table.Cell>
                        </Table.Row>
                      )}
                    </For>
                  </Table.Body>
                </Table>

                <Pagination
                  page={page()}
                  totalPages={data().pagination.totalPages}
                  onPageChange={handlePageChange}
                />
              </>
            )}
          </Show>
        </Suspense>
      </div>
    </AdminLayout>
  );
}

Form Handling

tsx
// routes/(auth)/login.tsx
import { createSignal, Show } from 'solid-js';
import { useNavigate, useSearchParams, A } from '@solidjs/router';
import { authStore } from '~/stores/auth';
import { AuthLayout } from '~/components/layout/AuthLayout';
import { Input, Button, Card } from '~/components/ui';

export default function LoginPage() {
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();

  const [email, setEmail] = createSignal('');
  const [password, setPassword] = createSignal('');
  const [errors, setErrors] = createSignal<Record<string, string>>({});

  const validate = () => {
    const newErrors: Record<string, string> = {};

    if (!email()) {
      newErrors.email = 'Please enter email';
    } else if (!email().includes('@')) {
      newErrors.email = 'Please enter a valid email address';
    }

    if (!password()) {
      newErrors.password = 'Please enter password';
    } else if (password().length < 6) {
      newErrors.password = 'Password must be at least 6 characters';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async (e: Event) => {
    e.preventDefault();

    if (!validate()) return;

    try {
      await authStore.login({
        email: email(),
        password: password(),
      });

      // Redirect to original page or dashboard
      const redirect = searchParams.redirect || '/dashboard';
      navigate(redirect);
    } catch (e) {
      setErrors({ form: e instanceof Error ? e.message : 'Login failed' });
    }
  };

  // Fill demo account
  const fillDemo = () => {
    const demoEmail = import.meta.env.VITE_DEMO_EMAIL;
    const demoPassword = import.meta.env.VITE_DEMO_PASSWORD;
    if (demoEmail) setEmail(demoEmail);
    if (demoPassword) setPassword(demoPassword);
  };

  return (
    <AuthLayout title="Login">
      <Card class="w-full max-w-md">
        <Card.Header class="text-center">
          <Card.Title class="text-2xl">Welcome Back</Card.Title>
          <Card.Description>Login to your account</Card.Description>
        </Card.Header>

        <Card.Content>
          {/* Error message */}
          <Show when={errors().form}>
            <div class="mb-4 rounded-md bg-destructive/10 p-3 text-sm text-destructive">
              {errors().form}
            </div>
          </Show>

          {/* Demo hint */}
          <Show when={import.meta.env.VITE_SHOW_DEMO_HINT === 'true'}>
            <div class="mb-4 rounded-md bg-muted p-3 text-sm">
              <p>Demo Account:</p>
              <p class="font-mono text-xs">
                Email: {import.meta.env.VITE_DEMO_EMAIL}
              </p>
              <p class="font-mono text-xs">
                Password: {import.meta.env.VITE_DEMO_PASSWORD}
              </p>
              <Button variant="link" size="sm" onClick={fillDemo} class="mt-1 h-auto p-0">
                Click to fill
              </Button>
            </div>
          </Show>

          <form onSubmit={handleSubmit} class="space-y-4">
            <div class="space-y-2">
              <label for="email" class="text-sm font-medium">
                Email
              </label>
              <Input
                id="email"
                type="email"
                value={email()}
                onInput={(e) => setEmail(e.currentTarget.value)}
                placeholder="[email protected]"
                autocomplete="email"
              />
              <Show when={errors().email}>
                <p class="text-sm text-destructive">{errors().email}</p>
              </Show>
            </div>

            <div class="space-y-2">
              <div class="flex items-center justify-between">
                <label for="password" class="text-sm font-medium">
                  Password
                </label>
                <A href="/forgot-password" class="text-sm text-primary hover:underline">
                  Forgot password?
                </A>
              </div>
              <Input
                id="password"
                type="password"
                value={password()}
                onInput={(e) => setPassword(e.currentTarget.value)}
                placeholder="••••••••"
                autocomplete="current-password"
              />
              <Show when={errors().password}>
                <p class="text-sm text-destructive">{errors().password}</p>
              </Show>
            </div>

            <Button type="submit" class="w-full" disabled={authStore.loading}>
              {authStore.loading ? 'Logging in...' : 'Login'}
            </Button>
          </form>
        </Card.Content>

        <Card.Footer class="justify-center">
          <p class="text-sm text-muted-foreground">
            Don't have an account?{' '}
            <A href="/register" class="text-primary hover:underline">
              Sign up now
            </A>
          </p>
        </Card.Footer>
      </Card>
    </AuthLayout>
  );
}

Error Handling

tsx
// components/shared/ErrorBoundary.tsx
import { ErrorBoundary as SolidErrorBoundary, type ParentComponent } from 'solid-js';
import { A, useNavigate } from '@solidjs/router';
import { Button, Card } from '~/components/ui';

interface Props {
  fallback?: (error: Error, reset: () => void) => JSX.Element;
}

export const ErrorBoundary: ParentComponent<Props> = (props) => {
  return (
    <SolidErrorBoundary
      fallback={(error, reset) => {
        if (props.fallback) {
          return props.fallback(error, reset);
        }

        return <DefaultErrorFallback error={error} reset={reset} />;
      }}
    >
      {props.children}
    </SolidErrorBoundary>
  );
};

function DefaultErrorFallback(props: { error: Error; reset: () => void }) {
  const navigate = useNavigate();

  return (
    <div class="flex min-h-[400px] items-center justify-center p-4">
      <Card class="w-full max-w-md">
        <Card.Header class="text-center">
          <Card.Title class="text-destructive">An Error Occurred</Card.Title>
          <Card.Description>{props.error.message}</Card.Description>
        </Card.Header>
        <Card.Content class="space-y-4">
          <div class="flex justify-center gap-2">
            <Button variant="outline" onClick={props.reset}>
              Retry
            </Button>
            <Button onClick={() => navigate('/')}>
              Back to Home
            </Button>
          </div>
        </Card.Content>
      </Card>
    </div>
  );
}
tsx
// routes/[...404].tsx - 404 page
import { A } from '@solidjs/router';
import { Button } from '~/components/ui';

export default function NotFoundPage() {
  return (
    <div class="flex min-h-screen items-center justify-center">
      <div class="text-center">
        <h1 class="text-9xl font-bold text-muted-foreground">404</h1>
        <p class="mt-4 text-2xl text-foreground">Page Not Found</p>
        <p class="mt-2 text-muted-foreground">
          The page you're looking for doesn't exist or has been removed
        </p>
        <Button as={A} href="/" class="mt-8">
          Back to Home
        </Button>
      </div>
    </div>
  );
}

Meta (TDK Meta Information)

tsx
// lib/meta.ts
interface PageMeta {
  title: string;
  description: string;
  keywords?: string[];
}

export const pageMetas: Record<string, PageMeta> = {
  '/': {
    title: 'Dashboard',
    description: 'Admin Pro management system dashboard with data overview and analytics',
    keywords: ['dashboard', 'analytics', 'management'],
  },
  '/users': {
    title: 'User Management',
    description: 'Manage system user accounts including creation, editing, and permission configuration',
    keywords: ['user management', 'account management', 'permissions'],
  },
  '/analytics': {
    title: 'Analytics',
    description: 'Business data statistics and visualization charts',
    keywords: ['analytics', 'charts', 'statistics'],
  },
  '/settings': {
    title: 'System Settings',
    description: 'System configuration and personalization settings',
    keywords: ['settings', 'configuration', 'personalization'],
  },
};

export function generateMeta(path: string, overrides?: Partial<PageMeta>) {
  const meta = { ...pageMetas[path], ...overrides } || {
    title: 'Page',
    description: 'Admin Pro Management System',
  };

  const brandName = import.meta.env.VITE_BRAND_NAME || 'Halolight';
  const fullTitle = `${meta.title} - ${brandName}`;

  return {
    title: fullTitle,
    description: meta.description,
    keywords: meta.keywords?.join(', ') || '',
  };
}
tsx
// Usage in pages
import { Title, Meta } from '@solidjs/meta';
import { generateMeta } from '~/lib/meta';

export default function UsersPage() {
  const meta = generateMeta('/users');

  return (
    <>
      <Title>{meta.title}</Title>
      <Meta name="description" content={meta.description} />
      <Meta name="keywords" content={meta.keywords} />

      {/* Page content */}
    </>
  );
}

Theme System

Skin Presets

Supports 11 preset skins, switchable via Quick Settings panel:

Skin Primary Color CSS Variable
Default Purple --primary: 51.1% 0.262 276.97
Blue Blue --primary: 54.8% 0.243 264.05
Emerald Emerald --primary: 64.6% 0.178 142.49
Amber Amber --primary: 76.9% 0.188 84.94
Violet Violet --primary: 54.1% 0.243 293.54
Rose Rose --primary: 64.5% 0.246 16.44
Teal Teal --primary: 60.0% 0.118 184.71
Slate Slate --primary: 45.9% 0.022 264.53
Ocean Ocean --primary: 54.3% 0.195 240.03
Sunset Sunset --primary: 70.5% 0.213 47.60
Aurora Aurora --primary: 62.8% 0.265 303.9

CSS Variables (OKLch)

css
/* src/styles/globals.css */
@import "tailwindcss";

:root {
  /* Background colors */
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;

  /* Card */
  --card: 100% 0 0;
  --card-foreground: 14.9% 0.017 285.75;

  /* Popover */
  --popover: 100% 0 0;
  --popover-foreground: 14.9% 0.017 285.75;

  /* Primary */
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;

  /* Secondary */
  --secondary: 96.7% 0.001 286.38;
  --secondary-foreground: 21% 0.006 285.75;

  /* Muted */
  --muted: 96.7% 0.001 286.38;
  --muted-foreground: 55.2% 0.014 285.94;

  /* Accent */
  --accent: 96.7% 0.001 286.38;
  --accent-foreground: 21% 0.006 285.75;

  /* Destructive */
  --destructive: 57.7% 0.245 27.32;
  --destructive-foreground: 100% 0 0;

  /* Border/Input */
  --border: 91.2% 0.004 286.32;
  --input: 91.2% 0.004 286.32;
  --ring: 51.1% 0.262 276.97;

  /* Radius */
  --radius: 0.5rem;
}

/* Skin presets */
[data-skin="blue"] {
  --primary: 54.8% 0.243 264.05;
  --ring: 54.8% 0.243 264.05;
}

[data-skin="ocean"] {
  --primary: 54.3% 0.195 240.03;
  --ring: 54.3% 0.195 240.03;
}

[data-skin="emerald"] {
  --primary: 64.6% 0.178 142.49;
  --ring: 64.6% 0.178 142.49;
}

/* Dark mode */
.dark {
  --background: 14.9% 0.017 285.75;
  --foreground: 98.5% 0 0;
  --card: 14.9% 0.017 285.75;
  --card-foreground: 98.5% 0 0;
  --popover: 14.9% 0.017 285.75;
  --popover-foreground: 98.5% 0 0;
  --secondary: 26.8% 0.019 286.07;
  --secondary-foreground: 98.5% 0 0;
  --muted: 26.8% 0.019 286.07;
  --muted-foreground: 71.2% 0.013 286.07;
  --accent: 26.8% 0.019 286.07;
  --accent-foreground: 98.5% 0 0;
  --border: 26.8% 0.019 286.07;
  --input: 26.8% 0.019 286.07;
}

/* Tailwind theme mapping */
@theme {
  --color-background: oklch(var(--background));
  --color-foreground: oklch(var(--foreground));
  --color-primary: oklch(var(--primary));
  --color-primary-foreground: oklch(var(--primary-foreground));
  /* ... */
}

Page Routes

Path Page Layout Permission
/ Home - Public
/login Login AuthLayout Public
/register Register AuthLayout Public
/forgot-password Forgot Password AuthLayout Public
/reset-password Reset Password AuthLayout Public
/dashboard Dashboard AdminLayout dashboard:view
/analytics Analytics AdminLayout analytics:view
/users User List AdminLayout users:list
/users/create Create User AdminLayout users:create
/users/[id] User Details AdminLayout users:view
/roles Role Management AdminLayout roles:list
/permissions Permission Management AdminLayout permissions:list
/messages Message Center AdminLayout messages:view
/notifications Notifications AdminLayout Authenticated
/documents Document Management AdminLayout documents:list
/calendar Calendar AdminLayout calendar:view
/settings System Settings AdminLayout settings:view
/profile User Profile AdminLayout Authenticated
/privacy Privacy Policy - Public
/terms Terms of Service - Public

Environment Variables

Variable Description Default
VITE_API_URL API base URL /api
VITE_USE_MOCK Enable mock data false
VITE_DEMO_EMAIL Demo account email -
VITE_DEMO_PASSWORD Demo account password -
VITE_SHOW_DEMO_HINT Show demo hint false
VITE_APP_TITLE Application title Admin Pro
VITE_BRAND_NAME Brand name Halolight
SESSION_SECRET Session secret (server-side) (required)

Common Commands

bash
# Development
pnpm dev              # Start development server
pnpm dev --host       # Allow LAN access

# Build
pnpm build            # Production build
pnpm start            # Start production server

# Code Quality
pnpm typecheck        # TypeScript type check
pnpm lint             # ESLint check
pnpm lint:fix         # ESLint auto fix
pnpm format           # Prettier format

# Testing
pnpm test             # Watch mode
pnpm test:run         # Single run
pnpm test:coverage    # Coverage report
pnpm test:ui          # Vitest UI

# Others
pnpm clean            # Clean build artifacts
pnpm deps             # Check dependency updates

Testing

Run Tests

bash
pnpm test:run      # Single run
pnpm test          # Watch mode
pnpm test:coverage # Coverage report

Test Examples

tsx
// tests/stores/auth.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { authStore } from '~/stores/auth';

describe('authStore', () => {
  beforeEach(() => {
    authStore.logout();
  });

  it('initial state should be unauthenticated', () => {
    expect(authStore.isAuthenticated()).toBe(false);
    expect(authStore.user).toBeNull();
    expect(authStore.token).toBeNull();
  });

  it('should update state after successful login', async () => {
    // Mock fetch
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () =>
        Promise.resolve({
          user: { id: 1, name: 'Admin', permissions: ['*'] },
          token: 'mock_token',
        }),
    });

    await authStore.login({
      email: '[email protected]',
      password: '123456',
    });

    expect(authStore.isAuthenticated()).toBe(true);
    expect(authStore.user?.name).toBe('Admin');
    expect(authStore.token).toBe('mock_token');
  });

  it('permission checks should work correctly', async () => {
    // Set user permissions
    authStore.login({
      email: '[email protected]',
      password: '123456',
    });

    // Mock successful login
    expect(authStore.hasPermission('*')).toBe(true);
    expect(authStore.hasPermission('users:list')).toBe(true);
    expect(authStore.hasPermission('unknown:action')).toBe(true); // * permission
  });

  it('should clear state after logout', () => {
    authStore.logout();

    expect(authStore.isAuthenticated()).toBe(false);
    expect(authStore.user).toBeNull();
    expect(authStore.token).toBeNull();
  });
});
tsx
// tests/stores/tabs.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { tabsStore } from '~/stores/tabs';

describe('tabsStore', () => {
  beforeEach(() => {
    tabsStore.clearTabs();
  });

  it('initial state should only have home tab', () => {
    expect(tabsStore.tabs.length).toBe(1);
    expect(tabsStore.tabs[0].id).toBe('home');
    expect(tabsStore.activeTabId).toBe('home');
  });

  it('should add new tab', () => {
    const id = tabsStore.addTab({ title: 'User Management', path: '/users' });

    expect(tabsStore.tabs.length).toBe(2);
    expect(tabsStore.tabs[1].title).toBe('User Management');
    expect(tabsStore.activeTabId).toBe(id);
  });

  it('should deduplicate existing routes', () => {
    const id1 = tabsStore.addTab({ title: 'User Management', path: '/users' });
    const id2 = tabsStore.addTab({ title: 'User Management', path: '/users' });

    expect(id1).toBe(id2);
    expect(tabsStore.tabs.length).toBe(2);
  });

  it('should close tab and switch to adjacent tab', () => {
    tabsStore.addTab({ title: 'User Management', path: '/users' });
    const id = tabsStore.addTab({ title: 'Settings', path: '/settings' });

    tabsStore.removeTab(id);

    expect(tabsStore.tabs.length).toBe(2);
    expect(tabsStore.activeTabId).not.toBe(id);
  });

  it('home tab should not be closable', () => {
    tabsStore.removeTab('home');

    expect(tabsStore.tabs.length).toBe(1);
    expect(tabsStore.tabs[0].id).toBe('home');
  });
});

Configuration

SolidStart Configuration

ts
// app.config.ts
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'node-server', // Default Node.js server
  },
  vite: {
    plugins: [],
    css: {
      postcss: './postcss.config.js',
    },
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['solid-js', '@solidjs/router'],
            ui: ['@kobalte/core'],
          },
        },
      },
    },
  },
  middleware: './src/middleware.ts',
});

Different Environment Presets

ts
// Development environment
export default defineConfig({
  server: { preset: 'node-server' },
});

// Vercel
export default defineConfig({
  server: { preset: 'vercel' },
});

// Cloudflare Pages
export default defineConfig({
  server: { preset: 'cloudflare-pages' },
});

// Netlify
export default defineConfig({
  server: { preset: 'netlify' },
});

// AWS Lambda
export default defineConfig({
  server: { preset: 'aws-lambda' },
});

// Bun
export default defineConfig({
  server: { preset: 'bun' },
});

Deployment

Node.js Server

bash
pnpm build
node .output/server/index.mjs

Docker

dockerfile
FROM node:20-alpine AS builder

RUN corepack enable && corepack prepare pnpm@latest --activate

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/.output ./.output
COPY --from=builder /app/package.json ./

ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - SESSION_SECRET=${SESSION_SECRET}
      - VITE_API_URL=${VITE_API_URL}
    restart: unless-stopped

Vercel

ts
// app.config.ts
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'vercel',
  },
});
bash
# Deploy
npx vercel

Cloudflare Pages

ts
// app.config.ts
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'cloudflare-pages',
  },
});
bash
# Install Wrangler
npm install -g wrangler

# Login
wrangler login

# Deploy
wrangler pages deploy .output/public

Netlify

ts
// app.config.ts
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'netlify',
  },
});
toml
# netlify.toml
[build]
  command = "pnpm build"
  publish = ".output/public"
  functions = ".output/server"

[functions]
  node_bundler = "esbuild"

GitHub Actions Deployment

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install --frozen-lockfile
      - run: pnpm build

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

CI/CD

Project has complete GitHub Actions CI workflow:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

createResource (Async Data)

tsx
import { createResource, Suspense, Show } from 'solid-js';

// Define data fetching function
const fetchUser = async (id: string) => {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error('User not found');
  return response.json();
};

function UserProfile(props: { userId: string }) {
  // createResource automatically manages loading/error states
  const [user, { refetch, mutate }] = createResource(
    () => props.userId,
    fetchUser
  );

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Show when={user()} fallback={<div>User not found</div>}>
        {(userData) => (
          <div>
            <h1>{userData().name}</h1>
            <p>{userData().email}</p>
            <button onClick={refetch}>Refresh</button>
          </div>
        )}
      </Show>
    </Suspense>
  );
}

Streaming SSR

tsx
// routes/dashboard.tsx
import { Suspense } from 'solid-js';
import { createAsync, cache } from '@solidjs/router';

// Fast data
const getQuickStats = cache(async () => {
  'use server';
  return await db.stats.getQuick();
}, 'quick-stats');

// Slow data
const getDetailedAnalytics = cache(async () => {
  'use server';
  return await db.analytics.getDetailed(); // Time-consuming operation
}, 'detailed-analytics');

export default function Dashboard() {
  const quickStats = createAsync(() => getQuickStats());
  const analytics = createAsync(() => getDetailedAnalytics());

  return (
    <div class="space-y-6">
      {/* Fast rendered content */}
      <Show when={quickStats()}>
        {(stats) => <QuickStats data={stats()} />}
      </Show>

      {/* Stream rendered slow content */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <Show when={analytics()}>
          {(data) => <DetailedAnalytics data={data()} />}
        </Show>
      </Suspense>
    </div>
  );
}

Optimistic Updates

tsx
import { createSignal, For } from 'solid-js';
import { createStore, produce } from 'solid-js/store';

function TodoList() {
  const [todos, setTodos] = createStore<Todo[]>([]);
  const [newTodo, setNewTodo] = createSignal('');

  const addTodo = async () => {
    const text = newTodo();
    if (!text.trim()) return;

    // Optimistic update - show immediately
    const tempId = `temp-${Date.now()}`;
    const optimisticTodo: Todo = {
      id: tempId,
      text,
      completed: false,
      pending: true,
    };

    setTodos(produce((draft) => draft.push(optimisticTodo)));
    setNewTodo('');

    try {
      // Actual request
      const response = await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify({ text }),
      });
      const realTodo = await response.json();

      // Replace with real data
      setTodos(
        (todo) => todo.id === tempId,
        { id: realTodo.id, pending: false }
      );
    } catch {
      // Rollback
      setTodos((todos) => todos.filter((t) => t.id !== tempId));
    }
  };

  return (
    <div>
      <input
        value={newTodo()}
        onInput={(e) => setNewTodo(e.currentTarget.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
      />
      <For each={todos}>
        {(todo) => (
          <div class={todo.pending ? 'opacity-50' : ''}>
            {todo.text}
            {todo.pending && <span>Saving...</span>}
          </div>
        )}
      </For>
    </div>
  );
}

Context Cross-component Communication

tsx
// context/theme.tsx
import { createContext, useContext, type ParentComponent } from 'solid-js';
import { createStore } from 'solid-js/store';

interface ThemeContextValue {
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;
  toggle: () => void;
}

const ThemeContext = createContext<ThemeContextValue>();

export const ThemeProvider: ParentComponent = (props) => {
  const [state, setState] = createStore({ theme: 'light' as const });

  const value: ThemeContextValue = {
    get theme() {
      return state.theme;
    },
    setTheme(theme) {
      setState('theme', theme);
      document.documentElement.classList.toggle('dark', theme === 'dark');
    },
    toggle() {
      value.setTheme(state.theme === 'light' ? 'dark' : 'light');
    },
  };

  return (
    <ThemeContext.Provider value={value}>
      {props.children}
    </ThemeContext.Provider>
  );
};

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

Portal and Modal

tsx
import { Portal, Show } from 'solid-js/web';
import { createSignal } from 'solid-js';

function Modal(props: { isOpen: boolean; onClose: () => void; children: JSX.Element }) {
  return (
    <Show when={props.isOpen}>
      <Portal mount={document.body}>
        <div class="fixed inset-0 z-50 flex items-center justify-center">
          {/* Backdrop */}
          <div
            class="absolute inset-0 bg-black/50"
            onClick={props.onClose}
          />
          {/* Content */}
          <div class="relative z-10 rounded-lg bg-background p-6 shadow-lg">
            {props.children}
          </div>
        </div>
      </Portal>
    </Show>
  );
}

// Usage
function App() {
  const [isOpen, setIsOpen] = createSignal(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      <Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
        <h2>Title</h2>
        <p>Content</p>
        <button onClick={() => setIsOpen(false)}>Close</button>
      </Modal>
    </>
  );
}

Performance Optimization

Fine-grained Updates

Solid.js core advantage is fine-grained updates, no manual optimization needed:

tsx
// Component doesn't re-execute when parent updates
function Parent() {
  const [count, setCount] = createSignal(0);

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>
        Count: {count()}
      </button>
      {/* Child component only created once */}
      <Child />
    </div>
  );
}

function Child() {
  console.log('Child rendered'); // Only executes once
  return <div>I'm a child</div>;
}

Lazy Loading Components

tsx
import { lazy, Suspense } from 'solid-js';

// Lazy load heavy components
const Chart = lazy(() => import('./components/Chart'));
const DataTable = lazy(() => import('./components/DataTable'));

function Dashboard() {
  return (
    <div>
      <Suspense fallback={<ChartSkeleton />}>
        <Chart />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <DataTable />
      </Suspense>
    </div>
  );
}

List Optimization

tsx
import { For, Index } from 'solid-js';

// For - suitable for object arrays, tracked by reference
<For each={users()}>
  {(user, index) => (
    <div>{index()}: {user.name}</div>
  )}
</For>

// Index - suitable for primitive arrays, tracked by index
<Index each={numbers()}>
  {(num, index) => (
    <div>{index}: {num()}</div>
  )}
</Index>

Preloading

tsx
// Route preload
export const route = {
  load: ({ params }) => {
    // Preload data
    void getUser({ id: params.id });
    void getUserPosts({ userId: params.id });
  },
};

// Link preload
<A href="/users" preload>
  User Management
</A>

FAQ

Q: What are the main differences between Solid.js and React?

A: Core differences:

  1. No Virtual DOM - Solid directly manipulates real DOM
  2. Fine-grained Reactivity - Only updates changed parts, components don't re-execute
  3. Compile-time Optimization - JSX compiled to efficient DOM operations
  4. Signals vs Hooks - Signals are true reactive primitives
tsx
// React - component re-executes on every state change
function ReactComponent() {
  const [count, setCount] = useState(0);
  console.log('render'); // Logs on every update
  return <div>{count}</div>;
}

// Solid - component executes once, only where signal is accessed updates
function SolidComponent() {
  const [count, setCount] = createSignal(0);
  console.log('setup'); // Only logs once
  return <div>{count()}</div>; // Only this updates
}

Q: How to handle async data?

A: Use createResource or createAsync:

tsx
// createResource - more granular control
const [data, { refetch, mutate }] = createResource(source, fetcher);

// createAsync - SolidStart route integration
const data = createAsync(() => getData());

Q: How to share state?

A: Three approaches:

  1. Export Signals/Store - Simple scenarios
tsx
// stores/counter.ts
export const [count, setCount] = createSignal(0);
  1. Context - Dependency injection
tsx
const CounterContext = createContext();
  1. solid-primitives/storage - Persistence
tsx
const [state, setState] = makePersisted(createStore({}), { name: 'key' });

Q: How to handle forms?

A: Use controlled components or @modular-forms/solid:

tsx
// Controlled component
const [email, setEmail] = createSignal('');
<input value={email()} onInput={(e) => setEmail(e.currentTarget.value)} />

// Or use form library
import { createForm } from '@modular-forms/solid';

const [form, { Form, Field }] = createForm<LoginForm>();

Q: How to implement route guards?

A: Use middleware or route load function:

tsx
// middleware.ts
export default createMiddleware({
  onRequest: [authMiddleware],
});

// Or in route
export const route = {
  load: ({ location }) => {
    if (!isAuthenticated()) {
      throw redirect('/login');
    }
  },
};

Comparison with Other Versions

Feature Solid.js Version Vue Version Next.js Version Remix Version
State Management Signals + Store Pinia Zustand Zustand
Data Fetching createAsync TanStack Query TanStack Query Loader/Action
Form Validation Custom + Zod VeeValidate + Zod React Hook Form + Zod Progressive Enhancement
Server-side SolidStart Built-in Separate Backend / Nuxt API Routes Built-in
Component Library Kobalte shadcn-vue shadcn/ui Radix UI
Routing File-based Routing Vue Router App Router File-based Routing
Reactivity Fine-grained Signals Proxy-based Hooks Hooks
Bundle Size ~7KB ~33KB ~85KB ~70KB
Runtime Performance Extremely High High Medium Medium
]]>
<![CDATA[SvelteKit Version]]> https://halolight.docs.h7ml.cn/en/guide/sveltekit https://halolight.docs.h7ml.cn/en/guide/sveltekit Fri, 19 Dec 2025 04:56:41 GMT SvelteKit Version

HaloLight SvelteKit version is built on SvelteKit 2, featuring Svelte 5 Runes + TypeScript with compile-time optimization and ultimate performance.

Live Preview: https://halolight-svelte.h7ml.cn

GitHub: https://github.com/halolight/halolight-svelte

Features

  • 🏗️ Svelte 5 Runes - New reactivity system ($state/$derived/$effect)
  • Compile-time Optimization - No virtual DOM, minimal runtime overhead
  • 🎨 Theme System - 11 skins, dark mode, View Transitions
  • 🔐 Authentication - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization and business management
  • 🛡️ Permission Control - RBAC fine-grained access control
  • 📑 Multi-tabs - Browser-style tab management
  • Command Palette - Keyboard shortcuts (⌘K)

Tech Stack

Technology Version Description
SvelteKit 2.x Svelte full-stack framework
Svelte 5.x Compile-time framework (Runes)
TypeScript 5.9 Type safety
Tailwind CSS 4.x Atomic CSS
shadcn-svelte latest UI component library
Superforms 2.x Form handling
TanStack Query 5.x Server state
Mock.js 1.x Data mocking

Core Features

  • Configurable Dashboard - 9 widgets, drag-and-drop layout, responsive
  • Multi-tab Navigation - Browser-style tabs, context menu, state caching
  • Permission System - RBAC control, route guards, permission components
  • Theme System - 11 skins, dark mode, View Transitions
  • Multi-account Switching - Quick account switching, remember login
  • Command Palette - Keyboard shortcuts (⌘K), global search
  • Real-time Notifications - WebSocket push, notification center

Directory Structure

halolight-svelte/
├── src/
│   ├── routes/                    # File-based routing
│   │   ├── (auth)/                # Auth pages
│   │   └── (dashboard)/           # Dashboard pages
│   ├── lib/
│   │   ├── components/            # Components
│   │   │   ├── ui/               # Base UI components
│   │   │   ├── layout/           # Layout components
│   │   │   └── dashboard/        # Dashboard components
│   │   ├── stores/               # State management (Runes)
│   │   ├── utils/                # Utilities
│   │   ├── mock/                 # Mock data
│   │   └── types/                # Type definitions
│   ├── hooks.server.ts           # Server hooks
│   └── app.css                   # Global styles
├── static/                        # Static assets
├── svelte.config.js              # Svelte config
└── package.json

Quick Start

Environment Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-svelte.git
cd halolight-svelte
pnpm install

Environment Variables

bash
cp .env.example .env
env
# .env
VITE_API_URL=/api
VITE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:5173

Build for Production

bash
pnpm build
pnpm preview

Core Features

State Management (Svelte 5 Runes)

ts
// lib/stores/auth.ts
import { browser } from '$app/environment';

interface User {
  id: number;
  name: string;
  email: string;
  permissions: string[];
}

class AuthStore {
  user = $state<User | null>(null);
  token = $state<string | null>(null);

  isAuthenticated = $derived(!!this.token && !!this.user);
  permissions = $derived(this.user?.permissions ?? []);

  constructor() {
    if (browser) {
      const saved = localStorage.getItem('auth');
      if (saved) {
        const { user, token } = JSON.parse(saved);
        this.user = user;
        this.token = token;
      }
    }
  }

  async login(credentials: { email: string; password: string }) {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
      headers: { 'Content-Type': 'application/json' },
    });

    const data = await response.json();
    this.user = data.user;
    this.token = data.token;
    this.persist();
  }

  logout() {
    this.user = null;
    this.token = null;
    localStorage.removeItem('auth');
  }

  hasPermission(permission: string): boolean {
    return this.permissions.some(
      (p) =>
        p === '*' || p === permission || (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    );
  }

  private persist() {
    if (browser) {
      localStorage.setItem(
        'auth',
        JSON.stringify({
          user: this.user,
          token: this.token,
        })
      );
    }
  }
}

export const authStore = new AuthStore();

Data Fetching (Load Functions)

ts
// routes/(dashboard)/+layout.ts
import type { LayoutLoad } from './$types';
import { redirect } from '@sveltejs/kit';

export const load: LayoutLoad = async ({ parent, url }) => {
  const { user } = await parent();

  if (!user) {
    throw redirect(302, `/auth/login?redirect=${url.pathname}`);
  }

  return { user };
};
svelte
<!-- routes/(dashboard)/dashboard/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data }: { data: PageData } = $props();
</script>

<h1>Welcome, {data.user.name}!</h1>

Permission Control

svelte
<!-- lib/components/PermissionGuard.svelte -->
<script lang="ts">
  import { authStore } from '$lib/stores/auth';

  interface Props {
    permission: string;
    children: import('svelte').Snippet;
    fallback?: import('svelte').Snippet;
  }

  let { permission, children, fallback }: Props = $props();

  const hasPermission = $derived(authStore.hasPermission(permission));
</script>

{#if hasPermission}
  {@render children()}
{:else if fallback}
  {@render fallback()}
{/if}
svelte
<!-- Usage example -->
<PermissionGuard permission="users:delete">
  {#snippet children()}
    <Button variant="destructive">Delete</Button>
  {/snippet}
  {#snippet fallback()}
    <span class="text-muted-foreground">No Permission</span>
  {/snippet}
</PermissionGuard>

Draggable Dashboard

svelte
<script lang="ts">
  import { SvelteSet } from 'svelte/reactivity';
  import GridLayout from '$lib/components/dashboard/GridLayout.svelte';

  // Reactive Set for managing widgets
  let activeWidgets = new SvelteSet(['stats', 'chart', 'recent']);

  const layout = $state([
    { i: 'stats', x: 0, y: 0, w: 4, h: 2 },
    { i: 'chart', x: 4, y: 0, w: 8, h: 4 },
    { i: 'recent', x: 0, y: 2, w: 4, h: 2 },
  ]);

  function onLayoutChange(newLayout: typeof layout) {
    layout.splice(0, layout.length, ...newLayout);
    localStorage.setItem('dashboard-layout', JSON.stringify(newLayout));
  }
</script>

<GridLayout {layout} on:change={onLayoutChange}>
  {#each [...activeWidgets] as widget}
    <div data-grid-item={widget}>
      <Widget type={widget} />
    </div>
  {/each}
</GridLayout>

Theme System

Skin Presets

Support 11 preset skins, switch via quick settings panel:

Skin Primary Color CSS Variable
Default Purple --primary: 51.1% 0.262 276.97
Blue Blue --primary: 54.8% 0.243 264.05
Emerald Emerald --primary: 64.6% 0.178 142.49
Orange Orange --primary: 68.9% 0.181 40.84
Rose Rose --primary: 60.7% 0.234 11.63
Teal Teal --primary: 62.8% 0.149 186.07
Yellow Yellow --primary: 82.3% 0.165 92.14
Violet Violet --primary: 58.9% 0.264 292.85
Cyan Cyan --primary: 73.2% 0.152 196.85
Pink Pink --primary: 70.5% 0.226 340.54
Indigo Indigo --primary: 52.4% 0.218 270.32

CSS Variables (OKLch)

css
/* app.css */
@layer base {
  :root {
    --background: 100% 0 0;
    --foreground: 14.9% 0.017 285.75;
    --primary: 51.1% 0.262 276.97;
    --primary-foreground: 98% 0.007 285.89;
    --secondary: 96.1% 0.006 286.32;
    --secondary-foreground: 14.9% 0.017 285.75;
    --muted: 96.1% 0.006 286.32;
    --muted-foreground: 45.5% 0.026 285.82;
    --accent: 96.1% 0.006 286.32;
    --accent-foreground: 14.9% 0.017 285.75;
    --destructive: 61.1% 0.246 29.23;
    --destructive-foreground: 98% 0.007 285.89;
    --border: 92.1% 0.011 286.32;
    --input: 92.1% 0.011 286.32;
    --ring: 51.1% 0.262 276.97;
    --radius: 0.5rem;
  }

  .dark {
    --background: 22.4% 0.015 285.88;
    --foreground: 98% 0.007 285.89;
    --primary: 61.1% 0.262 276.97;
    --primary-foreground: 98% 0.007 285.89;
    /* ... */
  }
}

View Transitions Theme Switching

svelte
<script lang="ts">
  function toggleTheme() {
    if (!document.startViewTransition) {
      document.documentElement.classList.toggle('dark');
      return;
    }

    document.startViewTransition(() => {
      document.documentElement.classList.toggle('dark');
    });
  }
</script>

<button onclick={toggleTheme}>Toggle Theme</button>

<style>
  :global(::view-transition-old(root)),
  :global(::view-transition-new(root)) {
    animation-duration: 0.3s;
  }
</style>

Page Routes

Path Page Permission
/ Home (redirect) Public
/auth/login Login Public
/auth/register Register Public
/auth/forgot-password Forgot Password Public
/auth/reset-password Reset Password Public
/dashboard Dashboard dashboard:view
/dashboard/users User Management users:view
/dashboard/analytics Analytics analytics:view
/dashboard/calendar Calendar calendar:view
/dashboard/documents Documents documents:view
/dashboard/files Files files:view
/dashboard/messages Messages messages:view
/dashboard/notifications Notifications notifications:view
/dashboard/settings Settings settings:view
/dashboard/profile Profile settings:view

Common Commands

bash
pnpm dev            # Start dev server
pnpm build          # Production build
pnpm preview        # Preview production build
pnpm lint           # Lint code
pnpm lint:fix       # Auto-fix lint issues
pnpm format         # Format code
pnpm check          # Type check (svelte-check)
pnpm test           # Run tests
pnpm test:coverage  # Test coverage
pnpm ci             # Full CI check

Deployment

Deploy to Cloudflare Pages

Project is configured with Cloudflare Pages adapter by default:

js
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter(),
  },
};
bash
pnpm build
# Cloudflare Pages will automatically deploy main branch

Docker

bash
docker build -t halolight-svelte .
docker run -p 3000:3000 halolight-svelte

Other Platforms

Demo Accounts

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Testing

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Single run
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI

Test Examples

ts
// tests/auth.test.ts
import { describe, it, expect } from 'vitest';
import { authStore } from '$lib/stores/auth';

describe('AuthStore', () => {
  it('should initialize with null user', () => {
    expect(authStore.user).toBeNull();
    expect(authStore.isAuthenticated).toBe(false);
  });

  it('should authenticate user', async () => {
    await authStore.login({
      email: '[email protected]',
      password: '123456',
    });

    expect(authStore.isAuthenticated).toBe(true);
    expect(authStore.user?.email).toBe('[email protected]');
  });

  it('should check permissions', () => {
    expect(authStore.hasPermission('users:view')).toBe(true);
    expect(authStore.hasPermission('invalid')).toBe(false);
  });
});

Configuration

SvelteKit Configuration

js
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

export default {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter(),
    alias: {
      $components: 'src/lib/components',
      $stores: 'src/lib/stores',
      $utils: 'src/lib/utils',
    },
  },
};

Vite Configuration

ts
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [sveltekit()],
  test: {
    include: ['src/**/*.{test,spec}.{js,ts}'],
    environment: 'jsdom',
  },
});

CI/CD

Complete GitHub Actions CI workflow configured:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm format:check

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

Reactive Collections (SvelteSet/SvelteMap)

svelte
<script lang="ts">
  import { SvelteSet, SvelteMap } from 'svelte/reactivity';

  // Reactive Set
  let selectedIds = new SvelteSet<string>();

  function toggleSelection(id: string) {
    if (selectedIds.has(id)) {
      selectedIds.delete(id);
    } else {
      selectedIds.add(id);
    }
  }

  // Reactive Map
  let itemStatus = new SvelteMap<string, 'pending' | 'done'>();

  function markDone(id: string) {
    itemStatus.set(id, 'done');
  }
</script>

<p>Selected: {selectedIds.size}</p>

Server Hooks

ts
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  const token = event.cookies.get('token');

  if (token) {
    // Validate token and set user info
    event.locals.user = await validateToken(token);
  }

  // Route protection
  if (event.url.pathname.startsWith('/dashboard')) {
    if (!event.locals.user) {
      return new Response(null, {
        status: 302,
        headers: { Location: '/auth/login' },
      });
    }
  }

  return resolve(event);
};

Performance Optimization

Lazy Load Components

svelte
<script lang="ts">
  const HeavyComponent = $lazy(() => import('$lib/components/Heavy.svelte'));
</script>

{#await HeavyComponent}
  <div>Loading...</div>
{:then component}
  <svelte:component this={component} />
{/await}

Preloading

svelte
<script lang="ts">
  import { preloadData } from '$app/navigation';

  function handleMouseEnter() {
    preloadData('/dashboard/analytics');
  }
</script>

<a href="/dashboard/analytics" onmouseenter={handleMouseEnter}>
  Analytics
</a>

Image Optimization

svelte
<script lang="ts">
  import { onMount } from 'svelte';

  let visible = $state(false);

  onMount(() => {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        visible = true;
        observer.disconnect();
      }
    });

    observer.observe(element);
  });
</script>

{#if visible}
  <img src="/large-image.jpg" alt="Optimized image" />
{:else}
  <div class="placeholder" />
{/if}

FAQ

Q: How to use TanStack Query in SvelteKit?

A: SvelteKit recommends using built-in Load functions for data loading, but you can also combine TanStack Query:

svelte
<script lang="ts">
  import { createQuery } from '@tanstack/svelte-query';

  const query = createQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
  });
</script>

{#if $query.isLoading}
  <p>Loading...</p>
{:else if $query.error}
  <p>Error: {$query.error.message}</p>
{:else if $query.data}
  <ul>
    {#each $query.data as user}
      <li>{user.name}</li>
    {/each}
  </ul>
{/if}

Q: How to implement form validation?

A: Recommended using Superforms + Zod:

ts
// routes/users/create/+page.server.ts
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

export const actions = {
  default: async ({ request }) => {
    const form = await superValidate(request, zod(schema));

    if (!form.valid) {
      return fail(400, { form });
    }

    // Process form data
    return { form };
  },
};

Q: How to deploy to Vercel?

A: Switch to Vercel adapter:

bash
pnpm add -D @sveltejs/adapter-vercel
js
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';

export default {
  kit: {
    adapter: adapter(),
  },
};

Comparison with Other Versions

Feature SvelteKit Next.js Vue
SSR/SSG ✅ (Nuxt)
State Management Svelte 5 Runes Zustand Pinia
Routing File-based App Router Vue Router
Build Tool Vite Turbopack Vite
Runtime No Virtual DOM Virtual DOM Virtual DOM
Forms Superforms React Hook Form VeeValidate
Component Library shadcn-svelte shadcn/ui shadcn-vue
]]>
<![CDATA[Web Components Library]]> https://halolight.docs.h7ml.cn/en/guide/ui https://halolight.docs.h7ml.cn/en/guide/ui Fri, 19 Dec 2025 04:56:41 GMT Web Components Library

HaloLight UI is a cross-framework Web Components library built with Stencil, featuring built-in Tailwind themes and OKLch color system.

GitHub: https://github.com/halolight/halolight-ui

npm: @halolight/ui

Tech Stack

Technology Version Description
Stencil 4.22.x Web Components compiler
TypeScript 5.x Type system
Tailwind CSS 4.0 Utility-first CSS (build time)
Jest 29.x Unit testing
Puppeteer 23.x E2E testing

Core Features

  • Cross-framework Compatible: Based on Web Components standard, works with React, Vue, Angular, Svelte, and any framework
  • OKLch Color Space: Uses perceptually uniform OKLch color space for theming
  • Shadow DOM Encapsulation: Complete style isolation, avoiding conflicts
  • TypeScript: Full type definition support
  • Accessibility: WCAG 2.1 AA compliant
  • Lightweight: On-demand loading, tree-shakeable

Component List

Component Tag Description
Button <hl-button> Button with multiple variants and sizes
Input <hl-input> Input with validation and error states
Select <hl-select> Dropdown selector
Modal <hl-modal> Modal dialog
Card <hl-card> Card container
Table <hl-table> Data table with sorting and selection
Form <hl-form> Form container with validation

Directory Structure

halolight-ui/
├── src/
│   ├── components/                # Component source
│   │   ├── hl-button/            # Button component
│   │   │   ├── hl-button.tsx     # Component logic
│   │   │   ├── hl-button.css     # Component styles
│   │   │   ├── readme.md         # Component docs
│   │   │   └── test/             # Component tests
│   │   ├── hl-input/             # Input
│   │   ├── hl-select/            # Select
│   │   ├── hl-modal/             # Modal
│   │   ├── hl-table/             # Table
│   │   ├── hl-card/              # Card
│   │   └── hl-form/              # Form
│   ├── global/                   # Global styles
│   │   └── global.css            # OKLch theme variables
│   ├── utils/                    # Utilities
│   ├── themes/                   # Theme configuration
│   ├── components.d.ts           # Auto-generated type definitions
│   └── index.ts                  # Export entry
├── dist/                         # Build output
├── loader/                       # Runtime loader
├── www/                          # Development preview site
├── stencil.config.ts             # Stencil configuration
├── tailwind.config.js            # Tailwind configuration
├── tsconfig.json
└── package.json

Quick Start

Installation

bash
npm install @halolight/ui
# or
pnpm add @halolight/ui

Define Custom Elements

Call once in any framework:

typescript
import { defineCustomElements } from '@halolight/ui/loader';
defineCustomElements();

Vanilla JavaScript

html
<!DOCTYPE html>
<html>
<head>
  <script type="module">
    import { defineCustomElements } from 'https://unpkg.com/@halolight/ui/loader/index.js';
    defineCustomElements();
  </script>
</head>
<body>
  <hl-button variant="primary">Click Me</hl-button>
</body>
</html>

React

tsx
import { defineCustomElements } from '@halolight/ui/loader';

// Call once at app entry
defineCustomElements();

function App() {
  return (
    <div>
      <hl-button variant="primary">Click Me</hl-button>
    </div>
  );
}

TypeScript Support:

tsx
// vite-env.d.ts
/// <reference types="@halolight/ui/dist/types/components" />

import { JSX as HaloLightJSX } from '@halolight/ui/dist/types/components';

declare global {
  namespace JSX {
    interface IntrinsicElements extends HaloLightJSX.IntrinsicElements {}
  }
}

Vue 3

vue
<template>
  <hl-button variant="primary" @hl-click="handleClick">
    Click Me
  </hl-button>
</template>

<script setup lang="ts">
import { defineCustomElements } from '@halolight/ui/loader';

defineCustomElements();

const handleClick = () => {
  console.log('Button clicked!');
};
</script>

Angular

typescript
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { defineCustomElements } from '@halolight/ui/loader';

defineCustomElements();

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
html
<!-- app.component.html -->
<hl-button variant="primary" (hlClick)="handleClick()">
  Click Me
</hl-button>

Component API

hl-button

Property Type Default Description
variant 'primary' | 'secondary' | 'outline' | 'ghost' 'primary' Button variant
size 'sm' | 'md' | 'lg' 'md' Button size
disabled boolean false Disabled state
loading boolean false Loading state

Events: hlClick

hl-input

Property Type Default Description
type 'text' | 'password' | 'email' | 'number' 'text' Input type
placeholder string '' Placeholder text
disabled boolean false Disabled state
error string '' Error message

Events: hlChange, hlInput, hlFocus, hlBlur

Theme Customization

Dark Mode

Add dark class to parent element to enable dark theme:

html
<div class="dark">
  <hl-button variant="primary">Dark Mode Button</hl-button>
</div>
javascript
// Dynamic toggle
document.body.classList.toggle('dark');

CSS Variable Override

Define theme variables using OKLch color space:

css
:root {
  /* Primary color - using OKLch */
  --hl-color-primary: oklch(0.65 0.15 250);
  --hl-color-primary-hover: oklch(0.55 0.18 250);
  --hl-color-primary-light: oklch(0.75 0.12 250);

  /* Semantic colors */
  --hl-color-success: oklch(0.7 0.18 145);
  --hl-color-danger: oklch(0.63 0.26 25);
  --hl-color-warning: oklch(0.78 0.16 75);
  --hl-color-info: oklch(0.73 0.15 195);

  /* Neutral colors */
  --hl-bg-base: oklch(1 0 0);
  --hl-text-primary: oklch(0.2 0 0);
  --hl-border-color: oklch(0.9 0 0);

  /* Border radius and spacing */
  --hl-border-radius: 0.5rem;
  --hl-spacing-md: 1rem;
}

/* Dark mode */
.dark {
  --hl-color-primary: oklch(0.7 0.15 250);
  --hl-bg-base: oklch(0.15 0 0);
  --hl-text-primary: oklch(0.98 0 0);
}

OKLch Color Space

OKLch is a perceptually uniform color space:

  • L (Lightness): 0 (black) ~ 1 (white)
  • C (Chroma): 0 (gray) ~ 0.4 (vivid)
  • H (Hue): 0° ~ 360° (hue wheel)
css
/* oklch(lightness chroma hue / alpha) */
oklch(0.65 0.15 250)        /* Blue */
oklch(0.7 0.18 145)         /* Green */
oklch(0.63 0.26 25 / 0.8)   /* Semi-transparent red */

Development Guide

Common Commands

bash
# Install dependencies
npm install

# Start development server
npm start

# Production build
npm run build

# Run tests
npm test

# Generate new component
npm run generate

Creating New Components

  1. Use Stencil CLI to generate skeleton:
bash
npm run generate
# Enter component name (without hl- prefix)
  1. Implement component logic:
tsx
import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';

@Component({
  tag: 'hl-example',
  styleUrl: 'hl-example.css',
  shadow: true,
})
export class HlExample {
  @Prop() size: 'sm' | 'md' | 'lg' = 'md';
  @Event() hlChange: EventEmitter<string>;

  render() {
    return (
      <Host>
        <div class={`hl-example hl-example--${this.size}`}>
          <slot></slot>
        </div>
      </Host>
    );
  }
}

Naming Conventions

  • Component tags: hl-{component-name}
  • CSS classes: BEM style .hl-button--primary
  • Event names: hl{EventName} (camelCase)

Browser Compatibility

OKLch color space support:

  • Chrome 111+
  • Safari 15.4+
  • Firefox 113+

For older browsers, use PostCSS plugins for fallback conversion.

]]>
<![CDATA[Vercel Deployment]]> https://halolight.docs.h7ml.cn/en/guide/vercel https://halolight.docs.h7ml.cn/en/guide/vercel Fri, 19 Dec 2025 04:56:41 GMT Vercel Deployment

HaloLight Vercel deployment version, optimized for Vercel platform with the best Next.js deployment experience.

Features

  • Vercel Native - Official Next.js deployment platform
  • Edge Functions - Edge computing support
  • 🌐 Global Edge Network - Lightning-fast access experience
  • 🔄 Preview Deployments - Automatic PR preview environments
  • 📊 Analytics - Built-in analytics features
  • 🔐 Environment Variables - Secure environment variable management

Quick Start

Method 1: One-Click Deploy

Deploy with Vercel

Method 2: Manual Deploy

bash
# Clone repository
git clone https://github.com/halolight/halolight-vercel.git
cd halolight-vercel

# Install dependencies
pnpm install

# Local development
pnpm dev

# Build
pnpm build

Configuration File

vercel.json

json
{
  "buildCommand": "pnpm build",
  "outputDirectory": ".next",
  "framework": "nextjs",
  "regions": ["hkg1", "sin1"],
  "functions": {
    "api/**/*.ts": {
      "memory": 1024,
      "maxDuration": 10
    }
  }
}

Environment Variables

Set in Vercel dashboard:

bash
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_USE_MOCK=false
DATABASE_URL=postgresql://...

Edge Functions

typescript
// app/api/edge/route.ts
import { NextRequest } from 'next/server'

export const runtime = 'edge'

export async function GET(request: NextRequest) {
  return new Response(JSON.stringify({ message: 'Hello from Edge!' }), {
    headers: { 'content-type': 'application/json' },
  })
}
]]>
<![CDATA[Vue Version]]> https://halolight.docs.h7ml.cn/en/guide/vue https://halolight.docs.h7ml.cn/en/guide/vue Fri, 19 Dec 2025 04:56:41 GMT Vue Version

HaloLight Vue version is built on Vue 3.5 + Vite 7, using Composition API + TypeScript.

Live Preview: https://halolight-vue.h7ml.cn/

GitHub: https://github.com/halolight/halolight-vue

Features

  • 🏗️ Composition API - Vue 3.5 Composition API for flexible logic reuse
  • Vite 7 + Rolldown - Lightning-fast HMR, Rust-powered build tool
  • 🎨 Theme System - 11 skins, light/dark mode, View Transitions
  • 🔐 Authentication - Complete login/register/password recovery flow
  • 📊 Dashboard - Data visualization and business management
  • 🛡️ Permission Control - Fine-grained RBAC permission management
  • 📑 Multi-Tab - Tab bar management
  • Command Palette - Keyboard shortcuts navigation

Tech Stack

Technology Version Description
Vue 3.5.x Progressive framework
Vite 7.x (Rolldown) Build tool
TypeScript 5.x Type safety
Vue Router 4.x Routing
Pinia 2.x State management
TanStack Query 5.x Server state
VeeValidate 4.x Form validation
Zod 3.x Data validation
Tailwind CSS 4.x Atomic CSS
shadcn-vue latest UI component library
grid-layout-plus 1.x Drag-and-drop layout
ECharts 5.x Chart visualization
Mock.js 1.x Data mocking

Core Features

  • Configurable Dashboard - 9 widgets, drag-and-drop layout, responsive adaptation
  • Multi-Tab Navigation - Browser-style tabs, context menu, state caching
  • Permission System - RBAC permission control, route guards, permission components
  • Theme System - 11 skins, light/dark mode, View Transitions
  • Multi-Account Switching - Quick account switching, remember login state
  • Command Palette - Keyboard shortcuts (⌘K), global search
  • Real-time Notifications - WebSocket push, notification center

Directory Structure

halolight-vue/
├── src/
│   ├── views/               # Page views
│   │   ├── (auth)/         # Auth pages
│   │   └── (dashboard)/    # Dashboard pages
│   ├── components/         # Components
│   │   ├── ui/             # Base UI components
│   │   ├── layout/         # Layout components
│   │   └── dashboard/      # Dashboard components
│   ├── composables/        # Composable functions
│   ├── stores/             # Pinia state management
│   ├── lib/                # Utility library
│   ├── mocks/              # Mock data
│   └── types/              # Type definitions
├── public/                 # Static assets
├── vite.config.ts
└── package.json

Quick Start

Environment Requirements

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation

bash
git clone https://github.com/halolight/halolight-vue.git
cd halolight-vue
pnpm install

Environment Variables

bash
cp .env.example .env.local
env
# .env.local
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Start Development

bash
pnpm dev

Visit http://localhost:5173

Build for Production

bash
pnpm build
pnpm preview

Demo Account

Role Email Password
Admin [email protected] 123456
User [email protected] 123456

Core Functionality

State Management (Pinia)

ts
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)

  // Getters
  const isAuthenticated = computed(() => !!token.value && !!user.value)
  const permissions = computed(() => user.value?.permissions || [])

  // Actions
  async function login(credentials: LoginCredentials) {
    const response = await authService.login(credentials)
    user.value = response.user
    token.value = response.token
  }

  function logout() {
    user.value = null
    token.value = null
  }

  function hasPermission(permission: string): boolean {
    return permissions.value.some(p =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  }

  return {
    user,
    token,
    isAuthenticated,
    permissions,
    login,
    logout,
    hasPermission,
  }
}, {
  persist: {
    paths: ['token', 'user'],
  },
})

Data Fetching (TanStack Query)

ts
// composables/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
import { userService } from '@/services/users'

export function useUsers(params?: Ref<UserQueryParams>) {
  return useQuery({
    queryKey: ['users', params],
    queryFn: () => userService.getList(unref(params)),
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: userService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}

Permission Control

ts
// composables/usePermission.ts
import { useAuthStore } from '@/stores/auth'

export function usePermission() {
  const authStore = useAuthStore()

  function hasPermission(permission: string): boolean {
    return authStore.hasPermission(permission)
  }

  function hasAnyPermission(permissions: string[]): boolean {
    return permissions.some(p => hasPermission(p))
  }

  function hasAllPermissions(permissions: string[]): boolean {
    return permissions.every(p => hasPermission(p))
  }

  return {
    hasPermission,
    hasAnyPermission,
    hasAllPermissions,
  }
}
ts
// directives/permission.ts
import { useAuthStore } from '@/stores/auth'

export const vPermission = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const authStore = useAuthStore()
    if (!authStore.hasPermission(binding.value)) {
      el.parentNode?.removeChild(el)
    }
  },
}

// Register directive
app.directive('permission', vPermission)
vue
<!-- Use permission directive -->
<button v-permission="'users:delete'">Delete</button>

<!-- Use permission component -->
<PermissionGuard permission="users:delete">
  <DeleteButton />
  <template #fallback>
    <span>No permission</span>
  </template>
</PermissionGuard>

Draggable Dashboard

vue
<!-- components/dashboard/DashboardGrid.vue -->
<script setup lang="ts">
import { GridLayout, GridItem } from 'grid-layout-plus'
import { useDashboardStore } from '@/stores/dashboard'

const dashboardStore = useDashboardStore()
const { layout, isEditing } = storeToRefs(dashboardStore)
</script>

<template>
  <GridLayout
    v-model:layout="layout"
    :col-num="12"
    :row-height="80"
    :is-draggable="isEditing"
    :is-resizable="isEditing"
    :margin="[16, 16]"
  >
    <GridItem
      v-for="item in layout"
      :key="item.i"
      :x="item.x"
      :y="item.y"
      :w="item.w"
      :h="item.h"
    >
      <WidgetWrapper :widget="getWidget(item.i)" />
    </GridItem>
  </GridLayout>
</template>

Theme System

Skin Presets

Supports 11 preset skins, switchable via quick settings panel:

Skin Primary Color CSS Variable
Default Purple --primary: 51.1% 0.262 276.97
Blue Blue --primary: 54.8% 0.243 264.05
Emerald Emerald --primary: 64.6% 0.178 142.49
Orange Orange --primary: 69.7% 0.196 49.27
Rose Rose --primary: 63.4% 0.243 357.61
Amber Amber --primary: 79.1% 0.177 77.54
Cyan Cyan --primary: 74.4% 0.167 197.13
Violet Violet --primary: 57.2% 0.267 285.75
Lime Lime --primary: 78.8% 0.184 127.38
Pink Pink --primary: 70.9% 0.254 347.58
Teal Teal --primary: 67.8% 0.157 181.02

CSS Variables (OKLch)

css
/* Example variable definitions */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 97.3% 0.006 285.75;
  --secondary-foreground: 17.9% 0.018 285.75;
  --muted: 97.3% 0.006 285.75;
  --muted-foreground: 49.5% 0.023 285.75;
  --accent: 97.3% 0.006 285.75;
  --accent-foreground: 17.9% 0.018 285.75;
  --destructive: 59.9% 0.24 29.23;
  --destructive-foreground: 98.3% 0.002 285.75;
  --border: 91.9% 0.010 285.75;
  --input: 91.9% 0.010 285.75;
  --ring: 51.1% 0.262 276.97;
  --radius: 0.5rem;
}

Theme Toggle

ts
// composables/useTheme.ts
import { ref, computed, watch } from 'vue'

export function useTheme() {
  const theme = ref<'light' | 'dark' | 'system'>('system')
  const skin = ref<SkinPreset>('default')

  const actualTheme = computed(() => {
    if (theme.value === 'system') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light'
    }
    return theme.value
  })

  async function toggleTheme(event?: MouseEvent) {
    const newTheme = actualTheme.value === 'dark' ? 'light' : 'dark'

    // View Transitions API
    if (!document.startViewTransition) {
      theme.value = newTheme
      return
    }

    await document.startViewTransition(() => {
      theme.value = newTheme
    }).ready

    // Circular reveal animation
    if (event) {
      const { clientX, clientY } = event
      const radius = Math.hypot(
        Math.max(clientX, window.innerWidth - clientX),
        Math.max(clientY, window.innerHeight - clientY)
      )

      document.documentElement.animate(
        {
          clipPath: [
            `circle(0px at ${clientX}px ${clientY}px)`,
            `circle(${radius}px at ${clientX}px ${clientY}px)`,
          ],
        },
        {
          duration: 500,
          easing: 'ease-in-out',
          pseudoElement: '::view-transition-new(root)',
        }
      )
    }
  }

  watch([theme, skin], () => {
    document.documentElement.classList.remove('light', 'dark')
    document.documentElement.classList.add(actualTheme.value)
    document.documentElement.setAttribute('data-skin', skin.value)
  }, { immediate: true })

  return { theme, skin, actualTheme, toggleTheme }
}

Page Routes

Path Page Permission
/ Redirect to /dashboard -
/login Login Public
/register Register Public
/forgot-password Forgot password Public
/reset-password Reset password Public
/dashboard Dashboard dashboard:view
/users User management users:view
/analytics Analytics analytics:view
/calendar Calendar calendar:view
/documents Documents documents:view
/files File storage files:view
/messages Messages messages:view
/notifications Notifications notifications:view
/settings System settings settings:view
/profile User profile settings:view

Environment Variables

Configuration Example

env
# .env.local
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Variable Description

Variable Name Description Default Value
VITE_API_URL API base path /api
VITE_USE_MOCK Whether to use Mock data true
VITE_DEMO_EMAIL Demo account email [email protected]
VITE_DEMO_PASSWORD Demo account password 123456
VITE_SHOW_DEMO_HINT Whether to show demo hint false
VITE_APP_TITLE Application title Admin Pro
VITE_BRAND_NAME Brand name Halolight

Usage

ts
// Use in code
const apiUrl = import.meta.env.VITE_API_URL
const useMock = import.meta.env.VITE_USE_MOCK === 'true'
const appTitle = import.meta.env.VITE_APP_TITLE

Common Commands

bash
pnpm dev            # Start development server
pnpm build          # Production build
pnpm preview        # Preview production build
pnpm lint           # Code linting
pnpm lint:fix       # Auto-fix
pnpm type-check     # Type checking
pnpm test           # Run tests
pnpm test:coverage  # Test coverage

Testing

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Run once
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI

Test Example

ts
// tests/components/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '@/components/ui/Button.vue'

describe('Button', () => {
  it('renders properly', () => {
    const wrapper = mount(Button, {
      props: { variant: 'default' },
      slots: { default: 'Click me' }
    })
    expect(wrapper.text()).toContain('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button)
    await wrapper.trigger('click')
    expect(wrapper.emitted()).toHaveProperty('click')
  })
})

Configuration

Vite Configuration

ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    open: true,
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['@tanstack/vue-query'],
        },
      },
    },
  },
})

Deployment

Deploy with Vercel

Docker

bash
docker build -t halolight-vue .
docker run -p 3000:3000 halolight-vue

Other Platforms

CI/CD

The project is configured with a complete GitHub Actions CI workflow:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features

ECharts Integration

vue
<script setup lang="ts">
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart, BarChart, PieChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import { useTheme } from '@/composables/useTheme'

use([CanvasRenderer, LineChart, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent])

const { actualTheme } = useTheme()

const option = computed(() => ({
  backgroundColor: 'transparent',
  textStyle: {
    color: actualTheme.value === 'dark' ? '#e5e5e5' : '#333',
  },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [{
    data: [820, 932, 901, 934, 1290, 1330, 1320],
    type: 'line'
  }]
}))
</script>

<template>
  <VChart :option="option" autoresize />
</template>

Route Guards

ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  history: createWebHistory(),
  routes: [...routes]
})

router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()

  // Pages requiring authentication
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next({ name: 'login', query: { redirect: to.fullPath } })
    return
  }

  // Permission check
  if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) {
    next({ name: '403' })
    return
  }

  next()
})

export default router

Performance Optimization

Image Optimization

vue
<script setup lang="ts">
const imageSrc = computed(() => {
  const { width } = useWindowSize()
  if (width.value < 768) return '/images/mobile.webp'
  if (width.value < 1024) return '/images/tablet.webp'
  return '/images/desktop.webp'
})
</script>

<template>
  <img
    :src="imageSrc"
    loading="lazy"
    decoding="async"
    alt="Responsive image"
  >
</template>

Lazy Load Components

ts
// router/routes.ts
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/users',
    component: () => import('@/views/Users.vue'),
    meta: { requiresAuth: true, permission: 'users:view' }
  },
]

Preloading

vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()

onMounted(() => {
  // Preload commonly used routes
  router.resolve({ name: 'users' })
  router.resolve({ name: 'settings' })
})
</script>

FAQ

Q: How to switch themes?

A: Use the useTheme composable:

vue
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme'

const { theme, toggleTheme, skin } = useTheme()

// Toggle light/dark theme
function handleToggle(event: MouseEvent) {
  toggleTheme(event)
}

// Change skin
function changeSkin(newSkin: SkinPreset) {
  skin.value = newSkin
}
</script>

<template>
  <button @click="handleToggle">Toggle Theme</button>
  <select v-model="skin">
    <option value="default">Default</option>
    <option value="blue">Blue</option>
    <option value="emerald">Emerald</option>
  </select>
</template>

Q: How to add new permissions?

A: Add permission strings in the authentication response:

ts
// types/auth.ts
interface User {
  id: string
  name: string
  email: string
  permissions: string[] // ['users:*', 'posts:view', 'posts:create']
}

// Using wildcards
// 'users:*' - All permissions for user module
// '*' - All permissions
// 'users:view' - Specific permission

Q: How to customize dashboard layout?

A: Manage layout through Dashboard Store:

ts
// stores/dashboard.ts
import { defineStore } from 'pinia'

export const useDashboardStore = defineStore('dashboard', () => {
  const layout = ref([
    { i: 'widget-1', x: 0, y: 0, w: 6, h: 4 },
    { i: 'widget-2', x: 6, y: 0, w: 6, h: 4 },
  ])

  function saveLayout(newLayout: Layout[]) {
    layout.value = newLayout
    // Save to server
  }

  return { layout, saveLayout }
})

Comparison with Other Versions

Feature Vue Next.js Angular
SSR/SSG ❌ (Requires Nuxt) ✅ (Requires Angular Universal)
State Management Pinia Zustand RxJS/Signals
Routing Vue Router App Router Angular Router
Build Tool Vite Next.js Angular CLI
Learning Curve Medium Medium High
Ecosystem Rich Rich Enterprise
]]>
<![CDATA[Web3 Wallet Integration]]> https://halolight.docs.h7ml.cn/en/guide/web3 https://halolight.docs.h7ml.cn/en/guide/web3 Fri, 19 Dec 2025 04:56:41 GMT Web3 Wallet Integration

HaloLight Web3 provides unified access to EVM + Solana + IPFS, with Core/React/Vue packages.

GitHub: https://github.com/halolight/halolight-web3

npm:

  • @halolight/web3-core - Core functionality (framework-agnostic)
  • @halolight/web3-react - React components and hooks
  • @halolight/web3-vue - Vue 3 components and composables

Tech Stack

Technology Version Description
wagmi ^2.12.x EVM wallet and contract calls
viem ^2.21.x EVM RPC & ABI utilities
@solana/web3.js ^1.95.x Solana JavaScript SDK
@solana/wallet-adapter latest Solana wallet adapters
@web3-storage/w3up-client ^16.0.x IPFS/web3.storage client
siwe ^2.3.x Sign-In with Ethereum
TypeScript ^5.7.x Type system
Turborepo ^2.3.x Monorepo build tool

Core Features

EVM (Ethereum/Polygon/BSC)

  • Wallet connection (MetaMask, WalletConnect, etc.)
  • Sign-In with Ethereum (SIWE)
  • ERC-20 token interactions
  • ERC-721 NFT support
  • Smart contract calls (read/write)
  • Gas estimation & management
  • Multi-chain support

Solana

  • Wallet adapter integration (Phantom, Solflare, etc.)
  • Signature-based authentication
  • SOL transfers
  • SPL Token support
  • Transaction management
  • Devnet/Testnet/Mainnet support

IPFS

  • File upload (web3.storage)
  • JSON metadata upload
  • NFT metadata handling
  • CID validation & conversion
  • Gateway URL management
  • Batch upload with progress

Directory Structure

halolight-web3/
├── packages/
│   ├── core/                      # Core package (@halolight/web3-core)
│   │   ├── src/
│   │   │   ├── evm/               # EVM/Ethereum functionality
│   │   │   │   ├── wallet.ts      # Wagmi configuration
│   │   │   │   ├── chains.ts      # Chain configuration
│   │   │   │   ├── siwe.ts        # Sign-In with Ethereum
│   │   │   │   └── contracts.ts   # Smart contract interaction
│   │   │   ├── solana/            # Solana functionality
│   │   │   │   ├── wallet.ts      # Wallet adapters
│   │   │   │   └── auth.ts        # Signature authentication
│   │   │   ├── storage/           # Storage functionality
│   │   │   │   └── ipfs.ts        # IPFS upload
│   │   │   ├── types/             # TypeScript types
│   │   │   └── utils/             # Utilities
│   │   └── package.json
│   │
│   ├── react/                     # React package (@halolight/web3-react)
│   │   ├── src/
│   │   │   ├── providers/
│   │   │   │   └── Web3Provider.tsx
│   │   │   ├── components/
│   │   │   │   ├── WalletButton.tsx
│   │   │   │   ├── TokenBalance.tsx
│   │   │   │   ├── NftGallery.tsx
│   │   │   │   └── ContractCall.tsx
│   │   │   └── hooks/
│   │   └── package.json
│   │
│   └── vue/                       # Vue package (@halolight/web3-vue)
│       ├── src/
│       │   ├── composables/
│       │   │   └── useWallet.ts
│       │   └── components/
│       │       ├── WalletButton.vue
│       │       └── TokenBalance.vue
│       └── package.json

├── .env.example
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

Quick Start

Installation

bash
# Using pnpm (recommended)
pnpm add @halolight/web3-core @halolight/web3-react
# or for Vue
pnpm add @halolight/web3-core @halolight/web3-vue

# Using npm
npm install @halolight/web3-core @halolight/web3-react

# Using yarn
yarn add @halolight/web3-core @halolight/web3-react

Environment Variables

Copy .env.example to .env and configure:

env
# EVM - RPC nodes
NEXT_PUBLIC_ALCHEMY_API_KEY=your_alchemy_key
NEXT_PUBLIC_INFURA_API_KEY=your_infura_key

# EVM - WalletConnect
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_project_id

# Solana
NEXT_PUBLIC_SOLANA_RPC_URL=https://api.mainnet-beta.solana.com

# IPFS/web3.storage
NEXT_PUBLIC_WEB3_STORAGE_TOKEN=your_web3_storage_token

React Example

tsx
import { Web3Provider, WalletButton, TokenBalance } from '@halolight/web3-react';

function App() {
  return (
    <Web3Provider
      evmNetwork="mainnet"
      solanaCluster="mainnet-beta"
      enableEvm={true}
      enableSolana={true}
    >
      <div>
        <WalletButton chain="evm" />
        <WalletButton chain="solana" />

        <TokenBalance
          chain="evm"
          tokenAddress="0x..." // USDC on Ethereum
          showSymbol
        />
      </div>
    </Web3Provider>
  );
}

Vue Example

vue
<script setup lang="ts">
import { WalletButton, TokenBalance, useEvmWallet } from '@halolight/web3-vue';
import { WagmiPlugin } from '@wagmi/vue';
import { createWagmiConfig } from '@halolight/web3-core';

const config = createWagmiConfig('mainnet');
</script>

<template>
  <div>
    <WalletButton />
    <TokenBalance
      token-address="0x..."
      :show-symbol="true"
    />
  </div>
</template>

Core Library (Framework-agnostic)

typescript
import {
  createWagmiConfig,
  getTokenBalance,
  uploadToIpfs,
  authenticateWithSiwe,
} from '@halolight/web3-core';

// EVM: Get token balance
const balance = await getTokenBalance(client, tokenAddress, walletAddress);

// IPFS: Upload file
const result = await uploadToIpfs(file);
console.log(result.cid, result.gateway);

// SIWE: Authenticate user
const auth = await authenticateWithSiwe({
  domain: 'example.com',
  address: walletAddress,
  chainId: 1,
  signMessage: async (msg) => wallet.signMessage(msg),
});

React Components

Web3Provider

Unified EVM + Solana Provider:

tsx
<Web3Provider
  evmNetwork="mainnet"           // or "testnet" | "development"
  solanaCluster="mainnet-beta"   // or "devnet" | "testnet"
  enableEvm={true}               // Enable EVM support
  enableSolana={true}            // Enable Solana support
>
  {children}
</Web3Provider>

WalletButton

tsx
import { WalletButton, DefaultWalletButton } from '@halolight/web3-react';

// EVM wallet
<WalletButton
  chain="evm"
  connectText="Connect Ethereum"
  className="custom-class"
/>

// Solana wallet
<WalletButton
  chain="solana"
  className="custom-class"
/>

// With default styling
<DefaultWalletButton chain="evm" />

TokenBalance

tsx
import { TokenBalance } from '@halolight/web3-react';

<TokenBalance
  chain="evm"
  tokenAddress="0x..." // ERC-20 contract address
  showSymbol
  decimals={4}
  loadingComponent={<Spinner />}
  errorComponent={(error) => <div>Error: {error}</div>}
/>

NftGallery

tsx
import { NftGallery } from '@halolight/web3-react';

<NftGallery
  contractAddress="0x..." // ERC-721 contract
  maxDisplay={50}
  columns={3}
  renderNft={(nft) => (
    <div>
      <img src={nft.image} alt={nft.name} />
      <h3>{nft.name}</h3>
    </div>
  )}
/>

ContractCall

tsx
import { ContractCallButton, useContractCall } from '@halolight/web3-react';

// Using component
<ContractCallButton
  contract={{
    address: '0x...',
    abi: MyABI,
    functionName: 'mint',
    args: [tokenId],
  }}
  type="write"
  buttonText="Mint NFT"
  onSuccess={(data) => console.log('Minted!', data)}
/>

// Using hook
const { call, loading, error, txHash } = useContractCall(
  {
    address: '0x...',
    abi: MyABI,
    functionName: 'balanceOf',
    args: [address],
  },
  'read'
);

Vue Composables

useEvmWallet

typescript
import { useEvmWallet } from '@halolight/web3-vue';

const { address, isConnected, connect, disconnect } = useEvmWallet();

useTokenBalance

typescript
import { useTokenBalance } from '@halolight/web3-vue';

const { balance, loading, error, refresh } = useTokenBalance('0x...');

useNativeBalance

typescript
import { useNativeBalance } from '@halolight/web3-vue';

const { balance, formatted, loading } = useNativeBalance();

Core API

EVM Wallet

typescript
import {
  createWagmiConfig,
  formatAddress,
  isValidAddress,
} from '@halolight/web3-core';

// Create wagmi configuration
const config = createWagmiConfig('mainnet');

// Format address
const short = formatAddress('0x1234...5678'); // "0x1234...5678"

// Validate address
const valid = isValidAddress('0x...'); // true/false

Smart Contracts

typescript
import {
  readContract,
  writeContract,
  getTokenBalance,
  transferToken,
  ERC20_ABI,
} from '@halolight/web3-core';

// Read contract
const name = await readContract(publicClient, {
  address: '0x...',
  abi: ERC20_ABI,
  functionName: 'name',
});

// Write contract
const hash = await writeContract(walletClient, publicClient, {
  address: '0x...',
  abi: ERC20_ABI,
  functionName: 'transfer',
  args: [toAddress, amount],
});

SIWE Authentication

typescript
import {
  createSiweMessage,
  formatSiweMessage,
  verifySiweMessage,
  authenticateWithSiwe,
} from '@halolight/web3-core';

// Complete authentication flow
const result = await authenticateWithSiwe({
  domain: 'example.com',
  address: walletAddress,
  chainId: 1,
  signMessage: async (message) => {
    return await wallet.signMessage(message);
  },
});

if (result.success) {
  console.log('Authenticated:', result.address);
}

Solana Wallet

typescript
import {
  createSolanaConnection,
  getSolBalance,
  transferSol,
} from '@halolight/web3-core';

// Create connection
const connection = createSolanaConnection('mainnet-beta');

// Get SOL balance
const balance = await getSolBalance(connection, walletAddress);

// Transfer SOL
const signature = await transferSol(
  connection,
  wallet,
  toAddress,
  1.0 // 1 SOL
);

IPFS Storage

typescript
import {
  uploadToIpfs,
  uploadJsonToIpfs,
  fetchJsonFromIpfs,
  ipfsToHttp,
} from '@halolight/web3-core';

// Upload file
const result = await uploadToIpfs(file);
console.log(result.cid); // "QmXxx..."
console.log(result.gateway); // "https://w3s.link/ipfs/QmXxx..."

// Upload JSON
const metadata = await uploadJsonToIpfs({
  name: 'My NFT',
  description: 'Cool NFT',
  image: 'ipfs://QmYyy...',
});

// Fetch JSON
const data = await fetchJsonFromIpfs('QmXxx...');

// Convert IPFS URL to HTTP
const url = ipfsToHttp('ipfs://QmXxx...'); // "https://w3s.link/ipfs/QmXxx..."

Development Guide

Common Commands

bash
# Install dependencies
pnpm install

# Build all packages
pnpm build

# Development mode (watch)
pnpm dev

# Run tests
pnpm test

# Type check
pnpm type-check

# Lint
pnpm lint

# Clean build artifacts
pnpm clean

Single Package Operations

bash
# In packages/core directory
pnpm build
pnpm dev
pnpm test

# Or from root
pnpm --filter @halolight/web3-core build
pnpm --filter @halolight/web3-react dev

Best Practices

RPC Configuration

  • Configure RPC keys properly (Alchemy/Infura) to avoid rate limiting
  • Use RPC fallback mechanism for better availability

Error Handling

  • Handle on-chain errors and rejection states
  • Provide clear UI feedback

Security

  • Never expose private keys/sensitive tokens in production
  • Use backend signing and proxy forwarding
  • Validate all user inputs

References

]]>
<![CDATA[签到定时任务平台]]> https://halolight.docs.h7ml.cn/guide/action https://halolight.docs.h7ml.cn/guide/action Fri, 19 Dec 2025 04:56:41 GMT 签到定时任务平台

HaloLight Action 是基于 Next.js 14 App Router 和 Supabase 构建的现代化签到定时任务平台,支持多平台自动签到、任务调度、执行记录追踪和推送通知。

在线预览https://halolight-action.h7ml.cn

GitHubhttps://github.com/halolight/halolight-action

技术栈

技术 版本 说明
Next.js 14.2 App Router 架构
TypeScript 5.7 类型安全
Supabase latest PostgreSQL + Auth + RLS
Tailwind CSS 3.4 原子化 CSS
shadcn/ui latest Radix UI 组件库
TanStack Query 5.x 服务端状态管理
Zustand 5.x 客户端状态管理
Framer Motion latest 流畅动画
Vitest latest 单元测试

核心特性

签到任务自动化

  • 多平台支持:V2EX、GitHub、掘金、CSDN、B 站等
  • Cron 调度:灵活的定时表达式
  • 优先级队列:任务优先级管理
  • 手动触发:支持即时执行
  • 启用/禁用:灵活控制任务状态

数据监控与分析

  • 签到记录:完整执行历史、成功率统计
  • 执行分析:耗时分析、错误追踪
  • 推送日志:多渠道推送历史、状态监控
  • 后端分页:Element UI 风格、支持大数据量

多渠道推送通知

  • 12 种推送服务:Server 酱、钉钉、企微、飞书、Telegram、Discord、Bark 等
  • 集成 push-all-in-one:统一推送接口
  • 推送测试:配置即时验证
  • 默认渠道:灵活的通知路由

完整的权限系统

  • Supabase RLS:数据库级别的行级安全
  • 角色管理:super_admin / admin / user / guest
  • API 令牌:细粒度的 API 访问控制
  • 审计日志:操作记录追踪

目录结构

halolight-action/
├── app/                    # Next.js 14 App Router
│   ├── (auth)/             # 认证页面
│   │   ├── login/
│   │   ├── register/
│   │   ├── forgot-password/
│   │   └── reset-password/
│   ├── (dashboard)/        # 仪表盘页面 (15个功能页面)
│   │   ├── dashboard/      # 仪表盘首页
│   │   ├── signin-tasks/   # 签到任务管理
│   │   ├── signin-records/ # 签到记录查看
│   │   ├── push-logs/      # 推送日志
│   │   ├── scheduled-tasks/# 定时任务
│   │   ├── notifications/  # 通知中心
│   │   ├── data-dictionary/# 数据字典
│   │   ├── users/          # 用户管理
│   │   └── settings/       # 设置
│   │       ├── profile/
│   │       ├── appearance/
│   │       ├── push-channels/
│   │       └── api-proxy/
│   └── api/                # API 路由
│       ├── cron/           # 定时任务执行
│       ├── signin/         # 签到执行
│       └── push/test/      # 推送测试
├── components/             # React 组件
│   ├── ui/                 # shadcn/ui 基础组件
│   ├── layout/             # 布局组件
│   └── auth/               # 认证组件
├── hooks/                  # React Query Hooks
├── lib/                    # 工具库
│   ├── supabase/           # Supabase 客户端
│   ├── dal/                # 数据访问层
│   ├── cron/               # Cron 执行器
│   └── push/               # 推送服务封装
├── providers/              # Context Providers
├── stores/                 # Zustand 状态管理
├── types/                  # TypeScript 类型定义
└── supabase/migrations/    # 数据库迁移脚本

快速开始

环境要求

  • Node.js >= 24.0.0
  • pnpm >= 10.x
  • Supabase 账号 (必需)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-action.git
cd halolight-action

# 安装依赖
pnpm install

环境变量

bash
cat > .env.local <<'EOF'
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_APP_NAME=HaloLight Action
NEXT_PUBLIC_APP_URL=http://localhost:3000
EOF

Supabase 初始化

  1. Supabase Dashboard 创建项目
  2. SQL Editor 执行 supabase/migrations/init.sql
  3. 复制 Project URLanon public key.env.local
  4. (可选) 运行 pnpm generate:types 生成类型定义

启动开发服务器

bash
pnpm dev
# 访问 http://localhost:3000

默认测试账号

数据库架构

核心数据表

表名 说明 关键功能
signin_tasks 签到任务配置 Cron 调度、优先级、凭证管理
signin_records 签到执行记录 历史追踪、成功率统计
push_channels 推送渠道配置 12种推送服务、测试与默认渠道
push_logs 推送执行日志 推送历史、错误追踪
data_dictionary 数据字典配置 系统配置、多类型支持
notifications 系统通知 用户消息、通知中心
cron_jobs 定时任务配置 HTTP 请求调度
users 用户信息 认证授权、角色管理
user_tokens API 令牌 访问控制、令牌管理

Row Level Security (RLS)

  • 用户隔离:用户只能访问自己的数据
  • 角色权限:管理员拥有更高权限
  • 系统保护:系统配置受保护,不可删除
  • 审计追踪:所有操作可追溯

常用命令

bash
# 开发
pnpm dev                # 启动开发服务器
pnpm build              # 生产构建
pnpm start              # 启动生产服务器

# 质量检查
pnpm lint               # ESLint 检查
pnpm type-check         # TypeScript 类型检查
pnpm format             # Prettier 格式化
pnpm ci                 # 完整 CI 检查

# 测试
pnpm test               # 运行测试 (watch)
pnpm test:run           # 运行测试 (单次)
pnpm test:coverage      # 测试覆盖率报告

# Supabase
pnpm generate:types     # 从 Supabase 生成类型

部署

Vercel (推荐)

一键部署:

Deploy with Vercel

手动部署

bash
# 构建
pnpm build

# 启动生产服务器
pnpm start

Vercel Cron Jobs

在 Vercel 项目设置中添加:

  • Schedule0 8 * * * (每天早上 8 点)
  • Path/api/cron

安全注意事项

重要提醒

  1. 不要提交密钥

    • 不要将 .env.local 提交到 Git
    • 不要在 Issue/PR 中泄露 Supabase 密钥
    • 不要硬编码敏感信息到代码中
  2. 使用正确的密钥

    • 前端使用 anon public key (安全)
    • 前端不要使用 service_role key (危险)
  3. 凭证存储

    • 签到任务的凭证建议加密存储
    • 生产环境考虑使用 Supabase Vault 或外部密钥管理服务
  4. RLS 策略

    • 定期审查 RLS 策略
    • 测试未授权访问场景
    • 新增表务必启用 RLS

与 halolight 系列的关系

项目 后端 特点
halolight Mock.js 纯前端演示,无需后端服务
halolight-action Supabase 签到定时任务平台,真实后端
halolight-vue Mock.js Vue 3.5 实现
halolight-angular Mock.js Angular 实现

相关项目

]]>
<![CDATA[超级管理面板]]> https://halolight.docs.h7ml.cn/guide/admin https://halolight.docs.h7ml.cn/guide/admin Fri, 19 Dec 2025 04:56:41 GMT 超级管理面板

HaloLight Admin 是一个功能强大的超级管理面板,用于管理多个 HaloLight 实例和租户。

特性

  • 🏢 多租户管理 - 支持多租户隔离和管理
  • ⚙️ 全局配置 - 统一管理所有实例的配置
  • 📊 系统监控 - 实时监控系统状态和性能
  • 📝 审计日志 - 完整的操作审计记录
  • 👥 用户管理 - 跨租户的用户管理
  • 🔐 权限控制 - 细粒度的权限管理

快速开始

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-admin.git
cd halolight-admin

# 安装依赖
pnpm install

# 运行开发服务器
pnpm dev

功能模块

租户管理

  • 创建/编辑/删除租户
  • 租户配额管理
  • 租户数据隔离

系统监控

  • CPU/内存使用率
  • API 请求统计
  • 错误日志监控
  • 性能指标分析

审计日志

  • 用户操作记录
  • 系统变更追踪
  • 安全事件告警
  • 日志导出功能

相关链接

]]>
<![CDATA[AI 智能助理]]> https://halolight.docs.h7ml.cn/guide/ai https://halolight.docs.h7ml.cn/guide/ai Fri, 19 Dec 2025 04:56:41 GMT AI 智能助理

HaloLight AI 服务基于 Hono + LangChain.js 构建,提供 RAG 检索增强、动作执行和多模型自动降级。

在线预览:内部 API 服务 (无独立 Demo)

GitHubhttps://github.com/halolight/halolight-ai

技术栈

技术 版本 说明
Hono latest 轻量 HTTP 框架
LangChain.js latest LLM 编排 + RAG 管道
pgvector latest PostgreSQL 向量检索扩展
Drizzle ORM latest TypeScript ORM & 迁移
PostgreSQL 14+ 持久化与向量存储
Node.js 22+ 运行时
Zod latest 数据验证

目录结构

halolight-ai/
├── src/
│   ├── index.ts                    # 应用入口
│   ├── routes/                     # API 路由
│   │   ├── chat.ts                 # 对话接口
│   │   ├── actions.ts              # 动作执行
│   │   ├── history.ts              # 历史记录
│   │   └── knowledge.ts            # 知识库管理
│   ├── services/
│   │   ├── llm/                    # LLM 服务层
│   │   │   ├── openai.ts
│   │   │   ├── anthropic.ts
│   │   │   └── factory.ts          # 模型工厂 (自动降级)
│   │   ├── rag/                    # RAG 管道
│   │   │   ├── embeddings.ts       # 文档分块与嵌入
│   │   │   ├── retriever.ts        # 向量检索
│   │   │   └── pipeline.ts         # RAG 管道
│   │   ├── actions/                # 动作执行系统
│   │   │   ├── executor.ts         # 动作执行器
│   │   │   ├── registry.ts         # 动作注册表
│   │   │   └── permissions.ts      # 权限校验
│   │   └── memory/
│   │       └── conversation.ts     # 对话记忆管理
│   ├── db/
│   │   ├── schema.ts               # Drizzle Schema (含 pgvector)
│   │   └── client.ts               # 数据库客户端
│   ├── middleware/
│   │   ├── auth.ts                 # 认证中间件
│   │   └── tenant.ts               # 租户隔离
│   └── types/
│       └── index.ts                # TypeScript 类型定义
├── drizzle/                        # 迁移文件
├── drizzle.config.ts
├── Dockerfile
├── docker-compose.yml
└── package.json

快速开始

前置要求

  • Node.js >= 22.0.0
  • PostgreSQL >= 14 (with pgvector extension)
  • 至少配置一个 LLM 提供商 API Key

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-ai.git
cd halolight-ai

# 安装依赖
pnpm install

环境变量

bash
cp .env.example .env

关键配置项:

env
# 数据库
DATABASE_URL=postgresql://user:password@localhost:5432/halolight_ai

# LLM 提供商 (至少配置一个)
OPENAI_API_KEY=sk-...
# 或
ANTHROPIC_API_KEY=sk-ant-...
# 或
AZURE_OPENAI_API_KEY=...

# RAG 配置
CHUNK_SIZE=1000              # 文档分块大小
CHUNK_OVERLAP=200            # 分块重叠
RETRIEVAL_TOP_K=5            # 检索数量

# 对话配置
MAX_CONVERSATION_HISTORY=20  # 对话历史长度
ENABLE_STREAMING=true        # 启用流式响应
ENABLE_AUDIT_LOG=true        # 启用审计日志

# 生产环境
JWT_SECRET=your-secret-key
CORS_ORIGINS=https://your-domain.com

初始化数据库

bash
# 生成迁移文件
pnpm db:generate

# 运行迁移
pnpm db:migrate

# 或直接推送 schema (开发环境)
pnpm db:push

# 打开 Drizzle Studio
pnpm db:studio

启动开发服务器

bash
pnpm dev

服务将在 http://localhost:3000 启动。

生产构建

bash
pnpm build
pnpm start

核心功能

多模型支持

系统会自动检测可用的 LLM 提供商并按优先级降级:

Azure OpenAI (1) → OpenAI (2) → Anthropic (3) → Ollama (4)

RAG 知识库

步骤 说明 配置
文档分块 RecursiveCharacterTextSplitter 1000 字符, 200 重叠
向量嵌入 OpenAI Embeddings text-embedding-3-small
向量存储 pgvector 1536 维
检索 余弦相似度 Top-K (默认 5)
上下文注入 将检索结果注入 LLM 提示词 -

流式响应

启用 SSE 流式输出,降低首字延迟:

bash
POST /api/ai/chat/stream
Content-Type: application/json

{
  "message": "你好",
  "streaming": true
}

权限控制

基于角色的访问控制 (RBAC):

角色 权限级别
super_admin 最高权限
admin 管理权限
user 普通用户
guest 访客

敏感操作需要二次确认 (_confirmed: true)。

对话记忆

  • 存储在 conversationsmessages
  • 默认保留最近 20 条消息
  • 支持多轮对话上下文

租户隔离

所有数据操作都基于 TenantContext

typescript
interface TenantContext {
  tenantId: string;
  userId: string;
  role: UserRole;
}

从请求头提取:

  • X-Tenant-ID
  • X-User-ID
  • X-User-Role

API 端点

健康检查

bash
GET /health
GET /health/ready
GET /api/ai/info

对话

bash
# 发送消息
POST /api/ai/chat
Content-Type: application/json

{
  "message": "你好,介绍一下 HaloLight",
  "conversationId": "uuid",    // 可选
  "includeContext": true,
  "maxContextDocs": 5
}

# 流式响应
POST /api/ai/chat/stream

动作执行

bash
# 执行动作
POST /api/ai/actions/execute
Content-Type: application/json

{
  "action": "query_users",
  "params": {
    "role": "admin",
    "limit": 10
  }
}

# 获取可用动作
GET /api/ai/actions/available

# 获取动作详情
GET /api/ai/actions/:name

历史记录

bash
GET /api/ai/history?limit=10
GET /api/ai/history/:id
DELETE /api/ai/history/:id
PATCH /api/ai/history/:id

知识库

bash
# 导入文档
POST /api/ai/knowledge/ingest
Content-Type: application/json

{
  "content": "文档内容...",
  "metadata": {
    "title": "文档标题",
    "category": "技术文档"
  },
  "source": "manual",
  "sourceId": "doc-001"
}

# 批量导入
POST /api/ai/knowledge/batch-ingest

# 列出文档
GET /api/ai/knowledge?limit=50&offset=0

# 删除文档
DELETE /api/ai/knowledge/:id

认证

所有 /api/* 端点需要以下请求头 (开发环境可省略):

X-Tenant-ID: your-tenant-id
X-User-ID: your-user-id
X-User-Role: admin | user | guest

部署

Docker

bash
# 构建镜像
docker build -t halolight-ai .

# 运行容器
docker run -p 3000:3000 \
  -e DATABASE_URL=postgresql://... \
  -e OPENAI_API_KEY=sk-... \
  halolight-ai

Docker Compose

bash
docker-compose up -d

生产环境必备

  • DATABASE_URL:PostgreSQL 连接字符串
  • NODE_ENV=production
  • 至少一个 LLM 提供商 API Key
  • JWT_SECRET:用于认证的密钥
  • CORS_ORIGINS:允许的跨域来源

故障排查

数据库连接失败

bash
# 检查 PostgreSQL 是否运行
psql $DATABASE_URL -c "SELECT 1"

# 检查 pgvector 扩展
psql $DATABASE_URL -c "CREATE EXTENSION IF NOT EXISTS vector"

LLM 提供商错误

bash
# 检查可用提供商
curl http://localhost:3000/api/ai/info

向量检索不准确

  • 增加 RETRIEVAL_TOP_K
  • 调整 CHUNK_SIZECHUNK_OVERLAP
  • 使用混合检索 (向量 + 关键词)

相关项目

]]>
<![CDATA[Angular 版本]]> https://halolight.docs.h7ml.cn/guide/angular https://halolight.docs.h7ml.cn/guide/angular Fri, 19 Dec 2025 04:56:41 GMT Angular 版本

HaloLight Angular 版本基于 Angular 21 构建,采用 Signals + 独立组件 + TypeScript。

在线预览https://halolight-angular.h7ml.cn/

GitHubhttps://github.com/halolight/halolight-angular

特性

  • 🏗️ Angular 21 - 最新企业级框架,Signals + 独立组件
  • NgRx Signals - 轻量级响应式状态管理
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 📑 多标签页 - 标签栏管理
  • 命令面板 - 快捷键导航

技术栈

技术 版本 说明
Angular 21.x 企业级框架
TypeScript 5.x 类型安全
Tailwind CSS 4.x 原子化 CSS
spartan/ui latest UI 组件库(Radix 风格)
NgRx Signals 21.x 响应式状态管理
TanStack Query 5.x 服务端状态
Mock.js 1.x 数据模拟

核心特性

  • 可配置仪表盘 - 9 种小部件,拖拽布局,响应式适配
  • 多标签导航 - 浏览器式标签,右键菜单,状态缓存
  • 权限系统 - RBAC 权限控制,路由守卫,权限指令/组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 多账户切换 - 快速切换账户,记住登录状态
  • 命令面板 - 键盘快捷键 (⌘K),全局搜索
  • 实时通知 - WebSocket 推送,通知中心

目录结构

halolight-angular/
├── src/
│   ├── app/
│   │   ├── pages/                  # 页面组件
│   │   │   ├── admin/             # 管理后台页面
│   │   │   │   ├── dashboard/     # 仪表盘
│   │   │   │   ├── users/         # 用户管理
│   │   │   │   ├── roles/         # 角色管理
│   │   │   │   ├── permissions/   # 权限管理
│   │   │   │   ├── settings/      # 系统设置
│   │   │   │   └── profile/       # 个人中心
│   │   │   └── auth/              # 认证页面
│   │   │       ├── login/
│   │   │       ├── register/
│   │   │       ├── forgot-password/
│   │   │       └── reset-password/
│   │   ├── components/
│   │   │   ├── ui/                # spartan/ui 组件
│   │   │   ├── layout/            # 布局组件
│   │   │   ├── dashboard/         # 仪表盘组件
│   │   │   └── shared/            # 共享组件
│   │   ├── services/              # 服务层
│   │   ├── stores/                # NgRx Signals Stores
│   │   ├── guards/                # 路由守卫
│   │   ├── interceptors/          # HTTP 拦截器
│   │   ├── directives/            # 指令
│   │   ├── pipes/                 # 管道
│   │   ├── lib/                   # 工具库
│   │   ├── types/                 # 类型定义
│   │   ├── mocks/                 # Mock 数据
│   │   ├── app.routes.ts          # 路由配置
│   │   ├── app.config.ts          # 应用配置
│   │   └── app.component.ts       # 根组件
│   ├── environments/              # 环境配置
│   └── styles.css                 # 全局样式
├── public/                        # 静态资源
├── angular.json
├── tailwind.config.js
├── tsconfig.json
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-angular.git
cd halolight-angular
pnpm install

环境变量

bash
cp src/environments/environment.example.ts src/environments/environment.development.ts
ts
// src/environments/environment.development.ts
export const environment = {
  production: false,
  apiUrl: '/api',
  useMock: true,
  appTitle: 'Admin Pro',
  brandName: 'Halolight',
  demoEmail: '[email protected]',
  demoPassword: '123456',
  showDemoHint: true,
};

启动开发

bash
pnpm start

访问 http://localhost:4200

构建生产

bash
pnpm build
ng build --configuration production

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

核心功能

状态管理 (NgRx Signals)

ts
// stores/auth.store.ts
import { signalStore, withState, withMethods, withComputed, patchState } from '@ngrx/signals';
import { computed, inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

interface AuthState {
  user: User | null;
  token: string | null;
  loading: boolean;
}

const initialState: AuthState = {
  user: null,
  token: null,
  loading: false,
};

export const AuthStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withComputed((store) => ({
    isAuthenticated: computed(() => !!store.token() && !!store.user()),
    permissions: computed(() => store.user()?.permissions ?? []),
  })),
  withMethods((store, authService = inject(AuthService)) => ({
    async login(credentials: LoginCredentials) {
      patchState(store, { loading: true });
      try {
        const response = await authService.login(credentials);
        patchState(store, {
          user: response.user,
          token: response.token,
          loading: false,
        });
      } catch (error) {
        patchState(store, { loading: false });
        throw error;
      }
    },

    logout() {
      patchState(store, { user: null, token: null });
    },

    hasPermission(permission: string): boolean {
      const permissions = store.permissions();
      return permissions.some(p =>
        p === '*' || p === permission ||
        (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
      );
    },
  }))
);

数据获取 (TanStack Query)

ts
// services/users.service.ts
import { Injectable, inject } from '@angular/core';
import { injectQuery, injectMutation, injectQueryClient } from '@tanstack/angular-query-experimental';
import { ApiService } from './api.service';

@Injectable({ providedIn: 'root' })
export class UsersService {
  private api = inject(ApiService);
  private queryClient = injectQueryClient();

  getUsers(params?: UserQueryParams) {
    return injectQuery(() => ({
      queryKey: ['users', params],
      queryFn: () => this.api.get<UserListResponse>('/users', { params }),
    }));
  }

  createUser() {
    return injectMutation(() => ({
      mutationFn: (data: CreateUserDto) => this.api.post<User>('/users', data),
      onSuccess: () => {
        this.queryClient.invalidateQueries({ queryKey: ['users'] });
      },
    }));
  }
}

权限控制

ts
// directives/permission.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef, inject, effect } from '@angular/core';
import { AuthStore } from '../stores/auth.store';

@Directive({
  selector: '[appPermission]',
  standalone: true,
})
export class PermissionDirective {
  private templateRef = inject(TemplateRef<unknown>);
  private viewContainer = inject(ViewContainerRef);
  private authStore = inject(AuthStore);

  @Input() set appPermission(permission: string) {
    effect(() => {
      const hasPermission = this.authStore.hasPermission(permission);
      this.viewContainer.clear();
      if (hasPermission) {
        this.viewContainer.createEmbeddedView(this.templateRef);
      }
    });
  }
}
html
<!-- 使用指令 -->
<button *appPermission="'users:delete'">删除</button>
ts
// components/permission-guard.component.ts
import { Component, Input, inject, computed } from '@angular/core';
import { AuthStore } from '../../stores/auth.store';

@Component({
  selector: 'app-permission-guard',
  standalone: true,
  template: `
    @if (hasPermission()) {
      <ng-content />
    } @else {
      <ng-content select="[fallback]" />
    }
  `,
})
export class PermissionGuardComponent {
  @Input({ required: true }) permission!: string;

  private authStore = inject(AuthStore);
  hasPermission = computed(() => this.authStore.hasPermission(this.permission));
}
html
<!-- 使用组件 -->
<app-permission-guard permission="users:delete">
  <app-delete-button />
  <span fallback>无权限</span>
</app-permission-guard>

可拖拽仪表盘

ts
// components/dashboard/dashboard-grid.component.ts
import { Component, inject, computed } from '@angular/core';
import { GridsterModule, GridsterConfig, GridsterItem } from 'angular-gridster2';
import { DashboardStore } from '../../stores/dashboard.store';

@Component({
  selector: 'app-dashboard-grid',
  standalone: true,
  imports: [GridsterModule, WidgetWrapperComponent],
  template: `
    <gridster [options]="options()">
      @for (widget of widgets(); track widget.id) {
        <gridster-item [item]="widget">
          <app-widget-wrapper [widget]="widget" />
        </gridster-item>
      }
    </gridster>
  `,
})
export class DashboardGridComponent {
  private dashboardStore = inject(DashboardStore);

  widgets = this.dashboardStore.widgets;
  isEditing = this.dashboardStore.isEditing;

  options = computed<GridsterConfig>(() => ({
    gridType: 'fit',
    displayGrid: this.isEditing() ? 'always' : 'none',
    draggable: { enabled: this.isEditing() },
    resizable: { enabled: this.isEditing() },
    pushItems: true,
    minCols: 12,
    maxCols: 12,
    minRows: 4,
    defaultItemCols: 3,
    defaultItemRows: 2,
    itemChangeCallback: (item) => this.dashboardStore.updateWidget(item),
  }));
}

主题系统

皮肤预设

支持 11 种预设皮肤,通过快捷设置面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿 --primary: 64.6% 0.178 142.49
Rose 玫瑰红 --primary: 59.3% 0.214 12.76
Orange 橙色 --primary: 65.4% 0.194 35.76
Amber 琥珀 --primary: 74.2% 0.167 83.25
Yellow 黄色 --primary: 84.5% 0.181 99.58
Lime 柠檬绿 --primary: 76.5% 0.165 128.35
Teal 青色 --primary: 59.8% 0.134 179.61
Cyan 青蓝 --primary: 68.3% 0.148 192.18
Sky 天蓝 --primary: 68.5% 0.171 227.08

CSS 变量 (OKLch)

css
/* 示例变量定义 */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 98% 0 0;
  --secondary: 96.1% 0.002 286.08;
  --secondary-foreground: 14.9% 0.017 285.75;
  --muted: 96.1% 0.002 286.08;
  --muted-foreground: 55.4% 0.009 285.82;
  --accent: 96.1% 0.002 286.08;
  --accent-foreground: 14.9% 0.017 285.75;
  --border: 92.2% 0.004 285.86;
  --input: 92.2% 0.004 285.86;
  --ring: 51.1% 0.262 276.97;
}

.dark {
  --background: 14.9% 0.017 285.75;
  --foreground: 98% 0 0;
  --primary: 56.1% 0.287 277.04;
  /* ... */
}

主题切换

ts
// 切换主题
const uiSettingsStore = inject(UiSettingsStore);
uiSettingsStore.setTheme('dark'); // 'light' | 'dark' | 'system'

// 切换皮肤
uiSettingsStore.setSkin('rose'); // 11 种皮肤预设

页面路由

路径 页面 权限
/ 重定向到 /dashboard -
/login 登录 公开
/register 注册 公开
/forgot-password 忘记密码 公开
/reset-password 重置密码 公开
/dashboard 仪表盘 dashboard:view
/users 用户列表 users:list
/users/create 创建用户 users:create
/users/:id 用户详情 users:view
/users/:id/edit 编辑用户 users:update
/roles 角色管理 roles:list
/permissions 权限管理 permissions:list
/settings 系统设置 settings:view
/profile 个人中心 登录即可

环境变量

配置示例

ts
// src/environments/environment.development.ts
export const environment = {
  production: false,
  apiUrl: '/api',
  useMock: true,
  appTitle: 'Admin Pro',
  brandName: 'Halolight',
  demoEmail: '[email protected]',
  demoPassword: '123456',
  showDemoHint: true,
};

变量说明

变量名 说明 默认值
production 是否生产环境 false
apiUrl API 基础路径 /api
useMock 是否使用 Mock 数据 true
appTitle 应用标题 Admin Pro
brandName 品牌名称 Halolight
demoEmail 演示账号邮箱 [email protected]
demoPassword 演示账号密码 123456
showDemoHint 是否显示演示提示 true

使用方式

ts
import { inject } from '@angular/core';
import { environment } from '../environments/environment';

// 在组件或服务中使用
export class ApiService {
  private apiUrl = environment.apiUrl;
  private useMock = environment.useMock;

  // ...
}

常用命令

bash
pnpm start          # 启动开发服务器
pnpm build          # 生产构建
pnpm lint           # 代码检查
pnpm lint:fix       # 自动修复
pnpm type-check     # 类型检查
pnpm test           # 运行测试
pnpm test:coverage  # 测试覆盖率

测试

bash
pnpm test           # 运行测试(watch 模式)
pnpm test:run       # 单次运行
pnpm test:coverage  # 覆盖率报告
pnpm test:ui        # Vitest UI 界面

测试示例

ts
// auth.store.spec.ts
import { TestBed } from '@angular/core/testing';
import { AuthStore } from './auth.store';
import { AuthService } from '../services/auth.service';

describe('AuthStore', () => {
  let store: InstanceType<typeof AuthStore>;
  let authService: jasmine.SpyObj<AuthService>;

  beforeEach(() => {
    const authServiceSpy = jasmine.createSpyObj('AuthService', ['login', 'logout']);

    TestBed.configureTestingModule({
      providers: [
        AuthStore,
        { provide: AuthService, useValue: authServiceSpy },
      ],
    });

    store = TestBed.inject(AuthStore);
    authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
  });

  it('should initialize with default state', () => {
    expect(store.user()).toBeNull();
    expect(store.token()).toBeNull();
    expect(store.isAuthenticated()).toBe(false);
  });

  it('should login successfully', async () => {
    const mockResponse = {
      user: { id: '1', email: '[email protected]', permissions: ['users:view'] },
      token: 'mock-token',
    };
    authService.login.and.returnValue(Promise.resolve(mockResponse));

    await store.login({ email: '[email protected]', password: '123456' });

    expect(store.user()).toEqual(mockResponse.user);
    expect(store.token()).toBe('mock-token');
    expect(store.isAuthenticated()).toBe(true);
  });

  it('should check permissions correctly', async () => {
    const mockResponse = {
      user: { id: '1', email: '[email protected]', permissions: ['users:*', 'dashboard:view'] },
      token: 'mock-token',
    };
    authService.login.and.returnValue(Promise.resolve(mockResponse));
    await store.login({ email: '[email protected]', password: '123456' });

    expect(store.hasPermission('users:view')).toBe(true);
    expect(store.hasPermission('users:delete')).toBe(true);
    expect(store.hasPermission('dashboard:view')).toBe(true);
    expect(store.hasPermission('settings:view')).toBe(false);
  });
});

配置

Angular 配置

ts
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideQueryClient } from '@tanstack/angular-query-experimental';
import { QueryClient } from '@tanstack/query-core';

import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
    provideAnimations(),
    provideQueryClient(new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 1000 * 60 * 5, // 5 minutes
          gcTime: 1000 * 60 * 10, // 10 minutes
        },
      },
    })),
  ],
};

Tailwind 配置

js
// tailwind.config.js
import { fontFamily } from 'tailwindcss/defaultTheme';

export default {
  darkMode: ['class'],
  content: ['./src/**/*.{html,ts}'],
  theme: {
    container: {
      center: true,
      padding: '2rem',
      screens: {
        '2xl': '1400px',
      },
    },
    extend: {
      fontFamily: {
        sans: ['Inter var', ...fontFamily.sans],
      },
      colors: {
        border: 'oklch(var(--border))',
        input: 'oklch(var(--input))',
        ring: 'oklch(var(--ring))',
        background: 'oklch(var(--background))',
        foreground: 'oklch(var(--foreground))',
        primary: {
          DEFAULT: 'oklch(var(--primary))',
          foreground: 'oklch(var(--primary-foreground))',
        },
        secondary: {
          DEFAULT: 'oklch(var(--secondary))',
          foreground: 'oklch(var(--secondary-foreground))',
        },
        // ... 更多颜色定义
      },
      keyframes: {
        'accordion-down': {
          from: { height: '0' },
          to: { height: 'var(--radix-accordion-content-height)' },
        },
        'accordion-up': {
          from: { height: 'var(--radix-accordion-content-height)' },
          to: { height: '0' },
        },
      },
      animation: {
        'accordion-down': 'accordion-down 0.2s ease-out',
        'accordion-up': 'accordion-up 0.2s ease-out',
      },
    },
  },
  plugins: [require('tailwindcss-animate')],
};

部署

Vercel (推荐)

Deploy with Vercel

bash
vercel

Docker

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
bash
docker build -t halolight-angular .
docker run -p 3000:80 halolight-angular

其他平台

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

路由守卫

ts
// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthStore } from '../stores/auth.store';

export const authGuard: CanActivateFn = (route, state) => {
  const authStore = inject(AuthStore);
  const router = inject(Router);

  if (!authStore.isAuthenticated()) {
    router.navigate(['/login'], { queryParams: { redirect: state.url } });
    return false;
  }

  return true;
};

// guards/permission.guard.ts
export const permissionGuard: CanActivateFn = (route) => {
  const authStore = inject(AuthStore);
  const router = inject(Router);
  const permission = route.data['permission'] as string;

  if (permission && !authStore.hasPermission(permission)) {
    router.navigate(['/403']);
    return false;
  }

  return true;
};

HTTP 拦截器

ts
// interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthStore } from '../stores/auth.store';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authStore = inject(AuthStore);
  const token = authStore.token();

  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
  }

  return next(req);
};

// interceptors/error.interceptor.ts
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);
  const authStore = inject(AuthStore);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        authStore.logout();
        router.navigate(['/login']);
      }
      return throwError(() => error);
    })
  );
};

Signals 计算属性

ts
// stores/ui-settings.store.ts
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { computed } from '@angular/core';

interface UiSettingsState {
  theme: 'light' | 'dark' | 'system';
  skin: string;
  sidebarCollapsed: boolean;
}

const initialState: UiSettingsState = {
  theme: 'system',
  skin: 'default',
  sidebarCollapsed: false,
};

export const UiSettingsStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withComputed((store) => ({
    effectiveTheme: computed(() => {
      if (store.theme() === 'system') {
        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
      }
      return store.theme();
    }),
    isDarkMode: computed(() => store.effectiveTheme() === 'dark'),
  })),
  withMethods((store) => ({
    setTheme(theme: 'light' | 'dark' | 'system') {
      patchState(store, { theme });
      document.documentElement.classList.toggle('dark', store.isDarkMode());
    },
    setSkin(skin: string) {
      patchState(store, { skin });
      document.documentElement.setAttribute('data-theme', skin);
    },
    toggleSidebar() {
      patchState(store, { sidebarCollapsed: !store.sidebarCollapsed() });
    },
  }))
);

性能优化

图片优化

ts
// 使用 NgOptimizedImage
import { NgOptimizedImage } from '@angular/common';

@Component({
  imports: [NgOptimizedImage],
  template: `
    <img
      ngSrc="proxy.php?url=https%3A%2F%2Fhalolight.docs.h7ml.cn%2Fassets%2Fimages%2Fhero.jpg"
      width="1200"
      height="600"
      priority
      alt="Hero image"
    />
  `,
})
export class HeroComponent {}

懒加载组件

ts
// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./pages/admin/dashboard/dashboard.component')
      .then(m => m.DashboardComponent),
  },
  {
    path: 'users',
    loadChildren: () => import('./pages/admin/users/users.routes')
      .then(m => m.USERS_ROUTES),
  },
];

预加载策略

ts
// app.config.ts
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules),
      withComponentInputBinding()
    ),
  ],
};

OnPush 变更检测

ts
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-user-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (user of users(); track user.id) {
      <app-user-card [user]="user" />
    }
  `,
})
export class UserListComponent {
  users = signal<User[]>([]);
}

常见问题

Q:如何配置 Mock 数据?

A:在 environment.ts 中设置 useMock: true,并在 src/mocks 目录下定义 Mock 数据:

ts
// mocks/users.mock.ts
import Mock from 'mockjs';

Mock.mock('/api/users', 'get', {
  'data|10-20': [{
    'id|+1': 1,
    'name': '@cname',
    'email': '@email',
    'avatar': '@image(100x100)',
    'role': '@pick(["admin", "user", "guest"])',
    'status': '@pick(["active", "inactive"])',
    'createdAt': '@datetime',
  }],
  total: '@integer(10, 100)',
});

Q:如何实现路由权限控制?

A:使用 permissionGuard 并在路由配置中指定所需权限:

ts
// app.routes.ts
{
  path: 'users',
  loadComponent: () => import('./pages/admin/users/users.component'),
  data: { permission: 'users:view' },
  canActivate: [authGuard, permissionGuard],
}

Q:如何自定义主题颜色?

A:在 styles.css 中覆盖 CSS 变量:

css
:root {
  --primary: 51.1% 0.262 276.97; /* 自定义主色调 */
  --primary-foreground: 98% 0 0;
}

.dark {
  --primary: 56.1% 0.287 277.04;
  --primary-foreground: 98% 0 0;
}

Q:如何集成第三方 UI 组件库?

A:spartan/ui 已集成,如需添加其他组件,可通过 Angular CDK 扩展:

ts
import { CdkDrag, CdkDropList } from '@angular/cdk/drag-drop';

@Component({
  imports: [CdkDrag, CdkDropList],
  template: `
    <div cdkDropList (cdkDropListDropped)="drop($event)">
      @for (item of items(); track item.id) {
        <div cdkDrag>{{ item.name }}</div>
      }
    </div>
  `,
})
export class DraggableListComponent {}

与其他版本对比

特性 Angular 版本 Next.js Vue
SSR/SSG ✅ Angular SSR ✅ (Nuxt)
状态管理 NgRx Signals Zustand Pinia
路由 Angular Router App Router Vue Router
构建工具 Angular CLI + esbuild Next.js Vite
类型安全 TypeScript (强制) TypeScript TypeScript
企业支持 Google Vercel 社区

相关链接

]]>
<![CDATA[Bun Hono 后端 API]]> https://halolight.docs.h7ml.cn/guide/api-bun https://halolight.docs.h7ml.cn/guide/api-bun Fri, 19 Dec 2025 04:56:41 GMT Bun Hono 后端 API

HaloLight Bun 后端 API 基于 Bun + Hono + Drizzle ORM 构建,提供超高性能后端服务。

API 文档https://halolight-api-bun.h7ml.cn/docs

GitHubhttps://github.com/halolight/halolight-api-bun

特性

  • 🔐 JWT 双令牌 - Access Token + Refresh Token,自动续期
  • 🛡️ RBAC 权限 - 基于角色的访问控制,通配符匹配
  • 📡 RESTful API - 标准化接口设计,OpenAPI 文档
  • 🗄️ Drizzle ORM - 类型安全的数据库操作
  • 数据验证 - 请求参数校验,错误处理
  • 📊 日志系统 - 请求日志,错误追踪
  • 🐳 Docker 支持 - 容器化部署
  • 极速性能 - 比 Node.js 快 4 倍

技术栈

技术 版本 说明
Bun 1.1+ 运行时
Hono 4.x Web 框架
Drizzle ORM 0.36+ 数据库 ORM
PostgreSQL 15+ 数据存储
Zod 3.x 数据验证
JWT - 身份认证
Swagger - API 文档

快速开始

环境要求

  • Bun >= 1.1
  • pnpm >= 8.0
  • PostgreSQL (可选,默认 SQLite)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-api-bun.git
cd halolight-api-bun

# 安装依赖
pnpm install

环境变量

bash
cp .env.example .env
env
# 数据库
DATABASE_URL=postgresql://user:password@localhost:5432/halolight

# JWT 密钥
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# 服务配置
PORT=3002
NODE_ENV=development
CORS_ORIGIN=http://localhost:3000
API_PREFIX=/api

数据库初始化

bash
bun run db:push
bun run db:seed

启动服务

bash
# 开发模式
bun run dev

# 生产模式
bun run build
bun run start

访问 http://localhost:3002

项目结构

halolight-api-bun/
├── src/
│   ├── routes/          # 控制器/路由处理
│   ├── services/        # 业务逻辑层
│   ├── db/              # 数据模型
│   ├── middleware/      # 中间件
│   ├── utils/           # 工具函数
│   └── index.ts         # 应用入口
├── test/                # 测试文件
├── Dockerfile           # Docker 配置
├── docker-compose.yml
└── package.json

API 模块

认证相关端点

方法 路径 描述 权限
POST /api/auth/login 用户登录 公开
POST /api/auth/register 用户注册 公开
POST /api/auth/refresh 刷新令牌 公开
POST /api/auth/logout 退出登录 需认证
POST /api/auth/forgot-password 忘记密码 公开
POST /api/auth/reset-password 重置密码 公开

用户管理端点

方法 路径 描述 权限
GET /api/users 获取用户列表 users:view
GET /api/users/:id 获取用户详情 users:view
POST /api/users 创建用户 users:create
PUT /api/users/:id 更新用户 users:update
DELETE /api/users/:id 删除用户 users:delete
GET /api/users/me 获取当前用户 需认证

完整端点清单

文档管理 (Documents) - 5 个端点

方法 路径 描述
GET /api/documents 获取文档列表
GET /api/documents/:id 获取文档详情
POST /api/documents 创建文档
PUT /api/documents/:id 更新文档
DELETE /api/documents/:id 删除文档

文件管理 (Files) - 5 个端点

方法 路径 描述
GET /api/files 获取文件列表
GET /api/files/:id 获取文件详情
POST /api/files/upload 上传文件
PUT /api/files/:id 更新文件信息
DELETE /api/files/:id 删除文件

消息管理 (Messages) - 5 个端点

方法 路径 描述
GET /api/messages 获取消息列表
GET /api/messages/:id 获取消息详情
POST /api/messages 发送消息
PUT /api/messages/:id/read 标记已读
DELETE /api/messages/:id 删除消息

通知管理 (Notifications) - 4 个端点

方法 路径 描述
GET /api/notifications 获取通知列表
PUT /api/notifications/:id/read 标记已读
PUT /api/notifications/read-all 全部已读
DELETE /api/notifications/:id 删除通知

日历管理 (Calendar) - 5 个端点

方法 路径 描述
GET /api/calendar/events 获取日程列表
GET /api/calendar/events/:id 获取日程详情
POST /api/calendar/events 创建日程
PUT /api/calendar/events/:id 更新日程
DELETE /api/calendar/events/:id 删除日程

仪表盘 (Dashboard) - 6 个端点

方法 路径 描述
GET /api/dashboard/stats 统计数据
GET /api/dashboard/visits 访问趋势
GET /api/dashboard/sales 销售数据
GET /api/dashboard/pie 饼图数据
GET /api/dashboard/tasks 待办任务
GET /api/dashboard/calendar 今日日程

认证机制

JWT 双令牌

Access Token:  15 分钟有效期,用于 API 请求
Refresh Token: 7 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

typescript
// 刷新令牌示例
const response = await fetch('/api/auth/refresh', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    refreshToken: 'your_refresh_token'
  })
});

const { accessToken, refreshToken } = await response.json();

权限系统

角色定义

角色 说明 权限
super_admin 超级管理员 * (所有权限)
admin 管理员 users:*, documents:*, ...
user 普通用户 documents:view, files:view, ...
guest 访客 dashboard:view

权限格式

{resource}:{action}

示例:
- users:view      # 查看用户
- users:create    # 创建用户
- users:*         # 用户所有操作
- *               # 所有权限

错误处理

错误响应格式

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" }
    ]
  }
}

错误码

状态码 错误码 说明
400 VALIDATION_ERROR 参数验证失败
401 UNAUTHORIZED 未授权
403 FORBIDDEN 无权限
404 NOT_FOUND 资源不存在
409 CONFLICT 资源冲突
500 INTERNAL_ERROR 服务器错误

常用命令

bash
# 开发
bun run dev                 # 启动开发服务器
bun run build               # 生产构建
bun run start               # 运行生产版本

# 构建
bun run build               # 构建生产版本

# 测试
bun test                    # 运行单元测试
bun test --coverage         # 生成覆盖率报告

# 数据库
bun run db:push             # 推送 Schema 到数据库
bun run db:generate         # 生成迁移文件
bun run db:migrate          # 运行数据库迁移
bun run db:seed             # 填充测试数据
bun run db:studio           # 打开 Drizzle Studio

# 代码质量
bun run lint                # ESLint 检查
bun run lint:fix            # ESLint 自动修复
bun run type-check          # TypeScript 类型检查

部署

Docker

bash
docker build -t halolight-api-bun .
docker run -p 3002:3002 halolight-api-bun

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3002:3002"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

生产环境配置

env
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=your-production-secret

测试

运行测试

bash
bun test                    # 运行所有测试
bun test --coverage         # 生成覆盖率报告

测试示例

typescript
// 认证测试示例
import { describe, test, expect } from 'bun:test';

describe('Auth API', () => {
  test('should login successfully', async () => {
    const response = await fetch('http://localhost:3002/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: '[email protected]',
        password: 'admin123'
      })
    });

    const data = await response.json();
    expect(data.success).toBe(true);
    expect(data.data.accessToken).toBeDefined();
  });
});

性能指标

基准测试

指标 数值 说明
请求吞吐量 ~50,000 req/s 单核,简单路由
平均响应时间 <5ms 本地数据库
内存占用 ~30MB 冷启动
CPU 使用率 <10% 空闲状态

可观测性

日志系统

typescript
// 日志配置示例
import { logger } from './utils/logger';

logger.info('User logged in', { userId: user.id });
logger.error('Database error', { error: err.message });

健康检查

typescript
// GET /health
app.get('/health', (c) => {
  return c.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

监控指标

typescript
// Prometheus metrics 端点
app.get('/metrics', async (c) => {
  return c.text(await register.metrics());
});

常见问题

Q:如何配置数据库连接?

A:在 .env 文件中设置 DATABASE_URL

env
DATABASE_URL=postgresql://user:password@localhost:5432/halolight

Q:如何使用 Bun 内置密码哈希?

A:使用 Bun.password API:

typescript
// 哈希密码
const hash = await Bun.password.hash(password, {
  algorithm: 'bcrypt',
  cost: 10
});

// 验证密码
const isValid = await Bun.password.verify(password, hash, 'bcrypt');

开发工具

推荐插件/工具

  • Drizzle Studio - 可视化数据库管理工具
  • Hoppscotch/Postman - API 测试工具
  • ESLint + Prettier - 代码格式化
  • Bun VSCode Extension - Bun 语法支持

与其他后端对比

特性 Bun + Hono NestJS FastAPI Spring Boot
语言 TypeScript TypeScript Python Java
ORM Drizzle Prisma SQLAlchemy JPA
性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
学习曲线 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐

相关链接

]]>
<![CDATA[Go Fiber 后端 API]]> https://halolight.docs.h7ml.cn/guide/api-go https://halolight.docs.h7ml.cn/guide/api-go Fri, 19 Dec 2025 04:56:41 GMT Go Fiber 后端 API

HaloLight Go Fiber 后端 API 基于 Fiber 3.0 构建,提供高性能 Go 后端服务和完整的 JWT 双令牌认证。

API 文档https://halolight-api-go.h7ml.cn/docs

GitHubhttps://github.com/halolight/halolight-api-go

特性

  • 🔐 JWT 双令牌 - Access Token + Refresh Token,自动续期
  • 🛡️ RBAC 权限 - 基于角色的访问控制,通配符匹配
  • 📡 RESTful API - 标准化接口设计,OpenAPI 文档
  • 🗄️ GORM 2 - 类型安全的数据库操作
  • 数据验证 - 请求参数校验,错误处理
  • 📊 日志系统 - 请求日志,错误追踪
  • 🐳 Docker 支持 - 容器化部署

技术栈

技术 版本 说明
Go 1.22+ 运行时
Fiber 3.0 Web 框架
GORM 2.0 数据库 ORM
PostgreSQL 16 数据存储
go-playground/validator v10 数据验证
JWT golang-jwt/jwt/v5 身份认证
Swagger UI - API 文档

快速开始

环境要求

  • Go >= 1.22
  • PostgreSQL 16 (可选,默认 SQLite)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-api-go.git
cd halolight-api-go

# 安装依赖
go mod download

环境变量

bash
cp .env.example .env
env
# 数据库
DATABASE_URL=postgresql://user:pass@localhost:5432/halolight?sslmode=disable

# JWT 密钥
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=7d
JWT_REFRESH_EXPIRES=30d

# 服务配置
PORT=8080
APP_ENV=development

数据库初始化

bash
# GORM 自动迁移
go run cmd/server/main.go

# 或使用 Makefile
make migrate

启动服务

bash
# 开发模式
go run cmd/server/main.go

# 生产模式
make build
./bin/server

访问 http://localhost:8080

项目结构

halolight-api-go/
├── cmd/
│   └── server/
│       └── main.go              # 应用入口
├── internal/
│   ├── handlers/                # 控制器/路由处理
│   │   ├── auth_handler.go      # 认证端点
│   │   ├── user_handler.go      # 用户管理
│   │   ├── role_handler.go      # 角色管理
│   │   ├── permission_handler.go # 权限管理
│   │   ├── team_handler.go      # 团队管理
│   │   ├── document_handler.go  # 文档管理
│   │   ├── file_handler.go      # 文件管理
│   │   ├── folder_handler.go    # 文件夹管理
│   │   ├── calendar_handler.go  # 日历事件
│   │   ├── notification_handler.go # 通知管理
│   │   ├── message_handler.go   # 消息管理
│   │   ├── dashboard_handler.go # 仪表盘统计
│   │   └── home_handler.go      # 首页 + 健康检查
│   ├── services/                # 业务逻辑层
│   │   ├── auth_service.go
│   │   ├── user_service.go
│   │   └── ...
│   ├── models/                  # 数据模型
│   │   ├── user.go
│   │   ├── role.go
│   │   └── ...
│   ├── middleware/              # 中间件
│   │   ├── auth.go
│   │   └── cors.go
│   └── routes/                  # 路由配置
│       └── router.go
├── pkg/
│   ├── config/                  # 配置管理
│   ├── database/                # 数据库连接
│   └── utils/                   # 工具函数
├── docs/                        # 文档
├── .github/workflows/           # GitHub Actions
├── Dockerfile                   # Docker 配置
├── docker-compose.yml
└── go.mod

API 模块

认证相关端点

方法 路径 描述 权限
POST /api/auth/login 用户登录 公开
POST /api/auth/register 用户注册 公开
POST /api/auth/refresh 刷新令牌 公开
POST /api/auth/logout 退出登录 需认证
POST /api/auth/forgot-password 忘记密码 公开
POST /api/auth/reset-password 重置密码 公开
GET /api/auth/me 获取当前用户 需认证

用户管理端点

方法 路径 描述 权限
GET /api/users 获取用户列表 users:view
GET /api/users/:id 获取用户详情 users:view
POST /api/users 创建用户 users:create
PUT /api/users/:id 更新用户 users:update
DELETE /api/users/:id 删除用户 users:delete
PATCH /api/users/:id/status 更新用户状态 users:update
POST /api/users/batch-delete 批量删除用户 users:delete

完整端点清单

角色管理 (Roles) - 6 个端点

方法 路径 描述
GET /api/roles 获取角色列表
GET /api/roles/:id 获取角色详情
POST /api/roles 创建角色
PUT /api/roles/:id 更新角色
POST /api/roles/:id/permissions 分配权限
DELETE /api/roles/:id 删除角色

权限管理 (Permissions) - 4 个端点

方法 路径 描述
GET /api/permissions 获取权限列表
GET /api/permissions/:id 获取权限详情
POST /api/permissions 创建权限
DELETE /api/permissions/:id 删除权限

团队管理 (Teams) - 7 个端点

方法 路径 描述
GET /api/teams 获取团队列表
GET /api/teams/:id 获取团队详情
POST /api/teams 创建团队
PATCH /api/teams/:id 更新团队
DELETE /api/teams/:id 删除团队
POST /api/teams/:id/members 添加成员
DELETE /api/teams/:id/members/:userId 移除成员

文档管理 (Documents) - 11 个端点

方法 路径 描述
GET /api/documents 获取文档列表
GET /api/documents/:id 获取文档详情
POST /api/documents 创建文档
PUT /api/documents/:id 更新文档
PATCH /api/documents/:id/rename 重命名文档
POST /api/documents/:id/move 移动文档
POST /api/documents/:id/tags 更新标签
POST /api/documents/:id/share 分享文档
POST /api/documents/:id/unshare 取消分享
POST /api/documents/batch-delete 批量删除
DELETE /api/documents/:id 删除文档

文件管理 (Files) - 14 个端点

方法 路径 描述
POST /api/files/upload 上传文件
POST /api/files/folder 创建文件夹
GET /api/files 获取文件列表
GET /api/files/storage 获取存储信息
GET /api/files/:id 获取文件详情
GET /api/files/:id/download-url 获取下载链接
PATCH /api/files/:id/rename 重命名文件
POST /api/files/:id/move 移动文件
POST /api/files/:id/copy 复制文件
PATCH /api/files/:id/favorite 收藏/取消收藏
POST /api/files/:id/share 分享文件
POST /api/files/batch-delete 批量删除
DELETE /api/files/:id 删除文件

文件夹管理 (Folders) - 5 个端点

方法 路径 描述
GET /api/folders 获取文件夹列表
GET /api/folders/tree 获取树形结构
GET /api/folders/:id 获取文件夹详情
POST /api/folders 创建文件夹
DELETE /api/folders/:id 删除文件夹

消息管理 (Messages) - 5 个端点

方法 路径 描述
GET /api/messages/conversations 获取会话列表
GET /api/messages/conversations/:id 获取会话详情
POST /api/messages 发送消息
PUT /api/messages/:id/read 标记已读
DELETE /api/messages/:id 删除消息

通知管理 (Notifications) - 5 个端点

方法 路径 描述
GET /api/notifications 获取通知列表
GET /api/notifications/unread-count 获取未读数量
PUT /api/notifications/:id/read 标记已读
PUT /api/notifications/read-all 全部已读
DELETE /api/notifications/:id 删除通知

日历管理 (Calendar) - 9 个端点

方法 路径 描述
GET /api/calendar/events 获取日程列表
GET /api/calendar/events/:id 获取日程详情
POST /api/calendar/events 创建日程
PUT /api/calendar/events/:id 更新日程
PATCH /api/calendar/events/:id/reschedule 重新安排
POST /api/calendar/events/:id/attendees 添加参会人
DELETE /api/calendar/events/:id/attendees/:attendeeId 移除参会人
POST /api/calendar/events/batch-delete 批量删除
DELETE /api/calendar/events/:id 删除日程

仪表盘 (Dashboard) - 9 个端点

方法 路径 描述
GET /api/dashboard/stats 统计数据
GET /api/dashboard/visits 访问趋势
GET /api/dashboard/sales 销售数据
GET /api/dashboard/products 产品数据
GET /api/dashboard/orders 订单数据
GET /api/dashboard/activities 活动数据
GET /api/dashboard/pie 饼图数据
GET /api/dashboard/tasks 待办任务
GET /api/dashboard/overview 总览数据

认证机制

JWT 双令牌

Access Token:  7 天有效期,用于 API 请求
Refresh Token: 30 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

go
// 刷新令牌示例
func RefreshToken(c *fiber.Ctx) error {
    type RefreshRequest struct {
        RefreshToken string `json:"refreshToken"`
    }

    var req RefreshRequest
    if err := c.BodyParser(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "success": false,
            "message": "Invalid request",
        })
    }

    // 验证 refresh token
    claims, err := utils.ValidateToken(req.RefreshToken)
    if err != nil {
        return c.Status(401).JSON(fiber.Map{
            "success": false,
            "message": "Invalid refresh token",
        })
    }

    // 生成新的 access token
    accessToken, err := utils.GenerateAccessToken(claims.UserID)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "success": false,
            "message": "Failed to generate token",
        })
    }

    return c.JSON(fiber.Map{
        "success": true,
        "accessToken": accessToken,
    })
}

权限系统

角色定义

角色 说明 权限
super_admin 超级管理员 * (所有权限)
admin 管理员 users:*, documents:*, files:*, teams:*
user 普通用户 documents:view, documents:create, files:*
guest 访客 dashboard:view

权限格式

{resource}:{action}

示例:
- users:view      # 查看用户
- users:create    # 创建用户
- users:*         # 用户所有操作
- *               # 所有权限

错误处理

错误响应格式

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" }
    ]
  }
}

错误码

状态码 错误码 说明
400 VALIDATION_ERROR 参数验证失败
401 UNAUTHORIZED 未授权
403 FORBIDDEN 无权限
404 NOT_FOUND 资源不存在
409 CONFLICT 资源冲突
500 INTERNAL_ERROR 服务器错误

常用命令

bash
# 开发
go run cmd/server/main.go

# 构建
go build -o bin/server cmd/server/main.go

# 测试
go test ./...

# 数据库
make migrate

# 代码质量
go vet ./...
golangci-lint run

部署

Docker

bash
docker build -t halolight-api-go .
docker run -p 8080:8080 halolight-api-go

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - APP_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

生产环境配置

env
APP_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=your-production-secret

测试

运行测试

bash
# 单元测试
go test ./...

# 测试覆盖率
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

测试示例

go
func TestUserLogin(t *testing.T) {
    app := fiber.New()

    // 设置路由
    app.Post("/api/auth/login", handlers.Login)

    // 准备测试数据
    reqBody := `{"email":"[email protected]","password":"password123"}`
    req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(reqBody))
    req.Header.Set("Content-Type", "application/json")

    // 执行请求
    resp, _ := app.Test(req)

    // 验证响应
    assert.Equal(t, 200, resp.StatusCode)
}

性能指标

基准测试

指标 数值 说明
请求吞吐量 10,000+ QPS 单机,8 核 CPU
平均响应时间 < 10ms 简单查询
内存占用 ~50MB 空闲状态
CPU 使用率 < 10% 空闲状态

可观测性

日志系统

go
// 日志配置
logger := log.New(os.Stdout, "API: ", log.LstdFlags)

app.Use(func(c *fiber.Ctx) error {
    start := time.Now()
    err := c.Next()
    logger.Printf("%s %s %s %v",
        c.Method(),
        c.Path(),
        c.IP(),
        time.Since(start),
    )
    return err
})

健康检查

go
// 健康检查端点
app.Get("/health", func(c *fiber.Ctx) error {
    return c.JSON(fiber.Map{
        "status": "healthy",
        "timestamp": time.Now(),
        "database": db.Ping() == nil,
    })
})

监控指标

go
// Prometheus 指标
import "github.com/prometheus/client_golang/prometheus"

var (
    requestCounter = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "api_requests_total",
            Help: "Total API requests",
        },
        []string{"method", "path", "status"},
    )
)

常见问题

Q:JWT 密钥长度要求?

A:JWT 密钥至少 32 字符,建议使用 64 字符以上的随机字符串。

bash
# 生成安全密钥
openssl rand -base64 64

Q:数据库连接失败?

A:检查数据库配置和网络连接。

bash
# 检查 PostgreSQL 状态
docker-compose ps postgres

# 测试连接
psql -h localhost -U postgres -d halolight

开发工具

推荐插件/工具

  • Air - Go 热重载工具
  • golangci-lint - Go 代码检查
  • goose - 数据库迁移工具
  • mockery - Mock 生成工具

与其他后端对比

特性 Go Fiber NestJS FastAPI Spring Boot
语言 Go TypeScript Python Java
ORM GORM Prisma SQLAlchemy JPA
性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
学习曲线 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐

相关链接

]]>
<![CDATA[Java Spring Boot 后端 API]]> https://halolight.docs.h7ml.cn/guide/api-java https://halolight.docs.h7ml.cn/guide/api-java Fri, 19 Dec 2025 04:56:41 GMT Java Spring Boot 后端 API

HaloLight Spring Boot 后端 API 基于 Spring Boot 3.4.1 构建,提供企业级后端服务和完整的 JWT 双令牌认证。

API 文档https://halolight-api-java.h7ml.cn/api/swagger-ui

GitHubhttps://github.com/halolight/halolight-api-java

特性

  • 🔐 JWT 双令牌 - Access Token + Refresh Token,自动续期
  • 🛡️ RBAC 权限 - 基于角色的访问控制,通配符匹配
  • 📡 RESTful API - 标准化接口设计,OpenAPI 文档
  • 🗄️ Spring Data JPA - 类型安全的数据库操作
  • 数据验证 - Bean Validation 请求参数校验
  • 📊 日志系统 - 请求日志,错误追踪
  • 🐳 Docker 支持 - 多阶段构建,容器化部署

技术栈

技术 版本 说明
Java 23 运行时
Spring Boot 3.4.1 Web 框架
Spring Data JPA 3.4.1 数据库 ORM
PostgreSQL 16 数据存储
Bean Validation jakarta.validation 数据验证
JWT JJWT 身份认证
Springdoc OpenAPI 2.7.0 API 文档

快速开始

环境要求

  • Java >= 17
  • Maven >= 3.9
  • PostgreSQL 16 (可选,默认 H2)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-api-java.git
cd halolight-api-java

# 安装依赖
./mvnw clean install

环境变量

bash
cp .env.example .env
env
# 数据库
DATABASE_URL=jdbc:postgresql://localhost:5432/halolight_db
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=your-password

# JWT 密钥
JWT_SECRET=your-super-secret-jwt-key-change-in-production-min-32-chars
JWT_EXPIRATION=86400000
JWT_REFRESH_EXPIRATION=604800000

# 服务配置
PORT=8080
SPRING_PROFILES_ACTIVE=production

数据库初始化

bash
# 自动创建表结构(首次启动)
./mvnw spring-boot:run

# 运行种子数据(可选)
./mvnw exec:java -Dexec.mainClass="com.halolight.seed.DataSeeder"

启动服务

bash
# 开发模式
./mvnw spring-boot:run

# 生产模式
./mvnw clean package -DskipTests
java -jar target/halolight-api-java-1.0.0.jar

访问 http://localhost:8080

项目结构

halolight-api-java/
├── src/main/java/com/halolight/
│   ├── controller/              # 控制器/路由处理
│   │   ├── AuthController.java
│   │   ├── UserController.java
│   │   └── ...
│   ├── service/                 # 业务逻辑层
│   │   ├── AuthService.java
│   │   └── ...
│   ├── domain/                  # 数据模型
│   │   ├── entity/              # JPA 实体
│   │   └── repository/          # Repository 接口
│   ├── config/                  # 中间件/配置
│   │   ├── SecurityConfig.java
│   │   └── ...
│   ├── web/dto/                 # 请求验证 DTO
│   ├── security/                # 安全组件
│   └── HalolightApplication.java  # 应用入口
├── src/main/resources/          # 资源文件
│   ├── application.yml
│   └── application-*.yml
├── src/test/                    # 测试文件
├── Dockerfile                   # Docker 配置
├── docker-compose.yml
└── pom.xml                      # Maven 配置

API 模块

认证相关端点

方法 路径 描述 权限
POST /api/auth/login 用户登录 公开
POST /api/auth/register 用户注册 公开
POST /api/auth/refresh 刷新令牌 公开
POST /api/auth/logout 退出登录 需认证
POST /api/auth/forgot-password 忘记密码 公开
POST /api/auth/reset-password 重置密码 公开
GET /api/auth/me 获取当前用户 需认证

用户管理端点

方法 路径 描述 权限
GET /api/users 获取用户列表 users:view
GET /api/users/{id} 获取用户详情 users:view
POST /api/users 创建用户 users:create
PUT /api/users/{id} 更新用户 users:update
PUT /api/users/{id}/status 更新用户状态 users:update
DELETE /api/users/{id} 删除用户 users:delete

完整端点清单

角色管理 (Roles) - 6 个端点

方法 路径 描述
GET /api/roles 获取角色列表
GET /api/roles/{id} 获取角色详情
POST /api/roles 创建角色
PUT /api/roles/{id} 更新角色
POST /api/roles/{id}/permissions 分配权限
DELETE /api/roles/{id} 删除角色

权限管理 (Permissions) - 4 个端点

方法 路径 描述
GET /api/permissions 获取权限列表
POST /api/permissions 创建权限
PUT /api/permissions/{id} 更新权限
DELETE /api/permissions/{id} 删除权限

文档管理 (Documents) - 10 个端点

方法 路径 描述
GET /api/documents 获取文档列表
GET /api/documents/{id} 获取文档详情
POST /api/documents 创建文档
PUT /api/documents/{id} 更新文档
PUT /api/documents/{id}/rename 重命名文档
POST /api/documents/{id}/move 移动文档
POST /api/documents/{id}/tags 更新标签
POST /api/documents/{id}/share 分享文档
POST /api/documents/{id}/unshare 取消分享
DELETE /api/documents/{id} 删除文档

文件管理 (Files) - 10 个端点

方法 路径 描述
POST /api/files/upload 上传文件
GET /api/files 获取文件列表
GET /api/files/storage 获取存储配额
GET /api/files/{id} 获取文件详情
GET /api/files/{id}/download 下载文件
PUT /api/files/{id}/rename 重命名文件
POST /api/files/{id}/move 移动文件
PUT /api/files/{id}/favorite 切换收藏
POST /api/files/{id}/share 分享文件
DELETE /api/files/{id} 删除文件

团队管理 (Teams) - 6 个端点

方法 路径 描述
GET /api/teams 获取团队列表
GET /api/teams/{id} 获取团队详情
POST /api/teams 创建团队
PUT /api/teams/{id} 更新团队
POST /api/teams/{id}/members 添加成员
DELETE /api/teams/{id}/members/{userId} 移除成员

消息管理 (Messages) - 5 个端点

方法 路径 描述
GET /api/messages/conversations 获取会话列表
GET /api/messages/conversations/{userId} 获取会话消息
POST /api/messages 发送消息
PUT /api/messages/{id}/read 标记已读
DELETE /api/messages/{id} 删除消息

通知管理 (Notifications) - 5 个端点

方法 路径 描述
GET /api/notifications 获取通知列表
GET /api/notifications/unread-count 获取未读数量
PUT /api/notifications/{id}/read 标记单条已读
PUT /api/notifications/read-all 全部已读
DELETE /api/notifications/{id} 删除通知

日历管理 (Calendar) - 8 个端点

方法 路径 描述
GET /api/calendar/events 获取日程列表
GET /api/calendar/events/{id} 获取日程详情
POST /api/calendar/events 创建日程
PUT /api/calendar/events/{id} 更新日程
PUT /api/calendar/events/{id}/reschedule 重新安排
POST /api/calendar/events/{id}/attendees 添加参会人
DELETE /api/calendar/events/{id}/attendees/{attendeeId} 移除参会人
DELETE /api/calendar/events/{id} 删除日程

仪表盘 (Dashboard) - 5 个端点

方法 路径 描述
GET /api/dashboard/stats 统计数据
GET /api/dashboard/visits 访问趋势
GET /api/dashboard/sales 销售数据
GET /api/dashboard/pie 饼图数据
GET /api/dashboard/tasks 待办任务

认证机制

JWT 双令牌

Access Token:  24 小时有效期,用于 API 请求
Refresh Token: 7 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

java
// 前端自动刷新示例
@Component
public class JwtTokenInterceptor {
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);

        // 401 自动刷新
        if (response.code() == 401) {
            String newToken = refreshToken(refreshToken);
            Request newRequest = request.newBuilder()
                .header("Authorization", "Bearer " + newToken)
                .build();
            return chain.proceed(newRequest);
        }

        return response;
    }
}

权限系统

角色定义

角色 说明 权限
super_admin 超级管理员 * (所有权限)
admin 管理员 users:*, documents:*, roles:*
user 普通用户 documents:view, files:view
guest 访客 dashboard:view

权限格式

{resource}:{action}

示例:
- users:view      # 查看用户
- users:create    # 创建用户
- users:*         # 用户所有操作
- *               # 所有权限

权限检查

java
@RestController
@RequestMapping("/api/users")
public class UserController {

    @PreAuthorize("hasPermission('users:view')")
    @GetMapping
    public Page<UserDTO> getUsers(Pageable pageable) {
        return userService.findAll(pageable);
    }

    @PreAuthorize("hasPermission('users:create')")
    @PostMapping
    public UserDTO createUser(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }
}

错误处理

错误响应格式

json
{
  "timestamp": "2025-12-04T12:00:00.000Z",
  "status": 400,
  "error": "Bad Request",
  "message": "请求参数验证失败",
  "path": "/api/users",
  "details": [
    { "field": "email", "message": "邮箱格式不正确" }
  ]
}

错误码

状态码 错误码 说明
400 Bad Request 参数验证失败
401 Unauthorized 未授权
403 Forbidden 无权限
404 Not Found 资源不存在
409 Conflict 资源冲突
422 Unprocessable Entity 业务逻辑错误
429 Too Many Requests 请求频率超限
500 Internal Server Error 服务器错误

数据库模型

Spring Data JPA 实体包含 17 个模型

java
// 用户实体
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String name;

    private String password;
    private String avatar;

    @Enumerated(EnumType.STRING)
    private UserStatus status = UserStatus.ACTIVE;

    @ManyToMany
    @JoinTable(name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles;

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

// 角色实体
@Entity
@Table(name = "roles")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String name;

    private String description;

    @ManyToMany
    @JoinTable(name = "role_permissions")
    private Set<Permission> permissions;
}

// 权限实体
@Entity
@Table(name = "permissions")
public class Permission {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String name;  // 格式: "users:create", "users:*", "*"

    private String description;
}

完整实体列表

  • User,Role,Permission (RBAC 核心)
  • Team,TeamMember (团队管理)
  • Document,File,Folder (文档/文件)
  • CalendarEvent,EventAttendee (日历)
  • Notification,Message,Conversation (通知/消息)
  • Dashboard,Visit,Sale (仪表盘统计)

环境变量

变量名 说明 默认值
SPRING_PROFILES_ACTIVE 运行环境 development
PORT 服务端口 8080
DATABASE_URL 数据库连接 jdbc:postgresql://localhost:5432/halolight_db
DATABASE_USERNAME 数据库用户名 postgres
DATABASE_PASSWORD 数据库密码 -
JWT_SECRET JWT 密钥(至少 32 字符) -
JWT_EXPIRATION AccessToken 过期时间(毫秒) 86400000 (24h)
JWT_REFRESH_EXPIRATION RefreshToken 过期时间(毫秒) 604800000 (7d)
CORS_ALLOWED_ORIGINS CORS 允许源 http://localhost:3000

使用方式

yaml
# application.yml
spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}

jwt:
  secret: ${JWT_SECRET}
  expiration: ${JWT_EXPIRATION:86400000}
  refreshExpiration: ${JWT_REFRESH_EXPIRATION:604800000}

常用命令

bash
# 开发
./mvnw spring-boot:run                # 启动开发服务器
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev  # 指定环境

# 构建
./mvnw clean package                  # 构建 JAR 包
./mvnw clean package -DskipTests      # 跳过测试构建
./mvnw clean install                  # 安装到本地仓库

# 测试
./mvnw test                           # 运行所有测试
./mvnw test -Dtest=UserServiceTest    # 运行指定测试
./mvnw verify                         # 运行集成测试
./mvnw test jacoco:report             # 生成覆盖率报告

# 数据库
./mvnw flyway:migrate                 # 运行迁移(如使用 Flyway)
./mvnw liquibase:update               # 更新 Schema(如使用 Liquibase)

# 代码质量
./mvnw checkstyle:check               # 代码风格检查
./mvnw spotbugs:check                 # 静态分析

部署

Docker

bash
docker build -t halolight-api-java .
docker run -p 8080:8080 --env-file .env halolight-api-java

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=production
      - DATABASE_URL=jdbc:postgresql://db:5432/halolight
      - DATABASE_USERNAME=postgres
      - DATABASE_PASSWORD=${DB_PASSWORD}
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

生产环境配置

env
SPRING_PROFILES_ACTIVE=production
DATABASE_URL=jdbc:postgresql://prod-db.example.com:5432/halolight
DATABASE_USERNAME=halolight_user
DATABASE_PASSWORD=your-production-password
JWT_SECRET=your-production-secret-min-32-chars
CORS_ALLOWED_ORIGINS=https://halolight.h7ml.cn

测试

运行测试

bash
./mvnw test                           # 运行单元测试
./mvnw test jacoco:report             # 生成覆盖率报告
./mvnw verify                         # 运行集成测试

测试示例

java
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void testLogin() throws Exception {
        LoginRequest request = new LoginRequest();
        request.setEmail("[email protected]");
        request.setPassword("123456");

        mockMvc.perform(post("/api/auth/login")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.accessToken").exists())
                .andExpect(jsonPath("$.refreshToken").exists());
    }

    @Test
    @WithMockUser(authorities = {"users:view"})
    public void testGetUsers() throws Exception {
        mockMvc.perform(get("/api/users")
                .param("page", "0")
                .param("size", "10"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content").isArray());
    }
}

性能指标

基准测试

指标 数值 说明
请求吞吐量 ~3000 QPS 简单查询,4 核 8GB
平均响应时间 15-30ms P50,数据库查询
P95 响应时间 50-100ms 包含复杂查询
内存占用 256-512 MB 稳定运行状态
CPU 使用率 10-30% 中等负载

性能测试

bash
# 使用 Apache Bench
ab -n 10000 -c 100 -H "Authorization: Bearer TOKEN" \
  http://localhost:8080/api/users

# 使用 wrk
wrk -t4 -c100 -d30s -H "Authorization: Bearer TOKEN" \
  http://localhost:8080/api/users

可观测性

日志系统

java
// Logback 配置
@Slf4j
@RestController
public class UserController {

    @GetMapping("/api/users/{id}")
    public UserDTO getUser(@PathVariable Long id) {
        log.info("Fetching user with id: {}", id);
        try {
            return userService.findById(id);
        } catch (Exception e) {
            log.error("Error fetching user {}: {}", id, e.getMessage(), e);
            throw e;
        }
    }
}

健康检查

java
@Component
public class CustomHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        // 检查数据库连接
        boolean dbUp = checkDatabase();

        if (dbUp) {
            return Health.up()
                .withDetail("database", "Available")
                .build();
        }

        return Health.down()
            .withDetail("database", "Unavailable")
            .build();
    }
}

端点GET /actuator/health

json
{
  "status": "UP",
  "components": {
    "db": { "status": "UP" },
    "diskSpace": { "status": "UP" }
  }
}

监控指标

yaml
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

Prometheus 端点GET /actuator/prometheus

常见问题

Q:如何修改 JWT 过期时间?

A:在 .envapplication.yml 中配置:

env
JWT_EXPIRATION=3600000          # 1 小时(毫秒)
JWT_REFRESH_EXPIRATION=86400000  # 1 天(毫秒)

Q:如何启用 HTTPS?

A:生成证书并配置 Spring Boot:

yaml
# application.yml
server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: your-password
    key-store-type: PKCS12
bash
# 生成自签名证书(开发环境)
keytool -genkeypair -alias halolight -keyalg RSA -keysize 2048 \
  -storetype PKCS12 -keystore keystore.p12 -validity 365

Q:如何处理数据库连接池配置?

A:使用 HikariCP (Spring Boot 默认):

yaml
spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

Q:如何实现分页和排序?

A:使用 Spring Data JPA Pageable:

java
@GetMapping("/api/users")
public Page<UserDTO> getUsers(
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "10") int size,
    @RequestParam(defaultValue = "id,desc") String sort
) {
    String[] sortParams = sort.split(",");
    Sort.Direction direction = sortParams.length > 1 &&
        sortParams[1].equals("desc") ? Sort.Direction.DESC : Sort.Direction.ASC;

    Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortParams[0]));
    return userService.findAll(pageable);
}

开发工具

推荐插件/工具

  • IntelliJ IDEA - 官方推荐 IDE,集成 Spring Boot 支持
  • Spring Boot DevTools - 热重载,自动重启
  • Lombok - 减少样板代码
  • MapStruct - DTO 映射生成
  • JaCoCo - 代码覆盖率工具
  • Postman/Insomnia - API 测试工具

与其他后端对比

特性 Spring Boot NestJS FastAPI Go Fiber
语言 Java TypeScript Python Go
ORM JPA/Hibernate Prisma SQLAlchemy GORM
性能 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
学习曲线 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
企业级 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
生态系统 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
社区支持 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐

相关链接

]]>
<![CDATA[TypeScript NestJS 后端 API]]> https://halolight.docs.h7ml.cn/guide/api-nestjs https://halolight.docs.h7ml.cn/guide/api-nestjs Fri, 19 Dec 2025 04:56:41 GMT TypeScript NestJS 后端 API

HaloLight NestJS 后端 API 基于 NestJS 11 构建,提供企业级后端服务和完整的 RBAC 权限系统。

API 文档https://halolight-api-nestjs.h7ml.cn/docs

GitHubhttps://github.com/halolight/halolight-api-nestjs

特性

  • 🔐 JWT 双令牌 - Access Token + Refresh Token,自动续期
  • 🛡️ RBAC 权限 - 基于角色的访问控制,通配符匹配
  • 📡 RESTful API - 标准化接口设计,OpenAPI 文档
  • 🗄️ Prisma ORM - 类型安全的数据库操作
  • 数据验证 - 请求参数校验,错误处理
  • 📊 日志系统 - 请求日志,错误追踪
  • 🐳 Docker 支持 - 容器化部署

技术栈

技术 版本 说明
TypeScript 5.7 运行时
NestJS 11 Web 框架
Prisma 5 数据库 ORM
PostgreSQL 16 数据存储
class-validator - 数据验证
JWT - 身份认证
Swagger - API 文档

快速开始

环境要求

  • Node.js >= 18
  • pnpm >= 8
  • PostgreSQL (可选,默认 SQLite)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-api-nestjs.git
cd halolight-api-nestjs

# 安装依赖
pnpm install

环境变量

bash
cp .env.example .env
env
# 数据库
DATABASE_URL=postgresql://user:password@localhost:5432/halolight_db

# JWT 密钥
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# 服务配置
PORT=3000
NODE_ENV=development

数据库初始化

bash
pnpm prisma:generate
pnpm prisma:migrate
pnpm prisma:seed

启动服务

bash
# 开发模式
pnpm dev

# 生产模式
pnpm build
pnpm start:prod

访问 http://localhost:3000

项目结构

halolight-api-nestjs/
├── src/
│   ├── common/                  # 共享模块
│   ├── configs/                 # 配置模块
│   ├── infrastructure/          # 基础设施层
│   ├── modules/                 # 业务模块(12 个)
│   │   ├── auth/                # 认证模块
│   │   ├── users/               # 用户管理
│   │   ├── roles/               # 角色管理
│   │   ├── permissions/         # 权限管理
│   │   ├── teams/               # 团队管理
│   │   ├── documents/           # 文档管理
│   │   ├── files/               # 文件管理
│   │   ├── folders/             # 文件夹管理
│   │   ├── calendar/            # 日历管理
│   │   ├── notifications/       # 通知管理
│   │   ├── messages/            # 消息管理
│   │   └── dashboard/           # 仪表盘统计
│   ├── app.controller.ts        # 根控制器
│   ├── app.service.ts           # 根服务
│   ├── app.module.ts            # 根模块
│   └── main.ts                  # 应用入口
├── prisma/
│   ├── schema.prisma            # 数据库模型定义
│   └── migrations/              # 数据库迁移历史
├── test/                        # 测试文件
├── Dockerfile                   # Docker 配置
├── docker-compose.yml
└── package.json

API 模块

认证相关端点

方法 路径 描述 权限
POST /api/auth/login 用户登录 公开
POST /api/auth/register 用户注册 公开
POST /api/auth/refresh 刷新令牌 公开
POST /api/auth/logout 退出登录 需认证
POST /api/auth/forgot-password 忘记密码 公开
POST /api/auth/reset-password 重置密码 公开

用户管理端点

方法 路径 描述 权限
GET /api/users 获取用户列表 users:view
GET /api/users/:id 获取用户详情 users:view
POST /api/users 创建用户 users:create
PUT /api/users/:id 更新用户 users:update
DELETE /api/users/:id 删除用户 users:delete
GET /api/users/me 获取当前用户 需认证

完整端点清单

文档管理 (Documents) - 11 个端点

方法 路径 描述
GET /api/documents 获取文档列表
GET /api/documents/:id 获取文档详情
POST /api/documents 创建文档
PUT /api/documents/:id 更新文档
DELETE /api/documents/:id 删除文档

文件管理 (Files) - 14 个端点

方法 路径 描述
GET /api/files 获取文件列表
GET /api/files/:id 获取文件详情
POST /api/files/upload 上传文件
PUT /api/files/:id 更新文件信息
DELETE /api/files/:id 删除文件

消息管理 (Messages) - 5 个端点

方法 路径 描述
GET /api/messages 获取消息列表
GET /api/messages/:id 获取消息详情
POST /api/messages 发送消息
PUT /api/messages/:id/read 标记已读
DELETE /api/messages/:id 删除消息

通知管理 (Notifications) - 5 个端点

方法 路径 描述
GET /api/notifications 获取通知列表
PUT /api/notifications/:id/read 标记已读
PUT /api/notifications/read-all 全部已读
DELETE /api/notifications/:id 删除通知

日历管理 (Calendar) - 9 个端点

方法 路径 描述
GET /api/calendar/events 获取日程列表
GET /api/calendar/events/:id 获取日程详情
POST /api/calendar/events 创建日程
PUT /api/calendar/events/:id 更新日程
DELETE /api/calendar/events/:id 删除日程

仪表盘 (Dashboard) - 9 个端点

方法 路径 描述
GET /api/dashboard/stats 统计数据
GET /api/dashboard/visits 访问趋势
GET /api/dashboard/sales 销售数据
GET /api/dashboard/pie 饼图数据
GET /api/dashboard/tasks 待办任务
GET /api/dashboard/overview 系统概览

认证机制

JWT 双令牌

Access Token:  15 分钟有效期,用于 API 请求
Refresh Token: 7 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

typescript
// 刷新令牌示例
const response = await fetch('/api/auth/refresh', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ refreshToken })
});

const { accessToken, refreshToken: newRefreshToken } = await response.json();

权限系统

角色定义

角色 说明 权限
super_admin 超级管理员 * (所有权限)
admin 管理员 users:*, documents:*, ...
user 普通用户 documents:view, files:view, ...
guest 访客 dashboard:view

权限格式

{resource}:{action}

示例:
- users:view      # 查看用户
- users:create    # 创建用户
- users:*         # 用户所有操作
- *               # 所有权限

错误处理

错误响应格式

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" }
    ]
  }
}

错误码

状态码 错误码 说明
400 VALIDATION_ERROR 参数验证失败
401 UNAUTHORIZED 未授权
403 FORBIDDEN 无权限
404 NOT_FOUND 资源不存在
409 CONFLICT 资源冲突
500 INTERNAL_ERROR 服务器错误

常用命令

bash
# 开发
pnpm dev              # 启动开发服务器
pnpm start:debug      # 调试模式

# 构建
pnpm build            # 构建生产版本
pnpm start:prod       # 运行生产版本

# 测试
pnpm test             # 运行单元测试
pnpm test:e2e         # 运行 E2E 测试
pnpm test:cov         # 生成覆盖率报告

# 数据库
pnpm prisma:generate  # 生成 Prisma Client
pnpm prisma:migrate   # 运行迁移
pnpm prisma:studio    # Prisma Studio GUI
pnpm prisma:seed      # 运行种子数据

# 代码质量
pnpm lint             # ESLint 检查
pnpm lint:fix         # 自动修复
pnpm format           # Prettier 格式化

部署

Docker

bash
docker build -t halolight-api-nestjs .
docker run -p 3000:3000 halolight-api-nestjs

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

生产环境配置

env
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=your-production-secret

测试

运行测试

bash
pnpm test             # 单元测试
pnpm test:e2e         # E2E 测试
pnpm test:cov         # 覆盖率报告

测试示例

typescript
describe('AuthController', () => {
  it('should login user', async () => {
    const response = await request(app.getHttpServer())
      .post('/api/auth/login')
      .send({ email: '[email protected]', password: 'password' })
      .expect(200);

    expect(response.body).toHaveProperty('accessToken');
  });
});

性能指标

基准测试

指标 数值 说明
请求吞吐量 5000+ QPS 单核 CPU
平均响应时间 <50ms 简单查询
内存占用 ~150MB 启动后
CPU 使用率 <30% 正常负载

可观测性

日志系统

typescript
// 日志配置
import { WinstonModule } from 'nest-winston';

WinstonModule.forRoot({
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

健康检查

typescript
// GET /health
{
  "status": "ok",
  "info": {
    "database": { "status": "up" },
    "redis": { "status": "up" }
  }
}

监控指标

typescript
// Prometheus 指标端点
// GET /metrics
http_requests_total{method="GET",status="200"} 1234
http_request_duration_seconds{quantile="0.99"} 0.052

常见问题

Q:如何配置数据库连接?

A:在 .env 文件中设置 DATABASE_URL

env
DATABASE_URL="postgresql://user:password@localhost:5432/halolight"

Q:如何处理文件上传?

A:使用 @nestjs/platform-expressFileInterceptor

typescript
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
  return { filename: file.originalname };
}

开发工具

推荐插件/工具

  • Prisma Studio - 数据库可视化管理
  • Swagger UI - API 文档和测试
  • Postman - API 调试工具
  • NestJS CLI - 代码生成工具

与其他后端对比

特性 NestJS FastAPI Spring Boot Laravel
语言 TypeScript Python Java PHP
ORM Prisma SQLAlchemy JPA Eloquent
性能 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
学习曲线 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐

相关链接

]]>
<![CDATA[Node.js Express 后端 API]]> https://halolight.docs.h7ml.cn/guide/api-node https://halolight.docs.h7ml.cn/guide/api-node Fri, 19 Dec 2025 04:56:41 GMT Node.js Express 后端 API

HaloLight Node.js 后端 API 基于 Express 5 + TypeScript + Prisma 构建,提供企业级 RESTful API 服务。

API 文档https://halolight-api-node.h7ml.cn/docs

GitHubhttps://github.com/halolight/halolight-api-node

特性

  • 🔐 JWT 双令牌 - Access Token + Refresh Token,自动续期
  • 🛡️ RBAC 权限 - 基于角色的访问控制,通配符匹配
  • 📡 RESTful API - 标准化接口设计,OpenAPI 文档
  • 🗄️ Prisma ORM - 类型安全的数据库操作
  • 数据验证 - Zod 请求参数校验,错误处理
  • 📊 日志系统 - Pino 日志记录,请求追踪
  • 🐳 Docker 支持 - 容器化部署
  • 🎯 12 个业务模块 - 90+ RESTful 端点

技术栈

技术 版本 说明
Node.js 20+ JavaScript 运行时
Express 5.x Web 框架
Prisma 6.x 数据库 ORM
PostgreSQL 16 数据存储
Zod 3.x 数据验证
JWT 9.x 身份认证
Pino 9.x 日志系统
Swagger UI 5.x API 文档

快速开始

环境要求

  • Node.js >= 20
  • pnpm >= 9
  • PostgreSQL (可选,默认 SQLite)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-api-node.git
cd halolight-api-node

# 安装依赖
pnpm install

环境变量

bash
cp .env.example .env
env
# 数据库
DATABASE_URL="postgresql://user:password@localhost:5432/halolight?schema=public"

# JWT 密钥(必须 ≥32 字符)
JWT_SECRET="your-super-secret-jwt-key-minimum-32-characters-long"
JWT_EXPIRES_IN=7d
REFRESH_TOKEN_SECRET="your-refresh-secret-key-minimum-32-characters-long"
REFRESH_TOKEN_EXPIRES_IN=30d

# 服务配置
PORT=3001
NODE_ENV=development

# CORS
CORS_ORIGIN="http://localhost:3000"

数据库初始化

bash
# 生成 Prisma Client
pnpm db:generate

# 推送数据库变更
pnpm db:push

# 填充种子数据(可选)
pnpm db:seed

启动服务

bash
# 开发模式
pnpm dev

# 生产模式
pnpm build
pnpm start

访问 http://localhost:3001

项目结构

halolight-api-node/
├── src/
│   ├── routes/               # 控制器/路由处理
│   │   ├── auth.ts          # 认证路由
│   │   ├── users.ts         # 用户管理
│   │   ├── roles.ts         # 角色管理
│   │   ├── permissions.ts   # 权限管理
│   │   ├── teams.ts         # 团队管理
│   │   ├── documents.ts     # 文档管理
│   │   ├── files.ts         # 文件管理
│   │   ├── folders.ts       # 文件夹管理
│   │   ├── calendar.ts      # 日历事件
│   │   ├── notifications.ts # 通知管理
│   │   ├── messages.ts      # 消息管理
│   │   └── dashboard.ts     # 仪表盘统计
│   ├── services/            # 业务逻辑层
│   ├── middleware/          # 中间件
│   │   ├── auth.ts          # JWT 认证 + RBAC
│   │   ├── validate.ts      # Zod 请求验证
│   │   └── error.ts         # 全局错误处理
│   ├── utils/               # 工具函数
│   ├── config/              # 配置文件
│   │   ├── env.ts           # 环境变量
│   │   └── swagger.ts       # Swagger 配置
│   └── index.ts             # 应用入口
├── prisma/                  # 数据库迁移/Schema
│   └── schema.prisma        # 数据库模型(17+ 模型)
├── tests/                   # 测试文件
├── Dockerfile               # Docker 配置
├── docker-compose.yml
└── package.json

API 模块

认证相关端点

方法 路径 描述 权限
POST /api/auth/login 用户登录 公开
POST /api/auth/register 用户注册 公开
POST /api/auth/refresh 刷新令牌 公开
POST /api/auth/logout 退出登录 需认证
POST /api/auth/forgot-password 忘记密码 公开
POST /api/auth/reset-password 重置密码 公开

用户管理端点

方法 路径 描述 权限
GET /api/users 获取用户列表 users:view
GET /api/users/:id 获取用户详情 users:view
POST /api/users 创建用户 users:create
PUT /api/users/:id 更新用户 users:update
DELETE /api/users/:id 删除用户 users:delete
GET /api/users/me 获取当前用户 需认证

完整端点清单

角色管理 (Roles) - 6 个端点

方法 路径 描述
GET /api/roles 获取角色列表
GET /api/roles/:id 获取角色详情
POST /api/roles 创建角色
PUT /api/roles/:id 更新角色
DELETE /api/roles/:id 删除角色
PUT /api/roles/:id/permissions 分配权限

权限管理 (Permissions) - 4 个端点

方法 路径 描述
GET /api/permissions 获取权限列表
GET /api/permissions/:id 获取权限详情
POST /api/permissions 创建权限
DELETE /api/permissions/:id 删除权限

团队管理 (Teams) - 7 个端点

方法 路径 描述
GET /api/teams 获取团队列表
GET /api/teams/:id 获取团队详情
POST /api/teams 创建团队
PUT /api/teams/:id 更新团队
DELETE /api/teams/:id 删除团队
POST /api/teams/:id/members 添加成员
DELETE /api/teams/:id/members/:userId 移除成员

文档管理 (Documents) - 11 个端点

方法 路径 描述
GET /api/documents 获取文档列表
GET /api/documents/:id 获取文档详情
POST /api/documents 创建文档
PUT /api/documents/:id 更新文档
DELETE /api/documents/:id 删除文档
POST /api/documents/:id/share 分享文档
DELETE /api/documents/:id/share/:shareId 取消分享
POST /api/documents/:id/tags 添加标签
DELETE /api/documents/:id/tags/:tagId 移除标签
POST /api/documents/:id/move 移动文档
POST /api/documents/:id/copy 复制文档

文件管理 (Files) - 14 个端点

方法 路径 描述
GET /api/files 获取文件列表
GET /api/files/:id 获取文件详情
POST /api/files/upload 上传文件
GET /api/files/:id/download 下载文件
PUT /api/files/:id 更新文件信息
DELETE /api/files/:id 删除文件
POST /api/files/:id/move 移动文件
POST /api/files/:id/copy 复制文件
POST /api/files/:id/share 分享文件
GET /api/files/:id/versions 获取版本历史
GET /api/files/storage 获取存储信息
POST /api/files/batch-delete 批量删除
POST /api/files/batch-move 批量移动
POST /api/files/search 搜索文件

文件夹管理 (Folders) - 5 个端点

方法 路径 描述
GET /api/folders 获取文件夹树
POST /api/folders 创建文件夹
PUT /api/folders/:id 更新文件夹
DELETE /api/folders/:id 删除文件夹
POST /api/folders/:id/move 移动文件夹

日历管理 (Calendar) - 9 个端点

方法 路径 描述
GET /api/calendar/events 获取日程列表
GET /api/calendar/events/:id 获取日程详情
POST /api/calendar/events 创建日程
PUT /api/calendar/events/:id 更新日程
DELETE /api/calendar/events/:id 删除日程
POST /api/calendar/events/:id/attendees 添加参会人
DELETE /api/calendar/events/:id/attendees/:userId 移除参会人
POST /api/calendar/events/:id/reminder 设置提醒
GET /api/calendar/events/upcoming 获取即将到来的事件

通知管理 (Notifications) - 5 个端点

方法 路径 描述
GET /api/notifications 获取通知列表
GET /api/notifications/:id 获取通知详情
PUT /api/notifications/:id/read 标记已读
PUT /api/notifications/read-all 全部已读
DELETE /api/notifications/:id 删除通知

消息管理 (Messages) - 5 个端点

方法 路径 描述
GET /api/messages/conversations 获取会话列表
GET /api/messages/conversations/:id 获取会话详情
POST /api/messages 发送消息
PUT /api/messages/:id/read 标记已读
DELETE /api/messages/:id 删除消息

仪表盘 (Dashboard) - 9 个端点

方法 路径 描述
GET /api/dashboard/stats 统计数据
GET /api/dashboard/visits 访问趋势
GET /api/dashboard/sales 销售数据
GET /api/dashboard/pie 饼图数据
GET /api/dashboard/tasks 待办任务
GET /api/dashboard/calendar 今日日程
GET /api/dashboard/activities 最近活动
GET /api/dashboard/notifications 未读通知
GET /api/dashboard/users 用户统计

认证机制

JWT 双令牌

Access Token:  7 天有效期,用于 API 请求
Refresh Token: 30 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

typescript
// POST /api/auth/refresh
const response = await fetch('/api/auth/refresh', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ refreshToken: 'your_refresh_token' })
});

const { accessToken, refreshToken } = await response.json();
// 更新本地存储的令牌
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);

权限系统

角色定义

角色 说明 权限
super_admin 超级管理员 * (所有权限)
admin 管理员 users:*, documents:*, files:*, teams:*
user 普通用户 documents:view, files:view, calendar:*
guest 访客 dashboard:view

权限格式

{resource}:{action}

示例:
- users:view      # 查看用户
- users:create    # 创建用户
- users:*         # 用户所有操作
- *               # 所有权限

权限验证示例

typescript
// 在路由中使用权限中间件
import { requireAuth, requirePermission } from './middleware/auth';

// 需要认证
router.get('/api/users/me', requireAuth, getUserProfile);

// 需要特定权限
router.post('/api/users', requireAuth, requirePermission('users:create'), createUser);

// 需要多个权限之一
router.put('/api/users/:id', requireAuth, requirePermission(['users:update', 'users:*']), updateUser);

错误处理

错误响应格式

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" }
    ]
  }
}

错误码

状态码 错误码 说明
400 VALIDATION_ERROR 参数验证失败
401 UNAUTHORIZED 未授权
403 FORBIDDEN 无权限
404 NOT_FOUND 资源不存在
409 CONFLICT 资源冲突
500 INTERNAL_ERROR 服务器错误

常用命令

bash
# 开发
pnpm dev              # 启动开发服务器(热重载)
pnpm build            # TypeScript 编译
pnpm start            # 启动生产服务器

# 测试
pnpm test             # 运行测试
pnpm test:coverage    # 测试覆盖率

# 数据库
pnpm db:generate      # 生成 Prisma Client
pnpm db:push          # 推送数据库变更
pnpm db:migrate       # 运行迁移
pnpm db:studio        # Prisma Studio(数据库 GUI)
pnpm db:seed          # 填充种子数据

# 代码质量
pnpm lint             # ESLint 检查
pnpm lint:fix         # 自动修复
pnpm format           # Prettier 格式化

部署

Docker

bash
docker build -t halolight-api-node .
docker run -p 3001:3001 halolight-api-node

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3001:3001"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
      - REFRESH_TOKEN_SECRET=${REFRESH_TOKEN_SECRET}
    restart: unless-stopped
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

生产环境配置

env
NODE_ENV=production
PORT=3001
DATABASE_URL=postgresql://user:pass@host:5432/halolight
JWT_SECRET=your-production-secret-minimum-32-characters
REFRESH_TOKEN_SECRET=your-refresh-secret-minimum-32-characters
CORS_ORIGIN=https://yourdomain.com

测试

运行测试

bash
# 运行所有测试
pnpm test

# 运行测试覆盖率
pnpm test:coverage

# 监听模式
pnpm test:watch

测试示例

typescript
// tests/auth.test.ts
import request from 'supertest';
import app from '../src/index';

describe('Authentication', () => {
  it('should login successfully', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({
        email: '[email protected]',
        password: 'password123'
      });

    expect(response.status).toBe(200);
    expect(response.body.success).toBe(true);
    expect(response.body.data).toHaveProperty('accessToken');
    expect(response.body.data).toHaveProperty('refreshToken');
  });

  it('should reject invalid credentials', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({
        email: '[email protected]',
        password: 'wrongpassword'
      });

    expect(response.status).toBe(401);
    expect(response.body.success).toBe(false);
  });
});

性能指标

基准测试

指标 数值 说明
请求吞吐量 ~8,000 req/s 单核,简单查询
平均响应时间 <10ms 本地数据库,无复杂查询
内存占用 ~80MB 启动后基础内存
CPU 使用率 <5% 空闲状态

性能优化建议

  • 使用连接池管理数据库连接
  • 启用数据库索引优化查询
  • 实现缓存策略 (Redis)
  • 使用 CDN 加速静态资源
  • 启用 Gzip 压缩

可观测性

日志系统

typescript
// 使用 Pino 日志记录
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'yyyy-mm-dd HH:MM:ss',
      ignore: 'pid,hostname'
    }
  }
});

// 记录请求日志
app.use((req, res, next) => {
  logger.info({
    method: req.method,
    url: req.url,
    ip: req.ip
  }, 'Incoming request');
  next();
});

健康检查

typescript
// GET /health
app.get('/health', async (req, res) => {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    database: 'connected'
  };

  try {
    // 检查数据库连接
    await prisma.$queryRaw`SELECT 1`;
  } catch (error) {
    health.status = 'unhealthy';
    health.database = 'disconnected';
    return res.status(503).json(health);
  }

  res.json(health);
});

监控指标

typescript
// 集成 Prometheus 指标
import promClient from 'prom-client';

const register = new promClient.Registry();

// 默认指标
promClient.collectDefaultMetrics({ register });

// 自定义指标
const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register]
});

// 暴露指标端点
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.send(await register.metrics());
});

常见问题

Q:如何在多服务间共享数据库?

A:配置相同的 DATABASE_URL 并确保使用相同的 Prisma Schema。

env
# 所有服务使用相同的数据库连接
DATABASE_URL="postgresql://user:pass@shared-db:5432/halolight"
bash
# 确保 Schema 一致
pnpm db:push

Q:JWT 令牌过期如何自动刷新?

A:在前端拦截器中检测 401 错误,自动调用刷新接口。

typescript
// Axios 拦截器示例
axios.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const { data } = await axios.post('/api/auth/refresh', { refreshToken });

        localStorage.setItem('accessToken', data.accessToken);
        localStorage.setItem('refreshToken', data.refreshToken);

        originalRequest.headers['Authorization'] = `Bearer ${data.accessToken}`;
        return axios(originalRequest);
      } catch (err) {
        // 刷新失败,跳转登录页
        window.location.href = '/login';
        return Promise.reject(err);
      }
    }

    return Promise.reject(error);
  }
);

Q:如何实现文件上传限制?

A:使用 multer 中间件配置文件大小和类型限制。

typescript
import multer from 'multer';

const upload = multer({
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
  },
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('不支持的文件类型'));
    }
  }
});

router.post('/api/files/upload', upload.single('file'), uploadFile);

Q:如何启用 HTTPS?

A:使用 Nginx 反向代理或在 Express 中配置 SSL 证书。

typescript
// Express 中启用 HTTPS
import https from 'https';
import fs from 'fs';

const options = {
  key: fs.readFileSync('path/to/private-key.pem'),
  cert: fs.readFileSync('path/to/certificate.pem')
};

https.createServer(options, app).listen(443, () => {
  console.log('HTTPS server running on port 443');
});

开发工具

推荐插件/工具

  • Prisma Studio - 可视化数据库管理工具 (pnpm db:studio)
  • Postman - API 测试工具 (可导入 Swagger 文档)
  • VSCode Extension Pack - ESLint + Prettier + TypeScript
  • Docker Desktop - 容器管理
  • pgAdmin - PostgreSQL 数据库管理

VSCode 配置

json
// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.tsdk": "node_modules/typescript/lib"
}

与其他后端对比

特性 Express NestJS Fastify Koa
语言 JavaScript/TypeScript TypeScript JavaScript/TypeScript JavaScript/TypeScript
性能 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
学习曲线 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
生态系统 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
内置功能 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
社区支持 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐

为什么选择 Express?

  • 成熟稳定 - 超过 10 年的生产验证
  • 灵活性高 - 最小化框架,可自由组合中间件
  • 生态丰富 - 海量第三方插件和工具
  • 学习成本低 - 简单易懂,适合快速上手
  • 社区活跃 - 大量文档和问题解答

相关链接

]]>
<![CDATA[PHP Laravel 后端 API]]> https://halolight.docs.h7ml.cn/guide/api-php https://halolight.docs.h7ml.cn/guide/api-php Fri, 19 Dec 2025 04:56:41 GMT PHP Laravel 后端 API

HaloLight PHP 企业级后端 API 服务,基于 Laravel 11 + PostgreSQL + Redis 构建,提供完整的 RESTful API。

API 文档https://halolight-api-php.h7ml.cn/docs

GitHubhttps://github.com/halolight/halolight-api-php

特性

  • 🔐 JWT 双令牌 - Access Token + Refresh Token,自动续期
  • 🛡️ RBAC 权限 - 基于角色的访问控制,通配符匹配
  • 📡 RESTful API - 标准化接口设计,OpenAPI 文档
  • 🗄️ Eloquent ORM - 优雅的 ActiveRecord 数据库操作
  • 数据验证 - Form Request 请求参数校验
  • 📊 日志系统 - 请求日志,错误追踪
  • 🐳 Docker 支持 - 容器化部署

技术栈

技术 版本 说明
PHP 8.2+ 运行时
Laravel 11.x Web 框架
Eloquent 11.x 数据库 ORM
PostgreSQL 16 数据存储
Redis 7 缓存/队列
Form Request 11.x 数据验证
JWT tymon/jwt-auth 身份认证
L5-Swagger 8.x API 文档

快速开始

环境要求

  • PHP >= 8.2
  • Composer >= 2.0
  • PostgreSQL 16 (可选,默认 SQLite)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-api-php.git
cd halolight-api-php

# 安装依赖
composer install

环境变量

bash
cp .env.example .env
env
# 数据库
DB_CONNECTION=pgsql
DB_HOST=localhost
DB_PORT=5432
DB_DATABASE=halolight
DB_USERNAME=postgres
DB_PASSWORD=your_password

# JWT 密钥
JWT_SECRET=your-super-secret-key-min-32-chars
JWT_TTL=10080  # 7天,单位:分钟

# 服务配置
APP_PORT=8080
APP_ENV=development
APP_DEBUG=true

数据库初始化

bash
# 生成应用密钥
php artisan key:generate

# 运行迁移
php artisan migrate

# 填充种子数据
php artisan db:seed

启动服务

bash
# 开发模式
php artisan serve --port=8080

# 生产模式
php artisan optimize
php artisan serve --port=8080 --env=production

访问 http://localhost:8080

项目结构

halolight-api-php/
├── app/
│   ├── Http/
│   │   ├── Controllers/     # 控制器/路由处理
│   │   ├── Middleware/      # 中间件
│   │   └── Requests/        # 请求验证
│   ├── Services/            # 业务逻辑层
│   ├── Models/              # 数据模型
│   ├── Enums/               # 枚举类型
│   └── Providers/           # 服务提供者
├── database/
│   ├── migrations/          # 数据库迁移
│   └── seeders/             # 种子数据
├── tests/                   # 测试文件
├── Dockerfile               # Docker 配置
├── docker-compose.yml
└── composer.json

API 模块

认证相关端点

方法 路径 描述 权限
POST /api/auth/login 用户登录 公开
POST /api/auth/register 用户注册 公开
POST /api/auth/refresh 刷新令牌 公开
POST /api/auth/logout 退出登录 需认证
POST /api/auth/forgot-password 忘记密码 公开
POST /api/auth/reset-password 重置密码 公开
GET /api/auth/me 获取当前用户 需认证

用户管理端点

方法 路径 描述 权限
GET /api/users 获取用户列表 users:view
GET /api/users/:id 获取用户详情 users:view
POST /api/users 创建用户 users:create
PUT /api/users/:id 更新用户 users:update
DELETE /api/users/:id 删除用户 users:delete
GET /api/users/me 获取当前用户 需认证
PATCH /api/users/:id/status 更新用户状态 users:update

完整端点清单

角色管理 (Roles) - 6 个端点

方法 路径 描述
GET /api/roles 获取角色列表
GET /api/roles/:id 获取角色详情
POST /api/roles 创建角色
PUT /api/roles/:id 更新角色
DELETE /api/roles/:id 删除角色
POST /api/roles/:id/permissions 分配权限

权限管理 (Permissions) - 4 个端点

方法 路径 描述
GET /api/permissions 获取权限列表
GET /api/permissions/:id 获取权限详情
POST /api/permissions 创建权限
DELETE /api/permissions/:id 删除权限

团队管理 (Teams) - 7 个端点

方法 路径 描述
GET /api/teams 获取团队列表
GET /api/teams/:id 获取团队详情
POST /api/teams 创建团队
PUT /api/teams/:id 更新团队
DELETE /api/teams/:id 删除团队
POST /api/teams/:id/members 添加成员
DELETE /api/teams/:id/members/:userId 移除成员

文档管理 (Documents) - 9 个端点

方法 路径 描述
GET /api/documents 获取文档列表
GET /api/documents/:id 获取文档详情
POST /api/documents 创建文档
PUT /api/documents/:id 更新文档
DELETE /api/documents/:id 删除文档
POST /api/documents/:id/share 分享文档
GET /api/documents/:id/versions 获取版本历史
POST /api/documents/:id/restore 恢复版本
POST /api/documents/:id/duplicate 复制文档

文件管理 (Files) - 9 个端点

方法 路径 描述
GET /api/files 获取文件列表
GET /api/files/:id 获取文件详情
POST /api/files/upload 上传文件
PUT /api/files/:id 更新文件信息
DELETE /api/files/:id 删除文件
GET /api/files/:id/download 下载文件
POST /api/files/:id/move 移动文件
POST /api/files/:id/copy 复制文件
GET /api/files/:id/preview 预览文件

文件夹管理 (Folders) - 5 个端点

方法 路径 描述
GET /api/folders 获取文件夹列表
GET /api/folders/:id 获取文件夹详情
POST /api/folders 创建文件夹
PUT /api/folders/:id 更新文件夹
DELETE /api/folders/:id 删除文件夹

消息管理 (Messages) - 5 个端点

方法 路径 描述
GET /api/messages 获取消息列表
GET /api/messages/:id 获取消息详情
POST /api/messages 发送消息
PUT /api/messages/:id/read 标记已读
DELETE /api/messages/:id 删除消息

通知管理 (Notifications) - 5 个端点

方法 路径 描述
GET /api/notifications 获取通知列表
GET /api/notifications/:id 获取通知详情
PUT /api/notifications/:id/read 标记已读
PUT /api/notifications/read-all 全部已读
DELETE /api/notifications/:id 删除通知

日历管理 (Calendar) - 8 个端点

方法 路径 描述
GET /api/calendar/events 获取日程列表
GET /api/calendar/events/:id 获取日程详情
POST /api/calendar/events 创建日程
PUT /api/calendar/events/:id 更新日程
DELETE /api/calendar/events/:id 删除日程
POST /api/calendar/events/:id/attendees 添加参会人
DELETE /api/calendar/events/:id/attendees/:userId 移除参会人
GET /api/calendar/availability 查询可用时间

仪表盘 (Dashboard) - 9 个端点

方法 路径 描述
GET /api/dashboard/stats 统计数据
GET /api/dashboard/visits 访问趋势
GET /api/dashboard/sales 销售数据
GET /api/dashboard/pie 饼图数据
GET /api/dashboard/tasks 待办任务
GET /api/dashboard/calendar 今日日程
GET /api/dashboard/notifications 最新通知
GET /api/dashboard/activity 活动日志
GET /api/dashboard/overview 概览数据

认证机制

JWT 双令牌

Access Token:  7 天有效期,用于 API 请求
Refresh Token: 30 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

php
<?php

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    public function refresh(Request $request)
    {
        $refreshToken = $request->input('refreshToken');

        // 验证 Refresh Token
        try {
            auth()->setToken($refreshToken)->authenticate();

            // 生成新的 Access Token
            $newAccessToken = auth()->refresh();

            return response()->json([
                'accessToken' => $newAccessToken,
                'refreshToken' => $refreshToken, // 可选:也可以生成新的 Refresh Token
                'expiresIn' => auth()->factory()->getTTL() * 60
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'error' => 'Invalid refresh token'
            ], 401);
        }
    }
}

权限系统

角色定义

角色 说明 权限
super_admin 超级管理员 * (所有权限)
admin 管理员 users:*, documents:*, files:*, teams:*
user 普通用户 documents:view, files:view, calendar:*
guest 访客 dashboard:view

权限格式

{resource}:{action}

示例:
- users:view      # 查看用户
- users:create    # 创建用户
- users:*         # 用户所有操作
- *               # 所有权限

错误处理

错误响应格式

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" }
    ]
  }
}

错误码

状态码 错误码 说明
400 VALIDATION_ERROR 参数验证失败
401 UNAUTHORIZED 未授权
403 FORBIDDEN 无权限
404 NOT_FOUND 资源不存在
409 CONFLICT 资源冲突
422 UNPROCESSABLE_ENTITY 无法处理的实体
500 INTERNAL_ERROR 服务器错误

常用命令

bash
# 开发
php artisan serve --port=8080    # 启动开发服务器
php artisan tinker                # 进入 REPL 环境

# 构建
php artisan optimize              # 优化应用
php artisan config:cache          # 缓存配置
php artisan route:cache           # 缓存路由

# 测试
php artisan test                  # 运行测试
php artisan test --coverage       # 运行测试并生成覆盖率报告

# 数据库
php artisan migrate               # 运行迁移
php artisan migrate:fresh --seed  # 重置并填充数据
php artisan db:seed               # 填充种子数据

# 代码质量
composer lint                     # Laravel Pint 代码风格检查
composer analyse                  # PHPStan 静态分析

部署

Docker

bash
docker build -t halolight-api-php .
docker run -p 8080:8080 halolight-api-php

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - APP_ENV=production
      - APP_DEBUG=false
      - DB_CONNECTION=pgsql
      - DB_HOST=db
      - DB_DATABASE=halolight
      - DB_USERNAME=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - REDIS_HOST=redis
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - db
      - redis
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

生产环境配置

env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://halolight-api-php.h7ml.cn

DB_CONNECTION=pgsql
DB_HOST=your-db-host
DB_DATABASE=halolight
DB_USERNAME=your-db-user
DB_PASSWORD=your-db-password

REDIS_HOST=your-redis-host
REDIS_PASSWORD=your-redis-password

JWT_SECRET=your-production-secret-min-32-chars
JWT_TTL=10080

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

测试

运行测试

bash
php artisan test
php artisan test --coverage

测试示例

php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class AuthTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_login_with_correct_credentials()
    {
        $user = User::factory()->create([
            'email' => '[email protected]',
            'password' => bcrypt('password123')
        ]);

        $response = $this->postJson('/api/auth/login', [
            'email' => '[email protected]',
            'password' => 'password123'
        ]);

        $response->assertStatus(200)
                 ->assertJsonStructure([
                     'accessToken',
                     'refreshToken',
                     'user'
                 ]);
    }

    public function test_user_cannot_login_with_incorrect_password()
    {
        $user = User::factory()->create([
            'email' => '[email protected]',
            'password' => bcrypt('password123')
        ]);

        $response = $this->postJson('/api/auth/login', [
            'email' => '[email protected]',
            'password' => 'wrong-password'
        ]);

        $response->assertStatus(401)
                 ->assertJson([
                     'error' => 'Unauthorized'
                 ]);
    }
}

性能指标

基准测试

指标 数值 说明
请求吞吐量 ~1,500 QPS 单核,使用 Swoole/Octane
平均响应时间 ~15ms 简单查询,PostgreSQL
内存占用 ~50MB 单进程,未开启缓存
CPU 使用率 ~40% 高负载,4核心

可观测性

日志系统

php
<?php

use Illuminate\Support\Facades\Log;

// 配置日志通道
// config/logging.php
return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['single', 'daily'],
        ],
        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'days' => 14,
        ],
    ],
];

// 使用日志
Log::info('User logged in', ['user_id' => $user->id]);
Log::error('Payment failed', ['error' => $exception->getMessage()]);

健康检查

php
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;

class HealthController extends Controller
{
    public function check()
    {
        $status = [
            'status' => 'healthy',
            'timestamp' => now()->toIso8601String(),
            'services' => []
        ];

        // 检查数据库
        try {
            DB::connection()->getPdo();
            $status['services']['database'] = 'healthy';
        } catch (\Exception $e) {
            $status['status'] = 'unhealthy';
            $status['services']['database'] = 'unhealthy';
        }

        // 检查 Redis
        try {
            Redis::ping();
            $status['services']['redis'] = 'healthy';
        } catch (\Exception $e) {
            $status['status'] = 'unhealthy';
            $status['services']['redis'] = 'unhealthy';
        }

        return response()->json($status);
    }
}

监控指标

php
<?php

namespace App\Http\Middleware;

use Illuminate\Support\Facades\Cache;

class MetricsMiddleware
{
    public function handle($request, $next)
    {
        $start = microtime(true);

        $response = $next($request);

        $duration = microtime(true) - $start;

        // 记录请求指标
        Cache::increment('metrics:requests_total');
        Cache::increment("metrics:requests_{$response->status()}");

        // 记录响应时间(可以使用 Prometheus 等工具)
        $this->recordDuration($request->path(), $duration);

        return $response;
    }

    private function recordDuration($path, $duration)
    {
        // 实现指标记录逻辑
    }
}

常见问题

Q:如何处理文件上传?

A:Laravel 提供了便捷的文件上传处理方式:

php
<?php

namespace App\Http\Controllers\Api\V1;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
    public function upload(Request $request)
    {
        $request->validate([
            'file' => 'required|file|max:10240', // 10MB
        ]);

        $file = $request->file('file');
        $path = $file->store('uploads', 'public');

        return response()->json([
            'path' => Storage::url($path),
            'name' => $file->getClientOriginalName(),
            'size' => $file->getSize(),
            'mime' => $file->getMimeType()
        ]);
    }
}

Q:如何实现数据库事务?

A:使用 Laravel 的 DB facade 或 Eloquent 模型:

php
<?php

use Illuminate\Support\Facades\DB;
use App\Models\Order;
use App\Models\Payment;

// 方式 1: DB facade
DB::transaction(function () {
    $order = Order::create([...]);
    $payment = Payment::create([
        'order_id' => $order->id,
        ...
    ]);
});

// 方式 2: 手动控制
DB::beginTransaction();
try {
    $order = Order::create([...]);
    $payment = Payment::create([...]);
    DB::commit();
} catch (\Exception $e) {
    DB::rollback();
    throw $e;
}

开发工具

推荐插件/工具

  • Laravel Idea - PhpStorm 的 Laravel 增强插件
  • Laravel Telescope - 本地开发调试工具
  • Laravel Debugbar - 开发环境性能分析
  • PHPStan - 静态分析工具
  • Laravel Pint - 代码风格格式化

与其他后端对比

特性 Laravel NestJS FastAPI Spring Boot
语言 PHP TypeScript Python Java
ORM Eloquent Prisma SQLAlchemy JPA
性能 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
学习曲线 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
生态系统 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
开发速度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐

相关链接

]]>
<![CDATA[Python FastAPI 后端 API]]> https://halolight.docs.h7ml.cn/guide/api-python https://halolight.docs.h7ml.cn/guide/api-python Fri, 19 Dec 2025 04:56:41 GMT Python FastAPI 后端 API

HaloLight FastAPI 后端 API 基于 FastAPI 0.115+ 构建,提供现代化异步 Python 后端服务。

API 文档https://halolight-api-python.h7ml.cn/api/docs

GitHubhttps://github.com/halolight/halolight-api-python

特性

  • 🔐 JWT 双令牌 - Access Token + Refresh Token,自动续期
  • 🛡️ RBAC 权限 - 基于角色的访问控制,通配符匹配
  • 📡 RESTful API - 标准化接口设计,OpenAPI 文档
  • 🗄️ SQLAlchemy 2.0 - 类型安全的数据库操作
  • 数据验证 - 请求参数校验,错误处理
  • 📊 日志系统 - 请求日志,错误追踪
  • 🐳 Docker 支持 - 容器化部署

技术栈

技术 版本 说明
Python 3.11+ 运行时
FastAPI 0.115+ Web 框架
SQLAlchemy 2.0+ 数据库 ORM
PostgreSQL 16 数据存储
Pydantic v2 数据验证
JWT python-jose 身份认证
Swagger UI - API 文档

快速开始

环境要求

  • Python >= 3.11
  • pip >= 23.0
  • PostgreSQL 16 (可选,默认 SQLite)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-api-python.git
cd halolight-api-python

# 创建虚拟环境
python3 -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate

# 安装依赖
pip install -e .

环境变量

bash
cp .env.example .env
env
# 数据库
DATABASE_URL=postgresql://user:password@localhost:5432/halolight_db

# JWT 密钥
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# 服务配置
PORT=8000
NODE_ENV=development

数据库初始化

bash
alembic upgrade head           # 运行迁移
python scripts/seed.py         # 填充种子数据

启动服务

bash
# 开发模式
uvicorn app.main:app --reload --port 8000

# 生产模式
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4

访问 http://localhost:8000

项目结构

halolight-api-python/
├── app/
│   ├── api/              # 控制器/路由处理
│   │   ├── auth.py       # 认证端点
│   │   ├── users.py      # 用户管理
│   │   └── ...
│   ├── services/         # 业务逻辑层
│   ├── models/           # 数据模型
│   ├── schemas/          # 请求验证
│   ├── core/             # 工具函数
│   └── main.py           # 应用入口
├── alembic/              # 数据库迁移/Schema
├── tests/                # 测试文件
├── Dockerfile            # Docker 配置
├── docker-compose.yml
└── pyproject.toml

API 模块

认证相关端点

方法 路径 描述 权限
POST /api/auth/login 用户登录 公开
POST /api/auth/register 用户注册 公开
POST /api/auth/refresh 刷新令牌 公开
POST /api/auth/logout 退出登录 需认证
POST /api/auth/forgot-password 忘记密码 公开
POST /api/auth/reset-password 重置密码 公开

用户管理端点

方法 路径 描述 权限
GET /api/users 获取用户列表 users:view
GET /api/users/:id 获取用户详情 users:view
POST /api/users 创建用户 users:create
PUT /api/users/:id 更新用户 users:update
DELETE /api/users/:id 删除用户 users:delete
GET /api/users/me 获取当前用户 需认证

完整端点清单

文档管理 (Documents) - 5 个端点

方法 路径 描述
GET /api/documents 获取文档列表
GET /api/documents/:id 获取文档详情
POST /api/documents 创建文档
PUT /api/documents/:id 更新文档
DELETE /api/documents/:id 删除文档

文件管理 (Files) - 5 个端点

方法 路径 描述
GET /api/files 获取文件列表
GET /api/files/:id 获取文件详情
POST /api/files/upload 上传文件
PUT /api/files/:id 更新文件信息
DELETE /api/files/:id 删除文件

消息管理 (Messages) - 5 个端点

方法 路径 描述
GET /api/messages 获取消息列表
GET /api/messages/:id 获取消息详情
POST /api/messages 发送消息
PUT /api/messages/:id/read 标记已读
DELETE /api/messages/:id 删除消息

通知管理 (Notifications) - 4 个端点

方法 路径 描述
GET /api/notifications 获取通知列表
PUT /api/notifications/:id/read 标记已读
PUT /api/notifications/read-all 全部已读
DELETE /api/notifications/:id 删除通知

日历管理 (Calendar) - 5 个端点

方法 路径 描述
GET /api/calendar/events 获取日程列表
GET /api/calendar/events/:id 获取日程详情
POST /api/calendar/events 创建日程
PUT /api/calendar/events/:id 更新日程
DELETE /api/calendar/events/:id 删除日程

仪表盘 (Dashboard) - 6 个端点

方法 路径 描述
GET /api/dashboard/stats 统计数据
GET /api/dashboard/visits 访问趋势
GET /api/dashboard/sales 销售数据
GET /api/dashboard/pie 饼图数据
GET /api/dashboard/tasks 待办任务
GET /api/dashboard/calendar 今日日程

认证机制

JWT 双令牌

Access Token:  15 分钟有效期,用于 API 请求
Refresh Token: 7 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

python
# 刷新令牌示例
import requests

response = requests.post(
    'http://localhost:8000/api/auth/refresh',
    json={'refreshToken': refresh_token}
)
new_tokens = response.json()

权限系统

角色定义

角色 说明 权限
super_admin 超级管理员 * (所有权限)
admin 管理员 users:*, documents:*, ...
user 普通用户 documents:view, files:view, ...
guest 访客 dashboard:view

权限格式

{resource}:{action}

示例:
- users:view      # 查看用户
- users:create    # 创建用户
- users:*         # 用户所有操作
- *               # 所有权限

错误处理

错误响应格式

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" }
    ]
  }
}

错误码

状态码 错误码 说明
400 VALIDATION_ERROR 参数验证失败
401 UNAUTHORIZED 未授权
403 FORBIDDEN 无权限
404 NOT_FOUND 资源不存在
409 CONFLICT 资源冲突
500 INTERNAL_ERROR 服务器错误

数据库模型

用户模型

python
# app/models/user.py
from sqlalchemy import Column, Integer, String, DateTime
from app.core.database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    username = Column(String, unique=True, nullable=False)
    hashed_password = Column(String, nullable=False)
    role = Column(String, default="user")
    created_at = Column(DateTime, server_default=func.now())
    updated_at = Column(DateTime, onupdate=func.now())

文档模型

python
# app/models/document.py
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import relationship
from app.core.database import Base

class Document(Base):
    __tablename__ = "documents"

    id = Column(Integer, primary_key=True)
    title = Column(String, nullable=False)
    content = Column(Text)
    author_id = Column(Integer, ForeignKey("users.id"))
    author = relationship("User", back_populates="documents")

环境变量

变量名 说明 默认值
DATABASE_URL 数据库连接字符串 sqlite:///./halolight.db
JWT_SECRET JWT 签名密钥 -
JWT_ACCESS_EXPIRES Access Token 过期时间 15m
JWT_REFRESH_EXPIRES Refresh Token 过期时间 7d
PORT 服务端口 8000
NODE_ENV 运行环境 development
CORS_ORIGINS CORS 允许的源 ["http://localhost:3000"]

统一响应格式

成功响应

json
{
  "success": true,
  "data": {
    "id": 1,
    "name": "示例数据"
  },
  "message": "操作成功"
}

分页响应

json
{
  "success": true,
  "data": {
    "items": [...],
    "total": 100,
    "page": 1,
    "pageSize": 10,
    "totalPages": 10
  }
}

错误响应

json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "错误描述",
    "details": []
  }
}

部署

Docker

bash
docker build -t halolight-api-python .
docker run -p 8000:8000 halolight-api-python

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

生产环境配置

env
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=your-production-secret

测试

运行测试

bash
pytest
pytest --cov=app tests/

测试示例

python
def test_login_success(client):
    response = client.post(
        "/api/auth/login",
        json={"email": "[email protected]", "password": "123456"}
    )
    assert response.status_code == 200
    assert "accessToken" in response.json()

def test_get_users_with_permission(client, admin_token):
    response = client.get(
        "/api/users",
        headers={"Authorization": f"Bearer {admin_token}"}
    )
    assert response.status_code == 200
    assert isinstance(response.json()["data"], list)

性能指标

基准测试

指标 数值 说明
请求吞吐量 5000+ QPS 单核 uvicorn
平均响应时间 < 10ms 简单查询
内存占用 ~100MB 基础运行
CPU 使用率 30-50% 高负载

可观测性

日志系统

python
import logging

logger = logging.getLogger(__name__)
logger.info("User logged in", extra={"user_id": user.id})

健康检查

python
@app.get("/health")
async def health_check():
    return {"status": "ok", "timestamp": datetime.now()}

监控指标

python
# Prometheus metrics endpoint
from prometheus_fastapi_instrumentator import Instrumentator

Instrumentator().instrument(app).expose(app)

常用命令

bash
# 开发
uvicorn app.main:app --reload --port 8000

# 构建
pip install -e .

# 测试
pytest
pytest --cov=app tests/

# 数据库
alembic upgrade head
alembic revision --autogenerate -m "描述"

# 代码质量
black app tests
ruff check app tests --fix

常见问题

Q:如何配置数据库连接池?

A:在 core/database.py 中配置 SQLAlchemy 连接池参数

python
engine = create_engine(
    DATABASE_URL,
    pool_size=10,
    max_overflow=20,
    pool_timeout=30
)

Q:如何启用 CORS?

A:在 main.py 中配置 CORS 中间件

python
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Q:如何实现文件上传?

A:使用 FastAPI 的 UploadFile 类型

python
from fastapi import UploadFile, File

@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...)):
    contents = await file.read()
    # 处理文件内容
    return {"filename": file.filename}

开发工具

推荐插件/工具

  • Black - Python 代码格式化
  • Ruff - 快速 Linter
  • mypy - 类型检查
  • pytest - 测试框架

架构特点

异步优势

FastAPI 基于 Python 的 asyncio,支持高并发异步操作:

python
@app.get("/api/async-example")
async def async_endpoint():
    result = await async_database_query()
    return result

自动文档生成

FastAPI 自动生成 OpenAPI (Swagger) 文档,无需额外配置:

  • Swagger UI: /docs
  • ReDoc: /redoc
  • OpenAPI Schema: /openapi.json

依赖注入系统

python
from fastapi import Depends

def get_current_user(token: str = Depends(oauth2_scheme)):
    return verify_token(token)

@app.get("/api/protected")
async def protected_route(user = Depends(get_current_user)):
    return {"user": user}

与其他后端对比

特性 FastAPI NestJS Go Fiber Spring Boot
语言 Python TypeScript Go Java
ORM SQLAlchemy Prisma GORM JPA
性能 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
学习曲线 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐

相关链接

]]>
<![CDATA[Astro 版本]]> https://halolight.docs.h7ml.cn/guide/astro https://halolight.docs.h7ml.cn/guide/astro Fri, 19 Dec 2025 04:56:41 GMT Astro 版本

HaloLight Astro 版本基于 Astro 5 构建,采用 Islands 架构实现零 JS 首屏和极致性能,支持多框架组件混用。

在线预览https://halolight-astro.h7ml.cn/

GitHubhttps://github.com/halolight/halolight-astro

特性

  • 🏝️ Islands 架构 - 默认零 JS,按需水合交互组件
  • 极致性能 - 首屏零 JavaScript,Lighthouse 100 分
  • 🔀 多框架混用 - 同一项目支持 React、Vue、Svelte、Solid 组件
  • 📄 内容优先 - 原生 Markdown/MDX 支持,内容集合
  • 🔄 视图过渡 - 原生 View Transitions API 支持
  • 🚀 SSR/SSG/Hybrid - 灵活的渲染模式选择
  • 📦 API 端点 - 原生支持 REST API 端点
  • 🎨 主题系统 - 深色/浅色主题切换
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理

技术栈

技术 版本 说明
Astro 5.x Islands 架构框架
TypeScript 5.x 类型安全
Tailwind CSS 3.x 原子化 CSS
Vite 内置 构建工具
@astrojs/node 9.x Node.js 适配器
Vitest 4.x 单元测试

核心特性

  • Islands 架构 - 默认零 JS,按需水合交互组件
  • 多框架支持 - 可在同一项目中使用 React、Vue、Svelte 组件
  • 内容优先 - 静态优先,极致首屏性能
  • SSR 支持 - 通过 @astrojs/node 适配器支持服务端渲染
  • 文件路由 - 基于文件系统的自动路由
  • API 端点 - 原生支持 REST API 端点

目录结构

halolight-astro/
├── src/
│   ├── pages/                    # 文件路由
│   │   ├── index.astro          # 首页
│   │   ├── privacy.astro        # 隐私政策
│   │   ├── terms.astro          # 服务条款
│   │   ├── auth/                # 认证页面
│   │   │   ├── login.astro
│   │   │   ├── register.astro
│   │   │   ├── forgot-password.astro
│   │   │   └── reset-password.astro
│   │   ├── dashboard/           # 仪表盘页面
│   │   │   ├── index.astro      # 仪表盘首页
│   │   │   ├── analytics.astro  # 数据分析
│   │   │   ├── users.astro      # 用户管理
│   │   │   ├── accounts.astro   # 账户管理
│   │   │   ├── documents.astro  # 文档管理
│   │   │   ├── files.astro      # 文件管理
│   │   │   ├── messages.astro   # 消息中心
│   │   │   ├── notifications.astro
│   │   │   ├── calendar.astro   # 日历
│   │   │   ├── profile.astro    # 个人中心
│   │   │   └── settings/        # 设置
│   │   └── api/                 # API 端点
│   │       └── auth/
│   │           ├── login.ts
│   │           ├── register.ts
│   │           ├── forgot-password.ts
│   │           └── reset-password.ts
│   ├── layouts/                 # 布局组件
│   │   ├── Layout.astro         # 基础布局
│   │   ├── AuthLayout.astro     # 认证布局
│   │   ├── DashboardLayout.astro # 仪表盘布局
│   │   └── LegalLayout.astro    # 法律页面布局
│   ├── components/              # UI 组件
│   │   └── dashboard/
│   │       ├── Sidebar.astro    # 侧边栏
│   │       └── Header.astro     # 顶部导航
│   ├── styles/                  # 全局样式
│   │   └── globals.css
│   └── assets/                  # 静态资源
├── public/                      # 公共资源
├── tests/                       # 测试文件
├── astro.config.mjs            # Astro 配置
├── tailwind.config.mjs         # Tailwind 配置
├── vitest.config.ts            # 测试配置
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-astro.git
cd halolight-astro
pnpm install

环境变量

bash
cp .env.example .env.local
env
# .env.local 示例
PUBLIC_API_URL=/api
PUBLIC_MOCK=true
[email protected]
PUBLIC_DEMO_PASSWORD=123456
PUBLIC_SHOW_DEMO_HINT=true
PUBLIC_APP_TITLE=Admin Pro
PUBLIC_BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:4321

构建生产

bash
pnpm build
pnpm preview

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

核心功能

Islands 架构

Astro 的 Islands 架构允许页面默认为静态 HTML,仅在需要交互的组件上添加 JavaScript:

astro
---
// 静态导入,无 JS
import StaticCard from '../components/StaticCard.astro';
// 交互组件(可来自 React/Vue/Svelte)
import Counter from '../components/Counter.tsx';
---

<!-- 纯静态,零 JS -->
<StaticCard title="统计数据" />

<!-- 页面加载时水合 -->
<Counter client:load />

<!-- 可见时水合(懒加载) -->
<Counter client:visible />

<!-- 浏览器空闲时水合 -->
<Counter client:idle />

客户端指令

指令 行为 使用场景
client:load 页面加载后立即水合 首屏关键交互
client:idle 浏览器空闲时水合 非关键交互
client:visible 元素可见时水合 懒加载组件
client:only 仅客户端渲染 依赖浏览器 API
client:media 媒体查询匹配时水合 响应式组件

布局系统

astro
---
// layouts/DashboardLayout.astro
import Layout from './Layout.astro';
import Sidebar from '../components/dashboard/Sidebar.astro';
import Header from '../components/dashboard/Header.astro';

interface Props {
  title: string;
  description?: string;
}

const { title, description } = Astro.props;
const currentPath = Astro.url.pathname;
---

<Layout title={title} description={description}>
  <div class="min-h-screen bg-gray-50 dark:bg-gray-900">
    <Sidebar currentPath={currentPath} />
    <div class="lg:pl-64">
      <Header title={title} />
      <main class="p-4 lg:p-6">
        <slot />
      </main>
    </div>
  </div>
</Layout>

API 端点

Astro 原生支持创建 API 端点:

typescript
// src/pages/api/auth/login.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();
  const { email, password } = body;

  // 验证逻辑
  if (!email || !password) {
    return new Response(
      JSON.stringify({ success: false, message: '邮箱和密码不能为空' }),
      { status: 400, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // 认证逻辑...

  return new Response(
    JSON.stringify({
      success: true,
      message: '登录成功',
      user: { id: 1, name: '管理员', role: 'admin' },
      token: 'mock_token',
    }),
    { status: 200, headers: { 'Content-Type': 'application/json' } }
  );
};

文件路由

文件路径 URL 说明
src/pages/index.astro / 首页
src/pages/auth/login.astro /auth/login 登录
src/pages/dashboard/index.astro /dashboard 仪表盘
src/pages/dashboard/[id].astro /dashboard/:id 动态路由
src/pages/api/auth/login.ts /api/auth/login API 端点

页面路由

路径 页面 权限
/ 首页 公开
/auth/login 登录 公开
/auth/register 注册 公开
/auth/forgot-password 忘记密码 公开
/auth/reset-password 重置密码 公开
/dashboard 仪表盘 dashboard:view
/dashboard/analytics 数据分析 analytics:view
/dashboard/users 用户管理 users:view
/dashboard/accounts 账户管理 accounts:view
/dashboard/documents 文档管理 documents:view
/dashboard/files 文件管理 files:view
/dashboard/messages 消息中心 messages:view
/dashboard/notifications 通知中心 notifications:view
/dashboard/calendar 日历 calendar:view
/dashboard/profile 个人中心 settings:view
/dashboard/settings 设置 settings:view
/privacy 隐私政策 公开
/terms 服务条款 公开

环境变量

配置

bash
# .env
PUBLIC_API_URL=/api
PUBLIC_MOCK=true
PUBLIC_DEMO_EMAIL=[email protected]
PUBLIC_DEMO_PASSWORD=123456
PUBLIC_SHOW_DEMO_HINT=true
PUBLIC_APP_TITLE=Admin Pro
PUBLIC_BRAND_NAME=Halolight

变量说明

变量名 说明 默认值
PUBLIC_API_URL API 基础 URL /api
PUBLIC_MOCK 启用 Mock 数据 true
PUBLIC_APP_TITLE 应用标题 Admin Pro
PUBLIC_BRAND_NAME 品牌名称 Halolight
PUBLIC_DEMO_EMAIL 演示账号邮箱 -
PUBLIC_DEMO_PASSWORD 演示账号密码 -
PUBLIC_SHOW_DEMO_HINT 显示演示提示 false

使用方式

astro
---
// 在 .astro 文件中
const apiUrl = import.meta.env.PUBLIC_API_URL;
const isMock = import.meta.env.PUBLIC_MOCK === 'true';
---
typescript
// 在 .ts 文件中
const apiUrl = import.meta.env.PUBLIC_API_URL;

常用命令

bash
# 开发
pnpm dev              # 启动开发服务器 (默认 4321 端口)
pnpm dev --port 3000  # 指定端口

# 构建
pnpm build            # 生产构建
pnpm preview          # 预览生产构建

# 检查
pnpm astro check      # 类型检查
pnpm lint             # ESLint 检查
pnpm lint:fix         # ESLint 自动修复

# 测试
pnpm test             # 运行测试
pnpm test:run         # 单次运行
pnpm test:coverage    # 覆盖率报告

# Astro CLI
pnpm astro add react     # 添加 React 集成
pnpm astro add vue       # 添加 Vue 集成
pnpm astro add tailwind  # 添加 Tailwind
pnpm astro add mdx       # 添加 MDX 支持

测试

bash
# 运行测试
pnpm test

# 生成覆盖率报告
pnpm test --coverage

测试示例

typescript
// tests/components/Counter.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from '../../src/components/Counter';

describe('Counter', () => {
  it('renders with initial count', () => {
    render(<Counter />);
    expect(screen.getByText('0')).toBeInTheDocument();
  });

  it('increments count on button click', () => {
    render(<Counter />);
    const button = screen.getByRole('button');
    fireEvent.click(button);
    expect(screen.getByText('1')).toBeInTheDocument();
  });
});

配置

Astro 配置

javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import node from '@astrojs/node';

export default defineConfig({
  integrations: [tailwind()],
  output: 'server',  // SSR 模式
  adapter: node({
    mode: 'standalone',
  }),
  server: {
    port: 4321,
    host: true,
  },
});

输出模式

模式 说明 适用场景
static 静态站点生成 (SSG) 博客、文档站
server 服务端渲染 (SSR) 动态应用
hybrid 混合模式 部分动态

部署

Vercel (推荐)

Deploy with Vercel

bash
# 安装适配器
pnpm add @astrojs/vercel

# astro.config.mjs
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  output: 'server',
  adapter: vercel(),
});

Docker

dockerfile
FROM node:20-alpine AS builder

RUN npm install -g pnpm

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

ENV HOST=0.0.0.0
ENV PORT=4321

EXPOSE 4321

CMD ["node", "./dist/server/entry.mjs"]

其他平台

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm astro check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

内容集合

Astro 内置的内容管理系统,支持类型安全的 Markdown/MDX 内容。

typescript
// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    author: z.string(),
    tags: z.array(z.string()).optional(),
    image: z.string().optional(),
  }),
});

export const collections = {
  blog: blogCollection,
};
astro
---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---

<BlogLayout title={post.data.title}>
  <article>
    <h1>{post.data.title}</h1>
    <time>{post.data.pubDate.toLocaleDateString()}</time>
    <Content />
  </article>
</BlogLayout>

视图过渡

原生 View Transitions API 支持,实现页面间流畅动画。

astro
---
// src/layouts/Layout.astro
import { ViewTransitions } from 'astro:transitions';
---

<html>
  <head>
    <ViewTransitions />
  </head>
  <body>
    <slot />
  </body>
</html>
astro
---
// 自定义过渡动画
---
<div transition:name="hero">
  <h1 transition:animate="slide">欢迎</h1>
</div>

<style>
  /* 自定义动画 */
  @keyframes slide-in {
    from { transform: translateX(-100%); opacity: 0; }
    to { transform: translateX(0); opacity: 1; }
  }

  ::view-transition-old(hero) {
    animation: slide-out 0.3s ease-out;
  }

  ::view-transition-new(hero) {
    animation: slide-in 0.3s ease-out;
  }
</style>

中间件

请求拦截和处理。

typescript
// src/middleware.ts
import { defineMiddleware, sequence } from 'astro:middleware';

// 认证中间件
const auth = defineMiddleware(async (context, next) => {
  const token = context.cookies.get('token')?.value;

  // 保护路由
  const protectedPaths = ['/dashboard', '/profile', '/settings'];
  const isProtected = protectedPaths.some(path =>
    context.url.pathname.startsWith(path)
  );

  if (isProtected && !token) {
    return context.redirect('/auth/login');
  }

  // 将用户信息传递给页面
  if (token) {
    context.locals.user = await verifyToken(token);
  }

  return next();
});

// 日志中间件
const logger = defineMiddleware(async (context, next) => {
  const start = Date.now();
  const response = await next();
  const duration = Date.now() - start;

  console.log(`${context.request.method} ${context.url.pathname} - ${duration}ms`);

  return response;
});

// 组合中间件
export const onRequest = sequence(logger, auth);

性能优化

图片优化

astro
---
import { Image } from 'astro:assets';
import myImage from '../assets/hero.png';
---

<!-- 自动优化图片 -->
<Image src={myImage} alt="Hero" width={800} height={600} />

<!-- 远程图片 -->
<Image
  src="https://example.com/image.jpg"
  alt="Remote"
  width={400}
  height={300}
  inferSize
/>

懒加载组件

astro
---
// 使用 client:visible 实现懒加载
import HeavyComponent from '../components/HeavyComponent';
---

<!-- 仅在元素可见时加载 -->
<HeavyComponent client:visible />

预加载

astro
---
// 预加载关键资源
---
<head>
  <link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin />
  <link rel="preconnect" href="https://api.example.com" />
  <link rel="dns-prefetch" href="https://cdn.example.com" />
</head>

代码分割

astro
---
// 动态导入重型组件
const Chart = await import('../components/Chart.tsx');
---

<Chart.default client:visible data={data} />

常见问题

Q:如何在 Islands 中共享状态?

A:使用 nanostores 或 Zustand:

bash
pnpm add nanostores @nanostores/react
typescript
// src/stores/counter.ts
import { atom } from 'nanostores';

export const $counter = atom(0);

export function increment() {
  $counter.set($counter.get() + 1);
}
tsx
// React 组件
import { useStore } from '@nanostores/react';
import { $counter, increment } from '../stores/counter';

export function Counter() {
  const count = useStore($counter);
  return <button onClick={increment}>{count}</button>;
}

Q:如何处理表单提交?

A:使用 API 端点:

astro
---
// src/pages/contact.astro
---
<form method="POST" action="/api/contact">
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button type="submit">提交</button>
</form>
typescript
// src/pages/api/contact.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) => {
  const data = await request.formData();
  const email = data.get('email');
  const message = data.get('message');

  // 处理表单数据
  await sendEmail({ email, message });

  return new Response(JSON.stringify({ success: true }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  });
};

Q:如何实现认证?

A:使用中间件 + Cookie:

typescript
// src/middleware.ts
export const onRequest = defineMiddleware(async (context, next) => {
  const token = context.cookies.get('auth-token')?.value;

  if (context.url.pathname.startsWith('/dashboard') && !token) {
    return context.redirect('/auth/login');
  }

  if (token) {
    try {
      const user = await verifyToken(token);
      context.locals.user = user;
    } catch {
      context.cookies.delete('auth-token');
      return context.redirect('/auth/login');
    }
  }

  return next();
});

Q:构建体积大怎么办?

A:优化建议:

  1. 检查 client: 指令使用,尽量用 client:visibleclient:idle
  2. 使用动态导入
  3. 移除未使用的集成
  4. 使用 @playform/compress 压缩输出
bash
pnpm add @playform/compress
javascript
// astro.config.mjs
import compress from '@playform/compress';

export default defineConfig({
  integrations: [compress()],
});

与其他版本对比

特性 Astro Next.js Vue
默认 JS 体积 0 KB ~80 KB ~70 KB
Islands 架构 原生支持 不支持 不支持 (Nuxt)
多框架组件 支持 不支持 不支持
SSG/SSR 支持 支持 支持 (Nuxt)
学习曲线

相关链接

]]>
<![CDATA[AWS 部署]]> https://halolight.docs.h7ml.cn/guide/aws https://halolight.docs.h7ml.cn/guide/aws Fri, 19 Dec 2025 04:56:41 GMT AWS 部署

HaloLight AWS 部署版本,面向企业级 AWS 生态的部署方案,支持 Amplify、S3 + CloudFront、ECS 等多种部署方式。

在线预览https://halolight-aws.h7ml.cn

GitHubhttps://github.com/halolight/halolight-aws

特性

  • 🟠 AWS Amplify - 全托管的前端部署,Git 集成
  • 📦 S3 - 静态资源存储,无限扩展
  • 🌐 CloudFront - 全球 CDN 分发,低延迟
  • Lambda@Edge - 边缘计算,SSR 支持
  • 🔐 IAM - 细粒度身份和访问管理
  • 📊 CloudWatch - 监控、告警和日志
  • 🗄️ RDS/DynamoDB - 托管数据库服务
  • 🔒 WAF - Web 应用防火墙

快速开始

方式一:Amplify Console 部署 (推荐)

  1. 登录 AWS Amplify Console
  2. 点击 “Host web app” → “From GitHub”
  3. 授权并选择 halolight/halolight-aws 仓库
  4. 选择 main 分支
  5. 配置构建设置 (使用默认或自定义 amplify.yml)
  6. 配置环境变量
  7. 点击 “Save and deploy”

方式二:Amplify CLI 部署

bash
# 安装 Amplify CLI
npm install -g @aws-amplify/cli

# 配置 AWS 凭证
amplify configure

# 克隆项目
git clone https://github.com/halolight/halolight-aws.git
cd halolight-aws

# 初始化 Amplify 项目
amplify init

# 添加托管
amplify add hosting
# 选择: Hosting with Amplify Console
# 选择: Continuous deployment

# 发布
amplify publish

方式三:S3 + CloudFront 静态部署

bash
# 安装 AWS CLI
brew install awscli  # macOS
# 或
pip install awscli

# 配置凭证
aws configure

# 构建静态站点
pnpm build
pnpm export  # 如果使用静态导出

# 创建 S3 存储桶
aws s3 mb s3://halolight-static --region ap-northeast-1

# 配置静态网站托管
aws s3 website s3://halolight-static \
  --index-document index.html \
  --error-document 404.html

# 上传文件
aws s3 sync out/ s3://halolight-static --delete

# 创建 CloudFront 分发
aws cloudfront create-distribution \
  --origin-domain-name halolight-static.s3.ap-northeast-1.amazonaws.com \
  --default-root-object index.html

配置文件

amplify.yml

yaml
version: 1
applications:
  - frontend:
      phases:
        preBuild:
          commands:
            - corepack enable
            - corepack prepare pnpm@latest --activate
            - pnpm install --frozen-lockfile
        build:
          commands:
            - pnpm build
      artifacts:
        baseDirectory: .next
        files:
          - '**/*'
      cache:
        paths:
          - node_modules/**/*
          - .next/cache/**/*
      buildPath: /
    appRoot: .

next.config.ts (Amplify 优化)

typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
  images: {
    unoptimized: process.env.AMPLIFY_ENV === "true",
    remotePatterns: [
      {
        protocol: "https",
        hostname: "**.amazonaws.com",
      },
    ],
  },
  // Amplify 需要的配置
  experimental: {
    serverActions: {
      bodySizeLimit: "2mb",
    },
  },
};

export default nextConfig;

环境变量

在 Amplify Console → App settings → Environment variables 设置:

变量名 说明 示例
NEXT_PUBLIC_API_URL API 基础 URL /api
NEXT_PUBLIC_MOCK 启用 Mock 数据 false
NEXT_PUBLIC_APP_TITLE 应用标题 Admin Pro
AWS_REGION AWS 区域 ap-northeast-1
AMPLIFY_ENV Amplify 环境标识 true
DATABASE_URL RDS 数据库连接 postgresql://...
DYNAMODB_TABLE DynamoDB 表名 halolight-users

设置方式

bash
# Amplify CLI 设置
amplify env add
amplify env checkout <env-name>

# AWS CLI 设置 (SSM Parameter Store)
aws ssm put-parameter \
  --name "/halolight/production/DATABASE_URL" \
  --value "postgresql://..." \
  --type SecureString

# 在 Amplify 中引用
# amplify.yml
build:
  commands:
    - export DATABASE_URL=$(aws ssm get-parameter --name "/halolight/production/DATABASE_URL" --with-decryption --query Parameter.Value --output text)
    - pnpm build

Lambda@Edge 函数

SSR 边缘渲染

typescript
// edge-functions/ssr.ts
import type { CloudFrontRequestHandler } from "aws-lambda";

export const handler: CloudFrontRequestHandler = async (event) => {
  const request = event.Records[0].cf.request;
  const uri = request.uri;

  // 处理 SSR 路由
  if (shouldSSR(uri)) {
    // 调用 Lambda 进行 SSR
    const response = await renderPage(uri);
    return {
      status: "200",
      statusDescription: "OK",
      headers: {
        "content-type": [{ value: "text/html; charset=utf-8" }],
        "cache-control": [{ value: "public, max-age=0, s-maxage=31536000" }],
      },
      body: response,
    };
  }

  return request;
};

function shouldSSR(uri: string): boolean {
  // 定义需要 SSR 的路由
  const ssrRoutes = ["/", "/dashboard", "/users"];
  return ssrRoutes.some((route) => uri.startsWith(route));
}

认证边缘函数

typescript
// edge-functions/auth.ts
import type { CloudFrontRequestHandler } from "aws-lambda";
import { verify } from "jsonwebtoken";

export const handler: CloudFrontRequestHandler = async (event) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  // 检查保护路由
  if (isProtectedRoute(request.uri)) {
    const authHeader = headers.authorization?.[0]?.value;
    const cookieHeader = headers.cookie?.[0]?.value;

    // 从 header 或 cookie 获取 token
    const token = extractToken(authHeader, cookieHeader);

    if (!token) {
      return {
        status: "401",
        statusDescription: "Unauthorized",
        headers: {
          "content-type": [{ value: "application/json" }],
        },
        body: JSON.stringify({ error: "Unauthorized" }),
      };
    }

    try {
      // 验证 JWT
      verify(token, process.env.JWT_SECRET!);
    } catch {
      return {
        status: "401",
        statusDescription: "Invalid Token",
        body: JSON.stringify({ error: "Invalid token" }),
      };
    }
  }

  return request;
};

function isProtectedRoute(uri: string): boolean {
  const protectedPaths = ["/api/users", "/api/admin", "/dashboard"];
  return protectedPaths.some((path) => uri.startsWith(path));
}

function extractToken(authHeader?: string, cookieHeader?: string): string | null {
  if (authHeader?.startsWith("Bearer ")) {
    return authHeader.substring(7);
  }
  if (cookieHeader) {
    const match = cookieHeader.match(/token=([^;]+)/);
    return match ? match[1] : null;
  }
  return null;
}

地理位置重定向

typescript
// edge-functions/geo-redirect.ts
import type { CloudFrontRequestHandler } from "aws-lambda";

export const handler: CloudFrontRequestHandler = async (event) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  // 获取 CloudFront 注入的地理位置信息
  const country = headers["cloudfront-viewer-country"]?.[0]?.value;
  const city = headers["cloudfront-viewer-city"]?.[0]?.value;

  // 基于地理位置重定向
  if (country === "CN" && !request.uri.startsWith("/cn/")) {
    return {
      status: "302",
      statusDescription: "Found",
      headers: {
        location: [{ value: `/cn${request.uri}` }],
      },
    };
  }

  // 添加地理位置 header
  request.headers["x-user-country"] = [{ value: country || "unknown" }];
  request.headers["x-user-city"] = [{ value: city || "unknown" }];

  return request;
};

CloudFront 配置

CloudFront 分发配置

json
{
  "CallerReference": "halolight-distribution",
  "Comment": "HaloLight CDN Distribution",
  "DefaultCacheBehavior": {
    "TargetOriginId": "S3-halolight",
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": ["GET", "HEAD", "OPTIONS"],
    "CachedMethods": ["GET", "HEAD"],
    "ForwardedValues": {
      "QueryString": true,
      "Cookies": {
        "Forward": "none"
      },
      "Headers": ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]
    },
    "MinTTL": 0,
    "DefaultTTL": 86400,
    "MaxTTL": 31536000,
    "Compress": true,
    "LambdaFunctionAssociations": [
      {
        "EventType": "viewer-request",
        "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789:function:auth:1"
      },
      {
        "EventType": "origin-request",
        "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789:function:ssr:1"
      }
    ]
  },
  "Origins": {
    "Items": [
      {
        "Id": "S3-halolight",
        "DomainName": "halolight-static.s3.ap-northeast-1.amazonaws.com",
        "S3OriginConfig": {
          "OriginAccessIdentity": "origin-access-identity/cloudfront/E1234567890ABC"
        }
      }
    ]
  },
  "Enabled": true,
  "PriceClass": "PriceClass_200",
  "HttpVersion": "http2and3",
  "IsIPV6Enabled": true
}

缓存策略

json
{
  "Name": "HaloLight-CachePolicy",
  "DefaultTTL": 86400,
  "MaxTTL": 31536000,
  "MinTTL": 1,
  "ParametersInCacheKeyAndForwardedToOrigin": {
    "EnableAcceptEncodingGzip": true,
    "EnableAcceptEncodingBrotli": true,
    "HeadersConfig": {
      "HeaderBehavior": "whitelist",
      "Headers": ["Authorization", "Accept-Language"]
    },
    "CookiesConfig": {
      "CookieBehavior": "none"
    },
    "QueryStringsConfig": {
      "QueryStringBehavior": "whitelist",
      "QueryStrings": ["page", "limit", "search"]
    }
  }
}

S3 存储桶配置

存储桶策略

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E1234567890ABC"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::halolight-static/*"
    }
  ]
}

CORS 配置

json
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedOrigins": ["https://halolight-aws.h7ml.cn", "https://halolight.h7ml.cn"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

IAM 策略

Amplify 部署角色

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AmplifyAccess",
      "Effect": "Allow",
      "Action": [
        "amplify:*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "S3Access",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::halolight-*",
        "arn:aws:s3:::halolight-*/*"
      ]
    },
    {
      "Sid": "CloudFrontAccess",
      "Effect": "Allow",
      "Action": [
        "cloudfront:CreateInvalidation",
        "cloudfront:GetDistribution",
        "cloudfront:UpdateDistribution"
      ],
      "Resource": "*"
    },
    {
      "Sid": "LambdaAccess",
      "Effect": "Allow",
      "Action": [
        "lambda:CreateFunction",
        "lambda:UpdateFunctionCode",
        "lambda:GetFunction",
        "lambda:EnableReplication*"
      ],
      "Resource": "arn:aws:lambda:*:*:function:halolight-*"
    },
    {
      "Sid": "SSMAccess",
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters"
      ],
      "Resource": "arn:aws:ssm:*:*:parameter/halolight/*"
    }
  ]
}

最小权限原则

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadOnlyAccess",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "dynamodb:GetItem",
        "dynamodb:Query",
        "rds:DescribeDBInstances"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:RequestedRegion": "ap-northeast-1"
        }
      }
    }
  ]
}

DynamoDB 集成

表定义

typescript
// lib/dynamodb.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";

const client = new DynamoDBClient({ region: process.env.AWS_REGION });
const docClient = DynamoDBDocumentClient.from(client);

const TABLE_NAME = process.env.DYNAMODB_TABLE || "halolight-users";

export async function getUser(userId: string) {
  const command = new GetCommand({
    TableName: TABLE_NAME,
    Key: { pk: `USER#${userId}`, sk: "PROFILE" },
  });

  const response = await docClient.send(command);
  return response.Item;
}

export async function createUser(user: User) {
  const command = new PutCommand({
    TableName: TABLE_NAME,
    Item: {
      pk: `USER#${user.id}`,
      sk: "PROFILE",
      ...user,
      createdAt: new Date().toISOString(),
    },
  });

  await docClient.send(command);
  return user;
}

export async function getUsersByRole(role: string) {
  const command = new QueryCommand({
    TableName: TABLE_NAME,
    IndexName: "GSI1",
    KeyConditionExpression: "gsi1pk = :role",
    ExpressionAttributeValues: {
      ":role": `ROLE#${role}`,
    },
  });

  const response = await docClient.send(command);
  return response.Items;
}

CloudFormation 模板

yaml
# cloudformation/dynamodb.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: HaloLight DynamoDB Tables

Resources:
  UsersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: halolight-users
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: pk
          AttributeType: S
        - AttributeName: sk
          AttributeType: S
        - AttributeName: gsi1pk
          AttributeType: S
        - AttributeName: gsi1sk
          AttributeType: S
      KeySchema:
        - AttributeName: pk
          KeyType: HASH
        - AttributeName: sk
          KeyType: RANGE
      GlobalSecondaryIndexes:
        - IndexName: GSI1
          KeySchema:
            - AttributeName: gsi1pk
              KeyType: HASH
            - AttributeName: gsi1sk
              KeyType: RANGE
          Projection:
            ProjectionType: ALL
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      Tags:
        - Key: Project
          Value: HaloLight

RDS 数据库

连接配置

typescript
// lib/rds.ts
import { Pool } from "pg";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: {
    rejectUnauthorized: false,
  },
  max: 10,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

export async function query<T>(text: string, params?: any[]): Promise<T[]> {
  const client = await pool.connect();
  try {
    const result = await client.query(text, params);
    return result.rows as T[];
  } finally {
    client.release();
  }
}

export async function getUsers() {
  return query<User>("SELECT * FROM users ORDER BY created_at DESC");
}

CloudWatch 监控

自定义指标

typescript
// lib/cloudwatch.ts
import { CloudWatchClient, PutMetricDataCommand } from "@aws-sdk/client-cloudwatch";

const client = new CloudWatchClient({ region: process.env.AWS_REGION });

export async function putMetric(name: string, value: number, unit: string = "Count") {
  const command = new PutMetricDataCommand({
    Namespace: "HaloLight",
    MetricData: [
      {
        MetricName: name,
        Value: value,
        Unit: unit,
        Dimensions: [
          {
            Name: "Environment",
            Value: process.env.NODE_ENV || "development",
          },
        ],
      },
    ],
  });

  await client.send(command);
}

// 使用示例
export async function trackApiRequest(endpoint: string, duration: number) {
  await putMetric(`API_${endpoint}_Requests`, 1, "Count");
  await putMetric(`API_${endpoint}_Duration`, duration, "Milliseconds");
}

告警配置

yaml
# cloudformation/alarms.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: HaloLight CloudWatch Alarms

Resources:
  HighErrorRateAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: HaloLight-HighErrorRate
      AlarmDescription: Alarm when error rate exceeds 5%
      MetricName: 5XXError
      Namespace: AWS/CloudFront
      Statistic: Average
      Period: 300
      EvaluationPeriods: 2
      Threshold: 5
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref AlertSNSTopic
      Dimensions:
        - Name: DistributionId
          Value: !Ref CloudFrontDistribution

  AlertSNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: halolight-alerts
      Subscription:
        - Protocol: email
          Endpoint: [email protected]

常用命令

bash
# Amplify CLI
amplify status                    # 查看状态
amplify push                      # 推送后端更改
amplify publish                   # 发布前端和后端
amplify env list                  # 列出环境
amplify env checkout prod         # 切换环境
amplify delete                    # 删除项目

# AWS CLI - S3
aws s3 ls                         # 列出存储桶
aws s3 sync ./out s3://bucket     # 同步文件
aws s3 rm s3://bucket --recursive # 清空存储桶

# AWS CLI - CloudFront
aws cloudfront create-invalidation \
  --distribution-id E1234567890 \
  --paths "/*"                    # 刷新缓存

# AWS CLI - Lambda
aws lambda update-function-code \
  --function-name halolight-ssr \
  --zip-file fileb://function.zip # 更新函数

# AWS CLI - CloudWatch
aws logs tail /aws/lambda/halolight --follow  # 查看日志

# AWS CLI - SSM
aws ssm get-parameter --name "/halolight/prod/DATABASE_URL" --with-decryption

自定义域名

Route 53 配置

bash
# 创建托管区域
aws route53 create-hosted-zone \
  --name halolight-aws.h7ml.cn \
  --caller-reference $(date +%s)

# 添加 A 记录 (指向 CloudFront)
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "halolight-aws.h7ml.cn",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d1234567890.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    }]
  }'

ACM 证书

bash
# 请求证书 (必须在 us-east-1 区域)
aws acm request-certificate \
  --domain-name halolight-aws.h7ml.cn \
  --validation-method DNS \
  --region us-east-1

# 验证后关联到 CloudFront
aws cloudfront update-distribution \
  --id E1234567890 \
  --viewer-certificate '{
    "ACMCertificateArn": "arn:aws:acm:us-east-1:123456789:certificate/xxx",
    "SSLSupportMethod": "sni-only",
    "MinimumProtocolVersion": "TLSv1.2_2021"
  }'

常见问题

Q:Amplify 构建失败?

A:检查以下几点:

  1. 查看构建日志中的错误信息
  2. 确认 Node.js 版本兼容 (Amplify 默认使用 Node 18)
  3. 检查 amplify.yml 配置是否正确
  4. 确认环境变量已设置

Q:CloudFront 缓存问题?

A:刷新缓存:

bash
aws cloudfront create-invalidation \
  --distribution-id E1234567890 \
  --paths "/*"

Q:Lambda@Edge 部署限制?

A:注意事项:

  1. Lambda@Edge 必须在 us-east-1 区域创建
  2. 函数包大小限制:1MB (压缩后)
  3. 运行时限制:5 秒 (viewer request/response),30 秒 (origin request/response)
  4. 内存限制:128MB - 10GB

Q:如何查看 CloudWatch 日志?

A:使用 AWS CLI 或控制台:

bash
# CLI 查看日志
aws logs tail /aws/lambda/halolight-ssr --follow

# 或在控制台
# CloudWatch → Log groups → /aws/lambda/halolight-ssr

费用说明

服务 免费额度 超出价格
Amplify 构建 1000 分钟/月 $0.01/分钟
Amplify 托管 15GB 存储,5GB 传输 $0.023/GB
S3 存储 5GB $0.025/GB
CloudFront 1TB 传输/月 $0.085/GB
Lambda@Edge 1M 请求/月 $0.60/M 请求
Route 53 - $0.50/托管区域

与其他平台对比

特性 AWS Amplify Vercel Netlify
全球边缘 ✅ CloudFront
SSR 支持 ✅ Lambda@Edge ✅ 原生
托管数据库 ✅ RDS/DynamoDB ✅ Postgres ❌ 需外部
免费带宽 5GB 100GB 100GB
企业功能 ✅ IAM/VPC ⚠️ 有限 ⚠️ 有限
学习曲线 较陡 平缓 平缓

相关链接

]]>
<![CDATA[Azure 部署]]> https://halolight.docs.h7ml.cn/guide/azure https://halolight.docs.h7ml.cn/guide/azure Fri, 19 Dec 2025 04:56:41 GMT Azure 部署

HaloLight Azure 部署版本,面向企业级 Microsoft 生态的部署方案。

在线预览https://halolight-azure.h7ml.cn

GitHubhttps://github.com/halolight/halolight-azure

特性

  • ☁️ Static Web Apps - 全球分布式静态站点托管
  • Azure Functions - Serverless 函数计算
  • 🔐 Azure AD - 企业身份认证与 SSO
  • 🌐 Azure CDN - 全球 CDN 加速
  • 📊 Application Insights - 应用性能监控
  • 🔒 企业级安全 - Microsoft 合规认证
  • 🗄️ Cosmos DB - 全球分布式数据库
  • 📦 Container Apps - 容器化部署支持

快速开始

方式一:GitHub Actions 部署 (推荐)

  1. Fork halolight-azure 仓库
  2. 在 Azure Portal 创建 Static Web App
  3. 选择 GitHub 作为源
  4. 授权并选择仓库
  5. Azure 自动生成 GitHub Actions 工作流

方式二:Azure CLI 部署

bash
# 安装 Azure CLI
# macOS
brew install azure-cli

# Windows
winget install -e --id Microsoft.AzureCLI

# Linux
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# 登录 Azure
az login

# 创建资源组
az group create \
  --name halolight-rg \
  --location eastasia

# 创建 Static Web App
az staticwebapp create \
  --name halolight \
  --resource-group halolight-rg \
  --source https://github.com/halolight/halolight-azure \
  --branch main \
  --app-location "/" \
  --output-location ".next" \
  --login-with-github

# 查看部署状态
az staticwebapp show \
  --name halolight \
  --resource-group halolight-rg

方式三:VS Code 扩展

  1. 安装 Azure Static Web Apps 扩展
  2. 登录 Azure 账号
  3. 右键点击项目 → “Create Static Web App...”
  4. 按向导完成配置

配置文件

staticwebapp.config.json

json
{
  "$schema": "https://json.schemastore.org/staticwebapp.config.json",
  "navigationFallback": {
    "rewrite": "/index.html",
    "exclude": ["/api/*", "/_next/*", "/static/*", "*.{css,js,json,ico,png,jpg,svg}"]
  },
  "routes": [
    {
      "route": "/api/*",
      "allowedRoles": ["authenticated"]
    },
    {
      "route": "/admin/*",
      "allowedRoles": ["admin"]
    },
    {
      "route": "/login",
      "rewrite": "/.auth/login/aad"
    },
    {
      "route": "/logout",
      "redirect": "/.auth/logout"
    },
    {
      "route": "/.auth/login/github",
      "statusCode": 404
    }
  ],
  "responseOverrides": {
    "401": {
      "redirect": "/.auth/login/aad",
      "statusCode": 302
    },
    "404": {
      "rewrite": "/404.html"
    }
  },
  "globalHeaders": {
    "X-Frame-Options": "DENY",
    "X-Content-Type-Options": "nosniff",
    "X-XSS-Protection": "1; mode=block",
    "Referrer-Policy": "strict-origin-when-cross-origin",
    "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
  },
  "mimeTypes": {
    ".json": "application/json",
    ".woff2": "font/woff2"
  },
  "platform": {
    "apiRuntime": "node:20"
  }
}

GitHub Actions 工作流

yaml
# .github/workflows/azure-static-web-apps.yml
name: Azure Static Web Apps CI/CD

on:
  push:
    branches: [main]
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches: [main]

jobs:
  build_and_deploy:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 9

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm build
        env:
          NEXT_PUBLIC_API_URL: /api
          NEXT_PUBLIC_MOCK: false

      - name: Deploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          app_location: "/"
          api_location: "api"
          output_location: ".next"

  close_pull_request:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Close Pull Request
    steps:
      - name: Close Pull Request
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          action: "close"

环境变量

在 Azure Portal → Static Web App → Configuration 设置:

变量名 说明 示例
NEXT_PUBLIC_API_URL API 基础 URL /api
NEXT_PUBLIC_MOCK 启用 Mock 数据 false
NEXT_PUBLIC_APP_TITLE 应用标题 Admin Pro
AZURE_AD_CLIENT_ID Azure AD 客户端 ID xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_AD_TENANT_ID Azure AD 租户 ID xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
DATABASE_URL 数据库连接字符串 mongodb://...
APPLICATIONINSIGHTS_CONNECTION_STRING Application Insights 连接 InstrumentationKey=...

设置方式

bash
# Azure CLI 设置
az staticwebapp appsettings set \
  --name halolight \
  --resource-group halolight-rg \
  --setting-names \
    NEXT_PUBLIC_API_URL=/api \
    NEXT_PUBLIC_MOCK=false

# 查看设置
az staticwebapp appsettings list \
  --name halolight \
  --resource-group halolight-rg

Azure Functions

基础函数

typescript
// api/hello/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const name = req.query.name || req.body?.name || "World";

  context.res = {
    status: 200,
    headers: {
      "Content-Type": "application/json",
    },
    body: {
      message: `Hello, ${name}!`,
      timestamp: new Date().toISOString(),
    },
  };
};

export default httpTrigger;

function.json 配置

json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get", "post"],
      "route": "hello"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

定时触发函数

typescript
// api/scheduled-task/index.ts
import { AzureFunction, Context } from "@azure/functions";

const timerTrigger: AzureFunction = async function (
  context: Context,
  myTimer: any
): Promise<void> {
  const timestamp = new Date().toISOString();

  if (myTimer.isPastDue) {
    context.log("Timer is running late!");
  }

  context.log("Timer trigger executed at:", timestamp);

  // 执行定时任务
  await processScheduledTask();
};

export default timerTrigger;
json
// api/scheduled-task/function.json
{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 9 * * *"
    }
  ]
}

Cosmos DB 绑定

typescript
// api/users/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest,
  documents: any[]
): Promise<void> {
  context.res = {
    status: 200,
    body: documents,
  };
};

export default httpTrigger;
json
// api/users/function.json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get"]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "type": "cosmosDB",
      "direction": "in",
      "name": "documents",
      "databaseName": "halolight",
      "containerName": "users",
      "connection": "CosmosDBConnection",
      "sqlQuery": "SELECT * FROM c"
    }
  ]
}

Azure AD 集成

配置 MSAL

typescript
// lib/msal-config.ts
import { Configuration, PublicClientApplication } from "@azure/msal-browser";

const msalConfig: Configuration = {
  auth: {
    clientId: process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID!,
    authority: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID}`,
    redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI || "/",
    postLogoutRedirectUri: "/",
  },
  cache: {
    cacheLocation: "sessionStorage",
    storeAuthStateInCookie: false,
  },
};

export const msalInstance = new PublicClientApplication(msalConfig);

export const loginRequest = {
  scopes: ["User.Read", "openid", "profile", "email"],
};

export const graphConfig = {
  graphMeEndpoint: "https://graph.microsoft.com/v1.0/me",
};

MsalProvider 组件

tsx
// providers/msal-provider.tsx
"use client";

import { MsalProvider as MsalProviderBase } from "@azure/msal-react";
import { msalInstance } from "@/lib/msal-config";

export function MsalProvider({ children }: { children: React.ReactNode }) {
  return (
    <MsalProviderBase instance={msalInstance}>
      {children}
    </MsalProviderBase>
  );
}

登录组件

tsx
// components/auth/azure-login.tsx
"use client";

import { useMsal } from "@azure/msal-react";
import { loginRequest } from "@/lib/msal-config";
import { Button } from "@/components/ui/button";

export function AzureLogin() {
  const { instance, accounts } = useMsal();

  const handleLogin = () => {
    instance.loginRedirect(loginRequest);
  };

  const handleLogout = () => {
    instance.logoutRedirect();
  };

  if (accounts.length > 0) {
    return (
      <div className="flex items-center gap-4">
        <span>Welcome, {accounts[0].name}</span>
        <Button onClick={handleLogout} variant="outline">
          Logout
        </Button>
      </div>
    );
  }

  return (
    <Button onClick={handleLogin}>
      Sign in with Microsoft
    </Button>
  );
}

获取用户信息

typescript
// lib/graph.ts
import { graphConfig } from "./msal-config";

export async function getGraphData(accessToken: string) {
  const response = await fetch(graphConfig.graphMeEndpoint, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });

  return response.json();
}

Application Insights

配置监控

typescript
// lib/app-insights.ts
import { ApplicationInsights } from "@microsoft/applicationinsights-web";
import { ReactPlugin } from "@microsoft/applicationinsights-react-js";

const reactPlugin = new ReactPlugin();

const appInsights = new ApplicationInsights({
  config: {
    connectionString: process.env.NEXT_PUBLIC_APPINSIGHTS_CONNECTION_STRING,
    extensions: [reactPlugin],
    extensionConfig: {
      [reactPlugin.identifier]: { history: null },
    },
    enableAutoRouteTracking: true,
    enableCorsCorrelation: true,
    enableRequestHeaderTracking: true,
    enableResponseHeaderTracking: true,
  },
});

appInsights.loadAppInsights();

export { appInsights, reactPlugin };

自定义遥测

typescript
// lib/telemetry.ts
import { appInsights } from "./app-insights";

// 追踪页面浏览
export function trackPageView(name: string, properties?: Record<string, string>) {
  appInsights.trackPageView({ name, properties });
}

// 追踪事件
export function trackEvent(name: string, properties?: Record<string, string>) {
  appInsights.trackEvent({ name, properties });
}

// 追踪异常
export function trackException(error: Error, severityLevel?: number) {
  appInsights.trackException({ exception: error, severityLevel });
}

// 追踪指标
export function trackMetric(name: string, average: number) {
  appInsights.trackMetric({ name, average });
}

Cosmos DB 集成

初始化客户端

typescript
// lib/cosmos.ts
import { CosmosClient, Database, Container } from "@azure/cosmos";

const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING!);

let database: Database;
let usersContainer: Container;

export async function initCosmosDB() {
  const { database: db } = await client.databases.createIfNotExists({
    id: "halolight",
  });
  database = db;

  const { container } = await database.containers.createIfNotExists({
    id: "users",
    partitionKey: { paths: ["/id"] },
  });
  usersContainer = container;

  return { database, usersContainer };
}

export async function getUsers() {
  const { resources } = await usersContainer.items
    .query("SELECT * FROM c")
    .fetchAll();
  return resources;
}

export async function createUser(user: any) {
  const { resource } = await usersContainer.items.create(user);
  return resource;
}

常用命令

bash
# 登录
az login

# 资源组管理
az group list
az group create --name halolight-rg --location eastasia
az group delete --name halolight-rg

# Static Web App 管理
az staticwebapp list
az staticwebapp show --name halolight --resource-group halolight-rg
az staticwebapp delete --name halolight --resource-group halolight-rg

# 环境配置
az staticwebapp appsettings set --name halolight --setting-names KEY=value
az staticwebapp appsettings list --name halolight

# 自定义域名
az staticwebapp hostname set --name halolight --hostname halolight-azure.h7ml.cn
az staticwebapp hostname list --name halolight

# Functions 管理
az functionapp list
az functionapp log tail --name halolight-api

# 部署
az staticwebapp environment list --name halolight
az staticwebapp users list --name halolight

自定义域名

添加域名

bash
# CLI 方式
az staticwebapp hostname set \
  --name halolight \
  --hostname halolight-azure.h7ml.cn

# 查看域名配置
az staticwebapp hostname list --name halolight

DNS 配置

# CNAME 记录
类型: CNAME
名称: halolight-azure
值: <app-name>.azurestaticapps.net

# TXT 记录 (验证所有权)
类型: TXT
名称: _dnsauth.halolight-azure
值: <validation-token>

HTTPS

Azure Static Web Apps 自动配置 HTTPS:

  • 自动申请免费 SSL 证书
  • 自动续期
  • 支持自定义证书上传

常见问题

Q:部署失败怎么办?

A:检查以下几点:

  1. 查看 GitHub Actions 日志
  2. 确认 app_locationoutput_location 配置正确
  3. 检查 Azure Static Web Apps API Token 是否有效
  4. 确认 Node.js 版本兼容

Q:API 路由 404?

A:检查 staticwebapp.config.json 配置:

  1. 确认 api_location 指向正确目录
  2. 检查 function.json 的 route 配置
  3. 确认 Functions 已正确部署

Q:Azure AD 登录失败?

A:检查以下配置:

  1. Azure AD 应用注册的重定向 URI
  2. 客户端 ID 和租户 ID 是否正确
  3. API 权限是否已授权

Q:如何启用 CORS?

A:在 staticwebapp.config.json 中配置:

json
{
  "globalHeaders": {
    "Access-Control-Allow-Origin": "https://your-domain.com",
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization"
  }
}

费用说明

计划 价格 特性
Free 免费 100GB 带宽,2 自定义域名,内置身份验证
Standard $9/应用/月 无限带宽,5 自定义域名,SLA 99.95%
Enterprise 联系销售 私有端点,企业 SLA,专属支持

Functions 计费

资源 免费额度 超出价格
执行次数 100 万次/月 $0.20/百万次
资源消耗 40 万 GB-s/月 $0.000016/GB-s

与其他平台对比

特性 Azure Static Web Apps Vercel Netlify
全球边缘
Serverless Functions ✅ Azure Functions ✅ Edge/Serverless ✅ Functions
企业 SSO ✅ Azure AD 原生 ⚠️ 需集成 ⚠️ 需集成
托管数据库 ✅ Cosmos DB/SQL ❌ 需外部 ❌ 需外部
免费额度 100GB 100GB 100GB
企业合规 ✅ SOC/ISO/HIPAA ⚠️ 有限 ⚠️ 有限

相关链接

]]>
<![CDATA[TypeScript tRPC 网关 API]]> https://halolight.docs.h7ml.cn/guide/bff https://halolight.docs.h7ml.cn/guide/bff Fri, 19 Dec 2025 04:56:41 GMT TypeScript tRPC 网关 API

基于 tRPC 11 + Express 5 构建的类型安全 API 网关,为前端应用提供统一的端到端类型安全接口层。

API 文档https://halolight-bff.h7ml.cn

GitHubhttps://github.com/halolight/halolight-bff

特性

  • 🎯 端到端类型安全 - tRPC 提供从服务器到客户端的完整类型推导,零运行时开销
  • 🔐 JWT 双令牌认证 - Access Token + Refresh Token 自动续期,RBAC 权限控制
  • 📡 服务网关聚合 - 统一聚合多个后端服务 (Python/Java/Go/Bun),自动故障转移
  • Zod 数据验证 - 所有输入自动验证,类型安全,详细错误信息
  • 🔄 SuperJSON 序列化 - 自动处理 Date、Map、Set、BigInt、RegExp 等复杂类型
  • 🎭 请求批处理 - 自动批量处理多个请求,减少网络开销
  • 📊 分布式追踪 - Trace ID 自动传播,完整请求链路日志
  • 🐳 Docker 支持 - 容器化部署,生产级配置

技术栈

技术 版本 说明
TypeScript 5.9 编程语言
tRPC 11 RPC 框架
Zod - 数据验证
Express 5 Web 服务器
SuperJSON - 序列化
JWT - 身份认证
Pino - 日志系统

快速开始

环境要求

  • Node.js >= 20.0
  • pnpm >= 8.0
  • 至少一个后端服务 (Python/Java/Go/Bun)

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-bff.git
cd halolight-bff

# 安装依赖
pnpm install

环境变量

bash
cp .env.example .env
env
# 服务器配置
PORT=3002
HOST=0.0.0.0
NODE_ENV=development

# JWT 密钥(生产环境必须修改)
JWT_SECRET=your-super-secret-key-at-least-32-characters-long
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# CORS 配置
CORS_ORIGIN=*

# 日志级别
LOG_LEVEL=info

# 后端服务注册(至少配置一个)
HALOLIGHT_API_PYTHON_URL=http://localhost:8000
HALOLIGHT_API_BUN_URL=http://localhost:3000
HALOLIGHT_API_JAVA_URL=http://localhost:8080
HALOLIGHT_API_NESTJS_URL=http://localhost:3001
HALOLIGHT_API_NODE_URL=http://localhost:3003
HALOLIGHT_API_GO_URL=http://localhost:8081

数据库初始化

无需数据库 (API 网关不直接操作数据库)。

启动服务

bash
# 开发模式(热重载)
pnpm dev

# 生产模式
pnpm build
pnpm start

访问 http://localhost:3002

项目结构

halolight-bff/
├── src/
│   ├── index.ts              # 应用入口
│   ├── server.ts             # Express 服务器 + tRPC 适配器
│   ├── trpc.ts               # tRPC 实例和 procedure 定义
│   ├── context.ts            # Context 创建(用户、追踪、服务)
│   ├── routers/
│   │   ├── index.ts          # 根 router(组合所有模块)
│   │   ├── auth.ts           # 认证模块(8 个端点)
│   │   ├── users.ts          # 用户管理(8 个端点)
│   │   ├── dashboard.ts      # 仪表盘统计(9 个端点)
│   │   ├── permissions.ts    # 权限管理(7 个端点)
│   │   ├── roles.ts          # 角色管理(8 个端点)
│   │   ├── teams.ts          # 团队管理(9 个端点)
│   │   ├── folders.ts        # 文件夹管理(8 个端点)
│   │   ├── files.ts          # 文件管理(9 个端点)
│   │   ├── documents.ts      # 文档管理(10 个端点)
│   │   ├── calendar.ts       # 日历事件(10 个端点)
│   │   ├── notifications.ts  # 通知(7 个端点)
│   │   └── messages.ts       # 消息/聊天(9 个端点)
│   ├── middleware/
│   │   └── auth.ts           # JWT 认证/授权中间件
│   ├── services/
│   │   ├── httpClient.ts     # HTTP 客户端(后端通信)
│   │   └── serviceRegistry.ts # 后端服务注册表
│   └── schemas/
│       ├── index.ts          # Schema 导出
│       └── common.ts         # 通用 Zod schemas(分页、排序、响应)
├── .env.example              # 环境变量模板
├── .github/workflows/        # CI/CD 配置
├── Dockerfile                # Docker 镜像构建
├── docker-compose.yml        # Docker Compose 配置
├── package.json              # 依赖配置
└── tsconfig.json             # TypeScript 配置

API 模块

HaloLight BFF 提供 12 个核心业务模块,覆盖 100+ tRPC 端点

模块 端点数 描述
auth 8 登录、注册、令牌刷新、登出、密码管理
users 8 用户 CRUD、角色/状态管理、个人资料
dashboard 9 统计数据、访问趋势、销售数据、任务、日程
permissions 7 权限 CRUD、树结构、模块权限、批量操作
roles 8 角色 CRUD、权限分配、用户关联
teams 9 团队 CRUD、成员管理、邀请、权限
folders 8 文件夹 CRUD、树结构、移动、面包屑
files 9 文件 CRUD、上传、下载、移动、复制、共享
documents 10 文档 CRUD、版本控制、协作、分享
calendar 10 事件 CRUD、参与者管理、RSVP、提醒
notifications 7 通知列表、未读数、标记已读、批量删除
messages 9 对话管理、消息 CRUD、发送、已读状态

认证相关端点

Procedure 类型 描述 权限
auth.login mutation 用户登录 公开
auth.register mutation 用户注册 公开
auth.refresh mutation 刷新令牌 公开
auth.logout mutation 退出登录 需认证
auth.forgotPassword mutation 忘记密码 公开
auth.resetPassword mutation 重置密码 公开
auth.verifyEmail mutation 验证邮箱 公开
auth.changePassword mutation 修改密码 需认证

用户管理端点

Procedure 类型 描述 权限
users.list query 获取用户列表 users:view
users.byId query 获取用户详情 users:view
users.me query 获取当前用户 需认证
users.create mutation 创建用户 users:create
users.update mutation 更新用户 users:update
users.delete mutation 删除用户 users:delete
users.updateRole mutation 更新用户角色 users:update
users.updateStatus mutation 更新用户状态 users:update

完整端点清单

仪表盘 (Dashboard) - 9 个端点

Procedure 类型 描述
dashboard.getStats query 统计数据(用户、文档、文件、任务)
dashboard.getVisits query 访问趋势(7天/30天)
dashboard.getSales query 销售数据(折线图)
dashboard.getPieData query 饼图数据(分类占比)
dashboard.getTasks query 待办任务列表
dashboard.getCalendar query 今日日程
dashboard.getActivities query 最近活动
dashboard.getNotifications query 最新通知
dashboard.getProgress query 项目进度

权限管理 (Permissions) - 7 个端点

Procedure 类型 描述
permissions.list query 获取权限列表
permissions.tree query 获取权限树
permissions.byId query 获取权限详情
permissions.create mutation 创建权限
permissions.update mutation 更新权限
permissions.delete mutation 删除权限
permissions.modules query 获取权限模块

角色管理 (Roles) - 8 个端点

Procedure 类型 描述
roles.list query 获取角色列表
roles.byId query 获取角色详情
roles.create mutation 创建角色
roles.update mutation 更新角色
roles.delete mutation 删除角色
roles.assignPermissions mutation 分配权限
roles.removePermissions mutation 移除权限
roles.users query 获取角色下的用户

团队管理 (Teams) - 9 个端点

Procedure 类型 描述
teams.list query 获取团队列表
teams.byId query 获取团队详情
teams.create mutation 创建团队
teams.update mutation 更新团队
teams.delete mutation 删除团队
teams.addMember mutation 添加成员
teams.removeMember mutation 移除成员
teams.updateMemberRole mutation 更新成员角色
teams.members query 获取团队成员

文件夹管理 (Folders) - 8 个端点

Procedure 类型 描述
folders.list query 获取文件夹列表
folders.tree query 获取文件夹树
folders.byId query 获取文件夹详情
folders.create mutation 创建文件夹
folders.update mutation 更新文件夹
folders.delete mutation 删除文件夹
folders.move mutation 移动文件夹
folders.breadcrumb query 获取面包屑路径

文件管理 (Files) - 9 个端点

Procedure 类型 描述
files.list query 获取文件列表
files.byId query 获取文件详情
files.upload mutation 上传文件
files.update mutation 更新文件信息
files.delete mutation 删除文件
files.move mutation 移动文件
files.copy mutation 复制文件
files.download query 获取下载链接
files.share mutation 共享文件

文档管理 (Documents) - 10 个端点

Procedure 类型 描述
documents.list query 获取文档列表
documents.byId query 获取文档详情
documents.create mutation 创建文档
documents.update mutation 更新文档
documents.delete mutation 删除文档
documents.versions query 获取版本历史
documents.restore mutation 恢复版本
documents.share mutation 共享文档
documents.unshare mutation 取消共享
documents.collaborators query 获取协作者

日历管理 (Calendar) - 10 个端点

Procedure 类型 描述
calendar.events query 获取日程列表
calendar.byId query 获取日程详情
calendar.create mutation 创建日程
calendar.update mutation 更新日程
calendar.delete mutation 删除日程
calendar.addAttendee mutation 添加参与者
calendar.removeAttendee mutation 移除参与者
calendar.rsvp mutation RSVP 响应
calendar.setReminder mutation 设置提醒
calendar.byMonth query 按月获取日程

通知管理 (Notifications) - 7 个端点

Procedure 类型 描述
notifications.list query 获取通知列表
notifications.unreadCount query 获取未读数
notifications.markRead mutation 标记已读
notifications.markAllRead mutation 全部已读
notifications.delete mutation 删除通知
notifications.deleteAll mutation 删除全部
notifications.preferences query 获取通知偏好

消息管理 (Messages) - 9 个端点

Procedure 类型 描述
messages.conversations query 获取对话列表
messages.byConversation query 获取对话消息
messages.send mutation 发送消息
messages.markRead mutation 标记已读
messages.delete mutation 删除消息
messages.createConversation mutation 创建对话
messages.deleteConversation mutation 删除对话
messages.search query 搜索消息
messages.unreadCount query 获取未读数

核心概念

tRPC Procedures

tRPC 提供三种 procedure 类型:

typescript
// 公开端点 - 无需认证
export const publicProcedure = t.procedure;

// 受保护端点 - 需要有效 JWT
export const protectedProcedure = t.procedure.use(isAuthenticated);

// 管理员端点 - 需要 admin 角色
export const adminProcedure = t.procedure.use(isAdmin);

使用示例

typescript
export const usersRouter = router({
  // Query - 查询数据
  list: protectedProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().default(10),
      keyword: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      // ctx.user 包含已认证用户信息
      const client = ctx.services.getDefault();
      const data = await client.get('/api/users', { query: input });
      return { code: 200, message: 'success', data };
    }),

  // Mutation - 修改数据
  create: adminProcedure
    .input(z.object({
      name: z.string().min(2),
      email: z.string().email(),
      role: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.post('/api/users', { body: input });
      return { code: 201, message: 'Created', data };
    }),
});

Context

每个请求都会创建一个独立的 context:

typescript
interface Context {
  req: Request;                    // Express 请求对象
  res: Response;                   // Express 响应对象
  user: JWTPayload | null;         // 已认证用户(通过 JWT)
  traceId: string;                 // 分布式追踪 ID(UUID)
  services: ServiceRegistry;       // 后端服务注册表
}

Context 创建流程

  1. 解析 Authorization 头中的 JWT Token
  2. 验证 Token 有效性,提取用户信息
  3. 生成唯一的 traceId (用于分布式追踪)
  4. 注入 ServiceRegistry (后端服务集合)

JWT Token 结构

typescript
interface JWTPayload {
  id: string;                      // 用户 ID
  name: string;                    // 用户名
  email: string;                   // 邮箱
  role: {
    id: string;                    // 角色 ID
    name: string;                  // 角色名称(如 admin, user)
    label: string;                 // 角色显示名称
    permissions: string[];         // 权限列表(如 ["users:*", "documents:view"])
  };
}

Token 使用

typescript
// 客户端发送请求
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// 服务端自动解析并注入到 ctx.user
const userId = ctx.user.id;
const userPermissions = ctx.user.role.permissions;

权限系统

支持灵活的通配符权限匹配:

权限格式 说明 示例
* 所有权限(超级管理员) 可执行任何操作
{resource}:* 模块所有操作 users:* = 用户模块所有权限
{resource}:{action} 特定操作 users:view = 仅查看用户

权限检查示例

typescript
// 在 middleware 中检查权限
export const requirePermission = (permission: string) => {
  return t.middleware(({ ctx, next }) => {
    if (!ctx.user) {
      throw new TRPCError({ code: 'UNAUTHORIZED' });
    }

    const hasPermission = ctx.user.role.permissions.some(p =>
      p === '*' ||
      p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    );

    if (!hasPermission) {
      throw new TRPCError({ code: 'FORBIDDEN' });
    }

    return next();
  });
};

// 使用
export const deleteUser = protectedProcedure
  .use(requirePermission('users:delete'))
  .input(z.object({ id: z.string() }))
  .mutation(async ({ input, ctx }) => {
    // 只有拥有 users:delete 权限的用户可以执行
  });

服务注册与发现

通过环境变量配置多个后端服务:

bash
# Python FastAPI
HALOLIGHT_API_PYTHON_URL=http://api-python:8000

# Bun Hono
HALOLIGHT_API_BUN_URL=http://api-bun:3000

# Java Spring Boot
HALOLIGHT_API_JAVA_URL=http://api-java:8080

# Go Fiber
HALOLIGHT_API_GO_URL=http://api-go:8081

服务优先级:按配置顺序,第一个可用的服务作为默认服务。

使用示例

typescript
// 使用默认服务(优先级最高的)
const client = ctx.services.getDefault();
const data = await client.get('/api/users');

// 使用特定服务
const pythonClient = ctx.services.get('python');
const stats = await pythonClient.get('/api/dashboard/stats');

// 故障转移:如果默认服务不可用,自动切换到下一个服务
try {
  const data = await ctx.services.getDefault().get('/api/users');
} catch (error) {
  // ServiceRegistry 自动重试其他服务
}

响应格式

所有 API 遵循统一的响应结构:

typescript
// 标准响应
interface APIResponse<T> {
  code: number;        // HTTP 状态码(200, 201, 400, 500...)
  message: string;     // 人类可读消息(success, error, ...)
  data: T | null;      // 响应数据(成功时)或 null(失败时)
}

// 分页响应
interface PaginatedResponse<T> {
  code: number;
  message: string;
  data: {
    list: T[];         // 数据列表
    total: number;     // 总记录数
    page: number;      // 当前页码
    limit: number;     // 每页条数
    totalPages?: number; // 总页数(可选)
  };
}

示例

typescript
// 成功响应
{
  "code": 200,
  "message": "success",
  "data": {
    "id": "1",
    "name": "John Doe",
    "email": "[email protected]"
  }
}

// 分页响应
{
  "code": 200,
  "message": "success",
  "data": {
    "list": [{ "id": "1", "name": "User 1" }],
    "total": 100,
    "page": 1,
    "limit": 10,
    "totalPages": 10
  }
}

// 错误响应(tRPC 自动格式化)
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Not authenticated"
  }
}

认证机制

JWT 双令牌

Access Token:  15 分钟有效期,用于 API 请求
Refresh Token: 7 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

typescript
// 客户端示例
const refreshToken = async () => {
  const refreshToken = localStorage.getItem('refreshToken');
  const result = await trpc.auth.refresh.mutate({ refreshToken });

  localStorage.setItem('accessToken', result.data.accessToken);
  localStorage.setItem('refreshToken', result.data.refreshToken);

  return result.data.accessToken;
};

// tRPC 客户端配置 - 自动刷新
const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      async headers() {
        let token = localStorage.getItem('accessToken');

        // 如果 token 过期,自动刷新
        if (isTokenExpired(token)) {
          token = await refreshToken();
        }

        return {
          authorization: `Bearer ${token}`,
        };
      },
    }),
  ],
});

错误处理

tRPC 错误类型

typescript
import { TRPCError } from '@trpc/server';

// 400 - 请求参数错误
throw new TRPCError({
  code: 'BAD_REQUEST',
  message: 'Invalid input',
});

// 401 - 未认证
throw new TRPCError({
  code: 'UNAUTHORIZED',
  message: 'Not authenticated',
});

// 403 - 无权限
throw new TRPCError({
  code: 'FORBIDDEN',
  message: 'Insufficient permissions',
});

// 404 - 资源不存在
throw new TRPCError({
  code: 'NOT_FOUND',
  message: 'Resource not found',
});

// 409 - 资源冲突
throw new TRPCError({
  code: 'CONFLICT',
  message: 'Email already exists',
});

// 500 - 服务器错误
throw new TRPCError({
  code: 'INTERNAL_SERVER_ERROR',
  message: 'Something went wrong',
});

错误响应格式

json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Not authenticated",
    "data": {
      "code": "UNAUTHORIZED",
      "httpStatus": 401,
      "path": "auth.login"
    }
  }
}

客户端使用

React + @tanstack/react-query

typescript
import { createTRPCReact } from '@trpc/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from 'halolight-bff';

// 创建 tRPC React hooks
const trpc = createTRPCReact<AppRouter>();

// 创建 tRPC 客户端
const trpcClient = trpc.createClient({
  transformer: superjson, // 支持 Date、Map、Set 等复杂类型
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      headers() {
        return {
          authorization: `Bearer ${localStorage.getItem('token')}`,
        };
      },
    }),
  ],
});

// 创建 React Query 客户端
const queryClient = new QueryClient();

// 根组件
function App() {
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <UserList />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

// 使用 tRPC hooks
function UserList() {
  // Query - 自动管理加载状态、缓存、重新获取
  const { data, isLoading, error } = trpc.users.list.useQuery({
    page: 1,
    limit: 10,
  });

  // Mutation - 自动管理加载状态、错误处理
  const createUser = trpc.users.create.useMutation({
    onSuccess: () => {
      // 自动刷新用户列表
      trpc.users.list.invalidate();
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <button onClick={() => createUser.mutate({
        name: 'New User',
        email: '[email protected]',
        role: 'user',
      })}>
        Create User
      </button>

      {data?.data.list.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Next.js App Router

typescript
// app/api/trpc/[trpc]/route.ts - tRPC API 路由
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  });

export { handler as GET, handler as POST };

// app/providers.tsx - tRPC Provider
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import superjson from 'superjson';
import type { AppRouter } from '@/server/routers';

const trpc = createTRPCReact<AppRouter>();

export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      transformer: superjson,
      links: [
        httpBatchLink({
          url: '/api/trpc',
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

// app/page.tsx - Server Component
import { createCaller } from '@/server/routers';

export default async function Page() {
  const caller = createCaller({ req: {}, res: {}, user: null });
  const stats = await caller.dashboard.getStats();

  return <div>Total Users: {stats.data.totalUsers}</div>;
}

Vue 3 + TanStack Query

typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query';
import superjson from 'superjson';
import type { AppRouter } from 'halolight-bff';

// 创建 tRPC 客户端
const trpc = createTRPCProxyClient<AppRouter>({
  transformer: superjson,
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      headers() {
        return {
          authorization: `Bearer ${localStorage.getItem('token')}`,
        };
      },
    }),
  ],
});

// 在组件中使用
export default {
  setup() {
    const queryClient = useQueryClient();

    // Query
    const { data, isLoading } = useQuery({
      queryKey: ['users', { page: 1 }],
      queryFn: () => trpc.users.list.query({ page: 1, limit: 10 }),
    });

    // Mutation
    const createUser = useMutation({
      mutationFn: (user) => trpc.users.create.mutate(user),
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ['users'] });
      },
    });

    return { data, isLoading, createUser };
  },
};

Vanilla TypeScript

typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from 'halolight-bff';
import superjson from 'superjson';

const client = createTRPCProxyClient<AppRouter>({
  transformer: superjson,
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      headers() {
        return {
          authorization: `Bearer ${localStorage.getItem('token')}`,
        };
      },
    }),
  ],
});

// 使用(完整类型推导)
const users = await client.users.list.query({ page: 1 });
console.log(users.data.list); // TS 自动推导类型

const newUser = await client.users.create.mutate({
  name: 'John',
  email: '[email protected]',
  role: 'user',
});

开发指南

添加新 Router

  1. 创建新的 router 文件
typescript
// src/routers/products.ts
import { z } from 'zod';
import { router, protectedProcedure, adminProcedure } from '../trpc';

export const productsRouter = router({
  // Query - 获取产品列表
  list: protectedProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().default(10),
      category: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.get('/api/products', { query: input });
      return { code: 200, message: 'success', data };
    }),

  // Query - 获取产品详情
  byId: protectedProcedure
    .input(z.object({
      id: z.string(),
    }))
    .query(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.get(`/api/products/${input.id}`);
      return { code: 200, message: 'success', data };
    }),

  // Mutation - 创建产品(需要管理员权限)
  create: adminProcedure
    .input(z.object({
      name: z.string().min(2),
      price: z.number().positive(),
      category: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      const data = await client.post('/api/products', { body: input });
      return { code: 201, message: 'Created', data };
    }),

  // Mutation - 更新产品
  update: adminProcedure
    .input(z.object({
      id: z.string(),
      name: z.string().min(2).optional(),
      price: z.number().positive().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { id, ...updateData } = input;
      const client = ctx.services.getDefault();
      const data = await client.put(`/api/products/${id}`, { body: updateData });
      return { code: 200, message: 'Updated', data };
    }),

  // Mutation - 删除产品
  delete: adminProcedure
    .input(z.object({
      id: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const client = ctx.services.getDefault();
      await client.delete(`/api/products/${input.id}`);
      return { code: 200, message: 'Deleted', data: null };
    }),
});
  1. 在根 router 中注册
typescript
// src/routers/index.ts
import { router } from '../trpc';
import { authRouter } from './auth';
import { usersRouter } from './users';
import { productsRouter } from './products'; // 导入新 router

export const appRouter = router({
  auth: authRouter,
  users: usersRouter,
  products: productsRouter, // 注册新 router
  // ... 其他 routers
});

export type AppRouter = typeof appRouter;
  1. 客户端使用
typescript
// 类型自动推导,无需手动定义
const products = await trpc.products.list.query({ page: 1 });
const product = await trpc.products.byId.query({ id: '1' });
const newProduct = await trpc.products.create.mutate({
  name: 'iPhone 15',
  price: 999,
  category: 'electronics',
});

添加自定义 Middleware

typescript
// src/middleware/rateLimit.ts
import { TRPCError } from '@trpc/server';
import { t } from '../trpc';

const rateLimitMap = new Map<string, { count: number; resetAt: number }>();

export const rateLimit = (maxRequests: number, windowMs: number) => {
  return t.middleware(({ ctx, next }) => {
    const key = ctx.user?.id || ctx.req.ip;
    const now = Date.now();

    const record = rateLimitMap.get(key);

    if (!record || now > record.resetAt) {
      rateLimitMap.set(key, { count: 1, resetAt: now + windowMs });
      return next();
    }

    if (record.count >= maxRequests) {
      throw new TRPCError({
        code: 'TOO_MANY_REQUESTS',
        message: 'Rate limit exceeded',
      });
    }

    record.count++;
    return next();
  });
};

// 使用
export const limitedProcedure = protectedProcedure.use(
  rateLimit(10, 60000) // 每分钟最多 10 个请求
);

添加 Schema 验证

typescript
// src/schemas/product.ts
import { z } from 'zod';

export const productSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(100),
  price: z.number().positive(),
  category: z.enum(['electronics', 'clothing', 'books']),
  stock: z.number().int().nonnegative(),
  createdAt: z.date(),
  updatedAt: z.date(),
});

export const createProductSchema = productSchema.omit({
  id: true,
  createdAt: true,
  updatedAt: true,
});

export const updateProductSchema = createProductSchema.partial();

// 在 router 中使用
export const productsRouter = router({
  create: adminProcedure
    .input(createProductSchema)
    .mutation(async ({ input, ctx }) => {
      // input 已经过 Zod 验证,类型安全
    }),

  update: adminProcedure
    .input(z.object({
      id: z.string(),
      data: updateProductSchema,
    }))
    .mutation(async ({ input, ctx }) => {
      // ...
    }),
});

常用命令

bash
# 开发
pnpm dev              # 启动开发服务器(热重载)
pnpm dev:watch        # 启动开发服务器(文件监听)

# 构建
pnpm build            # 构建生产版本
pnpm start            # 启动生产服务器

# 测试
pnpm test             # 运行测试
pnpm test:watch       # 监听模式运行测试
pnpm test:coverage    # 生成测试覆盖率

# 代码质量
pnpm lint             # 运行 ESLint
pnpm lint:fix         # 自动修复 lint 错误
pnpm type-check       # TypeScript 类型检查
pnpm format           # Prettier 格式化代码

部署

Docker

bash
# 构建镜像
docker build -t halolight-bff .

# 运行容器
docker run -p 3002:3002 \
  -e JWT_SECRET=your-secret-key \
  -e HALOLIGHT_API_PYTHON_URL=http://api-python:8000 \
  halolight-bff

Docker Compose

yaml
# docker-compose.yml
version: '3.8'

services:
  bff:
    build: .
    ports:
      - "3002:3002"
    environment:
      - NODE_ENV=production
      - JWT_SECRET=${JWT_SECRET}
      - HALOLIGHT_API_PYTHON_URL=http://api-python:8000
      - HALOLIGHT_API_BUN_URL=http://api-bun:3000
      - HALOLIGHT_API_JAVA_URL=http://api-java:8080
    depends_on:
      - api-python
      - api-bun
      - api-java
    restart: unless-stopped

  api-python:
    image: halolight-api-python
    ports:
      - "8000:8000"

  api-bun:
    image: halolight-api-bun
    ports:
      - "3000:3000"

  api-java:
    image: halolight-api-java
    ports:
      - "8080:8080"
bash
docker-compose up -d

生产环境配置

env
NODE_ENV=production
PORT=3002
HOST=0.0.0.0

# 强密钥(至少 32 字符)
JWT_SECRET=your-production-secret-key-with-at-least-32-characters
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# 限制 CORS
CORS_ORIGIN=https://your-frontend.com

# 生产日志
LOG_LEVEL=warn

# 后端服务
HALOLIGHT_API_PYTHON_URL=https://api-python.production.com
HALOLIGHT_API_BUN_URL=https://api-bun.production.com
HALOLIGHT_API_JAVA_URL=https://api-java.production.com

性能优化

1。启用请求批处理

tRPC 自动批处理多个并发请求,减少网络开销:

typescript
// 客户端配置
const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: 'http://localhost:3002/trpc',
      maxURLLength: 2083, // 最大 URL 长度
    }),
  ],
});

// 这三个请求会自动批处理为一个 HTTP 请求
const [users, stats, notifications] = await Promise.all([
  trpc.users.list.query({ page: 1 }),
  trpc.dashboard.getStats.query(),
  trpc.notifications.unreadCount.query(),
]);

2。使用 DataLoader 避免 N+1 查询

typescript
import DataLoader from 'dataloader';

// 创建 DataLoader
const userLoader = new DataLoader(async (ids: string[]) => {
  const users = await db.user.findMany({
    where: { id: { in: ids } },
  });
  return ids.map(id => users.find(u => u.id === id));
});

// 在 context 中注入
export const createContext = (opts: CreateExpressContextOptions) => {
  return {
    ...opts,
    loaders: {
      user: userLoader,
    },
  };
};

// 在 router 中使用
export const postsRouter = router({
  list: protectedProcedure.query(async ({ ctx }) => {
    const posts = await db.post.findMany();

    // 批量加载作者信息,避免 N+1 查询
    const postsWithAuthors = await Promise.all(
      posts.map(async (post) => ({
        ...post,
        author: await ctx.loaders.user.load(post.authorId),
      }))
    );

    return postsWithAuthors;
  }),
});

3。缓存策略

typescript
// 使用 Redis 缓存
import Redis from 'ioredis';

const redis = new Redis();

export const dashboardRouter = router({
  getStats: protectedProcedure.query(async ({ ctx }) => {
    const cacheKey = `dashboard:stats:${ctx.user.id}`;

    // 尝试从缓存获取
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // 从后端服务获取
    const client = ctx.services.getDefault();
    const data = await client.get('/api/dashboard/stats');

    // 缓存 5 分钟
    await redis.setex(cacheKey, 300, JSON.stringify(data));

    return data;
  }),
});

4。限流保护

typescript
import rateLimit from 'express-rate-limit';

// 在 Express 中配置全局限流
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100, // 最多 100 个请求
  message: 'Too many requests from this IP',
});

app.use('/trpc', limiter);

安全建议

1。使用强 JWT 密钥

bash
# 生成强密钥(至少 32 字符)
openssl rand -base64 32

# 在 .env 中配置
JWT_SECRET=your-generated-secret-key-with-at-least-32-characters

2。启用 HTTPS

typescript
// 在生产环境强制使用 HTTPS
if (process.env.NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (!req.secure) {
      return res.redirect(`https://${req.headers.host}${req.url}`);
    }
    next();
  });
}

3。限制 CORS

bash
# 只允许特定源
CORS_ORIGIN=https://your-frontend.com

# 或多个源(逗号分隔)
CORS_ORIGIN=https://app1.com,https://app2.com

4。输入验证

typescript
// 使用 Zod 严格验证所有输入
export const createUser = protectedProcedure
  .input(z.object({
    name: z.string().min(2).max(50),
    email: z.string().email(),
    age: z.number().int().positive().max(150),
    role: z.enum(['admin', 'user']),
  }))
  .mutation(async ({ input, ctx }) => {
    // input 已经过严格验证
  });

5。日志脱敏

typescript
// 使用 Pino redact 配置
const logger = pino({
  redact: {
    paths: [
      'req.headers.authorization',
      'req.body.password',
      'req.body.token',
      'res.headers["set-cookie"]',
    ],
    remove: true, // 完全移除敏感字段
  },
});

可观测性

日志系统

typescript
// 使用 Pino 结构化日志
import pino from 'pino';
import pinoHttp from 'pino-http';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'SYS:standard',
      ignore: 'pid,hostname',
    },
  },
});

// HTTP 请求日志
app.use(pinoHttp({ logger }));

// 在 router 中使用
export const usersRouter = router({
  create: adminProcedure.mutation(async ({ input, ctx }) => {
    logger.info({ userId: ctx.user.id, input }, 'Creating user');

    try {
      const data = await createUser(input);
      logger.info({ userId: ctx.user.id, data }, 'User created');
      return data;
    } catch (error) {
      logger.error({ userId: ctx.user.id, error }, 'Failed to create user');
      throw error;
    }
  }),
});

健康检查

typescript
// 健康检查端点
app.get('/health', async (req, res) => {
  try {
    // 检查后端服务连接
    const services = await Promise.all([
      fetch(`${process.env.HALOLIGHT_API_PYTHON_URL}/health`),
      fetch(`${process.env.HALOLIGHT_API_BUN_URL}/health`),
    ]);

    const allHealthy = services.every(s => s.ok);

    res.status(allHealthy ? 200 : 503).json({
      status: allHealthy ? 'healthy' : 'unhealthy',
      timestamp: new Date().toISOString(),
      services: {
        python: services[0].ok,
        bun: services[1].ok,
      },
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message,
    });
  }
});

监控指标

typescript
// Prometheus 指标
import promClient from 'prom-client';

// 创建注册表
const register = new promClient.Registry();

// 收集默认指标
promClient.collectDefaultMetrics({ register });

// 自定义指标
const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

// 暴露指标端点
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

常见问题

Q:端口已被占用

A:修改 .env 中的 PORT 配置,或者终止占用端口的进程:

bash
# 查找占用端口的进程
lsof -i :3002

# 终止进程
kill -9 <PID>

# 或修改端口
echo "PORT=3003" >> .env

Q:CORS 错误

A:更新 .env 中的 CORS_ORIGIN 为允许的源地址:

bash
# 开发环境允许所有源
CORS_ORIGIN=*

# 生产环境指定源
CORS_ORIGIN=https://your-frontend.com

Q:Token 验证失败

A:确保 JWT_SECRET 在所有环境中保持一致:

bash
# 检查 JWT_SECRET 是否一致
echo $JWT_SECRET

# 重新生成 Token
curl -X POST http://localhost:3002/trpc/auth.login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password"}'

Q:后端服务连接失败

A:检查后端服务是否正常运行,以及 URL 配置是否正确:

bash
# 检查服务健康状态
curl http://localhost:8000/health
curl http://localhost:3000/health

# 检查环境变量
echo $HALOLIGHT_API_PYTHON_URL
echo $HALOLIGHT_API_BUN_URL

# 测试连接
curl http://localhost:3002/health

Q:类型推导不工作

A:确保正确导出 AppRouter 类型,并在客户端正确引入:

typescript
// 服务端 - src/routers/index.ts
export const appRouter = router({
  // ... routers
});

export type AppRouter = typeof appRouter; // 必须导出类型

// 客户端 - 确保从正确的路径导入
import type { AppRouter } from 'halolight-bff'; // NPM 包
// 或
import type { AppRouter } from '@/server/routers'; // Monorepo

与其他网关对比

特性 tRPC BFF GraphQL REST API gRPC
类型安全 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
开发体验 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐
性能 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
学习曲线 ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
生态系统 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
文档 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐

相关链接

]]>
<![CDATA[Cloudflare 部署]]> https://halolight.docs.h7ml.cn/guide/cloudflare https://halolight.docs.h7ml.cn/guide/cloudflare Fri, 19 Dec 2025 04:56:41 GMT Cloudflare 部署

HaloLight Cloudflare 部署版本,基于 Next.js 15 App Router + React 19 构建,使用 @opennextjs/cloudflare 适配器部署到 Cloudflare Workers/Pages 边缘运行时,实现全球 300+ 节点低延迟访问。

在线预览https://halolight-cloudflare.h7ml.cn/

GitHubhttps://github.com/halolight/halolight-cloudflare

特性

  • ☁️ Cloudflare Workers - 全球边缘计算,< 50ms 冷启动
  • 📄 Cloudflare Pages - 静态资源全球分发
  • 💾 KV 存储 - 全球分布式键值存储
  • 🗄️ D1 数据库 - 边缘 SQLite 数据库
  • 📦 R2 对象存储 - S3 兼容,零出口费
  • 🤖 Workers AI - 边缘 AI 推理
  • 🔄 Durable Objects - 有状态边缘对象
  • 📊 Analytics Engine - 实时分析引擎
  • 🔒 Zero Trust - 企业级安全访问
  • 🌐 300+ 全球节点 - 极致低延迟

与原版差异

特性 原版 (Next.js) Cloudflare 版
Next.js 14.x 15.5.x
React 18.x 19.x
运行时 Node.js (Vercel) Cloudflare Workers (Edge)
部署平台 Vercel / Docker Cloudflare Pages
开发工具 webpack Turbopack
部署命令 pnpm build && pnpm start pnpm deploy
SSR 位置 服务器/Serverless 全球边缘节点
冷启动 取决于平台 < 50ms

技术栈

技术 版本 说明
Next.js 15.5.x React 全栈框架
React 19.x UI 库
TypeScript 5.x 类型安全
Tailwind CSS 4.x 原子化 CSS
@opennextjs/cloudflare 1.x Cloudflare 适配层
Wrangler 4.x Cloudflare CLI
shadcn/ui latest UI 组件库
Zustand 5.x 状态管理
TanStack Query 5.x 服务端状态
Vitest 4.x 单元测试
Mock.js 1.x 数据模拟

目录结构

halolight-cloudflare/
├── src/
│   ├── app/                      # App Router 页面
│   │   ├── (dashboard)/          # 管理后台路由组
│   │   ├── (auth)/               # 认证路由组
│   │   ├── (legal)/              # 法律条款路由组
│   │   ├── layout.tsx            # 根布局
│   │   └── page.tsx              # 首页
│   ├── components/               # React 组件
│   │   ├── ui/                   # shadcn/ui 组件
│   │   ├── layout/               # 布局组件
│   │   └── dashboard/            # 仪表盘组件
│   ├── hooks/                    # React Hooks
│   ├── stores/                   # Zustand Stores
│   ├── lib/                      # 工具库
│   ├── mock/                     # Mock 数据
│   ├── providers/                # Context Providers
│   ├── config/                   # 配置文件
│   └── __tests__/                # 单元测试
├── public/                       # 静态资源
├── .github/workflows/            # GitHub Actions CI
├── .open-next/                   # OpenNext 构建产物(自动生成)
├── coverage/                     # 测试覆盖率(自动生成)
├── cloudflare-env.d.ts           # Cloudflare 环境类型
├── vitest.config.ts              # Vitest 测试配置
├── open-next.config.ts           # OpenNext 配置
├── wrangler.jsonc                # Wrangler 配置
├── next.config.ts                # Next.js 配置
└── package.json

快速开始

环境要求

  • Node.js >= 18
  • pnpm >= 8
  • Wrangler CLI (需登录 Cloudflare)

安装

bash
git clone https://github.com/halolight/halolight-cloudflare.git
cd halolight-cloudflare
pnpm install

环境变量

bash
cp .dev.vars.example .dev.vars
bash
# .dev.vars 示例
NEXT_PUBLIC_API_URL=/api
NEXT_PUBLIC_MOCK=true
NEXT_PUBLIC_APP_TITLE=HaloLight
NEXT_PUBLIC_BRAND_NAME=HaloLight
NEXT_PUBLIC_DEMO_EMAIL=[email protected]
NEXT_PUBLIC_DEMO_PASSWORD=Admin@123

启动开发

bash
pnpm dev

访问 http://localhost:3000

本地预览 (Edge 环境)

bash
pnpm preview

模拟 Cloudflare Workers 环境,检测 Edge Runtime 兼容性问题。

部署到 Cloudflare

bash
wrangler login   # 首次需要登录
pnpm deploy      # 构建并部署

常用脚本

bash
pnpm dev          # 启动开发服务器(Turbopack,Node.js 环境)
pnpm build        # Next.js 生产构建
pnpm preview      # 本地预览 Cloudflare 环境
pnpm deploy       # 部署到 Cloudflare
pnpm upload       # 仅上传不部署
pnpm lint         # ESLint 检查
pnpm type-check   # TypeScript 类型检查
pnpm test         # 运行单元测试(watch 模式)
pnpm test:run     # 运行单元测试(单次)
pnpm test:coverage # 运行测试并生成覆盖率报告
pnpm cf-typegen   # 生成 Cloudflare 环境类型

Edge Runtime 约束

Cloudflare Workers 是边缘运行时,部分 Node.js API 不可用:

不可用的 API

  • fs - 文件系统操作
  • child_process - 子进程
  • netdgram - 原生网络套接字
  • crypto.createCipher 等旧加密 API

部分可用 (通过 nodejs_compat):

  • Buffer - 二进制数据处理
  • process.env - 环境变量
  • crypto 部分 API - 如 randomUUID()

注意

使用 @opennextjs/cloudflare 时,整个应用自动运行在边缘环境,无需手动声明 export const runtime = 'edge'

Cloudflare 服务集成

可用服务

服务 用途 说明
KV 键值存储 全球分布式缓存
D1 SQLite 数据库 边缘 SQL 数据库
R2 对象存储 S3 兼容存储
Queues 消息队列 异步任务处理
Durable Objects 有状态对象 实时协作
Workers AI AI 推理 边缘 AI 模型

使用示例

ts
import { getRequestContext } from '@opennextjs/cloudflare';

export async function GET() {
  const { env } = getRequestContext();
  const value = await env.MY_KV.get('key');
  return Response.json({ value });
}

配置 KV 存储

jsonc
// wrangler.jsonc
{
  "kv_namespaces": [
    { "binding": "MY_KV", "id": "xxx" }
  ]
}

配置 D1 数据库

jsonc
// wrangler.jsonc
{
  "d1_databases": [
    { "binding": "MY_DB", "database_id": "xxx" }
  ]
}

SSR/SSG/ISR 支持

渲染模式 支持状态 说明
SSR ✅ 支持 每次请求在边缘渲染
SSG ✅ 支持 构建时生成静态页面
ISR ⚠️ 部分 需配置 R2 缓存

启用 ISR

ts
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";

export default defineCloudflareConfig({
  incrementalCache: r2IncrementalCache,
});

CI/CD

项目已配置完整的 GitHub Actions CI 工作流:

Job 说明
lint ESLint + TypeScript 类型检查
test Vitest 单元测试 + Codecov 覆盖率
build OpenNext Cloudflare 生产构建
security 依赖安全审计
dependency-review PR 依赖变更审查

部署工作流示例

yaml
# .github/workflows/deploy.yml
name: Deploy to Cloudflare

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install
      - run: pnpm deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}

部署架构

用户请求 → Cloudflare CDN → Workers (Edge) → KV/D1/R2/外部 API

          全球 300+ 节点
          就近响应 < 50ms

配额限制

限制项 免费版 付费版
Worker 脚本大小 1MB(压缩后) 10MB
CPU 时间 10-50ms 数秒
内存 128MB 128MB
子请求数 50 1000
请求持续时间 30s 30s

参考

实际限制请查阅 Cloudflare 官方文档

版本回滚

Cloudflare Pages 保留历史部署,支持以下回滚方式:

  1. Dashboard 回滚

    • Cloudflare Dashboard → Workers & Pages → 项目 → Deployments
    • 选择历史版本 → “Rollback to this deployment”
  2. 重新部署指定提交

    bash
    git checkout <commit-hash>
    pnpm deploy

常见问题

“Cannot find module ‘fs’” 错误

Edge Runtime 不支持 Node.js 内置模块。使用 Web API 替代或确保该代码仅在客户端运行。

构建体积过大

  • 检查依赖是否有 Node.js 专用代码
  • 使用动态导入拆分代码
  • 移除未使用的依赖

冷启动慢

  • 减少 Worker 脚本体积
  • 使用 Smart Placement 就近部署
  • 预热关键路径

快速部署

方式一:Wrangler CLI (推荐)

bash
# 安装 Wrangler CLI
npm install -g wrangler

# 登录 Cloudflare
wrangler login

# 克隆项目
git clone https://github.com/halolight/halolight-cloudflare.git
cd halolight-cloudflare

# 安装依赖
pnpm install

# 部署
pnpm deploy

方式二:Cloudflare Dashboard

  1. 登录 Cloudflare Dashboard
  2. 进入 Workers & Pages
  3. 点击 “Create application” → “Pages”
  4. 选择 “Connect to Git”
  5. 授权并选择 halolight/halolight-cloudflare 仓库
  6. 配置构建设置:
    • Build command: pnpm build
    • Build output directory: .open-next
  7. 添加环境变量
  8. 点击 “Save and Deploy”

方式三:GitHub Actions

yaml
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm build
        env:
          NEXT_PUBLIC_API_URL: /api
          NEXT_PUBLIC_MOCK: false

      - name: Deploy
        run: pnpm deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}

配置文件

wrangler.jsonc

jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "halolight",
  "compatibility_date": "2024-12-01",
  "compatibility_flags": ["nodejs_compat"],
  "main": ".open-next/worker.js",
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  },
  // KV 存储
  "kv_namespaces": [
    {
      "binding": "CACHE_KV",
      "id": "your-kv-namespace-id"
    }
  ],
  // D1 数据库
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "halolight-db",
      "database_id": "your-d1-database-id"
    }
  ],
  // R2 对象存储
  "r2_buckets": [
    {
      "binding": "STORAGE",
      "bucket_name": "halolight-assets"
    }
  ],
  // Durable Objects
  "durable_objects": {
    "bindings": [
      {
        "name": "COUNTER",
        "class_name": "Counter"
      }
    ]
  },
  // AI 绑定
  "ai": {
    "binding": "AI"
  },
  // 环境变量
  "vars": {
    "ENVIRONMENT": "production"
  }
}

open-next.config.ts

typescript
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";

export default defineCloudflareConfig({
  // ISR 缓存配置
  incrementalCache: r2IncrementalCache,

  // 或使用 KV 缓存
  // incrementalCache: kvIncrementalCache,
});

环境变量

本地开发 (。dev.vars)

bash
# .dev.vars
NEXT_PUBLIC_API_URL=/api
NEXT_PUBLIC_MOCK=true
NEXT_PUBLIC_APP_TITLE=HaloLight
NEXT_PUBLIC_BRAND_NAME=HaloLight
NEXT_PUBLIC_DEMO_EMAIL=[email protected]
NEXT_PUBLIC_DEMO_PASSWORD=Admin@123
NEXT_PUBLIC_SHOW_DEMO_HINT=true

生产环境

在 Cloudflare Dashboard → Workers & Pages → 项目 → Settings → Variables 设置:

变量名 说明 示例
NEXT_PUBLIC_API_URL API 基础 URL /api
NEXT_PUBLIC_MOCK 启用 Mock 数据 false
NEXT_PUBLIC_APP_TITLE 应用标题 Admin Pro
NEXT_PUBLIC_BRAND_NAME 品牌名称 HaloLight
JWT_SECRET JWT 密钥 your-secret-key
DATABASE_URL D1 连接 (自动绑定) -

Wrangler CLI 设置

bash
# 设置普通变量
wrangler secret put JWT_SECRET

# 批量设置
wrangler deploy --var ENVIRONMENT:production

# 查看变量
wrangler secret list

Workers 服务详解

KV 存储

全球分布式键值存储,适合会话缓存、配置数据等场景。

typescript
// lib/kv.ts
import { getRequestContext } from "@opennextjs/cloudflare";

export async function getFromKV(key: string) {
  const { env } = getRequestContext();
  return await env.CACHE_KV.get(key, { type: "json" });
}

export async function setToKV(key: string, value: any, ttl?: number) {
  const { env } = getRequestContext();
  await env.CACHE_KV.put(key, JSON.stringify(value), {
    expirationTtl: ttl || 3600, // 默认 1 小时
  });
}

export async function deleteFromKV(key: string) {
  const { env } = getRequestContext();
  await env.CACHE_KV.delete(key);
}

// 使用示例:会话管理
export async function getSession(sessionId: string) {
  return await getFromKV(`session:${sessionId}`);
}

export async function setSession(sessionId: string, data: SessionData) {
  await setToKV(`session:${sessionId}`, data, 86400); // 24 小时
}

D1 数据库

边缘 SQLite 数据库,支持 SQL 查询。

typescript
// lib/db.ts
import { getRequestContext } from "@opennextjs/cloudflare";

export async function query<T>(sql: string, params?: any[]): Promise<T[]> {
  const { env } = getRequestContext();
  const stmt = env.DB.prepare(sql);

  if (params) {
    const result = await stmt.bind(...params).all();
    return result.results as T[];
  }

  const result = await stmt.all();
  return result.results as T[];
}

export async function execute(sql: string, params?: any[]) {
  const { env } = getRequestContext();
  const stmt = env.DB.prepare(sql);

  if (params) {
    return await stmt.bind(...params).run();
  }

  return await stmt.run();
}

// 使用示例
export async function getUsers(page = 1, limit = 10) {
  const offset = (page - 1) * limit;
  return await query<User>(
    "SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?",
    [limit, offset]
  );
}

export async function createUser(user: CreateUserInput) {
  return await execute(
    "INSERT INTO users (email, name, role) VALUES (?, ?, ?)",
    [user.email, user.name, user.role || "user"]
  );
}

D1 数据库迁移

bash
# 创建数据库
wrangler d1 create halolight-db

# 创建迁移
wrangler d1 migrations create halolight-db init

# 编辑迁移文件 migrations/0001_init.sql
sql
-- migrations/0001_init.sql
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  role TEXT DEFAULT 'user',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
bash
# 应用迁移(本地)
wrangler d1 migrations apply halolight-db --local

# 应用迁移(生产)
wrangler d1 migrations apply halolight-db --remote

R2 对象存储

S3 兼容的对象存储,零出口费。

typescript
// lib/r2.ts
import { getRequestContext } from "@opennextjs/cloudflare";

export async function uploadFile(
  key: string,
  file: ArrayBuffer | ReadableStream,
  contentType: string
) {
  const { env } = getRequestContext();

  await env.STORAGE.put(key, file, {
    httpMetadata: {
      contentType,
    },
  });

  return `https://your-bucket.r2.cloudflarestorage.com/${key}`;
}

export async function getFile(key: string) {
  const { env } = getRequestContext();
  return await env.STORAGE.get(key);
}

export async function deleteFile(key: string) {
  const { env } = getRequestContext();
  await env.STORAGE.delete(key);
}

export async function listFiles(prefix?: string, limit = 100) {
  const { env } = getRequestContext();
  const options: R2ListOptions = { limit };

  if (prefix) {
    options.prefix = prefix;
  }

  const list = await env.STORAGE.list(options);
  return list.objects;
}

// API 路由:文件上传
// app/api/upload/route.ts
export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get("file") as File;

  if (!file) {
    return Response.json({ error: "No file provided" }, { status: 400 });
  }

  const key = `uploads/${Date.now()}-${file.name}`;
  const buffer = await file.arrayBuffer();
  const url = await uploadFile(key, buffer, file.type);

  return Response.json({ url, key });
}

Workers AI

边缘 AI 推理,支持多种模型。

typescript
// lib/ai.ts
import { getRequestContext } from "@opennextjs/cloudflare";

// 文本生成
export async function generateText(prompt: string) {
  const { env } = getRequestContext();

  const response = await env.AI.run("@cf/meta/llama-2-7b-chat-int8", {
    prompt,
    max_tokens: 512,
  });

  return response.response;
}

// 文本嵌入
export async function getEmbedding(text: string) {
  const { env } = getRequestContext();

  const response = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
    text: [text],
  });

  return response.data[0];
}

// 图片生成
export async function generateImage(prompt: string) {
  const { env } = getRequestContext();

  const response = await env.AI.run("@cf/stabilityai/stable-diffusion-xl-base-1.0", {
    prompt,
  });

  return response; // ArrayBuffer
}

// 图片分类
export async function classifyImage(imageBuffer: ArrayBuffer) {
  const { env } = getRequestContext();

  const response = await env.AI.run("@cf/microsoft/resnet-50", {
    image: [...new Uint8Array(imageBuffer)],
  });

  return response;
}

// API 路由:AI 聊天
// app/api/chat/route.ts
export async function POST(request: Request) {
  const { message } = await request.json();

  const response = await generateText(`User: ${message}\nAssistant:`);

  return Response.json({ response });
}

Durable Objects

有状态边缘对象,适合实时协作、计数器等场景。

typescript
// lib/counter.ts
export class Counter {
  state: DurableObjectState;
  value: number = 0;

  constructor(state: DurableObjectState) {
    this.state = state;
    this.state.blockConcurrencyWhile(async () => {
      const stored = await this.state.storage.get<number>("value");
      this.value = stored || 0;
    });
  }

  async fetch(request: Request) {
    const url = new URL(request.url);

    switch (url.pathname) {
      case "/increment":
        this.value++;
        await this.state.storage.put("value", this.value);
        return Response.json({ value: this.value });

      case "/decrement":
        this.value--;
        await this.state.storage.put("value", this.value);
        return Response.json({ value: this.value });

      case "/value":
        return Response.json({ value: this.value });

      default:
        return new Response("Not found", { status: 404 });
    }
  }
}

// 使用 Durable Object
// app/api/counter/route.ts
import { getRequestContext } from "@opennextjs/cloudflare";

export async function GET() {
  const { env } = getRequestContext();
  const id = env.COUNTER.idFromName("global");
  const stub = env.COUNTER.get(id);

  const response = await stub.fetch("https://counter/value");
  return response;
}

export async function POST() {
  const { env } = getRequestContext();
  const id = env.COUNTER.idFromName("global");
  const stub = env.COUNTER.get(id);

  const response = await stub.fetch("https://counter/increment");
  return response;
}

Queues 消息队列

异步任务处理。

typescript
// 发送消息到队列
// app/api/tasks/route.ts
import { getRequestContext } from "@opennextjs/cloudflare";

export async function POST(request: Request) {
  const { env } = getRequestContext();
  const task = await request.json();

  await env.MY_QUEUE.send({
    type: "email",
    to: task.email,
    subject: task.subject,
    body: task.body,
  });

  return Response.json({ success: true, message: "Task queued" });
}

// 队列消费者 (在 wrangler.jsonc 中配置)
export default {
  async queue(batch: MessageBatch, env: Env) {
    for (const message of batch.messages) {
      const task = message.body as EmailTask;

      try {
        await sendEmail(task);
        message.ack();
      } catch (error) {
        message.retry();
      }
    }
  },
};

常用命令

bash
# 认证
wrangler login                    # 浏览器登录
wrangler logout                   # 登出
wrangler whoami                   # 查看当前用户

# 开发
pnpm dev                          # 启动开发服务器 (Node.js)
pnpm preview                      # 本地预览 (Workers 环境)
wrangler dev                      # Wrangler 开发模式

# 部署
pnpm deploy                       # 构建并部署
wrangler deploy                   # 仅部署
wrangler rollback                 # 回滚到上一版本

# KV 管理
wrangler kv namespace list        # 列出 KV 命名空间
wrangler kv namespace create <name>  # 创建 KV 命名空间
wrangler kv key list --namespace-id <id>  # 列出键
wrangler kv key get <key> --namespace-id <id>  # 获取值
wrangler kv key put <key> <value> --namespace-id <id>  # 设置值

# D1 数据库
wrangler d1 list                  # 列出数据库
wrangler d1 create <name>         # 创建数据库
wrangler d1 execute <db> --command "SELECT * FROM users"  # 执行 SQL
wrangler d1 migrations list <db>  # 列出迁移
wrangler d1 migrations apply <db> # 应用迁移

# R2 存储
wrangler r2 bucket list           # 列出存储桶
wrangler r2 bucket create <name>  # 创建存储桶
wrangler r2 object list <bucket>  # 列出对象
wrangler r2 object get <bucket> <key>  # 获取对象

# 日志
wrangler tail                     # 实时日志
wrangler tail --format pretty     # 格式化日志

# 密钥管理
wrangler secret list              # 列出密钥
wrangler secret put <name>        # 设置密钥
wrangler secret delete <name>     # 删除密钥

# 类型生成
pnpm cf-typegen                   # 生成 Cloudflare 环境类型

自定义域名

添加域名

bash
# 方式一:Cloudflare Dashboard
# Workers & Pages → 项目 → Custom domains → Add custom domain

# 方式二:API
curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/pages/projects/{project_name}/domains" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  --data '{"name":"halolight-cloudflare.h7ml.cn"}'

DNS 配置

如果域名已在 Cloudflare:

# 自动配置,无需手动设置

如果域名在其他服务商:

# CNAME 记录
类型: CNAME
名称: halolight-cloudflare
值: <project-name>.pages.dev

# 或使用自定义域名
类型: CNAME
名称: halolight-cloudflare
值: <custom-domain-target>.pages.dev

SSL/TLS

Cloudflare Pages 自动配置 HTTPS:

  • 自动申请 SSL 证书
  • 自动续期
  • 默认启用 TLS 1.3
  • 支持 HTTP/2 和 HTTP/3

边缘证书设置

bash
# 在 Cloudflare Dashboard → SSL/TLS → Edge Certificates

# 推荐配置:
# - SSL Mode: Full (strict)
# - Minimum TLS Version: TLS 1.2
# - TLS 1.3: Enabled
# - Automatic HTTPS Rewrites: Enabled

常见问题

Q:“Cannot find module ‘fs’” 错误?

A:Edge Runtime 不支持 Node.js 内置模块。解决方案:

  1. 使用 Web API 替代
  2. 确保代码仅在客户端运行
  3. 使用 nodejs_compat 兼容标志
jsonc
// wrangler.jsonc
{
  "compatibility_flags": ["nodejs_compat"]
}

Q:构建体积过大?

A:优化建议:

  1. 检查依赖是否有 Node.js 专用代码
  2. 使用动态导入拆分代码
  3. 移除未使用的依赖
  4. 使用 @cloudflare/next-on-pages 分析器
bash
npx @cloudflare/next-on-pages --info

Q:冷启动慢?

A:优化方案:

  1. 减少 Worker 脚本体积
  2. 使用 Smart Placement 就近部署
  3. 预热关键路径
  4. 考虑使用 Durable Objects 保持状态

Q:D1 数据库连接超时?

A:D1 是边缘数据库,注意:

  1. 单次查询限制 100ms CPU 时间
  2. 批量操作使用事务
  3. 避免复杂 JOIN 查询
typescript
// 使用批量操作
const batch = [
  db.prepare("INSERT INTO users VALUES (?, ?)").bind(1, "Alice"),
  db.prepare("INSERT INTO users VALUES (?, ?)").bind(2, "Bob"),
];
await db.batch(batch);

Q:KV 读取延迟高?

A:KV 特性:

  • 写入后约 60 秒全球同步
  • 适合读多写少场景
  • 高频写入使用 Durable Objects

Q:如何调试生产环境?

A:使用以下方法:

  1. wrangler tail 实时查看日志
  2. 添加 console.log 输出调试信息
  3. 使用 Cloudflare Dashboard → Workers → 日志
bash
# 实时日志
wrangler tail --format pretty

# 过滤错误
wrangler tail --status error

Q:ISR 不生效?

A:确保配置 R2 缓存:

typescript
// open-next.config.ts
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";

export default defineCloudflareConfig({
  incrementalCache: r2IncrementalCache,
});

并在 wrangler.jsonc 中绑定 R2 bucket。

费用说明

Workers 计费

计划 价格 包含额度
Free 免费 10 万请求/天
Paid $5/月起 1000 万请求/月

详细费用

资源 免费额度 超出价格
Workers 请求 10 万/天 $0.15/百万
CPU 时间 10ms/请求 $0.02/百万 ms
KV 读取 10 万/天 $0.50/百万
KV 写入 1 千/天 $5.00/百万
D1 读取 500 万/天 $0.001/百万
D1 写入 10 万/天 $1.00/百万
R2 存储 10GB $0.015/GB/月
R2 A 类操作 100 万/月 $4.50/百万
R2 B 类操作 1000 万/月 $0.36/百万
Workers AI 按模型计费 见官网

性价比配置

jsonc
// 小型项目(免费)
{
  "compatibility_flags": ["nodejs_compat"],
  // 使用 KV 缓存,不使用 R2
}

// 中型项目($5-20/月)
{
  "kv_namespaces": [...],
  "d1_databases": [...],
  // 添加 D1 数据库
}

// 大型项目($50+/月)
{
  "kv_namespaces": [...],
  "d1_databases": [...],
  "r2_buckets": [...],
  "durable_objects": {...},
  // 全功能配置
}

与其他平台对比

特性 Cloudflare Vercel Edge AWS Lambda@Edge
全球节点 300+ 无公开数据 13
冷启动 < 50ms < 100ms 100-500ms
免费请求 10 万/天 100 万/月 100 万/月
边缘数据库 ✅ D1
KV 存储 ✅ 内置 ✅ Vercel KV ❌ 需外部
对象存储 ✅ R2 (零出口费) ✅ Vercel Blob ✅ S3 (有出口费)
AI 推理 ✅ Workers AI ✅ SageMaker
实时协作 ✅ Durable Objects
消息队列 ✅ Queues ✅ SQS
脚本大小限制 10MB 4MB 50MB
执行时间限制 30s 25s 30s

部署架构

用户请求 → Cloudflare CDN (300+ 节点)

         Cloudflare Workers (Edge)

    ┌─────────┼─────────┬─────────┐
    ↓         ↓         ↓         ↓
   KV       D1        R2      外部 API
 (缓存)   (数据库)  (存储)    (后端)

相关链接

]]>
<![CDATA[架构组合指南]]> https://halolight.docs.h7ml.cn/guide/combinations https://halolight.docs.h7ml.cn/guide/combinations Fri, 19 Dec 2025 04:56:41 GMT B{团队规模?} B -->|小团队 <5人| C{是否需要SEO?} B -->|中型团队 5-20人| D{技术栈偏好?} B -->|大型团队 >20人| E[Angular + Spring Boot] C -->|需要SEO| F[Nuxt + FastAPI] C -->|不需要| G[Vue + FastAPI] D -->|Node.js| H[Next.js + NestJS] D -->|Python| I[Vue + FastAPI] D -->|Java| J[Angular + Spring Boot] D -->|Go| K[SvelteKit + Go Fiber] ``` ## 📊 组合评分矩阵 根据不同维度为主流组合打分 (满分 ⭐⭐⭐⭐⭐): ### Next.js + NestJS | 维度 | 评分 | 说明 | |]]> 架构组合指南

HaloLight 的核心优势在于前后端完全解耦,支持任意组合。本文档帮助你选择最适合的技术栈组合。

🎯 快速决策流程图

mermaid
graph TD
    A[开始选择] --> B{团队规模?}
    B -->|小团队 <5人| C{是否需要SEO?}
    B -->|中型团队 5-20人| D{技术栈偏好?}
    B -->|大型团队 >20人| E[Angular + Spring Boot]

    C -->|需要SEO| F[Nuxt + FastAPI]
    C -->|不需要| G[Vue + FastAPI]

    D -->|Node.js| H[Next.js + NestJS]
    D -->|Python| I[Vue + FastAPI]
    D -->|Java| J[Angular + Spring Boot]
    D -->|Go| K[SvelteKit + Go Fiber]

📊 组合评分矩阵

根据不同维度为主流组合打分 (满分 ⭐⭐⭐⭐⭐):

Next.js + NestJS

维度 评分 说明
开发效率 ⭐⭐⭐⭐⭐ TypeScript 全栈统一,类型共享
性能 ⭐⭐⭐⭐ SSR + 边缘缓存优化
学习曲线 ⭐⭐⭐ 需要理解 React 和 NestJS 架构
生态成熟度 ⭐⭐⭐⭐⭐ npm 生态极其丰富
部署难度 ⭐⭐⭐⭐ Vercel + Railway/Fly.io 一键部署
总评 ⭐⭐⭐⭐ 多租户 SaaS、企业后台首选

Vue + FastAPI

维度 评分 说明
开发效率 ⭐⭐⭐⭐⭐ Vue 学习曲线平滑,FastAPI 开发快
性能 ⭐⭐⭐⭐ Vue 3 编译优化,Python 异步高效
学习曲线 ⭐⭐⭐⭐⭐ 两者都相对容易上手
数据处理 ⭐⭐⭐⭐⭐ Python 数据科学生态无敌
部署难度 ⭐⭐⭐⭐ 前端 CDN,后端容器化
总评 ⭐⭐⭐⭐⭐ 数据/AI 驱动应用首选

Angular + Spring Boot

维度 评分 说明
开发效率 ⭐⭐⭐ 架构规范严谨,初期投入大
性能 ⭐⭐⭐⭐ 企业级优化成熟
学习曲线 ⭐⭐ 两者都有一定复杂度
企业成熟度 ⭐⭐⭐⭐⭐ 大型企业首选技术栈
长期维护性 ⭐⭐⭐⭐⭐ 架构清晰、可维护性强
总评 ⭐⭐⭐⭐ 大型企业、长周期项目首选

SvelteKit + Go Fiber

维度 评分 说明
开发效率 ⭐⭐⭐⭐ 代码简洁、开发体验好
性能 ⭐⭐⭐⭐⭐ 两者都是性能标杆
学习曲线 ⭐⭐⭐ Svelte 独特语法,Go 需要学习
资源占用 ⭐⭐⭐⭐⭐ 内存和 CPU 占用都极低
部署难度 ⭐⭐⭐⭐⭐ 容器镜像极小,适合边缘部署
总评 ⭐⭐⭐⭐⭐ 高性能实时应用首选

💡 组合选择建议

按团队规模

小团队 (< 5 人)

  • Vue + FastAPI - 快速上手、开发效率高
  • Preact + Bun - 轻量级、性能好
  • Astro + Node.js - 内容为主的场景

中型团队 (5-20 人)

  • Next.js + NestJS - TypeScript 统一栈
  • Vue + Spring Boot - 平衡易用性和企业特性
  • SvelteKit + FastAPI - 性能与效率兼顾

大型团队 (> 20 人)

  • Angular + Spring Boot - 架构规范、可维护性强
  • Next.js + NestJS + tRPC BFF - 微前端 + BFF 架构
  • 任意前端 + GraphQL Gateway + 微服务集群

按技术栈偏好

TypeScript 全栈

  • Next.js / Nuxt / Remix + NestJS + tRPC BFF
  • Solid.js / Qwik + Bun + Hono

Python 生态

  • Vue / React / Astro + FastAPI
  • SvelteKit + FastAPI

Java 生态

  • Angular / Vue + Spring Boot

Go 生态

  • SvelteKit / Solid / Qwik + Go Fiber

按部署环境

Serverless / 边缘优先

  • Next.js + NestJS(Vercel + Vercel Functions)
  • Nuxt + Bun(Cloudflare Workers)
  • Astro + Deno Deploy

传统服务器

  • 任意前端 (Nginx 静态托管)+ 任意后端 (PM2/Systemd)

容器化 (Kubernetes)

  • 任意组合 (Docker 镜像 + K8s Deployment)

混合云

  • 前端 (CDN)+ 后端 (私有云)+ tRPC BFF (边缘节点)

🔀 迁移与切换

前端框架迁移

如果你想从 Vue 迁移到 Angular:

  1. 接口不变:后端 API 契约保持不变
  2. 数据兼容:Mock 数据结构相同
  3. UI 一致:shadcn/ui 设计语言一致
  4. 权限同步:RBAC 权限配置通用

迁移成本:主要是组件语法转换 (Vue → Angular),业务逻辑可直接复用。

后端 API 切换

如果你想从 NestJS 切换到 FastAPI:

  1. OpenAPI 规范:接口定义保持一致
  2. 数据模型:数据库 schema 相同 (Prisma/SQLAlchemy)
  3. 认证机制:JWT 双令牌机制统一
  4. 权限系统:RBAC 通配符规则相同

迁移成本:重写服务层逻辑 (TS → Python),接口层面完全兼容。

🎨 组合矩阵

下表展示所有前端与后端的组合,每个单元格代表可选的技术搭配。

前端框架 (横向) × 后端 API (纵向)

前端 \ 后端 NestJS Node.js FastAPI Spring Boot Go PHP Bun tRPC BFF
Next.js ⭐ 最佳 ⭐ 最佳 ⭐ 最佳
Nuxt ⭐ 最佳
Vue ⭐ 最佳
Angular ⭐ 最佳 ⭐ 最佳
SvelteKit ⭐ 最佳
Astro ⭐ 最佳
Solid.js ⭐ 最佳 ⭐ 最佳
Qwik ⭐ 最佳 ⭐ 最佳
Remix ⭐ 最佳 ⭐ 最佳 ⭐ 最佳 ⭐ 最佳
Preact ⭐ 最佳 ⭐ 最佳
Lit ⭐ 最佳
Fresh ⭐ 最佳 ⭐ 最佳

图例

  • ⭐ 最佳:在特定场景下有显著优势
  • ✅ 可用:完全兼容,可直接使用

🔧 技术栈对比

前端框架特性

特性 Next.js Vue Angular SvelteKit Solid Qwik
学习曲线
TypeScript ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
SSR/SSG ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
性能 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
生态 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
包体积 极小 极小

后端技术特性

特性 NestJS FastAPI Spring Boot Go Fiber
开发效率 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
性能 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
TypeScript ⭐⭐⭐⭐⭐ - - -
企业成熟度 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
数据科学 ⭐⭐⭐⭐⭐ ⭐⭐
资源占用 极小

🚀 快速上手

选择组合后,按以下步骤启动示例:

第一步:启动前端

bash
# 以 Vue 为例
git clone https://github.com/halolight/halolight-vue.git
cd halolight-vue
pnpm install
pnpm dev

第二步:启动后端 API

bash
# 以 FastAPI 为例
git clone https://github.com/halolight/halolight-api-python.git
cd halolight-api-python
pip install -e ".[dev]"
uvicorn app.main:app --reload

第三步:前端对接后端

bash
# 前端项目 .env.local
VITE_API_URL=http://localhost:8000/api
VITE_USE_MOCK=false  # 关闭 Mock,使用真实 API

📚 相关文档

]]>
<![CDATA[Deno Fresh 后端 API]]> https://halolight.docs.h7ml.cn/guide/deno https://halolight.docs.h7ml.cn/guide/deno Fri, 19 Dec 2025 04:56:41 GMT Deno Fresh 后端 API

HaloLight Deno 后端 API 基于 Fresh 框架和 Deno KV 构建,采用 Deno 原生运行时,提供高性能的 RESTful API 服务。

API 文档https://halolight-deno.h7ml.cn/docs

GitHubhttps://github.com/halolight/halolight-deno

特性

  • 🔐 JWT 双令牌 - Access Token + Refresh Token,自动续期
  • 🛡️ RBAC 权限 - 基于角色的访问控制,通配符匹配
  • 📡 RESTful API - 标准化接口设计,OpenAPI 文档
  • 💾 Deno KV - 内置键值存储,无需外部数据库
  • Islands 架构 - 部分水合,极致性能
  • 数据验证 - 请求参数校验,错误处理
  • 📊 日志系统 - 请求日志,错误追踪
  • 🐳 Docker 支持 - 容器化部署

技术栈

技术 版本 说明
Deno 2.x 运行时 (内置 TypeScript)
Fresh 1.x Deno 原生 Web 框架
Preact 10.x 轻量级 UI 库
Deno KV - 内置键值存储 (数据库)
Hono 4.x API 路由框架 (可选)
JWT - 身份认证
Tailwind CSS 3.x 原子化 CSS

快速开始

环境要求

  • Deno >= 2.0.0

安装

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-deno.git
cd halolight-deno

# 无需安装依赖,Deno 自动管理

环境变量

bash
cp .env.example .env
env
# API 配置
API_URL=/api
USE_MOCK=true

# JWT 密钥
JWT_SECRET=your-super-secret-key
JWT_ACCESS_EXPIRES=15m
JWT_REFRESH_EXPIRES=7d

# Deno KV (可选,默认使用本地)
DENO_KV_PATH=./data/kv.db

# 服务配置
PORT=8000
NODE_ENV=development

# 演示账号
[email protected]
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=false

# 品牌配置
APP_TITLE=Admin Pro
BRAND_NAME=Halolight

数据库初始化

bash
# Deno KV 无需迁移,自动创建
# 如需种子数据
deno task seed

启动服务

bash
# 开发模式
deno task dev

# 生产模式
deno task build
deno task start

访问 http://localhost:8000

项目结构

halolight-deno/
├── routes/                  # 路由处理
│   ├── api/                 # API 端点
│   │   ├── auth/            # 认证路由
│   │   ├── users/           # 用户路由
│   │   ├── dashboard/       # 仪表盘路由
│   │   ├── documents/       # 文档路由
│   │   ├── files/           # 文件路由
│   │   ├── messages/        # 消息路由
│   │   ├── notifications/   # 通知路由
│   │   └── calendar/        # 日历路由
│   ├── (auth)/              # 认证页面
│   │   ├── login.tsx
│   │   ├── register.tsx
│   │   └── forgot-password.tsx
│   └── (dashboard)/         # 仪表盘页面
│       ├── index.tsx
│       ├── users.tsx
│       └── settings.tsx
├── utils/                   # 工具函数
│   ├── auth.ts              # 认证工具
│   ├── kv.ts                # Deno KV 封装
│   ├── permissions.ts       # 权限检查
│   ├── jwt.ts               # JWT 工具
│   └── validation.ts        # 数据验证
├── islands/                 # 交互式组件 (客户端)
│   ├── AuthProvider.tsx
│   ├── ThemeToggle.tsx
│   └── Dashboard.tsx
├── components/              # UI 组件
│   ├── ui/
│   ├── layout/
│   └── dashboard/
├── static/                  # 静态资源
├── fresh.gen.ts             # Fresh 生成文件
├── fresh.config.ts          # Fresh 配置
├── deno.json                # Deno 配置
└── import_map.json          # 导入映射

API 模块

认证相关端点

方法 路径 描述 权限
POST /api/auth/login 用户登录 公开
POST /api/auth/register 用户注册 公开
POST /api/auth/refresh 刷新令牌 公开
POST /api/auth/logout 退出登录 需认证
POST /api/auth/forgot-password 忘记密码 公开
POST /api/auth/reset-password 重置密码 公开

用户管理端点

方法 路径 描述 权限
GET /api/users 获取用户列表 users:view
GET /api/users/:id 获取用户详情 users:view
POST /api/users 创建用户 users:create
PUT /api/users/:id 更新用户 users:update
DELETE /api/users/:id 删除用户 users:delete
GET /api/users/me 获取当前用户 需认证

完整端点清单

文档管理 (Documents) - 5 个端点

方法 路径 描述
GET /api/documents 获取文档列表
GET /api/documents/:id 获取文档详情
POST /api/documents 创建文档
PUT /api/documents/:id 更新文档
DELETE /api/documents/:id 删除文档

文件管理 (Files) - 5 个端点

方法 路径 描述
GET /api/files 获取文件列表
GET /api/files/:id 获取文件详情
POST /api/files/upload 上传文件
PUT /api/files/:id 更新文件信息
DELETE /api/files/:id 删除文件

消息管理 (Messages) - 5 个端点

方法 路径 描述
GET /api/messages 获取消息列表
GET /api/messages/:id 获取消息详情
POST /api/messages 发送消息
PUT /api/messages/:id/read 标记已读
DELETE /api/messages/:id 删除消息

通知管理 (Notifications) - 4 个端点

方法 路径 描述
GET /api/notifications 获取通知列表
PUT /api/notifications/:id/read 标记已读
PUT /api/notifications/read-all 全部已读
DELETE /api/notifications/:id 删除通知

日历管理 (Calendar) - 5 个端点

方法 路径 描述
GET /api/calendar/events 获取日程列表
GET /api/calendar/events/:id 获取日程详情
POST /api/calendar/events 创建日程
PUT /api/calendar/events/:id 更新日程
DELETE /api/calendar/events/:id 删除日程

仪表盘 (Dashboard) - 6 个端点

方法 路径 描述
GET /api/dashboard/stats 统计数据
GET /api/dashboard/visits 访问趋势
GET /api/dashboard/sales 销售数据
GET /api/dashboard/pie 饼图数据
GET /api/dashboard/tasks 待办任务
GET /api/dashboard/calendar 今日日程

认证机制

JWT 双令牌

Access Token:  15 分钟有效期,用于 API 请求
Refresh Token: 7 天有效期,用于刷新 Access Token

请求头

http
Authorization: Bearer <access_token>

刷新流程

typescript
// utils/jwt.ts
import { create, verify } from "https://deno.land/x/[email protected]/mod.ts";

const key = await crypto.subtle.generateKey(
  { name: "HMAC", hash: "SHA-256" },
  true,
  ["sign", "verify"],
);

export async function createAccessToken(userId: string): Promise<string> {
  return await create(
    { alg: "HS256", typ: "JWT" },
    { userId, exp: Date.now() + 15 * 60 * 1000 }, // 15分钟
    key,
  );
}

export async function refreshToken(refreshToken: string): Promise<string> {
  const payload = await verify(refreshToken, key);
  return await createAccessToken(payload.userId as string);
}

权限系统

角色定义

角色 说明 权限
super_admin 超级管理员 * (所有权限)
admin 管理员 users:*, documents:*, files:*, messages:*, calendar:*
user 普通用户 documents:view, files:view, messages:view, calendar:view
guest 访客 dashboard:view

权限格式

{resource}:{action}

示例:
- users:view      # 查看用户
- users:create    # 创建用户
- users:*         # 用户所有操作
- *               # 所有权限

权限检查实现

typescript
// utils/permissions.ts
export function hasPermission(user: User, permission: string): boolean {
  const userPermissions = user.permissions || [];

  return userPermissions.some((p) => {
    if (p === "*") return true;
    if (p.endsWith(":*")) {
      const resource = p.slice(0, -2);
      return permission.startsWith(resource + ":");
    }
    return p === permission;
  });
}

// 路由中间件
export function requirePermission(permission: string) {
  return async (req: Request, ctx: any) => {
    const user = ctx.state.user;
    if (!hasPermission(user, permission)) {
      return new Response("Forbidden", { status: 403 });
    }
    return await ctx.next();
  };
}

错误处理

错误响应格式

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" }
    ]
  }
}

错误码

状态码 错误码 说明
400 VALIDATION_ERROR 参数验证失败
401 UNAUTHORIZED 未授权
403 FORBIDDEN 无权限
404 NOT_FOUND 资源不存在
409 CONFLICT 资源冲突
500 INTERNAL_ERROR 服务器错误

常用命令

bash
# 开发
deno task dev              # 启动开发服务器 (热重载)

# 构建
deno task build            # 构建生产版本

# 测试
deno test                  # 运行测试
deno test --coverage       # 测试覆盖率

# 数据库
deno task seed             # 初始化种子数据

# 代码质量
deno lint                  # 代码检查
deno fmt                   # 代码格式化
deno check **/*.ts         # 类型检查

部署

Docker

bash
docker build -t halolight-deno .
docker run -p 8000:8000 halolight-deno

Docker Compose

bash
docker-compose up -d
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - NODE_ENV=production
      - JWT_SECRET=${JWT_SECRET}
      - DENO_KV_PATH=/data/kv.db
    volumes:
      - deno_kv_data:/data
    restart: unless-stopped

volumes:
  deno_kv_data:

Deno Deploy (推荐)

bash
# 安装 deployctl
deno install -Arf jsr:@deno/deployctl

# 部署到 Deno Deploy
deployctl deploy --project=halolight-deno main.ts

生产环境配置

env
NODE_ENV=production
JWT_SECRET=your-production-secret-key-min-32-chars
DENO_KV_PATH=/data/kv.db
PORT=8000

测试

运行测试

bash
deno test                  # 运行所有测试
deno test --coverage       # 生成覆盖率报告
deno test --watch          # 监听模式

测试示例

typescript
// routes/api/auth/_test.ts
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";

Deno.test("POST /api/auth/login - success", async () => {
  const response = await fetch("http://localhost:8000/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      email: "[email protected]",
      password: "123456",
    }),
  });

  assertEquals(response.status, 200);
  const data = await response.json();
  assertEquals(data.success, true);
  assertEquals(typeof data.data.accessToken, "string");
});

Deno.test("POST /api/auth/login - invalid credentials", async () => {
  const response = await fetch("http://localhost:8000/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      email: "[email protected]",
      password: "wrong",
    }),
  });

  assertEquals(response.status, 401);
  const data = await response.json();
  assertEquals(data.success, false);
});

性能指标

基准测试

指标 数值 说明
请求吞吐量 ~45,000 req/s Deno 2.0, 单核, wrk 测试
平均响应时间 <5ms 本地 Deno KV, 无外部依赖
内存占用 ~30MB 启动后基础内存
CPU 使用率 <10% 空闲状态

可观测性

日志系统

typescript
// utils/logger.ts
import { Logger } from "https://deno.land/[email protected]/log/mod.ts";

const logger = new Logger("app");

export function logRequest(req: Request, res: Response, duration: number) {
  logger.info(
    `${req.method} ${new URL(req.url).pathname} ${res.status} ${duration}ms`,
  );
}

export function logError(error: Error, context?: any) {
  logger.error(`Error: ${error.message}`, { error, context });
}

健康检查

typescript
// routes/api/health.ts
export const handler = async (req: Request): Promise<Response> => {
  try {
    // 检查 Deno KV 连接
    const kv = await Deno.openKv();
    await kv.get(["health"]);
    await kv.close();

    return Response.json({
      status: "healthy",
      timestamp: new Date().toISOString(),
      uptime: Deno.memoryUsage(),
    });
  } catch (error) {
    return Response.json(
      { status: "unhealthy", error: error.message },
      { status: 503 },
    );
  }
};

监控指标

typescript
// utils/metrics.ts
const metrics = {
  requests: 0,
  errors: 0,
  responseTime: [] as number[],
};

export function recordRequest(duration: number) {
  metrics.requests++;
  metrics.responseTime.push(duration);
}

export function recordError() {
  metrics.errors++;
}

export function getMetrics() {
  const avg = metrics.responseTime.reduce((a, b) => a + b, 0) /
    metrics.responseTime.length;
  return {
    totalRequests: metrics.requests,
    totalErrors: metrics.errors,
    avgResponseTime: avg || 0,
  };
}

常见问题

Q:Deno KV 数据如何持久化?

A:通过配置 DENO_KV_PATH 环境变量指定数据文件路径。

bash
# .env
DENO_KV_PATH=./data/kv.db
typescript
// 使用自定义路径
const kv = await Deno.openKv(Deno.env.get("DENO_KV_PATH"));

Q:如何启用远程 Deno KV (Deno Deploy)?

A:在 Deno Deploy 上部署时,使用 Deno.openKv() 会自动连接到托管的分布式 KV。

typescript
// 生产环境自动使用远程 KV
const kv = await Deno.openKv();

Q:如何处理文件上传?

A:使用 Fresh 的 FormData API 处理文件上传。

typescript
// routes/api/files/upload.ts
export const handler = async (req: Request): Promise<Response> => {
  const formData = await req.formData();
  const file = formData.get("file") as File;

  if (!file) {
    return Response.json({ error: "No file uploaded" }, { status: 400 });
  }

  // 保存文件到 Deno KV 或云存储
  const bytes = await file.arrayBuffer();
  const kv = await Deno.openKv();
  await kv.set(["files", crypto.randomUUID()], {
    name: file.name,
    type: file.type,
    size: file.size,
    data: new Uint8Array(bytes),
  });

  return Response.json({ success: true });
};

Q:Islands 架构如何与 API 集成?

A:Islands 是客户端交互组件,通过 fetch 调用后端 API。

typescript
// islands/UserList.tsx
import { useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";

export default function UserList() {
  const users = useSignal([]);

  useEffect(() => {
    fetch("/api/users")
      .then((res) => res.json())
      .then((data) => users.value = data);
  }, []);

  return (
    <div>
      {users.value.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

开发工具

推荐插件/工具

  • Deno VSCode Extension - 官方 VS Code 插件,提供智能提示和调试
  • deployctl - Deno Deploy 命令行工具
  • wrk / autocannon - HTTP 压力测试工具
  • Deno Lint - 内置代码检查工具

与其他后端对比

特性 Deno Fresh NestJS FastAPI Spring Boot
语言 TypeScript (Deno) TypeScript Python Java
ORM Deno KV Prisma SQLAlchemy JPA
性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
学习曲线 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
启动时间 <100ms ~2s ~1s ~5s
内存占用 30MB 80MB 50MB 150MB
部署 Deno Deploy Docker/云 Docker/云 Docker/云

相关链接

]]>
<![CDATA[Docker 部署]]> https://halolight.docs.h7ml.cn/guide/docker https://halolight.docs.h7ml.cn/guide/docker Fri, 19 Dec 2025 04:56:41 GMT process.exit(r.statusCode === 200 ? 0 : 1))" # 启动命令 CMD ["node", "server.js"] ``` ### 开发环境 Dockerfile ```dockerfile # Dockerfile.dev FROM node:20-alpine WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate # 复制依赖文件 COPY package.json pnpm-lock.yaml ./ # 安装所有依赖(包括 devDependencies) RUN pnpm install # 复制源代码 COPY . . EXPOSE 3000 # 启动开发服务器 CMD ["pnpm", "dev"] ``` ## Docker Compose ### 完整生产环境配置 ```yaml # docker-compose.yml version: '3.8' services: # ============================================ # 应用服务 # ============================================ app: build: context: . dockerfile: Dockerfile args: NEXT_PUBLIC_API_URL: /api NEXT_PUBLIC_MOCK: "false" image: halolight/halolight:latest container_name: halolight-app restart: unless-stopped ports: - "3000:3000" environment: - NODE_ENV=production - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/halolight - REDIS_URL=redis://redis:6379 - JWT_SECRET=${JWT_SECRET} depends_on: db: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health')"] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - halolight-network deploy: resources: limits: cpus: '1' memory: 512M reservations: cpus: '0.5' memory: 256M # ============================================ # PostgreSQL 数据库 # ============================================ db: image: postgres:16-alpine container_name: halolight-db restart: unless-stopped environment: POSTGRES_DB: halolight POSTGRES_USER: postgres POSTGRES_PASSWORD: ${DB_PASSWORD} PGDATA: /var/lib/postgresql/data/pgdata volumes: - postgres_data:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d halolight"] interval: 10s timeout: 5s retries: 5 networks: - halolight-network deploy: resources: limits: cpus: '0.5' memory: 256M # ============================================ # Redis 缓存 # ============================================ redis: image: redis:7-alpine container_name: halolight-redis restart: unless-stopped command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 networks: - halolight-network deploy: resources: limits: cpus: '0.25' memory: 128M # ============================================ # Nginx 反向代理 # ============================================ nginx: image: nginx:alpine container_name: halolight-nginx restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/ssl:/etc/nginx/ssl:ro - nginx_logs:/var/log/nginx depends_on: - app networks: - halolight-network deploy: resources: limits: cpus: '0.25' memory: 64M # ============================================ # Adminer 数据库管理 (可选) # ============================================ adminer: image: adminer:latest container_name: halolight-adminer restart: unless-stopped ports: - "8080:8080" depends_on: - db networks: - halolight-network profiles: - tools networks: halolight-network: driver: bridge volumes: postgres_data: redis_data: nginx_logs: ``` ### 开发环境配置 ```yaml # docker-compose.dev.yml version: '3.8' services: app: build: context: . dockerfile: Dockerfile.dev container_name: halolight-dev ports: - "3000:3000" volumes: - .:/app - /app/node_modules - /app/.next environment: - NODE_ENV=development - NEXT_PUBLIC_MOCK=true command: pnpm dev db: image: postgres:16-alpine container_name: halolight-db-dev ports: - "5432:5432" environment: POSTGRES_DB: halolight POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres volumes: - postgres_dev_data:/var/lib/postgresql/data redis: image: redis:7-alpine container_name: halolight-redis-dev ports: - "6379:6379" volumes: postgres_dev_data: ``` ## Nginx 配置 ### nginx.conf ```nginx # nginx/nginx.conf user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; use epoll; multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # 日志格式 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' 'rt=$request_time uct="$upstream_connect_time" ' 'uht="$upstream_header_time" urt="$upstream_response_time"'; access_log /var/log/nginx/access.log main; # 性能优化 sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # Gzip 压缩 gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/xml application/xml+rss text/javascript application/x-javascript; # 上游服务器 upstream app_servers { least_conn; server app:3000 weight=1 max_fails=3 fail_timeout=30s; keepalive 32; } # 限流配置 limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; limit_conn_zone $binary_remote_addr zone=conn_limit:10m; include /etc/nginx/conf.d/*.conf; } ``` ### 站点配置 ```nginx # nginx/conf.d/default.conf server { listen 80; server_name localhost; # 重定向到 HTTPS (生产环境启用) # return 301 https://$server_name$request_uri; location / { proxy_pass http://app_servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; # 超时设置 proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # API 限流 location /api/ { limit_req zone=api_limit burst=20 nodelay; limit_conn conn_limit 10; proxy_pass http://app_servers; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源缓存 location /_next/static/ { proxy_pass http://app_servers; proxy_cache_valid 200 365d; add_header Cache-Control "public, max-age=31536000, immutable"; } # 健康检查 location /health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } } # HTTPS 配置 (生产环境) server { listen 443 ssl http2; server_name localhost; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; # 安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; location / { proxy_pass http://app_servers; # ... 其他配置同上 } } ``` ## Kubernetes 部署 ### Deployment ```yaml # k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: halolight namespace: halolight labels: app: halolight spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: halolight template: metadata: labels: app: halolight spec: containers: - name: halolight image: halolight/halolight:latest imagePullPolicy: Always ports: - containerPort: 3000 protocol: TCP env: - name: NODE_ENV value: "production" - name: DATABASE_URL valueFrom: secretKeyRef: name: halolight-secrets key: database-url - name: JWT_SECRET valueFrom: secretKeyRef: name: halolight-secrets key: jwt-secret resources: requests: cpu: "100m" memory: "256Mi" limits: cpu: "500m" memory: "512Mi" livenessProbe: httpGet: path: /api/health port: 3000 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /api/health port: 3000 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - halolight topologyKey: kubernetes.io/hostname ``` ### Service ```yaml # k8s/service.yaml apiVersion: v1 kind: Service metadata: name: halolight namespace: halolight spec: type: ClusterIP ports: - port: 80 targetPort: 3000 protocol: TCP selector: app: halolight ``` ### Ingress ```yaml # k8s/ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: halolight namespace: halolight annotations: kubernetes.io/ingress.class: nginx cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-body-size: "10m" spec: tls: - hosts: - halolight.example.com secretName: halolight-tls rules: - host: halolight.example.com http: paths: - path: / pathType: Prefix backend: service: name: halolight port: number: 80 ``` ### HPA 自动扩缩容 ```yaml # k8s/hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: halolight namespace: halolight spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: halolight minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 ``` ### ConfigMap 和 Secret ```yaml # k8s/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: halolight-config namespace: halolight data: NEXT_PUBLIC_API_URL: "/api" NEXT_PUBLIC_MOCK: "false" NEXT_PUBLIC_APP_TITLE: "Admin Pro" ]]> Docker 部署

HaloLight Docker 容器化部署方案,支持多阶段构建、Docker Compose 编排和 Kubernetes 部署。

Docker Hubhttps://hub.docker.com/r/halolight/halolight

GitHubhttps://github.com/halolight/halolight-docker

特性

  • 🐳 Docker 容器化 - 标准化的容器部署,环境一致性
  • 🏗️ 多阶段构建 - 优化镜像大小,生产镜像 < 150MB
  • 📦 Docker Compose - 多服务编排,一键启动完整环境
  • 🔄 Nginx 反代 - 高性能反向代理,负载均衡
  • 健康检查 - 容器健康监控,自动重启
  • ☸️ K8s Ready - Kubernetes 部署支持,Helm Charts
  • 🔒 安全加固 - 非 root 用户,最小化镜像
  • 📊 日志集成 - 支持 ELK/Loki 日志收集

快速开始

方式一:Docker Run

bash
# 拉取镜像
docker pull halolight/halolight:latest

# 运行容器
docker run -d \
  --name halolight \
  -p 3000:3000 \
  -e NEXT_PUBLIC_API_URL=/api \
  -e NEXT_PUBLIC_MOCK=true \
  halolight/halolight:latest

# 查看日志
docker logs -f halolight

# 停止容器
docker stop halolight

方式二:Docker Compose (推荐)

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-docker.git
cd halolight-docker

# 复制环境变量
cp .env.example .env

# 启动所有服务
docker-compose up -d

# 查看服务状态
docker-compose ps

# 查看日志
docker-compose logs -f

# 停止服务
docker-compose down

方式三:从源码构建

bash
# 克隆主仓库
git clone https://github.com/halolight/halolight.git
cd halolight

# 构建镜像
docker build -t halolight:local .

# 运行
docker run -d -p 3000:3000 halolight:local

Dockerfile

生产环境 Dockerfile

dockerfile
# ============================================
# 阶段 1: 依赖安装
# ============================================
FROM node:20-alpine AS deps

RUN apk add --no-cache libc6-compat
WORKDIR /app

# 安装 pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate

# 复制依赖文件
COPY package.json pnpm-lock.yaml ./

# 安装依赖
RUN pnpm install --frozen-lockfile

# ============================================
# 阶段 2: 构建应用
# ============================================
FROM node:20-alpine AS builder

WORKDIR /app

RUN corepack enable && corepack prepare pnpm@latest --activate

# 复制依赖
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 设置构建时环境变量
ARG NEXT_PUBLIC_API_URL=/api
ARG NEXT_PUBLIC_MOCK=false

ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_MOCK=$NEXT_PUBLIC_MOCK
ENV NEXT_TELEMETRY_DISABLED=1

# 构建
RUN pnpm build

# ============================================
# 阶段 3: 生产运行
# ============================================
FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# 创建非 root 用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# 复制构建产物
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# 切换到非 root 用户
USER nextjs

# 暴露端口
EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"

# 启动命令
CMD ["node", "server.js"]

开发环境 Dockerfile

dockerfile
# Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

RUN corepack enable && corepack prepare pnpm@latest --activate

# 复制依赖文件
COPY package.json pnpm-lock.yaml ./

# 安装所有依赖(包括 devDependencies)
RUN pnpm install

# 复制源代码
COPY . .

EXPOSE 3000

# 启动开发服务器
CMD ["pnpm", "dev"]

Docker Compose

完整生产环境配置

yaml
# docker-compose.yml
version: '3.8'

services:
  # ============================================
  # 应用服务
  # ============================================
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        NEXT_PUBLIC_API_URL: /api
        NEXT_PUBLIC_MOCK: "false"
    image: halolight/halolight:latest
    container_name: halolight-app
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/halolight
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health')"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    networks:
      - halolight-network
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

  # ============================================
  # PostgreSQL 数据库
  # ============================================
  db:
    image: postgres:16-alpine
    container_name: halolight-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d halolight"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - halolight-network
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M

  # ============================================
  # Redis 缓存
  # ============================================
  redis:
    image: redis:7-alpine
    container_name: halolight-redis
    restart: unless-stopped
    command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - halolight-network
    deploy:
      resources:
        limits:
          cpus: '0.25'
          memory: 128M

  # ============================================
  # Nginx 反向代理
  # ============================================
  nginx:
    image: nginx:alpine
    container_name: halolight-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - nginx_logs:/var/log/nginx
    depends_on:
      - app
    networks:
      - halolight-network
    deploy:
      resources:
        limits:
          cpus: '0.25'
          memory: 64M

  # ============================================
  # Adminer 数据库管理 (可选)
  # ============================================
  adminer:
    image: adminer:latest
    container_name: halolight-adminer
    restart: unless-stopped
    ports:
      - "8080:8080"
    depends_on:
      - db
    networks:
      - halolight-network
    profiles:
      - tools

networks:
  halolight-network:
    driver: bridge

volumes:
  postgres_data:
  redis_data:
  nginx_logs:

开发环境配置

yaml
# docker-compose.dev.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: halolight-dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
      - /app/.next
    environment:
      - NODE_ENV=development
      - NEXT_PUBLIC_MOCK=true
    command: pnpm dev

  db:
    image: postgres:16-alpine
    container_name: halolight-db-dev
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: halolight
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_dev_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    container_name: halolight-redis-dev
    ports:
      - "6379:6379"

volumes:
  postgres_dev_data:

Nginx 配置

nginx.conf

nginx
# nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
    multi_accept on;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # 日志格式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'uht="$upstream_header_time" urt="$upstream_response_time"';

    access_log /var/log/nginx/access.log main;

    # 性能优化
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript
               application/xml application/xml+rss text/javascript application/x-javascript;

    # 上游服务器
    upstream app_servers {
        least_conn;
        server app:3000 weight=1 max_fails=3 fail_timeout=30s;
        keepalive 32;
    }

    # 限流配置
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    include /etc/nginx/conf.d/*.conf;
}

站点配置

nginx
# nginx/conf.d/default.conf
server {
    listen 80;
    server_name localhost;

    # 重定向到 HTTPS (生产环境启用)
    # return 301 https://$server_name$request_uri;

    location / {
        proxy_pass http://app_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;

        # 超时设置
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # API 限流
    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        limit_conn conn_limit 10;

        proxy_pass http://app_servers;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # 静态资源缓存
    location /_next/static/ {
        proxy_pass http://app_servers;
        proxy_cache_valid 200 365d;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # 健康检查
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

# HTTPS 配置 (生产环境)
server {
    listen 443 ssl http2;
    server_name localhost;

    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    # 安全头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
        proxy_pass http://app_servers;
        # ... 其他配置同上
    }
}

Kubernetes 部署

Deployment

yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: halolight
  namespace: halolight
  labels:
    app: halolight
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: halolight
  template:
    metadata:
      labels:
        app: halolight
    spec:
      containers:
        - name: halolight
          image: halolight/halolight:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 3000
              protocol: TCP
          env:
            - name: NODE_ENV
              value: "production"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: halolight-secrets
                  key: database-url
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: halolight-secrets
                  key: jwt-secret
          resources:
            requests:
              cpu: "100m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          livenessProbe:
            httpGet:
              path: /api/health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /api/health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - halolight
                topologyKey: kubernetes.io/hostname

Service

yaml
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: halolight
  namespace: halolight
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 3000
      protocol: TCP
  selector:
    app: halolight

Ingress

yaml
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: halolight
  namespace: halolight
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
  tls:
    - hosts:
        - halolight.example.com
      secretName: halolight-tls
  rules:
    - host: halolight.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: halolight
                port:
                  number: 80

HPA 自动扩缩容

yaml
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: halolight
  namespace: halolight
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: halolight
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

ConfigMap 和 Secret

yaml
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: halolight-config
  namespace: halolight
data:
  NEXT_PUBLIC_API_URL: "/api"
  NEXT_PUBLIC_MOCK: "false"
  NEXT_PUBLIC_APP_TITLE: "Admin Pro"

---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: halolight-secrets
  namespace: halolight
type: Opaque
stringData:
  database-url: "postgresql://user:password@postgres:5432/halolight"
  jwt-secret: "your-super-secret-key"
  redis-url: "redis://redis:6379"

常用命令

bash
# Docker 基础命令
docker build -t halolight .              # 构建镜像
docker run -d -p 3000:3000 halolight     # 运行容器
docker logs -f <container_id>            # 查看日志
docker exec -it <container_id> sh        # 进入容器
docker stop <container_id>               # 停止容器
docker rm <container_id>                 # 删除容器
docker rmi halolight                     # 删除镜像

# Docker Compose 命令
docker-compose up -d                     # 后台启动
docker-compose down                      # 停止并删除
docker-compose down -v                   # 停止并删除(含数据卷)
docker-compose ps                        # 查看状态
docker-compose logs -f app               # 查看指定服务日志
docker-compose exec app sh               # 进入服务容器
docker-compose pull                      # 拉取最新镜像
docker-compose up -d --build             # 重新构建并启动

# Kubernetes 命令
kubectl apply -f k8s/                    # 应用所有配置
kubectl get pods -n halolight            # 查看 Pod
kubectl logs -f <pod_name> -n halolight  # 查看日志
kubectl exec -it <pod_name> -n halolight -- sh  # 进入 Pod
kubectl rollout restart deployment/halolight -n halolight  # 重启
kubectl rollout status deployment/halolight -n halolight   # 查看状态
kubectl scale deployment/halolight --replicas=5 -n halolight  # 扩容

环境变量

变量名 说明 示例
NODE_ENV 运行环境 production
PORT 服务端口 3000
NEXT_PUBLIC_API_URL API 基础 URL /api
NEXT_PUBLIC_MOCK 启用 Mock 数据 false
DATABASE_URL PostgreSQL 连接 postgresql://...
REDIS_URL Redis 连接 redis://redis:6379
JWT_SECRET JWT 密钥 your-secret-key
DB_PASSWORD 数据库密码 your-db-password

镜像优化

镜像大小对比

构建方式 镜像大小
单阶段构建 ~1.5GB
多阶段构建 ~150MB
多阶段 + Alpine ~120MB
多阶段 + Distroless ~100MB

优化建议

  1. 使用多阶段构建 - 分离构建和运行环境
  2. 使用 Alpine 镜像 - 基础镜像更小
  3. 使用 standalone 输出 - Next.js 独立运行模式
  4. 清理缓存 - 构建后清理 npm/pnpm 缓存
  5. 合并 RUN 指令 - 减少镜像层数

监控与日志

Prometheus 指标

yaml
# prometheus/prometheus.yml
scrape_configs:
  - job_name: 'halolight'
    static_configs:
      - targets: ['app:3000']
    metrics_path: '/api/metrics'

Loki 日志收集

yaml
# docker-compose with Loki
services:
  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml

  promtail:
    image: grafana/promtail:latest
    volumes:
      - /var/log:/var/log
      - ./promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml

常见问题

Q:容器启动失败?

A:检查以下几点:

  1. 查看日志:docker logs <container_id>
  2. 检查端口是否被占用
  3. 确认环境变量配置正确
  4. 检查依赖服务 (数据库、Redis) 是否就绪

Q:如何进行滚动更新?

A: Docker Compose:

bash
docker-compose pull
docker-compose up -d --no-deps app

Kubernetes:

bash
kubectl set image deployment/halolight halolight=halolight/halolight:v2

Q:数据持久化?

A:使用 Docker volumes:

yaml
volumes:
  - postgres_data:/var/lib/postgresql/data
  - redis_data:/data

Q:如何备份数据?

A:PostgreSQL 备份:

bash
docker exec halolight-db pg_dump -U postgres halolight > backup.sql

恢复:

bash
docker exec -i halolight-db psql -U postgres halolight < backup.sql

与其他部署方式对比

特性 Docker Vercel Kubernetes
部署复杂度 中等
可移植性 ✅ 高 ❌ 平台锁定 ✅ 高
扩展性 手动/Swarm 自动 ✅ HPA
成本 自行承担 按用量 自行承担
适用场景 自托管/私有云 快速上线 大规模生产

相关链接

]]>
<![CDATA[Fly.io 部署]]> https://halolight.docs.h7ml.cn/guide/fly https://halolight.docs.h7ml.cn/guide/fly Fri, 19 Dec 2025 04:56:41 GMT Fly.io 部署

HaloLight Fly.io 部署版本,全球边缘部署方案,支持多区域分布式部署。

在线预览https://halolight-fly.h7ml.cn

GitHubhttps://github.com/halolight/halolight-fly

特性

  • ✈️ 全球边缘 - 部署到全球 30+ 区域
  • 📈 自动扩缩容 - 按需自动扩展实例
  • 💾 Volumes - 持久化存储卷支持
  • 🔒 私有网络 - 内置 WireGuard 私有网络
  • 📊 监控指标 - Prometheus/Grafana 集成
  • 🔄 蓝绿部署 - 零停机滚动部署
  • 🐘 托管数据库 - PostgreSQL/Redis 一键创建
  • 🖥️ Machines API - 细粒度实例控制

快速开始

方式一:CLI 部署 (推荐)

bash
# 安装 Fly CLI
# macOS
brew install flyctl

# Linux
curl -L https://fly.io/install.sh | sh

# Windows
powershell -Command "iwr https://fly.io/install.ps1 -useb | iex"

# 登录 Fly.io
fly auth login

# 克隆项目
git clone https://github.com/halolight/halolight-fly.git
cd halolight-fly

# 初始化应用 (会创建 fly.toml)
fly launch

# 部署
fly deploy

方式二:从 Dockerfile

bash
# 克隆项目
git clone https://github.com/halolight/halolight-fly.git
cd halolight-fly

# 登录
fly auth login

# 创建应用
fly apps create halolight

# 使用 Dockerfile 部署
fly deploy --dockerfile Dockerfile

方式三:GitHub Actions

yaml
# .github/workflows/fly.yml
name: Fly Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: superfly/flyctl-actions/setup-flyctl@master

      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

配置文件

fly.toml

toml
# 应用名称
app = "halolight"

# 主区域 (香港)
primary_region = "hkg"

# 构建配置
[build]
  dockerfile = "Dockerfile"

# 环境变量
[env]
  NODE_ENV = "production"
  PORT = "3000"
  NEXT_PUBLIC_API_URL = "/api"
  NEXT_PUBLIC_MOCK = "false"

# HTTP 服务配置
[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = true      # 空闲时自动停止
  auto_start_machines = true     # 请求时自动启动
  min_machines_running = 1       # 最少保持 1 个实例
  processes = ["app"]

# TCP 服务端口映射
[[services]]
  protocol = "tcp"
  internal_port = 3000

  [[services.ports]]
    port = 80
    handlers = ["http"]

  [[services.ports]]
    port = 443
    handlers = ["tls", "http"]

  # 健康检查
  [[services.http_checks]]
    interval = "10s"
    timeout = "2s"
    grace_period = "5s"
    method = "GET"
    path = "/api/health"
    protocol = "http"
    tls_skip_verify = false

  # TCP 检查
  [[services.tcp_checks]]
    interval = "15s"
    timeout = "2s"
    grace_period = "1s"

# 虚拟机配置
[[vm]]
  cpu_kind = "shared"
  cpus = 1
  memory_mb = 512

# 进程组 (可选多进程)
[processes]
  app = "pnpm start"
  # worker = "pnpm run worker"

# 挂载卷
[mounts]
  source = "halolight_data"
  destination = "/data"
  initial_size = "1gb"

# 部署策略
[deploy]
  strategy = "rolling"
  max_unavailable = 0.33

# 静态资源
[[statics]]
  guest_path = "/app/public"
  url_prefix = "/static/"

Dockerfile

dockerfile
# 构建阶段
FROM node:20-alpine AS builder

RUN npm install -g pnpm

WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

# 生产阶段
FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production

# 创建非 root 用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

环境变量

设置方式

bash
# CLI 设置单个变量
fly secrets set DATABASE_URL="postgresql://user:pass@host:5432/db"

# CLI 设置多个变量
fly secrets set \
  JWT_SECRET="your-secret" \
  REDIS_URL="redis://..."

# 从 .env 文件导入
fly secrets import < .env.production

# 查看已设置的变量
fly secrets list

# 删除变量
fly secrets unset DATABASE_URL

常用变量

变量名 说明 示例
NODE_ENV 运行环境 production
PORT 服务端口 3000
NEXT_PUBLIC_API_URL API 基础 URL /api
NEXT_PUBLIC_MOCK 启用 Mock 数据 false
DATABASE_URL PostgreSQL 连接 postgresql://...
REDIS_URL Redis 连接 redis://...
JWT_SECRET JWT 密钥 your-secret-key

内置变量

Fly.io 自动注入以下环境变量:

bash
FLY_APP_NAME        # 应用名称
FLY_REGION          # 当前区域代码 (如 hkg)
FLY_ALLOC_ID        # 实例分配 ID
FLY_PUBLIC_IP       # 公网 IP
FLY_PRIVATE_IP      # 私有网络 IP
PRIMARY_REGION      # 主区域

持久化存储

创建 Volume

bash
# 在指定区域创建 Volume
fly volumes create halolight_data \
  --region hkg \
  --size 10 \
  --count 1

# 查看 Volumes
fly volumes list

# 扩展大小
fly volumes extend vol_xxx --size 20

# 删除 Volume
fly volumes destroy vol_xxx

挂载配置

toml
# fly.toml
[mounts]
  source = "halolight_data"
  destination = "/data"

使用 SQLite

typescript
// lib/db.ts
import Database from 'better-sqlite3';

const db = new Database('/data/app.db');

// 初始化表结构
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT
  )
`);

托管数据库

PostgreSQL

bash
# 创建 PostgreSQL 集群
fly postgres create --name halolight-db

# 可选配置
fly postgres create \
  --name halolight-db \
  --region hkg \
  --vm-size shared-cpu-1x \
  --volume-size 10 \
  --initial-cluster-size 1

# 连接到应用
fly postgres attach halolight-db --app halolight

# 这会自动设置 DATABASE_URL 环境变量

# 连接到数据库 (交互式)
fly postgres connect -a halolight-db

# 使用 psql
fly proxy 5432 -a halolight-db
# 然后在另一个终端
psql "postgresql://postgres:xxx@localhost:5432/halolight"

Redis

bash
# 创建 Redis (Upstash)
fly redis create

# 或使用 Fly 托管 Redis
fly apps create halolight-redis
fly deploy --config redis.toml

# 获取连接信息
fly redis status halolight-redis

Redis 配置文件

toml
# redis.toml
app = "halolight-redis"
primary_region = "hkg"

[build]
  image = "flyio/redis:6.2.6"

[env]
  REDIS_PASSWORD = ""

[[mounts]]
  source = "redis_data"
  destination = "/data"

[[services]]
  internal_port = 6379
  protocol = "tcp"

  [[services.ports]]
    port = 6379

多区域部署

添加区域

bash
# 查看可用区域
fly platform regions

# 添加区域
fly regions add sin nrt syd

# 查看当前区域
fly regions list

# 移除区域
fly regions remove syd

区域代码参考

代码 位置 延迟 (亚洲)
hkg 香港 ~5ms
sin 新加坡 ~30ms
nrt 东京 ~50ms
syd 悉尼 ~100ms
lax 洛杉矶 ~150ms
iad 华盛顿 ~200ms
lhr 伦敦 ~200ms
fra 法兰克福 ~180ms

扩展实例

bash
# 设置实例数量
fly scale count 3

# 按区域设置实例数
fly scale count hkg=2 sin=1 nrt=1

# 查看当前实例
fly scale show

# 调整实例规格
fly scale vm shared-cpu-2x
fly scale memory 1024

# 或在 fly.toml 中配置
[[vm]]
  cpu_kind = "shared"
  cpus = 2
  memory_mb = 1024

私有网络

服务间通信

bash
# 查看私有 IP
fly ips private

# 应用间通信使用 .internal 域名
# 格式: <app-name>.internal

# 例如连接到数据库
DATABASE_URL=postgres://user:[email protected]:5432/db

WireGuard 隧道

bash
# 创建 WireGuard 配置
fly wireguard create

# 查看配置
fly wireguard list

# 导入到 WireGuard 客户端后可直接访问
# 内部服务: http://halolight.internal:3000

常用命令

bash
# 应用管理
fly apps list                 # 列出所有应用
fly apps create <name>        # 创建应用
fly apps destroy <name>       # 删除应用

# 部署
fly deploy                    # 部署
fly deploy --remote-only      # 仅远程构建
fly deploy --local-only       # 仅本地构建
fly deploy --strategy rolling # 滚动部署

# 状态与日志
fly status                    # 查看状态
fly logs                      # 查看日志
fly logs -a halolight         # 指定应用日志

# 实例管理
fly machines list             # 列出实例
fly machines start <id>       # 启动实例
fly machines stop <id>        # 停止实例
fly machines destroy <id>     # 销毁实例

# SSH 访问
fly ssh console               # SSH 到实例
fly ssh issue                 # 生成 SSH 证书

# 代理
fly proxy 5432 -a halolight-db  # 代理端口

# 监控
fly dashboard                 # 打开控制台
fly metrics                   # 查看指标

# 发布管理
fly releases                  # 查看发布历史
fly releases rollback         # 回滚到上一版本

监控与告警

Prometheus 指标

Fly.io 自动暴露 Prometheus 指标:

bash
# 访问指标端点
curl https://halolight.fly.dev/_metrics

# 配置 Prometheus 采集
scrape_configs:
  - job_name: 'fly'
    static_configs:
      - targets: ['halolight.fly.dev']
    metrics_path: '/_metrics'

Grafana 集成

bash
# 部署 Grafana
fly apps create halolight-grafana
fly deploy --config grafana.toml

# 配置数据源连接到 Prometheus

自定义健康检查

typescript
// app/api/health/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const checks = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    region: process.env.FLY_REGION,
    checks: {
      database: await checkDatabase(),
      redis: await checkRedis(),
    },
  };

  const allHealthy = Object.values(checks.checks).every(c => c === 'ok');

  return NextResponse.json(checks, {
    status: allHealthy ? 200 : 503,
  });
}

自定义域名

添加域名

bash
# 添加自定义域名
fly certs create halolight-fly.h7ml.cn

# 查看证书状态
fly certs show halolight-fly.h7ml.cn

# 列出所有证书
fly certs list

DNS 配置

# A 记录
类型: A
名称: halolight-fly
值: <fly-app-ipv4>

# AAAA 记录 (IPv6)
类型: AAAA
名称: halolight-fly
值: <fly-app-ipv6>

# 或使用 CNAME
类型: CNAME
名称: halolight-fly
值: halolight.fly.dev

获取 IP

bash
# 查看应用 IP
fly ips list

# 分配专用 IPv4 (付费)
fly ips allocate-v4

# 分配 IPv6 (免费)
fly ips allocate-v6

常见问题

Q:部署失败怎么办?

A:检查以下几点:

  1. 查看构建日志:fly logs --build
  2. 检查 Dockerfile 是否正确
  3. 确认 fly.toml 配置无误
  4. 检查内存是否足够

Q:如何回滚部署?

A:使用以下命令:

bash
# 查看发布历史
fly releases

# 回滚到上一版本
fly releases rollback

# 回滚到指定版本
fly releases rollback v5

Q:冷启动太慢?

A:优化建议:

  1. 保持 min_machines_running = 1
  2. 增加实例数量
  3. 使用 auto_start_machines = true
  4. 优化 Docker 镜像大小

Q:如何调试应用?

A:使用 SSH 访问:

bash
# SSH 到实例
fly ssh console

# 运行命令
fly ssh console -C "ls -la"

# 查看进程
fly ssh console -C "ps aux"

Q:数据库连接问题?

A:检查以下几点:

  1. 确认 DATABASE_URL 正确
  2. 使用 .internal 域名进行内部连接
  3. 检查 PostgreSQL 是否在同一私有网络

费用说明

资源 免费额度 超出价格
共享 CPU 3 个实例 $1.94/月/实例
内存 256MB/实例 $0.01/GB/小时
带宽 160GB/月 $0.02/GB
IPv4 - $2/月
IPv6 无限 免费
Volumes 3GB $0.15/GB/月
PostgreSQL - 从 $6.44/月

性价比配置推荐

toml
# 开发/测试环境
[[vm]]
  cpu_kind = "shared"
  cpus = 1
  memory_mb = 256

# 生产环境
[[vm]]
  cpu_kind = "shared"
  cpus = 2
  memory_mb = 1024

与其他平台对比

特性 Fly.io Railway Render
全球区域 30+ 2 4
私有网络 ✅ WireGuard
托管数据库 ✅ PostgreSQL
自动扩缩容 ✅ Pro
免费额度 3 实例 $5/月 750 小时
Docker 支持 ✅ 原生
边缘计算

相关链接

]]>
<![CDATA[Fresh (Deno) 版本]]> https://halolight.docs.h7ml.cn/guide/fresh https://halolight.docs.h7ml.cn/guide/fresh Fri, 19 Dec 2025 04:56:41 GMT Fresh (Deno) 版本

HaloLight Fresh 版本基于 Fresh 2 + Deno 构建,采用 Islands 架构 + Preact,实现零配置、极速启动的管理后台。

在线预览https://halolight-fresh.h7ml.cn

GitHubhttps://github.com/halolight/halolight-fresh

特性

  • 🏗️ Islands 架构 - 默认零 JS,按需水合,极致性能
  • 零配置启动 - 开箱即用,无需构建步骤
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 🔒 安全默认 - Deno 沙盒安全模型
  • 🌐 边缘优先 - 原生支持 Deno Deploy 边缘部署

技术栈

技术 版本 说明
Fresh 2.x Deno 全栈框架
Deno 2.x 现代 JavaScript 运行时
Preact 10.x 轻量 UI 库
@preact/signals 2.x 响应式状态管理
TypeScript 内置 类型安全
Tailwind CSS 内置 原子化 CSS
Zod 3.x 数据验证
Chart.js 4.x 图表可视化

核心特性

  • Islands 架构 - 默认零 JS,仅交互组件水合,极致性能
  • 零配置开发 - JIT 渲染,无构建步骤,即时启动
  • 权限系统 - RBAC 权限控制,路由守卫,权限组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 边缘部署 - 原生支持 Deno Deploy 边缘运行时
  • 类型安全 - 内置 TypeScript,无需配置
  • 安全模型 - Deno 沙盒,显式权限,默认安全

目录结构

halolight-fresh/
├── routes/                        # 文件路由
│   ├── _app.tsx                  # 根布局
│   ├── _layout.tsx               # 默认布局
│   ├── _middleware.ts            # 全局中间件
│   ├── index.tsx                 # 首页
│   ├── auth/                     # 认证页面
│   │   ├── login.tsx
│   │   ├── register.tsx
│   │   ├── forgot-password.tsx
│   │   └── reset-password.tsx
│   ├── dashboard/                # 仪表盘页面
│   │   ├── _layout.tsx           # 仪表盘布局
│   │   ├── _middleware.ts        # 认证中间件
│   │   ├── index.tsx
│   │   ├── users/
│   │   │   ├── index.tsx
│   │   │   ├── create.tsx
│   │   │   └── [id].tsx
│   │   ├── roles.tsx
│   │   ├── permissions.tsx
│   │   ├── settings.tsx
│   │   └── profile.tsx
│   └── api/                      # API 路由
│       └── auth/
│           ├── login.ts
│           ├── register.ts
│           └── me.ts
├── islands/                      # 交互式 Islands
│   ├── LoginForm.tsx
│   ├── UserTable.tsx
│   ├── DashboardGrid.tsx
│   ├── ThemeToggle.tsx
│   └── Sidebar.tsx
├── components/                   # 静态组件
│   ├── ui/                       # UI 组件
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   ├── Card.tsx
│   │   └── ...
│   ├── layout/                   # 布局组件
│   │   ├── AdminLayout.tsx
│   │   ├── AuthLayout.tsx
│   │   └── Header.tsx
│   └── shared/                   # 共享组件
│       └── PermissionGuard.tsx
├── lib/                          # 工具库
│   ├── auth.ts
│   ├── permission.ts
│   ├── session.ts
│   └── cn.ts
├── signals/                      # 状态管理
│   ├── auth.ts
│   ├── ui-settings.ts
│   └── dashboard.ts
├── static/                       # 静态资源
├── fresh.config.ts              # Fresh 配置
├── deno.json                    # Deno 配置
└── tailwind.config.ts           # Tailwind 配置

快速开始

环境要求

  • Deno >= 2.x

安装 Deno

bash
# macOS/Linux
curl -fsSL https://deno.land/install.sh | sh

# Windows
irm https://deno.land/install.ps1 | iex

安装

bash
git clone https://github.com/halolight/halolight-fresh.git
cd halolight-fresh

环境变量

bash
cp .env.example .env
env
# .env
API_URL=/api
USE_MOCK=true
[email protected]
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=true
APP_TITLE=Admin Pro
BRAND_NAME=Halolight
SESSION_SECRET=your-secret-key

启动开发

bash
deno task dev

访问 http://localhost:8000

构建生产

bash
deno task build
deno task start

核心功能

状态管理 (@preact/signals)

ts
// signals/auth.ts
import { signal, computed, effect } from '@preact/signals'
import { IS_BROWSER } from '$fresh/runtime.ts'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

export const user = signal<User | null>(null)
export const token = signal<string | null>(null)
export const loading = signal(false)

export const isAuthenticated = computed(() => !!token.value && !!user.value)
export const permissions = computed(() => user.value?.permissions ?? [])

// 仅在浏览器端持久化
if (IS_BROWSER) {
  const saved = localStorage.getItem('auth')
  if (saved) {
    const { user: savedUser, token: savedToken } = JSON.parse(saved)
    user.value = savedUser
    token.value = savedToken
  }

  effect(() => {
    if (user.value && token.value) {
      localStorage.setItem('auth', JSON.stringify({
        user: user.value,
        token: token.value,
      }))
    }
  })
}

export async function login(credentials: { email: string; password: string }) {
  loading.value = true
  try {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
      headers: { 'Content-Type': 'application/json' },
    })
    const data = await response.json()

    user.value = data.user
    token.value = data.token
  } finally {
    loading.value = false
  }
}

export function logout() {
  user.value = null
  token.value = null
  if (IS_BROWSER) {
    localStorage.removeItem('auth')
  }
}

export function hasPermission(permission: string): boolean {
  const perms = permissions.value
  return perms.some((p) =>
    p === '*' || p === permission ||
    (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
  )
}

数据获取 (Handlers)

ts
// routes/api/auth/login.ts
import { Handlers } from '$fresh/server.ts'
import { z } from 'zod'
import { setCookie } from '$std/http/cookie.ts'
import { createToken } from '../../../lib/auth.ts'

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6),
})

export const handler: Handlers = {
  async POST(req) {
    try {
      const body = await req.json()
      const { email, password } = loginSchema.parse(body)

      // 验证用户(示例)
      const user = await authenticateUser(email, password)
      if (!user) {
        return new Response(
          JSON.stringify({ error: '邮箱或密码错误' }),
          { status: 401, headers: { 'Content-Type': 'application/json' } }
        )
      }

      const token = await createToken({ userId: user.id })

      const response = new Response(
        JSON.stringify({ user, token }),
        { headers: { 'Content-Type': 'application/json' } }
      )

      setCookie(response.headers, {
        name: 'token',
        value: token,
        path: '/',
        httpOnly: true,
        sameSite: 'Lax',
        maxAge: 60 * 60 * 24 * 7,
      })

      return response
    } catch (e) {
      if (e instanceof z.ZodError) {
        return new Response(
          JSON.stringify({ error: '参数验证失败', details: e.errors }),
          { status: 400, headers: { 'Content-Type': 'application/json' } }
        )
      }
      return new Response(
        JSON.stringify({ error: '服务器错误' }),
        { status: 500, headers: { 'Content-Type': 'application/json' } }
      )
    }
  },
}

权限控制

tsx
// components/shared/PermissionGuard.tsx
import { ComponentChildren } from 'preact'

interface Props {
  permission: string
  userPermissions: string[]
  children: ComponentChildren
  fallback?: ComponentChildren
}

function checkPermission(
  userPermissions: string[],
  permission: string
): boolean {
  return userPermissions.some((p) =>
    p === '*' || p === permission ||
    (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
  )
}

export function PermissionGuard({
  permission,
  userPermissions,
  children,
  fallback,
}: Props) {
  if (!checkPermission(userPermissions, permission)) {
    return fallback ?? null
  }

  return <>{children}</>
}
tsx
// 使用(在服务端渲染)
<PermissionGuard
  permission="users:delete"
  userPermissions={ctx.state.user.permissions}
  fallback={<span class="text-muted-foreground">无权限</span>}
>
  <Button variant="destructive">删除</Button>
</PermissionGuard>

Islands 架构

tsx
// islands/LoginForm.tsx
import { useSignal } from '@preact/signals'
import { login, loading } from '../signals/auth.ts'
import { Button } from '../components/ui/Button.tsx'
import { Input } from '../components/ui/Input.tsx'

interface Props {
  redirectTo?: string
}

export default function LoginForm({ redirectTo = '/dashboard' }: Props) {
  const email = useSignal('')
  const password = useSignal('')
  const error = useSignal('')

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    error.value = ''

    try {
      await login({
        email: email.value,
        password: password.value,
      })
      globalThis.location.href = redirectTo
    } catch (e) {
      error.value = '邮箱或密码错误'
    }
  }

  return (
    <form onSubmit={handleSubmit} class="space-y-4">
      {error.value && (
        <div class="text-destructive text-sm">{error.value}</div>
      )}

      <Input
        type="email"
        label="邮箱"
        value={email.value}
        onInput={(e) => email.value = e.currentTarget.value}
        required
      />

      <Input
        type="password"
        label="密码"
        value={password.value}
        onInput={(e) => password.value = e.currentTarget.value}
        required
      />

      <Button type="submit" class="w-full" disabled={loading.value}>
        {loading.value ? '登录中...' : '登录'}
      </Button>
    </form>
  )
}
tsx
// routes/auth/login.tsx
import { Handlers, PageProps } from '$fresh/server.ts'
import { AuthLayout } from '../../components/layout/AuthLayout.tsx'
import LoginForm from '../../islands/LoginForm.tsx'

export const handler: Handlers = {
  GET(req, ctx) {
    const url = new URL(req.url)
    const redirect = url.searchParams.get('redirect') || '/dashboard'
    return ctx.render({ redirect })
  },
}

export default function LoginPage({ data }: PageProps<{ redirect: string }>) {
  return (
    <AuthLayout>
      <div class="max-w-md mx-auto">
        <h1 class="text-2xl font-bold text-center mb-8">登录</h1>
        <LoginForm redirectTo={data.redirect} />
      </div>
    </AuthLayout>
  )
}

主题系统

皮肤预设

支持 11 种预设皮肤,通过快捷设置面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿 --primary: 64.6% 0.178 142.49
Orange 橙色 --primary: 69.7% 0.186 37.37
Rose 玫红 --primary: 62.8% 0.241 12.48
Teal 青绿 --primary: 66.7% 0.151 193.65
Amber 琥珀 --primary: 77.5% 0.166 69.76
Cyan 青蓝 --primary: 75.1% 0.146 204.66
Pink 粉色 --primary: 65.7% 0.255 347.69
Indigo 靛青 --primary: 51.9% 0.235 272.75
Lime 柠檬绿 --primary: 78.1% 0.167 136.29

CSS 变量 (OKLch)

css
/* 主题变量定义 */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 96.1% 0.006 285.75;
  --secondary-foreground: 14.9% 0.017 285.75;
  --muted: 96.1% 0.006 285.75;
  --muted-foreground: 44.7% 0.025 285.75;
  --accent: 96.1% 0.006 285.75;
  --accent-foreground: 14.9% 0.017 285.75;
  --destructive: 62.8% 0.241 12.48;
  --destructive-foreground: 100% 0 0;
  --border: 89.8% 0.011 285.75;
  --input: 89.8% 0.011 285.75;
  --ring: 51.1% 0.262 276.97;
  --radius: 0.5rem;
}

页面路由

路径 页面 权限
/ 首页 公开
/auth/login 登录 公开
/auth/register 注册 公开
/auth/forgot-password 忘记密码 公开
/auth/reset-password 重置密码 公开
/dashboard 仪表盘 dashboard:view
/dashboard/users 用户列表 users:list
/dashboard/users/create 创建用户 users:create
/dashboard/users/[id] 用户详情 users:view
/dashboard/roles 角色管理 roles:list
/dashboard/permissions 权限管理 permissions:list
/dashboard/settings 系统设置 settings:view
/dashboard/profile 个人中心 登录即可

常用命令

bash
deno task dev            # 启动开发服务器
deno task build          # 生产构建
deno task start          # 启动生产服务器
deno task check          # 格式和类型检查
deno task fmt            # 格式化代码
deno task fmt:check      # 检查代码格式
deno task lint           # 代码检查
deno task test           # 运行测试
deno task test:watch     # 测试 watch 模式
deno task test:coverage  # 测试覆盖率
deno task ci             # 运行完整 CI 检查

部署

Deno Deploy (推荐)

bash
# 安装 deployctl
deno install -A --no-check -r -f https://deno.land/x/deploy/deployctl.ts

# 部署
deployctl deploy --project=halolight-fresh main.ts

Docker

dockerfile
FROM denoland/deno:2.0.0

WORKDIR /app
COPY . .

RUN deno cache main.ts

EXPOSE 8000
CMD ["run", "-A", "main.ts"]
bash
docker build -t halolight-fresh .
docker run -p 8000:8000 halolight-fresh

其他平台

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

测试

项目使用 Deno 内置测试框架,测试文件位于 tests/ 目录。

测试结构

tests/
├── setup.ts              # 测试环境设置
│   ├── localStorage mock
│   ├── sessionStorage mock
│   ├── matchMedia mock
│   └── 辅助函数(createMockUser, mockAuthenticatedState 等)
└── lib/
    ├── utils.test.ts     # 工具函数测试
    ├── config.test.ts    # 配置测试
    └── stores.test.ts    # 状态管理测试

运行测试

bash
# 运行所有测试
deno task test

# 监视模式
deno task test:watch

# 测试覆盖率
deno task test:coverage

# 覆盖率报告输出到 coverage/lcov.info

测试示例

ts
// tests/lib/config.test.ts
import { assertEquals, assertExists } from "$std/assert/mod.ts";
import "../setup.ts";

import { hasPermission } from "../../lib/config.ts";
import type { Permission } from "../../lib/types.ts";

Deno.test("hasPermission - 权限检查", async (t) => {
  const userPermissions: Permission[] = ["dashboard:view", "users:view"];

  await t.step("应该返回 true 当用户有权限时", () => {
    const result = hasPermission(userPermissions, "dashboard:view");
    assertEquals(result, true);
  });

  await t.step("应该支持通配符权限", () => {
    const adminPermissions: Permission[] = ["*"];
    const result = hasPermission(adminPermissions, "dashboard:view");
    assertEquals(result, true);
  });
});

配置

Fresh 配置

ts
// fresh.config.ts
import { defineConfig } from '$fresh/server.ts'
import tailwind from '$fresh/plugins/tailwind.ts'

export default defineConfig({
  plugins: [tailwind()],
})

Deno 配置

json
// deno.json
{
  "lock": false,
  "tasks": {
    "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
    "dev": "deno run -A --watch=static/,routes/ dev.ts",
    "build": "deno run -A dev.ts build",
    "start": "deno run -A main.ts",
    "update": "deno run -A -r https://fresh.deno.dev/update ."
  },
  "imports": {
    "$fresh/": "https://deno.land/x/[email protected]/",
    "$std/": "https://deno.land/[email protected]/",
    "preact": "https://esm.sh/[email protected]",
    "preact/": "https://esm.sh/[email protected]/",
    "@preact/signals": "https://esm.sh/@preact/[email protected]",
    "zod": "https://deno.land/x/[email protected]/mod.ts"
  },
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

CI/CD

项目使用 GitHub Actions 进行持续集成,配置文件位于 .github/workflows/ci.yml

工作流任务

任务 说明 触发条件
lint 格式检查、代码检查、类型检查 push/PR
test 运行测试并上传覆盖率 push/PR
build 生产构建验证 lint/test 通过后
security Deno 安全审计 push/PR
dependency-review 依赖安全审查 PR only

代码质量配置

json
// deno.json
{
  "lint": {
    "rules": {
      "tags": ["recommended"],
      "exclude": [
        "no-explicit-any",
        "explicit-function-return-type",
        "explicit-module-boundary-types",
        "jsx-button-has-type",
        "no-unused-vars"
      ]
    }
  },
  "fmt": {
    "lineWidth": 100,
    "indentWidth": 2,
    "singleQuote": false,
    "semiColons": true
  }
}

高级功能

中间件系统

ts
// routes/dashboard/_middleware.ts
import { FreshContext } from '$fresh/server.ts'
import { getCookies } from '$std/http/cookie.ts'
import { verifyToken, getUser } from '../../lib/auth.ts'

export async function handler(req: Request, ctx: FreshContext) {
  const cookies = getCookies(req.headers)
  const token = cookies.token

  if (!token) {
    const url = new URL(req.url)
    return new Response(null, {
      status: 302,
      headers: { Location: `/auth/login?redirect=${url.pathname}` },
    })
  }

  try {
    const payload = await verifyToken(token)
    const user = await getUser(payload.userId)
    ctx.state.user = user
    ctx.state.token = token
  } catch {
    return new Response(null, {
      status: 302,
      headers: { Location: '/auth/login' },
    })
  }

  return ctx.next()
}

嵌套布局

tsx
// routes/dashboard/_layout.tsx
import { PageProps } from '$fresh/server.ts'
import { AdminLayout } from '../../components/layout/AdminLayout.tsx'
import Sidebar from '../../islands/Sidebar.tsx'

export default function DashboardLayout({ Component, state }: PageProps) {
  return (
    <AdminLayout>
      <div class="flex min-h-screen">
        <Sidebar user={state.user} />
        <main class="flex-1 p-6">
          <Component />
        </main>
      </div>
    </AdminLayout>
  )
}

性能优化

Islands 架构优化

Fresh 默认零 JS,仅交互组件需要水合:

tsx
// 静态组件(components/)- 零 JS
export function Card({ title, content }) {
  return (
    <div class="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  )
}

// 交互式 Island(islands/)- 按需水合
export default function Counter() {
  const count = useSignal(0)
  return (
    <button onClick={() => count.value++}>
      Count: {count.value}
    </button>
  )
}

边缘部署优化

ts
// 利用 Deno Deploy 边缘运行时
export const handler: Handlers = {
  async GET(req) {
    // 在边缘节点执行,降低延迟
    const data = await fetchFromDatabase()
    return new Response(JSON.stringify(data))
  }
}

预加载

tsx
// 预加载关键资源
<link rel="preload" href="/api/auth/me" as="fetch" crossOrigin="anonymous" />

常见问题

Q:如何在 Islands 和服务端组件之间共享状态?

A:使用 @preact/signals,它在服务端和客户端都能工作:

ts
// signals/auth.ts
export const user = signal<User | null>(null)

// islands/UserProfile.tsx (客户端)
import { user } from '../signals/auth.ts'
export default function UserProfile() {
  return <div>{user.value?.name}</div>
}

// routes/dashboard/index.tsx (服务端)
import { user } from '../signals/auth.ts'
export default function Dashboard({ data }: PageProps) {
  return <div>Welcome {data.user.name}</div>
}

Q:如何处理环境变量?

A:Fresh 使用 Deno 的环境变量系统:

ts
// 读取环境变量
const apiUrl = Deno.env.get('API_URL') || '/api'

// .env 文件(开发环境)
// 使用 deno task dev 自动加载

Q:如何实现数据持久化?

A:使用 Deno KV (内置键值数据库):

ts
// lib/db.ts
const kv = await Deno.openKv()

export async function saveUser(user: User) {
  await kv.set(['users', user.id], user)
}

export async function getUser(id: number) {
  const result = await kv.get(['users', id])
  return result.value as User
}

与其他版本对比

特性 Fresh 版本 Astro 版本 Next.js 版本
运行时 Deno Node.js Node.js
状态管理 @preact/signals - Zustand
数据获取 Handlers Load 函数 TanStack Query
表单验证 Zod Zod React Hook Form + Zod
服务端 内置 @astrojs/node API Routes
组件库 自定义 - shadcn/ui
Islands 架构
零配置
边缘部署 Deno Deploy Cloudflare Vercel Edge
构建步骤 可选 必须 必须

相关链接

]]>
<![CDATA[快速开始]]> https://halolight.docs.h7ml.cn/guide/getting-started https://halolight.docs.h7ml.cn/guide/getting-started Fri, 19 Dec 2025 04:56:41 GMT 快速开始

选择你熟悉的前端框架,并按需搭配后端 API,快速启动 HaloLight。

🎯 选择组合方案

HaloLight 采用前后端完全分离架构,支持 12 个前端 × 8 个后端 = 96 种组合

第一步:选择前端框架 (12 选 1)

框架 适用场景 特点
Next.js / Nuxt 多租户 SaaS、SEO 需求 SSR + 边缘渲染友好
Vue 中小团队快速交付 轻量高效、学习曲线平滑
Angular 大中型、长周期项目 强类型、架构清晰
SvelteKit / Solid / Qwik 高交互、实时场景 极致性能与响应式体验
Remix / Preact / Lit 渐进增强、轻量化 Web Components、小体积
Astro 内容为主的管理后台 Islands 架构、零 JS 默认

第二步:选择后端 API (8 选 1)

后端技术 适用场景 特点
NestJS / Express Node 生态团队 与前端 TS 契合度高
FastAPI 数据/AI 驱动应用 Python 生态、快速迭代
Spring Boot 企业级、金融行业 成熟中间件生态
Go Fiber 高性能、高并发 低资源占用
PHP Laravel 传统 Web 团队 生态完善、上手快
Bun + Hono 极致性能追求 新一代运行时
tRPC BFF 移动/桌面多端 类型共享、聚合与降噪

第三步:推荐组合 (按场景)

📊 多租户 SaaS / 企业管理后台

推荐组合:Next.js + NestJS

优势

  • SSR + TypeScript 端到端统一
  • 代码共享 (类型、工具函数)
  • 成熟的部署生态 (Vercel + Railway/Fly.io)
🤖 数据密集 / AI 驱动应用

推荐组合:Vue + FastAPI 或 React + FastAPI

优势

  • Python 数据科学生态 (Pandas、NumPy、scikit-learn)
  • 快速 API 开发 (自动文档、依赖注入)
  • 前端轻量、易于集成图表库
🏢 大型企业 / 长周期项目

推荐组合:Angular + Spring Boot

优势

  • 强类型、模块化架构清晰
  • 成熟的企业中间件 (认证、缓存、消息队列)
  • 长期支持与稳定性
⚡ 高性能实时应用

推荐组合:SvelteKit + Go Fiber

优势

  • 前端编译优化、极小体积
  • 后端高吞吐、低延迟
  • 资源占用少,成本优化
📱 移动/桌面多端统一

推荐组合:任意前端 + tRPC BFF + 任意后端

优势

  • BFF 聚合裁剪接口适配多端
  • TypeScript 端到端类型安全
  • 降低前端复杂度

环境要求

  • Node.js 18.17 或更高版本
  • pnpm 8+ (推荐) / npm / yarn

Next.js 版本

bash
# 克隆仓库
git clone https://github.com/halolight/halolight.git
cd halolight

# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

访问 http://localhost:3000 查看效果。

详细文档:Next.js 版本指南

Vue 版本

bash
# 克隆仓库
git clone https://github.com/halolight/halolight-vue.git
cd halolight-vue

# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

访问 http://localhost:5173 查看效果。

详细文档:Vue 版本指南

默认账号

所有版本使用相同的 Mock 账号:

角色 邮箱 密码
管理员 [email protected] 123456

目录结构

项目根目录/
├── src/
│   ├── app/              # 页面路由
│   ├── components/       # 组件
│   │   ├── ui/          # shadcn/ui 组件
│   │   ├── layout/      # 布局组件
│   │   └── dashboard/   # 仪表盘组件
│   ├── hooks/           # 自定义 hooks
│   ├── stores/          # 状态管理
│   ├── services/        # API 服务
│   ├── lib/             # 工具库
│   ├── types/           # 类型定义
│   └── mocks/           # Mock 数据
├── public/              # 静态资源
└── package.json

🚀 快速示例

方案 1:Next.js + NestJS

bash
# 终端 1:启动前端
git clone https://github.com/halolight/halolight.git && cd halolight
pnpm install && pnpm dev
bash
# 终端 2:启动后端
git clone https://github.com/halolight/halolight-api-nestjs.git && cd halolight-api-nestjs
pnpm install && pnpm dev

方案 2:Vue + FastAPI

bash
# 终端 1:启动前端
git clone https://github.com/halolight/halolight-vue.git && cd halolight-vue
pnpm install && pnpm dev
bash
# 终端 2:启动后端
git clone https://github.com/halolight/halolight-api-python.git && cd halolight-api-python
pip install -e ".[dev]" && uvicorn app.main:app --reload

下一步

]]>
<![CDATA[简介]]> https://halolight.docs.h7ml.cn/guide/ https://halolight.docs.h7ml.cn/guide/ Fri, 19 Dec 2025 04:56:41 GMT 简介

HaloLight 是一套多框架实现的企业级管理后台解决方案。

什么是 HaloLight

HaloLight 采用 “一套设计规范,多框架实现” 的理念,为开发者提供统一的 Admin Dashboard 体验。无论你使用 React、Vue、Angular 还是其他现代框架,都能获得一致的功能和设计。

核心特性

可拖拽仪表盘

基于 Grid Layout 的自定义 Dashboard 系统,支持:

  • Widget 拖拽排列
  • 响应式布局适配
  • 布局状态持久化

权限控制

完整的 RBAC 权限管理系统:

  • 细粒度权限控制 (页面级/按钮级)
  • 通配符权限匹配 (users:**)
  • 动态菜单渲染

主题系统

丰富的视觉定制能力:

  • 11 种皮肤预设
  • 明/暗模式切换
  • View Transitions 动画效果

组件库

基于 shadcn/ui 设计系统:

  • 30+ 精美 UI 组件
  • 完整的表单/表格解决方案
  • 高度可定制

前后端任意组合

  • 98 种组合方案:14 个前端框架 × 7 个后端 API,根据团队技术栈或业务场景自由选择
  • BFF/网关解耦:可选 tRPC、GraphQL Gateway 做聚合、鉴权与降噪
  • 升级不锁栈:更换任意前端或后端实现时,遵守接口契约即可无痛切换
  • 独立演进:前端可选 SSR/SSG/SPA,后端可选单体/微服务/Serverless

框架实现

全部框架版本均已实现并部署 (预览地址见各自仓库 README)。当前作为规范基准的参考实现:

框架 状态 预览 仓库
Next.js 14 ✅ 已部署 预览 GitHub
React (Vite) ✅ 已部署 预览 GitHub
Vue 3.5 ✅ 已部署 预览 GitHub
Angular 21 ✅ 已部署 预览 GitHub
Nuxt 4 ✅ 已部署 预览 GitHub
SvelteKit 2 ✅ 已部署 预览 GitHub
Astro 5 ✅ 已部署 预览 GitHub
Solid.js ✅ 已部署 预览 GitHub
Qwik ✅ 已部署 预览 GitHub
Remix ✅ 已部署 预览 GitHub
Preact ✅ 已部署 预览 GitHub
Lit ✅ 已部署 预览 GitHub
Fresh (Deno) ✅ 已部署 预览 GitHub
Deno ✅ 已部署 预览 GitHub

💡 灵活组合:前端主线支持 14 个框架,任意前端均可与 7 个后端 API 组合,形成 98+ 种搭配方案。

后端 API 实现

后端技术 状态 预览 仓库
NestJS 11 ✅ 已部署 API Docs GitHub
Python FastAPI ✅ 已部署 API Docs GitHub
Java Spring Boot ✅ 已部署 API Docs GitHub
Go Fiber ✅ 已部署 API Docs GitHub
Node.js Express ✅ 已部署 - GitHub
PHP Laravel ✅ 已部署 - GitHub
Bun + Hono ✅ 已部署 - GitHub

中间层 / 全栈

项目 状态 说明 仓库
tRPC BFF ✅ 已部署 类型安全网关 GitHub
Next.js Action ✅ 已部署 Server Actions 全栈方案 GitHub

技术栈

所有框架版本共享以下技术栈:

  • TypeScript - 类型安全
  • Tailwind CSS - 原子化 CSS
  • shadcn/ui - UI 组件库
  • TanStack Query - 服务端状态管理
  • ECharts - 图表可视化
  • Mock.js - 数据模拟

下一步

]]>
<![CDATA[Lit 版本]]> https://halolight.docs.h7ml.cn/guide/lit https://halolight.docs.h7ml.cn/guide/lit Fri, 19 Dec 2025 04:56:41 GMT Lit 版本

HaloLight Lit 版本基于 Lit 3 构建,采用 Web Components 标准 + TypeScript,提供跨框架可复用的 Web Components 组件库。

在线预览https://halolight-lit.h7ml.cn

GitHubhttps://github.com/halolight/halolight-lit

特性

  • 🎯 Web Components 标准 - 原生浏览器支持,无框架锁定
  • 跨框架复用 - 组件可在 React/Vue/Angular 中使用
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 🪶 轻量高效 - 核心库约 5KB gzip
  • 🌓 Shadow DOM - 样式隔离,避免冲突

技术栈

技术 版本 说明
Lit 3.x Web Components 框架
TypeScript 5.x 类型安全
Tailwind CSS 4.x 原子化 CSS
@lit-labs/router 0.1.x 客户端路由
@lit-labs/context 1.x 上下文状态
Shoelace 2.x Web Components UI 库
Zod 3.x 数据验证
ECharts 5.x 图表可视化
Vite 6.x 构建工具
Mock.js 1.x 数据模拟

核心特性

  • 可配置仪表盘 - 9 种小部件,拖拽布局,响应式适配
  • 权限系统 - RBAC 权限控制,路由守卫,权限组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 响应式属性 - @property 装饰器实现响应式
  • Shadow DOM 隔离 - 样式封装,避免全局冲突
  • 原生支持 - 基于 Web 标准,兼容所有现代浏览器

目录结构

halolight-lit/
├── src/
│   ├── pages/                     # 页面组件
│   │   ├── hl-home.ts            # 首页
│   │   ├── auth/                 # 认证页面
│   │   │   ├── hl-login.ts
│   │   │   ├── hl-register.ts
│   │   │   ├── hl-forgot-password.ts
│   │   │   └── hl-reset-password.ts
│   │   └── dashboard/            # 仪表盘页面
│   │       ├── hl-dashboard.ts
│   │       ├── hl-users.ts
│   │       ├── hl-user-detail.ts
│   │       ├── hl-user-create.ts
│   │       ├── hl-roles.ts
│   │       ├── hl-permissions.ts
│   │       ├── hl-settings.ts
│   │       └── hl-profile.ts
│   ├── components/               # 组件库
│   │   ├── ui/                   # UI 组件
│   │   │   ├── hl-button.ts
│   │   │   ├── hl-input.ts
│   │   │   ├── hl-card.ts
│   │   │   └── hl-dialog.ts
│   │   ├── layout/               # 布局组件
│   │   │   ├── hl-admin-layout.ts
│   │   │   ├── hl-auth-layout.ts
│   │   │   ├── hl-sidebar.ts
│   │   │   └── hl-header.ts
│   │   ├── dashboard/            # 仪表盘组件
│   │   │   ├── hl-dashboard-grid.ts
│   │   │   ├── hl-widget-wrapper.ts
│   │   │   └── hl-stats-widget.ts
│   │   └── shared/               # 共享组件
│   │       └── hl-permission-guard.ts
│   ├── stores/                   # 状态管理
│   │   ├── auth-context.ts
│   │   ├── ui-settings-context.ts
│   │   └── dashboard-context.ts
│   ├── lib/                      # 工具库
│   │   ├── api.ts
│   │   ├── permission.ts
│   │   └── styles.ts
│   ├── mock/                     # Mock 数据
│   ├── types/                    # 类型定义
│   ├── hl-app.ts                 # 根组件
│   ├── router.ts                 # 路由配置
│   └── main.ts                   # 入口文件
├── public/                       # 静态资源
├── vite.config.ts               # Vite 配置
├── tailwind.config.ts           # Tailwind 配置
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-lit.git
cd halolight-lit
pnpm install

环境变量

bash
cp .env.example .env
env
# .env
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:5173

构建生产

bash
pnpm build
pnpm preview

核心功能

状态管理 (@lit-labs/context)

ts
// stores/auth-context.ts
import { createContext } from '@lit-labs/context'
import { html, LitElement } from 'lit'
import { customElement, state } from 'lit/decorators.js'
import { provide } from '@lit-labs/context'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

interface AuthState {
  user: User | null
  token: string | null
  loading: boolean
  login: (credentials: { email: string; password: string }) => Promise<void>
  logout: () => void
  hasPermission: (permission: string) => boolean
}

export const authContext = createContext<AuthState>('auth')

@customElement('hl-auth-provider')
export class AuthProvider extends LitElement {
  @state() private user: User | null = null
  @state() private token: string | null = null
  @state() private loading = false

  @provide({ context: authContext })
  authState: AuthState = {
    user: null,
    token: null,
    loading: false,
    login: this.login.bind(this),
    logout: this.logout.bind(this),
    hasPermission: this.hasPermission.bind(this),
  }

  connectedCallback() {
    super.connectedCallback()
    this.loadFromStorage()
  }

  private loadFromStorage() {
    const saved = localStorage.getItem('auth')
    if (saved) {
      const { user, token } = JSON.parse(saved)
      this.user = user
      this.token = token
      this.updateContext()
    }
  }

  private updateContext() {
    this.authState = {
      ...this.authState,
      user: this.user,
      token: this.token,
      loading: this.loading,
    }
  }

  async login(credentials: { email: string; password: string }) {
    this.loading = true
    this.updateContext()

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: { 'Content-Type': 'application/json' },
      })
      const data = await response.json()

      this.user = data.user
      this.token = data.token
      localStorage.setItem('auth', JSON.stringify({
        user: this.user,
        token: this.token,
      }))
    } finally {
      this.loading = false
      this.updateContext()
    }
  }

  logout() {
    this.user = null
    this.token = null
    localStorage.removeItem('auth')
    this.updateContext()
  }

  hasPermission(permission: string): boolean {
    const perms = this.user?.permissions ?? []
    return perms.some(p =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  }

  render() {
    return html`<slot></slot>`
  }
}

基础组件

ts
// components/ui/hl-button.ts
import { LitElement, html, css } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { classMap } from 'lit/directives/class-map.js'

@customElement('hl-button')
export class HlButton extends LitElement {
  static styles = css`
    :host {
      display: inline-block;
    }

    button {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border-radius: 0.375rem;
      font-weight: 500;
      cursor: pointer;
      transition: all 0.2s;
    }

    .default {
      background-color: var(--primary);
      color: var(--primary-foreground);
    }

    .default:hover {
      opacity: 0.9;
    }

    .destructive {
      background-color: var(--destructive);
      color: var(--destructive-foreground);
    }

    .outline {
      border: 1px solid var(--border);
      background: transparent;
    }

    .sm { height: 2rem; padding: 0 0.75rem; font-size: 0.875rem; }
    .md { height: 2.5rem; padding: 0 1rem; }
    .lg { height: 3rem; padding: 0 1.5rem; font-size: 1.125rem; }

    .disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
  `

  @property() variant: 'default' | 'destructive' | 'outline' | 'ghost' = 'default'
  @property() size: 'sm' | 'md' | 'lg' = 'md'
  @property({ type: Boolean }) disabled = false

  render() {
    const classes = {
      [this.variant]: true,
      [this.size]: true,
      disabled: this.disabled,
    }

    return html`
      <button class=${classMap(classes)} ?disabled=${this.disabled}>
        <slot></slot>
      </button>
    `
  }
}

路由配置

ts
// router.ts
import { Router } from '@lit-labs/router'
import { html } from 'lit'

// 延迟加载页面组件
const routes = [
  {
    path: '/',
    render: () => html`<hl-home></hl-home>`,
    enter: async () => {
      await import('./pages/hl-home.js')
      return true
    },
  },
  {
    path: '/login',
    render: () => html`<hl-login></hl-login>`,
    enter: async () => {
      await import('./pages/auth/hl-login.js')
      return true
    },
  },
  {
    path: '/dashboard',
    render: () => html`<hl-dashboard></hl-dashboard>`,
    enter: async ({ router }) => {
      // 路由守卫
      const authState = document.querySelector('hl-auth-provider')?.authState
      if (!authState?.token) {
        router.goto('/login?redirect=/dashboard')
        return false
      }
      await import('./pages/dashboard/hl-dashboard.js')
      return true
    },
  },
  {
    path: '/users',
    render: () => html`<hl-users></hl-users>`,
    enter: async ({ router }) => {
      const authState = document.querySelector('hl-auth-provider')?.authState
      if (!authState?.hasPermission('users:list')) {
        return false
      }
      await import('./pages/dashboard/hl-users.js')
      return true
    },
  },
  // 更多路由...
]

export function createRouter(host: HTMLElement) {
  return new Router(host, routes)
}

权限控制

ts
// components/shared/hl-permission-guard.ts
import { LitElement, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { consume } from '@lit-labs/context'
import { authContext, type AuthState } from '../../stores/auth-context'

@customElement('hl-permission-guard')
export class HlPermissionGuard extends LitElement {
  @property() permission = ''

  @consume({ context: authContext, subscribe: true })
  authState!: AuthState

  render() {
    const hasPermission = this.authState?.hasPermission(this.permission)

    if (!hasPermission) {
      return html`<slot name="fallback"></slot>`
    }

    return html`<slot></slot>`
  }
}

使用示例:

html
<hl-permission-guard permission="users:delete">
  <hl-button variant="destructive">删除</hl-button>
  <span slot="fallback" class="text-muted-foreground">无权限</span>
</hl-permission-guard>

可拖拽仪表盘

ts
// components/dashboard/hl-dashboard-grid.ts
import { LitElement, html, css } from 'lit'
import { customElement, state } from 'lit/decorators.js'
import Sortable from 'sortablejs'

@customElement('hl-dashboard-grid')
export class HlDashboardGrid extends LitElement {
  static styles = css`
    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
      gap: 1rem;
    }

    .widget {
      background: var(--card);
      border-radius: 0.5rem;
      padding: 1rem;
      cursor: move;
    }
  `

  @state() private widgets = [
    { id: 'stats', type: 'stats' },
    { id: 'chart', type: 'chart' },
    { id: 'table', type: 'table' },
  ]

  firstUpdated() {
    const grid = this.shadowRoot?.querySelector('.grid')
    if (grid) {
      new Sortable(grid as HTMLElement, {
        animation: 150,
        onEnd: (evt) => {
          const { oldIndex, newIndex } = evt
          if (oldIndex !== undefined && newIndex !== undefined) {
            const item = this.widgets.splice(oldIndex, 1)[0]
            this.widgets.splice(newIndex, 0, item)
            this.requestUpdate()
          }
        },
      })
    }
  }

  render() {
    return html`
      <div class="grid">
        ${this.widgets.map(widget => html`
          <div class="widget" data-id=${widget.id}>
            <hl-widget-wrapper type=${widget.type}></hl-widget-wrapper>
          </div>
        `)}
      </div>
    `
  }
}

主题系统

皮肤预设

支持 11 种预设皮肤,通过快捷设置面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿 --primary: 64.6% 0.178 142.49
Rose 玫红 --primary: 58.5% 0.217 12.53
Orange 橘色 --primary: 68.4% 0.197 41.73

CSS 变量 (OKLch)

css
/* 主题变量定义 */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 98% 0 0;
  --card: 100% 0 0;
  --card-foreground: 14.9% 0.017 285.75;
  --border: 93.3% 0.011 285.88;
  --radius: 0.5rem;
}

.dark {
  --background: 14.9% 0.017 285.75;
  --foreground: 98% 0 0;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 98% 0 0;
  --card: 14.9% 0.017 285.75;
  --card-foreground: 98% 0 0;
  --border: 25.1% 0.025 285.82;
}

页面路由

路径 页面 权限
/ 首页 公开
/login 登录 公开
/register 注册 公开
/forgot-password 忘记密码 公开
/reset-password 重置密码 公开
/dashboard 仪表盘 dashboard:view
/users 用户管理 users:view
/users/create 创建用户 users:create
/users/:id 用户详情 users:view
/roles 角色管理 roles:view
/permissions 权限管理 permissions:view
/settings 系统设置 settings:view
/profile 个人资料 settings:view

在其他框架中使用

React

tsx
import '@halolight/lit/hl-button'

function App() {
  return (
    <hl-button variant="default" onClick={() => console.log('clicked')}>
      点击
    </hl-button>
  )
}

Vue

vue
<template>
  <hl-button variant="default" @click="handleClick">
    点击
  </hl-button>
</template>

<script setup>
import '@halolight/lit/hl-button'

function handleClick() {
  console.log('clicked')
}
</script>

Angular

ts
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'
import '@halolight/lit/hl-button'

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
html
<hl-button variant="default" (click)="handleClick()">
  点击
</hl-button>

常用命令

bash
pnpm dev            # 启动开发服务器
pnpm build          # 生产构建
pnpm preview        # 预览生产构建
pnpm lint           # 代码检查
pnpm lint:fix       # 自动修复
pnpm type-check     # 类型检查
pnpm test           # 运行测试
pnpm test:coverage  # 测试覆盖率

部署

Vercel (推荐)

Deploy with Vercel

Docker

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

其他平台

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

测试

bash
pnpm test           # 运行测试(watch 模式)
pnpm test:run       # 单次运行
pnpm test:coverage  # 覆盖率报告
pnpm test:ui        # Vitest UI 界面

测试示例

ts
// __tests__/hl-button.test.ts
import { expect, fixture, html } from '@open-wc/testing'
import '../src/components/ui/hl-button'

describe('hl-button', () => {
  it('renders with default variant', async () => {
    const el = await fixture(html`<hl-button>Click me</hl-button>`)
    const button = el.shadowRoot?.querySelector('button')
    expect(button).to.exist
    expect(button?.textContent?.trim()).to.equal('Click me')
  })

  it('applies variant classes', async () => {
    const el = await fixture(html`<hl-button variant="destructive">Delete</hl-button>`)
    const button = el.shadowRoot?.querySelector('button')
    expect(button?.classList.contains('destructive')).to.be.true
  })

  it('handles disabled state', async () => {
    const el = await fixture(html`<hl-button disabled>Disabled</hl-button>`)
    const button = el.shadowRoot?.querySelector('button')
    expect(button?.disabled).to.be.true
  })
})

配置

Vite 配置

ts
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: 'src/main.ts',
      formats: ['es'],
    },
    rollupOptions: {
      external: /^lit/,
    },
  },
  server: {
    port: 5173,
  },
})

Tailwind 配置

ts
// tailwind.config.ts
import type { Config } from 'tailwindcss'

export default {
  content: ['./index.html', './src/**/*.{ts,js}'],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        border: 'oklch(var(--border))',
        background: 'oklch(var(--background))',
        foreground: 'oklch(var(--foreground))',
        primary: {
          DEFAULT: 'oklch(var(--primary))',
          foreground: 'oklch(var(--primary-foreground))',
        },
      },
    },
  },
} satisfies Config

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

生命周期钩子

ts
// 组件生命周期
@customElement('my-component')
export class MyComponent extends LitElement {
  // 首次连接到 DOM
  connectedCallback() {
    super.connectedCallback()
    console.log('Component connected')
  }

  // 首次更新完成
  firstUpdated(changedProperties: PropertyValues) {
    super.firstUpdated(changedProperties)
    console.log('First render complete')
  }

  // 每次更新完成
  updated(changedProperties: PropertyValues) {
    super.updated(changedProperties)
    if (changedProperties.has('value')) {
      console.log('Value changed:', this.value)
    }
  }

  // 从 DOM 中移除
  disconnectedCallback() {
    super.disconnectedCallback()
    console.log('Component disconnected')
  }
}

自定义指令

ts
// lib/directives/tooltip.ts
import { directive, Directive } from 'lit/directive.js'
import { AsyncDirective } from 'lit/async-directive.js'

class TooltipDirective extends AsyncDirective {
  render(text: string) {
    return text
  }

  update(part: any, [text]: [string]) {
    const element = part.element
    element.setAttribute('title', text)
    element.style.cursor = 'help'
    return this.render(text)
  }
}

export const tooltip = directive(TooltipDirective)
ts
// 使用
import { tooltip } from './lib/directives/tooltip'

render() {
  return html`
    <span ${tooltip('这是提示信息')}>悬停查看提示</span>
  `
}

性能优化

虚拟滚动

ts
// components/ui/hl-virtual-list.ts
import { LitElement, html } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { repeat } from 'lit/directives/repeat.js'

@customElement('hl-virtual-list')
export class HlVirtualList extends LitElement {
  @property({ type: Array }) items: any[] = []
  @property({ type: Number }) itemHeight = 50
  @state() private visibleStart = 0
  @state() private visibleEnd = 20

  private handleScroll(e: Event) {
    const target = e.target as HTMLElement
    const scrollTop = target.scrollTop
    this.visibleStart = Math.floor(scrollTop / this.itemHeight)
    this.visibleEnd = this.visibleStart + 20
  }

  render() {
    const visibleItems = this.items.slice(this.visibleStart, this.visibleEnd)

    return html`
      <div class="container" @scroll=${this.handleScroll}>
        <div style="height: ${this.items.length * this.itemHeight}px">
          <div style="transform: translateY(${this.visibleStart * this.itemHeight}px)">
            ${repeat(
              visibleItems,
              item => item.id,
              item => html`<div class="item">${item.name}</div>`
            )}
          </div>
        </div>
      </div>
    `
  }
}

懒加载组件

ts
// 路由懒加载
{
  path: '/dashboard',
  enter: async () => {
    await import('./pages/dashboard/hl-dashboard.js')
    return true
  },
}

// 动态导入
async loadWidget(type: string) {
  const module = await import(`./widgets/hl-${type}-widget.js`)
  return module.default
}

预加载

ts
// 预加载关键路由
const preloadRoutes = ['/dashboard', '/users']

preloadRoutes.forEach(async (route) => {
  const link = document.createElement('link')
  link.rel = 'modulepreload'
  link.href = `./pages${route}.js`
  document.head.appendChild(link)
})

常见问题

Q:如何在 Shadow DOM 中使用全局样式?

A:使用 CSS 自定义属性或 @import 导入全局样式:

ts
static styles = css`
  @import url('/global.css');

  :host {
    color: var(--foreground);
    background: var(--background);
  }
`

Q:如何处理表单数据双向绑定?

A:使用 @input 事件和 @state 装饰器:

ts
@customElement('hl-form')
export class HlForm extends LitElement {
  @state() private formData = { name: '', email: '' }

  private handleInput(field: string, value: string) {
    this.formData = { ...this.formData, [field]: value }
  }

  render() {
    return html`
      <input
        .value=${this.formData.name}
        @input=${(e: Event) =>
          this.handleInput('name', (e.target as HTMLInputElement).value)}
      />
    `
  }
}

Q:如何在组件间通信?

A:使用自定义事件或 Context API:

ts
// 发送事件
this.dispatchEvent(new CustomEvent('data-changed', {
  detail: { data: this.data },
  bubbles: true,
  composed: true, // 穿透 Shadow DOM
}))

// 监听事件
@customElement('parent-component')
export class ParentComponent extends LitElement {
  render() {
    return html`
      <child-component @data-changed=${this.handleDataChanged}></child-component>
    `
  }

  private handleDataChanged(e: CustomEvent) {
    console.log('Data:', e.detail.data)
  }
}

与其他版本对比

特性 Lit 版本 Next.js 版本 Vue 版本
SSR/SSG ✅ (实验性) ✅ (Nuxt)
状态管理 @lit-labs/context Zustand Pinia
路由 @lit-labs/router App Router Vue Router
构建工具 Vite Next.js Vite
跨框架复用 ✅ 原生支持
Shadow DOM
包大小 5KB (gzip) ~90KB ~60KB

相关链接

]]>
<![CDATA[Netlify 部署]]> https://halolight.docs.h7ml.cn/guide/netlify https://halolight.docs.h7ml.cn/guide/netlify Fri, 19 Dec 2025 04:56:41 GMT Netlify 部署

HaloLight Netlify 部署版本,针对 Netlify 平台优化的一键部署方案。

在线预览https://halolight-netlify.h7ml.cn

GitHubhttps://github.com/halolight/halolight-netlify

特性

  • 🔷 一键部署 - Deploy to Netlify 按钮快速上线
  • 全球 CDN - 300+ 边缘节点极速分发
  • 🔄 自动 CI/CD - Git 推送自动构建部署
  • 📝 表单处理 - 无需后端的表单提交
  • 🔐 Identity - 内置用户认证服务
  • 🌐 Functions - Serverless 函数 (AWS Lambda)
  • 🔗 Split Testing - A/B 测试与分流
  • 📊 Analytics - 服务端分析 (付费)

快速开始

方式一:一键部署 (推荐)

Deploy to Netlify

点击按钮后:

  1. 登录 Netlify 账号 (支持 GitHub/GitLab/Bitbucket)
  2. 授权访问仓库
  3. 配置环境变量
  4. 自动克隆并部署

方式二:CLI 部署

bash
# 安装 Netlify CLI
npm install -g netlify-cli

# 登录 Netlify
netlify login

# 克隆项目
git clone https://github.com/halolight/halolight-netlify.git
cd halolight-netlify

# 安装依赖
pnpm install

# 初始化 Netlify 站点
netlify init

# 本地开发 (带 Functions)
netlify dev

# 部署到生产
netlify deploy --prod

方式三:GitHub 集成

  1. Fork halolight-netlify 仓库
  2. 在 Netlify 控制台点击 “Add new site” → “Import an existing project”
  3. 选择 GitHub 并授权
  4. 选择你的 Fork 仓库
  5. 配置构建设置并部署

配置文件

netlify.toml

toml
[build]
  command = "pnpm build"
  publish = ".next"

[build.environment]
  NODE_VERSION = "20"
  PNPM_VERSION = "9"

# Next.js 插件 (自动处理 SSR/ISR)
[[plugins]]
  package = "@netlify/plugin-nextjs"

# 生产环境
[context.production]
  command = "pnpm build"

[context.production.environment]
  NEXT_PUBLIC_MOCK = "false"

# 预览环境 (分支部署)
[context.deploy-preview]
  command = "pnpm build"

[context.deploy-preview.environment]
  NEXT_PUBLIC_MOCK = "true"

# 分支部署
[context.branch-deploy]
  command = "pnpm build"

# 重定向规则
[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/:splat"
  status = 200

# SPA 回退
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200
  conditions = {Role = ["admin"]}

# 自定义头
[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    X-XSS-Protection = "1; mode=block"
    Referrer-Policy = "strict-origin-when-cross-origin"

[[headers]]
  for = "/_next/static/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

package.json 脚本

json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "netlify:dev": "netlify dev",
    "netlify:build": "netlify build",
    "netlify:deploy": "netlify deploy --prod"
  }
}

环境变量

在 Netlify 控制台 → Site settings → Environment variables 设置:

变量名 说明 示例
NODE_ENV 运行环境 production
NEXT_PUBLIC_API_URL API 基础 URL /api
NEXT_PUBLIC_MOCK 启用 Mock 数据 false
NEXT_PUBLIC_APP_TITLE 应用标题 Admin Pro
DATABASE_URL 数据库连接 postgresql://...

环境变量作用域

Netlify 支持按部署上下文配置变量:

Production     - 生产环境变量
Deploy Preview - PR 预览环境变量
Branch Deploy  - 分支部署变量
All            - 所有环境共享

Serverless Functions

基础函数

typescript
// netlify/functions/hello.ts
import type { Handler, HandlerEvent, HandlerContext } from "@netlify/functions";

export const handler: Handler = async (
  event: HandlerEvent,
  context: HandlerContext
) => {
  const { httpMethod, body, queryStringParameters } = event;

  return {
    statusCode: 200,
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      message: "Hello from Netlify Functions!",
      method: httpMethod,
      query: queryStringParameters,
    }),
  };
};

后台函数 (长时间运行)

typescript
// netlify/functions/background-task.ts
import type { BackgroundHandler } from "@netlify/functions";

export const handler: BackgroundHandler = async (event) => {
  // 最长运行 15 分钟
  console.log("Processing background task...");

  // 执行耗时操作
  await processLongRunningTask(event.body);

  // 后台函数不返回响应
};

// 配置为后台函数
export const config = {
  type: "background",
};

定时函数 (Scheduled Functions)

typescript
// netlify/functions/daily-report.ts
import type { Handler } from "@netlify/functions";

export const handler: Handler = async () => {
  console.log("Generating daily report...");

  await generateReport();

  return {
    statusCode: 200,
    body: "Report generated",
  };
};

// 每天 UTC 9:00 执行
export const config = {
  schedule: "0 9 * * *",
};

Edge Functions

typescript
// netlify/edge-functions/geolocation.ts
import type { Context } from "@netlify/edge-functions";

export default async (request: Request, context: Context) => {
  const { country, city } = context.geo;

  // 基于地理位置的响应
  return new Response(
    JSON.stringify({
      country,
      city,
      message: `Hello from ${city}, ${country}!`,
    }),
    {
      headers: { "Content-Type": "application/json" },
    }
  );
};

export const config = {
  path: "/api/geo",
};

Netlify Identity

配置认证

typescript
// lib/netlify-identity.ts
import netlifyIdentity from "netlify-identity-widget";

// 初始化
netlifyIdentity.init({
  container: "#netlify-modal",
  locale: "zh",
});

// 登录
export function login() {
  netlifyIdentity.open("login");
}

// 注册
export function signup() {
  netlifyIdentity.open("signup");
}

// 登出
export function logout() {
  netlifyIdentity.logout();
}

// 获取当前用户
export function getCurrentUser() {
  return netlifyIdentity.currentUser();
}

// 监听认证状态
netlifyIdentity.on("login", (user) => {
  console.log("User logged in:", user);
  netlifyIdentity.close();
});

netlifyIdentity.on("logout", () => {
  console.log("User logged out");
});

保护函数

typescript
// netlify/functions/protected.ts
import type { Handler } from "@netlify/functions";

export const handler: Handler = async (event) => {
  const { user } = event.context.clientContext || {};

  if (!user) {
    return {
      statusCode: 401,
      body: JSON.stringify({ error: "Unauthorized" }),
    };
  }

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: `Hello ${user.email}!`,
      roles: user.app_metadata?.roles || [],
    }),
  };
};

表单处理

HTML 表单

html
<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field">
  <input type="hidden" name="form-name" value="contact" />
  <p class="hidden">
    <label>Don't fill this out: <input name="bot-field" /></label>
  </p>
  <p>
    <label>Email: <input type="email" name="email" required /></label>
  </p>
  <p>
    <label>Message: <textarea name="message" required></textarea></label>
  </p>
  <p>
    <button type="submit">Send</button>
  </p>
</form>

React 表单

tsx
// components/ContactForm.tsx
"use client";

import { useState } from "react";

export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setStatus("loading");

    const formData = new FormData(e.currentTarget);

    try {
      const response = await fetch("/", {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams(formData as any).toString(),
      });

      if (response.ok) {
        setStatus("success");
      } else {
        setStatus("error");
      }
    } catch {
      setStatus("error");
    }
  };

  return (
    <form
      name="contact"
      method="POST"
      data-netlify="true"
      onSubmit={handleSubmit}
    >
      <input type="hidden" name="form-name" value="contact" />
      {/* 表单字段 */}
      <button type="submit" disabled={status === "loading"}>
        {status === "loading" ? "Sending..." : "Send"}
      </button>
      {status === "success" && <p>Message sent!</p>}
      {status === "error" && <p>Error sending message</p>}
    </form>
  );
}

常用命令

bash
# 登录
netlify login

# 查看站点状态
netlify status

# 本地开发
netlify dev

# 构建
netlify build

# 部署预览
netlify deploy

# 部署到生产
netlify deploy --prod

# 打开站点
netlify open

# 打开控制台
netlify open:admin

# 查看日志
netlify logs

# 环境变量
netlify env:list
netlify env:set KEY value
netlify env:unset KEY

# 链接到站点
netlify link

# 取消链接
netlify unlink

监控与日志

查看日志

bash
# CLI 查看日志
netlify logs

# 实时日志流
netlify logs --live

# 查看函数日志
netlify logs:function hello

Build Plugins

toml
# netlify.toml

# 性能分析
[[plugins]]
  package = "netlify-plugin-lighthouse"

# 缓存优化
[[plugins]]
  package = "netlify-plugin-cache"
  [plugins.inputs]
    paths = [".next/cache", "node_modules/.cache"]

# 提交状态通知
[[plugins]]
  package = "netlify-plugin-checklinks"

自定义域名

添加域名

  1. 进入站点设置 → Domain management
  2. 点击 “Add custom domain”
  3. 输入你的域名

DNS 配置

# A 记录 (根域名)
类型: A
名称: @
值: 75.2.60.5

# CNAME 记录 (子域名)
类型: CNAME
名称: www
值: your-site.netlify.app

HTTPS

Netlify 自动配置 HTTPS:

  • 自动申请 Let's Encrypt 证书
  • 自动续期
  • 强制 HTTPS 重定向

分支部署与预览

Deploy Contexts

上下文 触发条件 URL 格式
Production main 分支 push your-site.netlify.app
Deploy Preview PR 创建/更新 deploy-preview-123--your-site.netlify.app
Branch Deploy 其他分支 push branch-name--your-site.netlify.app

锁定部署

bash
# 锁定当前部署 (停止自动部署)
netlify deploy:lock

# 解锁
netlify deploy:unlock

常见问题

Q:构建失败怎么办?

A:检查以下几点:

  1. 查看构建日志,确认依赖安装正确
  2. 确认 pnpm-lock.yaml 已提交
  3. 检查 Node.js 版本 (build.environment.NODE_VERSION)
  4. 确认构建命令正确

Q:如何回滚部署?

A:在 Deploys 页面:

  1. 找到之前的成功部署
  2. 点击 “Publish deploy”
  3. 或使用 CLI:netlify rollback

Q:Functions 冷启动慢?

A:优化建议:

  1. 减少函数包大小
  2. 使用 Edge Functions (无冷启动)
  3. 使用 Background Functions 处理耗时任务

Q:如何设置重定向?

A:在 netlify.toml_redirects 文件中配置:

toml
# netlify.toml
[[redirects]]
  from = "/old-path"
  to = "/new-path"
  status = 301

# 代理
[[redirects]]
  from = "/api/*"
  to = "https://api.example.com/:splat"
  status = 200

费用说明

计划 价格 特性
Starter 免费 100GB 带宽,300 分钟构建
Pro $19/成员/月 1TB 带宽,25000 分钟构建
Business $99/成员/月 自定义 SLA,SSO
Enterprise 联系销售 专属支持,合规认证

Functions 配额

计划 调用次数 运行时间
Starter 125K/月 100 小时
Pro 无限 1000 小时

与其他平台对比

特性 Netlify Vercel Cloudflare
一键部署
Edge Functions
表单处理 ✅ 内置 ❌ 需外部 ❌ 需外部
Identity ✅ 内置 ❌ 需外部 ✅ Access
免费带宽 100GB 100GB 无限
免费构建 300 分钟 6000 分钟 500 次
Split Testing ⚠️ 有限

相关链接

]]>
<![CDATA[Next.js 版本]]> https://halolight.docs.h7ml.cn/guide/nextjs https://halolight.docs.h7ml.cn/guide/nextjs Fri, 19 Dec 2025 04:56:41 GMT Next.js 版本

HaloLight Next.js 版本基于 Next.js 14 App Router 构建,采用 React 18 + TypeScript。

在线预览https://halolight.h7ml.cn/

GitHubhttps://github.com/halolight/halolight

特性

  • 🏗️ Next.js 14 App Router - 服务端组件与流式渲染
  • Zustand 状态管理 - 轻量级状态管理方案
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 📑 多标签页 - 标签栏管理
  • 命令面板 - 快捷键导航

技术栈

技术 版本 说明
Next.js 14.x React 全栈框架 (App Router)
React 18.x UI 库
TypeScript 5.x 类型安全
Tailwind CSS 4.x 原子化 CSS
shadcn/ui latest UI 组件库 (28 组件)
Zustand 5.x 状态管理 (6 Store)
TanStack Query 5.x 服务端状态
React Hook Form 7.x 表单处理
Zod 4.x 数据验证
react-grid-layout 1.x 拖拽布局
Recharts 3.x 图表可视化
Framer Motion 12.x 动画效果
Mock.js 1.x 数据模拟
next-pwa 5.x PWA 支持

核心特性

  • 可配置仪表盘 - 9 种小部件,拖拽布局,响应式适配
  • 多标签导航 - 浏览器式标签,右键菜单,状态缓存
  • 权限系统 - RBAC 权限控制,路由守卫,权限组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 多账户切换 - 快速切换账户,记住登录状态
  • 命令面板 - 键盘快捷键 (⌘K),全局搜索
  • 实时通知 - WebSocket 推送,通知中心

目录结构

halolight/
├── src/
│   ├── app/                      # App Router 页面
│   │   ├── (auth)/              # 认证路由组
│   │   │   ├── login/           # 登录
│   │   │   ├── register/        # 注册
│   │   │   ├── forgot-password/ # 忘记密码
│   │   │   ├── reset-password/  # 重置密码
│   │   │   └── layout.tsx       # 认证布局
│   │   ├── (dashboard)/         # 仪表盘路由组
│   │   │   ├── page.tsx         # 仪表盘首页(可配置)
│   │   │   ├── accounts/        # 账户与权限
│   │   │   ├── analytics/       # 数据分析
│   │   │   ├── calendar/        # 日程管理
│   │   │   ├── docs/            # 帮助文档
│   │   │   ├── documents/       # 文档管理
│   │   │   ├── files/           # 文件存储
│   │   │   ├── messages/        # 消息中心
│   │   │   ├── notifications/   # 通知中心
│   │   │   ├── profile/         # 个人资料
│   │   │   ├── users/           # 用户管理
│   │   │   ├── settings/        # 系统设置
│   │   │   │   └── teams/       # 团队管理
│   │   │   │       └── roles/   # 角色管理
│   │   │   └── layout.tsx       # 仪表盘布局
│   │   ├── (legal)/             # 法律条款路由组
│   │   │   ├── privacy/         # 隐私政策
│   │   │   ├── terms/           # 服务条款
│   │   │   └── layout.tsx
│   │   ├── layout.tsx           # 根布局
│   │   ├── error.tsx            # 错误页面
│   │   └── not-found.tsx        # 404 页面
│   ├── components/
│   │   ├── ui/                  # shadcn/ui 组件 (28 个)
│   │   ├── layout/              # 布局组件 (11 个)
│   │   │   ├── admin-layout.tsx # 管理后台布局
│   │   │   ├── sidebar.tsx      # 可折叠侧边栏
│   │   │   ├── header.tsx       # 页头(通知/错误/用户菜单)
│   │   │   ├── footer.tsx       # 页脚
│   │   │   ├── tab-bar.tsx      # 多标签导航
│   │   │   ├── command-menu.tsx # 命令面板 (⌘K)
│   │   │   ├── quick-settings.tsx # 界面设置面板
│   │   │   ├── theme-toggle.tsx # 主题切换
│   │   │   └── pending-overlay.tsx # 加载遮罩
│   │   ├── dashboard/           # 仪表盘组件
│   │   │   ├── configurable-dashboard.tsx # 可配置仪表盘
│   │   │   ├── charts.tsx       # 图表组件
│   │   │   ├── stats-card.tsx   # 统计卡片
│   │   │   └── recent-activity.tsx # 最近活动
│   │   └── shared/              # 共享组件
│   ├── hooks/                   # React Hooks (15 个)
│   │   ├── use-users.ts         # 用户 CRUD
│   │   ├── use-teams.ts         # 团队管理
│   │   ├── use-messages.ts      # 消息管理
│   │   ├── use-notifications.ts # 通知管理
│   │   ├── use-calendar.ts      # 日历数据
│   │   ├── use-documents.ts     # 文档管理
│   │   ├── use-files.ts         # 文件管理
│   │   ├── use-dashboard.ts     # 仪表盘状态
│   │   ├── use-dashboard-data.ts # 仪表盘数据 Hook 集合
│   │   ├── use-chart-palette.ts # 图表配色(主题感知)
│   │   ├── use-action-mutation.ts # Server Action 封装
│   │   ├── use-keep-alive.tsx   # 页面状态缓存
│   │   ├── use-tdk.ts           # TDK 管理
│   │   └── use-title.ts         # 页面标题
│   ├── stores/                  # Zustand Stores (6 个)
│   │   ├── auth-store.ts        # 认证状态(含多账户)
│   │   ├── ui-settings-store.ts # UI 设置
│   │   ├── dashboard-store.ts   # 仪表盘状态
│   │   ├── navigation-store.ts  # 导航状态
│   │   ├── tabs-store.ts        # 标签页状态
│   │   └── error-store.ts       # 错误收集
│   ├── providers/               # React Providers (8 个)
│   │   ├── app-providers.tsx    # Provider 聚合
│   │   ├── auth-provider.tsx    # 认证 Provider
│   │   ├── theme-provider.tsx   # 主题 Provider
│   │   ├── query-provider.tsx   # TanStack Query
│   │   ├── error-provider.tsx   # 错误处理
│   │   ├── permission-provider.tsx # 权限检查
│   │   ├── websocket-provider.tsx # WebSocket 实时通知
│   │   └── keep-alive-provider.tsx # 页面保活
│   ├── actions/                 # Server Actions
│   ├── config/                  # 配置文件
│   │   ├── routes.ts            # 路由与权限配置
│   │   └── tdk.ts               # TDK 配置
│   ├── lib/                     # 工具库
│   │   └── api/                 # API 客户端
│   ├── mock/                    # Mock 数据 (9 模块)
│   └── middleware.ts            # 中间件(认证+安全头)
├── public/
│   ├── manifest.json            # PWA 清单
│   ├── sw.js                    # Service Worker
│   ├── icons/                   # PWA 图标 (8 尺寸)
│   ├── screenshots/             # PWA 截图
│   └── fonts/                   # 自托管字体
├── next.config.mjs              # Next.js + PWA 配置
├── tailwind.config.js
├── tsconfig.json
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight.git
cd halolight
pnpm install

环境变量

bash
cp .env.example .env.local
env
# .env.local 示例
NEXT_PUBLIC_API_URL=/api
NEXT_PUBLIC_MOCK=true              # 开启 Mock 数据
[email protected]
NEXT_PUBLIC_DEMO_PASSWORD=123456
NEXT_PUBLIC_SHOW_DEMO_HINT=false
NEXT_PUBLIC_WS_URL=                # WebSocket 地址
NEXT_PUBLIC_APP_TITLE=Admin Pro
NEXT_PUBLIC_BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:3000

构建生产

bash
pnpm build
pnpm start

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

核心功能

1。状态管理 (Zustand)

tsx
// stores/auth-store.ts
interface AuthState {
  user: AccountWithToken | null
  accounts: AccountWithToken[]        // 多账户列表
  activeAccountId: string | null      // 当前账户

  login: (data: LoginRequest) => Promise<void>
  register: (data: RegisterRequest) => Promise<void>
  logout: () => Promise<void>
  switchAccount: (accountId: string) => Promise<void>  // 快速切换账户
  forgotPassword: (email: string) => Promise<void>
  resetPassword: (token: string, password: string) => Promise<void>
  checkAuth: () => Promise<void>
}

// 使用 Cookie 存储 Token,支持"记住我"(7天/1天)
Cookies.set("token", response.token, {
  expires: data.remember ? 7 : 1,
  secure: process.env.NODE_ENV === "production",
  sameSite: "strict",
})

2。数据获取 (TanStack Query)

tsx
// hooks/use-users.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

export function useUsers() {
  const queryClient = useQueryClient()

  // 查询用户列表
  const { data, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  // 创建用户
  const createUser = useMutation({
    mutationFn: createUserApi,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })

  return { data, isLoading, createUser }
}

3。权限控制

tsx
// 路由权限配置
export const ROUTE_PERMISSIONS: Record<string, Permission> = {
  "/": "dashboard:view",
  "/users": "users:view",
  "/analytics": "analytics:view",
  // ...
}

// 权限检查
const { hasPermission } = usePermission()
if (hasPermission("users:delete")) {
  // 显示删除按钮
}
tsx
// 权限守卫组件
<PermissionGuard permission="users:delete" fallback={<Disabled />}>
  <DeleteButton />
</PermissionGuard>

4。可拖拽仪表盘

tsx
// 仪表盘编辑模式
const { isEditing, setIsEditing, addWidget, removeWidget, resetToDefault } = useDashboardStore()

// 响应式布局 (列数自动适配)
// lg: 12列, md: 8列, sm: 4列, xs: 2列, mobile: 1列

支持 9 种小部件类型:

小部件类型 说明 数据来源
stats 数据统计卡片(4 指标) useDashboardStats
chart-line 折线图(访问趋势) useDashboardVisits
chart-bar 柱状图(销售统计) useDashboardSales
chart-pie 饼图(流量占比) useDashboardPie
recent-users 最近用户列表 useDashboardUsers
notifications 通知列表 useDashboardNotifications
tasks 待办任务 useDashboardTasks
calendar 今日日程 useDashboardCalendar
quick-actions 快捷操作入口 静态配置

主题系统

皮肤预设

支持 11 种皮肤预设,带实时预览和平滑过渡动画:

预设 名称 说明
default Shadcn · Neutral 官方默认中性色
blue Shadcn · Blue 蓝色主色 + Charts 冷色调
emerald Shadcn · Emerald 清新绿色,适合数据展示
amber Shadcn · Amber 琥珀/橙色,温暖明快
violet Shadcn · Violet 紫色高饱和,科技感
rose Shadcn · Rose 玫红主色,图表撞色
teal Shadcn · Teal 青色主色,现代感
slate Shadcn · Slate 低饱和灰蓝,工具感
ocean 旧 · 深海蓝 蓝绿渐变
sunset 旧 · 暮光橙 橙粉撞色
aurora 旧 · 极光绿 青绿 + 紫色

CSS 变量 (OKLch)

css
/* 示例变量定义 */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --muted: 96.4% 0.004 285.75;
  --accent: 96.4% 0.004 285.75;
  /* ... */
}

页面路由

路径 页面 权限
/ 重定向到仪表盘 -
/login 登录 公开
/register 注册 公开
/forgot-password 忘记密码 公开
/reset-password 重置密码 公开
/dashboard 可配置仪表盘 dashboard:view
/accounts 账号与权限 settings:view
/analytics 数据分析 analytics:view
/calendar 日程管理 calendar:view
/documents 文档管理 documents:view
/files 文件存储 files:view
/messages 消息中心 messages:view
/notifications 通知中心 notifications:view
/users 用户管理 users:view
/settings 系统设置 settings:view
/settings/teams 团队设置 settings:view
/settings/teams/roles 角色管理 settings:view
/profile 个人资料 settings:view
/docs 帮助文档 documents:view
/privacy 隐私政策 公开
/terms 服务条款 公开

环境变量

配置示例

bash
cp .env.example .env.local
env
# .env.local
NEXT_PUBLIC_API_URL=/api
NEXT_PUBLIC_MOCK=true
[email protected]
NEXT_PUBLIC_DEMO_PASSWORD=123456
NEXT_PUBLIC_SHOW_DEMO_HINT=false
NEXT_PUBLIC_WS_URL=
NEXT_PUBLIC_APP_TITLE=Admin Pro
NEXT_PUBLIC_BRAND_NAME=Halolight

变量说明

变量名 说明 默认值
NEXT_PUBLIC_API_URL API 基础路径 /api
NEXT_PUBLIC_MOCK 是否启用 Mock 数据 true
NEXT_PUBLIC_DEMO_EMAIL 演示账号邮箱 [email protected]
NEXT_PUBLIC_DEMO_PASSWORD 演示账号密码 123456
NEXT_PUBLIC_SHOW_DEMO_HINT 是否显示演示提示 false
NEXT_PUBLIC_WS_URL WebSocket 地址 -
NEXT_PUBLIC_APP_TITLE 应用标题 Admin Pro
NEXT_PUBLIC_BRAND_NAME 品牌名称 Halolight

使用方式

tsx
// 在客户端组件中使用
const apiUrl = process.env.NEXT_PUBLIC_API_URL
const isMock = process.env.NEXT_PUBLIC_MOCK === 'true'

常用命令

bash
pnpm dev            # 启动开发服务器
pnpm build          # 生产构建
pnpm start          # 预览生产构建
pnpm lint           # 代码检查
pnpm lint:fix       # 自动修复
pnpm type-check     # 类型检查
pnpm test           # 运行测试
pnpm test:coverage  # 测试覆盖率

测试

bash
pnpm test           # 运行测试(watch 模式)
pnpm test:run       # 单次运行
pnpm test:coverage  # 覆盖率报告
pnpm test:ui        # Vitest UI 界面

测试示例

tsx
// __tests__/components/button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '@/components/ui/button'

describe('Button Component', () => {
  it('renders correctly', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('handles click events', () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

配置

Next.js 配置

js
// next.config.mjs
import withPWA from "next-pwa"

const nextConfig = {
  // 包导入优化 - 减少打包体积
  experimental: {
    optimizePackageImports: [
      "@radix-ui/react-*",
      "lucide-react",
      "framer-motion",
      "@tanstack/react-query",
      "recharts",
      "zustand",
    ],
  },

  // 生产环境移除 console
  compiler: {
    removeConsole: { exclude: ["error", "warn"] },
  },

  // 关闭 source map
  productionBrowserSourceMaps: false,

  // 图片优化
  images: {
    formats: ["image/avif", "image/webp"],
  },
}

const pwaConfig = withPWA({
  dest: "public",
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === "development",
})

export default pwaConfig(nextConfig)

部署

Vercel (推荐)

Deploy with Vercel

bash
vercel

Docker

dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]
bash
docker build -t halolight-nextjs .
docker run -p 3000:3000 halolight-nextjs

其他平台

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

多标签导航

tsx
// stores/tabs-store.ts
interface Tab {
  id: string
  title: string
  path: string
  icon?: string
  closable?: boolean  // 首页不可关闭
}

// 右键菜单功能
- 刷新页面
- 关闭标签
- 关闭其他
- 关闭右侧
- 关闭所有

页面状态缓存 (Keep-Alive)

tsx
// hooks/use-keep-alive.tsx

// 自动保存/恢复滚动位置
useScrollRestore()

// 保存表单状态
const [values, saveValues, clearCache] = useFormCache('filter-form', initialValues)

// 保存自定义状态
const [state, setState] = useStateCache('my-key', initialValue)

命令面板 (⌘K)

tsx
// components/layout/command-menu.tsx
// 支持键盘快速导航、主题切换、账户切换、退出登录等操作

快捷键:
-K / Ctrl+K - 打开命令面板
- 搜索页面 - 快速导航到任意页面
- 切换主题 - 明暗模式切换
- 切换账户 - 多账户快速切换

实时通知 (WebSocket)

tsx
// providers/websocket-provider.tsx
const { status, lastMessage, sendMessage, reconnect } = useWebSocket()

// 监听新通知
useRealtimeNotifications((notification) => {
  console.log('新通知:', notification)
})

// 连接状态
status === 'Open' // 已连接
status === 'Connecting' // 连接中
status === 'Closed' // 已断开

PWA 支持

js
// next.config.mjs
const pwaConfig = withPWA({
  dest: "public",
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === "development",
  runtimeCaching: [
    // 字体缓存 (1年)
    { urlPattern: /\.(?:woff|woff2|ttf)$/i, handler: "CacheFirst" },
    // 图片缓存 (24小时)
    { urlPattern: /\.(?:jpg|png|svg|webp)$/i, handler: "StaleWhileRevalidate" },
    // Next.js 静态资源 (1年)
    { urlPattern: /\/_next\/static\/.+\.(js|css)$/i, handler: "CacheFirst" },
    // 页面数据 (1小时)
    { urlPattern: /\/_next\/data\/.+\.json$/i, handler: "NetworkFirst" },
  ],
})

功能特性:

  • 离线访问 - Service Worker 缓存静态资源
  • 安装到桌面 - 支持 Add to Home Screen
  • 自托管字体 - Inter + JetBrains Mono
  • 图标支持 - 8 种尺寸 (72x72 ~ 512x512)

性能优化

图片优化

tsx
// 使用 Next.js Image 组件
import Image from 'next/image'

<Image
  src="/images/hero.png"
  alt="Hero"
  width={800}
  height={600}
  priority // 优先加载
  placeholder="blur" // 模糊占位符
/>

// next.config.mjs
images: {
  formats: ["image/avif", "image/webp"],
}

懒加载组件

tsx
// 动态导入组件
import dynamic from 'next/dynamic'

const DashboardChart = dynamic(
  () => import('@/components/dashboard/chart'),
  {
    loading: () => <Skeleton />,
    ssr: false // 禁用 SSR
  }
)

预加载

tsx
// 路由预加载
import Link from 'next/link'

<Link href="/dashboard" prefetch>
  Dashboard
</Link>

// 数据预加载
queryClient.prefetchQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})

包导入优化

js
// next.config.mjs
experimental: {
  optimizePackageImports: [
    "@radix-ui/react-*",
    "lucide-react",
    "framer-motion",
    "@tanstack/react-query",
    "recharts",
    "zustand",
  ],
}

常见问题

Q:如何关闭 Mock 数据?

A:在 .env.local 中设置 NEXT_PUBLIC_MOCK=false,并配置真实的 API 地址。

env
NEXT_PUBLIC_MOCK=false
NEXT_PUBLIC_API_URL=https://api.example.com

Q:如何添加新页面?

A:在 src/app/(dashboard) 下创建新目录和 page.tsx 文件。

tsx
// src/app/(dashboard)/my-page/page.tsx
export default function MyPage() {
  return <div>My Page</div>
}

// 添加路由权限
// src/config/routes.ts
export const ROUTE_PERMISSIONS = {
  // ...
  "/my-page": "my-page:view",
}

Q:如何自定义主题颜色?

A:修改 tailwind.config.js 中的 CSS 变量。

js
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: 'oklch(var(--primary))',
          foreground: 'oklch(var(--primary-foreground))',
        },
      },
    },
  },
}
css
/* app/globals.css */
:root {
  --primary: 51.1% 0.262 276.97; /* 修改为你的颜色 */
}

Q:如何禁用 PWA?

A:在 next.config.mjs 中设置 disable: true

js
const pwaConfig = withPWA({
  dest: "public",
  disable: true, // 禁用 PWA
})

Q:如何部署到静态托管平台?

A:配置静态导出模式。

js
// next.config.mjs
export default {
  output: 'export',
  images: {
    unoptimized: true, // 静态导出需要禁用图片优化
  },
}
bash
pnpm build
# 输出到 out/ 目录

与其他版本对比

特性 Next.js Vue Angular
SSR/SSG ✅ (Nuxt) ✅ (Universal)
状态管理 Zustand Pinia Services + RxJS
路由 App Router Vue Router Angular Router
构建工具 Next.js Vite esbuild + Vite
组件库 shadcn/ui shadcn-vue Angular Material
学习曲线 中等 较低 较高
性能 优秀 优秀 优秀

相关链接

]]>
<![CDATA[Nuxt 版本]]> https://halolight.docs.h7ml.cn/guide/nuxt https://halolight.docs.h7ml.cn/guide/nuxt Fri, 19 Dec 2025 04:56:41 GMT Nuxt 版本

HaloLight Nuxt 版本基于 Nuxt 3 构建,采用 Vue 3.5 + Composition API + TypeScript,提供开箱即用的全栈开发体验。

在线预览https://halolight-nuxt.h7ml.cn/

GitHubhttps://github.com/halolight/halolight-nuxt

特性

  • 🔄 自动导入 - 组件、composables、API 自动导入,零配置
  • 📁 文件路由 - 基于文件系统的自动路由
  • 🌐 全栈开发 - 内置 Nitro 服务端,前后端统一
  • 🚀 SSR/SSG/SPA - 多种渲染模式灵活选择
  • Vite 驱动 - 极速 HMR 热更新
  • 🔌 模块生态 - 丰富的 Nuxt 模块扩展
  • 🎨 主题系统 - 深色/浅色主题切换
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 命令面板 - ⌘/Ctrl + K 快速导航

技术栈

技术 版本 说明
Nuxt 3.10 Vue 全栈框架
Vue 3.5+ 渐进式框架
TypeScript 5.7 类型安全
Tailwind CSS 3.x (CDN) 原子化 CSS
Pinia 0.5 状态管理
VueUse 10.x 组合式工具库
Mock.js 1.x 数据模拟

核心特性

  • 全栈开发:内置 Nitro 服务端,前后端统一开发
  • 自动导入:组件、composables、API 自动导入
  • 文件路由:基于文件系统的自动路由
  • SSR/SSG:服务端渲染与静态生成可选
  • 命令面板⌘/Ctrl + K 快速导航
  • 热更新:开发体验极佳的 HMR
  • 模块生态:丰富的 Nuxt 模块扩展

目录结构

halolight-nuxt/
├── nuxt.config.ts              # Nuxt 配置
├── app.vue                     # 应用根组件
├── pages/                      # 文件路由
│   ├── index.vue              # 首页
│   ├── login.vue              # 登录
│   ├── register.vue           # 注册
│   ├── forgot-password.vue    # 忘记密码
│   ├── reset-password.vue     # 重置密码
│   ├── terms.vue              # 服务条款
│   ├── privacy.vue            # 隐私政策
│   ├── dashboard/             # 仪表盘
│   │   └── index.vue
│   ├── users/                 # 用户管理
│   │   └── index.vue
│   ├── messages/              # 消息中心
│   │   └── index.vue
│   ├── analytics/             # 数据分析
│   │   └── index.vue
│   ├── profile/               # 个人中心
│   │   └── index.vue
│   └── settings/              # 系统设置
│       └── index.vue
├── components/                 # 自动导入组件
│   └── common/                # 通用组件
│       ├── AppHeader.vue
│       ├── AppSidebar.vue
│       ├── AppFooter.vue
│       ├── AppTabs.vue
│       ├── CommandMenu.vue
│       └── ToastContainer.vue
├── composables/                # 组合式函数
│   ├── useTheme.ts
│   ├── useToast.ts
│   └── useCommandMenu.ts
├── layouts/                    # 布局
│   ├── default.vue            # 管理后台布局
│   └── auth.vue               # 认证页面布局
├── middleware/                 # 中间件
│   └── auth.global.ts
├── plugins/                    # 插件
│   └── pinia-persist.client.ts
├── stores/                     # Pinia stores
│   ├── auth.ts
│   ├── ui-settings.ts
│   ├── dashboard.ts
│   ├── layout.ts
│   └── tabs.ts
├── utils/                      # 工具函数
│   ├── index.ts
│   └── mock.ts
├── assets/css/                 # 样式文件
│   ├── main.css
│   └── tailwind.css
├── tests/                      # 测试文件
│   └── unit/
├── .github/                    # GitHub Actions
│   └── workflows/
│       ├── ci.yml
│       └── deploy.yml
└── public/                     # 静态资源

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-nuxt.git
cd halolight-nuxt
pnpm install

环境变量

bash
cp .env.example .env.local
env
# .env.local
NUXT_PUBLIC_API_BASE=/api
NUXT_PUBLIC_MOCK=true
[email protected]
NUXT_PUBLIC_DEMO_PASSWORD=123456
NUXT_PUBLIC_SHOW_DEMO_HINT=true
NUXT_PUBLIC_APP_TITLE=Admin Pro
NUXT_PUBLIC_BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:3000

构建生产

bash
pnpm build
pnpm preview

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

核心功能

状态管理 (Pinia)

ts
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null)
  const token = ref('')
  const loading = ref(false)
  const error = ref('')

  const isAuthenticated = computed(() => !!token.value && !!user.value)

  async function login(credentials: LoginCredentials) {
    loading.value = true
    error.value = ''
    try {
      // 登录逻辑
      const result = await mockLogin(credentials)
      user.value = result.user
      token.value = result.token
    } catch (e) {
      error.value = e.message
      throw e
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
    token.value = ''
    navigateTo('/login')
  }

  return { user, token, loading, error, isAuthenticated, login, logout }
})

数据获取 (useFetch)

vue
<script setup lang="ts">
// 使用 useFetch 自动处理 SSR
const { data: users, pending, error, refresh } = await useFetch('/api/users', {
  query: { page: 1, limit: 10 },
})

// 使用 useAsyncData 自定义 key
const { data: stats } = await useAsyncData('dashboard-stats', () =>
  $fetch('/api/dashboard/stats')
)
</script>

权限控制

ts
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
  const authStore = useAuthStore()

  // 公开页面列表
  const publicPages = ['/login', '/register', '/forgot-password', '/reset-password']

  if (publicPages.includes(to.path)) {
    return
  }

  if (!authStore.isAuthenticated) {
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath },
    })
  }
})

可拖拽仪表盘

vue
<script setup lang="ts">
// 仪表盘配置
const dashboardStore = useDashboardStore()
const widgets = computed(() => dashboardStore.widgets)

// 拖拽实现
function handleDragEnd(event) {
  dashboardStore.updateLayout(event.newLayout)
}
</script>

页面路由

路径 页面 权限
/ 首页 公开
/login 登录 公开
/register 注册 公开
/forgot-password 忘记密码 公开
/reset-password 重置密码 公开
/terms 服务条款 公开
/privacy 隐私政策 公开
/dashboard 仪表盘 dashboard:view
/users 用户管理 users:view
/messages 消息中心 messages:view
/analytics 数据分析 analytics:view
/profile 个人中心 settings:view
/settings 系统设置 settings:view

环境变量

配置示例

bash
# .env
NUXT_PUBLIC_API_BASE=/api
NUXT_PUBLIC_MOCK=true
NUXT_PUBLIC_DEMO_EMAIL=[email protected]
NUXT_PUBLIC_DEMO_PASSWORD=123456
NUXT_PUBLIC_SHOW_DEMO_HINT=true
NUXT_PUBLIC_APP_TITLE=Admin Pro
NUXT_PUBLIC_BRAND_NAME=Halolight

# 服务端私有变量
NUXT_JWT_SECRET=your-jwt-secret
NUXT_DATABASE_URL=postgresql://localhost:5432/halolight

变量说明

变量名 说明 默认值
NUXT_PUBLIC_API_BASE API 基础 URL /api
NUXT_PUBLIC_MOCK 启用 Mock 数据 true
NUXT_PUBLIC_APP_TITLE 应用标题 Admin Pro
NUXT_PUBLIC_BRAND_NAME 品牌名称 Halolight
NUXT_PUBLIC_DEMO_EMAIL 演示账号邮箱 -
NUXT_PUBLIC_DEMO_PASSWORD 演示账号密码 -
NUXT_JWT_SECRET JWT 密钥 (服务端) -
NUXT_DATABASE_URL 数据库连接 (服务端) -

使用方式

vue
<script setup lang="ts">
// 在组件中使用
const config = useRuntimeConfig();

// 公开变量
const apiBase = config.public.apiBase;

// 私有变量(仅服务端)
// const jwtSecret = config.jwtSecret; // 客户端不可访问
</script>
typescript
// 在 server/api 中使用
export default defineEventHandler((event) => {
  const config = useRuntimeConfig();
  const jwtSecret = config.jwtSecret; // 可以访问私有变量
});

配置

Nuxt 配置

ts
// nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2025-11-30',
  devtools: { enabled: false },

  modules: [
    '@pinia/nuxt',
    '@vueuse/nuxt',
  ],

  css: ['~/assets/css/main.css'],

  runtimeConfig: {
    public: {
      apiBase: '/api',
      mock: true,
      demoEmail: '[email protected]',
      demoPassword: '123456',
      showDemoHint: true,
      appTitle: 'Admin Pro',
      brandName: 'Halolight',
    },
  },

  app: {
    head: {
      title: 'Admin Pro',
      script: [
        { src: 'https://cdn.tailwindcss.com' },
      ],
    },
  },
})

部署

Vercel (推荐)

bash
npx vercel

或使用 Vercel 按钮一键部署:

Deploy with Vercel

Docker

dockerfile
FROM node:20-alpine AS builder

RUN npm install -g pnpm

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/.output ./.output

ENV HOST=0.0.0.0
ENV PORT=3000

EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]

其他平台

  • Cloudflare Pages:配置 nitro.preset: 'cloudflare-pages'
  • Netlify:配置 nitro.preset: 'netlify'
  • Node.js 服务器pnpm build && node .output/server/index.mjs

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

Server Routes (API 端点)

Nuxt 3 内置 Nitro 服务器,支持创建服务端 API。

typescript
// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const { page = 1, limit = 10 } = query;

  // 模拟数据库查询
  const users = await getUsersFromDB({ page: Number(page), limit: Number(limit) });

  return {
    success: true,
    data: users,
    pagination: {
      page: Number(page),
      limit: Number(limit),
      total: 100,
    },
  };
});

Server Middleware

typescript
// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const url = getRequestURL(event);

  // 保护 API 路由
  if (url.pathname.startsWith('/api/admin')) {
    const token = getHeader(event, 'authorization')?.replace('Bearer ', '');

    if (!token) {
      throw createError({
        statusCode: 401,
        message: '未授权访问',
      });
    }

    try {
      const user = verifyToken(token);
      event.context.user = user;
    } catch {
      throw createError({
        statusCode: 401,
        message: 'Token 无效或已过期',
      });
    }
  }
});

Plugins

typescript
// plugins/api.ts
export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig();

  const api = $fetch.create({
    baseURL: config.public.apiBase,
    onRequest({ options }) {
      const authStore = useAuthStore();
      if (authStore.token) {
        options.headers = {
          ...options.headers,
          Authorization: `Bearer ${authStore.token}`,
        };
      }
    },
    onResponseError({ response }) {
      if (response.status === 401) {
        const authStore = useAuthStore();
        authStore.logout();
        navigateTo('/login');
      }
    },
  });

  return {
    provide: {
      api,
    },
  };
});

Composables

typescript
// composables/useUsers.ts
export function useUsers() {
  const { $api } = useNuxtApp();

  const users = ref<User[]>([]);
  const loading = ref(false);
  const error = ref<string | null>(null);

  async function fetchUsers(params?: { page?: number; limit?: number }) {
    loading.value = true;
    error.value = null;

    try {
      const response = await $api<ApiResponse<User[]>>('/api/users', {
        params,
      });
      users.value = response.data;
      return response;
    } catch (e: any) {
      error.value = e.message;
      throw e;
    } finally {
      loading.value = false;
    }
  }

  return {
    users,
    loading,
    error,
    fetchUsers,
  };
}

性能优化

图片优化

vue
<script setup lang="ts">
// 使用 @nuxt/image
</script>

<template>
  <NuxtImg
    src="/hero.jpg"
    alt="Hero"
    width="800"
    height="600"
    loading="lazy"
    format="webp"
  />

  <NuxtPicture
    src="/hero.jpg"
    alt="Hero"
    width="800"
    height="600"
    sizes="sm:100vw md:50vw lg:33vw"
  />
</template>

懒加载组件

vue
<script setup lang="ts">
// 延迟加载重型组件
const Chart = defineAsyncComponent(() => import('~/components/Chart.vue'));
</script>

<template>
  <ClientOnly>
    <Chart :data="chartData" />
    <template #fallback>
      <div class="h-80 animate-pulse bg-gray-200" />
    </template>
  </ClientOnly>
</template>

预加载

vue
<script setup lang="ts">
// 预加载关键数据
const { data } = await useFetch('/api/critical-data', {
  key: 'critical',
  lazy: false,
});
</script>

常见问题

Q:如何配置 SSG (静态生成)?

A:修改 nuxt.config.ts

typescript
export default defineNuxtConfig({
  ssr: true,
  nitro: {
    prerender: {
      routes: ['/', '/about', '/contact'],
      crawlLinks: true,
    },
  },
});

运行 pnpm generate 生成静态站点。

Q:如何配置 SPA 模式?

A:禁用 SSR:

typescript
export default defineNuxtConfig({
  ssr: false,
});

Q:useFetch 和 $fetch 的区别?

A:

  • useFetch 是 composable,自动处理 SSR 数据同步
  • $fetch 是底层方法,不处理 SSR
vue
<script setup lang="ts">
// 推荐:自动处理 SSR
const { data } = await useFetch('/api/users');

// 手动调用
const fetchData = async () => {
  const data = await $fetch('/api/users');
};
</script>

Q:如何添加全局 CSS?

A:在 nuxt.config.ts 中配置:

typescript
export default defineNuxtConfig({
  css: ['~/assets/css/main.css'],
});

Q:如何配置代理?

A:使用 nitro.routeRules

typescript
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/api/external/**': {
        proxy: 'https://api.example.com/**',
      },
    },
  },
});

Q:如何优化构建体积?

A:优化建议:

typescript
export default defineNuxtConfig({
  // 按需导入组件
  components: {
    dirs: ['~/components'],
    global: false,
  },

  // 实验性功能
  experimental: {
    treeshakeClientOnly: true,
  },

  // Vite 优化
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            'vue-vendor': ['vue', 'vue-router', 'pinia'],
          },
        },
      },
    },
  },
});

与其他版本对比

功能 Nuxt 版本 Vue 版本 Next.js 版本
状态管理 Pinia Pinia Zustand
数据获取 useFetch Axios TanStack Query
表单验证 原生 VeeValidate + Zod React Hook Form + Zod
服务端 Nitro 内置 独立后端 API Routes
样式 Tailwind CDN Tailwind Tailwind
路由 文件路由 Vue Router App Router
SSR 内置支持 需配置 内置支持

相关链接

]]>
<![CDATA[Preact 版本]]> https://halolight.docs.h7ml.cn/guide/preact https://halolight.docs.h7ml.cn/guide/preact Fri, 19 Dec 2025 04:56:41 GMT Preact 版本

HaloLight Preact 版本基于 Preact + Vite 构建,采用 Signals + TypeScript,实现轻量高性能的管理后台。

在线预览https://halolight-preact.h7ml.cn

GitHubhttps://github.com/halolight/halolight-preact

特性

  • 🪶 极致轻量 - 核心库仅 3KB gzip
  • 高性能 Signals - 响应式状态管理,自动依赖追踪
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • ⚛️ React 兼容 - 可直接使用大部分 React 生态
  • 🚀 快速启动 - Vite 提供极速开发体验

技术栈

技术 版本 说明
Preact 10.x 轻量 React 替代方案
@preact/signals 2.x 响应式状态管理
TypeScript 5.9 类型安全
Tailwind CSS 4.x 原子化 CSS
shadcn/ui latest UI 组件库(兼容层)
Vite 7.2 构建工具
preact-router 4.x 客户端路由
TanStack Query 5.x 服务端状态
Mock.js 1.x 数据模拟

核心特性

  • Signals 状态管理 - 高性能响应式,自动依赖追踪,细粒度更新
  • 权限系统 - RBAC 权限控制,路由守卫,权限组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 数据模拟 - Mock.js + Fetch 拦截,完整后端模拟
  • React 兼容 - 通过 preact/compat 使用 React 生态库

目录结构

halolight-preact/
├── src/
│   ├── pages/                     # 页面组件
│   │   ├── Home.tsx              # 首页
│   │   ├── auth/                 # 认证页面
│   │   │   ├── Login.tsx
│   │   │   ├── Register.tsx
│   │   │   ├── ForgotPassword.tsx
│   │   │   └── ResetPassword.tsx
│   │   └── dashboard/            # 仪表盘页面
│   │       ├── Dashboard.tsx
│   │       ├── Users.tsx
│   │       ├── UserDetail.tsx
│   │       ├── UserCreate.tsx
│   │       ├── Roles.tsx
│   │       ├── Permissions.tsx
│   │       ├── Settings.tsx
│   │       └── Profile.tsx
│   ├── components/               # 组件库
│   │   ├── ui/                   # UI 组件
│   │   │   ├── Button.tsx
│   │   │   ├── Input.tsx
│   │   │   ├── Card.tsx
│   │   │   └── Dialog.tsx
│   │   ├── layout/               # 布局组件
│   │   │   ├── AdminLayout.tsx
│   │   │   ├── AuthLayout.tsx
│   │   │   ├── Sidebar.tsx
│   │   │   └── Header.tsx
│   │   ├── dashboard/            # 仪表盘组件
│   │   │   ├── DashboardGrid.tsx
│   │   │   ├── WidgetWrapper.tsx
│   │   │   └── StatsWidget.tsx
│   │   └── shared/               # 共享组件
│   │       └── PermissionGuard.tsx
│   ├── stores/                   # 状态管理
│   │   ├── auth.ts
│   │   ├── ui-settings.ts
│   │   └── dashboard.ts
│   ├── hooks/                    # 自定义 Hooks
│   │   ├── useAuth.ts
│   │   └── usePermission.ts
│   ├── lib/                      # 工具库
│   │   ├── api.ts
│   │   ├── permission.ts
│   │   └── cn.ts
│   ├── mock/                     # Mock 数据
│   │   ├── index.ts
│   │   └── handlers/
│   ├── types/                    # 类型定义
│   ├── App.tsx                   # 根组件
│   ├── routes.tsx                # 路由配置
│   └── main.tsx                  # 入口文件
├── public/                       # 静态资源
├── vite.config.ts               # Vite 配置
├── tailwind.config.ts           # Tailwind 配置
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-preact.git
cd halolight-preact
pnpm install

环境变量

bash
cp .env.example .env
env
# .env
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:5173

构建生产

bash
pnpm build
pnpm preview

核心功能

状态管理 (@preact/signals)

tsx
// stores/auth.ts
import { signal, computed, effect } from '@preact/signals'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

// 响应式状态
export const user = signal<User | null>(null)
export const token = signal<string | null>(null)
export const loading = signal(false)

// 计算属性
export const isAuthenticated = computed(() => !!token.value && !!user.value)
export const permissions = computed(() => user.value?.permissions ?? [])

// 持久化
effect(() => {
  if (user.value && token.value) {
    localStorage.setItem('auth', JSON.stringify({
      user: user.value,
      token: token.value,
    }))
  }
})

// 初始化
const saved = localStorage.getItem('auth')
if (saved) {
  const { user: savedUser, token: savedToken } = JSON.parse(saved)
  user.value = savedUser
  token.value = savedToken
}

// 方法
export async function login(credentials: { email: string; password: string }) {
  loading.value = true
  try {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
      headers: { 'Content-Type': 'application/json' },
    })
    const data = await response.json()

    user.value = data.user
    token.value = data.token
  } finally {
    loading.value = false
  }
}

export function logout() {
  user.value = null
  token.value = null
  localStorage.removeItem('auth')
}

export function hasPermission(permission: string): boolean {
  const perms = permissions.value
  return perms.some(p =>
    p === '*' || p === permission ||
    (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
  )
}

Signals 特性

  • 细粒度更新 - 只更新依赖该 Signal 的组件
  • 自动依赖追踪 - 无需手动声明依赖
  • 无需记忆化 - 计算属性自动缓存
  • 跨组件通信 - 全局状态自动同步

数据获取 (TanStack Query)

tsx
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { token } from '../stores/auth'

export function useUsers(page = 1) {
  return useQuery({
    queryKey: ['users', page],
    queryFn: async () => {
      const response = await fetch(`/api/users?page=${page}`, {
        headers: { Authorization: `Bearer ${token.value}` },
      })
      return response.json()
    },
    enabled: !!token.value,
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async (data: CreateUserDto) => {
      const response = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(data),
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${token.value}`,
        },
      })
      return response.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}
tsx
// 使用
import { useUsers } from '../hooks/useUsers'

export function UsersPage() {
  const { data, isLoading, error } = useUsers(1)

  if (isLoading) return <div>加载中...</div>
  if (error) return <div>加载失败</div>

  return (
    <ul>
      {data.data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

权限控制

tsx
// hooks/usePermission.ts
import { hasPermission } from '../stores/auth'

export function usePermission() {
  return {
    hasPermission,
    can: (permission: string) => hasPermission(permission),
  }
}
tsx
// components/shared/PermissionGuard.tsx
import { ComponentChildren } from 'preact'
import { hasPermission } from '../../stores/auth'

interface Props {
  permission: string
  children: ComponentChildren
  fallback?: ComponentChildren
}

export function PermissionGuard({ permission, children, fallback }: Props) {
  if (!hasPermission(permission)) {
    return fallback ?? null
  }

  return children
}
tsx
// 使用
<PermissionGuard
  permission="users:delete"
  fallback={<span class="text-muted-foreground">无权限</span>}
>
  <Button variant="destructive">删除</Button>
</PermissionGuard>

路由配置

tsx
// routes.tsx
import Router, { Route } from 'preact-router'
import { isAuthenticated, hasPermission } from './stores/auth'

// 页面组件
import Home from './pages/Home'
import Login from './pages/auth/Login'
import Register from './pages/auth/Register'
import Dashboard from './pages/dashboard/Dashboard'
import Users from './pages/dashboard/Users'

// 路由守卫 HOC
function ProtectedRoute({ component: Component, permission, ...rest }) {
  if (!isAuthenticated.value) {
    route('/login?redirect=' + rest.path)
    return null
  }

  if (permission && !hasPermission(permission)) {
    return <div>无权限访问</div>
  }

  return <Component {...rest} />
}

export function AppRouter() {
  return (
    <Router>
      <Route path="/" component={Home} />
      <Route path="/login" component={Login} />
      <Route path="/register" component={Register} />
      <ProtectedRoute
        path="/dashboard"
        component={Dashboard}
        permission="dashboard:view"
      />
      <ProtectedRoute
        path="/users"
        component={Users}
        permission="users:list"
      />
    </Router>
  )
}

主题系统

皮肤预设

支持 11 种预设皮肤,通过快捷设置面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿 --primary: 64.6% 0.178 142.49
Amber 琥珀 --primary: 78.3% 0.177 74.21
Rose 玫瑰 --primary: 62.8% 0.243 12.48
Slate 石板 --primary: 51.4% 0.032 257.42
Zinc 锌灰 --primary: 50.7% 0.017 285.96
Stone 石灰 --primary: 53.4% 0.015 69.82
Neutral 中性 --primary: 50.9% 0.016 286.13
Red 红色 --primary: 55.5% 0.238 25.33
Orange 橙色 --primary: 72.3% 0.187 56.24

CSS 变量 (OKLch)

css
/* 亮色模式 */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 96.5% 0.006 286.32;
  --secondary-foreground: 21.7% 0.026 285.88;
  --accent: 96.5% 0.006 286.32;
  --accent-foreground: 21.7% 0.026 285.88;
}

/* 暗色模式 */
.dark {
  --background: 15.5% 0.018 285.88;
  --foreground: 98.3% 0.006 286.32;
  --primary: 74.1% 0.196 275.74;
  --primary-foreground: 21.7% 0.043 286.07;
  --secondary: 20.7% 0.021 286.05;
  --secondary-foreground: 98.3% 0.006 286.32;
}

主题切换

tsx
// stores/ui-settings.ts
import { signal, effect } from '@preact/signals'

export const theme = signal<'light' | 'dark'>('light')
export const skin = signal<string>('default')

// 持久化
effect(() => {
  localStorage.setItem('theme', theme.value)
  document.documentElement.classList.toggle('dark', theme.value === 'dark')
})

effect(() => {
  localStorage.setItem('skin', skin.value)
  document.documentElement.dataset.skin = skin.value
})

// 初始化
theme.value = (localStorage.getItem('theme') as any) || 'light'
skin.value = localStorage.getItem('skin') || 'default'

export function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

export function setSkin(newSkin: string) {
  skin.value = newSkin
}

页面路由

路径 页面 权限
/ 首页 公开
/login 登录 公开
/register 注册 公开
/forgot-password 忘记密码 公开
/reset-password 重置密码 公开
/dashboard 仪表盘 dashboard:view
/users 用户列表 users:list
/users/create 创建用户 users:create
/users/:id 用户详情 users:view
/roles 角色管理 roles:list
/permissions 权限管理 permissions:list
/settings 系统设置 settings:view
/profile 个人中心 登录即可

常用命令

bash
pnpm dev            # 启动开发服务器
pnpm build          # 生产构建
pnpm preview        # 预览生产构建
pnpm lint           # 代码检查
pnpm lint:fix       # 自动修复
pnpm type-check     # 类型检查
pnpm test           # 运行测试
pnpm test:coverage  # 测试覆盖率

部署

Vercel (推荐)

Deploy with Vercel

Docker

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
bash
docker build -t halolight-preact .
docker run -p 80:80 halolight-preact

其他平台

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

测试

测试命令

bash
pnpm test           # 运行测试(watch 模式)
pnpm test:run       # 单次运行
pnpm test:coverage  # 覆盖率报告
pnpm test:ui        # Vitest UI 界面

测试文件组织

测试文件与源文件放在一起,使用 .test.ts.test.tsx 后缀:

src/components/ui/
├── Button.tsx
├── Button.test.tsx     # Button 组件测试
├── Input.tsx
└── Input.test.tsx      # Input 组件测试

测试示例

tsx
// src/components/ui/Button.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/preact'
import { Button } from './Button'

describe('Button', () => {
  it('渲染默认按钮', () => {
    render(<Button>点击</Button>)
    expect(screen.getByRole('button')).toHaveTextContent('点击')
  })

  it('渲染不同变体', () => {
    render(<Button variant="destructive">删除</Button>)
    expect(screen.getByRole('button')).toHaveClass('bg-destructive')
  })

  it('禁用状态', () => {
    render(<Button disabled>禁用</Button>)
    expect(screen.getByRole('button')).toBeDisabled()
  })
})

配置

Vite 配置

ts
// vite.config.ts
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
import path from 'path'

export default defineConfig({
  plugins: [preact()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      // React 兼容
      react: 'preact/compat',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
  build: {
    target: 'esnext',
    minify: 'terser',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['preact', 'preact/hooks'],
          router: ['preact-router'],
          query: ['@tanstack/react-query'],
        },
      },
    },
  },
})

Tailwind 配置

ts
// tailwind.config.ts
import type { Config } from 'tailwindcss'

export default {
  darkMode: ['class'],
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        border: 'oklch(var(--border) / <alpha-value>)',
        input: 'oklch(var(--input) / <alpha-value>)',
        ring: 'oklch(var(--ring) / <alpha-value>)',
        background: 'oklch(var(--background) / <alpha-value>)',
        foreground: 'oklch(var(--foreground) / <alpha-value>)',
        primary: {
          DEFAULT: 'oklch(var(--primary) / <alpha-value>)',
          foreground: 'oklch(var(--primary-foreground) / <alpha-value>)',
        },
      },
    },
  },
  plugins: [require('tailwindcss-animate')],
} satisfies Config

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

组件示例

tsx
// components/ui/Button.tsx
import { ComponentChildren } from 'preact'
import { cn } from '../../lib/cn'

interface Props {
  variant?: 'default' | 'destructive' | 'outline' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  class?: string
  children: ComponentChildren
  onClick?: () => void
}

export function Button({
  variant = 'default',
  size = 'md',
  disabled,
  class: className,
  children,
  onClick,
}: Props) {
  return (
    <button
      class={cn(
        'inline-flex items-center justify-center rounded-md font-medium transition-colors',
        {
          'bg-primary text-primary-foreground hover:bg-primary/90':
            variant === 'default',
          'bg-destructive text-destructive-foreground hover:bg-destructive/90':
            variant === 'destructive',
          'border border-input bg-background hover:bg-accent':
            variant === 'outline',
          'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
          'h-8 px-3 text-sm': size === 'sm',
          'h-10 px-4': size === 'md',
          'h-12 px-6 text-lg': size === 'lg',
          'opacity-50 cursor-not-allowed': disabled,
        },
        className
      )}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

表单处理

tsx
// pages/auth/Login.tsx
import { useState } from 'preact/hooks'
import { route } from 'preact-router'
import { login, loading } from '../../stores/auth'

export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState('')

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    setError('')

    try {
      await login({ email, password })
      const params = new URLSearchParams(location.search)
      route(params.get('redirect') || '/dashboard')
    } catch (e) {
      setError('邮箱或密码错误')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <div class="text-destructive">{error}</div>}

      <input
        type="email"
        value={email}
        onInput={(e) => setEmail(e.currentTarget.value)}
        placeholder="邮箱"
        required
      />

      <input
        type="password"
        value={password}
        onInput={(e) => setPassword(e.currentTarget.value)}
        placeholder="密码"
        required
      />

      <button type="submit" disabled={loading.value}>
        {loading.value ? '登录中...' : '登录'}
      </button>
    </form>
  )
}

性能优化

懒加载组件

tsx
// App.tsx
import { lazy, Suspense } from 'preact/compat'

const Dashboard = lazy(() => import('./pages/dashboard/Dashboard'))
const Users = lazy(() => import('./pages/dashboard/Users'))

export function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Router>
        <Route path="/dashboard" component={Dashboard} />
        <Route path="/users" component={Users} />
      </Router>
    </Suspense>
  )
}

代码分割

ts
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['preact', 'preact/hooks'],
          router: ['preact-router'],
          query: ['@tanstack/react-query'],
        },
      },
    },
  },
})

Signals 优化

tsx
// 使用 computed 避免重复计算
import { signal, computed } from '@preact/signals'

const items = signal([1, 2, 3, 4, 5])
const filter = signal('all')

// 计算属性自动缓存
const filteredItems = computed(() => {
  if (filter.value === 'all') return items.value
  return items.value.filter(item => item > 2)
})

// 组件中使用
function ItemList() {
  return (
    <ul>
      {filteredItems.value.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  )
}

常见问题

Q:如何使用 React 生态库?

A:Preact 通过 preact/compat 提供 React 兼容层,大部分 React 库可直接使用:

ts
// vite.config.ts
export default defineConfig({
  resolve: {
    alias: {
      react: 'preact/compat',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
})

Q:Signals 如何与 React Hook 结合?

A:Signals 可以直接在组件中使用,无需 useState:

tsx
import { signal } from '@preact/signals'

const count = signal(0)

function Counter() {
  // 直接使用 signal.value
  return (
    <button onClick={() => count.value++}>
      Count: {count.value}
    </button>
  )
}

Q:如何优化首屏加载?

A:使用代码分割和懒加载:

tsx
import { lazy, Suspense } from 'preact/compat'

const HeavyComponent = lazy(() => import('./HeavyComponent'))

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  )
}

与其他版本对比

特性 Preact Next.js Vue
SSR/SSG ❌ (SPA) ✅ (Nuxt)
状态管理 Signals Zustand Pinia
路由 preact-router App Router Vue Router
构建工具 Vite Next.js Vite
Bundle 大小 ~3KB ~85KB ~33KB
React 兼容 -
学习曲线

相关链接

]]>
<![CDATA[Qwik 版本]]> https://halolight.docs.h7ml.cn/guide/qwik https://halolight.docs.h7ml.cn/guide/qwik Fri, 19 Dec 2025 04:56:41 GMT Qwik 版本

HaloLight Qwik 版本基于 Qwik City 构建,采用 Qwik 可恢复性架构 + TypeScript,实现零水合的极致性能。

在线预览https://halolight-qwik.h7ml.cn

GitHubhttps://github.com/halolight/halolight-qwik

特性

  • 🔄 可恢复性 - 无需水合,服务端状态直接恢复
  • 懒加载一切 - 代码按需加载,首屏 JS 极小 (~1KB)
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 📡 Signals - 细粒度响应式系统
  • 🌐 边缘部署 - 原生支持 Cloudflare Workers 等边缘平台

技术栈

技术 版本 说明
Qwik 2.x 可恢复性框架
Qwik City 2.x 全栈框架
TypeScript 5.x 类型安全
Tailwind CSS 4.x 原子化 CSS
Qwik UI latest UI 组件库
Modular Forms latest 表单处理
Zod 3.x 数据验证
ECharts 5.x 图表可视化
Mock.js 1.x 数据模拟

核心特性

  • 可配置仪表盘 - 9 种小部件,拖拽布局,响应式适配
  • 权限系统 - RBAC 权限控制,路由守卫,权限组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 服务端渲染 - 内置 SSR 支持,SEO 优化
  • 文件路由 - 基于目录的路由系统
  • 实时通知 - WebSocket 推送,通知中心

目录结构

halolight-qwik/
├── src/
│   ├── routes/                    # 文件路由
│   │   ├── index.tsx            # 首页
│   │   ├── layout.tsx           # 根布局
│   │   ├── (auth)/              # 认证路由组
│   │   │   ├── layout.tsx
│   │   │   ├── login/
│   │   │   │   └── index.tsx
│   │   │   ├── register/
│   │   │   ├── forgot-password/
│   │   │   └── reset-password/
│   │   ├── (dashboard)/         # 仪表盘路由组
│   │   │   ├── layout.tsx
│   │   │   ├── dashboard/
│   │   │   │   └── index.tsx
│   │   │   ├── users/
│   │   │   │   ├── index.tsx
│   │   │   │   ├── create/
│   │   │   │   └── [id]/
│   │   │   ├── roles/
│   │   │   ├── permissions/
│   │   │   ├── settings/
│   │   │   └── profile/
│   │   └── api/                 # API 端点
│   │       └── auth/
│   │           └── login/
│   │               └── index.ts
│   ├── components/              # 组件库
│   │   ├── ui/                  # Qwik UI 组件
│   │   ├── layout/              # 布局组件
│   │   │   ├── admin-layout/
│   │   │   ├── auth-layout/
│   │   │   ├── sidebar/
│   │   │   └── header/
│   │   ├── dashboard/           # 仪表盘组件
│   │   │   ├── dashboard-grid/
│   │   │   ├── widget-wrapper/
│   │   │   └── stats-widget/
│   │   └── shared/              # 共享组件
│   │       └── permission-guard/
│   ├── stores/                  # 状态管理
│   │   ├── auth.ts
│   │   ├── ui-settings.ts
│   │   └── dashboard.ts
│   ├── lib/                     # 工具库
│   │   ├── api.ts
│   │   ├── permission.ts
│   │   └── cn.ts
│   ├── mock/                    # Mock 数据
│   └── types/                   # 类型定义
├── public/                      # 静态资源
├── vite.config.ts              # Vite 配置
├── tailwind.config.ts          # Tailwind 配置
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-qwik.git
cd halolight-qwik
pnpm install

环境变量

bash
cp .env.example .env
env
# .env 示例
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:5173

构建生产

bash
pnpm build
pnpm serve

核心功能

状态管理 (Context + Signals)

tsx
// stores/auth.ts
import {
  createContextId,
  useContext,
  useStore,
  useComputed$,
  $,
  type Signal,
} from '@builder.io/qwik'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

interface AuthState {
  user: User | null
  token: string | null
  loading: boolean
}

export const AuthContext = createContextId<AuthState>('auth')

export function useAuth() {
  const state = useContext(AuthContext)

  const isAuthenticated = useComputed$(() => !!state.token && !!state.user)
  const permissions = useComputed$(() => state.user?.permissions ?? [])

  const login = $(async (credentials: { email: string; password: string }) => {
    state.loading = true
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: { 'Content-Type': 'application/json' },
      })
      const data = await response.json()

      state.user = data.user
      state.token = data.token
    } finally {
      state.loading = false
    }
  })

  const logout = $(() => {
    state.user = null
    state.token = null
  })

  const hasPermission = $((permission: string) => {
    const perms = state.user?.permissions ?? []
    return perms.some(p =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  })

  return {
    state,
    isAuthenticated,
    permissions,
    login,
    logout,
    hasPermission,
  }
}

数据获取 (routeLoader$)

tsx
// routes/(dashboard)/users/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'

export const useUsers = routeLoader$(async ({ query, cookie, status }) => {
  const token = cookie.get('token')?.value
  const page = Number(query.get('page')) || 1

  // 权限检查
  const user = await validateToken(token)
  if (!hasPermission(user, 'users:list')) {
    status(403)
    return { error: '无权限访问' }
  }

  const response = await fetch(`/api/users?page=${page}`, {
    headers: { Authorization: `Bearer ${token}` },
  })

  return response.json()
})

export default component$(() => {
  const users = useUsers()

  return (
    <div>
      <h1>用户列表</h1>

      {users.value.error ? (
        <div class="text-destructive">{users.value.error}</div>
      ) : (
        <ul>
          {users.value.data.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  )
})

权限控制

路由守卫

tsx
// routes/(dashboard)/layout.tsx
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
import { useAuth } from '~/stores/auth'
import { AdminLayout } from '~/components/layout/admin-layout'

export const useAuthGuard = routeLoader$(async ({ cookie, redirect, url }) => {
  const token = cookie.get('token')?.value

  if (!token) {
    throw redirect(302, `/login?redirect=${url.pathname}`)
  }

  // 验证 token 并返回用户信息
  return {
    user: await validateToken(token),
  }
})

export default component$(() => {
  const data = useAuthGuard()

  return (
    <AdminLayout user={data.value.user}>
      <Slot />
    </AdminLayout>
  )
})

权限组件

tsx
// components/shared/permission-guard/index.tsx
import { component$, Slot, useComputed$ } from '@builder.io/qwik'
import { useAuth } from '~/stores/auth'

interface Props {
  permission: string
}

export const PermissionGuard = component$<Props>(({ permission }) => {
  const { hasPermission } = useAuth()

  const allowed = useComputed$(async () => {
    return await hasPermission(permission)
  })

  return (
    <>
      {allowed.value ? (
        <Slot />
      ) : (
        <Slot name="fallback" />
      )}
    </>
  )
})

// 使用
<PermissionGuard permission="users:delete">
  <Button variant="destructive" q:slot="">删除</Button>
  <span q:slot="fallback" class="text-muted-foreground">无权限</span>
</PermissionGuard>

表单提交 (routeAction$)

tsx
// routes/(auth)/login/index.tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, zod$, z, Form } from '@builder.io/qwik-city'

export const useLogin = routeAction$(
  async (data, { cookie, redirect, fail }) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' },
      })

      if (!response.ok) {
        return fail(401, { message: '邮箱或密码错误' })
      }

      const result = await response.json()

      cookie.set('token', result.token, {
        path: '/',
        httpOnly: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 24 * 7,
      })

      throw redirect(302, '/dashboard')
    } catch (e) {
      return fail(500, { message: '服务器错误' })
    }
  },
  zod$({
    email: z.string().email('请输入有效邮箱'),
    password: z.string().min(6, '密码至少6位'),
  })
)

export default component$(() => {
  const action = useLogin()

  return (
    <Form action={action}>
      {action.value?.failed && (
        <div class="text-destructive">{action.value.message}</div>
      )}

      <input type="email" name="email" placeholder="邮箱" />
      {action.value?.fieldErrors?.email && (
        <span class="text-destructive">{action.value.fieldErrors.email}</span>
      )}

      <input type="password" name="password" placeholder="密码" />
      {action.value?.fieldErrors?.password && (
        <span class="text-destructive">{action.value.fieldErrors.password}</span>
      )}

      <button type="submit" disabled={action.isRunning}>
        {action.isRunning ? '登录中...' : '登录'}
      </button>
    </Form>
  )
})

API 路由

ts
// routes/api/auth/login/index.ts
import type { RequestHandler } from '@builder.io/qwik-city'

export const onPost: RequestHandler = async ({ json, parseBody }) => {
  const body = await parseBody()
  const { email, password } = body as { email: string; password: string }

  // 验证逻辑
  if (!email || !password) {
    json(400, { success: false, message: '邮箱和密码不能为空' })
    return
  }

  // 认证逻辑...

  json(200, {
    success: true,
    user: { id: 1, name: '管理员', email },
    token: 'mock_token',
  })
}

可拖拽仪表盘

tsx
// components/dashboard/dashboard-grid/index.tsx
import { component$, useSignal, useStore, $ } from '@builder.io/qwik'

interface Widget {
  id: string
  type: string
  x: number
  y: number
  w: number
  h: number
}

export const DashboardGrid = component$(() => {
  const widgets = useStore<Widget[]>([
    { id: '1', type: 'stats', x: 0, y: 0, w: 3, h: 2 },
    { id: '2', type: 'chart', x: 3, y: 0, w: 6, h: 4 },
  ])

  const handleLayoutChange = $((newLayout: Widget[]) => {
    widgets.splice(0, widgets.length, ...newLayout)
  })

  return (
    <div class="dashboard-grid">
      {widgets.map((widget) => (
        <div
          key={widget.id}
          class="widget"
          style={{
            gridColumn: `${widget.x + 1} / span ${widget.w}`,
            gridRow: `${widget.y + 1} / span ${widget.h}`,
          }}
        >
          {/* Widget 内容 */}
        </div>
      ))}
    </div>
  )
})

主题系统

皮肤预设

支持 11 种预设皮肤,通过快捷设置面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿 --primary: 64.6% 0.178 142.49
Rose 玫瑰 --primary: 61.8% 0.238 12.57
Orange 橙色 --primary: 68.3% 0.199 36.35
Yellow 黄色 --primary: 88.1% 0.197 95.45
Violet 紫罗兰 --primary: 57.8% 0.24 305.4
Cyan 青色 --primary: 73.8% 0.139 196.85
Pink 粉色 --primary: 72.2% 0.218 345.82
Lime 青柠 --primary: 79.2% 0.183 123.7
Amber 琥珀 --primary: 82.5% 0.157 62.24

CSS 变量 (OKLch)

css
/* 全局变量定义 */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 98% 0 0;
  --secondary: 96.1% 0 0;
  --secondary-foreground: 9.8% 0 0;
  --muted: 95.1% 0.01 286.38;
  --muted-foreground: 45.1% 0.009 285.88;
  --accent: 95.1% 0.01 286.38;
  --accent-foreground: 9.8% 0 0;
  --destructive: 54.3% 0.227 25.78;
  --destructive-foreground: 98% 0 0;
  --border: 89.8% 0.006 286.32;
  --input: 89.8% 0.006 286.32;
  --ring: 51.1% 0.262 276.97;
  --radius: 0.5rem;
}

.dark {
  --background: 0% 0 0;
  --foreground: 98% 0 0;
  --primary: 59.6% 0.262 276.97;
  --primary-foreground: 14.9% 0.017 285.75;
  /* ... */
}

页面路由

路径 页面 权限
/ 首页 公开
/login 登录 公开
/register 注册 公开
/forgot-password 忘记密码 公开
/reset-password 重置密码 公开
/dashboard 仪表盘 dashboard:view
/users 用户列表 users:list
/users/create 创建用户 users:create
/users/[id] 用户详情 users:view
/roles 角色管理 roles:list
/permissions 权限管理 permissions:list
/settings 系统设置 settings:view
/profile 个人中心 登录即可

常用命令

bash
pnpm dev            # 启动开发服务器
pnpm build          # 生产构建
pnpm serve          # 预览生产构建
pnpm lint           # 代码检查
pnpm lint:fix       # 自动修复
pnpm type-check     # 类型检查
pnpm test           # 运行测试
pnpm test:e2e       # E2E 测试

部署

Vercel (推荐)

Deploy with Vercel

bash
# 使用 Vercel Edge 适配器
pnpm add -D @builder.io/qwik-city/adapters/vercel-edge

Docker

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server ./server
COPY --from=builder /app/package.json .
RUN npm install --production
EXPOSE 3000
CMD ["node", "server/entry.express.js"]

其他平台

  • Node.js 服务器

    bash
    pnpm build
    node server/entry.express.js
  • Cloudflare Pages

    bash
    # 使用 Cloudflare Pages 适配器
    pnpm add -D @builder.io/qwik-city/adapters/cloudflare-pages
  • Netlify

  • AWS Amplify

  • Azure Static Web Apps

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

测试

bash
pnpm test           # 运行测试
pnpm test:e2e       # E2E 测试
pnpm test:coverage  # 覆盖率报告

测试示例

tsx
// src/components/permission-guard/permission-guard.spec.tsx
import { describe, it, expect } from 'vitest'
import { createDOM } from '@builder.io/qwik/testing'
import { PermissionGuard } from './permission-guard'

describe('PermissionGuard', () => {
  it('应当在有权限时渲染内容', async () => {
    const { screen, render } = await createDOM()

    await render(
      <PermissionGuard permission="users:view">
        <div>Protected Content</div>
      </PermissionGuard>
    )

    expect(screen.innerHTML).toContain('Protected Content')
  })

  it('应当在无权限时渲染回退内容', async () => {
    const { screen, render } = await createDOM()

    await render(
      <PermissionGuard permission="admin:*">
        <div>Protected Content</div>
        <div q:slot="fallback">No Permission</div>
      </PermissionGuard>
    )

    expect(screen.innerHTML).toContain('No Permission')
  })
})

配置

Vite 配置

ts
// vite.config.ts
import { defineConfig } from 'vite'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { qwikCity } from '@builder.io/qwik-city/vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig(() => {
  return {
    plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
    preview: {
      headers: {
        'Cache-Control': 'public, max-age=600',
      },
    },
  }
})

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

Qwik 可恢复性原理

Qwik 的核心创新是 “可恢复性” 而非 “水合”:

tsx
// 传统框架(React/Vue):需要重新执行所有代码来重建状态
// Qwik:直接从 HTML 恢复状态,无需重新执行

// 服务端序列化状态
export default component$(() => {
  const count = useSignal(0)

  // Qwik 会将状态序列化到 HTML 中
  return <div>Count: {count.value}</div>
})

// 客户端直接从 HTML 恢复状态,不需要执行组件代码
// 只有在交互时才按需加载和执行代码

懒加载策略

Qwik 实现了最激进的代码分割:

tsx
// 每个事件处理器都是独立的懒加载单元
export default component$(() => {
  const count = useSignal(0)

  // 点击前这个函数不会被下载
  const handleClick = $(() => {
    count.value++
  })

  return (
    <button onClick$={handleClick}>
      Count: {count.value}
    </button>
  )
})

预加载优化

tsx
// routes/(dashboard)/layout.tsx
import { component$ } from '@builder.io/qwik'
import { routeLoader$, Link } from '@builder.io/qwik-city'

// 预加载数据
export const usePreloadData = routeLoader$(async () => {
  return {
    navigation: await fetchNavigation(),
  }
})

export default component$(() => {
  const data = usePreloadData()

  return (
    <nav>
      {data.value.navigation.map((item) => (
        // Link 会自动预加载目标页面
        <Link href={item.path} prefetch>
          {item.title}
        </Link>
      ))}
    </nav>
  )
})

性能优化

图片优化

tsx
import { component$ } from '@builder.io/qwik'
import { Image } from '@unpic/qwik'

export default component$(() => {
  return (
    <Image
      src="https://example.com/image.jpg"
      layout="constrained"
      width={800}
      height={600}
      alt="优化的图片"
    />
  )
})

懒加载组件

tsx
// 组件级别的懒加载
import { component$ } from '@builder.io/qwik'

export default component$(() => {
  return (
    <div>
      {/* 使用 resource$ 实现组件懒加载 */}
      <Resource
        value={heavyComponentResource}
        onPending={() => <div>加载中...</div>}
        onResolved={(HeavyComponent) => <HeavyComponent />}
      />
    </div>
  )
})

预加载关键资源

tsx
// routes/layout.tsx
import { component$, useVisibleTask$ } from '@builder.io/qwik'

export default component$(() => {
  useVisibleTask$(() => {
    // 预加载关键字体
    const link = document.createElement('link')
    link.rel = 'preload'
    link.as = 'font'
    link.href = '/fonts/main.woff2'
    link.type = 'font/woff2'
    link.crossOrigin = 'anonymous'
    document.head.appendChild(link)
  })

  return <Slot />
})

常见问题

Q:如何处理需要客户端状态的场景?

A:使用 useSignaluseStore 创建响应式状态:

tsx
import { component$, useSignal, useStore } from '@builder.io/qwik'

export default component$(() => {
  // 简单值使用 useSignal
  const count = useSignal(0)

  // 复杂对象使用 useStore
  const state = useStore({
    user: null,
    loading: false,
  })

  return (
    <div>
      <p>Count: {count.value}</p>
      <button onClick$={() => count.value++}>Increment</button>
    </div>
  )
})

Q:如何与第三方库集成?

A:使用 useVisibleTask$ 在客户端执行代码:

tsx
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'

export default component$(() => {
  const chartRef = useSignal<Element>()

  useVisibleTask$(({ cleanup }) => {
    // 在客户端初始化第三方库
    import('chart.js').then(({ Chart }) => {
      const chart = new Chart(chartRef.value, {
        // 配置...
      })

      cleanup(() => chart.destroy())
    })
  })

  return <canvas ref={chartRef} />
})

Q:如何优化首屏加载时间?

A:Qwik 自动优化,但你可以进一步:

  1. 使用 SSR:默认启用
  2. 预加载关键路由
    tsx
    <Link href="/dashboard" prefetch>Dashboard</Link>
  3. 延迟非关键资源
    tsx
    useVisibleTask$(({ track }) => {
      // 只在组件可见时加载
      track(() => isVisible.value)
      if (isVisible.value) {
        loadAnalytics()
      }
    })

Q:如何处理表单提交?

A:使用 routeAction$ 实现服务端处理:

tsx
import { component$ } from '@builder.io/qwik'
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'

export const useAddUser = routeAction$(
  async (data) => {
    // 服务端处理
    const user = await createUser(data)
    return { success: true, user }
  },
  zod$({
    name: z.string().min(2),
    email: z.string().email(),
  })
)

export default component$(() => {
  const action = useAddUser()

  return (
    <Form action={action}>
      <input name="name" />
      <input name="email" type="email" />
      <button type="submit">
        {action.isRunning ? '提交中...' : '提交'}
      </button>
    </Form>
  )
})

与其他版本对比

特性 Qwik 版本 Next.js Vue
SSR/SSG ✅ 内置 ✅ (Nuxt)
状态管理 Context + Signals Zustand Pinia
数据获取 routeLoader$ TanStack Query TanStack Query
表单验证 Modular Forms + Zod React Hook Form + Zod VeeValidate + Zod
路由 文件路由 App Router Vue Router
构建工具 Vite Next.js Vite
水合 可恢复(零水合) 传统水合 传统水合
首屏 JS ~1KB ~85KB ~33KB
服务端 内置全栈 API Routes 独立后端
组件库 Qwik UI shadcn/ui shadcn-vue

相关链接

]]>
<![CDATA[Railway 部署]]> https://halolight.docs.h7ml.cn/guide/railway https://halolight.docs.h7ml.cn/guide/railway Fri, 19 Dec 2025 04:56:41 GMT Railway 部署

HaloLight Railway 部署版本,针对 Railway 平台优化的一键部署方案。

在线预览https://halolight-railway.h7ml.cn

GitHubhttps://github.com/halolight/halolight-railway

特性

  • 🚂 一键部署 - 模板化快速部署,30 秒上线
  • 📈 自动扩缩容 - 按需自动扩展,零停机部署
  • 🐘 PostgreSQL - 一键添加托管数据库
  • 🔴 Redis - 内置缓存服务支持
  • 🌐 自定义域名 - 免费 HTTPS,自动续期
  • ⚙️ 环境变量 - 便捷的配置管理,支持引用
  • 📊 监控面板 - 实时资源监控,日志聚合
  • 🔄 自动部署 - Git push 触发自动部署

快速开始

方式一:一键部署 (推荐)

Deploy on Railway

点击按钮后:

  1. 登录 Railway 账号
  2. 选择 GitHub 仓库
  3. 配置环境变量
  4. 自动部署完成

方式二:CLI 部署

bash
# 安装 Railway CLI
npm install -g @railway/cli

# 登录 Railway
railway login

# 克隆项目
git clone https://github.com/halolight/halolight-railway.git
cd halolight-railway

# 初始化 Railway 项目
railway init

# 关联到已有项目 (可选)
railway link

# 部署
railway up

方式三:GitHub 集成

  1. Fork halolight-railway 仓库
  2. 在 Railway 控制台选择 “Deploy from GitHub repo”
  3. 选择你的 Fork 仓库
  4. 配置环境变量并部署

配置文件

railway.json

json
{
  "$schema": "https://railway.app/railway.schema.json",
  "build": {
    "builder": "NIXPACKS",
    "buildCommand": "pnpm install && pnpm build"
  },
  "deploy": {
    "startCommand": "pnpm start",
    "healthcheckPath": "/api/health",
    "healthcheckTimeout": 300,
    "restartPolicyType": "ON_FAILURE",
    "restartPolicyMaxRetries": 10,
    "numReplicas": 1
  }
}

nixpacks.toml (可选)

toml
[phases.setup]
nixPkgs = ["nodejs_20", "pnpm"]

[phases.install]
cmds = ["pnpm install --frozen-lockfile"]

[phases.build]
cmds = ["pnpm build"]

[start]
cmd = "pnpm start"

环境变量

应用配置

变量名 说明 示例
NODE_ENV 运行环境 production
PORT 服务端口 3000 (Railway 自动设置)
NEXT_PUBLIC_API_URL API 基础 URL /api
NEXT_PUBLIC_MOCK 启用 Mock 数据 false
NEXT_PUBLIC_APP_TITLE 应用标题 Admin Pro

Railway 变量引用

Railway 支持在环境变量中引用其他服务:

bash
# 引用自动生成的域名
NEXT_PUBLIC_API_URL=https://${{RAILWAY_PUBLIC_DOMAIN}}/api

# 引用 PostgreSQL 服务
DATABASE_URL=${{Postgres.DATABASE_URL}}

# 引用 Redis 服务
REDIS_URL=${{Redis.REDIS_URL}}

# 引用项目变量
JWT_SECRET=${{shared.JWT_SECRET}}

添加服务

PostgreSQL 数据库

bash
# CLI 方式
railway add --database postgres

# 或在控制台
# 1. 点击 "New Service"
# 2. 选择 "Database" → "PostgreSQL"
# 3. 自动生成 DATABASE_URL

生成的环境变量:

  • DATABASE_URL - 完整连接字符串
  • PGHOST - 主机地址
  • PGPORT - 端口
  • PGUSER - 用户名
  • PGPASSWORD - 密码
  • PGDATABASE - 数据库名

Redis 缓存

bash
# CLI 方式
railway add --database redis

# 或在控制台
# 1. 点击 "New Service"
# 2. 选择 "Database" → "Redis"
# 3. 自动生成 REDIS_URL

生成的环境变量:

  • REDIS_URL - 完整连接字符串
  • REDISHOST - 主机地址
  • REDISPORT - 端口
  • REDISPASSWORD - 密码

自定义域名

添加域名

  1. 在服务设置中点击 “Settings”
  2. 找到 “Domains” 部分
  3. 点击 “Generate Domain” (免费 Railway 域名)
  4. 或点击 “Add Custom Domain” (自定义域名)

DNS 配置

类型: CNAME
名称: your-subdomain
值: <your-app>.up.railway.app

HTTPS

Railway 自动为所有域名配置 HTTPS:

  • 自动申请 Let's Encrypt 证书
  • 自动续期
  • 强制 HTTPS 重定向

常用命令

bash
# 登录
railway login

# 查看状态
railway status

# 部署
railway up

# 查看日志
railway logs

# 打开控制台
railway open

# 运行远程命令
railway run <command>

# 连接数据库
railway connect postgres

# 环境变量
railway variables
railway variables set KEY=value

监控与日志

实时日志

bash
# CLI 查看日志
railway logs -f

# 或在控制台
# Service → Deployments → 点击部署 → View Logs

资源监控

Railway 控制台提供:

  • CPU 使用率
  • 内存使用量
  • 网络流量
  • 请求数/响应时间
  • 错误率

告警设置

  1. 进入项目设置
  2. 配置 Webhook 通知
  3. 支持 Slack、Discord、Email

扩缩容

手动扩容

json
// railway.json
{
  "deploy": {
    "numReplicas": 3
  }
}

自动扩容 (Pro 计划)

Railway Pro 支持基于指标的自动扩容:

  • CPU 阈值
  • 内存阈值
  • 请求队列深度

费用说明

计划 价格 特性
Hobby $5/月 500 小时执行时间,1GB 内存
Pro $20/月起 无限执行时间,更多资源
Enterprise 联系销售 专属支持,SLA 保障

常见问题

Q:部署失败怎么办?

A:检查以下几点:

  1. 查看构建日志,确认依赖安装正确
  2. 确认 pnpm-lock.yaml 已提交
  3. 检查环境变量是否配置正确
  4. 确认 start 命令正确

Q:如何回滚部署?

A:在 Deployments 页面:

  1. 找到之前的成功部署
  2. 点击 “Redeploy”
  3. 或使用 CLI:railway rollback

Q:如何配置私有网络?

A:Railway 服务间通过内部网络通信:

bash
# 使用内部 DNS
DATABASE_URL=postgres://user:[email protected]:5432/db

与其他平台对比

特性 Railway Vercel Fly.io
一键部署 ⚠️ 需 CLI
托管数据库 ✅ 内置 ❌ 需外部 ✅ 内置
免费额度 $5/月信用 100GB 3 个共享 VM
自动扩容 ✅ Pro
私有网络 ⚠️ 有限

相关链接

]]>
<![CDATA[React 版本]]> https://halolight.docs.h7ml.cn/guide/react https://halolight.docs.h7ml.cn/guide/react Fri, 19 Dec 2025 04:56:41 GMT React 版本

HaloLight React 版本基于 React 19 + Vite 6 构建,是一个纯客户端渲染 (CSR) 的单页应用 (SPA)。

在线预览https://halolight-react.h7ml.cn/

GitHubhttps://github.com/halolight/halolight-react

特性

  • 🏗️ React 19 - 最新的 React 特性和性能优化
  • Vite 6 - 极速冷启动与 HMR
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 📑 多标签页 - 浏览器式标签管理
  • 命令面板 - 快捷键导航 (⌘K)

技术栈

技术 版本 说明
React 19.x UI 框架
Vite 6.x 构建工具
TypeScript 5.x 类型安全
React Router 6.x 客户端路由
Zustand 5.x 状态管理
TanStack Query 5.x 服务端状态
React Hook Form 7.x 表单处理
Zod 4.x 数据验证
Tailwind CSS 4.x 原子化 CSS
shadcn/ui latest UI 组件库
react-grid-layout 1.5.x 拖拽布局
Recharts 3.x 图表可视化
Framer Motion 12.x 动画效果
Mock.js 1.x 数据模拟

核心特性

  • 可配置仪表盘 - 9 种小部件,拖拽布局,响应式适配
  • 多标签导航 - 浏览器式标签,右键菜单,状态缓存
  • 权限系统 - RBAC 权限控制,路由守卫,权限组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 多账户切换 - 快速切换账户,记住登录状态
  • 命令面板 - 键盘快捷键 (⌘K),全局搜索
  • 实时通知 - WebSocket 推送,通知中心

目录结构

halolight-react/
├── src/
│   ├── pages/                     # 页面组件
│   │   ├── auth/                  # 认证页面
│   │   │   ├── login/
│   │   │   ├── register/
│   │   │   ├── forgot-password/
│   │   │   └── reset-password/
│   │   ├── dashboard/             # 仪表盘
│   │   └── legal/                 # 法律条款
│   ├── components/
│   │   ├── ui/                    # shadcn/ui 组件 (20+)
│   │   ├── layout/                # 布局组件
│   │   │   ├── admin-layout.tsx
│   │   │   ├── auth-layout.tsx
│   │   │   ├── sidebar.tsx
│   │   │   ├── header.tsx
│   │   │   └── footer.tsx
│   │   ├── dashboard/             # 仪表盘组件
│   │   │   ├── configurable-dashboard.tsx
│   │   │   ├── widget-wrapper.tsx
│   │   │   ├── stats-widget.tsx
│   │   │   ├── chart-widget.tsx
│   │   │   └── ...
│   │   └── shared/                # 共享组件
│   ├── hooks/                     # 自定义 Hooks
│   │   ├── use-users.ts
│   │   ├── use-auth.ts
│   │   ├── use-theme.ts
│   │   └── ...
│   ├── stores/                    # Zustand Stores
│   │   ├── auth.ts
│   │   ├── ui-settings.ts
│   │   ├── dashboard-layout.ts
│   │   └── tabs.ts
│   ├── lib/
│   │   ├── api/                   # API 服务
│   │   ├── auth/                  # 认证逻辑
│   │   ├── validations/           # Zod schemas
│   │   └── utils.ts               # 工具函数
│   ├── routes/                    # 路由配置
│   │   └── index.tsx
│   ├── config/                    # 配置文件
│   │   ├── routes.ts
│   │   └── tdk.ts
│   ├── types/                     # 类型定义
│   ├── mock/                      # Mock 数据
│   ├── providers/                 # Context Providers
│   ├── App.tsx
│   └── main.tsx
├── public/                        # 静态资源
├── vite.config.ts
├── tsconfig.json
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-react.git
cd halolight-react
pnpm install

环境变量

bash
cp .env.example .env.development
env
# .env.development 示例
VITE_API_URL=/api
VITE_MOCK=true
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=true

启动开发

bash
pnpm dev

访问 http://localhost:5173

构建生产

bash
pnpm build
pnpm preview

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

核心功能

状态管理 (Zustand)

tsx
// stores/auth.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface AuthState {
  user: User | null
  token: string | null
  isAuthenticated: boolean
  login: (credentials: LoginCredentials) => Promise<void>
  logout: () => void
  hasPermission: (permission: string) => boolean
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: async (credentials) => {
        const response = await authApi.login(credentials)
        set({
          user: response.user,
          token: response.token,
          isAuthenticated: true,
        })
      },

      logout: () => {
        set({ user: null, token: null, isAuthenticated: false })
      },

      hasPermission: (permission) => {
        const { user } = get()
        if (!user) return false
        return user.permissions.some(p =>
          p === '*' || p === permission ||
          (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
        )
      },
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({ token: state.token, user: state.user }),
    }
  )
)

数据获取 (TanStack Query)

tsx
// hooks/use-users.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { usersApi } from '@/lib/api'

export function useUsers(params?: UserQueryParams) {
  return useQuery({
    queryKey: ['users', params],
    queryFn: () => usersApi.getList(params),
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: usersApi.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}

// 在组件中使用
function UsersPage() {
  const { data: users, isLoading, error } = useUsers()

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return <div>{/* 渲染用户列表 */}</div>
}

权限控制

tsx
// hooks/use-permission.ts
import { useAuthStore } from '@/stores/auth'

export function usePermission(permission: string): boolean {
  const hasPermission = useAuthStore((state) => state.hasPermission)
  return hasPermission(permission)
}

export function usePermissions(permissions: string[]): boolean {
  const hasPermission = useAuthStore((state) => state.hasPermission)
  return permissions.every(p => hasPermission(p))
}
tsx
// 使用
function DeleteButton() {
  const canDelete = usePermission('users:delete')

  if (!canDelete) return null

  return <Button variant="destructive">删除</Button>
}
tsx
// components/permission-guard.tsx
import { usePermission } from '@/hooks/use-permission'

interface PermissionGuardProps {
  permission: string
  children: React.ReactNode
  fallback?: React.ReactNode
}

export function PermissionGuard({
  permission,
  children,
  fallback = null,
}: PermissionGuardProps) {
  const hasPermission = usePermission(permission)

  if (!hasPermission) return fallback

  return <>{children}</>
}
tsx
<!-- 使用 -->
<PermissionGuard permission="users:delete" fallback={<span>无权限</span>}>
  <DeleteButton />
</PermissionGuard>

可拖拽仪表盘

tsx
// components/dashboard/configurable-dashboard.tsx
import GridLayout from 'react-grid-layout'
import { useDashboardStore } from '@/stores/dashboard-layout'

export function ConfigurableDashboard() {
  const { layout, setLayout, isEditing } = useDashboardStore()

  return (
    <GridLayout
      layout={layout}
      onLayoutChange={setLayout}
      cols={12}
      rowHeight={80}
      isDraggable={isEditing}
      isResizable={isEditing}
      margin={[16, 16]}
    >
      {layout.map((item) => (
        <div key={item.i}>
          <WidgetWrapper widget={getWidget(item.i)} />
        </div>
      ))}
    </GridLayout>
  )
}

主题系统

皮肤预设

支持 11 种预设皮肤,通过快捷设置面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿 --primary: 64.6% 0.178 142.49
Orange 橙色 --primary: 65.7% 0.198 45.13
Rose 玫红 --primary: 58.9% 0.238 11.26
Cyan 青色 --primary: 75.6% 0.146 191.68
Yellow 黄色 --primary: 85.1% 0.184 98.08
Violet 紫罗兰 --primary: 55.3% 0.264 293.49
Slate 石板灰 --primary: 47.9% 0.017 256.71
Zinc 锌灰 --primary: 48.3% 0 0
Neutral 中性灰 --primary: 48.5% 0 0

CSS 变量 (OKLch)

css
/* 示例变量定义 */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 96.1% 0.004 286.41;
  --secondary-foreground: 14.9% 0.017 285.75;
  --muted: 96.1% 0.004 286.41;
  --muted-foreground: 45.8% 0.009 285.77;
  --accent: 96.1% 0.004 286.41;
  --accent-foreground: 14.9% 0.017 285.75;
  --destructive: 59.3% 0.246 27.33;
  --destructive-foreground: 100% 0 0;
  --border: 89.8% 0.006 286.32;
  --input: 89.8% 0.006 286.32;
  --ring: 51.1% 0.262 276.97;
  --radius: 0.5rem;
}

.dark {
  --background: 0% 0 0;
  --foreground: 98.3% 0 0;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  /* ... */
}

页面路由

路径 页面 权限
/ 重定向到 /dashboard -
/login 登录 公开
/register 注册 公开
/forgot-password 忘记密码 公开
/reset-password 重置密码 公开
/dashboard 仪表盘 dashboard:view
/users 用户列表 users:list
/users/create 创建用户 users:create
/users/:id 用户详情 users:view
/users/:id/edit 编辑用户 users:update
/roles 角色管理 roles:list
/permissions 权限管理 permissions:list
/settings 系统设置 settings:view
/profile 个人中心 登录即可

环境变量

配置示例

bash
cp .env.example .env.development
env
# .env.development 示例
VITE_API_URL=/api
VITE_MOCK=true
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=true

变量说明

变量名 说明 默认值
VITE_API_URL API 基础路径 /api
VITE_MOCK 是否启用 Mock 数据 true
VITE_APP_TITLE 应用标题 Admin Pro
VITE_BRAND_NAME 品牌名称 Halolight
VITE_DEMO_EMAIL 演示账号邮箱 [email protected]
VITE_DEMO_PASSWORD 演示账号密码 123456
VITE_SHOW_DEMO_HINT 是否显示演示提示 true

使用方式

tsx
// 在代码中访问环境变量
const apiUrl = import.meta.env.VITE_API_URL
const isMock = import.meta.env.VITE_MOCK === 'true'

常用命令

bash
pnpm dev            # 启动开发服务器
pnpm build          # 生产构建
pnpm preview        # 预览生产构建
pnpm lint           # 代码检查
pnpm lint:fix       # 自动修复
pnpm type-check     # 类型检查
pnpm test           # 运行测试
pnpm test:coverage  # 测试覆盖率

测试

bash
pnpm test           # 运行测试(watch 模式)
pnpm test:run       # 单次运行
pnpm test:coverage  # 覆盖率报告
pnpm test:ui        # Vitest UI 界面

测试示例

tsx
// __tests__/components/Button.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from '@/components/ui/button'

describe('Button', () => {
  it('renders button with text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button')).toHaveTextContent('Click me')
  })

  it('handles click events', async () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)

    await userEvent.click(screen.getByRole('button'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('disables button when disabled prop is true', () => {
    render(<Button disabled>Click me</Button>)
    expect(screen.getByRole('button')).toBeDisabled()
  })
})

配置

Vite 配置

ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: 'dist',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
          'chart-vendor': ['recharts'],
        },
      },
    },
  },
})

部署

Vercel (推荐)

Deploy with Vercel

bash
vercel

Docker

dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
bash
docker build -t halolight-react .
docker run -p 3000:80 halolight-react

其他平台

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

PWA 支持

项目内置 PWA 支持,包括:

  • Service Worker 注册
  • 离线缓存
  • 应用清单 (manifest.json)
  • 多尺寸图标
json
// public/manifest.json
{
  "name": "Admin Pro",
  "short_name": "Admin",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

React Router 配置

tsx
// routes/index.tsx
import { createBrowserRouter, Navigate } from 'react-router-dom'
import { DashboardLayout } from '@/layouts/dashboard-layout'
import { AuthLayout } from '@/layouts/auth-layout'

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Navigate to="/dashboard" replace />,
  },
  {
    path: '/login',
    element: <AuthLayout />,
    children: [
      { index: true, element: <LoginPage /> },
    ],
  },
  {
    path: '/',
    element: <DashboardLayout />,
    children: [
      { path: 'dashboard', element: <HomePage /> },
      { path: 'users', element: <UsersPage /> },
      { path: 'settings', element: <SettingsPage /> },
      // 更多路由...
    ],
  },
])

路由守卫

tsx
// components/auth-guard.tsx
import { Navigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/stores/auth'

interface AuthGuardProps {
  children: React.ReactNode
  permission?: string
}

export function AuthGuard({ children, permission }: AuthGuardProps) {
  const location = useLocation()
  const { isAuthenticated, hasPermission } = useAuthStore()

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />
  }

  if (permission && !hasPermission(permission)) {
    return <Navigate to="/403" replace />
  }

  return <>{children}</>
}

性能优化

图片优化

tsx
// 使用 lazy 加载图片
import { useState } from 'react'

function LazyImage({ src, alt }: { src: string; alt: string }) {
  const [loaded, setLoaded] = useState(false)

  return (
    <div className="relative">
      {!loaded && <div className="skeleton" />}
      <img
        src={src}
        alt={alt}
        loading="lazy"
        onLoad={() => setLoaded(true)}
        className={loaded ? 'opacity-100' : 'opacity-0'}
      />
    </div>
  )
}

懒加载组件

tsx
// 路由级别代码分割
import { lazy, Suspense } from 'react'

const Dashboard = lazy(() => import('@/pages/dashboard'))
const Users = lazy(() => import('@/pages/users'))

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/users" element={<Users />} />
      </Routes>
    </Suspense>
  )
}

预加载

tsx
// 鼠标悬停时预加载组件
import { lazy } from 'react'

const UserDetails = lazy(() => import('@/pages/user-details'))

function UserList() {
  const preloadUserDetails = () => {
    // 触发预加载
    import('@/pages/user-details')
  }

  return (
    <Link
      to="/users/1"
      onMouseEnter={preloadUserDetails}
    >
      查看详情
    </Link>
  )
}

Memo 优化

tsx
import { memo } from 'react'

// 防止不必要的重渲染
const ExpensiveComponent = memo(({ data }: { data: any }) => {
  return <div>{/* 复杂渲染逻辑 */}</div>
})

常见问题

Q:如何添加新的路由?

A:在 src/routes/index.tsx 中添加路由配置:

tsx
{
  path: '/new-page',
  element: <NewPage />,
}

Q:如何自定义主题颜色?

A:修改 CSS 变量或使用主题切换功能:

css
:root {
  --primary: 51.1% 0.262 276.97; /* 修改主色调 */
}

Q:如何集成真实 API?

A:将 VITE_MOCK 设置为 false,并配置 VITE_API_URL

env
VITE_MOCK=false
VITE_API_URL=https://api.example.com

Q:如何添加新的权限?

A:在用户的 permissions 数组中添加权限字符串,并使用 usePermission Hook:

tsx
const canEdit = usePermission('users:edit')

与其他版本对比

特性 React 版本 Next.js Vue
SSR/SSG ✅ (Nuxt)
状态管理 Zustand Zustand Pinia
路由 React Router App Router Vue Router
构建工具 Vite Next.js Vite

相关链接

]]>
<![CDATA[Remix 版本]]> https://halolight.docs.h7ml.cn/guide/remix https://halolight.docs.h7ml.cn/guide/remix Fri, 19 Dec 2025 04:56:41 GMT Remix 版本

HaloLight Remix 版本基于 React Router 7 构建 (原 Remix 团队已合并至 React Router),采用 TypeScript + Web 标准优先的全栈开发体验。

在线预览https://halolight-remix.h7ml.cn/

GitHubhttps://github.com/halolight/halolight-remix

特性

  • 🌐 Web 标准优先 - 基于 Fetch API、FormData、Response 等原生 API
  • 🔄 Loader/Action - 优雅的服务端数据模式,渐进增强
  • 📁 文件路由 - 直观的嵌套路由和布局系统
  • 渐进增强 - 无 JS 也能工作的表单提交
  • 🎯 类型安全 - 自动生成的路由类型 (+types/)
  • 🎨 主题系统 - 11 种皮肤预设 + OKLch 色彩空间
  • 📑 多标签页 - 标签栏 + 右键菜单管理
  • 🚀 Vite 驱动 - 极速 HMR 热更新
  • 🌍 边缘部署 - Cloudflare Pages 一键部署
  • 📊 数据可视化 - Recharts 图表集成
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 🛡️ 权限控制 - RBAC 细粒度权限管理

技术栈

技术 版本 说明
React Router 7.x 全栈路由框架 (原 Remix)
React 19.x UI 框架
TypeScript 5.9 类型安全
Vite 7.x 构建工具
Tailwind CSS 4.x 原子化 CSS + OKLch
Radix UI latest 无障碍 UI 原语
Zustand 5.x 轻量状态管理
Recharts 3.x 图表可视化
Vitest 4.x 单元测试
Cloudflare Pages - 边缘部署

核心特性

  • Web 标准优先 - 基于 Fetch API、FormData、Response 等原生 API
  • Loader/Action 模式 - 优雅的服务端数据加载和表单处理
  • 文件系统路由 - 直观的嵌套路由和布局系统
  • 渐进增强 - 无 JavaScript 也能工作的表单提交
  • 类型安全 - 自动生成的路由类型定义 (+types/)
  • 主题系统 - 11 种皮肤预设 + OKLch 色彩空间 + 明暗模式
  • 多标签页管理 - 标签栏 + 右键菜单 + 状态持久化

目录结构

halolight-remix/
├── app/
│   ├── routes/                    # 文件路由
│   │   ├── _index.tsx            # 首页 (仪表盘)
│   │   ├── login.tsx             # 登录
│   │   ├── register.tsx          # 注册
│   │   ├── forgot-password.tsx   # 忘记密码
│   │   ├── reset-password.tsx    # 重置密码
│   │   ├── users.tsx             # 用户管理
│   │   ├── users.$id.tsx         # 用户详情 (动态路由)
│   │   ├── settings.tsx          # 系统设置
│   │   ├── profile.tsx           # 个人中心
│   │   ├── security.tsx          # 安全设置
│   │   ├── analytics.tsx         # 数据分析
│   │   ├── notifications.tsx     # 通知中心
│   │   ├── documents.tsx         # 文档管理
│   │   ├── calendar.tsx          # 日历
│   │   ├── api.users.ts          # API 端点
│   │   ├── api.auth.login.ts     # 登录 API
│   │   ├── api.auth.logout.ts    # 登出 API
│   │   └── +types/               # 自动生成类型
│   ├── components/               # 组件库
│   │   ├── ui/                   # 基础 UI 组件
│   │   │   ├── button.tsx
│   │   │   ├── card.tsx
│   │   │   ├── dialog.tsx
│   │   │   ├── dropdown-menu.tsx
│   │   │   ├── input.tsx
│   │   │   ├── select.tsx
│   │   │   ├── table.tsx
│   │   │   ├── toast.tsx
│   │   │   └── ...
│   │   ├── layout/               # 布局组件
│   │   │   ├── header.tsx
│   │   │   ├── sidebar.tsx
│   │   │   ├── footer.tsx
│   │   │   ├── tab-bar.tsx
│   │   │   └── quick-settings.tsx
│   │   ├── auth/                 # 认证组件
│   │   │   └── auth-shell.tsx
│   │   ├── dashboard/            # 仪表盘组件
│   │   │   ├── stats-card.tsx
│   │   │   └── chart-widget.tsx
│   │   ├── admin-layout.tsx      # 后台布局
│   │   └── theme-provider.tsx    # 主题提供者
│   ├── hooks/                    # React Hooks
│   │   ├── use-chart-palette.ts
│   │   ├── use-toast.ts
│   │   └── use-media-query.ts
│   ├── lib/                      # 工具库
│   │   ├── utils.ts              # cn() 类名工具
│   │   ├── meta.ts               # TDK 元信息
│   │   ├── session.server.ts     # 会话管理
│   │   ├── auth.server.ts        # 认证逻辑
│   │   └── project-info.ts       # 项目信息
│   ├── stores/                   # Zustand 状态
│   │   ├── tabs-store.ts         # 标签页状态
│   │   └── ui-settings-store.ts  # UI 设置状态
│   ├── types/                    # TypeScript 类型
│   │   ├── user.ts
│   │   └── api.ts
│   ├── root.tsx                  # 根组件
│   ├── routes.ts                 # 路由配置
│   └── app.css                   # 全局样式
├── tests/                        # 测试文件
│   ├── setup.ts
│   ├── lib/
│   ├── stores/
│   └── components/
├── public/                       # 静态资源
├── .github/workflows/ci.yml      # CI 配置
├── wrangler.json                 # Cloudflare 配置
├── vitest.config.ts              # Vitest 配置
├── eslint.config.js              # ESLint 配置
├── vite.config.ts                # Vite 配置
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-remix.git
cd halolight-remix
pnpm install

环境变量

bash
cp .env.example .env
bash
# .env 示例
SESSION_SECRET=your-super-secret-session-key
API_BASE_URL=https://api.halolight.h7ml.cn
MOCK_ENABLED=true
DEMO_EMAIL=[email protected]
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=true
APP_TITLE=Admin Pro
BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:5173

构建生产

bash
pnpm build
pnpm start

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

核心功能

Loader/Action 数据模式

路由文件约定

React Router 7 使用文件系统路由,文件名决定 URL 路径:

app/routes/
├── _index.tsx            → /              (索引路由)
├── about.tsx             → /about         (静态路由)
├── users.tsx             → /users         (静态路由)
├── users.$id.tsx         → /users/:id     (动态路由)
├── users.$id_.edit.tsx   → /users/:id/edit (嵌套路由)
├── _layout.tsx           → 布局路由 (无 URL 段)
├── _layout.dashboard.tsx → /dashboard      (带布局)
├── $.tsx                 → /*             (通配符路由)
├── api.users.ts          → /api/users     (资源路由)
└── [...slug].tsx         → /* 可选捕获

特殊文件约定

文件名 说明
_index.tsx 索引路由,匹配父路由精确路径
_layout.tsx 无路径布局,子路由共享布局
$param.tsx 动态路由参数
$.tsx 通配符路由,捕获所有子路径
api.*.ts 资源路由(仅 loader/action,无 UI)
+types/ 自动生成的类型定义

Loader (数据加载)

Loader 在服务端执行,用于页面数据获取:

tsx
// app/routes/users.tsx
import type { Route } from "./+types/users";

// 服务端数据加载
export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const page = Number(url.searchParams.get("page")) || 1;
  const limit = Number(url.searchParams.get("limit")) || 10;
  const search = url.searchParams.get("search") || "";

  // 检查认证
  const session = await getSession(request.headers.get("Cookie"));
  if (!session.has("userId")) {
    throw redirect("/login");
  }

  // 获取数据
  const response = await fetch(
    `${process.env.API_BASE_URL}/users?page=${page}&limit=${limit}&search=${search}`,
    {
      headers: {
        Authorization: `Bearer ${session.get("token")}`,
      },
    }
  );

  if (!response.ok) {
    throw new Response("获取用户列表失败", { status: response.status });
  }

  const { data, total } = await response.json();

  return {
    users: data,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  };
}

// 页面组件接收 loaderData
export default function UsersPage({ loaderData }: Route.ComponentProps) {
  const { users, pagination } = loaderData;

  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">用户管理</h1>

      <div className="rounded-md border">
        <table className="w-full">
          <thead>
            <tr className="border-b bg-muted/50">
              <th className="p-4 text-left">姓名</th>
              <th className="p-4 text-left">邮箱</th>
              <th className="p-4 text-left">角色</th>
              <th className="p-4 text-left">操作</th>
            </tr>
          </thead>
          <tbody>
            {users.map((user) => (
              <tr key={user.id} className="border-b">
                <td className="p-4">{user.name}</td>
                <td className="p-4">{user.email}</td>
                <td className="p-4">{user.role}</td>
                <td className="p-4">
                  <Link to={`/users/${user.id}`}>查看</Link>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <Pagination {...pagination} />
    </div>
  );
}

Action (表单处理)

Action 处理表单提交,支持渐进增强:

tsx
// app/routes/login.tsx
import type { Route } from "./+types/login";
import { Form, useActionData, useNavigation, redirect } from "react-router";
import { commitSession, getSession } from "~/lib/session.server";

// 服务端表单处理
export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;
  const redirectTo = formData.get("redirectTo") as string || "/";

  // 验证
  const errors: Record<string, string> = {};

  if (!email) {
    errors.email = "请输入邮箱";
  } else if (!email.includes("@")) {
    errors.email = "请输入有效的邮箱地址";
  }

  if (!password) {
    errors.password = "请输入密码";
  } else if (password.length < 6) {
    errors.password = "密码至少 6 位";
  }

  if (Object.keys(errors).length > 0) {
    return { errors, values: { email } };
  }

  // 调用登录 API
  const response = await fetch(`${process.env.API_BASE_URL}/auth/login`, {
    method: "POST",
    body: JSON.stringify({ email, password }),
    headers: { "Content-Type": "application/json" },
  });

  if (!response.ok) {
    const data = await response.json();
    return { errors: { form: data.message || "邮箱或密码错误" } };
  }

  const { user, token } = await response.json();

  // 创建会话
  const session = await getSession(request.headers.get("Cookie"));
  session.set("userId", user.id);
  session.set("token", token);
  session.set("user", user);

  // 重定向并设置 Cookie
  return redirect(redirectTo, {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

// Meta 信息
export function meta(): Route.MetaDescriptors {
  return [
    { title: "登录 - Admin Pro" },
    { name: "description", content: "登录到 Admin Pro 管理系统" },
  ];
}

// 页面组件
export default function LoginPage() {
  const actionData = useActionData<typeof action>();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="w-full max-w-md space-y-8 p-8">
        <div className="text-center">
          <h1 className="text-2xl font-bold">欢迎回来</h1>
          <p className="text-muted-foreground">登录到您的账户</p>
        </div>

        {/* 表单错误提示 */}
        {actionData?.errors?.form && (
          <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
            {actionData.errors.form}
          </div>
        )}

        {/* 渐进增强表单 - 无 JS 也能工作 */}
        <Form method="post" className="space-y-4">
          <div className="space-y-2">
            <label htmlFor="email" className="text-sm font-medium">
              邮箱
            </label>
            <input
              id="email"
              name="email"
              type="email"
              autoComplete="email"
              required
              defaultValue={actionData?.values?.email}
              className="w-full rounded-md border px-3 py-2"
              placeholder="[email protected]"
            />
            {actionData?.errors?.email && (
              <p className="text-sm text-destructive">{actionData.errors.email}</p>
            )}
          </div>

          <div className="space-y-2">
            <label htmlFor="password" className="text-sm font-medium">
              密码
            </label>
            <input
              id="password"
              name="password"
              type="password"
              autoComplete="current-password"
              required
              className="w-full rounded-md border px-3 py-2"
              placeholder="••••••••"
            />
            {actionData?.errors?.password && (
              <p className="text-sm text-destructive">{actionData.errors.password}</p>
            )}
          </div>

          <button
            type="submit"
            disabled={isSubmitting}
            className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
          >
            {isSubmitting ? "登录中..." : "登录"}
          </button>
        </Form>

        <p className="text-center text-sm text-muted-foreground">
          还没有账户?{" "}
          <Link to="/register" className="text-primary hover:underline">
            立即注册
          </Link>
        </p>
      </div>
    </div>
  );
}

资源路由 (API 端点)

资源路由没有 UI 组件,仅导出 loader/action:

ts
// app/routes/api.users.ts
import type { Route } from "./+types/api.users";
import { getSession } from "~/lib/session.server";

// GET /api/users
export async function loader({ request }: Route.LoaderArgs) {
  const session = await getSession(request.headers.get("Cookie"));

  if (!session.has("userId")) {
    return Response.json({ error: "未授权" }, { status: 401 });
  }

  const url = new URL(request.url);
  const page = Number(url.searchParams.get("page")) || 1;
  const limit = Number(url.searchParams.get("limit")) || 10;

  const response = await fetch(
    `${process.env.API_BASE_URL}/users?page=${page}&limit=${limit}`,
    {
      headers: {
        Authorization: `Bearer ${session.get("token")}`,
      },
    }
  );

  const data = await response.json();
  return Response.json(data);
}

// POST /api/users
export async function action({ request }: Route.ActionArgs) {
  const session = await getSession(request.headers.get("Cookie"));

  if (!session.has("userId")) {
    return Response.json({ error: "未授权" }, { status: 401 });
  }

  const body = await request.json();

  const response = await fetch(`${process.env.API_BASE_URL}/users`, {
    method: "POST",
    body: JSON.stringify(body),
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${session.get("token")}`,
    },
  });

  const data = await response.json();
  return Response.json(data, { status: response.status });
}
ts
// app/routes/api.users.$id.ts
import type { Route } from "./+types/api.users.$id";

// GET /api/users/:id
export async function loader({ params, request }: Route.LoaderArgs) {
  const { id } = params;
  const session = await getSession(request.headers.get("Cookie"));

  const response = await fetch(`${process.env.API_BASE_URL}/users/${id}`, {
    headers: {
      Authorization: `Bearer ${session.get("token")}`,
    },
  });

  if (!response.ok) {
    throw new Response("用户不存在", { status: 404 });
  }

  return Response.json(await response.json());
}

// PUT /api/users/:id
export async function action({ params, request }: Route.ActionArgs) {
  const { id } = params;
  const session = await getSession(request.headers.get("Cookie"));
  const body = await request.json();

  const response = await fetch(`${process.env.API_BASE_URL}/users/${id}`, {
    method: request.method,
    body: JSON.stringify(body),
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${session.get("token")}`,
    },
  });

  return Response.json(await response.json(), { status: response.status });
}

会话管理 (Session)

使用 Cookie 进行会话管理:

ts
// app/lib/session.server.ts
import { createCookieSessionStorage, redirect } from "react-router";

// 创建会话存储
export const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "__session",
    httpOnly: true,
    maxAge: 60 * 60 * 24 * 7, // 7 天
    path: "/",
    sameSite: "lax",
    secrets: [process.env.SESSION_SECRET!],
    secure: process.env.NODE_ENV === "production",
  },
});

export const { getSession, commitSession, destroySession } = sessionStorage;

// 获取当前用户
export async function getUser(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  const user = session.get("user");
  return user || null;
}

// 要求登录
export async function requireUser(request: Request) {
  const user = await getUser(request);
  if (!user) {
    const url = new URL(request.url);
    throw redirect(`/login?redirectTo=${encodeURIComponent(url.pathname)}`);
  }
  return user;
}

// 登出
export async function logout(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  return redirect("/login", {
    headers: {
      "Set-Cookie": await destroySession(session),
    },
  });
}

错误处理 (ErrorBoundary)

全局和路由级错误处理:

tsx
// app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse, useRouteError } from "react-router";

export function ErrorBoundary() {
  const error = useRouteError();

  // 路由错误(如 404、401)
  if (isRouteErrorResponse(error)) {
    return (
      <html lang="zh-CN">
        <head>
          <meta charSet="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          <title>错误 {error.status}</title>
          <Meta />
          <Links />
        </head>
        <body>
          <div className="flex min-h-screen items-center justify-center">
            <div className="text-center">
              <h1 className="text-9xl font-bold text-muted-foreground">
                {error.status}
              </h1>
              <p className="mt-4 text-xl">{error.statusText}</p>
              <p className="mt-2 text-muted-foreground">{error.data}</p>
              <a href="/" className="mt-8 inline-block text-primary hover:underline">
                返回首页
              </a>
            </div>
          </div>
          <Scripts />
        </body>
      </html>
    );
  }

  // 未知错误
  return (
    <html lang="zh-CN">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>发生错误</title>
        <Meta />
        <Links />
      </head>
      <body>
        <div className="flex min-h-screen items-center justify-center">
          <div className="text-center">
            <h1 className="text-2xl font-bold text-destructive">发生错误</h1>
            <p className="mt-2 text-muted-foreground">
              {error instanceof Error ? error.message : "未知错误"}
            </p>
            <a href="/" className="mt-8 inline-block text-primary hover:underline">
              返回首页
            </a>
          </div>
        </div>
        <Scripts />
      </body>
    </html>
  );
}
tsx
// app/routes/users.$id.tsx - 路由级错误边界
export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error) && error.status === 404) {
    return (
      <div className="flex h-96 items-center justify-center">
        <div className="text-center">
          <h2 className="text-xl font-semibold">用户不存在</h2>
          <p className="text-muted-foreground">请检查用户 ID 是否正确</p>
          <Link to="/users" className="mt-4 inline-block text-primary">
            返回用户列表
          </Link>
        </div>
      </div>
    );
  }

  throw error; // 向上抛出其他错误
}

Meta (TDK 元信息)

tsx
// app/routes/users.tsx
import type { Route } from "./+types/users";
import { generateMeta } from "~/lib/meta";

export function meta(): Route.MetaDescriptors {
  return generateMeta("/users");
}
ts
// app/lib/meta.ts
interface PageMeta {
  title: string;
  description: string;
  keywords?: string[];
}

export const pageMetas: Record<string, PageMeta> = {
  "/": {
    title: "仪表盘",
    description: "Admin Pro 管理系统仪表盘,数据概览与统计分析",
    keywords: ["仪表盘", "数据分析", "管理系统"],
  },
  "/users": {
    title: "用户管理",
    description: "管理系统用户账户,包括创建、编辑和权限配置",
    keywords: ["用户管理", "账户管理", "权限配置"],
  },
  "/analytics": {
    title: "数据分析",
    description: "业务数据统计分析,可视化图表展示",
    keywords: ["数据分析", "图表", "统计"],
  },
  "/settings": {
    title: "系统设置",
    description: "系统配置与个性化设置",
    keywords: ["系统设置", "配置", "个性化"],
  },
};

export function generateMeta(path: string, overrides?: Partial<PageMeta>): MetaDescriptor[] {
  const meta = { ...pageMetas[path], ...overrides } || {
    title: "页面",
    description: "Admin Pro 管理系统",
  };

  const brandName = process.env.BRAND_NAME || "Halolight";
  const fullTitle = `${meta.title} - ${brandName}`;

  return [
    { title: fullTitle },
    { name: "description", content: meta.description },
    { name: "keywords", content: meta.keywords?.join(", ") || "" },
    { property: "og:title", content: fullTitle },
    { property: "og:description", content: meta.description },
    { property: "og:type", content: "website" },
  ];
}

状态管理 (Zustand)

Tabs Store (标签页)

tsx
// app/stores/tabs-store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

interface Tab {
  id: string;
  title: string;
  path: string;
  closable?: boolean;
}

const homeTab: Tab = {
  id: "home",
  title: "首页",
  path: "/",
  closable: false,
};

interface TabsState {
  tabs: Tab[];
  activeTabId: string | null;
  addTab: (tab: Omit<Tab, "id">) => string;
  removeTab: (id: string) => void;
  setActiveTab: (id: string) => void;
  closeOthers: (id: string) => void;
  closeRight: (id: string) => void;
  clearTabs: () => void;
}

export const useTabsStore = create<TabsState>()(
  persist(
    (set, get) => ({
      tabs: [homeTab],
      activeTabId: "home",

      addTab: (tab) => {
        const { tabs } = get();
        // 检查是否已存在
        const existing = tabs.find((t) => t.path === tab.path);
        if (existing) {
          set({ activeTabId: existing.id });
          return existing.id;
        }

        const id = crypto.randomUUID();
        const newTab = { ...tab, id, closable: true };
        set({
          tabs: [...tabs, newTab],
          activeTabId: id,
        });
        return id;
      },

      removeTab: (id) => {
        const { tabs, activeTabId } = get();
        const tab = tabs.find((t) => t.id === id);
        if (!tab?.closable) return;

        const newTabs = tabs.filter((t) => t.id !== id);
        let newActiveId = activeTabId;

        // 如果关闭的是当前标签,切换到相邻标签
        if (activeTabId === id) {
          const index = tabs.findIndex((t) => t.id === id);
          newActiveId = newTabs[Math.min(index, newTabs.length - 1)]?.id || "home";
        }

        set({ tabs: newTabs, activeTabId: newActiveId });
      },

      setActiveTab: (id) => set({ activeTabId: id }),

      closeOthers: (id) => {
        const { tabs } = get();
        const newTabs = tabs.filter((t) => t.id === id || !t.closable);
        set({ tabs: newTabs, activeTabId: id });
      },

      closeRight: (id) => {
        const { tabs } = get();
        const index = tabs.findIndex((t) => t.id === id);
        const newTabs = tabs.filter((t, i) => i <= index || !t.closable);
        set({ tabs: newTabs });
      },

      clearTabs: () => set({ tabs: [homeTab], activeTabId: "home" }),
    }),
    { name: "tabs-storage" }
  )
);

UI Settings Store (皮肤/布局)

tsx
// app/stores/ui-settings-store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

export type SkinPreset =
  | "default"
  | "blue"
  | "emerald"
  | "amber"
  | "violet"
  | "rose"
  | "teal"
  | "slate"
  | "ocean"
  | "sunset"
  | "aurora";

export type ThemeMode = "light" | "dark" | "system";

interface UiSettingsState {
  skin: SkinPreset;
  theme: ThemeMode;
  showFooter: boolean;
  showTabBar: boolean;
  sidebarCollapsed: boolean;
  setSkin: (skin: SkinPreset) => void;
  setTheme: (theme: ThemeMode) => void;
  setShowFooter: (visible: boolean) => void;
  setShowTabBar: (visible: boolean) => void;
  toggleSidebar: () => void;
}

export const useUiSettingsStore = create<UiSettingsState>()(
  persist(
    (set) => ({
      skin: "default",
      theme: "system",
      showFooter: true,
      showTabBar: true,
      sidebarCollapsed: false,

      setSkin: (skin) => {
        document.documentElement.setAttribute("data-skin", skin);
        set({ skin });
      },

      setTheme: (theme) => {
        if (theme === "system") {
          const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
          document.documentElement.classList.toggle("dark", isDark);
        } else {
          document.documentElement.classList.toggle("dark", theme === "dark");
        }
        set({ theme });
      },

      setShowFooter: (visible) => set({ showFooter: visible }),
      setShowTabBar: (visible) => set({ showTabBar: visible }),
      toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
    }),
    { name: "ui-settings-storage" }
  )
);

主题系统

皮肤预设

支持 11 种预设皮肤,通过 Quick Settings 面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿色 --primary: 64.6% 0.178 142.49
Amber 琥珀色 --primary: 76.9% 0.188 84.94
Violet 紫罗兰 --primary: 54.1% 0.243 293.54
Rose 玫瑰色 --primary: 64.5% 0.246 16.44
Teal 青色 --primary: 60.0% 0.118 184.71
Slate 石板灰 --primary: 45.9% 0.022 264.53
Ocean 海洋蓝 --primary: 54.3% 0.195 240.03
Sunset 日落橙 --primary: 70.5% 0.213 47.60
Aurora 极光色 --primary: 62.8% 0.265 303.9

CSS 变量 (OKLch)

css
/* app/app.css */
@import "tailwindcss";

:root {
  /* 背景色 */
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;

  /* 卡片 */
  --card: 100% 0 0;
  --card-foreground: 14.9% 0.017 285.75;

  /* 弹出层 */
  --popover: 100% 0 0;
  --popover-foreground: 14.9% 0.017 285.75;

  /* 主色 */
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;

  /* 次要色 */
  --secondary: 96.7% 0.001 286.38;
  --secondary-foreground: 21% 0.006 285.75;

  /* 静音色 */
  --muted: 96.7% 0.001 286.38;
  --muted-foreground: 55.2% 0.014 285.94;

  /* 强调色 */
  --accent: 96.7% 0.001 286.38;
  --accent-foreground: 21% 0.006 285.75;

  /* 危险色 */
  --destructive: 57.7% 0.245 27.32;
  --destructive-foreground: 100% 0 0;

  /* 边框/输入框 */
  --border: 91.2% 0.004 286.32;
  --input: 91.2% 0.004 286.32;
  --ring: 51.1% 0.262 276.97;

  /* 圆角 */
  --radius: 0.5rem;
}

/* 皮肤预设 */
[data-skin="blue"] {
  --primary: 54.8% 0.243 264.05;
  --ring: 54.8% 0.243 264.05;
}

[data-skin="ocean"] {
  --primary: 54.3% 0.195 240.03;
  --ring: 54.3% 0.195 240.03;
}

[data-skin="emerald"] {
  --primary: 64.6% 0.178 142.49;
  --ring: 64.6% 0.178 142.49;
}

[data-skin="sunset"] {
  --primary: 70.5% 0.213 47.60;
  --ring: 70.5% 0.213 47.60;
}

/* 深色模式 */
.dark {
  --background: 14.9% 0.017 285.75;
  --foreground: 98.5% 0 0;
  --card: 14.9% 0.017 285.75;
  --card-foreground: 98.5% 0 0;
  --popover: 14.9% 0.017 285.75;
  --popover-foreground: 98.5% 0 0;
  --secondary: 26.8% 0.019 286.07;
  --secondary-foreground: 98.5% 0 0;
  --muted: 26.8% 0.019 286.07;
  --muted-foreground: 71.2% 0.013 286.07;
  --accent: 26.8% 0.019 286.07;
  --accent-foreground: 98.5% 0 0;
  --border: 26.8% 0.019 286.07;
  --input: 26.8% 0.019 286.07;
}

/* Tailwind 主题映射 */
@theme {
  --color-background: oklch(var(--background));
  --color-foreground: oklch(var(--foreground));
  --color-card: oklch(var(--card));
  --color-card-foreground: oklch(var(--card-foreground));
  --color-popover: oklch(var(--popover));
  --color-popover-foreground: oklch(var(--popover-foreground));
  --color-primary: oklch(var(--primary));
  --color-primary-foreground: oklch(var(--primary-foreground));
  --color-secondary: oklch(var(--secondary));
  --color-secondary-foreground: oklch(var(--secondary-foreground));
  --color-muted: oklch(var(--muted));
  --color-muted-foreground: oklch(var(--muted-foreground));
  --color-accent: oklch(var(--accent));
  --color-accent-foreground: oklch(var(--accent-foreground));
  --color-destructive: oklch(var(--destructive));
  --color-destructive-foreground: oklch(var(--destructive-foreground));
  --color-border: oklch(var(--border));
  --color-input: oklch(var(--input));
  --color-ring: oklch(var(--ring));
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

页面路由

路径 页面 权限
/ 仪表盘 dashboard:view
/login 登录 公开
/register 注册 公开
/forgot-password 忘记密码 公开
/reset-password 重置密码 公开
/users 用户管理 users:view
/users/:id 用户详情 users:view
/settings 系统设置 settings:view
/profile 个人中心 settings:view
/security 安全设置 settings:view
/analytics 数据分析 analytics:view
/notifications 通知中心 notifications:view
/documents 文档管理 documents:view
/calendar 日历 calendar:view

环境变量

配置示例

bash
# .env
SESSION_SECRET=your-super-secret-session-key
API_BASE_URL=https://api.halolight.h7ml.cn
MOCK_ENABLED=true
DEMO_EMAIL=[email protected]
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=true
APP_TITLE=Admin Pro
BRAND_NAME=Halolight

变量说明

变量名 说明 默认值
SESSION_SECRET 会话密钥(必需) (必需)
API_BASE_URL API 基础 URL /api
MOCK_ENABLED 启用 Mock 数据 false
DEMO_EMAIL 演示账号邮箱 -
DEMO_PASSWORD 演示账号密码 -
SHOW_DEMO_HINT 显示演示提示 false
APP_TITLE 应用标题 Admin Pro
BRAND_NAME 品牌名称 Halolight

使用方式

ts
// app/routes/users.tsx
export async function loader({ request }: Route.LoaderArgs) {
  const apiUrl = process.env.API_BASE_URL;
  const response = await fetch(`${apiUrl}/users`);
  return response.json();
}

常用命令

bash
# 开发
pnpm dev              # 启动开发服务器
pnpm dev --host       # 允许局域网访问

# 构建
pnpm build            # 生产构建
pnpm start            # 启动生产服务器

# 代码质量
pnpm typecheck        # TypeScript 类型检查
pnpm lint             # ESLint 检查
pnpm lint:fix         # ESLint 自动修复
pnpm format           # Prettier 格式化

# 测试
pnpm test             # 监视模式
pnpm test:run         # 单次运行
pnpm test:coverage    # 覆盖率报告
pnpm test:ui          # Vitest UI 界面

# 部署
pnpm preview          # Cloudflare 本地预览
pnpm deploy           # 部署到 Cloudflare Pages

测试

运行命令

bash
pnpm test:run      # 单次运行
pnpm test          # 监视模式
pnpm test:coverage # 覆盖率报告

测试示例

tsx
// tests/stores/tabs-store.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { useTabsStore } from "~/stores/tabs-store";

describe("useTabsStore", () => {
  beforeEach(() => {
    useTabsStore.getState().clearTabs();
  });

  it("初始状态应该只有首页标签", () => {
    const { tabs, activeTabId } = useTabsStore.getState();
    expect(tabs).toHaveLength(1);
    expect(tabs[0].id).toBe("home");
    expect(activeTabId).toBe("home");
  });

  it("应该添加新标签", () => {
    const { addTab } = useTabsStore.getState();
    const id = addTab({ title: "用户管理", path: "/users" });

    const { tabs, activeTabId } = useTabsStore.getState();
    expect(tabs).toHaveLength(2);
    expect(tabs[1].title).toBe("用户管理");
    expect(activeTabId).toBe(id);
  });

  it("应该去重已存在的路由", () => {
    const { addTab } = useTabsStore.getState();
    const id1 = addTab({ title: "用户管理", path: "/users" });
    const id2 = addTab({ title: "用户管理", path: "/users" });

    expect(id1).toBe(id2);
    const { tabs } = useTabsStore.getState();
    expect(tabs).toHaveLength(2);
  });

  it("应该关闭标签并切换到相邻标签", () => {
    const { addTab, removeTab } = useTabsStore.getState();
    addTab({ title: "用户管理", path: "/users" });
    const id = addTab({ title: "设置", path: "/settings" });

    removeTab(id);

    const { tabs, activeTabId } = useTabsStore.getState();
    expect(tabs).toHaveLength(2);
    expect(activeTabId).not.toBe(id);
  });

  it("首页标签不可关闭", () => {
    const { removeTab } = useTabsStore.getState();
    removeTab("home");

    const { tabs } = useTabsStore.getState();
    expect(tabs).toHaveLength(1);
    expect(tabs[0].id).toBe("home");
  });
});
tsx
// tests/lib/meta.test.ts
import { describe, it, expect } from "vitest";
import { generateMeta, pageMetas } from "~/lib/meta";

describe("generateMeta", () => {
  it("应该生成正确的 meta 标签", () => {
    const meta = generateMeta("/users");

    expect(meta).toContainEqual(
      expect.objectContaining({ name: "description" })
    );
    expect(meta).toContainEqual(
      expect.objectContaining({ property: "og:title" })
    );
  });

  it("应该支持覆盖默认值", () => {
    const meta = generateMeta("/users", { title: "自定义标题" });

    const titleMeta = meta.find((m) => "title" in m);
    expect(titleMeta?.title).toContain("自定义标题");
  });
});

配置

React Router 配置

ts
// vite.config.ts
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";

export default defineConfig({
  plugins: [reactRouter()],
});

Wrangler 配置

json
// wrangler.json
{
  "name": "halolight-remix",
  "compatibility_date": "2024-12-01",
  "compatibility_flags": ["nodejs_compat"],
  "pages_build_output_dir": "./build/client"
}

ESLint 配置

js
// eslint.config.js
import js from "@eslint/js";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import tseslint from "typescript-eslint";

export default tseslint.config(
  { ignores: ["build", ".react-router"] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      "react-hooks": reactHooks,
      "react-refresh": reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      "react-refresh/only-export-components": [
        "warn",
        { allowConstantExport: true },
      ],
    },
  }
);

部署

Cloudflare Pages (推荐)

bash
# 安装 Wrangler CLI
npm install -g wrangler

# 登录
wrangler login

# 部署
pnpm deploy

Cloudflare 配置

json
// wrangler.json
{
  "name": "halolight-remix",
  "compatibility_date": "2024-12-01",
  "compatibility_flags": ["nodejs_compat"],
  "pages_build_output_dir": "./build/client"
}

GitHub Actions 部署

yaml
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install --frozen-lockfile
      - run: pnpm build

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: halolight-remix
          directory: build/client

Node.js 服务器

bash
pnpm build
pnpm start

Docker

dockerfile
# Dockerfile
FROM node:20-alpine AS builder

RUN corepack enable && corepack prepare pnpm@latest --activate

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

FROM node:20-alpine AS runner

WORKDIR /app

RUN corepack enable && corepack prepare pnpm@latest --activate

COPY --from=builder /app/build ./build
COPY --from=builder /app/package.json .
COPY --from=builder /app/pnpm-lock.yaml .

RUN pnpm install --prod --frozen-lockfile

ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

CMD ["pnpm", "start"]
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - SESSION_SECRET=${SESSION_SECRET}
      - API_BASE_URL=${API_BASE_URL}
    restart: unless-stopped

Vercel

bash
# 安装 Vercel CLI
npm install -g vercel

# 部署
vercel

其他平台

CI/CD

项目配置了完整的 GitHub Actions CI 流程:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

useFetcher (无导航数据获取)

tsx
// app/routes/users.tsx
import { useFetcher } from "react-router";

export default function UsersPage() {
  const fetcher = useFetcher();

  const handleDelete = (userId: string) => {
    if (confirm("确定删除此用户?")) {
      fetcher.submit(
        { userId },
        { method: "delete", action: "/api/users" }
      );
    }
  };

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>
          <span>{user.name}</span>
          <button
            onClick={() => handleDelete(user.id)}
            disabled={fetcher.state === "submitting"}
          >
            {fetcher.state === "submitting" ? "删除中..." : "删除"}
          </button>
        </div>
      ))}
    </div>
  );
}

乐观 UI 更新

tsx
// app/routes/notifications.tsx
import { useFetcher } from "react-router";

function NotificationItem({ notification }) {
  const fetcher = useFetcher();

  // 乐观 UI:立即显示已读状态
  const isRead = fetcher.formData
    ? fetcher.formData.get("read") === "true"
    : notification.read;

  return (
    <div className={isRead ? "opacity-50" : ""}>
      <p>{notification.message}</p>
      {!isRead && (
        <fetcher.Form method="post" action="/api/notifications/mark-read">
          <input type="hidden" name="id" value={notification.id} />
          <input type="hidden" name="read" value="true" />
          <button type="submit">标为已读</button>
        </fetcher.Form>
      )}
    </div>
  );
}

defer 和 Suspense

tsx
// app/routes/analytics.tsx
import type { Route } from "./+types/analytics";
import { Await, defer } from "react-router";
import { Suspense } from "react";

export async function loader({ request }: Route.LoaderArgs) {
  // 快速数据立即返回
  const summary = await getSummary();

  // 慢速数据延迟加载
  const chartDataPromise = getChartData();
  const reportPromise = generateReport();

  return defer({
    summary,
    chartData: chartDataPromise,
    report: reportPromise,
  });
}

export default function AnalyticsPage({ loaderData }: Route.ComponentProps) {
  const { summary, chartData, report } = loaderData;

  return (
    <div className="space-y-6">
      {/* 立即显示 */}
      <SummaryCard data={summary} />

      {/* 延迟加载的图表 */}
      <Suspense fallback={<ChartSkeleton />}>
        <Await resolve={chartData}>
          {(data) => <Chart data={data} />}
        </Await>
      </Suspense>

      {/* 延迟加载的报告 */}
      <Suspense fallback={<ReportSkeleton />}>
        <Await resolve={report}>
          {(data) => <Report data={data} />}
        </Await>
      </Suspense>
    </div>
  );
}

并行数据加载

tsx
// app/routes/dashboard.tsx
export async function loader({ request }: Route.LoaderArgs) {
  // 并行请求多个数据源
  const [stats, recentUsers, notifications, activities] = await Promise.all([
    getStats(),
    getRecentUsers(),
    getNotifications(),
    getActivities(),
  ]);

  return { stats, recentUsers, notifications, activities };
}

中间件模式

ts
// app/lib/middleware.ts
import { redirect } from "react-router";
import { getSession } from "./session.server";

type LoaderFunction = (args: LoaderArgs) => Promise<any>;

// 认证中间件
export function withAuth(loader: LoaderFunction): LoaderFunction {
  return async (args) => {
    const session = await getSession(args.request.headers.get("Cookie"));

    if (!session.has("userId")) {
      const url = new URL(args.request.url);
      throw redirect(`/login?redirectTo=${encodeURIComponent(url.pathname)}`);
    }

    // 注入用户信息
    const user = session.get("user");
    return loader({ ...args, user });
  };
}

// 角色检查中间件
export function withRole(role: string, loader: LoaderFunction): LoaderFunction {
  return withAuth(async (args) => {
    const { user } = args as any;

    if (user.role !== role) {
      throw new Response("权限不足", { status: 403 });
    }

    return loader(args);
  });
}

// 使用示例
// app/routes/admin.tsx
export const loader = withRole("admin", async ({ request }) => {
  // 只有 admin 角色才能访问
  return getAdminData();
});

性能优化

代码分割

tsx
// 使用 React.lazy 动态导入
import { lazy, Suspense } from "react";

const Chart = lazy(() => import("~/components/dashboard/chart"));

export default function Dashboard() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <Chart data={data} />
    </Suspense>
  );
}

预加载

tsx
// 链接预加载
import { Link, prefetchRouteModule } from "react-router";

function NavLink({ to, children }) {
  return (
    <Link
      to={to}
      onMouseEnter={() => prefetchRouteModule(to)}
      onFocus={() => prefetchRouteModule(to)}
    >
      {children}
    </Link>
  );
}

缓存策略

tsx
// app/routes/api.static-data.ts
export async function loader() {
  const data = await getStaticData();

  return Response.json(data, {
    headers: {
      "Cache-Control": "public, max-age=3600, s-maxage=86400",
    },
  });
}

常见问题

Q:如何处理表单验证?

A:结合服务端和客户端验证:

tsx
// app/routes/register.tsx
import { z } from "zod";

const registerSchema = z.object({
  email: z.string().email("请输入有效的邮箱"),
  password: z.string().min(6, "密码至少 6 位"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "两次密码不一致",
  path: ["confirmPassword"],
});

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const data = Object.fromEntries(formData);

  // 服务端验证
  const result = registerSchema.safeParse(data);
  if (!result.success) {
    return { errors: result.error.flatten().fieldErrors };
  }

  // 创建用户...
}

Q:如何实现文件上传?

A:使用 FormData 处理文件:

tsx
// app/routes/upload.tsx
export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const file = formData.get("file") as File;

  if (!file || file.size === 0) {
    return { error: "请选择文件" };
  }

  // 上传到存储服务
  const buffer = await file.arrayBuffer();
  const url = await uploadToStorage(buffer, file.name, file.type);

  return { url };
}

export default function UploadPage() {
  const actionData = useActionData<typeof action>();

  return (
    <Form method="post" encType="multipart/form-data">
      <input type="file" name="file" required />
      <button type="submit">上传</button>
      {actionData?.url && <p>上传成功: {actionData.url}</p>}
      {actionData?.error && <p className="text-destructive">{actionData.error}</p>}
    </Form>
  );
}

Q:如何处理国际化?

A:使用 Cookie 或 URL 前缀:

tsx
// app/lib/i18n.ts
export const locales = ["zh-CN", "en-US"] as const;
export type Locale = typeof locales[number];

export function getLocale(request: Request): Locale {
  const url = new URL(request.url);
  const cookie = request.headers.get("Cookie");

  // 1. 检查 URL 参数
  const urlLocale = url.searchParams.get("locale");
  if (urlLocale && locales.includes(urlLocale as Locale)) {
    return urlLocale as Locale;
  }

  // 2. 检查 Cookie
  const cookieLocale = getCookie(cookie, "locale");
  if (cookieLocale && locales.includes(cookieLocale as Locale)) {
    return cookieLocale as Locale;
  }

  // 3. 检查 Accept-Language
  const acceptLanguage = request.headers.get("Accept-Language");
  if (acceptLanguage?.includes("zh")) {
    return "zh-CN";
  }

  return "en-US";
}

Q:如何实现实时更新?

A:使用 SSE (Server-Sent Events):

ts
// app/routes/api.events.ts
export async function loader({ request }: Route.LoaderArgs) {
  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder();

      const sendEvent = (data: any) => {
        controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
      };

      // 定时发送事件
      const interval = setInterval(() => {
        sendEvent({ type: "ping", timestamp: Date.now() });
      }, 5000);

      // 清理
      request.signal.addEventListener("abort", () => {
        clearInterval(interval);
        controller.close();
      });
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}
tsx
// 客户端使用
useEffect(() => {
  const eventSource = new EventSource("/api/events");

  eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    // 处理事件
  };

  return () => eventSource.close();
}, []);

性能优化

代码分割

tsx
// 使用 React.lazy 动态导入
import { lazy, Suspense } from "react";

const Chart = lazy(() => import("~/components/dashboard/chart"));

export default function Dashboard() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <Chart data={data} />
    </Suspense>
  );
}

预加载

tsx
// 链接预加载
import { Link, prefetchRouteModule } from "react-router";

function NavLink({ to, children }) {
  return (
    <Link
      to={to}
      onMouseEnter={() => prefetchRouteModule(to)}
      onFocus={() => prefetchRouteModule(to)}
    >
      {children}
    </Link>
  );
}

缓存策略

tsx
// app/routes/api.static-data.ts
export async function loader() {
  const data = await getStaticData();

  return Response.json(data, {
    headers: {
      "Cache-Control": "public, max-age=3600, s-maxage=86400",
    },
  });
}

与其他版本对比

功能 Remix 版本 Vue 版本 Next.js 版本
状态管理 Zustand Pinia Zustand
数据获取 Loader/Action TanStack Query TanStack Query
表单处理 渐进增强 Form VeeValidate React Hook Form
服务端 内置 SSR Nuxt App Router
组件库 Radix UI shadcn-vue shadcn/ui
路由 文件路由 Vue Router App Router
主题 OKLch CSS 变量 OKLch CSS 变量 OKLch CSS 变量
测试 Vitest Vitest Vitest
构建工具 Vite Vite Turbopack

相关链接

]]>
<![CDATA[Solid.js 版本]]> https://halolight.docs.h7ml.cn/guide/solidjs https://halolight.docs.h7ml.cn/guide/solidjs Fri, 19 Dec 2025 04:56:41 GMT Solid.js 版本

HaloLight Solid.js 版本基于 SolidStart 1.0 构建,采用 Solid.js 细粒度响应式 + TypeScript,实现高性能管理后台。无虚拟 DOM、编译时优化、极小 Bundle 体积。

在线预览https://halolight-solidjs.h7ml.cn/

GitHubhttps://github.com/halolight/halolight-solidjs

特性

  • 细粒度响应式 - 无虚拟 DOM,精确追踪依赖更新,毫秒级响应
  • 🔧 编译时优化 - JSX 编译为高效 DOM 操作,运行时零开销
  • 📦 极小 Bundle - 核心 ~7KB gzip,比 React 小 10 倍+
  • 🎯 Signals 原语 - 简洁优雅的响应式状态管理
  • 🌐 SolidStart 全栈 - 内置 SSR/SSG、文件路由、RPC
  • 🔄 服务端函数 - "use server" 无缝调用服务端逻辑
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 📑 多标签页 - 标签栏管理
  • 命令面板 - 快捷键导航

技术栈

技术 版本 说明
SolidStart 1.x Solid 全栈框架
Solid.js 1.9+ 细粒度响应式框架
TypeScript 5.x 类型安全
Tailwind CSS 4.x 原子化 CSS + OKLch
Kobalte 0.13+ 无障碍 UI 原语
solid-primitives latest 响应式工具库
Zod 3.x 数据验证
@solid-primitives/storage latest 持久化存储
solid-charts latest 图表可视化
Vitest 4.x 单元测试
Mock.js 1.x 数据模拟

核心特性

  • 细粒度响应式 - 无虚拟 DOM,精确追踪依赖更新,毫秒级响应
  • 编译时优化 - JSX 编译为高效 DOM 操作,运行时零开销
  • Signals 原语 - 简洁优雅的响应式状态管理
  • 服务端渲染 - SolidStart 内置 SSR 支持
  • 文件路由 - 基于文件系统的路由
  • RPC 调用 - 无缝服务端函数调用
  • 可配置仪表盘 - 9 种小部件,拖拽布局,响应式适配
  • 多标签导航 - 浏览器式标签,右键菜单,状态缓存
  • 权限系统 - RBAC 权限控制,路由守卫,权限组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 命令面板 - 键盘快捷键 (⌘K),全局搜索

目录结构

halolight-solidjs/
├── src/
│   ├── routes/                    # 文件路由
│   │   ├── index.tsx             # 首页 (仪表盘)
│   │   ├── (auth)/               # 认证路由组 (无布局路径)
│   │   │   ├── login.tsx         # 登录
│   │   │   ├── register.tsx      # 注册
│   │   │   ├── forgot-password.tsx # 忘记密码
│   │   │   └── reset-password.tsx  # 重置密码
│   │   ├── (dashboard)/          # 仪表盘路由组 (带 AdminLayout)
│   │   │   ├── dashboard.tsx     # 仪表盘首页
│   │   │   ├── analytics.tsx     # 数据分析
│   │   │   ├── users/            # 用户管理
│   │   │   │   ├── index.tsx     # 用户列表
│   │   │   │   ├── create.tsx    # 创建用户
│   │   │   │   └── [id].tsx      # 用户详情 (动态路由)
│   │   │   ├── roles.tsx         # 角色管理
│   │   │   ├── permissions.tsx   # 权限管理
│   │   │   ├── messages.tsx      # 消息中心
│   │   │   ├── notifications.tsx # 通知列表
│   │   │   ├── documents.tsx     # 文档管理
│   │   │   ├── calendar.tsx      # 日历
│   │   │   ├── settings.tsx      # 系统设置
│   │   │   └── profile.tsx       # 个人中心
│   │   ├── privacy.tsx           # 隐私政策
│   │   ├── terms.tsx             # 服务条款
│   │   └── api/                  # API 路由
│   │       ├── auth/
│   │       │   ├── login.ts      # POST /api/auth/login
│   │       │   ├── register.ts   # POST /api/auth/register
│   │       │   └── logout.ts     # POST /api/auth/logout
│   │       └── users/
│   │           ├── index.ts      # GET/POST /api/users
│   │           └── [id].ts       # GET/PUT/DELETE /api/users/:id
│   ├── components/               # 组件库
│   │   ├── ui/                   # Kobalte 封装组件
│   │   │   ├── Button.tsx
│   │   │   ├── Card.tsx
│   │   │   ├── Dialog.tsx
│   │   │   ├── DropdownMenu.tsx
│   │   │   ├── Input.tsx
│   │   │   ├── Select.tsx
│   │   │   ├── Table.tsx
│   │   │   ├── Toast.tsx
│   │   │   └── ...
│   │   ├── layout/               # 布局组件
│   │   │   ├── AdminLayout.tsx   # 后台主布局
│   │   │   ├── AuthLayout.tsx    # 认证页布局
│   │   │   ├── Sidebar.tsx       # 侧边栏
│   │   │   ├── Header.tsx        # 顶部导航
│   │   │   ├── Footer.tsx        # 页脚
│   │   │   ├── TabBar.tsx        # 标签栏
│   │   │   └── QuickSettings.tsx # 快捷设置面板
│   │   ├── dashboard/            # 仪表盘组件
│   │   │   ├── DashboardGrid.tsx # 可拖拽网格
│   │   │   ├── WidgetWrapper.tsx # 部件包装器
│   │   │   ├── StatsWidget.tsx   # 统计卡片
│   │   │   └── ChartWidget.tsx   # 图表部件
│   │   ├── auth/                 # 认证组件
│   │   │   └── AuthShell.tsx     # 认证外壳
│   │   └── shared/               # 共享组件
│   │       ├── PermissionGuard.tsx # 权限守卫
│   │       └── ErrorBoundary.tsx   # 错误边界
│   ├── stores/                   # 状态管理 (Signals + Store)
│   │   ├── auth.ts               # 认证状态
│   │   ├── ui-settings.ts        # UI 设置状态
│   │   ├── tabs.ts               # 标签页状态
│   │   └── dashboard.ts          # 仪表盘布局状态
│   ├── lib/                      # 工具库
│   │   ├── api.ts                # API 客户端
│   │   ├── permission.ts         # 权限工具
│   │   ├── meta.ts               # TDK 元信息
│   │   └── cn.ts                 # 类名工具
│   ├── server/                   # 服务端代码
│   │   ├── auth.ts               # 认证逻辑
│   │   ├── session.ts            # 会话管理
│   │   └── middleware.ts         # 中间件
│   ├── hooks/                    # 自定义 Hooks
│   │   ├── createUsers.ts        # 用户数据
│   │   └── createToast.ts        # Toast 通知
│   └── types/                    # TypeScript 类型
│       ├── user.ts
│       └── api.ts
├── tests/                        # 测试文件
│   ├── setup.ts
│   ├── stores/
│   └── components/
├── public/                       # 静态资源
├── .github/workflows/ci.yml      # CI 配置
├── app.config.ts                 # SolidStart 配置
├── tailwind.config.ts            # Tailwind 配置
├── vitest.config.ts              # Vitest 配置
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-solidjs.git
cd halolight-solidjs
pnpm install

环境变量

bash
cp .env.example .env
bash
# .env 示例
VITE_API_URL=/api
VITE_USE_MOCK=true
VITE_DEMO_EMAIL=[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=true
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:3000

构建生产

bash
pnpm build
pnpm start

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

核心功能

Signals - 细粒度响应式

Solid.js 的核心是 Signals,它提供了最细粒度的响应式更新:

tsx
import { createSignal, createEffect, createMemo } from 'solid-js';

// 创建信号 - 响应式状态
const [count, setCount] = createSignal(0);

// 创建派生值 - 自动追踪依赖
const doubled = createMemo(() => count() * 2);

// 创建副作用 - 自动响应变化
createEffect(() => {
  console.log('count changed:', count());
});

// 更新状态
setCount(1);        // 设置新值
setCount(c => c + 1); // 函数式更新

Store - 嵌套响应式对象

对于复杂嵌套数据,使用 Store:

tsx
import { createStore, produce } from 'solid-js/store';

interface User {
  id: number;
  name: string;
  profile: {
    avatar: string;
    bio: string;
  };
}

const [user, setUser] = createStore<User>({
  id: 1,
  name: '管理员',
  profile: {
    avatar: '/avatar.png',
    bio: '',
  },
});

// 访问 - 自动追踪
console.log(user.name);
console.log(user.profile.avatar);

// 更新 - 路径式
setUser('name', '新名称');
setUser('profile', 'bio', '这是我的简介');

// 更新 - 函数式 (Immer 风格)
setUser(
  produce((draft) => {
    draft.name = '新名称';
    draft.profile.bio = '这是我的简介';
  })
);

状态管理 (Signals + Store)

tsx
// stores/auth.ts
import { createSignal, createMemo } from 'solid-js';
import { createStore } from 'solid-js/store';
import { makePersisted } from '@solid-primitives/storage';

interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;
  role: string;
  permissions: string[];
}

interface AuthState {
  user: User | null;
  token: string | null;
}

// 创建持久化 store
const [state, setState] = makePersisted(
  createStore<AuthState>({
    user: null,
    token: null,
  }),
  { name: 'auth-storage' }
);

// 独立的加载状态
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal<string | null>(null);

export const authStore = {
  // Getters - 响应式访问
  get user() {
    return state.user;
  },
  get token() {
    return state.token;
  },
  get loading() {
    return loading();
  },
  get error() {
    return error();
  },

  // 派生状态
  isAuthenticated: createMemo(() => !!state.token && !!state.user),
  permissions: createMemo(() => state.user?.permissions ?? []),
  isAdmin: createMemo(() => state.user?.role === 'admin'),

  // Actions
  async login(credentials: { email: string; password: string }) {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
        headers: { 'Content-Type': 'application/json' },
      });

      if (!response.ok) {
        const data = await response.json();
        throw new Error(data.message || '登录失败');
      }

      const data = await response.json();
      setState({
        user: data.user,
        token: data.token,
      });

      return data;
    } catch (e) {
      const message = e instanceof Error ? e.message : '登录失败';
      setError(message);
      throw e;
    } finally {
      setLoading(false);
    }
  },

  logout() {
    setState({ user: null, token: null });
  },

  updateProfile(updates: Partial<User>) {
    setState('user', (user) => (user ? { ...user, ...updates } : null));
  },

  // 权限检查
  hasPermission(permission: string): boolean {
    const perms = state.user?.permissions ?? [];
    return perms.some(
      (p) =>
        p === '*' ||
        p === permission ||
        (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    );
  },

  hasAnyPermission(permissions: string[]): boolean {
    return permissions.some((p) => authStore.hasPermission(p));
  },

  hasAllPermissions(permissions: string[]): boolean {
    return permissions.every((p) => authStore.hasPermission(p));
  },
};

数据获取 (createAsync)

tsx
// stores/ui-settings.ts
import { createStore } from 'solid-js/store';
import { makePersisted } from '@solid-primitives/storage';

export type SkinPreset =
  | 'default'
  | 'blue'
  | 'emerald'
  | 'amber'
  | 'violet'
  | 'rose'
  | 'teal'
  | 'slate'
  | 'ocean'
  | 'sunset'
  | 'aurora';

export type ThemeMode = 'light' | 'dark' | 'system';

interface UiSettingsState {
  skin: SkinPreset;
  theme: ThemeMode;
  showFooter: boolean;
  showTabBar: boolean;
  sidebarCollapsed: boolean;
}

const [state, setState] = makePersisted(
  createStore<UiSettingsState>({
    skin: 'default',
    theme: 'system',
    showFooter: true,
    showTabBar: true,
    sidebarCollapsed: false,
  }),
  { name: 'ui-settings-storage' }
);

export const uiSettingsStore = {
  get skin() {
    return state.skin;
  },
  get theme() {
    return state.theme;
  },
  get showFooter() {
    return state.showFooter;
  },
  get showTabBar() {
    return state.showTabBar;
  },
  get sidebarCollapsed() {
    return state.sidebarCollapsed;
  },

  setSkin(skin: SkinPreset) {
    document.documentElement.setAttribute('data-skin', skin);
    setState('skin', skin);
  },

  setTheme(theme: ThemeMode) {
    if (theme === 'system') {
      const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      document.documentElement.classList.toggle('dark', isDark);
    } else {
      document.documentElement.classList.toggle('dark', theme === 'dark');
    }
    setState('theme', theme);
  },

  setShowFooter(visible: boolean) {
    setState('showFooter', visible);
  },

  setShowTabBar(visible: boolean) {
    setState('showTabBar', visible);
  },

  toggleSidebar() {
    setState('sidebarCollapsed', (c) => !c);
  },
};

Tabs Store (标签页)

tsx
// stores/tabs.ts
import { createStore, produce } from 'solid-js/store';
import { makePersisted } from '@solid-primitives/storage';

interface Tab {
  id: string;
  title: string;
  path: string;
  closable: boolean;
}

const homeTab: Tab = {
  id: 'home',
  title: '首页',
  path: '/',
  closable: false,
};

interface TabsState {
  tabs: Tab[];
  activeTabId: string;
}

const [state, setState] = makePersisted(
  createStore<TabsState>({
    tabs: [homeTab],
    activeTabId: 'home',
  }),
  { name: 'tabs-storage' }
);

export const tabsStore = {
  get tabs() {
    return state.tabs;
  },
  get activeTabId() {
    return state.activeTabId;
  },
  get activeTab() {
    return state.tabs.find((t) => t.id === state.activeTabId);
  },

  addTab(tab: Omit<Tab, 'id' | 'closable'>): string {
    // 检查是否已存在
    const existing = state.tabs.find((t) => t.path === tab.path);
    if (existing) {
      setState('activeTabId', existing.id);
      return existing.id;
    }

    const id = crypto.randomUUID();
    const newTab: Tab = { ...tab, id, closable: true };

    setState(
      produce((draft) => {
        draft.tabs.push(newTab);
        draft.activeTabId = id;
      })
    );

    return id;
  },

  removeTab(id: string) {
    const tab = state.tabs.find((t) => t.id === id);
    if (!tab?.closable) return;

    const index = state.tabs.findIndex((t) => t.id === id);
    const newTabs = state.tabs.filter((t) => t.id !== id);

    let newActiveId = state.activeTabId;
    if (state.activeTabId === id) {
      // 切换到相邻标签
      newActiveId = newTabs[Math.min(index, newTabs.length - 1)]?.id || 'home';
    }

    setState({
      tabs: newTabs,
      activeTabId: newActiveId,
    });
  },

  setActiveTab(id: string) {
    setState('activeTabId', id);
  },

  closeOthers(id: string) {
    setState(
      produce((draft) => {
        draft.tabs = draft.tabs.filter((t) => t.id === id || !t.closable);
        draft.activeTabId = id;
      })
    );
  },

  closeRight(id: string) {
    const index = state.tabs.findIndex((t) => t.id === id);
    setState('tabs', (tabs) => tabs.filter((t, i) => i <= index || !t.closable));
  },

  clearTabs() {
    setState({
      tabs: [homeTab],
      activeTabId: 'home',
    });
  },
};

路由中间件

tsx
// src/middleware.ts
import { createMiddleware } from '@solidjs/start/middleware';

export default createMiddleware({
  onRequest: [
    // 日志中间件
    async (event) => {
      const start = Date.now();
      const response = await event.next();
      const duration = Date.now() - start;
      console.log(`${event.request.method} ${event.request.url} - ${duration}ms`);
      return response;
    },

    // 认证中间件
    async (event) => {
      const url = new URL(event.request.url);

      // 公开路径
      const publicPaths = ['/login', '/register', '/forgot-password', '/reset-password', '/api/auth'];
      const isPublic = publicPaths.some((path) => url.pathname.startsWith(path));

      if (isPublic) {
        return;
      }

      // 保护 dashboard 路由
      if (url.pathname.startsWith('/dashboard') || url.pathname.startsWith('/api/')) {
        const cookies = event.request.headers.get('cookie') || '';
        const token = cookies.match(/token=([^;]+)/)?.[1];

        if (!token) {
          // API 路由返回 401
          if (url.pathname.startsWith('/api/')) {
            return new Response(JSON.stringify({ error: '未授权' }), {
              status: 401,
              headers: { 'Content-Type': 'application/json' },
            });
          }

          // 页面路由重定向
          return new Response(null, {
            status: 302,
            headers: { Location: `/login?redirect=${encodeURIComponent(url.pathname)}` },
          });
        }

        // 验证 token 并注入用户信息
        try {
          const user = await verifyToken(token);
          event.locals.user = user;
        } catch {
          // Token 无效,清除 cookie 并重定向
          return new Response(null, {
            status: 302,
            headers: {
              Location: '/login',
              'Set-Cookie': 'token=; Max-Age=0; Path=/',
            },
          });
        }
      }
    },
  ],
});

async function verifyToken(token: string) {
  // 实际项目中验证 JWT
  return { id: 1, name: '管理员', permissions: ['*'] };
}

服务端函数 (RPC)

SolidStart 支持 "use server" 标记的服务端函数:

tsx
// server/auth.ts
'use server';
import { z } from 'zod';
import { useSession } from 'vinxi/http';

const loginSchema = z.object({
  email: z.string().email('请输入有效的邮箱地址'),
  password: z.string().min(6, '密码至少 6 位'),
});

const registerSchema = loginSchema.extend({
  name: z.string().min(2, '姓名至少 2 个字符'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: '两次密码不一致',
  path: ['confirmPassword'],
});

export async function login(credentials: z.infer<typeof loginSchema>) {
  const validated = loginSchema.parse(credentials);

  // 模拟验证
  if (validated.email !== '[email protected]' || validated.password !== '123456') {
    throw new Error('邮箱或密码错误');
  }

  const user = {
    id: 1,
    name: '管理员',
    email: validated.email,
    role: 'admin',
    permissions: ['*'],
  };

  const token = `mock_token_${Date.now()}`;

  // 设置 session
  const session = await useSession({
    password: process.env.SESSION_SECRET!,
  });
  await session.update({ userId: user.id, token });

  return {
    success: true,
    user,
    token,
  };
}

export async function register(data: z.infer<typeof registerSchema>) {
  const validated = registerSchema.parse(data);

  // 检查邮箱是否已存在
  const existing = await db.users.findByEmail(validated.email);
  if (existing) {
    throw new Error('该邮箱已被注册');
  }

  // 创建用户
  const user = await db.users.create({
    email: validated.email,
    name: validated.name,
    password: await hashPassword(validated.password),
  });

  return { success: true, user };
}

export async function getCurrentUser() {
  const session = await useSession({
    password: process.env.SESSION_SECRET!,
  });

  if (!session.data.userId) {
    return null;
  }

  const user = await db.users.findById(session.data.userId);
  return user;
}

export async function logout() {
  const session = await useSession({
    password: process.env.SESSION_SECRET!,
  });

  await session.clear();
  return { success: true };
}

API 路由

tsx
// routes/api/users/index.ts
import type { APIEvent } from '@solidjs/start/server';
import { json } from '@solidjs/router';

// GET /api/users
export async function GET(event: APIEvent) {
  const url = new URL(event.request.url);
  const page = Number(url.searchParams.get('page')) || 1;
  const limit = Number(url.searchParams.get('limit')) || 10;
  const search = url.searchParams.get('search') || '';

  // 模拟数据
  const users = generateMockUsers(page, limit, search);
  const total = 100;

  return json({
    success: true,
    data: users,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  });
}

// POST /api/users
export async function POST(event: APIEvent) {
  const body = await event.request.json();
  const { email, name, role } = body;

  // 验证
  if (!email || !name) {
    return json({ success: false, message: '邮箱和姓名不能为空' }, { status: 400 });
  }

  // 创建用户
  const user = {
    id: Date.now(),
    email,
    name,
    role: role || 'user',
    createdAt: new Date().toISOString(),
  };

  return json({
    success: true,
    data: user,
    message: '用户创建成功',
  });
}
tsx
// routes/api/users/[id].ts
import type { APIEvent } from '@solidjs/start/server';
import { json } from '@solidjs/router';

// GET /api/users/:id
export async function GET(event: APIEvent) {
  const id = event.params.id;

  const user = await db.users.findById(id);
  if (!user) {
    return json({ success: false, message: '用户不存在' }, { status: 404 });
  }

  return json({ success: true, data: user });
}

// PUT /api/users/:id
export async function PUT(event: APIEvent) {
  const id = event.params.id;
  const body = await event.request.json();

  const user = await db.users.update(id, body);
  return json({
    success: true,
    data: user,
    message: '用户更新成功',
  });
}

// DELETE /api/users/:id
export async function DELETE(event: APIEvent) {
  const id = event.params.id;

  await db.users.delete(id);
  return json({
    success: true,
    message: '用户删除成功',
  });
}

权限组件

tsx
// components/shared/PermissionGuard.tsx
import { Show, type ParentComponent, type JSX, createMemo } from 'solid-js';
import { authStore } from '~/stores/auth';

interface Props {
  permission?: string;
  permissions?: string[];
  mode?: 'any' | 'all';
  fallback?: JSX.Element;
}

export const PermissionGuard: ParentComponent<Props> = (props) => {
  const hasPermission = createMemo(() => {
    // 单权限检查
    if (props.permission) {
      return authStore.hasPermission(props.permission);
    }

    // 多权限检查
    if (props.permissions) {
      return props.mode === 'all'
        ? authStore.hasAllPermissions(props.permissions)
        : authStore.hasAnyPermission(props.permissions);
    }

    return true;
  });

  return (
    <Show when={hasPermission()} fallback={props.fallback}>
      {props.children}
    </Show>
  );
};

// 使用示例
<PermissionGuard
  permission="users:delete"
  fallback={<span class="text-muted-foreground">无权限</span>}
>
  <Button variant="destructive" onClick={handleDelete}>
    删除用户
  </Button>
</PermissionGuard>

// 多权限检查
<PermissionGuard
  permissions={['users:edit', 'users:delete']}
  mode="any"
>
  <DropdownMenu>
    <DropdownMenuItem>编辑</DropdownMenuItem>
    <DropdownMenuItem>删除</DropdownMenuItem>
  </DropdownMenu>
</PermissionGuard>

数据获取

使用 createAsynccache 进行数据获取:

tsx
// routes/(dashboard)/users/index.tsx
import { createAsync, cache, useSearchParams } from '@solidjs/router';
import { Show, For, Suspense } from 'solid-js';
import { AdminLayout } from '~/components/layout/AdminLayout';
import { Table, Pagination, Button, Input } from '~/components/ui';

// 定义缓存函数
const getUsers = cache(async (params: { page: number; limit: number; search?: string }) => {
  'use server';

  const response = await fetch(
    `${process.env.API_BASE_URL}/users?page=${params.page}&limit=${params.limit}&search=${params.search || ''}`
  );

  if (!response.ok) {
    throw new Error('获取用户列表失败');
  }

  return response.json();
}, 'users');

// 预加载
export const route = {
  load: ({ location }) => {
    const page = Number(new URLSearchParams(location.search).get('page')) || 1;
    void getUsers({ page, limit: 10 });
  },
};

export default function UsersPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  const page = () => Number(searchParams.page) || 1;
  const search = () => searchParams.search || '';

  const users = createAsync(() =>
    getUsers({ page: page(), limit: 10, search: search() })
  );

  const handleSearch = (value: string) => {
    setSearchParams({ search: value, page: '1' });
  };

  const handlePageChange = (newPage: number) => {
    setSearchParams({ page: String(newPage) });
  };

  return (
    <AdminLayout title="用户管理">
      <div class="space-y-6">
        {/* 搜索栏 */}
        <div class="flex items-center justify-between">
          <Input
            type="search"
            placeholder="搜索用户..."
            value={search()}
            onInput={(e) => handleSearch(e.currentTarget.value)}
            class="max-w-sm"
          />
          <Button>
            <PlusIcon class="mr-2 h-4 w-4" />
            添加用户
          </Button>
        </div>

        {/* 表格 */}
        <Suspense fallback={<TableSkeleton />}>
          <Show when={users()}>
            {(data) => (
              <>
                <Table>
                  <Table.Header>
                    <Table.Row>
                      <Table.Head>姓名</Table.Head>
                      <Table.Head>邮箱</Table.Head>
                      <Table.Head>角色</Table.Head>
                      <Table.Head>状态</Table.Head>
                      <Table.Head class="text-right">操作</Table.Head>
                    </Table.Row>
                  </Table.Header>
                  <Table.Body>
                    <For each={data().data}>
                      {(user) => (
                        <Table.Row>
                          <Table.Cell>{user.name}</Table.Cell>
                          <Table.Cell>{user.email}</Table.Cell>
                          <Table.Cell>
                            <Badge>{user.role}</Badge>
                          </Table.Cell>
                          <Table.Cell>
                            <StatusBadge status={user.status} />
                          </Table.Cell>
                          <Table.Cell class="text-right">
                            <UserActions user={user} />
                          </Table.Cell>
                        </Table.Row>
                      )}
                    </For>
                  </Table.Body>
                </Table>

                <Pagination
                  page={page()}
                  totalPages={data().pagination.totalPages}
                  onPageChange={handlePageChange}
                />
              </>
            )}
          </Show>
        </Suspense>
      </div>
    </AdminLayout>
  );
}

表单处理

tsx
// routes/(auth)/login.tsx
import { createSignal, Show } from 'solid-js';
import { useNavigate, useSearchParams, A } from '@solidjs/router';
import { authStore } from '~/stores/auth';
import { AuthLayout } from '~/components/layout/AuthLayout';
import { Input, Button, Card } from '~/components/ui';

export default function LoginPage() {
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();

  const [email, setEmail] = createSignal('');
  const [password, setPassword] = createSignal('');
  const [errors, setErrors] = createSignal<Record<string, string>>({});

  const validate = () => {
    const newErrors: Record<string, string> = {};

    if (!email()) {
      newErrors.email = '请输入邮箱';
    } else if (!email().includes('@')) {
      newErrors.email = '请输入有效的邮箱地址';
    }

    if (!password()) {
      newErrors.password = '请输入密码';
    } else if (password().length < 6) {
      newErrors.password = '密码至少 6 位';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async (e: Event) => {
    e.preventDefault();

    if (!validate()) return;

    try {
      await authStore.login({
        email: email(),
        password: password(),
      });

      // 跳转到原页面或仪表盘
      const redirect = searchParams.redirect || '/dashboard';
      navigate(redirect);
    } catch (e) {
      setErrors({ form: e instanceof Error ? e.message : '登录失败' });
    }
  };

  // 填充演示账号
  const fillDemo = () => {
    const demoEmail = import.meta.env.VITE_DEMO_EMAIL;
    const demoPassword = import.meta.env.VITE_DEMO_PASSWORD;
    if (demoEmail) setEmail(demoEmail);
    if (demoPassword) setPassword(demoPassword);
  };

  return (
    <AuthLayout title="登录">
      <Card class="w-full max-w-md">
        <Card.Header class="text-center">
          <Card.Title class="text-2xl">欢迎回来</Card.Title>
          <Card.Description>登录到您的账户</Card.Description>
        </Card.Header>

        <Card.Content>
          {/* 错误提示 */}
          <Show when={errors().form}>
            <div class="mb-4 rounded-md bg-destructive/10 p-3 text-sm text-destructive">
              {errors().form}
            </div>
          </Show>

          {/* 演示提示 */}
          <Show when={import.meta.env.VITE_SHOW_DEMO_HINT === 'true'}>
            <div class="mb-4 rounded-md bg-muted p-3 text-sm">
              <p>演示账号:</p>
              <p class="font-mono text-xs">
                邮箱:{import.meta.env.VITE_DEMO_EMAIL}
              </p>
              <p class="font-mono text-xs">
                密码:{import.meta.env.VITE_DEMO_PASSWORD}
              </p>
              <Button variant="link" size="sm" onClick={fillDemo} class="mt-1 h-auto p-0">
                点击填充
              </Button>
            </div>
          </Show>

          <form onSubmit={handleSubmit} class="space-y-4">
            <div class="space-y-2">
              <label for="email" class="text-sm font-medium">
                邮箱
              </label>
              <Input
                id="email"
                type="email"
                value={email()}
                onInput={(e) => setEmail(e.currentTarget.value)}
                placeholder="[email protected]"
                autocomplete="email"
              />
              <Show when={errors().email}>
                <p class="text-sm text-destructive">{errors().email}</p>
              </Show>
            </div>

            <div class="space-y-2">
              <div class="flex items-center justify-between">
                <label for="password" class="text-sm font-medium">
                  密码
                </label>
                <A href="/forgot-password" class="text-sm text-primary hover:underline">
                  忘记密码?
                </A>
              </div>
              <Input
                id="password"
                type="password"
                value={password()}
                onInput={(e) => setPassword(e.currentTarget.value)}
                placeholder="••••••••"
                autocomplete="current-password"
              />
              <Show when={errors().password}>
                <p class="text-sm text-destructive">{errors().password}</p>
              </Show>
            </div>

            <Button type="submit" class="w-full" disabled={authStore.loading}>
              {authStore.loading ? '登录中...' : '登录'}
            </Button>
          </form>
        </Card.Content>

        <Card.Footer class="justify-center">
          <p class="text-sm text-muted-foreground">
            还没有账户?{' '}
            <A href="/register" class="text-primary hover:underline">
              立即注册
            </A>
          </p>
        </Card.Footer>
      </Card>
    </AuthLayout>
  );
}

错误处理

tsx
// components/shared/ErrorBoundary.tsx
import { ErrorBoundary as SolidErrorBoundary, type ParentComponent } from 'solid-js';
import { A, useNavigate } from '@solidjs/router';
import { Button, Card } from '~/components/ui';

interface Props {
  fallback?: (error: Error, reset: () => void) => JSX.Element;
}

export const ErrorBoundary: ParentComponent<Props> = (props) => {
  return (
    <SolidErrorBoundary
      fallback={(error, reset) => {
        if (props.fallback) {
          return props.fallback(error, reset);
        }

        return <DefaultErrorFallback error={error} reset={reset} />;
      }}
    >
      {props.children}
    </SolidErrorBoundary>
  );
};

function DefaultErrorFallback(props: { error: Error; reset: () => void }) {
  const navigate = useNavigate();

  return (
    <div class="flex min-h-[400px] items-center justify-center p-4">
      <Card class="w-full max-w-md">
        <Card.Header class="text-center">
          <Card.Title class="text-destructive">发生错误</Card.Title>
          <Card.Description>{props.error.message}</Card.Description>
        </Card.Header>
        <Card.Content class="space-y-4">
          <div class="flex justify-center gap-2">
            <Button variant="outline" onClick={props.reset}>
              重试
            </Button>
            <Button onClick={() => navigate('/')}>
              返回首页
            </Button>
          </div>
        </Card.Content>
      </Card>
    </div>
  );
}
tsx
// routes/[...404].tsx - 404 页面
import { A } from '@solidjs/router';
import { Button } from '~/components/ui';

export default function NotFoundPage() {
  return (
    <div class="flex min-h-screen items-center justify-center">
      <div class="text-center">
        <h1 class="text-9xl font-bold text-muted-foreground">404</h1>
        <p class="mt-4 text-2xl text-foreground">页面未找到</p>
        <p class="mt-2 text-muted-foreground">
          您访问的页面不存在或已被移除
        </p>
        <Button as={A} href="/" class="mt-8">
          返回首页
        </Button>
      </div>
    </div>
  );
}

Meta (TDK 元信息)

tsx
// lib/meta.ts
interface PageMeta {
  title: string;
  description: string;
  keywords?: string[];
}

export const pageMetas: Record<string, PageMeta> = {
  '/': {
    title: '仪表盘',
    description: 'Admin Pro 管理系统仪表盘,数据概览与统计分析',
    keywords: ['仪表盘', '数据分析', '管理系统'],
  },
  '/users': {
    title: '用户管理',
    description: '管理系统用户账户,包括创建、编辑和权限配置',
    keywords: ['用户管理', '账户管理', '权限配置'],
  },
  '/analytics': {
    title: '数据分析',
    description: '业务数据统计分析,可视化图表展示',
    keywords: ['数据分析', '图表', '统计'],
  },
  '/settings': {
    title: '系统设置',
    description: '系统配置与个性化设置',
    keywords: ['系统设置', '配置', '个性化'],
  },
};

export function generateMeta(path: string, overrides?: Partial<PageMeta>) {
  const meta = { ...pageMetas[path], ...overrides } || {
    title: '页面',
    description: 'Admin Pro 管理系统',
  };

  const brandName = import.meta.env.VITE_BRAND_NAME || 'Halolight';
  const fullTitle = `${meta.title} - ${brandName}`;

  return {
    title: fullTitle,
    description: meta.description,
    keywords: meta.keywords?.join(', ') || '',
  };
}
tsx
// 在页面中使用
import { Title, Meta } from '@solidjs/meta';
import { generateMeta } from '~/lib/meta';

export default function UsersPage() {
  const meta = generateMeta('/users');

  return (
    <>
      <Title>{meta.title}</Title>
      <Meta name="description" content={meta.description} />
      <Meta name="keywords" content={meta.keywords} />

      {/* 页面内容 */}
    </>
  );
}

主题系统

皮肤预设

支持 11 种预设皮肤,通过 Quick Settings 面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿色 --primary: 64.6% 0.178 142.49
Amber 琥珀色 --primary: 76.9% 0.188 84.94
Violet 紫罗兰 --primary: 54.1% 0.243 293.54
Rose 玫瑰色 --primary: 64.5% 0.246 16.44
Teal 青色 --primary: 60.0% 0.118 184.71
Slate 石板灰 --primary: 45.9% 0.022 264.53
Ocean 海洋蓝 --primary: 54.3% 0.195 240.03
Sunset 日落橙 --primary: 70.5% 0.213 47.60
Aurora 极光色 --primary: 62.8% 0.265 303.9

CSS 变量 (OKLch)

css
/* src/styles/globals.css */
@import "tailwindcss";

:root {
  /* 背景色 */
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;

  /* 卡片 */
  --card: 100% 0 0;
  --card-foreground: 14.9% 0.017 285.75;

  /* 弹出层 */
  --popover: 100% 0 0;
  --popover-foreground: 14.9% 0.017 285.75;

  /* 主色 */
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;

  /* 次要色 */
  --secondary: 96.7% 0.001 286.38;
  --secondary-foreground: 21% 0.006 285.75;

  /* 静音色 */
  --muted: 96.7% 0.001 286.38;
  --muted-foreground: 55.2% 0.014 285.94;

  /* 强调色 */
  --accent: 96.7% 0.001 286.38;
  --accent-foreground: 21% 0.006 285.75;

  /* 危险色 */
  --destructive: 57.7% 0.245 27.32;
  --destructive-foreground: 100% 0 0;

  /* 边框/输入框 */
  --border: 91.2% 0.004 286.32;
  --input: 91.2% 0.004 286.32;
  --ring: 51.1% 0.262 276.97;

  /* 圆角 */
  --radius: 0.5rem;
}

/* 皮肤预设 */
[data-skin="blue"] {
  --primary: 54.8% 0.243 264.05;
  --ring: 54.8% 0.243 264.05;
}

[data-skin="ocean"] {
  --primary: 54.3% 0.195 240.03;
  --ring: 54.3% 0.195 240.03;
}

[data-skin="emerald"] {
  --primary: 64.6% 0.178 142.49;
  --ring: 64.6% 0.178 142.49;
}

/* 深色模式 */
.dark {
  --background: 14.9% 0.017 285.75;
  --foreground: 98.5% 0 0;
  --card: 14.9% 0.017 285.75;
  --card-foreground: 98.5% 0 0;
  --popover: 14.9% 0.017 285.75;
  --popover-foreground: 98.5% 0 0;
  --secondary: 26.8% 0.019 286.07;
  --secondary-foreground: 98.5% 0 0;
  --muted: 26.8% 0.019 286.07;
  --muted-foreground: 71.2% 0.013 286.07;
  --accent: 26.8% 0.019 286.07;
  --accent-foreground: 98.5% 0 0;
  --border: 26.8% 0.019 286.07;
  --input: 26.8% 0.019 286.07;
}

/* Tailwind 主题映射 */
@theme {
  --color-background: oklch(var(--background));
  --color-foreground: oklch(var(--foreground));
  --color-primary: oklch(var(--primary));
  --color-primary-foreground: oklch(var(--primary-foreground));
  /* ... */
}

页面路由

路径 页面 布局 权限
/ 首页 - 公开
/login 登录 AuthLayout 公开
/register 注册 AuthLayout 公开
/forgot-password 忘记密码 AuthLayout 公开
/reset-password 重置密码 AuthLayout 公开
/dashboard 仪表盘 AdminLayout dashboard:view
/analytics 数据分析 AdminLayout analytics:view
/users 用户列表 AdminLayout users:list
/users/create 创建用户 AdminLayout users:create
/users/[id] 用户详情 AdminLayout users:view
/roles 角色管理 AdminLayout roles:list
/permissions 权限管理 AdminLayout permissions:list
/messages 消息中心 AdminLayout messages:view
/notifications 通知列表 AdminLayout 登录即可
/documents 文档管理 AdminLayout documents:list
/calendar 日历 AdminLayout calendar:view
/settings 系统设置 AdminLayout settings:view
/profile 个人中心 AdminLayout 登录即可
/privacy 隐私政策 - 公开
/terms 服务条款 - 公开

环境变量

变量名 说明 默认值
VITE_API_URL API 基础 URL /api
VITE_USE_MOCK 启用 Mock 数据 false
VITE_DEMO_EMAIL 演示账号邮箱 -
VITE_DEMO_PASSWORD 演示账号密码 -
VITE_SHOW_DEMO_HINT 显示演示提示 false
VITE_APP_TITLE 应用标题 Admin Pro
VITE_BRAND_NAME 品牌名称 Halolight
SESSION_SECRET 会话密钥 (服务端) (必需)

常用命令

bash
# 开发
pnpm dev              # 启动开发服务器
pnpm dev --host       # 允许局域网访问

# 构建
pnpm build            # 生产构建
pnpm start            # 启动生产服务器

# 代码质量
pnpm typecheck        # TypeScript 类型检查
pnpm lint             # ESLint 检查
pnpm lint:fix         # ESLint 自动修复
pnpm format           # Prettier 格式化

# 测试
pnpm test             # 监视模式
pnpm test:run         # 单次运行
pnpm test:coverage    # 覆盖率报告
pnpm test:ui          # Vitest UI 界面

# 其他
pnpm clean            # 清理构建产物
pnpm deps             # 检查依赖更新

测试

运行测试

bash
pnpm test:run      # 单次运行
pnpm test          # 监视模式
pnpm test:coverage # 覆盖率报告

测试示例

tsx
// tests/stores/auth.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { authStore } from '~/stores/auth';

describe('authStore', () => {
  beforeEach(() => {
    authStore.logout();
  });

  it('初始状态应该是未登录', () => {
    expect(authStore.isAuthenticated()).toBe(false);
    expect(authStore.user).toBeNull();
    expect(authStore.token).toBeNull();
  });

  it('登录成功后应该更新状态', async () => {
    // Mock fetch
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () =>
        Promise.resolve({
          user: { id: 1, name: '管理员', permissions: ['*'] },
          token: 'mock_token',
        }),
    });

    await authStore.login({
      email: '[email protected]',
      password: '123456',
    });

    expect(authStore.isAuthenticated()).toBe(true);
    expect(authStore.user?.name).toBe('管理员');
    expect(authStore.token).toBe('mock_token');
  });

  it('权限检查应该正确工作', async () => {
    // 设置用户权限
    authStore.login({
      email: '[email protected]',
      password: '123456',
    });

    // 模拟登录成功后
    expect(authStore.hasPermission('*')).toBe(true);
    expect(authStore.hasPermission('users:list')).toBe(true);
    expect(authStore.hasPermission('unknown:action')).toBe(true); // * 权限
  });

  it('登出后应该清除状态', () => {
    authStore.logout();

    expect(authStore.isAuthenticated()).toBe(false);
    expect(authStore.user).toBeNull();
    expect(authStore.token).toBeNull();
  });
});
tsx
// tests/stores/tabs.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { tabsStore } from '~/stores/tabs';

describe('tabsStore', () => {
  beforeEach(() => {
    tabsStore.clearTabs();
  });

  it('初始状态应该只有首页标签', () => {
    expect(tabsStore.tabs.length).toBe(1);
    expect(tabsStore.tabs[0].id).toBe('home');
    expect(tabsStore.activeTabId).toBe('home');
  });

  it('应该添加新标签', () => {
    const id = tabsStore.addTab({ title: '用户管理', path: '/users' });

    expect(tabsStore.tabs.length).toBe(2);
    expect(tabsStore.tabs[1].title).toBe('用户管理');
    expect(tabsStore.activeTabId).toBe(id);
  });

  it('应该去重已存在的路由', () => {
    const id1 = tabsStore.addTab({ title: '用户管理', path: '/users' });
    const id2 = tabsStore.addTab({ title: '用户管理', path: '/users' });

    expect(id1).toBe(id2);
    expect(tabsStore.tabs.length).toBe(2);
  });

  it('应该关闭标签并切换到相邻标签', () => {
    tabsStore.addTab({ title: '用户管理', path: '/users' });
    const id = tabsStore.addTab({ title: '设置', path: '/settings' });

    tabsStore.removeTab(id);

    expect(tabsStore.tabs.length).toBe(2);
    expect(tabsStore.activeTabId).not.toBe(id);
  });

  it('首页标签不可关闭', () => {
    tabsStore.removeTab('home');

    expect(tabsStore.tabs.length).toBe(1);
    expect(tabsStore.tabs[0].id).toBe('home');
  });
});

配置

SolidStart 配置

ts
// app.config.ts
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'node-server', // 默认 Node.js 服务器
  },
  vite: {
    plugins: [],
    css: {
      postcss: './postcss.config.js',
    },
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['solid-js', '@solidjs/router'],
            ui: ['@kobalte/core'],
          },
        },
      },
    },
  },
  middleware: './src/middleware.ts',
});

不同环境预设

ts
// 开发环境
export default defineConfig({
  server: { preset: 'node-server' },
});

// Vercel
export default defineConfig({
  server: { preset: 'vercel' },
});

// Cloudflare Pages
export default defineConfig({
  server: { preset: 'cloudflare-pages' },
});

// Netlify
export default defineConfig({
  server: { preset: 'netlify' },
});

// AWS Lambda
export default defineConfig({
  server: { preset: 'aws-lambda' },
});

// Bun
export default defineConfig({
  server: { preset: 'bun' },
});

部署

Node.js 服务器

bash
pnpm build
node .output/server/index.mjs

Docker

dockerfile
FROM node:20-alpine AS builder

RUN corepack enable && corepack prepare pnpm@latest --activate

WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/.output ./.output
COPY --from=builder /app/package.json ./

ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - SESSION_SECRET=${SESSION_SECRET}
      - VITE_API_URL=${VITE_API_URL}
    restart: unless-stopped

Vercel

ts
// app.config.ts
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'vercel',
  },
});
bash
# 部署
npx vercel

Cloudflare Pages

ts
// app.config.ts
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'cloudflare-pages',
  },
});
bash
# 安装 Wrangler
npm install -g wrangler

# 登录
wrangler login

# 部署
wrangler pages deploy .output/public

Netlify

ts
// app.config.ts
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'netlify',
  },
});
toml
# netlify.toml
[build]
  command = "pnpm build"
  publish = ".output/public"
  functions = ".output/server"

[functions]
  node_bundler = "esbuild"

GitHub Actions 部署

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install --frozen-lockfile
      - run: pnpm build

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

CI/CD

项目配置了完整的 GitHub Actions CI 流程:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

createResource (异步数据)

tsx
import { createResource, Suspense, Show } from 'solid-js';

// 定义数据获取函数
const fetchUser = async (id: string) => {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error('用户不存在');
  return response.json();
};

function UserProfile(props: { userId: string }) {
  // createResource 自动管理加载/错误状态
  const [user, { refetch, mutate }] = createResource(
    () => props.userId,
    fetchUser
  );

  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Show when={user()} fallback={<div>用户不存在</div>}>
        {(userData) => (
          <div>
            <h1>{userData().name}</h1>
            <p>{userData().email}</p>
            <button onClick={refetch}>刷新</button>
          </div>
        )}
      </Show>
    </Suspense>
  );
}

Streaming SSR

tsx
// routes/dashboard.tsx
import { Suspense } from 'solid-js';
import { createAsync, cache } from '@solidjs/router';

// 快速数据
const getQuickStats = cache(async () => {
  'use server';
  return await db.stats.getQuick();
}, 'quick-stats');

// 慢速数据
const getDetailedAnalytics = cache(async () => {
  'use server';
  return await db.analytics.getDetailed(); // 耗时操作
}, 'detailed-analytics');

export default function Dashboard() {
  const quickStats = createAsync(() => getQuickStats());
  const analytics = createAsync(() => getDetailedAnalytics());

  return (
    <div class="space-y-6">
      {/* 快速渲染的内容 */}
      <Show when={quickStats()}>
        {(stats) => <QuickStats data={stats()} />}
      </Show>

      {/* 流式渲染的慢内容 */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <Show when={analytics()}>
          {(data) => <DetailedAnalytics data={data()} />}
        </Show>
      </Suspense>
    </div>
  );
}

乐观更新

tsx
import { createSignal, For } from 'solid-js';
import { createStore, produce } from 'solid-js/store';

function TodoList() {
  const [todos, setTodos] = createStore<Todo[]>([]);
  const [newTodo, setNewTodo] = createSignal('');

  const addTodo = async () => {
    const text = newTodo();
    if (!text.trim()) return;

    // 乐观更新 - 立即显示
    const tempId = `temp-${Date.now()}`;
    const optimisticTodo: Todo = {
      id: tempId,
      text,
      completed: false,
      pending: true,
    };

    setTodos(produce((draft) => draft.push(optimisticTodo)));
    setNewTodo('');

    try {
      // 实际请求
      const response = await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify({ text }),
      });
      const realTodo = await response.json();

      // 替换为真实数据
      setTodos(
        (todo) => todo.id === tempId,
        { id: realTodo.id, pending: false }
      );
    } catch {
      // 回滚
      setTodos((todos) => todos.filter((t) => t.id !== tempId));
    }
  };

  return (
    <div>
      <input
        value={newTodo()}
        onInput={(e) => setNewTodo(e.currentTarget.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
      />
      <For each={todos}>
        {(todo) => (
          <div class={todo.pending ? 'opacity-50' : ''}>
            {todo.text}
            {todo.pending && <span>保存中...</span>}
          </div>
        )}
      </For>
    </div>
  );
}

Context 跨组件通信

tsx
// context/theme.tsx
import { createContext, useContext, type ParentComponent } from 'solid-js';
import { createStore } from 'solid-js/store';

interface ThemeContextValue {
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;
  toggle: () => void;
}

const ThemeContext = createContext<ThemeContextValue>();

export const ThemeProvider: ParentComponent = (props) => {
  const [state, setState] = createStore({ theme: 'light' as const });

  const value: ThemeContextValue = {
    get theme() {
      return state.theme;
    },
    setTheme(theme) {
      setState('theme', theme);
      document.documentElement.classList.toggle('dark', theme === 'dark');
    },
    toggle() {
      value.setTheme(state.theme === 'light' ? 'dark' : 'light');
    },
  };

  return (
    <ThemeContext.Provider value={value}>
      {props.children}
    </ThemeContext.Provider>
  );
};

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

Portal 和 Modal

tsx
import { Portal, Show } from 'solid-js/web';
import { createSignal } from 'solid-js';

function Modal(props: { isOpen: boolean; onClose: () => void; children: JSX.Element }) {
  return (
    <Show when={props.isOpen}>
      <Portal mount={document.body}>
        <div class="fixed inset-0 z-50 flex items-center justify-center">
          {/* Backdrop */}
          <div
            class="absolute inset-0 bg-black/50"
            onClick={props.onClose}
          />
          {/* Content */}
          <div class="relative z-10 rounded-lg bg-background p-6 shadow-lg">
            {props.children}
          </div>
        </div>
      </Portal>
    </Show>
  );
}

// 使用
function App() {
  const [isOpen, setIsOpen] = createSignal(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>打开模态框</button>
      <Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
        <h2>标题</h2>
        <p>内容</p>
        <button onClick={() => setIsOpen(false)}>关闭</button>
      </Modal>
    </>
  );
}

性能优化

细粒度更新

Solid.js 的核心优势是细粒度更新,无需手动优化:

tsx
// 组件不会因为父组件更新而重新执行
function Parent() {
  const [count, setCount] = createSignal(0);

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>
        Count: {count()}
      </button>
      {/* Child 组件只创建一次 */}
      <Child />
    </div>
  );
}

function Child() {
  console.log('Child rendered'); // 只执行一次
  return <div>I'm a child</div>;
}

懒加载组件

tsx
import { lazy, Suspense } from 'solid-js';

// 懒加载重型组件
const Chart = lazy(() => import('./components/Chart'));
const DataTable = lazy(() => import('./components/DataTable'));

function Dashboard() {
  return (
    <div>
      <Suspense fallback={<ChartSkeleton />}>
        <Chart />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <DataTable />
      </Suspense>
    </div>
  );
}

列表优化

tsx
import { For, Index } from 'solid-js';

// For - 适合对象数组,按引用追踪
<For each={users()}>
  {(user, index) => (
    <div>{index()}: {user.name}</div>
  )}
</For>

// Index - 适合原始值数组,按索引追踪
<Index each={numbers()}>
  {(num, index) => (
    <div>{index}: {num()}</div>
  )}
</Index>

预加载

tsx
// 路由预加载
export const route = {
  load: ({ params }) => {
    // 预加载数据
    void getUser({ id: params.id });
    void getUserPosts({ userId: params.id });
  },
};

// 链接预加载
<A href="/users" preload>
  用户管理
</A>

常见问题

Q:Solid.js 和 React 的主要区别?

A:核心区别:

  1. 无虚拟 DOM - Solid 直接操作真实 DOM
  2. 细粒度响应式 - 只更新变化的部分,组件不重新执行
  3. 编译时优化 - JSX 编译为高效 DOM 操作
  4. Signals vs Hooks - Signals 是真正的响应式原语
tsx
// React - 组件每次状态变化都重新执行
function ReactComponent() {
  const [count, setCount] = useState(0);
  console.log('render'); // 每次更新都打印
  return <div>{count}</div>;
}

// Solid - 组件只执行一次,只有访问 signal 的地方更新
function SolidComponent() {
  const [count, setCount] = createSignal(0);
  console.log('setup'); // 只打印一次
  return <div>{count()}</div>; // 只有这里更新
}

Q:如何处理异步数据?

A:使用 createResourcecreateAsync

tsx
// createResource - 更细粒度控制
const [data, { refetch, mutate }] = createResource(source, fetcher);

// createAsync - SolidStart 路由集成
const data = createAsync(() => getData());

Q:如何共享状态?

A:三种方式:

  1. 导出 Signals/Store - 简单场景
tsx
// stores/counter.ts
export const [count, setCount] = createSignal(0);
  1. Context - 依赖注入
tsx
const CounterContext = createContext();
  1. solid-primitives/storage - 持久化
tsx
const [state, setState] = makePersisted(createStore({}), { name: 'key' });

Q:如何处理表单?

A:使用受控组件或 @modular-forms/solid

tsx
// 受控组件
const [email, setEmail] = createSignal('');
<input value={email()} onInput={(e) => setEmail(e.currentTarget.value)} />

// 或使用表单库
import { createForm } from '@modular-forms/solid';

const [form, { Form, Field }] = createForm<LoginForm>();

Q:如何实现路由守卫?

A:使用中间件或路由 load 函数:

tsx
// middleware.ts
export default createMiddleware({
  onRequest: [authMiddleware],
});

// 或在路由中
export const route = {
  load: ({ location }) => {
    if (!isAuthenticated()) {
      throw redirect('/login');
    }
  },
};

与其他版本对比

功能 Solid.js 版本 Vue 版本 Next.js 版本 Remix 版本
状态管理 Signals + Store Pinia Zustand Zustand
数据获取 createAsync TanStack Query TanStack Query Loader/Action
表单验证 自定义 + Zod VeeValidate + Zod React Hook Form + Zod 渐进增强
服务端 SolidStart 内置 独立后端 / Nuxt API Routes 内置
组件库 Kobalte shadcn-vue shadcn/ui Radix UI
路由 文件路由 Vue Router App Router 文件路由
响应式 细粒度 Signals Proxy-based Hooks Hooks
Bundle 大小 ~7KB ~33KB ~85KB ~70KB
运行时性能 极高

相关链接

]]>
<![CDATA[SvelteKit 版本]]> https://halolight.docs.h7ml.cn/guide/sveltekit https://halolight.docs.h7ml.cn/guide/sveltekit Fri, 19 Dec 2025 04:56:41 GMT SvelteKit 版本

HaloLight SvelteKit 版本基于 SvelteKit 2 构建,采用 Svelte 5 Runes + TypeScript,具备编译时优化和极致性能。

在线预览https://halolight-svelte.h7ml.cn

GitHubhttps://github.com/halolight/halolight-svelte

特性

  • 🏗️ Svelte 5 Runes - 全新响应式系统 ($state/$derived/$effect)
  • 编译时优化 - 无虚拟 DOM,极小运行时开销
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 📑 多标签页 - 标签栏管理
  • 命令面板 - 快捷键导航 (⌘K)

技术栈

技术 版本 说明
SvelteKit 2.x Svelte 全栈框架
Svelte 5.x 编译时框架 (Runes)
TypeScript 5.9 类型安全
Tailwind CSS 4.x 原子化 CSS
shadcn-svelte latest UI 组件库
Superforms 2.x 表单处理
TanStack Query 5.x 服务端状态
Mock.js 1.x 数据模拟

核心特性

  • 可配置仪表盘 - 9 种小部件,拖拽布局,响应式适配
  • 多标签导航 - 浏览器式标签,右键菜单,状态缓存
  • 权限系统 - RBAC 权限控制,路由守卫,权限组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 多账户切换 - 快速切换账户,记住登录状态
  • 命令面板 - 键盘快捷键 (⌘K),全局搜索
  • 实时通知 - WebSocket 推送,通知中心

目录结构

halolight-svelte/
├── src/
│   ├── routes/                    # 文件路由
│   │   ├── (auth)/                # 认证页面
│   │   └── (dashboard)/           # 仪表盘页面
│   ├── lib/
│   │   ├── components/            # 组件
│   │   │   ├── ui/               # 基础 UI 组件
│   │   │   ├── layout/           # 布局组件
│   │   │   └── dashboard/        # 仪表盘组件
│   │   ├── stores/               # 状态管理 (Runes)
│   │   ├── utils/                # 工具库
│   │   ├── mock/                 # Mock 数据
│   │   └── types/                # 类型定义
│   ├── hooks.server.ts           # 服务端钩子
│   └── app.css                   # 全局样式
├── static/                        # 静态资源
├── svelte.config.js              # Svelte 配置
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-svelte.git
cd halolight-svelte
pnpm install

环境变量

bash
cp .env.example .env
env
# .env
VITE_API_URL=/api
VITE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:5173

构建生产

bash
pnpm build
pnpm preview

核心功能

状态管理 (Svelte 5 Runes)

ts
// lib/stores/auth.ts
import { browser } from '$app/environment';

interface User {
  id: number;
  name: string;
  email: string;
  permissions: string[];
}

class AuthStore {
  user = $state<User | null>(null);
  token = $state<string | null>(null);

  isAuthenticated = $derived(!!this.token && !!this.user);
  permissions = $derived(this.user?.permissions ?? []);

  constructor() {
    if (browser) {
      const saved = localStorage.getItem('auth');
      if (saved) {
        const { user, token } = JSON.parse(saved);
        this.user = user;
        this.token = token;
      }
    }
  }

  async login(credentials: { email: string; password: string }) {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
      headers: { 'Content-Type': 'application/json' },
    });

    const data = await response.json();
    this.user = data.user;
    this.token = data.token;
    this.persist();
  }

  logout() {
    this.user = null;
    this.token = null;
    localStorage.removeItem('auth');
  }

  hasPermission(permission: string): boolean {
    return this.permissions.some(
      (p) =>
        p === '*' || p === permission || (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    );
  }

  private persist() {
    if (browser) {
      localStorage.setItem(
        'auth',
        JSON.stringify({
          user: this.user,
          token: this.token,
        })
      );
    }
  }
}

export const authStore = new AuthStore();

数据获取 (Load 函数)

ts
// routes/(dashboard)/+layout.ts
import type { LayoutLoad } from './$types';
import { redirect } from '@sveltejs/kit';

export const load: LayoutLoad = async ({ parent, url }) => {
  const { user } = await parent();

  if (!user) {
    throw redirect(302, `/auth/login?redirect=${url.pathname}`);
  }

  return { user };
};
svelte
<!-- routes/(dashboard)/dashboard/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data }: { data: PageData } = $props();
</script>

<h1>欢迎, {data.user.name}!</h1>

权限控制

svelte
<!-- lib/components/PermissionGuard.svelte -->
<script lang="ts">
  import { authStore } from '$lib/stores/auth';

  interface Props {
    permission: string;
    children: import('svelte').Snippet;
    fallback?: import('svelte').Snippet;
  }

  let { permission, children, fallback }: Props = $props();

  const hasPermission = $derived(authStore.hasPermission(permission));
</script>

{#if hasPermission}
  {@render children()}
{:else if fallback}
  {@render fallback()}
{/if}
svelte
<!-- 使用示例 -->
<PermissionGuard permission="users:delete">
  {#snippet children()}
    <Button variant="destructive">删除</Button>
  {/snippet}
  {#snippet fallback()}
    <span class="text-muted-foreground">无权限</span>
  {/snippet}
</PermissionGuard>

可拖拽仪表盘

svelte
<script lang="ts">
  import { SvelteSet } from 'svelte/reactivity';
  import GridLayout from '$lib/components/dashboard/GridLayout.svelte';

  // 响应式 Set 管理小部件
  let activeWidgets = new SvelteSet(['stats', 'chart', 'recent']);

  const layout = $state([
    { i: 'stats', x: 0, y: 0, w: 4, h: 2 },
    { i: 'chart', x: 4, y: 0, w: 8, h: 4 },
    { i: 'recent', x: 0, y: 2, w: 4, h: 2 },
  ]);

  function onLayoutChange(newLayout: typeof layout) {
    layout.splice(0, layout.length, ...newLayout);
    localStorage.setItem('dashboard-layout', JSON.stringify(newLayout));
  }
</script>

<GridLayout {layout} on:change={onLayoutChange}>
  {#each [...activeWidgets] as widget}
    <div data-grid-item={widget}>
      <Widget type={widget} />
    </div>
  {/each}
</GridLayout>

主题系统

皮肤预设

支持 11 种预设皮肤,通过快捷设置面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿 --primary: 64.6% 0.178 142.49
Orange 橙色 --primary: 68.9% 0.181 40.84
Rose 玫瑰 --primary: 60.7% 0.234 11.63
Teal 青色 --primary: 62.8% 0.149 186.07
Yellow 黄色 --primary: 82.3% 0.165 92.14
Violet 紫罗兰 --primary: 58.9% 0.264 292.85
Cyan 青蓝 --primary: 73.2% 0.152 196.85
Pink 粉红 --primary: 70.5% 0.226 340.54
Indigo 靛蓝 --primary: 52.4% 0.218 270.32

CSS 变量 (OKLch)

css
/* app.css */
@layer base {
  :root {
    --background: 100% 0 0;
    --foreground: 14.9% 0.017 285.75;
    --primary: 51.1% 0.262 276.97;
    --primary-foreground: 98% 0.007 285.89;
    --secondary: 96.1% 0.006 286.32;
    --secondary-foreground: 14.9% 0.017 285.75;
    --muted: 96.1% 0.006 286.32;
    --muted-foreground: 45.5% 0.026 285.82;
    --accent: 96.1% 0.006 286.32;
    --accent-foreground: 14.9% 0.017 285.75;
    --destructive: 61.1% 0.246 29.23;
    --destructive-foreground: 98% 0.007 285.89;
    --border: 92.1% 0.011 286.32;
    --input: 92.1% 0.011 286.32;
    --ring: 51.1% 0.262 276.97;
    --radius: 0.5rem;
  }

  .dark {
    --background: 22.4% 0.015 285.88;
    --foreground: 98% 0.007 285.89;
    --primary: 61.1% 0.262 276.97;
    --primary-foreground: 98% 0.007 285.89;
    /* ... */
  }
}

View Transitions 主题切换

svelte
<script lang="ts">
  function toggleTheme() {
    if (!document.startViewTransition) {
      document.documentElement.classList.toggle('dark');
      return;
    }

    document.startViewTransition(() => {
      document.documentElement.classList.toggle('dark');
    });
  }
</script>

<button onclick={toggleTheme}>切换主题</button>

<style>
  :global(::view-transition-old(root)),
  :global(::view-transition-new(root)) {
    animation-duration: 0.3s;
  }
</style>

页面路由

路径 页面 权限
/ 首页(重定向) 公开
/auth/login 登录 公开
/auth/register 注册 公开
/auth/forgot-password 忘记密码 公开
/auth/reset-password 重置密码 公开
/dashboard 仪表盘 dashboard:view
/dashboard/users 用户管理 users:view
/dashboard/analytics 数据分析 analytics:view
/dashboard/calendar 日程管理 calendar:view
/dashboard/documents 文档管理 documents:view
/dashboard/files 文件存储 files:view
/dashboard/messages 消息中心 messages:view
/dashboard/notifications 通知中心 notifications:view
/dashboard/settings 系统设置 settings:view
/dashboard/profile 个人资料 settings:view

常用命令

bash
pnpm dev            # 启动开发服务器
pnpm build          # 生产构建
pnpm preview        # 预览生产构建
pnpm lint           # 代码检查
pnpm lint:fix       # 自动修复
pnpm format         # 格式化代码
pnpm check          # 类型检查 (svelte-check)
pnpm test           # 运行测试
pnpm test:coverage  # 测试覆盖率
pnpm ci             # 完整 CI 检查

部署

Cloudflare Pages (推荐)

Deploy to Cloudflare Pages

项目默认配置 Cloudflare Pages 适配器:

js
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter(),
  },
};
bash
pnpm build
# Cloudflare Pages 会自动部署 main 分支

Docker

bash
docker build -t halolight-svelte .
docker run -p 3000:3000 halolight-svelte

其他平台

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

测试

bash
pnpm test           # 运行测试(watch 模式)
pnpm test:run       # 单次运行
pnpm test:coverage  # 覆盖率报告
pnpm test:ui        # Vitest UI 界面

测试示例

ts
// tests/auth.test.ts
import { describe, it, expect } from 'vitest';
import { authStore } from '$lib/stores/auth';

describe('AuthStore', () => {
  it('should initialize with null user', () => {
    expect(authStore.user).toBeNull();
    expect(authStore.isAuthenticated).toBe(false);
  });

  it('should authenticate user', async () => {
    await authStore.login({
      email: '[email protected]',
      password: '123456',
    });

    expect(authStore.isAuthenticated).toBe(true);
    expect(authStore.user?.email).toBe('[email protected]');
  });

  it('should check permissions', () => {
    expect(authStore.hasPermission('users:view')).toBe(true);
    expect(authStore.hasPermission('invalid')).toBe(false);
  });
});

配置

SvelteKit 配置

js
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

export default {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter(),
    alias: {
      $components: 'src/lib/components',
      $stores: 'src/lib/stores',
      $utils: 'src/lib/utils',
    },
  },
};

Vite 配置

ts
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [sveltekit()],
  test: {
    include: ['src/**/*.{test,spec}.{js,ts}'],
    environment: 'jsdom',
  },
});

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm format:check

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

响应式集合 (SvelteSet/SvelteMap)

svelte
<script lang="ts">
  import { SvelteSet, SvelteMap } from 'svelte/reactivity';

  // 响应式 Set
  let selectedIds = new SvelteSet<string>();

  function toggleSelection(id: string) {
    if (selectedIds.has(id)) {
      selectedIds.delete(id);
    } else {
      selectedIds.add(id);
    }
  }

  // 响应式 Map
  let itemStatus = new SvelteMap<string, 'pending' | 'done'>();

  function markDone(id: string) {
    itemStatus.set(id, 'done');
  }
</script>

<p>已选择: {selectedIds.size}</p>

服务端钩子

ts
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  const token = event.cookies.get('token');

  if (token) {
    // 验证 token 并设置用户信息
    event.locals.user = await validateToken(token);
  }

  // 路由保护
  if (event.url.pathname.startsWith('/dashboard')) {
    if (!event.locals.user) {
      return new Response(null, {
        status: 302,
        headers: { Location: '/auth/login' },
      });
    }
  }

  return resolve(event);
};

性能优化

懒加载组件

svelte
<script lang="ts">
  const HeavyComponent = $lazy(() => import('$lib/components/Heavy.svelte'));
</script>

{#await HeavyComponent}
  <div>加载中...</div>
{:then component}
  <svelte:component this={component} />
{/await}

预加载

svelte
<script lang="ts">
  import { preloadData } from '$app/navigation';

  function handleMouseEnter() {
    preloadData('/dashboard/analytics');
  }
</script>

<a href="/dashboard/analytics" onmouseenter={handleMouseEnter}>
  数据分析
</a>

图片优化

svelte
<script lang="ts">
  import { onMount } from 'svelte';

  let visible = $state(false);

  onMount(() => {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        visible = true;
        observer.disconnect();
      }
    });

    observer.observe(element);
  });
</script>

{#if visible}
  <img src="/large-image.jpg" alt="优化图片" />
{:else}
  <div class="placeholder" />
{/if}

常见问题

Q:如何在 SvelteKit 中使用 TanStack Query?

A:SvelteKit 推荐使用内置的 Load 函数进行数据加载,但也可以结合 TanStack Query:

svelte
<script lang="ts">
  import { createQuery } from '@tanstack/svelte-query';

  const query = createQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
  });
</script>

{#if $query.isLoading}
  <p>加载中...</p>
{:else if $query.error}
  <p>错误: {$query.error.message}</p>
{:else if $query.data}
  <ul>
    {#each $query.data as user}
      <li>{user.name}</li>
    {/each}
  </ul>
{/if}

Q:如何实现表单验证?

A:推荐使用 Superforms + Zod:

ts
// routes/users/create/+page.server.ts
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

export const actions = {
  default: async ({ request }) => {
    const form = await superValidate(request, zod(schema));

    if (!form.valid) {
      return fail(400, { form });
    }

    // 处理表单数据
    return { form };
  },
};

Q:如何部署到 Vercel?

A:切换到 Vercel 适配器:

bash
pnpm add -D @sveltejs/adapter-vercel
js
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';

export default {
  kit: {
    adapter: adapter(),
  },
};

与其他版本对比

特性 SvelteKit Next.js Vue
SSR/SSG ✅ (Nuxt)
状态管理 Svelte 5 Runes Zustand Pinia
路由 文件路由 App Router Vue Router
构建工具 Vite Turbopack Vite
运行时 无虚拟 DOM 虚拟 DOM 虚拟 DOM
表单 Superforms React Hook Form VeeValidate
组件库 shadcn-svelte shadcn/ui shadcn-vue

相关链接

]]>
<![CDATA[Web Components 组件库]]> https://halolight.docs.h7ml.cn/guide/ui https://halolight.docs.h7ml.cn/guide/ui Fri, 19 Dec 2025 04:56:41 GMT Web Components 组件库

HaloLight UI 是基于 Stencil 的跨框架 Web Components 组件库,内置 Tailwind 主题与 OKLch 配色系统。

GitHubhttps://github.com/halolight/halolight-ui

npm@halolight/ui

技术栈

技术 版本 说明
Stencil 4.22.x Web Components 编译器
TypeScript 5.x 类型系统
Tailwind CSS 4.0 工具类 CSS(开发时)
Jest 29.x 单元测试
Puppeteer 23.x E2E 测试

核心特性

  • 跨框架兼容:基于 Web Components 标准,可在 React、Vue、Angular、Svelte 等任何框架中使用
  • OKLch 色彩空间:使用感知均匀的 OKLch 色彩空间实现主题系统
  • Shadow DOM 封装:完全样式隔离,避免样式冲突
  • TypeScript:完整的类型定义支持
  • 无障碍性:遵循 WCAG 2.1 AA 标准
  • 轻量级:按需加载,Tree-shakeable

组件列表

组件 标签 说明
Button <hl-button> 按钮组件,支持多种变体和尺寸
Input <hl-input> 输入框,支持验证和错误状态
Select <hl-select> 下拉选择器
Modal <hl-modal> 弹窗对话框
Card <hl-card> 卡片容器
Table <hl-table> 数据表格,支持排序和选择
Form <hl-form> 表单容器,支持验证

目录结构

halolight-ui/
├── src/
│   ├── components/                # 组件源码
│   │   ├── hl-button/            # 按钮组件
│   │   │   ├── hl-button.tsx     # 组件逻辑
│   │   │   ├── hl-button.css     # 组件样式
│   │   │   ├── readme.md         # 组件文档
│   │   │   └── test/             # 组件测试
│   │   ├── hl-input/             # 输入框
│   │   ├── hl-select/            # 选择器
│   │   ├── hl-modal/             # 弹窗
│   │   ├── hl-table/             # 表格
│   │   ├── hl-card/              # 卡片
│   │   └── hl-form/              # 表单
│   ├── global/                   # 全局样式
│   │   └── global.css            # OKLch 主题变量
│   ├── utils/                    # 工具函数
│   ├── themes/                   # 主题配置
│   ├── components.d.ts           # 自动生成的类型定义
│   └── index.ts                  # 导出入口
├── dist/                         # 构建产物
├── loader/                       # 运行时加载器
├── www/                          # 开发预览站点
├── stencil.config.ts             # Stencil 配置
├── tailwind.config.js            # Tailwind 配置
├── tsconfig.json
└── package.json

快速开始

安装

bash
npm install @halolight/ui
# 或
pnpm add @halolight/ui

定义自定义元素

任意框架均需一次性调用:

typescript
import { defineCustomElements } from '@halolight/ui/loader';
defineCustomElements();

Vanilla JavaScript

html
<!DOCTYPE html>
<html>
<head>
  <script type="module">
    import { defineCustomElements } from 'https://unpkg.com/@halolight/ui/loader/index.js';
    defineCustomElements();
  </script>
</head>
<body>
  <hl-button variant="primary">Click Me</hl-button>
</body>
</html>

React

tsx
import { defineCustomElements } from '@halolight/ui/loader';

// 在应用入口调用一次
defineCustomElements();

function App() {
  return (
    <div>
      <hl-button variant="primary">Click Me</hl-button>
    </div>
  );
}

TypeScript 类型支持

tsx
// vite-env.d.ts
/// <reference types="@halolight/ui/dist/types/components" />

import { JSX as HaloLightJSX } from '@halolight/ui/dist/types/components';

declare global {
  namespace JSX {
    interface IntrinsicElements extends HaloLightJSX.IntrinsicElements {}
  }
}

Vue 3

vue
<template>
  <hl-button variant="primary" @hl-click="handleClick">
    Click Me
  </hl-button>
</template>

<script setup lang="ts">
import { defineCustomElements } from '@halolight/ui/loader';

defineCustomElements();

const handleClick = () => {
  console.log('Button clicked!');
};
</script>

Angular

typescript
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { defineCustomElements } from '@halolight/ui/loader';

defineCustomElements();

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
html
<!-- app.component.html -->
<hl-button variant="primary" (hlClick)="handleClick()">
  Click Me
</hl-button>

组件 API

hl-button

属性 类型 默认值 说明
variant 'primary' | 'secondary' | 'outline' | 'ghost' 'primary' 按钮变体
size 'sm' | 'md' | 'lg' 'md' 按钮尺寸
disabled boolean false 禁用状态
loading boolean false 加载状态

事件hlClick

hl-input

属性 类型 默认值 说明
type 'text' | 'password' | 'email' | 'number' 'text' 输入类型
placeholder string '' 占位文本
disabled boolean false 禁用状态
error string '' 错误信息

事件hlChangehlInputhlFocushlBlur

主题定制

暗色模式

在父级元素添加 dark 类启用暗色主题:

html
<div class="dark">
  <hl-button variant="primary">Dark Mode Button</hl-button>
</div>
javascript
// 动态切换
document.body.classList.toggle('dark');

CSS 变量覆盖

使用 OKLch 色彩空间定义主题变量:

css
:root {
  /* 主色 - 使用 OKLch */
  --hl-color-primary: oklch(0.65 0.15 250);
  --hl-color-primary-hover: oklch(0.55 0.18 250);
  --hl-color-primary-light: oklch(0.75 0.12 250);

  /* 语义色 */
  --hl-color-success: oklch(0.7 0.18 145);
  --hl-color-danger: oklch(0.63 0.26 25);
  --hl-color-warning: oklch(0.78 0.16 75);
  --hl-color-info: oklch(0.73 0.15 195);

  /* 中性色 */
  --hl-bg-base: oklch(1 0 0);
  --hl-text-primary: oklch(0.2 0 0);
  --hl-border-color: oklch(0.9 0 0);

  /* 圆角和间距 */
  --hl-border-radius: 0.5rem;
  --hl-spacing-md: 1rem;
}

/* 暗色模式 */
.dark {
  --hl-color-primary: oklch(0.7 0.15 250);
  --hl-bg-base: oklch(0.15 0 0);
  --hl-text-primary: oklch(0.98 0 0);
}

OKLch 色彩空间说明

OKLch 是基于人类视觉感知的色彩空间:

  • L (Lightness):0 (黑) ~ 1 (白)
  • C (Chroma):0 (灰) ~ 0.4 (鲜艳)
  • H (Hue):0° ~ 360° (色相环)
css
/* oklch(明度 色度 色相 / 透明度) */
oklch(0.65 0.15 250)        /* 蓝色 */
oklch(0.7 0.18 145)         /* 绿色 */
oklch(0.63 0.26 25 / 0.8)   /* 半透明红色 */

开发指南

常用命令

bash
# 安装依赖
npm install

# 启动开发服务器
npm start

# 生产构建
npm run build

# 运行测试
npm test

# 生成新组件
npm run generate

创建新组件

  1. 使用 Stencil CLI 生成骨架:
bash
npm run generate
# 输入组件名称(不含 hl- 前缀)
  1. 实现组件逻辑:
tsx
import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';

@Component({
  tag: 'hl-example',
  styleUrl: 'hl-example.css',
  shadow: true,
})
export class HlExample {
  @Prop() size: 'sm' | 'md' | 'lg' = 'md';
  @Event() hlChange: EventEmitter<string>;

  render() {
    return (
      <Host>
        <div class={`hl-example hl-example--${this.size}`}>
          <slot></slot>
        </div>
      </Host>
    );
  }
}

命名约定

  • 组件标签hl-{component-name}
  • CSS 类名:BEM 风格 .hl-button--primary
  • 事件名hl{EventName} (小驼峰)

浏览器兼容性

OKLch 色彩空间支持:

  • Chrome 111+
  • Safari 15.4+
  • Firefox 113+

对于旧版浏览器,可使用 PostCSS 插件进行降级转换。

相关项目

]]>
<![CDATA[Vercel 部署]]> https://halolight.docs.h7ml.cn/guide/vercel https://halolight.docs.h7ml.cn/guide/vercel Fri, 19 Dec 2025 04:56:41 GMT Vercel 部署

HaloLight Vercel 部署版本,针对 Vercel 平台优化的部署方案,提供最佳的 Next.js 部署体验。

在线预览https://halolight-vercel.h7ml.cn

GitHubhttps://github.com/halolight/halolight-vercel

特性

  • Vercel 原生 - Next.js 官方部署平台,零配置部署
  • Edge Functions - 边缘计算,全球低延迟
  • 🌐 全球边缘网络 - 100+ 边缘节点极速分发
  • 🔄 预览部署 - PR 自动预览环境
  • 📊 Analytics - 内置 Web Vitals 分析
  • 🔐 环境变量 - 安全的密钥管理
  • 🖼️ Image Optimization - 自动图片优化
  • 💾 KV/Blob/Postgres - Vercel 存储服务

快速开始

方式一:一键部署 (推荐)

Deploy with Vercel

点击按钮后:

  1. 登录 Vercel 账号 (支持 GitHub/GitLab/Bitbucket)
  2. 选择团队/个人账号
  3. 配置项目名称和环境变量
  4. 自动克隆并部署

方式二:Vercel CLI 部署

bash
# 安装 Vercel CLI
npm install -g vercel

# 登录 Vercel
vercel login

# 克隆项目
git clone https://github.com/halolight/halolight-vercel.git
cd halolight-vercel

# 安装依赖
pnpm install

# 本地开发
pnpm dev

# 部署到预览环境
vercel

# 部署到生产环境
vercel --prod

方式三:GitHub 集成

  1. Fork halolight-vercel 仓库
  2. 访问 vercel.com/new
  3. 导入你的 GitHub 仓库
  4. 配置环境变量
  5. 点击 Deploy

配置文件

vercel.json

json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "buildCommand": "pnpm build",
  "outputDirectory": ".next",
  "framework": "nextjs",
  "regions": ["hkg1", "sin1", "nrt1"],
  "functions": {
    "api/**/*.ts": {
      "memory": 1024,
      "maxDuration": 10
    }
  },
  "crons": [
    {
      "path": "/api/cron/daily-report",
      "schedule": "0 9 * * *"
    }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        },
        {
          "key": "Referrer-Policy",
          "value": "strict-origin-when-cross-origin"
        }
      ]
    },
    {
      "source": "/_next/static/(.*)",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    }
  ],
  "rewrites": [
    {
      "source": "/api/proxy/:path*",
      "destination": "https://api.example.com/:path*"
    }
  ],
  "redirects": [
    {
      "source": "/old-path",
      "destination": "/new-path",
      "permanent": true
    }
  ]
}

next.config.ts

typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "**.h7ml.cn",
      },
      {
        protocol: "https",
        hostname: "avatars.githubusercontent.com",
      },
    ],
    formats: ["image/avif", "image/webp"],
  },
  experimental: {
    serverActions: {
      bodySizeLimit: "2mb",
    },
  },
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          {
            key: "X-DNS-Prefetch-Control",
            value: "on",
          },
        ],
      },
    ];
  },
};

export default nextConfig;

环境变量

在 Vercel 控制台 → Settings → Environment Variables 设置:

变量名 说明 示例
NEXT_PUBLIC_API_URL API 基础 URL /api
NEXT_PUBLIC_MOCK 启用 Mock 数据 false
NEXT_PUBLIC_APP_TITLE 应用标题 Admin Pro
DATABASE_URL 数据库连接字符串 postgresql://...
JWT_SECRET JWT 密钥 your-secret-key
VERCEL_URL 部署 URL (自动) your-app.vercel.app
KV_REST_API_URL Vercel KV URL https://xxx.kv.vercel-storage.com
KV_REST_API_TOKEN Vercel KV Token xxx

环境变量作用域

Production     - 生产环境 (main 分支)
Preview        - 预览环境 (其他分支/PR)
Development    - 本地开发 (vercel dev)

CLI 管理

bash
# 查看环境变量
vercel env ls

# 添加环境变量
vercel env add VARIABLE_NAME

# 删除环境变量
vercel env rm VARIABLE_NAME

# 拉取到本地 .env.local
vercel env pull

Edge Functions

基础 Edge 函数

typescript
// app/api/edge/route.ts
import { NextRequest, NextResponse } from "next/server";

export const runtime = "edge";

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const name = searchParams.get("name") || "World";

  return NextResponse.json({
    message: `Hello, ${name}!`,
    timestamp: new Date().toISOString(),
    region: request.headers.get("x-vercel-id")?.split("::")[0],
  });
}

地理位置 Edge 函数

typescript
// app/api/geo/route.ts
import { NextRequest, NextResponse } from "next/server";
import { geolocation } from "@vercel/functions";

export const runtime = "edge";

export async function GET(request: NextRequest) {
  const geo = geolocation(request);

  return NextResponse.json({
    country: geo.country,
    city: geo.city,
    region: geo.countryRegion,
    latitude: geo.latitude,
    longitude: geo.longitude,
    ip: request.ip,
  });
}

Edge Middleware

typescript
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export const config = {
  matcher: [
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};

export function middleware(request: NextRequest) {
  // 获取地理位置
  const country = request.geo?.country || "US";

  // 基于位置的重定向
  if (country === "CN" && !request.nextUrl.pathname.startsWith("/cn")) {
    return NextResponse.redirect(new URL("/cn" + request.nextUrl.pathname, request.url));
  }

  // 添加自定义头
  const response = NextResponse.next();
  response.headers.set("x-country", country);

  return response;
}

Serverless Functions

API 路由

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get("page") || "1");
  const limit = parseInt(searchParams.get("limit") || "10");

  // 获取用户列表
  const users = await getUsers({ page, limit });

  return NextResponse.json({
    success: true,
    data: users,
    pagination: { page, limit },
  });
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  // 创建用户
  const user = await createUser(body);

  return NextResponse.json({
    success: true,
    data: user,
  }, { status: 201 });
}

流式响应

typescript
// app/api/stream/route.ts
import { NextRequest } from "next/server";

export const runtime = "edge";

export async function GET(request: NextRequest) {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        const data = JSON.stringify({ count: i, timestamp: Date.now() });
        controller.enqueue(encoder.encode(`data: ${data}\n\n`));
        await new Promise((resolve) => setTimeout(resolve, 1000));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

Vercel 存储服务

Vercel KV (Redis)

typescript
// lib/kv.ts
import { kv } from "@vercel/kv";

// 设置值
export async function setUser(id: string, user: User) {
  await kv.set(`user:${id}`, JSON.stringify(user));
  await kv.expire(`user:${id}`, 3600); // 1 小时过期
}

// 获取值
export async function getUser(id: string): Promise<User | null> {
  const data = await kv.get<string>(`user:${id}`);
  return data ? JSON.parse(data) : null;
}

// 哈希操作
export async function setSession(sessionId: string, data: SessionData) {
  await kv.hset(`session:${sessionId}`, data);
}

// 列表操作
export async function addNotification(userId: string, notification: string) {
  await kv.lpush(`notifications:${userId}`, notification);
  await kv.ltrim(`notifications:${userId}`, 0, 99); // 保留最近 100 条
}

Vercel Blob

typescript
// lib/blob.ts
import { put, del, list } from "@vercel/blob";

// 上传文件
export async function uploadFile(file: File, folder: string) {
  const blob = await put(`${folder}/${file.name}`, file, {
    access: "public",
    contentType: file.type,
  });

  return blob.url;
}

// 删除文件
export async function deleteFile(url: string) {
  await del(url);
}

// 列出文件
export async function listFiles(prefix: string) {
  const { blobs } = await list({ prefix });
  return blobs;
}

Vercel Postgres

typescript
// lib/postgres.ts
import { sql } from "@vercel/postgres";

// 查询
export async function getUsers() {
  const { rows } = await sql`SELECT * FROM users ORDER BY created_at DESC`;
  return rows;
}

// 插入
export async function createUser(email: string, name: string) {
  const { rows } = await sql`
    INSERT INTO users (email, name)
    VALUES (${email}, ${name})
    RETURNING *
  `;
  return rows[0];
}

// 事务
export async function transferCredits(fromId: string, toId: string, amount: number) {
  await sql`BEGIN`;
  try {
    await sql`UPDATE users SET credits = credits - ${amount} WHERE id = ${fromId}`;
    await sql`UPDATE users SET credits = credits + ${amount} WHERE id = ${toId}`;
    await sql`COMMIT`;
  } catch (error) {
    await sql`ROLLBACK`;
    throw error;
  }
}

Cron Jobs

配置定时任务

json
// vercel.json
{
  "crons": [
    {
      "path": "/api/cron/daily-report",
      "schedule": "0 9 * * *"
    },
    {
      "path": "/api/cron/cleanup",
      "schedule": "0 0 * * 0"
    }
  ]
}

Cron 处理函数

typescript
// app/api/cron/daily-report/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  // 验证 Cron 密钥
  const authHeader = request.headers.get("authorization");
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // 执行定时任务
  await generateDailyReport();

  return NextResponse.json({ success: true });
}

常用命令

bash
# 登录
vercel login

# 部署预览
vercel

# 部署生产
vercel --prod

# 本地开发 (模拟 Vercel 环境)
vercel dev

# 查看项目
vercel ls

# 查看部署
vercel inspect <deployment-url>

# 查看日志
vercel logs <deployment-url>

# 回滚
vercel rollback

# 域名管理
vercel domains ls
vercel domains add example.com

# 环境变量
vercel env ls
vercel env pull

# 项目设置
vercel project ls
vercel link
vercel unlink

监控与分析

Vercel Analytics

tsx
// app/layout.tsx
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

自定义事件追踪

typescript
// lib/analytics.ts
import { track } from "@vercel/analytics";

// 追踪自定义事件
export function trackEvent(name: string, properties?: Record<string, string | number>) {
  track(name, properties);
}

// 使用示例
trackEvent("button_click", { button_id: "submit", page: "/login" });
trackEvent("purchase", { amount: 99.99, currency: "USD" });

自定义域名

添加域名

bash
# CLI 方式
vercel domains add halolight-vercel.h7ml.cn

# 查看域名
vercel domains ls

# 删除域名
vercel domains rm halolight-vercel.h7ml.cn

DNS 配置

# A 记录
类型: A
名称: halolight-vercel
值: 76.76.21.21

# CNAME 记录 (推荐)
类型: CNAME
名称: halolight-vercel
值: cname.vercel-dns.com

通配符域名

bash
# 添加通配符域名
vercel domains add "*.halolight.h7ml.cn"

常见问题

Q:构建失败怎么办?

A:检查以下几点:

  1. 查看构建日志中的错误信息
  2. 确认 pnpm-lock.yaml 已提交
  3. 检查 Node.js 版本兼容性
  4. 确认环境变量已正确设置

Q:如何回滚部署?

A:使用以下方式:

bash
# CLI 回滚
vercel rollback

# 或在控制台
# Deployments → 选择之前的部署 → Promote to Production

Q:Edge Function 超时?

A:优化建议:

  1. Edge Functions 最大运行时间 25 秒
  2. 减少外部 API 调用
  3. 使用流式响应处理大数据
  4. 考虑使用 Serverless Functions (最大 60 秒)

Q:如何配置 ISR?

A:在页面中配置 revalidate:

typescript
// app/posts/[id]/page.tsx
export const revalidate = 60; // 60 秒后重新验证

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await getPost(params.id);
  return <Post data={post} />;
}

Q:如何调试生产环境?

A:使用以下方法:

  1. vercel logs <url> 查看实时日志
  2. Vercel 控制台 → Functions → 查看执行日志
  3. 使用 console.log 输出到日志
  4. 配置 Source Maps 进行错误追踪

费用说明

计划 价格 特性
Hobby 免费 100GB 带宽,Serverless 100GB-Hrs
Pro $20/成员/月 1TB 带宽,1000GB-Hrs,预览保护
Enterprise 联系销售 无限带宽,SLA,专属支持

用量计费

资源 Hobby 免费额度 Pro 免费额度 超出价格
带宽 100GB 1TB $0.15/GB
Serverless 100GB-Hrs 1000GB-Hrs $0.18/GB-Hr
Edge Functions 500K 调用 1M 调用 $0.65/M
Edge Middleware 1M 调用 1M 调用 $0.65/M
Image Optimization 1000 次 5000 次 $5/1000 次

与其他平台对比

特性 Vercel Netlify Cloudflare
Next.js 支持 ✅ 官方最佳 ⚠️ 有限
Edge Functions ✅ Workers
预览部署
内置存储 ✅ KV/Blob/Postgres ✅ KV/R2/D1
免费带宽 100GB 100GB 无限
免费构建 6000 分钟 300 分钟 500 次
ISR 支持 ✅ 原生 ⚠️ 有限

相关链接

]]>
<![CDATA[Vue 版本]]> https://halolight.docs.h7ml.cn/guide/vue https://halolight.docs.h7ml.cn/guide/vue Fri, 19 Dec 2025 04:56:41 GMT Vue 版本

HaloLight Vue 版本基于 Vue 3.5 + Vite 7 构建,采用 Composition API + TypeScript。

在线预览https://halolight-vue.h7ml.cn/

GitHubhttps://github.com/halolight/halolight-vue

特性

  • 🏗️ Composition API - Vue 3.5 组合式 API,逻辑复用更灵活
  • Vite 7 + Rolldown - 极速热更新,Rust 驱动的构建工具
  • 🎨 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 🔐 认证系统 - 完整登录/注册/找回密码流程
  • 📊 仪表盘 - 数据可视化与业务管理
  • 🛡️ 权限控制 - RBAC 细粒度权限管理
  • 📑 多标签页 - 标签栏管理
  • 命令面板 - 快捷键导航

技术栈

技术 版本 说明
Vue 3.5.x 渐进式框架
Vite 7.x (Rolldown) 构建工具
TypeScript 5.x 类型安全
Vue Router 4.x 路由管理
Pinia 2.x 状态管理
TanStack Query 5.x 服务端状态
VeeValidate 4.x 表单验证
Zod 3.x 数据验证
Tailwind CSS 4.x 原子化 CSS
shadcn-vue latest UI 组件库
grid-layout-plus 1.x 拖拽布局
ECharts 5.x 图表可视化
Mock.js 1.x 数据模拟

核心特性

  • 可配置仪表盘 - 9 种小部件,拖拽布局,响应式适配
  • 多标签导航 - 浏览器式标签,右键菜单,状态缓存
  • 权限系统 - RBAC 权限控制,路由守卫,权限组件
  • 主题系统 - 11 种皮肤,明暗模式,View Transitions
  • 多账户切换 - 快速切换账户,记住登录状态
  • 命令面板 - 键盘快捷键 (⌘K),全局搜索
  • 实时通知 - WebSocket 推送,通知中心

目录结构

halolight-vue/
├── src/
│   ├── views/               # 页面视图
│   │   ├── (auth)/         # 认证页面
│   │   └── (dashboard)/    # 仪表盘页面
│   ├── components/         # 组件
│   │   ├── ui/             # 基础 UI 组件
│   │   ├── layout/         # 布局组件
│   │   └── dashboard/      # 仪表盘组件
│   ├── composables/        # 组合式函数
│   ├── stores/             # Pinia 状态管理
│   ├── lib/                # 工具库
│   ├── mocks/              # Mock 数据
│   └── types/              # 类型定义
├── public/                 # 静态资源
├── vite.config.ts
└── package.json

快速开始

环境要求

  • Node.js >= 18.0.0
  • pnpm >= 9.x

安装

bash
git clone https://github.com/halolight/halolight-vue.git
cd halolight-vue
pnpm install

环境变量

bash
cp .env.example .env.local
env
# .env.local
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

启动开发

bash
pnpm dev

访问 http://localhost:5173

构建生产

bash
pnpm build
pnpm preview

演示账号

角色 邮箱 密码
管理员 [email protected] 123456
普通用户 [email protected] 123456

核心功能

状态管理 (Pinia)

ts
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)

  // Getters
  const isAuthenticated = computed(() => !!token.value && !!user.value)
  const permissions = computed(() => user.value?.permissions || [])

  // Actions
  async function login(credentials: LoginCredentials) {
    const response = await authService.login(credentials)
    user.value = response.user
    token.value = response.token
  }

  function logout() {
    user.value = null
    token.value = null
  }

  function hasPermission(permission: string): boolean {
    return permissions.value.some(p =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  }

  return {
    user,
    token,
    isAuthenticated,
    permissions,
    login,
    logout,
    hasPermission,
  }
}, {
  persist: {
    paths: ['token', 'user'],
  },
})

数据获取 (TanStack Query)

ts
// composables/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
import { userService } from '@/services/users'

export function useUsers(params?: Ref<UserQueryParams>) {
  return useQuery({
    queryKey: ['users', params],
    queryFn: () => userService.getList(unref(params)),
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: userService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}

权限控制

ts
// composables/usePermission.ts
import { useAuthStore } from '@/stores/auth'

export function usePermission() {
  const authStore = useAuthStore()

  function hasPermission(permission: string): boolean {
    return authStore.hasPermission(permission)
  }

  function hasAnyPermission(permissions: string[]): boolean {
    return permissions.some(p => hasPermission(p))
  }

  function hasAllPermissions(permissions: string[]): boolean {
    return permissions.every(p => hasPermission(p))
  }

  return {
    hasPermission,
    hasAnyPermission,
    hasAllPermissions,
  }
}
ts
// directives/permission.ts
import { useAuthStore } from '@/stores/auth'

export const vPermission = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const authStore = useAuthStore()
    if (!authStore.hasPermission(binding.value)) {
      el.parentNode?.removeChild(el)
    }
  },
}

// 注册指令
app.directive('permission', vPermission)
vue
<!-- 使用权限指令 -->
<button v-permission="'users:delete'">删除</button>

<!-- 使用权限组件 -->
<PermissionGuard permission="users:delete">
  <DeleteButton />
  <template #fallback>
    <span>无权限</span>
  </template>
</PermissionGuard>

可拖拽仪表盘

vue
<!-- components/dashboard/DashboardGrid.vue -->
<script setup lang="ts">
import { GridLayout, GridItem } from 'grid-layout-plus'
import { useDashboardStore } from '@/stores/dashboard'

const dashboardStore = useDashboardStore()
const { layout, isEditing } = storeToRefs(dashboardStore)
</script>

<template>
  <GridLayout
    v-model:layout="layout"
    :col-num="12"
    :row-height="80"
    :is-draggable="isEditing"
    :is-resizable="isEditing"
    :margin="[16, 16]"
  >
    <GridItem
      v-for="item in layout"
      :key="item.i"
      :x="item.x"
      :y="item.y"
      :w="item.w"
      :h="item.h"
    >
      <WidgetWrapper :widget="getWidget(item.i)" />
    </GridItem>
  </GridLayout>
</template>

主题系统

皮肤预设

支持 11 种预设皮肤,通过快捷设置面板切换:

皮肤 主色调 CSS 变量
Default 紫色 --primary: 51.1% 0.262 276.97
Blue 蓝色 --primary: 54.8% 0.243 264.05
Emerald 翠绿 --primary: 64.6% 0.178 142.49
Orange 橙色 --primary: 69.7% 0.196 49.27
Rose 玫瑰 --primary: 63.4% 0.243 357.61
Amber 琥珀 --primary: 79.1% 0.177 77.54
Cyan 青色 --primary: 74.4% 0.167 197.13
Violet 紫罗兰 --primary: 57.2% 0.267 285.75
Lime 青柠 --primary: 78.8% 0.184 127.38
Pink 粉色 --primary: 70.9% 0.254 347.58
Teal 青蓝 --primary: 67.8% 0.157 181.02

CSS 变量 (OKLch)

css
/* 示例变量定义 */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 97.3% 0.006 285.75;
  --secondary-foreground: 17.9% 0.018 285.75;
  --muted: 97.3% 0.006 285.75;
  --muted-foreground: 49.5% 0.023 285.75;
  --accent: 97.3% 0.006 285.75;
  --accent-foreground: 17.9% 0.018 285.75;
  --destructive: 59.9% 0.24 29.23;
  --destructive-foreground: 98.3% 0.002 285.75;
  --border: 91.9% 0.010 285.75;
  --input: 91.9% 0.010 285.75;
  --ring: 51.1% 0.262 276.97;
  --radius: 0.5rem;
}

主题切换

ts
// composables/useTheme.ts
import { ref, computed, watch } from 'vue'

export function useTheme() {
  const theme = ref<'light' | 'dark' | 'system'>('system')
  const skin = ref<SkinPreset>('default')

  const actualTheme = computed(() => {
    if (theme.value === 'system') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light'
    }
    return theme.value
  })

  async function toggleTheme(event?: MouseEvent) {
    const newTheme = actualTheme.value === 'dark' ? 'light' : 'dark'

    // View Transitions API
    if (!document.startViewTransition) {
      theme.value = newTheme
      return
    }

    await document.startViewTransition(() => {
      theme.value = newTheme
    }).ready

    // 圆形展开动画
    if (event) {
      const { clientX, clientY } = event
      const radius = Math.hypot(
        Math.max(clientX, window.innerWidth - clientX),
        Math.max(clientY, window.innerHeight - clientY)
      )

      document.documentElement.animate(
        {
          clipPath: [
            `circle(0px at ${clientX}px ${clientY}px)`,
            `circle(${radius}px at ${clientX}px ${clientY}px)`,
          ],
        },
        {
          duration: 500,
          easing: 'ease-in-out',
          pseudoElement: '::view-transition-new(root)',
        }
      )
    }
  }

  watch([theme, skin], () => {
    document.documentElement.classList.remove('light', 'dark')
    document.documentElement.classList.add(actualTheme.value)
    document.documentElement.setAttribute('data-skin', skin.value)
  }, { immediate: true })

  return { theme, skin, actualTheme, toggleTheme }
}

页面路由

路径 页面 权限
/ 重定向到 /dashboard -
/login 登录 公开
/register 注册 公开
/forgot-password 忘记密码 公开
/reset-password 重置密码 公开
/dashboard 仪表盘 dashboard:view
/users 用户管理 users:view
/analytics 数据分析 analytics:view
/calendar 日程管理 calendar:view
/documents 文档管理 documents:view
/files 文件存储 files:view
/messages 消息中心 messages:view
/notifications 通知中心 notifications:view
/settings 系统设置 settings:view
/profile 个人资料 settings:view

环境变量

配置示例

env
# .env.local
VITE_API_URL=/api
VITE_USE_MOCK=true
[email protected]
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

变量说明

变量名 说明 默认值
VITE_API_URL API 基础路径 /api
VITE_USE_MOCK 是否使用 Mock 数据 true
VITE_DEMO_EMAIL 演示账号邮箱 [email protected]
VITE_DEMO_PASSWORD 演示账号密码 123456
VITE_SHOW_DEMO_HINT 是否显示演示提示 false
VITE_APP_TITLE 应用标题 Admin Pro
VITE_BRAND_NAME 品牌名称 Halolight

使用方式

ts
// 在代码中使用
const apiUrl = import.meta.env.VITE_API_URL
const useMock = import.meta.env.VITE_USE_MOCK === 'true'
const appTitle = import.meta.env.VITE_APP_TITLE

常用命令

bash
pnpm dev            # 启动开发服务器
pnpm build          # 生产构建
pnpm preview        # 预览生产构建
pnpm lint           # 代码检查
pnpm lint:fix       # 自动修复
pnpm type-check     # 类型检查
pnpm test           # 运行测试
pnpm test:coverage  # 测试覆盖率

测试

bash
pnpm test           # 运行测试(watch 模式)
pnpm test:run       # 单次运行
pnpm test:coverage  # 覆盖率报告
pnpm test:ui        # Vitest UI 界面

测试示例

ts
// tests/components/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '@/components/ui/Button.vue'

describe('Button', () => {
  it('renders properly', () => {
    const wrapper = mount(Button, {
      props: { variant: 'default' },
      slots: { default: 'Click me' }
    })
    expect(wrapper.text()).toContain('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button)
    await wrapper.trigger('click')
    expect(wrapper.emitted()).toHaveProperty('click')
  })
})

配置

Vite 配置

ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    open: true,
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['@tanstack/vue-query'],
        },
      },
    },
  },
})

部署

Vercel (推荐)

Deploy with Vercel

Docker

bash
docker build -t halolight-vue .
docker run -p 3000:3000 halolight-vue

其他平台

CI/CD

项目配置了完整的 GitHub Actions CI 工作流:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

高级功能

ECharts 集成

vue
<script setup lang="ts">
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart, BarChart, PieChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import { useTheme } from '@/composables/useTheme'

use([CanvasRenderer, LineChart, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent])

const { actualTheme } = useTheme()

const option = computed(() => ({
  backgroundColor: 'transparent',
  textStyle: {
    color: actualTheme.value === 'dark' ? '#e5e5e5' : '#333',
  },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [{
    data: [820, 932, 901, 934, 1290, 1330, 1320],
    type: 'line'
  }]
}))
</script>

<template>
  <VChart :option="option" autoresize />
</template>

路由守卫

ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  history: createWebHistory(),
  routes: [...routes]
})

router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()

  // 需要认证的页面
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next({ name: 'login', query: { redirect: to.fullPath } })
    return
  }

  // 权限检查
  if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) {
    next({ name: '403' })
    return
  }

  next()
})

export default router

性能优化

图片优化

vue
<script setup lang="ts">
const imageSrc = computed(() => {
  const { width } = useWindowSize()
  if (width.value < 768) return '/images/mobile.webp'
  if (width.value < 1024) return '/images/tablet.webp'
  return '/images/desktop.webp'
})
</script>

<template>
  <img
    :src="imageSrc"
    loading="lazy"
    decoding="async"
    alt="响应式图片"
  >
</template>

懒加载组件

ts
// router/routes.ts
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/users',
    component: () => import('@/views/Users.vue'),
    meta: { requiresAuth: true, permission: 'users:view' }
  },
]

预加载

vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()

onMounted(() => {
  // 预加载常用路由
  router.resolve({ name: 'users' })
  router.resolve({ name: 'settings' })
})
</script>

常见问题

Q:如何切换主题?

A:使用 useTheme composable:

vue
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme'

const { theme, toggleTheme, skin } = useTheme()

// 切换明暗主题
function handleToggle(event: MouseEvent) {
  toggleTheme(event)
}

// 切换皮肤
function changeSkin(newSkin: SkinPreset) {
  skin.value = newSkin
}
</script>

<template>
  <button @click="handleToggle">切换主题</button>
  <select v-model="skin">
    <option value="default">Default</option>
    <option value="blue">Blue</option>
    <option value="emerald">Emerald</option>
  </select>
</template>

Q:如何添加新的权限?

A:在认证响应中添加权限字符串:

ts
// types/auth.ts
interface User {
  id: string
  name: string
  email: string
  permissions: string[] // ['users:*', 'posts:view', 'posts:create']
}

// 使用通配符
// 'users:*' - 用户模块所有权限
// '*' - 所有权限
// 'users:view' - 特定权限

Q:如何自定义仪表盘布局?

A:通过 Dashboard Store 管理布局:

ts
// stores/dashboard.ts
import { defineStore } from 'pinia'

export const useDashboardStore = defineStore('dashboard', () => {
  const layout = ref([
    { i: 'widget-1', x: 0, y: 0, w: 6, h: 4 },
    { i: 'widget-2', x: 6, y: 0, w: 6, h: 4 },
  ])

  function saveLayout(newLayout: Layout[]) {
    layout.value = newLayout
    // 保存到服务器
  }

  return { layout, saveLayout }
})

与其他版本对比

特性 Vue Next.js Angular
SSR/SSG ❌ (需要 Nuxt) ✅ (需要 Angular Universal)
状态管理 Pinia Zustand RxJS/Signals
路由 Vue Router App Router Angular Router
构建工具 Vite Next.js Angular CLI
学习曲线
生态系统 丰富 丰富 企业级

相关链接

]]>
<![CDATA[Web3 钱包集成]]> https://halolight.docs.h7ml.cn/guide/web3 https://halolight.docs.h7ml.cn/guide/web3 Fri, 19 Dec 2025 04:56:41 GMT Web3 钱包集成

HaloLight Web3 提供 EVM + Solana + IPFS 的统一接入,包含 Core/React/Vue 三个包。

GitHubhttps://github.com/halolight/halolight-web3

npm

  • @halolight/web3-core - 核心功能 (框架无关)
  • @halolight/web3-react - React 组件和 Hooks
  • @halolight/web3-vue - Vue 3 组件和 Composables

技术栈

技术 版本 说明
wagmi ^2.12.x EVM 钱包与合约调用
viem ^2.21.x EVM RPC & ABI 工具
@solana/web3.js ^1.95.x Solana JavaScript SDK
@solana/wallet-adapter latest Solana 钱包适配器
@web3-storage/w3up-client ^16.0.x IPFS/web3.storage 客户端
siwe ^2.3.x Sign-In with Ethereum
TypeScript ^5.7.x 类型系统
Turborepo ^2.3.x Monorepo 构建工具

核心特性

EVM (Ethereum/Polygon/BSC)

  • 钱包连接 (MetaMask,WalletConnect 等)
  • Sign-In with Ethereum (SIWE)
  • ERC-20 代币交互
  • ERC-721 NFT 支持
  • 智能合约调用 (读/写)
  • Gas 估算与管理
  • 多链支持

Solana

  • 钱包适配器集成 (Phantom,Solflare 等)
  • 签名认证
  • SOL 转账
  • SPL Token 支持
  • 交易管理
  • Devnet/Testnet/Mainnet 支持

IPFS

  • 文件上传 (web3.storage)
  • JSON 元数据上传
  • NFT 元数据处理
  • CID 验证与转换
  • Gateway URL 管理
  • 批量上传与进度

目录结构

halolight-web3/
├── packages/
│   ├── core/                      # 核心包 (@halolight/web3-core)
│   │   ├── src/
│   │   │   ├── evm/               # EVM/Ethereum 功能
│   │   │   │   ├── wallet.ts      # Wagmi 配置
│   │   │   │   ├── chains.ts      # 链配置
│   │   │   │   ├── siwe.ts        # Sign-In with Ethereum
│   │   │   │   └── contracts.ts   # 智能合约交互
│   │   │   ├── solana/            # Solana 功能
│   │   │   │   ├── wallet.ts      # 钱包适配器
│   │   │   │   └── auth.ts        # 签名认证
│   │   │   ├── storage/           # 存储功能
│   │   │   │   └── ipfs.ts        # IPFS 上传
│   │   │   ├── types/             # TypeScript 类型
│   │   │   └── utils/             # 工具函数
│   │   └── package.json
│   │
│   ├── react/                     # React 包 (@halolight/web3-react)
│   │   ├── src/
│   │   │   ├── providers/
│   │   │   │   └── Web3Provider.tsx
│   │   │   ├── components/
│   │   │   │   ├── WalletButton.tsx
│   │   │   │   ├── TokenBalance.tsx
│   │   │   │   ├── NftGallery.tsx
│   │   │   │   └── ContractCall.tsx
│   │   │   └── hooks/
│   │   └── package.json
│   │
│   └── vue/                       # Vue 包 (@halolight/web3-vue)
│       ├── src/
│       │   ├── composables/
│       │   │   └── useWallet.ts
│       │   └── components/
│       │       ├── WalletButton.vue
│       │       └── TokenBalance.vue
│       └── package.json

├── .env.example
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

快速开始

安装

bash
# 使用 pnpm (推荐)
pnpm add @halolight/web3-core @halolight/web3-react
# 或 Vue
pnpm add @halolight/web3-core @halolight/web3-vue

# 使用 npm
npm install @halolight/web3-core @halolight/web3-react

# 使用 yarn
yarn add @halolight/web3-core @halolight/web3-react

环境变量

复制 .env.example.env 并配置:

env
# EVM - RPC 节点
NEXT_PUBLIC_ALCHEMY_API_KEY=your_alchemy_key
NEXT_PUBLIC_INFURA_API_KEY=your_infura_key

# EVM - WalletConnect
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_project_id

# Solana
NEXT_PUBLIC_SOLANA_RPC_URL=https://api.mainnet-beta.solana.com

# IPFS/web3.storage
NEXT_PUBLIC_WEB3_STORAGE_TOKEN=your_web3_storage_token

React 示例

tsx
import { Web3Provider, WalletButton, TokenBalance } from '@halolight/web3-react';

function App() {
  return (
    <Web3Provider
      evmNetwork="mainnet"
      solanaCluster="mainnet-beta"
      enableEvm={true}
      enableSolana={true}
    >
      <div>
        <WalletButton chain="evm" />
        <WalletButton chain="solana" />

        <TokenBalance
          chain="evm"
          tokenAddress="0x..." // USDC on Ethereum
          showSymbol
        />
      </div>
    </Web3Provider>
  );
}

Vue 示例

vue
<script setup lang="ts">
import { WalletButton, TokenBalance, useEvmWallet } from '@halolight/web3-vue';
import { WagmiPlugin } from '@wagmi/vue';
import { createWagmiConfig } from '@halolight/web3-core';

const config = createWagmiConfig('mainnet');
</script>

<template>
  <div>
    <WalletButton />
    <TokenBalance
      token-address="0x..."
      :show-symbol="true"
    />
  </div>
</template>

Core 库 (框架无关)

typescript
import {
  createWagmiConfig,
  getTokenBalance,
  uploadToIpfs,
  authenticateWithSiwe,
} from '@halolight/web3-core';

// EVM: 获取代币余额
const balance = await getTokenBalance(client, tokenAddress, walletAddress);

// IPFS: 上传文件
const result = await uploadToIpfs(file);
console.log(result.cid, result.gateway);

// SIWE: 用户认证
const auth = await authenticateWithSiwe({
  domain: 'example.com',
  address: walletAddress,
  chainId: 1,
  signMessage: async (msg) => wallet.signMessage(msg),
});

React 组件

Web3Provider

统一的 EVM + Solana Provider:

tsx
<Web3Provider
  evmNetwork="mainnet"           // 或 "testnet" | "development"
  solanaCluster="mainnet-beta"   // 或 "devnet" | "testnet"
  enableEvm={true}               // 启用 EVM 支持
  enableSolana={true}            // 启用 Solana 支持
>
  {children}
</Web3Provider>

WalletButton

tsx
import { WalletButton, DefaultWalletButton } from '@halolight/web3-react';

// EVM 钱包
<WalletButton
  chain="evm"
  connectText="Connect Ethereum"
  className="custom-class"
/>

// Solana 钱包
<WalletButton
  chain="solana"
  className="custom-class"
/>

// 带默认样式
<DefaultWalletButton chain="evm" />

TokenBalance

tsx
import { TokenBalance } from '@halolight/web3-react';

<TokenBalance
  chain="evm"
  tokenAddress="0x..." // ERC-20 合约地址
  showSymbol
  decimals={4}
  loadingComponent={<Spinner />}
  errorComponent={(error) => <div>Error: {error}</div>}
/>

NftGallery

tsx
import { NftGallery } from '@halolight/web3-react';

<NftGallery
  contractAddress="0x..." // ERC-721 合约
  maxDisplay={50}
  columns={3}
  renderNft={(nft) => (
    <div>
      <img src={nft.image} alt={nft.name} />
      <h3>{nft.name}</h3>
    </div>
  )}
/>

ContractCall

tsx
import { ContractCallButton, useContractCall } from '@halolight/web3-react';

// 使用组件
<ContractCallButton
  contract={{
    address: '0x...',
    abi: MyABI,
    functionName: 'mint',
    args: [tokenId],
  }}
  type="write"
  buttonText="Mint NFT"
  onSuccess={(data) => console.log('Minted!', data)}
/>

// 使用 Hook
const { call, loading, error, txHash } = useContractCall(
  {
    address: '0x...',
    abi: MyABI,
    functionName: 'balanceOf',
    args: [address],
  },
  'read'
);

Vue Composables

useEvmWallet

typescript
import { useEvmWallet } from '@halolight/web3-vue';

const { address, isConnected, connect, disconnect } = useEvmWallet();

useTokenBalance

typescript
import { useTokenBalance } from '@halolight/web3-vue';

const { balance, loading, error, refresh } = useTokenBalance('0x...');

useNativeBalance

typescript
import { useNativeBalance } from '@halolight/web3-vue';

const { balance, formatted, loading } = useNativeBalance();

Core API

EVM 钱包

typescript
import {
  createWagmiConfig,
  formatAddress,
  isValidAddress,
} from '@halolight/web3-core';

// 创建 wagmi 配置
const config = createWagmiConfig('mainnet');

// 格式化地址
const short = formatAddress('0x1234...5678'); // "0x1234...5678"

// 验证地址
const valid = isValidAddress('0x...'); // true/false

智能合约

typescript
import {
  readContract,
  writeContract,
  getTokenBalance,
  transferToken,
  ERC20_ABI,
} from '@halolight/web3-core';

// 读取合约
const name = await readContract(publicClient, {
  address: '0x...',
  abi: ERC20_ABI,
  functionName: 'name',
});

// 写入合约
const hash = await writeContract(walletClient, publicClient, {
  address: '0x...',
  abi: ERC20_ABI,
  functionName: 'transfer',
  args: [toAddress, amount],
});

SIWE 认证

typescript
import {
  createSiweMessage,
  formatSiweMessage,
  verifySiweMessage,
  authenticateWithSiwe,
} from '@halolight/web3-core';

// 完整认证流程
const result = await authenticateWithSiwe({
  domain: 'example.com',
  address: walletAddress,
  chainId: 1,
  signMessage: async (message) => {
    return await wallet.signMessage(message);
  },
});

if (result.success) {
  console.log('Authenticated:', result.address);
}

Solana 钱包

typescript
import {
  createSolanaConnection,
  getSolBalance,
  transferSol,
} from '@halolight/web3-core';

// 创建连接
const connection = createSolanaConnection('mainnet-beta');

// 获取 SOL 余额
const balance = await getSolBalance(connection, walletAddress);

// 转账 SOL
const signature = await transferSol(
  connection,
  wallet,
  toAddress,
  1.0 // 1 SOL
);

IPFS 存储

typescript
import {
  uploadToIpfs,
  uploadJsonToIpfs,
  fetchJsonFromIpfs,
  ipfsToHttp,
} from '@halolight/web3-core';

// 上传文件
const result = await uploadToIpfs(file);
console.log(result.cid); // "QmXxx..."
console.log(result.gateway); // "https://w3s.link/ipfs/QmXxx..."

// 上传 JSON
const metadata = await uploadJsonToIpfs({
  name: 'My NFT',
  description: 'Cool NFT',
  image: 'ipfs://QmYyy...',
});

// 获取 JSON
const data = await fetchJsonFromIpfs('QmXxx...');

// 转换 IPFS URL 到 HTTP
const url = ipfsToHttp('ipfs://QmXxx...'); // "https://w3s.link/ipfs/QmXxx..."

开发指南

常用命令

bash
# 安装依赖
pnpm install

# 构建所有包
pnpm build

# 开发模式 (watch)
pnpm dev

# 运行测试
pnpm test

# 类型检查
pnpm type-check

# 代码检查
pnpm lint

# 清理构建产物
pnpm clean

单个包操作

bash
# 在 packages/core 目录下
pnpm build
pnpm dev
pnpm test

# 或从根目录
pnpm --filter @halolight/web3-core build
pnpm --filter @halolight/web3-react dev

注意事项

RPC 配置

  • 合理配置 RPC Key (Alchemy/Infura),避免速率限制
  • 使用 RPC 回退机制提高可用性

错误处理

  • 处理链上错误与拒签状态
  • 给出清晰的 UI 反馈

安全

  • 生产环境避免暴露私钥/敏感令牌
  • 使用后端签名与代理转发
  • 验证所有用户输入

相关项目

参考资源

]]>