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

View File

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

View File

@@ -4,11 +4,17 @@
* Provides a dialog for managing material type keywords used to identify
* materials for deletion. Admin users can see all records and filter by manager.
* 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 { Plus, Trash2, Save, RotateCcw, Users } from 'lucide-react'
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 { ConfirmDialog } from './ui/ConfirmDialog'
import { useConfirmDialog } from './ui/useConfirmDialog'
@@ -317,18 +323,18 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
onClose()
}
// Get row background color
const getRowStyle = (row: RowState): React.CSSProperties => {
// Get row background color class
const getRowClass = (row: RowState): string => {
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') {
return { backgroundColor: '#dcfce7' }
return 'bg-green-100 dark:bg-green-900/20'
}
if (row.state === 'modified') {
return { backgroundColor: '#fef9c3' }
return 'bg-yellow-100 dark:bg-yellow-900/20'
}
return {}
return ''
}
return (
@@ -342,22 +348,22 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
<div onKeyDown={handleKeyDown}>
{/* Manager filter (admin only) */}
{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 gap-2 text-sm font-medium text-slate-700">
<div className="flex items-center gap-2 text-sm font-medium">
<Users size={16} />
</div>
<div className="flex gap-2">
<button
onClick={() => setSelectedManagers(new Set(managers))}
className="text-xs text-blue-600 hover:underline"
className="text-xs text-primary hover:underline"
>
</button>
<button
onClick={() => setSelectedManagers(new Set())}
className="text-xs text-slate-500 hover:underline"
className="text-xs text-muted-foreground hover:underline"
>
</button>
@@ -367,11 +373,11 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
{managers.map((manager) => (
<label
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
type="checkbox"
className="rounded text-blue-600"
className="rounded text-primary"
checked={selectedManagers.has(manager)}
onChange={(e) => {
setSelectedManagers((prev) => {
@@ -392,131 +398,97 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
{/* Toolbar */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<button
<Button
size="sm"
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)
</button>
<button
</Button>
<Button
size="sm"
onClick={() => selectedRowIndex !== null && deleteRow(selectedRowIndex)}
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)
</button>
<button
</Button>
<Button
size="sm"
onClick={handleReset}
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} />
</button>
</Button>
</div>
<div className="flex items-center gap-3">
{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}
</span>
)}
<button
<Button
size="sm"
onClick={handleSave}
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 ? '保存中...' : '保存'}
</button>
</Button>
</div>
</div>
{/* 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 ? (
<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">
<thead className="bg-slate-100 sticky top-0">
<tr>
<th className="px-4 py-2 text-left font-medium text-slate-700 w-64">
</th>
<th className="px-4 py-2 text-left font-medium text-slate-700"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredRows.filter((r) => r.state !== 'deleted').length === 0 ? (
<tr>
<td colSpan={2} className="px-4 py-8 text-center text-slate-400">
&quot;&quot;
</td>
</tr>
) : (
filteredRows
.filter((r) => r.state !== 'deleted')
.map((row, index) => {
const originalIndex = rows.indexOf(row)
const isSelected = selectedRowIndex === originalIndex
const isEditingMaterial =
editingCell?.rowIndex === originalIndex &&
editingCell?.field === 'materialName'
const isEditingManager =
editingCell?.rowIndex === originalIndex &&
editingCell?.field === 'managerName'
<ScrollArea className="max-h-[400px]">
<Table ref={tableRef}>
<TableHeader className="sticky top-0 bg-muted">
<TableRow>
<TableHead className="w-64">
</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRows.filter((r) => r.state !== 'deleted').length === 0 ? (
<TableRow>
<TableCell colSpan={2} className="text-center text-muted-foreground py-8">
"新增"
</TableCell>
</TableRow>
) : (
filteredRows
.filter((r) => r.state !== 'deleted')
.map((row, index) => {
const originalIndex = rows.indexOf(row)
const isSelected = selectedRowIndex === originalIndex
const isEditingMaterial =
editingCell?.rowIndex === originalIndex &&
editingCell?.field === 'materialName'
const isEditingManager =
editingCell?.rowIndex === originalIndex &&
editingCell?.field === 'managerName'
return (
<tr
key={index}
style={getRowStyle(row)}
className={`${isSelected ? 'ring-2 ring-blue-300 ring-inset' : ''} hover:bg-slate-50 cursor-pointer`}
onClick={() => setSelectedRowIndex(originalIndex)}
>
<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>
return (
<TableRow
key={index}
className={cn(
getRowClass(row),
isSelected && 'ring-2 ring-primary ring-inset',
'cursor-pointer'
)}
</td>
<td className="px-4 py-2">
{isEditingManager ? (
isAdmin ? (
<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>
) : (
onClick={() => setSelectedRowIndex(originalIndex)}
>
<TableCell className="border-r border-border">
{isEditingMaterial ? (
<input
ref={inputRef}
type="text"
@@ -527,31 +499,77 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
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"
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-slate-400 italic"></span>
)}
</div>
)}
</td>
</tr>
)
})
)}
</tbody>
</table>
) : (
<div
className="min-h-[24px] cursor-text"
onDoubleClick={() => startEdit(originalIndex, 'materialName')}
>
{row.record.materialName || (
<span className="text-muted-foreground italic"></span>
)}
</div>
)}
</TableCell>
<TableCell>
{isEditingManager ? (
isAdmin ? (
<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-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>
{/* 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>
| Insert | Delete
{isAdmin && ' | 绿色=新增 | 黄色=已修改'}

View File

@@ -5,10 +5,15 @@
* - Display list of all users
* - Allow admin to select a user
* - Return selected user info
*
* Migrated to use shadcn/ui components
*/
import React, { useState, useRef } from 'react'
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 {
id: number
@@ -60,8 +65,8 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
}
const userTypeStyles: Record<string, string> = {
Admin: 'bg-amber-50 text-amber-600',
User: 'bg-blue-50 text-blue-600'
Admin: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
User: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
}
if (!isOpen) return null
@@ -77,19 +82,20 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
ariaDescribedBy="user-selection-description"
>
<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>
<div className="flex-1 overflow-y-auto mb-4">
<ScrollArea className="flex-1 mb-4 pr-4">
<div className="flex flex-col gap-2">
{users.map((user) => (
<div
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 ${
selectedUserId === user.id ? 'border-blue-500 bg-blue-50' : ''
}`}
className={cn(
"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)}
onDoubleClick={() => handleDoubleClick(user)}
tabIndex={0}
@@ -100,40 +106,42 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
}}
>
<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
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}
</span>
</div>
{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')}
</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">
<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"
<Button
onClick={handleConfirm}
disabled={selectedUserId === null}
>
</button>
<button
className="px-6 py-2 rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium transition-colors"
</Button>
<Button
variant="secondary"
onClick={handleCancel}
>
</button>
</Button>
</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>
</Modal>

View File

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