= {
+ bind_images: { label: '绑定图', keysKey: 'bind_images' },
+ home_images: { label: '主页账号图', keysKey: 'home_images' },
+};
+
+const IMPORT_PREVIEW_ROWS = 20;
+const MAX_IMAGES_PER_SLOT = 20;
+
+function formatTime(value: string | null | undefined): string {
+ if (!value) return '-';
+ const time = new Date(value);
+ return Number.isNaN(time.getTime()) ? String(value) : time.toLocaleString();
+}
+
+/** 点开即改、失焦提交(回车=失焦提交,Esc=放弃) */
+function EditableText({
+ value, disabled, placeholder, onCommit,
+}: {
+ value: string;
+ disabled?: boolean;
+ placeholder?: string;
+ onCommit: (next: string) => void;
+}) {
+ const [editing, setEditing] = useState(false);
+ const [draft, setDraft] = useState(value);
+
+ useEffect(() => {
+ setDraft(value);
+ setEditing(false);
+ }, [value]);
+
+ if (!editing || disabled) {
+ return (
+ { if (!disabled) setEditing(true); }}
+ style={{ cursor: disabled ? 'default' : 'text', minHeight: 22 }}
+ title={disabled ? undefined : '点击编辑'}
+ >
+ {value || {placeholder || '—'}}
+
+ );
+ }
+
+ return (
+ setDraft(event.target.value)}
+ onBlur={() => {
+ setEditing(false);
+ if (draft !== value) onCommit(draft);
+ }}
+ onPressEnter={(event) => event.currentTarget.blur()}
+ onKeyDown={(event) => {
+ if (event.key === 'Escape') {
+ setDraft(value);
+ setEditing(false);
+ event.currentTarget.blur();
+ }
+ }}
+ />
+ );
+}
+
+/** 两个图片槽位(object_key 数组,第一张是封面) */
+function ImageSlotEditor({
+ order, slot, canEdit, busy, onUpload, onRemove, onSetCover,
+}: {
+ order: HuyaCollabOrder;
+ slot: HuyaCollabImageSlot;
+ canEdit: boolean;
+ busy: boolean;
+ onUpload: (slot: HuyaCollabImageSlot, file: File) => void;
+ onRemove: (slot: HuyaCollabImageSlot, key: string) => void;
+ onSetCover: (slot: HuyaCollabImageSlot, key: string) => void;
+}) {
+ const keys = slot === 'bind_images' ? order.bind_images : order.home_images;
+ const urls = slot === 'bind_images' ? order.bind_image_urls : order.home_image_urls;
+ const thumbs = slot === 'bind_images' ? order.bind_image_thumb_urls : order.home_image_thumb_urls;
+ const full = keys.length >= MAX_IMAGES_PER_SLOT;
+
+ return (
+
+
+
+ {keys.length === 0 && 还没有图}
+ {keys.length > 0 && (
+
+ {keys.map((key, index) => (
+
+
+
+ {index === 0 && (
+
+ 封面
+
+ )}
+
+ {canEdit && (
+
+ {index !== 0 && (
+
+ }
+ disabled={busy} onClick={() => onSetCover(slot, key)} />
+
+ )}
+ onRemove(slot, key)} disabled={busy}>
+
+ } disabled={busy} />
+
+
+
+ )}
+
+ ))}
+
+ )}
+
+ {canEdit && (
+ { onUpload(slot, file as File); return false; }}
+ >
+ } disabled={busy || full} loading={busy}>
+ {full ? `最多 ${MAX_IMAGES_PER_SLOT} 张` : '上传图片'}
+
+
+ )}
+
+
+ );
+}
+
+export default function HuyaCollabPage() {
+ const { modal } = App.useApp();
+ const { can } = usePermissions();
+ const canEdit = can('huya:collab:edit');
+ const canManage = can('huya:collab:manage');
+
+ const [rows, setRows] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(50);
+ const [loading, setLoading] = useState(false);
+ const [busy, setBusy] = useState(false);
+ const [summary, setSummary] = useState(null);
+ const [owners, setOwners] = useState([]);
+ const [shops, setShops] = useState([]);
+ const [filters, setFilters] = useState({ include_void: false });
+ const [selectedIds, setSelectedIds] = useState([]);
+
+ const [detail, setDetail] = useState(null);
+ const [timeline, setTimeline] = useState([]);
+ const [timelineLoading, setTimelineLoading] = useState(false);
+ const [detailForm] = Form.useForm();
+
+ const [createOpen, setCreateOpen] = useState(false);
+ const [createForm] = Form.useForm>();
+ const [importOpen, setImportOpen] = useState(false);
+ const [importText, setImportText] = useState('');
+ const [importing, setImporting] = useState(false);
+ const [importResult, setImportResult] = useState<{
+ created: number; updated: number; skipped: number;
+ errors: { line: number; order_no: string; message: string }[];
+ } | null>(null);
+
+ const [ckTarget, setCkTarget] = useState<{ order: HuyaCollabOrder; text: string } | null>(null);
+ const [statusAction, setStatusAction] = useState<{
+ order: HuyaCollabOrder; mode: 'void' | 'rollback'; target?: HuyaCollabStatus;
+ } | null>(null);
+ const [statusReason, setStatusReason] = useState('');
+ const [rollbackTarget, setRollbackTarget] = useState('pending');
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const [list, nextSummary, nextOwners, nextShops] = await Promise.all([
+ huyaApi.listCollabOrders({ ...filters, page, page_size: pageSize }),
+ huyaApi.collabOrdersSummary(),
+ huyaApi.collabOrderOwners(),
+ huyaApi.collabOrderShops(),
+ ]);
+ setRows(list.items);
+ setTotal(list.total);
+ setSummary(nextSummary);
+ setOwners(nextOwners);
+ setShops(nextShops);
+ // 选中是「当前页」的概念:刷新后剔除已经不在这页上的行,避免点到别人的脏 id。
+ setSelectedIds((ids) => ids.filter((id) => list.items.some((row) => row.id === id)));
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setLoading(false);
+ }
+ }, [filters, page, pageSize]);
+
+ useEffect(() => { void load(); }, [load]);
+
+ const applyUpdated = useCallback((updated: HuyaCollabOrder) => {
+ setRows((list) => list.map((row) => (row.id === updated.id ? updated : row)));
+ setDetail((current) => (current && current.id === updated.id ? updated : current));
+ }, []);
+
+ const patchOrder = async (row: HuyaCollabOrder, payload: HuyaCollabOrderUpdate) => {
+ try {
+ const updated = await huyaApi.updateCollabOrder(row.id, { ...payload, expected_version: row.version });
+ applyUpdated(updated);
+ message.success('已保存');
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ void load();
+ }
+ };
+
+ const openDetail = async (row: HuyaCollabOrder) => {
+ setDetail(row);
+ detailForm.setFieldsValue({
+ order_no: row.order_no,
+ shop: row.shop,
+ owner_id: row.owner_id,
+ bind_douyu_account: row.bind_douyu_account,
+ gift_name: row.gift_name,
+ remark: row.remark,
+ });
+ setTimeline([]);
+ setTimelineLoading(true);
+ try {
+ setTimeline(await huyaApi.collabOrderTimeline(row.id));
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setTimelineLoading(false);
+ }
+ };
+
+ const verifyOne = async (row: HuyaCollabOrder) => {
+ setBusy(true);
+ try {
+ const updated = await huyaApi.verifyCollabOrder(row.id);
+ applyUpdated(updated);
+ if (updated.ck_status === 'valid') {
+ message.success(
+ updated.bind_game_role
+ ? `CK 有效:绑定角色 ${updated.bind_game_role}`
+ : 'CK 有效',
+ );
+ } else {
+ message.warning(updated.ck_check_message || 'CK 验证未通过');
+ }
+ void load();
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const verifySelected = async () => {
+ if (selectedIds.length === 0) return;
+ setBusy(true);
+ try {
+ const result = await huyaApi.verifyCollabOrders(selectedIds);
+ const failures = result.results.filter((item) => !item.ok);
+ if (failures.length === 0) {
+ message.success(`验证完成:${result.valid} 条全部有效`);
+ } else {
+ modal.info({
+ title: `验证完成:有效 ${result.valid} / 未通过 ${result.failed}`,
+ width: 640,
+ content: (
+
+ {failures.map((item) => (
+
+ {item.order_no}:{item.message}
+
+ ))}
+
+ ),
+ });
+ }
+ setSelectedIds([]);
+ void load();
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const advance = async (row: HuyaCollabOrder) => {
+ if (!row.next_status) return;
+ setBusy(true);
+ try {
+ applyUpdated(await huyaApi.updateCollabOrderStatus(row.id, {
+ status: row.next_status,
+ expected_version: row.version,
+ }));
+ message.success(`已标记为「${STATUS_META[row.next_status].label}」`);
+ void load();
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ void load();
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const submitStatusAction = async () => {
+ if (!statusAction) return;
+ const { order, mode, target } = statusAction;
+ if (mode === 'void' && !statusReason.trim()) {
+ message.warning('作废必须填原因');
+ return;
+ }
+ setBusy(true);
+ try {
+ applyUpdated(await huyaApi.updateCollabOrderStatus(order.id, {
+ status: mode === 'void' ? 'void' : (target ?? rollbackTarget),
+ reason: statusReason,
+ rollback: mode === 'rollback',
+ expected_version: order.version,
+ }));
+ message.success(mode === 'void' ? '已作废' : '已回退');
+ setStatusAction(null);
+ setStatusReason('');
+ void load();
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const submitCk = async (action: 'paste' | 'rebuild') => {
+ if (!ckTarget) return;
+ if (action === 'paste' && !ckTarget.text.trim()) {
+ message.warning('请先贴一份 CK');
+ return;
+ }
+ setBusy(true);
+ try {
+ const updated = await huyaApi.updateCollabOrderCk(ckTarget.order.id, {
+ ck_text: ckTarget.text,
+ action,
+ expected_version: ckTarget.order.version,
+ });
+ applyUpdated(updated);
+ message.success(action === 'rebuild' ? '已从 App 原始凭据重建' : 'CK 已保存,待验证');
+ setCkTarget(null);
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const toggleCkDisabled = async (row: HuyaCollabOrder) => {
+ const next = row.ck_status === 'disabled' ? 'unverified' : 'disabled';
+ setBusy(true);
+ try {
+ const updated = await huyaApi.updateCollabOrderCkStatus(row.id, {
+ status: next,
+ message: next === 'disabled' ? '人工停用' : '人工恢复,待验证',
+ expected_version: row.version,
+ });
+ applyUpdated(updated);
+ message.success(next === 'disabled' ? '已停用这份 CK' : '已恢复(待验证)');
+ void load();
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const removeOrder = async (row: HuyaCollabOrder) => {
+ setBusy(true);
+ try {
+ await huyaApi.deleteCollabOrder(row.id);
+ message.success(`已删除订单 ${row.order_no}`);
+ if (detail?.id === row.id) setDetail(null);
+ void load();
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const commitSlot = async (row: HuyaCollabOrder, slot: HuyaCollabImageSlot, keys: string[]) => {
+ const updated = await huyaApi.updateCollabOrderImages(row.id, {
+ slot, images: keys, expected_version: row.version,
+ });
+ applyUpdated(updated);
+ };
+
+ const uploadImage = async (row: HuyaCollabOrder, slot: HuyaCollabImageSlot, file: File) => {
+ setBusy(true);
+ try {
+ const uploaded = await fileApi.upload(file, 'collab');
+ const current = slot === 'bind_images' ? row.bind_images : row.home_images;
+ await commitSlot(row, slot, [...current, uploaded.object_key]);
+ message.success('图片已上传');
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ void load();
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const removeImage = async (row: HuyaCollabOrder, slot: HuyaCollabImageSlot, key: string) => {
+ setBusy(true);
+ try {
+ const current = slot === 'bind_images' ? row.bind_images : row.home_images;
+ await commitSlot(row, slot, current.filter((item) => item !== key));
+ message.success('已从这一行移除');
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const setCoverImage = async (row: HuyaCollabOrder, slot: HuyaCollabImageSlot, key: string) => {
+ setBusy(true);
+ try {
+ const current = slot === 'bind_images' ? row.bind_images : row.home_images;
+ await commitSlot(row, slot, [key, ...current.filter((item) => item !== key)]);
+ message.success('已设为封面');
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const submitCreate = async () => {
+ try {
+ const values = await createForm.validateFields();
+ const created = await huyaApi.createCollabOrder({
+ order_no: String(values.order_no ?? '').trim(),
+ shop: String(values.shop ?? ''),
+ owner_id: (values.owner_id as number | undefined) ?? null,
+ ck_text: String(values.ck_text ?? ''),
+ bind_douyu_account: String(values.bind_douyu_account ?? ''),
+ gift_name: String(values.gift_name ?? ''),
+ remark: String(values.remark ?? ''),
+ });
+ message.success(`已新建 ${created.order_no}`);
+ setCreateOpen(false);
+ createForm.resetFields();
+ void load();
+ } catch (e: unknown) {
+ if (e instanceof Error) message.error(getErrorMessage(e));
+ }
+ };
+
+ const submitImport = async () => {
+ if (!importText.trim()) {
+ message.warning('请先粘贴表格内容');
+ return;
+ }
+ setImporting(true);
+ try {
+ const result = await huyaApi.importCollabOrders(importText);
+ setImportResult(result);
+ message.success(`导入完成:新建 ${result.created} / 更新 ${result.updated} / 跳过 ${result.skipped}`);
+ void load();
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ } finally {
+ setImporting(false);
+ }
+ };
+
+ /** 导入弹窗里的本地预览(后端还会再解析一次,这里只是让人确认列没串) */
+ const importPreview = useMemo(() => {
+ const lines = importText.split('\n').map((line) => line.replace(/\r$/, '')).filter((line) => line.trim());
+ const cells = lines.map((line) => (line.includes('\t') ? line.split('\t') : line.split(',')));
+ return {
+ header: cells[0] ?? [],
+ rows: cells.slice(1, IMPORT_PREVIEW_ROWS + 1),
+ total: Math.max(0, cells.length - 1),
+ };
+ }, [importText]);
+
+ const ownerOptions = owners.map((owner) => ({ value: owner.id, label: owner.username }));
+ const shopOptions = shops.map((shop) => ({ value: shop, label: shop }));
+ const finished = summary ? (summary.by_status.done ?? 0) : 0;
+
+ const columns: TableProps['columns'] = [
+ {
+ title: '订单编号', dataIndex: 'order_no', width: 170, fixed: 'left',
+ render: (value: string, row) => (
+
+ {value}
+ {row.status === 'void' && 已作废:{row.void_reason || '-'}}
+
+ ),
+ },
+ {
+ title: '店铺', dataIndex: 'shop', width: 110,
+ render: (value: string, row) => (
+ void patchOrder(row, { shop: next })} />
+ ),
+ },
+ {
+ title: '负责人', dataIndex: 'owner_id', width: 130,
+ render: (value: number | null, row) => (
+