优化退出登录, cookie 可以复制
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Layout, Menu, Avatar, Space, Typography, Button } from 'antd';
|
import { Layout, Menu, Avatar, Space, Typography, Button, Modal } from 'antd';
|
||||||
import {
|
import {
|
||||||
DashboardOutlined, UserOutlined, LogoutOutlined,
|
DashboardOutlined, UserOutlined, LogoutOutlined,
|
||||||
CloudServerOutlined, TeamOutlined, ApiOutlined, KeyOutlined,
|
CloudServerOutlined, TeamOutlined, ApiOutlined, KeyOutlined,
|
||||||
@@ -60,19 +60,31 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
|||||||
menuItems.push({ key: '/users', label: '用户管理', icon: <TeamOutlined /> });
|
menuItems.push({ key: '/users', label: '用户管理', icon: <TeamOutlined /> });
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认退出',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
okText: '退出',
|
||||||
|
cancelText: '取消',
|
||||||
|
okButtonProps: { type: 'primary' },
|
||||||
|
centered: true,
|
||||||
|
onOk: async () => {
|
||||||
try {
|
try {
|
||||||
await authApi.logout();
|
await authApi.logout();
|
||||||
} catch {}
|
} catch {}
|
||||||
clearAuth();
|
clearAuth();
|
||||||
onLogout?.();
|
onLogout?.();
|
||||||
navigate('/login', { replace: true });
|
navigate('/login', { replace: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ height: '100vh', overflow: 'hidden' }}>
|
<Layout style={{ height: '100vh', overflow: 'hidden' }}>
|
||||||
<Sider trigger={null} collapsed={collapsed} onCollapse={setCollapsed}
|
<Sider trigger={null} collapsed={collapsed} onCollapse={setCollapsed}>
|
||||||
style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{
|
||||||
|
display: 'flex', flexDirection: 'column', height: '100%',
|
||||||
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
height: 48, margin: '12px 12px 0', color: '#fff', fontSize: 16,
|
height: 48, margin: '12px 12px 0', color: '#fff', fontSize: 16,
|
||||||
textAlign: 'center', lineHeight: '48px', fontWeight: 'bold',
|
textAlign: 'center', lineHeight: '48px', fontWeight: 'bold',
|
||||||
@@ -114,18 +126,29 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
{!collapsed && (
|
{collapsed ? (
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<LogoutOutlined />}
|
icon={<LogoutOutlined />}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
style={{ textAlign: 'left' }}
|
style={{ color: 'rgba(255,255,255,0.65)' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<LogoutOutlined />}
|
||||||
|
onClick={handleLogout}
|
||||||
|
style={{ color: 'rgba(255,255,255,0.65)', justifyContent: 'flex-start' }}
|
||||||
>
|
>
|
||||||
退出登录
|
退出登录
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Content style={{ margin: 16, padding: 24, background: '#fff', borderRadius: 8, overflow: 'auto' }}>
|
<Content style={{ margin: 16, padding: 24, background: '#fff', borderRadius: 8, overflow: 'auto' }}>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Table, Button, Card, Row, Col, Statistic, message, Tag, Popconfirm } from 'antd';
|
import { Table, Button, Card, Row, Col, Statistic, message, Tag, Popconfirm, Space } from 'antd';
|
||||||
import { DownloadOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { DownloadOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons';
|
||||||
import { cookieApi } from '../api/modules';
|
import { cookieApi } from '../api/modules';
|
||||||
import { getUser, hasPerm } from '../store/auth';
|
import { getUser, hasPerm } from '../store/auth';
|
||||||
|
|
||||||
export default function CookiePage() {
|
export default function CookiePage() {
|
||||||
const [cookies, setCookies] = useState<any[]>([]);
|
const [cookies, setCookies] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
const user = getUser();
|
const user = getUser();
|
||||||
|
|
||||||
const canView = hasPerm(user, 'cookie:view');
|
const canView = hasPerm(user, 'cookie:view');
|
||||||
@@ -43,10 +44,39 @@ export default function CookiePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopyCookie = (cookie: string, username: string) => {
|
||||||
|
if (!cookie) {
|
||||||
|
message.warning('Cookie 为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(cookie).then(() => {
|
||||||
|
message.success(`已复制 ${username} 的 Cookie`);
|
||||||
|
}).catch(() => {
|
||||||
|
message.error('复制失败');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopySelected = () => {
|
||||||
|
if (selectedRowKeys.length === 0) {
|
||||||
|
message.warning('请先选择 Cookie');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selected = cookies.filter((c) => selectedRowKeys.includes(c.id));
|
||||||
|
const text = selected
|
||||||
|
.map((c) => `${c.account_username}: ${c.cookie || '(空)'}`)
|
||||||
|
.join('\n');
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
message.success(`已复制 ${selected.length} 条 Cookie`);
|
||||||
|
}).catch(() => {
|
||||||
|
message.error('复制失败');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await cookieApi.delete(id);
|
await cookieApi.delete(id);
|
||||||
message.success('已删除');
|
message.success('已删除');
|
||||||
|
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
|
||||||
loadCookies();
|
loadCookies();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e.message);
|
message.error(e.message);
|
||||||
@@ -65,30 +95,47 @@ export default function CookiePage() {
|
|||||||
return <span style={{ fontFamily: 'monospace', fontSize: 12 }}>{val}</span>;
|
return <span style={{ fontFamily: 'monospace', fontSize: 12 }}>{val}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ title: '时间', dataIndex: 'created_at', width: 180 },
|
{ title: '时间', dataIndex: 'created_at', width: 170 },
|
||||||
];
|
{
|
||||||
|
|
||||||
if (canExport) {
|
|
||||||
columns.push({
|
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 80,
|
width: 160,
|
||||||
render: (_: any, record: any) => (
|
render: (_: any, record: any) => (
|
||||||
|
<Space size={4}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => handleCopyCookie(record.cookie, record.account_username)}
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</Button>
|
||||||
|
{canExport && (
|
||||||
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
|
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
|
||||||
<Button danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
<Button danger size="small" icon={<DeleteOutlined />} />
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<h2>Cookie 管理</h2>
|
<h2>Cookie 管理</h2>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={handleCopySelected}
|
||||||
|
disabled={selectedRowKeys.length === 0}
|
||||||
|
>
|
||||||
|
复制选中 {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||||
|
</Button>
|
||||||
{canExport && (
|
{canExport && (
|
||||||
<Button type="primary" icon={<DownloadOutlined />} onClick={handleExport}>
|
<Button type="primary" icon={<DownloadOutlined />} onClick={handleExport}>
|
||||||
导出 CSV
|
导出 CSV
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
@@ -96,6 +143,10 @@ export default function CookiePage() {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Table
|
<Table
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (keys) => setSelectedRowKeys(keys),
|
||||||
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={cookies}
|
dataSource={cookies}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|||||||
Reference in New Issue
Block a user