= {
+ valid: { label: '有效', color: 'success' },
+ invalid: { label: '失效', color: 'error' },
+ error: { label: '查询失败', color: 'warning' },
+};
+
+function renderBindStatus(account: WeiboAccountItem) {
+ if (account.is_bound === true && account.can_rebind === true) {
+ return
可换绑;
+ }
+ if (account.is_bound === true) {
+ const days = account.rebind_remain_days ?? 0;
+ return
{days > 0 ? `${days} 天后可换绑` : '已绑定'};
+ }
+ if (account.is_bound === false) return
未绑定;
+ return
未查询;
+}
+
+export default function WeiboAssignmentsPage() {
+ const { token } = theme.useToken();
+ const [summary, setSummary] = useState
(null);
+ const [supportUsers, setSupportUsers] = useState([]);
+ const [selectedUser, setSelectedUser] = useState(null);
+ const [accounts, setAccounts] = useState([]);
+ const [totalCount, setTotalCount] = useState(0);
+ const [tableLoading, setTableLoading] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+ const [assigning, setAssigning] = useState(false);
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const [knownTags, setKnownTags] = useState([]);
+ const [searchText, setSearchText] = useState('');
+ const [filterTag, setFilterTag] = useState();
+ const [enabledFilter, setEnabledFilter] = useState('');
+ const [activeTab, setActiveTab] = useState<'unassigned' | 'assigned'>('unassigned');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(() => {
+ const saved = localStorage.getItem(STORAGE_KEY_PAGE_SIZE);
+ return saved ? Number(saved) || 15 : 15;
+ });
+
+ const loadSummary = useCallback(async (silent = false) => {
+ try {
+ const data = await weiboApi.assignmentsSummary();
+ setSummary(data);
+ setSupportUsers(data.support_users);
+ return data.support_users;
+ } catch (error) {
+ if (!silent) message.error(getErrorMessage(error));
+ return [];
+ }
+ }, []);
+
+ const loadTags = useCallback(async () => {
+ try {
+ const result = await weiboApi.accountTags();
+ setKnownTags(result.tags);
+ } catch {
+ // 标签字典失败不影响分配列表。
+ }
+ }, []);
+
+ const loadAccounts = useCallback(
+ async (silent = false) => {
+ if (activeTab === 'assigned' && !selectedUser) return;
+ if (!silent) setTableLoading(true);
+ try {
+ const data: PaginatedResponse = await weiboApi.listAccounts({
+ page: currentPage,
+ page_size: pageSize,
+ search: searchText.trim() || undefined,
+ tag: filterTag,
+ enabled:
+ enabledFilter === 'enabled'
+ ? true
+ : enabledFilter === 'disabled'
+ ? false
+ : undefined,
+ assign_status: activeTab === 'unassigned' ? 'unassigned' : undefined,
+ assigned_user_id:
+ activeTab === 'assigned' ? selectedUser?.id : undefined,
+ });
+ setAccounts(data.items);
+ setTotalCount(data.total);
+ } catch (error) {
+ if (!silent) message.error(getErrorMessage(error));
+ } finally {
+ if (!silent) setTableLoading(false);
+ }
+ },
+ [
+ activeTab,
+ currentPage,
+ enabledFilter,
+ filterTag,
+ pageSize,
+ searchText,
+ selectedUser,
+ ],
+ );
+
+ useEffect(() => {
+ const initialize = async () => {
+ void loadTags();
+ const users = await loadSummary();
+ if (users.length > 0) {
+ const savedId = localStorage.getItem(STORAGE_KEY_SELECTED_USER);
+ const savedUser = savedId
+ ? users.find((user) => user.id === Number(savedId))
+ : undefined;
+ setSelectedUser(savedUser || users[0]);
+ }
+ };
+ void initialize();
+ }, [loadSummary, loadTags]);
+
+ useEffect(() => {
+ void loadAccounts();
+ }, [loadAccounts]);
+
+ const refreshData = async (silent = false) => {
+ await Promise.all([loadSummary(silent), loadAccounts(silent)]);
+ };
+
+ const handleRefresh = async () => {
+ setRefreshing(true);
+ try {
+ await refreshData();
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ const handleSelectUser = (user: SupportUserItem) => {
+ setSelectedUser(user);
+ setActiveTab('unassigned');
+ setSelectedRowKeys([]);
+ setCurrentPage(1);
+ localStorage.setItem(STORAGE_KEY_SELECTED_USER, String(user.id));
+ };
+
+ const handlePageChange = (page: number, size: number) => {
+ setCurrentPage(page);
+ if (size !== pageSize) {
+ setPageSize(size);
+ localStorage.setItem(STORAGE_KEY_PAGE_SIZE, String(size));
+ setCurrentPage(1);
+ }
+ };
+
+ const handleBatchAssign = async () => {
+ if (!selectedUser || selectedRowKeys.length === 0) {
+ message.warning('请先选择客服和微博账号');
+ return;
+ }
+ setAssigning(true);
+ try {
+ const result = await weiboApi.batchAssign(selectedRowKeys, selectedUser.id);
+ message.success(
+ result.message || `已将 ${selectedRowKeys.length} 个账号分配给 ${selectedUser.username}`,
+ );
+ setSelectedRowKeys([]);
+ await refreshData();
+ } catch (error) {
+ message.error(getErrorMessage(error));
+ } finally {
+ setAssigning(false);
+ }
+ };
+
+ const handleAssignAllFiltered = async () => {
+ if (!selectedUser) {
+ message.warning('请先选择客服');
+ return;
+ }
+ setAssigning(true);
+ try {
+ const result = await weiboApi.listAccountIds({
+ search: searchText.trim() || undefined,
+ tag: filterTag,
+ enabled:
+ enabledFilter === 'enabled'
+ ? true
+ : enabledFilter === 'disabled'
+ ? false
+ : undefined,
+ assign_status: 'unassigned',
+ });
+ if (result.account_ids.length === 0) {
+ message.info('当前筛选下没有未分配账号');
+ return;
+ }
+ await weiboApi.batchAssign(result.account_ids, selectedUser.id);
+ message.success(
+ `已将 ${result.account_ids.length} 个账号分配给 ${selectedUser.username}`,
+ );
+ setSelectedRowKeys([]);
+ await refreshData();
+ } catch (error) {
+ message.error(getErrorMessage(error));
+ } finally {
+ setAssigning(false);
+ }
+ };
+
+ const handleBatchUnassign = async () => {
+ if (selectedRowKeys.length === 0) {
+ message.warning('请先选择微博账号');
+ return;
+ }
+ setAssigning(true);
+ try {
+ await weiboApi.batchAssign(selectedRowKeys, null);
+ message.success(`已取消 ${selectedRowKeys.length} 个账号的分配`);
+ setSelectedRowKeys([]);
+ await refreshData();
+ } catch (error) {
+ message.error(getErrorMessage(error));
+ } finally {
+ setAssigning(false);
+ }
+ };
+
+ const selectedUserCount =
+ supportUsers.find((user) => user.id === selectedUser?.id)?.assigned_count ??
+ selectedUser?.assigned_count ??
+ 0;
+ const unassignedCount = summary?.unassigned_count ?? 0;
+ const assignedCount = summary?.assigned_count ?? 0;
+ const accountCount = summary?.total_count ?? assignedCount + unassignedCount;
+
+ const columns: TableProps['columns'] = [
+ {
+ title: '序号',
+ key: 'index',
+ width: 58,
+ align: 'center',
+ render: (_, __, index) => (currentPage - 1) * pageSize + index + 1,
+ },
+ {
+ title: '微博账号',
+ key: 'account',
+ width: 190,
+ align: 'center',
+ render: (_, account) => (
+
+ {account.nickname || account.uid}
+
+ UID {account.uid}
+
+
+ ),
+ },
+ {
+ title: '标签',
+ dataIndex: 'tag',
+ width: 100,
+ align: 'center',
+ render: (tag: string) =>
+ tag ? {tag} : -,
+ },
+ {
+ title: 'CK 状态',
+ dataIndex: 'ck_check_status',
+ width: 100,
+ align: 'center',
+ render: (status: string) => {
+ const state = CK_STATUS[status] || { label: '未检测', color: 'default' };
+ return {state.label};
+ },
+ },
+ {
+ title: '角色绑定',
+ key: 'bind',
+ width: 120,
+ align: 'center',
+ render: (_, account) => renderBindStatus(account),
+ },
+ {
+ title: '微豆',
+ dataIndex: 'weidou_balance',
+ width: 90,
+ align: 'center',
+ render: (value: number | null) =>
+ value == null ? - : value.toLocaleString(),
+ },
+ {
+ title: '生命周期',
+ dataIndex: 'is_enabled',
+ width: 90,
+ align: 'center',
+ render: (enabled: boolean) => (
+
+ {enabled ? '已启用' : '已停用'}
+
+ ),
+ },
+ {
+ title: '更新时间',
+ dataIndex: 'updated_at',
+ width: 155,
+ align: 'center',
+ render: (value: string | null) =>
+ value ? formatTime(value) : -,
+ },
+ ];
+
+ return (
+
+
+
+
微博分配管理
+ 管理员和运营查看全部账号;客服只能看到分配给自己的账号。
+
+
} loading={refreshing} onClick={handleRefresh}>
+ 刷新
+
+
+
+
+
+
+ } />
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+ {supportUsers.map((user) => {
+ const isSelected = selectedUser?.id === user.id;
+ return (
+
handleSelectUser(user)}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: '10px 14px',
+ borderRadius: 6,
+ cursor: 'pointer',
+ border: `1px solid ${isSelected ? token.colorPrimary : token.colorBorderSecondary}`,
+ background: isSelected ? token.colorPrimaryBg : token.colorBgContainer,
+ }}
+ >
+
+
+ {user.username}
+
+
+
+
+
+ );
+ })}
+ {supportUsers.length === 0 && (
+
+ 暂无客服用户
+
+ )}
+
+
+
+
+
+
+ 已选中客服:
+
+ {selectedUser.username}(已分配 {selectedUserCount} 个)
+
+
+ ) : (
+ 请选择左侧客服进行操作
+ )
+ }
+ style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}
+ styles={{
+ body: {
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ minHeight: 0,
+ padding: 0,
+ },
+ }}
+ >
+
+ {
+ setSearchText(event.target.value);
+ setCurrentPage(1);
+ }}
+ style={{ width: 230 }}
+ size="small"
+ />
+
+
+ {
+ setActiveTab(key as 'unassigned' | 'assigned');
+ setSelectedRowKeys([]);
+ setCurrentPage(1);
+ }}
+ style={{ padding: '0 16px', flexShrink: 0, marginBottom: 0 }}
+ items={[
+ { key: 'unassigned', label: `未分配 (${unassignedCount})` },
+ {
+ key: 'assigned',
+ label: selectedUser
+ ? `已分配给 ${selectedUser.username} (${selectedUserCount})`
+ : '已分配账号',
+ disabled: !selectedUser,
+ },
+ ]}
+ />
+
+
+ {activeTab === 'unassigned' && selectedUser && (
+ <>
+
}
+ loading={assigning}
+ disabled={selectedRowKeys.length === 0}
+ onClick={handleBatchAssign}
+ >
+ 分配选中 ({selectedRowKeys.length}) 给 {selectedUser.username}
+
+
+
+
+ >
+ )}
+ {activeTab === 'assigned' && selectedUser && (
+
}
+ loading={assigning}
+ disabled={selectedRowKeys.length === 0}
+ onClick={handleBatchUnassign}
+ >
+ 取消分配选中 ({selectedRowKeys.length})
+
+ )}
+
+
+
+ columns={columns}
+ dataSource={accounts}
+ rowKey="id"
+ size="small"
+ loading={tableLoading}
+ pagination={{
+ current: currentPage,
+ pageSize,
+ total: totalCount,
+ size: 'small',
+ showSizeChanger: true,
+ showTotal: (total) => `共 ${total} 条`,
+ onChange: handlePageChange,
+ }}
+ rowSelection={{
+ selectedRowKeys,
+ preserveSelectedRowKeys: true,
+ onChange: (keys) => setSelectedRowKeys(keys.map(Number)),
+ }}
+ scroll={{ x: 940, y: 'calc(100vh - 500px)' }}
+ />
+
+
+
+
+ );
+}