ionic 教程

Ionic 教程

Ionic 是一个开源移动端 UI 框架,让开发者用 Web 技术(HTML/CSS/JS)构建跨平台移动应用,一套代码可以打包成 iOS、Android 应用,也能运行在 Web 上。

本教程内容

本教程从 Ionic 的安装与项目创建讲起,系统覆盖常用组件:头部底部、按钮、列表、卡片、表单控件(toggle/checkbox/单选框/select/range)、选项卡、网格布局、图标、上拉菜单、下拉刷新、模态窗口、导航、侧栏菜单、对话框、滚动条等。

Ionic 的核心

  • 跨平台:iOS / Android / Web 一套代码
  • 原生体验:精心设计的移动端 UI 组件
  • Web 技术栈:HTML/CSS/JS,上手门槛低
  • 框架集成:支持 Angular、React、Vue
  • Capacitor:调用原生设备能力(相机/定位等)

版本说明

Ionic 历经多版本迭代:1.x(基于 AngularJS)、3.x、4.x+(框架无关)。本教程以现代 Ionic(Angular 版本)为例,组件概念各版本通用。

学习建议

建议掌握 HTML/CSS/JS 基础与任一前端框架(Angular/React/Vue)。跟着教程动手创建项目、实践每个组件,快速上手移动端开发。

ionic 安装

Ionic 安装

安装 Ionic 开发环境:需要 Node.js 与 Ionic CLI,即可创建、运行和构建跨平台应用。

环境要求

# 1. Node.js 18+(LTS 推荐)
node --version
npm --version

# 2. 可选: Git
# 3. 移动端调试: Android Studio / Xcode(打包时)

安装 Ionic CLI

# 全局安装
npm install -g @ionic/cli

# 验证
ionic --version
ionic --help

# 国内加速(可选)
npm config set registry https://registry.npmmirror.com

创建新项目

# 交互式创建
ionic start myApp

# 指定模板与框架
ionic start myApp blank --type=angular
ionic start myApp tabs --type=angular
ionic start myApp sidemenu --type=angular

# 常用模板:
# blank(空白) / tabs(选项卡)
# sidemenu(侧栏菜单) / super(综合)

# 框架选项:
# --type=angular / react / vue

运行项目

# 进入项目
cd myApp

# Web 浏览器运行(开发调试)
ionic serve

# 启动后浏览器打开 localhost:8100
# 支持热更新,改代码即时生效

项目结构

myApp/
├── src/
│   ├── app/           # 应用根组件与路由
│   ├── pages/         # 页面组件
│   ├── components/    # 自定义组件
│   ├── services/      # 服务(数据/业务)
│   ├── theme/         # 全局样式
│   └── index.html
├── capacitor.config.ts  # 原生构建配置
├── package.json
└── angular.json

常用 CLI 命令

# 生成页面
ionic generate page login

# 生成组件/服务
ionic generate component my-comp
ionic generate service api

# 构建
ionic build                # Web 构建
ionic build --prod         # 生产构建

# 添加平台
ionic capacitor add android
ionic capacitor add ios

# 打开原生项目
ionic capacitor open android

常见问题

# 1. 安装慢: 换国内镜像
# 2. 权限错误: sudo 或修复 npm 目录权限
# 3. 版本冲突: 清缓存重装
npm cache clean --force
rm -rf node_modules package-lock.json
npm install

# 4. 端口占用: ionic serve --port 8101

环境就绪后,就可以创建应用并开始使用 Ionic 组件了。

ionic 创建 APP

Ionic 创建 APP

本节手把手创建一个完整的 Ionic 应用:项目初始化、添加页面、配置导航,跑通"创建→运行"全流程。

1. 创建项目

# 创建空白 Angular 项目
ionic start myTodo blank --type=angular
cd myTodo

# 启动开发服务器
ionic serve
# 浏览器打开 http://localhost:8100

2. 创建页面

# 生成待办页面
ionic generate page todo

# 生成后的文件:
# src/pages/todo/
# ├── todo.page.html    # 页面模板
# ├── todo.page.ts      # 逻辑
# ├── todo.page.scss    # 样式
# └── todo.page.spec.ts # 测试

# 路由会自动注册
# src/app/app.routes.ts

3. 编写页面

<!-- todo.page.html -->
<ion-header>
  <ion-toolbar>
    <ion-title>待办清单</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item *ngFor="let item of todos">
      <ion-checkbox [checked]="item.done"></ion-checkbox>
      <ion-label>{{ item.text }}</ion-label>
    </ion-item>
  </ion-list>

  <ion-item>
    <ion-input [(ngModel)]="newTodo"
               placeholder="输入新待办"></ion-input>
    <ion-button (click)="addTodo()">添加</ion-button>
  </ion-item>
</ion-content>
// todo.page.ts
import { Component } from "@angular/core";

@Component({
  selector: "app-todo",
  templateUrl: "./todo.page.html",
})
export class TodoPage {
  todos: { text: string; done: boolean }[] = [];
  newTodo = "";

  addTodo() {
    const text = this.newTodo.trim();
    if (text) {
      this.todos.push({ text, done: false });
      this.newTodo = "";
    }
  }
}

4. 配置首页跳转

// app.routes.ts
import { Routes } from "@angular/router";

export const routes: Routes = [
  { path: "", redirectTo: "todo", pathMatch: "full" },
  { path: "todo", loadChildren: () =>
      import("./pages/todo/todo.page").then(
          m => m.TodoPageModule) },
];

5. 运行与调试

# 浏览器
ionic serve

# 真机预览(Ionic DevApp 或浏览器设备模式)
# F12 → 设备工具栏,模拟手机

# 构建原生包(需 Capacitor + 平台工具)
ionic build
ionic capacitor add android
ionic capacitor run android

开发小技巧

# 热更新: 保存即刷新,无需手动
# 控制台: 浏览器 F12 查看错误
# 模拟数据: 先用假数据开发 UI
# 组件库: 随时查官方文档
# https://ionicframework.com/docs/components

# 常见错误:
# - 路由未配置 → 404
# - 组件未导入 → 模板报错
# - 服务未提供 → DI 错误

至此你已创建并运行了第一个 Ionic 应用。接下来逐个学习组件,丰富你的应用界面。

ionic 头部与底部

Ionic 头部与底部

头部(Header)与底部(Footer)是移动页面最常见的布局区域:头部放标题与导航,底部放操作按钮或选项卡。

基础头部

<ion-header>
  <ion-toolbar>
    <ion-title>页面标题</ion-title>
  </ion-toolbar>
</ion-header>

头部常用内容

<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-back-button></ion-back-button>
      <ion-button>菜单</ion-button>
    </ion-buttons>

    <ion-title>我的页面</ion-title>

    <ion-buttons slot="end">
      <ion-button>
        <ion-icon name="search"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>

  <!-- 第二行工具栏(搜索栏等) -->
  <ion-toolbar>
    <ion-searchbar></ion-searchbar>
  </ion-toolbar>
</ion-header>

# slot 位置:
# start: 左侧 / end: 右侧
# 标题自动居中

底部工具栏

<ion-footer>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-button>取消</ion-button>
    </ion-buttons>

    <ion-buttons slot="end">
      <ion-button color="primary">保存</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-footer>

样式定制

// 全局主题变量(src/theme/variables.scss)
:root {
  --ion-color-primary: #3880ff;
  --ion-toolbar-background: #3880ff;
  --ion-toolbar-color: #ffffff;
}

// 或单独页面覆盖(页面 scss)
ion-toolbar {
  --background: #f5f5f5;
  --color: #333;
}

其他工具栏特性

# 1. 透明工具栏
<ion-toolbar translucent>...</ion-toolbar>

# 2. 多彩标题
<ion-title color="danger">红色标题</ion-title>

# 3. 分组按钮
<ion-buttons>
  <ion-button>A</ion-button>
  <ion-button>B</ion-button>
</ion-buttons>

# 4. 无障碍/固定标题
<ion-title size="large">大标题</ion-title>

完整页面骨架

<ion-header>
  <ion-toolbar>
    <ion-title>首页</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <!-- 页面主体内容 -->
  <p>这里是内容区域</p>
</ion-content>

<ion-footer>
  <ion-toolbar>
    <ion-title size="small">© 2026 版权信息</ion-title>
  </ion-toolbar>
</ion-footer>

头部底部是页面的"骨架"。掌握工具栏与 slot 布局,页面结构一目了然。

ionic 按钮

Ionic 按钮

按钮是用户交互的核心元素。Ionic 提供丰富的按钮样式:颜色、形状、尺寸、加载状态等。

基础按钮

<!-- 默认实心按钮 -->
<ion-button>默认按钮</ion-button>

<!-- 空心(outline) -->
<ion-button fill="outline">描边按钮</ion-button>

<!-- 透明(clear) -->
<ion-button fill="clear">文字按钮</ion-button>

<!-- 链接样式按钮 -->
<ion-button href="/home">跳转链接</ion-button>

颜色

<!-- Ionic 预定义颜色 -->
<ion-button color="primary">主色</ion-button>
<ion-button color="secondary">次色</ion-button>
<ion-button color="success">成功</ion-button>
<ion-button color="warning">警告</ion-button>
<ion-button color="danger">危险</ion-button>
<ion-button color="dark">深色</ion-button>
<ion-button color="light">浅色</ion-button>
<ion-button color="medium">中灰</ion-button>

形状与尺寸

<!-- 圆角 -->
<ion-button shape="round">圆角按钮</ion-button>

<!-- 尺寸 -->
<ion-button size="small">小</ion-button>
<ion-button size="default">默认</ion-button>
<ion-button size="large">大</ion-button>

<!-- 全宽 -->
<ion-button expand="full">全宽按钮</ion-button>
<ion-button expand="block">块级按钮</ion-button>

图标按钮

<!-- 仅图标 -->
<ion-button>
  <ion-icon name="star"></ion-icon>
</ion-button>

<!-- 图标+文字 -->
<ion-button>
  <ion-icon name="trash" slot="start"></ion-icon>
  删除
</ion-button>

<!-- 常用图标名: -->
<!-- add / search / home / settings -->
<!-- camera / trash / heart / person -->
<!-- send / download / share -->

事件处理

<!-- Angular 事件绑定 -->
<ion-button (click)="onClick()">点击</ion-button>

<ion-button (ionFocus)="onFocus()">聚焦</ion-button>
<ion-button (ionBlur)="onBlur()">失焦</ion-button>

// TS
onClick() {
  console.log("按钮被点击");
}

加载与禁用状态

<!-- 禁用 -->
<ion-button disabled>不可用</ion-button>
<ion-button [disabled]="!canSave">保存</ion-button>

<!-- 加载状态(Angular) -->
<ion-button [disabled]="loading">
  <ion-spinner name="crescent" *ngIf="loading">
  </ion-spinner>
  <span *ngIf="!loading">提交</span>
  <span *ngIf="loading">提交中...</span>
</ion-button>

自定义样式

/* 页面 scss */
.my-button {
  --background: linear-gradient(135deg,
      #667eea, #764ba2);   /* 渐变背景 */
  --border-radius: 12px;   /* 圆角 */
  --box-shadow: 0 4px 12px rgba(0,0,0,0.2);
  --color: white;          /* 文字颜色 */
  font-weight: bold;
}

<ion-button class="my-button">渐变按钮</ion-button>

/* 常用 CSS 变量:
   --background / --color / --border-radius
   --box-shadow / --padding-start/end
   --height / --font-size */

按钮组

<!-- 并排按钮 -->
<ion-buttons>
  <ion-button fill="outline">取消</ion-button>
  <ion-button>确认</ion-button>
</ion-buttons>

<!-- 水平排列(flex) -->
<div style="display:flex; gap:8px;">
  <ion-button expand="block" fill="outline">
    次要
  </ion-button>
  <ion-button expand="block">主要</ion-button>
</div>

按钮是界面交互的主力。掌握颜色/形状/图标/状态组合,配合事件绑定,就能做出直观的操作入口。

ionic 列表

Ionic 列表

列表是移动应用最常见的组件,用于展示数据集合:设置项、消息、商品等。Ionic 的 ion-list 与 ion-item 高度可定制。

基础列表

<ion-list>
  <ion-item>
    <ion-label>项目一</ion-label>
  </ion-item>
  <ion-item>
    <ion-label>项目二</ion-label>
  </ion-item>
  <ion-item>
    <ion-label>项目三</ion-label>
  </ion-item>
</ion-list>

动态数据列表(Angular)

<ion-list>
  <ion-item *ngFor="let user of users">
    <ion-avatar slot="start">
      <img [src]="user.avatar">
    </ion-avatar>
    <ion-label>
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    </ion-label>
  </ion-item>
</ion-list>

// TS
users = [
  { name: "张三", email: "z@e.com",
    avatar: "..." },
  ...
];

列表项布局(slot)

<ion-item>
  <ion-icon name="star" slot="start"></ion-icon>
  <ion-label>文字标签</ion-label>
  <ion-icon name="chevron-forward"
            slot="end"></ion-icon>
</ion-item>

<!-- slot 位置: -->
<!-- start(左侧) / end(右侧) -->
<!-- 可放: 图标/头像/缩略图/按钮 -->

<!-- 缩略图 -->
<ion-thumbnail slot="start">
  <img src="...">
</ion-thumbnail>

可点击与滑动项

<!-- 可点击 -->
<ion-item button (click)="open(user)">
  <ion-label>{{ user.name }}</ion-label>
</ion-item>

<!-- 滑动操作(删除/收藏) -->
<ion-item-sliding>
  <ion-item>
    <ion-label>滑动我</ion-label>
  </ion-item>

  <ion-item-options side="end">
    <ion-item-option color="danger"
      (click)="delete(item)">
      <ion-icon name="trash" slot="start"></ion-icon>
      删除
    </ion-item-option>
  </ion-item-options>
</ion-item-sliding>

分组与分隔

<!-- 分组标题 -->
<ion-list>
  <ion-list-header>设置</ion-list-header>
  <ion-item>账号</ion-item>
  <ion-item>隐私</ion-item>
</ion-list>

<!-- 插入线控制 -->
<ion-list lines="full">  <!-- full/inset/none -->
  <ion-item>带完整分割线</ion-item>
</ion-list>

<!-- 单项控制 -->
<ion-item lines="none">无分割线</ion-item>

虚拟滚动(大数据列表)

<!-- 上万条数据用虚拟滚动 -->
<ion-virtual-scroll [items]="items">
  <ion-item *virtualItem="let item">
    <ion-label>{{ item.name }}</ion-label>
  </ion-item>
</ion-virtual-scroll>

# 或 Angular 的 CDK virtual scroll
# 大数据量时性能关键,避免渲染全部 DOM

列表中的表单控件

<ion-item>
  <ion-label position="stacked">用户名</ion-label>
  <ion-input [(ngModel)]="name"></ion-input>
</ion-item>

<ion-item>
  <ion-label>接收通知</ion-label>
  <ion-toggle [(ngModel)]="notify"></ion-toggle>
</ion-item>

<ion-item>
  <ion-label>公开资料</ion-label>
  <ion-checkbox [(ngModel)]="public"></ion-checkbox>
</ion-item>

列表是数据展示的骨架。掌握 item 布局、滑动操作与分组,配合 *ngFor 渲染数据,列表页轻松搞定。

ionic 卡片

Ionic 卡片

卡片(Card)用于展示独立的信息块:文章、商品、用户资料等。卡片自带圆角、阴影与内边距,视觉聚焦。

基础卡片

<ion-card>
  <ion-card-header>
    <ion-card-title>卡片标题</ion-card-title>
    <ion-card-subtitle>副标题</ion-card-subtitle>
  </ion-card-header>

  <ion-card-content>
    这里是卡片的主要内容文字。
  </ion-card-content>
</ion-card>

带图片的卡片(文章示例)

<ion-card>
  <img src="https://example.com/cover.jpg"
       alt="封面">

  <ion-card-header>
    <ion-card-title>Python 入门指南</ion-card-title>
    <ion-card-subtitle>编程教程 · 5 分钟阅读</ion-card-subtitle>
  </ion-card-header>

  <ion-card-content>
    从零开始学习 Python 的完整教程...
  </ion-card-content>

  <ion-button fill="clear">阅读全文
    <ion-icon name="arrow-forward"
              slot="end"></ion-icon>
  </ion-button>
</ion-card>

动态卡片列表

<!-- 商品列表 -->
<ion-card *ngFor="let product of products">
  <img [src]="product.image">
  <ion-card-header>
    <ion-card-title>{{ product.name }}</ion-card-title>
    <ion-card-subtitle>¥{{ product.price }}</ion-card-subtitle>
  </ion-card-header>
  <ion-card-content>
    <ion-button expand="block" color="primary"
                (click)="addToCart(product)">
      加入购物车
    </ion-button>
  </ion-card-content>
</ion-card>

卡片内的操作

<ion-card>
  <ion-item>
    <ion-avatar slot="start">
      <img src="avatar.png">
    </ion-avatar>
    <ion-label>
      <h2>作者名</h2>
      <p>发布时间</p>
    </ion-label>
  </ion-item>

  <img src="content.jpg">

  <ion-card-content>
    分享的内容文字...
  </ion-card-content>

  <ion-row>
    <ion-col>
      <ion-button fill="clear">
        <ion-icon name="heart"></ion-icon> 点赞
      </ion-button>
    </ion-col>
    <ion-col>
      <ion-button fill="clear">
        <ion-icon name="chatbubble"></ion-icon> 评论
      </ion-button>
    </ion-col>
  </ion-row>
</ion-card>

样式定制

/* 页面 scss */
ion-card {
  --background: #ffffff;
  --color: #333;
  border-radius: 16px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
  margin: 16px;
}

/* 主题变量 */
:root {
  --ion-card-background: #ffffff;
  --ion-card-color: #333;
}

/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
  ion-card {
    --background: #1e1e1e;
    --color: #eee;
  }
}

布局技巧

<!-- 双列卡片(瀑布流风格) -->
<ion-grid>
  <ion-row>
    <ion-col size="6" *ngFor="let item of items">
      <ion-card>...</ion-card>
    </ion-col>
  </ion-row>
</ion-grid>

<!-- 卡片可点击 -->
<ion-card (click)="openDetail(item)">...</ion-card>

<!-- 卡片按钮跳转 -->
<ion-card routerLink="/detail/{{ item.id }}">
  ...
</ion-card>

卡片让信息"自成一体"。配合网格布局与动态渲染,商品、文章、用户卡片信手拈来。

ionic 表单和输入框

Ionic 表单和输入框

表单是用户输入的门户。Ionic 提供完善的表单控件与样式:输入框、文本域、标签、校验反馈等。

基础输入框

<!-- 带浮动标签 -->
<ion-item>
  <ion-label position="floating">邮箱</ion-label>
  <ion-input type="email"
             [(ngModel)]="email"></ion-input>
</ion-item>

<!-- 标签位置: -->
<!-- floating(浮动) / stacked(堆叠) -->
<!-- fixed(固定) / 默认(行内) -->

输入类型

<!-- 文本 -->
<ion-input type="text"></ion-input>

<!-- 密码(可加显示切换) -->
<ion-input type="password"></ion-input>

<!-- 数字 -->
<ion-input type="number"></ion-input>

<!-- 邮箱/电话/URL -->
<ion-input type="email"></ion-input>
<ion-input type="tel"></ion-input>

<!-- 日期/时间 -->
<ion-input type="date"></ion-input>
<ion-input type="time"></ion-input>

<!-- 搜索 -->
<ion-searchbar [(ngModel)]="keyword"
               placeholder="搜索"></ion-searchbar>

<!-- 多行文本 -->
<ion-textarea rows="4"
              placeholder="请输入内容">
</ion-textarea>

完整登录表单

<form [formGroup]="loginForm"
      (ngSubmit)="onLogin()">

  <ion-item>
    <ion-label position="floating">邮箱</ion-label>
    <ion-input type="email"
               formControlName="email">
    </ion-input>
  </ion-item>
  <ion-text color="danger"
    *ngIf="loginForm.get('email')?.invalid
      && loginForm.get('email')?.touched">
    请输入有效邮箱
  </ion-text>

  <ion-item>
    <ion-label position="floating">密码</ion-label>
    <ion-input type="password"
               formControlName="password">
    </ion-input>
  </ion-item>

  <ion-button expand="block" type="submit"
              [disabled]="loginForm.invalid">
    登录
  </ion-button>
</form>
// TS: Angular 响应式表单
import { FormGroup, FormBuilder,
         Validators } from "@angular/forms";

loginForm: FormGroup;

constructor(private fb: FormBuilder) {
  this.loginForm = this.fb.group({
    email: ["", [Validators.required,
                 Validators.email]],
    password: ["", [Validators.required,
                    Validators.minLength(6)]],
  });
}

onLogin() {
  console.log(this.loginForm.value);
}

输入框辅助元素

<!-- 清除按钮 -->
<ion-input clearInput placeholder="可清除">
</ion-input>

<!-- 右侧图标 -->
<ion-item>
  <ion-label position="stacked">密码</ion-label>
  <ion-input [type]="showPwd ? 'text' : 'password'">
  </ion-input>
  <ion-button fill="clear" slot="end"
              (click)="showPwd = !showPwd">
    <ion-icon [name]="showPwd ? 'eye-off'
                             : 'eye'">
    </ion-icon>
  </ion-button>
</ion-item>

校验反馈样式

<!-- 无效状态 -->
<ion-item class="ion-invalid ion-touched">
  <ion-label position="floating">必填项</ion-label>
  <ion-input required></ion-input>
</ion-item>

<!-- 自定义错误消息 -->
<ion-note color="danger">此字段为必填</ion-note>

<!-- 成功提示 -->
<ion-note color="success">✓ 输入有效</ion-note>

/* 校验成功/失败时自动应用类 */
.ion-valid { border-color: green; }
.ion-invalid { border-color: red; }

表单最佳实践

# 1. 用 Angular 响应式表单(可测试性强)
# 2. 校验规则声明式配置(Validators)
# 3. 错误消息及时反馈(blur 后显示)
# 4. 提交按钮禁用直到表单有效
# 5. 键盘适配: 输入框多时滚动
# 6. 数字输入注意键盘类型

# 键盘类型(移动端):
<ion-input type="tel" inputmode="numeric">

# 避免自动大写:
<ion-input autocomplete="off"
          autocapitalize="none">

表单是应用的"数据入口"。掌握输入框类型、标签布局与响应式校验,登录、注册、设置等表单页不再是难题。

ionic toggle

Ionic Toggle

Toggle(开关)是二选一控件:开/关、启用/禁用。常见于设置页面,比复选框更直观。

基础用法

<!-- 简单开关 -->
<ion-toggle></ion-toggle>

<!-- 默认状态 -->
<ion-toggle checked></ion-toggle>
<ion-toggle [checked]="isEnabled"></ion-toggle>

<!-- 禁用 -->
<ion-toggle disabled></ion-toggle>

双向绑定(Angular)

<ion-toggle [(ngModel)]="wifiEnabled">
</ion-toggle>

<!-- 事件 -->
<ion-toggle [(ngModel)]="notify"
            (ionChange)="onNotifyChange($event)">
</ion-toggle>

// TS
wifiEnabled = true;

onNotifyChange(event: any) {
  console.log("通知开关:", event.detail.checked);
}

设置页典型用法

<ion-list>
  <ion-item>
    <ion-label>Wi-Fi</ion-label>
    <ion-toggle [(ngModel)]="wifi"></ion-toggle>
  </ion-item>

  <ion-item>
    <ion-label>蓝牙</ion-label>
    <ion-toggle [(ngModel)]="bluetooth"
                color="primary">
    </ion-toggle>
  </ion-item>

  <ion-item>
    <ion-label>夜间模式</ion-label>
    <ion-toggle [(ngModel)]="darkMode"
                color="dark">
    </ion-toggle>
  </ion-item>

  <ion-item>
    <ion-label>省电模式</ion-label>
    <ion-toggle [(ngModel)]="batterySaver"
                color="success"
                [disabled]="!batteryOK">
    </ion-toggle>
  </ion-item>
</ion-list>

颜色与样式

<!-- 颜色 -->
<ion-toggle color="primary"></ion-toggle>
<ion-toggle color="success"></ion-toggle>
<ion-toggle color="danger"></ion-toggle>
<ion-toggle color="warning"></ion-toggle>

<!-- 自定义样式 -->
ion-toggle {
  --handle-background: #ffffff;
  --handle-background-checked: #ffffff;
  --track-background: #ccc;
  --track-background-checked: #3880ff;
}

<!-- 尺寸调整 -->
ion-toggle {
  --handle-width: 24px;
  --handle-height: 24px;
}

响应式表单中使用

<!-- 用 formControlName 而非 ngModel -->
<form [formGroup]="settingsForm">
  <ion-item>
    <ion-label>自动更新</ion-label>
    <ion-toggle formControlName="autoUpdate">
    </ion-toggle>
  </ion-item>
</form>

// TS
settingsForm = this.fb.group({
  autoUpdate: [true],
  notifications: [false],
});

// 读取值
this.settingsForm.value.autoUpdate

自定义开关图标(内容插槽)

<!-- 开关内显示图标 -->
<ion-toggle>
  <ion-icon slot="start" name="sunny"></ion-icon>
  <ion-icon slot="end" name="moon"></ion-icon>
</ion-toggle>

<!-- 或用 enableOnOffLabels 显示文字 -->
<ion-toggle enableOnOffLabels></ion-toggle>

注意事项

# 1. 与 label 搭配使用,描述开关含义
# 2. ionChange 事件获取最新值
# 3. 切换关联状态(如省电模式影响其他项)
# 4. 持久化设置(本地存储)

// 保存设置
saveSettings() {
  localStorage.setItem("settings",
    JSON.stringify(this.settingsForm.value));
}

Toggle 是设置页的标配控件。配合双向绑定与颜色定制,交互直观、状态清晰。

ionic checkbox

Ionic Checkbox

Checkbox(复选框)用于多项选择:勾选/取消。适合"多项选择"与"协议确认"等场景。

基础用法

<!-- 单个复选框 -->
<ion-checkbox></ion-checkbox>

<!-- 默认勾选 -->
<ion-checkbox checked></ion-checkbox>
<ion-checkbox [checked]="agree"></ion-checkbox>

<!-- 禁用 -->
<ion-checkbox disabled></ion-checkbox>

双向绑定

<ion-checkbox [(ngModel)]="acceptTerms">
</ion-checkbox>

<!-- 事件 -->
<ion-checkbox (ionChange)="onChange($event)">
</ion-checkbox>

// TS
onChange(event: any) {
  console.log("选中:", event.detail.checked);
}

列表中的复选框(多选场景)

<ion-list>
  <ion-item *ngFor="let item of items">
    <ion-checkbox [checked]="item.selected"
      (ionChange)="toggleItem(item, $event)">
    </ion-checkbox>
    <ion-label>{{ item.name }}</ion-label>
  </ion-item>
</ion-list>

// TS
items = [
  { name: "苹果", selected: false },
  { name: "香蕉", selected: true },
  { name: "橙子", selected: false },
];

toggleItem(item: any, event: any) {
  item.selected = event.detail.checked;
}

getSelected() {
  return this.items
    .filter(i => i.selected)
    .map(i => i.name);
}

复选框组(Angular 表单)

<!-- 响应式表单数组 -->
<form [formGroup]="form">
  <ion-item *ngFor="let opt of options">
    <ion-label>{{ opt.label }}</ion-label>
    <ion-checkbox [formControlName]="opt.key">
    </ion-checkbox>
  </ion-item>
</form>

// TS
form = this.fb.group({
  optionA: [false],
  optionB: [true],
  optionC: [false],
});

颜色与样式

<ion-checkbox color="primary"></ion-checkbox>
<ion-checkbox color="success"></ion-checkbox>
<ion-checkbox color="danger"></ion-checkbox>

/* 自定义 */
ion-checkbox {
  --background: #fff;
  --background-checked: #3880ff;
  --checkmark-color: #fff;
  --border-radius: 4px;
  --size: 22px;
}

<!-- 圆形 -->
<ion-checkbox shape="circle"></ion-checkbox>

使用场景

# 1. 用户协议确认
<ion-item>
  <ion-checkbox [(ngModel)]="agree"></ion-checkbox>
  <ion-label>我已阅读并同意<b>服务条款</b></ion-label>
</ion-item>
<ion-button [disabled]="!agree" expand="block">
  注册
</ion-button>

# 2. 批量选择
# 全选/取消全选
selectAll(checked: boolean) {
  this.items.forEach(i => i.selected = checked);
}

# 3. 多选筛选(筛选标签/分类)

与 Toggle 的选择

# Checkbox: 多项选择(可勾多个)
# Toggle: 单一开关(开/关)
# 单选: 用 ion-radio-group

# 场景对照:
# 多项爱好选择 → checkbox
# 是否接收通知 → toggle
# 单选支付方式 → radio

# 注意: 列表场景 checkbox 常与
# ion-item + ion-label 搭配使用

Checkbox 负责"多选"。掌握双向绑定与列表组合,配合表单数组,多选功能信手拈来。

ionic 单选框

Ionic 单选框

单选框(Radio)用于互斥选择:多选一。常与列表结合,如支付方式、性别、选项设置。

基础用法

<!-- 单选组 -->
<ion-radio-group [(ngModel)]="selected">
  <ion-item>
    <ion-label>选项A</ion-label>
    <ion-radio value="a"></ion-radio>
  </ion-item>
  <ion-item>
    <ion-label>选项B</ion-label>
    <ion-radio value="b"></ion-radio>
  </ion-item>
</ion-radio-group>

默认值与事件

<!-- 默认选中 -->
<ion-radio-group [(ngModel)]="gender">
  <ion-item>
    <ion-label>男</ion-label>
    <ion-radio value="male"></ion-radio>
  </ion-item>
  <ion-item>
    <ion-label>女</ion-label>
    <ion-radio value="female"></ion-radio>
  </ion-item>
</ion-radio-group>

// TS
gender = "male";

<!-- 变更事件 -->
<ion-radio-group [(ngModel)]="gender"
                 (ionChange)="onGenderChange($event)">

onGenderChange(event: any) {
  console.log("选择:", event.detail.value);
}

典型场景: 支付方式

<ion-radio-group [(ngModel)]="payMethod">
  <ion-item>
    <ion-radio value="alipay" slot="start">
    </ion-radio>
    <ion-label>
      <h2>支付宝</h2>
      <p>支持余额与花呗</p>
    </ion-label>
  </ion-item>

  <ion-item>
    <ion-radio value="wechat" slot="start">
    </ion-radio>
    <ion-label>
      <h2>微信支付</h2>
      <p>推荐使用</p>
    </ion-label>
  </ion-item>
</ion-radio-group>

// TS
payMethod = "alipay";

响应式表单

<form [formGroup]="form">
  <ion-radio-group formControlName="plan">
    <ion-item>
      <ion-label>基础版</ion-label>
      <ion-radio value="basic"></ion-radio>
    </ion-item>
    <ion-item>
      <ion-label>专业版</ion-label>
      <ion-radio value="pro"></ion-radio>
    </ion-item>
  </ion-radio-group>
</form>

// TS
form = this.fb.group({
  plan: ["pro"],
});

颜色与样式

<!-- 颜色 -->
<ion-radio color="primary"></ion-radio>
<ion-radio color="success"></ion-radio>

/* 自定义 */
ion-radio {
  --color: #888;               /* 未选中 */
  --color-checked: #3880ff;    /* 选中 */
}

<!-- 禁用 -->
<ion-radio value="x" disabled></ion-radio>
<ion-radio-group disabled>...</ion-radio-group>

<!-- 内容放置 -->
<!-- slot="start" 圆圈在左 -->
<!-- 默认圆圈在右 -->

动态选项渲染

<!-- 从数据生成 -->
<ion-radio-group [(ngModel)]="choice">
  <ion-item *ngFor="let opt of options">
    <ion-label>{{ opt.label }}</ion-label>
    <ion-radio [value]="opt.value"
               [disabled]="opt.disabled">
    </ion-radio>
  </ion-item>
</ion-radio-group>

// TS
options = [
  { label: "红色", value: "red" },
  { label: "绿色", value: "green" },
  { label: "蓝色", value: "blue", disabled: true },
];
choice = "red";

注意事项

# 1. radio 必须放在 radio-group 内(互斥)
# 2. value 唯一,用于标识选项
# 3. 默认值要与某选项 value 匹配
# 4. 移动端点击区域要大(item 内)

# 与 checkbox 对比:
# radio: 多选一(互斥)
# checkbox: 多选多
# toggle: 单一开/关

Radio 负责"互斥单选"。掌握 radio-group 与列表组合,支付方式、选项设置等场景轻松实现。

ionic range

Ionic Range

Range(滑块)用于在区间内选择值:音量、亮度、价格区间等。支持单滑块与双滑块(区间)。

基础用法

<!-- 基本滑块 -->
<ion-range></ion-range>

<!-- 指定范围与步长 -->
<ion-range min="0" max="100" step="10">
</ion-range>

<!-- 默认值 -->
<ion-range [value]="50"></ion-range>

<!-- 禁用 -->
<ion-range disabled></ion-range>

双向绑定

<!-- 音量调节 -->
<ion-range [(ngModel)]="volume"
           min="0" max="100">
</ion-range>
<p>音量: {{ volume }}</p>

<!-- 事件 -->
<ion-range (ionChange)="onVolumeChange($event)">
</ion-range>

// TS
volume = 70;

onVolumeChange(event: any) {
  console.log("音量:", event.detail.value);
}

价格区间(双滑块)

<ion-range dualKnobs="true"
           min="0" max="1000" step="50"
           [(ngModel)]="priceRange">
</ion-range>
<p>价格区间:
  ¥{{ priceRange.lower }} - ¥{{ priceRange.upper }}
</p>

// TS
priceRange = { lower: 100, upper: 800 };

带图标的滑块

<!-- 音量 -->
<ion-item>
  <ion-icon name="volume-low"
            slot="start"></ion-icon>
  <ion-range [(ngModel)]="volume"
             min="0" max="100">
  </ion-range>
  <ion-icon name="volume-high"
            slot="end"></ion-icon>
</ion-item>

<!-- 亮度 -->
<ion-item>
  <ion-icon name="sunny" slot="start"></ion-icon>
  <ion-range [(ngModel)]="brightness">
  </ion-range>
</ion-item>

显示数值标签

<!-- 使用 pin 显示当前值 -->
<ion-range pin="true" [(ngModel)]="value">
</ion-range>

<!-- 自定义标签内容 -->
<ion-range pin="true" [(ngModel)]="rating">
  <ion-label slot="start">0</ion-label>
  <ion-label slot="end">5</ion-label>
</ion-range>

<!-- 评分场景 -->
<ion-range min="0" max="5" step="1"
           [(ngModel)]="rating">
</ion-range>
<p>评分: {{ rating }} / 5</p>

响应式表单

<form [formGroup]="form">
  <ion-item>
    <ion-label>期望薪资(万)</ion-label>
    <ion-range formControlName="salary"
               min="5" max="50" step="1">
    </ion-range>
  </ion-item>
</form>

// TS
form = this.fb.group({
  salary: [15],
});

// 读取
this.form.value.salary

样式定制

/* 自定义颜色与尺寸 */
ion-range {
  --bar-background: #ddd;
  --bar-background-active: #3880ff;
  --knob-background: #3880ff;
  --knob-size: 24px;
  --height: 40px;
}

/* 颜色变体 */
<ion-range color="success"></ion-range>
<ion-range color="danger"></ion-range>

/* 渐变轨道(自定义) */
ion-range::part(track) {
  background: linear-gradient(90deg,
      #667eea, #764ba2);
}

应用场景

# 1. 音量/亮度调节
# 2. 价格/年龄筛选区间
# 3. 评分打分
# 4. 设置数值(字体大小/透明度)
# 5. 视频播放进度(配合视频元素)

// 视频进度示例
video.currentTime = seekValue;

注意事项

# 1. min/max 默认 0-100
# 2. step 控制步进(整型值用 step=1)
# 3. dualKnobs 双滑块返回 {lower, upper}
# 4. 拖动中频繁触发 ionChange
#    需要防抖处理(性能敏感场景)

// 防抖示例
onChange(e: any) {
  clearTimeout(this.timer);
  this.timer = setTimeout(() => {
    // 最终值处理
  }, 200);
}

Range 滑块让"区间选择"直观友好。掌握单/双滑块与双向绑定,音量、筛选、评分场景一键实现。

ionic select

Ionic Select

Select(选择器)用于从列表中选值,移动端弹出选择界面(底部弹层或弹窗)。适合枚举选择:地区、分类、语言等。

基础用法

<!-- 基本选择器 -->
<ion-select placeholder="请选择城市">
  <ion-select-option value="bj">北京</ion-select-option>
  <ion-select-option value="sh">上海</ion-select-option>
  <ion-select-option value="gz">广州</ion-select-option>
</ion-select>

双向绑定

<ion-item>
  <ion-label>所在城市</ion-label>
  <ion-select [(ngModel)]="city"
              placeholder="选择城市">
    <ion-select-option *ngFor="let c of cities"
                       [value]="c.id">
      {{ c.name }}
    </ion-select-option>
  </ion-select>
</ion-item>

// TS
cities = [
  { id: "bj", name: "北京" },
  { id: "sh", name: "上海" },
];
city = "bj";

// 事件
<ion-select (ionChange)="onCityChange($event)">

onCityChange(event: any) {
  console.log("选择:", event.detail.value);
}

多选

<!-- multiple: 多选 -->
<ion-select multiple="true"
           placeholder="选择标签">
  <ion-select-option value="python">
    Python
  </ion-select-option>
  <ion-select-option value="js">
    JavaScript
  </ion-select-option>
</ion-select>

// TS
selectedTags: string[] = [];

// 多选返回数组
onChange(event: any) {
  this.selectedTags = event.detail.value;
}

<!-- 显示选中数量 -->
<ion-select multiple="true"
           [interfaceOptions]="{
             header: '选择标签'
           }">

选择界面模式(interface)

<!-- 底部弹层(默认) -->
<ion-select interface="action-sheet">
  ...
</ion-select>

<!-- 弹窗 -->
<ion-select interface="alert">
  ...
</ion-select>

<!-- 弹出式下拉 -->
<ion-select interface="popover">
  ...
</ion-select>

<!-- 自定义界面选项 -->
<ion-select [interfaceOptions]="{
    header: '请选择',
    subHeader: '城市列表',
    translucent: true
  }">

响应式表单

<form [formGroup]="form">
  <ion-item>
    <ion-label>学历</ion-label>
    <ion-select formControlName="education"
                placeholder="选择学历">
      <ion-select-option value="high">
        高中
      </ion-select-option>
      <ion-select-option value="bachelor">
        本科
      </ion-select-option>
      <ion-select-option value="master">
        硕士
      </ion-select-option>
    </ion-select>
  </ion-item>
</form>

// TS
form = this.fb.group({
  education: ["bachelor"],
});

样式与提示

<!-- 颜色 -->
<ion-select color="primary"></ion-select>

<!-- 取消/确定按钮文字 -->
<ion-select [cancelText]="'取消'"
            [okText]="'确定'">

<!-- 选中值显示 -->
<!-- 默认显示选项文本 -->
<!-- 多选用分隔符 -->
<ion-select multiple="true"
           [selectedText]="selectedTextFn">

selectedTextFn(values: any[]) {
  return values.length + " 个已选";
}

禁用与只读

<!-- 禁用 -->
<ion-select disabled></ion-select>

<!-- 条件禁用 -->
<ion-select [disabled]="!canEdit">

<!-- 隐藏箭头 -->
<ion-select [interfaceOptions]="{
  cssClass: 'no-arrow'
}">

注意事项

# 1. 在 ion-item 中与 label 搭配
# 2. 选项数据量大时考虑搜索(自定义弹层)
# 3. 移动端默认底部弹层体验最佳
# 4. 级联选择(省市区)需手动处理

// 级联示例
onProvinceChange(e: any) {
  this.cities = this.allData[e.detail.value] || [];
  this.city = null;   // 重置下级
}

Select 让枚举选择规范统一。掌握单选/多选与界面模式,配合级联逻辑,表单选择交互完整落地。

ionic tab(选项卡)

Ionic Tab(选项卡)

Tab(选项卡)是移动应用的底部导航标准:首页、发现、消息、我的。Ionic 提供完整的选项卡导航方案。

创建选项卡项目

# 用 tabs 模板创建
ionic start myApp tabs --type=angular

# 自动生成:
# src/pages/tab1/ tab2/ tab3/
# src/tabs/tabs.page.html  (容器)

选项卡容器

<!-- tabs.page.html -->
<ion-tabs>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="home">
      <ion-icon name="home"></ion-icon>
      <ion-label>首页</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="discover">
      <ion-icon name="compass"></ion-icon>
      <ion-label>发现</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="messages">
      <ion-icon name="chatbubbles"></ion-icon>
      <ion-label>消息</ion-label>
      <ion-badge color="danger">3</ion-badge>
    </ion-tab-button>

    <ion-tab-button tab="profile">
      <ion-icon name="person"></ion-icon>
      <ion-label>我的</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

路由配置

// tabs 路由
const routes: Routes = [
  {
    path: "tabs",
    component: TabsPage,
    children: [
      {
        path: "home",
        loadChildren: () =>
          import("./pages/home/home.page")
            .then(m => m.HomePageModule)
      },
      { path: "", redirectTo: "home",
        pathMatch: "full" },
    ],
  },
  { path: "", redirectTo: "tabs", pathMatch: "full" },
];

徽标与角标

<!-- 未读消息角标 -->
<ion-tab-button tab="messages">
  <ion-icon name="chatbubbles"></ion-icon>
  <ion-label>消息</ion-label>
  <ion-badge color="danger">{{ unread }}</ion-badge>
</ion-tab-button>

<!-- 小红点(无数字) -->
<ion-badge color="danger"
           class="dot-badge"></ion-badge>

/* 小红点样式 */
.dot-badge {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  padding: 0;
  position: absolute;
  top: 4px;
  right: 12px;
}

顶部选项卡

<!-- slot="top" 放顶部 -->
<ion-tabs>
  <ion-tab-bar slot="top">
    <ion-tab-button tab="all">
      <ion-label>全部</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="unread">
      <ion-label>未读</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

<!-- 左侧选项卡 -->
<ion-tab-bar slot="start">
  <!-- 竖排按钮 -->
</ion-tab-bar>

动态切换与样式

<!-- 当前选中高亮 -->
<ion-tab-button tab="home"
                [selected]="currentTab === 'home'">

<!-- 自定义颜色 -->
<ion-tab-bar color="primary">...</ion-tab-bar>
<ion-tab-bar [translucent]="true">...</ion-tab-bar>

/* 选中态样式 */
ion-tab-button.tab-selected {
  color: #3880ff;
}

/* 隐藏 Tab 切换时的过渡动画 */
ion-tabs { --transition-duration: 0ms; }

程序化切换

// TS: 切换到指定 tab
import { IonTabs } from "@ionic/angular";

@ViewChild(IonTabs) tabs!: IonTabs;

switchTab(name: string) {
  this.tabs.select(name);
}

// 或路由导航
router.navigate(["/tabs", "profile"]);

注意事项

# 1. tab 按钮 tab 属性与路由对应
# 2. 每个 tab 是独立页面(懒加载)
# 3. 底部导航常驻(内容在中间滚动)
# 4. 隐藏某 tab 用 *ngIf 或样式
# 5. 深链接: /tabs/home 可直达

# 常见问题:
# - 图标不显示: 确认 name 有效
# - tab 点击无跳转: 检查路由配置
# - 徽标更新: 数据驱动,用服务共享状态

Tab 是 App 的"一级导航"。掌握容器/路由/角标/切换,主流 App 的底部导航结构信手拈来。

ionic 网格(Grid)

Ionic 网格(Grid)

网格(Grid)是基于 12 列的系统,用于响应式布局。Ionic 的 ion-grid/ion-row/ion-col 让页面自适应不同屏幕。

基本结构

<ion-grid>
  <ion-row>
    <ion-col>列 1</ion-col>
    <ion-col>列 2</ion-col>
    <ion-col>列 3</ion-col>
  </ion-row>
</ion-grid>
<!-- 等宽三列,自动换行 -->

列宽控制(12 列系统)

<ion-grid>
  <ion-row>
    <ion-col size="6">占一半(6/12)</ion-col>
    <ion-col size="6">占一半</ion-col>
  </ion-row>

  <ion-row>
    <ion-col size="4">1/3</ion-col>
    <ion-col size="4">1/3</ion-col>
    <ion-col size="4">1/3</ion-col>
  </ion-row>

  <ion-row>
    <ion-col size="8">2/3</ion-col>
    <ion-col size="4">1/3</ion-col>
  </ion-row>
</ion-grid>

响应式断点

<!-- 不同屏幕宽度不同列数 -->
<ion-grid>
  <ion-row>
    <!-- 手机: 整行; 平板: 半行; 桌面: 1/3 -->
    <ion-col size="12"
             size-sm="6"
             size-md="4"
             size-lg="3">
      响应式卡片
    </ion-col>
  </ion-row>
</ion-grid>

# 断点:
# size: 默认(手机)
# size-sm: >=576px
# size-md: >=768px(平板)
# size-lg: >=992px(桌面)
# size-xl: >=1200px

商品网格案例

<!-- 商品双列布局 -->
<ion-grid>
  <ion-row>
    <ion-col size="6" *ngFor="let p of products">
      <ion-card>
        <img [src]="p.image">
        <ion-card-header>
          <ion-card-subtitle>
            ¥{{ p.price }}
          </ion-card-subtitle>
          <ion-card-title>
            {{ p.name }}
          </ion-card-title>
        </ion-card-header>
      </ion-card>
    </ion-col>
  </ion-row>
</ion-grid>

<!-- 三列布局(小图标) -->
<ion-col size="4" *ngFor="let app of apps">
  <div class="app-icon">
    <ion-icon [name]="app.icon"></ion-icon>
    <p>{{ app.name }}</p>
  </div>
</ion-col>

对齐与间距

<!-- 垂直/水平对齐 -->
<ion-row>
  <ion-col size="6">左</ion-col>
  <ion-col size="6" class="ion-text-end">
    右
  </ion-col>
</ion-row>

<!-- 垂直居中 -->
<ion-row class="ion-align-items-center">
  <ion-col>文字居中</ion-col>
</ion-row>

# 工具类:
# ion-align-items-start/center/end
# ion-justify-content-center/end
# ion-text-start/center/end
# ion-text-wrap / ion-text-nowrap

<!-- 偏移与间距 -->
<ion-col size="8" offset="2">居中偏移</ion-col>

嵌套网格

<!-- 网格内嵌网格 -->
<ion-grid>
  <ion-row>
    <ion-col size="6">
      <ion-grid>
        <ion-row>
          <ion-col size="12">子内容 1</ion-col>
          <ion-col size="12">子内容 2</ion-col>
        </ion-row>
      </ion-grid>
    </ion-col>
    <ion-col size="6">右侧内容</ion-col>
  </ion-row>
</ion-grid>

自定义间距

/* 全局或页面 scss */
ion-grid {
  padding: 8px;
}

ion-col {
  margin-bottom: 8px;
}

/* 卡片间距 */
ion-col ion-card {
  margin: 4px;
}

/* 响应式间距 */
@media (min-width: 768px) {
  ion-col {
    padding: 8px;
  }
}

注意事项

# 1. 12 列系统,size 和为 12 占满一行
# 2. 超出行宽自动换行
# 3. 响应式用断点前缀(size-sm 等)
# 4. 与 flexbox 类似,可互相替代
# 5. 大量列表项用虚拟滚动(网格也适用)

# 常见场景:
# - 商品/照片网格
# - 功能图标宫格
# - 统计面板多列
# - 表单双列布局

网格让布局"自适应"。掌握 12 列系统与响应式断点,配合工具类对齐,任何屏幕都优雅呈现。

ionic 颜色

Ionic 颜色

Ionic 提供一套语义化颜色系统:primary、secondary、success 等,通过 CSS 变量高度可定制,并自动支持暗色模式。

预定义颜色

<!-- 组件 color 属性 -->
<ion-button color="primary">主色</ion-button>
<ion-button color="secondary">次色</ion-button>
<ion-button color="tertiary">第三色</ion-button>
<ion-button color="success">成功</ion-button>
<ion-button color="warning">警告</ion-button>
<ion-button color="danger">危险</ion-button>
<ion-button color="light">浅色</ion-button>
<ion-button color="medium">中灰</ion-button>
<ion-button color="dark">深色</ion-button>

<!-- 几乎适用于所有组件 -->
<ion-text color="danger">错误提示</ion-text>
<ion-icon color="success" name="checkmark"></ion-icon>
<ion-badge color="warning">待处理</ion-badge>
<ion-item color="light">浅色条目</ion-item>

自定义主题色

// src/theme/variables.scss
:root {
  /* 品牌主色 */
  --ion-color-primary: #6c5ce7;
  --ion-color-primary-rgb: 108, 92, 231;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255, 255, 255;
  --ion-color-primary-shade: #5f51cb;
  --ion-color-primary-tint: #7b6dea;

  /* 自定义颜色(完整声明) */
  --ion-color-brand: #ff6b6b;
  --ion-color-brand-rgb: 255, 107, 107;
  --ion-color-brand-contrast: #ffffff;
  --ion-color-brand-contrast-rgb: 255, 255, 255;
  --ion-color-brand-shade: #e05e5e;
  --ion-color-brand-tint: #ff7a7a;
}

/* 注册自定义颜色类 */
.ion-color-brand {
  --ion-color-base: var(--ion-color-brand);
  --ion-color-base-rgb: var(--ion-color-brand-rgb);
  --ion-color-contrast: var(--ion-color-brand-contrast);
  --ion-color-contrast-rgb: var(--ion-color-brand-contrast-rgb);
  --ion-color-shade: var(--ion-color-brand-shade);
  --ion-color-tint: var(--ion-color-brand-tint);
}

/* 使用 */
<ion-button class="ion-color-brand">
  品牌色按钮
</ion-button>

组件级变量

/* 每个组件有自己的颜色变量 */
ion-button {
  --color: #333;
  --background: #fff;
  --background-hover: #f0f0f0;
}

ion-card {
  --background: #ffffff;
  --color: #222;
}

ion-toolbar {
  --background: #3880ff;
  --color: #ffffff;
}

/* 页面背景 */
ion-content {
  --background: #f5f6f8;
}

暗色模式

// variables.scss 自动适配系统
@media (prefers-color-scheme: dark) {
  :root {
    --ion-color-primary: #8a7dff;
    --ion-background-color: #121212;
    --ion-background-color-rgb: 18, 18, 18;
    --ion-text-color: #ffffff;
    --ion-text-color-rgb: 255, 255, 255;
    --ion-card-background: #1e1e1e;
    --ion-item-background: #1e1e1e;
    --ion-toolbar-background: #1e1e1e;
  }
}

// 强制暗色(class 方式)
html.dark {
  --ion-color-primary: #8a7dff;
  ...
}
document.body.classList.add("dark");

常用主题变量

:root {
  /* 背景与文字 */
  --ion-background-color: #ffffff;
  --ion-text-color: #222;

  /* 组件背景 */
  --ion-card-background: #ffffff;
  --ion-item-background: #ffffff;
  --ion-toolbar-background: #ffffff;

  /* 边框与分隔 */
  --ion-border-color: #ddd;
  --ion-item-border-color: #ddd;

  /* 覆盖层 */
  --ion-overlay-background-color: #fff;

  /* 字体 */
  --ion-font-family: -apple-system,
      "PingFang SC", "Microsoft YaHei", sans-serif;

  /* 圆角与阴影 */
  --ion-border-radius: 8px;
  --ion-box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

语义化用色建议

# 语义化颜色选择:
# primary: 主要操作/品牌
# success: 成功/完成
# warning: 警告/待处理
# danger: 错误/删除/危险
# medium: 次要/禁用感
# light/dark: 层级对比

# 避免:
# - 滥用 warning/danger(用户紧张)
# - 文字对比度不足(可读性)
# - 硬编码颜色(破坏主题切换)

# 最佳实践:
# 1. 品牌色只改 primary 变量
# 2. 状态用语义色(成功/危险)
# 3. 自定义色完整注册 5 个变量
# 4. 适配暗色模式

颜色系统让应用"有品牌感"。掌握预定义色、CSS 变量定制与暗色适配,界面风格统一且可切换。

ionic icon(图标)

Ionic Icon(图标)

图标是界面视觉语言的重要组成部分。Ionic 内置 Ionicons 图标库(1200+ 图标),也可使用自定义 SVG。

基础用法

<!-- 简单图标 -->
<ion-icon name="home"></ion-icon>
<ion-icon name="person"></ion-icon>

<!-- 尺寸 -->
<ion-icon name="star" size="small"></ion-icon>
<ion-icon name="star" size="large"></ion-icon>

<!-- 自定义像素尺寸 -->
<ion-icon name="star"
          style="font-size: 32px;">
</ion-icon>

<!-- 颜色 -->
<ion-icon name="heart" color="danger">
</ion-icon>
<ion-icon name="checkmark" color="success">
</ion-icon>

图标命名

<!-- 基础图标名 -->
<ion-icon name="home"></ion-icon>
<ion-icon name="search"></ion-icon>
<ion-icon name="settings"></ion-icon>
<ion-icon name="close"></ion-icon>
<ion-icon name="add"></ion-icon>
<ion-icon name="trash"></ion-icon>
<ion-icon name="share"></ion-icon>
<ion-icon name="download"></ion-icon>
<ion-icon name="camera"></ion-icon>

<!-- 变体(实心/描边) -->
<ion-icon name="heart"></ion-icon>       <!-- 描边 -->
<ion-icon name="heart-outline"></ion-icon>
<ion-icon name="heart-sharp"></ion-icon>

<!-- 平台自适应 -->
<ion-icon name="home"
          md="home" ios="home-outline">
</ion-icon>

# 完整列表查官方文档:
# https://ionic.io/ionicons

图标与文字组合

<!-- 按钮图标 -->
<ion-button>
  <ion-icon name="trash" slot="start"></ion-icon>
  删除
</ion-button>

<!-- 列表图标 -->
<ion-item>
  <ion-icon name="settings" slot="start">
  </ion-icon>
  <ion-label>设置</ion-label>
</ion-item>

<!-- 头像 -->
<ion-avatar>
  <ion-icon name="person" size="large">
  </ion-icon>
</ion-avatar>

<!-- 文字内嵌 -->
<p>
  <ion-icon name="checkmark-circle"
            color="success"></ion-icon>
  操作成功
</p>

自定义 SVG 图标

<!-- 方式1: 内联 SVG -->
<ion-icon>
  <svg viewBox="0 0 24 24">
    <path d="M12 2L2 22h20L12 2z"/>
  </svg>
</ion-icon>

<!-- 方式2: 自定义图标集 -->
// Angular
import { addIcons } from "ionicons";
import { logoGithub } from "ionicons/icons";

addIcons({ "logo-github": logoGithub });

// 模板中使用
<ion-icon name="logo-github"></ion-icon>

// 常用品牌图标:
// logo-github / logo-twitter / logo-facebook
// logo-apple / logo-google / logo-whatsapp

动态图标

<!-- 动态切换 -->
<ion-icon [name]="liked ? 'heart'
                   : 'heart-outline'"
          [color]="liked ? 'danger' : 'medium'"
          (click)="toggleLike()">
</ion-icon>

// TS
liked = false;
toggleLike() {
  this.liked = !this.liked;
}

<!-- 加载图标(旋转) -->
<ion-icon name="refresh"
          class="spin"></ion-icon>

/* 旋转动画 */
.spin {
  animation: spin 1s linear infinite;
}
@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

图标样式

/* CSS 控制 */
ion-icon {
  color: #666;
  font-size: 24px;
}

/* 悬浮效果 */
ion-icon:hover {
  opacity: 0.8;
}

/* 图标容器 */
.icon-circle {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: #e8f0fe;
  display: flex;
  align-items: center;
  justify-content: center;
  ion-icon { font-size: 24px; color: #3880ff; }
}

注意事项

# 1. name 必须正确(查文档确认)
# 2. 性能: 图标可打包,勿动态注入大量 SVG
# 3. 无障碍: 纯图标加 aria-label
<ion-icon name="trash"
          aria-label="删除"></ion-icon>

# 4. 色彩对比: 图标颜色保证可辨识
# 5. 品牌图标单独注册(addIcons)

图标让界面"表意清晰"。掌握内置图标、变体与自定义 SVG,配合动态切换,视觉细节更专业。

ionic 上拉菜单

Ionic 上拉菜单

上拉菜单(Action Sheet)从底部弹出操作选项列表,用于"分享/删除/取消"等操作集合,是移动端常用交互。

基础用法(Angular)

import { ActionSheetController } from "@ionic/angular";

constructor(private actionSheetCtrl: ActionSheetController) {}

async showActions() {
  const actionSheet = await this.actionSheetCtrl.create({
    header: "选择操作",
    buttons: [
      {
        text: "分享",
        icon: "share",
        handler: () => this.share()
      },
      {
        text: "收藏",
        icon: "star",
        handler: () => this.favorite()
      },
      {
        text: "删除",
        icon: "trash",
        role: "destructive",     // 危险(红色)
        handler: () => this.delete()
      },
      {
        text: "取消",
        icon: "close",
        role: "cancel"           // 取消
      },
    ],
  });
  await actionSheet.present();
}

取消与事件

async showActions() {
  const sheet = await this.actionSheetCtrl.create({
    header: "更多操作",
    subHeader: "请选择",
    buttons: [...],
    // 点击背景取消
    backdropDismiss: true,
  });

  await sheet.present();

  // 监听关闭
  const { role } = await sheet.onDidDismiss();
  console.log("关闭原因:", role);
  // 'cancel' / 'destructive' / 自定义 role
}

样式定制

async showActions() {
  const sheet = await this.actionSheetCtrl.create({
    header: "操作",
    cssClass: "my-action-sheet",
    buttons: [
      { text: "红色操作", role: "destructive" },
      { text: "取消", role: "cancel" },
    ],
  });
  await sheet.present();
}

/* 全局 scss */
.my-action-sheet {
  --background: #f8f8f8;
  --backdrop-opacity: 0.4;
}

.my-action-sheet .action-sheet-button {
  font-size: 16px;
}

.my-action-sheet .action-sheet-title {
  color: #666;
  font-weight: bold;
}

/* 暗色适配 */
@media (prefers-color-scheme: dark) {
  .my-action-sheet {
    --background: #1e1e1e;
  }
}

分享菜单示例

async shareOptions() {
  const sheet = await this.actionSheetCtrl.create({
    header: "分享到",
    buttons: [
      {
        text: "微信",
        icon: "logo-wechat",
        handler: () => this.shareTo("wechat")
      },
      {
        text: "朋友圈",
        icon: "logo-wechat",
        handler: () => this.shareTo("moments")
      },
      {
        text: "微博",
        icon: "logo-twitter",
        handler: () => this.shareTo("weibo")
      },
      {
        text: "复制链接",
        icon: "link",
        handler: () => this.copyLink()
      },
      {
        text: "取消",
        icon: "close",
        role: "cancel"
      },
    ],
  });
  await sheet.present();
}

private shareTo(platform: string) {
  console.log("分享到:", platform);
}

与 Alert 对比

# Action Sheet(上拉菜单):
# - 底部弹出,操作列表
# - 适合: 分享/删除/多项操作
# - 移动端习惯

# Alert(弹窗):
# - 居中弹窗
# - 适合: 确认/输入/单选(少于3项)
# - 打断性强

# 选择:
# 操作集合(2-6项) → Action Sheet
# 简单确认/输入 → Alert
# 复杂表单 → Modal(模态窗口)

注意事项

# 1. role="destructive" 显示危险样式(红)
# 2. role="cancel" 固定为取消(底部)
# 3. handler 返回 false 可阻止关闭
# 4. 记得 await present()

// 阻止关闭示例
{
  text: "确认退出",
  handler: () => {
    if (!confirm) {
      return false;   // 不关闭
    }
    return true;
  }
}

上拉菜单是"轻量操作选择"的标配。掌握按钮角色(取消/危险)与事件回调,交互反馈完整自然。

ionic 背景层

Ionic 背景层

背景层(Overlay/Backdrop)是覆盖在页面内容之上的半透明层,常见于弹窗、上拉菜单、模态窗口等弹出组件背后,用于聚焦内容与点击关闭。

什么是背景层

# 弹出组件(Alert/ActionSheet/Modal)显示时,
# 页面内容被半透明背景层覆盖:
# - 视觉聚焦: 突出弹出内容
# - 点击关闭: 点背景可关闭(可配置)
# - 阻止交互: 防止误操作底层内容

# 背景层默认样式:
# 半透明黑色(可定制透明度/颜色)

控制背景层行为

// Alert
const alert = await alertCtrl.create({
  header: "提示",
  message: "内容",
  buttons: ["确定"],
  backdropDismiss: false,   // 禁止点背景关闭
});
await alert.present();

// ActionSheet
await this.actionSheetCtrl.create({
  buttons: [...],
  backdropDismiss: true,    // 允许点背景关闭(默认)
}).present();

// Modal
const modal = await modalCtrl.create({
  component: MyModalPage,
  backdropDismiss: false,   // 必须显式关闭
});
await modal.present();

自定义背景样式

// 全局 scss
ion-modal {
  --backdrop-opacity: 0.5;      // 透明度
  --backdrop-color: #000;       // 颜色
}

ion-alert {
  --backdrop-opacity: 0.4;
}

/* 或组件 cssClass 定制 */
async show() {
  const alert = await this.alertCtrl.create({
    header: "提示",
    cssClass: "custom-backdrop",
    buttons: ["确定"],
  });
  await alert.present();
}

/* 全局 scss */
.custom-backdrop {
  --backdrop-opacity: 0.6;
  --backdrop-color: #333;
}

完全自定义背景层

/* 手动创建覆盖层(少用) */
<div class="overlay" *ngIf="showOverlay"
     (click)="hideOverlay()">
  <div class="overlay-content"
       (click)="$event.stopPropagation()">
    自定义弹层内容
  </div>
</div>

/* scss */
.overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.overlay-content {
  background: #fff;
  padding: 24px;
  border-radius: 12px;
  max-width: 80%;
}

与模态窗口配合

// Modal 的半透明效果
async openModal() {
  const modal = await this.modalCtrl.create({
    component: FilterPage,
    backdropDismiss: true,
    // 底部弹出
    cssClass: "bottom-modal",
  });
  await modal.present();
}

/* 底部弹窗样式 */
.bottom-modal {
  --height: 60%;
  --border-radius: 16px 16px 0 0;
  align-items: flex-end;
}

/* 背景层模糊(毛玻璃) */
.bottom-modal {
  --backdrop-filter: blur(4px);
}

背景层事件

// 监听背景点击关闭
const modal = await modalCtrl.create({...});
await modal.present();

const { role, data } = await modal.onWillDismiss();
// role: 'backdrop' 表示点背景关闭

if (role === "backdrop") {
  console.log("用户点击背景关闭");
}

// 阻止意外关闭的场景:
// - 表单未保存确认
// - 必须完成的流程(授权)

最佳实践

# 1. 重要操作(删除确认)禁点背景关闭
#   → backdropDismiss: false
# 2. 轻量选择(筛选)允许点背景关闭
# 3. 背景透明度不宜过低(聚焦)
# 4. 暗色模式下背景层自动适配
# 5. 自定义弹层注意 z-index 层级

# 常见问题:
# - 点背景关不掉: backdropDismiss=false
# - 背景太浅/太深: 调整 --backdrop-opacity
# - 弹层被遮: 检查 z-index

背景层是弹出组件的"舞台布景"。掌握 backdropDismiss 与透明度定制,弹出交互张弛有度。

ionic 下拉刷新

Ionic 下拉刷新

下拉刷新(Refresher)是移动端经典交互:下拉页面顶部触发刷新,加载最新数据。Ionic 提供 ion-refresher 组件。

基础用法

<ion-refresher slot="fixed"
               (ionRefresh)="doRefresh($event)">
  <ion-refresher-content
    pullingIcon="arrow-down"
    pullingText="下拉刷新"
    refreshingSpinner="circles"
    refreshingText="刷新中...">
  </ion-refresher-content>
</ion-refresher>

// TS
async doRefresh(event: any) {
  // 模拟请求数据
  await this.loadData();

  // 完成刷新(必须调用!)
  event.target.complete();
}

刷新数据示例

// TS
async doRefresh(event: any) {
  try {
    this.items = await this.apiService
      .getLatest();
    console.log("刷新完成:", this.items.length);
  } catch (err) {
    console.error("刷新失败:", err);
  } finally {
    event.target.complete();   // 结束动画
  }
}

// 带超时保护
async doRefresh(event: any) {
  await Promise.race([
    this.loadData(),
    new Promise(r => setTimeout(r, 10000)),
  ]);
  event.target.complete();
}

自定义刷新文案

<ion-refresher (ionRefresh)="doRefresh($event)">
  <ion-refresher-content
    pullingIcon="refresh"
    pullingText="松开刷新"
    refreshingSpinner="bubbles"
    refreshingText="努力加载中..."
    cancelPullText="取消"
  ></ion-refresher-content>
</ion-refresher>

# 常用 spinner 类型:
# circles / bubbles / crescent
# lines / dots

# 属性:
# pullingIcon: 下拉时的图标
# pullingText: 下拉文案
# refreshingSpinner: 刷新动画
# refreshingText: 刷新文案

与列表结合

<ion-content>
  <ion-refresher slot="fixed"
                 (ionRefresh)="refresh($event)">
    <ion-refresher-content>
    </ion-refresher-content>
  </ion-refresher>

  <!-- 空状态 -->
  <div *ngIf="items.length === 0"
       class="empty-state">
    <ion-icon name="cloud-offline"
              size="large"></ion-icon>
    <p>暂无数据,下拉刷新</p>
  </div>

  <!-- 数据列表 -->
  <ion-list *ngIf="items.length > 0">
    <ion-item *ngFor="let item of items">
      <ion-label>{{ item.title }}</ion-label>
    </ion-item>
  </ion-list>
</ion-content>

程序化触发

<!-- 首次进入自动刷新 -->
<ion-refresher #refresher
               (ionRefresh)="refresh($event)">
  <ion-refresher-content>
  </ion-refresher-content>
</ion-refresher>

// TS
import { Component, ViewChild } from "@angular/core";
import { IonRefresher } from "@ionic/angular";

@ViewChild("refresher") refresher!: IonRefresher;

ngOnInit() {
  // 延迟触发自动刷新
  setTimeout(() => this.refresher.emit("ionRefresh"),
             100);
}

禁用与关闭

<!-- 条件禁用 -->
<ion-refresher [disabled]="!canRefresh">

<!-- 手动关闭 -->
event.target.complete()    // 完成
event.target.disabled = true   // 禁用

// 异常时重置
async refresh(event: any) {
  try {
    ...
  } catch {
    event.target.complete();
  }
}

注意事项

# 1. 必须调用 complete() 结束动画!
# 2. slot="fixed" 固定(不随内容滚动)
# 3. 刷新失败也要 complete(否则一直转圈)
# 4. 页面滚动顶部才能触发
# 5. 与加载更多(上拉)配合使用

# 常见问题:
# - 刷新动画不停: 忘记 complete()
# - 无法下拉: 内容未填满或 slot 错误
# - 触发频繁: 防抖处理

下拉刷新是移动端数据更新的标准姿势。掌握组件用法与 complete 契约,配合列表与空状态,体验完整。

ionic 复选框

Ionic 复选框(Checkbox 进阶)

本节深入复选框的进阶用法:批量选择、全选/反选、表格选择、筛选标签等复杂场景。

批量选择列表

<ion-list>
  <ion-item *ngFor="let item of items">
    <ion-checkbox
      [checked]="isSelected(item)"
      (ionChange)="toggle(item, $event)">
    </ion-checkbox>
    <ion-label>
      <h2>{{ item.name }}</h2>
      <p>{{ item.desc }}</p>
    </ion-label>
  </ion-item>
</ion-list>

// TS
selected: any[] = [];

isSelected(item: any) {
  return this.selected.includes(item);
}

toggle(item: any, event: any) {
  if (event.detail.checked) {
    this.selected.push(item);
  } else {
    this.selected = this.selected
      .filter(i => i !== item);
  }
}

getSelectedCount() {
  return this.selected.length;
}

全选/反选

<!-- 全选控制项 -->
<ion-item>
  <ion-checkbox
    [checked]="isAllSelected()"
    [indeterminate]="isPartial()"
    (ionChange)="selectAll($event.detail.checked)">
  </ion-checkbox>
  <ion-label>全选</ion-label>
</ion-item>

// TS
isAllSelected() {
  return this.items.length > 0
    && this.selected.length === this.items.length;
}

isPartial() {
  return this.selected.length > 0
    && this.selected.length < this.items.length;
}

selectAll(checked: boolean) {
  this.selected = checked
    ? [...this.items]
    : [];
}

// indeterminate: 半选状态(部分选中)

复选框样式定制

/* 自定义 */
ion-checkbox {
  --size: 22px;
  --border-radius: 6px;
  --border-color: #bbb;
  --border-color-checked: #3880ff;
  --background: #fff;
  --background-checked: #3880ff;
  --checkmark-color: #fff;
}

/* 圆形 */
ion-checkbox shape="circle" {
  --border-radius: 50%;
}

/* 大尺寸 */
ion-checkbox {
  --size: 28px;
}

筛选标签场景

<!-- 筛选标签 -->
<ion-chip *ngFor="let tag of tags"
          [outline]="!selectedTags.includes(tag)"
          (click)="toggleTag(tag)">
  <ion-label>{{ tag }}</ion-label>
  <ion-icon *ngIf="selectedTags.includes(tag)"
            name="checkmark"></ion-icon>
</ion-chip>

// TS
selectedTags: string[] = [];
tags = ["Python", "AI", "前端", "后端", "运维"];

toggleTag(tag: string) {
  const idx = this.selectedTags.indexOf(tag);
  if (idx > -1) {
    this.selectedTags.splice(idx, 1);
  } else {
    this.selectedTags.push(tag);
  }
}

// 获取筛选结果
getFiltered() {
  return this.allItems.filter(item =>
    item.tags.some(t =>
      this.selectedTags.includes(t)));
}

表单集成

<!-- 响应式表单数组 -->
<ion-item *ngFor="let interest of interests">
  <ion-checkbox
    [formControl]="getControl(interest.key)">
  </ion-checkbox>
  <ion-label>{{ interest.label }}</ion-label>
</ion-item>

// TS
form = this.fb.group({});
interests = [
  { key: "coding", label: "编程" },
  { key: "music", label: "音乐" },
  { key: "sports", label: "运动" },
];

constructor() {
  this.interests.forEach(i =>
    this.form.addControl(i.key,
      new FormControl(false)));
}

getControl(key: string) {
  return this.form.get(key) as FormControl;
}

与数据结构同步

// 对象数组直接绑定选中状态
interface Selectable {
  id: number;
  name: string;
  selected: boolean;
}

toggle(item: Selectable) {
  item.selected = !item.selected;
}

// 收集选中数据
getSelectedItems() {
  return this.items.filter(i => i.selected);
}

// 初始化选中
markSelected(ids: number[]) {
  this.items.forEach(i => {
    i.selected = ids.includes(i.id);
  });
}

最佳实践

# 1. 大数据列表: 用 Set 存储选中(性能)
#    selected = new Set<number>()
#    isSelected = (i) => this.selected.has(i.id)
# 2. 全选/反选提供 indeterminate 反馈
# 3. 批量操作(删除/移动)基于选中集
# 4. 与搜索/分页组合时选中需跨页保留

// 批量删除
async deleteSelected() {
  const ids = [...this.selectedSet];
  await this.api.batchDelete(ids);
  this.items = this.items.filter(
    i => !this.selectedSet.has(i.id));
  this.selectedSet.clear();
}

复选框进阶覆盖批量操作核心。掌握全选/半选/跨页保留,再配合批量操作,数据管理界面专业完整。

ionic 单选框操作

Ionic 单选框操作(Radio 进阶)

本节深入单选框的进阶操作:动态选项、级联选择、表单集成与自定义样式。

动态选项加载

<!-- 异步加载选项 -->
<ion-radio-group [(ngModel)]="country">
  <ion-item *ngFor="let c of countries">
    <ion-label>{{ c.name }}</ion-label>
    <ion-radio [value]="c.code"
               [disabled]="c.disabled">
    </ion-radio>
  </ion-item>
</ion-radio-group>

// TS
countries = [];
country = "";

ngOnInit() {
  this.api.getCountries().subscribe(data => {
    this.countries = data;
    this.country = data[0]?.code;
  });
}

级联选择(省市区)

<!-- 省 → 市 -->
<ion-item>
  <ion-label>省份</ion-label>
  <ion-select [(ngModel)]="province"
              (ionChange)="onProvinceChange()">
    <ion-select-option *ngFor="let p of provinces"
                       [value]="p.code">
      {{ p.name }}
    </ion-select-option>
  </ion-select>
</ion-item>

<!-- 市(随省变化) -->
<ion-item>
  <ion-label>城市</ion-label>
  <ion-select [(ngModel)]="city">
    <ion-select-option *ngFor="let c of cities"
                       [value]="c.code">
      {{ c.name }}
    </ion-select-option>
  </ion-select>
</ion-item>

// TS
provinces = data.allProvince;
cities: any[] = [];

onProvinceChange() {
  this.cities = data.cities[this.province] || [];
  this.city = null;   // 重置下级
}

单选卡片样式

<!-- 卡片式单选(套餐选择) -->
<ion-radio-group [(ngModel)]="plan">
  <ion-card *ngFor="let p of plans"
            [class.selected]="plan === p.id"
            (click)="plan = p.id">
    <ion-card-header>
      <ion-card-title>{{ p.name }}</ion-card-title>
      <ion-card-subtitle>
        ¥{{ p.price }}/月
      </ion-card-subtitle>
    </ion-card-header>
    <ion-card-content>
      {{ p.desc }}
    </ion-card-content>
  </ion-card>
</ion-radio-group>

/* 选中高亮 */
ion-card.selected {
  border: 2px solid #3880ff;
  box-shadow: 0 4px 16px rgba(56,128,255,0.3);
}

RadioGroup 事件

<ion-radio-group [(ngModel)]="value"
                 (ionChange)="onChange($event)">
  ...
</ion-radio-group>

// TS
onChange(event: any) {
  const value = event.detail.value;
  console.log("选中:", value);

  // 触发生成联动数据
  this.loadDetail(value);
}

// 读取当前值
console.log(this.value);

// 响应式表单
form.get("choice")?.valueChanges
  .subscribe(v => this.onChangeValue(v));

自定义样式

/* 圆点样式 */
ion-radio {
  --color: #999;
  --color-checked: #3880ff;
  --border-width: 2px;
  --border-style: solid;
  --border-color: #ccc;
  --border-color-checked: #3880ff;
  --size: 24px;
}

/* 选中项文字高亮 */
ion-item {
  --background-hover: #f0f4ff;
}
ion-item.ion-activated {
  --background: #e8f0fe;
}

/* 隐藏圆点(自定义指示) */
ion-radio {
  visibility: hidden;
  position: absolute;
}

/* 按钮样式单选 */
.option-btn {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 8px 16px;
  margin: 4px;
}
.option-btn.selected {
  border-color: #3880ff;
  color: #3880ff;
  background: #e8f0fe;
}

表单校验

<!-- 必选校验 -->
<form [formGroup]="form">
  <ion-radio-group formControlName="gender">
    ...
  </ion-radio-group>
</form>

// TS
form = this.fb.group({
  gender: ["", Validators.required],
});

// 提交校验
submit() {
  if (this.form.invalid) {
    alert("请选择性别");
    return;
  }
  console.log(this.form.value);
}

// 错误提示
<ion-text color="danger" *ngIf="
  form.get('gender')?.invalid &&
  form.get('gender')?.touched">
  此项必选
</ion-text>

最佳实践

# 1. 选项少(≤5)用 radio; 多则用 select/弹层
# 2. 选项带说明用卡片式单选
# 3. 级联选择注意重置下级
# 4. 值类型一致(数字/字符串)
# 5. 表单集成用 formControlName

单选框进阶覆盖动态与级联场景。掌握选项加载、级联联动与样式定制,选择交互专业完善。

ionic 手势事件

Ionic 手势事件

手势是移动端的核心交互:滑动、长按、缩放等。Ionic/Angular 提供多种手势处理方式,也可使用 Hammer.js 等库。

原生触摸事件

<!-- Angular 触摸事件 -->
<div (touchstart)="onTouchStart($event)"
     (touchmove)="onTouchMove($event)"
     (touchend)="onTouchEnd($event)">
  触摸区域
</div>

// TS
onTouchStart(e: any) {
  this.startX = e.touches[0].clientX;
  this.startY = e.touches[0].clientY;
}

onTouchMove(e: any) {
  const dx = e.touches[0].clientX - this.startX;
  const dy = e.touches[0].clientY - this.startY;
  // 判断方向
  if (Math.abs(dx) > Math.abs(dy)) {
    console.log("水平滑动", dx);
  } else {
    console.log("垂直滑动", dy);
  }
}

onTouchEnd(e: any) {
  console.log("触摸结束");
}

Ionic Gesture 控制器

import { Gesture, GestureController } from "@ionic/angular";

constructor(private gestureCtrl: GestureController) {}

ngAfterViewInit() {
  const el = document.querySelector("#swipe-area")!;

  const gesture: Gesture = this.gestureCtrl.create({
    el: el,
    gestureName: "swipe",
    threshold: 10,       // 触发阈值(px)
    gesturePriority: 30,
    onStart: () => {
      console.log("手势开始");
    },
    onMove: (detail) => {
      // detail.deltaX/deltaY 位移
      console.log("移动:", detail.deltaX);
      el.style.transform =
        `translateX(${detail.deltaX}px)`;
    },
    onEnd: (detail) => {
      console.log("手势结束");
      el.style.transform = "";
      if (detail.deltaX < -80) {
        this.swipeLeft();
      }
    },
  });

  gesture.enable(true);   // 启用
}

滑动删除/操作

<!-- 列表滑动操作(内置) -->
<ion-item-sliding>
  <ion-item>
    <ion-label>消息内容</ion-label>
  </ion-item>

  <ion-item-options side="start">
    <ion-item-option color="success"
                    (click)="archive()">
      归档
    </ion-item-option>
  </ion-item-options>

  <ion-item-options side="end">
    <ion-item-option color="danger"
                    (click)="delete()">
      删除
    </ion-item-option>
  </ion-item-options>
</ion-item-sliding>

Hammer.js(更丰富的手势)

# 安装
npm install hammerjs

// main.ts 引入
import "hammerjs";

// 使用
import { HammerModule } from "@angular/platform-browser";
imports: [HammerModule]

// 模板中直接用手势事件
<div (swipeleft)="onSwipeLeft()"
     (swiperight)="onSwipeRight()"
     (pan)="onPan($event)"
     (pinch)="onPinch($event)"
     (longpress)="onLongPress()">
  手势区域
</div>

// TS
onSwipeLeft() {
  console.log("左滑");
  this.goNext();
}

onSwipeRight() {
  console.log("右滑");
  this.goBack();
}

onLongPress() {
  console.log("长按");
}

onPinch(e: any) {
  console.log("缩放:", e.scale);
}

下拉/上拉手势(自定义)

// 监听页面滚动判断方向
<ion-content (ionScroll)="onScroll($event)">
  ...
</ion-content>

// TS
onScroll(event: any) {
  const detail = event.detail;
  const scrollingDown = detail.deltaY > 0;

  // 滚动到底部附近 → 加载更多
  if (detail.scrollTop + detail.scrollHeight
      - detail.scrollTop > ...) {
    this.loadMore();
  }
}

// 触底加载
onIonScroll(event: any) {
  const el = event.target;
  if (el.scrollTop + el.clientHeight
      >= el.scrollHeight - 50) {
    this.loadMore();
  }
}

手势冲突处理

# 手势与滚动冲突是常见问题
# 1. 水平手势 vs 垂直滚动: 判断方向
# 2. gesturePriority: 优先级
# 3. threshold: 阈值过滤

// 只响应水平手势(让垂直滚动正常工作)
const gesture = this.gestureCtrl.create({
  el,
  gestureName: "h-swipe",
  direction: "x",       // 只监听水平
  threshold: 20,
  ...
});

// 或手动判断
onMove(detail: any) {
  if (Math.abs(detail.deltaX) <
      Math.abs(detail.deltaY)) {
    return;   // 垂直方向,交给滚动
  }
  // 处理水平滑动
}

注意事项

# 1. 移动端触摸事件用 touch*,不用 click 判断滑动
# 2. 手势中修改 transform 注意性能(避免 reflow)
# 3. 长按触发系统菜单(禁用 contextmenu)
<div (contextmenu)="preventDefault()">

# 4. 手势后清除状态(防止下次误触发)
# 5. 与组件内置手势(滑动删除)共存

# 常见场景:
# - 轮播图滑动
# - 左滑返回
# - 长按删除
# - 卡片滑动操作

手势让交互"自然流畅"。掌握触摸事件、Gesture 控制器与 Hammer.js,配合方向判断,移动端交互体验专业。

ionic 头部和底部

Ionic 头部和底部(进阶)

本节深入头部与底部的进阶用法:动态标题、滚动隐藏、多彩工具栏、透明效果与多行布局。

动态标题

<!-- 动态标题 -->
<ion-header>
  <ion-toolbar>
    <ion-title>{{ pageTitle }}</ion-title>
  </ion-toolbar>
</ion-header>

// TS
pageTitle = "首页";

// 数据加载后更新
this.api.getData().subscribe(data => {
  this.pageTitle = data.title;
});

<!-- 条件显示按钮 -->
<ion-buttons slot="end" *ngIf="isEditable">
  <ion-button (click)="edit()">编辑</ion-button>
</ion-buttons>

滚动隐藏头部

<!-- 向上滚动隐藏,向下显示 -->
<ion-header collapse="condense">
  <ion-toolbar>
    <ion-title>可折叠标题</ion-title>
  </ion-toolbar>
</ion-header>

<!-- 大标题(折叠效果) -->
<ion-header collapse="condense">
  <ion-toolbar>
    <ion-title size="large">
      大标题
    </ion-title>
  </ion-toolbar>
</ion-header>

# collapse="condense": 滚动时标题缩小
# collapse="fade": 滚动时淡出
# 注意: 需配合页面滚动内容

多彩工具栏

<!-- 渐变背景 -->
<ion-header>
  <ion-toolbar class="gradient-toolbar">
    <ion-title>渐变头部</ion-title>
  </ion-toolbar>
</ion-header>

/* scss */
.gradient-toolbar {
  --background: linear-gradient(
      90deg, #667eea, #764ba2);
  --color: white;
}

<!-- 图片背景 -->
<ion-header>
  <ion-toolbar class="img-toolbar">
    <ion-title>图片背景</ion-title>
  </ion-toolbar>
</ion-header>

.img-toolbar {
  --background: url("/assets/banner.jpg")
      center/cover no-repeat;
  --color: white;
}

<!-- 半透明(毛玻璃) -->
<ion-toolbar translucent>
  <ion-title>透明工具栏</ion-title>
</ion-toolbar>
<ion-content fullscreen>...</ion-content>

多行工具栏

<!-- 头部两行: 标题 + 搜索 -->
<ion-header>
  <ion-toolbar>
    <ion-title>商品列表</ion-title>
  </ion-toolbar>
  <ion-toolbar>
    <ion-searchbar placeholder="搜索商品">
    </ion-searchbar>
  </ion-toolbar>
</ion-header>

<!-- 底部两行: 操作 + 汇总 -->
<ion-footer>
  <ion-toolbar>
    <ion-title size="small">
      合计: ¥{{ total }}
    </ion-title>
  </ion-toolbar>
  <ion-toolbar>
    <ion-button expand="block">
      去结算
    </ion-button>
  </ion-toolbar>
</ion-footer>

头部内容布局

<!-- 左侧返回+标题+右侧操作 -->
<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/">
      </ion-back-button>
    </ion-buttons>

    <ion-title>详情页</ion-title>

    <ion-buttons slot="end">
      <ion-button>
        <ion-icon name="heart-outline">
        </ion-icon>
      </ion-button>
      <ion-button>
        <ion-icon name="ellipsis-vertical">
        </ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<!-- 返回按钮自定义 -->
<ion-back-button defaultHref="/home"
                text="首页"
                icon="arrow-back">
</ion-back-button>

底部导航操作

<!-- 底部操作栏 -->
<ion-footer>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-button fill="outline">
        <ion-icon name="heart"></ion-icon>
        收藏
      </ion-button>
    </ion-buttons>

    <ion-buttons slot="end">
      <ion-button color="primary">
        立即购买
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-footer>

<!-- 底部安全区适配(iPhone 等) -->
<ion-footer>
  <ion-toolbar class="safe-bottom">
    ...
  </ion-toolbar>
</ion-footer>

.safe-bottom {
  padding-bottom: env(safe-area-inset-bottom);
}

工具栏阴影与边框

/* 去除阴影 */
ion-toolbar {
  --border-width: 0;
  --box-shadow: none;
}

/* 自定义阴影 */
ion-toolbar {
  --box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

/* 底部分割线颜色 */
ion-header {
  --border-color: #eee;
}

/* 全局(Theme) */
:root {
  --ion-toolbar-border-color: #eee;
  --ion-toolbar-box-shadow: none;
}

最佳实践

# 1. 标题/按钮统一放工具栏(自动安全区)
# 2. 返回按钮用 ion-back-button(自带手势)
# 3. 顶部搜索用第二行工具栏
# 4. 底部操作栏避免遮挡内容
#    (ion-content 自动处理 padding)
# 5. 渐变/图片头部注意对比度(白字)
# 6. 暗色模式验证两种配色

# 常见问题:
# - 工具栏背景不生效: 用 --background
# - 返回按钮不出现: 无历史页面时给 defaultHref
# - 安全区: 使用内置组件自动适配

头部与底部的进阶用法,让页面"骨架"更专业。掌握折叠、渐变与布局组合,视觉与交互双重提升。

ionic 列表操作

Ionic 列表操作

列表操作涵盖:滑动操作、拖拽排序、点击跳转、分组与空状态。本节实现一个完整的"可管理列表"。

滑动操作

<ion-item-sliding *ngFor="let item of items">
  <ion-item>
    <ion-label>
      <h2>{{ item.title }}</h2>
      <p>{{ item.date }}</p>
    </ion-label>
  </ion-item>

  <!-- 左侧操作 -->
  <ion-item-options side="start">
    <ion-item-option color="success"
                    (click)="markRead(item)">
      <ion-icon name="checkmark"></ion-icon>
      已读
    </ion-item-option>
  </ion-item-options>

  <!-- 右侧操作 -->
  <ion-item-options side="end">
    <ion-item-option (click)="archive(item)">
      <ion-icon name="archive"></ion-icon>
      归档
    </ion-item-option>
    <ion-item-option color="danger"
                    (click)="remove(item)">
      <ion-icon name="trash"></ion-icon>
      删除
    </ion-item-option>
  </ion-item-options>
</ion-item-sliding>

点击跳转

<!-- 路由跳转 -->
<ion-item *ngFor="let item of items"
          routerLink="/detail/{{ item.id }}">
  <ion-label>{{ item.title }}</ion-label>
  <ion-icon name="chevron-forward"
            slot="end"></ion-icon>
</ion-item>

<!-- 或事件跳转 -->
<ion-item button *ngFor="let item of items"
          (click)="openDetail(item)">
  ...
</ion-item>

// TS
openDetail(item: any) {
  this.router.navigate(["/detail", item.id],
    { queryParams: { from: "list" } });
}

拖拽排序

# 安装: npm install @angular/cdk

<!-- CDK 拖拽排序 -->
<ion-list cdkDropList
          (cdkDropListDropped)="onDrop($event)">
  <ion-item *ngFor="let item of items"
            cdkDrag>
    <ion-icon name="reorder-three"
              slot="start"></ion-icon>
    <ion-label>{{ item.title }}</ion-label>
  </ion-item>
</ion-list>

// TS
onDrop(event: any) {
  const prev = event.previousIndex;
  const curr = event.currentIndex;
  const moved = this.items.splice(prev, 1)[0];
  this.items.splice(curr, 0, moved);

  // 保存新顺序
  this.saveOrder(this.items);
}

分组与索引

<!-- 按组显示 -->
<ion-list *ngFor="let group of groupedItems">
  <ion-list-header>
    {{ group.name }}
    <ion-badge slot="end">
      {{ group.items.length }}
    </ion-badge>
  </ion-list-header>

  <ion-item *ngFor="let item of group.items">
    <ion-label>{{ item.name }}</ion-label>
  </ion-item>
</ion-list>

// TS 分组
import { groupBy } from "lodash-es";

this.groupedItems = Object.entries(
  groupBy(this.allItems, i => i.category)
).map(([name, items]) => ({ name, items }));

加载更多(上拉加载)

<!-- 触底加载更多 -->
<ion-infinite-scroll
    (ionInfinite)="loadMore($event)">
  <ion-infinite-scroll-content
    loadingSpinner="bubbles"
    loadingText="加载中...">
  </ion-infinite-scroll-content>
</ion-infinite-scroll>

// TS
page = 1;
hasMore = true;

async loadMore(event: any) {
  const newItems = await this.api.fetchPage(this.page++);

  if (newItems.length === 0) {
    this.hasMore = false;
    event.target.complete();
    event.target.disabled = true;   // 禁用
    return;
  }

  this.items.push(...newItems);
  event.target.complete();   // 必须调用
}

搜索与过滤

<!-- 搜索过滤 -->
<ion-searchbar [(ngModel)]="keyword"
               (ionInput)="onSearch()">
</ion-searchbar>

<ion-list>
  <ion-item *ngFor="let item of filtered">
    <ion-label>{{ item.title }}</ion-label>
  </ion-item>
</ion-list>

// TS
filtered: any[] = [];

onSearch() {
  const kw = this.keyword.trim().toLowerCase();
  this.filtered = this.allItems.filter(i =>
    i.title.toLowerCase().includes(kw) ||
    i.desc?.toLowerCase().includes(kw));
}

空状态与骨架屏

<!-- 空状态 -->
<div class="empty-state" *ngIf="items.length === 0">
  <ion-icon name="file-tray-outline"
            size="large"></ion-icon>
  <h3>暂无数据</h3>
  <p>点击刷新或添加第一条数据</p>
  <ion-button (click)="load()">刷新</ion-button>
</div>

/* 骨架屏(加载占位) */
<ion-list *ngIf="loading">
  <ion-item *ngFor="let _ of [].constructor(5)">
    <ion-skeleton-text animated
      style="width: 100%"></ion-skeleton-text>
  </ion-item>
</ion-list>

最佳实践

# 1. 滑动操作放常用动作(删除/收藏)
# 2. 危险操作(删除)加确认
# 3. 大列表虚拟滚动
# 4. 触底加载 + 下拉刷新配套
# 5. 操作后更新数据与 UI(动画反馈)
# 6. 排序结果持久化

// 删除确认
async remove(item: any) {
  const alert = await this.alertCtrl.create({
    header: "确认删除",
    message: `删除"${item.title}"?`,
    buttons: [
      { text: "取消", role: "cancel" },
      {
        text: "删除",
        role: "destructive",
        handler: () => this.doDelete(item),
      },
    ],
  });
  await alert.present();
}

列表操作是数据管理的核心交互。掌握滑动、拖拽、分组、加载与搜索,列表页功能完整且体验流畅。

ionic 加载动作

Ionic 加载动作(Loading)

Loading(加载指示器)在耗时操作期间显示,防止用户重复操作并反馈进度。Ionic 提供 LoadingController。

基础用法

import { LoadingController } from "@ionic/angular";

constructor(private loadingCtrl: LoadingController) {}

async showLoading() {
  const loading = await this.loadingCtrl.create({
    message: "加载中...",
    spinner: "circles",
  });

  await loading.present();
  return loading;
}

// 使用
async loadData() {
  const loading = await this.showLoading();
  try {
    this.data = await this.api.fetch();
  } finally {
    await loading.dismiss();   // 必须关闭!
  }
}

加载动画类型

# spinner 类型:
# circles / bubbles / crescent
# lines / lines-small / dots

const loading = await this.loadingCtrl.create({
  message: "处理中",
  spinner: "bubbles",       // 动画
  // spinner: null + 自定义内容
});

// 自定义内容(无 spinner)
const loading = await this.loadingCtrl.create({
  message: `
    <div class="custom-loading">
      <img src="/assets/loading.gif">
      <p>请稍候...</p>
    </div>`,
  spinner: null,            // 关闭默认动画
});

自动关闭与时长

// 固定时长自动关闭
const loading = await this.loadingCtrl.create({
  message: "上传中...",
  duration: 3000,          // 3秒自动关闭
});

await loading.present();

// 点击背景关闭
const loading = await this.loadingCtrl.create({
  message: "处理中",
  backdropDismiss: true,   // 可点背景关闭
});

// 判断是否已关闭
if (!loading) return;
await loading.dismiss();

防止重复提交

// 提交按钮防抖
isSubmitting = false;

async submit() {
  if (this.isSubmitting) return;   // 防重复
  this.isSubmitting = true;

  const loading = await this.loadingCtrl.create({
    message: "提交中...",
  });
  await loading.present();

  try {
    await this.api.submit(this.form.value);
  } finally {
    this.isSubmitting = false;
    await loading.dismiss();
  }
}

配合 HTTP 请求

// 请求期间显示加载
async fetchUsers() {
  const loading = await this.loadingCtrl.create({
    message: "获取用户...",
  });
  await loading.present();

  try {
    const data = await firstValueFrom(
      this.api.getUsers());
    this.users = data;
  } catch (err) {
    console.error(err);
    this.errorMsg = "请求失败";
  } finally {
    await loading.dismiss();
  }
}

// 或全局拦截器(统一处理)
// Angular HTTP Interceptor 中
// 请求发起显示 loading,完成关闭

样式定制

// cssClass
const loading = await this.loadingCtrl.create({
  message: "加载中",
  cssClass: "my-loading",
});
await loading.present();

/* 全局 scss */
.my-loading {
  --background: rgba(0, 0, 0, 0.8);
  --spinner-color: #fff;
  color: #fff;
  border-radius: 12px;
}

/* 或直接改全局 */
ion-loading {
  --background: #222;
  --spinner-color: #fff;
  --color: #fff;
}

与进度条对比

# Loading: 不确定时长的操作(转圈)
# Progress Bar: 确定进度(上传百分比)

<!-- 进度条(上传等) -->
<ion-progress-bar [value]="uploadProgress"
                  color="primary">
</ion-progress-bar>

// TS
uploadProgress = 0;
// 更新进度
this.uploadProgress = event.loaded / event.total;

注意事项

# 1. present 后必须 dismiss(finally 中)
# 2. 多个 loading 叠加: 用计数器管理
# 3. 短暂操作(<200ms)可省略 loading
# 4. 关闭前判断 isPresented(避免报错)

// 安全关闭
private loading: any;
async safeDismiss() {
  if (this.loading) {
    await this.loading.dismiss();
    this.loading = null;
  }
}

// 防抖显示(操作太快不闪 loading)
let timer: any;
const show = () => {
  timer = setTimeout(async () => {
    this.loading = await this.loadingCtrl.create({
      message: "加载中" });
    await this.loading.present();
  }, 200);
};
// 完成后
clearTimeout(timer);
await this.loading?.dismiss();

Loading 是耗时操作的"状态反馈"。掌握创建/关闭契约与防重复,配合请求流程,交互反馈专业。

ionic 模态窗口

Ionic 模态窗口(Modal)

模态窗口(Modal)是覆盖在当前页面上的全屏或部分屏内容,用于表单填写、详情查看、筛选等需要专注的场景。

基础用法

import { ModalController } from "@ionic/angular";

constructor(private modalCtrl: ModalController) {}

// 打开模态
async openModal() {
  const modal = await this.modalCtrl.create({
    component: AddItemPage,      // 模态内容组件
    componentProps: {
      title: "新增待办",
      item: { id: 1, name: "示例" },
    },
  });

  // 组件返回数据
  modal.onDidDismiss().then((data) => {
    console.log("返回:", data.data, data.role);
    if (data.data) {
      this.refresh(data.data);
    }
  });

  await modal.present();
}

模态组件内部

// add-item.page.ts
import { ModalController } from "@ionic/angular";

@Component({...})
export class AddItemPage {
  // 接收父页面数据
  title = "";
  item: any;

  constructor(private modalCtrl: ModalController) {}

  // 关闭并返回数据
  save() {
    this.modalCtrl.dismiss(
      { name: "新数据" },   // data
      "save"                // role
    );
  }

  // 直接关闭(无数据)
  cancel() {
    this.modalCtrl.dismiss(null, "cancel");
  }
}

弹出方式(动画)

// 默认: 全屏滑入(类似新页面)

// 底部弹出
const modal = await this.modalCtrl.create({
  component: FilterPage,
  cssClass: "bottom-sheet",
});
await modal.present();

/* 全局 scss */
.bottom-sheet {
  --height: 70%;
  --border-radius: 20px 20px 0 0;
  align-items: flex-end;
}

// 居中弹窗(小型)
.modal-center {
  --width: 85%;
  --height: auto;
  --border-radius: 16px;
}

筛选器案例

// 父页面
async openFilter() {
  const modal = await this.modalCtrl.create({
    component: FilterPage,
    componentProps: {
      currentFilter: this.filter,
    },
    cssClass: "bottom-sheet",
  });

  modal.onDidDismiss().then(({ data }) => {
    if (data) {
      this.filter = data;
      this.loadList();
    }
  });

  await modal.present();
}

// 子页面(filter.page.ts)
save() {
  this.modalCtrl.dismiss(this.form.value, "apply");
}

reset() {
  this.modalCtrl.dismiss(null, "reset");
}

表单模态(添加/编辑)

// 新增编辑通用
async openEditor(item?: any) {
  const modal = await this.modalCtrl.create({
    component: ItemEditorPage,
    componentProps: { item: item || null },
  });

  const { data, role } = await modal.onWillDismiss();
  if (role === "save" && data) {
    // 新增或更新
    if (item) {
      await this.api.update(item.id, data);
    } else {
      await this.api.create(data);
    }
    this.loadList();
  }
}

导航到模态

// 模态内可以包含多个页面(导航)
<!-- modal-root.page.html -->
<ion-nav>
  <ng-container *ngFor="let page of pages">
    <ion-nav-link [component]="page.component"
                  [componentProps]="page.props">
    </ion-nav-link>
  </ng-container>
</ion-nav>

// 或手动管理
// 用 ion-nav 支持模态内多级导航
// 适合: 多步表单、向导流程

获取返回数据

// 方式1: onDidDismiss(关闭后)
modal.onDidDismiss().then((result) => {
  const { data, role } = result;
});

// 方式2: onWillDismiss(关闭前)
modal.onWillDismiss().then((result) => {
  // 可在这里拦截处理
});

// 子组件主动关闭
this.modalCtrl.dismiss(data, role);

// 返回值约定:
// data: 业务数据
// role: 'save' / 'cancel' / 'delete' 等语义

注意事项

# 1. 模态组件需要在 module 中声明
# 2. 传参用 componentProps(单向)
# 3. 返回用 dismiss(data, role)
# 4. 表单模态记得关闭时校验
# 5. 底部弹窗高度自适应内容

// 模块声明
@NgModule({
  declarations: [AddItemPage],
  imports: [IonicModule, CommonModule,
            FormsModule],
})
export class AddItemPageModule {}

最佳实践

# 1. 复杂表单用模态(不丢页面上下文)
# 2. 轻量选择用 ActionSheet/Alert
# 3. 底部弹窗放筛选/分享
# 4. 全屏模态放编辑/详情
# 5. 返回值用 role 区分语义
# 6. 模态内表单丢失时给提示(可选)

// 防误关
const modal = await this.modalCtrl.create({
  component: EditorPage,
  backdropDismiss: false,   // 防点背景关闭
});

模态窗口是"上下文专注"的容器。掌握传参/返回值/弹窗样式,筛选、编辑、详情页专业流畅。

ionic 导航

Ionic 导航

导航是移动应用的结构骨架。Ionic 基于 Angular Router 提供页面导航:路由跳转、参数传递、返回与历史管理。

路由配置

// app.routes.ts
import { Routes } from "@angular/router";

export const routes: Routes = [
  { path: "", redirectTo: "/home", pathMatch: "full" },
  { path: "home",
    loadChildren: () =>
      import("./pages/home/home.page")
        .then(m => m.HomePageModule) },
  { path: "detail/:id",
    loadChildren: () =>
      import("./pages/detail/detail.page")
        .then(m => m.DetailPageModule) },
  { path: "**", redirectTo: "/home" },  // 404
];

页面跳转方式

// 方式1: 模板 routerLink
<ion-button routerLink="/home">去首页</ion-button>
<ion-item routerLink="/detail/42">
  查看详情 42
</ion-item>

// 方式2: 代码跳转
import { Router } from "@angular/router";

constructor(private router: Router) {}

goHome() {
  this.router.navigate(["/home"]);
}

goDetail(id: number) {
  this.router.navigate(["/detail", id]);
}

// 方式3: 带查询参数
this.router.navigate(["/list"], {
  queryParams: { page: 2, keyword: "python" },
});
// URL: /list?page=2&keyword=python

接收参数

import { ActivatedRoute } from "@angular/router";

constructor(private route: ActivatedRoute) {}

// 路径参数 /detail/42
ngOnInit() {
  this.id = this.route.snapshot.paramMap.get("id");

  // 监听变化(同一组件复用)
  this.route.paramMap.subscribe((params) => {
    this.id = params.get("id");
    this.loadData();
  });

  // 查询参数
  this.route.queryParamMap.subscribe((q) => {
    this.keyword = q.get("keyword");
  });
}

返回与历史

// 模板返回按钮
<ion-back-button defaultHref="/home">
</ion-back-button>

// 代码返回
import { Location } from "@angular/common";
import { Router } from "@angular/router";

constructor(
  private location: Location,
  private router: Router,
) {}

goBack() {
  this.location.back();      // 返回上一页
}

// 没有历史时的兜底
goBackSafe() {
  const history = this.router
    .getCurrentNavigation();
  if (history?.previousNavigation) {
    this.location.back();
  } else {
    this.router.navigate(["/home"]);
  }
}

导航参数传递(复杂对象)

// 方式1: 状态传递(推荐,可序列化)
this.router.navigate(["/detail"], {
  state: { item: this.currentItem },
});

// 接收
const state = this.router.getCurrentNavigation()
  ?.extras.state;
if (state) {
  this.item = state["item"];
}

// 方式2: 服务共享(跨页面)
// data.service.ts
private itemSubject = new BehaviorSubject(null);
setItem(item) { this.itemSubject.next(item); }
getItem() { return this.itemSubject.asObservable(); }

选项卡内导航

// tabs 结构
const routes: Routes = [
  {
    path: "tabs",
    component: TabsPage,
    children: [
      { path: "home", component: HomePage },
      { path: "detail/:id", component: DetailPage },
      { path: "", redirectTo: "home", pathMatch: "full" },
    ],
  },
];

// 跳转到 tabs 内页面
this.router.navigate(["/tabs/detail", id]);

// 切换到某个 tab
this.router.navigate(["/tabs/profile"]);

页面生命周期

// Angular 生命周期 + Ionic 生命周期
<!-- Ionic 特有: -->
// ionViewWillEnter: 每次进入页面(含返回)
// ionViewDidEnter
// ionViewWillLeave
// ionViewDidLeave

// 用法: 返回时刷新数据
ionViewWillEnter() {
  this.loadData();   // 每次进入都刷新
}

// 与 ngOnInit 区别:
// ngOnInit: 只执行一次(组件创建)
// ionViewWillEnter: 每次进入都执行

导航过渡动画

// 自定义过渡(全局)
// app.module 或路由配置
import { IonicRouteStrategy } from "@ionic/angular";

// 关闭动画
providers: [
  { provide: RouteReuseStrategy,
    useClass: IonicRouteStrategy },
];

/* 全局关闭转场动画 */
ion-router-outlet {
  --transition-duration: 0ms;
}

// 自定义页面转场(高级)
// 实现 PageTransition 接口
// 参考 Ionic 官方文档自定义转场

最佳实践

# 1. 页面懒加载(loadChildren)
# 2. 参数用路由传递,复杂对象用 state/服务
# 3. 返回按钮给 defaultHref 兜底
# 4. 返回时刷新用 ionViewWillEnter
# 5. 深链接支持(分享/推送直达页面)
# 6. 未登录页面守卫(Router Guard)

// 路由守卫
import { CanActivate } from "@angular/router";

@Injectable({ providedIn: "root" })
export class AuthGuard implements CanActivate {
  canActivate() {
    return this.auth.isLoggedIn();
  }
}

// 路由配置
{ path: "profile",
  loadChildren: ...,
  canActivate: [AuthGuard] }

导航是应用的"动脉"。掌握路由配置、参数传递与返回管理,配合守卫与生命周期,页面流转顺畅安全。

ionic 平台

Ionic 平台

平台(Platform)能力:检测运行设备(Android/iOS/Web)、屏幕信息、原生能力调用(Capacitor),实现跨平台适配。

检测平台

import { Platform } from "@ionic/angular";

constructor(private platform: Platform) {}

ngOnInit() {
  console.log("是 Android:", this.platform.is("android"));
  console.log("是 iOS:", this.platform.is("ios"));
  console.log("是 Web:", this.platform.is("desktop"));
  console.log("是手机:", this.platform.is("mobile"));

  // 等待平台就绪
  this.platform.ready().then(() => {
    console.log("平台就绪(原生API可用)");
  });
}

// 常用平台标识:
// android / ios / desktop / mobile
// phablet / tablet / hybrid / mobileweb

屏幕信息

import { Platform } from "@ionic/angular";

// 屏幕尺寸
console.log(this.platform.width());   // 像素宽
console.log(this.platform.height());  // 像素高

// 横竖屏
const isLandscape = this.platform.isLandscape();
const isPortrait = this.platform.isPortrait();

// 监听
this.platform.resize.subscribe(() => {
  console.log("尺寸变化:",
    this.platform.width());
});

// 设备像素比
const dpr = window.devicePixelRatio;

// 断点判断(类似 CSS)
if (this.platform.is("tablet")) {
  // 平板布局
}

Capacitor 原生能力

# Capacitor 是 Ionic 的原生运行时
# 调用相机、定位、存储等设备能力

# 安装
npm install @capacitor/core @capacitor/cli
npx cap init
npm install @capacitor/camera @capacitor/geolocation

# 使用相机
import { Camera, CameraResultType } from "@capacitor/camera";

async takePhoto() {
  const image = await Camera.getPhoto({
    quality: 90,
    resultType: CameraResultType.Uri,
    source: CameraSource.Camera,
  });
  this.photoUrl = image.webPath;
}

# 使用定位
import { Geolocation } from "@capacitor/geolocation";

async getLocation() {
  const pos = await Geolocation.getCurrentPosition();
  console.log(pos.coords.latitude, pos.coords.longitude);
}

本地存储

# Web: localStorage(简单)
localStorage.setItem("key", "value");
const v = localStorage.getItem("key");

# 跨端安全存储(Capacitor Preferences)
import { Preferences } from "@capacitor/preferences";

// 保存
await Preferences.set({
  key: "user",
  value: JSON.stringify(user),
});

// 读取
const { value } = await Preferences.get({
  key: "user",
});
const user = value ? JSON.parse(value) : null;

// 删除
await Preferences.remove({ key: "user" });

网络状态检测

import { Network } from "@capacitor/network";

// 当前状态
const status = await Network.getStatus();
console.log("在线:", status.connected);
console.log("类型:", status.connectionType);

// 监听变化
Network.addListener("networkStatusChange",
  (status) => {
    console.log("网络变化:", status.connected);
    this.online = status.connected;
  });

// 离线提示
<div class="offline-banner" *ngIf="!online">
  网络连接已断开
</div>

深链接与推送

# 深链接(App 打开 URL)
import { App, URLOpenListenerEvent } from "@capacitor/app";

App.addListener("appUrlOpen", (event: URLOpenListenerEvent) => {
  console.log("打开链接:", event.url);
  const url = new URL(event.url);
  const id = url.searchParams.get("id");
  this.router.navigate(["/detail", id]);
});

# 推送通知(需 @capacitor/push-notifications)
import { PushNotifications } from "@capacitor/push-notifications";

// 请求权限并注册
PushNotifications.requestPermissions();
PushNotifications.register();

// 监听通知
PushNotifications.addListener("pushNotificationReceived",
  (notification) => { ... });

平台条件渲染

<!-- 模板中判断 -->
<ion-button *ngIf="platform.is('android')"
            (click)="scanQR()">
  扫码(仅 Android)
</ion-button>

<!-- 平台特定样式 -->
<div class="app-content">...</div>

/* scss: 平台样式 */
.platform-ios .app-content {
  padding-top: 44px;    /* iPhone 状态栏 */
}

.platform-android .app-content {
  padding-top: 24px;    /* 通知栏 */
}

/* 或 CSS 媒体查询 */
@media (hover: none) and (pointer: coarse) {
  /* 触屏设备 */
}

// TS 判断(平台差异逻辑)
getButtonText() {
  return this.platform.is("ios") ? "iCloud 同步"
                                 : "云同步";
}

构建与签名

# Web
ionic build

# Android
ionic build
npx cap add android
npx cap open android      # Android Studio
# 生成 APK/AAB: Build → Build App Bundle

# iOS(需 macOS + Xcode)
npx cap add ios
npx cap open ios
# 签名与上架: Xcode 配置

# 更新原生项目
npx cap sync

# 调试真机
npx cap run android --device

最佳实践

# 1. 平台检测尽量少用(优先响应式)
# 2. 原生能力封装成服务(可 mock 测试)
# 3. 离线场景缓存数据
# 4. 状态栏/安全区适配
# 5. 性能: 减少 bridge 调用(原生-Web)

# 安全区适配
<ion-content>
  <div style="padding-top: env(safe-area-inset-top)">
  </div>
</ion-content>

# 状态栏
import { StatusBar } from "@capacitor/status-bar";
await StatusBar.setBackgroundColor({ color: "#3880ff" });

平台能力让应用"多端一致"。掌握平台检测、Capacitor 原生调用与构建流程,一次开发多端上线。

ionic 浮动框

Ionic 浮动框(Popover)

浮动框(Popover)是依附于触发元素的弹出层,类似桌面端的右键菜单/气泡。适合小菜单、快捷操作与提示。

基础用法

import { PopoverController } from "@ionic/angular";

constructor(private popoverCtrl: PopoverController) {}

// 打开浮动框
async openMenu(event: any) {
  const popover = await this.popoverCtrl.create({
    component: MenuPopoverPage,   // 浮动内容
    event: event,                 // 触发位置
    translucent: true,            // 半透明
    showBackdrop: false,          // 无背景层
  });
  await popover.present();

  // 接收返回值
  const { data, role } = await popover.onDidDismiss();
  if (data) {
    this.handleAction(data.action);
  }
}

浮动框内容组件

// menu-popover.page.html
<ion-content>
  <ion-list>
    <ion-item button (click)="choose('edit')">
      <ion-icon name="create" slot="start">
      </ion-icon>
      编辑
    </ion-item>
    <ion-item button (click)="choose('share')">
      <ion-icon name="share" slot="start">
      </ion-icon>
      分享
    </ion-item>
    <ion-item button (click)="choose('delete')">
      <ion-icon name="trash" slot="start"
                color="danger">
      </ion-icon>
      <ion-label color="danger">删除</ion-label>
    </ion-item>
  </ion-list>
</ion-content>

// menu-popover.page.ts
import { PopoverController } from "@ionic/angular";

export class MenuPopoverPage {
  constructor(private popoverCtrl: PopoverController) {}

  choose(action: string) {
    this.popoverCtrl.dismiss({ action }, action);
  }
}

尺寸与定位

// 自定义尺寸
const popover = await this.popoverCtrl.create({
  component: MenuPage,
  event,
  size: "auto",      // auto / cover / 指定大小
  // 或 cssClass 控制
  cssClass: "my-popover",
});
await popover.present();

/* 全局 scss */
.my-popover {
  --width: 200px;
  --max-width: 80%;
  --min-width: 150px;
  --max-height: 60%;
}

// 定位参考
// event: 以点击位置弹出
// 无 event: 屏幕居中
// 可传 reference: 'event' | 'trigger'

快捷操作菜单

<!-- 触发按钮 -->
<ion-button (click)="openMenu($event)">
  <ion-icon name="ellipsis-vertical">
  </ion-icon>
</ion-button>

// TS
async openMenu(event: any) {
  const popover = await this.popoverCtrl.create({
    component: QuickActionsPage,
    event,
    showBackdrop: false,
  });
  await popover.present();
}

// 常见操作:
// 编辑 / 复制 / 分享 / 收藏 / 删除
// 置顶 / 移动 / 导出 / 更多

// 配合列表项
<ion-item>
  <ion-label>文件.txt</ion-label>
  <ion-button slot="end" fill="clear"
              (click)="openFileMenu($event, file)">
    <ion-icon name="ellipsis-horizontal">
    </ion-icon>
  </ion-button>
</ion-item>

与 ActionSheet 对比

# Popover(浮动框):
# - 出现在触发元素旁(上下文相关)
# - 适合: 小菜单、快捷操作、提示
# - 桌面/平板体验好

# ActionSheet(上拉菜单):
# - 底部弹出,大按钮
# - 适合: 移动端操作集合(分享/删除)
# - 拇指友好

# 选择:
# 点击点附近 → Popover
# 移动端底部操作 → ActionSheet
# 也可按平台: 手机用 ActionSheet,
#             平板/桌面用 Popover

传参与返回值

// 传参(componentProps)
const popover = await this.popoverCtrl.create({
  component: DetailPopoverPage,
  componentProps: { item: this.currentItem },
  event,
});

// 子组件读取
item: any;   // 自动注入 componentProps

// 返回
this.popoverCtrl.dismiss({ result: "ok" }, "action");

// 监听
const { data, role } = await popover.onWillDismiss();
console.log(data, role);

注意事项

# 1. 浮动框组件需在模块声明
# 2. 小屏设备慎用(空间不足)
# 3. showBackdrop=false 更轻量(点外部关闭)
# 4. 层级高,注意与其他弹层冲突
# 5. 动态内容高度自适应(size: auto)

# 常见问题:
# - 位置偏移: 传 event 或 trigger 引用
# - 内容不显示: 组件未声明
# - 关闭不执行: 检查 dismiss 调用

// 关闭全部浮动框
await this.popoverCtrl.dismiss();

浮动框是"上下文菜单"的利器。掌握触发定位、内容组件与返回值,快捷操作体验轻快精准。

ionic 对话框

Ionic 对话框(Alert)

对话框(Alert)是居中弹窗,用于确认、提示、输入与简单选择。打断性强,适合重要信息或必需操作。

基础提示

import { AlertController } from "@ionic/angular";

constructor(private alertCtrl: AlertController) {}

// 简单提示
async showAlert() {
  const alert = await this.alertCtrl.create({
    header: "提示",
    message: "操作成功!",
    buttons: ["确定"],
  });
  await alert.present();
}

// 带副标题与图标
await this.alertCtrl.create({
  header: "更新可用",
  subHeader: "v2.0.0",
  message: "包含新功能与性能优化",
  buttons: ["知道了"],
}).present();

确认对话框

// 确认操作
async confirmDelete() {
  const alert = await this.alertCtrl.create({
    header: "确认删除",
    message: "删除后不可恢复,确定继续?",
    buttons: [
      { text: "取消", role: "cancel" },
      {
        text: "删除",
        role: "destructive",
        handler: () => {
          this.doDelete();   // 点击后执行
        },
      },
    ],
  });
  await alert.present();
}

// 处理取消
buttons: [
  {
    text: "取消",
    role: "cancel",
    handler: () => console.log("已取消"),
  },
  { text: "确定", handler: () => this.doOk() },
]

输入对话框

// 单输入
async promptInput() {
  const alert = await this.alertCtrl.create({
    header: "重命名",
    inputs: [
      {
        name: "name",
        type: "text",
        placeholder: "新名称",
        value: this.currentName,
      },
    ],
    buttons: [
      { text: "取消", role: "cancel" },
      {
        text: "保存",
        handler: (data) => {
          console.log("新名称:", data.name);
          this.rename(data.name);
        },
      },
    ],
  });
  await alert.present();
}

// 多输入(登录/注册)
inputs: [
  { name: "username", type: "text",
    placeholder: "用户名" },
  { name: "password", type: "password",
    placeholder: "密码" },
  { name: "email", type: "email",
    placeholder: "邮箱" },
]

选择对话框

// 单选
await this.alertCtrl.create({
  header: "选择排序方式",
  inputs: [
    { type: "radio", label: "最新",
      value: "new", checked: true },
    { type: "radio", label: "最热",
      value: "hot" },
    { type: "radio", label: "价格",
      value: "price" },
  ],
  buttons: [
    { text: "取消", role: "cancel" },
    {
      text: "确定",
      handler: (data) => console.log("选择:", data),
    },
  ],
}).then(a => a.present());

// 多选
inputs: [
  { type: "checkbox", label: "Python",
    value: "py", checked: true },
  { type: "checkbox", label: "Java",
    value: "java" },
]
// handler: data 是数组

动态按钮与样式

// 动态按钮数组
const buttons: any[] = [
  { text: "取消", role: "cancel" },
];
if (this.canDelete) {
  buttons.push({ text: "删除",
    role: "destructive",
    handler: () => this.delete() });
}
buttons.push({ text: "保存", handler: () => this.save() });

const alert = await this.alertCtrl.create({
  header: "管理",
  buttons,
});
await alert.present();

// 自定义样式
const alert = await this.alertCtrl.create({
  header: "重要通知",
  cssClass: "custom-alert",
  buttons: ["确定"],
});
await alert.present();

/* 全局 scss */
.custom-alert {
  --background: #fff;
  --max-width: 320px;
  border-radius: 16px;
}

.custom-alert .alert-title {
  color: #333;
  font-weight: bold;
}

配合表单校验

// 输入校验(handler 返回 false 阻止关闭)
async promptWithValidation() {
  const alert = await this.alertCtrl.create({
    header: "输入名称",
    inputs: [
      { name: "name", type: "text", placeholder: "必填" },
    ],
    buttons: [
      { text: "取消", role: "cancel" },
      {
        text: "确定",
        handler: (data) => {
          if (!data.name || !data.name.trim()) {
            // 显示错误
            const msg = alert.querySelector(".alert-message");
            if (msg) msg.textContent = "名称不能为空";
            return false;   // 阻止关闭
          }
          return true;
        },
      },
    ],
  });
  await alert.present();
}

注意事项

# 1. handler 返回 false 阻止关闭
# 2. role: cancel/destructive 语义化
# 3. 按钮顺序: 取消在左,确认在右
# 4. 输入框用 form 校验(见上例)
# 5. 同时只能一个 Alert(管理队列)

// 自动关闭(定时)
const alert = await this.alertCtrl.create({
  header: "成功",
  buttons: [],
});
await alert.present();
setTimeout(() => alert.dismiss(), 1500);

# 选择控件:
# 确认/提示 → Alert
# 多项操作 → ActionSheet
# 复杂表单 → Modal
# 上下文菜单 → Popover

对话框是"打断式交互"的载体。掌握确认/输入/选择三类用法与校验,关键操作可控可感知。

ionic 滚动条

Ionic 滚动条(Scroll)

滚动是移动内容浏览的核心。Ionic 的 ion-content 提供原生滚动,配合滚动事件、滚动到指定位置与自定义滚动容器。

基础滚动

<!-- ion-content 自带滚动 -->
<ion-content>
  <!-- 内容超出屏幕自动滚动 -->
  <div *ngFor="let item of manyItems">
    {{ item }}
  </div>
</ion-content>

<!-- 滚动设置 -->
<ion-content scrollY="true">   <!-- 垂直滚动 -->
</ion-content>

<!-- 禁用滚动 -->
<ion-content scrollY="false">
</ion-content>

<!-- 滚动阻尼/惯性 -->
<ion-content [scrollEvents]="true"
             (ionScroll)="onScroll($event)">
</ion-content>

滚动到指定位置

import { IonContent } from "@ionic/angular";

@ViewChild(IonContent) content!: IonContent;

// 滚动到底部
scrollToBottom() {
  this.content.scrollToBottom(300);  // 300ms 动画
}

// 滚动到顶部
scrollToTop() {
  this.content.scrollToTop(300);
}

// 滚动到指定像素
scrollToPoint() {
  this.content.scrollToPoint(0, 500, 300);
}

// 滚动到元素
scrollToElement() {
  this.content.scrollToPoint(
    0, 500, 300);  // 或用元素 offsetTop
}

// 通过元素滚动(手动)
scrollIntoView(id: string) {
  document.getElementById(id)
    ?.scrollIntoView({ behavior: "smooth" });
}

滚动事件

<ion-content scrollEvents="true"
             (ionScrollStart)="onScrollStart()"
             (ionScroll)="onScroll($event)"
             (ionScrollEnd)="onScrollEnd()">
</ion-content>

// TS
onScrollStart() {
  this.scrolling = true;
}

onScroll(event: any) {
  const detail = event.detail;

  // 滚动位置
  const scrollTop = detail.scrollTop;
  const maxScroll = detail.scrollHeight
    - detail.clientHeight;

  // 顶部/底部判断
  this.atTop = scrollTop < 10;
  this.atBottom = scrollTop > maxScroll - 10;

  // 滚动方向
  const direction = detail.deltaY > 0
    ? "down" : "up";
}

onScrollEnd() {
  this.scrolling = false;
}

// 应用: 滚动时显示/隐藏头部
<!-- 返回顶部按钮 -->
<ion-fab *ngIf="showTopBtn"
         vertical="bottom" horizontal="end"
         (click)="scrollToTop()">
  <ion-fab-button>
    <ion-icon name="arrow-up"></ion-icon>
  </ion-fab-button>
</ion-fab>

自定义滚动容器

<!-- 局部滚动区域 -->
<div class="scroll-area">
  <div class="scroll-content">
    <!-- 可滚动内容 -->
  </div>
</div>

/* scss */
.scroll-area {
  height: 300px;
  overflow-y: auto;      /* 关键: 滚动 */
  -webkit-overflow-scrolling: touch;  /* iOS */
}

/* 自定义滚动条(Web) */
.scroll-area::-webkit-scrollbar {
  width: 6px;
}
.scroll-area::-webkit-scrollbar-thumb {
  background: #ccc;
  border-radius: 3px;
}

<!-- 横向滚动 -->
<div class="h-scroll">
  <ion-chip *ngFor="let tag of tags">
    {{ tag }}
  </ion-chip>
</div>

.h-scroll {
  display: flex;
  overflow-x: auto;
  gap: 8px;
  -webkit-overflow-scrolling: touch;
}

滚动加载(触底)

<!-- 触底加载 -->
<ion-content scrollEvents="true"
             (ionScroll)="onScroll($event)">
  <ion-list>...</ion-list>

  <!-- 加载指示 -->
  <div class="load-more" *ngIf="loading">
    <ion-spinner name="crescent"></ion-spinner>
    加载中...
  </div>
</ion-content>

// TS
onScroll(event: any) {
  const el = event.target;
  const nearBottom = el.scrollTop
    + el.clientHeight >= el.scrollHeight - 100;

  if (nearBottom && !this.loading && this.hasMore) {
    this.loadMore();
  }
}

// 或用 IonInfiniteScroll(组件方案)
<ion-infinite-scroll (ionInfinite)="loadMore($event)">
  <ion-infinite-scroll-content>
  </ion-infinite-scroll-content>
</ion-infinite-scroll>

滚动性能优化

# 1. 大量列表用虚拟滚动
<ion-virtual-scroll [items]="items">
  <ion-item *virtualItem="let item">
    {{ item.name }}
  </ion-item>
</ion-virtual-scroll>

# 2. 避免滚动时复杂计算
# 3. 图片懒加载
<img [src]="item.thumb" loading="lazy">

# 4. 事件节流(throttle)
import { throttleTime } from "rxjs/operators";

// 滚动事件节流(60fps 内最多触发一次)
this.content.ionScroll
  .pipe(throttleTime(100))
  .subscribe((e) => this.onScroll(e));

# 5. 固定高度容器(减少 reflow)
# 6. iOS 橡皮筋效果(overscroll)
ion-content {
  overscroll-behavior: contain;
}

滚动位置恢复

// 返回时恢复滚动位置
import { IonContent } from "@ionic/angular";

@ViewChild(IonContent) content!: IonContent;
private savedPosition = 0;

ionViewWillEnter() {
  // 恢复位置
  this.content.scrollToPoint(0, this.savedPosition, 0);
}

ionViewWillLeave() {
  // 保存位置
  this.content.getScrollElement().then((el) => {
    this.savedPosition = el.scrollTop;
  });
}

// 或监听 ionScroll 持续保存
onScroll(event: any) {
  this.savedPosition = event.detail.scrollTop;
}

注意事项

# 1. 滚动事件高频触发,注意性能
# 2. scrollEvents=true 才触发事件
# 3. 嵌套滚动(内层 scroll)注意手势冲突
# 4. 固定头部/底部时内容滚动区适配
# 5. iOS 回弹效果是特性,勿强行禁用

# 常见问题:
# - 事件不触发: 未开 scrollEvents
# - 滚动卡顿: 列表未虚拟化
# - 位置不准: scrollToPoint 单位像素
# - 冲突: 横向滚动容器内禁纵向手势

滚动是内容的"流动方式"。掌握事件、定位与性能优化,长页面浏览流畅舒适。

ionic 侧栏菜单

Ionic 侧栏菜单

侧栏菜单(Side Menu / Menu)从屏幕侧边滑出导航或工具面板,是抽屉式导航的标配。

基础用法

<!-- 菜单容器 -->
<ion-menu contentId="main-content">
  <ion-header>
    <ion-toolbar>
      <ion-title>菜单</ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-content>
    <ion-list>
      <ion-item button (click)="goTo('home')">
        <ion-icon name="home" slot="start">
        </ion-icon>
        首页
      </ion-item>
      <ion-item button (click)="goTo('profile')">
        <ion-icon name="person" slot="start">
        </ion-icon>
        我的
      </ion-item>
    </ion-list>
  </ion-content>
</ion-menu>

<!-- 内容区域(id 对应) -->
<ion-router-outlet id="main-content">
</ion-router-outlet>

菜单开关

import { MenuController } from "@ionic/angular";

constructor(private menuCtrl: MenuController) {}

// 打开/关闭
openMenu() { this.menuCtrl.open(); }
closeMenu() { this.menuCtrl.close(); }
toggleMenu() { this.menuCtrl.toggle(); }

// 指定菜单
this.menuCtrl.open("main-menu");

// 头部按钮开关
<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
      <!-- 自动关联菜单 -->
    </ion-buttons>
    <ion-title>首页</ion-title>
  </ion-toolbar>
</ion-header>

// 手势滑动开关(默认启用)
// menu.swipeGesture = false;  // 禁用滑动

侧边与布局

<!-- 左侧(默认) -->
<ion-menu side="start" contentId="main">
</ion-menu>

<!-- 右侧 -->
<ion-menu side="end" contentId="main">
</ion-menu>

<!-- 两个菜单(左右各一) -->
<ion-menu side="start" menu-id="left-menu"
          contentId="main">
</ion-menu>
<ion-menu side="end" menu-id="right-menu"
          contentId="main">
</ion-menu>

<!-- 菜单宽度 -->
/* scss */
ion-menu {
  --width: 280px;
  --max-width: 80%;
  --min-width: 200px;
}

菜单内容: 用户信息

<ion-menu contentId="main">
  <ion-content>
    <!-- 用户信息头部 -->
    <div class="menu-profile"
         (click)="goProfile()">
      <ion-avatar>
        <img [src]="user.avatar">
      </ion-avatar>
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>

    <!-- 菜单项 -->
    <ion-list>
      <ion-item button *ngFor="let item of menuItems"
                (click)="navigate(item.url)">
        <ion-icon [name]="item.icon"
                  slot="start"></ion-icon>
        <ion-label>{{ item.label }}</ion-label>
        <ion-badge *ngIf="item.badge"
                   color="danger">
          {{ item.badge }}
        </ion-badge>
      </ion-item>
    </ion-list>

    <!-- 底部操作 -->
    <div class="menu-footer">
      <ion-item button (click)="logout()">
        <ion-icon name="log-out" slot="start">
        </ion-icon>
        退出登录
      </ion-item>
    </div>
  </ion-content>
</ion-menu>

/* scss */
.menu-profile {
  padding: 24px 16px;
  background: linear-gradient(135deg,
      #667eea, #764ba2);
  color: white;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.menu-footer {
  position: absolute;
  bottom: 0;
  width: 100%;
}

导航联动

// 点击菜单项: 跳转并关闭
async navigate(url: string) {
  await this.menuCtrl.close();      // 先关闭
  this.router.navigate([url]);
}

// 菜单高亮当前项
currentUrl = "";

ngOnInit() {
  this.router.events.subscribe((event) => {
    if (event instanceof NavigationEnd) {
      this.currentUrl = event.url;
    }
  });
}

// 模板判断高亮
<ion-item button
          [class.active]="currentUrl === item.url">

/* 高亮样式 */
ion-item.active {
  --background: #e8f0fe;
  color: #3880ff;
  font-weight: bold;
}

事件与状态

// 菜单事件
this.menuCtrl.events().subscribe((event) => {
  console.log("菜单事件:", event.type);
  // menuWillOpen / menuDidOpen
  // menuWillClose / menuDidClose
});

// 判断状态
const isOpen = await this.menuCtrl.isOpen();

// 禁用/启用
this.menuCtrl.enable(false, "left-menu");

// 打开时锁定内容滚动
ion-menu {
  --backdrop-opacity: 0.4;
}

响应式: 桌面显示固定侧栏

// 桌面端固定侧栏(非抽屉)
// 用 CSS 媒体查询
@media (min-width: 992px) {
  ion-menu {
    --width: 250px;
  }

  // 内容推挤而非覆盖
  ion-menu::part(container) {
    position: relative;
  }
}

// 或按平台配置
// 桌面: 菜单常驻显示
if (this.platform.is("desktop")) {
  await this.menuCtrl.open();
  this.menuCtrl.swipeGesture = false;
}

// 或者使用 split-pane(推荐)
<ion-split-pane when="lg">
  <ion-menu contentId="main">...</ion-menu>
  <ion-router-outlet id="main">
  </ion-router-outlet>
</ion-split-pane>
// 大屏: 侧栏常驻; 小屏: 抽屉

注意事项

# 1. contentId 必须与内容 id 匹配
# 2. 菜单内跳转记得关闭
# 3. 多菜单用 menu-id 区分
# 4. 手势滑动与横向滚动冲突时禁用
# 5. 桌面用 split-pane 更友好

# 常见问题:
# - 菜单不显示: contentId 不匹配
# - 打不开: enable(false) 或手势禁用
# - 内容穿透: 检查 z-index/背景

侧栏菜单是"抽屉导航"的标准方案。掌握打开方式、用户信息区与导航联动,再配合 split-pane 响应式,导航体验完整。

ionic 滑动框

Ionic 滑动框(Slides/Carousel)

滑动框(轮播/幻灯片)用于展示多张图片或页面:首页 Banner、引导页、图片查看等。Ionic 集成 Swiper 实现。

基础轮播

<!-- Swiper 轮播(现代 Ionic) -->
<ion-slides>    <!-- 旧版(已弃用) -->

# 新版使用 Swiper:
# 安装
npm install swiper

# 引入(angular.json 或组件中)
import { SwiperModule } from "swiper/angular";
imports: [SwiperModule]

// 模板
<swiper>
  <ng-template swiperSlide *ngFor="let img of images">
    <img [src]="img">
  </ng-template>
</swiper>

Swiper 配置

// TS 配置
slidesPerView = 1;
pagination = true;
navigation = true;
autoplay = { delay: 3000, disableOnInteraction: false };
loop = true;

<!-- 模板绑定 -->
<swiper [slidesPerView]="slidesPerView"
        [pagination]="pagination"
        [autoplay]="autoplay"
        [loop]="loop">
  <ng-template swiperSlide *ngFor="let b of banners">
    <img [src]="b.image">
  </ng-template>
</swiper>

// 常见配置:
// slidesPerView: 每屏显示数
// spaceBetween: 间距
// loop: 循环
// autoplay: 自动播放
// pagination: 分页点
// speed: 动画速度
// direction: horizontal/vertical

首页 Banner 案例

<div class="banner-wrapper">
  <swiper [slidesPerView]="1"
          [pagination]="{ clickable: true }"
          [autoplay]="{ delay: 4000 }"
          [loop]="true"
          (swiperSlideChange)="onSlide($event)">
    <ng-template swiperSlide
                 *ngFor="let banner of banners">
      <div class="banner-item"
           (click)="openBanner(banner)">
        <img [src]="banner.image">
        <div class="banner-title">
          {{ banner.title }}
        </div>
      </div>
    </ng-template>
  </swiper>
</div>

/* scss */
.banner-wrapper {
  height: 160px;
  border-radius: 12px;
  overflow: hidden;
  margin: 8px 0;
}

.banner-item {
  position: relative;
  height: 100%;
  img { width: 100%; height: 100%;
        object-fit: cover; }
}

.banner-title {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  padding: 24px 12px 8px;
  background: linear-gradient(transparent, rgba(0,0,0,.6));
  color: white;
  font-size: 14px;
}

多图滑动(商品图/画廊)

<!-- 商品轮播 -->
<swiper [slidesPerView]="1"
        [pagination]="{ clickable: true }">
  <ng-template swiperSlide
               *ngFor="let img of product.images">
    <div class="product-img">
      <img [src]="img">
    </div>
  </ng-template>
</swiper>

<!-- 多列滑动(推荐) -->
<swiper [slidesPerView]="2.5"
        [spaceBetween]="8"
        [freeMode]="true">
  <ng-template swiperSlide
               *ngFor="let item of recommendations">
    <ion-card>...</ion-card>
  </ng-template>
</swiper>

// freeMode: 自由滑动(无吸附)

滑动事件

<swiper (swiperSlideChange)="onSlideChange($event)"
        (swiperReachBeginning)="onFirst()"
        (swiperReachEnd)="onLast()">
</swiper>

// TS(通过 Swiper 实例)
onSlideChange(event: any) {
  const swiper = event.detail[0];
  console.log("当前页:", swiper.activeIndex);
  this.currentIndex = swiper.activeIndex;
}

// 获取实例(程序控制)
import Swiper from "swiper";

@ViewChild("mySwiper") swiperRef: any;

nextSlide() {
  this.swiperRef.swiper.slideNext();
}

prevSlide() {
  this.swiperRef.swiper.slidePrev();
}

goToSlide(n: number) {
  this.swiperRef.swiper.slideTo(n);
}

引导页案例

<!-- 首次启动引导 -->
<swiper [slidesPerView]="1">
  <ng-template swiperSlide *ngFor="let page of pages">
    <div class="onboarding">
      <img [src]="page.image">
      <h2>{{ page.title }}</h2>
      <p>{{ page.desc }}</p>
    </div>
  </ng-template>
</swiper>

<!-- 底部按钮(最后一页显示) -->
<ion-button expand="block"
            *ngIf="currentIndex === pages.length - 1"
            (click)="finishOnboarding()">
  开始使用
</ion-button>

// TS
pages = [
  { title: "欢迎", desc: "...", image: "..." },
  { title: "功能", desc: "...", image: "..." },
  { title: "开始", desc: "...", image: "..." },
];

finishOnboarding() {
  localStorage.setItem("onboarded", "true");
  this.router.navigate(["/home"]);
}

图片查看器(全屏)

<!-- 全屏图片查看 -->
<ion-modal [isOpen]="showViewer">
  <ng-template>
    <ion-content>
      <swiper [initialSlide]="viewIndex"
              [zoom]="true">   <!-- 支持缩放 -->
        <ng-template swiperSlide
                     *ngFor="let img of images">
          <div class="viewer-wrap">
            <img [src]="img" (click)="close()">
          </div>
        </ng-template>
      </swiper>
      <ion-button fill="clear" color="light"
                  class="close-btn"
                  (click)="close()">
        <ion-icon name="close"></ion-icon>
      </ion-button>
    </ion-content>
  </ng-template>
</ion-modal>

/* 缩放: swiper [zoom]="true" 需引入 Zoom 模块 */

注意事项

# 1. 旧版 ion-slides 已弃用,用 swiper
# 2. swiperSlideChange 事件取实例方式
# 3. 图片懒加载(大数据量)
# 4. 自动播放注意用户交互暂停
# 5. 高度适配内容(容器固定高度)

# 常见问题:
# - 滑动不生效: 模块未导入
# - 分页点不显示: pagination 配置
# - 循环异常: loop 与 slidesPerView 配合
# - 图片变形: object-fit: cover

# 性能: 轮播图用 CDN 压缩图
# 懒加载: <img loading="lazy">

滑动框让内容"流动起来"。掌握 Swiper 配置、事件与多场景应用,轮播、画廊、引导页一次到位。

ionic 加载动画

Ionic 加载动画(Spinner/Skeleton)

加载动画反馈数据加载状态,提升体验。Ionic 提供 Spinner(转圈)、Skeleton(骨架屏)与 Progress(进度)多种形式。

Spinner 转圈

<!-- 基础 -->
<ion-spinner></ion-spinner>

<!-- 类型 -->
<ion-spinner name="circles"></ion-spinner>
<ion-spinner name="bubbles"></ion-spinner>
<ion-spinner name="crescent"></ion-spinner>
<ion-spinner name="dots"></ion-spinner>
<ion-spinner name="lines"></ion-spinner>
<ion-spinner name="lines-small"></ion-spinner>

<!-- 尺寸与颜色 -->
<ion-spinner name="crescent"
             color="primary"
             style="font-size: 32px;">
</ion-spinner>

/* scss 定制 */
ion-spinner {
  --spinner-color: #3880ff;
}

/* 全屏居中加载 */
.loading-center {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 60vh;
}

骨架屏(Skeleton)

<!-- 列表骨架屏 -->
<ion-list *ngIf="loading">
  <ion-item *ngFor="let _ of [].constructor(6)">
    <ion-avatar slot="start">
      <ion-skeleton-text></ion-skeleton-text>
    </ion-avatar>
    <ion-label>
      <h3>
        <ion-skeleton-text
          animated style="width: 60%">
        </ion-skeleton-text>
      </h3>
      <p>
        <ion-skeleton-text
          animated style="width: 90%">
        </ion-skeleton-text>
      </p>
    </ion-label>
  </ion-item>
</ion-list>

<!-- 卡片骨架屏 -->
<ion-card *ngIf="loading">
  <ion-skeleton-text animated
    style="width: 100%; height: 180px;">
  </ion-skeleton-text>
  <ion-card-header>
    <ion-skeleton-text animated
      style="width: 70%">
    </ion-skeleton-text>
  </ion-card-header>
</ion-card>

// animated: 呼吸动画
// 真实数据到达后 *ngIf 切换

内容切换模式

// 骨架屏与内容切换
loading = true;

ngOnInit() {
  this.loadData();
}

async loadData() {
  this.loading = true;    // 显示骨架
  try {
    this.items = await this.api.fetch();
  } finally {
    this.loading = false; // 显示内容
  }
}

<!-- 模板: 三态切换 -->
<!-- 加载中: 骨架屏 -->
<ion-skeleton-text *ngIf="loading">...</ion-skeleton-text>

<!-- 有数据: 内容 -->
<ion-list *ngIf="!loading && items.length">...</ion-list>

<!-- 空: 空状态 -->
<div *ngIf="!loading && !items.length">...</div>

Progress 进度条

<!-- 确定进度 -->
<ion-progress-bar [value]="progress"
                  color="primary">
</ion-progress-bar>

<!-- 不确定进度(缓冲动画) -->
<ion-progress-bar type="indeterminate">
</ion-progress-bar>

<!-- 缓冲区 -->
<ion-progress-bar [value]="0.5"
                  [buffer]="0.8">
</ion-progress-bar>

// TS 更新进度
uploadProgress = 0;
onUpload(event: any) {
  this.uploadProgress =
    event.loaded / event.total;
}

// 颜色
<ion-progress-bar value="0.8"
                  color="success">
</ion-progress-bar>

/* 自定义 */
ion-progress-bar {
  --progress-background: #3880ff;
  --buffer-background: #e8f0fe;
  height: 6px;
  border-radius: 3px;
}

加载状态组合

<!-- 按钮加载 -->
<ion-button [disabled]="loading">
  <ion-spinner name="crescent"
               *ngIf="loading">
  </ion-spinner>
  <span *ngIf="!loading">提交</span>
  <span *ngIf="loading">提交中...</span>
</ion-button>

<!-- 刷新指示 -->
<ion-refresher-content
  refreshingSpinner="circles"
  refreshingText="刷新中...">
</ion-refresher-content>

<!-- 加载更多 -->
<ion-infinite-scroll-content
  loadingSpinner="bubbles"
  loadingText="加载更多...">
</ion-infinite-scroll-content>

最佳实践

# 1. 首选骨架屏(体验最好,不跳屏)
# 2. 局部加载用 spinner
# 3. 上传/下载用进度条
# 4. 加载完成平滑过渡(淡入)
# 5. 加载失败给重试按钮

// 淡入过渡
.content-loaded {
  animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

# 6. 避免加载动画闪烁:
#    延迟 200ms 显示骨架(快速请求不闪)
# 7. 骨架屏形状尽量接近真实内容

加载动画是体验的"润滑剂"。掌握 Spinner/Skeleton/Progress 三种形态与三态切换,加载过程优雅不焦虑。

ionic 选项卡栏操作

Ionic 选项卡栏操作(Tab Bar 进阶)

本节深入选项卡栏的进阶操作:角标管理、隐藏/显示、自定义样式、中间凸起按钮与动态切换。

角标管理

<ion-tab-button tab="messages">
  <ion-icon name="chatbubbles"></ion-icon>
  <ion-label>消息</ion-label>

  <!-- 角标 -->
  <ion-badge color="danger" *ngIf="unread > 0">
    {{ unread > 99 ? "99+" : unread }}
  </ion-badge>

  <!-- 小红点 -->
  <span class="dot" *ngIf="hasNew"></span>
</ion-tab-button>

// TS 动态更新
unread = 5;
hasNew = false;

// 收到消息
onMessage() {
  this.unread++;
}

// 已读
markRead() {
  this.unread = 0;
  this.hasNew = false;
}

/* 小红点样式 */
.dot {
  position: absolute;
  top: 4px;
  right: 14px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #eb445a;
}

隐藏与显示

// 某些页面隐藏 Tab 栏
// 方案1: *ngIf 控制(动态)
<ion-tab-bar slot="bottom"
             *ngIf="showTabBar">
</ion-tab-bar>

// TS
showTabBar = true;

// 进入隐藏页面前
ionViewWillEnter() {
  this.showTabBar = !this.hideOnThisPage;
}

// 方案2: 路由配置(custom 页面)
// 把页面放在 tabs 外(独立路由)
{ path: "fullscreen", component: FullscreenPage }

// 方案3: 样式隐藏
ion-tab-bar {
  display: none;
}

自定义样式

/* 全局 Tab 样式 */
ion-tab-bar {
  --background: #ffffff;
  --border: 1px solid #eee;
  --color: #999;              /* 未选中 */
  --color-selected: #3880ff;  /* 选中 */
  height: 60px;
}

/* 选中项放大动画 */
ion-tab-button {
  transition: transform 0.2s;
}
ion-tab-button.tab-selected {
  transform: scale(1.1);
  color: #3880ff;
}

/* 图标底色 */
ion-tab-button.tab-selected ion-icon {
  background: #e8f0fe;
  border-radius: 50%;
  padding: 4px;
}

/* 暗色模式 */
@media (prefers-color-scheme: dark) {
  ion-tab-bar {
    --background: #1e1e1e;
    --border: 1px solid #333;
    --color: #888;
    --color-selected: #8a7dff;
  }
}

中间凸起按钮

<!-- 发布按钮(凸起) -->
<ion-tab-bar slot="bottom">
  <ion-tab-button tab="home">...</ion-tab-button>

  <!-- 中间发布 -->
  <div class="center-btn"
       (click)="openPublish()">
    <ion-icon name="add"></ion-icon>
  </div>

  <ion-tab-button tab="profile">...</ion-tab-button>
</ion-tab-bar>

/* scss */
.center-btn {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: linear-gradient(135deg,
      #667eea, #764ba2);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28px;
  margin-top: -24px;      /* 凸起 */
  box-shadow: 0 4px 12px rgba(102,126,234,.4);
}

.center-btn ion-icon {
  font-size: 28px;
}

程序化切换与刷新

import { IonTabs } from "@ionic/angular";

@ViewChild(IonTabs) tabs!: IonTabs;

// 切换到指定 tab
goToTab(name: string) {
  this.tabs.select(name);
}

// 获取当前 tab
getCurrentTab() {
  return this.tabs.getSelected();
}

// 事件: tab 变更
this.tabs.ionTabsDidChange.subscribe(({ tab }) => {
  console.log("当前 tab:", tab);
  // 切回首页刷新
  if (tab === "home") {
    this.refreshHome();
  }
});

// 双击刷新(常见交互)
lastTap = 0;
onTabClick(name: string) {
  const now = Date.now();
  if (now - this.lastTap < 300
      && name === this.tabs.getSelected()) {
    this.refresh();      // 双击刷新
  }
  this.lastTap = now;
}

Tab 与数据状态

// 跨 tab 共享状态(服务)
@Injectable({ providedIn: "root" })
export class CartService {
  private count = new BehaviorSubject(0);
  count$ = this.count.asObservable();

  add() { this.count.next(this.count.value + 1); }
}

// 购物车角标
export class TabPage {
  cartCount = 0;
  constructor(private cart: CartService) {
    this.cart.count$.subscribe(n => {
      this.cartCount = n;
    });
  }
}

// 模板
<ion-badge color="danger" *ngIf="cartCount">
  {{ cartCount }}
</ion-badge>

注意事项

# 1. 角标更新用服务/Subject(跨页同步)
# 2. 隐藏 Tab 时确保返回机制
# 3. 凸起按钮不占 tab 位(独立 div)
# 4. 选中样式用 tab-selected 类
# 5. 暗色模式完整适配
# 6. 多 tab 数据懒加载

# 常见问题:
# - 角标不同步: 用服务共享
# - 凸起被遮挡: 调整 z-index
# - 隐藏后无法返回: 提供关闭/返回按钮

选项卡栏是 App 的"一级导航+状态中心"。掌握角标、隐藏、凸起按钮与数据共享,导航栏专业且鲜活。

Ionic 教程

Ionic 教程总结

恭喜你完成了 Ionic 教程的学习!本节回顾核心知识,并给出从"会用组件"到"完整上架"的进阶路线。

核心知识地图

# 基础
- 安装与项目创建(CLI/模板)
- 项目结构与运行

# 布局
- 头部/底部(工具栏/slot)
- 网格(12 列响应式)
- 列表/卡片
- 侧栏菜单/选项卡
- 滚动条/滑动框

# 表单
- 输入框/文本域
- 复选框(多选)
- 单选框(互斥)
- Toggle(开关)
- Select(选择器)
- Range(滑块)

# 交互
- 按钮/图标
- 手势事件
- 下拉刷新/上拉加载
- 加载动画(Spinner/骨架屏)
- 对话框(Alert)
- 上拉菜单(ActionSheet)
- 模态窗口(Modal)
- 浮动框(Popover)

# 进阶
- 导航与路由
- 平台与原生能力(Capacitor)
- 颜色主题与暗色模式

实战能力清单

# 学完本教程,你应该能:
# 1. 创建并运行 Ionic 项目
# 2. 搭建完整的页面布局
# 3. 实现各类表单与校验
# 4. 列表增删改查与搜索
# 5. 弹窗/菜单/模态交互
# 6. 多页面导航与参数传递
# 7. 下拉刷新/上拉加载
# 8. 调用相机/定位等原生能力
# 9. 主题定制与暗色适配
# 10. 打包 Android/iOS/Web

# 自测项目:
# - 待办清单(CRUD + 本地存储)
# - 新闻阅读器(列表+详情+收藏)
# - 商城 App(商品+购物车+下单)
# - 记账应用(表单+图表+统计)

进阶学习路线

# 1. 状态管理
# - Angular: Services + RxJS(基础)
# - NgRx / Akita(大型应用)

# 2. 后端对接
# - REST API(HTTPClient/拦截器)
# - 认证(JWT/OAuth)
# - WebSocket(实时)

# 3. 原生能力
# - Capacitor 插件生态
# - 推送/支付/地图/相机

# 4. 性能优化
# - 懒加载/虚拟滚动
# - 代码分割/预渲染
# - 图片优化/缓存

# 5. 测试与发布
# - 单元测试(Jasmine/Karma)
# - E2E 测试(Cypress)
# - App Store / 应用市场发布

资源推荐

# 官方文档(权威)
# https://ionicframework.com/docs

# 官方示例
# https://github.com/ionic-team/ionic
# (ionicons、starter 模板)

# 中文社区
# CSDN / 掘金 / 思否(Ionic 标签)

# 学习建议:
# 1. 官方文档的组件页逐个过一遍
# 2. 用 starter 模板改造成自己的应用
# 3. 真机调试(Capacitor)
# 4. 阅读优秀开源 App 源码

常见误区

# 1. ❌ 只用 Web 思路写移动端
#    ✅ 遵循移动端交互习惯(手势/触控区域)

# 2. ❌ 忽略安全区/状态栏适配
#    ✅ 用 Ionic 内置组件自动适配

# 3. ❌ 大量原生调用不封装
#    ✅ 封装成服务,便于测试与替换

# 4. ❌ 不区分平台差异
#    ✅ 按平台优化(桌面/平板/手机)

# 5. ❌ 忽略性能
#    ✅ 虚拟滚动/懒加载/图片优化

最后建议

# 1. 动手: 把每个组件在真项目里用一遍
# 2. 模仿: 克隆主流 App 的一个页面练手
# 3. 迭代: 从简单功能逐步完整
# 4. 上架: 完成一个小应用走全流程
# 5. 关注更新: Ionic 与 Capacitor 迭代快

# Ionic 的价值:
# 一套 Web 技能,构建多端应用
# 开发效率高,生态成熟

# 祝你用 Ionic 构建出优秀的移动应用!
第 1 / 37 节