403 lines
19 KiB
HTML
403 lines
19 KiB
HTML
<!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 & 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>
|