Files
safelist-plus/index.html
Julian Freeman 7d27fd2814 更改下拉框
2025-12-21 22:27:58 -04:00

372 lines
14 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.prod.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);
--color-safe: #10b981;
--color-unsafe: #ef4444;
--color-unknown: #f59e0b;
font-size: 16px;
}
/* --- 暗黑模式变量 --- */
body.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"], .custom-select {
padding: 0.6rem 1rem;
border-radius: 0.75rem;
border: 1px solid var(--border-color);
background: var(--bg-color);
color: var(--text-main);
font-size: 0.95rem;
outline: none;
transition: all 0.2s;
}
input[type="text"] { width: 260px; }
input[type="text"]:focus, .custom-select:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* 自定义下拉框样式 */
.custom-select {
appearance: none;
-webkit-appearance: none;
padding-right: 2.5rem; /* 为箭头留位 */
cursor: pointer;
font-weight: 600;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.8rem center;
background-size: 1rem;
}
body.dark-mode .custom-select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
}
.theme-btn {
background: var(--bg-color);
border: 1px solid var(--border-color);
padding: 0.5rem 0.75rem;
border-radius: 0.75rem;
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
}
/* --- 表格样式 --- */
.table-container {
background: var(--card-bg);
border-radius: 1rem;
overflow: hidden;
border: 1px solid var(--border-color);
box-shadow: var(--card-shadow);
margin-bottom: 1.5rem;
}
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; }
tr:hover { background: rgba(37, 99, 235, 0.02); }
.item-cell { display: flex; align-items: center; gap: 1rem; }
.icon-box { position: relative; width: 52px; height: 52px; flex-shrink: 0; }
.icon-img { width: 100%; height: 100%; object-fit: cover; border-radius: 0.85rem; border: 1px solid var(--border-color); }
.status-dot { position: absolute; bottom: -2px; right: -2px; width: 13px; height: 13px; 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); margin-right: 4px; }
.date-val { font-family: 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; }
/* --- 分页 --- */
.pagination { display: flex; justify-content: center; align-items: center; gap: 1.5rem; padding: 1rem 0; }
.page-info { font-size: 0.9rem; color: var(--text-muted); font-weight: 600; }
.page-btn {
padding: 0.5rem 1.25rem; background: var(--card-bg); border: 1px solid var(--border-color);
border-radius: 0.5rem; cursor: pointer; color: var(--text-main); font-weight: 600; transition: 0.2s;
}
.page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.page-btn:hover:not(:disabled) { border-color: var(--accent-blue); color: var(--accent-blue); }
@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>
<div id="app" v-cloak class="container">
<header class="header">
<div class="brand">
<h1>SECURITY</h1>
<p>{{ ui[lang].subtitle }}</p>
</div>
<div class="controls">
<input v-model="searchTerm" type="text" :placeholder="ui[lang].searchPlaceholder">
<select v-model="lang" class="custom-select">
<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 paginatedItems" :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 v-if="loading" style="padding: 5rem; text-align: center; color: var(--text-muted);">{{ ui[lang].syncing }}</div>
<div v-else-if="filteredItems.length === 0" style="padding: 5rem; text-align: center; color: var(--text-muted);">No results.</div>
</div>
<div class="pagination" v-if="totalPages > 1">
<button class="page-btn" @click="currentPage--" :disabled="currentPage === 1">{{ ui[lang].prev }}</button>
<div class="page-info">{{ ui[lang].page }} {{ currentPage }} / {{ totalPages }}</div>
<button class="page-btn" @click="currentPage++" :disabled="currentPage === totalPages">{{ ui[lang].next }}</button>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted, watch } = 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 currentPage = ref(1);
const pageSize = 20;
const defaultIcon = 'https://cdn-icons-png.flaticon.com/512/684/684831.png';
const ui = {
cn: { subtitle: '安全软件列表Plus', searchPlaceholder: '搜索...', colName: '项目描述', colType: '类型', colPlat: '平台', colSafety: '安全性', colDate: '最近更新', colAction: '操作', linkBtn: '访问官网', syncing: '同步云端数据中...', prev: '上一页', next: '下一页', page: '页码' },
en: { subtitle: 'Security Software List', searchPlaceholder: 'Search...', colName: 'Description', colType: 'Type', colPlat: 'Platform', colSafety: 'Security', colDate: 'Updated', colAction: 'Action', linkBtn: 'VISIT', syncing: 'Syncing...', prev: 'Prev', next: 'Next', page: 'Page' },
es: { subtitle: 'Lista de software de seguridad', searchPlaceholder: 'Buscar...', colName: 'Descripción', colType: 'Tipo', colPlat: 'Plataforma', colSafety: 'Seguridad', colDate: 'Fecha', colAction: 'Acción', linkBtn: 'VISITAR', syncing: 'Sincronizando...', prev: 'Anterior', next: 'Siguiente', page: 'Página' }
};
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) || (item.url && item.url.toLowerCase().includes(s));
})
.sort((a, b) => new Date(b.date) - new Date(a.date));
});
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize;
const end = start + pageSize;
return filteredItems.value.slice(start, end);
});
const totalPages = computed(() => Math.ceil(filteredItems.value.length / pageSize));
// 修复:移除 lang 监听,切换语言不再重置页码
watch(searchTerm, () => { currentPage.value = 1; });
const translateStatic = (val) => (staticMap[val] && staticMap[val][lang.value]) || val;
const splitPlatform = (str) => str ? str.split(/[,]/).map(s => s.trim()) : [];
const getSafetyColor = (val) => val === '安全' ? '#10b981' : (val === '不安全' ? '#ef4444' : '#f59e0b');
const getTypeColor = (type) => type === '网站' ? '#06b6d4' : (type === '软件' ? '#6366f1' : '#f59e0b');
const formatDate = (dateStr) => dateStr ? new Date(dateStr).toISOString().split('T')[0].replace(/-/g, '/') : '--';
const toggleDark = () => {
isDark.value = !isDark.value;
document.body.classList.toggle('dark-mode', isDark.value);
};
onMounted(fetchData);
return { lang, searchTerm, loading, isDark, filteredItems, paginatedItems, totalPages, currentPage, ui, defaultIcon, toggleDark, translateStatic, getSafetyColor, getTypeColor, formatDate, splitPlatform };
}
}).mount('#app');
</script>
</body>
</html>