feat(weibo): 增加协作 CK 失效筛选与实时同步开关
This commit is contained in:
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from core.weibo import WeiboAuthError
|
||||||
from web.backend.models import AuditLog, User, WeiboAccount, WeiboExternalOrderMetric
|
from web.backend.models import AuditLog, User, WeiboAccount, WeiboExternalOrderMetric
|
||||||
|
from web.backend.routers import weibo_collab as weibo_collab_router
|
||||||
from web.backend.schemas import WeiboCollabCkExportOut
|
from web.backend.schemas import WeiboCollabCkExportOut
|
||||||
from web.backend.services import weibo_collab_service as collab
|
from web.backend.services import weibo_collab_service as collab
|
||||||
|
|
||||||
@@ -223,3 +225,67 @@ def test_export_cks_exposes_skipped_orders(db):
|
|||||||
assert dumped["exported"] == 1
|
assert dumped["exported"] == 1
|
||||||
assert {item["id"] for item in dumped["skipped"]} == {unbound.id, 999999}
|
assert {item["id"] for item in dumped["skipped"]} == {unbound.id, 999999}
|
||||||
assert any("还没关联微博账号" in item["reason"] for item in dumped["skipped"])
|
assert any("还没关联微博账号" in item["reason"] for item in dumped["skipped"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_orders_filters_invalid_ck_and_summary_excludes_error(db):
|
||||||
|
user = make_user(db)
|
||||||
|
valid_account = make_account(db, uid="4014000101")
|
||||||
|
invalid_account = make_account(db, uid="4014000102")
|
||||||
|
error_account = make_account(db, uid="4014000103")
|
||||||
|
invalid_account.ck_check_status = "invalid"
|
||||||
|
error_account.ck_check_status = "error"
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
for index, account in enumerate(
|
||||||
|
(valid_account, invalid_account, error_account), start=1
|
||||||
|
):
|
||||||
|
collab.create_order(
|
||||||
|
db,
|
||||||
|
user,
|
||||||
|
order_no=f"WB-CK-FILTER-{index}",
|
||||||
|
account_id=account.id,
|
||||||
|
goods_name="商品A",
|
||||||
|
required_weidou=100,
|
||||||
|
require_verified=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
invalid = collab.list_orders(
|
||||||
|
db,
|
||||||
|
page=1,
|
||||||
|
page_size=20,
|
||||||
|
ck_status="invalid",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert invalid["total"] == 1
|
||||||
|
assert invalid["items"][0]["account_id"] == invalid_account.id
|
||||||
|
assert collab.summary(db)["ck_invalid"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_batch_persists_ck_invalid_after_row_rollback(monkeypatch, db):
|
||||||
|
user = make_user(db)
|
||||||
|
account = make_account(db)
|
||||||
|
order = collab.create_order(
|
||||||
|
db,
|
||||||
|
user,
|
||||||
|
order_no="WB-CK-VERIFY-INVALID",
|
||||||
|
account_id=account.id,
|
||||||
|
goods_name="商品A",
|
||||||
|
required_weidou=100,
|
||||||
|
require_verified=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fail_refresh(*_args, **_kwargs):
|
||||||
|
raise WeiboAuthError("CK 已过期")
|
||||||
|
|
||||||
|
monkeypatch.setattr(collab, "refresh_weibo_account", fail_refresh)
|
||||||
|
|
||||||
|
result = weibo_collab_router.verify_external_orders_batch(
|
||||||
|
[order.id],
|
||||||
|
db=db,
|
||||||
|
current=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert result["results"][0]["ck_status"] == "invalid"
|
||||||
|
db.refresh(account)
|
||||||
|
assert account.ck_check_status == "invalid"
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ from ..schemas import (
|
|||||||
)
|
)
|
||||||
from ..services import weibo_collab_service as collab
|
from ..services import weibo_collab_service as collab
|
||||||
from ..services.weibo_collab_events import collab_events
|
from ..services.weibo_collab_events import collab_events
|
||||||
|
from ..services.weibo_service import mark_weibo_account_refresh_failure
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/weibo", tags=["微博外部单协作"])
|
router = APIRouter(prefix="/api/weibo", tags=["微博外部单协作"])
|
||||||
WEIBO_DEFAULT_ACTIVITY = os.getenv("WEIBO_DEFAULT_ACTIVITY", "").strip()
|
WEIBO_DEFAULT_ACTIVITY = os.getenv("WEIBO_DEFAULT_ACTIVITY", "").strip()
|
||||||
@@ -320,6 +321,7 @@ def list_external_orders(
|
|||||||
page_size: int = Query(20, ge=1, le=200),
|
page_size: int = Query(20, ge=1, le=200),
|
||||||
archive_scope: str = Query("active"),
|
archive_scope: str = Query("active"),
|
||||||
status: str = Query(""),
|
status: str = Query(""),
|
||||||
|
ck_status: str = Query(""),
|
||||||
shop: str = Query(""),
|
shop: str = Query(""),
|
||||||
owner_id: int | None = Query(None),
|
owner_id: int | None = Query(None),
|
||||||
goods_name: str = Query(""),
|
goods_name: str = Query(""),
|
||||||
@@ -334,6 +336,7 @@ def list_external_orders(
|
|||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
archive_scope=archive_scope,
|
archive_scope=archive_scope,
|
||||||
status=status,
|
status=status,
|
||||||
|
ck_status=ck_status,
|
||||||
shop=shop,
|
shop=shop,
|
||||||
owner_id=owner_id,
|
owner_id=owner_id,
|
||||||
goods_name=goods_name,
|
goods_name=goods_name,
|
||||||
@@ -397,30 +400,44 @@ def verify_external_orders_batch(
|
|||||||
rows = collab.list_orders_by_ids(db, ids)
|
rows = collab.list_orders_by_ids(db, ids)
|
||||||
results: list[dict] = []
|
results: list[dict] = []
|
||||||
for order in rows:
|
for order in rows:
|
||||||
|
ck_status = ""
|
||||||
try:
|
try:
|
||||||
with db.begin_nested():
|
with db.begin_nested():
|
||||||
collab.verify_order_account(
|
account = collab.verify_order_account(
|
||||||
db,
|
db,
|
||||||
current,
|
current,
|
||||||
order,
|
order,
|
||||||
activity=WEIBO_DEFAULT_ACTIVITY,
|
activity=WEIBO_DEFAULT_ACTIVITY,
|
||||||
commit=False,
|
commit=False,
|
||||||
)
|
)
|
||||||
|
ck_status = account.ck_check_status
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
"id": order.id,
|
"id": order.id,
|
||||||
"order_no": order.order_no,
|
"order_no": order.order_no,
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
"ck_status": ck_status,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
|
if order.account_id:
|
||||||
|
account = (
|
||||||
|
db.query(WeiboAccount)
|
||||||
|
.filter(WeiboAccount.id == order.account_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if account and mark_weibo_account_refresh_failure(
|
||||||
|
db, account, exc, commit=False
|
||||||
|
):
|
||||||
|
ck_status = account.ck_check_status
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
"id": order.id,
|
"id": order.id,
|
||||||
"order_no": order.order_no,
|
"order_no": order.order_no,
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": str(exc),
|
"message": str(exc),
|
||||||
|
"ck_status": ck_status,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -1553,6 +1553,7 @@ def _apply_filters(
|
|||||||
*,
|
*,
|
||||||
archive_scope: str = "active",
|
archive_scope: str = "active",
|
||||||
status: str = "",
|
status: str = "",
|
||||||
|
ck_status: str = "",
|
||||||
shop: str = "",
|
shop: str = "",
|
||||||
owner_id: int | None = None,
|
owner_id: int | None = None,
|
||||||
goods_name: str = "",
|
goods_name: str = "",
|
||||||
@@ -1568,6 +1569,10 @@ def _apply_filters(
|
|||||||
query = query.filter(WeiboExternalOrder.status != STATUS_VOID)
|
query = query.filter(WeiboExternalOrder.status != STATUS_VOID)
|
||||||
if _text(status):
|
if _text(status):
|
||||||
query = query.filter(WeiboExternalOrder.status == status)
|
query = query.filter(WeiboExternalOrder.status == status)
|
||||||
|
if _text(ck_status):
|
||||||
|
query = query.join(
|
||||||
|
WeiboAccount, WeiboAccount.id == WeiboExternalOrder.account_id
|
||||||
|
).filter(WeiboAccount.ck_check_status == ck_status)
|
||||||
if _text(shop):
|
if _text(shop):
|
||||||
query = query.filter(WeiboExternalOrder.shop == shop)
|
query = query.filter(WeiboExternalOrder.shop == shop)
|
||||||
if owner_id is not None:
|
if owner_id is not None:
|
||||||
@@ -1598,6 +1603,7 @@ def list_orders(
|
|||||||
page_size: int,
|
page_size: int,
|
||||||
archive_scope: str = "active",
|
archive_scope: str = "active",
|
||||||
status: str = "",
|
status: str = "",
|
||||||
|
ck_status: str = "",
|
||||||
shop: str = "",
|
shop: str = "",
|
||||||
owner_id: int | None = None,
|
owner_id: int | None = None,
|
||||||
goods_name: str = "",
|
goods_name: str = "",
|
||||||
@@ -1608,6 +1614,7 @@ def list_orders(
|
|||||||
_base_query(db),
|
_base_query(db),
|
||||||
archive_scope=archive_scope,
|
archive_scope=archive_scope,
|
||||||
status=status,
|
status=status,
|
||||||
|
ck_status=ck_status,
|
||||||
shop=shop,
|
shop=shop,
|
||||||
owner_id=owner_id,
|
owner_id=owner_id,
|
||||||
goods_name=goods_name,
|
goods_name=goods_name,
|
||||||
@@ -1710,7 +1717,7 @@ def summary(
|
|||||||
)
|
)
|
||||||
ck_invalid = (
|
ck_invalid = (
|
||||||
query.join(WeiboAccount, WeiboAccount.id == WeiboExternalOrder.account_id)
|
query.join(WeiboAccount, WeiboAccount.id == WeiboExternalOrder.account_id)
|
||||||
.filter(WeiboAccount.ck_check_status.in_(("invalid", "error")))
|
.filter(WeiboAccount.ck_check_status == "invalid")
|
||||||
.with_entities(func.count(WeiboExternalOrder.id))
|
.with_entities(func.count(WeiboExternalOrder.id))
|
||||||
.scalar()
|
.scalar()
|
||||||
or 0
|
or 0
|
||||||
|
|||||||
@@ -190,9 +190,7 @@ def refresh_weibo_account(
|
|||||||
snapshot = client.fetch_home(activity)
|
snapshot = client.fetch_home(activity)
|
||||||
client.check_payment_access(activity, home=snapshot)
|
client.check_payment_access(activity, home=snapshot)
|
||||||
except (WeiboError, ValueError) as exc:
|
except (WeiboError, ValueError) as exc:
|
||||||
account.ck_check_status = _error_status(exc)
|
mark_weibo_account_refresh_failure(db, account, exc, commit=False)
|
||||||
account.ck_check_message = str(exc)
|
|
||||||
account.ck_checked_at = _now()
|
|
||||||
if commit:
|
if commit:
|
||||||
db.commit()
|
db.commit()
|
||||||
raise
|
raise
|
||||||
@@ -204,6 +202,26 @@ def refresh_weibo_account(
|
|||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
def mark_weibo_account_refresh_failure(
|
||||||
|
db: Session,
|
||||||
|
account: WeiboAccount,
|
||||||
|
exc: Exception,
|
||||||
|
*,
|
||||||
|
commit: bool = True,
|
||||||
|
) -> bool:
|
||||||
|
"""把一次刷新失败写回账号;业务校验失败不应被误标成 CK 失效。"""
|
||||||
|
|
||||||
|
if not isinstance(exc, (WeiboError, ValueError)):
|
||||||
|
return False
|
||||||
|
account.ck_check_status = _error_status(exc)
|
||||||
|
account.ck_check_message = str(exc)
|
||||||
|
account.ck_checked_at = _now()
|
||||||
|
if commit:
|
||||||
|
db.commit()
|
||||||
|
db.refresh(account)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def update_weibo_account_metadata(
|
def update_weibo_account_metadata(
|
||||||
db: Session,
|
db: Session,
|
||||||
account: WeiboAccount,
|
account: WeiboAccount,
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ export interface WeiboCollabVerifyBatchResult {
|
|||||||
order_no: string;
|
order_no: string;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
|
ck_status: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Col,
|
Col,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Statistic,
|
Statistic,
|
||||||
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -92,6 +94,16 @@ const STATUS_ORDER: WeiboCollabStatus[] = [
|
|||||||
'void',
|
'void',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const REALTIME_PAUSED_STORAGE_KEY = 'weibo.collab.realtime-paused';
|
||||||
|
|
||||||
|
function readRealtimePaused(): boolean {
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem(REALTIME_PAUSED_STORAGE_KEY) === '1';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const MANUAL_STATUS_OPTIONS = STATUS_ORDER.filter(
|
const MANUAL_STATUS_OPTIONS = STATUS_ORDER.filter(
|
||||||
(item) => item !== 'weidou_error',
|
(item) => item !== 'weidou_error',
|
||||||
).map((value) => ({ value, label: STATUS_META[value].label }));
|
).map((value) => ({ value, label: STATUS_META[value].label }));
|
||||||
@@ -147,12 +159,14 @@ export default function WeiboCollabPage() {
|
|||||||
const [copyingCk, setCopyingCk] = useState(false);
|
const [copyingCk, setCopyingCk] = useState(false);
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
const [status, setStatus] = useState('');
|
const [status, setStatus] = useState('');
|
||||||
|
const [ckStatus, setCkStatus] = useState('');
|
||||||
const [shop, setShop] = useState('');
|
const [shop, setShop] = useState('');
|
||||||
const [ownerId, setOwnerId] = useState<number | null>(null);
|
const [ownerId, setOwnerId] = useState<number | null>(null);
|
||||||
const [goodsName, setGoodsName] = useState('');
|
const [goodsName, setGoodsName] = useState('');
|
||||||
const [includeVoid, setIncludeVoid] = useState(false);
|
const [includeVoid, setIncludeVoid] = useState(false);
|
||||||
const [archiveScope, setArchiveScope] = useState<'active' | 'archived' | 'all'>('active');
|
const [archiveScope, setArchiveScope] = useState<'active' | 'archived' | 'all'>('active');
|
||||||
const [threshold, setThreshold] = useState(100);
|
const [threshold, setThreshold] = useState(100);
|
||||||
|
const [realtimePaused, setRealtimePaused] = useState(readRealtimePaused);
|
||||||
|
|
||||||
const [form] = Form.useForm<OrderFormValues>();
|
const [form] = Form.useForm<OrderFormValues>();
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
@@ -214,6 +228,7 @@ export default function WeiboCollabPage() {
|
|||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
archive_scope: archiveScope,
|
archive_scope: archiveScope,
|
||||||
status: status || undefined,
|
status: status || undefined,
|
||||||
|
ck_status: ckStatus || undefined,
|
||||||
shop: shop || undefined,
|
shop: shop || undefined,
|
||||||
owner_id: ownerId ?? undefined,
|
owner_id: ownerId ?? undefined,
|
||||||
goods_name: goodsName || undefined,
|
goods_name: goodsName || undefined,
|
||||||
@@ -230,6 +245,7 @@ export default function WeiboCollabPage() {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
archiveScope,
|
archiveScope,
|
||||||
|
ckStatus,
|
||||||
goodsName,
|
goodsName,
|
||||||
includeVoid,
|
includeVoid,
|
||||||
keyword,
|
keyword,
|
||||||
@@ -253,12 +269,32 @@ export default function WeiboCollabPage() {
|
|||||||
void loadOrders();
|
void loadOrders();
|
||||||
}, [loadOrders]);
|
}, [loadOrders]);
|
||||||
|
|
||||||
useCollabRealtime({
|
const { connected: realtimeConnected, reconnecting: realtimeReconnecting } =
|
||||||
enabled: true,
|
useCollabRealtime({
|
||||||
|
enabled: !realtimePaused,
|
||||||
path: '/api/weibo/external-orders/ws',
|
path: '/api/weibo/external-orders/ws',
|
||||||
onInvalidate: () => refreshAll(),
|
onInvalidate: () => refreshAll(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggleRealtimeSync = (nextEnabled: boolean) => {
|
||||||
|
const nextPaused = !nextEnabled;
|
||||||
|
setRealtimePaused(nextPaused);
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
REALTIME_PAUSED_STORAGE_KEY,
|
||||||
|
nextPaused ? '1' : '0',
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// localStorage 不可写时仍保留本次会话内的开关状态。
|
||||||
|
}
|
||||||
|
if (nextPaused) {
|
||||||
|
message.info('已暂停实时同步,需要最新数据时请点「刷新」');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.success('已恢复实时同步');
|
||||||
|
refreshAll();
|
||||||
|
};
|
||||||
|
|
||||||
const accountOptions = accounts.map((item) => ({
|
const accountOptions = accounts.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: `${item.nickname || item.uid} · UID ${item.uid}${item.game_role_name ? ` · ${item.game_role_name}` : ''}`,
|
label: `${item.nickname || item.uid} · UID ${item.uid}${item.game_role_name ? ` · ${item.game_role_name}` : ''}`,
|
||||||
@@ -466,8 +502,22 @@ export default function WeiboCollabPage() {
|
|||||||
if (!ids.length) return;
|
if (!ids.length) return;
|
||||||
try {
|
try {
|
||||||
const result = await weiboCollabApi.verifyBatch(ids);
|
const result = await weiboCollabApi.verifyBatch(ids);
|
||||||
|
const invalidCount = result.results.filter(
|
||||||
|
(item) => item.ck_status === 'invalid',
|
||||||
|
).length;
|
||||||
|
if (invalidCount > 0) {
|
||||||
|
setStatus('');
|
||||||
|
setCkStatus('invalid');
|
||||||
|
setPage(1);
|
||||||
|
void loadBase();
|
||||||
|
message.warning(`检测到 ${invalidCount} 个 CK 失效,已切换到 CK 失效列表`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (result.success) message.success(`已校验 ${ids.length} 单`);
|
if (result.success) message.success(`已校验 ${ids.length} 单`);
|
||||||
else message.warning(`校验完成,${result.results.filter((item) => !item.success).length} 单失败`);
|
else
|
||||||
|
message.warning(
|
||||||
|
`校验完成,${result.results.filter((item) => !item.success).length} 单失败`,
|
||||||
|
);
|
||||||
refreshAll();
|
refreshAll();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(getErrorMessage(error));
|
message.error(getErrorMessage(error));
|
||||||
@@ -615,6 +665,18 @@ export default function WeiboCollabPage() {
|
|||||||
[summary],
|
[summary],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const applyStatusCard = (nextStatus: string) => {
|
||||||
|
setStatus(nextStatus);
|
||||||
|
setCkStatus('');
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyCkInvalidCard = () => {
|
||||||
|
setStatus('');
|
||||||
|
setCkStatus('invalid');
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
const copySelectedUsernames = async () => {
|
const copySelectedUsernames = async () => {
|
||||||
const selectedOrders = orders.filter((row) => selectedIds.includes(row.id));
|
const selectedOrders = orders.filter((row) => selectedIds.includes(row.id));
|
||||||
const values = Array.from(
|
const values = Array.from(
|
||||||
@@ -894,26 +956,96 @@ export default function WeiboCollabPage() {
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{statusCards.map((item) => (
|
{statusCards.map((item) => {
|
||||||
<button
|
const active = !ckStatus && status === item.key;
|
||||||
key={item.key || 'all'}
|
return (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => {
|
key={item.key || 'all'}
|
||||||
setStatus(item.key);
|
type="button"
|
||||||
setPage(1);
|
onClick={() => applyStatusCard(item.key)}
|
||||||
}}
|
style={{
|
||||||
style={{
|
border: `1px solid ${active ? token.colorPrimary : token.colorBorderSecondary}`,
|
||||||
border: `1px solid ${status === item.key ? token.colorPrimary : token.colorBorderSecondary}`,
|
background: active ? token.colorPrimaryBg : token.colorBgContainer,
|
||||||
background: status === item.key ? token.colorPrimaryBg : token.colorBgContainer,
|
borderRadius: 6,
|
||||||
borderRadius: 6,
|
padding: '10px 12px',
|
||||||
padding: '10px 12px',
|
textAlign: 'left',
|
||||||
textAlign: 'left',
|
cursor: 'pointer',
|
||||||
cursor: 'pointer',
|
}}
|
||||||
}}
|
>
|
||||||
|
<Statistic
|
||||||
|
title={item.label}
|
||||||
|
value={item.value}
|
||||||
|
valueStyle={{ fontSize: 20 }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={applyCkInvalidCard}
|
||||||
|
style={{
|
||||||
|
border: `1px solid ${ckStatus === 'invalid' ? token.colorPrimary : token.colorBorderSecondary}`,
|
||||||
|
background:
|
||||||
|
ckStatus === 'invalid'
|
||||||
|
? token.colorPrimaryBg
|
||||||
|
: token.colorBgContainer,
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: '10px 12px',
|
||||||
|
textAlign: 'left',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
title="CK 失效"
|
||||||
|
value={summary?.ck_invalid ?? 0}
|
||||||
|
valueStyle={{ fontSize: 20, color: token.colorError }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: `1px solid ${token.colorBorderSecondary}`,
|
||||||
|
background: token.colorBgContainer,
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: '10px 12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
size={6}
|
||||||
|
style={{ width: '100%', justifyContent: 'space-between' }}
|
||||||
>
|
>
|
||||||
<Statistic title={item.label} value={item.value} valueStyle={{ fontSize: 20 }} />
|
<Text strong style={{ fontSize: 13 }}>
|
||||||
</button>
|
实时同步
|
||||||
))}
|
</Text>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={!realtimePaused}
|
||||||
|
onChange={toggleRealtimeSync}
|
||||||
|
aria-label="实时同步开关"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<Badge
|
||||||
|
status={
|
||||||
|
realtimePaused
|
||||||
|
? 'default'
|
||||||
|
: realtimeConnected
|
||||||
|
? 'success'
|
||||||
|
: realtimeReconnecting
|
||||||
|
? 'warning'
|
||||||
|
: 'error'
|
||||||
|
}
|
||||||
|
text={
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{realtimePaused
|
||||||
|
? '已暂停,仅手动刷新'
|
||||||
|
: realtimeConnected
|
||||||
|
? '已连接,自动刷新'
|
||||||
|
: realtimeReconnecting
|
||||||
|
? '重连中'
|
||||||
|
: '未连接'}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -972,6 +1104,21 @@ export default function WeiboCollabPage() {
|
|||||||
...STATUS_ORDER.map((item) => ({ value: item, label: STATUS_META[item].label })),
|
...STATUS_ORDER.map((item) => ({ value: item, label: STATUS_META[item].label })),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
value={ckStatus || undefined}
|
||||||
|
onChange={(value) => {
|
||||||
|
setCkStatus(value || '');
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
placeholder="CK 状态"
|
||||||
|
style={{ width: 130 }}
|
||||||
|
options={[
|
||||||
|
{ value: 'valid', label: ACCOUNT_CK_LABELS.valid },
|
||||||
|
{ value: 'invalid', label: ACCOUNT_CK_LABELS.invalid },
|
||||||
|
{ value: 'error', label: ACCOUNT_CK_LABELS.error },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={archiveScope}
|
value={archiveScope}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user