diff --git a/Robocopy镜像文件小程序/MirrorFiles v0.1.ps1 b/Robocopy_GUI/MirrorFiles v0.1.ps1 similarity index 100% rename from Robocopy镜像文件小程序/MirrorFiles v0.1.ps1 rename to Robocopy_GUI/MirrorFiles v0.1.ps1 diff --git a/Robocopy镜像文件小程序/MirrorFiles v0.9.ps1 b/Robocopy_GUI/MirrorFiles v0.9.ps1 similarity index 100% rename from Robocopy镜像文件小程序/MirrorFiles v0.9.ps1 rename to Robocopy_GUI/MirrorFiles v0.9.ps1 diff --git a/Robocopy镜像文件小程序/MirrorFiles v1.2.ps1 b/Robocopy_GUI/MirrorFiles v1.2.ps1 similarity index 98% rename from Robocopy镜像文件小程序/MirrorFiles v1.2.ps1 rename to Robocopy_GUI/MirrorFiles v1.2.ps1 index f5b1429..bd89ca1 100644 --- a/Robocopy镜像文件小程序/MirrorFiles v1.2.ps1 +++ b/Robocopy_GUI/MirrorFiles v1.2.ps1 @@ -1,278 +1,278 @@ -param( - [string]$SourcePath, - [string]$TargetPath -) - -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.Drawing - -function Show-GUI { - $form = New-Object System.Windows.Forms.Form - $form.Text = "Folder Mirror Tool" - $form.Size = New-Object System.Drawing.Size(600,450) - $form.StartPosition = "CenterScreen" - $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog - - # Source Controls - $lblSource = New-Object Windows.Forms.Label - $lblSource.Location = New-Object Drawing.Point(10,10) - $lblSource.Text = "Source Path:" - $txtSource = New-Object Windows.Forms.TextBox - $txtSource.Location = New-Object Drawing.Point(120,10) - $txtSource.Size = New-Object Drawing.Size(350,20) - $btnSource = New-Object Windows.Forms.Button - $btnSource.Location = New-Object Drawing.Point(480,10) - $btnSource.Text = "Browse" - $btnSource.Add_Click({ - $folder = New-Object Windows.Forms.FolderBrowserDialog - if($folder.ShowDialog() -eq "OK") { $txtSource.Text = $folder.SelectedPath } - }) - - # Target Controls - $lblTarget = New-Object Windows.Forms.Label - $lblTarget.Location = New-Object Drawing.Point(10,40) - $lblTarget.Text = "Target Path:" - $txtTarget = New-Object Windows.Forms.TextBox - $txtTarget.Location = New-Object Drawing.Point(120,40) - $txtTarget.Size = New-Object Drawing.Size(350,20) - $btnTarget = New-Object Windows.Forms.Button - $btnTarget.Location = New-Object Drawing.Point(480,40) - $btnTarget.Text = "Browse" - $btnTarget.Add_Click({ - $folder = New-Object Windows.Forms.FolderBrowserDialog - if($folder.ShowDialog() -eq "OK") { $txtTarget.Text = $folder.SelectedPath } - }) - - # Log Box - $script:txtLogGui = New-Object Windows.Forms.RichTextBox - $script:txtLogGui.Location = New-Object Drawing.Point(10,70) - $script:txtLogGui.Size = New-Object Drawing.Size(560,300) - $script:txtLogGui.ReadOnly = $true - $script:txtLogGui.Font = New-Object System.Drawing.Font("Consolas", 8) - $script:txtLogGui.WordWrap = $false - $script:txtLogGui.ScrollBars = [System.Windows.Forms.RichTextBoxScrollBars]::Both - - # Start Button - $script:btnStartGui = New-Object Windows.Forms.Button - $script:btnStartGui.Location = New-Object Drawing.Point(480,380) - $script:btnStartGui.Size = New-Object Drawing.Size(90,30) - $script:btnStartGui.Text = "Start Mirror" - $script:btnStartGui.Add_Click({ - $currentSourcePath = $txtSource.Text - $currentTargetPath = $txtTarget.Text - - try { - if (-not (Test-Path $currentSourcePath -PathType Container)) { throw "Source path invalid or not a folder." } - if (-not (Test-Path $currentTargetPath -PathType Container)) { - try { - $script:txtLogGui.AppendText("Target path does not exist. Attempting to create: $currentTargetPath`n") - $null = New-Item -ItemType Directory -Path $currentTargetPath -Force -ErrorAction Stop - $script:txtLogGui.AppendText("Created target directory: $currentTargetPath`n") - } catch { - throw "Target path invalid or not a folder, and could not be created: $($_.Exception.Message)" - } - } - - $script:txtLogGui.Clear() - $script:txtLogGui.AppendText("Starting mirror operation...`n") - $script:txtLogGui.AppendText("Source: $currentSourcePath`n") - $script:txtLogGui.AppendText("Target: $currentTargetPath`n") - - $global:logFile = "$PSScriptRoot\MirrorLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" - $script:txtLogGui.AppendText("Detailed log will also be saved to: $global:logFile`n`n") - - # Robocopy parameters for GUI (Verbose, but no per-file % progress to keep GUI clean) - $robocopyArgs = @( - "`"$currentSourcePath`"", - "`"$currentTargetPath`"", - "/MIR", # Mirror mode - "/V", # Verbose output (shows skipped files) - "/FP", # Include Full Pathname of files in output - "/BYTES", # Print sizes as bytes - "/NP", # No Progress percentage (to keep GUI log cleaner) - # Removed /NDL (No Directory List) -> will show directory list - # Removed /NJH (No Job Header) -> will show job header - # Removed /NJS (No Job Summary) -> will show job summary - "/LOG+:`"$global:logFile`"", - "/R:5", # 5 retries on failed files - "/W:5" # 5 second wait between retries - ) - - $script:btnStartGui.Enabled = $false - - $process = New-Object System.Diagnostics.Process - $process.StartInfo.FileName = "robocopy.exe" - $process.StartInfo.Arguments = $robocopyArgs -join " " - $process.StartInfo.UseShellExecute = $false - $process.StartInfo.RedirectStandardOutput = $true - $process.StartInfo.RedirectStandardError = $true - $process.StartInfo.CreateNoWindow = $true - $process.EnableRaisingEvents = $true - - $process.add_OutputDataReceived({ - if (-not [string]::IsNullOrEmpty($_.Data)) { - $script:txtLogGui.BeginInvoke([Action[string]]{ - param($line) - $script:txtLogGui.AppendText("$line`r`n") - $script:txtLogGui.ScrollToCaret() - }, $_.Data) - } - }) - - $process.add_ErrorDataReceived({ - if (-not [string]::IsNullOrEmpty($_.Data)) { - $script:txtLogGui.BeginInvoke([Action[string]]{ - param($line) - # Robocopy often uses stderr for summary or non-critical info too, - # so not always coloring it as a hard error. - # Let Robocopy's output speak for itself mostly. - $script:txtLogGui.AppendText("[STDERR] $line`r`n") - $script:txtLogGui.ScrollToCaret() - }, $_.Data) - } - }) - - $process.add_Exited({ - $exitCode = $process.ExitCode - $script:txtLogGui.BeginInvoke([Action]{ - $script:txtLogGui.AppendText("`n------------------------------------`n") - $script:txtLogGui.AppendText("Robocopy process finished.`n") - $script:txtLogGui.AppendText("Robocopy Exit Code: $exitCode`n") - - if ($exitCode -ge 8) { - $script:txtLogGui.SelectionStart = $script:txtLogGui.TextLength - $script:txtLogGui.SelectionLength = 0 - $script:txtLogGui.SelectionColor = [System.Drawing.Color]::Red - $script:txtLogGui.AppendText("Robocopy reported errors or critical failures (Code >= 8). Check log for details.`n") - $script:txtLogGui.SelectionColor = $script:txtLogGui.ForeColor - [Windows.Forms.MessageBox]::Show("Robocopy completed with errors or critical failures (Exit Code: $exitCode). Check Robocopy output and the log file for details.", "Robocopy Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) - } elseif ($exitCode -eq 0) { - $script:txtLogGui.AppendText("Robocopy: No files were copied. No failure was encountered. No files were mismatched. The files already exist in the destination directory; therefore, the copy operation was skipped.`n") - } elseif ($exitCode -gt 0 -and $exitCode -lt 8) { - $script:txtLogGui.SelectionStart = $script:txtLogGui.TextLength - $script:txtLogGui.SelectionLength = 0 - $script:txtLogGui.SelectionColor = [System.Drawing.Color]::DarkGreen - $script:txtLogGui.AppendText("Robocopy completed. Some files may have been copied, or extra/mismatched files detected (Code $exitCode). This is generally considered successful or informational. Review output for details.`n") - $script:txtLogGui.SelectionColor = $script:txtLogGui.ForeColor - } - $script:txtLogGui.AppendText("Log file: $global:logFile`n") - $script:txtLogGui.ScrollToCaret() - $script:btnStartGui.Enabled = $true - }) - $process.Close() - $process.Dispose() - }) - - $process.Start() | Out-Null - $process.BeginOutputReadLine() - $process.BeginErrorReadLine() - } - catch { - $errorMessage = $_.Exception.Message - if ($_.Exception.InnerException) { $errorMessage += " Inner: " + $_.Exception.InnerException.Message } - $script:txtLogGui.AppendText("[SCRIPT ERROR] $errorMessage`n") - [Windows.Forms.MessageBox]::Show($errorMessage, "Script Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) - if ($script:btnStartGui) {$script:btnStartGui.Enabled = $true} - } - }) - - $form.Controls.AddRange(@( - $lblSource, $txtSource, $btnSource, - $lblTarget, $txtTarget, $btnTarget, - $script:txtLogGui, - $script:btnStartGui - )) - - if ($SourcePath) { $txtSource.Text = $SourcePath } - if ($TargetPath) { $txtTarget.Text = $TargetPath } - - $form.Add_Shown({ - # If parameters were passed and GUI is shown, optionally auto-start - # For now, we require manual click even if params are pre-filled. - # if ($txtSource.Text -and $txtTarget.Text) { $script:btnStartGui.PerformClick() } - }) - - [void]$form.ShowDialog() -} - -# --- Main script execution logic --- - -if (-not $PSBoundParameters.ContainsKey('SourcePath') -or -not $PSBoundParameters.ContainsKey('TargetPath')) { - Show-GUI # Pass potentially existing parameters for pre-filling -} -else { - $cliErrorOccurred = $false - try { - if (-not (Test-Path $SourcePath -PathType Container)) { throw "Source path '$SourcePath' invalid or not a folder." } - if (-not (Test-Path $TargetPath -PathType Container)) { - Write-Host "Target path '$TargetPath' does not exist. Attempting to create..." -ForegroundColor Yellow - try { - $null = New-Item -ItemType Directory -Path $TargetPath -Force -ErrorAction Stop - Write-Host "Created target directory: $TargetPath" -ForegroundColor Green - } catch { - throw "Target path '$TargetPath' invalid or not a folder, and could not be created: $($_.Exception.Message)" - } - } - - $logFile = "$PSScriptRoot\MirrorLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" - Write-Host "[$(Get-Date)] Starting mirror operation..." - - Write-Host "Source: $SourcePath" - Write-Host "Target: $TargetPath" - Write-Host "Log file: $logFile" - - # Robocopy parameters for CLI (Fully verbose, including progress) - $robocopyArgs = @( - "`"$SourcePath`"", - "`"$TargetPath`"", - "/MIR", # Mirror mode - "/V", # Verbose output (shows skipped files) - "/FP", # Include Full Pathname of files in output - "/BYTES", # Print sizes as bytes - # Removed /NP -> will show progress percentage - # Removed /NDL -> will show directory list - # Removed /NJH -> will show job header - # Removed /NJS -> will show job summary - "/TEE", # Output to console AND log file - "/LOG+:`"$logFile`"", - "/R:5", - "/W:5" - ) - - Write-Host "Executing: robocopy $($robocopyArgs -join " ")" - Write-Host "------------------------------------ ROBOCOPY OUTPUT START ------------------------------------" - $process = Start-Process robocopy -ArgumentList $robocopyArgs -NoNewWindow -PassThru -Wait - Write-Host "------------------------------------ ROBOCOPY OUTPUT END --------------------------------------" - Write-Host "Robocopy Exit Code: $($process.ExitCode)" - - - if ($process.ExitCode -ge 8) { - throw "Robocopy reported errors or critical failures with exit code $($process.ExitCode). Check output above and log: $logFile" - } elseif ($process.ExitCode -eq 0) { - Write-Host "[$(Get-Date)] Robocopy: No files were copied. No failure was encountered. No files were mismatched. (Exit Code: 0)" -ForegroundColor Cyan - } else { # 1 to 7 - Write-Host "[$(Get-Date)] Robocopy completed. Some files may have been copied, or extra/mismatched files detected (Exit Code: $($process.ExitCode)). This is generally considered successful or informational. Review output for details." -ForegroundColor Cyan - } - - Write-Host "[$(Get-Date)] Mirror operation main phase completed." -ForegroundColor Green - if ($process.ExitTime -and $process.StartTime) { - Write-Host "Total Robocopy processing time: $($process.ExitTime - $process.StartTime)" - } - } - catch { - Write-Host "[SCRIPT ERROR] $($_.Exception.Message)" -ForegroundColor Red - if ($_.Exception.InnerException) { Write-Host "[INNER SCRIPT ERROR] $($_.Exception.InnerException.Message)" -ForegroundColor Red } - $cliErrorOccurred = $true - } - finally { - if ($cliErrorOccurred) { - Write-Host "Operation finished with errors." -ForegroundColor Red - } else { - Write-Host "Operation finished." -ForegroundColor Green - } - Read-Host "Press Enter to exit..." - if ($cliErrorOccurred) { - if ($Host.Name -eq "ConsoleHost") { exit 1 } # Exit with error code for automation - } - } +param( + [string]$SourcePath, + [string]$TargetPath +) + +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +function Show-GUI { + $form = New-Object System.Windows.Forms.Form + $form.Text = "Folder Mirror Tool" + $form.Size = New-Object System.Drawing.Size(600,450) + $form.StartPosition = "CenterScreen" + $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog + + # Source Controls + $lblSource = New-Object Windows.Forms.Label + $lblSource.Location = New-Object Drawing.Point(10,10) + $lblSource.Text = "Source Path:" + $txtSource = New-Object Windows.Forms.TextBox + $txtSource.Location = New-Object Drawing.Point(120,10) + $txtSource.Size = New-Object Drawing.Size(350,20) + $btnSource = New-Object Windows.Forms.Button + $btnSource.Location = New-Object Drawing.Point(480,10) + $btnSource.Text = "Browse" + $btnSource.Add_Click({ + $folder = New-Object Windows.Forms.FolderBrowserDialog + if($folder.ShowDialog() -eq "OK") { $txtSource.Text = $folder.SelectedPath } + }) + + # Target Controls + $lblTarget = New-Object Windows.Forms.Label + $lblTarget.Location = New-Object Drawing.Point(10,40) + $lblTarget.Text = "Target Path:" + $txtTarget = New-Object Windows.Forms.TextBox + $txtTarget.Location = New-Object Drawing.Point(120,40) + $txtTarget.Size = New-Object Drawing.Size(350,20) + $btnTarget = New-Object Windows.Forms.Button + $btnTarget.Location = New-Object Drawing.Point(480,40) + $btnTarget.Text = "Browse" + $btnTarget.Add_Click({ + $folder = New-Object Windows.Forms.FolderBrowserDialog + if($folder.ShowDialog() -eq "OK") { $txtTarget.Text = $folder.SelectedPath } + }) + + # Log Box + $script:txtLogGui = New-Object Windows.Forms.RichTextBox + $script:txtLogGui.Location = New-Object Drawing.Point(10,70) + $script:txtLogGui.Size = New-Object Drawing.Size(560,300) + $script:txtLogGui.ReadOnly = $true + $script:txtLogGui.Font = New-Object System.Drawing.Font("Consolas", 8) + $script:txtLogGui.WordWrap = $false + $script:txtLogGui.ScrollBars = [System.Windows.Forms.RichTextBoxScrollBars]::Both + + # Start Button + $script:btnStartGui = New-Object Windows.Forms.Button + $script:btnStartGui.Location = New-Object Drawing.Point(480,380) + $script:btnStartGui.Size = New-Object Drawing.Size(90,30) + $script:btnStartGui.Text = "Start Mirror" + $script:btnStartGui.Add_Click({ + $currentSourcePath = $txtSource.Text + $currentTargetPath = $txtTarget.Text + + try { + if (-not (Test-Path $currentSourcePath -PathType Container)) { throw "Source path invalid or not a folder." } + if (-not (Test-Path $currentTargetPath -PathType Container)) { + try { + $script:txtLogGui.AppendText("Target path does not exist. Attempting to create: $currentTargetPath`n") + $null = New-Item -ItemType Directory -Path $currentTargetPath -Force -ErrorAction Stop + $script:txtLogGui.AppendText("Created target directory: $currentTargetPath`n") + } catch { + throw "Target path invalid or not a folder, and could not be created: $($_.Exception.Message)" + } + } + + $script:txtLogGui.Clear() + $script:txtLogGui.AppendText("Starting mirror operation...`n") + $script:txtLogGui.AppendText("Source: $currentSourcePath`n") + $script:txtLogGui.AppendText("Target: $currentTargetPath`n") + + $global:logFile = "$PSScriptRoot\MirrorLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" + $script:txtLogGui.AppendText("Detailed log will also be saved to: $global:logFile`n`n") + + # Robocopy parameters for GUI (Verbose, but no per-file % progress to keep GUI clean) + $robocopyArgs = @( + "`"$currentSourcePath`"", + "`"$currentTargetPath`"", + "/MIR", # Mirror mode + "/V", # Verbose output (shows skipped files) + "/FP", # Include Full Pathname of files in output + "/BYTES", # Print sizes as bytes + "/NP", # No Progress percentage (to keep GUI log cleaner) + # Removed /NDL (No Directory List) -> will show directory list + # Removed /NJH (No Job Header) -> will show job header + # Removed /NJS (No Job Summary) -> will show job summary + "/LOG+:`"$global:logFile`"", + "/R:5", # 5 retries on failed files + "/W:5" # 5 second wait between retries + ) + + $script:btnStartGui.Enabled = $false + + $process = New-Object System.Diagnostics.Process + $process.StartInfo.FileName = "robocopy.exe" + $process.StartInfo.Arguments = $robocopyArgs -join " " + $process.StartInfo.UseShellExecute = $false + $process.StartInfo.RedirectStandardOutput = $true + $process.StartInfo.RedirectStandardError = $true + $process.StartInfo.CreateNoWindow = $true + $process.EnableRaisingEvents = $true + + $process.add_OutputDataReceived({ + if (-not [string]::IsNullOrEmpty($_.Data)) { + $script:txtLogGui.BeginInvoke([Action[string]]{ + param($line) + $script:txtLogGui.AppendText("$line`r`n") + $script:txtLogGui.ScrollToCaret() + }, $_.Data) + } + }) + + $process.add_ErrorDataReceived({ + if (-not [string]::IsNullOrEmpty($_.Data)) { + $script:txtLogGui.BeginInvoke([Action[string]]{ + param($line) + # Robocopy often uses stderr for summary or non-critical info too, + # so not always coloring it as a hard error. + # Let Robocopy's output speak for itself mostly. + $script:txtLogGui.AppendText("[STDERR] $line`r`n") + $script:txtLogGui.ScrollToCaret() + }, $_.Data) + } + }) + + $process.add_Exited({ + $exitCode = $process.ExitCode + $script:txtLogGui.BeginInvoke([Action]{ + $script:txtLogGui.AppendText("`n------------------------------------`n") + $script:txtLogGui.AppendText("Robocopy process finished.`n") + $script:txtLogGui.AppendText("Robocopy Exit Code: $exitCode`n") + + if ($exitCode -ge 8) { + $script:txtLogGui.SelectionStart = $script:txtLogGui.TextLength + $script:txtLogGui.SelectionLength = 0 + $script:txtLogGui.SelectionColor = [System.Drawing.Color]::Red + $script:txtLogGui.AppendText("Robocopy reported errors or critical failures (Code >= 8). Check log for details.`n") + $script:txtLogGui.SelectionColor = $script:txtLogGui.ForeColor + [Windows.Forms.MessageBox]::Show("Robocopy completed with errors or critical failures (Exit Code: $exitCode). Check Robocopy output and the log file for details.", "Robocopy Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + } elseif ($exitCode -eq 0) { + $script:txtLogGui.AppendText("Robocopy: No files were copied. No failure was encountered. No files were mismatched. The files already exist in the destination directory; therefore, the copy operation was skipped.`n") + } elseif ($exitCode -gt 0 -and $exitCode -lt 8) { + $script:txtLogGui.SelectionStart = $script:txtLogGui.TextLength + $script:txtLogGui.SelectionLength = 0 + $script:txtLogGui.SelectionColor = [System.Drawing.Color]::DarkGreen + $script:txtLogGui.AppendText("Robocopy completed. Some files may have been copied, or extra/mismatched files detected (Code $exitCode). This is generally considered successful or informational. Review output for details.`n") + $script:txtLogGui.SelectionColor = $script:txtLogGui.ForeColor + } + $script:txtLogGui.AppendText("Log file: $global:logFile`n") + $script:txtLogGui.ScrollToCaret() + $script:btnStartGui.Enabled = $true + }) + $process.Close() + $process.Dispose() + }) + + $process.Start() | Out-Null + $process.BeginOutputReadLine() + $process.BeginErrorReadLine() + } + catch { + $errorMessage = $_.Exception.Message + if ($_.Exception.InnerException) { $errorMessage += " Inner: " + $_.Exception.InnerException.Message } + $script:txtLogGui.AppendText("[SCRIPT ERROR] $errorMessage`n") + [Windows.Forms.MessageBox]::Show($errorMessage, "Script Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + if ($script:btnStartGui) {$script:btnStartGui.Enabled = $true} + } + }) + + $form.Controls.AddRange(@( + $lblSource, $txtSource, $btnSource, + $lblTarget, $txtTarget, $btnTarget, + $script:txtLogGui, + $script:btnStartGui + )) + + if ($SourcePath) { $txtSource.Text = $SourcePath } + if ($TargetPath) { $txtTarget.Text = $TargetPath } + + $form.Add_Shown({ + # If parameters were passed and GUI is shown, optionally auto-start + # For now, we require manual click even if params are pre-filled. + # if ($txtSource.Text -and $txtTarget.Text) { $script:btnStartGui.PerformClick() } + }) + + [void]$form.ShowDialog() +} + +# --- Main script execution logic --- + +if (-not $PSBoundParameters.ContainsKey('SourcePath') -or -not $PSBoundParameters.ContainsKey('TargetPath')) { + Show-GUI # Pass potentially existing parameters for pre-filling +} +else { + $cliErrorOccurred = $false + try { + if (-not (Test-Path $SourcePath -PathType Container)) { throw "Source path '$SourcePath' invalid or not a folder." } + if (-not (Test-Path $TargetPath -PathType Container)) { + Write-Host "Target path '$TargetPath' does not exist. Attempting to create..." -ForegroundColor Yellow + try { + $null = New-Item -ItemType Directory -Path $TargetPath -Force -ErrorAction Stop + Write-Host "Created target directory: $TargetPath" -ForegroundColor Green + } catch { + throw "Target path '$TargetPath' invalid or not a folder, and could not be created: $($_.Exception.Message)" + } + } + + $logFile = "$PSScriptRoot\MirrorLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" + Write-Host "[$(Get-Date)] Starting mirror operation..." + + Write-Host "Source: $SourcePath" + Write-Host "Target: $TargetPath" + Write-Host "Log file: $logFile" + + # Robocopy parameters for CLI (Fully verbose, including progress) + $robocopyArgs = @( + "`"$SourcePath`"", + "`"$TargetPath`"", + "/MIR", # Mirror mode + "/V", # Verbose output (shows skipped files) + "/FP", # Include Full Pathname of files in output + "/BYTES", # Print sizes as bytes + # Removed /NP -> will show progress percentage + # Removed /NDL -> will show directory list + # Removed /NJH -> will show job header + # Removed /NJS -> will show job summary + "/TEE", # Output to console AND log file + "/LOG+:`"$logFile`"", + "/R:5", + "/W:5" + ) + + Write-Host "Executing: robocopy $($robocopyArgs -join " ")" + Write-Host "------------------------------------ ROBOCOPY OUTPUT START ------------------------------------" + $process = Start-Process robocopy -ArgumentList $robocopyArgs -NoNewWindow -PassThru -Wait + Write-Host "------------------------------------ ROBOCOPY OUTPUT END --------------------------------------" + Write-Host "Robocopy Exit Code: $($process.ExitCode)" + + + if ($process.ExitCode -ge 8) { + throw "Robocopy reported errors or critical failures with exit code $($process.ExitCode). Check output above and log: $logFile" + } elseif ($process.ExitCode -eq 0) { + Write-Host "[$(Get-Date)] Robocopy: No files were copied. No failure was encountered. No files were mismatched. (Exit Code: 0)" -ForegroundColor Cyan + } else { # 1 to 7 + Write-Host "[$(Get-Date)] Robocopy completed. Some files may have been copied, or extra/mismatched files detected (Exit Code: $($process.ExitCode)). This is generally considered successful or informational. Review output for details." -ForegroundColor Cyan + } + + Write-Host "[$(Get-Date)] Mirror operation main phase completed." -ForegroundColor Green + if ($process.ExitTime -and $process.StartTime) { + Write-Host "Total Robocopy processing time: $($process.ExitTime - $process.StartTime)" + } + } + catch { + Write-Host "[SCRIPT ERROR] $($_.Exception.Message)" -ForegroundColor Red + if ($_.Exception.InnerException) { Write-Host "[INNER SCRIPT ERROR] $($_.Exception.InnerException.Message)" -ForegroundColor Red } + $cliErrorOccurred = $true + } + finally { + if ($cliErrorOccurred) { + Write-Host "Operation finished with errors." -ForegroundColor Red + } else { + Write-Host "Operation finished." -ForegroundColor Green + } + Read-Host "Press Enter to exit..." + if ($cliErrorOccurred) { + if ($Host.Name -eq "ConsoleHost") { exit 1 } # Exit with error code for automation + } + } } \ No newline at end of file diff --git a/Robocopy镜像文件小程序/RobocopyMirrorGUI.spec b/Robocopy_GUI/RobocopyMirrorGUI.spec similarity index 100% rename from Robocopy镜像文件小程序/RobocopyMirrorGUI.spec rename to Robocopy_GUI/RobocopyMirrorGUI.spec diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/Analysis-00.toc b/Robocopy_GUI/build/RobocopyMirrorGUI/Analysis-00.toc similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/Analysis-00.toc rename to Robocopy_GUI/build/RobocopyMirrorGUI/Analysis-00.toc diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/EXE-00.toc b/Robocopy_GUI/build/RobocopyMirrorGUI/EXE-00.toc similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/EXE-00.toc rename to Robocopy_GUI/build/RobocopyMirrorGUI/EXE-00.toc diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/PKG-00.toc b/Robocopy_GUI/build/RobocopyMirrorGUI/PKG-00.toc similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/PKG-00.toc rename to Robocopy_GUI/build/RobocopyMirrorGUI/PKG-00.toc diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/PYZ-00.pyz b/Robocopy_GUI/build/RobocopyMirrorGUI/PYZ-00.pyz similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/PYZ-00.pyz rename to Robocopy_GUI/build/RobocopyMirrorGUI/PYZ-00.pyz diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/PYZ-00.toc b/Robocopy_GUI/build/RobocopyMirrorGUI/PYZ-00.toc similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/PYZ-00.toc rename to Robocopy_GUI/build/RobocopyMirrorGUI/PYZ-00.toc diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/RobocopyMirrorGUI.pkg b/Robocopy_GUI/build/RobocopyMirrorGUI/RobocopyMirrorGUI.pkg similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/RobocopyMirrorGUI.pkg rename to Robocopy_GUI/build/RobocopyMirrorGUI/RobocopyMirrorGUI.pkg diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/base_library.zip b/Robocopy_GUI/build/RobocopyMirrorGUI/base_library.zip similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/base_library.zip rename to Robocopy_GUI/build/RobocopyMirrorGUI/base_library.zip diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/pyimod01_archive.pyc b/Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/pyimod01_archive.pyc similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/pyimod01_archive.pyc rename to Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/pyimod01_archive.pyc diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/pyimod02_importers.pyc b/Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/pyimod02_importers.pyc similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/pyimod02_importers.pyc rename to Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/pyimod02_importers.pyc diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/pyimod03_ctypes.pyc b/Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/pyimod03_ctypes.pyc similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/pyimod03_ctypes.pyc rename to Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/pyimod03_ctypes.pyc diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/pyimod04_pywin32.pyc b/Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/pyimod04_pywin32.pyc similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/pyimod04_pywin32.pyc rename to Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/pyimod04_pywin32.pyc diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/struct.pyc b/Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/struct.pyc similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/localpycs/struct.pyc rename to Robocopy_GUI/build/RobocopyMirrorGUI/localpycs/struct.pyc diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/warn-RobocopyMirrorGUI.txt b/Robocopy_GUI/build/RobocopyMirrorGUI/warn-RobocopyMirrorGUI.txt similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/warn-RobocopyMirrorGUI.txt rename to Robocopy_GUI/build/RobocopyMirrorGUI/warn-RobocopyMirrorGUI.txt diff --git a/Robocopy镜像文件小程序/build/RobocopyMirrorGUI/xref-RobocopyMirrorGUI.html b/Robocopy_GUI/build/RobocopyMirrorGUI/xref-RobocopyMirrorGUI.html similarity index 100% rename from Robocopy镜像文件小程序/build/RobocopyMirrorGUI/xref-RobocopyMirrorGUI.html rename to Robocopy_GUI/build/RobocopyMirrorGUI/xref-RobocopyMirrorGUI.html diff --git a/Robocopy镜像文件小程序/dist/RobocopyMirrorGUI.exe b/Robocopy_GUI/dist/RobocopyMirrorGUI.exe similarity index 100% rename from Robocopy镜像文件小程序/dist/RobocopyMirrorGUI.exe rename to Robocopy_GUI/dist/RobocopyMirrorGUI.exe diff --git a/Robocopy镜像文件小程序/robocopy_mirror_gui.py b/Robocopy_GUI/robocopy_mirror_gui.py similarity index 100% rename from Robocopy镜像文件小程序/robocopy_mirror_gui.py rename to Robocopy_GUI/robocopy_mirror_gui.py diff --git a/课堂点名小程序/rollcall_icon.png b/课堂点名小程序/rollcall_icon.png deleted file mode 100644 index ab771a2..0000000 Binary files a/课堂点名小程序/rollcall_icon.png and /dev/null differ diff --git a/课堂点名小程序/课堂随机点名.html b/课堂点名小程序/课堂随机点名.html deleted file mode 100644 index fb16ac5..0000000 --- a/课堂点名小程序/课堂随机点名.html +++ /dev/null @@ -1,510 +0,0 @@ - - - - 课堂随机点名小程序 - - - -
-

课堂点名小程序

-
已点名: 0/0 人
-
请先加载学生名单
-
-
-
- -

- 格式: 每行一人, 学号,姓名,拼音 (例如: 1001,张三,Zhang San)
- 或 学号,姓名 (例如: 1002,李四)。以 # 开头的行为注释。 -

-
- -
- - -
- - -
-
- - - - - - - - - - \ No newline at end of file diff --git a/课堂点名小程序/课堂随机点名.py b/课堂点名小程序/课堂随机点名.py deleted file mode 100644 index 5c5f69f..0000000 --- a/课堂点名小程序/课堂随机点名.py +++ /dev/null @@ -1,490 +0,0 @@ -import tkinter as tk -# Add PhotoImage to the import -from tkinter import ttk, filedialog, messagebox, PhotoImage -import random -# import time -import os -import csv -from datetime import date -from pypinyin import pinyin, Style -import sys # Import sys for path joining - -class RollCallApp: - def __init__(self, master): - self.master = master - master.title("杨昱幸老师的课堂点名系统") - - # --- Set Window Icon --- - try: - # Determine the base path (works for scripts and frozen executables) - if getattr(sys, 'frozen', False): - base_path = sys._MEIPASS # PyInstaller temp folder - else: - base_path = os.path.dirname(os.path.abspath(__file__)) - - icon_path = os.path.join(base_path, 'rollcall_icon.png') # Make sure 'rollcall_icon.png' exists here! - - if os.path.exists(icon_path): - # Use PhotoImage for broader compatibility (PNG, GIF) - app_icon = PhotoImage(file=icon_path) - master.iconphoto(True, app_icon) # The 'True' makes it the default icon - else: - print(f"Warning: Icon file not found at {icon_path}. Using default icon.") - except tk.TclError as e: - # Handle cases where the image format might be unsupported or file is corrupt - print(f"Warning: Could not load icon '{icon_path}': {e}. Using default icon.") - except Exception as e: - # Catch any other potential errors during icon loading - print(f"Warning: An unexpected error occurred while loading the icon: {e}. Using default icon.") - # --- End Set Window Icon --- - - - master.minsize(width=1100, height=700) - - # ... (the rest of your __init__ code remains exactly the same) ... - # --- Define Font Families (User specified) --- - self.font_family_ui = "微软雅黑" - self.font_family_display = "华文中宋" - - # --- Define Font Sizes (Slightly adjusted for modern feel, easy to revert) --- - self.font_size_standard = 14 - self.font_size_counter = 18 - self.font_size_display = 42 - self.font_size_title = 28 - - self.font_title_tuple = (self.font_family_ui, self.font_size_title, "bold") - self.font_counter_tuple = (self.font_family_ui, self.font_size_counter) - self.font_display_tuple = (self.font_family_display, self.font_size_display, "bold") - self.font_standard_tuple = (self.font_family_ui, self.font_size_standard) - self.font_button_tuple = (self.font_family_ui, self.font_size_standard, "bold") - self.font_footer_tuple = (self.font_family_ui, 11) - - - # --- Style and Colors --- - self.style = ttk.Style(master) - try: - self.style.theme_use('clam') - except tk.TclError: - print("Clam theme not available, using default theme. Styling will still be applied.") - - self.COLOR_BACKGROUND = "#ECEFF1" - self.COLOR_TEXT = "#263238" - self.COLOR_PRIMARY = "#007BFF" - self.COLOR_PRIMARY_DARK = "#0056b3" - self.COLOR_PRIMARY_HOVER = "#0069D9" - self.COLOR_PRIMARY_TEXT = "#FFFFFF" - self.COLOR_SECONDARY_BG = "#FFFFFF" - self.COLOR_BORDER = "#B0BEC5" - self.COLOR_DISABLED_FG = "#78909C" - self.COLOR_DISABLED_BG = "#CFD8DC" - - self.COLOR_NAME_DISPLAY_DEFAULT_BG = self.COLOR_SECONDARY_BG - self.COLOR_NAME_DISPLAY_DEFAULT_FG = self.COLOR_TEXT - self.COLOR_NAME_DISPLAY_FLASH_BG = "#FFF9C4" # Pale Yellow (Material Design Yellow 100) - self.COLOR_NAME_DISPLAY_FLASH_FG = self.COLOR_TEXT - self.COLOR_NAME_DISPLAY_SELECTED_BG = "#C8E6C9" # Pale Green (Material Design Green 100) - self.COLOR_NAME_DISPLAY_SELECTED_FG = "#2E7D32" # Dark Green (Material Design Green 800) - self.COLOR_NAME_DISPLAY_ABSENT_BG = "#FFCDD2" # Pale Red (Material Design Red 100) - self.COLOR_NAME_DISPLAY_ABSENT_FG = "#C62828" # Dark Red (Material Design Red 800) - - master.configure(bg=self.COLOR_BACKGROUND) - - self.style.configure(".", background=self.COLOR_BACKGROUND, foreground=self.COLOR_TEXT, - font=self.font_standard_tuple) - self.style.configure("TFrame", background=self.COLOR_BACKGROUND) - - self.style.configure("TLabel", background=self.COLOR_BACKGROUND, foreground=self.COLOR_TEXT) - self.style.configure("Title.TLabel", font=self.font_title_tuple, foreground=self.COLOR_PRIMARY) - self.style.configure("Counter.TLabel", font=self.font_counter_tuple) - self.style.configure("Value.TLabel", font=self.font_standard_tuple) - self.style.configure("Footer.TLabel", font=self.font_footer_tuple, foreground="#546E7A") - - self.style.configure("TButton", font=self.font_button_tuple, padding=(12, 7), relief="raised") - self.style.map("TButton", - foreground=[('disabled', self.COLOR_DISABLED_FG), - ('pressed', self.COLOR_PRIMARY_TEXT), - ('active', self.COLOR_PRIMARY_TEXT), - ('!disabled', self.COLOR_PRIMARY_TEXT)], - background=[('disabled', self.COLOR_DISABLED_BG), - ('pressed', self.COLOR_PRIMARY_DARK), - ('active', self.COLOR_PRIMARY_HOVER), - ('!disabled', self.COLOR_PRIMARY)], - relief=[('pressed', 'sunken'), ('!pressed', 'raised')]) - - self.style.configure("TEntry", font=self.font_standard_tuple, fieldbackground=self.COLOR_SECONDARY_BG, - relief="flat") - self.style.map("TEntry", - bordercolor=[('focus', self.COLOR_PRIMARY), ('!focus', self.COLOR_BORDER)], - borderwidth=[('focus', 1), ('!focus', 1)], - lightcolor=[('focus', self.COLOR_PRIMARY)]) - - self.style.configure("Horizontal.TScale", background=self.COLOR_BACKGROUND, troughcolor=self.COLOR_SECONDARY_BG) - - # --- Data storage --- - self.all_students = [] - self.remaining_students = [] - self.called_students = [] - self.absent_students = [] - self.current_student_info = None - self.flash_timer = None - self.stop_timer = None - self.is_flashing = False - self.file_path = tk.StringVar(value="尚未选择文件") - self.flash_duration_var = tk.IntVar(value=1) - self.flash_interval_var = tk.IntVar(value=25) - - # --- UI Elements --- - self.title_label = ttk.Label(master, text="杨昱幸老师的课堂点名系统", style="Title.TLabel") - self.title_label.pack(pady=(25, 15)) - - self.counter_label = ttk.Label(master, text="已点名:0/0人 (总人数:0)", style="Counter.TLabel") - self.counter_label.pack(pady=(0, 20)) - - self.name_display_border_frame = tk.Frame(master, background=self.COLOR_BORDER, relief="flat", bd=1) - self.name_display_border_frame.pack(pady=10, padx=30, fill=tk.X) - self.name_display = tk.Label(self.name_display_border_frame, text="请先加载名单", font=self.font_display_tuple, - bg=self.COLOR_NAME_DISPLAY_DEFAULT_BG, - fg=self.COLOR_NAME_DISPLAY_DEFAULT_FG, - justify="center") - self.name_display.pack(fill=tk.X, ipady=15, padx=1, pady=1) - - flash_control_frame = ttk.Frame(master, style="TFrame") - flash_control_frame.pack(pady=15, fill=tk.X, padx=30) - - duration_frame = ttk.Frame(flash_control_frame) # Will inherit style from parent or "." - duration_frame.pack(pady=8, anchor='center') - ttk.Label(duration_frame, text="闪烁时间 (秒):").pack(side=tk.LEFT, padx=(0,10)) - self.duration_slider = ttk.Scale(duration_frame, from_=1, to=5, orient=tk.HORIZONTAL, - variable=self.flash_duration_var, length=250, - command=lambda v: self.flash_duration_var.set(int(float(v))), - style="Horizontal.TScale") - self.duration_slider.pack(side=tk.LEFT, padx=5) - self.duration_label = ttk.Label(duration_frame, textvariable=self.flash_duration_var, width=3, style="Value.TLabel") - self.duration_label.pack(side=tk.LEFT) - self.duration_slider.config(state=tk.DISABLED) - - interval_frame = ttk.Frame(flash_control_frame) - interval_frame.pack(pady=8, anchor='center') - ttk.Label(interval_frame, text="闪烁间隔 (毫秒):").pack(side=tk.LEFT, padx=(0,10)) - self.speed_interval_slider = ttk.Scale(interval_frame, from_=25, to=300, orient=tk.HORIZONTAL, - variable=self.flash_interval_var, length=250, - command=lambda v: self.flash_interval_var.set(int(float(v))), - style="Horizontal.TScale") - self.speed_interval_slider.pack(side=tk.LEFT, padx=5) - self.interval_label = ttk.Label(interval_frame, textvariable=self.flash_interval_var, width=4, style="Value.TLabel") - self.interval_label.pack(side=tk.LEFT) - self.speed_interval_slider.config(state=tk.DISABLED) - - # --- File Selection Area --- - file_frame = ttk.Frame(master) - file_frame.pack(pady=20, padx=30) - ttk.Label(file_frame, text="学生名单 (学号,姓名.txt):").pack(side=tk.LEFT, padx=(0,10)) - self.file_entry = ttk.Entry(file_frame, textvariable=self.file_path, width=25, state='readonly') - self.file_entry.pack(side=tk.LEFT, padx=5, ipady=2) # Small ipady for entry height consistency - self.load_button = ttk.Button(file_frame, text="选择文件", command=self.load_file) - self.load_button.pack(side=tk.LEFT, padx=(5,0)) - - # --- Control Button Area --- - button_frame = ttk.Frame(master) - button_frame.pack(pady=25) - # Grid padx/pady for spacing between buttons - self.start_button = ttk.Button(button_frame, text="开始点名", command=self.start_roll_call, state=tk.DISABLED) - self.start_button.grid(row=0, column=0, padx=10, pady=8) - self.mark_absent_button = ttk.Button(button_frame, text="标记未到场", command=self.mark_absent, state=tk.DISABLED) - self.mark_absent_button.grid(row=0, column=1, padx=10, pady=8) - self.export_button = ttk.Button(button_frame, text="导出未到场(CSV)", command=self.export_absent_list, state=tk.DISABLED) - self.export_button.grid(row=1, column=0, padx=10, pady=8) - self.clear_button = ttk.Button(button_frame, text="清空考勤数据", command=self.clear_data, state=tk.DISABLED) - self.clear_button.grid(row=1, column=1, padx=10, pady=8) - - # --- 底部版权信息 --- - self.footer_label = ttk.Label( - master, - text="Ver. 2.2 @杨昱幸. All Rights Reserved.", - style="Footer.TLabel", - anchor='center' - ) - self.footer_label.pack(side=tk.BOTTOM, fill=tk.X, pady=(15,10)) - - self.update_counter() - - - # ... (load_file, _reset_data_core, etc. methods remain unchanged) ... - def load_file(self): - filepath = filedialog.askopenfilename( - title="请选择学生名单文件 (学号,姓名)", - filetypes=[("Text files", "*.txt")] - ) - if not filepath: return - - try: - self._reset_data_full() - self.file_path.set(os.path.basename(filepath)) - loaded_students = [] - invalid_lines = [] - line_num = 0 - with open(filepath, 'r', encoding='utf-8') as f: - for line in f: - line_num += 1 - original_line = line - line = line.strip() - if not line: continue - parts = line.split(',', 1) - if len(parts) == 2: - student_id_str = parts[0].strip() - name = parts[1].strip() - if student_id_str and name: - pinyin_list = pinyin(name, style=Style.NORMAL) - pinyin_str = "".join([item[0] for item in pinyin_list]) - student_info = (student_id_str, name, pinyin_str) - loaded_students.append(student_info) - else: invalid_lines.append((line_num, "学号或姓名为空", original_line)) - else: invalid_lines.append((line_num, "格式错误", original_line)) - - if not loaded_students: - messagebox.showwarning("警告", "文件未包含有效格式的学生信息 (学号,姓名)。") - self._reset_ui_after_load_fail() - return - - self.all_students = loaded_students - self.remaining_students = self.all_students[:] - random.shuffle(self.remaining_students) - - self.name_display.config(text="名单已加载,准备点名", fg=self.COLOR_NAME_DISPLAY_DEFAULT_FG, bg=self.COLOR_NAME_DISPLAY_DEFAULT_BG) - self.master.update_idletasks() - - self.update_counter() - self.start_button.config(state=tk.NORMAL) - self.clear_button.config(state=tk.NORMAL) - self.export_button.config(state=tk.DISABLED) - self.mark_absent_button.config(state=tk.DISABLED) - self.duration_slider.config(state=tk.NORMAL) - self.speed_interval_slider.config(state=tk.NORMAL) - - info_msg = f"成功加载 {len(self.all_students)} 位学生!" - if invalid_lines: - info_msg += f"\n\n但有 {len(invalid_lines)} 行格式无效已被跳过:" - for i, (ln, reason, orig) in enumerate(invalid_lines[:5]): - info_msg += f"\n第{ln}行: {reason} -> '{orig.strip()}'" - if len(invalid_lines) > 5: info_msg += "\n..." - messagebox.showwarning("加载部分成功", info_msg) - else: - messagebox.showinfo("成功", info_msg) - - except FileNotFoundError: - messagebox.showerror("错误", f"文件未找到: {filepath}") - self._reset_ui_after_load_fail() - except Exception as e: - messagebox.showerror("错误", f"加载文件时出错: {e}") - self._reset_ui_after_load_fail() - - def _reset_data_core(self): - self.remaining_students = [] - self.called_students = [] - self.absent_students = [] - self.current_student_info = None - if self.flash_timer: self.master.after_cancel(self.flash_timer); self.flash_timer = None - if self.stop_timer: self.master.after_cancel(self.stop_timer); self.stop_timer = None - self.is_flashing = False - - def _reset_data_full(self): - self._reset_data_core() - self.all_students = [] - self.file_path.set("尚未选择文件") - - def _reset_ui_after_load_fail(self): - self.file_path.set("尚未选择文件") - self.name_display.config(text="请先加载名单", fg=self.COLOR_NAME_DISPLAY_DEFAULT_FG, bg=self.COLOR_NAME_DISPLAY_DEFAULT_BG) - self.start_button.config(state=tk.DISABLED) - self.mark_absent_button.config(state=tk.DISABLED) - self.export_button.config(state=tk.DISABLED) - self.clear_button.config(state=tk.DISABLED) - self.duration_slider.config(state=tk.DISABLED) - self.speed_interval_slider.config(state=tk.DISABLED) - self.update_counter() - - def update_counter(self): - total = len(self.all_students) - called_count = len(self.called_students) + len(self.absent_students) - self.counter_label.config(text=f"已点名:{called_count}/{total}人 (总人数:{total})") - - def start_roll_call(self): - if self.is_flashing: return - if not self.remaining_students: - self.name_display.config(text="所有学生已点完!", fg=self.COLOR_NAME_DISPLAY_SELECTED_FG, bg=self.COLOR_NAME_DISPLAY_SELECTED_BG) - messagebox.showinfo("提示", "所有学生均已点名。") - self.start_button.config(state=tk.DISABLED); self.mark_absent_button.config(state=tk.DISABLED) - return - - self.start_button.config(state=tk.DISABLED) - self.load_button.config(state=tk.DISABLED) - self.mark_absent_button.config(state=tk.DISABLED) - self.clear_button.config(state=tk.DISABLED) - self.export_button.config(state=tk.DISABLED) - self.duration_slider.config(state=tk.DISABLED) - self.speed_interval_slider.config(state=tk.DISABLED) - - self.is_flashing = True - self.name_display.config(bg=self.COLOR_NAME_DISPLAY_FLASH_BG, fg=self.COLOR_NAME_DISPLAY_FLASH_FG) - self._flash_name() - - duration_ms = self.flash_duration_var.get() * 1000 - self.stop_timer = self.master.after(duration_ms, self.stop_flashing) - - def _flash_name(self): - if not self.is_flashing or not self.remaining_students: - if self.is_flashing: - self.stop_flashing() # Cleanly stop if conditions change - return - - display_student = random.choice(self.remaining_students) - display_text = f"{display_student[0]},{display_student[1]},{display_student[2]}" - # Text color for flashing already set in start_roll_call - self.name_display.config(text=display_text) - - flash_interval_ms = self.flash_interval_var.get() - self.flash_timer = self.master.after(flash_interval_ms, self._flash_name) - - def stop_flashing(self): - if not self.is_flashing: return # Already stopped or never started - - if self.flash_timer: - self.master.after_cancel(self.flash_timer) - self.flash_timer = None - # self.stop_timer is the timer for the duration of flashing. - # If this method is called by that timer, self.stop_timer will be None automatically. - # If called externally (e.g. by _flash_name), cancel it. - if self.stop_timer: - self.master.after_cancel(self.stop_timer) - self.stop_timer = None - - self.is_flashing = False - self._select_final_student() - - def _select_final_student(self): - if self.is_flashing: # Should not happen if stop_flashing was called correctly - self.is_flashing = False # Force stop - if self.flash_timer: self.master.after_cancel(self.flash_timer); self.flash_timer = None - if self.stop_timer: self.master.after_cancel(self.stop_timer); self.stop_timer = None - - # Restore button states - self.load_button.config(state=tk.NORMAL) - self.clear_button.config(state=tk.NORMAL) - can_enable_sliders = bool(self.all_students) - self.duration_slider.config(state=tk.NORMAL if can_enable_sliders else tk.DISABLED) - self.speed_interval_slider.config(state=tk.NORMAL if can_enable_sliders else tk.DISABLED) - - if not self.remaining_students: - self.name_display.config(text="所有学生已点完!", fg=self.COLOR_NAME_DISPLAY_SELECTED_FG, bg=self.COLOR_NAME_DISPLAY_SELECTED_BG) - self.start_button.config(state=tk.DISABLED) - self.mark_absent_button.config(state=tk.DISABLED) - self.export_button.config(state=tk.NORMAL if self.absent_students else tk.DISABLED) - return - - self.current_student_info = self.remaining_students.pop(0) - self.called_students.append(self.current_student_info) - - display_text = f"{self.current_student_info[0]},{self.current_student_info[1]},{self.current_student_info[2]}" - self.name_display.config(text=display_text, fg=self.COLOR_NAME_DISPLAY_SELECTED_FG, bg=self.COLOR_NAME_DISPLAY_SELECTED_BG) - self.update_counter() - - if self.remaining_students: - self.start_button.config(state=tk.NORMAL) - else: # No more students left to call - self.start_button.config(state=tk.DISABLED) - self.name_display.config(text=display_text + " (最后一位)", fg=self.COLOR_NAME_DISPLAY_SELECTED_FG, bg=self.COLOR_NAME_DISPLAY_SELECTED_BG) - - self.mark_absent_button.config(state=tk.NORMAL) - self.export_button.config(state=tk.NORMAL if self.absent_students else tk.DISABLED) - - def mark_absent(self): - if not self.current_student_info or self.is_flashing: return - - if self.current_student_info in self.called_students: # Check if they were marked present first - self.called_students.remove(self.current_student_info) - if self.current_student_info not in self.absent_students: # Avoid duplicates - self.absent_students.append(self.current_student_info) - - self.update_counter() # Counter includes both called and absent - display_text = f"{self.current_student_info[0]},{self.current_student_info[1]},{self.current_student_info[2]} [未到]" - self.name_display.config(text=display_text, fg=self.COLOR_NAME_DISPLAY_ABSENT_FG, bg=self.COLOR_NAME_DISPLAY_ABSENT_BG) - self.mark_absent_button.config(state=tk.DISABLED) # Prevent re-marking for current student - self.export_button.config(state=tk.NORMAL) # Enable export since there's an absent student - # else: Student might already be in absent_students or not selected, do nothing. - - def export_absent_list(self): - if not self.absent_students: - messagebox.showinfo("提示", "当前没有未到场的学生记录。") - return - - today_str = date.today().strftime('%Y-%m-%d') - default_filename = f"未到场名单_{today_str}.csv" - - filepath = filedialog.asksaveasfilename( - title="保存未到场名单 (CSV)", - defaultextension=".csv", - filetypes=[("CSV files", "*.csv"), ("All files", "*.*")], - initialfile=default_filename - ) - if not filepath: return - - try: - sorted_absent = sorted(self.absent_students, key=lambda x: x[0]) # Sort by student ID - with open(filepath, 'w', encoding='utf-8-sig', newline='') as f: - writer = csv.writer(f) - writer.writerow(["学号", "姓名"]) # Header row - for student in sorted_absent: - writer.writerow([student[0], student[1]]) # Student ID and Name - messagebox.showinfo("导出成功", f"未到场名单已成功导出为 CSV 文件:\n{filepath}") - except Exception as e: - messagebox.showerror("导出失败", f"导出文件时出错: {e}") - - def clear_data(self): - if not self.all_students: # No student list loaded - messagebox.showinfo("提示", "尚未加载学生名单,无数据可清空。") - return - - if messagebox.askyesno("确认", "确定要清空当前考勤数据吗?\n这将重置点名状态,但学生名单会保留。"): - self._reset_data_core() # Clears called, absent, current_student, timers - - # Re-initialize remaining_students from all_students - self.remaining_students = self.all_students[:] - random.shuffle(self.remaining_students) - - self.name_display.config(text="考勤数据已清空,可重新点名", fg=self.COLOR_NAME_DISPLAY_DEFAULT_FG, bg=self.COLOR_NAME_DISPLAY_DEFAULT_BG) - - can_enable_controls = bool(self.all_students) - self.start_button.config(state=tk.NORMAL if can_enable_controls else tk.DISABLED) - self.mark_absent_button.config(state=tk.DISABLED) # No student selected yet - self.export_button.config(state=tk.DISABLED) # Absent list is now empty - self.clear_button.config(state=tk.NORMAL if can_enable_controls else tk.DISABLED) - - self.duration_slider.config(state=tk.NORMAL if can_enable_controls else tk.DISABLED) - self.speed_interval_slider.config(state=tk.NORMAL if can_enable_controls else tk.DISABLED) - - self.update_counter() - self.load_button.config(state=tk.NORMAL) # Load button should generally be active - - -# --- Main program entry point --- -if __name__ == "__main__": - missing_libs = [] - try: - import pypinyin - except ImportError: - missing_libs.append("pypinyin (`pip install pypinyin`)") - try: - import csv - except ImportError: # csv is standard, but good practice if it were external - missing_libs.append("csv (standard library module)") - - if missing_libs: - temp_root = tk.Tk() - temp_root.withdraw() - messagebox.showerror("依赖缺失", f"请先安装或确保以下库可用:\n" + "\n".join(missing_libs)) - temp_root.destroy() - exit() - - root = tk.Tk() - app = RollCallApp(root) - root.mainloop() \ No newline at end of file diff --git a/课堂点名小程序/课堂随机点名_旧.html b/课堂点名小程序/课堂随机点名_旧.html deleted file mode 100644 index b62fcd6..0000000 --- a/课堂点名小程序/课堂随机点名_旧.html +++ /dev/null @@ -1,215 +0,0 @@ - - - - 课堂随机点名系统 - - - -
-

杨老师的点名利器

-
点击开始
-
- - -
-
- - - - \ No newline at end of file