feat(ui): migrate more components to shadcn/ui
Continue migrating components to use shadcn/ui for better consistency and dark mode support. ## Components Migrated ### ConfirmDialog - Replace Modal with shadcn AlertDialog component - Use AlertDialogAction and AlertDialogCancel with proper asChild prop - Update icon colors for dark mode support ### UpdateDialog - Replace buttons with shadcn Button component - Use shadcn Card for version info display - Add shadcn ScrollArea for changelog view - Update styling with shadcn color tokens ### PlaywrightDownloadDialog - Replace progress bar with shadcn Progress component - Use shadcn Card for stats display - Update colors for dark mode support - Use shadcn Button throughout ### CleanerSidebar - Replace card divs with shadcn Card component - Add shadcn ScrollArea for manager list - Update radio button styling with shadcn tokens - Improve dark mode support ### CleanerToolbar - Replace all buttons with shadcn Button component - Use shadcn Separator for visual separation - Update toolbar styling with shadcn tokens ### Button Component Enhancement - Add React.forwardRef for ref forwarding support - Enables proper ref usage with parent components ## Improvements - Full dark mode support across all migrated components - Consistent styling using shadcn design tokens - Better accessibility with semantic components - Ref forwarding support for Button component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react'
|
||||
import { DownloadCloud, LoaderCircle, X } from 'lucide-react'
|
||||
import Modal from './ui/Modal'
|
||||
import { Button } from './ui/Button'
|
||||
import { Progress } from './ui/shadcn/progress'
|
||||
import { Card, CardContent } from './ui/shadcn/card'
|
||||
|
||||
interface DownloadProgress {
|
||||
percent: number // 0-100
|
||||
downloadedBytes: number
|
||||
@@ -137,23 +141,18 @@ export default function PlaywrightDownloadDialog({
|
||||
{/* Progress Bar */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-slate-600">下载进度</span>
|
||||
<span className="font-semibold text-blue-600">{progress?.percent ?? 0}%</span>
|
||||
</div>
|
||||
<div className="h-3 w-full overflow-hidden rounded-full bg-slate-200">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-blue-600 transition-all duration-300 ease-out"
|
||||
style={{ width: `${progress?.percent ?? 0}%` }}
|
||||
/>
|
||||
<span className="text-muted-foreground">下载进度</span>
|
||||
<span className="font-semibold text-primary">{progress?.percent ?? 0}%</span>
|
||||
</div>
|
||||
<Progress value={progress?.percent ?? 0} className="h-3" />
|
||||
</div>
|
||||
|
||||
{/* Current File */}
|
||||
{progress?.currentFile && (
|
||||
<div className="rounded-lg bg-slate-50 px-4 py-3">
|
||||
<div className="text-xs font-medium text-slate-500">当前文件</div>
|
||||
<div className="rounded-lg bg-muted/50 px-4 py-3">
|
||||
<div className="text-xs font-medium text-muted-foreground">当前文件</div>
|
||||
<div
|
||||
className="mt-1 truncate text-sm text-slate-700"
|
||||
className="mt-1 truncate text-sm text-foreground"
|
||||
title={progress.currentFile}
|
||||
>
|
||||
{progress.currentFile}
|
||||
@@ -163,54 +162,63 @@ export default function PlaywrightDownloadDialog({
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-lg border border-slate-200 px-4 py-3">
|
||||
<div className="text-xs text-slate-500">已下载</div>
|
||||
<div className="mt-1 text-lg font-semibold text-slate-900">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-xs text-muted-foreground">已下载</div>
|
||||
<div className="mt-1 text-lg font-semibold text-foreground">
|
||||
{progress ? formatBytes(progress.downloadedBytes) : '0 B'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 px-4 py-3">
|
||||
<div className="text-xs text-slate-500">总大小</div>
|
||||
<div className="mt-1 text-lg font-semibold text-slate-900">
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-xs text-muted-foreground">总大小</div>
|
||||
<div className="mt-1 text-lg font-semibold text-foreground">
|
||||
{progress && progress.totalBytes > 0
|
||||
? formatBytes(progress.totalBytes)
|
||||
: '计算中...'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 px-4 py-3">
|
||||
<div className="text-xs text-slate-500">速度</div>
|
||||
<div className="mt-1 text-lg font-semibold text-slate-900">
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-xs text-muted-foreground">速度</div>
|
||||
<div className="mt-1 text-lg font-semibold text-foreground">
|
||||
{progress && progress.speed >= 0
|
||||
? `${formatBytes(progress.speed)}/秒`
|
||||
: '计算中...'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 px-4 py-3">
|
||||
<div className="text-xs text-slate-500">剩余时间</div>
|
||||
<div className="mt-1 text-lg font-semibold text-slate-900">
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-xs text-muted-foreground">剩余时间</div>
|
||||
<div className="mt-1 text-lg font-semibold text-foreground">
|
||||
{progress?.eta !== undefined && progress.eta >= 0
|
||||
? formatETA(progress.eta)
|
||||
: '计算中...'}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Cancel Button */}
|
||||
<div className="flex justify-center pt-2">
|
||||
<button
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleCancelDownloadClick}
|
||||
className="inline-flex items-center gap-2 rounded-md border border-rose-200 bg-rose-50 px-4 py-2 text-sm text-rose-700 hover:bg-rose-100"
|
||||
className="gap-2"
|
||||
>
|
||||
<X size={16} />
|
||||
取消下载
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Section */}
|
||||
{error && (
|
||||
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
||||
<div className="rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm text-destructive dark:bg-destructive/20">
|
||||
<div className="font-medium">下载失败</div>
|
||||
<div className="mt-1">{error}</div>
|
||||
</div>
|
||||
@@ -218,7 +226,7 @@ export default function PlaywrightDownloadDialog({
|
||||
|
||||
{/* Success Section (shown briefly before closing) */}
|
||||
{!isDownloading && !error && progress?.percent === 100 && (
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 dark:border-green-800/30 dark:bg-green-900/20 px-4 py-3 text-sm text-green-700 dark:text-green-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<DownloadCloud size={18} />
|
||||
<span className="font-medium">下载完成</span>
|
||||
@@ -230,8 +238,8 @@ export default function PlaywrightDownloadDialog({
|
||||
{/* Initial Loading State */}
|
||||
{isDownloading && !progress && (
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<LoaderCircle size={48} className="animate-spin text-blue-500" />
|
||||
<div className="mt-4 text-sm text-slate-600">正在初始化下载...</div>
|
||||
<LoaderCircle size={48} className="animate-spin text-primary" />
|
||||
<div className="mt-4 text-sm text-muted-foreground">正在初始化下载...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -246,24 +254,25 @@ export default function PlaywrightDownloadDialog({
|
||||
isAlertDialog
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800/30 dark:bg-amber-900/20 px-4 py-3 text-sm text-amber-800 dark:text-amber-400">
|
||||
<div className="font-medium">确定要取消下载吗?</div>
|
||||
<div className="mt-1">这将退出应用程序,您需要重新启动来继续下载。</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleCloseConfirm}
|
||||
className="rounded-md border border-slate-200 px-4 py-2 text-sm text-slate-600 hover:bg-slate-50"
|
||||
>
|
||||
继续下载
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleConfirmCancel}
|
||||
className="inline-flex items-center gap-2 rounded-md bg-rose-600 px-4 py-2 text-sm font-medium text-white hover:bg-rose-700"
|
||||
className="gap-2"
|
||||
>
|
||||
<X size={16} />
|
||||
取消并退出
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -3,6 +3,9 @@ import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { DownloadCloud, LoaderCircle, RefreshCw } from 'lucide-react'
|
||||
import Modal from './ui/Modal'
|
||||
import { Button } from './ui/Button'
|
||||
import { ScrollArea } from './ui/shadcn/scroll-area'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/shadcn/card'
|
||||
import { useUpdateDialogState } from '../hooks/useUpdateDialogState'
|
||||
import type { UserType } from '../../../main/types/user.types'
|
||||
import type {
|
||||
@@ -44,7 +47,7 @@ export default function UpdateDialog({
|
||||
const renderReleaseList = (title: string, releases: UpdateRelease[]) => {
|
||||
if (releases.length === 0) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-slate-200 px-3 py-4 text-sm text-slate-500">
|
||||
<div className="rounded-lg border border-dashed border-border px-3 py-4 text-sm text-muted-foreground">
|
||||
暂无版本
|
||||
</div>
|
||||
)
|
||||
@@ -52,7 +55,7 @@ export default function UpdateDialog({
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</div>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{title}</div>
|
||||
{releases.map((release) => {
|
||||
const selected = isSelectedRelease(release)
|
||||
return (
|
||||
@@ -65,19 +68,19 @@ export default function UpdateDialog({
|
||||
}}
|
||||
className={`w-full rounded-lg border px-3 py-3 text-left transition ${
|
||||
selected
|
||||
? 'border-blue-500 bg-blue-50 text-blue-900'
|
||||
: 'border-slate-200 bg-white text-slate-700 hover:border-slate-300'
|
||||
? 'border-primary bg-primary/10 text-primary'
|
||||
: 'border-border bg-background text-foreground hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="font-medium">V{release.version}</span>
|
||||
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs uppercase text-slate-600">
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs uppercase text-muted-foreground">
|
||||
{release.channel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-500">{release.publishedAt}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{release.publishedAt}</div>
|
||||
{release.notesSummary && (
|
||||
<div className="mt-2 text-xs text-slate-600">{release.notesSummary}</div>
|
||||
<div className="mt-2 text-xs text-foreground/80">{release.notesSummary}</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
@@ -96,21 +99,23 @@ export default function UpdateDialog({
|
||||
disableEscapeKey={isBusy}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between rounded-lg bg-slate-50 px-4 py-3 text-sm">
|
||||
<div className="text-slate-600">
|
||||
当前版本 <span className="font-semibold text-slate-900">V{status?.currentVersion}</span>
|
||||
<span className="ml-2 rounded-full bg-slate-200 px-2 py-0.5 text-xs uppercase text-slate-700">
|
||||
<div className="flex items-center justify-between rounded-lg bg-muted/50 px-4 py-3 text-sm">
|
||||
<div className="text-muted-foreground">
|
||||
当前版本 <span className="font-semibold text-foreground">V{status?.currentVersion}</span>
|
||||
<span className="ml-2 rounded-full bg-muted px-2 py-0.5 text-xs uppercase text-muted-foreground">
|
||||
{status?.currentChannel ?? __APP_CHANNEL__}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => void onRefreshCatalog()}
|
||||
className="inline-flex items-center gap-2 rounded-md border border-slate-200 px-3 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
|
||||
disabled={isBusy}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
刷新
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{catalog?.mode === 'admin' ? (
|
||||
@@ -119,104 +124,106 @@ export default function UpdateDialog({
|
||||
{renderReleaseList('Stable', catalog.channels?.stable ?? [])}
|
||||
{renderReleaseList('Preview', catalog.channels?.preview ?? [])}
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-200 bg-white">
|
||||
<div className="border-b border-slate-200 px-4 py-3">
|
||||
<div className="text-sm font-semibold text-slate-900">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">
|
||||
{selectedRelease
|
||||
? `${selectedRelease.channel.toUpperCase()} V${selectedRelease.version}`
|
||||
: '请选择一个版本'}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
</CardTitle>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{selectedRelease?.notesSummary ?? '选择版本后可查看详细更新说明。'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[420px] overflow-auto px-4 py-4 prose prose-slate max-w-none prose-headings:mb-2 prose-p:my-2 prose-li:my-1">
|
||||
</CardHeader>
|
||||
<CardContent className="max-h-[420px]">
|
||||
<ScrollArea className="h-[340px]">
|
||||
{isLoadingChangelog ? (
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderCircle size={16} className="animate-spin" />
|
||||
正在加载更新说明...
|
||||
</div>
|
||||
) : (
|
||||
<div className="prose prose-slate dark:prose-invert max-w-none prose-headings:mb-2 prose-p:my-2 prose-li:my-1">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{changelog || '暂无更新说明。'}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-slate-200 bg-white">
|
||||
<div className="border-b border-slate-200 px-4 py-3">
|
||||
<div className="text-sm font-semibold text-slate-900">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">
|
||||
{selectedRelease ? `发现稳定版 V${selectedRelease.version}` : '暂无可用更新'}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
</CardTitle>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{status?.currentChannel === 'preview'
|
||||
? '当前设备正在运行预览版,普通用户将更新回稳定版。'
|
||||
: '更新包已在后台准备完成,确认后将自动关闭并重启应用。'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[420px] overflow-auto px-4 py-4 prose prose-slate max-w-none prose-headings:mb-2 prose-p:my-2 prose-li:my-1">
|
||||
</CardHeader>
|
||||
<CardContent className="max-h-[420px]">
|
||||
<ScrollArea className="h-[340px]">
|
||||
{isLoadingChangelog ? (
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderCircle size={16} className="animate-spin" />
|
||||
正在加载更新说明...
|
||||
</div>
|
||||
) : (
|
||||
<div className="prose prose-slate dark:prose-invert max-w-none prose-headings:mb-2 prose-p:my-2 prose-li:my-1">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{changelog || '暂无更新说明。'}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{status?.error && (
|
||||
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
||||
<div className="rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm text-destructive dark:bg-destructive/20">
|
||||
{status.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between border-t border-slate-200 pt-4">
|
||||
<div className="text-sm text-slate-500">{status?.message ?? ' '}</div>
|
||||
<div className="flex items-center justify-between border-t border-border pt-4">
|
||||
<div className="text-sm text-muted-foreground">{status?.message ?? ' '}</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
className="rounded-md border border-slate-200 px-4 py-2 text-sm text-slate-600 hover:bg-slate-50"
|
||||
disabled={isBusy}
|
||||
>
|
||||
稍后
|
||||
</button>
|
||||
</Button>
|
||||
{userType === 'Admin' ? (
|
||||
<button
|
||||
<Button
|
||||
onClick={() =>
|
||||
selectedRelease
|
||||
? void onDownloadAndInstallAdminRelease(selectedRelease)
|
||||
: undefined
|
||||
}
|
||||
className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-300"
|
||||
disabled={!selectedRelease || isBusy}
|
||||
loading={isBusy}
|
||||
className="gap-2"
|
||||
>
|
||||
{isBusy ? (
|
||||
<LoaderCircle size={16} className="animate-spin" />
|
||||
) : (
|
||||
<DownloadCloud size={16} />
|
||||
)}
|
||||
{isBusy ? null : <DownloadCloud size={16} />}
|
||||
下载并更新
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => void onInstallUserRelease()}
|
||||
className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-300"
|
||||
disabled={!selectedRelease || isBusy}
|
||||
loading={isBusy}
|
||||
className="gap-2"
|
||||
>
|
||||
{isBusy ? (
|
||||
<LoaderCircle size={16} className="animate-spin" />
|
||||
) : (
|
||||
<DownloadCloud size={16} />
|
||||
)}
|
||||
{isBusy ? null : <DownloadCloud size={16} />}
|
||||
立即更新
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react'
|
||||
import { DatabaseZap, Layers, Users } from 'lucide-react'
|
||||
import { Card } from '../ui/shadcn/card'
|
||||
import { ScrollArea } from '../ui/shadcn/scroll-area'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
|
||||
interface CleanerSidebarProps {
|
||||
valMode: 'full' | 'filtered'
|
||||
@@ -18,15 +21,20 @@ export function CleanerSidebar({
|
||||
}: CleanerSidebarProps): React.JSX.Element {
|
||||
return (
|
||||
<div className="w-full xl:w-[380px] flex-shrink-0 flex flex-col gap-5 xl:self-start overflow-auto">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||
<h3 className="text-base font-semibold mb-4 flex items-center gap-2 text-slate-800">
|
||||
<Card className="p-5">
|
||||
<h3 className="text-base font-semibold mb-4 flex items-center gap-2">
|
||||
<DatabaseZap size={18} className="text-blue-500" />
|
||||
校验数据来源
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${valMode === 'full' ? 'bg-blue-50 border-blue-200' : 'hover:bg-slate-50 border-slate-200'}`}
|
||||
className={cn(
|
||||
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
|
||||
valMode === 'full'
|
||||
? 'bg-primary/10 border-primary'
|
||||
: 'hover:bg-muted/50 border-border'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -36,13 +44,18 @@ export function CleanerSidebar({
|
||||
onChange={() => setValMode('full')}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-800">数据库 - 全表校验</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">校验数据库中所有待处理物料</div>
|
||||
<div className="text-sm font-medium">数据库 - 全表校验</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">校验数据库中所有待处理物料</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${valMode === 'filtered' ? 'bg-blue-50 border-blue-200' : 'hover:bg-slate-50 border-slate-200'}`}
|
||||
className={cn(
|
||||
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
|
||||
valMode === 'filtered'
|
||||
? 'bg-primary/10 border-primary'
|
||||
: 'hover:bg-muted/50 border-border'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
@@ -52,10 +65,10 @@ export function CleanerSidebar({
|
||||
onChange={() => setValMode('filtered')}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<div className="text-sm font-medium text-slate-800">数据库 - ProductionID 过滤</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">仅校验指定订单号相关的物料</div>
|
||||
<div className="text-sm font-medium">数据库 - ProductionID 过滤</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">仅校验指定订单号相关的物料</div>
|
||||
{valMode === 'filtered' && (
|
||||
<div className="mt-3 text-xs text-blue-700 bg-blue-100/50 rounded p-2 flex items-start gap-1.5">
|
||||
<div className="mt-3 text-xs text-primary bg-primary/10 rounded p-2 flex items-start gap-1.5">
|
||||
<Layers size={14} className="mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
自动使用<strong>【数据提取】</strong>模块中共享的订单号列表进行过滤。
|
||||
@@ -65,39 +78,40 @@ export function CleanerSidebar({
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||
<Card className="p-5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-base font-semibold flex items-center gap-2 text-slate-800">
|
||||
<h3 className="text-base font-semibold flex items-center gap-2">
|
||||
<Users size={18} className="text-indigo-500" />
|
||||
筛选 (按负责人)
|
||||
</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 mt-3 max-h-[120px] overflow-y-auto pr-1">
|
||||
<ScrollArea className="h-[120px] pr-1">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{managers.map((manager) => (
|
||||
<label
|
||||
key={manager}
|
||||
className="flex items-center gap-2 text-sm text-slate-700 cursor-pointer hover:bg-slate-50 p-1.5 rounded"
|
||||
className="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 p-1.5 rounded"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded text-blue-600"
|
||||
className="rounded text-primary"
|
||||
checked={selectedManagers.has(manager)}
|
||||
onChange={(e) => {
|
||||
setSelectedManagers((prev) => {
|
||||
@@ -111,9 +125,10 @@ export function CleanerSidebar({
|
||||
{manager || '未分配'}
|
||||
</label>
|
||||
))}
|
||||
{managers.length === 0 && <div className="text-sm text-slate-400">暂无负责人数据</div>}
|
||||
</div>
|
||||
{managers.length === 0 && <div className="text-sm text-muted-foreground">暂无负责人数据</div>}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
Settings2,
|
||||
Square
|
||||
} from 'lucide-react'
|
||||
import { Button } from '../ui/Button'
|
||||
import { Separator } from '../ui/shadcn/separator'
|
||||
import type { ValidationResult } from '../../hooks/cleaner/types'
|
||||
|
||||
interface CleanerToolbarProps {
|
||||
@@ -48,25 +50,29 @@ export function CleanerToolbar({
|
||||
}: CleanerToolbarProps): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="p-4 border-b border-slate-200 flex-shrink-0">
|
||||
<button
|
||||
<div className="p-4 border-b border-border flex-shrink-0">
|
||||
<Button
|
||||
onClick={() => void handleValidation()}
|
||||
disabled={isValidationRunning}
|
||||
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-800 py-3 rounded-lg font-medium transition-colors shadow-sm flex justify-center items-center gap-2 border border-slate-200 disabled:opacity-50"
|
||||
className="w-full gap-2"
|
||||
loading={isValidationRunning}
|
||||
>
|
||||
<Search size={18} /> {isValidationRunning ? '正在获取...' : '获取并校验物料状态'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 border-b border-slate-200 px-4 py-3 flex justify-between items-center flex-shrink-0">
|
||||
<div className="bg-muted/30 border-b border-border px-4 py-3 flex justify-between items-center flex-shrink-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setSelectedItems(new Set(filteredResults.map((r) => r.materialCode)))}
|
||||
className="text-xs bg-white border border-slate-300 text-slate-700 px-2.5 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1"
|
||||
variant="secondary"
|
||||
className="gap-1"
|
||||
>
|
||||
<CheckSquare size={14} className="text-blue-600" /> 全选
|
||||
</button>
|
||||
<button
|
||||
<CheckSquare size={14} className="text-primary" /> 全选
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const visibleCodes = new Set(filteredResults.map((r) => r.materialCode))
|
||||
setSelectedItems((prev) => {
|
||||
@@ -77,14 +83,16 @@ export function CleanerToolbar({
|
||||
return next
|
||||
})
|
||||
}}
|
||||
className="text-xs bg-white border border-slate-300 text-slate-700 px-2.5 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1"
|
||||
variant="secondary"
|
||||
className="gap-1"
|
||||
>
|
||||
<Square size={14} className="text-slate-400" /> 取消
|
||||
</button>
|
||||
<Square size={14} className="text-muted-foreground" /> 取消
|
||||
</Button>
|
||||
|
||||
<div className="w-px h-4 bg-slate-300 mx-1"></div>
|
||||
<Separator orientation="vertical" className="h-4 mx-1" />
|
||||
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const checkedCodes = filteredResults
|
||||
.filter((r) => selectedItems.has(r.materialCode))
|
||||
@@ -93,49 +101,58 @@ export function CleanerToolbar({
|
||||
setHiddenItems((prev) => new Set([...prev, ...checkedCodes]))
|
||||
}
|
||||
}}
|
||||
className="text-xs bg-white border border-slate-300 text-slate-700 px-2.5 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1"
|
||||
variant="secondary"
|
||||
className="gap-1"
|
||||
>
|
||||
<EyeOff size={14} /> 隐藏勾选
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setHiddenItems(new Set())}
|
||||
className="text-xs bg-white border border-slate-300 text-slate-700 px-2.5 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1"
|
||||
variant="secondary"
|
||||
className="gap-1"
|
||||
>
|
||||
<Eye size={14} /> 显示全部
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<div className="w-px h-4 bg-slate-300 mx-1"></div>
|
||||
<Separator orientation="vertical" className="h-4 mx-1" />
|
||||
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void handleConfirmDeletion()}
|
||||
disabled={isRunning || validationResults.length === 0}
|
||||
className="text-xs bg-indigo-50 border border-indigo-200 text-indigo-700 px-3 py-1.5 rounded shadow-sm hover:bg-indigo-100 flex items-center gap-1.5 font-medium transition-colors disabled:opacity-50"
|
||||
className="gap-1.5 bg-indigo-50 text-indigo-700 border-indigo-200 hover:bg-indigo-100 dark:bg-indigo-900/20 dark:text-indigo-400 dark:border-indigo-800"
|
||||
>
|
||||
<HardDrive size={14} /> 确认删除 (同步数据库)
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setIsTypeDialogOpen(true)}
|
||||
ref={typeManagementButtonRef}
|
||||
className="text-xs bg-white border border-slate-300 text-slate-700 px-3 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1.5"
|
||||
variant="secondary"
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Settings2 size={14} /> 类型管理
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void handleExportResults()}
|
||||
disabled={isExporting || filteredResults.length === 0}
|
||||
className="text-xs bg-blue-50 border border-blue-200 text-blue-700 px-3 py-1.5 rounded shadow-sm hover:bg-blue-100 flex items-center gap-1.5 font-medium disabled:opacity-50"
|
||||
loading={isExporting}
|
||||
className="gap-1.5 bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800"
|
||||
>
|
||||
<FileSpreadsheet size={14} /> {isExporting ? '导出中...' : '导出结果'}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setIsReportViewerOpen(true)}
|
||||
className="text-xs bg-emerald-50 border border-emerald-200 text-emerald-700 px-3 py-1.5 rounded shadow-sm hover:bg-emerald-100 flex items-center gap-1.5 font-medium transition-colors"
|
||||
className="gap-1.5 bg-emerald-50 text-emerald-700 border-emerald-200 hover:bg-emerald-100 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800"
|
||||
>
|
||||
<FileText size={14} /> 查看报告
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -34,7 +34,8 @@ const sizeMap: Record<ButtonSize, 'default' | 'sm' | 'lg' | 'icon'> = {
|
||||
lg: 'lg'
|
||||
}
|
||||
|
||||
export function Button({
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
@@ -43,12 +44,13 @@ export function Button({
|
||||
className = '',
|
||||
disabled,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
}, ref) => {
|
||||
const newVariant = variantMap[variant]
|
||||
const newSize = sizeMap[size]
|
||||
|
||||
return (
|
||||
<ShadcnButton
|
||||
ref={ref}
|
||||
variant={newVariant}
|
||||
size={newSize}
|
||||
loading={loading}
|
||||
@@ -60,6 +62,9 @@ export function Button({
|
||||
{children}
|
||||
</ShadcnButton>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button }
|
||||
export default Button
|
||||
|
||||
@@ -2,12 +2,23 @@
|
||||
* ConfirmDialog Component
|
||||
*
|
||||
* A confirmation dialog component for displaying confirmation prompts.
|
||||
* Extends the Modal component with consistent styling and behavior.
|
||||
* Now uses shadcn/ui AlertDialog component for better accessibility and styling.
|
||||
*
|
||||
* Migrated to use shadcn/ui AlertDialog
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { AlertTriangle, Info, AlertCircle } from 'lucide-react'
|
||||
import { Modal } from './Modal'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from './shadcn/alert-dialog'
|
||||
import { Button } from './Button'
|
||||
|
||||
export type ConfirmDialogVariant = 'danger' | 'warning' | 'info'
|
||||
@@ -29,18 +40,18 @@ const variantStyles: Record<
|
||||
> = {
|
||||
danger: {
|
||||
icon: <AlertCircle className="w-6 h-6" />,
|
||||
iconColor: 'text-red-500',
|
||||
buttonVariant: 'danger'
|
||||
iconColor: 'text-destructive',
|
||||
buttonVariant: 'danger' as const
|
||||
},
|
||||
warning: {
|
||||
icon: <AlertTriangle className="w-6 h-6" />,
|
||||
iconColor: 'text-yellow-500',
|
||||
buttonVariant: 'primary'
|
||||
iconColor: 'text-amber-500 dark:text-amber-400',
|
||||
buttonVariant: 'primary' as const
|
||||
},
|
||||
info: {
|
||||
icon: <Info className="w-6 h-6" />,
|
||||
iconColor: 'text-blue-500',
|
||||
buttonVariant: 'primary'
|
||||
iconColor: 'text-primary',
|
||||
buttonVariant: 'primary' as const
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,40 +89,56 @@ export function ConfirmDialog({
|
||||
|
||||
const styles = variantStyles[variant]
|
||||
|
||||
// For AlertDialog, we use open/onOpenChange
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onCancel}
|
||||
title={title}
|
||||
size="md"
|
||||
showCloseButton={false}
|
||||
isAlertDialog={true}
|
||||
initialFocusSelector="[data-autofocus]"
|
||||
>
|
||||
<AlertDialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Icon */}
|
||||
<div className={`flex-shrink-0 ${styles.iconColor}`}>{styles.icon}</div>
|
||||
|
||||
{/* Message */}
|
||||
{/* Title and Description */}
|
||||
<div className="flex-1">
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="mt-2">
|
||||
{typeof message === 'string' ? (
|
||||
<p className="text-gray-700 whitespace-pre-wrap">{message}</p>
|
||||
<p className="whitespace-pre-wrap text-foreground">{message}</p>
|
||||
) : (
|
||||
message
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogHeader>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="secondary" onClick={onCancel}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button data-autofocus="true" variant={styles.buttonVariant} onClick={onConfirm}>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button
|
||||
data-autofocus="true"
|
||||
variant={styles.buttonVariant}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user