chore(i18n,ui): 全站冗长文案精简 + shadcn 样板汉化 + 一致性修复

- 文案:约 25 组长文案中英同步精简(数据源引导/RBAC 注解/采集提示/删除确认等),三处重复 Ingress 教学收敛
- 汉化 shadcn 样板:分页 Previous/Next/aria、弹窗与抽屉 Close 接入 i18n(common.ui.*)
- 一致性:查询授权面板 p-4、仪表盘卡片 p-6、分析卡头 pb-3、部署总数图标配色、
  每日统计错误可重试、策略卡/最近部署行 truncate、状态徽章字重、三处原生 checkbox 换 shadcn、Label 浏览器圆角
- 词典:删除无引用的 raw.*/views.* 死键;zh 复数键风格统一(importCount/hits)
- 快捷查询提示、Ingress 统计口径与表内说明按反馈删减
This commit is contained in:
opsx committed 2026-10-28 22:44:06 +08:00
1 parent 2da0a2bece
commit 3a8eb521e2
34 files changed
+162 -240

No files matched your search

+4 -5
View File
@@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useState } from 'react'
import { Link } from 'react-router'
import { BellRing, Plus, Play, Pencil, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { Card, CardContent } from '@/components/ui/card'
@@ -380,14 +381,12 @@ const AlertsPage = memo(function AlertsPage() {
const checked = field.value.includes(String(ch.id))
return (
<label key={ch.id} className={`flex items-center gap-2 text-sm cursor-pointer rounded-md border px-2.5 py-1.5 ${checked ? 'border-primary bg-primary/5 text-primary' : 'border-border'}`}>
<input
type="checkbox"
<Checkbox
checked={checked}
onChange={(e) => {
if (e.target.checked) field.onChange([...field.value, String(ch.id)])
onCheckedChange={(v) => {
if (v) field.onChange([...field.value, String(ch.id)])
else field.onChange(field.value.filter((x) => x !== String(ch.id)))
}}
className="accent-primary"
/>
{ch.name}
{!ch.enabled && <span className="text-xs text-muted-foreground">{t('form.channelDisabled')}</span>}
@@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/checkbox'
import type { Collector } from '@/types'
// 表单 schema:错误消息经 t 注入(随语言重建,RHF 每次渲染刷新 resolver)
@@ -153,7 +154,7 @@ export const CollectorEditDialog = memo(function CollectorEditDialog({ collector
{nsError && <p className="text-xs text-destructive">{nsError}</p>}
<div className="rounded-md border">
<label className="flex items-center gap-2 border-b px-3 py-2 text-sm font-medium cursor-pointer hover:bg-muted/50">
<input type="checkbox" checked={allSelected} onChange={setAll} />
<Checkbox checked={allSelected} onCheckedChange={() => setAll()} />
{t('edit.allCheckbox')}
</label>
<div className="p-2">
@@ -60,7 +60,7 @@ export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
<p className="text-sm font-medium text-foreground truncate">
{d.name}
</p>
<p className="text-xs text-muted-foreground">
<p className="truncate text-xs text-muted-foreground">
{d.mode} &middot; {d.namespace || 'loki'}
</p>
</div>
@@ -79,7 +79,7 @@ export const ResourceListCard = memo(function ResourceListCard({
)}
</div>
<span
className={`px-2 py-0.5 rounded-full text-xs flex-shrink-0 ${getStatusColorClass(
className={`px-2 py-0.5 rounded-full text-xs font-medium flex-shrink-0 ${getStatusColorClass(
getItemStatus(item)
)}`}
>
+2 -2
View File
@@ -46,7 +46,7 @@ const DashboardPage = memo(function DashboardPage() {
label={t('stat.totalDeployments')}
value={stats.totalDeployments}
icon={Rocket}
iconColor="text-primary"
iconColor="text-success"
iconBg="bg-success/10"
sub={{
icon: TrendingUp,
@@ -100,7 +100,7 @@ const DashboardPage = memo(function DashboardPage() {
{/* 快速开始引导:尚无任何部署时置顶(状态推导式引导) */}
{(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">
<CardContent className="p-6 flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
<Wand2 className="w-5 h-5 text-primary" />
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import { ChevronDown, ChevronRight, Tags } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Input } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/checkbox'
import { logsApi } from '@/api/logs'
import { applyLabelFilter, extractSelector } from '../query-utils'
@@ -76,7 +77,7 @@ export function LokiLabelBrowser({ sourceId, query, onQueryChange, start, end }:
}
}
return <section className="overflow-hidden rounded-lg border bg-card" aria-label={t('labels.browser')}>
return <section className="overflow-hidden rounded-xl border bg-card" aria-label={t('labels.browser')}>
<button type="button" className="flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-medium hover:bg-muted/50" aria-expanded={open} onClick={() => { setOpen(!open); if (!open) setLoadingLabels(true) }}>
{open ? <ChevronDown className="size-4" /> : <ChevronRight className="size-4" />}
<Tags className="size-4 text-info" />{t('labels.browser')}
@@ -88,7 +89,7 @@ export function LokiLabelBrowser({ sourceId, query, onQueryChange, start, end }:
</div>
<div className="min-w-0 p-3">
{!activeLabel ? <p className="py-12 text-center text-sm text-muted-foreground">{t('labels.pickHint')}</p> : <><div className="mb-3 flex flex-wrap items-center gap-2"><strong className="text-sm">{t('labels.valuesOf', { label: activeLabel })}</strong><Input aria-label={t('labels.searchAria', { label: activeLabel })} className="h-8 max-w-sm" placeholder={t('labels.searchPlaceholder')} value={filter} onChange={event => setFilter(event.target.value)} /></div>
<label className="mb-2 flex items-center gap-1.5 text-xs text-muted-foreground"><input type="checkbox" checked={scoped} onChange={event => setScoped(event.target.checked)} />{t('labels.scoped')}</label>
<label className="mb-2 flex items-center gap-1.5 text-xs text-muted-foreground"><Checkbox checked={scoped} onCheckedChange={v => setScoped(Boolean(v))} />{t('labels.scoped')}</label>
<div className="grid max-h-48 gap-1 overflow-auto sm:grid-cols-2 xl:grid-cols-3">{loadingValues ? <p className="p-2 text-sm text-muted-foreground">{t('labels.loading')}</p> : shown.length === 0 ? <p className="p-2 text-sm text-muted-foreground">{t('labels.noValues')}</p> : shown.map(value => <button key={value} type="button" title={value} onClick={() => choose(activeLabel, value)} className={`truncate rounded px-2 py-1.5 text-left font-mono text-sm ${selected[activeLabel] === value ? 'bg-success/10 text-success' : 'hover:bg-muted'}`}>{value}</button>)}</div>
{filter.trim() && !values.includes(filter.trim()) && <button type="button" className="mt-2 rounded border px-2 py-1 text-xs text-primary hover:bg-muted" onClick={() => choose(activeLabel, filter.trim())}>{t('labels.useAsValue', { value: filter.trim() })}</button>}
<p className="mt-2 text-xs text-muted-foreground">{t('labels.hint')}</p>
@@ -86,7 +86,7 @@ export function PolicyGroupsPage({ sources }: { sources: LogSource[] }) {
const sourceName = (id: number) => sources.find(source => source.id === id)?.name ?? t('access.sourceRef', { id })
return <div className="grid gap-6 xl:grid-cols-[minmax(240px,0.85fr)_minmax(0,2fr)]">
<aside className="rounded-xl border bg-card p-5 space-y-4">
<aside className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center justify-between gap-2">
<h2 className="font-semibold">{t('access.listTitle')} <span className="text-sm font-normal text-muted-foreground">({groups.length})</span></h2>
<Button size="sm" onClick={() => setForm(emptyForm(sources[0]?.id ?? 0))}><Plus className="mr-1 size-4" />{t('access.create')}</Button>
@@ -96,7 +96,7 @@ export function PolicyGroupsPage({ sources }: { sources: LogSource[] }) {
{error && <ErrorState message={error} onRetry={() => void reload()} />}
{!loading && !error && groups.length === 0 && <p className="text-sm text-muted-foreground">{t('access.empty')}</p>}
<div className="space-y-2">{groups.map(group => <button key={group.id} type="button" onClick={() => setForm({ ...group, labels: { ...group.labels }, userIds: [...group.userIds] })} className={`w-full rounded-lg border p-3 text-left transition-colors ${form.id === group.id ? 'border-primary bg-primary/5' : 'hover:bg-muted'}`}>
<strong className="block text-sm">{group.name}</strong><span className="block mt-1 text-xs text-muted-foreground">{sourceName(group.sourceId)} · {group.userIds.includes(ANYONE) ? t('access.anyone') : t('access.userCount', { count: group.userIds.length })}</span>{Object.keys(group.labels).length > 0 ? (
<strong className="block text-sm">{group.name}</strong><span className="block mt-1 truncate text-xs text-muted-foreground">{sourceName(group.sourceId)} · {group.userIds.includes(ANYONE) ? t('access.anyone') : t('access.userCount', { count: group.userIds.length })}</span>{Object.keys(group.labels).length > 0 ? (
<span className="mt-1 flex flex-wrap gap-1">
{Object.entries(group.labels).map(([key, value]) => <span key={key} className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground">{key}={value}</span>)}
</span>
@@ -105,7 +105,7 @@ export function PolicyGroupsPage({ sources }: { sources: LogSource[] }) {
)}
</button>)}</div>
</aside>
<section className="rounded-xl border bg-card p-5 space-y-6">
<section className="rounded-xl border bg-card p-4 space-y-6">
<div className="flex items-start justify-between gap-3"><div><h2 className="font-semibold">{form.id ? t('access.editTitle') : t('access.createTitle')}</h2><p className="mt-1 text-sm text-muted-foreground">{t('access.formHint')}</p></div>{form.id && <Button variant="outline" size="sm" className="text-destructive" onClick={() => setDeleteTarget(groups.find(group => group.id === form.id) ?? null)}>{t('access.remove')}</Button>}</div>
<div className="grid gap-4 sm:grid-cols-2"><label className="text-sm font-medium">{t('access.nameLabel')}<Input className="mt-1" value={form.name} onChange={event => setForm(current => ({ ...current, name: event.target.value }))} placeholder={t('access.namePlaceholder')} /></label><label className="text-sm font-medium">{t('access.sourceLabel')}
<Select