model = new \app\admin\model\FcCategory; $parentList = [0 => '顶级分类(一级大类)']; $topRows = Db::name('fc_category') ->where('pid', 0) ->order('sort asc, id asc') ->select(); foreach ($topRows as $r) { $parentList[(int)$r['id']] = (string)$r['name'] . ((int)$r['enabled'] ? '' : ' (已停用)'); } $this->view->assign('parentList', $parentList); } /** * 分类列表(按一级分类 + 二级分类层级分组展示,支持附带父分类名称) * * @return string|\think\response\Json */ public function index() { $this->request->filter(['strip_tags', 'trim']); if ($this->request->isAjax()) { if ($this->request->request('keyField')) { return $this->selectpage(); } list($where, $sort, $order, $offset, $limit) = $this->buildparams(); // 分类总量有限:全量取回后按「父在前、子随父」的树形顺序在内存中重排, // 再切片分页,避免数据库层先 limit 导致翻页后父子分组被打散。 $rows = $this->model->where($where) ->order('pid asc, sort asc, id asc') ->select(); $total = count($rows); $rows = collection($rows)->toArray(); $parentMap = Db::name('fc_category')->where('pid', 0)->column('name', 'id'); // 树形顺序排列(一级分类紧跟其下属二级分类) $byParent = []; $tops = []; $others = []; foreach ($rows as &$r) { $pid = isset($r['pid']) ? (int)$r['pid'] : 0; $r['parent_name'] = $pid > 0 && isset($parentMap[$pid]) ? (string)$parentMap[$pid] : '顶级分类'; if (empty($r['image']) && !empty($r['icon_key'])) { $r['image'] = $this->resolveImageFromKey($r['icon_key']); } if ($pid === 0) { $tops[] = $r; } else { $byParent[$pid][] = $r; $others[] = $r; } } unset($r); $ordered = []; $usedIds = []; foreach ($tops as $top) { $ordered[] = $top; $usedIds[$top['id']] = true; $tid = (int)$top['id']; if (!empty($byParent[$tid])) { foreach ($byParent[$tid] as $child) { $ordered[] = $child; $usedIds[$child['id']] = true; } } } foreach ($others as $o) { if (!isset($usedIds[$o['id']])) { $ordered[] = $o; } } return json(['total' => $total, 'rows' => array_slice($ordered, $offset, $limit)]); } $homeCount = Db::name('fc_category')->where('pid', '>', 0)->where('is_home', 1)->count(); $this->view->assign('homeCount', $homeCount); return $this->view->fetch(); } /** * 新增分类(自动同步 image 与 icon_key) * * @return string */ /** * 校验首页金刚区推荐上限(最多8个,且仅允许二级分类) * * @param array $idArr 涉及的分类ID数组 * @param int $isHome 设定的推荐值(1开启,0关闭) * @return void */ protected function checkHomeNavLimit(array $idArr, $isHome) { if ((int)$isHome !== 1 || empty($idArr)) { return; } // 1. 检查是否存在一级大类 $topCategories = Db::name('fc_category') ->where('id', 'in', $idArr) ->where('pid', 0) ->column('name'); if (!empty($topCategories)) { $topNames = implode('、', $topCategories); $this->error("首页金刚区专用于二级品类推荐,一级大类【{$topNames}】不可推荐至金刚区"); } // 2. 统计当前已推荐的二级分类数量(排除当前正在编辑的记录) $currentCount = Db::name('fc_category') ->where('pid', '>', 0) ->where('is_home', 1) ->whereNotIn('id', $idArr) ->count(); $newTotal = $currentCount + count($idArr); if ($newTotal > 8) { $this->error("首页金刚区按钮上限仅为 8 个!当前已有 {$currentCount} 个已推荐,本次操作将达到 {$newTotal} 个,请先关闭其他已推荐分类。"); } } /** * 批量更新与单项开关切换(严格控制首页金刚区最多 8 个) * * @param string $ids 待操作的记录ID串 * @return void */ public function multi($ids = "") { $ids = $ids ? $ids : $this->request->param("ids"); if ($ids) { if ($this->request->has('params')) { parse_str($this->request->post("params"), $values); if (isset($values['is_home'])) { $idArr = array_filter(explode(',', (string)$ids)); $this->checkHomeNavLimit($idArr, $values['is_home']); } } } parent::multi($ids); } public function add() { if ($this->request->isPost()) { $params = $this->request->post('row/a', []); $params = $this->normalizeCategoryParams($params); $this->request->post(['row' => $params]); } return parent::add(); } /** * 编辑分类(自动补全图片路径并同步 image 与 icon_key) * * @param mixed $ids * @return string */ public function edit($ids = null) { $row = $this->model->get($ids); if (!$row) { $this->error(__('No Results were found')); } $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) { $this->error(__('You have no permission')); } if ($this->request->isPost()) { $params = $this->request->post('row/a', []); $params = $this->normalizeCategoryParams($params, (int)$ids); if (isset($params['is_home'])) { $this->checkHomeNavLimit([(int)$ids], $params['is_home']); } $this->request->post(['row' => $params]); return parent::edit($ids); } if (empty($row['image']) && !empty($row['icon_key'])) { $row['image'] = $this->resolveImageFromKey($row['icon_key']); } $this->view->assign("row", $row); return $this->view->fetch(); } /** * 规范化分类参数:同步实拍图 image 与 icon_key,防止将自身设为上级分类 * * @param array $params * @param int $currentId * @return array */ protected function normalizeCategoryParams(array $params, $currentId = 0) { $pid = isset($params['pid']) ? max(0, (int)$params['pid']) : 0; if ($currentId > 0 && $pid === $currentId) { $pid = 0; } $params['pid'] = $pid; // 分类类型:farm=农产品(发布页显示产地规格字段),general=通用信息(招聘求职等) // 二级分类强制继承上级类型;编辑一级分类时级联同步其下属二级分类 $kind = isset($params['kind']) ? trim((string)$params['kind']) : ''; if (!in_array($kind, ['farm', 'general'], true)) { $kind = 'general'; } if ($pid > 0) { $parentKind = Db::name('fc_category')->where('id', $pid)->value('kind'); if ($parentKind === 'farm' || $parentKind === 'general') { $kind = $parentKind; } } elseif ($currentId > 0) { Db::name('fc_category')->where('pid', $currentId)->update(['kind' => $kind, 'updatetime' => time()]); } $params['kind'] = $kind; $iconKey = isset($params['icon_key']) ? trim((string)$params['icon_key']) : ''; $image = isset($params['image']) ? trim((string)$params['image']) : ''; if ($image !== '' && $iconKey === '') { $params['icon_key'] = $image; } elseif ($iconKey !== '' && $image === '') { $params['image'] = $this->resolveImageFromKey($iconKey); } elseif ($iconKey !== '' && preg_match('/^(\/uploads\/|\/static\/|https?:\/\/)/i', $iconKey)) { $params['image'] = $iconKey; } return $params; } /** * 根据 icon_key 推导分类图片路径 * * @param string $iconKey * @return string */ protected function resolveImageFromKey($iconKey) { $iconKey = trim((string)$iconKey); if ($iconKey === '') { return '/static/icons/cat-service.png'; } if (preg_match('/^(\/uploads\/|\/static\/|https?:\/\/)/i', $iconKey)) { return $iconKey; } $file = strpos($iconKey, '.') !== false ? $iconKey : ($iconKey . '.png'); if (is_file(ROOT_PATH . 'public/static/categories/' . $file)) { return '/static/categories/' . $file; } return '/static/icons/' . $file; } /** * SelectPage搜索(供信息/草稿表单选择同城分类) * * @internal */ public function selectpage() { return parent::selectpage(); } }