1532 lines
49 KiB
TypeScript
1532 lines
49 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||
import { useLocation, useNavigate } from 'react-router-dom';
|
||
import {
|
||
Alert,
|
||
AutoComplete,
|
||
Button,
|
||
Checkbox,
|
||
Col,
|
||
Descriptions,
|
||
Drawer,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
Modal,
|
||
Popconfirm,
|
||
Row,
|
||
Select,
|
||
Space,
|
||
Statistic,
|
||
Table,
|
||
Tag,
|
||
Tooltip,
|
||
Typography,
|
||
theme,
|
||
} from 'antd';
|
||
import type { TableProps } from 'antd';
|
||
import {
|
||
CopyOutlined,
|
||
DeleteOutlined,
|
||
EditOutlined,
|
||
FileExcelOutlined,
|
||
InboxOutlined,
|
||
PlusOutlined,
|
||
ReloadOutlined,
|
||
SafetyCertificateOutlined,
|
||
SearchOutlined,
|
||
ShopOutlined,
|
||
SyncOutlined,
|
||
UndoOutlined,
|
||
} from '@ant-design/icons';
|
||
import HuyaCollabHomeImageUploader from '../components/HuyaCollabHomeImageUploader';
|
||
import HuyaCollabShopPicker from '../components/HuyaCollabShopPicker';
|
||
import {
|
||
weiboCollabApi,
|
||
type WeiboCollabAccountLookup,
|
||
type WeiboCollabAccountOption,
|
||
type WeiboCollabBalanceCheckResult,
|
||
type WeiboCollabExchangeCheckResult,
|
||
type WeiboCollabGoods,
|
||
type WeiboCollabMetric,
|
||
type WeiboCollabOrder,
|
||
type WeiboCollabOrderCreate,
|
||
type WeiboCollabOrderUpdate,
|
||
type WeiboCollabShop,
|
||
type WeiboCollabStatus,
|
||
type WeiboCollabSummary,
|
||
type WeiboCollabTimeline,
|
||
} from '../api/modules';
|
||
import { useCollabRealtime } from '../hooks/useCollabRealtime';
|
||
import { usePermissions } from '../hooks/usePermissions';
|
||
import { getErrorMessage } from '../utils/error';
|
||
import { message } from '../utils/antdMessage';
|
||
import { copyTextToClipboard } from '../utils/clipboard';
|
||
import { formatTime } from '../utils/time';
|
||
|
||
const { Text, Title } = Typography;
|
||
const { TextArea } = Input;
|
||
|
||
const STATUS_META: Record<WeiboCollabStatus, { label: string; color: string }> = {
|
||
incomplete: { label: '待补资料', color: 'default' },
|
||
to_recharge: { label: '待充值', color: 'blue' },
|
||
recharged: { label: '已充值', color: 'cyan' },
|
||
exchange_pending: { label: '兑换待确认', color: 'purple' },
|
||
done: { label: '兑换成功', color: 'success' },
|
||
weidou_error: { label: '微豆异常', color: 'orange' },
|
||
failed: { label: '兑换失败', color: 'error' },
|
||
void: { label: '已作废', color: 'default' },
|
||
};
|
||
|
||
const ACCOUNT_CK_LABELS: Record<string, string> = {
|
||
valid: '有效',
|
||
invalid: '失效',
|
||
error: '查询失败',
|
||
};
|
||
|
||
const STATUS_ORDER: WeiboCollabStatus[] = [
|
||
'incomplete',
|
||
'to_recharge',
|
||
'recharged',
|
||
'exchange_pending',
|
||
'done',
|
||
'weidou_error',
|
||
'failed',
|
||
'void',
|
||
];
|
||
|
||
const MANUAL_STATUS_OPTIONS = STATUS_ORDER.filter(
|
||
(item) => item !== 'recharged' && item !== 'weidou_error',
|
||
).map((value) => ({ value, label: STATUS_META[value].label }));
|
||
|
||
const VERDICT_META: Record<string, { label: string; color: string }> = {
|
||
success: { label: '唯一命中', color: 'success' },
|
||
failed: { label: '未命中', color: 'error' },
|
||
unknown: { label: '待确认', color: 'warning' },
|
||
'': { label: '未判定', color: 'default' },
|
||
};
|
||
|
||
function balanceMeta(reason: string) {
|
||
if (reason === 'ok') return { label: '余额正常', color: 'success' };
|
||
if (reason === 'shortage') return { label: '微豆不足', color: 'error' };
|
||
if (reason === 'excess') return { label: '超出阈值', color: 'warning' };
|
||
return { label: '未核账', color: 'default' };
|
||
}
|
||
|
||
interface OrderFormValues {
|
||
order_no: string;
|
||
shop?: string;
|
||
owner_id?: number | null;
|
||
account_id?: number | null;
|
||
goods_name?: string;
|
||
goods_image?: string;
|
||
goods_id?: number | null;
|
||
required_weidou?: number | null;
|
||
home_images?: string[];
|
||
remark?: string;
|
||
draft?: boolean;
|
||
}
|
||
|
||
export default function WeiboCollabPage() {
|
||
const { token } = theme.useToken();
|
||
const { can } = usePermissions();
|
||
const location = useLocation();
|
||
const navigate = useNavigate();
|
||
const canEdit = can('weibo:collab:edit');
|
||
const canManage = can('weibo:collab:manage');
|
||
|
||
const [orders, setOrders] = useState<WeiboCollabOrder[]>([]);
|
||
const [summary, setSummary] = useState<WeiboCollabSummary | null>(null);
|
||
const [owners, setOwners] = useState<Array<{ id: number; username: string }>>([]);
|
||
const [shops, setShops] = useState<WeiboCollabShop[]>([]);
|
||
const [goodsCatalog, setGoodsCatalog] = useState<WeiboCollabGoods[]>([]);
|
||
const [accounts, setAccounts] = useState<WeiboCollabAccountOption[]>([]);
|
||
const [total, setTotal] = useState(0);
|
||
const [page, setPage] = useState(1);
|
||
const [pageSize, setPageSize] = useState(20);
|
||
const [loading, setLoading] = useState(false);
|
||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||
const [keyword, setKeyword] = useState('');
|
||
const [status, setStatus] = useState('');
|
||
const [shop, setShop] = useState('');
|
||
const [ownerId, setOwnerId] = useState<number | null>(null);
|
||
const [goodsName, setGoodsName] = useState('');
|
||
const [includeVoid, setIncludeVoid] = useState(false);
|
||
const [archiveScope, setArchiveScope] = useState<'active' | 'archived' | 'all'>('active');
|
||
const [threshold, setThreshold] = useState(100);
|
||
|
||
const [form] = Form.useForm<OrderFormValues>();
|
||
const [editOpen, setEditOpen] = useState(false);
|
||
const [editing, setEditing] = useState<WeiboCollabOrder | null>(null);
|
||
const [saving, setSaving] = useState(false);
|
||
const [lookup, setLookup] = useState<WeiboCollabAccountLookup | null>(null);
|
||
const [lookupLoading, setLookupLoading] = useState(false);
|
||
|
||
const [detailOpen, setDetailOpen] = useState(false);
|
||
const [detail, setDetail] = useState<WeiboCollabOrder | null>(null);
|
||
const [detailMetrics, setDetailMetrics] = useState<WeiboCollabMetric[]>([]);
|
||
const [detailTimeline, setDetailTimeline] = useState<WeiboCollabTimeline[]>([]);
|
||
const [detailLoading, setDetailLoading] = useState(false);
|
||
const transferHandledRef = useRef<number | null>(null);
|
||
|
||
const [statusOpen, setStatusOpen] = useState(false);
|
||
const [statusTarget, setStatusTarget] = useState<WeiboCollabStatus>('to_recharge');
|
||
const [statusReason, setStatusReason] = useState('');
|
||
const [statusRollback, setStatusRollback] = useState(false);
|
||
const [statusIds, setStatusIds] = useState<number[]>([]);
|
||
const [statusSaving, setStatusSaving] = useState(false);
|
||
|
||
const [shopOpen, setShopOpen] = useState(false);
|
||
const [goodsOpen, setGoodsOpen] = useState(false);
|
||
const [shopForm] = Form.useForm<{ name: string; remark?: string }>();
|
||
const [goodsForm] = Form.useForm<{
|
||
name: string;
|
||
price: number;
|
||
goods_id?: number | null;
|
||
goods_image?: string;
|
||
remark?: string;
|
||
}>();
|
||
|
||
const loadBase = useCallback(async () => {
|
||
try {
|
||
const [summaryData, ownerRows, shopRows, goodsRows, accountData] =
|
||
await Promise.all([
|
||
weiboCollabApi.summary({ include_void: includeVoid, archive_scope: archiveScope }),
|
||
weiboCollabApi.owners(),
|
||
weiboCollabApi.listShops(),
|
||
weiboCollabApi.listGoods(),
|
||
weiboCollabApi.accounts(),
|
||
]);
|
||
setSummary(summaryData);
|
||
setOwners(ownerRows);
|
||
setShops(shopRows);
|
||
setGoodsCatalog(goodsRows);
|
||
setAccounts(accountData);
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
}, [archiveScope, includeVoid]);
|
||
|
||
const loadOrders = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await weiboCollabApi.listOrders({
|
||
page,
|
||
page_size: pageSize,
|
||
archive_scope: archiveScope,
|
||
status: status || undefined,
|
||
shop: shop || undefined,
|
||
owner_id: ownerId ?? undefined,
|
||
goods_name: goodsName || undefined,
|
||
keyword: keyword.trim() || undefined,
|
||
include_void: includeVoid,
|
||
});
|
||
setOrders(data.items);
|
||
setTotal(data.total);
|
||
setSelectedIds((ids) => ids.filter((id) => data.items.some((row) => row.id === id)));
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [
|
||
archiveScope,
|
||
goodsName,
|
||
includeVoid,
|
||
keyword,
|
||
ownerId,
|
||
page,
|
||
pageSize,
|
||
shop,
|
||
status,
|
||
]);
|
||
|
||
const refreshAll = useCallback(() => {
|
||
void loadBase();
|
||
void loadOrders();
|
||
}, [loadBase, loadOrders]);
|
||
|
||
useEffect(() => {
|
||
void loadBase();
|
||
}, [loadBase]);
|
||
|
||
useEffect(() => {
|
||
void loadOrders();
|
||
}, [loadOrders]);
|
||
|
||
useCollabRealtime({
|
||
enabled: true,
|
||
path: '/api/weibo/external-orders/ws',
|
||
onInvalidate: () => refreshAll(),
|
||
});
|
||
|
||
const accountOptions = accounts.map((item) => ({
|
||
value: item.id,
|
||
label: `${item.nickname || item.uid} · UID ${item.uid}${item.game_role_name ? ` · ${item.game_role_name}` : ''}`,
|
||
title: item.nickname || `UID ${item.uid}`,
|
||
}));
|
||
|
||
const liveGoodsOptions = (lookup?.goods || []).map((item) => ({
|
||
value: item.goods_name,
|
||
label: `${item.goods_name} · ${item.price} 微豆${item.is_sold_out ? ' · 售罄' : ''}`,
|
||
goods: {
|
||
name: item.goods_name,
|
||
price: item.price,
|
||
goods_id: item.id,
|
||
goods_image: item.goods_image,
|
||
},
|
||
}));
|
||
const catalogGoodsOptions = goodsCatalog
|
||
.filter((item) => item.is_active)
|
||
.map((item) => ({
|
||
value: item.name,
|
||
label: `${item.name} · ${item.price} 微豆(自定义)`,
|
||
goods: {
|
||
name: item.name,
|
||
price: item.price,
|
||
goods_id: item.goods_id,
|
||
goods_image: item.goods_image,
|
||
},
|
||
}));
|
||
const goodsOptions: Array<{
|
||
value: string;
|
||
label: string;
|
||
goods: {
|
||
name: string;
|
||
price: number;
|
||
goods_id: number | null;
|
||
goods_image: string;
|
||
};
|
||
}> = [...catalogGoodsOptions];
|
||
for (const option of liveGoodsOptions) {
|
||
if (!goodsOptions.some((item) => item.value === option.value)) goodsOptions.push(option);
|
||
}
|
||
|
||
const lookupAccount = useCallback(async (accountId?: number | null) => {
|
||
setLookup(null);
|
||
if (!accountId) return;
|
||
setLookupLoading(true);
|
||
try {
|
||
const result = await weiboCollabApi.lookupAccount(accountId, true);
|
||
setLookup(result);
|
||
if (!result.ok) message.warning(result.message || '账号当前不可用');
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
} finally {
|
||
setLookupLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
const openCreate = () => {
|
||
setEditing(null);
|
||
setLookup(null);
|
||
form.resetFields();
|
||
form.setFieldsValue({
|
||
owner_id: owners[0]?.id,
|
||
required_weidou: 0,
|
||
home_images: [],
|
||
draft: false,
|
||
});
|
||
setEditOpen(true);
|
||
};
|
||
|
||
const transferAccountId = (
|
||
location.state as { transferAccountId?: number } | null
|
||
)?.transferAccountId;
|
||
|
||
useEffect(() => {
|
||
if (!transferAccountId || transferHandledRef.current === transferAccountId) {
|
||
return;
|
||
}
|
||
transferHandledRef.current = transferAccountId;
|
||
setEditing(null);
|
||
setLookup(null);
|
||
form.resetFields();
|
||
form.setFieldsValue({
|
||
owner_id: owners[0]?.id,
|
||
account_id: transferAccountId,
|
||
required_weidou: 0,
|
||
home_images: [],
|
||
draft: false,
|
||
});
|
||
setEditOpen(true);
|
||
void lookupAccount(transferAccountId);
|
||
navigate(location.pathname, { replace: true, state: null });
|
||
}, [
|
||
form,
|
||
location.pathname,
|
||
lookupAccount,
|
||
navigate,
|
||
owners,
|
||
transferAccountId,
|
||
]);
|
||
|
||
const openEdit = (order: WeiboCollabOrder) => {
|
||
setEditing(order);
|
||
form.setFieldsValue({
|
||
order_no: order.order_no,
|
||
shop: order.shop,
|
||
owner_id: order.owner_id,
|
||
account_id: order.account_id,
|
||
goods_id: order.goods_id,
|
||
goods_name: order.goods_name,
|
||
goods_image: order.goods_image,
|
||
required_weidou: order.required_weidou,
|
||
home_images: order.home_images,
|
||
remark: order.remark,
|
||
draft: false,
|
||
});
|
||
void lookupAccount(order.account_id);
|
||
setEditOpen(true);
|
||
};
|
||
|
||
const handleGoodsChange = (value: string) => {
|
||
const selected = goodsOptions.find((item) => item.value === value);
|
||
if (!selected) return;
|
||
form.setFieldsValue({
|
||
goods_name: selected.goods.name,
|
||
required_weidou: selected.goods.price,
|
||
goods_id: selected.goods.goods_id,
|
||
goods_image: selected.goods.goods_image,
|
||
});
|
||
};
|
||
|
||
const saveOrder = async () => {
|
||
const values = await form.validateFields();
|
||
setSaving(true);
|
||
try {
|
||
if (editing) {
|
||
const update: WeiboCollabOrderUpdate = {
|
||
order_no: values.order_no,
|
||
shop: values.shop || '',
|
||
owner_id: values.owner_id ?? null,
|
||
goods_id: values.goods_id ?? null,
|
||
goods_name: values.goods_name || '',
|
||
goods_image: values.goods_image || '',
|
||
required_weidou: values.required_weidou ?? null,
|
||
home_images: values.home_images || [],
|
||
remark: values.remark || '',
|
||
expected_version: editing.version,
|
||
};
|
||
await weiboCollabApi.updateOrder(editing.id, update);
|
||
if (editing.account_id !== values.account_id) {
|
||
if (values.account_id) {
|
||
await weiboCollabApi.setAccount(editing.id, values.account_id);
|
||
} else {
|
||
message.warning('订单暂不支持取消关联账号');
|
||
}
|
||
}
|
||
} else {
|
||
const create: WeiboCollabOrderCreate = {
|
||
order_no: values.order_no,
|
||
shop: values.shop || '',
|
||
owner_id: values.owner_id ?? null,
|
||
account_id: values.account_id ?? null,
|
||
goods_id: values.goods_id ?? null,
|
||
goods_name: values.goods_name || '',
|
||
goods_image: values.goods_image || '',
|
||
required_weidou: values.required_weidou ?? null,
|
||
home_images: values.home_images || [],
|
||
remark: values.remark || '',
|
||
draft: Boolean(values.draft),
|
||
require_verified: !values.draft,
|
||
};
|
||
await weiboCollabApi.createOrder(create);
|
||
}
|
||
setEditOpen(false);
|
||
message.success(editing ? '订单已更新' : values.draft ? '草稿已创建' : '协作单已创建');
|
||
refreshAll();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const openDetail = async (order: WeiboCollabOrder) => {
|
||
setDetail(order);
|
||
setDetailOpen(true);
|
||
setDetailLoading(true);
|
||
try {
|
||
const [fresh, metrics, timeline] = await Promise.all([
|
||
weiboCollabApi.getOrder(order.id),
|
||
weiboCollabApi.metrics(order.id),
|
||
weiboCollabApi.timeline(order.id),
|
||
]);
|
||
setDetail(fresh);
|
||
setDetailMetrics(metrics);
|
||
setDetailTimeline(timeline);
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
} finally {
|
||
setDetailLoading(false);
|
||
}
|
||
};
|
||
|
||
const verifyOrders = async (ids: number[]) => {
|
||
if (!ids.length) return;
|
||
try {
|
||
const result = await weiboCollabApi.verifyBatch(ids);
|
||
if (result.success) message.success(`已校验 ${ids.length} 单`);
|
||
else message.warning(`校验完成,${result.results.filter((item) => !item.success).length} 单失败`);
|
||
refreshAll();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
};
|
||
|
||
const runBalanceCheck = async (ids: number[]) => {
|
||
if (!ids.length) return;
|
||
try {
|
||
const result: WeiboCollabBalanceCheckResult = await weiboCollabApi.checkBalance(
|
||
ids,
|
||
threshold,
|
||
);
|
||
const failed = result.failures.length;
|
||
message[failed ? 'warning' : 'success'](
|
||
`余额核账完成:更新 ${result.updated} 单${failed ? `,失败 ${failed} 单` : ''}`,
|
||
);
|
||
refreshAll();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
};
|
||
|
||
const runExchangeCheck = async (ids: number[]) => {
|
||
if (!ids.length) return;
|
||
try {
|
||
const result: WeiboCollabExchangeCheckResult =
|
||
await weiboCollabApi.checkExchangeRecords(ids);
|
||
message.success(
|
||
`兑换记录查询完成:命中 ${result.success},待确认 ${result.unknown},失败 ${result.failed}`,
|
||
);
|
||
refreshAll();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
};
|
||
|
||
const openStatus = (ids: number[], initial?: WeiboCollabStatus) => {
|
||
if (!ids.length) return;
|
||
setStatusIds(ids);
|
||
setStatusTarget(initial || 'to_recharge');
|
||
setStatusReason('');
|
||
setStatusRollback(false);
|
||
setStatusOpen(true);
|
||
};
|
||
|
||
const saveStatus = async () => {
|
||
setStatusSaving(true);
|
||
try {
|
||
if (statusIds.length === 1) {
|
||
const row = orders.find((item) => item.id === statusIds[0]);
|
||
await weiboCollabApi.updateStatus(statusIds[0], {
|
||
status: statusTarget,
|
||
reason: statusReason,
|
||
rollback: statusRollback,
|
||
expected_version: row?.version,
|
||
});
|
||
} else {
|
||
await weiboCollabApi.batchStatus({
|
||
ids: statusIds,
|
||
status: statusTarget,
|
||
reason: statusReason,
|
||
rollback: statusRollback,
|
||
});
|
||
}
|
||
setStatusOpen(false);
|
||
message.success(`已更新为「${STATUS_META[statusTarget].label}」`);
|
||
refreshAll();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
} finally {
|
||
setStatusSaving(false);
|
||
}
|
||
};
|
||
|
||
const archiveOrders = async (ids: number[]) => {
|
||
if (!ids.length) return;
|
||
try {
|
||
const result = await weiboCollabApi.archive(ids);
|
||
message[result.archived ? 'success' : 'warning'](
|
||
`已归档 ${result.archived} 单${result.skipped ? `,跳过 ${result.skipped} 单` : ''}`,
|
||
);
|
||
setSelectedIds([]);
|
||
refreshAll();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
};
|
||
|
||
const unarchiveOrders = async (ids: number[]) => {
|
||
if (!ids.length) return;
|
||
try {
|
||
const result = await weiboCollabApi.unarchive(ids);
|
||
message.success(`已取消归档 ${result.unarchived} 单`);
|
||
setSelectedIds([]);
|
||
refreshAll();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
};
|
||
|
||
const deleteOrder = async (id: number) => {
|
||
try {
|
||
await weiboCollabApi.deleteOrder(id);
|
||
message.success('协作单已删除');
|
||
refreshAll();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
};
|
||
|
||
const createShop = async () => {
|
||
const values = await shopForm.validateFields();
|
||
try {
|
||
await weiboCollabApi.createShop(values);
|
||
shopForm.resetFields();
|
||
await loadBase();
|
||
message.success('店铺已新增');
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
};
|
||
|
||
const createGoods = async () => {
|
||
const values = await goodsForm.validateFields();
|
||
try {
|
||
await weiboCollabApi.createGoods(values);
|
||
goodsForm.resetFields();
|
||
await loadBase();
|
||
message.success('商品已新增');
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
};
|
||
|
||
const statusCards = useMemo(
|
||
() => [
|
||
{ key: '', label: '全部', value: summary?.total || 0 },
|
||
...STATUS_ORDER.map((item) => ({
|
||
key: item,
|
||
label: STATUS_META[item].label,
|
||
value: summary?.by_status[item] || 0,
|
||
})),
|
||
],
|
||
[summary],
|
||
);
|
||
|
||
const copySelectedAccountValues = async (kind: 'username' | 'uid') => {
|
||
const selectedOrders = orders.filter((row) => selectedIds.includes(row.id));
|
||
const values = Array.from(
|
||
new Set(
|
||
selectedOrders
|
||
.map((row) =>
|
||
kind === 'uid'
|
||
? row.account_uid
|
||
: row.account_nickname || row.account_uid,
|
||
)
|
||
.map((value) => String(value || '').trim())
|
||
.filter(Boolean),
|
||
),
|
||
);
|
||
if (values.length === 0) {
|
||
message.warning(
|
||
kind === 'uid' ? '所选订单没有微博 UID' : '所选订单没有微博用户名',
|
||
);
|
||
return;
|
||
}
|
||
try {
|
||
await copyTextToClipboard(values.join('\n'));
|
||
message.success(
|
||
`已复制 ${values.length} 个${
|
||
kind === 'uid' ? ' UID' : '用户名'
|
||
},可直接粘贴到工作台导入搜索框`,
|
||
);
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
};
|
||
|
||
const columns: TableProps<WeiboCollabOrder>['columns'] = [
|
||
{
|
||
title: '序号',
|
||
key: 'index',
|
||
width: 72,
|
||
align: 'center',
|
||
fixed: 'left',
|
||
render: (_, __, index) => (page - 1) * pageSize + index + 1,
|
||
},
|
||
{
|
||
title: '订单编号',
|
||
dataIndex: 'order_no',
|
||
width: 170,
|
||
fixed: 'left',
|
||
render: (value: string, row) => (
|
||
<Space orientation="vertical" size={0}>
|
||
<Text strong copyable={{ text: value }}>
|
||
{value}
|
||
</Text>
|
||
{row.is_archived && <Tag>已归档</Tag>}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '店铺 / 负责人',
|
||
width: 150,
|
||
render: (_, row) => (
|
||
<Space orientation="vertical" size={0}>
|
||
<Text>{row.shop || '-'}</Text>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
{row.owner_name || '-'}
|
||
</Text>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '微博账号 / 角色',
|
||
width: 210,
|
||
render: (_, row) => (
|
||
<Space orientation="vertical" size={0}>
|
||
<Text
|
||
copyable={
|
||
row.account_nickname || row.account_uid
|
||
? {
|
||
text: row.account_nickname || row.account_uid || '',
|
||
tooltips: ['复制用户名', '已复制用户名'],
|
||
}
|
||
: false
|
||
}
|
||
>
|
||
{row.account_nickname || row.account_uid || '未关联'}
|
||
</Text>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
{row.account_uid ? (
|
||
<Text
|
||
type="secondary"
|
||
style={{ fontSize: 12 }}
|
||
copyable={{
|
||
text: row.account_uid,
|
||
tooltips: ['复制 UID', '已复制 UID'],
|
||
}}
|
||
>
|
||
UID {row.account_uid}
|
||
</Text>
|
||
) : (
|
||
'UID -'
|
||
)}
|
||
</Text>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
{row.game_role_name || '未读到角色'}
|
||
</Text>
|
||
<Space size={4}>
|
||
{row.account_is_bound === false && <Tag color="warning">未绑定</Tag>}
|
||
{!row.account_enabled && row.account_exists && <Tag>账号停用</Tag>}
|
||
{row.account_cluster && <Tag color="blue">同账号 {row.account_cluster.total} 单</Tag>}
|
||
</Space>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '商品 / 微豆',
|
||
width: 190,
|
||
render: (_, row) => (
|
||
<Space orientation="vertical" size={0}>
|
||
<Text>{row.goods_name || '未选商品'}</Text>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
需要 {row.required_weidou?.toLocaleString() ?? '-'} 微豆
|
||
</Text>
|
||
<Tag color={VERDICT_META[row.exchange_verdict]?.color}>
|
||
{VERDICT_META[row.exchange_verdict]?.label}
|
||
</Tag>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
width: 130,
|
||
render: (value: WeiboCollabStatus, row) => (
|
||
<Space size={4} wrap>
|
||
<Tag color={STATUS_META[value]?.color}>{row.status_label || value}</Tag>
|
||
{row.exchange_verdict === 'unknown' && <Tag color="warning">待确认</Tag>}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '最近核账',
|
||
width: 200,
|
||
render: (_, row) => {
|
||
const meta = balanceMeta(row.weidou_check_reason);
|
||
return (
|
||
<Tooltip title={row.weidou_check_message}>
|
||
<Space orientation="vertical" size={0}>
|
||
<Text>
|
||
余额 {row.weidou_balance?.toLocaleString() ?? '-'} / 合计{' '}
|
||
{row.weidou_required_total?.toLocaleString() ?? '-'}
|
||
</Text>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
结余 {row.weidou_surplus?.toLocaleString() ?? '-'}
|
||
</Text>
|
||
<Tag color={meta.color}>{meta.label}</Tag>
|
||
</Space>
|
||
</Tooltip>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: '最近改动',
|
||
width: 150,
|
||
render: (_, row) => (
|
||
<Space orientation="vertical" size={0}>
|
||
<Text>{row.updated_by_name || '-'}</Text>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
{formatTime(row.updated_at)}
|
||
</Text>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '操作',
|
||
width: 300,
|
||
fixed: 'right',
|
||
render: (_, row) => (
|
||
<Space size={4} wrap>
|
||
<Button size="small" onClick={() => void openDetail(row)}>
|
||
详情
|
||
</Button>
|
||
{canEdit && !row.is_archived && (
|
||
<>
|
||
<Button size="small" icon={<EditOutlined />} onClick={() => openEdit(row)}>
|
||
编辑
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
icon={<SafetyCertificateOutlined />}
|
||
onClick={() => void verifyOrders([row.id])}
|
||
>
|
||
校验
|
||
</Button>
|
||
<Button size="small" onClick={() => openStatus([row.id])}>
|
||
状态
|
||
</Button>
|
||
</>
|
||
)}
|
||
{canManage && (
|
||
<Popconfirm
|
||
title="删除这条协作单?"
|
||
okText="删除"
|
||
cancelText="取消"
|
||
okButtonProps={{ danger: true }}
|
||
onConfirm={() => void deleteOrder(row.id)}
|
||
>
|
||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||
</Popconfirm>
|
||
)}
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<Space orientation="vertical" size={16} style={{ width: '100%' }}>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
gap: 16,
|
||
flexWrap: 'wrap',
|
||
}}
|
||
>
|
||
<div>
|
||
<Title level={4} style={{ margin: 0 }}>
|
||
微博外部单协作
|
||
</Title>
|
||
<Text type="secondary">外部 CK 账号核账、兑换确认与协作台账</Text>
|
||
</div>
|
||
<Space wrap>
|
||
<Input
|
||
allowClear
|
||
prefix={<SearchOutlined />}
|
||
placeholder="单号、UID、昵称、角色、商品、备注"
|
||
value={keyword}
|
||
onChange={(event) => {
|
||
setKeyword(event.target.value);
|
||
setPage(1);
|
||
}}
|
||
style={{ width: 260 }}
|
||
/>
|
||
<Button icon={<ReloadOutlined />} onClick={refreshAll}>
|
||
刷新
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(118px, 1fr))',
|
||
gap: 8,
|
||
}}
|
||
>
|
||
{statusCards.map((item) => (
|
||
<button
|
||
key={item.key || 'all'}
|
||
type="button"
|
||
onClick={() => {
|
||
setStatus(item.key);
|
||
setPage(1);
|
||
}}
|
||
style={{
|
||
border: `1px solid ${status === item.key ? token.colorPrimary : token.colorBorderSecondary}`,
|
||
background: status === item.key ? token.colorPrimaryBg : token.colorBgContainer,
|
||
borderRadius: 6,
|
||
padding: '10px 12px',
|
||
textAlign: 'left',
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
<Statistic title={item.label} value={item.value} valueStyle={{ fontSize: 20 }} />
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
background: token.colorBgContainer,
|
||
border: `1px solid ${token.colorBorderSecondary}`,
|
||
borderRadius: 6,
|
||
padding: 12,
|
||
}}
|
||
>
|
||
<Space wrap>
|
||
<Select
|
||
value={shop}
|
||
onChange={(value) => {
|
||
setShop(value);
|
||
setPage(1);
|
||
}}
|
||
allowClear
|
||
placeholder="店铺"
|
||
style={{ width: 140 }}
|
||
options={shops
|
||
.filter((item) => item.is_active)
|
||
.map((item) => ({ value: item.name, label: item.name }))}
|
||
/>
|
||
<Select
|
||
value={ownerId ?? undefined}
|
||
onChange={(value) => {
|
||
setOwnerId(value ?? null);
|
||
setPage(1);
|
||
}}
|
||
allowClear
|
||
placeholder="负责人"
|
||
style={{ width: 130 }}
|
||
options={owners.map((item) => ({ value: item.id, label: item.username }))}
|
||
/>
|
||
<Select
|
||
value={goodsName || undefined}
|
||
onChange={(value) => {
|
||
setGoodsName(value || '');
|
||
setPage(1);
|
||
}}
|
||
allowClear
|
||
placeholder="商品"
|
||
style={{ width: 180 }}
|
||
options={goodsCatalog.map((item) => ({ value: item.name, label: item.name }))}
|
||
/>
|
||
<Select
|
||
value={status}
|
||
onChange={(value) => {
|
||
setStatus(value);
|
||
setPage(1);
|
||
}}
|
||
style={{ width: 130 }}
|
||
options={[
|
||
{ value: '', label: '全部状态' },
|
||
...STATUS_ORDER.map((item) => ({ value: item, label: STATUS_META[item].label })),
|
||
]}
|
||
/>
|
||
<Select
|
||
value={archiveScope}
|
||
onChange={(value) => {
|
||
setArchiveScope(value);
|
||
setPage(1);
|
||
}}
|
||
style={{ width: 120 }}
|
||
options={[
|
||
{ value: 'active', label: '活跃台账' },
|
||
{ value: 'archived', label: '已归档' },
|
||
{ value: 'all', label: '全部' },
|
||
]}
|
||
/>
|
||
<Checkbox
|
||
checked={includeVoid}
|
||
onChange={(event) => {
|
||
setIncludeVoid(event.target.checked);
|
||
setPage(1);
|
||
}}
|
||
>
|
||
显示作废
|
||
</Checkbox>
|
||
<InputNumber
|
||
min={0}
|
||
max={1_000_000}
|
||
value={threshold}
|
||
onChange={(value) => setThreshold(Number(value) || 0)}
|
||
addonBefore="核账阈值"
|
||
style={{ width: 150 }}
|
||
/>
|
||
<Button icon={<ShopOutlined />} onClick={() => setShopOpen(true)}>
|
||
店铺管理
|
||
</Button>
|
||
<Button icon={<FileExcelOutlined />} onClick={() => setGoodsOpen(true)}>
|
||
商品管理
|
||
</Button>
|
||
{canEdit && (
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||
新建协作单
|
||
</Button>
|
||
)}
|
||
</Space>
|
||
</div>
|
||
|
||
<Space wrap>
|
||
<Text type="secondary">已选 {selectedIds.length} 单</Text>
|
||
{selectedIds.length > 0 && (
|
||
<>
|
||
<Button
|
||
icon={<CopyOutlined />}
|
||
onClick={() => void copySelectedAccountValues('username')}
|
||
>
|
||
复制所选用户名
|
||
</Button>
|
||
<Button
|
||
icon={<CopyOutlined />}
|
||
onClick={() => void copySelectedAccountValues('uid')}
|
||
>
|
||
复制所选 UID
|
||
</Button>
|
||
</>
|
||
)}
|
||
{canEdit && selectedIds.length > 0 && archiveScope !== 'archived' && (
|
||
<>
|
||
<Button
|
||
icon={<SafetyCertificateOutlined />}
|
||
onClick={() => void verifyOrders(selectedIds)}
|
||
>
|
||
批量校验
|
||
</Button>
|
||
<Button icon={<SyncOutlined />} onClick={() => void runBalanceCheck(selectedIds)}>
|
||
充值核账
|
||
</Button>
|
||
<Button icon={<SyncOutlined />} onClick={() => void runExchangeCheck(selectedIds)}>
|
||
查兑换记录
|
||
</Button>
|
||
<Button onClick={() => openStatus(selectedIds)}>批量改状态</Button>
|
||
<Button icon={<InboxOutlined />} onClick={() => void archiveOrders(selectedIds)}>
|
||
归档
|
||
</Button>
|
||
</>
|
||
)}
|
||
{canEdit && selectedIds.length > 0 && archiveScope === 'archived' && (
|
||
<Button icon={<UndoOutlined />} onClick={() => void unarchiveOrders(selectedIds)}>
|
||
取消归档
|
||
</Button>
|
||
)}
|
||
</Space>
|
||
|
||
{archiveScope === 'archived' && (
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="正在查看已归档订单"
|
||
description="归档订单只读,取消归档后可继续操作。"
|
||
/>
|
||
)}
|
||
|
||
<Table<WeiboCollabOrder>
|
||
rowKey="id"
|
||
loading={loading}
|
||
columns={columns}
|
||
dataSource={orders}
|
||
scroll={{ x: 1500 }}
|
||
rowSelection={{
|
||
selectedRowKeys: selectedIds,
|
||
onChange: (keys) => setSelectedIds(keys.map(Number)),
|
||
}}
|
||
pagination={{
|
||
current: page,
|
||
pageSize,
|
||
total,
|
||
showSizeChanger: true,
|
||
showTotal: (value) => `共 ${value} 单`,
|
||
onChange: (nextPage, nextSize) => {
|
||
setPage(nextPage);
|
||
setPageSize(nextSize);
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<Modal
|
||
title={editing ? `编辑协作单 · ${editing.order_no}` : '新建微博外部单'}
|
||
open={editOpen}
|
||
width={920}
|
||
confirmLoading={saving}
|
||
okText={editing ? '保存' : form.getFieldValue('draft') ? '保存草稿' : '创建并校验'}
|
||
cancelText="取消"
|
||
onOk={() => void saveOrder()}
|
||
onCancel={() => setEditOpen(false)}
|
||
styles={{ body: { maxHeight: '78vh', overflowY: 'auto', paddingRight: 12 } }}
|
||
destroyOnHidden
|
||
>
|
||
<Form form={form} layout="vertical">
|
||
<Row gutter={16}>
|
||
<Col xs={24} md={12}>
|
||
<Form.Item
|
||
name="order_no"
|
||
label="订单编号"
|
||
rules={[{ required: true, message: '请填写订单编号' }]}
|
||
>
|
||
<Input maxLength={64} />
|
||
</Form.Item>
|
||
</Col>
|
||
<Col xs={24} md={12}>
|
||
<Form.Item name="owner_id" label="负责人">
|
||
<Select
|
||
allowClear
|
||
options={owners.map((item) => ({ value: item.id, label: item.username }))}
|
||
/>
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
<Form.Item name="shop" label="店铺">
|
||
<HuyaCollabShopPicker
|
||
shops={shops}
|
||
allowClear={false}
|
||
allowCustom
|
||
size="large"
|
||
/>
|
||
</Form.Item>
|
||
<Row gutter={16}>
|
||
<Col xs={24} md={12}>
|
||
<Form.Item name="account_id" label="微博账号">
|
||
<Select
|
||
allowClear
|
||
showSearch
|
||
optionFilterProp="label"
|
||
optionLabelProp="title"
|
||
options={accountOptions}
|
||
loading={lookupLoading}
|
||
placeholder="搜索并选择微博账号"
|
||
onChange={(value) => void lookupAccount(value)}
|
||
/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col xs={24} md={8}>
|
||
<Form.Item name="goods_name" label="商品">
|
||
<AutoComplete
|
||
options={goodsOptions}
|
||
onChange={handleGoodsChange}
|
||
placeholder={
|
||
lookup
|
||
? '选择当前账号商城商品'
|
||
: '选择自定义商品,或先选账号加载实时商品'
|
||
}
|
||
filterOption
|
||
/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col xs={24} md={4}>
|
||
<Form.Item
|
||
name="required_weidou"
|
||
label="所需微豆"
|
||
rules={[{ required: true, message: '请填写所需微豆' }]}
|
||
>
|
||
<InputNumber min={0} precision={0} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
<Form.Item name="goods_id" hidden>
|
||
<InputNumber />
|
||
</Form.Item>
|
||
<Form.Item name="goods_image" hidden>
|
||
<Input />
|
||
</Form.Item>
|
||
{lookup && (
|
||
<>
|
||
<Alert
|
||
type={lookup.ok ? 'success' : 'warning'}
|
||
showIcon
|
||
style={{ marginBottom: 12 }}
|
||
message={
|
||
lookup.ok
|
||
? `${lookup.nickname || lookup.uid} · 当前 ${lookup.weidou_balance?.toLocaleString() ?? '-'} 微豆`
|
||
: lookup.message || '账号不可用'
|
||
}
|
||
/>
|
||
{lookup.ok && (
|
||
<Descriptions
|
||
size="small"
|
||
column={3}
|
||
style={{ marginBottom: 16 }}
|
||
items={[
|
||
{ key: 'uid', label: 'UID', children: lookup.uid || '-' },
|
||
{
|
||
key: 'role',
|
||
label: '绑定角色',
|
||
children: lookup.game_role_name || '未读取到角色',
|
||
},
|
||
{
|
||
key: 'bind',
|
||
label: '绑定状态',
|
||
children:
|
||
lookup.is_bound === true
|
||
? '已绑定'
|
||
: lookup.is_bound === false
|
||
? '未绑定'
|
||
: '未知',
|
||
},
|
||
{
|
||
key: 'ck',
|
||
label: 'CK 状态',
|
||
children:
|
||
ACCOUNT_CK_LABELS[lookup.ck_check_status] ||
|
||
lookup.ck_check_status ||
|
||
'未检测',
|
||
},
|
||
{
|
||
key: 'orders',
|
||
label: '同账号订单',
|
||
span: 2,
|
||
children:
|
||
lookup.orders_using_account.length > 0
|
||
? lookup.orders_using_account
|
||
.map(
|
||
(item) =>
|
||
`${item.order_no}(${item.status_label || item.status})`,
|
||
)
|
||
.join('、')
|
||
: '暂无',
|
||
},
|
||
]}
|
||
/>
|
||
)}
|
||
</>
|
||
)}
|
||
<Row gutter={16}>
|
||
<Col xs={24} md={14}>
|
||
<Form.Item name="home_images" label="主页截图">
|
||
<HuyaCollabHomeImageUploader optional />
|
||
</Form.Item>
|
||
</Col>
|
||
<Col xs={24} md={10}>
|
||
<Form.Item name="remark" label="备注">
|
||
<TextArea rows={6} maxLength={512} showCount />
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
{!editing && (
|
||
<Form.Item name="draft" valuePropName="checked">
|
||
<Checkbox>先保存草稿,不校验账号</Checkbox>
|
||
</Form.Item>
|
||
)}
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title={`修改状态${statusIds.length > 1 ? `(${statusIds.length} 单)` : ''}`}
|
||
open={statusOpen}
|
||
confirmLoading={statusSaving}
|
||
okText="确认修改"
|
||
cancelText="取消"
|
||
onOk={() => void saveStatus()}
|
||
onCancel={() => setStatusOpen(false)}
|
||
>
|
||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||
<Select
|
||
value={statusTarget}
|
||
onChange={setStatusTarget}
|
||
options={MANUAL_STATUS_OPTIONS}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
<TextArea
|
||
rows={3}
|
||
value={statusReason}
|
||
onChange={(event) => setStatusReason(event.target.value)}
|
||
placeholder="原因或备注,作废时必填"
|
||
maxLength={256}
|
||
/>
|
||
<Checkbox checked={statusRollback} onChange={(e) => setStatusRollback(e.target.checked)}>
|
||
这是显式回退
|
||
</Checkbox>
|
||
</Space>
|
||
</Modal>
|
||
|
||
<Drawer
|
||
title={detail ? `协作单详情 · ${detail.order_no}` : '协作单详情'}
|
||
open={detailOpen}
|
||
onClose={() => setDetailOpen(false)}
|
||
size={880}
|
||
loading={detailLoading}
|
||
>
|
||
{detail && (
|
||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||
<Descriptions
|
||
size="small"
|
||
column={2}
|
||
items={[
|
||
{ key: 'shop', label: '店铺 / 负责人', children: `${detail.shop || '-'} / ${detail.owner_name || '-'}` },
|
||
{
|
||
key: 'account',
|
||
label: '微博账号',
|
||
children: (
|
||
<Space size={4} wrap>
|
||
<Text
|
||
copyable={
|
||
detail.account_nickname || detail.account_uid
|
||
? {
|
||
text:
|
||
detail.account_nickname ||
|
||
detail.account_uid ||
|
||
'',
|
||
tooltips: ['复制用户名', '已复制用户名'],
|
||
}
|
||
: false
|
||
}
|
||
>
|
||
{detail.account_nickname || '-'}
|
||
</Text>
|
||
{detail.account_uid && (
|
||
<Text
|
||
type="secondary"
|
||
copyable={{
|
||
text: detail.account_uid,
|
||
tooltips: ['复制 UID', '已复制 UID'],
|
||
}}
|
||
>
|
||
UID {detail.account_uid}
|
||
</Text>
|
||
)}
|
||
</Space>
|
||
),
|
||
},
|
||
{ key: 'role', label: '绑定角色', children: detail.game_role_name || '-' },
|
||
{ key: 'goods', label: '商品 / 所需微豆', children: `${detail.goods_name || '-'} / ${detail.required_weidou ?? '-'}` },
|
||
{ key: 'status', label: '业务状态', children: <Tag color={STATUS_META[detail.status]?.color}>{detail.status_label}</Tag> },
|
||
{ key: 'balance', label: '最近核账', children: detail.weidou_check_message || '-' },
|
||
{ key: 'exchange', label: '兑换确认', children: detail.exchange_message || '-' },
|
||
{ key: 'updated', label: '最后修改', children: `${detail.updated_by_name || '-'} · ${formatTime(detail.updated_at)}` },
|
||
]}
|
||
/>
|
||
{detail.home_images.length > 0 && (
|
||
<Space wrap>
|
||
{detail.home_images.map((key) => (
|
||
<img
|
||
key={key}
|
||
src={`/api/files/object?key=${encodeURIComponent(key)}&variant=thumb`}
|
||
alt="主页截图"
|
||
style={{ width: 96, height: 96, objectFit: 'cover', borderRadius: 4 }}
|
||
/>
|
||
))}
|
||
</Space>
|
||
)}
|
||
<Space wrap>
|
||
{canEdit && !detail.is_archived && (
|
||
<>
|
||
<Button icon={<SafetyCertificateOutlined />} onClick={() => void verifyOrders([detail.id])}>
|
||
校验账号
|
||
</Button>
|
||
<Button icon={<SyncOutlined />} onClick={() => void runBalanceCheck([detail.id])}>
|
||
充值核账
|
||
</Button>
|
||
<Button icon={<SyncOutlined />} onClick={() => void runExchangeCheck([detail.id])}>
|
||
查兑换记录
|
||
</Button>
|
||
<Button onClick={() => openStatus([detail.id], 'exchange_pending')}>
|
||
标记兑换发起
|
||
</Button>
|
||
</>
|
||
)}
|
||
</Space>
|
||
<Title level={5}>指标快照</Title>
|
||
<Table
|
||
rowKey="id"
|
||
size="small"
|
||
pagination={false}
|
||
dataSource={detailMetrics}
|
||
columns={[
|
||
{ title: '时间', dataIndex: 'created_at', width: 170, render: formatTime },
|
||
{ title: '来源', dataIndex: 'source', width: 130 },
|
||
{
|
||
title: '核账',
|
||
width: 220,
|
||
render: (_, row) =>
|
||
`余额 ${row.weidou_balance ?? '-'} / 合计 ${row.weidou_required_total ?? '-'} / 结余 ${row.weidou_surplus ?? '-'}`,
|
||
},
|
||
{
|
||
title: '兑换',
|
||
width: 220,
|
||
render: (_, row) =>
|
||
`${row.verdict_label || '未判定'} · ${row.records_matched}/${row.records_total}`,
|
||
},
|
||
{ title: '结论', dataIndex: 'message' },
|
||
]}
|
||
/>
|
||
<Title level={5}>操作时间线</Title>
|
||
<Table
|
||
rowKey="id"
|
||
size="small"
|
||
pagination={false}
|
||
dataSource={detailTimeline}
|
||
columns={[
|
||
{ title: '时间', dataIndex: 'created_at', width: 170, render: formatTime },
|
||
{ title: '操作人', dataIndex: 'username', width: 120 },
|
||
{ title: '动作', dataIndex: 'action', width: 190 },
|
||
{ title: '详情', dataIndex: 'detail' },
|
||
]}
|
||
/>
|
||
</Space>
|
||
)}
|
||
</Drawer>
|
||
|
||
<Modal
|
||
title="店铺管理"
|
||
open={shopOpen}
|
||
footer={null}
|
||
width={680}
|
||
onCancel={() => setShopOpen(false)}
|
||
>
|
||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||
<Form form={shopForm} layout="inline" onFinish={() => void createShop()}>
|
||
<Form.Item name="name" rules={[{ required: true, message: '店铺名' }]}>
|
||
<Input placeholder="店铺名" />
|
||
</Form.Item>
|
||
<Form.Item name="remark">
|
||
<Input placeholder="备注" />
|
||
</Form.Item>
|
||
<Button type="primary" htmlType="submit">
|
||
新增
|
||
</Button>
|
||
</Form>
|
||
<Table
|
||
rowKey="id"
|
||
size="small"
|
||
pagination={false}
|
||
dataSource={shops}
|
||
columns={[
|
||
{ title: '店铺', dataIndex: 'name' },
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'is_active',
|
||
width: 90,
|
||
render: (value: boolean, row) => (
|
||
<Button
|
||
size="small"
|
||
onClick={async () => {
|
||
await weiboCollabApi.updateShop(row.id, { is_active: !value });
|
||
await loadBase();
|
||
}}
|
||
>
|
||
{value ? '停用' : '启用'}
|
||
</Button>
|
||
),
|
||
},
|
||
{
|
||
title: '操作',
|
||
width: 80,
|
||
render: (_, row) => (
|
||
<Popconfirm
|
||
title="删除店铺?"
|
||
onConfirm={async () => {
|
||
try {
|
||
await weiboCollabApi.deleteShop(row.id);
|
||
await loadBase();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
}}
|
||
>
|
||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||
</Popconfirm>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</Space>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title="商品字典管理"
|
||
open={goodsOpen}
|
||
footer={null}
|
||
width={820}
|
||
onCancel={() => setGoodsOpen(false)}
|
||
>
|
||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||
<Form form={goodsForm} layout="inline" onFinish={() => void createGoods()}>
|
||
<Form.Item name="name" rules={[{ required: true, message: '商品名' }]}>
|
||
<Input placeholder="商品名" />
|
||
</Form.Item>
|
||
<Form.Item name="price" rules={[{ required: true, message: '微豆' }]}>
|
||
<InputNumber min={0} placeholder="微豆" />
|
||
</Form.Item>
|
||
<Form.Item name="goods_id">
|
||
<InputNumber min={1} placeholder="商城 ID(可选)" />
|
||
</Form.Item>
|
||
<Button type="primary" htmlType="submit">
|
||
新增
|
||
</Button>
|
||
</Form>
|
||
<Table
|
||
rowKey="id"
|
||
size="small"
|
||
pagination={false}
|
||
dataSource={goodsCatalog}
|
||
columns={[
|
||
{ title: '商品', dataIndex: 'name' },
|
||
{ title: '微豆', dataIndex: 'price', width: 100 },
|
||
{ title: '商城 ID', dataIndex: 'goods_id', width: 100 },
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'is_active',
|
||
width: 90,
|
||
render: (value: boolean, row) => (
|
||
<Button
|
||
size="small"
|
||
onClick={async () => {
|
||
await weiboCollabApi.updateGoods(row.id, { is_active: !value });
|
||
await loadBase();
|
||
}}
|
||
>
|
||
{value ? '停用' : '启用'}
|
||
</Button>
|
||
),
|
||
},
|
||
{
|
||
title: '操作',
|
||
width: 80,
|
||
render: (_, row) => (
|
||
<Popconfirm
|
||
title="删除商品字典?"
|
||
onConfirm={async () => {
|
||
try {
|
||
await weiboCollabApi.deleteGoods(row.id);
|
||
await loadBase();
|
||
} catch (error) {
|
||
message.error(getErrorMessage(error));
|
||
}
|
||
}}
|
||
>
|
||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||
</Popconfirm>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</Space>
|
||
</Modal>
|
||
</Space>
|
||
);
|
||
}
|