feat: 综合平台services多服务支持+SW网络优先缓存修复+适配层/定时任务完善

This commit is contained in:
gouki
2026-08-05 11:57:17 +00:00
parent 7f1268f508
commit 1b7d4823c3
47 changed files with 2895 additions and 1189 deletions
+172
View File
@@ -0,0 +1,172 @@
/* 域名视图:域名列表 + 子域名管理 + 站点 SSL 状态
* 交互:每行可展开「子域名」面板;子域名可添加/编辑/删除、一键探测 SSL 证书;
* 域名行显示该域名下最近的证书到期徽章。
*/
const DomainsView = {
template: `
<div class="space-y-3">
<!-- 说明条 -->
<div class="text-xs text-slate-500 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800 rounded-lg px-3 py-2">
🌐 域名资产下管理子域名与站点 SSL 证书:展开任意域名添加子域名(如 www/api),点「探测SSL」即可监控该站点证书到期情况。
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 overflow-x-auto">
<table class="w-full text-sm min-w-[820px]">
<thead class="bg-slate-50 dark:bg-slate-800/50 text-slate-500 text-left text-xs">
<tr>
<th class="px-4 py-2.5 font-medium">域名</th>
<th class="px-4 py-2.5 font-medium">注册商</th>
<th class="px-4 py-2.5 font-medium">DNS</th>
<th class="px-4 py-2.5 font-medium">到期</th>
<th class="px-4 py-2.5 font-medium">站点SSL</th>
<th class="px-4 py-2.5 font-medium">使用中</th>
<th class="px-4 py-2.5 font-medium text-right">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
<tr v-if="!domains.length"><td colspan="7" class="px-4 py-10 text-center text-slate-400">暂无域名资产</td></tr>
<template v-for="a in domains" :key="a.id">
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/30">
<td class="px-4 py-2.5 font-medium">{{ a.domain_detail.domain_name }}</td>
<td class="px-4 py-2.5 text-slate-500">{{ a.domain_detail.registrar || '—' }}</td>
<td class="px-4 py-2.5 text-slate-500">{{ a.domain_detail.dns_provider || '—' }}</td>
<td class="px-4 py-2.5" :class="Fmt.expiryText(a.days_to_expiry)">{{ a.expiry_date || '—' }}<span v-if="a.days_to_expiry!==null" class="text-xs ml-1">({{ a.days_to_expiry }}天)</span></td>
<td class="px-4 py-2.5">
<span v-if="sslByAsset[a.id]" class="text-xs px-2 py-0.5 rounded-full whitespace-nowrap" :class="certBadge(sslByAsset[a.id].days_to_expiry)" :title="sslByAsset[a.id].issuer || ''">
{{ sslByAsset[a.id].hostname }} · {{ certText(sslByAsset[a.id]) }}
</span>
<span v-else class="text-xs text-slate-400">—</span>
</td>
<td class="px-4 py-2.5"><span class="text-xs px-2 py-0.5 rounded-full" :class="a.domain_detail.is_using ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' : 'bg-slate-500/10 text-slate-400'">{{ a.domain_detail.is_using ? '使用' : '闲置' }}</span></td>
<td class="px-4 py-2.5 text-right whitespace-nowrap">
<button @click="toggle(a)" class="text-indigo-600 dark:text-indigo-400 hover:underline mr-3 text-xs">{{ expanded[a.id] ? '收起' : '子域名' }}</button>
<button @click="edit(a)" class="text-blue-600 dark:text-blue-400 hover:underline text-xs">编辑/绑定</button>
</td>
</tr>
<!-- 子域名展开区 -->
<tr v-if="expanded[a.id]">
<td colspan="7" class="px-4 py-3 bg-slate-50/60 dark:bg-slate-800/20">
<div class="flex flex-wrap items-center gap-2 mb-2">
<span class="text-xs font-semibold text-slate-600 dark:text-slate-300">子域名({{ domainName(a) }}</span>
<button @click="resetForm(a)" class="text-xs px-2 py-1 rounded-lg bg-blue-600 text-white">+ 新增子域名</button>
<button @click="probeSsl(domainName(a), a)" class="text-xs px-2 py-1 rounded-lg border border-violet-300 dark:border-violet-700 text-violet-600 dark:text-violet-400">🔒 探测根域名SSL</button>
<span class="ml-auto text-xs text-slate-400">共 {{ (subsByAsset[a.id]||[]).length }} 条</span>
</div>
<div v-if="(subsByAsset[a.id]||[]).length" class="space-y-1 mb-2">
<div v-for="s in subsByAsset[a.id]||[]" :key="s.id" class="flex flex-wrap items-center gap-2 bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 px-3 py-2 text-xs">
<span class="font-mono font-medium text-slate-700 dark:text-slate-200">{{ fullHost(s, a) }}</span>
<span v-if="s.record_type" class="px-1.5 py-0.5 rounded bg-slate-500/10 text-slate-500">{{ s.record_type }}</span>
<span v-if="s.record_value" class="text-slate-400 truncate max-w-[180px]" :title="s.record_value">{{ s.record_value }}</span>
<span :class="s.is_active ? 'text-emerald-600 dark:text-emerald-400' : 'text-slate-400'">{{ s.is_active ? '启用' : '停用' }}</span>
<span v-if="s.note" class="text-slate-400 truncate max-w-[120px]" :title="s.note">{{ s.note }}</span>
<span class="ml-auto flex gap-3">
<button @click="probeSsl(fullHost(s, a), a)" class="text-violet-600 dark:text-violet-400 hover:underline">🔒 探测SSL</button>
<button @click="startEdit(a, s)" class="text-blue-600 dark:text-blue-400 hover:underline">编辑</button>
<button @click="delSub(s)" class="text-red-600 dark:text-red-400 hover:underline">删除</button>
</span>
</div>
</div>
<div v-else class="text-xs text-slate-400 mb-2">暂无子域名,点「+ 新增子域名」开始管理</div>
<!-- 新增/编辑表单 -->
<form v-if="forms[a.id]" @submit.prevent="saveSub(a)" class="flex flex-wrap gap-2 items-end bg-white dark:bg-slate-900 rounded-lg border border-blue-200 dark:border-blue-900/50 px-3 py-2">
<label class="text-xs text-slate-500">主机名
<input v-model="forms[a.id].host" placeholder="www / api@=根域名)" class="block text-sm px-2 py-1 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 w-28 mt-0.5">
</label>
<label class="text-xs text-slate-500">类型
<select v-model="forms[a.id].record_type" class="block text-sm px-2 py-1 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 mt-0.5">
<option v-for="t in ['A','CNAME','AAAA','MX','TXT']" :key="t" :value="t">{{ t }}</option>
</select>
</label>
<label class="text-xs text-slate-500 flex-1 min-w-[130px]">记录值
<input v-model="forms[a.id].record_value" placeholder="IP 或目标域名" class="block text-sm px-2 py-1 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 w-full mt-0.5">
</label>
<label class="text-xs text-slate-500">备注
<input v-model="forms[a.id].note" class="block text-sm px-2 py-1 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 w-24 mt-0.5">
</label>
<label class="text-xs text-slate-500 flex items-center gap-1 pb-1.5"><input type="checkbox" v-model="forms[a.id].is_active" class="accent-blue-600">启用</label>
<button type="submit" class="text-sm px-3 py-1.5 rounded-lg bg-blue-600 text-white">{{ forms[a.id].editing ? '保存' : '添加' }}</button>
<button type="button" @click="resetForm(a)" class="text-sm px-2 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 text-slate-500">取消</button>
</form>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>`,
setup() {
const domains = Vue.computed(() => store.assets.filter(a => a.asset_type === 'domain' && a.domain_detail));
const expanded = Vue.reactive({}); // asset_id -> 是否展开子域名面板
const forms = Vue.reactive({}); // asset_id -> 新增/编辑表单
const subsByAsset = Vue.computed(() => {
const m = {};
for (const s of store.subdomains) { (m[s.asset_id] = m[s.asset_id] || []).push(s); }
return m;
});
// 每个域名资产下最近到期的一条证书(用于主行徽章)
const sslByAsset = Vue.computed(() => {
const m = {};
for (const c of store.siteCerts) {
if (!c.asset_id) continue;
if (!m[c.asset_id] || (c.days_to_expiry ?? 9999) < (m[c.asset_id].days_to_expiry ?? 9999)) m[c.asset_id] = c;
}
return m;
});
function domainName(a) { return a.domain_detail.domain_name; }
function fullHost(s, a) { return s.host === '@' ? domainName(a) : s.host + '.' + domainName(a); }
function toggle(a) {
expanded[a.id] = !expanded[a.id];
if (expanded[a.id] && !forms[a.id]) resetForm(a);
}
function resetForm(a) {
forms[a.id] = { host: '', record_type: 'A', record_value: '', note: '', is_active: true, editing: null };
}
function startEdit(a, s) {
expanded[a.id] = true;
forms[a.id] = { host: s.host, record_type: s.record_type || 'A', record_value: s.record_value || '', note: s.note || '', is_active: !!s.is_active, editing: s.id };
}
async function saveSub(a) {
const f = forms[a.id];
if (!f.host.trim()) { alert('请填写主机名'); return; }
const payload = { host: f.host.trim(), record_type: f.record_type, record_value: f.record_value, note: f.note, is_active: !!f.is_active };
try {
if (f.editing) await Api.put('/subdomains/' + f.editing, payload);
else await Api.post('/subdomains', { ...payload, asset_id: a.id });
await loadSubdomains();
resetForm(a);
} catch (e) { alert('保存失败:' + e.message); }
}
async function delSub(s) {
if (!confirm('删除子域名「' + s.host + '」?')) return;
try { await Api.del('/subdomains/' + s.id); await loadSubdomains(); }
catch (e) { alert('删除失败:' + e.message); }
}
async function probeSsl(defaultHost, a) {
const target = prompt('探测 SSL 证书的站点(留空取消):', defaultHost);
if (!target) return;
try {
await Api.post('/site-certs', { hostname: target, asset_id: a.id });
await loadSiteCerts();
alert('已探测:' + target);
} catch (e) { alert('探测失败:' + e.message); }
}
function certText(c) {
const d = c.days_to_expiry;
if (d === null || d === undefined) return '未知';
if (d < 0) return '已过期' + Math.abs(d) + '天';
if (d === 0) return '今天到期';
return d + '天';
}
function certBadge(d) {
if (d === null || d === undefined) return 'bg-slate-500/10 text-slate-500';
if (d < 0 || d === 0 || d <= 7) return 'bg-red-500/10 text-red-600 dark:text-red-400';
if (d <= 30) return 'bg-amber-500/10 text-amber-600 dark:text-amber-400';
if (d <= 90) return 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400';
return 'bg-slate-500/10 text-slate-500';
}
return { store, Fmt, domains, subsByAsset, sslByAsset, expanded, forms, domainName, fullHost, toggle, resetForm, startEdit, saveSub, delSub, probeSsl, certText, certBadge, edit: openAssetEdit };
},
};