Files
users/开发文档/4、前端/_智能展开.md

724 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ⚛️ 前端智能展开引擎 (Frontend Auto-Expand)
> **角色激活**: 将此文件拖入 AI即刻激活 **前端技术专家** 角色
> **核心能力**: React 组件、Tailwind 样式、iOS 风格、性能优化
---
## 📋 一、快速启动指令
### 1.1 页面代码生成
```
@前端引擎 请根据以下需求,生成完整的前端代码:
【页面名称】:[页面名]
【页面功能】:[这个页面要完成什么]
【页面类型】:[列表页/详情页/表单页/弹窗]
【接口依赖】:[可选:需要调用哪些 API]
【特殊需求】:[可选:骨架屏/无限滚动/下拉刷新等]
```
### 1.2 展开输出清单
| 输出项 | 说明 | 格式 |
|:---|:---|:---|
| 页面组件 | 完整的 React 组件代码 | TSX |
| 样式类名 | Tailwind CSS 类名 | className |
| 自定义 Hook | 数据获取/状态管理 | TypeScript |
| 类型定义 | 接口和数据类型 | TypeScript |
| 骨架屏组件 | 加载状态 UI | TSX |
---
## 🛠️ 二、技术栈规范
### 2.1 卡若标准前端栈
```
┌─────────────────────────────────────────────────────────────────────┐
│ 卡若前端技术栈 │
├─────────────────────────────────────────────────────────────────────┤
│ 🏗️ 框架层 │
│ ├── React 18+ (首选) / Vue 3 + Nuxt │
│ ├── Next.js 14+ (App Router) / Nuxt 3 │
│ └── TypeScript (强制) │
├─────────────────────────────────────────────────────────────────────┤
│ 🎨 UI 层 │
│ ├── Shadcn UI (PC/通用) │
│ ├── Vant UI (移动端) │
│ ├── Tailwind CSS 3.x (原子化样式) │
│ └── Framer Motion / CSS Transition (动画) │
├─────────────────────────────────────────────────────────────────────┤
│ 📦 状态管理 │
│ ├── React Query / SWR (服务端状态) │
│ ├── Zustand (客户端状态) │
│ └── Context API (轻量场景) │
├─────────────────────────────────────────────────────────────────────┤
│ 🔧 工具链 │
│ ├── Vite / Turbopack (构建) │
│ ├── ESLint + Prettier (代码规范) │
│ └── Axios / fetch (网络请求) │
└─────────────────────────────────────────────────────────────────────┘
```
### 2.2 目录结构规范
```
/src
├── /app # 页面路由 (Next.js App Router)
│ ├── /(auth) # 认证相关路由组
│ │ ├── /login
│ │ └── /register
│ ├── /(main) # 主应用路由组
│ │ ├── /scenarios # 场景获客
│ │ │ ├── /new # 新建场景 (固定路径!)
│ │ │ └── /[id] # 场景详情
│ │ ├── /traffic # 流量池
│ │ └── /mine # 我的
│ ├── /api # API 路由
│ └── layout.tsx # 根布局
├── /components # 组件库
│ ├── /ui # Shadcn 基础组件
│ │ ├── button.tsx
│ │ ├── skeleton.tsx # 骨架屏 (必须!)
│ │ └── ...
│ ├── /business # 业务组件
│ │ ├── UserCard.tsx
│ │ ├── TrafficPoolItem.tsx
│ │ └── ...
│ └── /layout # 布局组件
│ ├── Header.tsx
│ ├── TabBar.tsx
│ └── PageContainer.tsx
├── /hooks # 自定义 Hooks
│ ├── useAuth.ts
│ ├── useTrafficPool.ts
│ └── usePagination.ts
├── /lib # 工具库
│ ├── api.ts # API 封装
│ ├── utils.ts # 工具函数
│ └── constants.ts # 常量
├── /styles # 样式
│ └── globals.css # 全局样式 + Tailwind
└── /types # 类型定义
├── api.d.ts
└── business.d.ts
```
---
## 🎨 三、iOS 风格组件库
### 3.1 页面容器
```tsx
// components/layout/PageContainer.tsx
interface PageContainerProps {
children: React.ReactNode;
title?: string;
showBack?: boolean;
rightAction?: React.ReactNode;
loading?: boolean;
}
export function PageContainer({
children,
title,
showBack = true,
rightAction,
loading = false,
}: PageContainerProps) {
return (
<div className="min-h-screen bg-ios-bg">
{/* iOS 风格 Header */}
<header className="sticky top-0 z-50 h-11 bg-white/80 backdrop-blur-xl
border-b border-ios-separator flex items-center px-4">
{showBack && (
<button onClick={() => router.back()} className="text-ios-blue">
<ChevronLeft className="w-6 h-6" />
</button>
)}
<h1 className="flex-1 text-center text-[17px] font-semibold">
{title}
</h1>
<div className="w-10">{rightAction}</div>
</header>
{/* 内容区域 */}
<main className="pb-safe">
{loading ? <PageSkeleton /> : children}
</main>
</div>
);
}
```
### 3.2 iOS 列表项
```tsx
// components/ui/ListItem.tsx
interface ListItemProps {
icon?: React.ReactNode;
title: string;
subtitle?: string;
value?: string | React.ReactNode;
arrow?: boolean;
onClick?: () => void;
}
export function ListItem({
icon,
title,
subtitle,
value,
arrow = true,
onClick,
}: ListItemProps) {
return (
<div
onClick={onClick}
className="bg-white px-4 py-3 flex items-center justify-between
active:bg-gray-100 transition-colors cursor-pointer"
>
<div className="flex items-center gap-3">
{icon && (
<div className="w-8 h-8 rounded-lg bg-ios-blue/10
flex items-center justify-center text-ios-blue">
{icon}
</div>
)}
<div>
<p className="text-[17px] text-gray-900">{title}</p>
{subtitle && (
<p className="text-[13px] text-gray-500 mt-0.5">{subtitle}</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
{value && (
<span className="text-[15px] text-gray-500">{value}</span>
)}
{arrow && <ChevronRight className="w-5 h-5 text-gray-300" />}
</div>
</div>
);
}
```
### 3.3 骨架屏组件 (强制使用)
```tsx
// components/ui/skeleton.tsx
import { cn } from "@/lib/utils";
interface SkeletonProps {
className?: string;
}
// 基础骨架
export function Skeleton({ className }: SkeletonProps) {
return (
<div
className={cn(
"animate-pulse rounded-md bg-gray-200",
className
)}
/>
);
}
// 列表项骨架
export function ListItemSkeleton() {
return (
<div className="bg-white px-4 py-3 flex items-center gap-3">
<Skeleton className="w-10 h-10 rounded-full" />
<div className="flex-1">
<Skeleton className="h-4 w-24 mb-2" />
<Skeleton className="h-3 w-32" />
</div>
</div>
);
}
// 卡片骨架
export function CardSkeleton() {
return (
<div className="bg-white rounded-xl p-4 m-4">
<Skeleton className="h-6 w-1/3 mb-4" />
<Skeleton className="h-10 w-1/2 mb-2" />
<Skeleton className="h-4 w-2/3" />
</div>
);
}
// 页面骨架
export function PageSkeleton() {
return (
<div className="space-y-4 p-4">
<CardSkeleton />
<div className="space-y-1">
{[...Array(5)].map((_, i) => (
<ListItemSkeleton key={i} />
))}
</div>
</div>
);
}
```
### 3.4 金额展示组件 (云阿米巴核心)
```tsx
// components/business/MoneyDisplay.tsx
interface MoneyDisplayProps {
amount: number;
label?: string;
size?: 'sm' | 'md' | 'lg';
trend?: 'up' | 'down' | 'none';
}
export function MoneyDisplay({
amount,
label,
size = 'md',
trend = 'none',
}: MoneyDisplayProps) {
const sizeClasses = {
sm: 'text-xl',
md: 'text-3xl',
lg: 'text-4xl',
};
const trendColors = {
up: 'text-ios-green',
down: 'text-ios-red',
none: 'text-gray-900',
};
return (
<div className="text-center">
{label && (
<p className="text-[13px] text-gray-500 mb-1">{label}</p>
)}
<p className={cn(
'font-bold tabular-nums',
sizeClasses[size],
trendColors[trend]
)}>
<span className="text-base mr-1">¥</span>
{amount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</p>
</div>
);
}
```
---
## 🔄 四、交互规范代码
### 4.1 路由转场动画
```tsx
// app/template.tsx - 全局转场动画
'use client';
import { motion } from 'framer-motion';
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -20, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
{children}
</motion.div>
);
}
```
### 4.2 下拉刷新
```tsx
// hooks/usePullRefresh.ts
import { useState, useCallback } from 'react';
export function usePullRefresh(onRefresh: () => Promise<void>) {
const [refreshing, setRefreshing] = useState(false);
const handleRefresh = useCallback(async () => {
setRefreshing(true);
try {
await onRefresh();
} finally {
setRefreshing(false);
}
}, [onRefresh]);
return { refreshing, handleRefresh };
}
```
### 4.3 无限滚动
```tsx
// hooks/useInfiniteScroll.ts
import { useEffect, useRef, useCallback } from 'react';
export function useInfiniteScroll(
onLoadMore: () => void,
hasMore: boolean,
loading: boolean
) {
const observerRef = useRef<IntersectionObserver | null>(null);
const loadMoreRef = useCallback(
(node: HTMLDivElement | null) => {
if (loading) return;
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
onLoadMore();
}
});
if (node) observerRef.current.observe(node);
},
[loading, hasMore, onLoadMore]
);
return loadMoreRef;
}
```
---
## 🔗 五、API 调用规范
### 5.1 统一请求封装
```typescript
// lib/api.ts
import axios from 'axios';
import { toast } from 'sonner';
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
});
// 请求拦截
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截
api.interceptors.response.use(
(response) => {
const { code, message, data } = response.data;
if (code !== 200) {
toast.error(message || '请求失败');
return Promise.reject(new Error(message));
}
return data;
},
(error) => {
if (error.response?.status === 401) {
// Token 过期,跳转登录
window.location.href = '/login';
}
toast.error('网络错误,请稍后重试');
return Promise.reject(error);
}
);
export { api };
```
### 5.2 React Query 封装
```typescript
// hooks/useTrafficPool.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
// 获取流量池列表
export function useTrafficPools(page = 1, pageSize = 20) {
return useQuery({
queryKey: ['trafficPools', page, pageSize],
queryFn: () => api.get('/api/v1/traffic-pools', {
params: { page, pageSize }
}),
});
}
// 创建流量池
export function useCreateTrafficPool() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateTrafficPoolDTO) =>
api.post('/api/v1/traffic-pools', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['trafficPools'] });
toast.success('创建成功');
},
});
}
```
---
## 📱 六、页面模板
### 6.1 列表页模板
```tsx
// app/(main)/traffic/page.tsx
'use client';
import { useState } from 'react';
import { PageContainer } from '@/components/layout/PageContainer';
import { ListItem } from '@/components/ui/ListItem';
import { ListItemSkeleton } from '@/components/ui/skeleton';
import { useTrafficPools } from '@/hooks/useTrafficPool';
import { useInfiniteScroll } from '@/hooks/useInfiniteScroll';
export default function TrafficPoolPage() {
const [page, setPage] = useState(1);
const { data, isLoading, hasMore } = useTrafficPools(page);
const loadMoreRef = useInfiniteScroll(
() => setPage((p) => p + 1),
hasMore,
isLoading
);
return (
<PageContainer title="流量池" showBack={false}>
{/* 搜索栏 */}
<div className="sticky top-11 z-40 bg-ios-bg px-4 py-2">
<input
type="search"
placeholder="搜索流量池..."
className="w-full h-9 px-4 bg-gray-200 rounded-lg
text-[15px] placeholder:text-gray-400"
/>
</div>
{/* 列表区域 */}
<div className="mt-2">
{isLoading && !data ? (
// 首次加载:显示骨架屏
[...Array(10)].map((_, i) => <ListItemSkeleton key={i} />)
) : (
// 数据列表
<>
{data?.list.map((item) => (
<ListItem
key={item.id}
icon={<Pool className="w-4 h-4" />}
title={item.name}
subtitle={`${item.count} 条流量`}
value={${item.revenue}`}
onClick={() => router.push(`/traffic/${item.id}`)}
/>
))}
{/* 加载更多触发器 */}
<div ref={loadMoreRef} className="h-10 flex items-center justify-center">
{isLoading && <span className="text-gray-400">加载中...</span>}
{!hasMore && <span className="text-gray-400">没有更多了</span>}
</div>
</>
)}
</div>
</PageContainer>
);
}
```
### 6.2 表单页模板
```tsx
// app/(main)/scenarios/new/page.tsx
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { PageContainer } from '@/components/layout/PageContainer';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useCreateScenario } from '@/hooks/useScenario';
const schema = z.object({
name: z.string().min(2, '名称至少2个字符'),
description: z.string().optional(),
});
type FormData = z.infer<typeof schema>;
export default function NewScenarioPage() {
const { mutate, isPending } = useCreateScenario();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = (data: FormData) => {
mutate(data);
};
return (
<PageContainer
title="新建场景"
rightAction={
<button
onClick={handleSubmit(onSubmit)}
disabled={isPending}
className="text-ios-blue font-medium disabled:opacity-50"
>
{isPending ? '保存中...' : '保存'}
</button>
}
>
<form className="p-4 space-y-6">
{/* 场景名称 */}
<div>
<label className="text-[13px] text-gray-500 mb-2 block">
场景名称
</label>
<Input
{...register('name')}
placeholder="请输入场景名称"
className="h-11"
/>
{errors.name && (
<p className="text-ios-red text-[13px] mt-1">
{errors.name.message}
</p>
)}
</div>
{/* 场景描述 */}
<div>
<label className="text-[13px] text-gray-500 mb-2 block">
场景描述
</label>
<textarea
{...register('description')}
placeholder="请输入场景描述(可选)"
className="w-full h-24 px-3 py-2 bg-white border border-gray-200
rounded-lg text-[15px] resize-none"
/>
</div>
</form>
</PageContainer>
);
}
```
---
## 🔗 七、跨目录联动
### 7.1 上下游关系
```mermaid
graph LR
A[3、原型] -->|页面结构| B[4、前端]
C[5、接口] -->|API定义| B
B -->|联调需求| C
B -->|部署资源| D[8、部署]
```
### 7.2 联动指令
```
# 基于原型生成组件
@联动 原型→前端:基于 [页面结构] 生成 React 组件代码
# 基于接口生成 Hook
@联动 接口→前端:基于 [API文档] 生成 React Query Hook
# 生成完整页面
@联动 全量:基于 [需求+原型+接口] 生成完整页面代码
```
---
## 🤖 八、AI 协作指令
### 8.1 角色设定
```yaml
角色: 前端技术专家
风格:
- iOS 原生风格,像素级还原
- TypeScript 强类型
- 组件化、Hook 化
输出: 必须包含完整可运行代码
检查: 必须包含骨架屏、类型定义、错误处理
```
### 8.2 指令集
| 指令 | 功能 | 示例 |
|:---|:---|:---|
| `@生成页面` | 生成完整页面代码 | `@生成页面 流量池列表` |
| `@生成组件` | 生成单个组件 | `@生成组件 用户信息卡片` |
| `@生成Hook` | 生成自定义 Hook | `@生成Hook 分页加载` |
| `@生成类型` | 生成 TypeScript 类型 | `@生成类型 用户信息` |
| `@样式优化` | 优化 Tailwind 类名 | `@样式优化 [代码片段]` |
| `@性能优化` | 分析性能问题 | `@性能优化 列表渲染` |
---
## ⚠️ 九、注意事项
### 9.1 强制规则
```yaml
必须做:
- [ ] 所有数据加载使用 Skeleton 骨架屏
- [ ] 所有路由切换有转场动画
- [ ] 所有组件使用 TypeScript
- [ ] 所有 API 调用封装在 Hook 中
- [ ] 所有表单使用 react-hook-form + zod
禁止做:
- [ ] 使用 Spinner/Loading 代替骨架屏
- [ ] 硬编码 API 地址
- [ ] 使用 any 类型
- [ ] 在组件中直接调用 fetch
```
### 9.2 常见问题
| 问题 | 解决方案 |
|:---|:---|
| 首屏白屏 | 添加 Skeleton 骨架屏 |
| 页面闪烁 | 添加路由转场动画 |
| 类型报错 | 完善 TypeScript 类型定义 |
| 性能问题 | 使用 React.memo / useMemo |
---
> **下一步**: 前端开发完成后,拖入 `5、接口/_智能展开.md` 进行 API 联调