{ if (!disabled) setEditing(true); }}
- style={{ cursor: disabled ? 'default' : 'text', minHeight: 22 }}
+ // wordBreak:礼品名 / 角色名里偶发的一长串不带空格的字符也不会顶穿这一列
+ style={{ cursor: disabled ? 'default' : 'text', minHeight: 22, wordBreak: 'break-word' }}
title={disabled ? undefined : '点击编辑'}
>
{value ||
{placeholder || '—'}}
@@ -208,7 +244,7 @@ function ImageSlotEditor({
export default function HuyaCollabPage() {
const { modal } = App.useApp();
- const { can } = usePermissions();
+ const { can, user } = usePermissions();
const canEdit = can('huya:collab:edit');
const canManage = can('huya:collab:manage');
@@ -220,7 +256,8 @@ export default function HuyaCollabPage() {
const [busy, setBusy] = useState(false);
const [summary, setSummary] = useState
(null);
const [owners, setOwners] = useState([]);
- const [shops, setShops] = useState([]);
+ const [shops, setShops] = useState([]);
+ const [shopModalOpen, setShopModalOpen] = useState(false);
const [filters, setFilters] = useState({ include_void: false });
const [selectedIds, setSelectedIds] = useState([]);
@@ -231,6 +268,22 @@ export default function HuyaCollabPage() {
const [createOpen, setCreateOpen] = useState(false);
const [createForm] = Form.useForm>();
+ const [ckPreview, setCkPreview] = useState(null);
+ const [previewing, setPreviewing] = useState(false);
+ // 这次预览有没有带「拉礼包」(没有就是纯探针 + 用本地礼包缓存),只影响提示文案
+ const [previewPullsGifts, setPreviewPullsGifts] = useState(false);
+ const ckReady = !!ckPreview?.ok && !!ckPreview?.has_role;
+ // 礼包列表:优先用浏览器缓存那份 —— 有数据就不去虎牙拉,要更新点「强制刷新礼包」。
+ // 直接拿缓存做初值(而不是等 useEffect):贴第一份 CK 时就已经有数据了,不会白拉一次。
+ const [giftOptions, setGiftOptions] = useState(
+ () => readGiftCache()?.gifts ?? [],
+ );
+ const [giftsCachedAt, setGiftsCachedAt] = useState(
+ () => readGiftCache()?.at ?? null,
+ );
+ const giftsRef = useRef(giftOptions);
+ // 记忆里的礼包不在「这次拉到的列表」里时的提示(会顺手清空,免得提交一个拉不到的礼包名)
+ const [giftHint, setGiftHint] = useState('');
const [importOpen, setImportOpen] = useState(false);
const [importText, setImportText] = useState('');
const [importing, setImporting] = useState(false);
@@ -253,7 +306,7 @@ export default function HuyaCollabPage() {
huyaApi.listCollabOrders({ ...filters, page, page_size: pageSize }),
huyaApi.collabOrdersSummary(),
huyaApi.collabOrderOwners(),
- huyaApi.collabOrderShops(),
+ huyaApi.listCollabShops(),
]);
setRows(list.items);
setTotal(list.total);
@@ -276,6 +329,88 @@ export default function HuyaCollabPage() {
setDetail((current) => (current && current.id === updated.id ? updated : current));
}, []);
+ const previewTimer = useRef(null);
+ // 预览请求序号:贴了新 CK 之后,旧请求迟到回来的结论一律丢掉(否则会显示上一份 CK 的"有效")
+ const previewSeq = useRef(0);
+
+ useEffect(() => {
+ giftsRef.current = giftOptions;
+ }, [giftOptions]);
+
+ const emptyPreview = useCallback(
+ (message: string): HuyaCollabCkPreview => ({
+ ok: false, message, uid: '', yyuid: '', hy_username: '', ck_status: 'invalid',
+ bind_game_account: '', bind_game_role: '', bind_game_channel: '',
+ has_role: false, gifts: [], gifts_message: '', uid_used_by: [],
+ }),
+ [],
+ );
+
+ /** 跑一次预览(探针 + 可选拉礼包),只读,不写库 */
+ const runCkPreview = useCallback(
+ async (text: string, withGifts: boolean) => {
+ const seq = previewSeq.current;
+ try {
+ const preview = await huyaApi.previewCollabCk(text, withGifts);
+ if (seq !== previewSeq.current) return; // 已经贴了新的 CK,旧结论丢掉
+ setCkPreview(preview);
+ if (preview.gifts.length > 0) {
+ // 真拉到列表了(首次或强制刷新):更新浏览器缓存,接下来几单直接用
+ setGiftOptions(preview.gifts);
+ setGiftsCachedAt(Date.now());
+ writeGiftCache(preview.gifts);
+ }
+ if (preview.ok && preview.has_role) {
+ message.success(`CK 有效:绑定角色 ${preview.bind_game_role}`);
+ // 「记住的礼包」如果不在当前列表里就清掉重选;列表为空时不动它(允许手填)
+ const options = preview.gifts.length > 0 ? preview.gifts : giftsRef.current;
+ const picked = String(createForm.getFieldValue('gift_name') ?? '').trim();
+ if (picked && options.length > 0 && !options.some((gift) => gift.name === picked)) {
+ createForm.setFieldValue('gift_name', undefined);
+ setGiftHint(`上次选的「${picked}」不在当前的礼包列表里,已清空,请重新点选`);
+ } else {
+ setGiftHint('');
+ }
+ }
+ } catch (e: unknown) {
+ if (seq !== previewSeq.current) return;
+ setCkPreview(emptyPreview(getErrorMessage(e)));
+ } finally {
+ if (seq === previewSeq.current) setPreviewing(false);
+ }
+ },
+ [emptyPreview, createForm],
+ );
+
+ /**
+ * 输入 CK 后防抖预览;改动了就先作废旧结论(避免拿旧的"有效"去提交)。
+ *
+ * ``forceGifts=true``(点了「强制刷新礼包」)才重新去虎牙拉列表;否则**本地有数据就用本地的** ——
+ * 一个活动期间礼包清单基本不变,每次贴 CK 都拉一遍纯属白挨接口。
+ */
+ const scheduleCkPreview = useCallback(
+ (ckText: string, immediate = false, forceGifts = false) => {
+ if (previewTimer.current) window.clearTimeout(previewTimer.current);
+ const text = (ckText || '').trim();
+ previewSeq.current += 1; // 无论跑不跑,都让在途请求作废
+ if (!text) {
+ setCkPreview(null);
+ setPreviewing(false);
+ return;
+ }
+ setCkPreview(null);
+ setPreviewing(true);
+ // 有缓存(giftsRef 非空)就不带 with_gifts,只有没数据或用户强制刷新才真去拉
+ const withGifts = forceGifts || giftsRef.current.length === 0;
+ setPreviewPullsGifts(withGifts);
+ previewTimer.current = window.setTimeout(
+ () => void runCkPreview(text, withGifts),
+ immediate ? 0 : 600,
+ );
+ },
+ [runCkPreview],
+ );
+
const patchOrder = async (row: HuyaCollabOrder, payload: HuyaCollabOrderUpdate) => {
try {
const updated = await huyaApi.updateCollabOrder(row.id, { ...payload, expected_version: row.version });
@@ -293,7 +428,6 @@ export default function HuyaCollabPage() {
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,
});
@@ -308,6 +442,15 @@ export default function HuyaCollabPage() {
}
};
+ /** 打开「同一买家的另一单」:先按 id 取最新数据,再复用详情抽屉 */
+ const openSiblingOrder = async (id: number) => {
+ try {
+ await openDetail(await huyaApi.getCollabOrder(id));
+ } catch (e: unknown) {
+ message.error(getErrorMessage(e));
+ }
+ };
+
const verifyOne = async (row: HuyaCollabOrder) => {
setBusy(true);
try {
@@ -511,19 +654,27 @@ export default function HuyaCollabPage() {
};
const submitCreate = async () => {
+ if (!ckReady) {
+ message.warning('这份 CK 没通过验证(或读不到绑定角色),不能提交');
+ return;
+ }
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 ?? ''),
+ // 「绑定斗鱼号」不再录入:绑了谁由探针读(bind_game_*),人工那一列已下线
gift_name: String(values.gift_name ?? ''),
+ home_images: (values.home_images as string[] | undefined) ?? [],
remark: String(values.remark ?? ''),
+ require_verified: true, // 服务端再校验一次:CK 必须读到绑定角色才让建
});
+ if (created.gift_name) window.localStorage.setItem(LAST_GIFT_KEY, created.gift_name);
message.success(`已新建 ${created.order_no}`);
setCreateOpen(false);
+ setCkPreview(null);
+ setGiftHint('');
createForm.resetFields();
void load();
} catch (e: unknown) {
@@ -561,10 +712,25 @@ export default function HuyaCollabPage() {
}, [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 giftCacheLabel = giftsCachedAt ? formatTime(new Date(giftsCachedAt).toISOString()) : '';
+ const giftSourceText = ckPreview?.gifts?.length
+ ? ckPreview.gifts_message
+ : giftOptions.length > 0
+ ? `礼包用本地缓存(${giftOptions.length} 个${giftCacheLabel ? ` · ${giftCacheLabel}` : ''}),要更新点「强制刷新礼包」`
+ : (ckPreview?.gifts_message || '');
+
const columns: TableProps['columns'] = [
+ {
+ title: '序号', width: 70, fixed: 'left', align: 'center',
+ render: (_, __, index) => (
+ // 跨页连续:第 2 页第 1 行 = pageSize + 1
+ {(page - 1) * pageSize + index + 1}
+ ),
+ },
{
title: '订单编号', dataIndex: 'order_no', width: 170, fixed: 'left',
render: (value: string, row) => (
@@ -575,10 +741,26 @@ export default function HuyaCollabPage() {
),
},
{
- title: '店铺', dataIndex: 'shop', width: 110,
+ title: '店铺', dataIndex: 'shop', width: 120,
render: (value: string, row) => (
- void patchOrder(row, { shop: next })} />
+ { if (next !== value) void patchOrder(row, { shop: next }); }}
+ />
+ }
+ >
+
+
),
},
{
@@ -612,31 +794,75 @@ export default function HuyaCollabPage() {
),
},
{
- title: '绑定图 / 主页图', width: 170,
- render: (_, row) => (
-
- {(['bind_images', 'home_images'] as HuyaCollabImageSlot[]).map((slot) => {
- const keys = slot === 'bind_images' ? row.bind_images : row.home_images;
- const urls = slot === 'bind_images' ? row.bind_image_urls : row.home_image_urls;
- const thumbs = slot === 'bind_images' ? row.bind_image_thumb_urls : row.home_image_thumb_urls;
- if (keys.length === 0) {
- return (
-
- {SLOT_META[slot].label} 0
+ title: '同买家', width: 120,
+ render: (_, row) => {
+ const cluster = row.buyer_cluster;
+ if (!cluster || cluster.others.length === 0) {
+ return —;
+ }
+ return (
+
+ 同一个买家,共 {cluster.total} 单
+ {cluster.key}
+
+ }
+ content={
+
+ {cluster.others.map((item) => (
+
+
+ {item.status_label || item.status}
+
+ {item.gift_name || '未填礼包'} · {item.shop || '未填店铺'}
+
+
+ ))}
+
+ 识别口径:探针读到的「绑定账号 + 绑定角色」相同(不是人工绑定关系)
- );
+
+ }
+ >
+
+
+ );
+ },
+ },
+ {
+ title: '主页截图', width: 170,
+ render: (_, row) => (
+
+ {(() => {
+ const keys = row.home_images;
+ const urls = row.home_image_urls;
+ const thumbs = row.home_image_thumb_urls;
+ if (keys.length === 0) {
+ return 还没传 0;
}
return (
-
+
+ style={{ objectFit: 'cover', borderRadius: 4 }} alt="主页截图封面" />
{keys.length} 张
);
- })}
+ })()}
+ {row.bind_images.length > 0 && (
+
+ 历史绑定图 {row.bind_images.length} 张
+
+ )}
),
},
@@ -677,13 +903,27 @@ export default function HuyaCollabPage() {
),
},
{
- title: 'CK 状态', dataIndex: 'ck_status', width: 150,
+ title: 'CK 状态', dataIndex: 'ck_status', width: 170,
render: (value: string, row) => (
-
+
{row.ck_status_label || CK_META[value]?.label || value}
{row.ck_check_message && (
+ // 探针结论可能很长(账号 / 角色 / 平台·区服全在里面):**必须截断**,
+ // 否则长文案会顶穿这一列压到「最后改动」上(2026-10-16 用户反馈)。全文放 Tooltip。
- {row.ck_check_message}
+
+ {row.ck_check_message}
+
)}
@@ -788,10 +1028,17 @@ export default function HuyaCollabPage() {
-
@@ -838,7 +1105,7 @@ export default function HuyaCollabPage() {
loading={loading}
columns={columns}
dataSource={rows}
- scroll={{ x: 1680, y: 'calc(100vh - 460px)' }}
+ scroll={{ x: 1800, y: 'calc(100vh - 460px)' }}
rowSelection={canEdit ? {
selectedRowKeys: selectedIds,
onChange: (keys) => setSelectedIds(keys.map(Number)),
@@ -887,12 +1154,45 @@ export default function HuyaCollabPage() {
children: `${formatTime(detail.scripted_at)} / ${formatTime(detail.done_at)}` },
{ key: 'updated', label: '最后改动',
children: `${detail.updated_by_name || '-'} · ${formatTime(detail.updated_at)}` },
+ // 老数据可能还留着人工填的「绑定斗鱼号」:只读展示,不再给人填(绑定关系看探针)
+ ...(detail.bind_douyu_account
+ ? [{
+ key: 'bind_douyu_account',
+ label: '绑定斗鱼号(历史字段,不再录入)',
+ children: detail.bind_douyu_account,
+ }]
+ : []),
]} />
{detail.ck_check_message && (
探针结论:{detail.ck_check_message}
)}
+ {detail.buyer_cluster && detail.buyer_cluster.others.length > 0 && (
+ // 同一个买家(绑定账号 + 绑定角色相同)名下的其它单:软识别,点一下直接切过去
+ <>
+
+
+ 同一个买家的其它单({detail.buyer_cluster.key} · 共 {detail.buyer_cluster.total} 单)
+
+
+
+ {detail.buyer_cluster.others.map((item) => (
+
+
+ {item.status_label || item.status}
+
+ {item.gift_name || '未填礼包'} · {item.shop || '未填店铺'}
+ {item.uid ? ` · 虎牙号 ${item.uid}` : ''}
+
+
+ ))}
+
+ >
+ )}
+
{canEdit && (
<>
可编辑字段
@@ -902,7 +1202,6 @@ export default function HuyaCollabPage() {
size="small"
initialValues={{
order_no: detail.order_no, shop: detail.shop, owner_id: detail.owner_id,
- bind_douyu_account: detail.bind_douyu_account,
gift_name: detail.gift_name, remark: detail.remark,
}}
onFinish={(values) => void patchOrder(detail, values)}
@@ -914,19 +1213,15 @@ export default function HuyaCollabPage() {
-
+
+
+
-
-
-
-
-
@@ -949,19 +1244,40 @@ export default function HuyaCollabPage() {
>
)}
- 截图(第一张是封面)
- {(['bind_images', 'home_images'] as HuyaCollabImageSlot[]).map((slot) => (
- void uploadImage(detail, targetSlot, file)}
- onRemove={(targetSlot, key) => void removeImage(detail, targetSlot, key)}
- onSetCover={(targetSlot, key) => void setCoverImage(detail, targetSlot, key)}
- />
- ))}
+ 主页截图(第一张是封面)
+ void uploadImage(detail, targetSlot, file)}
+ onRemove={(targetSlot, key) => void removeImage(detail, targetSlot, key)}
+ onSetCover={(targetSlot, key) => void setCoverImage(detail, targetSlot, key)}
+ />
+ {detail.bind_images.length > 0 && (
+ // 老数据只读展示:绑定图已停用,但已经传过的图不该从界面上凭空消失
+ <>
+
+
+ 历史绑定图(只读,已停用 —— 绑定关系改由「验证 CK」的探针读取)
+
+
+
+
+ {detail.bind_images.map((key, index) => (
+
+ ))}
+
+
+ >
+ )}
{canEdit && (
@@ -1012,39 +1328,181 @@ export default function HuyaCollabPage() {
setCreateOpen(false)}
onOk={() => void submitCreate()}
confirmLoading={busy}
okText="新建"
+ okButtonProps={{ disabled: !ckReady || previewing }}
+ // 内容比一屏高:给个滚动区,别让底部按钮被顶出可视范围
+ styles={{ body: { maxHeight: '78vh', overflowY: 'auto', paddingRight: 12 } }}
destroyOnHidden
>
-
-
-
-
-
+
-
+ {/* 店铺独占一行:按钮要够宽才不会被挤到第二行(「新店」跟在同一行后面) */}
+
+
-
-
-
+
+ {/* 高度随内容走:短 CK 就 3 行,别固定 5 行把弹窗撑高 */}
+
+
+ {(previewing || ckPreview) && (
+
+ {previewing ? (
+
+ ) : ckPreview?.ok && ckPreview.has_role ? (
+
+
+ {`订单编号里的账号:${ckPreview.bind_game_account || '-'}`
+ + `(${ckPreview.bind_game_channel || '-'})`
+ + `;虎牙号 ${ckPreview.uid}`
+ + (giftSourceText ? `;${giftSourceText}` : '')}
+
+ {ckPreview.uid_used_by.length > 0 && (
+ // 放开 uid 唯一性后同一份 CK 可以服务多单,这里只提示已经挂在哪些单上
+
+ {`这份 CK 的虎牙号已经挂在:${ckPreview.uid_used_by
+ .map((item) => `${item.order_no}(${item.status_label || item.status})`)
+ .join('、')} —— 同一个号服务多单是允许的,照常提交`}
+
+ )}
+ >
+ }
+ />
+ ) : (
+
+ )}
+
+ )}
+
+
+ 0
+ ? `礼包列表缓存于 ${giftCacheLabel}(${giftOptions.length} 个)`
+ : undefined)}
+ validateStatus={giftHint ? 'warning' : undefined}
+ >
+ ({
+ 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())}
+ onChange={(value: string) => {
+ // 记住最近选的礼包:连开几单同一个礼包就不用反复点
+ setGiftHint('');
+ if (value) window.localStorage.setItem(LAST_GIFT_KEY, value);
+ }}
+ />
+
+
+
+
+ }
+ disabled={!ckReady || previewing}
+ onClick={() => scheduleCkPreview(createForm.getFieldValue('ck_text') || '', true, true)}
+ >
+ 强制刷新礼包{giftOptions.length ? `(${giftOptions.length})` : ''}
+
+
+
+
+
+
+ (
+ value && value.length > 0
+ ? Promise.resolve()
+ : Promise.reject(new Error('请至少上传 1 张主页截图'))
+ ),
+ }]}
+ >
+
+
+
+
+
+
+
+
-
setImportOpen(false)}
onOk={() => void submitImport()}
confirmLoading={importing}
@@ -1145,6 +1603,15 @@ export default function HuyaCollabPage() {
)}
+
+ setShopModalOpen(false)}
+ onChanged={() => void load()}
+ />
);
}