-
📡
- 选择一个订阅
-查看详情或生成转换链接
+👋
+ Select a subscription to manage
-
- 订阅名称
+Subscription Name
-
-
+
+
+
-
📝 基本信息
+
+
📝 Basic Information
+
-
+
-
-
+
-
-
+
- -
-
-
-
+
+
+
-
-
-
+
🔗 转换链接
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1989,51 +2173,50 @@ function getHTML() {
-
+
+
+
+ 🔗 Converter Link
+
+
+
+
+
+
+
+
+
+
+
+ VALIDITY
+
+
+
+
+
+ --
+
+
+
- --
-
-
-
@@ -2041,55 +2224,52 @@ function getHTML() {
新建订阅
+ New Subscription
-
-
+
+
-
+
-
支持 Clash 或 V2Ray 链接,系统自动识别
+ Supports Clash or V2Ray/SS/VMess links
-
+
-
-
+
+
-
-
+
+
-
续期订阅
+ Renew Link
-
+
-
-
+
+
-
-
-
+
提示
-
-
-
-
+
+
Notice
+
+
-
-
确认
-
-
-
-
+
@@ -2105,8 +2285,8 @@ function getHTML() {
window.onload = loadSubscriptions;
- // --- Custom Dialog Functions ---
- function showCustomAlert(message, title = '提示') {
+ // --- Custom Dialog ---
+ function showCustomAlert(message, title = 'Notice') {
document.getElementById('customAlertTitle').textContent = title;
document.getElementById('customAlertMessage').textContent = message;
document.getElementById('customAlertModal').classList.add('active');
@@ -2116,7 +2296,7 @@ function getHTML() {
document.getElementById('customAlertModal').classList.remove('active');
}
- function showCustomConfirm(message, callback, title = '确认') {
+ function showCustomConfirm(message, callback, title = 'Confirm Action') {
document.getElementById('customConfirmTitle').textContent = title;
document.getElementById('customConfirmMessage').textContent = message;
confirmCallback = callback;
@@ -2130,28 +2310,17 @@ function getHTML() {
function executeCustomConfirm() {
document.getElementById('customConfirmModal').classList.remove('active');
- if (confirmCallback && typeof confirmCallback === 'function') {
- confirmCallback();
- }
+ if (confirmCallback) confirmCallback();
confirmCallback = null;
}
- // --- UI Logic ---
+ // --- Logic ---
function onSubscriptionModeChange() {
const mode = document.getElementById('subMode').value;
- const permExp = document.getElementById('permanentExpiration');
- const tempExp = document.getElementById('temporaryExpiration');
-
- if (mode === 'temporary') {
- permExp.style.display = 'none';
- tempExp.style.display = 'block';
- } else {
- permExp.style.display = 'block';
- tempExp.style.display = 'none';
- }
+ document.getElementById('permanentExpiration').style.display = mode === 'permanent' ? 'block' : 'none';
+ document.getElementById('temporaryExpiration').style.display = mode === 'temporary' ? 'block' : 'none';
}
- // --- Data Logic ---
async function loadSubscriptions() {
try {
const res = await fetch('/api/subscriptions');
@@ -2164,48 +2333,40 @@ function getHTML() {
} else if (allSubscriptions.length === 0) {
showEmptyState();
}
- } catch (e) {
- console.error(e);
- }
+ } catch (e) { console.error(e); }
}
function renderList() {
const container = document.getElementById('subscriptionList');
if (allSubscriptions.length === 0) {
- container.innerHTML = '
+
Confirm
+
+
+
+
暂无订阅
';
+ container.innerHTML = 'No subscriptions found
';
return;
}
container.innerHTML = allSubscriptions.map(sub => {
- const activeClass = sub.id === currentSelectedId ? 'active' : '';
- const typeBadge = sub.subscriptionType === 'v2ray' ? 'badge-v2ray' : 'badge-clash';
+ const active = sub.id === currentSelectedId ? 'active' : '';
const typeText = sub.subscriptionType === 'v2ray' ? 'V2Ray' : 'Clash';
- const modeBadge = sub.subscriptionMode === 'temporary' ? 'badge-temp' : 'badge-perm';
- const modeText = sub.subscriptionMode === 'temporary' ? '临时' : '正式';
+ const typeClass = sub.subscriptionType === 'v2ray' ? 'badge-v2ray' : 'badge-clash';
+ const modeText = sub.subscriptionMode === 'temporary' ? 'TEMP' : 'PERM';
- // 计算状态标记(仅对长期订阅显示快过期/已过期)
- let statusBadge = '';
+ let statusHtml = '';
if (sub.subscriptionMode === 'permanent' && sub.expiresAt) {
- const now = Date.now();
- const daysRemaining = (sub.expiresAt - now) / (1000 * 60 * 60 * 24);
- if (sub.isExpired || daysRemaining <= 0) {
- statusBadge = '已过期';
- } else if (daysRemaining <= 15) {
- statusBadge = '快过期';
- }
+ const days = (sub.expiresAt - Date.now()) / 86400000;
+ if (sub.isExpired || days <= 0) statusHtml = 'Expired';
+ else if (days <= 15) statusHtml = 'Expiring';
}
-
- return \`
+
+ return \`
\${escapeHtml(sub.name)}
-
- \${typeText}
- \${statusBadge}
+
-
+ \${typeText}
+ \${statusHtml}
- \${modeText}
- \${sub.isExpired ? '已过期' : (sub.expiresAt ? formatDateSimple(sub.expiresAt) : '无限期')}
+
\`;
}).join('');
@@ -2213,7 +2374,7 @@ function getHTML() {
function selectSubscription(id) {
currentSelectedId = id;
- renderList(); // Update active state
+ renderList();
const sub = allSubscriptions.find(s => s.id === id);
if (!sub) return;
@@ -2221,31 +2382,26 @@ function getHTML() {
document.getElementById('emptyState').style.display = 'none';
document.getElementById('detailView').style.display = 'block';
- // Fill Info
document.getElementById('detailTitle').textContent = sub.name;
- const typeText = sub.subscriptionType === 'v2ray' ? 'V2Ray' : 'Clash';
- const typeClass = sub.subscriptionType === 'v2ray' ? 'badge-v2ray' : 'badge-clash';
- const modeText = sub.subscriptionMode === 'temporary' ? '临时订阅' : '正式订阅';
- const modeClass = sub.subscriptionMode === 'temporary' ? 'badge-temp' : 'badge-perm';
-
+ // Badges
const typeBadge = document.getElementById('detailTypeBadge');
- typeBadge.className = 'badge ' + typeClass;
- typeBadge.textContent = typeText;
+ typeBadge.className = 'badge ' + (sub.subscriptionType === 'v2ray' ? 'badge-v2ray' : 'badge-clash');
+ typeBadge.textContent = sub.subscriptionType === 'v2ray' ? 'V2Ray' : 'Clash';
const modeBadge = document.getElementById('detailModeBadge');
- modeBadge.className = 'badge ' + modeClass;
- modeBadge.textContent = modeText;
+ modeBadge.className = 'badge ' + (sub.subscriptionMode === 'temporary' ? 'badge-temp' : 'badge-perm');
+ modeBadge.textContent = sub.subscriptionMode === 'temporary' ? 'Temporary' : 'Permanent';
- document.getElementById('detailSourceType').textContent = typeText;
+ // Info
+ document.getElementById('detailSourceType').textContent = sub.subscriptionType === 'v2ray' ? 'V2Ray / VLESS / VMess' : 'Clash YAML';
document.getElementById('detailCreatedAt').textContent = formatDate(sub.createdAt);
- document.getElementById('detailExpiration').textContent = sub.expiresAt ? formatDate(sub.expiresAt) : '无限期';
+ document.getElementById('detailExpiration').textContent = sub.expiresAt ? formatDate(sub.expiresAt) : 'Unlimited';
document.getElementById('detailOriginalUrl').textContent = sub.clashUrl;
- // Set default tab based on source type preference, or stick to current if valid
- // Logic: if source is v2ray, default to v2ray tab, else clash
+ // Link logic
const defaultType = sub.subscriptionType === 'v2ray' ? 'v2ray' : 'clash';
- switchLinkType(defaultType);
+ switchLinkType(defaultType); // Reset to default preference or keep state? Resetting is safer for now.
}
function showEmptyState() {
@@ -2258,38 +2414,31 @@ function getHTML() {
if (!currentSelectedId) return;
currentLinkType = type;
- // Update Tabs
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.type === type);
});
const sub = allSubscriptions.find(s => s.id === currentSelectedId);
- if (!sub) return;
-
- const shortId = sub.shortIds && sub.shortIds[type];
- const expiry = sub.shortIdsExpiry && sub.shortIdsExpiry[type];
+ const shortId = sub?.shortIds?.[type];
+ const expiry = sub?.shortIdsExpiry?.[type];
if (shortId) {
document.getElementById('detailShortUrl').value = window.location.origin + '/s/' + shortId;
updateLinkStatus(expiry);
} else {
- // Auto generate if missing
generateLink(type);
}
}
async function generateLink(type) {
const sub = allSubscriptions.find(s => s.id === currentSelectedId);
-
- if (!sub) {
- document.getElementById('detailShortUrl').value = '订阅不存在';
- return;
- }
-
- document.getElementById('detailShortUrl').value = '正在生成链接...';
+ if (!sub) return;
+
+ const input = document.getElementById('detailShortUrl');
+ input.value = 'Generating...';
document.getElementById('linkProgressBar').style.width = '100%';
- document.getElementById('linkProgressBar').style.background = 'var(--warning)';
- document.getElementById('linkProgressText').textContent = '生成中';
+ document.getElementById('linkProgressBar').style.backgroundColor = 'var(--warning)';
+ document.getElementById('linkProgressText').textContent = 'Processing';
try {
const res = await fetch('/api/subscriptions/' + currentSelectedId + '/renew', {
@@ -2301,20 +2450,16 @@ function getHTML() {
})
});
- if (!res.ok) throw new Error('Failed');
-
const data = await res.json();
- document.getElementById('detailShortUrl').value = data.shortUrl;
+ if (!res.ok) throw new Error(data.error);
- // Refresh data
+ input.value = data.shortUrl;
await loadSubscriptions();
- const updatedSub = allSubscriptions.find(s => s.id === currentSelectedId);
- if (updatedSub) {
- updateLinkStatus(updatedSub.shortIdsExpiry[type]);
- }
+ const updated = allSubscriptions.find(s => s.id === currentSelectedId);
+ updateLinkStatus(updated?.shortIdsExpiry?.[type]);
} catch (e) {
- document.getElementById('detailShortUrl').value = '生成失败';
- document.getElementById('linkProgressBar').style.background = 'var(--danger)';
+ input.value = 'Error generating link';
+ document.getElementById('linkProgressBar').style.backgroundColor = 'var(--danger)';
}
}
@@ -2324,8 +2469,8 @@ function getHTML() {
if (!expiry) {
bar.style.width = '100%';
- bar.style.background = 'var(--success)';
- text.textContent = '永久有效';
+ bar.style.backgroundColor = 'var(--success)';
+ text.textContent = 'Unlimited';
return;
}
@@ -2334,62 +2479,45 @@ function getHTML() {
if (remaining <= 0) {
bar.style.width = '0%';
- bar.style.background = 'var(--danger)';
- text.textContent = '已过期';
+ bar.style.backgroundColor = 'var(--danger)';
+ text.textContent = 'Expired';
return;
}
- const days = Math.ceil(remaining / (86400000));
- const total = 7 * 86400000; // Assume 7 days base for visual
- const pct = Math.min(100, (remaining / total) * 100);
+ const total = 7 * 86400000; // Base reference
+ const pct = Math.min(100, Math.max(5, (remaining / total) * 100));
bar.style.width = pct + '%';
- bar.style.background = pct < 20 ? 'var(--danger)' : (pct < 50 ? 'var(--warning)' : 'var(--success)');
+ bar.style.backgroundColor = pct < 20 ? 'var(--danger)' : (pct < 50 ? 'var(--warning)' : 'var(--success)');
- if (days > 1) text.textContent = \`剩余 \${days} 天\`;
- else {
- const hours = Math.ceil(remaining / 3600000);
- text.textContent = \`剩余 \${hours} 小时\`;
- }
+ const days = Math.ceil(remaining / 86400000);
+ text.textContent = days > 1 ? \`\${days} Days\` : \`\${Math.ceil(remaining / 3600000)} Hours\`;
}
- // --- Actions ---
+ // Actions
function showAddModal() {
- document.getElementById('modalTitle').textContent = '新建订阅';
+ document.getElementById('modalTitle').textContent = 'New Subscription';
document.getElementById('subName').value = '';
document.getElementById('subClashUrl').value = '';
document.getElementById('subMode').value = 'permanent';
onSubscriptionModeChange();
+ // Show expiration field for new subscriptions
+ document.getElementById('expirationGroup').style.display = 'block';
currentEditId = null;
document.getElementById('subscriptionModal').classList.add('active');
}
- async function editSubscription() {
+ function editSubscription() {
if (!currentSelectedId) return;
const sub = allSubscriptions.find(s => s.id === currentSelectedId);
- document.getElementById('modalTitle').textContent = '编辑订阅';
+ document.getElementById('modalTitle').textContent = 'Edit Subscription';
document.getElementById('subName').value = sub.name;
document.getElementById('subClashUrl').value = sub.clashUrl;
document.getElementById('subMode').value = sub.subscriptionMode || 'permanent';
-
- // Restore expiration UI state
- if (sub.subscriptionMode === 'temporary') {
- document.getElementById('subTempExpiration').value = sub.expiration || '3d';
- } else {
- // 设置永久订阅的有效期下拉框
- const expSelect = document.getElementById('subPermanentExpiration');
- const exp = sub.expiration || '7d';
- // 检查是否是预设选项
- const options = Array.from(expSelect.options).map(o => o.value);
- if (options.includes(exp)) {
- expSelect.value = exp;
- } else {
- expSelect.value = '7d'; // 默认值
- }
- }
onSubscriptionModeChange();
-
+ // Hide expiration field when editing
+ document.getElementById('expirationGroup').style.display = 'none';
currentEditId = currentSelectedId;
document.getElementById('subscriptionModal').classList.add('active');
}
@@ -2398,16 +2526,12 @@ function getHTML() {
const name = document.getElementById('subName').value.trim();
const url = document.getElementById('subClashUrl').value.trim();
const mode = document.getElementById('subMode').value;
-
- let expiration;
- if (mode === 'temporary') {
- expiration = document.getElementById('subTempExpiration').value;
- } else {
- expiration = document.getElementById('subPermanentExpiration').value;
- }
+ const expiration = mode === 'temporary'
+ ? document.getElementById('subTempExpiration').value
+ : document.getElementById('subPermanentExpiration').value;
if (!name || !url) {
- showCustomAlert('请填写完整信息');
+ showCustomAlert('Please fill in all fields');
return;
}
@@ -2421,139 +2545,84 @@ function getHTML() {
body: JSON.stringify({ name, clashUrl: url, expiration, subscriptionMode: mode })
});
- const data = await res.json();
-
- if (!res.ok) throw new Error(data.error || 'Failed');
+ if (!res.ok) throw new Error((await res.json()).error);
closeModal();
-
- // 新建订阅时,设置 currentSelectedId 避免 loadSubscriptions 自动选中触发重复生成
- const newSubId = !currentEditId && data.subscription ? data.subscription.id : null;
- if (newSubId) {
- currentSelectedId = newSubId;
- }
-
await loadSubscriptions();
-
- // 新建订阅时选中它(此时 loadSubscriptions 不会重复调用 selectSubscription)
- if (newSubId) {
- selectSubscription(newSubId);
- }
} catch (e) {
- showCustomAlert('保存失败: ' + e.message);
+ showCustomAlert('Failed: ' + e.message);
}
}
async function deleteCurrentSubscription() {
- showCustomConfirm('确定要删除此订阅吗?', async () => {
- try {
- await fetch('/api/subscriptions/' + currentSelectedId, { method: 'DELETE' });
- currentSelectedId = null;
- await loadSubscriptions();
- } catch (e) {
- showCustomAlert('删除失败');
- }
+ showCustomConfirm('Are you sure you want to delete this?', async () => {
+ await fetch('/api/subscriptions/' + currentSelectedId, { method: 'DELETE' });
+ currentSelectedId = null;
+ await loadSubscriptions();
});
}
async function clearAllSubscriptions() {
- showCustomConfirm('⚠️ 警告:这将清空所有订阅数据,此操作不可恢复!确定继续?', async () => {
- try {
- const res = await fetch('/api/subscriptions/clear-all', { method: 'POST' });
- const data = await res.json();
- console.log('Clear all response:', res.status, data);
- if (res.ok) {
- currentSelectedId = null;
- allSubscriptions = [];
- renderList();
- showEmptyState();
- showCustomAlert(data.message || '已清空所有数据');
- } else {
- showCustomAlert('清空失败: ' + (data.error || '未知错误'));
- }
- } catch (e) {
- console.error('Clear all error:', e);
- showCustomAlert('清空失败: ' + e.message);
- }
+ // Close dropdown if open
+ const dropdown = document.getElementById('dropdownMenu');
+ if (dropdown) dropdown.classList.remove('active');
+
+ showCustomConfirm('WARNING: This will delete ALL subscriptions! Continue?', async () => {
+ await fetch('/api/subscriptions/clear-all', { method: 'POST' });
+ currentSelectedId = null;
+ await loadSubscriptions();
+ showCustomAlert('All data cleared');
});
}
- // --- Renew/Regenerate ---
- function renewSubscription() {
- openRenewModal('renew');
+ function toggleDropdown() {
+ const dropdown = document.getElementById('dropdownMenu');
+ dropdown.classList.toggle('active');
}
- function regenerateShortUrl() {
- openRenewModal('regenerate');
- }
+ function renewSubscription() { openRenewModal('renew'); }
+ function regenerateShortUrl() { openRenewModal('regenerate'); }
function openRenewModal(action) {
currentRenewAction = action;
currentRenewId = currentSelectedId;
const sub = allSubscriptions.find(s => s.id === currentSelectedId);
+ if (!sub) return;
+
+ document.getElementById('renewModalTitle').textContent = action === 'renew' ? 'Renew Validity' : 'Regenerate Token';
- if (!sub) {
- showCustomAlert('请先选择一个订阅');
- return;
- }
-
- document.getElementById('renewModalTitle').textContent = action === 'renew' ? '续期链接' : '更换新链接';
-
- if (sub.subscriptionMode === 'temporary') {
- document.getElementById('renewPermanentExpiration').style.display = 'none';
- document.getElementById('renewTemporaryExpiration').style.display = 'block';
- } else {
- document.getElementById('renewPermanentExpiration').style.display = 'block';
- document.getElementById('renewTemporaryExpiration').style.display = 'none';
- }
+ const isTemp = sub.subscriptionMode === 'temporary';
+ document.getElementById('renewPermanentExpiration').style.display = isTemp ? 'none' : 'block';
+ document.getElementById('renewTemporaryExpiration').style.display = isTemp ? 'block' : 'none';
document.getElementById('renewModal').classList.add('active');
}
async function confirmRenew() {
const sub = allSubscriptions.find(s => s.id === currentRenewId);
-
- if (!sub) {
- showCustomAlert('订阅不存在');
- closeRenewModal();
- return;
- }
-
- let expiration;
-
- if (sub.subscriptionMode === 'temporary') {
- expiration = document.getElementById('renewTempExpiration').value;
- } else {
- expiration = document.getElementById('renewPermanentExp').value;
- }
+ const expiration = sub.subscriptionMode === 'temporary'
+ ? document.getElementById('renewTempExpiration').value
+ : document.getElementById('renewPermanentExp').value;
- const endpoint = \`/api/subscriptions/\${currentRenewId}/\${currentRenewAction}\`;
-
try {
- const res = await fetch(endpoint, {
+ await fetch(\`/api/subscriptions/\${currentRenewId}/\${currentRenewAction}\`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expiration, linkType: currentLinkType })
});
-
- if (!res.ok) throw new Error('Failed');
-
closeRenewModal();
await loadSubscriptions();
- // Refresh view
- const updatedSub = allSubscriptions.find(s => s.id === currentRenewId);
- if (updatedSub) {
- const shortId = updatedSub.shortIds[currentLinkType];
- document.getElementById('detailShortUrl').value = window.location.origin + '/s/' + shortId;
- updateLinkStatus(updatedSub.shortIdsExpiry[currentLinkType]);
- }
+ // Refresh link view
+ const updated = allSubscriptions.find(s => s.id === currentRenewId);
+ document.getElementById('detailShortUrl').value = window.location.origin + '/s/' + updated.shortIds[currentLinkType];
+ updateLinkStatus(updated.shortIdsExpiry[currentLinkType]);
} catch (e) {
- showCustomAlert('操作失败');
+ showCustomAlert('Operation failed');
}
}
- // --- Utils ---
+ // Utils
function closeModal() { document.getElementById('subscriptionModal').classList.remove('active'); }
function closeRenewModal() { document.getElementById('renewModal').classList.remove('active'); }
@@ -2564,12 +2633,12 @@ function getHTML() {
const btn = e.target;
const original = btn.textContent;
- btn.textContent = '已复制';
+ btn.textContent = 'Copied!';
btn.style.color = 'var(--success)';
setTimeout(() => {
btn.textContent = original;
btn.style.color = '';
- }, 2000);
+ }, 1000);
}
function escapeHtml(text) {
@@ -2579,25 +2648,28 @@ function getHTML() {
}
function formatDate(ts) {
- return new Date(ts).toLocaleString('zh-CN', { hour12: false });
+ return new Date(ts).toLocaleString();
}
function formatDateSimple(ts) {
- const d = new Date(ts);
- return \`\${d.getMonth()+1}-\${d.getDate()} \${d.getHours()}:\${d.getMinutes().toString().padStart(2,'0')}\`;
+ return new Date(ts).toLocaleDateString() + ' ' + new Date(ts).getHours() + ':' + new Date(ts).getMinutes().toString().padStart(2, '0');
}
- window.onclick = function(event) {
- if (event.target.classList.contains('modal')) {
- event.target.classList.remove('active');
+ window.onclick = function(e) {
+ if (e.target.classList.contains('modal')) e.target.classList.remove('active');
+
+ // Close dropdown if clicking outside
+ const dropdown = document.getElementById('dropdownMenu');
+ if (dropdown && !e.target.closest('.dropdown-container')) {
+ dropdown.classList.remove('active');
}
}
-
+ \${modeText}
+ \${sub.expiresAt ? formatDateSimple(sub.expiresAt) : 'Unlimited'}