浅做了一下加入我们

This commit is contained in:
2026-03-05 21:51:37 +08:00
parent 9f001635b3
commit a683ab35d9
5 changed files with 2078 additions and 0 deletions
+66
View File
@@ -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.
+475
View File
@@ -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;
}
}
+496
View File
@@ -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
View File
@@ -1104,3 +1104,829 @@ function itstudio_track_post_views() {
update_post_meta($post_id, 'itstudio_views', $views); update_post_meta($post_id, 'itstudio_views', $views);
} }
add_action('template_redirect', 'itstudio_track_post_views', 20); 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
View File
@@ -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(); ?>