diff --git a/lib/routes/njmuseum/exhibitionindex.tsx b/lib/routes/njmuseum/exhibitionindex.tsx new file mode 100644 index 000000000000..814474d18df3 --- /dev/null +++ b/lib/routes/njmuseum/exhibitionindex.tsx @@ -0,0 +1,166 @@ +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: titleTag } = await fetchExhibits(currentType); + + 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, + location, + startDate, + endDate, + }, + }; + }); + + 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: '南京博物院', + }, +};