234 lines
6.8 KiB
PHP
Executable File
234 lines
6.8 KiB
PHP
Executable File
<?php
|
||
|
||
namespace App\Service;
|
||
|
||
use App\BaseApp\BaseService;
|
||
use App\Models\MenuModel;
|
||
use Exception;
|
||
use Psr\Container\ContainerExceptionInterface;
|
||
use Psr\Container\NotFoundExceptionInterface;
|
||
|
||
/**
|
||
* 菜单服务:列表树、搜索(含子菜单)、下拉
|
||
*/
|
||
class MenuService extends BaseService
|
||
{
|
||
public function __construct()
|
||
{
|
||
parent::__construct();
|
||
$this->model = MenuModel::class;
|
||
$this->selectField = [
|
||
'id', 'pid', 'title', 'icon', 'name', 'affix_tab', 'path', 'component', 'redirect', 'keep_alive', 'hide_in_menu', 'badge', 'badge_type', 'badge_variants', 'iframe_src', 'sort', 'query', 'created_at'
|
||
];
|
||
$this->queryField = [ 'title' => 'like', 'pid' => '=' ];
|
||
$this->optionField = ['id', 'title'];
|
||
$this->orderBy = [
|
||
'name' => 'sort',
|
||
'sort' => 'asc'
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取菜单列表(扁平树,前端 transform)
|
||
* 有 title 搜索时:命中节点 + 祖先 + 子孙,保证子菜单可搜到并完整展示路径
|
||
* 无搜索时:仍按根菜单分页,再挂两级子节点
|
||
*
|
||
* @throws ContainerExceptionInterface
|
||
* @throws NotFoundExceptionInterface
|
||
*/
|
||
public function list()
|
||
{
|
||
$title = trim((string) request()->get('title', ''));
|
||
if ($title !== '') {
|
||
return $this->listByTitleSearch($title);
|
||
}
|
||
|
||
$this->where[] = ['pid', '=', 0];
|
||
$list = $this->getPageList();
|
||
$ids = array_column($list['items'], 'id');
|
||
if (empty($ids)) {
|
||
return $list;
|
||
}
|
||
$items = $this->model::where('deleted_at', 0)->whereIn('pid', $ids)->orderBy('sort')->get($this->selectField);
|
||
$childrenIds = [];
|
||
foreach ($items as $v) {
|
||
$list['items'][] = $v;
|
||
$childrenIds[] = $v['id'];
|
||
}
|
||
if (!empty($childrenIds)) {
|
||
$items = $this->model::where('deleted_at', 0)->whereIn('pid', $childrenIds)->orderBy('sort')->get($this->selectField);
|
||
foreach ($items as $v) {
|
||
$list['items'][] = $v;
|
||
}
|
||
}
|
||
return $list;
|
||
}
|
||
|
||
/**
|
||
* 按标题模糊搜索:返回命中项及其祖先、子孙,供树表正确拼装
|
||
* 为什么要带祖先:仅返回子节点时 pid 找不到父级,树会丢节点或挂到根外
|
||
*/
|
||
private function listByTitleSearch(string $title): array
|
||
{
|
||
$all = $this->model::where('deleted_at', 0)
|
||
->orderBy('sort')
|
||
->get($this->selectField)
|
||
->keyBy('id');
|
||
if ($all->isEmpty()) {
|
||
return $this->emptyPageList();
|
||
}
|
||
|
||
$matchedIds = [];
|
||
foreach ($all as $id => $row) {
|
||
$rowTitle = (string) ($row['title'] ?? '');
|
||
if ($rowTitle !== '' && mb_stripos($rowTitle, $title) !== false) {
|
||
$matchedIds[] = (int) $id;
|
||
}
|
||
}
|
||
if (empty($matchedIds)) {
|
||
return $this->emptyPageList();
|
||
}
|
||
|
||
// 子节点按 pid 分组,便于向下展开子孙
|
||
$childrenMap = [];
|
||
foreach ($all as $id => $row) {
|
||
$pid = (int) ($row['pid'] ?? 0);
|
||
$childrenMap[$pid][] = (int) $id;
|
||
}
|
||
|
||
$needIds = [];
|
||
foreach ($matchedIds as $mid) {
|
||
// 向上:自身 + 祖先
|
||
$cur = $mid;
|
||
while ($cur > 0 && $all->has($cur) && !isset($needIds[$cur])) {
|
||
$needIds[$cur] = true;
|
||
$cur = (int) ($all[$cur]['pid'] ?? 0);
|
||
}
|
||
// 向下:命中节点的全部子孙(搜父级时仍能看到子树)
|
||
$stack = [$mid];
|
||
while (!empty($stack)) {
|
||
$nodeId = array_pop($stack);
|
||
foreach ($childrenMap[$nodeId] ?? [] as $cid) {
|
||
if (!isset($needIds[$cid])) {
|
||
$needIds[$cid] = true;
|
||
$stack[] = $cid;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
$items = [];
|
||
foreach ($all as $id => $row) {
|
||
if (isset($needIds[(int) $id])) {
|
||
$items[] = $row->toArray();
|
||
}
|
||
}
|
||
|
||
return [
|
||
'items' => $items,
|
||
'page' => 1,
|
||
'size' => count($items),
|
||
'total' => count($items),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 空分页结构(与 getPageList 对齐)
|
||
*/
|
||
private function emptyPageList(): array
|
||
{
|
||
return [
|
||
'items' => [],
|
||
'page' => 1,
|
||
'size' => 0,
|
||
'total' => 0,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取树形结构的菜单下拉
|
||
* @param bool $isSelect
|
||
* @return array
|
||
*/
|
||
public function getTreeOption(bool $isSelect = false): array
|
||
{
|
||
$this->optionField = ['id', 'title', 'icon', 'pid'];
|
||
$list = $this->model::get($this->optionField);
|
||
$result = $this->utils->tree($list);
|
||
if ($isSelect == 'true') {
|
||
array_unshift($result, [ 'id' => 0, 'title' => '根目录', 'pid' => 0, 'icon' => '' ]);
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 获取下拉列表
|
||
* @return mixed
|
||
* @throws ContainerExceptionInterface
|
||
* @throws NotFoundExceptionInterface
|
||
* @throws Exception
|
||
*/
|
||
public function option(): mixed
|
||
{
|
||
$option = $this->getOption();
|
||
if (request()->get('is_select', false)) {
|
||
$result = [
|
||
[ 'id' => 0, 'title' => '根目录' ]
|
||
];
|
||
foreach ($option as $item) {
|
||
$result[] = [ 'id' => $item['id'], 'title' => $item['title'] ];
|
||
}
|
||
return $result;
|
||
}
|
||
return $option;
|
||
}
|
||
|
||
/**
|
||
* 获取数据详情
|
||
* @param $id
|
||
* @return mixed
|
||
* @throws Exception
|
||
*/
|
||
public function detail($id): mixed
|
||
{
|
||
return $this->getDetail($id);
|
||
}
|
||
|
||
/**
|
||
* 新增数据
|
||
* @param $params
|
||
* @return mixed
|
||
*/
|
||
public function create($params): mixed
|
||
{
|
||
$params['badge_type'] = !empty($params['badge_type']) ? $params['badge_type']: 0;
|
||
$params['badge_variants'] = !empty($params['badge_variants']) ? $params['badge_variants']: 0;
|
||
$params['created_at'] = time();
|
||
return $this->insert($params);
|
||
}
|
||
|
||
/**
|
||
* 编辑数据
|
||
* @param $id
|
||
* @param $params
|
||
* @return mixed
|
||
*/
|
||
public function update($id, $params): mixed
|
||
{
|
||
$params['badge_type'] = !empty($params['badge_type']) ? $params['badge_type']: 0;
|
||
$params['badge_variants'] = !empty($params['badge_variants']) ? $params['badge_variants']: 0;
|
||
$params['updated_at'] = time();
|
||
return $this->save($id, $params);
|
||
}
|
||
|
||
/**
|
||
* 删除数据
|
||
* @param $ids
|
||
* @return mixed
|
||
*/
|
||
public function delete($ids): mixed
|
||
{
|
||
return $this->del($ids);
|
||
}
|
||
}
|