昨天在討論匯入功能時有提到前面都是假設題目都是單選的前提來製作的,但是我們在匯入時有複選題的存在,這會讓我們前台的測試功能出現狀況,這問題的解決需要一點工夫,我們今天想先試試看 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>
因為我們要匯出的資料已經在測驗結果頁了,原本我想利用 Inertia
的 Link
組件,直接把資料傳回後台去,然後把檔案傳回前端下載就好,但後來查了一下原作者的回應,大意是說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 的官網找找。