From 1d0e862299e915e7384a95a5e838a5f17f07af1f Mon Sep 17 00:00:00 2001 From: QWQLwToo <2467013926@qq.com> Date: Tue, 30 Jun 2026 11:45:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=90=8E=E7=AB=AF=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1UI=20=E5=AE=A2=E6=88=B7=E7=AB=AF=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E9=83=A8=E5=88=86=E7=95=8C=E9=9D=A2UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unified-management/web/admin/src/App.vue | 66 +++- .../web/admin/src/stores/feedback.ts | 16 +- .../web/admin/src/views/AuditLogView.vue | 107 +++++ .../web/admin/src/views/DashboardView.vue | 53 ++- .../web/admin/src/views/FeedbacksView.vue | 368 +++++++++++++++--- server/update/public/update-info.json | 18 +- src/box-winUI/ModernUi.cs | 63 ++- src/box-winUI/Views/HomePage.cs | 3 + src/box-winUI/Views/SettingsPage.cs | 55 ++- src/box-winUI/Views/ToolboxPage.cs | 47 ++- .../Tools/AdaptiveToolPage.GalleryLayouts.cs | 61 +++ .../Tools/AdaptiveToolPage.ResultRenderers.cs | 202 ++++++++++ src/box-winUI/Views/Tools/AdaptiveToolPage.cs | 4 +- 13 files changed, 942 insertions(+), 121 deletions(-) create mode 100644 server/unified-management/web/admin/src/views/AuditLogView.vue diff --git a/server/unified-management/web/admin/src/App.vue b/server/unified-management/web/admin/src/App.vue index 4f4086c..a9ab5de 100644 --- a/server/unified-management/web/admin/src/App.vue +++ b/server/unified-management/web/admin/src/App.vue @@ -20,6 +20,7 @@ import LegacyJsonView from "./views/LegacyJsonView.vue"; import ReleasesView from "./views/ReleasesView.vue"; import SourcesView from "./views/SourcesView.vue"; import SystemView from "./views/SystemView.vue"; +import AuditLogView from "./views/AuditLogView.vue"; import { adminFetch, toChineseError, uploadAdminFile } from "./api/admin"; import { createAuthStore } from "./stores/auth"; import { createDashboardStore } from "./stores/dashboard"; @@ -78,7 +79,7 @@ const systemStore = createSystemStore(); const { csrf, captcha, bootstrap: authBootstrap, loginForm, passwordForm } = authStore; const { dashboard, sourceCheckJobs } = dashboardStore; -const { page: feedbackPage, selected: selectedFeedback, filters: feedbackFilters, update: feedbackUpdate, commentDraft } = feedbackStore; +const { page: feedbackPage, selected: selectedFeedback, filters: feedbackFilters, update: feedbackUpdate, commentDraft, selectedCodes: feedbackSelectedCodes, detailTab: feedbackDetailTab, tagInput: feedbackTagInput } = feedbackStore; const { releases, notices: releaseNotices, selectedNotice, noticeDraft, uploadDraft } = releaseStore; const { sync: legacySync, documents: legacyDocuments, drafts: legacyDrafts, modal: legacyModal, activeMediaCategoryIndex } = legacyStore; const { sources, endpoints, draft: sourceDraft } = sourceStore; @@ -92,6 +93,7 @@ const routes: RouteItem[] = [ { path: "/admin/legacy/media-types", label: "媒体源 JSON", description: "维护旧客户端媒体源结构", icon: ClipboardList }, { path: "/admin/sources", label: "来源目录", description: "媒体/数据源目录和健康检测", icon: Network }, { path: "/admin/endpoints", label: "客户端接口", description: "新版客户端动态接口配置", icon: Code2 }, + { path: "/admin/audit", label: "审计日志", description: "操作审计、登录记录与安全事件", icon: ShieldCheck }, { path: "/admin/system", label: "系统运维", description: "数据库、旧项目同步、安全、健康与审计", icon: Database }, ]; @@ -100,7 +102,7 @@ const navGroups = [ { label: "反馈", items: routes.filter((item) => ["/admin/feedbacks"].includes(item.path)) }, { label: "发布与兼容", items: routes.filter((item) => ["/admin/releases", "/admin/legacy/update-info", "/admin/legacy/media-types"].includes(item.path)) }, { label: "客户端接口", items: routes.filter((item) => ["/admin/sources", "/admin/endpoints"].includes(item.path)) }, - { label: "系统运维", items: routes.filter((item) => ["/admin/system"].includes(item.path)) }, + { label: "系统运维", items: routes.filter((item) => ["/admin/audit", "/admin/system"].includes(item.path)) }, ]; const pageMeta = computed(() => routes.find((item) => item.path === currentPath.value) || routes[0]); @@ -270,6 +272,16 @@ const viewContext = computed(() => ({ feedbackOption: feedbackOption.value, feedbackPage: feedbackPage.value, feedbackUpdate, + feedbackSelectedCodes: feedbackSelectedCodes.value, + feedbackDetailTab: feedbackDetailTab.value, + feedbackTagInput: feedbackTagInput.value, + setFeedbackDetailTab: (tab: string) => { feedbackDetailTab.value = tab as any; }, + setFeedbackTagInput: (val: string) => { feedbackTagInput.value = val; }, + addFeedbackTag, + removeFeedbackTag, + toggleFeedbackCode, + toggleAllFeedbackCodes, + bulkUpdateFeedbacks, formatBytes, formatHealthOutput, healthOption: healthOption.value, @@ -476,6 +488,7 @@ async function load() { if (currentPath.value === "/admin/releases") await loadReleases(); if (currentPath.value === "/admin/sources") await loadSources(); if (currentPath.value === "/admin/endpoints") await loadEndpoints(); + if (currentPath.value === "/admin/audit") await loadAudit(); if (currentPath.value === "/admin/system") await loadSystem({ preserveForms: true }); const legacyName = activeLegacyName.value; if (legacyName) await loadLegacy(legacyName); @@ -500,13 +513,17 @@ async function loadSystem(options: LoadSystemOptions = {}) { ]); } -async function loadFeedbacks() { +async function loadFeedbacks(page?: number) { + if (page !== undefined) feedbackFilters.page = page; const params = new URLSearchParams({ page: String(feedbackFilters.page), perPage: String(feedbackFilters.perPage) }); if (feedbackFilters.q) params.set("q", feedbackFilters.q); if (feedbackFilters.status) params.set("status", feedbackFilters.status); if (feedbackFilters.priority) params.set("priority", feedbackFilters.priority); + if ((feedbackFilters as any).category) params.set("category", (feedbackFilters as any).category); + if ((feedbackFilters as any).assignee) params.set("assignee", (feedbackFilters as any).assignee); const data = await api<{ page: any }>(`/api/admin/feedbacks?${params}`); feedbackPage.value = data.page || { items: [], total: 0, page: 1, perPage: 20 }; + feedbackSelectedCodes.value = []; } async function openFeedback(item: any) { @@ -516,6 +533,48 @@ async function openFeedback(item: any) { feedbackUpdate.priority = data.feedback.priority || "normal"; feedbackUpdate.statusDetail = data.feedback.statusDetail || ""; feedbackUpdate.publicReply = data.feedback.publicReply || ""; + feedbackUpdate.assignee = data.feedback.assignee || ""; + feedbackUpdate.tags = data.feedback.tags ? [...data.feedback.tags] : []; + feedbackDetailTab.value = "info"; +} + +function toggleFeedbackCode(code: string) { + const idx = feedbackSelectedCodes.value.indexOf(code); + if (idx >= 0) feedbackSelectedCodes.value.splice(idx, 1); + else feedbackSelectedCodes.value.push(code); +} + +function toggleAllFeedbackCodes() { + const items: any[] = feedbackPage.value?.items || []; + if (feedbackSelectedCodes.value.length === items.length) { + feedbackSelectedCodes.value = []; + } else { + feedbackSelectedCodes.value = items.map((item: any) => item.code); + } +} + +async function bulkUpdateFeedbacks(payload: { status?: string; assignee?: string; tags?: string[] }) { + if (!feedbackSelectedCodes.value.length) return; + await guarded(async () => { + await api("/api/admin/feedbacks/bulk", { + method: "PATCH", + body: JSON.stringify({ codes: feedbackSelectedCodes.value, ...payload }), + }); + setToast(`已批量更新 ${feedbackSelectedCodes.value.length} 条工单`); + feedbackSelectedCodes.value = []; + await loadFeedbacks(); + }); +} + +function addFeedbackTag() { + const tag = feedbackTagInput.value.trim(); + if (!tag || feedbackUpdate.tags.includes(tag)) { feedbackTagInput.value = ""; return; } + feedbackUpdate.tags = [...feedbackUpdate.tags, tag]; + feedbackTagInput.value = ""; +} + +function removeFeedbackTag(tag: string) { + feedbackUpdate.tags = feedbackUpdate.tags.filter((t) => t !== tag); } async function saveFeedbackUpdate() { @@ -1597,6 +1656,7 @@ function connectAdminEvents() { + diff --git a/server/unified-management/web/admin/src/stores/feedback.ts b/server/unified-management/web/admin/src/stores/feedback.ts index 767078c..34389c5 100644 --- a/server/unified-management/web/admin/src/stores/feedback.ts +++ b/server/unified-management/web/admin/src/stores/feedback.ts @@ -3,9 +3,19 @@ import { reactive, ref } from "vue"; export function createFeedbackStore() { const page = ref({ items: [], total: 0, page: 1, perPage: 20 }); const selected = ref(null); - const filters = reactive({ q: "", status: "", priority: "", page: 1, perPage: 20 }); - const update = reactive({ status: "", priority: "", statusDetail: "", publicReply: "" }); + const filters = reactive({ q: "", status: "", priority: "", category: "", assignee: "", page: 1, perPage: 20 }); + const update = reactive<{ status: string; priority: string; statusDetail: string; publicReply: string; assignee: string; tags: string[] }>({ + status: "", + priority: "", + statusDetail: "", + publicReply: "", + assignee: "", + tags: [], + }); const commentDraft = reactive({ body: "", internal: true }); + const selectedCodes = ref([]); + const detailTab = ref<"info" | "comments" | "activity">("info"); + const tagInput = ref(""); - return { page, selected, filters, update, commentDraft }; + return { page, selected, filters, update, commentDraft, selectedCodes, detailTab, tagInput }; } diff --git a/server/unified-management/web/admin/src/views/AuditLogView.vue b/server/unified-management/web/admin/src/views/AuditLogView.vue new file mode 100644 index 0000000..3d43759 --- /dev/null +++ b/server/unified-management/web/admin/src/views/AuditLogView.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/server/unified-management/web/admin/src/views/DashboardView.vue b/server/unified-management/web/admin/src/views/DashboardView.vue index d325191..741fc19 100644 --- a/server/unified-management/web/admin/src/views/DashboardView.vue +++ b/server/unified-management/web/admin/src/views/DashboardView.vue @@ -16,7 +16,7 @@ use([CanvasRenderer, LineChart, PieChart, BarChart, GaugeChart, GridComponent, T
反馈总数{{ ctx.kpis.feedbackTotal || 0 }}今日新增 {{ ctx.kpis.feedbackToday || 0 }}
可见接口{{ ctx.kpis.sourceVisible || 0 }}接口总数 {{ ctx.kpis.sourceTotal || 0 }}
-
版本日志{{ ctx.kpis.releaseNotices || 0 }}{{ ctx.latestNotice?.version || "暂无最新版本" }}
+
版本日志{{ ctx.kpis.releaseNotices || 0 }}{{ ctx.latestNotice && ctx.latestNotice.version ? ctx.latestNotice.version : "暂无最新版本" }}
邮件失败{{ ctx.kpis.mailFailed || 0 }}旧反馈兼容记录
@@ -26,7 +26,8 @@ use([CanvasRenderer, LineChart, PieChart, BarChart, GaugeChart, GridComponent, T {{ ctx.autoRefreshPaused ? "恢复自动刷新" : "暂停自动刷新" }} - 每 15 秒自动刷新仪表盘数据。 + 每 15 秒自动刷新。 + 上次刷新:{{ ctx.lastRefreshedAt }}
@@ -37,10 +38,10 @@ use([CanvasRenderer, LineChart, PieChart, BarChart, GaugeChart, GridComponent, T {{ job.id }} {{ job.checked || 0 }} / {{ job.total || 0 }} - {{ job.stats?.ok || 0 }} - {{ job.stats?.redirected || 0 }} - {{ job.stats?.degraded || 0 }} - {{ job.stats?.error || 0 }} + {{ (job.stats && job.stats.ok) || 0 }} + {{ (job.stats && job.stats.redirected) || 0 }} + {{ (job.stats && job.stats.degraded) || 0 }} + {{ (job.stats && job.stats.error) || 0 }} {{ job.startedAt || "-" }} @@ -53,7 +54,7 @@ use([CanvasRenderer, LineChart, PieChart, BarChart, GaugeChart, GridComponent, T
暂无服务端检测记录 - 点击“立即服务端检测”后会生成延迟曲线。 + 点击立即服务端检测后会生成延迟曲线。

接口健康分布

@@ -68,12 +69,12 @@ use([CanvasRenderer, LineChart, PieChart, BarChart, GaugeChart, GridComponent, T {{ item.name || item.sourceId }} - {{ ctx.labelStatus(item.status) }} + {{ ctx.labelStatus(item.status) }} {{ item.latencyMs || 0 }}ms {{ ctx.formatHealthOutput(item) }} {{ item.checkedAt || "-" }} - 暂无检测记录,点击“立即服务端检测”后会刷新。 + 暂无检测记录,点击立即服务端检测后会刷新。 @@ -85,7 +86,7 @@ use([CanvasRenderer, LineChart, PieChart, BarChart, GaugeChart, GridComponent, T {{ item.sourceId }} - {{ ctx.labelStatus(item.status) }} + {{ ctx.labelStatus(item.status) }} {{ item.latencyMs || 0 }}ms {{ item.client || "-" }} {{ item.createdAt || "-" }} @@ -94,5 +95,37 @@ use([CanvasRenderer, LineChart, PieChart, BarChart, GaugeChart, GridComponent, T + +
+
+

系统日志

+
+ + +
+
+ + + + + + + + + + + + +
分类类型状态消息时间
{{ item.category || "-" }}{{ item.type || "-" }}{{ ctx.labelStatus(item.status) }}{{ item.message || "-" }}{{ item.createdAt || "-" }}
暂无系统日志。
+
diff --git a/server/unified-management/web/admin/src/views/FeedbacksView.vue b/server/unified-management/web/admin/src/views/FeedbacksView.vue index 02321d5..5ee5a74 100644 --- a/server/unified-management/web/admin/src/views/FeedbacksView.vue +++ b/server/unified-management/web/admin/src/views/FeedbacksView.vue @@ -1,11 +1,12 @@ + + diff --git a/server/update/public/update-info.json b/server/update/public/update-info.json index b3fed9f..3424466 100644 --- a/server/update/public/update-info.json +++ b/server/update/public/update-info.json @@ -10,15 +10,15 @@ "fullInstaller": { "fileName": "YMhut_Box_WinUI_Setup_2.0.7.6.exe", "url": "https://update.ymhut.cn/downloads/YMhut_Box_WinUI_Setup_2.0.7.6.exe", - "sha256": "972478ab77d016165e2f35171571b1f59ba7dfa97d7c86ed9fb44b0b82ad4d93", - "size": 113486448, + "sha256": "38990585884af04fe4b86344418e9021dcf319bce351cd46bbe18762bd18bd1d", + "size": 113488208, "version": "2.0.7.6" }, "msix": { "fileName": "YMhutBox_2.0.7.6_x64.msix", "url": "https://update.ymhut.cn/downloads/YMhutBox_2.0.7.6_x64.msix", - "sha256": "a05b02ebf5f2d1b71e051e906bf9bdd71bd018893badf6f462e3bd61e3f232c0", - "size": 259970957, + "sha256": "015820df9a5076d3759619fc90cb23b0250d6828adb6713ac48d0587162e4070", + "size": 259972073, "version": "2.0.7.6" }, "appInstaller": { @@ -32,15 +32,15 @@ "fullInstaller": { "fileName": "YMhut_Box_WinUI_Setup_2.0.7.6.exe", "url": "https://update.ymhut.cn/downloads/YMhut_Box_WinUI_Setup_2.0.7.6.exe", - "sha256": "972478ab77d016165e2f35171571b1f59ba7dfa97d7c86ed9fb44b0b82ad4d93", - "size": 113486448, + "sha256": "38990585884af04fe4b86344418e9021dcf319bce351cd46bbe18762bd18bd1d", + "size": 113488208, "version": "2.0.7.6" }, "msix": { "fileName": "YMhutBox_2.0.7.6_x64.msix", "url": "https://update.ymhut.cn/downloads/YMhutBox_2.0.7.6_x64.msix", - "sha256": "a05b02ebf5f2d1b71e051e906bf9bdd71bd018893badf6f462e3bd61e3f232c0", - "size": 259970957, + "sha256": "015820df9a5076d3759619fc90cb23b0250d6828adb6713ac48d0587162e4070", + "size": 259972073, "version": "2.0.7.6" }, "appInstaller": { @@ -56,5 +56,5 @@ "updateInfo": "The official update-info catalog only describes the full offline installer, MSIX, and appinstaller artifacts.", "distribution": "The update channel publishes the full offline installer, MSIX, and appinstaller artifacts." }, - "createdAt": "2026-06-29T06:32:36.4668331Z" + "createdAt": "2026-06-30T02:26:21.1755219Z" } \ No newline at end of file diff --git a/src/box-winUI/ModernUi.cs b/src/box-winUI/ModernUi.cs index 3f432de..ab59c06 100644 --- a/src/box-winUI/ModernUi.cs +++ b/src/box-winUI/ModernUi.cs @@ -25,6 +25,13 @@ internal static class ModernUi internal static SolidColorBrush Danger { get; } = Brush("#C42B1C"); internal static SolidColorBrush Info { get; } = Brush("#0078D4"); + // Hover-specific brushes — updated by SetPalette so they track theme changes. + internal static SolidColorBrush HoverSurface { get; } = Brush("#EBEBEB"); + internal static SolidColorBrush HoverAccentSoft { get; } = Brush("#D0EAFF"); + internal static SolidColorBrush PrimaryHover { get; } = Brush("#005AA3"); + internal static SolidColorBrush PrimaryPressed { get; } = Brush("#004F8E"); + internal static SolidColorBrush Transparent { get; } = Brush("#00FFFFFF"); + internal static void SetPalette(bool dark) { SetBrush(AppBackground, dark ? "#202020" : "#F7F7F7"); @@ -41,6 +48,12 @@ internal static class ModernUi SetBrush(Bronze, dark ? "#F7B189" : "#CA5010"); SetBrush(Danger, dark ? "#F1707B" : "#C42B1C"); SetBrush(Info, dark ? "#60CDFF" : "#0078D4"); + // Hover brushes + SetBrush(HoverSurface, dark ? "#3A3A3A" : "#EBEBEB"); + SetBrush(HoverAccentSoft, dark ? "#0D4F78" : "#D0EAFF"); + SetBrush(PrimaryHover, dark ? "#4BB8F0" : "#005AA3"); + SetBrush(PrimaryPressed, dark ? "#38A8E5" : "#004F8E"); + // Transparent is always the same — no update needed } internal static SolidColorBrush Brush(string hex) @@ -138,16 +151,40 @@ internal static class ModernUi }; } + /// + /// Overrides the WinUI 3 Button ControlTemplate's PointerOver/Pressed theme resource keys + /// at the instance level, so VSM animations still run but with the correct custom colors. + /// Call this after constructing a Button whose Background/BorderBrush were set manually. + /// + internal static void ApplyHoverResources( + Button button, + Brush hoverBackground, + Brush hoverBorder, + Brush? pressedBackground = null, + Brush? pressedBorder = null, + Brush? hoverForeground = null) + { + button.Resources["ButtonBackgroundPointerOver"] = hoverBackground; + button.Resources["ButtonBorderBrushPointerOver"] = hoverBorder; + button.Resources["ButtonBackgroundPressed"] = pressedBackground ?? hoverBackground; + button.Resources["ButtonBorderBrushPressed"] = pressedBorder ?? hoverBorder; + if (hoverForeground is not null) + { + button.Resources["ButtonForegroundPointerOver"] = hoverForeground; + button.Resources["ButtonForegroundPressed"] = hoverForeground; + } + } + internal static Button IconButton(string glyph, string tooltip, Action? click = null) { var button = new Button { - Width = 40, + Width = 36, Height = 36, Padding = new Thickness(0), CornerRadius = new CornerRadius(6), - Background = Brush("#00FFFFFF"), - BorderBrush = Brush("#00FFFFFF"), + Background = Transparent, + BorderBrush = Transparent, Content = new FontIcon { Glyph = glyph, @@ -155,6 +192,7 @@ internal static class ModernUi Foreground = TextSecondary } }; + ApplyHoverResources(button, SurfaceAlt, Stroke, HoverSurface, StrokeStrong); ToolTipService.SetToolTip(button, tooltip); AutomationProperties.SetName(button, tooltip); if (click is not null) @@ -167,6 +205,7 @@ internal static class ModernUi internal static Button PillButton(string text, string glyph, Action? click = null, bool primary = false) { + var fgBrush = primary ? Brush("#FFFFFFFF") : TextPrimary; var panel = new StackPanel { Orientation = Orientation.Horizontal, @@ -175,8 +214,8 @@ internal static class ModernUi VerticalAlignment = VerticalAlignment.Center, Children = { - new FontIcon { Glyph = glyph, FontSize = 15 }, - Text(text, 14, FontWeights.SemiBold, primary ? Brush("#FFFFFFFF") : TextPrimary) + new FontIcon { Glyph = glyph, FontSize = 15, Foreground = fgBrush }, + Text(text, 14, FontWeights.SemiBold, fgBrush) } }; @@ -184,12 +223,22 @@ internal static class ModernUi { Padding = new Thickness(14, 8, 14, 8), CornerRadius = new CornerRadius(6), - Background = primary ? Accent : Brush("#00FFFFFF"), - Foreground = primary ? Brush("#FFFFFFFF") : TextPrimary, + Background = primary ? Accent : Transparent, + Foreground = fgBrush, BorderBrush = primary ? Accent : Stroke, BorderThickness = new Thickness(1), Content = panel }; + + if (primary) + { + ApplyHoverResources(button, PrimaryHover, PrimaryHover, PrimaryPressed, PrimaryPressed, Brush("#FFFFFFFF")); + } + else + { + ApplyHoverResources(button, SurfaceAlt, StrokeStrong, HoverSurface, StrokeStrong); + } + ToolTipService.SetToolTip(button, text); AutomationProperties.SetName(button, text); if (click is not null) diff --git a/src/box-winUI/Views/HomePage.cs b/src/box-winUI/Views/HomePage.cs index 6f55abb..4b2948f 100644 --- a/src/box-winUI/Views/HomePage.cs +++ b/src/box-winUI/Views/HomePage.cs @@ -208,6 +208,7 @@ public sealed class HomePage : Page BorderThickness = new Thickness(1), Content = new Border { Padding = new Thickness(16, 12, 16, 12), Child = grid } }; + ModernUi.ApplyHoverResources(button, ModernUi.SurfaceAlt, ModernUi.StrokeStrong, ModernUi.HoverSurface, ModernUi.StrokeStrong); button.Click += async (_, _) => await ShowAnnouncementDialogAsync(); ToolTipService.SetToolTip(button, AppLocalizer.T("查看完整公告", "View full announcement")); return button; @@ -663,6 +664,7 @@ public sealed class HomePage : Page BorderThickness = new Thickness(1), Content = grid }; + ModernUi.ApplyHoverResources(button, ModernUi.HoverSurface, ModernUi.StrokeStrong, ModernUi.Stroke, ModernUi.StrokeStrong); ToolTipService.SetToolTip(button, $"{ToolText.Name(module)}\n{ToolText.Description(module)}"); button.Click += (_, _) => _openTool(module); return button; @@ -707,6 +709,7 @@ public sealed class HomePage : Page BorderThickness = new Thickness(1), Content = grid }; + ModernUi.ApplyHoverResources(button, ModernUi.HoverSurface, ModernUi.StrokeStrong, ModernUi.Stroke, ModernUi.StrokeStrong); ToolTipService.SetToolTip(button, label); button.Click += (_, _) => _openToolbox(label); return button; diff --git a/src/box-winUI/Views/SettingsPage.cs b/src/box-winUI/Views/SettingsPage.cs index 270b705..0e1a247 100644 --- a/src/box-winUI/Views/SettingsPage.cs +++ b/src/box-winUI/Views/SettingsPage.cs @@ -293,13 +293,14 @@ public sealed class SettingsPage : Page }, new StackPanel { - Spacing = 10, + Spacing = 8, Children = { _cpuStrip, _memoryStrip, - BuildControlStatusLine("\uE950", AppLocalizer.T("处理器", "Processor"), _controlProcessorStatus), - BuildControlStatusLine("\uE823", AppLocalizer.T("运行", "Uptime"), _controlUptimeStatus) + new Border { Height = 1, Background = ModernUi.Stroke, Margin = new Thickness(0, 2, 0, 2) }, + BuildControlStatusLine("", AppLocalizer.T("处理器", "Processor"), _controlProcessorStatus), + BuildControlStatusLine("", AppLocalizer.T("运行", "Uptime"), _controlUptimeStatus) } } } @@ -535,12 +536,13 @@ public sealed class SettingsPage : Page MinHeight = 38, Padding = new Thickness(7, 4, 7, 4), CornerRadius = new CornerRadius(8), - Background = ModernUi.Brush("#00FFFFFF"), - BorderBrush = ModernUi.Brush("#00FFFFFF"), + Background = ModernUi.Transparent, + BorderBrush = ModernUi.Transparent, BorderThickness = new Thickness(1), Content = grid, Tag = key }; + ModernUi.ApplyHoverResources(button, ModernUi.SurfaceAlt, ModernUi.Stroke, ModernUi.HoverSurface, ModernUi.StrokeStrong); button.Click += (_, _) => ShowSettingsPage(key); AutomationProperties.SetName(button, title); return button; @@ -563,8 +565,15 @@ public sealed class SettingsPage : Page foreach (var pair in _settingsPageButtons) { var selected = string.Equals(pair.Key, _activeSettingsPage, StringComparison.OrdinalIgnoreCase); - pair.Value.Background = selected ? ModernUi.AccentSoft : ModernUi.Brush("#00FFFFFF"); - pair.Value.BorderBrush = selected ? ModernUi.Accent : ModernUi.Brush("#00FFFFFF"); + pair.Value.Background = selected ? ModernUi.AccentSoft : ModernUi.Transparent; + pair.Value.BorderBrush = selected ? ModernUi.Accent : ModernUi.Transparent; + // Keep hover resources in sync with selected state + ModernUi.ApplyHoverResources( + pair.Value, + selected ? ModernUi.HoverAccentSoft : ModernUi.SurfaceAlt, + selected ? ModernUi.Accent : ModernUi.Stroke, + selected ? ModernUi.HoverAccentSoft : ModernUi.HoverSurface, + selected ? ModernUi.Accent : ModernUi.StrokeStrong); if (pair.Value.Content is Grid grid && grid.Children.FirstOrDefault() is Border iconTile) { iconTile.Background = selected ? ModernUi.AccentSoft : ModernUi.SurfaceAlt; @@ -902,19 +911,27 @@ public sealed class SettingsPage : Page private UIElement BuildActionRow(string glyph, string title, UIElement value, Func action) { var row = BaseRow(glyph, title, value); + var chevron = new FontIcon { Glyph = "", FontSize = 14, Foreground = ModernUi.TextSecondary, VerticalAlignment = VerticalAlignment.Center }; + row.Children.Add(chevron); + Grid.SetColumn(chevron, 2); + var tooltip = AppLocalizer.T($"打开 {title}", $"Open {title}"); - var button = ModernUi.IconButton("\uE974", tooltip, async () => await action()); - button.Width = 42; - button.Height = 38; - button.MinWidth = 42; - button.MinHeight = 38; - button.VerticalAlignment = VerticalAlignment.Center; - button.HorizontalAlignment = HorizontalAlignment.Right; - ToolTipService.SetToolTip(button, tooltip); - AutomationProperties.SetName(button, tooltip); - row.Children.Add(button); - Grid.SetColumn(button, 2); - return row; + var rowButton = new Button + { + HorizontalAlignment = HorizontalAlignment.Stretch, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + Padding = new Thickness(0), + CornerRadius = new CornerRadius(6), + Background = ModernUi.Transparent, + BorderBrush = ModernUi.Transparent, + BorderThickness = new Thickness(0), + Content = row + }; + ModernUi.ApplyHoverResources(rowButton, ModernUi.SurfaceAlt, ModernUi.Transparent, ModernUi.HoverSurface, ModernUi.Transparent); + rowButton.Click += async (_, _) => await action(); + ToolTipService.SetToolTip(rowButton, tooltip); + AutomationProperties.SetName(rowButton, tooltip); + return rowButton; } private UIElement BuildToggleRow(string glyph, string title, string subtitle, ToggleSwitch toggle) diff --git a/src/box-winUI/Views/ToolboxPage.cs b/src/box-winUI/Views/ToolboxPage.cs index f4a3e52..6682375 100644 --- a/src/box-winUI/Views/ToolboxPage.cs +++ b/src/box-winUI/Views/ToolboxPage.cs @@ -342,13 +342,13 @@ public sealed class ToolboxPage : Page var grid = new Grid { ColumnSpacing = 20, RowSpacing = 12 }; grid.ColumnDefinitions.Add(new ColumnDefinition()); - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.72, GridUnitType.Star) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.85, GridUnitType.Star) }); grid.Children.Add(title); Grid.SetColumn(right, 1); grid.Children.Add(right); grid.SizeChanged += (_, e) => { - var narrow = e.NewSize.Width < 940; + var narrow = e.NewSize.Width < 860; grid.ColumnDefinitions.Clear(); grid.RowDefinitions.Clear(); grid.ColumnDefinitions.Add(new ColumnDefinition()); @@ -361,7 +361,7 @@ public sealed class ToolboxPage : Page } else { - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.72, GridUnitType.Star) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.85, GridUnitType.Star) }); Grid.SetColumn(right, 1); Grid.SetRow(right, 0); } @@ -708,6 +708,12 @@ public sealed class ToolboxPage : Page count, ToolText.CategoryGlyph(category)) }; + ModernUi.ApplyHoverResources( + button, + selected ? ModernUi.HoverAccentSoft : ModernUi.HoverSurface, + selected ? ModernUi.Accent : ModernUi.StrokeStrong, + selected ? ModernUi.HoverAccentSoft : ModernUi.SurfaceAlt, + selected ? ModernUi.Accent : ModernUi.StrokeStrong); button.Click += (_, _) => { @@ -780,6 +786,12 @@ public sealed class ToolboxPage : Page } } }; + ModernUi.ApplyHoverResources( + button, + selected ? ModernUi.HoverAccentSoft : ModernUi.HoverSurface, + selected ? ModernUi.Accent : ModernUi.StrokeStrong, + selected ? ModernUi.HoverAccentSoft : ModernUi.SurfaceAlt, + selected ? ModernUi.Accent : ModernUi.StrokeStrong); button.Click += async (_, _) => { _selectedScope = scope; @@ -942,11 +954,13 @@ public sealed class ToolboxPage : Page HorizontalContentAlignment = HorizontalAlignment.Stretch, VerticalContentAlignment = VerticalAlignment.Stretch, CornerRadius = new CornerRadius(8), - Background = ModernUi.Brush("#00FFFFFF"), - BorderBrush = ModernUi.Brush("#00FFFFFF"), + Background = ModernUi.Transparent, + BorderBrush = ModernUi.Transparent, BorderThickness = new Thickness(0), ContextFlyout = BuildToolFlyout(module, favorite) }; + // Suppress the built-in template hover overlay on the transparent inner button + ModernUi.ApplyHoverResources(button, ModernUi.Transparent, ModernUi.Transparent, ModernUi.Transparent, ModernUi.Transparent); grid.IsHitTestVisible = false; ToolTipService.SetToolTip(button, $"{viewModel.Name}\n{viewModel.Description}"); AutomationProperties.SetName(button, viewModel.Name); @@ -958,14 +972,20 @@ public sealed class ToolboxPage : Page favoriteButton.VerticalAlignment = VerticalAlignment.Top; favoriteButton.Margin = new Thickness(0, 12, 12, 0); Canvas.SetZIndex(favoriteButton, 2); - return new Border + + var normalBg = ModernUi.Surface; + var normalBorder = favorite ? ModernUi.Bronze : IsRisky(module) ? ModernUi.Danger : ModernUi.Stroke; + var hoverBg = ModernUi.HoverSurface; + var hoverBorder = favorite ? ModernUi.Bronze : IsRisky(module) ? ModernUi.Danger : ModernUi.StrokeStrong; + + var outerBorder = new Border { Width = buttonWidth, Height = compact ? 170 : 236, Margin = new Thickness(7), CornerRadius = new CornerRadius(8), - Background = ModernUi.Surface, - BorderBrush = favorite ? ModernUi.Bronze : IsRisky(module) ? ModernUi.Danger : ModernUi.Stroke, + Background = normalBg, + BorderBrush = normalBorder, BorderThickness = new Thickness(1), Child = new Grid { @@ -973,6 +993,17 @@ public sealed class ToolboxPage : Page }, ContextFlyout = BuildToolFlyout(module, favorite) }; + outerBorder.PointerEntered += (_, _) => + { + outerBorder.Background = hoverBg; + outerBorder.BorderBrush = hoverBorder; + }; + outerBorder.PointerExited += (_, _) => + { + outerBorder.Background = normalBg; + outerBorder.BorderBrush = normalBorder; + }; + return outerBorder; } private MenuFlyout BuildToolFlyout(IToolModule module, bool favorite) diff --git a/src/box-winUI/Views/Tools/AdaptiveToolPage.GalleryLayouts.cs b/src/box-winUI/Views/Tools/AdaptiveToolPage.GalleryLayouts.cs index e7ce60b..a593e05 100644 --- a/src/box-winUI/Views/Tools/AdaptiveToolPage.GalleryLayouts.cs +++ b/src/box-winUI/Views/Tools/AdaptiveToolPage.GalleryLayouts.cs @@ -206,6 +206,12 @@ public abstract partial class AdaptiveToolPage private Border BuildCompactGalleryInputCard(IToolModule module) { + // Auto-load tools with no user input: collapse to a minimal reload card + if (_spec.AutoRunOnOpen && _spec.PrimaryInput is ToolPrimaryInputKind.None) + { + return BuildAutoLoadInputCard(module); + } + var panel = new StackPanel { Spacing = _spec.PageDensity == ToolPageDensity.Compact ? 10 : 14 }; panel.Children.Add(BuildGallerySectionHeader( AppLocalizer.T("输入", "Input"), @@ -660,4 +666,59 @@ public abstract partial class AdaptiveToolPage _ => AppLocalizer.T("文本", "Text") }; } + + /// + /// Minimal input card for auto-load tools that have no user-supplied input. + /// Shows only the tool description, a reload button, and a cancel button. + /// + private Border BuildAutoLoadInputCard(IToolModule module) + { + var reloadButton = ModernUi.PillButton( + AppLocalizer.T("重新加载", "Reload"), + "", + async () => await RunCurrentToolAsync(), + primary: true); + + var cancelButton = ModernUi.PillButton( + AppLocalizer.T("取消", "Cancel"), + "", + () => CancelRunningTool()); + + var actions = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 10, + Children = { reloadButton, cancelButton } + }; + + var grid = new Grid { ColumnSpacing = 12 }; + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + grid.ColumnDefinitions.Add(new ColumnDefinition()); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + grid.Children.Add(ModernUi.IconTile(module.Metadata.IconGlyph, 38, ModernUi.AccentSoft, ModernUi.Accent, 17)); + + var text = new StackPanel + { + Spacing = 2, + VerticalAlignment = VerticalAlignment.Center, + Children = + { + ModernUi.Text(AppLocalizer.T("数据源", "Data source"), 11, FontWeights.SemiBold, ModernUi.Accent, maxLines: 1), + ModernUi.Text(AppLocalizer.T("自动加载", "Auto load"), 14, FontWeights.SemiBold, maxLines: 1), + ModernUi.Text(CompactInputDescription(module), 12, foreground: ModernUi.TextSecondary, maxLines: 2) + } + }; + Grid.SetColumn(text, 1); + grid.Children.Add(text); + + Grid.SetColumn(_progressRing, 2); + grid.Children.Add(_progressRing); + + return ModernUi.Card(new StackPanel + { + Spacing = 12, + Children = { grid, actions } + }, new Thickness(14), radius: 8); + } } diff --git a/src/box-winUI/Views/Tools/AdaptiveToolPage.ResultRenderers.cs b/src/box-winUI/Views/Tools/AdaptiveToolPage.ResultRenderers.cs index 633a2b3..121b0c6 100644 --- a/src/box-winUI/Views/Tools/AdaptiveToolPage.ResultRenderers.cs +++ b/src/box-winUI/Views/Tools/AdaptiveToolPage.ResultRenderers.cs @@ -68,6 +68,18 @@ public abstract partial class AdaptiveToolPage return true; } + // Ranked list / news / hotboard tools get a dedicated renderer before generic JSON + var rankedListToolIds = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "movie_box_office", "baidu_hot", "bili_hot", "zhihu_hot", "weibo_hot", + "cctv_news", "tech_news", "football_news", "history_today", + "earthquake_info", "gold_price", "oil_price", "hotboard" + }; + if (rankedListToolIds.Contains(module.Id) || _spec.Result == ToolResultKind.RankedList || _spec.Result == ToolResultKind.NewsCards) + { + if (TryRenderRankedList(module, output, lines)) return true; + } + if (TryRenderJson(output, module)) { return true; @@ -637,4 +649,194 @@ public abstract partial class AdaptiveToolPage _ => "结果摘要" }; } + + /// + /// Renders ranked/hotboard/news results with a dedicated rank-badge card layout. + /// Handles both JSON array output and numbered-text-line output (e.g. "1. Title · meta"). + /// + private bool TryRenderRankedList(IToolModule module, string output, IReadOnlyList lines) + { + // Try JSON array first + var trimmed = output.TrimStart(); + if (trimmed.StartsWith('[')) + { + try + { + using var document = JsonDocument.Parse(output); + if (document.RootElement.ValueKind != JsonValueKind.Array) + { + return false; + } + + var items = document.RootElement.EnumerateArray().Take(50).ToList(); + if (items.Count == 0) + { + return false; + } + + _resultCards.Children.Add(BuildInfoGrid([ + (AppLocalizer.T("条目数", "Items"), items.Count.ToString()), + (AppLocalizer.T("类型", "Kind"), AppLocalizer.T("榜单 / 资讯", "Ranked list")) + ])); + + for (var index = 0; index < items.Count; index++) + { + var item = items[index]; + var rank = index + 1; + var title = FirstJsonString(item, "title", "name", "text", "subject", "headline") ?? $"#{rank}"; + var meta = FirstJsonString(item, "score", "hot", "value", "heat", "count", "view", "desc", "description", "meta", "category", "type"); + var url = FirstJsonString(item, "url", "link", "href"); + + // Some APIs return an explicit rank field + if (item.TryGetProperty("rank", out var rankElem) && rankElem.TryGetInt32(out var explicitRank)) + { + rank = explicitRank; + } + + _resultCards.Children.Add(BuildRankedItemCard(rank, title, meta, url)); + } + + return true; + } + catch + { + // Fall through to text parsing + } + } + + // Try numbered text lines: "1. Title · meta", "1、Title", "#1 Title" + var rankedLines = lines + .Select(ParseRankedLine) + .Where(item => item.HasValue) + .Select(item => item!.Value) + .Take(50) + .ToList(); + + if (rankedLines.Count < 2) + { + return false; + } + + _resultCards.Children.Add(BuildInfoGrid([ + (AppLocalizer.T("条目数", "Items"), rankedLines.Count.ToString()), + (AppLocalizer.T("来源", "Source"), AppLocalizer.T("实时榜单", "Live ranking")) + ])); + + foreach (var item in rankedLines) + { + _resultCards.Children.Add(BuildRankedItemCard(item.Rank, item.Title, item.Meta, null)); + } + + return true; + } + + private static (int Rank, string Title, string? Meta)? ParseRankedLine(string line) + { + if (string.IsNullOrWhiteSpace(line)) return null; + + // Formats: "1. Title · meta", "1. Title", "#1 Title", "1、Title" + var match = Regex.Match(line.Trim(), @"^(?:#?(\d+)[.、\s]+|(\d+)\.\s*)(.+)$"); + if (!match.Success) return null; + + var rankStr = match.Groups[1].Value.Length > 0 ? match.Groups[1].Value : match.Groups[2].Value; + if (!int.TryParse(rankStr, out var rank)) return null; + + var rest = match.Groups[3].Value.Trim(); + string? meta = null; + string title = rest; + + // Split "Title · meta" or "Title - meta" or "Title | meta" + var sep = rest.IndexOfAny([' ']); + var dotSep = rest.IndexOf(" · ", StringComparison.Ordinal); + var dashSep = rest.IndexOf(" - ", StringComparison.Ordinal); + var pipeSep = rest.IndexOf(" | ", StringComparison.Ordinal); + var tabSep = rest.IndexOf('\t'); + + var splitAt = new[] { dotSep, dashSep, pipeSep, tabSep }.Where(pos => pos > 0).OrderBy(pos => pos).FirstOrDefault(-1); + if (splitAt > 0) + { + title = rest[..splitAt].Trim(); + meta = rest[(splitAt + 1)..].TrimStart(' ', '-', '·', '|', '\t').Trim(); + } + + return string.IsNullOrWhiteSpace(title) ? null : (rank, title, string.IsNullOrWhiteSpace(meta) ? null : meta); + } + + private static string? FirstJsonString(JsonElement element, params string[] keys) + { + foreach (var key in keys) + { + if (element.TryGetProperty(key, out var prop) && prop.ValueKind == JsonValueKind.String) + { + var value = prop.GetString(); + if (!string.IsNullOrWhiteSpace(value)) return value; + } + + // Also try camelCase variants with number suffixes + if (element.TryGetProperty(key.ToLowerInvariant(), out var lowerProp) && lowerProp.ValueKind == JsonValueKind.String) + { + var value = lowerProp.GetString(); + if (!string.IsNullOrWhiteSpace(value)) return value; + } + } + + return null; + } + + private static Border BuildRankedItemCard(int rank, string title, string? meta, string? url) + { + // Rank badge color: gold/silver/bronze for top 3, default for rest + var rankFg = rank switch + { + 1 => ModernUi.Brush("#CA5010"), // gold-ish / bronze + 2 => ModernUi.TextSecondary, + 3 => ModernUi.TextSecondary, + _ => ModernUi.TextSecondary + }; + var rankBg = rank switch + { + 1 => ModernUi.AccentSoft, + 2 => ModernUi.SurfaceAlt, + 3 => ModernUi.SurfaceAlt, + _ => ModernUi.SurfaceAlt + }; + + var rankBadge = ModernUi.Badge(rank.ToString(), rank == 1 ? ModernUi.Accent : rankFg, rankBg); + rankBadge.MinWidth = 32; + rankBadge.HorizontalAlignment = HorizontalAlignment.Center; + + var text = new StackPanel + { + Spacing = 2, + VerticalAlignment = VerticalAlignment.Center, + Children = + { + ModernUi.Text(ModernUi.Ellipsize(title, 100), 14, FontWeights.SemiBold, maxLines: 1), + } + }; + + if (!string.IsNullOrWhiteSpace(meta)) + { + text.Children.Add(ModernUi.Text(ModernUi.Ellipsize(meta, 80), 12, foreground: ModernUi.TextSecondary, maxLines: 1)); + } + + var row = new Grid { ColumnSpacing = 12, MinHeight = 40 }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(36) }); + row.ColumnDefinitions.Add(new ColumnDefinition()); + + row.Children.Add(rankBadge); + Grid.SetColumn(text, 1); + row.Children.Add(text); + + if (!string.IsNullOrWhiteSpace(url)) + { + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + var linkButton = ModernUi.IconButton("", AppLocalizer.T("打开链接", "Open link"), + async () => await Windows.System.Launcher.LaunchUriAsync(new Uri(url!))); + Grid.SetColumn(linkButton, 2); + row.Children.Add(linkButton); + } + + return ModernUi.Card(row, new Thickness(10, 8, 10, 8), radius: 8, background: ModernUi.SurfaceAlt); + } } diff --git a/src/box-winUI/Views/Tools/AdaptiveToolPage.cs b/src/box-winUI/Views/Tools/AdaptiveToolPage.cs index 1eeed9f..deeb6a2 100644 --- a/src/box-winUI/Views/Tools/AdaptiveToolPage.cs +++ b/src/box-winUI/Views/Tools/AdaptiveToolPage.cs @@ -946,8 +946,8 @@ public abstract partial class AdaptiveToolPage : ToolPageBase var wrap = new VariableSizedWrapGrid { Orientation = Orientation.Horizontal, - ItemWidth = 190, - ItemHeight = 74 + ItemWidth = 200, + ItemHeight = 60 }; wrap.Children.Add(_numberBox); if (_secondaryNumberBox.Visibility == Visibility.Visible)