Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63a292c5f9 | ||
|
|
51f8e0a6e7 | ||
|
|
c8ab58d390 | ||
|
|
811361a1a3 | ||
|
|
ffbda4c618 | ||
|
|
348b02600d | ||
|
|
d004f8e9f8 |
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小时)。
|
||||||
|
- 操作历史支持一键复制总排号和订单号。
|
||||||
12
docs/releases/1.8.0.md
Normal file
12
docs/releases/1.8.0.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 1.8.0
|
||||||
|
|
||||||
|
## 权限控制
|
||||||
|
|
||||||
|
- 操作历史删除按钮仅对管理员可见,普通用户无法删除历史记录。
|
||||||
|
- 修复用户状态传递问题,确保权限判断正确生效。
|
||||||
|
|
||||||
|
## 界面与交互
|
||||||
|
|
||||||
|
- 管理员可使用多选标签(Chip)按用户筛选操作历史。
|
||||||
|
- 支持同时选择多个用户查看记录,点击标签即可切换选中状态。
|
||||||
|
- 添加"清空筛选"按钮,一键恢复显示所有用户记录。
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "erpauto",
|
"name": "erpauto",
|
||||||
"version": "1.7.1",
|
"version": "1.8.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "erpauto",
|
"name": "erpauto",
|
||||||
"version": "1.7.1",
|
"version": "1.8.0",
|
||||||
"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.1",
|
"version": "1.8.0",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
@@ -162,10 +173,7 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
* @param status - New status (success, failed, partial)
|
* @param status - New status (success, failed, partial)
|
||||||
* @returns Update result
|
* @returns Update result
|
||||||
*/
|
*/
|
||||||
async updateBatchStatus(
|
async updateBatchStatus(batchId: string, status: string): Promise<UpdateBatchStatusResult> {
|
||||||
batchId: string,
|
|
||||||
status: string
|
|
||||||
): Promise<UpdateBatchStatusResult> {
|
|
||||||
try {
|
try {
|
||||||
const dbService = await this.getDatabaseService()
|
const dbService = await this.getDatabaseService()
|
||||||
const tableName = this.getTableName()
|
const tableName = this.getTableName()
|
||||||
@@ -254,7 +262,7 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
/**
|
/**
|
||||||
* Get batch statistics with optional user filtering
|
* Get batch statistics with optional user filtering
|
||||||
* @param userId - Optional user ID for filtering (Admin gets all, User gets own)
|
* @param userId - Optional user ID for filtering (Admin gets all, User gets own)
|
||||||
* @param options - Query options (limit, offset)
|
* @param options - Query options (limit, offset, usernames)
|
||||||
* @returns Array of batch statistics
|
* @returns Array of batch statistics
|
||||||
*/
|
*/
|
||||||
async getBatches(userId?: number, options?: GetBatchesOptions): Promise<BatchStats[]> {
|
async getBatches(userId?: number, options?: GetBatchesOptions): Promise<BatchStats[]> {
|
||||||
@@ -282,6 +290,11 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
if (userId !== undefined) {
|
if (userId !== undefined) {
|
||||||
sqlString += isSqlServer ? ` WHERE UserId = @p0 ` : ` WHERE UserId = ? `
|
sqlString += isSqlServer ? ` WHERE UserId = @p0 ` : ` WHERE UserId = ? `
|
||||||
params.push(userId)
|
params.push(userId)
|
||||||
|
} else if (options?.usernames && options.usernames.length > 0) {
|
||||||
|
// Admin user filtering by multiple usernames using IN clause
|
||||||
|
const placeholders = this.buildPlaceholders(options.usernames.length, isSqlServer)
|
||||||
|
sqlString += ` WHERE Username IN (${placeholders}) `
|
||||||
|
params.push(...options.usernames)
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlString += `
|
sqlString += `
|
||||||
@@ -324,9 +337,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,
|
||||||
@@ -432,9 +443,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,
|
||||||
@@ -566,9 +575,10 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
/**
|
/**
|
||||||
* Count total batches with optional user filtering
|
* Count total batches with optional user filtering
|
||||||
* @param userId - Optional user ID for filtering
|
* @param userId - Optional user ID for filtering
|
||||||
|
* @param usernames - Optional usernames filter for Admin users
|
||||||
* @returns Total number of batches
|
* @returns Total number of batches
|
||||||
*/
|
*/
|
||||||
async countBatches(userId?: number): Promise<number> {
|
async countBatches(userId?: number, usernames?: string[]): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const dbService = await this.getDatabaseService()
|
const dbService = await this.getDatabaseService()
|
||||||
const tableName = this.getTableName()
|
const tableName = this.getTableName()
|
||||||
@@ -579,11 +589,16 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
FROM ${tableName}
|
FROM ${tableName}
|
||||||
`
|
`
|
||||||
|
|
||||||
const params: number[] = []
|
const params: (number | string)[] = []
|
||||||
|
|
||||||
if (userId !== undefined) {
|
if (userId !== undefined) {
|
||||||
sqlString += isSqlServer ? ` WHERE UserId = @p0 ` : ` WHERE UserId = ? `
|
sqlString += isSqlServer ? ` WHERE UserId = @p0 ` : ` WHERE UserId = ? `
|
||||||
params.push(userId)
|
params.push(userId)
|
||||||
|
} else if (usernames && usernames.length > 0) {
|
||||||
|
// Admin user filtering by multiple usernames using IN clause
|
||||||
|
const placeholders = this.buildPlaceholders(usernames.length, isSqlServer)
|
||||||
|
sqlString += ` WHERE Username IN (${placeholders}) `
|
||||||
|
params.push(...usernames)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await dbService.query(sqlString, params)
|
const result = await dbService.query(sqlString, params)
|
||||||
|
|||||||
@@ -83,4 +83,6 @@ export interface GetBatchesOptions {
|
|||||||
limit?: number
|
limit?: number
|
||||||
/** Number of batches to skip (for pagination) */
|
/** Number of batches to skip (for pagination) */
|
||||||
offset?: number
|
offset?: number
|
||||||
|
/** Optional username filter for Admin users (supports multiple) */
|
||||||
|
usernames?: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +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 {
|
import type {
|
||||||
BatchStats,
|
BatchStats,
|
||||||
OperationHistoryRecord
|
OperationHistoryRecord
|
||||||
} from '../../../main/types/operation-history.types'
|
} from '../../../main/types/operation-history.types'
|
||||||
|
import { showSuccess, showError, showWarning } from '../stores/useAppStore'
|
||||||
|
|
||||||
interface ExtractorOperationHistoryModalProps {
|
interface ExtractorOperationHistoryModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -51,13 +53,20 @@ const statusIcons: Record<string, React.ReactNode> = {
|
|||||||
|
|
||||||
const formatDateTime = (dateStr: string) => {
|
const formatDateTime = (dateStr: string) => {
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
return date.toLocaleString('zh-CN', {
|
|
||||||
year: 'numeric',
|
// Check if the date is valid
|
||||||
month: '2-digit',
|
if (isNaN(date.getTime())) {
|
||||||
day: '2-digit',
|
return dateStr // Return original if invalid
|
||||||
hour: '2-digit',
|
}
|
||||||
minute: '2-digit'
|
|
||||||
})
|
// 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> = ({
|
||||||
@@ -71,6 +80,8 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
const [expandedBatches, setExpandedBatches] = useState<Set<string>>(new Set())
|
const [expandedBatches, setExpandedBatches] = useState<Set<string>>(new Set())
|
||||||
const [batchDetails, setBatchDetails] = useState<Map<string, OperationHistoryRecord[]>>(new Map())
|
const [batchDetails, setBatchDetails] = useState<Map<string, OperationHistoryRecord[]>>(new Map())
|
||||||
const [deleting, setDeleting] = useState<Set<string>>(new Set())
|
const [deleting, setDeleting] = useState<Set<string>>(new Set())
|
||||||
|
const [allUsers, setAllUsers] = useState<string[]>([])
|
||||||
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([])
|
||||||
|
|
||||||
const isAdmin = user?.userType === 'Admin'
|
const isAdmin = user?.userType === 'Admin'
|
||||||
|
|
||||||
@@ -78,7 +89,13 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const result = await window.electron.operationHistory.getBatches({ limit: 100 })
|
// Admin user can pass usernames filter
|
||||||
|
const options =
|
||||||
|
isAdmin && selectedUsers.length > 0
|
||||||
|
? { limit: 100, usernames: selectedUsers }
|
||||||
|
: { limit: 100 }
|
||||||
|
|
||||||
|
const result = await window.electron.operationHistory.getBatches(options)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setBatches(result.data)
|
setBatches(result.data)
|
||||||
} else {
|
} else {
|
||||||
@@ -89,6 +106,18 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
}, [isAdmin, selectedUsers])
|
||||||
|
|
||||||
|
const fetchAllUsers = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.electron.auth.getAllUsers()
|
||||||
|
if (result.success && result.data) {
|
||||||
|
const usernames = result.data.map((u: UserInfo) => u.username)
|
||||||
|
setAllUsers(usernames)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch users:', err)
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchBatchDetails = useCallback(
|
const fetchBatchDetails = useCallback(
|
||||||
@@ -114,8 +143,11 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
void fetchBatches()
|
void fetchBatches()
|
||||||
|
if (isAdmin) {
|
||||||
|
void fetchAllUsers()
|
||||||
}
|
}
|
||||||
}, [isOpen, fetchBatches])
|
}
|
||||||
|
}, [isOpen, fetchBatches, fetchAllUsers, isAdmin])
|
||||||
|
|
||||||
const toggleBatchExpansion = (batchId: string) => {
|
const toggleBatchExpansion = (batchId: string) => {
|
||||||
setExpandedBatches((prev) => {
|
setExpandedBatches((prev) => {
|
||||||
@@ -167,17 +199,83 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCopyColumn = async (field: 'productionId' | 'orderNumber', batchId: string) => {
|
||||||
|
const details = batchDetails.get(batchId) || []
|
||||||
|
const values = details
|
||||||
|
.map((d) => (field === 'productionId' ? d.productionId : d.orderNumber))
|
||||||
|
.filter(Boolean) // 移除空值
|
||||||
|
.join('\n') // 使用换行符分隔
|
||||||
|
|
||||||
|
if (!values) {
|
||||||
|
showWarning('没有可复制的数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(values)
|
||||||
|
showSuccess(`已复制 ${values.split('\n').length} 条数据`)
|
||||||
|
} catch {
|
||||||
|
showError('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleUserFilter = (username: string) => {
|
||||||
|
setSelectedUsers((prev) =>
|
||||||
|
prev.includes(username)
|
||||||
|
? prev.filter((u) => u !== username)
|
||||||
|
: [...prev, username]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearUserFilters = () => {
|
||||||
|
setSelectedUsers([])
|
||||||
|
}
|
||||||
|
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title="操作历史" size="3xl">
|
<Modal isOpen={isOpen} onClose={onClose} title="操作历史" size="3xl">
|
||||||
<div className="flex flex-col h-[70vh]">
|
<div className="flex flex-col h-[70vh]">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200">
|
<div className="flex items-start justify-between mb-4 pb-4 border-b border-gray-200">
|
||||||
|
<div className="flex-1">
|
||||||
|
{isAdmin && allUsers.length > 0 && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="text-xs text-gray-500 mb-2">筛选用户:</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{allUsers.map((username) => {
|
||||||
|
const isSelected = selectedUsers.includes(username)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={username}
|
||||||
|
onClick={() => toggleUserFilter(username)}
|
||||||
|
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-all ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-blue-600 text-white shadow-sm'
|
||||||
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{username}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{selectedUsers.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={clearUserFilters}
|
||||||
|
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium bg-red-50 text-red-600 hover:bg-red-100 transition-all"
|
||||||
|
>
|
||||||
|
清空筛选
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<span className="text-amber-600 font-medium">管理员模式:显示所有用户记录</span>
|
<span className="text-amber-600 font-medium">
|
||||||
|
管理员模式:{selectedUsers.length > 0 ? `已选择 ${selectedUsers.length} 个用户` : '显示所有用户记录'}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>仅显示您的操作记录</span>
|
<span>仅显示您的操作记录</span>
|
||||||
)}
|
)}
|
||||||
@@ -186,8 +284,9 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
<span className="text-sm text-gray-500">共 {batches.length} 条批次</span>
|
<span className="text-sm text-gray-500">共 {batches.length} 条批次</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50 flex-shrink-0"
|
||||||
onClick={() => void fetchBatches()}
|
onClick={() => void fetchBatches()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
title="刷新"
|
title="刷新"
|
||||||
@@ -280,6 +379,7 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isAdmin && (
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-red-50 text-gray-400 hover:text-red-600 rounded transition-colors disabled:opacity-50"
|
className="p-2 hover:bg-red-50 text-gray-400 hover:text-red-600 rounded transition-colors disabled:opacity-50"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -291,6 +391,7 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
>
|
>
|
||||||
<Trash2 size={16} className={isDeleting ? 'animate-pulse' : ''} />
|
<Trash2 size={16} className={isDeleting ? 'animate-pulse' : ''} />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Batch details */}
|
{/* Batch details */}
|
||||||
@@ -301,10 +402,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">
|
||||||
状态
|
状态
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export function AuthenticatedAppShell({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{currentPage === 'extractor' && <ExtractorPage />}
|
{currentPage === 'extractor' && <ExtractorPage currentUser={currentUser} />}
|
||||||
{currentPage === 'cleaner' && <CleanerPage />}
|
{currentPage === 'cleaner' && <CleanerPage />}
|
||||||
{currentPage === 'settings' && <SettingsPage />}
|
{currentPage === 'settings' && <SettingsPage />}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -7,12 +7,28 @@ import { useSharedProductionIds } from '../hooks/useSharedProductionIds'
|
|||||||
import LogPanel from '../components/ui/LogPanel'
|
import LogPanel from '../components/ui/LogPanel'
|
||||||
import { SegmentedProgressBar } from '../components/ui/SegmentedProgressBar'
|
import { SegmentedProgressBar } from '../components/ui/SegmentedProgressBar'
|
||||||
import ExtractorOperationHistoryModal from '../components/ExtractorOperationHistoryModal'
|
import ExtractorOperationHistoryModal from '../components/ExtractorOperationHistoryModal'
|
||||||
import { useUserStore } from '../stores/useUserStore'
|
import type { CurrentUser } from '../hooks/useAppBootstrap'
|
||||||
|
|
||||||
const ExtractorPage: React.FC = () => {
|
interface ExtractorPageProps {
|
||||||
|
currentUser: CurrentUser | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtractorPage: React.FC<ExtractorPageProps> = ({ currentUser }) => {
|
||||||
const [orderNumbers, setOrderNumbers] = usePersistentTextState('extractor_orderNumbers')
|
const [orderNumbers, setOrderNumbers] = usePersistentTextState('extractor_orderNumbers')
|
||||||
const [showHistoryModal, setShowHistoryModal] = React.useState(false)
|
const [showHistoryModal, setShowHistoryModal] = React.useState(false)
|
||||||
const user = useUserStore((state) => state.user)
|
|
||||||
|
// Convert currentUser to UserInfo format for the modal
|
||||||
|
const user = React.useMemo(
|
||||||
|
() =>
|
||||||
|
currentUser
|
||||||
|
? {
|
||||||
|
id: 0, // ID is not needed for modal display logic
|
||||||
|
username: currentUser.username,
|
||||||
|
userType: currentUser.userType
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
[currentUser]
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isRunning,
|
isRunning,
|
||||||
|
|||||||
Reference in New Issue
Block a user