orderBy('sort')->orderBy('id')->get(); $tree = $this->buildMenuTree($rows); return $this->ok($tree->values()->all()); } public function store(Request $request): JsonResponse { $data = $this->validated($request); $menu = Menu::query()->create($data); return $this->ok(['id' => $menu->id], '已创建'); } public function update(Request $request, int $menu): JsonResponse { $model = Menu::query()->findOrFail($menu); $data = $this->validated($request, false); $model->fill($data); $model->save(); return $this->ok(null, '已保存'); } public function destroy(int $menu): JsonResponse { $model = Menu::query()->findOrFail($menu); if (Menu::query()->where('parent_id', $model->id)->exists()) { return $this->fail('请先删除子菜单', 422); } $model->delete(); return $this->ok(null, '已删除'); } /** * @return array */ protected function validated(Request $request, bool $creating = true): array { $rules = [ 'parent_id' => ['nullable', 'integer', 'exists:menus,id'], 'path' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:128'], 'title' => ['required', 'string', 'max:128'], 'component' => ['nullable', 'string', 'max:255'], 'icon' => ['nullable', 'string', 'max:64'], 'sort' => ['nullable', 'integer'], 'visible' => ['nullable', 'integer', 'in:0,1'], 'keep_alive' => ['nullable', 'integer', 'in:0,1'], 'permission_code' => ['nullable', 'string', 'max:128'], 'status' => ['nullable', 'integer', 'in:0,1'], ]; $data = $request->validate($rules); if (array_key_exists('component', $data) && $data['component'] !== null && $data['component'] !== '') { $this->assertSafeComponent($data['component']); } if (! $creating) { foreach (['sort', 'visible', 'keep_alive', 'status'] as $k) { if (array_key_exists($k, $data)) { $data[$k] = (int) $data[$k]; } } } else { $data['sort'] = (int) ($data['sort'] ?? 0); $data['status'] = (int) ($data['status'] ?? 1); $data['visible'] = (int) ($data['visible'] ?? 1); $data['keep_alive'] = (int) ($data['keep_alive'] ?? 0); } return $data; } protected function assertSafeComponent(string $component): void { if (str_contains($component, '..') || ! preg_match('/^[a-zA-Z0-9\/_-]+$/', $component)) { abort(422, 'component 不符合白名单规则'); } } /** * @param Collection $menus */ protected function buildMenuTree(Collection $menus): Collection { $byParent = $menus->groupBy(fn (Menu $m) => $m->parent_id ?? 0); $build = function ($parentId) use (&$build, $byParent): Collection { return ($byParent->get($parentId, collect()))->map(function (Menu $m) use (&$build) { $node = [ 'id' => $m->id, 'parent_id' => $m->parent_id, 'path' => $m->path, 'name' => $m->name, 'title' => $m->title, 'component' => $m->component, 'icon' => $m->icon, 'sort' => (int) $m->sort, 'visible' => (int) $m->visible, 'keep_alive' => (int) $m->keep_alive, 'permission_code' => $m->permission_code, 'status' => (int) $m->status, ]; $children = $build($m->id); if ($children->isNotEmpty()) { $node['children'] = $children->values()->all(); } return $node; }); }; return $build(0); } }