feat(weibo): 增加外部 CK 账号 CRUD 与生命周期
This commit is contained in:
@@ -753,6 +753,10 @@ export interface WeiboAccountItem {
|
||||
uid: string;
|
||||
nickname: string;
|
||||
tag: string;
|
||||
remark: string;
|
||||
is_enabled: boolean;
|
||||
disabled_at: string | null;
|
||||
disabled_reason: string;
|
||||
ck_check_status: string;
|
||||
ck_check_message: string;
|
||||
ck_checked_at: string | null;
|
||||
|
||||
@@ -17,6 +17,7 @@ export const weiboApi = {
|
||||
listAccounts: (params: {
|
||||
search?: string;
|
||||
bind_status?: string;
|
||||
enabled?: boolean;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}) =>
|
||||
@@ -24,11 +25,32 @@ export const weiboApi = {
|
||||
'/weibo/accounts',
|
||||
{ params },
|
||||
),
|
||||
getAccount: (accountId: number) =>
|
||||
api.get<WeiboAccountItem, WeiboAccountItem>(`/weibo/accounts/${accountId}`),
|
||||
importCookies: (text: string, tag = '') =>
|
||||
api.post<WeiboImportResult, WeiboImportResult>('/weibo/accounts/import-cookies', {
|
||||
text,
|
||||
tag,
|
||||
}),
|
||||
updateAccount: (
|
||||
accountId: number,
|
||||
data: { tag?: string; remark?: string },
|
||||
) =>
|
||||
api.put<WeiboAccountItem, WeiboAccountItem>(
|
||||
`/weibo/accounts/${accountId}`,
|
||||
data,
|
||||
),
|
||||
disableAccount: (accountId: number, reason = '') =>
|
||||
api.post<WeiboAccountItem, WeiboAccountItem>(
|
||||
`/weibo/accounts/${accountId}/disable`,
|
||||
{ reason },
|
||||
),
|
||||
enableAccount: (accountId: number) =>
|
||||
api.post<WeiboAccountItem, WeiboAccountItem>(
|
||||
`/weibo/accounts/${accountId}/enable`,
|
||||
undefined,
|
||||
{ timeout: 120000 },
|
||||
),
|
||||
refreshAccount: (accountId: number) =>
|
||||
api.post<WeiboAccountItem, WeiboAccountItem>(
|
||||
`/weibo/accounts/${accountId}/refresh`,
|
||||
|
||||
@@ -24,10 +24,13 @@ import type { TableProps } from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
AppstoreOutlined,
|
||||
CheckCircleOutlined,
|
||||
EditOutlined,
|
||||
ImportOutlined,
|
||||
QrcodeOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
StopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
weiboApi,
|
||||
@@ -78,6 +81,7 @@ export default function WeiboAccountsPage() {
|
||||
const { token } = theme.useToken();
|
||||
const { can } = usePermissions();
|
||||
const canImport = can('weibo:import');
|
||||
const canManage = can('weibo:manage');
|
||||
const canBind = can('weibo:bind');
|
||||
const canRecharge = can('weibo:recharge');
|
||||
const canExchange = can('weibo:exchange');
|
||||
@@ -89,13 +93,19 @@ export default function WeiboAccountsPage() {
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [search, setSearch] = useState('');
|
||||
const [bindFilter, setBindFilter] = useState('');
|
||||
const [enabledFilter, setEnabledFilter] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshingId, setRefreshingId] = useState<number | null>(null);
|
||||
const [mutatingId, setMutatingId] = useState<number | null>(null);
|
||||
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importErrors, setImportErrors] = useState<WeiboImportError[]>([]);
|
||||
const [form] = Form.useForm<{ text: string; tag?: string }>();
|
||||
const [editForm] = Form.useForm<{ tag?: string; remark?: string }>();
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [editAccount, setEditAccount] = useState<WeiboAccountItem | null>(null);
|
||||
const [editSaving, setEditSaving] = useState(false);
|
||||
|
||||
const [qrOpen, setQrOpen] = useState(false);
|
||||
const [qrAccount, setQrAccount] = useState<WeiboAccountItem | null>(null);
|
||||
@@ -127,6 +137,12 @@ export default function WeiboAccountsPage() {
|
||||
const data: PaginatedResponse<WeiboAccountItem> = await weiboApi.listAccounts({
|
||||
search: search.trim() || undefined,
|
||||
bind_status: bindFilter || undefined,
|
||||
enabled:
|
||||
enabledFilter === 'enabled'
|
||||
? true
|
||||
: enabledFilter === 'disabled'
|
||||
? false
|
||||
: undefined,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
@@ -137,7 +153,7 @@ export default function WeiboAccountsPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [bindFilter, page, pageSize, search]);
|
||||
}, [bindFilter, enabledFilter, page, pageSize, search]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadAccounts();
|
||||
@@ -167,6 +183,10 @@ export default function WeiboAccountsPage() {
|
||||
};
|
||||
|
||||
const handleRefresh = async (account: WeiboAccountItem) => {
|
||||
if (!account.is_enabled) {
|
||||
message.warning('账号已停用,请先启用');
|
||||
return;
|
||||
}
|
||||
setRefreshingId(account.id);
|
||||
try {
|
||||
const updated = await weiboApi.refreshAccount(account.id);
|
||||
@@ -186,6 +206,10 @@ export default function WeiboAccountsPage() {
|
||||
};
|
||||
|
||||
const openQr = async (account: WeiboAccountItem) => {
|
||||
if (!account.is_enabled) {
|
||||
message.warning('账号已停用,请先启用');
|
||||
return;
|
||||
}
|
||||
setQrAccount(account);
|
||||
setQrResult(null);
|
||||
setQrOpen(true);
|
||||
@@ -213,6 +237,67 @@ export default function WeiboAccountsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const openEdit = (account: WeiboAccountItem) => {
|
||||
setEditAccount(account);
|
||||
editForm.setFieldsValue({ tag: account.tag, remark: account.remark });
|
||||
setEditOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = async () => {
|
||||
if (!editAccount) return;
|
||||
const values = await editForm.validateFields();
|
||||
setEditSaving(true);
|
||||
try {
|
||||
const updated = await weiboApi.updateAccount(editAccount.id, {
|
||||
tag: values.tag || '',
|
||||
remark: values.remark || '',
|
||||
});
|
||||
setItems((current) =>
|
||||
current.map((item) => (item.id === updated.id ? updated : item)),
|
||||
);
|
||||
setEditOpen(false);
|
||||
message.success('账号资料已更新');
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setEditSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisable = async (account: WeiboAccountItem) => {
|
||||
setMutatingId(account.id);
|
||||
try {
|
||||
const updated = await weiboApi.disableAccount(account.id);
|
||||
setItems((current) =>
|
||||
current.map((item) => (item.id === updated.id ? updated : item)),
|
||||
);
|
||||
setFeatureAccount((current) =>
|
||||
current?.id === updated.id ? updated : current,
|
||||
);
|
||||
message.success('账号已停用');
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setMutatingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnable = async (account: WeiboAccountItem) => {
|
||||
setMutatingId(account.id);
|
||||
try {
|
||||
const updated = await weiboApi.enableAccount(account.id);
|
||||
setItems((current) =>
|
||||
current.map((item) => (item.id === updated.id ? updated : item)),
|
||||
);
|
||||
message.success('账号已通过检测并启用');
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
void loadAccounts();
|
||||
} finally {
|
||||
setMutatingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMall = async (account: WeiboAccountItem) => {
|
||||
setMallLoading(true);
|
||||
try {
|
||||
@@ -226,6 +311,10 @@ export default function WeiboAccountsPage() {
|
||||
|
||||
const handleExchange = async (goods: WeiboMallListResult['goods'][number]) => {
|
||||
if (!featureAccount) return;
|
||||
if (!featureAccount.is_enabled) {
|
||||
message.warning('账号已停用,请先启用');
|
||||
return;
|
||||
}
|
||||
setExchangingGoodsId(goods.id);
|
||||
try {
|
||||
const result = await weiboApi.exchangeGoods(
|
||||
@@ -273,6 +362,10 @@ export default function WeiboAccountsPage() {
|
||||
};
|
||||
|
||||
const openFeatures = (account: WeiboAccountItem) => {
|
||||
if (!account.is_enabled) {
|
||||
message.warning('账号已停用,请先启用');
|
||||
return;
|
||||
}
|
||||
setFeatureAccount(account);
|
||||
setFeatureOpen(true);
|
||||
setFeatureTab('goods');
|
||||
@@ -297,6 +390,10 @@ export default function WeiboAccountsPage() {
|
||||
|
||||
const createRecharge = async () => {
|
||||
if (!featureAccount) return;
|
||||
if (!featureAccount.is_enabled) {
|
||||
message.warning('账号已停用,请先启用');
|
||||
return;
|
||||
}
|
||||
setRechargeCreating(true);
|
||||
setPaymentStatus(null);
|
||||
try {
|
||||
@@ -346,6 +443,13 @@ export default function WeiboAccountsPage() {
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
UID {account.uid}
|
||||
</Text>
|
||||
{account.remark && (
|
||||
<Tooltip title={account.remark}>
|
||||
<Text type="secondary" ellipsis style={{ fontSize: 12, maxWidth: 190 }}>
|
||||
{account.remark}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -371,6 +475,24 @@ export default function WeiboAccountsPage() {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '启用状态',
|
||||
dataIndex: 'is_enabled',
|
||||
width: 100,
|
||||
render: (value: boolean, account) => (
|
||||
<Tooltip
|
||||
title={
|
||||
value
|
||||
? '账号正在使用'
|
||||
: account.disabled_reason || account.disabled_at || '账号已停用'
|
||||
}
|
||||
>
|
||||
<Tag color={value ? 'success' : 'default'}>
|
||||
{value ? '已启用' : '已停用'}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '角色绑定',
|
||||
key: 'bind',
|
||||
@@ -404,17 +526,19 @@ export default function WeiboAccountsPage() {
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 390,
|
||||
width: 540,
|
||||
fixed: 'right',
|
||||
render: (_, account) => {
|
||||
const cannotBind =
|
||||
account.is_bound === true && account.can_rebind !== true;
|
||||
const busy = mutatingId === account.id;
|
||||
return (
|
||||
<Space>
|
||||
<Space wrap>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={refreshingId === account.id}
|
||||
disabled={!account.is_enabled}
|
||||
onClick={() => void handleRefresh(account)}
|
||||
>
|
||||
刷新
|
||||
@@ -431,7 +555,7 @@ export default function WeiboAccountsPage() {
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<QrcodeOutlined />}
|
||||
disabled={!canBind || cannotBind}
|
||||
disabled={!canBind || cannotBind || !account.is_enabled}
|
||||
onClick={() => void openQr(account)}
|
||||
>
|
||||
绑定码
|
||||
@@ -440,10 +564,51 @@ export default function WeiboAccountsPage() {
|
||||
<Button
|
||||
size="small"
|
||||
icon={<AppstoreOutlined />}
|
||||
disabled={!account.is_enabled}
|
||||
onClick={() => openFeatures(account)}
|
||||
>
|
||||
功能
|
||||
</Button>
|
||||
{canManage && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => openEdit(account)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
{canManage &&
|
||||
(account.is_enabled ? (
|
||||
<Popconfirm
|
||||
title="停用微博账号?"
|
||||
description="停用后不能刷新、绑定、充值或兑换,但会保留 CK 和历史资料。"
|
||||
okText="停用"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => void handleDisable(account)}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
loading={busy}
|
||||
>
|
||||
停用
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={busy}
|
||||
onClick={() => void handleEnable(account)}
|
||||
>
|
||||
启用
|
||||
</Button>
|
||||
))}
|
||||
{canDelete && (
|
||||
<Popconfirm
|
||||
title="删除微博账号?"
|
||||
@@ -514,6 +679,7 @@ export default function WeiboAccountsPage() {
|
||||
const balance = mallResult?.weidou_balance ?? featureAccount?.weidou_balance ?? 0;
|
||||
const disabled =
|
||||
!canExchange ||
|
||||
!featureAccount?.is_enabled ||
|
||||
goods.is_sold_out ||
|
||||
goods.remain_count === 0 ||
|
||||
goods.price > balance;
|
||||
@@ -600,15 +766,15 @@ export default function WeiboAccountsPage() {
|
||||
>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
微博账号
|
||||
微博外部 CK 账号
|
||||
</Title>
|
||||
<Text type="secondary">CK 导入、微豆资料刷新和角色绑定二维码</Text>
|
||||
<Text type="secondary">外部 CK 导入、账号生命周期、微豆资料和角色绑定</Text>
|
||||
</div>
|
||||
<Space wrap>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="UID、昵称、角色、标签"
|
||||
placeholder="UID、昵称、角色、标签、备注"
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
@@ -630,6 +796,19 @@ export default function WeiboAccountsPage() {
|
||||
{ value: 'rebindable', label: '可换绑' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={enabledFilter}
|
||||
onChange={(value) => {
|
||||
setEnabledFilter(value);
|
||||
setPage(1);
|
||||
}}
|
||||
style={{ width: 130 }}
|
||||
options={[
|
||||
{ value: '', label: '全部生命周期' },
|
||||
{ value: 'enabled', label: '已启用' },
|
||||
{ value: 'disabled', label: '已停用' },
|
||||
]}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void loadAccounts()}>
|
||||
刷新列表
|
||||
</Button>
|
||||
@@ -642,7 +821,7 @@ export default function WeiboAccountsPage() {
|
||||
setImportOpen(true);
|
||||
}}
|
||||
>
|
||||
导入 CK
|
||||
导入外部 CK
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -660,7 +839,7 @@ export default function WeiboAccountsPage() {
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
scroll={{ x: 1180 }}
|
||||
scroll={{ x: 1360 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
@@ -718,6 +897,35 @@ export default function WeiboAccountsPage() {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
editAccount
|
||||
? `编辑账号 · ${editAccount.nickname || editAccount.uid}`
|
||||
: '编辑账号'
|
||||
}
|
||||
open={editOpen}
|
||||
confirmLoading={editSaving}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
onOk={() => void handleEdit()}
|
||||
onCancel={() => setEditOpen(false)}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={editForm} layout="vertical" preserve={false}>
|
||||
<Form.Item name="tag" label="标签">
|
||||
<Input placeholder="可选,例如:客户A、活动账号" maxLength={64} />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="记录客户、用途或人工处理说明"
|
||||
maxLength={256}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={qrAccount ? `绑定二维码 · ${qrAccount.nickname || qrAccount.uid}` : '绑定二维码'}
|
||||
open={qrOpen}
|
||||
@@ -901,6 +1109,7 @@ export default function WeiboAccountsPage() {
|
||||
<Button
|
||||
type="primary"
|
||||
loading={rechargeCreating}
|
||||
disabled={!featureAccount.is_enabled}
|
||||
onClick={() => void createRecharge()}
|
||||
>
|
||||
创建充值订单
|
||||
|
||||
Reference in New Issue
Block a user