fix: 优化前端安全与生命周期清理

- 登录页记住我仅保存用户名和 rememberMe,并清理历史明文密码缓存
- 修复下载 Blob 链接释放时机,提升浏览器兼容性
- 关闭消息推送时清理事件处理器并阻止重复重连
- 稳定流程审批组件 variables 默认值,避免无效重复加载
- 忽略 IDE 本地配置文件
- 删除未使用的 RightToolbar 组件
This commit is contained in:
疯狂的狮子Li 2026-06-08 17:58:52 +08:00
parent cf53df4f4a
commit d5e1b42ad1
6 changed files with 28 additions and 115 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ src/.umi
src/.umi-production
.env.local
*.log
.idea/
*.iml

View File

@ -1,109 +0,0 @@
import { MenuOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { Button, Checkbox, Popover, Space, Tooltip } from 'antd';
import { useEffect, useMemo } from 'react';
export interface RightToolbarColumn {
key: string;
label: string;
visible?: boolean;
}
export interface RightToolbarProps {
showSearch?: boolean;
onShowSearchChange?: (value: boolean) => void;
search?: boolean;
columns?: RightToolbarColumn[];
onColumnsChange?: (columns: RightToolbarColumn[]) => void;
onRefresh?: () => void;
storageKey?: string;
gutter?: number;
}
function readStorage(storageKey?: string) {
if (!storageKey) {
return undefined;
}
try {
const text = localStorage.getItem(storageKey);
return text ? (JSON.parse(text) as Record<string, boolean>) : undefined;
} catch {
return undefined;
}
}
function writeStorage(storageKey: string | undefined, columns: RightToolbarColumn[]) {
if (!storageKey) {
return;
}
const state = Object.fromEntries(columns.map(column => [column.key, column.visible !== false]));
localStorage.setItem(storageKey, JSON.stringify(state));
}
export default function RightToolbar({
showSearch = true,
onShowSearchChange,
search = true,
columns,
onColumnsChange,
onRefresh,
storageKey,
gutter = 10
}: RightToolbarProps) {
const checkedKeys = useMemo(
() => (columns || []).filter(column => column.visible !== false).map(column => column.key),
[columns]
);
// biome-ignore lint/correctness/useExhaustiveDependencies: storage restore should run when storage key changes, not on every parent columns render.
useEffect(() => {
const stored = readStorage(storageKey);
if (!stored || !columns?.length) {
return;
}
const nextColumns = columns.map(column => ({ ...column, visible: stored[column.key] ?? column.visible ?? true }));
onColumnsChange?.(nextColumns);
}, [storageKey]);
const changeColumns = (keys: string[]) => {
const nextColumns = (columns || []).map(column => ({ ...column, visible: keys.includes(column.key) }));
writeStorage(storageKey, nextColumns);
onColumnsChange?.(nextColumns);
};
return (
<Space size={8} className="right-toolbar-react" style={{ marginRight: gutter / 2 }}>
{search && (
<Tooltip title={showSearch ? '隐藏搜索' : '显示搜索'}>
<Button shape="circle" icon={<SearchOutlined />} onClick={() => onShowSearchChange?.(!showSearch)} />
</Tooltip>
)}
<Tooltip title="刷新">
<Button shape="circle" icon={<ReloadOutlined />} onClick={onRefresh} />
</Tooltip>
{columns?.length ? (
<Popover
placement="bottomRight"
trigger="click"
content={
<div className="right-toolbar-column-popover">
<div className="right-toolbar-column-title">/</div>
<Checkbox.Group value={checkedKeys} onChange={values => changeColumns(values as string[])}>
<Space direction="vertical" size={6}>
{columns.map(column => (
<Checkbox key={column.key} value={column.key}>
{column.label}
</Checkbox>
))}
</Space>
</Checkbox.Group>
</div>
}
>
<Tooltip title="显示/隐藏列">
<Button shape="circle" icon={<MenuOutlined />} />
</Tooltip>
</Popover>
) : null}
</Space>
);
}

View File

@ -44,11 +44,13 @@ interface SubmitVerifyProps {
onSubmitted: () => void;
}
const emptyVariables: Record<string, unknown> = {};
function buttonVisible(task?: FlowTaskVO, code?: string) {
return !!task?.buttonList?.find(item => item.code === code && item.show);
}
export default function SubmitVerify({ open, taskId, variables = {}, onOpenChange, onSubmitted }: SubmitVerifyProps) {
export default function SubmitVerify({ open, taskId, variables = emptyVariables, onOpenChange, onSubmitted }: SubmitVerifyProps) {
const [approveForm] = Form.useForm<{
message?: string;
messageType?: string[];

View File

@ -40,10 +40,9 @@ function getRememberedLogin() {
if (typeof window === 'undefined') return { rememberMe: false };
const rememberMe = localStorage.getItem('rememberMe') === 'true';
const username = localStorage.getItem('username') || undefined;
const password = localStorage.getItem('password') || undefined;
return {
username: rememberMe ? username : 'admin',
password: rememberMe ? password : 'admin123',
password: rememberMe ? undefined : 'admin123',
rememberMe
};
}
@ -172,13 +171,12 @@ export default function Login() {
try {
if (values.rememberMe) {
localStorage.setItem('username', String(values.username || ''));
localStorage.setItem('password', String(values.password || ''));
localStorage.setItem('rememberMe', 'true');
} else {
localStorage.removeItem('username');
localStorage.removeItem('password');
localStorage.removeItem('rememberMe');
}
localStorage.removeItem('password');
const res = await login({
username: values.username,
password: values.password,

View File

@ -32,8 +32,11 @@ export function saveBlob(blob: Blob, fileName: string) {
const link = document.createElement('a');
link.href = objectUrl;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(objectUrl);
document.body.removeChild(link);
window.setTimeout(() => window.URL.revokeObjectURL(objectUrl), 0);
}
export async function saveValidatedBlob(blob: Blob, fileName: string) {

View File

@ -13,6 +13,7 @@ let eventSource: EventSource | undefined;
let webSocket: WebSocket | undefined;
let reconnectTimer: number | undefined;
let reconnectAttempts = 0;
let pushClosed = true;
function messageEnabled() {
return appEnv.messageEnabled;
@ -91,6 +92,7 @@ function clearReconnectTimer() {
}
function scheduleReconnect(connect: () => void, maxRetries: number, delay: number) {
if (pushClosed) return;
if (reconnectAttempts >= maxRetries) return;
reconnectAttempts += 1;
clearReconnectTimer();
@ -99,6 +101,7 @@ function scheduleReconnect(connect: () => void, maxRetries: number, delay: numbe
function initSsePush(path: string) {
const connect = () => {
if (pushClosed) return;
eventSource?.close();
eventSource = new EventSource(buildHttpUrl(path));
eventSource.onopen = () => {
@ -117,6 +120,7 @@ function initSsePush(path: string) {
function initWsPush(path: string) {
const connect = () => {
if (pushClosed) return;
webSocket?.close();
webSocket = new WebSocket(buildWsUrl(path));
webSocket.onopen = () => {
@ -149,6 +153,7 @@ export async function initMessageBox() {
export function initPush() {
closePush();
if (!messageEnabled()) return;
pushClosed = false;
if (appEnv.messageTransport === 'websocket') {
initWsPush(appEnv.messagePath);
return;
@ -157,7 +162,19 @@ export function initPush() {
}
export function closePush() {
pushClosed = true;
clearReconnectTimer();
if (webSocket) {
webSocket.onopen = null;
webSocket.onmessage = null;
webSocket.onclose = null;
webSocket.onerror = null;
}
if (eventSource) {
eventSource.onopen = null;
eventSource.onmessage = null;
eventSource.onerror = null;
}
eventSource?.close();
webSocket?.close();
eventSource = undefined;