feat(weibo): 增加强制分配权限与账号详情
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Table, Button, Modal, Form, Input, Select, Tag, Popconfirm, Space,
|
||||
Checkbox, Divider, Alert,
|
||||
Checkbox, Divider, Alert, Switch, Typography,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import { message } from '../utils/antdMessage';
|
||||
@@ -22,6 +22,9 @@ const ROLE_COLORS: Record<string, string> = {
|
||||
support: 'green',
|
||||
};
|
||||
|
||||
const { Text } = Typography;
|
||||
const WEIBO_FORCE_ASSIGN_PERMISSION = 'weibo:assign:force';
|
||||
|
||||
// 权限分组定义
|
||||
const PERMISSION_GROUPS = [
|
||||
{ label: '用户管理', prefix: 'user:' },
|
||||
@@ -225,6 +228,19 @@ export default function UsersPage() {
|
||||
setSelectedPerms(rolePerms);
|
||||
};
|
||||
|
||||
const handleWeiboForceAssignToggle = (checked: boolean) => {
|
||||
setUseCustom(true);
|
||||
setSelectedPerms((current) => {
|
||||
const next = new Set(current);
|
||||
if (checked) {
|
||||
next.add(WEIBO_FORCE_ASSIGN_PERMISSION);
|
||||
} else {
|
||||
next.delete(WEIBO_FORCE_ASSIGN_PERMISSION);
|
||||
}
|
||||
return Array.from(next);
|
||||
});
|
||||
};
|
||||
|
||||
const columns: TableProps<UserInfo>['columns'] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 60 },
|
||||
{ title: '用户名', dataIndex: 'username', width: 128, ellipsis: true },
|
||||
@@ -443,6 +459,29 @@ export default function UsersPage() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Divider style={{ margin: '8px 0 12px' }}>微博高级分配</Divider>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
checked={selectedPerms.includes(WEIBO_FORCE_ASSIGN_PERMISSION)}
|
||||
onChange={handleWeiboForceAssignToggle}
|
||||
/>
|
||||
<div>
|
||||
<Text strong>全部账号与强制分配</Text>
|
||||
<div>
|
||||
<Text type="secondary">
|
||||
打开后可查看“全部账号”页签,并把勾选账号或指定标签的账号强制转给客服。
|
||||
该权限仅超级管理员默认拥有,运营默认关闭。
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox.Group
|
||||
value={selectedPerms}
|
||||
onChange={(vals) => setSelectedPerms(vals as string[])}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
CopyOutlined,
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
ImportOutlined,
|
||||
QrcodeOutlined,
|
||||
ReloadOutlined,
|
||||
@@ -287,6 +288,21 @@ export default function WeiboAccountsPage({
|
||||
(entry: BatchRechargeEntry) => Promise<void>
|
||||
>(async () => {});
|
||||
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailAccount, setDetailAccount] = useState<WeiboAccountItem | null>(
|
||||
null,
|
||||
);
|
||||
const [detailTab, setDetailTab] = useState('basic');
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailExchangeLoading, setDetailExchangeLoading] = useState(false);
|
||||
const [detailExchangeError, setDetailExchangeError] = useState('');
|
||||
const [detailExchangeResult, setDetailExchangeResult] =
|
||||
useState<WeiboExchangeRecordsResult | null>(null);
|
||||
const [detailPointLoading, setDetailPointLoading] = useState(false);
|
||||
const [detailPointError, setDetailPointError] = useState('');
|
||||
const [detailPointResult, setDetailPointResult] =
|
||||
useState<WeiboPointHistoryResult | null>(null);
|
||||
|
||||
const loadAccounts = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -940,6 +956,71 @@ export default function WeiboAccountsPage({
|
||||
}
|
||||
};
|
||||
|
||||
const loadDetailExchangeRecords = async (
|
||||
account: WeiboAccountItem,
|
||||
page = 1,
|
||||
) => {
|
||||
setDetailExchangeLoading(true);
|
||||
setDetailExchangeError('');
|
||||
try {
|
||||
setDetailExchangeResult(
|
||||
await weiboApi.exchangeRecords(account.id, page, 20),
|
||||
);
|
||||
} catch (error) {
|
||||
setDetailExchangeError(getErrorMessage(error));
|
||||
} finally {
|
||||
setDetailExchangeLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadDetailPointHistory = async (
|
||||
account: WeiboAccountItem,
|
||||
page = 1,
|
||||
) => {
|
||||
setDetailPointLoading(true);
|
||||
setDetailPointError('');
|
||||
try {
|
||||
setDetailPointResult(await weiboApi.pointHistory(account.id, page, 20));
|
||||
} catch (error) {
|
||||
setDetailPointError(getErrorMessage(error));
|
||||
} finally {
|
||||
setDetailPointLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openAccountDetail = async (account: WeiboAccountItem) => {
|
||||
setDetailAccount(account);
|
||||
setDetailOpen(true);
|
||||
setDetailTab('basic');
|
||||
setDetailExchangeResult(null);
|
||||
setDetailExchangeError('');
|
||||
setDetailPointResult(null);
|
||||
setDetailPointError('');
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const updated = await weiboApi.getAccount(account.id);
|
||||
setDetailAccount(updated);
|
||||
setItems((current) =>
|
||||
current.map((item) => (item.id === updated.id ? updated : item)),
|
||||
);
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const switchDetailTab = (key: string) => {
|
||||
setDetailTab(key);
|
||||
if (!detailAccount || !detailAccount.is_enabled) return;
|
||||
if (key === 'exchange' && !detailExchangeResult) {
|
||||
void loadDetailExchangeRecords(detailAccount);
|
||||
}
|
||||
if (key === 'points' && !detailPointResult) {
|
||||
void loadDetailPointHistory(detailAccount);
|
||||
}
|
||||
};
|
||||
|
||||
const openFeatures = (account: WeiboAccountItem) => {
|
||||
if (!account.is_enabled) {
|
||||
message.warning('账号已停用,请先启用');
|
||||
@@ -1439,7 +1520,7 @@ export default function WeiboAccountsPage({
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 540,
|
||||
width: 610,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
onHeaderCell: centerHeader,
|
||||
@@ -1476,6 +1557,15 @@ export default function WeiboAccountsPage({
|
||||
绑定码
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{!isWorkbench && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => void openAccountDetail(account)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
icon={<AppstoreOutlined />}
|
||||
@@ -1672,7 +1762,12 @@ export default function WeiboAccountsPage({
|
||||
align: 'right',
|
||||
render: (value: number | null) => value ?? '-',
|
||||
},
|
||||
{ title: '兑换时间', dataIndex: 'exchange_time', width: 170 },
|
||||
{
|
||||
title: '兑换时间',
|
||||
dataIndex: 'exchange_time',
|
||||
width: 170,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'reward_status',
|
||||
@@ -1696,8 +1791,18 @@ export default function WeiboAccountsPage({
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{ title: '到账时间', dataIndex: 'create_time', width: 170 },
|
||||
{ title: '过期时间', dataIndex: 'expire_time', width: 170 },
|
||||
{
|
||||
title: '到账时间',
|
||||
dataIndex: 'create_time',
|
||||
width: 170,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{
|
||||
title: '过期时间',
|
||||
dataIndex: 'expire_time',
|
||||
width: 170,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{ title: '订单号', dataIndex: 'source_id' },
|
||||
];
|
||||
const activeQrScheme = qrResult?.schemes.find(
|
||||
@@ -1730,6 +1835,21 @@ export default function WeiboAccountsPage({
|
||||
const batchRechargeCreditedCount = batchRechargeEntries.filter(
|
||||
(entry) => entry.creditPhase === 'credited',
|
||||
).length;
|
||||
const detailPointIncome =
|
||||
detailPointResult?.items.reduce(
|
||||
(total, item) => total + Math.max(0, item.point_num),
|
||||
0,
|
||||
) ?? 0;
|
||||
const detailPointSpent =
|
||||
detailPointResult?.items.reduce(
|
||||
(total, item) => total + Math.max(0, -item.point_num),
|
||||
0,
|
||||
) ?? 0;
|
||||
const detailExchangeCost =
|
||||
detailExchangeResult?.items.reduce(
|
||||
(total, item) => total + Math.max(0, item.cost_weidou ?? 0),
|
||||
0,
|
||||
) ?? 0;
|
||||
|
||||
return (
|
||||
<Space orientation="vertical" size={16} style={{ width: '100%' }}>
|
||||
@@ -3025,6 +3145,342 @@ export default function WeiboAccountsPage({
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
detailAccount
|
||||
? `账号详情 · ${detailAccount.nickname || detailAccount.uid}`
|
||||
: '账号详情'
|
||||
}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
size={920}
|
||||
destroyOnHidden
|
||||
>
|
||||
{detailAccount && (
|
||||
<Spin spinning={detailLoading}>
|
||||
<Tabs
|
||||
activeKey={detailTab}
|
||||
onChange={switchDetailTab}
|
||||
items={[
|
||||
{
|
||||
key: 'basic',
|
||||
label: '账号资料',
|
||||
children: (
|
||||
<Space
|
||||
orientation="vertical"
|
||||
size={16}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{!detailAccount.is_enabled && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
title="账号已停用"
|
||||
description={
|
||||
detailAccount.disabled_reason ||
|
||||
'停用期间仍可查看本地账号资料,但不能实时查询微豆明细和兑换记录。'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
column={2}
|
||||
items={[
|
||||
{
|
||||
key: 'uid',
|
||||
label: '微博 UID',
|
||||
children: (
|
||||
<Text copyable>{detailAccount.uid}</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'nickname',
|
||||
label: '微博昵称',
|
||||
children: detailAccount.nickname || '-',
|
||||
},
|
||||
{
|
||||
key: 'created',
|
||||
label: '导入时间',
|
||||
children: formatTime(detailAccount.created_at),
|
||||
},
|
||||
{
|
||||
key: 'updated',
|
||||
label: '最近更新',
|
||||
children: formatTime(detailAccount.updated_at),
|
||||
},
|
||||
{
|
||||
key: 'assigned',
|
||||
label: '归属客服',
|
||||
children: detailAccount.assigned_username ? (
|
||||
<Tag color="cyan">
|
||||
{detailAccount.assigned_username}
|
||||
</Tag>
|
||||
) : (
|
||||
<Text type="secondary">未分配</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'tag',
|
||||
label: '标签',
|
||||
children: detailAccount.tag ? (
|
||||
<Tag color="blue">{detailAccount.tag}</Tag>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
label: '启用状态',
|
||||
children: (
|
||||
<Tag
|
||||
color={
|
||||
detailAccount.is_enabled
|
||||
? 'success'
|
||||
: 'default'
|
||||
}
|
||||
>
|
||||
{detailAccount.is_enabled
|
||||
? '已启用'
|
||||
: '已停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'balance',
|
||||
label: '微豆余额',
|
||||
children:
|
||||
detailAccount.weidou_balance?.toLocaleString() ??
|
||||
'-',
|
||||
},
|
||||
{
|
||||
key: 'ck',
|
||||
label: 'CK 状态',
|
||||
children: (
|
||||
<Space orientation="vertical" size={2}>
|
||||
<Tag
|
||||
color={
|
||||
CK_STATUS[detailAccount.ck_check_status]
|
||||
?.color || 'default'
|
||||
}
|
||||
>
|
||||
{CK_STATUS[detailAccount.ck_check_status]
|
||||
?.label || '未检测'}
|
||||
</Tag>
|
||||
{detailAccount.ck_check_message && (
|
||||
<Text type="secondary">
|
||||
{detailAccount.ck_check_message}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'checked',
|
||||
label: '最近检查',
|
||||
children: formatTime(
|
||||
detailAccount.ck_checked_at,
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'cookie',
|
||||
label: 'CK 更新时间',
|
||||
children: formatTime(
|
||||
detailAccount.cookie_updated_at,
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'bind',
|
||||
label: '角色绑定',
|
||||
children: bindStatus(detailAccount),
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
label: '游戏角色',
|
||||
children: detailAccount.game_role_name || '-',
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
label: '备注',
|
||||
span: 2,
|
||||
children: detailAccount.remark || '-',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'points',
|
||||
label: '微豆收支',
|
||||
children: (
|
||||
<Space
|
||||
orientation="vertical"
|
||||
size={12}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
title="微豆收支来自微博实时数据"
|
||||
description="展示充值、活动发放、过期和消费等变动;下方的收入、消耗统计只计算当前页。"
|
||||
/>
|
||||
{!detailAccount.is_enabled ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
title="账号已停用,无法查询实时微豆明细"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Space wrap>
|
||||
<Tag color="blue">
|
||||
当前余额{' '}
|
||||
{detailAccount.weidou_balance?.toLocaleString() ??
|
||||
'-'}
|
||||
</Tag>
|
||||
<Tag color="success">
|
||||
本页收入 +{detailPointIncome.toLocaleString()}
|
||||
</Tag>
|
||||
<Tag color="error">
|
||||
本页消耗 -{detailPointSpent.toLocaleString()}
|
||||
</Tag>
|
||||
</Space>
|
||||
{detailPointError && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
title="微豆明细查询失败"
|
||||
description={detailPointError}
|
||||
action={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
void loadDetailPointHistory(
|
||||
detailAccount,
|
||||
detailPointResult?.page || 1,
|
||||
)
|
||||
}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
rowKey={(record) => record.id || record.source_id}
|
||||
size="small"
|
||||
loading={detailPointLoading}
|
||||
columns={pointColumns}
|
||||
dataSource={detailPointResult?.items || []}
|
||||
locale={{ emptyText: '暂无微豆收支明细' }}
|
||||
pagination={{
|
||||
current: detailPointResult?.page || 1,
|
||||
pageSize: detailPointResult?.page_size || 20,
|
||||
total: detailPointResult?.total || 0,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page) =>
|
||||
void loadDetailPointHistory(
|
||||
detailAccount,
|
||||
page,
|
||||
),
|
||||
}}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'exchange',
|
||||
label: '兑换记录',
|
||||
children: (
|
||||
<Space
|
||||
orientation="vertical"
|
||||
size={12}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
title="兑换记录来自微博实时数据"
|
||||
description="展示商品、绑定角色、微豆消耗、兑换时间和发放状态;本页消耗只计算当前页。"
|
||||
/>
|
||||
{!detailAccount.is_enabled ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
title="账号已停用,无法查询实时兑换记录"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Space wrap>
|
||||
<Tag color="blue">
|
||||
共 {detailExchangeResult?.total ?? 0} 条
|
||||
</Tag>
|
||||
<Tag color="error">
|
||||
本页消耗 {detailExchangeCost.toLocaleString()}{' '}
|
||||
微豆
|
||||
</Tag>
|
||||
</Space>
|
||||
{detailExchangeError && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
title="兑换记录查询失败"
|
||||
description={detailExchangeError}
|
||||
action={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
void loadDetailExchangeRecords(
|
||||
detailAccount,
|
||||
detailExchangeResult?.page || 1,
|
||||
)
|
||||
}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
rowKey={(record, index) =>
|
||||
`${record.goods_name}-${record.exchange_time}-${index}`
|
||||
}
|
||||
size="small"
|
||||
loading={detailExchangeLoading}
|
||||
columns={exchangeColumns}
|
||||
dataSource={detailExchangeResult?.items || []}
|
||||
locale={{ emptyText: '暂无兑换记录' }}
|
||||
pagination={{
|
||||
current: detailExchangeResult?.page || 1,
|
||||
pageSize:
|
||||
detailExchangeResult?.page_size || 20,
|
||||
total: detailExchangeResult?.total || 0,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page) =>
|
||||
void loadDetailExchangeRecords(
|
||||
detailAccount,
|
||||
page,
|
||||
),
|
||||
}}
|
||||
scroll={{ x: 760 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Spin>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
featureAccount
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
App,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
type SupportUserItem,
|
||||
type WeiboAccountItem,
|
||||
} from '../api/modules';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
import { message } from '../utils/antdMessage';
|
||||
import { formatTime } from '../utils/time';
|
||||
@@ -62,6 +64,9 @@ function renderBindStatus(account: WeiboAccountItem) {
|
||||
|
||||
export default function WeiboAssignmentsPage() {
|
||||
const { token } = theme.useToken();
|
||||
const { modal } = App.useApp();
|
||||
const { can } = usePermissions();
|
||||
const canForceAssign = can('weibo:assign:force');
|
||||
const [summary, setSummary] = useState<AssignmentsSummary | null>(null);
|
||||
const [supportUsers, setSupportUsers] = useState<SupportUserItem[]>([]);
|
||||
const [selectedUser, setSelectedUser] = useState<SupportUserItem | null>(null);
|
||||
@@ -75,7 +80,10 @@ export default function WeiboAssignmentsPage() {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [filterTag, setFilterTag] = useState<string | undefined>();
|
||||
const [enabledFilter, setEnabledFilter] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<'unassigned' | 'assigned'>('unassigned');
|
||||
const [assignFilter, setAssignFilter] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
'unassigned' | 'assigned' | 'all'
|
||||
>('unassigned');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(() => {
|
||||
const saved = localStorage.getItem(STORAGE_KEY_PAGE_SIZE);
|
||||
@@ -119,7 +127,12 @@ export default function WeiboAssignmentsPage() {
|
||||
: enabledFilter === 'disabled'
|
||||
? false
|
||||
: undefined,
|
||||
assign_status: activeTab === 'unassigned' ? 'unassigned' : undefined,
|
||||
assign_status:
|
||||
activeTab === 'unassigned'
|
||||
? 'unassigned'
|
||||
: activeTab === 'all' && assignFilter
|
||||
? assignFilter
|
||||
: undefined,
|
||||
assigned_user_id:
|
||||
activeTab === 'assigned' ? selectedUser?.id : undefined,
|
||||
});
|
||||
@@ -133,6 +146,7 @@ export default function WeiboAssignmentsPage() {
|
||||
},
|
||||
[
|
||||
activeTab,
|
||||
assignFilter,
|
||||
currentPage,
|
||||
enabledFilter,
|
||||
filterTag,
|
||||
@@ -176,7 +190,7 @@ export default function WeiboAssignmentsPage() {
|
||||
|
||||
const handleSelectUser = (user: SupportUserItem) => {
|
||||
setSelectedUser(user);
|
||||
setActiveTab('unassigned');
|
||||
setActiveTab((current) => (current === 'all' ? 'all' : 'unassigned'));
|
||||
setSelectedRowKeys([]);
|
||||
setCurrentPage(1);
|
||||
localStorage.setItem(STORAGE_KEY_SELECTED_USER, String(user.id));
|
||||
@@ -211,6 +225,88 @@ export default function WeiboAssignmentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleForceAssignSelected = async () => {
|
||||
if (!selectedUser || selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择客服和微博账号');
|
||||
return;
|
||||
}
|
||||
setAssigning(true);
|
||||
try {
|
||||
const result = await weiboApi.forceAssign(
|
||||
selectedUser.id,
|
||||
selectedRowKeys.length,
|
||||
{ accountIds: selectedRowKeys },
|
||||
);
|
||||
message.success(
|
||||
`已强制将 ${result.updated} 个账号分配给 ${result.assigned_username}`,
|
||||
);
|
||||
setSelectedRowKeys([]);
|
||||
await refreshData();
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setAssigning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForceAssignByTag = async () => {
|
||||
if (!selectedUser) {
|
||||
message.warning('请先选择客服');
|
||||
return;
|
||||
}
|
||||
const tag = filterTag?.trim();
|
||||
if (!tag) {
|
||||
message.warning('请先选择要强制分配的统一标签');
|
||||
return;
|
||||
}
|
||||
|
||||
setAssigning(true);
|
||||
try {
|
||||
const preview = await weiboApi.listAccountIds({ tag });
|
||||
const count = preview.account_ids.length;
|
||||
if (count === 0) {
|
||||
message.info(`标签「${tag}」下没有账号`);
|
||||
return;
|
||||
}
|
||||
modal.confirm({
|
||||
title: `强制分配标签「${tag}」下的 ${count} 个账号?`,
|
||||
content: (
|
||||
<Space orientation="vertical" size={4}>
|
||||
<Text>目标客服:{selectedUser.username}</Text>
|
||||
<Text type="warning">
|
||||
已分配给其他客服的账号也会转走,原客服的普通工作台映射会同步清理。
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
okText: '确认强制分配',
|
||||
cancelText: '取消',
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
setAssigning(true);
|
||||
try {
|
||||
const result = await weiboApi.forceAssign(selectedUser.id, count, {
|
||||
tag,
|
||||
});
|
||||
message.success(
|
||||
`已强制将标签「${tag}」下的 ${result.updated} 个账号分配给 ${result.assigned_username}`,
|
||||
);
|
||||
setSelectedRowKeys([]);
|
||||
await refreshData();
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
throw error;
|
||||
} finally {
|
||||
setAssigning(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setAssigning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignAllFiltered = async () => {
|
||||
if (!selectedUser) {
|
||||
message.warning('请先选择客服');
|
||||
@@ -302,6 +398,22 @@ export default function WeiboAssignmentsPage() {
|
||||
render: (tag: string) =>
|
||||
tag ? <Tag color="blue">{tag}</Tag> : <Text type="secondary">-</Text>,
|
||||
},
|
||||
...(activeTab === 'all'
|
||||
? [
|
||||
{
|
||||
title: '归属客服',
|
||||
dataIndex: 'assigned_username',
|
||||
width: 110,
|
||||
align: 'center' as const,
|
||||
render: (username: string) =>
|
||||
username ? (
|
||||
<Tag color="cyan">{username}</Tag>
|
||||
) : (
|
||||
<Text type="secondary">未分配</Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'CK 状态',
|
||||
dataIndex: 'ck_check_status',
|
||||
@@ -524,13 +636,29 @@ export default function WeiboAssignmentsPage() {
|
||||
{ value: 'disabled', label: '已停用' },
|
||||
]}
|
||||
/>
|
||||
{activeTab === 'all' && (
|
||||
<Select
|
||||
value={assignFilter}
|
||||
onChange={(value) => {
|
||||
setAssignFilter(value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ width: 120 }}
|
||||
size="small"
|
||||
options={[
|
||||
{ value: '', label: '全部归属' },
|
||||
{ value: 'unassigned', label: '未分配' },
|
||||
{ value: 'assigned', label: '已分配' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
size="small"
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => {
|
||||
setActiveTab(key as 'unassigned' | 'assigned');
|
||||
setActiveTab(key as 'unassigned' | 'assigned' | 'all');
|
||||
setSelectedRowKeys([]);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
@@ -544,6 +672,9 @@ export default function WeiboAssignmentsPage() {
|
||||
: '已分配账号',
|
||||
disabled: !selectedUser,
|
||||
},
|
||||
...(canForceAssign
|
||||
? [{ key: 'all', label: `全部账号 (${accountCount})` }]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -593,6 +724,43 @@ export default function WeiboAssignmentsPage() {
|
||||
取消分配选中 ({selectedRowKeys.length})
|
||||
</Button>
|
||||
)}
|
||||
{activeTab === 'all' && canForceAssign && selectedUser && (
|
||||
<>
|
||||
<Popconfirm
|
||||
title={`强制把选中的 ${selectedRowKeys.length} 个账号分配给 ${selectedUser.username}?`}
|
||||
description="已分配给其他客服的账号也会转走,原客服的普通工作台映射会同步清理。"
|
||||
okText="确认强制分配"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onConfirm={handleForceAssignSelected}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<SwapOutlined />}
|
||||
loading={assigning}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
强制分配选中 ({selectedRowKeys.length}) 给 {selectedUser.username}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Button
|
||||
danger
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
icon={<SwapOutlined />}
|
||||
loading={assigning}
|
||||
disabled={!filterTag}
|
||||
onClick={() => void handleForceAssignByTag()}
|
||||
>
|
||||
{filterTag
|
||||
? `强制分配标签「${filterTag}」全部`
|
||||
: '选择标签后按标签强制分配'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Table<WeiboAccountItem>
|
||||
@@ -615,7 +783,7 @@ export default function WeiboAssignmentsPage() {
|
||||
preserveSelectedRowKeys: true,
|
||||
onChange: (keys) => setSelectedRowKeys(keys.map(Number)),
|
||||
}}
|
||||
scroll={{ x: 940, y: 'calc(100vh - 500px)' }}
|
||||
scroll={{ x: activeTab === 'all' ? 1050 : 940, y: 'calc(100vh - 500px)' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
Reference in New Issue
Block a user