Initial Green_Datura site

This commit is contained in:
2026-05-31 22:12:04 +08:00
commit 43a0f33186
19 changed files with 3222 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.next/
.next-dev-logs/
out/
node_modules/
tsconfig.tsbuildinfo
*.log
.env*
*.zip
deploy-single-file/

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Green_Datura 官网
基于 Next.js App Router 与 Ant Design 的单页官网,包含福州大学至诚学院 Green_Datura 网络安全俱乐部简介、荣誉成果和微信公众号入口。
静态构建产物输出到 `out/`

6
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

13
next.config.mjs Normal file
View File

@@ -0,0 +1,13 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
const projectRoot = path.dirname(fileURLToPath(import.meta.url));
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
trailingSlash: true,
outputFileTracingRoot: projectRoot
};
export default nextConfig;

2069
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "cyber-school-team-site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@ant-design/nextjs-registry": "^1.0.2",
"antd": "^5.25.0",
"next": "^15.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.8.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

605
src/app/globals.css Normal file
View File

@@ -0,0 +1,605 @@
:root {
--primary: #16a34a;
--accent: #52c41a;
--cyan: #0f766e;
--title: #12301f;
--light-title: #12301f;
--muted: #52715d;
--light-muted: rgba(34, 69, 48, 0.82);
--line: #d9e3f0;
--glass-line: rgba(22, 101, 52, 0.16);
--page-bg: #edfdf2;
--surface: #ffffff;
--glass: rgba(255, 255, 255, 0.72);
--glass-strong: rgba(255, 255, 255, 0.86);
--dark: #e4f9ec;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
scroll-padding-top: 78px;
}
body {
margin: 0;
background:
radial-gradient(circle at 12% 8%, rgba(34, 197, 94, 0.18), transparent 30%),
radial-gradient(circle at 82% 24%, rgba(15, 118, 110, 0.1), transparent 28%),
linear-gradient(145deg, #f4fff7 0%, #dff7e8 48%, #f8fff9 100%);
color: var(--light-title);
}
a {
color: inherit;
text-decoration: none;
}
.site-shell {
min-height: 100vh;
background: transparent;
}
.site-header {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
gap: 24px;
height: 64px;
padding: 0 32px;
background: rgba(244, 255, 247, 0.84);
border-bottom: 1px solid var(--glass-line);
box-shadow:
0 18px 45px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.16);
backdrop-filter: blur(18px);
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
flex: 0 0 auto;
min-width: 148px;
color: var(--title);
font-weight: 700;
line-height: 1;
}
.brand-logo {
display: block;
width: 36px;
height: 36px;
object-fit: contain;
flex: 0 0 auto;
filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.24));
}
.kicker-logo {
display: block;
width: 18px;
height: 18px;
object-fit: contain;
filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.28));
}
.site-anchor {
flex: 1 1 auto;
min-width: 0;
}
.site-anchor .ant-anchor {
justify-content: flex-end;
}
.site-anchor .ant-anchor-link {
padding-inline: 12px 0;
}
.site-header .site-anchor .ant-anchor-link-title {
color: rgba(18, 48, 31, 0.72) !important;
font-weight: 600;
transition: color 0.2s ease;
}
.site-header .site-anchor .ant-anchor-link-title:hover,
.site-header .site-anchor .ant-anchor-link-active > .ant-anchor-link-title {
color: #166534 !important;
}
.site-anchor .ant-anchor-ink {
background: rgba(82, 196, 26, 0.58);
}
.hero-section {
position: relative;
display: flex;
align-items: center;
min-height: 640px;
overflow: hidden;
background:
linear-gradient(90deg, rgba(244, 255, 247, 0.98) 0%, rgba(229, 250, 236, 0.9) 36%, rgba(220, 252, 231, 0.56) 74%, rgba(220, 252, 231, 0.24) 100%),
linear-gradient(180deg, rgba(244, 255, 247, 0.18) 0%, rgba(237, 253, 242, 0.82) 100%),
url("/assets/cyber-hero.png");
background-position: center;
background-size: cover;
}
.hero-section::after {
position: absolute;
inset: auto 0 0;
height: 130px;
background: linear-gradient(180deg, rgba(245, 247, 251, 0), var(--page-bg));
content: "";
}
.hero-inner,
.section-inner {
width: min(1180px, calc(100% - 48px));
margin: 0 auto;
}
.hero-inner {
position: relative;
z-index: 1;
padding: 96px 0 118px;
}
.hero-copy {
max-width: 650px;
padding: 30px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.86), rgba(231, 250, 238, 0.68));
border: 1px solid var(--glass-line);
border-radius: 8px;
box-shadow:
0 26px 70px rgba(0, 0, 0, 0.24),
inset 0 1px 0 rgba(255, 255, 255, 0.18),
inset 0 0 24px rgba(255, 255, 255, 0.04);
backdrop-filter: blur(16px);
}
.hero-copy.ant-card,
.intro-copy.ant-card,
.stats-panel.ant-card,
.timeline-panel.ant-card {
color: var(--light-muted);
}
.hero-kicker,
.section-kicker,
.stats-kicker {
color: var(--primary);
font-weight: 700;
}
.hero-kicker {
color: #15803d;
}
.hero-title.ant-typography {
margin: 14px 0 18px;
color: var(--title);
font-size: 64px;
line-height: 1.06;
letter-spacing: 0;
}
.hero-text.ant-typography {
max-width: 590px;
color: rgba(34, 69, 48, 0.8);
font-size: 20px;
line-height: 1.72;
}
.hero-actions {
margin-top: 18px;
}
.ant-btn-primary.ant-btn-color-primary {
color: #ffffff;
background: var(--primary);
border-color: var(--primary);
box-shadow: 0 8px 18px rgba(22, 163, 74, 0.22);
}
.ant-btn-primary.ant-btn-color-primary:not(:disabled):not(.ant-btn-disabled):hover {
color: #ffffff;
background: #15803d;
border-color: #15803d;
}
.hero-secondary-button {
color: #166534;
border-color: rgba(22, 101, 52, 0.24);
background: rgba(255, 255, 255, 0.72);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
}
.section {
padding: 92px 0;
position: relative;
}
.section::before {
position: absolute;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(22, 101, 52, 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(22, 101, 52, 0.06) 1px, transparent 1px);
background-size: 44px 44px;
mask-image: linear-gradient(180deg, transparent, #000 16%, #000 84%, transparent);
content: "";
}
.section-heading {
position: relative;
max-width: 720px;
margin-bottom: 36px;
}
.section-heading .ant-typography {
margin-bottom: 0;
}
.section-heading h2.ant-typography,
.wechat-copy h2.ant-typography {
margin-top: 12px;
color: var(--light-title);
font-size: 38px;
line-height: 1.18;
letter-spacing: 0;
}
.section-heading .ant-typography + .ant-typography,
.wechat-copy .ant-typography + .ant-typography {
margin-top: 14px;
color: var(--light-muted);
font-size: 16px;
line-height: 1.78;
}
.intro-section {
background: linear-gradient(180deg, rgba(237, 253, 242, 0.96), rgba(220, 248, 230, 0.92));
}
.intro-copy {
position: relative;
min-height: 100%;
padding: 34px;
color: var(--light-muted);
background: var(--glass);
border: 1px solid var(--glass-line);
border-radius: 8px;
box-shadow:
0 22px 55px rgba(0, 0, 0, 0.18),
inset 0 1px 0 rgba(255, 255, 255, 0.14);
backdrop-filter: blur(18px);
}
.intro-copy .ant-typography {
color: var(--light-muted);
}
.intro-copy h3.ant-typography {
margin: 0 0 18px;
color: var(--light-title);
font-size: 24px;
}
.intro-copy p.ant-typography {
color: var(--light-muted);
line-height: 1.78;
}
.tag-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 22px;
}
.tag-grid .ant-tag {
display: inline-flex;
align-items: center;
min-height: 32px;
margin: 0;
padding: 0 12px;
font-weight: 600;
background: rgba(34, 197, 94, 0.14);
border-color: rgba(82, 196, 26, 0.4);
}
.culture-grid {
margin-top: 28px;
}
.culture-item {
padding: 16px;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(22, 101, 52, 0.12);
border-radius: 8px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
color: var(--light-muted);
}
.culture-icon {
display: inline-grid;
place-items: center;
width: 42px;
height: 42px;
color: #166534;
background: rgba(82, 196, 26, 0.16);
border-radius: 8px;
}
.culture-item strong {
display: block;
margin-bottom: 5px;
color: var(--light-title);
}
.culture-item p {
margin: 0;
color: var(--light-muted);
line-height: 1.65;
}
.stats-panel {
display: grid;
gap: 16px;
height: 100%;
padding: 30px;
color: #ffffff;
background:
linear-gradient(135deg, rgba(22, 163, 74, 0.86), rgba(15, 118, 110, 0.52)),
rgba(255, 255, 255, 0.1);
border: 1px solid var(--glass-line);
border-radius: 8px;
box-shadow:
0 24px 55px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.16);
backdrop-filter: blur(18px);
}
.stats-kicker {
color: rgba(255, 255, 255, 0.9);
}
.stat-tile {
min-height: 112px;
padding: 20px;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.26);
border-radius: 8px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
.stat-tile .ant-card-body,
.stat-tile-content {
width: 100%;
min-height: 72px;
}
.stat-tile .ant-statistic-title,
.stat-tile .ant-statistic-content {
color: #ffffff;
}
.stat-tile .ant-statistic-title {
margin-bottom: 8px;
}
.honors-section {
background:
linear-gradient(180deg, rgba(228, 249, 236, 0.94) 0%, rgba(239, 253, 244, 0.98) 100%);
}
.timeline-panel {
position: relative;
padding: 34px 34px 10px;
background: var(--glass);
border: 1px solid var(--glass-line);
border-radius: 8px;
box-shadow:
0 22px 55px rgba(0, 0, 0, 0.18),
inset 0 1px 0 rgba(255, 255, 255, 0.14);
backdrop-filter: blur(18px);
}
.timeline-item {
display: grid;
gap: 6px;
padding-bottom: 16px;
}
.timeline-year {
color: var(--primary);
font-weight: 700;
}
.timeline-item strong {
color: var(--light-title);
font-size: 17px;
}
.timeline-item p {
margin: 0;
color: var(--light-muted);
}
.wechat-section {
background:
linear-gradient(135deg, rgba(220, 252, 231, 0.86), rgba(209, 250, 229, 0.74)),
var(--dark);
}
.wechat-inner {
justify-content: space-between;
}
.wechat-copy {
max-width: 660px;
}
.wechat-copy .section-kicker,
.wechat-copy h2.ant-typography {
color: var(--title);
}
.wechat-copy .ant-typography + .ant-typography {
color: var(--light-muted);
}
.wechat-card {
flex: 0 0 320px;
padding: 28px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 8px;
box-shadow:
0 22px 55px rgba(0, 0, 0, 0.24),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
backdrop-filter: blur(16px);
}
.wechat-qrcode-image {
display: block;
width: min(238px, 100%);
height: auto;
border-radius: 8px;
}
.wechat-card span {
color: var(--muted);
font-weight: 600;
}
.site-footer {
color: rgba(18, 48, 31, 0.72);
text-align: center;
background: #dcfce7;
}
@media (max-width: 900px) {
.site-header {
padding: 0 18px;
}
.brand {
min-width: 44px;
}
.brand span {
display: none;
}
.site-anchor {
overflow-x: auto;
scrollbar-width: none;
}
.site-anchor::-webkit-scrollbar {
display: none;
}
.site-anchor .ant-anchor {
min-width: max-content;
}
.site-anchor .ant-anchor-link {
padding-inline: 10px 0;
}
.hero-section {
min-height: 620px;
background-position: 60% center;
}
.hero-title.ant-typography {
font-size: 48px;
}
.hero-text.ant-typography {
font-size: 18px;
}
.section {
padding: 72px 0;
}
.section-heading h2.ant-typography,
.wechat-copy h2.ant-typography {
font-size: 32px;
}
.wechat-inner {
flex-direction: column;
align-items: flex-start;
}
.wechat-card {
flex-basis: auto;
max-width: 340px;
}
}
@media (max-width: 560px) {
html {
scroll-padding-top: 68px;
}
.site-header {
gap: 8px;
}
.site-anchor .ant-anchor-link {
padding-inline: 8px 0;
font-size: 14px;
}
.hero-inner,
.section-inner {
width: min(100% - 32px, 1180px);
}
.hero-section {
min-height: 590px;
}
.hero-inner {
padding: 74px 0 96px;
}
.hero-title.ant-typography {
font-size: 40px;
}
.hero-text.ant-typography {
font-size: 16px;
}
.intro-copy,
.stats-panel,
.timeline-panel {
padding: 22px;
}
.section-heading h2.ant-typography,
.wechat-copy h2.ant-typography {
font-size: 28px;
}
.culture-item-content {
flex-direction: column;
}
.wechat-card {
padding: 20px;
}
}

29
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,29 @@
import type { Metadata } from "next";
import { AntdRegistry } from "@ant-design/nextjs-registry";
import "antd/dist/reset.css";
import "./globals.css";
import { Providers } from "./providers";
export const metadata: Metadata = {
title: "Green_Datura 官网",
description: "展示福州大学至诚学院 Green_Datura 网络安全俱乐部简介、荣誉成果与微信公众号入口。",
icons: {
icon: "/assets/green-datura-logo-mark.png"
}
};
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN">
<body>
<AntdRegistry>
<Providers>{children}</Providers>
</AntdRegistry>
</body>
</html>
);
}

69
src/app/page.tsx Normal file
View File

@@ -0,0 +1,69 @@
"use client";
import { ArrowRightOutlined } from "@ant-design/icons";
import { Anchor, Button, Card, Flex, Layout, Space, Typography } from "antd";
import type { AnchorProps } from "antd";
import { Honors } from "@/components/Honors";
import { TeamIntro } from "@/components/TeamIntro";
import { WeChatSection } from "@/components/WeChatSection";
const { Content, Footer, Header } = Layout;
const { Paragraph, Title } = Typography;
const navItems: AnchorProps["items"] = [
{ key: "home", href: "#home", title: "首页" },
{ key: "intro", href: "#intro", title: "俱乐部简介" },
{ key: "honors", href: "#honors", title: "荣誉成果" },
{ key: "wechat", href: "#wechat", title: "微信公众号" }
];
export default function Home() {
return (
<Layout className="site-shell">
<Header className="site-header">
<a className="brand" href="#home" aria-label="Green_Datura 首页">
<img className="brand-logo" src="/assets/green-datura-logo-mark.png" alt="" />
<span>Green_Datura</span>
</a>
<Anchor
affix={false}
direction="horizontal"
targetOffset={76}
items={navItems}
className="site-anchor"
/>
</Header>
<Content>
<section id="home" className="hero-section">
<div className="hero-inner">
<Card className="hero-copy" variant="borderless" styles={{ body: { padding: 0 } }}>
<Space className="hero-kicker" size={8}>
<img className="kicker-logo" src="/assets/green-datura-logo-mark.png" alt="" />
</Space>
<Title className="hero-title">Green_Datura</Title>
<Paragraph className="hero-text">
2020 3 CTF
</Paragraph>
<Flex gap={12} wrap="wrap" className="hero-actions">
<Button type="primary" size="large" href="#intro" icon={<ArrowRightOutlined />}>
</Button>
<Button size="large" href="#honors" className="hero-secondary-button">
</Button>
</Flex>
</Card>
</div>
</section>
<TeamIntro />
<Honors />
<WeChatSection />
</Content>
<Footer className="site-footer">Green_Datura · </Footer>
</Layout>
);
}

48
src/app/providers.tsx Normal file
View File

@@ -0,0 +1,48 @@
"use client";
import type { ReactNode } from "react";
import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
type ProvidersProps = {
children: ReactNode;
};
export function Providers({ children }: ProvidersProps) {
return (
<ConfigProvider
locale={zhCN}
theme={{
token: {
colorPrimary: "#16a34a",
colorSuccess: "#52c41a",
colorText: "#12301f",
colorBgLayout: "#edfdf2",
colorBgContainer: "rgba(255, 255, 255, 0.82)",
colorBorder: "rgba(22, 101, 52, 0.16)",
borderRadius: 8,
fontFamily:
'Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif'
},
components: {
Button: {
borderRadius: 8,
controlHeight: 42
},
Card: {
borderRadiusLG: 8
},
Anchor: {
linkPaddingBlock: 8,
linkPaddingInlineStart: 12
},
Tag: {
borderRadiusSM: 6
}
}
}}
>
{children}
</ConfigProvider>
);
}

45
src/components/Honors.tsx Normal file
View File

@@ -0,0 +1,45 @@
"use client";
import { Card, Space, Timeline, Typography } from "antd";
import { TrophyOutlined } from "@ant-design/icons";
import { timelineHonors, type HonorLevel } from "@/data/honors";
const { Title } = Typography;
const levelColor: Record<HonorLevel, string> = {
attackDefense: "#52c41a",
ctf: "#1677ff",
vulnerability: "#13c2c2",
campus: "#faad14"
};
export function Honors() {
const timelineItems = timelineHonors.map((honor) => ({
color: levelColor[honor.level],
children: (
<div className="timeline-item">
<span className="timeline-year">{honor.year}</span>
<strong>{honor.event}</strong>
<p>{honor.award}</p>
</div>
)
}));
return (
<section id="honors" className="section honors-section">
<div className="section-inner">
<div className="section-heading">
<Space className="section-kicker" size={8}>
<TrophyOutlined />
</Space>
<Title level={2}></Title>
</div>
<Card className="timeline-panel" variant="borderless" styles={{ body: { padding: 0 } }}>
<Timeline mode="left" items={timelineItems} />
</Card>
</div>
</section>
);
}

View File

@@ -0,0 +1,116 @@
"use client";
import { Card, Col, Flex, Row, Space, Statistic, Tag, Typography } from "antd";
import {
AimOutlined,
BugOutlined,
CodeOutlined,
ExperimentOutlined,
TeamOutlined
} from "@ant-design/icons";
const { Paragraph, Title } = Typography;
const directions = [
"WEB安全研究",
"渗透测试",
"漏洞挖掘",
"代码审计",
"二进制安全",
"移动安全",
"数据安全",
"密码安全"
];
const stats = [
{ title: "成立时间", value: "2020年3月" },
{ title: "研究方向", value: "8+" },
{ title: "CNVD上报", value: "20+" }
];
const cultureItems = [
{ icon: <AimOutlined />, title: "生生不息", text: "Green_Datura 寓意生生不息的希望,俱乐部希望在至诚延续、代代传承。" },
{ icon: <ExperimentOutlined />, title: "实战实践", text: "参与国家级、省级攻防演练、CTF 赛事与漏洞响应平台,把学习转化为真实安全能力。" },
{ icon: <TeamOutlined />, title: "校园守护", text: "在学院网信办老师协作下,参与维护学校网络安全。" }
];
export function TeamIntro() {
return (
<section id="intro" className="section intro-section">
<div className="section-inner">
<div className="section-heading">
<Space className="section-kicker" size={8}>
<BugOutlined />
</Space>
<Title level={2}></Title>
<Paragraph>
Green_Datura 2020
CTF
</Paragraph>
</div>
<Row gutter={[24, 24]} align="stretch">
<Col xs={24} lg={14}>
<Card className="intro-copy" variant="borderless" styles={{ body: { padding: 0 } }}>
<Title level={3}></Title>
<Flex wrap="wrap" gap={10} className="tag-grid">
{directions.map((item) => (
<Tag key={item} color="blue">
{item}
</Tag>
))}
</Flex>
<Paragraph>
WEB
BOSS直聘58CNVD 20
</Paragraph>
<Flex vertical gap={14} className="culture-grid">
{cultureItems.map((item) => (
<Card
className="culture-item"
key={item.title}
size="small"
variant="borderless"
styles={{ body: { padding: 0 } }}
>
<Flex gap={14} align="flex-start" className="culture-item-content">
<span className="culture-icon">{item.icon}</span>
<div>
<strong>{item.title}</strong>
<p>{item.text}</p>
</div>
</Flex>
</Card>
))}
</Flex>
</Card>
</Col>
<Col xs={24} lg={10}>
<Card className="stats-panel" variant="borderless" styles={{ body: { padding: 0 } }}>
<Flex vertical gap={16}>
<Space className="stats-kicker" size={8}>
<CodeOutlined />
Club Profile
</Space>
{stats.map((item) => (
<Card
className="stat-tile"
key={item.title}
size="small"
variant="borderless"
styles={{ body: { padding: 0 } }}
>
<Flex align="center" className="stat-tile-content">
<Statistic title={item.title} value={item.value} />
</Flex>
</Card>
))}
</Flex>
</Card>
</Col>
</Row>
</div>
</section>
);
}

View File

@@ -0,0 +1,56 @@
"use client";
import { Button, Card, Flex, Image, Space, Typography } from "antd";
import { LinkOutlined, WechatOutlined } from "@ant-design/icons";
const { Paragraph, Title } = Typography;
const WECHAT_URL =
"https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=Mzg4NDY5NjIyNw==#wechat_redirect";
export function WeChatSection() {
return (
<section id="wechat" className="section wechat-section">
<Flex className="section-inner wechat-inner" align="center" gap={54}>
<div className="wechat-copy">
<Space className="section-kicker" size={8}>
<WechatOutlined />
</Space>
<Title level={2}></Title>
<Paragraph>
Green_Datura
</Paragraph>
<Button
type="primary"
size="large"
href={WECHAT_URL}
target="_blank"
rel="noopener noreferrer"
icon={<LinkOutlined />}
>
</Button>
</div>
<Card
className="wechat-card"
aria-label="微信公众号二维码"
variant="borderless"
styles={{ body: { padding: 0 } }}
>
<Flex vertical align="center" gap={16}>
<Image
src="/assets/green-datura-wechat-qrcode.png"
alt="Green_Datura 微信公众号二维码"
width={238}
preview={false}
className="wechat-qrcode-image"
/>
<span>访</span>
</Flex>
</Card>
</Flex>
</section>
);
}

101
src/data/honors.ts Normal file
View File

@@ -0,0 +1,101 @@
export type HonorLevel = "attackDefense" | "ctf" | "vulnerability" | "campus";
export type Honor = {
year: string;
event: string;
award: string;
level: HonorLevel;
};
export const timelineHonors: Honor[] = [
{
year: "2025年11月26日",
event: "2025第九届一带一路暨金砖国家技能发展与技术创新大赛 - 第二届网络安全防护治理实战技能区域赛",
award: "省级一等奖",
level: "ctf"
},
{
year: "2024-2025年",
event: "第五届福建省“闽盾杯”网络安全空间大赛",
award: "二等奖 1 项,三等奖 1 项",
level: "ctf"
},
{
year: "2024年12月15日",
event: "2024深育杯大学生网络安全大赛网络攻防赛道",
award: "国家级特等奖",
level: "ctf"
},
{
year: "2024年",
event: "“数字中国创新大赛”数字安全赛道",
award: "铜奖 1 项,优秀奖 3 项",
level: "ctf"
},
{
year: "2024年",
event: "“闽盾-2024”网络安全攻防演练",
award: "优秀攻击方",
level: "attackDefense"
},
{
year: "2024年",
event: "福建省文旅厅攻防演练",
award: "第二名",
level: "attackDefense"
},
{
year: "2024年",
event: "眉山市攻防护网行动",
award: "攻击队排名第六,得分超过 24,000 分",
level: "attackDefense"
},
{
year: "2024年",
event: "福州网信市护行动",
award: "三等奖,得分超过 20,000 分",
level: "attackDefense"
},
{
year: "2023-2024年",
event: "第四届福建省“闽盾杯”网络安全空间大赛",
award: "二等奖 1 项,三等奖 1 项",
level: "ctf"
},
{
year: "2023-2024年",
event: "第 20 届信息安全与对抗技术竞赛",
award: "个人挑战赛一等奖 8 项,另获多项二等奖、三等奖",
level: "ctf"
},
{
year: "2023年",
event: "交通运输部海事局攻防演练",
award: "第一名",
level: "attackDefense"
},
{
year: "2023年",
event: "福建省生态环境护网行动(红队)",
award: "总分超过 7,000 分,排名第二",
level: "attackDefense"
},
{
year: "2023年",
event: "福建省教育攻防护网行动(红队)",
award: "高校组二等奖",
level: "attackDefense"
},
{
year: "2023年",
event: "“闽盾-2023”网络安全攻防演练",
award: "优秀攻击方",
level: "attackDefense"
},
{
year: "2023年",
event: "福州市攻防护网行动(红队)",
award: "攻击队三等奖,得分超过 10,000 分",
level: "attackDefense"
}
];

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}