convert to plugins+theme

This commit is contained in:
OldTaoge
2026-07-21 09:55:39 +08:00
parent 803841cd12
commit aea6095c0a
57 changed files with 2742 additions and 1619 deletions
+10
View File
@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ITStudioMainSite.iml" filepath="$PROJECT_DIR$/.idea/ITStudioMainSite.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+16
View File
@@ -0,0 +1,16 @@
<?php get_header(); ?>
<main class="site-main error-404">
<div class="container">
<div class="error-content">
<h1 class="error-title">404</h1>
<h2><?php _e('页面未找到', 'itstudio'); ?></h2>
<p><?php _e('抱歉,您访问的页面不存在。', 'itstudio'); ?></p>
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn-home">
<?php _e('返回首页', 'itstudio'); ?>
</a>
</div>
</div>
</main>
<?php get_footer(); ?>
+291
View File
@@ -0,0 +1,291 @@
<div align="center">
<img src="resources/it_logo_2024.svg" width=100/>
</div>
<div align="center">
<h1>爱特工作室总站</h1>
</div>
<br>
<div align="center">
<img src="resources/Organization-ITstudio-shield.svg"/><br>
<img src="https://img.shields.io/badge/Language-php-purple">
<img src="https://img.shields.io/badge/Language-javascript-yellow">
<img src="https://img.shields.io/badge/Language-HTML-green">
<img src="https://img.shields.io/badge/Language-CSS-blue">
<img src="https://img.shields.io/badge/WordPress-Theme-21759b">
</div>
<br>
## 📖 项目简介
中国海洋大学爱特工作室官方网站 WordPress 主题。这是一个现代化、响应式的主题,支持暗色/亮色模式切换,融合海洋元素与 IT 风格。
## ✨ 主要特性
- 🎨 **现代简洁设计** - IT风格与海洋元素完美融合
- 🌓 **主题切换** - 支持暗色/亮色模式,可根据系统主题自动切换
- 📱 **响应式布局** - 完美适配 PC 端和移动端
- 🌐 **多语言支持** - 支持中文/英文切换
- ⚡ **性能优化** - 轻量级设计,加载快速
- 🎯 **自定义内容类型** - 支持公告通知、社团新闻、便民服务独立管理
## 🚀 快速开始
### 安装方法
1. 克隆本仓库到 WordPress 主题目录:
```bash
cd wp-content/themes
git clone https://github.com/itstudio-2002/ITStudioMainSite.git
```
2. 在 WordPress 后台激活主题:
- 进入 **外观 > 主题**
- 找到 "IT Studio Theme"
- 点击 **激活**
### 主题配置
#### 1. 导航菜单设置
进入 **外观 > 菜单**,创建主导航菜单并分配到 "Primary Menu" 位置。
推荐菜单结构:
- 首页
- 公告通知
- 博客
- 便民服务
- 社团介绍
- 加入我们
#### 2. 发布公告
主题内置了"公告通知"自定义文章类型:
1. 进入 **Announcement > 新建 Announcement**
2. 填写标题和内容
3. 发布即可在首页显示
#### 3. 发布新闻
主题内置了"社团新闻"自定义文章类型:
1. 进入 **News > 新建 News**
2. 填写标题和内容
3. 发布即可在首页显示
#### 4. 新闻公告页权重字段(ACF)
新闻公告页右侧“高权重文章”读取 ACF 数值字段 `itstudio_weight`(数值越大,排序越靠前)。
适用范围:
- 默认文章(`post`)
- 公告(`announcement`)
- 新闻(`news`)
设置步骤:
1. 安装并启用插件 **Advanced Custom Fields (ACF)**。
2. 主题会自动注册字段组“内容权重”,字段名为 `itstudio_weight`(显示名“权重”)。
3. 编辑文章时在右侧填写“权重”数值(例如 `10`、`50`、`100`)。
4. 更新或发布文章。
说明:
- 当前仅读取 ACF 字段值,不再使用 Gutenberg 的“自定义字段”面板作为权重来源。
- 未设置 `itstudio_weight` 的文章按 `0` 处理。
- 当高权重文章不足 4 篇时,页面会自动用最新文章补足。
#### 5. 维护便民服务目录
主题内置了“便民服务(service)”内容类型,`/services` 页面会自动读取并展示。
每条服务信息来源:
- 图标:文章特色图(Featured Image)
- 名称:服务双语字段(中文名称 / 英文名称)
- 简介:服务双语字段(中文简介 / 英文简介)
- 类别:服务分类双语字段(中文名称 / 英文名称)
- 跳转链接:编辑页中的“服务跳转链接”字段
- 访问属性:编辑页中的“是否为校内服务”勾选项
后台新增步骤:
1. 进入 **便民服务 > 新增便民服务**。
2. 在“服务双语与链接”填写:
- 中文名称 / 英文名称
- 中文简介 / 英文简介
- 服务跳转链接
- 是否为校内服务(可选)
3. 设置特色图作为服务图标。
4. 在右侧选择或新建“服务分类”。
5. 发布后,该服务会自动出现在 `/services` 页面中。
分类双语设置:
1. 进入 **便民服务 > 服务分类**。
2. 新增或编辑分类时,可填写“中文名称”和“英文名称”。
3. 前台会根据语言切换自动显示对应分类名。
说明:
- 若未设置“服务跳转链接”,前端会回退到该服务文章自身链接。
- 若未设置特色图,前端会使用 `resources/it_logo_2024.svg` 作为默认图标。
- 勾选“是否为校内服务”后,服务卡片右上角会显示“仅校内访问”标签;不勾选则不显示该标签。
- 新增内容类型后若前台路由未生效,请进入 **设置 > 固定链接** 点击一次“保存更改”刷新重写规则。
## 🎨 设计规范
### 主题色彩
- **海洋浅蓝色** (#f0f8ff) - 亮色模式背景
- **海洋深蓝色** (#0a1929) - 暗色模式背景
- **强调色** (#8e88c7) - 链接、按钮等
- **中性色** - 灰度系列用于文本和边框
### 页面结构
- **Header** - 左侧 Logo,右侧导航栏、社交图标、主题切换按钮
- **首页** - 工作室介绍、公告列表、博客列表双栏布局
- **Footer** - 友情链接、联系方式、版权信息
## 📂 文件结构
```
ITStudioMainSite/
├── assets/
│ ├── css/
│ │ ├── content.css # 内容页样式
│ │ ├── front-page.css # 首页样式
│ │ └── services-page.css # 便民服务页样式
│ └── js/
│ ├── theme-toggle.js # 主题切换功能
│ └── main.js # 主要 JavaScript
├── resources/
│ ├── it_logo_2024.svg # 工作室 Logo
│ ├── ouc-logo.svg # 海大 Logo
│ └── ...
├── style.css # WordPress 主题样式表(必需)
├── functions.php # 主题功能文件
├── index.php # 首页模板
├── header.php # 头部模板
├── footer.php # 底部模板
├── single.php # 单篇文章模板
├── page.php # 页面模板
├── page-services.php # 便民服务页面模板
├── archive.php # 归档页面模板
└── 404.php # 404 错误页面
```
## 🛠️ 技术栈
- **WordPress** - 内容管理系统
- **PHP** - 后端开发语言
- **HTML5/CSS3** - 前端标记与样式
- **JavaScript** - 前端交互逻辑
- **SVG** - 矢量图标和 Logo
## 🔧 开发指南
### 本地开发环境
1. 安装 WordPress 本地开发环境(如 XAMPP、MAMP 等)
2. 克隆本仓库到 `wp-content/themes` 目录
3. 激活主题开始开发
### 自定义开发
- 修改 `assets/css/main.css` 自定义样式
- 编辑 `functions.php` 添加新功能
- 创建新的模板文件扩展功能
## 📝 使用许可
MIT License - 详见 LICENSE 文件
## 👥 关于我们
**爱特工作室**成立于 2002 年,是中国海洋大学信息科学与工程学部领导主持下的技术性团队。
🎯 **使命**: 发现人才,培养人才,输送人才
🔗 **链接**:
- GitHub: [https://github.com/ITStudioOUC](https://github.com/ITStudioOUC)
- Email: contact@itstudio.club
## 🤝 贡献
欢迎提交 Issue 和 Pull Request!
## 📄 更新日志
### v1.0.0 (2026-02-05)
- 🎉 初始版本发布
- ✅ 基础主题功能实现
- ✅ 响应式设计完成
- ✅ 主题切换功能
- ✅ 自定义文章类型(公告)
---
## 加入我们页面(/join)使用说明
主题已内置「加入我们」页面模板(`page-join.php`),并支持在没有创建 WordPress 页面时通过 `/join` 自动 fallback 渲染。
### 1. 需要安装的插件
1. **Formidable Forms**
用于报名表单渲染(前台报名)。
2. **WP Mail SMTP**
用于 Formidable 提交邮件发送(站点邮件走 SMTP)。
### 2. 后台时间节点设置
进入:**设置 > 招新设置**
可配置字段:
- 报名开始时间(datetime)
- 报名结束时间(datetime)
- 第一次面试开始/结束时间(datetime)
- 第一次面试地点(中/英)
- 第二次面试开始/结束时间(datetime)
- 第二次面试地点(中/英)
- 国庆能力摸底开始/结束日期(调试)
- 录取结果公布开始日期(date,系统自动延续 7 天)
- 报名表单 Shortcode
说明:
- 「国庆能力摸底阶段」固定为每年 **10/01 - 10/07**。
- 若填写“国庆能力摸底开始/结束日期(调试)”,将优先使用调试时间。
### 3. 阶段结果文件配置(重点)
在同一页面中,为以下阶段上传结果文件(CSV 或 XLSX):
- 第一次面试结果文件
- 国庆能力摸底结果文件
- 第二次面试结果文件
- 录取结果文件
文件列顺序必须为:
`姓名,QQ,邮箱,学号,手机,是否通过`
其中:
- `是否通过 = 1` 表示通过
- 其他任意值(含 `0`、空)都按未通过处理
注意:
- 不需要配置任何“字段映射”或“阶段识别字段”。
- 系统会自动按上述固定列进行识别与查询。
### 4. 前台显示逻辑
- 报名表单:仅在报名阶段显示。
- 录取进度查询:报名阶段不显示;报名结束后按当前阶段显示对应结果(使用姓名/QQ/邮箱/学号查询)。
- Canvas 进度条:根据当前时间自动高亮阶段(已完成/进行中/未开始)。
### 5. WP Mail SMTP 配置建议
1. 安装并启用 **WP Mail SMTP**。
2. 在插件中填写 SMTP 服务信息(Host、Port、加密方式、账号密码)。
3. 设置发件人邮箱与发件人名称。
4. 使用插件自带发送测试邮件,确认可达后再开放报名。
## 加入我们页面快速说明
- 后台路径:`设置 -> 招新设置`。
- 先配置时间节点:报名开始/结束、一面开始/结束、二面开始/结束、录取结果公布开始日期。
- “国庆能力摸底”默认是每年 `10/01 - 10/07`(可用调试日期覆盖)。
- 报名表单短代码使用 Formidable 表单,邮件发送建议配合 WP Mail SMTP。
- 阶段结果统一使用 CSV/XLSX 上传,不再使用 Formidable Entries 做结果查询。
+363
View File
@@ -0,0 +1,363 @@
<?php get_header(); ?>
<?php
$itstudio_archive_mode = isset($GLOBALS['itstudio_archive_mode']) ? sanitize_key((string) $GLOBALS['itstudio_archive_mode']) : '';
$is_itstudio_news_page = is_post_type_archive('news') || ($itstudio_archive_mode === 'news');
$is_itstudio_notice_page = is_post_type_archive('announcement') || ($itstudio_archive_mode === 'announcement');
$is_itstudio_content_page = $is_itstudio_news_page || $is_itstudio_notice_page;
?>
<?php if ($is_itstudio_content_page) : ?>
<?php
$keyword = isset($_GET['q']) ? sanitize_text_field(wp_unslash($_GET['q'])) : '';
$tag_slug = isset($_GET['tag']) ? sanitize_title(wp_unslash($_GET['tag'])) : '';
$paged = max(1, (int) get_query_var('paged'), (int) get_query_var('page'));
$active_post_type = $is_itstudio_news_page ? 'news' : 'announcement';
$archive_url = $is_itstudio_news_page ? get_post_type_archive_link('news') : get_post_type_archive_link('announcement');
if (!$archive_url) {
$archive_url = $is_itstudio_news_page ? home_url('/news/') : home_url('/announcement/');
}
$default_cover_url = get_template_directory_uri() . '/resources/it_logo_2024.svg';
$weight_meta_key = 'itstudio_weight';
$title_cn = $is_itstudio_news_page ? '社团新闻' : '公告通知';
$title_en = $is_itstudio_news_page ? 'Club News' : 'Announcements';
$empty_cn = $is_itstudio_news_page ? '暂无新闻' : '暂无公告';
$empty_en = $is_itstudio_news_page ? 'No news found.' : 'No announcements found.';
$side_title_cn = $is_itstudio_news_page ? '要闻' : '重要公告';
$side_title_en = $is_itstudio_news_page ? 'Top Stories' : 'Important Announcements';
if ($tag_slug !== '') {
$resolved_tag = get_term_by('slug', $tag_slug, 'post_tag');
if (!($resolved_tag instanceof WP_Term)) {
$tag_slug = '';
}
}
$available_tags = get_terms(array(
'taxonomy' => 'post_tag',
'hide_empty' => true,
'orderby' => 'name',
'order' => 'ASC',
));
if (is_wp_error($available_tags)) {
$available_tags = array();
}
$list_query_args = array(
'post_type' => $active_post_type,
'post_status' => 'publish',
'posts_per_page' => 10,
'paged' => $paged,
'ignore_sticky_posts' => true,
'orderby' => 'date',
'order' => 'DESC',
'suppress_filters' => false,
);
if ($keyword !== '') {
$list_query_args['itstudio_archive_extend_search'] = true;
$list_query_args['itstudio_archive_keyword'] = $keyword;
}
if ($tag_slug !== '') {
$list_query_args['tax_query'] = array(
array(
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => array($tag_slug),
),
);
}
$list_query = new WP_Query($list_query_args);
$featured_query = new WP_Query(array(
'post_type' => $active_post_type,
'post_status' => 'publish',
'posts_per_page' => 4,
'ignore_sticky_posts' => true,
'meta_key' => $weight_meta_key,
'orderby' => array(
'meta_value_num' => 'DESC',
'date' => 'DESC',
),
'order' => 'DESC',
'no_found_rows' => true,
));
$featured_ids = array_map('intval', wp_list_pluck($featured_query->posts, 'ID'));
wp_reset_postdata();
if (count($featured_ids) < 4) {
$fill_query = new WP_Query(array(
'post_type' => $active_post_type,
'post_status' => 'publish',
'posts_per_page' => 4 - count($featured_ids),
'ignore_sticky_posts' => true,
'post__not_in' => $featured_ids,
'orderby' => 'date',
'order' => 'DESC',
'no_found_rows' => true,
));
$featured_ids = array_merge($featured_ids, array_map('intval', wp_list_pluck($fill_query->posts, 'ID')));
wp_reset_postdata();
}
$featured_ids = array_slice(array_values(array_unique($featured_ids)), 0, 4);
?>
<main class="site-main news-archive-page">
<div class="container">
<header class="news-archive-head">
<h1 class="news-archive-title" data-cn="<?php echo esc_attr($title_cn); ?>" data-en="<?php echo esc_attr($title_en); ?>"><?php echo esc_html($title_en); ?></h1>
</header>
<div class="news-archive-tools">
<form class="news-archive-search" method="get" action="<?php echo esc_url($archive_url); ?>">
<div class="news-archive-search-controls">
<input
type="search"
name="q"
value="<?php echo esc_attr($keyword); ?>"
placeholder="<?php esc_attr_e('Search title or author...', 'itstudio'); ?>"
aria-label="<?php esc_attr_e('Search title or author', 'itstudio'); ?>"
data-cn-placeholder="搜索文章或发布者..."
data-en-placeholder="Search title or author..."
data-cn-aria-label="搜索文章或发布者"
data-en-aria-label="Search title or author"
>
<select
name="tag"
class="news-archive-search-tag"
aria-label="<?php esc_attr_e('Filter by tag', 'itstudio'); ?>"
data-cn-aria-label="按标签筛选"
data-en-aria-label="Filter by tag"
>
<option value="" data-cn="全部标签" data-en="All tags">全部标签</option>
<?php foreach ($available_tags as $tag_option) : ?>
<option value="<?php echo esc_attr($tag_option->slug); ?>" <?php selected($tag_slug, $tag_option->slug); ?>>
<?php echo esc_html($tag_option->name); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" data-cn="搜索" data-en="Search">搜索</button>
</form>
</div>
<div class="news-archive-layout">
<section class="news-main-column">
<?php if ($list_query->have_posts()) : ?>
<div class="news-stream">
<?php while ($list_query->have_posts()) : $list_query->the_post(); ?>
<?php
$post_id = get_the_ID();
$excerpt = function_exists('itstudio_get_post_excerpt_chars')
? itstudio_get_post_excerpt_chars($post_id, 200)
: wp_html_excerpt(wp_strip_all_tags(get_the_excerpt() ?: get_the_content()), 200, '...');
$views = function_exists('itstudio_get_post_views') ? (int) itstudio_get_post_views($post_id) : 0;
$char_count = function_exists('itstudio_get_post_char_count') ? (int) itstudio_get_post_char_count($post_id) : (int) strlen(wp_strip_all_tags(get_the_content()));
$tags = get_the_terms($post_id, 'post_tag');
?>
<article class="news-story">
<div class="news-story-body">
<h2 class="news-story-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<div class="news-story-meta">
<span class="news-story-meta-item">
<span data-cn="浏览量" data-en="Views">Views</span>
<strong><?php echo esc_html(number_format_i18n($views)); ?></strong>
</span>
<span class="news-story-meta-dot" aria-hidden="true">/</span>
<time class="news-story-meta-item" datetime="<?php echo esc_attr(get_the_date('c')); ?>">
<span data-cn="发表于" data-en="Published">Published</span>
<?php echo esc_html(get_the_date('Y-m-d H:i')); ?>
</time>
<span class="news-story-meta-dot" aria-hidden="true">/</span>
<span class="news-story-meta-item">
<span data-cn="发布者" data-en="Author">Author</span>
<?php echo esc_html(get_the_author()); ?>
</span>
<span class="news-story-meta-dot" aria-hidden="true">/</span>
<span class="news-story-meta-item">
<span data-cn="字数" data-en="Words">Words</span>
<strong><?php echo esc_html(number_format_i18n($char_count)); ?></strong>
</span>
</div>
<p class="news-story-excerpt"><?php echo esc_html($excerpt); ?></p>
<div class="news-story-tags">
<span class="news-story-tag-label" data-cn="标签" data-en="Tags">Tags</span>
<?php if (!empty($tags) && !is_wp_error($tags)) : ?>
<?php foreach (array_slice($tags, 0, 5) as $tag) : ?>
<?php
$tag_link = function_exists('itstudio_get_archive_tag_filter_url')
? itstudio_get_archive_tag_filter_url($tag, $active_post_type)
: '';
if ($tag_link === '') {
$tag_link = get_term_link($tag);
}
?>
<?php if (!is_wp_error($tag_link) && !empty($tag_link)) : ?>
<a class="news-story-tag" href="<?php echo esc_url($tag_link); ?>">#<?php echo esc_html($tag->name); ?></a>
<?php endif; ?>
<?php endforeach; ?>
<?php else : ?>
<span class="news-story-tag-empty" data-cn="无标签" data-en="No tags">No tags</span>
<?php endif; ?>
</div>
</div>
<a class="news-story-cover" href="<?php the_permalink(); ?>" aria-label="<?php echo esc_attr(get_the_title()); ?>">
<?php if (has_post_thumbnail()) : ?>
<?php the_post_thumbnail('medium_large', array('loading' => 'lazy')); ?>
<?php else : ?>
<img class="news-story-cover-fallback" src="<?php echo esc_url($default_cover_url); ?>" alt="<?php echo esc_attr(get_the_title()); ?>" loading="lazy">
<?php endif; ?>
</a>
</article>
<?php endwhile; ?>
</div>
<nav class="news-archive-pagination" aria-label="Pagination">
<?php
$pagination_base = esc_url_raw(add_query_arg('paged', '%#%', $archive_url));
echo wp_kses_post(
paginate_links(array(
'base' => $pagination_base,
'format' => '',
'current' => $paged,
'total' => max(1, (int) $list_query->max_num_pages),
'mid_size' => 2,
'prev_text' => '<span data-cn="上一页" data-en="Previous">Previous</span>',
'next_text' => '<span data-cn="下一页" data-en="Next">Next</span>',
'add_args' => array_filter(
array(
'q' => $keyword,
'tag' => $tag_slug,
),
static function ($value) {
return $value !== '';
}
),
))
);
?>
</nav>
<?php else : ?>
<p class="news-archive-empty" data-cn="<?php echo esc_attr($empty_cn); ?>" data-en="<?php echo esc_attr($empty_en); ?>"><?php echo esc_html($empty_en); ?></p>
<?php endif; ?>
<?php wp_reset_postdata(); ?>
</section>
<aside class="news-side-column">
<section class="news-side-block">
<h2 class="news-side-title" data-cn="<?php echo esc_attr($side_title_cn); ?>" data-en="<?php echo esc_attr($side_title_en); ?>"><?php echo esc_html($side_title_en); ?></h2>
<ul class="news-side-list">
<?php if (!empty($featured_ids)) : ?>
<?php foreach ($featured_ids as $featured_id) : ?>
<?php
$featured_title = get_the_title($featured_id);
$featured_link = get_permalink($featured_id);
$featured_views = function_exists('itstudio_get_post_views') ? (int) itstudio_get_post_views($featured_id) : 0;
$featured_cover = get_the_post_thumbnail_url($featured_id, 'thumbnail');
?>
<li class="news-side-item">
<a class="news-side-cover" href="<?php echo esc_url($featured_link); ?>" aria-label="<?php echo esc_attr($featured_title); ?>">
<?php if (!empty($featured_cover)) : ?>
<img src="<?php echo esc_url($featured_cover); ?>" alt="<?php echo esc_attr($featured_title); ?>" loading="lazy">
<?php else : ?>
<img class="news-side-cover-fallback" src="<?php echo esc_url($default_cover_url); ?>" alt="<?php echo esc_attr($featured_title); ?>" loading="lazy">
<?php endif; ?>
</a>
<div class="news-side-body">
<h3 class="news-side-item-title">
<a href="<?php echo esc_url($featured_link); ?>"><?php echo esc_html($featured_title); ?></a>
</h3>
<div class="news-side-meta">
<time datetime="<?php echo esc_attr(get_the_date('c', $featured_id)); ?>"><?php echo esc_html(get_the_date('Y-m-d H:i', $featured_id)); ?></time>
<span class="news-side-meta-dot" aria-hidden="true">/</span>
<span>
<span data-cn="浏览" data-en="Views">Views</span>
<?php echo esc_html(number_format_i18n($featured_views)); ?>
</span>
</div>
</div>
</li>
<?php endforeach; ?>
<?php else : ?>
<li class="news-side-empty" data-cn="暂无文章" data-en="No posts found.">No posts found.</li>
<?php endif; ?>
</ul>
</section>
</aside>
</div>
</div>
</main>
<?php else : ?>
<main class="site-main archive-page">
<div class="container">
<header class="archive-header">
<?php
the_archive_title('<h1 class="archive-title">', '</h1>');
the_archive_description('<div class="archive-description">', '</div>');
?>
</header>
<?php if (have_posts()) : ?>
<div class="posts-grid">
<?php
while (have_posts()) :
the_post();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class('archive-item'); ?>>
<?php if (has_post_thumbnail()) : ?>
<div class="archive-thumbnail">
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail('medium'); ?>
</a>
</div>
<?php endif; ?>
<div class="archive-content">
<h2 class="archive-item-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h2>
<div class="archive-meta">
<time datetime="<?php echo esc_attr(get_the_date('c')); ?>">
<?php echo esc_html(get_the_date()); ?>
</time>
<?php if (get_post_type() === 'post') : ?>
<span class="separator">|</span>
<span class="author"><?php the_author(); ?></span>
<?php endif; ?>
</div>
<?php if (has_excerpt()) : ?>
<div class="archive-excerpt">
<?php the_excerpt(); ?>
</div>
<?php endif; ?>
<a href="<?php the_permalink(); ?>" class="read-more">
<?php esc_html_e('Read More', 'itstudio'); ?> ->
</a>
</div>
</article>
<?php endwhile; ?>
</div>
<div class="pagination">
<?php
the_posts_pagination(array(
'mid_size' => 2,
'prev_text' => __('Previous', 'itstudio'),
'next_text' => __('Next', 'itstudio'),
));
?>
</div>
<?php else : ?>
<p><?php esc_html_e('No posts found.', 'itstudio'); ?></p>
<?php endif; ?>
</div>
</main>
<?php endif; ?>
<?php get_footer(); ?>
@@ -0,0 +1,589 @@
/* =========================================
工作室介绍页 - 旧首页视觉
========================================= */
.site-main {
position: relative;
}
body.intro-about {
-ms-overflow-style: none;
scrollbar-width: none;
}
body.intro-about::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
html:has(body.intro-about) {
-ms-overflow-style: none;
scrollbar-width: none;
}
html:has(body.intro-about)::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
body.intro-about .site-header,
body.intro-about .site-footer {
display: none !important;
}
body.intro-about .hero-section {
min-height: 100vh;
}
body.intro-about .about-theme-toggle {
position: fixed;
top: 18px;
right: 18px;
z-index: 40;
width: 72px;
height: 72px;
background: transparent !important;
border: none !important;
border-radius: 0;
color: color-mix(in srgb, var(--text-primary) 82%, var(--color-primary) 18%);
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
body.intro-about .about-theme-toggle::before {
display: none;
content: none;
}
body.intro-about .about-theme-toggle svg {
width: 60px;
height: 60px;
}
body.intro-about .about-theme-toggle:hover,
body.intro-about .about-theme-toggle:focus-visible {
background: transparent !important;
border: none !important;
color: color-mix(in srgb, var(--text-primary) 72%, var(--color-primary) 28%);
box-shadow: none;
}
body.intro-about .about-theme-toggle:hover {
transform: translateY(-1px);
}
body.intro-about .about-theme-toggle:hover::before,
body.intro-about .about-theme-toggle:focus-visible::before {
display: none;
}
.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;
flex-direction: column;
align-items: center;
justify-content: center;
gap: clamp(2px, 0.6vw, 10px);
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: none;
text-decoration: none;
pointer-events: none;
cursor: default;
}
.service-item:hover {
transform: none;
box-shadow: none;
}
.service-icon {
margin-bottom: 1.5rem;
--svc-stroke: #2f3d4f;
--svc-muted: #5f7288;
--svc-accent: var(--color-primary);
--svc-soft: rgba(9, 105, 218, 0.12);
--svc-soft-strong: rgba(9, 105, 218, 0.2);
color: var(--svc-stroke);
}
[data-theme="dark"] .service-icon {
--svc-stroke: #c4d1de;
--svc-muted: #93a4b7;
--svc-soft: rgba(88, 166, 255, 0.16);
--svc-soft-strong: rgba(88, 166, 255, 0.26);
}
.service-item.service-resources .service-icon {
--svc-accent: #2f7de1;
--svc-soft: rgba(47, 125, 225, 0.14);
--svc-soft-strong: rgba(47, 125, 225, 0.24);
}
.service-item.service-mirror .service-icon {
--svc-accent: #1f9d8d;
--svc-soft: rgba(31, 157, 141, 0.14);
--svc-soft-strong: rgba(31, 157, 141, 0.24);
}
.service-item.service-git .service-icon {
--svc-accent: #8b5cf6;
--svc-soft: rgba(139, 92, 246, 0.14);
--svc-soft-strong: rgba(139, 92, 246, 0.24);
}
.service-item.service-minecraft .service-icon {
--svc-accent: #2e9b45;
--svc-soft: rgba(46, 155, 69, 0.14);
--svc-soft-strong: rgba(46, 155, 69, 0.24);
}
.service-item.service-forum .service-icon {
--svc-accent: #d97706;
--svc-soft: rgba(217, 119, 6, 0.14);
--svc-soft-strong: rgba(217, 119, 6, 0.24);
}
.service-item.service-repair .service-icon {
--svc-accent: #d94f45;
--svc-soft: rgba(217, 79, 69, 0.14);
--svc-soft-strong: rgba(217, 79, 69, 0.24);
}
.service-item.service-workshop .service-icon {
--svc-accent: #c58a00;
--svc-soft: rgba(197, 138, 0, 0.14);
--svc-soft-strong: rgba(197, 138, 0, 0.24);
}
.service-item.service-campus .service-icon {
--svc-accent: #0f8fb8;
--svc-soft: rgba(15, 143, 184, 0.14);
--svc-soft-strong: rgba(15, 143, 184, 0.24);
}
[data-theme="dark"] .service-item.service-resources .service-icon {
--svc-accent: #7db5ff;
--svc-soft: rgba(125, 181, 255, 0.18);
--svc-soft-strong: rgba(125, 181, 255, 0.28);
}
[data-theme="dark"] .service-item.service-mirror .service-icon {
--svc-accent: #5edac7;
--svc-soft: rgba(94, 218, 199, 0.18);
--svc-soft-strong: rgba(94, 218, 199, 0.28);
}
[data-theme="dark"] .service-item.service-git .service-icon {
--svc-accent: #b9a0ff;
--svc-soft: rgba(185, 160, 255, 0.18);
--svc-soft-strong: rgba(185, 160, 255, 0.28);
}
[data-theme="dark"] .service-item.service-minecraft .service-icon {
--svc-accent: #6fda83;
--svc-soft: rgba(111, 218, 131, 0.18);
--svc-soft-strong: rgba(111, 218, 131, 0.28);
}
[data-theme="dark"] .service-item.service-forum .service-icon {
--svc-accent: #f5b04d;
--svc-soft: rgba(245, 176, 77, 0.18);
--svc-soft-strong: rgba(245, 176, 77, 0.28);
}
[data-theme="dark"] .service-item.service-repair .service-icon {
--svc-accent: #ff8a80;
--svc-soft: rgba(255, 138, 128, 0.18);
--svc-soft-strong: rgba(255, 138, 128, 0.28);
}
[data-theme="dark"] .service-item.service-workshop .service-icon {
--svc-accent: #ffd166;
--svc-soft: rgba(255, 209, 102, 0.18);
--svc-soft-strong: rgba(255, 209, 102, 0.28);
}
[data-theme="dark"] .service-item.service-campus .service-icon {
--svc-accent: #68d5ff;
--svc-soft: rgba(104, 213, 255, 0.18);
--svc-soft-strong: rgba(104, 213, 255, 0.28);
}
.service-icon .service-symbol {
width: 90px;
height: 90px;
display: block;
}
.service-icon .svc-stroke,
.service-icon .svc-muted,
.service-icon .svc-accent {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.service-icon .svc-stroke {
stroke: var(--svc-stroke);
stroke-width: 2.2;
}
.service-icon .svc-muted {
stroke: var(--svc-muted);
stroke-width: 2.05;
}
.service-icon .svc-accent {
stroke: var(--svc-accent);
stroke-width: 2.3;
}
.service-icon .svc-soft {
fill: var(--svc-soft);
}
.service-icon .svc-soft-strong {
fill: var(--svc-soft-strong);
}
.service-icon .svc-dot {
fill: var(--svc-accent);
}
.service-icon .svc-dot-muted {
fill: var(--svc-muted);
}
.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) {
body.intro-about .about-theme-toggle {
top: 12px;
right: 12px;
}
.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 .service-symbol {
width: 64px;
height: 64px;
}
.service-item span {
font-size: 1rem;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
/* =========================================
页脚布局 (Footer)
========================================= */
.site-footer {
background-color: var(--bg-surface);
border-top: 1px solid var(--border-default);
padding: 1rem 0 0.75rem;
margin-top: auto; /* 确保置底 */
flex-shrink: 0; /* 防止Footer被压缩 */
font-size: 15px; /* 增加页脚基础字号 */
}
/* --- 页脚内容网格 --- */
.footer-content {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 4列布局 */
gap: 1.5rem;
margin-bottom: 1.75rem;
}
.footer-section h3 {
font-size: 17px; /* 增大标题字号 (原1rem/16px) */
font-weight: 600;
margin-bottom: 0.6rem;
color: var(--text-primary);
}
.footer-links li {
margin-bottom: 0.5rem; /* 增加行间距 */
}
.footer-links a {
color: var(--text-secondary);
font-size: 14px; /* 增大链接字号 (原0.9rem/14.4px) */
}
.footer-links a:hover {
color: var(--color-primary);
text-decoration: underline;
}
/* --- 底部版权区 --- */
.footer-bottom {
border-top: 1px solid var(--border-muted);
padding-top: 1rem;
text-align: center;
color: var(--text-secondary);
font-size: 13px; /* 增大版权信息字号 (原0.85rem/13.6px) */
}
/* --- 响应式适配 --- */
@media (max-width: 768px) {
.footer-content {
grid-template-columns: repeat(2, 1fr); /* 平板 2列 */
}
}
@media (max-width: 480px) {
.footer-content {
grid-template-columns: 1fr; /* 手机 1列 */
}
.site-footer {
padding: 1.5rem 0 1rem;
}
}
.footer-address-text {
color: var(--text-secondary);
line-height: 1.6;
overflow-wrap: anywhere;
margin: 0;
}
.footer-icp {
margin-top: 0.45rem;
}
.footer-icp a {
color: var(--text-secondary);
}
.footer-icp a:hover {
color: var(--color-primary);
text-decoration: underline;
}
@@ -0,0 +1,549 @@
/* =========================================
Front Page - Minimal Landing Layout
========================================= */
.home-landing {
background-color: var(--bg-body);
}
.landing-hero {
position: relative;
min-height: clamp(420px, 68vh, 700px);
display: flex;
align-items: center;
text-align: center;
color: var(--hero-fg);
isolation: isolate;
overflow: hidden;
--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);
}
[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);
}
.landing-hero-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
z-index: 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: 24px 0 14px;
}
.services-provided h2,
.landing-feed-head h2 {
margin: 0 0 12px;
font-size: clamp(1.35rem, 0.5vw + 1.2rem, 1.75rem);
color: var(--color-primary);
display: flex;
align-items: center;
}
.services-provided h2::before,
.landing-feed-head h2::before {
content: none;
}
.services-grid-box {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
}
.service-item {
min-height: 170px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 20px 14px;
border: none;
border-radius: 0;
background-color: transparent;
color: var(--text-primary);
transition: none;
}
.service-icon {
margin-bottom: 14px;
--svc-stroke: #2f3d4f;
--svc-muted: #5f7288;
--svc-accent: var(--color-primary);
--svc-soft: rgba(9, 105, 218, 0.12);
--svc-soft-strong: rgba(9, 105, 218, 0.2);
color: var(--svc-stroke);
}
[data-theme="dark"] .service-icon {
--svc-stroke: #c4d1de;
--svc-muted: #93a4b7;
--svc-soft: rgba(88, 166, 255, 0.16);
--svc-soft-strong: rgba(88, 166, 255, 0.26);
}
.service-item.service-resources .service-icon {
--svc-accent: #2f7de1;
--svc-soft: rgba(47, 125, 225, 0.14);
--svc-soft-strong: rgba(47, 125, 225, 0.24);
}
.service-item.service-mirror .service-icon {
--svc-accent: #1f9d8d;
--svc-soft: rgba(31, 157, 141, 0.14);
--svc-soft-strong: rgba(31, 157, 141, 0.24);
}
.service-item.service-git .service-icon {
--svc-accent: #8b5cf6;
--svc-soft: rgba(139, 92, 246, 0.14);
--svc-soft-strong: rgba(139, 92, 246, 0.24);
}
.service-item.service-minecraft .service-icon {
--svc-accent: #2e9b45;
--svc-soft: rgba(46, 155, 69, 0.14);
--svc-soft-strong: rgba(46, 155, 69, 0.24);
}
.service-item.service-forum .service-icon {
--svc-accent: #d97706;
--svc-soft: rgba(217, 119, 6, 0.14);
--svc-soft-strong: rgba(217, 119, 6, 0.24);
}
.service-item.service-repair .service-icon {
--svc-accent: #d94f45;
--svc-soft: rgba(217, 79, 69, 0.14);
--svc-soft-strong: rgba(217, 79, 69, 0.24);
}
.service-item.service-workshop .service-icon {
--svc-accent: #c58a00;
--svc-soft: rgba(197, 138, 0, 0.14);
--svc-soft-strong: rgba(197, 138, 0, 0.24);
}
.service-item.service-campus .service-icon {
--svc-accent: #0f8fb8;
--svc-soft: rgba(15, 143, 184, 0.14);
--svc-soft-strong: rgba(15, 143, 184, 0.24);
}
[data-theme="dark"] .service-item.service-resources .service-icon {
--svc-accent: #7db5ff;
--svc-soft: rgba(125, 181, 255, 0.18);
--svc-soft-strong: rgba(125, 181, 255, 0.28);
}
[data-theme="dark"] .service-item.service-mirror .service-icon {
--svc-accent: #5edac7;
--svc-soft: rgba(94, 218, 199, 0.18);
--svc-soft-strong: rgba(94, 218, 199, 0.28);
}
[data-theme="dark"] .service-item.service-git .service-icon {
--svc-accent: #b9a0ff;
--svc-soft: rgba(185, 160, 255, 0.18);
--svc-soft-strong: rgba(185, 160, 255, 0.28);
}
[data-theme="dark"] .service-item.service-minecraft .service-icon {
--svc-accent: #6fda83;
--svc-soft: rgba(111, 218, 131, 0.18);
--svc-soft-strong: rgba(111, 218, 131, 0.28);
}
[data-theme="dark"] .service-item.service-forum .service-icon {
--svc-accent: #f5b04d;
--svc-soft: rgba(245, 176, 77, 0.18);
--svc-soft-strong: rgba(245, 176, 77, 0.28);
}
[data-theme="dark"] .service-item.service-repair .service-icon {
--svc-accent: #ff8a80;
--svc-soft: rgba(255, 138, 128, 0.18);
--svc-soft-strong: rgba(255, 138, 128, 0.28);
}
[data-theme="dark"] .service-item.service-workshop .service-icon {
--svc-accent: #ffd166;
--svc-soft: rgba(255, 209, 102, 0.18);
--svc-soft-strong: rgba(255, 209, 102, 0.28);
}
[data-theme="dark"] .service-item.service-campus .service-icon {
--svc-accent: #68d5ff;
--svc-soft: rgba(104, 213, 255, 0.18);
--svc-soft-strong: rgba(104, 213, 255, 0.28);
}
.service-icon .service-symbol {
width: 78px;
height: 78px;
display: block;
}
.service-icon .svc-stroke,
.service-icon .svc-muted,
.service-icon .svc-accent {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.service-icon .svc-stroke {
stroke: var(--svc-stroke);
stroke-width: 2.2;
}
.service-icon .svc-muted {
stroke: var(--svc-muted);
stroke-width: 2.05;
}
.service-icon .svc-accent {
stroke: var(--svc-accent);
stroke-width: 2.3;
}
.service-icon .svc-soft {
fill: var(--svc-soft);
}
.service-icon .svc-soft-strong {
fill: var(--svc-soft-strong);
}
.service-icon .svc-dot {
fill: var(--svc-accent);
}
.service-icon .svc-dot-muted {
fill: var(--svc-muted);
}
.service-item span {
display: block;
font-size: 1.08rem;
font-weight: 600;
color: var(--text-primary);
}
.landing-updates {
padding: 16px 0 58px;
}
.landing-updates-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.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));
}
}
@media (max-width: 820px) {
.landing-updates-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 680px) {
.landing-hero {
min-height: clamp(210px, 36vh, 260px);
--hero-overlay-highlight: rgba(255, 255, 255, 0.1);
--hero-overlay-top: rgba(248, 251, 255, 0.06);
--hero-overlay-bottom: rgba(206, 220, 243, 0.06);
}
.landing-hero-content {
max-width: 95%;
padding: 12px 0;
}
.landing-hero-title {
margin-bottom: 6px;
font-size: clamp(1.55rem, 7.2vw, 2.2rem);
}
.landing-hero-subtitle {
font-size: clamp(0.84rem, 3.9vw, 1.06rem);
line-height: 1.35;
}
.landing-hero-btn {
margin-top: 10px;
min-height: 34px;
padding: 0 14px;
font-size: 0.86rem;
}
.service-item {
min-height: 132px;
padding: 12px 8px;
}
.service-icon {
margin-bottom: 8px;
}
.service-icon .service-symbol {
width: 58px;
height: 58px;
}
.service-item span {
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;
}
}
@media (max-width: 420px) {
.landing-hero {
min-height: clamp(190px, 33vh, 235px);
}
.landing-hero-content {
padding: 8px 0;
}
.landing-hero-title {
font-size: clamp(1.38rem, 7.5vw, 1.9rem);
}
.landing-hero-subtitle {
font-size: clamp(0.78rem, 3.6vw, 0.95rem);
}
.landing-hero-btn {
min-height: 32px;
margin-top: 8px;
padding: 0 12px;
font-size: 0.82rem;
}
}
@@ -0,0 +1,265 @@
/* =========================================
页眉布局 (Header & Navigation)
========================================= */
.site-header {
height: 80px;
background-color: var(--bg-body);
border-bottom: 1px solid var(--border-muted);
position: sticky; /* 粘性定位,确保始终置顶 */
top: 0;
z-index: 1000;
backdrop-filter: blur(12px); /* 毛玻璃效果 */
-webkit-backdrop-filter: blur(12px);
/* 使用半透明背景以增强毛玻璃效果 */
background-color: rgba(255, 255, 255, 0.85);
transition: background-color var(--duration-normal), border-color var(--duration-normal), transform 0.45s var(--ease-in-out);
will-change: transform;
flex-shrink: 0; /* 防止被父容器 Flex 布局压缩 */
}
.site-header .container {
height: 100%; /* 关键修复:让容器撑满 Header 高度,使内部 flex 垂直居中生效 */
}
[data-theme="dark"] .site-header {
background-color: rgba(13, 17, 23, 0.85);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
/* --- Logo 区域 --- */
.logo-container,
.logo-link {
display: flex;
align-items: center;
height: 100%;
}
.logo-main {
height: 60px; /* 增大 Logo 尺寸 (原52px) */
width: auto;
color: var(--text-primary);
}
/* --- 主导航 (Navigation) --- */
.main-navigation {
display: flex;
align-items: center;
gap: 24px;
}
.nav-menu {
display: flex;
list-style: none;
gap: 8px; /* 菜单项间距 */
margin: 0;
padding: 0;
}
.nav-menu a {
color: var(--text-secondary);
font-weight: 500;
font-size: 16px; /* 增大导航字号 (原15px) */
padding: 8px 16px;
border-radius: var(--radius-sm);
transition: all var(--duration-fast);
display: flex;
align-items: center;
}
.nav-menu a:hover,
.nav-menu a:focus {
color: var(--text-primary);
background-color: var(--bg-surface);
}
/* --- 社交链接与工具 (Social & Tools) --- */
.social-links {
display: flex;
align-items: center;
gap: 12px;
padding-left: 12px;
margin-left: 12px;
border-left: 1px solid var(--border-muted);
}
.social-links a,
.theme-toggle,
.lang-toggle {
width: 40px; /* 增大图标按钮尺寸 (原36px) */
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
background: transparent;
border: 1px solid transparent; /* 预留边框位置防止抖动 */
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--duration-fast);
}
.social-links a:hover,
.theme-toggle:hover,
.lang-toggle:hover {
background-color: var(--bg-surface);
color: var(--color-primary);
border-color: var(--border-default);
}
/* --- 主题切换动画 (Theme Toggle) --- */
.theme-toggle {
position: relative; /* 为内部 SVG 绝对定位做准备 */
overflow: hidden;
}
.theme-toggle svg {
position: absolute;
transition: transform 0.5s var(--ease-in-out), opacity 0.5s;
}
/* 默认 / 浅色模式状态: 显示月亮 (提示切换到深色) */
/* 修正逻辑:通常浅色模式下,按钮为了表示“点击切换到深色”,会显示月亮;
但也有一种逻辑是显示“当前是太阳”。
根据之前的代码意图,我们从 CSS 变量来看:
浅色模式下,.moon-icon 显示,.sun-icon 隐藏 */
/* 浅色模式下:太阳隐藏 */
html:not([data-theme="dark"]) .sun-icon {
opacity: 0;
transform: rotate(90deg) scale(0);
}
/* 浅色模式下:月亮显示 */
html:not([data-theme="dark"]) .moon-icon {
opacity: 1;
transform: rotate(0) scale(1);
}
/* 深色模式下:太阳显示 */
[data-theme="dark"] .sun-icon {
opacity: 1;
transform: rotate(0) scale(1);
}
/* 深色模式下:月亮隐藏 */
[data-theme="dark"] .moon-icon {
opacity: 0;
transform: rotate(-90deg) scale(0);
}
/* 语言切换按钮文字 */
.lang-toggle {
font-weight: 600;
font-size: 15px; /* 增大切换字号 (原14px) */
width: auto;
padding: 0 12px;
}
/* --- 移动端菜单开关 (Hamburger) --- */
.mobile-menu-toggle {
display: none; /* 默认隐藏 */
flex-direction: column;
justify-content: space-between;
width: 24px;
height: 18px;
background: transparent;
border: none;
cursor: pointer;
z-index: 1100;
padding: 0;
}
.mobile-menu-toggle span {
display: block;
width: 100%;
height: 2px;
background-color: var(--text-secondary);
border-radius: 2px;
transition: all 0.3s var(--ease-in-out);
transform-origin: left center;
}
/* 激活状态 (X 形状) */
.mobile-menu-toggle.active span:nth-child(1) {
transform: rotate(45deg) translate(0px, -2px);
width: 115%;
}
.mobile-menu-toggle.active span:nth-child(2) {
opacity: 0;
transform: translateX(-10px);
}
.mobile-menu-toggle.active span:nth-child(3) {
transform: rotate(-45deg) translate(0px, 2px);
width: 115%;
}
/* 默认情况:显示主 Logo,隐藏移动端 Logo */
.logo-mobile {
display: none;
height: 48px; /* 调整高度 */
width: auto;
}
/* --- 响应式适配 --- */
@media (max-width: 900px) {
/* 移动端隐藏主Logo,显示移动端Logo */
.logo-main {
display: none;
}
.logo-mobile {
display: block;
}
.mobile-menu-toggle {
display: flex;
order: 2; /* 放在最右侧 */
}
.nav-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--bg-body);
border-bottom: 1px solid var(--border-default);
flex-direction: column;
padding: 1rem;
gap: 0;
/* 默认收起状态 */
opacity: 0;
transform: translateY(-10px);
pointer-events: none;
transition: all var(--duration-normal) var(--ease-in-out);
box-shadow: var(--shadow-md);
z-index: 1000;
}
.nav-menu.active {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.nav-menu a {
width: 100%;
padding: 12px 16px;
justify-content: flex-start;
}
/* 移动端隐藏社交分割线 */
.social-links {
border-left: none;
margin-left: auto; /* 推到右侧 */
order: 1; /* 放在 toggle 左侧 */
margin-right: 12px; /* 和 toggle 之间增加一点间距 */
}
}
@@ -0,0 +1,408 @@
/* =========================================
Introduction Page (About)
========================================= */
.intro-page {
position: relative;
overflow: hidden;
background-color: var(--bg-body);
background-image:
linear-gradient(160deg, var(--intro-glow-primary), transparent 55%),
linear-gradient(330deg, var(--intro-glow-accent), transparent 50%);
--intro-glow-primary: rgba(9, 105, 218, 0.14);
--intro-glow-accent: rgba(142, 136, 199, 0.14);
}
[data-theme="dark"] .intro-page {
--intro-glow-primary: rgba(88, 166, 255, 0.22);
--intro-glow-accent: rgba(142, 136, 199, 0.2);
}
.intro-page::before {
display: none;
}
.intro-about .site-header {
background-color: rgba(14, 22, 36, 0.78);
border-bottom-color: rgba(255, 255, 255, 0.06);
}
html:not([data-theme="dark"]) .intro-about .site-header {
background-color: rgba(246, 249, 252, 0.9);
border-bottom-color: rgba(36, 41, 47, 0.08);
}
.intro-page > * {
position: relative;
z-index: 1;
}
.intro-step {
padding: 120px 0;
scroll-margin-top: 110px;
}
.intro-hero {
padding-top: 140px;
padding-bottom: 120px;
}
.intro-hero-grid,
.intro-grid,
.intro-join-grid {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 64px;
align-items: center;
}
.intro-grid.reverse .intro-media,
.intro-grid.reverse .intro-join-media {
order: -1;
}
.intro-copy {
display: flex;
flex-direction: column;
gap: 16px;
}
.intro-hero .intro-copy {
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;
letter-spacing: -0.02em;
line-height: 1.15;
margin-bottom: 0;
background: linear-gradient(120deg, var(--text-primary), var(--color-primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.intro-heading {
font-size: clamp(2rem, 3vw, 2.8rem);
font-weight: 700;
letter-spacing: -0.01em;
margin-bottom: 0.5rem;
}
.intro-lead {
font-size: 1.2rem;
color: var(--text-secondary);
}
.intro-sub,
.intro-text {
color: var(--text-secondary);
font-size: 1.05rem;
}
.intro-cta-group {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-top: 4px;
}
.intro-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 20px;
border-radius: var(--radius-sm);
border: 1px solid var(--color-primary);
font-weight: 600;
font-size: 1rem;
color: var(--text-primary);
background-color: transparent;
transition: all var(--duration-fast);
}
.intro-btn--primary {
background: linear-gradient(120deg, var(--color-primary), var(--color-primary-light));
color: #fff;
border-color: transparent;
box-shadow: var(--shadow-sm);
}
.intro-btn--primary:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.intro-btn--ghost:hover {
color: var(--color-primary);
border-color: var(--color-primary);
}
.intro-stats {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin-top: 8px;
}
.intro-stat {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px 16px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-default);
background-color: var(--bg-card);
min-width: 120px;
}
.stat-number {
font-size: 1.4rem;
font-weight: 700;
color: var(--text-primary);
}
.stat-label {
font-size: 0.9rem;
color: var(--text-secondary);
}
.intro-media,
.intro-hero-media,
.intro-join-media {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.intro-hero-media {
align-items: stretch;
}
.media-card {
min-height: 200px;
border-radius: var(--radius-md);
border: 1px dashed var(--border-default);
background-color: var(--bg-card);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-family: var(--font-mono);
font-size: 0.9rem;
color: var(--text-secondary);
padding: 16px;
}
.media-card--tall {
grid-row: span 2;
min-height: 420px;
}
.media-card--wide {
grid-column: span 2;
min-height: 240px;
}
.intro-points {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
}
.intro-points li,
.intro-list li {
position: relative;
padding-left: 18px;
color: var(--text-secondary);
}
.intro-points li::before,
.intro-list li::before {
content: "";
position: absolute;
left: 0;
top: 0.55em;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--color-primary);
}
.intro-org-grid,
.intro-feature-grid,
.intro-adv-grid {
display: grid;
gap: 16px;
margin-top: 16px;
}
.intro-org-grid,
.intro-feature-grid {
grid-template-columns: repeat(3, 1fr);
}
.intro-adv-grid {
grid-template-columns: repeat(2, 1fr);
}
.intro-card {
border-radius: var(--radius-md);
border: 1px solid var(--border-default);
background-color: var(--bg-card);
padding: 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.intro-card h3 {
font-size: 1.1rem;
font-weight: 600;
}
.intro-card p {
color: var(--text-secondary);
font-size: 0.95rem;
}
.intro-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 4px;
}
.intro-join {
padding-bottom: 140px;
}
.intro-join-grid {
align-items: start;
}
.intro-join-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.intro-join-media {
grid-template-columns: repeat(3, 1fr);
}
/* Stream text characters */
.stream-char {
transition: opacity 0.4s ease;
}
/* Animations */
.intro-animate {
transition: opacity 0.8s var(--ease-in-out), transform 0.8s var(--ease-in-out);
transition-delay: var(--delay, 0s);
}
body.has-intro-animations .intro-animate {
opacity: 0;
transform: translateY(30px);
}
body.has-intro-animations .intro-step.is-active .intro-animate {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.intro-animate {
transition: none !important;
opacity: 1 !important;
transform: none !important;
}
}
/* Responsive */
@media (max-width: 1100px) {
.intro-hero-grid,
.intro-grid,
.intro-join-grid {
grid-template-columns: 1fr;
}
.intro-grid.reverse .intro-media,
.intro-grid.reverse .intro-join-media {
order: 0;
}
.intro-org-grid,
.intro-feature-grid {
grid-template-columns: repeat(2, 1fr);
}
.intro-adv-grid {
grid-template-columns: 1fr;
}
.intro-join-media {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.intro-step {
padding: 80px 0;
}
.intro-hero {
padding-top: 110px;
}
.intro-brand-svg {
width: min(86vw, 320px);
}
.intro-title {
font-size: 2.2rem;
}
.intro-cta-group {
flex-direction: column;
align-items: flex-start;
}
.intro-org-grid,
.intro-feature-grid,
.intro-join-media {
grid-template-columns: 1fr;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,369 @@
.services-directory-page {
padding: 36px 0 68px;
}
.services-directory-head {
margin-bottom: 40px;
}
.services-directory-title {
margin: 0;
font-size: clamp(2rem, 1.6vw + 1.5rem, 3rem);
line-height: 1.15;
color: var(--color-primary);
}
.services-directory-layout {
display: grid;
grid-template-columns: 230px minmax(0, 1fr);
gap: 28px;
align-items: start;
}
.services-directory-sidebar {
position: sticky;
top: calc(var(--header-height) + 18px);
}
.services-directory-nav-box {
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--bg-surface) 92%, transparent);
padding: 14px;
}
.services-directory-nav-title {
margin: 0;
font-size: 1rem;
color: var(--text-primary);
}
.services-directory-nav-list {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 5px;
}
.services-directory-nav-list a {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border-radius: 8px;
color: var(--text-secondary);
text-decoration: none;
border: 1px solid transparent;
}
.services-directory-nav-list a:hover {
color: var(--color-primary);
border-color: var(--border-default);
background: color-mix(in srgb, var(--bg-card) 88%, transparent);
}
.services-directory-nav-name {
font-size: 0.94rem;
}
.services-directory-nav-count {
min-width: 26px;
padding: 1px 7px;
border-radius: 999px;
text-align: center;
font-size: 0.74rem;
font-weight: 700;
color: var(--text-secondary);
background: color-mix(in srgb, var(--bg-card) 72%, var(--bg-surface) 28%);
border: 1px solid var(--border-default);
}
.services-directory-content {
min-width: 0;
}
.services-directory-group {
scroll-margin-top: calc(var(--header-height) + 18px);
}
.services-directory-group + .services-directory-group {
margin-top: 28px;
}
.services-directory-group-head {
margin-bottom: 12px;
}
.services-directory-group-head h2 {
margin: 0;
font-size: 1.52rem;
color: var(--text-primary);
}
.services-directory-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
align-items: start;
}
.services-directory-card {
display: block;
position: relative;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--border-default) 86%, transparent);
border-radius: 6px;
background:
linear-gradient(
132deg,
color-mix(in srgb, var(--color-primary) 10%, transparent) 0%,
transparent 38%
),
linear-gradient(
180deg,
color-mix(in srgb, var(--bg-card) 92%, #0f1726 8%) 0%,
color-mix(in srgb, var(--bg-card) 98%, transparent) 100%
);
padding: 12px 12px 11px;
min-height: 0;
color: inherit;
text-decoration: none;
box-shadow:
0 14px 26px -22px color-mix(in srgb, #000000 68%, transparent),
inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
transition: transform var(--duration-fast) var(--ease-in-out), border-color var(--duration-fast) var(--ease-in-out), box-shadow var(--duration-fast) var(--ease-in-out), background var(--duration-fast) var(--ease-in-out);
}
.services-directory-card::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(90deg, color-mix(in srgb, var(--color-primary) 64%, transparent), transparent)
top left / 58px 1px no-repeat,
linear-gradient(180deg, color-mix(in srgb, var(--color-primary) 64%, transparent), transparent)
top left / 1px 24px no-repeat;
opacity: 0.5;
}
.services-directory-card:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--color-primary) 58%, var(--border-default) 42%);
box-shadow:
0 20px 32px -24px color-mix(in srgb, #000000 72%, transparent),
inset 0 1px 0 color-mix(in srgb, #ffffff 10%, transparent);
background:
linear-gradient(
132deg,
color-mix(in srgb, var(--color-primary) 14%, transparent) 0%,
transparent 40%
),
linear-gradient(
180deg,
color-mix(in srgb, var(--bg-card) 90%, #0f1726 10%) 0%,
color-mix(in srgb, var(--bg-card) 97%, transparent) 100%
);
}
.services-directory-card:hover .services-directory-name {
color: color-mix(in srgb, var(--text-primary) 82%, var(--color-primary) 18%);
}
.services-directory-card-head {
display: flex;
align-items: center;
gap: 12px;
}
.services-directory-icon-wrap {
width: 54px;
height: 54px;
border-radius: 6px;
border: none;
background: transparent;
overflow: hidden;
display: block;
flex-shrink: 0;
}
.services-directory-icon {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.services-directory-icon-fallback {
opacity: 1;
}
.services-directory-meta {
min-width: 0;
}
.services-directory-name {
margin: 0;
font-size: 1.16rem;
line-height: 1.28;
color: var(--text-primary);
font-weight: 760;
}
.services-directory-category {
margin-top: 4px;
display: inline-block;
font-size: 0.8rem;
color: var(--text-secondary);
}
.services-directory-access {
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
margin: 0;
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 8px 2px 7px;
border-radius: 4px;
font-size: 0.68rem;
font-weight: 700;
line-height: 1.35;
letter-spacing: 0.02em;
border: 1px solid color-mix(in srgb, var(--color-primary) 62%, var(--border-default) 38%);
background: color-mix(in srgb, var(--color-primary) 20%, var(--bg-card) 80%);
color: var(--color-primary);
}
.services-directory-access::before {
content: "";
width: 5px;
height: 5px;
border-radius: 1px;
background: currentColor;
opacity: 0.9;
}
.services-directory-card.is-campus-only .services-directory-card-head {
padding-right: 98px;
}
.services-directory-desc {
margin: 10px 0 0;
padding-top: 10px;
border-top: 1px solid color-mix(in srgb, var(--border-default) 84%, transparent);
color: var(--text-secondary);
font-size: 0.94rem;
line-height: 1.55;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.services-directory-empty {
margin-top: 16px;
border: 1px solid var(--border-default);
border-radius: 12px;
padding: 28px;
text-align: center;
background: color-mix(in srgb, var(--bg-card) 95%, transparent);
}
.services-directory-empty h2 {
margin: 0;
font-size: 1.6rem;
color: var(--text-primary);
}
.services-directory-empty p {
margin: 10px 0 0;
color: var(--text-secondary);
}
.services-directory-empty-btn {
margin-top: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 148px;
height: 38px;
padding: 0 16px;
border-radius: 10px;
border: 1px solid color-mix(in srgb, var(--color-primary) 65%, var(--border-default) 35%);
color: var(--color-primary);
text-decoration: none;
font-weight: 700;
}
.services-directory-empty-btn:hover {
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
}
@media (max-width: 1280px) {
.services-directory-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 980px) {
.services-directory-head {
margin-bottom: 30px;
}
.services-directory-layout {
grid-template-columns: 1fr;
gap: 16px;
}
.services-directory-sidebar {
position: static;
}
.services-directory-nav-list {
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.services-directory-nav-list li {
flex: 0 0 auto;
}
.services-directory-nav-list a {
padding: 7px 11px;
}
}
@media (max-width: 680px) {
.services-directory-page {
padding: 26px 0 52px;
}
.services-directory-head {
margin-bottom: 24px;
}
.services-directory-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.services-directory-card {
min-height: 0;
}
.services-directory-access {
top: 8px;
right: 8px;
font-size: 0.64rem;
padding: 2px 6px;
}
.services-directory-card.is-campus-only .services-directory-card-head {
padding-right: 88px;
}
}
@@ -0,0 +1,140 @@
(() => {
const main = document.querySelector('.site-main');
const heroSection = document.querySelector('.hero-section');
const servicesSection = document.querySelector('.services-section');
const canvas = document.querySelector('.hero-waves');
if (!main || !canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)');
let width = 0;
let height = 0;
let dpr = 1;
let rafId = null;
const waveDefs = [
{ yRatio: 0.3, amplitude: 24, wavelength: 940, speed: 0.5, alpha: 0.98 },
{ yRatio: 0.45, amplitude: 14, wavelength: 1020, speed: 0.58, alpha: 0.92 },
{ yRatio: 0.65, amplitude: 10, wavelength: 1160, speed: 0.6, alpha: 0.9 },
];
const palettes = {
light: {
gradient: ['#d6e8fa', '#a7c8e8', '#6f9fc8', '#4f84b6', '#376e9f'],
waves: ['rgba(124, 167, 205, 0.96)', 'rgba(90, 143, 190, 0.97)', 'rgba(62, 118, 169, 0.98)'],
},
dark: {
gradient: ['#0d1117', '#111d2b', '#14334d', '#17507a', '#1b6aa6'],
waves: ['rgba(90, 165, 225, 0.9)', 'rgba(65, 140, 210, 0.88)', 'rgba(50, 120, 190, 0.86)'],
},
};
const getTheme = () => (document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light');
const getCoverageHeight = () => {
if (servicesSection) {
const coverage = servicesSection.offsetTop + servicesSection.offsetHeight * 0.5;
return Math.min(coverage, main.scrollHeight);
}
if (heroSection) {
return heroSection.offsetHeight;
}
return main.scrollHeight;
};
const resize = () => {
dpr = window.devicePixelRatio || 1;
width = main.clientWidth;
height = Math.max(1, Math.round(getCoverageHeight()));
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 drawBackground = (palette) => {
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, palette.gradient[0]);
gradient.addColorStop(0.35, palette.gradient[1]);
gradient.addColorStop(0.55, palette.gradient[2]);
gradient.addColorStop(0.75, palette.gradient[3]);
gradient.addColorStop(1, palette.gradient[4]);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
};
const drawWave = (palette, wave, time, index) => {
const yBase = height * wave.yRatio;
const phase = time * wave.speed;
const prevAlpha = ctx.globalAlpha;
ctx.globalAlpha = wave.alpha ?? 1;
ctx.beginPath();
ctx.moveTo(0, height);
for (let x = 0; x <= width; x += 18) {
const theta = (x / wave.wavelength) * Math.PI * 2 + phase;
const y = yBase + Math.sin(theta) * wave.amplitude;
ctx.lineTo(x, y);
}
ctx.lineTo(width, height);
ctx.closePath();
ctx.fillStyle = palette.waves[index % palette.waves.length];
ctx.fill();
ctx.globalAlpha = prevAlpha;
};
const render = (timestamp) => {
const palette = palettes[getTheme()];
const time = timestamp * 0.001;
ctx.clearRect(0, 0, width, height);
drawBackground(palette);
waveDefs.forEach((wave, index) => drawWave(palette, wave, time, index));
rafId = requestAnimationFrame(render);
};
const stop = () => {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
};
const drawStatic = () => {
const palette = palettes[getTheme()];
ctx.clearRect(0, 0, width, height);
drawBackground(palette);
waveDefs.forEach((wave, index) => drawWave(palette, wave, 0, index));
};
const start = () => {
stop();
resize();
if (prefersReduced.matches) {
drawStatic();
return;
}
rafId = requestAnimationFrame(render);
};
const onResize = () => {
resize();
if (prefersReduced.matches) {
drawStatic();
}
};
const themeObserver = new MutationObserver(() => {
if (prefersReduced.matches) {
drawStatic();
}
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
prefersReduced.addEventListener('change', start);
window.addEventListener('resize', onResize);
window.addEventListener('load', onResize);
start();
})();
@@ -0,0 +1,191 @@
(() => {
const hero = document.querySelector('.hero-section');
if (!hero) return;
const body = document.body;
const header = document.querySelector('.site-header');
const titleSvg = hero.querySelector('.hero-title-svg');
const scrollIndicator = hero.querySelector('.hero-scroll-indicator');
const scrollArrow = scrollIndicator ? scrollIndicator.querySelector('.scroll-arrow') : null;
const scrollText = scrollIndicator ? scrollIndicator.querySelector('.scroll-text') : null;
const desktopQuery = window.matchMedia('(min-width: 900px)');
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
const anime = window.anime;
const hasAnime = typeof anime === 'function';
let arrowPulse = null;
let didIntro = false;
let ticking = false;
const stopArrowPulse = () => {
if (!arrowPulse) return;
arrowPulse.pause();
arrowPulse = null;
};
const startArrowPulse = () => {
if (!hasAnime || reducedMotionQuery.matches || !scrollArrow) return;
stopArrowPulse();
scrollArrow.style.animation = 'none';
anime.remove(scrollArrow);
anime.set(scrollArrow, {
translateY: 0,
opacity: 0.5,
rotate: 45,
});
arrowPulse = anime({
targets: scrollArrow,
translateY: [0, 6, 0],
opacity: [0.5, 1, 0.5],
rotate: 45,
duration: 1600,
easing: 'easeInOutSine',
loop: true,
});
};
const clearInlineState = () => {
if (header) {
header.style.transform = '';
}
if (scrollIndicator) {
scrollIndicator.style.opacity = '';
scrollIndicator.style.transform = '';
}
};
const animateScrollState = (isAtTop) => {
if (!hasAnime || reducedMotionQuery.matches) return;
if (header) {
anime.remove(header);
anime({
targets: header,
translateY: isAtTop ? '-100%' : '0%',
duration: isAtTop ? 500 : 380,
easing: isAtTop ? 'easeInOutCubic' : 'easeOutCubic',
});
}
if (scrollIndicator) {
anime.remove(scrollIndicator);
anime({
targets: scrollIndicator,
opacity: isAtTop ? 1 : 0,
duration: isAtTop ? 380 : 260,
easing: isAtTop ? 'easeOutCubic' : 'easeOutQuad',
});
}
};
const applyState = (animated) => {
if (!desktopQuery.matches) {
body.classList.remove('home-hero-initial', 'home-hero-scrolled');
stopArrowPulse();
clearInlineState();
return;
}
const isAtTop = window.scrollY <= 6;
body.classList.toggle('home-hero-initial', isAtTop);
body.classList.toggle('home-hero-scrolled', !isAtTop);
if (animated) {
animateScrollState(isAtTop);
} else if (hasAnime) {
if (header) {
anime.remove(header);
}
if (scrollIndicator) {
anime.remove(scrollIndicator);
}
}
if (isAtTop) {
startArrowPulse();
} else {
stopArrowPulse();
}
};
const playIntro = () => {
if (didIntro || !hasAnime || reducedMotionQuery.matches || !desktopQuery.matches || window.scrollY > 6) {
return;
}
didIntro = true;
if (titleSvg) {
anime.set(titleSvg, { opacity: 0, translateY: 14, scale: 1.01 });
}
if (scrollArrow) {
anime.set(scrollArrow, { opacity: 0, translateY: 8, rotate: 45 });
}
if (scrollText) {
anime.set(scrollText, { opacity: 0, translateY: 8 });
}
const timeline = anime.timeline({
easing: 'easeOutCubic',
});
if (titleSvg) {
timeline.add({
targets: titleSvg,
opacity: 1,
translateY: -8,
scale: 1.06,
duration: 860,
easing: 'easeOutExpo',
});
}
if (scrollArrow || scrollText) {
timeline.add({
targets: [scrollArrow, scrollText].filter(Boolean),
opacity: (el) => (el === scrollArrow ? [0, 1] : [0, 1]),
translateY: 0,
rotate: (el) => (el === scrollArrow ? 45 : 0),
duration: 420,
delay: anime.stagger(80),
}, '-=300');
}
timeline.finished.then(() => {
startArrowPulse();
});
};
const onScroll = () => {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(() => {
applyState(true);
ticking = false;
});
};
const onResize = () => {
applyState(false);
};
const onReducedMotionChange = () => {
if (reducedMotionQuery.matches) {
stopArrowPulse();
if (scrollArrow) {
scrollArrow.style.animation = 'none';
}
} else if (window.scrollY <= 6 && desktopQuery.matches) {
startArrowPulse();
}
applyState(false);
};
applyState(false);
playIntro();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onResize);
desktopQuery.addEventListener('change', onResize);
reducedMotionQuery.addEventListener('change', onReducedMotionChange);
})();
@@ -0,0 +1,31 @@
(() => {
const steps = document.querySelectorAll('.intro-step');
if (!steps.length) return;
const markActive = () => {
const vh = window.innerHeight;
steps.forEach((step) => {
const rect = step.getBoundingClientRect();
const isActive = rect.top < vh * 0.8 && rect.bottom > vh * 0.2;
step.classList.toggle('is-active', isActive);
});
};
markActive();
document.body.classList.add('has-intro-animations');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
entry.target.classList.toggle('is-active', entry.isIntersecting);
});
},
{
threshold: 0.35,
rootMargin: '0px 0px -15% 0px',
}
);
steps.forEach((step) => observer.observe(step));
window.addEventListener('resize', markActive);
})();
@@ -0,0 +1,863 @@
(() => {
const canvas = document.getElementById('joinProgressCanvas');
if (!canvas) {
return;
}
const context = canvas.getContext('2d');
if (!context) {
return;
}
const animeLib = window.anime;
const hasAnime = typeof animeLib === 'function';
const waveTrack = document.getElementById('joinWaveTrack');
const waveFill = document.getElementById('joinWaveFill');
const waveBoat = document.getElementById('joinWaveBoat');
const waveMarks = document.getElementById('joinWaveMarks');
const waveProgress = waveTrack ? waveTrack.closest('.join-wave-progress') : null;
const overlayPanel = document.querySelector('.join-canvas-overlay');
const joinData = window.itstudioJoinData && typeof window.itstudioJoinData === 'object'
? window.itstudioJoinData
: {};
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let width = 0;
let height = 0;
let dpr = 1;
let rafId = 0;
let targetProgress = 0;
let displayedProgress = 0;
let trackLeft = 0;
let trackWidth = 0;
let stageMarkers = [];
let stageLayout = null;
const waveShape1 = { base: 0.16, amp: 7.5, len: 280, speed: 0.9 };
const waveShape2 = { base: 0.44, amp: 6, len: 340, speed: 0.75 };
const waveShape3 = { base: 0.68, amp: 6.5, len: 400, speed: 0.62 };
const DAY_MS = 24 * 60 * 60 * 1000;
const ONE_DAY_STAGE_WEIGHT_MS = DAY_MS * 1.8;
const palettes = {
light: {
wave1: 'rgba(122, 178, 230, 0.95)',
wave2: 'rgba(73, 134, 197, 0.72)',
wave3: 'rgba(40, 96, 161, 0.78)',
},
dark: {
wave1: 'rgba(86, 146, 214, 0.92)',
wave2: 'rgba(49, 104, 172, 0.78)',
wave3: 'rgba(27, 73, 136, 0.82)',
},
};
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function getTheme() {
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
}
function getNumber(value) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
function getStages() {
return Array.isArray(joinData.stages) ? joinData.stages : [];
}
function getStageDurationMs(stage) {
const start = getNumber(stage ? stage.startTs : null);
const end = getNumber(stage ? stage.endTs : null);
if (start === null || end === null) {
return null;
}
return Math.max(0, end - start);
}
function getEffectiveStageWeightMs(stage) {
const duration = getStageDurationMs(stage);
if (duration === null) {
return ONE_DAY_STAGE_WEIGHT_MS;
}
if (duration <= DAY_MS) {
return ONE_DAY_STAGE_WEIGHT_MS;
}
return duration;
}
function buildStageLayout(stages) {
if (!stages.length) {
return {
markerByIndex: [],
endByIndex: [],
};
}
const weights = stages.map((stage) => getEffectiveStageWeightMs(stage));
const totalWeight = Math.max(1, weights.reduce((sum, w) => sum + w, 0));
const markerByIndex = [];
const endByIndex = [];
let cumulative = 0;
for (let i = 0; i < stages.length; i += 1) {
markerByIndex[i] = clamp(cumulative / totalWeight, 0, 1);
cumulative += weights[i];
endByIndex[i] = clamp(cumulative / totalWeight, 0, 1);
}
endByIndex[endByIndex.length - 1] = 1;
return { markerByIndex, endByIndex };
}
function getStageLayout(stages) {
if (!stageLayout || !stageLayout.markerByIndex || stageLayout.markerByIndex.length !== stages.length) {
stageLayout = buildStageLayout(stages);
}
return stageLayout;
}
function getNavigationType() {
if (window.performance && typeof window.performance.getEntriesByType === 'function') {
const entries = window.performance.getEntriesByType('navigation');
if (entries && entries.length > 0 && entries[0].type) {
return entries[0].type;
}
}
if (window.performance && window.performance.navigation) {
if (window.performance.navigation.type === 1) {
return 'reload';
}
if (window.performance.navigation.type === 2) {
return 'back_forward';
}
}
return 'navigate';
}
function shouldAnimateEntry() {
if (prefersReducedMotion.matches) {
return false;
}
return getNavigationType() !== 'reload';
}
function setupOverlayEntryAnimation() {
if (!overlayPanel) {
return;
}
overlayPanel.classList.remove('is-enter-animate');
if (!shouldAnimateEntry()) {
return;
}
requestAnimationFrame(() => {
overlayPanel.classList.add('is-enter-animate');
});
}
function resize() {
dpr = window.devicePixelRatio || 1;
width = canvas.clientWidth;
height = canvas.clientHeight;
canvas.width = Math.max(1, Math.floor(width * dpr));
canvas.height = Math.max(1, Math.floor(height * dpr));
context.setTransform(dpr, 0, 0, dpr, 0, 0);
updateTrackMetrics();
}
function updateTrackMetrics() {
if (!waveTrack) {
trackLeft = 0;
trackWidth = width;
return;
}
const canvasRect = canvas.getBoundingClientRect();
const trackRect = waveTrack.getBoundingClientRect();
trackLeft = clamp(trackRect.left - canvasRect.left, 0, width);
trackWidth = Number.isFinite(trackRect.width) && trackRect.width > 0 ? trackRect.width : width;
}
function drawWave(color, yBase, amplitude, wavelength, speed, time) {
const overscan = 24;
const step = 6;
context.beginPath();
context.moveTo(-overscan, height + 2);
for (let x = -overscan; x <= width + overscan; x += step) {
const theta = (x / wavelength) * Math.PI * 2 + time * speed;
const y = yBase + Math.sin(theta) * amplitude;
context.lineTo(x, y);
}
context.lineTo(width + overscan, height + 2);
context.lineTo(-overscan, height + 2);
context.closePath();
context.fillStyle = color;
context.fill();
}
function sampleWaveAtX(shape, x, time) {
const theta = (x / shape.len) * Math.PI * 2 + time * shape.speed;
const y = Math.sin(theta) * shape.amp;
const slope = Math.cos(theta) * (Math.PI * 2 / shape.len) * shape.amp;
return { y, slope };
}
function getWaveXFromProgress(progress) {
const safeProgress = clamp(progress, 0, 1);
const currentTrackWidth = trackWidth > 0 ? trackWidth : width;
return trackLeft + (safeProgress * currentTrackWidth);
}
function applyFloatingMotion(time) {
if (width <= 0) {
return;
}
const boatWave = sampleWaveAtX(waveShape3, getWaveXFromProgress(displayedProgress), time);
if (waveBoat) {
const boatTilt = clamp(boatWave.slope * 34, -7.2, 7.2);
waveBoat.style.setProperty('--boat-wave-offset', `${boatWave.y.toFixed(2)}px`);
waveBoat.style.setProperty('--boat-tilt', `${boatTilt.toFixed(2)}deg`);
}
stageMarkers.forEach((marker) => {
if (marker.type === 'lighthouse') {
marker.element.style.setProperty('--mark-wave-offset', '0px');
marker.element.style.setProperty('--mark-tilt', '0deg');
return;
}
const markerWave = sampleWaveAtX(waveShape3, getWaveXFromProgress(marker.progress), time);
const markerTilt = clamp(markerWave.slope * 34, -7.2, 7.2);
marker.element.style.setProperty('--mark-wave-offset', `${markerWave.y.toFixed(2)}px`);
marker.element.style.setProperty('--mark-tilt', `${markerTilt.toFixed(2)}deg`);
});
}
function renderWaves(timestamp) {
const theme = getTheme();
const palette = palettes[theme];
const time = timestamp * 0.001;
context.clearRect(0, 0, width, height);
drawWave(palette.wave1, height * waveShape1.base, waveShape1.amp, waveShape1.len, waveShape1.speed, time);
drawWave(palette.wave2, height * waveShape2.base, waveShape2.amp, waveShape2.len, waveShape2.speed, time);
drawWave(palette.wave3, height * waveShape3.base, waveShape3.amp, waveShape3.len, waveShape3.speed, time);
applyFloatingMotion(time);
rafId = requestAnimationFrame(renderWaves);
}
function stopWaves() {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = 0;
}
}
function drawStaticWaves() {
const theme = getTheme();
const palette = palettes[theme];
context.clearRect(0, 0, width, height);
drawWave(palette.wave1, height * waveShape1.base, waveShape1.amp, waveShape1.len, waveShape1.speed, 0);
drawWave(palette.wave2, height * waveShape2.base, waveShape2.amp, waveShape2.len, waveShape2.speed, 0);
drawWave(palette.wave3, height * waveShape3.base, waveShape3.amp, waveShape3.len, waveShape3.speed, 0);
applyFloatingMotion(0);
}
function startWaves() {
stopWaves();
resize();
if (prefersReducedMotion.matches) {
drawStaticWaves();
return;
}
rafId = requestAnimationFrame(renderWaves);
}
function computeTargetProgress() {
const stages = getStages();
if (!stages.length) {
return 0;
}
const layout = getStageLayout(stages);
const markerByIndex = layout.markerByIndex;
const endByIndex = layout.endByIndex;
const currentIndex = getNumber(joinData.currentStageIndex);
const nowTs = getNumber(joinData.nowTs) || Date.now();
// 有进行中阶段时,在该阶段对应区间内按时间连续推进。
if (currentIndex !== null && currentIndex >= 0 && currentIndex < stages.length) {
const stage = stages[currentIndex];
const startProgress = getNumber(markerByIndex[currentIndex]) ?? 0;
const endProgress = getNumber(endByIndex[currentIndex]) ?? startProgress;
const startTs = getNumber(stage ? stage.startTs : null);
const endTs = getNumber(stage ? stage.endTs : null);
if (startTs !== null && endTs !== null && endTs > startTs) {
const ratio = clamp((nowTs - startTs) / (endTs - startTs), 0, 1);
return clamp(startProgress + ((endProgress - startProgress) * ratio), 0, 1);
}
return clamp(startProgress, 0, 1);
}
// 无进行中阶段时,停在最近已完成阶段的末端。
let lastCompletedIndex = -1;
for (let i = 0; i < stages.length; i += 1) {
if (stages[i] && stages[i].status === 'completed') {
lastCompletedIndex = i;
}
}
// 阶段空档期:在“上一阶段浮标”和“下一阶段浮标”之间按时间线性推进。
if (lastCompletedIndex >= 0) {
let nextUpcomingIndex = -1;
for (let i = lastCompletedIndex + 1; i < stages.length; i += 1) {
if (stages[i] && stages[i].status === 'upcoming') {
nextUpcomingIndex = i;
break;
}
}
if (nextUpcomingIndex >= 0) {
const completedStage = stages[lastCompletedIndex];
const upcomingStage = stages[nextUpcomingIndex];
const gapStartTs = getNumber(completedStage ? completedStage.endTs : null);
const gapEndTs = getNumber(upcomingStage ? upcomingStage.startTs : null);
const fromProgress = clamp(getNumber(markerByIndex[lastCompletedIndex]) ?? 0, 0, 1);
const toProgress = clamp(getNumber(markerByIndex[nextUpcomingIndex]) ?? fromProgress, 0, 1);
if (gapStartTs !== null && gapEndTs !== null && gapEndTs > gapStartTs && nowTs > gapStartTs && nowTs < gapEndTs) {
const gapRatio = clamp((nowTs - gapStartTs) / (gapEndTs - gapStartTs), 0, 1);
return clamp(fromProgress + ((toProgress - fromProgress) * gapRatio), 0, 1);
}
if (nowTs <= gapStartTs) {
return fromProgress;
}
if (nowTs < gapEndTs) {
return clamp((fromProgress + toProgress) * 0.5, 0, 1);
}
}
}
if (lastCompletedIndex >= 0) {
return clamp(getNumber(endByIndex[lastCompletedIndex]) ?? 0, 0, 1);
}
return 0;
}
function getBuoyMarkerSvg() {
return `
<svg viewBox="0 0 40 52" role="img" aria-hidden="true" focusable="false">
<ellipse class="marker-buoy-ring" cx="20" cy="40.5" rx="12.8" ry="5.4"></ellipse>
<path class="marker-buoy-base" d="M13.2 21.2C13.2 16.8 16.8 13.2 21.2 13.2C25.6 13.2 29.2 16.8 29.2 21.2V31.8C29.2 35.1 26.5 37.8 23.2 37.8H19.2C15.9 37.8 13.2 35.1 13.2 31.8V21.2Z"></path>
<path class="marker-buoy-stripe" d="M13.2 23.6H29.2V28.1H13.2Z"></path>
<path class="marker-buoy-cap" d="M16.6 12.6H25.8V15.4H16.6Z"></path>
<circle class="marker-buoy-light" cx="21.2" cy="10.2" r="2.4"></circle>
<path class="marker-buoy-gloss" d="M17 18.4C17.6 16.9 19 15.9 20.5 15.9V35.1C18.4 35.1 16.7 33.4 16.7 31.3V19.2C16.7 18.9 16.8 18.6 17 18.4Z"></path>
</svg>
`;
}
function getLighthouseMarkerSvg() {
return `
<svg viewBox="0 0 48 66" role="img" aria-hidden="true" focusable="false">
<path class="marker-reef-rock" d="M6 59L12 52L20 55L27 49L35 52L42 48L45 59L6 59Z"></path>
<path class="marker-reef-highlight" d="M13 56L19 54L24 55L31 52L36 54L33 57L20 58L13 56Z"></path>
<path class="marker-lh-base" d="M15 50H33L30 57H18L15 50Z"></path>
<path class="marker-lh-tower" d="M18 50L21 15H27L30 50H18Z"></path>
<path class="marker-lh-top" d="M18 15H30L27.5 10H20.5L18 15Z"></path>
<path class="marker-lh-top" d="M16 20H32V23H16Z"></path>
<path class="marker-lh-beam marker-lh-beam-right" d="M32 18L46 15V22L32 20Z"></path>
<path class="marker-lh-beam marker-lh-beam-left" d="M16 18L2 15V22L16 20Z"></path>
</svg>
`;
}
function renderStageMarks() {
if (!waveMarks) {
return;
}
stageMarkers = [];
waveMarks.innerHTML = '';
if (waveProgress) {
waveProgress.querySelectorAll('.join-wave-mark.is-lighthouse.is-docked').forEach((node) => node.remove());
}
const stages = getStages();
if (!stages.length) {
return;
}
const layout = getStageLayout(stages);
const markerByIndex = layout.markerByIndex;
const currentIndex = getNumber(joinData.currentStageIndex);
const safeCurrentIndex = currentIndex === null ? -1 : Math.round(currentIndex);
stages.forEach((stage, index) => {
const marker = document.createElement('span');
marker.className = 'join-wave-mark';
const isLighthouse = index === stages.length - 1 || (stage && stage.key === 'public_notice');
const progress = isLighthouse
? 1
: clamp(getNumber(markerByIndex[index]) ?? 0, 0, 1);
marker.classList.add(isLighthouse ? 'is-lighthouse' : 'is-buoy');
if (index === safeCurrentIndex) {
marker.classList.add('is-active');
}
const icon = document.createElement('span');
icon.className = 'join-wave-mark-icon';
icon.innerHTML = isLighthouse ? getLighthouseMarkerSvg() : getBuoyMarkerSvg();
marker.appendChild(icon);
const beams = isLighthouse ? Array.from(icon.querySelectorAll('.marker-lh-beam')) : [];
if (isLighthouse && waveProgress) {
marker.classList.add('is-docked');
marker.style.right = '';
marker.style.left = 'calc(100% - clamp(18px, 2.2vw, 30px))';
waveProgress.appendChild(marker);
} else {
marker.style.left = `${(progress * 100).toFixed(3)}%`;
waveMarks.appendChild(marker);
}
stageMarkers.push({
element: marker,
icon,
beams,
progress,
type: isLighthouse ? 'lighthouse' : 'buoy',
});
});
}
function stopBoatAnimations() {
if (!hasAnime) {
return;
}
const targets = [waveFill, waveBoat];
stageMarkers.forEach((marker) => {
if (marker.icon) {
targets.push(marker.icon);
}
if (Array.isArray(marker.beams) && marker.beams.length) {
marker.beams.forEach((beam) => targets.push(beam));
}
});
animeLib.remove(targets);
}
function startLighthouseBeacon() {
const allBeams = [];
const leftBeams = [];
const rightBeams = [];
stageMarkers.forEach((marker) => {
if (marker.type !== 'lighthouse' || !Array.isArray(marker.beams)) {
return;
}
marker.beams.forEach((beam) => {
allBeams.push(beam);
beam.style.opacity = '0.28';
if (beam.classList.contains('marker-lh-beam-left')) {
leftBeams.push(beam);
} else if (beam.classList.contains('marker-lh-beam-right')) {
rightBeams.push(beam);
}
});
});
if (!allBeams.length || !hasAnime || prefersReducedMotion.matches) {
return;
}
const duration = 3600;
const easing = 'easeInOutSine';
if (rightBeams.length) {
animeLib({
targets: rightBeams,
opacity: [0.2, 0.86, 0.2],
duration,
easing,
loop: true,
});
}
if (leftBeams.length) {
animeLib({
targets: leftBeams,
opacity: [0.2, 0.82, 0.2],
duration,
delay: duration / 2,
easing,
loop: true,
});
} else if (!rightBeams.length) {
animeLib({
targets: allBeams,
opacity: [0.2, 0.84, 0.2],
duration,
easing,
loop: true,
});
}
}
function setProgress(value) {
const safe = clamp(value, 0, 1);
displayedProgress = safe;
const percent = `${(safe * 100).toFixed(3)}%`;
if (waveFill) {
waveFill.style.width = percent;
}
if (waveBoat) {
waveBoat.style.left = percent;
}
}
function animateBoatToTarget() {
if (!waveBoat || !waveFill) {
return;
}
stopBoatAnimations();
if (!hasAnime || !shouldAnimateEntry()) {
setProgress(targetProgress);
startLighthouseBeacon();
return;
}
const state = { value: 0 };
setProgress(0);
animeLib({
targets: state,
value: targetProgress,
duration: 1750,
easing: 'easeOutCubic',
update: () => {
setProgress(state.value);
},
complete: () => {
setProgress(targetProgress);
startLighthouseBeacon();
},
});
animeLib({
targets: waveFill,
opacity: [0.55, 1],
duration: 1100,
easing: 'easeOutSine',
});
const activeIcon = waveMarks ? waveMarks.querySelector('.join-wave-mark.is-active .join-wave-mark-icon') : null;
if (activeIcon) {
animeLib({
targets: activeIcon,
scale: [1, 1.24, 1],
opacity: [0.82, 1, 0.92],
duration: 850,
delay: 980,
easing: 'easeOutSine',
});
}
}
function handleMotionPreferenceChange() {
targetProgress = computeTargetProgress();
setProgress(targetProgress);
stopBoatAnimations();
startWaves();
startLighthouseBeacon();
}
const observer = new MutationObserver(() => {
if (prefersReducedMotion.matches) {
drawStaticWaves();
}
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
if (typeof prefersReducedMotion.addEventListener === 'function') {
prefersReducedMotion.addEventListener('change', handleMotionPreferenceChange);
} else if (typeof prefersReducedMotion.addListener === 'function') {
prefersReducedMotion.addListener(handleMotionPreferenceChange);
}
window.addEventListener('resize', () => {
startWaves();
setProgress(targetProgress);
});
window.addEventListener('orientationchange', () => {
startWaves();
setProgress(targetProgress);
});
window.addEventListener('beforeunload', () => {
stopWaves();
stopBoatAnimations();
});
function resolveLanguage() {
const bootLang = typeof window.__ITSTUDIO_LANG__ === 'string' ? window.__ITSTUDIO_LANG__ : '';
const htmlLang = document.documentElement.getAttribute('lang') || '';
const lang = (bootLang || htmlLang || 'zh').toLowerCase();
return lang.indexOf('en') === 0 ? 'en' : 'zh';
}
function pickLocaleText(cnText, enText) {
return resolveLanguage() === 'en'
? (enText || cnText || '')
: (cnText || enText || '');
}
function getProgressLookupConfig() {
const config = joinData && typeof joinData.progressLookup === 'object' ? joinData.progressLookup : null;
if (!config) {
return null;
}
if (!config.ajaxUrl || !config.action || !config.nonce) {
return null;
}
return config;
}
function ensureProgressFeedbackNode(formElement) {
const host = formElement.closest('.join-form-content') || formElement.parentElement;
if (!host) {
return null;
}
let feedback = host.querySelector('.join-progress-query-feedback');
if (!feedback) {
feedback = document.createElement('p');
feedback.className = 'join-progress-query-feedback';
feedback.hidden = true;
feedback.setAttribute('aria-live', 'polite');
host.appendChild(feedback);
}
return feedback;
}
function setProgressFeedback(feedback, tone, messageCn, messageEn) {
if (!feedback) {
return;
}
const normalizedTone = ['success', 'warning', 'error', 'info'].indexOf(tone) >= 0 ? tone : 'info';
feedback.classList.remove('is-success', 'is-warning', 'is-error', 'is-info');
feedback.classList.add(`is-${normalizedTone}`);
feedback.dataset.cn = messageCn || '';
feedback.dataset.en = messageEn || '';
feedback.textContent = pickLocaleText(messageCn, messageEn);
feedback.hidden = !(feedback.textContent && feedback.textContent.trim() !== '');
}
function setupProgressLookupAjax() {
const formElement = document.querySelector('.join-progress-query-form');
if (!formElement || typeof window.fetch !== 'function') {
return;
}
const config = getProgressLookupConfig();
if (!config) {
return;
}
const identityInput = formElement.querySelector('input[name="join_query_identity"]');
const submitButton = formElement.querySelector('button[type="submit"], input[type="submit"]');
const feedback = ensureProgressFeedbackNode(formElement);
formElement.addEventListener('submit', async (event) => {
event.preventDefault();
const identity = identityInput ? identityInput.value.trim() : '';
const payload = new URLSearchParams();
payload.set('action', config.action);
payload.set('nonce', config.nonce);
payload.set('join_progress_lookup', '1');
payload.set('join_query_identity', identity);
if (submitButton) {
submitButton.disabled = true;
submitButton.setAttribute('aria-busy', 'true');
}
setProgressFeedback(feedback, 'info', '查询中,请稍候…', 'Checking, please wait...');
try {
const response = await window.fetch(config.ajaxUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: payload.toString(),
});
const result = await response.json();
if (!result || !result.success || !result.data) {
throw new Error('invalid_response');
}
const data = result.data;
setProgressFeedback(
feedback,
data.tone || 'info',
data.message_cn || '',
data.message_en || ''
);
} catch (error) {
setProgressFeedback(
feedback,
'error',
'查询失败,请稍后重试。',
'Lookup failed. Please try again later.'
);
} finally {
if (submitButton) {
submitButton.disabled = false;
submitButton.removeAttribute('aria-busy');
}
}
});
}
function detectJoinSubmitIntentFromElement(element) {
if (!element) {
return '';
}
const name = (element.getAttribute('name') || '').toLowerCase();
const value = String(element.value || element.textContent || '').toLowerCase();
if (name.indexOf('draft') !== -1 || value.indexOf('draft') !== -1 || value.indexOf('草稿') !== -1) {
return 'draft';
}
return 'submitted';
}
function ensureJoinSubmitNoticeNode() {
let notice = document.getElementById('joinSubmitNotice');
if (notice) {
return notice;
}
notice = document.querySelector('.join-submit-notice');
if (notice) {
notice.id = 'joinSubmitNotice';
return notice;
}
const head = document.querySelector('.join-head');
if (!head || !head.parentNode) {
return null;
}
notice = document.createElement('p');
notice.id = 'joinSubmitNotice';
notice.className = 'join-submit-notice';
notice.hidden = true;
head.parentNode.insertBefore(notice, head.nextSibling);
return notice;
}
function showJoinSubmitNotice(intent) {
const notice = ensureJoinSubmitNoticeNode();
if (!notice) {
return;
}
const isDraft = intent === 'draft';
const messageCn = isDraft ? '草稿已保存' : '你的报名表单已提交,感谢报名';
const messageEn = isDraft ? 'Draft saved.' : 'Your registration form has been submitted. Thank you for applying.';
notice.dataset.cn = messageCn;
notice.dataset.en = messageEn;
notice.textContent = pickLocaleText(messageCn, messageEn);
notice.hidden = false;
}
function setupRegistrationSubmitNoticeBridge() {
const formElement = document.querySelector('.join-form-content .frm_forms form');
if (!formElement) {
return;
}
let submitIntent = 'submitted';
formElement.addEventListener('click', (event) => {
const control = event.target.closest('button, input[type="submit"]');
const nextIntent = detectJoinSubmitIntentFromElement(control);
if (nextIntent) {
submitIntent = nextIntent;
}
});
formElement.addEventListener('submit', (event) => {
const submitter = event.submitter || null;
const nextIntent = detectJoinSubmitIntentFromElement(submitter);
if (nextIntent) {
submitIntent = nextIntent;
}
});
const formHost = formElement.closest('.join-form-content') || formElement.parentElement;
if (!formHost) {
return;
}
const updateSuccessMessage = () => {
const successNode = formHost.querySelector('.frm_message, .frm_success_style, .frm_success');
if (!successNode) {
return;
}
const lowerText = (successNode.textContent || '').toLowerCase();
const resolvedIntent = (lowerText.indexOf('draft') !== -1 || lowerText.indexOf('草稿') !== -1)
? 'draft'
: submitIntent;
const messageCn = resolvedIntent === 'draft' ? '草稿已保存' : '你的报名表单已提交,感谢报名';
const messageEn = resolvedIntent === 'draft' ? 'Draft saved.' : 'Your registration form has been submitted. Thank you for applying.';
successNode.dataset.cn = messageCn;
successNode.dataset.en = messageEn;
successNode.textContent = pickLocaleText(messageCn, messageEn);
showJoinSubmitNotice(resolvedIntent);
};
const observer = new MutationObserver(updateSuccessMessage);
observer.observe(formHost, { childList: true, subtree: true });
}
targetProgress = computeTargetProgress();
setupOverlayEntryAnimation();
renderStageMarks();
startWaves();
animateBoatToTarget();
setupProgressLookupAjax();
setupRegistrationSubmitNoticeBridge();
})();
@@ -0,0 +1,861 @@
(() => {
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 compact = width <= 680;
const narrow = width <= 420;
const gutterWidth = compact
? clamp(Math.round(width * (narrow ? 0.054 : 0.06)), 30, narrow ? 42 : 48)
: clamp(Math.round(width * 0.08), 56, 92);
const fontSize = compact
? clamp(Math.round(width / (narrow ? 152 : 136)), narrow ? 8 : 9, narrow ? 11 : 12)
: clamp(Math.round(width / 98), 14, 20);
const lineHeight = Math.round(fontSize * (compact ? (narrow ? 1.34 : 1.42) : 1.72));
const topPadding = Math.round(lineHeight * (compact ? (narrow ? 0.58 : 0.7) : 1.24));
const codeStartX = gutterWidth + (compact ? (narrow ? 6 : 8) : 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);
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(
compact ? (narrow ? 7 : 8) : 12,
y - lineHeight * 0.38,
compact ? (narrow ? 1.9 : 2.2) : 3.2,
0,
Math.PI * 2
);
ctx.fill();
} else if ((lineIndex + 3) % 23 === 0) {
ctx.beginPath();
ctx.fillStyle = palette.markerWarn;
ctx.arc(
compact ? (narrow ? 7 : 8) : 12,
y - lineHeight * 0.38,
compact ? (narrow ? 1.9 : 2.2) : 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();
})();
@@ -0,0 +1,80 @@
const langToggle = document.querySelector('.lang-toggle');
const body = document.body;
function getPreferredLang() {
if (window.__ITSTUDIO_LANG__ === 'zh' || window.__ITSTUDIO_LANG__ === 'en') {
return window.__ITSTUDIO_LANG__;
}
const savedLang = localStorage.getItem('language');
if (savedLang === 'zh' || savedLang === 'en') {
return savedLang;
}
return 'zh';
}
function setElementTextByLang(element, lang) {
const nextText = lang === 'en' ? element.getAttribute('data-en') : element.getAttribute('data-cn');
if (!nextText) {
return;
}
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
const inputType = (element.getAttribute('type') || '').toLowerCase();
if (inputType === 'submit' || inputType === 'button' || inputType === 'reset') {
element.value = nextText;
return;
}
}
element.textContent = nextText;
}
function setElementPlaceholderByLang(element, lang) {
const key = lang === 'en' ? 'data-en-placeholder' : 'data-cn-placeholder';
const value = element.getAttribute(key);
if (value) {
element.setAttribute('placeholder', value);
}
}
function setElementAriaLabelByLang(element, lang) {
const key = lang === 'en' ? 'data-en-aria-label' : 'data-cn-aria-label';
const value = element.getAttribute(key);
if (value) {
element.setAttribute('aria-label', value);
}
}
function setLang(lang) {
body.setAttribute('lang', lang);
if (langToggle) {
const langText = langToggle.querySelector('.lang-text');
if (langText) {
langText.textContent = lang === 'en' ? '中' : 'EN';
}
}
const textNodes = document.querySelectorAll('[data-cn][data-en]');
textNodes.forEach((element) => setElementTextByLang(element, lang));
const placeholderNodes = document.querySelectorAll('[data-cn-placeholder][data-en-placeholder]');
placeholderNodes.forEach((element) => setElementPlaceholderByLang(element, lang));
const ariaNodes = document.querySelectorAll('[data-cn-aria-label][data-en-aria-label]');
ariaNodes.forEach((element) => setElementAriaLabelByLang(element, lang));
localStorage.setItem('language', lang);
}
const initialLang = getPreferredLang();
setLang(initialLang);
if (langToggle) {
langToggle.addEventListener('click', () => {
const currentLang = body.getAttribute('lang');
const nextLang = currentLang === 'en' ? 'zh' : 'en';
setLang(nextLang);
});
}
+31
View File
@@ -0,0 +1,31 @@
const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
const navMenu = document.querySelector('.nav-menu');
if (mobileMenuToggle && navMenu) {
mobileMenuToggle.addEventListener('click', () => {
navMenu.classList.toggle('active');
mobileMenuToggle.classList.toggle('active');
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.main-navigation')) {
navMenu.classList.remove('active');
mobileMenuToggle.classList.remove('active');
}
});
window.addEventListener('resize', () => {
if (window.innerWidth > 768) {
navMenu.classList.remove('active');
mobileMenuToggle.classList.remove('active');
}
});
}
document.addEventListener('DOMContentLoaded', () => {
const postItems = document.querySelectorAll('.post-item');
postItems.forEach((item, index) => {
item.style.animationDelay = `${index * 0.1}s`;
});
});
@@ -0,0 +1,128 @@
(function () {
"use strict";
var TITLE_SELECTOR = ".single-article-title";
var MAX_LINES = 2;
var MIN_FONT_SIZE = 12;
var MAX_FONT_SIZE = 92;
var SEARCH_STEPS = 16;
function getLineHeightPx(element) {
var computed = window.getComputedStyle(element);
var lineHeight = computed.lineHeight;
var fontSize = parseFloat(computed.fontSize) || 16;
if (lineHeight && lineHeight.indexOf("px") > -1) {
return parseFloat(lineHeight);
}
if (lineHeight === "normal") {
return fontSize * 1.2;
}
var ratio = parseFloat(lineHeight);
if (Number.isFinite(ratio)) {
return ratio * fontSize;
}
return fontSize * 1.2;
}
function getLineCount(element) {
var lineHeightPx = getLineHeightPx(element);
if (!lineHeightPx) {
return 0;
}
return element.getBoundingClientRect().height / lineHeightPx;
}
function setFontSize(element, size) {
element.style.fontSize = size.toFixed(2) + "px";
}
function fitsWithinLines(element, size) {
setFontSize(element, size);
return getLineCount(element) <= MAX_LINES + 0.01;
}
function fitTitle(title) {
if (!title) {
return;
}
var header = title.closest(".single-article-header") || title.parentElement || title;
if (!header || header.getBoundingClientRect().width <= 0) {
return;
}
title.style.fontSize = "";
var computedSize = parseFloat(window.getComputedStyle(title).fontSize) || 48;
var low = MIN_FONT_SIZE;
var high = Math.min(MAX_FONT_SIZE, Math.max(computedSize * 1.8, computedSize + 8));
while (high < MAX_FONT_SIZE && fitsWithinLines(title, high)) {
high = Math.min(MAX_FONT_SIZE, high + 8);
if (high === MAX_FONT_SIZE) {
break;
}
}
var best = low;
for (var i = 0; i < SEARCH_STEPS; i += 1) {
var mid = (low + high) / 2;
if (fitsWithinLines(title, mid)) {
best = mid;
low = mid;
} else {
high = mid;
}
}
if (!fitsWithinLines(title, best)) {
while (best > MIN_FONT_SIZE && !fitsWithinLines(title, best)) {
best -= 0.5;
}
}
setFontSize(title, Math.max(MIN_FONT_SIZE, best - 0.05));
}
function initSingleTitleFitter() {
var title = document.querySelector(TITLE_SELECTOR);
if (!title) {
return;
}
var scheduled = false;
var scheduleFit = function () {
if (scheduled) {
return;
}
scheduled = true;
window.requestAnimationFrame(function () {
scheduled = false;
fitTitle(title);
});
};
scheduleFit();
window.addEventListener("resize", scheduleFit, { passive: true });
window.addEventListener("pageshow", scheduleFit);
if ("ResizeObserver" in window) {
var resizeObserver = new ResizeObserver(scheduleFit);
resizeObserver.observe(title);
var header = title.closest(".single-article-header");
if (header) {
resizeObserver.observe(header);
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initSingleTitleFitter);
} else {
initSingleTitleFitter();
}
})();
+100
View File
@@ -0,0 +1,100 @@
document.addEventListener('DOMContentLoaded', function() {
const container = document.querySelector('.stream-text') || document.querySelector('.hero-description');
if (!container) return;
const cnText = container.getAttribute('data-cn') || '';
const enText = container.getAttribute('data-en') || '';
// 标记动画是否已经完整播放过一次
let hasPlayedOnce = false;
// 存储当前的定时器,以便清理
let timeouts = [];
function getCurrentText() {
return document.body.getAttribute('lang') === 'en' ? enText : cnText;
}
function clearTimeouts() {
timeouts.forEach(t => clearTimeout(t));
timeouts = [];
}
// 渲染文本的核心函数
// text: 要显示的文本
// animate: 是否需要逐字流式动画
function render(text, animate) {
clearTimeouts();
// 使用 Array.from 正确处理 Unicode 字符
const chars = Array.from(text);
// 生成 HTML:每个字符包裹在 span 中
// 如果需要动画,初始透明度为 0
// 如果不需要动画(切换语言时),初始透明度直接为 1
const initialOpacity = animate ? '0' : '1';
// 生成 spans 字符串
const html = chars.map(char => {
// 对空格也可以处理,虽然空格看不见
return `<span class="stream-char" style="opacity: ${initialOpacity};">${char}</span>`;
}).join('');
// 一次性插入 DOM,这时容器会被透明字符撑开,布局位置固定
container.innerHTML = html;
// 如果不需要动画,直接结束
if (!animate) {
return;
}
// 开始执行流式淡入动画
const spans = container.querySelectorAll('.stream-char');
// 基础延迟累加器
let accumulatedDelay = 0;
spans.forEach((span, index) => {
// 模拟流式生成的随机节奏
// 基础 20ms,随机增加 0-30ms
const step = 20 + Math.random() * 30;
accumulatedDelay += step;
// 遇到标点符号稍微停顿一下
if (/[,,.。;;!!??::]/.test(span.textContent)) {
accumulatedDelay += 100;
}
const t = setTimeout(() => {
span.style.opacity = '1';
// 当最后一个字符显示完毕,标记动画已播放
if (index === spans.length - 1) {
hasPlayedOnce = true;
}
}, accumulatedDelay);
timeouts.push(t);
});
}
// 监听语言切换
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'lang') {
const newText = getCurrentText();
// 需求:如果动画已经播放完成过一次,切换语言就不再播放
// 注意:如果用户在动画还没播放完时就切换语言,为了体验,通常也应该直接显示新语言(视为打断),防止混乱
// 所以只要是切换语言,我们将动画设为 false (除非你想每次刷新页面都看)
// 既然用户强调 "如果动画已经播放完成过一次...就不应该再播放",隐含初次加载要播放。
// 我们这里处理逻辑:切换语言时,强制不播放动画 (直接显示),并标记 hasPlayedOnce = true (以防万一)
hasPlayedOnce = true;
render(newText, false);
}
});
});
observer.observe(document.body, { attributes: true });
// 页面首次加载:播放动画
render(getCurrentText(), true);
});
@@ -0,0 +1,148 @@
const themeToggles = document.querySelectorAll('.theme-toggle');
const html = document.documentElement;
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
const supportsViewTransition = typeof document.startViewTransition === 'function';
const viewTransitionStyleId = 'itstudio-view-transition-style';
let isThemeTransitioning = false;
function ensureViewTransitionStyles() {
if (!supportsViewTransition || document.getElementById(viewTransitionStyleId)) {
return;
}
const style = document.createElement('style');
style.id = viewTransitionStyleId;
style.textContent = `
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-group(root) {
animation: none;
}
`;
document.head.appendChild(style);
}
function updateLogoColor(theme) {
const logoText = document.querySelector('#logo-text-cn');
if (logoText) {
logoText.setAttribute('fill', theme === 'dark' ? '#ffffff' : '#000000');
}
}
function getPreferredTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
return savedTheme;
}
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
return systemPreference;
}
function setTheme(theme) {
html.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
themeToggles.forEach((toggle) => {
toggle.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false');
});
updateLogoColor(theme);
}
const initialTheme = getPreferredTheme();
setTheme(initialTheme);
ensureViewTransitionStyles();
function getToggleCenter(toggle) {
if (!toggle) {
return {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
};
}
const rect = toggle.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
}
function getTransitionOrigin(event, toggle) {
const hasPointerPoint = Number.isFinite(event.clientX) && Number.isFinite(event.clientY) && (event.clientX !== 0 || event.clientY !== 0);
if (hasPointerPoint) {
return {
x: event.clientX,
y: event.clientY,
};
}
return getToggleCenter(toggle);
}
function getRevealRadius(x, y) {
const maxX = Math.max(x, window.innerWidth - x);
const maxY = Math.max(y, window.innerHeight - y);
return Math.hypot(maxX, maxY);
}
function switchThemeWithScatter(theme, origin) {
if (isThemeTransitioning || prefersReducedMotion.matches || !supportsViewTransition) {
setTheme(theme);
return;
}
isThemeTransitioning = true;
const x = origin.x;
const y = origin.y;
const endRadius = getRevealRadius(x, y);
const clipFrom = `circle(0px at ${x}px ${y}px)`;
const clipTo = `circle(${endRadius}px at ${x}px ${y}px)`;
const transition = document.startViewTransition(() => {
setTheme(theme);
});
transition.ready
.then(() => {
document.documentElement.animate(
{
clipPath: [clipFrom, clipTo],
},
{
duration: 650,
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
pseudoElement: '::view-transition-new(root)',
}
);
})
.catch(() => {})
.finally(() => {
transition.finished.finally(() => {
isThemeTransitioning = false;
});
});
}
themeToggles.forEach((themeToggle) => {
themeToggle.addEventListener('click', (event) => {
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
const origin = getTransitionOrigin(event, themeToggle);
switchThemeWithScatter(newTheme, origin);
});
});
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
const newTheme = e.matches ? 'dark' : 'light';
setTheme(newTheme);
}
});
window.addEventListener('load', () => {
updateLogoColor(html.getAttribute('data-theme') || getPreferredTheme());
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,87 @@
# ITStudioMainSite 主题业务逻辑拆分方案
> 目标:把 `functions.php`(3959 行)里混入的插件级业务逻辑抽成 3 个独立插件,主题只保留展示层。
## 一、数据安全铁律
迁移只搬代码,不动数据库。以下标识符字符串必须一字不改,否则数据库中老数据失联:
| 类型 | 必须保持不变的值 |
|---|---|
| 文章类型 slug | `news`、`announcement`、`service`、`post` |
| 分类法 slug | `service_category` |
| 选项名 | `itstudio_join_settings` |
| post meta / ACF 字段名 | `itstudio_weight`、`itstudio_is_recruitment_article`、各 service meta key |
| AJAX action / nonce | `itstudio_join_progress_lookup` |
CPT 注册从主题挪到插件后,重写规则改变,必须在插件激活钩子 `flush_rewrite_rules()`,或手动访问一次“设置→固定链接”。
## 二、三个插件与函数清单
### 插件 1 `itstudio-content`(核心内容,地基,无依赖)
- news / announcement CPT(从 `itstudio_custom_post_types` 拆出)+ post_tag 绑定
- ACF `group_itstudio_content_priority`(权重,从 `itstudio_register_acf_fields` 拆出)
- 浏览量统计:`get_post_views`(1206)、`is_probably_bot_request`(1394)、view cookie 组(1436-1494)、`should_count_post_view`(1494)、`track_post_views`(1534)
- 助手:`get_post_weight`(1232)、`get_post_char_count`(1250)、`get_post_excerpt_chars`(1269)
- 归档 URL:`get_archive_url_by_post_type`(1289)、`get_archive_tag_filter_url`(1311)
- 归档扩展搜索:`archive_extended_search_enabled/join/where`(1337-1392)
- 日期精度显示:`expand_news_notice_date_precision`(155)
### 插件 2 `itstudio-services`(便民服务,独立,无依赖)
- service CPT + service_category(从 `itstudio_custom_post_types` 拆出)
- service meta key 助手(638-670)
- `get_service_category_i18n_labels`(670)、`get_service_i18n_content`(695)
- meta box:`add_service_meta_boxes`(745)、`render_service_link_meta_box`(757)、`save_service_meta`(847)
- 分类法字段:`service_category_add_fields`(914)、`edit_fields`(929)、`save_service_category_meta`(947)
- 前端读取:`get_service_target_url`(975)、`is_service_campus_only`(990)
- 后台列:`service_admin_columns`(999)、`service_admin_custom_column`(1006)
### 插件 3 `itstudio-recruitment`(招新系统,依赖插件1 + Formidable + ACF)
- 招新文章标记 meta:`get_recruitment_article_meta_key`(406)、`normalize_recruitment_flag`(410)、`is_recruitment_article`(423)、meta box 462/477/501
- ACF `group_itstudio_recruitment_article`(从 `itstudio_register_acf_fields` 拆出)
- 招新 feed:`join_get_recruitment_feed_items`(538)
- `is_join_page_context`(1562)
- 设置读写/清洗(1573-1748)、时间解析(1750-1811)、结果字段映射(1814-1848)
- CSV/XLSX 解析器(1863-2050)
- Formidable 耦合(2121-2427)
- 查询匹配/身份识别(2453-2678)
- 录取进度查询核心 `join_resolve_progress_lookup`(2693)
- 阶段状态机(2893-3336)
- AJAX 接口(3366 + 钩子 3402/3403)
- 强制 Formidable AJAX(3405 + 钩子 3418)
- 前端资源(3420 + 钩子 3450)
- 后台设置页(3452-3572 + 钩子 3463/3472/3483)
## 三、需手术拆开的函数
1. `itstudio_custom_post_types()`(231-306):news/announcement/post_tag → 插件1;service/service_category → 插件2。
2. `itstudio_register_acf_fields()`(308-403):`group_itstudio_content_priority` → 插件1;`group_itstudio_recruitment_article` → 插件3。
## 四、留在主题的部分(纯展示)
`itstudio_theme_setup`、评论禁用(21-60)、favicon/bootstrap 脚本(76-101)、`enqueue_scripts`(103)、`register_sidebars`(192)、`intro_body_class`(215)、`archive_document_title`(1040)、各 `*_fallback` 渲染(1052-1204、3907)。
**建议删除**:`itstudio_apply_site_identity()`(62-74)——每次强制覆写站点标题/副标题,反模式。
## 五、主题模板解耦
- 插件用 `add_shortcode` 暴露内容(如 `[itstudio_join_app]`、`[itstudio_services_grid]`),模板用 `shortcode_exists()` 判断。
- 助手函数调用处一律加 `function_exists()` 守卫,插件未启用时走降级,保证不白屏。
## 六、迁移顺序(按依赖,每步原子提交)
1. 建 `itstudio-content`(地基)→ 搬入 + 从主题删除 → 加模板守卫 → 刷新重写规则 → 验证。
2. 建 `itstudio-services`(独立)→ 同上。
3. 建 `itstudio-recruitment`(依赖插件1)→ 声明 `Requires Plugins` → 验证。
4. 收尾:清理 `functions.php`,删除 `apply_site_identity`。
关键:每个插件“搬入 + 从主题删除”是一次原子提交。绝不能让同一函数名/CPT 注册在主题和插件同时存在(PHP `Cannot redeclare` 致命错误)。是剪切,不是复制双跑。
## 七、迁移期间不崩策略
- 本地/staging 先做,别直上生产。
- 每个插件搬完过验证清单再 commit,可单独 revert。
- 生产上线:插件上传 + 启用 + 主题更新尽量同一操作。
- 上线后立刻刷一次固定链接。
## 八、验证清单(每插件搬完都跑)
- 新闻/公告归档正常、按作者名搜索有效、浏览量在涨
- 便民服务列表/分类/双语/仅校内标签正常,meta box 能存能读
- 加入我们页:报名表单能提交、进度查询返回正确阶段、招新设置能存能读、CSV/XLSX 解析正常
- 后台开 `WP_DEBUG` 无 PHP 错误
- 停用任一插件对应页面走降级不白屏
+40
View File
@@ -0,0 +1,40 @@
<footer class="site-footer">
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3 data-cn="友情链接" data-en="Links"></h3>
<ul class="footer-links">
<li><a href="https://www.ouc.edu.cn" target="_blank" rel="noopener" data-cn="中国海洋大学" data-en="Ocean University of China"></a></li>
<li><a href="https://it.ouc.edu.cn" target="_blank" rel="noopener" data-cn="信息科学与工程学部" data-en="Faculty of Information Science and Engineering"></a></li>
</ul>
</div>
<div class="footer-section">
<h3 data-cn="联系我们" data-en="Contact Us"></h3>
<ul class="footer-links">
<li><a href="mailto:contact@itstudio.club" target="_blank" rel="noopener">Email: Contact@itstudio.club</a></li>
<li><a href="https://github.com/ITStudioOUC/" target="_blank" rel="noopener">GitHub</a></li>
</ul>
</div>
<div class="footer-section footer-address">
<h3 data-cn="办公地址" data-en="Office Address"></h3>
<p class="footer-address-text">
<span
data-cn="中国海洋大学西海岸校区信息南楼五八创新创业工坊"
data-en="58 Workshop, South Information Building, West Coast Campus, Ocean University of China
"></span>
</p>
</div>
</div>
<div class="footer-bottom">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" data-cn="桂ICP备2022004639号-2" data-en="桂ICP备2022004639号-2"></a>
<p data-cn="&copy; <?php echo date('Y'); ?> 中国海洋大学爱特工作室. 保留所有权利." data-en="&copy; <?php echo date('Y'); ?> IT Studio, Ocean University of China. All rights reserved."></p>
</div>
</div>
</footer>
<?php wp_footer(); ?>
</body>
</html>
+412
View File
@@ -0,0 +1,412 @@
<?php
function itstudio_theme_setup() {
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('automatic-feed-links');
add_theme_support('html5', array(
'search-form',
'gallery',
'caption',
));
register_nav_menus(array(
'primary' => __('Primary Menu', 'itstudio'),
));
load_theme_textdomain('itstudio', get_template_directory() . '/languages');
}
add_action('after_setup_theme', 'itstudio_theme_setup');
add_filter('comments_open', '__return_false', 20, 2);
add_filter('pings_open', '__return_false', 20, 2);
add_filter('comments_array', '__return_empty_array', 10, 2);
function itstudio_disable_comments_post_types() {
$post_types = array('post', 'page', 'announcement', 'news', 'service');
foreach ($post_types as $post_type) {
if (post_type_supports($post_type, 'comments')) {
remove_post_type_support($post_type, 'comments');
}
if (post_type_supports($post_type, 'trackbacks')) {
remove_post_type_support($post_type, 'trackbacks');
}
}
}
add_action('init', 'itstudio_disable_comments_post_types', 100);
function itstudio_hide_comments_menu() {
remove_menu_page('edit-comments.php');
}
add_action('admin_menu', 'itstudio_hide_comments_menu', 999);
function itstudio_hide_admin_bar_comments() {
remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60);
}
add_action('init', 'itstudio_hide_admin_bar_comments');
function itstudio_redirect_comments_admin_pages() {
global $pagenow;
if ($pagenow === 'edit-comments.php' || $pagenow === 'comment.php') {
wp_safe_redirect(admin_url());
exit;
}
}
add_action('admin_init', 'itstudio_redirect_comments_admin_pages');
function itstudio_remove_comments_dashboard_widget() {
remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal');
}
add_action('wp_dashboard_setup', 'itstudio_remove_comments_dashboard_widget');
function itstudio_apply_site_identity() {
$site_name = '爱特工作室';
$site_tagline = '爱特工作室官方网站';
if (get_option('blogname') !== $site_name) {
update_option('blogname', $site_name);
}
if (get_option('blogdescription') !== $site_tagline) {
update_option('blogdescription', $site_tagline);
}
}
add_action('init', 'itstudio_apply_site_identity', 20);
function itstudio_output_favicon() {
$favicon_url = get_template_directory_uri() . '/resources/it_logo_2024.svg';
echo '<link rel="icon" href="' . esc_url($favicon_url) . '" type="image/svg+xml">' . "\n";
echo '<link rel="shortcut icon" href="' . esc_url($favicon_url) . '" type="image/svg+xml">' . "\n";
}
function itstudio_output_theme_bootstrap_script() {
echo '<meta name="color-scheme" content="dark light">' . "\n";
echo '<script>(function(){var d=document.documentElement;var t="light";try{var s=localStorage.getItem("theme");if(s==="dark"||s==="light"){t=s;}else if(window.matchMedia){t=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";}}catch(e){if(window.matchMedia){t=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";}}d.setAttribute("data-theme",t);d.style.colorScheme=t;})();</script>' . "\n";
}
function itstudio_output_lang_bootstrap_script() {
echo '<style id="itstudio-lang-boot-style">html.itstudio-lang-pending body{visibility:hidden;}</style>' . "\n";
echo '<script>(function(){var d=document.documentElement;var lang="zh";d.classList.add("itstudio-lang-pending");try{var s=localStorage.getItem("language");if(s==="zh"||s==="en"){lang=s;}}catch(e){}d.setAttribute("lang",lang);window.__ITSTUDIO_LANG__=lang;var apply=function(root){var scope=root||document;var txt=scope.querySelectorAll("[data-cn][data-en]");for(var i=0;i<txt.length;i++){var el=txt[i];var next=lang==="en"?el.getAttribute("data-en"):el.getAttribute("data-cn");if(!next){continue;}if(el.tagName==="INPUT"||el.tagName==="TEXTAREA"){var tp=(el.getAttribute("type")||"").toLowerCase();if(tp==="submit"||tp==="button"||tp==="reset"){el.value=next;continue;}}el.textContent=next;}var ph=scope.querySelectorAll("[data-cn-placeholder][data-en-placeholder]");for(var j=0;j<ph.length;j++){var ep=ph[j];var pk=lang==="en"?"data-en-placeholder":"data-cn-placeholder";var pv=ep.getAttribute(pk);if(pv){ep.setAttribute("placeholder",pv);}}var ar=scope.querySelectorAll("[data-cn-aria-label][data-en-aria-label]");for(var k=0;k<ar.length;k++){var ea=ar[k];var ak=lang==="en"?"data-en-aria-label":"data-cn-aria-label";var av=ea.getAttribute(ak);if(av){ea.setAttribute("aria-label",av);}}};var finish=function(){if(document.body){document.body.setAttribute("lang",lang);}d.classList.remove("itstudio-lang-pending");var st=document.getElementById("itstudio-lang-boot-style");if(st&&st.parentNode){st.parentNode.removeChild(st);}};var run=function(){apply(document);finish();};if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",run,{once:true});}else{run();}})();</script>' . "\n";
}
function itstudio_disable_default_site_icon() {
remove_action('wp_head', 'wp_site_icon', 99);
remove_action('admin_head', 'wp_site_icon', 99);
remove_action('login_head', 'wp_site_icon', 99);
}
add_action('init', 'itstudio_disable_default_site_icon');
add_action('wp_head', 'itstudio_output_theme_bootstrap_script', 0);
add_action('wp_head', 'itstudio_output_lang_bootstrap_script', 1);
add_action('wp_head', 'itstudio_output_favicon', 1);
add_action('admin_head', 'itstudio_output_favicon', 1);
add_action('login_head', 'itstudio_output_favicon', 1);
function itstudio_enqueue_scripts() {
// 基础样式 (Style.css)
wp_enqueue_style('itstudio-style', get_stylesheet_uri(), array(), '2.1.2');
wp_enqueue_style('itstudio-header', get_template_directory_uri() . '/assets/css/header.css', array('itstudio-style'), '2.1.2');
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');
// 仅在首页加载首页样式
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-landing-hero-canvas', get_template_directory_uri() . '/assets/js/landing-hero-canvas.js', array(), '1.0.0', true);
}
// 仅在工作室介绍页加载(包含 /about fallback)
$is_about = is_page('about') || is_page_template('page-about.php');
if (!$is_about && is_404()) {
global $wp;
$request = isset($wp->request) ? trim($wp->request, '/') : '';
$is_about = ($request === 'about');
}
if ($is_about) {
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-animejs', get_template_directory_uri() . '/assets/js/vendor/anime.min.js', array(), '3.2.2', true);
wp_enqueue_script('itstudio-about-hero-waves', get_template_directory_uri() . '/assets/js/hero-waves.js', array('itstudio-animejs'), '1.0.0', true);
wp_enqueue_script('itstudio-about-hero', get_template_directory_uri() . '/assets/js/home-hero.js', array('itstudio-animejs'), '1.0.0', true);
}
// 仅在便民服务页加载(包含 /services fallback)
$is_services = is_page('services') || is_page_template('page-services.php');
if (!$is_services && is_404()) {
global $wp;
$request = isset($wp->request) ? trim($wp->request, '/') : '';
$is_services = ($request === 'services');
}
if ($is_services) {
wp_enqueue_style('itstudio-services-page', get_template_directory_uri() . '/assets/css/services-page.css', array('itstudio-content'), '1.0.0');
}
// 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);
wp_enqueue_script('itstudio-main', get_template_directory_uri() . '/assets/js/main.js', array(), '1.0.0', true);
if (is_singular(array('post', 'announcement', 'news'))) {
wp_enqueue_script('itstudio-single-title-fit', get_template_directory_uri() . '/assets/js/single-title-fit.js', array(), '1.0.0', true);
}
}
add_action('wp_enqueue_scripts', 'itstudio_enqueue_scripts');
function itstudio_register_sidebars() {
register_sidebar(array(
'name' => __('Footer - Column 1', 'itstudio'),
'id' => 'footer-1',
'description' => __('Footer widget area 1', 'itstudio'),
'before_widget' => '<div class="footer-widget">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
));
register_sidebar(array(
'name' => __('Footer - Column 2', 'itstudio'),
'id' => 'footer-2',
'description' => __('Footer widget area 2', 'itstudio'),
'before_widget' => '<div class="footer-widget">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
));
}
add_action('widgets_init', 'itstudio_register_sidebars');
function itstudio_intro_body_class($classes) {
$is_about = is_page('about') || is_page_template('page-about.php');
if (!$is_about && is_404()) {
global $wp;
$request = isset($wp->request) ? trim($wp->request, '/') : '';
$is_about = ($request === 'about');
}
if ($is_about) {
$classes[] = 'intro-about';
}
return $classes;
}
add_filter('body_class', 'itstudio_intro_body_class');
function itstudio_archive_document_title($parts) {
if (is_post_type_archive('announcement')) {
$parts['title'] = '公告通知';
} elseif (is_post_type_archive('news')) {
$parts['title'] = '社团新闻';
}
return $parts;
}
add_filter('document_title_parts', 'itstudio_archive_document_title', 20);
// Fallback: render /about even if the page isn't created in WP admin.
function itstudio_about_fallback() {
if (!is_404()) {
return;
}
global $wp;
$request = isset($wp->request) ? trim($wp->request, '/') : '';
if ($request !== 'about') {
return;
}
$template = locate_template('page-about.php');
if ($template) {
global $wp_query;
if ($wp_query) {
$wp_query->is_404 = false;
$wp_query->is_page = true;
$wp_query->is_singular = true;
$virtual_post = new WP_Post((object) array(
'ID' => 0,
'post_type' => 'page',
'post_parent' => 0,
'post_title' => __('工作室介绍', 'itstudio'),
'post_status' => 'publish',
'post_name' => 'about',
'post_content' => '',
));
$wp_query->post = $virtual_post;
$wp_query->posts = array($virtual_post);
$wp_query->queried_object = $virtual_post;
$wp_query->queried_object_id = 0;
$wp_query->post_count = 1;
$wp_query->found_posts = 1;
$wp_query->max_num_pages = 1;
global $post;
$post = $virtual_post;
setup_postdata($post);
}
add_filter('document_title_parts', function ($parts) {
$parts['title'] = __('工作室介绍', 'itstudio');
return $parts;
});
status_header(200);
nocache_headers();
include $template;
exit;
}
}
add_action('template_redirect', 'itstudio_about_fallback');
// Fallback: render /news via archive.php even if the page isn't created in WP admin.
function itstudio_news_fallback() {
if (!is_404()) {
return;
}
global $wp;
$request = isset($wp->request) ? trim($wp->request, '/') : '';
if ($request !== 'news') {
return;
}
$template = locate_template('archive.php');
if ($template) {
global $wp_query;
if ($wp_query) {
$wp_query->is_404 = false;
$wp_query->is_page = true;
$wp_query->is_singular = true;
$virtual_post = new WP_Post((object) array(
'ID' => 0,
'post_type' => 'page',
'post_parent' => 0,
'post_title' => __('社团新闻', 'itstudio'),
'post_status' => 'publish',
'post_name' => 'news',
'post_content' => '',
));
$wp_query->post = $virtual_post;
$wp_query->posts = array($virtual_post);
$wp_query->queried_object = $virtual_post;
$wp_query->queried_object_id = 0;
$wp_query->post_count = 1;
$wp_query->found_posts = 1;
$wp_query->max_num_pages = 1;
global $post;
$post = $virtual_post;
setup_postdata($post);
}
add_filter('document_title_parts', function ($parts) {
$parts['title'] = __('社团新闻', 'itstudio');
return $parts;
});
$GLOBALS['itstudio_archive_mode'] = 'news';
status_header(200);
nocache_headers();
include $template;
unset($GLOBALS['itstudio_archive_mode']);
exit;
}
}
add_action('template_redirect', 'itstudio_news_fallback');
// Fallback: render /services even if the page isn't created in WP admin.
function itstudio_services_fallback() {
if (!is_404()) {
return;
}
global $wp;
$request = isset($wp->request) ? trim($wp->request, '/') : '';
if ($request !== 'services') {
return;
}
$template = locate_template('page-services.php');
if ($template) {
global $wp_query;
if ($wp_query) {
$wp_query->is_404 = false;
$wp_query->is_page = true;
$wp_query->is_singular = true;
$virtual_post = new WP_Post((object) array(
'ID' => 0,
'post_type' => 'page',
'post_parent' => 0,
'post_title' => __('便民服务', 'itstudio'),
'post_status' => 'publish',
'post_name' => 'services',
'post_content' => '',
));
$wp_query->post = $virtual_post;
$wp_query->posts = array($virtual_post);
$wp_query->queried_object = $virtual_post;
$wp_query->queried_object_id = 0;
$wp_query->post_count = 1;
$wp_query->found_posts = 1;
$wp_query->max_num_pages = 1;
global $post;
$post = $virtual_post;
setup_postdata($post);
}
add_filter('document_title_parts', function ($parts) {
$parts['title'] = __('便民服务', 'itstudio');
return $parts;
});
status_header(200);
nocache_headers();
include $template;
exit;
}
}
add_action('template_redirect', 'itstudio_services_fallback');
function itstudio_join_fallback() {
if (!is_404()) {
return;
}
global $wp;
$request = isset($wp->request) ? trim((string) $wp->request, '/') : '';
if ($request !== 'join') {
return;
}
$template = locate_template('page-join.php');
if (!$template) {
return;
}
global $wp_query;
if ($wp_query) {
$wp_query->is_404 = false;
$wp_query->is_page = true;
$wp_query->is_singular = true;
$virtual_post = new WP_Post((object) array(
'ID' => 0,
'post_type' => 'page',
'post_parent' => 0,
'post_title' => '加入我们',
'post_status' => 'publish',
'post_name' => 'join',
'post_content' => '',
));
$wp_query->post = $virtual_post;
$wp_query->posts = array($virtual_post);
$wp_query->queried_object = $virtual_post;
$wp_query->queried_object_id = 0;
$wp_query->post_count = 1;
$wp_query->found_posts = 1;
$wp_query->max_num_pages = 1;
global $post;
$post = $virtual_post;
setup_postdata($post);
}
add_filter('document_title_parts', static function ($parts) {
$parts['title'] = '加入我们';
return $parts;
});
status_header(200);
nocache_headers();
include $template;
exit;
}
add_action('template_redirect', 'itstudio_join_fallback', 9);
+138
View File
@@ -0,0 +1,138 @@
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<header class="site-header">
<div class="container">
<div class="header-content">
<div class="logo-container">
<a href="<?php echo esc_url(home_url('/')); ?>" class="logo-link">
<svg class="logo-main" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="260" height="70" viewBox="0 0 260 70">
<defs>
<style>
.cls-1 { fill: #f8d1d9; }
.cls-1, .cls-2, .cls-3 { fill-rule: evenodd; }
.cls-2 { fill: #bab5ec; }
.cls-3 { fill: url(#linear-gradient); }
.text-group {
font-family: "PingFang SC", "Microsoft YaHei", "Heiti SC", sans-serif;
text-anchor: middle;
}
.text-cn {
font-size: 26px;
font-weight: 800;
}
.text-en {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 11px;
font-weight: 600;
letter-spacing: 3px;
fill: #8e88c7;
text-transform: uppercase;
}
.deco-solid-circle { fill: #f1b7bf; }
.deco-hollow-circle { fill: none; stroke: #bab5ec; stroke-width: 2; }
.deco-plus { stroke: #bab5ec; stroke-width: 3; stroke-linecap: round; }
.deco-square { fill: #bab5ec; opacity: 0.8; }
.deco-line { stroke: #f8d1d9; stroke-width: 3; stroke-linecap: round; }
</style>
<linearGradient id="linear-gradient" x1="19.41" y1="66" x2="44.59" y2="12" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#bab5ec"/>
<stop offset="1" stop-color="#f1b7bf"/>
</linearGradient>
</defs>
<g id="decorations">
<rect class="deco-square" x="5" y="8" width="6" height="6" transform="rotate(15 8 11)" />
<path class="deco-plus" d="M78 20 L88 20 M83 15 L83 25" />
<circle class="deco-hollow-circle" cx="245" cy="15" r="5" />
<circle class="deco-solid-circle" cx="235" cy="55" r="4" />
<path class="deco-line" d="M250 60 L256 54" />
</g>
<g id="logo-main" transform="translate(12, 2) scale(0.95)">
<path id="cls-1" class="cls-1" d="M0,17L30,0V39L0,57V17Z"/>
<path id="cls-2" class="cls-2" d="M33,39L62,56V17L33,0V39Z"/>
<path id="cls-3" data-name="cls-3" class="cls-3" d="M18,28L46,12V49L18,66V28Z"/>
</g>
<g class="text-group" transform="translate(165, 0)">
<text y="38" class="text-cn" fill="currentColor">爱特工作室</text>
<text y="56" class="text-en">IT Studio</text>
</g>
</svg>
<img src="<?php echo get_template_directory_uri(); ?>/resources/it_logo_2024.svg" class="logo-mobile" alt="IT Studio">
</a>
</div>
<nav class="main-navigation">
<button class="mobile-menu-toggle" aria-label="Toggle Menu">
<span></span>
<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');
}
$news_nav_url = get_post_type_archive_link('news');
if (!$news_nav_url) {
$news_nav_url = home_url('/news');
}
$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');
}
$blog_nav_url = 'https://blog.itstudio.club';
?>
<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($announcement_nav_url); ?>" data-cn="公告通知" data-en="Announcements"></a></li>
<li><a href="<?php echo esc_url($news_nav_url); ?>" data-cn="社团新闻" data-en="News"></a></li>
<li><a href="<?php echo esc_url($blog_nav_url); ?>" target="_blank" rel="noopener noreferrer" 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>
</ul>
<div class="social-links">
<button class="lang-toggle" aria-label="Toggle Language">
<span class="lang-text">EN</span>
</button>
<a href="https://github.com/itstudio-2002" target="_blank" rel="noopener noreferrer" aria-label="GitHub">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
<a href="https://space.bilibili.com/3546373051910563" target="_blank" rel="noopener noreferrer" aria-label="Bilibili">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z"/>
</svg>
</a>
<button class="theme-toggle" aria-label="Toggle Theme">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
</div>
</nav>
</div>
</div>
</header>
+239
View File
@@ -0,0 +1,239 @@
<?php get_header(); ?>
<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');
}
$club_news_url = get_post_type_archive_link('news');
if (!$club_news_url) {
$club_news_url = home_url('/news');
}
?>
<section class="landing-hero">
<canvas class="landing-hero-canvas" aria-hidden="true"></canvas>
<div class="container">
<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>
</section>
<section class="services-section">
<div class="container">
<div class="services-provided">
<h2 data-cn="@ 服务提供" data-en="@ Services"></h2>
<div class="services-grid-box">
<div class="service-item service-resources">
<div class="service-icon">
<svg class="service-symbol" viewBox="0 0 72 72" fill="none" aria-hidden="true">
<circle class="svc-soft" cx="36" cy="36" r="24" />
<path class="svc-stroke" d="M17 24h14l4 5h20a4 4 0 0 1 4 4v22a4 4 0 0 1-4 4H17a4 4 0 0 1-4-4V28a4 4 0 0 1 4-4z" />
<rect class="svc-soft-strong" x="23" y="33" width="26" height="17" rx="2.6" />
<path class="svc-accent" d="M28 38h16M28 43h12M28 48h9" />
<path class="svc-muted" d="M51 35v13M51 48l3-3M51 48l-3-3" />
</svg>
</div>
<span data-cn="资源站" data-en="Resources"></span>
</div>
<div class="service-item service-mirror">
<div class="service-icon">
<svg class="service-symbol" viewBox="0 0 72 72" fill="none" aria-hidden="true">
<circle class="svc-soft" cx="36" cy="36" r="24" />
<circle class="svc-stroke" cx="36" cy="36" r="17" />
<path class="svc-muted" d="M19 36h34M36 19c5 4.7 7.8 10.5 7.8 17S41 48.3 36 53M36 19c-5 4.7-7.8 10.5-7.8 17S31 48.3 36 53" />
<path class="svc-accent" d="M50 29l7 7-7 7" />
<path class="svc-accent" d="M57 36H45" />
<path class="svc-accent" d="M22 43l-7-7 7-7" />
<path class="svc-accent" d="M15 36h12" />
</svg>
</div>
<span data-cn="校内镜像站" data-en="Mirror Site"></span>
</div>
<div class="service-item service-git">
<div class="service-icon">
<svg class="service-symbol" viewBox="0 0 72 72" fill="none" aria-hidden="true">
<circle class="svc-soft" cx="36" cy="36" r="24" />
<rect class="svc-stroke" x="12" y="19" width="30" height="30" rx="4" />
<path class="svc-muted" d="M17 27h20M17 33h14" />
<circle class="svc-dot" cx="49" cy="24" r="3" />
<circle class="svc-dot" cx="49" cy="37" r="3" />
<circle class="svc-dot" cx="56" cy="47" r="3" />
<path class="svc-accent" d="M41 24h8" />
<path class="svc-accent" d="M49 24v13" />
<path class="svc-accent" d="M49 37c4 0 7 3 7 7" />
</svg>
</div>
<span data-cn="代码托管" data-en="Git Hosting"></span>
</div>
<div class="service-item service-minecraft">
<div class="service-icon">
<svg class="service-symbol" viewBox="0 0 72 72" fill="none" aria-hidden="true">
<circle class="svc-soft" cx="36" cy="36" r="24" />
<path class="svc-stroke" d="M36 15l20 11v23L36 60 16 49V26l20-11z" />
<path class="svc-muted" d="M16 26l20 11 20-11M36 60V37" />
<path class="svc-accent" d="M20 23h32" />
<rect class="svc-soft-strong" x="24" y="40" width="6" height="6" rx="1.2" />
<rect class="svc-soft-strong" x="34" y="43" width="5" height="5" rx="1.1" />
<rect class="svc-dot" x="43" y="39" width="6" height="6" rx="1.2" />
</svg>
</div>
<span data-cn="Minecraft服务器" data-en="Minecraft Server"></span>
</div>
<div class="service-item service-forum">
<div class="service-icon">
<svg class="service-symbol" viewBox="0 0 72 72" fill="none" aria-hidden="true">
<circle class="svc-soft" cx="36" cy="36" r="24" />
<path class="svc-stroke" d="M11 28a6 6 0 0 1 6-6h22a6 6 0 0 1 6 6v13a6 6 0 0 1-6 6H28l-8 7v-7h-3a6 6 0 0 1-6-6V28z" />
<path class="svc-accent" d="M36 19a6 6 0 0 1 6-6h13a6 6 0 0 1 6 6v9a6 6 0 0 1-6 6h-5l-4 4v-4h-4a6 6 0 0 1-6-6v-9z" />
<circle class="svc-dot" cx="24" cy="35" r="1.8" />
<circle class="svc-dot" cx="31" cy="35" r="1.8" />
<circle class="svc-dot" cx="46" cy="24" r="1.7" />
<circle class="svc-dot" cx="52" cy="24" r="1.7" />
</svg>
</div>
<span data-cn="OUC论坛" data-en="OUC Forum"></span>
</div>
<div class="service-item service-repair">
<div class="service-icon">
<svg class="service-symbol" viewBox="0 0 72 72" fill="none" aria-hidden="true">
<circle class="svc-soft" cx="36" cy="36" r="24" />
<rect class="svc-stroke" x="11" y="18" width="37" height="25" rx="4" />
<path class="svc-muted" d="M29.5 43v6M22 49h15" />
<path class="svc-accent" d="M38 24.5a4.7 4.7 0 0 0-6.5 6.6l-7.2 7.2 3.4 3.4 7.2-7.2a4.7 4.7 0 0 0 6.6-6.5l-3 3-2.6-2.6 2.1-3z" />
<path class="svc-accent" d="M50 19l10 10" />
<circle class="svc-dot" cx="55" cy="24" r="2" />
</svg>
</div>
<span data-cn="电脑维修" data-en="PC Repair"></span>
</div>
<div class="service-item service-workshop">
<div class="service-icon">
<svg class="service-symbol" viewBox="0 0 72 72" fill="none" aria-hidden="true">
<circle class="svc-soft" cx="36" cy="36" r="24" />
<rect class="svc-stroke" x="12" y="17" width="48" height="36" rx="5" />
<path class="svc-muted" d="M12 30h48M24 12v10M48 12v10" />
<rect class="svc-soft-strong" x="24" y="36" width="24" height="13" rx="3" />
<path class="svc-accent" d="M29 42l4 4 9-9" />
<circle class="svc-dot" cx="22" cy="24" r="1.8" />
<circle class="svc-dot" cx="30" cy="24" r="1.8" />
</svg>
</div>
<span data-cn="五八工坊预约" data-en="Workshop Booking"></span>
</div>
<div class="service-item service-campus">
<div class="service-icon">
<svg class="service-symbol" viewBox="0 0 72 72" fill="none" aria-hidden="true">
<circle class="svc-soft" cx="36" cy="36" r="24" />
<path class="svc-stroke" d="M36 14l18 8v14c0 12-7 20-18 25-11-5-18-13-18-25V22l18-8z" />
<path class="svc-muted" d="M36 24c-4.5 0-8 3.5-8 8 0 5.5 8 13 8 13s8-7.5 8-13c0-4.5-3.5-8-8-8z" />
<circle class="svc-soft-strong" cx="36" cy="32" r="3.2" />
<circle class="svc-soft-strong" cx="52" cy="22" r="5.2" />
<path class="svc-accent" d="M49.5 22H54.5M52 19.5v5" />
</svg>
</div>
<span data-cn="OUC便民服务" data-en="OUC Services"></span>
</div>
</div>
</div>
</div>
</section>
<section class="landing-updates">
<div class="container">
<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',
'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, '...');
}
?>
<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 esc_attr(get_the_date('c')); ?>"><?php echo esc_html(get_the_date('Y-m-d H:i')); ?></time>
</div>
<p class="landing-feed-excerpt"><?php echo esc_html($announcement_excerpt); ?></p>
</li>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<li class="landing-feed-empty" data-cn="暂无公告" data-en="No announcements found."></li>
<?php endif; ?>
</ul>
</article>
<article class="landing-feed-box">
<header class="landing-feed-head">
<h2 data-cn="@ 社团新闻" data-en="@ Club News"></h2>
<a href="<?php echo esc_url($club_news_url); ?>" data-cn="更多" data-en="More"></a>
</header>
<ul class="landing-feed">
<?php
$blogs = new WP_Query(array(
'post_type' => 'news',
'post_status' => 'publish',
'posts_per_page' => 5,
'orderby' => 'date',
'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, '...');
}
?>
<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 esc_attr(get_the_date('c')); ?>"><?php echo esc_html(get_the_date('Y-m-d H:i')); ?></time>
</div>
<p class="landing-feed-excerpt"><?php echo esc_html($blog_excerpt); ?></p>
</li>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<li class="landing-feed-empty" data-cn="暂无社团新闻" data-en="No club news found."></li>
<?php endif; ?>
</ul>
</article>
</div>
</div>
</section>
</main>
<?php get_footer(); ?>
+45
View File
@@ -0,0 +1,45 @@
<?php
/* Template Name: IT Studio Introduction */
get_header();
?>
<main class="site-main">
<canvas class="hero-waves" aria-hidden="true"></canvas>
<section class="hero-section">
<button
type="button"
class="theme-toggle about-theme-toggle"
aria-label="Toggle Theme"
data-cn-aria-label="切换主题"
data-en-aria-label="Toggle Theme">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
<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>
</main>
<?php get_footer(); ?>
+432
View File
@@ -0,0 +1,432 @@
<?php
/* Template Name: Join Us */
get_header();
$join_runtime = function_exists('itstudio_join_get_runtime_data')
? itstudio_join_get_runtime_data()
: array();
$join_settings = isset($join_runtime['settings']) && is_array($join_runtime['settings'])
? $join_runtime['settings']
: array();
$join_stages = isset($join_runtime['stages']) && is_array($join_runtime['stages'])
? $join_runtime['stages']
: array();
$current_stage = isset($join_runtime['current_stage']) && is_array($join_runtime['current_stage'])
? $join_runtime['current_stage']
: array();
$current_stage_mode = isset($join_runtime['current_stage_mode']) ? (string) $join_runtime['current_stage_mode'] : 'inactive';
$is_registration_open = !empty($join_runtime['is_registration_open']);
$is_notice_open = !empty($join_runtime['is_notice_open']);
$is_notice_finished = !empty($join_runtime['is_notice_finished']);
$show_progress_visual = !empty($join_runtime['show_progress_visual']);
$is_progress_query_open = !empty($join_runtime['is_query_open']);
$current_label_cn = isset($current_stage['label_cn']) ? (string) $current_stage['label_cn'] : '当前未在招新时段';
$current_label_en = isset($current_stage['label_en']) ? (string) $current_stage['label_en'] : 'Recruitment is currently closed';
$current_range_cn = isset($current_stage['range_cn']) ? (string) $current_stage['range_cn'] : '请关注后续通知';
$current_range_en = isset($current_stage['range_en']) ? (string) $current_stage['range_en'] : 'Please check later updates';
$current_location_cn = trim((string) ($current_stage['location_cn'] ?? ''));
$current_location_en = trim((string) ($current_stage['location_en'] ?? ''));
if ($current_location_en === '') {
$current_location_en = $current_location_cn;
}
if ($current_location_cn === '') {
$current_location_cn = $current_location_en;
}
$current_stage_heading_cn = $current_stage_mode === 'next' ? '下一招新阶段' : '当前招新阶段';
$current_stage_heading_en = $current_stage_mode === 'next' ? 'Next Recruitment Stage' : 'Current Recruitment Stage';
$current_stage_photo_url = isset($join_runtime['current_stage_photo_url']) ? (string) $join_runtime['current_stage_photo_url'] : '';
if ($current_stage_photo_url === '') {
$current_stage_photo_url = get_template_directory_uri() . '/resources/it_logo_2024.svg';
}
$signup_shortcode = trim((string) ($join_settings['signup_form_shortcode'] ?? ''));
$has_formidable = shortcode_exists('formidable') || class_exists('FrmFormsController');
$progress_lookup = function_exists('itstudio_join_resolve_progress_lookup')
? itstudio_join_resolve_progress_lookup($join_runtime, $_GET)
: array(
'submitted' => false,
'has_query' => false,
'name' => '',
'qq' => '',
'email' => '',
'student_id' => '',
'message_cn' => '',
'message_en' => '',
'tone' => 'info',
);
$join_feed_data = function_exists('itstudio_join_get_recruitment_feed_items')
? itstudio_join_get_recruitment_feed_items($join_runtime, 5)
: array('display_year' => (int) wp_date('Y'), 'items' => array());
$join_feed_items = isset($join_feed_data['items']) && is_array($join_feed_data['items'])
? $join_feed_data['items']
: array();
$join_feed_items = array_values(array_filter($join_feed_items, static function ($item) {
if (!is_array($item)) {
return false;
}
$title = trim((string) ($item['title'] ?? ''));
$url = trim((string) ($item['url'] ?? ''));
return ($title !== '' && $url !== '');
}));
$join_form_status = '';
if (isset($_GET['join_form_status']) && is_string($_GET['join_form_status'])) {
$join_form_status = strtolower(trim((string) wp_unslash($_GET['join_form_status'])));
}
if ($join_form_status !== 'submitted' && $join_form_status !== 'draft') {
$join_form_status = (isset($_GET['join_form_submitted']) && (string) $_GET['join_form_submitted'] === '1') ? 'submitted' : '';
}
$is_post_request = isset($_SERVER['REQUEST_METHOD']) && strtoupper((string) $_SERVER['REQUEST_METHOD']) === 'POST';
$post_form_status = 'submitted';
if ($is_post_request && function_exists('itstudio_join_detect_form_submission_status')) {
$post_form_status = itstudio_join_detect_form_submission_status($_POST);
}
?>
<main class="site-main join-page">
<div class="container">
<header class="join-head">
<h1 class="join-title" data-cn="加入我们" data-en="Join Us">加入我们</h1>
</header>
<?php if ($join_form_status === 'submitted' || $join_form_status === 'draft') : ?>
<?php
$notice_cn = $join_form_status === 'draft' ? '草稿已保存' : '你的报名表单已提交,感谢报名';
$notice_en = $join_form_status === 'draft' ? 'Draft saved.' : 'Your registration form has been submitted. Thank you for applying.';
?>
<p class="join-submit-notice" data-cn="<?php echo esc_attr($notice_cn); ?>" data-en="<?php echo esc_attr($notice_en); ?>"><?php echo esc_html($notice_cn); ?></p>
<script>
(function () {
if (!window.history || !window.history.replaceState) {
return;
}
var url = new URL(window.location.href);
if (!url.searchParams.has('join_form_status') && !url.searchParams.has('join_form_submitted')) {
return;
}
url.searchParams.delete('join_form_status');
url.searchParams.delete('join_form_submitted');
var nextUrl = url.pathname + (url.search ? url.search : '') + url.hash;
window.history.replaceState({}, document.title, nextUrl);
})();
</script>
<?php endif; ?>
<section class="join-hero">
<div class="join-canvas-shell">
<div class="join-stage-photo-frame">
<img
class="join-stage-photo"
src="<?php echo esc_url($current_stage_photo_url); ?>"
alt="<?php echo esc_attr($current_label_cn); ?>"
loading="lazy"
>
<div class="join-canvas-overlay">
<p class="join-current-label" data-cn="<?php echo esc_attr($current_stage_heading_cn); ?>" data-en="<?php echo esc_attr($current_stage_heading_en); ?>"><?php echo esc_html($current_stage_heading_cn); ?></p>
<h2
id="joinCurrentStage"
class="join-current-stage"
data-cn="<?php echo esc_attr($current_label_cn); ?>"
data-en="<?php echo esc_attr($current_label_en); ?>"
>
<?php echo esc_html($current_label_cn); ?>
</h2>
<p
id="joinCurrentRange"
class="join-current-range"
data-cn="<?php echo esc_attr($current_range_cn); ?>"
data-en="<?php echo esc_attr($current_range_en); ?>"
>
<?php echo esc_html($current_range_cn); ?>
</p>
<?php if ($current_location_cn !== '' || $current_location_en !== '') : ?>
<?php
$current_location_line_cn = '地点:' . $current_location_cn;
$current_location_line_en = 'Location: ' . $current_location_en;
?>
<p
id="joinCurrentLocation"
class="join-current-location"
data-cn="<?php echo esc_attr($current_location_line_cn); ?>"
data-en="<?php echo esc_attr($current_location_line_en); ?>"
>
<?php echo esc_html($current_location_line_cn); ?>
</p>
<?php endif; ?>
</div>
</div>
<div class="join-wave-layer">
<canvas id="joinProgressCanvas" class="join-progress-canvas" aria-hidden="true"></canvas>
<?php if ($show_progress_visual) : ?>
<div class="join-wave-progress" aria-hidden="true">
<div id="joinWaveTrack" class="join-wave-track">
<span id="joinWaveFill" class="join-wave-fill"></span>
<span id="joinWaveMarks" class="join-wave-marks"></span>
<span id="joinWaveBoat" class="join-wave-boat">
<span class="join-wave-boat-icon">
<svg viewBox="0 0 120 64" role="img" aria-hidden="true" focusable="false">
<path class="boat-sail-main" d="M54 12L54 41L74 41L54 12Z"></path>
<path class="boat-sail-side" d="M54 14L40 39L54 39L54 14Z"></path>
<path class="boat-mast" d="M53 10H56V44H53Z"></path>
<path class="boat-hull" d="M16 42H104C99 53 88 59 72 60H48C32 59 21 53 16 42Z"></path>
<circle class="boat-porthole" cx="50" cy="50" r="2.2"></circle>
<circle class="boat-porthole" cx="61" cy="51" r="2.2"></circle>
<circle class="boat-porthole" cx="72" cy="50" r="2.2"></circle>
</svg>
</span>
</span>
</div>
</div>
<?php endif; ?>
</div>
</div>
<ol class="join-stage-list">
<?php foreach ($join_stages as $stage) : ?>
<?php
$stage_status = isset($stage['status']) ? (string) $stage['status'] : 'pending';
if ($is_notice_finished) {
// 录取结果公布结束后,所有阶段统一显示为“已结束”。
$stage_status = 'completed';
}
$status_cn = '待设置';
$status_en = 'Pending';
if ($stage_status === 'completed') {
$status_cn = '已结束';
$status_en = 'Ended';
} elseif ($stage_status === 'active') {
$status_cn = '进行中';
$status_en = 'In Progress';
} elseif ($stage_status === 'upcoming') {
$status_cn = '未开始';
$status_en = 'Upcoming';
}
$stage_key = (string) ($stage['key'] ?? '');
$stage_result_uploaded = !empty($stage['result_uploaded']);
$stage_location_cn = trim((string) ($stage['location_cn'] ?? ''));
$stage_location_en = trim((string) ($stage['location_en'] ?? ''));
if ($stage_location_en === '') {
$stage_location_en = $stage_location_cn;
}
if ($stage_location_cn === '') {
$stage_location_cn = $stage_location_en;
}
$is_current_stage = !empty($current_stage['key']) && !empty($stage['key']) && ((string) $current_stage['key'] === (string) $stage['key']);
$is_public_notice_stage = ($stage_key === 'public_notice');
$is_mid_stage_query_ready = in_array($stage_key, array('first_interview', 'assessment', 'second_interview'), true)
&& $stage_status === 'completed'
&& $stage_result_uploaded;
$is_query_ready_status = false;
$is_waiting_notice_status = false;
if (!$is_notice_finished && $is_mid_stage_query_ready) {
$status_cn = '可查询结果';
$status_en = 'Query Available';
$is_query_ready_status = true;
}
if (!$is_notice_finished && $is_public_notice_stage && in_array($stage_status, array('active', 'completed'), true)) {
if ($stage_result_uploaded) {
$status_cn = '可查询结果';
$status_en = 'Query Available';
$is_query_ready_status = true;
} else {
$status_cn = '请耐心等待通知';
$status_en = 'Please wait for notice';
$is_query_ready_status = false;
$is_waiting_notice_status = true;
}
}
?>
<li class="join-stage-item is-<?php echo esc_attr($stage_status); ?><?php echo $is_current_stage ? ' is-current' : ''; ?><?php echo $is_waiting_notice_status ? ' is-waiting-notice' : ''; ?>">
<div class="join-stage-title-row">
<h3
class="join-stage-name"
data-cn="<?php echo esc_attr((string) ($stage['label_cn'] ?? '')); ?>"
data-en="<?php echo esc_attr((string) ($stage['label_en'] ?? '')); ?>"
>
<?php echo esc_html((string) ($stage['label_cn'] ?? '')); ?>
</h3>
<span
class="join-stage-status<?php echo $is_query_ready_status ? ' is-query-ready' : ''; ?><?php echo $is_waiting_notice_status ? ' is-waiting-notice' : ''; ?>"
data-cn="<?php echo esc_attr($status_cn); ?>"
data-en="<?php echo esc_attr($status_en); ?>"
>
<?php echo esc_html($status_cn); ?>
</span>
</div>
<p
class="join-stage-time"
data-cn="<?php echo esc_attr((string) ($stage['range_cn'] ?? '')); ?>"
data-en="<?php echo esc_attr((string) ($stage['range_en'] ?? '')); ?>"
>
<?php echo esc_html((string) ($stage['range_cn'] ?? '')); ?>
</p>
<?php if ($stage_location_cn !== '' || $stage_location_en !== '') : ?>
<?php
$stage_location_line_cn = '地点:' . $stage_location_cn;
$stage_location_line_en = 'Location: ' . $stage_location_en;
?>
<p
class="join-stage-location"
data-cn="<?php echo esc_attr($stage_location_line_cn); ?>"
data-en="<?php echo esc_attr($stage_location_line_en); ?>"
>
<?php echo esc_html($stage_location_line_cn); ?>
</p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</section>
<?php
$show_registration_form = $is_registration_open;
$show_progress_query_form = $is_progress_query_open;
$visible_form_count = ($show_registration_form ? 1 : 0) + ($show_progress_query_form ? 1 : 0);
?>
<section class="join-info-layout">
<div class="join-info-main<?php echo $visible_form_count === 0 ? ' is-empty' : ''; ?>">
<?php if ($show_registration_form) : ?>
<h2 class="join-panel-title" data-cn="报名表单" data-en="Registration Form">报名表单</h2>
<article class="join-form-card">
<div class="join-form-content">
<?php if (!$has_formidable) : ?>
<p class="join-form-tip" data-cn="未检测到 Formidable Forms 插件,请先启用插件" data-en="Formidable Forms is not active.">未检测到 Formidable Forms 插件,请先启用插件</p>
<?php elseif ($signup_shortcode === '') : ?>
<p class="join-form-tip" data-cn="请在 设置 > 招新设置 中填写报名表单 Shortcode" data-en="Please configure the registration form shortcode in Settings > Recruitment Settings.">请在 设置 > 招新设置 中填写报名表单 Shortcode</p>
<?php else : ?>
<?php echo do_shortcode($signup_shortcode); ?>
<?php endif; ?>
</div>
</article>
<?php endif; ?>
<?php if ($show_progress_query_form) : ?>
<h2 class="join-panel-title" data-cn="录取进度查询" data-en="Admission Progress Lookup">录取进度查询</h2>
<article class="join-form-card">
<div class="join-form-content">
<form method="get" class="join-progress-query-form">
<div class="join-progress-query-grid">
<label class="join-progress-query-field">
<input
type="text"
name="join_query_identity"
value="<?php echo esc_attr((string) ($progress_lookup['identity'] ?? '')); ?>"
data-cn-placeholder="请输入中文姓名 / QQ / 邮箱 / 学号"
data-en-placeholder="Enter Name / QQ / Email / Student ID"
placeholder="请输入中文姓名 / QQ / 邮箱 / 学号"
>
</label>
</div>
<div class="join-progress-query-actions">
<button type="submit" name="join_progress_lookup" value="1" class="join-progress-query-submit" data-cn="查询录取进度" data-en="Check Progress">查询录取进度</button>
</div>
</form>
<?php if (!empty($progress_lookup['submitted'])) : ?>
<?php
$lookup_tone = trim((string) ($progress_lookup['tone'] ?? 'info'));
if (!in_array($lookup_tone, array('success', 'warning', 'error', 'info'), true)) {
$lookup_tone = 'info';
}
?>
<p
class="join-progress-query-feedback is-<?php echo esc_attr($lookup_tone); ?>"
data-cn="<?php echo esc_attr((string) ($progress_lookup['message_cn'] ?? '')); ?>"
data-en="<?php echo esc_attr((string) ($progress_lookup['message_en'] ?? '')); ?>"
>
<?php echo esc_html((string) ($progress_lookup['message_cn'] ?? '')); ?>
</p>
<?php endif; ?>
</div>
</article>
<?php endif; ?>
<?php if ($visible_form_count === 0) : ?>
<article class="join-form-card join-form-card-placeholder">
<div class="join-form-content">
<p class="join-form-tip" data-cn="当前阶段暂无可操作表单" data-en="No available form in the current stage.">当前阶段暂无可操作表单</p>
</div>
</article>
<?php endif; ?>
</div>
<aside class="join-info-side">
<h2 class="join-panel-title" data-cn="招新资讯" data-en="Related News">招新资讯</h2>
<section class="join-news-strip" aria-label="Related News">
<?php if (!empty($join_feed_items)) : ?>
<div class="join-news-strip-track">
<?php foreach ($join_feed_items as $feed_item) : ?>
<?php
$feed_title = isset($feed_item['title']) ? (string) $feed_item['title'] : '';
$feed_excerpt = isset($feed_item['excerpt']) ? (string) $feed_item['excerpt'] : '';
$feed_url = isset($feed_item['url']) ? (string) $feed_item['url'] : '';
$feed_author = isset($feed_item['author']) ? (string) $feed_item['author'] : '';
$feed_date = isset($feed_item['date']) ? (string) $feed_item['date'] : '';
$feed_date_iso = isset($feed_item['date_iso']) ? (string) $feed_item['date_iso'] : '';
?>
<article class="join-news-item">
<h3 class="join-news-item-title">
<a href="<?php echo esc_url($feed_url); ?>"><?php echo esc_html($feed_title); ?></a>
</h3>
<div class="join-news-item-meta">
<?php if ($feed_author !== '') : ?>
<span class="join-news-item-meta-author">
<span data-cn="发布者" data-en="Author">发布者</span>
<?php echo esc_html($feed_author); ?>
</span>
<?php endif; ?>
<?php if ($feed_date !== '') : ?>
<time class="join-news-item-meta-time" datetime="<?php echo esc_attr($feed_date_iso !== '' ? $feed_date_iso : $feed_date); ?>">
<span data-cn="时间" data-en="Time">时间</span>
<?php echo esc_html($feed_date); ?>
</time>
<?php endif; ?>
</div>
<p class="join-news-item-excerpt"><?php echo esc_html($feed_excerpt); ?></p>
</article>
<?php endforeach; ?>
</div>
<?php else : ?>
<p class="join-news-strip-empty" data-cn="暂无可展示的招新新闻通告" data-en="No recruitment updates available yet.">暂无可展示的招新新闻通告</p>
<?php endif; ?>
</section>
</aside>
</section>
<?php if ($is_post_request) : ?>
<script>
(function () {
var hasSuccessMessage = document.querySelector('.frm_message, .frm_success_style, .frm_success');
if (!hasSuccessMessage) {
return;
}
try {
var url = new URL(window.location.href);
['join_form_status', 'join_form_submitted', 'frm_action', 'frm_state', 'frm_data', 'frm_page_order', 'frm_test', 'frm_nonce'].forEach(function (key) {
url.searchParams.delete(key);
});
var status = <?php echo wp_json_encode($post_form_status); ?>;
if (status !== 'draft' && status !== 'submitted') {
status = 'submitted';
}
var successText = (hasSuccessMessage.textContent || '').toLowerCase();
if (successText.indexOf('draft') !== -1 || successText.indexOf('草稿') !== -1) {
status = 'draft';
}
url.searchParams.set('join_form_status', status);
var nextUrl = url.pathname + (url.search ? url.search : '') + url.hash;
window.location.replace(nextUrl);
} catch (error) {
// keep current page if URL parsing fails
}
})();
</script>
<?php endif; ?>
</div>
</main>
<?php get_footer(); ?>
+200
View File
@@ -0,0 +1,200 @@
<?php get_header(); ?>
<?php
$default_icon_url = get_template_directory_uri() . '/resources/it_logo_2024.svg';
$services_query = new WP_Query(array(
'post_type' => 'service',
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => array(
'menu_order' => 'ASC',
'date' => 'DESC',
),
));
$grouped_services = array();
$uncategorized_items = array();
if ($services_query->have_posts()) {
while ($services_query->have_posts()) {
$services_query->the_post();
$service_id = get_the_ID();
$service_terms = get_the_terms($service_id, 'service_category');
$service_url = function_exists('itstudio_get_service_target_url') ? itstudio_get_service_target_url($service_id) : get_permalink($service_id);
$service_url = is_string($service_url) ? trim($service_url) : '';
if ($service_url === '') {
$service_url = '#';
}
$i18n_content = function_exists('itstudio_get_service_i18n_content')
? itstudio_get_service_i18n_content($service_id, 90)
: array(
'title_cn' => get_the_title($service_id),
'title_en' => get_the_title($service_id),
'excerpt_cn' => function_exists('itstudio_get_post_excerpt_chars')
? itstudio_get_post_excerpt_chars($service_id, 90)
: wp_html_excerpt(wp_strip_all_tags(get_the_excerpt() ?: get_the_content()), 90, '...'),
'excerpt_en' => function_exists('itstudio_get_post_excerpt_chars')
? itstudio_get_post_excerpt_chars($service_id, 90)
: wp_html_excerpt(wp_strip_all_tags(get_the_excerpt() ?: get_the_content()), 90, '...'),
);
$item = array(
'id' => $service_id,
'title_cn' => $i18n_content['title_cn'],
'title_en' => $i18n_content['title_en'],
'url' => $service_url,
'excerpt_cn' => $i18n_content['excerpt_cn'],
'excerpt_en' => $i18n_content['excerpt_en'],
'has_thumb' => has_post_thumbnail($service_id),
'is_campus_only' => function_exists('itstudio_is_service_campus_only') ? itstudio_is_service_campus_only($service_id) : false,
'term_name_cn' => '',
'term_name_en' => '',
);
if (!empty($service_terms) && !is_wp_error($service_terms)) {
$primary_term = $service_terms[0];
$term_id = (int) $primary_term->term_id;
$term_labels = function_exists('itstudio_get_service_category_i18n_labels')
? itstudio_get_service_category_i18n_labels($primary_term)
: array('cn' => $primary_term->name, 'en' => $primary_term->name);
$item['term_name_cn'] = $term_labels['cn'];
$item['term_name_en'] = $term_labels['en'];
if (!isset($grouped_services[$term_id])) {
$grouped_services[$term_id] = array(
'term' => $primary_term,
'items' => array(),
);
}
$grouped_services[$term_id]['items'][] = $item;
} else {
$item['term_name_cn'] = '未分类';
$item['term_name_en'] = 'Uncategorized';
$uncategorized_items[] = $item;
}
}
wp_reset_postdata();
}
$sections = array();
if (!empty($grouped_services)) {
$ordered_terms = get_terms(array(
'taxonomy' => 'service_category',
'hide_empty' => true,
));
if (!is_wp_error($ordered_terms) && !empty($ordered_terms)) {
foreach ($ordered_terms as $term) {
$term_id = (int) $term->term_id;
if (!isset($grouped_services[$term_id])) {
continue;
}
$term_labels = function_exists('itstudio_get_service_category_i18n_labels')
? itstudio_get_service_category_i18n_labels($term)
: array('cn' => $term->name, 'en' => $term->name);
$sections[] = array(
'slug' => sanitize_title($term->slug),
'name_cn' => $term_labels['cn'],
'name_en' => $term_labels['en'],
'items' => $grouped_services[$term_id]['items'],
);
}
} else {
foreach ($grouped_services as $group) {
$term_labels = function_exists('itstudio_get_service_category_i18n_labels')
? itstudio_get_service_category_i18n_labels($group['term'])
: array('cn' => $group['term']->name, 'en' => $group['term']->name);
$sections[] = array(
'slug' => sanitize_title($group['term']->slug),
'name_cn' => $term_labels['cn'],
'name_en' => $term_labels['en'],
'items' => $group['items'],
);
}
}
}
if (!empty($uncategorized_items)) {
$sections[] = array(
'slug' => 'uncategorized',
'name_cn' => '未分类',
'name_en' => 'Uncategorized',
'items' => $uncategorized_items,
);
}
?>
<main class="site-main services-directory-page">
<div class="container">
<header class="services-directory-head">
<h1 class="services-directory-title" data-cn="便民服务" data-en="Service">便民服务</h1>
</header>
<?php if (!empty($sections)) : ?>
<div class="services-directory-layout">
<aside class="services-directory-sidebar">
<div class="services-directory-nav-box">
<h2 class="services-directory-nav-title" data-cn="分类" data-en="Categories">分类</h2>
<ul class="services-directory-nav-list">
<?php foreach ($sections as $section) : ?>
<?php $count = count($section['items']); ?>
<li>
<a href="#service-section-<?php echo esc_attr($section['slug']); ?>">
<span class="services-directory-nav-name" data-cn="<?php echo esc_attr($section['name_cn']); ?>" data-en="<?php echo esc_attr($section['name_en']); ?>"><?php echo esc_html($section['name_cn']); ?></span>
<span class="services-directory-nav-count"><?php echo esc_html($count); ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
</aside>
<div class="services-directory-content">
<?php foreach ($sections as $section) : ?>
<section class="services-directory-group" id="service-section-<?php echo esc_attr($section['slug']); ?>">
<header class="services-directory-group-head">
<h2 data-cn="<?php echo esc_attr($section['name_cn']); ?>" data-en="<?php echo esc_attr($section['name_en']); ?>"><?php echo esc_html($section['name_cn']); ?></h2>
</header>
<div class="services-directory-grid">
<?php foreach ($section['items'] as $item) : ?>
<?php $card_classes = 'services-directory-card' . (!empty($item['is_campus_only']) ? ' is-campus-only' : ''); ?>
<a class="<?php echo esc_attr($card_classes); ?>" href="<?php echo esc_url($item['url']); ?>" target="_blank" rel="noopener noreferrer">
<?php if (!empty($item['is_campus_only'])) : ?>
<span class="services-directory-access services-directory-access-campus" data-cn="仅校内访问" data-en="Campus-only">仅校内访问</span>
<?php endif; ?>
<div class="services-directory-card-head">
<div class="services-directory-icon-wrap">
<?php if ($item['has_thumb']) : ?>
<?php echo get_the_post_thumbnail($item['id'], 'medium', array('class' => 'services-directory-icon')); ?>
<?php else : ?>
<img class="services-directory-icon services-directory-icon-fallback" src="<?php echo esc_url($default_icon_url); ?>" alt="<?php echo esc_attr($item['title_cn']); ?>">
<?php endif; ?>
</div>
<div class="services-directory-meta">
<h3 class="services-directory-name" data-cn="<?php echo esc_attr($item['title_cn']); ?>" data-en="<?php echo esc_attr($item['title_en']); ?>"><?php echo esc_html($item['title_cn']); ?></h3>
<span class="services-directory-category" data-cn="<?php echo esc_attr($item['term_name_cn']); ?>" data-en="<?php echo esc_attr($item['term_name_en']); ?>"><?php echo esc_html($item['term_name_cn']); ?></span>
</div>
</div>
<p class="services-directory-desc" data-cn="<?php echo esc_attr($item['excerpt_cn']); ?>" data-en="<?php echo esc_attr($item['excerpt_en']); ?>"><?php echo esc_html($item['excerpt_cn']); ?></p>
</a>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
</div>
<?php else : ?>
<section class="services-directory-empty">
<h2 data-cn="暂无便民服务" data-en="No services yet">暂无便民服务</h2>
<p data-cn="请先在后台“便民服务”中添加服务条目。" data-en="Please add services in wp-admin first.">请先在后台“便民服务”中添加服务条目。</p>
<?php if (current_user_can('edit_posts')) : ?>
<a class="services-directory-empty-btn" href="<?php echo esc_url(admin_url('post-new.php?post_type=service')); ?>" target="_blank" rel="noopener noreferrer" data-cn="前往新增服务" data-en="Create a service">前往新增服务</a>
<?php endif; ?>
</section>
<?php endif; ?>
</div>
</main>
<?php get_footer(); ?>
+28
View File
@@ -0,0 +1,28 @@
<?php get_header(); ?>
<main class="site-main page-content">
<div class="container">
<?php
while (have_posts()) :
the_post();
?>
<article id="page-<?php the_ID(); ?>" <?php post_class(); ?>>
<header class="entry-header">
<h1 class="entry-title"><?php the_title(); ?></h1>
</header>
<?php if (has_post_thumbnail()) : ?>
<div class="entry-thumbnail">
<?php the_post_thumbnail('large'); ?>
</div>
<?php endif; ?>
<div class="entry-content">
<?php the_content(); ?>
</div>
</article>
<?php endwhile; ?>
</div>
</main>
<?php get_footer(); ?>
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="153" height="18" role="img" aria-label="Organization: ITstudio">
<title>Organization: ITStudio</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#fff" stop-opacity=".7"/>
<stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
<stop offset=".9" stop-color="#000" stop-opacity=".3"/>
<stop offset="1" stop-color="#000" stop-opacity=".5"/>
</linearGradient><clipPath id="r"><rect width="153" height="18" rx="4" fill="#fff"/>
</clipPath><g clip-path="url(#r)"><rect width="98" height="18" fill="#555"/>
<rect x="98" width="55" height="18" fill="#f8d1d9"/><rect width="153" height="18" fill="url(#s)"/></g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
<image x="5" y="2" width="14" height="14" href="data:image/svg+xml;base64,PHN2ZyBpZD0ibG9nbyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjYyIiBoZWlnaHQ9IjY2IiB2aWV3Qm94PSIwIDAgNjIgNjYiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICNmOGQxZDk7CiAgICAgIH0KCiAgICAgIC5jbHMtMSwgLmNscy0yLCAuY2xzLTMgewogICAgICAgIGZpbGwtcnVsZTogZXZlbm9kZDsKICAgICAgfQoKICAgICAgLmNscy0yIHsKICAgICAgICBmaWxsOiAjYmFiNWVjOwogICAgICB9CgogICAgICAuY2xzLTMgewogICAgICAgIGZpbGw6IHVybCgjbGluZWFyLWdyYWRpZW50KTsKICAgICAgfQogICAgPC9zdHlsZT4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iMTkuNDEiIHkxPSI2NiIgeDI9IjQ0LjU5IiB5Mj0iMTIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjYmFiNWVjIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2YxYjdiZiIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHBhdGggaWQ9ImNscy0xIiBjbGFzcz0iY2xzLTEiIGQ9Ik0wLDE3TDMwLDBWMzlMMCw1N1YxN1oiLz4KICA8cGF0aCBpZD0iY2xzLTIiIGNsYXNzPSJjbHMtMiIgZD0iTTMzLDM5TDYyLDU2VjE3TDMzLDBWMzlaIi8+CiAgPHBhdGggaWQ9ImNscy0zIiBkYXRhLW5hbWU9ImNscy0zIiBjbGFzcz0iY2xzLTMiIGQ9Ik0xOCwyOEw0NiwxMlY0OUwxOCw2NlYyOFoiLz4KPC9zdmc+Cg=="/><text aria-hidden="true" x="585" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="710">Organization</text><text x="585" y="130" transform="scale(.1)" fill="#fff" textLength="710">Organization</text><text aria-hidden="true" x="1245" y="140" fill="#ccc" fill-opacity=".3" transform="scale(.1)" textLength="450">ITStudio</text><text x="1245" y="130" transform="scale(.1)" fill="#333" textLength="450">ITStudio</text></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="260" height="70" viewBox="0 0 260 70">
<defs>
<style>
/* 原始Logo样式 */
.cls-1 { fill: #f8d1d9; }
.cls-1, .cls-2, .cls-3 { fill-rule: evenodd; }
.cls-2 { fill: #bab5ec; }
.cls-3 { fill: url(#linear-gradient); }
/* 文字样式 */
.text-group {
font-family: "PingFang SC", "Microsoft YaHei", "Heiti SC", sans-serif;
text-anchor: middle;
}
.text-cn {
font-size: 26px;
font-weight: 800;
fill: currentColor;
}
.text-en {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 11px;
font-weight: 600;
letter-spacing: 3px;
fill: #8e88c7;
text-transform: uppercase;
}
/* 装饰元素样式 */
.deco-solid-circle { fill: #f1b7bf; }
.deco-hollow-circle { fill: none; stroke: #bab5ec; stroke-width: 2; }
.deco-plus { stroke: #bab5ec; stroke-width: 3; stroke-linecap: round; }
.deco-square { fill: #bab5ec; opacity: 0.8; }
.deco-line { stroke: #f8d1d9; stroke-width: 3; stroke-linecap: round; }
</style>
<linearGradient id="linear-gradient" x1="19.41" y1="66" x2="44.59" y2="12" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#bab5ec"/>
<stop offset="1" stop-color="#f1b7bf"/>
</linearGradient>
</defs>
<!-- 装饰元素组 -->
<g id="decorations">
<!-- 左上角:实心小方块 (已修复坐标,向右下方移动) -->
<rect class="deco-square" x="5" y="8" width="6" height="6" transform="rotate(15 8 11)" />
<!-- Logo与文字之间:加粗的十字星 -->
<path class="deco-plus" d="M78 20 L88 20 M83 15 L83 25" />
<!-- 右上角:空心圆环 -->
<circle class="deco-hollow-circle" cx="245" cy="15" r="5" />
<!-- 右下角:实心圆点 -->
<circle class="deco-solid-circle" cx="235" cy="55" r="4" />
<!-- 底部装饰线 -->
<path class="deco-line" d="M250 60 L256 54" />
</g>
<!-- 原Logo主体 -->
<g id="logo-main" transform="translate(12, 2) scale(0.95)">
<path id="cls-1" class="cls-1" d="M0,17L30,0V39L0,57V17Z"/>
<path id="cls-2" class="cls-2" d="M33,39L62,56V17L33,0V39Z"/>
<path id="cls-3" data-name="cls-3" class="cls-3" d="M18,28L46,12V49L18,66V28Z"/>
</g>
<!-- 文字部分 -->
<g class="text-group" transform="translate(165, 0)">
<text y="38" id="logo-text-cn" class="text-cn" style="font-family: 'PingFang SC', 'Microsoft YaHei', 'Heiti SC', sans-serif; text-anchor: middle; font-size: 26px; font-weight: 800; fill: currentColor;">爱特工作室</text>
<text y="56" class="text-en" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-anchor: middle; font-size: 11px; font-weight: 600; letter-spacing: 3px; fill: #8e88c7; text-transform: uppercase;">IT Studio</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1,28 @@
<svg id="logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="62" height="66" viewBox="0 0 62 66">
<defs>
<style>
.cls-1 {
fill: #f8d1d9;
}
.cls-1, .cls-2, .cls-3 {
fill-rule: evenodd;
}
.cls-2 {
fill: #bab5ec;
}
.cls-3 {
fill: url(#linear-gradient);
}
</style>
<linearGradient id="linear-gradient" x1="19.41" y1="66" x2="44.59" y2="12" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#bab5ec"/>
<stop offset="1" stop-color="#f1b7bf"/>
</linearGradient>
</defs>
<path id="cls-1" class="cls-1" d="M0,17L30,0V39L0,57V17Z"/>
<path id="cls-2" class="cls-2" d="M33,39L62,56V17L33,0V39Z"/>
<path id="cls-3" data-name="cls-3" class="cls-3" d="M18,28L46,12V49L18,66V28Z"/>
</svg>

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

@@ -0,0 +1,43 @@
<svg width="88" height="88" viewBox="0 0 88 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="44" cy="44" r="44" fill="white"/>
<mask id="mask0_316_79" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="88" height="88">
<path d="M0 0H88V87.9993H0V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_316_79)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.79467 43.9965C2.79467 21.2765 21.2803 2.7937 43.9961 2.7937C66.7175 2.7937 85.2031 21.2765 85.2031 43.9965C85.2031 66.7165 66.7175 85.2021 43.9961 85.2021C21.2803 85.2021 2.79467 66.7165 2.79467 43.9965ZM-0.00146484 43.9965C-0.00146484 68.2602 19.7366 87.9982 43.9961 87.9982C68.2612 87.9982 87.9992 68.2602 87.9992 43.9965C87.9992 19.7356 68.2612 -0.00244141 43.9961 -0.00244141C19.7366 -0.00244141 -0.00146484 19.7356 -0.00146484 43.9965Z" fill="#004098"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5901 30.8249L11.2669 30.2729L11.9535 28.6197L13.2767 29.1731L12.5901 30.8249ZM12.5471 33.8541L12.4237 33.8208L13.0964 32.2064L14.6026 32.8319L15.4584 33.2244L15.8953 32.1662L15.0173 31.8361L13.5111 31.2091L14.2004 29.5545L14.3211 29.6169L14.7441 28.5989L13.8384 28.2521L12.181 27.56L11.4417 27.2244L11.1407 28.0316L10.3654 29.8984L9.90216 29.7057L8.99924 29.2937L8.55957 30.3506L9.48746 30.7029L9.94793 30.8957L9.19342 32.7154L8.84251 33.4741L9.60119 33.7639L11.2572 34.4547L12.1352 34.8416L12.5471 33.8541ZM10.1768 32.8888L10.8495 31.2688L12.1726 31.825L11.5013 33.4394L10.1768 32.8888Z" fill="#E60012"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.9366 17.9076L21.5399 14.73L24.9755 18.6288L21.3749 21.8064L17.9366 17.9076ZM17.214 18.504L20.7008 22.4527L21.2847 23.1545L22.0032 22.5207L21.9352 22.4374L25.5358 19.2599L25.644 19.3833L26.3777 18.737L25.7522 18.0699L22.2334 14.0768L21.6911 13.4221L21.1183 13.963L17.2459 17.3764L16.6328 17.8826L17.214 18.504ZM21.171 21.3723L21.6772 20.8979L24.006 18.8452L24.5732 18.3736L24.0184 17.7425L23.5857 18.1517C23.5885 17.9561 23.5954 17.7744 23.601 17.6122L23.3305 17.5927L22.9449 17.5803C22.8853 17.5747 22.7937 17.5664 22.6717 17.547L23.1377 17.1822L22.5635 16.5317L22.189 16.8785L21.4498 17.5331L20.9408 16.9561L21.7202 16.271L22.2598 15.841L21.68 15.1822L21.1405 15.6857L18.9255 17.6399L18.4026 18.0782L18.9712 18.7231L19.4678 18.2543L20.2334 17.5803L20.7438 18.1572L20.1308 18.6982L19.6079 19.1351L20.1765 19.78L20.6745 19.3112L21.2847 18.7717L21.9838 19.5623L21.1571 20.2973L20.6162 20.7412L21.171 21.3723ZM22.6939 18.9395L21.9935 18.1489L22.404 17.7828C22.3888 17.9964 22.3555 18.1864 22.3028 18.3486C22.6648 18.3403 22.895 18.3486 22.9962 18.3681C23.1017 18.3903 23.196 18.4083 23.2736 18.4249L22.6939 18.9395Z" fill="#E60012"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.2245 9.21561C32.4034 9.31824 32.5588 9.41117 32.6905 9.48884C32.7335 9.37788 32.7932 9.21838 32.8722 9.01033C32.9471 8.80645 32.9998 8.66637 33.0262 8.59286C32.5713 8.35152 32.1163 8.14348 31.6586 7.95901C31.5712 8.24334 31.4423 8.52212 31.2661 8.78981C31.7238 8.96873 32.0456 9.11297 32.2245 9.21561ZM33.0941 15.0312C33.2134 15.1158 33.3216 15.1893 33.4215 15.2614L33.5144 14.8453C33.5865 14.5277 33.6517 14.0978 33.7113 13.561L33.8015 12.6539C33.8071 12.5943 33.814 12.5041 33.8306 12.3765L33.6032 12.3072C33.5629 12.2961 33.3299 12.1976 32.8986 12.0062L32.9069 12.2572C32.9125 12.364 32.9 12.5582 32.8722 12.837C32.8445 13.1185 32.8154 13.3474 32.7849 13.5222C32.7557 13.7011 32.7127 13.8855 32.6683 14.0811C32.6226 14.2753 32.5851 14.4112 32.5574 14.4875C32.5283 14.5624 32.4977 14.6414 32.4617 14.7233C32.7627 14.8453 32.9721 14.948 33.0941 15.0312ZM32.2079 11.2822C32.3701 11.3585 32.5934 11.4778 32.8722 11.6428L32.9527 11.364L33.126 10.9146C33.1427 10.8758 33.1649 10.8134 33.194 10.7274C32.9804 10.6303 32.531 10.4348 31.8375 10.1449C31.764 10.4126 31.7044 10.597 31.6503 10.6969C31.6018 10.7981 31.5477 10.9008 31.4908 11.0034C31.8043 11.1144 32.04 11.2073 32.2079 11.2822ZM36.9541 10.2988C36.5963 10.0658 36.3424 9.90909 36.1954 9.81755L37.4132 9.50548L37.5935 10.364L36.6975 10.5915L36.9541 10.2988ZM37.0359 12.3904C37.1788 12.2364 37.3092 12.1061 37.4284 12.0034C37.0761 11.7593 36.8126 11.5887 36.6406 11.4805L37.7752 11.1934L37.8529 11.5665C37.8806 11.7316 37.9167 11.9271 37.9486 12.1602L37.0359 12.3904ZM35.0387 10.1102L35.9166 9.8869C35.8334 10.0006 35.6892 10.1587 35.4811 10.3502C35.6268 10.4209 35.8431 10.5513 36.1302 10.7357L35.1524 10.984L35.0387 10.1102ZM33.2966 10.1338C33.4034 10.2073 33.5199 10.2905 33.6434 10.389L33.7987 10.1255C33.8334 10.0686 33.9069 9.91464 34.0179 9.66499L34.0914 10.0533C34.0914 10.0658 34.115 10.2045 34.1566 10.4611C34.1968 10.7246 34.2204 10.8841 34.2287 10.941C34.237 11.0006 34.2467 11.0936 34.2592 11.2115L33.4575 11.3987L33.6739 12.2572L34.3868 12.0547L34.4215 12.4389C34.4326 12.586 34.4464 12.7801 34.4562 13.0242C34.47 13.2683 34.4756 13.4362 34.4783 13.5222C34.4811 13.6123 34.4811 13.7427 34.4811 13.9105L35.2634 13.6775L38.0193 12.9785L38.0026 13.095C37.9971 13.1893 37.9028 13.2767 37.7266 13.3543C37.5505 13.4389 37.4284 13.4847 37.3605 13.4986C37.2953 13.5125 37.0761 13.5471 36.71 13.6012C36.9291 13.8342 37.0678 14.0076 37.1275 14.1158C37.1871 14.224 37.244 14.3377 37.2953 14.4598C37.9056 14.2864 38.2981 14.1324 38.4825 13.9882C38.6642 13.8481 38.7835 13.7011 38.8459 13.5527C38.9125 13.4015 38.9486 13.2572 38.9583 13.1185C38.9666 12.9785 38.9721 12.8536 38.9832 12.7344L39.6254 12.5915L39.4174 11.7649L38.8154 11.941L38.7669 11.6622L38.6906 11.2558C38.6767 11.1962 38.6628 11.0977 38.642 10.9715L39.3549 10.8134L39.1372 9.95764L38.4479 10.1477L38.3438 9.61089C38.3355 9.56235 38.3008 9.40562 38.2454 9.13932L38.1108 8.45971L37.2412 8.71214L34.8029 9.32934L34.0775 9.49161C34.1371 9.31824 34.1968 9.14487 34.2564 8.96873L37.3854 8.17538L38.4118 7.94514L38.1927 7.08105L37.1788 7.36816L34.5061 8.045C34.5602 7.86053 34.6059 7.66358 34.6462 7.44999C34.3646 7.46802 34.0317 7.44444 33.6462 7.3848C33.6309 7.61227 33.6032 7.81754 33.5713 7.99923C33.5352 8.18092 33.4756 8.43058 33.3923 8.73849C33.3077 9.05056 33.2134 9.28912 33.1025 9.45139C32.9887 9.61089 32.8861 9.75237 32.7876 9.87165C33.0206 9.97151 33.1885 10.0575 33.2966 10.1338ZM35.2592 11.8328L36.2772 11.5721L36.201 11.6373L35.9791 11.8439C35.9471 11.873 35.8972 11.9077 35.8376 11.9618C36.1594 12.1491 36.3563 12.2711 36.4229 12.328C36.4964 12.3821 36.5671 12.4362 36.6351 12.493L35.3424 12.8217L35.2592 11.8328Z" fill="#E60012"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.3041 11.7235L51.3846 11.9857L51.6023 11.098L50.5566 10.8428L49.8132 10.6805L50.0281 9.79424L50.7161 9.98148L51.8076 10.2478L52.0212 9.36982L50.5857 9.02031L49.8894 8.87051L50.1016 8.00088L50.7785 8.18812L51.472 8.35595C51.4123 8.0203 51.3097 7.65414 51.1655 7.25053C51.5344 7.18673 51.8367 7.11877 52.0698 7.05913C52.2112 7.50018 52.3388 7.92599 52.4498 8.33792C52.2958 8.37537 52.1266 8.42114 51.9533 8.47245L53.4956 8.84694C53.3569 8.74153 53.2085 8.63473 53.0531 8.52377C53.2223 8.35872 53.368 8.20199 53.4845 8.05497C53.6038 7.91212 53.7369 7.73597 53.8881 7.51683C54.1377 7.71933 54.3971 7.92321 54.6662 8.12294C54.3402 8.50019 54.1613 8.70269 54.135 8.7332L53.9533 8.95789L54.6218 9.12017L55.3375 9.27412L55.1239 10.1632L54.4567 9.98148L52.9616 9.61809L52.7535 10.4752L53.7369 10.718L54.4734 10.8733L54.2598 11.761L53.6301 11.5904L52.5469 11.3255L52.3305 12.2145L53.8534 12.5821L54.5649 12.736L54.3486 13.6362L53.6648 13.4461L52.1225 13.0758L51.9408 13.8178L51.7286 14.804L50.7327 14.5599L50.9976 13.5876L51.1765 12.8428L50.1044 12.5821L49.0448 12.3435L49.2639 11.4475L50.3041 11.7235ZM48.7008 7.0619C48.9893 6.88021 49.2487 6.69019 49.4803 6.49463C49.8506 6.93707 50.1391 7.29491 50.3416 7.57647C50.293 7.62501 50.0531 7.85525 49.6301 8.26718C49.5247 8.11739 49.2154 7.71378 48.7008 7.0619ZM47.4054 12.1244C47.5524 11.951 47.7258 11.718 47.931 11.4336C48.1349 11.1465 48.2667 10.951 48.3319 10.8428L48.6079 10.3879C48.7868 10.6126 49.0045 10.8511 49.2528 11.1119C48.9796 11.5446 48.741 11.908 48.5385 12.2006C48.3346 12.4905 48.1419 12.761 47.9643 13.0106C47.7854 13.2561 47.6148 13.4739 47.4511 13.6556C47.2861 13.2755 47.1543 13.0162 47.0531 12.8719L46.8853 12.6306C47.0836 12.4683 47.257 12.2977 47.4054 12.1244ZM47.766 8.98702C48.1418 8.67911 48.3887 8.47523 48.4997 8.37814C48.7688 8.68466 49.0683 9.06053 49.4095 9.51268C49.0254 9.8622 48.7868 10.0841 48.7008 10.1743C48.619 10.0494 48.533 9.93016 48.4428 9.80811C48.3568 9.68883 48.1294 9.4156 47.766 8.98702Z" fill="#E60012"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.9051 17.4531C61.2809 17.6292 61.7095 17.7832 62.1977 17.9219C62.6818 18.0578 63.145 18.0675 63.5778 17.9454C64.0091 17.8317 64.3531 17.6944 64.6111 17.5294C64.8677 17.3671 65.091 17.1826 65.2851 16.9704L65.6541 16.5862C64.8496 15.8955 64.3226 15.4614 64.0715 15.2783L63.3073 14.7083L63.8982 13.969L64.6568 14.6056L66.2449 15.8789C66.5889 15.4614 66.79 15.2103 66.8621 15.1174L67.4044 14.4087C67.772 14.761 67.9675 14.9413 67.9939 14.9579L68.2408 15.1562C68.0161 15.4128 67.894 15.5571 67.8691 15.5862L67.0674 16.5404L68.7068 17.8511L69.4918 18.4503L68.8885 19.2034L68.1326 18.5668L66.4932 17.2547C66.385 17.399 66.3046 17.5155 66.2546 17.607C66.2033 17.7027 66.1381 17.8622 66.0632 18.0897C65.9814 18.3144 65.9218 18.564 65.8788 18.8317C65.8385 19.0952 65.8302 19.4961 65.8566 20.0287C65.8871 20.5682 65.998 20.9885 66.1894 21.2922C66.3795 21.5973 66.5806 21.8761 66.7803 22.1341C66.6776 22.166 66.5639 22.2076 66.4474 22.2562C66.3323 22.3075 66.1853 22.3949 66.0091 22.5211L65.7567 22.6889C65.6402 22.5003 65.4807 22.2076 65.2768 21.8193C65.0715 21.4323 64.9412 20.9399 64.8843 20.3435C64.8274 19.7527 64.8219 19.3491 64.8677 19.141L64.98 18.4933C64.7706 18.6015 64.557 18.7013 64.3434 18.7887C64.1284 18.8775 63.8246 18.9427 63.4238 18.9843C63.0271 19.0217 62.568 18.9968 62.0618 18.9052C61.5486 18.8123 61.1159 18.7152 60.7622 18.6154C60.7913 18.5183 60.8191 18.4115 60.8454 18.295C60.8704 18.184 60.8912 17.9025 60.9051 17.4531Z" fill="#E60012"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M78.781 28.7823C78.2747 29.2996 77.9571 29.6242 77.8198 29.7573L77.4287 30.1235C77.1693 29.7975 76.9308 29.5326 76.7088 29.3274C76.8545 29.2206 76.9793 29.129 77.0875 29.0472C77.1915 28.9695 77.3205 28.8558 77.4731 28.7088C77.6242 28.5631 77.8226 28.3412 78.0653 28.0458C78.2803 28.319 78.5188 28.5631 78.781 28.7823ZM74.2414 26.8918L74.842 26.645L75.3357 26.4203L75.6478 27.2594L75.8767 27.8211C76.2775 27.3953 76.559 27.0208 76.7283 26.7046L76.9307 26.3176C77.2608 26.5936 77.4592 26.7448 77.5216 26.7753L77.774 26.8974C77.4564 27.43 77.1915 27.8308 76.9793 28.1082L76.7255 28.4383C76.4522 28.2192 76.1749 28.0292 75.8933 27.8641L77.2289 31.1277C77.2692 30.9446 77.2941 30.7171 77.3094 30.4425C77.5133 30.4994 77.6825 30.5437 77.8198 30.5701C77.9571 30.6006 78.136 30.6186 78.3552 30.6325C78.5785 30.6464 78.7408 30.6519 78.8434 30.6436L79.3469 30.6048C79.3385 30.9792 79.358 31.3579 79.4093 31.7351C79.2317 31.7268 79.0819 31.7185 78.9488 31.713C78.8156 31.7019 78.5965 31.6811 78.2844 31.645C77.971 31.6103 77.7657 31.5812 77.67 31.559L77.3801 31.4966L77.6104 32.0625L77.9987 32.9293L77.4564 33.136L76.7713 33.4175L76.2622 33.6422L75.8767 32.6963L76.4925 32.4689L76.7823 32.3468L74.8753 27.688L74.5382 27.8267L74.0126 28.0611L73.6284 27.1165L74.2414 26.8918ZM74.5008 29.9959C74.4495 30.2122 74.3815 30.4314 74.2955 30.6602C74.4897 30.7629 74.6852 30.8544 74.885 30.9335L74.5008 29.9959ZM71.8655 27.6574L72.7476 27.2982L73.0514 28.1262L73.7338 29.7948C73.7698 29.7143 73.799 29.6269 73.8239 29.5326C74.0514 29.706 74.2705 29.8447 74.4814 29.9529L74.1845 29.2178L73.8392 28.455L74.6783 28.1138L74.9613 28.8905L76.1776 31.8711L75.842 32.3607C75.7601 32.3385 75.6492 32.3038 75.5119 32.258C75.3718 32.2123 75.0486 32.0444 74.5355 31.7546L75.1679 33.2927L75.5188 34.0777L74.6422 34.4411L74.3219 33.5937L73.5687 31.7462C73.4439 31.8295 73.3413 31.8946 73.2608 31.9376C73.1818 31.9848 73.0597 32.0444 72.8947 32.1221C72.7324 32.1984 72.5451 32.2386 72.3371 32.2469C72.1346 32.2525 71.9432 32.1679 71.7726 31.9876C71.6034 31.8031 71.4744 31.6297 71.3967 31.4675C71.3121 31.3038 71.2275 31.1013 71.1401 30.86C71.2566 30.7892 71.3551 30.724 71.4425 30.6644C71.5271 30.6048 71.5992 30.5437 71.6672 30.4702L71.9141 30.2012C71.9793 30.3773 72.0278 30.5271 72.068 30.638C72.1041 30.749 72.1346 30.8308 72.1582 30.8794C72.2358 31.0583 72.3371 31.1526 72.4508 31.1554C72.5646 31.1582 72.6727 31.1415 72.7754 31.1041C72.921 31.0417 73.0167 30.9876 73.0597 30.9335L73.179 30.7948L72.222 28.455L71.8655 27.6574Z" fill="#E60012"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.54196 44.1092C7.19104 44.2424 5.59325 44.8873 5.75275 46.5392C5.90671 48.0801 7.45318 48.5115 8.98024 48.3617C10.6918 48.1966 11.9511 47.3714 11.8097 45.9345C11.6779 44.5724 10.4158 43.9275 8.54196 44.1092ZM8.66262 45.0218C9.52393 44.9386 10.6973 45.0329 10.7972 46.0385C10.8998 47.1217 9.57941 47.3783 8.91783 47.4463C7.22294 47.6113 6.8124 46.9026 6.7694 46.4338C6.71809 45.8998 7.07731 45.5475 7.41019 45.3686C7.63488 45.241 8.00104 45.0842 8.66262 45.0218Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.86882 53.3497C8.40141 53.358 6.9326 53.3816 6.61915 51.9738C6.35978 50.806 7.31541 49.7546 9.10738 49.3621C10.7024 49.0043 12.2614 49.2969 12.572 50.7006C12.6261 50.9419 12.726 51.5536 12.2059 52.1638C11.9645 52.4454 11.6511 52.6312 11.2697 52.8018L10.805 52.0029C11.4084 51.7574 11.665 51.3261 11.5679 50.8795C11.3279 49.8087 9.75509 50.1583 9.38616 50.2401C8.84662 50.3594 8.31958 50.5272 7.99086 50.7893C7.70376 51.0168 7.52761 51.358 7.61638 51.7436C7.74259 52.3233 8.38615 52.4579 9.03664 52.426L8.86882 53.3497Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1052 53.2678L14.2162 56.3455L13.2842 56.6867L12.47 54.4412L11.2425 54.885L11.9943 56.9738L11.0608 57.3095L10.3063 55.2207L8.80562 55.7658L9.64335 58.0876L8.70853 58.4232L7.56982 55.2692L13.1052 53.2678Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4641 63.4699L15.686 58.9996L15.2131 58.1799L9.25049 59.6265L9.72483 60.449L11.0619 60.0995L11.944 61.6307L10.9787 62.6252L11.4641 63.4699ZM12.0605 59.8401L14.2575 59.2687L12.6611 60.8859L12.0605 59.8401Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9083 60.9609L17.4353 61.6433L15.4381 65.4742L15.4436 65.4825L18.5463 63.0691L19.0525 63.7182L14.4062 67.3382L13.8555 66.6337L15.8015 62.8791L15.7973 62.8708L12.7654 65.23L12.2563 64.5768L16.9083 60.9609Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.1639 67.7172L21.1846 70.2124C20.7505 70.7588 20.5716 70.9835 20.4842 71.272C20.3552 71.7075 20.566 71.9974 20.7769 72.1652C21.444 72.6937 21.8906 72.1333 22.5106 71.3469L24.5286 68.8032L25.2263 69.3566L23.3053 71.7755C22.6521 72.6035 22.2651 73.0404 21.8171 73.254C21.1181 73.5924 20.5494 73.2873 20.1624 72.9808C18.745 71.8559 19.6715 70.6923 20.5161 69.6271L22.4676 67.168L23.1639 67.7172Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.0766 69.9441L26.8186 70.3852L26.3443 74.6848L26.3526 74.6875L28.3706 71.3116L29.0808 71.7347L26.0613 76.7916L25.2943 76.3339L25.7492 72.1327L25.7368 72.1244L23.7659 75.424L23.0571 74.9996L26.0766 69.9441Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.0008 72.1599L30.8024 72.5386L28.2782 77.859L27.4751 77.4776L30.0008 72.1599Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.2817 72.8208L32.1888 73.1537L31.72 77.6294L31.7338 77.6322L34.2512 73.9123L35.1361 74.2397L31.6229 79.2189L30.7241 78.8888L31.2817 72.8208Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.6903 74.3757L38.879 75.1122L38.657 76.0803L36.3297 75.5436L36.0343 76.8168L38.1966 77.3203L37.9719 78.287L35.8096 77.7863L35.449 79.3411L37.854 79.8987L37.6265 80.8695L34.3574 80.1081L35.6903 74.3757Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.1577 81.1239L40.044 81.2155L40.3047 78.7702L40.4795 78.7911C41.0759 78.8535 41.1189 78.9728 41.5419 80.0837L42.0454 81.4305L43.105 81.5414L42.6931 80.3417C42.3519 79.3528 42.1993 79.1225 41.8637 78.8049V78.7883C42.0398 78.7841 42.2104 78.7342 42.3727 78.6593C42.8276 78.4665 43.1286 77.9297 43.1938 77.2904C43.2562 76.7217 43.1175 76.0254 42.6362 75.7175C42.5086 75.6468 42.3034 75.5358 41.6446 75.4651L39.7805 75.2695L39.1577 81.1239ZM40.5613 76.3514L41.252 76.4207C41.8443 76.4887 41.9594 76.4998 42.1106 76.6454C42.259 76.7952 42.3006 77.031 42.2756 77.2695C42.2562 77.4401 42.1841 77.8132 41.8637 77.9214C41.7014 77.9755 41.3269 77.9381 41.0593 77.9089L40.4004 77.841L40.5613 76.3514Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.125 79.4982C45.1901 79.8019 45.358 80.6147 46.1541 80.5634C46.7796 80.5273 46.9974 80.0724 46.9724 79.6965C46.9447 79.2028 46.6257 79.1168 45.799 78.9018C45.3524 78.7853 44.358 78.5259 44.2775 77.2277C44.2359 76.5883 44.4883 75.5675 45.7838 75.4857C46.6451 75.4316 47.4579 75.8324 47.5799 77.1598L46.6909 77.2666C46.634 76.9628 46.5341 76.4177 45.8295 76.4621C45.566 76.4801 45.0903 76.6161 45.1222 77.0932C45.1444 77.4718 45.4315 77.6188 46.1541 77.7936C46.8393 77.967 47.7685 78.1958 47.8628 79.6424C47.9031 80.354 47.6063 80.9739 47.2387 81.2291C46.8587 81.4996 46.4148 81.544 46.211 81.5537C45.7394 81.587 44.5674 81.6036 44.272 79.6577L45.125 79.4982Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.4219 75.3193L49.2998 75.1833L50.1986 81.0059L49.3234 81.1404L48.4219 75.3193Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.524 75.7551L50.2535 76.0644L50.0205 75.0991L53.42 74.2683L53.6558 75.235L52.3881 75.5443L53.5448 80.2975L52.6835 80.5097L51.524 75.7551Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.3598 76.8496L53.6538 74.2171L54.6288 73.8509L56.4139 75.6679L56.5609 73.1325L57.5193 72.7747L57.1975 76.5458L58.0561 78.8565L57.2239 79.1686L56.3598 76.8496Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.944 73.3058C63.7082 74.4251 65.0411 75.5167 66.4142 74.5846C67.693 73.7094 67.3268 72.1435 66.46 70.8731C65.4933 69.457 64.1687 68.7468 62.9731 69.5596C61.8414 70.3308 61.8816 71.7482 62.944 73.3058ZM63.686 72.7607C63.1992 72.0464 62.7193 70.9702 63.5529 70.3987C64.4503 69.7871 65.3116 70.8217 65.6888 71.3738C66.65 72.7774 66.2214 73.4764 65.8345 73.7413C65.3906 74.0451 64.908 73.8967 64.5903 73.6928C64.3684 73.5582 64.0605 73.3086 63.686 72.7607Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M65.4087 67.9072L67.6667 65.8892L68.3297 66.6312L66.7319 68.0556L67.6639 69.0958L69.0426 67.8698L69.7083 68.609L68.3255 69.8434L69.9913 71.7075L69.3269 72.2984L65.4087 67.9072Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.1593 61.5354C76.495 61.8627 77.5435 62.8932 76.7599 64.1041C76.108 65.111 74.6933 65.1651 73.1524 64.1679C71.7793 63.2774 70.8958 61.964 71.6739 60.7573C71.8084 60.5492 72.1718 60.0485 72.9762 59.9889C73.3452 59.9639 73.6975 60.0569 74.0872 60.208L73.8431 61.1026C73.2453 60.8474 72.7543 60.9639 72.5074 61.3454C71.911 62.2663 73.2633 63.1443 73.5837 63.3509C74.0498 63.6505 74.5366 63.9085 74.9554 63.9598C75.3174 64.0028 75.685 63.8863 75.9 63.5562C76.2218 63.0611 75.8667 62.5049 75.384 62.0638L76.1593 61.5354Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M73.0742 58.6532L73.4126 57.8293L75.5583 58.7101L76.2199 57.0956L74.077 56.2149L74.4126 55.3938L79.8607 57.6351L79.5209 58.4521L77.1408 57.4743L76.4792 59.0887L78.8607 60.0693L78.5223 60.8904L73.0742 58.6532Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6221 54.4415L74.8565 53.5857L80.5361 55.1405L80.3003 55.9963L74.6221 54.4415Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.1455 52.5583L75.3106 51.7108L79.5117 50.7053L79.5145 50.6942L75.6573 49.9383L75.8168 49.1255L81.5949 50.2586L81.4216 51.1366L77.3147 52.1269L77.312 52.1408L81.0804 52.8773L80.9236 53.6872L75.1455 52.5583Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.23 44.678L76.1952 45.8458L76.0981 46.7834L81.7847 49.0913L81.879 48.1454L80.5919 47.6503L80.7681 45.8916L82.1342 45.6475L82.23 44.678ZM79.628 47.2786L77.5129 46.4602L79.75 46.076L79.628 47.2786Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8247 56.2804C18.858 56.3567 18.9024 56.4288 18.937 56.5051C19.091 56.8061 19.2491 57.1043 19.4142 57.3997C19.4696 57.5051 19.5307 57.6078 19.5889 57.7104C19.7609 58.0114 19.9398 58.3068 20.1201 58.605C20.1562 58.6619 20.1853 58.7159 20.2242 58.77C20.2242 58.77 20.2297 58.7839 20.2366 58.7936C20.4378 59.1112 20.6458 59.4233 20.8594 59.7284C25.2187 55.8144 30.9843 53.4302 37.3061 53.4302C40.2451 53.4302 43.0704 53.9323 45.6821 54.8768C45.7015 54.8851 45.7209 54.8907 45.7389 54.8962C45.7611 54.9073 45.7847 54.9184 45.8097 54.9226C49.4158 56.2402 53.3076 56.9573 57.3659 56.9573C61.5421 56.9573 65.5394 56.1958 69.2287 54.8088C69.2634 54.7312 69.2994 54.6577 69.3313 54.5758C69.3452 54.5384 69.3619 54.4982 69.3771 54.4607C69.4784 54.2166 69.5727 53.967 69.667 53.7145C69.7086 53.6008 69.7571 53.4871 69.7988 53.3706C69.7988 53.3678 69.8001 53.3622 69.8001 53.3622C69.9028 53.071 69.9985 52.7811 70.0928 52.494C70.1302 52.3719 70.1718 52.2582 70.2065 52.1445C70.2981 51.8463 70.3799 51.5453 70.4603 51.2415C70.4936 51.125 70.5255 51.0113 70.556 50.892C70.6337 50.5883 70.7044 50.2831 70.7752 49.9711C70.8001 49.8573 70.8265 49.7408 70.8487 49.6243C70.9139 49.3109 70.9707 48.996 71.0248 48.684C71.0359 48.6146 71.0484 48.5439 71.0595 48.4731C66.5713 51.9142 60.961 53.9586 54.8708 53.9586C50.7487 53.9586 46.8471 53.0238 43.363 51.3497C43.2008 51.2693 43.0385 51.1847 42.8762 51.107L42.8651 51.0987C40.0232 49.7173 36.829 48.9503 33.4573 48.9503C27.5488 48.9503 22.2034 51.3317 18.3115 55.1889C18.3198 55.2027 18.3282 55.2208 18.3351 55.2402C18.496 55.5897 18.6527 55.9392 18.8247 56.2804Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.0066 64.8104C60.8124 65.1516 59.5516 65.3361 58.2492 65.3361C56.191 65.3361 54.2423 64.8812 52.4878 64.0712C52.4198 64.0379 52.3602 64.0032 52.2922 63.9713C52.2409 63.9436 52.1868 63.9242 52.1355 63.9006C50.2228 63.0226 48.0911 62.5344 45.8511 62.5344C40.4627 62.5344 35.7359 65.3652 33.0757 69.6246C36.342 70.9811 39.9273 71.7342 43.6916 71.7342C50.7124 71.7342 57.123 69.117 62.0093 64.816L62.0066 64.8104Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.5669 60.1953C56.0426 60.1953 52.6972 59.4075 49.6985 58.0094C49.6819 58.0039 49.668 57.9983 49.6528 57.99C49.5973 57.9636 49.539 57.9414 49.4836 57.9137C46.8539 56.7001 43.9315 56.0288 40.8483 56.0288C34.2061 56.0288 28.3031 59.1689 24.5264 64.0372C24.611 64.1204 24.6984 64.2022 24.7871 64.2827C24.7941 64.291 24.8038 64.2966 24.8079 64.3049C24.9466 64.4325 25.0881 64.5587 25.2268 64.6863C25.267 64.7224 25.3017 64.7543 25.3419 64.7903C25.4529 64.8846 25.561 64.979 25.6734 65.0774C25.7733 65.1579 25.8676 65.2425 25.9647 65.3201C26.0548 65.3964 26.1436 65.4755 26.2393 65.5476C26.3364 65.6294 26.4362 65.7057 26.5375 65.7862C26.6346 65.8597 26.7261 65.936 26.8246 66.0081C26.8759 66.0497 26.9272 66.0899 26.9785 66.1274C27.1547 66.2605 27.3308 66.3881 27.507 66.5157C27.6373 66.61 27.7691 66.7043 27.9023 66.7973C28.0895 66.9276 28.2781 67.0539 28.4709 67.1815C28.6013 67.2661 28.7344 67.3493 28.8634 67.4339C28.9675 67.4991 29.0673 67.5615 29.1727 67.6253C32.2823 62.9068 37.6277 59.7917 43.7027 59.7917C46.7124 59.7917 49.5543 60.5587 52.0245 61.904C52.0841 61.9429 52.1521 61.972 52.2145 62.0053C52.2991 62.0538 52.3796 62.1079 52.4628 62.1537C54.4364 63.2133 56.6903 63.8167 59.087 63.8167C60.786 63.8167 62.4115 63.5143 63.915 62.9609C64.1203 62.7473 64.3159 62.5254 64.5156 62.3035C64.5724 62.2383 64.6293 62.1759 64.6862 62.1107C64.8956 61.8694 65.0953 61.6225 65.2964 61.3742C65.3644 61.2924 65.4296 61.2133 65.4934 61.1301C65.6834 60.8915 65.8651 60.6474 66.044 60.4033C66.1175 60.3062 66.1883 60.2119 66.2576 60.1135C66.431 59.8735 66.596 59.6294 66.7611 59.3797C66.8291 59.2771 66.9026 59.1717 66.9733 59.0691C66.9927 59.033 67.0107 59.0011 67.0329 58.9706C64.6862 59.7598 62.1785 60.1953 59.5669 60.1953Z" fill="#004098"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.9068 28.3841H53.997V30.1858H52.1759V28.3841H48.56V27.7822L52.1759 24.7683H53.997V27.7822H54.9068V28.3841ZM48.739 30.1858H43.2993V29.5866C43.2993 29.1664 43.8846 28.631 45.0524 27.9639L45.1731 27.8931C46.3367 27.2301 46.9179 26.6906 46.9179 26.2787V25.9791C46.9179 25.8085 46.8263 25.6698 46.6502 25.5505C46.4685 25.4312 46.2521 25.3772 46.0163 25.3772H45.112C44.5572 25.3772 44.2521 25.5783 44.2174 25.986H43.2993C43.4421 25.176 44.0482 24.7683 45.112 24.7683H46.9262C47.42 24.7683 47.8471 24.8876 48.205 25.1247C48.56 25.3605 48.739 25.6476 48.739 25.9735V26.2787C48.739 26.6906 48.1564 27.2301 46.9858 27.8931L46.8666 27.9639C45.7043 28.6254 45.1162 29.1539 45.1162 29.5325V29.5783H48.739V30.1858ZM42.5697 28.0831C42.5697 28.6601 42.2618 29.1539 41.6391 29.563C41.0149 29.9791 40.2673 30.1858 39.3977 30.1858H38.0412V29.5783H38.9455C39.4449 29.5783 39.872 29.4562 40.2243 29.2218C40.5739 28.9861 40.7556 28.7031 40.7556 28.373V27.7669C40.6848 28.1788 40.3866 28.3841 39.8512 28.3841H38.9455C38.4546 28.3841 38.0301 28.2648 37.6723 28.029C37.3117 27.796 37.1314 27.5117 37.1314 27.1844V25.9791C37.1314 25.6476 37.3089 25.3633 37.6723 25.1275C38.0246 24.8875 38.449 24.7683 38.9455 24.7683H40.7583C41.2535 24.7683 41.682 24.8875 42.0385 25.1275C42.3894 25.3633 42.5697 25.6476 42.5697 25.9791V28.0831ZM34.5877 30.1858H32.768V25.986H31.8623V25.3772L33.6737 24.7683H34.5877V30.1858ZM43.6918 16.262C38.9885 16.262 34.5627 17.4367 30.6806 19.5019C21.9246 24.1636 15.9551 33.3841 15.9551 43.9987C15.9551 45.6533 16.109 47.2677 16.3864 48.842C20.0314 46.258 24.4822 44.7324 29.2908 44.7324C33.5031 44.7324 37.4601 45.8919 40.8207 47.9266C40.8665 47.9501 40.9095 47.9751 40.9553 48.0028C41.0565 48.0639 41.1536 48.1263 41.2507 48.1915C44.904 50.3801 49.1745 51.6409 53.7446 51.6409C60.7099 51.6409 66.9915 48.706 71.4257 44.0167L71.4285 43.9987C71.4285 28.6795 59.0081 16.262 43.6918 16.262Z" fill="#00A7EA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.4877 25.5501C40.3088 25.4308 40.1007 25.3767 39.8538 25.3767C39.6042 25.3767 39.3906 25.4308 39.22 25.5501C39.0411 25.6694 38.9551 25.8122 38.9551 25.9787V27.1839C38.9551 27.349 39.0383 27.4821 39.2172 27.6014C39.3906 27.7193 39.6042 27.7817 39.8538 27.7817C40.1007 27.7817 40.3088 27.7193 40.4877 27.6014C40.6638 27.4766 40.7554 27.3296 40.7554 27.1728V25.9731C40.7554 25.8081 40.6638 25.6666 40.4877 25.5501Z" fill="#00A7EA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M52.1775 27.7824V25.5286L49.4688 27.7824H52.1775Z" fill="#00A7EA"/>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="3000" height="2000" viewBox="0 500 3000 2000">
<defs>
<style>
.cls-1 {
fill: #000000;
stroke-width: 1px;
fill-rule: evenodd;
}
</style>
</defs>
<path id="形状_1" data-name="形状 1" class="cls-1" d="M382,1228l415-32,4,68s-219.245,8.45-339.022,13.07C416.965,1278.81,386,1280,386,1280Zm26,78,54-27,31,71,64-4-24-44,53-27,24,66,72-6,32-68h30l33,17-30,46,40-3,69,42-15,111-55,5-2-89-251,11-7,24,245-8-15,65-243-1-5,27,218-6,46,47-64,72,126,52-32,64-143-68-186,68-28-59,130-47-82-40,20-32-19-2L389,1720l-52-40,100-141,14-52H417l-8-48,54-4,7-23-72,1-2,66-59,12-10-145,71,3,1,8,36-3Zm151,258,108,4-30,29Zm364-141-34,127,31,12,21-59h30l1,95-69,26,19,44,50-24v111h37l2-129,38-18-12-34-25,10,1-81,27-2,6-46-31,2,1-65-44-5-1,67-15,2,8-21"/>
<path id="形状_2" data-name="形状 2" class="cls-1" d="M1067,1428l5,36,69-1-1,50-100,5,2,37h219l5-46-87,2,3-47,67-1,5-46-69,2,1-33-41-3-2,39Z"/>
<path id="形状_3" data-name="形状 3" class="cls-1" d="M1179,1560l45,4v23h37l-4,46-34,1-2,121-42,2-33-16,12-34,21,7,1-80H1054l-4-39,130-6Zm-74,83,43,50-25,25-50-46Zm217-298,371-14-4,61-155,1-5,202,188-4-5,62-413,1-8-53,182-3-5-203-141-1Zm480,4-122,169,39,27,28-34-1,280,47-5,10-327-11-6,57-69Zm102,2-81,177,42,19,45-79,33,1,2,327,48-3,1-104,112-3,8-55-118,8,2-56h97l5-52-102,4,2-65,113-2,3-59-177,6,20-39Zm424-59-41,37,24,22-109,8v-7l-59-2,3,115,48-6,5-55,301-4-5,61,46-4,18-80-53-35-129,7Zm-123,146,4,42,67,2-69,62,33,39,216-7,11,25,46-20-48-84-39,17,10,20-148,3,54-55,144-2,6-49Zm130,149,52,5v17l111-2-6,52-109,1-4,34,152-6-3,53-350,5-8-44,168-8-4-33H2207l-5-42,133-6v-26Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+136
View File
@@ -0,0 +1,136 @@
<?php get_header(); ?>
<main class="site-main single-article-page">
<div class="container single-article-shell">
<?php while (have_posts()) : the_post(); ?>
<?php
$post_id = get_the_ID();
$post_type = get_post_type($post_id);
$post_type_object = get_post_type_object($post_type);
$post_type_archive_url = get_post_type_archive_link($post_type);
$post_type_label_en = $post_type_object ? $post_type_object->labels->name : '';
$post_type_label_cn = $post_type_label_en;
if ($post_type === 'announcement') {
$post_type_label_cn = '公告通知';
$post_type_label_en = 'Announcements';
} elseif ($post_type === 'news') {
$post_type_label_cn = '社团新闻';
$post_type_label_en = 'News';
} elseif ($post_type === 'post') {
$post_type_label_cn = '技术博客';
$post_type_label_en = 'Blog';
}
$views = function_exists('itstudio_get_post_views') ? (int) itstudio_get_post_views($post_id) : 0;
?>
<article id="post-<?php the_ID(); ?>" <?php post_class('single-article'); ?>>
<header class="single-article-header">
<div class="single-article-kicker">
<?php if (!empty($post_type_archive_url) && !empty($post_type_label_en)) : ?>
<a class="single-article-kicker-link" href="<?php echo esc_url($post_type_archive_url); ?>" data-cn="<?php echo esc_attr($post_type_label_cn); ?>" data-en="<?php echo esc_attr($post_type_label_en); ?>"><?php echo esc_html($post_type_label_en); ?></a>
<?php endif; ?>
</div>
<h1 class="single-article-title"><?php the_title(); ?></h1>
<div class="single-article-meta">
<span class="single-article-meta-item">
<span class="single-article-meta-label" data-cn="发布日期:" data-en="Published: ">Published: </span>
<time class="single-article-meta-value" datetime="<?php echo esc_attr(get_the_date('c')); ?>"><?php echo esc_html(get_the_date('Y-m-d H:i')); ?></time>
</span>
<span class="single-article-meta-item">
<span class="single-article-meta-label" data-cn="作者:" data-en="Author: ">Author: </span>
<span class="single-article-meta-value"><?php echo esc_html(get_the_author()); ?></span>
</span>
<span class="single-article-meta-item">
<span class="single-article-meta-label" data-cn="浏览量:" data-en="Views: ">Views: </span>
<span class="single-article-meta-value"><?php echo esc_html(number_format_i18n($views)); ?></span>
</span>
</div>
</header>
<div class="single-article-body">
<?php the_content(); ?>
</div>
<?php
$tags = get_the_terms($post_id, 'post_tag');
if (!empty($tags) && !is_wp_error($tags)) :
?>
<footer class="single-article-tags">
<?php foreach (array_slice($tags, 0, 8) as $tag) : ?>
<?php
$tag_link = function_exists('itstudio_get_archive_tag_filter_url')
? itstudio_get_archive_tag_filter_url($tag, $post_type)
: '';
if ($tag_link === '') {
$tag_link = get_term_link($tag);
}
?>
<?php if (!is_wp_error($tag_link) && !empty($tag_link)) : ?>
<a class="single-article-tag" href="<?php echo esc_url($tag_link); ?>">#<?php echo esc_html($tag->name); ?></a>
<?php endif; ?>
<?php endforeach; ?>
</footer>
<?php endif; ?>
</article>
<section class="single-related">
<div class="single-divider" aria-hidden="true"></div>
<h2 class="single-related-title" data-cn="相关阅读" data-en="Related Content">Related Content</h2>
<?php
$related_args = array(
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => 3,
'post__not_in' => array($post_id),
'ignore_sticky_posts' => true,
'orderby' => 'date',
'order' => 'DESC',
);
$tag_ids = wp_get_post_terms($post_id, 'post_tag', array('fields' => 'ids'));
if (!empty($tag_ids) && !is_wp_error($tag_ids)) {
$related_args['tax_query'] = array(
array(
'taxonomy' => 'post_tag',
'field' => 'term_id',
'terms' => $tag_ids,
),
);
}
$related_query = new WP_Query($related_args);
if (!$related_query->have_posts() && isset($related_args['tax_query'])) {
unset($related_args['tax_query']);
$related_query = new WP_Query($related_args);
}
?>
<div class="single-related-grid">
<?php if ($related_query->have_posts()) : ?>
<?php while ($related_query->have_posts()) : $related_query->the_post(); ?>
<?php
$related_excerpt = function_exists('itstudio_get_post_excerpt_chars')
? itstudio_get_post_excerpt_chars(get_the_ID(), 96)
: wp_html_excerpt(wp_strip_all_tags(get_the_excerpt() ?: get_the_content()), 96, '...');
?>
<article class="single-related-card">
<h3 class="single-related-card-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<p class="single-related-card-excerpt"><?php echo esc_html($related_excerpt); ?></p>
<a class="single-related-card-link" href="<?php the_permalink(); ?>" data-cn="阅读更多 ->" data-en="Read more ->">Read more -></a>
</article>
<?php endwhile; ?>
<?php wp_reset_postdata(); ?>
<?php else : ?>
<p class="single-related-empty" data-cn="暂无相关文章" data-en="No related content">No related content</p>
<?php endif; ?>
</div>
</section>
<?php endwhile; ?>
</div>
</main>
<?php get_footer(); ?>
+128
View File
@@ -0,0 +1,128 @@
/*
Theme Name: IT Studio Theme
Theme URI: https://github.com/itstudio-2002/ITStudioMainSite
Description: 中国海洋大学爱特工作室官方网站主题 - A modern WordPress theme for IT Studio of Ocean University of China
Version: 2.1.0
Author: IT Studio
Author URI: https://it.ouc.edu.cn
License: MIT
License URI: https://opensource.org/licenses/MIT
Text Domain: itstudio
Tags: responsive, modern, dark-mode, bilingual
*/
/* =========================================
1. 设计令牌 (Design Tokens) - 全局变量
========================================= */
:root {
/* --- 调色板: 海洋与极客 --- */
--color-primary: #0969da; /* 核心蓝:用于链接、主按钮 */
--color-primary-light: #58a6ff; /* 亮蓝:用于深色模式主色 */
--color-success: #2dba4e; /* 成功绿:代码风格点缀 */
--color-purple: #8957e5; /* 极客紫:强调色 */
--color-danger: #cf222e; /* 警告红 */
/* --- 主题色: 浅色模式 (Light) --- */
--bg-body: #ffffff; /* 页面背景 */
--bg-surface: #f6f8fa; /* 次级背景 (Sidebar/Footer) */
--bg-card: #ffffff; /* 卡片背景 */
--text-primary: #24292f; /* 主要文字 */
--text-secondary: #343a40; /* 次要文字 */
--border-default: #d0d7de; /* 默认边框 */
--border-muted: #d8dee4; /* 淡边框 */
--shadow-sm: 0 1px 3px rgba(140, 149, 159, 0.12);
--shadow-md: 0 4px 12px rgba(140, 149, 159, 0.15);
--shadow-lg: 0 8px 24px rgba(140, 149, 159, 0.2);
/* --- 排版 (Typography) --- */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", "Menlo", "Consolas", monospace;
/* --- 布局 (Layout) --- */
--header-height: 90px;
--container-width: 1400px;
--radius-sm: 6px;
--radius-md: 8px;
/* --- 动画 (Animation) --- */
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--duration-fast: 0.2s;
--duration-normal: 0.3s;
}
/* --- 主题色: 深色模式 (Dark) --- */
[data-theme="dark"] {
--color-primary: #58a6ff; /* 深色模式下蓝色需要更亮 */
--bg-body: #0d1117; /* 深邃蓝黑 (Github Dark) */
--bg-surface: #161b22;
--bg-card: #161b22;
--text-primary: #c9d1d9; /* 灰白 */
--text-secondary: #6d737c; /* 灰 */
--border-default: #30363d;
--border-muted: #21262d;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.8);
}
/* =========================================
2. 全局重置 (Reset & Base)
========================================= */
* { box-sizing: border-box; margin: 0; padding: 0; }
html {
min-height: 100vh; /* 确保HTML高度撑满 */
font-size: 18px; /* 增加基础字号 (原默认为16px) */
}
body {
font-family: var(--font-sans);
background-color: var(--bg-body);
color: var(--text-primary);
line-height: 1.6;
transition: background-color var(--duration-normal), color var(--duration-normal);
-webkit-font-smoothing: antialiased;
display: flex;
flex-direction: column;
min-height: 100vh; /* 确保Body高度撑满并使用Flex布局 */
}
a {
color: var(--color-primary);
text-decoration: none;
transition: opacity var(--duration-fast);
}
a:hover { opacity: 0.85; }
ul { list-style: none; }
button { font-family: inherit; }
img { max-width: 100%; height: auto; display: block; }
svg { fill: currentColor; }
/* =========================================
3. 全局工具类 (Global Utilities)
========================================= */
.container {
max-width: var(--container-width);
margin: 0 auto;
padding: 0 24px;
width: 100%;
}
/* 双语内容自动填充系统 */
/* 中文模式:填充 data-cn 到伪元素 */
body:not([lang="en"]) [data-cn]:not(.hero-description):not(.gh-state):empty::before {
content: attr(data-cn);
}
/* 英文模式:填充 data-en 到伪元素 */
body[lang="en"] [data-en]:not(.hero-description):not(.gh-state):empty::before {
content: attr(data-en);
}
/* WordPress Admin Bar 兼容性修复 */
body.admin-bar .site-header { top: 32px; }
@media screen and (max-width: 782px) { body.admin-bar .site-header { top: 46px; } }