From 16001e77e2144c5ba738761f47b80f56fa5310ba Mon Sep 17 00:00:00 2001 From: Misaka Date: Wed, 1 Apr 2026 20:03:56 +0800 Subject: [PATCH] feat(ui): add shadcn/ui infrastructure and migrate all buttons Phase 0: Set up shadcn/ui foundation with OKLCH theme variables, ThemeProvider, useThemeStore, cn() utility, and components.json. Phase 1: Replace all raw + {/* Error message */} @@ -487,12 +489,12 @@ export const ExtractorOperationHistoryModal: React.FC - + diff --git a/src/renderer/src/components/MaterialTypeManagementDialog.tsx b/src/renderer/src/components/MaterialTypeManagementDialog.tsx index b2ebafb..a375617 100644 --- a/src/renderer/src/components/MaterialTypeManagementDialog.tsx +++ b/src/renderer/src/components/MaterialTypeManagementDialog.tsx @@ -9,6 +9,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react' import { Plus, Trash2, Save, RotateCcw, Users } from 'lucide-react' import { Modal } from './ui/Modal' +import { Button } from './ui/Button' import { showSuccess, showError, showInfo } from '../stores/useAppStore' import { ConfirmDialog } from './ui/ConfirmDialog' import { useConfirmDialog } from './ui/useConfirmDialog' @@ -392,26 +393,28 @@ export const MaterialTypeManagementDialog: React.FC
- - - +
{pendingCount > 0 && ( @@ -419,13 +422,13 @@ export const MaterialTypeManagementDialog: React.FC )} - +
diff --git a/src/renderer/src/components/ThemeProvider.tsx b/src/renderer/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..229934c --- /dev/null +++ b/src/renderer/src/components/ThemeProvider.tsx @@ -0,0 +1,41 @@ +import { useEffect } from 'react' +import { useThemeStore } from '@renderer/stores/useThemeStore' + +interface ThemeProviderProps { + children: React.ReactNode +} + +const getSystemTheme = (): 'light' | 'dark' => { + if (typeof window === 'undefined') return 'light' + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +const applyTheme = (theme: 'light' | 'dark') => { + const root = document.documentElement + if (theme === 'dark') { + root.classList.add('dark') + } else { + root.classList.remove('dark') + } +} + +export function ThemeProvider({ children }: ThemeProviderProps) { + const { theme } = useThemeStore() + + useEffect(() => { + const effectiveTheme = theme === 'system' ? getSystemTheme() : theme + applyTheme(effectiveTheme) + + if (theme === 'system') { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const handleChange = () => { + applyTheme(getSystemTheme()) + } + mediaQuery.addEventListener('change', handleChange) + return () => mediaQuery.removeEventListener('change', handleChange) + } + return undefined + }, [theme]) + + return <>{children} +} diff --git a/src/renderer/src/components/UpdateDialog.tsx b/src/renderer/src/components/UpdateDialog.tsx index 9939152..9e309a3 100644 --- a/src/renderer/src/components/UpdateDialog.tsx +++ b/src/renderer/src/components/UpdateDialog.tsx @@ -3,6 +3,7 @@ import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { DownloadCloud, LoaderCircle, RefreshCw } from 'lucide-react' import Modal from './ui/Modal' +import { Button } from './ui/Button' import { useUpdateDialogState } from '../hooks/useUpdateDialogState' import type { UserType } from '../../../main/types/user.types' import type { @@ -103,14 +104,15 @@ export default function UpdateDialog({ {status?.currentChannel ?? __APP_CHANNEL__} - + {catalog?.mode === 'admin' ? ( @@ -180,21 +182,21 @@ export default function UpdateDialog({
{status?.message ?? ' '}
- + {userType === 'Admin' ? ( - + ) : ( - + )}
diff --git a/src/renderer/src/components/UserSelectionDialog.tsx b/src/renderer/src/components/UserSelectionDialog.tsx index 6b39da3..fefda83 100644 --- a/src/renderer/src/components/UserSelectionDialog.tsx +++ b/src/renderer/src/components/UserSelectionDialog.tsx @@ -9,6 +9,7 @@ import React, { useState, useRef } from 'react' import { Modal } from './ui/Modal' +import { Button } from './ui/Button' export interface UserInfo { id: number @@ -119,19 +120,19 @@ export const UserSelectionDialog: React.FC = ({
- - +
双击用户可直接选择
diff --git a/src/renderer/src/components/cleaner/CleanerExecutionBar.tsx b/src/renderer/src/components/cleaner/CleanerExecutionBar.tsx index 35e34bc..a849aaa 100644 --- a/src/renderer/src/components/cleaner/CleanerExecutionBar.tsx +++ b/src/renderer/src/components/cleaner/CleanerExecutionBar.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Play, Settings2, ToggleLeft, ToggleRight } from 'lucide-react' +import { Button } from '../ui/Button' interface CleanerExecutionBarProps { filteredCount: number @@ -43,12 +44,12 @@ export function CleanerExecutionBar({
- + {showSettingsMenu && (
@@ -107,14 +108,15 @@ export function CleanerExecutionBar({
)}
- +
) diff --git a/src/renderer/src/components/cleaner/CleanerToolbar.tsx b/src/renderer/src/components/cleaner/CleanerToolbar.tsx index 93292f0..ed93abb 100644 --- a/src/renderer/src/components/cleaner/CleanerToolbar.tsx +++ b/src/renderer/src/components/cleaner/CleanerToolbar.tsx @@ -10,6 +10,7 @@ import { Settings2, Square } from 'lucide-react' +import { Button } from '../ui/Button' import type { ValidationResult } from '../../hooks/cleaner/types' interface CleanerToolbarProps { @@ -49,24 +50,25 @@ export function CleanerToolbar({ return ( <>
- +
- - +
- - +
- +
- - - +
diff --git a/src/renderer/src/components/ui/Button.tsx b/src/renderer/src/components/ui/Button.tsx index 7e3e753..b3635bb 100644 --- a/src/renderer/src/components/ui/Button.tsx +++ b/src/renderer/src/components/ui/Button.tsx @@ -1,13 +1,9 @@ -/** - * Button Component - * - * A reusable button component with variants and sizes. - */ - import React from 'react' +import { Button as ShadcnButton } from './shadcn/button' +import { cn } from '@renderer/lib/utils' -type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost' -type ButtonSize = 'sm' | 'md' | 'lg' +type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost' | 'outline' +type ButtonSize = 'sm' | 'md' | 'lg' | 'icon' interface ButtonProps extends React.ButtonHTMLAttributes { variant?: ButtonVariant @@ -15,19 +11,22 @@ interface ButtonProps extends React.ButtonHTMLAttributes { loading?: boolean icon?: React.ReactNode children: React.ReactNode + ref?: React.Ref } -const variantStyles: Record = { - primary: 'bg-blue-600 hover:bg-blue-700 text-white border-transparent', - secondary: 'bg-gray-100 hover:bg-gray-200 text-gray-800 border-gray-300', - danger: 'bg-red-600 hover:bg-red-700 text-white border-transparent', - ghost: 'bg-transparent hover:bg-gray-100 text-gray-700 border-transparent' +const variantMap: Record = { + primary: 'default', + secondary: 'secondary', + danger: 'destructive', + ghost: 'ghost', + outline: 'outline' } -const sizeStyles: Record = { - sm: 'px-3 py-1.5 text-sm', - md: 'px-4 py-2 text-base', - lg: 'px-6 py-3 text-lg' +const sizeMap: Record = { + sm: 'sm', + md: 'default', + lg: 'lg', + icon: 'icon' } export function Button({ @@ -36,17 +35,18 @@ export function Button({ loading = false, icon, children, - className = '', + className, disabled, + ref, ...props }: ButtonProps) { - const baseStyles = - 'inline-flex items-center justify-center font-medium rounded-lg border transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed' - return ( - + ) } diff --git a/src/renderer/src/components/ui/shadcn/button.tsx b/src/renderer/src/components/ui/shadcn/button.tsx new file mode 100644 index 0000000..4b56615 --- /dev/null +++ b/src/renderer/src/components/ui/shadcn/button.tsx @@ -0,0 +1,57 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@renderer/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-ring/50 focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer", + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40', + outline: + 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline' + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9' + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : 'button' + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/renderer/src/lib/utils.ts b/src/renderer/src/lib/utils.ts new file mode 100644 index 0000000..fed2fe9 --- /dev/null +++ b/src/renderer/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 5905ed1..3be55e0 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -3,9 +3,12 @@ import './assets/main.css' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' +import { ThemeProvider } from './components/ThemeProvider' createRoot(document.getElementById('root')!).render( - + + + ) diff --git a/src/renderer/src/pages/ExtractorPage.tsx b/src/renderer/src/pages/ExtractorPage.tsx index 33db177..6c5fef2 100644 --- a/src/renderer/src/pages/ExtractorPage.tsx +++ b/src/renderer/src/pages/ExtractorPage.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Download, Play, CheckCircle, History } from 'lucide-react' +import { Button } from '../components/ui/Button' import OrderNumberInput from '../components/OrderNumberInput' import { useExtractor } from '../hooks/useExtractor' import { usePersistentTextState } from '../hooks/usePersistentTextState' @@ -87,22 +88,22 @@ const ExtractorPage: React.FC = ({ currentUser }) => {
- - +
diff --git a/src/renderer/src/pages/SettingsPage.tsx b/src/renderer/src/pages/SettingsPage.tsx index 35517dc..9a15454 100644 --- a/src/renderer/src/pages/SettingsPage.tsx +++ b/src/renderer/src/pages/SettingsPage.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react' import { Settings as SettingsIcon, Save, User, Key } from 'lucide-react' import { showSuccess, showError } from '../stores/useAppStore' +import { Button } from '../components/ui/Button' interface ErpCredentials { username: string @@ -133,14 +134,14 @@ const SettingsPage: React.FC = () => {
- +
diff --git a/src/renderer/src/stores/useThemeStore.ts b/src/renderer/src/stores/useThemeStore.ts new file mode 100644 index 0000000..edeb25e --- /dev/null +++ b/src/renderer/src/stores/useThemeStore.ts @@ -0,0 +1,50 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +type Theme = 'light' | 'dark' | 'system' + +interface ThemeState { + theme: Theme + setTheme: (theme: Theme) => void + getEffectiveTheme: () => 'light' | 'dark' +} + +const getSystemTheme = (): 'light' | 'dark' => { + if (typeof window === 'undefined') return 'light' + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +const applyTheme = (theme: 'light' | 'dark') => { + const root = document.documentElement + if (theme === 'dark') { + root.classList.add('dark') + } else { + root.classList.remove('dark') + } +} + +export const useThemeStore = create()( + persist( + (set, get) => ({ + theme: 'dark', + setTheme: (theme) => { + set({ theme }) + const effectiveTheme = theme === 'system' ? getSystemTheme() : theme + applyTheme(effectiveTheme) + }, + getEffectiveTheme: () => { + const { theme } = get() + return theme === 'system' ? getSystemTheme() : theme + } + }), + { + name: 'erpauto-theme-storage', + onRehydrateStorage: () => (state) => { + if (state) { + const effectiveTheme = state.theme === 'system' ? getSystemTheme() : state.theme + applyTheme(effectiveTheme) + } + } + } + ) +)