mirror of
https://github.com/imputnet/cobalt.git
synced 2025-06-28 09:28:29 +00:00
831 lines
24 KiB
Svelte
831 lines
24 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { t } from '$lib/i18n/translations';
|
|
import SettingsCategory from '$components/settings/SettingsCategory.svelte';
|
|
import ActionButton from '$components/buttons/ActionButton.svelte';
|
|
import QRCode from 'qrcode';
|
|
import { currentApiURL } from '$lib/api/api-url';
|
|
// Import clipboard components
|
|
import SessionManager from '$components/clipboard/SessionManager.svelte';
|
|
import TabNavigation from '$components/clipboard/TabNavigation.svelte';
|
|
import FileTransfer from '$components/clipboard/FileTransfer.svelte';
|
|
import TextSharing from '$components/clipboard/TextSharing.svelte';
|
|
// Import clipboard manager
|
|
import { ClipboardManager, clipboardState, type FileItem } from '$lib/clipboard/clipboard-manager'; // Types
|
|
interface ReceivingFile {
|
|
name: string;
|
|
size: number;
|
|
type: string;
|
|
chunks: Uint8Array[];
|
|
receivedSize: number;
|
|
}
|
|
|
|
// Constants
|
|
const CHUNK_SIZE = 64 * 1024; // 64KB chunks for file transfer
|
|
|
|
// State variables - bound to clipboard manager
|
|
let sessionId = '';
|
|
let joinCode = '';
|
|
let isConnected = false;
|
|
let isCreating = false;
|
|
let isJoining = false;
|
|
let isCreator = false;
|
|
let peerConnected = false;
|
|
let qrCodeUrl = '';
|
|
|
|
// Navigation state
|
|
let activeTab: 'files' | 'text' = 'files';
|
|
|
|
// File transfer state
|
|
let files: File[] = [];
|
|
let receivedFiles: FileItem[] = [];
|
|
let textContent = '';
|
|
let receivedText = '';
|
|
let dragover = false;
|
|
let sendingFiles = false; let receivingFiles = false;
|
|
let transferProgress = 0;
|
|
let currentReceivingFile: ReceivingFile | null = null;
|
|
let dataChannel: RTCDataChannel | null = null;
|
|
let peerConnection: RTCPeerConnection | null = null;
|
|
|
|
// Error handling state
|
|
let errorMessage = '';
|
|
let showError = false;
|
|
let waitingForCreator = false;
|
|
|
|
// Clipboard manager instance
|
|
let clipboardManager: ClipboardManager; // Subscribe to clipboard state
|
|
$: if (clipboardManager) {
|
|
clipboardState.subscribe(state => {
|
|
sessionId = state.sessionId;
|
|
isConnected = state.isConnected;
|
|
isCreating = state.isCreating;
|
|
isJoining = state.isJoining;
|
|
isCreator = state.isCreator;
|
|
peerConnected = state.peerConnected;
|
|
qrCodeUrl = state.qrCodeUrl;
|
|
activeTab = state.activeTab;
|
|
files = state.files;
|
|
receivedFiles = state.receivedFiles;
|
|
// Don't overwrite textContent - it's managed by the TextSharing component binding
|
|
// textContent = state.textContent;
|
|
receivedText = state.receivedText;
|
|
dragover = state.dragover;
|
|
sendingFiles = state.sendingFiles;
|
|
receivingFiles = state.receivingFiles;
|
|
transferProgress = state.transferProgress;
|
|
dataChannel = state.dataChannel;
|
|
peerConnection = state.peerConnection;
|
|
errorMessage = state.errorMessage;
|
|
showError = state.showError;
|
|
waitingForCreator = state.waitingForCreator;
|
|
});
|
|
}
|
|
|
|
// Event handlers for components
|
|
function handleCreateSession() {
|
|
clipboardManager?.createSession();
|
|
} function handleJoinSession() {
|
|
if (joinCode.trim()) {
|
|
clipboardManager?.joinSession(joinCode.trim());
|
|
}
|
|
}
|
|
|
|
function handleCleanup() {
|
|
clipboardManager?.cleanup();
|
|
}
|
|
|
|
function handleTabChange(event: CustomEvent<'files' | 'text'>) {
|
|
clipboardState.update(state => ({ ...state, activeTab: event.detail }));
|
|
}
|
|
|
|
// File transfer handlers
|
|
function handleFilesSelected(event: CustomEvent) {
|
|
clipboardState.update(state => ({
|
|
...state,
|
|
files: [...state.files, ...event.detail.files]
|
|
}));
|
|
}
|
|
|
|
function handleRemoveFile(event: CustomEvent) {
|
|
clipboardState.update(state => ({
|
|
...state,
|
|
files: state.files.filter((_, i) => i !== event.detail.index)
|
|
}));
|
|
} function handleSendFiles() {
|
|
clipboardManager?.sendFiles();
|
|
}
|
|
|
|
function handleDownloadFile(event: CustomEvent) {
|
|
const file = event.detail.file;
|
|
const url = URL.createObjectURL(file.blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = file.name;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function handleRemoveReceivedFile(event: CustomEvent) {
|
|
clipboardState.update(state => ({
|
|
...state,
|
|
receivedFiles: state.receivedFiles.filter((_, i) => i !== event.detail.index)
|
|
}));
|
|
} // Text sharing handlers
|
|
function handleSendText(event?: CustomEvent) {
|
|
const text = event?.detail?.text || textContent;
|
|
console.log('handleSendText called with text:', text);
|
|
console.log('clipboardManager exists:', !!clipboardManager);
|
|
console.log('dataChannel ready:', clipboardManager?.debugInfo?.dataChannel?.readyState);
|
|
console.log('peerConnected:', peerConnected);
|
|
|
|
if (text.trim()) {
|
|
clipboardManager?.sendText(text);
|
|
} else {
|
|
console.log('No text to send');
|
|
} } function handleClearText() {
|
|
clipboardState.update(state => ({ ...state, receivedText: '' }));
|
|
}
|
|
|
|
function handleClearError() {
|
|
clipboardManager?.clearError();
|
|
}
|
|
|
|
// Lifecycle functions
|
|
onMount(async () => {
|
|
clipboardManager = new ClipboardManager();
|
|
|
|
// Check for session parameter in URL
|
|
if (typeof window !== 'undefined') {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const sessionParam = urlParams.get('session');
|
|
if (sessionParam) {
|
|
joinCode = sessionParam;
|
|
await clipboardManager.joinSession(joinCode);
|
|
}
|
|
}
|
|
}); onDestroy(() => {
|
|
clipboardManager?.cleanup();
|
|
});
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>cobalt | {$t("clipboard.title")}</title>
|
|
<meta property="og:title" content="cobalt | {$t("clipboard.title")}" />
|
|
<meta property="og:description" content={$t("clipboard.description")} />
|
|
<meta property="description" content={$t("clipboard.description")} />
|
|
</svelte:head>
|
|
|
|
<div class="clipboard-container">
|
|
<div class="clipboard-header">
|
|
<h1>{$t("clipboard.title")}</h1>
|
|
<div class="description-container">
|
|
<p class="description-main">{$t("clipboard.description")}</p>
|
|
<p class="description-subtitle">{$t("clipboard.description_subtitle")}</p>
|
|
</div> </div>
|
|
|
|
<!-- Error Message Display -->
|
|
{#if showError && errorMessage}
|
|
<div class="error-notification" class:waiting={waitingForCreator}>
|
|
<div class="error-content">
|
|
<div class="error-icon">
|
|
{#if waitingForCreator}
|
|
<div class="spinner"></div>
|
|
{:else}
|
|
⚠️
|
|
{/if}
|
|
</div> <div class="error-text">
|
|
<strong>{waitingForCreator ? '等待中' : '提示'}</strong>
|
|
<p>{errorMessage}</p>
|
|
</div>
|
|
<button class="error-close" on:click={handleClearError} aria-label="关闭">
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if isConnected && peerConnected}
|
|
<!-- Connection Status Indicator -->
|
|
<div class="session-status">
|
|
<div class="status-badge">
|
|
<div class="status-dot connected"></div>
|
|
<span class="status-text">已连接到会话</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Navigation Component - Moved to top -->
|
|
<TabNavigation
|
|
{activeTab}
|
|
on:tabChange={handleTabChange}
|
|
/>
|
|
|
|
<!-- Content Area - Moved to top -->
|
|
<div class="tab-content">
|
|
<!-- File Transfer Component -->
|
|
{#if activeTab === 'files'}
|
|
<FileTransfer
|
|
{files}
|
|
{receivedFiles}
|
|
{sendingFiles}
|
|
{receivingFiles}
|
|
{transferProgress}
|
|
{dragover}
|
|
{peerConnected}
|
|
on:filesSelected={handleFilesSelected}
|
|
on:removeFile={handleRemoveFile}
|
|
on:sendFiles={handleSendFiles}
|
|
on:downloadFile={handleDownloadFile}
|
|
on:removeReceivedFile={handleRemoveReceivedFile}
|
|
/>
|
|
{/if}
|
|
|
|
<!-- Text Sharing Component -->
|
|
{#if activeTab === 'text'}
|
|
<TextSharing
|
|
{receivedText}
|
|
{peerConnected}
|
|
on:sendText={handleSendText}
|
|
on:clearText={handleClearText}
|
|
bind:textContent
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Session Management Section - Moved to bottom -->
|
|
<div class="session-management-section">
|
|
<div class="session-info">
|
|
<h3>会话管理</h3>
|
|
<p>会话ID: <code>{sessionId}</code></p> <div class="session-actions">
|
|
<button class="btn-secondary danger" on:click={handleCleanup}>
|
|
断开连接
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<!-- Session Management Component - Show when not connected -->
|
|
<SessionManager
|
|
{sessionId} {isConnected}
|
|
{isCreating}
|
|
{isJoining}
|
|
{isCreator}
|
|
{peerConnected}
|
|
{qrCodeUrl}
|
|
on:createSession={handleCreateSession}
|
|
on:joinSession={handleJoinSession}
|
|
on:cleanup={handleCleanup}
|
|
bind:joinCode
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
<style> /* Main container styles */
|
|
.clipboard-container {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
padding: 1rem;
|
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
|
border-radius: 20px;
|
|
backdrop-filter: blur(10px);
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
min-height: 60vh;
|
|
}.clipboard-header {
|
|
text-align: center;
|
|
margin-bottom: 0.5rem;
|
|
padding: 0.25rem 0;
|
|
position: relative;
|
|
}
|
|
|
|
.clipboard-header::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 60px;
|
|
height: 4px;
|
|
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
|
|
border-radius: 2px;
|
|
margin-bottom: 1rem;
|
|
} .clipboard-header h1 {
|
|
margin-bottom: 0.2rem;
|
|
font-size: 2.2rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, var(--accent), var(--accent-hover));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
margin-top: 0.3rem;
|
|
}.clipboard-header p {
|
|
color: var(--subtext);
|
|
font-size: 1.1rem;
|
|
font-weight: 400;
|
|
opacity: 0.8;
|
|
} .description-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.02rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.description-main {
|
|
color: var(--text);
|
|
font-size: 1.3rem !important;
|
|
font-weight: 500 !important;
|
|
opacity: 0.9 !important;
|
|
margin: 0;
|
|
}
|
|
|
|
.description-subtitle {
|
|
color: var(--subtext);
|
|
font-size: 0.95rem !important;
|
|
font-weight: 400 !important;
|
|
opacity: 0.7 !important;
|
|
margin: 0;
|
|
max-width: 600px; line-height: 1.4;
|
|
}
|
|
|
|
/* Error notification styles */
|
|
.error-notification {
|
|
margin: 1rem 0;
|
|
padding: 1rem;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
border-radius: 12px;
|
|
backdrop-filter: blur(8px);
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
.error-notification.waiting {
|
|
background: rgba(34, 197, 94, 0.1);
|
|
border-color: rgba(34, 197, 94, 0.3);
|
|
}
|
|
|
|
.error-content {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.error-icon {
|
|
flex-shrink: 0;
|
|
font-size: 1.2rem;
|
|
margin-top: 0.1rem;
|
|
}
|
|
|
|
.spinner {
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 2px solid rgba(34, 197, 94, 0.3);
|
|
border-top: 2px solid #22c55e;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
.error-text {
|
|
flex: 1;
|
|
}
|
|
|
|
.error-text strong {
|
|
color: #ef4444;
|
|
font-weight: 600;
|
|
display: block;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.error-notification.waiting .error-text strong {
|
|
color: #22c55e;
|
|
}
|
|
|
|
.error-text p {
|
|
color: var(--text);
|
|
margin: 0;
|
|
font-size: 0.9rem;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.error-close {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text);
|
|
font-size: 1.2rem;
|
|
cursor: pointer;
|
|
opacity: 0.7;
|
|
transition: opacity 0.2s ease;
|
|
padding: 0;
|
|
line-height: 1;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.error-close:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes spin {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* Enhanced session status */
|
|
.session-status {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0.25rem 0;
|
|
padding: 0.4rem;
|
|
background: rgba(34, 197, 94, 0.1);
|
|
border: 1px solid rgba(34, 197, 94, 0.2);
|
|
border-radius: 12px;
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.status-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.8rem;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: #22c55e;
|
|
box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.status-text {
|
|
font-weight: 500;
|
|
color: #22c55e;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.7;
|
|
transform: scale(1.1);
|
|
}
|
|
} /* Tab content styling */
|
|
.tab-content {
|
|
background: rgba(255, 255, 255, 0.03);
|
|
border-radius: 15px;
|
|
padding: 0.75rem;
|
|
margin-top: 0.25rem;
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
backdrop-filter: blur(8px);
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: visible; /* 允许通知显示在容器外 */
|
|
/* PC端高度限制 - 增加高度 */
|
|
max-height: 65vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.tab-content:hover {
|
|
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.08);
|
|
border-color: rgba(255, 255, 255, 0.12);
|
|
}
|
|
|
|
/* Session management section styling */
|
|
.session-management-section {
|
|
margin-top: 1.5rem;
|
|
padding: 1rem;
|
|
background: rgba(255, 255, 255, 0.03);
|
|
border-radius: 15px;
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.session-info {
|
|
text-align: center;
|
|
}
|
|
|
|
.session-info h3 {
|
|
color: var(--text);
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.session-info p {
|
|
color: var(--subtext);
|
|
font-size: 0.9rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.session-info code {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
padding: 0.2rem 0.5rem;
|
|
border-radius: 6px;
|
|
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
|
|
color: var(--accent);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.session-actions {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
border-radius: 10px;
|
|
padding: 0.6rem 1.2rem;
|
|
color: var(--text);
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: rgba(255, 255, 255, 0.12);
|
|
border-color: rgba(255, 255, 255, 0.25);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-secondary.danger {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border-color: rgba(239, 68, 68, 0.3);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.btn-secondary.danger:hover {
|
|
background: rgba(239, 68, 68, 0.15);
|
|
border-color: rgba(239, 68, 68, 0.4);
|
|
}
|
|
|
|
/* Enhanced card-like sections */
|
|
:global(.card) {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
backdrop-filter: blur(10px);
|
|
transition: all 0.3s ease;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
:global(.card:hover) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
|
border-color: rgba(255, 255, 255, 0.15);
|
|
}
|
|
|
|
/* Progress indicators */
|
|
:global(.progress-container) {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 10px;
|
|
padding: 1rem;
|
|
margin: 1rem 0;
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
/* File drop zone enhancements */
|
|
:global(.drop-zone) {
|
|
border: 2px dashed rgba(255, 255, 255, 0.2);
|
|
border-radius: 16px;
|
|
padding: 3rem 2rem;
|
|
text-align: center;
|
|
transition: all 0.3s ease;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:global(.drop-zone::before) {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
|
|
transition: left 0.6s ease;
|
|
}
|
|
|
|
:global(.drop-zone:hover::before) {
|
|
left: 100%;
|
|
}
|
|
|
|
:global(.drop-zone.dragover) {
|
|
border-color: var(--accent);
|
|
background: rgba(var(--accent-rgb), 0.05);
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
/* Button enhancements */
|
|
:global(.btn-primary) {
|
|
background: linear-gradient(135deg, var(--accent), var(--accent-hover));
|
|
border: none;
|
|
border-radius: 12px;
|
|
padding: 0.8rem 2rem;
|
|
font-weight: 600;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 15px rgba(var(--accent-rgb), 0.3);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:global(.btn-primary:hover) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 25px rgba(var(--accent-rgb), 0.4);
|
|
}
|
|
|
|
:global(.btn-primary::before) {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
transition: left 0.5s ease;
|
|
}
|
|
|
|
:global(.btn-primary:hover::before) {
|
|
left: 100%;
|
|
}
|
|
|
|
/* Text areas and inputs */
|
|
:global(.text-input, .textarea) {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 12px;
|
|
padding: 1rem;
|
|
transition: all 0.3s ease;
|
|
backdrop-filter: blur(5px);
|
|
}
|
|
|
|
:global(.text-input:focus, .textarea:focus) {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.1);
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
/* Enhanced spacing and layout */
|
|
:global(.section-spacing) {
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
:global(.content-spacing) {
|
|
margin: 1.5rem 0;
|
|
}
|
|
|
|
/* Loading states */
|
|
:global(.loading-shimmer) {
|
|
background: linear-gradient(90deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0.05) 100%);
|
|
background-size: 200% 100%;
|
|
animation: shimmer 1.5s infinite;
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% {
|
|
background-position: -200% 0;
|
|
}
|
|
100% {
|
|
background-position: 200% 0;
|
|
} } /* Override SettingsCategory default padding for clipboard page */
|
|
:global(.settings-content) {
|
|
padding: 0.25rem !important;
|
|
gap: 0.5rem !important;
|
|
}
|
|
|
|
@media screen and (max-width: 750px) {
|
|
:global(.settings-content) {
|
|
padding: 0.1rem !important;
|
|
gap: 0.25rem !important;
|
|
}
|
|
} /* Responsive Design */ /* PC/Desktop 优化 - 1024px 及以上 */
|
|
@media (min-width: 1024px) {
|
|
.clipboard-container {
|
|
max-height: 95vh;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.tab-content {
|
|
max-height: 85vh;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
padding: 1rem;
|
|
/* 确保子组件可以使用横向布局 */
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.tab-content::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.tab-content::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.tab-content::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.tab-content::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(255, 255, 255, 0.25);
|
|
}
|
|
|
|
/* 确保内容区域不会溢出 */
|
|
.session-management-section {
|
|
flex-shrink: 0;
|
|
}
|
|
} /* 超大桌面屏幕优化 - 1440px 及以上 */
|
|
@media (min-width: 1440px) {
|
|
.clipboard-container {
|
|
max-height: 98vh;
|
|
}
|
|
|
|
.tab-content {
|
|
max-height: 90vh;
|
|
padding: 1.5rem;
|
|
}
|
|
}/* 平板优化 - 768px 到 1023px */ @media (min-width: 768px) and (max-width: 1023px) {
|
|
.clipboard-container {
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.tab-content {
|
|
max-height: 70vh;
|
|
overflow-y: auto;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.clipboard-container {
|
|
padding: 0.75rem;
|
|
margin: 0.75rem;
|
|
border-radius: 16px;
|
|
max-height: 85vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.clipboard-header h1 {
|
|
font-size: 1.9rem;
|
|
} .clipboard-header {
|
|
padding: 0.5rem 0;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
:global(.card) {
|
|
padding: 0.75rem;
|
|
}
|
|
|
|
:global(.tab-content) {
|
|
padding: 0.75rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
} @media (max-width: 480px) {
|
|
.clipboard-container {
|
|
margin: 0.5rem;
|
|
padding: 0.5rem;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.clipboard-header h1 {
|
|
font-size: 1.6rem;
|
|
}
|
|
|
|
:global(.drop-zone) {
|
|
padding: 1.5rem 1rem;
|
|
}
|
|
}
|
|
</style>
|