refactor(frontend): unify theme tokens to single --dg-* source

- remove shadcn HSL dual-track vars (523->416->~300 lines index.css);
  tailwind semantic colors (border/background/card/primary/destructive/
  popover/muted/accent) now bridge directly to var(--dg-*) tokens
- drop dead darkMode:['class'] config and inert dark: variants in kept
  ui components; theme switching is data-theme based (4 themes intact)
- first-frame inline theme script in index.html (reads namespaced key
  with legacy fallback, resolves system, sets color-scheme) - no flash
- consolidate duplicated db-type brand colors into lib/constants.ts
  single source (dbTypeColorMap + dbTypeBrandColorMap, with pg/mongodb
  aliases); instance health stroke now uses theme tokens
- verified via browser: dark #0d1117 / light #f5f6f8 both resolve,
  tokens cascade correctly
This commit is contained in:
Logan.Li committed 2026-10-26 17:38:27 +08:00
1 parent a213d08e31
commit ac563d1c5e
7 files changed
+99 -223

No files matched your search

+4 -4
View File
@@ -5,19 +5,19 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20:ring-destructive/40",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
"hover:bg-accent hover:text-accent-foreground:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
+3 -146
View File
@@ -3,89 +3,10 @@
@tailwind utilities;
@layer base {
/* ===== Theme 1: Ocean Deep (深蓝专业) — Default ===== */
/* ===== Design Tokens(--dg-* 单一事实源;多主题经 data-theme 整组覆盖) =====
tailwind 语义色(border/background/card/primary/...)由 tailwind.config.js 桥接到这些变量 */
:root {
--background: 220 13% 5%;
--foreground: 210 40% 98%;
--card: 220 13% 7%;
--card-foreground: 210 40% 98%;
--popover: 220 13% 7%;
--popover-foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 220 13% 14%;
--secondary-foreground: 210 40% 98%;
--muted: 220 13% 14%;
--muted-foreground: 215 20% 55%;
--accent: 220 13% 14%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62% 50%;
--destructive-foreground: 0 0% 100%;
--border: 220 13% 18%;
--input: 220 13% 14%;
--ring: 217 91% 60%;
--radius: 0.625rem;
--sidebar-background: 220 13% 7%;
--sidebar-foreground: 210 40% 90%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 13% 14%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 220 13% 18%;
--sidebar-ring: 217 91% 60%;
--dg-bg-primary: #0d1117;
--dg-bg-secondary: #161b22;
--dg-bg-tertiary: #21262d;
--dg-bg-hover: #30363d;
--dg-border: #30363d;
--dg-border-light: #21262d;
--dg-text-primary: #f0f6fc;
--dg-text-secondary: #8b949e;
--dg-text-muted: #6e7681;
--dg-blue: #58a6ff;
--dg-green: #3fb950;
--dg-red: #f85149;
--dg-orange: #f0883e;
--dg-purple: #7c8aa5;
--dg-cyan: #39c5cf;
--dg-yellow: #d29922;
--dg-card-hover-border: rgba(88, 166, 255, 0.5);
--dg-gradient-start: #2563eb;
--dg-gradient-end: #0ea5e9;
--dg-gradient-text-start: #60a5fa;
--dg-gradient-text-end: #93c5fd;
--dg-input-focus-ring: rgba(96, 165, 250, 0.2);
}
[data-theme="ocean-deep"] {
--background: 220 13% 5%;
--foreground: 210 40% 98%;
--card: 220 13% 7%;
--card-foreground: 210 40% 98%;
--popover: 220 13% 7%;
--popover-foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 220 13% 14%;
--secondary-foreground: 210 40% 98%;
--muted: 220 13% 14%;
--muted-foreground: 215 20% 55%;
--accent: 220 13% 14%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62% 50%;
--destructive-foreground: 0 0% 100%;
--border: 220 13% 18%;
--input: 220 13% 14%;
--ring: 217 91% 60%;
--radius: 0.625rem;
--sidebar-background: 220 13% 7%;
--sidebar-foreground: 210 40% 90%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 13% 14%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 220 13% 18%;
--sidebar-ring: 217 91% 60%;
--radius: 0.5rem; /* 卡片/按钮/输入框圆角主值 */
--dg-bg-primary: #0d1117;
--dg-bg-secondary: #161b22;
--dg-bg-tertiary: #21262d;
@@ -113,34 +34,6 @@
/* ===== Theme 2: Light Professional (浅色商务) ===== */
[data-theme="light-professional"],
[data-theme="light"] {
--background: 220 14% 97%;
--foreground: 220 14% 10%;
--card: 0 0% 100%;
--card-foreground: 220 14% 10%;
--popover: 0 0% 100%;
--popover-foreground: 220 14% 10%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 220 14% 96%;
--secondary-foreground: 220 14% 10%;
--muted: 220 14% 96%;
--muted-foreground: 220 10% 46%;
--accent: 220 14% 96%;
--accent-foreground: 220 14% 10%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 217 91% 60%;
--radius: 0.625rem;
--sidebar-background: 0 0% 100%;
--sidebar-foreground: 220 14% 20%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 14% 94%;
--sidebar-accent-foreground: 220 14% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217 91% 60%;
--dg-bg-primary: #f5f6f8;
--dg-bg-secondary: #ffffff;
--dg-bg-tertiary: #eef1f5;
@@ -167,34 +60,6 @@
/* ===== Theme 4: Pine Green (松绿自然) ===== */
[data-theme="pine-green"] {
--background: 150 25% 5%;
--foreground: 140 60% 97%;
--card: 150 25% 9%;
--card-foreground: 140 60% 97%;
--popover: 150 25% 9%;
--popover-foreground: 140 60% 97%;
--primary: 142 71% 45%;
--primary-foreground: 150 25% 5%;
--secondary: 150 20% 18%;
--secondary-foreground: 140 60% 97%;
--muted: 150 20% 18%;
--muted-foreground: 150 20% 55%;
--accent: 150 20% 18%;
--accent-foreground: 140 60% 97%;
--destructive: 0 90% 72%;
--destructive-foreground: 150 25% 5%;
--border: 150 20% 18%;
--input: 150 20% 18%;
--ring: 142 71% 45%;
--radius: 0.625rem;
--sidebar-background: 150 25% 7%;
--sidebar-foreground: 140 40% 88%;
--sidebar-primary: 142 71% 45%;
--sidebar-primary-foreground: 150 25% 5%;
--sidebar-accent: 150 20% 18%;
--sidebar-accent-foreground: 140 60% 97%;
--sidebar-border: 150 20% 18%;
--sidebar-ring: 142 71% 45%;
--dg-bg-primary: #0a1210;
--dg-bg-secondary: #111f1a;
--dg-bg-tertiary: #192c24;
@@ -289,14 +154,6 @@
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: var(--dg-text-muted);
}
[data-theme="light-professional"] .scrollbar-thin::-webkit-scrollbar-thumb,
[data-theme="light"] .scrollbar-thin::-webkit-scrollbar-thumb {
background: #d1d5db;
}
[data-theme="light-professional"] .scrollbar-thin::-webkit-scrollbar-thumb:hover,
[data-theme="light"] .scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
/* ===== Design System Utilities ===== */
+21 -1
View File
@@ -65,9 +65,29 @@ export const statusColorMap: Record<string, string> = {
rollback: 'bg-orange-500/15 text-orange-400',
};
export const dbTypeColorMap: Record<string, string> = {
/**
* 数据库品牌色(刻意不随主题切换,是产品语义而非主题语义)
* 全站唯一源:徽章淡色版 dbTypeColorMap / 实色版 dbTypeBrandColorMap
*/
const DB_TYPE_BADGE = {
mysql: 'bg-[rgba(0,117,143,0.15)] text-[#26a69a]',
postgres: 'bg-[rgba(51,103,145,0.15)] text-[#42a5f5]',
redis: 'bg-[rgba(220,50,32,0.15)] text-[#ef5350]',
mongo: 'bg-[rgba(71,162,72,0.15)] text-[#66bb6a]',
};
export const dbTypeColorMap: Record<string, string> = {
...DB_TYPE_BADGE,
postgresql: DB_TYPE_BADGE.postgres,
pg: DB_TYPE_BADGE.postgres,
mongodb: DB_TYPE_BADGE.mongo,
};
export const dbTypeBrandColorMap: Record<string, string> = {
mysql: 'bg-[#00758f]',
postgres: 'bg-[#336791]',
postgresql: 'bg-[#336791]',
redis: 'bg-[#dc382d]',
mongo: 'bg-[#47a248]',
mongodb: 'bg-[#47a248]',
};
+6 -9
View File
@@ -7,7 +7,7 @@ import {
FileText, ArrowUpRight, ArrowDownRight,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { statusMap, statusColorMap, dbTypeColorMap } from '@/lib/constants';
import { statusMap, statusColorMap, dbTypeColorMap, dbTypeBrandColorMap } from '@/lib/constants';
import { useTheme } from '@/hooks/useTheme';
import { useAuth } from '@/hooks/useAuth';
import { dashboardApi } from '@/api/dashboard';
@@ -378,10 +378,7 @@ export default function Dashboard() {
return (
<div key={inst.id} className="flex items-center px-5 py-3 border-b border-[var(--dg-border-light)] last:border-0 hover:bg-[var(--dg-bg-hover)] transition-colors cursor-pointer group">
<div className={`w-9 h-9 rounded-lg flex items-center justify-center mr-3.5 flex-shrink-0 ${
inst.dbType === 'mysql' ? 'bg-[rgba(0,117,143,0.2)] text-[#26a69a]' :
inst.dbType === 'redis' ? 'bg-[rgba(220,50,32,0.2)] text-[#ef5350]' :
inst.dbType === 'postgres' ? 'bg-[rgba(51,103,145,0.2)] text-[#42a5f5]' :
'bg-[rgba(71,162,72,0.2)] text-[#66bb6a]'
dbTypeColorMap[inst.dbType] || dbTypeColorMap.mysql
}`}>
<Database className="w-4.5 h-4.5" />
</div>
@@ -529,10 +526,10 @@ export default function Dashboard() {
<div className="space-y-2.5">
<div className="text-xs text-[var(--dg-text-muted)] font-medium">查询类型分布</div>
{(queryStats?.type_distribution ?? [
{ label: 'MySQL', count: 0, color: 'bg-[#00758f]' },
{ label: 'PostgreSQL', count: 0, color: 'bg-[#336791]' },
{ label: 'Redis', count: 0, color: 'bg-[#dc382d]' },
{ label: 'MongoDB', count: 0, color: 'bg-[#47a248]' },
{ label: 'MySQL', count: 0, color: dbTypeBrandColorMap.mysql },
{ label: 'PostgreSQL', count: 0, color: dbTypeBrandColorMap.postgres },
{ label: 'Redis', count: 0, color: dbTypeBrandColorMap.redis },
{ label: 'MongoDB', count: 0, color: dbTypeBrandColorMap.mongodb },
]).map((item: any) => {
const total = queryStats?.total || 1;
return (
+14 -13
View File
@@ -9,6 +9,7 @@ import {
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useActionPermission } from '@/hooks/useAuth';
import { dbTypeColorMap } from '@/lib/constants';
import PageHeader from '@/components/PageHeader';
import EmptyState from '@/components/EmptyState';
import { instanceApi } from '@/api/instance';
@@ -51,13 +52,13 @@ type ViewMode = 'grid' | 'list';
/* ------------------------------------------------------------------ */
/* DB Brand Icons */
const dbTypeConfig: Record<string, { icon: React.ElementType; color: string; bg: string; label: string }> = {
mysql: { icon: MySQLIcon, color: 'text-[#26a69a]', bg: 'bg-[rgba(0,117,143,0.15)]', label: 'MySQL' },
postgres: { icon: PostgreSQLIcon, color: 'text-[#42a5f5]', bg: 'bg-[rgba(51,103,145,0.15)]', label: 'PostgreSQL' },
postgresql: { icon: PostgreSQLIcon, color: 'text-[#42a5f5]', bg: 'bg-[rgba(51,103,145,0.15)]', label: 'PostgreSQL' },
pg: { icon: PostgreSQLIcon, color: 'text-[#42a5f5]', bg: 'bg-[rgba(51,103,145,0.15)]', label: 'PostgreSQL' },
redis: { icon: RedisIcon, color: 'text-[#ef5350]', bg: 'bg-[rgba(220,50,32,0.15)]', label: 'Redis' },
mongo: { icon: MongoDBIcon, color: 'text-[#66bb6a]', bg: 'bg-[rgba(71,162,72,0.15)]', label: 'MongoDB' },
const dbTypeConfig: Record<string, { icon: React.ElementType; badge: string; label: string }> = {
mysql: { icon: MySQLIcon, badge: dbTypeColorMap.mysql, label: 'MySQL' },
postgres: { icon: PostgreSQLIcon, badge: dbTypeColorMap.postgres, label: 'PostgreSQL' },
postgresql: { icon: PostgreSQLIcon, badge: dbTypeColorMap.postgresql, label: 'PostgreSQL' },
pg: { icon: PostgreSQLIcon, badge: dbTypeColorMap.pg, label: 'PostgreSQL' },
redis: { icon: RedisIcon, badge: dbTypeColorMap.redis, label: 'Redis' },
mongo: { icon: MongoDBIcon, badge: dbTypeColorMap.mongo, label: 'MongoDB' },
};
const envLabel: Record<string, string> = {
@@ -541,7 +542,7 @@ export default function InstancePage() {
</div>
<div className="flex items-start justify-between mb-4 pl-6">
<div className={`w-9 h-9 rounded-lg ${ic.bg} flex items-center justify-center`}>
<div className={`w-9 h-9 rounded-lg ${ic.badge} flex items-center justify-center`}>
<IconComp className="w-5 h-5 object-contain" />
</div>
<div className="flex items-center gap-2">
@@ -689,7 +690,7 @@ export default function InstancePage() {
</td>
<td className="px-4 py-3 font-medium">{instance.name}</td>
<td className="px-4 py-3">
<span className={`flex items-center gap-1.5 ${ic.color}`}>
<span className={`flex items-center gap-1.5 ${ic.badge} px-1.5 rounded-md`}>
<IconComp className="w-4 h-4 object-contain" />
<span className="text-xs">{ic.label}</span>
</span>
@@ -923,7 +924,7 @@ function QuickImportDialog({ onClose, onImported }: { onClose: () => void; onImp
/* ------------------------------------------------------------------ */
function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete, onEdit, onClone }: {
instance: InstanceItem; onBack: () => void;
config: { icon: React.ElementType; color: string; bg: string };
config: { icon: React.ElementType; badge: string };
canEdit: boolean; canDelete: boolean; onDelete: () => void; onEdit: () => void; onClone?: () => void;
}) {
const [testing, setTesting] = useState(false);
@@ -965,8 +966,8 @@ function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete
<ChevronLeft className="w-4 h-4" />
</button>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl ${config.bg} flex items-center justify-center`}>
<DetailIcon className={`w-5 h-5 ${config.color}`} />
<div className={`w-10 h-10 rounded-xl ${config.badge} flex items-center justify-center`}>
<DetailIcon className="w-5 h-5" />
</div>
<div>
<h1 className="text-xl font-semibold flex items-center gap-3">
@@ -1045,7 +1046,7 @@ function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete
<circle cx="50" cy="50" r="42" fill="none" stroke="var(--dg-bg-tertiary)" strokeWidth="8" />
<circle
cx="50" cy="50" r="42" fill="none"
stroke={health >= 90 ? '#4ade80' : health >= 70 ? '#facc15' : health >= 50 ? '#fb923c' : '#f87171'}
stroke={health >= 90 ? 'var(--dg-green)' : health >= 70 ? 'var(--dg-yellow)' : health >= 50 ? 'var(--dg-orange)' : 'var(--dg-red)'}
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={`${(health / 100) * 264} 264`}