浅做了一下加入我们
This commit is contained in:
@@ -213,3 +213,69 @@ MIT License - 详见 LICENSE 文件
|
||||
- ✅ 响应式设计完成
|
||||
- ✅ 主题切换功能
|
||||
- ✅ 自定义文章类型(公告)
|
||||
|
||||
---
|
||||
|
||||
## 加入我们页面(/join)使用说明
|
||||
|
||||
主题已内置「加入我们」页面模板(`page-join.php`),并支持在没有创建 WordPress 页面时通过 `/join` 自动 fallback 渲染。
|
||||
|
||||
### 1. 需要安装的插件
|
||||
|
||||
1. **Formidable Forms**
|
||||
用于报名表单、进度查询表单、公示视图(可选使用 Formidable Views)。
|
||||
2. **WP Mail SMTP**
|
||||
用于 Formidable 提交邮件发送(站点邮件走 SMTP)。
|
||||
|
||||
### 2. 后台时间节点设置
|
||||
|
||||
进入:**设置 > 招新设置**
|
||||
|
||||
可配置字段:
|
||||
- 报名开始时间(datetime)
|
||||
- 报名结束时间(datetime)
|
||||
- 第一次面试日期(date)
|
||||
- 第二次面试日期(date)
|
||||
- 录取公示开始日期(date,系统自动延续 7 天)
|
||||
- 报名表单 Shortcode
|
||||
- 结果查询 Shortcode
|
||||
- 公示视图 Shortcode
|
||||
|
||||
说明:
|
||||
- 「国庆能力摸底阶段」固定为每年 **10/01 - 10/07**。
|
||||
- 年份优先取报名开始时间年份;若未设置,则取公示开始年份;仍未设置则取当前年份。
|
||||
|
||||
### 3. 前台显示逻辑
|
||||
|
||||
- 报名表单:仅在报名阶段显示。
|
||||
- 结果查询表单:从报名开始到公示结束前可用。
|
||||
- 公示视图:仅在公示 7 天窗口内显示。
|
||||
- Canvas 进度条:根据当前时间自动高亮阶段(已完成/进行中/未开始)。
|
||||
|
||||
### 4. Formidable 推荐配置方式
|
||||
|
||||
1. 新建报名表单,复制 shortcode,填入「报名表单 Shortcode」。
|
||||
2. 新建结果查询表单(例如:学号/邮箱/手机号 + 状态查询逻辑),填入「结果查询 Shortcode」。
|
||||
3. 如需公示名单展示,创建 Formidable View,复制 shortcode,填入「公示视图 Shortcode」。
|
||||
|
||||
### 5. WP Mail SMTP 配置建议
|
||||
|
||||
1. 安装并启用 **WP Mail SMTP**。
|
||||
2. 在插件中填写 SMTP 服务信息(Host、Port、加密方式、账号密码)。
|
||||
3. 设置发件人邮箱与发件人名称。
|
||||
4. 使用插件自带发送测试邮件,确认可达后再开放报名。
|
||||
|
||||
## Join Page Quick Guide (ASCII)
|
||||
|
||||
- Admin path: Settings -> Recruitment Settings.
|
||||
- Configure timeline nodes: registration start/end, interview I date, interview II date, notice start date.
|
||||
- Stage "National Day Assessment" is fixed to Oct 1 - Oct 7 (auto year).
|
||||
- Set Formidable shortcodes:
|
||||
- registration form shortcode
|
||||
- progress lookup shortcode
|
||||
- public notice view shortcode
|
||||
- Frontend visibility rules:
|
||||
- registration form: only in registration window
|
||||
- lookup form: from registration start until notice window ends
|
||||
- public notice view: only during 7-day notice window
|
||||
- Mail delivery: configure WP Mail SMTP plugin for Formidable emails.
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
.join-page {
|
||||
padding: 36px 0 72px;
|
||||
}
|
||||
|
||||
.join-head {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.join-title {
|
||||
margin: 0;
|
||||
font-size: clamp(2.1rem, 1.9vw + 1.3rem, 3.2rem);
|
||||
line-height: 1.08;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.join-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
|
||||
.join-canvas-shell {
|
||||
position: relative;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in srgb, var(--bg-card) 94%, transparent);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.join-stage-photo-frame {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.join-stage-photo {
|
||||
width: 100%;
|
||||
height: clamp(220px, 28vw, 360px);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.join-wave-layer {
|
||||
position: relative;
|
||||
margin-top: clamp(-20px, -2.4vw, -12px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.join-progress-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: clamp(56px, 5.8vw, 82px);
|
||||
}
|
||||
|
||||
.join-wave-progress {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.join-wave-track {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
left: clamp(18px, 2.2vw, 30px);
|
||||
right: clamp(18px, 2.2vw, 30px);
|
||||
top: 68%;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
transform: translateY(-50%);
|
||||
background: color-mix(in srgb, var(--text-secondary) 26%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-default) 60%, transparent);
|
||||
}
|
||||
|
||||
.join-wave-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
color-mix(in srgb, var(--color-primary) 56%, transparent) 0%,
|
||||
color-mix(in srgb, var(--color-primary) 82%, #7ed7ff 18%) 100%
|
||||
);
|
||||
box-shadow: 0 0 10px color-mix(in srgb, var(--color-primary) 35%, transparent);
|
||||
}
|
||||
|
||||
.join-wave-marks {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.join-wave-mark {
|
||||
--mark-wave-offset: 0px;
|
||||
--mark-tilt: 0deg;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: clamp(20px, 1.9vw, 27px);
|
||||
transform: translate(-50%, calc(-58% + var(--mark-wave-offset))) rotate(var(--mark-tilt));
|
||||
transform-origin: 50% 78%;
|
||||
filter: drop-shadow(0 2px 6px color-mix(in srgb, #00142b 50%, transparent));
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.join-wave-mark.is-lighthouse {
|
||||
width: clamp(22px, 2vw, 30px);
|
||||
transform: translate(-50%, -80%);
|
||||
transform-origin: 50% 96%;
|
||||
}
|
||||
|
||||
.join-wave-mark.is-active {
|
||||
filter: drop-shadow(0 0 10px color-mix(in srgb, var(--color-primary) 36%, transparent));
|
||||
}
|
||||
|
||||
.join-wave-mark-icon {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
transform-origin: 50% 80%;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.join-wave-mark svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-buoy-ring {
|
||||
fill: color-mix(in srgb, #5fa0df 72%, #8ec8ff 28%);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-buoy-base {
|
||||
fill: color-mix(in srgb, #fbfdff 92%, #d5e9ff 8%);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-buoy-stripe {
|
||||
fill: color-mix(in srgb, #f6814c 70%, #ec4d44 30%);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-buoy-cap {
|
||||
fill: color-mix(in srgb, #29497d 74%, #1c2e57 26%);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-buoy-light {
|
||||
fill: color-mix(in srgb, #ffe083 72%, #ffc74a 28%);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-buoy-gloss {
|
||||
fill: color-mix(in srgb, #ffffff 88%, transparent);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-lh-base {
|
||||
fill: color-mix(in srgb, var(--color-primary) 70%, #0d2e62 30%);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-lh-tower {
|
||||
fill: color-mix(in srgb, #f6fbff 88%, var(--color-primary) 12%);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-lh-top {
|
||||
fill: color-mix(in srgb, #d44545 72%, #a52f2f 28%);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-lh-beam {
|
||||
fill: color-mix(in srgb, #ffef9f 64%, transparent);
|
||||
opacity: 0.28;
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-reef-rock {
|
||||
fill: color-mix(in srgb, #365a7f 72%, #2a4868 28%);
|
||||
}
|
||||
|
||||
.join-wave-mark .marker-reef-highlight {
|
||||
fill: color-mix(in srgb, #5f82a6 64%, #486a8e 36%);
|
||||
}
|
||||
|
||||
.join-wave-boat {
|
||||
--boat-wave-offset: 0px;
|
||||
--boat-tilt: 0deg;
|
||||
--boat-waterline-adjust: -4px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: clamp(42px, 4.4vw, 58px);
|
||||
transform: translate(-50%, calc(-62% + var(--boat-waterline-adjust) + var(--boat-wave-offset))) rotate(var(--boat-tilt));
|
||||
color: color-mix(in srgb, var(--color-primary) 78%, #8ed5ff 22%);
|
||||
filter: drop-shadow(0 3px 8px color-mix(in srgb, #00132f 60%, transparent));
|
||||
will-change: transform, left;
|
||||
}
|
||||
|
||||
.join-wave-boat-icon {
|
||||
display: block;
|
||||
transform-origin: 50% 60%;
|
||||
}
|
||||
|
||||
.join-wave-boat svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.join-wave-boat .boat-hull {
|
||||
fill: color-mix(in srgb, var(--color-primary) 64%, #102e5f 36%);
|
||||
}
|
||||
|
||||
.join-wave-boat .boat-sail-main {
|
||||
fill: color-mix(in srgb, #f2f9ff 90%, var(--color-primary) 10%);
|
||||
}
|
||||
|
||||
.join-wave-boat .boat-sail-side {
|
||||
fill: color-mix(in srgb, #cbe6ff 80%, var(--color-primary) 20%);
|
||||
}
|
||||
|
||||
.join-wave-boat .boat-mast {
|
||||
fill: color-mix(in srgb, var(--text-primary) 75%, #294b7a 25%);
|
||||
}
|
||||
|
||||
.join-wave-boat .boat-porthole {
|
||||
fill: color-mix(in srgb, #eef6ff 85%, var(--color-primary) 15%);
|
||||
}
|
||||
|
||||
.join-canvas-overlay {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
max-width: 540px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-default) 72%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-body) 68%, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.join-current-label {
|
||||
margin: 0;
|
||||
font-size: 0.84rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.join-current-stage {
|
||||
margin: 4px 0 0;
|
||||
font-size: clamp(1.2rem, 0.9vw + 1rem, 1.7rem);
|
||||
line-height: 1.25;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.join-current-range {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.92rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.join-stage-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.join-stage-item {
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--bg-card) 95%, transparent);
|
||||
padding: 10px 11px;
|
||||
min-height: 98px;
|
||||
}
|
||||
|
||||
.join-stage-item.is-current {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 52%, var(--border-default) 48%);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.join-stage-item.is-completed .join-stage-status {
|
||||
color: #0f8a4f;
|
||||
}
|
||||
|
||||
.join-stage-item.is-active .join-stage-status {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.join-stage-item.is-upcoming .join-stage-status,
|
||||
.join-stage-item.is-pending .join-stage-status {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.join-stage-title-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
column-gap: 8px;
|
||||
row-gap: 4px;
|
||||
}
|
||||
|
||||
.join-stage-name {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.3;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.join-stage-status {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
align-self: start;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.join-stage-time {
|
||||
margin: 8px 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.join-forms-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.join-form-card {
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 14px;
|
||||
background: color-mix(in srgb, var(--bg-card) 95%, transparent);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.join-form-card-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.join-form-head h2 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.22rem;
|
||||
}
|
||||
|
||||
.join-form-head p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.join-form-content {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.join-form-content form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.join-form-content .frm_forms {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_form_fields,
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_submit {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="text"],
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="email"],
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="number"],
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="tel"],
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="date"],
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="datetime-local"],
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style textarea,
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style select {
|
||||
background: color-mix(in srgb, var(--bg-body) 90%, transparent);
|
||||
border-color: var(--border-default);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="submit"],
|
||||
.join-form-content .frm_style_formidable-style.with_frm_style button.frm_button_submit {
|
||||
border-radius: 9px;
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary) 62%, var(--border-default) 38%);
|
||||
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.join-form-tip {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed var(--border-default);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.join-form-footnote {
|
||||
margin-top: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.join-stage-list {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.join-page {
|
||||
padding: 30px 0 60px;
|
||||
}
|
||||
|
||||
.join-stage-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.join-forms-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.join-form-card-full {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.join-head {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.join-hero {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.join-canvas-overlay {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
padding: 10px 11px;
|
||||
}
|
||||
|
||||
.join-stage-photo {
|
||||
height: 230px;
|
||||
}
|
||||
|
||||
.join-current-stage {
|
||||
margin-top: 2px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.join-current-range {
|
||||
margin-top: 5px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.join-stage-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.join-stage-item {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
(() => {
|
||||
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 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 = [];
|
||||
|
||||
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 palettes = {
|
||||
light: {
|
||||
wave1: 'rgba(198, 227, 255, 0.92)',
|
||||
wave2: 'rgba(142, 193, 236, 0.4)',
|
||||
wave3: 'rgba(84, 151, 214, 0.48)',
|
||||
},
|
||||
dark: {
|
||||
wave1: 'rgba(132, 190, 242, 0.88)',
|
||||
wave2: 'rgba(88, 151, 214, 0.4)',
|
||||
wave3: 'rgba(56, 118, 183, 0.48)',
|
||||
},
|
||||
};
|
||||
|
||||
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 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 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 = Array.isArray(joinData.stages) ? joinData.stages : [];
|
||||
if (!stages.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const denominator = stages.length > 1 ? (stages.length - 1) : 1;
|
||||
const currentIndex = getNumber(joinData.currentStageIndex);
|
||||
|
||||
// 有进行中阶段时,船严格对齐对应浮标节点。
|
||||
if (currentIndex !== null && currentIndex >= 0) {
|
||||
return clamp(currentIndex / denominator, 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) {
|
||||
return clamp(lastCompletedIndex / denominator, 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 = '';
|
||||
|
||||
const stages = Array.isArray(joinData.stages) ? joinData.stages : [];
|
||||
if (!stages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = getNumber(joinData.currentStageIndex);
|
||||
const safeCurrentIndex = currentIndex === null ? -1 : Math.round(currentIndex);
|
||||
const denominator = stages.length > 1 ? (stages.length - 1) : 1;
|
||||
|
||||
stages.forEach((stage, index) => {
|
||||
const marker = document.createElement('span');
|
||||
marker.className = 'join-wave-mark';
|
||||
const progress = denominator > 0 ? (index / denominator) : 0;
|
||||
marker.style.left = `${(progress * 100).toFixed(3)}%`;
|
||||
|
||||
const isLighthouse = index === stages.length - 1 || (stage && stage.key === 'public_notice');
|
||||
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')) : [];
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
targetProgress = computeTargetProgress();
|
||||
renderStageMarks();
|
||||
startWaves();
|
||||
animateBoatToTarget();
|
||||
})();
|
||||
+826
@@ -1104,3 +1104,829 @@ function itstudio_track_post_views() {
|
||||
update_post_meta($post_id, 'itstudio_views', $views);
|
||||
}
|
||||
add_action('template_redirect', 'itstudio_track_post_views', 20);
|
||||
|
||||
function itstudio_is_join_page_context() {
|
||||
$is_join = is_page('join') || is_page_template('page-join.php');
|
||||
if (!$is_join && is_404()) {
|
||||
global $wp;
|
||||
$request = isset($wp->request) ? trim((string) $wp->request, '/') : '';
|
||||
$is_join = ($request === 'join');
|
||||
}
|
||||
|
||||
return $is_join;
|
||||
}
|
||||
|
||||
function itstudio_join_get_default_settings() {
|
||||
return array(
|
||||
'registration_start' => '',
|
||||
'registration_end' => '',
|
||||
'first_interview_date' => '',
|
||||
'second_interview_date' => '',
|
||||
'notice_start_date' => '',
|
||||
'photo_registration' => 0,
|
||||
'photo_first_interview' => 0,
|
||||
'photo_assessment' => 0,
|
||||
'photo_second_interview' => 0,
|
||||
'photo_public_notice' => 0,
|
||||
'photo_inactive' => 0,
|
||||
'signup_form_shortcode' => '',
|
||||
'query_form_shortcode' => '',
|
||||
'notice_view_shortcode' => '',
|
||||
);
|
||||
}
|
||||
|
||||
function itstudio_join_get_photo_field_map() {
|
||||
return array(
|
||||
'registration' => 'photo_registration',
|
||||
'first_interview' => 'photo_first_interview',
|
||||
'assessment' => 'photo_assessment',
|
||||
'second_interview' => 'photo_second_interview',
|
||||
'public_notice' => 'photo_public_notice',
|
||||
'inactive' => 'photo_inactive',
|
||||
);
|
||||
}
|
||||
|
||||
function itstudio_join_sanitize_datetime_local_value($value) {
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = preg_replace('/\s+/', 'T', $value);
|
||||
|
||||
// 兼容旧数据:仅日期
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// 兼容输入:YYYY-MM-DDTHH:MM 或 YYYY-MM-DDTHH:MM:SS
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(:\d{2})?$/', $value, $matches)) {
|
||||
return $matches[1] . 'T' . $matches[2];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function itstudio_join_sanitize_date_value($value) {
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function itstudio_join_sanitize_shortcode_value($value) {
|
||||
if (!is_string($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return trim(sanitize_text_field(wp_unslash($value)));
|
||||
}
|
||||
|
||||
function itstudio_join_sanitize_settings($input) {
|
||||
$defaults = itstudio_join_get_default_settings();
|
||||
$input = is_array($input) ? $input : array();
|
||||
|
||||
$sanitized = array(
|
||||
'registration_start' => itstudio_join_sanitize_datetime_local_value($input['registration_start'] ?? ''),
|
||||
'registration_end' => itstudio_join_sanitize_datetime_local_value($input['registration_end'] ?? ''),
|
||||
'first_interview_date' => itstudio_join_sanitize_date_value($input['first_interview_date'] ?? ''),
|
||||
'second_interview_date' => itstudio_join_sanitize_date_value($input['second_interview_date'] ?? ''),
|
||||
'notice_start_date' => itstudio_join_sanitize_date_value($input['notice_start_date'] ?? ''),
|
||||
'signup_form_shortcode' => itstudio_join_sanitize_shortcode_value($input['signup_form_shortcode'] ?? ''),
|
||||
'query_form_shortcode' => itstudio_join_sanitize_shortcode_value($input['query_form_shortcode'] ?? ''),
|
||||
'notice_view_shortcode' => itstudio_join_sanitize_shortcode_value($input['notice_view_shortcode'] ?? ''),
|
||||
);
|
||||
|
||||
foreach (itstudio_join_get_photo_field_map() as $field_key) {
|
||||
$sanitized[$field_key] = isset($input[$field_key]) ? absint($input[$field_key]) : 0;
|
||||
}
|
||||
|
||||
return array_merge($defaults, $sanitized);
|
||||
}
|
||||
|
||||
function itstudio_join_get_settings() {
|
||||
$defaults = itstudio_join_get_default_settings();
|
||||
$stored = get_option('itstudio_join_settings', array());
|
||||
if (!is_array($stored)) {
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
return array_merge($defaults, itstudio_join_sanitize_settings($stored));
|
||||
}
|
||||
|
||||
function itstudio_join_parse_datetime_local($value, $date_only_as_end_of_day = false) {
|
||||
$value = itstudio_join_sanitize_datetime_local_value($value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timezone = wp_timezone();
|
||||
$format = 'Y-m-d\TH:i';
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||
$format = 'Y-m-d';
|
||||
}
|
||||
|
||||
$parsed = DateTimeImmutable::createFromFormat('!' . $format, $value, $timezone);
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
if (!($parsed instanceof DateTimeImmutable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($errors) && (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($format === 'Y-m-d') {
|
||||
return $date_only_as_end_of_day
|
||||
? $parsed->setTime(23, 59, 59)
|
||||
: $parsed->setTime(0, 0, 0);
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
function itstudio_join_parse_date($value) {
|
||||
$value = itstudio_join_sanitize_date_value($value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timezone = wp_timezone();
|
||||
$parsed = DateTimeImmutable::createFromFormat('!Y-m-d', $value, $timezone);
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
if (!($parsed instanceof DateTimeImmutable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($errors) && (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
function itstudio_join_datetime_to_ms($date) {
|
||||
if (!($date instanceof DateTimeImmutable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $date->format('U') * 1000;
|
||||
}
|
||||
|
||||
function itstudio_join_resolve_stage_status($now, $start, $end) {
|
||||
if (!($now instanceof DateTimeImmutable)) {
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
if (!($start instanceof DateTimeImmutable) && !($end instanceof DateTimeImmutable)) {
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
if ($start instanceof DateTimeImmutable && $now < $start) {
|
||||
return 'upcoming';
|
||||
}
|
||||
|
||||
if ($start instanceof DateTimeImmutable && $end instanceof DateTimeImmutable) {
|
||||
if ($now >= $start && $now <= $end) {
|
||||
return 'active';
|
||||
}
|
||||
if ($now > $end) {
|
||||
return 'completed';
|
||||
}
|
||||
}
|
||||
|
||||
if ($start instanceof DateTimeImmutable && !($end instanceof DateTimeImmutable)) {
|
||||
if ($now >= $start) {
|
||||
return 'active';
|
||||
}
|
||||
}
|
||||
|
||||
if (!($start instanceof DateTimeImmutable) && $end instanceof DateTimeImmutable) {
|
||||
return $now <= $end ? 'active' : 'completed';
|
||||
}
|
||||
|
||||
return 'upcoming';
|
||||
}
|
||||
|
||||
function itstudio_join_is_in_window($now, $start, $end) {
|
||||
if (!($now instanceof DateTimeImmutable) || !($start instanceof DateTimeImmutable)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($now < $start) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($end instanceof DateTimeImmutable && $now > $end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function itstudio_join_format_stage_range($start, $end, $all_day = false) {
|
||||
if (!($start instanceof DateTimeImmutable) && !($end instanceof DateTimeImmutable)) {
|
||||
return array(
|
||||
'cn' => '时间待定',
|
||||
'en' => 'Schedule TBA',
|
||||
);
|
||||
}
|
||||
|
||||
$format_cn_day = 'Y.m.d';
|
||||
$format_cn_full = 'Y.m.d H:i';
|
||||
$format_en_day = 'M j, Y';
|
||||
$format_en_full = 'M j, Y H:i';
|
||||
|
||||
if ($start instanceof DateTimeImmutable && !($end instanceof DateTimeImmutable)) {
|
||||
return array(
|
||||
'cn' => $all_day ? $start->format($format_cn_day) : $start->format($format_cn_full),
|
||||
'en' => $all_day ? $start->format($format_en_day) : $start->format($format_en_full),
|
||||
);
|
||||
}
|
||||
|
||||
if (!($start instanceof DateTimeImmutable) && $end instanceof DateTimeImmutable) {
|
||||
return array(
|
||||
'cn' => $all_day ? $end->format($format_cn_day) : $end->format($format_cn_full),
|
||||
'en' => $all_day ? $end->format($format_en_day) : $end->format($format_en_full),
|
||||
);
|
||||
}
|
||||
|
||||
if (!($start instanceof DateTimeImmutable) || !($end instanceof DateTimeImmutable)) {
|
||||
return array(
|
||||
'cn' => '时间待定',
|
||||
'en' => 'Schedule TBA',
|
||||
);
|
||||
}
|
||||
|
||||
$start_cn = $all_day ? $start->format($format_cn_day) : $start->format($format_cn_full);
|
||||
$end_cn = $all_day ? $end->format($format_cn_day) : $end->format($format_cn_full);
|
||||
$start_en = $all_day ? $start->format($format_en_day) : $start->format($format_en_full);
|
||||
$end_en = $all_day ? $end->format($format_en_day) : $end->format($format_en_full);
|
||||
|
||||
if ($start_cn === $end_cn) {
|
||||
return array(
|
||||
'cn' => $start_cn,
|
||||
'en' => $start_en,
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'cn' => $start_cn . ' - ' . $end_cn,
|
||||
'en' => $start_en . ' - ' . $end_en,
|
||||
);
|
||||
}
|
||||
|
||||
function itstudio_join_get_stage_photo_url($settings, $stage_key) {
|
||||
$settings = is_array($settings) ? $settings : array();
|
||||
$field_map = itstudio_join_get_photo_field_map();
|
||||
$field_key = isset($field_map[$stage_key]) ? $field_map[$stage_key] : '';
|
||||
|
||||
$attachment_id = 0;
|
||||
if ($field_key !== '' && isset($settings[$field_key])) {
|
||||
$attachment_id = absint($settings[$field_key]);
|
||||
}
|
||||
|
||||
if ($attachment_id > 0) {
|
||||
$photo_url = wp_get_attachment_image_url($attachment_id, 'full');
|
||||
if (is_string($photo_url) && $photo_url !== '') {
|
||||
return $photo_url;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function itstudio_join_get_runtime_data() {
|
||||
static $cached = null;
|
||||
if (is_array($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$settings = itstudio_join_get_settings();
|
||||
$timezone = wp_timezone();
|
||||
$now = new DateTimeImmutable('now', $timezone);
|
||||
|
||||
$registration_start = itstudio_join_parse_datetime_local($settings['registration_start'], false);
|
||||
$registration_end = itstudio_join_parse_datetime_local($settings['registration_end'], true);
|
||||
if ($registration_start instanceof DateTimeImmutable) {
|
||||
$registration_start = $registration_start->setTime(0, 0, 0);
|
||||
}
|
||||
if ($registration_end instanceof DateTimeImmutable) {
|
||||
$registration_end = $registration_end->setTime(23, 59, 59);
|
||||
}
|
||||
if ($registration_start instanceof DateTimeImmutable && $registration_end instanceof DateTimeImmutable && $registration_end < $registration_start) {
|
||||
$registration_end = $registration_start;
|
||||
}
|
||||
|
||||
$first_interview_day = itstudio_join_parse_date($settings['first_interview_date']);
|
||||
$first_interview_start = $first_interview_day instanceof DateTimeImmutable ? $first_interview_day->setTime(0, 0, 0) : null;
|
||||
$first_interview_end = $first_interview_day instanceof DateTimeImmutable ? $first_interview_day->setTime(23, 59, 59) : null;
|
||||
|
||||
$second_interview_day = itstudio_join_parse_date($settings['second_interview_date']);
|
||||
$second_interview_start = $second_interview_day instanceof DateTimeImmutable ? $second_interview_day->setTime(0, 0, 0) : null;
|
||||
$second_interview_end = $second_interview_day instanceof DateTimeImmutable ? $second_interview_day->setTime(23, 59, 59) : null;
|
||||
|
||||
$notice_start_day = itstudio_join_parse_date($settings['notice_start_date']);
|
||||
$notice_start = $notice_start_day instanceof DateTimeImmutable ? $notice_start_day->setTime(0, 0, 0) : null;
|
||||
$notice_end = $notice_start instanceof DateTimeImmutable ? $notice_start->modify('+6 days')->setTime(23, 59, 59) : null;
|
||||
|
||||
$recruitment_year = null;
|
||||
if ($registration_start instanceof DateTimeImmutable) {
|
||||
$recruitment_year = (int) $registration_start->format('Y');
|
||||
} elseif ($notice_start instanceof DateTimeImmutable) {
|
||||
$recruitment_year = (int) $notice_start->format('Y');
|
||||
} else {
|
||||
$recruitment_year = (int) $now->format('Y');
|
||||
}
|
||||
|
||||
$assessment_start = DateTimeImmutable::createFromFormat('!Y-m-d', sprintf('%04d-10-01', $recruitment_year), $timezone);
|
||||
$assessment_end_base = DateTimeImmutable::createFromFormat('!Y-m-d', sprintf('%04d-10-07', $recruitment_year), $timezone);
|
||||
$assessment_end = $assessment_end_base instanceof DateTimeImmutable ? $assessment_end_base->setTime(23, 59, 59) : null;
|
||||
|
||||
$stage_seed = array(
|
||||
array(
|
||||
'key' => 'registration',
|
||||
'label_cn' => '报名阶段',
|
||||
'label_en' => 'Registration',
|
||||
'short_cn' => '报名',
|
||||
'short_en' => 'Reg',
|
||||
'start' => $registration_start,
|
||||
'end' => $registration_end,
|
||||
'all_day' => true,
|
||||
),
|
||||
array(
|
||||
'key' => 'first_interview',
|
||||
'label_cn' => '第一次面试',
|
||||
'label_en' => 'Interview I',
|
||||
'short_cn' => '一面',
|
||||
'short_en' => 'I-1',
|
||||
'start' => $first_interview_start,
|
||||
'end' => $first_interview_end,
|
||||
'all_day' => true,
|
||||
),
|
||||
array(
|
||||
'key' => 'assessment',
|
||||
'label_cn' => '国庆能力摸底',
|
||||
'label_en' => 'Assessment',
|
||||
'short_cn' => '摸底',
|
||||
'short_en' => 'Assess',
|
||||
'start' => $assessment_start,
|
||||
'end' => $assessment_end,
|
||||
'all_day' => true,
|
||||
),
|
||||
array(
|
||||
'key' => 'second_interview',
|
||||
'label_cn' => '第二次面试',
|
||||
'label_en' => 'Interview II',
|
||||
'short_cn' => '二面',
|
||||
'short_en' => 'I-2',
|
||||
'start' => $second_interview_start,
|
||||
'end' => $second_interview_end,
|
||||
'all_day' => true,
|
||||
),
|
||||
array(
|
||||
'key' => 'public_notice',
|
||||
'label_cn' => '录取结果公示',
|
||||
'label_en' => 'Public Notice',
|
||||
'short_cn' => '公示',
|
||||
'short_en' => 'Notice',
|
||||
'start' => $notice_start,
|
||||
'end' => $notice_end,
|
||||
'all_day' => true,
|
||||
),
|
||||
);
|
||||
|
||||
$stages = array();
|
||||
foreach ($stage_seed as $stage) {
|
||||
$range = itstudio_join_format_stage_range($stage['start'], $stage['end'], !empty($stage['all_day']));
|
||||
$status = itstudio_join_resolve_stage_status($now, $stage['start'], $stage['end']);
|
||||
$stages[] = array(
|
||||
'key' => $stage['key'],
|
||||
'label_cn' => $stage['label_cn'],
|
||||
'label_en' => $stage['label_en'],
|
||||
'short_cn' => $stage['short_cn'],
|
||||
'short_en' => $stage['short_en'],
|
||||
'status' => $status,
|
||||
'range_cn' => $range['cn'],
|
||||
'range_en' => $range['en'],
|
||||
'start_ts' => itstudio_join_datetime_to_ms($stage['start']),
|
||||
'end_ts' => itstudio_join_datetime_to_ms($stage['end']),
|
||||
);
|
||||
}
|
||||
|
||||
$current_stage_index = -1;
|
||||
foreach ($stages as $index => $stage) {
|
||||
if ($stage['status'] === 'active') {
|
||||
$current_stage_index = (int) $index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$current_stage = ($current_stage_index >= 0 && isset($stages[$current_stage_index]))
|
||||
? $stages[$current_stage_index]
|
||||
: array(
|
||||
'key' => 'inactive',
|
||||
'label_cn' => '当前未在招新时段',
|
||||
'label_en' => 'Recruitment is currently closed',
|
||||
'short_cn' => '',
|
||||
'short_en' => '',
|
||||
'status' => 'inactive',
|
||||
'range_cn' => '请关注后续通知',
|
||||
'range_en' => 'Please check later updates',
|
||||
'start_ts' => null,
|
||||
'end_ts' => null,
|
||||
);
|
||||
|
||||
$is_registration_open = itstudio_join_is_in_window($now, $registration_start, $registration_end);
|
||||
|
||||
$is_query_open = false;
|
||||
if ($registration_start instanceof DateTimeImmutable) {
|
||||
if ($notice_end instanceof DateTimeImmutable) {
|
||||
$is_query_open = itstudio_join_is_in_window($now, $registration_start, $notice_end);
|
||||
} else {
|
||||
$is_query_open = ($now >= $registration_start);
|
||||
}
|
||||
}
|
||||
|
||||
$is_notice_open = itstudio_join_is_in_window($now, $notice_start, $notice_end);
|
||||
$current_stage_photo_url = itstudio_join_get_stage_photo_url($settings, (string) ($current_stage['key'] ?? ''));
|
||||
if ($current_stage_photo_url === '') {
|
||||
$current_stage_photo_url = get_template_directory_uri() . '/resources/it_logo_2024.svg';
|
||||
}
|
||||
|
||||
$cached = array(
|
||||
'settings' => $settings,
|
||||
'timezone' => $timezone->getName(),
|
||||
'recruitment_year' => $recruitment_year,
|
||||
'now_ts' => (int) $now->format('U') * 1000,
|
||||
'stages' => $stages,
|
||||
'current_stage_index' => $current_stage_index,
|
||||
'current_stage' => $current_stage,
|
||||
'is_registration_open' => $is_registration_open,
|
||||
'is_query_open' => $is_query_open,
|
||||
'is_notice_open' => $is_notice_open,
|
||||
'current_stage_photo_url' => $current_stage_photo_url,
|
||||
'query_deadline_cn' => $notice_end instanceof DateTimeImmutable ? $notice_end->format('Y-m-d H:i') : '',
|
||||
'query_deadline_en' => $notice_end instanceof DateTimeImmutable ? $notice_end->format('M j, Y H:i') : '',
|
||||
);
|
||||
|
||||
return $cached;
|
||||
}
|
||||
|
||||
function itstudio_join_get_frontend_payload() {
|
||||
$runtime = itstudio_join_get_runtime_data();
|
||||
return array(
|
||||
'nowTs' => (int) ($runtime['now_ts'] ?? 0),
|
||||
'currentStageIndex' => (int) ($runtime['current_stage_index'] ?? -1),
|
||||
'stages' => array_values(array_map(static function ($stage) {
|
||||
return array(
|
||||
'key' => (string) ($stage['key'] ?? ''),
|
||||
'labelCn' => (string) ($stage['label_cn'] ?? ''),
|
||||
'labelEn' => (string) ($stage['label_en'] ?? ''),
|
||||
'shortCn' => (string) ($stage['short_cn'] ?? ''),
|
||||
'shortEn' => (string) ($stage['short_en'] ?? ''),
|
||||
'status' => (string) ($stage['status'] ?? 'pending'),
|
||||
'rangeCn' => (string) ($stage['range_cn'] ?? ''),
|
||||
'rangeEn' => (string) ($stage['range_en'] ?? ''),
|
||||
'startTs' => isset($stage['start_ts']) ? $stage['start_ts'] : null,
|
||||
'endTs' => isset($stage['end_ts']) ? $stage['end_ts'] : null,
|
||||
);
|
||||
}, (array) ($runtime['stages'] ?? array()))),
|
||||
);
|
||||
}
|
||||
|
||||
function itstudio_join_enqueue_assets() {
|
||||
if (!itstudio_is_join_page_context()) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'itstudio-join-page',
|
||||
get_template_directory_uri() . '/assets/css/join-page.css',
|
||||
array('itstudio-content'),
|
||||
'1.1.0'
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'itstudio-animejs',
|
||||
get_template_directory_uri() . '/assets/js/vendor/anime.min.js',
|
||||
array(),
|
||||
'3.2.2',
|
||||
true
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'itstudio-join-canvas',
|
||||
get_template_directory_uri() . '/assets/js/join-canvas.js',
|
||||
array('itstudio-animejs'),
|
||||
'1.1.0',
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script('itstudio-join-canvas', 'itstudioJoinData', itstudio_join_get_frontend_payload());
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'itstudio_join_enqueue_assets', 30);
|
||||
|
||||
function itstudio_join_register_settings() {
|
||||
register_setting(
|
||||
'itstudio_join_settings_group',
|
||||
'itstudio_join_settings',
|
||||
array(
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'itstudio_join_sanitize_settings',
|
||||
'default' => itstudio_join_get_default_settings(),
|
||||
)
|
||||
);
|
||||
}
|
||||
add_action('admin_init', 'itstudio_join_register_settings');
|
||||
|
||||
function itstudio_join_register_settings_page() {
|
||||
add_options_page(
|
||||
'招新设置',
|
||||
'招新设置',
|
||||
'manage_options',
|
||||
'itstudio-join-settings',
|
||||
'itstudio_join_render_settings_page'
|
||||
);
|
||||
}
|
||||
add_action('admin_menu', 'itstudio_join_register_settings_page');
|
||||
|
||||
function itstudio_join_render_photo_field_row($field_key, $label, $settings) {
|
||||
$attachment_id = isset($settings[$field_key]) ? absint($settings[$field_key]) : 0;
|
||||
$preview_url = $attachment_id > 0 ? wp_get_attachment_image_url($attachment_id, 'medium_large') : '';
|
||||
?>
|
||||
<tr>
|
||||
<th scope="row"><label><?php echo esc_html($label); ?></label></th>
|
||||
<td>
|
||||
<input type="hidden" class="itstudio-join-photo-id" name="itstudio_join_settings[<?php echo esc_attr($field_key); ?>]" value="<?php echo esc_attr($attachment_id); ?>">
|
||||
<div class="itstudio-join-photo-preview-wrap" style="margin-bottom:10px;">
|
||||
<img class="itstudio-join-photo-preview" src="<?php echo esc_url($preview_url); ?>" alt="" style="max-width:300px;height:auto;border:1px solid #dcdcde;border-radius:8px;display:<?php echo $preview_url !== '' ? 'block' : 'none'; ?>;">
|
||||
</div>
|
||||
<button type="button" class="button itstudio-join-photo-upload"><?php esc_html_e('上传 / 选择图片', 'itstudio'); ?></button>
|
||||
<button type="button" class="button-link-delete itstudio-join-photo-clear" style="margin-left:8px;<?php echo $preview_url !== '' ? '' : 'display:none;'; ?>"><?php esc_html_e('移除', 'itstudio'); ?></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
function itstudio_join_render_settings_page() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_media();
|
||||
|
||||
$settings = itstudio_join_get_settings();
|
||||
$runtime = itstudio_join_get_runtime_data();
|
||||
$formidable_active = shortcode_exists('formidable') || class_exists('FrmFormsController');
|
||||
$smtp_active = class_exists('\WPMailSMTP\WP') || defined('WPMS_PLUGIN_VER');
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>爱特工作室招新设置</h1>
|
||||
<p>用于配置「加入我们」页面的时间节点、表单和公示视图。</p>
|
||||
|
||||
<?php if (!$formidable_active) : ?>
|
||||
<div class="notice notice-warning inline">
|
||||
<p><strong>提示:</strong>未检测到 Formidable Forms 插件。报名表单、查询表单、公示视图将无法在前台渲染。</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$smtp_active) : ?>
|
||||
<div class="notice notice-warning inline">
|
||||
<p><strong>提示:</strong>未检测到 WP Mail SMTP 插件。建议启用后再开放报名邮件通知。</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="options.php">
|
||||
<?php settings_fields('itstudio_join_settings_group'); ?>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><label for="itstudio_join_registration_start">报名开始时间</label></th>
|
||||
<td>
|
||||
<input type="datetime-local" id="itstudio_join_registration_start" name="itstudio_join_settings[registration_start]" value="<?php echo esc_attr((string) $settings['registration_start']); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="itstudio_join_registration_end">报名结束时间</label></th>
|
||||
<td>
|
||||
<input type="datetime-local" id="itstudio_join_registration_end" name="itstudio_join_settings[registration_end]" value="<?php echo esc_attr((string) $settings['registration_end']); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="itstudio_join_first_interview_date">第一次面试日期</label></th>
|
||||
<td>
|
||||
<input type="date" id="itstudio_join_first_interview_date" name="itstudio_join_settings[first_interview_date]" value="<?php echo esc_attr((string) $settings['first_interview_date']); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="itstudio_join_second_interview_date">第二次面试日期</label></th>
|
||||
<td>
|
||||
<input type="date" id="itstudio_join_second_interview_date" name="itstudio_join_settings[second_interview_date]" value="<?php echo esc_attr((string) $settings['second_interview_date']); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="itstudio_join_notice_start_date">录取公示开始日期</label></th>
|
||||
<td>
|
||||
<input type="date" id="itstudio_join_notice_start_date" name="itstudio_join_settings[notice_start_date]" value="<?php echo esc_attr((string) $settings['notice_start_date']); ?>" class="regular-text">
|
||||
<p class="description">公示会自动持续 7 天(开始日 + 后续 6 天)。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php itstudio_join_render_photo_field_row('photo_registration', '报名阶段图片', $settings); ?>
|
||||
<?php itstudio_join_render_photo_field_row('photo_first_interview', '第一次面试图片', $settings); ?>
|
||||
<?php itstudio_join_render_photo_field_row('photo_assessment', '国庆能力摸底图片', $settings); ?>
|
||||
<?php itstudio_join_render_photo_field_row('photo_second_interview', '第二次面试图片', $settings); ?>
|
||||
<?php itstudio_join_render_photo_field_row('photo_public_notice', '录取公示阶段图片', $settings); ?>
|
||||
<?php itstudio_join_render_photo_field_row('photo_inactive', '非招新时段图片', $settings); ?>
|
||||
<tr>
|
||||
<th scope="row"><label for="itstudio_join_signup_shortcode">报名表单 Shortcode</label></th>
|
||||
<td>
|
||||
<input type="text" id="itstudio_join_signup_shortcode" name="itstudio_join_settings[signup_form_shortcode]" value="<?php echo esc_attr((string) $settings['signup_form_shortcode']); ?>" class="regular-text code">
|
||||
<p class="description">示例:<code>[formidable id="12"]</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="itstudio_join_query_shortcode">结果查询 Shortcode</label></th>
|
||||
<td>
|
||||
<input type="text" id="itstudio_join_query_shortcode" name="itstudio_join_settings[query_form_shortcode]" value="<?php echo esc_attr((string) $settings['query_form_shortcode']); ?>" class="regular-text code">
|
||||
<p class="description">示例:<code>[formidable id="13"]</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="itstudio_join_notice_shortcode">公示视图 Shortcode</label></th>
|
||||
<td>
|
||||
<input type="text" id="itstudio_join_notice_shortcode" name="itstudio_join_settings[notice_view_shortcode]" value="<?php echo esc_attr((string) $settings['notice_view_shortcode']); ?>" class="regular-text code">
|
||||
<p class="description">示例:<code>[display-frm-data id="5"]</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button('保存设置'); ?>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
<h2>阶段预览</h2>
|
||||
<p>国庆能力摸底阶段固定为每年 10 月 1 日至 10 月 7 日,年份自动取报名开始年份(未配置则取公示开始年份,再否则取当前年份)。</p>
|
||||
<table class="widefat striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>阶段</th>
|
||||
<th>时间</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ((array) ($runtime['stages'] ?? array()) as $stage) : ?>
|
||||
<?php
|
||||
$status = (string) ($stage['status'] ?? 'pending');
|
||||
$status_label = '待设置';
|
||||
if ($status === 'completed') {
|
||||
$status_label = '已完成';
|
||||
} elseif ($status === 'active') {
|
||||
$status_label = '进行中';
|
||||
} elseif ($status === 'upcoming') {
|
||||
$status_label = '未开始';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html((string) ($stage['label_cn'] ?? '')); ?></td>
|
||||
<td><?php echo esc_html((string) ($stage['range_cn'] ?? '')); ?></td>
|
||||
<td><?php echo esc_html($status_label); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
(function () {
|
||||
const uploadButtons = document.querySelectorAll('.itstudio-join-photo-upload');
|
||||
if (!uploadButtons.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
if (!window.wp || !wp.media) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = button.closest('td');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = row.querySelector('.itstudio-join-photo-id');
|
||||
const preview = row.querySelector('.itstudio-join-photo-preview');
|
||||
const clearBtn = row.querySelector('.itstudio-join-photo-clear');
|
||||
if (!input || !preview) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = wp.media({
|
||||
title: '选择阶段图片',
|
||||
button: { text: '使用此图片' },
|
||||
multiple: false,
|
||||
library: { type: 'image' },
|
||||
});
|
||||
|
||||
frame.on('select', () => {
|
||||
const selection = frame.state().get('selection').first();
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
const data = selection.toJSON();
|
||||
const imageUrl = (data.sizes && data.sizes.medium_large && data.sizes.medium_large.url)
|
||||
? data.sizes.medium_large.url
|
||||
: data.url;
|
||||
input.value = data.id || '';
|
||||
preview.src = imageUrl || '';
|
||||
preview.style.display = imageUrl ? 'block' : 'none';
|
||||
if (clearBtn) {
|
||||
clearBtn.style.display = imageUrl ? 'inline' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
frame.open();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.itstudio-join-photo-clear').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const row = button.closest('td');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const input = row.querySelector('.itstudio-join-photo-id');
|
||||
const preview = row.querySelector('.itstudio-join-photo-preview');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
if (preview) {
|
||||
preview.src = '';
|
||||
preview.style.display = 'none';
|
||||
}
|
||||
button.style.display = 'none';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
<?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();
|
||||
|
||||
$is_registration_open = !empty($join_runtime['is_registration_open']);
|
||||
$is_query_open = !empty($join_runtime['is_query_open']);
|
||||
$is_notice_open = !empty($join_runtime['is_notice_open']);
|
||||
|
||||
$query_deadline_cn = isset($join_runtime['query_deadline_cn']) ? (string) $join_runtime['query_deadline_cn'] : '';
|
||||
$query_deadline_en = isset($join_runtime['query_deadline_en']) ? (string) $join_runtime['query_deadline_en'] : '';
|
||||
|
||||
$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_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'] ?? ''));
|
||||
$query_shortcode = trim((string) ($join_settings['query_form_shortcode'] ?? ''));
|
||||
$notice_shortcode = trim((string) ($join_settings['notice_view_shortcode'] ?? ''));
|
||||
|
||||
$has_formidable = shortcode_exists('formidable') || class_exists('FrmFormsController');
|
||||
?>
|
||||
|
||||
<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>
|
||||
|
||||
<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="当前招新阶段" data-en="Current Recruitment Stage">当前招新阶段</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="join-wave-layer">
|
||||
<canvas id="joinProgressCanvas" class="join-progress-canvas" aria-hidden="true"></canvas>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol class="join-stage-list">
|
||||
<?php foreach ($join_stages as $stage) : ?>
|
||||
<?php
|
||||
$stage_status = isset($stage['status']) ? (string) $stage['status'] : 'pending';
|
||||
$status_cn = '待设置';
|
||||
$status_en = 'Pending';
|
||||
if ($stage_status === 'completed') {
|
||||
$status_cn = '已完成';
|
||||
$status_en = 'Completed';
|
||||
} elseif ($stage_status === 'active') {
|
||||
$status_cn = '进行中';
|
||||
$status_en = 'In Progress';
|
||||
} elseif ($stage_status === 'upcoming') {
|
||||
$status_cn = '未开始';
|
||||
$status_en = 'Upcoming';
|
||||
}
|
||||
$is_current_stage = !empty($current_stage['key']) && !empty($stage['key']) && ((string) $current_stage['key'] === (string) $stage['key']);
|
||||
?>
|
||||
<li class="join-stage-item is-<?php echo esc_attr($stage_status); ?><?php echo $is_current_stage ? ' is-current' : ''; ?>">
|
||||
<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"
|
||||
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>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="join-forms-grid">
|
||||
<article class="join-form-card">
|
||||
<header class="join-form-head">
|
||||
<h2 data-cn="报名表单" data-en="Registration Form">报名表单</h2>
|
||||
<p data-cn="仅在报名阶段开放" data-en="Available during registration stage only.">仅在报名阶段开放</p>
|
||||
</header>
|
||||
<div class="join-form-content">
|
||||
<?php if (!$is_registration_open) : ?>
|
||||
<p class="join-form-tip" data-cn="当前不在报名时间段 请关注后续通知" data-en="Registration is currently closed.">当前不在报名时间段 请关注后续通知</p>
|
||||
<?php elseif (!$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>
|
||||
|
||||
<article class="join-form-card">
|
||||
<header class="join-form-head">
|
||||
<h2 data-cn="结果查询" data-en="Progress Lookup">结果查询</h2>
|
||||
<p data-cn="报名开始后至公示结束前可查询进度" data-en="Available from registration start until the end of public notice.">报名开始后至公示结束前可查询进度</p>
|
||||
</header>
|
||||
<div class="join-form-content">
|
||||
<?php if (!$is_query_open) : ?>
|
||||
<p class="join-form-tip" data-cn="当前查询通道未开放或已关闭" data-en="Lookup is currently unavailable.">当前查询通道未开放或已关闭</p>
|
||||
<?php elseif (!$has_formidable) : ?>
|
||||
<p class="join-form-tip" data-cn="未检测到 Formidable Forms 插件 请先启用插件" data-en="Formidable Forms is not active.">未检测到 Formidable Forms 插件 请先启用插件</p>
|
||||
<?php elseif ($query_shortcode === '') : ?>
|
||||
<p class="join-form-tip" data-cn="请在 设置 > 招新设置 中填写查询表单 Shortcode" data-en="Please configure the lookup form shortcode in Settings > Recruitment Settings.">请在 设置 > 招新设置 中填写查询表单 Shortcode</p>
|
||||
<?php else : ?>
|
||||
<?php echo do_shortcode($query_shortcode); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($query_deadline_cn !== '' || $query_deadline_en !== '') : ?>
|
||||
<footer
|
||||
class="join-form-footnote"
|
||||
data-cn="<?php echo esc_attr('查询截止时间 ' . $query_deadline_cn); ?>"
|
||||
data-en="<?php echo esc_attr('Lookup closes at: ' . $query_deadline_en); ?>"
|
||||
>
|
||||
<?php echo esc_html('查询截止时间 ' . $query_deadline_cn); ?>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
|
||||
<article class="join-form-card join-form-card-full">
|
||||
<header class="join-form-head">
|
||||
<h2 data-cn="录取结果公示" data-en="Admission Public Notice">录取结果公示</h2>
|
||||
<p data-cn="公示阶段自动展示 公示期为 7 天" data-en="Published automatically during the 7-day notice window.">公示阶段自动展示 公示期为 7 天</p>
|
||||
</header>
|
||||
<div class="join-form-content">
|
||||
<?php if (!$is_notice_open) : ?>
|
||||
<p class="join-form-tip" data-cn="当前未进入公示阶段" data-en="Public notice is not active yet.">当前未进入公示阶段</p>
|
||||
<?php elseif (!$has_formidable) : ?>
|
||||
<p class="join-form-tip" data-cn="未检测到 Formidable Forms 插件 请先启用插件" data-en="Formidable Forms is not active.">未检测到 Formidable Forms 插件 请先启用插件</p>
|
||||
<?php elseif ($notice_shortcode === '') : ?>
|
||||
<p class="join-form-tip" data-cn="请在 设置 > 招新设置 中填写公示视图 Shortcode" data-en="Please configure the notice view shortcode in Settings > Recruitment Settings.">请在 设置 > 招新设置 中填写公示视图 Shortcode</p>
|
||||
<?php else : ?>
|
||||
<?php echo do_shortcode($notice_shortcode); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php get_footer(); ?>
|
||||
Reference in New Issue
Block a user