chore(history): import initial web product snapshot

This commit is contained in:
wmq
2026-07-25 00:30:00 +08:00
commit cf0eed46c3
131 changed files with 15693 additions and 0 deletions
+27
View File
@@ -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>
)
}
+80
View File
@@ -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} />
}
+43
View File
@@ -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')
}
+27
View File
@@ -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)
}
+43
View File
@@ -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 '操作失败,请稍后重试'
}
+15
View File
@@ -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 }),
}))