feat:未绑定token导入
This commit is contained in:
@@ -710,6 +710,26 @@ export interface RockPurchaseStage {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface PackageImportItem {
|
||||
package_id: number;
|
||||
share_code: string;
|
||||
inserted: number;
|
||||
skipped_bound: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ImportAllResult {
|
||||
scanned: number;
|
||||
skipped: number;
|
||||
empty: number;
|
||||
imported: number;
|
||||
failed: number;
|
||||
tokens_imported: number;
|
||||
tokens_skipped_bound: number;
|
||||
remaining: number;
|
||||
results: PackageImportItem[];
|
||||
}
|
||||
|
||||
export const rockApi = {
|
||||
getConfig: () =>
|
||||
request.get("/platform/rock/config").then((r) => r.data.data as RockConfig),
|
||||
@@ -747,6 +767,12 @@ export const rockApi = {
|
||||
(r) =>
|
||||
r.data.data as { log: RockPurchaseLog; stages: RockPurchaseStage[] },
|
||||
),
|
||||
importAllUpstreamPackages: (maxPackages = 50) =>
|
||||
request
|
||||
.post("/platform/rock/upstream-packages/import-all", {
|
||||
max_packages: maxPackages,
|
||||
})
|
||||
.then((r) => r.data.data as ImportAllResult),
|
||||
listTokens: (params?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Alert, Button, Form, Modal, Space, message } from 'antd'
|
||||
import { HistoryOutlined, ReloadOutlined, ShoppingCartOutlined } from '@ant-design/icons'
|
||||
import { HistoryOutlined, ImportOutlined, ReloadOutlined, ShoppingCartOutlined } from '@ant-design/icons'
|
||||
import { rockApi, type RockConfig, type RockStats, type RockTokenItem } from '../api'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
import { RockMetricCards } from './platformRock/RockMetricCards'
|
||||
@@ -9,6 +9,7 @@ import { RockTokenFilterBar } from './platformRock/RockTokenFilterBar'
|
||||
import { RockTokenTable } from './platformRock/RockTokenTable'
|
||||
import { PurchaseTokenModal } from './platformRock/PurchaseTokenModal'
|
||||
import { PurchaseLogDrawer } from './platformRock/PurchaseLogDrawer'
|
||||
import { ImportAllPackagesModal } from './platformRock/ImportAllPackagesModal'
|
||||
import { UidEditModal } from './platformRock/UidEditModal'
|
||||
import { VoucherPreviewModal } from './platformRock/VoucherPreviewModal'
|
||||
import type { UidEditState } from './platformRock/types'
|
||||
@@ -35,6 +36,7 @@ export default function PlatformRock() {
|
||||
const [purchaseModalOpen, setPurchaseModalOpen] = useState(false)
|
||||
const [purchaseCount, setPurchaseCount] = useState(50)
|
||||
const [purchaseLogOpen, setPurchaseLogOpen] = useState(false)
|
||||
const [importPackageOpen, setImportPackageOpen] = useState(false)
|
||||
const [rawDataRecord, setRawDataRecord] = useState<RockTokenItem | null>(null)
|
||||
|
||||
// 凭证图片操作状态
|
||||
@@ -302,6 +304,9 @@ export default function PlatformRock() {
|
||||
<Button icon={<HistoryOutlined />} onClick={() => setPurchaseLogOpen(true)}>
|
||||
采购日志
|
||||
</Button>
|
||||
<Button icon={<ImportOutlined />} onClick={() => setImportPackageOpen(true)}>
|
||||
全量导入
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
@@ -437,6 +442,15 @@ export default function PlatformRock() {
|
||||
/>
|
||||
|
||||
<PurchaseLogDrawer open={purchaseLogOpen} onClose={() => setPurchaseLogOpen(false)} />
|
||||
|
||||
<ImportAllPackagesModal
|
||||
open={importPackageOpen}
|
||||
onClose={() => setImportPackageOpen(false)}
|
||||
onImported={() => {
|
||||
loadData()
|
||||
loadTokens()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useState } from 'react'
|
||||
import { Alert, Button, Descriptions, Modal, Space, Table, Tag, Typography, message } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { rockApi, type ImportAllResult, type PackageImportItem } from '../../api'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
interface ImportAllPackagesModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onImported: () => void
|
||||
}
|
||||
|
||||
const MAX_PACKAGES = 50
|
||||
|
||||
export function ImportAllPackagesModal({ open, onClose, onImported }: ImportAllPackagesModalProps) {
|
||||
const [running, setRunning] = useState(false)
|
||||
const [result, setResult] = useState<ImportAllResult | null>(null)
|
||||
|
||||
const handleClose = () => {
|
||||
if (running) {
|
||||
return
|
||||
}
|
||||
setResult(null)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
setRunning(true)
|
||||
setResult(null)
|
||||
try {
|
||||
const res = await rockApi.importAllUpstreamPackages(MAX_PACKAGES)
|
||||
setResult(res)
|
||||
if (res.imported > 0 || res.failed > 0) {
|
||||
onImported()
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('全量导入失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<PackageImportItem> = [
|
||||
{ title: '包 ID', dataIndex: 'package_id', width: 90, align: 'right' },
|
||||
{
|
||||
title: 'share_code',
|
||||
dataIndex: 'share_code',
|
||||
width: 220,
|
||||
render: (value: string) => (
|
||||
<Text style={{ fontFamily: 'monospace', fontSize: 12 }}>{value}</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '新增 Token',
|
||||
dataIndex: 'inserted',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
render: (value: number) => (value > 0 ? <Tag color="success">{value}</Tag> : '-'),
|
||||
},
|
||||
{
|
||||
title: '跳过已绑定',
|
||||
dataIndex: 'skipped_bound',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
render: (value: number) => (value > 0 ? <Text type="secondary">{value}</Text> : '-'),
|
||||
},
|
||||
{
|
||||
title: '结果',
|
||||
dataIndex: 'error',
|
||||
ellipsis: true,
|
||||
render: (value?: string) =>
|
||||
value ? (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
{value}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="success">成功</Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const detailRows = (result?.results || []).filter(
|
||||
(item) => item.inserted > 0 || Boolean(item.error),
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="全量导入上游包"
|
||||
width={900}
|
||||
open={open}
|
||||
onCancel={handleClose}
|
||||
maskClosable={!running}
|
||||
closable={!running}
|
||||
footer={
|
||||
result
|
||||
? [
|
||||
<Button key="close" onClick={handleClose}>
|
||||
关闭
|
||||
</Button>,
|
||||
<Button key="again" type="primary" loading={running} onClick={handleStart}>
|
||||
再次导入
|
||||
</Button>,
|
||||
]
|
||||
: [
|
||||
<Button key="cancel" onClick={handleClose} disabled={running}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="start" type="primary" loading={running} onClick={handleStart}>
|
||||
开始导入
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
>
|
||||
{!result ? (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="导入规则"
|
||||
description={
|
||||
<div style={{ fontSize: 13, lineHeight: 1.8 }}>
|
||||
· 扫描全部上游包,本地已存在该包 Token 的整包跳过
|
||||
<br />· 只导入<b>未绑定</b>的 Token:<Text code>bind_changed_at</Text> 与{' '}
|
||||
<Text code>bind_change_type</Text> 均为空
|
||||
<br />· 导入后统一标记为<b>可用</b>
|
||||
<br />· 单次最多处理 {MAX_PACKAGES} 个未导入包,剩余的下次再导入
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{running ? (
|
||||
<Text type="secondary">
|
||||
正在逐个拉取包详情并导入,可能需要 1 分钟,请勿关闭窗口…
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
) : (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Descriptions bordered size="small" column={2}>
|
||||
<Descriptions.Item label="扫描包数">{result.scanned}</Descriptions.Item>
|
||||
<Descriptions.Item label="已导入跳过">{result.skipped}</Descriptions.Item>
|
||||
<Descriptions.Item label="无可导入 Token">{result.empty}</Descriptions.Item>
|
||||
<Descriptions.Item label="成功导入包数">{result.imported}</Descriptions.Item>
|
||||
<Descriptions.Item label="新增 Token">{result.tokens_imported}</Descriptions.Item>
|
||||
<Descriptions.Item label="跳过已绑定 Token">{result.tokens_skipped_bound}</Descriptions.Item>
|
||||
<Descriptions.Item label="失败包数">{result.failed}</Descriptions.Item>
|
||||
<Descriptions.Item label="超过上限未处理">{result.remaining}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{result.failed > 0 ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`有 ${result.failed} 个包导入失败,详情见下表`}
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
type="success"
|
||||
showIcon
|
||||
message={
|
||||
result.tokens_imported > 0
|
||||
? `本次新增 ${result.tokens_imported} 个可用 Token`
|
||||
: '本次没有可导入的未绑定 Token'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{detailRows.length > 0 ? (
|
||||
<Table
|
||||
rowKey="package_id"
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={detailRows}
|
||||
pagination={false}
|
||||
scroll={{ y: 260 }}
|
||||
/>
|
||||
) : null}
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user