@@ -1,22 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ ctx: any }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel page-stack">
|
||||
<div class="section-head"><h2>审计日志</h2><button class="btn ghost" @click="ctx.loadAudit">刷新</button></div>
|
||||
<table>
|
||||
<thead><tr><th>类型</th><th>目标</th><th>信息</th><th>IP</th><th>时间</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="item in ctx.auditLogs" :key="item.id">
|
||||
<td><span class="badge neutral">{{ ctx.auditTypeLabel(item.type) }}</span></td>
|
||||
<td>{{ item.target }}</td>
|
||||
<td>{{ ctx.auditMessage(item) }}</td>
|
||||
<td>{{ item.ip || "-" }}</td>
|
||||
<td>{{ item.createdAt }}</td>
|
||||
</tr>
|
||||
<tr v-if="ctx.auditLogs.length === 0"><td colspan="5">暂无审计日志。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</template>
|
||||
@@ -47,17 +47,6 @@ use([CanvasRenderer, LineChart, PieChart, BarChart, GaugeChart, GridComponent, T
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="panel quick-panel">
|
||||
<div class="section-head"><h2>功能总览</h2><span class="badge">{{ ctx.quickActions.length }} 个入口</span></div>
|
||||
<div class="quick-grid">
|
||||
<button v-for="item in ctx.quickActions" :key="item.path" @click="ctx.navigate(item.path)">
|
||||
<component :is="item.icon" :size="18" />
|
||||
<strong>{{ item.label }}</strong>
|
||||
<span>{{ item.description }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="chart-grid">
|
||||
<section class="panel chart-panel"><h2>接口心跳延迟</h2><VChart class="chart" :option="ctx.heartbeatOption" autoresize /></section>
|
||||
<section class="panel chart-panel"><h2>接口健康分布</h2><VChart class="chart" :option="ctx.healthOption" autoresize /></section>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ ctx: any }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="split">
|
||||
<section class="panel page-stack">
|
||||
<div class="section-head"><h2>数据库运行状态</h2><span :class="['badge', ctx.statusTone(ctx.database?.activeProvider)]">{{ ctx.database?.activeProvider || "-" }}</span></div>
|
||||
<div class="kv-grid">
|
||||
<span>配置类型</span><strong>{{ ctx.database?.configProvider || "-" }}</strong>
|
||||
<span>SQLite</span><strong>{{ ctx.database?.sqliteReady ? "ready" : "missing" }}</strong>
|
||||
<span>MySQL</span><strong>{{ ctx.database?.remoteReady ? "ready" : "offline" }}</strong>
|
||||
<span>最后同步</span><strong>{{ ctx.database?.lastSyncAt || "-" }}</strong>
|
||||
<span>最近错误</span><strong>{{ ctx.database?.lastSyncError || ctx.database?.lastError || "-" }}</strong>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="section-head"><h2>旧项目同步</h2><button class="btn ghost" @click="ctx.previewLegacySync">预览</button></div>
|
||||
<pre class="json-preview small">{{ ctx.pretty(ctx.legacySync) }}</pre>
|
||||
<button class="btn primary" @click="ctx.runLegacySync">执行旧项目同步</button>
|
||||
</section>
|
||||
<aside class="panel editor-panel">
|
||||
<h2>连接与同步</h2>
|
||||
<label>Provider<select v-model="ctx.databaseForm.provider"><option>sqlite</option><option>mysql</option></select></label>
|
||||
<label>SQLite 路径<input v-model="ctx.databaseForm.sqlitePath" placeholder="留空使用当前配置" /></label>
|
||||
<label>MySQL DSN<input v-model="ctx.databaseForm.mysqlDsn" placeholder="user:pass@tcp(host:3306)/db?parseTime=true" /></label>
|
||||
<button class="btn ghost" @click="ctx.testDatabase">测试连接</button>
|
||||
<button class="btn primary" @click="ctx.syncDatabase('import')">SQLite 导入远端</button>
|
||||
<button class="btn ghost" @click="ctx.syncDatabase('sync')">远端同步回本地</button>
|
||||
</aside>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,10 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ ctx: any }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel page-stack">
|
||||
<h2>健康快照</h2>
|
||||
<pre class="json-preview tall">{{ ctx.pretty(ctx.healthSnapshot) }}</pre>
|
||||
</section>
|
||||
</template>
|
||||
@@ -24,6 +24,10 @@ defineProps<{ ctx: any }>();
|
||||
<button :class="{ active: ctx.legacyDrafts[ctx.activeLegacyName].tab === 'history' }" @click="ctx.legacyDrafts[ctx.activeLegacyName].tab = 'history'">历史版本</button>
|
||||
</div>
|
||||
|
||||
<p v-if="ctx.activeLegacyName === 'media-types'" class="notice">
|
||||
生产环境不再自动依赖旧项目路径。需要以 server/update/public/media-types.json 为基板时,请切换到 Raw JSON 粘贴完整内容,校验通过后保存发布。
|
||||
</p>
|
||||
|
||||
<section v-if="ctx.legacyDrafts[ctx.activeLegacyName].tab === 'form' && ctx.activeLegacyName === 'update-info'" class="form-grid">
|
||||
<label>版本号<input v-model="ctx.legacyDrafts['update-info'].form.app_version" /></label>
|
||||
<label>标题<input v-model="ctx.legacyDrafts['update-info'].form.title" /></label>
|
||||
|
||||
@@ -25,7 +25,22 @@ defineProps<{ ctx: any }>();
|
||||
<label class="wide">发布说明<textarea v-model="ctx.uploadDraft.notes" rows="3"></textarea></label>
|
||||
<label class="checkbox wide"><input v-model="ctx.uploadDraft.updateManifest" type="checkbox" />上传后同步更新兼容 update-info.json</label>
|
||||
</div>
|
||||
<button class="btn primary" @click="ctx.uploadPackage"><UploadCloud :size="16" />上传发布包</button>
|
||||
<div v-if="ctx.uploadDraft.file || ctx.uploadDraft.uploading || ctx.uploadDraft.status" class="upload-progress">
|
||||
<div class="upload-progress-head">
|
||||
<strong>{{ ctx.uploadDraft.status || "等待上传" }}</strong>
|
||||
<span>{{ ctx.uploadDraft.progress || 0 }}%</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<span :style="{ width: `${ctx.uploadDraft.progress || 0}%` }"></span>
|
||||
</div>
|
||||
<small>
|
||||
{{ ctx.uploadDraft.file?.name || "发布包" }}
|
||||
<template v-if="ctx.uploadDraft.totalBytes">
|
||||
· {{ ctx.formatBytes(ctx.uploadDraft.loadedBytes || 0) }} / {{ ctx.formatBytes(ctx.uploadDraft.totalBytes || 0) }}
|
||||
</template>
|
||||
</small>
|
||||
</div>
|
||||
<button class="btn primary" :disabled="ctx.uploadDraft.uploading" @click="ctx.uploadPackage"><UploadCloud :size="16" />{{ ctx.uploadDraft.uploading ? "上传中" : "上传发布包" }}</button>
|
||||
</section>
|
||||
<table>
|
||||
<thead><tr><th>文件</th><th>版本</th><th>平台</th><th>大小</th><th>SHA256</th></tr></thead>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { KeyRound } from "lucide-vue-next";
|
||||
|
||||
defineProps<{ ctx: any }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="split">
|
||||
<section class="panel editor-panel">
|
||||
<h2>修改后台密码</h2>
|
||||
<label>当前密码<input v-model="ctx.passwordForm.currentPassword" type="password" /></label>
|
||||
<label>新密码<input v-model="ctx.passwordForm.newPassword" type="password" /></label>
|
||||
<button class="btn primary" @click="ctx.changePassword"><KeyRound :size="16" />保存密码</button>
|
||||
</section>
|
||||
<section class="panel page-stack">
|
||||
<div class="section-head"><h2>旧项目同步预览</h2><button class="btn ghost" @click="ctx.previewLegacySync">刷新预览</button></div>
|
||||
<pre class="json-preview">{{ ctx.pretty(ctx.legacySync) }}</pre>
|
||||
<button class="btn primary" @click="ctx.runLegacySync">执行同步</button>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { Activity, AlertTriangle, ArrowDownUp, Clock3, Database, KeyRound, ListChecks, RefreshCw, ShieldCheck } from "lucide-vue-next";
|
||||
|
||||
defineProps<{ ctx: any }>();
|
||||
|
||||
const tabs = [
|
||||
{ id: "database", label: "数据库", icon: Database },
|
||||
{ id: "sync", label: "旧项目同步", icon: RefreshCw },
|
||||
{ id: "security", label: "安全设置", icon: ShieldCheck },
|
||||
{ id: "health", label: "健康快照", icon: Activity },
|
||||
{ id: "audit", label: "审计日志", icon: ListChecks },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-stack">
|
||||
<nav class="tabs" aria-label="系统运维标签">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
:class="{ active: ctx.systemTab === tab.id }"
|
||||
@click="ctx.setSystemTab(tab.id)"
|
||||
>
|
||||
<component :is="tab.icon" :size="15" />
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<section v-if="ctx.systemTab === 'database'" class="split">
|
||||
<section class="panel page-stack">
|
||||
<div class="section-head">
|
||||
<h2>数据库运行状态</h2>
|
||||
<span :class="['badge', ctx.statusTone(ctx.database?.activeProvider)]">{{ ctx.database?.activeProvider || "-" }}</span>
|
||||
</div>
|
||||
<div class="kv-grid">
|
||||
<span>配置类型</span><strong>{{ ctx.database?.configProvider || "-" }}</strong>
|
||||
<span>SQLite</span><strong>{{ ctx.database?.sqliteReady ? "ready" : "missing" }}</strong>
|
||||
<span>MySQL</span><strong>{{ ctx.database?.remoteReady ? "ready" : "offline" }}</strong>
|
||||
<span>Failover</span><strong>{{ ctx.database?.failoverActive ? "active" : "standby" }}</strong>
|
||||
<span>最后同步</span><strong>{{ ctx.database?.lastSyncAt || "-" }}</strong>
|
||||
<span>最近错误</span><strong>{{ ctx.database?.lastSyncError || ctx.database?.lastError || "-" }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="sync-summary">
|
||||
<div>
|
||||
<span><ArrowDownUp :size="15" />最近同步方向</span>
|
||||
<strong>{{ ctx.databaseSyncDirectionLabel(ctx.databaseLastSync?.direction) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span><ListChecks :size="15" />影响记录</span>
|
||||
<strong>{{ ctx.databaseSyncTableCount(ctx.databaseLastSync) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span><Clock3 :size="15" />完成时间</span>
|
||||
<strong>{{ ctx.databaseLastSync?.finishedAt || ctx.database?.lastSyncAt || "-" }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ops-note">
|
||||
<AlertTriangle :size="16" />
|
||||
<span>数据库同步是覆盖式全表 upsert。执行前确认方向:SQLite 导入远端会以本地库为源,远端同步回本地会以 MySQL 为源。</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="panel editor-panel">
|
||||
<h2>连接与同步</h2>
|
||||
<label>Provider<select v-model="ctx.databaseForm.provider"><option>sqlite</option><option>mysql</option></select></label>
|
||||
<label>SQLite 路径<input v-model="ctx.databaseForm.sqlitePath" placeholder="留空使用当前配置" /></label>
|
||||
<label>MySQL DSN<input v-model="ctx.databaseForm.mysqlDsn" placeholder="user:pass@tcp(host:3306)/db?parseTime=true" /></label>
|
||||
<div class="button-row">
|
||||
<button class="btn ghost" @click="ctx.testDatabase">测试连接</button>
|
||||
<button class="btn primary" @click="ctx.syncDatabase('import')">SQLite 导入远端</button>
|
||||
<button class="btn ghost" @click="ctx.syncDatabase('sync')">远端同步回本地</button>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section v-else-if="ctx.systemTab === 'sync'" class="panel page-stack">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>旧项目同步</h2>
|
||||
<p class="muted">预览只检查旧目录和影响范围;执行会先备份当前发布目录,再复制旧项目数据并导入反馈记录。</p>
|
||||
</div>
|
||||
<button class="btn ghost" @click="ctx.previewLegacySync"><RefreshCw :size="16" />刷新预览</button>
|
||||
</div>
|
||||
|
||||
<div class="sync-summary">
|
||||
<div>
|
||||
<span>当前模式</span>
|
||||
<strong>{{ ctx.legacySyncMode === "run" || ctx.legacySync?.dryRun === false ? "执行结果" : "Dry-run 预览" }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>状态</span>
|
||||
<strong>{{ ctx.legacySync?.ok === false ? "存在错误" : "可继续" }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>完成时间</span>
|
||||
<strong>{{ ctx.legacySync?.finishedAt || "-" }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kv-grid">
|
||||
<span>复制文件</span><strong>{{ ctx.legacySync?.stats?.copiedFiles || 0 }}</strong>
|
||||
<span>复制目录</span><strong>{{ ctx.legacySync?.stats?.copiedDirectories || 0 }}</strong>
|
||||
<span>导入记录</span><strong>{{ ctx.legacySync?.stats?.importedRows || 0 }}</strong>
|
||||
<span>缺失路径</span><strong>{{ ctx.legacySync?.stats?.missingPaths || 0 }}</strong>
|
||||
</div>
|
||||
|
||||
<pre class="json-preview tall">{{ ctx.pretty(ctx.legacySync) }}</pre>
|
||||
<div class="button-row">
|
||||
<button class="btn primary" @click="ctx.runLegacySync">执行旧项目同步</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="ctx.systemTab === 'security'" class="split">
|
||||
<section class="panel editor-panel">
|
||||
<h2>修改后台密码</h2>
|
||||
<label>当前密码<input v-model="ctx.passwordForm.currentPassword" type="password" autocomplete="current-password" /></label>
|
||||
<label>新密码<input v-model="ctx.passwordForm.newPassword" type="password" autocomplete="new-password" /></label>
|
||||
<button class="btn primary" @click="ctx.changePassword"><KeyRound :size="16" />保存密码</button>
|
||||
</section>
|
||||
|
||||
<section class="panel page-stack">
|
||||
<div class="section-head"><h2>当前安全策略</h2><span class="badge good">已启用</span></div>
|
||||
<div class="kv-grid">
|
||||
<span>登录保护</span><strong>验证码 + 连续失败限流</strong>
|
||||
<span>写操作保护</span><strong>HttpOnly Session + CSRF Token</strong>
|
||||
<span>Cookie</span><strong>HTTPS 或 X-Forwarded-Proto=https 时自动 Secure</strong>
|
||||
<span>会话范围</span><strong>后台 API 与 SSE 事件流均要求登录</strong>
|
||||
<span>密码规则</span><strong>至少 8 位,不能为 admin,不能与当前密码相同</strong>
|
||||
<span>兼容哈希</span><strong>保留 SHA-256 登录兼容,后续可平滑迁移到更强算法</strong>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section v-else-if="ctx.systemTab === 'health'" class="panel page-stack">
|
||||
<div class="section-head"><h2>健康快照</h2><span class="badge neutral">只读</span></div>
|
||||
<pre class="json-preview tall">{{ ctx.pretty(ctx.healthSnapshot) }}</pre>
|
||||
</section>
|
||||
|
||||
<section v-else class="panel page-stack">
|
||||
<div class="section-head"><h2>审计日志</h2><button class="btn ghost" @click="ctx.loadAudit">刷新</button></div>
|
||||
<table>
|
||||
<thead><tr><th>类型</th><th>目标</th><th>信息</th><th>IP</th><th>时间</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="item in ctx.auditLogs" :key="item.id">
|
||||
<td><span class="badge neutral">{{ ctx.auditTypeLabel(item.type) }}</span></td>
|
||||
<td>{{ item.target }}</td>
|
||||
<td>{{ ctx.auditMessage(item) }}</td>
|
||||
<td>{{ item.ip || "-" }}</td>
|
||||
<td>{{ item.createdAt }}</td>
|
||||
</tr>
|
||||
<tr v-if="ctx.auditLogs.length === 0"><td colspan="5">暂无审计日志。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user