Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/ISSUE_TEMPLATE/event.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: 推荐活动或更新活动信息
description: 为活动与竞赛中心提供公开来源、时间修正或参与经验
title: '[活动] '
body:
- type: input
id: name
attributes:
label: 活动名称
validations:
required: true
- type: dropdown
id: kind
attributes:
label: 活动类型
options:
- 开源活动
- 学科竞赛
- 社区共建
validations:
required: true
- type: input
id: source
attributes:
label: 官方来源链接
description: 提供可公开访问的官网或正式通知地址。
validations:
required: true
- type: textarea
id: details
attributes:
label: 推荐或修正内容
description: 请说明面向对象、年份或届次、报名时间与时区;不确定的信息写待确认。修正已有活动时请附本站页面地址。
validations:
required: true
- type: textarea
id: experience
attributes:
label: 参与经验(可选)
description: 注明参加年份,将个人经验与官方规则分开。不填写个人隐私或非公开材料。
Binary file added .github/pr-assets/events-center-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-assets/events-center-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
node_modules/
.playwright-cli/
output/playwright/
dist/
.vitepress/cache/
.vitepress/dist/
Expand Down
2 changes: 2 additions & 0 deletions .markdownlint-cli2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ config:
- ContentMeta
- ElectiveCourseTable
- HomePage
- EventCenter
- EventDetails
- MaterialResourceList
- ProgrammingLanguageNav
- XingshuCampusMap
Expand Down
35 changes: 35 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ sources:

政策、流程、培养方案、奖助、考试等高风险内容应优先引用公开正式来源。没有来源时,只能提交待核验模板。

## 活动与竞赛中心

活动中心位于 `/events/`,每个活动在 `docs/events/` 下使用独立 Markdown 详情页。普通 Front Matter 之外增加 `event` 字段,列表在构建时自动读取这些字段;不要另建一份卡片数据。

```yaml
event:
kind: 开源活动 # 开源活动 / 学科竞赛 / 社区共建
eyebrow: 活动封面标题
cover: code # summer / code / fest / blue / ladder / wiki
tags: [Git, 项目实践]
beginner: false # 本站入门建议,不代表官方资格
schedule: 当届时间待核实
checked: '2026-09-12' # 资料核查日期
official: https://example.com/
order: 10
```

正文包含活动简介、参与入口、时间说明、准备建议和公开来源,并在标题后放置 `<EventDetails />`。`official` 必须同时出现在页面的 `sources` 中。目录和侧边栏自动读取详情页 Front Matter,无需手动添加重复的导航数据。

只对有来源的时间填写 `registrationOpen`、`registrationClose`、`start`、`end`,格式为含时区的 ISO 8601 时间,例如 `'2026-10-01T09:00:00+08:00'`。开放报名必须同时提供截止时间。未核验的届次使用 `status: needs-review`,不推测截止日期。长期贡献入口可填写 `ongoing: true`;手动归档用 `archived: true`。

状态按报名窗口、活动周期和当前时间计算。到达 `end` 的活动自动进入历史归档;报名截止但未开始的活动仍在目录中,标注“报名已截止”。有开始时间但没有结束时间时,不自动推断“进行中”。年度活动若含延长周期,应在正文解释,不能让“进行中”被误解为可报名。页面打开后每分钟刷新时间状态,无需后台服务。

封面由站内 CSS 图形与文字生成,不需要复制第三方海报。字段校验、状态边界和真实活动来源一致性已纳入 `pnpm check`。活动建议可通过 `.github/ISSUE_TEMPLATE/event.yml` 提交。

## 本地检查

```bash
Expand All @@ -65,3 +90,13 @@ pnpm build
```

不要通过关闭规则、删除测试或忽略错误来让检查通过。

### 活动名称、别名与收录范围

活动详情的 `title` 使用可核验的完整名称,`event.eyebrow` 可使用封面简称,`event.aliases` 保存常见缩写或历史名称。更名须在正文说明对应届次并附来源;不要把旧名称直接覆盖为没有出处的新名称。目录、详情和侧边栏均从详情页 Front Matter 生成。

`event.topic` 表示主要实践方向,用于筛选。优先复用已有方向:程序设计、软件开发、综合计算机、人工智能与数据、网络安全、系统开发、数学建模、机器人与硬件、创新创业、开源实践。

`event.scope: series` 表示常设赛事或活动入口,状态为“查看当届通知”,不可附带 `ongoing`、`archived` 或具体起止时间。`event.scope: edition` 表示具体活动,可按来源填写已核验时间。仅有日期但没有时刻的通知可以写入 `schedule` 和正文,不补写不存在的精确截止时间。

父赛事、子赛项、不同地区和届次不应重复充当独立机会以增加目录数量。来源访问受限时保留明确的待核验说明,研究依据见 [活动收录研究](docs/events/research.md)。
8 changes: 8 additions & 0 deletions docs/.vitepress/event-clock.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare const data: number;
export { data };

export default {
load() {
return Date.now();
},
};
54 changes: 54 additions & 0 deletions docs/.vitepress/event-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { z } from 'zod';

import { eventKinds } from './events';
const timestamp = z.string().datetime({ offset: true });
export const eventSchema = z
.object({
kind: z.enum(eventKinds),
scope: z.enum(['series', 'edition']).optional(),
aliases: z.array(z.string().min(1)).optional(),
topic: z.string().min(1).optional(),
eyebrow: z.string().min(1),
cover: z.enum(['summer', 'code', 'fest', 'blue', 'ladder', 'wiki']),
tags: z.array(z.string().min(1)).min(1),
beginner: z.boolean(),
schedule: z.string().min(1),
checked: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
official: z.string().url(),
order: z.number(),
ongoing: z.boolean().optional(),
archived: z.boolean().optional(),
registrationOpen: timestamp.optional(),
registrationClose: timestamp.optional(),
start: timestamp.optional(),
end: timestamp.optional(),
})
.superRefine((event, context) => {
if (
event.scope === 'series' &&
(event.ongoing ||
event.archived ||
event.registrationOpen ||
event.registrationClose ||
event.start ||
event.end)
) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: '常设入口不得混入具体届次的报名、起止或归档状态',
});
}
for (const [a, b] of [
[event.registrationOpen, event.registrationClose],
[event.start, event.end],
]) {
if (a && b && Date.parse(a) >= Date.parse(b)) {
context.addIssue({ code: z.ZodIssueCode.custom, message: '活动起止时间顺序错误' });
}
}
if (event.registrationOpen && !event.registrationClose) {
context.addIssue({ code: z.ZodIssueCode.custom, message: '开放报名必须提供截止时间' });
}
});

export type EventInfo = z.infer<typeof eventSchema>;
44 changes: 44 additions & 0 deletions docs/.vitepress/event-sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from 'node:fs';
import path from 'node:path';
import matter from 'gray-matter';
import type { DefaultTheme } from 'vitepress';
import { eventSchema } from './event-schema';
import { eventKinds } from './events';

// Build-time only: titles share the same source as the directory and detail pages.
export function eventSidebar(): DefaultTheme.SidebarItem[] {
const root = path.resolve('docs/events');
const entries = fs
.readdirSync(root)
.filter((file) => file.endsWith('.md'))
.flatMap((file) => {
const { data } = matter.read(path.join(root, file));
if (!data.event) return [];
return [
{
title: String(data.title),
link: `/events/${file.slice(0, -3)}`,
event: eventSchema.parse(data.event),
},
];
})
.sort((a, b) => a.event.order - b.event.order);
return [
{
text: '活动与竞赛',
items: [
{ text: '探索活动', link: '/events/' },
{ text: '收录研究与名称核验', link: '/events/research' },
{ text: '推荐活动与补充经验', link: '/events/contribute' },
{ text: '竞赛学习指南', link: '/competitions/' },
],
},
...eventKinds.map((kind) => ({
text: kind,
collapsed: true,
items: entries
.filter((entry) => entry.event.kind === kind)
.map((entry) => ({ text: entry.title, link: entry.link })),
})),
];
}
20 changes: 20 additions & 0 deletions docs/.vitepress/events.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createContentLoader } from 'vitepress';
import { eventSchema } from './event-schema';
import type { EventItem } from './events';

declare const data: EventItem[];
export { data };

export default createContentLoader('events/*.md', {
transform(pages): EventItem[] {
return pages
.filter((page) => page.frontmatter.event)
.map((page) => ({
...eventSchema.parse(page.frontmatter.event),
title: page.frontmatter.title,
description: page.frontmatter.description,
url: page.url,
}))
.sort((a, b) => a.order - b.order);
},
});
67 changes: 67 additions & 0 deletions docs/.vitepress/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { EventInfo } from './event-schema';
export type { EventInfo } from './event-schema';
export const eventKinds = ['开源活动', '学科竞赛', '社区共建'] as const;

export type EventItem = EventInfo & { title: string; description: string; url: string };
export type EventStatus =
| '长期开放'
| '报名中'
| '报名已截止'
| '即将开始'
| '进行中'
| '已结束'
| '查看当届通知'
| '时间待确认';

export function eventStatus(event: EventInfo, now: number): EventStatus {
if (event.scope === 'series') return '查看当届通知';
if (event.archived || (event.end && now >= Date.parse(event.end))) return '已结束';
if (event.ongoing) return '长期开放';
if (
event.registrationOpen &&
event.registrationClose &&
now >= Date.parse(event.registrationOpen) &&
now < Date.parse(event.registrationClose)
)
return '报名中';
if (event.start && event.end && now >= Date.parse(event.start)) return '进行中';
if (event.registrationClose && now >= Date.parse(event.registrationClose)) return '报名已截止';
if (event.start && now < Date.parse(event.start)) return '即将开始';
return '时间待确认';
}

export function filterEvents(
items: EventItem[],
filters: {
query: string;
kind: string;
status: string;
beginner: boolean;
archive: boolean;
topic?: string;
},
now: number,
) {
const words = filters.query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
return items.filter((event) => {
const status = eventStatus(event, now);
const searchable = [
event.title,
event.description,
event.eyebrow,
event.topic,
...(event.aliases ?? []),
...event.tags,
]
.join(' ')
.toLocaleLowerCase();
return (
(filters.archive ? status === '已结束' : status !== '已结束') &&
(!filters.kind || event.kind === filters.kind) &&
(!filters.topic || event.topic === filters.topic) &&
(!filters.status || status === filters.status) &&
(!filters.beginner || event.beginner) &&
words.every((word) => searchable.includes(word))
);
});
}
3 changes: 3 additions & 0 deletions docs/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { DefaultTheme } from 'vitepress';
import { eventSidebar } from './event-sidebar';

export const sidebar: DefaultTheme.Sidebar = {
'/events/': eventSidebar(),
'/start/': [
{
text: '新生专题',
Expand Down Expand Up @@ -62,6 +64,7 @@ export const sidebar: DefaultTheme.Sidebar = {
{
text: '学科竞赛',
items: [
{ text: '活动与竞赛中心', link: '/events/' },
{ text: '总览', link: '/competitions/' },
{ text: '算法类竞赛', link: '/competitions/algorithm' },
{ text: '软件类竞赛', link: '/competitions/software' },
Expand Down
5 changes: 3 additions & 2 deletions docs/.vitepress/site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ export const siteSections: SiteSection[] = [
title: '学业与课程',
},
{
activeMatch: '^/(competitions|development|research|career)/',
activeMatch: '^/(events|competitions|development|research|career)/',
description: '连接竞赛、开发、科研实践与升学就业。',
index: '03',
links: [
{ href: '/events/', title: '活动与竞赛' },
{ href: '/competitions/', title: '学科竞赛' },
{ href: '/development/', title: '开发与工具' },
{ href: '/research/', title: '科研与项目' },
{ href: '/career/', title: '升学与就业' },
],
primaryHref: '/competitions/',
primaryHref: '/events/',
title: '成长与实践',
},
{
Expand Down
Loading
Loading