Files
portal.tootaio.com/index.html
xiaomai 6cbcbb1cbb feat(ui): introduce games-first landing page design
This commit completely redesigns the landing page to prioritize games over tools, reflecting a strategic shift in focus. The previous generic list has been replaced with a modern, two-column layout.

- A new 'Games' section displays projects as large, visual cards with thumbnails.
- A secondary 'Tools' section lists other services in a compact format.
- The CSS has been entirely rewritten for a new visual identity, improved responsiveness, and better animations.
- JavaScript logic is updated to categorize services into 'games' or 'tools' and render them accordingly.
2025-09-11 15:52:10 +08:00

618 lines
17 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Tootaio — Games First</title>
<meta name="description" content="Tootaio — Game studio first, tools second." />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&family=Playfair+Display:wght@600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--bg: #07060a;
--card: #131118;
--card-hover: #1a1721;
--muted: #bdb8c4;
--text: #f5f3f7;
--accent: #ffbf58;
--accent-2: #ff7b7b;
--accent-gradient: linear-gradient(135deg, var(--accent), var(--accent-2));
--gap: 24px;
--radius: 16px;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
--transition: all 0.3s ease;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(180deg, #050406 0%, #0b0710 100%);
color: var(--text);
font-family: Inter, system-ui, Segoe UI, Roboto, sans-serif;
padding: 32px;
line-height: 1.6;
}
.wrap {
max-width: 1280px;
margin: 0 auto;
}
/* Header Styles */
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 40px;
}
.logo {
display: flex;
gap: 16px;
align-items: center;
transition: var(--transition);
}
.logo:hover {
transform: translateY(-2px);
}
.logo-mark {
width: 64px;
height: 64px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-family: Playfair Display, serif;
background: var(--accent-gradient);
color: #251200;
box-shadow: 0 8px 30px rgba(255, 150, 0, 0.2);
font-size: 22px;
}
h1 {
font-family: Playfair Display, serif;
margin: 0;
font-size: 36px;
font-weight: 800;
letter-spacing: -0.5px;
background: linear-gradient(to right, #ffbf58, #ff7b7b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: var(--muted);
font-size: 14px;
margin-top: 8px;
font-weight: 400;
}
.top-controls {
display: flex;
gap: 12px;
align-items: center;
}
.search {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
padding: 12px 16px;
border-radius: 12px;
display: flex;
gap: 10px;
align-items: center;
transition: var(--transition);
}
.search:focus-within {
border-color: rgba(255, 191, 88, 0.3);
box-shadow: 0 0 0 3px rgba(255, 191, 88, 0.1);
}
.search input {
background: transparent;
border: 0;
outline: none;
color: inherit;
width: 240px;
font-size: 14px;
}
.search input::placeholder {
color: var(--muted);
}
.btn {
padding: 12px 18px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
color: var(--muted);
cursor: pointer;
transition: var(--transition);
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12);
}
/* Main Content */
.main {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--gap);
margin-top: 32px;
}
.section-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 10px;
}
.section-title i {
color: var(--accent);
font-size: 18px;
}
.games, .tools {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent);
padding: 24px;
border-radius: var(--radius);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: var(--shadow);
transition: var(--transition);
}
.games:hover, .tools:hover {
border-color: rgba(255, 255, 255, 0.08);
}
.games-header, .tools-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.count {
font-size: 13px;
color: var(--muted);
background: rgba(255, 255, 255, 0.03);
padding: 4px 10px;
border-radius: 20px;
}
/* Games Grid */
.games-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.game-card {
position: relative;
border-radius: 14px;
overflow: hidden;
cursor: pointer;
min-height: 180px;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 16px;
background-size: cover;
background-position: center;
border: 1px solid rgba(0, 0, 0, 0.3);
transition: var(--transition);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
.game-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, transparent 70%);
opacity: 0.8;
transition: var(--transition);
}
.game-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
}
.game-card:hover::before {
opacity: 0.9;
}
.game-card .meta {
position: relative;
z-index: 2;
backdrop-filter: blur(6px);
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.6));
padding: 12px;
border-radius: 10px;
transition: var(--transition);
}
.game-card:hover .meta {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.7));
}
.game-title {
font-weight: 700;
font-size: 16px;
margin-bottom: 4px;
}
.game-sub {
font-size: 13px;
color: var(--muted);
line-height: 1.4;
}
/* Tools List */
.tools-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.tool-item {
display: flex;
gap: 16px;
align-items: center;
padding: 16px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.05);
background: rgba(255, 255, 255, 0.02);
cursor: pointer;
transition: var(--transition);
}
.tool-item:hover {
transform: translateX(5px);
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.08);
}
.tool-icon {
width: 52px;
height: 52px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
background: var(--accent-gradient);
color: #251200;
font-size: 18px;
flex-shrink: 0;
}
.tool-content {
flex: 1;
}
.tool-title {
font-weight: 700;
font-size: 16px;
margin-bottom: 4px;
}
.tool-desc {
font-size: 13px;
color: var(--muted);
line-height: 1.4;
}
/* Footer */
footer {
margin-top: 40px;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--muted);
font-size: 14px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.footer-brand {
font-weight: 600;
}
.footer-tagline {
display: flex;
align-items: center;
gap: 8px;
}
.footer-tagline i {
color: var(--accent);
font-size: 12px;
}
/* Responsive */
@media (max-width: 1024px) {
.main {
grid-template-columns: 1fr;
}
.games-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
body {
padding: 20px;
}
header {
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.top-controls {
width: 100%;
justify-content: space-between;
}
.search input {
width: 100%;
}
.games-grid {
grid-template-columns: 1fr;
}
footer {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
}
@media (max-width: 480px) {
.logo-mark {
width: 52px;
height: 52px;
font-size: 18px;
}
h1 {
font-size: 28px;
}
.btn {
padding: 10px 14px;
}
}
/* Animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.game-card, .tool-item {
animation: fadeIn 0.5s ease-out;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="logo">
<div class="logo-mark">T</div>
<div>
<h1>Tootaio</h1>
<div class="subtitle">Game Studio · Tools as a service</div>
</div>
</div>
<div class="top-controls">
<div class="search">
<i class="fas fa-search"></i>
<input id="q" placeholder="查找游戏或工具(回车)" />
</div>
<button id="langBtn" class="btn">
<i class="fas fa-globe"></i>
<span>中文</span>
</button>
</div>
</header>
<main class="main">
<section class="games" aria-label="Games">
<div class="games-header">
<div class="section-title">
<i class="fas fa-gamepad"></i>
<span>Games</span>
</div>
<div class="count" id="gamesCount"></div>
</div>
<div class="games-grid" id="gamesGrid"></div>
</section>
<aside class="tools" aria-label="Tools">
<div class="tools-header">
<div class="section-title">
<i class="fas fa-tools"></i>
<span>Tools</span>
</div>
<div class="count" id="toolsCount"></div>
</div>
<div class="tools-list" id="toolsList"></div>
</aside>
</main>
<footer>
<div class="footer-brand">Tootaio — <span id="year"></span></div>
<div class="footer-tagline">
<i class="fas fa-heart"></i>
<span>Focused on Games • Tools for ops & creators</span>
</div>
</footer>
</div>
<script>
// Minimal default config (falls back if siteConfig.json missing)
const defaultSiteConfig = {
services: [
{ name: "supergame.tootaio.com", url: "https://supergame.tootaio.com", description_en: "Our flagship game demo.", description_zh: "我们的旗舰游戏演示。", category: "game", thumbnail: "" },
{ name: "git.tootaio.com", url: "https://git.tootaio.com", description_en: "Gitea self-host.", description_zh: "代码托管服务。", category: "tool" },
{ name: "memos.tootaio.com", url: "https://memos.tootaio.com", description_en: "Notes", description_zh: "笔记空间", category: "tool" },
{ name: "life-restart.tootaio.com", url: "https://life-restart.tootaio.com", description_en: "Mirrored game.", description_zh: "游戏镜像", category: "game" }
]
};
async function loadConfig(){
try{
const res = await fetch('./siteConfig.json', {cache:'no-store'});
if(!res.ok) throw new Error('no siteConfig.json');
const cfg = await res.json();
return cfg;
}catch(e){
console.warn('siteConfig.json not found, using default config.');
return defaultSiteConfig;
}
}
function guessCategory(s){
// if not present, try to guess from name/description
if(s.category) return s.category;
const name = (s.name || '').toLowerCase();
const desc = (s.description_en||'' + s.description_zh||'').toLowerCase();
if(/game|play|demo|unity|godot|itch|gamepad|pixel/.test(name+desc)) return 'game';
return 'tool';
}
function makeThumbPlaceholder(name, size=320){
// generate simple SVG data URI with initials
const initials = (name || '').split(/[.\\-\\s]/).slice(0,2).map(x=>x[0]?.toUpperCase()||'').join('');
const bg = ['#d97706','#ef4444','#7c3aed','#06b6d4','#f59e0b'][Math.abs(name.length)%5];
const fg = '#fff';
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${size}' height='${Math.round(size*0.6)}'><rect width='100%' height='100%' fill='${bg}' rx='8'/><text x='50%' y='55%' font-size='36' dominant-baseline='middle' text-anchor='middle' fill='${fg}' font-family='Inter,system-ui'>${initials}</text></svg>`;
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
}
function render(cfg, lang='zh'){
const services = (cfg.services || []).map(s => ({...s, category: guessCategory(s)}));
const games = services.filter(s => s.category === 'game');
const tools = services.filter(s => s.category !== 'game');
const gamesGrid = document.getElementById('gamesGrid');
const toolsList = document.getElementById('toolsList');
gamesGrid.innerHTML = '';
toolsList.innerHTML = '';
games.forEach(s => {
const thumb = s.thumbnail || makeThumbPlaceholder(s.name);
const a = document.createElement('a');
a.className = 'game-card';
a.href = s.url;
a.target = '_blank';
a.rel = 'noreferrer';
a.style.backgroundImage = `url('${thumb}')`;
const meta = document.createElement('div'); meta.className = 'meta';
const title = document.createElement('div'); title.className = 'game-title'; title.textContent = s.name;
const sub = document.createElement('div'); sub.className = 'game-sub'; sub.textContent = lang === 'zh' ? (s.description_zh || s.description_en || '') : (s.description_en || s.description_zh || '');
meta.appendChild(title); meta.appendChild(sub);
a.appendChild(meta);
gamesGrid.appendChild(a);
});
tools.forEach(s => {
const icon = document.createElement('div'); icon.className = 'tool-icon';
icon.textContent = (s.name || '').slice(0,2).toUpperCase();
const toolContent = document.createElement('div'); toolContent.className = 'tool-content';
const tTitle = document.createElement('div'); tTitle.className = 'tool-title'; tTitle.textContent = s.name;
const tSub = document.createElement('div'); tSub.className = 'tool-desc'; tSub.textContent = lang === 'zh' ? (s.description_zh || '') : (s.description_en || '');
toolContent.appendChild(tTitle); toolContent.appendChild(tSub);
const a = document.createElement('a'); a.className = 'tool-item'; a.href = s.url; a.target='_blank'; a.rel='noreferrer';
a.appendChild(icon); a.appendChild(toolContent);
toolsList.appendChild(a);
});
document.getElementById('gamesCount').textContent = games.length + ' 项目';
document.getElementById('toolsCount').textContent = tools.length + ' 项目';
}
(async function init(){
const cfg = await loadConfig();
let lang = 'zh';
render(cfg, lang);
document.getElementById('year').textContent = new Date().getFullYear();
// search
const q = document.getElementById('q');
q.addEventListener('keydown',(e)=>{
if(e.key === 'Enter'){
const term = q.value.trim().toLowerCase();
if(!term){ render(cfg, lang); return; }
const filtered = (cfg.services||[]).filter(s => {
const txt = (s.name + ' ' + (s.description_en||'') + ' ' + (s.description_zh||'')).toLowerCase();
return txt.includes(term);
});
render({services:filtered}, lang);
}
});
// keyboard shortcut to focus search
window.addEventListener('keydown',(e)=>{ if(e.key === '/' && document.activeElement !== q){ e.preventDefault(); q.focus(); }});
// language toggle
document.getElementById('langBtn').addEventListener('click', ()=>{
lang = (lang === 'zh') ? 'en' : 'zh';
document.getElementById('langBtn').querySelector('span').textContent = (lang === 'zh') ? '中文' : 'EN';
render(cfg, lang);
});
})();
</script>
</body>
</html>