admin 培训-考试管理

main
panliang 2024-04-09 17:40:56 +08:00
parent 53684c9141
commit 081ce71626
13 changed files with 334 additions and 16 deletions

View File

@ -0,0 +1,98 @@
<?php
namespace App\Admin\Controllers\Train;
use App\Admin\Controllers\AdminController;
use App\Admin\Services\Train\ExaminationService;
use Slowlyo\OwlAdmin\Admin;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\Page;
use App\Enums\{ExamStatus, QuestionCate};
use App\Models\Train\Question;
/**
* 考试管理
*/
class ExaminationController extends AdminController
{
protected string $serviceName = ExaminationService::class;
public function list(): Page
{
$crud = $this->baseCRUD()
->tableLayout('fixed')
->headerToolbar([
$this->createButton()->visible(Admin::user()->can('admin.train.examinations.create')),
...$this->baseHeaderToolBar(),
])
->bulkActions([])
->filter($this->baseFilter()->body([
amis()->GroupControl()->mode('horizontal')->body([
amisMake()->TextControl()->name('search')->label(__('train_examination.name'))->columnRatio(3)->clearable(),
amisMake()->SelectControl()->options(ExamStatus::options())->name('exam_status')->label(__('train_examination.exam_status'))->columnRatio(3)->clearable(),
]),
]))
->columns([
amisMake()->TableColumn()->name('id')->label(__('train_examination.id')),
amisMake()->TableColumn()->name('name')->label(__('train_examination.name')),
amisMake()->TableColumn()->name('exam_status')->label(__('train_examination.exam_status'))->set('type', 'mapping')->set('map', ExamStatus::options()),
amisMake()->TableColumn()->name('published_at')->label(__('train_examination.published_at')),
amisMake()->TableColumn()->name('total_questions')->label(__('train_examination.total_questions')),
amisMake()->TableColumn()->name('total_score')->label(__('train_examination.total_score')),
$this->rowActions([
$this->rowShowButton()->visible(Admin::user()->can('admin.train.examinations.view')),
$this->rowEditButton()->visible(Admin::user()->can('admin.train.examinations.update')),
$this->rowDeleteButton()->visible(Admin::user()->can('admin.train.examinations.delete')),
]),
]);
return $this->baseList($crud);
}
public function form($edit): Form
{
return $this->baseForm()->title('')->body([
amisMake()->TextControl()->name('name')->label(__('train_examination.name'))->required(),
amisMake()->TableControl()
->addable()
->editable()
->removable()
->needConfirm(false)
->columns([
amisMake()->SelectControl()
->options(Question::get())
->labelField('title')
->valueField('id')
->searchable()
->name('question_id')
->label(__('train_examination.question_id'))
->required(),
amisMake()->NumberControl()->min(0)->step(1)->precision(0)->name('score')->label(__('train_examination.score'))->required(),
])
->name('questions')
->label(__('train_examination.questions')),
]);
}
public function detail(): Form
{
$detail = amisMake()->Property()->items([
['label' => __('train_examination.name'), 'content' => '${name}'],
['label' => __('train_examination.exam_status'), 'content' => amisMake()->Mapping()->map(ExamStatus::options())->name('exam_status')],
['label' => __('train_examination.published_at'), 'content' => '${published_at}'],
['label' => __('train_examination.total_questions'), 'content' => '${total_questions}'],
['label' => __('train_examination.total_score'), 'content' => '${total_score}'],
['label' => __('train_examination.created_at'), 'content' => '${created_at}'],
]);
$question = amisMake()->Table()->source('${questions}')->columns([
amisMake()->TableColumn()->name('title')->label(__('train_question.title')),
amisMake()->TableColumn()->name('cate')->label(__('train_question.cate'))->set('type', 'mapping')->map(QuestionCate::options()),
amisMake()->TableColumn()->name('options')->label(__('train_question.options'))->set('type', 'list')->source('${options}')->listItem([
'titleClassName' => 'text-${IF(is_true, "success", "danger")}',
'title' => '${text}',
]),
amisMake()->TableColumn()->name('score')->label(__('train_examination.score')),
]);
return $this->baseDetail()->title('')->body([$detail, amisMake()->Divider(), $question]);
}
}

View File

@ -21,7 +21,7 @@ class QuestionController extends AdminController
$crud = $this->baseCRUD()
->tableLayout('fixed')
->headerToolbar([
$this->createTypeButton('drawer', 'xl')->visible(Admin::user()->can('admin.train.questions.create')),
$this->createButton()->visible(Admin::user()->can('admin.train.questions.create')),
...$this->baseHeaderToolBar(),
])
->bulkActions([])
@ -35,10 +35,14 @@ class QuestionController extends AdminController
amisMake()->TableColumn()->name('id')->label(__('train_question.id')),
amisMake()->TableColumn()->name('title')->label(__('train_question.title')),
amisMake()->TableColumn()->name('cate')->label(__('train_question.cate'))->set('type', 'mapping')->set('map', QuestionCate::options()),
amisMake()->TableColumn()->name('options')->label(__('train_question.options'))->set('type', 'list')->source('${options}')->listItem([
'titleClassName' => 'text-${IF(is_true, "success", "danger")}',
'title' => '${text}',
]),
amisMake()->TableColumn()->name('created_at')->label(__('train_book.created_at')),
$this->rowActions([
$this->rowShowTypeButton('drawer', 'xl')->visible(Admin::user()->can('admin.train.questions.view')),
$this->rowEditTypeButton('drawer', 'xl')->visible(Admin::user()->can('admin.train.questions.update')),
$this->rowShowButton()->visible(Admin::user()->can('admin.train.questions.view')),
$this->rowEditButton()->visible(Admin::user()->can('admin.train.questions.update')),
$this->rowDeleteButton()->visible(Admin::user()->can('admin.train.questions.delete')),
]),
]);
@ -69,11 +73,11 @@ class QuestionController extends AdminController
{
return $this->baseDetail()->title('')->body(amisMake()->Property()->items([
['label' => __('train_question.title'), 'content' => '${title}'],
['label' => __('train_question.cate'), 'content' => '${cate}'],
['label' => __('train_question.cate'), 'content' => amisMake()->Mapping()->map(QuestionCate::options())->name('cate')],
['label' => __('train_question.created_at'), 'content' => '${created_at}'],
['label' => __('train_question.options'), 'content' => amisMake()->Table()->source('${options}')->columns([
['name' => 'text', 'label' => __('train_question.text')],
['name' => 'is_true', 'label' => __('train_question.is_true'), 'type' => 'status'],
['label' => __('train_question.options'), 'content' => amisMake()->ListRenderer()->source('${options}')->listItem([
'titleClassName' => 'text-${IF(is_true, "success", "danger")}',
'title' => '${text}',
]), 'span' => 3],
]));
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Admin\Filters;
use Carbon\Carbon;
use EloquentFilter\ModelFilter;
class TrainExaminationFilter extends ModelFilter
{
public function search($key)
{
$condition = '%'.$key.'%';
return $this->where('name', 'like', $condition);
}
public function examStatus($key)
{
return $this->where('exam_status', $key);
}
public function dateRange($dates)
{
$dates = explode(',', $dates);
$start = Carbon::createFromTimestamp(data_get($dates, 0, time()))->startOfDay();
$end = Carbon::createFromTimestamp(data_get($dates, 1, time()))->endOfDay();
$this->whereBetween('created_at', [$start, $end]);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Admin\Services\Train;
use App\Admin\Filters\TrainExaminationFilter;
use App\Models\Train\{Examination, Question};
use App\Admin\Services\BaseService;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\{Validator, Storage};
use App\Enums\ExamStatus;
class ExaminationService extends BaseService
{
protected array $withRelationships = [];
protected string $modelName = Examination::class;
protected string $modelFilterName = TrainExaminationFilter::class;
public function resloveData($data, $model = null)
{
if (isset($data['questions']) && $data['questions']) {
$totalQuestions = 0;
$totalScore = 0;
$questions = [];
$ids = array_column($data['questions'], 'question_id');
$questionList = Question::whereIn('id', $ids)->get();
foreach ($data['questions'] as $question) {
$model = $questionList->firstWhere('id', $question['question_id']);
if ($model) {
// title: 题目, cate: 类型, options: 选项, score: 分值
$question['title'] = $model->title;
$question['cate'] = $model->cate;
$question['options'] = $model->options;
array_push($questions, $question);
$totalQuestions++;
$totalScore+=$question['score'];
}
}
$data['questions'] = $questions;
$data['total_questions'] = $totalQuestions;
$data['total_score'] = $totalScore;
}
return $data;
}
public function publish(Examination $examination)
{
if ($examination->exam_status == ExamStatus::Published) {
return $this->setError('已经发布了');
}
}
}

View File

@ -191,6 +191,8 @@ Route::group([
$router->resource('books', \App\Admin\Controllers\Train\BookController::class);
// 题库管理
$router->resource('questions', \App\Admin\Controllers\Train\QuestionController::class);
// 考试管理
$router->resource('examinations', \App\Admin\Controllers\Train\ExaminationController::class);
});
$router->post('agreement/download', [AgreementController::class, 'download'])->name('agreement.download');

View File

@ -0,0 +1,32 @@
<?php
namespace App\Enums;
/**
* 考试状态
*/
enum ExamStatus: int
{
/**
* 未发布
*/
case None = 1;
/**
* 已发布
*/
case Published = 2;
public static function options()
{
return [
self::None->value => '未发布',
self::Published->value => '已发布',
];
}
public function text()
{
return data_get(self::options(), $this->value);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Models\Train;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasDateTimeFormatter;
use EloquentFilter\Filterable;
/**
* 培训管理-考试
*/
class Examination extends Model
{
use HasDateTimeFormatter, Filterable;
protected $table = 'train_examinations';
protected $guarded = [];
protected $casts = [
// [{title: 题目, cate: 类型, options: 选项, score: 分值}]
'questions' => 'json',
'published_at' => 'datetime',
'exam_status' => \App\Enums\ExamStatus::class,
];
public function modelFilter()
{
return \App\Admin\Filters\TrainExaminationFilter::class;
}
}

View File

@ -2,7 +2,6 @@
namespace App\Models\Train;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasDateTimeFormatter;
use EloquentFilter\Filterable;
@ -12,7 +11,7 @@ use EloquentFilter\Filterable;
*/
class Question extends Model
{
use HasFactory, HasDateTimeFormatter, Filterable;
use HasDateTimeFormatter, Filterable;
protected $table = 'train_questions';
@ -20,6 +19,7 @@ class Question extends Model
protected $casts = [
'cate' => \App\Enums\QuestionCate::class,
// [{text: 选项1, is_true: true}]
'options' => 'json',
];
}

View File

@ -0,0 +1,40 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\Train\Question;
use App\Enums\QuestionCate;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Model>
*/
class QuestionFactory extends Factory
{
protected $model = Question::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$cate = $this->faker->randomElement(QuestionCate::class);
$options = [];
$max = 4;
$index = [];
if ($cate == QuestionCate::Radio) {
$index = [$this->faker->randomElement(range(0, $max- 1))];
} else if ($cate == QuestionCate::Checkbox) {
$index = $this->faker->randomElements(range(0, $max- 1), $this->faker->numberBetween(2, 4));
}
for($i = 0; $i < $max; $i++) {
array_push($options, ['text' => $this->faker->word, 'is_true' => in_array($i, $index)]);
}
return [
'title' => $this->faker->sentence,
'cate' => $cate,
'options' => $options,
];
}
}

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Enums\{BookType, QuestionCate};
use App\Enums\{BookType, QuestionCate, ExamStatus};
return new class extends Migration
{
@ -40,10 +40,12 @@ return new class extends Migration
Schema::create('train_examinations', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('考试名称');
$table->json('questions')->comment('考题内容[{title: 题目, cate: 类型, options: [选项1, 选项2], score: 分值, answer: 正确选项}]');
$table->json('questions')->comment('考题内容[{title: 题目, cate: 类型, options: 选项, score: 分值}]');
$table->unsignedInteger('total_questions')->default(0)->comment('总题数');
$table->unsignedInteger('total_score')->default(0)->comment('总分数');
$table->string('remarks')->nullable()->comment('备注');
$table->timestamp('published_at')->nullable()->comment('发布时间');
$table->unsignedInteger('exam_status')->default(ExamStatus::None->value)->comment('状态(1: 未发布, 2: 已发布)');
$table->timestamps();
$table->comment('培训-考试记录');
@ -53,7 +55,7 @@ return new class extends Migration
$table->id();
$table->foreignId('examination_id')->comment('考试, train_examinations.id');
$table->foreignId('employee_id')->comment('考生');
$table->json('content')->comment('考卷内容[{title: 题目, cate: 类型, options: [选项1, 选项2], score: 分值, answer: 正确选项, user_score: 得分, user_answer: 回答}]');
$table->json('content')->comment('考卷内容[{title: 题目, cate: 类型, options: 选项, score: 分值 , user_score: 得分, user_answer: 回答}]');
$table->unsignedInteger('mark')->nullable()->comment('分数');
$table->timestamp('start_at')->nullable()->comment('答题开始时间');
$table->timestamp('end_at')->nullable()->comment('答题结束时间');

View File

@ -267,6 +267,12 @@ class AdminPermissionSeeder extends Seeder
'uri' => '/train/questions',
'resource' => true,
],
'examinations' => [
'name' => '考试管理',
'icon' => '',
'uri' => '/train/examinations',
'resource' => true,
],
]
],
'agreement' => [

View File

@ -6,7 +6,8 @@ use App\Models\Employee;
use App\Models\EmployeeSign;
use App\Models\EmployeeSignLog;
use App\Models\Store;
use Database\Factories\EmployeeFactory;
use App\Models\Train\{Question};
use Database\Factories\{EmployeeFactory, QuestionFactory};
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
@ -24,8 +25,11 @@ class EmployeeSeeder extends Seeder
// Store::truncate();
// Store::factory()->count(10)->create();
EmployeeSign::truncate();
EmployeeSignLog::truncate();
EmployeeSignLog::factory()->count(100)->create();
// EmployeeSign::truncate();
// EmployeeSignLog::truncate();
// EmployeeSignLog::factory()->count(100)->create();
Question::truncate();
(new QuestionFactory)->count(20)->create();
}
}

View File

@ -0,0 +1,17 @@
<?php
return [
'id' => 'ID',
'created_at' => '创建时间',
'updated_at' => '更新时间',
'name' => '名称',
'questions' => '考题内容',
'total_questions' => '总题数',
'total_score' => '总分数',
'remarks' => '备注',
'published_at' => '发布时间',
'exam_status' => '状态',
'question_id' => '考题',
'score' => '分值',
];