468 lines
20 KiB
Plaintext
468 lines
20 KiB
Plaintext
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
程序鉴别材料 - 源代码前30页
|
|||
|
|
纺织行业采购计划管理系统 V1.0.0
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
【文件1】src/index.tsx - 应用入口文件
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import React from 'react';
|
|||
|
|
import ReactDOM from 'react-dom/client';
|
|||
|
|
import App from './App';
|
|||
|
|
import './styles/index.css';
|
|||
|
|
|
|||
|
|
const root = ReactDOM.createRoot(
|
|||
|
|
document.getElementById('root') as HTMLElement
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
root.render(
|
|||
|
|
<React.StrictMode>
|
|||
|
|
<App />
|
|||
|
|
</React.StrictMode>
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
【文件2】src/App.tsx - 根组件与路由配置
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import React, { useState } from 'react';
|
|||
|
|
import { HashRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
|||
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|||
|
|
import { AuthProvider } from './contexts/AuthContext';
|
|||
|
|
import { ErrorBoundary, PageErrorBoundary } from './components/ErrorBoundary';
|
|||
|
|
import { LoginPage } from './pages/LoginPage';
|
|||
|
|
import { RegisterPage } from './pages/RegisterPage';
|
|||
|
|
import { RoleSelectPage } from './pages/RoleSelectPage';
|
|||
|
|
import { PurchaserDashboard } from './pages/purchaser/Dashboard';
|
|||
|
|
import { TextileDashboard } from './pages/textile/Dashboard';
|
|||
|
|
import { WashingDashboard } from './pages/washing/Dashboard';
|
|||
|
|
import { PlanOverview } from './pages/purchaser/PlanOverview';
|
|||
|
|
import { NewPlan } from './pages/purchaser/NewPlan';
|
|||
|
|
import { WarehouseManage } from './pages/purchaser/WarehouseManage';
|
|||
|
|
import { FactoryManage } from './pages/purchaser/FactoryManage';
|
|||
|
|
import { TextilePlanOverview } from './pages/textile/PlanOverview';
|
|||
|
|
import { YarnWarehouse } from './pages/textile/YarnWarehouse';
|
|||
|
|
import { FabricWarehouse } from './pages/textile/FabricWarehouse';
|
|||
|
|
import { PaymentPending } from './pages/textile/PaymentPending';
|
|||
|
|
import { MemberManage } from './pages/MemberManage';
|
|||
|
|
import { ImportPlanPage } from './pages/ImportPlanPage';
|
|||
|
|
import { NetworkStatusBar } from './components/NetworkStatusBar';
|
|||
|
|
import './styles/index.css';
|
|||
|
|
|
|||
|
|
// 页面过渡动画配置
|
|||
|
|
const pageTransitionVariants = {
|
|||
|
|
initial: { opacity: 0, y: 20, scale: 0.98 },
|
|||
|
|
animate: {
|
|||
|
|
opacity: 1, y: 0, scale: 1,
|
|||
|
|
transition: { duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }
|
|||
|
|
},
|
|||
|
|
exit: {
|
|||
|
|
opacity: 0, y: -10, scale: 0.98,
|
|||
|
|
transition: { duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function AnimatedRoute({ children, pageName }: { children: React.ReactNode; pageName?: string }) {
|
|||
|
|
return (
|
|||
|
|
<motion.div variants={pageTransitionVariants} initial="initial" animate="animate" exit="exit"
|
|||
|
|
style={{ willChange: 'transform, opacity' }}>
|
|||
|
|
<PageErrorBoundary pageName={pageName}>{children}</PageErrorBoundary>
|
|||
|
|
</motion.div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function AppRoutes() {
|
|||
|
|
const location = useLocation();
|
|||
|
|
return (
|
|||
|
|
<AnimatePresence mode="wait">
|
|||
|
|
<Routes location={location} key={location.pathname}>
|
|||
|
|
<Route path="/login" element={<AnimatedRoute pageName="登录"><LoginPage /></AnimatedRoute>} />
|
|||
|
|
<Route path="/register" element={<AnimatedRoute pageName="注册"><RegisterPage /></AnimatedRoute>} />
|
|||
|
|
<Route path="/role-select" element={<AnimatedRoute pageName="角色选择"><RoleSelectPage /></AnimatedRoute>} />
|
|||
|
|
<Route path="/purchaser" element={<AnimatedRoute pageName="采购商首页"><PurchaserDashboard /></AnimatedRoute>} />
|
|||
|
|
<Route path="/purchaser/plans" element={<AnimatedRoute pageName="计划总览"><PlanOverview /></AnimatedRoute>} />
|
|||
|
|
<Route path="/purchaser/plans/new" element={<AnimatedRoute pageName="新建计划"><NewPlan /></AnimatedRoute>} />
|
|||
|
|
<Route path="/purchaser/warehouse" element={<AnimatedRoute pageName="仓库管理"><WarehouseManage /></AnimatedRoute>} />
|
|||
|
|
<Route path="/purchaser/factories" element={<AnimatedRoute pageName="工厂管理"><FactoryManage /></AnimatedRoute>} />
|
|||
|
|
<Route path="/textile" element={<AnimatedRoute pageName="纺织厂首页"><TextileDashboard /></AnimatedRoute>} />
|
|||
|
|
<Route path="/textile/plans" element={<AnimatedRoute pageName="纺织厂计划"><TextilePlanOverview /></AnimatedRoute>} />
|
|||
|
|
<Route path="/textile/fabric-warehouse" element={<AnimatedRoute pageName="坯布仓库"><FabricWarehouse /></AnimatedRoute>} />
|
|||
|
|
<Route path="/textile/yarn-warehouse" element={<AnimatedRoute pageName="纱线仓库"><YarnWarehouse /></AnimatedRoute>} />
|
|||
|
|
<Route path="/textile/payments" element={<AnimatedRoute pageName="待结款"><PaymentPending /></AnimatedRoute>} />
|
|||
|
|
<Route path="/washing" element={<AnimatedRoute pageName="水洗厂首页"><WashingDashboard /></AnimatedRoute>} />
|
|||
|
|
<Route path="/members" element={<AnimatedRoute pageName="子账号管理"><MemberManage /></AnimatedRoute>} />
|
|||
|
|
<Route path="/import" element={<AnimatedRoute pageName="导入计划"><ImportPlanPage /></AnimatedRoute>} />
|
|||
|
|
<Route path="/" element={<Navigate to="/login" replace />} />
|
|||
|
|
</Routes>
|
|||
|
|
</AnimatePresence>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function App() {
|
|||
|
|
return (
|
|||
|
|
<ErrorBoundary>
|
|||
|
|
<AuthProvider>
|
|||
|
|
<HashRouter>
|
|||
|
|
<NetworkStatusBar />
|
|||
|
|
<AppRoutes />
|
|||
|
|
</HashRouter>
|
|||
|
|
</AuthProvider>
|
|||
|
|
</ErrorBoundary>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default App;
|
|||
|
|
|
|||
|
|
【文件3】src/contexts/AuthContext.tsx - 认证上下文(部分)
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
|||
|
|
import { supabase } from '../supabase/client';
|
|||
|
|
import type { Profile, Company, UserRole, AuthState } from '../types';
|
|||
|
|
import { encryptPhone, decryptPhone } from '../utils/crypto';
|
|||
|
|
|
|||
|
|
interface RegisterData {
|
|||
|
|
username: string;
|
|||
|
|
password: string;
|
|||
|
|
displayName: string;
|
|||
|
|
companyName: string;
|
|||
|
|
phone: string;
|
|||
|
|
role: UserRole;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface AuthContextType {
|
|||
|
|
auth: AuthState;
|
|||
|
|
login: (username: string, password: string) => Promise<boolean>;
|
|||
|
|
register: (data: RegisterData) => Promise<{ success: boolean; error?: string }>;
|
|||
|
|
logout: () => Promise<void>;
|
|||
|
|
selectRole: (role: UserRole) => void;
|
|||
|
|
loading: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||
|
|
|
|||
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||
|
|
const [auth, setAuth] = useState<AuthState>({
|
|||
|
|
isLoggedIn: false, user: null, company: null, currentRole: null
|
|||
|
|
});
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const initAuth = async () => {
|
|||
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|||
|
|
if (session?.user) {
|
|||
|
|
const { data: profile } = await supabase
|
|||
|
|
.from('profiles').select('*').eq('id', session.user.id).maybeSingle();
|
|||
|
|
if (profile) {
|
|||
|
|
let company: Company | null = null;
|
|||
|
|
if (profile.company_id) {
|
|||
|
|
const { data: comp } = await supabase
|
|||
|
|
.from('companies').select('*').eq('id', profile.company_id).maybeSingle();
|
|||
|
|
company = comp;
|
|||
|
|
}
|
|||
|
|
setAuth({ isLoggedIn: true, user: profile, company, currentRole: company?.role || null });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
setLoading(false);
|
|||
|
|
};
|
|||
|
|
initAuth();
|
|||
|
|
|
|||
|
|
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
|||
|
|
if (event === 'SIGNED_IN' && session?.user) {
|
|||
|
|
setTimeout(async () => {
|
|||
|
|
const { data: profile } = await supabase
|
|||
|
|
.from('profiles').select('*').eq('id', session.user.id).maybeSingle();
|
|||
|
|
if (profile) {
|
|||
|
|
let company: Company | null = null;
|
|||
|
|
if (profile.company_id) {
|
|||
|
|
const { data: comp } = await supabase
|
|||
|
|
.from('companies').select('*').eq('id', profile.company_id).maybeSingle();
|
|||
|
|
company = comp;
|
|||
|
|
}
|
|||
|
|
setAuth({ isLoggedIn: true, user: profile, company, currentRole: company?.role || null });
|
|||
|
|
}
|
|||
|
|
}, 0);
|
|||
|
|
} else if (event === 'SIGNED_OUT') {
|
|||
|
|
setAuth({ isLoggedIn: false, user: null, company: null, currentRole: null });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return () => subscription.unsubscribe();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const login = useCallback(async (username: string, password: string): Promise<boolean> => {
|
|||
|
|
const { error } = await supabase.auth.signInWithPassword({
|
|||
|
|
email: `${username}@meoo.local`, password,
|
|||
|
|
});
|
|||
|
|
return !error;
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const register = useCallback(async (data: RegisterData) => {
|
|||
|
|
try {
|
|||
|
|
const { data: companyData, error: companyError } = await supabase
|
|||
|
|
.from('companies').insert({ name: data.companyName, role: data.role, contact_phone: data.phone })
|
|||
|
|
.select().single();
|
|||
|
|
if (companyError || !companyData) return { success: false, error: '创建公司失败' };
|
|||
|
|
|
|||
|
|
const { data: authData, error: authError } = await supabase.auth.signUp({
|
|||
|
|
email: `${data.username}@meoo.local`, password: data.password,
|
|||
|
|
});
|
|||
|
|
if (authError || !authData.user) {
|
|||
|
|
await supabase.from('companies').delete().eq('id', companyData.id);
|
|||
|
|
return { success: false, error: '注册失败' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { error: profileError } = await supabase.from('profiles').insert({
|
|||
|
|
id: authData.user.id, username: data.username, display_name: data.displayName,
|
|||
|
|
phone: encryptPhone(data.phone), company_id: companyData.id, is_master: true
|
|||
|
|
});
|
|||
|
|
if (profileError) {
|
|||
|
|
await supabase.from('companies').delete().eq('id', companyData.id);
|
|||
|
|
return { success: false, error: '创建用户资料失败' };
|
|||
|
|
}
|
|||
|
|
return { success: true };
|
|||
|
|
} catch (err: any) {
|
|||
|
|
return { success: false, error: err?.message || '注册失败' };
|
|||
|
|
}
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const logout = useCallback(async () => {
|
|||
|
|
await supabase.auth.signOut();
|
|||
|
|
setAuth({ isLoggedIn: false, user: null, company: null, currentRole: null });
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const selectRole = useCallback((role: UserRole) => {
|
|||
|
|
setAuth(prev => ({ ...prev, currentRole: role }));
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<AuthContext.Provider value={{ auth, login, register, logout, selectRole, loading }}>
|
|||
|
|
{children}
|
|||
|
|
</AuthContext.Provider>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function useAuth() {
|
|||
|
|
const context = useContext(AuthContext);
|
|||
|
|
if (!context) throw new Error('useAuth must be used within AuthProvider');
|
|||
|
|
return context;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
【文件4】src/supabase/client.ts - Supabase客户端配置
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import { createClient } from '@supabase/supabase-js';
|
|||
|
|
import type { Database } from './types';
|
|||
|
|
|
|||
|
|
export function getSupabaseUrl(): string {
|
|||
|
|
return `${(window as any).MEOO_CONFIG?.meoo_app_access_url || location.origin}/sb-api`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const supabaseUrl = getSupabaseUrl();
|
|||
|
|
export const supabaseAnonKey = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...';
|
|||
|
|
|
|||
|
|
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
|||
|
|
auth: { persistSession: true, autoRefreshToken: true },
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
【文件5】src/types/index.ts - 类型定义(部分)
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
export type UserRole = 'purchaser' | 'textile' | 'washing';
|
|||
|
|
export type PlanStatus = 'pending' | 'producing' | 'completed';
|
|||
|
|
export type FactoryType = 'textile' | 'washing';
|
|||
|
|
|
|||
|
|
export interface Profile {
|
|||
|
|
id: string;
|
|||
|
|
username: string;
|
|||
|
|
display_name?: string;
|
|||
|
|
phone?: string;
|
|||
|
|
company_id?: string;
|
|||
|
|
is_master: boolean;
|
|||
|
|
master_id?: string;
|
|||
|
|
created_at: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface Company {
|
|||
|
|
id: string;
|
|||
|
|
name: string;
|
|||
|
|
role: UserRole;
|
|||
|
|
address?: string;
|
|||
|
|
contact_phone?: string;
|
|||
|
|
created_at: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface ProductionPlan {
|
|||
|
|
id: string;
|
|||
|
|
plan_code: string;
|
|||
|
|
product_name: string;
|
|||
|
|
color: string;
|
|||
|
|
fabric_code: string;
|
|||
|
|
color_code: string;
|
|||
|
|
remark?: string;
|
|||
|
|
purchaser_id: string;
|
|||
|
|
target_quantity: number;
|
|||
|
|
completed_quantity: number;
|
|||
|
|
yarn_usage_per_meter: number;
|
|||
|
|
production_price: number;
|
|||
|
|
status: PlanStatus;
|
|||
|
|
start_time?: string;
|
|||
|
|
created_by: string;
|
|||
|
|
created_at: string;
|
|||
|
|
updated_at: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface AuthState {
|
|||
|
|
isLoggedIn: boolean;
|
|||
|
|
user: Profile | null;
|
|||
|
|
company: Company | null;
|
|||
|
|
currentRole: UserRole | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
【文件6】src/hooks/useNetworkStatus.ts - 网络状态Hook(部分)
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|||
|
|
|
|||
|
|
interface NetworkState {
|
|||
|
|
isOnline: boolean;
|
|||
|
|
isSlowConnection: boolean;
|
|||
|
|
lastOnlineTime: number | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface QueuedOperation {
|
|||
|
|
id: string;
|
|||
|
|
operation: () => Promise<void>;
|
|||
|
|
retryCount: number;
|
|||
|
|
maxRetries: number;
|
|||
|
|
onSuccess?: () => void;
|
|||
|
|
onError?: (error: Error) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function useNetworkStatus() {
|
|||
|
|
const [state, setState] = useState<NetworkState>({
|
|||
|
|
isOnline: navigator.onLine,
|
|||
|
|
isSlowConnection: false,
|
|||
|
|
lastOnlineTime: navigator.onLine ? Date.now() : null
|
|||
|
|
});
|
|||
|
|
const queueRef = useRef<QueuedOperation[]>([]);
|
|||
|
|
const processingRef = useRef(false);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handleOnline = () => {
|
|||
|
|
setState(prev => ({ ...prev, isOnline: true, lastOnlineTime: Date.now() }));
|
|||
|
|
processQueue();
|
|||
|
|
};
|
|||
|
|
const handleOffline = () => {
|
|||
|
|
setState(prev => ({ ...prev, isOnline: false }));
|
|||
|
|
};
|
|||
|
|
window.addEventListener('online', handleOnline);
|
|||
|
|
window.addEventListener('offline', handleOffline);
|
|||
|
|
|
|||
|
|
const checkConnectionSpeed = async () => {
|
|||
|
|
const connection = (navigator as any).connection;
|
|||
|
|
if (connection) {
|
|||
|
|
setState(prev => ({
|
|||
|
|
...prev,
|
|||
|
|
isSlowConnection: connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g'
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
const interval = setInterval(checkConnectionSpeed, 120000);
|
|||
|
|
return () => {
|
|||
|
|
window.removeEventListener('online', handleOnline);
|
|||
|
|
window.removeEventListener('offline', handleOffline);
|
|||
|
|
clearInterval(interval);
|
|||
|
|
};
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const processQueue = useCallback(async () => { /* ... */ }, []);
|
|||
|
|
const enqueueOperation = useCallback((operation: () => Promise<void>, options?: any) => { /* ... */ }, []);
|
|||
|
|
const executeWithRetry = useCallback(async <T>(operation: () => Promise<T>, options?: any) => { /* ... */ }, []);
|
|||
|
|
|
|||
|
|
return { ...state, enqueueOperation, executeWithRetry, queueLength: queueRef.current.length };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
【文件7】src/components/ErrorBoundary.tsx - 错误边界组件(部分)
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|||
|
|
import { AlertCircle, RefreshCw, Home } from 'lucide-react';
|
|||
|
|
|
|||
|
|
interface Props { children: ReactNode; fallback?: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void; }
|
|||
|
|
interface State { hasError: boolean; error: Error | null; errorInfo: ErrorInfo | null; }
|
|||
|
|
|
|||
|
|
export class ErrorBoundary extends Component<Props, State> {
|
|||
|
|
constructor(props: Props) {
|
|||
|
|
super(props);
|
|||
|
|
this.state = { hasError: false, error: null, errorInfo: null };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static getDerivedStateFromError(error: Error): Partial<State> {
|
|||
|
|
return { hasError: true, error };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|||
|
|
this.setState({ errorInfo });
|
|||
|
|
if (this.props.onError) this.props.onError(error, errorInfo);
|
|||
|
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleReset = () => this.setState({ hasError: false, error: null, errorInfo: null });
|
|||
|
|
handleGoHome = () => window.location.href = '/';
|
|||
|
|
handleReload = () => window.location.reload();
|
|||
|
|
|
|||
|
|
render() {
|
|||
|
|
if (this.state.hasError) {
|
|||
|
|
if (this.props.fallback) return this.props.fallback;
|
|||
|
|
return (
|
|||
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
|||
|
|
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6 sm:p-8 max-w-md w-full">
|
|||
|
|
<div className="flex items-center gap-3 mb-4">
|
|||
|
|
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
|
|||
|
|
<AlertCircle className="w-6 h-6 text-red-600" />
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<h2 className="text-lg font-semibold text-gray-900">页面出错了</h2>
|
|||
|
|
<p className="text-sm text-gray-500">抱歉,发生了意外错误</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="bg-gray-50 rounded-lg p-3 mb-4 overflow-auto max-h-32">
|
|||
|
|
<p className="text-xs text-gray-600 font-mono break-all">{this.state.error?.toString()}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<button onClick={this.handleReset} className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium">
|
|||
|
|
<RefreshCw className="w-4 h-4" /> 重试
|
|||
|
|
</button>
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button onClick={this.handleGoHome} className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
|
|||
|
|
<Home className="w-4 h-4" /> 返回首页
|
|||
|
|
</button>
|
|||
|
|
<button onClick={this.handleReload} className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
|
|||
|
|
<RefreshCw className="w-4 h-4" /> 刷新页面
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
return this.props.children;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
源代码特征说明
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
1. 编程语言:TypeScript (.ts, .tsx)
|
|||
|
|
2. 前端框架:React 18 + React Router v6
|
|||
|
|
3. 状态管理:React Context + Hooks
|
|||
|
|
4. 动画库:Framer Motion
|
|||
|
|
5. 样式方案:TailwindCSS
|
|||
|
|
6. 后端服务:Supabase (PostgreSQL)
|
|||
|
|
7. 构建工具:Webpack 5
|
|||
|
|
|
|||
|
|
代码特点:
|
|||
|
|
- 使用函数组件和React Hooks
|
|||
|
|
- 类型安全的TypeScript开发
|
|||
|
|
- 模块化设计,组件化架构
|
|||
|
|
- 响应式设计,支持移动端
|
|||
|
|
- 错误边界处理机制
|
|||
|
|
- 实时数据同步
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
第 1 页 / 共 30 页(前30页)
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|