feat:采购日志
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { CSSProperties } from 'react'
|
||||
import { Collapse, Drawer, Select, Space, Table, Tag, Typography, message } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { rockApi, type RockPurchaseLog, type RockPurchaseStage } from '../../api'
|
||||
import { formatDateTime } from '../../utils/time'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
interface PurchaseLogDrawerProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const statusMeta: Record<string, { color: string; text: string }> = {
|
||||
running: { color: 'processing', text: '进行中' },
|
||||
success: { color: 'success', text: '成功' },
|
||||
partial: { color: 'warning', text: '部分成交' },
|
||||
failed: { color: 'error', text: '失败' },
|
||||
skipped: { color: 'default', text: '未采购' },
|
||||
}
|
||||
|
||||
const limitText: Record<string, string> = {
|
||||
points: '积分不足',
|
||||
upstream_stock: '上游货源不足',
|
||||
points_and_stock: '积分与货源不足',
|
||||
}
|
||||
|
||||
const stageText: Record<string, string> = {
|
||||
me: '查询积分',
|
||||
dashboard: '查询库存/单价',
|
||||
claim: '提取 Token',
|
||||
package: '创建打包',
|
||||
detail: '拉取包详情',
|
||||
import: '导入本地',
|
||||
}
|
||||
|
||||
function prettyJSON(raw: string): string {
|
||||
if (!raw) return ''
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(raw), null, 2)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
export function PurchaseLogDrawer({ open, onClose }: PurchaseLogDrawerProps) {
|
||||
const [logs, setLogs] = useState<RockPurchaseLog[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [triggerType, setTriggerType] = useState('')
|
||||
const [status, setStatus] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [stageMap, setStageMap] = useState<Record<number, RockPurchaseStage[]>>({})
|
||||
const [stageLoading, setStageLoading] = useState<Record<number, boolean>>({})
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await rockApi.listPurchaseLogs({
|
||||
page,
|
||||
page_size: pageSize,
|
||||
trigger_type: triggerType || undefined,
|
||||
status: status || undefined,
|
||||
})
|
||||
setLogs(res.list || [])
|
||||
setTotal(res.total || 0)
|
||||
} catch (e) {
|
||||
message.error('加载采购日志失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, pageSize, triggerType, status])
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
load()
|
||||
}
|
||||
}, [open, load])
|
||||
|
||||
const loadStages = async (id: number) => {
|
||||
if (stageMap[id] || stageLoading[id]) return
|
||||
setStageLoading((prev) => ({ ...prev, [id]: true }))
|
||||
try {
|
||||
const res = await rockApi.getPurchaseLog(id)
|
||||
setStageMap((prev) => ({ ...prev, [id]: res.stages || [] }))
|
||||
} catch (e) {
|
||||
message.error('加载阶段详情失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
} finally {
|
||||
setStageLoading((prev) => ({ ...prev, [id]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<RockPurchaseLog> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70, align: 'right' },
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'started_at',
|
||||
width: 175,
|
||||
render: (value?: string) => (value ? formatDateTime(value) : '-'),
|
||||
},
|
||||
{
|
||||
title: '触发方式',
|
||||
dataIndex: 'trigger_type',
|
||||
width: 90,
|
||||
render: (value: string) =>
|
||||
value === 'auto' ? <Tag color="blue">自动补货</Tag> : <Tag>手动采购</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (value: string) => {
|
||||
const meta = statusMeta[value] || { color: 'default', text: value }
|
||||
return <Tag color={meta.color}>{meta.text}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '计划 → 实际',
|
||||
width: 110,
|
||||
render: (_, record) => (
|
||||
<Text style={{ fontFamily: 'monospace' }}>
|
||||
{record.requested_count} → {record.planned_count}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '入库 / 提取',
|
||||
width: 105,
|
||||
render: (_, record) => (
|
||||
<Text style={{ fontFamily: 'monospace' }}>
|
||||
{record.imported_count} / {record.claimed_count}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '积分 / 单价',
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<Text style={{ fontFamily: 'monospace', fontSize: 12 }}>
|
||||
{record.points_before.toFixed(2)} / {record.unit_cost.toFixed(2)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '上游库存',
|
||||
dataIndex: 'upstream_available',
|
||||
width: 85,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '限制原因',
|
||||
dataIndex: 'limit_reason',
|
||||
width: 120,
|
||||
render: (value: string) =>
|
||||
value ? <Tag color="orange">{limitText[value] || value}</Tag> : <Text type="secondary">-</Text>,
|
||||
},
|
||||
{
|
||||
title: '耗时',
|
||||
dataIndex: 'duration_ms',
|
||||
width: 80,
|
||||
align: 'right',
|
||||
render: (value: number) => `${(value / 1000).toFixed(1)}s`,
|
||||
},
|
||||
{
|
||||
title: '错误',
|
||||
dataIndex: 'error_message',
|
||||
ellipsis: true,
|
||||
render: (value: string) =>
|
||||
value ? (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
{value}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="secondary">-</Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title="采购日志"
|
||||
width={1180}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Select
|
||||
value={triggerType}
|
||||
style={{ width: 140 }}
|
||||
onChange={(value) => {
|
||||
setTriggerType(value)
|
||||
setPage(1)
|
||||
}}
|
||||
options={[
|
||||
{ value: '', label: '全部触发方式' },
|
||||
{ value: 'auto', label: '自动补货' },
|
||||
{ value: 'manual', label: '手动采购' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={status}
|
||||
style={{ width: 140 }}
|
||||
onChange={(value) => {
|
||||
setStatus(value)
|
||||
setPage(1)
|
||||
}}
|
||||
options={[
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'partial', label: '部分成交' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
{ value: 'skipped', label: '未采购' },
|
||||
]}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
共 {total} 条记录,点击行可展开原始响应
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={logs}
|
||||
loading={loading}
|
||||
scroll={{ x: 1320 }}
|
||||
expandable={{
|
||||
expandedRowRender: (record) => {
|
||||
if (stageLoading[record.id]) {
|
||||
return <Text type="secondary">阶段详情加载中…</Text>
|
||||
}
|
||||
const stages = stageMap[record.id]
|
||||
if (!stages || stages.length === 0) {
|
||||
return <Text type="secondary">暂无阶段数据</Text>
|
||||
}
|
||||
return (
|
||||
<Collapse
|
||||
size="small"
|
||||
items={stages.map((stage) => ({
|
||||
key: String(stage.id),
|
||||
label: (
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">{stageText[stage.stage] || stage.stage}</Tag>
|
||||
<Text style={{ fontFamily: 'monospace', fontSize: 12 }}>
|
||||
{stage.method} {stage.http_status || '-'}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{stage.duration_ms}ms
|
||||
</Text>
|
||||
{stage.truncated ? <Tag color="orange">已截断</Tag> : null}
|
||||
{stage.error_message ? (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
{stage.error_message}
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-all' }}>
|
||||
{stage.url}
|
||||
</Text>
|
||||
{stage.request_body ? (
|
||||
<>
|
||||
<div style={{ marginTop: 8, fontWeight: 600, fontSize: 12 }}>请求体</div>
|
||||
<pre style={preStyle}>{prettyJSON(stage.request_body)}</pre>
|
||||
</>
|
||||
) : null}
|
||||
<div style={{ marginTop: 8, fontWeight: 600, fontSize: 12 }}>原始响应</div>
|
||||
<pre style={preStyle}>{prettyJSON(stage.response_body) || '(空)'}</pre>
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
},
|
||||
onExpand: (expanded, record) => {
|
||||
if (expanded) {
|
||||
loadStages(record.id)
|
||||
}
|
||||
},
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
showTotal: (count) => `共 ${count} 条采购记录`,
|
||||
onChange: (nextPage, nextSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextSize)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const preStyle: CSSProperties = {
|
||||
margin: '6px 0 0',
|
||||
padding: 10,
|
||||
background: '#0f172a',
|
||||
color: '#e2e8f0',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
maxHeight: 320,
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
}
|
||||
Reference in New Issue
Block a user