Files
safelist-plus/index.html
Julian Freeman 4fd2fd5e91 init 1
2025-12-21 21:34:56 -04:00

434 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;
--accent-hover: #1d4ed8;
--header-shadow: 0 1px 3px rgba(0,0,0,0.1);
--card-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
/* 状态颜色 */
--color-safe: #10b981;
--color-unsafe: #ef4444;
--color-unknown: #f59e0b;
--type-web: #06b6d4;
--type-soft: #6366f1;
--type-ext: #f59e0b;
font-size: 16px; /* 基础字号 */
}
/* --- 暗黑模式变量 --- */
.dark-mode {
--bg-color: #0f172a;
--card-bg: #1e293b;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--border-color: #334155;
--header-shadow: 0 4px 6px rgba(0,0,0,0.3);
--card-shadow: none;
}
/* --- 全局样式 --- */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
line-height: 1.5;
/* 字体平滑优化 */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color 0.3s, color 0.3s;
}
[v-cloak] { display: none; }
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
/* --- 头部设计 --- */
.header {
background: var(--card-bg);
padding: 1.5rem 2rem;
border-radius: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
border: 1px solid var(--border-color);
box-shadow: var(--header-shadow);
}
.brand h1 {
font-size: 1.75rem;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--accent-blue);
font-style: italic;
}
.brand p {
font-size: 0.85rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-top: 0.25rem;
}
.controls {
display: flex;
gap: 1rem;
align-items: center;
}
input[type="text"] {
padding: 0.75rem 1.25rem;
border-radius: 0.75rem;
border: 1px solid var(--border-color);
background: var(--bg-color);
color: var(--text-main);
font-size: 1rem;
width: 300px;
outline: none;
}
input[type="text"]:focus { border-color: var(--accent-blue); }
select {
padding: 0.75rem;
border-radius: 0.75rem;
border: 1px solid var(--border-color);
background: var(--bg-color);
color: var(--text-main);
cursor: pointer;
font-weight: 600;
}
.theme-btn {
background: var(--bg-color);
border: 1px solid var(--border-color);
padding: 0.75rem;
border-radius: 0.75rem;
cursor: pointer;
font-size: 1.2rem;
}
/* --- 表格设计 --- */
.table-card {
background: var(--card-bg);
border-radius: 1.5rem;
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: 1.25rem 1.5rem;
text-align: left;
font-size: 0.9rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--border-color);
}
td {
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
}
tr:last-child td { border-bottom: none; }
tr:hover { background: rgba(37, 99, 235, 0.03); }
/* --- 项目信息单元格 --- */
.item-info { display: flex; align-items: center; gap: 1.25rem; }
.icon-wrapper {
position: relative;
width: 56px;
height: 56px;
flex-shrink: 0;
}
.item-icon {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 1rem;
background: #eee;
border: 1px solid var(--border-color);
}
.safety-dot {
position: absolute;
bottom: -2px;
right: -2px;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid var(--card-bg);
}
.item-text h3 {
font-size: 1.2rem; /* 调大项目名 */
font-weight: 700;
margin-bottom: 0.25rem;
}
.item-text p {
font-size: 1rem; /* 调大备注 */
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* --- 标签样式 --- */
.badge {
display: inline-block;
padding: 0.4rem 0.8rem;
border-radius: 0.5rem;
font-size: 0.85rem;
font-weight: 700;
border: 1px solid transparent;
white-space: nowrap;
}
.type-badge.网站 { color: var(--type-web); border-color: var(--type-web); background: rgba(6, 182, 212, 0.1); }
.type-badge.软件 { color: var(--type-soft); border-color: var(--type-soft); background: rgba(99, 102, 241, 0.1); }
.type-badge.浏览器扩展 { color: var(--type-ext); border-color: var(--type-ext); background: rgba(245, 158, 11, 0.1); }
.safety-text {
font-size: 0.95rem;
font-weight: 800;
display: flex;
align-items: center;
gap: 0.5rem;
}
.platform-tag {
font-size: 0.75rem;
background: var(--bg-color);
padding: 0.25rem 0.5rem;
border-radius: 0.4rem;
border: 1px solid var(--border-color);
margin-right: 0.25rem;
}
.visit-btn {
display: inline-block;
padding: 0.6rem 1.25rem;
background: rgba(37, 99, 235, 0.1);
color: var(--accent-blue);
text-decoration: none;
border-radius: 0.75rem;
font-size: 0.9rem;
font-weight: 700;
transition: all 0.2s;
}
.visit-btn:hover {
background: var(--accent-blue);
color: white;
}
.date-text { font-family: monospace; font-size: 0.9rem; color: var(--text-muted); }
/* 响应式调整 */
@media (max-width: 768px) {
.header { flex-direction: column; }
input[type="text"] { width: 100%; }
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-card">
<table>
<thead>
<tr>
<th style="width: 40%;">{{ ui[lang].colName }}</th>
<th style="width: 15%;">{{ ui[lang].colType }}</th>
<th style="width: 15%;">Platforms</th>
<th style="width: 12%;">{{ ui[lang].colSafety }}</th>
<th style="width: 18%; text-align: right;">Action</th>
</tr>
</thead>
<tbody>
<tr v-for="item in filteredItems" :key="item.id">
<td>
<div class="item-info">
<div class="icon-wrapper">
<img :src="item.icon" @error="e => e.target.src = defaultIcon" class="item-icon">
<div class="safety-dot" :style="{ background: getSafetyColor(item.safety) }"></div>
</div>
<div class="item-text">
<h3>{{ item.name }}</h3>
<p>{{ item.note }}</p>
</div>
</div>
</td>
<td>
<span class="badge type-badge" :class="item.type">
{{ translateStatic(item.type) }}
</span>
</td>
<td>
<span v-for="p in splitPlatform(item.platform)" class="platform-tag">{{ p }}</span>
</td>
<td>
<div class="safety-text" :style="{ color: getSafetyColor(item.safety) }">
● {{ translateStatic(item.safety) }}
</div>
</td>
<td style="text-align: right;">
<div class="date-text" style="margin-bottom: 0.5rem;">{{ formatDate(item.date) }}</div>
<a v-if="item.url" :href="item.url" target="_blank" class="visit-btn">{{ ui[lang].linkBtn }}</a>
</td>
</tr>
</tbody>
</table>
<div v-if="loading" style="padding: 5rem; text-align: center; color: var(--text-muted);">
同步云端数据中...
</div>
</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: '类型', colSafety: '安全性', linkBtn: '访问官网' },
en: { subtitle: 'GLOBAL RESOURCE INDEX', searchPlaceholder: 'Search...', colName: 'Description', colType: 'Type', colSafety: 'Security', linkBtn: 'VISIT' },
es: { subtitle: 'ÍNDICE DE RECURSOS', searchPlaceholder: 'Buscar...', colName: 'Descripción', colType: 'Tipo', colSafety: 'Seguridad', 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 formatDate = (dateStr) => {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleDateString(lang.value === 'cn' ? 'zh-CN' : 'en-US');
};
const toggleDark = () => isDark.value = !isDark.value;
onMounted(fetchData);
return { lang, searchTerm, loading, isDark, filteredItems, ui, defaultIcon, toggleDark, translateStatic, getSafetyColor, formatDate, splitPlatform };
}
}).mount('#app');
</script>
</body>
</html>