Files
safelist-plus/index.html
Julian Freeman 8067b5f19d init 2
2025-12-21 21:38:24 -04:00

410 lines
12 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">
<head>
<base target="_top">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
/* --- 基础变量 --- */
:root {
--bg-color: #f8fafc;
--card-bg: #ffffff;
--text-main: #1e293b;
--text-muted: #64748b;
--border-color: #e2e8f0;
--accent-blue: #2563eb;
--card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--color-safe: #10b981;
--color-unsafe: #ef4444;
--color-unknown: #f59e0b;
font-size: 16px;
}
.dark-mode {
--bg-color: #0f172a;
--card-bg: #1e293b;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--border-color: #334155;
}
/* --- 全局样式 --- */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color 0.3s, color 0.3s;
}
[v-cloak] { display: none; }
.container {
max-width: 1280px;
margin: 0 auto;
padding: 2rem 1rem;
}
/* --- 头部 --- */
.header {
background: var(--card-bg);
padding: 1.5rem 2rem;
border-radius: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
border: 1px solid var(--border-color);
box-shadow: var(--card-shadow);
}
.brand h1 {
font-size: 1.5rem;
font-weight: 800;
color: var(--accent-blue);
font-style: italic;
}
.brand p {
font-size: 0.8rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.controls {
display: flex;
gap: 0.75rem;
align-items: center;
}
input[type="text"] {
padding: 0.6rem 1rem;
border-radius: 0.5rem;
border: 1px solid var(--border-color);
background: var(--bg-color);
color: var(--text-main);
font-size: 0.9rem;
width: 260px;
outline: none;
}
select {
padding: 0.6rem;
border-radius: 0.5rem;
border: 1px solid var(--border-color);
background: var(--bg-color);
color: var(--text-main);
cursor: pointer;
}
.theme-btn {
background: var(--bg-color);
border: 1px solid var(--border-color);
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
cursor: pointer;
}
/* --- 表格样式 --- */
.table-container {
background: var(--card-bg);
border-radius: 1rem;
overflow: hidden;
border: 1px solid var(--border-color);
box-shadow: var(--card-shadow);
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed; /* 固定布局确保宽度可控 */
}
th {
background: rgba(0, 0, 0, 0.02);
padding: 1rem 1.5rem;
text-align: left;
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
border-bottom: 1px solid var(--border-color);
}
td {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
font-size: 0.95rem;
}
tr:hover { background: rgba(37, 99, 235, 0.02); }
/* 项目信息列 */
.item-cell { display: flex; align-items: center; gap: 1rem; }
.icon-box {
position: relative;
width: 48px;
height: 48px;
flex-shrink: 0;
}
.icon-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.75rem;
border: 1px solid var(--border-color);
}
.status-dot {
position: absolute;
bottom: -2px;
right: -2px;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid var(--card-bg);
}
.item-desc h3 {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.15rem;
}
.item-desc p {
font-size: 0.85rem;
color: var(--text-muted);
}
/* 标签与文本 */
.type-label {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.8rem;
font-weight: 600;
border: 1px solid currentColor;
background: rgba(0,0,0,0.03);
}
.safety-text {
font-weight: 400; /* 取消加粗 */
display: flex;
align-items: center;
gap: 0.4rem;
}
.plat-tag {
font-size: 0.75rem;
background: var(--bg-color);
padding: 0.2rem 0.4rem;
border-radius: 0.3rem;
border: 1px solid var(--border-color);
color: var(--text-muted);
}
.date-val {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.85rem;
color: var(--text-muted);
}
.btn-link {
display: inline-block;
padding: 0.5rem 1rem;
background: #eff6ff;
color: var(--accent-blue);
text-decoration: none;
border-radius: 0.5rem;
font-size: 0.85rem;
font-weight: 700;
transition: 0.2s;
}
.btn-link:hover {
background: var(--accent-blue);
color: white;
}
/* 移动端隐藏部分列 */
@media (max-width: 1024px) {
th:nth-child(3), td:nth-child(3), th:nth-child(5), td:nth-child(5) { display: none; }
}
</style>
</head>
<body :class="{ 'dark-mode': isDark }">
<div id="app" v-cloak class="container">
<header class="header">
<div class="brand">
<h1>DIRECTORY</h1>
<p>{{ ui[lang].subtitle }}</p>
</div>
<div class="controls">
<input v-model="searchTerm" type="text" :placeholder="ui[lang].searchPlaceholder">
<select v-model="lang">
<option value="cn">中文</option>
<option value="en">ENGLISH</option>
<option value="es">ESPAÑOL</option>
</select>
<button @click="toggleDark" class="theme-btn">{{ isDark ? '☀️' : '🌙' }}</button>
</div>
</header>
<div class="table-container">
<table>
<thead>
<tr>
<th style="width: 35%;">{{ ui[lang].colName }}</th>
<th style="width: 12%;">{{ ui[lang].colType }}</th>
<th style="width: 15%;">{{ ui[lang].colPlat }}</th>
<th style="width: 12%;">{{ ui[lang].colSafety }}</th>
<th style="width: 13%;">{{ ui[lang].colDate }}</th>
<th style="width: 13%; text-align: right;">{{ ui[lang].colAction }}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in filteredItems" :key="item.id">
<td>
<div class="item-cell">
<div class="icon-box">
<img :src="item.icon" @error="e => e.target.src = defaultIcon" class="icon-img">
<div class="status-dot" :style="{ background: getSafetyColor(item.safety) }"></div>
</div>
<div class="item-desc">
<h3>{{ item.name }}</h3>
<p>{{ item.note }}</p>
</div>
</div>
</td>
<td>
<span class="type-label" :style="{ color: getTypeColor(item.type) }">
{{ translateStatic(item.type) }}
</span>
</td>
<td>
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
<span v-for="p in splitPlatform(item.platform)" class="plat-tag">{{ p }}</span>
</div>
</td>
<td>
<div class="safety-text" :style="{ color: getSafetyColor(item.safety) }">
● {{ translateStatic(item.safety) }}
</div>
</td>
<td>
<span class="date-val">{{ formatDate(item.date) }}</span>
</td>
<td style="text-align: right;">
<a v-if="item.url" :href="item.url" target="_blank" class="btn-link">{{ ui[lang].linkBtn }}</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted } = Vue;
createApp({
setup() {
const rawData = ref({ main: [], cn: [], en: [], es: [] });
const lang = ref('cn');
const searchTerm = ref('');
const loading = ref(true);
const isDark = ref(false);
const defaultIcon = 'https://cdn-icons-png.flaticon.com/512/684/684831.png';
const ui = {
cn: { subtitle: '全球数据资源索引', searchPlaceholder: '搜索...', colName: '项目描述', colType: '类型', colPlat: '平台', colSafety: '安全性', colDate: '最近更新', colAction: '操作', linkBtn: '访问官网' },
en: { subtitle: 'GLOBAL INDEX', searchPlaceholder: 'Search...', colName: 'Description', colType: 'Type', colPlat: 'Platform', colSafety: 'Security', colDate: 'Updated', colAction: 'Action', linkBtn: 'VISIT' },
es: { subtitle: 'ÍNDICE GLOBAL', searchPlaceholder: 'Buscar...', colName: 'Descripción', colType: 'Tipo', colPlat: 'Plataforma', colSafety: 'Seguridad', colDate: 'Fecha', colAction: 'Acción', linkBtn: 'VISITAR' }
};
const staticMap = {
'网站': { cn: '网站', en: 'Website', es: 'Sitio Web' },
'软件': { cn: '软件', en: 'Software', es: 'Software' },
'浏览器扩展': { cn: '浏览器扩展', en: 'Extension', es: 'Extensión' },
'安全': { cn: '安全', en: 'Safe', es: 'Seguro' },
'不安全': { cn: '不安全', en: 'Unsafe', es: 'Inseguro' },
'未知': { cn: '未知', en: 'Unknown', es: 'Desconocido' }
};
const fetchData = () => {
google.script.run
.withSuccessHandler(data => {
rawData.value = data;
loading.value = false;
})
.getAllData();
};
const filteredItems = computed(() => {
const main = rawData.value.main || [];
const transMap = {};
(rawData.value[lang.value] || []).forEach(row => { transMap[row["序号"]] = row; });
return main.map(row => {
const id = row["序号"];
const t = transMap[id] || {};
return {
id,
name: t[lang.value + "名字"] || row["名字"],
note: t[lang.value + "备注"] || row["备注"],
icon: row["图标链接"] || defaultIcon,
type: row["类型"],
platform: String(row["平台"] || ""),
url: row["官网链接"],
safety: row["安全性"],
date: row["更新日期"]
};
})
.filter(item => {
const s = searchTerm.value.toLowerCase();
return !s || item.name.toLowerCase().includes(s) || item.note.toLowerCase().includes(s);
})
.sort((a, b) => new Date(b.date) - new Date(a.date));
});
const translateStatic = (val) => (staticMap[val] && staticMap[val][lang.value]) || val;
const splitPlatform = (str) => str ? str.split(/[,]/).map(s => s.trim()) : [];
const getSafetyColor = (val) => {
if (val === '安全') return '#10b981';
if (val === '不安全') return '#ef4444';
return '#f59e0b';
};
const getTypeColor = (type) => {
if (type === '网站') return '#06b6d4';
if (type === '软件') return '#6366f1';
return '#f59e0b';
};
const formatDate = (dateStr) => {
if (!dateStr) return '--';
const d = new Date(dateStr);
return d.toISOString().split('T')[0].replace(/-/g, '/');
};
const toggleDark = () => isDark.value = !isDark.value;
onMounted(fetchData);
return { lang, searchTerm, loading, isDark, filteredItems, ui, defaultIcon, toggleDark, translateStatic, getSafetyColor, getTypeColor, formatDate, splitPlatform };
}
}).mount('#app');
</script>
</body>
</html>