提示信息
# HTML → Flutter 模板转换规范
> 本文档说明如何编写「可高效转换为同伴 App Flutter 代码」的 HTML 原型,以及 AI 将其翻译成 Flutter 时必须遵守的映射规则。
---
## 一、写 HTML 原型时推荐使用的工具栈
| HTML 端 | 对应 Flutter 端 | 说明 |
|---------------------------|-------------------------------|------------------------------|
| **Tailwind CSS v3** | VelocityX + AppSpacing/AppColors | 语义对齐最佳,间距/颜色命名一致 |
| **RemixIcon 4.8(Line/Fill)** | AppIcons(remixicon 包) | 图标名称直接映射 |
| **CSS 变量 `--color-*`** | AppColors 常量 | 建议统一颜色变量命名 |
| **Google Fonts: Inter** | 系统字体(项目未自定义字体) | HTML 原型加载即可 |
---
## 二、颜色变量对照表
在 HTML `<style>` 块中声明如下 CSS 变量,AI 转 Flutter 时直接替换为 `AppColors.*`:
```css
:root {
/* 品牌色 */
--color-primary: #6366F1; /* AppColors.primary */
--color-primary-light: #EEF2FF; /* AppColors.primaryLight */
--color-secondary: #8B5CF6; /* AppColors.secondary */
/* 状态色 */
--color-error: #EF4444; /* AppColors.error */
--color-warning: #F59E0B; /* AppColors.warning */
--color-success: #10B981; /* AppColors.success */
/* 文字色 */
--color-text-primary: #111827; /* AppColors.textPrimary / context.themeTextPrimary */
--color-text-secondary: #6B7280; /* AppColors.textSecondary / context.themeTextSecondary */
--color-text-hint: #9CA3AF; /* AppColors.textHint */
/* 背景 / 边界 */
--color-bg-primary: #FFFFFF; /* AppColors.bgPrimary / context.themeBackground */
--color-bg-secondary: #F9FAFB; /* AppColors.bgSecondary / context.themeInputBg */
--color-divider: #E5E7EB; /* AppColors.divider / context.themeDivider */
--color-surface: #FFFFFF; /* context.themeSurface */
/* 渐变(主题色渐变,转为 AppGradientTheme.primaryGradient) */
--gradient-primary: linear-gradient(to right, #6366F1, #A78BFA);
}
```
---
## 三、间距 / 尺寸对照表
使用 Tailwind 的间距类时,映射到 `AppSpacing` 常量:
| Tailwind 类 | px 值 | AppSpacing / AppRadius |
|--------------------|-------|------------------------|
| `p-1` / `gap-1` | 4 | `AppSpacing.xs` |
| `p-2` / `gap-2` | 8 | `AppSpacing.sm` |
| `p-3` / `gap-3` | 12 | `AppSpacing.md` |
| `p-4` / `gap-4` | 16 | `AppSpacing.lg`(pageMargin)|
| `p-6` / `gap-6` | 24 | `AppSpacing.xl` |
| `p-8` / `gap-8` | 32 | `AppSpacing.xxl` |
| `p-12` / `gap-12` | 48 | `AppSpacing.max` |
| `rounded-sm` | 4 | `AppRadius.xs` |
| `rounded` | 8 | `AppRadius.sm` |
| `rounded-md` | 12 | `AppRadius.md` |
| `rounded-lg` | 16 | `AppRadius.lg` |
| `rounded-xl` | 24 | `AppRadius.xl` |
| `rounded-2xl` | 32 | `AppRadius.xxl` |
| `rounded-full` | ∞ | `AppRadius.max` |
---
## 四、字体排版对照表
| 角色 | HTML(Tailwind) | Flutter(AppTextStyles) |
|--------------|----------------------------------------|--------------------------------|
| 大标题 | `text-3xl font-extrabold tracking-tight` | `AppTextStyles.h1Style`(32, w800)|
| 标题二 | `text-2xl font-bold tracking-tight` | `AppTextStyles.h2Style`(24, w700)|
| 标题三 | `text-xl font-semibold` | `AppTextStyles.h3Style`(20, w600)|
| 正文大 | `text-lg font-medium` | `AppTextStyles.bodyLg`(18, w500)|
| 正文 | `text-base` | `AppTextStyles.bodyMd`(16, w400)|
| 正文小 | `text-sm` | `AppTextStyles.bodySm`(14, w400)|
| 辅助文字 | `text-xs` | `AppTextStyles.caption`(12, w400)|
| 导航标题 | `text-[17px] font-semibold` | `AppTextStyles.navTitle` |
| 按钮文字 | `text-base font-semibold` | `AppTextStyles.button` |
---
## 五、布局规则(HTML → VelocityX)
### 5.1 禁止直接翻译的标签
| HTML 写法 | ❌ 错误 Flutter 翻译 | ✅ 正确 Flutter(VelocityX) |
|--------------------------|------------------------------|--------------------------------------|
| `<div class="flex">` | `Row(...)` | `[...].hStack()` |
| `<div class="flex-col">` | `Column(...)` | `[...].vStack()` |
| `<div class="p-4">` | `Padding(EdgeInsets.all(16))`| `widget.p16()` |
| `<div class="bg-white rounded-xl shadow">` | `Container(...)` | `widget.box.white.roundedXl.shadow.make()` |
| `<button onclick>` | `ElevatedButton` | `PrimaryActionButton` 或 `AppCapsuleButton` |
### 5.2 页面级布局
```dart
// HTML:
// <div class="px-4 py-6 flex flex-col gap-4">...</div>
// Flutter:
[
widget1,
widget2,
].vStack(spacing: AppSpacing.lg)
.scrollVertical()
.pSymmetric(h: AppSpacing.lg, v: AppSpacing.xl)
```
---
## 六、组件对照速查
### 6.1 按钮
```html
<!-- HTML 原型写法 → Flutter PrimaryActionButton -->
<button class="w-full h-13 rounded-full text-white font-semibold
bg-gradient-to-r from-[#6366F1] to-[#A78BFA]
shadow-lg shadow-indigo-500/25">
立即登录
</button>
```
```dart
// Flutter 翻译
PrimaryActionButton(label: '立即登录', onPressed: () async { ... })
.wFull(context)
```
```html
<!-- 社交胶囊按钮(出线/填充/次要) -->
<span class="px-3 py-1.5 rounded-full border border-indigo-500 text-indigo-500 text-sm font-medium">
+ 关注
</span>
```
```dart
AppCapsuleButton(variant: AppCapsuleVariant.primaryOutline, onTap: () {})
// variants: primaryOutline / primaryFilled / secondaryOutline / surfaceVariant
```
### 6.2 头像
```html
<!-- HTML 原型 -->
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-indigo-500 to-violet-500
flex items-center justify-center text-white font-bold text-lg">
U
</div>
<!-- 或含图片 -->
<img class="w-12 h-12 rounded-full object-cover" src="..." />
```
```dart
// 规格:sm=32 / df=48 / lg=64 / xl=80 / xxl=120
CustomAvatar(url: user.avatar, name: user.nickname, size: AvatarSize.df)
```
### 6.3 列表单元格(AppCell)
```html
<!-- 带箭头的设置项 -->
<div class="flex items-center min-h-[52px] px-4">
<span class="flex-1 text-base text-gray-900">通知设置</span>
<span class="text-sm text-gray-500">已开启</span>
<i class="ri-arrow-right-s-line text-gray-400 ml-1"></i>
</div>
```
```dart
AppCell(title: '通知设置', value: '已开启', onTap: () {})
// 分组容器
AppCellGroup(title: '账户', children: [
AppCell(title: '昵称', value: '同伴用户', onTap: () {}),
AppCell(title: '手机号', value: '138****8888', onTap: () {}),
])
```
### 6.4 Toast 通知
```html
<!-- HTML 原型 - 顶部 pill 样式 Toast -->
<div class="fixed top-safe left-4 right-4 flex items-center gap-2
px-5 py-3 rounded-full backdrop-blur-md bg-white/85
border border-gray-200/50 shadow-xl">
<i class="ri-checkbox-circle-line text-green-500"></i>
<span class="text-base font-semibold">保存成功</span>
</div>
```
```dart
AppToast.showSuccess('保存成功');
AppToast.showError('网络连接失败,请重试');
AppToast.showWarning('此操作不可恢复');
AppToast.showInfo('功能开发中');
```
### 6.5 空状态页(AppEmptyState)
```html
<!-- 空状态 -->
<div class="flex flex-col items-center gap-4 py-16">
<div class="w-24 h-24 rounded-full bg-indigo-50 flex items-center justify-center">
<i class="ri-inbox-line text-5xl text-indigo-400"></i>
</div>
<p class="text-base font-semibold text-gray-900">暂无数据</p>
<p class="text-sm text-gray-500">这里空空如也</p>
</div>
```
```dart
// 预设类型
AppEmptyState(type: AppEmptyType.noData)
AppEmptyState(type: AppEmptyType.networkError, actionLabel: '重新加载',
onAction: () => ref.refresh(myProvider))
// 自定义
AppEmptyState(type: AppEmptyType.custom,
title: '还没有信号', subtitle: '快去发布第一条吧',
actionLabel: '去发布', onAction: () => context.push(AppRoutes.xxx))
```
### 6.6 骨架屏
```html
<!-- 骨架屏:统一用 pulse 动画 + rounded bg-gray-200 -->
<div class="animate-pulse space-y-3">
<div class="h-4 bg-gray-200 rounded-full w-3/4"></div>
<div class="h-4 bg-gray-200 rounded-full w-1/2"></div>
</div>
```
```dart
// Flutter 翻译:用 AppSkeleton 三种写法
AppSkeleton.wrap(enabled: isLoading, child: RealWidget(...))
AppSkeleton.listTile() // 列表骨架
AppSkeleton.card() // 卡片骨架
```
### 6.7 徽章(AppBadge)
```html
<!-- 消息角标 -->
<div class="relative inline-flex">
<i class="ri-notification-3-line text-2xl"></i>
<!-- 纯红点 -->
<span class="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-red-500
border-2 border-white"></span>
<!-- 数字 -->
<span class="absolute -top-1 -right-1 px-1.5 py-0.5 rounded-full bg-red-500
border-2 border-white text-white text-[9px] font-bold">5</span>
</div>
```
```dart
AppBadge(child: Icon(AppIcons.notification)) // 纯红点
AppBadge(count: 5, child: Icon(AppIcons.message)) // 数字
AppBadge(count: 100, child: someWidget) // 99+
```
### 6.8 对话框 / 底部表单
```html
<!-- 确认弹窗 -->
<div class="fixed inset-0 bg-black/40 flex items-center justify-center">
<div class="bg-white rounded-3xl p-6 mx-6 w-full max-w-sm">
<h3 class="text-xl font-semibold text-center">确认删除?</h3>
<p class="text-sm text-gray-500 text-center mt-3">删除后将无法恢复</p>
<div class="flex gap-3 mt-6">
<button class="flex-1 h-11 rounded-full bg-gray-100 text-gray-500 font-semibold">取消</button>
<button class="flex-1 h-11 rounded-full bg-red-500 text-white font-semibold">确认删除</button>
</div>
</div>
</div>
```
```dart
final ok = await AppDialog.show(
context: context,
title: '确认删除?',
content: '删除后将无法恢复',
confirmLabel: '确认删除',
isDestructive: true,
);
```
---
## 七、图标使用规范(RemixIcon 4.8)
HTML 原型中使用 RemixIcon CDN:
```html
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.8.0/fonts/remixicon.css" rel="stylesheet">
```
| 使用场景 | HTML 类名 | Flutter |
|-------------|---------------------------|---------------------------|
| 默认图标 | `ri-xxx-line` | `AppIcons.xxxLine` (line) |
| 强调/激活图标 | `ri-xxx-fill` | `AppIcons.xxx` (fill) |
| 禁止 | 任何系统图标 / `ri-xxx-2-line` | 禁止 `Icons.xxx` |
---
## 八、动画规范
```html
<!-- HTML 动画(参考入场效果) -->
<div class="animate-fadeInUp">...</div>
```
```dart
// Flutter:flutter_animate
widget.animate().fadeIn(500.ms).slideY(begin: 0.2)
// 列表动画
[...items].animate(interval: 100.ms).fade().slideX()
// 禁止手动 AnimationController
```
---
## 九、AsyncValue 三态模板
HTML 原型中要同时画出三个状态:
```html
<!-- ① Loading(骨架屏) -->
<div class="animate-pulse space-y-3">…</div>
<!-- ② Data(真实列表) -->
<ul>…</ul>
<!-- ③ Error(空状态) -->
<div class="empty-state" data-type="networkError">…</div>
```
```dart
// Flutter 翻译
ref.watch(signalProvider).when(
data: (data) => ListView(...),
loading: () => AppSkeleton.listTile(),
error: (err, stack) => AppEmptyState(
type: AppEmptyType.networkError,
actionLabel: '重新加载',
onAction: () => ref.refresh(signalProvider),
),
)
```
---
## 十、页面级 HTML 模板(完整示例)
把下面的模板贴给 AI,它就知道用哪套规范来写页面:
```html
<!DOCTYPE html>
<html lang="zh-CN" class="bg-[#F9FAFB]">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>页面名称 — 同伴App原型</title>
<!-- RemixIcon 4.8 -->
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.8.0/fonts/remixicon.css" rel="stylesheet">
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* ── 同伴 App 设计 Token ── */
:root {
--color-primary: #6366F1;
--color-primary-light: #EEF2FF;
--color-secondary: #8B5CF6;
--color-error: #EF4444;
--color-warning: #F59E0B;
--color-success: #10B981;
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--color-text-hint: #9CA3AF;
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F9FAFB;
--color-divider: #E5E7EB;
--color-surface: #FFFFFF;
--gradient-primary: linear-gradient(to right, #6366F1, #A78BFA);
}
/* 模拟手机安全区 */
body { max-width: 390px; margin: 0 auto; font-family: 'Inter', sans-serif; }
/* 状态栏占位 */
.status-bar { height: 44px; }
/* 导航栏 */
.nav-bar { height: 44px; }
/* Tab Bar */
.tab-bar { height: 83px; padding-bottom: 28px; }
</style>
<script>
// Tailwind 颜色扩展
tailwind.config = {
theme: { extend: { colors: {
primary: '#6366F1',
secondary: '#8B5CF6',
'primary-light': '#EEF2FF',
}}}
}
</script>
</head>
<body class="bg-[var(--color-bg-secondary)] min-h-screen">
<!-- ① 状态栏(安全区占位) -->
<div class="status-bar bg-white"></div>
<!-- ② 导航栏 -->
<header class="nav-bar bg-white flex items-center px-4 border-b border-[var(--color-divider)]">
<button class="p-2 -ml-2"><i class="ri-arrow-left-s-line text-2xl text-gray-700"></i></button>
<h1 class="flex-1 text-center text-[17px] font-semibold text-gray-900">页面标题</h1>
<div class="w-10"></div><!-- 占位 -->
</header>
<!-- ③ 页面内容区(可滚动) -->
<main class="flex-1 overflow-y-auto px-4 py-6 flex flex-col gap-4">
<!-- 卡片容器:对应 content.p16().box.white.roundedXl.shadow.make() -->
<div class="bg-white rounded-2xl shadow-sm p-4 flex flex-col gap-4">
<!-- 文字层级示例 -->
<h2 class="text-xl font-semibold text-gray-900">卡片标题</h2>
<p class="text-sm text-gray-500">辅助说明文字,不超过两行</p>
<!-- 头像 + 用户信息 -->
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-indigo-500 to-violet-500
flex items-center justify-center text-white font-bold text-lg flex-shrink-0">U</div>
<div class="flex flex-col">
<span class="text-base font-medium text-gray-900">用户昵称</span>
<span class="text-xs text-gray-400">@username</span>
</div>
</div>
</div>
<!-- 设置列表:对应 AppCellGroup -->
<div class="bg-white rounded-2xl overflow-hidden shadow-sm">
<!-- AppCell:对应 AppCell(title:'', value:'', onTap:()) -->
<div class="flex items-center min-h-[52px] px-4 border-b border-gray-100">
<i class="ri-user-3-line text-xl text-indigo-500 mr-3"></i>
<span class="flex-1 text-base text-gray-900">账号设置</span>
<i class="ri-arrow-right-s-line text-gray-400"></i>
</div>
<div class="flex items-center min-h-[52px] px-4">
<i class="ri-notification-3-line text-xl text-indigo-500 mr-3"></i>
<span class="flex-1 text-base text-gray-900">消息通知</span>
<!-- 开关:对应 Switch -->
<div class="w-11 h-6 bg-indigo-500 rounded-full relative">
<div class="absolute right-0.5 top-0.5 w-5 h-5 bg-white rounded-full shadow"></div>
</div>
</div>
</div>
<!-- 主操作按钮:对应 PrimaryActionButton -->
<button class="w-full h-[52px] rounded-full text-white font-semibold text-base
bg-gradient-to-r from-indigo-500 to-violet-400
shadow-lg shadow-indigo-500/25">
立即确认
</button>
<!-- 危险操作按钮:isDestructive: true -->
<button class="w-full h-[52px] rounded-full text-white font-semibold text-base bg-red-500
shadow-lg shadow-red-500/25">
退出登录
</button>
<!-- 胶囊按钮组:对应 AppCapsuleButton -->
<div class="flex gap-2 flex-wrap">
<span class="px-3 py-1.5 rounded-full border border-indigo-500 text-indigo-500 text-sm font-medium">+ 关注</span>
<span class="px-3 py-1.5 rounded-full bg-indigo-500 text-white text-sm font-medium">回关</span>
<span class="px-3 py-1.5 rounded-full border border-gray-200 text-gray-500 text-sm font-medium">已关注</span>
<span class="px-3 py-1.5 rounded-full bg-gray-100 text-gray-500 text-sm font-medium">互相关注</span>
</div>
</main>
<!-- ④ Tab Bar(如需要) -->
<nav class="tab-bar bg-white border-t border-gray-100 flex items-start justify-around px-4 pt-2 fixed bottom-0 left-0 right-0 max-w-[390px] mx-auto">
<button class="flex flex-col items-center gap-1">
<i class="ri-home-5-fill text-2xl text-indigo-500"></i>
<span class="text-[10px] font-medium text-indigo-500">首页</span>
</button>
<button class="flex flex-col items-center gap-1">
<i class="ri-compass-3-line text-2xl text-gray-400"></i>
<span class="text-[10px] text-gray-400">发现</span>
</button>
<button class="flex flex-col items-center gap-1 relative">
<i class="ri-chat-3-line text-2xl text-gray-400"></i>
<!-- 角标:对应 AppBadge(count:5) -->
<span class="absolute -top-1 right-0 px-1.5 py-0.5 rounded-full bg-red-500 border-2
border-white text-white text-[9px] font-bold">5</span>
<span class="text-[10px] text-gray-400">消息</span>
</button>
<button class="flex flex-col items-center gap-1">
<i class="ri-user-3-line text-2xl text-gray-400"></i>
<span class="text-[10px] text-gray-400">我的</span>
</button>
</nav>
</body>
</html>
```
---
## 十一、给 AI 的转换指令模板(粘贴使用)
当你想让 AI 把 HTML 原型转成 Flutter 时,在对话开头加上:
```
请根据同伴App前端规范将以下HTML原型转为Flutter代码:
- 使用 VelocityX 链式语法替代所有原生布局(Column→.vStack(),Row→.hStack(),Padding→.p16()等)
- 颜色使用 AppColors.*,间距使用 AppSpacing.*,圆角使用 AppRadius.*,字体使用 AppTextStyles.*
- 图标使用 AppIcons.*(RemixIcon),禁止 Icons.*
- 按钮使用 PrimaryActionButton 或 AppCapsuleButton,禁止 ElevatedButton/TextButton
- 动画使用 flutter_animate,禁止手动 AnimationController
- 统一 import 'package:tongban_app/shared.dart',禁止单独 import theme/ 或 widgets/ 下的文件
- AsyncValue 必须处理 data / loading / error 三态
- 每个页面文件 ≤ 300 行,每个 Widget ≤ 150 行
HTML原型如下:
[粘贴HTML]
```