增加用户中心、用户注册登录
This commit is contained in:
68
src/components/Hooks/useAuthImageUrls.ts
Normal file
68
src/components/Hooks/useAuthImageUrls.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// src/components/Hooks/useAuthImageUrls.ts
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fetchImage } from '@/services/file/api';
|
||||
import { get } from 'lodash';
|
||||
import { getAuthization } from '@/utils/userUtils';
|
||||
|
||||
const useAuthImageUrls = (imageIds: string[]) => {
|
||||
const [imageUrls, setImageUrls] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchImageUrls = async () => {
|
||||
const newImageUrls: Record<string, string> = {};
|
||||
const newLoading: Record<string, boolean> = { ...loading };
|
||||
|
||||
const promises = imageIds
|
||||
.filter(id => !imageUrls[id] && !loading[id]) // 只获取尚未加载或正在加载的图片
|
||||
.map(async (id) => {
|
||||
if(!id) return;
|
||||
newLoading[id] = true;
|
||||
|
||||
try {
|
||||
const {data: response} = await fetchImage(id);
|
||||
if (response) {
|
||||
const objectUrl = URL.createObjectURL(response);
|
||||
newImageUrls[id] = objectUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch image ${id}:`, error);
|
||||
// 如果认证请求失败,使用原始URL作为备选
|
||||
newImageUrls[id] = `/file/image-low-res/${id}?Authorization=${getAuthization()}`;
|
||||
} finally {
|
||||
newLoading[id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (promises.length > 0) {
|
||||
setLoading({ ...newLoading });
|
||||
await Promise.all(promises);
|
||||
setImageUrls(prev => ({ ...prev, ...newImageUrls }));
|
||||
setLoading(prev => {
|
||||
const updatedLoading = { ...prev };
|
||||
Object.keys(newImageUrls).forEach(key => {
|
||||
delete updatedLoading[key];
|
||||
});
|
||||
return updatedLoading;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchImageUrls();
|
||||
}, [imageIds, imageUrls, loading]);
|
||||
|
||||
// 组件卸载时清理Object URLs以避免内存泄漏
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
Object.values(imageUrls).forEach(url => {
|
||||
if (url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [imageUrls]);
|
||||
|
||||
return { imageUrls, loading };
|
||||
};
|
||||
|
||||
export default useAuthImageUrls;
|
||||
56
src/components/Hooks/useFetchHighResImageUrl.ts
Normal file
56
src/components/Hooks/useFetchHighResImageUrl.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// src/components/Hooks/useFetchHighResImageUrl.ts
|
||||
import { useRequest } from '@umijs/max';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { fetchImage } from '@/services/file/api';
|
||||
|
||||
// 简单内存缓存,避免重复请求同一张图片
|
||||
const highResImageUrlCache = new Map<string, string>();
|
||||
|
||||
const useFetchHighResImageUrl = (imageInstanceId: string) => {
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const retryRef = useRef(0);
|
||||
const { run, loading } = useRequest(
|
||||
() => fetchImage(imageInstanceId),
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
const objectUrl = URL.createObjectURL(data);
|
||||
highResImageUrlCache.set(imageInstanceId, objectUrl);
|
||||
setImageUrl(objectUrl);
|
||||
} else {
|
||||
setImageUrl(null);
|
||||
}
|
||||
retryRef.current = 0;
|
||||
},
|
||||
onError: () => {
|
||||
if (retryRef.current < 2) {
|
||||
retryRef.current += 1;
|
||||
run();
|
||||
} else {
|
||||
setImageUrl(null);
|
||||
highResImageUrlCache.set(imageInstanceId, "error");
|
||||
retryRef.current = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (imageInstanceId) {
|
||||
retryRef.current = 0;
|
||||
const cached = highResImageUrlCache.get(imageInstanceId);
|
||||
if (cached && cached !== "error") {
|
||||
setImageUrl(cached);
|
||||
} else if (cached !== "error") {
|
||||
run();
|
||||
}
|
||||
} else {
|
||||
setImageUrl(null);
|
||||
}
|
||||
}, [imageInstanceId, run]);
|
||||
|
||||
return { imageUrl, loading };
|
||||
};
|
||||
|
||||
export default useFetchHighResImageUrl;
|
||||
@@ -1,31 +1,54 @@
|
||||
import { useRequest } from '@umijs/max';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {fetchImageLowRes} from "@/services/file/api";
|
||||
|
||||
// 简单内存缓存,避免因虚拟滚动重复请求同一张图片
|
||||
const imageUrlCache = new Map<string, string>();
|
||||
|
||||
const useFetchImageUrl = (imageInstanceId: string) => {
|
||||
const [imageUrl, setImageUrl] = useState("error");
|
||||
const retryRef = useRef(0);
|
||||
const { data: response, run, loading } = useRequest(
|
||||
() => {
|
||||
return fetchImageLowRes(imageInstanceId);
|
||||
},
|
||||
() => fetchImageLowRes(imageInstanceId),
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
const objectUrl = URL.createObjectURL(data);
|
||||
imageUrlCache.set(imageInstanceId, objectUrl);
|
||||
setImageUrl(objectUrl);
|
||||
} else {
|
||||
setImageUrl("error");
|
||||
}
|
||||
retryRef.current = 0;
|
||||
},
|
||||
onError: () => {
|
||||
if (retryRef.current < 2) {
|
||||
retryRef.current += 1;
|
||||
run();
|
||||
} else {
|
||||
setImageUrl("error");
|
||||
imageUrlCache.set(imageInstanceId, "error");
|
||||
retryRef.current = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (response) {
|
||||
setImageUrl(URL.createObjectURL(response));
|
||||
if (imageInstanceId) {
|
||||
retryRef.current = 0;
|
||||
const cached = imageUrlCache.get(imageInstanceId);
|
||||
if (cached) {
|
||||
setImageUrl(cached);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
} else {
|
||||
setImageUrl("error");
|
||||
}
|
||||
}, [response]);
|
||||
useEffect(() => {
|
||||
if (imageInstanceId) {
|
||||
run();
|
||||
}
|
||||
}, [imageInstanceId]);
|
||||
}, [imageInstanceId, run]);
|
||||
|
||||
return {imageUrl, loading};
|
||||
};
|
||||
export default useFetchImageUrl;
|
||||
|
||||
216
src/components/Hooks/useWebSocket.ts
Normal file
216
src/components/Hooks/useWebSocket.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { getAuthization } from '@/utils/userUtils';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import SockJS from 'sockjs-client';
|
||||
import { Client, IMessage } from '@stomp/stompjs';
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: 'chat' | 'system' | 'notification' | 'friend' | 'error';
|
||||
content?: string;
|
||||
sender?: string;
|
||||
title?: string;
|
||||
timestamp?: Date;
|
||||
[key: string]: any; // 允许其他属性
|
||||
}
|
||||
|
||||
interface WebSocketOptions {
|
||||
headers?: Record<string, string>;
|
||||
protocols?: string | string[];
|
||||
}
|
||||
|
||||
const useWebSocket = (url: string, options?: WebSocketOptions) => {
|
||||
const [messages, setMessages] = useState<WebSocketMessage[]>([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
const stompClient = useRef<Client | null>(null);
|
||||
const reconnectAttempts = useRef(0);
|
||||
const maxReconnectAttempts = 5;
|
||||
const reconnectDelay = 3000;
|
||||
|
||||
// 发送聊天消息
|
||||
const sendChatMessage = useCallback((toUserId: string, content: string) => {
|
||||
if (stompClient.current && stompClient.current.connected) {
|
||||
stompClient.current.publish({
|
||||
destination: '/app/chat/send',
|
||||
body: JSON.stringify({
|
||||
toUserId,
|
||||
content,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
});
|
||||
} else {
|
||||
setError(new Error('WebSocket连接未建立'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 发送通用消息
|
||||
const sendMessage = useCallback((destination: string, body: any) => {
|
||||
if (stompClient.current && stompClient.current.connected) {
|
||||
stompClient.current.publish({
|
||||
destination,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
} else {
|
||||
setError(new Error('WebSocket连接未建立'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 处理错误
|
||||
const handleError = useCallback((error: Event) => {
|
||||
setError(new Error(`WebSocket错误: ${(error as any).message || error.type}`));
|
||||
}, []);
|
||||
|
||||
// 处理连接关闭
|
||||
const handleClose = useCallback((event: CloseEvent) => {
|
||||
setIsConnected(false);
|
||||
console.log(`连接关闭: ${event.code} ${event.reason}`);
|
||||
|
||||
// 尝试重连
|
||||
if (reconnectAttempts.current < maxReconnectAttempts) {
|
||||
const delay = Math.pow(2, reconnectAttempts.current) * reconnectDelay;
|
||||
setTimeout(() => {
|
||||
reconnectAttempts.current++;
|
||||
connect();
|
||||
}, delay);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 连接WebSocket
|
||||
const connect = useCallback(() => {
|
||||
try {
|
||||
// 构造带有查询参数的URL
|
||||
const urlObj = new URL(url, window.location.href);
|
||||
|
||||
// 添加认证信息到查询参数
|
||||
const token = getAuthization();
|
||||
urlObj.searchParams.append('Authorization', token);
|
||||
|
||||
// 添加其他 headers 作为查询参数
|
||||
if (options?.headers) {
|
||||
Object.entries(options.headers).forEach(([key, value]) => {
|
||||
urlObj.searchParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
const finalUrl = urlObj.toString();
|
||||
|
||||
// 关闭之前的连接
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
}
|
||||
|
||||
// 创建WebSocket连接
|
||||
ws.current = new SockJS(finalUrl, options?.protocols as any);
|
||||
|
||||
// 创建STOMP客户端
|
||||
stompClient.current = new Client({
|
||||
webSocketFactory: () => ws.current as WebSocket,
|
||||
connectHeaders: {
|
||||
Authorization: getAuthization(),
|
||||
},
|
||||
debug: function (str) {
|
||||
console.log('[STOMP]', str);
|
||||
},
|
||||
reconnectDelay: 5000,
|
||||
heartbeatIncoming: 4000,
|
||||
heartbeatOutgoing: 4000,
|
||||
});
|
||||
|
||||
// 连接成功的回调
|
||||
stompClient.current.onConnect = () => {
|
||||
setIsConnected(true);
|
||||
reconnectAttempts.current = 0;
|
||||
console.log('WebSocket连接已建立');
|
||||
|
||||
// 订阅用户聊天消息
|
||||
stompClient.current?.subscribe('/user/queue/chat', (message: IMessage) => {
|
||||
try {
|
||||
const chatMessage = JSON.parse(message.body);
|
||||
setMessages(prev => [...prev, {
|
||||
type: 'chat',
|
||||
content: chatMessage.content,
|
||||
senderId: chatMessage.fromUserId,
|
||||
senderName: chatMessage.fromUserName,
|
||||
status: chatMessage.status,
|
||||
timestamp: new Date(chatMessage.timestamp || Date.now())
|
||||
}]);
|
||||
} catch (e) {
|
||||
console.error('解析聊天消息失败:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 订阅好友通知
|
||||
stompClient.current?.subscribe('/user/queue/friend', (message: IMessage) => {
|
||||
try {
|
||||
const friendMessage = JSON.parse(message.body);
|
||||
console.log(message.body);
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
type: 'friend',
|
||||
content: `好友请求: ${friendMessage.type}`,
|
||||
senderId: friendMessage.fromUserId,
|
||||
senderName: friendMessage.fromUserName,
|
||||
status: friendMessage.status,
|
||||
timestamp: new Date()
|
||||
}]);
|
||||
} catch (e) {
|
||||
console.error('解析好友消息失败:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 订阅系统通知
|
||||
stompClient.current?.subscribe('/user/queue/notification', (message: IMessage) => {
|
||||
try {
|
||||
const notification = JSON.parse(message.body);
|
||||
console.log('notification', message);
|
||||
setMessages(prev => [...prev, {
|
||||
type: 'notification',
|
||||
title: notification.title,
|
||||
content: notification.message || notification.content,
|
||||
status: notification.status,
|
||||
timestamp: new Date(notification.timestamp || Date.now())
|
||||
}]);
|
||||
} catch (e) {
|
||||
console.error('解析通知消息失败:', e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// STOMP错误处理
|
||||
stompClient.current.onStompError = (frame) => {
|
||||
console.error('Broker reported error: ' + frame.headers['message']);
|
||||
console.error('Additional details: ' + frame.body);
|
||||
setError(new Error(`STOMP错误: ${frame.headers['message']}`));
|
||||
};
|
||||
|
||||
// 激活客户端
|
||||
stompClient.current.activate();
|
||||
|
||||
} catch (e) {
|
||||
setError(e as Error);
|
||||
}
|
||||
}, [url, options]);
|
||||
|
||||
// 初始化连接
|
||||
useEffect(() => {
|
||||
connect();
|
||||
return () => {
|
||||
if (stompClient.current) {
|
||||
stompClient.current.deactivate();
|
||||
}
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
}
|
||||
};
|
||||
}, [connect]);
|
||||
|
||||
return {
|
||||
messages,
|
||||
sendChatMessage,
|
||||
sendMessage,
|
||||
isConnected,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default useWebSocket;
|
||||
@@ -1,12 +1,13 @@
|
||||
import { outLogin } from '@/services/ant-design-pro/api';
|
||||
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { history, useModel } from '@umijs/max';
|
||||
import { Spin } from 'antd';
|
||||
import { Spin, message } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { stringify } from 'querystring';
|
||||
import React, { useCallback } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import HeaderDropdown from '../HeaderDropdown';
|
||||
import { logoutUser } from '@/services/user/api';
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
menu?: boolean;
|
||||
@@ -16,7 +17,7 @@ export type GlobalHeaderRightProps = {
|
||||
export const AvatarName = () => {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const { currentUser } = initialState || {};
|
||||
return <span className="anticon">{currentUser?.name}</span>;
|
||||
return <span className="anticon">{currentUser?.username}</span>;
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
@@ -42,7 +43,13 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, childre
|
||||
* 退出登录,并且将当前的 url 保存
|
||||
*/
|
||||
const loginOut = async () => {
|
||||
await outLogin();
|
||||
const response = await logoutUser();
|
||||
console.log(response);
|
||||
if (response.code !== 200) {
|
||||
message.error(response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const { search, pathname } = window.location;
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
/** 此方法会跳转到 redirect 参数所在的位置 */
|
||||
@@ -94,7 +101,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, childre
|
||||
|
||||
const { currentUser } = initialState;
|
||||
|
||||
if (!currentUser || !currentUser.name) {
|
||||
if (!currentUser || !currentUser.username) {
|
||||
return loading;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import useFetchImageUrl from '@/components/Hooks/useFetchImageUrl';
|
||||
import useFetchHighResImageUrl from '@/components/Hooks/useFetchHighResImageUrl';
|
||||
import { Image } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { getAuthization } from '@/utils/userUtils';
|
||||
|
||||
interface ImageItem {
|
||||
instanceId: string;
|
||||
@@ -32,28 +34,22 @@ const TimelineImage: React.FC<Props> = (props) => {
|
||||
style,
|
||||
} = props;
|
||||
|
||||
const { imageUrl, loading } = useFetchImageUrl(imageInstanceId ?? '');
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
/* const { imageUrl, loading } = useFetchImageUrl(imageInstanceId ?? '');
|
||||
const [previewVisible, setPreviewVisible] = useState(false); */
|
||||
|
||||
// 构建预览列表
|
||||
imageList.map((item) => ({
|
||||
src: item.instanceId,
|
||||
title: item.imageName,
|
||||
}));
|
||||
// 预览配置
|
||||
const previewConfig = {
|
||||
visible: previewVisible,
|
||||
onVisibleChange: (visible: boolean) => setPreviewVisible(visible),
|
||||
current: currentIndex,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tl-image-container" style={{ width, height }}>
|
||||
<Image
|
||||
loading="lazy"
|
||||
src={src ?? `/file/image-low-res/${imageInstanceId}`}
|
||||
src={src ?? `/file/image-low-res/${imageInstanceId}?Authorization=${getAuthization()}`}
|
||||
preview={{
|
||||
src: `/file/image/${imageInstanceId}`,
|
||||
src: `/file/image/${imageInstanceId}?Authorization=${getAuthization()}`,
|
||||
}}
|
||||
height={height}
|
||||
width={width}
|
||||
@@ -68,4 +64,4 @@ const TimelineImage: React.FC<Props> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineImage;
|
||||
export default TimelineImage;
|
||||
@@ -10,3 +10,9 @@ import { Question, SelectLang } from './RightContent';
|
||||
import { AvatarDropdown, AvatarName } from './RightContent/AvatarDropdown';
|
||||
|
||||
export { Footer, Question, SelectLang, AvatarDropdown, AvatarName };
|
||||
|
||||
// 导出Hooks
|
||||
export { default as useFetchImageUrl } from './Hooks/useFetchImageUrl';
|
||||
export { default as useWebSocket } from './Hooks/useWebSocket';
|
||||
export { default as useFetchHighResImageUrl } from './Hooks/useFetchHighResImageUrl';
|
||||
export { default as useAuthImageUrls } from './Hooks/useAuthImageUrls';
|
||||
Reference in New Issue
Block a user