Files
42_104/caddy/www/url_cleaning_tool.html
2026-06-23 19:33:50 +08:00

403 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL 追蹤參數淨化器</title>
<meta name="description" content="移除網址中的 utm、fbclid、gclid 等追蹤碼,還你乾淨連結。">
<meta name="theme-color" content="#0E2B2A">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="URL 淨化器">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='24' fill='%230E2B2A'/><path d='M50 22C50 22 32 44 32 60a18 18 0 0 0 36 0C68 44 50 22 50 22Z' fill='%2312B5A5'/></svg>">
<link rel="apple-touch-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' fill='%230E2B2A'/><path d='M50 22C50 22 32 44 32 60a18 18 0 0 0 36 0C68 44 50 22 50 22Z' fill='%2312B5A5'/></svg>">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
display: ['"Space Grotesk"', 'sans-serif'],
sans: ['"Noto Sans TC"', 'sans-serif'],
mono: ['"JetBrains Mono"', 'monospace'],
},
colors: {
ink: '#0E2B2A',
mint: '#EAF4F0',
brand: { DEFAULT: '#12B5A5', deep: '#0A8F82' },
parasite: '#F0584F',
},
},
},
};
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Noto+Sans+TC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap');
body { font-family: 'Noto Sans TC', sans-serif; touch-action: manipulation; }
/* 水波背景:淡淡的青綠暈染,呼應「清水」 */
.water-bg {
background-color: #EAF4F0;
background-image:
radial-gradient(60rem 60rem at 110% -10%, rgba(18, 181, 165, 0.12), transparent 60%),
radial-gradient(50rem 50rem at -10% 110%, rgba(10, 143, 130, 0.10), transparent 60%);
}
.fade-in { animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
/* 寄生蟲標籤滑入 */
.tag-pop { animation: tagPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; }
@keyframes tagPop { from { opacity: 0; transform: scale(0.8) translateY(4px); } to { opacity: 1; transform: scale(1); } }
/* 數字輕微跳出 */
.stat-pop { animation: statPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes statPop { from { opacity: 0; transform: scale(0.7); } to { opacity: 1; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) {
.fade-in, .tag-pop, .stat-pop { animation: none; }
}
</style>
</head>
<body class="water-bg text-ink min-h-screen flex flex-col items-center justify-center p-4 sm:p-6">
<main class="w-full max-w-2xl bg-white rounded-3xl shadow-[0_20px_60px_-15px_rgba(14,43,42,0.25)] overflow-hidden border border-brand/10">
<!-- Header左對齊、編輯感避開置中模板樣式 -->
<header class="relative bg-ink px-6 sm:px-8 py-7 text-white overflow-hidden">
<div class="absolute -right-10 -top-10 w-44 h-44 rounded-full bg-brand/20 blur-2xl"></div>
<div class="relative">
<div class="flex items-center gap-2 text-brand text-[11px] font-mono font-medium tracking-[0.25em] uppercase mb-3">
<span class="inline-block w-6 h-px bg-brand"></span>
Link Sanitizer
</div>
<h1 class="font-display text-3xl sm:text-4xl font-bold leading-tight flex items-center gap-3">
<i class="ph-fill ph-drop-half text-brand text-3xl sm:text-4xl"></i>
URL 淨化器
</h1>
<p class="text-white/55 mt-3 text-sm leading-relaxed max-w-md">
把連結上的 <span class="font-mono text-brand">utm</span><span class="font-mono text-brand">fbclid</span><span class="font-mono text-brand">gclid</span> 等追蹤碼濾掉,留下乾淨的連結。
</p>
</div>
</header>
<!-- 輸入區 -->
<section class="px-6 sm:px-8 py-7 space-y-5">
<div>
<label for="urlInput" class="block text-xs font-semibold text-brand-deep tracking-wide uppercase mb-2">原始網址</label>
<div class="relative flex items-center">
<input type="text" id="urlInput"
class="w-full font-mono text-sm pl-4 pr-20 py-3.5 bg-mint/60 border border-brand/20 rounded-xl text-ink placeholder:text-ink/30 focus:ring-4 focus:ring-brand/15 focus:border-brand outline-none transition-all"
placeholder="貼上髒髒的網址…" autofocus>
<button onclick="pasteUrl()"
class="absolute right-2 px-3 py-1.5 text-xs font-semibold text-brand-deep bg-white border border-brand/25 hover:bg-brand hover:text-white hover:border-brand rounded-lg transition-colors">
貼上
</button>
</div>
</div>
<div class="flex gap-3">
<button onclick="cleanUrl()" id="cleanBtn"
class="flex-1 bg-brand hover:bg-brand-deep text-white font-semibold py-3.5 px-4 rounded-xl shadow-lg shadow-brand/25 transition-all flex items-center justify-center gap-2 active:scale-[0.98]">
<i class="ph-bold ph-sparkle text-lg"></i>
開始淨化
</button>
<button onclick="resetApp()"
class="px-5 py-3.5 border border-ink/15 text-ink/60 hover:bg-mint hover:text-ink rounded-xl font-medium transition-colors">
重設
</button>
</div>
</section>
<!-- 結果區 -->
<section id="resultArea" class="hidden border-t border-mint bg-mint/40 px-6 sm:px-8 py-7 fade-in">
<div id="offlineNote" class="hidden items-center gap-1.5 text-[11px] font-mono text-ink/45 mb-4">
<i class="ph-bold ph-wifi-slash"></i>
<span>後端未回應,已用前端離線淨化</span>
</div>
<div id="successMsg" class="hidden items-center gap-2 text-brand-deep font-semibold mb-5">
<i class="ph-fill ph-check-circle text-xl"></i>
<span>淨化完成</span>
</div>
<div id="noChangeMsg" class="hidden items-center gap-2 text-ink/55 font-medium bg-white border border-ink/10 p-3.5 rounded-xl mb-5">
<i class="ph ph-shield-check text-xl text-brand-deep"></i>
<span>這個網址已經很乾淨了,沒有發現追蹤參數。</span>
</div>
<div id="errorMsg" class="hidden items-center gap-2 text-parasite font-medium bg-parasite/8 border border-parasite/25 p-3.5 rounded-xl mb-5">
<i class="ph-bold ph-warning-circle text-xl"></i>
<span id="errorText">發生錯誤</span>
</div>
<!-- Signature清除成果大數字 -->
<div id="statsRow" class="hidden gap-3 mb-5">
<div class="flex-1 bg-white rounded-2xl border border-brand/15 px-4 py-3.5">
<div class="flex items-baseline gap-1.5">
<span id="statCount" class="font-display text-3xl font-bold text-parasite stat-pop">0</span>
<span class="text-xs text-ink/45 font-medium">隻寄生蟲</span>
</div>
<p class="text-[11px] text-ink/40 mt-0.5">已從連結上刮除</p>
</div>
<div class="flex-1 bg-white rounded-2xl border border-brand/15 px-4 py-3.5">
<div class="flex items-baseline gap-1.5">
<span id="statChars" class="font-display text-3xl font-bold text-brand-deep stat-pop">0</span>
<span class="text-xs text-ink/45 font-medium">字元</span>
</div>
<p class="text-[11px] text-ink/40 mt-0.5">網址變短了</p>
</div>
</div>
<label class="block text-xs font-semibold text-brand-deep tracking-wide uppercase mb-2">乾淨網址</label>
<!-- 手機直排、桌面並排:修正複製鈕被擠出畫面 -->
<div class="flex flex-col sm:flex-row gap-2">
<input type="text" id="cleanedUrl" readonly
class="min-w-0 flex-1 font-mono text-sm bg-white border border-brand/25 text-ink rounded-xl px-4 py-3.5 focus:outline-none focus:ring-4 focus:ring-brand/15 focus:border-brand">
<button onclick="copyResult()" id="copyBtn"
class="w-full sm:w-auto shrink-0 bg-brand-deep hover:bg-ink text-white px-5 py-3.5 rounded-xl font-semibold shadow-sm transition-all flex items-center justify-center gap-2 active:scale-[0.98]">
<i class="ph-bold ph-copy text-lg"></i>
複製
</button>
</div>
<div id="removedParamsContainer" class="mt-5 hidden">
<p class="text-[11px] text-ink/40 uppercase tracking-[0.2em] font-bold mb-2.5">已移除的寄生蟲</p>
<div id="tagsArea" class="flex flex-wrap gap-2"></div>
</div>
</section>
<footer class="bg-white px-6 py-3.5 text-center border-t border-mint">
<p class="text-[11px] text-ink/35 font-mono tracking-wide">Powered by FastAPI &amp; Tailwind CSS</p>
</footer>
</main>
<script>
// 設定你的 API 位址
// const API_URL = 'http://192.168.42.108:16888/clean';
const API_URL = 'https://ucapi.timmy.edu.kg/clean';
const urlInput = document.getElementById('urlInput');
const resultArea = document.getElementById('resultArea');
const cleanedUrlInput = document.getElementById('cleanedUrl');
const cleanBtn = document.getElementById('cleanBtn');
const tagsArea = document.getElementById('tagsArea');
const removedParamsContainer = document.getElementById('removedParamsContainer');
const statsRow = document.getElementById('statsRow');
const offlineNote = document.getElementById('offlineNote');
// 前端離線淨化用的追蹤參數清單(後端不通時的備援)
const TRACKING_EXACT = new Set([
'fbclid', 'gclid', 'dclid', 'gclsrc', 'gbraid', 'wbraid', 'msclkid',
'mc_eid', 'mc_cid', 'igshid', 'igsh', 'ttclid', 'twclid', 'yclid',
'vero_id', 'vero_conv', 'oly_enc_id', 'oly_anon_id', 'wickedid',
'soc_src', 'soc_trk', 'mkt_tok', 'cmpid', 's_kwcid', 'ef_id',
'fb_action_ids', 'fb_action_types', 'fb_source', 'fb_ref',
'action_object_map', 'action_type_map', 'action_ref_map',
'spm', 'scm', '_openstat', 'campaignid', 'adgroupid',
'ref_src', 'ref_url', '_branch_match_id', 'trk_contact', 'trk_msg'
]);
const TRACKING_PREFIX = ['utm_', 'mtm_', 'pk_', 'hsa_', '_hs', 'ga_', '_ga'];
// 純前端淨化:解析網址、移除追蹤參數,回傳與後端相同的資料結構
function cleanUrlLocally(input) {
let u = null;
const candidates = /^https?:\/\//i.test(input) ? [input] : ['https://' + input, input];
for (const c of candidates) {
try { u = new URL(c); break; } catch (e) { /* 換下一個 */ }
}
if (!u) return null;
const removed = [];
for (const key of [...u.searchParams.keys()]) {
const lk = key.toLowerCase();
if (TRACKING_EXACT.has(lk) || TRACKING_PREFIX.some(p => lk.startsWith(p))) {
u.searchParams.delete(key);
if (!removed.includes(key)) removed.push(key);
}
}
let cleaned = u.toString().replace(/\?$/, '');
return { success: true, cleaned_url: cleaned, removed_params: removed };
}
// 綁定 Enter 鍵觸發
urlInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
cleanUrl();
}
});
// 貼上網址功能
async function pasteUrl() {
try {
const text = await navigator.clipboard.readText();
urlInput.value = text;
// 貼上後如果內容不為空,自動觸發清理(可選)
if (text) cleanUrl();
} catch (err) {
alert('無法讀取剪貼簿,請手動貼上。');
}
}
// 重設介面
function resetApp() {
urlInput.value = '';
resultArea.classList.add('hidden');
urlInput.focus();
}
// 核心清理功能
async function cleanUrl() {
const originalUrl = urlInput.value.trim();
if (!originalUrl) {
alert('請先輸入網址!');
return;
}
// UI Loading 狀態
cleanBtn.innerHTML = '<i class="ph ph-spinner ph-spin text-lg"></i> 處理中…';
cleanBtn.disabled = true;
// 隱藏之前的結果
resultArea.classList.add('hidden');
hideAllMessages();
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ url: originalUrl })
});
if (!response.ok) {
throw new Error(`伺服器回應錯誤: ${response.status}`);
}
const data = await response.json();
if (data.success) {
showResult(data, originalUrl, false);
} else {
showError(data.message || '無法處理此網址');
}
} catch (error) {
console.error('API Error改用前端離線淨化:', error);
// 後端不通時的備援:純前端淨化
const local = cleanUrlLocally(originalUrl);
if (local) {
showResult(local, originalUrl, true);
} else {
showError('無法解析此網址,請確認格式是否正確。');
}
} finally {
// 還原按鈕狀態
cleanBtn.innerHTML = '<i class="ph-bold ph-sparkle text-lg"></i> 開始淨化';
cleanBtn.disabled = false;
}
}
// 顯示結果
function showResult(data, originalUrl, isLocal) {
resultArea.classList.remove('hidden');
cleanedUrlInput.value = data.cleaned_url;
// 離線淨化提示
offlineNote.classList.toggle('hidden', !isLocal);
offlineNote.classList.toggle('flex', !!isLocal);
// 判斷是否有移除參數
if (data.removed_params && data.removed_params.length > 0) {
const successMsg = document.getElementById('successMsg');
successMsg.classList.remove('hidden');
successMsg.classList.add('flex');
// Signature 統計:清除數量 + 縮短字元數
const count = data.removed_params.length;
const charsSaved = Math.max(0, (originalUrl || '').length - (data.cleaned_url || '').length);
statsRow.classList.remove('hidden');
statsRow.classList.add('flex');
animateNumber(document.getElementById('statCount'), count);
animateNumber(document.getElementById('statChars'), charsSaved);
// 顯示標籤(含進場動畫)
removedParamsContainer.classList.remove('hidden');
tagsArea.innerHTML = data.removed_params.map((param, i) =>
`<span class="tag-pop inline-flex items-center gap-1 bg-parasite/8 text-parasite px-2.5 py-1 rounded-lg text-sm font-mono border border-parasite/25 line-through decoration-parasite/50" style="animation-delay:${i * 50}ms">
<i class="ph-bold ph-bug text-xs no-underline"></i>${param}
</span>`
).join('');
} else {
const noChangeMsg = document.getElementById('noChangeMsg');
noChangeMsg.classList.remove('hidden');
noChangeMsg.classList.add('flex');
removedParamsContainer.classList.add('hidden');
}
}
// 數字遞增動畫
function animateNumber(el, target) {
const duration = 500;
const start = performance.now();
function tick(now) {
const p = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(eased * target);
if (p < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
// 顯示錯誤
function showError(msg) {
resultArea.classList.remove('hidden');
const errDiv = document.getElementById('errorMsg');
errDiv.classList.remove('hidden');
errDiv.classList.add('flex');
document.getElementById('errorText').innerText = msg;
}
function hideAllMessages() {
['successMsg', 'noChangeMsg', 'errorMsg'].forEach(id => {
const el = document.getElementById(id);
el.classList.add('hidden');
el.classList.remove('flex');
});
statsRow.classList.add('hidden');
statsRow.classList.remove('flex');
offlineNote.classList.add('hidden');
offlineNote.classList.remove('flex');
}
// 複製功能
function copyResult() {
const copyText = document.getElementById("cleanedUrl");
copyText.select();
copyText.setSelectionRange(0, 99999); // Mobile compatibility
navigator.clipboard.writeText(copyText.value).then(() => {
const btn = document.getElementById('copyBtn');
const originalHtml = btn.innerHTML;
// 視覺回饋
btn.innerHTML = '<i class="ph-bold ph-check text-lg"></i> 已複製';
btn.classList.remove('bg-brand-deep', 'hover:bg-ink');
btn.classList.add('bg-ink');
setTimeout(() => {
btn.innerHTML = originalHtml;
btn.classList.add('bg-brand-deep', 'hover:bg-ink');
btn.classList.remove('bg-ink');
}, 2000);
});
}
</script>
</body>
</html>