Screenfull Fullscreen API 技能
这份指南指导如何在 ng-events 建筑工地管理系统中使用 screenfull 库实现全屏功能。
何时使用这项技能
触发词: “fullscreen”, “screenfull”, “全屏模式”, “切换全屏”, “退出全屏”
当以下情况时使用这项技能:
- 添加全屏切换按钮
- 创建全屏查看器(图片、视频、仪表板)
- 实施演示模式
- 构建沉浸式 UI 体验
- 为特定组件提供全屏功能
安装 & 设置
# 已在 package.json 中安装
yarn add screenfull@^6.0.2
核心模式
1. 基本全屏切换
import { Component, inject, signal } from '@angular/core';
import screenfull from 'screenfull';
@Component({
selector: 'app-fullscreen-toggle',
standalone: true,
template: `
<button (click)="toggleFullscreen()" class="fullscreen-btn">
@if (isFullscreen()) {
<i nz-icon nzType="fullscreen-exit" nzTheme="outline"></i>
退出全屏
} @else {
<i nz-icon nzType="fullscreen" nzTheme="outline"></i>
进入全屏
}
</button>
`
})
export class FullscreenToggleComponent {
isFullscreen = signal(false);
toggleFullscreen(): void {
if (screenfull.isEnabled) {
screenfull.toggle();
this.updateFullscreenState();
// 监听全屏变化
screenfull.on('change', () => {
this.updateFullscreenState();
});
} else {
console.warn('全屏 API 不受支持');
}
}
private updateFullscreenState(): void {
this.isFullscreen.set(screenfull.isFullscreen);
}
}
2. 特定元素全屏
import { Component, ElementRef, ViewChild, signal } from '@angular/core';
import screenfull from 'screenfull';
@Component({
selector: 'app-dashboard-fullscreen',
standalone: true,
template: `
<div #dashboardContainer class="dashboard-container">
<div class="dashboard-header">
<h2>建设进度仪表板</h2>
<button (click)="toggleFullscreen()" class="btn-fullscreen">
@if (isFullscreen()) {
<i nz-icon nzType="fullscreen-exit"></i>
} @else {
<i nz-icon nzType="fullscreen"></i>
}
</button>
</div>
<div class="dashboard-content">
<app-progress-charts />
<app-task-summary />
<app-recent-activities />
</div>
</div>
`,
styles: [`
.dashboard-container {
position: relative;
background: white;
padding: 20px;
}
.dashboard-container:-webkit-full-screen {
background: #1f1f1f;
color: white;
}
.dashboard-container:fullscreen {
background: #1f1f1f;
color: white;
}
.btn-fullscreen {
position: absolute;
top: 20px;
right: 20px;
}
`]
})
export class DashboardFullscreenComponent {
@ViewChild('dashboardContainer', { static: true }) container!: ElementRef;
isFullscreen = signal(false);
toggleFullscreen(): void {
if (screenfull.isEnabled) {
screenfull.toggle(this.container.nativeElement);
screenfull.on('change', () => {
this.isFullscreen.set(screenfull.isFullscreen);
});
}
}
}
3. 图片画廊全屏
import { Component, signal } from '@angular/core';
import screenfull from 'screenfull';
import { NzImageModule } from 'ng-zorro-antd/image';
@Component({
selector: 'app-construction-photos',
standalone: true,
imports: [NzImageModule],
template: `
<div class="photo-gallery">
<h3>建筑工地照片</h3>
<div class="photo-grid">
@for (photo of photos(); track photo.id) {
<div class="photo-item" (click)="viewFullscreen(photo)">
<img [src]="photo.thumbnailUrl" [alt]="photo.title" />
<div class="photo-overlay">
<i nz-icon nzType="fullscreen" nzTheme="outline"></i>
</div>
</div>
}
</div>
</div>
@if (currentPhoto()) {
<div class="fullscreen-viewer" #viewer>
<button class="close-btn" (click)="exitFullscreen()">
<i nz-icon nzType="close"></i>
</button>
<img [src]="currentPhoto()!.fullUrl" [alt]="currentPhoto()!.title" />
<div class="photo-info">
<h4>{{ currentPhoto()!.title }}</h4>
<p>{{ currentPhoto()!.description }}</p>
<span>{{ currentPhoto()!.date | date }}</span>
</div>
</div>
}
`,
styles: [`
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.photo-item {
position: relative;
cursor: pointer;
overflow: hidden;
border-radius: 4px;
}
.photo-item img {
width: 100%;
height: 200px;
object-fit: cover;
transition: transform 0.3s;
}
.photo-item:hover img {
transform: scale(1.1);
}
.photo-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
}
.photo-item:hover .photo-overlay {
opacity: 1;
}
.fullscreen-viewer {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.fullscreen-viewer img {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
.close-btn {
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 10px;
border-radius: 4px;
}
.photo-info {
position: absolute;
bottom: 20px;
left: 20px;
color: white;
text-align: left;
}
`]
})
export class ConstructionPhotosComponent {
photos = signal([
{
id: '1',
title: '基础工作',
description: '基础浇筑混凝土',
thumbnailUrl: 'assets/photos/thumb1.jpg',
fullUrl: 'assets/photos/full1.jpg',
date: new Date()
}
// ... 更多照片
]);
currentPhoto = signal<Photo | null>(null);
viewFullscreen(photo: Photo): void {
this.currentPhoto.set(photo);
if (screenfull.isEnabled) {
const viewer = document.querySelector('.fullscreen-viewer');
if (viewer) {
screenfull.request(viewer as HTMLElement);
screenfull.on('change', () => {
if (!screenfull.isFullscreen) {
this.currentPhoto.set(null);
}
});
}
}
}
exitFullscreen(): void {
if (screenfull.isEnabled && screenfull.isFullscreen) {
screenfull.exit();
}
this.currentPhoto.set(null);
}
}
4. 演示模式
import { Component, signal, OnInit, OnDestroy } from '@angular/core';
import screenfull from 'screenfull';
@Component({
selector: 'app-presentation-mode',
standalone: true,
template: `
<div class="presentation-container" [class.presentation-active]="isPresenting()">
<div class="presentation-header">
<h2>{{ currentSlide().title }}</h2>
<div class="controls">
<button (click)="previousSlide()" [disabled]="currentSlideIndex() === 0">
<i nz-icon nzType="left"></i> 上一个
</button>
<span>{{ currentSlideIndex() + 1 }} / {{ slides().length }}</span>
<button (click)="nextSlide()" [disabled]="currentSlideIndex() === slides().length - 1">
下一个 <i nz-icon nzType="right"></i>
</button>
<button (click)="togglePresentation()" class="btn-present">
@if (isPresenting()) {
<i nz-icon nzType="fullscreen-exit"></i> 退出
} @else {
<i nz-icon nzType="fullscreen"></i> 演示
}
</button>
</div>
</div>
<div class="slide-content">
<div [innerHTML]="currentSlide().content"></div>
</div>
</div>
`,
styles: [`
.presentation-container {
background: white;
padding: 20px;
}
.presentation-active {
background: #1a1a1a;
color: white;
padding: 40px;
}
.presentation-active .slide-content {
font-size: 1.5em;
}
`]
})
export class PresentationModeComponent implements OnInit, OnDestroy {
slides = signal([
{
title: '项目概览',
content: '<h1>建筑工地进度</h1><p>2025年Q4更新</p>'
},
{
title: '达成的里程碑',
content: '<ul><li>基础完成</li><li>结构工作75%</li></ul>'
}
// ... 更多幻灯片
]);
currentSlideIndex = signal(0);
isPresenting = signal(false);
currentSlide = computed(() => this.slides()[this.currentSlideIndex()]);
ngOnInit(): void {
// 监听键盘快捷方式
document.addEventListener('keydown', this.handleKeyPress.bind(this));
}
ngOnDestroy(): void {
document.removeEventListener('keydown', this.handleKeyPress.bind(this));
if (screenfull.isEnabled) {
screenfull.off('change');
}
}
togglePresentation(): void {
if (screenfull.isEnabled) {
screenfull.toggle();
screenfull.on('change', () => {
this.isPresenting.set(screenfull.isFullscreen);
});
}
}
nextSlide(): void {
if (this.currentSlideIndex() < this.slides().length - 1) {
this.currentSlideIndex.update(i => i + 1);
}
}
previousSlide(): void {
if (this.currentSlideIndex() > 0) {
this.currentSlideIndex.update(i => i - 1);
}
}
private handleKeyPress(event: KeyboardEvent): void {
if (!this.isPresenting()) return;
switch (event.key) {
case 'ArrowRight':
case 'PageDown':
this.nextSlide();
break;
case 'ArrowLeft':
case 'PageUp':
this.previousSlide();
break;
case 'Escape':
if (screenfull.isEnabled && screenfull.isFullscreen) {
screenfull.exit();
}
break;
}
}
}
API 参考
screenfull 方法
// 检查全屏 API 是否支持
screenfull.isEnabled // boolean
// 切换全屏
screenfull.toggle() // 切换文档
screenfull.toggle(element) // 切换特定元素
// 请求全屏
screenfull.request() // 请求文档
screenfull.request(element) // 请求特定元素
// 退出全屏
screenfull.exit()
// 检查当前是否全屏
screenfull.isFullscreen // boolean
// 获取全屏元素
screenfull.element // Element | null
// 事件监听器
screenfull.on('change', () => {})
screenfull.on('error', (event) => {})
screenfull.off('change', handler)
集成检查表
使用 screenfull 时:
- [ ] 检查
screenfull.isEnabled后再使用 API - [ ] 优雅地处理浏览器兼容性
- [ ] 在组件销毁时清理事件监听器
- [ ] 提供退出全屏按钮
- [ ] 在不同浏览器上测试(Chrome、Firefox、Safari)
- [ ] 处理键盘快捷键(Esc 退出)
- [ ] 适当地为全屏模式设置样式
- [ ] 考虑移动浏览器(支持有限)
最佳实践
应该做 ✅
- 总是检查
screenfull.isEnabled后再使用 - 提供清晰的 UI 退出全屏
- 使用 Esc 键退出全屏
- 适当地为全屏元素设置样式
- 清理事件监听器
- 优雅地处理错误
不应该做 ❌
- 假设全屏 API 总是可用的
- 未经用户交互强制全屏
- 忘记移除事件监听器
- 忽视移动浏览器限制
- 将全屏用于敏感操作
浏览器兼容性
| 浏览器 | 支持 |
|---|---|
| Chrome | ✅ 全面支持 |
| Firefox | ✅ 全面支持 |
| Safari | ✅ 全面支持(iOS 有限) |
| Edge | ✅ 全面支持 |
| iOS Safari | ⚠️ 有限(仅限视频) |
| Android Chrome | ✅ 全面支持 |
参考资料
版本: 1.0.0
兼容于: screenfull 6.0.x
最后更新: 2025-12-25