HomePage.tsx 13.9 KB
import { useState, useEffect, useId, type CSSProperties } from "react";
import { Link } from "react-router";
import type { LucideIcon } from "lucide-react";
import {
  Share2,
  Compass,
  Library,
  Globe,
  BookOpen,
  Wrench,
} from "lucide-react";
import { KIOSK_EVENT, loadWelcome, type WelcomeMessages } from "../kioskStorage";
import { useI18n } from "../i18n";

/** 中 / 英 / 藏 欢迎语(与「语言切换」按钮及轮播同步) */
const WELCOME_MESSAGES = [
  {
    htmlLang: "zh-CN",
    label: "中文",
    text: "欢迎来到稻城天文台信息查询",
  },
  {
    htmlLang: "en",
    label: "English",
    text: "Welcome to the Daocheng Observatory Information Query",
  },
  {
    htmlLang: "bo",
    label: "བོད་ཡིག",
    text: "འདབ་ཆུ་གནམ་གཟིགས་ལྟེ་གནས་ཀྱི་ཆ་འཕྲིན་འཚོལ་ཞིབ་ལ་ཕེབས་པར་དགའ་བསུ་ཞུ།",
  },
] as const;

type MenuItem = {
  id: string;
  label: string;
  icon: LucideIcon;
  to?: string;
  href?: string;
};

/**
 * 六块渐变:与品牌 Logo 配色一致(天蓝 / 青柠绿 / 金橙 / 青绿 / 赭红 / 深金琥珀)
 * 浅→深,对角 bg-gradient-to-br
 */
const TILE_GRADIENTS = [
  "from-[#5DDBFF] via-[#29ABE2] to-[#1680C4]",
  "from-[#B8E85A] via-[#8DC63F] to-[#5FA028]",
  "from-[#FFD078] via-[#E8A33D] to-[#C67A18]",
  "from-[#6EC4B0] via-[#4D9E87] to-[#2F7562]",
  "from-[#D47868] via-[#A64D3D] to-[#6E3428]",
  "from-[#F0B84A] via-[#D97E1A] to-[#9A5A10]",
] as const;

function ArcMenuTile({
  item,
  index,
  total,
}: {
  item: MenuItem;
  index: number;
  total: number;
}) {
  const Icon = item.icon;
  const clipUid = useId().replace(/:/g, "");
  const gradient = TILE_GRADIENTS[index] ?? TILE_GRADIENTS[TILE_GRADIENTS.length - 1];

  const isOuterLeft = index === 0;
  const isOuterRight = index === total - 1;
  const isInnerLeftTrap = index === 1;
  const isInnerRightTrap = index === total - 2;
  const isOuterEdge = isOuterLeft || isOuterRight;
  const isInnerTrap = isInnerLeftTrap || isInnerRightTrap;
  /** 中间两块矩形(第 3、4 入口):去掉底部深色叠层,避免倒影里出现黑色遮罩感 */
  const isCenterPair = index === 2 || index === 3;
  const trapReflectClass = isOuterEdge
    ? "tile-reflect--trap-outer"
    : isInnerTrap
      ? "tile-reflect--trap-inner"
      : "";

  const clipLeftId = `tile-arc-l-${clipUid}`;
  const clipRightId = `tile-arc-r-${clipUid}`;
  const clipInnerLId = `tile-arc-il-${clipUid}`;
  const clipInnerRId = `tile-arc-ir-${clipUid}`;

  /**
   * 中间块高 Hc = 宽×4/3。内梯(2、5)短边 0.868·Hi = Hc,长边 0.932·Hi。
   * 外梯(1、6)短边 0.82·Ho 与内梯长边对齐 → Ho = Hc×0.932/(0.868×0.82)。
   * 第 3、4 块:矩形 aspect 3/4。
   */
  const faceShellClass = isOuterEdge
    ? "relative h-[calc(min(40vw,9.5rem)*4/3*0.932/0.868/0.82)] w-[min(38vw,9rem)] overflow-hidden shadow-[0_8px_22px_rgba(0,0,0,0.42)] ring-1 ring-white/30 sm:h-[calc(min(36vw,10rem)*4/3*0.932/0.868/0.82)] sm:w-[9.5rem] md:h-[calc(10.25rem*4/3*0.932/0.868/0.82)] md:w-[9.85rem]"
    : isInnerTrap
      ? "relative h-[calc(min(40vw,9.5rem)*4/3/0.868)] w-[min(40vw,9.5rem)] overflow-hidden shadow-[0_8px_22px_rgba(0,0,0,0.42)] ring-1 ring-white/30 sm:h-[calc(min(36vw,10rem)*4/3/0.868)] sm:w-[min(36vw,10rem)] md:h-[calc(10.25rem*4/3/0.868)] md:w-[10.25rem]"
      : "relative aspect-[3/4] w-[min(40vw,9.5rem)] overflow-hidden rounded-none shadow-[0_8px_22px_rgba(0,0,0,0.42)] ring-1 ring-white/30 sm:w-[min(36vw,10rem)] md:w-[10.25rem]";

  const faceClipStyle: CSSProperties | undefined = isOuterLeft
    ? { clipPath: `url(#${clipLeftId})` }
    : isOuterRight
      ? { clipPath: `url(#${clipRightId})` }
      : isInnerLeftTrap
        ? { clipPath: `url(#${clipInnerLId})` }
        : isInnerRightTrap
          ? { clipPath: `url(#${clipInnerRId})` }
          : undefined;

  const face = (
    <>
      {isOuterLeft ? (
        <svg className="pointer-events-none absolute h-0 w-0" aria-hidden focusable={false}>
          <defs>
            <clipPath id={clipLeftId} clipPathUnits="objectBoundingBox">
              <polygon points="0.03,0.027 1,0.09 1,0.91 0.03,0.973" />
            </clipPath>
          </defs>
        </svg>
      ) : null}
      {isOuterRight ? (
        <svg className="pointer-events-none absolute h-0 w-0" aria-hidden focusable={false}>
          <defs>
            <clipPath id={clipRightId} clipPathUnits="objectBoundingBox">
              <polygon points="0,0.09 0.97,0.027 0.97,0.973 0,0.91" />
            </clipPath>
          </defs>
        </svg>
      ) : null}
      {isInnerLeftTrap ? (
        <svg className="pointer-events-none absolute h-0 w-0" aria-hidden focusable={false}>
          <defs>
            <clipPath id={clipInnerLId} clipPathUnits="objectBoundingBox">
              <polygon points="0.02,0.034 1,0.066 1,0.934 0.02,0.966" />
            </clipPath>
          </defs>
        </svg>
      ) : null}
      {isInnerRightTrap ? (
        <svg className="pointer-events-none absolute h-0 w-0" aria-hidden focusable={false}>
          <defs>
            <clipPath id={clipInnerRId} clipPathUnits="objectBoundingBox">
              <polygon points="0,0.066 0.98,0.034 0.98,0.966 0,0.934" />
            </clipPath>
          </defs>
        </svg>
      ) : null}
      <div
        className={`menu-tile-face ${isOuterEdge ? "menu-tile--outer" : isInnerTrap ? "menu-tile--inner-trap" : "menu-tile--center"} ${faceShellClass}`}
        style={faceClipStyle}
      >
        <div
          className={`absolute inset-0 bg-gradient-to-br ${gradient} saturate-[1.15]`}
        />
        <div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-white/30 via-transparent to-transparent" />
        <div
          className="pointer-events-none absolute inset-0 opacity-[0.38]"
          style={{
            backgroundImage: `
            linear-gradient(135deg, transparent 41%, rgba(255,255,255,0.16) 50%, transparent 59%),
            linear-gradient(45deg, transparent 41%, rgba(255,255,255,0.11) 50%, transparent 59%)
          `,
          }}
        />
        {!isCenterPair ? (
          <div
            className="pointer-events-none absolute inset-0 opacity-90"
            style={{
              background:
                "linear-gradient(118deg, transparent 0%, transparent 38%, rgba(0,0,0,0.12) 52%, rgba(0,0,0,0.28) 100%)",
            }}
          />
        ) : null}
        <div className="menu-tile-content relative z-10 flex h-full flex-col items-center justify-center gap-3.5 px-1.5 pb-1.5 pt-1.5 max-md:gap-1.5 max-md:px-1 max-md:py-1 sm:gap-4 sm:px-2 sm:pb-2 sm:pt-2 md:gap-5">
          <div
            className={`clip-hexagon flex max-md:h-[2.6rem] max-md:w-[2.6rem] shrink-0 items-center justify-center bg-white/[0.26] ring-1 ring-white/45 backdrop-blur-[2px] ${isOuterEdge ? "h-[3.1rem] w-[3.1rem] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.55),inset_0_1px_0_rgba(255,255,255,0.3),0_0_18px_rgba(255,255,255,0.22)] sm:h-[3.55rem] sm:w-[3.55rem]" : "h-[3.2rem] w-[3.2rem] shadow-[inset_0_1px_0_rgba(255,255,255,0.45)] sm:h-[3.75rem] sm:w-[3.75rem]"}`}
          >
            <Icon
              className={`text-white drop-shadow-[0_2px_6px_rgba(0,0,0,0.35)] max-md:h-[1.15rem] max-md:w-[1.15rem] ${isOuterEdge ? "h-[1.45rem] w-[1.45rem] sm:h-[1.65rem] sm:w-[1.65rem]" : "h-[1.5rem] w-[1.5rem] sm:h-8 sm:w-8"}`}
              strokeWidth={1.55}
            />
          </div>
          <span className="max-w-[8rem] text-center text-[10px] font-semibold leading-tight tracking-wide text-white drop-shadow-[0_1px_3px_rgba(0,0,0,0.45)] max-md:max-w-[6.5rem] max-md:leading-snug sm:max-w-[9rem] sm:text-sm">
            {item.label}
          </span>
        </div>
      </div>
    </>
  );

  const wrapped = (
    <div className="carousel-tile-3d shrink-0 overflow-visible">
      <div
        className={`tile-reflect relative mx-auto w-max max-md:mb-1 mb-10 md:mb-[5.5rem]${trapReflectClass ? ` ${trapReflectClass}` : ""}${isCenterPair ? " tile-reflect--center-pair" : ""}`}
      >
        {face}
      </div>
    </div>
  );

  if (item.href) {
    return (
      <a
        href={item.href}
        target="_blank"
        rel="noopener noreferrer"
        className="block min-h-0 min-w-0 touch-manipulation overflow-visible outline-none focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent"
      >
        {wrapped}
      </a>
    );
  }
  return (
    <Link
      to={item.to ?? "/"}
      className="block min-h-0 min-w-0 touch-manipulation overflow-visible outline-none focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent"
    >
      {wrapped}
    </Link>
  );
}

function MenuCarouselArc() {
  const { t } = useI18n();
  const MENU_ITEMS: MenuItem[] = [
    { id: "tour", label: t("nav.tour"), icon: Share2, to: "/tour" },
    { id: "planetarium", label: t("nav.planetarium"), icon: Compass, to: "/planetarium" },
    { id: "wiki", label: t("nav.wiki"), icon: Library, to: "/search" },
    { id: "web", label: t("nav.web"), icon: Globe, to: "/web" },
    { id: "guide", label: t("nav.guide"), icon: BookOpen, to: "/guide" },
    { id: "maintain", label: t("nav.maintain"), icon: Wrench, to: "/admin" },
  ];
  return (
    <div className="home-menu-carousel relative mx-auto w-full min-w-0 max-w-[min(100%,88rem)] px-2 pb-4 max-md:px-1.5 sm:px-4 sm:pb-12 md:pb-20 md:max-xl:pb-24">
      <div className="px-0 sm:px-1">
        {/* 手机:两列网格;md+:横向一排(平板/桌面),xl+ 与 index.css 平板段配合 */}
        <div className="carousel-track mx-auto grid w-full min-w-0 grid-cols-2 justify-items-center gap-x-2 gap-y-2 overflow-visible py-2 max-md:max-w-[20rem] md:flex md:max-w-[85rem] md:flex-row md:items-center md:justify-center md:gap-5 md:overflow-x-auto md:overflow-y-auto md:py-4 lg:gap-6 lg:py-6 xl:overflow-visible">
          {MENU_ITEMS.map((item, i) => (
            <div
              key={item.id}
              className="flex w-full max-w-[min(100%,9.5rem)] justify-center md:max-w-[min(100%,11.5rem)] md:w-auto md:shrink-0 md:snap-center"
            >
              <ArcMenuTile item={item} index={i} total={MENU_ITEMS.length} />
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function defaultWelcomeFromMessages(): WelcomeMessages {
  return {
    "zh-CN": WELCOME_MESSAGES[0].text,
    en: WELCOME_MESSAGES[1].text,
    bo: WELCOME_MESSAGES[2].text,
  };
}

export function HomePage() {
  const { lang, setLang } = useI18n();
  const [welcomeIdx, setWelcomeIdx] = useState(0);
  const [welcomeTexts, setWelcomeTexts] = useState(() =>
    loadWelcome(defaultWelcomeFromMessages())
  );

  const welcomeRows = WELCOME_MESSAGES.map((m) => ({
    ...m,
    text: welcomeTexts[m.htmlLang as keyof WelcomeMessages],
  }));

  useEffect(() => {
    const sync = () => setWelcomeTexts(loadWelcome(defaultWelcomeFromMessages()));
    window.addEventListener(KIOSK_EVENT, sync);
    window.addEventListener("storage", sync);
    return () => {
      window.removeEventListener(KIOSK_EVENT, sync);
      window.removeEventListener("storage", sync);
    };
  }, []);

  useEffect(() => {
    const i = WELCOME_MESSAGES.findIndex((m) => m.htmlLang === lang);
    if (i >= 0) setWelcomeIdx(i);
  }, [lang]);

  return (
    <div className="flex w-full min-w-0 max-w-full flex-col pl-[max(0.75rem,env(safe-area-inset-left,0px))] pr-[max(0.75rem,env(safe-area-inset-right,0px))] pb-4 pt-8 max-md:pb-3 max-md:pt-6 sm:pb-12 sm:pt-12 md:pl-[max(2rem,env(safe-area-inset-left,0px))] md:pr-[max(2rem,env(safe-area-inset-right,0px))] md:pb-10 md:pt-14">
      <div className="fixed right-3 top-3 z-40 rounded-lg border border-white/15 bg-blue-950/55 px-3 py-2 text-xs text-white shadow-lg backdrop-blur-md sm:right-5 sm:top-5 sm:text-sm">
        <div className="flex items-center gap-2">
          <span className="text-blue-100/90">语言</span>
          <select
            className="rounded-md border border-white/15 bg-slate-900/60 px-2 py-1 text-xs text-white sm:text-sm"
            value={lang}
            onChange={(e) => setLang(e.target.value as typeof lang)}
            aria-label="语言"
          >
            <option value="zh-CN" className="bg-slate-900">
              中文
            </option>
            <option value="en" className="bg-slate-900">
              English
            </option>
            <option value="bo" className="bg-slate-900">
              བོད་ཡིག
            </option>
          </select>
        </div>
      </div>
      {/* 中央标题区 */}
      <header className="mx-auto mb-6 w-full max-w-full scroll-mt-16 text-center max-md:mb-5 sm:mb-24 md:mb-32 landscape:mb-16 landscape:max-md:mb-4 landscape:max-lg:mb-20">
        <h1
          className="mx-auto flex min-h-0 w-full max-w-full shrink-0 items-center justify-center overflow-x-hidden bg-transparent px-1 py-1 text-[clamp(0.95rem,3.4vw,1.35rem)] font-bold leading-snug tracking-wide shadow-none max-md:py-0.5 sm:py-2.5 sm:text-[clamp(1.25rem,4vw,1.875rem)] md:text-[clamp(1.5rem,4.2vw,2.25rem)] md:leading-snug xl:min-h-[3.35rem] xl:overflow-x-auto xl:leading-none xl:text-3xl 2xl:min-h-[5.1rem] 2xl:text-4xl [scrollbar-width:thin]"
          aria-live="polite"
          aria-atomic="true"
        >
          <span
            key={welcomeRows[welcomeIdx].htmlLang}
            lang={welcomeRows[welcomeIdx].htmlLang}
            className={`inline-block max-w-full bg-transparent py-1 text-center text-white shadow-none animate-welcome-fade-in [line-height:1.35] whitespace-normal [overflow-wrap:anywhere] text-balance xl:whitespace-nowrap ${
              welcomeRows[welcomeIdx].htmlLang === "bo"
                ? "font-serif"
                : ""
            }`}
          >
            {welcomeRows[welcomeIdx].text}
          </span>
        </h1>
      </header>

      {/* 单行弧形 3D 轮播 + 玻璃质感块(参考大屏) */}
      <MenuCarouselArc />
    </div>
  );
}