> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gravitex.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 网站公告 🔥

> GravitexAI 最新动态、模型更新、价格调整等重要公告

export const NoticeChangelog = () => {
  const noticeApiUrl = 'https://system.gravitex.ai/prod-api/system/notice/open/list?pageNum=1&pageSize=200';
  const getReleaseTime = notice => notice.releaseTime || notice.createTime;
  const formatReleaseDateLabel = notice => {
    const raw = getReleaseTime(notice);
    if (!raw) return '';
    const match = String(raw).match(/(\d{4})-(\d{2})-(\d{2})/);
    if (match) {
      return `（${match[2]}/${match[3]}）`;
    }
    const date = new Date(raw);
    if (Number.isNaN(date.getTime())) return '';
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `（${month}/${day}）`;
  };
  const getNoticeDisplayTitle = notice => {
    const title = (notice.noticeTitle || '').replace(/[（(]\d{1,2}\/\d{1,2}[）)]\s*$/u, '').trim();
    const dateLabel = formatReleaseDateLabel(notice);
    return dateLabel ? `${title}${dateLabel}` : title;
  };
  const sortRows = rows => rows.filter(item => item.status === '0' || item.status === 0).sort((a, b) => new Date(getReleaseTime(b)) - new Date(getReleaseTime(a)));
  const [notices, setNotices] = useState([]);
  const [loading, setLoading] = useState(true);
  const [activeId, setActiveId] = useState('changelog-latest');
  useEffect(() => {
    let ignore = false;
    const load = async () => {
      setLoading(true);
      let rows = [];
      try {
        const response = await fetch(noticeApiUrl);
        if (response.ok) {
          const json = await response.json();
          if (json?.code === 200 && Array.isArray(json?.rows)) {
            rows = sortRows(json.rows);
          }
        }
      } catch {
        rows = [];
      }
      if (ignore) return;
      setNotices(rows);
      setLoading(false);
    };
    load();
    return () => {
      ignore = true;
    };
  }, []);
  const renderContent = content => {
    if (!content) return null;
    if ((/<[a-z][\s\S]*>/i).test(content)) {
      return <div dangerouslySetInnerHTML={{
        __html: content
      }} />;
    }
    return <p>{content}</p>;
  };
  const groupByMonth = items => {
    const map = new Map();
    items.forEach(notice => {
      const date = new Date(getReleaseTime(notice));
      const year = date.getFullYear();
      const month = date.getMonth() + 1;
      const key = `${year}-${month}`;
      const label = `${year}年${month}月`;
      if (!map.has(key)) map.set(key, {
        key,
        label,
        items: []
      });
      map.get(key).items.push(notice);
    });
    return Array.from(map.values()).sort((a, b) => {
      const [ay, am] = a.key.split('-').map(Number);
      const [by, bm] = b.key.split('-').map(Number);
      if (ay !== by) return by - ay;
      return bm - am;
    });
  };
  const latestAnchorId = 'changelog-latest';
  const monthAnchorId = key => `month-${key}`;
  const noticeAnchorId = id => `notice-${id}`;
  const getScrollOffset = () => {
    const navbar = document.getElementById('navbar');
    const banner = document.getElementById('banner');
    let offset = (navbar?.offsetHeight ?? 72) + 20;
    if (banner && banner.offsetHeight > 0) {
      offset += banner.offsetHeight;
    }
    return offset;
  };
  const scrollToAnchor = id => {
    const el = document.getElementById(id);
    if (!el) return;
    const offset = getScrollOffset();
    const top = el.getBoundingClientRect().top + window.scrollY - offset;
    window.scrollTo({
      top: Math.max(0, top),
      behavior: 'smooth'
    });
    setActiveId(id);
  };
  const monthGroups = groupByMonth(notices);
  const TOC_DEPTH0 = 'break-words py-1 block font-medium hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300';
  const TOC_DEPTH0_ACTIVE = 'break-words py-1 block font-medium text-gray-900 dark:text-gray-200';
  const TOC_DEPTH1 = 'group flex items-start break-words py-1 whitespace-pre-wrap text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300';
  const TOC_DEPTH1_ACTIVE = 'group flex items-start break-words py-1 whitespace-pre-wrap text-gray-900 dark:text-gray-200';
  const getTocLinkClass = (depth, isActive) => {
    if (depth === 0) return isActive ? TOC_DEPTH0_ACTIVE : TOC_DEPTH0;
    return isActive ? TOC_DEPTH1_ACTIVE : TOC_DEPTH1;
  };
  const buildTocList = active => {
    const ul = document.createElement('ul');
    ul.id = 'table-of-contents-content';
    ul.className = 'toc';
    const appendLink = (li, id, label, depth, isActive) => {
      const a = document.createElement('a');
      a.href = `#${id}`;
      a.className = getTocLinkClass(depth, isActive);
      if (depth === 1) a.style.paddingLeft = '1rem';
      a.textContent = label;
      a.addEventListener('click', e => {
        e.preventDefault();
        scrollToAnchor(id);
      });
      li.appendChild(a);
    };
    const latestLi = document.createElement('li');
    latestLi.className = 'toc-item relative';
    latestLi.dataset.depth = '0';
    appendLink(latestLi, latestAnchorId, '🔥 最新动态', 0, active === latestAnchorId);
    ul.appendChild(latestLi);
    monthGroups.forEach(group => {
      const monthId = monthAnchorId(group.key);
      const monthLi = document.createElement('li');
      monthLi.className = 'toc-item relative';
      monthLi.dataset.depth = '0';
      appendLink(monthLi, monthId, `📅 ${group.label}`, 0, active === monthId);
      const childUl = document.createElement('ul');
      childUl.className = '';
      group.items.forEach(notice => {
        const id = noticeAnchorId(notice.noticeId);
        const itemLi = document.createElement('li');
        itemLi.className = 'toc-item relative';
        itemLi.dataset.depth = '1';
        appendLink(itemLi, id, getNoticeDisplayTitle(notice), 1, active === id);
        childUl.appendChild(itemLi);
      });
      monthLi.appendChild(childUl);
      ul.appendChild(monthLi);
    });
    return ul;
  };
  const createTocPanel = () => {
    const tocPanel = document.createElement('div');
    tocPanel.id = 'table-of-contents';
    tocPanel.className = 'text-gray-600 text-sm leading-6 w-[16.5rem] overflow-y-auto space-y-2 pb-4 -mt-10 pt-10';
    const nav = document.createElement('nav');
    nav.setAttribute('aria-labelledby', 'changelog-toc-heading');
    const heading = document.createElement('h2');
    heading.id = 'changelog-toc-heading';
    heading.className = 'm-0 font-normal';
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'text-gray-700 dark:text-gray-300 font-medium flex items-center space-x-2 hover:text-gray-900 dark:hover:text-gray-100 transition-colors cursor-pointer';
    const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    icon.setAttribute('width', '16');
    icon.setAttribute('height', '16');
    icon.setAttribute('viewBox', '0 0 16 16');
    icon.setAttribute('fill', 'none');
    icon.setAttribute('stroke', 'currentColor');
    icon.setAttribute('stroke-width', '2');
    icon.setAttribute('class', 'h-3 w-3');
    icon.setAttribute('aria-hidden', 'true');
    icon.innerHTML = '<path d="M2.44434 12.6665H13.5554" stroke-linecap="round" stroke-linejoin="round"></path><path d="M2.44434 3.3335H13.5554" stroke-linecap="round" stroke-linejoin="round"></path><path d="M2.44434 8H7.33323" stroke-linecap="round" stroke-linejoin="round"></path>';
    const title = document.createElement('span');
    title.textContent = '在此页面';
    button.appendChild(icon);
    button.appendChild(title);
    heading.appendChild(button);
    nav.appendChild(heading);
    const listWrap = document.createElement('div');
    listWrap.appendChild(buildTocList(activeId));
    nav.appendChild(listWrap);
    tocPanel.appendChild(nav);
    return tocPanel;
  };
  const mountToc = () => {
    const tocLayout = document.getElementById('table-of-contents-layout');
    if (!tocLayout) return false;
    const tocRoot = document.getElementById('table-of-contents-content');
    if (tocRoot?.parentElement) {
      tocRoot.replaceWith(buildTocList(activeId));
      return true;
    }
    const tocPanel = document.getElementById('table-of-contents');
    if (tocPanel) {
      const listWrap = tocPanel.querySelector('nav > div');
      if (listWrap) {
        listWrap.innerHTML = '';
        listWrap.appendChild(buildTocList(activeId));
        return true;
      }
    }
    if (!tocLayout.querySelector('#table-of-contents')) {
      tocLayout.appendChild(createTocPanel());
      return true;
    }
    return false;
  };
  useEffect(() => {
    if (loading || monthGroups.length === 0) return;
    let cancelled = false;
    let attempts = 0;
    const tryMount = () => {
      if (cancelled) return;
      if (mountToc()) return;
      attempts += 1;
      if (attempts < 40) {
        window.setTimeout(tryMount, 50);
      }
    };
    tryMount();
    return () => {
      cancelled = true;
    };
  }, [loading, notices.length]);
  useEffect(() => {
    if (loading || monthGroups.length === 0) return;
    const tocRoot = document.getElementById('table-of-contents-content');
    if (!tocRoot) return;
    tocRoot.querySelectorAll('a[href^="#"]').forEach(link => {
      const id = link.getAttribute('href')?.slice(1);
      if (!id) return;
      const depth = link.style.paddingLeft ? 1 : 0;
      link.className = getTocLinkClass(depth, activeId === id);
    });
  }, [activeId, loading, notices.length]);
  useEffect(() => {
    if (loading || monthGroups.length === 0) return;
    const headings = document.querySelectorAll('.changelog-anchor');
    if (!headings.length) return;
    const scrollOffset = getScrollOffset();
    const observer = new IntersectionObserver(entries => {
      const visible = entries.filter(entry => entry.isIntersecting).sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
      if (visible[0]?.target?.id) {
        setActiveId(visible[0].target.id);
      }
    }, {
      rootMargin: `-${scrollOffset}px 0px -55% 0px`,
      threshold: [0, 0.25, 1]
    });
    headings.forEach(heading => observer.observe(heading));
    return () => observer.disconnect();
  }, [loading, notices.length]);
  if (loading) return <p>正在加载公告...</p>;
  if (notices.length === 0) return <p>暂无公告</p>;
  return <div className="changelog-main">
      <h2 id={latestAnchorId} className="changelog-anchor flex whitespace-pre-wrap group font-semibold">
        🔥 最新动态
      </h2>

      {monthGroups.map(group => <div key={group.key}>
          <h2 id={monthAnchorId(group.key)} className="changelog-anchor flex whitespace-pre-wrap group font-semibold">
            📅 {group.label}
          </h2>
          {group.items.map(notice => <div key={notice.noticeId}>
              <h3 id={noticeAnchorId(notice.noticeId)} className="changelog-anchor flex whitespace-pre-wrap group font-semibold">
                {getNoticeDisplayTitle(notice)}
              </h3>
              {renderContent(notice.noticeContent)}
            </div>)}
        </div>)}
    </div>;
};

欢迎来到 GravitexAI 的更新日志页面。在这里，您可以了解到我们的最新模型上线、价格调整、功能更新等重要信息。

<Tip>
  **收藏更新**：Ctrl+D 收藏本网页，第一时间获取模型上新和优惠信息！
</Tip>

<NoticeChangelog />

***

<Note>
  **更新频率**：本页面会定期更新，建议收藏并经常查看。所有价格调整和模型更新都会第一时间在此公告。

  **小贴士**：新模型上线初期通常会有特别优惠，建议及时试用体验。
</Note>
