Add server components
build-winui / winui (push) Has been cancelled

This commit is contained in:
QWQLwToo
2026-06-26 13:28:09 +08:00
parent 7ecc6a8923
commit 079ee4eaeb
168 changed files with 37475 additions and 0 deletions
@@ -0,0 +1,22 @@
<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>{{ item.type }}</td>
<td>{{ item.target }}</td>
<td>{{ item.message }}</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>
@@ -0,0 +1,84 @@
<script setup lang="ts">
import VChart from "vue-echarts";
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { BarChart, GaugeChart, LineChart, PieChart } from "echarts/charts";
import { GridComponent, LegendComponent, TooltipComponent } from "echarts/components";
import { Activity, PauseCircle, PlayCircle } from "lucide-vue-next";
defineProps<{ ctx: any }>();
use([CanvasRenderer, LineChart, PieChart, BarChart, GaugeChart, GridComponent, TooltipComponent, LegendComponent]);
</script>
<template>
<section class="page-stack">
<div class="metric-grid">
<article class="metric"><span>反馈总数</span><strong>{{ ctx.kpis.feedbackTotal || 0 }}</strong><small>今日新增 {{ ctx.kpis.feedbackToday || 0 }}</small></article>
<article class="metric"><span>可见接口</span><strong>{{ ctx.kpis.sourceVisible || 0 }}</strong><small>接口总数 {{ ctx.kpis.sourceTotal || 0 }}</small></article>
<article class="metric"><span>版本日志</span><strong>{{ ctx.kpis.releaseNotices || 0 }}</strong><small>{{ ctx.latestNotice?.version || "暂无最新版本" }}</small></article>
<article class="metric"><span>邮件失败</span><strong>{{ ctx.kpis.mailFailed || 0 }}</strong><small>旧反馈兼容记录</small></article>
</div>
<div class="toolbar">
<button class="btn primary" @click="ctx.checkSources"><Activity :size="16" />立即心跳检测</button>
<button class="btn ghost" @click="ctx.toggleAutoRefresh">
<component :is="ctx.autoRefreshPaused ? PlayCircle : PauseCircle" :size="16" />
{{ ctx.autoRefreshPaused ? "恢复自动刷新" : "暂停自动刷新" }}
</button>
<span class="muted"> 15 秒自动刷新仪表盘数据</span>
</div>
<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>
<section class="panel chart-panel"><h2>反馈状态分布</h2><VChart class="chart" :option="ctx.feedbackOption" autoresize /></section>
<section class="panel chart-panel"><h2>服务可用率</h2><VChart class="chart" :option="ctx.availabilityOption" autoresize /></section>
</div>
<section class="panel">
<div class="section-head"><h2>最近接口心跳</h2><span class="badge">{{ ctx.heartbeats.length }} </span></div>
<table>
<thead><tr><th>接口</th><th>状态</th><th>延迟</th><th>错误</th><th>时间</th></tr></thead>
<tbody>
<tr v-for="item in ctx.heartbeats.slice(0, 10)" :key="item.id">
<td>{{ item.name || item.sourceId }}</td>
<td><span :class="['badge', ctx.statusTone(item.status)]">{{ ctx.labelStatus(item.status) }}</span></td>
<td>{{ item.latencyMs || 0 }}ms</td>
<td class="hash">{{ item.error || "-" }}</td>
<td>{{ item.checkedAt || "-" }}</td>
</tr>
<tr v-if="ctx.heartbeats.length === 0"><td colspan="5">暂无心跳记录点击立即心跳检测后会刷新</td></tr>
</tbody>
</table>
</section>
<section class="panel">
<div class="section-head"><h2>客户端调用上报</h2><span class="badge">{{ ctx.clientCalls.length }} </span></div>
<table>
<thead><tr><th>接口</th><th>状态</th><th>延迟</th><th>客户端</th><th>时间</th></tr></thead>
<tbody>
<tr v-for="item in ctx.clientCalls.slice(0, 8)" :key="item.id">
<td>{{ item.sourceId }}</td>
<td><span :class="['badge', ctx.statusTone(item.status)]">{{ ctx.labelStatus(item.status) }}</span></td>
<td>{{ item.latencyMs || 0 }}ms</td>
<td class="hash">{{ item.client || "-" }}</td>
<td>{{ item.createdAt || "-" }}</td>
</tr>
<tr v-if="ctx.clientCalls.length === 0"><td colspan="5">暂无客户端调用上报</td></tr>
</tbody>
</table>
</section>
</section>
</template>
@@ -0,0 +1,31 @@
<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>
@@ -0,0 +1,24 @@
<script setup lang="ts">
defineProps<{ ctx: any }>();
</script>
<template>
<section class="panel page-stack">
<div class="section-head"><h2>客户端动态接口</h2><span class="badge">{{ ctx.visibleEndpointCount }} 可见 / {{ ctx.healthyEndpointCount }} 健康</span></div>
<table>
<thead><tr><th>ID</th><th>分类</th><th>模式</th><th>健康</th><th>缓存</th><th>URL</th><th></th></tr></thead>
<tbody>
<tr v-for="item in ctx.endpoints" :key="item.id || item.sourceId">
<td class="mono">{{ item.id || item.sourceId }}</td>
<td>{{ item.category || item.categoryId }}</td>
<td>{{ item.proxyMode }}</td>
<td><span :class="['badge', ctx.statusTone(ctx.endpointStatus(item))]">{{ ctx.endpointStatus(item) }}</span></td>
<td>{{ item.cacheSeconds || 0 }}s</td>
<td class="hash">{{ item.urlTemplate || item.apiUrl }}</td>
<td><button class="btn ghost compact" @click="ctx.copyEndpointToSource(item)">编辑</button></td>
</tr>
<tr v-if="ctx.endpoints.length === 0"><td colspan="7">暂无客户端接口</td></tr>
</tbody>
</table>
</section>
</template>
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { Save, Search, UploadCloud } from "lucide-vue-next";
defineProps<{ ctx: any }>();
</script>
<template>
<section class="split">
<section class="panel page-stack">
<div class="toolbar">
<label class="search-box"><Search :size="16" /><input v-model="ctx.feedbackFilters.q" placeholder="搜索编号、标题、联系方式" @keyup.enter="ctx.loadFeedbacks" /></label>
<select v-model="ctx.feedbackFilters.status" @change="ctx.loadFeedbacks"><option value="">全部状态</option><option value="new">new</option><option value="processing">processing</option><option value="closed">closed</option></select>
<button class="btn ghost" @click="ctx.loadFeedbacks">查询</button>
<a class="btn ghost" href="/api/admin/feedbacks/export" target="_blank"><UploadCloud :size="16" />CSV</a>
</div>
<table>
<thead><tr><th>编号</th><th>标题</th><th>状态</th><th>优先级</th><th>最近活动</th></tr></thead>
<tbody>
<tr v-for="item in ctx.feedbackPage.items" :key="item.code" class="clickable" :class="{ selected: ctx.selectedFeedback?.code === item.code }" @click="ctx.openFeedback(item)">
<td class="mono">{{ item.code }}</td>
<td>{{ item.title || item.summaryText || "未命名反馈" }}</td>
<td><span :class="['badge', ctx.statusTone(item.status)]">{{ item.status }}</span></td>
<td>{{ item.priority || "-" }}</td>
<td>{{ item.lastActivityAt || item.createdAt }}</td>
</tr>
<tr v-if="!ctx.feedbackPage.items?.length"><td colspan="5">暂无反馈工单</td></tr>
</tbody>
</table>
</section>
<aside class="panel detail-panel">
<template v-if="ctx.selectedFeedback">
<h2>{{ ctx.selectedFeedback.code }}</h2>
<p class="muted">{{ ctx.selectedFeedback.title || ctx.selectedFeedback.body }}</p>
<label>状态<select v-model="ctx.feedbackUpdate.status"><option>new</option><option>processing</option><option>closed</option></select></label>
<label>状态说明<input v-model="ctx.feedbackUpdate.statusDetail" /></label>
<label>公开回复<textarea v-model="ctx.feedbackUpdate.publicReply" rows="3"></textarea></label>
<button class="btn primary" @click="ctx.saveFeedbackUpdate"><Save :size="16" />保存状态</button>
<hr />
<h3>评论</h3>
<div class="comment-list">
<div v-for="item in ctx.selectedFeedback.comments || []" :key="item.id" class="comment"><strong>{{ item.author }}</strong><p>{{ item.body }}</p></div>
</div>
<label>新增评论<textarea v-model="ctx.commentDraft.body" rows="3"></textarea></label>
<label class="checkbox"><input v-model="ctx.commentDraft.internal" type="checkbox" />内部备注</label>
<button class="btn ghost" @click="ctx.addFeedbackComment">添加评论</button>
<details>
<summary>旧反馈事件 / 邮件记录</summary>
<pre class="json-preview small">{{ ctx.pretty({ events: ctx.selectedFeedback.legacyEvents, mail: ctx.selectedFeedback.mailRecords }) }}</pre>
</details>
</template>
<div v-else class="empty-state">选择一条工单查看详情</div>
</aside>
</section>
</template>
@@ -0,0 +1,10 @@
<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>
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { CheckCircle2, Save } from "lucide-vue-next";
defineProps<{ ctx: any }>();
</script>
<template>
<section class="split wide-split">
<section class="panel editor-panel">
<div class="section-head">
<h2>{{ ctx.activeLegacyLabel }}</h2>
<div class="button-row">
<button class="btn ghost" @click="ctx.validateLegacy(ctx.activeLegacyName)"><CheckCircle2 :size="16" />校验</button>
<button class="btn primary" @click="ctx.saveLegacy(ctx.activeLegacyName)"><Save :size="16" />保存发布</button>
</div>
</div>
<textarea v-model="ctx.legacyDrafts[ctx.activeLegacyName].raw" class="code-editor"></textarea>
<label>保存备注<input v-model="ctx.legacyDrafts[ctx.activeLegacyName].note" /></label>
</section>
<aside class="panel page-stack">
<h2>预览与历史</h2>
<pre class="json-preview">{{ ctx.pretty(ctx.legacyDrafts[ctx.activeLegacyName].preview) }}</pre>
<div class="revision-list">
<button v-for="revision in ctx.legacyDocuments[ctx.activeLegacyName]?.revisions || []" :key="revision.id" @click="ctx.restoreLegacy(ctx.activeLegacyName, revision.id)">
#{{ revision.id }} {{ revision.createdAt }}<small>{{ revision.note || "无备注" }}</small>
</button>
</div>
</aside>
</section>
</template>
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { CheckCircle2, Save } from "lucide-vue-next";
defineProps<{ ctx: any }>();
</script>
<template>
<section class="split wide-split">
<section class="panel page-stack">
<div class="section-head">
<h2>发布包</h2>
<a href="/update-info.json" target="_blank">查看旧版 update-info.json</a>
</div>
<table>
<thead><tr><th>文件</th><th>版本</th><th>平台</th><th>大小</th><th>SHA256</th></tr></thead>
<tbody>
<tr v-for="pkg in ctx.releasePackages" :key="pkg.fileName || pkg.url">
<td>{{ pkg.fileName || pkg.name }}</td>
<td>{{ pkg.version || ctx.releases?.app_version || "-" }}</td>
<td>{{ pkg.platform || "-" }}/{{ pkg.arch || "-" }}</td>
<td>{{ ctx.formatBytes(pkg.sizeBytes || pkg.size || 0) }}</td>
<td class="hash">{{ pkg.sha256 || "-" }}</td>
</tr>
<tr v-if="ctx.releasePackages.length === 0"><td colspan="5">暂无可见发布包</td></tr>
</tbody>
</table>
</section>
<aside class="panel editor-panel">
<div class="section-head"><h2>版本日志</h2><button class="btn ghost" @click="ctx.importNotices">导入目录</button></div>
<div class="revision-list">
<button v-for="item in ctx.releaseNotices" :key="item.version" :class="{ active: ctx.noticeDraft.version === item.version }" @click="ctx.openNotice(item.version)">
<strong>{{ item.version }}</strong><small>{{ item.title || item.updatedAt }}</small>
</button>
<div v-if="ctx.releaseNotices.length === 0" class="empty-state compact">暂无版本日志可先执行导入</div>
</div>
<label>版本<input v-model="ctx.noticeDraft.version" placeholder="1.0.0" /></label>
<label>Raw JSON<textarea v-model="ctx.noticeDraft.raw" class="code-editor compact-editor"></textarea></label>
<label>备注<input v-model="ctx.noticeDraft.note" /></label>
<div class="button-row">
<button class="btn ghost" @click="ctx.validateNotice"><CheckCircle2 :size="16" />校验</button>
<button class="btn primary" @click="ctx.saveNotice"><Save :size="16" />保存日志</button>
</div>
<details v-if="ctx.selectedNotice?.revisions?.length">
<summary>历史版本</summary>
<div class="revision-list">
<button v-for="revision in ctx.selectedNotice.revisions" :key="revision.id" @click="ctx.restoreNotice(revision.id)">#{{ revision.id }} {{ revision.createdAt }}</button>
</div>
</details>
</aside>
</section>
</template>
@@ -0,0 +1,21 @@
<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,43 @@
<script setup lang="ts">
defineProps<{ ctx: any }>();
</script>
<template>
<section class="split">
<section class="panel page-stack">
<div class="section-head"><h2>媒体/数据源</h2><button class="btn primary" @click="ctx.checkSources">批量检测</button></div>
<div v-for="cat in ctx.sourceCategories" :key="cat.id || cat.name" class="source-group">
<h3>{{ cat.name || cat.id }} <span class="badge">{{ cat.subcategories?.length || 0 }}</span></h3>
<table>
<thead><tr><th>名称</th><th>模式</th><th>状态</th><th>延迟</th><th>URL</th></tr></thead>
<tbody>
<tr v-for="src in cat.subcategories || []" :key="src.id || src.sourceId">
<td>{{ src.name }}</td>
<td>{{ src.proxyMode || src.proxy_mode || "client_direct" }}</td>
<td><span :class="['badge', ctx.statusTone(src.health?.status || src.lastStatus)]">{{ src.health?.status || src.lastStatus || "unknown" }}</span></td>
<td>{{ src.health?.latency_ms || src.lastLatencyMs || 0 }}ms</td>
<td class="hash">{{ src.api_url || src.urlTemplate || src.apiUrl }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="ctx.sourceCategories.length === 0" class="empty-state">暂无接口源可从旧 media-types.json 导入或手动添加</div>
</section>
<aside class="panel editor-panel">
<h2>添加/覆盖接口</h2>
<label>ID<input v-model="ctx.sourceDraft.sourceId" /></label>
<label>名称<input v-model="ctx.sourceDraft.name" /></label>
<label>分类 ID<input v-model="ctx.sourceDraft.categoryId" /></label>
<label>分类名称<input v-model="ctx.sourceDraft.categoryName" /></label>
<label>URL<input v-model="ctx.sourceDraft.apiUrl" /></label>
<label>代理模式<select v-model="ctx.sourceDraft.proxyMode"><option>client_direct</option><option>server_proxy</option><option>disabled</option></select></label>
<div class="two-col">
<label>缓存秒数<input v-model.number="ctx.sourceDraft.cacheSeconds" type="number" /></label>
<label>检测间隔<input v-model.number="ctx.sourceDraft.checkIntervalSec" type="number" /></label>
</div>
<label class="checkbox"><input v-model="ctx.sourceDraft.enabled" type="checkbox" />启用</label>
<label class="checkbox"><input v-model="ctx.sourceDraft.clientVisible" type="checkbox" />客户端可见</label>
<button class="btn primary" @click="ctx.saveSource">保存接口</button>
</aside>
</section>
</template>