refactor(api): 统一API路径配置并优化相关服务调用
All checks were successful
test/timeline-frontend/pipeline/head This commit looks good

feat: 添加API URL全局配置文件
fix: 修复SSR环境下的窗口对象检查
perf: 优化代理配置路径匹配顺序
style: 移除无用注释和未使用的类型声明
This commit is contained in:
2026-02-27 10:07:03 +08:00
parent 97e4a135e1
commit c1a88ea4da
29 changed files with 392 additions and 225 deletions

View File

@@ -160,56 +160,56 @@ export default defineConfig({
* @name Webpack configuration
* @description Custom webpack configuration for code splitting and optimization
*/
chainWebpack: (config: any) => {
// Ignore Windows system files to prevent Watchpack errors
config.watchOptions({
ignored: [
'**/node_modules',
'**/.git',
'**/C:/DumpStack.log.tmp',
'**/C:/pagefile.sys',
'**/C:/swapfile.sys',
'**/C:/System Volume Information',
'**/C:/$*', // Ignore all system files starting with $
],
});
// chainWebpack: (config: any) => {
// // Ignore Windows system files to prevent Watchpack errors
// config.watchOptions({
// ignored: [
// '**/node_modules',
// '**/.git',
// '**/C:/DumpStack.log.tmp',
// '**/C:/pagefile.sys',
// '**/C:/swapfile.sys',
// '**/C:/System Volume Information',
// '**/C:/$*', // Ignore all system files starting with $
// ],
// });
// Split large vendor libraries into separate chunks
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
// Ant Design and related libraries
antd: {
name: 'antd',
test: /[\\/]node_modules[\\/](@ant-design|antd|@antv|rc-)/,
priority: 20,
},
// React and related libraries
react: {
name: 'react',
test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)/,
priority: 20,
},
// Other vendor libraries
vendors: {
name: 'vendors',
test: /[\\/]node_modules[\\/]/,
priority: 10,
},
},
});
// // Split large vendor libraries into separate chunks
// config.optimization.splitChunks({
// chunks: 'all',
// cacheGroups: {
// // Ant Design and related libraries
// antd: {
// name: 'antd',
// test: /[\\/]node_modules[\\/](@ant-design|antd|@antv|rc-)/,
// priority: 20,
// },
// // React and related libraries
// react: {
// name: 'react',
// test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)/,
// priority: 20,
// },
// // Other vendor libraries
// vendors: {
// name: 'vendors',
// test: /[\\/]node_modules[\\/]/,
// priority: 10,
// },
// },
// });
// Optimize images
config.module
.rule('images')
.test(/\.(png|jpe?g|gif|webp|svg)$/)
.use('url-loader')
.loader('url-loader')
.options({
limit: 8192, // Inline images smaller than 8KB
name: 'static/images/[name].[hash:8].[ext]',
});
// // Optimize images
// config.module
// .rule('images')
// .test(/\.(png|jpe?g|gif|webp|svg)$/)
// .use('url-loader')
// .loader('url-loader')
// .options({
// limit: 8192, // Inline images smaller than 8KB
// name: 'static/images/[name].[hash:8].[ext]',
// });
return config;
},
// return config;
// },
});

View File

@@ -15,13 +15,22 @@ export default {
// 如果需要自定义本地开发服务器 请取消注释按需调整
dev: {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
// 注意:更具体的路径应该放在前面,避免被通用路径覆盖
'/api/story/': {
// 要代理的地址
target: basePath,
// 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true,
pathRewrite: { '^/api/story': '/api/story' },
pathRewrite: { '^/api/story': '/story' },
},
'/api/user/': {
// 要代理的地址
target: basePath,
// 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true,
pathRewrite: { '^/api/user': '/user' },
},
'/file/': {
// 要代理的地址
@@ -39,10 +48,11 @@ export default {
changeOrigin: true,
pathRewrite: { '^/user-api': '/user' },
},
// 通用 /api/ 代理放在最后,避免覆盖上面的具体配置
'/api/': {
target: 'https://proapi.azurewebsites.net',
target: basePath,
changeOrigin: true,
pathRewrite: { '^': '' },
pathRewrite: { '^/api': '/api' },
},
},

View File

@@ -8,7 +8,6 @@ import { history, Link, useModel } from '@umijs/max';
import { useIsMobile } from '@/hooks/useIsMobile';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
import { useNotifications } from '@/hooks/useNotifications';
import BottomNav from '@/components/BottomNav';
import SyncStatusIndicator from '@/components/SyncStatus/SyncStatusIndicator';
@@ -32,22 +31,23 @@ function LayoutChildrenWrapper({
setInitialState: any;
}) {
const isMobile = useIsMobile();
const { effectiveTheme, getCurrentColorScheme, fetchThemePreferences } = useModel('theme');
// 暂时注释掉 theme 相关代码,避免 useModel 调用导致问题
const { effectiveTheme, getCurrentColorScheme, fetchThemePreferences } = useModel('theme');
useEffect(() => {
fetchThemePreferences();
}, [fetchThemePreferences]);
const colorScheme = getCurrentColorScheme();
// 暂时注释掉 useNotifications避免 useModel 调用导致问题
useNotifications();
return (
<ConfigProvider
theme={{
algorithm: effectiveTheme === 'dark' ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
algorithm: antdTheme.defaultAlgorithm,
token: {
colorPrimary: colorScheme.primaryColor,
colorPrimary: '#1890ff',
},
}}
>
@@ -77,24 +77,26 @@ export async function getInitialState(): Promise<{
currentUser?: API.CurrentUser;
loading?: boolean;
}> {
const fetchUserInfo = async () => {
try {
const msg = await queryCurrentUser({ skipErrorHandler: true });
return msg.data;
} catch (error) {
// history.push(loginPath);
// 从 localStorage 获取用户信息
const getUserFromStorage = () => {
if (typeof window !== 'undefined') {
const userStr = localStorage.getItem('timeline_user');
if (userStr) {
try {
return JSON.parse(userStr);
} catch (e) {
return undefined;
}
}
}
return undefined;
};
if (history.location.pathname !== loginPath) {
const currentUser = await fetchUserInfo();
return {
currentUser,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
// 直接从 localStorage 读取用户信息避免API调用阻塞页面渲染
const currentUser = getUserFromStorage();
return {
currentUser,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
@@ -103,8 +105,8 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
return {
actionsRender: () => [
<ClientOnly key="client-only-actions">
<SyncStatusIndicator key="sync-status" />,
<Question key="doc" />,
<SyncStatusIndicator key="sync-status" />
<Question key="doc" />
<SelectLang key="SelectLang" />
</ClientOnly>,
],

View File

@@ -48,6 +48,9 @@ const CommentList: React.FC<CommentListProps> = ({
}) => {
// Sort comments chronologically (oldest first)
const sortedComments = useMemo(() => {
if (!comments || !Array.isArray(comments)) {
return [];
}
return [...comments].sort((a, b) => {
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
});

View File

@@ -3,6 +3,7 @@ import { useEffect, useState, useCallback } from 'react';
import { useModel, request } from '@umijs/max';
import { notification as antdNotification } from 'antd';
import { Notification, NotificationType } from '@/types';
import { AUTH_API, USER_API } from '@/services/config/apiUrls';
export const useNotifications = () => {
const { initialState } = useModel('@@initialState');
@@ -13,7 +14,7 @@ export const useNotifications = () => {
const fetchUnreadNotifications = useCallback(async () => {
if (!initialState?.currentUser) return;
try {
const res = await request<Notification[]>('/user-api/message/unread');
const res = await request<Notification[]>(`${AUTH_API.INFO}/message/unread`);
setNotifications(res);
setUnreadCount(res.length);
} catch (error) {
@@ -53,7 +54,7 @@ export const useNotifications = () => {
const markAsRead = useCallback(async (ids: number[]) => {
try {
await request('/api/user/notifications/read', {
await request(USER_API.NOTIFICATIONS_READ, {
method: 'POST',
data: ids,
});
@@ -79,9 +80,14 @@ export const useNotifications = () => {
case NotificationType.SYSTEM:
return '系统通知';
default:
return '系统通知';
return '通知';
}
};
return { notifications, unreadCount, markAsRead };
return {
notifications,
unreadCount,
markAsRead,
fetchUnreadNotifications,
};
};

View File

@@ -156,7 +156,7 @@ export function useReactions(
// Helpers
hasReaction: !!userReaction,
totalReactions: reactions
totalReactions: reactions?.counts
? Object.values(reactions.counts).reduce((sum, count) => sum + count, 0)
: 0,
};

View File

@@ -15,7 +15,7 @@ export interface SyncState {
export default () => {
const [syncState, setSyncState] = useState<SyncState>({
isOnline: navigator.onLine,
isOnline: typeof window !== 'undefined' ? navigator.onLine : true,
isSyncing: false,
pendingChanges: 0,
lastSyncTime: undefined,
@@ -24,6 +24,8 @@ export default () => {
// Update online status
useEffect(() => {
if (typeof window === 'undefined') return;
const handleOnline = () => {
setSyncState((prev) => ({ ...prev, isOnline: true, syncError: undefined }));
// Automatically trigger sync when coming online

View File

@@ -1,19 +1,20 @@
import { request } from '@umijs/max';
import type { CurrentUser, FriendUser, HistoryMessage, ListItemDataType, MessageItem, UserInfo } from './data.d';
import { CommonResponse } from '@/types/common';
import { AUTH_API, STORY_API } from '@/services/config/apiUrls';
export async function queryCurrent(): Promise<{ data: CurrentUser }> {
return request('/user-api/info');
return request(AUTH_API.INFO);
}
export async function searchUsername(params: {username: string}) : Promise<{data: UserInfo[]}> {
return request('/user-api/search', {
return request(AUTH_API.FRIEND_SEARCH, {
params: params,
method: "GET"
})
}
export async function addFriend(userId: string) : Promise<CommonResponse<String>> {
return request('/user-api/friend/request', {
return request(AUTH_API.FRIEND_REQUEST, {
data: {
friendId: userId,
},
@@ -21,29 +22,29 @@ export async function addFriend(userId: string) : Promise<CommonResponse<String>
})
}
export async function queryFriendList(): Promise<CommonResponse<FriendUser[]>> {
return request('/user-api/friend/list');
return request(AUTH_API.FRIEND_LIST);
}
export async function acceptFriendRequest(friendId: string): Promise<CommonResponse<string>> {
return request('/user-api/friend/accept', {
return request(AUTH_API.FRIEND_ACCEPT, {
method: 'POST',
data: { friendId },
});
}
export async function rejectFriendRequest(friendId: string): Promise<CommonResponse<string>> {
return request('/user-api/friend/reject', {
return request(AUTH_API.FRIEND_REJECT, {
method: 'POST',
data: { friendId },
});
}
export async function queryHistoryMessages(): Promise<CommonResponse<HistoryMessage[]>> {
return request('/user-api/message/history/friend', {
return request(AUTH_API.MESSAGE_HISTORY, {
method: 'GET',
});
}
export async function queryFriendDynamic(): Promise<CommonResponse<HistoryMessage[]>> {
return request('/api/story/activity/authorized-items', {
return request(STORY_API.ACTIVITY_AUTHORIZED_ITEMS, {
method: 'GET',
});
}

View File

@@ -1,24 +1,25 @@
import { request } from '@umijs/max';
import type { CurrentUser, GeographicItemType } from './data';
import { CommonResponse } from '@/types/common';
import { AUTH_API, GEO_API, COMMON_API } from '@/services/config/apiUrls';
export async function queryCurrent(): Promise<{ data: CurrentUser }> {
return request('/user-api/info');
return request(AUTH_API.INFO);
}
export async function updateCurrentUser(params: CurrentUser): Promise<{data: CommonResponse<string>}> {
return request('/user-api/info', {
return request(AUTH_API.INFO, {
method: 'PUT',
data: params,
})
}
export async function queryProvince(): Promise<{ data: GeographicItemType[] }> {
return request('/api/geographic/province');
return request(GEO_API.PROVINCE);
}
export async function queryCity(province: string): Promise<{ data: GeographicItemType[] }> {
return request(`/api/geographic/city/${province}`);
return request(GEO_API.CITY(province));
}
export async function query() {
return request('/api/users');
return request(COMMON_API.USERS);
}

View File

@@ -7,6 +7,7 @@
import React, { useState, useEffect } from 'react';
import { Button, message, Spin, Empty, Checkbox } from 'antd';
import { useModel, request } from '@umijs/max';
import { USER_API } from '@/services/config/apiUrls';
import styles from '../index.less';
interface PhotoSelectorProps {
@@ -42,7 +43,7 @@ const PhotoSelector: React.FC<PhotoSelectorProps> = ({
setLoading(true);
try {
// Fetch user's photos from gallery
const response = await request('/api/v1/gallery/photos', {
const response = await request(USER_API.GALLERY_PHOTOS, {
method: 'GET',
params: { pageSize: 100 },
});

View File

@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { PlayCircleOutlined, PauseCircleOutlined, FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons';
import { Spin } from 'antd';
import { request } from '@umijs/max';
import { FILE_API } from '@/services/config/apiUrls';
interface TimelineVideoProps {
videoInstanceId: string;
@@ -21,7 +22,7 @@ const TimelineVideo: React.FC<TimelineVideoProps> = ({ videoInstanceId, thumbnai
useEffect(() => {
const fetchVideoUrl = async () => {
try {
const response = await request(`/file/get-video-url/${videoInstanceId}`, { method: 'GET' });
const response = await request(FILE_API.VIDEO_URL(videoInstanceId), { method: 'GET' });
if (response.code === 200 && response.data) {
setVideoSrc(response.data);
}

View File

@@ -1,6 +1,7 @@
import { request } from '@umijs/max';
import {StoryItem, StoryItemTimeQueryParams, StoryType} from './data.d';
import {CommonListResponse, CommonResponse} from "@/types/common";
import { STORY_API, FILE_API } from '@/services/config/apiUrls';
type ParamsType = {
count?: number;
@@ -13,19 +14,19 @@ type ParamsType = {
export async function queryTimelineList(
params: ParamsType,
): Promise<{ data: StoryType[] }> {
return await request('/api/story/list', {
return await request(STORY_API.LIST, {
params,
});
}
export async function deleteStory(params: ParamsType): Promise<{ data: { list: StoryType[] } }> {
return request(`/api/story/${params.instanceId}`, {
return request(STORY_API.DETAIL(params.instanceId!), {
method: 'DELETE',
});
}
export async function addStory(params: ParamsType): Promise<{ data: { list: StoryType[] } }> {
return request('/api/story/add', {
return request(STORY_API.ADD, {
method: 'POST',
data: {
...params,
@@ -35,7 +36,7 @@ export async function addStory(params: ParamsType): Promise<{ data: { list: Stor
}
export async function updateStory(params: ParamsType): Promise<{ data: { list: StoryType[] } }> {
return await request(`/api/story/${params.instanceId}`, {
return await request(STORY_API.DETAIL(params.instanceId!), {
method: 'PUT',
data: {
...params,
@@ -44,12 +45,12 @@ export async function updateStory(params: ParamsType): Promise<{ data: { list: S
});
}
export async function queryStoryDetail(itemId: string): Promise<{ data: StoryType }> {
return request(`/api/story/${itemId}`, {
return request(STORY_API.DETAIL(itemId), {
method: 'GET',
});
}
export async function addStoryItem(params: FormData): Promise<any> {
return request(`/api/story/item`, {
return request(STORY_API.ITEM, {
method: 'POST',
data: params,
requestType: 'form',
@@ -57,7 +58,7 @@ export async function addStoryItem(params: FormData): Promise<any> {
});
}
export async function updateStoryItem(params: FormData): Promise<any> {
return request(`/api/story/item`, {
return request(STORY_API.ITEM, {
method: 'PUT',
data: params,
requestType: 'form',
@@ -66,43 +67,43 @@ export async function updateStoryItem(params: FormData): Promise<any> {
}
export async function queryStoryItem(params: ParamsType): Promise<{ data: CommonListResponse<StoryItem> }> {
return request(`/api/story/item/list`, {
return request(STORY_API.ITEM_LIST, {
method: 'GET',
params: params,
});
}
export async function queryStoryItemDetail(itemId: string): Promise<{ data: StoryItem }> {
return request(`/api/story/item/${itemId}`, {
return request(STORY_API.ITEM_DETAIL(itemId), {
method: 'GET',
});
}
export async function countStoryItem(storyInstanceId: string): Promise<{ data: StoryItem }> {
return request(`/api/story/item/count/${storyInstanceId}`, {
return request(STORY_API.ITEM_COUNT(storyInstanceId), {
method: 'GET',
});
}
export async function queryStoryItemImages(itemId: string): Promise<{ data: string[] }> {
return request(`/api/story/item/images/${itemId}`, {
return request(STORY_API.ITEM_IMAGES(itemId), {
method: 'GET',
});
}
export async function removeStoryItem(instanceId: string): Promise<CommonResponse<void>> {
return request(`/api/story/item/${instanceId}`, {
return request(STORY_API.ITEM_DETAIL(instanceId), {
method: 'DELETE',
});
}
export async function searchStoryItems(params: { keyword: string; pageNum: number; pageSize: number }) {
return request('/api/story/item/search', {
return request(STORY_API.ITEM_SEARCH, {
method: 'GET',
params,
});
}
export async function fetchImage(imageInstanceId: string): Promise<any> {
return request(`/file/image/${imageInstanceId}`, {
return request(FILE_API.IMAGE(imageInstanceId), {
method: 'GET',
responseType: 'blob',
getResponse: true,
@@ -110,46 +111,46 @@ export async function fetchImage(imageInstanceId: string): Promise<any> {
}
export async function authorizeStoryPermission(params: {userId: string, storyInstanceId: string, permissionType: number}) {
return request('/api/story/permission/authorize', {
return request(STORY_API.PERMISSION_AUTHORIZE, {
method: 'POST',
data: params,
});
}
export async function getStoryPermissions(storyId: string) {
return request(`/api/story/permission/story/${storyId}`, {
return request(STORY_API.PERMISSION_STORY(storyId), {
method: 'GET',
});
}
export async function inviteUser(params: {userId: string, storyInstanceId: string, permissionType: number}) {
return request('/api/story/permission/invite', {
return request(STORY_API.PERMISSION_INVITE, {
method: 'POST',
data: params,
});
}
export async function acceptInvite(inviteId: string) {
return request(`/api/story/permission/invite/${inviteId}/accept`, {
return request(STORY_API.PERMISSION_INVITE_ACCEPT(inviteId), {
method: 'PUT',
});
}
export async function rejectInvite(inviteId: string) {
return request(`/api/story/permission/invite/${inviteId}/reject`, {
return request(STORY_API.PERMISSION_INVITE_REJECT(inviteId), {
method: 'PUT',
});
}
export async function updatePermission(params: {permissionId: string, permissionType: number}) {
return request('/api/story/permission', {
return request(STORY_API.PERMISSION, {
method: 'PUT',
data: params
});
}
export async function removePermission(permissionId: string) {
return request(`/api/story/permission/${permissionId}`, {
return request(`${STORY_API.PERMISSION}/${permissionId}`, {
method: 'DELETE'
});
}
@@ -173,7 +174,7 @@ export async function removePermission(permissionId: string) {
export async function updateStoryItemOrder(
orderData: Array<{ instanceId: string; sortOrder: number }>
): Promise<CommonResponse<void>> {
return request('/api/story/item/order', {
return request(`${STORY_API.ITEM}/order`, {
method: 'PUT',
data: { items: orderData },
});
@@ -195,7 +196,7 @@ export async function updateStoryItemOrder(
export async function batchDeleteStoryItems(
instanceIds: string[]
): Promise<CommonResponse<void>> {
return request('/api/story/item/batch-delete', {
return request(`${STORY_API.ITEM}/batch-delete`, {
method: 'POST',
data: { instanceIds },
});
@@ -215,7 +216,7 @@ export async function batchUpdateStoryItemTime(
instanceIds: string[],
storyItemTime: string
): Promise<CommonResponse<void>> {
return request('/api/story/item/batch-time', {
return request(`${STORY_API.ITEM}/batch-time`, {
method: 'PUT',
data: { instanceIds, storyItemTime },
});

View File

@@ -1,5 +1,6 @@
import { getFakeCaptcha } from '@/services/ant-design-pro/login';
import { loginUser } from '@/services/user/api';
import type { UserLoginParams, UserLoginResult } from '@/services/user/typing';
import { CommonResponse } from '@/types/common';
import {
AlipayCircleOutlined,
@@ -133,9 +134,9 @@ const Login: React.FC = () => {
currentUser: response.data,
}));
const urlParams = new URL(window.location.href).searchParams;
// 修复:直接使用 redirect 参数,如果不存在则跳转到首页
// 修复:使用 history.push 进行跳转,避免页面刷新导致状态丢失
const redirect = urlParams.get('redirect');
window.location.href = redirect || '/';
history.push(redirect || '/');
return;
}
console.log(response.message);

View File

@@ -120,8 +120,13 @@ export const errorConfig: RequestConfig = {
return { ...config, headers };
}
// 无 token 提示并引导登录
// 无 token 时,如果是获取当前用户信息的请求,不提示登录(让调用方处理)
if (!token) {
const skipAuthUrls = ['/api/currentUser', '/user-api/info'];
if (config?.url && skipAuthUrls.some((u) => config.url?.includes(u))) {
return config;
}
message.warning('请先登录');
const currentUrl = window.location.href;
const urlParams = new URL(currentUrl).searchParams;

View File

@@ -4,14 +4,13 @@
*/
import { request } from '@umijs/max';
const BASE_URL = '/api/v1/albums';
import { USER_API } from '@/services/config/apiUrls';
/**
* Get all albums for the current user
*/
export async function getAlbums(): Promise<API.Album[]> {
return request(`${BASE_URL}`, {
return request(USER_API.ALBUMS, {
method: 'GET',
});
}
@@ -20,7 +19,7 @@ export async function getAlbums(): Promise<API.Album[]> {
* Get album details by ID
*/
export async function getAlbumById(id: string): Promise<API.Album> {
return request(`${BASE_URL}/${id}`, {
return request(USER_API.ALBUM_DETAIL(id), {
method: 'GET',
});
}
@@ -29,7 +28,7 @@ export async function getAlbumById(id: string): Promise<API.Album> {
* Create a new album
*/
export async function createAlbum(data: API.CreateAlbumDTO): Promise<API.Album> {
return request(`${BASE_URL}`, {
return request(USER_API.ALBUMS, {
method: 'POST',
data,
});
@@ -39,7 +38,7 @@ export async function createAlbum(data: API.CreateAlbumDTO): Promise<API.Album>
* Update an existing album
*/
export async function updateAlbum(id: string, data: API.UpdateAlbumDTO): Promise<API.Album> {
return request(`${BASE_URL}/${id}`, {
return request(USER_API.ALBUM_DETAIL(id), {
method: 'PUT',
data,
});
@@ -49,7 +48,7 @@ export async function updateAlbum(id: string, data: API.UpdateAlbumDTO): Promise
* Delete an album
*/
export async function deleteAlbum(id: string): Promise<void> {
return request(`${BASE_URL}/${id}`, {
return request(USER_API.ALBUM_DETAIL(id), {
method: 'DELETE',
});
}
@@ -58,7 +57,7 @@ export async function deleteAlbum(id: string): Promise<void> {
* Add photos to an album
*/
export async function addPhotosToAlbum(id: string, photoIds: string[]): Promise<void> {
return request(`${BASE_URL}/${id}/photos`, {
return request(USER_API.ALBUM_PHOTOS(id), {
method: 'POST',
data: { photoIds },
});
@@ -68,7 +67,7 @@ export async function addPhotosToAlbum(id: string, photoIds: string[]): Promise<
* Remove photos from an album
*/
export async function removePhotosFromAlbum(id: string, photoIds: string[]): Promise<void> {
return request(`${BASE_URL}/${id}/photos`, {
return request(USER_API.ALBUM_PHOTOS(id), {
method: 'DELETE',
data: { photoIds },
});
@@ -78,7 +77,7 @@ export async function removePhotosFromAlbum(id: string, photoIds: string[]): Pro
* Reorder photos in an album
*/
export async function reorderPhotos(id: string, order: string[]): Promise<void> {
return request(`${BASE_URL}/${id}/photos/order`, {
return request(USER_API.ALBUM_PHOTOS_ORDER(id), {
method: 'PUT',
data: { order },
});
@@ -88,7 +87,7 @@ export async function reorderPhotos(id: string, order: string[]): Promise<void>
* Set album cover photo
*/
export async function setAlbumCover(id: string, coverPhotoId: string): Promise<void> {
return request(`${BASE_URL}/${id}/cover`, {
return request(USER_API.ALBUM_COVER(id), {
method: 'PUT',
data: { coverPhotoId },
});

View File

@@ -4,14 +4,13 @@
*/
import { request } from '@umijs/max';
const BASE_URL = '/api/v1/collections';
import { USER_API } from '@/services/config/apiUrls';
/**
* Get all smart collections for the current user
*/
export async function getSmartCollections(): Promise<API.SmartCollection[]> {
return request(`${BASE_URL}/smart`, {
return request(USER_API.COLLECTIONS_SMART, {
method: 'GET',
});
}
@@ -23,7 +22,7 @@ export async function getCollectionContent(
collectionId: string,
params: { page: number; pageSize: number },
): Promise<API.CollectionContent> {
return request(`${BASE_URL}/smart/${collectionId}/content`, {
return request(USER_API.COLLECTIONS_SMART_CONTENT(collectionId), {
method: 'GET',
params,
});
@@ -33,7 +32,7 @@ export async function getCollectionContent(
* Force refresh smart collections
*/
export async function refreshSmartCollections(): Promise<void> {
return request(`${BASE_URL}/smart/refresh`, {
return request(USER_API.COLLECTIONS_SMART_REFRESH, {
method: 'POST',
});
}

View File

@@ -4,6 +4,7 @@
*/
import { request } from '@umijs/max';
import { SOCIAL_API } from '@/services/config/apiUrls';
/**
* Get comments for an entity
@@ -12,7 +13,7 @@ export async function getComments(
entityType: API.SocialEntityType,
entityId: string,
): Promise<API.Comment[]> {
return request(`/api/v1/comments/${entityType}/${entityId}`, {
return request(SOCIAL_API.COMMENTS_BY_ENTITY(entityType, entityId), {
method: 'GET',
});
}
@@ -21,7 +22,7 @@ export async function getComments(
* Create a new comment
*/
export async function createComment(data: API.CreateCommentDTO): Promise<API.Comment> {
return request('/api/v1/comments', {
return request(SOCIAL_API.COMMENTS, {
method: 'POST',
data,
});
@@ -34,7 +35,7 @@ export async function updateComment(
id: string,
data: API.UpdateCommentDTO,
): Promise<API.Comment> {
return request(`/api/v1/comments/${id}`, {
return request(SOCIAL_API.COMMENTS_DETAIL(id), {
method: 'PUT',
data,
});
@@ -44,7 +45,7 @@ export async function updateComment(
* Delete a comment
*/
export async function deleteComment(id: string): Promise<void> {
return request(`/api/v1/comments/${id}`, {
return request(SOCIAL_API.COMMENTS_DETAIL(id), {
method: 'DELETE',
});
}

View File

@@ -0,0 +1,131 @@
/**
* API URL 全局配置
* 所有后端接口地址统一在此配置,便于管理和维护
*/
// 基础 API 前缀
const API_PREFIX = '/api';
const USER_API_PREFIX = `${API_PREFIX}/user/api/v1`;
const USER_AUTH_PREFIX = '/user-api';
// Story 相关接口
export const STORY_API = {
BASE: `${API_PREFIX}/story`,
LIST: `${API_PREFIX}/story/list`,
ADD: `${API_PREFIX}/story/add`,
DETAIL: (id: string) => `${API_PREFIX}/story/${id}`,
// Story Item 相关
ITEM: `${API_PREFIX}/story/item`,
ITEM_LIST: `${API_PREFIX}/story/item/list`,
ITEM_DETAIL: (id: string) => `${API_PREFIX}/story/item/${id}`,
ITEM_COUNT: (storyInstanceId: string) => `${API_PREFIX}/story/item/count/${storyInstanceId}`,
ITEM_IMAGES: (itemId: string) => `${API_PREFIX}/story/item/images/${itemId}`,
ITEM_SEARCH: `${API_PREFIX}/story/item/search`,
// 权限相关
PERMISSION: `${API_PREFIX}/story/permission`,
PERMISSION_AUTHORIZE: `${API_PREFIX}/story/permission/authorize`,
PERMISSION_STORY: (storyId: string) => `${API_PREFIX}/story/permission/story/${storyId}`,
PERMISSION_INVITE: `${API_PREFIX}/story/permission/invite`,
PERMISSION_INVITE_ACCEPT: (inviteId: string) => `${API_PREFIX}/story/permission/invite/${inviteId}/accept`,
PERMISSION_INVITE_REJECT: (inviteId: string) => `${API_PREFIX}/story/permission/invite/${inviteId}/reject`,
// 活动相关
ACTIVITY_AUTHORIZED_ITEMS: `${API_PREFIX}/story/activity/authorized-items`,
} as const;
// 文件相关接口
export const FILE_API = {
IMAGE: (imageInstanceId: string) => `${API_PREFIX}/file/image/${imageInstanceId}`,
IMAGE_LOW_RES: (imageInstanceId: string) => `${API_PREFIX}/file/image-low-res/${imageInstanceId}`,
IMAGE_LIST: `${API_PREFIX}/file/image/list`,
VIDEO_URL: (videoInstanceId: string) => `${API_PREFIX}/file/get-video-url/${videoInstanceId}`,
UPLOAD_IMAGE: `${API_PREFIX}/file/upload-image`,
GET_UPLOAD_URL: (fileName: string) => `${API_PREFIX}/file/get-upload-url/${fileName}`,
UPLOADED: `${API_PREFIX}/file/uploaded`,
BATCH_INFO: `${API_PREFIX}/file/batch-info`,
} as const;
// 用户相关接口
export const USER_API = {
PROFILE: `${USER_API_PREFIX}/profile`,
PROFILE_COVER: `${USER_API_PREFIX}/profile/cover`,
PROFILE_CUSTOM_FIELDS: `${USER_API_PREFIX}/profile/custom-fields`,
PREFERENCES: `${USER_API_PREFIX}/preferences`,
PREFERENCES_THEME: `${USER_API_PREFIX}/preferences/theme`,
PREFERENCES_LAYOUT: `${USER_API_PREFIX}/preferences/layout`,
PREFERENCES_TIMELINE: `${USER_API_PREFIX}/preferences/timeline`,
ALBUMS: `${USER_API_PREFIX}/albums`,
ALBUM_DETAIL: (id: string) => `${USER_API_PREFIX}/albums/${id}`,
ALBUM_PHOTOS: (id: string) => `${USER_API_PREFIX}/albums/${id}/photos`,
ALBUM_PHOTOS_ORDER: (id: string) => `${USER_API_PREFIX}/albums/${id}/photos/order`,
ALBUM_COVER: (id: string) => `${USER_API_PREFIX}/albums/${id}/cover`,
COLLECTIONS: `${USER_API_PREFIX}/collections`,
COLLECTIONS_SMART: `${USER_API_PREFIX}/collections/smart`,
COLLECTIONS_SMART_CONTENT: (collectionId: string) => `${USER_API_PREFIX}/collections/smart/${collectionId}/content`,
COLLECTIONS_SMART_REFRESH: `${USER_API_PREFIX}/collections/smart/refresh`,
// Gallery 相关
GALLERY_PHOTOS: `${API_PREFIX}/v1/gallery/photos`,
// 通知相关
NOTIFICATIONS_READ: `${API_PREFIX}/user/notifications/read`,
} as const;
// 用户认证相关接口
export const AUTH_API = {
INFO: `${USER_AUTH_PREFIX}/info`,
REGISTER: `${USER_AUTH_PREFIX}/auth/register`,
LOGIN: `${USER_AUTH_PREFIX}/auth/login`,
LOGOUT: `${USER_AUTH_PREFIX}/auth/logout`,
// 好友相关
FRIEND_SEARCH: `${USER_AUTH_PREFIX}/search`,
FRIEND_REQUEST: `${USER_AUTH_PREFIX}/friend/request`,
FRIEND_LIST: `${USER_AUTH_PREFIX}/friend/list`,
FRIEND_ACCEPT: `${USER_AUTH_PREFIX}/friend/accept`,
FRIEND_REJECT: `${USER_AUTH_PREFIX}/friend/reject`,
MESSAGE_HISTORY: `${USER_AUTH_PREFIX}/message/history/friend`,
} as const;
// 社交功能接口
export const SOCIAL_API = {
COMMENTS: `${API_PREFIX}/v1/comments`,
COMMENTS_DETAIL: (id: string) => `${API_PREFIX}/v1/comments/${id}`,
COMMENTS_BY_ENTITY: (entityType: string, entityId: string) => `${API_PREFIX}/v1/comments/${entityType}/${entityId}`,
REACTIONS: `${API_PREFIX}/v1/reactions`,
REACTIONS_BY_ENTITY: (entityType: string, entityId: string) => `${API_PREFIX}/v1/reactions/${entityType}/${entityId}`,
} as const;
// 同步相关接口
export const SYNC_API = {
BASE: `${API_PREFIX}/v1/sync`,
CHANGES: `${API_PREFIX}/v1/sync/changes`,
STATUS: `${API_PREFIX}/v1/sync/status`,
RESOLVE_CONFLICT: `${API_PREFIX}/v1/sync/resolve-conflict`,
} as const;
// 统计相关接口
export const STATISTICS_API = {
BASE: `${API_PREFIX}/v1/statistics`,
OVERVIEW: `${API_PREFIX}/v1/statistics/overview`,
UPLOADS: `${API_PREFIX}/v1/statistics/uploads`,
STORAGE: `${API_PREFIX}/v1/statistics/storage`,
REFRESH: `${API_PREFIX}/v1/statistics/refresh`,
} as const;
// 地理信息接口
export const GEO_API = {
PROVINCE: `${API_PREFIX}/geographic/province`,
CITY: (province: string) => `${API_PREFIX}/geographic/city/${province}`,
} as const;
// 其他通用接口
export const COMMON_API = {
USERS: `${API_PREFIX}/users`,
} as const;

View File

@@ -1,22 +1,24 @@
import {request} from "@@/exports";
import {CommonListResponse, CommonResponse} from "@/types/common";
import {ImageItem} from "@/pages/gallery/typings";
import { STORY_API, FILE_API } from '@/services/config/apiUrls';
// 查询storyItem图片列表
export async function queryStoryItemImages(itemId: string): Promise<{ data: string[] }> {
return request(`/api/story/item/images/${itemId}`, {
return request(STORY_API.ITEM_IMAGES(itemId), {
method: 'GET',
});
}
// 获取图片
export async function fetchImage(imageInstanceId: string): Promise<any> {
return request(`/file/image/${imageInstanceId}`, {
return request(FILE_API.IMAGE(imageInstanceId), {
method: 'GET',
responseType: 'blob',
getResponse: true,
});
}
export async function fetchImageLowRes(imageInstanceId: string): Promise<any> {
return request(`/file/image-low-res/${imageInstanceId}`, {
return request(FILE_API.IMAGE_LOW_RES(imageInstanceId), {
method: 'GET',
responseType: 'blob',
getResponse: true,
@@ -32,7 +34,7 @@ export async function getImagesList(
},
options?: { [key: string]: any },
) {
return request<CommonResponse<CommonListResponse<ImageItem>>>('/file/image/list', {
return request<CommonResponse<CommonListResponse<ImageItem>>>(FILE_API.IMAGE_LIST, {
method: 'GET',
params: {
...params,
@@ -42,7 +44,7 @@ export async function getImagesList(
}
export async function deleteImage(params: { instanceId: string }) {
return request<CommonResponse<any>>(`/file/image/${params.instanceId}`, {
return request<CommonResponse<any>>(FILE_API.IMAGE(params.instanceId), {
method: 'DELETE',
params: {
...params,
@@ -50,33 +52,33 @@ export async function deleteImage(params: { instanceId: string }) {
});
}
export async function uploadImage(params: FormData) {
return request<CommonResponse<any>>('/file/upload-image', {
return request<CommonResponse<any>>(FILE_API.UPLOAD_IMAGE, {
method: 'POST',
data: params,
});
}
export async function getUploadUrl(fileName: string) {
return request<CommonResponse<string>>(`/file/get-upload-url/${fileName}`, {
return request<CommonResponse<string>>(FILE_API.GET_UPLOAD_URL(fileName), {
method: 'GET',
});
}
export async function getVideoUrl(instanceId: string): Promise<CommonResponse<string>> {
return request<CommonResponse<string>>(`/file/get-video-url/${instanceId}`, {
return request<CommonResponse<string>>(FILE_API.VIDEO_URL(instanceId), {
method: 'GET',
});
}
export async function saveFileMetadata(data: any) {
return request<CommonResponse<string>>('/file/uploaded', {
return request<CommonResponse<string>>(FILE_API.UPLOADED, {
method: 'POST',
data,
});
}
export async function batchGetFileInfo(instanceIds: string[]): Promise<CommonResponse<any[]>> {
return request<CommonResponse<any[]>>('/file/batch-info', {
return request<CommonResponse<any[]>>(FILE_API.BATCH_INFO, {
method: 'POST',
data: instanceIds,
});

View File

@@ -4,14 +4,13 @@
*/
import { request } from '@umijs/max';
const BASE_URL = '/api/v1/preferences';
import { USER_API } from '@/services/config/apiUrls';
/**
* Get user preferences
*/
export async function getPreferences(): Promise<API.UserPreferences> {
return request(`${BASE_URL}`, {
return request(USER_API.PREFERENCES, {
method: 'GET',
});
}
@@ -20,7 +19,7 @@ export async function getPreferences(): Promise<API.UserPreferences> {
* Update theme preferences
*/
export async function updateThemePreferences(data: API.UpdateThemeDTO): Promise<void> {
return request(`${BASE_URL}/theme`, {
return request(USER_API.PREFERENCES_THEME, {
method: 'PUT',
data,
});
@@ -30,7 +29,7 @@ export async function updateThemePreferences(data: API.UpdateThemeDTO): Promise<
* Update layout preferences
*/
export async function updateLayoutPreferences(data: API.UpdateLayoutDTO): Promise<void> {
return request(`${BASE_URL}/layout`, {
return request(USER_API.PREFERENCES_LAYOUT, {
method: 'PUT',
data,
});
@@ -40,7 +39,7 @@ export async function updateLayoutPreferences(data: API.UpdateLayoutDTO): Promis
* Update timeline preferences
*/
export async function updateTimelinePreferences(data: API.UpdateTimelineDTO): Promise<void> {
return request(`${BASE_URL}/timeline`, {
return request(USER_API.PREFERENCES_TIMELINE, {
method: 'PUT',
data,
});

View File

@@ -4,8 +4,7 @@
*/
import { request } from '@umijs/max';
const BASE_URL = '/api/v1/profile';
import { USER_API } from '@/services/config/apiUrls';
export interface UserProfile {
userId: string;
@@ -32,7 +31,7 @@ export interface ProfileData {
* Get user profile
*/
export async function getProfile(): Promise<ProfileData> {
return request(`${BASE_URL}`, {
return request(USER_API.PROFILE, {
method: 'GET',
});
}
@@ -41,7 +40,7 @@ export async function getProfile(): Promise<ProfileData> {
* Update profile bio
*/
export async function updateProfile(data: { bio?: string }): Promise<void> {
return request(`${BASE_URL}`, {
return request(USER_API.PROFILE, {
method: 'PUT',
data,
});
@@ -54,7 +53,7 @@ export async function uploadCoverPhoto(file: File): Promise<string> {
const formData = new FormData();
formData.append('file', file);
return request(`${BASE_URL}/cover`, {
return request(USER_API.PROFILE_COVER, {
method: 'POST',
data: formData,
});
@@ -64,7 +63,7 @@ export async function uploadCoverPhoto(file: File): Promise<string> {
* Update custom fields
*/
export async function updateCustomFields(fields: CustomField[]): Promise<void> {
return request(`${BASE_URL}/custom-fields`, {
return request(USER_API.PROFILE_CUSTOM_FIELDS, {
method: 'PUT',
data: { fields },
});

View File

@@ -4,8 +4,7 @@
*/
import { request } from '@umijs/max';
const BASE_URL = '/api/v1/reactions';
import { SOCIAL_API } from '@/services/config/apiUrls';
/**
* Get reactions for an entity
@@ -14,7 +13,7 @@ export async function getReactions(
entityType: API.SocialEntityType,
entityId: string,
): Promise<API.ReactionSummary> {
return request(`${BASE_URL}/${entityType}/${entityId}`, {
return request(SOCIAL_API.REACTIONS_BY_ENTITY(entityType, entityId), {
method: 'GET',
});
}
@@ -23,7 +22,7 @@ export async function getReactions(
* Add or update a reaction
*/
export async function addReaction(data: API.AddReactionDTO): Promise<void> {
return request(`${BASE_URL}`, {
return request(SOCIAL_API.REACTIONS, {
method: 'POST',
data,
});
@@ -36,7 +35,7 @@ export async function removeReaction(
entityType: API.SocialEntityType,
entityId: string,
): Promise<void> {
return request(`${BASE_URL}/${entityType}/${entityId}`, {
return request(SOCIAL_API.REACTIONS_BY_ENTITY(entityType, entityId), {
method: 'DELETE',
});
}

View File

@@ -4,14 +4,13 @@
*/
import { request } from '@umijs/max';
const BASE_URL = '/api/v1/statistics';
import { STATISTICS_API } from '@/services/config/apiUrls';
/**
* Get user statistics overview
*/
export async function getStatisticsOverview(): Promise<API.UserStatistics> {
return request(`${BASE_URL}/overview`, {
return request(STATISTICS_API.OVERVIEW, {
method: 'GET',
});
}
@@ -20,7 +19,7 @@ export async function getStatisticsOverview(): Promise<API.UserStatistics> {
* Get upload trends
*/
export async function getUploadTrends(): Promise<API.UploadTrend[]> {
return request(`${BASE_URL}/uploads`, {
return request(STATISTICS_API.UPLOADS, {
method: 'GET',
});
}
@@ -29,7 +28,7 @@ export async function getUploadTrends(): Promise<API.UploadTrend[]> {
* Get storage breakdown
*/
export async function getStorageBreakdown(): Promise<API.StorageBreakdown> {
return request(`${BASE_URL}/storage`, {
return request(STATISTICS_API.STORAGE, {
method: 'GET',
});
}
@@ -38,7 +37,7 @@ export async function getStorageBreakdown(): Promise<API.StorageBreakdown> {
* Force refresh statistics
*/
export async function refreshStatistics(): Promise<void> {
return request(`${BASE_URL}/refresh`, {
return request(STATISTICS_API.REFRESH, {
method: 'POST',
});
}

View File

@@ -4,14 +4,13 @@
*/
import { request } from '@umijs/max';
const BASE_URL = '/api/v1/sync';
import { SYNC_API } from '@/services/config/apiUrls';
/**
* Upload offline changes batch
*/
export async function uploadChanges(changes: API.ChangeRecord[]): Promise<API.SyncResult> {
return request(`${BASE_URL}/changes`, {
return request(SYNC_API.CHANGES, {
method: 'POST',
data: { changes },
});
@@ -21,7 +20,7 @@ export async function uploadChanges(changes: API.ChangeRecord[]): Promise<API.Sy
* Get sync status
*/
export async function getSyncStatus(): Promise<{ status: API.SyncStatus; pendingCount: number }> {
return request(`${BASE_URL}/status`, {
return request(SYNC_API.STATUS, {
method: 'GET',
});
}
@@ -30,7 +29,7 @@ export async function getSyncStatus(): Promise<{ status: API.SyncStatus; pending
* Resolve sync conflict
*/
export async function resolveConflict(resolution: API.Resolution): Promise<void> {
return request(`${BASE_URL}/resolve-conflict`, {
return request(SYNC_API.RESOLVE_CONFLICT, {
method: 'POST',
data: resolution,
});

View File

@@ -1,23 +1,24 @@
import { CommonResponse } from "@/types/common";
import {request} from "@@/exports";
import { AUTH_API } from '@/services/config/apiUrls';
export async function registerUser(params: UserRegisterParams, options?: { skipErrorHandler?: boolean }): Promise<CommonResponse<string>> {
return request('/user-api/auth/register', {
return request(AUTH_API.REGISTER, {
method: 'POST',
data: params,
getResponse: true,
});
}
export async function loginUser(params: UserLoginParams): Promise<CommonResponse<UserLoginResult>> {
return request('/user-api/auth/login', {
return request(AUTH_API.LOGIN, {
method: 'POST',
data: params,
// getResponse: true,
});
}
export async function logoutUser() {
return request('/user-api/auth/logout', {
return request(AUTH_API.LOGOUT, {
method: 'POST',
});
}

View File

@@ -1,5 +1,5 @@
// 注册用户参数
interface UserRegisterParams {
export interface UserRegisterParams {
username: string;
nickname: string;
password: string;
@@ -7,25 +7,25 @@ interface UserRegisterParams {
phone: string;
}
// 登录用户参数
interface UserLoginParams {
export interface UserLoginParams {
username?: string;
password?: string;
loginType?: string;
}
// 退出登录参数
interface UserLogoutParams {
export interface UserLogoutParams {
username: string;
password: string;
}
// 登录返回参数
interface UserLoginResult {
export interface UserLoginResult {
accessToken: string;
refreshToken: string;
accessTokenExpiresInSeconds: number;
refreshTokenExpiresInSeconds: number;
}
// 用户信息
interface UserInfo {
export interface UserInfo {
username: string,
accessToken: string;
refreshToken: string;

26
src/typings.d.ts vendored
View File

@@ -19,17 +19,17 @@ declare module 'react-masonry-css';
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;
// Extend Namespaces type to include 'theme'
declare module '@umijs/max' {
export type Namespaces =
| 'menu'
| 'settings'
| 'pages'
| 'component'
| 'globalHeader'
| 'theme'
| 'sync'
| 'albums'
| 'collections'
| 'layout';
}
// declare module '@umijs/max' {
// export type Namespaces =
// | 'menu'
// | 'settings'
// | 'pages'
// | 'component'
// | 'globalHeader'
// | 'theme'
// | 'sync'
// | 'albums'
// | 'collections'
// | 'layout';
// }

View File

@@ -20,8 +20,8 @@ export class OfflineDatabase extends Dexie {
super('TimelineOffline');
// Define database schema
// Version 1: Initial schema
this.version(1).stores({
// Version 2: Added synced index to changes table
this.version(2).stores({
// Stories table with indexes
// Primary key: id
// Indexes: userId, createdAt, syncStatus for efficient querying
@@ -34,9 +34,8 @@ export class OfflineDatabase extends Dexie {
// Changes table with auto-incrementing primary key
// Primary key: ++id (auto-increment)
// Indexes: entityType, entityId, operation, timestamp
// Note: synced is stored as 0/1 for IndexedDB compatibility but not indexed
changes: '++id, entityType, entityId, operation, timestamp',
// Indexes: entityType, entityId, operation, timestamp, synced
changes: '++id, entityType, entityId, operation, timestamp, synced',
});
// Map tables to TypeScript types

View File

@@ -18,19 +18,25 @@ import { message } from 'antd';
export class SyncManager {
private queue: API.ChangeRecord[] = [];
private syncing: boolean = false;
private online: boolean = navigator.onLine;
private online: boolean = typeof window !== 'undefined' ? navigator.onLine : true;
private syncListeners: Array<(status: API.SyncStatus) => void> = [];
private autoSyncEnabled: boolean = true;
private initialized: boolean = false;
constructor() {
this.initializeEventListeners();
this.loadQueueFromDB();
// Defer initialization to avoid SSR issues
if (typeof window !== 'undefined') {
this.initializeEventListeners();
this.loadQueueFromDB();
this.initialized = true;
}
}
/**
* Initialize online/offline event listeners
*/
private initializeEventListeners(): void {
if (typeof window === 'undefined') return;
window.addEventListener('online', () => this.onOnline());
window.addEventListener('offline', () => this.onOffline());
}