TextbookExcelImportPage.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <?php
  2. namespace App\Filament\Pages\TextbookImport;
  3. use App\Services\Import\TextbookExcelImporter;
  4. use App\Services\TextbookApiService;
  5. use PhpOffice\PhpSpreadsheet\IOFactory;
  6. use Filament\Pages\Page;
  7. use UnitEnum;
  8. use BackedEnum;
  9. use Filament\Actions\Action;
  10. use Filament\Forms;
  11. use Filament\Forms\Components\FileUpload;
  12. use Filament\Notifications\Notification;
  13. use Illuminate\Support\Facades\Storage;
  14. use Illuminate\Support\Facades\Log;
  15. use Livewire\WithFileUploads;
  16. use Livewire\Component;
  17. use Livewire\Attributes\Validate;
  18. class TextbookExcelImportPage extends Page
  19. {
  20. use WithFileUploads;
  21. protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-arrow-up';
  22. protected static ?string $navigationLabel = 'Excel导入';
  23. protected static UnitEnum|string|null $navigationGroup = '教材管理';
  24. protected static ?int $navigationSort = 4;
  25. public string $view = 'filament.pages.textbook-import';
  26. #[Validate('required|string')]
  27. public $selectedType = 'textbook_series';
  28. #[Validate('required|file|mimes:xlsx,xls|max:10240')]
  29. public $file;
  30. public $importResult = null;
  31. protected $importer;
  32. protected $apiService;
  33. public function boot()
  34. {
  35. $this->importer = app(TextbookExcelImporter::class);
  36. $this->apiService = app(TextbookApiService::class);
  37. }
  38. public function mount()
  39. {
  40. $this->importer = app(TextbookExcelImporter::class);
  41. $this->apiService = app(TextbookApiService::class);
  42. // 检查URL参数,设置默认导入类型
  43. $type = request()->get('type');
  44. if (in_array($type, ['textbook_series', 'textbook', 'textbook_catalog'])) {
  45. $this->selectedType = $type;
  46. }
  47. }
  48. public function getHeaderActions(): array
  49. {
  50. return [
  51. Action::make('downloadTemplate')
  52. ->label('下载模板')
  53. ->icon('heroicon-o-arrow-down-tray')
  54. ->color('primary')
  55. ->action('downloadTemplate'),
  56. Action::make('import')
  57. ->label('导入数据')
  58. ->icon('heroicon-o-cloud-arrow-up')
  59. ->color('success')
  60. ->action('importData')
  61. ->requiresConfirmation()
  62. ->modalHeading('确认导入')
  63. ->modalDescription('确定要导入Excel文件中的数据吗?此操作将同步到题库服务。')
  64. ->modalSubmitActionLabel('确认导入'),
  65. ];
  66. }
  67. public function downloadTemplate()
  68. {
  69. try {
  70. $importer = app(TextbookExcelImporter::class);
  71. if ($this->selectedType === 'textbook_series') {
  72. $filePath = $importer->generateTextbookSeriesTemplate();
  73. $fileName = '教材系列导入模板.xlsx';
  74. } elseif ($this->selectedType === 'textbook') {
  75. $filePath = $importer->generateTextbookTemplate();
  76. $fileName = '教材导入模板.xlsx';
  77. } else {
  78. $filePath = $importer->generateTextbookCatalogTemplate();
  79. $fileName = '教材目录导入模板.xlsx';
  80. }
  81. return response()->download($filePath, $fileName)->deleteFileAfterSend();
  82. } catch (\Exception $e) {
  83. Notification::make()
  84. ->title('模板生成失败')
  85. ->body($e->getMessage())
  86. ->danger()
  87. ->send();
  88. }
  89. }
  90. public function importData()
  91. {
  92. $this->validate();
  93. $temporaryPath = null;
  94. $finalPath = null;
  95. try {
  96. $importer = app(TextbookExcelImporter::class);
  97. // 获取文件的临时路径(Livewire 上传的文件会有临时路径)
  98. $temporaryPath = $this->file->getRealPath();
  99. if (!$temporaryPath || !file_exists($temporaryPath)) {
  100. throw new \Exception('文件上传失败,请重新上传');
  101. }
  102. // 检查文件是否可读
  103. if (!is_readable($temporaryPath)) {
  104. throw new \Exception('文件不可读,请检查文件权限');
  105. }
  106. // 调试信息
  107. Log::info('文件上传信息', [
  108. 'original_name' => $this->file->getClientOriginalName(),
  109. 'temporary_path' => $temporaryPath,
  110. 'exists' => file_exists($temporaryPath),
  111. 'is_file' => is_file($temporaryPath),
  112. 'readable' => is_readable($temporaryPath),
  113. 'file_size' => filesize($temporaryPath),
  114. 'mime_type' => $this->file->getMimeType(),
  115. ]);
  116. // 执行导入 - 直接使用临时路径
  117. if ($this->selectedType === 'textbook_series') {
  118. $result = $importer->importTextbookSeries($temporaryPath);
  119. } elseif ($this->selectedType === 'textbook') {
  120. $result = $importer->importTextbook($temporaryPath);
  121. } else {
  122. // 教材目录导入 - 需要从Excel中获取textbook_id
  123. $spreadsheet = IOFactory::load($temporaryPath);
  124. $sheet = $spreadsheet->getActiveSheet();
  125. $data = $sheet->toArray();
  126. // 获取第一行数据中的教材ID
  127. $firstRow = $data[1] ?? [];
  128. $textbookId = (int)($firstRow[0] ?? 0);
  129. if ($textbookId <= 0) {
  130. throw new \Exception('Excel文件中未找到有效的教材ID,请确保第一列包含教材ID');
  131. }
  132. $result = $importer->importTextbookCatalog($temporaryPath, $textbookId);
  133. }
  134. $this->importResult = $result;
  135. if ($result['success']) {
  136. $message = sprintf(
  137. '导入完成!成功: %d 条,失败: %d 条',
  138. $result['success_count'],
  139. $result['error_count']
  140. );
  141. Notification::make()
  142. ->title('导入成功')
  143. ->body($message)
  144. ->success()
  145. ->send();
  146. } else {
  147. Notification::make()
  148. ->title('导入失败')
  149. ->body($result['message'])
  150. ->danger()
  151. ->send();
  152. }
  153. } catch (\Exception $e) {
  154. Notification::make()
  155. ->title('导入失败')
  156. ->body($e->getMessage())
  157. ->danger()
  158. ->send();
  159. Log::error('Excel导入失败', [
  160. 'error' => $e->getMessage(),
  161. 'temporary_path' => $temporaryPath,
  162. 'final_path' => $finalPath,
  163. 'trace' => $e->getTraceAsString(),
  164. ]);
  165. }
  166. // 不需要手动删除文件,临时文件会在请求结束后自动清理
  167. }
  168. }