feat(ui): migrate business components to shadcn/ui

Migrate key business components to use shadcn/ui components while
maintaining existing functionality and improving dark mode support.

## Components Migrated

### LoginDialog
- Replace native inputs with shadcn Input component
- Replace native buttons with shadcn Button component
- Add shadcn Label component for accessibility
- Update error message styling with shadcn color tokens

### UserSelectionDialog
- Replace native buttons with shadcn Button component
- Add shadcn ScrollArea for user list
- Update styling with shadcn color tokens for dark mode

### AuthenticatedAppShell
- Add ThemeSwitcher component to header
- Replace navigation buttons with shadcn Button component
- Add shadcn ScrollArea for main content area
- Add shadcn Separator for visual separation
- Update header and main content styling for dark mode

### MaterialTypeManagementDialog
- Replace toolbar buttons with shadcn Button component
- Use shadcn Table, TableHeader, TableBody, TableRow, TableCell
- Add shadcn ScrollArea for table container
- Update status color styling with shadcn tokens
- Improve dark mode support throughout

### ExtractorOperationHistoryModal
- Replace toolbar buttons with shadcn Button component
- Use shadcn Table components for batch details
- Add shadcn ScrollArea for batch list
- Update status styling with shadcn color tokens
- Improve dark mode support

## Improvements
- Full dark mode support for all migrated components
- Consistent styling using shadcn color tokens
- Better accessibility with semantic shadcn components
- Maintained backward compatibility with existing APIs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-04-01 10:39:03 +08:00
parent 5695634ac3
commit 8ef08be649
5 changed files with 337 additions and 288 deletions

View File

@@ -3,10 +3,16 @@
* *
* Displays extraction operation history with batch statistics and details. * Displays extraction operation history with batch statistics and details.
* Admin users see all users' records, regular users see only their own. * Admin users see all users' records, regular users see only their own.
*
* Migrated to use shadcn/ui components
*/ */
import React, { useState, useEffect, useCallback } from 'react' import React, { useState, useEffect, useCallback } from 'react'
import { Modal } from './ui/Modal' import { Modal } from './ui/Modal'
import { Button } from './ui/Button'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/shadcn/table'
import { ScrollArea } from './ui/shadcn/scroll-area'
import { cn } from '@renderer/lib/utils'
import { import {
RefreshCw, RefreshCw,
Trash2, Trash2,
@@ -31,10 +37,10 @@ interface ExtractorOperationHistoryModalProps {
} }
const statusStyles: Record<string, string> = { const statusStyles: Record<string, string> = {
success: 'bg-green-100 text-green-700', success: 'bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400',
partial: 'bg-amber-100 text-amber-700', partial: 'bg-amber-100 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400',
failed: 'bg-red-100 text-red-700', failed: 'bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400',
pending: 'bg-gray-100 text-gray-700' pending: 'bg-muted text-muted-foreground'
} }
const statusLabels: Record<string, string> = { const statusLabels: Record<string, string> = {
@@ -45,10 +51,10 @@ const statusLabels: Record<string, string> = {
} }
const statusIcons: Record<string, React.ReactNode> = { const statusIcons: Record<string, React.ReactNode> = {
success: <CheckCircle size={16} className="text-green-600" />, success: <CheckCircle size={16} className="text-green-600 dark:text-green-400" />,
partial: <Clock size={16} className="text-amber-600" />, partial: <Clock size={16} className="text-amber-600 dark:text-amber-400" />,
failed: <XCircle size={16} className="text-red-600" />, failed: <XCircle size={16} className="text-red-600 dark:text-red-400" />,
pending: <Clock size={16} className="text-gray-500" /> pending: <Clock size={16} className="text-muted-foreground" />
} }
const formatDateTime = (dateStr: string) => { const formatDateTime = (dateStr: string) => {
@@ -195,44 +201,45 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
<Modal isOpen={isOpen} onClose={onClose} title="操作历史" size="3xl"> <Modal isOpen={isOpen} onClose={onClose} title="操作历史" size="3xl">
<div className="flex flex-col h-[70vh]"> <div className="flex flex-col h-[70vh]">
{/* Toolbar */} {/* Toolbar */}
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200"> <div className="flex items-center justify-between mb-4 pb-4 border-b border-border">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-sm text-gray-600"> <span className="text-sm text-muted-foreground">
{isAdmin ? ( {isAdmin ? (
<span className="text-amber-600 font-medium"></span> <span className="text-amber-600 dark:text-amber-400 font-medium"></span>
) : ( ) : (
<span></span> <span></span>
)} )}
</span> </span>
{batches.length > 0 && ( {batches.length > 0 && (
<span className="text-sm text-gray-500"> {batches.length} </span> <span className="text-sm text-muted-foreground"> {batches.length} </span>
)} )}
</div> </div>
<button <Button
className="p-2 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50" variant="ghost"
onClick={() => void fetchBatches()} onClick={() => void fetchBatches()}
disabled={loading} disabled={loading}
title="刷新" title="刷新"
className="h-8 w-8 p-0"
> >
<RefreshCw size={18} className={loading ? 'animate-spin' : ''} /> <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
</button> </Button>
</div> </div>
{/* Error message */} {/* Error message */}
{error && ( {error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm"> <div className="mb-4 p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-destructive text-sm dark:bg-destructive/20 dark:border-destructive/30">
{error} {error}
</div> </div>
)} )}
{/* Batch list */} {/* Batch list */}
<div className="flex-1 overflow-y-auto"> <ScrollArea className="flex-1">
{loading && batches.length === 0 ? ( {loading && batches.length === 0 ? (
<div className="flex items-center justify-center h-32 text-gray-500">...</div> <div className="flex items-center justify-center h-32 text-muted-foreground">...</div>
) : batches.length === 0 ? ( ) : batches.length === 0 ? (
<div className="flex items-center justify-center h-32 text-gray-500"></div> <div className="flex items-center justify-center h-32 text-muted-foreground"></div>
) : ( ) : (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3 pr-4">
{batches.map((batch) => { {batches.map((batch) => {
const isExpanded = expandedBatches.has(batch.batchId) const isExpanded = expandedBatches.has(batch.batchId)
const details = batchDetails.get(batch.batchId) || [] const details = batchDetails.get(batch.batchId) || []
@@ -241,60 +248,65 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
return ( return (
<div <div
key={batch.batchId} key={batch.batchId}
className="border border-gray-200 rounded-lg overflow-hidden" className="border border-border rounded-lg overflow-hidden"
> >
{/* Batch summary */} {/* Batch summary */}
<div <div
className={`flex items-center justify-between p-4 cursor-pointer transition-colors ${ className={cn(
isExpanded ? 'bg-gray-50' : 'hover:bg-gray-50' "flex items-center justify-between p-4 cursor-pointer transition-colors",
}`} isExpanded ? 'bg-muted/50' : 'hover:bg-muted/30'
)}
onClick={() => toggleBatchExpansion(batch.batchId)} onClick={() => toggleBatchExpansion(batch.batchId)}
> >
<div className="flex items-center gap-4 flex-1"> <div className="flex items-center gap-4 flex-1">
<button className="p-1 hover:bg-gray-200 rounded"> <Button
variant="ghost"
className="h-6 w-6 p-0"
>
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />} {isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
</button> </Button>
<div className="flex-1 grid grid-cols-6 gap-4 text-sm"> <div className="flex-1 grid grid-cols-6 gap-4 text-sm">
<div> <div>
<div className="text-gray-500 text-xs"></div> <div className="text-muted-foreground text-xs"></div>
<div className="font-medium text-gray-900"> <div className="font-medium">
{formatDateTime(batch.operationTime)} {formatDateTime(batch.operationTime)}
</div> </div>
</div> </div>
<div> <div>
<div className="text-gray-500 text-xs"></div> <div className="text-muted-foreground text-xs"></div>
<div className="font-medium text-gray-900">{batch.username}</div> <div className="font-medium">{batch.username}</div>
</div> </div>
<div> <div>
<div className="text-gray-500 text-xs"></div> <div className="text-muted-foreground text-xs"></div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{statusIcons[batch.status] || statusIcons.pending} {statusIcons[batch.status] || statusIcons.pending}
<span <span
className={`px-2 py-0.5 rounded text-xs font-medium ${ className={cn(
"px-2 py-0.5 rounded text-xs font-medium",
statusStyles[batch.status] || statusStyles.pending statusStyles[batch.status] || statusStyles.pending
}`} )}
> >
{statusLabels[batch.status] || batch.status} {statusLabels[batch.status] || batch.status}
</span> </span>
</div> </div>
</div> </div>
<div> <div>
<div className="text-gray-500 text-xs"></div> <div className="text-muted-foreground text-xs"></div>
<div className="font-medium text-gray-900">{batch.totalOrders}</div> <div className="font-medium">{batch.totalOrders}</div>
</div> </div>
<div> <div>
<div className="text-gray-500 text-xs"></div> <div className="text-muted-foreground text-xs"></div>
<div className="font-medium text-gray-900">{batch.totalRecords}</div> <div className="font-medium">{batch.totalRecords}</div>
</div> </div>
<div> <div>
<div className="text-gray-500 text-xs">/</div> <div className="text-muted-foreground text-xs">/</div>
<div className="font-medium text-gray-900"> <div className="font-medium">
<span className="text-green-600">{batch.successCount}</span> <span className="text-green-600 dark:text-green-400">{batch.successCount}</span>
{batch.failedCount > 0 && ( {batch.failedCount > 0 && (
<> <>
{' / '} {' / '}
<span className="text-red-600">{batch.failedCount}</span> <span className="text-destructive">{batch.failedCount}</span>
</> </>
)} )}
</div> </div>
@@ -302,8 +314,9 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
</div> </div>
</div> </div>
<button <Button
className="p-2 hover:bg-red-50 text-gray-400 hover:text-red-600 rounded transition-colors disabled:opacity-50" variant="ghost"
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10 h-8 w-8 p-0"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
void handleDeleteBatch(batch.batchId) void handleDeleteBatch(batch.batchId)
@@ -312,21 +325,22 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
title="删除批次" title="删除批次"
> >
<Trash2 size={16} className={isDeleting ? 'animate-pulse' : ''} /> <Trash2 size={16} className={isDeleting ? 'animate-pulse' : ''} />
</button> </Button>
</div> </div>
{/* Batch details */} {/* Batch details */}
{isExpanded && details.length > 0 && ( {isExpanded && details.length > 0 && (
<div className="border-t border-gray-200 bg-white"> <div className="border-t border-border bg-background">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm"> <Table>
<thead className="bg-gray-50"> <TableHeader className="bg-muted/50">
<tr> <TableRow>
<th className="px-4 py-2 text-left font-medium text-gray-600"> <TableHead>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <Button
className="p-1 hover:bg-gray-200 rounded transition-colors" variant="ghost"
className="h-5 w-5 p-0"
onClick={() => onClick={() =>
void handleCopyColumn('productionId', batch.batchId) void handleCopyColumn('productionId', batch.batchId)
} }
@@ -334,16 +348,17 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
> >
<Copy <Copy
size={14} size={14}
className="text-gray-500 hover:text-gray-700" className="text-muted-foreground hover:text-foreground"
/> />
</button> </Button>
</div> </div>
</th> </TableHead>
<th className="px-4 py-2 text-left font-medium text-gray-600"> <TableHead>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <Button
className="p-1 hover:bg-gray-200 rounded transition-colors" variant="ghost"
className="h-5 w-5 p-0"
onClick={() => onClick={() =>
void handleCopyColumn('orderNumber', batch.batchId) void handleCopyColumn('orderNumber', batch.batchId)
} }
@@ -351,51 +366,46 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
> >
<Copy <Copy
size={14} size={14}
className="text-gray-500 hover:text-gray-700" className="text-muted-foreground hover:text-foreground"
/> />
</button> </Button>
</div> </div>
</th> </TableHead>
<th className="px-4 py-2 text-left font-medium text-gray-600"> <TableHead></TableHead>
<TableHead></TableHead>
</th> <TableHead></TableHead>
<th className="px-4 py-2 text-left font-medium text-gray-600"> </TableRow>
</TableHeader>
</th> <TableBody>
<th className="px-4 py-2 text-left font-medium text-gray-600">
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{details.map((detail) => ( {details.map((detail) => (
<tr key={detail.id} className="hover:bg-gray-50"> <TableRow key={detail.id} className="hover:bg-muted/30">
<td className="px-4 py-2 text-gray-900"> <TableCell className="font-mono">
{detail.productionId || '-'} {detail.productionId || '-'}
</td> </TableCell>
<td className="px-4 py-2 text-gray-900 font-mono text-xs"> <TableCell className="font-mono text-xs">
{detail.orderNumber} {detail.orderNumber}
</td> </TableCell>
<td className="px-4 py-2"> <TableCell>
<span <span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium ${ className={cn(
"inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium",
statusStyles[detail.status] || statusStyles.pending statusStyles[detail.status] || statusStyles.pending
}`} )}
> >
{statusIcons[detail.status]} {statusIcons[detail.status]}
{statusLabels[detail.status] || detail.status} {statusLabels[detail.status] || detail.status}
</span> </span>
</td> </TableCell>
<td className="px-4 py-2 text-gray-900"> <TableCell>
{detail.recordCount ?? '-'} {detail.recordCount ?? '-'}
</td> </TableCell>
<td className="px-4 py-2 text-red-600 text-xs max-w-xs truncate"> <TableCell className="text-destructive text-xs max-w-xs truncate">
{detail.errorMessage || '-'} {detail.errorMessage || '-'}
</td> </TableCell>
</tr> </TableRow>
))} ))}
</tbody> </TableBody>
</table> </Table>
</div> </div>
</div> </div>
)} )}
@@ -404,16 +414,16 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
})} })}
</div> </div>
)} )}
</div> </ScrollArea>
{/* Footer */} {/* Footer */}
<div className="pt-4 border-t border-gray-200 flex justify-end"> <div className="pt-4 border-t border-border flex justify-end">
<button <Button
className="px-6 py-2 rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium transition-colors" variant="secondary"
onClick={onClose} onClick={onClose}
> >
</button> </Button>
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@@ -5,10 +5,15 @@
* - Modal dialog for username/password input * - Modal dialog for username/password input
* - Display computer name * - Display computer name
* - Enter key to submit * - Enter key to submit
*
* Migrated to use shadcn/ui components
*/ */
import React, { useState, useRef } from 'react' import React, { useState, useRef } from 'react'
import { Modal } from './ui/Modal' import { Modal } from './ui/Modal'
import { Button } from './ui/Button'
import { Input } from './ui/shadcn/input'
import { Label } from './ui/shadcn/label'
interface LoginDialogProps { interface LoginDialogProps {
isOpen: boolean isOpen: boolean
@@ -84,7 +89,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
<div <div
ref={errorRef} ref={errorRef}
id="login-dialog-error" id="login-dialog-error"
className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md text-red-700 text-sm" className="mb-4 p-3 bg-destructive/10 border border-destructive/20 rounded-md text-destructive text-sm dark:bg-destructive/20 dark:border-destructive/30"
role="alert" role="alert"
aria-live="polite" aria-live="polite"
tabIndex={-1} tabIndex={-1}
@@ -94,14 +99,14 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
)} )}
<div className="space-y-4"> <div className="space-y-4">
<div className="text-sm text-gray-600">{computerName}</div> <div className="text-sm text-muted-foreground">{computerName}</div>
<div> <div className="space-y-2">
<label className="block text-sm text-slate-700 mb-1">:</label> <Label htmlFor="username"></Label>
<input <Input
ref={usernameInputRef} ref={usernameInputRef}
id="username"
type="text" type="text"
className="border border-slate-300 rounded-md p-2 w-full text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
placeholder="请输入用户名" placeholder="请输入用户名"
@@ -109,11 +114,11 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
/> />
</div> </div>
<div> <div className="space-y-2">
<label className="block text-sm text-slate-700 mb-1">:</label> <Label htmlFor="password"></Label>
<input <Input
id="password"
type="password" type="password"
className="border border-slate-300 rounded-md p-2 w-full text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码" placeholder="请输入密码"
@@ -123,23 +128,23 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
</div> </div>
<div className="mt-6 flex justify-end gap-3"> <div className="mt-6 flex justify-end gap-3">
<button <Button
className="px-4 py-2 rounded-md bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors" variant="secondary"
onClick={onCancel} onClick={onCancel}
disabled={isLoggingIn} disabled={isLoggingIn}
> >
</button> </Button>
<button <Button
className="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50"
onClick={handleLogin} onClick={handleLogin}
disabled={isLoggingIn} disabled={isLoggingIn}
loading={isLoggingIn}
> >
{isLoggingIn ? '登录中...' : '登录'} {isLoggingIn ? '登录中...' : '登录'}
</button> </Button>
</div> </div>
<div className="mt-4 text-xs text-gray-400 text-right">v1.0</div> <div className="mt-4 text-xs text-muted-foreground text-right">v1.0</div>
</div> </div>
</Modal> </Modal>
) )

View File

@@ -4,11 +4,17 @@
* Provides a dialog for managing material type keywords used to identify * Provides a dialog for managing material type keywords used to identify
* materials for deletion. Admin users can see all records and filter by manager. * materials for deletion. Admin users can see all records and filter by manager.
* Regular users can only see and edit their own records. * Regular users can only see and edit their own records.
*
* Migrated to use shadcn/ui components
*/ */
import React, { useState, useEffect, useCallback, useRef } from 'react' import React, { useState, useEffect, useCallback, useRef } from 'react'
import { Plus, Trash2, Save, RotateCcw, Users } from 'lucide-react' import { Plus, Trash2, Save, RotateCcw, Users } from 'lucide-react'
import { Modal } from './ui/Modal' import { Modal } from './ui/Modal'
import { Button } from './ui/Button'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/shadcn/table'
import { ScrollArea } from './ui/shadcn/scroll-area'
import { cn } from '@renderer/lib/utils'
import { showSuccess, showError, showInfo } from '../stores/useAppStore' import { showSuccess, showError, showInfo } from '../stores/useAppStore'
import { ConfirmDialog } from './ui/ConfirmDialog' import { ConfirmDialog } from './ui/ConfirmDialog'
import { useConfirmDialog } from './ui/useConfirmDialog' import { useConfirmDialog } from './ui/useConfirmDialog'
@@ -317,18 +323,18 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
onClose() onClose()
} }
// Get row background color // Get row background color class
const getRowStyle = (row: RowState): React.CSSProperties => { const getRowClass = (row: RowState): string => {
if (row.state === 'deleted') { if (row.state === 'deleted') {
return { backgroundColor: '#fee2e2', textDecoration: 'line-through', opacity: 0.6 } return 'bg-destructive/10 line-through opacity-60'
} }
if (row.state === 'new') { if (row.state === 'new') {
return { backgroundColor: '#dcfce7' } return 'bg-green-100 dark:bg-green-900/20'
} }
if (row.state === 'modified') { if (row.state === 'modified') {
return { backgroundColor: '#fef9c3' } return 'bg-yellow-100 dark:bg-yellow-900/20'
} }
return {} return ''
} }
return ( return (
@@ -342,22 +348,22 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
<div onKeyDown={handleKeyDown}> <div onKeyDown={handleKeyDown}>
{/* Manager filter (admin only) */} {/* Manager filter (admin only) */}
{isAdmin && ( {isAdmin && (
<div className="mb-4 p-3 bg-slate-50 rounded-lg border border-slate-200"> <div className="mb-4 p-3 bg-muted/50 rounded-lg border border-border">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2 text-sm font-medium text-slate-700"> <div className="flex items-center gap-2 text-sm font-medium">
<Users size={16} /> <Users size={16} />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => setSelectedManagers(new Set(managers))} onClick={() => setSelectedManagers(new Set(managers))}
className="text-xs text-blue-600 hover:underline" className="text-xs text-primary hover:underline"
> >
</button> </button>
<button <button
onClick={() => setSelectedManagers(new Set())} onClick={() => setSelectedManagers(new Set())}
className="text-xs text-slate-500 hover:underline" className="text-xs text-muted-foreground hover:underline"
> >
</button> </button>
@@ -367,11 +373,11 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
{managers.map((manager) => ( {managers.map((manager) => (
<label <label
key={manager} key={manager}
className="flex items-center gap-1.5 text-xs text-slate-600 cursor-pointer hover:bg-white px-2 py-1 rounded" className="flex items-center gap-1.5 text-xs cursor-pointer hover:bg-background px-2 py-1 rounded"
> >
<input <input
type="checkbox" type="checkbox"
className="rounded text-blue-600" className="rounded text-primary"
checked={selectedManagers.has(manager)} checked={selectedManagers.has(manager)}
onChange={(e) => { onChange={(e) => {
setSelectedManagers((prev) => { setSelectedManagers((prev) => {
@@ -392,131 +398,97 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
{/* Toolbar */} {/* Toolbar */}
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <Button
size="sm"
onClick={insertNewRow} onClick={insertNewRow}
className="flex items-center gap-1.5 text-xs bg-green-50 border border-green-200 text-green-700 px-3 py-1.5 rounded hover:bg-green-100" className="gap-1.5 bg-green-100 hover:bg-green-200 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800"
> >
<Plus size={14} /> (Insert) <Plus size={14} /> (Insert)
</button> </Button>
<button <Button
size="sm"
onClick={() => selectedRowIndex !== null && deleteRow(selectedRowIndex)} onClick={() => selectedRowIndex !== null && deleteRow(selectedRowIndex)}
disabled={selectedRowIndex === null} disabled={selectedRowIndex === null}
className="flex items-center gap-1.5 text-xs bg-red-50 border border-red-200 text-red-700 px-3 py-1.5 rounded hover:bg-red-100 disabled:opacity-50 disabled:cursor-not-allowed" variant="danger"
className="gap-1.5"
> >
<Trash2 size={14} /> (Delete) <Trash2 size={14} /> (Delete)
</button> </Button>
<button <Button
size="sm"
onClick={handleReset} onClick={handleReset}
disabled={pendingCount === 0} disabled={pendingCount === 0}
className="flex items-center gap-1.5 text-xs bg-slate-50 border border-slate-200 text-slate-700 px-3 py-1.5 rounded hover:bg-slate-100 disabled:opacity-50 disabled:cursor-not-allowed" variant="secondary"
className="gap-1.5"
> >
<RotateCcw size={14} /> <RotateCcw size={14} />
</button> </Button>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{pendingCount > 0 && ( {pendingCount > 0 && (
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded"> <span className="text-xs text-amber-600 bg-amber-100 dark:bg-amber-900/20 dark:text-amber-400 px-2 py-1 rounded">
{pendingCount} {pendingCount}
</span> </span>
)} )}
<button <Button
size="sm"
onClick={handleSave} onClick={handleSave}
disabled={saving || pendingCount === 0} disabled={saving || pendingCount === 0}
className="flex items-center gap-1.5 text-xs bg-blue-500 text-white px-3 py-1.5 rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed" loading={saving}
className="gap-1.5"
> >
<Save size={14} /> {saving ? '保存中...' : '保存'} <Save size={14} /> {saving ? '保存中...' : '保存'}
</button> </Button>
</div> </div>
</div> </div>
{/* Table */} {/* Table */}
<div className="border border-slate-200 rounded-lg overflow-hidden max-h-[400px] overflow-y-auto"> <div className="border border-border rounded-lg overflow-hidden">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12 text-slate-500">...</div> <div className="flex items-center justify-center py-12 text-muted-foreground">...</div>
) : ( ) : (
<table ref={tableRef} className="w-full text-sm"> <ScrollArea className="max-h-[400px]">
<thead className="bg-slate-100 sticky top-0"> <Table ref={tableRef}>
<tr> <TableHeader className="sticky top-0 bg-muted">
<th className="px-4 py-2 text-left font-medium text-slate-700 w-64"> <TableRow>
<TableHead className="w-64">
</th>
<th className="px-4 py-2 text-left font-medium text-slate-700"></th> </TableHead>
</tr> <TableHead></TableHead>
</thead> </TableRow>
<tbody className="divide-y divide-slate-100"> </TableHeader>
{filteredRows.filter((r) => r.state !== 'deleted').length === 0 ? ( <TableBody>
<tr> {filteredRows.filter((r) => r.state !== 'deleted').length === 0 ? (
<td colSpan={2} className="px-4 py-8 text-center text-slate-400"> <TableRow>
&quot;&quot; <TableCell colSpan={2} className="text-center text-muted-foreground py-8">
</td> "新增"
</tr> </TableCell>
) : ( </TableRow>
filteredRows ) : (
.filter((r) => r.state !== 'deleted') filteredRows
.map((row, index) => { .filter((r) => r.state !== 'deleted')
const originalIndex = rows.indexOf(row) .map((row, index) => {
const isSelected = selectedRowIndex === originalIndex const originalIndex = rows.indexOf(row)
const isEditingMaterial = const isSelected = selectedRowIndex === originalIndex
editingCell?.rowIndex === originalIndex && const isEditingMaterial =
editingCell?.field === 'materialName' editingCell?.rowIndex === originalIndex &&
const isEditingManager = editingCell?.field === 'materialName'
editingCell?.rowIndex === originalIndex && const isEditingManager =
editingCell?.field === 'managerName' editingCell?.rowIndex === originalIndex &&
editingCell?.field === 'managerName'
return ( return (
<tr <TableRow
key={index} key={index}
style={getRowStyle(row)} className={cn(
className={`${isSelected ? 'ring-2 ring-blue-300 ring-inset' : ''} hover:bg-slate-50 cursor-pointer`} getRowClass(row),
onClick={() => setSelectedRowIndex(originalIndex)} isSelected && 'ring-2 ring-primary ring-inset',
> 'cursor-pointer'
<td className="px-4 py-2 border-r border-slate-100">
{isEditingMaterial ? (
<input
ref={inputRef}
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={saveEdit}
onKeyDown={(e) => {
if (e.key === 'Enter') saveEdit()
if (e.key === 'Escape') cancelEdit()
}}
className="w-full px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
) : (
<div
className="min-h-[24px] cursor-text"
onDoubleClick={() => startEdit(originalIndex, 'materialName')}
>
{row.record.materialName || (
<span className="text-slate-400 italic"></span>
)}
</div>
)} )}
</td> onClick={() => setSelectedRowIndex(originalIndex)}
<td className="px-4 py-2"> >
{isEditingManager ? ( <TableCell className="border-r border-border">
isAdmin ? ( {isEditingMaterial ? (
<select
ref={selectRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={saveEdit}
onKeyDown={(e) => {
if (e.key === 'Enter') saveEdit()
if (e.key === 'Escape') cancelEdit()
}}
className="w-full px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value=""></option>
{managers.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
) : (
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
@@ -527,31 +499,77 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
if (e.key === 'Enter') saveEdit() if (e.key === 'Enter') saveEdit()
if (e.key === 'Escape') cancelEdit() if (e.key === 'Escape') cancelEdit()
}} }}
className="w-full px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-2 py-1 border border-primary rounded focus:outline-none focus:ring-2 focus:ring-ring bg-background"
/> />
) ) : (
) : ( <div
<div className="min-h-[24px] cursor-text"
className="min-h-[24px] cursor-text" onDoubleClick={() => startEdit(originalIndex, 'materialName')}
onDoubleClick={() => startEdit(originalIndex, 'managerName')} >
> {row.record.materialName || (
{row.record.managerName || ( <span className="text-muted-foreground italic"></span>
<span className="text-slate-400 italic"></span> )}
)} </div>
</div> )}
)} </TableCell>
</td> <TableCell>
</tr> {isEditingManager ? (
) isAdmin ? (
}) <select
)} ref={selectRef}
</tbody> value={editValue}
</table> onChange={(e) => setEditValue(e.target.value)}
onBlur={saveEdit}
onKeyDown={(e) => {
if (e.key === 'Enter') saveEdit()
if (e.key === 'Escape') cancelEdit()
}}
className="w-full px-2 py-1 border border-primary rounded focus:outline-none focus:ring-2 focus:ring-ring bg-background"
>
<option value=""></option>
{managers.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
) : (
<input
ref={inputRef}
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={saveEdit}
onKeyDown={(e) => {
if (e.key === 'Enter') saveEdit()
if (e.key === 'Escape') cancelEdit()
}}
className="w-full px-2 py-1 border border-primary rounded focus:outline-none focus:ring-2 focus:ring-ring bg-background"
/>
)
) : (
<div
className="min-h-[24px] cursor-text"
onDoubleClick={() => startEdit(originalIndex, 'managerName')}
>
{row.record.managerName || (
<span className="text-muted-foreground italic"></span>
)}
</div>
)}
</TableCell>
</TableRow>
)
})
)}
</TableBody>
</Table>
</ScrollArea>
)} )}
</div> </div>
{/* Footer info */} {/* Footer info */}
<div className="mt-3 text-xs text-slate-500 flex justify-between"> <div className="mt-3 text-xs text-muted-foreground flex justify-between">
<span> <span>
| Insert | Delete | Insert | Delete
{isAdmin && ' | 绿色=新增 | 黄色=已修改'} {isAdmin && ' | 绿色=新增 | 黄色=已修改'}

View File

@@ -5,10 +5,15 @@
* - Display list of all users * - Display list of all users
* - Allow admin to select a user * - Allow admin to select a user
* - Return selected user info * - Return selected user info
*
* Migrated to use shadcn/ui components
*/ */
import React, { useState, useRef } from 'react' import React, { useState, useRef } from 'react'
import { Modal } from './ui/Modal' import { Modal } from './ui/Modal'
import { Button } from './ui/Button'
import { ScrollArea } from './ui/shadcn/scroll-area'
import { cn } from '@renderer/lib/utils'
export interface UserInfo { export interface UserInfo {
id: number id: number
@@ -60,8 +65,8 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
} }
const userTypeStyles: Record<string, string> = { const userTypeStyles: Record<string, string> = {
Admin: 'bg-amber-50 text-amber-600', Admin: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
User: 'bg-blue-50 text-blue-600' User: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
} }
if (!isOpen) return null if (!isOpen) return null
@@ -77,19 +82,20 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
ariaDescribedBy="user-selection-description" ariaDescribedBy="user-selection-description"
> >
<div ref={dialogRef} className="max-h-[60vh] flex flex-col"> <div ref={dialogRef} className="max-h-[60vh] flex flex-col">
<div className="text-sm text-gray-600 mb-4">{currentUsername}</div> <div className="text-sm text-muted-foreground mb-4">{currentUsername}</div>
<p id="user-selection-description" className="sr-only"> <p id="user-selection-description" className="sr-only">
</p> </p>
<div className="flex-1 overflow-y-auto mb-4"> <ScrollArea className="flex-1 mb-4 pr-4">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{users.map((user) => ( {users.map((user) => (
<div <div
key={user.id} key={user.id}
className={`user-item p-3 border border-gray-200 rounded-lg cursor-pointer transition-all hover:border-blue-500 hover:bg-green-50 ${ className={cn(
selectedUserId === user.id ? 'border-blue-500 bg-blue-50' : '' "user-item p-3 border rounded-lg cursor-pointer transition-all hover:border-primary hover:bg-accent",
}`} selectedUserId === user.id ? 'border-primary bg-accent' : 'border-border'
)}
onClick={() => setSelectedUserId(user.id)} onClick={() => setSelectedUserId(user.id)}
onDoubleClick={() => handleDoubleClick(user)} onDoubleClick={() => handleDoubleClick(user)}
tabIndex={0} tabIndex={0}
@@ -100,40 +106,42 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
}} }}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-900">{user.username}</span> <span className="text-sm font-medium">{user.username}</span>
<span <span
className={`text-xs px-2 py-1 rounded font-medium ${userTypeStyles[user.userType]}`} className={cn(
"text-xs px-2 py-1 rounded font-medium",
userTypeStyles[user.userType]
)}
> >
{user.userType} {user.userType}
</span> </span>
</div> </div>
{user.createTime && ( {user.createTime && (
<div className="mt-1 text-xs text-gray-500"> <div className="mt-1 text-xs text-muted-foreground">
{new Date(user.createTime).toLocaleString('zh-CN')} {new Date(user.createTime).toLocaleString('zh-CN')}
</div> </div>
)} )}
</div> </div>
))} ))}
</div> </div>
</div> </ScrollArea>
<div className="border-t border-gray-200 pt-4"> <div className="border-t border-border pt-4">
<div className="flex justify-center gap-3"> <div className="flex justify-center gap-3">
<button <Button
className="px-6 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleConfirm} onClick={handleConfirm}
disabled={selectedUserId === null} disabled={selectedUserId === null}
> >
</button> </Button>
<button <Button
className="px-6 py-2 rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium transition-colors" variant="secondary"
onClick={handleCancel} onClick={handleCancel}
> >
</button> </Button>
</div> </div>
<div className="text-center text-xs text-gray-500 mt-3"></div> <div className="text-center text-xs text-muted-foreground mt-3"></div>
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@@ -17,6 +17,10 @@ import type {
} from '../../../../main/types/update.types' } from '../../../../main/types/update.types'
import type { CurrentUser, Page } from '../../hooks/useAppBootstrap' import type { CurrentUser, Page } from '../../hooks/useAppBootstrap'
import { Toast } from '../ui/Toast' import { Toast } from '../ui/Toast'
import { ThemeSwitcher } from '../ThemeSwitcher'
import { Button } from '../ui/shadcn/button'
import { ScrollArea } from '../ui/shadcn/scroll-area'
import { Separator } from '../ui/shadcn/separator'
import CleanerPage from '../../pages/CleanerPage' import CleanerPage from '../../pages/CleanerPage'
import ExtractorPage from '../../pages/ExtractorPage' import ExtractorPage from '../../pages/ExtractorPage'
import SettingsPage from '../../pages/SettingsPage' import SettingsPage from '../../pages/SettingsPage'
@@ -81,9 +85,9 @@ export function AuthenticatedAppShell({
: '发现新版本' : '发现新版本'
return ( return (
<div className="flex flex-col h-screen bg-slate-50 text-slate-800 font-sans overflow-hidden"> <div className="flex flex-col h-screen bg-background text-foreground font-sans overflow-hidden">
<header <header
className="h-16 bg-slate-900 text-slate-300 flex items-center justify-between px-4 shadow-md z-20 flex-shrink-0" className="h-16 bg-slate-900 text-slate-300 dark:bg-slate-950 flex items-center justify-between px-4 shadow-md z-20 flex-shrink-0 border-b border-border"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties} style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
> >
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
@@ -109,22 +113,20 @@ export function AuthenticatedAppShell({
</div> </div>
<nav <nav
className="flex items-center gap-2 bg-slate-800 p-1 rounded-lg" className="flex items-center gap-2 bg-slate-800 dark:bg-slate-900 p-1 rounded-lg border border-border"
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties} style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
> >
{navItems.map((item) => ( {navItems.map((item) => (
<button <Button
key={item.id} key={item.id}
variant={currentPage === item.id ? 'default' : 'ghost'}
size="sm"
onClick={() => onNavigate(item.id)} onClick={() => onNavigate(item.id)}
className={`flex items-center gap-2 px-4 py-1.5 rounded-md text-sm font-medium transition-all ${ className="gap-2"
currentPage === item.id
? 'bg-blue-600 text-white shadow'
: 'text-slate-400 hover:text-white hover:bg-slate-700'
}`}
> >
{item.icon} {item.icon}
{item.label} {item.label}
</button> </Button>
))} ))}
</nav> </nav>
@@ -133,9 +135,11 @@ export function AuthenticatedAppShell({
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties} style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
> >
{showUpdateEntry && ( {showUpdateEntry && (
<button <Button
variant="outline"
size="sm"
onClick={() => void onOpenUpdateDialog()} onClick={() => void onOpenUpdateDialog()}
className="inline-flex items-center gap-2 rounded-full border border-blue-500/40 bg-blue-500/10 px-3 py-1.5 text-xs font-medium text-blue-100 transition hover:bg-blue-500/20" className="rounded-full border-blue-500/40 bg-blue-500/10 text-blue-100 hover:bg-blue-500/20"
title={updateStatus?.message || '查看更新'} title={updateStatus?.message || '查看更新'}
> >
{updateStatus?.phase === 'downloading' || updateStatus?.phase === 'installing' ? ( {updateStatus?.phase === 'downloading' || updateStatus?.phase === 'installing' ? (
@@ -144,16 +148,16 @@ export function AuthenticatedAppShell({
<ArrowUpCircle size={15} /> <ArrowUpCircle size={15} />
)} )}
<span>{updateButtonLabel}</span> <span>{updateButtonLabel}</span>
</button> </Button>
)} )}
<div className="flex items-center gap-2 text-xs bg-slate-800 px-3 py-1.5 rounded-full border border-slate-700"> <div className="flex items-center gap-2 text-xs bg-slate-800 dark:bg-slate-900 px-3 py-1.5 rounded-full border border-border">
<Database size={14} className="text-green-500" /> <Database size={14} className="text-green-500" />
<span className="text-slate-300"></span> <span className="text-slate-300 dark:text-slate-400"></span>
</div> </div>
<div className="flex items-center gap-2 bg-slate-800 px-3 py-1.5 rounded-full"> <div className="flex items-center gap-2 bg-slate-800 dark:bg-slate-900 px-3 py-1.5 rounded-full">
<User size={16} className="text-slate-400" /> <User size={16} className="text-slate-400" />
<span <span
className="font-medium text-slate-200" className="font-medium text-slate-200 dark:text-slate-300"
title={`User Type: ${currentUser?.userType}`} title={`User Type: ${currentUser?.userType}`}
> >
{currentUser?.username} {currentUser?.username}
@@ -169,25 +173,29 @@ export function AuthenticatedAppShell({
</button> </button>
)} )}
</div> </div>
<Separator orientation="vertical" className="h-6" />
<ThemeSwitcher />
</div> </div>
</header> </header>
<div className="flex flex-1 overflow-hidden relative"> <div className="flex flex-1 overflow-hidden relative">
<main className="flex-1 overflow-hidden bg-slate-50 p-6 h-full"> <main className="flex-1 overflow-hidden bg-muted/30 p-6 h-full">
{currentPage === 'home' && ( <ScrollArea className="h-full">
<div className="h-full overflow-auto"> {currentPage === 'home' && (
<div className="max-w-4xl mx-auto mt-10 text-center animate-in fade-in slide-in-from-bottom-4 duration-500"> <div className="h-full">
<LayoutDashboard size={48} className="mx-auto text-blue-500 mb-4" /> <div className="max-w-4xl mx-auto mt-10 text-center animate-in fade-in slide-in-from-bottom-4 duration-500">
<h1 className="text-3xl font-bold text-slate-800 mb-4">使 ERP Auto</h1> <LayoutDashboard size={48} className="mx-auto text-primary mb-4" />
<p className="text-slate-500 text-lg max-w-2xl mx-auto"> <h1 className="text-3xl font-bold mb-4">使 ERP Auto</h1>
ERP 使 <p className="text-muted-foreground text-lg max-w-2xl mx-auto">
</p> ERP 使
</p>
</div>
</div> </div>
</div> )}
)} {currentPage === 'extractor' && <ExtractorPage />}
{currentPage === 'extractor' && <ExtractorPage />} {currentPage === 'cleaner' && <CleanerPage />}
{currentPage === 'cleaner' && <CleanerPage />} {currentPage === 'settings' && <SettingsPage />}
{currentPage === 'settings' && <SettingsPage />} </ScrollArea>
</main> </main>
</div> </div>