style: polish admin operations pages

This commit is contained in:
boos
2026-06-06 22:19:42 +08:00
parent 6629d9d0a3
commit 16162095f7
9 changed files with 462 additions and 19 deletions
+442
View File
@@ -13992,3 +13992,445 @@ textarea:focus {
width: 100%;
}
}
/* ===== Admin Operations Pages Polish ===== */
.admin-ops-page {
--ops-line: color-mix(in srgb, var(--color-border) 74%, transparent);
--ops-line-strong: color-mix(in srgb, var(--color-border) 92%, var(--color-text-muted) 8%);
--ops-surface: color-mix(in srgb, var(--color-bg-card) 92%, var(--color-bg) 8%);
--ops-surface-soft: color-mix(in srgb, var(--color-bg-card) 84%, var(--color-bg) 16%);
--ops-ink: var(--color-text-primary);
--ops-muted: var(--color-text-muted);
min-width: 0;
}
.admin-ops-page .page-header {
align-items: flex-start;
gap: 14px 18px;
margin-bottom: 16px;
padding: 14px 16px;
border: 1px solid var(--ops-line);
border-radius: 18px;
background:
linear-gradient(180deg, color-mix(in srgb, var(--color-bg-card) 96%, transparent), var(--ops-surface-soft));
box-shadow: 0 12px 28px rgba(21, 32, 54, 0.045);
}
.admin-ops-page .page-header-copy {
gap: 6px;
}
.admin-ops-page .page-title {
color: var(--ops-ink);
font-size: 20px;
font-weight: 780;
letter-spacing: 0;
}
.admin-ops-page .page-subtitle {
max-width: 760px;
color: var(--ops-muted);
font-size: 12.5px;
line-height: 1.6;
}
.admin-ops-page .page-actions {
justify-content: flex-end;
gap: 8px;
}
.admin-ops-page .page-actions .btn,
.admin-ops-page .toolbar .btn,
.admin-ops-page .oauth-toolbar .btn,
.admin-ops-page .route-filter-bar .btn {
min-height: 34px;
border-radius: 10px;
white-space: nowrap;
}
.admin-ops-page .btn-ghost,
.admin-ops-page .oauth-outline-button {
background: color-mix(in srgb, var(--color-bg-card) 72%, transparent);
}
.admin-ops-page .btn-ghost:hover,
.admin-ops-page .oauth-outline-button:hover {
border-color: color-mix(in srgb, var(--color-primary) 26%, var(--ops-line-strong));
background: color-mix(in srgb, var(--color-primary) 5%, var(--color-bg-card));
}
.admin-ops-page .card,
.admin-ops-page .oauth-toolbar-card,
.admin-ops-page .oauth-workbench-card,
.admin-ops-page .oauth-drawer-panel,
.admin-ops-page .route-filter-bar,
.admin-ops-page .route-card-collapsed,
.admin-ops-page .route-card-expanded {
border-color: var(--ops-line);
border-radius: 16px;
background:
linear-gradient(180deg, color-mix(in srgb, var(--color-bg-card) 97%, transparent), color-mix(in srgb, var(--color-bg-card) 88%, var(--color-bg) 12%));
box-shadow: 0 10px 24px rgba(21, 32, 54, 0.04);
}
.admin-ops-page .card:hover,
.admin-ops-page .oauth-toolbar-card:hover,
.admin-ops-page .oauth-workbench-card:hover,
.admin-ops-page .route-filter-bar:hover {
box-shadow: 0 12px 26px rgba(21, 32, 54, 0.05);
}
.admin-ops-page .toolbar,
.admin-ops-page .oauth-toolbar,
.admin-ops-page .route-filter-bar-expanded {
gap: 10px;
}
.admin-ops-page .toolbar,
.admin-ops-page .oauth-toolbar-card {
padding: 12px;
border: 1px solid var(--ops-line);
border-radius: 16px;
background: var(--ops-surface);
}
.admin-ops-page .toolbar-search {
min-width: min(320px, 100%);
}
.admin-ops-page .toolbar-search input,
.admin-ops-page .oauth-input,
.admin-ops-page .oauth-textarea,
.site-announcements-page input,
.site-announcements-page textarea,
.site-announcements-page select,
.notification-settings-page input:not([type="checkbox"]):not([type="radio"]) {
border-color: var(--ops-line-strong) !important;
border-radius: 11px !important;
background: color-mix(in srgb, var(--color-bg-card) 78%, var(--color-bg) 22%) !important;
box-shadow: inset 0 1px 0 color-mix(in srgb, white 56%, transparent);
}
.admin-ops-page .toolbar-search input:focus,
.admin-ops-page .oauth-input:focus,
.admin-ops-page .oauth-textarea:focus,
.site-announcements-page input:focus,
.site-announcements-page textarea:focus,
.site-announcements-page select:focus,
.notification-settings-page input:not([type="checkbox"]):not([type="radio"]):focus {
border-color: color-mix(in srgb, var(--color-primary) 52%, var(--ops-line-strong)) !important;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 12%, transparent) !important;
}
.admin-ops-page .data-table {
font-size: 12.5px;
}
.admin-ops-page .data-table thead {
background: color-mix(in srgb, var(--color-bg) 64%, var(--color-bg-card) 36%);
}
.admin-ops-page .data-table th {
height: 38px;
padding: 9px 12px;
color: color-mix(in srgb, var(--color-text-muted) 82%, var(--color-text-primary) 18%);
font-size: 11.5px;
font-weight: 760;
letter-spacing: 0;
text-transform: none;
}
.admin-ops-page .data-table td {
padding: 11px 12px;
color: var(--color-text-secondary);
line-height: 1.45;
}
.admin-ops-page .data-table tbody tr:hover {
background: color-mix(in srgb, var(--color-primary) 5%, transparent);
}
.admin-ops-page .mobile-card {
border-color: var(--ops-line);
border-radius: 15px;
background: var(--ops-surface);
box-shadow: 0 10px 22px rgba(21, 32, 54, 0.04);
}
.sites-page-actions,
.accounts-page-actions {
align-items: center;
}
.sites-row-actions,
.accounts-row-actions,
.oauth-row-actions {
gap: 7px;
justify-content: flex-end;
}
.sites-row-actions .btn-link,
.accounts-row-actions .btn-link,
.oauth-row-actions .btn-link,
.site-announcements-row-actions .btn-link {
min-height: 28px;
padding-inline: 4px;
font-size: 12px;
font-weight: 680;
}
.downstream-keys-page {
padding: 0 !important;
}
.downstream-keys-page .card {
padding: 16px !important;
}
.downstream-keys-page .toolbar {
margin-bottom: 0 !important;
}
.downstream-keys-page .kpi-chip,
.proxy-logs-page .kpi-chip {
border-color: var(--ops-line);
background: color-mix(in srgb, var(--color-bg-card) 78%, var(--color-bg) 22%);
}
.token-routes-page > div:first-child {
padding: 12px;
border: 1px solid var(--ops-line);
border-radius: 16px;
background: var(--ops-surface);
}
.token-routes-page .route-filter-bar {
margin-top: 10px;
margin-bottom: 12px;
}
.token-routes-page .route-card-grid {
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
gap: 12px;
}
.token-routes-page .route-card-collapsed,
.token-routes-page .route-card-expanded {
border-radius: 15px;
}
.proxy-logs-page .page-actions {
max-width: 760px;
}
.proxy-logs-page .toolbar {
margin-bottom: 12px !important;
align-items: stretch;
}
.proxy-logs-page .proxy-logs-filter-select,
.proxy-logs-page .proxy-logs-time-field,
.proxy-logs-page .proxy-logs-filter-reset {
min-height: 34px;
}
.oauth-ops-page .oauth-toolbar-card {
margin-bottom: 14px;
}
.oauth-ops-page .oauth-workbench-head {
padding-bottom: 12px;
border-bottom: 1px solid var(--ops-line);
}
.oauth-ops-page .oauth-table th,
.oauth-ops-page .oauth-table td {
padding-inline: 12px;
}
.site-announcements-layout {
grid-template-columns: minmax(300px, 380px) minmax(0, 1fr) !important;
gap: 16px !important;
}
.site-announcements-form-card {
position: sticky;
top: calc(var(--topbar-height) + 20px);
}
.site-announcements-section-title {
color: var(--ops-ink);
letter-spacing: 0;
}
.site-announcements-form-card label span {
color: var(--color-text-secondary) !important;
font-weight: 680;
}
.site-announcements-form-card textarea {
min-height: 188px;
}
.site-announcements-list-card {
overflow: hidden;
}
.site-announcements-list-head {
background: color-mix(in srgb, var(--color-bg) 62%, var(--color-bg-card) 38%);
}
.site-announcements-row {
transition: background-color 0.16s ease;
}
.site-announcements-row:hover {
background: color-mix(in srgb, var(--color-primary) 4%, transparent);
}
.site-announcements-row-head {
min-width: 0;
}
.site-announcements-row-head > div:first-child {
min-width: 0;
}
.site-announcements-row-head > div:first-child > div:first-child {
overflow: hidden;
color: var(--ops-ink);
text-overflow: ellipsis;
white-space: nowrap;
}
.site-announcements-meta {
padding-top: 8px;
border-top: 1px dashed var(--ops-line);
}
.notification-settings-page .page-header {
align-items: flex-start;
}
.notification-settings-layout {
width: 100%;
max-width: 1120px !important;
gap: 16px !important;
}
.notification-settings-page .card {
padding: 20px !important;
}
.notification-channel-card {
position: relative;
overflow: hidden;
border-radius: 17px !important;
}
.notification-channel-card::before,
.notification-settings-page .card:first-child::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: color-mix(in srgb, var(--color-primary) 68%, transparent);
opacity: 0.45;
}
.notification-channel-card > div:first-child,
.notification-settings-page .card:first-child > div:first-child {
position: relative;
}
.notification-channel-card code {
border-color: var(--ops-line) !important;
border-radius: 11px !important;
background: color-mix(in srgb, var(--color-bg) 74%, var(--color-bg-card) 26%) !important;
overflow-wrap: anywhere;
}
.notification-channel-card label,
.notification-settings-page .card label {
user-select: none;
}
.notification-channel-card input[type="checkbox"],
.notification-settings-page input[type="checkbox"] {
width: 16px !important;
height: 16px !important;
accent-color: var(--color-primary);
box-shadow: none !important;
}
[data-theme="dark"] .admin-ops-page {
--ops-line: color-mix(in srgb, var(--color-border) 78%, transparent);
--ops-line-strong: color-mix(in srgb, var(--color-border) 92%, #94a3b8 8%);
--ops-surface: color-mix(in srgb, var(--color-bg-card) 90%, #020617 10%);
--ops-surface-soft: color-mix(in srgb, var(--color-bg-card) 86%, #020617 14%);
}
[data-theme="dark"] .admin-ops-page .page-header,
[data-theme="dark"] .admin-ops-page .card,
[data-theme="dark"] .admin-ops-page .toolbar,
[data-theme="dark"] .admin-ops-page .oauth-toolbar-card,
[data-theme="dark"] .admin-ops-page .oauth-workbench-card,
[data-theme="dark"] .admin-ops-page .route-filter-bar,
[data-theme="dark"] .admin-ops-page .route-card-collapsed,
[data-theme="dark"] .admin-ops-page .route-card-expanded {
box-shadow: none;
}
[data-theme="dark"] .admin-ops-page .toolbar-search input,
[data-theme="dark"] .admin-ops-page .oauth-input,
[data-theme="dark"] .admin-ops-page .oauth-textarea,
[data-theme="dark"] .site-announcements-page input,
[data-theme="dark"] .site-announcements-page textarea,
[data-theme="dark"] .site-announcements-page select,
[data-theme="dark"] .notification-settings-page input:not([type="checkbox"]):not([type="radio"]) {
box-shadow: none !important;
}
@media (max-width: 1180px) {
.site-announcements-layout {
grid-template-columns: 1fr !important;
}
.site-announcements-form-card {
position: static;
}
}
@media (max-width: 860px) {
.admin-ops-page .page-header {
padding: 12px;
border-radius: 16px;
}
.admin-ops-page .page-actions {
width: 100%;
justify-content: flex-start;
}
.admin-ops-page .page-actions .btn,
.sites-page-actions .btn,
.accounts-page-actions .btn {
flex: 1 1 auto;
justify-content: center;
}
.token-routes-page .route-card-grid {
grid-template-columns: 1fr;
}
.notification-channel-card > div:first-child,
.notification-settings-page .card > div:first-child {
align-items: flex-start !important;
flex-direction: column;
}
.notification-channel-card > div:nth-child(2),
.notification-channel-card > div:nth-child(3) {
grid-template-columns: 1fr !important;
}
}
+1 -1
View File
@@ -1339,7 +1339,7 @@ export default function Accounts() {
: canAddVerifiedConnection;
return (
<div className="animate-fade-in">
<div className="animate-fade-in admin-ops-page accounts-ops-page">
<div className="page-header">
<h2 className="page-title">{tr("连接管理")}</h2>
{activeSegment !== "tokens" && (
+1 -1
View File
@@ -1065,7 +1065,7 @@ export default function DownstreamKeys() {
const empty = !loading && visibleItems.length === 0;
return (
<div className="animate-fade-in" style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 12 }}>
<div className="animate-fade-in admin-ops-page downstream-keys-page" style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 12 }}>
<div className="page-header" style={{ marginBottom: 0 }}>
<div>
<h2 className="page-title">下游密钥</h2>
+4 -4
View File
@@ -179,7 +179,7 @@ export default function NotificationSettings() {
if (loading) {
return (
<div className="animate-fade-in">
<div className="animate-fade-in admin-ops-page notification-settings-page">
<div className="skeleton" style={{ width: 220, height: 28, marginBottom: 20 }} />
<div className="skeleton" style={{ width: '100%', height: 320, borderRadius: 'var(--radius-sm)' }} />
</div>
@@ -187,7 +187,7 @@ export default function NotificationSettings() {
}
return (
<div className="animate-fade-in notification-settings-page" style={{ paddingBottom: 40 }}>
<div className="animate-fade-in admin-ops-page notification-settings-page" style={{ paddingBottom: 40 }}>
<PageHeader
title={tr('通知设置')}
actions={(
@@ -204,7 +204,7 @@ export default function NotificationSettings() {
<div className="notification-settings-layout" style={{ maxWidth: 980, display: 'flex', flexDirection: 'column', gap: 20 }}>
<div className="card animate-slide-up stagger-1" style={{ padding: 20 }}>
<div className="card notification-settings-card animate-slide-up stagger-1" style={{ padding: 20 }}>
<div style={{ fontWeight: 600, fontSize: 15, marginBottom: 8 }}>告警去噪与冷静期</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
相同告警在冷静期内不会重复推送;冷静期结束后会自动合并重复条数。
@@ -227,7 +227,7 @@ export default function NotificationSettings() {
</div>
{/* 卡片:Webhook & Bark */}
<div className="card animate-slide-up stagger-2" style={{ padding: 24, border: (runtime.webhookEnabled || runtime.barkEnabled) ? '1px solid var(--color-primary)' : '1px solid var(--color-border-light)' }}>
<div className="card notification-channel-card animate-slide-up stagger-2" style={{ padding: 24, border: (runtime.webhookEnabled || runtime.barkEnabled) ? '1px solid var(--color-primary)' : '1px solid var(--color-border-light)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'var(--color-primary-light)', color: 'var(--color-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
+1 -1
View File
@@ -2016,7 +2016,7 @@ export default function OAuthManagement() {
);
return (
<div className="page-container animate-fade-in">
<div className="page-container animate-fade-in admin-ops-page oauth-ops-page">
<div className="page-header">
<div>
<h2 className="page-title">OAuth 管理</h2>
+1 -1
View File
@@ -2105,7 +2105,7 @@ export default function ProxyLogs() {
);
return (
<div className="animate-fade-in">
<div className="animate-fade-in admin-ops-page proxy-logs-page">
<div className="page-header" style={{ marginBottom: 16 }}>
<div>
<h2 className="page-title">{tr("使用日志")}</h2>
+10 -9
View File
@@ -176,7 +176,7 @@ export default function SiteAnnouncements() {
};
return (
<div className="animate-fade-in">
<div className="animate-fade-in admin-ops-page site-announcements-page">
<PageHeader
title={tr('用户公告')}
subtitle="管理员发布给普通用户的站内公告,会在普通用户面板中展示。"
@@ -201,9 +201,9 @@ export default function SiteAnnouncements() {
)}
/>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(280px, 360px) 1fr', gap: 14, alignItems: 'start' }}>
<div className="card" style={{ padding: 18 }}>
<h3 style={{ fontSize: 15, fontWeight: 600, marginBottom: 14 }}>
<div className="site-announcements-layout" style={{ display: 'grid', gridTemplateColumns: 'minmax(280px, 360px) 1fr', gap: 14, alignItems: 'start' }}>
<div className="card site-announcements-form-card" style={{ padding: 18 }}>
<h3 className="site-announcements-section-title" style={{ fontSize: 15, fontWeight: 600, marginBottom: 14 }}>
{form.id ? '编辑公告' : '发布公告'}
</h3>
<div style={{ display: 'grid', gap: 12 }}>
@@ -265,8 +265,9 @@ export default function SiteAnnouncements() {
</div>
</div>
<div className="card" style={{ padding: 0 }}>
<div className="card site-announcements-list-card" style={{ padding: 0 }}>
<div
className="site-announcements-list-head"
style={{
padding: '14px 18px',
borderBottom: '1px solid var(--color-border-light)',
@@ -297,13 +298,13 @@ export default function SiteAnnouncements() {
return (
<div
key={row.id}
className={`animate-slide-up stagger-${Math.min(index + 1, 5)}`}
className={`site-announcements-row animate-slide-up stagger-${Math.min(index + 1, 5)}`}
style={{
padding: '16px 18px',
borderBottom: index === rows.length - 1 ? 'none' : '1px solid var(--color-border-light)',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'flex-start', marginBottom: 8 }}>
<div className="site-announcements-row-head" style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'flex-start', marginBottom: 8 }}>
<div>
<div style={{ fontSize: 15, fontWeight: 600 }}>{row.title}</div>
<div style={{ marginTop: 5, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
@@ -316,7 +317,7 @@ export default function SiteAnnouncements() {
<span className="badge badge-muted">已读 {row.readCount || 0}</span>
</div>
</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<div className="site-announcements-row-actions" style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<button type="button" className="btn btn-link btn-link-primary" onClick={() => editRow(row)}>
编辑
</button>
@@ -326,7 +327,7 @@ export default function SiteAnnouncements() {
</div>
</div>
<SiteAnnouncementContent content={row.content} />
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--color-text-muted)' }}>
<div className="site-announcements-meta" style={{ marginTop: 8, fontSize: 12, color: 'var(--color-text-muted)' }}>
发布时间:{row.publishedAt || row.firstSeenAt || '-'}
</div>
</div>
+1 -1
View File
@@ -1148,7 +1148,7 @@ export default function Sites() {
};
return (
<div className="animate-fade-in">
<div className="animate-fade-in admin-ops-page sites-ops-page">
<div className="page-header">
<h2 className="page-title">{tr('站点管理')}</h2>
<div className="page-actions sites-page-actions">
+1 -1
View File
@@ -1559,7 +1559,7 @@ export default function TokenRoutes() {
};
return (
<div className="animate-fade-in" style={{ minHeight: 400 }}>
<div className="animate-fade-in admin-ops-page token-routes-page" style={{ minHeight: 400 }}>
{/* Toolbar: search + sort + actions */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, flexWrap: 'wrap' }}>
<div className="toolbar-search" style={{ minWidth: 220, flex: 1, maxWidth: 360 }}>