feat(huya): 协作台账勾选导出 CK 到剪贴板(单点权限 + 逐行审计)+ 工具区固定两行
This commit is contained in:
@@ -34,6 +34,7 @@ import type {
|
||||
HuyaCollabAuditItem,
|
||||
HuyaCollabActivityBatchResult,
|
||||
HuyaCollabActivityItem,
|
||||
HuyaCollabCkExportResult,
|
||||
HuyaCollabCkPreview,
|
||||
HuyaCollabCkRequest,
|
||||
HuyaCollabCkStatusRequest,
|
||||
@@ -277,6 +278,12 @@ export const huyaApi = {
|
||||
/** 这一单的「账号全生命周期」时间线(每次查询的快照,新→旧) */
|
||||
collabOrderMetrics: (id: number) =>
|
||||
api.get<HuyaCollabMetric[], HuyaCollabMetric[]>(`/huya/external-orders/${id}/metrics`),
|
||||
/** 勾选导出 CK 明文(前端写剪贴板;单独权限点 huya:collab:ck_export,逐行写审计) */
|
||||
exportCollabOrderCk: (ids: number[]) =>
|
||||
api.post<HuyaCollabCkExportResult, HuyaCollabCkExportResult>(
|
||||
'/huya/external-orders/ck/export',
|
||||
{ ids },
|
||||
),
|
||||
/** 删除一行(只给运营:huya:collab:manage) */
|
||||
deleteCollabOrder: (id: number) =>
|
||||
api.delete<{ success: boolean; order_no: string }, { success: boolean; order_no: string }>(
|
||||
|
||||
@@ -1394,6 +1394,34 @@ export interface HuyaCollabStatusBatchResult {
|
||||
results: HuyaCollabStatusBatchItem[];
|
||||
}
|
||||
|
||||
/** 导出 CK 里的一行(ck_text 是**明文凭据**,只有导出接口会返回) */
|
||||
export interface HuyaCollabCkExportItem {
|
||||
id: number;
|
||||
order_no: string;
|
||||
uid: string;
|
||||
nickname: string;
|
||||
status: HuyaCollabStatus;
|
||||
status_label: string;
|
||||
ck_status: string;
|
||||
ck_status_label: string;
|
||||
ck_text: string;
|
||||
}
|
||||
|
||||
/** 没导出的行(例如这行还没贴 CK)——原因显式回给界面,不静默丢 */
|
||||
export interface HuyaCollabCkExportSkipped {
|
||||
id: number;
|
||||
order_no: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** 勾选导出 CK 的结果:exported + skipped.length = total */
|
||||
export interface HuyaCollabCkExportResult {
|
||||
total: number;
|
||||
exported: number;
|
||||
skipped: HuyaCollabCkExportSkipped[];
|
||||
items: HuyaCollabCkExportItem[];
|
||||
}
|
||||
|
||||
export interface HuyaCollabOrderList {
|
||||
items: HuyaCollabOrder[];
|
||||
total: number;
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert, App, AutoComplete, Button, Card, Checkbox, Col, Descriptions, Divider, Drawer, Dropdown,
|
||||
Empty, Form, Image, Input, Modal, Popconfirm, Popover, Progress, Row, Select, Space, Statistic,
|
||||
Empty, Form, Image, Input, Modal, Popconfirm, Popover, Progress, Radio, Row, Select, Space, Statistic,
|
||||
Switch, Table, Tag, Timeline, Tooltip, Typography, Upload,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined, CloudUploadOutlined, DeleteOutlined, EditOutlined, FileExcelOutlined,
|
||||
CheckCircleOutlined, CloudUploadOutlined, CopyOutlined, DeleteOutlined, EditOutlined, FileExcelOutlined,
|
||||
LinkOutlined, PlusOutlined, ReloadOutlined, SafetyCertificateOutlined, ShopOutlined,
|
||||
StopOutlined, SwapOutlined, SyncOutlined, UndoOutlined,
|
||||
} from '@ant-design/icons';
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
fileApi,
|
||||
huyaApi,
|
||||
type HuyaCollabAuditItem,
|
||||
type HuyaCollabCkExportResult,
|
||||
type HuyaCollabCkPreview,
|
||||
type HuyaCollabGift,
|
||||
type HuyaCollabImageSlot,
|
||||
@@ -39,6 +40,7 @@ import HuyaCollabShopPicker from '../components/HuyaCollabShopPicker';
|
||||
import HuyaCollabHomeImageUploader from '../components/HuyaCollabHomeImageUploader';
|
||||
import { message } from '../utils/antdMessage';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
import { copyTextToClipboard } from '../utils/clipboard';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
@@ -275,6 +277,8 @@ export default function HuyaCollabPage() {
|
||||
const { can, user } = usePermissions();
|
||||
const canEdit = can('huya:collab:edit');
|
||||
const canManage = can('huya:collab:manage');
|
||||
// 导出 CK 是把凭据带出系统,单独一个权限点(默认只给运营,见 permissions.py)
|
||||
const canExportCk = can('huya:collab:ck_export');
|
||||
|
||||
const [rows, setRows] = useState<HuyaCollabOrder[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -336,6 +340,11 @@ export default function HuyaCollabPage() {
|
||||
const [querying, setQuerying] = useState(false);
|
||||
const [metrics, setMetrics] = useState<HuyaCollabMetric[]>([]);
|
||||
const [metricsLoading, setMetricsLoading] = useState(false);
|
||||
// 勾选导出 CK(明文只在这一个弹窗里出现;复制走剪贴板)
|
||||
const [ckExportOpen, setCkExportOpen] = useState(false);
|
||||
const [ckExporting, setCkExporting] = useState(false);
|
||||
const [ckExport, setCkExport] = useState<HuyaCollabCkExportResult | null>(null);
|
||||
const [ckExportFormat, setCkExportFormat] = useState<'plain' | 'with_order'>('plain');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -549,6 +558,60 @@ export default function HuyaCollabPage() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 勾选导出 CK:先把明文拉回来(后端逐行写审计),再让用户复制。
|
||||
*
|
||||
* 明文只活在这次弹窗的 state 里:不进 localStorage、不写 URL、
|
||||
* 关掉弹窗就清掉(`closeCkExport()`)。
|
||||
*/
|
||||
const openCkExport = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
setCkExporting(true);
|
||||
try {
|
||||
const result = await huyaApi.exportCollabOrderCk(selectedIds);
|
||||
setCkExport(result);
|
||||
setCkExportOpen(true);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setCkExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeCkExport = () => {
|
||||
setCkExportOpen(false);
|
||||
setCkExport(null);
|
||||
};
|
||||
|
||||
/** 导出文本:默认每行一份纯 CK(直接粘),也可以带上订单号(多选时好对号) */
|
||||
const ckExportText = useMemo(() => {
|
||||
if (!ckExport) return '';
|
||||
return ckExport.items
|
||||
.map((item) => (
|
||||
ckExportFormat === 'with_order' ? `${item.order_no}\t${item.ck_text}` : item.ck_text
|
||||
))
|
||||
.join('\n');
|
||||
}, [ckExport, ckExportFormat]);
|
||||
|
||||
// 已失效 / 已停用的 CK 照样导出(用户可能就是要看它),但得在界面上点名
|
||||
const ckExportRisky = useMemo(
|
||||
() => (ckExport?.items ?? []).filter(
|
||||
(item) => item.ck_status === 'invalid' || item.ck_status === 'disabled',
|
||||
),
|
||||
[ckExport],
|
||||
);
|
||||
|
||||
const copyCkExport = async () => {
|
||||
if (!ckExport?.exported) return;
|
||||
try {
|
||||
await copyTextToClipboard(ckExportText);
|
||||
message.success(`已复制 ${ckExport.exported} 份 CK 到剪贴板`);
|
||||
} catch (e: unknown) {
|
||||
// 复制失败别吞:明文就在下面的框里,让人手动选中复制
|
||||
message.error(`复制失败:${getErrorMessage(e)}(可以在下面的框里手动选中复制)`);
|
||||
}
|
||||
};
|
||||
|
||||
const patchOrder = async (row: HuyaCollabOrder, payload: HuyaCollabOrderUpdate) => {
|
||||
try {
|
||||
const updated = await huyaApi.updateCollabOrder(row.id, { ...payload, expected_version: row.version });
|
||||
@@ -1213,8 +1276,13 @@ export default function HuyaCollabPage() {
|
||||
</Row>
|
||||
|
||||
<Card size="small">
|
||||
<Space wrap style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Space wrap>
|
||||
{/*
|
||||
固定两行:第一行永远是筛选,第二行永远是动作按钮组 —— 不要用「筛选 + 动作同一个 wrap」
|
||||
那种排法:不勾选时两组挤成一行,勾选后按钮多了「(N)」就会被顶到第二行,
|
||||
列表跟着往下跳(2026-10-16 用户反馈「不要选中后两行」)。
|
||||
*/}
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<Space wrap style={{ width: '100%' }}>
|
||||
<Space size={4}>
|
||||
<Text type="secondary">店铺</Text>
|
||||
<HuyaCollabShopPicker
|
||||
@@ -1247,7 +1315,8 @@ export default function HuyaCollabPage() {
|
||||
onChange={(checked) => { setPage(1); setFilters((prev) => ({ ...prev, include_void: checked })); }} />
|
||||
</Space>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
{/* 第二行:动作(固定在这一行;勾选后加「(N)」只是按钮变宽,不会把列表顶下去) */}
|
||||
<Space wrap style={{ width: '100%' }}>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void load()} disabled={loading}>刷新</Button>
|
||||
{canEdit && (
|
||||
<Button icon={<SafetyCertificateOutlined />} disabled={selectedIds.length === 0 || busy}
|
||||
@@ -1284,6 +1353,14 @@ export default function HuyaCollabPage() {
|
||||
批量查积分/兑换{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
)}
|
||||
{canExportCk && (
|
||||
// 导出 CK 到剪贴板(勾选几行就导几行;明文只在这个弹窗里出现)
|
||||
<Button icon={<CopyOutlined />} disabled={selectedIds.length === 0 || busy}
|
||||
loading={ckExporting}
|
||||
onClick={() => void openCkExport()}>
|
||||
导出 CK{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button icon={<FileExcelOutlined />} onClick={() => { setImportResult(null); setImportOpen(true); }}>
|
||||
粘贴导入
|
||||
@@ -1934,6 +2011,58 @@ export default function HuyaCollabPage() {
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`导出 CK(勾选 ${ckExport?.total ?? 0} 条,可导出 ${ckExport?.exported ?? 0} 条)`}
|
||||
open={ckExportOpen}
|
||||
onCancel={closeCkExport}
|
||||
width={760}
|
||||
footer={[
|
||||
<Button key="close" onClick={closeCkExport}>关闭</Button>,
|
||||
<Button key="copy" type="primary" icon={<CopyOutlined />}
|
||||
disabled={!ckExport?.exported}
|
||||
onClick={() => void copyCkExport()}>
|
||||
复制到剪贴板
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="CK 是账号凭据"
|
||||
description="复制后请勿发群 / 截图外传;每次导出都会写审计(谁、什么时候、导出了哪几单),审计里不存 CK 本体。"
|
||||
/>
|
||||
{ckExport && ckExport.skipped.length > 0 && (
|
||||
<Text type="warning" style={{ fontSize: 12 }}>
|
||||
跳过 {ckExport.skipped.length} 条:
|
||||
{ckExport.skipped.map((item) => `${item.order_no}(${item.reason})`).join(';')}
|
||||
</Text>
|
||||
)}
|
||||
{ckExportRisky.length > 0 && (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
其中 {ckExportRisky.length} 条的 CK 已失效 / 已停用:
|
||||
{ckExportRisky.map((item) => `${item.order_no}(${item.ck_status_label})`).join(';')}
|
||||
</Text>
|
||||
)}
|
||||
<Radio.Group value={ckExportFormat} onChange={(event) => setCkExportFormat(event.target.value)}>
|
||||
<Radio value="plain">每行一份纯 CK(直接粘)</Radio>
|
||||
<Radio value="with_order">订单号 + Tab + CK(多选时好对号)</Radio>
|
||||
</Radio.Group>
|
||||
<Input.TextArea
|
||||
rows={10}
|
||||
value={ckExportText}
|
||||
readOnly
|
||||
placeholder="没有可导出的 CK"
|
||||
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12 }}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
共 {ckExport?.exported ?? 0} 份 · {ckExportText.length} 字符;
|
||||
关掉这个弹窗就清掉,不会留在页面上。
|
||||
</Text>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<HuyaCollabShopModal
|
||||
open={shopModalOpen}
|
||||
shops={shops}
|
||||
|
||||
Reference in New Issue
Block a user