feat(weibo): 增加外部 CK 账号 CRUD 与生命周期

This commit is contained in:
yml2213
2026-11-08 11:07:23 +08:00
parent dd3508d614
commit a0afa430ec
11 changed files with 611 additions and 10 deletions
@@ -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)
+11
View File
@@ -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=""
)
+2
View File
@@ -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",
+111
View File
@@ -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:
+13
View File
@@ -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
+58
View File
@@ -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,
+4
View File
@@ -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;
+22
View File
@@ -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`,
+218 -9
View File
@@ -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()}
>
创建充值订单