From 11ac498de38dd9e975060f8aa0918f34054e7742 Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Fri, 6 Mar 2026 21:12:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8A=A0=E5=85=A5=E6=88=91?= =?UTF-8?q?=E4=BB=AC=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新了招新新闻 优化招生进度样式 优化录取进度查询 --- assets/css/join-page.css | 614 ++++++++++++++++++++++-- assets/js/join-canvas.js | 173 ++++++- functions.php | 983 +++++++++++++++++++++++++++++++++++++-- page-join.php | 390 ++++++++++++---- 4 files changed, 1968 insertions(+), 192 deletions(-) diff --git a/assets/css/join-page.css b/assets/css/join-page.css index 7f12f36..56a013c 100644 --- a/assets/css/join-page.css +++ b/assets/css/join-page.css @@ -13,6 +13,72 @@ color: var(--color-primary); } +.join-submit-notice { + margin: -6px 0 14px; + padding: 10px 12px; + border: 1px solid color-mix(in srgb, #2f82ff 42%, var(--border-default) 58%); + border-left-width: 3px; + border-radius: 4px; + background: color-mix(in srgb, #2f82ff 10%, var(--bg-card) 90%); + color: var(--text-primary); + font-size: 0.9rem; + line-height: 1.5; +} + +.join-news-strip { + margin-bottom: 20px; + padding: 0 0 10px; +} + +.join-news-strip-track { + display: flex; + gap: 14px; +} + +.join-news-item { + --join-news-cols: 5; + flex: 0 0 calc((100% - (var(--join-news-cols) - 1) * 14px) / var(--join-news-cols)); + width: calc((100% - (var(--join-news-cols) - 1) * 14px) / var(--join-news-cols)); + min-width: 0; + padding-right: 12px; + border-right: 1px solid color-mix(in srgb, var(--border-default) 82%, transparent); +} + +.join-news-item:last-child { + border-right: 0; + padding-right: 0; +} + +.join-news-item-title { + margin: 0; + font-size: 0.94rem; + line-height: 1.38; + font-weight: 700; +} + +.join-news-item-title a { + color: var(--text-primary); + text-decoration: none; +} + +.join-news-item-title a:hover, +.join-news-item-title a:focus-visible { + color: var(--color-primary); +} + +.join-news-item-excerpt { + margin: 7px 0 0; + color: var(--text-secondary); + font-size: 0.82rem; + line-height: 1.5; +} + +.join-news-strip-empty { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; +} + .join-hero { display: flex; flex-direction: column; @@ -22,7 +88,7 @@ .join-canvas-shell { position: relative; - border: 1px solid var(--border-default); + height: clamp(280px, 31vw, 440px); border-radius: 18px; overflow: hidden; background: color-mix(in srgb, var(--bg-card) 94%, transparent); @@ -30,21 +96,25 @@ } .join-stage-photo-frame { - position: relative; + position: absolute; + inset: 0; overflow: hidden; } .join-stage-photo { width: 100%; - height: clamp(220px, 28vw, 360px); + height: 100%; object-fit: cover; display: block; } .join-wave-layer { - position: relative; - margin-top: clamp(-20px, -2.4vw, -12px); - z-index: 1; + position: absolute; + left: 0; + right: 0; + bottom: 0; + margin-top: 0; + z-index: 2; } .join-progress-canvas { @@ -107,11 +177,18 @@ } .join-wave-mark.is-lighthouse { - width: clamp(22px, 2vw, 30px); - transform: translate(-50%, -80%); + width: clamp(54px, 5.1vw, 81px); + transform: translate(-50%, -82%); transform-origin: 50% 96%; } +.join-wave-mark.is-lighthouse.is-docked { + top: auto; + bottom: -1px; + left: auto; + transform: translate(-50%, 0); +} + .join-wave-mark.is-active { filter: drop-shadow(0 0 10px color-mix(in srgb, var(--color-primary) 36%, transparent)); } @@ -227,16 +304,105 @@ .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); + left: 0; + right: auto; + width: min(44vw, 360px); + z-index: 3; + isolation: isolate; + overflow: hidden; + max-width: 100%; + padding: 12px 22px 12px 24px; + border-radius: 0; + border: 0; + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--bg-body) 78%, transparent) 0%, + color-mix(in srgb, var(--bg-body) 78%, transparent) 34%, + color-mix(in srgb, var(--bg-body) 48%, transparent) 50%, + color-mix(in srgb, var(--bg-body) 16%, transparent) 64%, + rgba(0, 0, 0, 0) 78%, + rgba(0, 0, 0, 0) 100% + ); +} + +.join-canvas-overlay.is-enter-animate { + animation: join-overlay-slide-in 720ms cubic-bezier(0.2, 0.78, 0.18, 1) both; + will-change: transform, opacity; +} + +@keyframes join-overlay-slide-in { + from { + transform: translateX(-112%); + opacity: 0.24; + } + + to { + transform: translateX(0); + opacity: 1; + } +} + +.join-canvas-overlay::before { + content: ""; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + opacity: 0.88; + mix-blend-mode: soft-light; + background: + linear-gradient( + 90deg, + rgba(255, 255, 255, 0.12) 0%, + rgba(255, 255, 255, 0.06) 42%, + rgba(255, 255, 255, 0.03) 60%, + rgba(255, 255, 255, 0) 100% + ), + repeating-linear-gradient( + 135deg, + rgba(255, 255, 255, 0.05) 0 1px, + rgba(255, 255, 255, 0) 1px 3px + ), + repeating-linear-gradient( + 45deg, + rgba(0, 0, 0, 0.03) 0 1px, + rgba(0, 0, 0, 0) 1px 4px + ); + backdrop-filter: blur(9px) saturate(130%); + -webkit-backdrop-filter: blur(9px) saturate(130%); + -webkit-mask-image: linear-gradient( + 90deg, + #000 0%, + #000 56%, + rgba(0, 0, 0, 0.6) 66%, + rgba(0, 0, 0, 0) 78%, + rgba(0, 0, 0, 0) 100% + ); + mask-image: linear-gradient( + 90deg, + #000 0%, + #000 56%, + rgba(0, 0, 0, 0.6) 66%, + rgba(0, 0, 0, 0) 78%, + rgba(0, 0, 0, 0) 100% + ); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; +} + +.join-canvas-overlay > * { + position: relative; + z-index: 1; +} + +@media (prefers-reduced-motion: reduce) { + .join-canvas-overlay.is-enter-animate { + animation: none; + transform: none; + opacity: 1; + } } .join-current-label { @@ -259,6 +425,14 @@ color: var(--text-secondary); } +.join-current-location { + margin: 4px 0 0; + font-size: 0.88rem; + color: color-mix(in srgb, var(--text-primary) 84%, var(--text-secondary) 16%); + line-height: 1.45; + overflow-wrap: anywhere; +} + .join-stage-list { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); @@ -269,22 +443,77 @@ } .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; + position: relative; + border: 1px solid color-mix(in srgb, var(--border-default) 90%, transparent); + border-radius: 4px; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-card) 90%, transparent) 0%, + color-mix(in srgb, var(--bg-card) 97%, transparent) 100% + ); + padding: 11px 12px; min-height: 98px; + overflow: hidden; + transition: border-color 0.22s ease, background 0.22s ease, box-shadow 0.22s ease, transform 0.22s ease; } +.join-stage-item::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 0; + height: 2px; + background: color-mix(in srgb, var(--border-default) 75%, transparent); + opacity: 0.86; +} + +.join-stage-item.is-active, .join-stage-item.is-current { border-color: color-mix(in srgb, var(--color-primary) 52%, var(--border-default) 48%); - box-shadow: var(--shadow-sm); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--color-primary) 10%, var(--bg-card) 90%) 0%, + color-mix(in srgb, var(--bg-card) 96%, transparent) 100% + ); + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--color-primary) 20%, transparent), + 0 12px 22px -18px color-mix(in srgb, var(--color-primary) 46%, transparent); + transform: translateY(-1px); +} + +.join-stage-item.is-active::before, +.join-stage-item.is-current::before { + height: 3px; + opacity: 1; + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--color-primary) 84%, #7ed7ff 16%) 0%, + color-mix(in srgb, var(--color-primary) 52%, #7ed7ff 48%) 100% + ); } .join-stage-item.is-completed .join-stage-status { color: #0f8a4f; } +.join-stage-item .join-stage-status.is-query-ready { + color: var(--color-primary); +} + +.join-stage-item.is-completed { + border-color: color-mix(in srgb, #0f8a4f 48%, var(--border-default) 52%); + background: linear-gradient( + 180deg, + color-mix(in srgb, #0f8a4f 7%, var(--bg-card) 93%) 0%, + color-mix(in srgb, var(--bg-card) 96%, transparent) 100% + ); +} + +.join-stage-item.is-completed::before { + background: color-mix(in srgb, #0f8a4f 65%, transparent); +} + .join-stage-item.is-active .join-stage-status { color: var(--color-primary); } @@ -294,6 +523,34 @@ color: var(--text-secondary); } +.join-stage-item.is-upcoming, +.join-stage-item.is-pending { + border-color: color-mix(in srgb, var(--border-default) 88%, transparent); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-card) 95%, transparent) 0%, + color-mix(in srgb, var(--bg-card) 99%, transparent) 100% + ); +} + +.join-stage-item.is-upcoming::after, +.join-stage-item.is-pending::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 24px; + background: + repeating-linear-gradient( + -45deg, + transparent 0 10px, + color-mix(in srgb, var(--border-default) 35%, transparent) 10px 12px + ); + opacity: 0.2; + pointer-events: none; +} + .join-stage-title-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -328,17 +585,34 @@ line-height: 1.45; } +.join-stage-location { + margin: 4px 0 0; + color: color-mix(in srgb, var(--text-primary) 88%, var(--text-secondary) 12%); + font-size: 0.76rem; + line-height: 1.42; + overflow-wrap: anywhere; +} + .join-forms-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } +.join-forms-grid.is-single { + grid-template-columns: 1fr; +} + .join-form-card { - border: 1px solid var(--border-default); + border: 1px solid color-mix(in srgb, var(--border-default) 86%, transparent); border-radius: 14px; - background: color-mix(in srgb, var(--bg-card) 95%, transparent); + background: linear-gradient( + 145deg, + color-mix(in srgb, var(--bg-card) 82%, #1b2430 18%) 0%, + color-mix(in srgb, var(--bg-card) 94%, transparent) 100% + ); padding: 18px; + box-shadow: 0 16px 30px -24px color-mix(in srgb, #000000 65%, transparent); } .join-form-card-full { @@ -353,8 +627,9 @@ .join-form-head p { margin: 6px 0 0; - color: var(--text-secondary); - font-size: 0.9rem; + color: color-mix(in srgb, var(--text-primary) 72%, var(--text-secondary) 28%); + font-size: 0.94rem; + line-height: 1.55; } .join-form-content { @@ -365,6 +640,89 @@ margin: 0; } +.join-progress-query-form { + display: grid; + gap: 12px; +} + +.join-progress-query-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.join-progress-query-field { + display: grid; + gap: 5px; +} + +.join-progress-query-field span { + font-size: 0.82rem; + color: color-mix(in srgb, var(--text-primary) 85%, var(--text-secondary) 15%); +} + +.join-progress-query-field input { + width: 100%; + border: 1px solid color-mix(in srgb, var(--border-default) 86%, transparent); + border-radius: 10px; + background: color-mix(in srgb, var(--bg-card) 90%, transparent); + color: var(--text-primary); + padding: 9px 10px; + line-height: 1.3; +} + +.join-progress-query-field input:focus { + outline: none; + border-color: color-mix(in srgb, var(--color-primary) 55%, var(--border-default) 45%); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 16%, transparent); +} + +.join-progress-query-actions { + display: flex; + justify-content: flex-start; +} + +.join-progress-query-submit { + border: 1px solid color-mix(in srgb, var(--color-primary) 48%, var(--border-default) 52%); + border-radius: 10px; + background: color-mix(in srgb, var(--color-primary) 18%, var(--bg-card) 82%); + color: var(--text-primary); + padding: 9px 16px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; +} + +.join-progress-query-submit:hover { + background: color-mix(in srgb, var(--color-primary) 24%, var(--bg-card) 76%); +} + +.join-progress-query-feedback { + margin: 2px 0 0; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--border-default) 80%, transparent); + background: color-mix(in srgb, var(--bg-card) 90%, transparent); + color: var(--text-primary); + font-size: 0.88rem; + line-height: 1.55; +} + +.join-progress-query-feedback.is-success { + border-color: color-mix(in srgb, #0f8a4f 58%, var(--border-default) 42%); + background: color-mix(in srgb, #0f8a4f 14%, var(--bg-card) 86%); +} + +.join-progress-query-feedback.is-warning { + border-color: color-mix(in srgb, #d89a1b 58%, var(--border-default) 42%); + background: color-mix(in srgb, #d89a1b 14%, var(--bg-card) 86%); +} + +.join-progress-query-feedback.is-error { + border-color: color-mix(in srgb, #c34646 58%, var(--border-default) 42%); + background: color-mix(in srgb, #c34646 14%, var(--bg-card) 86%); +} + .join-form-content .frm_forms { margin: 0; } @@ -374,6 +732,62 @@ max-width: none; } +.join-form-content .frm_style_formidable-style.with_frm_style, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_form_fields, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_form_field, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_field_wrapper { + color: color-mix(in srgb, var(--text-primary) 95%, #ffffff 5%) !important; +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_form_field { + margin-bottom: 14px !important; +} + +.join-form-content .frm_style_formidable-style.with_frm_style label { + color: color-mix(in srgb, var(--text-primary) 95%, #ffffff 5%) !important; + opacity: 1 !important; +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_primary_label, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_section_heading h3, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_inline_box label, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_radio label, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_checkbox label, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_opt_container label { + color: color-mix(in srgb, var(--text-primary) 92%, #ffffff 8%) !important; + font-weight: 600 !important; + opacity: 1 !important; + letter-spacing: 0.01em; +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_required { + color: #ff6767 !important; +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_description, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_error, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_error_style { + color: color-mix(in srgb, var(--text-primary) 82%, var(--text-secondary) 18%) !important; +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_error_style, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_error { + border-left: 3px solid #ff6767 !important; + background: color-mix(in srgb, #ff6767 10%, transparent) !important; + padding: 8px 10px !important; + border-radius: 4px; +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_message, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_success_style, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_success { + border-left: 3px solid #42b983 !important; + background: color-mix(in srgb, #42b983 14%, transparent) !important; + color: color-mix(in srgb, var(--text-primary) 94%, #ffffff 6%) !important; + border-radius: 4px; + padding: 10px 12px !important; +} + .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"], @@ -382,17 +796,114 @@ .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); + background: color-mix(in srgb, var(--bg-body) 84%, #ffffff 16%) !important; + border: 1px solid color-mix(in srgb, var(--border-default) 42%, var(--color-primary) 58%) !important; + color: color-mix(in srgb, var(--text-primary) 96%, #ffffff 4%) !important; + min-height: 42px; + border-radius: 8px !important; + padding: 10px 12px !important; +} + +.join-form-content .frm_style_formidable-style.with_frm_style input::placeholder, +.join-form-content .frm_style_formidable-style.with_frm_style textarea::placeholder { + color: color-mix(in srgb, var(--text-secondary) 78%, var(--text-primary) 22%) !important; + opacity: 1; +} + +.join-form-content .frm_style_formidable-style.with_frm_style input[type="text"]:focus, +.join-form-content .frm_style_formidable-style.with_frm_style input[type="email"]:focus, +.join-form-content .frm_style_formidable-style.with_frm_style input[type="number"]:focus, +.join-form-content .frm_style_formidable-style.with_frm_style input[type="tel"]:focus, +.join-form-content .frm_style_formidable-style.with_frm_style input[type="date"]:focus, +.join-form-content .frm_style_formidable-style.with_frm_style input[type="datetime-local"]:focus, +.join-form-content .frm_style_formidable-style.with_frm_style textarea:focus, +.join-form-content .frm_style_formidable-style.with_frm_style select:focus { + border-color: color-mix(in srgb, var(--color-primary) 76%, #8ec9ff 24%) !important; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 24%, transparent) !important; + outline: none !important; +} + +.join-form-content .frm_style_formidable-style.with_frm_style input[type="checkbox"], +.join-form-content .frm_style_formidable-style.with_frm_style input[type="radio"] { + accent-color: var(--color-primary); + transform: scale(1.06); +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_checkbox label, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_radio label, +.join-form-content .frm_style_formidable-style.with_frm_style .frm_opt_container label { + line-height: 1.55 !important; + font-size: 0.96rem !important; +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_submit { + display: flex !important; + flex-wrap: wrap; + gap: 10px; + align-items: center; } .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); + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--color-primary) 82%, #9cd3ff 18%) !important; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--color-primary) 82%, #78c3ff 18%) 0%, + color-mix(in srgb, var(--color-primary) 92%, #5ea7f8 8%) 100% + ) !important; + color: #ffffff !important; + font-weight: 700 !important; + letter-spacing: 0.01em; + min-height: 44px; + padding: 0 20px !important; + box-shadow: 0 8px 18px -12px color-mix(in srgb, var(--color-primary) 65%, transparent); +} + +.join-form-content .frm_style_formidable-style.with_frm_style input[type="submit"]:hover, +.join-form-content .frm_style_formidable-style.with_frm_style button.frm_button_submit:hover { + filter: brightness(1.05); +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_save_draft, +.join-form-content .frm_style_formidable-style.with_frm_style button[name*="draft"], +.join-form-content .frm_style_formidable-style.with_frm_style input[name*="draft"], +.join-form-content .frm_style_formidable-style.with_frm_style button[value*="草稿"], +.join-form-content .frm_style_formidable-style.with_frm_style input[value*="草稿"], +.join-form-content .frm_style_formidable-style.with_frm_style button[value*="Draft"], +.join-form-content .frm_style_formidable-style.with_frm_style input[value*="Draft"] { + border: 1px solid color-mix(in srgb, var(--border-default) 30%, var(--color-primary) 70%) !important; + background: color-mix(in srgb, var(--bg-card) 88%, transparent) !important; + color: var(--color-primary) !important; + box-shadow: none !important; +} + +.join-form-content .frm_style_formidable-style.with_frm_style .frm_save_draft:hover, +.join-form-content .frm_style_formidable-style.with_frm_style button[name*="draft"]:hover, +.join-form-content .frm_style_formidable-style.with_frm_style input[name*="draft"]:hover { + background: color-mix(in srgb, var(--bg-card) 74%, var(--color-primary) 26%) !important; + color: #ffffff !important; +} + +[data-theme="light"] .join-form-card { + background: linear-gradient( + 145deg, + color-mix(in srgb, #ffffff 94%, #e8f2ff 6%) 0%, + color-mix(in srgb, #ffffff 98%, #e8f2ff 2%) 100% + ); + box-shadow: 0 16px 30px -24px rgba(120, 140, 165, 0.35); +} + +[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style input[type="text"], +[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style input[type="email"], +[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style input[type="number"], +[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style input[type="tel"], +[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style input[type="date"], +[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style input[type="datetime-local"], +[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style textarea, +[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style select { + background: #ffffff !important; + border-color: color-mix(in srgb, #8bbcff 46%, var(--border-default) 54%) !important; } .join-form-tip { @@ -422,6 +933,26 @@ padding: 30px 0 60px; } + .join-news-strip { + margin-bottom: 16px; + } + + .join-news-strip-track { + display: flex; + gap: 12px; + overflow-x: auto; + padding-bottom: 6px; + scrollbar-width: thin; + } + + .join-news-item { + flex: 0 0 220px; + padding-right: 0; + border-right: 0; + border-bottom: 1px solid color-mix(in srgb, var(--border-default) 82%, transparent); + padding-bottom: 8px; + } + .join-stage-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -446,13 +977,14 @@ .join-canvas-overlay { top: 12px; - left: 12px; - right: 12px; - padding: 10px 11px; + left: 0; + right: auto; + width: min(86vw, 272px); + padding: 10px 16px 10px 18px; } - .join-stage-photo { - height: 230px; + .join-canvas-shell { + height: 240px; } .join-current-stage { @@ -472,4 +1004,8 @@ .join-stage-item { min-height: 0; } + + .join-progress-query-grid { + grid-template-columns: 1fr; + } } diff --git a/assets/js/join-canvas.js b/assets/js/join-canvas.js index bad553e..3018b85 100644 --- a/assets/js/join-canvas.js +++ b/assets/js/join-canvas.js @@ -16,6 +16,8 @@ const waveFill = document.getElementById('joinWaveFill'); const waveBoat = document.getElementById('joinWaveBoat'); const waveMarks = document.getElementById('joinWaveMarks'); + const waveProgress = waveTrack ? waveTrack.closest('.join-wave-progress') : null; + const overlayPanel = document.querySelector('.join-canvas-overlay'); const joinData = window.itstudioJoinData && typeof window.itstudioJoinData === 'object' ? window.itstudioJoinData : {}; @@ -31,21 +33,24 @@ let trackLeft = 0; let trackWidth = 0; let stageMarkers = []; + let stageLayout = null; const waveShape1 = { base: 0.16, amp: 7.5, len: 280, speed: 0.9 }; const waveShape2 = { base: 0.44, amp: 6, len: 340, speed: 0.75 }; const waveShape3 = { base: 0.68, amp: 6.5, len: 400, speed: 0.62 }; + const DAY_MS = 24 * 60 * 60 * 1000; + const ONE_DAY_STAGE_WEIGHT_MS = DAY_MS * 1.8; const palettes = { light: { - wave1: 'rgba(198, 227, 255, 0.92)', - wave2: 'rgba(142, 193, 236, 0.4)', - wave3: 'rgba(84, 151, 214, 0.48)', + wave1: 'rgba(122, 178, 230, 0.95)', + wave2: 'rgba(73, 134, 197, 0.72)', + wave3: 'rgba(40, 96, 161, 0.78)', }, dark: { - wave1: 'rgba(132, 190, 242, 0.88)', - wave2: 'rgba(88, 151, 214, 0.4)', - wave3: 'rgba(56, 118, 183, 0.48)', + wave1: 'rgba(86, 146, 214, 0.92)', + wave2: 'rgba(49, 104, 172, 0.78)', + wave3: 'rgba(27, 73, 136, 0.82)', }, }; @@ -62,6 +67,61 @@ return Number.isFinite(numeric) ? numeric : null; } + function getStages() { + return Array.isArray(joinData.stages) ? joinData.stages : []; + } + + function getStageDurationMs(stage) { + const start = getNumber(stage ? stage.startTs : null); + const end = getNumber(stage ? stage.endTs : null); + if (start === null || end === null) { + return null; + } + return Math.max(0, end - start); + } + + function getEffectiveStageWeightMs(stage) { + const duration = getStageDurationMs(stage); + if (duration === null) { + return ONE_DAY_STAGE_WEIGHT_MS; + } + if (duration <= DAY_MS) { + return ONE_DAY_STAGE_WEIGHT_MS; + } + return duration; + } + + function buildStageLayout(stages) { + if (!stages.length) { + return { + markerByIndex: [], + endByIndex: [], + }; + } + + const weights = stages.map((stage) => getEffectiveStageWeightMs(stage)); + const totalWeight = Math.max(1, weights.reduce((sum, w) => sum + w, 0)); + const markerByIndex = []; + const endByIndex = []; + let cumulative = 0; + + for (let i = 0; i < stages.length; i += 1) { + markerByIndex[i] = clamp(cumulative / totalWeight, 0, 1); + cumulative += weights[i]; + endByIndex[i] = clamp(cumulative / totalWeight, 0, 1); + } + + endByIndex[endByIndex.length - 1] = 1; + return { markerByIndex, endByIndex }; + } + + function getStageLayout(stages) { + if (!stageLayout || !stageLayout.markerByIndex || stageLayout.markerByIndex.length !== stages.length) { + stageLayout = buildStageLayout(stages); + } + return stageLayout; + } + function getNavigationType() { if (window.performance && typeof window.performance.getEntriesByType === 'function') { const entries = window.performance.getEntriesByType('navigation'); @@ -90,6 +150,21 @@ return getNavigationType() !== 'reload'; } + function setupOverlayEntryAnimation() { + if (!overlayPanel) { + return; + } + + overlayPanel.classList.remove('is-enter-animate'); + if (!shouldAnimateEntry()) { + return; + } + + requestAnimationFrame(() => { + overlayPanel.classList.add('is-enter-animate'); + }); + } + function resize() { dpr = window.devicePixelRatio || 1; width = canvas.clientWidth; @@ -212,20 +287,34 @@ } function computeTargetProgress() { - const stages = Array.isArray(joinData.stages) ? joinData.stages : []; + const stages = getStages(); if (!stages.length) { return 0; } - const denominator = stages.length > 1 ? (stages.length - 1) : 1; + const layout = getStageLayout(stages); + const markerByIndex = layout.markerByIndex; + const endByIndex = layout.endByIndex; const currentIndex = getNumber(joinData.currentStageIndex); + const nowTs = getNumber(joinData.nowTs) || Date.now(); - // 有进行中阶段时,船严格对齐对应浮标节点。 - if (currentIndex !== null && currentIndex >= 0) { - return clamp(currentIndex / denominator, 0, 1); + // 有进行中阶段时,在该阶段对应区间内按时间连续推进。 + if (currentIndex !== null && currentIndex >= 0 && currentIndex < stages.length) { + const stage = stages[currentIndex]; + const startProgress = getNumber(markerByIndex[currentIndex]) ?? 0; + const endProgress = getNumber(endByIndex[currentIndex]) ?? startProgress; + const startTs = getNumber(stage ? stage.startTs : null); + const endTs = getNumber(stage ? stage.endTs : null); + + if (startTs !== null && endTs !== null && endTs > startTs) { + const ratio = clamp((nowTs - startTs) / (endTs - startTs), 0, 1); + return clamp(startProgress + ((endProgress - startProgress) * ratio), 0, 1); + } + + return clamp(startProgress, 0, 1); } - // 无进行中阶段时,停在最近已完成阶段节点。 + // 无进行中阶段时,停在最近已完成阶段的末端。 let lastCompletedIndex = -1; for (let i = 0; i < stages.length; i += 1) { if (stages[i] && stages[i].status === 'completed') { @@ -233,8 +322,41 @@ } } + // 阶段空档期:在“上一阶段浮标”和“下一阶段浮标”之间按时间线性推进。 if (lastCompletedIndex >= 0) { - return clamp(lastCompletedIndex / denominator, 0, 1); + let nextUpcomingIndex = -1; + for (let i = lastCompletedIndex + 1; i < stages.length; i += 1) { + if (stages[i] && stages[i].status === 'upcoming') { + nextUpcomingIndex = i; + break; + } + } + + if (nextUpcomingIndex >= 0) { + const completedStage = stages[lastCompletedIndex]; + const upcomingStage = stages[nextUpcomingIndex]; + const gapStartTs = getNumber(completedStage ? completedStage.endTs : null); + const gapEndTs = getNumber(upcomingStage ? upcomingStage.startTs : null); + const fromProgress = clamp(getNumber(markerByIndex[lastCompletedIndex]) ?? 0, 0, 1); + const toProgress = clamp(getNumber(markerByIndex[nextUpcomingIndex]) ?? fromProgress, 0, 1); + + if (gapStartTs !== null && gapEndTs !== null && gapEndTs > gapStartTs && nowTs > gapStartTs && nowTs < gapEndTs) { + const gapRatio = clamp((nowTs - gapStartTs) / (gapEndTs - gapStartTs), 0, 1); + return clamp(fromProgress + ((toProgress - fromProgress) * gapRatio), 0, 1); + } + + if (nowTs <= gapStartTs) { + return fromProgress; + } + + if (nowTs < gapEndTs) { + return clamp((fromProgress + toProgress) * 0.5, 0, 1); + } + } + } + + if (lastCompletedIndex >= 0) { + return clamp(getNumber(endByIndex[lastCompletedIndex]) ?? 0, 0, 1); } return 0; @@ -275,23 +397,29 @@ stageMarkers = []; waveMarks.innerHTML = ''; + if (waveProgress) { + waveProgress.querySelectorAll('.join-wave-mark.is-lighthouse.is-docked').forEach((node) => node.remove()); + } - const stages = Array.isArray(joinData.stages) ? joinData.stages : []; + const stages = getStages(); if (!stages.length) { return; } + const layout = getStageLayout(stages); + const markerByIndex = layout.markerByIndex; const currentIndex = getNumber(joinData.currentStageIndex); const safeCurrentIndex = currentIndex === null ? -1 : Math.round(currentIndex); - 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'); + const progress = isLighthouse + ? 1 + : clamp(getNumber(markerByIndex[index]) ?? 0, 0, 1); + marker.classList.add(isLighthouse ? 'is-lighthouse' : 'is-buoy'); if (index === safeCurrentIndex) { marker.classList.add('is-active'); @@ -304,7 +432,15 @@ const beams = isLighthouse ? Array.from(icon.querySelectorAll('.marker-lh-beam')) : []; - waveMarks.appendChild(marker); + if (isLighthouse && waveProgress) { + marker.classList.add('is-docked'); + marker.style.right = ''; + marker.style.left = 'calc(100% - clamp(18px, 2.2vw, 30px))'; + waveProgress.appendChild(marker); + } else { + marker.style.left = `${(progress * 100).toFixed(3)}%`; + waveMarks.appendChild(marker); + } stageMarkers.push({ element: marker, icon, @@ -490,6 +626,7 @@ }); targetProgress = computeTargetProgress(); + setupOverlayEntryAnimation(); renderStageMarks(); startWaves(); animateBoatToTarget(); diff --git a/functions.php b/functions.php index d3b0c6b..9efebb0 100644 --- a/functions.php +++ b/functions.php @@ -317,9 +317,279 @@ function itstudio_register_acf_fields() { 'active' => true, 'show_in_rest' => 1, )); + + acf_add_local_field_group(array( + 'key' => 'group_itstudio_recruitment_article', + 'title' => '招新文章标记', + 'fields' => array( + array( + 'key' => 'field_itstudio_is_recruitment_article', + 'label' => '是否为招新文章', + 'name' => 'itstudio_is_recruitment_article', + 'type' => 'true_false', + 'instructions' => '勾选后,该文章会在“加入我们”页面顶部新闻条中显示(按发布时间排序)。', + 'required' => 0, + 'default_value' => 0, + 'ui' => 1, + 'ui_on_text' => '是', + 'ui_off_text' => '否', + ), + ), + 'location' => array( + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'announcement', + ), + ), + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'news', + ), + ), + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'post', + ), + ), + ), + 'position' => 'side', + 'style' => 'default', + 'active' => true, + 'show_in_rest' => 1, + )); } add_action('acf/init', 'itstudio_register_acf_fields'); +function itstudio_get_recruitment_article_meta_key() { + return 'itstudio_is_recruitment_article'; +} + +function itstudio_normalize_recruitment_flag($value) { + if (is_bool($value)) { + return $value; + } + + if (is_numeric($value)) { + return ((int) $value) > 0; + } + + $value = strtolower(trim((string) $value)); + return in_array($value, array('1', 'true', 'yes', 'on', 'y'), true); +} + +function itstudio_is_recruitment_article($post_id) { + $post_id = (int) $post_id; + if ($post_id <= 0) { + return false; + } + + $meta_key = itstudio_get_recruitment_article_meta_key(); + $meta_keys = array( + $meta_key, + '_itstudio_is_recruitment_article', + 'itstudio_join_article', + 'join_recruitment_article', + 'is_recruitment_article', + ); + + foreach ($meta_keys as $key) { + $raw = get_post_meta($post_id, $key, true); + if ($raw !== '' && $raw !== null && itstudio_normalize_recruitment_flag($raw)) { + return true; + } + } + + if (function_exists('get_field')) { + $acf_keys = array( + 'itstudio_is_recruitment_article', + 'is_recruitment_article', + 'itstudio_join_article', + ); + foreach ($acf_keys as $acf_key) { + $raw = get_field($acf_key, $post_id, false); + if ($raw !== '' && $raw !== null && itstudio_normalize_recruitment_flag($raw)) { + return true; + } + } + } + + return false; +} + +function itstudio_add_recruitment_meta_boxes() { + $screens = array('announcement', 'news', 'post'); + foreach ($screens as $screen) { + add_meta_box( + 'itstudio_recruitment_flag', + __('招新文章', 'itstudio'), + 'itstudio_render_recruitment_meta_box', + $screen, + 'side', + 'high' + ); + } +} +add_action('add_meta_boxes', 'itstudio_add_recruitment_meta_boxes'); + +function itstudio_render_recruitment_meta_box($post) { + $meta_key = itstudio_get_recruitment_article_meta_key(); + $checked = itstudio_is_recruitment_article((int) $post->ID); + wp_nonce_field('itstudio_save_recruitment_meta', 'itstudio_recruitment_meta_nonce'); + ?> +

+ +

+

+ +

+ + format('Y'); + $display_year = isset($join_runtime['recruitment_year']) && is_numeric($join_runtime['recruitment_year']) + ? (int) $join_runtime['recruitment_year'] + : $now_year; + + $season_start_ts = null; + $season_end_ts = null; + $stages = isset($join_runtime['stages']) && is_array($join_runtime['stages']) ? $join_runtime['stages'] : array(); + foreach ($stages as $stage) { + if (!is_array($stage)) { + continue; + } + $start_ts = isset($stage['start_ts']) && is_numeric($stage['start_ts']) ? (int) $stage['start_ts'] : null; + $end_ts = isset($stage['end_ts']) && is_numeric($stage['end_ts']) ? (int) $stage['end_ts'] : null; + if ($start_ts !== null && ($season_start_ts === null || $start_ts < $season_start_ts)) { + $season_start_ts = $start_ts; + } + if ($end_ts !== null && ($season_end_ts === null || $end_ts > $season_end_ts)) { + $season_end_ts = $end_ts; + } + } + + $now_ts = (int) (isset($join_runtime['now_ts']) && is_numeric($join_runtime['now_ts']) ? $join_runtime['now_ts'] : ((int) $now->format('U') * 1000)); + if ($season_start_ts !== null && $now_ts < $season_start_ts && $display_year > $now_year) { + // 下年招新未开始:继续展示本年招新资讯 + $display_year = $now_year; + } + + $query = new WP_Query(array( + 'post_type' => array('announcement', 'news', 'post'), + 'post_status' => 'publish', + 'posts_per_page' => 80, + 'orderby' => 'date', + 'order' => 'DESC', + 'no_found_rows' => true, + 'ignore_sticky_posts' => true, + 'date_query' => array( + array( + 'year' => $display_year, + ), + ), + )); + + $items = array(); + if ($query->have_posts()) { + foreach ($query->posts as $post) { + if (count($items) >= $limit) { + break; + } + + $post_id = (int) $post->ID; + if (!itstudio_is_recruitment_article($post_id)) { + continue; + } + + $title = trim((string) get_the_title($post_id)); + $url = (string) get_permalink($post_id); + if ($title === '' || $url === '') { + continue; + } + + $excerpt = function_exists('itstudio_get_post_excerpt_chars') + ? itstudio_get_post_excerpt_chars($post_id, 72) + : wp_html_excerpt(trim(wp_strip_all_tags((string) get_post_field('post_excerpt', $post_id))), 72, '...'); + + if ($excerpt === '') { + $excerpt = '...'; + } + + $items[] = array( + 'id' => $post_id, + 'title' => $title, + 'excerpt' => $excerpt, + 'url' => $url, + 'date' => get_the_date('Y-m-d', $post_id), + 'type' => get_post_type($post_id), + ); + } + } + wp_reset_postdata(); + + return array( + 'display_year' => $display_year, + 'items' => $items, + ); +} + function itstudio_get_service_url_meta_key() { return '_itstudio_service_url'; } @@ -1121,8 +1391,21 @@ function itstudio_join_get_default_settings() { 'registration_start' => '', 'registration_end' => '', 'first_interview_date' => '', + 'first_interview_end' => '', + 'first_interview_location_cn' => '', + 'first_interview_location_en' => '', 'second_interview_date' => '', + 'second_interview_end' => '', + 'second_interview_location_cn' => '', + 'second_interview_location_en' => '', + 'assessment_start_date' => '', + 'assessment_end_date' => '', 'notice_start_date' => '', + 'result_registration_records' => '', + 'result_first_interview_records' => '', + 'result_assessment_records' => '', + 'result_second_interview_records' => '', + 'result_admission_records' => '', 'photo_registration' => 0, 'photo_first_interview' => 0, 'photo_assessment' => 0, @@ -1130,8 +1413,6 @@ function itstudio_join_get_default_settings() { 'photo_public_notice' => 0, 'photo_inactive' => 0, 'signup_form_shortcode' => '', - 'query_form_shortcode' => '', - 'notice_view_shortcode' => '', ); } @@ -1188,6 +1469,26 @@ function itstudio_join_sanitize_shortcode_value($value) { return trim(sanitize_text_field(wp_unslash($value))); } +function itstudio_join_sanitize_records_value($value) { + if (!is_string($value)) { + return ''; + } + + $value = sanitize_textarea_field(wp_unslash($value)); + $value = str_replace(array("\r\n", "\r"), "\n", $value); + $lines = explode("\n", $value); + $clean_lines = array(); + foreach ($lines as $line) { + $line = trim((string) $line); + if ($line === '') { + continue; + } + $clean_lines[] = $line; + } + + return implode("\n", $clean_lines); +} + function itstudio_join_sanitize_settings($input) { $defaults = itstudio_join_get_default_settings(); $input = is_array($input) ? $input : array(); @@ -1195,12 +1496,23 @@ function itstudio_join_sanitize_settings($input) { $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'] ?? ''), + 'first_interview_date' => itstudio_join_sanitize_datetime_local_value($input['first_interview_date'] ?? ''), + 'first_interview_end' => itstudio_join_sanitize_datetime_local_value($input['first_interview_end'] ?? ''), + 'first_interview_location_cn' => itstudio_join_sanitize_shortcode_value($input['first_interview_location_cn'] ?? ''), + 'first_interview_location_en' => itstudio_join_sanitize_shortcode_value($input['first_interview_location_en'] ?? ''), + 'second_interview_date' => itstudio_join_sanitize_datetime_local_value($input['second_interview_date'] ?? ''), + 'second_interview_end' => itstudio_join_sanitize_datetime_local_value($input['second_interview_end'] ?? ''), + 'second_interview_location_cn' => itstudio_join_sanitize_shortcode_value($input['second_interview_location_cn'] ?? ''), + 'second_interview_location_en' => itstudio_join_sanitize_shortcode_value($input['second_interview_location_en'] ?? ''), + 'assessment_start_date' => itstudio_join_sanitize_date_value($input['assessment_start_date'] ?? ''), + 'assessment_end_date' => itstudio_join_sanitize_date_value($input['assessment_end_date'] ?? ''), 'notice_start_date' => itstudio_join_sanitize_date_value($input['notice_start_date'] ?? ''), + 'result_registration_records' => itstudio_join_sanitize_records_value($input['result_registration_records'] ?? ''), + 'result_first_interview_records' => itstudio_join_sanitize_records_value($input['result_first_interview_records'] ?? ''), + 'result_assessment_records' => itstudio_join_sanitize_records_value($input['result_assessment_records'] ?? ''), + 'result_second_interview_records' => itstudio_join_sanitize_records_value($input['result_second_interview_records'] ?? ''), + 'result_admission_records' => itstudio_join_sanitize_records_value($input['result_admission_records'] ?? ''), '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) { @@ -1271,6 +1583,387 @@ function itstudio_join_parse_date($value) { return $parsed; } +function itstudio_join_to_datetime_local_input_value($value, $date_only_as_end_of_day = false) { + $parsed = itstudio_join_parse_datetime_local($value, $date_only_as_end_of_day); + return $parsed instanceof DateTimeImmutable ? $parsed->format('Y-m-d\TH:i') : ''; +} + +function itstudio_join_to_date_input_value($value) { + $parsed = itstudio_join_parse_date($value); + if (!($parsed instanceof DateTimeImmutable)) { + $parsed = itstudio_join_parse_datetime_local($value, false); + } + return $parsed instanceof DateTimeImmutable ? $parsed->format('Y-m-d') : ''; +} + +function itstudio_join_get_result_field_map() { + return array( + 'registration' => 'result_registration_records', + 'first_interview' => 'result_first_interview_records', + 'assessment' => 'result_assessment_records', + 'second_interview' => 'result_second_interview_records', + 'public_notice' => 'result_admission_records', + ); +} + +function itstudio_join_has_uploaded_result_for_stage($settings, $stage_key) { + $settings = is_array($settings) ? $settings : array(); + $field_map = itstudio_join_get_result_field_map(); + $field_key = isset($field_map[$stage_key]) ? $field_map[$stage_key] : ''; + if ($field_key === '') { + return false; + } + + return trim((string) ($settings[$field_key] ?? '')) !== ''; +} + +function itstudio_join_normalize_lookup_value($field, $value) { + $field = trim((string) $field); + $value = trim((string) $value); + if ($value === '') { + return ''; + } + + if ($field === 'qq') { + return preg_replace('/\D+/', '', $value); + } + + if ($field === 'email') { + return strtolower($value); + } + + if ($field === 'student_id') { + return strtoupper(preg_replace('/\s+/u', '', $value)); + } + + $value = preg_replace('/\s+/u', '', $value); + if (function_exists('mb_strtolower')) { + return mb_strtolower($value, 'UTF-8'); + } + + return strtolower($value); +} + +function itstudio_join_parse_result_records($raw) { + $raw = str_replace(array("\r\n", "\r"), "\n", (string) $raw); + if ($raw === '') { + return array(); + } + + $records = array(); + $lines = explode("\n", $raw); + foreach ($lines as $line) { + $line = trim((string) $line); + if ($line === '' || strpos($line, '#') === 0) { + continue; + } + + $header_probe = preg_replace('/\s+/u', '', $line); + $header_probe = function_exists('mb_strtolower') ? mb_strtolower($header_probe, 'UTF-8') : strtolower($header_probe); + $is_cn_header = (strpos($header_probe, '姓名') !== false) && (strpos($header_probe, 'qq') !== false || strpos($header_probe, '邮箱') !== false || strpos($header_probe, '学号') !== false); + $is_en_header = (strpos($header_probe, 'name') !== false) && (strpos($header_probe, 'qq') !== false || strpos($header_probe, 'email') !== false || strpos($header_probe, 'student') !== false); + if ($is_cn_header || $is_en_header) { + continue; + } + + $parts = preg_split('/[,\|,\t]+/u', $line); + $parts = is_array($parts) ? array_values(array_filter(array_map('trim', $parts), static function ($item) { + return $item !== ''; + })) : array(); + if (empty($parts)) { + continue; + } + + $name = ''; + $qq = ''; + $email = ''; + $student_id = ''; + + if (count($parts) === 1) { + $single = (string) $parts[0]; + if (strpos($single, '@') !== false) { + $email = $single; + } elseif (preg_match('/^\d{5,}$/', $single)) { + $qq = $single; + } else { + $name = $single; + } + } else { + $name = (string) ($parts[0] ?? ''); + $qq = (string) ($parts[1] ?? ''); + $email = (string) ($parts[2] ?? ''); + $student_id = (string) ($parts[3] ?? ''); + } + + $record = array( + 'name' => itstudio_join_normalize_lookup_value('name', $name), + 'qq' => itstudio_join_normalize_lookup_value('qq', $qq), + 'email' => itstudio_join_normalize_lookup_value('email', $email), + 'student_id' => itstudio_join_normalize_lookup_value('student_id', $student_id), + ); + + if ($record['name'] === '' && $record['qq'] === '' && $record['email'] === '' && $record['student_id'] === '') { + continue; + } + + $records[] = $record; + } + + return $records; +} + +function itstudio_join_record_matches_query($record, $query) { + $record = is_array($record) ? $record : array(); + $query = is_array($query) ? $query : array(); + $has_any_query = false; + + foreach (array('name', 'qq', 'email', 'student_id') as $field) { + $q = trim((string) ($query[$field] ?? '')); + if ($q === '') { + continue; + } + $has_any_query = true; + $r = trim((string) ($record[$field] ?? '')); + if ($r === '' || $r !== $q) { + return false; + } + } + + return $has_any_query; +} + +function itstudio_join_find_record_in_stage_results($settings, $stage_key, $query) { + $settings = is_array($settings) ? $settings : array(); + $field_map = itstudio_join_get_result_field_map(); + $field_key = isset($field_map[$stage_key]) ? $field_map[$stage_key] : ''; + if ($field_key === '') { + return false; + } + + $raw = (string) ($settings[$field_key] ?? ''); + if (trim($raw) === '') { + return false; + } + + $records = itstudio_join_parse_result_records($raw); + if (empty($records)) { + return false; + } + + foreach ($records as $record) { + if (itstudio_join_record_matches_query($record, $query)) { + return true; + } + } + + return false; +} + +function itstudio_join_get_stage_status_by_key($runtime, $stage_key) { + $runtime = is_array($runtime) ? $runtime : array(); + $stages = isset($runtime['stages']) && is_array($runtime['stages']) ? $runtime['stages'] : array(); + foreach ($stages as $stage) { + if (!is_array($stage)) { + continue; + } + if ((string) ($stage['key'] ?? '') === (string) $stage_key) { + return (string) ($stage['status'] ?? 'pending'); + } + } + + return 'pending'; +} + +function itstudio_join_resolve_progress_lookup($runtime = array(), $request_source = null) { + $runtime = is_array($runtime) ? $runtime : array(); + $settings = isset($runtime['settings']) && is_array($runtime['settings']) ? $runtime['settings'] : itstudio_join_get_settings(); + $request_source = is_array($request_source) ? $request_source : $_GET; + + $raw_name = isset($request_source['join_query_name']) ? sanitize_text_field(wp_unslash((string) $request_source['join_query_name'])) : ''; + $raw_qq = isset($request_source['join_query_qq']) ? sanitize_text_field(wp_unslash((string) $request_source['join_query_qq'])) : ''; + $raw_email = isset($request_source['join_query_email']) ? sanitize_text_field(wp_unslash((string) $request_source['join_query_email'])) : ''; + $raw_student_id = isset($request_source['join_query_student_id']) ? sanitize_text_field(wp_unslash((string) $request_source['join_query_student_id'])) : ''; + + $query = array( + 'name' => itstudio_join_normalize_lookup_value('name', $raw_name), + 'qq' => itstudio_join_normalize_lookup_value('qq', $raw_qq), + 'email' => itstudio_join_normalize_lookup_value('email', $raw_email), + 'student_id' => itstudio_join_normalize_lookup_value('student_id', $raw_student_id), + ); + + $has_query_value = false; + foreach ($query as $value) { + if ($value !== '') { + $has_query_value = true; + break; + } + } + + $submitted = isset($request_source['join_progress_lookup']) || $has_query_value; + $response = array( + 'submitted' => $submitted, + 'has_query' => $has_query_value, + 'name' => $raw_name, + 'qq' => $raw_qq, + 'email' => $raw_email, + 'student_id' => $raw_student_id, + 'message_cn' => '', + 'message_en' => '', + 'tone' => 'info', + ); + + if (!$submitted) { + return $response; + } + + if (!$has_query_value) { + $response['message_cn'] = '请至少填写姓名、QQ、邮箱、学号中的一项。'; + $response['message_en'] = 'Please fill at least one item: name, QQ, email or student ID.'; + $response['tone'] = 'warning'; + return $response; + } + + $status_registration = itstudio_join_get_stage_status_by_key($runtime, 'registration'); + $status_first = itstudio_join_get_stage_status_by_key($runtime, 'first_interview'); + $status_assessment = itstudio_join_get_stage_status_by_key($runtime, 'assessment'); + $status_second = itstudio_join_get_stage_status_by_key($runtime, 'second_interview'); + $status_notice = itstudio_join_get_stage_status_by_key($runtime, 'public_notice'); + + $uploaded_registration = itstudio_join_has_uploaded_result_for_stage($settings, 'registration'); + $uploaded_first = itstudio_join_has_uploaded_result_for_stage($settings, 'first_interview'); + $uploaded_assessment = itstudio_join_has_uploaded_result_for_stage($settings, 'assessment'); + $uploaded_second = itstudio_join_has_uploaded_result_for_stage($settings, 'second_interview'); + $uploaded_notice = itstudio_join_has_uploaded_result_for_stage($settings, 'public_notice'); + + if (!$uploaded_registration) { + $response['message_cn'] = '报名数据尚未上传,暂无法查询。'; + $response['message_en'] = 'Registration data has not been uploaded yet.'; + $response['tone'] = 'warning'; + return $response; + } + + $is_registered = itstudio_join_find_record_in_stage_results($settings, 'registration', $query); + if (!$is_registered) { + $response['message_cn'] = '未报名。'; + $response['message_en'] = 'Not registered.'; + $response['tone'] = 'error'; + return $response; + } + + if ($status_registration === 'active' || $status_registration === 'upcoming' || $status_first === 'upcoming' || $status_first === 'pending') { + $response['message_cn'] = '已报名。'; + $response['message_en'] = 'Registered.'; + $response['tone'] = 'success'; + return $response; + } + + if ($status_first === 'completed') { + if (!$uploaded_first) { + $response['message_cn'] = '第一次面试已结束,结果尚未上传。'; + $response['message_en'] = 'Interview I has ended, but results are not uploaded yet.'; + $response['tone'] = 'warning'; + return $response; + } + + $passed_first = itstudio_join_find_record_in_stage_results($settings, 'first_interview', $query); + if (!$passed_first) { + $response['message_cn'] = '未通过第一次面试,期待来年再次报名。'; + $response['message_en'] = 'You did not pass Interview I. Welcome to apply again next year.'; + $response['tone'] = 'error'; + return $response; + } + + if ($status_assessment === 'upcoming' || $status_assessment === 'pending' || $status_assessment === 'active') { + $response['message_cn'] = '恭喜,您已通过第一次面试。'; + $response['message_en'] = 'Congratulations! You have passed Interview I.'; + $response['tone'] = 'success'; + return $response; + } + } + + if ($status_assessment === 'completed') { + if (!$uploaded_assessment) { + $response['message_cn'] = '国庆能力摸底已结束,结果尚未上传。'; + $response['message_en'] = 'Assessment stage has ended, but results are not uploaded yet.'; + $response['tone'] = 'warning'; + return $response; + } + + $passed_assessment = itstudio_join_find_record_in_stage_results($settings, 'assessment', $query); + if (!$passed_assessment) { + $response['message_cn'] = '未通过国庆能力摸底,期待来年再次报名。'; + $response['message_en'] = 'You did not pass the assessment stage. Welcome to apply again next year.'; + $response['tone'] = 'error'; + return $response; + } + + if ($status_second === 'upcoming' || $status_second === 'pending' || $status_second === 'active') { + $response['message_cn'] = '恭喜,您已通过国庆能力摸底。'; + $response['message_en'] = 'Congratulations! You have passed the assessment stage.'; + $response['tone'] = 'success'; + return $response; + } + } + + if ($status_second === 'completed') { + if (!$uploaded_second) { + $response['message_cn'] = '第二次面试已结束,结果尚未上传。'; + $response['message_en'] = 'Interview II has ended, but results are not uploaded yet.'; + $response['tone'] = 'warning'; + return $response; + } + + $passed_second = itstudio_join_find_record_in_stage_results($settings, 'second_interview', $query); + if (!$passed_second) { + $response['message_cn'] = '未通过第二次面试,期待来年再次报名。'; + $response['message_en'] = 'You did not pass Interview II. Welcome to apply again next year.'; + $response['tone'] = 'error'; + return $response; + } + + if ($status_notice === 'pending' || $status_notice === 'upcoming' || $status_notice === 'active') { + if ($status_notice === 'active' && $uploaded_notice) { + $admitted = itstudio_join_find_record_in_stage_results($settings, 'public_notice', $query); + if ($admitted) { + $response['message_cn'] = '恭喜,您已被录取。'; + $response['message_en'] = 'Congratulations! You have been admitted.'; + $response['tone'] = 'success'; + return $response; + } + + $response['message_cn'] = '很遗憾,您未被录取,期待来年再次报名。'; + $response['message_en'] = 'Sorry, you were not admitted. Welcome to apply again next year.'; + $response['tone'] = 'error'; + return $response; + } + + $response['message_cn'] = '恭喜,您已通过第二次面试,请等待录取结果公布。'; + $response['message_en'] = 'Congratulations! You have passed Interview II. Please wait for the final result.'; + $response['tone'] = 'success'; + return $response; + } + } + + if (($status_notice === 'active' || $status_notice === 'completed') && $uploaded_notice) { + $admitted = itstudio_join_find_record_in_stage_results($settings, 'public_notice', $query); + if ($admitted) { + $response['message_cn'] = '恭喜,您已被录取。'; + $response['message_en'] = 'Congratulations! You have been admitted.'; + $response['tone'] = 'success'; + } else { + $response['message_cn'] = '很遗憾,您未被录取,期待来年再次报名。'; + $response['message_en'] = 'Sorry, you were not admitted. Welcome to apply again next year.'; + $response['tone'] = 'error'; + } + return $response; + } + + $response['message_cn'] = '已报名。'; + $response['message_en'] = 'Registered.'; + $response['tone'] = 'success'; + return $response; +} + function itstudio_join_datetime_to_ms($date) { if (!($date instanceof DateTimeImmutable)) { return null; @@ -1424,13 +2117,49 @@ function itstudio_join_get_runtime_data() { $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; + $first_interview_start_raw = isset($settings['first_interview_date']) ? (string) $settings['first_interview_date'] : ''; + $first_interview_end_raw = isset($settings['first_interview_end']) ? (string) $settings['first_interview_end'] : ''; + $first_interview_start = itstudio_join_parse_datetime_local($first_interview_start_raw, false); + $first_interview_end = itstudio_join_parse_datetime_local($first_interview_end_raw, true); + if ($first_interview_start instanceof DateTimeImmutable && !($first_interview_end instanceof DateTimeImmutable)) { + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', trim($first_interview_start_raw))) { + $first_interview_end = $first_interview_start->setTime(23, 59, 59); + } else { + $first_interview_end = $first_interview_start; + } + } elseif (!($first_interview_start instanceof DateTimeImmutable) && $first_interview_end instanceof DateTimeImmutable) { + $first_interview_start = $first_interview_end; + } elseif ($first_interview_start instanceof DateTimeImmutable && $first_interview_end instanceof DateTimeImmutable && $first_interview_end < $first_interview_start) { + $first_interview_end = $first_interview_start; + } - $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; + $second_interview_start_raw = isset($settings['second_interview_date']) ? (string) $settings['second_interview_date'] : ''; + $second_interview_end_raw = isset($settings['second_interview_end']) ? (string) $settings['second_interview_end'] : ''; + $second_interview_start = itstudio_join_parse_datetime_local($second_interview_start_raw, false); + $second_interview_end = itstudio_join_parse_datetime_local($second_interview_end_raw, true); + if ($second_interview_start instanceof DateTimeImmutable && !($second_interview_end instanceof DateTimeImmutable)) { + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', trim($second_interview_start_raw))) { + $second_interview_end = $second_interview_start->setTime(23, 59, 59); + } else { + $second_interview_end = $second_interview_start; + } + } elseif (!($second_interview_start instanceof DateTimeImmutable) && $second_interview_end instanceof DateTimeImmutable) { + $second_interview_start = $second_interview_end; + } elseif ($second_interview_start instanceof DateTimeImmutable && $second_interview_end instanceof DateTimeImmutable && $second_interview_end < $second_interview_start) { + $second_interview_end = $second_interview_start; + } + + $first_interview_location_cn = trim((string) ($settings['first_interview_location_cn'] ?? '')); + $first_interview_location_en = trim((string) ($settings['first_interview_location_en'] ?? '')); + if ($first_interview_location_en === '') { + $first_interview_location_en = $first_interview_location_cn; + } + + $second_interview_location_cn = trim((string) ($settings['second_interview_location_cn'] ?? '')); + $second_interview_location_en = trim((string) ($settings['second_interview_location_en'] ?? '')); + if ($second_interview_location_en === '') { + $second_interview_location_en = $second_interview_location_cn; + } $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; @@ -1445,9 +2174,27 @@ function itstudio_join_get_runtime_data() { $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; + $default_assessment_start = DateTimeImmutable::createFromFormat('!Y-m-d', sprintf('%04d-10-01', $recruitment_year), $timezone); + $default_assessment_end_base = DateTimeImmutable::createFromFormat('!Y-m-d', sprintf('%04d-10-07', $recruitment_year), $timezone); + $default_assessment_end = $default_assessment_end_base instanceof DateTimeImmutable ? $default_assessment_end_base->setTime(23, 59, 59) : null; + + $assessment_start_day = itstudio_join_parse_date($settings['assessment_start_date']); + $assessment_end_day = itstudio_join_parse_date($settings['assessment_end_date']); + $assessment_start = $assessment_start_day instanceof DateTimeImmutable + ? $assessment_start_day->setTime(0, 0, 0) + : $default_assessment_start; + $assessment_end = $assessment_end_day instanceof DateTimeImmutable + ? $assessment_end_day->setTime(23, 59, 59) + : $default_assessment_end; + + if ($assessment_start_day instanceof DateTimeImmutable && !($assessment_end_day instanceof DateTimeImmutable)) { + $assessment_end = $assessment_start->setTime(23, 59, 59); + } elseif (!($assessment_start_day instanceof DateTimeImmutable) && $assessment_end_day instanceof DateTimeImmutable) { + $assessment_start = $assessment_end_day->setTime(0, 0, 0); + } + if ($assessment_start instanceof DateTimeImmutable && $assessment_end instanceof DateTimeImmutable && $assessment_end < $assessment_start) { + $assessment_end = $assessment_start->setTime(23, 59, 59); + } $stage_seed = array( array( @@ -1459,6 +2206,9 @@ function itstudio_join_get_runtime_data() { 'start' => $registration_start, 'end' => $registration_end, 'all_day' => true, + 'location_cn' => '', + 'location_en' => '', + 'result_uploaded' => itstudio_join_has_uploaded_result_for_stage($settings, 'registration'), ), array( 'key' => 'first_interview', @@ -1468,7 +2218,10 @@ function itstudio_join_get_runtime_data() { 'short_en' => 'I-1', 'start' => $first_interview_start, 'end' => $first_interview_end, - 'all_day' => true, + 'all_day' => false, + 'location_cn' => $first_interview_location_cn, + 'location_en' => $first_interview_location_en, + 'result_uploaded' => itstudio_join_has_uploaded_result_for_stage($settings, 'first_interview'), ), array( 'key' => 'assessment', @@ -1479,6 +2232,9 @@ function itstudio_join_get_runtime_data() { 'start' => $assessment_start, 'end' => $assessment_end, 'all_day' => true, + 'location_cn' => '', + 'location_en' => '', + 'result_uploaded' => itstudio_join_has_uploaded_result_for_stage($settings, 'assessment'), ), array( 'key' => 'second_interview', @@ -1488,17 +2244,23 @@ function itstudio_join_get_runtime_data() { 'short_en' => 'I-2', 'start' => $second_interview_start, 'end' => $second_interview_end, - 'all_day' => true, + 'all_day' => false, + 'location_cn' => $second_interview_location_cn, + 'location_en' => $second_interview_location_en, + 'result_uploaded' => itstudio_join_has_uploaded_result_for_stage($settings, 'second_interview'), ), array( 'key' => 'public_notice', - 'label_cn' => '录取结果公示', + 'label_cn' => '录取结果公布', 'label_en' => 'Public Notice', - 'short_cn' => '公示', + 'short_cn' => '公布', 'short_en' => 'Notice', 'start' => $notice_start, 'end' => $notice_end, 'all_day' => true, + 'location_cn' => '', + 'location_en' => '', + 'result_uploaded' => itstudio_join_has_uploaded_result_for_stage($settings, 'public_notice'), ), ); @@ -1515,6 +2277,9 @@ function itstudio_join_get_runtime_data() { 'status' => $status, 'range_cn' => $range['cn'], 'range_en' => $range['en'], + 'location_cn' => isset($stage['location_cn']) ? (string) $stage['location_cn'] : '', + 'location_en' => isset($stage['location_en']) ? (string) $stage['location_en'] : '', + 'result_uploaded' => !empty($stage['result_uploaded']), 'start_ts' => itstudio_join_datetime_to_ms($stage['start']), 'end_ts' => itstudio_join_datetime_to_ms($stage['end']), ); @@ -1528,8 +2293,28 @@ function itstudio_join_get_runtime_data() { } } - $current_stage = ($current_stage_index >= 0 && isset($stages[$current_stage_index])) - ? $stages[$current_stage_index] + $next_stage_index = -1; + if ($current_stage_index < 0) { + foreach ($stages as $index => $stage) { + if (($stage['status'] ?? '') === 'upcoming') { + $next_stage_index = (int) $index; + break; + } + } + } + + $current_stage_mode = 'inactive'; + $display_stage_index = -1; + if ($current_stage_index >= 0 && isset($stages[$current_stage_index])) { + $display_stage_index = $current_stage_index; + $current_stage_mode = 'active'; + } elseif ($next_stage_index >= 0 && isset($stages[$next_stage_index])) { + $display_stage_index = $next_stage_index; + $current_stage_mode = 'next'; + } + + $current_stage = ($display_stage_index >= 0 && isset($stages[$display_stage_index])) + ? $stages[$display_stage_index] : array( 'key' => 'inactive', 'label_cn' => '当前未在招新时段', @@ -1539,6 +2324,9 @@ function itstudio_join_get_runtime_data() { 'status' => 'inactive', 'range_cn' => '请关注后续通知', 'range_en' => 'Please check later updates', + 'location_cn' => '', + 'location_en' => '', + 'result_uploaded' => false, 'start_ts' => null, 'end_ts' => null, ); @@ -1560,6 +2348,36 @@ function itstudio_join_get_runtime_data() { $current_stage_photo_url = get_template_directory_uri() . '/resources/it_logo_2024.svg'; } + $season_start = null; + $season_end = null; + foreach ($stage_seed as $stage_window) { + $stage_start = $stage_window['start'] instanceof DateTimeImmutable ? $stage_window['start'] : null; + $stage_end = $stage_window['end'] instanceof DateTimeImmutable ? $stage_window['end'] : null; + if (!($stage_start instanceof DateTimeImmutable) && !($stage_end instanceof DateTimeImmutable)) { + continue; + } + + $window_start = $stage_start instanceof DateTimeImmutable ? $stage_start : $stage_end; + $window_end = $stage_end instanceof DateTimeImmutable ? $stage_end : $stage_start; + if (!($window_start instanceof DateTimeImmutable) || !($window_end instanceof DateTimeImmutable)) { + continue; + } + + if (!($season_start instanceof DateTimeImmutable) || $window_start < $season_start) { + $season_start = $window_start; + } + if (!($season_end instanceof DateTimeImmutable) || $window_end > $season_end) { + $season_end = $window_end; + } + } + + $show_progress_visual = false; + if ($season_start instanceof DateTimeImmutable && $season_end instanceof DateTimeImmutable) { + $show_progress_visual = ($now >= $season_start && $now <= $season_end); + } elseif ($current_stage_index >= 0) { + $show_progress_visual = true; + } + $cached = array( 'settings' => $settings, 'timezone' => $timezone->getName(), @@ -1567,10 +2385,12 @@ function itstudio_join_get_runtime_data() { 'now_ts' => (int) $now->format('U') * 1000, 'stages' => $stages, 'current_stage_index' => $current_stage_index, + 'current_stage_mode' => $current_stage_mode, 'current_stage' => $current_stage, 'is_registration_open' => $is_registration_open, 'is_query_open' => $is_query_open, 'is_notice_open' => $is_notice_open, + 'show_progress_visual' => $show_progress_visual, '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') : '', @@ -1594,6 +2414,9 @@ function itstudio_join_get_frontend_payload() { 'status' => (string) ($stage['status'] ?? 'pending'), 'rangeCn' => (string) ($stage['range_cn'] ?? ''), 'rangeEn' => (string) ($stage['range_en'] ?? ''), + 'locationCn' => (string) ($stage['location_cn'] ?? ''), + 'locationEn' => (string) ($stage['location_en'] ?? ''), + 'resultUploaded' => !empty($stage['result_uploaded']), 'startTs' => isset($stage['start_ts']) ? $stage['start_ts'] : null, 'endTs' => isset($stage['end_ts']) ? $stage['end_ts'] : null, ); @@ -1675,6 +2498,29 @@ function itstudio_join_render_photo_field_row($field_key, $label, $settings) { + + + + + +

+ +

每行一条记录,格式:姓名,QQ,邮箱,学号。可用逗号、中文逗号、竖线或 Tab 分隔。

+ + + +

爱特工作室招新设置

-

用于配置「加入我们」页面的时间节点、表单和公示视图。

+

用于配置「加入我们」页面的时间节点、阶段结果和表单。

-

提示:未检测到 Formidable Forms 插件。报名表单、查询表单、公示视图将无法在前台渲染。

+

提示:未检测到 Formidable Forms 插件。报名表单将无法在前台渲染。

@@ -1709,40 +2555,95 @@ function itstudio_join_render_settings_page() { - + - + - + - + - + - + - + - -

公示会自动持续 7 天(开始日 + 后续 6 天)。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

留空时默认使用每年 10 月 1 日。

+ + + + + + +

留空时默认使用每年 10 月 7 日。

+ + + + + + +

公布会自动持续 7 天(开始日 + 后续 6 天)。

- + + + + + + @@ -1750,27 +2651,13 @@ function itstudio_join_render_settings_page() {

示例:[formidable id="12"]

- - - - -

示例:[formidable id="13"]

- - - - - - -

示例:[display-frm-data id="5"]

- -

阶段预览

-

国庆能力摸底阶段固定为每年 10 月 1 日至 10 月 7 日,年份自动取报名开始年份(未配置则取公示开始年份,再否则取当前年份)。

+

国庆能力摸底默认固定为每年 10 月 1 日至 10 月 7 日;如填写上方“摸底开始/结束日期(调试)”则优先使用调试时间,留空则恢复默认固定窗口。

diff --git a/page-join.php b/page-join.php index 686fb62..7255710 100644 --- a/page-join.php +++ b/page-join.php @@ -15,28 +15,76 @@ $join_stages = isset($join_runtime['stages']) && is_array($join_runtime['stages' $current_stage = isset($join_runtime['current_stage']) && is_array($join_runtime['current_stage']) ? $join_runtime['current_stage'] : array(); +$current_stage_mode = isset($join_runtime['current_stage_mode']) ? (string) $join_runtime['current_stage_mode'] : 'inactive'; $is_registration_open = !empty($join_runtime['is_registration_open']); -$is_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'] : ''; +$show_progress_visual = !empty($join_runtime['show_progress_visual']); +$is_progress_query_open = !empty($join_runtime['show_progress_visual']); $current_label_cn = isset($current_stage['label_cn']) ? (string) $current_stage['label_cn'] : '当前未在招新时段'; $current_label_en = isset($current_stage['label_en']) ? (string) $current_stage['label_en'] : 'Recruitment is currently closed'; $current_range_cn = isset($current_stage['range_cn']) ? (string) $current_stage['range_cn'] : '请关注后续通知'; $current_range_en = isset($current_stage['range_en']) ? (string) $current_stage['range_en'] : 'Please check later updates'; +$current_location_cn = trim((string) ($current_stage['location_cn'] ?? '')); +$current_location_en = trim((string) ($current_stage['location_en'] ?? '')); +if ($current_location_en === '') { + $current_location_en = $current_location_cn; +} +if ($current_location_cn === '') { + $current_location_cn = $current_location_en; +} +$current_stage_heading_cn = $current_stage_mode === 'next' ? '下一招新阶段' : '当前招新阶段'; +$current_stage_heading_en = $current_stage_mode === 'next' ? 'Next Recruitment Stage' : 'Current Recruitment Stage'; $current_stage_photo_url = isset($join_runtime['current_stage_photo_url']) ? (string) $join_runtime['current_stage_photo_url'] : ''; if ($current_stage_photo_url === '') { $current_stage_photo_url = get_template_directory_uri() . '/resources/it_logo_2024.svg'; } $signup_shortcode = trim((string) ($join_settings['signup_form_shortcode'] ?? '')); -$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'); +$progress_lookup = function_exists('itstudio_join_resolve_progress_lookup') + ? itstudio_join_resolve_progress_lookup($join_runtime, $_GET) + : array( + 'submitted' => false, + 'has_query' => false, + 'name' => '', + 'qq' => '', + 'email' => '', + 'student_id' => '', + 'message_cn' => '', + 'message_en' => '', + 'tone' => 'info', + ); +$join_feed_data = function_exists('itstudio_join_get_recruitment_feed_items') + ? itstudio_join_get_recruitment_feed_items($join_runtime, 5) + : array('display_year' => (int) wp_date('Y'), 'items' => array()); +$join_feed_items = isset($join_feed_data['items']) && is_array($join_feed_data['items']) + ? $join_feed_data['items'] + : array(); +$join_feed_items = array_values(array_filter($join_feed_items, static function ($item) { + if (!is_array($item)) { + return false; + } + $title = trim((string) ($item['title'] ?? '')); + $url = trim((string) ($item['url'] ?? '')); + return ($title !== '' && $url !== ''); +})); + +$join_form_status = ''; +if (isset($_GET['join_form_status']) && is_string($_GET['join_form_status'])) { + $join_form_status = strtolower(trim((string) wp_unslash($_GET['join_form_status']))); +} +if ($join_form_status !== 'submitted' && $join_form_status !== 'draft') { + $join_form_status = (isset($_GET['join_form_submitted']) && (string) $_GET['join_form_submitted'] === '1') ? 'submitted' : ''; +} + +$is_post_request = isset($_SERVER['REQUEST_METHOD']) && strtoupper((string) $_SERVER['REQUEST_METHOD']) === 'POST'; +$post_form_status = 'submitted'; +if ($is_post_request && function_exists('itstudio_join_detect_form_submission_status')) { + $post_form_status = itstudio_join_detect_form_submission_status($_POST); +} ?>
@@ -45,6 +93,51 @@ $has_formidable = shortcode_exists('formidable') || class_exists('FrmFormsContro

加入我们

+ + +

+ + + +
+ +
+ + +
+

+ +

+

+
+ +
+ +

暂无可展示的招新新闻通告

+ +
+
@@ -55,7 +148,7 @@ $has_formidable = shortcode_exists('formidable') || class_exists('FrmFormsContro loading="lazy" >
-

当前招新阶段

+

+ + +

+ +

+

-
@@ -114,7 +223,32 @@ $has_formidable = shortcode_exists('formidable') || class_exists('FrmFormsContro $status_cn = '未开始'; $status_en = 'Upcoming'; } + $stage_key = (string) ($stage['key'] ?? ''); + $stage_result_uploaded = !empty($stage['result_uploaded']); + $stage_location_cn = trim((string) ($stage['location_cn'] ?? '')); + $stage_location_en = trim((string) ($stage['location_en'] ?? '')); + if ($stage_location_en === '') { + $stage_location_en = $stage_location_cn; + } + if ($stage_location_cn === '') { + $stage_location_cn = $stage_location_en; + } $is_current_stage = !empty($current_stage['key']) && !empty($stage['key']) && ((string) $current_stage['key'] === (string) $stage['key']); + $is_public_notice_stage = ($stage_key === 'public_notice'); + $is_mid_stage_query_ready = in_array($stage_key, array('first_interview', 'assessment', 'second_interview'), true) + && $stage_status === 'completed' + && $stage_result_uploaded; + $is_query_ready_status = false; + if ($is_mid_stage_query_ready) { + $status_cn = '可查询结果'; + $status_en = 'Query Available'; + $is_query_ready_status = true; + } + if ($is_public_notice_stage && in_array($stage_status, array('active', 'completed'), true)) { + $status_cn = '可查询结果'; + $status_en = 'Query Available'; + $is_query_ready_status = true; + } ?>
  • @@ -126,7 +260,7 @@ $has_formidable = shortcode_exists('formidable') || class_exists('FrmFormsContro @@ -140,75 +274,157 @@ $has_formidable = shortcode_exists('formidable') || class_exists('FrmFormsContro >

    + + +

    + +

    +
  • -
    -
    -
    -

    报名表单

    -

    仅在报名阶段开放

    -
    -
    - -

    当前不在报名时间段 请关注后续通知

    - -

    未检测到 Formidable Forms 插件 请先启用插件

    - -

    请在 设置 > 招新设置 中填写报名表单 Shortcode

    - - - -
    -
    - -
    -
    -

    结果查询

    -

    报名开始后至公示结束前可查询进度

    -
    -
    - -

    当前查询通道未开放或已关闭

    - -

    未检测到 Formidable Forms 插件 请先启用插件

    - -

    请在 设置 > 招新设置 中填写查询表单 Shortcode

    - - - -
    - -
    - -
    + + 0) : ?> +
    + +
    +
    +

    报名表单

    +

    仅在报名阶段开放

    +
    +
    + +

    未检测到 Formidable Forms 插件 请先启用插件

    + +

    请在 设置 > 招新设置 中填写报名表单 Shortcode

    + + + +
    +
    -
    -
    -
    -

    录取结果公示

    -

    公示阶段自动展示 公示期为 7 天

    -
    -
    - -

    当前未进入公示阶段

    - -

    未检测到 Formidable Forms 插件 请先启用插件

    - -

    请在 设置 > 招新设置 中填写公示视图 Shortcode

    - - - -
    -
    -
    + +
    +
    +

    录取进度查询

    +

    报名阶段至录取结果公布阶段可查询

    +
    +
    +
    +
    + + + + +
    +
    + +
    + + + +

    + +

    + +
    +
    + + + + + + +