fix: resolve lint and typecheck issues

This commit is contained in:
Misaka
2026-03-21 09:33:07 +08:00
parent 2fba07fd8f
commit 2b4a09dabe
26 changed files with 356 additions and 336 deletions

View File

@@ -8,7 +8,7 @@
* - Display main content after successful authentication
*/
import React, { useState, useEffect } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import {
LayoutDashboard,
Download,
@@ -77,54 +77,21 @@ function App(): React.JSX.Element {
setTimeout(() => setErrorMessage(''), 3000)
}
// Initialize authentication on mount
useEffect(() => {
logger.info('=== Initializing auth... ===')
initializeAuth()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
const unsubscribe = window.electron.update.onStatusChanged((status) => {
setUpdateStatus(status)
if (status.phase === 'available' || status.phase === 'downloaded' || status.phase === 'idle') {
void refreshUpdateCatalog()
}
})
void refreshUpdateState()
return unsubscribe
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (isAuthenticated) {
void refreshUpdateState()
void refreshUpdateCatalog()
return
}
setUpdateStatus(null)
setUpdateCatalog(null)
setShowUpdateDialog(false)
}, [isAuthenticated])
const refreshUpdateState = async () => {
const refreshUpdateState = useCallback(async () => {
const result = await window.electron.update.getStatus()
if (result.success && result.data) {
setUpdateStatus(result.data)
}
}
}, [])
const refreshUpdateCatalog = async () => {
const refreshUpdateCatalog = useCallback(async () => {
const result = await window.electron.update.getCatalog()
if (result.success && result.data) {
setUpdateCatalog(result.data)
}
}
}, [])
const initializeAuth = async () => {
const initializeAuth = useCallback(async () => {
logger.info('=== Starting initializeAuth ===')
try {
// Get computer name
@@ -174,7 +141,42 @@ function App(): React.JSX.Element {
setIsAuthenticating(false)
}
logger.info('=== Auth initialization complete ===')
}
}, [logger])
// Initialize authentication on mount
useEffect(() => {
logger.info('=== Initializing auth... ===')
void initializeAuth()
}, [initializeAuth, logger])
useEffect(() => {
const unsubscribe = window.electron.update.onStatusChanged((status) => {
setUpdateStatus(status)
if (
status.phase === 'available' ||
status.phase === 'downloaded' ||
status.phase === 'idle'
) {
void refreshUpdateCatalog()
}
})
void refreshUpdateState()
return unsubscribe
}, [refreshUpdateCatalog, refreshUpdateState])
useEffect(() => {
if (isAuthenticated) {
void refreshUpdateState()
void refreshUpdateCatalog()
return
}
setUpdateStatus(null)
setUpdateCatalog(null)
setShowUpdateDialog(false)
}, [isAuthenticated, refreshUpdateCatalog, refreshUpdateState])
// Handle login dialog submit
const handleLogin = async (username: string, password: string): Promise<boolean> => {
@@ -266,7 +268,9 @@ function App(): React.JSX.Element {
}
if (updateStatus?.phase !== 'downloaded') {
const downloadResult = await window.electron.update.downloadRelease(updateCatalog.recommendedRelease)
const downloadResult = await window.electron.update.downloadRelease(
updateCatalog.recommendedRelease
)
if (!downloadResult.success) {
showError(downloadResult.error || '下载更新失败')
return
@@ -554,9 +558,7 @@ function App(): React.JSX.Element {
catalog={updateCatalog}
onClose={() => setShowUpdateDialog(false)}
onInstallUserRelease={handleInstallUserRelease}
onDownloadAndInstallAdminRelease={async (release) =>
handleAdminDownloadAndInstall(release)
}
onDownloadAndInstallAdminRelease={async (release) => handleAdminDownloadAndInstall(release)}
onRefreshCatalog={async () => {
await window.electron.update.checkNow()
await refreshUpdateCatalog()

View File

@@ -10,8 +10,8 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'
import { Plus, Trash2, Save, RotateCcw, Users } from 'lucide-react'
import { Modal } from './ui/Modal'
import { showSuccess, showError, showInfo } from '../stores/useAppStore'
import { useConfirmDialog } from './ui/ConfirmDialog'
import { ConfirmDialog } from './ui/ConfirmDialog'
import { useConfirmDialog } from './ui/useConfirmDialog'
interface MaterialTypeRecord {
id?: number
@@ -51,6 +51,7 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
const tableRef = useRef<HTMLTableElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const selectRef = useRef<HTMLSelectElement>(null)
// Confirmation dialog hook
const { confirm, dialog: confirmDialog } = useConfirmDialog()
@@ -60,22 +61,7 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
(r) => r.state === 'new' || r.state === 'modified' || r.state === 'deleted'
).length
// Load data when dialog opens
useEffect(() => {
if (isOpen) {
loadData()
}
}, [isOpen, isAdmin, currentUsername])
// Focus input when editing starts
useEffect(() => {
if (editingCell && inputRef.current) {
inputRef.current.focus()
inputRef.current.select()
}
}, [editingCell])
const loadData = async () => {
const loadData = useCallback(async () => {
setLoading(true)
try {
// Load managers list
@@ -114,7 +100,25 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
} finally {
setLoading(false)
}
}
}, [currentUsername, isAdmin])
// Load data when dialog opens
useEffect(() => {
if (isOpen) {
void loadData()
}
}, [isOpen, loadData])
// Focus input when editing starts
useEffect(() => {
const activeElement = inputRef.current ?? selectRef.current
if (editingCell && activeElement) {
activeElement.focus()
if (activeElement instanceof HTMLInputElement) {
activeElement.select()
}
}
}, [editingCell])
// Filter rows by selected managers (admin only)
const filteredRows = React.useMemo(() => {
@@ -296,7 +300,7 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
variant: 'warning'
})
if (confirmed) {
loadData()
void loadData()
}
}
@@ -443,7 +447,7 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
{filteredRows.filter((r) => r.state !== 'deleted').length === 0 ? (
<tr>
<td colSpan={2} className="px-4 py-8 text-center text-slate-400">
"新增"
&quot;&quot;
</td>
</tr>
) : (
@@ -495,7 +499,7 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
{isEditingManager ? (
isAdmin ? (
<select
ref={inputRef as any}
ref={selectRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={saveEdit}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState, useMemo } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { X, FileText, Loader2, ChevronDown } from 'lucide-react'
import { Combobox, Transition } from '@headlessui/react'
import ReactMarkdown from 'react-markdown'
@@ -38,19 +38,7 @@ export const ReportViewerDialog: React.FC<ReportViewerDialogProps> = ({
const [error, setError] = useState<string | null>(null)
const [query, setQuery] = useState('')
useEffect(() => {
if (isOpen) {
loadReports()
} else {
// Reset state when closed
setReports([])
setSelectedReport(null)
setReportContent('')
setError(null)
}
}, [isOpen, isAdmin, currentUsername])
const loadReports = async () => {
const loadReports = useCallback(async () => {
setIsLoadingList(true)
setError(null)
try {
@@ -66,12 +54,24 @@ export const ReportViewerDialog: React.FC<ReportViewerDialogProps> = ({
} else {
setError(result.error || '无法获取报告列表')
}
} catch (err) {
} catch {
setError('获取报告列表时发生错误')
} finally {
setIsLoadingList(false)
}
}
}, [currentUsername, isAdmin])
useEffect(() => {
if (isOpen) {
void loadReports()
} else {
// Reset state when closed
setReports([])
setSelectedReport(null)
setReportContent('')
setError(null)
}
}, [isOpen, loadReports])
const handleReportChange = async (report: ReportMetadata | null) => {
setSelectedReport(report)
@@ -91,7 +91,7 @@ export const ReportViewerDialog: React.FC<ReportViewerDialogProps> = ({
setError(result.error || '无法获取报告内容')
setReportContent('')
}
} catch (err) {
} catch {
setError('获取报告内容时发生错误')
setReportContent('')
} finally {

View File

@@ -86,7 +86,9 @@ export default function UpdateDialog({
}, [isOpen, selectedRelease])
const isBusy =
status?.phase === 'downloading' || status?.phase === 'installing' || status?.phase === 'checking'
status?.phase === 'downloading' ||
status?.phase === 'installing' ||
status?.phase === 'checking'
const renderReleaseList = (title: string, releases: UpdateRelease[]) => {
if (releases.length === 0) {
@@ -233,7 +235,9 @@ export default function UpdateDialog({
{userType === 'Admin' ? (
<button
onClick={() =>
selectedRelease ? void onDownloadAndInstallAdminRelease(selectedRelease) : undefined
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}

View File

@@ -7,7 +7,7 @@
* - Return selected user info
*/
import React, { useState, useEffect, useRef } from 'react'
import React, { useState, useRef } from 'react'
import { Modal } from './ui/Modal'
export interface UserInfo {
@@ -37,13 +37,6 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
const [selectedUserId, setSelectedUserId] = useState<number | null>(null)
const dialogRef = useRef<HTMLDivElement>(null)
// Reset selection when dialog opens
useEffect(() => {
if (isOpen) {
setSelectedUserId(null)
}
}, [isOpen])
const handleConfirm = () => {
if (selectedUserId === null) {
return
@@ -51,14 +44,21 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
const selectedUser = users.find((u) => u.id === selectedUserId)
if (selectedUser) {
setSelectedUserId(null)
onSelectUser(selectedUser)
}
}
const handleDoubleClick = (user: UserInfo) => {
setSelectedUserId(null)
onSelectUser(user)
}
const handleCancel = () => {
setSelectedUserId(null)
onCancel()
}
const userTypeStyles: Record<string, string> = {
Admin: 'bg-amber-50 text-amber-600',
User: 'bg-blue-50 text-blue-600',
@@ -70,7 +70,7 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
return (
<Modal
isOpen={isOpen}
onClose={onCancel}
onClose={handleCancel}
title="选择用户"
size="md"
triggerRef={triggerRef}
@@ -129,7 +129,7 @@ export const UserSelectionDialog: React.FC<UserSelectionDialogProps> = ({
</button>
<button
className="px-6 py-2 rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium transition-colors"
onClick={onCancel}
onClick={handleCancel}
>
</button>

View File

@@ -5,7 +5,7 @@
* Extends the Modal component with consistent styling and behavior.
*/
import React, { useState, useCallback, useEffect } from 'react'
import React, { useCallback, useEffect } from 'react'
import { AlertTriangle, Info, AlertCircle } from 'lucide-react'
import { Modal } from './Modal'
import { Button } from './Button'
@@ -115,54 +115,4 @@ export function ConfirmDialog({
)
}
/**
* 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

View File

@@ -0,0 +1,60 @@
import { useCallback, useState } from 'react'
import type { ConfirmDialogProps } from './ConfirmDialog'
type ConfirmDialogConfig = Omit<ConfirmDialogProps, 'isOpen' | 'onConfirm' | 'onCancel'> & {
resolve: (value: boolean) => void
}
export function useConfirmDialog(): {
confirm: (
options: Omit<ConfirmDialogProps, 'isOpen' | 'onConfirm' | 'onCancel'>
) => Promise<boolean>
dialog:
| (Omit<ConfirmDialogProps, 'isOpen'> & {
isOpen: true
})
| null
} {
const [config, setConfig] = useState<ConfirmDialogConfig | 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) {
return
}
config.resolve(true)
setConfig(null)
}, [config])
const handleCancel = useCallback(() => {
if (!config) {
return
}
config.resolve(false)
setConfig(null)
}, [config])
const dialog = config
? {
...config,
isOpen: true as const,
onConfirm: handleConfirm,
onCancel: handleCancel
}
: null
return { confirm, dialog }
}

View File

@@ -7,6 +7,21 @@ import type {
} from './types'
import type { CleanerExportItem, MaterialBatchChange } from './helpers'
interface CleanerDataPayload {
success?: boolean
orderNumbers?: string[]
materialCodes?: string[]
}
interface CleanerRunPayload {
ordersProcessed: number
materialsDeleted: number
materialsSkipped: number
errors: string[]
retriedOrders: number
successfulRetries: number
}
export async function initializeCleanerPage(): Promise<CleanerInitializationResult> {
const adminResult = await window.electron.auth.isAdmin()
const userResult = await window.electron.auth.getCurrentUser()
@@ -105,7 +120,9 @@ export async function runCleanerExecution(params: {
processConcurrency: number
}): Promise<CleanerReportData> {
const cleanerDataResult = await window.electron.validation.getCleanerData()
const cleanerData = cleanerDataResult.success ? (cleanerDataResult.data as any) : null
const cleanerData = cleanerDataResult.success
? (cleanerDataResult.data as CleanerDataPayload | null)
: null
if (!cleanerDataResult.success || cleanerData?.success === false) {
throw new Error(cleanerDataResult.error || '获取清理数据失败')
@@ -130,7 +147,7 @@ export async function runCleanerExecution(params: {
processConcurrency: params.processConcurrency
})
const cleanerRunData = response.success ? (response.data as any) : null
const cleanerRunData = response.success ? (response.data as CleanerRunPayload | null) : null
if (!response.success || !cleanerRunData) {
throw new Error(response.error || '清理失败')
}

View File

@@ -1,6 +1,11 @@
import { useEffect } from 'react'
import type { LogLevel } from '../stores/extractorStore'
import { useExtractorStore } from '../stores/extractorStore'
function isLogLevel(value: string): value is LogLevel {
return ['info', 'success', 'warning', 'error', 'system'].includes(value)
}
export function useExtractor() {
const {
isRunning,
@@ -30,7 +35,7 @@ export function useExtractor() {
})
const unsubscribeLog = window.electron.extractor.onLog((data) => {
addLog(data.level as any, data.message)
addLog(isLogLevel(data.level) ? data.level : 'info', data.message)
})
return () => {

View File

@@ -1,4 +1,4 @@
import { useRef, useCallback } from 'react'
import { useCallback } from 'react'
import type { LogLevel } from '../../../shared/ipc-channels'
/**
@@ -50,26 +50,24 @@ export interface RendererLogger {
* ```
*/
export function useLogger(context: string): RendererLogger {
// Use ref to store context to avoid recreating logger on re-renders
const contextRef = useRef(context)
contextRef.current = context
// Create stable logger instance using useCallback
const logger = useCallback((level: LogLevel, message: string, meta?: Record<string, unknown>) => {
// Check if window.electron is available (safety check)
if (typeof window !== 'undefined' && window.electron?.logger?.log) {
window.electron.logger.log(level, message, {
...meta,
context: contextRef.current
})
} else {
// Fallback to console in development if IPC not available
if (process.env.NODE_ENV === 'development') {
const consoleMethod = console[level] || console.log
consoleMethod(`[${contextRef.current}] ${message}`, meta || '')
const logger = useCallback(
(level: LogLevel, message: string, meta?: Record<string, unknown>) => {
// Check if window.electron is available (safety check)
if (typeof window !== 'undefined' && window.electron?.logger?.log) {
window.electron.logger.log(level, message, {
...meta,
context
})
} else {
// Fallback to console in development if IPC not available
if (process.env.NODE_ENV === 'development') {
const consoleMethod = console[level] || console.log
consoleMethod(`[${context}] ${message}`, meta || '')
}
}
}
}, [])
},
[context]
)
// Return memoized logger methods
return {

View File

@@ -38,7 +38,7 @@ const SettingsPage: React.FC = () => {
showError(response.error || '加载 ERP 配置失败')
}
setIsModified(false)
} catch (error) {
} catch {
showError('加载 ERP 配置失败')
} finally {
setIsLoading(false)
@@ -64,7 +64,7 @@ const SettingsPage: React.FC = () => {
} else {
showError(result.error || saveData?.error || '保存失败')
}
} catch (error) {
} catch {
showError('保存配置时发生错误')
}
}