Compare commits
14 Commits
d860a34680
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ebe225835e | |||
| 0004bffd8b | |||
| 93e8fdf045 | |||
| 261b9c87fa | |||
| 49d07c21b0 | |||
| e0bf7677a3 | |||
| e835010a2b | |||
| 14d7ca1add | |||
| b4c2bd6350 | |||
| 93d5883dbb | |||
| 8ceebab216 | |||
| 0901c9c99f | |||
| d8f4f8c321 | |||
| 092f89b2a8 |
@@ -0,0 +1,110 @@
|
|||||||
|
/* =========================================
|
||||||
|
ITStudio 邮箱 OTP 验证码组件样式 (Modern Geek Style)
|
||||||
|
========================================= */
|
||||||
|
|
||||||
|
.itstudio-otp-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itstudio-otp-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itstudio-otp-group .itstudio-otp-input {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
letter-spacing: 0.1em !important;
|
||||||
|
font-family: var(--font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itstudio-otp-send {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
background: linear-gradient(135deg, #1f6feb 0%, #1158c7 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(31, 111, 235, 0.35);
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itstudio-otp-send:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.08);
|
||||||
|
box-shadow: 0 4px 14px rgba(31, 111, 235, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.itstudio-otp-send:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
filter: brightness(0.96);
|
||||||
|
box-shadow: 0 1px 4px rgba(31, 111, 235, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.itstudio-otp-send:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itstudio-otp-send.is-countdown {
|
||||||
|
background: #6b7280 !important;
|
||||||
|
border: 1px solid #7c8490 !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itstudio-otp-send.is-loading {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itstudio-otp-status {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浅色模式适配 */
|
||||||
|
[data-theme="light"] .itstudio-otp-send {
|
||||||
|
background: linear-gradient(135deg, #0969da 0%, #0550ae 100%);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||||
|
box-shadow: 0 2px 8px rgba(9, 105, 218, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .itstudio-otp-send:hover:not(:disabled) {
|
||||||
|
box-shadow: 0 4px 14px rgba(9, 105, 218, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .itstudio-otp-send.is-countdown {
|
||||||
|
background: #9ca3af !important;
|
||||||
|
border: 1px solid #9ca3af !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.itstudio-otp-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.itstudio-otp-send {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = window.itstudioJoinOtp;
|
||||||
|
if (!config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OTP_COOLDOWN_SECONDS = 60;
|
||||||
|
|
||||||
|
function getLanguage() {
|
||||||
|
return document.documentElement.lang === 'en' ? 'en' : 'cn';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessage(payload, fallback) {
|
||||||
|
const key = getLanguage() === 'en' ? 'message_en' : 'message_cn';
|
||||||
|
return payload && payload[key] ? payload[key] : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findField(form, fieldId) {
|
||||||
|
return form.querySelector('[name="item_meta[' + fieldId + ']"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTargetForm() {
|
||||||
|
const candidates = document.querySelectorAll('.join-form-content .frm_forms form');
|
||||||
|
for (const form of candidates) {
|
||||||
|
const formIdInput = form.querySelector('input[name="form_id"]');
|
||||||
|
if (!formIdInput || Number(formIdInput.value) === Number(config.formId)) {
|
||||||
|
if (findField(form, config.emailFieldId) && findField(form, config.codeFieldId)) {
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(status, message, tone) {
|
||||||
|
if (!message) {
|
||||||
|
status.textContent = '';
|
||||||
|
status.className = 'itstudio-otp-status';
|
||||||
|
status.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
status.style.display = 'flex';
|
||||||
|
status.textContent = message;
|
||||||
|
status.className = 'itstudio-otp-status ' + (tone === 'error' ? 'frm_error_style' : 'frm_message');
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCooldown(button, seconds) {
|
||||||
|
let remaining = Math.max(1, Number(seconds) || OTP_COOLDOWN_SECONDS);
|
||||||
|
if (button.itstudioOtpTimer) {
|
||||||
|
window.clearInterval(button.itstudioOtpTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
button.dataset.otpLocked = '1';
|
||||||
|
button.setAttribute('aria-disabled', 'true');
|
||||||
|
button.classList.add('is-countdown');
|
||||||
|
|
||||||
|
const update = function () {
|
||||||
|
button.textContent = String(config.labels.resend || '重新发送 (%s秒)').replace('%s', remaining);
|
||||||
|
remaining -= 1;
|
||||||
|
if (remaining < 0) {
|
||||||
|
window.clearInterval(button.itstudioOtpTimer);
|
||||||
|
button.itstudioOtpTimer = null;
|
||||||
|
button.dataset.otpLocked = '0';
|
||||||
|
button.setAttribute('aria-disabled', 'false');
|
||||||
|
button.classList.remove('is-countdown');
|
||||||
|
button.textContent = config.labels.send || '发送验证码';
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
update();
|
||||||
|
button.itstudioOtpTimer = window.setInterval(update, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readAjaxResult(response) {
|
||||||
|
const raw = await response.text();
|
||||||
|
const normalized = raw.replace(/^\uFEFF/, '').trim();
|
||||||
|
if (!normalized) {
|
||||||
|
throw new Error('Empty AJAX response');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = JSON.parse(normalized);
|
||||||
|
if (result && typeof result === 'object' && typeof result.success !== 'undefined') {
|
||||||
|
return {
|
||||||
|
success: result.success === true || result.success === 1 || result.success === '1',
|
||||||
|
payload: result.data && typeof result.data === 'object' ? result.data : {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result && typeof result === 'object' && (result.message_cn || result.message_en)) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payload: result
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unexpected AJAX response');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendCode(form, button, status) {
|
||||||
|
if (button.dataset.otpLocked === '1' || button.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailInput = findField(form, config.emailFieldId);
|
||||||
|
const email = emailInput ? emailInput.value.trim() : '';
|
||||||
|
if (!email) {
|
||||||
|
setStatus(status, config.labels.emailRequired || '请先填写邮箱地址。', 'error');
|
||||||
|
emailInput?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (emailInput && !emailInput.checkValidity()) {
|
||||||
|
setStatus(status, config.labels.invalidEmail || '请输入有效的邮箱地址。', 'error');
|
||||||
|
emailInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block rapid duplicate clicks while the request is in flight.
|
||||||
|
button.disabled = true;
|
||||||
|
button.dataset.otpLocked = '1';
|
||||||
|
button.setAttribute('aria-disabled', 'true');
|
||||||
|
button.classList.add('is-loading');
|
||||||
|
button.textContent = config.labels.sending || '发送中…';
|
||||||
|
setStatus(status, config.labels.sending || '正在发送验证码…', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.fetch(config.ajaxUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
action: 'itstudio_join_otp_send',
|
||||||
|
email: email,
|
||||||
|
nonce: config.nonce
|
||||||
|
}).toString()
|
||||||
|
});
|
||||||
|
const result = await readAjaxResult(response);
|
||||||
|
const payload = result.payload || {};
|
||||||
|
button.classList.remove('is-loading');
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
setStatus(status, getMessage(payload, config.labels.networkError || '发送失败,请稍后重试。'), 'error');
|
||||||
|
button.dataset.otpLocked = '0';
|
||||||
|
button.setAttribute('aria-disabled', 'false');
|
||||||
|
button.textContent = config.labels.send || '发送验证码';
|
||||||
|
button.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(status, getMessage(payload, '验证码已发送,请检查邮箱及垃圾箱。'), 'success');
|
||||||
|
startCooldown(button, payload.cooldown || OTP_COOLDOWN_SECONDS);
|
||||||
|
} catch (error) {
|
||||||
|
button.classList.remove('is-loading');
|
||||||
|
setStatus(status, config.labels.networkError || '网络错误,请稍后重试。', 'error');
|
||||||
|
button.dataset.otpLocked = '0';
|
||||||
|
button.setAttribute('aria-disabled', 'false');
|
||||||
|
button.textContent = config.labels.send || '发送验证码';
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mount() {
|
||||||
|
const form = findTargetForm();
|
||||||
|
if (!form || form.querySelector('.itstudio-otp-wrap')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeInput = findField(form, config.codeFieldId);
|
||||||
|
if (!codeInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
codeInput.setAttribute('inputmode', 'numeric');
|
||||||
|
codeInput.setAttribute('pattern', '[0-9]{6}');
|
||||||
|
codeInput.setAttribute('maxlength', '6');
|
||||||
|
codeInput.setAttribute('autocomplete', 'one-time-code');
|
||||||
|
if (!codeInput.getAttribute('placeholder')) {
|
||||||
|
codeInput.setAttribute('placeholder', '请输入 6 位验证码');
|
||||||
|
}
|
||||||
|
codeInput.classList.add('itstudio-otp-input');
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'itstudio-otp-wrap';
|
||||||
|
|
||||||
|
const group = document.createElement('div');
|
||||||
|
group.className = 'itstudio-otp-group';
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = 'itstudio-otp-send';
|
||||||
|
button.textContent = config.labels.send || '发送验证码';
|
||||||
|
button.dataset.otpLocked = '0';
|
||||||
|
button.setAttribute('aria-disabled', 'false');
|
||||||
|
|
||||||
|
const status = document.createElement('div');
|
||||||
|
status.className = 'itstudio-otp-status';
|
||||||
|
status.setAttribute('role', 'status');
|
||||||
|
status.setAttribute('aria-live', 'polite');
|
||||||
|
status.style.display = 'none';
|
||||||
|
|
||||||
|
if (codeInput.parentNode) {
|
||||||
|
codeInput.parentNode.insertBefore(wrapper, codeInput);
|
||||||
|
group.appendChild(codeInput);
|
||||||
|
group.appendChild(button);
|
||||||
|
wrapper.appendChild(group);
|
||||||
|
wrapper.appendChild(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendCode(form, button, status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mountQueued = false;
|
||||||
|
const scheduleMount = function () {
|
||||||
|
if (mountQueued) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mountQueued = true;
|
||||||
|
window.requestAnimationFrame(function () {
|
||||||
|
mountQueued = false;
|
||||||
|
mount();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
mount();
|
||||||
|
const observerRoot = document.querySelector('.join-form-content') || document.body;
|
||||||
|
const observer = new MutationObserver(function (mutations) {
|
||||||
|
const formStructureChanged = mutations.some(function (mutation) {
|
||||||
|
return Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes)).some(function (node) {
|
||||||
|
return node.nodeType === 1 && (
|
||||||
|
node.matches('.frm_forms, form') ||
|
||||||
|
Boolean(node.querySelector && node.querySelector('.frm_forms, form'))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formStructureChanged) {
|
||||||
|
scheduleMount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(observerRoot, {childList: true, subtree: true});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 招新报名表单邮箱 OTP:前台发码、限流、服务端校验和一次性消费。
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_get_config() {
|
||||||
|
$settings = itstudio_join_get_settings();
|
||||||
|
$form_id = absint($settings['otp_form_id'] ?? 0);
|
||||||
|
if ($form_id <= 0 && preg_match('/\[formidable\s+[^\]]*\bid=["\']?(\d+)/i', (string) ($settings['signup_form_shortcode'] ?? ''), $matches)) {
|
||||||
|
$form_id = absint($matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'enabled' => !empty($settings['otp_enabled']),
|
||||||
|
'form_id' => $form_id,
|
||||||
|
'email_field_id' => absint($settings['otp_email_field_id'] ?? 0),
|
||||||
|
'code_field_id' => absint($settings['otp_code_field_id'] ?? 0),
|
||||||
|
'ttl' => 10 * MINUTE_IN_SECONDS,
|
||||||
|
'cooldown' => 60,
|
||||||
|
'max_attempts' => 5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_is_configured() {
|
||||||
|
$config = itstudio_join_otp_get_config();
|
||||||
|
return !empty($config['enabled'])
|
||||||
|
&& $config['form_id'] > 0
|
||||||
|
&& $config['email_field_id'] > 0
|
||||||
|
&& $config['code_field_id'] > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_normalize_email($email) {
|
||||||
|
return strtolower(trim(sanitize_email((string) $email)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_get_client_ip() {
|
||||||
|
return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash((string) $_SERVER['REMOTE_ADDR'])) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_key($email, $form_id) {
|
||||||
|
return 'itstudio_join_otp_' . md5((string) $form_id . '|' . itstudio_join_otp_normalize_email($email));
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_ip_key($ip) {
|
||||||
|
return 'itstudio_join_otp_ip_' . md5((string) $ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_hash_code($email, $code) {
|
||||||
|
return hash_hmac('sha256', itstudio_join_otp_normalize_email($email) . '|' . (string) $code, wp_salt('auth'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_random_code() {
|
||||||
|
return (string) wp_rand(100000, 999999);
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_email_exists_in_form($form_id, $field_id, $email) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$form_id = absint($form_id);
|
||||||
|
$field_id = absint($field_id);
|
||||||
|
$email = itstudio_join_otp_normalize_email($email);
|
||||||
|
if ($form_id <= 0 || $field_id <= 0 || $email === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items_table = $wpdb->prefix . 'frm_items';
|
||||||
|
$metas_table = $wpdb->prefix . 'frm_item_metas';
|
||||||
|
$count = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(1)
|
||||||
|
FROM {$metas_table} AS meta
|
||||||
|
INNER JOIN {$items_table} AS item ON item.id = meta.item_id
|
||||||
|
WHERE item.form_id = %d
|
||||||
|
AND item.is_draft = 0
|
||||||
|
AND meta.field_id = %d
|
||||||
|
AND LOWER(TRIM(meta.meta_value)) = %s",
|
||||||
|
$form_id,
|
||||||
|
$field_id,
|
||||||
|
$email
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (int) $count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_json_error($message_cn, $message_en, $status = 400, $extra = array()) {
|
||||||
|
wp_send_json_error(
|
||||||
|
array_merge(
|
||||||
|
array(
|
||||||
|
'message_cn' => $message_cn,
|
||||||
|
'message_en' => $message_en,
|
||||||
|
),
|
||||||
|
$extra
|
||||||
|
),
|
||||||
|
$status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_otp_send_ajax() {
|
||||||
|
if (!check_ajax_referer('itstudio_join_otp', 'nonce', false)) {
|
||||||
|
itstudio_join_otp_json_error('请求已过期,请刷新页面后重试。', 'Request expired. Please refresh the page and try again.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = itstudio_join_otp_get_config();
|
||||||
|
if (!itstudio_join_otp_is_configured()) {
|
||||||
|
itstudio_join_otp_json_error('邮箱验证码尚未完成配置,请联系管理员。', 'Email verification is not configured yet. Please contact the administrator.', 503);
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = itstudio_join_otp_normalize_email(isset($_POST['email']) ? wp_unslash($_POST['email']) : '');
|
||||||
|
if ($email === '' || !is_email($email)) {
|
||||||
|
itstudio_join_otp_json_error('请输入有效的邮箱地址。', 'Please enter a valid email address.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itstudio_join_otp_email_exists_in_form($config['form_id'], $config['email_field_id'], $email)) {
|
||||||
|
itstudio_join_otp_json_error('该邮箱已经报名,无需重复提交。', 'This email has already been used to apply.', 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ip = itstudio_join_otp_get_client_ip();
|
||||||
|
$otp_key = itstudio_join_otp_key($email, $config['form_id']);
|
||||||
|
$existing = get_transient($otp_key);
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
if (is_array($existing) && !empty($existing['last_sent']) && ($now - (int) $existing['last_sent']) < $config['cooldown']) {
|
||||||
|
$wait = max(1, $config['cooldown'] - ($now - (int) $existing['last_sent']));
|
||||||
|
itstudio_join_otp_json_error("请等待 {$wait} 秒后再发送。", "Please wait {$wait} seconds before requesting another code.", 429, array('wait' => $wait));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ip !== '') {
|
||||||
|
$ip_key = itstudio_join_otp_ip_key($ip);
|
||||||
|
$ip_last_sent = (int) get_transient($ip_key);
|
||||||
|
if ($ip_last_sent > 0 && ($now - $ip_last_sent) < 10) {
|
||||||
|
$wait = max(1, 10 - ($now - $ip_last_sent));
|
||||||
|
itstudio_join_otp_json_error("请求过于频繁,请等待 {$wait} 秒。", "Too many requests. Please wait {$wait} seconds.", 429, array('wait' => $wait));
|
||||||
|
}
|
||||||
|
set_transient($ip_key, $now, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = itstudio_join_otp_random_code();
|
||||||
|
$payload = array(
|
||||||
|
'code_hash' => itstudio_join_otp_hash_code($email, $code),
|
||||||
|
'expires' => $now + $config['ttl'],
|
||||||
|
'last_sent' => $now,
|
||||||
|
'attempts' => 0,
|
||||||
|
'form_id' => $config['form_id'],
|
||||||
|
'email' => $email,
|
||||||
|
);
|
||||||
|
set_transient($otp_key, $payload, $config['ttl']);
|
||||||
|
|
||||||
|
$site_name = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES);
|
||||||
|
$subject = sprintf('%s 报名邮箱验证码', $site_name !== '' ? $site_name : '爱特工作室');
|
||||||
|
$message = "您好!\n\n您的报名邮箱验证码是:{$code}\n\n验证码 10 分钟内有效,且只能使用一次。如非本人操作,请忽略此邮件。\n\n{$site_name}";
|
||||||
|
$sent = wp_mail($email, $subject, $message, array('Content-Type: text/plain; charset=UTF-8'));
|
||||||
|
|
||||||
|
if (!$sent) {
|
||||||
|
delete_transient($otp_key);
|
||||||
|
itstudio_join_otp_json_error('邮件发送失败,请稍后重试。', 'The email could not be sent. Please try again later.', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'message_cn' => '验证码已发送,请检查邮箱及垃圾邮件。',
|
||||||
|
'message_en' => 'Verification code sent. Please check your inbox.',
|
||||||
|
'cooldown' => $config['cooldown'],
|
||||||
|
'ttl' => $config['ttl'],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action('wp_ajax_itstudio_join_otp_send', 'itstudio_join_otp_send_ajax');
|
||||||
|
add_action('wp_ajax_nopriv_itstudio_join_otp_send', 'itstudio_join_otp_send_ajax');
|
||||||
|
|
||||||
|
function itstudio_join_otp_validate_field($errors, $posted_field, $posted_value) {
|
||||||
|
if (!is_object($posted_field)) {
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = itstudio_join_otp_get_config();
|
||||||
|
if (!itstudio_join_otp_is_configured()
|
||||||
|
|| (int) ($posted_field->form_id ?? 0) !== $config['form_id']
|
||||||
|
|| (int) ($posted_field->id ?? 0) !== $config['code_field_id']) {
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = itstudio_join_otp_normalize_email(isset($_POST['item_meta'][$config['email_field_id']]) ? wp_unslash($_POST['item_meta'][$config['email_field_id']]) : '');
|
||||||
|
$code = is_scalar($posted_value) ? trim((string) $posted_value) : '';
|
||||||
|
$field_key = 'field' . $config['code_field_id'];
|
||||||
|
|
||||||
|
if ($email === '' || !is_email($email)) {
|
||||||
|
$errors[$field_key] = '请输入有效的邮箱并先获取验证码。';
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
if (!preg_match('/^\d{6}$/', $code)) {
|
||||||
|
$errors[$field_key] = '请输入 6 位数字验证码。';
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$otp_key = itstudio_join_otp_key($email, $config['form_id']);
|
||||||
|
$payload = get_transient($otp_key);
|
||||||
|
if (!is_array($payload) || empty($payload['code_hash']) || empty($payload['expires'])) {
|
||||||
|
$errors[$field_key] = '验证码不存在或已失效,请重新获取。';
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
if (time() > (int) $payload['expires']) {
|
||||||
|
delete_transient($otp_key);
|
||||||
|
$errors[$field_key] = '验证码已过期,请重新获取。';
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
if ((int) ($payload['attempts'] ?? 0) >= $config['max_attempts']) {
|
||||||
|
delete_transient($otp_key);
|
||||||
|
$errors[$field_key] = '验证码尝试次数过多,请重新获取。';
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
if (!hash_equals((string) $payload['code_hash'], itstudio_join_otp_hash_code($email, $code))) {
|
||||||
|
$payload['attempts'] = (int) ($payload['attempts'] ?? 0) + 1;
|
||||||
|
$remaining = max(0, (int) $payload['expires'] - time());
|
||||||
|
if ($remaining > 0) {
|
||||||
|
set_transient($otp_key, $payload, $remaining);
|
||||||
|
}
|
||||||
|
$errors[$field_key] = '验证码错误,请重试。';
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['verified_at'] = time();
|
||||||
|
$remaining = max(1, (int) $payload['expires'] - time());
|
||||||
|
set_transient($otp_key, $payload, $remaining);
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
add_filter('frm_validate_field_entry', 'itstudio_join_otp_validate_field', 20, 3);
|
||||||
|
|
||||||
|
function itstudio_join_otp_consume_after_entry($entry_id, $form_id) {
|
||||||
|
$config = itstudio_join_otp_get_config();
|
||||||
|
if (!itstudio_join_otp_is_configured() || (int) $form_id !== $config['form_id']) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = itstudio_join_otp_normalize_email(isset($_POST['item_meta'][$config['email_field_id']]) ? wp_unslash($_POST['item_meta'][$config['email_field_id']]) : '');
|
||||||
|
if ($email !== '' && is_email($email)) {
|
||||||
|
delete_transient(itstudio_join_otp_key($email, $config['form_id']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action('frm_after_create_entry', 'itstudio_join_otp_consume_after_entry', 20, 2);
|
||||||
|
|
||||||
|
function itstudio_join_otp_enqueue_assets() {
|
||||||
|
if (!itstudio_is_join_page_context() || !itstudio_join_otp_is_configured() || !shortcode_exists('formidable')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = itstudio_join_otp_get_config();
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'itstudio-join-otp',
|
||||||
|
plugins_url('../assets/otp.css', __FILE__),
|
||||||
|
array('itstudio-join-page'),
|
||||||
|
ITSTUDIO_RECRUITMENT_VERSION
|
||||||
|
);
|
||||||
|
wp_enqueue_script(
|
||||||
|
'itstudio-join-otp',
|
||||||
|
plugins_url('../assets/otp.js', __FILE__),
|
||||||
|
array(),
|
||||||
|
ITSTUDIO_RECRUITMENT_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
wp_localize_script(
|
||||||
|
'itstudio-join-otp',
|
||||||
|
'itstudioJoinOtp',
|
||||||
|
array(
|
||||||
|
'formId' => $config['form_id'],
|
||||||
|
'emailFieldId' => $config['email_field_id'],
|
||||||
|
'codeFieldId' => $config['code_field_id'],
|
||||||
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('itstudio_join_otp'),
|
||||||
|
'cooldown' => $config['cooldown'],
|
||||||
|
'labels' => array(
|
||||||
|
'send' => '发送验证码',
|
||||||
|
'resend' => '重新发送 (%s秒)',
|
||||||
|
'sending' => '发送中…',
|
||||||
|
'emailRequired' => '请先填写邮箱地址。',
|
||||||
|
'invalidEmail' => '请输入有效的邮箱地址。',
|
||||||
|
'networkError' => '网络错误,请稍后重试。',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action('wp_enqueue_scripts', 'itstudio_join_otp_enqueue_assets', 40);
|
||||||
@@ -63,6 +63,10 @@ function itstudio_join_get_default_settings() {
|
|||||||
'photo_public_notice' => 0,
|
'photo_public_notice' => 0,
|
||||||
'photo_inactive' => 0,
|
'photo_inactive' => 0,
|
||||||
'signup_form_shortcode' => '',
|
'signup_form_shortcode' => '',
|
||||||
|
'otp_enabled' => 0,
|
||||||
|
'otp_form_id' => '',
|
||||||
|
'otp_email_field_id' => '',
|
||||||
|
'otp_code_field_id' => '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +115,45 @@ function itstudio_join_sanitize_date_value($value) {
|
|||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function itstudio_join_sanitize_time_value($value) {
|
||||||
|
$value = trim((string) $value);
|
||||||
|
if (!preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/', $value)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_sanitize_interview_schedule($input, $prefix) {
|
||||||
|
$input = is_array($input) ? $input : array();
|
||||||
|
$prefix = trim((string) $prefix);
|
||||||
|
$day_key = $prefix . '_day';
|
||||||
|
$start_time_key = $prefix . '_start_time';
|
||||||
|
$end_time_key = $prefix . '_end_time';
|
||||||
|
|
||||||
|
if (!array_key_exists($day_key, $input)
|
||||||
|
&& !array_key_exists($start_time_key, $input)
|
||||||
|
&& !array_key_exists($end_time_key, $input)) {
|
||||||
|
return array(
|
||||||
|
'start' => itstudio_join_sanitize_datetime_local_value($input[$prefix . '_date'] ?? ''),
|
||||||
|
'end' => itstudio_join_sanitize_datetime_local_value($input[$prefix . '_end'] ?? ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$day = itstudio_join_sanitize_date_value($input[$day_key] ?? '');
|
||||||
|
if ($day === '') {
|
||||||
|
return array('start' => '', 'end' => '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$start_time = itstudio_join_sanitize_time_value($input[$start_time_key] ?? '');
|
||||||
|
$end_time = itstudio_join_sanitize_time_value($input[$end_time_key] ?? '');
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'start' => $start_time !== '' ? $day . 'T' . $start_time : $day,
|
||||||
|
'end' => $end_time !== '' ? $day . 'T' . $end_time : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function itstudio_join_sanitize_shortcode_value($value) {
|
function itstudio_join_sanitize_shortcode_value($value) {
|
||||||
if (!is_string($value)) {
|
if (!is_string($value)) {
|
||||||
return '';
|
return '';
|
||||||
@@ -142,16 +185,18 @@ function itstudio_join_sanitize_records_value($value) {
|
|||||||
function itstudio_join_sanitize_settings($input) {
|
function itstudio_join_sanitize_settings($input) {
|
||||||
$defaults = itstudio_join_get_default_settings();
|
$defaults = itstudio_join_get_default_settings();
|
||||||
$input = is_array($input) ? $input : array();
|
$input = is_array($input) ? $input : array();
|
||||||
|
$first_interview_schedule = itstudio_join_sanitize_interview_schedule($input, 'first_interview');
|
||||||
|
$second_interview_schedule = itstudio_join_sanitize_interview_schedule($input, 'second_interview');
|
||||||
|
|
||||||
$sanitized = array(
|
$sanitized = array(
|
||||||
'registration_start' => itstudio_join_sanitize_datetime_local_value($input['registration_start'] ?? ''),
|
'registration_start' => itstudio_join_sanitize_datetime_local_value($input['registration_start'] ?? ''),
|
||||||
'registration_end' => itstudio_join_sanitize_datetime_local_value($input['registration_end'] ?? ''),
|
'registration_end' => itstudio_join_sanitize_datetime_local_value($input['registration_end'] ?? ''),
|
||||||
'first_interview_date' => itstudio_join_sanitize_datetime_local_value($input['first_interview_date'] ?? ''),
|
'first_interview_date' => $first_interview_schedule['start'],
|
||||||
'first_interview_end' => itstudio_join_sanitize_datetime_local_value($input['first_interview_end'] ?? ''),
|
'first_interview_end' => $first_interview_schedule['end'],
|
||||||
'first_interview_location_cn' => itstudio_join_sanitize_shortcode_value($input['first_interview_location_cn'] ?? ''),
|
'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'] ?? ''),
|
'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_date' => $second_interview_schedule['start'],
|
||||||
'second_interview_end' => itstudio_join_sanitize_datetime_local_value($input['second_interview_end'] ?? ''),
|
'second_interview_end' => $second_interview_schedule['end'],
|
||||||
'second_interview_location_cn' => itstudio_join_sanitize_shortcode_value($input['second_interview_location_cn'] ?? ''),
|
'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'] ?? ''),
|
'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_start_date' => itstudio_join_sanitize_date_value($input['assessment_start_date'] ?? ''),
|
||||||
@@ -179,6 +224,10 @@ function itstudio_join_sanitize_settings($input) {
|
|||||||
'result_second_interview_records' => itstudio_join_sanitize_records_value($input['result_second_interview_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'] ?? ''),
|
'result_admission_records' => itstudio_join_sanitize_records_value($input['result_admission_records'] ?? ''),
|
||||||
'signup_form_shortcode' => itstudio_join_sanitize_shortcode_value($input['signup_form_shortcode'] ?? ''),
|
'signup_form_shortcode' => itstudio_join_sanitize_shortcode_value($input['signup_form_shortcode'] ?? ''),
|
||||||
|
'otp_enabled' => !empty($input['otp_enabled']) ? 1 : 0,
|
||||||
|
'otp_form_id' => absint($input['otp_form_id'] ?? 0),
|
||||||
|
'otp_email_field_id' => absint($input['otp_email_field_id'] ?? 0),
|
||||||
|
'otp_code_field_id' => absint($input['otp_code_field_id'] ?? 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach (itstudio_join_get_photo_field_map() as $field_key) {
|
foreach (itstudio_join_get_photo_field_map() as $field_key) {
|
||||||
@@ -273,6 +322,11 @@ function itstudio_join_to_date_input_value($value) {
|
|||||||
return $parsed instanceof DateTimeImmutable ? $parsed->format('Y-m-d') : '';
|
return $parsed instanceof DateTimeImmutable ? $parsed->format('Y-m-d') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function itstudio_join_to_time_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('H:i') : '';
|
||||||
|
}
|
||||||
|
|
||||||
function itstudio_join_get_result_field_map() {
|
function itstudio_join_get_result_field_map() {
|
||||||
return array(
|
return array(
|
||||||
'registration' => 'result_registration_records',
|
'registration' => 'result_registration_records',
|
||||||
@@ -514,30 +568,27 @@ function itstudio_join_parse_result_rows_to_records($rows) {
|
|||||||
$records = array();
|
$records = array();
|
||||||
|
|
||||||
$is_header_row = static function ($name, $qq, $email, $student_id, $phone, $passed_raw) {
|
$is_header_row = static function ($name, $qq, $email, $student_id, $phone, $passed_raw) {
|
||||||
$joined = implode(',', array($name, $qq, $email, $student_id, $phone, $passed_raw));
|
$normalize_header = static function ($value) {
|
||||||
$joined = preg_replace('/^\xEF\xBB\xBF/', '', (string) $joined);
|
$value = preg_replace('/^\xEF\xBB\xBF/', '', trim((string) $value));
|
||||||
$joined = preg_replace('/\s+/u', '', (string) $joined);
|
$value = preg_replace('/[\s_-]+/u', '', (string) $value);
|
||||||
$joined_lower = function_exists('mb_strtolower') ? mb_strtolower($joined, 'UTF-8') : strtolower($joined);
|
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
|
||||||
|
};
|
||||||
$is_cn_header = (
|
$values = array_map($normalize_header, array($name, $qq, $email, $student_id, $phone, $passed_raw));
|
||||||
strpos($joined_lower, '姓名') !== false
|
$header_names = array(
|
||||||
|| strpos($joined_lower, 'qq') !== false
|
array('姓名', 'name'),
|
||||||
|| strpos($joined_lower, '邮箱') !== false
|
array('qq', 'qq号', 'qq号码'),
|
||||||
|| strpos($joined_lower, '学号') !== false
|
array('邮箱', '电子邮箱', 'email'),
|
||||||
|| strpos($joined_lower, '手机') !== false
|
array('学号', 'studentid', 'studentnumber'),
|
||||||
|| strpos($joined_lower, '是否通过') !== false
|
array('手机', '手机号', '手机号码', '电话', 'phone'),
|
||||||
|
array('是否通过', '通过', 'pass', 'passed'),
|
||||||
);
|
);
|
||||||
|
$matched_columns = 0;
|
||||||
$is_en_header = (
|
foreach ($values as $index => $value) {
|
||||||
strpos($joined_lower, 'name') !== false
|
if ($value !== '' && in_array($value, $header_names[$index], true)) {
|
||||||
|| strpos($joined_lower, 'qq') !== false
|
$matched_columns++;
|
||||||
|| strpos($joined_lower, 'email') !== false
|
}
|
||||||
|| strpos($joined_lower, 'student') !== false
|
}
|
||||||
|| strpos($joined_lower, 'phone') !== false
|
return $matched_columns >= 2;
|
||||||
|| strpos($joined_lower, 'pass') !== false
|
|
||||||
);
|
|
||||||
|
|
||||||
return $is_cn_header || $is_en_header;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach ($rows as $idx => $row) {
|
foreach ($rows as $idx => $row) {
|
||||||
@@ -545,7 +596,7 @@ function itstudio_join_parse_result_rows_to_records($rows) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = trim((string) ($row[0] ?? ''));
|
$name = preg_replace('/^\xEF\xBB\xBF/', '', trim((string) ($row[0] ?? '')));
|
||||||
$qq = trim((string) ($row[1] ?? ''));
|
$qq = trim((string) ($row[1] ?? ''));
|
||||||
$email = trim((string) ($row[2] ?? ''));
|
$email = trim((string) ($row[2] ?? ''));
|
||||||
$student_id = trim((string) ($row[3] ?? ''));
|
$student_id = trim((string) ($row[3] ?? ''));
|
||||||
@@ -566,7 +617,7 @@ function itstudio_join_parse_result_rows_to_records($rows) {
|
|||||||
'qq' => itstudio_join_normalize_lookup_value('qq', $qq),
|
'qq' => itstudio_join_normalize_lookup_value('qq', $qq),
|
||||||
'email' => itstudio_join_normalize_lookup_value('email', $email),
|
'email' => itstudio_join_normalize_lookup_value('email', $email),
|
||||||
'student_id' => itstudio_join_normalize_lookup_value('student_id', $student_id),
|
'student_id' => itstudio_join_normalize_lookup_value('student_id', $student_id),
|
||||||
'phone' => preg_replace('/\D+/', '', $phone),
|
'phone' => itstudio_join_normalize_lookup_value('phone', $phone),
|
||||||
'passed' => ($passed_raw === '1'),
|
'passed' => ($passed_raw === '1'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -717,9 +768,38 @@ function itstudio_join_is_truthy_result_value($value) {
|
|||||||
), true);
|
), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function itstudio_join_expand_progress_query_candidates($query) {
|
||||||
|
$query = is_array($query) ? $query : array();
|
||||||
|
$alternatives = isset($query['_alternatives']) && is_array($query['_alternatives']) ? $query['_alternatives'] : array();
|
||||||
|
if (empty($alternatives)) {
|
||||||
|
unset($query['_alternatives']);
|
||||||
|
return array($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = array();
|
||||||
|
foreach ($alternatives as $candidate) {
|
||||||
|
if (!is_array($candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
unset($candidate['_alternatives']);
|
||||||
|
$candidates[] = $candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !empty($candidates) ? $candidates : array($query);
|
||||||
|
}
|
||||||
|
|
||||||
function itstudio_join_find_formidable_entry_id_by_query($settings, $query) {
|
function itstudio_join_find_formidable_entry_id_by_query($settings, $query) {
|
||||||
$settings = is_array($settings) ? $settings : array();
|
$settings = is_array($settings) ? $settings : array();
|
||||||
$query = is_array($query) ? $query : array();
|
$query = is_array($query) ? $query : array();
|
||||||
|
if (!empty($query['_alternatives'])) {
|
||||||
|
foreach (itstudio_join_expand_progress_query_candidates($query) as $candidate) {
|
||||||
|
$entry_id = itstudio_join_find_formidable_entry_id_by_query($settings, $candidate);
|
||||||
|
if ($entry_id > 0) {
|
||||||
|
return $entry_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (!class_exists('FrmEntry') || !class_exists('FrmEntryMeta')) {
|
if (!class_exists('FrmEntry') || !class_exists('FrmEntryMeta')) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -863,6 +943,14 @@ function itstudio_join_formidable_stage_has_data($settings, $stage_key) {
|
|||||||
function itstudio_join_formidable_has_queryable_identity_field($settings, $query) {
|
function itstudio_join_formidable_has_queryable_identity_field($settings, $query) {
|
||||||
$settings = is_array($settings) ? $settings : array();
|
$settings = is_array($settings) ? $settings : array();
|
||||||
$query = is_array($query) ? $query : array();
|
$query = is_array($query) ? $query : array();
|
||||||
|
if (!empty($query['_alternatives'])) {
|
||||||
|
foreach (itstudio_join_expand_progress_query_candidates($query) as $candidate) {
|
||||||
|
if (itstudio_join_formidable_has_queryable_identity_field($settings, $candidate)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$form_id = itstudio_join_get_formidable_form_id($settings);
|
$form_id = itstudio_join_get_formidable_form_id($settings);
|
||||||
if ($form_id <= 0) {
|
if ($form_id <= 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -919,7 +1007,7 @@ function itstudio_join_normalize_lookup_value($field, $value) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($field === 'qq') {
|
if ($field === 'qq' || $field === 'phone') {
|
||||||
return preg_replace('/\D+/', '', $value);
|
return preg_replace('/\D+/', '', $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,6 +1034,7 @@ function itstudio_join_detect_progress_query_identity($raw_identity) {
|
|||||||
$detected = array(
|
$detected = array(
|
||||||
'raw' => $raw_identity,
|
'raw' => $raw_identity,
|
||||||
'field' => '',
|
'field' => '',
|
||||||
|
'fields' => array(),
|
||||||
'value' => '',
|
'value' => '',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -956,6 +1045,7 @@ function itstudio_join_detect_progress_query_identity($raw_identity) {
|
|||||||
$email = sanitize_email($compacted);
|
$email = sanitize_email($compacted);
|
||||||
if ($email !== '' && is_email($email)) {
|
if ($email !== '' && is_email($email)) {
|
||||||
$detected['field'] = 'email';
|
$detected['field'] = 'email';
|
||||||
|
$detected['fields'] = array('email');
|
||||||
$detected['value'] = itstudio_join_normalize_lookup_value('email', $email);
|
$detected['value'] = itstudio_join_normalize_lookup_value('email', $email);
|
||||||
return $detected;
|
return $detected;
|
||||||
}
|
}
|
||||||
@@ -964,15 +1054,21 @@ function itstudio_join_detect_progress_query_identity($raw_identity) {
|
|||||||
$digits = $compacted;
|
$digits = $compacted;
|
||||||
$len = strlen($digits);
|
$len = strlen($digits);
|
||||||
|
|
||||||
if ($len >= 10 && $len <= 12) {
|
if (preg_match('/^1[3-9]\d{9}$/', $digits)) {
|
||||||
$detected['field'] = 'student_id';
|
$detected['field'] = 'phone';
|
||||||
$detected['value'] = itstudio_join_normalize_lookup_value('student_id', $digits);
|
$detected['fields'] = array('phone');
|
||||||
|
$detected['value'] = itstudio_join_normalize_lookup_value('phone', $digits);
|
||||||
return $detected;
|
return $detected;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($len >= 5 && $len <= 12) {
|
if ($len >= 5 && $len <= 12) {
|
||||||
$detected['field'] = 'qq';
|
if ($len <= 10) {
|
||||||
$detected['value'] = itstudio_join_normalize_lookup_value('qq', $digits);
|
$detected['fields'] = $len === 10 ? array('qq', 'student_id') : array('qq');
|
||||||
|
} else {
|
||||||
|
$detected['fields'] = array('student_id', 'qq');
|
||||||
|
}
|
||||||
|
$detected['field'] = $detected['fields'][0];
|
||||||
|
$detected['value'] = itstudio_join_normalize_lookup_value($detected['field'], $digits);
|
||||||
return $detected;
|
return $detected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -981,6 +1077,7 @@ function itstudio_join_detect_progress_query_identity($raw_identity) {
|
|||||||
|
|
||||||
if (preg_match('/^[\x{4e00}-\x{9fff}·]{2,20}$/u', $compacted)) {
|
if (preg_match('/^[\x{4e00}-\x{9fff}·]{2,20}$/u', $compacted)) {
|
||||||
$detected['field'] = 'name';
|
$detected['field'] = 'name';
|
||||||
|
$detected['fields'] = array('name');
|
||||||
$detected['value'] = itstudio_join_normalize_lookup_value('name', $compacted);
|
$detected['value'] = itstudio_join_normalize_lookup_value('name', $compacted);
|
||||||
return $detected;
|
return $detected;
|
||||||
}
|
}
|
||||||
@@ -1059,9 +1156,17 @@ function itstudio_join_parse_result_records($raw) {
|
|||||||
function itstudio_join_record_matches_query($record, $query) {
|
function itstudio_join_record_matches_query($record, $query) {
|
||||||
$record = is_array($record) ? $record : array();
|
$record = is_array($record) ? $record : array();
|
||||||
$query = is_array($query) ? $query : array();
|
$query = is_array($query) ? $query : array();
|
||||||
|
if (!empty($query['_alternatives'])) {
|
||||||
|
foreach (itstudio_join_expand_progress_query_candidates($query) as $candidate) {
|
||||||
|
if (itstudio_join_record_matches_query($record, $candidate)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$has_any_query = false;
|
$has_any_query = false;
|
||||||
|
|
||||||
foreach (array('name', 'qq', 'email', 'student_id') as $field) {
|
foreach (array('name', 'qq', 'email', 'student_id', 'phone') as $field) {
|
||||||
$q = trim((string) ($query[$field] ?? ''));
|
$q = trim((string) ($query[$field] ?? ''));
|
||||||
if ($q === '') {
|
if ($q === '') {
|
||||||
continue;
|
continue;
|
||||||
@@ -1081,19 +1186,21 @@ function itstudio_join_find_record_in_stage_results($settings, $stage_key, $quer
|
|||||||
if (itstudio_join_is_file_result_mode($settings)) {
|
if (itstudio_join_is_file_result_mode($settings)) {
|
||||||
$attachment_id = itstudio_join_get_result_file_attachment_id($settings, $stage_key);
|
$attachment_id = itstudio_join_get_result_file_attachment_id($settings, $stage_key);
|
||||||
if ($attachment_id <= 0) {
|
if ($attachment_id <= 0) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
$rows = itstudio_join_read_result_rows_from_attachment($attachment_id);
|
$rows = itstudio_join_read_result_rows_from_attachment($attachment_id);
|
||||||
$records = itstudio_join_parse_result_rows_to_records($rows);
|
$records = itstudio_join_parse_result_rows_to_records($rows);
|
||||||
if (empty($records)) {
|
if (empty($records)) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
foreach (itstudio_join_expand_progress_query_candidates($query) as $candidate) {
|
||||||
foreach ($records as $record) {
|
foreach ($records as $record) {
|
||||||
if (itstudio_join_record_matches_query($record, $query) && !empty($record['passed'])) {
|
if (itstudio_join_record_matches_query($record, $candidate)) {
|
||||||
return true;
|
return !empty($record['passed']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itstudio_join_is_formidable_result_mode($settings)) {
|
if (itstudio_join_is_formidable_result_mode($settings)) {
|
||||||
@@ -1107,7 +1214,7 @@ function itstudio_join_find_record_in_stage_results($settings, $stage_key, $quer
|
|||||||
}
|
}
|
||||||
$entry_id = (int) $entry_cache[$cache_key];
|
$entry_id = (int) $entry_cache[$cache_key];
|
||||||
if ($entry_id <= 0) {
|
if ($entry_id <= 0) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
return itstudio_join_formidable_entry_matches_stage($settings, $entry_id, $stage_key);
|
return itstudio_join_formidable_entry_matches_stage($settings, $entry_id, $stage_key);
|
||||||
}
|
}
|
||||||
@@ -1115,26 +1222,36 @@ function itstudio_join_find_record_in_stage_results($settings, $stage_key, $quer
|
|||||||
$field_map = itstudio_join_get_result_field_map();
|
$field_map = itstudio_join_get_result_field_map();
|
||||||
$field_key = isset($field_map[$stage_key]) ? $field_map[$stage_key] : '';
|
$field_key = isset($field_map[$stage_key]) ? $field_map[$stage_key] : '';
|
||||||
if ($field_key === '') {
|
if ($field_key === '') {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$raw = (string) ($settings[$field_key] ?? '');
|
$raw = (string) ($settings[$field_key] ?? '');
|
||||||
if (trim($raw) === '') {
|
if (trim($raw) === '') {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$records = itstudio_join_parse_result_records($raw);
|
$records = itstudio_join_parse_result_records($raw);
|
||||||
if (empty($records)) {
|
if (empty($records)) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (itstudio_join_expand_progress_query_candidates($query) as $candidate) {
|
||||||
foreach ($records as $record) {
|
foreach ($records as $record) {
|
||||||
if (itstudio_join_record_matches_query($record, $query)) {
|
if (itstudio_join_record_matches_query($record, $candidate)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_join_progress_record_not_found_response($response) {
|
||||||
|
$response = is_array($response) ? $response : array();
|
||||||
|
$response['message_cn'] = '未查询到该报名者,请检查输入信息是否正确。';
|
||||||
|
$response['message_en'] = 'No matching applicant was found. Please check the information and try again.';
|
||||||
|
$response['tone'] = 'warning';
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function itstudio_join_get_stage_status_by_key($runtime, $stage_key) {
|
function itstudio_join_get_stage_status_by_key($runtime, $stage_key) {
|
||||||
@@ -1180,20 +1297,30 @@ function itstudio_join_resolve_progress_lookup($runtime = array(), $request_sour
|
|||||||
'qq' => '',
|
'qq' => '',
|
||||||
'email' => '',
|
'email' => '',
|
||||||
'student_id' => '',
|
'student_id' => '',
|
||||||
|
'phone' => '',
|
||||||
);
|
);
|
||||||
if ($detected_identity['field'] !== '') {
|
$detected_fields = isset($detected_identity['fields']) && is_array($detected_identity['fields']) ? $detected_identity['fields'] : array();
|
||||||
$query[$detected_identity['field']] = (string) $detected_identity['value'];
|
if (count($detected_fields) === 1) {
|
||||||
|
$query[$detected_fields[0]] = (string) $detected_identity['value'];
|
||||||
|
} elseif (count($detected_fields) > 1) {
|
||||||
|
$query['_alternatives'] = array();
|
||||||
|
foreach ($detected_fields as $field) {
|
||||||
|
$candidate = $query;
|
||||||
|
unset($candidate['_alternatives']);
|
||||||
|
$candidate[$field] = itstudio_join_normalize_lookup_value($field, $raw_identity);
|
||||||
|
$query['_alternatives'][] = $candidate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$has_input_value = trim((string) $raw_identity) !== '';
|
$has_input_value = trim((string) $raw_identity) !== '';
|
||||||
$has_query_value = $detected_identity['field'] !== '';
|
$has_query_value = !empty($detected_fields);
|
||||||
|
|
||||||
$submitted = isset($request_source['join_progress_lookup']) || $has_input_value;
|
$submitted = isset($request_source['join_progress_lookup']) || $has_input_value;
|
||||||
$response = array(
|
$response = array(
|
||||||
'submitted' => $submitted,
|
'submitted' => $submitted,
|
||||||
'has_query' => $has_query_value,
|
'has_query' => $has_query_value,
|
||||||
'identity' => $raw_identity,
|
'identity' => $raw_identity,
|
||||||
'identity_field' => (string) ($detected_identity['field'] ?? ''),
|
'identity_field' => implode('|', $detected_fields),
|
||||||
'name' => $raw_name,
|
'name' => $raw_name,
|
||||||
'qq' => $raw_qq,
|
'qq' => $raw_qq,
|
||||||
'email' => $raw_email,
|
'email' => $raw_email,
|
||||||
@@ -1208,15 +1335,15 @@ function itstudio_join_resolve_progress_lookup($runtime = array(), $request_sour
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$has_input_value) {
|
if (!$has_input_value) {
|
||||||
$response['message_cn'] = '请输入姓名(中文)/ QQ / 邮箱 / 学号。';
|
$response['message_cn'] = '请输入姓名(中文)/ QQ / 邮箱 / 学号 / 手机号。';
|
||||||
$response['message_en'] = 'Please enter Name (Chinese) / QQ / Email / Student ID.';
|
$response['message_en'] = 'Please enter Name (Chinese) / QQ / Email / Student ID / Phone.';
|
||||||
$response['tone'] = 'warning';
|
$response['tone'] = 'warning';
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$has_query_value) {
|
if (!$has_query_value) {
|
||||||
$response['message_cn'] = '无法识别输入内容,请输入姓名(中文)/ QQ / 邮箱 / 学号。';
|
$response['message_cn'] = '无法识别输入内容,请输入姓名(中文)/ QQ / 邮箱 / 学号 / 手机号。';
|
||||||
$response['message_en'] = 'Input cannot be recognized. Please enter Name (Chinese) / QQ / Email / Student ID.';
|
$response['message_en'] = 'Input cannot be recognized. Please enter Name (Chinese) / QQ / Email / Student ID / Phone.';
|
||||||
$response['tone'] = 'warning';
|
$response['tone'] = 'warning';
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
@@ -1254,6 +1381,9 @@ function itstudio_join_resolve_progress_lookup($runtime = array(), $request_sour
|
|||||||
}
|
}
|
||||||
|
|
||||||
$passed_first = itstudio_join_find_record_in_stage_results($settings, 'first_interview', $query);
|
$passed_first = itstudio_join_find_record_in_stage_results($settings, 'first_interview', $query);
|
||||||
|
if ($passed_first === null) {
|
||||||
|
return itstudio_join_progress_record_not_found_response($response);
|
||||||
|
}
|
||||||
if (!$passed_first) {
|
if (!$passed_first) {
|
||||||
$response['message_cn'] = '未通过第一次面试,期待来年再次报名。';
|
$response['message_cn'] = '未通过第一次面试,期待来年再次报名。';
|
||||||
$response['message_en'] = 'You did not pass Interview I. Welcome to apply again next year.';
|
$response['message_en'] = 'You did not pass Interview I. Welcome to apply again next year.';
|
||||||
@@ -1278,6 +1408,9 @@ function itstudio_join_resolve_progress_lookup($runtime = array(), $request_sour
|
|||||||
}
|
}
|
||||||
|
|
||||||
$passed_assessment = itstudio_join_find_record_in_stage_results($settings, 'assessment', $query);
|
$passed_assessment = itstudio_join_find_record_in_stage_results($settings, 'assessment', $query);
|
||||||
|
if ($passed_assessment === null) {
|
||||||
|
return itstudio_join_progress_record_not_found_response($response);
|
||||||
|
}
|
||||||
if (!$passed_assessment) {
|
if (!$passed_assessment) {
|
||||||
$response['message_cn'] = '未通过国庆能力摸底,期待来年再次报名。';
|
$response['message_cn'] = '未通过国庆能力摸底,期待来年再次报名。';
|
||||||
$response['message_en'] = 'You did not pass the assessment stage. Welcome to apply again next year.';
|
$response['message_en'] = 'You did not pass the assessment stage. Welcome to apply again next year.';
|
||||||
@@ -1302,6 +1435,9 @@ function itstudio_join_resolve_progress_lookup($runtime = array(), $request_sour
|
|||||||
}
|
}
|
||||||
|
|
||||||
$passed_second = itstudio_join_find_record_in_stage_results($settings, 'second_interview', $query);
|
$passed_second = itstudio_join_find_record_in_stage_results($settings, 'second_interview', $query);
|
||||||
|
if ($passed_second === null) {
|
||||||
|
return itstudio_join_progress_record_not_found_response($response);
|
||||||
|
}
|
||||||
if (!$passed_second) {
|
if (!$passed_second) {
|
||||||
$response['message_cn'] = '未通过第二次面试,期待来年再次报名。';
|
$response['message_cn'] = '未通过第二次面试,期待来年再次报名。';
|
||||||
$response['message_en'] = 'You did not pass Interview II. Welcome to apply again next year.';
|
$response['message_en'] = 'You did not pass Interview II. Welcome to apply again next year.';
|
||||||
@@ -1312,6 +1448,9 @@ function itstudio_join_resolve_progress_lookup($runtime = array(), $request_sour
|
|||||||
if ($status_notice === 'pending' || $status_notice === 'upcoming' || $status_notice === 'active') {
|
if ($status_notice === 'pending' || $status_notice === 'upcoming' || $status_notice === 'active') {
|
||||||
if ($status_notice === 'active' && $uploaded_notice) {
|
if ($status_notice === 'active' && $uploaded_notice) {
|
||||||
$admitted = itstudio_join_find_record_in_stage_results($settings, 'public_notice', $query);
|
$admitted = itstudio_join_find_record_in_stage_results($settings, 'public_notice', $query);
|
||||||
|
if ($admitted === null) {
|
||||||
|
return itstudio_join_progress_record_not_found_response($response);
|
||||||
|
}
|
||||||
if ($admitted) {
|
if ($admitted) {
|
||||||
$response['message_cn'] = '恭喜,您已被录取。';
|
$response['message_cn'] = '恭喜,您已被录取。';
|
||||||
$response['message_en'] = 'Congratulations! You have been admitted.';
|
$response['message_en'] = 'Congratulations! You have been admitted.';
|
||||||
@@ -1334,6 +1473,9 @@ function itstudio_join_resolve_progress_lookup($runtime = array(), $request_sour
|
|||||||
|
|
||||||
if (($status_notice === 'active' || $status_notice === 'completed') && $uploaded_notice) {
|
if (($status_notice === 'active' || $status_notice === 'completed') && $uploaded_notice) {
|
||||||
$admitted = itstudio_join_find_record_in_stage_results($settings, 'public_notice', $query);
|
$admitted = itstudio_join_find_record_in_stage_results($settings, 'public_notice', $query);
|
||||||
|
if ($admitted === null) {
|
||||||
|
return itstudio_join_progress_record_not_found_response($response);
|
||||||
|
}
|
||||||
if ($admitted) {
|
if ($admitted) {
|
||||||
$response['message_cn'] = '恭喜,您已被录取。';
|
$response['message_cn'] = '恭喜,您已被录取。';
|
||||||
$response['message_en'] = 'Congratulations! You have been admitted.';
|
$response['message_en'] = 'Congratulations! You have been admitted.';
|
||||||
@@ -1445,6 +1587,20 @@ function itstudio_join_format_stage_range($start, $end, $all_day = false) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$all_day && $start->format('Y-m-d') === $end->format('Y-m-d')) {
|
||||||
|
$date = $start->format($format_cn_day);
|
||||||
|
$start_time = $start->format('H:i');
|
||||||
|
$end_time = $end->format('H:i');
|
||||||
|
$range = $date . ' ' . $start_time;
|
||||||
|
if ($start_time !== $end_time) {
|
||||||
|
$range .= '-' . $end_time;
|
||||||
|
}
|
||||||
|
return array(
|
||||||
|
'cn' => $range,
|
||||||
|
'en' => $range,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$start_cn = $all_day ? $start->format($format_cn_day) : $start->format($format_cn_full);
|
$start_cn = $all_day ? $start->format($format_cn_day) : $start->format($format_cn_full);
|
||||||
$end_cn = $all_day ? $end->format($format_cn_day) : $end->format($format_cn_full);
|
$end_cn = $all_day ? $end->format($format_cn_day) : $end->format($format_cn_full);
|
||||||
$start_en = $all_day ? $start->format($format_en_day) : $start->format($format_en_full);
|
$start_en = $all_day ? $start->format($format_en_day) : $start->format($format_en_full);
|
||||||
@@ -1463,6 +1619,24 @@ function itstudio_join_format_stage_range($start, $end, $all_day = false) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function itstudio_join_format_stage_overlay_range($start, $end, $all_day = false) {
|
||||||
|
if (!$all_day) {
|
||||||
|
$date = $start instanceof DateTimeImmutable ? $start : $end;
|
||||||
|
$is_same_day = !($start instanceof DateTimeImmutable)
|
||||||
|
|| !($end instanceof DateTimeImmutable)
|
||||||
|
|| $start->format('Y-m-d') === $end->format('Y-m-d');
|
||||||
|
if ($date instanceof DateTimeImmutable && $is_same_day) {
|
||||||
|
$formatted = $date->format('Y.m.d');
|
||||||
|
return array(
|
||||||
|
'cn' => $formatted,
|
||||||
|
'en' => $formatted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return itstudio_join_format_stage_range($start, $end, $all_day);
|
||||||
|
}
|
||||||
|
|
||||||
function itstudio_join_get_stage_photo_url($settings, $stage_key) {
|
function itstudio_join_get_stage_photo_url($settings, $stage_key) {
|
||||||
$settings = is_array($settings) ? $settings : array();
|
$settings = is_array($settings) ? $settings : array();
|
||||||
$field_map = itstudio_join_get_photo_field_map();
|
$field_map = itstudio_join_get_photo_field_map();
|
||||||
@@ -1655,6 +1829,7 @@ function itstudio_join_get_runtime_data() {
|
|||||||
$stages = array();
|
$stages = array();
|
||||||
foreach ($stage_seed as $stage) {
|
foreach ($stage_seed as $stage) {
|
||||||
$range = itstudio_join_format_stage_range($stage['start'], $stage['end'], !empty($stage['all_day']));
|
$range = itstudio_join_format_stage_range($stage['start'], $stage['end'], !empty($stage['all_day']));
|
||||||
|
$overlay_range = itstudio_join_format_stage_overlay_range($stage['start'], $stage['end'], !empty($stage['all_day']));
|
||||||
$status = itstudio_join_resolve_stage_status($now, $stage['start'], $stage['end']);
|
$status = itstudio_join_resolve_stage_status($now, $stage['start'], $stage['end']);
|
||||||
$stages[] = array(
|
$stages[] = array(
|
||||||
'key' => $stage['key'],
|
'key' => $stage['key'],
|
||||||
@@ -1665,6 +1840,8 @@ function itstudio_join_get_runtime_data() {
|
|||||||
'status' => $status,
|
'status' => $status,
|
||||||
'range_cn' => $range['cn'],
|
'range_cn' => $range['cn'],
|
||||||
'range_en' => $range['en'],
|
'range_en' => $range['en'],
|
||||||
|
'overlay_range_cn' => $overlay_range['cn'],
|
||||||
|
'overlay_range_en' => $overlay_range['en'],
|
||||||
'location_cn' => isset($stage['location_cn']) ? (string) $stage['location_cn'] : '',
|
'location_cn' => isset($stage['location_cn']) ? (string) $stage['location_cn'] : '',
|
||||||
'location_en' => isset($stage['location_en']) ? (string) $stage['location_en'] : '',
|
'location_en' => isset($stage['location_en']) ? (string) $stage['location_en'] : '',
|
||||||
'result_uploaded' => !empty($stage['result_uploaded']),
|
'result_uploaded' => !empty($stage['result_uploaded']),
|
||||||
@@ -1944,6 +2121,15 @@ function itstudio_join_register_settings_page() {
|
|||||||
}
|
}
|
||||||
add_action('admin_menu', 'itstudio_join_register_settings_page');
|
add_action('admin_menu', 'itstudio_join_register_settings_page');
|
||||||
|
|
||||||
|
function itstudio_join_enqueue_settings_page_media($hook_suffix) {
|
||||||
|
if ($hook_suffix !== 'settings_page_itstudio-join-settings') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_media();
|
||||||
|
}
|
||||||
|
add_action('admin_enqueue_scripts', 'itstudio_join_enqueue_settings_page_media');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一跳回招新设置页并带上提示参数。
|
* 统一跳回招新设置页并带上提示参数。
|
||||||
*/
|
*/
|
||||||
@@ -1962,11 +2148,50 @@ function itstudio_join_settings_error_message($code) {
|
|||||||
'delete_last' => '至少要保留一个学期。',
|
'delete_last' => '至少要保留一个学期。',
|
||||||
'delete_failed' => '删除失败,学期不存在。',
|
'delete_failed' => '删除失败,学期不存在。',
|
||||||
'save_failed' => '保存失败,学期不存在。',
|
'save_failed' => '保存失败,学期不存在。',
|
||||||
|
'result_upload_failed' => '结果文件上传失败,请确认文件为 CSV 或 XLSX 后重试。',
|
||||||
'need_year' => '请填写学年或自定义名称。',
|
'need_year' => '请填写学年或自定义名称。',
|
||||||
);
|
);
|
||||||
return isset($map[$code]) ? $map[$code] : '操作失败。';
|
return isset($map[$code]) ? $map[$code] : '操作失败。';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function itstudio_join_handle_result_file_uploads($settings) {
|
||||||
|
$settings = is_array($settings) ? $settings : array();
|
||||||
|
|
||||||
|
foreach (itstudio_join_get_result_file_field_map() as $field_key) {
|
||||||
|
$upload_key = 'itstudio_join_upload_' . $field_key;
|
||||||
|
if (!isset($_FILES[$upload_key]) || !is_array($_FILES[$upload_key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$upload_error = isset($_FILES[$upload_key]['error']) ? (int) $_FILES[$upload_key]['error'] : UPLOAD_ERR_NO_FILE;
|
||||||
|
if ($upload_error === UPLOAD_ERR_NO_FILE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($upload_error !== UPLOAD_ERR_OK) {
|
||||||
|
return new WP_Error('result_upload_failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = isset($_FILES[$upload_key]['name']) ? sanitize_file_name(wp_unslash($_FILES[$upload_key]['name'])) : '';
|
||||||
|
$extension = strtolower((string) pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
if (!in_array($extension, array('csv', 'xlsx'), true)) {
|
||||||
|
return new WP_Error('result_upload_failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||||
|
|
||||||
|
$attachment_id = media_handle_upload($upload_key, 0);
|
||||||
|
if (is_wp_error($attachment_id)) {
|
||||||
|
return new WP_Error('result_upload_failed', $attachment_id->get_error_message());
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings[$field_key] = absint($attachment_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存某学期的配置(含学期元信息)。
|
* 保存某学期的配置(含学期元信息)。
|
||||||
*/
|
*/
|
||||||
@@ -1980,6 +2205,11 @@ function itstudio_join_handle_save_term_settings() {
|
|||||||
$raw = (isset($_POST['itstudio_join_settings']) && is_array($_POST['itstudio_join_settings'])) ? wp_unslash($_POST['itstudio_join_settings']) : array();
|
$raw = (isset($_POST['itstudio_join_settings']) && is_array($_POST['itstudio_join_settings'])) ? wp_unslash($_POST['itstudio_join_settings']) : array();
|
||||||
$meta = (isset($_POST['itstudio_join_term_meta']) && is_array($_POST['itstudio_join_term_meta'])) ? wp_unslash($_POST['itstudio_join_term_meta']) : array();
|
$meta = (isset($_POST['itstudio_join_term_meta']) && is_array($_POST['itstudio_join_term_meta'])) ? wp_unslash($_POST['itstudio_join_term_meta']) : array();
|
||||||
|
|
||||||
|
$raw = itstudio_join_handle_result_file_uploads($raw);
|
||||||
|
if (is_wp_error($raw)) {
|
||||||
|
itstudio_join_settings_redirect(array('term' => $term_id, 'error' => $raw->get_error_code()));
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($meta)) {
|
if (!empty($meta)) {
|
||||||
itstudio_join_update_term_meta(
|
itstudio_join_update_term_meta(
|
||||||
$term_id,
|
$term_id,
|
||||||
@@ -2125,15 +2355,58 @@ function itstudio_join_render_result_file_row($field_key, $label, $settings, $de
|
|||||||
$attachment_id = isset($settings[$field_key]) ? absint($settings[$field_key]) : 0;
|
$attachment_id = isset($settings[$field_key]) ? absint($settings[$field_key]) : 0;
|
||||||
$file_url = $attachment_id > 0 ? wp_get_attachment_url($attachment_id) : '';
|
$file_url = $attachment_id > 0 ? wp_get_attachment_url($attachment_id) : '';
|
||||||
$filename = $attachment_id > 0 ? basename((string) get_attached_file($attachment_id)) : '';
|
$filename = $attachment_id > 0 ? basename((string) get_attached_file($attachment_id)) : '';
|
||||||
|
static $result_files = null;
|
||||||
|
if ($result_files === null) {
|
||||||
|
$result_files = get_posts(array(
|
||||||
|
'post_type' => 'attachment',
|
||||||
|
'post_status' => 'inherit',
|
||||||
|
'post_mime_type' => array(
|
||||||
|
'text/csv',
|
||||||
|
'application/csv',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
),
|
||||||
|
'posts_per_page' => 100,
|
||||||
|
'orderby' => 'date',
|
||||||
|
'order' => 'DESC',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if ($attachment_id > 0 && !in_array($attachment_id, wp_list_pluck($result_files, 'ID'), true)) {
|
||||||
|
$current_file = get_post($attachment_id);
|
||||||
|
if ($current_file instanceof WP_Post && $current_file->post_type === 'attachment') {
|
||||||
|
$result_files[] = $current_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row"><label><?php echo esc_html($label); ?></label></th>
|
<th scope="row"><label><?php echo esc_html($label); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
<input type="hidden" class="itstudio-join-result-file-id" name="itstudio_join_settings[<?php echo esc_attr($field_key); ?>]" value="<?php echo esc_attr($attachment_id); ?>">
|
|
||||||
<div class="itstudio-join-result-file-preview" style="margin-bottom:10px;<?php echo $file_url !== '' ? '' : 'display:none;'; ?>">
|
<div class="itstudio-join-result-file-preview" style="margin-bottom:10px;<?php echo $file_url !== '' ? '' : 'display:none;'; ?>">
|
||||||
<a class="itstudio-join-result-file-link" href="<?php echo esc_url($file_url); ?>" target="_blank" rel="noopener"><?php echo esc_html($filename !== '' ? $filename : $file_url); ?></a>
|
<a class="itstudio-join-result-file-link" href="<?php echo esc_url($file_url); ?>" target="_blank" rel="noopener"><?php echo esc_html($filename !== '' ? $filename : $file_url); ?></a>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="button itstudio-join-result-file-upload"><?php esc_html_e('上传 / 选择结果文件', 'itstudio'); ?></button>
|
<select class="itstudio-join-result-file-id" name="itstudio_join_settings[<?php echo esc_attr($field_key); ?>]">
|
||||||
|
<option value="0">不选择已有文件</option>
|
||||||
|
<?php foreach ($result_files as $result_file) : ?>
|
||||||
|
<?php
|
||||||
|
$result_file_id = absint($result_file->ID);
|
||||||
|
$result_file_url = wp_get_attachment_url($result_file_id);
|
||||||
|
$result_file_name = basename((string) get_attached_file($result_file_id));
|
||||||
|
?>
|
||||||
|
<option
|
||||||
|
value="<?php echo esc_attr($result_file_id); ?>"
|
||||||
|
data-url="<?php echo esc_url($result_file_url); ?>"
|
||||||
|
data-filename="<?php echo esc_attr($result_file_name); ?>"
|
||||||
|
<?php selected($attachment_id, $result_file_id); ?>
|
||||||
|
><?php echo esc_html($result_file_name !== '' ? $result_file_name : $result_file->post_title); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="itstudio-join-result-file-upload"
|
||||||
|
name="<?php echo esc_attr('itstudio_join_upload_' . $field_key); ?>"
|
||||||
|
accept=".csv,.xlsx,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
style="display:block;margin-top:8px;"
|
||||||
|
>
|
||||||
<button type="button" class="button-link-delete itstudio-join-result-file-clear" style="margin-left:8px;<?php echo $file_url !== '' ? '' : 'display:none;'; ?>"><?php esc_html_e('移除', 'itstudio'); ?></button>
|
<button type="button" class="button-link-delete itstudio-join-result-file-clear" style="margin-left:8px;<?php echo $file_url !== '' ? '' : 'display:none;'; ?>"><?php esc_html_e('移除', 'itstudio'); ?></button>
|
||||||
<?php if ($description !== '') : ?>
|
<?php if ($description !== '') : ?>
|
||||||
<p class="description"><?php echo esc_html($description); ?></p>
|
<p class="description"><?php echo esc_html($description); ?></p>
|
||||||
@@ -2150,8 +2423,6 @@ function itstudio_join_render_settings_page() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wp_enqueue_media();
|
|
||||||
|
|
||||||
$terms = itstudio_join_get_terms();
|
$terms = itstudio_join_get_terms();
|
||||||
$active_id = itstudio_join_get_active_term_id();
|
$active_id = itstudio_join_get_active_term_id();
|
||||||
$editing_term_id = isset($_GET['term']) ? sanitize_text_field(wp_unslash($_GET['term'])) : '';
|
$editing_term_id = isset($_GET['term']) ? sanitize_text_field(wp_unslash($_GET['term'])) : '';
|
||||||
@@ -2251,7 +2522,7 @@ function itstudio_join_render_settings_page() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="<?php echo esc_url($admin_post_url); ?>">
|
<form method="post" action="<?php echo esc_url($admin_post_url); ?>" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="action" value="itstudio_join_save_term_settings">
|
<input type="hidden" name="action" value="itstudio_join_save_term_settings">
|
||||||
<input type="hidden" name="itstudio_join_term_id" value="<?php echo esc_attr($editing_term_id); ?>">
|
<input type="hidden" name="itstudio_join_term_id" value="<?php echo esc_attr($editing_term_id); ?>">
|
||||||
<?php wp_nonce_field('itstudio_join_save_term_settings'); ?>
|
<?php wp_nonce_field('itstudio_join_save_term_settings'); ?>
|
||||||
@@ -2292,15 +2563,14 @@ function itstudio_join_render_settings_page() {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row"><label for="itstudio_join_first_interview_date">第一次面试开始时间</label></th>
|
<th scope="row"><label for="itstudio_join_first_interview_day">第一次面试日期与时间</label></th>
|
||||||
<td>
|
<td>
|
||||||
<input type="datetime-local" id="itstudio_join_first_interview_date" name="itstudio_join_settings[first_interview_date]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['first_interview_date'], false)); ?>" class="regular-text">
|
<fieldset style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;">
|
||||||
</td>
|
<label>日期<br><input type="date" id="itstudio_join_first_interview_day" name="itstudio_join_settings[first_interview_day]" value="<?php echo esc_attr(itstudio_join_to_date_input_value((string) $settings['first_interview_date'])); ?>"></label>
|
||||||
</tr>
|
<label>开始时间<br><input type="time" name="itstudio_join_settings[first_interview_start_time]" value="<?php echo esc_attr(itstudio_join_to_time_input_value((string) $settings['first_interview_date'], false)); ?>"></label>
|
||||||
<tr>
|
<label>结束时间<br><input type="time" name="itstudio_join_settings[first_interview_end_time]" value="<?php echo esc_attr(itstudio_join_to_time_input_value((string) $settings['first_interview_end'], true)); ?>"></label>
|
||||||
<th scope="row"><label for="itstudio_join_first_interview_end">第一次面试结束时间</label></th>
|
</fieldset>
|
||||||
<td>
|
<p class="description">面试按同一天计算,请依次设置日期、开始时间和结束时间。</p>
|
||||||
<input type="datetime-local" id="itstudio_join_first_interview_end" name="itstudio_join_settings[first_interview_end]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['first_interview_end'], true)); ?>" class="regular-text">
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -2316,15 +2586,14 @@ function itstudio_join_render_settings_page() {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row"><label for="itstudio_join_second_interview_date">第二次面试开始时间</label></th>
|
<th scope="row"><label for="itstudio_join_second_interview_day">第二次面试日期与时间</label></th>
|
||||||
<td>
|
<td>
|
||||||
<input type="datetime-local" id="itstudio_join_second_interview_date" name="itstudio_join_settings[second_interview_date]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['second_interview_date'], false)); ?>" class="regular-text">
|
<fieldset style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;">
|
||||||
</td>
|
<label>日期<br><input type="date" id="itstudio_join_second_interview_day" name="itstudio_join_settings[second_interview_day]" value="<?php echo esc_attr(itstudio_join_to_date_input_value((string) $settings['second_interview_date'])); ?>"></label>
|
||||||
</tr>
|
<label>开始时间<br><input type="time" name="itstudio_join_settings[second_interview_start_time]" value="<?php echo esc_attr(itstudio_join_to_time_input_value((string) $settings['second_interview_date'], false)); ?>"></label>
|
||||||
<tr>
|
<label>结束时间<br><input type="time" name="itstudio_join_settings[second_interview_end_time]" value="<?php echo esc_attr(itstudio_join_to_time_input_value((string) $settings['second_interview_end'], true)); ?>"></label>
|
||||||
<th scope="row"><label for="itstudio_join_second_interview_end">第二次面试结束时间</label></th>
|
</fieldset>
|
||||||
<td>
|
<p class="description">面试按同一天计算,请依次设置日期、开始时间和结束时间。</p>
|
||||||
<input type="datetime-local" id="itstudio_join_second_interview_end" name="itstudio_join_settings[second_interview_end]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['second_interview_end'], true)); ?>" class="regular-text">
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -2385,6 +2654,23 @@ function itstudio_join_render_settings_page() {
|
|||||||
<p class="description">示例:<code>[formidable id="12"]</code></p>
|
<p class="description">示例:<code>[formidable id="12"]</code></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">邮箱 OTP 验证</th>
|
||||||
|
<td>
|
||||||
|
<label for="itstudio_join_otp_enabled">
|
||||||
|
<input type="checkbox" id="itstudio_join_otp_enabled" name="itstudio_join_settings[otp_enabled]" value="1" <?php checked(!empty($settings['otp_enabled'])); ?>>
|
||||||
|
在报名表单提交前启用邮箱验证码
|
||||||
|
</label>
|
||||||
|
<p class="description">启用后,前台会在指定验证码字段旁自动显示“发送验证码”按钮,不需要手动添加 HTML 按钮。</p>
|
||||||
|
<p style="margin:10px 0 4px;"><label for="itstudio_join_otp_form_id">Formidable 表单 ID</label></p>
|
||||||
|
<input type="number" min="1" step="1" id="itstudio_join_otp_form_id" name="itstudio_join_settings[otp_form_id]" value="<?php echo esc_attr((string) ($settings['otp_form_id'] ?? '')); ?>" class="small-text">
|
||||||
|
<p style="margin:10px 0 4px;"><label for="itstudio_join_otp_email_field_id">邮箱字段 ID</label></p>
|
||||||
|
<input type="number" min="1" step="1" id="itstudio_join_otp_email_field_id" name="itstudio_join_settings[otp_email_field_id]" value="<?php echo esc_attr((string) ($settings['otp_email_field_id'] ?? '')); ?>" class="small-text">
|
||||||
|
<p style="margin:10px 0 4px;"><label for="itstudio_join_otp_code_field_id">验证码字段 ID</label></p>
|
||||||
|
<input type="number" min="1" step="1" id="itstudio_join_otp_code_field_id" name="itstudio_join_settings[otp_code_field_id]" value="<?php echo esc_attr((string) ($settings['otp_code_field_id'] ?? '')); ?>" class="small-text">
|
||||||
|
<p class="description">验证码字段建议使用 Short Text,设置为仅允许数字、最大长度 6。邮箱字段必须是 Email 类型。验证码有效期 10 分钟,同一邮箱 60 秒内不能重复发送,最多尝试 5 次。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<?php submit_button('保存设置'); ?>
|
<?php submit_button('保存设置'); ?>
|
||||||
</form>
|
</form>
|
||||||
@@ -2502,57 +2788,28 @@ function itstudio_join_render_settings_page() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const resultFileButtons = document.querySelectorAll('.itstudio-join-result-file-upload');
|
document.querySelectorAll('.itstudio-join-result-file-id').forEach((select) => {
|
||||||
resultFileButtons.forEach((button) => {
|
select.addEventListener('change', () => {
|
||||||
button.addEventListener('click', () => {
|
const row = select.closest('td');
|
||||||
if (!window.wp || !wp.media) {
|
const option = select.options[select.selectedIndex];
|
||||||
return;
|
const preview = row ? row.querySelector('.itstudio-join-result-file-preview') : null;
|
||||||
|
const link = row ? row.querySelector('.itstudio-join-result-file-link') : null;
|
||||||
|
const clearBtn = row ? row.querySelector('.itstudio-join-result-file-clear') : null;
|
||||||
|
const upload = row ? row.querySelector('.itstudio-join-result-file-upload') : null;
|
||||||
|
const fileUrl = option ? option.dataset.url || '' : '';
|
||||||
|
const filename = option ? option.dataset.filename || '' : '';
|
||||||
|
if (upload) {
|
||||||
|
upload.value = '';
|
||||||
}
|
}
|
||||||
|
if (preview && link) {
|
||||||
const row = button.closest('td');
|
|
||||||
if (!row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = row.querySelector('.itstudio-join-result-file-id');
|
|
||||||
const preview = row.querySelector('.itstudio-join-result-file-preview');
|
|
||||||
const link = row.querySelector('.itstudio-join-result-file-link');
|
|
||||||
const clearBtn = row.querySelector('.itstudio-join-result-file-clear');
|
|
||||||
if (!input || !preview || !link) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frame = wp.media({
|
|
||||||
title: '选择结果文件',
|
|
||||||
button: { text: '使用此文件' },
|
|
||||||
multiple: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
frame.on('select', () => {
|
|
||||||
const selection = frame.state().get('selection').first();
|
|
||||||
if (!selection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = selection.toJSON();
|
|
||||||
const fileUrl = data.url || '';
|
|
||||||
const filename = data.filename || (fileUrl ? fileUrl.split('/').pop() : '');
|
|
||||||
const ext = filename.includes('.') ? filename.split('.').pop().toLowerCase() : '';
|
|
||||||
if (!['csv', 'xlsx'].includes(ext)) {
|
|
||||||
window.alert('请上传 CSV 或 XLSX 文件。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.value = data.id || '';
|
|
||||||
link.href = fileUrl || '#';
|
link.href = fileUrl || '#';
|
||||||
link.textContent = filename || fileUrl || '';
|
link.textContent = filename || fileUrl;
|
||||||
preview.style.display = fileUrl ? 'block' : 'none';
|
preview.style.display = fileUrl ? 'block' : 'none';
|
||||||
|
}
|
||||||
if (clearBtn) {
|
if (clearBtn) {
|
||||||
clearBtn.style.display = fileUrl ? 'inline' : 'none';
|
clearBtn.style.display = fileUrl ? 'inline' : 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frame.open();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.itstudio-join-result-file-clear').forEach((button) => {
|
document.querySelectorAll('.itstudio-join-result-file-clear').forEach((button) => {
|
||||||
@@ -2562,10 +2819,14 @@ function itstudio_join_render_settings_page() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const input = row.querySelector('.itstudio-join-result-file-id');
|
const input = row.querySelector('.itstudio-join-result-file-id');
|
||||||
|
const upload = row.querySelector('.itstudio-join-result-file-upload');
|
||||||
const preview = row.querySelector('.itstudio-join-result-file-preview');
|
const preview = row.querySelector('.itstudio-join-result-file-preview');
|
||||||
const link = row.querySelector('.itstudio-join-result-file-link');
|
const link = row.querySelector('.itstudio-join-result-file-link');
|
||||||
if (input) {
|
if (input) {
|
||||||
input.value = '';
|
input.value = '0';
|
||||||
|
}
|
||||||
|
if (upload) {
|
||||||
|
upload.value = '';
|
||||||
}
|
}
|
||||||
if (link) {
|
if (link) {
|
||||||
link.href = '#';
|
link.href = '#';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: ITStudio 招新系统
|
* Plugin Name: ITStudio 招新系统
|
||||||
* Plugin URI: https://git.itouc.cn/ITStudio_OUC/ITStudioMainSite
|
* Plugin URI: https://git.itouc.cn/ITStudio_OUC/ITStudioMainSite
|
||||||
* Description: ITStudio “加入我们”招新系统:招新文章标记、招新阶段时间状态机、录取进度查询(AJAX)、CSV/XLSX 结果解析、Formidable Forms 对接、以及后台“招新设置”页面。
|
* Description: ITStudio “加入我们”招新系统:招新文章标记、招新阶段时间状态机、录取进度查询(AJAX)、CSV/XLSX 结果解析、Formidable Forms 对接、以及后台“招新设置”页面。
|
||||||
* Version: 1.0.0
|
* Version: 1.0.2
|
||||||
* Author: ITStudio OUC
|
* Author: ITStudio OUC
|
||||||
* Text Domain: itstudio-recruitment
|
* Text Domain: itstudio-recruitment
|
||||||
* Requires Plugins: itstudio-content
|
* Requires Plugins: itstudio-content
|
||||||
@@ -23,8 +23,10 @@ define('ITSTUDIO_RECRUITMENT_PATH', plugin_dir_path(__FILE__));
|
|||||||
|
|
||||||
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/recruitment-articles.php';
|
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/recruitment-articles.php';
|
||||||
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/join-system.php';
|
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/join-system.php';
|
||||||
|
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/email-otp.php';
|
||||||
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/terms.php';
|
require_once ITSTUDIO_RECRUITMENT_PATH . 'includes/terms.php';
|
||||||
|
|
||||||
|
|
||||||
function itstudio_recruitment_activate() {
|
function itstudio_recruitment_activate() {
|
||||||
flush_rewrite_rules();
|
flush_rewrite_rules();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,589 +0,0 @@
|
|||||||
/* =========================================
|
|
||||||
工作室介绍页 - 旧首页视觉
|
|
||||||
========================================= */
|
|
||||||
|
|
||||||
.site-main {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html:has(body.intro-about) {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html:has(body.intro-about)::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about .site-header,
|
|
||||||
body.intro-about .site-footer {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about .hero-section {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about .about-theme-toggle {
|
|
||||||
position: fixed;
|
|
||||||
top: 18px;
|
|
||||||
right: 18px;
|
|
||||||
z-index: 40;
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
background: transparent !important;
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 0;
|
|
||||||
color: color-mix(in srgb, var(--text-primary) 82%, var(--color-primary) 18%);
|
|
||||||
box-shadow: none;
|
|
||||||
backdrop-filter: none;
|
|
||||||
-webkit-backdrop-filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about .about-theme-toggle::before {
|
|
||||||
display: none;
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about .about-theme-toggle svg {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about .about-theme-toggle:hover,
|
|
||||||
body.intro-about .about-theme-toggle:focus-visible {
|
|
||||||
background: transparent !important;
|
|
||||||
border: none !important;
|
|
||||||
color: color-mix(in srgb, var(--text-primary) 72%, var(--color-primary) 28%);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about .about-theme-toggle:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.intro-about .about-theme-toggle:hover::before,
|
|
||||||
body.intro-about .about-theme-toggle:focus-visible::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-section {
|
|
||||||
padding: 40px 0;
|
|
||||||
background-color: var(--bg-body);
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
background-image: none;
|
|
||||||
min-height: calc(100vh - var(--header-height));
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
isolation: isolate;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-scroll-indicator {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
bottom: 28px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
z-index: 4;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.45s var(--ease-in-out), transform 0.45s var(--ease-in-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-scroll-indicator .scroll-arrow {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-right: 2px solid currentColor;
|
|
||||||
border-bottom: 2px solid currentColor;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
animation: hero-scroll-bounce 1.6s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-scroll-indicator .scroll-text {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
letter-spacing: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes hero-scroll-bounce {
|
|
||||||
0%, 100% { transform: translateY(0) rotate(45deg); opacity: 0.5; }
|
|
||||||
50% { transform: translateY(6px) rotate(45deg); opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.home-hero-initial .site-header {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-hero-initial .site-header,
|
|
||||||
.home-hero-scrolled .site-header {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-hero-initial .hero-scroll-indicator {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(-50%) translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-hero-scrolled .hero-scroll-indicator {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-50%) translateY(10px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-waves {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 140vh;
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 2;
|
|
||||||
mix-blend-mode: screen;
|
|
||||||
filter: saturate(1.08);
|
|
||||||
display: block;
|
|
||||||
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%);
|
|
||||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.9) 55%, rgba(0, 0, 0, 0.35) 75%, rgba(0, 0, 0, 0) 100%);
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
-webkit-mask-size: 100% 100%;
|
|
||||||
mask-size: 100% 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not([data-theme="dark"]) .hero-waves {
|
|
||||||
opacity: 0.55;
|
|
||||||
mix-blend-mode: multiply;
|
|
||||||
filter: saturate(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .hero-waves {
|
|
||||||
opacity: 0.5;
|
|
||||||
mix-blend-mode: screen;
|
|
||||||
filter: saturate(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-section::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: clamp(2px, 0.6vw, 10px);
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
overflow: visible;
|
|
||||||
--hero-title-gradient: linear-gradient(135deg, var(--text-primary) 0%, var(--color-primary) 100%);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title-svg {
|
|
||||||
display: block;
|
|
||||||
width: min(88vw, 900px);
|
|
||||||
margin: 0 auto;
|
|
||||||
aspect-ratio: 3 / 2;
|
|
||||||
background-image: var(--hero-title-gradient);
|
|
||||||
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
|
|
||||||
mask: url("../../resources/title.svg") center / contain no-repeat;
|
|
||||||
filter:
|
|
||||||
drop-shadow(0 10px 24px rgba(12, 20, 32, 0.25))
|
|
||||||
drop-shadow(0 0 18px rgba(120, 190, 255, 0.28));
|
|
||||||
position: relative;
|
|
||||||
transform: translateY(-8px) scale(1.06);
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title-svg::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: -6% -4%;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 30% 35%, rgba(255, 255, 255, 0.35), transparent 55%),
|
|
||||||
radial-gradient(circle at 70% 45%, rgba(140, 205, 255, 0.28), transparent 60%),
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0));
|
|
||||||
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
|
|
||||||
mask: url("../../resources/title.svg") center / contain no-repeat;
|
|
||||||
opacity: 0.22;
|
|
||||||
filter: blur(6px);
|
|
||||||
mix-blend-mode: screen;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not([data-theme="dark"]) .hero-title-svg::before {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title-svg::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 100%;
|
|
||||||
height: 55%;
|
|
||||||
background-image: var(--hero-title-gradient);
|
|
||||||
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
|
|
||||||
mask: url("../../resources/title.svg") center / contain no-repeat;
|
|
||||||
transform: scaleY(-1);
|
|
||||||
opacity: 0.18;
|
|
||||||
filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title-text {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-description {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
min-height: 3.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-char {
|
|
||||||
transition: opacity 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-provided {
|
|
||||||
margin-top: 0;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-section {
|
|
||||||
padding: 80px 0;
|
|
||||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bg-body) 55%);
|
|
||||||
position: relative;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-provided h2 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: none;
|
|
||||||
color: var(--text-primary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-provided h2::before {
|
|
||||||
content: "$";
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-right: 10px;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-grid-box {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 32px 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
transition: none;
|
|
||||||
text-decoration: none;
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item:hover {
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
--svc-stroke: #2f3d4f;
|
|
||||||
--svc-muted: #5f7288;
|
|
||||||
--svc-accent: var(--color-primary);
|
|
||||||
--svc-soft: rgba(9, 105, 218, 0.12);
|
|
||||||
--svc-soft-strong: rgba(9, 105, 218, 0.2);
|
|
||||||
color: var(--svc-stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .service-icon {
|
|
||||||
--svc-stroke: #c4d1de;
|
|
||||||
--svc-muted: #93a4b7;
|
|
||||||
--svc-soft: rgba(88, 166, 255, 0.16);
|
|
||||||
--svc-soft-strong: rgba(88, 166, 255, 0.26);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.service-resources .service-icon {
|
|
||||||
--svc-accent: #2f7de1;
|
|
||||||
--svc-soft: rgba(47, 125, 225, 0.14);
|
|
||||||
--svc-soft-strong: rgba(47, 125, 225, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.service-mirror .service-icon {
|
|
||||||
--svc-accent: #1f9d8d;
|
|
||||||
--svc-soft: rgba(31, 157, 141, 0.14);
|
|
||||||
--svc-soft-strong: rgba(31, 157, 141, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.service-git .service-icon {
|
|
||||||
--svc-accent: #8b5cf6;
|
|
||||||
--svc-soft: rgba(139, 92, 246, 0.14);
|
|
||||||
--svc-soft-strong: rgba(139, 92, 246, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.service-minecraft .service-icon {
|
|
||||||
--svc-accent: #2e9b45;
|
|
||||||
--svc-soft: rgba(46, 155, 69, 0.14);
|
|
||||||
--svc-soft-strong: rgba(46, 155, 69, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.service-forum .service-icon {
|
|
||||||
--svc-accent: #d97706;
|
|
||||||
--svc-soft: rgba(217, 119, 6, 0.14);
|
|
||||||
--svc-soft-strong: rgba(217, 119, 6, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.service-repair .service-icon {
|
|
||||||
--svc-accent: #d94f45;
|
|
||||||
--svc-soft: rgba(217, 79, 69, 0.14);
|
|
||||||
--svc-soft-strong: rgba(217, 79, 69, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.service-workshop .service-icon {
|
|
||||||
--svc-accent: #c58a00;
|
|
||||||
--svc-soft: rgba(197, 138, 0, 0.14);
|
|
||||||
--svc-soft-strong: rgba(197, 138, 0, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.service-campus .service-icon {
|
|
||||||
--svc-accent: #0f8fb8;
|
|
||||||
--svc-soft: rgba(15, 143, 184, 0.14);
|
|
||||||
--svc-soft-strong: rgba(15, 143, 184, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .service-item.service-resources .service-icon {
|
|
||||||
--svc-accent: #7db5ff;
|
|
||||||
--svc-soft: rgba(125, 181, 255, 0.18);
|
|
||||||
--svc-soft-strong: rgba(125, 181, 255, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .service-item.service-mirror .service-icon {
|
|
||||||
--svc-accent: #5edac7;
|
|
||||||
--svc-soft: rgba(94, 218, 199, 0.18);
|
|
||||||
--svc-soft-strong: rgba(94, 218, 199, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .service-item.service-git .service-icon {
|
|
||||||
--svc-accent: #b9a0ff;
|
|
||||||
--svc-soft: rgba(185, 160, 255, 0.18);
|
|
||||||
--svc-soft-strong: rgba(185, 160, 255, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .service-item.service-minecraft .service-icon {
|
|
||||||
--svc-accent: #6fda83;
|
|
||||||
--svc-soft: rgba(111, 218, 131, 0.18);
|
|
||||||
--svc-soft-strong: rgba(111, 218, 131, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .service-item.service-forum .service-icon {
|
|
||||||
--svc-accent: #f5b04d;
|
|
||||||
--svc-soft: rgba(245, 176, 77, 0.18);
|
|
||||||
--svc-soft-strong: rgba(245, 176, 77, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .service-item.service-repair .service-icon {
|
|
||||||
--svc-accent: #ff8a80;
|
|
||||||
--svc-soft: rgba(255, 138, 128, 0.18);
|
|
||||||
--svc-soft-strong: rgba(255, 138, 128, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .service-item.service-workshop .service-icon {
|
|
||||||
--svc-accent: #ffd166;
|
|
||||||
--svc-soft: rgba(255, 209, 102, 0.18);
|
|
||||||
--svc-soft-strong: rgba(255, 209, 102, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .service-item.service-campus .service-icon {
|
|
||||||
--svc-accent: #68d5ff;
|
|
||||||
--svc-soft: rgba(104, 213, 255, 0.18);
|
|
||||||
--svc-soft-strong: rgba(104, 213, 255, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .service-symbol {
|
|
||||||
width: 90px;
|
|
||||||
height: 90px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .svc-stroke,
|
|
||||||
.service-icon .svc-muted,
|
|
||||||
.service-icon .svc-accent {
|
|
||||||
fill: none;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .svc-stroke {
|
|
||||||
stroke: var(--svc-stroke);
|
|
||||||
stroke-width: 2.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .svc-muted {
|
|
||||||
stroke: var(--svc-muted);
|
|
||||||
stroke-width: 2.05;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .svc-accent {
|
|
||||||
stroke: var(--svc-accent);
|
|
||||||
stroke-width: 2.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .svc-soft {
|
|
||||||
fill: var(--svc-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .svc-soft-strong {
|
|
||||||
fill: var(--svc-soft-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .svc-dot {
|
|
||||||
fill: var(--svc-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .svc-dot-muted {
|
|
||||||
fill: var(--svc-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item span {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.services-grid-box {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body.intro-about .about-theme-toggle {
|
|
||||||
top: 12px;
|
|
||||||
right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title-svg {
|
|
||||||
width: min(92vw, 560px);
|
|
||||||
transform: translateY(-6px) scale(1.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-description {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-section {
|
|
||||||
padding: 28px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-section {
|
|
||||||
min-height: auto;
|
|
||||||
padding: 16px 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-waves {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-scroll-indicator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.services-grid-box {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item {
|
|
||||||
padding: 12px 8px;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item:hover {
|
|
||||||
transform: none;
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon .service-symbol {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item span {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,8 +42,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-links a {
|
.footer-links a {
|
||||||
|
display: block;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 14px; /* 增大链接字号 (原0.9rem/14.4px) */
|
font-size: 14px; /* 增大链接字号 (原0.9rem/14.4px) */
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-links a:hover {
|
.footer-links a:hover {
|
||||||
@@ -68,10 +70,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.footer-links-columns {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-content {
|
.footer-content {
|
||||||
grid-template-columns: 1fr; /* 手机 1列 */
|
grid-template-columns: 1fr; /* 手机 1列 */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,408 +0,0 @@
|
|||||||
/* =========================================
|
|
||||||
Introduction Page (About)
|
|
||||||
========================================= */
|
|
||||||
|
|
||||||
.intro-page {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: var(--bg-body);
|
|
||||||
background-image:
|
|
||||||
linear-gradient(160deg, var(--intro-glow-primary), transparent 55%),
|
|
||||||
linear-gradient(330deg, var(--intro-glow-accent), transparent 50%);
|
|
||||||
--intro-glow-primary: rgba(9, 105, 218, 0.14);
|
|
||||||
--intro-glow-accent: rgba(142, 136, 199, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .intro-page {
|
|
||||||
--intro-glow-primary: rgba(88, 166, 255, 0.22);
|
|
||||||
--intro-glow-accent: rgba(142, 136, 199, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-page::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-about .site-header {
|
|
||||||
background-color: rgba(14, 22, 36, 0.78);
|
|
||||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not([data-theme="dark"]) .intro-about .site-header {
|
|
||||||
background-color: rgba(246, 249, 252, 0.9);
|
|
||||||
border-bottom-color: rgba(36, 41, 47, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-page > * {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-step {
|
|
||||||
padding: 120px 0;
|
|
||||||
scroll-margin-top: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-hero {
|
|
||||||
padding-top: 140px;
|
|
||||||
padding-bottom: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-hero-grid,
|
|
||||||
.intro-grid,
|
|
||||||
.intro-join-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1.05fr 0.95fr;
|
|
||||||
gap: 64px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-grid.reverse .intro-media,
|
|
||||||
.intro-grid.reverse .intro-join-media {
|
|
||||||
order: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-copy {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-hero .intro-copy {
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-brand-mark {
|
|
||||||
position: relative;
|
|
||||||
width: min(420px, 100%);
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-brand-svg {
|
|
||||||
display: block;
|
|
||||||
width: min(76vw, 360px);
|
|
||||||
aspect-ratio: 3 / 2;
|
|
||||||
background-image: linear-gradient(135deg, var(--text-primary), var(--color-primary));
|
|
||||||
-webkit-mask: url("../../resources/title.svg") center / contain no-repeat;
|
|
||||||
mask: url("../../resources/title.svg") center / contain no-repeat;
|
|
||||||
filter:
|
|
||||||
drop-shadow(0 10px 24px rgba(12, 20, 32, 0.24))
|
|
||||||
drop-shadow(0 0 16px rgba(120, 190, 255, 0.24));
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-brand-text {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-title {
|
|
||||||
font-size: clamp(2.6rem, 4vw, 4rem);
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
line-height: 1.15;
|
|
||||||
margin-bottom: 0;
|
|
||||||
background: linear-gradient(120deg, var(--text-primary), var(--color-primary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-heading {
|
|
||||||
font-size: clamp(2rem, 3vw, 2.8rem);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-lead {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-sub,
|
|
||||||
.intro-text {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-cta-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border: 1px solid var(--color-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
background-color: transparent;
|
|
||||||
transition: all var(--duration-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-btn--primary {
|
|
||||||
background: linear-gradient(120deg, var(--color-primary), var(--color-primary-light));
|
|
||||||
color: #fff;
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-btn--primary:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-btn--ghost:hover {
|
|
||||||
color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-stats {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 24px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-stat {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border: 1px solid var(--border-default);
|
|
||||||
background-color: var(--bg-card);
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-number {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-media,
|
|
||||||
.intro-hero-media,
|
|
||||||
.intro-join-media {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-hero-media {
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-card {
|
|
||||||
min-height: 200px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px dashed var(--border-default);
|
|
||||||
background-color: var(--bg-card);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-card--tall {
|
|
||||||
grid-row: span 2;
|
|
||||||
min-height: 420px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-card--wide {
|
|
||||||
grid-column: span 2;
|
|
||||||
min-height: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-points {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-points li,
|
|
||||||
.intro-list li {
|
|
||||||
position: relative;
|
|
||||||
padding-left: 18px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-points li::before,
|
|
||||||
.intro-list li::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0.55em;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-org-grid,
|
|
||||||
.intro-feature-grid,
|
|
||||||
.intro-adv-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-org-grid,
|
|
||||||
.intro-feature-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-adv-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-card {
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px solid var(--border-default);
|
|
||||||
background-color: var(--bg-card);
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-card h3 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-card p {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-join {
|
|
||||||
padding-bottom: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-join-grid {
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-join-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-join-media {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stream text characters */
|
|
||||||
.stream-char {
|
|
||||||
transition: opacity 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
.intro-animate {
|
|
||||||
transition: opacity 0.8s var(--ease-in-out), transform 0.8s var(--ease-in-out);
|
|
||||||
transition-delay: var(--delay, 0s);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.has-intro-animations .intro-animate {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.has-intro-animations .intro-step.is-active .intro-animate {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.intro-animate {
|
|
||||||
transition: none !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.intro-hero-grid,
|
|
||||||
.intro-grid,
|
|
||||||
.intro-join-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-grid.reverse .intro-media,
|
|
||||||
.intro-grid.reverse .intro-join-media {
|
|
||||||
order: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-org-grid,
|
|
||||||
.intro-feature-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-adv-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-join-media {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.intro-step {
|
|
||||||
padding: 80px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-hero {
|
|
||||||
padding-top: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-brand-svg {
|
|
||||||
width: min(86vw, 320px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-title {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-cta-group {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-org-grid,
|
|
||||||
.intro-feature-grid,
|
|
||||||
.intro-join-media {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.join-page {
|
.join-page {
|
||||||
padding: 36px 0 72px;
|
padding: 36px 0 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,32 +659,30 @@
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
招新报名表单与 Formidable Forms 样式全面重构
|
||||||
|
========================================= */
|
||||||
|
|
||||||
.join-form-card {
|
.join-form-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid color-mix(in srgb, var(--color-primary) 28%, var(--border-default) 72%);
|
border: 1px solid color-mix(in srgb, var(--color-primary) 32%, var(--border-default) 68%);
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(
|
|
||||||
118deg,
|
|
||||||
color-mix(in srgb, var(--color-primary) 12%, transparent) 0%,
|
|
||||||
transparent 36%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
radial-gradient(
|
||||||
120% 80% at 100% 0%,
|
120% 90% at 100% 0%,
|
||||||
color-mix(in srgb, var(--color-primary) 14%, transparent) 0%,
|
color-mix(in srgb, var(--color-primary) 12%, transparent) 0%,
|
||||||
transparent 52%
|
transparent 55%
|
||||||
),
|
),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
color-mix(in srgb, var(--bg-card) 84%, #0f1a2b 16%) 0%,
|
color-mix(in srgb, var(--bg-card) 92%, #0d1117 8%) 0%,
|
||||||
color-mix(in srgb, var(--bg-card) 95%, transparent) 100%
|
color-mix(in srgb, var(--bg-card) 98%, transparent) 100%
|
||||||
);
|
);
|
||||||
padding: 18px;
|
padding: 24px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 22px 40px -32px color-mix(in srgb, #000000 78%, transparent),
|
0 16px 36px -20px color-mix(in srgb, #000000 70%, transparent),
|
||||||
inset 0 1px 0 color-mix(in srgb, #9bc9ff 18%, transparent),
|
inset 0 1px 0 color-mix(in srgb, #9bc9ff 16%, transparent);
|
||||||
inset 0 -1px 0 color-mix(in srgb, #0a101a 70%, transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-card::before {
|
.join-form-card::before {
|
||||||
@@ -695,32 +693,32 @@
|
|||||||
background:
|
background:
|
||||||
repeating-linear-gradient(
|
repeating-linear-gradient(
|
||||||
0deg,
|
0deg,
|
||||||
color-mix(in srgb, var(--color-primary) 8%, transparent) 0 1px,
|
color-mix(in srgb, var(--color-primary) 6%, transparent) 0 1px,
|
||||||
transparent 1px 13px
|
transparent 1px 16px
|
||||||
),
|
),
|
||||||
repeating-linear-gradient(
|
repeating-linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
color-mix(in srgb, var(--color-primary) 7%, transparent) 0 1px,
|
color-mix(in srgb, var(--color-primary) 5%, transparent) 0 1px,
|
||||||
transparent 1px 18px
|
transparent 1px 20px
|
||||||
);
|
);
|
||||||
opacity: 0.26;
|
opacity: 0.22;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-card::after {
|
.join-form-card::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 10px;
|
inset: 12px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, color-mix(in srgb, var(--color-primary) 72%, transparent), transparent)
|
linear-gradient(90deg, color-mix(in srgb, var(--color-primary) 70%, transparent), transparent)
|
||||||
top left / 88px 1px no-repeat,
|
top left / 80px 1px no-repeat,
|
||||||
linear-gradient(180deg, color-mix(in srgb, var(--color-primary) 72%, transparent), transparent)
|
linear-gradient(180deg, color-mix(in srgb, var(--color-primary) 70%, transparent), transparent)
|
||||||
top left / 1px 44px no-repeat,
|
top left / 1px 40px no-repeat,
|
||||||
linear-gradient(270deg, color-mix(in srgb, var(--color-primary) 48%, transparent), transparent)
|
linear-gradient(270deg, color-mix(in srgb, var(--color-primary) 50%, transparent), transparent)
|
||||||
bottom right / 66px 1px no-repeat,
|
bottom right / 60px 1px no-repeat,
|
||||||
linear-gradient(0deg, color-mix(in srgb, var(--color-primary) 48%, transparent), transparent)
|
linear-gradient(0deg, color-mix(in srgb, var(--color-primary) 50%, transparent), transparent)
|
||||||
bottom right / 1px 30px no-repeat;
|
bottom right / 1px 30px no-repeat;
|
||||||
opacity: 0.65;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-card > * {
|
.join-form-card > * {
|
||||||
@@ -729,7 +727,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.join-form-card + .join-panel-title {
|
.join-form-card + .join-panel-title {
|
||||||
margin-top: 16px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-card-placeholder {
|
.join-form-card-placeholder {
|
||||||
@@ -739,13 +737,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content {
|
.join-form-content {
|
||||||
margin-top: 14px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content form {
|
.join-form-content form {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- 录取进度查询表单 --- */
|
||||||
.join-progress-query-form {
|
.join-progress-query-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -775,7 +774,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: color-mix(in srgb, var(--bg-card) 90%, transparent) !important;
|
background: color-mix(in srgb, var(--bg-card) 90%, transparent) !important;
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
padding: 9px 10px;
|
padding: 10px 12px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
transition: border-color var(--duration-fast), box-shadow var(--duration-fast), background-color var(--duration-fast);
|
transition: border-color var(--duration-fast), box-shadow var(--duration-fast), background-color var(--duration-fast);
|
||||||
}
|
}
|
||||||
@@ -791,10 +790,10 @@
|
|||||||
|
|
||||||
.join-progress-query-field input:focus {
|
.join-progress-query-field input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: color-mix(in srgb, var(--color-primary) 55%, var(--border-default) 45%) !important;
|
border-color: var(--color-primary) !important;
|
||||||
background: color-mix(in srgb, var(--bg-card) 90%, transparent) !important;
|
background: color-mix(in srgb, var(--bg-card) 95%, transparent) !important;
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 16%, transparent) !important;
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-progress-query-field input:-webkit-autofill,
|
.join-progress-query-field input:-webkit-autofill,
|
||||||
@@ -827,23 +826,25 @@
|
|||||||
|
|
||||||
.join-progress-query-submit {
|
.join-progress-query-submit {
|
||||||
border: 1px solid color-mix(in srgb, var(--color-primary) 48%, var(--border-default) 52%);
|
border: 1px solid color-mix(in srgb, var(--color-primary) 48%, var(--border-default) 52%);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
background: color-mix(in srgb, var(--color-primary) 18%, var(--bg-card) 82%);
|
background: color-mix(in srgb, var(--color-primary) 18%, var(--bg-card) 82%);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 9px 16px;
|
padding: 10px 18px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-progress-query-submit:hover {
|
.join-progress-query-submit:hover {
|
||||||
background: color-mix(in srgb, var(--color-primary) 24%, var(--bg-card) 76%);
|
background: color-mix(in srgb, var(--color-primary) 28%, var(--bg-card) 72%);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-progress-query-feedback {
|
.join-progress-query-feedback {
|
||||||
margin: 2px 0 0;
|
margin: 8px 0 0;
|
||||||
padding: 10px 12px;
|
padding: 10px 14px;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
border: 1px solid color-mix(in srgb, var(--border-default) 80%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border-default) 80%, transparent);
|
||||||
background: color-mix(in srgb, var(--bg-card) 90%, transparent);
|
background: color-mix(in srgb, var(--bg-card) 90%, transparent);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -871,296 +872,390 @@
|
|||||||
background: color-mix(in srgb, #c34646 14%, var(--bg-card) 86%);
|
background: color-mix(in srgb, #c34646 14%, var(--bg-card) 86%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_forms {
|
/* --- Formidable 表单字段容器与排版 --- */
|
||||||
|
.join-form-content .frm_forms,
|
||||||
|
.join-form-content .frm_form_fields {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_form_fields,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_submit {
|
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style,
|
.join-form-content .frm_fields_container {
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_form_fields,
|
display: flex;
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_form_field,
|
flex-direction: column;
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_field_wrapper {
|
gap: 16px;
|
||||||
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 {
|
.join-form-content .frm_form_field,
|
||||||
margin-bottom: 14px !important;
|
.join-form-content .form-field {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style label {
|
.join-form-content .frm_primary_label,
|
||||||
color: color-mix(in srgb, var(--text-primary) 95%, #ffffff 5%) !important;
|
.join-form-content label.frm_primary_label,
|
||||||
opacity: 1 !important;
|
.join-form-content .frm_section_heading h3,
|
||||||
}
|
.join-form-content .frm_inline_box label {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_primary_label,
|
font-size: 0.92rem !important;
|
||||||
.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;
|
font-weight: 600 !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
letter-spacing: 0.01em !important;
|
||||||
|
margin-bottom: 7px !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 3px !important;
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
letter-spacing: 0.01em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_required {
|
.join-form-content .frm_required {
|
||||||
color: #ff6767 !important;
|
color: #f85149 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 1em !important;
|
||||||
|
margin-left: 2px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_description,
|
/* --- 顶部全局错误/提示横幅 (Modern Alert Banner) --- */
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_error,
|
.join-form-content .frm_error_style,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_error_style {
|
.join-form-content .frm_message.frm_error_style,
|
||||||
color: color-mix(in srgb, var(--text-primary) 82%, var(--text-secondary) 18%) !important;
|
.join-form-content div.frm_error_style,
|
||||||
}
|
.join-form-content .frm_error {
|
||||||
|
position: relative !important;
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_error_style,
|
display: flex !important;
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_error {
|
align-items: center !important;
|
||||||
border-left: 3px solid #ff6767 !important;
|
gap: 10px !important;
|
||||||
background: color-mix(in srgb, #ff6767 10%, transparent) !important;
|
padding: 12px 16px 12px 42px !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"],
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="tel"],
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="date"],
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="datetime-local"],
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style textarea,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style select {
|
|
||||||
background: color-mix(in srgb, var(--bg-body) 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;
|
border-radius: 8px !important;
|
||||||
padding: 10px 12px !important;
|
border: 1px solid rgba(248, 81, 73, 0.4) !important;
|
||||||
|
border-left: 4px solid #f85149 !important;
|
||||||
|
background: linear-gradient(135deg, rgba(248, 81, 73, 0.16) 0%, rgba(248, 81, 73, 0.06) 100%) !important;
|
||||||
|
color: #ff7b72 !important;
|
||||||
|
font-size: 0.9rem !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
line-height: 1.45 !important;
|
||||||
|
margin: 8px 0 18px 0 !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(248, 81, 73, 0.1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input::placeholder,
|
.join-form-content .frm_error_style::before,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style textarea::placeholder {
|
.join-form-content .frm_message.frm_error_style::before,
|
||||||
color: color-mix(in srgb, var(--text-secondary) 78%, var(--text-primary) 22%) !important;
|
.join-form-content .frm_error::before {
|
||||||
opacity: 1;
|
content: "" !important;
|
||||||
|
position: absolute !important;
|
||||||
|
left: 14px !important;
|
||||||
|
top: 50% !important;
|
||||||
|
transform: translateY(-50%) !important;
|
||||||
|
width: 18px !important;
|
||||||
|
height: 18px !important;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f85149' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E") !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-size: contain !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="text"]:focus,
|
/* --- 成功提示 (Success Message) --- */
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="email"]:focus,
|
.join-form-content .frm_message,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="number"]:focus,
|
.join-form-content .frm_success_style,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="tel"]:focus,
|
.join-form-content .frm_success {
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="date"]:focus,
|
position: relative !important;
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="datetime-local"]:focus,
|
display: flex !important;
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style textarea:focus,
|
align-items: center !important;
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style select:focus {
|
gap: 10px !important;
|
||||||
border-color: color-mix(in srgb, var(--color-primary) 76%, #8ec9ff 24%) !important;
|
padding: 12px 16px 12px 42px !important;
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 24%, transparent) !important;
|
border-radius: 8px !important;
|
||||||
|
border: 1px solid rgba(63, 185, 80, 0.4) !important;
|
||||||
|
border-left: 4px solid #3fb950 !important;
|
||||||
|
background: linear-gradient(135deg, rgba(63, 185, 80, 0.16) 0%, rgba(63, 185, 80, 0.06) 100%) !important;
|
||||||
|
color: #7ee787 !important;
|
||||||
|
font-size: 0.9rem !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
line-height: 1.45 !important;
|
||||||
|
margin-bottom: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form-content .frm_message::before,
|
||||||
|
.join-form-content .frm_success_style::before,
|
||||||
|
.join-form-content .frm_success::before {
|
||||||
|
content: "" !important;
|
||||||
|
position: absolute !important;
|
||||||
|
left: 14px !important;
|
||||||
|
top: 50% !important;
|
||||||
|
transform: translateY(-50%) !important;
|
||||||
|
width: 18px !important;
|
||||||
|
height: 18px !important;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233fb950' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 11.08V12a10 10 0 1 1-5.93-9.14'%3E%3C/path%3E%3Cpolyline points='22 4 12 14.01 9 11.01'%3E%3C/polyline%3E%3C/svg%3E") !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-size: contain !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form-content .frm_description {
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
font-size: 0.82rem !important;
|
||||||
|
line-height: 1.45 !important;
|
||||||
|
margin-top: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 输入框统一质感 (Text, Email, Tel, Number, Select, Textarea) --- */
|
||||||
|
.join-form-content input[type="text"],
|
||||||
|
.join-form-content input[type="email"],
|
||||||
|
.join-form-content input[type="number"],
|
||||||
|
.join-form-content input[type="tel"],
|
||||||
|
.join-form-content input[type="password"],
|
||||||
|
.join-form-content input[type="date"],
|
||||||
|
.join-form-content input[type="datetime-local"],
|
||||||
|
.join-form-content textarea,
|
||||||
|
.join-form-content select {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
min-height: 44px !important;
|
||||||
|
padding: 10px 14px !important;
|
||||||
|
font-size: 0.94rem !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
color: #f0f6fc !important;
|
||||||
|
background: rgba(13, 17, 23, 0.72) !important;
|
||||||
|
border: 1px solid rgba(240, 246, 252, 0.14) !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25) !important;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form-content input[type="text"]:hover,
|
||||||
|
.join-form-content input[type="email"]:hover,
|
||||||
|
.join-form-content input[type="number"]:hover,
|
||||||
|
.join-form-content input[type="tel"]:hover,
|
||||||
|
.join-form-content input[type="password"]:hover,
|
||||||
|
.join-form-content input[type="date"]:hover,
|
||||||
|
.join-form-content input[type="datetime-local"]:hover,
|
||||||
|
.join-form-content textarea:hover,
|
||||||
|
.join-form-content select:hover {
|
||||||
|
border-color: rgba(88, 166, 255, 0.45) !important;
|
||||||
|
background: rgba(22, 27, 34, 0.85) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form-content input[type="text"]:focus,
|
||||||
|
.join-form-content input[type="email"]:focus,
|
||||||
|
.join-form-content input[type="number"]:focus,
|
||||||
|
.join-form-content input[type="tel"]:focus,
|
||||||
|
.join-form-content input[type="password"]:focus,
|
||||||
|
.join-form-content input[type="date"]:focus,
|
||||||
|
.join-form-content input[type="datetime-local"]:focus,
|
||||||
|
.join-form-content textarea:focus,
|
||||||
|
.join-form-content select:focus {
|
||||||
|
border-color: #58a6ff !important;
|
||||||
|
background: rgba(22, 27, 34, 0.95) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(56, 139, 253, 0.28), inset 0 1px 2px rgba(0, 0, 0, 0.2) !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti {
|
.join-form-content input::placeholder,
|
||||||
|
.join-form-content textarea::placeholder {
|
||||||
|
color: #6d737c !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form-content textarea {
|
||||||
|
min-height: 96px !important;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 自动填充处理 --- */
|
||||||
|
.join-form-content input:-webkit-autofill,
|
||||||
|
.join-form-content input:-webkit-autofill:hover,
|
||||||
|
.join-form-content input:-webkit-autofill:focus,
|
||||||
|
.join-form-content textarea:-webkit-autofill,
|
||||||
|
.join-form-content textarea:-webkit-autofill:hover,
|
||||||
|
.join-form-content textarea:-webkit-autofill:focus,
|
||||||
|
.join-form-content select:-webkit-autofill,
|
||||||
|
.join-form-content select:-webkit-autofill:hover,
|
||||||
|
.join-form-content select:-webkit-autofill:focus {
|
||||||
|
-webkit-text-fill-color: #f0f6fc !important;
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px #161b22 inset !important;
|
||||||
|
box-shadow: 0 0 0 1000px #161b22 inset !important;
|
||||||
|
caret-color: #f0f6fc !important;
|
||||||
|
transition: background-color 9999s ease-out 0s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 国际电话组件 (iti) --- */
|
||||||
|
.join-form-content .iti {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti input[type="tel"] {
|
.join-form-content .iti input[type="tel"] {
|
||||||
padding-right: 12px !important;
|
padding-right: 12px !important;
|
||||||
padding-left: calc(var(--iti-selected-country-arrow-padding, 64px) + 4px) !important;
|
padding-left: calc(var(--iti-selected-country-arrow-padding, 64px) + 4px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__selected-country {
|
.join-form-content .iti__selected-country {
|
||||||
border-top-left-radius: 8px;
|
border-top-left-radius: 8px;
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input:-webkit-autofill,
|
.join-form-content .iti__dropdown-content,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input:-webkit-autofill:hover,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input:-webkit-autofill:focus,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style textarea:-webkit-autofill,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style textarea:-webkit-autofill:hover,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style textarea:-webkit-autofill:focus,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style select:-webkit-autofill,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style select:-webkit-autofill:hover,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style select:-webkit-autofill:focus {
|
|
||||||
-webkit-text-fill-color: color-mix(in srgb, var(--text-primary) 96%, #ffffff 4%) !important;
|
|
||||||
-webkit-box-shadow: 0 0 0 1000px color-mix(in srgb, var(--bg-body) 84%, #ffffff 16%) inset !important;
|
|
||||||
box-shadow: 0 0 0 1000px color-mix(in srgb, var(--bg-body) 84%, #ffffff 16%) inset !important;
|
|
||||||
caret-color: color-mix(in srgb, var(--text-primary) 96%, #ffffff 4%) !important;
|
|
||||||
transition: background-color 9999s ease-out 0s !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__dropdown-content {
|
|
||||||
background: color-mix(in srgb, var(--bg-card) 94%, var(--bg-body) 6%) !important;
|
|
||||||
border: 1px solid color-mix(in srgb, var(--border-default) 46%, var(--color-primary) 54%) !important;
|
|
||||||
color: color-mix(in srgb, var(--text-primary) 92%, #ffffff 8%) !important;
|
|
||||||
box-shadow: 0 18px 36px -22px color-mix(in srgb, #000000 68%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__search-input-wrapper {
|
|
||||||
margin: 6px 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__search-input {
|
|
||||||
border: 1px solid color-mix(in srgb, var(--border-default) 38%, var(--color-primary) 62%) !important;
|
|
||||||
background: color-mix(in srgb, var(--bg-body) 84%, #ffffff 16%) !important;
|
|
||||||
color: color-mix(in srgb, var(--text-primary) 94%, #ffffff 6%) !important;
|
|
||||||
min-height: 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__search-input::placeholder {
|
|
||||||
color: color-mix(in srgb, var(--text-secondary) 76%, var(--text-primary) 24%) !important;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__country-list {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__country {
|
|
||||||
color: color-mix(in srgb, var(--text-primary) 90%, #ffffff 10%) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__dial-code,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__country-name {
|
|
||||||
color: color-mix(in srgb, var(--text-primary) 88%, var(--text-secondary) 12%) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__country.iti__highlight,
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__country:hover {
|
|
||||||
background: color-mix(in srgb, var(--color-primary) 16%, var(--bg-card) 84%) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .iti__divider {
|
|
||||||
border-bottom-color: color-mix(in srgb, var(--border-default) 70%, transparent) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.frm_forms.with_frm_style .iti__dropdown-content {
|
.frm_forms.with_frm_style .iti__dropdown-content {
|
||||||
background: color-mix(in srgb, var(--bg-card) 94%, var(--bg-body) 6%) !important;
|
background: #161b22 !important;
|
||||||
border: 1px solid color-mix(in srgb, var(--border-default) 46%, var(--color-primary) 54%) !important;
|
border: 1px solid rgba(56, 139, 253, 0.4) !important;
|
||||||
color: color-mix(in srgb, var(--text-primary) 92%, #ffffff 8%) !important;
|
color: #f0f6fc !important;
|
||||||
box-shadow: 0 18px 36px -22px color-mix(in srgb, #000000 68%, transparent);
|
box-shadow: 0 18px 36px -16px rgba(0, 0, 0, 0.75);
|
||||||
}
|
border-radius: 8px;
|
||||||
|
|
||||||
.frm_forms.with_frm_style .iti__search-input-wrapper {
|
|
||||||
margin: 6px 8px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.join-form-content .iti__search-input,
|
||||||
.frm_forms.with_frm_style .iti__search-input {
|
.frm_forms.with_frm_style .iti__search-input {
|
||||||
border: 1px solid color-mix(in srgb, var(--border-default) 38%, var(--color-primary) 62%) !important;
|
border: 1px solid rgba(240, 246, 252, 0.2) !important;
|
||||||
background: color-mix(in srgb, var(--bg-body) 84%, #ffffff 16%) !important;
|
background: #0d1117 !important;
|
||||||
color: color-mix(in srgb, var(--text-primary) 94%, #ffffff 6%) !important;
|
color: #f0f6fc !important;
|
||||||
min-height: 40px !important;
|
min-height: 38px !important;
|
||||||
}
|
border-radius: 6px;
|
||||||
|
|
||||||
.frm_forms.with_frm_style .iti__search-input::placeholder {
|
|
||||||
color: color-mix(in srgb, var(--text-secondary) 76%, var(--text-primary) 24%) !important;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.frm_forms.with_frm_style .iti__country-list {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.join-form-content .iti__country,
|
||||||
.frm_forms.with_frm_style .iti__country {
|
.frm_forms.with_frm_style .iti__country {
|
||||||
color: color-mix(in srgb, var(--text-primary) 90%, #ffffff 10%) !important;
|
color: #c9d1d9 !important;
|
||||||
}
|
|
||||||
|
|
||||||
.frm_forms.with_frm_style .iti__dial-code,
|
|
||||||
.frm_forms.with_frm_style .iti__country-name {
|
|
||||||
color: color-mix(in srgb, var(--text-primary) 88%, var(--text-secondary) 12%) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.join-form-content .iti__country.iti__highlight,
|
||||||
|
.join-form-content .iti__country:hover,
|
||||||
.frm_forms.with_frm_style .iti__country.iti__highlight,
|
.frm_forms.with_frm_style .iti__country.iti__highlight,
|
||||||
.frm_forms.with_frm_style .iti__country:hover {
|
.frm_forms.with_frm_style .iti__country:hover {
|
||||||
background: color-mix(in srgb, var(--color-primary) 16%, var(--bg-card) 84%) !important;
|
background: rgba(56, 139, 253, 0.2) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.join-form-content .iti__divider,
|
||||||
.frm_forms.with_frm_style .iti__divider {
|
.frm_forms.with_frm_style .iti__divider {
|
||||||
border-bottom-color: color-mix(in srgb, var(--border-default) 70%, transparent) !important;
|
border-bottom-color: rgba(240, 246, 252, 0.1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="checkbox"],
|
/* --- 选项卡片化重构 (Checkbox & Radio Chips) --- */
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="radio"] {
|
.join-form-content .frm_opt_container {
|
||||||
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;
|
display: flex !important;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap !important;
|
||||||
gap: 10px;
|
gap: 8px 12px !important;
|
||||||
align-items: center;
|
margin-top: 6px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="submit"],
|
.join-form-content .frm_checkbox,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style button.frm_button_submit {
|
.join-form-content .frm_radio {
|
||||||
border-radius: 10px;
|
margin: 0 !important;
|
||||||
border: 1px solid color-mix(in srgb, var(--color-primary) 82%, #9cd3ff 18%) !important;
|
display: inline-flex !important;
|
||||||
background: linear-gradient(
|
align-items: center !important;
|
||||||
180deg,
|
}
|
||||||
color-mix(in srgb, var(--color-primary) 82%, #78c3ff 18%) 0%,
|
|
||||||
color-mix(in srgb, var(--color-primary) 92%, #5ea7f8 8%) 100%
|
.join-form-content .frm_checkbox label,
|
||||||
) !important;
|
.join-form-content .frm_radio label {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
padding: 8px 14px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
border: 1px solid rgba(240, 246, 252, 0.12) !important;
|
||||||
|
background: rgba(22, 27, 34, 0.6) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
font-size: 0.92rem !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
|
user-select: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form-content .frm_checkbox label:hover,
|
||||||
|
.join-form-content .frm_radio label:hover {
|
||||||
|
border-color: rgba(88, 166, 255, 0.45) !important;
|
||||||
|
background: rgba(56, 139, 253, 0.1) !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form-content .frm_checkbox input[type="checkbox"],
|
||||||
|
.join-form-content .frm_radio input[type="radio"] {
|
||||||
|
width: 16px !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
height: 16px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
accent-color: var(--color-primary) !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 提交与操作按钮 (Submit & Actions) --- */
|
||||||
|
.join-form-content .frm_submit {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
gap: 12px !important;
|
||||||
|
align-items: center !important;
|
||||||
|
margin-top: 24px !important;
|
||||||
|
padding-top: 14px !important;
|
||||||
|
border-top: 1px solid rgba(240, 246, 252, 0.08) !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-form-content input[type="submit"],
|
||||||
|
.join-form-content button.frm_button_submit {
|
||||||
|
min-height: 46px !important;
|
||||||
|
padding: 0 28px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16) !important;
|
||||||
|
background: linear-gradient(135deg, #1f6feb 0%, #1158c7 100%) !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
font-weight: 700 !important;
|
font-size: 0.95rem !important;
|
||||||
letter-spacing: 0.01em;
|
font-weight: 600 !important;
|
||||||
min-height: 44px;
|
letter-spacing: 0.02em !important;
|
||||||
padding: 0 20px !important;
|
cursor: pointer !important;
|
||||||
box-shadow: 0 8px 18px -12px color-mix(in srgb, var(--color-primary) 65%, transparent);
|
box-shadow: 0 3px 12px rgba(31, 111, 235, 0.35) !important;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
|
user-select: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[type="submit"]:hover,
|
.join-form-content input[type="submit"]:hover,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style button.frm_button_submit:hover {
|
.join-form-content button.frm_button_submit:hover {
|
||||||
filter: brightness(1.05);
|
transform: translateY(-1px) !important;
|
||||||
|
filter: brightness(1.08) !important;
|
||||||
|
box-shadow: 0 6px 18px rgba(31, 111, 235, 0.45) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_save_draft,
|
.join-form-content input[type="submit"]:active,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style button[name*="draft"],
|
.join-form-content button.frm_button_submit:active {
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[name*="draft"],
|
transform: translateY(0) !important;
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style button[value*="草稿"],
|
filter: brightness(0.96) !important;
|
||||||
.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"] {
|
.join-form-content .frm_save_draft,
|
||||||
border: 1px solid color-mix(in srgb, var(--border-default) 30%, var(--color-primary) 70%) !important;
|
.join-form-content button[name*="draft"],
|
||||||
background: color-mix(in srgb, var(--bg-card) 88%, transparent) !important;
|
.join-form-content input[name*="draft"],
|
||||||
color: var(--color-primary) !important;
|
.join-form-content button[value*="草稿"],
|
||||||
|
.join-form-content input[value*="草稿"],
|
||||||
|
.join-form-content button[value*="Draft"],
|
||||||
|
.join-form-content input[value*="Draft"] {
|
||||||
|
min-height: 46px !important;
|
||||||
|
padding: 0 22px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
border: 1px solid rgba(240, 246, 252, 0.16) !important;
|
||||||
|
background: rgba(22, 27, 34, 0.6) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
font-size: 0.92rem !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
cursor: pointer !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style .frm_save_draft:hover,
|
.join-form-content .frm_save_draft:hover,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style button[name*="draft"]:hover,
|
.join-form-content button[name*="draft"]:hover,
|
||||||
.join-form-content .frm_style_formidable-style.with_frm_style input[name*="draft"]:hover {
|
.join-form-content input[name*="draft"]:hover {
|
||||||
background: color-mix(in srgb, var(--bg-card) 74%, var(--color-primary) 26%) !important;
|
border-color: var(--color-primary) !important;
|
||||||
color: #ffffff !important;
|
background: rgba(56, 139, 253, 0.1) !important;
|
||||||
|
color: var(--color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
浅色模式适配 (Light Theme)
|
||||||
|
========================================= */
|
||||||
|
|
||||||
[data-theme="light"] .join-form-card {
|
[data-theme="light"] .join-form-card {
|
||||||
border-color: color-mix(in srgb, #8bbcff 52%, var(--border-default) 48%);
|
border-color: color-mix(in srgb, #8bbcff 52%, var(--border-default) 48%);
|
||||||
background:
|
background:
|
||||||
linear-gradient(
|
|
||||||
118deg,
|
|
||||||
color-mix(in srgb, #d9eaff 42%, transparent) 0%,
|
|
||||||
transparent 40%
|
|
||||||
),
|
|
||||||
radial-gradient(
|
radial-gradient(
|
||||||
140% 90% at 100% 0%,
|
140% 90% at 100% 0%,
|
||||||
color-mix(in srgb, #dbe9ff 48%, transparent) 0%,
|
color-mix(in srgb, #dbe9ff 48%, transparent) 0%,
|
||||||
@@ -1176,16 +1271,134 @@
|
|||||||
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style input[type="text"],
|
[data-theme="light"] .join-form-content 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 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 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 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 input[type="password"],
|
||||||
[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style input[type="datetime-local"],
|
[data-theme="light"] .join-form-content input[type="date"],
|
||||||
[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style textarea,
|
[data-theme="light"] .join-form-content input[type="datetime-local"],
|
||||||
[data-theme="light"] .join-form-content .frm_style_formidable-style.with_frm_style select {
|
[data-theme="light"] .join-form-content textarea,
|
||||||
|
[data-theme="light"] .join-form-content select {
|
||||||
background: #ffffff !important;
|
background: #ffffff !important;
|
||||||
border-color: color-mix(in srgb, #8bbcff 46%, var(--border-default) 54%) !important;
|
border: 1px solid #d0d7de !important;
|
||||||
|
color: #1f2328 !important;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(31, 35, 40, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content input[type="text"]:hover,
|
||||||
|
[data-theme="light"] .join-form-content input[type="email"]:hover,
|
||||||
|
[data-theme="light"] .join-form-content input[type="number"]:hover,
|
||||||
|
[data-theme="light"] .join-form-content input[type="tel"]:hover,
|
||||||
|
[data-theme="light"] .join-form-content input[type="password"]:hover,
|
||||||
|
[data-theme="light"] .join-form-content input[type="date"]:hover,
|
||||||
|
[data-theme="light"] .join-form-content input[type="datetime-local"]:hover,
|
||||||
|
[data-theme="light"] .join-form-content textarea:hover,
|
||||||
|
[data-theme="light"] .join-form-content select:hover {
|
||||||
|
border-color: #0969da !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content input[type="text"]:focus,
|
||||||
|
[data-theme="light"] .join-form-content input[type="email"]:focus,
|
||||||
|
[data-theme="light"] .join-form-content input[type="number"]:focus,
|
||||||
|
[data-theme="light"] .join-form-content input[type="tel"]:focus,
|
||||||
|
[data-theme="light"] .join-form-content input[type="password"]:focus,
|
||||||
|
[data-theme="light"] .join-form-content input[type="date"]:focus,
|
||||||
|
[data-theme="light"] .join-form-content input[type="datetime-local"]:focus,
|
||||||
|
[data-theme="light"] .join-form-content textarea:focus,
|
||||||
|
[data-theme="light"] .join-form-content select:focus {
|
||||||
|
border-color: #0969da !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
color: #1f2328 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.2), inset 0 1px 2px rgba(31, 35, 40, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content input::placeholder,
|
||||||
|
[data-theme="light"] .join-form-content textarea::placeholder {
|
||||||
|
color: #8c959f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content input:-webkit-autofill,
|
||||||
|
[data-theme="light"] .join-form-content input:-webkit-autofill:hover,
|
||||||
|
[data-theme="light"] .join-form-content input:-webkit-autofill:focus,
|
||||||
|
[data-theme="light"] .join-form-content textarea:-webkit-autofill,
|
||||||
|
[data-theme="light"] .join-form-content textarea:-webkit-autofill:hover,
|
||||||
|
[data-theme="light"] .join-form-content textarea:-webkit-autofill:focus,
|
||||||
|
[data-theme="light"] .join-form-content select:-webkit-autofill,
|
||||||
|
[data-theme="light"] .join-form-content select:-webkit-autofill:hover,
|
||||||
|
[data-theme="light"] .join-form-content select:-webkit-autofill:focus {
|
||||||
|
-webkit-text-fill-color: #1f2328 !important;
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px #ffffff inset !important;
|
||||||
|
box-shadow: 0 0 0 1000px #ffffff inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content .frm_error_style,
|
||||||
|
[data-theme="light"] .join-form-content .frm_message.frm_error_style,
|
||||||
|
[data-theme="light"] .join-form-content .frm_error {
|
||||||
|
background: #ffebe9 !important;
|
||||||
|
border: 1px solid rgba(207, 34, 46, 0.3) !important;
|
||||||
|
border-left: 4px solid #cf222e !important;
|
||||||
|
color: #a40e26 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content .frm_error_style::before,
|
||||||
|
[data-theme="light"] .join-form-content .frm_message.frm_error_style::before,
|
||||||
|
[data-theme="light"] .join-form-content .frm_error::before {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23cf222e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E") !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content .frm_message,
|
||||||
|
[data-theme="light"] .join-form-content .frm_success_style,
|
||||||
|
[data-theme="light"] .join-form-content .frm_success {
|
||||||
|
background: #dafbe1 !important;
|
||||||
|
border: 1px solid rgba(31, 136, 61, 0.3) !important;
|
||||||
|
border-left: 4px solid #1f883d !important;
|
||||||
|
color: #1a7f37 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content .frm_message::before,
|
||||||
|
[data-theme="light"] .join-form-content .frm_success_style::before,
|
||||||
|
[data-theme="light"] .join-form-content .frm_success::before {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231a7f37' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 11.08V12a10 10 0 1 1-5.93-9.14'%3E%3C/path%3E%3Cpolyline points='22 4 12 14.01 9 11.01'%3E%3C/polyline%3E%3C/svg%3E") !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content .frm_checkbox label,
|
||||||
|
[data-theme="light"] .join-form-content .frm_radio label {
|
||||||
|
border: 1px solid #d0d7de !important;
|
||||||
|
background: #f6f8fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content .frm_checkbox label:hover,
|
||||||
|
[data-theme="light"] .join-form-content .frm_radio label:hover {
|
||||||
|
border-color: #0969da !important;
|
||||||
|
background: rgba(9, 105, 218, 0.06) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content input[type="submit"],
|
||||||
|
[data-theme="light"] .join-form-content button.frm_button_submit {
|
||||||
|
background: linear-gradient(135deg, #0969da 0%, #0550ae 100%) !important;
|
||||||
|
box-shadow: 0 3px 12px rgba(9, 105, 218, 0.28) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content input[type="submit"]:hover,
|
||||||
|
[data-theme="light"] .join-form-content button.frm_button_submit:hover {
|
||||||
|
box-shadow: 0 6px 18px rgba(9, 105, 218, 0.38) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content .frm_save_draft,
|
||||||
|
[data-theme="light"] .join-form-content button[name*="draft"],
|
||||||
|
[data-theme="light"] .join-form-content input[name*="draft"] {
|
||||||
|
border: 1px solid #d0d7de !important;
|
||||||
|
background: #f6f8fa !important;
|
||||||
|
color: #1f2328 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .join-form-content .frm_save_draft:hover,
|
||||||
|
[data-theme="light"] .join-form-content button[name*="draft"]:hover,
|
||||||
|
[data-theme="light"] .join-form-content input[name*="draft"]:hover {
|
||||||
|
border-color: #0969da !important;
|
||||||
|
background: rgba(9, 105, 218, 0.06) !important;
|
||||||
|
color: #0969da !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-form-tip {
|
.join-form-tip {
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
(() => {
|
|
||||||
const main = document.querySelector('.site-main');
|
|
||||||
const heroSection = document.querySelector('.hero-section');
|
|
||||||
const servicesSection = document.querySelector('.services-section');
|
|
||||||
const canvas = document.querySelector('.hero-waves');
|
|
||||||
if (!main || !canvas) return;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
||||||
let width = 0;
|
|
||||||
let height = 0;
|
|
||||||
let dpr = 1;
|
|
||||||
let rafId = null;
|
|
||||||
|
|
||||||
const waveDefs = [
|
|
||||||
{ yRatio: 0.3, amplitude: 24, wavelength: 940, speed: 0.5, alpha: 0.98 },
|
|
||||||
{ yRatio: 0.45, amplitude: 14, wavelength: 1020, speed: 0.58, alpha: 0.92 },
|
|
||||||
{ yRatio: 0.65, amplitude: 10, wavelength: 1160, speed: 0.6, alpha: 0.9 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const palettes = {
|
|
||||||
light: {
|
|
||||||
gradient: ['#d6e8fa', '#a7c8e8', '#6f9fc8', '#4f84b6', '#376e9f'],
|
|
||||||
waves: ['rgba(124, 167, 205, 0.96)', 'rgba(90, 143, 190, 0.97)', 'rgba(62, 118, 169, 0.98)'],
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
gradient: ['#0d1117', '#111d2b', '#14334d', '#17507a', '#1b6aa6'],
|
|
||||||
waves: ['rgba(90, 165, 225, 0.9)', 'rgba(65, 140, 210, 0.88)', 'rgba(50, 120, 190, 0.86)'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTheme = () => (document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light');
|
|
||||||
|
|
||||||
const getCoverageHeight = () => {
|
|
||||||
if (servicesSection) {
|
|
||||||
const coverage = servicesSection.offsetTop + servicesSection.offsetHeight * 0.5;
|
|
||||||
return Math.min(coverage, main.scrollHeight);
|
|
||||||
}
|
|
||||||
if (heroSection) {
|
|
||||||
return heroSection.offsetHeight;
|
|
||||||
}
|
|
||||||
return main.scrollHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resize = () => {
|
|
||||||
dpr = window.devicePixelRatio || 1;
|
|
||||||
width = main.clientWidth;
|
|
||||||
height = Math.max(1, Math.round(getCoverageHeight()));
|
|
||||||
canvas.width = Math.floor(width * dpr);
|
|
||||||
canvas.height = Math.floor(height * dpr);
|
|
||||||
canvas.style.width = `${width}px`;
|
|
||||||
canvas.style.height = `${height}px`;
|
|
||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawBackground = (palette) => {
|
|
||||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
||||||
gradient.addColorStop(0, palette.gradient[0]);
|
|
||||||
gradient.addColorStop(0.35, palette.gradient[1]);
|
|
||||||
gradient.addColorStop(0.55, palette.gradient[2]);
|
|
||||||
gradient.addColorStop(0.75, palette.gradient[3]);
|
|
||||||
gradient.addColorStop(1, palette.gradient[4]);
|
|
||||||
ctx.fillStyle = gradient;
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawWave = (palette, wave, time, index) => {
|
|
||||||
const yBase = height * wave.yRatio;
|
|
||||||
const phase = time * wave.speed;
|
|
||||||
const prevAlpha = ctx.globalAlpha;
|
|
||||||
ctx.globalAlpha = wave.alpha ?? 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, height);
|
|
||||||
for (let x = 0; x <= width; x += 18) {
|
|
||||||
const theta = (x / wave.wavelength) * Math.PI * 2 + phase;
|
|
||||||
const y = yBase + Math.sin(theta) * wave.amplitude;
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
ctx.lineTo(width, height);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fillStyle = palette.waves[index % palette.waves.length];
|
|
||||||
ctx.fill();
|
|
||||||
ctx.globalAlpha = prevAlpha;
|
|
||||||
};
|
|
||||||
|
|
||||||
const render = (timestamp) => {
|
|
||||||
const palette = palettes[getTheme()];
|
|
||||||
const time = timestamp * 0.001;
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
drawBackground(palette);
|
|
||||||
waveDefs.forEach((wave, index) => drawWave(palette, wave, time, index));
|
|
||||||
rafId = requestAnimationFrame(render);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stop = () => {
|
|
||||||
if (rafId) {
|
|
||||||
cancelAnimationFrame(rafId);
|
|
||||||
rafId = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawStatic = () => {
|
|
||||||
const palette = palettes[getTheme()];
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
drawBackground(palette);
|
|
||||||
waveDefs.forEach((wave, index) => drawWave(palette, wave, 0, index));
|
|
||||||
};
|
|
||||||
|
|
||||||
const start = () => {
|
|
||||||
stop();
|
|
||||||
resize();
|
|
||||||
if (prefersReduced.matches) {
|
|
||||||
drawStatic();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rafId = requestAnimationFrame(render);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResize = () => {
|
|
||||||
resize();
|
|
||||||
if (prefersReduced.matches) {
|
|
||||||
drawStatic();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const themeObserver = new MutationObserver(() => {
|
|
||||||
if (prefersReduced.matches) {
|
|
||||||
drawStatic();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
||||||
prefersReduced.addEventListener('change', start);
|
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
window.addEventListener('load', onResize);
|
|
||||||
|
|
||||||
start();
|
|
||||||
})();
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
(() => {
|
|
||||||
const hero = document.querySelector('.hero-section');
|
|
||||||
if (!hero) return;
|
|
||||||
|
|
||||||
const body = document.body;
|
|
||||||
const header = document.querySelector('.site-header');
|
|
||||||
const titleSvg = hero.querySelector('.hero-title-svg');
|
|
||||||
const scrollIndicator = hero.querySelector('.hero-scroll-indicator');
|
|
||||||
const scrollArrow = scrollIndicator ? scrollIndicator.querySelector('.scroll-arrow') : null;
|
|
||||||
const scrollText = scrollIndicator ? scrollIndicator.querySelector('.scroll-text') : null;
|
|
||||||
|
|
||||||
const desktopQuery = window.matchMedia('(min-width: 900px)');
|
|
||||||
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
||||||
const anime = window.anime;
|
|
||||||
const hasAnime = typeof anime === 'function';
|
|
||||||
|
|
||||||
let arrowPulse = null;
|
|
||||||
let didIntro = false;
|
|
||||||
let ticking = false;
|
|
||||||
|
|
||||||
const stopArrowPulse = () => {
|
|
||||||
if (!arrowPulse) return;
|
|
||||||
arrowPulse.pause();
|
|
||||||
arrowPulse = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startArrowPulse = () => {
|
|
||||||
if (!hasAnime || reducedMotionQuery.matches || !scrollArrow) return;
|
|
||||||
stopArrowPulse();
|
|
||||||
scrollArrow.style.animation = 'none';
|
|
||||||
anime.remove(scrollArrow);
|
|
||||||
anime.set(scrollArrow, {
|
|
||||||
translateY: 0,
|
|
||||||
opacity: 0.5,
|
|
||||||
rotate: 45,
|
|
||||||
});
|
|
||||||
arrowPulse = anime({
|
|
||||||
targets: scrollArrow,
|
|
||||||
translateY: [0, 6, 0],
|
|
||||||
opacity: [0.5, 1, 0.5],
|
|
||||||
rotate: 45,
|
|
||||||
duration: 1600,
|
|
||||||
easing: 'easeInOutSine',
|
|
||||||
loop: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearInlineState = () => {
|
|
||||||
if (header) {
|
|
||||||
header.style.transform = '';
|
|
||||||
}
|
|
||||||
if (scrollIndicator) {
|
|
||||||
scrollIndicator.style.opacity = '';
|
|
||||||
scrollIndicator.style.transform = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const animateScrollState = (isAtTop) => {
|
|
||||||
if (!hasAnime || reducedMotionQuery.matches) return;
|
|
||||||
|
|
||||||
if (header) {
|
|
||||||
anime.remove(header);
|
|
||||||
anime({
|
|
||||||
targets: header,
|
|
||||||
translateY: isAtTop ? '-100%' : '0%',
|
|
||||||
duration: isAtTop ? 500 : 380,
|
|
||||||
easing: isAtTop ? 'easeInOutCubic' : 'easeOutCubic',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollIndicator) {
|
|
||||||
anime.remove(scrollIndicator);
|
|
||||||
anime({
|
|
||||||
targets: scrollIndicator,
|
|
||||||
opacity: isAtTop ? 1 : 0,
|
|
||||||
duration: isAtTop ? 380 : 260,
|
|
||||||
easing: isAtTop ? 'easeOutCubic' : 'easeOutQuad',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyState = (animated) => {
|
|
||||||
if (!desktopQuery.matches) {
|
|
||||||
body.classList.remove('home-hero-initial', 'home-hero-scrolled');
|
|
||||||
stopArrowPulse();
|
|
||||||
clearInlineState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAtTop = window.scrollY <= 6;
|
|
||||||
body.classList.toggle('home-hero-initial', isAtTop);
|
|
||||||
body.classList.toggle('home-hero-scrolled', !isAtTop);
|
|
||||||
|
|
||||||
if (animated) {
|
|
||||||
animateScrollState(isAtTop);
|
|
||||||
} else if (hasAnime) {
|
|
||||||
if (header) {
|
|
||||||
anime.remove(header);
|
|
||||||
}
|
|
||||||
if (scrollIndicator) {
|
|
||||||
anime.remove(scrollIndicator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAtTop) {
|
|
||||||
startArrowPulse();
|
|
||||||
} else {
|
|
||||||
stopArrowPulse();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const playIntro = () => {
|
|
||||||
if (didIntro || !hasAnime || reducedMotionQuery.matches || !desktopQuery.matches || window.scrollY > 6) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
didIntro = true;
|
|
||||||
if (titleSvg) {
|
|
||||||
anime.set(titleSvg, { opacity: 0, translateY: 14, scale: 1.01 });
|
|
||||||
}
|
|
||||||
if (scrollArrow) {
|
|
||||||
anime.set(scrollArrow, { opacity: 0, translateY: 8, rotate: 45 });
|
|
||||||
}
|
|
||||||
if (scrollText) {
|
|
||||||
anime.set(scrollText, { opacity: 0, translateY: 8 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeline = anime.timeline({
|
|
||||||
easing: 'easeOutCubic',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (titleSvg) {
|
|
||||||
timeline.add({
|
|
||||||
targets: titleSvg,
|
|
||||||
opacity: 1,
|
|
||||||
translateY: -8,
|
|
||||||
scale: 1.06,
|
|
||||||
duration: 860,
|
|
||||||
easing: 'easeOutExpo',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollArrow || scrollText) {
|
|
||||||
timeline.add({
|
|
||||||
targets: [scrollArrow, scrollText].filter(Boolean),
|
|
||||||
opacity: (el) => (el === scrollArrow ? [0, 1] : [0, 1]),
|
|
||||||
translateY: 0,
|
|
||||||
rotate: (el) => (el === scrollArrow ? 45 : 0),
|
|
||||||
duration: 420,
|
|
||||||
delay: anime.stagger(80),
|
|
||||||
}, '-=300');
|
|
||||||
}
|
|
||||||
|
|
||||||
timeline.finished.then(() => {
|
|
||||||
startArrowPulse();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onScroll = () => {
|
|
||||||
if (ticking) return;
|
|
||||||
ticking = true;
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
applyState(true);
|
|
||||||
ticking = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResize = () => {
|
|
||||||
applyState(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onReducedMotionChange = () => {
|
|
||||||
if (reducedMotionQuery.matches) {
|
|
||||||
stopArrowPulse();
|
|
||||||
if (scrollArrow) {
|
|
||||||
scrollArrow.style.animation = 'none';
|
|
||||||
}
|
|
||||||
} else if (window.scrollY <= 6 && desktopQuery.matches) {
|
|
||||||
startArrowPulse();
|
|
||||||
}
|
|
||||||
applyState(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
applyState(false);
|
|
||||||
playIntro();
|
|
||||||
|
|
||||||
window.addEventListener('scroll', onScroll, { passive: true });
|
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
desktopQuery.addEventListener('change', onResize);
|
|
||||||
reducedMotionQuery.addEventListener('change', onReducedMotionChange);
|
|
||||||
})();
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
(() => {
|
|
||||||
const steps = document.querySelectorAll('.intro-step');
|
|
||||||
if (!steps.length) return;
|
|
||||||
|
|
||||||
const markActive = () => {
|
|
||||||
const vh = window.innerHeight;
|
|
||||||
steps.forEach((step) => {
|
|
||||||
const rect = step.getBoundingClientRect();
|
|
||||||
const isActive = rect.top < vh * 0.8 && rect.bottom > vh * 0.2;
|
|
||||||
step.classList.toggle('is-active', isActive);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
markActive();
|
|
||||||
document.body.classList.add('has-intro-animations');
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
entry.target.classList.toggle('is-active', entry.isIntersecting);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: 0.35,
|
|
||||||
rootMargin: '0px 0px -15% 0px',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
steps.forEach((step) => observer.observe(step));
|
|
||||||
window.addEventListener('resize', markActive);
|
|
||||||
})();
|
|
||||||
@@ -34,6 +34,8 @@
|
|||||||
let trackWidth = 0;
|
let trackWidth = 0;
|
||||||
let stageMarkers = [];
|
let stageMarkers = [];
|
||||||
let stageLayout = null;
|
let stageLayout = null;
|
||||||
|
let pageVisible = document.visibilityState !== 'hidden';
|
||||||
|
let canvasVisible = true;
|
||||||
|
|
||||||
const waveShape1 = { base: 0.16, amp: 7.5, len: 280, speed: 0.9 };
|
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 waveShape2 = { base: 0.44, amp: 6, len: 340, speed: 0.75 };
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
dpr = window.devicePixelRatio || 1;
|
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
width = canvas.clientWidth;
|
width = canvas.clientWidth;
|
||||||
height = canvas.clientHeight;
|
height = canvas.clientHeight;
|
||||||
canvas.width = Math.max(1, Math.floor(width * dpr));
|
canvas.width = Math.max(1, Math.floor(width * dpr));
|
||||||
@@ -245,6 +247,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderWaves(timestamp) {
|
function renderWaves(timestamp) {
|
||||||
|
if (!pageVisible || !canvasVisible || prefersReducedMotion.matches) {
|
||||||
|
rafId = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const theme = getTheme();
|
const theme = getTheme();
|
||||||
const palette = palettes[theme];
|
const palette = palettes[theme];
|
||||||
const time = timestamp * 0.001;
|
const time = timestamp * 0.001;
|
||||||
@@ -279,7 +286,7 @@
|
|||||||
function startWaves() {
|
function startWaves() {
|
||||||
stopWaves();
|
stopWaves();
|
||||||
resize();
|
resize();
|
||||||
if (prefersReducedMotion.matches) {
|
if (prefersReducedMotion.matches || !pageVisible || !canvasVisible) {
|
||||||
drawStaticWaves();
|
drawStaticWaves();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -604,6 +611,25 @@
|
|||||||
});
|
});
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||||
|
|
||||||
|
const updateVisibility = () => {
|
||||||
|
pageVisible = document.visibilityState !== 'hidden';
|
||||||
|
if (pageVisible && canvasVisible) {
|
||||||
|
startWaves();
|
||||||
|
} else {
|
||||||
|
stopWaves();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', updateVisibility);
|
||||||
|
|
||||||
|
if (typeof IntersectionObserver !== 'undefined') {
|
||||||
|
const visibilityObserver = new IntersectionObserver((entries) => {
|
||||||
|
canvasVisible = entries.length === 0 || entries[0].isIntersecting;
|
||||||
|
updateVisibility();
|
||||||
|
}, { threshold: 0.01 });
|
||||||
|
visibilityObserver.observe(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof prefersReducedMotion.addEventListener === 'function') {
|
if (typeof prefersReducedMotion.addEventListener === 'function') {
|
||||||
prefersReducedMotion.addEventListener('change', handleMotionPreferenceChange);
|
prefersReducedMotion.addEventListener('change', handleMotionPreferenceChange);
|
||||||
} else if (typeof prefersReducedMotion.addListener === 'function') {
|
} else if (typeof prefersReducedMotion.addListener === 'function') {
|
||||||
@@ -830,8 +856,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const successSelector = '.frm_message:not(.itstudio-otp-status), .frm_success_style, .frm_success';
|
||||||
const updateSuccessMessage = () => {
|
const updateSuccessMessage = () => {
|
||||||
const successNode = formHost.querySelector('.frm_message, .frm_success_style, .frm_success');
|
const successNode = formHost.querySelector(successSelector);
|
||||||
if (!successNode) {
|
if (!successNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -843,13 +870,37 @@
|
|||||||
|
|
||||||
const messageCn = resolvedIntent === 'draft' ? '草稿已保存' : '你的报名表单已提交,感谢报名';
|
const messageCn = resolvedIntent === 'draft' ? '草稿已保存' : '你的报名表单已提交,感谢报名';
|
||||||
const messageEn = resolvedIntent === 'draft' ? 'Draft saved.' : 'Your registration form has been submitted. Thank you for applying.';
|
const messageEn = resolvedIntent === 'draft' ? 'Draft saved.' : 'Your registration form has been submitted. Thank you for applying.';
|
||||||
|
const nextText = pickLocaleText(messageCn, messageEn);
|
||||||
|
if (successNode.dataset.itstudioNoticeIntent === resolvedIntent && successNode.textContent === nextText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
successNode.dataset.itstudioNoticeIntent = resolvedIntent;
|
||||||
successNode.dataset.cn = messageCn;
|
successNode.dataset.cn = messageCn;
|
||||||
successNode.dataset.en = messageEn;
|
successNode.dataset.en = messageEn;
|
||||||
successNode.textContent = pickLocaleText(messageCn, messageEn);
|
successNode.textContent = nextText;
|
||||||
showJoinSubmitNotice(resolvedIntent);
|
showJoinSubmitNotice(resolvedIntent);
|
||||||
};
|
};
|
||||||
|
|
||||||
const observer = new MutationObserver(updateSuccessMessage);
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
const successChanged = mutations.some((mutation) => {
|
||||||
|
const target = mutation.target && mutation.target.nodeType === 1 ? mutation.target : null;
|
||||||
|
if (target && (target.matches(successSelector) || target.closest(successSelector))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes)).some((node) => {
|
||||||
|
return node.nodeType === 1 && (
|
||||||
|
node.matches(successSelector) ||
|
||||||
|
Boolean(node.querySelector && node.querySelector(successSelector))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (successChanged) {
|
||||||
|
updateSuccessMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
observer.observe(formHost, { childList: true, subtree: true });
|
observer.observe(formHost, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,66 +13,134 @@
|
|||||||
let dpr = 1;
|
let dpr = 1;
|
||||||
let rafId = null;
|
let rafId = null;
|
||||||
let startAt = performance.now();
|
let startAt = performance.now();
|
||||||
|
let pageVisible = document.visibilityState !== 'hidden';
|
||||||
|
let heroVisible = true;
|
||||||
|
|
||||||
const EMPTY_SET = new Set();
|
const EMPTY_SET = new Set();
|
||||||
const WORD_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
const WORD_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||||
|
const VARIABLE_RE = /^\$[A-Za-z_][A-Za-z0-9_]*$/;
|
||||||
const NUMBER_RE = /^\d+(?:_\d+)*(?:\.\d+)?$/;
|
const NUMBER_RE = /^\d+(?:_\d+)*(?:\.\d+)?$/;
|
||||||
const STRING_RE = /^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)$/;
|
const STRING_RE = /^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)$/;
|
||||||
const CONSTANT_RE = /^[A-Z][A-Z0-9_]{2,}$/;
|
const CONSTANT_RE = /^[A-Z][A-Z0-9_]{2,}$/;
|
||||||
const TYPE_RE = /^[A-Z][A-Za-z0-9_]*$/;
|
const TYPE_RE = /^[A-Z][A-Za-z0-9_]*$/;
|
||||||
const OPERATOR_RE = /^(::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.#])$/;
|
const OPERATOR_RE = /^(::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.#])$/;
|
||||||
const TOKEN_RE = /(\/\/.*$|#\[.*$|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:_\d+)*(?:\.\d+)?\b|[A-Za-z_][A-Za-z0-9_]*|::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.,;()[\]{}#])/g;
|
const TOKEN_RE = /(\/\*.*?\*\/|<!--.*?-->|#\[[^\]]*\]|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|@[A-Za-z_][A-Za-z0-9_]*|\$[A-Za-z_][A-Za-z0-9_]*|\b\d+(?:_\d+)*(?:\.\d+)?\b|[A-Za-z_][A-Za-z0-9_]*|::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.,;()[\]{}#])/g;
|
||||||
|
const keywordSet = (words) => new Set(words.split(/\s+/).filter(Boolean));
|
||||||
|
const mergeKeywordSets = (...sets) => new Set(sets.flatMap((set) => [...set]));
|
||||||
|
|
||||||
|
const C_KEYWORDS = keywordSet('auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned void volatile while _Bool true false null');
|
||||||
|
const CPP_KEYWORDS = mergeKeywordSets(C_KEYWORDS, keywordSet('alignas alignof and and_eq asm bitand bitor bool catch char8_t char16_t char32_t class compl concept constexpr consteval constinit const_cast co_await co_return co_yield decltype delete dynamic_cast explicit export false friend mutable namespace new noexcept not not_eq nullptr operator or or_eq private protected public reinterpret_cast requires static_assert static_cast template this thread_local throw true try typeid typename using virtual wchar_t xor xor_eq std include'));
|
||||||
|
const JVM_KEYWORDS = keywordSet('abstract as assert boolean break byte case catch char class const constructor continue data default do double else enum extends false final finally float for fun goto if implements import in instanceof int interface internal is long native new null object open package private protected public record return sealed short static strictfp super switch synchronized this throw throws trait transient true try typealias typeof val var void volatile when while yield');
|
||||||
|
const JS_KEYWORDS = keywordSet('as async await break case catch class const continue debugger default delete do else export extends false finally for from function get if import in instanceof let new null of return set static super switch this throw true try typeof undefined var void while with yield interface implements enum private protected public readonly abstract declare namespace type keyof infer never unknown any number string boolean symbol bigint');
|
||||||
|
const PYTHON_KEYWORDS = keywordSet('and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield match case');
|
||||||
|
const SHELL_KEYWORDS = keywordSet('alias bg bind break builtin caller case cd command compgen complete continue declare dirs disown echo enable eval exec exit export false fc fg for function getopts hash help history if in jobs kill let local logout mapfile popd printf pushd pwd read readonly return select set shift shopt source suspend test then time times trap true type typeset ulimit umask unalias unset until wait while do done elif else esac fi');
|
||||||
|
const SQL_KEYWORDS = keywordSet('add all alter analyze and as asc begin between by case check column commit constraint create database default delete desc distinct drop else end exists explain false foreign from full grant group having index inner insert into is join key left like limit not null on or order outer primary procedure references return revoke right rollback row select set table then trigger true union unique update values view when where with');
|
||||||
|
|
||||||
|
const LANGUAGE_ALIASES = {
|
||||||
|
springboot: 'java', js: 'javascript', ts: 'typescript', py: 'python', rb: 'ruby',
|
||||||
|
sh: 'bash', shell: 'bash', ps1: 'powershell', cs: 'csharp', html5: 'html', yml: 'yaml',
|
||||||
|
};
|
||||||
|
|
||||||
const LANGUAGE_KEYWORDS = {
|
const LANGUAGE_KEYWORDS = {
|
||||||
springboot: new Set([
|
c: C_KEYWORDS,
|
||||||
'package', 'import', 'public', 'private', 'protected', 'class', 'interface',
|
cpp: CPP_KEYWORDS,
|
||||||
'static', 'final', 'new', 'return', 'if', 'else', 'try', 'catch', 'finally',
|
objectivec: mergeKeywordSets(C_KEYWORDS, keywordSet('id self super nil YES NO interface implementation protocol property synthesize dynamic selector encode end autoreleasepool')),
|
||||||
'throw', 'throws', 'void', 'long', 'int', 'boolean', 'null', 'true', 'false',
|
csharp: keywordSet('abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly record ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void volatile while async await var dynamic get set init value yield'),
|
||||||
'extends', 'implements', 'this',
|
java: JVM_KEYWORDS,
|
||||||
]),
|
kotlin: JVM_KEYWORDS,
|
||||||
rust: new Set([
|
scala: JVM_KEYWORDS,
|
||||||
'use', 'pub', 'struct', 'enum', 'impl', 'async', 'await', 'fn', 'let', 'mut',
|
groovy: JVM_KEYWORDS,
|
||||||
'loop', 'match', 'if', 'else', 'return', 'while', 'for', 'in', 'break', 'continue',
|
javascript: JS_KEYWORDS,
|
||||||
'crate', 'self', 'super', 'mod', 'where', 'Result', 'Ok', 'Err',
|
typescript: JS_KEYWORDS,
|
||||||
]),
|
jsx: JS_KEYWORDS,
|
||||||
cpp: new Set([
|
tsx: JS_KEYWORDS,
|
||||||
'include', 'template', 'typename', 'concept', 'requires', 'struct', 'class',
|
vue: JS_KEYWORDS,
|
||||||
'public', 'private', 'protected', 'explicit', 'void', 'auto', 'const', 'constexpr',
|
python: PYTHON_KEYWORDS,
|
||||||
'if', 'else', 'while', 'for', 'return', 'using', 'namespace', 'std', 'nullptr',
|
go: keywordSet('break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var true false nil'),
|
||||||
'true', 'false',
|
rust: keywordSet('as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while abstract become box do final macro override priv typeof unsized virtual yield try'),
|
||||||
]),
|
php: keywordSet('abstract and array as break callable case catch class clone const continue declare default die do echo else elseif empty enddeclare endfor endforeach endif endswitch endwhile enum eval exit extends final finally fn for foreach function global goto if implements include include_once instanceof insteadof interface isset list match namespace new null or print private protected public readonly require require_once return static switch throw trait true try unset use var while xor yield'),
|
||||||
python: new Set([
|
ruby: keywordSet('alias and begin break case class def defined do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield'),
|
||||||
'import', 'from', 'as', 'class', 'def', 'return', 'if', 'elif', 'else', 'for',
|
swift: keywordSet('associatedtype class deinit enum extension fileprivate func import init inout internal let open operator private protocol public rethrows static struct subscript typealias var break case continue default defer do else fallthrough for guard if in repeat return switch where while as Any catch false is nil super self Self throw throws true try async await actor some'),
|
||||||
'while', 'in', 'and', 'or', 'not', 'await', 'async', 'with', 'try', 'except',
|
dart: keywordSet('abstract as assert async await break case catch class const continue covariant default deferred do dynamic else enum export extends extension external factory false final finally for function get hide if implements import in interface is late library mixin new null on operator part required rethrow return set show static super switch sync this throw true try typedef var void while with yield'),
|
||||||
'finally', 'lambda', 'None', 'True', 'False', 'yield',
|
lua: keywordSet('and break do else elseif end false for function goto if in local nil not or repeat return then true until while'),
|
||||||
]),
|
perl: keywordSet('continue do else elsif eval for foreach given goto if last local my next no package redo require return state sub unless until use when while our'),
|
||||||
javascript: new Set([
|
r: keywordSet('break else FALSE for function if Inf NA NaN next NULL repeat return TRUE while in'),
|
||||||
'class', 'constructor', 'return', 'if', 'else', 'while', 'for', 'const', 'let',
|
matlab: keywordSet('break case catch classdef continue else elseif end for function global if otherwise parfor persistent return spmd switch try while'),
|
||||||
'var', 'new', 'async', 'await', 'try', 'catch', 'finally', 'throw', 'export',
|
julia: keywordSet('baremodule begin break catch const continue do else elseif end export false finally for function global if import let local macro module quote return struct true try using while abstract type primitive mutable where'),
|
||||||
'import', 'from', 'this', 'null', 'true', 'false', 'yield', 'function',
|
nim: keywordSet('addr and as asm bind block break case cast concept const continue converter defer discard distinct div do elif else end enum except export finally for from func generic if import in include interface is isnot iterator let macro method mixin mod nil not object of or out proc ptr raise ref return shl shr static template try tuple type using var when while with without xor yield'),
|
||||||
]),
|
haskell: keywordSet('as case class data default deriving do else family forall foreign hiding if import in infix infixl infixr instance let mdo module newtype of qualified rec then type where'),
|
||||||
nim: new Set([
|
elixir: keywordSet('after alias and case catch cond def defmodule defp defprotocol defstruct do else end fn for if import in not or quote raise receive require rescue try unless use when'),
|
||||||
'import', 'type', 'enum', 'object', 'ref', 'proc', 'result', 'let', 'var',
|
erlang: keywordSet('after begin case catch cond end fun if let of query receive try when bnot not div rem band and bor bxor bsl bsr or xor'),
|
||||||
'if', 'elif', 'else', 'for', 'in', 'while', 'return', 'await', 'defer',
|
bash: SHELL_KEYWORDS,
|
||||||
'mod', 'echo', 'discard',
|
powershell: keywordSet('begin break catch class continue data define do dynamicparam else elseif end enum exit filter finally for foreach from function hidden if in param process return static switch throw trap try until using var while workflow parallel sequence true false null'),
|
||||||
]),
|
sql: SQL_KEYWORDS,
|
||||||
|
graphql: keywordSet('query mutation subscription fragment on schema scalar type interface union enum input directive extend implements repeatable true false null'),
|
||||||
|
solidity: keywordSet('abstract address after alias apply auto byte case catch constant constructor contract delete do else emit enum event external fallback false final finally for function if immutable implements import in indexed inline interface internal is mapping memory modifier new null of override payable private promise public pure receive reference relocatable return returns sealed sizeof static storage string struct supports switch this throw true try type typedef typeof unchecked using var view virtual while'),
|
||||||
|
html: keywordSet('doctype html head body main header footer nav section article aside div span canvas script style link meta title template slot form label input button select option textarea table thead tbody tr th td img svg path'),
|
||||||
|
xml: EMPTY_SET,
|
||||||
|
css: keywordSet('important inherit initial revert unset auto none block inline flex grid relative absolute fixed sticky hidden visible transparent currentColor media supports keyframes from to'),
|
||||||
|
scss: keywordSet('mixin include extend function return if else each for while use forward import debug warn error at-root'),
|
||||||
|
less: keywordSet('import media supports plugin inline reference optional multiple when extend all'),
|
||||||
|
dockerfile: keywordSet('ADD ARG CMD COPY ENTRYPOINT ENV EXPOSE FROM HEALTHCHECK LABEL MAINTAINER ONBUILD RUN SHELL STOPSIGNAL USER VOLUME WORKDIR AS'),
|
||||||
|
json: keywordSet('true false null'),
|
||||||
|
yaml: keywordSet('true false null yes no on off'),
|
||||||
|
toml: keywordSet('true false'),
|
||||||
|
markdown: EMPTY_SET,
|
||||||
|
plaintext: EMPTY_SET,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LINE_COMMENT_MARKERS = {
|
||||||
|
bash: ['#'], powershell: ['#'], python: ['#'], ruby: ['#'], r: ['#'], perl: ['#'], yaml: ['#'], toml: ['#'], dockerfile: ['#'],
|
||||||
|
php: ['//', '#'], sql: ['--'], lua: ['--'], haskell: ['--'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeLanguage = (language) => {
|
||||||
|
const key = String(language || 'plaintext').toLowerCase().replace(/[^a-z0-9#+-]/g, '');
|
||||||
|
return LANGUAGE_ALIASES[key] || key || 'plaintext';
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLineCommentIndex = (line, language) => {
|
||||||
|
const markers = LINE_COMMENT_MARKERS[language]
|
||||||
|
|| (['html', 'xml', 'css', 'scss', 'less', 'json', 'markdown', 'plaintext'].includes(language) ? [] : ['//']);
|
||||||
|
if (!markers.length) return -1;
|
||||||
|
|
||||||
|
let quote = '';
|
||||||
|
for (let index = 0; index < line.length; index += 1) {
|
||||||
|
const char = line[index];
|
||||||
|
if (quote) {
|
||||||
|
if (char === '\\') {
|
||||||
|
index += 1;
|
||||||
|
} else if (char === quote) {
|
||||||
|
quote = '';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '"' || char === "'" || char === '`') {
|
||||||
|
quote = char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (markers.some((marker) => line.slice(index, index + marker.length) === marker)) return index;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const classifyToken = (token, language, nextToken, prevToken, atLineStart) => {
|
const classifyToken = (token, language, nextToken, prevToken, atLineStart) => {
|
||||||
const keywords = LANGUAGE_KEYWORDS[language] || EMPTY_SET;
|
const normalizedLanguage = normalizeLanguage(language);
|
||||||
|
const keywords = LANGUAGE_KEYWORDS[normalizedLanguage] || EMPTY_SET;
|
||||||
|
|
||||||
if (token.startsWith('//')) return 'comment';
|
if (token.startsWith('/*') || token.startsWith('<!--')) return 'comment';
|
||||||
if (token.startsWith('#[') || token.startsWith('@')) return 'annotation';
|
if (token.startsWith('#[') || token.startsWith('@')) return 'annotation';
|
||||||
if (STRING_RE.test(token)) return 'string';
|
if (STRING_RE.test(token)) return 'string';
|
||||||
if (NUMBER_RE.test(token)) return 'number';
|
if (NUMBER_RE.test(token)) return 'number';
|
||||||
if (OPERATOR_RE.test(token)) return 'operator';
|
if (OPERATOR_RE.test(token)) return 'operator';
|
||||||
|
if (VARIABLE_RE.test(token)) return 'field';
|
||||||
|
|
||||||
if (!WORD_RE.test(token)) {
|
if (!WORD_RE.test(token)) {
|
||||||
return 'plain';
|
return 'plain';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keywords.has(token)) return 'keyword';
|
if (keywords.has(token)) return 'keyword';
|
||||||
if (language === 'cpp' && atLineStart && prevToken === '#' && token === 'include') return 'keyword';
|
if ((normalizedLanguage === 'c' || normalizedLanguage === 'cpp' || normalizedLanguage === 'objectivec') && atLineStart && prevToken === '#' && token === 'include') return 'keyword';
|
||||||
if (CONSTANT_RE.test(token)) return 'constant';
|
if (CONSTANT_RE.test(token)) return 'constant';
|
||||||
if (nextToken === '(') return 'method';
|
if (nextToken === '(') return 'method';
|
||||||
if (TYPE_RE.test(token)) return 'type';
|
if (TYPE_RE.test(token)) return 'type';
|
||||||
@@ -82,8 +150,13 @@
|
|||||||
const tokenizeLine = (line, language) => {
|
const tokenizeLine = (line, language) => {
|
||||||
if (line.length === 0) return [['', 'plain']];
|
if (line.length === 0) return [['', 'plain']];
|
||||||
|
|
||||||
const matches = [...line.matchAll(TOKEN_RE)];
|
const normalizedLanguage = normalizeLanguage(language);
|
||||||
if (matches.length === 0) return [[line, 'plain']];
|
const commentIndex = findLineCommentIndex(line, normalizedLanguage);
|
||||||
|
const code = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
|
||||||
|
const comment = commentIndex >= 0 ? line.slice(commentIndex) : '';
|
||||||
|
|
||||||
|
const matches = [...code.matchAll(TOKEN_RE)];
|
||||||
|
if (matches.length === 0) return comment ? [[code, 'plain'], [comment, 'comment']] : [[line, 'plain']];
|
||||||
|
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
@@ -94,24 +167,21 @@
|
|||||||
const raw = match[0];
|
const raw = match[0];
|
||||||
const nextToken = i + 1 < matches.length ? matches[i + 1][0] : '';
|
const nextToken = i + 1 < matches.length ? matches[i + 1][0] : '';
|
||||||
const prevToken = i > 0 ? matches[i - 1][0] : '';
|
const prevToken = i > 0 ? matches[i - 1][0] : '';
|
||||||
const atLineStart = line.slice(0, start).trim().length === 0;
|
const atLineStart = code.slice(0, start).trim().length === 0;
|
||||||
|
|
||||||
if (start > cursor) {
|
if (start > cursor) {
|
||||||
tokens.push([line.slice(cursor, start), 'plain']);
|
tokens.push([code.slice(cursor, start), 'plain']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((language === 'python' || language === 'nim') && raw === '#') {
|
tokens.push([raw, classifyToken(raw, normalizedLanguage, nextToken, prevToken, atLineStart)]);
|
||||||
tokens.push([line.slice(start), 'comment']);
|
|
||||||
cursor = line.length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens.push([raw, classifyToken(raw, language, nextToken, prevToken, atLineStart)]);
|
|
||||||
cursor = start + raw.length;
|
cursor = start + raw.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor < line.length) {
|
if (cursor < code.length) {
|
||||||
tokens.push([line.slice(cursor), 'plain']);
|
tokens.push([code.slice(cursor), 'plain']);
|
||||||
|
}
|
||||||
|
if (comment) {
|
||||||
|
tokens.push([comment, 'comment']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokens.length === 0) {
|
if (tokens.length === 0) {
|
||||||
@@ -538,7 +608,22 @@
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const snippetCatalog = rawSnippetCatalog.map((entry) => ({
|
const configuredSnippets = Array.isArray(window.ITStudioLandingHero?.snippets)
|
||||||
|
? window.ITStudioLandingHero.snippets
|
||||||
|
.filter((entry) => entry && typeof entry.code === 'string' && entry.code.trim() !== '')
|
||||||
|
.map((entry) => {
|
||||||
|
const lines = entry.code.replace(/\r\n?/g, '\n').split('\n').slice(0, 240);
|
||||||
|
return {
|
||||||
|
name: normalizeLanguage(entry.language),
|
||||||
|
title: typeof entry.title === 'string' ? entry.title : '',
|
||||||
|
repeat: Math.min(8, Math.max(1, Math.ceil(160 / Math.max(1, lines.length)))),
|
||||||
|
lines,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const sourceSnippetCatalog = configuredSnippets.length ? configuredSnippets : rawSnippetCatalog;
|
||||||
|
|
||||||
|
const snippetCatalog = sourceSnippetCatalog.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
lines: tokenizeSnippet(entry.lines, entry.name),
|
lines: tokenizeSnippet(entry.lines, entry.name),
|
||||||
}));
|
}));
|
||||||
@@ -615,12 +700,16 @@
|
|||||||
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
||||||
const getTheme = () => (document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light');
|
const getTheme = () => (document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light');
|
||||||
const tokenCount = (tokens) => tokens.reduce((sum, token) => sum + token[0].length, 0);
|
const tokenCount = (tokens) => tokens.reduce((sum, token) => sum + token[0].length, 0);
|
||||||
|
const lineCharTotals = codeLines.map((line) => tokenCount(line));
|
||||||
|
const lineDurations = lineCharTotals.map((chars) => Math.max(150, chars * 8));
|
||||||
|
const totalTypingDuration = lineDurations.reduce((sum, duration) => sum + duration, 0);
|
||||||
|
const cycleDuration = totalTypingDuration + 500;
|
||||||
|
|
||||||
const resizeCanvas = () => {
|
const resizeCanvas = () => {
|
||||||
const rect = hero.getBoundingClientRect();
|
const rect = hero.getBoundingClientRect();
|
||||||
width = Math.max(1, Math.round(rect.width));
|
width = Math.max(1, Math.round(rect.width));
|
||||||
height = Math.max(1, Math.round(rect.height));
|
height = Math.max(1, Math.round(rect.height));
|
||||||
dpr = window.devicePixelRatio || 1;
|
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
canvas.width = Math.floor(width * dpr);
|
canvas.width = Math.floor(width * dpr);
|
||||||
canvas.height = Math.floor(height * dpr);
|
canvas.height = Math.floor(height * dpr);
|
||||||
canvas.style.width = `${width}px`;
|
canvas.style.width = `${width}px`;
|
||||||
@@ -668,12 +757,6 @@
|
|||||||
const baseLineNo = 1;
|
const baseLineNo = 1;
|
||||||
const scrollLeadLines = 2;
|
const scrollLeadLines = 2;
|
||||||
|
|
||||||
const lineCharTotals = codeLines.map((line) => tokenCount(line));
|
|
||||||
const lineDurations = lineCharTotals.map((chars) => Math.max(150, chars * 8));
|
|
||||||
const totalTypingDuration = lineDurations.reduce((sum, duration) => sum + duration, 0);
|
|
||||||
const holdDuration = 500;
|
|
||||||
const cycleDuration = totalTypingDuration + holdDuration;
|
|
||||||
|
|
||||||
ctx.fillStyle = palette.background;
|
ctx.fillStyle = palette.background;
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
ctx.fillStyle = palette.editor;
|
ctx.fillStyle = palette.editor;
|
||||||
@@ -683,7 +766,7 @@
|
|||||||
ctx.fillStyle = palette.divider;
|
ctx.fillStyle = palette.divider;
|
||||||
ctx.fillRect(gutterWidth, 0, 1, height);
|
ctx.fillRect(gutterWidth, 0, 1, height);
|
||||||
|
|
||||||
const cyclePos = isStatic ? totalTypingDuration + holdDuration : elapsed % cycleDuration;
|
const cyclePos = isStatic ? totalTypingDuration + 500 : elapsed % cycleDuration;
|
||||||
const writingPhase = cyclePos < totalTypingDuration;
|
const writingPhase = cyclePos < totalTypingDuration;
|
||||||
|
|
||||||
let completedLines = codeLines.length;
|
let completedLines = codeLines.length;
|
||||||
@@ -795,6 +878,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const render = (timestamp) => {
|
const render = (timestamp) => {
|
||||||
|
if (!pageVisible || !heroVisible || reducedMotionQuery.matches) {
|
||||||
|
rafId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const elapsed = timestamp - startAt;
|
const elapsed = timestamp - startAt;
|
||||||
const palette = themes[getTheme()];
|
const palette = themes[getTheme()];
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
@@ -817,6 +905,10 @@
|
|||||||
const start = () => {
|
const start = () => {
|
||||||
stop();
|
stop();
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
|
drawStatic();
|
||||||
|
if (!pageVisible || !heroVisible || reducedMotionQuery.matches) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
startAt = performance.now();
|
startAt = performance.now();
|
||||||
rafId = requestAnimationFrame(render);
|
rafId = requestAnimationFrame(render);
|
||||||
};
|
};
|
||||||
@@ -848,5 +940,24 @@
|
|||||||
window.addEventListener('resize', onResize);
|
window.addEventListener('resize', onResize);
|
||||||
window.addEventListener('load', onResize);
|
window.addEventListener('load', onResize);
|
||||||
|
|
||||||
|
const updateVisibility = () => {
|
||||||
|
pageVisible = document.visibilityState !== 'hidden';
|
||||||
|
if (pageVisible && heroVisible) {
|
||||||
|
start();
|
||||||
|
} else {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', updateVisibility);
|
||||||
|
|
||||||
|
if (typeof IntersectionObserver !== 'undefined') {
|
||||||
|
const visibilityObserver = new IntersectionObserver((entries) => {
|
||||||
|
heroVisible = entries.length === 0 || entries[0].isIntersecting;
|
||||||
|
updateVisibility();
|
||||||
|
}, { threshold: 0.01 });
|
||||||
|
visibilityObserver.observe(hero);
|
||||||
|
}
|
||||||
|
|
||||||
start();
|
start();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ function ensureViewTransitionStyles() {
|
|||||||
mix-blend-mode: normal;
|
mix-blend-mode: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root) {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
::view-transition-group(root) {
|
::view-transition-group(root) {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
@@ -86,7 +94,7 @@ function getTransitionOrigin(event, toggle) {
|
|||||||
function getRevealRadius(x, y) {
|
function getRevealRadius(x, y) {
|
||||||
const maxX = Math.max(x, window.innerWidth - x);
|
const maxX = Math.max(x, window.innerWidth - x);
|
||||||
const maxY = Math.max(y, window.innerHeight - y);
|
const maxY = Math.max(y, window.innerHeight - y);
|
||||||
return Math.hypot(maxX, maxY);
|
return Math.hypot(maxX, maxY) * 1.05 + 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchThemeWithScatter(theme, origin) {
|
function switchThemeWithScatter(theme, origin) {
|
||||||
@@ -108,16 +116,19 @@ function switchThemeWithScatter(theme, origin) {
|
|||||||
|
|
||||||
transition.ready
|
transition.ready
|
||||||
.then(() => {
|
.then(() => {
|
||||||
document.documentElement.animate(
|
const revealAnimation = document.documentElement.animate(
|
||||||
{
|
{
|
||||||
clipPath: [clipFrom, clipTo],
|
clipPath: [clipFrom, clipTo],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 650,
|
duration: 650,
|
||||||
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||||
|
fill: 'both',
|
||||||
pseudoElement: '::view-transition-new(root)',
|
pseudoElement: '::view-transition-new(root)',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return revealAnimation.finished;
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ CPT 注册从主题挪到插件后,重写规则改变,必须在插件激活
|
|||||||
2. `itstudio_register_acf_fields()`(308-403):`group_itstudio_content_priority` → 插件1;`group_itstudio_recruitment_article` → 插件3。
|
2. `itstudio_register_acf_fields()`(308-403):`group_itstudio_content_priority` → 插件1;`group_itstudio_recruitment_article` → 插件3。
|
||||||
|
|
||||||
## 四、留在主题的部分(纯展示)
|
## 四、留在主题的部分(纯展示)
|
||||||
`itstudio_theme_setup`、评论禁用(21-60)、favicon/bootstrap 脚本(76-101)、`enqueue_scripts`(103)、`register_sidebars`(192)、`intro_body_class`(215)、`archive_document_title`(1040)、各 `*_fallback` 渲染(1052-1204、3907)。
|
`itstudio_theme_setup`、评论禁用(21-60)、favicon/bootstrap 脚本(76-101)、`enqueue_scripts`(103)、`register_sidebars`(192)、`archive_document_title`(1040)、各 `*_fallback` 渲染(1052-1204、3907)。
|
||||||
|
|
||||||
**建议删除**:`itstudio_apply_site_identity()`(62-74)——每次强制覆写站点标题/副标题,反模式。
|
**建议删除**:`itstudio_apply_site_identity()`(62-74)——每次强制覆写站点标题/副标题,反模式。
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,162 @@ function itstudio_theme_setup() {
|
|||||||
}
|
}
|
||||||
add_action('after_setup_theme', 'itstudio_theme_setup');
|
add_action('after_setup_theme', 'itstudio_theme_setup');
|
||||||
|
|
||||||
|
function itstudio_get_default_menu_item_english_title($title_cn) {
|
||||||
|
$titles = array(
|
||||||
|
'首页' => 'Home',
|
||||||
|
'公告通知' => 'Announcements',
|
||||||
|
'新闻' => 'News',
|
||||||
|
'社团新闻' => 'Club News',
|
||||||
|
'博客' => 'Blog',
|
||||||
|
'便民服务' => 'Services',
|
||||||
|
'加入我们' => 'Join Us',
|
||||||
|
'社团介绍' => 'About Us',
|
||||||
|
);
|
||||||
|
|
||||||
|
return isset($titles[$title_cn]) ? $titles[$title_cn] : $title_cn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_render_menu_item_english_field($item_id, $item, $depth, $args, $current_object_id) {
|
||||||
|
$title_en = get_post_meta($item_id, '_itstudio_menu_title_en', true);
|
||||||
|
if ($title_en === '') {
|
||||||
|
$title_en = itstudio_get_default_menu_item_english_title(wp_strip_all_tags((string) $item->title));
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<p class="description description-wide">
|
||||||
|
<label for="edit-menu-item-itstudio-title-en-<?php echo esc_attr($item_id); ?>">
|
||||||
|
<?php esc_html_e('英文名称', 'itstudio'); ?><br>
|
||||||
|
<input type="text" id="edit-menu-item-itstudio-title-en-<?php echo esc_attr($item_id); ?>" class="widefat" name="itstudio_menu_title_en[<?php echo esc_attr($item_id); ?>]" value="<?php echo esc_attr($title_en); ?>" placeholder="<?php esc_attr_e('例如 Announcements', 'itstudio'); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
add_action('wp_nav_menu_item_custom_fields', 'itstudio_render_menu_item_english_field', 10, 5);
|
||||||
|
|
||||||
|
function itstudio_save_menu_item_english_field($menu_id, $menu_item_db_id, $args) {
|
||||||
|
if (!current_user_can('edit_theme_options') || !isset($_POST['itstudio_menu_title_en']) || !is_array($_POST['itstudio_menu_title_en'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$titles_en = wp_unslash($_POST['itstudio_menu_title_en']);
|
||||||
|
$title_en = isset($titles_en[$menu_item_db_id]) ? sanitize_text_field($titles_en[$menu_item_db_id]) : '';
|
||||||
|
if ($title_en === '') {
|
||||||
|
delete_post_meta($menu_item_db_id, '_itstudio_menu_title_en');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta($menu_item_db_id, '_itstudio_menu_title_en', $title_en);
|
||||||
|
}
|
||||||
|
add_action('wp_update_nav_menu_item', 'itstudio_save_menu_item_english_field', 10, 3);
|
||||||
|
|
||||||
|
function itstudio_render_bilingual_menu_item_title($title, $item, $args, $depth) {
|
||||||
|
if (is_admin()) {
|
||||||
|
return $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title_cn = wp_strip_all_tags((string) $title);
|
||||||
|
$title_en = (string) get_post_meta($item->ID, '_itstudio_menu_title_en', true);
|
||||||
|
if ($title_en === '') {
|
||||||
|
$title_en = itstudio_get_default_menu_item_english_title($title_cn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('<span data-cn="%s" data-en="%s"></span>', esc_attr($title_cn), esc_attr($title_en));
|
||||||
|
}
|
||||||
|
add_filter('nav_menu_item_title', 'itstudio_render_bilingual_menu_item_title', 10, 4);
|
||||||
|
|
||||||
|
function itstudio_is_tidb_database() {
|
||||||
|
static $is_tidb = null;
|
||||||
|
|
||||||
|
if ($is_tidb !== null) {
|
||||||
|
return $is_tidb;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$server_info = method_exists($wpdb, 'db_server_info') ? (string) $wpdb->db_server_info() : '';
|
||||||
|
if ($server_info === '') {
|
||||||
|
$server_info = (string) $wpdb->get_var('SELECT VERSION()');
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_tidb = stripos($server_info, 'tidb') !== false;
|
||||||
|
return $is_tidb;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_tidb_media_query_args($query) {
|
||||||
|
if (!wp_doing_ajax() || !itstudio_is_tidb_database()) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query['no_found_rows'] = true;
|
||||||
|
$query['itstudio_tidb_media_query'] = true;
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
add_filter('ajax_query_attachments_args', 'itstudio_tidb_media_query_args');
|
||||||
|
|
||||||
|
function itstudio_tidb_media_query_found_posts($posts, $query) {
|
||||||
|
if (!$query->get('itstudio_tidb_media_query') || !is_array($posts)) {
|
||||||
|
return $posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->found_posts = count($posts);
|
||||||
|
$posts_per_page = (int) $query->get('posts_per_page');
|
||||||
|
$query->max_num_pages = $posts_per_page > 0 ? (int) ceil($query->found_posts / $posts_per_page) : 0;
|
||||||
|
return $posts;
|
||||||
|
}
|
||||||
|
add_filter('the_posts', 'itstudio_tidb_media_query_found_posts', 10, 2);
|
||||||
|
|
||||||
|
function itstudio_tidb_remove_sql_calc_found_rows($request, $query) {
|
||||||
|
if (!itstudio_is_tidb_database() || stripos($request, 'SQL_CALC_FOUND_ROWS') === false) {
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query_id = spl_object_id($query);
|
||||||
|
$GLOBALS['itstudio_tidb_found_rows_queries'][$query_id] = true;
|
||||||
|
$query->set('itstudio_tidb_found_rows_query', true);
|
||||||
|
|
||||||
|
return preg_replace('/SQL_CALC_FOUND_ROWS\s+/i', '', $request, 1);
|
||||||
|
}
|
||||||
|
add_filter('posts_request', 'itstudio_tidb_remove_sql_calc_found_rows', 10, 2);
|
||||||
|
|
||||||
|
function itstudio_tidb_calculate_found_posts($found_posts, $query) {
|
||||||
|
$query_id = spl_object_id($query);
|
||||||
|
$is_marked = !empty($GLOBALS['itstudio_tidb_found_rows_queries'][$query_id]) || $query->get('itstudio_tidb_found_rows_query');
|
||||||
|
if (!$is_marked) {
|
||||||
|
return $found_posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($GLOBALS['itstudio_tidb_found_rows_queries'][$query_id]);
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$count_query = preg_replace('/^\s*SELECT\s+.*?\s+FROM\s+/is', 'SELECT COUNT(*) FROM ', $query->request, 1);
|
||||||
|
$count_query = preg_replace('/\s+ORDER BY\s+.*?(?=\s+LIMIT\s+|\s*$)/is', ' ', $count_query, 1);
|
||||||
|
$count_query = preg_replace('/\s+LIMIT\s+\d+(?:\s*,\s*\d+)?\s*$/i', '', trim($count_query), 1);
|
||||||
|
|
||||||
|
if ($count_query === null || $count_query === '') {
|
||||||
|
return $found_posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = $wpdb->get_var($count_query);
|
||||||
|
return is_numeric($count) ? (int) $count : $found_posts;
|
||||||
|
}
|
||||||
|
add_filter('found_posts', 'itstudio_tidb_calculate_found_posts', 10, 2);
|
||||||
|
|
||||||
|
function itstudio_open_honor_menu_item_in_new_tab($atts, $item, $args) {
|
||||||
|
$href = isset($atts['href']) ? (string) $atts['href'] : '';
|
||||||
|
$host = wp_parse_url($href, PHP_URL_HOST);
|
||||||
|
|
||||||
|
if ($host !== 'honor.itouc.cn') {
|
||||||
|
return $atts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$atts['target'] = '_blank';
|
||||||
|
$rel = isset($atts['rel']) ? (string) $atts['rel'] : '';
|
||||||
|
$rel_tokens = preg_split('/\s+/', trim($rel), -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
$rel_tokens = array_unique(array_merge($rel_tokens, array('noopener', 'noreferrer')));
|
||||||
|
$atts['rel'] = implode(' ', $rel_tokens);
|
||||||
|
|
||||||
|
return $atts;
|
||||||
|
}
|
||||||
|
add_filter('nav_menu_link_attributes', 'itstudio_open_honor_menu_item_in_new_tab', 10, 3);
|
||||||
|
|
||||||
function itstudio_get_site_content_defaults() {
|
function itstudio_get_site_content_defaults() {
|
||||||
return array(
|
return array(
|
||||||
'footer_links' => array(
|
'footer_links' => array(
|
||||||
@@ -69,6 +225,7 @@ function itstudio_get_site_content_defaults() {
|
|||||||
array('name_cn' => '五八工坊预约', 'name_en' => 'Workshop Booking', 'image_id' => 0),
|
array('name_cn' => '五八工坊预约', 'name_en' => 'Workshop Booking', 'image_id' => 0),
|
||||||
array('name_cn' => 'OUC便民服务', 'name_en' => 'OUC Services', 'image_id' => 0),
|
array('name_cn' => 'OUC便民服务', 'name_en' => 'OUC Services', 'image_id' => 0),
|
||||||
),
|
),
|
||||||
|
'homepage_code_snippets' => array(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +244,57 @@ function itstudio_get_site_content_settings() {
|
|||||||
'footer_icp' => isset($stored['footer_icp']) && is_array($stored['footer_icp']) ? array_merge($defaults['footer_icp'], $stored['footer_icp']) : array_merge($defaults['footer_icp'], array()),
|
'footer_icp' => isset($stored['footer_icp']) && is_array($stored['footer_icp']) ? array_merge($defaults['footer_icp'], $stored['footer_icp']) : array_merge($defaults['footer_icp'], array()),
|
||||||
'footer_copyright' => isset($stored['footer_copyright']) && is_array($stored['footer_copyright']) ? array_merge($defaults['footer_copyright'], $stored['footer_copyright']) : array_merge($defaults['footer_copyright'], array()),
|
'footer_copyright' => isset($stored['footer_copyright']) && is_array($stored['footer_copyright']) ? array_merge($defaults['footer_copyright'], $stored['footer_copyright']) : array_merge($defaults['footer_copyright'], array()),
|
||||||
'homepage_services' => isset($stored['homepage_services']) && is_array($stored['homepage_services']) ? $stored['homepage_services'] : array(),
|
'homepage_services' => isset($stored['homepage_services']) && is_array($stored['homepage_services']) ? $stored['homepage_services'] : array(),
|
||||||
|
'homepage_code_snippets' => isset($stored['homepage_code_snippets']) && is_array($stored['homepage_code_snippets']) ? $stored['homepage_code_snippets'] : array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_get_code_animation_languages() {
|
||||||
|
return array(
|
||||||
|
'plaintext' => __('纯文本', 'itstudio'),
|
||||||
|
'bash' => 'Bash / Shell',
|
||||||
|
'powershell' => 'PowerShell',
|
||||||
|
'c' => 'C',
|
||||||
|
'cpp' => 'C++',
|
||||||
|
'csharp' => 'C#',
|
||||||
|
'java' => 'Java / Spring Boot',
|
||||||
|
'kotlin' => 'Kotlin',
|
||||||
|
'scala' => 'Scala',
|
||||||
|
'groovy' => 'Groovy',
|
||||||
|
'javascript' => 'JavaScript',
|
||||||
|
'typescript' => 'TypeScript',
|
||||||
|
'jsx' => 'JSX',
|
||||||
|
'tsx' => 'TSX',
|
||||||
|
'vue' => 'Vue',
|
||||||
|
'python' => 'Python',
|
||||||
|
'go' => 'Go',
|
||||||
|
'rust' => 'Rust',
|
||||||
|
'php' => 'PHP',
|
||||||
|
'ruby' => 'Ruby',
|
||||||
|
'swift' => 'Swift',
|
||||||
|
'objectivec' => 'Objective-C',
|
||||||
|
'dart' => 'Dart',
|
||||||
|
'lua' => 'Lua',
|
||||||
|
'perl' => 'Perl',
|
||||||
|
'r' => 'R',
|
||||||
|
'matlab' => 'MATLAB',
|
||||||
|
'julia' => 'Julia',
|
||||||
|
'nim' => 'Nim',
|
||||||
|
'haskell' => 'Haskell',
|
||||||
|
'elixir' => 'Elixir',
|
||||||
|
'erlang' => 'Erlang',
|
||||||
|
'sql' => 'SQL',
|
||||||
|
'html' => 'HTML',
|
||||||
|
'xml' => 'XML',
|
||||||
|
'css' => 'CSS',
|
||||||
|
'scss' => 'SCSS',
|
||||||
|
'less' => 'Less',
|
||||||
|
'json' => 'JSON',
|
||||||
|
'yaml' => 'YAML',
|
||||||
|
'toml' => 'TOML',
|
||||||
|
'markdown' => 'Markdown',
|
||||||
|
'dockerfile' => 'Dockerfile',
|
||||||
|
'graphql' => 'GraphQL',
|
||||||
|
'solidity' => 'Solidity',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +324,7 @@ function itstudio_sanitize_site_content_settings($input) {
|
|||||||
'text_en' => '',
|
'text_en' => '',
|
||||||
),
|
),
|
||||||
'homepage_services' => array(),
|
'homepage_services' => array(),
|
||||||
|
'homepage_code_snippets' => array(),
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach (array('footer_links' => 8, 'footer_contacts' => 4) as $key => $limit) {
|
foreach (array('footer_links' => 8, 'footer_contacts' => 4) as $key => $limit) {
|
||||||
@@ -181,6 +390,35 @@ function itstudio_sanitize_site_content_settings($input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$languages = itstudio_get_code_animation_languages();
|
||||||
|
$snippet_rows = isset($input['homepage_code_snippets']) && is_array($input['homepage_code_snippets']) ? $input['homepage_code_snippets'] : array();
|
||||||
|
foreach (array_slice($snippet_rows, 0, 12) as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$language = sanitize_key($row['language'] ?? 'plaintext');
|
||||||
|
if (!isset($languages[$language])) {
|
||||||
|
$language = 'plaintext';
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = isset($row['code']) ? (string) wp_unslash($row['code']) : '';
|
||||||
|
$code = str_replace(array("\r\n", "\r", "\0"), array("\n", "\n", ''), $code);
|
||||||
|
if (strlen($code) > 20000) {
|
||||||
|
$code = substr($code, 0, 20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim($code) === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clean['homepage_code_snippets'][] = array(
|
||||||
|
'title' => sanitize_text_field($row['title'] ?? ''),
|
||||||
|
'language' => $language,
|
||||||
|
'code' => $code,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return $clean;
|
return $clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +433,7 @@ function itstudio_register_site_content_settings() {
|
|||||||
'itstudio_site_content_intro',
|
'itstudio_site_content_intro',
|
||||||
__('站点内容', 'itstudio'),
|
__('站点内容', 'itstudio'),
|
||||||
static function () {
|
static function () {
|
||||||
echo '<p>' . esc_html__('管理页脚内容和首页“服务提供”模块。留空的项目不会在前台显示。', 'itstudio') . '</p>';
|
echo '<p>' . esc_html__('管理页脚内容、首页“服务提供”和代码打字动画。留空的项目不会在前台显示。', 'itstudio') . '</p>';
|
||||||
},
|
},
|
||||||
'itstudio-site-content'
|
'itstudio-site-content'
|
||||||
);
|
);
|
||||||
@@ -205,6 +443,7 @@ function itstudio_register_site_content_settings() {
|
|||||||
add_settings_field('itstudio_footer_addresses', __('办公地址', 'itstudio'), 'itstudio_render_footer_addresses_field', 'itstudio-site-content', 'itstudio_site_content_intro');
|
add_settings_field('itstudio_footer_addresses', __('办公地址', 'itstudio'), 'itstudio_render_footer_addresses_field', 'itstudio-site-content', 'itstudio_site_content_intro');
|
||||||
add_settings_field('itstudio_footer_bottom', __('备案与版权', 'itstudio'), 'itstudio_render_footer_bottom_field', 'itstudio-site-content', 'itstudio_site_content_intro');
|
add_settings_field('itstudio_footer_bottom', __('备案与版权', 'itstudio'), 'itstudio_render_footer_bottom_field', 'itstudio-site-content', 'itstudio_site_content_intro');
|
||||||
add_settings_field('itstudio_homepage_services', __('首页服务提供', 'itstudio'), 'itstudio_render_homepage_services_field', 'itstudio-site-content', 'itstudio_site_content_intro');
|
add_settings_field('itstudio_homepage_services', __('首页服务提供', 'itstudio'), 'itstudio_render_homepage_services_field', 'itstudio-site-content', 'itstudio_site_content_intro');
|
||||||
|
add_settings_field('itstudio_homepage_code_snippets', __('首页代码动画', 'itstudio'), 'itstudio_render_homepage_code_snippets_field', 'itstudio-site-content', 'itstudio_site_content_intro');
|
||||||
}
|
}
|
||||||
add_action('admin_init', 'itstudio_register_site_content_settings');
|
add_action('admin_init', 'itstudio_register_site_content_settings');
|
||||||
|
|
||||||
@@ -298,6 +537,58 @@ function itstudio_render_homepage_services_field() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function itstudio_render_homepage_code_snippet_row($index, $row = array()) {
|
||||||
|
$base = 'itstudio_site_content_settings[homepage_code_snippets][' . $index . ']';
|
||||||
|
$language = sanitize_key($row['language'] ?? 'javascript');
|
||||||
|
$languages = itstudio_get_code_animation_languages();
|
||||||
|
if (!isset($languages[$language])) {
|
||||||
|
$language = 'plaintext';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="itstudio-code-snippet-row">
|
||||||
|
<div class="itstudio-code-snippet-toolbar">
|
||||||
|
<input type="text" class="regular-text itstudio-code-snippet-title" name="<?php echo esc_attr($base); ?>[title]" value="<?php echo esc_attr($row['title'] ?? ''); ?>" placeholder="<?php esc_attr_e('程序名称,例如 app.py', 'itstudio'); ?>">
|
||||||
|
<select name="<?php echo esc_attr($base); ?>[language]" aria-label="<?php esc_attr_e('代码语言', 'itstudio'); ?>">
|
||||||
|
<?php foreach ($languages as $value => $label) : ?>
|
||||||
|
<option value="<?php echo esc_attr($value); ?>" <?php selected($language, $value); ?>><?php echo esc_html($label); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<span class="itstudio-code-snippet-actions">
|
||||||
|
<button type="button" class="button itstudio-move-code-snippet-up" aria-label="<?php esc_attr_e('上移程序', 'itstudio'); ?>">↑</button>
|
||||||
|
<button type="button" class="button itstudio-move-code-snippet-down" aria-label="<?php esc_attr_e('下移程序', 'itstudio'); ?>">↓</button>
|
||||||
|
<button type="button" class="button-link-delete itstudio-remove-code-snippet"><?php esc_html_e('删除', 'itstudio'); ?></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<textarea class="large-text code" rows="10" spellcheck="false" name="<?php echo esc_attr($base); ?>[code]" placeholder="<?php esc_attr_e('粘贴需要在首页动画中展示的代码', 'itstudio'); ?>"><?php echo esc_textarea($row['code'] ?? ''); ?></textarea>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function itstudio_render_homepage_code_snippets_field() {
|
||||||
|
$settings = itstudio_get_site_content_settings();
|
||||||
|
$rows = isset($settings['homepage_code_snippets']) && is_array($settings['homepage_code_snippets']) ? array_values($settings['homepage_code_snippets']) : array();
|
||||||
|
?>
|
||||||
|
<p class="description"><?php esc_html_e('最多 12 个程序。首页每次随机播放其中一个;未添加时继续使用主题内置示例。单个程序最多保存 20,000 个字符。', 'itstudio'); ?></p>
|
||||||
|
<div id="itstudio-code-snippets" data-max="12">
|
||||||
|
<?php foreach ($rows as $index => $row) : ?>
|
||||||
|
<?php itstudio_render_homepage_code_snippet_row($index, $row); ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<p><button type="button" class="button button-secondary" id="itstudio-add-code-snippet"><?php esc_html_e('添加程序', 'itstudio'); ?></button></p>
|
||||||
|
<template id="itstudio-code-snippet-template">
|
||||||
|
<?php itstudio_render_homepage_code_snippet_row('__INDEX__'); ?>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.itstudio-code-snippet-row{max-width:900px;margin:0 0 14px;padding:12px;border:1px solid #dcdcde;border-radius:4px;background:#fff}
|
||||||
|
.itstudio-code-snippet-toolbar{display:grid;grid-template-columns:minmax(180px,1fr) minmax(150px,220px) auto;gap:8px;align-items:center;margin-bottom:8px}
|
||||||
|
.itstudio-code-snippet-toolbar .regular-text{width:100%}
|
||||||
|
.itstudio-code-snippet-actions{display:flex;gap:6px;align-items:center;justify-content:flex-end}
|
||||||
|
.itstudio-code-snippet-row textarea{min-height:180px;resize:vertical;font-family:Consolas,Monaco,monospace}
|
||||||
|
@media (max-width:782px){.itstudio-code-snippet-toolbar{grid-template-columns:1fr}.itstudio-code-snippet-actions{justify-content:flex-start}}
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
function itstudio_render_site_content_page() {
|
function itstudio_render_site_content_page() {
|
||||||
if (!current_user_can('manage_options')) {
|
if (!current_user_can('manage_options')) {
|
||||||
return;
|
return;
|
||||||
@@ -334,7 +625,65 @@ function itstudio_site_content_admin_media_script() {
|
|||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var frame;
|
var frame;
|
||||||
|
var snippetsContainer = document.getElementById('itstudio-code-snippets');
|
||||||
|
var snippetTemplate = document.getElementById('itstudio-code-snippet-template');
|
||||||
|
var addSnippetButton = document.getElementById('itstudio-add-code-snippet');
|
||||||
|
|
||||||
|
function refreshCodeSnippetRows() {
|
||||||
|
if (!snippetsContainer) return;
|
||||||
|
var rows = Array.prototype.slice.call(snippetsContainer.querySelectorAll('.itstudio-code-snippet-row'));
|
||||||
|
rows.forEach(function (row, index) {
|
||||||
|
row.querySelectorAll('[name]').forEach(function (field) {
|
||||||
|
field.name = field.name.replace(/\[homepage_code_snippets\]\[[^\]]+\]/, '[homepage_code_snippets][' + index + ']');
|
||||||
|
});
|
||||||
|
var upButton = row.querySelector('.itstudio-move-code-snippet-up');
|
||||||
|
var downButton = row.querySelector('.itstudio-move-code-snippet-down');
|
||||||
|
if (upButton) upButton.disabled = index === 0;
|
||||||
|
if (downButton) downButton.disabled = index === rows.length - 1;
|
||||||
|
});
|
||||||
|
if (addSnippetButton) {
|
||||||
|
addSnippetButton.disabled = rows.length >= Number(snippetsContainer.dataset.max || 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addSnippetButton && snippetsContainer && snippetTemplate) {
|
||||||
|
addSnippetButton.addEventListener('click', function () {
|
||||||
|
var count = snippetsContainer.querySelectorAll('.itstudio-code-snippet-row').length;
|
||||||
|
var max = Number(snippetsContainer.dataset.max || 12);
|
||||||
|
if (count >= max) return;
|
||||||
|
var html = snippetTemplate.innerHTML.replace(/__INDEX__/g, String(count));
|
||||||
|
snippetsContainer.insertAdjacentHTML('beforeend', html);
|
||||||
|
refreshCodeSnippetRows();
|
||||||
|
var rows = snippetsContainer.querySelectorAll('.itstudio-code-snippet-row');
|
||||||
|
var title = rows.length ? rows[rows.length - 1].querySelector('.itstudio-code-snippet-title') : null;
|
||||||
|
if (title) title.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshCodeSnippetRows();
|
||||||
|
|
||||||
document.addEventListener('click', function (event) {
|
document.addEventListener('click', function (event) {
|
||||||
|
var removeSnippetButton = event.target.closest('.itstudio-remove-code-snippet');
|
||||||
|
var moveUpButton = event.target.closest('.itstudio-move-code-snippet-up');
|
||||||
|
var moveDownButton = event.target.closest('.itstudio-move-code-snippet-down');
|
||||||
|
if (removeSnippetButton && snippetsContainer) {
|
||||||
|
removeSnippetButton.closest('.itstudio-code-snippet-row').remove();
|
||||||
|
refreshCodeSnippetRows();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((moveUpButton || moveDownButton) && snippetsContainer) {
|
||||||
|
var row = (moveUpButton || moveDownButton).closest('.itstudio-code-snippet-row');
|
||||||
|
var sibling = moveUpButton ? row.previousElementSibling : row.nextElementSibling;
|
||||||
|
if (sibling) {
|
||||||
|
if (moveUpButton) {
|
||||||
|
snippetsContainer.insertBefore(row, sibling);
|
||||||
|
} else {
|
||||||
|
snippetsContainer.insertBefore(sibling, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshCodeSnippetRows();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var selectButton = event.target.closest('.itstudio-select-service-image');
|
var selectButton = event.target.closest('.itstudio-select-service-image');
|
||||||
var clearButton = event.target.closest('.itstudio-clear-service-image');
|
var clearButton = event.target.closest('.itstudio-clear-service-image');
|
||||||
if (clearButton) {
|
if (clearButton) {
|
||||||
@@ -478,22 +827,19 @@ function itstudio_enqueue_scripts() {
|
|||||||
// 仅在首页加载首页样式
|
// 仅在首页加载首页样式
|
||||||
if (is_front_page() || is_home()) {
|
if (is_front_page() || is_home()) {
|
||||||
wp_enqueue_style('itstudio-front-page', get_template_directory_uri() . '/assets/css/front-page.css', array('itstudio-style'), '2.1.2');
|
wp_enqueue_style('itstudio-front-page', get_template_directory_uri() . '/assets/css/front-page.css', array('itstudio-style'), '2.1.2');
|
||||||
wp_enqueue_script('itstudio-landing-hero-canvas', get_template_directory_uri() . '/assets/js/landing-hero-canvas.js', array(), '1.0.1', true);
|
wp_enqueue_script('itstudio-landing-hero-canvas', get_template_directory_uri() . '/assets/js/landing-hero-canvas.js', array(), '1.1.0', true);
|
||||||
}
|
|
||||||
|
|
||||||
// 仅在工作室介绍页加载(包含 /about fallback)
|
$site_content = itstudio_get_site_content_settings();
|
||||||
$is_about = is_page('about') || is_page_template('page-about.php');
|
$code_snippets = isset($site_content['homepage_code_snippets']) && is_array($site_content['homepage_code_snippets']) ? $site_content['homepage_code_snippets'] : array();
|
||||||
if (!$is_about && is_404()) {
|
wp_localize_script('itstudio-landing-hero-canvas', 'ITStudioLandingHero', array(
|
||||||
global $wp;
|
'snippets' => array_values(array_map(static function ($snippet) {
|
||||||
$request = isset($wp->request) ? trim($wp->request, '/') : '';
|
return array(
|
||||||
$is_about = ($request === 'about');
|
'title' => (string) ($snippet['title'] ?? ''),
|
||||||
}
|
'language' => (string) ($snippet['language'] ?? 'plaintext'),
|
||||||
|
'code' => (string) ($snippet['code'] ?? ''),
|
||||||
if ($is_about) {
|
);
|
||||||
wp_enqueue_style('itstudio-about-hero', get_template_directory_uri() . '/assets/css/about-hero.css', array('itstudio-content'), '2.1.2');
|
}, $code_snippets)),
|
||||||
wp_enqueue_script('itstudio-animejs', get_template_directory_uri() . '/assets/js/vendor/anime.min.js', array(), '3.2.2', true);
|
));
|
||||||
wp_enqueue_script('itstudio-about-hero-waves', get_template_directory_uri() . '/assets/js/hero-waves.js', array('itstudio-animejs'), '1.0.0', true);
|
|
||||||
wp_enqueue_script('itstudio-about-hero', get_template_directory_uri() . '/assets/js/home-hero.js', array('itstudio-animejs'), '1.0.0', true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅在便民服务页加载(包含 /services fallback)
|
// 仅在便民服务页加载(包含 /services fallback)
|
||||||
@@ -509,7 +855,7 @@ function itstudio_enqueue_scripts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scripts
|
// Scripts
|
||||||
wp_enqueue_script('itstudio-theme-toggle', get_template_directory_uri() . '/assets/js/theme-toggle.js', array(), '1.0.0', true);
|
wp_enqueue_script('itstudio-theme-toggle', get_template_directory_uri() . '/assets/js/theme-toggle.js', array(), '1.0.1', true);
|
||||||
wp_enqueue_script('itstudio-lang-toggle', get_template_directory_uri() . '/assets/js/lang-toggle.js', array(), '1.0.0', true);
|
wp_enqueue_script('itstudio-lang-toggle', get_template_directory_uri() . '/assets/js/lang-toggle.js', array(), '1.0.0', true);
|
||||||
wp_enqueue_script('itstudio-main', get_template_directory_uri() . '/assets/js/main.js', array(), '1.0.0', true);
|
wp_enqueue_script('itstudio-main', get_template_directory_uri() . '/assets/js/main.js', array(), '1.0.0', true);
|
||||||
|
|
||||||
@@ -542,22 +888,6 @@ function itstudio_register_sidebars() {
|
|||||||
}
|
}
|
||||||
add_action('widgets_init', 'itstudio_register_sidebars');
|
add_action('widgets_init', 'itstudio_register_sidebars');
|
||||||
|
|
||||||
function itstudio_intro_body_class($classes) {
|
|
||||||
$is_about = is_page('about') || is_page_template('page-about.php');
|
|
||||||
if (!$is_about && is_404()) {
|
|
||||||
global $wp;
|
|
||||||
$request = isset($wp->request) ? trim($wp->request, '/') : '';
|
|
||||||
$is_about = ($request === 'about');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($is_about) {
|
|
||||||
$classes[] = 'intro-about';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $classes;
|
|
||||||
}
|
|
||||||
add_filter('body_class', 'itstudio_intro_body_class');
|
|
||||||
|
|
||||||
function itstudio_archive_document_title($parts) {
|
function itstudio_archive_document_title($parts) {
|
||||||
if (is_post_type_archive('announcement')) {
|
if (is_post_type_archive('announcement')) {
|
||||||
$parts['title'] = '公告通知';
|
$parts['title'] = '公告通知';
|
||||||
@@ -569,57 +899,6 @@ function itstudio_archive_document_title($parts) {
|
|||||||
}
|
}
|
||||||
add_filter('document_title_parts', 'itstudio_archive_document_title', 20);
|
add_filter('document_title_parts', 'itstudio_archive_document_title', 20);
|
||||||
|
|
||||||
// Fallback: render /about even if the page isn't created in WP admin.
|
|
||||||
function itstudio_about_fallback() {
|
|
||||||
if (!is_404()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
global $wp;
|
|
||||||
$request = isset($wp->request) ? trim($wp->request, '/') : '';
|
|
||||||
if ($request !== 'about') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$template = locate_template('page-about.php');
|
|
||||||
if ($template) {
|
|
||||||
global $wp_query;
|
|
||||||
if ($wp_query) {
|
|
||||||
$wp_query->is_404 = false;
|
|
||||||
$wp_query->is_page = true;
|
|
||||||
$wp_query->is_singular = true;
|
|
||||||
$virtual_post = new WP_Post((object) array(
|
|
||||||
'ID' => 0,
|
|
||||||
'post_type' => 'page',
|
|
||||||
'post_parent' => 0,
|
|
||||||
'post_title' => __('工作室介绍', 'itstudio'),
|
|
||||||
'post_status' => 'publish',
|
|
||||||
'post_name' => 'about',
|
|
||||||
'post_content' => '',
|
|
||||||
));
|
|
||||||
$wp_query->post = $virtual_post;
|
|
||||||
$wp_query->posts = array($virtual_post);
|
|
||||||
$wp_query->queried_object = $virtual_post;
|
|
||||||
$wp_query->queried_object_id = 0;
|
|
||||||
$wp_query->post_count = 1;
|
|
||||||
$wp_query->found_posts = 1;
|
|
||||||
$wp_query->max_num_pages = 1;
|
|
||||||
global $post;
|
|
||||||
$post = $virtual_post;
|
|
||||||
setup_postdata($post);
|
|
||||||
}
|
|
||||||
add_filter('document_title_parts', function ($parts) {
|
|
||||||
$parts['title'] = __('工作室介绍', 'itstudio');
|
|
||||||
return $parts;
|
|
||||||
});
|
|
||||||
status_header(200);
|
|
||||||
nocache_headers();
|
|
||||||
include $template;
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_action('template_redirect', 'itstudio_about_fallback');
|
|
||||||
|
|
||||||
// Fallback: render /news via archive.php even if the page isn't created in WP admin.
|
// Fallback: render /news via archive.php even if the page isn't created in WP admin.
|
||||||
function itstudio_news_fallback() {
|
function itstudio_news_fallback() {
|
||||||
if (!is_404()) {
|
if (!is_404()) {
|
||||||
@@ -777,3 +1056,5 @@ function itstudio_join_fallback() {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
add_action('template_redirect', 'itstudio_join_fallback', 9);
|
add_action('template_redirect', 'itstudio_join_fallback', 9);
|
||||||
|
|
||||||
|
show_admin_bar(false);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="landing-hero-content">
|
<div class="landing-hero-content">
|
||||||
<h1 class="landing-hero-title" data-cn="爱特工作室" data-en="IT STUDIO"></h1>
|
<h1 class="landing-hero-title" data-cn="爱特工作室" data-en="IT STUDIO"></h1>
|
||||||
<p class="landing-hero-subtitle" data-cn="中国海洋大学信息技术与工程实践团队" data-en="Technology and Engineering Practice Team at OUC"></p>
|
<p class="landing-hero-subtitle" data-cn="中国海洋大学信息技术与工程实践团队" data-en="Technology and Engineering Practice Team at OUC"></p>
|
||||||
<a class="landing-hero-btn" href="<?php echo esc_url(home_url('/about')); ?>" data-cn="了解更多" data-en="Learn More"></a>
|
<a class="landing-hero-btn" href="https://honor.itouc.cn/" target="_blank" rel="noopener noreferrer" data-cn="了解更多" data-en="Learn More"></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
/* Template Name: IT Studio Introduction */
|
|
||||||
get_header();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<main class="site-main">
|
|
||||||
<canvas class="hero-waves" aria-hidden="true"></canvas>
|
|
||||||
<section class="hero-section">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="theme-toggle about-theme-toggle"
|
|
||||||
aria-label="Toggle Theme"
|
|
||||||
data-cn-aria-label="切换主题"
|
|
||||||
data-en-aria-label="Toggle Theme">
|
|
||||||
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
||||||
<circle cx="12" cy="12" r="5"></circle>
|
|
||||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
||||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
||||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
||||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
||||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
||||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
||||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
||||||
</svg>
|
|
||||||
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="container">
|
|
||||||
<div class="hero-content">
|
|
||||||
<h1 class="hero-title">
|
|
||||||
<span class="hero-title-svg" aria-hidden="true"></span>
|
|
||||||
<span class="hero-title-text" data-cn="爱特工作室" data-en="IT STUDIO"></span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="hero-scroll-indicator" aria-hidden="true">
|
|
||||||
<span class="scroll-arrow"></span>
|
|
||||||
<span class="scroll-text" data-cn="向下滑动开始" data-en="Scroll to begin"></span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
|
||||||
@@ -25,8 +25,12 @@ $is_progress_query_open = !empty($join_runtime['is_query_open']);
|
|||||||
|
|
||||||
$current_label_cn = isset($current_stage['label_cn']) ? (string) $current_stage['label_cn'] : '当前未在招新时段';
|
$current_label_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_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_cn = isset($current_stage['overlay_range_cn'])
|
||||||
$current_range_en = isset($current_stage['range_en']) ? (string) $current_stage['range_en'] : 'Please check later updates';
|
? (string) $current_stage['overlay_range_cn']
|
||||||
|
: (isset($current_stage['range_cn']) ? (string) $current_stage['range_cn'] : '请关注后续通知');
|
||||||
|
$current_range_en = isset($current_stage['overlay_range_en'])
|
||||||
|
? (string) $current_stage['overlay_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_cn = trim((string) ($current_stage['location_cn'] ?? ''));
|
||||||
$current_location_en = trim((string) ($current_stage['location_en'] ?? ''));
|
$current_location_en = trim((string) ($current_stage['location_en'] ?? ''));
|
||||||
if ($current_location_en === '') {
|
if ($current_location_en === '') {
|
||||||
@@ -316,9 +320,9 @@ if ($is_post_request && function_exists('itstudio_join_detect_form_submission_st
|
|||||||
type="text"
|
type="text"
|
||||||
name="join_query_identity"
|
name="join_query_identity"
|
||||||
value="<?php echo esc_attr((string) ($progress_lookup['identity'] ?? '')); ?>"
|
value="<?php echo esc_attr((string) ($progress_lookup['identity'] ?? '')); ?>"
|
||||||
data-cn-placeholder="请输入中文姓名 / QQ / 邮箱 / 学号"
|
data-cn-placeholder="请输入中文姓名 / QQ / 邮箱 / 学号 / 手机号"
|
||||||
data-en-placeholder="Enter Name / QQ / Email / Student ID"
|
data-en-placeholder="Enter Name / QQ / Email / Student ID / Phone"
|
||||||
placeholder="请输入中文姓名 / QQ / 邮箱 / 学号"
|
placeholder="请输入中文姓名 / QQ / 邮箱 / 学号 / 手机号"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user