From baa7c9e265c234785f882b7d617a531f626679be Mon Sep 17 00:00:00 2001 From: Contributor Date: Fri, 31 Jul 2026 09:27:57 +0800 Subject: [PATCH] fix: resolve #99 --- components/Navigator/MainNavigator.tsx | 1 + models/Wuhan2020.ts | 338 +++++++++++++++++++++++++ pages/wuhan2020.tsx | 272 ++++++++++++++++++++ styles/Wuhan2020.module.less | 172 +++++++++++++ translation/en-US.ts | 29 +++ translation/zh-CN.ts | 28 ++ translation/zh-TW.ts | 28 ++ 7 files changed, 868 insertions(+) create mode 100644 models/Wuhan2020.ts create mode 100644 pages/wuhan2020.tsx create mode 100644 styles/Wuhan2020.module.less diff --git a/components/Navigator/MainNavigator.tsx b/components/Navigator/MainNavigator.tsx index 25350fa..2e7b507 100644 --- a/components/Navigator/MainNavigator.tsx +++ b/components/Navigator/MainNavigator.tsx @@ -63,6 +63,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [ ], }, { href: '/bounty', title: t('bounty') }, + { href: '/wuhan2020', title: t('wuhan2020') }, { title: t('NGO'), subs: [ diff --git a/models/Wuhan2020.ts b/models/Wuhan2020.ts new file mode 100644 index 0000000..101a7e5 --- /dev/null +++ b/models/Wuhan2020.ts @@ -0,0 +1,338 @@ +/** + * WuHan-2020 新冠救援信息平台 — 归档数据模型 + * + * 数据来源:https://github.com/nicedoc/wuhan2020 + * 最后更新时间:2020 年春季 + */ + +export type RescueCategory = + | 'hospital' + | 'hotel' + | 'logistics' + | 'factory' + | 'donation' + | 'consulting'; + +export interface RescueRecord { + id: string; + category: RescueCategory; + name: string; + city: string; + province: string; + address?: string; + phone?: string; + description: string; + url?: string; + verified?: boolean; + /** ISO date string */ + createdAt?: string; +} + +export interface RescueCategoryMeta { + key: RescueCategory; + icon: string; + count: number; +} + +// ─── Sample archival data (historical snapshot from early 2020) ─────────────── + +export const rescueRecords: RescueRecord[] = [ + // ── 定点医院 ── + { + id: 'h1', + category: 'hospital', + name: '武汉市金银潭医院', + city: '武汉市', + province: '湖北省', + address: '武汉市东西湖区银潭路1号', + phone: '027-85509106', + description: '武汉市传染病定点收治医院,最早收治新冠肺炎患者的医院之一。', + verified: true, + createdAt: '2020-01-20', + }, + { + id: 'h2', + category: 'hospital', + name: '火神山医院', + city: '武汉市', + province: '湖北省', + address: '武汉市蔡甸区知音湖大道', + description: '参照北京小汤山医院模式建设的专门医院,2020年2月2日建成投入使用,设置床位1000张。', + verified: true, + createdAt: '2020-02-02', + }, + { + id: 'h3', + category: 'hospital', + name: '雷神山医院', + city: '武汉市', + province: '湖北省', + address: '武汉市江夏区黄家湖畔', + description: '参照火神山医院模式建设,2020年2月8日投入使用,设置床位1600张。', + verified: true, + createdAt: '2020-02-08', + }, + { + id: 'h4', + category: 'hospital', + name: '武汉大学中南医院', + city: '武汉市', + province: '湖北省', + address: '武汉市武昌区东湖路169号', + phone: '027-67812888', + description: '大型综合性三甲医院,承担新冠肺炎重症患者救治任务。', + verified: true, + createdAt: '2020-01-22', + }, + { + id: 'h5', + category: 'hospital', + name: '武汉市中心医院', + city: '武汉市', + province: '湖北省', + address: '武汉市江岸区胜利街26号', + phone: '027-82811080', + description: '新冠肺炎定点收治医院,该院医生李文亮在疫情初期发出预警。', + verified: true, + createdAt: '2020-01-21', + }, + + // ── 酒店住宿 ── + { + id: 'o1', + category: 'hotel', + name: '武汉洲际酒店', + city: '武汉市', + province: '湖北省', + address: '武汉市汉阳区晴川大道666号', + phone: '027-84888888', + description: '免费为医疗救援人员提供住宿。', + verified: true, + createdAt: '2020-01-26', + }, + { + id: 'o2', + category: 'hotel', + name: '武汉光谷希尔顿酒店', + city: '武汉市', + province: '湖北省', + address: '武汉市东湖新技术开发区花山生态新城', + phone: '027-59338888', + description: '为赴鄂医疗队提供免费住宿服务。', + verified: true, + createdAt: '2020-01-28', + }, + { + id: 'o3', + category: 'hotel', + name: '如家酒店(武汉多家门店)', + city: '武汉市', + province: '湖北省', + description: '全国3000余家如家酒店为医护人员提供免费住宿。', + verified: true, + createdAt: '2020-01-27', + }, + { + id: 'o4', + category: 'hotel', + name: '汉庭酒店武汉区域门店', + city: '武汉市', + province: '湖北省', + description: '武汉地区所有汉庭酒店为医疗人员提供免费入住。', + verified: true, + createdAt: '2020-01-27', + }, + + // ── 物流运输 ── + { + id: 'l1', + category: 'logistics', + name: '顺丰速运', + city: '全国', + province: '全国', + description: '开通全国至武汉救援物资免费运输绿色通道,不限运输次数。', + url: 'https://www.sf-express.com', + verified: true, + createdAt: '2020-01-25', + }, + { + id: 'l2', + category: 'logistics', + name: '京东物流', + city: '全国', + province: '全国', + description: '向武汉地区捐赠大量医疗物资,并开通绿色通道免费承运救援物资。', + url: 'https://www.jdl.com', + verified: true, + createdAt: '2020-01-25', + }, + { + id: 'l3', + category: 'logistics', + name: '中通快递', + city: '全国', + province: '全国', + description: '开通全国至武汉的绿色通道,免费运输救援物资。', + verified: true, + createdAt: '2020-01-27', + }, + { + id: 'l4', + category: 'logistics', + name: '菜鸟网络', + city: '全国', + province: '全国', + description: '联合全球物流合作伙伴开通绿色通道,免费将救援物资运抵武汉。', + verified: true, + createdAt: '2020-01-25', + }, + { + id: 'l5', + category: 'logistics', + name: '满帮集团', + city: '全国', + province: '全国', + description: '开通免费运输通道,组织运力支援武汉物资运输。', + verified: true, + createdAt: '2020-01-28', + }, + + // ── 物资生产工厂 ── + { + id: 'f1', + category: 'factory', + name: '稳健医疗', + city: '武汉', + province: '湖北省', + description: '全力生产口罩、防护服等医疗防护物资,春节期间不停工。', + verified: true, + createdAt: '2020-01-23', + }, + { + id: 'f2', + category: 'factory', + name: '3M 中国', + city: '上海', + province: '上海市', + description: '加大口罩生产力度,全力保障防疫物资供应。', + verified: true, + createdAt: '2020-01-24', + }, + { + id: 'f3', + category: 'factory', + name: '振德医疗', + city: '绍兴', + province: '浙江省', + description: '紧急调配医疗物资,全力生产口罩和医用敷料供应疫区。', + verified: true, + createdAt: '2020-01-24', + }, + + // ── 捐赠信息 ── + { + id: 'd1', + category: 'donation', + name: '武汉市红十字会', + city: '武汉市', + province: '湖北省', + address: '武汉市江岸区胜利街1号', + phone: '027-82785900', + description: '接受社会各界捐赠的疫情防控物资和资金。', + verified: true, + createdAt: '2020-01-23', + }, + { + id: 'd2', + category: 'donation', + name: '湖北省慈善总会', + city: '武汉市', + province: '湖北省', + phone: '027-87433333', + description: '接受社会捐赠,用于湖北省新冠肺炎疫情防控工作。', + verified: true, + createdAt: '2020-01-24', + }, + { + id: 'd3', + category: 'donation', + name: '韩红爱心慈善基金会', + city: '北京', + province: '北京市', + description: '通过韩红爱心慈善基金会进行定向捐赠,支援武汉医疗物资。', + url: 'https://www.hhax.org', + verified: true, + createdAt: '2020-01-25', + }, + + // ── 免费咨询 ── + { + id: 'c1', + category: 'consulting', + name: '丁香医生在线问诊', + city: '全国', + province: '全国', + description: '提供免费在线问诊服务,7x24小时医生在线咨询。', + url: 'https://dxy.com', + verified: true, + createdAt: '2020-01-24', + }, + { + id: 'c2', + category: 'consulting', + name: '阿里健康在线义诊', + city: '全国', + province: '全国', + description: '在支付宝上线在线义诊服务,提供免费医生问诊。', + url: 'https://www.alihealth.cn', + verified: true, + createdAt: '2020-01-24', + }, + { + id: 'c3', + category: 'consulting', + name: '微医互联网总医院', + city: '全国', + province: '全国', + description: '提供免费在线义诊服务,汇集全国2万余名医生。', + url: 'https://www.guahao.com', + verified: true, + createdAt: '2020-01-23', + }, +]; + +export const categoryMeta: RescueCategoryMeta[] = [ + { key: 'hospital', icon: '🏥', count: 5 }, + { key: 'hotel', icon: '🏨', count: 4 }, + { key: 'logistics', icon: '🚚', count: 5 }, + { key: 'factory', icon: '🏭', count: 3 }, + { key: 'donation', icon: '💝', count: 3 }, + { key: 'consulting', icon: '🩺', count: 3 }, +]; + +export const categoryLabelMap: Record = { + hospital: 'hospital', + hotel: 'hotel', + logistics: 'logistics', + factory: 'factory', + donation: 'donation', + consulting: 'consulting', +}; + +/** Get records filtered by category (or all if omitted) */ +export function getRecords(category?: RescueCategory) { + return category ? rescueRecords.filter(r => r.category === category) : rescueRecords; +} + +/** Get unique provinces from records */ +export function getProvinces() { + return [...new Set(rescueRecords.map(r => r.province))].sort(); +} + +/** Get stats per category */ +export function getCategoryStats() { + return categoryMeta.map(meta => ({ + ...meta, + count: rescueRecords.filter(r => r.category === meta.key).length, + })); +} diff --git a/pages/wuhan2020.tsx b/pages/wuhan2020.tsx new file mode 100644 index 0000000..5cb0871 --- /dev/null +++ b/pages/wuhan2020.tsx @@ -0,0 +1,272 @@ +import { observer } from 'mobx-react'; +import { FC, useContext, useState } from 'react'; +import { Badge, Card, Col, Container, Row } from 'react-bootstrap'; + +import { PageHead } from '../components/Layout/PageHead'; +import { SectionTitle } from '../components/Layout/SectionTitle'; +import { + categoryMeta, + getCategoryStats, + getProvinces, + RescueCategory, + rescueRecords, + RescueRecord, +} from '../models/Wuhan2020'; +import { I18nContext } from '../models/Translation'; +import styles from '../styles/Wuhan2020.module.less'; + +const categoryColor: Record = { + hospital: '#c62828', + hotel: '#1565c0', + logistics: '#2e7d32', + factory: '#e65100', + donation: '#6a1b9a', + consulting: '#00838f', +}; + +const RecordCard: FC<{ record: RescueRecord }> = ({ record }) => { + const { t } = useContext(I18nContext); + + return ( + + +
+ + {record.name} + + {record.verified && ( + {t('wuhan2020_verified')} + )} +
+ + + {t(`wuhan2020_cat_${record.category}`)} + + + {record.description} + +
+
+ 📍 + + {record.province} + {record.city !== record.province ? ` · ${record.city}` : ''} + {record.address ? ` · ${record.address}` : ''} + +
+ {record.phone && ( +
+ 📞 + {record.phone} +
+ )} + {record.url && ( + + )} + {record.createdAt && ( +
+ 📅 + {record.createdAt} +
+ )} +
+
+
+ ); +}; + +const categoryIcons: Record = { + hospital: '🏥', + hotel: '🏨', + logistics: '🚚', + factory: '🏭', + donation: '💝', + consulting: '🩺', +}; + +const Wuhan2020Page: FC = observer(() => { + const { t } = useContext(I18nContext); + const [activeCategory, setActiveCategory] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + + const stats = getCategoryStats(); + const provinces = getProvinces(); + + const filteredRecords = rescueRecords.filter(r => { + const matchCategory = activeCategory ? r.category === activeCategory : true; + const matchSearch = searchTerm + ? r.name.includes(searchTerm) || + r.description.includes(searchTerm) || + r.city.includes(searchTerm) || + r.province.includes(searchTerm) + : true; + return matchCategory && matchSearch; + }); + + return ( + <> + + + {/* Hero */} +
+ +

{t('wuhan2020_title')}

+

{t('wuhan2020_description')}

+ +
+ 📅 2020.01 — 2020.04 + 📊 {rescueRecords.length} {t('wuhan2020_records')} + 🌍 {provinces.length} {t('wuhan2020_regions')} +
+
+
+ + {/* Category Stats */} + + + {stats.map(({ key, icon, count }) => ( + + setActiveCategory(activeCategory === key ? null : key)} + > +
{icon}
+
{count}
+
{t(`wuhan2020_cat_${key}`)}
+
+ + ))} +
+
+ + {/* Records */} + +
+ + {activeCategory + ? `${categoryIcons[activeCategory]} ${t(`wuhan2020_cat_${activeCategory}`)}` + : t('wuhan2020_all_records')} + + + {/* Search */} +
+ 🔍 + setSearchTerm(e.target.value)} + style={{ maxWidth: 220 }} + /> + {searchTerm && ( + + )} +
+
+ + {/* Filter buttons */} +
+ + {categoryMeta.map(({ key, icon }) => ( + + ))} +
+ + {filteredRecords.length === 0 ? ( +
+

📭 {t('wuhan2020_no_results')}

+ +
+ ) : ( + + {filteredRecords.map(record => ( + + + + ))} + + )} +
+ + {/* Timeline / Data Source */} +
+ +

{t('wuhan2020_timeline_title')}

+ + +
+ {[ + { date: '2020-01-20', event: t('wuhan2020_timeline_1') }, + { date: '2020-01-23', event: t('wuhan2020_timeline_2') }, + { date: '2020-01-25', event: t('wuhan2020_timeline_3') }, + { date: '2020-02-02', event: t('wuhan2020_timeline_4') }, + { date: '2020-02-08', event: t('wuhan2020_timeline_5') }, + { date: '2020-04-08', event: t('wuhan2020_timeline_6') }, + ].map(({ date, event }) => ( +
+
+
+ {date} +

{event}

+
+
+ ))} +
+ + + +
+

+ {t('wuhan2020_source_note')}{' '} + + github.com/nicedoc/wuhan2020 + +

+
+ +
+ + ); +}); + +export default Wuhan2020Page; diff --git a/styles/Wuhan2020.module.less b/styles/Wuhan2020.module.less new file mode 100644 index 0000000..2089882 --- /dev/null +++ b/styles/Wuhan2020.module.less @@ -0,0 +1,172 @@ +// Wuhan-2020 Archival Page styles + +.hero { + position: relative; + background: linear-gradient(135deg, #1a237e 0%, #283593 50%, #3949ab 100%); + padding: 5rem 0; + overflow: hidden; + color: #fff; + + &::before { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.08) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 50%); + pointer-events: none; + content: ''; + } +} + +.title { + margin-bottom: 1rem; + font-weight: 700; + font-size: 2.5rem; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + + @media (max-width: 768px) { + font-size: 1.8rem; + } +} + +.subtitle { + opacity: 0.9; + margin: 0 auto; + max-width: 700px; + font-size: 1.15rem; + line-height: 1.6; + + @media (max-width: 768px) { + font-size: 0.95rem; + } +} + +.metaBadge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin: 0.25rem; + padding: 0.3rem 0.7rem; + border-radius: 1rem; + background: rgba(255, 255, 255, 0.15); + font-size: 0.85rem; + backdrop-filter: blur(4px); +} + +.statsRow { + margin-top: -2rem; +} + +.statCard { + transition: all 0.3s ease; + border: none; + border-radius: 1rem; + background: #fff; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + cursor: pointer; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); + } + + &.active { + border-bottom: 3px solid #1a237e; + box-shadow: 0 4px 20px rgba(26, 35, 126, 0.2); + } +} + +.statIcon { + font-size: 2rem; + line-height: 1; +} + +.statCount { + font-weight: 700; + font-size: 1.5rem; + color: #1a237e; +} + +.statLabel { + font-size: 0.85rem; + color: #666; +} + +.recordCard { + transition: all 0.3s ease; + border: none; + border-radius: 0.75rem; + background: #fff; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); + + &:hover { + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1); + } +} + +.badgeVerified { + color: #fff; + background: #2e7d32; +} + +.categoryBadge { + color: #fff; + font-size: 0.75rem; +} + +.description { + color: #555; + font-size: 0.9rem; + line-height: 1.6; +} + +.infoItem { + display: flex; + align-items: flex-start; + gap: 0.5rem; + color: #666; + font-size: 0.85rem; +} + +.filterBar { + background: #f8f9fa; + border-radius: 0.75rem; +} + +.filterBtn { + transition: all 0.2s ease; + border: 2px solid transparent; + border-radius: 2rem; + background: #fff; + color: #555; + + &:hover { + color: #1a237e; + border-color: #1a237e; + } + + &.active { + color: #fff; + background: #1a237e; + border-color: #1a237e; + } +} + +.timeline { + border-left: 3px solid #e3f2fd; +} + +.timelineDot { + width: 12px; + height: 12px; + margin-left: -8px; + border-radius: 50%; + background: #1a237e; +} + +.sourceNote { + font-size: 0.8rem; + color: #999; +} diff --git a/translation/en-US.ts b/translation/en-US.ts index e346d5c..9c9376c 100644 --- a/translation/en-US.ts +++ b/translation/en-US.ts @@ -340,6 +340,35 @@ export default { team_works: 'Team Works', no_news_yet: 'No news yet', + // Wuhan-2020 Archival + wuhan2020: 'COVID-19 Rescue', + wuhan2020_title: 'Wuhan COVID-19 Rescue Platform · Data Archive', + wuhan2020_description: + 'During the early 2020 COVID-19 outbreak, enterprises, organizations, and individuals across China mobilized to help. This page is an open data archive of the WuHan-2020 COVID rescue information platform, preserving the history of that united effort.', + wuhan2020_records: 'records', + wuhan2020_regions: 'regions', + wuhan2020_all_records: 'All Records', + wuhan2020_all: 'All', + wuhan2020_search_placeholder: 'Search by name, region…', + wuhan2020_no_results: 'No matching records found', + wuhan2020_reset: 'Reset Filters', + wuhan2020_verified: 'Verified', + wuhan2020_cat_hospital: 'Hospitals', + wuhan2020_cat_hotel: 'Hotels', + wuhan2020_cat_logistics: 'Logistics', + wuhan2020_cat_factory: 'Factories', + wuhan2020_cat_donation: 'Donations', + wuhan2020_cat_consulting: 'Consulting', + wuhan2020_timeline_title: 'Key Timeline', + wuhan2020_timeline_1: 'Zhong Nanshan confirms human-to-human transmission of COVID-19', + wuhan2020_timeline_2: + 'Wuhan announces city lockdown; public transit, metro, ferries, and long-distance buses suspended', + wuhan2020_timeline_3: 'Rescue supplies and medical teams from across China begin massive support for Wuhan', + wuhan2020_timeline_4: 'Huoshenshan Hospital officially delivered and put into use', + wuhan2020_timeline_5: 'Leishenshan Hospital officially delivered and put into use', + wuhan2020_timeline_6: 'Wuhan officially lifts lockdown controls', + wuhan2020_source_note: 'Data source: ', + // Open Library open_library: 'Open Library', diff --git a/translation/zh-CN.ts b/translation/zh-CN.ts index f390f2c..2abd7ca 100644 --- a/translation/zh-CN.ts +++ b/translation/zh-CN.ts @@ -316,6 +316,34 @@ export default { team_works: '团队作品', no_news_yet: '暂无动态', + // Wuhan-2020 Archival + wuhan2020: '新冠救援信息', + wuhan2020_title: '武汉新冠救援信息平台 · 数据存档', + wuhan2020_description: + '2020 年初新冠疫情爆发期间,来自全国各地的企业、机构和个人纷纷伸出援手。本页面为 WuHan-2020 新冠救援信息平台的开放数据存档,记录了那段众志成城的历史。', + wuhan2020_records: '条记录', + wuhan2020_regions: '个地区', + wuhan2020_all_records: '全部信息', + wuhan2020_all: '全部', + wuhan2020_search_placeholder: '搜索名称、地区……', + wuhan2020_no_results: '没有找到匹配的记录', + wuhan2020_reset: '重置筛选', + wuhan2020_verified: '已核实', + wuhan2020_cat_hospital: '定点医院', + wuhan2020_cat_hotel: '酒店住宿', + wuhan2020_cat_logistics: '物流运输', + wuhan2020_cat_factory: '物资工厂', + wuhan2020_cat_donation: '捐赠渠道', + wuhan2020_cat_consulting: '免费咨询', + wuhan2020_timeline_title: '关键时间线', + wuhan2020_timeline_1: '钟南山确认新冠病毒人传人', + wuhan2020_timeline_2: '武汉宣布"封城",全市城市公交、地铁、轮渡、长途客运暂停运营', + wuhan2020_timeline_3: '全国各地救援物资和医疗队开始大规模驰援武汉', + wuhan2020_timeline_4: '火神山医院正式交付使用', + wuhan2020_timeline_5: '雷神山医院正式交付使用', + wuhan2020_timeline_6: '武汉正式解除离汉通道管控', + wuhan2020_source_note: '数据来源:', + // Open Library open_library: '开源图书馆', diff --git a/translation/zh-TW.ts b/translation/zh-TW.ts index c3a5ad4..5c8751a 100644 --- a/translation/zh-TW.ts +++ b/translation/zh-TW.ts @@ -316,6 +316,34 @@ export default { team_works: '團隊作品', no_news_yet: '暫無動態', + // Wuhan-2020 Archival + wuhan2020: '新冠救援資訊', + wuhan2020_title: '武漢新冠救援資訊平台 · 資料存檔', + wuhan2020_description: + '2020 年初新冠疫情爆發期間,來自全國各地的企業、機構和個人紛紛伸出援手。本頁面為 WuHan-2020 新冠救援資訊平台的開放資料存檔,記錄了那段眾志成城的歷史。', + wuhan2020_records: '筆記錄', + wuhan2020_regions: '個地區', + wuhan2020_all_records: '全部資訊', + wuhan2020_all: '全部', + wuhan2020_search_placeholder: '搜尋名稱、地區……', + wuhan2020_no_results: '沒有找到匹配的記錄', + wuhan2020_reset: '重置篩選', + wuhan2020_verified: '已核實', + wuhan2020_cat_hospital: '定點醫院', + wuhan2020_cat_hotel: '酒店住宿', + wuhan2020_cat_logistics: '物流運輸', + wuhan2020_cat_factory: '物資工廠', + wuhan2020_cat_donation: '捐贈渠道', + wuhan2020_cat_consulting: '免費諮詢', + wuhan2020_timeline_title: '關鍵時間線', + wuhan2020_timeline_1: '鍾南山確認新冠病毒人傳人', + wuhan2020_timeline_2: '武漢宣布「封城」,全市城市公交、地鐵、輪渡、長途客運暫停運營', + wuhan2020_timeline_3: '全國各地救援物資和醫療隊開始大規模馳援武漢', + wuhan2020_timeline_4: '火神山醫院正式交付使用', + wuhan2020_timeline_5: '雷神山醫院正式交付使用', + wuhan2020_timeline_6: '武漢正式解除離漢通道管控', + wuhan2020_source_note: '資料來源:', + // Open Library open_library: '開源圖書館',