chore(history): import initial web product snapshot
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { I18nProvider } from '@/i18n'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
import { Confirmer } from '@/components/ui/confirmer'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
staleTime: 30_000,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function AppProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
<Confirmer />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/app/store/auth'
|
||||
import { AppLayout } from '@/components/layout/AppLayout'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { LoginPage } from '@/pages/auth/LoginPage'
|
||||
import { RegisterPage } from '@/pages/auth/RegisterPage'
|
||||
import { OnboardingPage } from '@/pages/onboarding/OnboardingPage'
|
||||
import { DashboardPage } from '@/pages/dashboard/DashboardPage'
|
||||
import { InboxPage } from '@/pages/inbox/InboxPage'
|
||||
import { CustomersPage } from '@/pages/customers/CustomersPage'
|
||||
import { CustomerDetailPage } from '@/pages/customers/CustomerDetailPage'
|
||||
import { OpportunitiesPage } from '@/pages/opportunities/OpportunitiesPage'
|
||||
import { TranslatePage } from '@/pages/translate/TranslatePage'
|
||||
import { InterpretPage } from '@/pages/interpret/InterpretPage'
|
||||
import { RisksPage } from '@/pages/risks/RisksPage'
|
||||
import { KnowledgePage } from '@/pages/knowledge/KnowledgePage'
|
||||
import { AnalyticsPage } from '@/pages/analytics/AnalyticsPage'
|
||||
import { ChannelsPage } from '@/pages/channels/ChannelsPage'
|
||||
import { TeamPage } from '@/pages/team/TeamPage'
|
||||
import { BillingPage } from '@/pages/billing/BillingPage'
|
||||
import { SettingsPage } from '@/pages/settings/SettingsPage'
|
||||
import { HelpPage } from '@/pages/help/HelpPage'
|
||||
import { NotFoundPage } from '@/pages/NotFoundPage'
|
||||
|
||||
function RequireAuth({ children }: { children: ReactNode }) {
|
||||
const token = useAuthStore((s) => s.token)
|
||||
if (!token) return <Navigate to="/login" replace />
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
function GuestOnly({ children }: { children: ReactNode }) {
|
||||
const token = useAuthStore((s) => s.token)
|
||||
if (token) return <Navigate to="/dashboard" replace />
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{ path: '/login', element: <GuestOnly><LoginPage /></GuestOnly> },
|
||||
{ path: '/register', element: <GuestOnly><RegisterPage /></GuestOnly> },
|
||||
{
|
||||
path: '/onboarding',
|
||||
element: (
|
||||
<RequireAuth>
|
||||
<OnboardingPage />
|
||||
</RequireAuth>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: (
|
||||
<RequireAuth>
|
||||
<AppLayout />
|
||||
</RequireAuth>
|
||||
),
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="/dashboard" replace /> },
|
||||
{ path: 'dashboard', element: <DashboardPage /> },
|
||||
{ path: 'inbox', element: <InboxPage /> },
|
||||
{ path: 'customers', element: <CustomersPage /> },
|
||||
{ path: 'customers/:id', element: <CustomerDetailPage /> },
|
||||
{ path: 'opportunities', element: <OpportunitiesPage /> },
|
||||
{ path: 'translate', element: <TranslatePage /> },
|
||||
{ path: 'interpret', element: <InterpretPage /> },
|
||||
{ path: 'risks', element: <RisksPage /> },
|
||||
{ path: 'knowledge', element: <KnowledgePage /> },
|
||||
{ path: 'analytics', element: <AnalyticsPage /> },
|
||||
{ path: 'channels', element: <ChannelsPage /> },
|
||||
{ path: 'team', element: <TeamPage /> },
|
||||
{ path: 'billing', element: <BillingPage /> },
|
||||
{ path: 'settings', element: <SettingsPage /> },
|
||||
{ path: 'help', element: <HelpPage /> },
|
||||
{ path: '*', element: <NotFoundPage /> },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
export function AppRouter() {
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import type { AuthSession, Tenant, User } from '@/types'
|
||||
import type { RoleKey } from '@/config/roles'
|
||||
import { ROLE_META } from '@/config/roles'
|
||||
import { setTokenGetter } from '@/services/api/client'
|
||||
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
user: User | null
|
||||
tenant: Tenant | null
|
||||
onboarded: boolean
|
||||
setSession: (session: AuthSession) => void
|
||||
logout: () => void
|
||||
/** 演示用:切换当前用户角色以预览不同权限 */
|
||||
switchRole: (role: RoleKey) => void
|
||||
setOnboarded: (v: boolean) => void
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
token: null,
|
||||
user: null,
|
||||
tenant: null,
|
||||
onboarded: true,
|
||||
setSession: (session) =>
|
||||
set({ token: session.token, user: session.user, tenant: session.tenant, onboarded: session.onboarded }),
|
||||
logout: () => set({ token: null, user: null, tenant: null }),
|
||||
switchRole: (role) =>
|
||||
set((s) => (s.user ? { user: { ...s.user, role, title: ROLE_META[role].label } } : s)),
|
||||
setOnboarded: (v) => set({ onboarded: v }),
|
||||
}),
|
||||
{ name: 'lx-auth' },
|
||||
),
|
||||
)
|
||||
|
||||
// 让 API client 能读取 token(真实后端模式使用)
|
||||
setTokenGetter(() => useAuthStore.getState().token)
|
||||
|
||||
export function useCurrentRole(): RoleKey {
|
||||
return useAuthStore((s) => s.user?.role ?? 'viewer')
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title: string
|
||||
description?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
danger?: boolean
|
||||
onConfirm: () => void | Promise<void>
|
||||
}
|
||||
|
||||
interface ConfirmState {
|
||||
current: ConfirmOptions | null
|
||||
ask: (opts: ConfirmOptions) => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export const useConfirmStore = create<ConfirmState>()((set) => ({
|
||||
current: null,
|
||||
ask: (opts) => set({ current: opts }),
|
||||
close: () => set({ current: null }),
|
||||
}))
|
||||
|
||||
/** 任意位置调用:confirmDialog({ title, danger, onConfirm }) */
|
||||
export function confirmDialog(opts: ConfirmOptions): void {
|
||||
useConfirmStore.getState().ask(opts)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { create } from 'zustand'
|
||||
import { uid } from '@/utils/misc'
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'info' | 'warning'
|
||||
|
||||
export interface ToastItem {
|
||||
id: string
|
||||
type: ToastType
|
||||
title: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
toasts: ToastItem[]
|
||||
push: (toast: Omit<ToastItem, 'id'>) => void
|
||||
dismiss: (id: string) => void
|
||||
}
|
||||
|
||||
export const useToastStore = create<ToastState>()((set) => ({
|
||||
toasts: [],
|
||||
push: (t) => {
|
||||
const id = uid('toast')
|
||||
set((s) => ({ toasts: [...s.toasts.slice(-4), { ...t, id }] }))
|
||||
setTimeout(() => {
|
||||
set((s) => ({ toasts: s.toasts.filter((x) => x.id !== id) }))
|
||||
}, 3600)
|
||||
},
|
||||
dismiss: (id) => set((s) => ({ toasts: s.toasts.filter((x) => x.id !== id) })),
|
||||
}))
|
||||
|
||||
/** 任意位置调用:toast.success('已保存') */
|
||||
export const toast = {
|
||||
success: (title: string, description?: string) => useToastStore.getState().push({ type: 'success', title, description }),
|
||||
error: (title: string, description?: string) => useToastStore.getState().push({ type: 'error', title, description }),
|
||||
info: (title: string, description?: string) => useToastStore.getState().push({ type: 'info', title, description }),
|
||||
warning: (title: string, description?: string) => useToastStore.getState().push({ type: 'warning', title, description }),
|
||||
}
|
||||
|
||||
/** 从异常提取可读信息 */
|
||||
export function errorMessage(e: unknown): string {
|
||||
if (e instanceof Error) return e.message
|
||||
return '操作失败,请稍后重试'
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface UiState {
|
||||
sidebarCollapsed: boolean
|
||||
mobileNavOpen: boolean
|
||||
toggleSidebar: () => void
|
||||
setMobileNavOpen: (open: boolean) => void
|
||||
}
|
||||
|
||||
export const useUiStore = create<UiState>()((set) => ({
|
||||
sidebarCollapsed: false,
|
||||
mobileNavOpen: false,
|
||||
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
|
||||
setMobileNavOpen: (open) => set({ mobileNavOpen: open }),
|
||||
}))
|
||||
Reference in New Issue
Block a user