Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
811361a1a3 | ||
|
|
ffbda4c618 | ||
|
|
348b02600d | ||
|
|
d004f8e9f8 | ||
|
|
3cbe9eef12 | ||
|
|
5b310d944b | ||
|
|
6e04f21b10 | ||
|
|
17fbd7d251 |
6
docs/releases/1.7.1.md
Normal file
6
docs/releases/1.7.1.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 1.7.1
|
||||||
|
|
||||||
|
## 问题修复
|
||||||
|
|
||||||
|
- 修复 MySQL 数据库下操作历史查询报错问题。
|
||||||
|
- 优化历史记录数据结构,支持按订单统计记录数量。
|
||||||
6
docs/releases/1.7.2.md
Normal file
6
docs/releases/1.7.2.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 1.7.2
|
||||||
|
|
||||||
|
## 问题修复
|
||||||
|
|
||||||
|
- 修复操作历史时间显示错误(时区转换导致时间快8小时)。
|
||||||
|
- 操作历史支持一键复制总排号和订单号。
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "erpauto",
|
"name": "erpauto",
|
||||||
"version": "1.7.0",
|
"version": "1.7.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "erpauto",
|
"name": "erpauto",
|
||||||
"version": "1.7.0",
|
"version": "1.7.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.929.0",
|
"@aws-sdk/client-s3": "^3.929.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "erpauto",
|
"name": "erpauto",
|
||||||
"version": "1.7.0",
|
"version": "1.7.2",
|
||||||
"description": "An Electron application with React and TypeScript",
|
"description": "An Electron application with React and TypeScript",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "example.com",
|
"author": "example.com",
|
||||||
|
|||||||
@@ -256,7 +256,14 @@ export function registerExtractorHandlers(): void {
|
|||||||
: result.errors.length > 0
|
: result.errors.length > 0
|
||||||
? 'failed'
|
? 'failed'
|
||||||
: 'success'
|
: 'success'
|
||||||
await historyDao.updateBatchStatus(batchId, status, result.recordCount)
|
|
||||||
|
// Write per-order record counts
|
||||||
|
for (const { orderNumber, recordCount } of result.orderRecordCounts) {
|
||||||
|
await historyDao.updateRecordStatus(batchId, orderNumber, status, undefined, recordCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update batch status without recordCount (per-order counts are set individually)
|
||||||
|
await historyDao.updateBatchStatus(batchId, status)
|
||||||
log.info('Operation history batch status updated', { batchId, status })
|
log.info('Operation history batch status updated', { batchId, status })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ import type {
|
|||||||
|
|
||||||
const log = createLogger('ExtractorOperationHistoryDAO')
|
const log = createLogger('ExtractorOperationHistoryDAO')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format datetime value from database to ISO string
|
||||||
|
* mssql driver returns Date objects in UTC format
|
||||||
|
*/
|
||||||
|
function formatDateTime(value: unknown): string {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value.toISOString()
|
||||||
|
}
|
||||||
|
return value ? String(value) : new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for ExtractorOperationHistory table
|
* Configuration for ExtractorOperationHistory table
|
||||||
*/
|
*/
|
||||||
@@ -160,43 +171,27 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
* Update the status of all records in a batch
|
* Update the status of all records in a batch
|
||||||
* @param batchId - Batch identifier
|
* @param batchId - Batch identifier
|
||||||
* @param status - New status (success, failed, partial)
|
* @param status - New status (success, failed, partial)
|
||||||
* @param recordCount - Total record count for the batch
|
|
||||||
* @returns Update result
|
* @returns Update result
|
||||||
*/
|
*/
|
||||||
async updateBatchStatus(
|
async updateBatchStatus(
|
||||||
batchId: string,
|
batchId: string,
|
||||||
status: string,
|
status: string
|
||||||
recordCount: number | null
|
|
||||||
): Promise<UpdateBatchStatusResult> {
|
): Promise<UpdateBatchStatusResult> {
|
||||||
try {
|
try {
|
||||||
const dbService = await this.getDatabaseService()
|
const dbService = await this.getDatabaseService()
|
||||||
const tableName = this.getTableName()
|
const tableName = this.getTableName()
|
||||||
const isSqlServer = dbService.type === 'sqlserver'
|
const isSqlServer = dbService.type === 'sqlserver'
|
||||||
|
|
||||||
const placeholder = isSqlServer ? '@p0' : '?'
|
const sqlString = `
|
||||||
let sqlString: string
|
|
||||||
let params: (string | number | null)[]
|
|
||||||
|
|
||||||
if (recordCount !== null) {
|
|
||||||
sqlString = `
|
|
||||||
UPDATE ${tableName}
|
UPDATE ${tableName}
|
||||||
SET Status = ${isSqlServer ? '@p0' : '?'},
|
SET Status = ${isSqlServer ? '@p0' : '?'}
|
||||||
RecordCount = ${isSqlServer ? '@p1' : '?'}
|
|
||||||
WHERE BatchId = ${isSqlServer ? '@p2' : '?'}
|
|
||||||
`
|
|
||||||
params = isSqlServer ? [status, recordCount, batchId] : [status, recordCount, batchId]
|
|
||||||
} else {
|
|
||||||
sqlString = `
|
|
||||||
UPDATE ${tableName}
|
|
||||||
SET Status = ${placeholder}
|
|
||||||
WHERE BatchId = ${isSqlServer ? '@p1' : '?'}
|
WHERE BatchId = ${isSqlServer ? '@p1' : '?'}
|
||||||
`
|
`
|
||||||
params = isSqlServer ? [status, batchId] : [status, batchId]
|
const params = [status, batchId]
|
||||||
}
|
|
||||||
|
|
||||||
await dbService.query(sqlString, params)
|
await dbService.query(sqlString, params)
|
||||||
|
|
||||||
log.info('Batch status updated', { batchId, status, recordCount })
|
log.info('Batch status updated', { batchId, status })
|
||||||
return { success: true, updatedCount: 1 }
|
return { success: true, updatedCount: 1 }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Update batch status error', {
|
log.error('Update batch status error', {
|
||||||
@@ -208,33 +203,51 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a single record's status and error message
|
* Update a single record's status, error message, and optional record count
|
||||||
* @param batchId - Batch identifier
|
* @param batchId - Batch identifier
|
||||||
* @param orderNumber - Order number
|
* @param orderNumber - Order number
|
||||||
* @param status - New status
|
* @param status - New status
|
||||||
* @param errorMessage - Optional error message
|
* @param errorMessage - Optional error message
|
||||||
|
* @param recordCount - Optional per-order record count
|
||||||
* @returns True if successful
|
* @returns True if successful
|
||||||
*/
|
*/
|
||||||
async updateRecordStatus(
|
async updateRecordStatus(
|
||||||
batchId: string,
|
batchId: string,
|
||||||
orderNumber: string,
|
orderNumber: string,
|
||||||
status: string,
|
status: string,
|
||||||
errorMessage?: string
|
errorMessage?: string,
|
||||||
|
recordCount?: number
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const dbService = await this.getDatabaseService()
|
const dbService = await this.getDatabaseService()
|
||||||
const tableName = this.getTableName()
|
const tableName = this.getTableName()
|
||||||
const isSqlServer = dbService.type === 'sqlserver'
|
const isSqlServer = dbService.type === 'sqlserver'
|
||||||
|
|
||||||
const sqlString = `
|
let sqlString: string
|
||||||
|
let params: (string | number | null)[]
|
||||||
|
|
||||||
|
if (recordCount !== undefined) {
|
||||||
|
sqlString = `
|
||||||
|
UPDATE ${tableName}
|
||||||
|
SET Status = ${isSqlServer ? '@p0' : '?'},
|
||||||
|
ErrorMessage = ${isSqlServer ? '@p1' : '?'},
|
||||||
|
RecordCount = ${isSqlServer ? '@p2' : '?'}
|
||||||
|
WHERE BatchId = ${isSqlServer ? '@p3' : '?'}
|
||||||
|
AND OrderNumber = ${isSqlServer ? '@p4' : '?'}
|
||||||
|
`
|
||||||
|
params = [status, errorMessage || null, recordCount, batchId, orderNumber]
|
||||||
|
} else {
|
||||||
|
sqlString = `
|
||||||
UPDATE ${tableName}
|
UPDATE ${tableName}
|
||||||
SET Status = ${isSqlServer ? '@p0' : '?'},
|
SET Status = ${isSqlServer ? '@p0' : '?'},
|
||||||
ErrorMessage = ${isSqlServer ? '@p1' : '?'}
|
ErrorMessage = ${isSqlServer ? '@p1' : '?'}
|
||||||
WHERE BatchId = ${isSqlServer ? '@p2' : '?'}
|
WHERE BatchId = ${isSqlServer ? '@p2' : '?'}
|
||||||
AND OrderNumber = ${isSqlServer ? '@p3' : '?'}
|
AND OrderNumber = ${isSqlServer ? '@p3' : '?'}
|
||||||
`
|
`
|
||||||
|
params = [status, errorMessage || null, batchId, orderNumber]
|
||||||
|
}
|
||||||
|
|
||||||
await dbService.query(sqlString, [status, errorMessage || null, batchId, orderNumber])
|
await dbService.query(sqlString, params)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -288,31 +301,30 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
`
|
`
|
||||||
|
|
||||||
if (options?.limit) {
|
if (options?.limit) {
|
||||||
// Add pagination - track current param count before adding new params
|
const safeLimit = Math.floor(options.limit)
|
||||||
const offsetIndex = params.length
|
const safeOffset = options.offset !== undefined ? Math.floor(options.offset) : undefined
|
||||||
const limitIndex = params.length + 1
|
|
||||||
|
|
||||||
if (options.offset !== undefined) {
|
|
||||||
params.push(options.offset)
|
|
||||||
}
|
|
||||||
params.push(options.limit)
|
|
||||||
|
|
||||||
if (isSqlServer) {
|
if (isSqlServer) {
|
||||||
if (options.offset !== undefined) {
|
// SQL Server: use parameterized OFFSET/FETCH
|
||||||
sqlString += ` OFFSET @p${offsetIndex} ROWS FETCH NEXT @p${limitIndex} ROWS ONLY`
|
const offsetIndex = params.length
|
||||||
|
if (safeOffset !== undefined) {
|
||||||
|
params.push(safeOffset)
|
||||||
|
}
|
||||||
|
params.push(safeLimit)
|
||||||
|
|
||||||
|
if (safeOffset !== undefined) {
|
||||||
|
sqlString += ` OFFSET @p${offsetIndex} ROWS FETCH NEXT @p${offsetIndex + 1} ROWS ONLY`
|
||||||
} else {
|
} else {
|
||||||
// When no offset, use 0 for offset and next index for limit
|
|
||||||
sqlString += ` OFFSET 0 ROWS FETCH NEXT @p${offsetIndex} ROWS ONLY`
|
sqlString += ` OFFSET 0 ROWS FETCH NEXT @p${offsetIndex} ROWS ONLY`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (options.offset !== undefined) {
|
// MySQL: embed validated integer values directly.
|
||||||
sqlString += ` LIMIT ?`
|
// connection.execute() uses binary protocol prepared statements,
|
||||||
// For MySQL with offset, we need to modify the query
|
// which do not reliably support ? placeholders in LIMIT/OFFSET clauses.
|
||||||
// Replace LIMIT with OFFSET LIMIT
|
if (safeOffset !== undefined) {
|
||||||
const parts = sqlString.split(' LIMIT ?')
|
sqlString += ` LIMIT ${safeLimit} OFFSET ${safeOffset}`
|
||||||
sqlString = parts[0] + ` OFFSET ? LIMIT ?` + (parts[1] || '')
|
|
||||||
} else {
|
} else {
|
||||||
sqlString += ` LIMIT ?`
|
sqlString += ` LIMIT ${safeLimit}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,9 +335,7 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
batchId: row.BatchId as string,
|
batchId: row.BatchId as string,
|
||||||
userId: row.UserId as number,
|
userId: row.UserId as number,
|
||||||
username: row.Username as string,
|
username: row.Username as string,
|
||||||
operationTime: row.OperationTime
|
operationTime: formatDateTime(row.OperationTime),
|
||||||
? new Date(row.OperationTime as string).toISOString()
|
|
||||||
: new Date().toISOString(),
|
|
||||||
status: row.Status as string,
|
status: row.Status as string,
|
||||||
totalOrders: row.TotalOrders as number,
|
totalOrders: row.TotalOrders as number,
|
||||||
totalRecords: (row.TotalRecords as number) || 0,
|
totalRecords: (row.TotalRecords as number) || 0,
|
||||||
@@ -431,9 +441,7 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
batchId: row.BatchId as string,
|
batchId: row.BatchId as string,
|
||||||
userId: row.UserId as number,
|
userId: row.UserId as number,
|
||||||
username: row.Username as string,
|
username: row.Username as string,
|
||||||
operationTime: row.OperationTime
|
operationTime: formatDateTime(row.OperationTime),
|
||||||
? new Date(row.OperationTime as string).toISOString()
|
|
||||||
: new Date().toISOString(),
|
|
||||||
status: row.Status as string,
|
status: row.Status as string,
|
||||||
totalOrders: row.TotalOrders as number,
|
totalOrders: row.TotalOrders as number,
|
||||||
totalRecords: (row.TotalRecords as number) || 0,
|
totalRecords: (row.TotalRecords as number) || 0,
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ export class ExtractorService {
|
|||||||
downloadedFiles: [],
|
downloadedFiles: [],
|
||||||
mergedFile: null,
|
mergedFile: null,
|
||||||
recordCount: 0,
|
recordCount: 0,
|
||||||
errors: []
|
errors: [],
|
||||||
|
orderRecordCounts: []
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -79,6 +80,7 @@ export class ExtractorService {
|
|||||||
const mergeResult = await this.mergeFiles(result.downloadedFiles)
|
const mergeResult = await this.mergeFiles(result.downloadedFiles)
|
||||||
result.mergedFile = mergeResult.mergedFile
|
result.mergedFile = mergeResult.mergedFile
|
||||||
result.recordCount = mergeResult.recordCount
|
result.recordCount = mergeResult.recordCount
|
||||||
|
result.orderRecordCounts = mergeResult.orderRecordCounts
|
||||||
|
|
||||||
// Add merge error to result if any
|
// Add merge error to result if any
|
||||||
if (mergeResult.error) {
|
if (mergeResult.error) {
|
||||||
@@ -123,9 +125,14 @@ export class ExtractorService {
|
|||||||
*/
|
*/
|
||||||
private async mergeFiles(
|
private async mergeFiles(
|
||||||
filePaths: string[]
|
filePaths: string[]
|
||||||
): Promise<{ mergedFile: string | null; recordCount: number; error?: string }> {
|
): Promise<{
|
||||||
|
mergedFile: string | null
|
||||||
|
recordCount: number
|
||||||
|
error?: string
|
||||||
|
orderRecordCounts: Array<{ orderNumber: string; recordCount: number }>
|
||||||
|
}> {
|
||||||
if (filePaths.length === 0) {
|
if (filePaths.length === 0) {
|
||||||
return { mergedFile: null, recordCount: 0 }
|
return { mergedFile: null, recordCount: 0, orderRecordCounts: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Starting merge', { fileCount: filePaths.length })
|
log.info('Starting merge', { fileCount: filePaths.length })
|
||||||
@@ -154,15 +161,21 @@ export class ExtractorService {
|
|||||||
|
|
||||||
// Calculate total record count (total material rows)
|
// Calculate total record count (total material rows)
|
||||||
let recordCount = 0
|
let recordCount = 0
|
||||||
|
const orderRecordCounts: Array<{ orderNumber: string; recordCount: number }> = []
|
||||||
for (const order of allOrders) {
|
for (const order of allOrders) {
|
||||||
recordCount += order.materials.length
|
const count = order.materials.length
|
||||||
|
recordCount += count
|
||||||
|
orderRecordCounts.push({
|
||||||
|
orderNumber: order.orderInfo.productionOrder || '',
|
||||||
|
recordCount: count
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Merge summary', { orderCount: allOrders.length, recordCount })
|
log.info('Merge summary', { orderCount: allOrders.length, recordCount })
|
||||||
|
|
||||||
if (recordCount === 0) {
|
if (recordCount === 0) {
|
||||||
log.warn('No records found in any downloaded files')
|
log.warn('No records found in any downloaded files')
|
||||||
return { mergedFile: null, recordCount: 0 }
|
return { mergedFile: null, recordCount: 0, orderRecordCounts }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate output filename with timestamp
|
// Generate output filename with timestamp
|
||||||
@@ -178,13 +191,13 @@ export class ExtractorService {
|
|||||||
log.info('Saving merged file', { outputPath })
|
log.info('Saving merged file', { outputPath })
|
||||||
await this.saveMergedOrders(allOrders, outputPath)
|
await this.saveMergedOrders(allOrders, outputPath)
|
||||||
log.info('Merged file saved successfully', { recordCount })
|
log.info('Merged file saved successfully', { recordCount })
|
||||||
return { mergedFile: outputPath, recordCount }
|
return { mergedFile: outputPath, recordCount, orderRecordCounts }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||||
const errorStack = error instanceof Error ? error.stack : ''
|
const errorStack = error instanceof Error ? error.stack : ''
|
||||||
log.error('Failed to save merged file', { error: errorMsg, stack: errorStack })
|
log.error('Failed to save merged file', { error: errorMsg, stack: errorStack })
|
||||||
// Return parsed record count and error info even if save fails
|
// Return parsed record count and error info even if save fails
|
||||||
return { mergedFile: null, recordCount, error: `保存合并文件失败:${errorMsg}` }
|
return { mergedFile: null, recordCount, orderRecordCounts, error: `保存合并文件失败:${errorMsg}` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export interface ExtractorResult {
|
|||||||
errors: string[]
|
errors: string[]
|
||||||
/** Database import result (only populated if mergedFile was created) */
|
/** Database import result (only populated if mergedFile was created) */
|
||||||
importResult?: ImportResult
|
importResult?: ImportResult
|
||||||
|
/** Per-order material row counts */
|
||||||
|
orderRecordCounts: Array<{ orderNumber: string; recordCount: number }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderInfo {
|
export interface OrderInfo {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Admin users see all users' records, regular users see only their own.
|
* Admin users see all users' records, regular users see only their own.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import { Modal } from './ui/Modal'
|
import { Modal } from './ui/Modal'
|
||||||
import {
|
import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -14,35 +14,15 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
Clock
|
Clock,
|
||||||
|
Copy
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { UserInfo } from './UserSelectionDialog'
|
import type { UserInfo } from './UserSelectionDialog'
|
||||||
|
import type {
|
||||||
// Local type definitions matching the backend types
|
BatchStats,
|
||||||
interface BatchStats {
|
OperationHistoryRecord
|
||||||
batchId: string
|
} from '../../../main/types/operation-history.types'
|
||||||
userId: number
|
import { showSuccess, showError, showWarning } from '../stores/useAppStore'
|
||||||
username: string
|
|
||||||
operationTime: string
|
|
||||||
status: string
|
|
||||||
totalOrders: number
|
|
||||||
totalRecords: number
|
|
||||||
successCount: number
|
|
||||||
failedCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OperationHistoryRecord {
|
|
||||||
id?: number
|
|
||||||
batchId: string
|
|
||||||
userId: number
|
|
||||||
username: string
|
|
||||||
productionId: string | null
|
|
||||||
orderNumber: string
|
|
||||||
operationTime: Date
|
|
||||||
status: string
|
|
||||||
recordCount: number | null
|
|
||||||
errorMessage: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExtractorOperationHistoryModalProps {
|
interface ExtractorOperationHistoryModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -71,6 +51,24 @@ const statusIcons: Record<string, React.ReactNode> = {
|
|||||||
pending: <Clock size={16} className="text-gray-500" />
|
pending: <Clock size={16} className="text-gray-500" />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatDateTime = (dateStr: string) => {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
|
||||||
|
// Check if the date is valid
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return dateStr // Return original if invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use UTC methods to display the time as stored in database (without timezone conversion)
|
||||||
|
const year = date.getUTCFullYear()
|
||||||
|
const month = String(date.getUTCMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getUTCDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getUTCHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getUTCMinutes()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryModalProps> = ({
|
export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryModalProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -85,14 +83,7 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
|
|
||||||
const isAdmin = user?.userType === 'Admin'
|
const isAdmin = user?.userType === 'Admin'
|
||||||
|
|
||||||
// Fetch batches when modal opens
|
const fetchBatches = useCallback(async () => {
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
void fetchBatches()
|
|
||||||
}
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
const fetchBatches = async () => {
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
@@ -107,9 +98,10 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const fetchBatchDetails = async (batchId: string) => {
|
const fetchBatchDetails = useCallback(
|
||||||
|
async (batchId: string) => {
|
||||||
// If already loaded, don't fetch again
|
// If already loaded, don't fetch again
|
||||||
if (batchDetails.has(batchId)) {
|
if (batchDetails.has(batchId)) {
|
||||||
return
|
return
|
||||||
@@ -123,7 +115,16 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch batch details:', err)
|
console.error('Failed to fetch batch details:', err)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[batchDetails]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch batches when modal opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
void fetchBatches()
|
||||||
}
|
}
|
||||||
|
}, [isOpen, fetchBatches])
|
||||||
|
|
||||||
const toggleBatchExpansion = (batchId: string) => {
|
const toggleBatchExpansion = (batchId: string) => {
|
||||||
setExpandedBatches((prev) => {
|
setExpandedBatches((prev) => {
|
||||||
@@ -175,15 +176,24 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDateTime = (dateStr: string) => {
|
const handleCopyColumn = async (field: 'productionId' | 'orderNumber', batchId: string) => {
|
||||||
const date = new Date(dateStr)
|
const details = batchDetails.get(batchId) || []
|
||||||
return date.toLocaleString('zh-CN', {
|
const values = details
|
||||||
year: 'numeric',
|
.map((d) => (field === 'productionId' ? d.productionId : d.orderNumber))
|
||||||
month: '2-digit',
|
.filter(Boolean) // 移除空值
|
||||||
day: '2-digit',
|
.join('\n') // 使用换行符分隔
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
if (!values) {
|
||||||
})
|
showWarning('没有可复制的数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(values)
|
||||||
|
showSuccess(`已复制 ${values.split('\n').length} 条数据`)
|
||||||
|
} catch {
|
||||||
|
showError('复制失败,请手动复制')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
@@ -320,10 +330,38 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
总排号
|
总排号
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-gray-200 rounded transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
void handleCopyColumn('productionId', batch.batchId)
|
||||||
|
}
|
||||||
|
title="复制所有总排号"
|
||||||
|
>
|
||||||
|
<Copy
|
||||||
|
size={14}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
订单号
|
订单号
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-gray-200 rounded transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
void handleCopyColumn('orderNumber', batch.batchId)
|
||||||
|
}
|
||||||
|
title="复制所有订单号"
|
||||||
|
>
|
||||||
|
<Copy
|
||||||
|
size={14}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
||||||
状态
|
状态
|
||||||
|
|||||||
@@ -90,13 +90,13 @@ const ExtractorPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showHistoryModal && (
|
{showHistoryModal ? (
|
||||||
<ExtractorOperationHistoryModal
|
<ExtractorOperationHistoryModal
|
||||||
isOpen={showHistoryModal}
|
isOpen={showHistoryModal}
|
||||||
onClose={() => setShowHistoryModal(false)}
|
onClose={() => setShowHistoryModal(false)}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{!isRunning && isComplete && (
|
{!isRunning && isComplete && (
|
||||||
<div className="bg-green-50 rounded-xl p-8 flex items-center justify-center gap-4 shadow-md">
|
<div className="bg-green-50 rounded-xl p-8 flex items-center justify-center gap-4 shadow-md">
|
||||||
|
|||||||
Reference in New Issue
Block a user