feat: 平台预设字典+凭证加密+分库备份+Agent采集+移动端优先前端重做

This commit is contained in:
gouki
2026-08-01 23:10:31 +00:00
parent 1dac5d8735
commit e672086b67
31 changed files with 1792 additions and 421 deletions
+12 -252
View File
@@ -2,262 +2,22 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{ app_name }}</title>
<script src="/static/js/tailwind.js"></script>
<script>tailwind.config = { darkMode: 'class' };</script>
<script src="/static/js/vue.global.prod.js"></script>
<script>window.APP_CONFIG = { appName: "{{ app_name }}" };</script>
<style>
html { -webkit-tap-highlight-color: transparent; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: rgba(100,116,139,.3); border-radius: 4px; }
[v-cloak] { display: none; }
</style>
</head>
<body class="bg-slate-100 text-slate-800 min-h-screen">
<div id="app" class="max-w-7xl mx-auto px-4 py-6">
{% raw %}
<!-- 顶部栏 -->
<header class="flex flex-wrap items-center justify-between gap-3 mb-6">
<h1 class="text-2xl font-bold text-slate-900">📦 {{ appName }}</h1>
<div class="flex items-center gap-3">
<input v-model="search" @input="loadAssets" placeholder="搜索名称 / 服务商…"
class="px-3 py-2 rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm w-56">
<button @click="openCreate"
class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition">+ 新增资产</button>
</div>
</header>
<!-- 全局错误提示 -->
<div v-if="error" class="mb-4 px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm flex justify-between">
<span>⚠️ {{ error }}</span>
<button @click="error = ''" class="font-bold"></button>
</div>
<!-- Dashboard 卡片 -->
<section class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl p-4 shadow-sm">
<div class="text-sm text-slate-500">总资产</div>
<div class="text-3xl font-bold text-slate-900 mt-1">{{ overview.total || 0 }}</div>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm border-l-4 border-red-500">
<div class="text-sm text-slate-500">30 天内到期</div>
<div class="text-3xl font-bold text-red-600 mt-1">{{ overview.expiring_30 || 0 }}</div>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm">
<div class="text-sm text-slate-500">本年支出</div>
<div class="text-3xl font-bold text-slate-900 mt-1">{{ overview.year_cost || 0 }}</div>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm border-l-4 border-amber-500">
<div class="text-sm text-slate-500">异常状态</div>
<div class="text-3xl font-bold text-amber-600 mt-1">{{ overview.abnormal_count || 0 }}</div>
</div>
</section>
<!-- 续费倒计时 -->
<section v-if="expiring.length" class="bg-white rounded-xl p-4 shadow-sm mb-6">
<h2 class="font-semibold text-slate-900 mb-3">⏰ 续费倒计时(30 天内)</h2>
<div class="space-y-2">
<div v-for="a in expiring" :key="a.id"
class="flex justify-between items-center px-3 py-2 rounded-lg bg-slate-50">
<span class="text-sm">
<span class="font-medium">{{ a.name }}</span>
<span class="text-slate-400"> · {{ a.provider }}</span>
</span>
<span class="text-sm px-2 py-0.5 rounded-full" :class="expiryBadge(a.days_to_expiry)">
{{ a.days_to_expiry }} 天
</span>
</div>
</div>
</section>
<!-- 筛选 + 资产表格 -->
<section class="bg-white rounded-xl shadow-sm overflow-hidden">
<div class="p-4 flex flex-wrap gap-2 border-b border-slate-100">
<select v-model="filterType" @change="loadAssets"
class="px-3 py-1.5 rounded-lg border border-slate-300 text-sm">
<option value="">全部类型</option>
<option v-for="(label, key) in typeLabels" :key="key" :value="key">{{ label }}</option>
</select>
<select v-model="filterStatus" @change="loadAssets"
class="px-3 py-1.5 rounded-lg border border-slate-300 text-sm">
<option value="">全部状态</option>
<option v-for="(label, key) in statusLabels" :key="key" :value="key">{{ label }}</option>
</select>
<span class="ml-auto text-sm text-slate-400 self-center">共 {{ assets.length }} 条</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-500 text-left">
<tr>
<th class="px-4 py-3 font-medium">名称</th>
<th class="px-4 py-3 font-medium">类型</th>
<th class="px-4 py-3 font-medium">服务商</th>
<th class="px-4 py-3 font-medium">到期时间</th>
<th class="px-4 py-3 font-medium">剩余</th>
<th class="px-4 py-3 font-medium">状态</th>
<th class="px-4 py-3 font-medium text-right">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
<tr v-if="!assets.length">
<td colspan="7" class="px-4 py-10 text-center text-slate-400">暂无资产,点击右上角「新增资产」开始录入</td>
</tr>
<tr v-for="a in assets" :key="a.id" class="hover:bg-slate-50" :class="{ 'opacity-50': a.is_archived }">
<td class="px-4 py-3">
<span class="font-medium text-slate-900">{{ a.name }}</span>
<span v-if="a.is_archived" class="ml-1 text-xs text-slate-400">(已归档)</span>
</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded-full text-xs" :class="typeBadge(a.asset_type)">{{ typeLabels[a.asset_type] }}</span>
</td>
<td class="px-4 py-3 text-slate-600">{{ a.provider }}</td>
<td class="px-4 py-3 text-slate-600">{{ a.expiry_date || '—' }}</td>
<td class="px-4 py-3" :class="expiryText(a.days_to_expiry)">
{{ a.days_to_expiry === null || a.days_to_expiry === undefined ? '—' : a.days_to_expiry + '天' }}
</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded-full text-xs" :class="statusBadge(a.status)">{{ statusLabels[a.status] }}</span>
</td>
<td class="px-4 py-3 text-right whitespace-nowrap">
<button @click="openEdit(a)" class="text-blue-600 hover:underline mr-3">编辑</button>
<button @click="deleteAsset(a)" class="text-red-600 hover:underline">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- 新增 / 编辑模态框 -->
<div v-if="showModal" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-y-auto shadow-xl">
<div class="px-6 py-4 border-b border-slate-100 flex justify-between items-center">
<h3 class="font-semibold text-slate-900">{{ editing ? '编辑资产' : '新增资产' }}</h3>
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600"></button>
</div>
<form @submit.prevent="saveAsset" class="px-6 py-4 space-y-4">
<!-- 主表字段 -->
<div class="grid grid-cols-2 gap-3">
<label class="col-span-2 block">
<span class="text-sm text-slate-600">名称 *</span>
<input v-model="form.name" required class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 text-sm">
</label>
<label class="block">
<span class="text-sm text-slate-600">类型 *</span>
<select v-model="form.asset_type" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 text-sm">
<option v-for="(label, key) in typeLabels" :key="key" :value="key">{{ label }}</option>
</select>
</label>
<label class="block">
<span class="text-sm text-slate-600">服务商 *</span>
<input v-model="form.provider" required placeholder="aliyun / vultr / namesilo" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 text-sm">
</label>
<label class="block">
<span class="text-sm text-slate-600">所属账号</span>
<input v-model="form.account" placeholder="如 Cloudflare 邮箱" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 text-sm">
</label>
<label class="block">
<span class="text-sm text-slate-600">到期时间</span>
<input type="date" v-model="form.expiry_date" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 text-sm">
</label>
<label class="block">
<span class="text-sm text-slate-600">续费费用</span>
<input type="number" step="0.01" v-model.number="form.cost" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 text-sm">
</label>
<label class="block">
<span class="text-sm text-slate-600">币种</span>
<select v-model="form.currency" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 text-sm">
<option>USD</option><option>CNY</option><option>JPY</option><option>EUR</option>
</select>
</label>
<label class="block">
<span class="text-sm text-slate-600">状态</span>
<select v-model="form.status" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 text-sm">
<option v-for="(label, key) in statusLabels" :key="key" :value="key">{{ label }}</option>
</select>
</label>
<div class="col-span-2 flex gap-6 text-sm text-slate-600">
<label class="flex items-center gap-2"><input type="checkbox" v-model="form.auto_renew"> 自动续费</label>
<label class="flex items-center gap-2"><input type="checkbox" v-model="form.is_archived"> 已归档(不再使用)</label>
</div>
<label class="col-span-2 block">
<span class="text-sm text-slate-600">备注</span>
<textarea v-model="form.remark" rows="2" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 text-sm"></textarea>
</label>
</div>
<!-- VPS 详情 -->
<fieldset v-if="form.asset_type === 'vps'" class="border border-slate-200 rounded-lg p-3">
<legend class="text-sm font-medium text-slate-700 px-1">VPS 详情</legend>
<div class="grid grid-cols-2 gap-3">
<label class="block"><span class="text-xs text-slate-500">公网 IP *</span>
<input v-model="form.vps_detail.ip_address" required class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">Tailscale IP</span>
<input v-model="form.vps_detail.tailscale_ip" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">区域</span>
<input v-model="form.vps_detail.region" placeholder="Japan / US West" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">操作系统</span>
<input v-model="form.vps_detail.os" placeholder="Debian 12" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">CPU 核数</span>
<input type="number" v-model.number="form.vps_detail.cpu_cores" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">内存 (GB)</span>
<input type="number" step="0.5" v-model.number="form.vps_detail.memory_gb" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">磁盘 (GB)</span>
<input type="number" v-model.number="form.vps_detail.disk_gb" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">月流量 (GB)</span>
<input type="number" v-model.number="form.vps_detail.bandwidth_gb" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">SSH 端口</span>
<input type="number" v-model.number="form.vps_detail.ssh_port" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">面板 URL</span>
<input v-model="form.vps_detail.panel_url" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
</div>
</fieldset>
<!-- 域名详情 -->
<fieldset v-if="form.asset_type === 'domain'" class="border border-slate-200 rounded-lg p-3">
<legend class="text-sm font-medium text-slate-700 px-1">域名详情</legend>
<div class="grid grid-cols-2 gap-3">
<label class="col-span-2 block"><span class="text-xs text-slate-500">域名 *</span>
<input v-model="form.domain_detail.domain_name" required placeholder="example.com" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">注册商</span>
<input v-model="form.domain_detail.registrar" placeholder="namesilo / 新网" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">DNS 托管</span>
<input v-model="form.domain_detail.dns_provider" placeholder="cloudflare" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">Cloudflare 账号</span>
<input v-model="form.domain_detail.cloudflare_account" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">跳转目标</span>
<input v-model="form.domain_detail.redirect_target" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="col-span-2 flex items-center gap-2 text-sm text-slate-600">
<input type="checkbox" v-model="form.domain_detail.is_using"> 仍在实际使用</label>
</div>
</fieldset>
<!-- AI 账号详情 -->
<fieldset v-if="form.asset_type === 'ai_agent'" class="border border-slate-200 rounded-lg p-3">
<legend class="text-sm font-medium text-slate-700 px-1">AI 账号详情</legend>
<div class="grid grid-cols-2 gap-3">
<label class="block"><span class="text-xs text-slate-500">服务商 *</span>
<input v-model="form.ai_detail.provider" required placeholder="openai / kimi / deepseek" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">订阅计划</span>
<input v-model="form.ai_detail.plan" placeholder="ChatGPT Plus" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="col-span-2 block"><span class="text-xs text-slate-500">API Key</span>
<input v-model="form.ai_detail.api_key" type="password" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">当前余额</span>
<input type="number" step="0.01" v-model.number="form.ai_detail.balance" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">币种</span>
<input v-model="form.ai_detail.currency" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">本月用量</span>
<input type="number" step="0.01" v-model.number="form.ai_detail.monthly_usage" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
<label class="block"><span class="text-xs text-slate-500">月度限额</span>
<input type="number" step="0.01" v-model.number="form.ai_detail.monthly_limit" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 text-sm"></label>
</div>
</fieldset>
<div class="flex justify-end gap-3 pt-2">
<button type="button" @click="showModal = false" class="px-4 py-2 rounded-lg border border-slate-300 text-sm text-slate-600 hover:bg-slate-50">取消</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700">{{ editing ? '保存修改' : '创建资产' }}</button>
</div>
</form>
</div>
</div>
{% endraw %}
</div>
<script src="/static/js/app.js"></script>
<body class="bg-slate-50 dark:bg-slate-950 text-slate-800 dark:text-slate-200 antialiased">
<div id="app"></div>
<script src="/static/js/api.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>