mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-15 08:25:10 +08:00
fix: 优化前端安全与生命周期清理
- 登录页记住我仅保存用户名和 rememberMe,并清理历史明文密码缓存 - 修复下载 Blob 链接释放时机,提升浏览器兼容性 - 关闭消息推送时清理事件处理器并阻止重复重连 - 稳定流程审批组件 variables 默认值,避免无效重复加载 - 忽略 IDE 本地配置文件 - 删除未使用的 RightToolbar 组件
This commit is contained in:
parent
cf53df4f4a
commit
d5e1b42ad1
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ src/.umi
|
|||||||
src/.umi-production
|
src/.umi-production
|
||||||
.env.local
|
.env.local
|
||||||
*.log
|
*.log
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -44,11 +44,13 @@ interface SubmitVerifyProps {
|
|||||||
onSubmitted: () => void;
|
onSubmitted: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyVariables: Record<string, unknown> = {};
|
||||||
|
|
||||||
function buttonVisible(task?: FlowTaskVO, code?: string) {
|
function buttonVisible(task?: FlowTaskVO, code?: string) {
|
||||||
return !!task?.buttonList?.find(item => item.code === code && item.show);
|
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<{
|
const [approveForm] = Form.useForm<{
|
||||||
message?: string;
|
message?: string;
|
||||||
messageType?: string[];
|
messageType?: string[];
|
||||||
|
|||||||
@ -40,10 +40,9 @@ function getRememberedLogin() {
|
|||||||
if (typeof window === 'undefined') return { rememberMe: false };
|
if (typeof window === 'undefined') return { rememberMe: false };
|
||||||
const rememberMe = localStorage.getItem('rememberMe') === 'true';
|
const rememberMe = localStorage.getItem('rememberMe') === 'true';
|
||||||
const username = localStorage.getItem('username') || undefined;
|
const username = localStorage.getItem('username') || undefined;
|
||||||
const password = localStorage.getItem('password') || undefined;
|
|
||||||
return {
|
return {
|
||||||
username: rememberMe ? username : 'admin',
|
username: rememberMe ? username : 'admin',
|
||||||
password: rememberMe ? password : 'admin123',
|
password: rememberMe ? undefined : 'admin123',
|
||||||
rememberMe
|
rememberMe
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -172,13 +171,12 @@ export default function Login() {
|
|||||||
try {
|
try {
|
||||||
if (values.rememberMe) {
|
if (values.rememberMe) {
|
||||||
localStorage.setItem('username', String(values.username || ''));
|
localStorage.setItem('username', String(values.username || ''));
|
||||||
localStorage.setItem('password', String(values.password || ''));
|
|
||||||
localStorage.setItem('rememberMe', 'true');
|
localStorage.setItem('rememberMe', 'true');
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('username');
|
localStorage.removeItem('username');
|
||||||
localStorage.removeItem('password');
|
|
||||||
localStorage.removeItem('rememberMe');
|
localStorage.removeItem('rememberMe');
|
||||||
}
|
}
|
||||||
|
localStorage.removeItem('password');
|
||||||
const res = await login({
|
const res = await login({
|
||||||
username: values.username,
|
username: values.username,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
|
|||||||
@ -32,8 +32,11 @@ export function saveBlob(blob: Blob, fileName: string) {
|
|||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = objectUrl;
|
link.href = objectUrl;
|
||||||
link.download = fileName;
|
link.download = fileName;
|
||||||
|
link.style.display = 'none';
|
||||||
|
document.body.appendChild(link);
|
||||||
link.click();
|
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) {
|
export async function saveValidatedBlob(blob: Blob, fileName: string) {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ let eventSource: EventSource | undefined;
|
|||||||
let webSocket: WebSocket | undefined;
|
let webSocket: WebSocket | undefined;
|
||||||
let reconnectTimer: number | undefined;
|
let reconnectTimer: number | undefined;
|
||||||
let reconnectAttempts = 0;
|
let reconnectAttempts = 0;
|
||||||
|
let pushClosed = true;
|
||||||
|
|
||||||
function messageEnabled() {
|
function messageEnabled() {
|
||||||
return appEnv.messageEnabled;
|
return appEnv.messageEnabled;
|
||||||
@ -91,6 +92,7 @@ function clearReconnectTimer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleReconnect(connect: () => void, maxRetries: number, delay: number) {
|
function scheduleReconnect(connect: () => void, maxRetries: number, delay: number) {
|
||||||
|
if (pushClosed) return;
|
||||||
if (reconnectAttempts >= maxRetries) return;
|
if (reconnectAttempts >= maxRetries) return;
|
||||||
reconnectAttempts += 1;
|
reconnectAttempts += 1;
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
@ -99,6 +101,7 @@ function scheduleReconnect(connect: () => void, maxRetries: number, delay: numbe
|
|||||||
|
|
||||||
function initSsePush(path: string) {
|
function initSsePush(path: string) {
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
|
if (pushClosed) return;
|
||||||
eventSource?.close();
|
eventSource?.close();
|
||||||
eventSource = new EventSource(buildHttpUrl(path));
|
eventSource = new EventSource(buildHttpUrl(path));
|
||||||
eventSource.onopen = () => {
|
eventSource.onopen = () => {
|
||||||
@ -117,6 +120,7 @@ function initSsePush(path: string) {
|
|||||||
|
|
||||||
function initWsPush(path: string) {
|
function initWsPush(path: string) {
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
|
if (pushClosed) return;
|
||||||
webSocket?.close();
|
webSocket?.close();
|
||||||
webSocket = new WebSocket(buildWsUrl(path));
|
webSocket = new WebSocket(buildWsUrl(path));
|
||||||
webSocket.onopen = () => {
|
webSocket.onopen = () => {
|
||||||
@ -149,6 +153,7 @@ export async function initMessageBox() {
|
|||||||
export function initPush() {
|
export function initPush() {
|
||||||
closePush();
|
closePush();
|
||||||
if (!messageEnabled()) return;
|
if (!messageEnabled()) return;
|
||||||
|
pushClosed = false;
|
||||||
if (appEnv.messageTransport === 'websocket') {
|
if (appEnv.messageTransport === 'websocket') {
|
||||||
initWsPush(appEnv.messagePath);
|
initWsPush(appEnv.messagePath);
|
||||||
return;
|
return;
|
||||||
@ -157,7 +162,19 @@ export function initPush() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function closePush() {
|
export function closePush() {
|
||||||
|
pushClosed = true;
|
||||||
clearReconnectTimer();
|
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();
|
eventSource?.close();
|
||||||
webSocket?.close();
|
webSocket?.close();
|
||||||
eventSource = undefined;
|
eventSource = undefined;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user