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
This commit is contained in:
Jiamin
2026-08-22 11:56:56 +08:00
committed by GitHub
parent 3646809d06
commit 7f958db83e
3 changed files with 209 additions and 0 deletions
@@ -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<Data> => {
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(
<div>
<img src={item.imgUrl} />
<br />
<p>
<b></b>
{location || '参考详情'}
</p>
<p>
<b></b>
{startDate || '未定/常设'}
</p>
<p>
<b></b>
{endDate || '未定/常设'}
</p>
{fullDuration && (
<p>
<small>{fullDuration}</small>
</p>
)}
</div>
);
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,
};
},
};
+10
View File
@@ -0,0 +1,10 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'China Grand Canal Museum',
url: 'www.grandcanalmuseum.cn',
zh: {
name: '中国大运河博物馆',
},
};
@@ -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,
};
},
};