新版首页样式

This commit is contained in:
2026-03-01 23:39:08 +08:00
parent c9ff0c0901
commit 16b0ad38f3
8 changed files with 1846 additions and 537 deletions
+358
View File
@@ -0,0 +1,358 @@
/* =========================================
工作室介绍页 - 旧首页视觉
========================================= */
.site-main {
position: relative;
}
.hero-section {
padding: 40px 0;
background-color: var(--bg-body);
position: relative;
overflow: visible;
background-image: none;
min-height: calc(100vh - var(--header-height));
display: flex;
align-items: center;
isolation: isolate;
z-index: 1;
}
.hero-scroll-indicator {
position: absolute;
left: 50%;
bottom: 28px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
z-index: 4;
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: 0.9rem;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0;
pointer-events: none;
transition: opacity 0.45s var(--ease-in-out), transform 0.45s var(--ease-in-out);
}
.hero-scroll-indicator .scroll-arrow {
width: 18px;
height: 18px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
animation: hero-scroll-bounce 1.6s ease-in-out infinite;
}
.hero-scroll-indicator .scroll-text {
font-size: 0.7rem;
letter-spacing: 0.3em;
}
@keyframes hero-scroll-bounce {
0%, 100% { transform: translateY(0) rotate(45deg); opacity: 0.5; }
50% { transform: translateY(6px) rotate(45deg); opacity: 1; }
}
@media (min-width: 900px) {
.home-hero-initial .site-header {
transform: translateY(-100%);
}
.home-hero-initial .site-header,
.home-hero-scrolled .site-header {
position: fixed;
left: 0;
right: 0;
width: 100%;
}
.home-hero-initial .hero-scroll-indicator {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.home-hero-scrolled .hero-scroll-indicator {
opacity: 0;
transform: translateX(-50%) translateY(10px);
}
}
.hero-waves {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 140vh;
opacity: 0.5;
pointer-events: none;
z-index: 2;
mix-blend-mode: screen;
filter: saturate(1.08);
display: block;
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%);
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
html:not([data-theme="dark"]) .hero-waves {
opacity: 0.55;
mix-blend-mode: multiply;
filter: saturate(1.2);
}
[data-theme="dark"] .hero-waves {
opacity: 0.5;
mix-blend-mode: screen;
filter: saturate(1.08);
}
.hero-section::after {
display: none;
}
.hero-content {
max-width: 800px;
margin: 0 auto;
text-align: center;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.hero-title {
display: flex;
justify-content: center;
margin: 0 0 1.2rem;
overflow: visible;
--hero-title-gradient: linear-gradient(135deg, var(--text-primary) 0%, var(--color-primary) 100%);
width: 100%;
}
.hero-title-svg {
display: block;
width: min(88vw, 900px);
margin: 0 auto;
aspect-ratio: 3 / 2;
background-image: var(--hero-title-gradient);
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
mask: url("../../resources/title.svg") center / contain no-repeat;
filter:
drop-shadow(0 10px 24px rgba(12, 20, 32, 0.25))
drop-shadow(0 0 18px rgba(120, 190, 255, 0.28));
position: relative;
transform: translateY(-8px) scale(1.06);
transform-origin: center;
}
.hero-title-svg::before {
content: "";
position: absolute;
inset: -6% -4%;
background:
radial-gradient(circle at 30% 35%, rgba(255, 255, 255, 0.35), transparent 55%),
radial-gradient(circle at 70% 45%, rgba(140, 205, 255, 0.28), transparent 60%),
linear-gradient(180deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0));
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
mask: url("../../resources/title.svg") center / contain no-repeat;
opacity: 0.22;
filter: blur(6px);
mix-blend-mode: screen;
pointer-events: none;
}
html:not([data-theme="dark"]) .hero-title-svg::before {
opacity: 0;
}
.hero-title-svg::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 100%;
height: 55%;
background-image: var(--hero-title-gradient);
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
mask: url("../../resources/title.svg") center / contain no-repeat;
transform: scaleY(-1);
opacity: 0.18;
filter: blur(2px);
}
.hero-title-text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.hero-description {
font-size: 1.5rem;
color: var(--text-secondary);
margin-bottom: 3rem;
font-family: var(--font-mono);
min-height: 3.6em;
}
.stream-char {
transition: opacity 0.4s ease;
}
.services-provided {
margin-top: 0;
position: relative;
z-index: 2;
}
.services-section {
padding: 80px 0;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bg-body) 55%);
position: relative;
z-index: 3;
}
.services-provided h2 {
font-size: 1.8rem;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: none;
color: var(--text-primary);
display: flex;
align-items: center;
}
.services-provided h2::before {
content: "$";
color: var(--color-primary);
margin-right: 10px;
font-family: var(--font-mono);
font-weight: 800;
}
.services-grid-box {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
padding: 0 10px;
}
.service-item {
background: transparent;
border: none;
border-radius: var(--radius-md);
padding: 32px 24px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
transition: all var(--duration-fast);
text-decoration: none;
}
.service-item:hover {
transform: translateY(-4px);
box-shadow: none;
}
.service-icon {
margin-bottom: 1.5rem;
color: var(--color-primary);
}
.service-icon svg {
width: 90px;
height: 90px;
display: block;
}
.service-item span {
font-weight: 600;
color: var(--text-primary);
font-size: 1.25rem;
}
@media (max-width: 1100px) {
.services-grid-box {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.hero-title {
margin-bottom: 1rem;
}
.hero-title-svg {
width: min(92vw, 560px);
transform: translateY(-6px) scale(1.03);
}
.hero-description {
font-size: 1rem;
}
.services-section {
padding: 28px 0;
}
.hero-section {
min-height: auto;
padding: 16px 0 16px;
}
.hero-waves {
display: none;
}
.hero-scroll-indicator {
display: none;
}
}
@media (max-width: 600px) {
.services-grid-box {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.service-item {
padding: 12px 8px;
border: none;
background: transparent;
box-shadow: none;
}
.service-item:hover {
transform: none;
border-color: transparent;
box-shadow: none;
}
.service-icon {
margin-bottom: 0.75rem;
}
.service-icon svg {
width: 64px;
height: 64px;
}
.service-item span {
font-size: 1rem;
}
}
+291 -294
View File
@@ -1,368 +1,365 @@
/* =========================================
首页独有样式 (Front Page / Hero)
Front Page - Minimal Landing Layout
========================================= */
/* --- Hero 区域 --- */
.site-main {
position: relative;
}
.hero-section {
padding: 40px 0;
.home-landing {
background-color: var(--bg-body);
}
.landing-hero {
position: relative;
overflow: visible;
background-image: none;
min-height: calc(100vh - var(--header-height));
min-height: clamp(420px, 68vh, 700px);
display: flex;
align-items: center;
isolation: isolate;
z-index: 1;
}
.hero-scroll-indicator {
position: absolute;
left: 50%;
bottom: 28px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
z-index: 4;
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: 0.9rem;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0;
pointer-events: none;
transition: opacity 0.45s var(--ease-in-out), transform 0.45s var(--ease-in-out);
}
.hero-scroll-indicator .scroll-arrow {
width: 18px;
height: 18px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
animation: hero-scroll-bounce 1.6s ease-in-out infinite;
}
.hero-scroll-indicator .scroll-text {
font-size: 0.7rem;
letter-spacing: 0.3em;
}
@keyframes hero-scroll-bounce {
0%, 100% { transform: translateY(0) rotate(45deg); opacity: 0.5; }
50% { transform: translateY(6px) rotate(45deg); opacity: 1; }
}
@media (min-width: 900px) {
.home-hero-initial .site-header {
transform: translateY(-100%);
}
.home-hero-initial .site-header,
.home-hero-scrolled .site-header {
position: fixed;
left: 0;
right: 0;
width: 100%;
}
.home-hero-initial .hero-scroll-indicator {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.home-hero-scrolled .hero-scroll-indicator {
opacity: 0;
transform: translateX(-50%) translateY(10px);
}
}
.hero-waves {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 140vh;
opacity: 0.5;
pointer-events: none;
z-index: 2;
mix-blend-mode: screen;
filter: saturate(1.08);
display: block;
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%);
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
html:not([data-theme="dark"]) .hero-waves {
opacity: 0.55;
mix-blend-mode: multiply;
filter: saturate(1.2);
}
[data-theme="dark"] .hero-waves {
opacity: 0.5;
mix-blend-mode: screen;
filter: saturate(1.08);
}
/* 底部渐变遮罩 */
.hero-section::after {
display: none;
}
.hero-content {
max-width: 800px;
margin: 0 auto;
text-align: center;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
}
/* 大标题 */
.hero-title {
display: flex;
justify-content: center;
margin: 0 0 1.2rem;
overflow: visible;
--hero-title-gradient: linear-gradient(135deg, var(--text-primary) 0%, var(--color-primary) 100%);
width: 100%;
}
.hero-title-svg {
display: block;
width: min(88vw, 900px);
margin: 0 auto;
aspect-ratio: 3 / 2;
background-image: var(--hero-title-gradient);
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
mask: url("../../resources/title.svg") center / contain no-repeat;
filter:
drop-shadow(0 10px 24px rgba(12, 20, 32, 0.25))
drop-shadow(0 0 18px rgba(120, 190, 255, 0.28));
position: relative;
transform: translateY(-8px) scale(1.06);
transform-origin: center;
}
.hero-title-svg::before {
content: "";
position: absolute;
inset: -6% -4%;
background:
radial-gradient(circle at 30% 35%, rgba(255, 255, 255, 0.35), transparent 55%),
radial-gradient(circle at 70% 45%, rgba(140, 205, 255, 0.28), transparent 60%),
linear-gradient(180deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0));
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
mask: url("../../resources/title.svg") center / contain no-repeat;
opacity: 0.22;
filter: blur(6px);
mix-blend-mode: screen;
pointer-events: none;
}
html:not([data-theme="dark"]) .hero-title-svg::before {
opacity: 0;
}
.hero-title-svg::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 100%;
height: 55%;
background-image: var(--hero-title-gradient);
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
mask: url("../../resources/title.svg") center / contain no-repeat;
transform: scaleY(-1);
opacity: 0.18;
filter: blur(2px);
}
.hero-title-text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
color: var(--hero-fg);
isolation: isolate;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
--hero-fg: #1c2430;
--hero-subtitle: rgba(28, 36, 48, 0.86);
--hero-title-shadow: rgba(24, 36, 58, 0.14);
--hero-btn-border: rgba(30, 42, 59, 0.52);
--hero-btn-bg: rgba(255, 255, 255, 0.42);
--hero-btn-hover-bg: rgba(255, 255, 255, 0.65);
--hero-overlay-highlight: rgba(255, 255, 255, 0.45);
--hero-overlay-top: rgba(248, 251, 255, 0.24);
--hero-overlay-bottom: rgba(206, 220, 243, 0.2);
}
/* 描述文本 (代码风格) */
.hero-description {
font-size: 1.5rem; /* 增大 Hero 描述 (原1.25rem) */
color: var(--text-secondary);
margin-bottom: 3rem;
font-family: var(--font-mono);
min-height: 3.6em; /* 预留高度防止抖动 */
[data-theme="dark"] .landing-hero {
--hero-fg: #ffffff;
--hero-subtitle: rgba(255, 255, 255, 0.92);
--hero-title-shadow: rgba(0, 0, 0, 0.34);
--hero-btn-border: rgba(255, 255, 255, 0.86);
--hero-btn-bg: rgba(255, 255, 255, 0.08);
--hero-btn-hover-bg: rgba(255, 255, 255, 0.23);
--hero-overlay-highlight: rgba(255, 255, 255, 0.16);
--hero-overlay-top: rgba(13, 18, 27, 0.14);
--hero-overlay-bottom: rgba(12, 18, 28, 0.52);
}
/* --- 流式输出字符样式 --- */
.stream-char {
transition: opacity 0.4s ease; /* 平滑淡入 */
.landing-hero-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
z-index: 0;
}
/* --- 服务展示区 (Services) --- */
.services-provided {
margin-top: 0;
.landing-hero::before {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background:
radial-gradient(circle at 72% 18%, var(--hero-overlay-highlight), rgba(255, 255, 255, 0) 36%),
linear-gradient(180deg, var(--hero-overlay-top), var(--hero-overlay-bottom));
}
.landing-hero .container {
position: relative;
z-index: 2;
}
.landing-hero-content {
max-width: 880px;
margin: 0 auto;
padding: 56px 0;
}
.landing-hero-title {
margin: 0 0 18px;
font-size: clamp(2.4rem, 7vw, 4.4rem);
line-height: 1.12;
letter-spacing: 0.02em;
font-weight: 700;
text-shadow: 0 4px 18px var(--hero-title-shadow);
}
.landing-hero-subtitle {
margin: 0;
font-size: clamp(1.1rem, 2.6vw, 1.9rem);
color: var(--hero-subtitle);
}
.landing-hero-btn {
display: inline-flex;
align-items: center;
justify-content: center;
margin-top: 30px;
padding: 0 30px;
min-height: 48px;
border: 1px solid var(--hero-btn-border);
border-radius: var(--radius-sm);
color: var(--hero-fg);
background-color: var(--hero-btn-bg);
backdrop-filter: blur(1px);
font-size: 1.05rem;
font-weight: 600;
transition: all var(--duration-fast);
}
.landing-hero-btn:hover {
background-color: var(--hero-btn-hover-bg);
border-color: var(--hero-fg);
}
.services-section {
padding: 80px 0;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bg-body) 55%);
position: relative;
z-index: 3;
padding: 24px 0 14px;
}
/* 标题带有命令提示符风格 */
.services-provided h2 {
font-size: 1.8rem; /* 增大服务标题 (原1.5rem) */
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: none;
.services-provided h2,
.landing-feed-head h2 {
margin: 0 0 12px;
font-size: 1.15rem;
color: var(--text-primary);
display: flex; /* 改为 flex 以支持全宽下划线 */
display: flex;
align-items: center;
}
.services-provided h2::before {
content: "$";
color: var(--color-primary); /* 统一使用蓝色,保持与 blog h2 风格一致 */
margin-right: 10px; /* 保持与 blog h2 一致的间距 */
.services-provided h2::before,
.landing-feed-head h2::before {
content: "#";
margin-right: 8px;
color: var(--color-primary);
font-family: var(--font-mono);
font-weight: 800;
font-weight: 700;
}
/* 服务卡片网格 */
.services-grid-box {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
padding: 0 10px;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
}
.service-item {
background: transparent;
border: none;
border-radius: var(--radius-md);
padding: 32px 24px;
min-height: 170px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
transition: all var(--duration-fast);
text-decoration: none;
}
.service-item:hover {
transform: translateY(-4px);
box-shadow: none;
padding: 20px 14px;
border: none;
border-radius: 0;
background-color: transparent;
color: var(--text-primary);
transition: none;
}
.service-icon {
margin-bottom: 1.5rem;
margin-bottom: 14px;
color: var(--color-primary);
/* 确保 SVG 继承颜色 */
}
.service-icon svg {
width: 90px; /* 增大图标 (原80px) */
height: 90px;
width: 78px;
height: 78px;
display: block;
}
.service-item span {
display: block;
font-size: 1.08rem;
font-weight: 600;
color: var(--text-primary);
font-size: 1.25rem; /* 增大服务项文字 (原1.1rem) */
}
/* --- 响应式适配 --- */
@media (max-width: 1100px) {
.services-grid-box {
grid-template-columns: repeat(2, 1fr); /* 平板/小屏变 2 列 */
}
.landing-updates {
padding: 16px 0 58px;
}
@media (max-width: 768px) {
.hero-title {
margin-bottom: 1rem;
}
.hero-title-svg {
width: min(92vw, 560px);
transform: translateY(-6px) scale(1.03);
}
.hero-description {
font-size: 1rem;
}
.services-section {
padding: 28px 0;
}
.hero-section {
min-height: auto;
padding: 16px 0 16px;
}
.hero-waves {
display: none;
}
.hero-scroll-indicator {
display: none;
}
.landing-updates-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
@media (max-width: 600px) {
.landing-feed-box {
border: none;
border-radius: 0;
background-color: transparent;
overflow: visible;
}
.landing-feed-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 0 12px;
}
.landing-feed-head h2 {
margin: 0;
}
.landing-feed-head a {
font-size: 0.94rem;
font-weight: 600;
color: var(--text-secondary);
white-space: nowrap;
}
.landing-feed-head a::after {
content: " ->";
font-family: var(--font-mono);
}
.landing-feed-head a:hover {
color: var(--color-primary);
}
.landing-feed {
margin: 0;
padding: 0;
list-style: none;
}
.landing-feed-item {
padding: 14px 0 16px;
border-bottom: 1px solid var(--border-muted);
}
.landing-feed-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.landing-feed-title {
margin: 0;
display: flex;
align-items: flex-start;
gap: 10px;
}
.landing-feed-title::before {
content: "";
width: 8px;
height: 8px;
margin-top: 0.54em;
border-radius: 2px;
background: var(--color-primary);
flex-shrink: 0;
opacity: 0.9;
}
.landing-feed-link {
display: block;
min-width: 0;
font-size: clamp(1.1rem, 1.2vw + 0.82rem, 1.45rem);
line-height: 1.36;
font-weight: 600;
color: var(--text-primary);
overflow-wrap: anywhere;
}
.landing-feed-link:hover {
color: var(--color-primary);
}
.landing-feed-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0 0 18px;
font-size: 0.9rem;
color: var(--text-secondary);
}
.landing-feed-author {
font-weight: 500;
}
.landing-feed-meta time {
font-family: var(--font-mono);
font-size: 0.84rem;
color: var(--text-secondary);
}
.landing-feed-meta time::before {
content: "/";
margin-right: 8px;
color: var(--border-default);
}
.landing-feed-excerpt {
margin: 8px 0 0 18px;
font-size: 0.98rem;
line-height: 1.66;
color: var(--text-secondary);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
.landing-feed-empty {
padding: 14px 0;
color: var(--text-secondary);
}
@media (max-width: 1060px) {
.services-grid-box {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
}
@media (max-width: 820px) {
.landing-updates-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 680px) {
.landing-hero {
min-height: 370px;
}
.landing-hero-content {
padding: 44px 0;
}
.landing-hero-btn {
min-height: 44px;
padding: 0 24px;
}
.service-item {
min-height: 132px;
padding: 12px 8px;
border: none;
background: transparent;
box-shadow: none;
}
.service-item:hover {
transform: none;
border-color: transparent;
box-shadow: none;
}
.service-icon {
margin-bottom: 0.75rem;
margin-bottom: 8px;
}
.service-icon svg {
width: 64px;
height: 64px;
width: 58px;
height: 58px;
}
.service-item span {
font-size: 1rem;
font-size: 0.95rem;
}
.landing-feed-head {
padding-bottom: 10px;
}
.landing-feed-link {
font-size: 1.08rem;
}
.landing-feed-meta,
.landing-feed-excerpt {
margin-left: 14px;
}
.landing-feed-excerpt {
-webkit-line-clamp: 2;
}
}
+34
View File
@@ -71,6 +71,36 @@ html:not([data-theme="dark"]) .intro-about .site-header {
gap: 14px;
}
.intro-brand-mark {
position: relative;
width: min(420px, 100%);
margin-bottom: 2px;
}
.intro-brand-svg {
display: block;
width: min(76vw, 360px);
aspect-ratio: 3 / 2;
background-image: linear-gradient(135deg, var(--text-primary), var(--color-primary));
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
mask: url("../../resources/title.svg") center / contain no-repeat;
filter:
drop-shadow(0 10px 24px rgba(12, 20, 32, 0.24))
drop-shadow(0 0 16px rgba(120, 190, 255, 0.24));
}
.intro-brand-text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.intro-title {
font-size: clamp(2.6rem, 4vw, 4rem);
font-weight: 800;
@@ -357,6 +387,10 @@ body.has-intro-animations .intro-step.is-active .intro-animate {
padding-top: 110px;
}
.intro-brand-svg {
width: min(86vw, 320px);
}
.intro-title {
font-size: 2.2rem;
}
+852
View File
@@ -0,0 +1,852 @@
(() => {
const hero = document.querySelector('.landing-hero');
const canvas = document.querySelector('.landing-hero-canvas');
if (!hero || !canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
let width = 0;
let height = 0;
let dpr = 1;
let rafId = null;
let startAt = performance.now();
const EMPTY_SET = new Set();
const WORD_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
const NUMBER_RE = /^\d+(?:_\d+)*(?:\.\d+)?$/;
const STRING_RE = /^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)$/;
const CONSTANT_RE = /^[A-Z][A-Z0-9_]{2,}$/;
const TYPE_RE = /^[A-Z][A-Za-z0-9_]*$/;
const OPERATOR_RE = /^(::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.#])$/;
const TOKEN_RE = /(\/\/.*$|#\[.*$|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:_\d+)*(?:\.\d+)?\b|[A-Za-z_][A-Za-z0-9_]*|::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.,;()[\]{}#])/g;
const LANGUAGE_KEYWORDS = {
springboot: new Set([
'package', 'import', 'public', 'private', 'protected', 'class', 'interface',
'static', 'final', 'new', 'return', 'if', 'else', 'try', 'catch', 'finally',
'throw', 'throws', 'void', 'long', 'int', 'boolean', 'null', 'true', 'false',
'extends', 'implements', 'this',
]),
rust: new Set([
'use', 'pub', 'struct', 'enum', 'impl', 'async', 'await', 'fn', 'let', 'mut',
'loop', 'match', 'if', 'else', 'return', 'while', 'for', 'in', 'break', 'continue',
'crate', 'self', 'super', 'mod', 'where', 'Result', 'Ok', 'Err',
]),
cpp: new Set([
'include', 'template', 'typename', 'concept', 'requires', 'struct', 'class',
'public', 'private', 'protected', 'explicit', 'void', 'auto', 'const', 'constexpr',
'if', 'else', 'while', 'for', 'return', 'using', 'namespace', 'std', 'nullptr',
'true', 'false',
]),
python: new Set([
'import', 'from', 'as', 'class', 'def', 'return', 'if', 'elif', 'else', 'for',
'while', 'in', 'and', 'or', 'not', 'await', 'async', 'with', 'try', 'except',
'finally', 'lambda', 'None', 'True', 'False', 'yield',
]),
javascript: new Set([
'class', 'constructor', 'return', 'if', 'else', 'while', 'for', 'const', 'let',
'var', 'new', 'async', 'await', 'try', 'catch', 'finally', 'throw', 'export',
'import', 'from', 'this', 'null', 'true', 'false', 'yield', 'function',
]),
nim: new Set([
'import', 'type', 'enum', 'object', 'ref', 'proc', 'result', 'let', 'var',
'if', 'elif', 'else', 'for', 'in', 'while', 'return', 'await', 'defer',
'mod', 'echo', 'discard',
]),
};
const classifyToken = (token, language, nextToken, prevToken, atLineStart) => {
const keywords = LANGUAGE_KEYWORDS[language] || EMPTY_SET;
if (token.startsWith('//')) return 'comment';
if (token.startsWith('#[') || token.startsWith('@')) return 'annotation';
if (STRING_RE.test(token)) return 'string';
if (NUMBER_RE.test(token)) return 'number';
if (OPERATOR_RE.test(token)) return 'operator';
if (!WORD_RE.test(token)) {
return 'plain';
}
if (keywords.has(token)) return 'keyword';
if (language === 'cpp' && atLineStart && prevToken === '#' && token === 'include') return 'keyword';
if (CONSTANT_RE.test(token)) return 'constant';
if (nextToken === '(') return 'method';
if (TYPE_RE.test(token)) return 'type';
return 'field';
};
const tokenizeLine = (line, language) => {
if (line.length === 0) return [['', 'plain']];
const matches = [...line.matchAll(TOKEN_RE)];
if (matches.length === 0) return [[line, 'plain']];
const tokens = [];
let cursor = 0;
for (let i = 0; i < matches.length; i += 1) {
const match = matches[i];
const start = match.index ?? 0;
const raw = match[0];
const nextToken = i + 1 < matches.length ? matches[i + 1][0] : '';
const prevToken = i > 0 ? matches[i - 1][0] : '';
const atLineStart = line.slice(0, start).trim().length === 0;
if (start > cursor) {
tokens.push([line.slice(cursor, start), 'plain']);
}
if ((language === 'python' || language === 'nim') && raw === '#') {
tokens.push([line.slice(start), 'comment']);
cursor = line.length;
break;
}
tokens.push([raw, classifyToken(raw, language, nextToken, prevToken, atLineStart)]);
cursor = start + raw.length;
}
if (cursor < line.length) {
tokens.push([line.slice(cursor), 'plain']);
}
if (tokens.length === 0) {
tokens.push(['', 'plain']);
}
return tokens;
};
const tokenizeSnippet = (lines, language) => lines.map((line) => tokenizeLine(line, language));
const rawSnippetCatalog = [
{
name: 'springboot',
repeat: 4,
lines: [
'package cn.edu.ouc.itstudio.payment.workflow;',
'',
'import org.springframework.stereotype.Service;',
'import org.springframework.transaction.annotation.Transactional;',
'import org.springframework.transaction.annotation.Propagation;',
'import org.springframework.transaction.annotation.Isolation;',
'import org.springframework.retry.support.RetryTemplate;',
'import java.math.BigDecimal;',
'import java.time.Duration;',
'import java.util.Optional;',
'import java.util.concurrent.CompletableFuture;',
'import java.util.concurrent.ConcurrentHashMap;',
'import java.util.concurrent.ConcurrentMap;',
'import java.util.concurrent.atomic.AtomicLong;',
'',
'@Service',
'@RequiredArgsConstructor',
'public class PaymentWorkflowOrchestrator {',
' private static final String IDEMPOTENCY_PREFIX = "pay:idem:";',
' private static final BigDecimal RISK_THRESHOLD = new BigDecimal("9999.99");',
' private static final Duration LOCK_TTL = Duration.ofSeconds(45);',
'',
' private final PaymentRepository paymentRepository;',
' private final RiskEngine riskEngine;',
' private final RetryTemplate retryTemplate;',
' private final DistributedLockManager lockManager;',
' private final IdempotencyGateway idempotencyGateway;',
' private final OutboxPublisher outboxPublisher;',
' private final CompensationService compensator;',
' private final GatewayClient gateway;',
' private final Clock clock;',
' private final TraceContext traceContext;',
' private final ConcurrentMap<String, AtomicLong> inMemorySeq = new ConcurrentHashMap<>();',
'',
' @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)',
' public PaymentResult orchestrate(PaymentCommand command, String idemToken) {',
' String idemKey = IDEMPOTENCY_PREFIX + Digest.sha256Hex(idemToken + ":" + command.getOrderNo());',
' Optional<PaymentResult> cached = idempotencyGateway.read(idemKey, PaymentResult.class);',
' if (cached.isPresent()) { return cached.get(); }',
' DistributedLock lock = lockManager.tryLock("lock:order:" + command.getOrderNo(), LOCK_TTL);',
' if (lock == null) { throw new BusyException("order is processing by another worker"); }',
' try {',
' validateCommand(command);',
' long seq = inMemorySeq.computeIfAbsent(command.getTenantId(), k -> new AtomicLong(0)).incrementAndGet();',
' RiskSnapshot risk = riskEngine.evaluate(command, seq);',
' if (risk.getScore().compareTo(RISK_THRESHOLD) > 0 && !command.isForceSubmit()) {',
' throw new RiskBlockedException("risk score exceeded threshold");',
' }',
' PaymentAggregate aggregate = aggregateFactory.rehydrateOrCreate(command.getOrderNo(), command.getTenantId());',
' aggregate.apply(command.toEvent(clock.instant(), traceContext.currentTraceId()));',
' paymentRepository.save(aggregate);',
' PaymentResult result = retryTemplate.execute(ctx -> gateway.charge(aggregate.buildChargeRequest(ctx.getRetryCount())));',
' outboxPublisher.publish(new PaymentSucceededEvent(aggregate.getOrderNo(), result.getTransactionId(), clock.instant(), traceContext.currentTraceId()));',
' idempotencyGateway.write(idemKey, result, Duration.ofHours(6));',
' return result;',
' } catch (Exception ex) {',
' compensator.compensate(command.getOrderNo(), ex.getMessage(), clock.instant());',
' throw ex;',
' } finally {',
' lock.unlock();',
' }',
' }',
'}',
],
},
{
name: 'rust',
repeat: 5,
lines: [
'use std::collections::{BTreeMap, HashMap};',
'use std::sync::Arc;',
'use tokio::sync::{Mutex, RwLock};',
'use tokio::time::{sleep, Duration, Instant};',
'use serde::{Deserialize, Serialize};',
'',
'#[derive(Debug, Clone, Serialize, Deserialize)]',
'struct Command { tenant: String, key: String, amount: i64, deadline_ms: u64 }',
'',
'#[derive(Debug, Clone, Serialize, Deserialize)]',
'enum Event { Reserved { id: String, cents: i64 }, Committed { id: String }, Failed { id: String, reason: String } }',
'',
'#[derive(Default)]',
'struct Snapshot {',
' ledger: HashMap<String, i64>,',
' inbox: BTreeMap<u64, Event>,',
' watermark: u64,',
'}',
'',
'struct Orchestrator {',
' state: Arc<RwLock<Snapshot>>,',
' idempotency: Arc<Mutex<HashMap<String, Event>>>,',
'}',
'',
'impl Orchestrator {',
' async fn apply(&self, cmd: Command) -> Result<Event, String> {',
' let idem = format!("{}:{}:{}", cmd.tenant, cmd.key, cmd.amount);',
' if let Some(hit) = self.idempotency.lock().await.get(&idem).cloned() {',
' return Ok(hit);',
' }',
' let start = Instant::now();',
' let mut retries = 0u8;',
' loop {',
' retries += 1;',
' match self.try_once(&cmd).await {',
' Ok(evt) => {',
' self.idempotency.lock().await.insert(idem.clone(), evt.clone());',
' return Ok(evt);',
' }',
' Err(e) if retries < 5 && start.elapsed() < Duration::from_millis(cmd.deadline_ms) => {',
' sleep(Duration::from_millis((retries as u64) * 17)).await;',
' continue;',
' }',
' Err(e) => return Err(format!("orchestration failed after retries: {}", e)),',
' }',
' }',
' }',
'',
' async fn try_once(&self, cmd: &Command) -> Result<Event, String> {',
' let mut guard = self.state.write().await;',
' let balance = guard.ledger.get(&cmd.key).copied().unwrap_or_default();',
' if balance < cmd.amount {',
' let evt = Event::Failed { id: cmd.key.clone(), reason: "insufficient_funds".to_string() };',
' guard.watermark += 1;',
' guard.inbox.insert(guard.watermark, evt.clone());',
' return Ok(evt);',
' }',
' guard.ledger.insert(cmd.key.clone(), balance - cmd.amount);',
' guard.watermark += 1;',
' let evt = Event::Committed { id: cmd.key.clone() };',
' guard.inbox.insert(guard.watermark, evt.clone());',
' Ok(evt)',
' }',
'}',
],
},
{
name: 'cpp',
repeat: 5,
lines: [
'#include <algorithm>',
'#include <atomic>',
'#include <chrono>',
'#include <concepts>',
'#include <future>',
'#include <map>',
'#include <mutex>',
'#include <optional>',
'#include <queue>',
'#include <shared_mutex>',
'#include <string>',
'#include <unordered_map>',
'#include <variant>',
'#include <vector>',
'',
'template <typename T>',
'concept Hashable = requires(T v) { std::hash<T>{}(v); };',
'',
'struct Job {',
' std::string id;',
' std::vector<std::string> deps;',
' std::function<std::variant<int, std::string>()> run;',
'};',
'',
'class DagExecutor {',
'public:',
' explicit DagExecutor(std::size_t maxConcurrency) : maxConcurrency_(maxConcurrency) {}',
'',
' void add(Job job) {',
' std::unique_lock lk(mu_);',
' graph_[job.id] = std::move(job);',
' }',
'',
' std::map<std::string, std::variant<int, std::string>> execute() {',
' auto order = topoSort();',
' std::map<std::string, std::variant<int, std::string>> out;',
' std::vector<std::future<void>> inflight;',
' std::atomic_size_t cursor{0};',
'',
' auto worker = [&]() {',
' while (true) {',
' auto idx = cursor.fetch_add(1);',
' if (idx >= order.size()) { break; }',
' const auto& id = order[idx];',
' auto result = graph_.at(id).run();',
' {',
' std::unique_lock lk(outMu_);',
' out[id] = std::move(result);',
' }',
' }',
' };',
'',
' for (std::size_t i = 0; i < maxConcurrency_; ++i) {',
' inflight.emplace_back(std::async(std::launch::async, worker));',
' }',
' for (auto& f : inflight) { f.get(); }',
' return out;',
' }',
'',
'private:',
' std::vector<std::string> topoSort();',
' std::size_t maxConcurrency_;',
' std::unordered_map<std::string, Job> graph_;',
' std::shared_mutex mu_;',
' std::mutex outMu_;',
'};',
],
},
{
name: 'python',
repeat: 5,
lines: [
'import asyncio',
'import contextvars',
'import dataclasses',
'import hashlib',
'import random',
'from collections import defaultdict',
'from typing import Any, Callable, Dict, Iterable, List',
'',
'trace_id_var = contextvars.ContextVar("trace_id", default="n/a")',
'',
'@dataclasses.dataclass',
'class TaskSpec:',
' key: str',
' deps: List[str]',
' timeout: float',
' fn: Callable[[Dict[str, Any]], "asyncio.Future[Any]"]',
'',
'@dataclasses.dataclass',
'class EngineState:',
' done: Dict[str, Any]',
' failed: Dict[str, str]',
' latency_ms: Dict[str, float]',
'',
'class AsyncDagEngine:',
' def __init__(self, specs: Iterable[TaskSpec], concurrency: int = 8) -> None:',
' self.specs = {s.key: s for s in specs}',
' self.children = defaultdict(list)',
' self.indegree = defaultdict(int)',
' for spec in self.specs.values():',
' self.indegree[spec.key] = len(spec.deps)',
' for dep in spec.deps:',
' self.children[dep].append(spec.key)',
' self.sem = asyncio.Semaphore(concurrency)',
'',
' async def run(self, seed: Dict[str, Any]) -> EngineState:',
' state = EngineState(done=dict(seed), failed={}, latency_ms={})',
' ready = asyncio.Queue()',
' for k, v in self.indegree.items():',
' if v == 0:',
' ready.put_nowait(k)',
'',
' async def worker() -> None:',
' while True:',
' key = await ready.get()',
' if key is None:',
' return',
' spec = self.specs[key]',
' token = trace_id_var.set(hashlib.sha1(key.encode()).hexdigest()[:12])',
' started = asyncio.get_running_loop().time()',
' try:',
' async with self.sem:',
' result = await asyncio.wait_for(spec.fn(state.done), timeout=spec.timeout)',
' state.done[key] = result',
' except Exception as exc:',
' state.failed[key] = f"{type(exc).__name__}:{exc}"',
' finally:',
' elapsed = (asyncio.get_running_loop().time() - started) * 1000',
' state.latency_ms[key] = elapsed',
' trace_id_var.reset(token)',
'',
' for child in self.children[key]:',
' self.indegree[child] -= 1',
' if self.indegree[child] == 0 and child not in state.failed:',
' ready.put_nowait(child)',
'',
' workers = [asyncio.create_task(worker()) for _ in range(6)]',
' await ready.join()',
' for _ in workers: ready.put_nowait(None)',
' await asyncio.gather(*workers)',
' return state',
],
},
{
name: 'javascript',
repeat: 5,
lines: [
'class CircuitBreaker {',
' constructor({ threshold = 0.35, minRequests = 20, coolDownMs = 2500 } = {}) {',
' this.threshold = threshold;',
' this.minRequests = minRequests;',
' this.coolDownMs = coolDownMs;',
' this.state = "closed";',
' this.window = [];',
' this.openUntil = 0;',
' }',
'',
' record(ok) {',
' this.window.push(ok ? 1 : 0);',
' if (this.window.length > 120) this.window.shift();',
' const failRatio = 1 - this.window.reduce((a, b) => a + b, 0) / this.window.length;',
' if (this.window.length >= this.minRequests && failRatio >= this.threshold) {',
' this.state = "open";',
' this.openUntil = Date.now() + this.coolDownMs;',
' }',
' }',
'',
' canPass() {',
' if (this.state === "closed") return true;',
' if (Date.now() >= this.openUntil) {',
' this.state = "half-open";',
' return true;',
' }',
' return false;',
' }',
'}',
'',
'export class StreamOrchestrator {',
' constructor(fetchers, { concurrency = 6 } = {}) {',
' this.fetchers = fetchers;',
' this.concurrency = concurrency;',
' this.cache = new Map();',
' this.breakers = new Map();',
' }',
'',
' async *collect(keys, signal) {',
' const queue = [...keys];',
' const inflight = new Set();',
' const next = async () => {',
' if (!queue.length) return;',
' const key = queue.shift();',
' const fn = this.fetchers.get(key);',
' if (!fn) return;',
' const breaker = this.breakers.get(key) ?? new CircuitBreaker();',
' this.breakers.set(key, breaker);',
' if (!breaker.canPass()) return;',
'',
' const p = fn({ signal })',
' .then((value) => { breaker.record(true); return { key, value, ok: true }; })',
' .catch((err) => { breaker.record(false); return { key, error: err, ok: false }; })',
' .finally(() => inflight.delete(p));',
' inflight.add(p);',
' };',
'',
' while (queue.length || inflight.size) {',
' while (inflight.size < this.concurrency && queue.length) await next();',
' if (!inflight.size) continue;',
' const settled = await Promise.race([...inflight]);',
' if (settled.ok) this.cache.set(settled.key, settled.value);',
' yield settled;',
' }',
' }',
'}',
],
},
{
name: 'nim',
repeat: 5,
lines: [
'import asyncdispatch, options, tables, sequtils, algorithm, strformat, times, hashes, locks',
'',
'type',
' EventKind = enum',
' ekWrite, ekCompact, ekSnapshot, ekReplicate',
'',
' Event = object',
' tenantId: string',
' streamId: string',
' offset: int64',
' payload: string',
' kind: EventKind',
' ts: DateTime',
'',
' ShardState = ref object',
' watermark: int64',
' pending: Table[int64, Future[void]]',
' snapshotEvery: int',
' lock: Lock',
'',
'proc routeShard(tenantId, streamId: string; shardCount: int): int =',
' result = abs(hash(tenantId & ":" & streamId)) mod shardCount',
'',
'proc applyEvent(state: ShardState; ev: Event): Future[void] {.async.} =',
' acquire(state.lock)',
' defer: release(state.lock)',
' if ev.offset <= state.watermark:',
' return',
'',
' let delayMs = (ev.payload.len mod 13) + 3',
' await sleepAsync(delayMs)',
' state.watermark = ev.offset',
'',
' if state.watermark mod int64(state.snapshotEvery) == 0:',
' let sid = state.watermark',
' state.pending[sid] = asyncCheck proc() {.async.} =',
' await sleepAsync(20)',
' echo &"[snapshot] stream={ev.streamId} offset={sid}"',
'',
'proc mergeOrdered(buffers: seq[seq[Event]]): seq[Event] =',
' result = @[]',
' for b in buffers: result.add(b)',
' result.sort(proc(a, b: Event): int = cmp(a.offset, b.offset))',
'',
'proc replay(shards: seq[ShardState]; buffers: seq[seq[Event]]) {.async.} =',
' for ev in mergeOrdered(buffers):',
' let idx = routeShard(ev.tenantId, ev.streamId, shards.len)',
' await shards[idx].applyEvent(ev)',
],
},
];
const snippetCatalog = rawSnippetCatalog.map((entry) => ({
...entry,
lines: tokenizeSnippet(entry.lines, entry.name),
}));
const selectedSnippet = snippetCatalog[Math.floor(Math.random() * snippetCatalog.length)];
const snippetLines = selectedSnippet.lines;
const repeatCount = selectedSnippet.repeat ?? 6;
const codeLines = [];
for (let i = 0; i < repeatCount; i += 1) {
snippetLines.forEach((line) => codeLines.push(line));
codeLines.push([['', 'plain']]);
}
const themes = {
light: {
background: '#f7f9fd',
editor: '#fbfcff',
gutter: '#f1f4fa',
divider: '#d5dde9',
guide: 'rgba(115, 138, 170, 0.14)',
currentLine: 'rgba(46, 118, 245, 0.13)',
lineNo: '#8a93a4',
lineNoActive: '#4f5e77',
caret: '#2e76f5',
markerOk: '#238551',
markerWarn: '#bb7a15',
fadeTop: 'rgba(0, 0, 0, 0)',
fadeBottom: 'rgba(0, 0, 0, 0)',
code: {
plain: '#3e4553',
keyword: '#0f59c5',
type: '#136a4d',
method: '#7c3ec1',
annotation: '#9a5f0a',
comment: '#758191',
string: '#1c7f3f',
number: '#8f437d',
field: '#4c607f',
operator: '#4a607e',
constant: '#3f67b0',
},
},
dark: {
background: '#1d1f23',
editor: '#1f2126',
gutter: '#1a1b1f',
divider: '#333842',
guide: 'rgba(103, 122, 157, 0.2)',
currentLine: 'rgba(76, 149, 255, 0.17)',
lineNo: '#5f6674',
lineNoActive: '#8da1bf',
caret: '#4c95ff',
markerOk: '#53d779',
markerWarn: '#e8b04a',
fadeTop: 'rgba(24, 26, 32, 0.5)',
fadeBottom: 'rgba(20, 22, 28, 0.38)',
code: {
plain: '#ccd3dd',
keyword: '#cc7832',
type: '#4ea1f2',
method: '#d8c66c',
annotation: '#bbb529',
comment: '#6a7484',
string: '#6aab73',
number: '#8a6db6',
field: '#c4cbd6',
operator: '#909ba9',
constant: '#9876aa',
},
},
};
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
const getTheme = () => (document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light');
const tokenCount = (tokens) => tokens.reduce((sum, token) => sum + token[0].length, 0);
const resizeCanvas = () => {
const rect = hero.getBoundingClientRect();
width = Math.max(1, Math.round(rect.width));
height = Math.max(1, Math.round(rect.height));
dpr = window.devicePixelRatio || 1;
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
const drawTokenLine = (tokens, x, y, charBudget, palette) => {
let cursorX = x;
const limit = Number.isFinite(charBudget);
let remaining = limit ? charBudget : 0;
for (const token of tokens) {
if (limit && remaining <= 0) break;
const text = token[0];
if (!text) continue;
const visibleText = limit ? text.slice(0, remaining) : text;
ctx.fillStyle = palette.code[token[1]] || palette.code.plain;
ctx.fillText(visibleText, cursorX, y);
cursorX += ctx.measureText(visibleText).width;
if (limit) {
remaining -= visibleText.length;
}
}
return cursorX - x;
};
const drawCodeEditor = (palette, elapsed, isStatic) => {
const gutterWidth = clamp(Math.round(width * 0.08), 56, 92);
const fontSize = clamp(Math.round(width / 98), 14, 20);
const lineHeight = Math.round(fontSize * 1.72);
const topPadding = Math.round(lineHeight * 1.24);
const codeStartX = gutterWidth + 18;
const visibleLines = Math.max(1, Math.ceil((height - topPadding * 2) / lineHeight) + 2);
const baseLineNo = 1;
const scrollLeadLines = 2;
const lineCharTotals = codeLines.map((line) => tokenCount(line));
const lineDurations = lineCharTotals.map((chars) => Math.max(150, chars * 8));
const totalTypingDuration = lineDurations.reduce((sum, duration) => sum + duration, 0);
const holdDuration = 500;
const cycleDuration = totalTypingDuration + holdDuration;
ctx.fillStyle = palette.background;
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = palette.editor;
ctx.fillRect(gutterWidth, 0, width - gutterWidth, height);
ctx.fillStyle = palette.gutter;
ctx.fillRect(0, 0, gutterWidth, height);
ctx.fillStyle = palette.divider;
ctx.fillRect(gutterWidth, 0, 1, height);
ctx.strokeStyle = palette.guide;
ctx.lineWidth = 1;
for (let x = codeStartX + 110; x < width; x += 120) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
const cyclePos = isStatic ? totalTypingDuration + holdDuration : elapsed % cycleDuration;
const writingPhase = cyclePos < totalTypingDuration;
let completedLines = codeLines.length;
let activeLine = codeLines.length - 1;
let activeLineProgress = 1;
if (writingPhase && !isStatic) {
let remaining = cyclePos;
completedLines = 0;
activeLine = 0;
activeLineProgress = 0;
for (let i = 0; i < lineDurations.length; i += 1) {
const duration = lineDurations[i];
if (remaining >= duration) {
remaining -= duration;
completedLines += 1;
continue;
}
activeLine = i;
activeLineProgress = duration > 0 ? remaining / duration : 1;
break;
}
}
const startLine = isStatic
? Math.max(0, codeLines.length - visibleLines)
: Math.max(0, completedLines - Math.max(1, visibleLines - 1 - scrollLeadLines));
ctx.font = `500 ${fontSize}px "JetBrains Mono", "Fira Code", "SFMono-Regular", Consolas, monospace`;
ctx.textBaseline = 'alphabetic';
let caret = null;
for (let i = 0; i < visibleLines; i += 1) {
const lineIndex = startLine + i;
const tokens = codeLines[lineIndex];
if (!tokens) break;
const y = topPadding + i * lineHeight;
if (y < -lineHeight || y > height + lineHeight) continue;
const charsInLine = lineCharTotals[lineIndex];
const lineCompleted = isStatic || lineIndex < completedLines || !writingPhase;
const isActive = writingPhase && lineIndex === activeLine;
let charBudget = 0;
if (lineCompleted) {
charBudget = Infinity;
} else if (isActive) {
charBudget = charsInLine > 0 ? Math.floor(charsInLine * clamp(activeLineProgress, 0, 1)) : 0;
if (charsInLine > 0) {
charBudget = Math.max(1, charBudget);
}
}
const hasStarted = lineCompleted || isActive;
if (isActive) {
ctx.fillStyle = palette.currentLine;
ctx.fillRect(gutterWidth + 1, y - lineHeight * 0.76, width - gutterWidth - 1, lineHeight);
}
if (hasStarted) {
const lineNumber = String(baseLineNo + lineIndex);
ctx.fillStyle = isActive ? palette.lineNoActive : palette.lineNo;
ctx.fillText(lineNumber, gutterWidth - 10 - ctx.measureText(lineNumber).width, y);
}
let drawnWidth = 0;
if (hasStarted && (charBudget > 0 || charBudget === Infinity)) {
drawnWidth = drawTokenLine(tokens, codeStartX, y, charBudget, palette);
}
if (isActive && writingPhase && Math.floor(elapsed / 430) % 2 === 0) {
caret = { x: codeStartX + drawnWidth + 1, y };
}
if (!hasStarted) continue;
if ((lineIndex + 7) % 19 === 0) {
ctx.beginPath();
ctx.fillStyle = palette.markerOk;
ctx.arc(12, y - lineHeight * 0.38, 3.2, 0, Math.PI * 2);
ctx.fill();
} else if ((lineIndex + 3) % 23 === 0) {
ctx.beginPath();
ctx.fillStyle = palette.markerWarn;
ctx.arc(12, y - lineHeight * 0.38, 3.2, 0, Math.PI * 2);
ctx.fill();
}
}
if (caret) {
ctx.fillStyle = palette.caret;
ctx.fillRect(caret.x, caret.y - fontSize, 1.8, Math.max(12, fontSize + 1));
}
};
const render = (timestamp) => {
const elapsed = timestamp - startAt;
const palette = themes[getTheme()];
ctx.clearRect(0, 0, width, height);
drawCodeEditor(palette, elapsed, false);
rafId = requestAnimationFrame(render);
};
const drawStatic = () => {
const palette = themes[getTheme()];
ctx.clearRect(0, 0, width, height);
drawCodeEditor(palette, 0, true);
};
const stop = () => {
if (!rafId) return;
cancelAnimationFrame(rafId);
rafId = null;
};
const start = () => {
stop();
resizeCanvas();
startAt = performance.now();
if (reducedMotionQuery.matches) {
drawStatic();
return;
}
rafId = requestAnimationFrame(render);
};
const onResize = () => {
resizeCanvas();
if (reducedMotionQuery.matches) {
drawStatic();
}
};
const themeObserver = new MutationObserver(() => {
if (reducedMotionQuery.matches) {
drawStatic();
}
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
if (typeof ResizeObserver !== 'undefined') {
const resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(hero);
}
if (reducedMotionQuery.addEventListener) {
reducedMotionQuery.addEventListener('change', start);
} else if (reducedMotionQuery.addListener) {
reducedMotionQuery.addListener(start);
}
window.addEventListener('resize', onResize);
window.addEventListener('load', onResize);
start();
})();
+5 -9
View File
@@ -28,11 +28,10 @@ function itstudio_enqueue_scripts() {
wp_enqueue_style('itstudio-footer', get_template_directory_uri() . '/assets/css/footer.css', array('itstudio-style'), '2.1.2');
wp_enqueue_style('itstudio-content', get_template_directory_uri() . '/assets/css/content.css', array('itstudio-style'), '2.1.2');
// 仅在首页加载 Hero 样式
// 仅在首页加载首页样式
if (is_front_page() || is_home()) {
wp_enqueue_style('itstudio-front-page', get_template_directory_uri() . '/assets/css/front-page.css', array('itstudio-style'), '2.1.2');
wp_enqueue_script('itstudio-hero-waves', get_template_directory_uri() . '/assets/js/hero-waves.js', array(), '1.0.0', true);
wp_enqueue_script('itstudio-home-hero', get_template_directory_uri() . '/assets/js/home-hero.js', array(), '1.0.0', true);
wp_enqueue_script('itstudio-landing-hero-canvas', get_template_directory_uri() . '/assets/js/landing-hero-canvas.js', array(), '1.0.0', true);
}
// 仅在工作室介绍页加载(包含 /about fallback
@@ -44,17 +43,14 @@ function itstudio_enqueue_scripts() {
}
if ($is_about) {
wp_enqueue_style('itstudio-intro', get_template_directory_uri() . '/assets/css/intro.css', array('itstudio-content'), '2.1.2');
wp_enqueue_script('itstudio-intro-scroll', get_template_directory_uri() . '/assets/js/intro-scroll.js', array(), '1.0.0', true);
wp_enqueue_style('itstudio-about-hero', get_template_directory_uri() . '/assets/css/about-hero.css', array('itstudio-content'), '2.1.2');
wp_enqueue_script('itstudio-about-hero-waves', get_template_directory_uri() . '/assets/js/hero-waves.js', array(), '1.0.0', true);
wp_enqueue_script('itstudio-about-hero', get_template_directory_uri() . '/assets/js/home-hero.js', array(), '1.0.0', true);
}
// Scripts
wp_enqueue_script('itstudio-theme-toggle', get_template_directory_uri() . '/assets/js/theme-toggle.js', array(), '1.0.0', true);
wp_enqueue_script('itstudio-lang-toggle', get_template_directory_uri() . '/assets/js/lang-toggle.js', array(), '1.0.0', true);
// 注册并加载打字机效果脚本 - 仅在工作室介绍页
if ($is_about) {
wp_enqueue_script('itstudio-stream', get_template_directory_uri() . '/assets/js/stream.js', array(), '1.0.0', true);
}
wp_enqueue_script('itstudio-main', get_template_directory_uri() . '/assets/js/main.js', array(), '1.0.0', true);
}
add_action('wp_enqueue_scripts', 'itstudio_enqueue_scripts');
+13 -2
View File
@@ -74,11 +74,22 @@
<span></span>
<span></span>
</button>
<?php
$announcement_nav_url = get_post_type_archive_link('announcement');
if (!$announcement_nav_url) {
$announcement_nav_url = home_url('/announcements');
}
$posts_page_id = (int) get_option('page_for_posts');
$blog_nav_url = $posts_page_id ? get_permalink($posts_page_id) : '';
if (!$blog_nav_url) {
$blog_nav_url = home_url('/blog');
}
?>
<ul class="nav-menu">
<li><a href="<?php echo esc_url(home_url('/')); ?>" data-cn="首页" data-en="Home"></a></li>
<li><a href="<?php echo esc_url(home_url('/announcements')); ?>" data-cn="公告通知" data-en="Events"></a></li>
<li><a href="<?php echo esc_url(home_url('/blog')); ?>" data-cn="技术博客" data-en="Blog"></a></li>
<li><a href="<?php echo esc_url($announcement_nav_url); ?>" data-cn="公告通知" data-en="Events"></a></li>
<li><a href="<?php echo esc_url($blog_nav_url); ?>" data-cn="技术博客" data-en="Blog"></a></li>
<li><a href="<?php echo esc_url(home_url('/services')); ?>" data-cn="便民服务" data-en="Service"></a></li>
<li><a href="<?php echo esc_url(home_url('/about')); ?>" data-cn="工作室介绍" data-en="Introduction"></a></li>
<li><a href="<?php echo esc_url(home_url('/join')); ?>" data-cn="加入我们" data-en="Join"></a></li>
+90 -77
View File
@@ -1,29 +1,35 @@
<?php get_header(); ?>
<main class="site-main">
<canvas class="hero-waves" aria-hidden="true"></canvas>
<section class="hero-section">
<main class="site-main home-landing">
<?php
$announcement_archive_url = get_post_type_archive_link('announcement');
if (!$announcement_archive_url) {
$announcement_archive_url = home_url('/announcements');
}
$posts_page_id = (int) get_option('page_for_posts');
$blog_archive_url = $posts_page_id ? get_permalink($posts_page_id) : '';
if (!$blog_archive_url) {
$blog_archive_url = home_url('/blog');
}
?>
<section class="landing-hero">
<canvas class="landing-hero-canvas" aria-hidden="true"></canvas>
<div class="container">
<div class="hero-content">
<h1 class="hero-title">
<span class="hero-title-svg" aria-hidden="true"></span>
<span class="hero-title-text" data-cn="爱特工作室" data-en="IT STUDIO"></span>
</h1>
<div class="landing-hero-content">
<h1 class="landing-hero-title" data-cn="爱特工作室" data-en="IT STUDIO"></h1>
<p class="landing-hero-subtitle" data-cn="中国海洋大学信息技术与工程实践团队" data-en="Technology and Engineering Practice Team at OUC"></p>
<a class="landing-hero-btn" href="<?php echo esc_url(home_url('/about')); ?>" data-cn="了解更多" data-en="Learn More"></a>
</div>
</div>
<div class="hero-scroll-indicator" aria-hidden="true">
<span class="scroll-arrow"></span>
<span class="scroll-text" data-cn="向下滑动开始" data-en="Scroll to begin"></span>
</div>
</section>
<section class="services-section">
<div class="container">
<!-- 服务提供模块 -->
<div class="services-provided">
<h2 data-cn="# 服务提供" data-en="# Our Services"></h2>
<h2 data-cn="@ 服务提供" data-en="@ Services"></h2>
<div class="services-grid-box">
<a href="#" class="service-item">
<div class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 14L24 14L28 20H54C56.2091 20 58 21.7909 58 24V50C58 52.2091 56.2091 54 54 54H10C7.79086 54 6 52.2091 6 50V18C6 15.7909 7.79086 14 10 14Z" stroke="#ccd6f6"/>
@@ -33,9 +39,8 @@
</svg>
</div>
<span data-cn="资源站" data-en="Resources"></span>
</a>
<a href="#" class="service-item">
</div>
<div class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="32" cy="32" r="24" stroke="#ccd6f6"/>
@@ -48,9 +53,8 @@
</svg>
</div>
<span data-cn="校内镜像站" data-en="Mirror Site"></span>
</a>
<a href="#" class="service-item">
</div>
<div class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="16" y1="12" x2="16" y2="52" stroke="#ccd6f6"/>
@@ -62,9 +66,8 @@
</svg>
</div>
<span data-cn="代码托管" data-en="Git Hosting"></span>
</a>
<a href="#" class="service-item">
</div>
<div class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M32 8L54 20V44L32 56L10 44V20L32 8Z" stroke="#ccd6f6"/>
@@ -75,9 +78,8 @@
</svg>
</div>
<span data-cn="Minecraft服务器" data-en="Minecraft Server"></span>
</a>
<a href="#" class="service-item">
</div>
<div class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 16H42V40H20L12 48V16Z" stroke="#ccd6f6"/>
@@ -87,9 +89,8 @@
</svg>
</div>
<span data-cn="OUC论坛" data-en="OUC Forum"></span>
</a>
<a href="#" class="service-item">
</div>
<div class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M46 14L50 18L26 42L22 38L46 14Z" stroke="#ccd6f6"/>
@@ -101,9 +102,8 @@
</svg>
</div>
<span data-cn="电脑维修" data-en="PC Repair"></span>
</a>
<a href="#" class="service-item">
</div>
<div class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="10" y="12" width="44" height="40" rx="4" stroke="#ccd6f6"/>
@@ -116,9 +116,8 @@
</svg>
</div>
<span data-cn="五八工坊预约" data-en="Workshop Booking"></span>
</a>
<a href="#" class="service-item">
</div>
<div class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M32 56C32 56 56 44 56 26C56 16 46 10 38 16C35 18 32 22 32 22C32 22 29 18 26 16C18 10 8 16 8 26C8 44 32 56 32 56Z" stroke="#ccd6f6"/>
@@ -129,86 +128,100 @@
</svg>
</div>
<span data-cn="OUC便民服务" data-en="OUC Services"></span>
</a>
</div>
</div>
</div>
</div>
</section>
<section class="content-section">
<section class="landing-updates">
<div class="container">
<div class="content-grid">
<div class="announcements-column">
<h2 data-cn="# 公告通知" data-en="# Announcements"></h2>
<div class="post-list">
<div class="landing-updates-grid">
<article class="landing-feed-box">
<header class="landing-feed-head">
<h2 data-cn="@ 公告通知" data-en="@ Announcements"></h2>
<a href="<?php echo esc_url($announcement_archive_url); ?>" data-cn="更多" data-en="More"></a>
</header>
<ul class="landing-feed">
<?php
$announcements = new WP_Query(array(
'post_type' => 'announcement',
'post_status' => 'publish',
'posts_per_page' => 5,
'orderby' => 'date',
'order' => 'DESC'
'order' => 'DESC',
'ignore_sticky_posts' => true,
'no_found_rows' => true
));
if ($announcements->have_posts()) :
while ($announcements->have_posts()) : $announcements->the_post();
$announcement_excerpt = get_the_excerpt();
if ('' === trim($announcement_excerpt)) {
$announcement_excerpt = wp_trim_words(wp_strip_all_tags(get_the_content()), 40, '...');
}
?>
<article class="post-item">
<div class="post-content-wrapper">
<h3 class="post-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<div class="post-excerpt">
<?php the_excerpt(); ?>
<li class="landing-feed-item">
<h3 class="landing-feed-title">
<a class="landing-feed-link" href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h3>
<div class="landing-feed-meta">
<span class="landing-feed-author"><?php echo esc_html(get_the_author()); ?></span>
<time datetime="<?php echo get_the_date('c'); ?>"><?php echo get_the_date('Y-m-d'); ?></time>
</div>
<div class="post-meta">
<span class="post-author"><?php echo get_the_author(); ?></span>
<time datetime="<?php echo get_the_date('c'); ?>"><?php echo get_the_date('Y年m月d日'); ?></time>
</div>
</div>
</article>
<p class="landing-feed-excerpt"><?php echo esc_html($announcement_excerpt); ?></p>
</li>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<p data-cn="暂无公告" data-en="No announcements found."></p>
<li class="landing-feed-empty" data-cn="暂无公告" data-en="No announcements found."></li>
<?php endif; ?>
</div>
</div>
</ul>
</article>
<div class="blog-column">
<h2 data-cn="# 技术博客" data-en="# Blog"></h2>
<div class="post-list">
<article class="landing-feed-box">
<header class="landing-feed-head">
<h2 data-cn="@ 技术博客" data-en="@ Blog"></h2>
<a href="<?php echo esc_url($blog_archive_url); ?>" data-cn="更多" data-en="More"></a>
</header>
<ul class="landing-feed">
<?php
$blogs = new WP_Query(array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 5,
'orderby' => 'date',
'order' => 'DESC'
'order' => 'DESC',
'ignore_sticky_posts' => true,
'no_found_rows' => true
));
if ($blogs->have_posts()) :
while ($blogs->have_posts()) : $blogs->the_post();
$blog_excerpt = get_the_excerpt();
if ('' === trim($blog_excerpt)) {
$blog_excerpt = wp_trim_words(wp_strip_all_tags(get_the_content()), 40, '...');
}
?>
<article class="post-item">
<div class="post-content-wrapper">
<h3 class="post-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<div class="post-excerpt">
<?php the_excerpt(); ?>
<li class="landing-feed-item">
<h3 class="landing-feed-title">
<a class="landing-feed-link" href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h3>
<div class="landing-feed-meta">
<span class="landing-feed-author"><?php echo esc_html(get_the_author()); ?></span>
<time datetime="<?php echo get_the_date('c'); ?>"><?php echo get_the_date('Y-m-d'); ?></time>
</div>
<div class="post-meta">
<span class="post-author"><?php echo get_the_author(); ?></span>
<time datetime="<?php echo get_the_date('c'); ?>"><?php echo get_the_date('Y年m月d日'); ?></time>
</div>
</div>
</article>
<p class="landing-feed-excerpt"><?php echo esc_html($blog_excerpt); ?></p>
</li>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<p data-cn="暂无博客文章" data-en="No blog posts found."></p>
<li class="landing-feed-empty" data-cn="暂无博客文章" data-en="No blog posts found."></li>
<?php endif; ?>
</div>
</div>
</ul>
</article>
</div>
</div>
</section>
+194 -146
View File
@@ -3,172 +3,220 @@
get_header();
?>
<main class="site-main intro-page">
<section class="intro-hero intro-step is-active" id="intro">
<div class="container intro-hero-grid">
<div class="intro-copy">
<h1 class="intro-title intro-animate" style="--delay: 0.05s" data-cn="与海同频的技术社团" data-en="A Tech Studio Tuned to the Ocean"></h1>
<p class="intro-lead intro-animate stream-text" style="--delay: 0.1s" data-cn="中国海洋大学爱特工作室,自 2002 年以来聚焦人才培养与真实项目,连接设计、开发与创新实践。" data-en="Since 2002, IT Studio at OUC connects design, engineering, and real projects to grow talent."></p>
<p class="intro-sub intro-animate" style="--delay: 0.15s" data-cn="在海洋与科技之间,探索更有温度的数字创造。" data-en="Where ocean spirit meets modern engineering."></p>
<div class="intro-cta-group intro-animate" style="--delay: 0.2s">
<a class="intro-btn intro-btn--primary" href="#overview" data-cn="开始探索" data-en="Explore" aria-label="Explore"></a>
<a class="intro-btn intro-btn--ghost" href="<?php echo esc_url(home_url('/join')); ?>" data-cn="加入我们" data-en="Join Us" aria-label="Join Us"></a>
</div>
<div class="intro-stats intro-animate" style="--delay: 0.25s">
<div class="intro-stat">
<span class="stat-number">2002</span>
<span class="stat-label" data-cn="成立" data-en="Founded"></span>
</div>
<div class="intro-stat">
<span class="stat-number">6</span>
<span class="stat-label" data-cn="方向" data-en="Tracks"></span>
</div>
<div class="intro-stat">
<span class="stat-number">20+</span>
<span class="stat-label" data-cn="积累" data-en="Years"></span>
<main class="site-main">
<canvas class="hero-waves" aria-hidden="true"></canvas>
<section class="hero-section">
<div class="container">
<div class="hero-content">
<h1 class="hero-title">
<span class="hero-title-svg" aria-hidden="true"></span>
<span class="hero-title-text" data-cn="爱特工作室" data-en="IT STUDIO"></span>
</h1>
</div>
</div>
<div class="hero-scroll-indicator" aria-hidden="true">
<span class="scroll-arrow"></span>
<span class="scroll-text" data-cn="向下滑动开始" data-en="Scroll to begin"></span>
</div>
</section>
<section class="services-section">
<div class="container">
<div class="services-provided">
<h2 data-cn="# 服务提供" data-en="# Our Services"></h2>
<div class="services-grid-box">
<a href="#" class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 14L24 14L28 20H54C56.2091 20 58 21.7909 58 24V50C58 52.2091 56.2091 54 54 54H10C7.79086 54 6 52.2091 6 50V18C6 15.7909 7.79086 14 10 14Z" stroke="#ccd6f6"/>
<path d="M22 34H42" stroke="#64ffda"/>
<path d="M22 42H34" stroke="#64ffda"/>
<circle cx="48" cy="42" r="2" fill="#64ffda" stroke="none"/>
</svg>
</div>
<span data-cn="资源站" data-en="Resources"></span>
</a>
<a href="#" class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="32" cy="32" r="24" stroke="#ccd6f6"/>
<circle cx="32" cy="32" r="8" stroke="#ccd6f6"/>
<path d="M32 24V8" stroke="#64ffda"/>
<path d="M32 40V56" stroke="#233554"/>
<path d="M49 32H56" stroke="#64ffda"/>
<path d="M8 32H15" stroke="#233554"/>
<path d="M44 14C48 18 50 24 50 32" stroke="#64ffda" stroke-dasharray="4 4"/>
</svg>
</div>
<span data-cn="校内镜像站" data-en="Mirror Site"></span>
</a>
<a href="#" class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="16" y1="12" x2="16" y2="52" stroke="#ccd6f6"/>
<circle cx="16" cy="12" r="4" stroke="#ccd6f6"/>
<circle cx="16" cy="32" r="4" stroke="#ccd6f6"/>
<circle cx="16" cy="52" r="4" stroke="#ccd6f6"/>
<path d="M16 32C26 32 36 36 36 44V48" stroke="#64ffda"/>
<circle cx="36" cy="48" r="4" stroke="#64ffda"/>
</svg>
</div>
<span data-cn="代码托管" data-en="Git Hosting"></span>
</a>
<a href="#" class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M32 8L54 20V44L32 56L10 44V20L32 8Z" stroke="#ccd6f6"/>
<path d="M10 20L32 32L54 20" stroke="#ccd6f6"/>
<path d="M32 56V32" stroke="#ccd6f6"/>
<path d="M26 16L32 20L38 16" stroke="#64ffda"/>
<path d="M46 29L46 38" stroke="#64ffda"/>
</svg>
</div>
<span data-cn="Minecraft服务器" data-en="Minecraft Server"></span>
</a>
<a href="#" class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 16H42V40H20L12 48V16Z" stroke="#ccd6f6"/>
<path d="M26 10H56V34H44L38 40V34H26V10Z" stroke="#64ffda"/>
<line x1="32" y1="20" x2="50" y2="20" stroke="#64ffda"/>
<line x1="32" y1="26" x2="44" y2="26" stroke="#64ffda"/>
</svg>
</div>
<span data-cn="OUC论坛" data-en="OUC Forum"></span>
</a>
<a href="#" class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M46 14L50 18L26 42L22 38L46 14Z" stroke="#ccd6f6"/>
<path d="M22 38L14 46L18 50L26 42" stroke="#ccd6f6"/>
<path d="M14 46L10 54" stroke="#ccd6f6"/>
<path d="M45 28L36 19" stroke="#64ffda"/>
<path d="M20 44L14 50" stroke="#233554"/>
<path d="M50 15C53 12 58 12 60 14C62 16 62 21 59 24L34 49C32 51 29 51 27 49L25 47C23 45 23 42 25 40L50 15Z" stroke="#64ffda"/>
</svg>
</div>
<span data-cn="电脑维修" data-en="PC Repair"></span>
</a>
<a href="#" class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="10" y="12" width="44" height="40" rx="4" stroke="#ccd6f6"/>
<path d="M10 24H54" stroke="#ccd6f6"/>
<path d="M20 6V16" stroke="#ccd6f6"/>
<path d="M44 6V16" stroke="#ccd6f6"/>
<circle cx="32" cy="38" r="6" stroke="#64ffda"/>
<path d="M32 38L36 34" stroke="#64ffda"/>
<circle cx="32" cy="38" r="2" fill="#64ffda" stroke="none"/>
</svg>
</div>
<span data-cn="五八工坊预约" data-en="Workshop Booking"></span>
</a>
<a href="#" class="service-item">
<div class="service-icon">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M32 56C32 56 56 44 56 26C56 16 46 10 38 16C35 18 32 22 32 22C32 22 29 18 26 16C18 10 8 16 8 26C8 44 32 56 32 56Z" stroke="#ccd6f6"/>
<path d="M32 22V36" stroke="#64ffda"/>
<circle cx="32" cy="40" r="3" stroke="#64ffda"/>
<path d="M18 28H24L26 32" stroke="#233554"/>
<path d="M46 28H40L38 32" stroke="#64ffda"/>
</svg>
</div>
<span data-cn="OUC便民服务" data-en="OUC Services"></span>
</a>
</div>
<div class="intro-hero-media">
<div class="media-card media-card--tall intro-animate" style="--delay: 0.15s" data-cn="视频占位:工作室剪影" data-en="Video Slot: Studio Reel"></div>
<div class="media-card intro-animate" style="--delay: 0.2s" data-cn="图片占位:成员日常" data-en="Image Slot: Daily Life"></div>
<div class="media-card intro-animate" style="--delay: 0.25s" data-cn="图片占位:赛事/项目" data-en="Image Slot: Projects & Competitions"></div>
</div>
</div>
</section>
<section class="intro-section intro-step" id="overview">
<div class="container intro-grid">
<div class="intro-copy">
<h2 class="intro-heading intro-animate" style="--delay: 0s" data-cn="工作室概述" data-en="Overview"></h2>
<p class="intro-text intro-animate" style="--delay: 0.05s" data-cn="依托信息科学与工程学部,爱特坚持项目驱动与教学并行,面向真实场景输出作品与人才。" data-en="Backed by the School of Information Science and Engineering, IT Studio blends project delivery with hands-on training."></p>
<ul class="intro-points">
<li class="intro-animate" style="--delay: 0.1s"><span data-cn="项目驱动,贯通设计到开发" data-en="Project-driven pipeline from design to delivery"></span></li>
<li class="intro-animate" style="--delay: 0.15s"><span data-cn="课堂与实践并行,培养工程思维" data-en="Learning meets practice to build engineering mindset"></span></li>
<li class="intro-animate" style="--delay: 0.2s"><span data-cn="开放协作,跨方向互相赋能" data-en="Open collaboration across tracks"></span></li>
</ul>
</div>
<div class="intro-media">
<div class="media-card media-card--wide intro-animate" style="--delay: 0.1s" data-cn="视频占位:项目发布" data-en="Video Slot: Project Launch"></div>
<div class="media-card intro-animate" style="--delay: 0.15s" data-cn="图片占位:工作室环境" data-en="Image Slot: Workspace"></div>
<div class="media-card intro-animate" style="--delay: 0.2s" data-cn="图片占位:团队合影" data-en="Image Slot: Team Photo"></div>
</div>
</div>
</section>
<section class="content-section">
<div class="container">
<div class="content-grid">
<div class="announcements-column">
<h2 data-cn="# 公告通知" data-en="# Announcements"></h2>
<div class="post-list">
<?php
$announcements = new WP_Query(array(
'post_type' => 'announcement',
'post_status' => 'publish',
'posts_per_page' => 5,
'orderby' => 'date',
'order' => 'DESC',
'ignore_sticky_posts' => true,
'no_found_rows' => true
));
<section class="intro-section intro-step" id="structure">
<div class="container intro-grid reverse">
<div class="intro-copy">
<h2 class="intro-heading intro-animate" style="--delay: 0s" data-cn="组织架构" data-en="Organization"></h2>
<p class="intro-text intro-animate" style="--delay: 0.05s" data-cn="矩阵式结构让每个方向深耕专业,同时保持跨团队协作与统一标准。" data-en="A matrix structure keeps each track focused while enabling cross-team collaboration."></p>
<div class="intro-org-grid">
<div class="intro-card intro-animate" style="--delay: 0.1s">
<h3 data-cn="产品与设计" data-en="Product & Design"></h3>
<p data-cn="从需求到体验,定义产品路径" data-en="Define experience from insights to prototype"></p>
if ($announcements->have_posts()) :
while ($announcements->have_posts()) : $announcements->the_post();
?>
<article class="post-item">
<div class="post-content-wrapper">
<h3 class="post-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<div class="post-excerpt">
<?php the_excerpt(); ?>
</div>
<div class="intro-card intro-animate" style="--delay: 0.12s">
<h3 data-cn="Web 开发" data-en="Web Development"></h3>
<p data-cn="构建稳定高效的应用与平台" data-en="Build reliable platforms and services"></p>
</div>
<div class="intro-card intro-animate" style="--delay: 0.14s">
<h3 data-cn="程序设计" data-en="Programming"></h3>
<p data-cn="算法与工程能力双线成长" data-en="Algorithmic rigor with engineering skill"></p>
</div>
<div class="intro-card intro-animate" style="--delay: 0.16s">
<h3 data-cn="移动开发" data-en="Mobile Development"></h3>
<p data-cn="打造便捷流畅的移动体验" data-en="Deliver polished mobile experiences"></p>
</div>
<div class="intro-card intro-animate" style="--delay: 0.18s">
<h3 data-cn="游戏与交互" data-en="Game & Interaction"></h3>
<p data-cn="创意玩法与交互体验探索" data-en="Experiment with interactive innovation"></p>
</div>
<div class="intro-card intro-animate" style="--delay: 0.2s">
<h3 data-cn="运营与传播" data-en="Operations & Media"></h3>
<p data-cn="记录成长与团队品牌传播" data-en="Tell stories and amplify impact"></p>
<div class="post-meta">
<span class="post-author"><?php echo get_the_author(); ?></span>
<time datetime="<?php echo get_the_date('c'); ?>"><?php echo get_the_date('Y年m月d日'); ?></time>
</div>
</div>
</div>
<div class="intro-media">
<div class="media-card media-card--wide intro-animate" style="--delay: 0.1s" data-cn="结构图占位:组织矩阵" data-en="Org Chart Slot"></div>
<div class="media-card intro-animate" style="--delay: 0.15s" data-cn="图片占位:部门协作" data-en="Image Slot: Collaboration"></div>
<div class="media-card intro-animate" style="--delay: 0.2s" data-cn="图片占位:技术分享" data-en="Image Slot: Tech Talk"></div>
</article>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<p data-cn="暂无公告" data-en="No announcements found."></p>
<?php endif; ?>
</div>
</div>
</section>
<section class="intro-section intro-step" id="culture">
<div class="container intro-grid">
<div class="intro-copy">
<h2 class="intro-heading intro-animate" style="--delay: 0s" data-cn="社团特色" data-en="Culture"></h2>
<p class="intro-text intro-animate" style="--delay: 0.05s" data-cn="舒适环境与严谨技术并重,爱特强调分享、共创与持续成长。" data-en="We balance a comfortable space with rigorous craft, highlighting sharing and growth."></p>
<div class="intro-feature-grid">
<div class="intro-card intro-animate" style="--delay: 0.1s">
<h3 data-cn="海洋元素空间" data-en="Ocean-inspired Space"></h3>
<p data-cn="清爽明快的海洋色调与科技感布局" data-en="Ocean palettes with a modern tech layout"></p>
</div>
<div class="intro-card intro-animate" style="--delay: 0.14s">
<h3 data-cn="导师与学长共创" data-en="Mentors & Alumni"></h3>
<p data-cn="前辈指导与同行交流并行" data-en="Guided mentorship and peer exchange"></p>
</div>
<div class="intro-card intro-animate" style="--delay: 0.18s">
<h3 data-cn="分享与教学氛围" data-en="Teaching Culture"></h3>
<p data-cn="内部课程、公开分享与成果展示" data-en="Workshops, open talks, and demos"></p>
</div>
</div>
</div>
<div class="intro-media">
<div class="media-card media-card--tall intro-animate" style="--delay: 0.12s" data-cn="视频占位:空间巡礼" data-en="Video Slot: Studio Tour"></div>
<div class="media-card intro-animate" style="--delay: 0.16s" data-cn="图片占位:教学现场" data-en="Image Slot: Workshop"></div>
<div class="media-card intro-animate" style="--delay: 0.2s" data-cn="图片占位:文化活动" data-en="Image Slot: Community"></div>
</div>
</div>
</section>
<div class="blog-column">
<h2 data-cn="# 技术博客" data-en="# Blog"></h2>
<div class="post-list">
<?php
$blogs = new WP_Query(array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 5,
'orderby' => 'date',
'order' => 'DESC',
'ignore_sticky_posts' => true,
'no_found_rows' => true
));
<section class="intro-section intro-step" id="advantages">
<div class="container intro-grid reverse">
<div class="intro-copy">
<h2 class="intro-heading intro-animate" style="--delay: 0s" data-cn="独特优势" data-en="Advantages"></h2>
<p class="intro-text intro-animate" style="--delay: 0.05s" data-cn="资源与支持覆盖从想法到落地的全链路,帮助成员更快成长。" data-en="Resources cover the full cycle from idea to launch, accelerating growth."></p>
<div class="intro-adv-grid">
<div class="intro-card intro-animate" style="--delay: 0.1s">
<h3 data-cn="资源池" data-en="Resources"></h3>
<ul class="intro-list">
<li><span data-cn="高性能计算与私有服务器支持" data-en="High-performance compute and servers"></span></li>
<li><span data-cn="专属场地与硬件设备" data-en="Dedicated space and hardware"></span></li>
<li><span data-cn="校内导师与行业资源" data-en="Academic and industry mentors"></span></li>
<li><span data-cn="优秀学长学姐经验传承" data-en="Alumni knowledge sharing"></span></li>
</ul>
if ($blogs->have_posts()) :
while ($blogs->have_posts()) : $blogs->the_post();
?>
<article class="post-item">
<div class="post-content-wrapper">
<h3 class="post-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<div class="post-excerpt">
<?php the_excerpt(); ?>
</div>
<div class="intro-card intro-animate" style="--delay: 0.14s">
<h3 data-cn="成员福利" data-en="Benefits"></h3>
<ul class="intro-list">
<li><span data-cn="主流 AI 工具与算力支持" data-en="Access to AI tools and compute"></span></li>
<li><span data-cn="赛事奖励与项目展示机会" data-en="Competitions and demo opportunities"></span></li>
<li><span data-cn="企业项目与实习推荐" data-en="Industry projects and internships"></span></li>
<li><span data-cn="礼品激励与成长认证" data-en="Rewards and growth recognition"></span></li>
</ul>
<div class="post-meta">
<span class="post-author"><?php echo get_the_author(); ?></span>
<time datetime="<?php echo get_the_date('c'); ?>"><?php echo get_the_date('Y年m月d日'); ?></time>
</div>
</div>
</div>
<div class="intro-media">
<div class="media-card media-card--wide intro-animate" style="--delay: 0.12s" data-cn="视频占位:算力与设备" data-en="Video Slot: Compute Lab"></div>
<div class="media-card intro-animate" style="--delay: 0.16s" data-cn="图片占位:资源展示" data-en="Image Slot: Resources"></div>
<div class="media-card intro-animate" style="--delay: 0.2s" data-cn="图片占位:奖励与成果" data-en="Image Slot: Achievements"></div>
</article>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<p data-cn="暂无博客文章" data-en="No blog posts found."></p>
<?php endif; ?>
</div>
</div>
</section>
<section class="intro-section intro-step intro-join" id="join">
<div class="container intro-join-grid">
<div class="intro-join-content">
<h2 class="intro-heading intro-animate" style="--delay: 0s" data-cn="加入爱特工作室" data-en="Join IT Studio"></h2>
<p class="intro-text intro-animate" style="--delay: 0.05s" data-cn="如果你热爱技术与创造,我们欢迎你。一起把想法变成作品。" data-en="If you love building with technology, you're welcome here. Let's turn ideas into impact."></p>
<a class="intro-btn intro-btn--primary intro-animate" style="--delay: 0.1s" href="<?php echo esc_url(home_url('/join')); ?>" data-cn="立即加入" data-en="Apply Now" aria-label="Apply Now"></a>
</div>
<div class="intro-join-media intro-animate" style="--delay: 0.15s">
<div class="media-card" data-cn="图片占位:招新现场" data-en="Image Slot: Recruitment"></div>
<div class="media-card" data-cn="图片占位:成员合影" data-en="Image Slot: Team Moment"></div>
<div class="media-card" data-cn="视频占位:未来愿景" data-en="Video Slot: Vision"></div>
</div>
</div>
</section>