feat(huya): 协作台账补资料与管理员复制 CK
This commit is contained in:
@@ -30,7 +30,7 @@ from fastapi import (
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ...database import get_db
|
||||
from ...deps import authenticate_websocket, require_permission
|
||||
from ...deps import authenticate_websocket, get_current_user, require_permission
|
||||
from ...models import AuditLog, HuyaExternalOrder, User
|
||||
from ...permissions import get_user_permissions
|
||||
from ...schemas import (
|
||||
@@ -442,6 +442,30 @@ def export_external_orders_ck(
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/external-orders/{order_id}/ck/copy",
|
||||
response_model=HuyaExternalOrderCkExportOut,
|
||||
)
|
||||
def copy_external_order_ck(
|
||||
order_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""复制单行 CK 明文(**仅超级管理员**;前端直接写剪贴板)。
|
||||
|
||||
这个入口比勾选导出更敏感:它把「单行凭据」做成一键复制,因此只给管理员。
|
||||
仍然复用 :func:`collab.export_cks`:逐行写 ``huya:collab:ck_export`` 审计,
|
||||
但审计里只记单号,不写 CK 本体。
|
||||
"""
|
||||
if current.role != "super_admin":
|
||||
raise HTTPException(status_code=403, detail="仅超级管理员可复制 CK")
|
||||
try:
|
||||
order = collab.get_order(db, order_id)
|
||||
return collab.export_cks(db, current, [order])
|
||||
except collab.CollabError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.websocket("/external-orders/ws")
|
||||
async def ws_external_orders(websocket: WebSocket):
|
||||
"""协作台账实时失效通知:客户端收到后按自己的筛选静默重拉。"""
|
||||
|
||||
@@ -286,6 +286,11 @@ export const huyaApi = {
|
||||
'/huya/external-orders/ck/export',
|
||||
{ ids },
|
||||
),
|
||||
/** 单行复制 CK(仅超级管理员;后端再次校验角色) */
|
||||
copyCollabOrderCk: (id: number) =>
|
||||
api.post<HuyaCollabCkExportResult, HuyaCollabCkExportResult>(
|
||||
`/huya/external-orders/${id}/ck/copy`,
|
||||
),
|
||||
/** 删除一行(只给运营:huya:collab:manage) */
|
||||
deleteCollabOrder: (id: number) =>
|
||||
api.delete<{ success: boolean; order_no: string }, { success: boolean; order_no: string }>(
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert, App, AutoComplete, Button, Card, Checkbox, Col, Descriptions, Divider, Drawer, Dropdown,
|
||||
Alert, App, Button, Card, Checkbox, Col, Descriptions, Divider, Drawer, Dropdown,
|
||||
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, CopyOutlined, DeleteOutlined, EditOutlined, FileExcelOutlined,
|
||||
LinkOutlined, PlusOutlined, ReloadOutlined, SafetyCertificateOutlined, ShopOutlined,
|
||||
CheckCircleOutlined, CloudUploadOutlined, CopyOutlined, DeleteOutlined, EditOutlined, FileAddOutlined,
|
||||
FileExcelOutlined, LinkOutlined, PlusOutlined, ReloadOutlined, SafetyCertificateOutlined, ShopOutlined,
|
||||
StopOutlined, SwapOutlined, SyncOutlined, UndoOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
@@ -267,6 +267,68 @@ function hasDraftContent(draft: Omit<CreateDraft, 'saved_at'>): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
// 「补资料」草稿:按登录用户 + 订单 id 分开存。填一半关掉 / 刷新后,
|
||||
// 再点这一行的「补资料」会原样恢复(含刚贴的 CK 和刚上传的截图 key)。
|
||||
const SUPPLEMENT_DRAFT_KEY_PREFIX = 'huya.collab.supplementDraft';
|
||||
|
||||
interface SupplementDraft {
|
||||
shop: string;
|
||||
ck_text: string;
|
||||
gift_name: string;
|
||||
remark: string;
|
||||
home_images: string[];
|
||||
saved_at: number;
|
||||
}
|
||||
|
||||
interface SupplementFormValues {
|
||||
shop: string;
|
||||
ck_text?: string;
|
||||
gift_name?: string;
|
||||
remark?: string;
|
||||
home_images?: string[];
|
||||
}
|
||||
|
||||
function supplementDraftKey(userId?: number | null, orderId?: number): string {
|
||||
return userId && orderId ? `${SUPPLEMENT_DRAFT_KEY_PREFIX}.${userId}.${orderId}` : '';
|
||||
}
|
||||
|
||||
function readSupplementDraft(userId: number | null | undefined, orderId: number): SupplementDraft | null {
|
||||
const key = supplementDraftKey(userId, orderId);
|
||||
if (!key) return null;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(key);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as Partial<SupplementDraft>;
|
||||
const savedAt = Number(parsed.saved_at) || 0;
|
||||
if (!savedAt || Date.now() - savedAt > 7 * 24 * 60 * 60 * 1000) {
|
||||
window.localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
shop: typeof parsed.shop === 'string' ? parsed.shop : '',
|
||||
ck_text: typeof parsed.ck_text === 'string' ? parsed.ck_text : '',
|
||||
gift_name: typeof parsed.gift_name === 'string' ? parsed.gift_name : '',
|
||||
remark: typeof parsed.remark === 'string' ? parsed.remark : '',
|
||||
home_images: Array.isArray(parsed.home_images)
|
||||
? parsed.home_images.filter((item): item is string => typeof item === 'string')
|
||||
: [],
|
||||
saved_at: savedAt,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearSupplementDraft(userId: number | null | undefined, orderId: number): void {
|
||||
const key = supplementDraftKey(userId, orderId);
|
||||
if (!key) return;
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
} catch {
|
||||
// 忽略:清不掉也不影响当前表单
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(value: string | null | undefined): string {
|
||||
if (!value) return '-';
|
||||
const time = new Date(value);
|
||||
@@ -410,6 +472,8 @@ export default function HuyaCollabPage() {
|
||||
const canManage = can('huya:collab:manage');
|
||||
// 导出 CK 是把凭据带出系统,单独一个权限点(默认只给运营,见 permissions.py)
|
||||
const canExportCk = can('huya:collab:ck_export');
|
||||
// 「复制 CK」是单行一键带走凭据,按用户要求只给超级管理员(后端也会再校验一次)
|
||||
const isAdmin = user?.role === 'super_admin';
|
||||
|
||||
const [rows, setRows] = useState<HuyaCollabOrder[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -438,6 +502,16 @@ export default function HuyaCollabPage() {
|
||||
const [createForm] = Form.useForm<Record<string, unknown>>();
|
||||
const [draftRestoredAt, setDraftRestoredAt] = useState<number | null>(null);
|
||||
const draftSaveTimer = useRef<number | null>(null);
|
||||
|
||||
// 「补资料」:对待补资料的单补 CK / 礼包 / 主页截图 / 备注,草稿按订单存在本机
|
||||
const [supplementTarget, setSupplementTarget] = useState<HuyaCollabOrder | null>(null);
|
||||
const [supplementForm] = Form.useForm<SupplementFormValues>();
|
||||
const [supplementDraftAt, setSupplementDraftAt] = useState<number | null>(null);
|
||||
const supplementDraftTimer = useRef<number | null>(null);
|
||||
const createGiftValue = Form.useWatch('gift_name', createForm) as string | undefined;
|
||||
const supplementGiftValue = Form.useWatch('gift_name', supplementForm) as string | undefined;
|
||||
const supplementCkText = Form.useWatch('ck_text', supplementForm) as string | undefined;
|
||||
const supplementHomeImages = Form.useWatch('home_images', supplementForm) as string[] | undefined;
|
||||
const [ckPreview, setCkPreview] = useState<HuyaCollabCkPreview | null>(null);
|
||||
const [previewing, setPreviewing] = useState(false);
|
||||
// 这次预览有没有带「拉礼包」(没有就是纯探针 + 用本地礼包缓存),只影响提示文案
|
||||
@@ -521,7 +595,7 @@ export default function HuyaCollabPage() {
|
||||
|
||||
const realtimeEnabled = can('huya:collab:view');
|
||||
const collabModalOpen = Boolean(
|
||||
createOpen || detail || statusAction || batchStatusOpen || ckTarget || ckExportOpen || shopModalOpen,
|
||||
createOpen || supplementTarget || detail || statusAction || batchStatusOpen || ckTarget || ckExportOpen || shopModalOpen,
|
||||
);
|
||||
|
||||
const handleRealtimeInvalidate = useCallback((event: CollabRealtimeEvent) => {
|
||||
@@ -1019,6 +1093,203 @@ export default function HuyaCollabPage() {
|
||||
}
|
||||
};
|
||||
|
||||
/** 单行「复制 CK」:只有超级管理员能看到按钮,后端还会再校验一次角色。 */
|
||||
const copyOneCk = async (row: HuyaCollabOrder) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
const result = await huyaApi.copyCollabOrderCk(row.id);
|
||||
const item = result.items[0];
|
||||
if (!item) {
|
||||
message.warning(result.skipped[0]?.reason || '这行还没有可复制的 CK');
|
||||
return;
|
||||
}
|
||||
await copyTextToClipboard(item.ck_text);
|
||||
message.success(`已复制 ${row.order_no} 的 CK`);
|
||||
} catch (e: unknown) {
|
||||
message.error(`复制失败:${getErrorMessage(e)}`);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 把「补资料」表单写进本机草稿:仍按用户 + 订单分键,最多留 7 天。 */
|
||||
const writeSupplementDraft = useCallback((values: SupplementFormValues, target: HuyaCollabOrder) => {
|
||||
const userId = user?.id;
|
||||
const key = supplementDraftKey(userId, target.id);
|
||||
if (!key) return;
|
||||
const images = Array.isArray(values.home_images)
|
||||
? values.home_images.filter((item): item is string => typeof item === 'string')
|
||||
: target.home_images;
|
||||
const payload: Omit<SupplementDraft, 'saved_at'> = {
|
||||
shop: String(values.shop ?? ''),
|
||||
ck_text: String(values.ck_text ?? ''),
|
||||
gift_name: String(values.gift_name ?? ''),
|
||||
remark: String(values.remark ?? ''),
|
||||
home_images: images,
|
||||
};
|
||||
const changed = Boolean(
|
||||
payload.ck_text.trim()
|
||||
|| payload.shop !== target.shop
|
||||
|| payload.gift_name !== target.gift_name
|
||||
|| payload.remark !== target.remark
|
||||
|| payload.home_images.join('\n') !== target.home_images.join('\n'),
|
||||
);
|
||||
try {
|
||||
if (!changed) {
|
||||
window.localStorage.removeItem(key);
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(key, JSON.stringify({ ...payload, saved_at: Date.now() }));
|
||||
} catch {
|
||||
// 隐私模式 / 配额满:暂存失败不影响提交
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
const persistSupplementDraft = useCallback((values: SupplementFormValues, target: HuyaCollabOrder) => {
|
||||
if (supplementDraftTimer.current) window.clearTimeout(supplementDraftTimer.current);
|
||||
supplementDraftTimer.current = window.setTimeout(() => writeSupplementDraft(values, target), 400);
|
||||
}, [writeSupplementDraft]);
|
||||
|
||||
const openSupplement = (row: HuyaCollabOrder) => {
|
||||
if (supplementDraftTimer.current) {
|
||||
window.clearTimeout(supplementDraftTimer.current);
|
||||
supplementDraftTimer.current = null;
|
||||
}
|
||||
const draft = readSupplementDraft(user?.id, row.id);
|
||||
const base: SupplementFormValues = {
|
||||
shop: row.shop,
|
||||
ck_text: '',
|
||||
gift_name: row.gift_name,
|
||||
remark: row.remark,
|
||||
home_images: row.home_images,
|
||||
};
|
||||
supplementForm.setFieldsValue(draft ? { ...base, ...draft } : base);
|
||||
setSupplementDraftAt(draft?.saved_at ?? null);
|
||||
setSupplementTarget(row);
|
||||
};
|
||||
|
||||
const closeSupplement = useCallback(() => {
|
||||
if (supplementDraftTimer.current) {
|
||||
window.clearTimeout(supplementDraftTimer.current);
|
||||
supplementDraftTimer.current = null;
|
||||
}
|
||||
if (supplementTarget) {
|
||||
writeSupplementDraft(
|
||||
supplementForm.getFieldsValue(true) as SupplementFormValues,
|
||||
supplementTarget,
|
||||
);
|
||||
}
|
||||
setSupplementTarget(null);
|
||||
setSupplementDraftAt(null);
|
||||
supplementForm.resetFields();
|
||||
}, [supplementForm, supplementTarget, writeSupplementDraft]);
|
||||
|
||||
const clearSupplement = () => {
|
||||
if (!supplementTarget) return;
|
||||
if (supplementDraftTimer.current) {
|
||||
window.clearTimeout(supplementDraftTimer.current);
|
||||
supplementDraftTimer.current = null;
|
||||
}
|
||||
clearSupplementDraft(user?.id, supplementTarget.id);
|
||||
setSupplementDraftAt(null);
|
||||
supplementForm.setFieldsValue({
|
||||
shop: supplementTarget.shop,
|
||||
ck_text: '',
|
||||
gift_name: supplementTarget.gift_name,
|
||||
remark: supplementTarget.remark,
|
||||
home_images: supplementTarget.home_images,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存「补资料」:字段 / 新 CK / 截图分别走各自接口(每个都有乐观锁),
|
||||
* 新 CK 保存后立即跑一次只读探针;用户点「保存并进入待处理」时再校验资料完整性。
|
||||
*/
|
||||
const submitSupplement = async (markPending = false) => {
|
||||
if (!supplementTarget) return;
|
||||
let values: SupplementFormValues;
|
||||
try {
|
||||
values = await supplementForm.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
let current = supplementTarget;
|
||||
const payload: HuyaCollabOrderUpdate = {};
|
||||
const shop = String(values.shop ?? '').trim();
|
||||
const giftName = String(values.gift_name ?? '').trim();
|
||||
const remark = String(values.remark ?? '');
|
||||
if (shop !== current.shop) payload.shop = shop;
|
||||
if (giftName !== current.gift_name) payload.gift_name = giftName;
|
||||
if (remark !== current.remark) payload.remark = remark;
|
||||
if (Object.keys(payload).length > 0) {
|
||||
current = await huyaApi.updateCollabOrder(current.id, {
|
||||
...payload,
|
||||
expected_version: current.version,
|
||||
});
|
||||
}
|
||||
|
||||
const ckText = String(values.ck_text ?? '').trim();
|
||||
if (ckText) {
|
||||
current = await huyaApi.updateCollabOrderCk(current.id, {
|
||||
ck_text: ckText,
|
||||
action: 'paste',
|
||||
expected_version: current.version,
|
||||
});
|
||||
current = await huyaApi.verifyCollabOrder(current.id);
|
||||
}
|
||||
|
||||
const images = values.home_images ?? current.home_images;
|
||||
if (images.join('\n') !== current.home_images.join('\n')) {
|
||||
current = await huyaApi.updateCollabOrderImages(current.id, {
|
||||
slot: 'home_images',
|
||||
images,
|
||||
expected_version: current.version,
|
||||
});
|
||||
}
|
||||
|
||||
// 没有换 CK、但准备进「待处理」时,先跑一次只读探针;避免拿一份没验证的旧 CK 往前推
|
||||
if (markPending && !ckText && current.has_cookie && current.ck_status !== 'valid') {
|
||||
current = await huyaApi.verifyCollabOrder(current.id);
|
||||
}
|
||||
|
||||
if (markPending && current.ck_status !== 'valid') {
|
||||
applyUpdated(current);
|
||||
setSupplementTarget(current);
|
||||
supplementForm.setFieldValue('ck_text', '');
|
||||
void load();
|
||||
message.warning(
|
||||
'资料已保存,但这份 CK 还没验证通过,暂时不能进入「待处理」;请先补 / 换一份有效 CK',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (markPending) {
|
||||
current = await huyaApi.updateCollabOrderStatus(current.id, {
|
||||
status: 'pending',
|
||||
expected_version: current.version,
|
||||
});
|
||||
}
|
||||
|
||||
applyUpdated(current);
|
||||
clearSupplementDraft(user?.id, supplementTarget.id);
|
||||
setSupplementDraftAt(null);
|
||||
supplementForm.resetFields();
|
||||
setSupplementTarget(null);
|
||||
message.success(
|
||||
markPending
|
||||
? `已补齐资料,${current.order_no} 进入「待处理」`
|
||||
: `已保存 ${current.order_no} 的资料`,
|
||||
);
|
||||
void load();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
void load();
|
||||
} 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,
|
||||
@@ -1111,6 +1382,7 @@ export default function HuyaCollabPage() {
|
||||
|
||||
useEffect(() => () => {
|
||||
if (draftSaveTimer.current) window.clearTimeout(draftSaveTimer.current);
|
||||
if (supplementDraftTimer.current) window.clearTimeout(supplementDraftTimer.current);
|
||||
}, []);
|
||||
|
||||
/** 清掉草稿并恢复空表单(「清空草稿」入口用) */
|
||||
@@ -1229,6 +1501,27 @@ export default function HuyaCollabPage() {
|
||||
? `礼包用本地缓存(${giftOptions.length} 个${giftCacheLabel ? ` · ${giftCacheLabel}` : ''}),要更新点「强制刷新礼包」`
|
||||
: (ckPreview?.gifts_message || '');
|
||||
|
||||
/**
|
||||
* 礼包下拉选项:只允许点选缓存里的礼包,不接受手输。
|
||||
* 当前值是历史/已下线礼包时补一个只读选项,保证界面能显示,但依然只能从下拉里选。
|
||||
*/
|
||||
const giftSelectOptions = (current?: string | null) => {
|
||||
const value = String(current ?? '').trim();
|
||||
const options = giftOptions.map((gift) => ({
|
||||
value: gift.name,
|
||||
label: [
|
||||
gift.category_name ? `[${gift.category_name}]` : '',
|
||||
gift.name,
|
||||
gift.price ? `· ${gift.price} 积分` : '',
|
||||
gift.remain_text ? `· 剩 ${gift.remain_text}` : '',
|
||||
].filter(Boolean).join(' '),
|
||||
}));
|
||||
if (value && !giftOptions.some((gift) => gift.name === value)) {
|
||||
options.unshift({ value, label: `${value}(历史值)` });
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
// 表头排序:一次正序、再点反序(Table 上限制 sortDirections,不会出现「第三次取消」)。
|
||||
const sortProps = (key: CollabSortKey) => ({
|
||||
key,
|
||||
@@ -1392,8 +1685,21 @@ export default function HuyaCollabPage() {
|
||||
...sortProps('gift_name'),
|
||||
render: (_, row) => (
|
||||
<Space direction="vertical" size={0} style={{ width: '100%' }}>
|
||||
<EditableText value={row.gift_name} disabled={!canEdit} placeholder="+ 礼包名"
|
||||
onCommit={(next) => void patchOrder(row, { gift_name: next })} />
|
||||
{/* 礼包名只能从缓存列表点选:手输会出现“看起来一样、接口拉不到”的脏值 */}
|
||||
<Select
|
||||
size="small"
|
||||
variant="borderless"
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={giftSelectOptions(row.gift_name)}
|
||||
value={row.gift_name || undefined}
|
||||
disabled={!canEdit}
|
||||
placeholder="+ 点选礼包"
|
||||
popupMatchSelectWidth={false}
|
||||
notFoundContent={giftOptions.length === 0 ? '礼包缓存为空:先在「新建」里贴 CK 拉一次' : '没有匹配的礼包'}
|
||||
onChange={(next: string) => void patchOrder(row, { gift_name: next })}
|
||||
/>
|
||||
<EditableText value={row.remark} disabled={!canEdit} placeholder="+ 备注"
|
||||
onCommit={(next) => void patchOrder(row, { remark: next })} />
|
||||
</Space>
|
||||
@@ -1483,9 +1789,24 @@ export default function HuyaCollabPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作', width: 150, fixed: 'right', align: 'center',
|
||||
title: '操作', width: 300, fixed: 'right', align: 'center',
|
||||
render: (_, row) => (
|
||||
<Space size={4}>
|
||||
<Space size={4} wrap>
|
||||
{canEdit && row.status === 'incomplete' && (
|
||||
<Button size="small" type="primary" ghost icon={<FileAddOutlined />}
|
||||
onClick={() => openSupplement(row)}>
|
||||
补资料
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Tooltip title={row.has_cookie ? '复制这一行的 CK 明文(仅管理员)' : '这行还没贴 CK'}>
|
||||
<Button size="small" icon={<CopyOutlined />}
|
||||
disabled={busy || !row.has_cookie}
|
||||
onClick={() => void copyOneCk(row)}>
|
||||
复制CK
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => void openDetail(row)}>详情</Button>
|
||||
{canEdit && (
|
||||
<Dropdown
|
||||
@@ -1710,16 +2031,31 @@ export default function HuyaCollabPage() {
|
||||
const draft = readCreateDraft(user?.id);
|
||||
if (draft && hasDraftContent(draft)) {
|
||||
// 上次填了一半:原样恢复(CK / 礼包 / 截图 / 备注都在),并重新验证暂存的 CK
|
||||
createForm.setFieldsValue({ ...draft });
|
||||
const restored = { ...draft };
|
||||
const restoredGift = String(restored.gift_name ?? '').trim();
|
||||
if (
|
||||
restoredGift
|
||||
&& giftOptions.length > 0
|
||||
&& !giftOptions.some((gift) => gift.name === restoredGift)
|
||||
) {
|
||||
restored.gift_name = '';
|
||||
setGiftHint(`草稿里的「${restoredGift}」不在当前礼包缓存里,已清空,请重新点选`);
|
||||
} else {
|
||||
setGiftHint('');
|
||||
}
|
||||
createForm.setFieldsValue({ ...restored });
|
||||
setDraftRestoredAt(draft.saved_at || Date.now());
|
||||
setCreateOpen(true);
|
||||
if (draft.ck_text.trim()) scheduleCkPreview(draft.ck_text, true);
|
||||
return;
|
||||
}
|
||||
setDraftRestoredAt(null);
|
||||
// 记住最近选的礼包:同一个礼包连开几单时不用反复选
|
||||
// 记住最近选的礼包:只在当前缓存里还有它时才自动带上(礼包名不接受手输)
|
||||
const lastGift = window.localStorage.getItem(LAST_GIFT_KEY);
|
||||
if (lastGift) createForm.setFieldsValue({ gift_name: lastGift });
|
||||
if (lastGift && giftOptions.some((gift) => gift.name === lastGift)) {
|
||||
createForm.setFieldsValue({ gift_name: lastGift });
|
||||
}
|
||||
setGiftHint('');
|
||||
setCreateOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -1736,7 +2072,7 @@ export default function HuyaCollabPage() {
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
scroll={{ x: 1800, y: 'calc(100vh - 460px)' }}
|
||||
scroll={{ x: 2200, y: 'calc(100vh - 460px)' }}
|
||||
sortDirections={['ascend', 'descend']}
|
||||
onChange={(_pagination, _filters, sorter, extra) => {
|
||||
if (extra.action !== 'sort') return;
|
||||
@@ -1890,7 +2226,19 @@ export default function HuyaCollabPage() {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="gift_name" label="礼包名"><Input maxLength={128} /></Form.Item>
|
||||
<Form.Item name="gift_name" label="礼包名"
|
||||
tooltip="只接受礼包缓存里的点选结果,可输入关键字搜索;不接受手工输入">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="输入关键字搜索,点选礼包"
|
||||
options={giftSelectOptions(detail.gift_name)}
|
||||
notFoundContent={giftOptions.length === 0
|
||||
? '礼包缓存为空:先在「新建」里贴 CK 拉一次'
|
||||
: '没有匹配的礼包'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item name="remark" label="备注"><Input.TextArea rows={2} maxLength={512} /></Form.Item>
|
||||
@@ -2184,26 +2532,20 @@ export default function HuyaCollabPage() {
|
||||
<Form.Item
|
||||
name="gift_name"
|
||||
label="礼包名(用户购买的物品,可稍后补)"
|
||||
tooltip="先贴有效 CK,再从礼包列表里点选(列表有本地缓存,不用每次重拉;拉不到时可以直接手输)。不选也能先建单,之后在详情里补。"
|
||||
tooltip="只接受礼包缓存里的点选结果,可输入关键字搜索。先贴有效 CK 拉一次礼包;不选也能先建单,之后点「补资料」再选。"
|
||||
extra={giftHint || (giftsCachedAt && giftOptions.length > 0
|
||||
? `礼包列表缓存于 ${giftCacheLabel}(${giftOptions.length} 个)`
|
||||
: undefined)}
|
||||
validateStatus={giftHint ? 'warning' : undefined}
|
||||
>
|
||||
<AutoComplete
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={ckReady ? '从礼包里点选(可直接输)' : '可先空着,之后在详情里补'}
|
||||
options={giftOptions.map((gift) => ({
|
||||
value: gift.name,
|
||||
label: [
|
||||
gift.category_name ? `[${gift.category_name}]` : '',
|
||||
gift.name,
|
||||
gift.price ? `· ${gift.price} 积分` : '',
|
||||
gift.remain_text ? `· 剩 ${gift.remain_text}` : '',
|
||||
].filter(Boolean).join(' '),
|
||||
}))}
|
||||
filterOption={(input, option) =>
|
||||
String(option?.value ?? '').toLowerCase().includes(input.toLowerCase())}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={giftOptions.length > 0 ? '输入关键字搜索,点选礼包' : '礼包缓存为空:先贴 CK 拉取'}
|
||||
options={giftSelectOptions(createGiftValue)}
|
||||
disabled={giftOptions.length === 0 && !createGiftValue}
|
||||
notFoundContent={giftOptions.length === 0 ? '先在左侧贴一份有效 CK 拉取礼包列表' : '没有匹配的礼包'}
|
||||
onChange={(value: string) => {
|
||||
// 记住最近选的礼包:连开几单同一个礼包就不用反复点
|
||||
setGiftHint('');
|
||||
@@ -2246,6 +2588,118 @@ export default function HuyaCollabPage() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={supplementTarget ? `补资料 · ${supplementTarget.order_no}` : '补资料'}
|
||||
open={!!supplementTarget}
|
||||
width={760}
|
||||
onCancel={closeSupplement}
|
||||
destroyOnHidden
|
||||
styles={{ body: { maxHeight: '74vh', overflowY: 'auto', paddingRight: 12 } }}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={closeSupplement} disabled={busy}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="save" onClick={() => void submitSupplement(false)} loading={busy}>
|
||||
保存资料
|
||||
</Button>,
|
||||
<Button key="pending" type="primary" onClick={() => void submitSupplement(true)} loading={busy}>
|
||||
保存并进入待处理
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{supplementTarget && (
|
||||
<>
|
||||
{supplementDraftAt && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message={`已恢复上次没填完的补资料草稿(${formatTime(new Date(supplementDraftAt).toISOString())} 暂存)`}
|
||||
action={<Button size="small" type="link" onClick={clearSupplement}>清空草稿</Button>}
|
||||
/>
|
||||
)}
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message="还缺什么就补什么;填到一半可以直接关掉,下次再点「补资料」会从这里继续"
|
||||
description={(() => {
|
||||
const hasCk = supplementTarget.has_cookie || Boolean(supplementCkText?.trim());
|
||||
const images = supplementHomeImages ?? supplementTarget.home_images;
|
||||
const missing = [
|
||||
!hasCk ? '外部 CK' : '',
|
||||
!supplementGiftValue ? '礼包名' : '',
|
||||
images.length === 0 ? '主页截图' : '',
|
||||
].filter(Boolean);
|
||||
return missing.length > 0 ? `当前还缺:${missing.join('、')}` : '资料已齐,可以直接「保存并进入待处理」。';
|
||||
})()}
|
||||
/>
|
||||
<Form
|
||||
form={supplementForm}
|
||||
layout="vertical"
|
||||
size="middle"
|
||||
onValuesChange={(_changed, allValues) => {
|
||||
persistSupplementDraft(allValues as SupplementFormValues, supplementTarget);
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="shop" label="店铺"
|
||||
rules={[{ required: true, message: '请点一下选店铺' }]}
|
||||
tooltip="点一下选;历史订单的店铺即使已停用也会保留">
|
||||
<HuyaCollabShopPicker shops={shops} allowClear={false} size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="gift_name" label="礼包名"
|
||||
tooltip="只接受礼包缓存里的点选结果,可输入关键字搜索;不接受手工输入">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={giftOptions.length > 0 ? '输入关键字搜索,点选礼包' : '礼包缓存为空:先在「新建」里贴 CK 拉一次'}
|
||||
options={giftSelectOptions(supplementGiftValue ?? supplementTarget.gift_name)}
|
||||
disabled={giftOptions.length === 0 && !supplementGiftValue}
|
||||
notFoundContent={giftOptions.length === 0
|
||||
? '礼包缓存为空:先在「新建」里贴 CK 拉一次'
|
||||
: '没有匹配的礼包'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
name="ck_text"
|
||||
label="外部 CK(不换就留空)"
|
||||
tooltip="支持「纯CK」或「账号----密码----CK」。贴新 CK 会覆盖并立即验证;这行已经存过 CK 时,不换就留空"
|
||||
extra={supplementTarget.has_cookie
|
||||
? `这行已存 CK(当前:${supplementTarget.ck_status_label};最近验证 ${formatTime(supplementTarget.ck_checked_at)})`
|
||||
: '这行还没有 CK,可以现在补'}
|
||||
>
|
||||
<Input.TextArea
|
||||
autoSize={{ minRows: 3, maxRows: 7 }}
|
||||
placeholder={supplementTarget.has_cookie
|
||||
? '已存 CK;要换就贴一份新的,不换留空'
|
||||
: '粘贴外部 CK,保存后自动验证'}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={14}>
|
||||
<Form.Item name="home_images" label="主页截图(第一张作封面)"
|
||||
tooltip="支持点击选择、拖拽或 Ctrl/Cmd+V 粘贴;补资料时也可以只补截图">
|
||||
<HuyaCollabHomeImageUploader max={MAX_IMAGES_PER_SLOT} optional />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item name="remark" label="备注(可选)">
|
||||
<Input.TextArea rows={3} maxLength={512} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="粘贴导入(Excel 复制出来的 TSV)"
|
||||
open={importOpen}
|
||||
|
||||
Reference in New Issue
Block a user