685 lines
22 KiB
Plaintext
685 lines
22 KiB
Plaintext
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
程序鉴别材料 - 源代码后30页
|
|||
|
|
纺织行业采购计划管理系统 V1.0.0
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
【文件1】src/utils/crypto.ts - 加密工具模块
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
/**
|
|||
|
|
* 数据加密/解密工具
|
|||
|
|
* 使用 AES 对称加密保护敏感数据
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// 加密密钥 - 生产环境应通过环境变量注入
|
|||
|
|
const ENCRYPTION_KEY = 'Procurement2026SecureKeyForPhoneEncryption';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 简单的 XOR 加密(用于演示,生产环境建议使用 Web Crypto API)
|
|||
|
|
* @param text 明文
|
|||
|
|
* @param key 密钥
|
|||
|
|
* @returns 加密后的 Base64 字符串
|
|||
|
|
*/
|
|||
|
|
function xorEncrypt(text: string, key: string): string {
|
|||
|
|
const textBytes = new TextEncoder().encode(text);
|
|||
|
|
const keyBytes = new TextEncoder().encode(key);
|
|||
|
|
const result = new Uint8Array(textBytes.length);
|
|||
|
|
|
|||
|
|
for (let i = 0; i < textBytes.length; i++) {
|
|||
|
|
result[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return btoa(String.fromCharCode(...result));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* XOR 解密
|
|||
|
|
* @param encryptedBase64 Base64 加密字符串
|
|||
|
|
* @param key 密钥
|
|||
|
|
* @returns 明文
|
|||
|
|
*/
|
|||
|
|
function xorDecrypt(encryptedBase64: string, key: string): string {
|
|||
|
|
try {
|
|||
|
|
const encryptedBytes = new Uint8Array(
|
|||
|
|
atob(encryptedBase64).split('').map(c => c.charCodeAt(0))
|
|||
|
|
);
|
|||
|
|
const keyBytes = new TextEncoder().encode(key);
|
|||
|
|
const result = new Uint8Array(encryptedBytes.length);
|
|||
|
|
|
|||
|
|
for (let i = 0; i < encryptedBytes.length; i++) {
|
|||
|
|
result[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return new TextDecoder().decode(result);
|
|||
|
|
} catch {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 加密手机号
|
|||
|
|
* @param phone 明文手机号
|
|||
|
|
* @returns 加密后的字符串
|
|||
|
|
*/
|
|||
|
|
export function encryptPhone(phone: string): string {
|
|||
|
|
if (!phone) return '';
|
|||
|
|
return `ENC:${xorEncrypt(phone, ENCRYPTION_KEY)}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 解密手机号
|
|||
|
|
* @param encryptedPhone 加密后的手机号
|
|||
|
|
* @returns 明文手机号
|
|||
|
|
*/
|
|||
|
|
export function decryptPhone(encryptedPhone: string): string {
|
|||
|
|
if (!encryptedPhone) return '';
|
|||
|
|
if (!encryptedPhone.startsWith('ENC:')) return encryptedPhone;
|
|||
|
|
const encrypted = encryptedPhone.slice(4);
|
|||
|
|
return xorDecrypt(encrypted, ENCRYPTION_KEY);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 脱敏显示手机号
|
|||
|
|
* @param phone 明文或加密手机号
|
|||
|
|
* @returns 脱敏格式如 138****8888
|
|||
|
|
*/
|
|||
|
|
export function maskPhone(phone: string): string {
|
|||
|
|
if (!phone) return '';
|
|||
|
|
const plainPhone = phone.startsWith('ENC:') ? decryptPhone(phone) : phone;
|
|||
|
|
if (plainPhone.length !== 11) return plainPhone;
|
|||
|
|
return `${plainPhone.slice(0, 3)}****${plainPhone.slice(7)}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证是否为有效的加密格式
|
|||
|
|
* @param value 待验证的值
|
|||
|
|
* @returns 是否已加密
|
|||
|
|
*/
|
|||
|
|
export function isEncrypted(value: string): boolean {
|
|||
|
|
return value?.startsWith('ENC:') ?? false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
【文件2】src/utils/helpers.ts - 工具函数模块(部分)
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import type { StepType, StepStatus } from '../types';
|
|||
|
|
import { stepOrder } from './constants';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算生产进度百分比
|
|||
|
|
*/
|
|||
|
|
export function calculateProgress(completed: number, target: number): number {
|
|||
|
|
if (target === 0) return 0;
|
|||
|
|
return Math.round((completed / target) * 100);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 格式化日期显示
|
|||
|
|
*/
|
|||
|
|
export function formatDate(dateString: string | null | undefined): string {
|
|||
|
|
if (!dateString) return '-';
|
|||
|
|
return new Date(dateString).toLocaleDateString('zh-CN', {
|
|||
|
|
year: 'numeric',
|
|||
|
|
month: '2-digit',
|
|||
|
|
day: '2-digit'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 格式化日期时间显示
|
|||
|
|
*/
|
|||
|
|
export function formatDateTime(dateString: string | null | undefined): string {
|
|||
|
|
if (!dateString) return '-';
|
|||
|
|
return new Date(dateString).toLocaleString('zh-CN');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 格式化时间(仅时分)
|
|||
|
|
*/
|
|||
|
|
export function formatTime(dateString: string | null | undefined): string {
|
|||
|
|
if (!dateString) return '-';
|
|||
|
|
return new Date(dateString).toLocaleTimeString('zh-CN', {
|
|||
|
|
hour: '2-digit',
|
|||
|
|
minute: '2-digit'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 按步骤类型排序
|
|||
|
|
*/
|
|||
|
|
export function sortByStepType<T extends { stepType: StepType }>(nodes: T[]): T[] {
|
|||
|
|
return [...nodes].sort((a, b) => {
|
|||
|
|
const indexA = stepOrder.indexOf(a.stepType);
|
|||
|
|
const indexB = stepOrder.indexOf(b.stepType);
|
|||
|
|
return indexA - indexB;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 查找第一个待确认的节点索引
|
|||
|
|
*/
|
|||
|
|
export function findFirstPendingIndex(nodes: { status: StepStatus }[]): number {
|
|||
|
|
return nodes.findIndex(n => n.status !== 'completed');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成产品码
|
|||
|
|
*/
|
|||
|
|
export function generateProductCode(name: string, weight: string, colorCode: string): string {
|
|||
|
|
const nameLetters = name.replace(/[^a-zA-Z]/g, '').toUpperCase();
|
|||
|
|
const nameCode = nameLetters.slice(0, 2) || 'XX';
|
|||
|
|
const weightNum = weight.replace(/[^0-9]/g, '') || '0';
|
|||
|
|
const colorNum = colorCode.replace(/[^0-9]/g, '') || '00';
|
|||
|
|
return `${nameCode}${weightNum}${colorNum}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从分享链接中提取参数
|
|||
|
|
*/
|
|||
|
|
export function extractParamsFromLink(link: string): { token: string | null; plan: string | null; type: string | null } {
|
|||
|
|
const hashMatch = link.match(/#\/import\?(.+)$/);
|
|||
|
|
if (hashMatch) {
|
|||
|
|
const queryString = hashMatch[1];
|
|||
|
|
const params = new URLSearchParams(queryString);
|
|||
|
|
return {
|
|||
|
|
token: params.get('token'),
|
|||
|
|
plan: params.get('plan'),
|
|||
|
|
type: params.get('type')
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return { token: null, plan: null, type: null };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
【文件3】src/stores/index.ts - 状态管理模块(部分)
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import { create } from 'zustand';
|
|||
|
|
import { immer } from 'zustand/middleware/immer';
|
|||
|
|
import type { ProductionPlan, PlanProcessStep, Company, Profile } from '../types';
|
|||
|
|
|
|||
|
|
// ========== 计划状态管理 ==========
|
|||
|
|
|
|||
|
|
interface PlanState {
|
|||
|
|
plans: ProductionPlan[];
|
|||
|
|
selectedPlan: ProductionPlan | null;
|
|||
|
|
planSteps: Record<string, PlanProcessStep[]>;
|
|||
|
|
loading: boolean;
|
|||
|
|
error: string | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface PlanActions {
|
|||
|
|
setPlans: (plans: ProductionPlan[]) => void;
|
|||
|
|
addPlan: (plan: ProductionPlan) => void;
|
|||
|
|
updatePlan: (id: string, updates: Partial<ProductionPlan>) => void;
|
|||
|
|
deletePlan: (id: string) => void;
|
|||
|
|
selectPlan: (plan: ProductionPlan | null) => void;
|
|||
|
|
setPlanSteps: (planId: string, steps: PlanProcessStep[]) => void;
|
|||
|
|
updatePlanStep: (planId: string, stepId: string, updates: Partial<PlanProcessStep>) => void;
|
|||
|
|
setLoading: (loading: boolean) => void;
|
|||
|
|
setError: (error: string | null) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const usePlanStore = create<PlanState & PlanActions>()(
|
|||
|
|
immer((set) => ({
|
|||
|
|
plans: [],
|
|||
|
|
selectedPlan: null,
|
|||
|
|
planSteps: {},
|
|||
|
|
loading: false,
|
|||
|
|
error: null,
|
|||
|
|
|
|||
|
|
setPlans: (plans) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.plans = plans;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
addPlan: (plan) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.plans.unshift(plan);
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
updatePlan: (id, updates) =>
|
|||
|
|
set((state) => {
|
|||
|
|
const plan = state.plans.find((p) => p.id === id);
|
|||
|
|
if (plan) {
|
|||
|
|
Object.assign(plan, updates);
|
|||
|
|
}
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
deletePlan: (id) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.plans = state.plans.filter((p) => p.id !== id);
|
|||
|
|
if (state.selectedPlan?.id === id) {
|
|||
|
|
state.selectedPlan = null;
|
|||
|
|
}
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
selectPlan: (plan) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.selectedPlan = plan;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
setPlanSteps: (planId, steps) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.planSteps[planId] = steps;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
updatePlanStep: (planId, stepId, updates) =>
|
|||
|
|
set((state) => {
|
|||
|
|
const steps = state.planSteps[planId];
|
|||
|
|
if (steps) {
|
|||
|
|
const step = steps.find((s) => s.id === stepId);
|
|||
|
|
if (step) {
|
|||
|
|
Object.assign(step, updates);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
setLoading: (loading) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.loading = loading;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
setError: (error) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.error = error;
|
|||
|
|
}),
|
|||
|
|
}))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// ========== 库存状态管理 ==========
|
|||
|
|
|
|||
|
|
interface InventoryItem {
|
|||
|
|
id: string;
|
|||
|
|
planId: string;
|
|||
|
|
quantity: number;
|
|||
|
|
rolls: number;
|
|||
|
|
createdAt: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface InventoryState {
|
|||
|
|
items: InventoryItem[];
|
|||
|
|
loading: boolean;
|
|||
|
|
error: string | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface InventoryActions {
|
|||
|
|
setItems: (items: InventoryItem[]) => void;
|
|||
|
|
addItem: (item: InventoryItem) => void;
|
|||
|
|
updateItem: (id: string, updates: Partial<InventoryItem>) => void;
|
|||
|
|
deleteItem: (id: string) => void;
|
|||
|
|
setLoading: (loading: boolean) => void;
|
|||
|
|
setError: (error: string | null) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const useInventoryStore = create<InventoryState & InventoryActions>()(
|
|||
|
|
immer((set) => ({
|
|||
|
|
items: [],
|
|||
|
|
loading: false,
|
|||
|
|
error: null,
|
|||
|
|
|
|||
|
|
setItems: (items) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.items = items;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
addItem: (item) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.items.unshift(item);
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
updateItem: (id, updates) =>
|
|||
|
|
set((state) => {
|
|||
|
|
const item = state.items.find((i) => i.id === id);
|
|||
|
|
if (item) {
|
|||
|
|
Object.assign(item, updates);
|
|||
|
|
}
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
deleteItem: (id) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.items = state.items.filter((i) => i.id !== id);
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
setLoading: (loading) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.loading = loading;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
setError: (error) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.error = error;
|
|||
|
|
}),
|
|||
|
|
}))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// ========== UI 状态管理 ==========
|
|||
|
|
|
|||
|
|
interface UIState {
|
|||
|
|
sidebarOpen: boolean;
|
|||
|
|
modalOpen: boolean;
|
|||
|
|
modalContent: string | null;
|
|||
|
|
toast: { message: string; type: 'success' | 'error' | 'info' } | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface UIActions {
|
|||
|
|
toggleSidebar: () => void;
|
|||
|
|
setSidebarOpen: (open: boolean) => void;
|
|||
|
|
openModal: (content: string) => void;
|
|||
|
|
closeModal: () => void;
|
|||
|
|
showToast: (message: string, type: 'success' | 'error' | 'info') => void;
|
|||
|
|
hideToast: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const useUIStore = create<UIState & UIActions>()(
|
|||
|
|
immer((set) => ({
|
|||
|
|
sidebarOpen: false,
|
|||
|
|
modalOpen: false,
|
|||
|
|
modalContent: null,
|
|||
|
|
toast: null,
|
|||
|
|
|
|||
|
|
toggleSidebar: () =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.sidebarOpen = !state.sidebarOpen;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
setSidebarOpen: (open) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.sidebarOpen = open;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
openModal: (content) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.modalOpen = true;
|
|||
|
|
state.modalContent = content;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
closeModal: () =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.modalOpen = false;
|
|||
|
|
state.modalContent = null;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
showToast: (message, type) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.toast = { message, type };
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
hideToast: () =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.toast = null;
|
|||
|
|
}),
|
|||
|
|
}))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// ========== 网络状态管理 ==========
|
|||
|
|
|
|||
|
|
interface NetworkState {
|
|||
|
|
isOnline: boolean;
|
|||
|
|
isSlowConnection: boolean;
|
|||
|
|
pendingRequests: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface NetworkActions {
|
|||
|
|
setOnline: (online: boolean) => void;
|
|||
|
|
setSlowConnection: (slow: boolean) => void;
|
|||
|
|
incrementPending: () => void;
|
|||
|
|
decrementPending: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const useNetworkStore = create<NetworkState & NetworkActions>()(
|
|||
|
|
immer((set) => ({
|
|||
|
|
isOnline: true,
|
|||
|
|
isSlowConnection: false,
|
|||
|
|
pendingRequests: 0,
|
|||
|
|
|
|||
|
|
setOnline: (online) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.isOnline = online;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
setSlowConnection: (slow) =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.isSlowConnection = slow;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
incrementPending: () =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.pendingRequests++;
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
decrementPending: () =>
|
|||
|
|
set((state) => {
|
|||
|
|
state.pendingRequests = Math.max(0, state.pendingRequests - 1);
|
|||
|
|
}),
|
|||
|
|
}))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
【文件4】src/components/ProcessFlow.tsx - 流程节点组件(部分)
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import React, { useState } from 'react';
|
|||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|||
|
|
import { Check, Clock, AlertCircle } from 'lucide-react';
|
|||
|
|
import type { StepType, StepStatus } from '../types';
|
|||
|
|
|
|||
|
|
interface ProcessNode {
|
|||
|
|
id: string;
|
|||
|
|
stepType: StepType;
|
|||
|
|
status: StepStatus;
|
|||
|
|
timestamp?: string;
|
|||
|
|
operatorName?: string;
|
|||
|
|
notes?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ProcessFlowProps {
|
|||
|
|
nodes: ProcessNode[];
|
|||
|
|
onConfirm?: (nodeId: string, notes?: string) => void;
|
|||
|
|
canConfirm?: boolean;
|
|||
|
|
currentUserName?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const stepConfig: Record<StepType, { label: string; icon: any; color: string }> = {
|
|||
|
|
confirm: { label: '确认计划', icon: Check, color: 'blue' },
|
|||
|
|
yarn_purchase: { label: '采纱', icon: Clock, color: 'amber' },
|
|||
|
|
dyeing: { label: '染纱', icon: Clock, color: 'purple' },
|
|||
|
|
machine_start: { label: '上机', icon: Clock, color: 'indigo' },
|
|||
|
|
fabric_warehouse: { label: '坯布入库', icon: Check, color: 'emerald' },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export function ProcessFlow({ nodes, onConfirm, canConfirm, currentUserName }: ProcessFlowProps) {
|
|||
|
|
const [expandedNode, setExpandedNode] = useState<string | null>(null);
|
|||
|
|
const [notes, setNotes] = useState('');
|
|||
|
|
|
|||
|
|
const sortedNodes = [...nodes].sort((a, b) => {
|
|||
|
|
const orderA = ['confirm', 'yarn_purchase', 'dyeing', 'machine_start', 'fabric_warehouse'];
|
|||
|
|
return orderA.indexOf(a.stepType) - orderA.indexOf(b.stepType);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const handleConfirm = (nodeId: string) => {
|
|||
|
|
if (onConfirm) {
|
|||
|
|
onConfirm(nodeId, notes);
|
|||
|
|
setNotes('');
|
|||
|
|
setExpandedNode(null);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{sortedNodes.map((node, index) => {
|
|||
|
|
const config = stepConfig[node.stepType];
|
|||
|
|
const Icon = config.icon;
|
|||
|
|
const isActive = node.status === 'active';
|
|||
|
|
const isCompleted = node.status === 'completed';
|
|||
|
|
const isPending = node.status === 'pending';
|
|||
|
|
const isExpanded = expandedNode === node.id;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<motion.div
|
|||
|
|
key={node.id}
|
|||
|
|
initial={{ opacity: 0, x: -20 }}
|
|||
|
|
animate={{ opacity: 1, x: 0 }}
|
|||
|
|
transition={{ delay: index * 0.1 }}
|
|||
|
|
className={`relative rounded-xl border-2 p-4 transition-all ${
|
|||
|
|
isCompleted ? 'border-emerald-200 bg-emerald-50' :
|
|||
|
|
isActive ? 'border-blue-200 bg-blue-50' :
|
|||
|
|
'border-gray-200 bg-gray-50'
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
|||
|
|
isCompleted ? 'bg-emerald-500' :
|
|||
|
|
isActive ? 'bg-blue-500' :
|
|||
|
|
'bg-gray-300'
|
|||
|
|
}`}>
|
|||
|
|
<Icon className="w-5 h-5 text-white" />
|
|||
|
|
</div>
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<h4 className="font-medium text-gray-900">{config.label}</h4>
|
|||
|
|
{node.timestamp && (
|
|||
|
|
<p className="text-sm text-gray-500">
|
|||
|
|
{new Date(node.timestamp).toLocaleString('zh-CN')}
|
|||
|
|
{node.operatorName && ` · ${node.operatorName}`}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
{isActive && canConfirm && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => setExpandedNode(isExpanded ? null : node.id)}
|
|||
|
|
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 transition-colors"
|
|||
|
|
>
|
|||
|
|
确认
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{isCompleted && <Check className="w-5 h-5 text-emerald-600" />}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<AnimatePresence>
|
|||
|
|
{isExpanded && (
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ height: 0, opacity: 0 }}
|
|||
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|||
|
|
exit={{ height: 0, opacity: 0 }}
|
|||
|
|
className="mt-3 pt-3 border-t border-blue-200"
|
|||
|
|
>
|
|||
|
|
<textarea
|
|||
|
|
value={notes}
|
|||
|
|
onChange={(e) => setNotes(e.target.value)}
|
|||
|
|
placeholder="添加备注(可选)"
|
|||
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|||
|
|
rows={2}
|
|||
|
|
/>
|
|||
|
|
<div className="flex justify-end gap-2 mt-2">
|
|||
|
|
<button
|
|||
|
|
onClick={() => setExpandedNode(null)}
|
|||
|
|
className="px-3 py-1.5 text-gray-600 hover:text-gray-900 text-sm"
|
|||
|
|
>
|
|||
|
|
取消
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleConfirm(node.id)}
|
|||
|
|
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700"
|
|||
|
|
>
|
|||
|
|
确认完成
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</motion.div>
|
|||
|
|
)}
|
|||
|
|
</AnimatePresence>
|
|||
|
|
</motion.div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
【文件5】src/components/PlanCard.tsx - 计划卡片组件(部分)
|
|||
|
|
───────────────────────────────────────────────────────────────────────────────
|
|||
|
|
import React from 'react';
|
|||
|
|
import { motion } from 'framer-motion';
|
|||
|
|
import { FileText, TrendingUp, Package, Clock } from 'lucide-react';
|
|||
|
|
import type { ProductionPlan } from '../types';
|
|||
|
|
|
|||
|
|
interface PlanCardProps {
|
|||
|
|
plan: ProductionPlan;
|
|||
|
|
onClick?: () => void;
|
|||
|
|
showProgress?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const statusMap = {
|
|||
|
|
pending: { label: '待确定', color: 'text-amber-600', bgColor: 'bg-amber-50', icon: Clock },
|
|||
|
|
producing: { label: '生产中', color: 'text-blue-600', bgColor: 'bg-blue-50', icon: TrendingUp },
|
|||
|
|
completed: { label: '已完成', color: 'text-emerald-600', bgColor: 'bg-emerald-50', icon: Package },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export function PlanCard({ plan, onClick, showProgress = true }: PlanCardProps) {
|
|||
|
|
const status = statusMap[plan.status];
|
|||
|
|
const StatusIcon = status.icon;
|
|||
|
|
const progress = plan.target_quantity > 0
|
|||
|
|
? Math.round((plan.completed_quantity / plan.target_quantity) * 100)
|
|||
|
|
: 0;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<motion.div
|
|||
|
|
whileHover={{ scale: 1.02, y: -2 }}
|
|||
|
|
whileTap={{ scale: 0.98 }}
|
|||
|
|
onClick={onClick}
|
|||
|
|
className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 cursor-pointer hover:shadow-md hover:border-blue-200 transition-all"
|
|||
|
|
>
|
|||
|
|
<div className="flex items-start justify-between mb-3">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${status.bgColor}`}>
|
|||
|
|
<StatusIcon className={`w-4 h-4 ${status.color}`} />
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<h3 className="font-medium text-gray-900 text-sm">{plan.product_name}</h3>
|
|||
|
|
<p className="text-xs text-gray-500">{plan.fabric_code}-{plan.color_code}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<span className={`px-2 py-0.5 text-xs rounded-full ${status.bgColor} ${status.color}`}>
|
|||
|
|
{status.label}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{showProgress && (
|
|||
|
|
<div className="mt-3">
|
|||
|
|
<div className="flex items-center justify-between text-xs text-gray-500 mb-1">
|
|||
|
|
<span>进度</span>
|
|||
|
|
<span>{progress}%</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="h-1.5 bg-gray-100 rounded-full overflow-hidden">
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ width: 0 }}
|
|||
|
|
animate={{ width: `${progress}%` }}
|
|||
|
|
transition={{ duration: 0.5 }}
|
|||
|
|
className="h-full bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-xs text-gray-400 mt-1">
|
|||
|
|
{plan.completed_quantity} / {plan.target_quantity} 米
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</motion.div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
源代码特征说明
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
1. 编程语言:TypeScript (.ts, .tsx)
|
|||
|
|
2. 状态管理:Zustand + Immer
|
|||
|
|
3. 加密算法:XOR对称加密
|
|||
|
|
4. 工具函数:日期格式化、进度计算、排序算法
|
|||
|
|
5. 组件设计:函数组件 + Hooks
|
|||
|
|
6. 动画效果:Framer Motion
|
|||
|
|
7. 样式方案:TailwindCSS
|
|||
|
|
|
|||
|
|
代码特点:
|
|||
|
|
- 使用Zustand进行全局状态管理
|
|||
|
|
- 使用Immer实现不可变状态更新
|
|||
|
|
- 数据加密保护敏感信息
|
|||
|
|
- 组件化设计,高复用性
|
|||
|
|
- 响应式布局支持
|
|||
|
|
- 动画交互流畅
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
第 1 页 / 共 30 页(后30页)
|
|||
|
|
═══════════════════════════════════════════════════════════════════════════════
|