diff --git a/app/common.php b/app/common.php index 62e7bb3..6f965b1 100644 --- a/app/common.php +++ b/app/common.php @@ -1637,3 +1637,61 @@ if (!function_exists('isHcListEmpty')) { return false; } } + +/** + * 获取指定栏目下所有子栏目及其内容 + * @param int $parentId 父栏目ID + * @param int $sellerId 商户ID + * @param int $limit 每个子栏目返回的内容条数 + * @param string $sort 排序方式 + * @return array + */ +if (!function_exists('getChildCategoriesContent')) { + function getChildCategoriesContent(int $parentId, int $sellerId, int $limit = 6, string $sort = 'publish_time desc'): array + { + if (empty($parentId)) { + return []; + } + try { + $categoryModel = new \app\model\Category(); + + // 1. 查询所有子栏目 + $childCates = $categoryModel->where('parent_id', $parentId) + ->where('seller_id', $sellerId) + ->order('sort asc') + ->select() + ->toArray(); + + if (empty($childCates)) { + return []; + } + + // 2. 获取所有子栏目ID + $childCateIds = array_column($childCates, 'id'); + + // 3. 查询这些子栏目下的文章 + $contents = \app\model\SubContent::where('category_id', 'in', $childCateIds) + ->where('seller_id', $sellerId) + ->where('is_del', 1) + ->field('id,main_id,title,category_id,description,thumbnail,publish_time') + ->order($sort) + ->limit($limit) + ->select() + ->toArray(); + + // 4. 批量查询附件URL + $thumbIds = array_unique(array_filter(array_column($contents, 'thumbnail'))); + if (!empty($thumbIds)) { + $attachments = \app\model\Attachment::whereIn('id', $thumbIds)->column('url', 'id'); + foreach ($contents as &$content) { + $content['thumbnail_url'] = $attachments[$content['thumbnail']] ?? ''; + } + unset($content); + } + + return $contents; + } catch (\Exception $e) { + return []; + } + } +} diff --git a/app/controller/frontend/SearchController.php b/app/controller/frontend/SearchController.php index 9d3e815..a55f258 100644 --- a/app/controller/frontend/SearchController.php +++ b/app/controller/frontend/SearchController.php @@ -4,40 +4,132 @@ namespace app\controller\frontend; -use app\exception\ModelEmptyException; -use app\exception\ModelException; use app\model\Category; use app\model\CategorySubContent; use app\model\SubContent; use app\model\SysSetting; -use app\service\ContentService; -use app\validate\ContentValidate; -use think\exception\ValidateException; -use think\facade\Db; class SearchController extends BaseController { + /** 产品根栏目 */ + private const PRODUCT_CATE_ID = 333; + + /** 新闻根栏目 */ + private const NEWS_CATE_ID = 361; + + // 下载中心 + private const DOWNLOAD_CATE_ID = 356; + + /** 常见问题根栏目 */ + private const FAQ_CATE_ID = 357; + + // 视频教程 + private const VIDEO_CATE_ID = 358; + + //产品公告 + private const ANNOUNCEMENT_CATE_ID = 359; + /** - * 按栏目标题映射搜索 Tab 类型 + * 获取栏目自身及全部子栏目 ID */ - private function resolveSearchType(string $title): string + private function getCategoryTreeIds(int $rootId): array { - if ($title === '') { - return 'other'; + $category = new Category(); + $root = $category->where([ + 'id' => $rootId, + 'seller_id' => $this->sellerId, + 'website_id' => $this->siteId, + ])->find(); + if (empty($root)) { + return [$rootId]; } - if (mb_strpos($title, '产品') !== false) { - return 'product'; + $root = $root->toArray(); + $parentPath = ($root['path'] ?? '') . $root['id'] . '-'; + $childIds = $category->where([ + ['seller_id', '=', $this->sellerId], + ['website_id', '=', $this->siteId], + ['path', 'like', $parentPath . '%'], + ])->column('id'); + + return array_values(array_unique(array_merge( + [(int)$root['id']], + array_map('intval', $childIds ?: []) + ))); + } + + /** + * 按栏目 ID 取关联内容 ID + */ + private function getContentIdsByCategoryIds(array $cateIds): array + { + if (empty($cateIds)) { + return []; } - if (mb_strpos($title, '解决方案') !== false || mb_strpos($title, '方案') !== false) { - return 'solution'; + $cateSub = new CategorySubContent(); + $ids = $cateSub->where([ + ['seller_id', '=', $this->siteId], + ['category_id', 'in', $cateIds], + ])->column('sub_content_id'); + + return array_values(array_unique(array_map('intval', $ids ?: []))); + } + + /** + * 关键词命中的内容 ID + */ + private function getMatchedContentIds(array $subIds, string $keywords, bool $hasKeyword): array + { + $query = (new SubContent())->where([ + 'seller_id' => $this->sellerId, + 'is_del' => 1, + ])->whereIn('id', $subIds ?: [0]); + if ($hasKeyword) { + $query = $query->whereLike('title|description|sub_title', '%' . $keywords . '%'); } - if (mb_strpos($title, '新闻') !== false) { - return 'news'; + $ids = $query->column('id'); + + return array_map('intval', $ids ?: []); + } + + /** + * 统一格式化为 Y-m-d + */ + private function formatSearchDate($time): string + { + if ($time === null || $time === '') { + return ''; } - if (mb_strpos($title, '常见问题') !== false || mb_stripos($title, 'FAQ') !== false || mb_strpos($title, '问题') !== false) { - return 'faq'; + if (is_numeric($time)) { + return date('Y-m-d', (int)$time); } - return 'other'; + $ts = strtotime((string)$time); + return $ts ? date('Y-m-d', $ts) : substr((string)$time, 0, 10); + } + + /** + * 解析搜索结果类型 + */ + private function resolveSearchKind(int $contentId, array $sets): array + { + if (isset($sets['product'][$contentId])) { + return ['product', '产品']; + } + if (isset($sets['news'][$contentId])) { + return ['news', '新闻']; + } + if (isset($sets['faq'][$contentId])) { + return ['faq', '常见问题']; + } + if (isset($sets['download'][$contentId])) { + return ['download', '下载中心']; + } + if (isset($sets['video'][$contentId])) { + return ['video', '视频']; + } + if (isset($sets['announce'][$contentId])) { + return ['announce', '公告']; + } + return ['content', '内容']; } /** @@ -70,99 +162,120 @@ class SearchController extends BaseController //搜索 $param = $this->request->param(); $keywords = $param['q'] ?? ''; + $hasKeyword = !empty($param['q']); $type = $param['type'] ?? 'all'; - if (!in_array($type, ['all', 'product', 'solution', 'news', 'faq'], true)) { + if (!in_array($type, ['all', 'product', 'other', 'news', 'faq'], true)) { $type = 'all'; } $this->assign('keywords', $keywords); $this->assign('sr_type', $type); - // 获取可以查询的栏目 - $category = new Category(); - $categories = $category -> getAllCustomArrayData(['is_search' => 1],'id desc','id')['data']; - // status: 1 正常 2 禁用 - // $categories = $category -> getAllCustomArrayData(['status' => 1],'id desc','id')['data']; - // $categories = $category -> getAllCustomArrayData(['status' => 1],'id desc','id')['data']; - - $cateIds = array_column($categories,'id'); - - $cateTypeMap = []; - foreach ($categories as $cate) { - $cateTypeMap[(int)$cate['id']] = $this->resolveSearchType((string)($cate['title'] ?? '')); - } - + // 全部:可搜索栏目(is_search=1) + $categoryModel = new Category(); + $searchCategories = $categoryModel->getAllCustomArrayData(['is_search' => 1], 'id desc', 'id')['data']; + $searchCateIds = array_column($searchCategories, 'id'); $cateSub = new CategorySubContent(); - $cateSubWhere = [ - ['seller_id','=',$this->siteId], - ['category_id','in',$cateIds] - ]; - $cateSubCon = $cateSub->getAllCategorySubContent($cateSubWhere)['data']; - $subIds = array_column($cateSubCon,'sub_content_id'); - - // 内容 -> 首个栏目(与列表展示 category[0] 一致) - $contentCateMap = []; - foreach ($cateSubCon as $row) { - $sid = (int)$row['sub_content_id']; - if (!isset($contentCateMap[$sid])) { - $contentCateMap[$sid] = (int)$row['category_id']; - } - } + $searchSubIds = array_column($cateSub->getAllCategorySubContent([ + ['seller_id', '=', $this->siteId], + ['category_id', 'in', $searchCateIds ?: [0]], + ])['data'], 'sub_content_id'); - $SubContent = new SubContent(); - $baseWhere = ['seller_id' => $this->sellerId, 'is_del' => 1]; + // 各 Tab 固定根栏目(含子栏目) + $productContentIds = $this->getContentIdsByCategoryIds($this->getCategoryTreeIds(self::PRODUCT_CATE_ID)); + $newsContentIds = $this->getContentIdsByCategoryIds($this->getCategoryTreeIds(self::NEWS_CATE_ID)); + $faqContentIds = $this->getContentIdsByCategoryIds($this->getCategoryTreeIds(self::FAQ_CATE_ID)); - // 关键词命中的全量 ID(用于 Tab 全量统计) - $countQuery = (new SubContent())->where($baseWhere)->whereIn('id', $subIds ?: [0]); - if (!empty($param['q'])) { - $countQuery = $countQuery->whereLike('title|description|sub_title', '%' . $keywords . '%'); - } - $matchedIds = $countQuery->column('id'); + // 其他 = 下载中心 + 视频 + 公告 + $downloadContentIds = $this->getContentIdsByCategoryIds($this->getCategoryTreeIds(self::DOWNLOAD_CATE_ID)); + $videoContentIds = $this->getContentIdsByCategoryIds($this->getCategoryTreeIds(self::VIDEO_CATE_ID)); + $announceContentIds = $this->getContentIdsByCategoryIds($this->getCategoryTreeIds(self::ANNOUNCEMENT_CATE_ID)); + $otherContentIds = array_values(array_unique(array_merge( + $downloadContentIds, + $videoContentIds, + $announceContentIds + ))); + + $allMatchedIds = $this->getMatchedContentIds($searchSubIds, $keywords, $hasKeyword); + $productMatchedIds = $this->getMatchedContentIds($productContentIds, $keywords, $hasKeyword); + $newsMatchedIds = $this->getMatchedContentIds($newsContentIds, $keywords, $hasKeyword); + $faqMatchedIds = $this->getMatchedContentIds($faqContentIds, $keywords, $hasKeyword); + $otherMatchedIds = $this->getMatchedContentIds($otherContentIds, $keywords, $hasKeyword); $srCounts = [ - 'all' => count($matchedIds), - 'product' => 0, - 'solution' => 0, - 'news' => 0, - 'faq' => 0, + 'all' => count($allMatchedIds), + 'product' => count($productMatchedIds), + 'other' => count($otherMatchedIds), + 'news' => count($newsMatchedIds), + 'faq' => count($faqMatchedIds), ]; - foreach ($matchedIds as $mid) { - $cid = $contentCateMap[(int)$mid] ?? 0; - $itemType = $cateTypeMap[$cid] ?? 'other'; - if (isset($srCounts[$itemType])) { - $srCounts[$itemType]++; - } - } $this->assign('sr_counts', $srCounts); - // 按 Tab 类型筛选列表用的内容 ID - $listSubIds = $matchedIds; - if ($type !== 'all') { - $listSubIds = []; - foreach ($matchedIds as $mid) { - $cid = $contentCateMap[(int)$mid] ?? 0; - if (($cateTypeMap[$cid] ?? 'other') === $type) { - $listSubIds[] = (int)$mid; - } - } + // 列表:全部不变;其余按对应栏目查 + if ($type === 'product') { + $listSubIds = $productMatchedIds; + } elseif ($type === 'news') { + $listSubIds = $newsMatchedIds; + } elseif ($type === 'faq') { + $listSubIds = $faqMatchedIds; + } elseif ($type === 'other') { + $listSubIds = $otherMatchedIds; + } else { + $listSubIds = $allMatchedIds; } + $baseWhere = ['seller_id' => $this->sellerId, 'is_del' => 1]; $content = (new SubContent())->with(['thumbnail'=>function($q){ $q->field('id,name,url,type'); },'category'])->where($baseWhere)->whereIn('id', $listSubIds ?: [0]); - $param['siteId'] = $this->siteId; - $param['sellerId'] = $this->sellerId; + $productIdSet = array_flip($productContentIds); + $newsIdSet = array_flip($newsContentIds); + $faqIdSet = array_flip($faqContentIds); + $downloadIdSet = array_flip($downloadContentIds); + $videoIdSet = array_flip($videoContentIds); + $announceIdSet = array_flip($announceContentIds); + $kindSets = [ + 'product' => $productIdSet, + 'news' => $newsIdSet, + 'faq' => $faqIdSet, + 'download' => $downloadIdSet, + 'video' => $videoIdSet, + 'announce' => $announceIdSet, + ]; + $tabKindMap = [ + 'product' => ['product', '产品'], + 'news' => ['news', '新闻'], + 'faq' => ['faq', '常见问题'], + ]; + $data = $content ->order('id','desc') ->paginate([ 'page' => $param['page'] ?? 1, 'list_rows' => $param['limit'] ?? 10, - ])->each(function (&$item)use($param){ - if(!empty($item['category'])) { - $item['category'] = $item->toArray()['category']; - $item['category_id'] = $item['category'][0]['id']; + ])->each(function (&$item) use ($type, $tabKindMap, $kindSets) { + $row = $item->toArray(); + if (!empty($row['category'])) { + $item['category'] = $row['category']; + $item['category_id'] = $row['category'][0]['id'] ?? 0; + } + $item['thumbnail'] = $row['thumbnail'] ?? null; + $item['url'] = $row['url'] ?? ''; + $rawContent = $row['content'] ?? ''; + // 与 ContentService / 常见问题页一致:先解码再交给模板 strip_tags + $item['content'] = $rawContent !== '' ? htmlspecialchars_decode((string)$rawContent) : ''; + $item['sr_date'] = $this->formatSearchDate($row['publish_time'] ?? ($row['create_time'] ?? '')); + + if (isset($tabKindMap[$type])) { + [$kind, $name] = $tabKindMap[$type]; + } else { + [$kind, $name] = $this->resolveSearchKind((int)($row['id'] ?? 0), $kindSets); + if ($kind === 'content' && !empty($row['category'][0]['title'])) { + $name = $row['category'][0]['title']; + } } - $item['thumbnail'] = $item->toArray()['thumbnail']; + $item['sr_kind'] = $kind; + $item['sr_tab_name'] = $name; }); $append = ['q' => $keywords]; if ($type !== 'all') { diff --git a/public/themes/dist_static/static/css/search.css b/public/themes/dist_static/static/css/search.css index 5b70d6e..60f3542 100644 --- a/public/themes/dist_static/static/css/search.css +++ b/public/themes/dist_static/static/css/search.css @@ -231,6 +231,70 @@ color: var(--color-primary); } +/* 分类型:统一左右布局,仅叠加交互样式(视频播放与视频教程页一致) */ +.sr-item--video .sr-item__media--video { + position: relative; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + overflow: hidden; + text-align: left; + appearance: none; + -webkit-appearance: none; +} +.sr-item--video .v-card__media { + width: 100%; + height: 100%; + aspect-ratio: auto; + border-radius: inherit; +} +.sr-item--video .v-card__cover { + object-fit: cover; +} +.sr-item--video .sr-item__media--video:hover .v-card__cover { + transform: scale(1.04); +} +.sr-item--video .sr-item__media--video:hover .v-card__play { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} +.sr-item--video .v-card__play img, +.sr-item--video .sr-item__media:hover .v-card__play img { + width: 1rem; + height: auto; + margin-left: 0.125rem; + object-fit: contain; + transform: none; +} +.sr-item--faq .sr-item__faq { + margin: 0; + border-bottom: 0; + border-top: none; +} +.sr-item--faq .f-item__trigger { + padding-left: 0; + padding-right: 0; +} +.sr-item--faq .f-item__panel > .f-item__a { + min-height: 0; +} +.sr-item--faq .f-item.is-open .f-item__a { + padding-left: 1.25rem; + padding-right: 1.25rem; +} +.sr-item__more--download img, +.sr-item__more--product .s-product-card__arrow { + display: block; + width: 1.25rem; + height: auto; + flex-shrink: 0; +} +.sr-item__more--download:hover, +.sr-item__more--product:hover { + color: var(--color-primary); +} + @media (max-width: 992px) { .sr-page { padding-top: calc(var(--header-h) + 32Px); diff --git a/public/themes/dist_static/static/js/search.js b/public/themes/dist_static/static/js/search.js index 674b72f..b32dcbc 100644 --- a/public/themes/dist_static/static/js/search.js +++ b/public/themes/dist_static/static/js/search.js @@ -1,13 +1,74 @@ import "./common.js"; import "./pagination.js"; //#region js/search.js +function initHotTags() { + $(".sr-hot__tag").on("click", function() { + const keyword = ($(this).attr("data-keyword") || $(this).text() || "").trim(); + if (!keyword) return; + const $form = $(".sr-search"); + $form.find(".s-search__input").val(keyword); + const form = $form.get(0); + if (!form) return; + if (typeof form.requestSubmit === "function") form.requestSubmit(); + else form.submit(); + }); +} +function initFaqAccordion() { + $(".sr-list .f-item__trigger").on("click", function() { + const $item = $(this).closest(".f-item"); + const willOpen = !$item.hasClass("is-open"); + $(".sr-list .f-item.is-open").not($item).removeClass("is-open").find(".f-item__trigger").attr("aria-expanded", "false"); + $item.toggleClass("is-open", willOpen); + $(this).attr("aria-expanded", willOpen ? "true" : "false"); + }); +} +function initVideoModal() { + const modal = document.getElementById("v-modal"); + const video = document.getElementById("v-modal-video"); + const titleEl = document.getElementById("v-modal-title"); + if (!modal || !video || !titleEl) return; + let lastFocus = null; + const openModal = (src, title) => { + if (!src) return; + lastFocus = document.activeElement; + titleEl.textContent = title || "视频播放"; + if (video.getAttribute("src") !== src) video.src = src; + modal.hidden = false; + document.body.style.overflow = "hidden"; + if (window.lenis?.stop) window.lenis.stop(); + video.play().catch(() => {}); + modal.querySelector(".v-modal__close")?.focus(); + }; + const closeModal = () => { + video.pause(); + video.removeAttribute("src"); + video.load(); + modal.hidden = true; + document.body.style.overflow = ""; + if (window.lenis?.start) window.lenis.start(); + if (lastFocus && typeof lastFocus.focus === "function") lastFocus.focus(); + }; + document.querySelectorAll(".sr-item--video [data-video-src]").forEach((card) => { + card.addEventListener("click", () => { + const title = + card.closest(".sr-item")?.querySelector(".sr-item__title")?.textContent?.trim() || + card.getAttribute("aria-label")?.replace(/^播放[::]/, "").trim() || + "视频播放"; + openModal(card.getAttribute("data-video-src") || "", title); + }); + }); + modal.querySelectorAll("[data-v-modal-close]").forEach((el) => { + el.addEventListener("click", closeModal); + }); + document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && !modal.hidden) closeModal(); + }); +} $(function() { const q = new URLSearchParams(window.location.search).get("q")?.trim() || ""; if (q) $(".sr-search .s-search__input").val(q); - $(".sr-hot__tag").on("click", function() { - const keyword = $(this).attr("data-keyword") || $(this).text().trim(); - $(".sr-search .s-search__input").val(keyword); - $(".sr-search").trigger("submit"); - }); + initHotTags(); + initFaqAccordion(); + initVideoModal(); }); //#endregion diff --git a/public/themes/website/1/zh/demo/download.html b/public/themes/website/1/zh/demo/download.html index 5877cfa..2592364 100644 --- a/public/themes/website/1/zh/demo/download.html +++ b/public/themes/website/1/zh/demo/download.html @@ -105,7 +105,7 @@
{hcTaglib:contentPage mainField="*" subField="*" name="hc_content" limit="12" item="vo"} - +
{$vo.title} diff --git a/public/themes/website/1/zh/demo/single_index.html b/public/themes/website/1/zh/demo/single_index.html index 4b3f806..be323d5 100644 --- a/public/themes/website/1/zh/demo/single_index.html +++ b/public/themes/website/1/zh/demo/single_index.html @@ -3,9 +3,9 @@ - 睿能科技 - - + {$current_cate.seo_title} + +