refactor(ui): replace native alerts with toast notifications and confirm dialogs
This commit refactors all native browser alert() and confirm() dialogs to use the app's custom UI components for consistent user experience. **Changes:** - Add ConfirmDialog component with danger/warning/info variants - Add useConfirmDialog hook for promise-based dialog API - Add formatListMessage utility for truncating long lists - Replace 18 alert() calls with toast notifications in useCleaner.ts - Replace 7 alert()/confirm() calls in MaterialTypeManagementDialog.tsx - Render Toast component in App.tsx for global notifications - Add keyboard shortcuts (Enter to confirm, Escape to cancel) **Benefits:** - Consistent UI design across all notifications - Non-blocking notifications for better UX - Better accessibility with proper ARIA roles and focus management - Multi-line message support with truncation for long lists **Files Modified:** - src/renderer/src/components/ui/ConfirmDialog.tsx (new) - src/renderer/src/stores/useAppStore.ts (add formatListMessage) - src/renderer/src/hooks/useCleaner.ts (refactor error handling) - src/renderer/src/components/MaterialTypeManagementDialog.tsx (refactor dialogs) - src/renderer/src/pages/CleanerPage.tsx (add ConfirmDialog) - src/renderer/src/App.tsx (render Toast component) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import LoginDialog from './components/LoginDialog'
|
|||||||
import UserSelectionDialog, {
|
import UserSelectionDialog, {
|
||||||
type UserInfo as SelectedUserInfo
|
type UserInfo as SelectedUserInfo
|
||||||
} from './components/UserSelectionDialog'
|
} from './components/UserSelectionDialog'
|
||||||
|
import { Toast } from './components/ui/Toast'
|
||||||
import ExtractorPage from './pages/ExtractorPage'
|
import ExtractorPage from './pages/ExtractorPage'
|
||||||
import CleanerPage from './pages/CleanerPage'
|
import CleanerPage from './pages/CleanerPage'
|
||||||
import SettingsPage from './pages/SettingsPage'
|
import SettingsPage from './pages/SettingsPage'
|
||||||
@@ -409,6 +410,9 @@ function App(): React.JSX.Element {
|
|||||||
{currentPage === 'settings' && <SettingsPage />}
|
{currentPage === 'settings' && <SettingsPage />}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Toast Notifications */}
|
||||||
|
<Toast />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
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 { showSuccess, showError, showInfo } from '../stores/useAppStore'
|
||||||
|
import { useConfirmDialog } from './ui/ConfirmDialog'
|
||||||
|
import { ConfirmDialog } from './ui/ConfirmDialog'
|
||||||
|
|
||||||
interface MaterialTypeRecord {
|
interface MaterialTypeRecord {
|
||||||
id?: number
|
id?: number
|
||||||
@@ -49,6 +52,9 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
|
|||||||
const tableRef = useRef<HTMLTableElement>(null)
|
const tableRef = useRef<HTMLTableElement>(null)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
// Confirmation dialog hook
|
||||||
|
const { confirm, dialog: confirmDialog } = useConfirmDialog()
|
||||||
|
|
||||||
// Calculate pending changes count
|
// Calculate pending changes count
|
||||||
const pendingCount = rows.filter(
|
const pendingCount = rows.filter(
|
||||||
(r) => r.state === 'new' || r.state === 'modified' || r.state === 'deleted'
|
(r) => r.state === 'new' || r.state === 'modified' || r.state === 'deleted'
|
||||||
@@ -239,7 +245,7 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toInsert.length === 0 && toUpdate.length === 0 && toDelete.length === 0) {
|
if (toInsert.length === 0 && toUpdate.length === 0 && toDelete.length === 0) {
|
||||||
alert('没有需要保存的更改')
|
showInfo('没有需要保存的更改')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +254,12 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
|
|||||||
if (toUpdate.length > 0) confirmParts.push(`更新 ${toUpdate.length} 条记录`)
|
if (toUpdate.length > 0) confirmParts.push(`更新 ${toUpdate.length} 条记录`)
|
||||||
if (toDelete.length > 0) confirmParts.push(`删除 ${toDelete.length} 条记录`)
|
if (toDelete.length > 0) confirmParts.push(`删除 ${toDelete.length} 条记录`)
|
||||||
|
|
||||||
if (!window.confirm(`确认以下操作?\n\n${confirmParts.join('\n')}`)) return
|
const confirmed = await confirm({
|
||||||
|
title: '确认操作',
|
||||||
|
message: `确认以下操作?\n\n${confirmParts.join('\n')}`,
|
||||||
|
variant: 'warning'
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
@@ -262,31 +273,42 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert(
|
showSuccess(
|
||||||
`保存完成!\n成功:${payload?.stats?.success || 0} 条\n失败:${payload?.stats?.failed || 0} 条`
|
`保存完成!\n成功:${payload?.stats?.success || 0} 条\n失败:${payload?.stats?.failed || 0} 条`
|
||||||
)
|
)
|
||||||
await loadData()
|
await loadData()
|
||||||
} else {
|
} else {
|
||||||
alert(`保存失败:${result.error || '未知错误'}`)
|
showError(`保存失败:${result.error || '未知错误'}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`保存失败:${error instanceof Error ? error.message : '未知错误'}`)
|
showError(`保存失败:${error instanceof Error ? error.message : '未知错误'}`)
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset changes
|
// Reset changes
|
||||||
const handleReset = () => {
|
const handleReset = async () => {
|
||||||
if (pendingCount === 0) return
|
if (pendingCount === 0) return
|
||||||
if (!window.confirm('确定要放弃所有未保存的更改吗?')) return
|
const confirmed = await confirm({
|
||||||
|
title: '确认重置',
|
||||||
|
message: '确定要放弃所有未保存的更改吗?',
|
||||||
|
variant: 'warning'
|
||||||
|
})
|
||||||
|
if (confirmed) {
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle close with unsaved changes warning
|
// Handle close with unsaved changes warning
|
||||||
const handleClose = () => {
|
const handleClose = async () => {
|
||||||
if (pendingCount > 0) {
|
if (pendingCount > 0) {
|
||||||
if (!window.confirm('有未保存的更改,确定要关闭吗?')) return
|
const confirmed = await confirm({
|
||||||
|
title: '确认关闭',
|
||||||
|
message: '有未保存的更改,确定要关闭吗?',
|
||||||
|
variant: 'warning'
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
}
|
}
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
@@ -533,6 +555,9 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
|
|||||||
<span>共 {filteredRows.filter((r) => r.state !== 'deleted').length} 条记录</span>
|
<span>共 {filteredRows.filter((r) => r.state !== 'deleted').length} 条记录</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Confirmation Dialog */}
|
||||||
|
{confirmDialog && <ConfirmDialog {...confirmDialog} />}
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
172
src/renderer/src/components/ui/ConfirmDialog.tsx
Normal file
172
src/renderer/src/components/ui/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* ConfirmDialog Component
|
||||||
|
*
|
||||||
|
* A confirmation dialog component for displaying confirmation prompts.
|
||||||
|
* Extends the Modal component with consistent styling and behavior.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useCallback, useEffect } from 'react'
|
||||||
|
import { AlertTriangle, Info, AlertCircle } from 'lucide-react'
|
||||||
|
import { Modal } from './Modal'
|
||||||
|
import { Button } from './Button'
|
||||||
|
|
||||||
|
export type ConfirmDialogVariant = 'danger' | 'warning' | 'info'
|
||||||
|
|
||||||
|
export interface ConfirmDialogProps {
|
||||||
|
isOpen: boolean
|
||||||
|
title: string
|
||||||
|
message: string | React.ReactNode
|
||||||
|
confirmText?: string
|
||||||
|
cancelText?: string
|
||||||
|
variant?: ConfirmDialogVariant
|
||||||
|
onConfirm: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantStyles: Record<
|
||||||
|
ConfirmDialogVariant,
|
||||||
|
{ icon: React.ReactNode; iconColor: string; buttonVariant: 'danger' | 'primary' }
|
||||||
|
> = {
|
||||||
|
danger: {
|
||||||
|
icon: <AlertCircle className="w-6 h-6" />,
|
||||||
|
iconColor: 'text-red-500',
|
||||||
|
buttonVariant: 'danger'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
icon: <AlertTriangle className="w-6 h-6" />,
|
||||||
|
iconColor: 'text-yellow-500',
|
||||||
|
buttonVariant: 'primary'
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
icon: <Info className="w-6 h-6" />,
|
||||||
|
iconColor: 'text-blue-500',
|
||||||
|
buttonVariant: 'primary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmDialog({
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmText = '确认',
|
||||||
|
cancelText = '取消',
|
||||||
|
variant = 'info',
|
||||||
|
onConfirm,
|
||||||
|
onCancel
|
||||||
|
}: ConfirmDialogProps) {
|
||||||
|
// Handle keyboard shortcuts
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (!isOpen) return
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
onConfirm()
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isOpen, onConfirm, onCancel]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
}, [handleKeyDown])
|
||||||
|
|
||||||
|
const styles = variantStyles[variant]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onCancel}
|
||||||
|
title={title}
|
||||||
|
size="md"
|
||||||
|
showCloseButton={false}
|
||||||
|
isAlertDialog={true}
|
||||||
|
initialFocusSelector="[data-autofocus]"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className={`flex-shrink-0 ${styles.iconColor}`}>{styles.icon}</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<div className="flex-1">
|
||||||
|
{typeof message === 'string' ? (
|
||||||
|
<p className="text-gray-700 whitespace-pre-wrap">{message}</p>
|
||||||
|
) : (
|
||||||
|
message
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="flex justify-end gap-3 mt-6">
|
||||||
|
<Button variant="secondary" onClick={onCancel}>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-autofocus="true"
|
||||||
|
variant={styles.buttonVariant}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for using confirmation dialogs
|
||||||
|
* Returns a confirm function that shows a dialog and resolves with user's choice
|
||||||
|
*/
|
||||||
|
export function useConfirmDialog() {
|
||||||
|
const [config, setConfig] = useState<
|
||||||
|
(Omit<ConfirmDialogProps, 'isOpen' | 'onConfirm' | 'onCancel'> & {
|
||||||
|
resolve: (value: boolean) => void
|
||||||
|
}) | null>(null)
|
||||||
|
|
||||||
|
const confirm = useCallback(
|
||||||
|
(
|
||||||
|
options: Omit<ConfirmDialogProps, 'isOpen' | 'onConfirm' | 'onCancel'>
|
||||||
|
): Promise<boolean> => {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
setConfig({
|
||||||
|
...options,
|
||||||
|
resolve
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleConfirm = useCallback(() => {
|
||||||
|
if (config) {
|
||||||
|
config.resolve(true)
|
||||||
|
setConfig(null)
|
||||||
|
}
|
||||||
|
}, [config])
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
if (config) {
|
||||||
|
config.resolve(false)
|
||||||
|
setConfig(null)
|
||||||
|
}
|
||||||
|
}, [config])
|
||||||
|
|
||||||
|
const dialog = config
|
||||||
|
? {
|
||||||
|
...config,
|
||||||
|
isOpen: true,
|
||||||
|
onConfirm: handleConfirm,
|
||||||
|
onCancel: handleCancel
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
|
||||||
|
return { confirm, dialog }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfirmDialog
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
||||||
|
import { showSuccess, showError, showWarning, formatListMessage } from '../stores/useAppStore'
|
||||||
|
import { ConfirmDialogProps } from '../components/ui/ConfirmDialog'
|
||||||
|
|
||||||
export interface ValidationResult {
|
export interface ValidationResult {
|
||||||
materialName: string
|
materialName: string
|
||||||
@@ -81,6 +83,32 @@ export function useCleaner() {
|
|||||||
const [editValue, setEditValue] = useState('')
|
const [editValue, setEditValue] = useState('')
|
||||||
const inputRef = useRef<HTMLInputElement | HTMLSelectElement>(null)
|
const inputRef = useRef<HTMLInputElement | HTMLSelectElement>(null)
|
||||||
|
|
||||||
|
// Confirmation dialog state
|
||||||
|
const [confirmDialog, setConfirmDialog] = useState<ConfirmDialogProps | null>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a confirmation dialog and return user's choice
|
||||||
|
*/
|
||||||
|
const showConfirmDialog = useCallback(
|
||||||
|
(options: Omit<ConfirmDialogProps, 'isOpen' | 'onConfirm' | 'onCancel'>): Promise<boolean> => {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
setConfirmDialog({
|
||||||
|
...options,
|
||||||
|
isOpen: true,
|
||||||
|
onConfirm: () => {
|
||||||
|
setConfirmDialog(null)
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
setConfirmDialog(null)
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
// Check admin status and get shared Production IDs on mount
|
// Check admin status and get shared Production IDs on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializePage = async () => {
|
const initializePage = async () => {
|
||||||
@@ -188,11 +216,12 @@ export function useCleaner() {
|
|||||||
setManagers(Array.from(uniqueManagers))
|
setManagers(Array.from(uniqueManagers))
|
||||||
setSelectedManagers(uniqueManagers)
|
setSelectedManagers(uniqueManagers)
|
||||||
}
|
}
|
||||||
|
showSuccess('校验完成')
|
||||||
} else {
|
} else {
|
||||||
alert(response.error || validationData?.error || '校验失败')
|
showError(response.error || validationData?.error || '校验失败')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err instanceof Error ? err.message : '校验过程中发生未知错误')
|
showError(err instanceof Error ? err.message : '校验过程中发生未知错误')
|
||||||
} finally {
|
} finally {
|
||||||
setIsValidationRunning(false)
|
setIsValidationRunning(false)
|
||||||
}
|
}
|
||||||
@@ -260,7 +289,10 @@ export function useCleaner() {
|
|||||||
const handleConfirmDeletion = async () => {
|
const handleConfirmDeletion = async () => {
|
||||||
const resultsToProcess = isAdmin ? validationResults : filteredResults
|
const resultsToProcess = isAdmin ? validationResults : filteredResults
|
||||||
|
|
||||||
if (resultsToProcess.length === 0) return alert('没有可处理的数据')
|
if (resultsToProcess.length === 0) {
|
||||||
|
showWarning('没有可处理的数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const materialsToUpsert: { materialCode: string; managerName: string }[] = []
|
const materialsToUpsert: { materialCode: string; managerName: string }[] = []
|
||||||
const materialsToDelete: string[] = []
|
const materialsToDelete: string[] = []
|
||||||
@@ -279,21 +311,28 @@ export function useCleaner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (missingManager.length > 0) {
|
if (missingManager.length > 0) {
|
||||||
alert(
|
showError(
|
||||||
`以下已勾选的记录缺少负责人信息,无法保存:\n\n${missingManager.slice(0, 10).join('\n')}`
|
`以下已勾选的记录缺少负责人信息,无法保存:\n\n${formatListMessage(missingManager, 10)}`
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (materialsToUpsert.length === 0 && materialsToDelete.length === 0)
|
if (materialsToUpsert.length === 0 && materialsToDelete.length === 0) {
|
||||||
return alert('没有需要处理的记录')
|
showWarning('没有需要处理的记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const confirmParts: string[] = []
|
const confirmParts: string[] = []
|
||||||
if (materialsToUpsert.length > 0)
|
if (materialsToUpsert.length > 0)
|
||||||
confirmParts.push(`写入/更新 ${materialsToUpsert.length} 条记录`)
|
confirmParts.push(`写入/更新 ${materialsToUpsert.length} 条记录`)
|
||||||
if (materialsToDelete.length > 0) confirmParts.push(`删除 ${materialsToDelete.length} 条记录`)
|
if (materialsToDelete.length > 0) confirmParts.push(`删除 ${materialsToDelete.length} 条记录`)
|
||||||
|
|
||||||
if (!window.confirm(`确认以下操作吗?\n\n${confirmParts.join('\n')}`)) return
|
const confirmed = await showConfirmDialog({
|
||||||
|
title: '确认操作',
|
||||||
|
message: `确认以下操作吗?\n\n${confirmParts.join('\n')}`,
|
||||||
|
variant: 'warning'
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsRunning(true)
|
setIsRunning(true)
|
||||||
@@ -315,7 +354,7 @@ export function useCleaner() {
|
|||||||
msgParts.push(`删除成功:${payload?.count || 0} 条`)
|
msgParts.push(`删除成功:${payload?.count || 0} 条`)
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(`操作完成!\n\n${msgParts.join('\n')}`)
|
showSuccess(`操作完成!\n\n${msgParts.join('\n')}`)
|
||||||
|
|
||||||
// Reload managers if admin
|
// Reload managers if admin
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
@@ -326,7 +365,7 @@ export function useCleaner() {
|
|||||||
setManagers(payload?.managers ?? [])
|
setManagers(payload?.managers ?? [])
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err instanceof Error ? err.message : '操作失败')
|
showError(err instanceof Error ? err.message : '操作失败')
|
||||||
} finally {
|
} finally {
|
||||||
setIsRunning(false)
|
setIsRunning(false)
|
||||||
}
|
}
|
||||||
@@ -334,7 +373,14 @@ export function useCleaner() {
|
|||||||
|
|
||||||
const handleExecuteDeletion = async () => {
|
const handleExecuteDeletion = async () => {
|
||||||
if (!dryRun) {
|
if (!dryRun) {
|
||||||
if (!window.confirm('警告:正式执行将删除 ERP 系统中的物料数据,是否继续?')) return
|
const confirmed = await showConfirmDialog({
|
||||||
|
title: '警告',
|
||||||
|
message: '正式执行将删除 ERP 系统中的物料数据,是否继续?',
|
||||||
|
variant: 'danger',
|
||||||
|
confirmText: '继续',
|
||||||
|
cancelText: '取消'
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsRunning(true)
|
setIsRunning(true)
|
||||||
@@ -386,7 +432,7 @@ export function useCleaner() {
|
|||||||
throw new Error(response.error || '清理失败')
|
throw new Error(response.error || '清理失败')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err instanceof Error ? err.message : '发生未知错误')
|
showError(err instanceof Error ? err.message : '发生未知错误')
|
||||||
} finally {
|
} finally {
|
||||||
setIsRunning(false)
|
setIsRunning(false)
|
||||||
setIsExecuting(false)
|
setIsExecuting(false)
|
||||||
@@ -397,7 +443,7 @@ export function useCleaner() {
|
|||||||
|
|
||||||
const handleExportResults = async () => {
|
const handleExportResults = async () => {
|
||||||
if (filteredResults.length === 0) {
|
if (filteredResults.length === 0) {
|
||||||
alert('没有数据可导出')
|
showWarning('没有数据可导出')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,12 +464,12 @@ export function useCleaner() {
|
|||||||
const exportData = response.success ? (response.data as any) : null
|
const exportData = response.success ? (response.data as any) : null
|
||||||
|
|
||||||
if (response.success && exportData?.success !== false) {
|
if (response.success && exportData?.success !== false) {
|
||||||
alert(`导出成功!\n文件已保存到:${exportData?.filePath ?? ''}`)
|
showSuccess(`导出成功!\n文件已保存到:${exportData?.filePath ?? ''}`)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.error || exportData?.error || '导出失败')
|
throw new Error(response.error || exportData?.error || '导出失败')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err instanceof Error ? err.message : '导出过程中发生错误')
|
showError(err instanceof Error ? err.message : '导出过程中发生错误')
|
||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false)
|
setIsExporting(false)
|
||||||
}
|
}
|
||||||
@@ -473,6 +519,7 @@ export function useCleaner() {
|
|||||||
handleCheckboxToggle,
|
handleCheckboxToggle,
|
||||||
handleConfirmDeletion,
|
handleConfirmDeletion,
|
||||||
handleExecuteDeletion,
|
handleExecuteDeletion,
|
||||||
handleExportResults
|
handleExportResults,
|
||||||
|
confirmDialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import MaterialTypeManagementDialog from '../components/MaterialTypeManagementDialog'
|
import MaterialTypeManagementDialog from '../components/MaterialTypeManagementDialog'
|
||||||
import ExecutionReportDialog from '../components/ExecutionReportDialog'
|
import ExecutionReportDialog from '../components/ExecutionReportDialog'
|
||||||
|
import { ConfirmDialog } from '../components/ui/ConfirmDialog'
|
||||||
import { useCleaner } from '../hooks/useCleaner'
|
import { useCleaner } from '../hooks/useCleaner'
|
||||||
|
|
||||||
const CleanerPage: React.FC = () => {
|
const CleanerPage: React.FC = () => {
|
||||||
@@ -65,7 +66,8 @@ const CleanerPage: React.FC = () => {
|
|||||||
handleCheckboxToggle,
|
handleCheckboxToggle,
|
||||||
handleConfirmDeletion,
|
handleConfirmDeletion,
|
||||||
handleExecuteDeletion,
|
handleExecuteDeletion,
|
||||||
handleExportResults
|
handleExportResults,
|
||||||
|
confirmDialog
|
||||||
} = useCleaner()
|
} = useCleaner()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -495,6 +497,9 @@ const CleanerPage: React.FC = () => {
|
|||||||
startTime={startTime}
|
startTime={startTime}
|
||||||
triggerRef={executeButtonRef}
|
triggerRef={executeButtonRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Confirmation Dialog */}
|
||||||
|
{confirmDialog && <ConfirmDialog {...confirmDialog} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,4 +112,17 @@ export const selectToasts = (state: AppState) => state.toasts
|
|||||||
export const selectSidebarCollapsed = (state: AppState) => state.sidebarCollapsed
|
export const selectSidebarCollapsed = (state: AppState) => state.sidebarCollapsed
|
||||||
export const selectCurrentPage = (state: AppState) => state.currentPage
|
export const selectCurrentPage = (state: AppState) => state.currentPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a list of items into a multi-line message with truncation
|
||||||
|
* @param items - Array of strings to format
|
||||||
|
* @param maxItems - Maximum number of items to show before truncating (default: 10)
|
||||||
|
* @returns Formatted multi-line string
|
||||||
|
*/
|
||||||
|
export function formatListMessage(items: string[], maxItems: number = 10): string {
|
||||||
|
if (items.length <= maxItems) {
|
||||||
|
return items.join('\n')
|
||||||
|
}
|
||||||
|
return `${items.slice(0, maxItems).join('\n')}\n...及其他 ${items.length - maxItems} 项`
|
||||||
|
}
|
||||||
|
|
||||||
export default useAppStore
|
export default useAppStore
|
||||||
|
|||||||
Reference in New Issue
Block a user