feat: 为CK页面增加标签筛选
This commit is contained in:
@@ -14,6 +14,7 @@ from web.backend.models import Account, LoginTask, User
|
|||||||
from web.backend.routers.cookies import (
|
from web.backend.routers.cookies import (
|
||||||
check_cookie_operations,
|
check_cookie_operations,
|
||||||
get_cookie,
|
get_cookie,
|
||||||
|
list_cookie_operation_tags,
|
||||||
list_cookie_operations,
|
list_cookie_operations,
|
||||||
list_cookies,
|
list_cookies,
|
||||||
)
|
)
|
||||||
@@ -62,6 +63,7 @@ class CookieOperationTests(unittest.TestCase):
|
|||||||
def test_support_operation_list_is_scoped_and_never_returns_credentials(self):
|
def test_support_operation_list_is_scoped_and_never_returns_credentials(self):
|
||||||
result = list_cookie_operations(
|
result = list_cookie_operations(
|
||||||
search="",
|
search="",
|
||||||
|
tag="",
|
||||||
page=1,
|
page=1,
|
||||||
page_size=20,
|
page_size=20,
|
||||||
db=self.session,
|
db=self.session,
|
||||||
@@ -72,6 +74,7 @@ class CookieOperationTests(unittest.TestCase):
|
|||||||
self.assertEqual(result["items"], [{
|
self.assertEqual(result["items"], [{
|
||||||
"id": self.owned_task.id,
|
"id": self.owned_task.id,
|
||||||
"account_username": "owned-account",
|
"account_username": "owned-account",
|
||||||
|
"tag": "",
|
||||||
"ck_check_status": "",
|
"ck_check_status": "",
|
||||||
"ck_checked_at": None,
|
"ck_checked_at": None,
|
||||||
"created_at": None,
|
"created_at": None,
|
||||||
@@ -85,6 +88,45 @@ class CookieOperationTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(Exception, "cookie:view"):
|
with self.assertRaisesRegex(Exception, "cookie:view"):
|
||||||
get_cookie(self.owned_task.id, db=self.session, current=self.support)
|
get_cookie(self.owned_task.id, db=self.session, current=self.support)
|
||||||
|
|
||||||
|
def test_cookie_list_filters_by_account_tag(self):
|
||||||
|
admin = User(username="admin", password_hash="hash", role="super_admin")
|
||||||
|
self.session.add(admin)
|
||||||
|
self.session.query(Account).filter(Account.id == self.owned_task.account_id).update({"tag": "A组"})
|
||||||
|
self.session.query(Account).filter(Account.id == self.other_task.account_id).update({"tag": "B组"})
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
result = list_cookies(
|
||||||
|
search="",
|
||||||
|
tag="A组",
|
||||||
|
account_names="",
|
||||||
|
page=1,
|
||||||
|
page_size=20,
|
||||||
|
include_cookie=False,
|
||||||
|
db=self.session,
|
||||||
|
current=admin,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result["total"], 1)
|
||||||
|
self.assertEqual([item["id"] for item in result["items"]], [self.owned_task.id])
|
||||||
|
|
||||||
|
def test_support_operation_tag_filter_and_list_stay_scoped(self):
|
||||||
|
self.session.query(Account).filter(Account.id == self.owned_task.account_id).update({"tag": "我的标签"})
|
||||||
|
self.session.query(Account).filter(Account.id == self.other_task.account_id).update({"tag": "别人的标签"})
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
tags = list_cookie_operation_tags(db=self.session, current=self.support)
|
||||||
|
result = list_cookie_operations(
|
||||||
|
search="",
|
||||||
|
tag="别人的标签",
|
||||||
|
page=1,
|
||||||
|
page_size=20,
|
||||||
|
db=self.session,
|
||||||
|
current=self.support,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(tags, ["我的标签"])
|
||||||
|
self.assertEqual(result["total"], 0)
|
||||||
|
|
||||||
@patch("web.backend.routers.cookies._check_one_cookie")
|
@patch("web.backend.routers.cookies._check_one_cookie")
|
||||||
def test_support_can_check_only_assigned_cookie(self, check_one_cookie):
|
def test_support_can_check_only_assigned_cookie(self, check_one_cookie):
|
||||||
check_one_cookie.return_value = {
|
check_one_cookie.return_value = {
|
||||||
@@ -119,6 +161,7 @@ class CookieOperationTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(Exception, "cookie:operate"):
|
with self.assertRaisesRegex(Exception, "cookie:operate"):
|
||||||
list_cookie_operations(
|
list_cookie_operations(
|
||||||
search="",
|
search="",
|
||||||
|
tag="",
|
||||||
page=1,
|
page=1,
|
||||||
page_size=20,
|
page_size=20,
|
||||||
db=self.session,
|
db=self.session,
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ def _visible_cookie_tasks_query(db: Session, current: User):
|
|||||||
@router.get("")
|
@router.get("")
|
||||||
def list_cookies(
|
def list_cookies(
|
||||||
search: str = Query(""),
|
search: str = Query(""),
|
||||||
|
tag: str = Query(""),
|
||||||
account_names: str = Query(""),
|
account_names: str = Query(""),
|
||||||
page: int | None = Query(None, ge=1),
|
page: int | None = Query(None, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=200),
|
page_size: int = Query(20, ge=1, le=200),
|
||||||
@@ -180,6 +181,7 @@ def list_cookies(
|
|||||||
joinedload(LoginTask.account).joinedload(Account.assigned_user),
|
joinedload(LoginTask.account).joinedload(Account.assigned_user),
|
||||||
)
|
)
|
||||||
search_text = (search or "").strip()
|
search_text = (search or "").strip()
|
||||||
|
tag_value = (tag or "").strip()
|
||||||
selected_names = _parse_account_names(account_names)
|
selected_names = _parse_account_names(account_names)
|
||||||
can_view_all = user_has_permission(current, "login:view_all")
|
can_view_all = user_has_permission(current, "login:view_all")
|
||||||
account_joined = not can_view_all
|
account_joined = not can_view_all
|
||||||
@@ -188,6 +190,11 @@ def list_cookies(
|
|||||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||||
account_joined = True
|
account_joined = True
|
||||||
query = query.filter(Account.username.in_(selected_names))
|
query = query.filter(Account.username.in_(selected_names))
|
||||||
|
if tag_value:
|
||||||
|
if not account_joined:
|
||||||
|
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||||
|
account_joined = True
|
||||||
|
query = query.filter(Account.tag == tag_value)
|
||||||
if search_text:
|
if search_text:
|
||||||
pattern = f"%{search_text}%"
|
pattern = f"%{search_text}%"
|
||||||
if not account_joined:
|
if not account_joined:
|
||||||
@@ -195,6 +202,7 @@ def list_cookies(
|
|||||||
account_joined = True
|
account_joined = True
|
||||||
query = query.outerjoin(User, Account.assigned_to == User.id).filter(or_(
|
query = query.outerjoin(User, Account.assigned_to == User.id).filter(or_(
|
||||||
Account.username.ilike(pattern),
|
Account.username.ilike(pattern),
|
||||||
|
Account.tag.ilike(pattern),
|
||||||
User.username.ilike(pattern),
|
User.username.ilike(pattern),
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -276,6 +284,7 @@ def cookies_summary(
|
|||||||
@router.get("/operations")
|
@router.get("/operations")
|
||||||
def list_cookie_operations(
|
def list_cookie_operations(
|
||||||
search: str = Query(""),
|
search: str = Query(""),
|
||||||
|
tag: str = Query(""),
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=200),
|
page_size: int = Query(20, ge=1, le=200),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -287,8 +296,14 @@ def list_cookie_operations(
|
|||||||
if user_has_permission(current, "login:view_all"):
|
if user_has_permission(current, "login:view_all"):
|
||||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||||
search_text = (search or "").strip()
|
search_text = (search or "").strip()
|
||||||
|
tag_value = (tag or "").strip()
|
||||||
|
if tag_value:
|
||||||
|
query = query.filter(Account.tag == tag_value)
|
||||||
if search_text:
|
if search_text:
|
||||||
query = query.filter(Account.username.ilike(f"%{search_text}%"))
|
query = query.filter(or_(
|
||||||
|
Account.username.ilike(f"%{search_text}%"),
|
||||||
|
Account.tag.ilike(f"%{search_text}%"),
|
||||||
|
))
|
||||||
|
|
||||||
total = query.order_by(None).count()
|
total = query.order_by(None).count()
|
||||||
tasks = (
|
tasks = (
|
||||||
@@ -302,6 +317,7 @@ def list_cookie_operations(
|
|||||||
{
|
{
|
||||||
"id": task.id,
|
"id": task.id,
|
||||||
"account_username": task.account.username if task.account else "",
|
"account_username": task.account.username if task.account else "",
|
||||||
|
"tag": task.account.tag if task.account else "",
|
||||||
"ck_check_status": task.ck_check_status or "",
|
"ck_check_status": task.ck_check_status or "",
|
||||||
"ck_checked_at": _fmt_dt(task.ck_checked_at),
|
"ck_checked_at": _fmt_dt(task.ck_checked_at),
|
||||||
"created_at": _fmt_dt(task.finished_at),
|
"created_at": _fmt_dt(task.finished_at),
|
||||||
@@ -314,6 +330,26 @@ def list_cookie_operations(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/operations/tags")
|
||||||
|
def list_cookie_operation_tags(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""返回当前用户可操作 CK 记录对应的标签,不跨越账号分配范围。"""
|
||||||
|
_require_cookie_operation_perm(current)
|
||||||
|
query = _visible_cookie_tasks_query(db, current)
|
||||||
|
if user_has_permission(current, "login:view_all"):
|
||||||
|
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||||
|
rows = (
|
||||||
|
query.filter(Account.tag != "", Account.tag.isnot(None))
|
||||||
|
.with_entities(Account.tag)
|
||||||
|
.distinct()
|
||||||
|
.order_by(Account.tag.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [tag for tag, in rows if tag]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/duplicates")
|
@router.get("/duplicates")
|
||||||
def find_duplicate_cookies(
|
def find_duplicate_cookies(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import type { BasicSummary, CookieCheckResult, CookieDuplicateResponse, CookieIt
|
|||||||
|
|
||||||
export const cookieApi = {
|
export const cookieApi = {
|
||||||
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
|
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
|
||||||
listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string }) =>
|
listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string; tag?: string }) =>
|
||||||
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
|
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
|
||||||
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
|
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
|
||||||
listOperations: (params: PageParams) =>
|
listOperations: (params: PageParams & { tag?: string }) =>
|
||||||
api.get<PaginatedResponse<CookieOperationItem>, PaginatedResponse<CookieOperationItem>>('/cookies/operations', { params }),
|
api.get<PaginatedResponse<CookieOperationItem>, PaginatedResponse<CookieOperationItem>>('/cookies/operations', { params }),
|
||||||
|
listOperationTags: () => api.get<string[], string[]>('/cookies/operations/tags'),
|
||||||
duplicates: () => api.get<CookieDuplicateResponse, CookieDuplicateResponse>('/cookies/duplicates'),
|
duplicates: () => api.get<CookieDuplicateResponse, CookieDuplicateResponse>('/cookies/duplicates'),
|
||||||
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
|
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
|
||||||
exportCsv: (format?: string, accountNames?: string) => api.get<Blob, Blob>('/cookies/export', {
|
exportCsv: (format?: string, accountNames?: string) => api.get<Blob, Blob>('/cookies/export', {
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ export interface CookieItem {
|
|||||||
export interface CookieOperationItem {
|
export interface CookieOperationItem {
|
||||||
id: number;
|
id: number;
|
||||||
account_username: string;
|
account_username: string;
|
||||||
|
tag: string;
|
||||||
ck_check_status: string;
|
ck_check_status: string;
|
||||||
ck_checked_at: string | null;
|
ck_checked_at: string | null;
|
||||||
created_at: string | null;
|
created_at: string | null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button, Input, Popconfirm, Space, Table, Tag, Typography } from 'antd';
|
import { Button, Input, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
|
||||||
import { message } from '../utils/antdMessage';
|
import { message } from '../utils/antdMessage';
|
||||||
import { LoadingOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
|
import { LoadingOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
import { cookieApi, type CookieOperationCheckResult, type CookieOperationItem } from '../api/modules';
|
import { cookieApi, type CookieOperationCheckResult, type CookieOperationItem } from '../api/modules';
|
||||||
@@ -12,6 +12,8 @@ export default function CookieOperationsPage() {
|
|||||||
const [items, setItems] = useState<CookieOperationItem[]>([]);
|
const [items, setItems] = useState<CookieOperationItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [tagFilter, setTagFilter] = useState('');
|
||||||
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
const [checkingIds, setCheckingIds] = useState<Set<number>>(new Set());
|
const [checkingIds, setCheckingIds] = useState<Set<number>>(new Set());
|
||||||
const [reloginIds, setReloginIds] = useState<Set<number>>(new Set());
|
const [reloginIds, setReloginIds] = useState<Set<number>>(new Set());
|
||||||
@@ -30,6 +32,7 @@ export default function CookieOperationsPage() {
|
|||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
search: search.trim() || undefined,
|
search: search.trim() || undefined,
|
||||||
|
tag: tagFilter || undefined,
|
||||||
});
|
});
|
||||||
setItems(data.items);
|
setItems(data.items);
|
||||||
setTotal(data.total);
|
setTotal(data.total);
|
||||||
@@ -50,12 +53,18 @@ export default function CookieOperationsPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currentPage, pageSize, search]);
|
}, [currentPage, pageSize, search, tagFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadItems();
|
void loadItems();
|
||||||
}, [loadItems]);
|
}, [loadItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cookieApi.listOperationTags().then(setTags).catch(() => {
|
||||||
|
// 标签加载失败不影响 CK 检测与重登。
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCheck = async (ids: number[]) => {
|
const handleCheck = async (ids: number[]) => {
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
setCheckingIds((previous) => new Set([...previous, ...ids]));
|
setCheckingIds((previous) => new Set([...previous, ...ids]));
|
||||||
@@ -103,6 +112,13 @@ export default function CookieOperationsPage() {
|
|||||||
const selectedIds = selectedRowKeys.map((key) => Number(key));
|
const selectedIds = selectedRowKeys.map((key) => Number(key));
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '账号', dataIndex: 'account_username', ellipsis: true },
|
{ title: '账号', dataIndex: 'account_username', ellipsis: true },
|
||||||
|
{
|
||||||
|
title: '标签',
|
||||||
|
dataIndex: 'tag',
|
||||||
|
width: 140,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (tag: string) => tag ? <Tag color="blue">{tag}</Tag> : <Text type="secondary">-</Text>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '有效性',
|
title: '有效性',
|
||||||
width: 110,
|
width: 110,
|
||||||
@@ -168,18 +184,33 @@ export default function CookieOperationsPage() {
|
|||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<Input.Search
|
<Space wrap style={{ marginBottom: 12 }}>
|
||||||
allowClear
|
<Select
|
||||||
prefix={<SearchOutlined />}
|
allowClear
|
||||||
placeholder="搜索账号"
|
showSearch
|
||||||
style={{ width: 280, marginBottom: 12 }}
|
placeholder="按标签筛选"
|
||||||
value={search}
|
style={{ width: 160 }}
|
||||||
onChange={(event) => {
|
value={tagFilter || undefined}
|
||||||
setSearch(event.target.value);
|
onChange={(value) => {
|
||||||
setCurrentPage(1);
|
setTagFilter(value || '');
|
||||||
setSelectedRowKeys([]);
|
setCurrentPage(1);
|
||||||
}}
|
setSelectedRowKeys([]);
|
||||||
/>
|
}}
|
||||||
|
options={tags.map((tag) => ({ value: tag, label: tag }))}
|
||||||
|
/>
|
||||||
|
<Input.Search
|
||||||
|
allowClear
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
placeholder="搜索账号或标签"
|
||||||
|
style={{ width: 280 }}
|
||||||
|
value={search}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSearch(event.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown, Tooltip, Modal, Alert, Empty } from 'antd';
|
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, Select, theme, Dropdown, Tooltip, Modal, Alert, Empty } from 'antd';
|
||||||
import { message } from '../utils/antdMessage';
|
import { message } from '../utils/antdMessage';
|
||||||
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined, ReloadOutlined, FilterOutlined, ScanOutlined } from '@ant-design/icons';
|
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined, ReloadOutlined, FilterOutlined, ScanOutlined } from '@ant-design/icons';
|
||||||
import { cookieApi, type BasicSummary, type CookieCheckResult, type CookieDuplicateGroup, type CookieDuplicateResponse, type CookieItem } from '../api/modules';
|
import { accountApi, cookieApi, type BasicSummary, type CookieCheckResult, type CookieDuplicateGroup, type CookieDuplicateResponse, type CookieItem } from '../api/modules';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { formatTime } from '../utils/time';
|
import { formatTime } from '../utils/time';
|
||||||
import { getErrorMessage } from '../utils/error';
|
import { getErrorMessage } from '../utils/error';
|
||||||
@@ -53,6 +53,8 @@ export default function CookiePage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [tagFilter, setTagFilter] = useState('');
|
||||||
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
const [customNamesText, setCustomNamesText] = useState('');
|
const [customNamesText, setCustomNamesText] = useState('');
|
||||||
const [customAccountNames, setCustomAccountNames] = useState<string[]>([]);
|
const [customAccountNames, setCustomAccountNames] = useState<string[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
@@ -181,6 +183,7 @@ export default function CookiePage() {
|
|||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
search: searchText.trim() || undefined,
|
search: searchText.trim() || undefined,
|
||||||
|
tag: tagFilter || undefined,
|
||||||
account_names: customAccountNames.length > 0 ? customAccountNames.join('\n') : undefined,
|
account_names: customAccountNames.length > 0 ? customAccountNames.join('\n') : undefined,
|
||||||
include_cookie: false,
|
include_cookie: false,
|
||||||
});
|
});
|
||||||
@@ -208,7 +211,7 @@ export default function CookiePage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currentPage, pageSize, searchText, customAccountNames]);
|
}, [currentPage, pageSize, searchText, tagFilter, customAccountNames]);
|
||||||
|
|
||||||
const loadSummary = useCallback(async () => {
|
const loadSummary = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -227,6 +230,12 @@ export default function CookiePage() {
|
|||||||
loadSummary();
|
loadSummary();
|
||||||
}, [loadSummary]);
|
}, [loadSummary]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
accountApi.listTags().then(setTags).catch(() => {
|
||||||
|
// 标签加载失败不影响 Cookie 管理。
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleExport = async (format: string = 'csv') => {
|
const handleExport = async (format: string = 'csv') => {
|
||||||
try {
|
try {
|
||||||
const blob = await cookieApi.exportCsv(
|
const blob = await cookieApi.exportCsv(
|
||||||
@@ -626,9 +635,22 @@ export default function CookiePage() {
|
|||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Space wrap style={{ marginBottom: 12 }}>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
placeholder="按标签筛选"
|
||||||
|
style={{ width: 160 }}
|
||||||
|
value={tagFilter || undefined}
|
||||||
|
onChange={(value) => {
|
||||||
|
setTagFilter(value || '');
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
}}
|
||||||
|
options={tags.map((tag) => ({ value: tag, label: tag }))}
|
||||||
|
/>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="搜索账号或分配客服"
|
placeholder="搜索账号、标签或分配客服"
|
||||||
allowClear
|
allowClear
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -639,7 +661,7 @@ export default function CookiePage() {
|
|||||||
size="small"
|
size="small"
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Space>
|
||||||
<Table
|
<Table
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
selectedRowKeys,
|
selectedRowKeys,
|
||||||
|
|||||||
Reference in New Issue
Block a user