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页)
|
||
═══════════════════════════════════════════════════════════════════════════════
|