feat:未绑定token导入

This commit is contained in:
2026-10-31 14:03:04 +08:00
parent 8f30e6fe57
commit c50f471f64
8 changed files with 768 additions and 1 deletions
+26
View File
@@ -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;
+15 -1
View File
@@ -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>
)
}