关于 ionic-skills
ionic-skills 是使用 Angular、React 或 Vue 开发 Ionic Capacitor 移动应用的指南。它规定导航、必需页面、插件和存储规范,并提供各框架对应的实现模式。
主要功能
- 01涵盖 Angular、React 和 Vue
- 02规定引导、付费墙和设置页面
- 03统一采用 ion-tabs 标签导航
- 04列出 Capacitor 插件与存储规范
- 05提供路由和配置示例
适用场景
- 01启动 Ionic Capacitor 移动应用
- 02规划不同框架的应用结构
- 03实现存储与原生插件调用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ erkamyaman/ ionic-capacitor-skills --skill ionic-skills安装后在 Claude Code 中输入/ionic-skills确认已加载。
附带文件metadata.json
详细介绍
Ionic Capacitor 应用开发指南
重要:这是一个 SKILL 文件,不是项目。切勿在此文件夹中运行 npm install。切勿在此处创建代码文件。创建新项目时,务必先询问用户项目路径,或在单独的目录中创建(例如
~/Projects/app-name)。
本指南介绍如何使用 Angular、React 或 Vue,通过 Ionic Capacitor 构建可用于生产环境的移动应用。
必须遵守的要求
创建新的 Ionic 项目时,必须包含以下所有内容:
必需页面(始终创建)
- 引导页 - 通过滑动切换的引导页,使用全屏背景视频和渐变叠加层
- 付费墙页面 - RevenueCat 付费墙页面(在引导页之后显示)
- 设置页面 - 设置语言、主题、通知和重置引导页选项
必需导航(始终使用)
- 使用
ion-tabs和ion-tab-bar进行标签导航——切勿使用自定义标签导航或第三方标签库
必需库(始终安装)
Angular
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob @ngx-translate/core @ngx-translate/http-loader swiper
React
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob react-i18next i18next i18next-http-backend swiper
Vue
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob vue-i18n swiper
通用库(所有框架)
@revenuecat/purchases-capacitor(RevenueCat)@capacitor-community/admob(AdMob)@capacitor/push-notifications(推送通知)@capacitor/preferences(键值存储)@capacitor/splash-screen(启动屏幕控制)@capacitor/status-bar(状态栏样式)swiper(引导页幻灯片)
框架专用 i18n 库
| 框架 | 库 | 用法 |
|---|---|---|
| Angular | @ngx-translate/core + @ngx-translate/http-loader | translate 管道 |
| React | react-i18next + i18next | useTranslation() Hook |
| Vue | vue-i18n | useI18n() 组合式函数 / $t() |
禁止使用(切勿使用)
所有框架
- ❌ 直接使用
localStorage- 改用@capacitor/preferences - ❌ 使用
@ionic/storage- 改用@capacitor/preferences - ❌ 使用自定义标签栏 - 改用
ion-tabs+ion-tab-bar - ❌ 使用
cordova-plugin-*插件 - 改用 Capacitor 插件 - ❌ 使用
any类型 - 始终使用合适的 TypeScript 类型 - ❌ 使用
ngx-admob-free或其他已弃用的广告库 - 只能使用@capacitor-community/admob - ❌ 同步调用 Capacitor - 始终对 Capacitor 插件方法使用
await
Angular 专属
- ❌ 为新页面或组件使用 NgModules - 改用独立组件
- ❌ 在独立组件中使用
IonicModule- 导入单个组件(IonButton、IonContent等) - ❌ 在
@Component中使用内联template或styles- 使用单独的.html、.ts、.scss文件,并设置templateUrl和styleUrls - ❌ 使用
@angular/http(已弃用)- 改用@angular/common/http
React 专属
- ❌ 使用类组件 - 改用带 Hooks 的函数组件
- ❌ 直接操作 DOM - 使用 React refs 和状态
- ❌ 导入
@ionic/angular- 改用@ionic/react
Vue 专属
- ❌ 为新代码使用选项式 API - 改用包含
<script setup>的组合式 API - ❌ 直接操作 DOM - 使用 Vue refs 和响应式功能
- ❌ 导入
@ionic/angular- 改用@ionic/vue
技术栈
| 关注点 | Angular | React | Vue |
|---|---|---|---|
| 框架 | Ionic 8 + Angular 19 | Ionic 8 + React 19 | Ionic 8 + Vue 3.5 |
| 原生运行时 | Capacitor 7 | Capacitor 7 | Capacitor 7 |
| 导航 | Angular Router(延迟加载) | @ionic/react-router | @ionic/vue-router |
| 标签页导航 | ion-tabs + ion-tab-bar | ion-tabs + ion-tab-bar | ion-tabs + ion-tab-bar |
| 状态管理 | Angular Services(Signals/RxJS) | Custom Hooks / Context | Composables / Pinia |
| 翻译 | @ngx-translate/core | react-i18next | vue-i18n |
| 内购 | @revenuecat/purchases-capacitor | @revenuecat/purchases-capacitor | @revenuecat/purchases-capacitor |
| 广告 | @capacitor-community/admob | @capacitor-community/admob | @capacitor-community/admob |
| 通知 | @capacitor/push-notifications | @capacitor/push-notifications | @capacitor/push-notifications |
| 存储 | @capacitor/preferences | @capacitor/preferences | @capacitor/preferences |
WARNING:切勿直接使用
localStorage!跨平台持久化存储请改用@capacitor/preferences。
创建项目
用户要求创建应用时,你 MUST:
- 首先询问 Bundle ID(例如:“Bundle ID 是什么?示例:com.company.appname”)
- 在当前目录中创建项目
- 然后实现所有必需页面
创建项目(Angular)
npm install -g @ionic/cli
ionic start app-name blank --type=angular --capacitor
创建项目(React)
npm install -g @ionic/cli
ionic start app-name blank --type=react --capacitor
创建项目(Vue)
npm install -g @ionic/cli
ionic start app-name blank --type=vue --capacitor
Capacitor 配置(所有框架)
// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.company.appname',
appName: 'App Name',
webDir: 'www',
server: {
androidScheme: 'https',
},
};
export default config;
Note:对于 React/Vue 项目,
webDir可能是'dist',而不是'www'。请检查项目的构建输出目录。
添加原生平台
npx cap add ios
npx cap add android
项目结构
Angular 项目结构
project-root/
├── src/
│ ├── app/
│ │ ├── app.component.ts
│ │ ├── app.component.html
│ │ ├── app.config.ts
│ │ ├── app.routes.ts
│ │ ├── tabs/
│ │ │ ├── tabs.page.ts
│ │ │ ├── tabs.page.html
│ │ │ ├── tabs.page.scss
│ │ │ └── tabs.routes.ts
│ │ ├── home/
│ │ │ ├── home.page.ts
│ │ │ ├── home.page.html
│ │ │ └── home.page.scss
│ │ ├── explore/
│ │ │ ├── explore.page.ts
│ │ │ ├── explore.page.html
│ │ │ └── explore.page.scss
│ │ ├── settings/
│ │ │ ├── settings.page.ts
│ │ │ ├── settings.page.html
│ │ │ └── settings.page.scss
│ │ ├── paywall/
│ │ │ ├── paywall.page.ts
│ │ │ ├── paywall.page.html
│ │ │ └── paywall.page.scss
│ │ ├── onboarding/
│ │ │ ├── onboarding.page.ts
│ │ │ ├── onboarding.page.html
│ │ │ └── onboarding.page.scss
│ │ ├── services/
│ │ │ ├── theme.service.ts
│ │ │ ├── onboarding.service.ts
│ │ │ ├── ads.service.ts
│ │ │ ├── purchases.service.ts
│ │ │ └── notifications.service.ts
│ │ ├── guards/
│ │ │ └── onboarding.guard.ts
│ │ └── utils/
│ │ ├── admob.ts
│ │ ├── purchases.ts
│ │ ├── onboarding.ts
│ │ ├── theme.ts
│ │ └── notifications.ts
│ ├── assets/
│ │ └── i18n/
│ │ ├── en.json
│ │ └── tr.json
│ ├── theme/
│ │ └── variables.scss
│ ├── global.scss
│ ├── index.html
│ └── main.ts
├── ios/
├── android/
├── capacitor.config.ts
├── angular.json
├── package.json
└── tsconfig.json
React 项目结构
project-root/
├── src/
│ ├── App.tsx
│ ├── main.tsx
│ ├── pages/
│ │ ├── OnboardingPage.tsx
│ │ ├── PaywallPage.tsx
│ │ ├── SettingsPage.tsx
│ │ ├── HomePage.tsx
│ │ └── ExplorePage.tsx
│ ├── components/
│ │ ├── TabsLayout.tsx
│ │ └── OnboardingGuard.tsx
│ ├── hooks/
│ │ ├── useTheme.ts
│ │ ├── useOnboarding.ts
│ │ ├── useAds.ts
│ │ ├── usePurchases.ts
│ │ └── useNotifications.ts
│ ├── utils/
│ │ ├── admob.ts
│ │ ├── purchases.ts
│ │ ├── onboarding.ts
│ │ ├── theme.ts
│ │ └── notifications.ts
│ ├── theme/
│ │ └── variables.css
│ └── i18n/
│ ├── index.ts
│ ├── en.json
│ └── tr.json
├── public/
├── ios/
├── android/
├── capacitor.config.ts
├── package.json
└── tsconfig.json
Vue 项目结构
project-root/
├── src/
│ ├── App.vue
│ ├── main.ts
│ ├── router/
│ │ └── index.ts
│ ├── views/
│ │ ├── OnboardingPage.vue
│ │ ├── PaywallPage.vue
│ │ ├── SettingsPage.vue
│ │ ├── HomePage.vue
│ │ ├── ExplorePage.vue
│ │ └── TabsLayout.vue
│ ├── composables/
│ │ ├── useTheme.ts
│ │ ├── useOnboarding.ts
│ │ ├── useAds.ts
│ │ ├── usePurchases.ts
│ │ └── useNotifications.ts
│ ├── utils/
│ │ ├── admob.ts
│ │ ├── purchases.ts
│ │ ├── onboarding.ts
│ │ ├── theme.ts
│ │ └── notifications.ts
│ ├── theme/
│ │ └── variables.css
│ └── assets/
│ └── i18n/
│ ├── en.json
│ └── tr.json
├── ios/
├── android/
├── capacitor.config.ts
├── package.json
└── tsconfig.json
应用配置
应用配置(Angular)
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, RouteReuseStrategy } from '@angular/router';
import { provideIonicAngular, IonicRouteStrategy } from '@ionic/angular/standalone';
import { provideHttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { routes } from './app.routes';
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideIonicAngular({ mode: 'md' }),
provideHttpClient(),
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
importProvidersFrom(
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
})
),
],
};
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';
import { AdsService } from './services/ads.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [IonApp, IonRouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
constructor(private adsService: AdsService) {}
async ngOnInit() {
await this.adsService.initialize();
}
}
应用配置(React)
// main.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
/* Core CSS required for Ionic components */
import '@ionic/react/css/core.css';
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';
import './theme/variables.css';
import './i18n';
const root = createRoot(document.getElementById('root')!);
root.render(<App />);
// App.tsx
import { useEffect } from 'react';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { Route, Redirect } from 'react-router-dom';
import OnboardingPage from './pages/OnboardingPage';
import PaywallPage from './pages/PaywallPage';
import TabsLayout from './components/TabsLayout';
import { OnboardingGuard } from './components/OnboardingGuard';
import { initializeAdMob } from './utils/admob';
setupIonicReact({ mode: 'md' });
const App: React.FC = () => {
useEffect(() => {
initializeAdMob();
}, []);
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/onboarding" component={OnboardingPage} />
<Route exact path="/paywall" component={PaywallPage} />
<Route path="/tabs">
<OnboardingGuard>
<TabsLayout />
</OnboardingGuard>
</Route>
<Route exact path="/">
<Redirect to="/tabs" />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
};
export default App;
// i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './en.json';
import tr from './tr.json';
const browserLang = navigator.language.split('-')[0];
i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
tr: { translation: tr },
},
lng: ['en', 'tr'].includes(browserLang) ? browserLang : 'en',
fallbackLng: 'en',
interpolation: { escapeValue: false },
});
export default i18n;
应用配置(Vue)
// main.ts
import { createApp } from 'vue';
import { IonicVue } from '@ionic/vue';
import App from './App.vue';
import router from './router';
import { createI18n } from 'vue-i18n';
import en from './assets/i18n/en.json';
import tr from './assets/i18n/tr.json';
/* Core CSS required for Ionic components */
import '@ionic/vue/css/core.css';
import '@ionic/vue/css/normalize.css';
import '@ionic/vue/css/structure.css';
import '@ionic/vue/css/typography.css';
import '@ionic/vue/css/padding.css';
import '@ionic/vue/css/float-elements.css';
import '@ionic/vue/css/text-alignment.css';
import '@ionic/vue/css/text-transformation.css';
import '@ionic/vue/css/flex-utils.css';
import '@ionic/vue/css/display.css';
import './theme/variables.css';
const browserLang = navigator.language.split('-')[0];
const i18n = createI18n({
legacy: false,
locale: ['en', 'tr'].includes(browserLang) ? browserLang : 'en',
fallbackLocale: 'en',
messages: { en, tr },
});
const app = createApp(App);
app.use(IonicVue, { mode: 'md' });
app.use(router);
app.use(i18n);
router.isReady().then(() => app.mount('#app'));
<template>
<ion-app>
<ion-router-outlet />
</ion-app>
</template>
<script setup lang="ts">
import { IonApp, IonRouterOutlet } from '@ionic/vue';
import { onMounted } from 'vue';
import { initializeAdMob } from './utils/admob';
onMounted(async () => {
await initializeAdMob();
});
</script>
路由
路由(Angular)
// app.routes.ts
import { Routes } from '@angular/router';
import { onboardingGuard } from './guards/onboarding.guard';
export const routes: Routes = [
{
path: 'onboarding',
loadComponent: () => import('./onboarding/onboarding.page').then(m => m.OnboardingPage),
},
{
path: 'paywall',
loadComponent: () => import('./paywall/paywall.page').then(m => m.PaywallPage),
},
{
path: 'tabs',
loadChildren: () => import('./tabs/tabs.routes').then(m => m.tabsRoutes),
canActivate: [onboardingGuard],
},
{
path: '',
redirectTo: 'tabs',
pathMatch: 'full',
},
];
// tabs/tabs.routes.ts
import { Routes } from '@angular/router';
import { TabsPage } from './tabs.page';
export const tabsRoutes: Routes = [
{
path: '',
component: TabsPage,
children: [
{
path: 'home',
loadComponent: () => import('../home/home.page').then(m => m.HomePage),
},
{
path: 'explore',
loadComponent: () => import('../explore/explore.page').then(m => m.ExplorePage),
},
{
path: 'settings',
loadComponent: () => import('../settings/settings.page').then(m => m.SettingsPage),
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full',
},
],
},
];
路由(React)
路由定义在 App.tsx 中(参见上文“应用配置”)。标签页路由定义在 TabsLayout 组件中:
// components/TabsLayout.tsx
import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
import { Route, Redirect } from 'react-router-dom';
import { home, compass, settings } from 'ionicons/icons';
import { useTranslation } from 'react-i18next';
import { useEffect } from 'react';
import HomePage from '../pages/HomePage';
import ExplorePage from '../pages/ExplorePage';
import SettingsPage from '../pages/SettingsPage';
import { showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
const TabsLayout: React.FC = () => {
const { t } = useTranslation();
useEffect(() => {
isPremiumUser().then((premium) => {
if (!premium) showBannerAd();
});
return () => { hideBannerAd(); };
}, []);
return (
<IonTabs>
<IonRouterOutlet>
<Route exact path="/tabs/home" component={HomePage} />
<Route exact path="/tabs/explore" component={ExplorePage} />
<Route exact path="/tabs/settings" component={SettingsPage} />
<Route exact path="/tabs">
<Redirect to="/tabs/home" />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="home" href="/tabs/home">
<IonIcon icon={home} />
<IonLabel>{t('tabs.home')}</IonLabel>
</IonTabButton>
<IonTabButton tab="explore" href="/tabs/explore">
<IonIcon icon={compass} />
<IonLabel>{t('tabs.explore')}</IonLabel>
</IonTabButton>
<IonTabButton tab="settings" href="/tabs/settings">
<IonIcon icon={settings} />
<IonLabel>{t('tabs.settings')}</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
export default TabsLayout;
路由(Vue)
// router/index.ts
import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router';
import { isOnboardingCompleted } from '../utils/onboarding';
import TabsLayout from '../views/TabsLayout.vue';
const routes: RouteRecordRaw[] = [
{
path: '/onboarding',
component: () => import('../views/OnboardingPage.vue'),
},
{
path: '/paywall',
component: () => import('../views/PaywallPage.vue'),
},
{
path: '/tabs/',
component: TabsLayout,
children: [
{
path: '',
redirect: '/tabs/home',
},
{
path: 'home',
component: () => import('../views/HomePage.vue'),
},
{
path: 'explore',
component: () => import('../views/ExplorePage.vue'),
},
{
path: 'settings',
component: () => import('../views/SettingsPage.vue'),
},
],
},
{
path: '/',
redirect: '/tabs/',
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
router.beforeEach(async (to, _from, next) => {
if (to.path.startsWith('/tabs') || to.path === '/') {
const completed = await isOnboardingCompleted();
if (!completed) {
return next('/onboarding');
}
}
next();
});
export default router;
<template>
<ion-page>
<ion-tabs>
<ion-router-outlet />
<ion-tab-bar slot="bottom">
<ion-tab-button tab="home" href="/tabs/home">
<ion-icon :icon="home" />
<ion-label>{{ t('tabs.home') }}</ion-label>
</ion-tab-button>
<ion-tab-button tab="explore" href="/tabs/explore">
<ion-icon :icon="compass" />
<ion-label>{{ t('tabs.explore') }}</ion-label>
</ion-tab-button>
<ion-tab-button tab="settings" href="/tabs/settings">
<ion-icon :icon="settings" />
<ion-label>{{ t('tabs.settings') }}</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-page>
</template>
<script setup lang="ts">
import {
IonPage, IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel,
} from '@ionic/vue';
import { home, compass, settings } from 'ionicons/icons';
import { useI18n } from 'vue-i18n';
import { onMounted, onUnmounted } from 'vue';
import { showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
const { t } = useI18n();
onMounted(async () => {
const premium = await isPremiumUser();
if (!premium) await showBannerAd();
});
onUnmounted(async () => {
await hideBannerAd();
});
</script>
共享实用函数(与框架无关)
这些实用文件包含纯 TypeScript 代码,并调用 Capacitor 插件。所有框架都会使用它们。每个框架都通过自身的模式封装这些文件(Angular 服务、React hooks、Vue composables)。
存储(所有框架)
import { Preferences } from '@capacitor/preferences';
// Set a value
await Preferences.set({ key: 'onboardingCompleted', value: 'true' });
// Get a value
const { value } = await Preferences.get({ key: 'onboardingCompleted' });
console.log(value); // 'true'
// Remove a value
await Preferences.remove({ key: 'onboardingCompleted' });
入门引导状态实用程序
// utils/onboarding.ts
import { Preferences } from '@capacitor/preferences';
const KEY = 'onboardingCompleted';
export async function isOnboardingCompleted(): Promise<boolean> {
const { value } = await Preferences.get({ key: KEY });
return value === 'true';
}
export async function setOnboardingCompleted(completed: boolean): Promise<void> {
await Preferences.set({ key: KEY, value: String(completed) });
}
export async function resetOnboarding(): Promise<void> {
await Preferences.remove({ key: KEY });
}
主题实用程序
// utils/theme.ts
import { Preferences } from '@capacitor/preferences';
export type ThemeMode = 'light' | 'dark' | 'system';
const KEY = 'themeMode';
export async function getTheme(): Promise<ThemeMode> {
const { value } = await Preferences.get({ key: KEY });
return (value as ThemeMode) || 'system';
}
export async function setTheme(mode: ThemeMode): Promise<void> {
await Preferences.set({ key: KEY, value: mode });
applyTheme(mode);
}
export function applyTheme(mode: ThemeMode): void {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = mode === 'dark' || (mode === 'system' && prefersDark);
document.documentElement.classList.toggle('ion-palette-dark', isDark);
}
AdMob 实用程序
// utils/admob.ts
import { AdMob, BannerAdOptions, BannerAdSize, BannerAdPosition } from '@capacitor-community/admob';
import { Capacitor } from '@capacitor/core';
let initialized = false;
export async function initializeAdMob(): Promise<void> {
if (!Capacitor.isNativePlatform()) return;
await AdMob.initialize({
initializeForTesting: true, // Set to false in production
});
initialized = true;
}
export async function showBannerAd(): Promise<void> {
if (!initialized) return;
const options: BannerAdOptions = {
adId: 'ca-app-pub-3940256099942544/6300978111', // Test ID - replace in production
adSize: BannerAdSize.ADAPTIVE_BANNER,
position: BannerAdPosition.BOTTOM_CENTER,
isTesting: true, // Set to false in production
};
await AdMob.showBanner(options);
}
export async function hideBannerAd(): Promise<void> {
if (!initialized) return;
await AdMob.hideBanner();
}
开发/测试时,请使用测试广告 ID:
- 横幅:
ca-app-pub-3940256099942544/6300978111
切勿跳过 AdMob 初始化,否则插件将无法正常工作。
RevenueCat 实用程序
// utils/purchases.ts
import { Capacitor } from '@capacitor/core';
import { Purchases, LOG_LEVEL, PURCHASES_ERROR_CODE, PurchasesPackage } from '@revenuecat/purchases-capacitor';
let purchasesInitialized = false;
export async function initializePurchases(): Promise<void> {
if (!Capacitor.isNativePlatform()) return;
await Purchases.setLogLevel({ level: LOG_LEVEL.DEBUG }); // Use WARN in production
const apiKey = Capacitor.getPlatform() === 'ios'
? 'appl_YOUR_IOS_API_KEY'
: 'goog_YOUR_ANDROID_API_KEY';
await Purchases.configure({ apiKey });
purchasesInitialized = true;
}
export async function isPremiumUser(): Promise<boolean> {
if (!purchasesInitialized) return false;
try {
const { customerInfo } = await Purchases.getCustomerInfo();
return Object.keys(customerInfo.entitlements.active).length > 0;
} catch {
return false;
}
}
export async function getOfferings(): Promise<PurchasesPackage[]> {
if (!purchasesInitialized) return [];
try {
const { offerings } = await Purchases.getOfferings();
return offerings?.current?.availablePackages ?? [];
} catch {
return [];
}
}
export async function purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
try {
const { customerInfo } = await Purchases.purchasePackage({ aPackage: pkg });
return Object.keys(customerInfo.entitlements.active).length > 0;
} catch (error: unknown) {
if ((error as { code?: string })?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
return false;
}
throw error;
}
}
export async function restorePurchases(): Promise<boolean> {
try {
const { customerInfo } = await Purchases.restorePurchases();
return Object.keys(customerInfo.entitlements.active).length > 0;
} catch {
return false;
}
}
通知实用程序
// utils/notifications.ts
import { Capacitor } from '@capacitor/core';
import { PushNotifications } from '@capacitor/push-notifications';
export async function requestNotificationPermission(): Promise<boolean> {
if (!Capacitor.isNativePlatform()) return false;
const result = await PushNotifications.requestPermissions();
if (result.receive === 'granted') {
await PushNotifications.register();
return true;
}
return false;
}
export async function addNotificationListeners(): Promise<void> {
await PushNotifications.addListener('registration', (token) => {
console.log('Push registration success, token:', token.value);
});
await PushNotifications.addListener('registrationError', (error) => {
console.error('Push registration error:', error);
});
await PushNotifications.addListener('pushNotificationReceived', (notification) => {
console.log('Push notification received:', notification);
});
await PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
console.log('Push notification action:', action);
});
}
框架专用服务封装
Angular 服务
// services/onboarding.service.ts
import { Injectable } from '@angular/core';
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';
@Injectable({ providedIn: 'root' })
export class OnboardingService {
isCompleted = isOnboardingCompleted;
setCompleted = setOnboardingCompleted;
reset = resetOnboarding;
}
// services/theme.service.ts
import { Injectable } from '@angular/core';
import { getTheme, setTheme, applyTheme, ThemeMode } from '../utils/theme';
@Injectable({ providedIn: 'root' })
export class ThemeService {
async initialize(): Promise<void> {
const theme = await getTheme();
applyTheme(theme);
}
getTheme = getTheme;
setTheme = setTheme;
}
// services/ads.service.ts
import { Injectable } from '@angular/core';
import { initializeAdMob, showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
@Injectable({ providedIn: 'root' })
export class AdsService {
async initialize(): Promise<void> {
await initializeAdMob();
}
async showBanner(): Promise<void> {
if (await isPremiumUser()) return;
await showBannerAd();
}
async hideBanner(): Promise<void> {
await hideBannerAd();
}
}
// services/purchases.service.ts
import { Injectable } from '@angular/core';
import {
initializePurchases,
isPremiumUser,
getOfferings,
purchasePackage,
restorePurchases,
} from '../utils/purchases';
@Injectable({ providedIn: 'root' })
export class PurchasesService {
initialize = initializePurchases;
isPremium = isPremiumUser;
getOfferings = getOfferings;
purchase = purchasePackage;
restorePurchases = restorePurchases;
}
// services/notifications.service.ts
import { Injectable } from '@angular/core';
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';
@Injectable({ providedIn: 'root' })
export class NotificationsService {
requestPermission = requestNotificationPermission;
addListeners = addNotificationListeners;
}
React Hooks
// hooks/useOnboarding.ts
import { useCallback } from 'react';
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';
export function useOnboarding() {
const isCompleted = useCallback(() => isOnboardingCompleted(), []);
const setCompleted = useCallback((v: boolean) => setOnboardingCompleted(v), []);
const reset = useCallback(() => resetOnboarding(), []);
return { isCompleted, setCompleted, reset };
}
// hooks/useTheme.ts
import { useCallback } from 'react';
import { getTheme, setTheme, applyTheme, ThemeMode } from '../utils/theme';
export function useTheme() {
const initialize = useCallback(async () => {
const theme = await getTheme();
applyTheme(theme);
}, []);
return {
initialize,
getTheme: useCallback(() => getTheme(), []),
setTheme: useCallback((mode: ThemeMode) => setTheme(mode), []),
};
}
// hooks/useAds.ts
import { useCallback } from 'react';
import { initializeAdMob, showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
export function useAds() {
const initialize = useCallback(() => initializeAdMob(), []);
const showBanner = useCallback(async () => {
if (await isPremiumUser()) return;
await showBannerAd();
}, []);
const hideBanner = useCallback(() => hideBannerAd(), []);
return { initialize, showBanner, hideBanner };
}
// hooks/usePurchases.ts
import { useCallback } from 'react';
import {
initializePurchases,
isPremiumUser,
getOfferings,
purchasePackage,
restorePurchases,
} from '../utils/purchases';
import { PurchasesPackage } from '@revenuecat/purchases-capacitor';
export function usePurchases() {
return {
initialize: useCallback(() => initializePurchases(), []),
isPremium: useCallback(() => isPremiumUser(), []),
getOfferings: useCallback(() => getOfferings(), []),
purchase: useCallback((pkg: PurchasesPackage) => purchasePackage(pkg), []),
restorePurchases: useCallback(() => restorePurchases(), []),
};
}
// hooks/useNotifications.ts
import { useCallback } from 'react';
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';
export function useNotifications() {
return {
requestPermission: useCallback(() => requestNotificationPermission(), []),
addListeners: useCallback(() => addNotificationListeners(), []),
};
}
Vue Composables
// composables/useOnboarding.ts
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';
export function useOnboarding() {
return {
isCompleted: isOnboardingCompleted,
setCompleted: setOnboardingCompleted,
reset: resetOnboarding,
};
}
// composables/useTheme.ts
import { getTheme, setTheme, applyTheme, ThemeMode } from '../utils/theme';
export function useTheme() {
const initialize = async () => {
const theme = await getTheme();
applyTheme(theme);
};
return { initialize, getTheme, setTheme };
}
// composables/useAds.ts
import { initializeAdMob, showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';
export function useAds() {
const initialize = () => initializeAdMob();
const showBanner = async () => {
if (await isPremiumUser()) return;
await showBannerAd();
};
const hideBanner = () => hideBannerAd();
return { initialize, showBanner, hideBanner };
}
// composables/usePurchases.ts
import {
initializePurchases,
isPremiumUser,
getOfferings,
purchasePackage,
restorePurchases,
} from '../utils/purchases';
export function usePurchases() {
return {
initialize: initializePurchases,
isPremium: isPremiumUser,
getOfferings,
purchase: purchasePackage,
restorePurchases,
};
}
// composables/useNotifications.ts
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';
export function useNotifications() {
return {
requestPermission: requestNotificationPermission,
addListeners: addNotificationListeners,
};
}
入门引导守卫
入门引导守卫(Angular)
// guards/onboarding.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { isOnboardingCompleted } from '../utils/onboarding';
export const onboardingGuard: CanActivateFn = async () => {
const router = inject(Router);
const completed = await isOnboardingCompleted();
if (!completed) {
router.navigateByUrl('/onboarding', { replaceUrl: true });
return false;
}
return true;
};
入门引导守卫(React)
// components/OnboardingGuard.tsx
import { useEffect, useState } from 'react';
import { useIonRouter } from '@ionic/react';
import { isOnboardingCompleted } from '../utils/onboarding';
export const OnboardingGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const router = useIonRouter();
const [checked, setChecked] = useState(false);
useEffect(() => {
isOnboardingCompleted().then((completed) => {
if (!completed) {
router.push('/onboarding', 'forward', 'replace');
} else {
setChecked(true);
}
});
}, []);
return checked ? <>{children}</> : null;
};
入门引导守卫(Vue)
Vue 入门引导守卫通过 router.beforeEach 钩子实现。请参见上文“路由(Vue)”部分。
入门引导页面
共享视频 CSS(所有框架)
.background-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
}
.gradient-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.7));
z-index: 1;
}
.onboarding-slides {
position: relative;
z-index: 2;
height: 100%;
}
切勿只引用视频而不实际渲染 <video> 元素。请使用原生 HTML5 <video>,不要使用 canvas、动画 GIF 或外部播放器。
入门引导页面(Angular)
<ion-content [fullscreen]="true" class="onboarding-content">
<video
#bgVideo
[src]="videoUrl"
autoplay
loop
muted
playsinline
class="background-video"
></video>
<div class="gradient-overlay"></div>
<div class="onboarding-slides">
</div>
</ion-content>
// onboarding/onboarding.page.scss
.background-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
}
.gradient-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7));
z-index: 1;
}
.onboarding-slides {
position: relative;
z-index: 2;
height: 100%;
}
// onboarding/onboarding.page.ts
import { Component } from '@angular/core';
import { IonContent, IonButton, IonIcon } from '@ionic/angular/standalone';
import { Router } from '@angular/router';
import { OnboardingService } from '../services/onboarding.service';
const VIDEO_URL =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
@Component({
selector: 'app-onboarding',
standalone: true,
imports: [IonContent, IonButton, IonIcon],
templateUrl: './onboarding.page.html',
styleUrls: ['./onboarding.page.scss'],
})
export class OnboardingPage {
videoUrl = VIDEO_URL;
constructor(
private router: Router,
private onboardingService: OnboardingService
) {}
async completeOnboarding() {
await this.onboardingService.setCompleted(true);
this.router.navigateByUrl('/paywall', { replaceUrl: true });
}
}
入门引导页面(React)
// pages/OnboardingPage.tsx
import { IonContent, IonButton, IonIcon, useIonRouter } from '@ionic/react';
import { useOnboarding } from '../hooks/useOnboarding';
import './OnboardingPage.css';
const VIDEO_URL =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
const OnboardingPage: React.FC = () => {
const router = useIonRouter();
const { setCompleted } = useOnboarding();
const completeOnboarding = async () => {
await setCompleted(true);
router.push('/paywall', 'forward', 'replace');
};
return (
<IonContent fullscreen className="onboarding-content">
<video
src={VIDEO_URL}
autoPlay
loop
muted
playsInline
className="background-video"
/>
<div className="gradient-overlay" />
<div className="onboarding-slides">
{/* Swiper slides content here */}
</div>
</IonContent>
);
};
export default OnboardingPage;
入门引导页面(Vue)
<template>
<ion-content :fullscreen="true" class="onboarding-content">
<video
:src="videoUrl"
autoplay
loop
muted
playsinline
class="background-video"
/>
<div class="gradient-overlay" />
<div class="onboarding-slides">
</div>
</ion-content>
</template>
<script setup lang="ts">
import { IonContent, IonButton, IonIcon } from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useOnboarding } from '../composables/useOnboarding';
const VIDEO_URL =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
const videoUrl = VIDEO_URL;
const router = useRouter();
const { setCompleted } = useOnboarding();
async function completeOnboarding() {
await setCompleted(true);
router.replace('/paywall');
}
</script>
<style scoped>
.background-video {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover;
z-index: 0;
}
.gradient-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.7));
z-index: 1;
}
.onboarding-slides {
position: relative;
z-index: 2;
height: 100%;
}
</style>
付费墙页面
重要:引导流程完成后,必须立即显示付费墙。
付费墙必须有两个订阅选项:
- 每周 - 默认选项
- 每年 - 带有“50% OFF”徽章(推荐,应突出显示)
三个按钮:订阅、继续使用广告、恢复购买。
流程:Onboarding -> Paywall -> Main App (tabs)
付费墙页面(Angular)
<ion-content [fullscreen]="true">
<div class="paywall-container">
<h1>{{ 'paywall.title' | translate }}</h1>
<div class="subscription-options">
<div
*ngFor="let option of subscriptionOptions"
class="option-card"
[class.selected]="selectedPlan === option.id"
(click)="selectedPlan = option.id"
>
<ion-badge *ngIf="option.badge" color="danger">{{ option.badge }}</ion-badge>
<h3>{{ option.title | translate }}</h3>
<p>{{ option.price }}</p>
</div>
</div>
<ion-button expand="block" (click)="subscribe()">
{{ 'paywall.subscribe' | translate }}
</ion-button>
<ion-button fill="clear" (click)="skip()">
{{ 'paywall.skip' | translate }}
</ion-button>
<ion-button fill="clear" size="small" (click)="restore()">
{{ 'paywall.restore' | translate }}
</ion-button>
</div>
</ion-content>
// paywall/paywall.page.scss
.paywall-container {
// Add your paywall styles here
}
.subscription-options {
// Add your subscription options styles here
}
.option-card {
// Add your option card styles here
&.selected {
// Selected state styles
}
}
// paywall/paywall.page.ts
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import {
IonContent, IonButton, IonIcon, IonBadge,
} from '@ionic/angular/standalone';
import { NgFor, NgIf, NgClass } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { PurchasesService } from '../services/purchases.service';
@Component({
selector: 'app-paywall',
standalone: true,
imports: [
IonContent, IonButton, IonIcon, IonBadge,
NgFor, NgIf, NgClass, TranslateModule,
],
templateUrl: './paywall.page.html',
styleUrls: ['./paywall.page.scss'],
})
export class PaywallPage {
selectedPlan = 'weekly';
subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
constructor(
private router: Router,
private purchasesService: PurchasesService,
) {}
async subscribe() {
// Use RevenueCat to process purchase
this.router.navigateByUrl('/tabs', { replaceUrl: true });
}
skip() {
this.router.navigateByUrl('/tabs', { replaceUrl: true });
}
async restore() {
const restored = await this.purchasesService.restorePurchases();
if (restored) {
this.router.navigateByUrl('/tabs', { replaceUrl: true });
}
}
}
付费墙页面(React)
// pages/PaywallPage.tsx
import { useState } from 'react';
import {
IonContent, IonButton, IonBadge, useIonRouter,
} from '@ionic/react';
import { useTranslation } from 'react-i18next';
import { usePurchases } from '../hooks/usePurchases';
const subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
const PaywallPage: React.FC = () => {
const { t } = useTranslation();
const router = useIonRouter();
const { restorePurchases } = usePurchases();
const [selectedPlan, setSelectedPlan] = useState('weekly');
const subscribe = async () => {
// Use RevenueCat to process purchase
router.push('/tabs', 'forward', 'replace');
};
const skip = () => {
router.push('/tabs', 'forward', 'replace');
};
const restore = async () => {
const restored = await restorePurchases();
if (restored) {
router.push('/tabs', 'forward', 'replace');
}
};
return (
<IonContent fullscreen>
<div className="paywall-container">
<h1>{t('paywall.title')}</h1>
<div className="subscription-options">
{subscriptionOptions.map((option) => (
<div
key={option.id}
className={`option-card ${selectedPlan === option.id ? 'selected' : ''}`}
onClick={() => setSelectedPlan(option.id)}
>
{option.badge && <IonBadge color="danger">{option.badge}</IonBadge>}
<h3>{t(option.title)}</h3>
<p>{option.price}</p>
</div>
))}
</div>
<IonButton expand="block" onClick={subscribe}>
{t('paywall.subscribe')}
</IonButton>
<IonButton fill="clear" onClick={skip}>
{t('paywall.skip')}
</IonButton>
<IonButton fill="clear" size="small" onClick={restore}>
{t('paywall.restore')}
</IonButton>
</div>
</IonContent>
);
};
export default PaywallPage;
付费墙页面(Vue)
<template>
<ion-content :fullscreen="true">
<div class="paywall-container">
<h1>{{ t('paywall.title') }}</h1>
<div class="subscription-options">
<div
v-for="option in subscriptionOptions"
:key="option.id"
class="option-card"
:class="{ selected: selectedPlan === option.id }"
@click="selectedPlan = option.id"
>
<ion-badge v-if="option.badge" color="danger">{{ option.badge }}</ion-badge>
<h3>{{ t(option.title) }}</h3>
<p>{{ option.price }}</p>
</div>
</div>
<ion-button expand="block" @click="subscribe">
{{ t('paywall.subscribe') }}
</ion-button>
<ion-button fill="clear" @click="skip">
{{ t('paywall.skip') }}
</ion-button>
<ion-button fill="clear" size="small" @click="restore">
{{ t('paywall.restore') }}
</ion-button>
</div>
</ion-content>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { IonContent, IonButton, IonBadge } from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { usePurchases } from '../composables/usePurchases';
const { t } = useI18n();
const router = useRouter();
const { restorePurchases } = usePurchases();
const selectedPlan = ref('weekly');
const subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
async function subscribe() {
// Use RevenueCat to process purchase
router.replace('/tabs');
}
function skip() {
router.replace('/tabs');
}
async function restore() {
const restored = await restorePurchases();
if (restored) {
router.replace('/tabs');
}
}
</script>
标签页导航
常用 Ionicons
| 用途 | Ionicon 名称 |
|---|---|
| 首页 | home |
| 探索 | compass |
| 设置 | settings |
| 个人资料 | person |
| 搜索 | search |
| 收藏 | heart |
| 通知 | notifications |
标签页导航(Angular)
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="home">
<ion-icon name="home"></ion-icon>
<ion-label>{{ 'tabs.home' | translate }}</ion-label>
</ion-tab-button>
<ion-tab-button tab="explore">
<ion-icon name="compass"></ion-icon>
<ion-label>{{ 'tabs.explore' | translate }}</ion-label>
</ion-tab-button>
<ion-tab-button tab="settings">
<ion-icon name="settings"></ion-icon>
<ion-label>{{ 'tabs.settings' | translate }}</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
// tabs/tabs.page.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { home, compass, settings } from 'ionicons/icons';
import { TranslateModule } from '@ngx-translate/core';
import { AdsService } from '../services/ads.service';
@Component({
selector: 'app-tabs',
standalone: true,
imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel, TranslateModule],
templateUrl: './tabs.page.html',
styleUrls: ['./tabs.page.scss'],
})
export class TabsPage implements OnInit, OnDestroy {
constructor(private adsService: AdsService) {
addIcons({ home, compass, settings });
}
async ngOnInit() {
await this.adsService.showBanner();
}
async ngOnDestroy() {
await this.adsService.hideBanner();
}
}
标签页导航(React)
请参阅上文 Routing(React)部分中的 TabsLayout 组件。
标签页导航(Vue)
请参阅上文 Routing(Vue)部分中的 TabsLayout.vue 组件。
设置页面
设置页面必须包括:
- 语言 - 更改应用语言
- 主题 - 浅色/深色/系统
- 通知 - 启用/禁用通知
- 移除广告 - 导航至付费墙(如果已是高级用户,则隐藏)
- 重置引导流程 - 重新启动引导流程(用于测试/演示)
设置页面(Angular)
<ion-header>
<ion-toolbar>
<ion-title>{{ 'settings.title' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-icon name="language" slot="start"></ion-icon>
<ion-label>{{ 'settings.language' | translate }}</ion-label>
<ion-select [value]="currentLang" (ionChange)="changeLanguage($event)">
<ion-select-option value="en">English</ion-select-option>
<ion-select-option value="tr">Türkçe</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="color-palette" slot="start"></ion-icon>
<ion-label>{{ 'settings.theme' | translate }}</ion-label>
<ion-select [value]="currentTheme" (ionChange)="changeTheme($event)">
<ion-select-option value="system">{{ 'settings.system' | translate }}</ion-select-option>
<ion-select-option value="light">{{ 'settings.light' | translate }}</ion-select-option>
<ion-select-option value="dark">{{ 'settings.dark' | translate }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="notifications" slot="start"></ion-icon>
<ion-label>{{ 'settings.notifications' | translate }}</ion-label>
<ion-toggle [checked]="notificationsEnabled" (ionChange)="toggleNotifications($event)"></ion-toggle>
</ion-item>
<ion-item *ngIf="!isPremium" button (click)="removeAds()">
<ion-icon name="star" slot="start"></ion-icon>
<ion-label>{{ 'settings.removeAds' | translate }}</ion-label>
</ion-item>
<ion-item button (click)="resetOnboarding()">
<ion-icon name="refresh" slot="start"></ion-icon>
<ion-label>{{ 'settings.resetOnboarding' | translate }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
// settings/settings.page.scss
// Add your settings page styles here
// settings/settings.page.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import {
IonContent, IonHeader, IonTitle, IonToolbar,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
} from '@ionic/angular/standalone';
import { NgIf } from '@angular/common';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Preferences } from '@capacitor/preferences';
import { ThemeService } from '../services/theme.service';
import { OnboardingService } from '../services/onboarding.service';
import { PurchasesService } from '../services/purchases.service';
@Component({
selector: 'app-settings',
standalone: true,
imports: [
IonContent, IonHeader, IonTitle, IonToolbar,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
NgIf, TranslateModule,
],
templateUrl: './settings.page.html',
styleUrls: ['./settings.page.scss'],
})
export class SettingsPage implements OnInit {
currentLang = 'en';
currentTheme = 'system';
notificationsEnabled = false;
isPremium = false;
constructor(
private router: Router,
private themeService: ThemeService,
private onboardingService: OnboardingService,
private purchasesService: PurchasesService,
private translate: TranslateService,
) {}
async ngOnInit() {
this.currentLang = this.translate.currentLang || 'en';
this.currentTheme = await this.themeService.getTheme();
this.isPremium = await this.purchasesService.isPremium();
}
changeLanguage(event: CustomEvent) {
const lang = event.detail.value;
this.translate.use(lang);
Preferences.set({ key: 'language', value: lang });
}
changeTheme(event: CustomEvent) {
this.themeService.setTheme(event.detail.value);
}
toggleNotifications(event: CustomEvent) {
this.notificationsEnabled = event.detail.checked;
}
removeAds() {
this.router.navigateByUrl('/paywall');
}
async resetOnboarding() {
await this.onboardingService.reset();
this.router.navigateByUrl('/onboarding', { replaceUrl: true });
}
}
设置页面(React)
// pages/SettingsPage.tsx
import { useState, useEffect } from 'react';
import {
IonContent, IonHeader, IonTitle, IonToolbar, IonPage,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
useIonRouter,
} from '@ionic/react';
import { language, colorPalette, notifications, star, refresh } from 'ionicons/icons';
import { useTranslation } from 'react-i18next';
import { Preferences } from '@capacitor/preferences';
import { useTheme } from '../hooks/useTheme';
import { useOnboarding } from '../hooks/useOnboarding';
import { usePurchases } from '../hooks/usePurchases';
import { ThemeMode } from '../utils/theme';
const SettingsPage: React.FC = () => {
const { t, i18n } = useTranslation();
const router = useIonRouter();
const { getTheme, setTheme } = useTheme();
const { reset } = useOnboarding();
const { isPremium } = usePurchases();
const [currentLang, setCurrentLang] = useState('en');
const [currentTheme, setCurrentTheme] = useState<ThemeMode>('system');
const [notificationsEnabled, setNotificationsEnabled] = useState(false);
const [premium, setPremium] = useState(false);
useEffect(() => {
setCurrentLang(i18n.language || 'en');
getTheme().then(setCurrentTheme);
isPremium().then(setPremium);
}, []);
const changeLanguage = (lang: string) => {
setCurrentLang(lang);
i18n.changeLanguage(lang);
Preferences.set({ key: 'language', value: lang });
};
const changeTheme = (mode: ThemeMode) => {
setCurrentTheme(mode);
setTheme(mode);
};
const resetOnboarding = async () => {
await reset();
router.push('/onboarding', 'forward', 'replace');
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>{t('settings.title')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
<IonItem>
<IonIcon icon={language} slot="start" />
<IonLabel>{t('settings.language')}</IonLabel>
<IonSelect value={currentLang} onIonChange={(e) => changeLanguage(e.detail.value)}>
<IonSelectOption value="en">English</IonSelectOption>
<IonSelectOption value="tr">Türkçe</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonIcon icon={colorPalette} slot="start" />
<IonLabel>{t('settings.theme')}</IonLabel>
<IonSelect value={currentTheme} onIonChange={(e) => changeTheme(e.detail.value)}>
<IonSelectOption value="system">{t('settings.system')}</IonSelectOption>
<IonSelectOption value="light">{t('settings.light')}</IonSelectOption>
<IonSelectOption value="dark">{t('settings.dark')}</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonIcon icon={notifications} slot="start" />
<IonLabel>{t('settings.notifications')}</IonLabel>
<IonToggle
checked={notificationsEnabled}
onIonChange={(e) => setNotificationsEnabled(e.detail.checked)}
/>
</IonItem>
{!premium && (
<IonItem button onClick={() => router.push('/paywall')}>
<IonIcon icon={star} slot="start" />
<IonLabel>{t('settings.removeAds')}</IonLabel>
</IonItem>
)}
<IonItem button onClick={resetOnboarding}>
<IonIcon icon={refresh} slot="start" />
<IonLabel>{t('settings.resetOnboarding')}</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
export default SettingsPage;
设置页面(Vue)
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ t('settings.title') }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-icon name="language" slot="start" />
<ion-label>{{ t('settings.language') }}</ion-label>
<ion-select :value="currentLang" @ion-change="changeLanguage($event)">
<ion-select-option value="en">English</ion-select-option>
<ion-select-option value="tr">Türkçe</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="color-palette" slot="start" />
<ion-label>{{ t('settings.theme') }}</ion-label>
<ion-select :value="currentTheme" @ion-change="changeTheme($event)">
<ion-select-option value="system">{{ t('settings.system') }}</ion-select-option>
<ion-select-option value="light">{{ t('settings.light') }}</ion-select-option>
<ion-select-option value="dark">{{ t('settings.dark') }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="notifications" slot="start" />
<ion-label>{{ t('settings.notifications') }}</ion-label>
<ion-toggle :checked="notificationsEnabled" @ion-change="toggleNotifications($event)" />
</ion-item>
<ion-item v-if="!premium" button @click="removeAds">
<ion-icon name="star" slot="start" />
<ion-label>{{ t('settings.removeAds') }}</ion-label>
</ion-item>
<ion-item button @click="resetOnboardingFlow">
<ion-icon name="refresh" slot="start" />
<ion-label>{{ t('settings.resetOnboarding') }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import {
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
} from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Preferences } from '@capacitor/preferences';
import { useTheme } from '../composables/useTheme';
import { useOnboarding } from '../composables/useOnboarding';
import { usePurchases } from '../composables/usePurchases';
import type { ThemeMode } from '../utils/theme';
const { t, locale } = useI18n();
const router = useRouter();
const { getTheme, setTheme } = useTheme();
const { reset } = useOnboarding();
const { isPremium } = usePurchases();
const currentLang = ref('en');
const currentTheme = ref<ThemeMode>('system');
const notificationsEnabled = ref(false);
const premium = ref(false);
onMounted(async () => {
currentLang.value = locale.value || 'en';
currentTheme.value = await getTheme();
premium.value = await isPremium();
});
function changeLanguage(event: CustomEvent) {
const lang = event.detail.value;
currentLang.value = lang;
locale.value = lang;
Preferences.set({ key: 'language', value: lang });
}
function changeTheme(event: CustomEvent) {
const mode = event.detail.value as ThemeMode;
currentTheme.value = mode;
setTheme(mode);
}
function toggleNotifications(event: CustomEvent) {
notificationsEnabled.value = event.detail.checked;
}
function removeAds() {
router.push('/paywall');
}
async function resetOnboardingFlow() {
await reset();
router.replace('/onboarding');
}
</script>
本地化
翻译文件(共享 - 所有框架)
// en.json
{
"tabs": {
"home": "Home",
"explore": "Explore",
"settings": "Settings"
},
"onboarding": {
"next": "Next",
"start": "Get Started",
"skip": "Skip"
},
"paywall": {
"title": "Go Premium",
"weekly": "Weekly",
"yearly": "Yearly",
"subscribe": "Subscribe",
"skip": "Continue with ads",
"restore": "Restore Purchases"
},
"settings": {
"title": "Settings",
"language": "Language",
"theme": "Theme",
"system": "System",
"light": "Light",
"dark": "Dark",
"notifications": "Notifications",
"removeAds": "Remove Ads",
"resetOnboarding": "Reset Onboarding"
}
}
// tr.json
{
"tabs": {
"home": "Ana Sayfa",
"explore": "Keşfet",
"settings": "Ayarlar"
},
"onboarding": {
"next": "İleri",
"start": "Başla",
"skip": "Atla"
},
"paywall": {
"title": "Premium'a Geç",
"weekly": "Haftalık",
"yearly": "Yıllık",
"subscribe": "Abone Ol",
"skip": "Reklamlı devam et",
"restore": "Satın Alımları Geri Yükle"
},
"settings": {
"title": "Ayarlar",
"language": "Dil",
"theme": "Tema",
"system": "Sistem",
"light": "Açık",
"dark": "Koyu",
"notifications": "Bildirimler",
"removeAds": "Reklamları Kaldır",
"resetOnboarding": "Tanıtımı Sıfırla"
}
}
土耳其语本地化(重要)
编写 tr.json 时,必须使用正确的土耳其语字符:
- ı(小写无点 i)- 不能写作 i
- İ(大写带点 I)- 不能写作 I
- ü、Ü、ö、Ö、ç、Ç、ş、Ş、ğ、Ğ
示例:
- ✅ “Ayarlar”、“Giriş”、“Çıkış”、“Başla”、“İleri”、“Güncelle”
- ❌ “Ayarlar”、“Giris”、“Cikis”、“Basla”、“Ileri”、“Guncelle”
i18n 设置(Angular)
i18n 使用 @ngx-translate/core 配置在 app.config.ts 中。请参阅应用配置(Angular)部分。
在 .html 模板文件中的用法:
{{ 'settings.title' | translate }}
在 app.component.ts 中检测语言:
import { TranslateService } from '@ngx-translate/core';
constructor(private translate: TranslateService) {
const browserLang = navigator.language.split('-')[0];
translate.setDefaultLang('en');
translate.use(['en', 'tr'].includes(browserLang) ? browserLang : 'en');
}
i18n 设置(React)
i18n 配置在 i18n/index.ts 中。请参阅应用配置(React)部分。
在组件中的用法:
import { useTranslation } from 'react-i18next';
const MyComponent: React.FC = () => {
const { t } = useTranslation();
return <h1>{t('settings.title')}</h1>;
};
i18n 设置(Vue)
i18n 使用 vue-i18n 配置在 main.ts 中。请参阅应用配置(Vue)部分。
在模板中的用法:
<template>
<h1>{{ t('settings.title') }}</h1>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
框架最佳实践
Angular 最佳实践
始终使用独立组件,并将 HTML、TS 和 SCSS 文件分开:
❌ 错误(NgModules + IonicModule):
// home.module.ts
@NgModule({
imports: [CommonModule, IonicModule],
declarations: [HomePage],
})
export class HomePageModule {}
❌ 错误(内联模板):
@Component({
selector: 'app-home',
standalone: true,
imports: [IonContent, IonHeader, IonTitle, IonToolbar, TranslateModule],
template: `
<ion-header>
<ion-toolbar>
<ion-title>{{ 'home.title' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>
`,
})
export class HomePage {}
✅ 正确(分开的 .html、.ts、.scss 文件):
<ion-header>
<ion-toolbar>
<ion-title>{{ 'home.title' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>
// home/home.page.scss
// Page-specific styles here
// home/home.page.ts
@Component({
selector: 'app-home',
standalone: true,
imports: [IonContent, IonHeader, IonTitle, IonToolbar, TranslateModule],
templateUrl: './home.page.html',
styleUrls: ['./home.page.scss'],
})
export class HomePage {}
单独导入 Ionic 组件(例如 IonContent、IonButton)——独立组件中绝不能导入 IonicModule。
始终使用 templateUrl + styleUrls——绝不能使用内联 template 或 styles。
React 最佳实践
始终使用带有 hooks 的函数组件:
❌ 错误:
class HomePage extends React.Component {
render() {
return <IonContent>...</IonContent>;
}
}
✅ 正确:
import { IonContent, IonHeader, IonTitle, IonToolbar, IonPage } from '@ionic/react';
import { useTranslation } from 'react-i18next';
const HomePage: React.FC = () => {
const { t } = useTranslation();
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>{t('home.title')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
{/* content */}
</IonContent>
</IonPage>
);
};
export default HomePage;
始终将页面内容包裹在 <IonPage> 中,以确保 Ionic 页面过渡和生命周期正常运行。
Vue 最佳实践
始终使用带有 <script setup> 的 Composition API:
❌ 错误:
<script>
export default {
data() {
return { title: 'Home' };
},
};
</script>
✅ 正确:
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ t('home.title') }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
始终将页面内容包裹在 <ion-page> 中,以确保 Ionic 页面过渡和生命周期正常运行。
创建后清理(始终执行)
创建新的 Ionic 项目后,必须:
- 移除与项目结构冲突的默认生成页面
- 确保构建配置包含正确的输出目录
- 确认
capacitor.config.ts中的appId和appName正确 - 检查所有 Ionic 导入是否使用了正确的框架专用包
完成代码后(始终运行)
Angular
npm install
npx ng build
npx cap sync
React
npm install
npm run build
npx cap sync
Vue
npm install
npm run build
npx cap sync
npm install安装所有新增依赖- Build 编译项目
cap sync将 Web 资源和插件同步到原生项目
请勿跳过这些步骤。
开发命令
Angular
npm install
ionic serve # Run in browser
ionic build # Build for production
npx cap sync # Sync web assets to native
npx cap open ios # Open in Xcode
npx cap open android # Open in Android Studio
npx cap run ios # Build and run on iOS device/simulator
npx cap run android # Build and run on Android device/emulator
React
npm install
ionic serve # Run in browser
ionic build # Build for production
npx cap sync # Sync web assets to native
npx cap open ios # Open in Xcode
npx cap open android # Open in Android Studio
npx cap run ios # Build and run on iOS
npx cap run android # Build and run on Android
Vue
npm install
ionic serve # Run in browser
ionic build # Build for production
npx cap sync # Sync web assets to native
npx cap open ios # Open in Xcode
npx cap open android # Open in Android Studio
npx cap run ios # Build and run on iOS
npx cap run android # Build and run on Android
编码规范
所有框架
- 严格使用 TypeScript——不得使用
any类型 - 避免硬编码字符串——使用翻译键
- 使用
@capacitor/preferences进行持久化存储 - 所有页面均采用懒加载
- 使用
Capacitor.isNativePlatform()对仅限原生平台的代码进行保护 - 始终对 Capacitor 插件方法使用
await
Angular 专属
- 使用独立组件(页面/组件绝不使用 NgModules)
- 始终使用单独的
.html、.ts、.scss文件——绝不内联template或styles - 在
@Component装饰器中使用templateUrl和styleUrls - 使用 Angular 的
inject()函数或构造函数注入 - 在路由中通过
loadComponent实现懒加载 - 尽可能使用 Angular Signals 管理响应式状态
- 单独导入 Ionic 组件——绝不使用
IonicModule - 使用
ionicons中的addIcons()注册图标
React 专属
- 使用带 hooks 的函数组件
- 使用
<IonPage>包裹页面 - 使用
useIonRouter()进行导航 - 使用
useTranslation()进行国际化 - 直接从
ionicons/icons导入图标
Vue 专属
- 使用 Composition API 和
<script setup lang="ts"> - 使用
<ion-page>包裹页面 - 使用
vue-router中的useRouter()进行导航 - 使用
useI18n()进行国际化 - 从
ionicons/icons导入图标,并通过:icon属性传入
重要说明
- iOS 权限在
Info.plist中定义(通过 Capacitor 插件添加) - Android 权限在
AndroidManifest.xml中定义(通过 Capacitor 插件添加) - 安装新的 Capacitor 插件后,始终运行
npx cap sync - 使用
Capacitor.isNativePlatform()对仅限原生平台的代码进行保护 - 使用
ionic serve在浏览器中进行快速开发测试,然后在设备上测试
App Store 和 Play Store 说明
- 个性化广告需要 iOS ATT 权限
- 必须确保恢复购买功能正常运行
- Target SDK 必须保持最新
- 每次原生构建前都要使用
npx cap sync
测试清单
- 在所有语言下测试 UI
- 深色/浅色模式
- 通知
- 高级版流程
- 恢复购买
- 离线支持
- 多种屏幕尺寸
- 浏览器(ionic serve)和原生平台
开发完成后
npm run build
npx cap sync
npx cap open ios
npx cap open android
NOTE:
cap sync会将构建后的 Web 应用复制到原生项目,并同步插件。在原生平台上测试之前,每次构建后都要运行此命令。