Initial commit: caddy static file server workspace

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 19:33:50 +08:00
commit a98b21114c
1438 changed files with 15044 additions and 0 deletions

View File

@@ -0,0 +1,402 @@
<!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>