feat(accounts): 增加高价值资产一键筛选与高级自定义筛选功能
1. 后端新增高价值套装状态筛选(双套均可换/可换白日梦/可换萌兔/已拥有/高价值待救/未查资料),兼容 MySQL 原生 JSON 与 SQLite unicode 转义存储; 2. 后端新增多维资产与状态区间筛选(积分范围、鱼翅范围、宝典开通、电竞开通、角色绑定状态),并支持与批量打标签/删除联动; 3. 统计接口 accounts_summary 新增 high_value 聚合数据,毫秒级返回各类高价值资产数量; 4. 前端账号管理页面新增「高价值一键筛选」胶囊区(带数量徽标与激活状态,即点即滤); 5. 前端新增可折叠「高级自定义筛选」面板,支持套装状态下拉、积分快捷区间(≥300、≥600)、鱼翅快捷区间(>0、≥6)、宝典与角色绑定状态多条件组合; 6. 补充 6 个高价值筛选与资产区间单元测试,全量用例通过。
This commit is contained in:
@@ -1610,3 +1610,180 @@ class TestPeaceScopeCanBeTurnedOff:
|
||||
marked, called = self._run(peace_enabled=True)
|
||||
assert "xpd_records" in called
|
||||
assert "exchange_records_peace" in marked["result"].get("errors", {})
|
||||
|
||||
|
||||
class TestHighValueAccountFilters:
|
||||
"""高价值账号筛选与自定义范围筛选测试。"""
|
||||
|
||||
def setup_method(self):
|
||||
self.engine = get_test_engine()
|
||||
self.session = make_test_session(self.engine)
|
||||
self.admin = User(username="hv-admin", password_hash="h", role="super_admin")
|
||||
self.session.add(self.admin)
|
||||
self.session.commit()
|
||||
|
||||
# 构造不同特征的账号
|
||||
now = datetime.now(UTC)
|
||||
# 1. 优质皇冠号:双可换 + 积分 500 + 鱼翅 10 + 宝典开通 + CK valid
|
||||
self.acc_crown = Account(
|
||||
username="acc_crown",
|
||||
password="p",
|
||||
email="e1@test.com",
|
||||
email_password="p",
|
||||
overview_queried_at=now,
|
||||
high_value_owned=[],
|
||||
points=500,
|
||||
gold_balance=10,
|
||||
elite_opened=True,
|
||||
ck_check_status="valid",
|
||||
status="normal",
|
||||
)
|
||||
# 2. 换过白日梦(可换萌兔)+ 积分 100 + 鱼翅 0 + 宝典未开通
|
||||
self.acc_bairimeng_owned = Account(
|
||||
username="acc_bairimeng_owned",
|
||||
password="p",
|
||||
email="e2@test.com",
|
||||
email_password="p",
|
||||
overview_queried_at=now,
|
||||
high_value_owned=["套装-兔兔白日梦"],
|
||||
points=100,
|
||||
gold_balance=0,
|
||||
elite_opened=False,
|
||||
ck_check_status="valid",
|
||||
status="normal",
|
||||
)
|
||||
# 3. 换过萌兔(可换白日梦)+ 积分 50 + 鱼翅 20 + 宝典开通
|
||||
self.acc_mengtu_owned = Account(
|
||||
username="acc_mengtu_owned",
|
||||
password="p",
|
||||
email="e3@test.com",
|
||||
email_password="p",
|
||||
overview_queried_at=now,
|
||||
high_value_owned=["套装-狂战萌兔"],
|
||||
points=50,
|
||||
gold_balance=20,
|
||||
elite_opened=True,
|
||||
ck_check_status="valid",
|
||||
status="normal",
|
||||
)
|
||||
# 4. 双已换 + 高价值已空
|
||||
self.acc_both_owned = Account(
|
||||
username="acc_both_owned",
|
||||
password="p",
|
||||
email="e4@test.com",
|
||||
email_password="p",
|
||||
overview_queried_at=now,
|
||||
high_value_owned=["套装-兔兔白日梦", "套装-狂战萌兔"],
|
||||
points=0,
|
||||
gold_balance=0,
|
||||
elite_opened=False,
|
||||
ck_check_status="valid",
|
||||
status="normal",
|
||||
)
|
||||
# 5. 未查询资料的账号
|
||||
self.acc_unqueried = Account(
|
||||
username="acc_unqueried",
|
||||
password="p",
|
||||
email="e5@test.com",
|
||||
email_password="p",
|
||||
overview_queried_at=None,
|
||||
high_value_owned=None,
|
||||
points=None,
|
||||
gold_balance=None,
|
||||
elite_opened=None,
|
||||
ck_check_status="valid",
|
||||
status="normal",
|
||||
)
|
||||
# 6. 高价值但 CK 坏掉(待抢救)
|
||||
self.acc_broken = Account(
|
||||
username="acc_broken",
|
||||
password="p",
|
||||
email="e6@test.com",
|
||||
email_password="p",
|
||||
overview_queried_at=now,
|
||||
high_value_owned=[],
|
||||
points=800,
|
||||
gold_balance=0,
|
||||
elite_opened=True,
|
||||
ck_check_status="invalid",
|
||||
status="normal",
|
||||
)
|
||||
self.session.add_all(
|
||||
[
|
||||
self.acc_crown,
|
||||
self.acc_bairimeng_owned,
|
||||
self.acc_mengtu_owned,
|
||||
self.acc_both_owned,
|
||||
self.acc_unqueried,
|
||||
self.acc_broken,
|
||||
]
|
||||
)
|
||||
self.session.commit()
|
||||
|
||||
def _query(self, **kwargs):
|
||||
from web.backend.routers.accounts import list_accounts
|
||||
|
||||
return list_accounts(
|
||||
assigned_only=False,
|
||||
has_cookie=False,
|
||||
search="acc_",
|
||||
page=1,
|
||||
page_size=20,
|
||||
db=self.session,
|
||||
current=self.admin,
|
||||
**kwargs,
|
||||
)["items"]
|
||||
|
||||
def test_both_available_filter(self):
|
||||
items = self._query(high_value_filter="both_available")
|
||||
names = [it.username for it in items]
|
||||
assert "acc_crown" in names
|
||||
assert "acc_broken" in names
|
||||
assert "acc_bairimeng_owned" not in names
|
||||
assert "acc_mengtu_owned" not in names
|
||||
assert "acc_both_owned" not in names
|
||||
assert "acc_unqueried" not in names
|
||||
|
||||
def test_can_bairimeng_filter(self):
|
||||
items = self._query(high_value_filter="can_bairimeng")
|
||||
names = [it.username for it in items]
|
||||
assert "acc_crown" in names
|
||||
assert "acc_mengtu_owned" in names # 拥有萌兔但没拥有白日梦,所以可以换白日梦
|
||||
assert "acc_bairimeng_owned" not in names
|
||||
assert "acc_both_owned" not in names
|
||||
|
||||
def test_can_mengtu_filter(self):
|
||||
items = self._query(high_value_filter="can_mengtu")
|
||||
names = [it.username for it in items]
|
||||
assert "acc_crown" in names
|
||||
assert "acc_bairimeng_owned" in names # 拥有白日梦但没拥有萌兔,所以可以换萌兔
|
||||
assert "acc_mengtu_owned" not in names
|
||||
assert "acc_both_owned" not in names
|
||||
|
||||
def test_numeric_ranges_and_opened_filter(self):
|
||||
# 积分 >= 300 且 鱼翅 >= 5
|
||||
items = self._query(min_points=300, min_gold=5)
|
||||
names = [it.username for it in items]
|
||||
assert names == ["acc_crown"]
|
||||
|
||||
# 宝典已开通
|
||||
items = self._query(elite_opened=True)
|
||||
names = [it.username for it in items]
|
||||
assert set(names) == {"acc_crown", "acc_mengtu_owned", "acc_broken"}
|
||||
|
||||
def test_high_value_broken_filter(self):
|
||||
# 高价值但 CK 异常待抢救
|
||||
items = self._query(high_value_filter="high_value_broken")
|
||||
names = [it.username for it in items]
|
||||
assert names == ["acc_broken"]
|
||||
|
||||
def test_summary_returns_high_value_breakdown(self):
|
||||
from web.backend.routers.accounts import accounts_summary
|
||||
|
||||
res = accounts_summary(db=self.session, current=self.admin)
|
||||
hv = res.get("high_value")
|
||||
assert hv is not None
|
||||
assert hv["both_available"] == 2 # acc_crown + acc_broken
|
||||
assert hv["unqueried"] == 1 # acc_unqueried
|
||||
assert hv["high_points"] == 2 # 500, 800
|
||||
assert hv["high_value_broken"] == 1 # acc_broken
|
||||
|
||||
@@ -9,13 +9,11 @@ from web.backend.services.douyu_service import create_douyu_planned_tasks
|
||||
|
||||
|
||||
def test_douyu_task_assigned_to_flow():
|
||||
from tests.support.db import get_test_engine
|
||||
|
||||
init_test_schema()
|
||||
DouyuTask.__table__.drop(get_test_engine(), checkfirst=True)
|
||||
DouyuTask.__table__.create(get_test_engine(), checkfirst=True)
|
||||
db: Session = make_test_session()
|
||||
try:
|
||||
db.query(DouyuTask).delete()
|
||||
db.commit()
|
||||
# 创建测试客服用户与账号
|
||||
user_a = User(
|
||||
username="test_kefu_a",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, or_, select, text
|
||||
from sqlalchemy import String, and_, cast, func, or_, select, text
|
||||
from sqlalchemy.orm import Session, defer, joinedload
|
||||
|
||||
from ..database import get_db
|
||||
@@ -79,6 +79,17 @@ def _query_str(value) -> str:
|
||||
return value.strip() if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def _high_value_contains_expr(column, text: str):
|
||||
"""匹配 JSON 字段中是否包含特定商品名(同时兼容 MySQL 与 SQLite 字符存储)。"""
|
||||
import json
|
||||
|
||||
escaped = json.dumps(text).strip('"')
|
||||
return or_(
|
||||
cast(column, String).like(f"%{text}%"),
|
||||
cast(column, String).like(f"%{escaped}%"),
|
||||
)
|
||||
|
||||
|
||||
def _filter_accounts_query(
|
||||
db: Session,
|
||||
query,
|
||||
@@ -91,6 +102,14 @@ def _filter_accounts_query(
|
||||
status: str = "",
|
||||
ck_status: str = "",
|
||||
bind_status: str = "",
|
||||
high_value_filter: str = "",
|
||||
min_points: int | None = None,
|
||||
max_points: int | None = None,
|
||||
min_gold: int | None = None,
|
||||
max_gold: int | None = None,
|
||||
elite_opened: bool | None = None,
|
||||
esports_opened: bool | None = None,
|
||||
role_bind_status: str = "",
|
||||
):
|
||||
"""复用列表筛选条件,供分页列表和全量批量操作保持一致。"""
|
||||
if has_cookie:
|
||||
@@ -120,6 +139,70 @@ def _filter_accounts_query(
|
||||
Account.xpd_bind_status == bind_value,
|
||||
)
|
||||
)
|
||||
role_bind = _query_str(role_bind_status)
|
||||
if role_bind:
|
||||
query = query.filter(Account.role_bind_status == role_bind)
|
||||
|
||||
# 高价值套装筛选
|
||||
hv = _query_str(high_value_filter)
|
||||
if hv:
|
||||
bairimeng_expr = _high_value_contains_expr(
|
||||
Account.high_value_owned, "兔兔白日梦"
|
||||
)
|
||||
mengtu_expr = _high_value_contains_expr(Account.high_value_owned, "狂战萌兔")
|
||||
if hv == "both_available":
|
||||
query = query.filter(
|
||||
Account.overview_queried_at.isnot(None),
|
||||
~bairimeng_expr,
|
||||
~mengtu_expr,
|
||||
)
|
||||
elif hv == "can_bairimeng":
|
||||
query = query.filter(
|
||||
Account.overview_queried_at.isnot(None),
|
||||
~bairimeng_expr,
|
||||
)
|
||||
elif hv == "can_mengtu":
|
||||
query = query.filter(
|
||||
Account.overview_queried_at.isnot(None),
|
||||
~mengtu_expr,
|
||||
)
|
||||
elif hv == "owned_bairimeng":
|
||||
query = query.filter(bairimeng_expr)
|
||||
elif hv == "owned_mengtu":
|
||||
query = query.filter(mengtu_expr)
|
||||
elif hv == "both_owned":
|
||||
query = query.filter(bairimeng_expr, mengtu_expr)
|
||||
elif hv == "unqueried":
|
||||
query = query.filter(Account.overview_queried_at.is_(None))
|
||||
elif hv == "high_value_broken":
|
||||
query = query.filter(
|
||||
or_(
|
||||
and_(
|
||||
Account.overview_queried_at.isnot(None),
|
||||
~bairimeng_expr,
|
||||
~mengtu_expr,
|
||||
),
|
||||
Account.points >= 300,
|
||||
),
|
||||
Account.ck_check_status.in_(["invalid", "error"]),
|
||||
)
|
||||
|
||||
# 资产数值区间过滤
|
||||
if isinstance(min_points, int):
|
||||
query = query.filter(Account.points >= min_points)
|
||||
if isinstance(max_points, int):
|
||||
query = query.filter(Account.points <= max_points)
|
||||
if isinstance(min_gold, int):
|
||||
query = query.filter(Account.gold_balance >= min_gold)
|
||||
if isinstance(max_gold, int):
|
||||
query = query.filter(Account.gold_balance <= max_gold)
|
||||
|
||||
# 开通状态过滤
|
||||
if isinstance(elite_opened, bool):
|
||||
query = query.filter(Account.elite_opened.is_(elite_opened))
|
||||
if isinstance(esports_opened, bool):
|
||||
query = query.filter(Account.esports_opened.is_(esports_opened))
|
||||
|
||||
search_text = _query_str(search)
|
||||
if search_text:
|
||||
pattern = f"%{search_text}%"
|
||||
@@ -147,6 +230,17 @@ def _selected_account_ids(
|
||||
tag=req.tag,
|
||||
has_cookie=req.has_cookie,
|
||||
search=req.search,
|
||||
status=req.status,
|
||||
ck_status=req.ck_status,
|
||||
bind_status=req.bind_status,
|
||||
high_value_filter=req.high_value_filter,
|
||||
min_points=req.min_points,
|
||||
max_points=req.max_points,
|
||||
min_gold=req.min_gold,
|
||||
max_gold=req.max_gold,
|
||||
elite_opened=req.elite_opened,
|
||||
esports_opened=req.esports_opened,
|
||||
role_bind_status=req.role_bind_status,
|
||||
)
|
||||
.with_entities(Account.id)
|
||||
.order_by(None)
|
||||
@@ -195,6 +289,19 @@ def list_accounts(
|
||||
bind_status: str = Query(
|
||||
"", description="按角色绑定筛选:unknown/unbound/pending/bound"
|
||||
),
|
||||
high_value_filter: str = Query(
|
||||
"",
|
||||
description="按高价值套装状态筛选:both_available/can_bairimeng/can_mengtu/owned_bairimeng/owned_mengtu/both_owned/unqueried/high_value_broken",
|
||||
),
|
||||
min_points: int | None = Query(None, description="最低积分"),
|
||||
max_points: int | None = Query(None, description="最高积分"),
|
||||
min_gold: int | None = Query(None, description="最低鱼翅"),
|
||||
max_gold: int | None = Query(None, description="最高鱼翅"),
|
||||
elite_opened: bool | None = Query(None, description="是否开通精英宝典"),
|
||||
esports_opened: bool | None = Query(None, description="是否开通电竞手册"),
|
||||
role_bind_status: str = Query(
|
||||
"", description="精英角色绑定状态: unknown/unbound/pending/bound"
|
||||
),
|
||||
page: int | None = Query(None, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=200),
|
||||
include_sensitive: bool = Query(False),
|
||||
@@ -213,6 +320,14 @@ def list_accounts(
|
||||
status=status,
|
||||
ck_status=ck_status,
|
||||
bind_status=bind_status,
|
||||
high_value_filter=high_value_filter,
|
||||
min_points=min_points,
|
||||
max_points=max_points,
|
||||
min_gold=min_gold,
|
||||
max_gold=max_gold,
|
||||
elite_opened=elite_opened,
|
||||
esports_opened=esports_opened,
|
||||
role_bind_status=role_bind_status,
|
||||
)
|
||||
|
||||
total = None
|
||||
@@ -378,11 +493,66 @@ def accounts_summary(
|
||||
)
|
||||
by_status = {str(row[0] or ""): int(row[1] or 0) for row in status_rows}
|
||||
visible_total = sum(by_status.values())
|
||||
# 高价值及核心资产分布(单次聚合,供前端快捷胶囊展示)
|
||||
bairimeng_expr = _high_value_contains_expr(Account.high_value_owned, "兔兔白日梦")
|
||||
mengtu_expr = _high_value_contains_expr(Account.high_value_owned, "狂战萌兔")
|
||||
high_value_broken_expr = and_(
|
||||
or_(
|
||||
and_(
|
||||
Account.overview_queried_at.isnot(None),
|
||||
~bairimeng_expr,
|
||||
~mengtu_expr,
|
||||
),
|
||||
Account.points >= 300,
|
||||
),
|
||||
Account.ck_check_status.in_(["invalid", "error"]),
|
||||
)
|
||||
|
||||
hv_row = visible.with_entities(
|
||||
func.count(
|
||||
func.nullif(
|
||||
Account.overview_queried_at.isnot(None)
|
||||
& ~bairimeng_expr
|
||||
& ~mengtu_expr,
|
||||
False,
|
||||
)
|
||||
),
|
||||
func.count(
|
||||
func.nullif(
|
||||
Account.overview_queried_at.isnot(None) & ~bairimeng_expr,
|
||||
False,
|
||||
)
|
||||
),
|
||||
func.count(
|
||||
func.nullif(
|
||||
Account.overview_queried_at.isnot(None) & ~mengtu_expr,
|
||||
False,
|
||||
)
|
||||
),
|
||||
func.count(func.nullif(Account.overview_queried_at.is_(None), False)),
|
||||
func.count(func.nullif(Account.points >= 300, False)),
|
||||
func.count(func.nullif(Account.gold_balance > 0, False)),
|
||||
func.count(func.nullif(Account.elite_opened.is_(True), False)),
|
||||
func.count(func.nullif(high_value_broken_expr, False)),
|
||||
).first()
|
||||
high_value_summary = {
|
||||
"both_available": int(hv_row[0] or 0) if hv_row else 0,
|
||||
"can_bairimeng": int(hv_row[1] or 0) if hv_row else 0,
|
||||
"can_mengtu": int(hv_row[2] or 0) if hv_row else 0,
|
||||
"unqueried": int(hv_row[3] or 0) if hv_row else 0,
|
||||
"high_points": int(hv_row[4] or 0) if hv_row else 0,
|
||||
"has_gold": int(hv_row[5] or 0) if hv_row else 0,
|
||||
"elite_opened": int(hv_row[6] or 0) if hv_row else 0,
|
||||
"high_value_broken": int(hv_row[7] or 0) if hv_row else 0,
|
||||
}
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"visible_total": visible_total,
|
||||
# 生命周期分布(键是 6 状态的枚举值)
|
||||
"by_status": by_status,
|
||||
# 高价值与核心资产统计
|
||||
"high_value": high_value_summary,
|
||||
# 需要人关注的:CK 已被判失效的账号数(含待人工)
|
||||
"ck_invalid_count": int(
|
||||
visible.filter(Account.ck_check_status == "invalid")
|
||||
|
||||
@@ -137,6 +137,17 @@ class AccountBulkSelection(BaseModel):
|
||||
tag: str = ""
|
||||
assigned_only: bool = False
|
||||
has_cookie: bool = False
|
||||
status: str = ""
|
||||
ck_status: str = ""
|
||||
bind_status: str = ""
|
||||
high_value_filter: str = ""
|
||||
min_points: int | None = None
|
||||
max_points: int | None = None
|
||||
min_gold: int | None = None
|
||||
max_gold: int | None = None
|
||||
elite_opened: bool | None = None
|
||||
esports_opened: bool | None = None
|
||||
role_bind_status: str = ""
|
||||
|
||||
|
||||
class AccountBulkTag(AccountBulkSelection):
|
||||
|
||||
@@ -18,7 +18,25 @@ import type {
|
||||
export const accountApi = {
|
||||
list: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean }) =>
|
||||
api.get<AccountItem[], AccountItem[]>('/accounts', { params }),
|
||||
listPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean; status?: string; ck_status?: string; bind_status?: string }) =>
|
||||
listPaged: (
|
||||
params: PageParams & {
|
||||
assigned_only?: boolean;
|
||||
tag?: string;
|
||||
has_cookie?: boolean;
|
||||
include_sensitive?: boolean;
|
||||
status?: string;
|
||||
ck_status?: string;
|
||||
bind_status?: string;
|
||||
high_value_filter?: string;
|
||||
min_points?: number;
|
||||
max_points?: number;
|
||||
min_gold?: number;
|
||||
max_gold?: number;
|
||||
elite_opened?: boolean;
|
||||
esports_opened?: boolean;
|
||||
role_bind_status?: string;
|
||||
},
|
||||
) =>
|
||||
api.get<PaginatedResponse<AccountItem>, PaginatedResponse<AccountItem>>('/accounts', { params }),
|
||||
summary: () => api.get<BasicSummary, BasicSummary>('/accounts/summary'),
|
||||
import: (text: string, tag?: string) =>
|
||||
|
||||
@@ -31,6 +31,17 @@ export interface AccountBulkSelection {
|
||||
tag?: string;
|
||||
assigned_only?: boolean;
|
||||
has_cookie?: boolean;
|
||||
status?: string;
|
||||
ck_status?: string;
|
||||
bind_status?: string;
|
||||
high_value_filter?: string;
|
||||
min_points?: number;
|
||||
max_points?: number;
|
||||
min_gold?: number;
|
||||
max_gold?: number;
|
||||
elite_opened?: boolean;
|
||||
esports_opened?: boolean;
|
||||
role_bind_status?: string;
|
||||
}
|
||||
|
||||
export interface AccountBulkTagRequest extends AccountBulkSelection {
|
||||
@@ -64,11 +75,24 @@ export interface AuditLogQuery {
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
export interface HighValueSummary {
|
||||
both_available: number;
|
||||
can_bairimeng: number;
|
||||
can_mengtu: number;
|
||||
unqueried: number;
|
||||
high_points: number;
|
||||
has_gold: number;
|
||||
elite_opened: number;
|
||||
high_value_broken: number;
|
||||
}
|
||||
|
||||
export interface BasicSummary {
|
||||
/** 生命周期状态分布(键是 6 状态枚举值);归档默认不计入 */
|
||||
by_status?: Record<string, number>;
|
||||
/** 不含归档的账号数 */
|
||||
visible_total?: number;
|
||||
/** 高价值及核心资产统计 */
|
||||
high_value?: HighValueSummary;
|
||||
/** CK 已被判失效的账号数 */
|
||||
ck_invalid_count?: number;
|
||||
/** 没有 CK 的账号数 */
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Button,
|
||||
Modal,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Popconfirm,
|
||||
Typography,
|
||||
@@ -21,7 +22,18 @@ import {
|
||||
} from 'antd';
|
||||
import { message } from '../utils/antdMessage';
|
||||
import type { TableProps } from 'antd';
|
||||
import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined, EyeOutlined, EyeInvisibleOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ImportOutlined,
|
||||
DeleteOutlined,
|
||||
TagOutlined,
|
||||
FilterOutlined,
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
ThunderboltOutlined,
|
||||
DownOutlined,
|
||||
UpOutlined,
|
||||
ClearOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { accountApi, douyuApi, type AccountExchangeRecord, type AccountExchangeRecords, type AccountItem, type BasicSummary } from '../api/modules';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { formatTime, formatTimeShort } from '../utils/time';
|
||||
@@ -73,6 +85,18 @@ const STATUS_CARDS = [
|
||||
{ value: 'disabled', label: '已停用', color: '#8c8c8c' },
|
||||
];
|
||||
|
||||
/** 高价值套装筛选项 */
|
||||
const HIGH_VALUE_OPTIONS = [
|
||||
{ value: 'both_available', label: '🌟 双套均可换 (高价值)' },
|
||||
{ value: 'can_bairimeng', label: '👗 可换白日梦爱丽丝' },
|
||||
{ value: 'can_mengtu', label: '🐰 可换狂战萌兔' },
|
||||
{ value: 'owned_bairimeng', label: '✓ 已拥有白日梦' },
|
||||
{ value: 'owned_mengtu', label: '✓ 已拥有萌兔' },
|
||||
{ value: 'both_owned', label: '✓ 两套均已拥有' },
|
||||
{ value: 'high_value_broken', label: '⚠️ 高价值但CK失效' },
|
||||
{ value: 'unqueried', label: '❓ 未查过资料' },
|
||||
];
|
||||
|
||||
|
||||
/** 单个作用域的绑定小标签:精/电/店。 */
|
||||
function BindChip({ scope, status }: { scope: string; status?: string }) {
|
||||
@@ -141,6 +165,34 @@ export default function AccountsPage() {
|
||||
const [ckFilter, setCkFilter] = useState<string>('');
|
||||
const [bindFilter, setBindFilter] = useState<string>('');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
// 高价值资产与高级自定义筛选状态
|
||||
const [highValueFilter, setHighValueFilter] = useState<string>('');
|
||||
const [minPoints, setMinPoints] = useState<number | undefined>(undefined);
|
||||
const [maxPoints, setMaxPoints] = useState<number | undefined>(undefined);
|
||||
const [minGold, setMinGold] = useState<number | undefined>(undefined);
|
||||
const [maxGold, setMaxGold] = useState<number | undefined>(undefined);
|
||||
const [eliteOpened, setEliteOpened] = useState<boolean | undefined>(undefined);
|
||||
const [esportsOpened, setEsportsOpened] = useState<boolean | undefined>(undefined);
|
||||
const [roleBindStatus, setRoleBindStatus] = useState<string>('');
|
||||
const [advFilterOpen, setAdvFilterOpen] = useState(false);
|
||||
|
||||
const resetAdvancedFiltersState = () => {
|
||||
setHighValueFilter('');
|
||||
setMinPoints(undefined);
|
||||
setMaxPoints(undefined);
|
||||
setMinGold(undefined);
|
||||
setMaxGold(undefined);
|
||||
setEliteOpened(undefined);
|
||||
setEsportsOpened(undefined);
|
||||
setRoleBindStatus('');
|
||||
};
|
||||
|
||||
const resetAdvancedFilters = () => {
|
||||
resetAdvancedFiltersState();
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// 兑换记录预览:点列表那一格**按需拉取**(不进列表响应),弹窗按作用域分组
|
||||
const [exRecords, setExRecords] = useState<AccountExchangeRecords | null>(null);
|
||||
const [exLoading, setExLoading] = useState(false);
|
||||
@@ -181,18 +233,26 @@ export default function AccountsPage() {
|
||||
const loadAccounts = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: { tag?: string; status?: string; ck_status?: string; bind_status?: string } = {};
|
||||
if (tagFilter) params.tag = tagFilter;
|
||||
if (statusFilter) params.status = statusFilter;
|
||||
if (ckFilter) params.ck_status = ckFilter;
|
||||
if (bindFilter) params.bind_status = bindFilter;
|
||||
const data = await accountApi.listPaged({
|
||||
...params,
|
||||
const params: Parameters<typeof accountApi.listPaged>[0] = {
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
search: searchText.trim() || undefined,
|
||||
include_sensitive: canViewFull && sensitiveVisible,
|
||||
});
|
||||
};
|
||||
if (tagFilter) params.tag = tagFilter;
|
||||
if (statusFilter) params.status = statusFilter;
|
||||
if (ckFilter) params.ck_status = ckFilter;
|
||||
if (bindFilter) params.bind_status = bindFilter;
|
||||
if (highValueFilter) params.high_value_filter = highValueFilter;
|
||||
if (minPoints !== undefined) params.min_points = minPoints;
|
||||
if (maxPoints !== undefined) params.max_points = maxPoints;
|
||||
if (minGold !== undefined) params.min_gold = minGold;
|
||||
if (maxGold !== undefined) params.max_gold = maxGold;
|
||||
if (eliteOpened !== undefined) params.elite_opened = eliteOpened;
|
||||
if (esportsOpened !== undefined) params.esports_opened = esportsOpened;
|
||||
if (roleBindStatus) params.role_bind_status = roleBindStatus;
|
||||
|
||||
const data = await accountApi.listPaged(params);
|
||||
setAccounts(data.items);
|
||||
setTotal(data.total);
|
||||
} catch (e: unknown) {
|
||||
@@ -200,7 +260,25 @@ export default function AccountsPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canViewFull, currentPage, pageSize, searchText, sensitiveVisible, tagFilter, statusFilter, ckFilter, bindFilter]);
|
||||
}, [
|
||||
canViewFull,
|
||||
currentPage,
|
||||
pageSize,
|
||||
searchText,
|
||||
sensitiveVisible,
|
||||
tagFilter,
|
||||
statusFilter,
|
||||
ckFilter,
|
||||
bindFilter,
|
||||
highValueFilter,
|
||||
minPoints,
|
||||
maxPoints,
|
||||
minGold,
|
||||
maxGold,
|
||||
eliteOpened,
|
||||
esportsOpened,
|
||||
roleBindStatus,
|
||||
]);
|
||||
|
||||
const loadSummary = useCallback(async () => {
|
||||
try {
|
||||
@@ -235,7 +313,34 @@ export default function AccountsPage() {
|
||||
all_matching: selectedAllMatching,
|
||||
search: searchText.trim(),
|
||||
tag: tagFilter,
|
||||
}), [searchText, selectedAllMatching, selectedRowKeys, tagFilter]);
|
||||
status: statusFilter || undefined,
|
||||
ck_status: ckFilter || undefined,
|
||||
bind_status: bindFilter || undefined,
|
||||
high_value_filter: highValueFilter || undefined,
|
||||
min_points: minPoints,
|
||||
max_points: maxPoints,
|
||||
min_gold: minGold,
|
||||
max_gold: maxGold,
|
||||
elite_opened: eliteOpened,
|
||||
esports_opened: esportsOpened,
|
||||
role_bind_status: roleBindStatus || undefined,
|
||||
}), [
|
||||
searchText,
|
||||
selectedAllMatching,
|
||||
selectedRowKeys,
|
||||
tagFilter,
|
||||
statusFilter,
|
||||
ckFilter,
|
||||
bindFilter,
|
||||
highValueFilter,
|
||||
minPoints,
|
||||
maxPoints,
|
||||
minGold,
|
||||
maxGold,
|
||||
eliteOpened,
|
||||
esportsOpened,
|
||||
roleBindStatus,
|
||||
]);
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedRowKeys([]);
|
||||
@@ -244,7 +349,21 @@ export default function AccountsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
clearSelection();
|
||||
}, [searchText, tagFilter]);
|
||||
}, [
|
||||
searchText,
|
||||
tagFilter,
|
||||
statusFilter,
|
||||
ckFilter,
|
||||
bindFilter,
|
||||
highValueFilter,
|
||||
minPoints,
|
||||
maxPoints,
|
||||
minGold,
|
||||
maxGold,
|
||||
eliteOpened,
|
||||
esportsOpened,
|
||||
roleBindStatus,
|
||||
]);
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!importText.trim()) {
|
||||
@@ -619,6 +738,157 @@ export default function AccountsPage() {
|
||||
});
|
||||
}
|
||||
|
||||
const hasCustomFilters = Boolean(
|
||||
highValueFilter ||
|
||||
minPoints !== undefined ||
|
||||
maxPoints !== undefined ||
|
||||
minGold !== undefined ||
|
||||
maxGold !== undefined ||
|
||||
eliteOpened !== undefined ||
|
||||
esportsOpened !== undefined ||
|
||||
roleBindStatus
|
||||
);
|
||||
|
||||
const PRESET_CHIPS = [
|
||||
{
|
||||
key: 'both_available',
|
||||
label: '双套均可换',
|
||||
icon: '🌟',
|
||||
count: summary.high_value?.both_available,
|
||||
color: '#52c41a',
|
||||
active: highValueFilter === 'both_available' && minPoints === undefined && maxPoints === undefined && minGold === undefined && maxGold === undefined && eliteOpened === undefined && esportsOpened === undefined && !roleBindStatus,
|
||||
onClick: () => {
|
||||
if (highValueFilter === 'both_available') {
|
||||
setHighValueFilter('');
|
||||
} else {
|
||||
resetAdvancedFiltersState();
|
||||
setHighValueFilter('both_available');
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'can_bairimeng',
|
||||
label: '可换白日梦',
|
||||
icon: '👗',
|
||||
count: summary.high_value?.can_bairimeng,
|
||||
color: '#1677ff',
|
||||
active: highValueFilter === 'can_bairimeng' && minPoints === undefined && maxPoints === undefined && minGold === undefined && maxGold === undefined && eliteOpened === undefined && esportsOpened === undefined && !roleBindStatus,
|
||||
onClick: () => {
|
||||
if (highValueFilter === 'can_bairimeng') {
|
||||
setHighValueFilter('');
|
||||
} else {
|
||||
resetAdvancedFiltersState();
|
||||
setHighValueFilter('can_bairimeng');
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'can_mengtu',
|
||||
label: '可换萌兔',
|
||||
icon: '🐰',
|
||||
count: summary.high_value?.can_mengtu,
|
||||
color: '#722ed1',
|
||||
active: highValueFilter === 'can_mengtu' && minPoints === undefined && maxPoints === undefined && minGold === undefined && maxGold === undefined && eliteOpened === undefined && esportsOpened === undefined && !roleBindStatus,
|
||||
onClick: () => {
|
||||
if (highValueFilter === 'can_mengtu') {
|
||||
setHighValueFilter('');
|
||||
} else {
|
||||
resetAdvancedFiltersState();
|
||||
setHighValueFilter('can_mengtu');
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'high_points',
|
||||
label: '积分≥300',
|
||||
icon: '💎',
|
||||
count: summary.high_value?.high_points,
|
||||
color: '#fa8c16',
|
||||
active: minPoints === 300 && maxPoints === undefined && !highValueFilter && minGold === undefined && maxGold === undefined && eliteOpened === undefined && esportsOpened === undefined && !roleBindStatus,
|
||||
onClick: () => {
|
||||
if (minPoints === 300) {
|
||||
setMinPoints(undefined);
|
||||
} else {
|
||||
resetAdvancedFiltersState();
|
||||
setMinPoints(300);
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'has_gold',
|
||||
label: '有鱼翅',
|
||||
icon: '💰',
|
||||
count: summary.high_value?.has_gold,
|
||||
color: '#faad14',
|
||||
active: minGold === 0.01 && maxGold === undefined && !highValueFilter && minPoints === undefined && maxPoints === undefined && eliteOpened === undefined && esportsOpened === undefined && !roleBindStatus,
|
||||
onClick: () => {
|
||||
if (minGold === 0.01) {
|
||||
setMinGold(undefined);
|
||||
} else {
|
||||
resetAdvancedFiltersState();
|
||||
setMinGold(0.01);
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'elite_opened',
|
||||
label: '宝典已开通',
|
||||
icon: '📜',
|
||||
count: summary.high_value?.elite_opened,
|
||||
color: '#13c2c2',
|
||||
active: eliteOpened === true && !highValueFilter && minPoints === undefined && maxPoints === undefined && minGold === undefined && maxGold === undefined && esportsOpened === undefined && !roleBindStatus,
|
||||
onClick: () => {
|
||||
if (eliteOpened === true) {
|
||||
setEliteOpened(undefined);
|
||||
} else {
|
||||
resetAdvancedFiltersState();
|
||||
setEliteOpened(true);
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'high_value_broken',
|
||||
label: '高价值待救',
|
||||
icon: '⚠️',
|
||||
count: summary.high_value?.high_value_broken,
|
||||
color: '#f5222d',
|
||||
tooltip: '可换高价值套装但 CK 失效,需重登抢救',
|
||||
active: highValueFilter === 'high_value_broken' && minPoints === undefined && maxPoints === undefined && minGold === undefined && maxGold === undefined && eliteOpened === undefined && esportsOpened === undefined && !roleBindStatus,
|
||||
onClick: () => {
|
||||
if (highValueFilter === 'high_value_broken') {
|
||||
setHighValueFilter('');
|
||||
} else {
|
||||
resetAdvancedFiltersState();
|
||||
setHighValueFilter('high_value_broken');
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'unqueried',
|
||||
label: '未查资料',
|
||||
icon: '🔍',
|
||||
count: summary.high_value?.unqueried,
|
||||
color: '#8c8c8c',
|
||||
active: highValueFilter === 'unqueried' && minPoints === undefined && maxPoints === undefined && minGold === undefined && maxGold === undefined && eliteOpened === undefined && esportsOpened === undefined && !roleBindStatus,
|
||||
onClick: () => {
|
||||
if (highValueFilter === 'unqueried') {
|
||||
setHighValueFilter('');
|
||||
} else {
|
||||
resetAdvancedFiltersState();
|
||||
setHighValueFilter('unqueried');
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
||||
@@ -765,6 +1035,287 @@ export default function AccountsPage() {
|
||||
})}
|
||||
</Row>
|
||||
|
||||
{/* 高价值资产一键筛选 & 高级自定义筛选面板 */}
|
||||
<Card
|
||||
size="small"
|
||||
style={{ marginBottom: 12, borderRadius: 6 }}
|
||||
styles={{ body: { padding: '8px 12px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
|
||||
<Space wrap size={[6, 6]} style={{ alignItems: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: 13, marginRight: 4 }}>
|
||||
<ThunderboltOutlined style={{ color: '#fa8c16', marginRight: 4 }} />
|
||||
高价值一键筛选:
|
||||
</Text>
|
||||
{PRESET_CHIPS.map((chip) => {
|
||||
const btn = (
|
||||
<Button
|
||||
key={chip.key}
|
||||
size="small"
|
||||
type={chip.active ? 'primary' : 'default'}
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
fontSize: 12,
|
||||
height: 26,
|
||||
padding: '0 10px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
borderColor: chip.active ? chip.color : undefined,
|
||||
backgroundColor: chip.active ? chip.color : undefined,
|
||||
color: chip.active ? '#fff' : undefined,
|
||||
opacity: (chip.count === 0 && !chip.active) ? 0.5 : 1,
|
||||
}}
|
||||
onClick={chip.onClick}
|
||||
>
|
||||
<span>{chip.icon} {chip.label}</span>
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
padding: '0 5px',
|
||||
borderRadius: 10,
|
||||
fontSize: 11,
|
||||
lineHeight: '16px',
|
||||
background: chip.active ? 'rgba(255,255,255,0.25)' : '#f0f0f0',
|
||||
color: chip.active ? '#fff' : '#666',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{chip.count ?? 0}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
return chip.tooltip ? (
|
||||
<Tooltip key={chip.key} title={chip.tooltip}>
|
||||
{btn}
|
||||
</Tooltip>
|
||||
) : btn;
|
||||
})}
|
||||
</Space>
|
||||
|
||||
<Space size={8}>
|
||||
{hasCustomFilters && (
|
||||
<Button size="small" type="link" danger onClick={resetAdvancedFilters} style={{ padding: 0 }}>
|
||||
清除高价值筛选
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
type={advFilterOpen ? 'primary' : 'default'}
|
||||
ghost={advFilterOpen}
|
||||
icon={advFilterOpen ? <UpOutlined /> : <DownOutlined />}
|
||||
onClick={() => setAdvFilterOpen(!advFilterOpen)}
|
||||
>
|
||||
{advFilterOpen ? '收起高级筛选' : '高级自定义筛选'}
|
||||
{hasCustomFilters && !advFilterOpen && (
|
||||
<Tag color="blue" style={{ marginLeft: 4, marginRight: 0, padding: '0 4px', fontSize: 11 }}>
|
||||
已生效
|
||||
</Tag>
|
||||
)}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 折叠展开的高级筛选面板 */}
|
||||
{advFilterOpen && (
|
||||
<div style={{ marginTop: 10, paddingTop: 10, borderTop: '1px dashed #f0f0f0' }}>
|
||||
<Row gutter={[16, 10]} align="middle">
|
||||
<Col>
|
||||
<Space size={6} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>套装状态:</Text>
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
placeholder="全部状态"
|
||||
style={{ width: 180 }}
|
||||
value={highValueFilter || undefined}
|
||||
onChange={(val) => {
|
||||
setHighValueFilter(val || '');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
options={HIGH_VALUE_OPTIONS}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<Space size={4} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>积分范围:</Text>
|
||||
<InputNumber
|
||||
size="small"
|
||||
placeholder="最小"
|
||||
min={0}
|
||||
value={minPoints}
|
||||
onChange={(v) => {
|
||||
setMinPoints(v ?? undefined);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ width: 75 }}
|
||||
/>
|
||||
<span>~</span>
|
||||
<InputNumber
|
||||
size="small"
|
||||
placeholder="最大"
|
||||
min={0}
|
||||
value={maxPoints}
|
||||
onChange={(v) => {
|
||||
setMaxPoints(v ?? undefined);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ width: 75 }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
type="dashed"
|
||||
style={{ fontSize: 11, padding: '0 6px' }}
|
||||
onClick={() => {
|
||||
setMinPoints(300);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
≥300
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="dashed"
|
||||
style={{ fontSize: 11, padding: '0 6px' }}
|
||||
onClick={() => {
|
||||
setMinPoints(600);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
≥600
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<Space size={4} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>鱼翅余额:</Text>
|
||||
<InputNumber
|
||||
size="small"
|
||||
placeholder="最小"
|
||||
min={0}
|
||||
step={1}
|
||||
value={minGold}
|
||||
onChange={(v) => {
|
||||
setMinGold(v ?? undefined);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ width: 75 }}
|
||||
/>
|
||||
<span>~</span>
|
||||
<InputNumber
|
||||
size="small"
|
||||
placeholder="最大"
|
||||
min={0}
|
||||
step={1}
|
||||
value={maxGold}
|
||||
onChange={(v) => {
|
||||
setMaxGold(v ?? undefined);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ width: 75 }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
type="dashed"
|
||||
style={{ fontSize: 11, padding: '0 6px' }}
|
||||
onClick={() => {
|
||||
setMinGold(0.01);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
>0
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="dashed"
|
||||
style={{ fontSize: 11, padding: '0 6px' }}
|
||||
onClick={() => {
|
||||
setMinGold(6);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
≥6
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<Space size={6} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>宝典:</Text>
|
||||
<Select
|
||||
size="small"
|
||||
allowClear
|
||||
placeholder="全部"
|
||||
style={{ width: 95 }}
|
||||
value={eliteOpened === undefined ? undefined : String(eliteOpened)}
|
||||
onChange={(val) => {
|
||||
setEliteOpened(val === undefined ? undefined : val === 'true');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
options={[
|
||||
{ value: 'true', label: '宝典已开' },
|
||||
{ value: 'false', label: '宝典未开' },
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<Space size={6} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>电竞:</Text>
|
||||
<Select
|
||||
size="small"
|
||||
allowClear
|
||||
placeholder="全部"
|
||||
style={{ width: 95 }}
|
||||
value={esportsOpened === undefined ? undefined : String(esportsOpened)}
|
||||
onChange={(val) => {
|
||||
setEsportsOpened(val === undefined ? undefined : val === 'true');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
options={[
|
||||
{ value: 'true', label: '电竞已开' },
|
||||
{ value: 'false', label: '电竞未开' },
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<Space size={6} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>角色绑定:</Text>
|
||||
<Select
|
||||
size="small"
|
||||
allowClear
|
||||
placeholder="全部"
|
||||
style={{ width: 100 }}
|
||||
value={roleBindStatus || undefined}
|
||||
onChange={(val) => {
|
||||
setRoleBindStatus(val || '');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
options={BIND_OPTIONS}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ClearOutlined />}
|
||||
onClick={resetAdvancedFilters}
|
||||
disabled={!hasCustomFilters}
|
||||
>
|
||||
重置自定义条件
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Table
|
||||
tableLayout="fixed"
|
||||
title={() => selectedRowKeys.length > 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user