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