diff --git a/lib/router.js b/lib/router.js index b92da7ecc630a5..3e78f10e4c50f6 100644 --- a/lib/router.js +++ b/lib/router.js @@ -1090,8 +1090,8 @@ router.get('/wikihow/index', lazyloadRouteHandler('./routes/wikihow/index.js')); router.get('/wikihow/category/:category/:type', lazyloadRouteHandler('./routes/wikihow/category.js')); // 正版中国 -router.get('/getitfree/category/:category?', lazyloadRouteHandler('./routes/getitfree/category.js')); -router.get('/getitfree/search/:keyword?', lazyloadRouteHandler('./routes/getitfree/search.js')); +// router.get('/getitfree/category/:category?', lazyloadRouteHandler('./routes/getitfree/category.js')); +// router.get('/getitfree/search/:keyword?', lazyloadRouteHandler('./routes/getitfree/search.js')); // 万联网 router.get('/10000link/news/:category?', lazyloadRouteHandler('./routes/10000link/news')); diff --git a/lib/routes/getitfree/category.js b/lib/routes/getitfree/category.js deleted file mode 100644 index a76536bc3b2a2f..00000000000000 --- a/lib/routes/getitfree/category.js +++ /dev/null @@ -1,59 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const utils = require('./utils'); - -module.exports = async (ctx) => { - const { category = 'all' } = ctx.params; - const baseUrl = `https://getitfree.cn`; - const categoryToLabel = { - 8: 'PC', - 17: 'Android', - 50: 'Mac', - 309: '限时折扣', - 310: '限时免费', - 311: '永久免费', - 312: 'UWP', - all: '全部文章', - }; - - let item = []; - if (category === 'all') { - const res = await got(baseUrl); - const $ = cheerio.load(res.data); - $('#page-content .top-content .top-slide > .item') - .toArray() - .forEach((ele) => { - const $item = cheerio.load(ele); - const infoNode = $item('.slider-image > a'); - const title = infoNode.attr('title'); - const link = infoNode.attr('href'); - const thumbnail = infoNode.attr('style').replace(/background-image:url\(|\)/g, ''); - const deadlineStr = utils.getDeadlineStr($item, '.countDownCont'); - const description = [title, deadlineStr, ``].filter((str) => !!str).join('
'); - item.push({ - title, - link, - description, - }); - }); - item = item.concat(utils.parseListItem($, '.main-content')); - } else { - const res = await got(baseUrl, { - searchParams: { - action: 'fa_load_postlist', - paged: 1, - category, - }, - }); - const $ = cheerio.load(res.data); - item = utils.parseListItem($, '.ajax-load-con'); - } - - const categoryLabel = categoryToLabel[category]; - ctx.state.data = { - title: `正版中国 - ${categoryLabel}`, - description: `正版中国 - ${categoryLabel}`, - link: baseUrl, - item, - }; -}; diff --git a/lib/routes/getitfree/search.js b/lib/routes/getitfree/search.js deleted file mode 100644 index a419195cdafc95..00000000000000 --- a/lib/routes/getitfree/search.js +++ /dev/null @@ -1,25 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const qs = require('query-string'); -const utils = require('./utils'); - -module.exports = async (ctx) => { - const { keyword } = ctx.params; - const link = 'https://getitfree.cn'; - const query = { - s: keyword, - }; - const res = await got(link, { - query, - }); - const $ = cheerio.load(res.data); - - const item = utils.parseListItem($, '#page-content'); - - ctx.state.data = { - title: `正版中国搜索 - ${keyword}`, - description: `正版中国搜索 - ${keyword}`, - link: `${link}?${qs.stringify(query)}`, - item, - }; -}; diff --git a/lib/routes/getitfree/utils.js b/lib/routes/getitfree/utils.js deleted file mode 100644 index 0354ea1cb3677d..00000000000000 --- a/lib/routes/getitfree/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -const cheerio = require('cheerio'); -const dayJs = require('dayjs'); -require('dayjs/locale/zh-cn'); - -const getDeadlineStr = ($, countDownNodeSelector) => { - const countDownNode = $(countDownNodeSelector); - let deadlineStr = ''; - if (countDownNode.text().includes('活动已经结束')) { - return '截至日期: 活动已经结束'; - } - const countdownCodeStr = countDownNode.children('script').html() || ''; - const arr = /ShowCountDown\("\d*"\s*,\s*([\d,]+)\s*\)/.exec(countdownCodeStr.trim()); - if (arr) { - const dateStr = arr[1].replace(/,/g, (...args) => { - const index = args[args.length - 2]; - return index === 10 ? ' ' : index < 10 ? '-' : ':'; - }); - deadlineStr = `截至日期: ${dayJs(dateStr).locale('zh-cn').format('YYYY-MM-DD HH:mm:ss')}`; - } - return deadlineStr; -}; - -const parseListItem = ($, listSelector) => - $(`${listSelector} .content-box`) - .map((_, ele) => { - const $item = cheerio.load(ele); - const infoNode = $item('.posts-default-title > h2 > a'); - const title = infoNode.attr('title'); - const link = infoNode.attr('href'); - const pubDateStr = $item('.posts-default-info .icon-time').text(); - const pubDate = new Date(pubDateStr).toUTCString(); - const thumbnail = $item('.posts-default-img img').attr('data-original'); - const deadlineStr = getDeadlineStr($item, '.countDownCont'); - const digest = $item('.posts-default-content > .posts-text').text().trim(); - return { - title, - link, - pubDate, - description: [title, deadlineStr, digest, ``].filter((str) => !!str).join('
'), - }; - }) - .get(); - -module.exports = { - getDeadlineStr, - parseListItem, -}; diff --git a/lib/v2/getitfree/index.js b/lib/v2/getitfree/index.js index 9d9bd971ed1721..2f0097f1bcbc52 100644 --- a/lib/v2/getitfree/index.js +++ b/lib/v2/getitfree/index.js @@ -2,53 +2,61 @@ const got = require('@/utils/got'); const cheerio = require('cheerio'); const { parseDate } = require('@/utils/parse-date'); +const { + apiSlug, + rootUrl, + + bakeFilterSearchParams, + bakeFiltersWithPair, + bakeUrl, + fetchData, + getFilterNameForTitle, + getFilterParamsForUrl, + parseFilterStr, +} = require('./util'); + module.exports = async (ctx) => { - const category = ctx.params.category ?? ''; + const { filter } = ctx.params; + const limit = ctx.query.limit ? parseInt(ctx.query.limit, 10) : 50; - const rootUrl = 'https://www.getitfree.cn'; - const currentUrl = `${rootUrl}${category ? `/category/${category}` : ''}/page/1`; + const filters = parseFilterStr(filter); + const filtersWithPair = await bakeFiltersWithPair(filters); - const response = await got({ - method: 'get', - url: currentUrl, - }); + const searchParams = bakeFilterSearchParams(filters, 'name', false); + const apiSearchParams = bakeFilterSearchParams(filtersWithPair, 'id', true); - const $ = cheerio.load(response.data); + apiSearchParams.append('_embed', 'true'); + apiSearchParams.append('per_page', limit); - let items = $('a.post-title') - .toArray() - .map((item) => { - item = $(item); + const apiUrl = bakeUrl(`${apiSlug}/posts`, rootUrl, apiSearchParams); + const currentUrl = bakeUrl(getFilterParamsForUrl(filtersWithPair) ?? '', rootUrl, searchParams); - return { - title: item.text(), - link: item.attr('href'), - }; - }); + const { data: response } = await got(apiUrl); - items = await Promise.all( - items.map((item) => - ctx.cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); + const items = (Array.isArray(response) ? response : JSON.parse(response.match(/(\[.*\])$/)[1])).slice(0, limit).map((item) => { + const terminologies = item._embedded['wp:term']; - const content = cheerio.load(detailResponse.data); + const content = cheerio.load(item.content?.rendered ?? item.content); - content('h3, footer').remove(); + content('div.mycred-sell-this-wrapper').prevUntil('hr').nextAll().remove(); - item.description = content('.post-content-text').html(); - item.pubDate = parseDate(content('time.entry-date').first().attr('datetime')); + return { + title: item.title?.rendered ?? item.title, + link: item.link, + description: content.html(), + author: item._embedded.author.map((a) => a.name).join('/'), + category: [...new Set([].concat(...terminologies).map((c) => c.name))], + guid: item.guid?.rendered ?? item.guid, + pubDate: parseDate(item.date_gmt), + updated: parseDate(item.modified_gmt), + }; + }); - return item; - }) - ) - ); + const subtitle = getFilterNameForTitle(filtersWithPair); ctx.state.data = { - title: $('title').text(), - link: currentUrl, + ...(await fetchData(currentUrl)), item: items, + title: `Getitfree${subtitle ? ` | ${subtitle}` : ''}`, }; }; diff --git a/lib/v2/getitfree/maintainer.js b/lib/v2/getitfree/maintainer.js index 81eb78d7328b63..3eb26bc9ad38ce 100644 --- a/lib/v2/getitfree/maintainer.js +++ b/lib/v2/getitfree/maintainer.js @@ -1,3 +1,5 @@ module.exports = { - '/:category?': ['nczitzk'], + '/category/:id': ['sanmmm', 'nczitzk'], + '/tag/:id': ['nczitzk'], + '/search/:keyword': ['sanmmm', 'nczitzk'], }; diff --git a/lib/v2/getitfree/radar.js b/lib/v2/getitfree/radar.js index 5f46146b76751b..d8a6b5315c5bbd 100644 --- a/lib/v2/getitfree/radar.js +++ b/lib/v2/getitfree/radar.js @@ -5,8 +5,25 @@ module.exports = { { title: '分类', docs: 'https://docs.rsshub.app/routes/shopping#zheng-ban-zhong-guo-fen-lei', - source: ['/category/:category', '/'], - target: '/getitfree/:category?', + source: ['/category/:id'], + target: '/getitfree/category/:id', + }, + { + title: '标签', + docs: 'https://docs.rsshub.app/routes/shopping#zheng-ban-zhong-guo-biao-qian', + source: ['/tag/:id'], + target: '/getitfree/tag/:id', + }, + { + title: '搜索', + docs: 'https://docs.rsshub.app/routes/shopping#zheng-ban-zhong-guo-sou-suo', + source: ['/'], + target: (_, url) => { + url = new URL(url); + const keyword = url.searchParams.get('s'); + + return `/getitfree/search${keyword ? `/${keyword}` : ''}`; + }, }, ], }, diff --git a/lib/v2/getitfree/router.js b/lib/v2/getitfree/router.js index cf781366fda0a7..c84aaedb2a0785 100644 --- a/lib/v2/getitfree/router.js +++ b/lib/v2/getitfree/router.js @@ -1,3 +1,11 @@ -module.exports = function (router) { - router.get('/:category?', require('./index')); +module.exports = (router) => { + router.get('/:filter*', (ctx) => { + const { filter } = ctx.params; + + if (filter && !filter.includes('/') && !filter.includes(',')) { + ctx.redirect(`/getitfree/category/${filter}`); + } else { + return require('./')(ctx); + } + }); }; diff --git a/lib/v2/getitfree/util.js b/lib/v2/getitfree/util.js new file mode 100644 index 00000000000000..f053ceebff4f0c --- /dev/null +++ b/lib/v2/getitfree/util.js @@ -0,0 +1,326 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); + +const rootUrl = 'https://getitfree.cn'; +const apiSlug = 'wp-json/wp/v2'; + +const filterKeys = { + search: 's', +}; + +const filterApiKeys = { + category: 'categories', + tag: 'tags', + search: undefined, +}; + +const filterApiKeysWithNoId = ['search']; + +/** + * Bake filter search parameters. + * + * @param {Object} filterPairs - The filter pairs object. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + * @param {string} pairKey - The filter pair key. + * e.g. `{ id: ..., name: ..., slug: ... }`. + * @param {boolean} [isApi=false] - IIndicates if the search parameters are for API. + * @returns {URLSearchParams} The baked filter search parameters. + */ +const bakeFilterSearchParams = (filterPairs, pairKey, isApi = false) => { + /** + * Bake filters recursively. + * + * @param {Object} filterPairs - The filter pairs object. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + * @param {URLSearchParams} filterSearchParams - The filter search parameters. + * e.g. `category=a,b&tag=c`. + * @returns {URLSearchParams} The baked filter search parameters. + * e.g. `category=a,b&tag=c`. + */ + const bakeFilters = (filterPairs, filterSearchParams) => { + const keys = Object.keys(filterPairs).filter((key) => filterPairs[key]?.length > 0 && (isApi ? filterApiKeys.hasOwnProperty(key) : filterKeys.hasOwnProperty(key))); + + if (keys.length === 0) { + return filterSearchParams; + } + + const key = keys[0]; + const pairs = filterPairs[key]; + + const originalFilters = Object.assign({}, filterPairs); + delete originalFilters[key]; + + filterSearchParams.append(getFilterKeyForSearchParams(key, isApi), pairs.map((pair) => (pair.hasOwnProperty(pairKey) ? pair[pairKey] : pair)).join(',')); + + return bakeFilters(originalFilters, filterSearchParams); + }; + + return bakeFilters(filterPairs, new URLSearchParams()); +}; + +/** + * Bake filters with pair. + * + * @param {Object} filters - The filters object. + * e.g. `{ category: [ a, b ], tag: [ c ] }`. + * @returns {Promise} The baked filters. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + */ +const bakeFiltersWithPair = async (filters) => { + /** + * Bake keywords recursively. + * + * @param {string} key - The key. + * e.g. `category` or `tag`. + * @param {Array} keywords - The keywords. + * e.g. `[ a, b ]`. + * @returns {Promise>} The baked keywords. + * e.g. `[ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ]`. + */ + const bakeKeywords = async (key, keywords) => { + if (keywords.length === 0) { + return []; + } + + const [keyword, ...rest] = keywords; + + const filter = await getFilterByKeyAndKeyword(key, keyword); + + return [ + ...(filter?.id && filter?.slug + ? [ + { + id: filter.id, + name: filter.name, + slug: filter.slug, + }, + ] + : []), + ...(await bakeKeywords(key, rest)), + ]; + }; + + /** + * Bake filters recursively. + * + * @param {Object} filters - The filters object. + * e.g. `{ category: [ a, b ], tag: [ c ] }`. + * @param {Object} filtersWithPair - The filters with pairs. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + * @returns {Promise} The baked filters. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + */ + const bakeFilters = async (filters, filtersWithPair) => { + const keys = Object.keys(filters); + + if (keys.length === 0) { + return filtersWithPair; + } + + const key = keys[0]; + const keywords = filters[key]; + + const originalFilters = Object.assign({}, filters); + delete originalFilters[key]; + + return bakeFilters(originalFilters, { + ...filtersWithPair, + [key]: filterApiKeysWithNoId.includes(key) ? keywords : await bakeKeywords(key, keywords), + }); + }; + + return await bakeFilters(filters, {}); +}; + +/** + * Bake URL with search parameters. + * + * @param {string} url - The URL. + * @param {string} rootUrl - The root URL. + * @param {URLSearchParams} [searchParams=new URLSearchParams()] - The search parameters. + * @returns {string} The baked URL. + */ +const bakeUrl = (url, rootUrl, searchParams = new URLSearchParams()) => { + const searchParamsStr = String(searchParams) ? `?${searchParams}` : ''; + + return new URL(`${url}${searchParamsStr}`, rootUrl).href; +}; + +/** + * Fetch data from the specified URL. + * + * @param {string} url - The URL to fetch data from. + * @returns {Promise} A promise that resolves to an object containing the fetched data + * to be added into `ctx.state.data`. + */ +const fetchData = async (url) => { + /** + * Request URLs recursively. + * + * @param {Array} urls - The URLs to request. + * @returns {Promise} A promise that resolves to the response data + * or undefined if no response is available. + */ + const requestUrls = async (urls) => { + if (urls.length === 0) { + return undefined; + } + + const [currentUrl, ...remainingUrls] = urls; + try { + const { data: response } = await got(currentUrl); + return response; + } catch (e) { + return requestUrls(remainingUrls); + } + }; + + const response = await requestUrls([url, rootUrl]); + + if (!response) { + return {}; + } + + const $ = cheerio.load(response); + + const title = $('title').text().split(/\|/)[0]; + const image = new URL('wp-content/uploads/site_logo.png', rootUrl).href; + const icon = new URL($('link[rel="shortcut icon"]').prop('href'), rootUrl).href; + + return { + title, + link: url, + description: $('meta[name="description"]').prop('content'), + language: $('html').prop('lang'), + image, + icon, + logo: icon, + subtitle: title.split(/【/)[0], + author: $('h1.logo a').prop('title'), + allowEmpty: true, + }; +}; + +/** + * Get filter by key and keyword. + * + * @param {string} key - The key. + * e.g. `category` or `tag`. + * @param {string} keyword - The keywords. + * e.g. `keyword1`. + * @returns {Promise} A promise that resolves to the filter object if found, + * or undefined if not found. + */ +const getFilterByKeyAndKeyword = async (key, keyword) => { + const apiFilterUrl = new URL(`${apiSlug}/${getFilterKeyForSearchParams(key, true)}`, rootUrl).href; + + const { data: response } = await got(apiFilterUrl, { + searchParams: { + search: keyword, + }, + }); + + return response.length > 0 ? response[0] : undefined; +}; + +/** + * Get filter key for search parameters. + * + * @param {string} key - The key. e.g. `category` or `tag`. + * @param {boolean} [isApi=false] - Indicates whether the key is for the API. + * @returns {string|undefined} The filter key for search parameters, or undefined if not found. + * e.g. `categories` or `tags`. + */ +const getFilterKeyForSearchParams = (key, isApi = false) => { + const keys = isApi ? filterApiKeys : filterKeys; + + return keys.hasOwnProperty(key) ? keys[key] ?? key : undefined; +}; + +/** + * Get filter names for titles. + * + * @param {Object} filterPairs - The filter pairs object. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + * @returns {string} A string containing the joined filter names for titles. + * e.g. `name1,name2`. + */ +const getFilterNameForTitle = (filterPairs) => + Object.values(filterPairs) + .flat() + .map((pair) => pair?.name ?? pair?.slug ?? pair) + .filter((name) => name) + .join(','); + +/** + * Get filter parameters for URL. + * + * @param {Object} filterPairs - The filter pairs object. + * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. + * @returns {string|undefined} The filter parameters for the URL, or undefined if no filters are available. + */ +const getFilterParamsForUrl = (filterPairs) => { + const keys = Object.keys(filterPairs).filter((key) => filterPairs[key]); + + if (keys.length === 0) { + return undefined; + } + + const key = keys[0]; + + return `${key}/${filterPairs[key].map((pair) => pair.slug).join('/')}`; +}; + +/** + * Parse filter string into filters object. + * + * @param {string} filterStr - The filter string to parse. + * e.g. `category/a,b/tag/c`. + * @returns {Object} The parsed filters object. + * e.g. `{ category: [ a, b ], tag: [ c ] }`. + */ +const parseFilterStr = (filterStr) => { + /** + * Parse filter string recursively. + * + * @param {string} filterStr - The remaining filter string to parse. + * e.g. `category/a,b/tag/c`. + * @param {Object} filters - The accumulated filters object. + * e.g. `{ category: [ a, b ], tag: [ c ] }`. + * @param {string} filterKey - The current filter key. + * e.g. `category` or `tag`. + * @returns {Object} The parsed filters object. + */ + const parseStr = (filterStr, filters = {}, filterKey = undefined) => { + if (!filterStr) { + return filters; + } + + const [word, ...rest] = filterStr.split(/\/|,/); + + const isKey = filterApiKeys.hasOwnProperty(word); + const key = isKey ? word : filterKey; + + const newFilters = { + ...filters, + [key]: [...(filters[key] || []), ...(isKey ? [] : [word])], + }; + + return parseStr(rest.join('/'), newFilters, key); + }; + + return parseStr(filterStr, {}); +}; + +module.exports = { + apiSlug, + rootUrl, + + bakeFilterSearchParams, + bakeFiltersWithPair, + bakeUrl, + fetchData, + getFilterNameForTitle, + getFilterParamsForUrl, + parseFilterStr, +}; diff --git a/website/docs/routes/other.mdx b/website/docs/routes/other.mdx index 41bd2266388d5c..dedfb512784ca5 100644 --- a/website/docs/routes/other.mdx +++ b/website/docs/routes/other.mdx @@ -1239,24 +1239,6 @@ Refer to [the list of supported currencies](https://wise.com/tools/exchange-rate -## 正版中国 {#zheng-ban-zhong-guo} - -### 分类列表 {#zheng-ban-zhong-guo-fen-lei-lie-biao} - - - -类型 - -| 全部文章 | 永久免费 | 限时折扣 | 限时免费 | PC | Mac | Android | UWP | -|----------|----------|----------|----------|----|-----|---------|-----| -| all | 311 | 309 | 310 | 8 | 50 | 17 | 312 | - - - -### 搜索 {#zheng-ban-zhong-guo-sou-suo} - - - ## 智联招聘 {#zhi-lian-zhao-pin} ### 搜索 {#zhi-lian-zhao-pin-sou-suo} diff --git a/website/docs/routes/shopping.mdx b/website/docs/routes/shopping.mdx index 25729d350c7f6c..6c23204c0c93d3 100644 --- a/website/docs/routes/shopping.mdx +++ b/website/docs/routes/shopping.mdx @@ -711,11 +711,77 @@ For instance, in `https://www.zagg.com/en_us/new-arrivals?brand=164&cat=3038%2C3 ### 分类 {#zheng-ban-zhong-guo-fen-lei} - + -| 所有类别 | Android | iOS | Mac | PC | UWP | 公告 | 永久免费 | 限时免费 | 限时折扣 | -| -------- | ------- | --- | --- | -- | --- | ------------ | -------- | -------- | -------- | -| | android | ios | mac | pc | uwp | notification | free | giveaway | discount | +:::tip + +可以叠加使用得到分类结果并集,如 [`/getitfree/category/pc,android`](https://rsshub.app/getitfree/category/pc,android) + +::: + +| 所有类别 | Android | iOS | Mac | PC | UWP | 公告 | 永久免费 | 限时免费 | 正版折扣 | +| -------- | ------- | --- | --- | --- | --- | ------------ | -------- | -------- | -------- | +| | android | ios | mac | pc | uwp | notification | free | giveaway | discount | + + + +### 标签 {#zheng-ban-zhong-guo-biao-qian} + + + +:::tip + +可以叠加使用得到标签结果并集,如 [`/getitfree/tag/ai,office`](https://rsshub.app/getitfree/tag/ai,office) + +::: + +| AI | Android | DVD | GIF | iCloud | +| --- | ------- | --- | --- | ------ | + +| ICON | iOS | iPhone | Leawo | Mac | +| ---- | --- | ------ | ----- | --- | + +| Markdown | OCR | Office | Office 办公 | PDF | +| -------- | --- | ------ | ----------- | --- | + +| PDF 转换 | Windows | 下载 | 书签管理 | 健康 | +| -------- | ------- | ---- | -------- | ---- | + +| 全平台 | 办公软件 | 加密保护 | 卸载/清理/优化 | 卸载清理 | +| ------ | -------- | -------- | -------------- | -------- | + +| 图像处理 | 图片/编辑/管理 | 壁纸 | 备份/恢复/加密 | 备份恢复 | +| -------- | -------------- | ---- | -------------- | -------- | + +| 字体 | 学习 | 密码管理 | 导航 | 开始菜单 | +| ---- | ---- | -------- | ---- | -------- | + +| 录屏截图 | 影音/刻录/转换 | 投屏镜像 | 提醒 | 摄影 | +| -------- | -------------- | -------- | ---- | ---- | + +| 播放器 | 收藏夹 | 效率工具 | 数据传输 | 数据恢复 | +| ------ | ------ | -------- | -------- | -------- | + +| 数据擦除 | 文件管理 | 文档/转换/压缩 | 文档扫描 | 日历 | +| -------- | -------- | -------------- | -------- | ---- | + +| 杀毒防护 | 查重 | 正版特惠 | 水印 | 清理优化 | +| -------- | ---- | -------- | ---- | -------- | + +| 滤镜 | 硬件外设 | 硬盘/分区/驱动 | 硬盘检测 | 稍后阅读 | +| ---- | -------- | -------------- | -------- | -------- | + +| 笔记 | 系统工具 | 素材 | 网络/安全/传输 | 网络安全 | +| ---- | -------- | ---- | -------------- | -------- | + +| 色彩 | 视频剪辑 | 视频转换 | 记账 | 设计 | +| ---- | -------- | -------- | ---- | ---- | + +| 软件卸载 | 远程控制 | 音乐 | 音频 | 驱动 | +| -------- | -------- | ---- | ---- | ---- | +### 搜索 {#zheng-ban-zhong-guo-sou-suo} + + \ No newline at end of file