| |
| |
| |
| |
|
|
| import * as XLSX from 'xlsx'; |
| import { ProjectData, ProjectStats, DEFAULT_COLUMN_MAPPING } from '@/types/project'; |
|
|
| |
| |
| |
| function setNestedValue(obj: any, path: string, value: any) { |
| const keys = path.split('.'); |
| let current = obj; |
| |
| for (let i = 0; i < keys.length - 1; i++) { |
| const key = keys[i]; |
| if (!current[key]) { |
| current[key] = {}; |
| } |
| current = current[key]; |
| } |
| |
| current[keys[keys.length - 1]] = value; |
| } |
|
|
| |
| |
| |
| function getNestedValue(obj: any, path: string): any { |
| const keys = path.split('.'); |
| let current = obj; |
| |
| for (const key of keys) { |
| if (current === undefined || current === null) { |
| return undefined; |
| } |
| current = current[key]; |
| } |
| |
| return current; |
| } |
|
|
| |
| |
| |
| function parseExcelDate(value: any): string { |
| if (!value) return ''; |
| |
| |
| if (typeof value === 'number') { |
| try { |
| const date = XLSX.SSF.parse_date_code(value); |
| return `${date.y}-${String(date.m).padStart(2, '0')}-${String(date.d).padStart(2, '0')}`; |
| } catch { |
| return String(value); |
| } |
| } |
| |
| |
| if (typeof value === 'string') { |
| const dateTest = new Date(value); |
| if (!isNaN(dateTest.getTime())) { |
| return dateTest.toISOString().split('T')[0]; |
| } |
| } |
| |
| return String(value); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function formatExcelDate(value: any): string { |
| |
| if (!value || value === '') { |
| return '暂无日期'; |
| } |
| |
| |
| if (typeof value === 'string' && value.includes('月')) { |
| return value; |
| } |
| |
| |
| if (!isNaN(value) && typeof value === 'number') { |
| try { |
| |
| const date = new Date((value - 25569) * 86400 * 1000); |
| const month = date.getMonth() + 1; |
| const day = date.getDate(); |
| return `${month}月${day}日`; |
| } catch { |
| return String(value); |
| } |
| } |
| |
| |
| if (typeof value === 'string') { |
| const dateMatch = value.match(/(\d{4})-(\d{1,2})-(\d{1,2})/); |
| if (dateMatch) { |
| const month = parseInt(dateMatch[2]); |
| const day = parseInt(dateMatch[3]); |
| return `${month}月${day}日`; |
| } |
| } |
| |
| |
| return String(value); |
| } |
|
|
| |
| |
| |
| export function parseExcelFile(file: File): Promise<ProjectData[]> { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| |
| reader.onload = (e) => { |
| try { |
| const data = e.target?.result; |
| const workbook = XLSX.read(data, { type: 'binary' }); |
| const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; |
| const jsonData = XLSX.utils.sheet_to_json(firstSheet, { |
| header: 1, |
| raw: false, |
| dateNF: 'mm"月"dd"日"' |
| }) as any[][]; |
| |
| |
| const headerRowIndex = 2; |
| const headers = jsonData[headerRowIndex] as string[]; |
| |
| |
| const projects: ProjectData[] = []; |
| |
| for (let i = headerRowIndex + 1; i < jsonData.length; i++) { |
| const row = jsonData[i]; |
| |
| |
| if (!row || !row[0] || String(row[0]).includes('列')) { |
| continue; |
| } |
| |
| |
| const project: any = { |
| no: '', |
| identity: {}, |
| milestones: {}, |
| details: {} |
| }; |
| |
| |
| project.no = row[0] !== undefined && row[0] !== null ? String(row[0]) : '-'; |
| |
| |
| headers.forEach((header, index) => { |
| if (index === 0) return; |
| |
| const mappedPath = DEFAULT_COLUMN_MAPPING[header]; |
| if (mappedPath) { |
| const value = row[index]; |
| let processedValue = value !== undefined && value !== null ? String(value) : '-'; |
| |
| |
| if (mappedPath.includes('milestones.') && |
| (mappedPath.includes('projectStart') || mappedPath.includes('t1') || |
| mappedPath.includes('glTime') || mappedPath.includes('vmp') || |
| mappedPath.includes('mp') || mappedPath.includes('detailDate'))) { |
| processedValue = parseExcelDate(value) || '-'; |
| } |
| |
| setNestedValue(project, mappedPath, processedValue); |
| } |
| }); |
| |
| |
| if (project.identity?.projectName && project.identity.projectName !== '-') { |
| projects.push(project as ProjectData); |
| } |
| } |
| |
| resolve(projects); |
| } catch (error) { |
| reject(error); |
| } |
| }; |
| |
| reader.onerror = () => reject(new Error('文件读取失败')); |
| reader.readAsBinaryString(file); |
| }); |
| } |
|
|
| |
| |
| |
| export function getRiskColor(riskLevel: string): string { |
| const level = riskLevel?.trim() || ''; |
| |
| if (level === '高' || level.includes('高')) { |
| return 'rgb(220, 38, 38)'; |
| } else if (level === '中' || level.includes('中')) { |
| return 'rgb(234, 179, 8)'; |
| } else { |
| return 'rgb(34, 197, 94)'; |
| } |
| } |
|
|
| |
| |
| |
| export function getRiskBadgeClass(riskLevel: string): string { |
| const level = riskLevel?.trim() || ''; |
| |
| if (level === '高' || level.includes('高')) { |
| return 'bg-red-100 text-red-700 border-red-300'; |
| } else if (level === '中' || level.includes('中')) { |
| return 'bg-yellow-100 text-yellow-700 border-yellow-300'; |
| } else { |
| return 'bg-green-100 text-green-700 border-green-300'; |
| } |
| } |
|
|
| |
| |
| |
| export function getStatusBadgeClass(status: string): string { |
| const statusText = status?.trim() || ''; |
| |
| if (statusText === '已完成' || statusText.includes('完成')) { |
| return 'bg-green-100 text-green-700 border-green-300'; |
| } else if (statusText === '进行中' || statusText.includes('进行')) { |
| return 'bg-yellow-100 text-yellow-700 border-yellow-300'; |
| } else if (statusText === '已超时' || statusText.includes('超时')) { |
| return 'bg-red-100 text-red-700 border-red-300'; |
| } else { |
| return 'bg-gray-100 text-gray-700 border-gray-300'; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function calculateStats(projects: ProjectData[]): ProjectStats { |
| const stats: ProjectStats = { |
| total: projects.length, |
| highRisk: 0, |
| inProgress: 0, |
| completed: 0 |
| }; |
| |
| projects.forEach(project => { |
| const currentNode = project.milestones?.currentNode?.trim() || ''; |
| |
| |
| if (currentNode === '已完成') { |
| stats.completed++; |
| } else if (currentNode === '已超时') { |
| stats.highRisk++; |
| } else if (currentNode === '进行中') { |
| stats.inProgress++; |
| } |
| |
| }); |
| |
| return stats; |
| } |
|
|
| |
| |
| |
| export function formatDate(dateStr: string): string { |
| if (!dateStr || dateStr === '-' || dateStr === 'N/A') { |
| return '-'; |
| } |
| |
| try { |
| |
| if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { |
| return dateStr; |
| } |
| |
| |
| const date = new Date(dateStr); |
| if (!isNaN(date.getTime())) { |
| return date.toISOString().split('T')[0]; |
| } |
| |
| return dateStr; |
| } catch { |
| return dateStr; |
| } |
| } |
|
|
| |
| |
| |
| export function getDisplayValue(value: string | undefined | null, fallback: string = '-'): string { |
| if (!value || value === 'undefined' || value === 'null' || value === '-') { |
| return fallback; |
| } |
| return value; |
| } |
|
|
| |
| |
| |
| |
| export function isHighRisk(riskLevel: string): boolean { |
| const level = riskLevel?.trim() || ''; |
| return level === '高' || level.includes('高'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function getCardVisualState(currentNode: string | undefined) { |
| const node = currentNode?.trim() || ''; |
| |
| |
| if (node === '已完成') { |
| return { |
| type: 'completed' as const, |
| borderClass: 'border-2 border-emerald-500', |
| bgClass: 'bg-emerald-50', |
| badgeClass: 'bg-emerald-100 text-emerald-700 border-emerald-300', |
| badgeLabel: '已完成', |
| showBadge: true |
| }; |
| } |
| |
| |
| if (node === '已超时') { |
| return { |
| type: 'overdue' as const, |
| borderClass: 'border-2 border-red-600', |
| bgClass: 'bg-red-50', |
| badgeClass: 'bg-red-100 text-red-700 border-red-300', |
| badgeLabel: '已超时', |
| showBadge: true |
| }; |
| } |
| |
| |
| if (node === '进行中') { |
| return { |
| type: 'ongoing' as const, |
| borderClass: 'border-2 border-blue-500', |
| bgClass: 'bg-white', |
| badgeClass: 'bg-blue-100 text-blue-700 border-blue-300', |
| badgeLabel: '进行中', |
| showBadge: true |
| }; |
| } |
| |
| |
| return { |
| type: 'unknown' as const, |
| borderClass: 'border border-slate-200', |
| bgClass: 'bg-white', |
| badgeClass: 'bg-slate-100 text-slate-500 border-slate-300', |
| badgeLabel: 'N/A', |
| showBadge: false |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| export function parseFAIValue(value: string | undefined | null): string { |
| if (!value || value === '-' || value === 'N/A') { |
| return '-'; |
| } |
| |
| const strValue = String(value).trim(); |
| |
| |
| if (strValue.includes('%')) { |
| return strValue; |
| } |
| |
| |
| const numValue = parseFloat(strValue); |
| if (isNaN(numValue)) { |
| return strValue; |
| } |
| |
| |
| if (numValue > 0 && numValue <= 1) { |
| return `${Math.round(numValue * 100)}%`; |
| } |
| |
| |
| if (numValue === 1) { |
| return '100%'; |
| } |
| |
| |
| return `${Math.round(numValue)}%`; |
| } |
|
|
| |
| |
| |
| |
| |
| export function getFAIColorClass(value: string | undefined | null): string { |
| const parsed = parseFAIValue(value); |
| |
| if (parsed === '-' || parsed === 'N/A') { |
| return 'text-gray-500'; |
| } |
| |
| |
| const numericMatch = parsed.match(/\d+/); |
| if (!numericMatch) { |
| return 'text-gray-500'; |
| } |
| |
| const numValue = parseInt(numericMatch[0], 10); |
| |
| if (numValue >= 100) { |
| return 'text-emerald-600 font-bold'; |
| } else { |
| return 'text-red-600 font-bold'; |
| } |
| } |
|
|
| |
| |
| |
| const MODULE_2_FIELDS = [ |
| 'milestones.projectStart', |
| 'milestones.t1', |
| 'milestones.glTime', |
| 'milestones.vmp', |
| 'milestones.mp', |
| 'milestones.currentStage', |
| 'milestones.trialCount', |
| 'milestones.toolingFAI', |
| 'milestones.partFAI', |
| 'milestones.t1SizeQualified', |
| ] as const; |
|
|
| |
| |
| |
| function isFieldEmpty(value: any): boolean { |
| if (value === null || value === undefined) return true; |
| if (typeof value === 'string') { |
| const trimmed = value.trim(); |
| return trimmed === '' || trimmed === '-' || trimmed === 'N/A'; |
| } |
| return false; |
| } |
|
|
| |
| |
| |
| export function calculateCompleteness(project: ProjectData): number { |
| const totalFields = MODULE_2_FIELDS.length; |
| let filledFields = 0; |
|
|
| for (const fieldPath of MODULE_2_FIELDS) { |
| const value = getNestedValue(project, fieldPath); |
| if (!isFieldEmpty(value)) { |
| filledFields++; |
| } |
| } |
|
|
| return Math.round((filledFields / totalFields) * 100); |
| } |
|
|
| |
| |
| |
| export function getMissingFields(project: ProjectData): string[] { |
| const fieldLabels: Record<string, string> = { |
| 'milestones.projectStart': '项目启动时间', |
| 'milestones.t1': 'T1时间', |
| 'milestones.glTime': 'G/L时间', |
| 'milestones.vmp': 'VMP时间', |
| 'milestones.mp': 'MP时间', |
| 'milestones.currentStage': '当前阶段', |
| 'milestones.trialCount': '试模次数', |
| 'milestones.toolingFAI': 'Tooling FAI', |
| 'milestones.partFAI': 'Part FAI', |
| 'milestones.t1SizeQualified': 'T1尺寸是否达标', |
| }; |
|
|
| const missingFields: string[] = []; |
|
|
| for (const fieldPath of MODULE_2_FIELDS) { |
| const value = getNestedValue(project, fieldPath); |
| if (isFieldEmpty(value)) { |
| missingFields.push(fieldLabels[fieldPath] || fieldPath); |
| } |
| } |
|
|
| return missingFields; |
| } |
|
|