feat: 新增平台

This commit is contained in:
2025-11-30 19:49:25 +08:00
parent c3e56a954d
commit fbd2c491b2
41 changed files with 4293 additions and 76 deletions

View File

@@ -201,6 +201,73 @@
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 输入框按钮组 */
.input-group {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.input-group .url-input {
flex: 1;
margin-bottom: 0;
}
.input-btn {
padding: 0 1.25rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 12px;
border: 2px solid var(--primary-200);
background: white;
color: var(--primary-600);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.input-btn:hover {
background: var(--primary-50);
border-color: var(--primary-400);
}
.input-btn:active {
transform: scale(0.96);
}
/* 进度条 */
.progress-container {
display: none;
margin-top: 1.5rem;
}
.progress-container.show {
display: block;
}
.progress-label {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.875rem;
color: var(--secondary-600);
}
.progress-bar {
height: 8px;
background: var(--secondary-100);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4f46e5 0%, #7c3aed 100%);
border-radius: 4px;
width: 0%;
transition: width 0.3s ease;
}
</style>
</head>
<body>
@@ -217,22 +284,36 @@
<div class="main-card">
<div class="input-wrapper">
<input type="text" class="url-input" id="videoUrl" placeholder="在此粘贴 抖音/TikTok/B站 视频链接...">
<div class="input-group">
<input type="text" class="url-input" id="videoUrl" placeholder="在此粘贴 抖音/TikTok/B站/快手/皮皮虾/微博 视频链接...">
<button class="input-btn" onclick="pasteUrl()">粘贴</button>
<button class="input-btn" onclick="clearAll()">清空</button>
</div>
</div>
<button class="action-btn" onclick="parseVideo()" id="parseBtn">
<span class="btn-text">立即解析</span>
<div class="loading-spinner"></div>
</button>
<div class="progress-container" id="progressContainer">
<div class="progress-label">
<span id="progressText">正在解析...</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
</div>
<div id="result" class="result-card">
<video id="videoPlayer" class="video-preview" controls></video>
<div class="result-content">
<h3 class="video-title" id="title"></h3>
<p class="video-meta" id="description"></p>
<div class="download-actions">
<a id="videoLink" class="ui-btn ui-btn-primary" href="" download target="_blank">
<button class="ui-btn ui-btn-primary" onclick="downloadVideo()">
下载视频
</a>
</button>
<button class="ui-btn ui-btn-primary" onclick="downloadCover()">
下载封面
</button>
@@ -240,12 +321,14 @@
复制链接
</button>
</div>
<input type="hidden" id="videoLink" value="">
<a id="coverLink" href="" style="display: none;"></a>
</div>
</div>
</div>
<div class="nav-links" id="navLinks">
<a href="/api-docs" class="nav-link">API 文档</a>
<a href="/auth/login" class="nav-link">登录账号</a>
<a href="/auth/register" class="nav-link">注册新用户</a>
</div>
@@ -258,13 +341,103 @@
<script src="/static/js/ui-components.js"></script>
<script>
// 粘贴按钮
async function pasteUrl() {
try {
const text = await navigator.clipboard.readText();
document.getElementById('videoUrl').value = text;
UI.notify('已粘贴', 'success');
} catch (e) {
UI.notify('无法访问剪贴板,请手动粘贴', 'warning');
}
}
// 清空按钮
function clearAll() {
document.getElementById('videoUrl').value = '';
document.getElementById('result').classList.remove('show');
document.getElementById('progressContainer').classList.remove('show');
document.getElementById('videoPlayer').src = '';
document.getElementById('videoPlayer').poster = '';
UI.notify('已清空', 'info');
}
// 进度条控制
let progressInterval = null;
function startProgress() {
const container = document.getElementById('progressContainer');
const fill = document.getElementById('progressFill');
const text = document.getElementById('progressText');
const percent = document.getElementById('progressPercent');
container.classList.add('show');
let progress = 0;
const steps = [
{ p: 15, t: '正在连接服务器...' },
{ p: 35, t: '正在解析链接...' },
{ p: 55, t: '正在获取视频信息...' },
{ p: 75, t: '正在提取无水印地址...' },
{ p: 90, t: '即将完成...' }
];
let stepIndex = 0;
progressInterval = setInterval(() => {
if (stepIndex < steps.length && progress >= steps[stepIndex].p - 10) {
text.textContent = steps[stepIndex].t;
stepIndex++;
}
if (progress < 90) {
progress += Math.random() * 8 + 2;
if (progress > 90) progress = 90;
fill.style.width = progress + '%';
percent.textContent = Math.round(progress) + '%';
}
}, 300);
}
function completeProgress() {
if (progressInterval) {
clearInterval(progressInterval);
progressInterval = null;
}
const fill = document.getElementById('progressFill');
const text = document.getElementById('progressText');
const percent = document.getElementById('progressPercent');
fill.style.width = '100%';
percent.textContent = '100%';
text.textContent = '解析完成!';
setTimeout(() => {
document.getElementById('progressContainer').classList.remove('show');
fill.style.width = '0%';
}, 500);
}
function resetProgress() {
if (progressInterval) {
clearInterval(progressInterval);
progressInterval = null;
}
const container = document.getElementById('progressContainer');
const fill = document.getElementById('progressFill');
container.classList.remove('show');
fill.style.width = '0%';
}
function extractVideoUrl(text) {
const patterns = [
/https?:\/\/v\.douyin\.com\/[A-Za-z0-9_-]+/,
/https?:\/\/www\.douyin\.com\/video\/\d+/,
/https?:\/\/(?:www\.)?bilibili\.com\/video\/[A-Za-z0-9?&=]+/,
/https?:\/\/b23\.tv\/[A-Za-z0-9]+/,
/https?:\/\/(?:www\.)?tiktok\.com\/@[^\/]+\/video\/\d+/
/https?:\/\/(?:www\.)?tiktok\.com\/@[^\/]+\/video\/\d+/,
/https?:\/\/(?:www\.)?kuaishou\.com\/[A-Za-z0-9\/_-]+/,
/https?:\/\/h5\.pipix\.com\/s\/[A-Za-z0-9_-]+\/?/,
/https?:\/\/(?:www\.)?pipix\.com\/[A-Za-z0-9\/_-]+/,
/https?:\/\/(?:video\.)?weibo\.com\/show\?fid=[0-9:]+/,
/https?:\/\/(?:www\.)?weibo\.com\/[A-Za-z0-9\/]+/
];
for (const pattern of patterns) {
@@ -295,6 +468,7 @@
resultCard.classList.remove('show');
btn.disabled = true;
btn.classList.add('loading');
startProgress();
try {
const response = await fetch('/api/parse', {
@@ -318,6 +492,7 @@
UI.notify(error.message, 'error');
btn.disabled = false;
btn.classList.remove('loading');
resetProgress();
}
}
@@ -331,6 +506,7 @@
UI.notify('解析超时,请稍后重试', 'error');
btn.disabled = false;
btn.classList.remove('loading');
resetProgress();
return;
}
@@ -350,6 +526,7 @@
UI.notify(error.message, 'error');
btn.disabled = false;
btn.classList.remove('loading');
resetProgress();
}
};
@@ -360,14 +537,31 @@
const btn = document.getElementById('parseBtn');
const resultCard = document.getElementById('result');
completeProgress();
document.getElementById('title').textContent = data.title || '无标题';
document.getElementById('description').textContent = data.description || '';
const player = document.getElementById('videoPlayer');
player.src = data.video_url;
player.poster = data.cover;
// 先停止并清空当前视频
player.pause();
player.removeAttribute('src');
player.load();
document.getElementById('videoLink').href = data.video_url;
// 使用代理播放视频,绕过防盗链
const proxyVideoUrl = '/proxy/download?url=' + encodeURIComponent(data.video_url);
player.src = proxyVideoUrl;
// 封面也使用代理
if (data.cover) {
const proxyCoverUrl = '/proxy/download?url=' + encodeURIComponent(data.cover);
player.poster = proxyCoverUrl;
}
// 强制重新加载视频
player.load();
document.getElementById('videoLink').value = data.video_url;
document.getElementById('coverLink').href = data.cover;
resultCard.classList.add('show');
@@ -377,6 +571,18 @@
UI.notify('解析成功', 'success');
}
function downloadVideo() {
const videoUrl = document.getElementById('videoLink').value;
if (!videoUrl) {
UI.notify('视频链接无效', 'error');
return;
}
// 使用代理下载
const proxyUrl = '/proxy/download?url=' + encodeURIComponent(videoUrl);
window.open(proxyUrl, '_blank');
}
async function downloadCover() {
const coverUrl = document.getElementById('coverLink').href;
if (!coverUrl) {
@@ -434,6 +640,7 @@
const data = await response.json();
if (data.success) {
document.getElementById('navLinks').innerHTML = `
<a href="/api-docs" class="nav-link">API 文档</a>
<a href="/auth/profile" class="nav-link">个人中心</a>
<a href="#" class="nav-link" onclick="logout(); return false;">退出登录</a>
`;