Skill: kuafuai/baas (version 1.0.2)

--- name: baas-frontend description: 端到端应用开发。当用户需要创建Web应用、管理系统或进行Vibe Coding时使用，从需求到部署全流程独立完成。 allowed-tools: Bash, Read, Write, Edit, Grep, Glob metadata: {"openclaw": {"requires": {"env": ["BAAS_MANAGE_TOKEN", "BAAS_BASE_URL"]}, "primaryEnv": "BAAS_MANAGE_TOKEN"}} --- ## 一、目标 端到端流程：从用户需求到可访问的前后端应用，其中后端不需要编写代码，完全基于 AiPexBase BaaS 实现。 **端到端流程**：编写prd.md → 应用创建 → 数据库设计 → 前端对接 → 部署上线。独立完成，无需其他技能支持。 --- ## 二、开发原则 ### 2.1 自动化原则 **新建应用** 和 **迭代开发** 全程自动执行，无需向用户确认。 - **新建应用**：编写prd.md → 建应用 → 建表 → 编写前端代码 - **迭代开发**：直接修改前端代码或表结构，复用已有的 `baas-config.json` **仅部署上线，需要用户确认后再执行。**，当用户确认后，部署方法再去读取当前skill下的 `references/deploy.md` 获取。 ### 2.2 数据隔离原则 **每个项目必须创建独立的 BaaS 应用**，通过不同的 `apiKey` 实现数据隔离。 每个项目目录下有一个 `baas-config.json`，包含该项目专属的连接信息，所有 CLI 和 SDK 操作统一使用这一个配置文件。 ### 2.3 开发步骤概览 1. **后端准备**：如果前端需要用到 aipexbase 能力（数据库、AI、文件等），先创建应用和表 2. **前端页面开发**：生成前端代码（HTML/Vue/React 等），集成 aipexbase-js SDK 3. **构建发布**：询问用户后部署 --- ## 三、环境准备（首次使用前置依赖安装） 使用 `baas` 命令前，需要先安装 nvm 并配置环境： ```bash # 安装 nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 加载 nvm 环境 source ~/.bashrc # 安装 Node.js 20 并使用 nvm install 20 nvm use 20 # 全局安装 baas CLI npm i aipexbase-cli -g # 安装 zip（部署打包时需要） sudo apt-get install -y zip ``` --- ## 四、项目初始化 ### 4.1 配置文件说明 | 配置文件 | 位置 | 说明 | |---------|------|------| | **全局配置** `config.json` | 本 skill(`baas-frontend/`) 目录下 | 包含 `baseUrl` 和 `manageToken`，需要向用户询问提供，所有项目共享 | | **项目配置** `baas-config.json` | 项目目录下 | 新建应用时从全局配置复制，批量创建后添加 `apiKey` 和 `appId`；迭代开发时直接使用 | ### 4.2 判断逻辑 检查项目目录下的 `baas-config.json`： - **不存在或缺少 `apiKey`** → 新建应用 - **存在且包含 `apiKey`** → 迭代开发 ### 4.3 初始化步骤 **步骤 1：检查并初始化全局配置** 读取 `<skill目录>/config.json`，检查 `baseUrl` 和 `manageToken` 是否已配置。 如果文件不存在或缺少必要字段： 1. 询问用户提供 `baseUrl` 2. 询问用户提供 `manageToken`（管理员令牌） - 如果用户没有 `manageToken`，引导用户前往 https://www.codeflying.net 注册并登录，进入【设置和账单】页面，选择API服务，在API密钥里申请密钥，作为manageToken **步骤 2：复制配置到项目** ```bash cd <项目目录，建议放到 /tmp/时间戳/项目名称 目录下> cp <当前skill目录>/config.json ./baas-config.json ``` --- ## 五、新建应用流程 ### 5.1 生成 app-schema.json 根据用户需求，在项目目录下创建 `app-schema.json`： ```json { "name": "应用名称", "userId": "10000", "needAuth": true, "authTable": "tableName", "rlsPolicies": ["policy1", "policy2"], "tables": [ { "tableName": "表名", "description": "表说明", "columns": [ { "columnName": "id", "columnType": "INT", "columnComment": "主键ID（必需）", "isNullable": true, "isPrimary": true, "isShow": true }, { "columnName": "user_name", "columnType": "VARCHAR(255)", "columnComment": "用户名", "isNullable": true, "isPrimary": false, "isShow": true }, { "columnName": "字段名", "columnType": "字段类型", "columnComment": "字段说明", "isNullable": true, "isPrimary": false, "isShow": true } ] } ] } ``` **字段说明**： - `userId`：固定参数，写死 `10000` - `needAuth`：是否需要用户认证（true/false）。**以下场景必须将 `needAuth` 设置为 `false`**，禁止默认设为 true： - 需求中没有登录、注册、用户认证等功能 - 简化了注册流程（如无需真正注册即可使用） - 实际未调用注册、登录 API（即使需求提及用户概念，但未使用 auth 相关接口） - `authTable`：认证使用的表名 - `rlsPolicies`：行级安全策略列表，如果有就设计，没有就不用设计 - `isNullable`：**true=必填，false=非必填**（注意：字段名虽叫 isNullable，但在本系统中语义为 true 代表必填、false 代表非必填，请严格按此理解） - `columnType`：如果为 `VARCHAR` 必须指定长度，比如 `VARCHAR(255)` - **DATETIME 字段规则（强制）**：所有 `columnType` 为 `DATETIME` 或 `datetime` 的字段，`isNullable` 必须设置为 `false`（非必填）。**严禁将任何 datetime 字段的 isNullable 设为 true（必填）**，无论该字段名称是什么（包括但不限于 created_at、updated_at、start_time、end_time、publish_time 等）。datetime 字段在前端表单中不显示红色星号、不添加 required 属性 ### 5.2 字段类型参考 | 类型 | 说明 | 使用场景 | 示例 | |------|------|---------|------| | `varchar` | 短文本（≤255字符） | 姓名、标题、用户名 | "张三" | | `fullText` | 长文本 | json长串，超过512字符长度 | "这是超长内容..." | | `number` | 整数 | 年龄、数量、评分 | 25 | | `decimal` | 小数 | 价格、金额、评分 | 99.99 | | `boolean` | 布尔值 | 是否启用、是否公开 | true / false | | `date` | 日期 | 生日、发布日期 | "2024-01-01" | | `datetime` | 日期时间（通常非必填，会自动赋值） | 创建时间、更新时间 | "2024-01-01 12:00:00" | | `password` | 密码（自动加密） | 用户密码 | "******" | | `phone` | 手机号（自动验证） | 联系电话 | "13800138000" | | `email` | 邮箱（自动验证） | 电子邮件 | "[REDACTED_SECRET]" | | `images` | 图片 | 头像、封面图 | ["url1", "url2"] | | `files` | 文件 | 附件、文档 | ["url1"] | | `videos` | 视频 | 视频内容 | ["url1"] | | `quote` | 关联对象 | 外键关联 | "rec_xxx" | ### 5.3 字段定义格式 ```json { "columnName": "字段名", "columnType": "字段类型", "columnComment": "字段说明", "isNullable": true, "isPrimary": false, "isShow": true, "referenceTableName": "关联表名", "showName": "关联表显示字段" } ``` #### 字段属性说明 - **columnName**：字段名称，只能包含字母、数字和下划线 - **columnType**：字段类型（见上表） - **columnComment**：字段描述/注释 - **isNullable**：**true=必填，false=非必填**（字段名虽叫 isNullable，但语义为 true 代表必填、false 代表非必填） - **isPrimary**：是否为主键（通常为 false） - **isShow**：是否在列表中显示（true=显示，false=隐藏） - **referenceTableName**：关联表名（仅当 columnType 为 "quote" 时需要） - **showName**：关联表显示字段（仅当 columnType 为 "quote" 时需要） #### 特殊字段类型说明 - `columnType` 如果为 `VARCHAR`，必须写明字符长度，比如：`VARCHAR(255)` - **关联字段（quote 类型）**：用于建立表之间的关系，必须指定 `referenceTableName`。 - 关联字段名通常以 `_id` 结尾 - `referenceTableName` 必须是已存在的表名 - 关联查询会自动处理外键关系 ### 5.4 强制规则（必须遵守） - 在 `app-schema.json` 中，任何 `columnType` 都**禁止**写成裸 `VARCHAR` - 只能写成带长度的格式：`VARCHAR(n)`，例如 `VARCHAR(50)`、`VARCHAR(255)`、`VARCHAR(1024)` - 发现 `columnType: "VARCHAR"` 时，必须立即改成带长度格式后再执行 `batch-create` - 推荐默认长度：通用文本字段使用 `VARCHAR(255)`，短标识可用 `VARCHAR(50)`，URL 可用 `VARCHAR(1024)` - 长文本类型选择：字段预期内容超过 512 字符时，优先使用 `FULLTEXT`，不使用 `TEXT` - `TEXT` 仅用于不超过 512 字符的中短文本场景 ### 5.5 表设计注意 - **必须设计主键**：每个表必须有且仅有一个主键字段 - 字段名建议使用 `id` - 类型必须为 `INT` - `isPrimary` 必须设置为 `true` ### 5.6 表设计检查清单（必读） 在生成 `app-schema.json` 或创建表之前，确认： - [ ] 每个表都包含主键字段（INT 类型，isPrimary: true） - [ ] 字段类型符合规范（参考"字段类型说明"部分） - [ ] 所有字符串字段的 `columnType` 均为 `VARCHAR(n)`，不存在裸 `VARCHAR` - [ ] 长文本字段（超过 512 字符）已使用 `FULLTEXT`，未误用 `TEXT` - [ ] 所有 DATETIME/datetime 字段的 `isNullable` 均为 `false`（非必填），无一例外 - [ ] 认证表设计正确（如需要用户认证） ### 5.7 批量创建应用和表 创建 `app-schema.json` 之后，执行表创建： ```bash cd <项目目录> baas -c baas-config.json manage batch-create --file app-schema.json ``` ### 5.8 更新 baas-config.json 批量创建成功后，使用 Edit 工具添加返回的 `apiKey` 和 `appId`。 --- ## 六、迭代开发流程 ### 6.1 新增表 如果需要在已有应用中新增表： ```bash baas -c baas-config.json manage create-table \ --app-id "<appId>" \ --table-name "表名" \ --table-desc "表说明" \ --columns '[ { "columnName": "id", "columnType": "INT", "columnComment": "主键ID（必需）", "isNullable": true, "isPrimary": true, "isShow": true }, { "columnName": "name", "columnType": "VARCHAR(255)", "columnComment": "名称", "isNullable": true, "isPrimary": false, "isShow": true }, { "columnName": "字段名", "columnType": "字段类型", "columnComment": "字段说明", "isNullable": true, "isPrimary": false, "isShow": true } ]' ``` **注意**： - `appId` 从 `baas-config.json` 中读取 - columns 是 JSON 数组格式 - 字段类型参考"字段类型说明"部分 - 字段 `columnType` 如果为 `VARCHAR` 必须指定长度，比如 `VARCHAR(255)`，严禁使用裸 `VARCHAR` ### 6.2 插入表数据 使用项目级配置： > 导入数据的时候，如果主键已经设置 int 类型，并且为 isPrimary，那么 id 的值会从 1 开始逐渐自增。 > 必须使用 baas 进行数据插入，不要使用其他脚本等操作。 ```bash baas -c baas-config.json db insert <table_name> -d '{"columnName":"商品A","columnName":99.9,"columnName":"https://...","columnName":"电子"}' baas -c baas-config.json db insert <table_name> -d '{"columnName":"商品B","columnName":59.9,"columnName":"https://...","columnName":"服饰"}' baas -c baas-config.json db list <table_name> ``` #### 图片最佳实践 插入数据时，**务必使用真实、相关的图片**，而非随机占位图。 **推荐免费图片来源（无需 API Key）**： 1. **Wikimedia Commons API** — 最适合产品图片： ``` https://commons.wikimedia.org/w/api.php?action=query&list=search&srsearch=KEYWORD&srnamespace=6&srlimit=5&format=json ``` 2. **Picsum**（仅作备选）：`https://picsum.photos/seed/KEYWORD/400/400` --- ## 七、开发步骤 0. 先简单和用户澄清需求，并帮助用户梳理补充必要的业务逻辑，可以包含多个html页面，参考 `references/prd-template.md` 格式，完成需求文档的编写（必须严格按照prd-template.md结构编写需求文档，明确规划好页面名称、页面的功能逻辑、也页面之前的跳转关系），存放在项目目录下，以 `prd.md` 命名。这个需求文档，作为后续开发的唯一需求来源。**设计需求时，必须参考 `references/aipexbase-js-api.md` 中的"平台扩展能力模块"章节**，了解平台已提供的 AI（对话、图生文、文生图、文生语音/视频/音频）、自定义 API、物流查询、地理位置、通知（飞书/企微/邮件）等能力，在需求设计中合理利用这些现成能力，避免重复造轮子。 1. 引入 aipexbase-js SDK（CDN 或 npm） 2. 从 `baas-config.json` 读取 baseUrl 和 apiKey，使用 `aipexbase.createClient()` 初始化客户端 3. 必须启用subagent进行每个页面的独立开发，使用下方的 **Subagent 任务模板** 启动每个页面的开发任务。subagent工作完成后，无需告诉用户（不用给用户发送消息）。 ### 7.1 Subagent 任务模板 启动页面开发 subagent 时，使用以下 prompt 结构： ``` 开发 {page_name}.html 页面 ## 前置读取（必须） 编写代码前，必须先用 Read 工具读取以下文件的完整内容： 1. {skill_dir}/references/html-template.html — 学习页面结构和 SDK 初始化模板 2. {skill_dir}/references/style-guide.md — 统一样式规范（必须严格遵循） 3. {skill_dir}/references/aipexbase-js-api.md — 所有 API 操作的唯一正确用法 4. {project_dir}/prd.md — 理解页面功能需求和跳转逻辑 5. {project_dir}/app-schema.json — 理解数据结构和字段定义 6. {project_dir}/baas-config.json — 获取 API 配置（baseUrl、apiKey） ## 开发约束 - 必填字段（isNullable: true）在 label 旁标注红色星号 * - 图片/文件类型字段必须提供上传组件，禁止纯文本输入框 - 表单字段必须与 app-schema.json 中的定义一一对应 - 登录成功后必须调用 getUser() 获取完整用户信息 - 禁止使用 client.from()，必须使用 client.db.from() ## 样式约束（确保页面高级感） - 必须复制 html-template.html 中的完整 Tailwind 配置（包括动画、阴影、自定义类） - 所有可点击元素必须添加 `transition-smooth` + hover 效果 + `active:scale-[0.98]` 点击反馈 - 按钮使用 `gradient-primary` 渐变 + `shadow-soft` + `hover:shadow-glow` 发光效果 - 卡片使用 `rounded-2xl` + `shadow-soft` + `hover-lift` 悬浮上升效果 - 页面入场使用 `animate-fade-in`，模态框使用 `animate-scale-in` - 导航栏使用 `glass` 玻璃态效果 - 操作成功/失败必须使用 `showToast()` 函数反馈 - 表单提交时按钮显示 loading 状态 - 数据加载时显示骨架屏或 loading spinner ## 认证流程规范（如涉及登录功能） ### 登录流程 ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 提交表单 │───►│ auth.login()│───►│ getUser() │───►│ 跳转主页面 │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ ▼ ▼ 保存 token 到 获取完整用户信息 localStorage 用于页面展示 ### 页面加载流程（已登录检测） ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 页面加载 │───►│ 检查 token │────Y───►│ getUser() │───►│ 跳转主页面 │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │N ▼ 显示登录表单 ### 认证禁止事项 - 禁止：直接使用 login() 返回值展示用户信息 - 必须：登录后调用 getUser() 获取完整信息 - 禁止：登录页面重复登录（已登录则跳转） - 必须：登录和自动登录共用同一进入主页面逻辑 ## 完成标准 - [ ] 所有功能点按 prd.md 实现 - [ ] 表单字段与 app-schema.json 一一对应 - [ ] 必填字段有红色星号和 required 属性 - [ ] 图片字段有上传组件 - [ ] 页面可独立运行，无 JS 错误 - [ ] 页面跳转逻辑正确 - [ ] 样式符合 style-guide.md 规范（颜色、组件、布局一致） - [ ] 已添加 tailwind.config 配置 ``` ### 7.2 前置读取清单（快速参考） | 文件 | 作用 | |------|------| | `references/html-template.html` | 页面结构和 SDK 初始化模板 | | `references/style-guide.md` | 统一样式规范（颜色、组件、布局） | | `references/aipexbase-js-api.md` | API 调用方式、方法链顺序、参数格式 | | `prd.md` | 需求文档 | | `app-schema.json` | 表结构定义 | | `baas-config.json` | API 配置（baseUrl、apiKey） |

path: config.json
content: baseUrl: https://baas.kuafuai.net/baas-api

path: references/prd-template.md
content: # 需求文档（只考虑页面功能逻辑和页面之前的跳转关系即可）

## 1. 页面列表

- **index.html** - 登录页面（应用入口文件）
- **workbench.html** - 主工作台页面
- **profile.html** - 个人中心页面

---

## 2. 页面跳转逻辑

- **启动应用** → 检测登录状态 → 已登录则跳转至 `workbench.html`，未登录则进入 `index.html`
- **index.html** → 登录成功 → `workbench.html`
- **workbench.html** → 点击底部导航【我的】→ `profile.html`
- **profile.html** → 点击【工作台】→ 返回 `workbench.html`（保持上次筛选状态和滚动位置）
- **profile.html** → 退出登录 → `index.html`

---

## 3. 页面功能设计

### 3.1 index.html（登录页面）

- **核心登录表单**：用户名输入框、密码输入框、登录按钮
- **注册功能**：点击【注册账号】链接 → 弹出注册弹框（包含用户名、邮箱、密码字段）→ 提交注册 → 关闭弹框并显示成功提示
- **登录流程**：输入用户名+密码 → 点击登录 → 成功则跳转 `workbench.html`，失败则页面内显示错误提示

---

### 3.2 workbench.html（主工作台页面）

- **顶部任务概览卡片**：
  - 今日待办任务数量（点击过滤今日任务）
  - 本周已完成任务统计（点击过滤已完成任务）
  - 高优先级任务Top5提醒（点击过滤高优先级任务）

- **中部任务管理区（主从视图）**：
  - **任务列表**：显示任务卡片（标题、优先级、截止时间、完成状态复选框），支持搜索、排序筛选、上滑加载更多
  - **任务详情区**：点击任务卡片后展示完整信息（标题、描述、截止日期、优先级、标签、创建时间、修改历史）
  - **操作按钮**：编辑（弹出编辑弹框）、归档（确认弹框）、删除（二次确认弹框）、标记完成/未完成（状态实时更新）

- **底部操作栏**：
  - 【工作台】：刷新当前页面或回到顶部
  - 【添加】：弹出新建任务弹框
  - 【我的】：跳转至 `profile.html`

---

### 3.3 profile.html（个人中心页面）

- **用户信息展示区**：显示头像、昵称、邮箱，【修改资料】按钮弹出编辑资料弹框（可修改头像、昵称、邮箱，保存后实时更新）
- **设置管理区**：账号与安全设置项
- **退出登录**：点击【退出登录】按钮 → 弹出确认弹框 → 确认后清除登录状态 → 跳转至 `index.html`

path: references/deploy.md
content: ## 部署上线

> ⚠️ **部署前必须询问用户确认**，得到明确同意后再执行打包和部署操作。

### 打包策略

根据项目类型选择对应的打包方式：

**纯 HTML 项目（无构建步骤）**：

```bash
cd <项目目录>
zip -r project.zip . -x "node_modules/*" -x ".git/*" -x "*.zip"
```

**Vue/React 等需要构建的项目**：

```bash
cd <项目目录>
# 1. 执行构建
npm run build  # 或 yarn build

cd dist   # 或 cd build
zip -r ../project.zip .
```

### 执行部署

```bash
baas -c ./baas-config.json deploy ./project.zip
```

部署成功后返回：

```json
{
  "code": 0,
  "data": {
    "url": "http://localhost:8080/14217"
  },
  "message": "ok",
  "success": true
}
```

path: references/aipexbase-js-api.md
content: ## aipexbase-js API 快速参考

### 初始化客户端

```javascript
const client = aipexbase.createClient({
  baseUrl: 'http://your-server.com',
  [REDACTED_SECRET]'
});
```

### 认证模块 (client.auth)

如果应用配置了认证，即可使用登录注册。

#### 注册

```javascript
await client.auth.register({})  // 这里数据结构即是绑定的认证表数据结构
```

#### 登录

需要认证的情况下，注册完成后必须选择以下方式进行登录：

```javascript
// 密码登录（密码登录必须使用user_name、phone、email其中之一，不允许使用其它字段名）
await client.auth.login({ email: '[REDACTED_SECRET]', [REDACTED_SECRET]' });
await client.auth.login({ phone: '13800138000', [REDACTED_SECRET]' });
await client.auth.login({ user_name: 'user_name', [REDACTED_SECRET]' });

// 验证码登录
await client.auth.loginByPhoneAndCode({ phone: '13800138000', code: '123456' });
await client.auth.loginByEmailAndCode({ email: '[REDACTED_SECRET]', code: '123456' });

// 微信登录
await client.auth.loginByWeChat({ code: 'wx_code' });
await client.auth.loginByWeApp({ code: 'weapp_code' });
```

#### 获取当前用户信息

```javascript
const userInfo = await client.auth.getUser();
```

#### 登出

```javascript
await client.auth.logout();
```

### 数据库模块 (client.db)

#### 查询操作

```javascript
// 列表查询
await client.db.from('users').list();

// 带条件查询
await client.db.from('users').list()
  .eq('status', 'active')
  .gt('age', 18)
  .order('created_at', 'desc');

// 分页查询
await client.db.from('users').page()
  .page(1, 20)
  .eq('status', 'active');
// 单条查询
await client.db.from('users').get()
  .eq('id', 'rec_xxx');
```
**返回格式：**
1. list() 列表返回格式:
    ```json
    {
       "code":0,
       "data":[],
       "message":"ok",
       "success":true
   }
    ```

2. page() 分页返回格式:
    ```json
    {
        "code": 0,
        "data": {
            "records": [],
            "total": 0,
            "size": 20,
            "current": 1
        },
        "message": "ok",
        "success": true
    }
    ```
   
3. get() 详情返回格式:
    ```json
    {
        "code": 0,
        "data": {},
        "message": "ok",
        "success": true
    }
    ```

#### 过滤操作符

```javascript
.eq('field', value)              // 等于
.neq('field', value)             // 不等于
.gt('field', value)              // 大于
.gte('field', value)             // 大于等于
.lt('field', value)              // 小于
.lte('field', value)             // 小于等于
.like('field', value)            // 模糊匹配
.in('field', [v1, v2, v3])       // 在列表中
.between('field', [min, max])    // 在区间内
```

#### OR 条件

```javascript
await client.db.from('users').list()
  .eq('status', 'active')
  .or((q) => {
    q.eq('role', 'admin').eq('role', 'moderator');
  });
```

#### 排序

```javascript
.order('created_at', 'asc')      // 升序
.order('created_at', 'desc')     // 降序
```

#### 插入数据

```javascript
await client.db.from('users').insert()
  .values({ name: 'John', email: '[REDACTED_SECRET]', age: 25 });
```

返回格式：

```javascript
{
  "code": 0,
  "success": true,
  "message": "ok",
  "data": 1  // 数据的主键id值
}
```

#### 更新数据

```javascript
await client.db.from('users').update()
  .set({ status: 'inactive' })
  .eq('id', 'rec_xxx');
```

返回格式：

```javascript
{
  "code": 0,
  "success": true,
  "message": "ok",
  "data": 1  // 1-更新成功 0-未更新
}
```

#### 删除数据

```javascript
await client.db.from('users').delete()
  .eq('id', 'rec_xxx');
```

### AI 模块 (client.ai)

```javascript
// 文本对话
await client.ai.chat()
  .text('你好，介绍一下你自己')
  .prompt('你是一个友好的助手')
  .conversationId('session-123');

// 图生文
await client.ai.imageToText()
  .url('https://example.com/image.jpg')
  .text('这张图片里有什么？')
  .prompt('请详细描述');

// 文生图
await client.ai.textToImage()
  .text('一只可爱的熊猫在竹林里');

// 文本转语音
await client.ai.textToSpeech()
  .text('欢迎使用语音合成服务');

// 文本转视频（自动执行）
await client.ai.textToVideo()
  .text('一只小猫在草地上玩耍');

// 文本转音频（自动执行）
await client.ai.textToAudio()
  .prompt('一段优美的旋律');

// 即梦文生图
await client.ai.jmTextToImage()
  .prompt('可爱的小猫')
  .scale(0.5);

// 即梦生成视频
await client.ai.jmGenToVideo()
  .prompt('视频描述')
  .image('https://example.com/image.jpg');
```

### API 模块 (client.api)

```javascript
// 调用自定义 API
await client.api.call('yourApiName')
  .param('key1', 'value1')
  .param('key2', 'value2');

// 批量设置参数
await client.api.call('yourApiName')
  .params({ key1: 'value1', key2: 'value2' });

// 自定义请求头
await client.api.call('yourApiName')
  .headers({ 'X-Custom-Header': 'value' })
  .params({ data: 'value' });
```

### 其他模块

#### 物流查询

```javascript
await client.logistics.trackPackage()
  .company('yuantong')
  .trackingNumber('123456789')
  .track();

// 顺丰需要手机号
await client.logistics.trackPackage()
  .company('shunfeng')
  .trackingNumber('SF123456789')
  .phone('13800138000')
  .track();
```

#### 地理位置

```javascript
// 经纬度转地址
await client.location.locationToAddress()
  .latitude(39.9042)
  .longitude(116.4074);

// 地址转经纬度
await client.location.addressToLocation()
  .address('北京市天安门广场');

// 路线规划
await client.location.driving()
  .from(39.9042, 116.4074)
  .to(40.0076, 116.4929);

// 附近搜索
await client.location.nearby()
  .lat(39.9042)
  .lng(116.4074)
  .radius(1000)
  .keyword('餐厅');
```

#### 通知模块

```javascript
// 飞书机器人
await client.notification.feishuRobot()
  .content('消息内容');

// 企业微信机器人
await client.notification.wechatRobot()
  .content('消息内容');

// 邮件发送
await client.notification.mail()
  .to('[REDACTED_SECRET]')
  .title('邮件标题')
  .content('邮件内容')
  .params({ username: 'John', code: 'ABC123' });
```

### 返回数据格式

所有 API 返回格式统一：

```javascript
{
  "code": 0,
  "success": true,
  "message": "ok",
  "data": { ... }  // 实际数据
}
```

### 错误处理

```javascript
try {
  const result = await client.db.from('users').list();
  if (result.success) {
    // 处理成功
    console.log(result.data);
  } else {
    // 处理失败
    console.error(result.message);
  }
} catch (error) {
  // 处理异常
  console.error(error.message);
}
```

---

# 前端开发注意事项
不限技术栈（纯 HTML/Vue/React 等），默认使用纯 HTML，所有文件放到项目目录下。

### SDK 引入（CDN 方式）

```html
<script src="https://cdn.jsdelivr.net/npm/aipexbase-js/dist/aipexbase.umd.min.js"></script>
```

### 客户端初始化（必须严格按此方式）

```javascript
const client = aipexbase.createClient({
    baseUrl: '<从 baas-config.json 读取 baseUrl>',
    apiKey: '<从 baas-config.json 读取 apiKey>'
});
```

> **禁止** 使用 `new AipexBase()`、`new AipexbaseClient()` 或任何其他初始化方式。唯一正确的方式是 `aipexbase.createClient({ baseUrl, apiKey })`。

### 必填字段处理

根据 `app-schema.json` 中的 `isNullable` 字段判断是否必填（`isNullable: true` 为必填）。前端表单中：

- 必填字段的 label 旁必须显示红色星号 `*` 标识（如 `<span class="text-red-500">*</span>`）
- 必填字段的 input 必须添加 `required` 属性
- 提交表单时必须校验所有必填字段，未填写时给出提示
- **datetime 类型字段例外**：datetime 字段的 `isNullable` 在 schema 中必须为 `false`（非必填），因此前端表单中 datetime 字段不显示红色星号、不添加 `required` 属性、提交时不做必填校验

### 表单字段渲染强制规则（新增/编辑页面）

- 新增和编辑表单的字段列表必须来源于 `app-schema.json`，禁止手写一部分字段后遗漏其余字段
- 所有 `isNullable: true` 的必填字段，必须在表单中可见、可输入、可提交，严禁漏渲染
- 若字段不应由用户输入（如自增主键 `id`），可不渲染输入框，但其余业务必填字段必须展示
- 提交前必须做"字段完整性检查"：逐一比对 schema 必填字段与页面已渲染字段，缺一不可提交
- 若是多步骤表单，也必须确保所有必填字段在某个步骤中出现并被校验

#### 新增/编辑页自检清单（提交前必查）

- [ ] `app-schema.json` 中每个 `isNullable: true` 的字段都在表单中有对应输入项（自增主键除外）
- [ ] 每个必填输入项都带 `required` 或等价前端校验规则
- [ ] 缺少任一必填字段时，页面会阻止提交并提示具体字段名

### 认证流程强制规则（needAuth: true 时必须遵守）

- 登录成功后，**必须立即调用 `client.auth.getUser()` 获取当前用户的完整信息**，用返回的用户数据作为 `currentUser`，禁止直接使用 `login()` 返回的 `res.data` 作为用户信息展示
- 页面加载时，应先调用 `client.auth.getUser()` 检查是否已登录，若已登录则直接进入主页面，无需重新登录
- 用户名、角色等展示信息必须来自 `getUser()` 的返回值，确保与数据库一致
- 登录和自动登录（页面刷新恢复会话）应共用同一个"进入主页面"逻辑，避免代码重复导致展示不一致

### 图片/文件上传强制规则

- `app-schema.json` 中字段类型为图片URL（如封面图 `cover`）或 `columnComment` 含"图片"的字段，前端**必须提供文件上传组件**（`<input type="file">`），**严禁**使用纯文本输入框让用户手动填写 URL
- 上传时应显示加载状态，上传成功后显示图片预览
- token 获取方式：`localStorage.getItem('baas_token')`（SDK 登录后自动存储）
- 上传文件示例：
```
                const response = await fetch('<baseUrl>/common/upload', {
                    method: 'POST',
                    headers: {
                        'CODE_FLYING': '<此处填写apiKey，从 baas-config.json 读取 apiKey>'
                    },
                    body: formData
                });

                const result = await response.json();
                if (result.success && result.data) {
                    callback(result.data.url);
                } else {
                    showToast('上传失败', 'error');
                }
```

### 其它事项
- **严禁凭记忆或猜测编写 API 调用代码**。方法链的调用顺序、参数传递方式必须与 `aipexbase-js-api.md` 中的示例完全一致。
- 实现业务逻辑，**数据库操作必须使用 `client.db` 模块**
- **⚠️ 禁止** 使用 `client.from()`、`client.select()` 等不存在的 API。所有数据库操作必须通过 `client.db.from('表名')` 开始，后接 `.list()` / `.get()` / `.page()` / `.insert()` / `.update()` / `.delete()` 等方法。
- **⚠️ 删除和更新操作必须携带条件**：`.update()` 和 `.delete()` 调用时必须附带 `.eq()` 等条件方法指定目标记录，**严禁**执行无条件的更新或删除，防止误操作影响全表数据。

---

## 平台扩展能力模块

除数据库和认证外，SDK 还提供以下能力模块，设计需求文档时应了解这些可用能力，合理利用。

### AI 模块 (client.ai)

```javascript
// 文本对话
await client.ai.chat()
  .text('你好，介绍一下你自己')
  .prompt('你是一个友好的助手')
  .conversationId('session-123');

// 图生文
await client.ai.imageToText()
  .url('https://example.com/image.jpg')
  .text('这张图片里有什么？')
  .prompt('请详细描述');

// 文生图
await client.ai.textToImage()
  .text('一只可爱的熊猫在竹林里');

// 文本转语音
await client.ai.textToSpeech()
  .text('欢迎使用语音合成服务');

// 文本转视频（自动执行）
await client.ai.textToVideo()
  .text('一只小猫在草地上玩耍');

// 文本转音频（自动执行）
await client.ai.textToAudio()
  .prompt('一段优美的旋律');

// 即梦文生图
await client.ai.jmTextToImage()
  .prompt('可爱的小猫')
  .scale(0.5);

// 即梦生成视频
await client.ai.jmGenToVideo()
  .prompt('视频描述')
  .image('https://example.com/image.jpg');
```

### API 模块 (client.api)

```javascript
// 调用自定义 API
await client.api.call('yourApiName')
  .param('key1', 'value1')
  .param('key2', 'value2');

// 批量设置参数
await client.api.call('yourApiName')
  .params({ key1: 'value1', key2: 'value2' });

// 自定义请求头
await client.api.call('yourApiName')
  .headers({ 'X-Custom-Header': 'value' })
  .params({ data: 'value' });
```

### 物流查询模块 (client.logistics)

```javascript
await client.logistics.trackPackage()
  .company('yuantong')
  .trackingNumber('123456789')
  .track();

// 顺丰需要手机号
await client.logistics.trackPackage()
  .company('shunfeng')
  .trackingNumber('SF123456789')
  .phone('13800138000')
  .track();
```

### 地理位置模块 (client.location)

```javascript
// 经纬度转地址
await client.location.locationToAddress()
  .latitude(39.9042)
  .longitude(116.4074);

// 地址转经纬度
await client.location.addressToLocation()
  .address('北京市天安门广场');

// 路线规划
await client.location.driving()
  .from(39.9042, 116.4074)
  .to(40.0076, 116.4929);

// 附近搜索
await client.location.nearby()
  .lat(39.9042)
  .lng(116.4074)
  .radius(1000)
  .keyword('餐厅');
```

### 通知模块 (client.notification)

```javascript
// 飞书机器人
await client.notification.feishuRobot()
  .content('消息内容');

// 企业微信机器人
await client.notification.wechatRobot()
  .content('消息内容');

// 邮件发送
await client.notification.mail()
  .to('[REDACTED_SECRET]')
  .title('邮件标题')
  .content('邮件内容')
  .params({ username: 'John', code: 'ABC123' });
```

path: references/html-template.html
content: <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>应用标题</title>
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- 统一 Tailwind 配置（必须保留） -->
    <script>
    tailwind.config = {
        theme: {
            extend: {
                colors: {
                    primary: {
                        50: '#eff6ff',
                        100: '#dbeafe',
                        200: '#bfdbfe',
                        300: '#93c5fd',
                        400: '#60a5fa',
                        500: '#3b82f6',
                        600: '#2563eb',
                        700: '#1d4ed8',
                        800: '#1e40af',
                        900: '#1e3a8a',
                    },
                },
                animation: {
                    'fade-in': 'fadeIn 0.3s ease-out',
                    'slide-up': 'slideUp 0.3s ease-out',
                    'slide-down': 'slideDown 0.3s ease-out',
                    'scale-in': 'scaleIn 0.2s ease-out',
                },
                keyframes: {
                    fadeIn: {
                        '0%': { opacity: '0' },
                        '100%': { opacity: '1' },
                    },
                    slideUp: {
                        '0%': { opacity: '0', transform: 'translateY(20px)' },
                        '100%': { opacity: '1', transform: 'translateY(0)' },
                    },
                    slideDown: {
                        '0%': { opacity: '0', transform: 'translateY(-20px)' },
                        '100%': { opacity: '1', transform: 'translateY(0)' },
                    },
                    scaleIn: {
                        '0%': { opacity: '0', transform: 'scale(0.95)' },
                        '100%': { opacity: '1', transform: 'scale(1)' },
                    },
                },
                boxShadow: {
                    'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
                    'soft-lg': '0 10px 40px -10px rgba(0, 0, 0, 0.1), 0 2px 10px -2px rgba(0, 0, 0, 0.04)',
                    'glow': '0 0 20px rgba(59, 130, 246, 0.3)',
                },
            }
        }
    }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .transition-smooth { @apply transition-all duration-300 ease-out; }
            .transition-fast { @apply transition-all duration-150 ease-out; }
            .glass { @apply bg-white/80 backdrop-blur-md border border-white/20; }
            .gradient-primary { @apply bg-gradient-to-r from-primary-600 to-primary-500; }
            .gradient-subtle { @apply bg-gradient-to-br from-gray-50 to-gray-100; }
            .hover-lift { @apply transition-smooth hover:-translate-y-1 hover:shadow-soft-lg; }
            .hover-glow { @apply transition-smooth hover:shadow-glow; }
        }
    </style>
    <!-- AiPexBase SDK -->
    <script src="https://cdn.jsdelivr.net/npm/aipexbase-js/dist/aipexbase.umd.min.js"></script>
</head>
<body class="bg-gradient-subtle min-h-screen">
    <!-- 顶部导航（玻璃态） -->
    <nav class="sticky top-0 z-50 glass border-b border-gray-200/50">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16 items-center">
                <div class="flex items-center space-x-3">
                    <div class="w-8 h-8 rounded-lg gradient-primary flex items-center justify-center shadow-soft">
                        <span class="text-white font-bold text-sm">A</span>
                    </div>
                    <span class="text-xl font-semibold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
                        应用名称
                    </span>
                </div>
                <div class="flex items-center space-x-2">
                    <a href="#" class="px-4 py-2 rounded-lg text-gray-600 hover:text-primary-600 hover:bg-primary-50 transition-smooth">
                        首页
                    </a>
                </div>
            </div>
        </div>
    </nav>

    <!-- 主内容区（带入场动画） -->
    <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
        <div class="mb-8">
            <h1 class="text-3xl font-bold bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 bg-clip-text text-transparent">
                页面标题
            </h1>
            <p class="mt-2 text-gray-500">页面描述文字</p>
        </div>

        <!-- 卡片示例 -->
        <div class="bg-white rounded-2xl shadow-soft p-6 hover-lift">
            <h3 class="text-lg font-semibold text-gray-900 mb-2">卡片标题</h3>
            <p class="text-gray-600">应用内容</p>
        </div>
    </main>

    <!-- Toast 容器 -->
    <div id="toast-container" class="fixed top-4 right-4 z-50 space-y-3"></div>

    <script>
        // ===== 初始化客户端 =====
        // 注意：baseUrl 和 apiKey 需要从 baas-config.json 中读取并替换
        const client = aipexbase.createClient({
            baseUrl: '<从 baas-config.json 读取 baseUrl>',
            apiKey: '<从 baas-config.json 读取 apiKey>'
        });

        // ===== Toast 提示函数 =====
        function showToast(message, type = 'success') {
            const container = document.getElementById('toast-container');
            const toast = document.createElement('div');
            const styles = {
                success: 'bg-green-50 border-green-200 text-green-800',
                error: 'bg-red-50 border-red-200 text-red-800',
                warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
                info: 'bg-blue-50 border-blue-200 text-blue-800'
            };
            const icons = {
                success: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>',
                error: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>',
                warning: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>',
                info: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>'
            };
            toast.className = `flex items-center px-4 py-3 rounded-xl border shadow-soft-lg animate-slide-down ${styles[type]}`;
            toast.innerHTML = `
                <svg class="w-5 h-5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">${icons[type]}</svg>
                <span class="font-medium">${message}</span>
            `;
            container.appendChild(toast);
            setTimeout(() => {
                toast.style.opacity = '0';
                toast.style.transform = 'translateX(100%)';
                toast.style.transition = 'all 0.3s ease-out';
                setTimeout(() => toast.remove(), 300);
            }, 3000);
        }

        // ===== 数据库操作示例（必须使用 client.db 模块）=====
        // 禁止使用 client.from()，必须使用 client.db.from()

        // 列表查询
        // const res = await client.db.from('表名').list();

        // 带条件查询
        // const res = await client.db.from('表名').list()
        //   .eq('status', 'active')
        //   .order('created_at', 'desc');

        // 分页查询
        // const res = await client.db.from('表名').page()
        //   .page(1, 20);

        // 单条查询
        // const res = await client.db.from('表名').get()
        //   .eq('id', 'rec_xxx');

        // 插入数据
        // const res = await client.db.from('表名').insert()
        //   .values({ name: 'John', age: 25 });

        // 更新数据
        // const res = await client.db.from('表名').update()
        //   .set({ status: 'inactive' })
        //   .eq('id', 'rec_xxx');

        // 删除数据
        // const res = await client.db.from('表名').delete()
        //   .eq('id', 'rec_xxx');

        console.log('AiPexBase 客户端已初始化');
    </script>
</body>
</html>


path: references/style-guide.md
content: # 统一样式规范（高级版）

所有页面必须遵循此规范，确保视觉高级感和交互体验一致性。

---

## 一、Tailwind 配置

在每个页面的 `<head>` 中，Tailwind CDN 后添加统一配置：

```html
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
    theme: {
        extend: {
            colors: {
                primary: {
                    50: '#eff6ff',
                    100: '#dbeafe',
                    200: '#bfdbfe',
                    300: '#93c5fd',
                    400: '#60a5fa',
                    500: '#3b82f6',
                    600: '#2563eb',
                    700: '#1d4ed8',
                    800: '#1e40af',
                    900: '#1e3a8a',
                },
            },
            animation: {
                'fade-in': 'fadeIn 0.3s ease-out',
                'slide-up': 'slideUp 0.3s ease-out',
                'slide-down': 'slideDown 0.3s ease-out',
                'scale-in': 'scaleIn 0.2s ease-out',
                'spin-slow': 'spin 1.5s linear infinite',
                'pulse-soft': 'pulseSoft 2s ease-in-out infinite',
                'bounce-soft': 'bounceSoft 0.5s ease-out',
            },
            keyframes: {
                fadeIn: {
                    '0%': { opacity: '0' },
                    '100%': { opacity: '1' },
                },
                slideUp: {
                    '0%': { opacity: '0', transform: 'translateY(20px)' },
                    '100%': { opacity: '1', transform: 'translateY(0)' },
                },
                slideDown: {
                    '0%': { opacity: '0', transform: 'translateY(-20px)' },
                    '100%': { opacity: '1', transform: 'translateY(0)' },
                },
                scaleIn: {
                    '0%': { opacity: '0', transform: 'scale(0.95)' },
                    '100%': { opacity: '1', transform: 'scale(1)' },
                },
                pulseSoft: {
                    '0%, 100%': { opacity: '1' },
                    '50%': { opacity: '0.7' },
                },
                bounceSoft: {
                    '0%, 100%': { transform: 'translateY(0)' },
                    '50%': { transform: 'translateY(-5px)' },
                },
            },
            boxShadow: {
                'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
                'soft-lg': '0 10px 40px -10px rgba(0, 0, 0, 0.1), 0 2px 10px -2px rgba(0, 0, 0, 0.04)',
                'inner-soft': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)',
                'glow': '0 0 20px rgba(59, 130, 246, 0.3)',
                'glow-lg': '0 0 40px rgba(59, 130, 246, 0.4)',
            },
            backdropBlur: {
                'xs': '2px',
            },
        }
    }
}
</script>
<style type="text/tailwindcss">
    /* 全局过渡效果 */
    @layer utilities {
        .transition-smooth {
            @apply transition-all duration-300 ease-out;
        }
        .transition-fast {
            @apply transition-all duration-150 ease-out;
        }
        /* 玻璃态效果 */
        .glass {
            @apply bg-white/80 backdrop-blur-md border border-white/20;
        }
        .glass-dark {
            @apply bg-gray-900/80 backdrop-blur-md border border-gray-700/50;
        }
        /* 渐变背景 */
        .gradient-primary {
            @apply bg-gradient-to-r from-primary-600 to-primary-500;
        }
        .gradient-subtle {
            @apply bg-gradient-to-br from-gray-50 to-gray-100;
        }
        /* 悬浮效果 */
        .hover-lift {
            @apply transition-smooth hover:-translate-y-1 hover:shadow-soft-lg;
        }
        .hover-glow {
            @apply transition-smooth hover:shadow-glow;
        }
        /* 点击效果 */
        .press-effect {
            @apply active:scale-[0.98] transition-fast;
        }
    }
</style>
```

---

## 二、页面布局

### 2.1 现代化基础结构

```html
<body class="bg-gradient-subtle min-h-screen">
    <!-- 顶部导航（玻璃态） -->
    <nav class="sticky top-0 z-50 glass border-b border-gray-200/50">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16 items-center">
                <div class="flex items-center space-x-3">
                    <!-- Logo 带悬浮效果 -->
                    <div class="w-8 h-8 rounded-lg gradient-primary flex items-center justify-center shadow-soft">
                        <span class="text-white font-bold text-sm">A</span>
                    </div>
                    <span class="text-xl font-semibold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
                        应用名称
                    </span>
                </div>
                <div class="flex items-center space-x-2">
                    <!-- 导航项带悬浮效果 -->
                    <a href="#" class="px-4 py-2 rounded-lg text-gray-600 hover:text-primary-600 hover:bg-primary-50 transition-smooth">
                        首页
                    </a>
                </div>
            </div>
        </div>
    </nav>

    <!-- 主内容区（带入场动画） -->
    <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
        <!-- 页面内容 -->
    </main>
</body>
```

### 2.2 页面标题（带装饰）

```html
<div class="mb-8">
    <h1 class="text-3xl font-bold bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 bg-clip-text text-transparent">
        页面标题
    </h1>
    <p class="mt-2 text-gray-500">页面描述文字</p>
</div>
```

---

## 三、高级组件规范

### 3.1 按钮（带交互效果）

```html
<!-- 主要按钮（渐变+发光+点击效果） -->
<button class="gradient-primary text-white px-6 py-2.5 rounded-xl font-medium shadow-soft
    hover:shadow-glow hover:scale-[1.02] active:scale-[0.98]
    transition-smooth disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100">
    提交
</button>

<!-- 次要按钮（边框+填充效果） -->
<button class="bg-white text-gray-700 px-6 py-2.5 rounded-xl font-medium border-2 border-gray-200
    hover:border-primary-300 hover:text-primary-600 hover:bg-primary-50
    active:scale-[0.98] transition-smooth">
    取消
</button>

<!-- 幽灵按钮（透明+悬浮变色） -->
<button class="text-gray-600 px-4 py-2 rounded-lg font-medium
    hover:text-primary-600 hover:bg-primary-50
    active:bg-primary-100 transition-smooth">
    更多
</button>

<!-- 危险按钮 -->
<button class="bg-gradient-to-r from-red-600 to-red-500 text-white px-6 py-2.5 rounded-xl font-medium shadow-soft
    hover:shadow-[0_0_20px_rgba(239,68,68,0.3)] hover:scale-[1.02] active:scale-[0.98] transition-smooth">
    删除
</button>

<!-- 图标按钮（圆形+悬浮效果） -->
<button class="w-10 h-10 rounded-full flex items-center justify-center text-gray-500
    hover:bg-gray-100 hover:text-gray-700 active:scale-95 transition-smooth">
    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
    </svg>
</button>
```

### 3.2 表单（现代风格）

#### 输入框（带焦点动画）

```html
<div class="mb-5 group">
    <label class="block text-sm font-medium text-gray-700 mb-1.5 group-focus-within:text-primary-600 transition-colors">
        字段名称 <span class="text-red-500">*</span>
    </label>
    <div class="relative">
        <input type="text" required
            class="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl
                focus:border-primary-500 focus:ring-4 focus:ring-primary-100
                hover:border-gray-300 outline-none transition-smooth
                placeholder:text-gray-400"
            placeholder="请输入...">
        <!-- 可选：右侧图标 -->
        <div class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400">
            <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
            </svg>
        </div>
    </div>
    <!-- 可选：帮助文本 -->
    <p class="mt-1.5 text-xs text-gray-500">帮助说明文字</p>
</div>
```

#### 下拉选择

```html
<div class="relative">
    <select class="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl appearance-none
        focus:border-primary-500 focus:ring-4 focus:ring-primary-100
        hover:border-gray-300 outline-none transition-smooth cursor-pointer">
        <option value="">请选择</option>
        <option value="1">选项一</option>
    </select>
    <div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400">
        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
        </svg>
    </div>
</div>
```

#### 文本域

```html
<textarea rows="4"
    class="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl
        focus:border-primary-500 focus:ring-4 focus:ring-primary-100
        hover:border-gray-300 outline-none transition-smooth resize-none
        placeholder:text-gray-400"
    placeholder="请输入详细内容..."></textarea>
```

#### 复选框（自定义样式）

```html
<label class="flex items-center space-x-3 cursor-pointer group">
    <div class="relative">
        <input type="checkbox" class="peer sr-only">
        <div class="w-5 h-5 border-2 border-gray-300 rounded-md
            peer-checked:bg-primary-600 peer-checked:border-primary-600
            peer-focus:ring-4 peer-focus:ring-primary-100
            group-hover:border-gray-400 transition-smooth"></div>
        <svg class="absolute top-0.5 left-0.5 w-4 h-4 text-white opacity-0 peer-checked:opacity-100 transition-opacity"
            fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
        </svg>
    </div>
    <span class="text-gray-700 group-hover:text-gray-900 transition-colors">记住我</span>
</label>
```

#### 开关（Switch）

```html
<label class="relative inline-flex items-center cursor-pointer">
    <input type="checkbox" class="sr-only peer">
    <div class="w-11 h-6 bg-gray-200 rounded-full
        peer-checked:bg-primary-600 peer-focus:ring-4 peer-focus:ring-primary-100
        after:content-[''] after:absolute after:top-0.5 after:left-[2px]
        after:bg-white after:rounded-full after:h-5 after:w-5 after:shadow-soft
        after:transition-transform after:duration-300 peer-checked:after:translate-x-full
        transition-colors"></div>
    <span class="ml-3 text-gray-700">启用通知</span>
</label>
```

### 3.3 卡片（多层次阴影+悬浮效果）

```html
<!-- 基础卡片（悬浮上升） -->
<div class="bg-white rounded-2xl shadow-soft p-6 hover-lift cursor-pointer">
    <h3 class="text-lg font-semibold text-gray-900 mb-2">卡片标题</h3>
    <p class="text-gray-600">卡片内容描述</p>
</div>

<!-- 特色卡片（渐变边框） -->
<div class="relative group">
    <div class="absolute -inset-0.5 bg-gradient-to-r from-primary-600 to-purple-600 rounded-2xl opacity-0 group-hover:opacity-100 blur transition-opacity"></div>
    <div class="relative bg-white rounded-2xl p-6">
        <h3 class="text-lg font-semibold text-gray-900 mb-2">特色卡片</h3>
        <p class="text-gray-600">悬浮时显示渐变边框</p>
    </div>
</div>

<!-- 统计卡片 -->
<div class="bg-white rounded-2xl shadow-soft p-6 hover-lift">
    <div class="flex items-center justify-between">
        <div>
            <p class="text-sm text-gray-500 mb-1">总用户数</p>
            <p class="text-3xl font-bold bg-gradient-to-r from-primary-600 to-primary-400 bg-clip-text text-transparent">
                12,345
            </p>
        </div>
        <div class="w-12 h-12 rounded-xl gradient-primary flex items-center justify-center shadow-soft">
            <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
            </svg>
        </div>
    </div>
    <div class="mt-4 flex items-center text-sm">
        <span class="text-green-600 font-medium flex items-center">
            <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
            </svg>
            +12.5%
        </span>
        <span class="text-gray-400 ml-2">较上月</span>
    </div>
</div>
```

### 3.4 列表项（滑动效果）

```html
<div class="bg-white rounded-2xl shadow-soft overflow-hidden divide-y divide-gray-100">
    <!-- 列表项（左滑显示操作） -->
    <div class="group relative">
        <div class="p-4 flex items-center justify-between hover:bg-gray-50 transition-smooth cursor-pointer">
            <div class="flex items-center space-x-4">
                <div class="w-10 h-10 rounded-full gradient-primary flex items-center justify-center text-white font-medium shadow-soft">
                    J
                </div>
                <div>
                    <h4 class="font-medium text-gray-900 group-hover:text-primary-600 transition-colors">John Doe</h4>
                    <p class="text-sm text-gray-500">[REDACTED_SECRET]</p>
                </div>
            </div>
            <svg class="w-5 h-5 text-gray-400 group-hover:text-primary-600 group-hover:translate-x-1 transition-all"
                fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
            </svg>
        </div>
    </div>
</div>
```

### 3.5 表格（现代风格）

```html
<div class="bg-white rounded-2xl shadow-soft overflow-hidden">
    <table class="min-w-full">
        <thead>
            <tr class="bg-gradient-to-r from-gray-50 to-gray-100/50">
                <th class="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">名称</th>
                <th class="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">状态</th>
                <th class="px-6 py-4 text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">操作</th>
            </tr>
        </thead>
        <tbody class="divide-y divide-gray-100">
            <tr class="hover:bg-primary-50/50 transition-colors group">
                <td class="px-6 py-4">
                    <div class="flex items-center">
                        <div class="w-8 h-8 rounded-lg bg-primary-100 flex items-center justify-center mr-3">
                            <span class="text-primary-600 font-medium text-sm">A</span>
                        </div>
                        <span class="font-medium text-gray-900">项目名称</span>
                    </div>
                </td>
                <td class="px-6 py-4">
                    <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700">
                        <span class="w-1.5 h-1.5 rounded-full bg-green-500 mr-1.5 animate-pulse"></span>
                        运行中
                    </span>
                </td>
                <td class="px-6 py-4 text-right">
                    <button class="text-gray-400 hover:text-primary-600 transition-colors p-1">
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"/>
                        </svg>
                    </button>
                </td>
            </tr>
        </tbody>
    </table>
</div>
```

### 3.6 徽章/状态标签

```html
<!-- 状态点+文字 -->
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700">
    <span class="w-1.5 h-1.5 rounded-full bg-green-500 mr-1.5 animate-pulse"></span>
    在线
</span>

<!-- 渐变徽章 -->
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-primary-500 to-purple-500 text-white shadow-soft">
    新功能
</span>

<!-- 边框徽章 -->
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border-2 border-primary-200 text-primary-600 bg-primary-50">
    进行中
</span>
```

### 3.7 模态框（带入场动画）

```html
<!-- 遮罩层 -->
<div id="modal" class="fixed inset-0 z-50 hidden">
    <!-- 背景遮罩（点击关闭） -->
    <div class="fixed inset-0 bg-black/50 backdrop-blur-sm animate-fade-in" onclick="closeModal()"></div>

    <!-- 模态框内容（缩放入场） -->
    <div class="fixed inset-0 flex items-center justify-center p-4">
        <div class="bg-white rounded-2xl shadow-soft-lg max-w-md w-full max-h-[90vh] overflow-hidden animate-scale-in">
            <!-- 头部 -->
            <div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
                <h3 class="text-lg font-semibold text-gray-900">标题</h3>
                <button onclick="closeModal()"
                    class="w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-smooth">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
                    </svg>
                </button>
            </div>
            <!-- 内容 -->
            <div class="px-6 py-4 overflow-y-auto">
                <!-- 表单或内容 -->
            </div>
            <!-- 底部 -->
            <div class="px-6 py-4 border-t border-gray-100 flex justify-end space-x-3 bg-gray-50/50">
                <button onclick="closeModal()"
                    class="px-4 py-2 rounded-xl text-gray-700 hover:bg-gray-100 transition-smooth">
                    取消
                </button>
                <button class="gradient-primary text-white px-6 py-2 rounded-xl shadow-soft hover:shadow-glow transition-smooth">
                    确认
                </button>
            </div>
        </div>
    </div>
</div>

<script>
function openModal() {
    document.getElementById('modal').classList.remove('hidden');
    document.body.style.overflow = 'hidden';
}
function closeModal() {
    document.getElementById('modal').classList.add('hidden');
    document.body.style.overflow = '';
}
</script>
```

### 3.8 消息提示（Toast）

```html
<!-- Toast 容器 -->
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-3"></div>

<script>
function showToast(message, type = 'success') {
    const container = document.getElementById('toast-container');
    const toast = document.createElement('div');

    const styles = {
        success: 'bg-green-50 border-green-200 text-green-800',
        error: 'bg-red-50 border-red-200 text-red-800',
        warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
        info: 'bg-blue-50 border-blue-200 text-blue-800'
    };

    const icons = {
        success: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>',
        error: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>',
        warning: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>',
        info: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>'
    };

    toast.className = `flex items-center px-4 py-3 rounded-xl border shadow-soft-lg animate-slide-down ${styles[type]}`;
    toast.innerHTML = `
        <svg class="w-5 h-5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            ${icons[type]}
        </svg>
        <span class="font-medium">${message}</span>
    `;

    container.appendChild(toast);

    setTimeout(() => {
        toast.style.opacity = '0';
        toast.style.transform = 'translateX(100%)';
        toast.style.transition = 'all 0.3s ease-out';
        setTimeout(() => toast.remove(), 300);
    }, 3000);
}
</script>
```

### 3.9 空状态（插画风格）

```html
<div class="text-center py-16 animate-fade-in">
    <div class="w-24 h-24 mx-auto mb-6 rounded-full bg-gradient-to-br from-primary-100 to-primary-50 flex items-center justify-center">
        <svg class="w-12 h-12 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
        </svg>
    </div>
    <h3 class="text-lg font-medium text-gray-900 mb-2">暂无数据</h3>
    <p class="text-gray-500 mb-6 max-w-sm mx-auto">还没有任何记录，点击下方按钮创建第一条</p>
    <button class="gradient-primary text-white px-6 py-2.5 rounded-xl shadow-soft hover:shadow-glow hover:scale-[1.02] transition-smooth">
        <span class="flex items-center">
            <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
            </svg>
            立即创建
        </span>
    </button>
</div>
```

### 3.10 加载状态

```html
<!-- 页面加载（全屏） -->
<div class="fixed inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center z-50">
    <div class="flex flex-col items-center">
        <div class="w-12 h-12 rounded-full border-4 border-primary-200 border-t-primary-600 animate-spin"></div>
        <p class="mt-4 text-gray-600 font-medium">加载中...</p>
    </div>
</div>

<!-- 按钮加载态 -->
<button disabled class="gradient-primary text-white px-6 py-2.5 rounded-xl opacity-80 cursor-wait flex items-center">
    <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
    </svg>
    处理中...
</button>

<!-- 骨架屏 -->
<div class="animate-pulse space-y-4">
    <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 class="h-32 bg-gray-200 rounded-xl"></div>
</div>
```

### 3.11 图片上传（拖拽+预览）

```html
<div class="mb-5">
    <label class="block text-sm font-medium text-gray-700 mb-1.5">上传图片</label>
    <div id="dropZone"
        class="relative border-2 border-dashed border-gray-300 rounded-2xl p-8 text-center
            hover:border-primary-400 hover:bg-primary-50/50 transition-smooth cursor-pointer
            group"
        onclick="document.getElementById('fileInput').click()"
        ondragover="event.preventDefault(); this.classList.add('border-primary-500', 'bg-primary-50')"
        ondragleave="this.classList.remove('border-primary-500', 'bg-primary-50')"
        ondrop="handleDrop(event)">

        <input type="file" id="fileInput" accept="image/*" class="hidden" onchange="handleUpload(this)">

        <div id="uploadPlaceholder" class="space-y-3">
            <div class="w-16 h-16 mx-auto rounded-full bg-gray-100 group-hover:bg-primary-100 flex items-center justify-center transition-colors">
                <svg class="w-8 h-8 text-gray-400 group-hover:text-primary-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
                </svg>
            </div>
            <div>
                <p class="text-gray-700 font-medium">点击或拖拽上传</p>
                <p class="text-sm text-gray-500 mt-1">支持 JPG、PNG、GIF，最大 5MB</p>
            </div>
        </div>

        <div id="uploadPreview" class="hidden">
            <img id="previewImage" class="max-h-48 mx-auto rounded-xl shadow-soft">
            <button type="button" onclick="event.stopPropagation(); clearUpload()"
                class="mt-3 text-sm text-red-600 hover:text-red-700 font-medium">
                移除图片
            </button>
        </div>
    </div>
</div>

<script>
function handleUpload(input) {
    const file = input.files[0];
    if (file) showPreview(file);
}

function handleDrop(event) {
    event.preventDefault();
    event.target.classList.remove('border-primary-500', 'bg-primary-50');
    const file = event.dataTransfer.files[0];
    if (file && file.type.startsWith('image/')) {
        document.getElementById('fileInput').files = event.dataTransfer.files;
        showPreview(file);
    }
}

function showPreview(file) {
    const reader = new FileReader();
    reader.onload = (e) => {
        document.getElementById('previewImage').src = e.target.result;
        document.getElementById('uploadPlaceholder').classList.add('hidden');
        document.getElementById('uploadPreview').classList.remove('hidden');
    };
    reader.readAsDataURL(file);
}

function clearUpload() {
    document.getElementById('fileInput').value = '';
    document.getElementById('uploadPlaceholder').classList.remove('hidden');
    document.getElementById('uploadPreview').classList.add('hidden');
}
</script>
```

---

## 四、交互反馈规范

### 4.1 点击反馈
所有可点击元素必须添加：
- `transition-smooth` 或 `transition-fast`
- `hover:` 状态变化（颜色、背景、阴影）
- `active:scale-[0.98]` 或 `active:scale-95` 点击缩放

### 4.2 加载状态
- 按钮提交时显示 loading spinner
- 列表加载时显示骨架屏
- 页面切换时显示全屏加载

### 4.3 操作反馈
- 成功操作：绿色 Toast
- 失败操作：红色 Toast
- 表单错误：输入框红色边框 + 错误提示

### 4.4 动画时机
| 场景 | 动画 | 时长 |
|------|------|------|
| 页面入场 | `animate-fade-in` | 0.3s |
| 模态框入场 | `animate-scale-in` | 0.2s |
| Toast 入场 | `animate-slide-down` | 0.3s |
| 列表项入场 | `animate-slide-up` | 0.3s |
| hover 效果 | `transition-smooth` | 0.3s |
| 点击反馈 | `transition-fast` | 0.15s |

---

## 五、响应式与间距

### 5.1 响应式网格

```html
<!-- 自适应卡片网格 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
    <!-- 卡片 -->
</div>
```

### 5.2 间距规范

| 用途 | 推荐值 |
|------|--------|
| 页面内边距 | `px-4 sm:px-6 lg:px-8` |
| 页面上下边距 | `py-8` |
| 区块间距 | `mb-8` |
| 卡片内边距 | `p-6` |
| 卡片圆角 | `rounded-2xl` |
| 表单字段间距 | `mb-5` |
| 按钮组间距 | `space-x-3` |

---

## 六、字体规范

| 用途 | 类名 |
|------|------|
| 页面大标题 | `text-3xl font-bold` + 渐变色 |
| 区块标题 | `text-lg font-semibold text-gray-900` |
| 卡片标题 | `text-base font-medium text-gray-900` |
| 正文 | `text-sm text-gray-600` |
| 辅助文字 | `text-xs text-gray-500` |
| 链接 | `text-primary-600 hover:text-primary-700 transition-colors` |

---

## 七、前端 DOM 渲染安全强制规则（必须遵守）

### 7.1 状态节点禁止放入可覆盖容器

加载态、空状态、错误态等状态节点（如 loadingState、emptyState、errorState）**禁止写在会被 `innerHTML`、`replaceChildren()`、`append()` 整体覆盖的列表容器内部**。

状态节点必须与列表容器保持**同级兄弟关系**，通过 `style.display` 或 `classList` 控制显示/隐藏。

❌ 错误（状态节点在列表容器内部，会被 innerHTML 覆盖）：
```html
<div id="leaderboardList">
    <div id="loadingState">加载中...</div>
    <div id="emptyState">暂无数据</div>
</div>
```

✅ 正确（状态节点与列表容器同级）：
```html
<div id="loadingState">加载中...</div>
<div id="leaderboardList"></div>
<div id="emptyState">暂无数据</div>
```

### 7.2 显示/隐藏控制示例

```javascript
// 加载中
loadingState.style.display = 'block';
leaderboardList.style.display = 'none';
emptyState.style.display = 'none';

// 有数据
loadingState.style.display = 'none';
leaderboardList.style.display = 'block';
leaderboardList.innerHTML = renderList(data); // 只影响列表容器内部
emptyState.style.display = 'none';

// 无数据
loadingState.style.display = 'none';
leaderboardList.style.display = 'none';
emptyState.style.display = 'block';
```

---

## 八、图片/资源字段处理强制规则（必须遵守）

### 8.1 数据结构规则

所有图片字段必须使用**资源数组格式**，禁止使用字符串格式。

✅ 正确（资源数组格式）：
```javascript
images = [{ url: "https://xxx.jpg" }]
avatar = [{ url: "https://xxx.jpg" }]
cover  = [{ url: "https://xxx.jpg" }]
```

❌ 错误（字符串格式，严禁使用）：
```javascript
image = "https://xxx.jpg"
avatar = "https://xxx.jpg"
```

### 8.2 上传规则

上传接口返回成功后，只提取图片 URL，保存到数据库时保持数组格式：

```javascript
// 单图上传 — 提取 URL
const imageUrl = result?.data?.url || '';

// 保存到数据库 — 数组格式
updateData.avatar = [{ url: imageUrl }];

// 多图上传
updateData.images = fileList.map(url => ({ url }));
```

### 8.3 读取显示规则

读取图片必须安全访问，img 标签必须提供默认占位图：

```javascript
// 安全读取
const imageUrl = data?.images?.[0]?.url || '';
const avatarUrl = user?.avatar?.[0]?.url || '';

// img 标签设置，必须带兜底占位图
img.src = imageUrl || 'https://via.placeholder.com/300';
```

### 8.4 编辑回显规则

打开编辑弹窗时，必须正确回显已上传的图片：

```javascript
previewUrl = currentData?.images?.[0]?.url || '';
```

### 8.5 删除和替换规则

- 替换图片时覆盖第一个元素：`avatar = [{ url: newUrl }]`
- 多图删除时按数组索引删除，**不允许破坏数组结构**

### 8.6 稳定性规则

- 所有 DOM 操作前必须判空
- 所有上传失败必须有 toast 提示
- 所有 `null` / `undefined` 必须兼容处理
- 禁止改变现有 UI 风格
- 禁止影响已有页面初始化流程


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (HIGH)