feat: Add base project structure with API and web components

This commit is contained in:
Ke Sun
2025-12-02 20:28:01 +08:00
parent f3de6d6cc9
commit c1adc62ec6
817 changed files with 111226 additions and 106 deletions

45
web/src/store/locale.ts Normal file
View File

@@ -0,0 +1,45 @@
import { create } from 'zustand'
import enUS from 'antd/locale/en_US';
import zhCN from 'antd/locale/zh_CN';
import type { Locale } from 'antd/es/locale';
import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import i18n from '@/i18n';
import { timezoneToAntdLocaleMap } from '@/utils/timezones';
// 扩展dayjs插件
dayjs.extend(utc);
dayjs.extend(timezone);
interface I18nState {
language: string;
locale: Locale;
timeZone: string;
changeLanguage: (language: string) => void;
changeTimeZone: (timeZone: string) => void;
}
const initialTimeZone = localStorage.getItem('timeZone') || 'Asia/Shanghai'
const initialLanguage = localStorage.getItem('language') || 'en'
const initialLocale = initialLanguage === 'en' ? enUS : zhCN
i18n.changeLanguage(initialLanguage)
export const useI18n = create<I18nState>((set, get) => ({
language: initialLanguage,
locale: initialLocale,
timeZone: initialTimeZone,
changeLanguage: (language: string) => {
i18n.changeLanguage(language)
const localeName = timezoneToAntdLocaleMap[language] || enUS;
set({ language: language, locale: localeName })
},
changeTimeZone: (timeZone: string) => {
const { timeZone: lastTimeZone } = get()
set({ timeZone })
if (lastTimeZone !== timeZone) {
window.location.reload()
}
},
}))

76
web/src/store/menu.ts Normal file
View File

@@ -0,0 +1,76 @@
import { create } from 'zustand'
import AllMenus from './menu.json'
export interface MenuItem {
id: number;
parent: number;
code: string | null;
label: string;
i18nKey: string | null;
path: string | null;
enable: boolean;
display: boolean;
level: number;
sort: number;
icon: string | null;
iconActive: string | null;
menuDesc: string | null;
deleted: string | null;
updateTime: number;
new_: string | null;
keepAlive: boolean;
master: string | null;
disposable: boolean;
appSystem: string | null;
subs: MenuItem[];
}
interface MenuState {
collapsed: boolean;
toggleSider: () => void;
allMenus: Record<'space' | 'manage', MenuItem[]>;
allBreadcrumbs: Record<'space' | 'manage' | string, MenuItem[]>;
loadMenus: (source: 'space' | 'manage') => void;
updateBreadcrumbs: (keyPath: string[], source: 'space' | 'manage') => void;
}
const initBreadcrumbs = localStorage.getItem('breadcrumbs') || '[]'
export const useMenu = create<MenuState>((set, get) => ({
collapsed: localStorage.getItem('collapsed') === 'true',
allMenus: {
manage: [],
space: []
},
allBreadcrumbs: JSON.parse(initBreadcrumbs),
loadMenus: async () => {
set({ allMenus: AllMenus })
},
toggleSider: () => {
set((state) => {
const newCollapsed = !state.collapsed
localStorage.setItem('collapsed', JSON.stringify(newCollapsed))
return { collapsed: newCollapsed }
})
},
updateBreadcrumbs: (paths, source) => {
const { allMenus } = get()
const menus = allMenus[source] || []
let result: MenuItem[] = []
const matchedMenu: MenuItem | undefined = menus.find(menu => menu.path === paths[paths.length - 1] || `${menu.id}` === paths[1]);
if (matchedMenu) {
let matchedSubMenu: MenuItem | undefined = undefined;
if (paths.length > 1 && matchedMenu?.subs?.length) {
matchedSubMenu = matchedMenu.subs.find(menu => menu.path === paths[0]);
}
result = [
{ ...matchedMenu, subs: null },
matchedSubMenu
].filter(item => item !== undefined) as MenuItem[]
} else {
result = [] as MenuItem[]
}
const allBreadcrumbs = { ...get().allBreadcrumbs, [source]: result }
set({ allBreadcrumbs })
localStorage.setItem('breadcrumbs', JSON.stringify(allBreadcrumbs))
},
}))

89
web/src/store/user.ts Normal file
View File

@@ -0,0 +1,89 @@
import { create } from 'zustand'
import { clearAuthData } from '@/utils/auth';
import type { User } from '@/views/UserManagement/types'
import { getUsers, refreshToken, logout } from '@/api/user'
import { getWorkspaceStorageType } from '@/api/workspaces';
export interface LoginInfo {
access_token: string;
expires_at: string;
refresh_expires_at: string;
refresh_token: string;
token_type: 'bearer'
}
export interface UserState {
user: User;
loginInfo: LoginInfo;
storageType: string | null;
updateLoginInfo: (values: LoginInfo) => void;
getUserInfo: (flag?: boolean) => void;
clearUserInfo: () => void;
logout: () => void;
getStorageType: () => void;
}
export const useUser = create<UserState>((set, get) => ({
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || '{}') as User : {} as User,
loginInfo: {} as LoginInfo,
storageType: null,
updateLoginInfo: (values: LoginInfo) => {
localStorage.setItem('token', values.access_token);
localStorage.setItem('refresh_token', values.refresh_token);
set({ loginInfo: values });
},
getUserInfo: async (flag?: boolean) => {
if (!localStorage.getItem('token')) {
return
}
const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User;
if (localUser.id) {
return
}
getUsers()
.then((res) => {
const response = res as User;
set({ user: response })
if (flag) {
window.location.href = response.role && response.current_workspace_id ? '/#/' : '/#/space'
}
localStorage.setItem('user', JSON.stringify(response))
})
.catch((err) => {
console.error('Failed to fetch user info:', err)
})
},
clearUserInfo: () => {
set({ user: {} as User })
clearAuthData();
},
logout: () => {
logout()
.then(() => {
const { clearUserInfo } = get()
clearUserInfo()
window.location.href = '/#/login'
})
.catch((err) => {
console.error('Failed to logout:', err)
})
},
refreshToken: () => {
refreshToken()
.then((res) => {
const response = res as { refresh_token: string }
localStorage.setItem('token', response.refresh_token);
})
.catch((err) => {
console.error('Failed to refresh token:', err)
})
},
getStorageType: () => {
getWorkspaceStorageType()
.then((res) => {
const response = res as { storage_type: string };
set({ storageType: response.storage_type || 'neo4j' });
})
.catch(() => {
console.error('Failed to load storage type');
})
}
}))