mirror of
https://github.com/ElmGates/ticiqi.git
synced 2026-01-12 17:14:55 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e4d315aac | ||
|
|
a5f54055ea | ||
|
|
208a91ab4d | ||
|
|
8af1960081 | ||
|
|
b806e5e30c | ||
|
|
172f978edf | ||
|
|
4966135ff5 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
97
README.md
97
README.md
@@ -1,10 +1,87 @@
|
||||
# 提词器源码
|
||||
两种提词器网页的源码,包含文本提词器和pdf提词器,支持直接在本地使用,但是可能需要联网。
|
||||
|
||||
# 使用方法
|
||||
1.直接下载源码,本地浏览器打开即可,需要联网,部分js使用的是网络链接。
|
||||
2.部署到自己的服务器,只要支持http访问即可。
|
||||
3.使用Cloudflare Pages部署即可。
|
||||
|
||||
# 版权说明
|
||||
本项目完全开源,可用于个人,商用请署名,二次开发请署名。
|
||||
# 网页版提词器 (Web Teleprompter) v1.5
|
||||
|
||||
一个功能强大、简单易用的网页版提词器工具。专为演讲、视频录制、直播等场景设计,支持多设备远程协同控制。
|
||||
<a href="https://teleprompter.superjia.com.cn">在线演示</a>
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
### 📝 文本提词器 (核心版本)
|
||||
包含以下特性:
|
||||
|
||||
* **智能文本处理**:
|
||||
* 支持文本粘贴与编辑。
|
||||
* **智能格式化**:自动根据标点符号进行分段,优化阅读体验。
|
||||
* **强制分段**:支持设置最大段落字数,防止单行过长。
|
||||
* **撤销/重做**:支持撤销格式化操作。
|
||||
* **专业的显示控制**:
|
||||
* **镜像模式**:支持横向镜像翻转,适配专业提词器玻璃。
|
||||
* **样式自定义**:自由调节字体大小、行间距、文字颜色、背景颜色。
|
||||
* **全屏模式**:沉浸式阅读体验。
|
||||
* **平滑滚动控制**:
|
||||
* 可调节滚动速度 (0.1 - 200)。
|
||||
* 支持暂停/开始、重置。
|
||||
* 支持上一行/下一行精准微调。
|
||||
* **键盘快捷键支持**。
|
||||
* **🤝 远程协作 (Beta)**:
|
||||
* 基于 P2P 技术 (PeerJS) 实现。
|
||||
* **多端同步**:支持手机/平板作为遥控器控制电脑端提词器。
|
||||
* **全量同步**:实时同步文本内容、滚动状态、速度设置及跳转进度。
|
||||
* **简易连接**:通过 4 位数字房间 ID 快速连接。
|
||||
* 无需服务器中转,隐私安全。
|
||||
* **移动端适配**:
|
||||
* 针对手机端优化的提示与交互体验。
|
||||
|
||||
### 📄 PDF 提词器
|
||||
位于 `pdf` 目录:
|
||||
* 支持直接上传 PDF 文件。
|
||||
* 自动处理 PDF 内容进行提词播放。
|
||||
* 支持镜像翻转与速度控制。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方式 1:直接运行
|
||||
本项目为纯静态网页,无需安装任何依赖。
|
||||
1. 双击 `index.html` 在浏览器中打开即可使用。
|
||||
|
||||
### 方式 2:部署
|
||||
可以将内容部署到任何静态网页托管服务(如 GitHub Pages, Vercel, Nginx 等)。
|
||||
|
||||
## 🎮 操作指南
|
||||
|
||||
### 快捷键
|
||||
* **空格键 (Space)**: 暂停 / 继续滚动
|
||||
* **R 键**: 重置滚动到顶部
|
||||
* **F 键**: 切换全屏模式
|
||||
* **Enter 键**: 在跳转输入框中确认跳转
|
||||
|
||||
### 远程控制使用方法
|
||||
1. **主机端(显示端)**:
|
||||
* 打开高级功能菜单 -> 远程协作。
|
||||
* 点击“创建房间”。
|
||||
* 将生成的 4 位数字 ID 发送给控制端。
|
||||
2. **控制端(遥控端)**:
|
||||
* 打开同样的网页。
|
||||
* 打开高级功能菜单 -> 远程协作。
|
||||
* 输入主机端的 ID,点击“加入”。
|
||||
* 连接成功后,控制端的任何操作(滚动、修改文本、设置)都会实时同步到主机端。
|
||||
|
||||
## 📂 目录结构
|
||||
|
||||
```
|
||||
/
|
||||
├── index.html # 主程序入口
|
||||
├── script.js # 核心逻辑 (含 Teleprompter 类与 RemoteController 类)
|
||||
├── styles.css # 样式文件
|
||||
└── pdf/ # PDF 提词器模块
|
||||
└── index.html
|
||||
```
|
||||
|
||||
|
||||
## 🛠️ 技术栈
|
||||
* HTML5 / CSS3 / JavaScript (ES6+)
|
||||
* **PeerJS**: 用于实现 WebRTC 远程 P2P 通信。
|
||||
* **PDF.js**: 用于解析和渲染 PDF 文件。
|
||||
* **Mammoth.js**: 用于文档处理支持。
|
||||
|
||||
## 📄 版权信息
|
||||
© 2025 SuperJia. All Rights Reserved.
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
<button id="pauseScroll">暂停</button>
|
||||
<button id="resetScroll">重置</button>
|
||||
</div>
|
||||
<div class="control-buttons" style="margin-top: 10px;">
|
||||
<button id="prevLineBtn">上一行</button>
|
||||
<button id="nextLineBtn">下一行</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
@@ -79,14 +83,37 @@
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="formatTextBtn" style="width: 100px;">格式化文本</button>
|
||||
<button id="undoFormatBtn" style="width: 100px; margin-left: 10px;">撤销格式化</button>
|
||||
<button id="undoFormatBtn" style="width: 100px; margin-left: 10px;">撤销格式化</button>
|
||||
</div>
|
||||
|
||||
<div class="remote-control-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px;">
|
||||
<h4 style="margin-bottom: 10px;">远程协作 (beta)</h4>
|
||||
<div id="connectionStatus" style="font-size: 12px; color: #666; margin-bottom: 10px;">状态: 未连接</div>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="createRoomBtn" style="width: 100%;">创建房间</button>
|
||||
</div>
|
||||
|
||||
<div id="roomInfo" style="display: none; margin-top: 10px; background: #f5f5f5; padding: 10px; border-radius: 4px;">
|
||||
<div style="font-size: 12px; margin-bottom: 5px;">房间 ID:</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<span id="roomIdDisplay" style="font-weight: bold; font-size: 18px; color: #007bff;"></span>
|
||||
<button id="copyRoomIdBtn" style="padding: 2px 8px; font-size: 12px;">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group" style="margin-top: 15px;">
|
||||
<input type="text" id="joinRoomInput" placeholder="输入房间ID" style="flex: 1; padding: 8px;">
|
||||
<button id="joinRoomBtn" style="width: 80px; margin-left: 10px;">加入</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<button id="toggleFullscreen">全屏显示</button>
|
||||
<button id="togglePanel">隐藏控制面板</button>
|
||||
<button onclick="window.location.href='pdf/index.html'" style="margin-top: 10px; background-color: #6c757d;">前往 PDF 提词器</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
@@ -107,7 +134,7 @@
|
||||
<!-- 版权信息 -->
|
||||
<div class="copyright-section">
|
||||
<p style="margin: 0; padding: 10px 0; text-align: center; font-size: 12px; color: #666; border-top: 1px solid #eee;">
|
||||
© 2025-Now SuperJia<br> All Rights Reserved<br>版本 v1.2, 发布于2025-09
|
||||
© 2025-Now SuperJia<br> All Rights Reserved<br>版本 v1.5, 发布于2025-12
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,6 +152,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
<!-- 手机端提示框 -->
|
||||
<div id="mobileAlert" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<h3>温馨提示</h3>
|
||||
<p>手机屏幕较小,可能不能获得最佳体验。</p>
|
||||
<p style="font-size: 12px; color: #666; margin-top: 5px;">建议使用平板或电脑访问,或尝试横屏使用。</p>
|
||||
<button id="closeMobileAlert">我知道了</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script>
|
||||
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
|
||||
<script src="script.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,174 @@
|
||||
class RemoteController {
|
||||
constructor(teleprompter) {
|
||||
this.teleprompter = teleprompter;
|
||||
this.peer = null;
|
||||
this.conn = null;
|
||||
this.roomId = null;
|
||||
this.isHost = false;
|
||||
this.isProcessingRemote = false;
|
||||
|
||||
this.initializeElements();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.createRoomBtn = document.getElementById('createRoomBtn');
|
||||
this.joinRoomBtn = document.getElementById('joinRoomBtn');
|
||||
this.joinRoomInput = document.getElementById('joinRoomInput');
|
||||
this.roomInfo = document.getElementById('roomInfo');
|
||||
this.roomIdDisplay = document.getElementById('roomIdDisplay');
|
||||
this.copyRoomIdBtn = document.getElementById('copyRoomIdBtn');
|
||||
this.connectionStatus = document.getElementById('connectionStatus');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
if(this.createRoomBtn) this.createRoomBtn.addEventListener('click', () => this.createRoom());
|
||||
if(this.joinRoomBtn) this.joinRoomBtn.addEventListener('click', () => {
|
||||
const roomId = this.joinRoomInput.value.trim();
|
||||
if (roomId) this.joinRoom(roomId);
|
||||
});
|
||||
if(this.copyRoomIdBtn) this.copyRoomIdBtn.addEventListener('click', () => {
|
||||
if (this.roomId) {
|
||||
navigator.clipboard.writeText(this.roomId);
|
||||
alert('房间 ID 已复制');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateShortId() {
|
||||
return Math.floor(1000 + Math.random() * 9000).toString();
|
||||
}
|
||||
|
||||
createRoom() {
|
||||
const id = this.generateShortId();
|
||||
this.peer = new Peer(id);
|
||||
|
||||
this.peer.on('open', (id) => {
|
||||
this.roomId = id;
|
||||
this.isHost = true;
|
||||
this.showRoomInfo(id);
|
||||
this.updateStatus('等待连接...');
|
||||
this.createRoomBtn.disabled = true;
|
||||
this.joinRoomBtn.disabled = true;
|
||||
});
|
||||
|
||||
this.peer.on('connection', (conn) => {
|
||||
this.handleConnection(conn);
|
||||
});
|
||||
|
||||
this.peer.on('error', (err) => {
|
||||
console.error(err);
|
||||
if (err.type === 'unavailable-id') {
|
||||
this.peer.destroy();
|
||||
this.createRoom();
|
||||
} else {
|
||||
alert('创建房间失败: ' + err.type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
joinRoom(roomId) {
|
||||
this.peer = new Peer();
|
||||
|
||||
this.peer.on('open', () => {
|
||||
const conn = this.peer.connect(roomId);
|
||||
this.handleConnection(conn);
|
||||
});
|
||||
|
||||
this.peer.on('error', (err) => {
|
||||
console.error(err);
|
||||
alert('连接失败: ' + err.type);
|
||||
});
|
||||
}
|
||||
|
||||
handleConnection(conn) {
|
||||
this.conn = conn;
|
||||
|
||||
conn.on('open', () => {
|
||||
this.updateStatus('已连接');
|
||||
if (this.isHost) {
|
||||
this.syncAllState();
|
||||
}
|
||||
});
|
||||
|
||||
conn.on('data', (data) => {
|
||||
this.handleData(data);
|
||||
});
|
||||
|
||||
conn.on('close', () => {
|
||||
this.updateStatus('连接断开');
|
||||
this.conn = null;
|
||||
});
|
||||
}
|
||||
|
||||
sendData(type, payload) {
|
||||
if (this.conn && this.conn.open) {
|
||||
this.conn.send({ type, payload });
|
||||
}
|
||||
}
|
||||
|
||||
handleData(data) {
|
||||
this.isProcessingRemote = true;
|
||||
try {
|
||||
switch(data.type) {
|
||||
case 'fullSync':
|
||||
this.teleprompter.textInput.value = data.payload.text;
|
||||
this.teleprompter.formatText(); // 简单显示
|
||||
// 应用设置
|
||||
const s = data.payload.settings;
|
||||
this.teleprompter.scrollSpeed.value = s.scrollSpeed;
|
||||
this.teleprompter.fontSize.value = s.fontSize;
|
||||
this.teleprompter.lineHeight.value = s.lineHeight;
|
||||
this.teleprompter.updateFontSize();
|
||||
this.teleprompter.updateLineHeight();
|
||||
|
||||
if (data.payload.currentLine !== undefined) {
|
||||
setTimeout(() => {
|
||||
this.teleprompter.jumpToLineByIndex(data.payload.currentLine);
|
||||
}, 100);
|
||||
}
|
||||
break;
|
||||
case 'control':
|
||||
if (data.payload.action === 'start') this.teleprompter.startScrolling();
|
||||
if (data.payload.action === 'pause') this.teleprompter.pauseScrolling();
|
||||
if (data.payload.action === 'reset') this.teleprompter.resetScrolling();
|
||||
if (data.payload.action === 'prevLine') this.teleprompter.prevLine();
|
||||
if (data.payload.action === 'nextLine') this.teleprompter.nextLine();
|
||||
break;
|
||||
case 'text':
|
||||
this.teleprompter.textInput.value = data.payload.text;
|
||||
this.teleprompter.formatText();
|
||||
break;
|
||||
case 'jump':
|
||||
this.teleprompter.jumpToLineByIndex(data.payload.index);
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
this.isProcessingRemote = false;
|
||||
}
|
||||
}
|
||||
|
||||
showRoomInfo(id) {
|
||||
if(this.roomInfo) this.roomInfo.style.display = 'block';
|
||||
if(this.roomIdDisplay) this.roomIdDisplay.textContent = id;
|
||||
}
|
||||
|
||||
updateStatus(msg) {
|
||||
if(this.connectionStatus) this.connectionStatus.textContent = '状态: ' + msg;
|
||||
}
|
||||
|
||||
syncAllState() {
|
||||
this.sendData('fullSync', {
|
||||
text: this.teleprompter.textInput.value,
|
||||
settings: {
|
||||
fontSize: this.teleprompter.fontSize.value,
|
||||
scrollSpeed: this.teleprompter.scrollSpeed.value,
|
||||
lineHeight: this.teleprompter.lineHeight.value
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Teleprompter {
|
||||
constructor() {
|
||||
console.log('开始初始化提词器...');
|
||||
@@ -15,8 +186,10 @@ class Teleprompter {
|
||||
this.bindEvents();
|
||||
this.loadSettings();
|
||||
this.initializeTextPosition();
|
||||
this.checkMobileDevice();
|
||||
|
||||
console.log('提词器初始化完成');
|
||||
this.remoteController = new RemoteController(this);
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
@@ -32,6 +205,8 @@ class Teleprompter {
|
||||
this.startScrollBtn = document.getElementById('startScroll');
|
||||
this.pauseScrollBtn = document.getElementById('pauseScroll');
|
||||
this.resetScrollBtn = document.getElementById('resetScroll');
|
||||
this.prevLineBtn = document.getElementById('prevLineBtn');
|
||||
this.nextLineBtn = document.getElementById('nextLineBtn');
|
||||
|
||||
// 显示设置
|
||||
this.fontSize = document.getElementById('fontSize');
|
||||
@@ -73,6 +248,10 @@ class Teleprompter {
|
||||
// 容器
|
||||
this.container = document.querySelector('.container');
|
||||
this.teleprompterContainer = document.getElementById('teleprompterContainer');
|
||||
|
||||
// 手机端提示
|
||||
this.mobileAlert = document.getElementById('mobileAlert');
|
||||
this.closeMobileAlertBtn = document.getElementById('closeMobileAlert');
|
||||
|
||||
console.log('元素初始化完成');
|
||||
}
|
||||
@@ -92,6 +271,8 @@ class Teleprompter {
|
||||
this.startScrollBtn.addEventListener('click', () => this.startScrolling());
|
||||
this.pauseScrollBtn.addEventListener('click', () => this.pauseScrolling());
|
||||
this.resetScrollBtn.addEventListener('click', () => this.resetScrolling());
|
||||
if(this.prevLineBtn) this.prevLineBtn.addEventListener('click', () => this.prevLine());
|
||||
if(this.nextLineBtn) this.nextLineBtn.addEventListener('click', () => this.nextLine());
|
||||
|
||||
// 显示设置
|
||||
this.fontSize.addEventListener('input', () => this.updateFontSize());
|
||||
@@ -124,6 +305,13 @@ class Teleprompter {
|
||||
|
||||
// 全屏状态变化
|
||||
document.addEventListener('fullscreenchange', () => this.handleFullscreenChange());
|
||||
|
||||
// 手机端提示
|
||||
if (this.closeMobileAlertBtn) {
|
||||
this.closeMobileAlertBtn.addEventListener('click', () => {
|
||||
this.mobileAlert.style.display = 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initializeTextPosition() {
|
||||
@@ -139,6 +327,24 @@ class Teleprompter {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
checkMobileDevice() {
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768;
|
||||
|
||||
console.log('设备检测:', {
|
||||
userAgent: navigator.userAgent,
|
||||
width: window.innerWidth,
|
||||
isMobile: isMobile
|
||||
});
|
||||
|
||||
if (isMobile) {
|
||||
// 检查是否已经显示过提示
|
||||
if (!sessionStorage.getItem('mobileAlertShown')) {
|
||||
this.mobileAlert.style.display = 'flex';
|
||||
sessionStorage.setItem('mobileAlertShown', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示文本功能(简单显示,不格式化)
|
||||
formatText() {
|
||||
const text = this.textInput.value.trim();
|
||||
@@ -170,7 +376,9 @@ class Teleprompter {
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('teleprompterText', text);
|
||||
|
||||
alert('文本显示完成!共' + lines.length + '行');
|
||||
this.notifyRemote('text', { text: text });
|
||||
|
||||
// alert('文本显示完成!共' + lines.length + '行');
|
||||
}
|
||||
|
||||
// 智能格式化功能
|
||||
@@ -204,7 +412,7 @@ class Teleprompter {
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('teleprompterText', text);
|
||||
|
||||
alert('智能格式化完成!共' + sentences.length + '行');
|
||||
// alert('智能格式化完成!共' + sentences.length + '行');
|
||||
}
|
||||
|
||||
// 撤销格式化功能
|
||||
@@ -238,7 +446,7 @@ class Teleprompter {
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('teleprompterText', text);
|
||||
|
||||
alert('格式化已撤销!共' + lines.length + '行');
|
||||
// alert('格式化已撤销!共' + lines.length + '行');
|
||||
}
|
||||
|
||||
splitByPunctuation(text) {
|
||||
@@ -372,6 +580,7 @@ class Teleprompter {
|
||||
|
||||
// 滚动功能
|
||||
startScrolling() {
|
||||
this.notifyRemote('control', { action: 'start' });
|
||||
if (this.formattedText.length === 0) {
|
||||
alert('请先格式化文本!');
|
||||
return;
|
||||
@@ -440,6 +649,7 @@ class Teleprompter {
|
||||
|
||||
pauseScrolling() {
|
||||
if (this.isScrolling && !this.isPaused) {
|
||||
this.notifyRemote('control', { action: 'pause' });
|
||||
this.isPaused = true;
|
||||
clearInterval(this.scrollInterval);
|
||||
this.pauseScrollBtn.textContent = '继续';
|
||||
@@ -654,12 +864,40 @@ class Teleprompter {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jumpToLineByIndex(targetLine - 1);
|
||||
this.notifyRemote('jump', { index: targetLine - 1 });
|
||||
}
|
||||
|
||||
notifyRemote(type, payload) {
|
||||
if (this.remoteController && !this.remoteController.isProcessingRemote) {
|
||||
this.remoteController.sendData(type, payload);
|
||||
}
|
||||
}
|
||||
|
||||
prevLine() {
|
||||
if (!this.formattedText || this.formattedText.length === 0) return;
|
||||
this.jumpToLineByIndex(this.currentLine - 1);
|
||||
this.notifyRemote('control', { action: 'prevLine' });
|
||||
}
|
||||
|
||||
nextLine() {
|
||||
if (!this.formattedText || this.formattedText.length === 0) return;
|
||||
this.jumpToLineByIndex(this.currentLine + 1);
|
||||
this.notifyRemote('control', { action: 'nextLine' });
|
||||
}
|
||||
|
||||
jumpToLineByIndex(index) {
|
||||
if (index < 0) index = 0;
|
||||
if (index >= this.totalLines) index = this.totalLines - 1;
|
||||
|
||||
this.stopScrolling();
|
||||
this.currentLine = targetLine - 1; // 转换为0基索引
|
||||
this.currentLine = index;
|
||||
this.currentPosition = this.calculatePositionForLine(this.currentLine);
|
||||
|
||||
const isMirror = this.mirrorText.checked;
|
||||
const mirrorScale = isMirror ? ' scaleX(-1)' : ' scaleX(1)';
|
||||
this.textDisplay.style.transform = `translateY(-${this.currentPosition}px)${mirrorScale}`;
|
||||
|
||||
this.updateProgress();
|
||||
}
|
||||
|
||||
@@ -221,6 +221,43 @@ button {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
#prevLineBtn, #nextLineBtn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#prevLineBtn:hover, #nextLineBtn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
#createRoomBtn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
#createRoomBtn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
#joinRoomBtn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
#joinRoomBtn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
#copyRoomIdBtn {
|
||||
background: #e9ecef;
|
||||
color: #333;
|
||||
border: 1px solid #ced4da;
|
||||
padding: 2px 8px;
|
||||
margin: 0;
|
||||
}
|
||||
#copyRoomIdBtn:hover {
|
||||
background: #dae0e5;
|
||||
}
|
||||
|
||||
#toggleFullscreen {
|
||||
background: #6f42c1;
|
||||
color: white;
|
||||
@@ -469,4 +506,68 @@ body.panel-hidden-bg {
|
||||
.panel-section:nth-child(2) { animation-delay: 0.2s; }
|
||||
.panel-section:nth-child(3) { animation-delay: 0.3s; }
|
||||
.panel-section:nth-child(4) { animation-delay: 0.4s; }
|
||||
.panel-section:nth-child(5) { animation-delay: 0.5s; }
|
||||
.panel-section:nth-child(5) { animation-delay: 0.5s; }
|
||||
|
||||
/* 模态框样式 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
backdrop-filter: blur(5px);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
width: 85%;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(0);
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
color: #555;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-content button {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.modal-content button:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user