461 lines
15 KiB
HTML
461 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>短视频解析平台 - 免费高清无水印</title>
|
|
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
|
<style>
|
|
body {
|
|
background: linear-gradient(135deg, #f0f9ff 0%, #e0e7ff 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.hero-container {
|
|
width: 100%;
|
|
max-width: 700px;
|
|
text-align: center;
|
|
margin-bottom: 2rem;
|
|
animation: fadeIn 0.8s ease-out;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 3rem;
|
|
font-weight: 800;
|
|
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
margin-bottom: 0.5rem;
|
|
letter-spacing: -0.05em;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 1.125rem;
|
|
color: var(--secondary-500);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.main-card {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
backdrop-filter: blur(20px);
|
|
border-radius: 24px;
|
|
padding: 3rem;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
|
width: 100%;
|
|
max-width: 700px;
|
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.input-wrapper {
|
|
position: relative;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.url-input {
|
|
width: 100%;
|
|
padding: 1.25rem 1.5rem;
|
|
font-size: 1rem;
|
|
border: 2px solid transparent;
|
|
border-radius: 16px;
|
|
background: #f1f5f9;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.url-input:focus {
|
|
outline: none;
|
|
background: white;
|
|
border-color: var(--primary-500);
|
|
box-shadow: 0 0 0 4px var(--primary-100);
|
|
}
|
|
|
|
.action-btn {
|
|
width: 100%;
|
|
padding: 1.25rem;
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
border-radius: 16px;
|
|
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 10px 20px -5px rgba(79, 70, 229, 0.4);
|
|
}
|
|
|
|
.action-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 15px 30px -5px rgba(79, 70, 229, 0.5);
|
|
}
|
|
|
|
.action-btn:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.action-btn:disabled {
|
|
opacity: 0.7;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.result-card {
|
|
margin-top: 2rem;
|
|
background: white;
|
|
border-radius: 20px;
|
|
overflow: hidden;
|
|
display: none;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
animation: fadeIn 0.5s ease;
|
|
}
|
|
|
|
.result-card.show {
|
|
display: block;
|
|
}
|
|
|
|
.video-preview {
|
|
width: 100%;
|
|
aspect-ratio: 16/9;
|
|
background: black;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.result-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.video-title {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
color: var(--secondary-900);
|
|
margin-bottom: 0.5rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.video-meta {
|
|
font-size: 0.875rem;
|
|
color: var(--secondary-500);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.download-actions {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
gap: 1rem;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.download-actions {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.nav-links {
|
|
margin-top: 2rem;
|
|
display: flex;
|
|
gap: 1.5rem;
|
|
justify-content: center;
|
|
}
|
|
|
|
.nav-link {
|
|
color: var(--secondary-500);
|
|
text-decoration: none;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.nav-link:hover {
|
|
color: var(--primary-600);
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from { opacity: 0; transform: translateY(20px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 24px;
|
|
height: 24px;
|
|
border: 3px solid rgba(255,255,255,0.3);
|
|
border-radius: 50%;
|
|
border-top-color: white;
|
|
animation: spin 1s linear infinite;
|
|
display: none;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.btn-text { display: inline; }
|
|
.action-btn.loading .btn-text { display: none; }
|
|
.action-btn.loading .loading-spinner { display: block; }
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{% if config.get('site_notice') %}
|
|
<div style="background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); padding: 1rem; text-align: center; border-radius: 12px; margin: 1rem auto; max-width: 700px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
|
<div style="color: #92400e; font-weight: 500; margin: 0;">{{ config.get('site_notice') | safe }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="hero-container">
|
|
<h1 class="logo-text">{{ config.get('home_title', 'JieXi Pro') }}</h1>
|
|
<p class="subtitle">{{ config.get('home_subtitle', '新一代全能短视频去水印解析工具') }}</p>
|
|
</div>
|
|
|
|
<div class="main-card">
|
|
<div class="input-wrapper">
|
|
<input type="text" class="url-input" id="videoUrl" placeholder="在此粘贴 抖音/TikTok/B站 视频链接...">
|
|
</div>
|
|
<button class="action-btn" onclick="parseVideo()" id="parseBtn">
|
|
<span class="btn-text">立即解析</span>
|
|
<div class="loading-spinner"></div>
|
|
</button>
|
|
|
|
<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">
|
|
下载视频
|
|
</a>
|
|
<button class="ui-btn ui-btn-primary" onclick="downloadCover()">
|
|
下载封面
|
|
</button>
|
|
<button class="ui-btn ui-btn-secondary" onclick="copyLink()">
|
|
复制链接
|
|
</button>
|
|
</div>
|
|
<a id="coverLink" href="" style="display: none;"></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-links" id="navLinks">
|
|
<a href="/auth/login" class="nav-link">登录账号</a>
|
|
<a href="/auth/register" class="nav-link">注册新用户</a>
|
|
</div>
|
|
|
|
{% if config.get('site_footer') %}
|
|
<footer style="margin-top: 3rem; text-align: center; color: var(--secondary-500); font-size: 0.875rem; line-height: 1.8;">
|
|
{{ config.get('site_footer') | safe }}
|
|
</footer>
|
|
{% endif %}
|
|
|
|
<script src="/static/js/ui-components.js"></script>
|
|
<script>
|
|
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+/
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
const match = text.match(pattern);
|
|
if (match) return match[0];
|
|
}
|
|
return text.trim();
|
|
}
|
|
|
|
async function parseVideo() {
|
|
const input = document.getElementById('videoUrl');
|
|
const btn = document.getElementById('parseBtn');
|
|
const resultCard = document.getElementById('result');
|
|
|
|
const rawUrl = input.value.trim();
|
|
if (!rawUrl) {
|
|
UI.notify('请先粘贴视频链接', 'warning');
|
|
return;
|
|
}
|
|
|
|
const url = extractVideoUrl(rawUrl);
|
|
if (!url) {
|
|
UI.notify('无法识别有效的视频链接', 'error');
|
|
return;
|
|
}
|
|
|
|
// Reset UI
|
|
resultCard.classList.remove('show');
|
|
btn.disabled = true;
|
|
btn.classList.add('loading');
|
|
|
|
try {
|
|
const response = await fetch('/api/parse', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url: url })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
if (data.status === 'completed') {
|
|
showResult(data.data);
|
|
} else if (data.status === 'queued') {
|
|
pollTaskResult(data.task_id);
|
|
}
|
|
} else {
|
|
throw new Error(data.message || '解析失败');
|
|
}
|
|
} catch (error) {
|
|
UI.notify(error.message, 'error');
|
|
btn.disabled = false;
|
|
btn.classList.remove('loading');
|
|
}
|
|
}
|
|
|
|
async function pollTaskResult(taskId) {
|
|
const maxAttempts = 30;
|
|
let attempts = 0;
|
|
const btn = document.getElementById('parseBtn');
|
|
|
|
const poll = async () => {
|
|
if (attempts >= maxAttempts) {
|
|
UI.notify('解析超时,请稍后重试', 'error');
|
|
btn.disabled = false;
|
|
btn.classList.remove('loading');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/task/${taskId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'completed') {
|
|
showResult(data.data);
|
|
} else if (data.status === 'failed') {
|
|
throw new Error(data.message || '解析失败');
|
|
} else {
|
|
attempts++;
|
|
setTimeout(poll, 2000);
|
|
}
|
|
} catch (error) {
|
|
UI.notify(error.message, 'error');
|
|
btn.disabled = false;
|
|
btn.classList.remove('loading');
|
|
}
|
|
};
|
|
|
|
poll();
|
|
}
|
|
|
|
function showResult(data) {
|
|
const btn = document.getElementById('parseBtn');
|
|
const resultCard = document.getElementById('result');
|
|
|
|
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;
|
|
|
|
document.getElementById('videoLink').href = data.video_url;
|
|
document.getElementById('coverLink').href = data.cover;
|
|
|
|
resultCard.classList.add('show');
|
|
btn.disabled = false;
|
|
btn.classList.remove('loading');
|
|
|
|
UI.notify('解析成功', 'success');
|
|
}
|
|
|
|
async function downloadCover() {
|
|
const coverUrl = document.getElementById('coverLink').href;
|
|
if (!coverUrl) {
|
|
UI.notify('封面链接无效', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
UI.notify('正在下载封面...', 'info');
|
|
|
|
// 使用 fetch 下载,设置 no-referrer 策略
|
|
const response = await fetch(coverUrl, {
|
|
method: 'GET',
|
|
referrerPolicy: 'no-referrer'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('下载失败');
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'cover_' + Date.now() + '.jpg';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
UI.notify('封面下载成功', 'success');
|
|
} catch (error) {
|
|
UI.notify('下载失败: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function copyLink() {
|
|
const url = document.getElementById('videoLink').href;
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
UI.notify('链接已复制', 'success');
|
|
}).catch(() => {
|
|
UI.notify('复制失败', 'error');
|
|
});
|
|
}
|
|
|
|
document.getElementById('videoUrl').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') parseVideo();
|
|
});
|
|
|
|
// 检查登录状态并更新导航
|
|
async function checkLoginStatus() {
|
|
try {
|
|
const response = await fetch('/auth/user-info');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
document.getElementById('navLinks').innerHTML = `
|
|
<a href="/auth/profile" class="nav-link">个人中心</a>
|
|
<a href="#" class="nav-link" onclick="logout(); return false;">退出登录</a>
|
|
`;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// 未登录,保持默认导航
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
try {
|
|
await fetch('/auth/logout', { method: 'POST' });
|
|
UI.notify('已退出登录', 'success');
|
|
setTimeout(() => location.reload(), 1000);
|
|
} catch (e) {
|
|
UI.notify('退出失败', 'error');
|
|
}
|
|
}
|
|
|
|
checkLoginStatus();
|
|
</script>
|
|
</body>
|
|
</html>
|