更新了update门户站点界面和部分功能

This commit is contained in:
QWQLwToo
2026-06-26 14:30:09 +08:00
parent 57f4d94d0a
commit cd2fd435a2
20 changed files with 1128 additions and 168 deletions
@@ -1,12 +1,9 @@
<script setup lang="ts">
const routes = [
{ path: "/api/client/bootstrap", label: "新版客户端 Bootstrap" },
{ path: "/api/client/releases", label: "新版发布信息" },
{ path: "/api/client/sources", label: "新版接口源目录" },
{ path: "/update-info.json", label: "旧版更新 JSON" },
{ path: "/media-types.json", label: "旧版媒体源 JSON" },
{ path: "/tool-status.json", label: "旧版工具状态" },
{ path: "/modules.json", label: "旧版模块清单" },
const items = [
{ title: "旧版更新能力", body: "客户端继续按原有方式读取更新信息、工具状态、模块清单和下载包。" },
{ title: "旧版媒体源能力", body: "媒体源结构保持旧字段兼容,后台保存后会同步到旧客户端可读结构。" },
{ title: "新版动态配置", body: "新版客户端优先从服务端读取发布、接口源、健康状态和缓存策略,失败时回退旧路径。" },
{ title: "反馈兼容", body: "旧反馈提交和状态查询入口继续保留,后台统一沉淀为反馈工单。" },
];
</script>
@@ -14,16 +11,13 @@ const routes = [
<section class="page-heading">
<p class="eyebrow">Compatibility</p>
<h1>兼容说明</h1>
<p>新旧客户端共用 update.ymhut.cn旧路径和旧 JSON 字段继续保留</p>
<p>新旧客户端共用 update.ymhut.cn门户只展示能力说明具体接口由客户端自动选择</p>
</section>
<section class="panel wide">
<h2>公开路径</h2>
<div class="route-list">
<a v-for="item in routes" :key="item.path" :href="item.path">
<strong>{{ item.path }}</strong>
<span>{{ item.label }}</span>
</a>
</div>
<section class="content-grid">
<article v-for="item in items" :key="item.title" class="panel compat-card">
<h2>{{ item.title }}</h2>
<p>{{ item.body }}</p>
</article>
</section>
</template>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Activity, ArrowDownToLine, Database, ExternalLink, HeartPulse, Network, ShieldCheck } from "lucide-vue-next";
import { Activity, ArrowDownToLine, Database, HeartPulse, Network, ShieldCheck } from "lucide-vue-next";
import { usePortalState } from "../state";
const state = usePortalState();
@@ -10,14 +10,12 @@ const state = usePortalState();
<div class="hero-copy">
<p class="eyebrow">update.ymhut.cn</p>
<h1>统一发布反馈与接口源状态门户</h1>
<p>
新版客户端通过 bootstrap 动态获取发布信息版本日志媒体/数据源目录和接口健康状态旧客户端仍可继续访问
update-info.jsonmedia-types.json下载路径和反馈根路径
</p>
<p>统一展示 YMhut Box 的发布状态反馈入口接口源可用性与版本日志新版客户端动态读取服务配置旧客户端兼容能力继续保留</p>
<div class="actions">
<a class="button primary" :href="state.downloadUrl.value"><ArrowDownToLine :size="18" />下载最新版本</a>
<a class="button" href="/api/client/bootstrap"><ShieldCheck :size="18" />客户端配置</a>
<RouterLink class="button" to="/compatibility"><ExternalLink :size="18" />兼容路径</RouterLink>
<a v-if="state.downloadUrl.value" class="button primary" :href="state.downloadUrl.value"><ArrowDownToLine :size="18" />下载最新版本</a>
<RouterLink v-else class="button primary" to="/releases"><ArrowDownToLine :size="18" />查看发布状态</RouterLink>
<RouterLink class="button" to="/sources"><ShieldCheck :size="18" />查看接口状态</RouterLink>
<RouterLink class="button" to="/compatibility">兼容说明</RouterLink>
</div>
<div class="hero-tags">
<span>Legacy JSON 兼容</span>
@@ -47,7 +45,7 @@ const state = usePortalState();
<section class="content-grid">
<article class="panel">
<div class="section-head"><h2>服务入口</h2><a href="/api/client/bootstrap">Bootstrap <ExternalLink :size="14" /></a></div>
<div class="section-head"><h2>服务入口</h2><span class="badge good">运行中</span></div>
<div class="route-list">
<RouterLink to="/releases"><strong>发布版本</strong><span>下载包版本公告和 update-notice 日志</span></RouterLink>
<RouterLink to="/sources"><strong>接口源健康</strong><span>媒体源数据源和动态客户端接口状态</span></RouterLink>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { BookOpenText, ExternalLink } from "lucide-vue-next";
import { BookOpenText } from "lucide-vue-next";
import { usePortalState } from "../state";
const state = usePortalState();
@@ -14,7 +14,7 @@ const state = usePortalState();
<section class="content-grid">
<article class="panel wide">
<div class="section-head"><h2>发布包</h2><a href="/update-info.json">旧版 update-info.json <ExternalLink :size="14" /></a></div>
<div class="section-head"><h2>发布包</h2><span class="badge">{{ state.packages.value.length }} 个可用包</span></div>
<table>
<thead><tr><th>文件</th><th>版本</th><th>平台</th><th>大小</th><th></th></tr></thead>
<tbody>
@@ -31,7 +31,7 @@ const state = usePortalState();
</article>
<article class="panel wide">
<div class="section-head"><h2>版本日志</h2><a href="/api/client/notices">Notices API <ExternalLink :size="14" /></a></div>
<div class="section-head"><h2>版本日志</h2><span class="badge good">自动同步</span></div>
<div class="notice-list">
<section v-for="notice in state.notices.value" :key="notice.version" class="notice-card">
<BookOpenText :size="22" />
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { CheckCircle2, ExternalLink } from "lucide-vue-next";
import { CheckCircle2 } from "lucide-vue-next";
import { usePortalState } from "../state";
const state = usePortalState();
@@ -13,7 +13,7 @@ const state = usePortalState();
</section>
<section class="panel wide">
<div class="section-head"><h2>接口源可用性</h2><a href="/api/client/sources">Sources API <ExternalLink :size="14" /></a></div>
<div class="section-head"><h2>接口源可用性</h2><span class="badge">{{ state.sourceCount.value }} 个接口源</span></div>
<div v-if="state.categories.value.length" class="source-board">
<section v-for="cat in state.categories.value" :key="cat.id || cat.name" class="source-group">
<div>
@@ -22,11 +22,11 @@ const state = usePortalState();
</div>
<div class="source-list">
<span v-for="src in cat.subcategories || []" :key="src.id || src.sourceId" :class="['badge', state.statusTone(state.sourceStatus(src))]">
<CheckCircle2 :size="13" />{{ src.name }}
<CheckCircle2 :size="13" />{{ src.name }}<small v-if="state.sourceStatus(src) === 'redirected'">重定向</small>
</span>
</div>
</section>
</div>
<p v-else class="empty">暂无接口源数据后台同步旧 media-types.json 或手动添加后会显示在这里</p>
<p v-else class="empty">暂无接口源数据后台同步旧媒体源配置或手动添加后会显示在这里</p>
</section>
</template>
@@ -23,7 +23,7 @@ export function usePortalState() {
return total + (cat.subcategories || []).filter((item: any) => sourceStatus(item) === "ok").length;
}, 0));
const availability = computed(() => sourceCount.value ? Math.round((healthyCount.value / sourceCount.value) * 100) : 0);
const downloadUrl = computed(() => releases.value?.download_url || bootstrap.value?.release?.download_url || "/update-info.json");
const downloadUrl = computed(() => releases.value?.download_url || bootstrap.value?.release?.download_url || packages.value[0]?.url || "");
const appVersion = computed(() => releases.value?.app_version || bootstrap.value?.release?.app_version || latestNotice.value?.version || "未发布");
const databaseStatus = computed(() => bootstrap.value?.health?.database?.activeProvider || bootstrap.value?.health?.database?.configProvider || "-");
const serviceVersion = computed(() => bootstrap.value?.serviceVersion || "-");
@@ -83,7 +83,7 @@ export function sourceStatus(item: any) {
export function statusTone(status: string) {
const value = String(status || "").toLowerCase();
if (["ok", "sqlite", "mysql", "online", "ready"].includes(value)) return "good";
if (["ok", "redirected", "sqlite", "mysql", "online", "ready"].includes(value)) return "good";
if (["degraded", "pending", "missing"].includes(value)) return "warn";
if (["error", "offline", "failed"].includes(value)) return "bad";
return "neutral";
@@ -2,23 +2,22 @@
color-scheme: light;
font-family: "Microsoft YaHei UI", "Segoe UI", Arial, sans-serif;
color: #172033;
background: #f7f9ff;
background: #f5f7f4;
--ink: #172033;
--muted: #63718a;
--soft: #f7f9ff;
--soft: #f5f7f4;
--panel: rgba(255, 255, 255, 0.82);
--panel-strong: #ffffff;
--line: rgba(112, 132, 170, 0.18);
--line-strong: rgba(94, 114, 158, 0.28);
--primary: #3b82f6;
--primary-dark: #2563eb;
--cyan: #06b6d4;
--violet: #8b5cf6;
--pink: #f472b6;
--primary: #1f6f5b;
--primary-dark: #155241;
--accent: #d99227;
--good: #059669;
--warn: #b7791f;
--bad: #dc2626;
--shadow: 0 22px 65px rgba(65, 88, 140, 0.16);
--shadow: 0 22px 65px rgba(31, 48, 40, 0.12);
--ease: cubic-bezier(.2,.8,.2,1);
}
* { box-sizing: border-box; }
@@ -27,9 +26,9 @@ body {
margin: 0;
min-width: 320px;
background:
radial-gradient(circle at 8% 6%, rgba(96, 165, 250, 0.30), transparent 28%),
radial-gradient(circle at 88% 8%, rgba(244, 114, 182, 0.20), transparent 30%),
linear-gradient(180deg, #eef6ff 0%, #f8fbff 42%, #ffffff 100%);
radial-gradient(circle at 8% 6%, rgba(31, 111, 91, 0.10), transparent 30%),
radial-gradient(circle at 90% 8%, rgba(217, 146, 39, 0.10), transparent 30%),
linear-gradient(180deg, #f2f5ef 0%, #f8faf6 42%, #ffffff 100%);
}
body::before {
content: "";
@@ -37,8 +36,8 @@ body::before {
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(59, 130, 246, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(59, 130, 246, 0.05) 1px, transparent 1px);
linear-gradient(rgba(31, 111, 91, 0.045) 1px, transparent 1px),
linear-gradient(90deg, rgba(31, 111, 91, 0.045) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.55), transparent 70%);
}
@@ -87,8 +86,8 @@ button { cursor: pointer; }
place-items: center;
border-radius: 50%;
color: #fff;
background: linear-gradient(135deg, var(--primary), var(--cyan));
box-shadow: 0 12px 26px rgba(37, 99, 235, 0.26);
background: linear-gradient(135deg, #10231d, var(--primary));
box-shadow: 0 12px 26px rgba(31, 111, 91, 0.22);
}
.brand strong { letter-spacing: 0; }
.nav-links {
@@ -108,18 +107,19 @@ button { cursor: pointer; }
text-decoration: none;
font-size: 14px;
font-weight: 800;
transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
transition: transform 0.18s var(--ease), background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.nav-links a:hover, .nav-links a.active {
color: var(--primary-dark);
background: rgba(59, 130, 246, 0.12);
background: rgba(31, 111, 91, 0.10);
transform: translateY(-1px);
}
.admin-link {
color: #fff;
background: linear-gradient(135deg, #2563eb, #7c3aed);
box-shadow: 0 12px 28px rgba(59, 130, 246, 0.24);
background: linear-gradient(135deg, #10231d, #1f6f5b);
box-shadow: 0 12px 28px rgba(31, 111, 91, 0.22);
}
.admin-link:hover { box-shadow: 0 16px 36px rgba(59, 130, 246, 0.32); }
.admin-link:hover { transform: translateY(-1px); box-shadow: 0 16px 36px rgba(31, 111, 91, 0.28); }
.hero {
position: relative;
@@ -134,8 +134,8 @@ button { cursor: pointer; }
border-radius: 32px;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.62)),
radial-gradient(circle at 88% 18%, rgba(14, 165, 233, 0.26), transparent 34%),
radial-gradient(circle at 18% 82%, rgba(139, 92, 246, 0.18), transparent 30%);
radial-gradient(circle at 88% 18%, rgba(31, 111, 91, 0.14), transparent 34%),
radial-gradient(circle at 18% 82%, rgba(217, 146, 39, 0.13), transparent 30%);
box-shadow: var(--shadow);
padding: clamp(28px, 5vw, 58px);
overflow: hidden;
@@ -148,7 +148,7 @@ button { cursor: pointer; }
width: 360px;
height: 360px;
border-radius: 50%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.20), transparent 68%);
background: radial-gradient(circle, rgba(31, 111, 91, 0.13), transparent 68%);
}
.hero-copy {
position: relative;
@@ -193,7 +193,7 @@ p {
margin-top: 22px;
}
.hero-tags span {
border: 1px solid rgba(59, 130, 246, 0.18);
border: 1px solid rgba(31, 111, 91, 0.16);
border-radius: 999px;
padding: 7px 11px;
color: #355075;
@@ -215,7 +215,7 @@ p {
color: #263856;
font-weight: 900;
box-shadow: 0 10px 26px rgba(65, 88, 140, 0.10);
transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
transition: transform 0.18s var(--ease), box-shadow 0.18s ease, background-color 0.18s ease, border-color 0.18s ease;
}
.button:hover {
transform: translateY(-1px);
@@ -225,8 +225,8 @@ p {
.button.primary {
color: #fff;
border-color: transparent;
background: linear-gradient(135deg, #2563eb, #06b6d4);
box-shadow: 0 16px 34px rgba(37, 99, 235, 0.26);
background: linear-gradient(135deg, #10231d, #1f6f5b);
box-shadow: 0 16px 34px rgba(31, 111, 91, 0.24);
}
.release-card, .panel, .metric {
@@ -236,6 +236,13 @@ p {
box-shadow: 0 14px 42px rgba(65, 88, 140, 0.11);
backdrop-filter: blur(16px);
}
.release-card, .panel, .metric, .source-group, .notice-card, .route-list a {
transition: transform 0.22s var(--ease), border-color 0.22s ease, box-shadow 0.22s ease, background-color 0.22s ease;
}
.release-card:hover, .panel:hover, .metric:hover, .source-group:hover, .notice-card:hover, .route-list a:hover {
transform: translateY(-2px);
box-shadow: 0 18px 46px rgba(31, 48, 40, 0.13);
}
.release-card {
position: relative;
z-index: 1;
@@ -396,8 +403,8 @@ input {
outline: none;
}
input:focus {
border-color: rgba(59, 130, 246, 0.65);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.14);
border-color: rgba(31, 111, 91, 0.58);
box-shadow: 0 0 0 4px rgba(31, 111, 91, 0.12);
}
.source-board {
display: grid;
@@ -434,7 +441,7 @@ input:focus {
}
.route-list a:hover {
transform: translateY(-1px);
border-color: rgba(59, 130, 246, 0.36);
border-color: rgba(31, 111, 91, 0.30);
background: rgba(255, 255, 255, 0.92);
}
.route-list span { color: var(--muted); font-size: 13px; font-weight: 700; }
@@ -476,5 +483,5 @@ input:focus {
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none !important; scroll-behavior: auto !important; }
*, *::before, *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}