feat(weibo): 支持管理员强制转入工作台且不影响原使用人
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""允许超级管理员在不影响原使用人的情况下附加微博工作台。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20261112_0077"
|
||||
down_revision: str | None = "20261111_0076"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
TABLE = "weibo_workbench_accounts"
|
||||
OLD_UNIQUE = "uq_weibo_workbench_account_owner"
|
||||
NEW_UNIQUE = "uq_weibo_workbench_account_exclusive"
|
||||
CHECK_CONSTRAINT = "ck_weibo_workbench_exclusive_slot"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if not inspector.has_table(TABLE):
|
||||
return
|
||||
|
||||
columns = {column["name"] for column in inspector.get_columns(TABLE)}
|
||||
if "exclusive_slot" not in columns:
|
||||
op.add_column(
|
||||
TABLE,
|
||||
sa.Column(
|
||||
"exclusive_slot",
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
server_default="1",
|
||||
),
|
||||
)
|
||||
|
||||
constraints = {item["name"] for item in inspector.get_unique_constraints(TABLE)}
|
||||
if OLD_UNIQUE in constraints:
|
||||
op.drop_constraint(OLD_UNIQUE, TABLE, type_="unique")
|
||||
|
||||
# 迁移前超级管理员已有映射时,改为并行挂载,不能继续占用唯一槽位。
|
||||
op.execute(
|
||||
sa.text(
|
||||
"UPDATE weibo_workbench_accounts AS w "
|
||||
"JOIN users AS u ON u.id = w.user_id "
|
||||
"SET w.exclusive_slot = NULL "
|
||||
"WHERE u.role = 'super_admin'"
|
||||
)
|
||||
)
|
||||
|
||||
constraints = {
|
||||
item["name"] for item in sa.inspect(op.get_bind()).get_unique_constraints(TABLE)
|
||||
}
|
||||
if NEW_UNIQUE not in constraints:
|
||||
op.create_unique_constraint(
|
||||
NEW_UNIQUE,
|
||||
TABLE,
|
||||
["account_id", "exclusive_slot"],
|
||||
)
|
||||
checks = {
|
||||
item["name"] for item in sa.inspect(op.get_bind()).get_check_constraints(TABLE)
|
||||
}
|
||||
if CHECK_CONSTRAINT not in checks:
|
||||
op.create_check_constraint(
|
||||
CHECK_CONSTRAINT,
|
||||
TABLE,
|
||||
"exclusive_slot IS NULL OR exclusive_slot = 1",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if not inspector.has_table(TABLE):
|
||||
return
|
||||
|
||||
constraints = {item["name"] for item in inspector.get_unique_constraints(TABLE)}
|
||||
if NEW_UNIQUE in constraints:
|
||||
op.drop_constraint(NEW_UNIQUE, TABLE, type_="unique")
|
||||
checks = {item["name"] for item in inspector.get_check_constraints(TABLE)}
|
||||
if CHECK_CONSTRAINT in checks:
|
||||
op.drop_constraint(CHECK_CONSTRAINT, TABLE, type_="check")
|
||||
|
||||
# 旧结构无法表达多个并行映射,降级时只保留普通用户唯一占用。
|
||||
op.execute(sa.text(f"DELETE FROM {TABLE} WHERE exclusive_slot IS NULL"))
|
||||
|
||||
columns = {column["name"] for column in inspector.get_columns(TABLE)}
|
||||
if "exclusive_slot" in columns:
|
||||
op.drop_column(TABLE, "exclusive_slot")
|
||||
|
||||
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 sa.inspect(op.get_bind()).get_unique_constraints(TABLE)
|
||||
}
|
||||
if OLD_UNIQUE not in constraints:
|
||||
op.create_unique_constraint(
|
||||
OLD_UNIQUE,
|
||||
TABLE,
|
||||
["account_id"],
|
||||
)
|
||||
+10
-1
@@ -8,6 +8,7 @@ from sqlalchemy import (
|
||||
JSON,
|
||||
BigInteger,
|
||||
Boolean,
|
||||
CheckConstraint,
|
||||
Computed,
|
||||
ForeignKey,
|
||||
Index,
|
||||
@@ -989,6 +990,9 @@ class WeiboWorkbenchAccount(Base):
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
# 普通用户固定为 1;超级管理员的并行工作台映射为 NULL。
|
||||
# MySQL 唯一索引允许多个 NULL,因此管理员附加映射不会挤掉原使用人。
|
||||
exclusive_slot: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
@@ -999,7 +1003,12 @@ class WeiboWorkbenchAccount(Base):
|
||||
),
|
||||
UniqueConstraint(
|
||||
"account_id",
|
||||
name="uq_weibo_workbench_account_owner",
|
||||
"exclusive_slot",
|
||||
name="uq_weibo_workbench_account_exclusive",
|
||||
),
|
||||
CheckConstraint(
|
||||
"exclusive_slot IS NULL OR exclusive_slot = 1",
|
||||
name="ck_weibo_workbench_exclusive_slot",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
+130
-33
@@ -313,8 +313,18 @@ def list_workbench_pool(
|
||||
"""查看可加入当前用户工作台、且尚未加入的账号池账号。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:workbench")
|
||||
occupied = db.query(WeiboWorkbenchAccount.account_id)
|
||||
pool_query = db.query(WeiboAccount).filter(WeiboAccount.id.not_in(occupied))
|
||||
if current.role == "super_admin":
|
||||
selected = db.query(WeiboWorkbenchAccount.account_id).filter(
|
||||
WeiboWorkbenchAccount.user_id == current.id
|
||||
)
|
||||
pool_query = db.query(WeiboAccount).filter(WeiboAccount.id.not_in(selected))
|
||||
else:
|
||||
exclusively_occupied = db.query(WeiboWorkbenchAccount.account_id).filter(
|
||||
WeiboWorkbenchAccount.exclusive_slot.is_not(None)
|
||||
)
|
||||
pool_query = db.query(WeiboAccount).filter(
|
||||
WeiboAccount.id.not_in(exclusively_occupied)
|
||||
)
|
||||
if not _can_view_all_weibo_accounts(current):
|
||||
pool_query = pool_query.filter(WeiboAccount.assigned_to == current.id)
|
||||
query = _apply_account_filters(
|
||||
@@ -339,6 +349,11 @@ def import_workbench_accounts_by_text(
|
||||
"""按 UID/用户名/角色批量解析账号并加入当前用户工作台。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:workbench")
|
||||
if req.force_add_occupied and current.role != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="仅超级管理员可以强制转入其他人的工作台账号",
|
||||
)
|
||||
identifiers: list[str] = []
|
||||
for token in re.split(r"[\s,,;;]+", req.text.strip()):
|
||||
value = token.strip()
|
||||
@@ -373,18 +388,42 @@ def import_workbench_accounts_by_text(
|
||||
by_nickname = grouped("nickname")
|
||||
by_role = grouped("game_role_name")
|
||||
account_ids = [account.id for account in accounts]
|
||||
occupancy = {
|
||||
account_id: user_id
|
||||
for account_id, user_id in db.query(
|
||||
memberships: dict[int, list[tuple[int, str, int | None]]] = {}
|
||||
for account_id, user_id, username, exclusive_slot in (
|
||||
db.query(
|
||||
WeiboWorkbenchAccount.account_id,
|
||||
WeiboWorkbenchAccount.user_id,
|
||||
User.username,
|
||||
WeiboWorkbenchAccount.exclusive_slot,
|
||||
)
|
||||
.outerjoin(User, User.id == WeiboWorkbenchAccount.user_id)
|
||||
.filter(WeiboWorkbenchAccount.account_id.in_(account_ids))
|
||||
.all()
|
||||
):
|
||||
memberships.setdefault(account_id, []).append(
|
||||
(user_id, username or "", exclusive_slot)
|
||||
)
|
||||
current_member_ids = {
|
||||
account_id
|
||||
for account_id, rows in memberships.items()
|
||||
if any(user_id == current.id for user_id, _username, _slot in rows)
|
||||
}
|
||||
exclusive_owners = {
|
||||
account_id: next(
|
||||
(
|
||||
(user_id, username)
|
||||
for user_id, username, exclusive_slot in rows
|
||||
if exclusive_slot is not None
|
||||
),
|
||||
None,
|
||||
)
|
||||
for account_id, rows in memberships.items()
|
||||
}
|
||||
|
||||
items: list[WeiboWorkbenchBatchImportItem] = []
|
||||
to_add: list[WeiboAccount] = []
|
||||
to_add: list[tuple[WeiboAccount, int | None]] = []
|
||||
forced_accounts: list[WeiboAccount] = []
|
||||
planned_accounts: list[WeiboAccount] = []
|
||||
planned_ids: set[int] = set()
|
||||
for identifier in identifiers:
|
||||
candidates = by_uid.get(identifier) or by_nickname.get(identifier) or []
|
||||
@@ -410,13 +449,19 @@ def import_workbench_accounts_by_text(
|
||||
continue
|
||||
|
||||
account = candidates[0]
|
||||
exclusive_owner = exclusive_owners.get(account.id)
|
||||
base = {
|
||||
"identifier": identifier,
|
||||
"account_id": account.id,
|
||||
"uid": account.uid,
|
||||
"nickname": account.nickname,
|
||||
"occupied_by_user_id": (
|
||||
exclusive_owner[0] if exclusive_owner is not None else None
|
||||
),
|
||||
"occupied_by_username": (
|
||||
exclusive_owner[1] if exclusive_owner is not None else ""
|
||||
),
|
||||
}
|
||||
occupant = occupancy.get(account.id)
|
||||
if not account.is_enabled:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
@@ -425,22 +470,6 @@ def import_workbench_accounts_by_text(
|
||||
**base,
|
||||
)
|
||||
)
|
||||
elif occupant is not None and occupant != current.id:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="unavailable",
|
||||
message="账号已在其他人的工作台中使用",
|
||||
**base,
|
||||
)
|
||||
)
|
||||
elif occupant == current.id:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="already_added",
|
||||
message="账号已在本工作台",
|
||||
**base,
|
||||
)
|
||||
)
|
||||
elif account.id in planned_ids:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
@@ -449,9 +478,51 @@ def import_workbench_accounts_by_text(
|
||||
**base,
|
||||
)
|
||||
)
|
||||
elif account.id in current_member_ids:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="already_added",
|
||||
message="账号已在本工作台",
|
||||
**base,
|
||||
)
|
||||
)
|
||||
elif exclusive_owner is not None and exclusive_owner[0] != current.id:
|
||||
if req.force_add_occupied:
|
||||
planned_ids.add(account.id)
|
||||
to_add.append((account, None))
|
||||
forced_accounts.append(account)
|
||||
planned_accounts.append(account)
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="forced_added",
|
||||
message=(
|
||||
f"已强制转入,{exclusive_owner[1] or '原使用人'}"
|
||||
"的工作台保持不变"
|
||||
),
|
||||
**base,
|
||||
)
|
||||
)
|
||||
continue
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="unavailable",
|
||||
message=(
|
||||
f"账号已在 {exclusive_owner[1]} 的工作台中使用"
|
||||
if exclusive_owner[1]
|
||||
else "账号已在其他人的工作台中使用"
|
||||
),
|
||||
**base,
|
||||
)
|
||||
)
|
||||
else:
|
||||
planned_ids.add(account.id)
|
||||
to_add.append(account)
|
||||
to_add.append(
|
||||
(
|
||||
account,
|
||||
None if current.role == "super_admin" else 1,
|
||||
)
|
||||
)
|
||||
planned_accounts.append(account)
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="added",
|
||||
@@ -460,11 +531,23 @@ def import_workbench_accounts_by_text(
|
||||
)
|
||||
)
|
||||
|
||||
forced_details = [
|
||||
{
|
||||
"account_id": account.id,
|
||||
"existing_owner_user_id": exclusive_owners[account.id][0],
|
||||
"existing_owner_username": exclusive_owners[account.id][1],
|
||||
}
|
||||
for account in forced_accounts
|
||||
]
|
||||
try:
|
||||
db.add_all(
|
||||
[
|
||||
WeiboWorkbenchAccount(user_id=current.id, account_id=account.id)
|
||||
for account in to_add
|
||||
WeiboWorkbenchAccount(
|
||||
user_id=current.id,
|
||||
account_id=account.id,
|
||||
exclusive_slot=exclusive_slot,
|
||||
)
|
||||
for account, exclusive_slot in to_add
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
@@ -474,24 +557,31 @@ def import_workbench_accounts_by_text(
|
||||
status_code=409,
|
||||
detail="部分账号刚被其他工作台占用,请刷新后重试",
|
||||
) from exc
|
||||
if to_add:
|
||||
if planned_accounts:
|
||||
_record_audit_safely(
|
||||
db,
|
||||
current,
|
||||
action="weibo:workbench:batch_import",
|
||||
target=f"weibo_user:{current.id}",
|
||||
detail={"account_ids": [account.id for account in to_add]},
|
||||
detail={
|
||||
"account_ids": [account.id for account in planned_accounts],
|
||||
"added_account_ids": [account.id for account, _slot in to_add],
|
||||
"forced_added_account_ids": [account.id for account in forced_accounts],
|
||||
"existing_owners": forced_details,
|
||||
},
|
||||
)
|
||||
|
||||
added = sum(item.status == "added" for item in items)
|
||||
forced_added = sum(item.status == "forced_added" for item in items)
|
||||
already_added = sum(item.status == "already_added" for item in items)
|
||||
failed = len(items) - added - already_added
|
||||
failed = len(items) - added - forced_added - already_added
|
||||
return WeiboWorkbenchBatchImportOut(
|
||||
requested=len(identifiers),
|
||||
added=added,
|
||||
forced_added=forced_added,
|
||||
already_added=already_added,
|
||||
failed=failed,
|
||||
account_ids=[account.id for account in to_add],
|
||||
account_ids=[account.id for account in planned_accounts],
|
||||
items=items,
|
||||
)
|
||||
|
||||
@@ -567,11 +657,12 @@ def add_workbench_accounts(
|
||||
for (account_id,) in db.query(WeiboWorkbenchAccount.account_id)
|
||||
.filter(
|
||||
WeiboWorkbenchAccount.user_id != current.id,
|
||||
WeiboWorkbenchAccount.exclusive_slot.is_not(None),
|
||||
WeiboWorkbenchAccount.account_id.in_(account_ids),
|
||||
)
|
||||
.all()
|
||||
}
|
||||
if taken:
|
||||
if taken and current.role != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="部分账号已在其他人的工作台中使用,请先让原使用人移出",
|
||||
@@ -587,7 +678,11 @@ def add_workbench_accounts(
|
||||
}
|
||||
db.add_all(
|
||||
[
|
||||
WeiboWorkbenchAccount(user_id=current.id, account_id=account_id)
|
||||
WeiboWorkbenchAccount(
|
||||
user_id=current.id,
|
||||
account_id=account_id,
|
||||
exclusive_slot=(None if current.role == "super_admin" else 1),
|
||||
)
|
||||
for account_id in account_ids
|
||||
if account_id not in selected_ids
|
||||
]
|
||||
@@ -763,12 +858,14 @@ def batch_assign_accounts(
|
||||
previous_assignments = {account.id: account.assigned_to for account in accounts}
|
||||
if target is None:
|
||||
db.query(WeiboWorkbenchAccount).filter(
|
||||
WeiboWorkbenchAccount.account_id.in_(actual_ids)
|
||||
WeiboWorkbenchAccount.account_id.in_(actual_ids),
|
||||
WeiboWorkbenchAccount.exclusive_slot.is_not(None),
|
||||
).delete(synchronize_session=False)
|
||||
else:
|
||||
db.query(WeiboWorkbenchAccount).filter(
|
||||
WeiboWorkbenchAccount.account_id.in_(actual_ids),
|
||||
WeiboWorkbenchAccount.user_id != target.id,
|
||||
WeiboWorkbenchAccount.exclusive_slot.is_not(None),
|
||||
).delete(synchronize_session=False)
|
||||
for account in accounts:
|
||||
account.assigned_to = target.id if target else None
|
||||
|
||||
@@ -542,6 +542,7 @@ class WeiboWorkbenchTransferToCollabOut(BaseModel):
|
||||
|
||||
class WeiboWorkbenchBatchImportRequest(BaseModel):
|
||||
text: str = Field(..., min_length=1, max_length=100_000)
|
||||
force_add_occupied: bool = False
|
||||
|
||||
|
||||
class WeiboWorkbenchBatchImportItem(BaseModel):
|
||||
@@ -551,11 +552,14 @@ class WeiboWorkbenchBatchImportItem(BaseModel):
|
||||
account_id: int | None = None
|
||||
uid: str = ""
|
||||
nickname: str = ""
|
||||
occupied_by_user_id: int | None = None
|
||||
occupied_by_username: str = ""
|
||||
|
||||
|
||||
class WeiboWorkbenchBatchImportOut(BaseModel):
|
||||
requested: int
|
||||
added: int
|
||||
forced_added: int = 0
|
||||
already_added: int
|
||||
failed: int
|
||||
account_ids: list[int] = Field(default_factory=list)
|
||||
|
||||
@@ -135,10 +135,13 @@ def import_weibo_cookies(
|
||||
continue
|
||||
owner_id = (
|
||||
db.query(WeiboWorkbenchAccount.user_id)
|
||||
.filter(WeiboWorkbenchAccount.account_id == account.id)
|
||||
.scalar()
|
||||
.filter(
|
||||
WeiboWorkbenchAccount.account_id == account.id,
|
||||
WeiboWorkbenchAccount.exclusive_slot.is_not(None),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if owner_id is not None and owner_id != current_user_id:
|
||||
if owner_id is not None and owner_id[0] != current_user_id:
|
||||
errors.append(
|
||||
WeiboImportIssue(
|
||||
line=line_no,
|
||||
|
||||
@@ -826,6 +826,7 @@ export interface WeiboWorkbenchBatchImportItem {
|
||||
identifier: string;
|
||||
status:
|
||||
| 'added'
|
||||
| 'forced_added'
|
||||
| 'already_added'
|
||||
| 'not_found'
|
||||
| 'ambiguous'
|
||||
@@ -836,11 +837,14 @@ export interface WeiboWorkbenchBatchImportItem {
|
||||
account_id: number | null;
|
||||
uid: string;
|
||||
nickname: string;
|
||||
occupied_by_user_id: number | null;
|
||||
occupied_by_username: string;
|
||||
}
|
||||
|
||||
export interface WeiboWorkbenchBatchImportResult {
|
||||
requested: number;
|
||||
added: number;
|
||||
forced_added: number;
|
||||
already_added: number;
|
||||
failed: number;
|
||||
account_ids: number[];
|
||||
|
||||
@@ -124,10 +124,10 @@ export const weiboApi = {
|
||||
'/weibo/workbench/accounts',
|
||||
{ account_ids: accountIds },
|
||||
),
|
||||
batchImportWorkbenchAccounts: (text: string) =>
|
||||
batchImportWorkbenchAccounts: (text: string, forceAddOccupied = false) =>
|
||||
api.post<WeiboWorkbenchBatchImportResult, WeiboWorkbenchBatchImportResult>(
|
||||
'/weibo/workbench/accounts/batch-import',
|
||||
{ text },
|
||||
{ text, force_add_occupied: forceAddOccupied },
|
||||
),
|
||||
transferWorkbenchToCollab: (accountId: number) =>
|
||||
api.post<
|
||||
|
||||
@@ -83,6 +83,7 @@ const WORKBENCH_IMPORT_STATUS: Record<
|
||||
{ label: string; color: string }
|
||||
> = {
|
||||
added: { label: '已导入', color: 'success' },
|
||||
forced_added: { label: '强制转入', color: 'orange' },
|
||||
already_added: { label: '已存在', color: 'blue' },
|
||||
not_found: { label: '未找到', color: 'error' },
|
||||
ambiguous: { label: '重名', color: 'warning' },
|
||||
@@ -161,8 +162,9 @@ export default function WeiboAccountsPage({
|
||||
mode?: WeiboAccountsPageMode;
|
||||
}) {
|
||||
const { token } = theme.useToken();
|
||||
const { can } = usePermissions();
|
||||
const { can, user } = usePermissions();
|
||||
const navigate = useNavigate();
|
||||
const isSuperAdmin = user?.role === 'super_admin';
|
||||
const isWorkbench = mode === 'workbench';
|
||||
const canImport = can('weibo:import');
|
||||
const canManage = can('weibo:manage');
|
||||
@@ -206,6 +208,7 @@ export default function WeiboAccountsPage({
|
||||
'paste',
|
||||
);
|
||||
const [batchImportText, setBatchImportText] = useState('');
|
||||
const [forceAddOccupied, setForceAddOccupied] = useState(false);
|
||||
const [batchImportLoading, setBatchImportLoading] = useState(false);
|
||||
const [batchImportResult, setBatchImportResult] =
|
||||
useState<WeiboWorkbenchBatchImportResult | null>(null);
|
||||
@@ -400,6 +403,7 @@ export default function WeiboAccountsPage({
|
||||
setPoolSelectedIds([]);
|
||||
setWorkbenchImportTab('paste');
|
||||
setBatchImportText('');
|
||||
setForceAddOccupied(isSuperAdmin);
|
||||
setBatchImportResult(null);
|
||||
setPoolOpen(true);
|
||||
void loadWorkbenchPool(1, poolPageSize, '');
|
||||
@@ -414,17 +418,24 @@ export default function WeiboAccountsPage({
|
||||
setBatchImportLoading(true);
|
||||
setBatchImportResult(null);
|
||||
try {
|
||||
const result = await weiboApi.batchImportWorkbenchAccounts(text);
|
||||
const result = await weiboApi.batchImportWorkbenchAccounts(
|
||||
text,
|
||||
isSuperAdmin && forceAddOccupied,
|
||||
);
|
||||
setBatchImportResult(result);
|
||||
await loadAccounts();
|
||||
await loadKnownTags();
|
||||
const forceAddSummary =
|
||||
result.forced_added > 0
|
||||
? `,强制转入 ${result.forced_added} 个`
|
||||
: '';
|
||||
if (result.failed > 0) {
|
||||
message.warning(
|
||||
`批量导入完成:新增 ${result.added} 个,已存在 ${result.already_added} 个,失败 ${result.failed} 个`,
|
||||
`批量导入完成:新增 ${result.added} 个${forceAddSummary},已存在 ${result.already_added} 个,失败 ${result.failed} 个`,
|
||||
);
|
||||
} else {
|
||||
message.success(
|
||||
`批量导入完成:新增 ${result.added} 个,已存在 ${result.already_added} 个`,
|
||||
`批量导入完成:新增 ${result.added} 个${forceAddSummary},已存在 ${result.already_added} 个`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -2589,6 +2600,16 @@ export default function WeiboAccountsPage({
|
||||
title="支持多行、空格、逗号分隔"
|
||||
description="优先按 UID 精确匹配;也支持微博用户名和角色名。重名时请改用 UID。"
|
||||
/>
|
||||
{isSuperAdmin && (
|
||||
<Checkbox
|
||||
checked={forceAddOccupied}
|
||||
onChange={(event) =>
|
||||
setForceAddOccupied(event.target.checked)
|
||||
}
|
||||
>
|
||||
其他人的工作台占用时,强制转入当前工作台(不影响原使用人)
|
||||
</Checkbox>
|
||||
)}
|
||||
<TextArea
|
||||
rows={8}
|
||||
value={batchImportText}
|
||||
@@ -2606,7 +2627,7 @@ export default function WeiboAccountsPage({
|
||||
<Alert
|
||||
type={batchImportResult.failed > 0 ? 'warning' : 'success'}
|
||||
showIcon
|
||||
title={`解析 ${batchImportResult.requested} 项:新增 ${batchImportResult.added},已存在 ${batchImportResult.already_added},失败 ${batchImportResult.failed}`}
|
||||
title={`解析 ${batchImportResult.requested} 项:新增 ${batchImportResult.added},强制转入 ${batchImportResult.forced_added},已存在 ${batchImportResult.already_added},失败 ${batchImportResult.failed}`}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user