feat: 品牌统一 LokiStack + 全站中英 i18n + 用户批量删除 + 主题定时切换与浅色侧栏

- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂
- i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏
- 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条
- 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器
- UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正
- 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简
- 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
This commit is contained in:
opsx committed 2026-10-27 17:11:38 +08:00
1 parent 02d1bd87a7
commit f3733f0bfc
201 files changed
+5438 -2388

No files matched your search

@@ -1,13 +1,20 @@
import { memo } from 'react'
import { useNavigate } from 'react-router'
import { Rocket } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { EmptyState } from '@/components/EmptyState'
import { StatusIcon } from '@/components/StatusBadge'
import { getStatusColorClass } from '@/components/status-config'
import { statusLabel } from '@/lib/utils'
import type { LokiDeployment } from '@/types'
// 状态标签走 common.status.*;未收录的状态原样展示(与 StatusBadge 口径一致)
function statusLabelOf(t: (key: string, opts?: { ns?: string }) => string, status: string): string {
const key = `status.${status}`
const label = t(key, { ns: 'common' })
return label === key ? status : label
}
/** 最近部署卡片 — 展示最近 5 条部署,点击跳转部署页 */
export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
deployments,
@@ -16,18 +23,19 @@ export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
deployments: LokiDeployment[]
onViewAll: () => void
}) {
const { t } = useTranslation('dashboard')
const navigate = useNavigate()
return (
<Card className="border-border">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base">最近部署</CardTitle>
<CardTitle className="text-base">{t('recent.title')}</CardTitle>
<button
onClick={onViewAll}
className="text-xs text-primary hover:text-primary/80 font-medium"
>
查看全部
{t('recent.viewAll')}
</button>
</div>
</CardHeader>
@@ -36,8 +44,8 @@ export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
{deployments.length === 0 ? (
<EmptyState
icon={Rocket}
title="暂无部署"
description="创建你的第一个 Loki 部署"
title={t('recent.emptyTitle')}
description={t('recent.emptyDesc')}
/>
) : (
deployments.map((d) => (
@@ -62,7 +70,7 @@ export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
d.status
)}`}
>
{statusLabel(d.status)}
{statusLabelOf(t, d.status)}
</span>
</div>
))
@@ -1,7 +1,7 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { getStatusColorClass } from '@/components/status-config'
import { statusLabel } from '@/lib/utils'
// 用具体结构类型而非泛型:memo 包裹泛型组件会导致 TS 无法从 props 推断 T
interface ResourceItem {
@@ -22,6 +22,13 @@ interface ResourceListCardProps {
getItemExtra?: (item: ResourceItem) => string | undefined
}
// 状态标签走 common.status.*;未收录的状态原样展示(与 StatusBadge 口径一致)
function statusLabelOf(t: (key: string, opts?: { ns?: string }) => string, status: string): string {
const key = `status.${status}`
const label = t(key, { ns: 'common' })
return label === key ? status : label
}
/** 资源列表卡片 — 集群/主机快捷列表(仪表盘) */
export const ResourceListCard = memo(function ResourceListCard({
title,
@@ -33,6 +40,7 @@ export const ResourceListCard = memo(function ResourceListCard({
getItemStatus,
getItemExtra,
}: ResourceListCardProps) {
const { t } = useTranslation('dashboard')
return (
<Card className="border-border">
<CardHeader className="pb-3">
@@ -45,14 +53,16 @@ export const ResourceListCard = memo(function ResourceListCard({
onClick={onManage}
className="text-xs text-primary hover:text-primary/80 font-medium"
>
管理
{t('list.manage')}
</button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
{items.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">暂无{title}</p>
<p className="text-sm text-muted-foreground text-center py-4">
{t('list.empty', { title })}
</p>
) : (
items.map((item) => (
<div
@@ -73,7 +83,7 @@ export const ResourceListCard = memo(function ResourceListCard({
getItemStatus(item)
)}`}
>
{statusLabel(getItemStatus(item))}
{statusLabelOf(t, getItemStatus(item))}
</span>
</div>
))
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import i18n from '@/i18n'
import { clusterApi, deploymentApi, hostApi } from '@/api'
import type { Cluster, DashboardStats, Host, LokiDeployment } from '@/types'
@@ -47,7 +48,7 @@ export function useDashboardStats(intervalMs: number = REFRESH_INTERVAL) {
setError(null)
} catch (err) {
if (version !== versionRef.current) return
setError(err instanceof Error ? err.message : '加载仪表盘数据失败')
setError(err instanceof Error ? err.message : i18n.t('dashboard:loadFailed'))
} finally {
if (version === versionRef.current) {
setLoading(false)
+26 -20
View File
@@ -2,6 +2,8 @@ import { memo, useCallback, useMemo } from 'react'
import { useNavigate } from 'react-router'
import { Server, Monitor, Activity, TrendingUp, CheckCircle, Rocket, Wand2 } from 'lucide-react'
import { Link } from 'react-router'
import { useTranslation } from 'react-i18next'
import '@/i18n/domains'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { PageContainer } from '@/components/PageContainer'
@@ -13,10 +15,11 @@ import { RecentDeploymentsCard } from './components/RecentDeploymentsCard'
import { ResourceListCard } from './components/ResourceListCard'
import type { DashboardStats } from '@/types'
// 状态概览三色指标;标签走 common.status.*(渲染处翻译)
const STATUS_ITEMS = [
{ key: 'running', label: '运行中', colorClass: 'text-primary' },
{ key: 'failed', label: '失败', colorClass: 'text-destructive' },
{ key: 'pending', label: '等待中', colorClass: 'text-warning' },
{ key: 'running', colorClass: 'text-primary' },
{ key: 'failed', colorClass: 'text-destructive' },
{ key: 'pending', colorClass: 'text-warning' },
] as const
/**
@@ -24,6 +27,7 @@ const STATUS_ITEMS = [
* 数据由 useDashboardStats 轮询(页面隐藏暂停);首次失败渲染错误态而非空数据。
*/
const DashboardPage = memo(function DashboardPage() {
const { t } = useTranslation('dashboard')
const { data, loading, error, refresh } = useDashboardStats()
const navigate = useNavigate()
@@ -39,7 +43,7 @@ const DashboardPage = memo(function DashboardPage() {
const cards: Array<React.ReactNode> = [
<StatCard
key="deployments"
label="部署总数"
label={t('stat.totalDeployments')}
value={stats.totalDeployments}
icon={Rocket}
iconColor="text-primary"
@@ -48,12 +52,12 @@ const DashboardPage = memo(function DashboardPage() {
icon: TrendingUp,
color: 'text-primary',
textColor: 'text-primary',
text: `${stats.running} 个运行中`,
text: t('stat.runningCount', { count: stats.running }),
}}
/>,
<StatCard
key="clusters"
label="集群"
label={t('stat.clusters')}
value={stats.clusterCount}
icon={Server}
iconColor="text-info"
@@ -62,12 +66,12 @@ const DashboardPage = memo(function DashboardPage() {
icon: CheckCircle,
color: 'text-primary',
textColor: 'text-muted-foreground',
text: '已接入',
text: t('stat.connected'),
}}
/>,
<StatCard
key="hosts"
label="主机"
label={t('stat.hosts')}
value={stats.hostCount}
icon={Monitor}
iconColor="text-purple-600"
@@ -76,12 +80,12 @@ const DashboardPage = memo(function DashboardPage() {
icon: CheckCircle,
color: 'text-primary',
textColor: 'text-muted-foreground',
text: 'SSH 就绪',
text: t('stat.sshReady'),
}}
/>,
]
return cards
}, [stats])
}, [stats, t])
if (loading) {
return <LoadingSpinner className="h-64" />
@@ -93,7 +97,7 @@ const DashboardPage = memo(function DashboardPage() {
return (
<PageContainer>
{/* 快速开始引导:尚无任何部署时置顶(状态推导引导,opsx-sec 范式) */}
{/* 快速开始引导:尚无任何部署时置顶(状态推导式引导) */}
{(data?.stats.totalDeployments ?? 0) === 0 && (
<Card className="border-primary/30 bg-primary/5">
<CardContent className="p-5 flex items-center justify-between flex-wrap gap-3">
@@ -102,15 +106,15 @@ const DashboardPage = memo(function DashboardPage() {
<Wand2 className="w-5 h-5 text-primary" />
</div>
<div>
<p className="font-medium">三步开始收集日志</p>
<p className="font-medium">{t('quickStart.title')}</p>
<p className="text-xs text-muted-foreground mt-0.5">
环境与目标 → 存储 → 部署 Loki 与 Grafana,最后添加采集策略
{t('quickStart.subtitle')}
</p>
</div>
</div>
<Button asChild className="bg-primary hover:bg-primary/90">
<Link to="/wizard">
进入部署向导
{t('quickStart.cta')}
<Wand2 className="w-4 h-4 ml-1" />
</Link>
</Button>
@@ -127,14 +131,16 @@ const DashboardPage = memo(function DashboardPage() {
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">状态</p>
<p className="text-sm text-muted-foreground">{t('stat.status')}</p>
<div className="flex gap-3 mt-1">
{STATUS_ITEMS.map((item) => (
<div key={item.key} className="text-center">
<p className={`text-lg font-bold tabular-nums ${item.colorClass}`}>
{(stats?.[item.key as keyof DashboardStats] as number) || 0}
</p>
<p className="text-xs text-muted-foreground">{item.label}</p>
<p className="text-xs text-muted-foreground">
{t(`status.${item.key}`, { ns: 'common' })}
</p>
</div>
))}
</div>
@@ -156,7 +162,7 @@ const DashboardPage = memo(function DashboardPage() {
<div className="space-y-4">
<ResourceListCard
title="集群"
title={t('stat.clusters')}
icon={<Server className="w-4 h-4 text-info" />}
items={(data?.clusters ?? []).slice(0, 3)}
onManage={navigateTo('/clusters')}
@@ -165,11 +171,11 @@ const DashboardPage = memo(function DashboardPage() {
getItemStatus={(c) => c.status}
/>
<ResourceListCard
title="主机"
icon={<Monitor className="w-4 h-4 text-purple-500" />}
title={t('stat.hosts')}
icon={<Monitor className="w-4 h-4 text-info" />}
items={(data?.hosts ?? []).slice(0, 3)}
onManage={navigateTo('/hosts')}
getItemIcon={() => <Monitor className="w-4 h-4 text-purple-500" />}
getItemIcon={() => <Monitor className="w-4 h-4 text-info" />}
getItemLabel={(h) => h.name}
getItemStatus={(h) => h.status}
getItemExtra={(h) => h.hostIP}