<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>演示站合集</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme:{
extend:{
colors:{
dark:"#111318",
card:"#1b1f29",
primary:"#3b82f6",
ok:"#22c55e",
down:"#ef4444"
}
}
}
}
</script>
<style>
*{box-sizing:border-box}
.card-img{min-height:90px;object-fit:cover}
.modal-wrap{background:rgba(0,0,0,0.75)}
</style>
</head>
<body class="bg-dark text-gray-200">
<div class="max-w-7xl mx-auto px-4 py-6">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
<div>
<h1 class="text-[22px] font-bold text-white">演示站合集</h1>
<p class="text-gray-400 text-sm mt-1">总计:<span id="total">0</span> 个演示站</p>
</div>
<div class="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
<input id="search" type="text" placeholder="🔍 搜索名称/简介" class="bg-card border-0 rounded px-3 py-2 w-full sm:w-64 outline-none focus:ring-1 focus:ring-primary">
<div id="tags" class="flex flex-wrap gap-2"></div>
</div>
</div>
<div id="list" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4"></div>
</div>
<!-- 弹窗 -->
<div id="modal" class="fixed inset-0 modal-wrap hidden items-center justify-center z-50 p-4">
<div class="bg-card w-full max-w-xl rounded-lg overflow-hidden">
<div class="relative">
<img id="modal-img" class="w-full max-h-72 object-contain bg-black">
<button id="close-modal" class="absolute top-2 right-2 bg-black/50 w-7 h-7 rounded-full"><i class="fa fa-times"></i></button>
</div>
<div class="p-5">
<h2 id="modal-name" class="text-xl font-bold text-white"></h2>
<div class="flex gap-3 mt-2 text-sm">
<span id="modal-cate" class="text-primary"></span>
<span id="modal-status"></span>
</div>
<div class="my-4 text-gray-300 text-sm" id="modal-desc"></div>
<div class="mb-4">
<div class="text-xs text-gray-400 mb-1">功能列表</div>
<div id="modal-feature" class="flex flex-wrap gap-2"></div>
</div>
<div class="text-sm space-y-1 mb-5">
<div>演示账号:<span id="modal-user"></span></div>
<div>演示密码:<span id="modal-pwd"></span></div>
</div>
<div class="flex gap-3">
<a id="modal-link" target="_blank" class="bg-primary px-4 py-2 rounded text-white text-sm"><i class="fa fa-external-link"></i> 访问演示</a>
<a id="modal-git" target="_blank" class="bg-gray-700 px-4 py-2 rounded text-white text-sm"><i class="fa fa-github"></i> 源码地址</a>
</div>
</div>
</div>
</div>
<script>
let allData = [];
let activeTag = "全部";
const $ = s=>document.querySelector(s);
const $$ = s=>document.querySelectorAll(s);
const listEl = $("#list");
const searchEl = $("#search");
const tagsEl = $("#tags");
const modal = $("#modal");
// 加载数据
fetch("./sites.json")
.then(res=>res.json())
.then(data=>{
allData = data;
renderTags();
render();
});
function renderTags(){
const cates = new Set(["全部"]);
allData.forEach(i=>cates.add(i.category));
tagsEl.innerHTML = "";
[...cates].forEach(cate=>{
const b = document.createElement("button");
b.innerText = cate;
b.className = `px-2 py-1 rounded text-sm ${cate===activeTag?"bg-primary text-white":"bg-card"}`;
b.onclick = ()=>{
activeTag = cate;
renderTags();
render();
}
tagsEl.appendChild(b);
})
}
function render(){
const kw = searchEl.value.trim().toLowerCase();
const filter = allData.filter(item=>{
const mkw = item.name.toLowerCase()+item.desc.toLowerCase();
const matchKw = !kw || mkw.includes(kw);
const matchCate = activeTag==="全部" || item.category === activeTag;
return matchKw && matchCate;
})
$("#total").innerText = filter.length;
listEl.innerHTML = "";
filter.forEach(item=>{
const statusClass = item.status==="正常"?"text-ok":"text-down";
const statusIcon = item.status==="正常"?"✅":"❌";
const div = document.createElement("div");
div.className = "bg-card rounded overflow-hidden cursor-pointer hover:ring-1 hover:ring-primary transition";
div.innerHTML = `
<img class="card-img w-full lazy" data-src="${item.thumb}" src="https://placehold.co/400x220/222/777?text=无截图">
<div class="p-3">
<div class="font-medium truncate">${item.name}</div>
<div class="flex justify-between text-xs mt-1">
<span class="text-primary">${item.category}</span>
<span class="${statusClass}">${statusIcon}</span>
</div>
</div>
`
div.onclick = ()=>openModal(item);
listEl.appendChild(div);
})
lazyLoad();
}
function openModal(item){
$("#modal-img").src = item.thumb || "https://placehold.co/800/222/777?text=无截图";
$("#modal-name").innerText = item.name;
$("#modal-cate").innerText = item.category;
$("#modal-status").innerText = item.status;
$("#modal-desc").innerText = item.desc;
$("#modal-user").innerText = item.user || "-";
$("#modal-pwd").innerText = item.pwd || "-";
$("#modal-link").href = item.url;
$("#modal-git").href = item.github || "#";
$("#modal-feature").innerHTML = "";
if(item.feature && item.feature.length){
item.feature.forEach(f=>{
const span = document.createElement("span");
span.className = "bg-dark px-2 py-1 rounded text-xs";
span.innerText = f;
$("#modal-feature").appendChild(span);
})
}
modal.classList.remove("hidden");
modal.classList.add("flex");
}
function closeModal(){
modal.classList.add("hidden");
modal.classList.remove("flex");
}
$("#close-modal").onclick = closeModal;
modal.onclick = (e)=>{if(e.target===modal) closeModal()}
searchEl.oninput = render;
// 简易懒加载
function lazyLoad(){
const imgs = $$(".lazy");
const io = new IntersectionObserver((entries)=>{
entries.forEach(en=>{
if(en.isIntersecting){
const img = en.target;
img.src = img.dataset.src;
io.unobserve(img);
}
})
})
imgs.forEach(img=>io.observe(img));
}
</script>
</body>
</html>