first commit

This commit is contained in:
2026-03-11 16:28:04 +08:00
commit c0f9ddabbf
101 changed files with 11601 additions and 0 deletions

View 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;