diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 64ad71c..aead9ac 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -26,7 +26,7 @@ TESTS_DIR = Path(__file__).resolve().parent PROJECT_ROOT = TESTS_DIR.parent VERSIONS_DIR = PROJECT_ROOT / "web" / "backend" / "migrations" / "versions" -HEAD_REVISION = "20261107_0069" +HEAD_REVISION = "20261108_0070" # 迁移新增、但不声明在模型里的复合查询索引。 EXTRA_INDEXES = ( diff --git a/tests/test_weibo_accounts.py b/tests/test_weibo_accounts.py index 15f244f..86af236 100644 --- a/tests/test_weibo_accounts.py +++ b/tests/test_weibo_accounts.py @@ -21,10 +21,17 @@ from web.backend.models import User, WeiboAccount from web.backend.routers import weibo as weibo_router from web.backend.routers.weibo import ( create_bind_qrcodes, + disable_account, + enable_account, + get_account, import_cookies, list_accounts, + refresh_account, + update_account, ) from web.backend.schemas import ( + WeiboAccountDisableRequest, + WeiboAccountUpdate, WeiboCookieImport, WeiboExchangeRequest, WeiboPaymentStatusRequest, @@ -204,6 +211,9 @@ def test_list_accounts_returns_parallel_page_shape(db, current_user): "id", "uid", "nickname", + "tag", + "remark", + "is_enabled", "is_bound", "can_rebind", "rebind_remain_days", @@ -212,6 +222,96 @@ def test_list_accounts_returns_parallel_page_shape(db, current_user): } +def test_account_crud_and_lifecycle(db, current_user): + account_out = import_cookies( + WeiboCookieImport(text="SCF=a; SUB=b", tag="初始"), + db=db, + current=current_user, + ).accounts[0] + + detail = get_account( + account_out.id, + db=db, + current=current_user, + ).model_dump() + assert detail["is_enabled"] is True + assert detail["remark"] == "" + + updated = update_account( + account_out.id, + WeiboAccountUpdate(tag="重点", remark="客户 A"), + db=db, + current=current_user, + ).model_dump() + assert updated["tag"] == "重点" + assert updated["remark"] == "客户 A" + + disabled = disable_account( + account_out.id, + WeiboAccountDisableRequest(reason="暂停使用"), + db=db, + current=current_user, + ).model_dump() + assert disabled["is_enabled"] is False + assert disabled["disabled_reason"] == "暂停使用" + + disabled_page = list_accounts( + enabled=False, + page=1, + page_size=20, + db=db, + current=current_user, + ) + assert disabled_page["total"] == 1 + assert disabled_page["items"][0].model_dump()["is_enabled"] is False + + with pytest.raises(HTTPException) as exc_info: + refresh_account(account_out.id, db=db, current=current_user) + assert exc_info.value.status_code == 409 + + disabled_actions = [ + lambda: create_bind_qrcodes(account_out.id, db=db, current=current_user), + lambda: weibo_router.list_mall_goods( + account_out.id, + db=db, + current=current_user, + ), + lambda: weibo_router.exchange_goods( + account_out.id, + WeiboExchangeRequest(goods_id=37, expected_price=100, confirm=True), + db=db, + current=current_user, + ), + lambda: weibo_router.create_recharge_order( + account_out.id, + WeiboRechargeRequest(amount_yuan=1), + db=db, + current=current_user, + ), + ] + for action in disabled_actions: + with pytest.raises(HTTPException) as disabled_exc: + action() + assert disabled_exc.value.status_code == 409 + + import_cookies( + WeiboCookieImport(text="SCF=c; SUB=d", tag=""), + db=db, + current=current_user, + ) + account = db.query(WeiboAccount).filter(WeiboAccount.id == account_out.id).one() + assert account.is_enabled is False + assert account.tag == "重点" + + enabled = enable_account( + account_out.id, + db=db, + current=current_user, + ).model_dump() + assert enabled["is_enabled"] is True + assert enabled["disabled_reason"] == "" + + def test_bind_qrcodes_returns_both_channels_and_partial_error(db, current_user): account = import_cookies( WeiboCookieImport(text="SCF=a; SUB=b", tag=""), diff --git a/web/backend/migrations/versions/20261108_0070_weibo_account_lifecycle.py b/web/backend/migrations/versions/20261108_0070_weibo_account_lifecycle.py new file mode 100644 index 0000000..92f825f --- /dev/null +++ b/web/backend/migrations/versions/20261108_0070_weibo_account_lifecycle.py @@ -0,0 +1,71 @@ +"""增加微博账号备注与启停生命周期。""" + +from __future__ import annotations + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "20261108_0070" +down_revision: str | None = "20261107_0069" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +TABLE = "weibo_accounts" + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + columns = {column["name"] for column in inspector.get_columns(TABLE)} + if "remark" not in columns: + op.add_column( + TABLE, + sa.Column( + "remark", + sa.String(length=256), + nullable=False, + server_default="", + ), + ) + if "is_enabled" not in columns: + op.add_column( + TABLE, + sa.Column( + "is_enabled", + sa.Boolean(), + nullable=False, + server_default=sa.text("1"), + ), + ) + op.create_index( + f"ix_{TABLE}_is_enabled", + TABLE, + ["is_enabled"], + unique=False, + ) + if "disabled_at" not in columns: + op.add_column(TABLE, sa.Column("disabled_at", sa.DateTime(), nullable=True)) + if "disabled_reason" not in columns: + op.add_column( + TABLE, + sa.Column( + "disabled_reason", + sa.String(length=256), + nullable=False, + server_default="", + ), + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + columns = {column["name"] for column in inspector.get_columns(TABLE)} + indexes = {index["name"] for index in inspector.get_indexes(TABLE)} + if f"ix_{TABLE}_is_enabled" in indexes: + op.drop_index(f"ix_{TABLE}_is_enabled", table_name=TABLE) + for column in ("disabled_reason", "disabled_at", "is_enabled", "remark"): + if column in columns: + op.drop_column(TABLE, column) diff --git a/web/backend/models.py b/web/backend/models.py index d1f2bbd..d67cc8a 100644 --- a/web/backend/models.py +++ b/web/backend/models.py @@ -930,8 +930,19 @@ class WeiboAccount(Base): UTCDateTime, nullable=True ) tag: Mapped[str] = mapped_column(String(64), default="") + remark: Mapped[str] = mapped_column( + String(256), nullable=False, default="", server_default="" + ) activity: Mapped[str] = mapped_column(String(64), default="") + is_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True, server_default="1", index=True + ) + disabled_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True) + disabled_reason: Mapped[str] = mapped_column( + String(256), nullable=False, default="", server_default="" + ) + ck_check_status: Mapped[str] = mapped_column( String(16), nullable=False, default="", server_default="" ) diff --git a/web/backend/permissions.py b/web/backend/permissions.py index 281259f..1ee7d41 100644 --- a/web/backend/permissions.py +++ b/web/backend/permissions.py @@ -44,6 +44,7 @@ PERMISSIONS = { # 微博微豆 "weibo:view": "查看微博账号", "weibo:import": "导入/刷新微博 CK", + "weibo:manage": "编辑、停用和启用微博账号", "weibo:bind": "生成微博绑定二维码", "weibo:recharge": "创建微博助威充值订单", "weibo:exchange": "执行微博微豆兑换", @@ -110,6 +111,7 @@ ROLE_PERMISSIONS = { "yyb:history", "weibo:view", "weibo:import", + "weibo:manage", "weibo:bind", "weibo:recharge", "weibo:exchange", diff --git a/web/backend/routers/weibo.py b/web/backend/routers/weibo.py index 2834d5b..2d9fac4 100644 --- a/web/backend/routers/weibo.py +++ b/web/backend/routers/weibo.py @@ -16,7 +16,9 @@ from ..deps import get_current_user from ..models import User, WeiboAccount from ..permissions import user_has_permission from ..schemas import ( + WeiboAccountDisableRequest, WeiboAccountOut, + WeiboAccountUpdate, WeiboBindQrOut, WeiboBindSchemeOut, WeiboCookieImport, @@ -38,6 +40,8 @@ from ..schemas import ( from ..services.audit_service import record_audit from ..services.weibo_service import ( create_weibo_recharge_order, + disable_weibo_account, + enable_weibo_account, exchange_weibo_goods, generate_weibo_bind_schemes, import_weibo_cookies, @@ -46,6 +50,7 @@ from ..services.weibo_service import ( query_weibo_payment_status, query_weibo_point_history, refresh_weibo_account, + update_weibo_account_metadata, ) router = APIRouter(prefix="/api/weibo", tags=["微博微豆"]) @@ -81,6 +86,11 @@ def _account_out(account: WeiboAccount) -> WeiboAccountOut: return WeiboAccountOut.model_validate(account) +def _ensure_enabled(account: WeiboAccount) -> None: + if not account.is_enabled: + raise HTTPException(status_code=409, detail="微博账号已停用,请先启用") + + def _refresh_or_http(account: WeiboAccount, db: Session): try: return refresh_weibo_account( @@ -128,6 +138,7 @@ def _record_audit_safely( def list_accounts( search: str = Query(""), bind_status: str = Query(""), + enabled: bool | None = Query(None), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=200), db: Session = Depends(get_db), @@ -146,8 +157,11 @@ def list_accounts( WeiboAccount.nickname.ilike(pattern), WeiboAccount.game_role_name.ilike(pattern), WeiboAccount.tag.ilike(pattern), + WeiboAccount.remark.ilike(pattern), ) ) + if isinstance(enabled, bool): + query = query.filter(WeiboAccount.is_enabled.is_(enabled)) bind_value = _query_str(bind_status) if bind_value == "unbound": query = query.filter(WeiboAccount.is_bound.is_(False)) @@ -175,6 +189,95 @@ def list_accounts( } +@router.get("/accounts/{account_id}", response_model=WeiboAccountOut) +def get_account( + account_id: int, + db: Session = Depends(get_db), + current: User = Depends(get_current_user), +): + """查看一个微博账号的本地资料。""" + + _require_weibo_perm(current, "weibo:view") + return _account_out(_get_account(db, account_id)) + + +@router.put("/accounts/{account_id}", response_model=WeiboAccountOut) +def update_account( + account_id: int, + req: WeiboAccountUpdate, + db: Session = Depends(get_db), + current: User = Depends(get_current_user), +): + """更新账号的人工标签和备注。""" + + _require_weibo_perm(current, "weibo:manage") + account = _get_account(db, account_id) + payload = req.model_dump(exclude_unset=True) + changes = update_weibo_account_metadata( + db, + account, + tag=payload.get("tag"), + remark=payload.get("remark"), + ) + if changes: + _record_audit_safely( + db, + current, + action="weibo:account:update", + target=f"weibo_account:{account.id}", + detail={"account_id": account.id, "uid": account.uid, "changes": changes}, + ) + return _account_out(account) + + +@router.post("/accounts/{account_id}/disable", response_model=WeiboAccountOut) +def disable_account( + account_id: int, + req: WeiboAccountDisableRequest, + db: Session = Depends(get_db), + current: User = Depends(get_current_user), +): + """停用账号,保留 CK 和历史资料。""" + + _require_weibo_perm(current, "weibo:manage") + account = _get_account(db, account_id) + disable_weibo_account(db, account, reason=req.reason) + _record_audit_safely( + db, + current, + action="weibo:account:disable", + target=f"weibo_account:{account.id}", + detail={ + "account_id": account.id, + "uid": account.uid, + "reason": account.disabled_reason, + }, + ) + return _account_out(account) + + +@router.post("/accounts/{account_id}/enable", response_model=WeiboAccountOut) +def enable_account( + account_id: int, + db: Session = Depends(get_db), + current: User = Depends(get_current_user), +): + """重新检测 CK 与支付风险,通过后启用账号。""" + + _require_weibo_perm(current, "weibo:manage") + account = _get_account(db, account_id) + _refresh_or_http(account, db) + enable_weibo_account(db, account) + _record_audit_safely( + db, + current, + action="weibo:account:enable", + target=f"weibo_account:{account.id}", + detail={"account_id": account.id, "uid": account.uid}, + ) + return _account_out(account) + + @router.post("/accounts/import-cookies", response_model=WeiboImportResult) def import_cookies( req: WeiboCookieImport, @@ -231,6 +334,7 @@ def refresh_account( _require_weibo_perm(current, "weibo:view") account = _get_account(db, account_id) + _ensure_enabled(account) _refresh_or_http(account, db) _record_audit_safely( db, @@ -252,6 +356,7 @@ def create_bind_qrcodes( _require_weibo_perm(current, "weibo:bind") account = _get_account(db, account_id) + _ensure_enabled(account) snapshot = _refresh_or_http(account, db) if snapshot.is_bound and not snapshot.can_rebind: detail = ( @@ -314,6 +419,7 @@ def list_mall_goods( _require_weibo_perm(current, "weibo:view") account = _get_account(db, account_id) + _ensure_enabled(account) try: data = query_weibo_mall( db, @@ -366,6 +472,7 @@ def exchange_goods( if not req.confirm: raise HTTPException(status_code=400, detail="请先确认兑换商品") account = _get_account(db, account_id) + _ensure_enabled(account) try: result = exchange_weibo_goods( db, @@ -423,6 +530,7 @@ def list_exchange_records( _require_weibo_perm(current, "weibo:view") account = _get_account(db, account_id) + _ensure_enabled(account) try: data = query_weibo_exchange_records( db, @@ -471,6 +579,7 @@ def list_point_history( _require_weibo_perm(current, "weibo:view") account = _get_account(db, account_id) + _ensure_enabled(account) try: data = query_weibo_point_history( db, @@ -520,6 +629,7 @@ def create_recharge_order( _require_weibo_perm(current, "weibo:recharge") account = _get_account(db, account_id) + _ensure_enabled(account) try: order = create_weibo_recharge_order( db, @@ -567,6 +677,7 @@ def get_recharge_status( _require_weibo_perm(current, "weibo:recharge") account = _get_account(db, account_id) + _ensure_enabled(account) try: status = query_weibo_payment_status(db, account, pay_id=req.pay_id) except (WeiboError, ValueError) as exc: diff --git a/web/backend/schemas.py b/web/backend/schemas.py index e532c15..1d65be2 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -480,7 +480,11 @@ class WeiboAccountOut(BaseModel): uid: str nickname: str = "" tag: str = "" + remark: str = "" activity: str = "" + is_enabled: bool = True + disabled_at: datetime | None = None + disabled_reason: str = "" ck_check_status: str = "" ck_check_message: str = "" ck_checked_at: datetime | None = None @@ -496,6 +500,15 @@ class WeiboAccountOut(BaseModel): model_config = ConfigDict(from_attributes=True) +class WeiboAccountUpdate(BaseModel): + tag: str | None = Field(None, max_length=64) + remark: str | None = Field(None, max_length=256) + + +class WeiboAccountDisableRequest(BaseModel): + reason: str = Field("", max_length=256) + + class WeiboImportResult(BaseModel): success: bool message: str diff --git a/web/backend/services/weibo_service.py b/web/backend/services/weibo_service.py index 88515ac..0f89a5a 100644 --- a/web/backend/services/weibo_service.py +++ b/web/backend/services/weibo_service.py @@ -135,6 +135,64 @@ def refresh_weibo_account( return snapshot +def update_weibo_account_metadata( + db: Session, + account: WeiboAccount, + *, + tag: str | None = None, + remark: str | None = None, +) -> dict[str, dict[str, str]]: + """更新人工维护的账号标签与备注。""" + + changes: dict[str, dict[str, str]] = {} + if tag is not None: + clean_tag = tag.strip() + if clean_tag != (account.tag or ""): + changes["tag"] = {"old": account.tag or "", "new": clean_tag} + account.tag = clean_tag + if remark is not None: + clean_remark = remark.strip() + if clean_remark != (account.remark or ""): + changes["remark"] = { + "old": account.remark or "", + "new": clean_remark, + } + account.remark = clean_remark + if changes: + db.commit() + db.refresh(account) + return changes + + +def disable_weibo_account( + db: Session, + account: WeiboAccount, + *, + reason: str = "", +) -> None: + """停用账号;保留 CK 和资料,后续可重新启用。""" + + if not account.is_enabled: + return + account.is_enabled = False + account.disabled_at = _now() + account.disabled_reason = reason.strip()[:256] + db.commit() + db.refresh(account) + + +def enable_weibo_account(db: Session, account: WeiboAccount) -> None: + """启用已通过完整 CK 检测的账号。""" + + if account.is_enabled: + return + account.is_enabled = True + account.disabled_at = None + account.disabled_reason = "" + db.commit() + db.refresh(account) + + def generate_weibo_bind_schemes( account: WeiboAccount, bind_url: str, diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index 3fd43be..ce985f1 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -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; diff --git a/web/frontend/src/api/weibo.ts b/web/frontend/src/api/weibo.ts index 84f42d9..291f9a1 100644 --- a/web/frontend/src/api/weibo.ts +++ b/web/frontend/src/api/weibo.ts @@ -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(`/weibo/accounts/${accountId}`), importCookies: (text: string, tag = '') => api.post('/weibo/accounts/import-cookies', { text, tag, }), + updateAccount: ( + accountId: number, + data: { tag?: string; remark?: string }, + ) => + api.put( + `/weibo/accounts/${accountId}`, + data, + ), + disableAccount: (accountId: number, reason = '') => + api.post( + `/weibo/accounts/${accountId}/disable`, + { reason }, + ), + enableAccount: (accountId: number) => + api.post( + `/weibo/accounts/${accountId}/enable`, + undefined, + { timeout: 120000 }, + ), refreshAccount: (accountId: number) => api.post( `/weibo/accounts/${accountId}/refresh`, diff --git a/web/frontend/src/pages/WeiboAccountsPage.tsx b/web/frontend/src/pages/WeiboAccountsPage.tsx index 32cd693..eecc689 100644 --- a/web/frontend/src/pages/WeiboAccountsPage.tsx +++ b/web/frontend/src/pages/WeiboAccountsPage.tsx @@ -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(null); + const [mutatingId, setMutatingId] = useState(null); const [importOpen, setImportOpen] = useState(false); const [importing, setImporting] = useState(false); const [importErrors, setImportErrors] = useState([]); 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(null); + const [editSaving, setEditSaving] = useState(false); const [qrOpen, setQrOpen] = useState(false); const [qrAccount, setQrAccount] = useState(null); @@ -127,6 +137,12 @@ export default function WeiboAccountsPage() { const data: PaginatedResponse = 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() { UID {account.uid} + {account.remark && ( + + + {account.remark} + + + )} ), }, @@ -371,6 +475,24 @@ export default function WeiboAccountsPage() { ); }, }, + { + title: '启用状态', + dataIndex: 'is_enabled', + width: 100, + render: (value: boolean, account) => ( + + + {value ? '已启用' : '已停用'} + + + ), + }, { 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 ( - + + {canManage && ( + + )} + {canManage && + (account.is_enabled ? ( + void handleDisable(account)} + > + + + ) : ( + + ))} {canDelete && ( balance; @@ -600,15 +766,15 @@ export default function WeiboAccountsPage() { >
- 微博账号 + 微博外部 CK 账号 - CK 导入、微豆资料刷新和角色绑定二维码 + 外部 CK 导入、账号生命周期、微豆资料和角色绑定
} - placeholder="UID、昵称、角色、标签" + placeholder="UID、昵称、角色、标签、备注" value={search} onChange={(event) => { setSearch(event.target.value); @@ -630,6 +796,19 @@ export default function WeiboAccountsPage() { { value: 'rebindable', label: '可换绑' }, ]} /> + + + +