feat(weibo): 增加工作台独占与快捷操作

This commit is contained in:
yml2213
2026-11-08 16:39:32 +08:00
parent d566518478
commit c8b62e5267
10 changed files with 341 additions and 29 deletions
@@ -17,7 +17,7 @@ status: implemented
- **账号管理**:全局微博账号池,负责 CK 导入、生命周期、标签、批量操作和资料维护。
- **微博工作台**:每个用户自己的账号子集,只展示自己导入的账号,用于绑定、充值、兑换和日常操作。
工作台采用斗鱼模式:**账号实体仍然全局唯一,工作台只保存用户与账号的映射**。同一个账号可以被多人分别加入自己的工作台,工作台增删互不影响;底层 CK 刷新后,所有引用该账号的工作台看到同一份最新资料。
工作台采用斗鱼模式:**账号实体仍然全局唯一,工作台只保存用户与账号的映射**。同一个全局账号同一时间只能被一个用户加入工作台;当前使用人移出后,其他人才可以加入。
## 2. 数据模型
@@ -33,6 +33,7 @@ weibo_workbench_accounts
created_at
UNIQUE(user_id, account_id)
UNIQUE(account_id) -- 同一账号同时只能属于一个工作台
```
删除账号或用户时,对应工作台映射级联删除,不保留孤儿记录。
@@ -57,7 +58,7 @@ UNIQUE(user_id, account_id)
| GET | `/api/weibo/accounts` | `weibo:view` | 全局账号池分页列表 |
| GET | `/api/weibo/workbench/accounts` | `weibo:workbench` | 当前用户工作台账号分页列表 |
| GET | `/api/weibo/workbench/pool` | `weibo:workbench` | 可加入当前工作台且尚未加入的账号 |
| POST | `/api/weibo/workbench/accounts` | `weibo:workbench` | 批量加入工作台,幂等 |
| POST | `/api/weibo/workbench/accounts` | `weibo:workbench` | 批量加入工作台;已被其他用户占用时返回 409 |
| POST | `/api/weibo/workbench/accounts/remove` | `weibo:workbench` | 批量移出工作台,不影响账号池和其他用户 |
| POST | `/api/weibo/accounts/batch-tag` | `weibo:manage` | 账号管理页批量设置标签 |
@@ -86,12 +87,23 @@ UNIQUE(user_id, account_id)
- 复用账号能力,但列表只显示当前用户工作台映射。
- 「导入账号」从账号池选择账号加入。
- 「导入外部 CK」会先写入全局账号池,再自动加入当前工作台。
- 从账号池只显示当前未被任何用户占用的账号。
- 支持单个/批量移出工作台,不删除账号池数据。
- 绑定二维码、充值、兑换、功能查询继续复用现有账号接口。
## 6. 数据隔离边界
- 工作台列表、账号池候选和增删接口都按当前登录用户过滤。
- 工作台列表和增删接口都按当前登录用户过滤。
- 账号池候选排除所有已被其他用户占用的账号。
- 加入/移出只操作 `weibo_workbench_accounts`,不修改账号归属或 CK。
- 账号池仍由全局权限控制;客服可以看到账号管理入口,但不能删除账号。
- 外部单协作继续引用全局账号 ID,不受个人工作台选择影响。
## 7. CK 重复导入
同一微博 UID 在全局池中只允许存在一条账号记录。
- 默认重复导入:不覆盖现有 CK,返回 `skipped=1`。
- 勾选「重复账号时更新 CK」:覆盖现有账号 CK 和最新资料,但**不新建账号**。
- 若账号已被其他人的工作台占用,其他用户不能更新 CK,必须由当前使用人操作。
- 无论是否更新,全局账号记录始终只有一条。
+1 -1
View File
@@ -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 = "20261108_0072"
HEAD_REVISION = "20261108_0073"
# 迁移新增、但不声明在模型里的复合查询索引。
EXTRA_INDEXES = (
+89 -5
View File
@@ -30,9 +30,11 @@ from web.backend.routers.weibo import (
enable_account,
get_account,
import_cookies,
list_account_tags,
list_accounts,
list_workbench_accounts,
list_workbench_pool,
list_workbench_tags,
refresh_account,
remove_workbench_accounts,
update_account,
@@ -138,23 +140,42 @@ def stub_client(monkeypatch):
monkeypatch.setattr(weibo_service, "WeiboClient", StubWeiboClient)
def test_import_creates_encrypted_account_and_is_idempotent(db, current_user):
def test_import_creates_once_and_updates_existing_only_when_requested(db, current_user):
first = import_cookies(
WeiboCookieImport(text="SCF=a; SUB=b", tag="运营"),
db=db,
current=current_user,
)
second = import_cookies(
duplicate = import_cookies(
WeiboCookieImport(text="SCF=c; SUB=d", tag="新标签"),
db=db,
current=current_user,
)
assert first.count == 1
assert second.count == 1
assert duplicate.count == 0
assert duplicate.skipped == 1
assert duplicate.accounts == []
assert db.query(WeiboAccount).count() == 1
account = db.query(WeiboAccount).one()
assert account.uid == "4014000001"
assert account.cookie == "SCF=a; SUB=b"
assert account.tag == "运营"
updated = import_cookies(
WeiboCookieImport(
text="SCF=c; SUB=d",
tag="新标签",
update_existing=True,
),
db=db,
current=current_user,
)
assert updated.count == 1
assert updated.skipped == 0
assert db.query(WeiboAccount).count() == 1
account = db.query(WeiboAccount).one()
assert account.cookie == "SCF=c; SUB=d"
assert account.tag == "新标签"
assert account.ck_check_status == "valid"
@@ -162,7 +183,7 @@ def test_import_creates_encrypted_account_and_is_idempotent(db, current_user):
assert raw_cookie.startswith("enc:v1:")
assert "SUB=d" not in raw_cookie
dumped = second.accounts[0].model_dump()
dumped = updated.accounts[0].model_dump()
assert dumped["uid"] == "4014000001"
assert dumped["is_bound"] is False
assert dumped["weidou_balance"] == 100
@@ -198,6 +219,50 @@ def test_import_reports_payment_access_failure(db, current_user):
assert db.query(WeiboAccount).count() == 0
def test_occupied_workbench_account_cannot_be_updated_by_another_user(db, current_user):
imported_out = import_cookies(
WeiboCookieImport(text="SCF=a; SUB=owner"),
db=db,
current=current_user,
).accounts[0]
imported = db.query(WeiboAccount).filter(WeiboAccount.id == imported_out.id).one()
original_cookie = imported.cookie
owner = User(
username="weibo-owner",
password_hash="hash",
role="support",
is_active=True,
)
other = User(
username="weibo-other",
password_hash="hash",
role="support",
is_active=True,
)
db.add_all([owner, other])
db.commit()
add_workbench_accounts(
WeiboWorkbenchAccountsRequest(account_ids=[imported.id]),
db=db,
current=owner,
)
result = import_cookies(
WeiboCookieImport(
text="SCF=z; SUB=other",
update_existing=True,
),
db=db,
current=other,
)
assert result.count == 0
assert len(result.errors) == 1
assert "其他人的工作台占用" in result.errors[0].message
db.refresh(imported)
assert imported.cookie == original_cookie
def test_refresh_marks_payment_access_failure(db, current_user):
account_out = import_cookies(
WeiboCookieImport(text="SCF=a; SUB=b", tag=""),
@@ -347,6 +412,15 @@ def test_weibo_workbench_is_isolated_per_user(db, current_user):
db=db,
current=user_a,
)
pool_before = list_workbench_pool(page=1, page_size=20, db=db, current=user_a)
assert [item.uid for item in pool_before["items"]] == [second.uid]
with pytest.raises(HTTPException) as exc_info:
add_workbench_accounts(
WeiboWorkbenchAccountsRequest(account_ids=[first.id]),
db=db,
current=user_b,
)
assert exc_info.value.status_code == 409
add_workbench_accounts(
WeiboWorkbenchAccountsRequest(account_ids=[second.id]),
db=db,
@@ -357,10 +431,18 @@ def test_weibo_workbench_is_isolated_per_user(db, current_user):
page_b = list_workbench_accounts(page=1, page_size=20, db=db, current=user_b)
assert [item.uid for item in page_a["items"]] == [first.uid]
assert [item.uid for item in page_b["items"]] == [second.uid]
assert (
list_workbench_accounts(tag="A", page=1, page_size=20, db=db, current=user_a)[
"total"
]
== 1
)
assert list_workbench_tags(db=db, current=user_a)["tags"] == ["A"]
assert list_account_tags(db=db, current=current_user)["tags"] == ["A", "B"]
assert db.query(WeiboWorkbenchAccount).count() == 2
pool_a = list_workbench_pool(page=1, page_size=20, db=db, current=user_a)
assert [item.uid for item in pool_a["items"]] == [second.uid]
assert pool_a["items"] == []
remove_workbench_accounts(
WeiboWorkbenchAccountsRequest(account_ids=[first.id]),
@@ -375,6 +457,8 @@ def test_weibo_workbench_is_isolated_per_user(db, current_user):
list_workbench_accounts(page=1, page_size=20, db=db, current=user_b)["total"]
== 1
)
pool_b = list_workbench_pool(page=1, page_size=20, db=db, current=user_b)
assert [item.uid for item in pool_b["items"]] == [first.uid]
def test_batch_tag_updates_selected_accounts(db, current_user):
@@ -0,0 +1,54 @@
"""限制微博账号同时只能属于一个个人工作台。"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20261108_0073"
down_revision: str | None = "20261108_0072"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
inspector = sa.inspect(op.get_bind())
if not inspector.has_table("weibo_workbench_accounts"):
return
# 历史若已有重复占用,保留最早加入的一条,其余释放。
op.execute(
sa.text(
"DELETE newer FROM weibo_workbench_accounts AS newer "
"JOIN weibo_workbench_accounts AS older "
"ON newer.account_id = older.account_id AND newer.id > older.id"
)
)
constraints = {
item["name"]
for item in inspector.get_unique_constraints("weibo_workbench_accounts")
}
if "uq_weibo_workbench_account_owner" not in constraints:
op.create_unique_constraint(
"uq_weibo_workbench_account_owner",
"weibo_workbench_accounts",
["account_id"],
)
def downgrade() -> None:
inspector = sa.inspect(op.get_bind())
if not inspector.has_table("weibo_workbench_accounts"):
return
constraints = {
item["name"]
for item in inspector.get_unique_constraints("weibo_workbench_accounts")
}
if "uq_weibo_workbench_account_owner" in constraints:
op.drop_constraint(
"uq_weibo_workbench_account_owner",
"weibo_workbench_accounts",
type_="unique",
)
+4
View File
@@ -983,6 +983,10 @@ class WeiboWorkbenchAccount(Base):
"account_id",
name="uq_weibo_workbench_account",
),
UniqueConstraint(
"account_id",
name="uq_weibo_workbench_account_owner",
),
)
+82 -7
View File
@@ -7,6 +7,7 @@ import os
from fastapi import APIRouter, Depends, HTTPException, Query
from loguru import logger
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, defer
from core.weibo import WeiboAuthError, WeiboError, WeiboExchangeUncertainError
@@ -94,6 +95,7 @@ def _apply_account_filters(
query,
*,
search: str,
tag: str,
bind_status: str,
enabled: bool | None,
):
@@ -109,6 +111,9 @@ def _apply_account_filters(
WeiboAccount.remark.ilike(pattern),
)
)
tag_value = _query_str(tag)
if tag_value:
query = query.filter(WeiboAccount.tag == tag_value)
if isinstance(enabled, bool):
query = query.filter(WeiboAccount.is_enabled.is_(enabled))
bind_value = _query_str(bind_status)
@@ -192,6 +197,7 @@ def _record_audit_safely(
@router.get("/accounts")
def list_accounts(
search: str = Query(""),
tag: str = Query(""),
bind_status: str = Query(""),
enabled: bool | None = Query(None),
page: int = Query(1, ge=1),
@@ -205,6 +211,7 @@ def list_accounts(
query = _apply_account_filters(
db.query(WeiboAccount),
search=search,
tag=tag,
bind_status=bind_status,
enabled=enabled,
)
@@ -214,6 +221,7 @@ def list_accounts(
@router.get("/workbench/accounts")
def list_workbench_accounts(
search: str = Query(""),
tag: str = Query(""),
bind_status: str = Query(""),
enabled: bool | None = Query(None),
page: int = Query(1, ge=1),
@@ -235,6 +243,7 @@ def list_workbench_accounts(
query = _apply_account_filters(
query,
search=search,
tag=tag,
bind_status=bind_status,
enabled=enabled,
)
@@ -244,6 +253,7 @@ def list_workbench_accounts(
@router.get("/workbench/pool")
def list_workbench_pool(
search: str = Query(""),
tag: str = Query(""),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=200),
db: Session = Depends(get_db),
@@ -252,18 +262,42 @@ def list_workbench_pool(
"""查看可加入当前用户工作台、且尚未加入的账号池账号。"""
_require_weibo_perm(current, "weibo:workbench")
selected = db.query(WeiboWorkbenchAccount.account_id).filter(
WeiboWorkbenchAccount.user_id == current.id
)
occupied = db.query(WeiboWorkbenchAccount.account_id)
query = _apply_account_filters(
db.query(WeiboAccount).filter(WeiboAccount.id.not_in(selected)),
db.query(WeiboAccount).filter(WeiboAccount.id.not_in(occupied)),
search=search,
tag=tag,
bind_status="",
enabled=True,
)
return _paginated_accounts(query, page=page, page_size=page_size)
@router.get("/workbench/tags")
def list_workbench_tags(
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""返回当前用户工作台中已有的标签。"""
_require_weibo_perm(current, "weibo:workbench")
rows = (
db.query(WeiboAccount.tag)
.join(
WeiboWorkbenchAccount,
WeiboWorkbenchAccount.account_id == WeiboAccount.id,
)
.filter(
WeiboWorkbenchAccount.user_id == current.id,
WeiboAccount.tag != "",
)
.distinct()
.order_by(WeiboAccount.tag.asc())
.all()
)
return {"tags": [tag for (tag,) in rows if tag]}
@router.post("/workbench/accounts")
def add_workbench_accounts(
req: WeiboWorkbenchAccountsRequest,
@@ -287,6 +321,20 @@ def add_workbench_accounts(
missing = sorted(set(account_ids) - existing_ids)
if missing:
raise HTTPException(status_code=404, detail=f"账号不存在: {missing}")
taken = {
account_id
for (account_id,) in db.query(WeiboWorkbenchAccount.account_id)
.filter(
WeiboWorkbenchAccount.user_id != current.id,
WeiboWorkbenchAccount.account_id.in_(account_ids),
)
.all()
}
if taken:
raise HTTPException(
status_code=409,
detail="部分账号已在其他人的工作台中使用,请先让原使用人移出",
)
selected_ids = {
account_id
for (account_id,) in db.query(WeiboWorkbenchAccount.account_id)
@@ -303,7 +351,14 @@ def add_workbench_accounts(
if account_id not in selected_ids
]
)
db.commit()
try:
db.commit()
except IntegrityError as exc:
db.rollback()
raise HTTPException(
status_code=409,
detail="账号已被其他工作台占用,请刷新后重试",
) from exc
_record_audit_safely(
db,
current,
@@ -379,6 +434,24 @@ def batch_tag_accounts(
return {"updated": len(accounts), "tag": tag, "success": True}
@router.get("/accounts/tags")
def list_account_tags(
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""返回全局账号池已有的标签。"""
_require_weibo_perm(current, "weibo:view")
rows = (
db.query(WeiboAccount.tag)
.filter(WeiboAccount.tag != "")
.distinct()
.order_by(WeiboAccount.tag.asc())
.all()
)
return {"tags": [tag for (tag,) in rows if tag]}
@router.get("/accounts/{account_id}", response_model=WeiboAccountOut)
def get_account(
account_id: int,
@@ -482,6 +555,8 @@ def import_cookies(
req.text,
req.tag,
WEIBO_DEFAULT_ACTIVITY,
req.update_existing,
current.id,
)
typed_errors = [
WeiboImportError(line=item.line, message=item.message) for item in errors
@@ -499,13 +574,13 @@ def import_cookies(
"errors": len(errors),
},
)
message = f"成功导入/更新 {len(accounts)} 个微博账号"
message = f"成功导入 {len(accounts)} 个微博账号"
if errors:
message += f",失败 {len(errors)} 条"
if skipped:
message += f",跳过 {skipped} 条重复"
return WeiboImportResult(
success=bool(accounts),
success=bool(accounts) or (skipped > 0 and not errors),
message=message,
count=len(accounts),
skipped=skipped,
+1
View File
@@ -468,6 +468,7 @@ class WeiboCookieImport(BaseModel):
text: str = Field(..., min_length=1)
tag: str = Field("", max_length=64)
update_existing: bool = False
class WeiboImportError(BaseModel):
+20 -1
View File
@@ -18,7 +18,7 @@ from core.weibo import (
normalize_weibo_cookie,
)
from ..models import WeiboAccount
from ..models import WeiboAccount, WeiboWorkbenchAccount
@dataclass(frozen=True, slots=True)
@@ -61,6 +61,8 @@ def import_weibo_cookies(
text: str,
tag: str = "",
activity: str = "",
update_existing: bool = False,
current_user_id: int | None = None,
) -> tuple[list[WeiboAccount], list[WeiboImportIssue], int]:
"""逐行验证并导入微博 CK。
@@ -95,6 +97,23 @@ def import_weibo_cookies(
account = (
db.query(WeiboAccount).filter(WeiboAccount.uid == snapshot.uid).first()
)
if account is not None and update_existing and current_user_id is not None:
owner_id = (
db.query(WeiboWorkbenchAccount.user_id)
.filter(WeiboWorkbenchAccount.account_id == account.id)
.scalar()
)
if owner_id is not None and owner_id != current_user_id:
errors.append(
WeiboImportIssue(
line=line_no,
message="该账号已被其他人的工作台占用,不能更新 CK",
)
)
continue
if account is not None and not update_existing:
skipped += 1
continue
if account is None:
account = WeiboAccount(uid=snapshot.uid, cookie=cookie)
db.add(account)
+9 -1
View File
@@ -19,6 +19,7 @@ import type {
export const weiboApi = {
listAccounts: (params: {
search?: string;
tag?: string;
bind_status?: string;
enabled?: boolean;
page: number;
@@ -30,10 +31,11 @@ export const weiboApi = {
),
getAccount: (accountId: number) =>
api.get<WeiboAccountItem, WeiboAccountItem>(`/weibo/accounts/${accountId}`),
importCookies: (text: string, tag = '') =>
importCookies: (text: string, tag = '', updateExisting = false) =>
api.post<WeiboImportResult, WeiboImportResult>('/weibo/accounts/import-cookies', {
text,
tag,
update_existing: updateExisting,
}),
updateAccount: (
accountId: number,
@@ -67,8 +69,13 @@ export const weiboApi = {
'/weibo/accounts/batch-tag',
{ ids, tag },
),
accountTags: () =>
api.get<{ tags: string[] }, { tags: string[] }>('/weibo/accounts/tags'),
workbenchTags: () =>
api.get<{ tags: string[] }, { tags: string[] }>('/weibo/workbench/tags'),
listWorkbenchAccounts: (params: {
search?: string;
tag?: string;
bind_status?: string;
enabled?: boolean;
page: number;
@@ -80,6 +87,7 @@ export const weiboApi = {
),
listWorkbenchPool: (params: {
search?: string;
tag?: string;
page: number;
page_size: number;
}) =>
+66 -11
View File
@@ -3,6 +3,7 @@ import {
Alert,
AutoComplete,
Button,
Checkbox,
Descriptions,
Drawer,
Empty,
@@ -137,6 +138,8 @@ export default function WeiboAccountsPage({
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [search, setSearch] = useState('');
const [tagFilter, setTagFilter] = useState('');
const [knownTags, setKnownTags] = useState<string[]>([]);
const [bindFilter, setBindFilter] = useState('');
const [enabledFilter, setEnabledFilter] = useState('');
const [loading, setLoading] = useState(false);
@@ -161,7 +164,11 @@ export default function WeiboAccountsPage({
const [importOpen, setImportOpen] = useState(false);
const [importing, setImporting] = useState(false);
const [importErrors, setImportErrors] = useState<WeiboImportError[]>([]);
const [form] = Form.useForm<{ text: string; tag?: string }>();
const [form] = Form.useForm<{
text: string;
tag?: string;
update_existing?: boolean;
}>();
const [editForm] = Form.useForm<{ tag?: string; remark?: string }>();
const [editOpen, setEditOpen] = useState(false);
const [editAccount, setEditAccount] = useState<WeiboAccountItem | null>(null);
@@ -199,6 +206,7 @@ export default function WeiboAccountsPage({
try {
const params = {
search: search.trim() || undefined,
tag: tagFilter || undefined,
bind_status: bindFilter || undefined,
enabled:
enabledFilter === 'enabled'
@@ -222,17 +230,36 @@ export default function WeiboAccountsPage({
} finally {
setLoading(false);
}
}, [bindFilter, enabledFilter, isWorkbench, page, pageSize, search]);
}, [bindFilter, enabledFilter, isWorkbench, page, pageSize, search, tagFilter]);
const loadKnownTags = useCallback(async () => {
try {
const result = isWorkbench
? await weiboApi.workbenchTags()
: await weiboApi.accountTags();
setKnownTags(result.tags);
} catch {
// 标签字典失败不影响账号列表;筛选选项退化为空。
}
}, [isWorkbench]);
useEffect(() => {
void loadAccounts();
}, [loadAccounts]);
useEffect(() => {
void loadKnownTags();
}, [loadKnownTags]);
const handleImport = async () => {
const values = await form.validateFields();
setImporting(true);
try {
const result = await weiboApi.importCookies(values.text, values.tag || '');
const result = await weiboApi.importCookies(
values.text,
values.tag || '',
Boolean(values.update_existing),
);
setImportErrors(result.errors);
let workbenchError = '';
if (isWorkbench && result.accounts.length > 0) {
@@ -253,11 +280,15 @@ export default function WeiboAccountsPage({
message.success(result.message);
}
await loadAccounts();
await loadKnownTags();
}
if (result.errors.length === 0 && result.count > 0) {
setImportOpen(false);
form.resetFields();
} else if (result.count === 0) {
} else if (result.skipped > 0) {
message.warning(result.message);
await loadAccounts();
} else {
message.error(result.message);
}
} catch (error) {
@@ -351,6 +382,7 @@ export default function WeiboAccountsPage({
setBatchTagValue('');
setSelectedIds([]);
await loadAccounts();
await loadKnownTags();
message.success(`已为 ${result.updated} 个账号设置标签`);
} catch (error) {
message.error(getErrorMessage(error));
@@ -819,7 +851,14 @@ export default function WeiboAccountsPage({
width: isWorkbench ? 90 : 120,
align: 'center',
onHeaderCell: centerHeader,
render: (value: string) => value || <Text type="secondary">-</Text>,
render: (value: string) =>
value ? (
<Tag color="blue" style={{ marginInlineEnd: 0 }}>
{value}
</Tag>
) : (
<Text type="secondary">-</Text>
),
},
{
title: 'CK',
@@ -1176,11 +1215,8 @@ export default function WeiboAccountsPage({
workbenchSelectedGoods.price >
(mallResult?.weidou_balance ?? featureAccount?.weidou_balance ?? 0);
const tagOptions = useMemo(
() =>
[...new Set(items.map((item) => item.tag.trim()).filter(Boolean))].map(
(tag) => ({ value: tag }),
),
[items],
() => knownTags.map((tag) => ({ value: tag })),
[knownTags],
);
return (
@@ -1216,6 +1252,17 @@ export default function WeiboAccountsPage({
}}
style={{ width: 240 }}
/>
<Select
allowClear
value={tagFilter || undefined}
onChange={(value) => {
setTagFilter(value || '');
setPage(1);
}}
placeholder="全部标签"
style={{ width: 130 }}
options={knownTags.map((tag) => ({ value: tag, label: tag }))}
/>
<Select
value={bindFilter}
onChange={(value) => {
@@ -1257,6 +1304,8 @@ export default function WeiboAccountsPage({
disabled={!canImport}
onClick={() => {
setImportErrors([]);
form.resetFields();
form.setFieldsValue({ update_existing: false });
setImportOpen(true);
}}
>
@@ -1736,7 +1785,8 @@ export default function WeiboAccountsPage({
title: '标签',
dataIndex: 'tag',
width: 120,
render: (value: string) => value || '-',
render: (value: string) =>
value ? <Tag color="blue">{value}</Tag> : '-',
},
{
title: 'CK',
@@ -1787,6 +1837,11 @@ export default function WeiboAccountsPage({
<Form.Item name="tag" label="标签">
<Input placeholder="可选" maxLength={64} />
</Form.Item>
<Form.Item name="update_existing" valuePropName="checked">
<Checkbox>
重复账号时更新 CK(仍只有一条全局账号,不新建)
</Checkbox>
</Form.Item>
</Form>
{importErrors.length > 0 && (
<Alert