refactor: share console page header

This commit is contained in:
boos
2026-06-06 20:18:28 +08:00
parent 823ac3c25d
commit 44dc89a0dd
11 changed files with 164 additions and 119 deletions
-3
View File
@@ -405,9 +405,6 @@ CREATE TABLE IF NOT EXISTS proxy_logs (
client_app_name TEXT, client_app_name TEXT,
client_confidence TEXT, client_confidence TEXT,
billing_details TEXT, billing_details TEXT,
content_summary TEXT,
request_content TEXT,
response_content TEXT,
created_at TEXT DEFAULT (datetime('now')) created_at TEXT DEFAULT (datetime('now'))
); );
@@ -1,3 +1,3 @@
-- No-op migration. ALTER TABLE proxy_logs ADD COLUMN content_summary TEXT;
-- The proxy log content columns are present in the baseline schema and are ALTER TABLE proxy_logs ADD COLUMN request_content TEXT;
-- also protected by src/db/schema_compat.rs for older SQLite databases. ALTER TABLE proxy_logs ADD COLUMN response_content TEXT;
-3
View File
@@ -405,9 +405,6 @@ CREATE TABLE IF NOT EXISTS proxy_logs (
client_app_name TEXT, client_app_name TEXT,
client_confidence TEXT, client_confidence TEXT,
billing_details TEXT, billing_details TEXT,
content_summary TEXT,
request_content TEXT,
response_content TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT) created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
); );
+32
View File
@@ -0,0 +1,32 @@
import type { CSSProperties, ReactNode } from 'react';
type PageHeaderProps = {
title: ReactNode;
subtitle?: ReactNode;
actions?: ReactNode;
className?: string;
style?: CSSProperties;
titleAs?: 'h1' | 'h2';
};
export default function PageHeader({
title,
subtitle,
actions,
className = '',
style,
titleAs = 'h2',
}: PageHeaderProps) {
const Heading = titleAs;
const classes = ['page-header', className].filter(Boolean).join(' ');
return (
<div className={classes} style={style}>
<div className="page-header-copy">
<Heading className="page-title">{title}</Heading>
{subtitle ? <div className="page-subtitle">{subtitle}</div> : null}
</div>
{actions ? <div className="page-actions">{actions}</div> : null}
</div>
);
}
+10
View File
@@ -2926,12 +2926,22 @@ textarea {
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
.page-header-copy {
min-width: 0;
display: grid;
gap: 4px;
}
.page-subtitle { .page-subtitle {
margin: 4px 0 0; margin: 4px 0 0;
font-size: 12px; font-size: 12px;
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.page-header-copy .page-subtitle {
margin: 0;
}
.page-actions { .page-actions {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
+8 -6
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { api, type RuntimeSettingsPayload } from '../api.js'; import { api, type RuntimeSettingsPayload } from '../api.js';
import PageHeader from '../components/PageHeader.js';
import { useToast } from '../components/Toast.js'; import { useToast } from '../components/Toast.js';
import { tr } from '../i18n.js'; import { tr } from '../i18n.js';
@@ -187,18 +188,19 @@ export default function NotificationSettings() {
return ( return (
<div className="animate-fade-in notification-settings-page" style={{ paddingBottom: 40 }}> <div className="animate-fade-in notification-settings-page" style={{ paddingBottom: 40 }}>
{/* 头部标题与操作 */} <PageHeader
<div className="page-header"> title={tr('通知设置')}
<h2 className="page-title">{tr('通知设置')}</h2> actions={(
<div className="page-actions"> <>
<button onClick={testNotify} disabled={testingNotify} className="btn btn-success"> <button onClick={testNotify} disabled={testingNotify} className="btn btn-success">
{testingNotify ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 发送中...</> : '发送测试通知'} {testingNotify ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 发送中...</> : '发送测试通知'}
</button> </button>
<button onClick={saveNotify} disabled={savingNotify} className="btn btn-primary"> <button onClick={saveNotify} disabled={savingNotify} className="btn btn-primary">
{savingNotify ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存通知设置'} {savingNotify ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存通知设置'}
</button> </button>
</div> </>
</div> )}
/>
<div className="notification-settings-layout" style={{ maxWidth: 980, display: 'flex', flexDirection: 'column', gap: 20 }}> <div className="notification-settings-layout" style={{ maxWidth: 980, display: 'flex', flexDirection: 'column', gap: 20 }}>
+31 -28
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { api } from '../api.js'; import { api } from '../api.js';
import { MobileCard, MobileField } from '../components/MobileCard.js'; import { MobileCard, MobileField } from '../components/MobileCard.js';
import PageHeader from '../components/PageHeader.js';
import ResponsiveFilterPanel from '../components/ResponsiveFilterPanel.js'; import ResponsiveFilterPanel from '../components/ResponsiveFilterPanel.js';
import { useToast } from '../components/Toast.js'; import { useToast } from '../components/Toast.js';
import { useIsMobile } from '../components/useIsMobile.js'; import { useIsMobile } from '../components/useIsMobile.js';
@@ -185,34 +186,36 @@ export default function ProgramLogs() {
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">
<div className="page-header"> <PageHeader
<h2 className="page-title">{tr('程序日志')}</h2> title={tr('程序日志')}
<div className="page-actions"> actions={(
<button <>
onClick={() => load(true)} <button
disabled={refreshing} onClick={() => load(true)}
className="btn btn-ghost" disabled={refreshing}
style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }} className="btn btn-ghost"
> style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }}
{refreshing ? <><span className="spinner spinner-sm" /> 刷新中...</> : '刷新'} >
</button> {refreshing ? <><span className="spinner spinner-sm" /> 刷新中...</> : '刷新'}
<button </button>
onClick={markAllRead} <button
disabled={markingAll} onClick={markAllRead}
className="btn btn-ghost" disabled={markingAll}
style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }} className="btn btn-ghost"
> style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }}
{markingAll ? <><span className="spinner spinner-sm" /> 标记中...</> : '全部已读'} >
</button> {markingAll ? <><span className="spinner spinner-sm" /> 标记中...</> : '全部已读'}
<button </button>
onClick={clearAll} <button
disabled={clearing} onClick={clearAll}
className="btn btn-link btn-link-danger" disabled={clearing}
> className="btn btn-link btn-link-danger"
{clearing ? <><span className="spinner spinner-sm" /> 清空中...</> : '清空日志'} >
</button> {clearing ? <><span className="spinner spinner-sm" /> 清空中...</> : '清空日志'}
</div> </button>
</div> </>
)}
/>
<ResponsiveFilterPanel <ResponsiveFilterPanel
isMobile={isMobile} isMobile={isMobile}
+24 -25
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { api } from '../api.js'; import { api } from '../api.js';
import PageHeader from '../components/PageHeader.js';
import { useToast } from '../components/Toast.js'; import { useToast } from '../components/Toast.js';
import { SiteAnnouncementContent } from './helpers/siteAnnouncementPresentation.js'; import { SiteAnnouncementContent } from './helpers/siteAnnouncementPresentation.js';
import { tr } from '../i18n.js'; import { tr } from '../i18n.js';
@@ -176,31 +177,29 @@ export default function SiteAnnouncements() {
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">
<div className="page-header"> <PageHeader
<div> title={tr('用户公告')}
<h2 className="page-title">{tr('用户公告')}</h2> subtitle="管理员发布给普通用户的站内公告,会在普通用户面板中展示。"
<div style={{ marginTop: 4, fontSize: 12, color: 'var(--color-text-tertiary)' }}> actions={(
管理员发布给普通用户的站内公告,会在普通用户面板中展示。 <>
</div> <button
</div> onClick={() => load(true)}
<div className="page-actions"> disabled={refreshing}
<button className="btn btn-ghost"
onClick={() => load(true)} style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }}
disabled={refreshing} >
className="btn btn-ghost" {refreshing ? <><span className="spinner spinner-sm" /> 刷新中...</> : '刷新'}
style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }} </button>
> <button
{refreshing ? <><span className="spinner spinner-sm" /> 刷新中...</> : '刷新'} onClick={clearAll}
</button> disabled={clearing || rows.length === 0}
<button className="btn btn-link btn-link-danger"
onClick={clearAll} >
disabled={clearing || rows.length === 0} {clearing ? <><span className="spinner spinner-sm" /> 清空中...</> : '清空公告'}
className="btn btn-link btn-link-danger" </button>
> </>
{clearing ? <><span className="spinner spinner-sm" /> 清空中...</> : '清空公告'} )}
</button> />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(280px, 360px) 1fr', gap: 14, alignItems: 'start' }}> <div style={{ display: 'grid', gridTemplateColumns: 'minmax(280px, 360px) 1fr', gap: 14, alignItems: 'start' }}>
<div className="card" style={{ padding: 18 }}> <div className="card" style={{ padding: 18 }}>
+15 -14
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { userApi } from '../api.js'; import { userApi } from '../api.js';
import PageHeader from '../components/PageHeader.js';
import { useToast } from '../components/Toast.js'; import { useToast } from '../components/Toast.js';
interface LogItem { interface LogItem {
@@ -182,20 +183,20 @@ export default function UsageRecords() {
return ( return (
<div className="page-container animate-fade-in"> <div className="page-container animate-fade-in">
<div className="page-header"> <PageHeader
<div> title="使用记录"
<h2 className="page-title">使用记录</h2> subtitle="当前账号下所有 API 调用日志"
<div className="page-subtitle">当前账号下所有 API 调用日志</div> actions={(
</div> <button
<button type="button"
type="button" className="btn btn-ghost"
className="btn btn-ghost" onClick={() => load(page, search, statusFilter, dateFrom, dateTo)}
onClick={() => load(page, search, statusFilter, dateFrom, dateTo)} disabled={loading}
disabled={loading} >
> 刷新
刷新 </button>
</button> )}
</div> />
{/* Summary cards */} {/* Summary cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12, marginBottom: 20 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12, marginBottom: 20 }}>
+26 -23
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { userApi } from '../api.js'; import { userApi } from '../api.js';
import PageHeader from '../components/PageHeader.js';
import { useToast } from '../components/Toast.js'; import { useToast } from '../components/Toast.js';
import { SiteAnnouncementContent } from './helpers/siteAnnouncementPresentation.js'; import { SiteAnnouncementContent } from './helpers/siteAnnouncementPresentation.js';
@@ -76,29 +77,31 @@ export default function UserAnnouncements() {
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">
<div className="page-header"> <PageHeader
<h2 className="page-title">用户公告</h2> title="用户公告"
<div className="page-actions"> actions={(
<button <>
type="button" <button
onClick={() => load(true)} type="button"
disabled={refreshing} onClick={() => load(true)}
className="btn btn-ghost" disabled={refreshing}
style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }} className="btn btn-ghost"
> style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }}
{refreshing ? <><span className="spinner spinner-sm" /> 刷新中...</> : '刷新'} >
</button> {refreshing ? <><span className="spinner spinner-sm" /> 刷新中...</> : '刷新'}
<button </button>
type="button" <button
onClick={markAllRead} type="button"
disabled={markingAll || rows.length === 0} onClick={markAllRead}
className="btn btn-ghost" disabled={markingAll || rows.length === 0}
style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }} className="btn btn-ghost"
> style={{ border: '1px solid var(--color-border)', padding: '8px 14px' }}
{markingAll ? <><span className="spinner spinner-sm" /> 标记中...</> : '全部已读'} >
</button> {markingAll ? <><span className="spinner spinner-sm" /> 标记中...</> : '全部已读'}
</div> </button>
</div> </>
)}
/>
<div className="card" style={{ padding: 0 }}> <div className="card" style={{ padding: 0 }}>
{loading ? ( {loading ? (
+15 -14
View File
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '../api.js'; import { api } from '../api.js';
import CenteredModal from '../components/CenteredModal.js'; import CenteredModal from '../components/CenteredModal.js';
import DeleteConfirmModal from '../components/DeleteConfirmModal.js'; import DeleteConfirmModal from '../components/DeleteConfirmModal.js';
import PageHeader from '../components/PageHeader.js';
type UserRole = 'admin' | 'user'; type UserRole = 'admin' | 'user';
type UserStatus = 'active' | 'disabled'; type UserStatus = 'active' | 'disabled';
@@ -493,20 +494,20 @@ export default function UserManagement() {
return ( return (
<div className="page-container animate-fade-in user-management-page"> <div className="page-container animate-fade-in user-management-page">
<div className="page-header"> <PageHeader
<div> title="用户管理"
<h2 className="page-title">用户管理</h2> subtitle="维护平台用户、角色与登录状态。禁用不会删除用户数据,可随时恢复。"
<div className="page-subtitle">维护平台用户、角色与登录状态。禁用不会删除用户数据,可随时恢复。</div> actions={(
</div> <>
<div className="page-actions"> <button type="button" className="btn btn-ghost" onClick={loadUsers} disabled={loading || saving}>
<button type="button" className="btn btn-ghost" onClick={loadUsers} disabled={loading || saving}> 刷新
刷新 </button>
</button> <button type="button" className="btn btn-primary" onClick={openCreateModal}>
<button type="button" className="btn btn-primary" onClick={openCreateModal}> 新建用户
新建用户 </button>
</button> </>
</div> )}
</div> />
{error ? <div className="alert alert-error user-management-alert">{error}</div> : null} {error ? <div className="alert alert-error user-management-alert">{error}</div> : null}