diff --git a/tests/test_account_overview.py b/tests/test_account_overview.py index 02f9f74..8146e81 100644 --- a/tests/test_account_overview.py +++ b/tests/test_account_overview.py @@ -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 diff --git a/tests/test_douyu_task_assigned_optimization.py b/tests/test_douyu_task_assigned_optimization.py index 0e2967d..b35a8af 100644 --- a/tests/test_douyu_task_assigned_optimization.py +++ b/tests/test_douyu_task_assigned_optimization.py @@ -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", diff --git a/web/backend/routers/accounts.py b/web/backend/routers/accounts.py index 8fd1785..bc574e8 100644 --- a/web/backend/routers/accounts.py +++ b/web/backend/routers/accounts.py @@ -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") diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 9f031f5..466c020 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -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): diff --git a/web/frontend/src/api/accounts.ts b/web/frontend/src/api/accounts.ts index d85479b..5ccfcbd 100644 --- a/web/frontend/src/api/accounts.ts +++ b/web/frontend/src/api/accounts.ts @@ -18,7 +18,25 @@ import type { export const accountApi = { list: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean }) => api.get('/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>('/accounts', { params }), summary: () => api.get('/accounts/summary'), import: (text: string, tag?: string) => diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index 0119cfb..926fce2 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -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; /** 不含归档的账号数 */ visible_total?: number; + /** 高价值及核心资产统计 */ + high_value?: HighValueSummary; /** CK 已被判失效的账号数 */ ck_invalid_count?: number; /** 没有 CK 的账号数 */ diff --git a/web/frontend/src/pages/AccountsPage.tsx b/web/frontend/src/pages/AccountsPage.tsx index 8aceca4..27838be 100644 --- a/web/frontend/src/pages/AccountsPage.tsx +++ b/web/frontend/src/pages/AccountsPage.tsx @@ -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(''); const [bindFilter, setBindFilter] = useState(''); const [searchText, setSearchText] = useState(''); + + // 高价值资产与高级自定义筛选状态 + const [highValueFilter, setHighValueFilter] = useState(''); + const [minPoints, setMinPoints] = useState(undefined); + const [maxPoints, setMaxPoints] = useState(undefined); + const [minGold, setMinGold] = useState(undefined); + const [maxGold, setMaxGold] = useState(undefined); + const [eliteOpened, setEliteOpened] = useState(undefined); + const [esportsOpened, setEsportsOpened] = useState(undefined); + const [roleBindStatus, setRoleBindStatus] = useState(''); + 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(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[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 (
@@ -765,6 +1035,287 @@ export default function AccountsPage() { })} + {/* 高价值资产一键筛选 & 高级自定义筛选面板 */} + +
+ + + + 高价值一键筛选: + + {PRESET_CHIPS.map((chip) => { + const btn = ( + + ); + return chip.tooltip ? ( + + {btn} + + ) : btn; + })} + + + + {hasCustomFilters && ( + + )} + + +
+ + {/* 折叠展开的高级筛选面板 */} + {advFilterOpen && ( +
+ + + + 套装状态: + { + setEliteOpened(val === undefined ? undefined : val === 'true'); + setCurrentPage(1); + }} + options={[ + { value: 'true', label: '宝典已开' }, + { value: 'false', label: '宝典未开' }, + ]} + /> + + + + + + 电竞: + { + setRoleBindStatus(val || ''); + setCurrentPage(1); + }} + options={BIND_OPTIONS} + /> + + + + + + + +
+ )} +
+ selectedRowKeys.length > 0 ? (