MedicalCalculatorController.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. <?php
  2. namespace App\Admin\Controllers;
  3. use App\Http\Controllers\Controller;
  4. use App\Model\MedicalCalculator;
  5. use App\Model\MedicalQuestion;
  6. use App\Model\MedicalOption;
  7. use Encore\Admin\Form;
  8. use Encore\Admin\Grid;
  9. use Encore\Admin\Facades\Admin;
  10. use Encore\Admin\Layout\Content;
  11. use Encore\Admin\Controllers\ModelForm;
  12. use Illuminate\Http\Request;
  13. use App\Admin\Actions\BatchDeleteAction;
  14. class MedicalCalculatorController extends Controller
  15. {
  16. use ModelForm;
  17. /**
  18. * 显示医学计算器列表
  19. *
  20. * @OA\Get(
  21. * path="/api/medical-calculators",
  22. * summary="获取医学计算器列表",
  23. * tags={"医学计算器"},
  24. * @OA\Response(
  25. * response=200,
  26. * description="成功",
  27. * @OA\JsonContent(type="array", @OA\Items(ref="#/components/schemas/MedicalCalculator"))
  28. * )
  29. * )
  30. */
  31. public function index()
  32. {
  33. return Admin::content(function (Content $content) {
  34. $content->header('医学计算器');
  35. $content->description('列表');
  36. $content->body($this->grid());
  37. });
  38. }
  39. /**
  40. * 创建新的医学计算器
  41. *
  42. * @OA\Post(
  43. * path="/api/medical-calculators",
  44. * summary="创建新的医学计算器",
  45. * tags={"医学计算器"},
  46. * @OA\RequestBody(
  47. * required=true,
  48. * @OA\JsonContent(ref="#/components/schemas/MedicalCalculator")
  49. * ),
  50. * @OA\Response(
  51. * response=201,
  52. * description="创建成功",
  53. * @OA\JsonContent(ref="#/components/schemas/MedicalCalculator")
  54. * )
  55. * )
  56. */
  57. public function create()
  58. {
  59. return Admin::content(function (Content $content) {
  60. $content->header('医学计算器');
  61. $content->description('新增');
  62. $content->body($this->form());
  63. });
  64. }
  65. /**
  66. * 编辑现有的医学计算器
  67. *
  68. * @OA\Get(
  69. * path="/api/medical-calculators/{id}",
  70. * summary="编辑现有的医学计算器",
  71. * tags={"医学计算器"},
  72. * @OA\Parameter(
  73. * name="id",
  74. * in="path",
  75. * required=true,
  76. * @OA\Schema(type="integer")
  77. * ),
  78. * @OA\Response(
  79. * response=200,
  80. * description="成功",
  81. * @OA\JsonContent(ref="#/components/schemas/MedicalCalculator")
  82. * )
  83. * )
  84. *
  85. * @param int $id 医学计算器ID
  86. */
  87. public function edit($id)
  88. {
  89. return Admin::content(function (Content $content) use ($id) {
  90. $content->header('医学计算器');
  91. $content->description('编辑');
  92. $content->body($this->form($id)->edit($id)); // 传递 $id 给 form 方法
  93. });
  94. }
  95. /**
  96. * 计算医学计算器的结果
  97. *
  98. * @OA\Post(
  99. * path="/api/medical-calculators/{id}/calculate",
  100. * summary="计算医学计算器的结果",
  101. * tags={"医学计算器"},
  102. * @OA\Parameter(
  103. * name="id",
  104. * in="path",
  105. * required=true,
  106. * @OA\Schema(type="integer")
  107. * ),
  108. * @OA\RequestBody(
  109. * required=true,
  110. * @OA\JsonContent(type="object")
  111. * ),
  112. * @OA\Response(
  113. * response=200,
  114. * description="计算结果",
  115. * @OA\JsonContent(type="object", @OA\Property(property="result", type="string"))
  116. * )
  117. * )
  118. *
  119. * @param Request $request 请求对象
  120. * @param int $id 医学计算器ID
  121. * @return \Illuminate\Http\JsonResponse 计算结果的JSON响应
  122. */
  123. public function calculate(Request $request, $id)
  124. {
  125. $calculator = MedicalCalculator::findOrFail($id);
  126. $inputs = $request->all();
  127. try {
  128. $result = $calculator->calculateResult($inputs);
  129. return response()->json(['result' => $result]);
  130. } catch (\Exception $e) {
  131. return response()->json(['error' => $e->getMessage()], 400);
  132. }
  133. }
  134. /**
  135. * 获取指定计算器的问题
  136. *
  137. * @OA\Get(
  138. * path="/api/medical-calculators/{id}/questions",
  139. * summary="获取指定计算器的问题",
  140. * tags={"医学计算器"},
  141. * @OA\Parameter(
  142. * name="id",
  143. * in="path",
  144. * required=true,
  145. * @OA\Schema(type="integer")
  146. * ),
  147. * @OA\Response(
  148. * response=200,
  149. * description="成功",
  150. * @OA\JsonContent(type="array", @OA\Items(ref="#/components/schemas/MedicalQuestion"))
  151. * )
  152. * )
  153. *
  154. * @param int $id 医学计算器ID
  155. * @return \Illuminate\Http\JsonResponse 问题列表的JSON响应
  156. */
  157. public function getQuestions($id)
  158. {
  159. $questions = MedicalQuestion::where('medical_calculator_id', $id)->with('options')->get();
  160. return response()->json($questions);
  161. }
  162. /**
  163. * 定义医学计算器的表单
  164. *
  165. * @param int|null $id 医学计算器ID
  166. * @return \Encore\Admin\Form 表单对象
  167. */
  168. protected function form($id = null)
  169. {
  170. return Admin::form(MedicalCalculator::class, function (Form $form) use ($id) {
  171. $form->text('name', '计算器名称');
  172. $form->text('disease_name', '疾病名称')->rules('nullable');
  173. // 确保传递正确的 calculatorId
  174. $calculatorId = $id ?? $form->model()->id;
  175. $form->html($this->questionsForm($calculatorId));
  176. $form->textarea('formula', '计算公式')->help('直接使用问题的变量名称,例如: weight / (height * height)');
  177. $form->table('results', '结果对照表', function ($table) {
  178. $table->decimal('min_score', 8, 2, '最小分数')->rules('required|numeric');
  179. $table->decimal('max_score', 8, 2, '最大分数')->rules('required|numeric');
  180. $table->text('result', '结果描述');
  181. })->default([])->customFormat(function ($value) {
  182. if (empty($value)) {
  183. return [];
  184. }
  185. if (is_string($value)) {
  186. $decoded = json_decode($value, true);
  187. return is_array($decoded) ? array_values($decoded) : [];
  188. }
  189. return is_array($value) ? array_values($value) : [];
  190. });
  191. $form->textarea('instructions', '使用说明')->rules('nullable');
  192. $form->saving(function (Form $form) {
  193. if (empty($form->results)) {
  194. $form->results = [];
  195. } elseif (is_array($form->results)) {
  196. $form->results = array_values($form->results);
  197. }
  198. });
  199. $form->saved(function (Form $form) {
  200. $this->saveQuestions($form);
  201. admin_toastr('保存成功', 'success');
  202. return redirect()->route('admin.medical-calculators.index');
  203. });
  204. // 在表单显示之前处理 results 字段
  205. if ($form->isEditing()) {
  206. $form->editing(function (Form $form) {
  207. $results = $form->model()->results;
  208. if (is_string($results)) {
  209. $decoded = json_decode($results, true);
  210. $form->results = is_array($decoded) ? array_values($decoded) : [];
  211. } elseif (!is_array($results)) {
  212. $form->results = [];
  213. } else {
  214. $form->results = array_values($results);
  215. }
  216. });
  217. }
  218. Admin::script($this->questionsScript());
  219. });
  220. }
  221. /**
  222. * 生成问题表单的HTML
  223. *
  224. * @param int $calculatorId 医学计算器ID
  225. * @return string 问题表单的HTML
  226. */
  227. protected function questionsForm($calculatorId)
  228. {
  229. if (!$calculatorId) {
  230. return '<div id="questions"></div><button type="button" id="add-question" class="btn btn-success">添加问题</button>';
  231. }
  232. $questions = MedicalQuestion::where('medical_calculator_id', $calculatorId)->with('options')->get();
  233. $html = '<div id="questions">';
  234. foreach ($questions as $index => $question) {
  235. $html .= $this->questionTemplate($index, $question);
  236. }
  237. $html .= '</div>';
  238. $html .= '<button type="button" id="add-question" class="btn btn-success">添加问题</button>';
  239. return $html;
  240. }
  241. /**
  242. * 生成单个问题的HTML模板
  243. *
  244. * @param int|string $index 问题索引
  245. * @param MedicalQuestion|null $question 问题对象
  246. * @return string 单个问题的HTML模板
  247. */
  248. protected function questionTemplate($index, $question = null)
  249. {
  250. $index = is_numeric($index) ? $index : "' + questionIndex + '";
  251. $html = '<div class="question" data-index="' . $index . '">';
  252. $html .= '<input type="hidden" name="questions[' . $index . '][id]" value="' . ($question ? htmlspecialchars($question->id) : '') . '">';
  253. $html .= '<div class="form-group">';
  254. $html .= '<label>问题内容:</label>';
  255. $html .= '<input type="text" name="questions[' . $index . '][question]" value="' . ($question ? htmlspecialchars($question->question) : '') . '" placeholder="请输入问题内容" class="form-control">';
  256. $html .= '</div>';
  257. $html .= '<div class="form-group">';
  258. $html .= '<label>变量名称:</label>';
  259. $html .= '<input type="text" name="questions[' . $index . '][variable_name]" value="' . ($question ? htmlspecialchars($question->variable_name) : '') . '" placeholder="请输入变量名称" class="form-control">';
  260. $html .= '</div>';
  261. $html .= '<div class="form-group">';
  262. $html .= '<label>问题类型:</label>';
  263. $html .= '<select name="questions[' . $index . '][type]" class="form-control question-type">';
  264. $html .= '<option value="text" ' . ($question && $question->type == 'text' ? 'selected' : '') . '>填空</option>';
  265. $html .= '<option value="radio" ' . ($question && $question->type == 'radio' ? 'selected' : '') . '>单选</option>';
  266. $html .= '<option value="checkbox" ' . ($question && $question->type == 'checkbox' ? 'selected' : '') . '>多选</option>';
  267. $html .= '</select>';
  268. $html .= '</div>';
  269. $html .= '<div class="form-group score-formula-group" ' . ($question && ($question->type == 'radio' || $question->type == 'checkbox') ? 'style="display:none;"' : '') . '>';
  270. $html .= '<label>分数:</label>';
  271. $html .= '<input type="text" name="questions[' . $index . '][score]" value="' . ($question ? $question->score : '') . '" placeholder="请输入分数" class="form-control score-formula">';
  272. $html .= '</div>';
  273. $html .= '<div class="options" ' . ($question && ($question->type == 'radio' || $question->type == 'checkbox') ? '' : 'style="display:none;"') . '>';
  274. if ($question && $question->options) {
  275. foreach ($question->options as $optionIndex => $option) {
  276. $html .= $this->optionTemplate($index, $optionIndex, $option);
  277. }
  278. }
  279. $html .= '</div>';
  280. $html .= '<button type="button" class="btn btn-info add-option" ' . ($question && ($question->type == 'radio' || $question->type == 'checkbox') ? '' : 'style="display:none;"') . '>添加选项</button>';
  281. $html .= '<button type="button" class="btn btn-danger remove-question">删除问题</button>';
  282. $html .= '</div>';
  283. return $html;
  284. }
  285. /**
  286. * 生成单个选项的HTML模板
  287. *
  288. * @param int|string $questionIndex 问题索引
  289. * @param int|string $optionIndex 选项索引
  290. * @param MedicalOption|null $option 选项对象
  291. * @return string 单个选项的HTML模板
  292. */
  293. protected function optionTemplate($questionIndex, $optionIndex, $option = null)
  294. {
  295. $questionIndex = is_numeric($questionIndex) ? $questionIndex : "' + questionIndex + '";
  296. $optionIndex = is_numeric($optionIndex) ? $optionIndex : "' + optionIndex + '";
  297. $html = '<div class="option">';
  298. $html .= '<input type="hidden" name="questions[' . $questionIndex . '][options][' . $optionIndex . '][id]" value="' . ($option ? $option->id : '') . '">';
  299. $html .= '<div class="form-group">';
  300. $html .= '<label>选项内容:</label>';
  301. $html .= '<input type="text" name="questions[' . $questionIndex . '][options][' . $optionIndex . '][content]" value="' . ($option ? $option->content : '') . '" placeholder="请输入选项内容" class="form-control">';
  302. $html .= '</div>';
  303. $html .= '<div class="form-group">';
  304. $html .= '<label>选项分数:</label>';
  305. $html .= '<input type="number" step="0.01" name="questions[' . $questionIndex . '][options][' . $optionIndex . '][score]" value="' . ($option ? $option->score : '') . '" placeholder="请输入选项分数" class="form-control">';
  306. $html .= '</div>';
  307. $html .= '<button type="button" class="btn btn-warning remove-option">删除选项</button>';
  308. $html .= '</div>';
  309. return $html;
  310. }
  311. /**
  312. * 生成问题和选项的JavaScript
  313. *
  314. * @return string JavaScript代码
  315. */
  316. protected function questionsScript()
  317. {
  318. $questionTemplate = json_encode($this->questionTemplate("' + questionIndex + '"));
  319. $optionTemplate = json_encode($this->optionTemplate("' + questionIndex + '", "' + optionIndex + '"));
  320. return <<<EOT
  321. $(document).ready(function() {
  322. let questionIndex = $('.question').length;
  323. // 移除所有现有的事件处理程序
  324. $(document).off('click', '#add-question');
  325. $(document).off('click', '.add-option');
  326. $(document).off('click', '.remove-question');
  327. $(document).off('click', '.remove-option');
  328. $(document).off('change', '.question-type');
  329. // 重新绑定事件处理程序
  330. $('#add-question').on('click', function() {
  331. let newQuestion = $($questionTemplate.replace(/' \+ questionIndex \+ '/g, questionIndex));
  332. $('#questions').append(newQuestion);
  333. questionIndex++;
  334. // 触发新添加问题的类型选择变化事件
  335. newQuestion.find('.question-type').trigger('change');
  336. });
  337. $(document).on('click', '.add-option', function() {
  338. let questionDiv = $(this).closest('.question');
  339. let questionIndex = questionDiv.data('index');
  340. let optionIndex = questionDiv.find('.option').length;
  341. let newOption = $($optionTemplate.replace(/' \+ questionIndex \+ '/g, questionIndex).replace(/' \+ optionIndex \+ '/g, optionIndex));
  342. questionDiv.find('.options').append(newOption);
  343. });
  344. $(document).on('click', '.remove-question', function() {
  345. $(this).closest('.question').remove();
  346. });
  347. $(document).on('click', '.remove-option', function() {
  348. $(this).closest('.option').remove();
  349. });
  350. $(document).on('change', '.question-type', function() {
  351. let questionDiv = $(this).closest('.question');
  352. let optionsDiv = questionDiv.find('.options');
  353. let scoreFormulaGroup = questionDiv.find('.score-formula-group');
  354. let addOptionButton = questionDiv.find('.add-option');
  355. if (this.value === 'text') {
  356. optionsDiv.hide();
  357. scoreFormulaGroup.show();
  358. addOptionButton.hide();
  359. } else {
  360. optionsDiv.show();
  361. scoreFormulaGroup.hide();
  362. addOptionButton.show();
  363. }
  364. });
  365. // 初始化时触发一次
  366. $('.question-type').trigger('change');
  367. });
  368. EOT;
  369. }
  370. /**
  371. * 获取所有计算器
  372. *
  373. * @return \Illuminate\Http\JsonResponse 所有计算器的JSON响应
  374. */
  375. public function getAllCalculators()
  376. {
  377. $calculators = MedicalCalculator::all();
  378. return response()->json($calculators);
  379. }
  380. /**
  381. * 获取指定计算器
  382. *
  383. * @param int $id 医学计算器ID
  384. * @return \Illuminate\Http\JsonResponse 计算器详情
  385. */
  386. public function show($id)
  387. {
  388. $calculator = MedicalCalculator::findOrFail($id);
  389. return response()->json($calculator);
  390. }
  391. /**
  392. * 更新指定计算器
  393. *
  394. * @param Request $request 请求对象
  395. * @param int $id 医学计算器ID
  396. * @return \Illuminate\Http\JsonResponse 更新后的计算器详情
  397. */
  398. public function update(Request $request, $id)
  399. {
  400. $calculator = MedicalCalculator::findOrFail($id);
  401. $calculator->update($request->all());
  402. $this->saveQuestions($calculator);
  403. admin_toastr('更新成功', 'success');
  404. return redirect()->route('admin.medical-calculators.index');
  405. }
  406. /**
  407. * 删除指定计算器(支持批量删除)
  408. *
  409. * @param Request $request
  410. * @return \Illuminate\Http\JsonResponse 删除结果
  411. */
  412. public function destroy($id)
  413. {
  414. $ids = is_array($id) ? $id : [$id];
  415. $successCount = 0;
  416. $failCount = 0;
  417. \Illuminate\Support\Facades\DB::transaction(function () use ($ids, &$successCount, &$failCount) {
  418. // 批量删除关联的问题和选项
  419. $deletedOptionsCount = MedicalOption::whereIn('medical_question_id', function($query) use ($ids) {
  420. $query->select('id')
  421. ->from('medical_questions')
  422. ->whereIn('medical_calculator_id', $ids);
  423. })->delete();
  424. $deletedQuestionsCount = MedicalQuestion::whereIn('medical_calculator_id', $ids)->delete();
  425. // 删除计算器
  426. $deletedCalculatorsCount = MedicalCalculator::whereIn('id', $ids)->delete();
  427. $successCount = $deletedCalculatorsCount;
  428. $failCount = count($ids) - $deletedCalculatorsCount;
  429. });
  430. $message = "成功删除 {$successCount} 个计算器";
  431. if ($failCount > 0) {
  432. $message .= ",{$failCount} 个计算器删除失败";
  433. }
  434. return response()->json(['message' => $message, 'status' => true]);
  435. }
  436. /**
  437. * 保存问题和选项
  438. *
  439. * @param Form|MedicalCalculator $formOrModel 表单对象或模型实例
  440. */
  441. protected function saveQuestions($formOrModel)
  442. {
  443. if ($formOrModel instanceof Form) {
  444. $formModelId = $formOrModel->model()->id;
  445. } elseif ($formOrModel instanceof MedicalCalculator) {
  446. $formModelId = $formOrModel->id;
  447. } else {
  448. throw new \InvalidArgumentException('Invalid argument type');
  449. }
  450. $questionsData = request()->input('questions');
  451. if (!is_array($questionsData)) {
  452. return; // 如果 $questionsData 不是数组,直接返回
  453. }
  454. foreach ($questionsData as $index => $questionData) {
  455. if (!isset($questionData['question']) || !isset($questionData['type'])) {
  456. continue;
  457. }
  458. try {
  459. $variableName = !empty($questionData['variable_name']) ? $questionData['variable_name'] : 'VAR_' . ($index + 1);
  460. $question = MedicalQuestion::updateOrCreate(
  461. ['id' => $questionData['id'] ?? null],
  462. [
  463. 'medical_calculator_id' => $formModelId,
  464. 'question' => $questionData['question'],
  465. 'variable_name' => $variableName,
  466. 'type' => $questionData['type'],
  467. 'score' => isset($questionData['score']) ? (float)$questionData['score'] : null,
  468. ]
  469. );
  470. // 删除旧的选项
  471. $question->options()->delete();
  472. if (isset($questionData['options']) && is_array($questionData['options'])) {
  473. foreach ($questionData['options'] as $optionData) {
  474. if (!isset($optionData['content'])) {
  475. continue;
  476. }
  477. $option = new MedicalOption([
  478. 'content' => $optionData['content'],
  479. 'score' => isset($optionData['score']) ? (float)$optionData['score'] : 0,
  480. ]);
  481. $question->options()->save($option);
  482. }
  483. }
  484. } catch (\Exception $e) {
  485. \Illuminate\Support\Facades\Log::error('Error saving question: ' . $e->getMessage());
  486. }
  487. }
  488. }
  489. /**
  490. * 定义医学计算器的表格
  491. *
  492. * @return \Encore\Admin\Grid 表格对象
  493. */
  494. protected function grid()
  495. {
  496. return Admin::grid(MedicalCalculator::class, function (Grid $grid) {
  497. $grid->name('计算器名称');
  498. $grid->disease_name('疾病名称');
  499. $grid->column('questions_count', '问题数量');
  500. $grid->paginate(20);
  501. $grid->disableExport();
  502. $grid->actions(function ($actions) {
  503. $actions->disableView();
  504. });
  505. $grid->filter(function ($filter) {
  506. $filter->disableIdFilter();
  507. $filter->like('name', '输入名称搜索');
  508. });
  509. // 启用批量删除
  510. $grid->batchActions(function ($batch) {
  511. $batch->disableDelete();
  512. $batch->add(new BatchDeleteAction());
  513. });
  514. });
  515. }
  516. }