${escapeHtml(tool.name)}
${escapeHtml(tool.description)}
const tools = [ {id: "tool_1", name: "ClipFlow", category: "开发效率", description: "轻量级代码片段管理工具,支持团队共享与快速插入。", tags: ["代码片段", "团队协作", "VSCode"], version: "2.4.1", size: "12.3 MB", downloads: 36850, updatedAt: "2026-03-12", features: ["多工作区同步", "标签归档", "快捷键模板插入"]}, {id: "tool_2", name: "TaskOrbit", category: "团队协作", description: "面向产品团队的任务看板与冲刺管理工具。", tags: ["看板", "项目管理", "甘特图"], version: "1.9.0", url: "https://example.com/tools/taskorbit", downloads: 29210, updatedAt: "2026-03-18", features: ["多视图任务管理", "冲刺模板", "成员工作负载分析"]}, {id: "tool_3", name: "PixelLint", category: "设计协作", description: "设计稿一致性检查工具,可自动扫描颜色与间距规范。", tags: ["设计规范", "Figma", "UI 质检"], version: "3.1.2", size: "7.8 MB", downloads: 21430, updatedAt: "2026-02-25", features: ["组件一致性对比", "批量标注问题", "导出质检报告"]}, {id: "tool_4", name: "DataSparrow", category: "数据分析", description: "可视化数据清洗与探索平台,适合中小团队快速上手。", tags: ["可视化", "数据清洗", "BI"], version: "4.0.0", url: "https://example.com/tools/datasparrow", downloads: 17680, updatedAt: "2026-03-21", features: ["拖拽式数据流", "字段质量检测", "图表模板库"]}, {id: "tool_5", name: "ShipMate", category: "自动化", description: "一键打包发布脚本管理器,统一多环境部署流程。", tags: ["CI/CD", "部署", "脚本"], version: "2.0.3", size: "10.1 MB", downloads: 25600, updatedAt: "2026-03-09", features: ["多环境变量模板", "发布回滚", "构建流水线监控"]}, {id: "tool_6", name: "InsightPanel", category: "数据分析", description: "业务指标仪表盘构建器,支持实时数据看板。", tags: ["仪表盘", "指标", "实时看板"], version: "5.2.1", url: "https://example.com/tools/insightpanel", downloads: 40980, updatedAt: "2026-03-20", features: ["实时刷新组件", "阈值告警", "多数据源连接"]}, {id: "tool_7", name: "CloudWatchdog", category: "运维监控", description: "基础设施健康监控工具,支持可视化告警规则。", tags: ["告警", "监控", "日志分析"], version: "1.6.8", size: "15.4 MB", downloads: 19870, updatedAt: "2026-03-15", features: ["阈值策略库", "异常聚合", "告警分级通知"]}, {id: "tool_8", name: "FormForge", category: "开发效率", description: "表单构建器,支持低代码生成校验规则与提交流程。", tags: ["低代码", "表单", "校验"], version: "3.7.5", size: "9.2 MB", downloads: 23200, updatedAt: "2026-03-11", features: ["可视化表单设计", "字段联动", "提交数据导出"]}, {id: "tool_9", name: "QuerySprint", category: "数据分析", description: "面向分析师的 SQL 协作平台,支持查询片段共享。", tags: ["SQL", "查询优化", "协作"], version: "2.8.4", url: "https://example.com/tools/querysprint", downloads: 27540, updatedAt: "2026-02-18", features: ["查询版本管理", "性能诊断建议", "团队模版库"]}, {id: "tool_10", name: "TestPilot", category: "自动化", description: "自动化测试流程编排工具,支持 API 与 UI 混合测试。", tags: ["自动化测试", "API", "回归测试"], version: "4.3.0", size: "22.4 MB", downloads: 31420, updatedAt: "2026-03-17", features: ["测试用例可视化", "失败重跑策略", "测试报告仪表盘"]}, {id: "tool_11", name: "BrandBoard", category: "设计协作", description: "品牌资产管理工具,统一素材规范与组件资产。", tags: ["品牌资产", "设计系统", "素材管理"], version: "1.4.2", url: "https://example.com/tools/brandboard", downloads: 16890, updatedAt: "2026-03-08", features: ["版本化素材库", "品牌规范手册", "跨团队共享链接"]}, {id: "tool_12", name: "DeployLens", category: "运维监控", description: "发布质量追踪平台,聚合版本、错误率与回滚记录。", tags: ["发布追踪", "SRE", "质量分析"], version: "2.2.6", url: "https://example.com/tools/deploylens", downloads: 22160, updatedAt: "2026-03-19", features: ["发布健康指标", "回滚影响分析", "问题根因视图"]} ]; const state = {query: "", category: "all", sortBy: "popular", page: 1, pageSize: 6}; const keywords = ["自动化", "设计系统", "仪表盘", "监控", "协作"]; let toastTimer = null; const elements = { headerWrap: document.querySelector(".header-wrap"), overviewBtn: document.getElementById("overviewBtn"), searchInput: document.getElementById("searchInput"), categorySelect: document.getElementById("categorySelect"), categorySidebarList: document.getElementById("categorySidebarList"), sortSelect: document.getElementById("sortSelect"), resetBtn: document.getElementById("resetBtn"), hotKeywords: document.getElementById("hotKeywords"), resultTip: document.getElementById("resultTip"), toolGrid: document.getElementById("toolGrid"), pagination: document.getElementById("pagination"), prevBtn: document.getElementById("prevBtn"), nextBtn: document.getElementById("nextBtn"), pageText: document.getElementById("pageText"), detailModal: document.getElementById("detailModal"), closeModalBtn: document.getElementById("closeModalBtn"), overviewModal: document.getElementById("overviewModal"), closeOverviewModalBtn: document.getElementById("closeOverviewModalBtn"), detailTitle: document.getElementById("detailTitle"), detailDescription: document.getElementById("detailDescription"), detailMeta: document.getElementById("detailMeta"), detailFeatures: document.getElementById("detailFeatures"), toast: document.getElementById("toast"), kpiTotal: document.getElementById("kpiTotal"), kpiCategories: document.getElementById("kpiCategories"), kpiDownloads: document.getElementById("kpiDownloads"), kpiFiltered: document.getElementById("kpiFiltered") }; function escapeHtml(value) { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function formatNumber(value) { return new Intl.NumberFormat("zh-CN").format(value); } function formatDate(dateText) { return new Intl.DateTimeFormat("zh-CN", {year: "numeric", month: "2-digit", day: "2-digit"}) .format(new Date(dateText)); } function getCategories() { return Array.from(new Set(tools.map((tool) => tool.category))); } function matchesQuery(tool, query) { if (!query) { return true; } const pool = [tool.name, tool.description, tool.category, ...tool.tags].join(" ").toLowerCase(); return pool.includes(query); } function buildOptions() { const categories = getCategories(); categories.forEach((category) => { const option = document.createElement("option"); option.value = category; option.textContent = category; elements.categorySelect.append(option); }); keywords.forEach((keyword) => { const button = document.createElement("button"); button.type = "button"; button.className = "chip"; button.dataset.keyword = keyword; button.textContent = keyword; elements.hotKeywords.append(button); }); } function renderCategorySidebar() { if (!elements.categorySidebarList) { return; } const query = state.query.trim().toLowerCase(); const queryMatchedTools = tools.filter((tool) => matchesQuery(tool, query)); const countMap = new Map(); queryMatchedTools.forEach((tool) => { countMap.set(tool.category, (countMap.get(tool.category) || 0) + 1); }); const items = [ {value: "all", label: "全部分类", count: queryMatchedTools.length}, ...getCategories().map((category) => ({ value: category, label: category, count: countMap.get(category) || 0 })) ]; elements.categorySidebarList.textContent = ""; const fragment = document.createDocumentFragment(); items.forEach((item) => { const button = document.createElement("button"); button.type = "button"; button.className = "category-side-btn"; button.dataset.category = item.value; button.setAttribute("aria-pressed", item.value === state.category ? "true" : "false"); if (item.value === state.category) { button.classList.add("active"); } const label = document.createElement("span"); label.className = "label"; label.textContent = item.label; const count = document.createElement("span"); count.className = "count"; count.textContent = formatNumber(item.count); button.append(label, count); fragment.append(button); }); elements.categorySidebarList.append(fragment); } function filterTools() { const query = state.query.trim().toLowerCase(); const filtered = tools.filter((tool) => { if (state.category !== "all" && state.category !== tool.category) { return false; } return matchesQuery(tool, query); }); const sorted = [...filtered]; if (state.sortBy === "popular") { sorted.sort((a, b) => b.downloads - a.downloads); } else if (state.sortBy === "latest") { sorted.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); } else { sorted.sort((a, b) => a.name.localeCompare(b.name, "zh-Hans-CN")); } return sorted; } function paginate(items) { const totalPages = Math.max(1, Math.ceil(items.length / state.pageSize)); if (state.page > totalPages) { state.page = totalPages; } const start = (state.page - 1) * state.pageSize; return {items: items.slice(start, start + state.pageSize), totalPages, start}; } function renderKpi(filteredCount) { elements.kpiTotal.textContent = formatNumber(tools.length); elements.kpiCategories.textContent = formatNumber(new Set(tools.map((tool) => tool.category)).size); elements.kpiDownloads.textContent = formatNumber(tools.reduce((sum, tool) => sum + tool.downloads, 0)); elements.kpiFiltered.textContent = formatNumber(filteredCount); } function render() { const filtered = filterTools(); const page = paginate(filtered); const displayStart = filtered.length ? page.start + 1 : 0; const displayEnd = Math.min(page.start + state.pageSize, filtered.length); renderKpi(filtered.length); renderCategorySidebar(); elements.resultTip.textContent = `共找到 ${formatNumber(filtered.length)} 个工具,当前显示 ${displayStart}-${displayEnd}。`; if (filtered.length === 0) { elements.toolGrid.innerHTML = `
没有匹配结果,请尝试更换关键词或分类。
${escapeHtml(tool.description)}