first commit
This commit is contained in:
308
frontend/src/pages/screenshots/Screenshots.tsx
Normal file
308
frontend/src/pages/screenshots/Screenshots.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
Typography,
|
||||
Select,
|
||||
Space,
|
||||
Badge,
|
||||
Descriptions,
|
||||
Empty,
|
||||
List,
|
||||
Drawer,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Segmented,
|
||||
} from 'antd';
|
||||
import {
|
||||
FileImageOutlined,
|
||||
CheckCircleOutlined,
|
||||
LoadingOutlined,
|
||||
CloseCircleOutlined,
|
||||
EditOutlined,
|
||||
ZoomInOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { EvidenceImage, SourceApp, PageType } from '../../types';
|
||||
import { fetchImages } from '../../services/api';
|
||||
|
||||
const appLabel: Record<SourceApp, { label: string; color: string }> = {
|
||||
wechat: { label: '微信', color: 'green' },
|
||||
alipay: { label: '支付宝', color: 'blue' },
|
||||
bank: { label: '银行', color: 'purple' },
|
||||
digital_wallet: { label: '数字钱包', color: 'orange' },
|
||||
other: { label: '其他', color: 'default' },
|
||||
};
|
||||
|
||||
const pageTypeLabel: Record<PageType, string> = {
|
||||
bill_list: '账单列表',
|
||||
bill_detail: '账单详情',
|
||||
transfer_receipt: '转账凭证',
|
||||
sms_notice: '短信通知',
|
||||
balance: '余额页',
|
||||
unknown: '未识别',
|
||||
};
|
||||
|
||||
const ocrStatusIcon: Record<string, React.ReactNode> = {
|
||||
done: <CheckCircleOutlined style={{ color: '#52c41a' }} />,
|
||||
processing: <LoadingOutlined style={{ color: '#1677ff' }} />,
|
||||
failed: <CloseCircleOutlined style={{ color: '#ff4d4f' }} />,
|
||||
pending: <FileImageOutlined style={{ color: '#d9d9d9' }} />,
|
||||
};
|
||||
|
||||
const Screenshots: React.FC = () => {
|
||||
const { id = '1' } = useParams();
|
||||
const [filterApp, setFilterApp] = useState<string>('all');
|
||||
const [selectedImage, setSelectedImage] = useState<EvidenceImage | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
const { data: allImages = [] } = useQuery({
|
||||
queryKey: ['images', id],
|
||||
queryFn: () => fetchImages(id),
|
||||
});
|
||||
|
||||
const filtered =
|
||||
filterApp === 'all'
|
||||
? allImages
|
||||
: allImages.filter((img: EvidenceImage) => img.sourceApp === filterApp);
|
||||
|
||||
const appCounts = allImages.reduce<Record<string, number>>((acc, img: EvidenceImage) => {
|
||||
acc[img.sourceApp] = (acc[img.sourceApp] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const handleSelect = (img: EvidenceImage) => {
|
||||
setSelectedImage(img);
|
||||
setDrawerOpen(true);
|
||||
};
|
||||
|
||||
const mockOcrFields = [
|
||||
{ label: '交易时间', value: '2026-03-06 10:25:00', confidence: 0.97 },
|
||||
{ label: '交易金额', value: '¥50,000.00', confidence: 0.99 },
|
||||
{ label: '交易方向', value: '支出', confidence: 0.95 },
|
||||
{ label: '对方账户', value: '李*华 (138****5678)', confidence: 0.88 },
|
||||
{ label: '订单号', value: 'AL20260306002', confidence: 0.96 },
|
||||
{ label: '备注', value: '投资款', confidence: 0.92 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<FileImageOutlined />
|
||||
<span>截图管理</span>
|
||||
<Tag>{allImages.length} 张</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Select
|
||||
value={filterApp}
|
||||
onChange={setFilterApp}
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ label: '全部来源', value: 'all' },
|
||||
...Object.entries(appLabel).map(([k, v]) => ({
|
||||
label: `${v.label} (${appCounts[k] || 0})`,
|
||||
value: k,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: '网格', value: 'grid' },
|
||||
{ label: '列表', value: 'list' },
|
||||
]}
|
||||
defaultValue="grid"
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
{filtered.map((img) => (
|
||||
<Col key={img.id} xs={12} sm={8} md={6} lg={4}>
|
||||
<Card
|
||||
hoverable
|
||||
onClick={() => handleSelect(img)}
|
||||
styles={{
|
||||
body: { padding: 12 },
|
||||
}}
|
||||
cover={
|
||||
<div
|
||||
style={{
|
||||
height: 200,
|
||||
background: '#f5f5f5',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<FileImageOutlined
|
||||
style={{ fontSize: 40, color: '#bfbfbf' }}
|
||||
/>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 12, marginTop: 8 }}
|
||||
>
|
||||
截图预览区
|
||||
</Typography.Text>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
}}
|
||||
>
|
||||
{ocrStatusIcon[img.ocrStatus]}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
}}
|
||||
>
|
||||
<Tag
|
||||
color={appLabel[img.sourceApp].color}
|
||||
style={{ fontSize: 11 }}
|
||||
>
|
||||
{appLabel[img.sourceApp].label}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Typography.Text ellipsis style={{ fontSize: 13 }}>
|
||||
{pageTypeLabel[img.pageType]}
|
||||
</Typography.Text>
|
||||
<br />
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{img.uploadedAt}
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{filtered.length === 0 && <Empty description="暂无截图" />}
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
selectedImage ? (
|
||||
<Space>
|
||||
<Tag color={appLabel[selectedImage.sourceApp].color}>
|
||||
{appLabel[selectedImage.sourceApp].label}
|
||||
</Tag>
|
||||
<span>{pageTypeLabel[selectedImage.pageType]}</span>
|
||||
<Badge
|
||||
status={selectedImage.ocrStatus === 'done' ? 'success' : 'processing'}
|
||||
text={selectedImage.ocrStatus === 'done' ? 'OCR已完成' : '处理中'}
|
||||
/>
|
||||
</Space>
|
||||
) : '截图详情'
|
||||
}
|
||||
placement="right"
|
||||
width={560}
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
>
|
||||
{selectedImage && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
background: '#fafafa',
|
||||
borderRadius: 8,
|
||||
height: 300,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 24,
|
||||
border: '1px dashed #d9d9d9',
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" align="center">
|
||||
<FileImageOutlined style={{ fontSize: 64, color: '#bfbfbf' }} />
|
||||
<Typography.Text type="secondary">
|
||||
原始截图预览
|
||||
</Typography.Text>
|
||||
<Button icon={<ZoomInOutlined />} size="small">
|
||||
放大查看
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Typography.Title level={5}>OCR 提取字段</Typography.Title>
|
||||
<Typography.Text type="secondary" style={{ marginBottom: 16, display: 'block' }}>
|
||||
以下为系统自动提取结果,可手工修正
|
||||
</Typography.Text>
|
||||
|
||||
<List
|
||||
dataSource={mockOcrFields}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
extra={
|
||||
<Space>
|
||||
<Tag
|
||||
color={
|
||||
item.confidence >= 0.95
|
||||
? 'green'
|
||||
: item.confidence >= 0.85
|
||||
? 'orange'
|
||||
: 'red'
|
||||
}
|
||||
>
|
||||
{(item.confidence * 100).toFixed(0)}%
|
||||
</Tag>
|
||||
<Button type="link" size="small" icon={<EditOutlined />}>
|
||||
修正
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{item.label}
|
||||
</Typography.Text>
|
||||
}
|
||||
description={
|
||||
<Typography.Text strong>{item.value}</Typography.Text>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Descriptions column={1} size="small">
|
||||
<Descriptions.Item label="图片ID">
|
||||
{selectedImage.id}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="文件哈希">
|
||||
{selectedImage.hash}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="上传时间">
|
||||
{selectedImage.uploadedAt}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Screenshots;
|
||||
Reference in New Issue
Block a user