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
>
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+