iT邦幫忙

2022 iThome 鐵人賽

DAY 17
0
Modern Web

LV的全端開發體驗系列 第 17

Day17 使用Laravel Excel來匯出資料

  • 分享至 

  • xImage
  •  

昨天在討論匯入功能時有提到前面都是假設題目都是單選的前提來製作的,但是我們在匯入時有複選題的存在,這會讓我們前台的測試功能出現狀況,這問題的解決需要一點工夫,我們今天想先試試看 Laravel Excel 的匯出功能,所以我們調整一下測驗的選題功能,先暫時只有單選題會被選出來就好:

function subjects($bank_id,$number){
    return $this->find($bank_id)
                ->subjects               //取出題庫中的所有題目
                ->where('multiple',0)    //增加條件為單選題
                ->random(10)
                ->map(function($subject){
                     $subject->options;
                 return $subject;
                })->shuffle();
}

接著我們來製作匯出的功能,這個功能最初的用意是讓非會員在做完測試後,可以把題目匯出成為一個excel檔來留存,也許有些人會覺得用 .csv 也可以,是沒錯,不過 .csv 是純檔案的操作,我覺得沒有特別說明的必要,而且 Laravel Excel 也是有支援 .csv 的輸出的;

測驗結果希望能匯出留存

選擇 .xlsx 來做匯出是因為自己的工作有這個需要,同時也是因為 Laravel Excel 的匯出是可以對欄位的格式進行訂製的,而 .csv 這種格式只是純文字,無法對欄位進行格式化,比如寬高的設定。

一樣的,我們先來想一下要匯出什麼樣的格式讓使用者好閱讀?仿照匯入的格式,但因為是給使用者看的,所以會簡化一些內容:

題庫 網頁設計乙級 測驗時間 2022-09-29
題目數 10 正確:7 錯誤:3
題號 正確 選擇 題目 選項1 選項2 選項3 選項4
1 2 2 題目1 選項1 選項2 選項3 選項4
2 1 3 題目2 選項1 選項2 選項3 選項4
3 3 1 題目3 選項1 選項2 選項3 選項4

給使用者看的,我們會把共同的資訊放在上方呈現,
而每題的詳細內容則在下方陳列。

先在測驗結果頁增加一個匯出按鈕:
resources\js\Pages\TestResult.vue

    <button class="px-4 py-2 bg-blue-100 text-blue-900 border rounded-lg"
            @click="testExport">
      匯出.xlsx檔
    </button>

因為我們要匯出的資料已經在測驗結果頁了,原本我想利用 InertiaLink 組件,直接把資料傳回後台去,然後把檔案傳回前端下載就好,但後來查了一下原作者的回應,大意是說Inertia不是這樣用的,它只負責處理SPA頁面,其它的請用傳統方式處理;

後來google了一下找到 [利用axois來下載檔案] 這篇文章,截取部份程式碼來測試後證實可用,所以最後我使用 axois 的方式來傳資料及處理前端下載的功能。

先設定路由 routes\web.php

Route::post('/test/export',[TestController::class,'testExport'])
        ->name('test.export');

app\Http\Controllers\TestController.php建立方法

function testExport(Request $request){
    dd($request->input());
}

測試一下後端是否真的有收到測驗結果的資料,等一下拿來輸出用的。

建立 Export

php artisan make:export TestExport

app\Http\Controllers\TestController.php中處理一下資料,整理成輸出要用的格式:

function testExport(Request $request){
    $collections=$this->test->exportPackge($request->input());
    
    return Excel::download(new TestExport($collections),'test.xlsx');
}

app\Services\TestService.php中設定要匯出成excel檔時的對應欄位及格式:

function exportPackge($testResult)
{
    $subjects=$testResult['subjects'];
    $rows=[];        //存放要匯出的資料陣列
    $correct=[];     //存放每題是否正確的陣列
    $info=['total'=>count($subjects),     //統計測驗結果
           'correct'=>0,
           'wrong'=>0];

    //使用迴圈來整理要匯出的資料格式
    foreach($subjects as $idx => $subject){
        $rows[]=[
            '題號'=>$idx+1,
            '正確'=>(int)(collect($subject['options'])->where('ans',1)->keys()[0])+1,
            '選擇'=>$subject['selectSeq'],
            '題目'=>$subject['subject'],
            '選項1'=>$subject['options'][0]['option'],
            '選項2'=>$subject['options'][1]['option'],
            '選項3'=>$subject['options'][2]['option'],
            '選項4'=>$subject['options'][3]['option'],
        ];

        $correct[]=($subject['result']==1)?true:false;
        $info['correct']=($subject['result']==1)?$info['correct']+1:$info['correct']+0;
        $info['wrong']=($subject['result']==0)?$info['wrong']+1:$info['wrong']+0;
    }
    return collect(['subjects'=>$rows,
                    'bank'=>$testResult['bank']['name'] . $testResult['bank']['levelC'] . '級',
                    'type'=>$testResult['type'],
                    'info'=>$info,
                    'correct'=>$correct],
                    );
}

app\Exports\TestExport.php設定匯出的各項設定

namespace App\Exports;

use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithColumnWidths;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Maatwebsite\Excel\Concerns\WithHeadings;
use PhpOffice\PhpSpreadsheet\Style\Fill;

class TestExport implements FromCollection,WithColumnWidths, WithStyles,WithHeadings
{

    protected $collections;

    function __construct($collections)
    {
        $this->collections=$collections;
    }

    public function collection()
    {
        return collect($this->collections['subjects']);
    }

    /*設定每一行的寬度*/
    public function columnWidths(): array
    {
        return [
            'A' => 7,
            'B' => 7,
            'C' => 7,
            'D' => 120,
            'E' => 40,
            'F' => 40,
            'G' => 40,
            'H' => 40,
        ];
    }
    
    /*設定標題列*/
    public function headings(): array
    {
        //第一列為先放一個空白的資料,後面會取代掉
        return [
            ['row1'],
            ['題號','正確', '選擇', '題目', '選項1', '選項2', '選項3', '選項4']
        ];
    }

    /*資料表的各種樣式設定*/
    public function styles(Worksheet $sheet)
    {
        $rows = count($this->collections['subjects']) + 2;
        
        //設定所有列的共同高度
        $sheet->getDefaultRowDimension()->setRowHeight(36);

        //合併第一列
        $sheet->mergeCells("A1:H1");

        //在第一格中寫入測驗的相關資料
        $sheet->setCellValue("A1",
                             "題庫:".$this->collections['bank'].
                             " 測驗日期:".date("Y-m-d H:i:s").
                             "\n題數:".$this->collections['info']['total'].
                             " 正確:".$this->collections['info']['correct'].
                             " 錯誤:".$this->collections['info']['wrong']);

        //使用迴圈來判斷每一題的結果,並填入不同的底色
        foreach($this->collections['correct'] as $idx => $correct){
            if($correct==true){
                $sheet->getStyleByColumnAndRow(3,$idx+3)
                      ->getFill()
                      ->setFillType(Fill::FILL_SOLID)
                      ->getStartColor()
                      ->setARGB('AAFFAA');
            }else{
                $sheet->getStyleByColumnAndRow(3,$idx+3)
                      ->getFill()
                      ->setFillType(Fill::FILL_SOLID)
                      ->getStartColor()
                      ->setARGB('FF9999');
            }
        }

        //設定相關欄位的樣式
        return [
            "A1:H$rows" => [
                'font' => [
                    'size' => 12,
                    'name' => '標楷體',
                ],
                'alignment' => [
                    'vertical' => 'center',
                    'wrapText' => true
                ],
                'borders' => [
                    'allBorders' => [
                        'borderStyle' => 'thin',
                        'color' => ['argb' => '000000'],
                    ],
                ],
            ],
            "A3:C$rows"=>[
                'alignment' => [
                    'horizontal' => 'center',
                ],
            ],
            "A2:H2" => [
                'fill'=>[
                    'fillType'=>Fill::FILL_SOLID,
                    'startColor'=>['argb'=>'000000'],
                ],
                'font'=>[
                    'color'=>['argb'=>'ffffff']
                ],
                'alignment' => [
                    'horizontal' => 'center',
                ],
                'borders' => [
                    'allBorders' => [
                        'borderStyle' => 'thin',
                        'color' => ['argb' => '999999'],
                    ],
                ],                
            ],
            "A3:A$rows" => [
                'fill'=>[
                    'fillType'=>Fill::FILL_SOLID,
                    'startColor'=>['argb'=>'000000'],
                ],
                'font'=>[
                    'color'=>['argb'=>'ffffff']
                ],
                'borders' => [
                    'allBorders' => [
                        'borderStyle' => 'thin',
                        'color' => ['argb' => '999999'],
                    ],
                ],                
            ],
            "B3:B$rows" => [
                'fill'=>[
                    'fillType'=>Fill::FILL_SOLID,
                    'startColor'=>['argb'=>'00EE00'],
                ],
                'font'=>[
                    'color'=>['argb'=>'000000']
                ],
                'borders' => [
                    'allBorders' => [
                        'borderStyle' => 'thin',
                        'color' => ['argb' => '000000'],
                    ],
                ],                
            ],
        ];
    }    
}

最後修改一下前端的js,改用axios來傳資料並提供使用者下載:
resources\js\Pages\TestResult.vue

<script setup>
import FrontLayout from '@/Layouts/FrontLayout.vue';
import { Head, Link, useForm } from "@inertiajs/inertia-vue3";
import { ref } from 'vue';
import  axios  from 'axios';    //引入axios
const props = defineProps({
  bank: Object,
  subjects:Array,
  type: String,
});

const correctClass=ref('text-green-600 font-semibold bg-green-100');
const errorClass=ref('text-red-600 font-semibold bg-red-100')
const form=useForm(props.subjects)
const testExport=()=>{
    axios.post(route('test.export'),
              {subjects:props.subjects,
               bank:props.bank,
               type:props.type},        
              {responseType:'blob'})    //回傳的資料格式改為blob
    .then(function(res){
        //利用回傳的資料產生一個暫時的連結
      const url=window.URL.createObjectURL(new Blob([res.data]));
        
        //動態建立一個<a>標籤
      const link=document.createElement('a');
        
        //標籤中設定屬性link為剛產生的連結
      link.href=url;
        
        //從回傳資料的header中取得檔案名稱字串
      const fileName=decodeURI(res.headers["content-disposition"]
                        .split(" ")[1]
                        .replace("filename=",""));
        
        //設定連結的download屬性並指定檔名
      link.setAttribute('download',fileName);
        
        //把產生的<a>標籤物件加入到頁面中
      document.body.appendChild(link);
        
        //觸發點擊事件讓下載開始
      link.click();

    })
}
</script>
<template>
<FrontLayout>
  .....略
      
      //按鈕上設定點擊事件去觸發傳送資料及下載的動作
    <button class="px-4 py-2 bg-blue-100 text-blue-900 border rounded-lg"
            @click="testExport">
      匯出.xlsx檔
    </button>
  </div>
</FrontLayout>
</template>

這樣就可以匯出一個漂漂亮亮的EXCEL檔了,格式化的部份 Laravel EXCEL 官網說的不多,客製化的設定細節請各位直接去PhpSpreadsheet 的官網找找。


上一篇
Day16 使用Laravel Excel來匯入資料
下一篇
Day18 提升一下操作體驗(一)-vue-final-modal
系列文
LV的全端開發體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言