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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,64 +398,72 @@ 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>
|
</TableHead>
|
||||||
<th className="px-4 py-2 text-left font-medium text-slate-700">负责人</th>
|
<TableHead>负责人</TableHead>
|
||||||
</tr>
|
</TableRow>
|
||||||
</thead>
|
</TableHeader>
|
||||||
<tbody className="divide-y divide-slate-100">
|
<TableBody>
|
||||||
{filteredRows.filter((r) => r.state !== 'deleted').length === 0 ? (
|
{filteredRows.filter((r) => r.state !== 'deleted').length === 0 ? (
|
||||||
<tr>
|
<TableRow>
|
||||||
<td colSpan={2} className="px-4 py-8 text-center text-slate-400">
|
<TableCell colSpan={2} className="text-center text-muted-foreground py-8">
|
||||||
暂无数据,点击"新增"按钮添加物料类型关键词
|
暂无数据,点击"新增"按钮添加物料类型关键词
|
||||||
</td>
|
</TableCell>
|
||||||
</tr>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
filteredRows
|
filteredRows
|
||||||
.filter((r) => r.state !== 'deleted')
|
.filter((r) => r.state !== 'deleted')
|
||||||
@@ -464,13 +478,16 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
|
|||||||
editingCell?.field === 'managerName'
|
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),
|
||||||
|
isSelected && 'ring-2 ring-primary ring-inset',
|
||||||
|
'cursor-pointer'
|
||||||
|
)}
|
||||||
onClick={() => setSelectedRowIndex(originalIndex)}
|
onClick={() => setSelectedRowIndex(originalIndex)}
|
||||||
>
|
>
|
||||||
<td className="px-4 py-2 border-r border-slate-100">
|
<TableCell className="border-r border-border">
|
||||||
{isEditingMaterial ? (
|
{isEditingMaterial ? (
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -482,7 +499,7 @@ 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
|
||||||
@@ -490,12 +507,12 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
|
|||||||
onDoubleClick={() => startEdit(originalIndex, 'materialName')}
|
onDoubleClick={() => startEdit(originalIndex, 'materialName')}
|
||||||
>
|
>
|
||||||
{row.record.materialName || (
|
{row.record.materialName || (
|
||||||
<span className="text-slate-400 italic">双击编辑</span>
|
<span className="text-muted-foreground italic">双击编辑</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</TableCell>
|
||||||
<td className="px-4 py-2">
|
<TableCell>
|
||||||
{isEditingManager ? (
|
{isEditingManager ? (
|
||||||
isAdmin ? (
|
isAdmin ? (
|
||||||
<select
|
<select
|
||||||
@@ -507,7 +524,7 @@ 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"
|
||||||
>
|
>
|
||||||
<option value="">选择负责人</option>
|
<option value="">选择负责人</option>
|
||||||
{managers.map((m) => (
|
{managers.map((m) => (
|
||||||
@@ -527,7 +544,7 @@ 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"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -536,22 +553,23 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
|
|||||||
onDoubleClick={() => startEdit(originalIndex, 'managerName')}
|
onDoubleClick={() => startEdit(originalIndex, 'managerName')}
|
||||||
>
|
>
|
||||||
{row.record.managerName || (
|
{row.record.managerName || (
|
||||||
<span className="text-slate-400 italic">双击编辑</span>
|
<span className="text-muted-foreground italic">双击编辑</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</TableCell>
|
||||||
</tr>
|
</TableRow>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</TableBody>
|
||||||
</table>
|
</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 && ' | 绿色=新增 | 黄色=已修改'}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,17 +173,20 @@ 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">
|
||||||
|
<ScrollArea className="h-full">
|
||||||
{currentPage === 'home' && (
|
{currentPage === 'home' && (
|
||||||
<div className="h-full overflow-auto">
|
<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">
|
<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" />
|
<LayoutDashboard size={48} className="mx-auto text-primary mb-4" />
|
||||||
<h1 className="text-3xl font-bold text-slate-800 mb-4">欢迎使用 ERP Auto</h1>
|
<h1 className="text-3xl font-bold mb-4">欢迎使用 ERP Auto</h1>
|
||||||
<p className="text-slate-500 text-lg max-w-2xl mx-auto">
|
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
|
||||||
自动化处理 ERP 系统中的数据提取和清理任务。请使用上方导航栏选择您需要的功能模块。
|
自动化处理 ERP 系统中的数据提取和清理任务。请使用上方导航栏选择您需要的功能模块。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,6 +195,7 @@ export function AuthenticatedAppShell({
|
|||||||
{currentPage === 'extractor' && <ExtractorPage />}
|
{currentPage === 'extractor' && <ExtractorPage />}
|
||||||
{currentPage === 'cleaner' && <CleanerPage />}
|
{currentPage === 'cleaner' && <CleanerPage />}
|
||||||
{currentPage === 'settings' && <SettingsPage />}
|
{currentPage === 'settings' && <SettingsPage />}
|
||||||
|
</ScrollArea>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user