<edit>changed structure: deleted edited func from website, added json file reader. added domains.

This commit is contained in:
2025-08-30 16:46:12 +08:00
parent 9e957a57bd
commit c75ea7a771
2 changed files with 199 additions and 217 deletions

134
domains.json Normal file
View File

@@ -0,0 +1,134 @@
[
{
"domainFull": "qsj.net",
"registrar": "spaceship",
"registerDate": "2024-03-27",
"expireDate": "2027-03-27",
"registerPrice": 80,
"renewPrice": 80,
"targetPrice": null,
"actualPrice": 0,
"notes": "全世界Internet"
},
{
"domainFull": "mqwq.com",
"registrar": "spaceship",
"registerDate": "2024-11-11",
"expireDate": "2027-11-11",
"registerPrice": 72,
"renewPrice": 72,
"targetPrice": null,
"actualPrice": 0,
"notes": "qwq"
},
{
"domainFull": "loohui.com",
"registrar": "spaceship",
"registerDate": "2025-07-01",
"expireDate": "2035-07-01",
"registerPrice": 72,
"renewPrice": 72,
"targetPrice": null,
"actualPrice": 0,
"notes": "Blog domain"
},
{
"domainFull": "learnchive.com",
"registrar": "spaceship",
"registerDate": "2025-07-21",
"expireDate": "2035-07-21",
"registerPrice": 72,
"renewPrice": 72,
"targetPrice": null,
"actualPrice": 0,
"notes": "项目域名"
},
{
"domainFull": "662612.xyz",
"registrar": "spaceship",
"registerDate": "2025-07-01",
"expireDate": "2035-07-01",
"registerPrice": 72,
"renewPrice": 4,
"targetPrice": null,
"actualPrice": 0,
"notes": ""
},
{
"domainFull": "310502.xyz",
"registrar": "spaceship",
"registerDate": "2025-07-01",
"expireDate": "2035-07-01",
"registerPrice": 72,
"renewPrice": 4,
"targetPrice": null,
"actualPrice": 0,
"notes": ""
},
{
"domainFull": "345674.xyz",
"registrar": "spaceship",
"registerDate": "2025-07-01",
"expireDate": "2035-07-01",
"registerPrice": 72,
"renewPrice": 4,
"targetPrice": null,
"actualPrice": 0,
"notes": "34567"
},
{
"domainFull": "4555556.xyz",
"registrar": "spaceship",
"registerDate": "2025-07-01",
"expireDate": "2035-07-01",
"registerPrice": 72,
"renewPrice": 4,
"targetPrice": null,
"actualPrice": 0,
"notes": "456, 5个5"
},
{
"domainFull": "kk.al",
"registrar": "quyu",
"registerDate": "2019-12-16",
"expireDate": "2026-01-12",
"registerPrice": 72,
"renewPrice": 99,
"targetPrice": null,
"actualPrice": 0,
"notes": "short domain"
},
{
"domainFull": "ez.gs",
"registrar": "西部数码国际",
"registerDate": "2016-05-10",
"expireDate": "2026-05-10",
"registerPrice": 72,
"renewPrice": 68,
"targetPrice": null,
"actualPrice": 0,
"notes": "short domain"
},
{
"domainFull": "studyhub.cn",
"registrar": "aliyun",
"registerDate": "2025-08-09",
"expireDate": "2035-08-09",
"registerPrice": 39,
"renewPrice": 39,
"targetPrice": null,
"actualPrice": 0,
"notes": "项目域名"
},
{
"domainFull": "eic.cc",
"registrar": "aliyun",
"registerDate": "2014-08-01",
"expireDate": "2026-08-01",
"registerPrice": 72,
"renewPrice": 75,
"targetPrice": null,
"actualPrice": 0,
"notes": "electronic Id"
}
]

View File

@@ -37,11 +37,6 @@
.s-active{background:#ecfdf5;color:#065f46}
.s-expiring{background:#fffbeb;color:#92400e}
.s-expired{background:#fff1f2;color:#7f1d1d}
.modal-content{border-radius:12px;border:1px solid var(--border)}
.form-label{font-weight:400;font-size:13px}
.input-inline{display:flex;gap:8px}
.input-inline .form-control{flex:1}
.small-muted{font-size:11px;color:var(--muted)}
footer{margin-top:2rem;text-align:center;font-size:12px;color:var(--muted)}
footer a{color:var(--muted);text-decoration:none}
nav[aria-label="域名分页"],
@@ -53,10 +48,7 @@
<nav class="navbar navbar-expand-lg">
<div class="container-fluid px-3">
<a class="navbar-brand brand" href="#"><i class="bi bi-globe2"></i> 全世界米米表</a>
<div class="ms-auto d-flex align-items-center gap-2">
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addDomainModal"><i class="bi bi-plus-lg"></i> 添加域名</button>
</div>
</div>
</nav>
<main class="container-main">
@@ -91,19 +83,13 @@
<option value="100">100 条/页</option>
</select>
</div>
<div class="control">
<button class="btn btn-outline-secondary" id="exportBtn"><i class="bi bi-download"></i> 导出CSV</button>
</div>
<div class="control">
<button class="btn btn-outline-secondary" id="importBtn"><i class="bi bi-upload"></i> 导入</button>
</div>
</div>
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead><tr><th>域名</th><th>注册商</th><th>注册价</th><th>续费价</th><th>意向售价</th><th>实际售价</th><th>到期时间</th><th>剩余天数</th><th>状态</th><th>操作</th></tr></thead>
<thead><tr><th>域名</th><th>注册商</th><th>注册价</th><th>续费价</th><th>意向售价</th><th>实际售价</th><th>到期时间</th><th>剩余天数</th><th>状态</th></tr></thead>
<tbody id="domainTableBody"></tbody>
</table>
</div>
@@ -117,107 +103,75 @@
<footer>
Copyright © 2025 <a href="https://qsj.net" target="_blank" rel="noopener">qsj.net</a> All Rights Reserved.
<p style="margin-top: 12px;">
</footer>
<div class="modal fade" id="addDomainModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header"><h5 class="modal-title" id="modalTitle">添加新域名</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<form id="domainForm"><input type="hidden" name="editIndex" value="-1">
<div class="row gy-3">
<div class="col-md-6"><label class="form-label">域名(含后缀)</label><input name="domainFull" class="form-control" placeholder="example.com" required></div>
<div class="col-md-6"><label class="form-label">注册商</label><input name="registrar" class="form-control" list="registrarsList" placeholder="可直接填写"></div>
<datalist id="registrarsList">
<option>阿里云</option><option>腾讯云</option><option>百度云</option><option>华为云</option>
<option>西部数码</option><option>西部数码国际</option><option>新网</option><option>万网</option><option>Namecheap</option>
<option>GoDaddy</option><option>Name.com</option><option>Google Domains</option>
<option>Cloudflare</option><option>Dynadot</option><option>Porkbun</option><option>Spaceship</option>
</datalist>
<div class="col-md-6"><label class="form-label">注册时间</label><input type="date" name="registerDate" class="form-control"></div>
<div class="col-md-6"><label class="form-label">到期时间</label><input type="date" name="expireDate" class="form-control" required></div>
<div class="col-md-6"><label class="form-label">注册价格 (¥)</label><input type="number" step="0.01" min="0" name="registerPrice" class="form-control"></div>
<div class="col-md-6"><label class="form-label">续费价格 (¥)</label><input type="number" step="0.01" min="0" name="renewPrice" class="form-control"></div>
<div class="col-md-6"><label class="form-label">意向售价 (¥)</label><input type="number" step="0.01" min="0" name="targetPrice" class="form-control"></div>
<div class="col-md-6"><label class="form-label">实际售价 (¥)</label><input type="number" step="0.01" min="0" name="actualPrice" class="form-control"></div>
<div class="col-12"><label class="form-label">备注</label><textarea name="notes" rows="3" class="form-control"></textarea></div>
</div>
</form>
</div>
<div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button class="btn btn-primary" onclick="saveDomain()">保存</button></div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
/* ========== 本地存储 key ========== */
const STORAGE_KEY = 'domainPanel_domains';
/* ========== 分页相关 ========== */
// 全局变量
let allDomains = []; // 存储从JSON加载的所有域名
let currentPage = 1;
let pageSize = 10;
/* ========== 数据读写 ========== */
function loadDomains() {
/* ========== 页面加载时,从 JSON 文件获取数据 ========== */
document.addEventListener('DOMContentLoaded', async () => {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch {
return [];
// 核心改动:从 domains.json 文件加载数据
const response = await fetch('domains.json?v=' + Date.now()); // 添加时间戳防止缓存
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
allDomains = await response.json();
// 数据加载成功后,渲染表格和统计信息
renderTable();
updateStats();
} catch (error) {
console.error("无法加载或解析 domains.json:", error);
const tbody = document.getElementById('domainTableBody');
tbody.innerHTML = `<tr><td colspan="9" class="text-center text-danger">数据文件 (domains.json) 加载失败!</td></tr>`;
}
}
function saveDomains() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(domains));
}
});
/* ========== 全局变量:从本地加载 ========== */
let domains = loadDomains();
/* ========== 首次初始化:如果本地为空,写入默认数据 ========== */
if (!domains.length) {
domains = [
];
saveDomains();
}
/* ========== 工具函数 ========== */
/* ========== 工具函数:计算剩余天数 ========== */
function daysRemaining(d) {
const today = new Date(), exp = new Date(d + 'T23:59:59');
if (!d) return 0;
const today = new Date();
today.setHours(0, 0, 0, 0); // 标准化到当天的开始
const exp = new Date(d);
return Math.ceil((exp - today) / (1000 * 60 * 60 * 24));
}
/* ========== 渲染表格(含分页) ========== */
function renderTable(list = domains) {
/* ========== 渲染表格(含过滤和分页) ========== */
function renderTable() {
const q = document.getElementById('searchInput').value.trim().toLowerCase();
const status = document.getElementById('filterStatus').value;
let filtered = list.filter(d => {
const full = (d.domainFull || '').toLowerCase();
const reg = (d.registrar || '').toLowerCase();
if (q && !(full.includes(q) || reg.includes(q))) return false;
if (status === 'all') return true;
const statusFilter = document.getElementById('filterStatus').value;
const filtered = allDomains.filter(d => {
const searchMatch = q ? (d.domainFull || '').toLowerCase().includes(q) || (d.registrar || '').toLowerCase().includes(q) : true;
if (!searchMatch) return false;
if (statusFilter === 'all') return true;
const days = daysRemaining(d.expireDate);
if (status === 'active') return days > 30;
if (status === 'expiring') return days > 0 && days <= 30;
if (status === 'expired') return days <= 0;
if (statusFilter === 'active') return days > 30;
if (statusFilter === 'expiring') return days > 0 && days <= 30;
if (statusFilter === 'expired') return days <= 0;
return true;
});
const total = filtered.length;
const totalPages = Math.ceil(total / pageSize);
if (currentPage > totalPages && totalPages !== 0) currentPage = totalPages;
const totalPages = Math.ceil(filtered.length / pageSize);
if (currentPage > totalPages && totalPages > 0) currentPage = totalPages;
const start = (currentPage - 1) * pageSize;
const pageData = filtered.slice(start, start + pageSize);
const tbody = document.getElementById('domainTableBody');
tbody.innerHTML = '';
pageData.forEach((d, idx) => {
pageData.forEach(d => {
const days = daysRemaining(d.expireDate);
let statusText = '正常', cls = 's-active';
if (days <= 0) { statusText = '已过期'; cls = 's-expired'; }
else if (days <= 30) { statusText = '即将到期'; cls = 's-expiring'; }
const globalIndex = start + idx;
if (days <= 0) { statusText = '已过期'; cls = 's-expired'; }
else if (days <= 30) { statusText = '即将到期'; cls = 's-expiring'; }
// 表格行已移除最后一列的操作按钮
tbody.insertAdjacentHTML('beforeend', `
<tr>
<td><strong>${d.domainFull}</strong><div class='small-muted'>${d.notes || ''}</div></td>
@@ -229,109 +183,52 @@ function renderTable(list = domains) {
<td>${d.expireDate}</td>
<td>${days}</td>
<td><span class='domain-status ${cls}'>${statusText}</span></td>
<td>
<div class='d-flex gap-1'>
<button class='btn btn-sm btn-outline-primary' onclick='openEdit(${globalIndex})'><i class='bi bi-pencil'></i></button>
<button class='btn btn-sm btn-outline-danger' onclick='removeDomain(${globalIndex})'><i class='bi bi-trash'></i></button>
</div>
</td>
</tr>
`);
});
renderPagination(totalPages);
renderPagination(totalPages, filtered.length);
}
/* ========== 分页按钮 ========== */
/* ========== 渲染分页按钮 ========== */
function renderPagination(totalPages) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
if (totalPages <= 1) return;
pagination.insertAdjacentHTML('beforeend', `
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${currentPage - 1})">上一页</a>
</li>
`);
const createPageItem = (page, text, disabled = false, active = false) => {
return `<li class="page-item ${disabled ? 'disabled' : ''} ${active ? 'active' : ''}">
<a class="page-link" href="#" onclick="event.preventDefault(); changePage(${page});">${text}</a>
</li>`;
};
pagination.innerHTML += createPageItem(currentPage - 1, '上一页', currentPage === 1);
for (let i = 1; i <= totalPages; i++) {
pagination.insertAdjacentHTML('beforeend', `
<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="#" onclick="changePage(${i})">${i}</a>
</li>
`);
pagination.innerHTML += createPageItem(i, i, false, i === currentPage);
}
pagination.insertAdjacentHTML('beforeend', `
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${currentPage + 1})">下一页</a>
</li>
`);
pagination.innerHTML += createPageItem(currentPage + 1, '下一页', currentPage === totalPages);
}
function changePage(page) {
if (page < 1) return;
const totalPages = Math.ceil(allDomains.length / pageSize); // Re-calculate based on filtered data if needed
if (page < 1 || page > totalPages && totalPages > 0) return;
currentPage = page;
renderTable();
}
/* ========== 统计更新 ========== */
function updateStats() {
document.getElementById('statTotal').textContent = domains.length;
document.getElementById('statExpiring').textContent = domains.filter(d => {
document.getElementById('statTotal').textContent = allDomains.length;
document.getElementById('statExpiring').textContent = allDomains.filter(d => {
const days = daysRemaining(d.expireDate);
return days > 0 && days <= 30;
}).length;
document.getElementById('statCost').textContent = '¥' + domains.reduce((s, d) => s + (Number(d.registerPrice) || 0), 0).toLocaleString();
document.getElementById('statIntent').textContent = '¥' + domains.reduce((s, d) => s + (Number(d.targetPrice) || 0), 0).toLocaleString();
document.getElementById('statActual').textContent = '¥' + domains.reduce((s, d) => s + (Number(d.actualPrice) || 0), 0).toLocaleString();
document.getElementById('statCost').textContent = '¥' + allDomains.reduce((s, d) => s + (Number(d.registerPrice) || 0), 0).toLocaleString();
document.getElementById('statIntent').textContent = '¥' + allDomains.reduce((s, d) => s + (Number(d.targetPrice) || 0), 0).toLocaleString();
document.getElementById('statActual').textContent = '¥' + allDomains.reduce((s, d) => s + (Number(d.actualPrice) || 0), 0).toLocaleString();
}
/* ========== 保存/编辑/删除 ========== */
function saveDomain() {
const fd = new FormData(document.getElementById('domainForm'));
const idx = Number(fd.get('editIndex'));
const item = {
domainFull: (fd.get('domainFull') || '').trim(),
registrar: fd.get('registrar') || '',
registerDate: fd.get('registerDate') || '',
expireDate: fd.get('expireDate') || '',
registerPrice: Number(fd.get('registerPrice')) || 0,
renewPrice: Number(fd.get('renewPrice')) || 0,
targetPrice: fd.get('targetPrice') ? Number(fd.get('targetPrice')) : 0,
actualPrice: fd.get('actualPrice') ? Number(fd.get('actualPrice')) : 0,
notes: fd.get('notes') || ''
};
if (!item.domainFull) { alert('请填写完整域名(含后缀)'); return; }
if (idx >= 0) { domains[idx] = item; } else { domains.push(item); }
bootstrap.Modal.getInstance(document.getElementById('addDomainModal')).hide();
saveDomains();
currentPage = 1;
renderTable(); updateStats();
document.getElementById('domainForm').reset();
}
function openEdit(i) {
const d = domains[i];
const f = document.getElementById('domainForm');
f.reset(); f.editIndex.value = i;
f.domainFull.value = d.domainFull;
f.registrar.value = d.registrar;
f.registerDate.value = d.registerDate;
f.expireDate.value = d.expireDate;
f.registerPrice.value = d.registerPrice;
f.renewPrice.value = d.renewPrice;
f.targetPrice.value = d.targetPrice;
f.actualPrice.value = d.actualPrice;
f.notes.value = d.notes;
new bootstrap.Modal(document.getElementById('addDomainModal')).show();
}
function removeDomain(i) {
if (confirm('确定删除该域名?')) {
domains.splice(i, 1);
saveDomains();
if (domains.length <= (currentPage - 1) * pageSize && currentPage > 1) currentPage--;
renderTable(); updateStats();
}
}
/* ========== 搜索/过滤/分页大小 ========== */
/* ========== 搜索/过滤/分页大小的事件监听 ========== */
document.getElementById('searchInput').addEventListener('input', () => { currentPage = 1; renderTable(); });
document.getElementById('filterStatus').addEventListener('change', () => { currentPage = 1; renderTable(); });
document.getElementById('pageSizeSelect').addEventListener('change', e => {
@@ -340,56 +237,7 @@ document.getElementById('pageSizeSelect').addEventListener('change', e => {
renderTable();
});
/* ========== 导出 CSV ========== */
document.getElementById('exportBtn').addEventListener('click', () => {
if (!domains.length) { alert('没有数据可导出'); return; }
const keys = ['domainFull', 'registrar', 'registerPrice', 'renewPrice', 'targetPrice', 'actualPrice', 'registerDate', 'expireDate', 'notes'];
const lines = [keys.join(',')];
domains.forEach(d => {
lines.push(keys.map(k => `"${(d[k] || '').toString().replace(/"/g, '""')}"`).join(','));
});
const blob = new Blob([lines.join('\n')], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'domains_export.csv';
a.click();
});
/* ========== 导入 CSV ========== */
document.getElementById('importBtn').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file'; input.accept = '.csv';
input.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = evt => {
const csv = evt.target.result;
const lines = csv.split('\n');
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
const importedDomains = [];
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue;
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
const domain = {};
headers.forEach((h, index) => { domain[h] = values[index] || ''; });
importedDomains.push(domain);
}
domains = [...domains, ...importedDomains];
saveDomains();
currentPage = 1;
renderTable(); updateStats();
};
reader.readAsText(file);
};
input.click();
});
/* ========== 首次加载 ========== */
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('input[name=registerDate]').value = new Date().toISOString().split('T')[0];
renderTable(); updateStats();
});
// 增、删、改相关的所有函数都已移除
</script>
</body>
</html>