diff --git a/.gitignore b/.gitignore index a547bf3..b8c4685 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ dist dist-ssr *.local +# 设计参考稿 / 切片源文件,正式素材已整理进 public/assets +images/ + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/public/assets/img/intl/README.md b/public/assets/img/intl/README.md new file mode 100644 index 0000000..4d7beb0 --- /dev/null +++ b/public/assets/img/intl/README.md @@ -0,0 +1,18 @@ +# 国际站切片素材(来自 images/*_slices.zip) + +| 文件 | 来源 | 用途 | +|------|------|------| +| tab-report.png | 首页1 The_Rating... | 首页 Tab1 图标 | +| tab-ai.png | 首页1 Frame | 首页 Tab2 图标 | +| tab-method.png | 首页1 STAR_Multi... | 首页 Tab3 图标 | +| report-cover.png | 首页1 占位图 | 报告封面 / Research 卡头 | +| feature-ai.png | 首页2 背景2_ | AI Tab 右侧插画 | +| feature-method.png | 首页3 背景3 | Methodology Tab 右侧插画 | +| bg-coop-light.png | 首页1 背景1 | Cooperation 页浅色区背景 | +| bg-coop-dark.png | cooperation-悬浮 背景6 | 首页 Cooperation 深色区底纹 | +| bg-mission.png | people 背景4 | People Mission & Vision 左图 | +| icon-join/meeting/partner/contact.png | 首页1 加入/会议/合作/联系 | Cooperation 卡片图标 | +| icon-improve/training.png | cooperation-悬浮 改进/发展 | Cooperation 额外图标 | +| icon-more.png | 首页1 ic_右-后 | more 箭头 | + +Hero「Guidelines Worth Applying」使用原站图:`/assets/img/carousel/jishitang.png` diff --git a/public/assets/img/intl/bg-coop-dark.png b/public/assets/img/intl/bg-coop-dark.png new file mode 100644 index 0000000..be77f87 Binary files /dev/null and b/public/assets/img/intl/bg-coop-dark.png differ diff --git a/public/assets/img/intl/bg-coop-light.png b/public/assets/img/intl/bg-coop-light.png new file mode 100644 index 0000000..3f994f5 Binary files /dev/null and b/public/assets/img/intl/bg-coop-light.png differ diff --git a/public/assets/img/intl/bg-mission.png b/public/assets/img/intl/bg-mission.png new file mode 100644 index 0000000..b0ea6f0 Binary files /dev/null and b/public/assets/img/intl/bg-mission.png differ diff --git a/public/assets/img/intl/feature-ai.png b/public/assets/img/intl/feature-ai.png new file mode 100644 index 0000000..3d8cc34 Binary files /dev/null and b/public/assets/img/intl/feature-ai.png differ diff --git a/public/assets/img/intl/feature-method.png b/public/assets/img/intl/feature-method.png new file mode 100644 index 0000000..f6363fb Binary files /dev/null and b/public/assets/img/intl/feature-method.png differ diff --git a/public/assets/img/intl/icon-contact.png b/public/assets/img/intl/icon-contact.png new file mode 100644 index 0000000..8c14bee Binary files /dev/null and b/public/assets/img/intl/icon-contact.png differ diff --git a/public/assets/img/intl/icon-improve.png b/public/assets/img/intl/icon-improve.png new file mode 100644 index 0000000..f239b4e Binary files /dev/null and b/public/assets/img/intl/icon-improve.png differ diff --git a/public/assets/img/intl/icon-join.png b/public/assets/img/intl/icon-join.png new file mode 100644 index 0000000..eefe919 Binary files /dev/null and b/public/assets/img/intl/icon-join.png differ diff --git a/public/assets/img/intl/icon-meeting.png b/public/assets/img/intl/icon-meeting.png new file mode 100644 index 0000000..d799822 Binary files /dev/null and b/public/assets/img/intl/icon-meeting.png differ diff --git a/public/assets/img/intl/icon-more.png b/public/assets/img/intl/icon-more.png new file mode 100644 index 0000000..b2d2c7c Binary files /dev/null and b/public/assets/img/intl/icon-more.png differ diff --git a/public/assets/img/intl/icon-partner.png b/public/assets/img/intl/icon-partner.png new file mode 100644 index 0000000..f664567 Binary files /dev/null and b/public/assets/img/intl/icon-partner.png differ diff --git a/public/assets/img/intl/icon-training.png b/public/assets/img/intl/icon-training.png new file mode 100644 index 0000000..eaf3b8b Binary files /dev/null and b/public/assets/img/intl/icon-training.png differ diff --git a/public/assets/img/intl/report-cover.png b/public/assets/img/intl/report-cover.png new file mode 100644 index 0000000..929c986 Binary files /dev/null and b/public/assets/img/intl/report-cover.png differ diff --git a/public/assets/img/intl/tab-ai.png b/public/assets/img/intl/tab-ai.png new file mode 100644 index 0000000..627fc05 Binary files /dev/null and b/public/assets/img/intl/tab-ai.png differ diff --git a/public/assets/img/intl/tab-method.png b/public/assets/img/intl/tab-method.png new file mode 100644 index 0000000..741bb04 Binary files /dev/null and b/public/assets/img/intl/tab-method.png differ diff --git a/public/assets/img/intl/tab-report.png b/public/assets/img/intl/tab-report.png new file mode 100644 index 0000000..293ce6e Binary files /dev/null and b/public/assets/img/intl/tab-report.png differ diff --git a/public/assets/img/logo-en-white.png b/public/assets/img/logo-en-white.png new file mode 100644 index 0000000..d689014 Binary files /dev/null and b/public/assets/img/logo-en-white.png differ diff --git a/public/assets/img/logo-star-mark-white.png b/public/assets/img/logo-star-mark-white.png new file mode 100644 index 0000000..06130fc Binary files /dev/null and b/public/assets/img/logo-star-mark-white.png differ diff --git a/public/assets/img/logo-star-mark.png b/public/assets/img/logo-star-mark.png new file mode 100644 index 0000000..f2a56d2 Binary files /dev/null and b/public/assets/img/logo-star-mark.png differ diff --git a/src/components/site/CoopCardIcon.jsx b/src/components/site/CoopCardIcon.jsx new file mode 100644 index 0000000..ebe09ea --- /dev/null +++ b/src/components/site/CoopCardIcon.jsx @@ -0,0 +1,23 @@ +import { cn } from "@/lib/utils"; + +/** + * Cooperation 卡片图标:用 mask 上色,随父级 currentColor 变化(含 hover)。 + */ +export default function CoopCardIcon({ src, className }) { + return ( + + ); +} diff --git a/src/components/site/PlaceholderMedia.jsx b/src/components/site/PlaceholderMedia.jsx new file mode 100644 index 0000000..ef1a045 --- /dev/null +++ b/src/components/site/PlaceholderMedia.jsx @@ -0,0 +1,62 @@ +import { cn } from "@/lib/utils"; + +/** + * 通用占位图:用渐变色块代替正式摄影/插画素材。 + */ +export function PlaceholderMedia({ + label = "Placeholder", + className, + tone = "blue", +}) { + const tones = { + blue: "from-[#2b4a9a] via-[#4a6fd0] to-[#9bb6f0]", + soft: "from-[#c5d6f5] via-[#e8f1ff] to-[#f7faff]", + warm: "from-[#1a2748] via-[#3d4f7a] to-[#c9a06a]", + teal: "from-[#1e4a6e] via-[#3a7ca5] to-[#8ec5e0]", + paper: "from-[#3a61c2] via-[#5b7fd4] to-[#d6e2ff]", + }; + + return ( +
+ + {label} + +
+
+
+ ); +} + +/** 合作区六边形纹理背景(深蓝 / 浅蓝),不含素材摄影 */ +export function HexPatternBg({ className, children, dark = false }) { + // 点阵蜂窝:两列错位六边形,stroke 用 URL 编码色值 + const hexStroke = dark ? "%23ffffff" : "%233a61c2"; + const hexTile = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='84' height='48' viewBox='0 0 84 48'%3E%3Cpath d='M22 2 L42 14 L42 34 L22 46 L2 34 L2 14 Z' fill='none' stroke='${hexStroke}' stroke-width='1'/%3E%3Cpath d='M64 2 L84 14 L84 34 L64 46 L44 34 L44 14 Z' fill='none' stroke='${hexStroke}' stroke-width='1'/%3E%3C/svg%3E")`; + + return ( +
+
+
{children}
+
+ ); +} diff --git a/src/components/site/SiteHero.jsx b/src/components/site/SiteHero.jsx new file mode 100644 index 0000000..928f060 --- /dev/null +++ b/src/components/site/SiteHero.jsx @@ -0,0 +1,54 @@ +import { cn } from "@/lib/utils"; + +/** 沿用原站轮播主视觉(图内已含 Guidelines Worth Applying 文案) */ +export const HERO_BG = "/assets/img/carousel/jishitang.png"; + +/** + * 全站共用 Hero。背景默认用原站积石堂图。 + * showTitle=false:图内已有标题时不再叠字。 + */ +export default function SiteHero({ + title = "Guidelines Worth Applying", + className, + heightClass = "h-[280px] md:h-[360px]", + overlay, + showTitle = false, + backgroundImage = HERO_BG, + /** 允许 overlay 卡片探出 Hero 底部(如 Join a committee) */ + overflowVisible = false, +}) { + return ( +
+
+
+ + {(overlay || showTitle) && ( +
+ {overlay || ( +

+ {title} +

+ )} +
+ )} +
+ ); +} diff --git a/src/config/intlAssets.js b/src/config/intlAssets.js new file mode 100644 index 0000000..44b1962 --- /dev/null +++ b/src/config/intlAssets.js @@ -0,0 +1,22 @@ +/** + * 国际站切片素材路径(来自 images/*_slices.zip,已整理到 public/assets/img/intl) + */ +export const INTL = { + hero: "/assets/img/carousel/jishitang.png", + tabReport: "/assets/img/intl/tab-report.png", + tabAi: "/assets/img/intl/tab-ai.png", + tabMethod: "/assets/img/intl/tab-method.png", + reportCover: "/assets/img/intl/report-cover.png", + featureAi: "/assets/img/intl/feature-ai.png", + featureMethod: "/assets/img/intl/feature-method.png", + bgCoopLight: "/assets/img/intl/bg-coop-light.png", + bgCoopDark: "/assets/img/intl/bg-coop-dark.png", + bgMission: "/assets/img/intl/bg-mission.png", + iconJoin: "/assets/img/intl/icon-join.png", + iconMeeting: "/assets/img/intl/icon-meeting.png", + iconPartner: "/assets/img/intl/icon-partner.png", + iconContact: "/assets/img/intl/icon-contact.png", + iconImprove: "/assets/img/intl/icon-improve.png", + iconTraining: "/assets/img/intl/icon-training.png", + iconMore: "/assets/img/intl/icon-more.png", +}; diff --git a/src/config/nav.js b/src/config/nav.js index 4255986..4afd877 100644 --- a/src/config/nav.js +++ b/src/config/nav.js @@ -85,5 +85,30 @@ export const FALLBACK_BANNERS = [ }, ]; -/** 固定头总高度:顶栏50 + logo行80 + 导航40 */ -export const HEADER_OFFSET = 170; +/** 国际站改版:单层顶栏高度 */ +export const HEADER_OFFSET = 72; + +/** 国际站一级导航(People / News / Research / Cooperation) */ +export const INTL_TOP_NAV = [ + { + id: "people", + label: "People", + to: "/people", + children: [ + { id: "mission", label: "Mission & Vision", to: "/people" }, + { id: "committees", label: "Committees", to: "/people#committees" }, + { id: "team", label: "Team Members", to: "/specialist" }, + ], + }, + { id: "news", label: "News", to: "/newsList" }, + { id: "research", label: "Research", to: "/research" }, + { + id: "cooperation", + label: "Cooperation", + to: "/cooperation", + children: [ + { id: "join", label: "Join a committee", to: "/cooperation/join" }, + { id: "contact", label: "Talk to us", to: "/relation" }, + ], + }, +]; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9cdc0fd..a9c0f1f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1,6 +1,7 @@ { "brand": "STAR International", - "brandTagline": "Scientificity, Transparency and Applicability Rankings for Practice Guidelines", + "brandTagline": "Scientificity, Transparency and Applicability", + "brandWords": ["Scientificity", "Transparency", "Applicability"], "nav": { "home": "Home", "ranking": "Ranking", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 6b10875..b7219fb 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1,6 +1,7 @@ { "brand": "STAR International", "brandTagline": "科学 · 透明 · 适用", + "brandWords": ["科学", "透明", "适用"], "nav": { "home": "首页", "ranking": "排名", diff --git a/src/index.css b/src/index.css index ba3f39e..622918a 100644 --- a/src/index.css +++ b/src/index.css @@ -1,22 +1,27 @@ @import "tailwindcss"; @theme { - --color-star-primary: #181b62; - --color-star-topbar: #1d2b40; - --color-star-bg: #f6f8fc; + --color-star-primary: #0b1a3d; + --color-star-accent: #3a61c2; + --color-star-topbar: #0b1a3d; + --color-star-bg: #f4f7fc; + --color-star-soft: #e8f1ff; --font-sans: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; + --font-hero: "Georgia", "Times New Roman", "Songti SC", serif; } :root { - --star-primary: #181b62; - --star-topbar: rgb(29, 43, 64); - --star-bg: #f6f8fc; + --star-primary: #0b1a3d; + --star-accent: #3a61c2; + --star-topbar: #0b1a3d; + --star-bg: #f4f7fc; + --star-soft: #e8f1ff; --star-muted: #999; - --star-nav-hover: rgba(24, 27, 98, 0.06); - --star-nav-active-bar: #181b62; - --star-nav-dropdown-hover: #f5f7fa; - --star-nav-border: #eee; + --star-nav-hover: rgba(255, 255, 255, 0.08); + --star-nav-active-bar: #ffffff; + --star-nav-dropdown-hover: #eef3fb; + --star-nav-border: rgba(255, 255, 255, 0.12); --star-select-border: #d8dce8; } diff --git a/src/layouts/PublicLayout.jsx b/src/layouts/PublicLayout.jsx index d61e5c7..400a2f2 100644 --- a/src/layouts/PublicLayout.jsx +++ b/src/layouts/PublicLayout.jsx @@ -1,11 +1,4 @@ -import { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from "react"; +import { useEffect, useMemo, useState } from "react"; import { Outlet, Link, @@ -15,50 +8,75 @@ import { } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; +import { Globe, Settings, Menu, X, ChevronDown } from "lucide-react"; import { setUiLanguage } from "@/i18n"; -import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; -import { HEADER_OFFSET } from "@/config/nav"; -import { contentApi } from "@/services"; -import BrandLogo from "@/components/site/BrandLogo"; -import { - buildColumnNavTree, - fallbackFooterNav, - fallbackTopNav, -} from "@/utils/navTree"; +import { HEADER_OFFSET, INTL_TOP_NAV } from "@/config/nav"; /** - * 详情页归属到列表栏目,避免进入详情后顶栏 active 消失。 - * key: 详情 pathname,value: 应对齐高亮的列表 path。 + * 详情页归属到列表栏目,保证顶栏高亮正确。 */ const DETAIL_PARENT_PATHS = { - "/fingerpostDetail": "/guidelineAndConsensus", - "/organization/detail": "/organization", - "/specialist/specialistDetail": "/specialist", + "/fingerpostDetail": "/research", + "/organization/detail": "/cooperation", + "/specialist/specialistDetail": "/people", + "/specialist": "/people", "/newsDetail": "/newsList", - "/publicationDetail": "/publication", + "/publicationDetail": "/research", + "/publication": "/research", + "/cooperation/join": "/cooperation", + "/about": "/people", }; function resolveActivePathname(pathname) { return DETAIL_PARENT_PATHS[pathname] || pathname; } -function navActive(pathname, search, to) { +function navActive(pathname, to) { const activePath = resolveActivePathname(pathname); - const [path, query = ""] = to.split("?"); + const path = to.split("#")[0].split("?")[0]; if (path === "/index") { return activePath === "/" || activePath === "/index"; } - if (!activePath.startsWith(path)) return false; - if (!query) return true; - // 详情页映射到列表后,不再用 query 细分子项(避免排名子菜单误高亮) - if (activePath !== pathname) return true; - const want = new URLSearchParams(query); - const cur = new URLSearchParams(search); - for (const [k, v] of want.entries()) { - if (cur.get(k) !== v) return false; + if (path === "/people") { + return ( + activePath === "/people" || + activePath.startsWith("/specialist") || + activePath === "/about" + ); } - return true; + if (path === "/research") { + return ( + activePath === "/research" || + activePath.startsWith("/publication") || + activePath === "/guidelineAndConsensus" || + activePath === "/fingerpostDetail" + ); + } + if (path === "/cooperation") { + return ( + activePath.startsWith("/cooperation") || + activePath === "/relation" || + activePath.startsWith("/organization") + ); + } + return activePath === path || activePath.startsWith(`${path}/`); +} + +/** 顶栏品牌:仅 STAR 字标(红三角 A) */ +function StarMark() { + return ( + + STAR + + ); } export default function PublicLayout() { @@ -68,34 +86,9 @@ export default function PublicLayout() { const dispatch = useDispatch(); const memberToken = useSelector((s) => s.member?.token); const memberProfile = useSelector((s) => s.member?.profile); - const [keyword, setKeyword] = useState(""); - const [openMenu, setOpenMenu] = useState(null); const [mobileOpen, setMobileOpen] = useState(false); - const [columns, setColumns] = useState(null); - const navRef = useRef(null); - const itemRefs = useRef(new Map()); - const [indicator, setIndicator] = useState({ - left: 0, - width: 0, - visible: false, - }); - const [indicatorReady, setIndicatorReady] = useState(false); - - useEffect(() => { - let alive = true; - contentApi - .listColumns({ limit: 200 }) - .then((res) => { - if (!alive) return; - setColumns(res.results || []); - }) - .catch(() => { - if (alive) setColumns([]); - }); - return () => { - alive = false; - }; - }, []); + const [openMenu, setOpenMenu] = useState(null); + const [settingsOpen, setSettingsOpen] = useState(false); useEffect(() => { if (memberToken && !memberProfile) { @@ -103,249 +96,57 @@ export default function PublicLayout() { } }, [memberToken, memberProfile, dispatch]); - const topNav = useMemo(() => { - if (!columns) return fallbackTopNav(t); - const tree = buildColumnNavTree(columns, 1); - return tree.length ? tree : fallbackTopNav(t); - }, [columns, t]); - - const footerNav = useMemo(() => { - if (!columns) return fallbackFooterNav(t); - const tree = buildColumnNavTree(columns, 3); - return tree.length ? tree : fallbackFooterNav(t); - }, [columns, t]); - - const hotWords = useMemo( - () => - i18n.language === "zh" - ? ["影像", "指南", "共识"] - : ["imaging", "guideline", "consensus"], - [i18n.language], - ); - - const onSearch = () => { - const q = keyword.trim(); - if (q) navigate(`/search?q=${encodeURIComponent(q)}`); - }; - - const isItemActive = (item) => { - if ( - item.children?.some((c) => - navActive(location.pathname, location.search, c.to), - ) - ) { - return true; - } - return navActive(location.pathname, location.search, item.to); - }; - - const activeNavId = useMemo(() => { - const active = topNav.find((item) => isItemActive(item)); - return active?.id ?? null; - }, [topNav, location.pathname, location.search]); - - const setItemRef = (id) => (el) => { - if (el) itemRefs.current.set(id, el); - else itemRefs.current.delete(id); - }; - - const updateIndicator = useCallback(() => { - const navEl = navRef.current; - if (!navEl || activeNavId == null) { - setIndicator((prev) => ({ ...prev, width: 0, visible: false })); - return; - } - const el = itemRefs.current.get(activeNavId); - if (!el) { - setIndicator((prev) => ({ ...prev, width: 0, visible: false })); - return; - } - const navRect = navEl.getBoundingClientRect(); - const rect = el.getBoundingClientRect(); - const inset = 10; - setIndicator({ - left: rect.left - navRect.left + inset, - width: Math.max(0, rect.width - inset * 2), - visible: true, - }); - setIndicatorReady(true); - }, [activeNavId]); - - useLayoutEffect(() => { - updateIndicator(); - }, [updateIndicator, topNav, i18n.language]); - useEffect(() => { - const onResize = () => updateIndicator(); - window.addEventListener("resize", onResize); - return () => window.removeEventListener("resize", onResize); - }, [updateIndicator]); + setMobileOpen(false); + setOpenMenu(null); + setSettingsOpen(false); + }, [location.pathname, location.search]); - // 桌面主导航(指示条由共享元素滑动,不在单项上画) - const linkClass = (active) => - cn( - "inline-flex h-10 items-center px-5 text-base no-underline transition-colors", - "text-(--star-primary) hover:bg-(--star-nav-hover)", - active ? "font-bold" : "font-semibold", - ); - - // 移动端一级项:左侧指示条 - const mobileLinkClass = (active) => - cn( - "relative flex h-10 items-center px-5 text-base no-underline transition-colors", - "text-(--star-primary)", - active - ? "bg-(--star-nav-hover) font-bold before:absolute before:top-2 before:bottom-2 before:left-0 before:w-0.5 before:rounded-full before:bg-(--star-nav-active-bar)" - : "font-semibold hover:bg-(--star-nav-hover)", - ); - - const dropdownItemClass = (active) => - cn( - "relative block px-5 py-2.5 text-base no-underline transition-colors", - active - ? "bg-(--star-nav-hover) font-bold text-(--star-primary) before:absolute before:top-1.5 before:bottom-1.5 before:left-0 before:w-0.5 before:rounded-full before:bg-(--star-nav-active-bar)" - : "font-medium text-[#333] hover:bg-(--star-nav-dropdown-hover) hover:text-(--star-primary)", - ); + const topNav = useMemo(() => INTL_TOP_NAV, []); return (
-
+
-
-
- {t("nav.hotSearch")} - {hotWords.map((w, i) => ( - - {i > 0 && ( - - · - - )} - - - ))} -
- {memberToken ? ( - - - {t("member.myCollections")} - - - {memberProfile?.name || t("member.account")} - - - - ) : ( - - {t("actions.login")} - - )} - -
-
+ -
- -
- setKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && onSearch()} - placeholder={t("search.placeholderShort")} - className="h-8 w-55 rounded-full border-[#ccc] px-3 text-sm focus:border-(--star-primary)" - /> - -
- -
- -
-
{mobileOpen && ( -
+
{topNav.map((item) => ( -
+
setMobileOpen(false)} > - {item.name} + {item.label} {item.children?.map((child) => ( setMobileOpen(false)} > - {child.name} + {child.label} ))}
))} +
+ + setMobileOpen(false)} + > + {t("actions.login")} + +
)}
- {/* 子页面根节点随主栏撑满,避免短内容页中间高度不足、页脚上顶 */} -
+
-
-
- {footerNav.map((group) => ( -
-
{group.name}
- {( - group.children || [ - { id: group.id, name: group.name, to: group.to }, - ] - ).map((link) => ( - - {link.name} - - ))} -
- ))} -
-
- -
- WeChat - Weibo -
-

- {t("footer.copyrightLine")} -

-

- {t("footer.icp")} -

- - {t("nav.admin")} - -
-
-
+ />
); } diff --git a/src/pages/public/CooperationPage.jsx b/src/pages/public/CooperationPage.jsx new file mode 100644 index 0000000..0f6e01e --- /dev/null +++ b/src/pages/public/CooperationPage.jsx @@ -0,0 +1,116 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; +import CoopCardIcon from "@/components/site/CoopCardIcon"; +import { HexPatternBg } from "@/components/site/PlaceholderMedia"; +import SiteHero from "@/components/site/SiteHero"; +import { INTL } from "@/config/intlAssets"; +import { cn } from "@/lib/utils"; + +const CARDS = [ + { + id: "join", + icon: INTL.iconJoin, + title: "Join a committee", + desc: "Contribute to STAR advisory committees and help improve guideline quality.", + to: "/cooperation/join", + }, + { + id: "meeting", + icon: INTL.iconMeeting, + title: "Come to a meeting", + desc: "Attend STAR meetings and workshops to exchange ideas with peers.", + to: "/relation", + }, + { + id: "work", + icon: INTL.iconPartner, + title: "Work with us", + desc: "Explore institutional partnerships and collaborative research projects.", + to: "/relation", + }, + { + id: "improve", + icon: INTL.iconImprove, + title: "Help us improve", + desc: "Share feedback on STAR tools, reports, and evaluation methodology.", + to: "/relation", + }, + { + id: "training", + icon: INTL.iconTraining, + title: "STAR training and development opportunities", + desc: "Access training resources and capacity-building opportunities.", + to: "/research", + }, + { + id: "talk", + icon: INTL.iconContact, + title: "Talk to us", + desc: "Reach out to the STAR team for questions, media, or collaboration.", + to: "/relation", + }, +]; + +export default function CooperationPage() { + const [hoverId, setHoverId] = useState("meeting"); + + return ( +
+ + {/* 浅色六边形底:去掉素材感过强的实验室手套主视觉 */} + +
+ {CARDS.map((card) => { + const active = hoverId === card.id; + return ( + setHoverId(card.id)} + className={cn( + "block rounded-md p-6 no-underline shadow-sm transition-colors", + active ? "bg-(--star-primary)" : "bg-white", + )} + style={{ color: active ? "#ffffff" : "#1a2b56" }} + > + +

+ {card.title} +

+

+ {card.desc} +

+ + more + + + + ); + })} +
+
+
+ ); +} diff --git a/src/pages/public/HomePage.jsx b/src/pages/public/HomePage.jsx index 650cb1e..a4a29d8 100644 --- a/src/pages/public/HomePage.jsx +++ b/src/pages/public/HomePage.jsx @@ -1,206 +1,276 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import SectionTitle from "@/components/site/SectionTitle"; -import HomeCarousel from "@/components/site/HomeCarousel"; -import { FALLBACK_BANNERS } from "@/config/nav"; +import CoopCardIcon from "@/components/site/CoopCardIcon"; +import { HexPatternBg } from "@/components/site/PlaceholderMedia"; +import SiteHero from "@/components/site/SiteHero"; +import { INTL } from "@/config/intlAssets"; import { contentApi } from "@/services"; +import { cn } from "@/lib/utils"; const FALLBACK_REPORTS = [ { id: "r1", - name: "STAR Report 2024", - year: 2024, - logo: "/assets/img/01@2x.jpg", - file_path: "#", + name: "Language models for data extraction from abstracts", + year: 2025, }, { id: "r2", - name: "STAR Report 2023", - year: 2023, - logo: "/assets/img/02@2x.jpg", - file_path: "#", + name: "Language models for data extraction from abstracts", + year: 2025, }, { id: "r3", - name: "STAR Report 2022", - year: 2022, - logo: "/assets/img/03@2x.jpg", - file_path: "#", + name: "Language models for data extraction from abstracts", + year: 2025, }, { id: "r4", - name: "STAR Report 2021", - year: 2021, - logo: "/assets/img/04@2x.jpg", - file_path: "#", + name: "Language models for data extraction from abstracts", + year: 2025, }, ]; +const FEATURE_TABS = [ + { + id: "report", + title: "The Rating Guidelines Report", + icon: INTL.tabReport, + heading: "The Rating Guidelines Report", + desc: "Explore STAR rating guideline reports and related research outputs. Placeholder copy for layout preview.", + }, + { + id: "ai", + title: "STAR AI-Driven Assessment System", + icon: INTL.tabAi, + heading: "STAR AI-Driven Assessment System", + desc: "An AI-assisted assessment workflow for guideline appraisal. Placeholder introduction text for the redesign mock.", + illustration: INTL.featureAi, + }, + { + id: "method", + title: "STAR Multi-dimensional Evaluation Methodology", + icon: INTL.tabMethod, + heading: "STAR Multi-dimensional Evaluation Methodology", + desc: "A multi-dimensional methodology for transparent guideline evaluation. Placeholder system introduction text.", + illustration: INTL.featureMethod, + }, +]; + +const COOP_CARDS = [ + { + icon: INTL.iconJoin, + title: "Become a Committee Member", + desc: "Our guidance and quality standards are developed by independent committees. Join and help improve guideline quality.", + to: "/cooperation/join", + }, + { + icon: INTL.iconMeeting, + title: "Attend our Events", + desc: "Advisory meetings and workshops are open to peers and partners. Come along and see how we work.", + to: "/relation", + }, + { + icon: INTL.iconPartner, + title: "Collaborate With Us", + desc: "Explore institutional partnerships and collaborative research projects with STAR.", + to: "/cooperation", + }, + { + icon: INTL.iconContact, + title: "Connect With Us", + desc: "Have a question or comment? Reach out to the STAR team for collaboration.", + to: "/relation", + }, +]; + +const PARTNERS = [ + "WHO", + "G-I-N", + "GRADE", + "Cochrane", + "McMaster", + "Lanzhou U", + "Partner A", + "Partner B", +]; + function formatDate(value) { - if (!value) return ""; + if (!value) return "2025-01-12"; return String(value).slice(0, 10); } export default function HomePage() { - const { t } = useTranslation(); - const [news, setNews] = useState([]); - const [publications, setPublications] = useState([]); + const [activeTab, setActiveTab] = useState("report"); const [reports, setReports] = useState(FALLBACK_REPORTS); - const [banners, setBanners] = useState(FALLBACK_BANNERS); useEffect(() => { let alive = true; - (async () => { - try { - const [newsRes, pubRes, reportRes, bannerRes] = await Promise.all([ - contentApi.listNews({ limit: 8 }), - contentApi.listPublications({ limit: 8 }), - contentApi.listReports({ limit: 20 }), - contentApi.listBanners(), - ]); + contentApi + .listReports({ limit: 8 }) + .then((res) => { if (!alive) return; - setNews(newsRes?.results || []); - setPublications(pubRes?.results || []); - const remote = reportRes?.results || []; + const remote = res?.results || []; setReports(remote.length ? remote : FALLBACK_REPORTS); - const bannerList = Array.isArray(bannerRes) - ? bannerRes - : bannerRes?.results || []; - const enabled = bannerList.filter((b) => !b.is_disabled); - setBanners(enabled.length ? enabled : FALLBACK_BANNERS); - } catch { - if (alive) { - setReports(FALLBACK_REPORTS); - setBanners(FALLBACK_BANNERS); - } - } - })(); + }) + .catch(() => { + if (alive) setReports(FALLBACK_REPORTS); + }); return () => { alive = false; }; }, []); + const tab = FEATURE_TABS.find((item) => item.id === activeTab) || FEATURE_TABS[0]; + return (
- + - {/* 新闻通知 + 资源中心 */} -
-
- -
    - {news.map((item) => ( -
  • - - - {formatDate(item.publication_time)} - -   {item.title} - -
  • - ))} - {!news.length && ( -
  • {t("news.empty")}
  • - )} -
+ {/* 三 Tab 功能区 */} +
+
+ {FEATURE_TABS.map((item) => { + const active = item.id === activeTab; + return ( + + ); + })}
-
- -
    - {publications.map((item) => ( -
  • - - - {formatDate(item.publication_time)} - -   {item.title} - -
  • - ))} - {!publications.length && ( -
  • - {t("publication.empty")} -
  • - )} -
-
-
+ - {/* 报告下载(对齐国内首页,非指南推荐) */} -
- - -
-
- ); -} - -function ReportCarousel({ reports }) { - const scrollerRef = useRef(null); - - const scrollBy = (dir) => { - const el = scrollerRef.current; - if (!el) return; - el.scrollBy({ left: dir * 320, behavior: "smooth" }); - }; - - const onDownload = (item) => { - if (!item.file_path || item.file_path === "#") return; - window.open(item.file_path, "_blank"); - }; - - return ( -
- - - -
- {reports.map((item) => ( - - ))} -
+ ) : ( +
+
+

+ {tab.heading} +

+

+ {tab.desc} +

+ + Learn more + +
+ +
+ )} +
+ + + {/* Cooperation:干净品牌蓝 + 六边形纹理,避免素材感过强的实验室底图 */} + +
+

+ Cooperation +

+
+ {COOP_CARDS.map((card) => ( +
+ +

+ {card.title} +

+

+ {card.desc} +

+ + more + + +
+ ))} +
+
+
+ + {/* Partners */} +
+
+

+ Supporters & Partners +

+
+ {PARTNERS.map((name) => ( +
+ {name} +
+ ))} +
+
+
); } diff --git a/src/pages/public/JoinCommitteePage.jsx b/src/pages/public/JoinCommitteePage.jsx new file mode 100644 index 0000000..b2b7386 --- /dev/null +++ b/src/pages/public/JoinCommitteePage.jsx @@ -0,0 +1,139 @@ +import { Link } from "react-router-dom"; +import SiteHero from "@/components/site/SiteHero"; + +const SECTIONS = [ + { + title: "Why join a STAR committee?", + body: "Committees help shape evaluation priorities, review methodology updates, and strengthen the credibility of guideline appraisal. Placeholder content for redesign preview.", + quote: + "Working with STAR committees is a unique opportunity to improve how guidelines are assessed and applied.", + }, + { + title: "What you will do", + bullets: [ + "Review draft reports and methodological proposals", + "Participate in scheduled virtual meetings", + "Advise on dissemination and stakeholder engagement", + ], + quote: + "Your expertise helps keep STAR transparent, rigorous, and useful for decision makers.", + }, + { + title: "How to apply", + body: "Submit a short expression of interest with your background and preferred committee. Placeholder application instructions for redesign preview. For questions, please contact the STAR office.", + }, +]; + +function QuoteBox({ text }) { + return ( + + ); +} + +export default function JoinCommitteePage() { + return ( +
+ {/* Hero + 悬浮介绍卡:absolute 贴 Hero 底边再下移,避开 Layout flex 对负 margin 的影响 */} +
+ +
+
+
+

+ Join a committee +

+

+ Help STAR improve guideline evaluation through advisory + participation. Placeholder intro for the redesign mock. +

+ + View our latest vacancies + +
+
+
+
+ + {/* 给探出卡片留出高度,避免正文顶上来 */} +
+ {SECTIONS.map((section) => ( +
+ {section.quote ? ( +
+
+

+ {section.title} +

+ {section.body && ( +

+ {section.body} +

+ )} + {section.bullets && ( +
    + {section.bullets.map((b) => ( +
  • {b}
  • + ))} +
+ )} +
+ +
+ ) : ( +
+

+ {section.title} +

+

+ {section.body} +

+ + Contact us >> + +
+ )} +
+ ))} +
+
+ ); +} diff --git a/src/pages/public/NewsListPage.jsx b/src/pages/public/NewsListPage.jsx index 7f3d52e..0e624bf 100644 --- a/src/pages/public/NewsListPage.jsx +++ b/src/pages/public/NewsListPage.jsx @@ -1,50 +1,218 @@ import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import SectionTitle from "@/components/site/SectionTitle"; +import SiteHero from "@/components/site/SiteHero"; +import { + HexPatternBg, + PlaceholderMedia, +} from "@/components/site/PlaceholderMedia"; +import { INTL } from "@/config/intlAssets"; import { contentApi } from "@/services"; +/** 后端不可用时的演示数据,避免整页空白 */ +const FALLBACK_NEWS = [ + { + id: "demo-1", + title: "STAR releases updated rating guidelines report", + summary: + "The latest rating guidelines report summarizes appraisal findings and methodology updates for clinical practice guidelines.", + publication_time: "2025-01-12", + }, + { + id: "demo-2", + title: "International collaboration advances trustworthy guideline appraisal", + summary: + "Partners from multiple institutions join STAR workshops to strengthen transparent and applicable guideline evaluation.", + publication_time: "2025-01-08", + }, + { + id: "demo-3", + title: "AI-assisted assessment system enters pilot phase", + summary: + "STAR begins a pilot of its AI-driven assessment workflow to support faster and more consistent guideline reviews.", + publication_time: "2024-12-20", + }, + { + id: "demo-4", + title: "Call for advisory committee members now open", + summary: + "Experts are invited to join STAR advisory committees and contribute to ongoing methodology development.", + publication_time: "2024-12-05", + }, + { + id: "demo-5", + title: "New research publications added to the STAR resource center", + summary: + "Recent publications on multi-dimensional evaluation methods are now available in the research section.", + publication_time: "2024-11-18", + }, + { + id: "demo-6", + title: "STAR training workshop announced for spring", + summary: + "Registration will open soon for the upcoming training and development opportunities focused on guideline appraisal.", + publication_time: "2024-11-02", + }, +]; + +function formatCardDate(value) { + const s = String(value || "2025-01-12").slice(0, 10); + const [y, m, d] = s.split("-"); + return { top: `${m || "01"}-${d || "12"}`, year: y || "2025", full: s }; +} + +/** 列表接口无 summary 字段时,从 content 抽一段纯文本摘要 */ +function newsSummary(item) { + if (item.summary) return item.summary; + if (item.abstract) return item.abstract; + if (!item.content) return ""; + return String(item.content) + .replace(/<[^>]+>/g, " ") + .replace(/\s+/g, " ") + .trim() + .slice(0, 160); +} + export default function NewsListPage() { - const { t } = useTranslation(); - const [loading, setLoading] = useState(true); - const [list, setList] = useState([]); + const [list, setList] = useState(FALLBACK_NEWS); + const [fromApi, setFromApi] = useState(false); useEffect(() => { let alive = true; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 4000); + contentApi .listNews({ limit: 50 }) - .then((res) => alive && setList(res.results || [])) - .finally(() => alive && setLoading(false)); + .then((res) => { + if (!alive) return; + const rows = res?.results || []; + if (rows.length) { + setList(rows); + setFromApi(true); + } + }) + .catch(() => { + /* 保持演示数据 */ + }) + .finally(() => { + clearTimeout(timer); + }); + return () => { alive = false; + clearTimeout(timer); + controller.abort(); }; }, []); + const featured = list.slice(0, 3); + const rest = list.slice(3); + return ( -
- -
    - {loading && ( -
  • {t("common.loading")}
  • - )} - {!loading && - list.map((item) => ( -
  • - - - {String(item.publication_time || "").slice(0, 10)} - -   {item.title} - -
  • - ))} - {!loading && !list.length && ( -
  • {t("news.empty")}
  • - )} -
+
+ + + +
+ {!fromApi && ( +

+ Showing preview content (API unavailable) +

+ )} + + {/* 精选三卡:不再等 loading,避免后端超时导致整页空白 */} +
+ {featured.map((item) => { + const date = formatCardDate(item.publication_time); + const summary = + newsSummary(item) || + "Brief summary placeholder for the news card layout."; + const to = String(item.id).startsWith("demo-") + ? "/newsList" + : `/newsDetail?id=${item.id}`; + return ( +
+ {item.cover || item.image_url ? ( + + ) : ( + + )} +
+
+
+ {date.top} +
+
{date.year}
+
+
+

+ {item.title} +

+

+ {summary} +

+ + more + + +
+
+
+ ); + })} +
+ +
    + {(rest.length ? rest : list).map((item) => { + const date = formatCardDate(item.publication_time); + const summary = + newsSummary(item) || + "Brief description snippet for the news list row."; + const to = String(item.id).startsWith("demo-") + ? "/newsList" + : `/newsDetail?id=${item.id}`; + return ( +
  • + + + {date.full} + + + + {item.title} + + + {summary} + + + + more + + + +
  • + ); + })} +
+
+
); } diff --git a/src/pages/public/PeoplePage.jsx b/src/pages/public/PeoplePage.jsx new file mode 100644 index 0000000..24ff1e1 --- /dev/null +++ b/src/pages/public/PeoplePage.jsx @@ -0,0 +1,122 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import SiteHero from "@/components/site/SiteHero"; +import { + HexPatternBg, + PlaceholderMedia, +} from "@/components/site/PlaceholderMedia"; +import { INTL } from "@/config/intlAssets"; +import { contentApi } from "@/services"; + +const FALLBACK_MEMBERS = [ + { id: 1, name: "Gordon Guyatt", org: "McMaster University, Canada" }, + { id: 2, name: "Gordon Guyatt", org: "McMaster University, Canada" }, + { id: 3, name: "Gordon Guyatt", org: "McMaster University, Canada" }, + { id: 4, name: "Gordon Guyatt", org: "McMaster University, Canada" }, +]; + +export default function PeoplePage() { + const [members, setMembers] = useState(FALLBACK_MEMBERS); + + useEffect(() => { + let alive = true; + contentApi + .listProfessors({ limit: 8 }) + .then((res) => { + if (!alive) return; + const list = res?.results || []; + if (list.length) { + setMembers( + list.slice(0, 4).map((p) => ({ + id: p.id, + name: p.name || p.name_en || "Member", + org: p.organization_name || p.title || "Affiliation", + logo: p.logo, + })), + ); + } + }) + .catch(() => {}); + return () => { + alive = false; + }; + }, []); + + return ( +
+ + + + {/* Mission & Vision */} +
+ Mission & Vision +
+

+ — Mission +

+

+ Promote trustworthy, transparent, and applicable clinical practice + guidelines through rigorous evaluation and open collaboration. + Placeholder mission text for redesign preview. +

+

+ — Vision +

+

+ A world where guideline users can quickly identify recommendations + worth applying. Placeholder vision text for redesign preview. +

+
+
+ + {/* Advisory Committees */} +
+

+ Advisory Committees +

+
+ {members.map((m) => ( +
+ {m.logo ? ( + {m.name} + ) : ( + + )} +
+ {m.name} +
+

{m.org}

+
+ ))} +
+
+ + View team members + + +
+
+
+
+ ); +} diff --git a/src/pages/public/ResearchPage.jsx b/src/pages/public/ResearchPage.jsx new file mode 100644 index 0000000..aaa7bf9 --- /dev/null +++ b/src/pages/public/ResearchPage.jsx @@ -0,0 +1,141 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import SiteHero from "@/components/site/SiteHero"; +import { HexPatternBg } from "@/components/site/PlaceholderMedia"; +import { INTL } from "@/config/intlAssets"; +import { contentApi } from "@/services"; + +const FALLBACK_REPORTS = Array.from({ length: 10 }).map((_, i) => ({ + id: `r-${i}`, + date: "01/01", + title: "report name report name report name", +})); + +const FALLBACK_PUBLICATIONS = Array.from({ length: 10 }).map((_, i) => ({ + id: `p-${i}`, + date: "01/01", + title: "report name report name report name", +})); + +function formatShortDate(value) { + if (!value) return "01/01"; + const s = String(value).slice(0, 10); + const parts = s.split("-"); + if (parts.length >= 3) return `${parts[1]}/${parts[2]}`; + return s; +} + +function ListCard({ title, items, moreTo }) { + return ( +
+
+

+ {title} +

+ + more + + +
+
    + {items.map((item, idx) => ( +
  • + + • {item.date} + {item.title} + + +
  • + ))} +
+
+ ); +} + +export default function ResearchPage() { + const [reports, setReports] = useState(FALLBACK_REPORTS); + const [publications, setPublications] = useState(FALLBACK_PUBLICATIONS); + + useEffect(() => { + let alive = true; + (async () => { + try { + const [reportRes, pubRes] = await Promise.all([ + contentApi.listReports({ limit: 12 }), + contentApi.listPublications({ limit: 12 }), + ]); + if (!alive) return; + const r = reportRes?.results || []; + const p = pubRes?.results || []; + if (r.length) { + setReports( + r.map((item) => ({ + id: item.id, + date: formatShortDate( + item.year ? `${item.year}-01-01` : item.updated_at, + ), + title: item.name || item.title || "Report", + to: + item.file_path && item.file_path !== "#" + ? item.file_path + : "/research", + })), + ); + } + if (p.length) { + setPublications( + p.map((item) => ({ + id: item.id, + date: formatShortDate(item.publication_time), + title: item.title || "Publication", + to: `/publicationDetail?id=${item.id}`, + })), + ); + } + } catch { + /* 使用占位列表 */ + } + })(); + return () => { + alive = false; + }; + }, []); + + return ( +
+ + +
+ + +
+
+
+ ); +} diff --git a/src/router.jsx b/src/router.jsx index 61cf9c0..0a29b74 100644 --- a/src/router.jsx +++ b/src/router.jsx @@ -4,6 +4,10 @@ import PublicLayout from "@/layouts/PublicLayout"; import AdminLayout from "@/layouts/AdminLayout"; import RouteErrorPage from "@/pages/RouteErrorPage"; import HomePage from "@/pages/public/HomePage"; +import PeoplePage from "@/pages/public/PeoplePage"; +import ResearchPage from "@/pages/public/ResearchPage"; +import CooperationPage from "@/pages/public/CooperationPage"; +import JoinCommitteePage from "@/pages/public/JoinCommitteePage"; import ArticleListPage from "@/pages/public/ArticleListPage"; import ArticleDetailPage from "@/pages/public/ArticleDetailPage"; import RankingPage from "@/pages/public/RankingPage"; @@ -67,6 +71,10 @@ const router = createBrowserRouter([ children: [ { index: true, element: }, { path: "index", element: }, + { path: "people", element: }, + { path: "research", element: }, + { path: "cooperation", element: }, + { path: "cooperation/join", element: }, { path: "guidelineAndConsensus", element: }, { path: "fingerpostDetail", element: }, { path: "guideRanking", element: }, diff --git a/tailwind.config.js b/tailwind.config.js index 0c2c4bf..087fc00 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,9 +5,11 @@ export default { extend: { colors: { star: { - primary: "#181b62", - topbar: "#1d2b40", - bg: "#f6f8fc", + primary: "#0b1a3d", + accent: "#3a61c2", + topbar: "#0b1a3d", + bg: "#f4f7fc", + soft: "#e8f1ff", }, }, maxWidth: {