Skip to content
Merged
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
Binary file added .github/pr-assets/events-guide-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-guide-list.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-guide-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions docs/.vitepress/event-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ export const eventSchema = z
schedule: z.string().min(1),
checked: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
reviewRequired: z.boolean().optional(),
artwork: z
.object({
src: z
.string()
.regex(/^\/events\/marks\/[a-z0-9-]+\.(?:png|jpg|webp)$/)
.optional(),
alt: z.string().min(1),
kind: z.enum(['official-mark', 'official-visual', 'project-mark', 'theme']),
source: z.string().min(1),
surface: z.enum(['light', 'dark']),
symbol: z.enum(['code', 'data', 'project', 'book']).optional(),
})
.refine((artwork) => artwork.kind === 'theme' || Boolean(artwork.src), {
message: '正式标识或主视觉必须提供站内图片',
})
.optional(),
overview: z
.object({
format: z.string().min(1),
focus: z.string().min(1),
preparation: z.string().min(1),
deliverable: z.string().min(1),
})
.optional(),
order: z.number(),
ongoing: z.boolean().optional(),
archived: z.boolean().optional(),
Expand Down
107 changes: 107 additions & 0 deletions docs/.vitepress/theme/components/EventArtwork.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { withBase } from 'vitepress';
import type { EventInfo } from '../../events';

const props = defineProps<{ event: EventInfo; eager?: boolean }>();
const failed = ref(false);
watch(
() => props.event.artwork?.src,
() => {
failed.value = false;
},
);
</script>

<template>
<div class="event-artwork" :data-surface="event.artwork?.surface ?? 'light'">
<img
v-if="event.artwork?.src && !failed"
:src="withBase(event.artwork.src)"
:alt="event.artwork.alt"
:loading="eager ? 'eager' : 'lazy'"
decoding="async"
width="720"
height="280"
@error="failed = true"
/>
<div v-else class="event-artwork-fallback">
<svg
viewBox="0 0 64 64"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<template v-if="event.artwork?.symbol === 'book'">
<path
d="M32 17C22 11 12 11 7 13v37c8-2 16-1 25 4 9-5 17-6 25-4V13c-5-2-15-2-25 4Z M32 17v37"
/>
<path d="M14 22c4 0 8 1 11 3m-11 7c4 0 8 1 11 3m14-10c3-2 7-3 11-3m-11 13c3-2 7-3 11-3" />
</template>
<template v-else-if="event.artwork?.symbol === 'data'">
<path d="M10 49h46M16 41V29m16 12V17m16 24V9M12 20l17-9 16-5" />
<circle cx="16" cy="49" r="3" />
<circle cx="32" cy="49" r="3" />
<circle cx="48" cy="49" r="3" />
</template>
<template v-else-if="event.artwork?.symbol === 'project'">
<rect x="8" y="12" width="48" height="35" rx="5" />
<path d="M8 22h48M21 55h22M32 47v8m-8-28-5 6 5 6m16-12 5 6-5 6" />
</template>
<template v-else>
<path d="M23 19 9 32l14 13m18-26 14 13-14 13m-6-33-6 40" />
</template>
</svg>
<strong>{{ event.eyebrow }}</strong>
<small>{{ failed ? '图片暂不可用 · 主题图示' : '本站主题图示' }}</small>
</div>
</div>
</template>

<style scoped>
.event-artwork {
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 140px;
width: 100%;
padding: 18px;
background: #fff;
color: #355391;
border-radius: 10px;
overflow: hidden;
}
.event-artwork[data-surface='dark'] {
background: #20354e;
color: #fff;
}
.event-artwork img {
display: block;
width: 100%;
height: 140px;
object-fit: contain;
}
.event-artwork-fallback {
display: grid;
justify-items: center;
gap: 9px;
text-align: center;
}
.event-artwork-fallback svg {
width: 54px;
height: 54px;
}
.event-artwork-fallback strong {
font-size: 18px;
line-height: 1.4;
}
.event-artwork-fallback small {
font-size: 10px;
font-weight: 400;
opacity: 0.7;
}
</style>
40 changes: 22 additions & 18 deletions docs/.vitepress/theme/components/EventCenter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { withBase } from 'vitepress';
import { data as events } from '../../events.data';
import { eventKinds, eventStatus, filterEvents } from '../../events';
import { useEventClock } from '../useEventClock';
import EventArtwork from './EventArtwork.vue';

const query = ref('');
const kind = ref('');
Expand Down Expand Up @@ -206,26 +207,29 @@ const switchArchive = (value: boolean) => {
<article v-for="event in visible.slice(0, limit)" :key="event.url" class="event-card">
<a
class="event-cover"
:class="`cover-${event.cover}`"
:class="[`cover-${event.cover}`, { 'has-artwork': event.artwork }]"
:href="withBase(event.url)"
:aria-label="`查看${event.title}详情`"
><span class="cover-category">{{ event.kind }}</span
><span class="cover-decoration" aria-hidden="true">{{
event.cover === 'wiki'
? '{ }'
: event.cover === 'ladder'
? '↗'
: event.cover === 'blue'
? '⌘'
: event.cover === 'fest'
? '✳'
: event.cover === 'summer'
? '☼'
: '&lt;/&gt;'
}}</span
><span class="cover-word">{{ event.eyebrow }}</span
><span class="cover-footer"
>{{ event.tags[0] }} <span aria-hidden="true">↗</span></span
><span class="cover-category">{{ event.kind }}</span>
<EventArtwork v-if="event.artwork" :event="event" />
<template v-else
><span class="cover-decoration" aria-hidden="true">{{
event.cover === 'wiki'
? '{ }'
: event.cover === 'ladder'
? '↗'
: event.cover === 'blue'
? '⌘'
: event.cover === 'fest'
? '✳'
: event.cover === 'summer'
? '☼'
: '&lt;/&gt;'
}}</span
><span class="cover-word">{{ event.eyebrow }}</span
><span class="cover-footer"
>{{ event.tags[0] }} <span aria-hidden="true">↗</span></span
></template
></a
>
<div class="event-card-body">
Expand Down
152 changes: 112 additions & 40 deletions docs/.vitepress/theme/components/EventDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,146 @@ import { computed } from 'vue';
import { useData, withBase } from 'vitepress';
import { eventStatus, type EventInfo } from '../../events';
import { useEventClock } from '../useEventClock';
import EventArtwork from './EventArtwork.vue';

const { frontmatter } = useData();
const event = computed(() => frontmatter.value.event as EventInfo);
const now = useEventClock();
const fields = computed(() => [
['参与形式', event.value.overview?.format ?? event.value.kind],
['主要内容', event.value.overview?.focus ?? event.value.tags.join('、')],
['建议基础', event.value.overview?.preparation ?? '按所选方向准备'],
['学习成果', event.value.overview?.deliverable ?? '参与记录与复盘'],
]);
const artworkLabels = {
'official-mark': '官方标识',
'official-visual': '官方主视觉',
'project-mark': '所属项目 / 系列标识',
theme: '本站主题图示',
};
</script>

<template>
<div class="event-detail-summary">
<a :href="withBase('/events/')">← 返回活动与竞赛中心</a>
<dl>
<div>
<dt>收录范围</dt>
<dd>{{ event.scope === 'series' ? '常设赛事 / 活动入口' : '具体活动' }}</dd>
</div>
<div v-if="event.aliases?.length">
<dt>简称与检索词</dt>
<dd>{{ event.aliases.join(' / ') }}</dd>
</div>
<div>
<dt>活动类型</dt>
<dd>{{ event.kind }}</dd>
</div>
<div>
<dt>当前状态</dt>
<dd>{{ eventStatus(event, now) }}</dd>
</div>
<div>
<dt>时间安排</dt>
<dd>{{ event.schedule }}</dd>
</div>
<div>
<dt>资料核查</dt>
<dd>{{ event.checked }}</dd>
<section class="event-guide-profile" aria-label="活动信息">
<figure class="event-guide-cover">
<EventArtwork :event="event" eager />
<figcaption v-if="event.artwork">
<span>{{ artworkLabels[event.artwork.kind] }}</span>
{{ event.artwork.source }}
</figcaption>
</figure>
<div class="event-guide-facts">
<div class="event-guide-status">
<span>{{ event.scope === 'series' ? '常设活动指南' : '当届活动指南' }}</span>
<span :class="{ pending: event.reviewRequired }">{{ eventStatus(event, now) }}</span>
</div>
</dl>
<p>本页提供活动线索与准备建议。具体资格、报名与安排需向主办方确认。</p>
</div>
<dl>
<div v-for="[label, value] in fields" :key="label">
<dt>{{ label }}</dt>
<dd>{{ value }}</dd>
</div>
</dl>
<p v-if="event.aliases?.length" class="event-guide-aliases">
简称与检索词:{{ event.aliases.join(' / ') }}
</p>
</div>
<p class="event-guide-note">
建议基础与学习产出由本站整理;参赛资格、报名和评审规则以主办方当届通知为准。
<a :href="withBase('/events/')">返回活动目录 →</a>
</p>
</section>
</template>

<style scoped>
.event-detail-summary {
margin: 24px 0;
.event-guide-profile {
display: grid;
grid-template-columns: minmax(180px, 1fr) minmax(0, 1.7fr);
gap: 24px;
padding: 24px;
margin: 24px 0 32px;
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
background: var(--vp-c-bg-soft);
}
.event-guide-cover {
display: flex;
flex-direction: column;
gap: 12px;
margin: 0;
min-width: 0;
}
.event-guide-cover figcaption {
color: var(--vp-c-text-2);
font-size: 11px;
line-height: 1.65;
}
.event-guide-cover figcaption span {
display: block;
color: var(--vp-c-text-1);
font-weight: 600;
margin-bottom: 3px;
}
.event-guide-facts {
min-width: 0;
}
.event-guide-status {
display: flex;
flex-wrap: wrap;
gap: 8px;
color: var(--vp-c-text-2);
font-size: 12px;
}
.event-guide-status span {
padding: 2px 9px;
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
}
.event-guide-status .pending {
color: var(--vp-c-warning-1);
background: var(--vp-c-warning-soft);
border-color: transparent;
}
dl {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin: 24px 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px 20px;
margin: 20px 0;
}
dt {
font-size: 12px;
color: var(--vp-c-text-2);
}
dd {
margin: 4px 0 0;
margin: 5px 0 0;
font-size: 14px;
font-weight: 600;
line-height: 1.75;
overflow-wrap: anywhere;
}
.event-detail-summary p {
font-size: 12px;
.event-guide-aliases,
.event-guide-note {
margin: 0;
color: var(--vp-c-text-2);
margin-bottom: 0;
font-size: 12px;
line-height: 1.8;
overflow-wrap: anywhere;
}
@media (max-width: 480px) {
dl {
.event-guide-note {
grid-column: 1 / -1;
padding-top: 16px;
border-top: 1px solid var(--vp-c-divider);
}
.event-guide-note a {
white-space: nowrap;
}
@media (max-width: 600px) {
.event-guide-profile {
grid-template-columns: 1fr;
gap: 20px;
padding: 18px;
}
.event-guide-cover {
gap: 8px;
}
}
</style>
Loading
Loading