From 48118f59d0884c653aeef07f6a5bd2f8062fb261 Mon Sep 17 00:00:00 2001 From: magazian Date: Sun, 14 Jun 2026 15:54:09 +0800 Subject: [PATCH 1/4] feat(route): add Nanjing Museum exhibition route --- lib/routes/njmuseum/exhibitionindex.tsx | 169 ++++++++++++++++++++++++ lib/routes/njmuseum/namespace.ts | 10 ++ 2 files changed, 179 insertions(+) create mode 100644 lib/routes/njmuseum/exhibitionindex.tsx create mode 100644 lib/routes/njmuseum/namespace.ts diff --git a/lib/routes/njmuseum/exhibitionindex.tsx b/lib/routes/njmuseum/exhibitionindex.tsx new file mode 100644 index 000000000000..f4f2c290c8ee --- /dev/null +++ b/lib/routes/njmuseum/exhibitionindex.tsx @@ -0,0 +1,169 @@ +import { renderToString } from 'hono/jsx/dom/server'; + +import type { Route } from '@/types'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +import { namespace } from './namespace'; + +// format the date to YYYY-MM-DD and handle missing year or month +const extractDates = (durationStr: string) => { + let startDate: string | undefined; + let endDate: string | undefined; + + if (!durationStr) { + return { startDate, endDate }; + } + + const parts = durationStr.split(/-+/).map((p) => p.trim()); // currently - is used + const startStr = parts[0]; + const endStr = parts[1]; + + let startYear: string | undefined; + let startMonth: string | undefined; + + const startRegex = /(\d{4})[年.](\d{1,2})(?:[月.](\d{1,2}))?/; // matches formats like "2024年5月10日", "2024.5.10" + const startMatch = startStr.match(startRegex); + + if (startMatch) { + startYear = startMatch[1]; + startMonth = startMatch[2].padStart(2, '0'); + const startDay = startMatch[3] ? startMatch[3].padStart(2, '0') : '01'; // use 1st day of month if day is missing + startDate = `${startYear}-${startMonth}-${startDay}`; + } + + if (endStr && startDate) { + const endRegex = /(?:(\d{4})[年.])?(\d{1,2})(?:[月.](\d{1,2}))?/; + const endMatch = endStr.match(endRegex); + + if (endMatch) { + const matchYear = endMatch[1]; + const matchMonth: string | undefined = endMatch[2]; + const matchDay: string | undefined = endMatch[3]; + + const finalEndYear = matchYear || startYear; + const finalEndMonth = matchMonth ? matchMonth.padStart(2, '0') : startMonth; + const finalEndDay = matchDay ? matchDay.padStart(2, '0') : '01'; + endDate = `${finalEndYear}-${finalEndMonth}-${finalEndDay}`; + } + } + + return { startDate, endDate }; +}; + +interface ExhibitType { + modular: string; + code: string; + value: string; + pageSize?: number; + pageNum?: number; + isList?: boolean; +} + +const TYPE_MAP: Record = { + review: { modular: 'exIndexReview', code: 'properties', value: '5' }, + abroad: { modular: 'exIndexGoAbroad', code: 'properties', value: '7' }, + virtual: { modular: 'exIndexVirtual', code: 'properties', value: '11' }, + forecast: { modular: 'exIndexForecast', code: 'properties', value: '1' }, + default: { modular: 'exIndex', code: '', value: '0', pageSize: 6, pageNum: 1, isList: true }, +}; + +export const route: Route = { + path: '/exhibitionIndex/:type?', + categories: ['travel'], + example: '/njmuseum/exhibitionIndex/review', + parameters: { + type: 'Exhibition type, supported values: review (展览回顾) | abroad (赴外展览) | virtual (虚拟展厅) | forecast (展览预告). Default: Current Exhibitions (正在展出).', + }, + name: 'Exhibitions', + maintainers: ['magazian'], + radar: [ + { + source: ['www.njmuseum.com/zh/exhibitionIndex'], + target: '/exhibitionIndex', + }, + ], + handler: async (ctx) => { + const typeParam = ctx.req.param('type') || 'default'; + const currentType = TYPE_MAP[typeParam]; + + const baseUrl = 'https://www.njmuseum.com'; + const resourceUrl = 'https://manage.njmuseum.org.cn'; // use for imgUrl + + const apiUrl = `${baseUrl}/api/exhibition/list`; + + const museumName = namespace.zh?.name || namespace.name; + + const fetchExhibits = async (config: ExhibitType) => { + const response = await got.post(apiUrl, { + form: config, + }); + + const data = response.data?.data; + + return { + list: data.list as Array>, + title: data.title as string, + }; + }; + + const { list: rawItems, title: fetchedTitle } = await fetchExhibits(currentType); + const titleTag = fetchedTitle; + + const items = rawItems.map((item) => { + const title = item.title; + const itemLink = `${baseUrl}/zh/${item.link}?id=${item.id}`; + const imgPath = item.imgSrc?.[0]; + const imgUrl = `${resourceUrl}${imgPath}`; + const location = item.position; + const fullDuration = item.timedesc; + const { startDate, endDate } = extractDates(fullDuration); + const pubDate = startDate ? parseDate(startDate) : undefined; // use start date as publication date if publication date is notavailable + + const description = renderToString( +
+ +
+

+ 地点: + {location} +

+

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

+

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

+

+ 原始展期:{fullDuration} +

+
+ ); + + return { + title, + link: itemLink, + pubDate, + description, + // For further .ics file processing + _extra: { + museumName, + title, + location, + startDate: startDate || '未定/常设', // format: YYYY-MM-DD or '未定/常设' + endDate: endDate || '未定/常设', // format: YYYY-MM-DD or '未定/常设' + itemLink, + }, + }; + }); + + return { + title: `${museumName} - 展览 - ${titleTag}`, + link: `${baseUrl}/zh/exhibitionIndex`, + language: 'zh-CN', + item: items, + }; + }, +}; diff --git a/lib/routes/njmuseum/namespace.ts b/lib/routes/njmuseum/namespace.ts new file mode 100644 index 000000000000..3b881819392d --- /dev/null +++ b/lib/routes/njmuseum/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Nanjing Museum', + url: 'www.njmuseum.com/zh', + + zh: { + name: '南京博物院', + }, +}; From 8eaf153b3ae0642ab138148e3a7586c9ff4f6a19 Mon Sep 17 00:00:00 2001 From: magazian <16831220+magazian@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:10:58 +0800 Subject: [PATCH 2/4] fix: update the startDate and endDate --- lib/routes/njmuseum/exhibitionindex.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/routes/njmuseum/exhibitionindex.tsx b/lib/routes/njmuseum/exhibitionindex.tsx index f4f2c290c8ee..a71be2de615b 100644 --- a/lib/routes/njmuseum/exhibitionindex.tsx +++ b/lib/routes/njmuseum/exhibitionindex.tsx @@ -126,7 +126,7 @@ export const route: Route = {

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

开展: @@ -150,11 +150,9 @@ export const route: Route = { // For further .ics file processing _extra: { museumName, - title, location, - startDate: startDate || '未定/常设', // format: YYYY-MM-DD or '未定/常设' - endDate: endDate || '未定/常设', // format: YYYY-MM-DD or '未定/常设' - itemLink, + startDate, + endDate, }, }; }); From e1dcff0cab94b16e7b039aaf82760f5db4e292ba Mon Sep 17 00:00:00 2001 From: magazian <16831220+magazian@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:54:14 +0800 Subject: [PATCH 3/4] fix: remove redundant fetchTitle --- lib/routes/njmuseum/exhibitionindex.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/routes/njmuseum/exhibitionindex.tsx b/lib/routes/njmuseum/exhibitionindex.tsx index a71be2de615b..7d88ef1cdff5 100644 --- a/lib/routes/njmuseum/exhibitionindex.tsx +++ b/lib/routes/njmuseum/exhibitionindex.tsx @@ -107,8 +107,7 @@ export const route: Route = { }; }; - const { list: rawItems, title: fetchedTitle } = await fetchExhibits(currentType); - const titleTag = fetchedTitle; + const { list: rawItems, title: titleTag } = await fetchExhibits(currentType); const items = rawItems.map((item) => { const title = item.title; From 908a2aa76653dd579078452d560547c3e619a346 Mon Sep 17 00:00:00 2001 From: Jiamin <16831220+magazian@users.noreply.github.com> Date: Sun, 5 Jul 2026 13:51:23 +0800 Subject: [PATCH 4/4] Update lib/routes/njmuseum/exhibitionindex.tsx Co-authored-by: Tony --- lib/routes/njmuseum/exhibitionindex.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routes/njmuseum/exhibitionindex.tsx b/lib/routes/njmuseum/exhibitionindex.tsx index 7d88ef1cdff5..814474d18df3 100644 --- a/lib/routes/njmuseum/exhibitionindex.tsx +++ b/lib/routes/njmuseum/exhibitionindex.tsx @@ -109,7 +109,7 @@ export const route: Route = { const { list: rawItems, title: titleTag } = await fetchExhibits(currentType); - const items = rawItems.map((item) => { + const items = rawItems?.map((item) => { const title = item.title; const itemLink = `${baseUrl}/zh/${item.link}?id=${item.id}`; const imgPath = item.imgSrc?.[0];