From 7f958db83eab5dc61b48e8da40ae9f742927225c Mon Sep 17 00:00:00 2001 From: Jiamin <16831220+magazian@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:56:56 +0800 Subject: [PATCH] feat(route): add Grand Canal Mus news&exhib route (#22898) * feat(route): add Grand Canal Mus news&exhib route * fix: update the parseExhibitionDuration * fix: add undefined parseExhibitionDuration --- lib/routes/grandcanalmuseum/linzhantezhan.tsx | 147 ++++++++++++++++++ lib/routes/grandcanalmuseum/namespace.ts | 10 ++ lib/routes/grandcanalmuseum/yunboxinwen.ts | 52 +++++++ 3 files changed, 209 insertions(+) create mode 100644 lib/routes/grandcanalmuseum/linzhantezhan.tsx create mode 100644 lib/routes/grandcanalmuseum/namespace.ts create mode 100644 lib/routes/grandcanalmuseum/yunboxinwen.ts diff --git a/lib/routes/grandcanalmuseum/linzhantezhan.tsx b/lib/routes/grandcanalmuseum/linzhantezhan.tsx new file mode 100644 index 0000000000..c5abee9834 --- /dev/null +++ b/lib/routes/grandcanalmuseum/linzhantezhan.tsx @@ -0,0 +1,147 @@ +import { load } from 'cheerio'; +import { renderToString } from 'hono/jsx/dom/server'; + +import type { Data, DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +import { namespace } from './namespace'; + +const formatExhibitionDate = (dateStr: string): string | undefined => { + const normalized = dateStr.replaceAll(/[年月.]/g, '-').replace('日', ''); + const [y, m, d] = normalized.split('-', 3); + if (!y || !m || !d) { + return undefined; + } + return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`; +}; + +// parse date formate for 2026年1月1日—2026年4月6日, 2025.09.30-2026.01.04 or 2025年6月17日-8月17日 +const parseExhibitionDuration = (fullDuration: string): { startDate: string | undefined; endDate: string | undefined } => { + if (!fullDuration) { + return { startDate: undefined, endDate: undefined }; + } + + const parts = fullDuration.split(/[—-]/); + if (parts.length < 2) { + return { startDate: undefined, endDate: undefined }; + } + + const startRaw = parts[0].trim(); + let endRaw = parts[1].trim(); + + const startDate = formatExhibitionDate(startRaw); + + if (startDate && !/\d{4}/.test(endRaw)) { + const startYear = startDate.slice(0, 4); + endRaw = `${startYear}年${endRaw}`; + } + + const endDate = formatExhibitionDate(endRaw); + + return { startDate, endDate }; +}; + +export const route: Route = { + path: '/linzhantezhan', + categories: ['travel'], + example: '/grandcanalmuseum/linzhantezhan', + radar: [ + { + source: ['www.grandcanalmuseum.cn/linzhantezhan.html'], + target: '/linzhantezhan', + }, + ], + name: '临展特展', + maintainers: ['magazian'], + handler: async (): Promise => { + const museumName = namespace.zh?.name || namespace.name; + const baseUrl = 'https://www.grandcanalmuseum.cn'; + const listUrl = `${baseUrl}/linzhantezhan.html`; + const response = await ofetch(listUrl); + const $ = load(response); + + const rawItems = $('.list_zhanlan .list > ul > li > a') + .toArray() + .map((el) => { + const $a = $(el); + const href = $a.attr('href')!; + const link = new URL(href, baseUrl).href; + const title = $a.find('.title').text(); + const imgUrl = $a.find('.img img').attr('src'); + return { title, link, imgUrl }; + }); + + const items = await Promise.all( + rawItems.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await ofetch(item.link); + const $d = load(detailResponse); + + // Extract location from the structured lanmu bar + const locationSpan = $d('.article .lanmu span') + .toArray() + .map((el) => $d(el).text().trim()) + .find((text) => text.startsWith('地点:')); + const location = locationSpan?.replace('地点:', ''); + + let fullDuration = ''; + $d('.zhengwen p').each((_, el) => { + const text = $d(el).text().trim(); + if (text.includes('展览时间:')) { + fullDuration = text.split('展览时间:', 2)[1]; + } + }); + + const { startDate, endDate } = parseExhibitionDuration(fullDuration); + const pubDate = startDate ? parseDate(startDate) : undefined; + + const description = renderToString( +
+ +
+

+ 地点: + {location || '参考详情'} +

+

+ 开展: + {startDate || '未定/常设'} +

+

+ 闭展: + {endDate || '未定/常设'} +

+ {fullDuration && ( +

+ 原始展期:{fullDuration} +

+ )} +
+ ); + + return { + title: item.title, + link: item.link, + pubDate, + description, + _extra: { + museumName, + location, + startDate, + endDate, + }, + } as DataItem; + }) + ) + ); + + return { + title: `${museumName} - 临展特展`, + link: listUrl, + language: 'zh-CN', + item: items, + }; + }, +}; diff --git a/lib/routes/grandcanalmuseum/namespace.ts b/lib/routes/grandcanalmuseum/namespace.ts new file mode 100644 index 0000000000..a24bc6a6f3 --- /dev/null +++ b/lib/routes/grandcanalmuseum/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'China Grand Canal Museum', + url: 'www.grandcanalmuseum.cn', + + zh: { + name: '中国大运河博物馆', + }, +}; diff --git a/lib/routes/grandcanalmuseum/yunboxinwen.ts b/lib/routes/grandcanalmuseum/yunboxinwen.ts new file mode 100644 index 0000000000..6d7cf0ae64 --- /dev/null +++ b/lib/routes/grandcanalmuseum/yunboxinwen.ts @@ -0,0 +1,52 @@ +import { load } from 'cheerio'; + +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +import { namespace } from './namespace'; + +export const route: Route = { + path: '/yunboxinwen', + categories: ['travel'], + example: '/grandcanalmuseum/yunboxinwen', + radar: [ + { + source: ['www.grandcanalmuseum.cn/yunboxinwen.html'], + target: '/yunboxinwen', + }, + ], + name: '运博新闻', + maintainers: ['magazian'], + handler: async () => { + const baseUrl = 'https://www.grandcanalmuseum.cn'; + const listUrl = `${baseUrl}/yunboxinwen.html`; + const response = await ofetch(listUrl); + const $ = load(response); + + const items = $('.list_news > ul > a') + .toArray() + .map((el) => { + const $a = $(el); + const $li = $a.find('li'); + const title = $li.find('.title').text(); + const dateText = $li.find('.time').text(); + const pubDate = parseDate(dateText, 'YYYY-MM-DD'); + const href = $a.attr('href')!; + const link = new URL(href, baseUrl).href; + + return { + title, + link, + pubDate, + }; + }); + + return { + title: `${namespace.zh?.name || namespace.name} - 运博新闻`, + link: listUrl, + language: 'zh-CN', + item: items, + }; + }, +};