feat(huya): 协作台账状态机新口径(跳步/已确认绑定/兑换失败)+ 账号全生命周期快照(迁移 0059)
This commit is contained in:
@@ -32,11 +32,14 @@ import type {
|
||||
HuyaTaskItem,
|
||||
HuyaBrowserExchangeKit,
|
||||
HuyaCollabAuditItem,
|
||||
HuyaCollabActivityBatchResult,
|
||||
HuyaCollabActivityItem,
|
||||
HuyaCollabCkPreview,
|
||||
HuyaCollabCkRequest,
|
||||
HuyaCollabCkStatusRequest,
|
||||
HuyaCollabImagesRequest,
|
||||
HuyaCollabImportResult,
|
||||
HuyaCollabMetric,
|
||||
HuyaCollabOrder,
|
||||
HuyaCollabOrderCreate,
|
||||
HuyaCollabOrderList,
|
||||
@@ -47,6 +50,8 @@ import type {
|
||||
HuyaCollabShopCreate,
|
||||
HuyaCollabShopUpdate,
|
||||
HuyaCollabStatusRequest,
|
||||
HuyaCollabStatusBatchRequest,
|
||||
HuyaCollabStatusBatchResult,
|
||||
HuyaCollabSummary,
|
||||
HuyaCollabVerifyResult,
|
||||
HuyaExchangeTargetKey,
|
||||
@@ -252,6 +257,26 @@ export const huyaApi = {
|
||||
/** 操作时间线(直接读 audit_logs) */
|
||||
collabOrderTimeline: (id: number) =>
|
||||
api.get<HuyaCollabAuditItem[], HuyaCollabAuditItem[]>(`/huya/external-orders/${id}/timeline`),
|
||||
/** 勾选多行一次改业务状态(单个也走这里;回退要带 rollback) */
|
||||
batchUpdateCollabOrderStatus: (data: HuyaCollabStatusBatchRequest) =>
|
||||
api.post<HuyaCollabStatusBatchResult, HuyaCollabStatusBatchResult>(
|
||||
'/huya/external-orders/status/batch',
|
||||
data,
|
||||
),
|
||||
/** 查这一单的积分 + 兑换记录(写一条生命周期快照,已上脚本的单会按结果自动改状态) */
|
||||
queryCollabOrderActivity: (id: number) =>
|
||||
api.post<HuyaCollabActivityItem, HuyaCollabActivityItem>(
|
||||
`/huya/external-orders/${id}/activity/query`,
|
||||
),
|
||||
/** 勾选多行查询积分 + 兑换记录(串行带间隔) */
|
||||
batchQueryCollabOrderActivity: (ids: number[]) =>
|
||||
api.post<HuyaCollabActivityBatchResult, HuyaCollabActivityBatchResult>(
|
||||
'/huya/external-orders/activity/query',
|
||||
{ ids },
|
||||
),
|
||||
/** 这一单的「账号全生命周期」时间线(每次查询的快照,新→旧) */
|
||||
collabOrderMetrics: (id: number) =>
|
||||
api.get<HuyaCollabMetric[], HuyaCollabMetric[]>(`/huya/external-orders/${id}/metrics`),
|
||||
/** 删除一行(只给运营:huya:collab:manage) */
|
||||
deleteCollabOrder: (id: number) =>
|
||||
api.delete<{ success: boolean; order_no: string }, { success: boolean; order_no: string }>(
|
||||
|
||||
@@ -1199,7 +1199,15 @@ export interface AccountExchangeRecords {
|
||||
// ---- 虎牙外部 CK 协作台账(/api/huya/external-orders,见 docs/设计-虎牙外部CK协作台账.md)----
|
||||
|
||||
/** 业务状态:一单卡在哪一步(void 是旁路) */
|
||||
export type HuyaCollabStatus = 'pending' | 'bound' | 'recharged' | 'scripted' | 'done' | 'void';
|
||||
export type HuyaCollabStatus =
|
||||
| 'pending'
|
||||
| 'bound'
|
||||
| 'recharged'
|
||||
| 'scripted'
|
||||
| 'done'
|
||||
/** 旁路终态:抢购后查兑换记录没查到这一单的礼包(人工确认后可作废或回退重试) */
|
||||
| 'failed'
|
||||
| 'void';
|
||||
|
||||
/** 凭据状态:CK 本身还有没有效 */
|
||||
export type HuyaCollabCkStatus = 'unverified' | 'valid' | 'invalid' | 'disabled';
|
||||
@@ -1264,6 +1272,12 @@ export interface HuyaCollabOrder {
|
||||
* 只有真的 ≥2 单才有值,否则是 null。
|
||||
*/
|
||||
buyer_cluster: HuyaCollabBuyerCluster | null;
|
||||
/** 最近一次「查积分 + 兑换记录」的快照(完整时间线走 GET /{id}/metrics) */
|
||||
last_score: number | null;
|
||||
last_metric_at: string | null;
|
||||
last_verdict: HuyaCollabVerdict | '';
|
||||
last_verdict_label: string;
|
||||
last_metric_message: string;
|
||||
}
|
||||
|
||||
/** 「同一买家的其它单」里的一条 */
|
||||
@@ -1288,6 +1302,98 @@ export interface HuyaCollabBuyerCluster {
|
||||
others: HuyaCollabBuyerOrder[];
|
||||
}
|
||||
|
||||
/** 系统与接口的判定结论(人工不写,只读) */
|
||||
export type HuyaCollabVerdict = 'success' | 'failed' | 'unknown';
|
||||
|
||||
/** 「账号全生命周期」里匹配到的兑换记录 */
|
||||
export interface HuyaCollabMetricMatched {
|
||||
prize_name: string;
|
||||
score: number | null;
|
||||
status_label: string;
|
||||
order_id: string;
|
||||
exchange_time: number | null;
|
||||
}
|
||||
|
||||
/** 一次「查积分 + 兑换记录」的快照(只追加,不更新) */
|
||||
export interface HuyaCollabMetric {
|
||||
id: number;
|
||||
order_id: number;
|
||||
order_no: string;
|
||||
uid: string;
|
||||
sid: number | null;
|
||||
score: number | null;
|
||||
gained_score: number | null;
|
||||
used_score: number | null;
|
||||
records_total: number;
|
||||
records_matched: number;
|
||||
verdict: HuyaCollabVerdict;
|
||||
verdict_label: string;
|
||||
message: string;
|
||||
/** manual(人工点)/ batch(勾选批量) */
|
||||
source: string;
|
||||
matched_preview: HuyaCollabMetricMatched[];
|
||||
created_by: number | null;
|
||||
created_by_name: string;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
/** 单个订单查询积分/兑换记录的结果 */
|
||||
export interface HuyaCollabActivityItem {
|
||||
ok: boolean;
|
||||
order_id: number;
|
||||
order_no: string;
|
||||
verdict: HuyaCollabVerdict;
|
||||
verdict_label: string;
|
||||
score: number | null;
|
||||
gained_score: number | null;
|
||||
used_score: number | null;
|
||||
records_total: number;
|
||||
records_matched: number;
|
||||
message: string;
|
||||
/** 查询后自动改成的状态(空 = 没改:只有「已上脚本」的单会自动改) */
|
||||
auto_status: HuyaCollabStatus | '';
|
||||
auto_status_label: string;
|
||||
status: HuyaCollabStatus;
|
||||
status_label: string;
|
||||
}
|
||||
|
||||
/** 批量查询汇总 */
|
||||
export interface HuyaCollabActivityBatchResult {
|
||||
total: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
unknown: number;
|
||||
auto_changed: number;
|
||||
results: HuyaCollabActivityItem[];
|
||||
}
|
||||
|
||||
/** 批量改状态请求(单个也走这里) */
|
||||
export interface HuyaCollabStatusBatchRequest {
|
||||
ids: number[];
|
||||
status: HuyaCollabStatus;
|
||||
reason?: string;
|
||||
/** 目标不是「向前」时必须显式 true */
|
||||
rollback?: boolean;
|
||||
}
|
||||
|
||||
/** 批量改状态里的一行结论 */
|
||||
export interface HuyaCollabStatusBatchItem {
|
||||
id: number;
|
||||
order_no: string;
|
||||
ok: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** 批量改状态汇总 */
|
||||
export interface HuyaCollabStatusBatchResult {
|
||||
total: number;
|
||||
updated: number;
|
||||
failed: number;
|
||||
target: HuyaCollabStatus;
|
||||
target_label: string;
|
||||
results: HuyaCollabStatusBatchItem[];
|
||||
}
|
||||
|
||||
export interface HuyaCollabOrderList {
|
||||
items: HuyaCollabOrder[];
|
||||
total: number;
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert, App, AutoComplete, Button, Card, Col, Descriptions, Divider, Drawer, Dropdown, Empty, Form,
|
||||
Image, Input, Modal, Popconfirm, Popover, Progress, Row, Select, Space, Statistic, Switch, Table,
|
||||
Tag, Timeline, Tooltip, Typography, Upload,
|
||||
Alert, App, AutoComplete, Button, Card, Checkbox, Col, Descriptions, Divider, Drawer, Dropdown,
|
||||
Empty, Form, Image, Input, Modal, Popconfirm, Popover, Progress, Row, Select, Space, Statistic,
|
||||
Switch, Table, Tag, Timeline, Tooltip, Typography, Upload,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined, CloudUploadOutlined, DeleteOutlined, EditOutlined, FileExcelOutlined,
|
||||
LinkOutlined, PlusOutlined, ReloadOutlined, SafetyCertificateOutlined, ShopOutlined,
|
||||
StopOutlined, SwapOutlined, UndoOutlined,
|
||||
StopOutlined, SwapOutlined, SyncOutlined, UndoOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
fileApi,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type HuyaCollabCkPreview,
|
||||
type HuyaCollabGift,
|
||||
type HuyaCollabImageSlot,
|
||||
type HuyaCollabMetric,
|
||||
type HuyaCollabOrder,
|
||||
type HuyaCollabOrderQuery,
|
||||
type HuyaCollabOrderUpdate,
|
||||
@@ -44,13 +45,39 @@ const { Text, Paragraph } = Typography;
|
||||
|
||||
const STATUS_META: Record<HuyaCollabStatus, { label: string; color: string }> = {
|
||||
pending: { label: '待处理', color: 'default' },
|
||||
bound: { label: '已绑定', color: 'blue' },
|
||||
// 人工核对过「探针读到的绑定角色」与「主页截图」一致
|
||||
bound: { label: '已确认绑定', color: 'blue' },
|
||||
recharged: { label: '已充值', color: 'geekblue' },
|
||||
scripted: { label: '已上脚本', color: 'purple' },
|
||||
done: { label: '已完成', color: 'success' },
|
||||
// 旁路终态:抢购后查兑换记录没查到这一单的礼包
|
||||
failed: { label: '兑换失败', color: 'red' },
|
||||
void: { label: '已作废', color: 'error' },
|
||||
};
|
||||
|
||||
/** 判定结论的配色(生命周期时间线用) */
|
||||
const VERDICT_META: Record<string, { label: string; color: string }> = {
|
||||
success: { label: '兑换成功', color: 'success' },
|
||||
failed: { label: '兑换失败', color: 'error' },
|
||||
unknown: { label: '无法判定', color: 'default' },
|
||||
};
|
||||
|
||||
/** 「改状态」能选的目标(顺序 = 业务流程顺序,回退要显式勾选) */
|
||||
const STATUS_ORDER_OPTIONS: { value: HuyaCollabStatus; label: string }[] = [
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'bound', label: '已确认绑定' },
|
||||
{ value: 'recharged', label: '已充值' },
|
||||
{ value: 'scripted', label: '已上脚本' },
|
||||
{ value: 'done', label: '已完成' },
|
||||
{ value: 'failed', label: '兑换失败' },
|
||||
{ value: 'void', label: '已作废' },
|
||||
];
|
||||
|
||||
/** 「回退」能选的目标:只在流程链里选(要进「已作废 / 兑换失败」走「改状态」或「作废」入口) */
|
||||
const FLOW_STATUS_OPTIONS = STATUS_ORDER_OPTIONS.filter(
|
||||
(option) => option.value !== 'failed' && option.value !== 'void',
|
||||
);
|
||||
|
||||
const CK_META: Record<string, { label: string; color: string }> = {
|
||||
unverified: { label: '未验证', color: 'default' },
|
||||
valid: { label: '有效', color: 'success' },
|
||||
@@ -293,11 +320,21 @@ export default function HuyaCollabPage() {
|
||||
} | null>(null);
|
||||
|
||||
const [ckTarget, setCkTarget] = useState<{ order: HuyaCollabOrder; text: string } | null>(null);
|
||||
// 单行状态弹窗:set=人工往前切(可跳步)/ rollback=回退 / void=作废
|
||||
const [statusAction, setStatusAction] = useState<{
|
||||
order: HuyaCollabOrder; mode: 'void' | 'rollback'; target?: HuyaCollabStatus;
|
||||
order: HuyaCollabOrder; mode: 'void' | 'rollback' | 'set'; target?: HuyaCollabStatus;
|
||||
} | null>(null);
|
||||
const [statusReason, setStatusReason] = useState('');
|
||||
const [rollbackTarget, setRollbackTarget] = useState<HuyaCollabStatus>('pending');
|
||||
// 批量改状态(勾选多行一次切)
|
||||
const [batchStatusOpen, setBatchStatusOpen] = useState(false);
|
||||
const [batchStatusTarget, setBatchStatusTarget] = useState<HuyaCollabStatus>('bound');
|
||||
const [batchStatusReason, setBatchStatusReason] = useState('');
|
||||
const [batchStatusRollback, setBatchStatusRollback] = useState(false);
|
||||
// 积分 / 兑换记录(「账号全生命周期」)
|
||||
const [querying, setQuerying] = useState(false);
|
||||
const [metrics, setMetrics] = useState<HuyaCollabMetric[]>([]);
|
||||
const [metricsLoading, setMetricsLoading] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -411,6 +448,106 @@ export default function HuyaCollabPage() {
|
||||
[runCkPreview],
|
||||
);
|
||||
|
||||
/** 勾选多行一次改状态(往前可跳步;回退要在弹窗里勾「回退」) */
|
||||
const submitBatchStatus = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
const result = await huyaApi.batchUpdateCollabOrderStatus({
|
||||
ids: selectedIds,
|
||||
status: batchStatusTarget,
|
||||
reason: batchStatusReason,
|
||||
rollback: batchStatusRollback,
|
||||
});
|
||||
const failures = result.results.filter((item) => !item.ok);
|
||||
if (failures.length === 0) {
|
||||
message.success(`已把 ${result.updated} 条改成「${result.target_label}」`);
|
||||
} else {
|
||||
modal.info({
|
||||
title: `改状态完成:成功 ${result.updated} / 未通过 ${result.failed}(目标「${result.target_label}」)`,
|
||||
width: 640,
|
||||
content: (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{failures.map((item) => (
|
||||
<Text key={item.id} type="danger">
|
||||
{item.order_no}:{item.message}
|
||||
</Text>
|
||||
))}
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
}
|
||||
setBatchStatusOpen(false);
|
||||
setSelectedIds([]);
|
||||
void load();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 查这一单的积分 + 兑换记录(写一条生命周期快照;已上脚本的单会按结果自动改状态) */
|
||||
const queryActivityOne = async (row: HuyaCollabOrder) => {
|
||||
setQuerying(true);
|
||||
try {
|
||||
const result = await huyaApi.queryCollabOrderActivity(row.id);
|
||||
message[result.verdict === 'success' ? 'success' : result.verdict === 'failed' ? 'warning' : 'info'](
|
||||
`${row.order_no}:${result.verdict_label} · ${result.message}`
|
||||
+ (result.auto_status ? `;状态已自动改为「${result.auto_status_label}」` : ''),
|
||||
);
|
||||
void load();
|
||||
if (detail?.id === row.id) {
|
||||
await openDetail(await huyaApi.getCollabOrder(row.id));
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setQuerying(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 勾选批量查询(串行带间隔,防风控) */
|
||||
const queryActivitySelected = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
setQuerying(true);
|
||||
try {
|
||||
const result = await huyaApi.batchQueryCollabOrderActivity(selectedIds);
|
||||
modal.info({
|
||||
title: `查询完成:成功 ${result.success} / 失败 ${result.failed} / 无法判定 ${result.unknown}`
|
||||
+ (result.auto_changed ? `(${result.auto_changed} 条按结果自动改状态)` : ''),
|
||||
width: 760,
|
||||
content: (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{result.results.map((item) => (
|
||||
<Text key={item.order_id} type={item.verdict === 'failed' ? 'danger' : undefined}>
|
||||
{item.order_no}:{item.verdict_label} · {item.message}
|
||||
{item.auto_status ? `;状态已改为「${item.auto_status_label}」` : ''}
|
||||
</Text>
|
||||
))}
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
void load();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setQuerying(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 详情里的「账号全生命周期」时间线 */
|
||||
const loadMetrics = async (orderId: number) => {
|
||||
setMetrics([]);
|
||||
setMetricsLoading(true);
|
||||
try {
|
||||
setMetrics(await huyaApi.collabOrderMetrics(orderId));
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setMetricsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const patchOrder = async (row: HuyaCollabOrder, payload: HuyaCollabOrderUpdate) => {
|
||||
try {
|
||||
const updated = await huyaApi.updateCollabOrder(row.id, { ...payload, expected_version: row.version });
|
||||
@@ -433,6 +570,7 @@ export default function HuyaCollabPage() {
|
||||
});
|
||||
setTimeline([]);
|
||||
setTimelineLoading(true);
|
||||
void loadMetrics(row.id);
|
||||
try {
|
||||
setTimeline(await huyaApi.collabOrderTimeline(row.id));
|
||||
} catch (e: unknown) {
|
||||
@@ -523,22 +661,36 @@ export default function HuyaCollabPage() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 单行状态弹窗的提交(三种模式共用):
|
||||
* - `void`:作废(必须填原因);
|
||||
* - `rollback`:回退到选定状态(`rollback=true`,会清掉后面阶段的时间戳);
|
||||
* - `set`:人工往前切,允许跳步(如 待处理 → 已上脚本),不用勾回退。
|
||||
*/
|
||||
const submitStatusAction = async () => {
|
||||
if (!statusAction) return;
|
||||
const { order, mode, target } = statusAction;
|
||||
if (mode === 'void' && !statusReason.trim()) {
|
||||
const nextStatus: HuyaCollabStatus = mode === 'void' ? 'void' : (target ?? rollbackTarget);
|
||||
// 服务层对「作废」一律要求原因(不管从哪个入口进来的)
|
||||
if (nextStatus === 'void' && !statusReason.trim()) {
|
||||
message.warning('作废必须填原因');
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
applyUpdated(await huyaApi.updateCollabOrderStatus(order.id, {
|
||||
status: mode === 'void' ? 'void' : (target ?? rollbackTarget),
|
||||
status: nextStatus,
|
||||
reason: statusReason,
|
||||
rollback: mode === 'rollback',
|
||||
expected_version: order.version,
|
||||
}));
|
||||
message.success(mode === 'void' ? '已作废' : '已回退');
|
||||
message.success(
|
||||
mode === 'void'
|
||||
? '已作废'
|
||||
: mode === 'rollback'
|
||||
? '已回退'
|
||||
: `已改为「${STATUS_META[nextStatus].label}」`,
|
||||
);
|
||||
setStatusAction(null);
|
||||
setStatusReason('');
|
||||
void load();
|
||||
@@ -929,6 +1081,24 @@ export default function HuyaCollabPage() {
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '积分', width: 130,
|
||||
render: (_, row) => {
|
||||
if (!row.last_metric_at) {
|
||||
return <Text type="secondary" style={{ fontSize: 12 }}>还没查</Text>;
|
||||
}
|
||||
const verdict = VERDICT_META[row.last_verdict];
|
||||
return (
|
||||
<Tooltip title={`${row.last_metric_message || ''}(${formatTime(row.last_metric_at)})`}>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong>{row.last_score ?? '-'}</Text>
|
||||
{verdict && <Tag color={verdict.color} style={{ marginTop: 2 }}>{verdict.label}</Tag>}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{formatTime(row.last_metric_at)}</Text>
|
||||
</Space>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后改动', width: 160,
|
||||
render: (_, row) => (
|
||||
@@ -948,10 +1118,12 @@ export default function HuyaCollabPage() {
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'verify', label: '验证 CK(只读探针)', icon: <SafetyCertificateOutlined /> },
|
||||
{ key: 'activity', label: '查积分 / 兑换记录', icon: <SyncOutlined />, disabled: !row.has_cookie },
|
||||
{ key: 'ck', label: '贴 / 换 CK', icon: <LinkOutlined /> },
|
||||
{ key: 'rebuild', label: '从 App 原始凭据重建', icon: <ReloadOutlined />, disabled: !row.has_raw_cookie },
|
||||
{ key: 'disable', label: row.ck_status === 'disabled' ? '恢复这份 CK' : '停用这份 CK', icon: <StopOutlined /> },
|
||||
{ type: 'divider' },
|
||||
{ key: 'set', label: '改状态…', icon: <CheckCircleOutlined /> },
|
||||
{ key: 'rollback', label: '回退状态…', icon: <UndoOutlined /> },
|
||||
{ key: 'void', label: '作废…', icon: <StopOutlined />, danger: true },
|
||||
...(canManage
|
||||
@@ -960,9 +1132,15 @@ export default function HuyaCollabPage() {
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'verify') void verifyOne(row);
|
||||
if (key === 'activity') void queryActivityOne(row);
|
||||
if (key === 'ck') setCkTarget({ order: row, text: '' });
|
||||
if (key === 'rebuild') setCkTarget({ order: row, text: '' });
|
||||
if (key === 'disable') void toggleCkDisabled(row);
|
||||
if (key === 'set') {
|
||||
setRollbackTarget(row.next_status ?? row.status);
|
||||
setStatusReason('');
|
||||
setStatusAction({ order: row, mode: 'set' });
|
||||
}
|
||||
if (key === 'rollback') {
|
||||
setRollbackTarget('pending');
|
||||
setStatusReason('');
|
||||
@@ -993,32 +1171,40 @@ export default function HuyaCollabPage() {
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
{/* 卡片用 flex 平分:状态多了一个「兑换失败」,固定 span=4 会换行参差不齐 */}
|
||||
<Row gutter={12}>
|
||||
<Col span={4}><Card size="small"><Statistic title="全部" value={summary?.total ?? 0} /></Card></Col>
|
||||
<Col span={4}>
|
||||
<Col flex="1"><Card size="small"><Statistic title="全部" value={summary?.total ?? 0} /></Card></Col>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title="待处理" value={summary?.by_status.pending ?? 0} valueStyle={{ color: '#faad14' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title="已绑定" value={summary?.by_status.bound ?? 0} valueStyle={{ color: '#1677ff' }} />
|
||||
<Statistic title={STATUS_META.bound.label} value={summary?.by_status.bound ?? 0}
|
||||
valueStyle={{ color: '#1677ff' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title="已充值 / 已上脚本"
|
||||
value={(summary?.by_status.recharged ?? 0) + (summary?.by_status.scripted ?? 0)}
|
||||
valueStyle={{ color: '#722ed1' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title="已完成" value={finished} valueStyle={{ color: '#52c41a' }} />
|
||||
<Progress percent={summary?.total ? Math.round((finished / summary.total) * 100) : 0} size="small" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title={STATUS_META.failed.label} value={summary?.by_status.failed ?? 0}
|
||||
valueStyle={{ color: '#ff4d4f' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title="CK 失效" value={summary?.ck_invalid ?? 0} valueStyle={{ color: '#ff4d4f' }} />
|
||||
</Card>
|
||||
@@ -1068,6 +1254,35 @@ export default function HuyaCollabPage() {
|
||||
批量验证 CK{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
// 勾选多行一次改状态(往前可跳步;回退在弹窗里勾「回退」)
|
||||
<Dropdown
|
||||
disabled={selectedIds.length === 0 || busy}
|
||||
menu={{
|
||||
items: STATUS_ORDER_OPTIONS.map((option) => ({
|
||||
key: option.value,
|
||||
label: option.label,
|
||||
})),
|
||||
onClick: ({ key }) => {
|
||||
setBatchStatusTarget(key as HuyaCollabStatus);
|
||||
setBatchStatusReason('');
|
||||
setBatchStatusRollback(false);
|
||||
setBatchStatusOpen(true);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button icon={<SwapOutlined />} disabled={selectedIds.length === 0 || busy}>
|
||||
批量改状态{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button icon={<SyncOutlined />} disabled={selectedIds.length === 0 || busy}
|
||||
loading={querying}
|
||||
onClick={() => void queryActivitySelected()}>
|
||||
批量查积分/兑换{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button icon={<FileExcelOutlined />} onClick={() => { setImportResult(null); setImportOpen(true); }}>
|
||||
粘贴导入
|
||||
@@ -1280,13 +1495,22 @@ export default function HuyaCollabPage() {
|
||||
)}
|
||||
|
||||
{canEdit && (
|
||||
<Space>
|
||||
<Space wrap>
|
||||
{detail.next_status && (
|
||||
<Button icon={<CheckCircleOutlined />} disabled={busy}
|
||||
onClick={() => void advance(detail)}>
|
||||
标记为「{STATUS_META[detail.next_status].label}」
|
||||
</Button>
|
||||
)}
|
||||
<Button icon={<SwapOutlined />} disabled={busy}
|
||||
onClick={() => {
|
||||
// 默认给「下一步」,终点态(已完成 / 已作废 / 兑换失败)退回当前状态,让人自己挑
|
||||
setRollbackTarget(detail.next_status ?? detail.status);
|
||||
setStatusReason('');
|
||||
setStatusAction({ order: detail, mode: 'set' });
|
||||
}}>
|
||||
改状态…
|
||||
</Button>
|
||||
<Button icon={<UndoOutlined />} disabled={busy}
|
||||
onClick={() => { setRollbackTarget('pending'); setStatusReason(''); setStatusAction({ order: detail, mode: 'rollback' }); }}>
|
||||
回退状态
|
||||
@@ -1321,6 +1545,68 @@ export default function HuyaCollabPage() {
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Divider style={{ margin: '4px 0' }}>账号全生命周期(积分 / 兑换记录)</Divider>
|
||||
<Space wrap size={8}>
|
||||
{canEdit && (
|
||||
<Button icon={<SyncOutlined />} disabled={busy || querying || !detail.has_cookie}
|
||||
loading={querying}
|
||||
onClick={() => void queryActivityOne(detail)}>
|
||||
查一次积分 / 兑换记录
|
||||
</Button>
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
每次查询都会追加一条快照(只追加、不修改);「已上脚本」的单会按结论自动改成「已完成」或「兑换失败」。
|
||||
</Text>
|
||||
</Space>
|
||||
{!detail.has_cookie && (
|
||||
<Text type="warning" style={{ fontSize: 12 }}>这单还没贴 CK,查不了积分 / 兑换记录。</Text>
|
||||
)}
|
||||
{metrics.length === 0 && !metricsLoading && (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="还没有查询快照" />
|
||||
)}
|
||||
<Timeline
|
||||
pending={metricsLoading ? '查询中…' : undefined}
|
||||
items={metrics.map((item) => ({
|
||||
color: item.verdict === 'success' ? 'green' : item.verdict === 'failed' ? 'red' : 'gray',
|
||||
children: (
|
||||
<Space direction="vertical" size={2} style={{ width: '100%' }}>
|
||||
<Space size={6} wrap>
|
||||
<Tag color={VERDICT_META[item.verdict]?.color}>{item.verdict_label}</Tag>
|
||||
<Text strong>积分 {item.score ?? '-'}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
获得 {item.gained_score ?? '-'} / 消耗 {item.used_score ?? '-'}
|
||||
{' · '}兑换记录 {item.records_total} 条(命中 {item.records_matched})
|
||||
</Text>
|
||||
</Space>
|
||||
{item.message && (
|
||||
<Text type={item.verdict === 'failed' ? 'danger' : 'secondary'} style={{ fontSize: 12 }}>
|
||||
{item.message}
|
||||
</Text>
|
||||
)}
|
||||
{item.matched_preview.length > 0 && (
|
||||
<Space direction="vertical" size={0}>
|
||||
{item.matched_preview.map((matched, index) => (
|
||||
<Text key={`${matched.order_id}-${index}`} style={{ fontSize: 12 }}>
|
||||
<Text code style={{ fontSize: 12 }}>{matched.prize_name || '(无礼包名)'}</Text>
|
||||
{' '}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{matched.status_label}
|
||||
{matched.order_id ? ` · 兑换单 ${matched.order_id}` : ''}
|
||||
{matched.exchange_time ? ` · ${formatTime(new Date(matched.exchange_time * 1000).toISOString())}` : ''}
|
||||
</Text>
|
||||
</Text>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{formatTime(item.created_at)} · {item.created_by_name || '系统'}
|
||||
{' · '}{item.source === 'batch' ? '批量查询' : '手动查询'}
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
</Drawer>
|
||||
@@ -1576,34 +1862,77 @@ export default function HuyaCollabPage() {
|
||||
<Modal
|
||||
title={statusAction?.mode === 'void'
|
||||
? `作废订单 ${statusAction.order.order_no}`
|
||||
: `回退订单 ${statusAction?.order.order_no ?? ''}`}
|
||||
: statusAction?.mode === 'set'
|
||||
? `改状态:订单 ${statusAction.order.order_no}`
|
||||
: `回退订单 ${statusAction?.order.order_no ?? ''}`}
|
||||
open={!!statusAction}
|
||||
onCancel={() => setStatusAction(null)}
|
||||
onOk={() => void submitStatusAction()}
|
||||
confirmLoading={busy}
|
||||
okButtonProps={{ danger: statusAction?.mode === 'void' }}
|
||||
okText={statusAction?.mode === 'void' ? '作废' : '回退'}
|
||||
okText={statusAction?.mode === 'void' ? '作废' : statusAction?.mode === 'set' ? '改状态' : '回退'}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
{statusAction?.mode === 'rollback' && (
|
||||
{statusAction && statusAction.mode !== 'void' && (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={rollbackTarget}
|
||||
onChange={(value: HuyaCollabStatus) => setRollbackTarget(value)}
|
||||
options={(['pending', 'bound', 'recharged', 'scripted'] as HuyaCollabStatus[]).map((key) => ({
|
||||
value: key, label: STATUS_META[key].label,
|
||||
}))}
|
||||
options={statusAction.mode === 'set' ? STATUS_ORDER_OPTIONS : FLOW_STATUS_OPTIONS}
|
||||
/>
|
||||
)}
|
||||
<Input.TextArea rows={2} value={statusReason} onChange={(event) => setStatusReason(event.target.value)}
|
||||
placeholder={statusAction?.mode === 'void' ? '作废原因(必填)' : '回退说明(可选)'} />
|
||||
placeholder={
|
||||
statusAction?.mode === 'void'
|
||||
? '作废原因(必填)'
|
||||
: rollbackTarget === 'void'
|
||||
? '作废原因(必填)'
|
||||
: statusAction?.mode === 'rollback'
|
||||
? '回退说明(可选)'
|
||||
: '改状态说明(可选)'
|
||||
} />
|
||||
{statusAction?.mode === 'set' && (
|
||||
<Text type="secondary">
|
||||
人工切换允许往前跳步(比如 待处理 → 已上脚本),不用一步一步点;
|
||||
切到「已确认绑定」要求先有绑定角色 + 主页截图,切到「已作废」必须填原因。
|
||||
</Text>
|
||||
)}
|
||||
{statusAction?.mode === 'rollback' && (
|
||||
<Text type="secondary">回退会清掉后面阶段的时间戳,并在审计里留痕。</Text>
|
||||
)}
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`批量改状态(已选 ${selectedIds.length} 条)`}
|
||||
open={batchStatusOpen}
|
||||
onCancel={() => setBatchStatusOpen(false)}
|
||||
onOk={() => void submitBatchStatus()}
|
||||
confirmLoading={busy}
|
||||
okText={`改成「${STATUS_META[batchStatusTarget]?.label ?? ''}」`}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={batchStatusTarget}
|
||||
onChange={(value: HuyaCollabStatus) => setBatchStatusTarget(value)}
|
||||
options={STATUS_ORDER_OPTIONS}
|
||||
/>
|
||||
<Input.TextArea rows={2} value={batchStatusReason}
|
||||
onChange={(event) => setBatchStatusReason(event.target.value)}
|
||||
placeholder={batchStatusTarget === 'void' ? '作废原因(必填)' : '改状态说明(可选)'} />
|
||||
<Checkbox checked={batchStatusRollback} onChange={(event) => setBatchStatusRollback(event.target.checked)}>
|
||||
回退(目标不是「往前」时必须勾选,比如 已上脚本 → 待处理)
|
||||
</Checkbox>
|
||||
<Text type="secondary">
|
||||
逐行独立提交:某一行前置校验没过(例如没主页截图进不了「已确认绑定」)只跳过那一行,
|
||||
结果会在弹窗里逐条列出来。一次最多 50 行。
|
||||
</Text>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<HuyaCollabShopModal
|
||||
open={shopModalOpen}
|
||||
shops={shops}
|
||||
|
||||
Reference in New Issue
Block a user