diff --git a/docs/zh/announcement/index.md b/announcements.md similarity index 100% rename from docs/zh/announcement/index.md rename to announcements.md diff --git a/docs/components/AnnouncementCards.css b/docs/components/AnnouncementCards.css new file mode 100644 index 0000000..b4be559 --- /dev/null +++ b/docs/components/AnnouncementCards.css @@ -0,0 +1,277 @@ +/* 首页公告卡片区 —— 全部使用 Rspress 主题变量,浅色/深色自动适配 */ + +.ann-section { + max-width: 1152px; + margin: 0 auto; + padding: 48px 24px 64px; +} + +/* ---------- 头部 ---------- */ +.ann-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin-bottom: 28px; +} + +.ann-eyebrow { + display: block; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.14em; + color: var(--rp-c-brand); + margin-bottom: 6px; +} + +.ann-title { + margin: 0; + font-size: 28px; + font-weight: 700; + line-height: 1.3; + color: var(--rp-c-text-1); +} + +.ann-subtitle { + margin: 6px 0 0; + font-size: 14px; + color: var(--rp-c-text-2); +} + +.ann-more { + flex-shrink: 0; + font-size: 14px; + font-weight: 500; + color: var(--rp-c-brand); + text-decoration: none; + padding: 8px 16px; + border: 1px solid var(--rp-c-divider); + border-radius: 999px; + transition: border-color 0.25s ease, background-color 0.25s ease; +} + +.ann-more:hover { + border-color: var(--rp-c-brand); + background-color: var(--rp-c-bg-soft); +} + +.ann-more-arrow { + display: inline-block; + margin-left: 6px; + transition: transform 0.25s ease; +} + +.ann-more:hover .ann-more-arrow { + transform: translateX(3px); +} + +.ann-actions { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.ann-create { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 600; + color: var(--rp-c-bg); + background-color: var(--rp-c-brand); + text-decoration: none; + padding: 8px 16px; + border-radius: 999px; + transition: opacity 0.25s ease, transform 0.25s ease; +} + +.ann-create:hover { + opacity: 0.88; + transform: translateY(-1px); +} + +/* ---------- 卡片网格 ---------- */ +.ann-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 16px; +} + +.ann-card { + --ann-lift: 0px; + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px; + min-height: 148px; + border: 1px solid var(--rp-c-divider); + border-radius: 12px; + background-color: var(--rp-c-bg-soft); + text-decoration: none; + transform: translateY(var(--ann-lift)); + transition: + transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), + border-color 0.3s ease, + box-shadow 0.3s ease; +} + +.ann-card:hover { + --ann-lift: -4px; + border-color: var(--rp-c-brand); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); +} + +:root.dark .ann-card:hover { + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); +} + +.ann-card-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.ann-date { + font-size: 13px; + font-weight: 600; + font-variant-numeric: tabular-nums; + color: var(--rp-c-brand); +} + +.ann-badge { + font-size: 12px; + font-weight: 500; + line-height: 1; + padding: 5px 10px; + border-radius: 999px; + white-space: nowrap; +} + +.ann-badge-wechat { + color: #07c160; + background-color: rgba(7, 193, 96, 0.1); +} + +.ann-badge-gitee { + color: #c71d23; + background-color: rgba(199, 29, 35, 0.08); +} + +.ann-badge-other { + color: var(--rp-c-text-2); + background-color: var(--rp-c-bg-mute); +} + +.ann-badge-milestone { + color: var(--rp-c-text-2); + background-color: var(--rp-c-bg-mute); +} + +/* 无链接的里程碑:静态卡片,不可点击 */ +.ann-card-static { + cursor: default; + background-color: var(--rp-c-bg); +} + +.ann-card-static:hover { + --ann-lift: 0px; + border-color: var(--rp-c-divider); + box-shadow: none; +} + +.ann-card-static:hover .ann-card-title { + color: var(--rp-c-text-1); +} + +.ann-card-title { + flex: 1; + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1.5; + color: var(--rp-c-text-1); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + transition: color 0.25s ease; +} + +.ann-card:hover .ann-card-title { + color: var(--rp-c-brand); +} + +.ann-card-foot { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 13px; + color: var(--rp-c-text-2); +} + +.ann-external { + transition: transform 0.25s ease; +} + +.ann-card:hover .ann-external { + transform: translate(2px, -2px); + color: var(--rp-c-brand); +} + +/* ---------- 滚动进入动画:卡片依次浮入 ---------- */ +.ann-card { + opacity: 0; + translate: 0 28px; +} + +.ann-revealed .ann-card { + opacity: 1; + translate: 0 0; + transition: + opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1), + translate 0.55s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), + border-color 0.3s ease, + box-shadow 0.3s ease; + transition-delay: + calc(var(--ann-i) * 70ms), + calc(var(--ann-i) * 70ms), + 0ms, + 0ms, + 0ms; +} + +/* 无障碍:减少动态偏好下直接呈现 */ +@media (prefers-reduced-motion: reduce) { + .ann-card { + opacity: 1; + translate: none; + transition: none; + } + + .ann-revealed .ann-card { + transition-delay: 0ms; + } +} + +/* ---------- 窄屏 ---------- */ +@media (max-width: 640px) { + .ann-section { + padding: 32px 20px 48px; + } + + .ann-header { + flex-direction: column; + align-items: flex-start; + } + + .ann-title { + font-size: 24px; + } + + .ann-grid { + grid-template-columns: 1fr; + } +} diff --git a/docs/components/AnnouncementCards.tsx b/docs/components/AnnouncementCards.tsx new file mode 100644 index 0000000..b0292d1 --- /dev/null +++ b/docs/components/AnnouncementCards.tsx @@ -0,0 +1,121 @@ +import { useEffect, useRef, useState } from 'react'; +import items from './announcements.json'; +import './AnnouncementCards.css'; + +interface Announcement { + title: string; + date: string; + url: string | null; + source: 'wechat' | 'gitee' | 'other' | 'milestone'; +} + +const SOURCE_LABEL: Record = { + wechat: '微信推文', + gitee: 'Gitee', + other: '外部链接', + milestone: '里程碑', +}; + +/** + * 首页公告卡片区。 + * - 数据来自 scripts/gen-announcements.mjs 生成的 announcements.json + * (自动同步 announcement/index.md,无需手工维护) + * - 滚动进入视口时卡片依次浮入(IntersectionObserver + 交错延迟) + * - prefers-reduced-motion 下直接呈现,无动画 + */ +export default function AnnouncementCards({ count = 6 }: { count?: number }) { + const sectionRef = useRef(null); + const [revealed, setRevealed] = useState(false); + + useEffect(() => { + const el = sectionRef.current; + if (!el) return; + // 不支持 IO 的老浏览器直接显示 + if (typeof IntersectionObserver === 'undefined') { + setRevealed(true); + return; + } + const io = new IntersectionObserver( + (entries) => { + if (entries.some((e) => e.isIntersecting)) { + setRevealed(true); + io.disconnect(); + } + }, + { threshold: 0.15 } + ); + io.observe(el); + return () => io.disconnect(); + }, []); + + const list = (items as Announcement[]).slice(0, count); + + return ( +
+
+
+ ANNOUNCEMENTS +

社团公告

+

社团动态与活动纪实,点击卡片查看原文

+
+
+ + 创建公告 + + + 查看全部 + + +
+
+ +
+ {list.map((item, i) => + item.url ? ( + +
+ {item.date && {item.date}} + + {SOURCE_LABEL[item.source]} + +
+

{item.title}

+
+ 阅读原文 + +
+
+ ) : ( +
+
+ {item.date && {item.date}} + + {SOURCE_LABEL[item.source]} + +
+

{item.title}

+
+ 社团里程碑 +
+
+ ) + )} +
+
+ ); +} diff --git a/docs/components/AnnouncementList.css b/docs/components/AnnouncementList.css new file mode 100644 index 0000000..ca18fd0 --- /dev/null +++ b/docs/components/AnnouncementList.css @@ -0,0 +1,97 @@ +/* 公告页全量卡片墙 —— 年份分组 + blur-fade 滚动入场(animate-ui 风格的纯 CSS 实现) */ + +.al-list { + display: flex; + flex-direction: column; + gap: 40px; + padding-bottom: 48px; +} + +/* ---------- 年份标题 ---------- */ +.al-year { + display: flex; + align-items: baseline; + gap: 12px; + margin: 0 0 20px; + padding-bottom: 12px; + border-bottom: 1px solid var(--rp-c-divider); +} + +.al-year-num { + font-size: 26px; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: var(--rp-c-text-1); +} + +.al-year-count { + font-size: 13px; + color: var(--rp-c-text-2); +} + +/* ---------- blur-fade 入场动画(按年份分组触发,组内交错) ---------- */ +.al-year-group .al-year { + opacity: 0; + translate: 0 12px; +} + +.al-year-group .ann-card { + opacity: 0; + translate: 0 20px; + filter: blur(6px); +} + +.al-year-group.al-in .al-year { + opacity: 1; + translate: 0 0; + transition: + opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1), + translate 0.5s cubic-bezier(0.22, 1, 0.36, 1); +} + +.al-year-group.al-in .ann-card { + opacity: 1; + translate: 0 0; + filter: blur(0); + transition: + opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1), + translate 0.55s cubic-bezier(0.22, 1, 0.36, 1), + filter 0.55s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), + border-color 0.3s ease, + box-shadow 0.3s ease; + transition-delay: + calc(80ms + var(--ann-i) * 60ms), + calc(80ms + var(--ann-i) * 60ms), + calc(80ms + var(--ann-i) * 60ms), + 0ms, + 0ms, + 0ms; +} + +/* 无障碍:减少动态偏好下直接呈现 */ +@media (prefers-reduced-motion: reduce) { + .al-year-group .al-year, + .al-year-group .ann-card { + opacity: 1; + translate: none; + filter: none; + } + + .al-year-group.al-in .al-year, + .al-year-group.al-in .ann-card { + transition: none; + transition-delay: 0ms; + } +} + +/* ---------- 窄屏 ---------- */ +@media (max-width: 640px) { + .al-list { + gap: 32px; + } + + .al-year-num { + font-size: 22px; + } +} diff --git a/docs/components/AnnouncementList.tsx b/docs/components/AnnouncementList.tsx new file mode 100644 index 0000000..227c997 --- /dev/null +++ b/docs/components/AnnouncementList.tsx @@ -0,0 +1,122 @@ +import { useEffect, useRef } from 'react'; +import items from './announcements.json'; +import './AnnouncementCards.css'; +import './AnnouncementList.css'; + +interface Announcement { + title: string; + date: string; + url: string | null; + source: 'wechat' | 'gitee' | 'other' | 'milestone'; +} + +const SOURCE_LABEL: Record = { + wechat: '微信推文', + gitee: 'Gitee', + other: '外部链接', + milestone: '里程碑', +}; + +function yearOf(date: string): string { + const m = date.match(/^(\d{4})/); + return m ? m[1] : '更早'; +} + +function Card({ item, order }: { item: Announcement; order: number }) { + const inner = ( + <> +
+ {item.date && {item.date}} + + {SOURCE_LABEL[item.source]} + +
+

{item.title}

+
+ + {item.url ? '阅读原文' : '社团里程碑'} + + {item.url && ( + + )} +
+ + ); + + const style = { ['--ann-i' as string]: order }; + + return item.url ? ( + + {inner} + + ) : ( +
+ {inner} +
+ ); +} + +/** + * 公告页全量卡片墙。 + * - 展示 announcements.json 全部条目,按年份分组(新→旧) + * - 有链接的渲染为可跳转卡片,无链接的里程碑渲染为静态卡片 + * - 每个年份分组滚动进入视口时,卡片依次 blur-fade 浮入 + * (animate-ui 同款效果,纯 CSS 实现,无额外依赖) + */ +export default function AnnouncementList() { + const rootRef = useRef(null); + + useEffect(() => { + const root = rootRef.current; + if (!root || typeof IntersectionObserver === 'undefined') { + root?.querySelectorAll('.al-year-group').forEach((g) => g.classList.add('al-in')); + return; + } + const io = new IntersectionObserver( + (entries) => { + entries.forEach((e) => { + if (e.isIntersecting) { + e.target.classList.add('al-in'); + io.unobserve(e.target); + } + }); + }, + { threshold: 0.1 } + ); + root.querySelectorAll('.al-year-group').forEach((g) => io.observe(g)); + return () => io.disconnect(); + }, []); + + // 按年份分组,保持新→旧顺序;组内序号用于动画交错(封顶 12 张) + const groups: { year: string; list: Announcement[] }[] = []; + (items as Announcement[]).forEach((it) => { + const y = yearOf(it.date); + const g = groups.find((g) => g.year === y); + if (g) g.list.push(it); + else groups.push({ year: y, list: [it] }); + }); + + return ( +
+ {groups.map((g) => ( +
+

+ {g.year} + {g.list.length} 条 +

+
+ {g.list.map((item, i) => ( + + ))} +
+
+ ))} +
+ ); +} diff --git a/docs/components/CreateAnnouncement.css b/docs/components/CreateAnnouncement.css new file mode 100644 index 0000000..d8d8720 --- /dev/null +++ b/docs/components/CreateAnnouncement.css @@ -0,0 +1,271 @@ +/* 创建公告表单页 —— 复用 ann-card 卡片样式,同样全部走主题变量 */ + +.ca-wrap { + max-width: 960px; + margin: 0 auto; + padding: 16px 0 48px; +} + +.ca-body { + display: grid; + grid-template-columns: 1fr 340px; + gap: 24px; + align-items: start; +} + +/* ---------- 表单 ---------- */ +.ca-form { + display: flex; + flex-direction: column; + gap: 20px; + padding: 28px; + border: 1px solid var(--rp-c-divider); + border-radius: 12px; + background-color: var(--rp-c-bg-soft); +} + +.ca-field { + display: flex; + flex-direction: column; + gap: 8px; +} + +.ca-label { + font-size: 14px; + font-weight: 600; + color: var(--rp-c-text-1); +} + +.ca-req { + font-size: 12px; + font-weight: 500; + color: var(--rp-c-brand); + margin-left: 4px; +} + +.ca-input { + font-size: 14px; + line-height: 1.5; + padding: 10px 14px; + border: 1px solid var(--rp-c-divider); + border-radius: 8px; + background-color: var(--rp-c-bg); + color: var(--rp-c-text-1); + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.ca-input:focus { + border-color: var(--rp-c-brand); + box-shadow: 0 0 0 3px var(--rp-c-brand-lighter, rgba(52, 81, 178, 0.15)); +} + +.ca-input::placeholder { + color: var(--rp-c-text-3, var(--rp-c-text-2)); + opacity: 0.6; +} + +.ca-help { + margin: 0; + font-size: 12px; + color: var(--rp-c-text-2); +} + +.ca-error { + margin: 0; + font-size: 12px; + color: #e5484d; +} + +/* ---------- 操作按钮 ---------- */ +.ca-actions { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; +} + +.ca-btn-primary { + font-size: 14px; + font-weight: 600; + padding: 10px 22px; + border: none; + border-radius: 999px; + background-color: var(--rp-c-brand); + color: var(--rp-c-bg); + cursor: pointer; + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.ca-btn-primary:hover:not(:disabled) { + opacity: 0.9; + transform: translateY(-1px); +} + +.ca-btn-primary:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.ca-btn-secondary { + font-size: 14px; + font-weight: 500; + padding: 10px 20px; + border: 1px solid var(--rp-c-divider); + border-radius: 999px; + color: var(--rp-c-text-1); + text-decoration: none; + transition: border-color 0.2s ease; +} + +.ca-btn-secondary:hover { + border-color: var(--rp-c-brand); + color: var(--rp-c-brand); +} + +.ca-steps { + margin: 4px 0 0; + padding-left: 20px; + font-size: 13px; + line-height: 2; + color: var(--rp-c-text-2); +} + +.ca-steps code { + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + background-color: var(--rp-c-bg-mute); + color: var(--rp-c-text-1); +} + +.ca-member-link { + margin: 0; + font-size: 13px; + color: var(--rp-c-text-2); +} + +.ca-member-link a { + color: var(--rp-c-brand); + text-decoration: none; + margin-left: 4px; +} + +.ca-member-link a:hover { + text-decoration: underline; +} + +.ca-preview-note { + margin: 0; + font-size: 12px; + line-height: 1.7; + padding: 10px 14px; + border-radius: 8px; + color: var(--rp-c-text-2); + background-color: var(--rp-c-bg-mute); +} + +.ca-preview-note code { + font-size: 12px; + padding: 1px 5px; + border-radius: 4px; + background-color: var(--rp-c-bg); + color: var(--rp-c-text-1); +} + +/* ---------- Fork 检测 ---------- */ +.ca-ghuser-row { + display: flex; + gap: 10px; + align-items: stretch; +} + +.ca-ghuser-row .ca-input { + flex: 1; + min-width: 0; +} + +.ca-btn-check { + flex-shrink: 0; + font-size: 13px; + font-weight: 500; + padding: 0 16px; + border: 1px solid var(--rp-c-divider); + border-radius: 8px; + background-color: var(--rp-c-bg); + color: var(--rp-c-text-1); + cursor: pointer; + transition: border-color 0.2s ease, color 0.2s ease; +} + +.ca-btn-check:hover:not(:disabled) { + border-color: var(--rp-c-brand); + color: var(--rp-c-brand); +} + +.ca-btn-check:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.ca-fork-ok { + margin: 0; + font-size: 12px; + line-height: 1.7; + color: #07c160; +} + +.ca-fork-none { + margin: 0; + font-size: 12px; + line-height: 1.7; + color: var(--rp-c-text-2); +} + +/* ---------- 右侧预览 ---------- */ +.ca-side { + display: flex; + flex-direction: column; + gap: 12px; + position: sticky; + top: 80px; +} + +.ca-side-label { + font-size: 12px; + font-weight: 600; + letter-spacing: 0.1em; + color: var(--rp-c-text-2); +} + +.ca-preview { + opacity: 1; + translate: none; +} + +.ca-preview-empty { + border-style: dashed; +} + +.ca-snippet { + margin: 0; + padding: 16px; + border-radius: 8px; + background-color: var(--rp-c-bg-mute); + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-all; + color: var(--rp-c-text-1); +} + +/* ---------- 窄屏 ---------- */ +@media (max-width: 768px) { + .ca-body { + grid-template-columns: 1fr; + } + + .ca-side { + position: static; + } +} diff --git a/docs/components/CreateAnnouncement.tsx b/docs/components/CreateAnnouncement.tsx new file mode 100644 index 0000000..2d481a8 --- /dev/null +++ b/docs/components/CreateAnnouncement.tsx @@ -0,0 +1,294 @@ +import { useEffect, useState } from 'react'; +import './AnnouncementCards.css'; +import './CreateAnnouncement.css'; + +type Source = 'wechat' | 'gitee' | 'other'; + +const SOURCE_LABEL: Record = { + wechat: '微信推文', + gitee: 'Gitee', + other: '外部链接', +}; + +// GitHub 编辑链接自带身份分流:未 Fork 引导创建 Fork,已 Fork 自动在自己的 +// Fork 中编辑,仓库成员则提示新建分支发 PR —— 一条链接覆盖所有提交者 +const GITHUB_EDIT_URL = + 'https://github.com/TSGU-OSC/website/edit/master/announcements.md'; +// 本地预览时 master 上还没有 announcements.md(要随本功能 PR 合入), +// 链接切到功能分支,否则 404 +const GITHUB_EDIT_URL_PREVIEW = + 'https://github.com/TSGU-OSC/website/edit/new_page/announcements.md'; + +function detectSource(url: string): Source { + if (url.includes('mp.weixin.qq.com')) return 'wechat'; + if (url.includes('gitee.com')) return 'gitee'; + return 'other'; +} + +function todayStr(): string { + const d = new Date(); + return `${d.getFullYear()}.${d.getMonth() + 1}.${d.getDate()}`; +} + +/** + * 创建公告卡片表单页(/announcement/create)。 + * + * 静态站没有后端,新公告必须回流到仓库: + * 表单生成标准 Markdown 片段,提交者 Fork 仓库后粘贴到 + * 根目录 announcements.md 末尾,再向官方仓库发起 PR; + * 合并后 prebuild 钩子会自动把它同步进首页卡片与公告页。 + */ +export default function CreateAnnouncement() { + const [url, setUrl] = useState(''); + const [title, setTitle] = useState(''); + const [date, setDate] = useState(todayStr()); + const [copied, setCopied] = useState(false); + const [editUrl, setEditUrl] = useState(GITHUB_EDIT_URL); + const [isLocalPreview, setIsLocalPreview] = useState(false); + const [ghUser, setGhUser] = useState(''); + const [forkState, setForkState] = useState< + 'idle' | 'checking' | 'exists' | 'none' | 'error' + >('idle'); + + useEffect(() => { + const host = window.location.hostname; + if (host === 'localhost' || host === '127.0.0.1') { + setEditUrl(GITHUB_EDIT_URL_PREVIEW); + setIsLocalPreview(true); + } + }, []); + + // 免鉴权 GitHub API(60 次/小时/IP):检测提交者是否已有 Fork + const checkFork = async () => { + const u = ghUser.trim(); + if (!u || forkState === 'checking') return; + setForkState('checking'); + try { + const res = await fetch( + `https://api.github.com/repos/${encodeURIComponent(u)}/website` + ); + if (res.ok) { + const data = await res.json(); + setForkState(data.fork ? 'exists' : 'none'); + } else if (res.status === 404) { + setForkState('none'); + } else { + setForkState('error'); + } + } catch { + setForkState('error'); + } + }; + + const userForkEditUrl = `https://github.com/${ghUser.trim()}/website/edit/master/announcements.md`; + // 已检测到 Fork → 直达本人 Fork 编辑页(仓库成员也走这里); + // 未检测/无 Fork → 上游编辑链接(GitHub 自动引导建 Fork) + const submitUrl = forkState === 'exists' ? userForkEditUrl : editUrl; + const submitLabel = + forkState === 'exists' ? '在我的 Fork 中编辑 →' : '去 GitHub 提交 →'; + + const urlValid = /^https?:\/\/\S+$/.test(url.trim()); + const ready = urlValid && title.trim().length > 0 && date.trim().length > 0; + const source = detectSource(url.trim()); + const snippet = `### [${title.trim() || '卡片标题'}:${date.trim()}](${url.trim() || 'https://…'})`; + + const copySnippet = async () => { + if (!ready) return; + try { + await navigator.clipboard.writeText(snippet); + } catch { + // 剪贴板 API 不可用时的兜底 + const ta = document.createElement('textarea'); + ta.value = snippet; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + } + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+ {/* 左:表单 */} +
+
+ + setUrl(e.target.value)} + /> +

支持微信公众号 / Gitee 链接,粘贴后自动识别来源

+ {url.trim() !== '' && !urlValid && ( +

链接格式不正确,需要以 http:// 或 https:// 开头

+ )} +
+ +
+ + setTitle(e.target.value)} + /> +
+ +
+ + setDate(e.target.value)} + /> +

默认今天,格式如 2026.9.30,时间范围可写 2026.9.30 - 2026.10.2

+
+ +
+ +
+ { + setGhUser(e.target.value); + setForkState('idle'); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + checkFork(); + } + }} + /> + +
+

+ 所有人(包括仓库管理者)都通过自己的 Fork 提交,保持官方仓库历史干净 +

+ {forkState === 'exists' && ( +

+ ✓ 已找到你的 Fork:{ghUser.trim()}/website。若编辑时提示文件不存在, + 请先在你的 Fork 页面点 Sync fork 同步最新代码 +

+ )} + {forkState === 'none' && ( +

+ 未找到你的 Fork——点下方「去 GitHub 提交」,GitHub 会自动引导你一键创建 +

+ )} + {forkState === 'error' && ( +

检测失败(网络或 API 限流),不影响正常提交

+ )} +
+ +
+ + + {submitLabel} + +
+ + {isLocalPreview && ( +

+ 本地预览模式:提交链接已指向 new_page 分支 + (announcements.md 还未合入 master,正式上线后自动指回 master) +

+ )} + +
    +
  1. 点击「复制 Markdown 片段」
  2. +
  3. + 填写 GitHub 用户名并「检测我的 Fork」:已 Fork 过会直达 + 你自己 Fork 的编辑页;没 Fork 过会自动引导你一键创建 +
  4. +
  5. + 在编辑器里把片段粘贴到文件末尾,点击 + Commit changes +
  6. +
  7. + GitHub 会自动跳转到 PR 页面,确认目标是{' '} + TSGU-OSC/website 的 master,提交即可 +
  8. +
+ +

+ 包括仓库管理者在内,所有人都通过自己的 Fork 提交 PR,官方仓库历史保持干净。 +

+
+ + {/* 右:实时预览 */} + +
+
+ ); +} diff --git a/docs/components/announcements.json b/docs/components/announcements.json new file mode 100644 index 0000000..beb455c --- /dev/null +++ b/docs/components/announcements.json @@ -0,0 +1,236 @@ +[ + { + "title": "开源鸿蒙社2025年度总结", + "date": "2026.2.16", + "url": "https://mp.weixin.qq.com/s/P129Q8HNEw4sA6T_uY95yQ", + "source": "wechat" + }, + { + "title": "COSCon'25 第十届中国开源年会", + "date": "2025.12.31", + "url": "https://mp.weixin.qq.com/s/A3LOU-Fuee5SzHlo5-SmdA", + "source": "wechat" + }, + { + "title": "开源鸿蒙社", + "date": "2025.12.21", + "url": "https://mp.weixin.qq.com/s/-hXizFPHLnVHKgunq002Yg", + "source": "wechat" + }, + { + "title": "赴约京城——共赴鸿蒙技术之约", + "date": "2025.11.13", + "url": "https://mp.weixin.qq.com/s/za3RjPYW7-fFUtnDJM1ZIQ", + "source": "wechat" + }, + { + "title": "鸿蒙新视野,中文代码共话未来——我校软通学院携手政企高层共访中文代码智慧工坊", + "date": "2025.10.30", + "url": "https://mp.weixin.qq.com/s/kz49P5srlHA8tAczIUzbCA", + "source": "wechat" + }, + { + "title": "注入新活力,共码新篇章|开源鸿蒙社纳新圆满落幕", + "date": "2025.10.8", + "url": "https://mp.weixin.qq.com/s/GM03aRxXesQjBaIDcjpNtw", + "source": "wechat" + }, + { + "title": "智创未来展锋芒,慧通世界启新程—软件通信学院携数智渤海人工智能产学协同论坛圆满落幕", + "date": "2025.6.20", + "url": "https://mp.weixin.qq.com/s/zonI_So9IHQo9GGgpw3tZQ", + "source": "wechat" + }, + { + "title": "艺术节-开源鸿蒙游园圆满落幕", + "date": "2025.5.25", + "url": "https://mp.weixin.qq.com/s/FXx7AlPE7ZtFK9qOr9neXA", + "source": "wechat" + }, + { + "title": "2025 CANN训练营线下实操圆满收官:Ascend C开发实操赋能AI人才", + "date": "2025.5.17", + "url": "https://mp.weixin.qq.com/s/Q6CGXFdyK9kcyh7QWyYOxw", + "source": "wechat" + }, + { + "title": "成就表彰|开源鸿蒙社团蓝桥杯省赛喜报", + "date": "2025.5.14", + "url": "https://mp.weixin.qq.com/s/tDwE0W4u5AnsYXPRIPVCWw", + "source": "wechat" + }, + { + "title": "开源鸿蒙社成员圆满结束鑫谷园面试", + "date": "2025.3.17", + "url": "https://mp.weixin.qq.com/s/m5MyFi1SySCbUC3R9rg4Rg", + "source": "wechat" + }, + { + "title": "开源鸿蒙社2024年度总结——沉淀过往,迎来新光", + "date": "2025.1.28", + "url": "https://mp.weixin.qq.com/s/c0JS6S83tA94rPr1bI_EXQ", + "source": "wechat" + }, + { + "title": "第十四届deepin开发者与用户大会", + "date": "2024.12.21", + "url": "https://mp.weixin.qq.com/s/taldILx_10v0IzH72BQZyQ", + "source": "wechat" + }, + { + "title": "2024开放原子开发者大会暨首届开源技术学术大会", + "date": "2024.12.20 - 2024.12.21", + "url": "https://mp.weixin.qq.com/s/FWJbjCUBKZJgZNIgpsfS7Q", + "source": "wechat" + }, + { + "title": "天津华为鲲鹏生态创新中心参观纪实:深度探索技术与应用的无缝对接", + "date": "2024.12.12", + "url": "https://mp.weixin.qq.com/s/n-Q2aYiDVdpGaG6Ys_fLpQ", + "source": "wechat" + }, + { + "title": "开放原子“校园行”Meetup——天津中德站", + "date": "2024.11.16", + "url": "https://mp.weixin.qq.com/s/ajZoU4L4n2RteRLIDI7ErQ", + "source": "wechat" + }, + { + "title": "第九届中国开源年会(COSCon'24)", + "date": "2024.11.2 - 2024.11.3", + "url": "https://mp.weixin.qq.com/s/7GoY-etFRVsZ2FLEKW3rzg?poc_token=HOt_mGejpYvlfV4wmwxDyEaPP3p9fAHmRcUbgdlv", + "source": "wechat" + }, + { + "title": "项目经验分享|BMF字节跳动多媒体框架 刘奇翰:拥抱开源是程序员的浪漫主义", + "date": "2024.10.22", + "url": "https://mp.weixin.qq.com/s/prQk-RFvSsa1fVTDuXu_2g", + "source": "wechat" + }, + { + "title": "开源鸿蒙社与天津科技大学人工智能学院交流:开源—程序员的浪漫主义", + "date": "2024.10.17", + "url": "https://mp.weixin.qq.com/s/L_F4VzPfvBK22IsGVzlu-A", + "source": "wechat" + }, + { + "title": "产教融合信息系统试运行", + "date": "2024.10.12", + "url": null, + "source": "milestone" + }, + { + "title": "开源鸿蒙社AI SIG组荣获华为昇腾AI创新大赛铜奖", + "date": "2024.10.10", + "url": "https://mp.weixin.qq.com/s/V2ljOXZpgHUZHMT0-Id2mw", + "source": "wechat" + }, + { + "title": "开源鸿蒙社2024迎新晚会", + "date": "2024.9.27", + "url": "https://mp.weixin.qq.com/s/Fc2I0YecKWqeZht-kF8ufA", + "source": "wechat" + }, + { + "title": "开源鸿蒙社2024纳新活动", + "date": "2024.9.21 - 2024.9.22", + "url": "https://mp.weixin.qq.com/s/aAVoXFOFfJcUa1ZkVr8qzA", + "source": "wechat" + }, + { + "title": "世界智能产业博览会", + "date": "2024.6.20", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active11/README.md", + "source": "gitee" + }, + { + "title": "开源鸿蒙社门户网站正式上线", + "date": "2024.4.20", + "url": null, + "source": "milestone" + }, + { + "title": "华为昇腾AI创享日", + "date": "2024.3.26", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active10/README.md", + "source": "gitee" + }, + { + "title": "中科院2024开源之夏启动会", + "date": "2024.3.10", + "url": null, + "source": "milestone" + }, + { + "title": "HDD•她力量", + "date": "2024.3.8", + "url": null, + "source": "milestone" + }, + { + "title": "社团管理系统正式上线", + "date": "2024.3.4", + "url": null, + "source": "milestone" + }, + { + "title": "开放原子校源行(北理工站)", + "date": "2024.1.6", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active9/README.md", + "source": "gitee" + }, + { + "title": "如何有效利用服务器资源", + "date": "2023.12.15", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active7/README.md", + "source": "gitee" + }, + { + "title": "华为昇腾AI全国行", + "date": "2023.12.9", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active8/README.md", + "source": "gitee" + }, + { + "title": "Linux入门培训", + "date": "2023.12.8", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active6/README.md", + "source": "gitee" + }, + { + "title": "如何使用开源项目", + "date": "2023.12.5", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active5/README.md", + "source": "gitee" + }, + { + "title": "开源鸿蒙社首届程序员节", + "date": "2023.10.24", + "url": null, + "source": "milestone" + }, + { + "title": "首次技术讲座", + "date": "2023.10.16", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active3/README.md", + "source": "gitee" + }, + { + "title": "开源鸿蒙社2023迎新晚会", + "date": "2023.9.27", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active2/README.md", + "source": "gitee" + }, + { + "title": "开源鸿蒙社正式成立", + "date": "2023.6.25", + "url": null, + "source": "milestone" + }, + { + "title": "Gitee建立时间", + "date": "2022.12.4", + "url": "https://gitee.com/TSGU-OSC/OSC_main/blob/master/event/active1/README.md", + "source": "gitee" + } +] diff --git a/docs/zh/announcement/create.mdx b/docs/zh/announcement/create.mdx new file mode 100644 index 0000000..0d77884 --- /dev/null +++ b/docs/zh/announcement/create.mdx @@ -0,0 +1,7 @@ +import CreateAnnouncement from '../../components/CreateAnnouncement' + +# 创建社团公告 + +填写原文链接与卡片标题即可生成公告卡片。复制生成的 Markdown 片段后,填写你的 GitHub 用户名检测 Fork——已 Fork 过会直接在你自己的 Fork 中编辑,没 Fork 过会自动引导你创建。包括仓库管理者在内,所有人都通过 Fork + PR 提交。合并后卡片会自动同步到官网首页与公告页。 + + diff --git a/docs/zh/announcement/index.mdx b/docs/zh/announcement/index.mdx new file mode 100644 index 0000000..f67a3d4 --- /dev/null +++ b/docs/zh/announcement/index.mdx @@ -0,0 +1,7 @@ +import AnnouncementList from '../../components/AnnouncementList' + +# 社团公告 + +社团动态与活动纪实,点击卡片查看原文。想发布新公告?前往 [创建公告](/announcement/create)。 + + diff --git a/package.json b/package.json index 8b76c27..2864695 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "1.0.0", "private": true, "scripts": { + "predev": "node scripts/gen-announcements.mjs", "dev": "rspress dev", + "prebuild": "node scripts/gen-announcements.mjs", "build": "rspress build", "preview": "rspress preview" }, diff --git a/scripts/gen-announcements.mjs b/scripts/gen-announcements.mjs new file mode 100644 index 0000000..6e98abc --- /dev/null +++ b/scripts/gen-announcements.mjs @@ -0,0 +1,58 @@ +/** + * 从仓库根目录 announcements.md 提取公告列表,生成 + * docs/components/announcements.json 供首页 AnnouncementCards 与 + * 公告页 AnnouncementList 组件使用。 + * + * 维护方式:只改 announcements.md,本脚本随 predev/prebuild 自动执行。 + * + * 支持的行格式: + * ### [标题:2024.9.27](https://...) —— 可跳转的公告文章 + * ### [标题:2024.9.21 - 2024.9.22](https://...) + * ### 标题:2023.6.25 —— 无链接的里程碑(生成静态卡片) + */ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const src = join(root, 'announcements.md'); +const out = join(root, 'docs/components/announcements.json'); + +const md = readFileSync(src, 'utf-8'); + +// ### [标题](链接) 或 ### 标题 +const entryRe = /^###\s+(?:\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|([^\n]+?))\s*$/gm; +// 尾部日期:2024.9.27 / 2024.9.21 - 2024.9.22 / 2023年6月25日 等 +const dateRe = /^(.*?)[::]\s*((?:\d{4}[./\-年]\s*\d{1,2}.*))$/; + +const items = []; +let m; +while ((m = entryRe.exec(md)) !== null) { + const rawTitle = (m[1] ?? m[3]).trim(); + const url = m[2] ? m[2].trim() : null; + + let title = rawTitle; + let date = ''; + const dm = rawTitle.match(dateRe); + if (dm) { + title = dm[1].trim(); + date = dm[2].trim().replace(/\s+/g, ' '); + } + + const source = !url + ? 'milestone' + : url.includes('mp.weixin.qq.com') + ? 'wechat' + : url.includes('gitee.com') + ? 'gitee' + : 'other'; + + items.push({ title, date, url, source }); +} + +// 文件中时间从旧到新排列,展示要最新的在前 +items.reverse(); + +mkdirSync(dirname(out), { recursive: true }); +writeFileSync(out, JSON.stringify(items, null, 2) + '\n', 'utf-8'); +console.log(`[gen-announcements] ${items.length} 条 -> ${out}`); diff --git a/theme/index.tsx b/theme/index.tsx index 04c8be5..7ab1214 100644 --- a/theme/index.tsx +++ b/theme/index.tsx @@ -1,11 +1,20 @@ import Theme from 'rspress/theme'; +import { useLang } from 'rspress/runtime'; +import AnnouncementCards from '../docs/components/AnnouncementCards'; - - +// 在首页 features 区块之后插入公告卡片区(仅中文站点) +const HomeLayout = () => { + const lang = useLang(); + return ( + : null} + /> + ); +}; export default { ...Theme, + HomeLayout, }; export * from 'rspress/theme'; -