diff --git a/README.md b/README.md index 7571d31..02b9c97 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@
- +

爱特工作室总站


-
+
@@ -35,7 +35,7 @@ 1. 克隆本仓库到 WordPress 主题目录: ```bash cd wp-content/themes -git clone https://github.com/itstudio-2002/ITStudioMainSite.git +git clone https://git.itouc.cn/ITStudio_OUC/ITStudioMainSite.git ``` 2. 在 WordPress 后台激活主题: diff --git a/plugins/itstudio-content/includes/archive-search.php b/plugins/itstudio-content/includes/archive-search.php new file mode 100644 index 0000000..9983175 --- /dev/null +++ b/plugins/itstudio-content/includes/archive-search.php @@ -0,0 +1,67 @@ +get('itstudio_archive_extend_search')) { + return false; + } + + $keyword = trim((string) $query->get('itstudio_archive_keyword')); + return $keyword !== ''; +} + +function itstudio_archive_extended_search_join($join, $query) { + if (!itstudio_archive_extended_search_enabled($query)) { + return $join; + } + + global $wpdb; + if (strpos($join, $wpdb->users) !== false) { + return $join; + } + + return $join . " LEFT JOIN {$wpdb->users} ON ({$wpdb->posts}.post_author = {$wpdb->users}.ID) "; +} +add_filter('posts_join', 'itstudio_archive_extended_search_join', 10, 2); + +function itstudio_archive_extended_search_where($where, $query) { + if (!itstudio_archive_extended_search_enabled($query)) { + return $where; + } + + global $wpdb; + $keyword = trim((string) $query->get('itstudio_archive_keyword')); + $like = '%' . $wpdb->esc_like($keyword) . '%'; + + $search_sql = $wpdb->prepare( + " AND ( + {$wpdb->posts}.post_title LIKE %s + OR {$wpdb->posts}.post_excerpt LIKE %s + OR {$wpdb->posts}.post_content LIKE %s + OR {$wpdb->users}.display_name LIKE %s + OR {$wpdb->users}.user_login LIKE %s + OR {$wpdb->users}.user_nicename LIKE %s + )", + $like, + $like, + $like, + $like, + $like, + $like + ); + + return $where . $search_sql; +} +add_filter('posts_where', 'itstudio_archive_extended_search_where', 10, 2); diff --git a/plugins/itstudio-content/includes/post-types.php b/plugins/itstudio-content/includes/post-types.php new file mode 100644 index 0000000..1512ef2 --- /dev/null +++ b/plugins/itstudio-content/includes/post-types.php @@ -0,0 +1,145 @@ + array( + 'name' => __('公告', 'itstudio'), + 'singular_name' => __('公告', 'itstudio'), + ), + 'public' => true, + 'has_archive' => true, + 'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'), + 'taxonomies' => array('post_tag'), + 'menu_icon' => 'dashicons-megaphone', + 'show_in_rest' => true, + )); + + register_post_type('news', array( + 'labels' => array( + 'name' => __('新闻', 'itstudio'), + 'singular_name' => __('新闻', 'itstudio'), + ), + 'public' => true, + 'has_archive' => 'news', + 'rewrite' => array( + 'slug' => 'news', + 'with_front' => false, + ), + 'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'), + 'taxonomies' => array('post_tag'), + 'menu_icon' => 'dashicons-media-document', + 'show_in_rest' => true, + )); + + register_taxonomy_for_object_type('post_tag', 'announcement'); + register_taxonomy_for_object_type('post_tag', 'news'); +} +add_action('init', 'itstudio_content_register_post_types'); + +/** + * 内容权重 ACF 字段组:数值越大文章在侧栏排序越靠前。 + * 招新文章标记(itstudio_is_recruitment_article)不在此插件,由 itstudio-recruitment 负责。 + */ +function itstudio_content_register_acf_fields() { + if (!function_exists('acf_add_local_field_group')) { + return; + } + + acf_add_local_field_group(array( + 'key' => 'group_itstudio_content_priority', + 'title' => '内容权重', + 'fields' => array( + array( + 'key' => 'field_itstudio_weight', + 'label' => '权重', + 'name' => 'itstudio_weight', + 'type' => 'number', + 'instructions' => '数值越大,文章在侧栏排序越靠前。', + 'required' => 0, + 'default_value' => 0, + 'min' => 0, + 'step' => 1, + ), + ), + 'location' => array( + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'post', + ), + ), + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'announcement', + ), + ), + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'news', + ), + ), + ), + 'position' => 'side', + 'style' => 'default', + 'active' => true, + 'show_in_rest' => 1, + )); +} +add_action('acf/init', 'itstudio_content_register_acf_fields'); + +/** + * 新闻/公告在只有日期格式时补上时:分,提升列表时间精度。 + */ +function itstudio_expand_news_notice_date_precision($the_date, $format, $post) { + $post_obj = $post instanceof WP_Post ? $post : get_post($post); + if (!($post_obj instanceof WP_Post)) { + return $the_date; + } + + if (!in_array((string) $post_obj->post_type, array('announcement', 'news'), true)) { + return $the_date; + } + + $format = (string) $format; + if ($format === '') { + $format = (string) get_option('date_format'); + } + + $lower = strtolower($format); + if (in_array($lower, array('c', 'r', 'u'), true)) { + return $the_date; + } + + // Already has time precision. + if (preg_match('/[HhGgis]/', $format)) { + return $the_date; + } + + $timestamp = function_exists('get_post_timestamp') + ? get_post_timestamp($post_obj, 'date') + : false; + if (!$timestamp) { + return $the_date; + } + + $format_with_time = rtrim($format) . ' H:i'; + return wp_date($format_with_time, (int) $timestamp, wp_timezone()); +} +add_filter('get_the_date', 'itstudio_expand_news_notice_date_precision', 10, 3); diff --git a/plugins/itstudio-content/includes/post-views.php b/plugins/itstudio-content/includes/post-views.php new file mode 100644 index 0000000..4394b55 --- /dev/null +++ b/plugins/itstudio-content/includes/post-views.php @@ -0,0 +1,203 @@ + $timestamp) { + $post_id = (int) $post_id; + $timestamp = (int) $timestamp; + if ($post_id > 0 && $timestamp > 0) { + $clean[(string) $post_id] = $timestamp; + } + } + + return $clean; +} + +function itstudio_write_view_cookie_map($map, $window_seconds) { + if (!headers_sent()) { + $cookie_name = itstudio_get_view_cookie_name(); + $expire_at = time() + max(DAY_IN_SECONDS, (int) $window_seconds * 2); + $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; + $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; + $secure = is_ssl(); + $http_only = true; + $encoded = wp_json_encode($map); + if (!is_string($encoded)) { + return; + } + + if (PHP_VERSION_ID >= 70300) { + setcookie($cookie_name, $encoded, array( + 'expires' => $expire_at, + 'path' => $path, + 'domain' => $domain, + 'secure' => $secure, + 'httponly' => $http_only, + 'samesite' => 'Lax', + )); + } else { + setcookie($cookie_name, $encoded, $expire_at, $path, $domain, $secure, $http_only); + } + + $_COOKIE[$cookie_name] = $encoded; + } +} + +function itstudio_should_count_post_view($post_id) { + $post_id = (int) $post_id; + if ($post_id <= 0) { + return false; + } + + if (itstudio_is_probably_bot_request()) { + return false; + } + + $window_seconds = (int) apply_filters('itstudio_post_view_window_seconds', 12 * HOUR_IN_SECONDS); + $window_seconds = max(60, $window_seconds); + $max_entries = (int) apply_filters('itstudio_post_view_cookie_max_entries', 120); + $max_entries = max(10, $max_entries); + + $now = time(); + $key = (string) $post_id; + $map = itstudio_read_view_cookie_map(); + + foreach ($map as $id => $timestamp) { + if (($now - (int) $timestamp) > $window_seconds) { + unset($map[$id]); + } + } + + if (isset($map[$key]) && ($now - (int) $map[$key]) < $window_seconds) { + return false; + } + + $map[$key] = $now; + if (count($map) > $max_entries) { + asort($map, SORT_NUMERIC); + $map = array_slice($map, -$max_entries, null, true); + } + + itstudio_write_view_cookie_map($map, $window_seconds); + + return true; +} + +function itstudio_track_post_views() { + if (is_admin() || wp_doing_ajax() || wp_doing_cron()) { + return; + } + + if (!is_singular(array('post', 'announcement', 'news')) || is_preview() || is_feed() || is_trackback() || is_embed()) { + return; + } + + if (!isset($_SERVER['REQUEST_METHOD']) || strtoupper((string) $_SERVER['REQUEST_METHOD']) !== 'GET') { + return; + } + + $post_id = (int) get_queried_object_id(); + if ($post_id <= 0) { + return; + } + + if (!itstudio_should_count_post_view($post_id)) { + return; + } + + $views = itstudio_get_post_views($post_id) + 1; + update_post_meta($post_id, 'post_views_count', $views); + update_post_meta($post_id, 'itstudio_views', $views); +} +add_action('template_redirect', 'itstudio_track_post_views', 20); diff --git a/plugins/itstudio-content/includes/template-helpers.php b/plugins/itstudio-content/includes/template-helpers.php new file mode 100644 index 0000000..df0337e --- /dev/null +++ b/plugins/itstudio-content/includes/template-helpers.php @@ -0,0 +1,115 @@ + $term_obj->slug, + ), + $archive_url + ); +} diff --git a/plugins/itstudio-content/itstudio-content.php b/plugins/itstudio-content/itstudio-content.php new file mode 100644 index 0000000..e6b383f --- /dev/null +++ b/plugins/itstudio-content/itstudio-content.php @@ -0,0 +1,42 @@ + __('Primary Menu', 'itstudio'), - )); - - load_theme_textdomain('itstudio', get_template_directory() . '/languages'); +if (!defined('ABSPATH')) { + exit; } -add_action('after_setup_theme', 'itstudio_theme_setup'); - -add_filter('comments_open', '__return_false', 20, 2); -add_filter('pings_open', '__return_false', 20, 2); -add_filter('comments_array', '__return_empty_array', 10, 2); - -function itstudio_disable_comments_post_types() { - $post_types = array('post', 'page', 'announcement', 'news', 'service'); - foreach ($post_types as $post_type) { - if (post_type_supports($post_type, 'comments')) { - remove_post_type_support($post_type, 'comments'); - } - if (post_type_supports($post_type, 'trackbacks')) { - remove_post_type_support($post_type, 'trackbacks'); - } - } -} -add_action('init', 'itstudio_disable_comments_post_types', 100); - -function itstudio_hide_comments_menu() { - remove_menu_page('edit-comments.php'); -} -add_action('admin_menu', 'itstudio_hide_comments_menu', 999); - -function itstudio_hide_admin_bar_comments() { - remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60); -} -add_action('init', 'itstudio_hide_admin_bar_comments'); - -function itstudio_redirect_comments_admin_pages() { - global $pagenow; - if ($pagenow === 'edit-comments.php' || $pagenow === 'comment.php') { - wp_safe_redirect(admin_url()); - exit; - } -} -add_action('admin_init', 'itstudio_redirect_comments_admin_pages'); - -function itstudio_remove_comments_dashboard_widget() { - remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal'); -} -add_action('wp_dashboard_setup', 'itstudio_remove_comments_dashboard_widget'); - -function itstudio_apply_site_identity() { - $site_name = '爱特工作室'; - $site_tagline = '爱特工作室官方网站'; - - if (get_option('blogname') !== $site_name) { - update_option('blogname', $site_name); - } - - if (get_option('blogdescription') !== $site_tagline) { - update_option('blogdescription', $site_tagline); - } -} -add_action('init', 'itstudio_apply_site_identity', 20); - -function itstudio_output_favicon() { - $favicon_url = get_template_directory_uri() . '/resources/it_logo_2024.svg'; - echo '' . "\n"; - echo '' . "\n"; -} - -function itstudio_output_theme_bootstrap_script() { - echo '' . "\n"; - echo '' . "\n"; -} - -function itstudio_output_lang_bootstrap_script() { - echo '' . "\n"; - echo '' . "\n"; -} -function itstudio_disable_default_site_icon() { - remove_action('wp_head', 'wp_site_icon', 99); - remove_action('admin_head', 'wp_site_icon', 99); - remove_action('login_head', 'wp_site_icon', 99); -} -add_action('init', 'itstudio_disable_default_site_icon'); -add_action('wp_head', 'itstudio_output_theme_bootstrap_script', 0); -add_action('wp_head', 'itstudio_output_lang_bootstrap_script', 1); -add_action('wp_head', 'itstudio_output_favicon', 1); -add_action('admin_head', 'itstudio_output_favicon', 1); -add_action('login_head', 'itstudio_output_favicon', 1); - -function itstudio_enqueue_scripts() { - // 基础样式 (Style.css) - wp_enqueue_style('itstudio-style', get_stylesheet_uri(), array(), '2.1.2'); - - wp_enqueue_style('itstudio-header', get_template_directory_uri() . '/assets/css/header.css', array('itstudio-style'), '2.1.2'); - wp_enqueue_style('itstudio-footer', get_template_directory_uri() . '/assets/css/footer.css', array('itstudio-style'), '2.1.2'); - wp_enqueue_style('itstudio-content', get_template_directory_uri() . '/assets/css/content.css', array('itstudio-style'), '2.1.2'); - - // 仅在首页加载首页样式 - 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_script('itstudio-landing-hero-canvas', get_template_directory_uri() . '/assets/js/landing-hero-canvas.js', array(), '1.0.0', true); - } - - // 仅在工作室介绍页加载(包含 /about fallback) - $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) { - wp_enqueue_style('itstudio-about-hero', get_template_directory_uri() . '/assets/css/about-hero.css', array('itstudio-content'), '2.1.2'); - 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) - $is_services = is_page('services') || is_page_template('page-services.php'); - if (!$is_services && is_404()) { - global $wp; - $request = isset($wp->request) ? trim($wp->request, '/') : ''; - $is_services = ($request === 'services'); - } - - if ($is_services) { - wp_enqueue_style('itstudio-services-page', get_template_directory_uri() . '/assets/css/services-page.css', array('itstudio-content'), '1.0.0'); - } - - // 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-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); - - if (is_singular(array('post', 'announcement', 'news'))) { - wp_enqueue_script('itstudio-single-title-fit', get_template_directory_uri() . '/assets/js/single-title-fit.js', array(), '1.0.0', true); - } -} -add_action('wp_enqueue_scripts', 'itstudio_enqueue_scripts'); - -function itstudio_expand_news_notice_date_precision($the_date, $format, $post) { - $post_obj = $post instanceof WP_Post ? $post : get_post($post); - if (!($post_obj instanceof WP_Post)) { - return $the_date; - } - - if (!in_array((string) $post_obj->post_type, array('announcement', 'news'), true)) { - return $the_date; - } - - $format = (string) $format; - if ($format === '') { - $format = (string) get_option('date_format'); - } - - $lower = strtolower($format); - if (in_array($lower, array('c', 'r', 'u'), true)) { - return $the_date; - } - - // Already has time precision. - if (preg_match('/[HhGgis]/', $format)) { - return $the_date; - } - - $timestamp = function_exists('get_post_timestamp') - ? get_post_timestamp($post_obj, 'date') - : false; - if (!$timestamp) { - return $the_date; - } - - $format_with_time = rtrim($format) . ' H:i'; - return wp_date($format_with_time, (int) $timestamp, wp_timezone()); -} -add_filter('get_the_date', 'itstudio_expand_news_notice_date_precision', 10, 3); - -function itstudio_register_sidebars() { - register_sidebar(array( - 'name' => __('Footer - Column 1', 'itstudio'), - 'id' => 'footer-1', - 'description' => __('Footer widget area 1', 'itstudio'), - 'before_widget' => '', - 'before_title' => '

', - 'after_title' => '

', - )); - - register_sidebar(array( - 'name' => __('Footer - Column 2', 'itstudio'), - 'id' => 'footer-2', - 'description' => __('Footer widget area 2', 'itstudio'), - 'before_widget' => '', - 'before_title' => '

', - 'after_title' => '

', - )); -} -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_custom_post_types() { - register_post_type('announcement', array( - 'labels' => array( - 'name' => __('公告', 'itstudio'), - 'singular_name' => __('公告', 'itstudio'), - ), - 'public' => true, - 'has_archive' => true, - 'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'), - 'taxonomies' => array('post_tag'), - 'menu_icon' => 'dashicons-megaphone', - 'show_in_rest' => true, - )); - - register_post_type('news', array( - 'labels' => array( - 'name' => __('新闻', 'itstudio'), - 'singular_name' => __('新闻', 'itstudio'), - ), - 'public' => true, - 'has_archive' => 'news', - 'rewrite' => array( - 'slug' => 'news', - 'with_front' => false, - ), - 'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'), - 'taxonomies' => array('post_tag'), - 'menu_icon' => 'dashicons-media-document', - 'show_in_rest' => true, - )); - - register_taxonomy('service_category', array('service'), array( - 'labels' => array( - 'name' => __('服务分类', 'itstudio'), - 'singular_name' => __('服务分类', 'itstudio'), - ), - 'hierarchical' => true, - 'public' => true, - 'show_ui' => true, - 'show_admin_column' => false, - 'show_in_rest' => true, - 'rewrite' => array( - 'slug' => 'service-category', - 'with_front' => false, - ), - )); - - register_post_type('service', array( - 'labels' => array( - 'name' => __('便民服务', 'itstudio'), - 'singular_name' => __('便民服务', 'itstudio'), - 'menu_name' => __('便民服务', 'itstudio'), - 'add_new_item' => __('新增便民服务', 'itstudio'), - 'edit_item' => __('编辑便民服务', 'itstudio'), - ), - 'public' => true, - 'show_ui' => true, - 'show_in_menu' => true, - 'show_in_nav_menus' => false, - 'exclude_from_search' => true, - 'publicly_queryable' => true, - 'has_archive' => false, - 'rewrite' => array( - 'slug' => 'service', - 'with_front' => false, - ), - 'supports' => array('title', 'excerpt', 'thumbnail', 'page-attributes'), - 'taxonomies' => array('service_category'), - 'menu_icon' => 'dashicons-admin-tools', - 'show_in_rest' => true, - )); - - register_taxonomy_for_object_type('post_tag', 'announcement'); - register_taxonomy_for_object_type('post_tag', 'news'); -} -add_action('init', 'itstudio_custom_post_types'); - -function itstudio_register_acf_fields() { - if (!function_exists('acf_add_local_field_group')) { - return; - } - - acf_add_local_field_group(array( - 'key' => 'group_itstudio_content_priority', - 'title' => '内容权重', - 'fields' => array( - array( - 'key' => 'field_itstudio_weight', - 'label' => '权重', - 'name' => 'itstudio_weight', - 'type' => 'number', - 'instructions' => '数值越大,文章在侧栏排序越靠前。', - 'required' => 0, - 'default_value' => 0, - 'min' => 0, - 'step' => 1, - ), - ), - 'location' => array( - array( - array( - 'param' => 'post_type', - 'operator' => '==', - 'value' => 'post', - ), - ), - array( - array( - 'param' => 'post_type', - 'operator' => '==', - 'value' => 'announcement', - ), - ), - array( - array( - 'param' => 'post_type', - 'operator' => '==', - 'value' => 'news', - ), - ), - ), - 'position' => 'side', - 'style' => 'default', - 'active' => true, - 'show_in_rest' => 1, - )); - - acf_add_local_field_group(array( - 'key' => 'group_itstudio_recruitment_article', - 'title' => '招新文章标记', - 'fields' => array( - array( - 'key' => 'field_itstudio_is_recruitment_article', - 'label' => '是否为招新文章', - 'name' => 'itstudio_is_recruitment_article', - 'type' => 'true_false', - 'instructions' => '勾选后,该文章会在“加入我们”页面顶部新闻条中显示(按发布时间排序)。', - 'required' => 0, - 'default_value' => 0, - 'ui' => 1, - 'ui_on_text' => '是', - 'ui_off_text' => '否', - ), - ), - 'location' => array( - array( - array( - 'param' => 'post_type', - 'operator' => '==', - 'value' => 'announcement', - ), - ), - array( - array( - 'param' => 'post_type', - 'operator' => '==', - 'value' => 'news', - ), - ), - array( - array( - 'param' => 'post_type', - 'operator' => '==', - 'value' => 'post', - ), - ), - ), - 'position' => 'side', - 'style' => 'default', - 'active' => true, - 'show_in_rest' => 1, - )); -} -add_action('acf/init', 'itstudio_register_acf_fields'); - -function itstudio_get_recruitment_article_meta_key() { - return 'itstudio_is_recruitment_article'; -} - -function itstudio_normalize_recruitment_flag($value) { - if (is_bool($value)) { - return $value; - } - - if (is_numeric($value)) { - return ((int) $value) > 0; - } - - $value = strtolower(trim((string) $value)); - return in_array($value, array('1', 'true', 'yes', 'on', 'y'), true); -} - -function itstudio_is_recruitment_article($post_id) { - $post_id = (int) $post_id; - if ($post_id <= 0) { - return false; - } - - $meta_key = itstudio_get_recruitment_article_meta_key(); - $meta_keys = array( - $meta_key, - '_itstudio_is_recruitment_article', - 'itstudio_join_article', - 'join_recruitment_article', - 'is_recruitment_article', - ); - - foreach ($meta_keys as $key) { - $raw = get_post_meta($post_id, $key, true); - if ($raw !== '' && $raw !== null && itstudio_normalize_recruitment_flag($raw)) { - return true; - } - } - - if (function_exists('get_field')) { - $acf_keys = array( - 'itstudio_is_recruitment_article', - 'is_recruitment_article', - 'itstudio_join_article', - ); - foreach ($acf_keys as $acf_key) { - $raw = get_field($acf_key, $post_id, false); - if ($raw !== '' && $raw !== null && itstudio_normalize_recruitment_flag($raw)) { - return true; - } - } - } - - return false; -} - -function itstudio_add_recruitment_meta_boxes() { - $screens = array('announcement', 'news', 'post'); - foreach ($screens as $screen) { - add_meta_box( - 'itstudio_recruitment_flag', - __('招新文章', 'itstudio'), - 'itstudio_render_recruitment_meta_box', - $screen, - 'side', - 'high' - ); - } -} -add_action('add_meta_boxes', 'itstudio_add_recruitment_meta_boxes'); - -function itstudio_render_recruitment_meta_box($post) { - $meta_key = itstudio_get_recruitment_article_meta_key(); - $checked = itstudio_is_recruitment_article((int) $post->ID); - wp_nonce_field('itstudio_save_recruitment_meta', 'itstudio_recruitment_meta_nonce'); - ?> -

- -

-

- -

- - format('Y'); - $display_year = isset($join_runtime['recruitment_year']) && is_numeric($join_runtime['recruitment_year']) - ? (int) $join_runtime['recruitment_year'] - : $now_year; - - $season_start_ts = null; - $season_end_ts = null; - $stages = isset($join_runtime['stages']) && is_array($join_runtime['stages']) ? $join_runtime['stages'] : array(); - foreach ($stages as $stage) { - if (!is_array($stage)) { - continue; - } - $start_ts = isset($stage['start_ts']) && is_numeric($stage['start_ts']) ? (int) $stage['start_ts'] : null; - $end_ts = isset($stage['end_ts']) && is_numeric($stage['end_ts']) ? (int) $stage['end_ts'] : null; - if ($start_ts !== null && ($season_start_ts === null || $start_ts < $season_start_ts)) { - $season_start_ts = $start_ts; - } - if ($end_ts !== null && ($season_end_ts === null || $end_ts > $season_end_ts)) { - $season_end_ts = $end_ts; - } - } - - $now_ts = (int) (isset($join_runtime['now_ts']) && is_numeric($join_runtime['now_ts']) ? $join_runtime['now_ts'] : ((int) $now->format('U') * 1000)); - if ($season_start_ts !== null && $now_ts < $season_start_ts && $display_year > $now_year) { - // 下年招新未开始:继续展示本年招新资讯 - $display_year = $now_year; - } - - $query = new WP_Query(array( - 'post_type' => array('announcement', 'news', 'post'), - 'post_status' => 'publish', - 'posts_per_page' => 80, - 'orderby' => 'date', - 'order' => 'DESC', - 'no_found_rows' => true, - 'ignore_sticky_posts' => true, - 'date_query' => array( - array( - 'year' => $display_year, - ), - ), - )); - - $items = array(); - if ($query->have_posts()) { - foreach ($query->posts as $post) { - if (count($items) >= $limit) { - break; - } - - $post_id = (int) $post->ID; - if (!itstudio_is_recruitment_article($post_id)) { - continue; - } - - $title = trim((string) get_the_title($post_id)); - $url = (string) get_permalink($post_id); - if ($title === '' || $url === '') { - continue; - } - - $excerpt = function_exists('itstudio_get_post_excerpt_chars') - ? itstudio_get_post_excerpt_chars($post_id, 72) - : wp_html_excerpt(trim(wp_strip_all_tags((string) get_post_field('post_excerpt', $post_id))), 72, '...'); - - if ($excerpt === '') { - $excerpt = '...'; - } - - $author_id = (int) get_post_field('post_author', $post_id); - $author = trim((string) get_the_author_meta('display_name', $author_id)); - if ($author === '') { - $author = 'Unknown'; - } - - $items[] = array( - 'id' => $post_id, - 'title' => $title, - 'excerpt' => $excerpt, - 'url' => $url, - 'date' => get_the_date('Y-m-d H:i', $post_id), - 'date_iso' => get_the_date('c', $post_id), - 'author' => $author, - 'type' => get_post_type($post_id), - ); - } - } - wp_reset_postdata(); - - return array( - 'display_year' => $display_year, - 'items' => $items, - ); -} - -function itstudio_get_service_url_meta_key() { - return '_itstudio_service_url'; -} - -function itstudio_get_service_title_cn_meta_key() { - return '_itstudio_service_title_cn'; -} - -function itstudio_get_service_title_en_meta_key() { - return '_itstudio_service_title_en'; -} - -function itstudio_get_service_excerpt_cn_meta_key() { - return '_itstudio_service_excerpt_cn'; -} - -function itstudio_get_service_excerpt_en_meta_key() { - return '_itstudio_service_excerpt_en'; -} - -function itstudio_get_service_campus_only_meta_key() { - return '_itstudio_service_campus_only'; -} - -function itstudio_get_service_category_name_cn_meta_key() { - return 'itstudio_service_category_name_cn'; -} - -function itstudio_get_service_category_name_en_meta_key() { - return 'itstudio_service_category_name_en'; -} - -function itstudio_get_service_category_i18n_labels($term) { - if (!$term || is_wp_error($term)) { - return array( - 'cn' => '未分类', - 'en' => 'Uncategorized', - ); - } - - $term_id = (int) $term->term_id; - $name_cn = trim((string) get_term_meta($term_id, itstudio_get_service_category_name_cn_meta_key(), true)); - $name_en = trim((string) get_term_meta($term_id, itstudio_get_service_category_name_en_meta_key(), true)); - - if ($name_cn === '') { - $name_cn = (string) $term->name; - } - if ($name_en === '') { - $name_en = $name_cn; - } - - return array( - 'cn' => $name_cn, - 'en' => $name_en, - ); -} - -function itstudio_get_service_i18n_content($service_id, $excerpt_limit = 90) { - $service_id = (int) $service_id; - if ($service_id <= 0) { - return array( - 'title_cn' => '', - 'title_en' => '', - 'excerpt_cn' => '', - 'excerpt_en' => '', - ); - } - - $title_cn = trim((string) get_post_meta($service_id, itstudio_get_service_title_cn_meta_key(), true)); - $title_en = trim((string) get_post_meta($service_id, itstudio_get_service_title_en_meta_key(), true)); - $excerpt_cn = trim((string) get_post_meta($service_id, itstudio_get_service_excerpt_cn_meta_key(), true)); - $excerpt_en = trim((string) get_post_meta($service_id, itstudio_get_service_excerpt_en_meta_key(), true)); - - $fallback_title = trim((string) get_the_title($service_id)); - if ($fallback_title === '') { - $fallback_title = '未命名服务'; - } - - $fallback_excerpt = function_exists('itstudio_get_post_excerpt_chars') - ? itstudio_get_post_excerpt_chars($service_id, $excerpt_limit) - : wp_html_excerpt(wp_strip_all_tags((string) get_post_field('post_excerpt', $service_id)), $excerpt_limit, '...'); - $fallback_excerpt = trim((string) $fallback_excerpt); - if ($fallback_excerpt === '') { - $fallback_excerpt = '暂无简介'; - } - - if ($title_cn === '') { - $title_cn = $fallback_title; - } - if ($title_en === '') { - $title_en = $title_cn; - } - if ($excerpt_cn === '') { - $excerpt_cn = $fallback_excerpt; - } - if ($excerpt_en === '') { - $excerpt_en = $excerpt_cn; - } - - return array( - 'title_cn' => $title_cn, - 'title_en' => $title_en, - 'excerpt_cn' => $excerpt_cn, - 'excerpt_en' => $excerpt_en, - ); -} - -function itstudio_add_service_meta_boxes() { - add_meta_box( - 'itstudio_service_link', - __('服务双语与链接', 'itstudio'), - 'itstudio_render_service_link_meta_box', - 'service', - 'normal', - 'high' - ); -} -add_action('add_meta_boxes', 'itstudio_add_service_meta_boxes'); - -function itstudio_render_service_link_meta_box($post) { - $service_url = (string) get_post_meta($post->ID, itstudio_get_service_url_meta_key(), true); - $title_cn = (string) get_post_meta($post->ID, itstudio_get_service_title_cn_meta_key(), true); - $title_en = (string) get_post_meta($post->ID, itstudio_get_service_title_en_meta_key(), true); - $excerpt_cn = (string) get_post_meta($post->ID, itstudio_get_service_excerpt_cn_meta_key(), true); - $excerpt_en = (string) get_post_meta($post->ID, itstudio_get_service_excerpt_en_meta_key(), true); - $is_campus_only = itstudio_is_service_campus_only($post->ID); - wp_nonce_field('itstudio_save_service_meta', 'itstudio_service_meta_nonce'); - ?> -

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

- trim($title_cn), - itstudio_get_service_title_en_meta_key() => trim($title_en), - itstudio_get_service_excerpt_cn_meta_key() => trim($excerpt_cn), - itstudio_get_service_excerpt_en_meta_key() => trim($excerpt_en), - ); - - foreach ($meta_updates as $meta_key => $value) { - if ($value === '') { - delete_post_meta($post_id, $meta_key); - } else { - update_post_meta($post_id, $meta_key, $value); - } - } - - $campus_only_meta_key = itstudio_get_service_campus_only_meta_key(); - $is_campus_only = isset($_POST['itstudio_service_campus_only']) && (string) wp_unslash($_POST['itstudio_service_campus_only']) === '1'; - if ($is_campus_only) { - update_post_meta($post_id, $campus_only_meta_key, '1'); - } else { - delete_post_meta($post_id, $campus_only_meta_key); - } - - $url_meta_key = itstudio_get_service_url_meta_key(); - $raw_url = isset($_POST['itstudio_service_url']) ? trim((string) wp_unslash($_POST['itstudio_service_url'])) : ''; - - if ($raw_url === '') { - delete_post_meta($post_id, $url_meta_key); - return; - } - - if (strpos($raw_url, '/') === 0) { - $raw_url = home_url($raw_url); - } - - $service_url = esc_url_raw($raw_url); - if ($service_url === '') { - delete_post_meta($post_id, $url_meta_key); - return; - } - - update_post_meta($post_id, $url_meta_key, $service_url); -} -add_action('save_post_service', 'itstudio_save_service_meta'); - -function itstudio_service_category_add_fields($taxonomy) { - wp_nonce_field('itstudio_save_service_category_meta', 'itstudio_service_category_meta_nonce'); - ?> -
- - -
-
- - -
- term_id; - $name_cn = (string) get_term_meta($term_id, itstudio_get_service_category_name_cn_meta_key(), true); - $name_en = (string) get_term_meta($term_id, itstudio_get_service_category_name_en_meta_key(), true); - wp_nonce_field('itstudio_save_service_category_meta', 'itstudio_service_category_meta_nonce'); - ?> - - - - - - - - - trim($name_cn), - itstudio_get_service_category_name_en_meta_key() => trim($name_en), - ); - - foreach ($term_meta as $meta_key => $value) { - if ($value === '') { - delete_term_meta($term_id, $meta_key); - } else { - update_term_meta($term_id, $meta_key, $value); - } - } -} -add_action('created_service_category', 'itstudio_save_service_category_meta'); -add_action('edited_service_category', 'itstudio_save_service_category_meta'); - -function itstudio_get_service_target_url($service_id) { - $service_id = (int) $service_id; - if ($service_id <= 0) { - return ''; - } - - $meta_key = itstudio_get_service_url_meta_key(); - $url = (string) get_post_meta($service_id, $meta_key, true); - if ($url !== '') { - return $url; - } - - return (string) get_permalink($service_id); -} - -function itstudio_is_service_campus_only($service_id) { - $service_id = (int) $service_id; - if ($service_id <= 0) { - return false; - } - - return (string) get_post_meta($service_id, itstudio_get_service_campus_only_meta_key(), true) === '1'; -} - -function itstudio_service_admin_columns($columns) { - $columns['service_category'] = __('分类', 'itstudio'); - $columns['service_url'] = __('跳转链接', 'itstudio'); - return $columns; -} -add_filter('manage_service_posts_columns', 'itstudio_service_admin_columns'); - -function itstudio_service_admin_custom_column($column, $post_id) { - if ($column === 'service_category') { - $terms = get_the_terms($post_id, 'service_category'); - if (empty($terms) || is_wp_error($terms)) { - echo '' . esc_html__('未分类', 'itstudio') . ''; - return; - } - - $labels = array(); - foreach ($terms as $term) { - $i18n = itstudio_get_service_category_i18n_labels($term); - $label = $i18n['cn']; - if ($i18n['en'] !== $i18n['cn']) { - $label .= ' / ' . $i18n['en']; - } - $labels[] = $label; - } - - echo esc_html(implode(' | ', $labels)); - return; - } - - if ($column === 'service_url') { - $url = itstudio_get_service_target_url($post_id); - if ($url === '') { - echo '-'; - return; - } - - echo '' . esc_html($url) . ''; - } -} -add_action('manage_service_posts_custom_column', 'itstudio_service_admin_custom_column', 10, 2); - -function itstudio_archive_document_title($parts) { - if (is_post_type_archive('announcement')) { - $parts['title'] = '公告通知'; - } elseif (is_post_type_archive('news')) { - $parts['title'] = '社团新闻'; - } - - return $parts; -} -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. -function itstudio_news_fallback() { - if (!is_404()) { - return; - } - - global $wp; - $request = isset($wp->request) ? trim($wp->request, '/') : ''; - if ($request !== 'news') { - return; - } - - $template = locate_template('archive.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' => 'news', - '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; - }); - $GLOBALS['itstudio_archive_mode'] = 'news'; - status_header(200); - nocache_headers(); - include $template; - unset($GLOBALS['itstudio_archive_mode']); - exit; - } -} -add_action('template_redirect', 'itstudio_news_fallback'); - -// Fallback: render /services even if the page isn't created in WP admin. -function itstudio_services_fallback() { - if (!is_404()) { - return; - } - - global $wp; - $request = isset($wp->request) ? trim($wp->request, '/') : ''; - if ($request !== 'services') { - return; - } - - $template = locate_template('page-services.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' => 'services', - '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_services_fallback'); - -function itstudio_get_post_views($post_id) { - $post_id = (int) $post_id; - if ($post_id <= 0) { - return 0; - } - - $meta_keys = array( - 'post_views_count', - 'itstudio_views', - 'views', - 'post_views', - 'view_count', - 'views_count', - ); - - $max_views = 0; - foreach ($meta_keys as $meta_key) { - $raw = get_post_meta($post_id, $meta_key, true); - if ($raw !== '' && is_numeric($raw)) { - $max_views = max($max_views, (int) $raw); - } - } - - return max(0, $max_views); -} - -function itstudio_get_post_weight($post_id, $field_name = 'itstudio_weight') { - $post_id = (int) $post_id; - if ($post_id <= 0) { - return 0; - } - - if (!function_exists('get_field')) { - return 0; - } - - $raw = get_field($field_name, $post_id, false); - if ($raw === '' || $raw === null || !is_numeric($raw)) { - return 0; - } - - return (int) $raw; -} - -function itstudio_get_post_char_count($post_id) { - $post_id = (int) $post_id; - if ($post_id <= 0) { - return 0; - } - - $content = (string) get_post_field('post_content', $post_id); - $plain_text = trim(wp_strip_all_tags($content)); - if ($plain_text === '') { - return 0; - } - - if (function_exists('mb_strlen')) { - return (int) mb_strlen($plain_text, 'UTF-8'); - } - - return (int) strlen($plain_text); -} - -function itstudio_get_post_excerpt_chars($post_id, $limit = 200) { - $post_id = (int) $post_id; - $limit = max(1, (int) $limit); - if ($post_id <= 0) { - return ''; - } - - $excerpt = (string) get_post_field('post_excerpt', $post_id); - if (trim($excerpt) === '') { - $excerpt = (string) get_post_field('post_content', $post_id); - } - - $excerpt = trim(wp_strip_all_tags($excerpt)); - if ($excerpt === '') { - return ''; - } - - return wp_html_excerpt($excerpt, $limit, '...'); -} - -function itstudio_get_archive_url_by_post_type($post_type) { - $post_type = sanitize_key((string) $post_type); - if ($post_type === '') { - return home_url('/'); - } - - $archive_url = get_post_type_archive_link($post_type); - if ($archive_url) { - return $archive_url; - } - - if ($post_type === 'news') { - return home_url('/news/'); - } - - if ($post_type === 'announcement') { - return home_url('/announcement/'); - } - - return home_url('/'); -} - -function itstudio_get_archive_tag_filter_url($term, $post_type) { - $term_obj = null; - if ($term instanceof WP_Term) { - $term_obj = $term; - } else { - $term_obj = get_term($term, 'post_tag'); - } - - if (!($term_obj instanceof WP_Term) || is_wp_error($term_obj)) { - return ''; - } - - $post_type = sanitize_key((string) $post_type); - if (!in_array($post_type, array('news', 'announcement'), true)) { - return ''; - } - - $archive_url = itstudio_get_archive_url_by_post_type($post_type); - return add_query_arg( - array( - 'tag' => $term_obj->slug, - ), - $archive_url - ); -} - -function itstudio_archive_extended_search_enabled($query) { - if (!($query instanceof WP_Query)) { - return false; - } - - if (!$query->get('itstudio_archive_extend_search')) { - return false; - } - - $keyword = trim((string) $query->get('itstudio_archive_keyword')); - return $keyword !== ''; -} - -function itstudio_archive_extended_search_join($join, $query) { - if (!itstudio_archive_extended_search_enabled($query)) { - return $join; - } - - global $wpdb; - if (strpos($join, $wpdb->users) !== false) { - return $join; - } - - return $join . " LEFT JOIN {$wpdb->users} ON ({$wpdb->posts}.post_author = {$wpdb->users}.ID) "; -} -add_filter('posts_join', 'itstudio_archive_extended_search_join', 10, 2); - -function itstudio_archive_extended_search_where($where, $query) { - if (!itstudio_archive_extended_search_enabled($query)) { - return $where; - } - - global $wpdb; - $keyword = trim((string) $query->get('itstudio_archive_keyword')); - $like = '%' . $wpdb->esc_like($keyword) . '%'; - - $search_sql = $wpdb->prepare( - " AND ( - {$wpdb->posts}.post_title LIKE %s - OR {$wpdb->posts}.post_excerpt LIKE %s - OR {$wpdb->posts}.post_content LIKE %s - OR {$wpdb->users}.display_name LIKE %s - OR {$wpdb->users}.user_login LIKE %s - OR {$wpdb->users}.user_nicename LIKE %s - )", - $like, - $like, - $like, - $like, - $like, - $like - ); - - return $where . $search_sql; -} -add_filter('posts_where', 'itstudio_archive_extended_search_where', 10, 2); - -function itstudio_is_probably_bot_request() { - $user_agent = strtolower(trim((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''))); - if ($user_agent === '') { - return true; - } - - $bot_signatures = array( - 'bot', - 'spider', - 'crawl', - 'slurp', - 'headless', - 'preview', - 'facebookexternalhit', - 'discordbot', - 'telegrambot', - 'linkedinbot', - 'applebot', - 'googlebot', - 'bingbot', - 'wget', - 'curl', - 'python-requests', - 'postmanruntime', - ); - - foreach ($bot_signatures as $signature) { - if (strpos($user_agent, $signature) !== false) { - return true; - } - } - - $purpose = strtolower((string) ($_SERVER['HTTP_PURPOSE'] ?? '')); - $sec_purpose = strtolower((string) ($_SERVER['HTTP_SEC_PURPOSE'] ?? '')); - $x_moz = strtolower((string) ($_SERVER['HTTP_X_MOZ'] ?? '')); - if (strpos($purpose, 'prefetch') !== false || strpos($sec_purpose, 'prefetch') !== false || $x_moz === 'prefetch') { - return true; - } - - return false; -} - -function itstudio_get_view_cookie_name() { - return 'itstudio_viewed_posts'; -} - -function itstudio_read_view_cookie_map() { - $cookie_name = itstudio_get_view_cookie_name(); - $raw_cookie = isset($_COOKIE[$cookie_name]) ? wp_unslash((string) $_COOKIE[$cookie_name]) : ''; - if ($raw_cookie === '') { - return array(); - } - - $decoded = json_decode($raw_cookie, true); - if (!is_array($decoded)) { - return array(); - } - - $clean = array(); - foreach ($decoded as $post_id => $timestamp) { - $post_id = (int) $post_id; - $timestamp = (int) $timestamp; - if ($post_id > 0 && $timestamp > 0) { - $clean[(string) $post_id] = $timestamp; - } - } - - return $clean; -} - -function itstudio_write_view_cookie_map($map, $window_seconds) { - if (!headers_sent()) { - $cookie_name = itstudio_get_view_cookie_name(); - $expire_at = time() + max(DAY_IN_SECONDS, (int) $window_seconds * 2); - $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; - $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; - $secure = is_ssl(); - $http_only = true; - $encoded = wp_json_encode($map); - if (!is_string($encoded)) { - return; - } - - if (PHP_VERSION_ID >= 70300) { - setcookie($cookie_name, $encoded, array( - 'expires' => $expire_at, - 'path' => $path, - 'domain' => $domain, - 'secure' => $secure, - 'httponly' => $http_only, - 'samesite' => 'Lax', - )); - } else { - setcookie($cookie_name, $encoded, $expire_at, $path, $domain, $secure, $http_only); - } - - $_COOKIE[$cookie_name] = $encoded; - } -} - -function itstudio_should_count_post_view($post_id) { - $post_id = (int) $post_id; - if ($post_id <= 0) { - return false; - } - - if (itstudio_is_probably_bot_request()) { - return false; - } - - $window_seconds = (int) apply_filters('itstudio_post_view_window_seconds', 12 * HOUR_IN_SECONDS); - $window_seconds = max(60, $window_seconds); - $max_entries = (int) apply_filters('itstudio_post_view_cookie_max_entries', 120); - $max_entries = max(10, $max_entries); - - $now = time(); - $key = (string) $post_id; - $map = itstudio_read_view_cookie_map(); - - foreach ($map as $id => $timestamp) { - if (($now - (int) $timestamp) > $window_seconds) { - unset($map[$id]); - } - } - - if (isset($map[$key]) && ($now - (int) $map[$key]) < $window_seconds) { - return false; - } - - $map[$key] = $now; - if (count($map) > $max_entries) { - asort($map, SORT_NUMERIC); - $map = array_slice($map, -$max_entries, null, true); - } - - itstudio_write_view_cookie_map($map, $window_seconds); - - return true; -} - -function itstudio_track_post_views() { - if (is_admin() || wp_doing_ajax() || wp_doing_cron()) { - return; - } - - if (!is_singular(array('post', 'announcement', 'news')) || is_preview() || is_feed() || is_trackback() || is_embed()) { - return; - } - - if (!isset($_SERVER['REQUEST_METHOD']) || strtoupper((string) $_SERVER['REQUEST_METHOD']) !== 'GET') { - return; - } - - $post_id = (int) get_queried_object_id(); - if ($post_id <= 0) { - return; - } - - if (!itstudio_should_count_post_view($post_id)) { - return; - } - - $views = itstudio_get_post_views($post_id) + 1; - update_post_meta($post_id, 'post_views_count', $views); - update_post_meta($post_id, 'itstudio_views', $views); -} -add_action('template_redirect', 'itstudio_track_post_views', 20); function itstudio_is_join_page_context() { $is_join = is_page('join') || is_page_template('page-join.php'); @@ -1737,8 +188,19 @@ function itstudio_join_sanitize_settings($input) { return array_merge($defaults, $sanitized); } -function itstudio_join_get_settings() { +function itstudio_join_get_settings($term_id = null) { $defaults = itstudio_join_get_default_settings(); + + // 学期感知:无参时取当前生效学期;有参时取指定学期。 + if (function_exists('itstudio_join_get_terms')) { + $target_id = $term_id !== null ? (string) $term_id : itstudio_join_get_active_term_id(); + $terms = itstudio_join_get_terms(); + if ($target_id !== '' && isset($terms[$target_id]) && is_array($terms[$target_id]['settings'] ?? null)) { + return array_merge($defaults, itstudio_join_sanitize_settings($terms[$target_id]['settings'])); + } + } + + // 兜底:迁移尚未发生时读旧的单一选项。 $stored = get_option('itstudio_join_settings', array()); if (!is_array($stored)) { return $defaults; @@ -3482,6 +1944,120 @@ function itstudio_join_register_settings_page() { } add_action('admin_menu', 'itstudio_join_register_settings_page'); +/** + * 统一跳回招新设置页并带上提示参数。 + */ +function itstudio_join_settings_redirect($args) { + $args = array_merge(array('page' => 'itstudio-join-settings'), $args); + wp_safe_redirect(add_query_arg($args, admin_url('options-general.php'))); + exit; +} + +/** + * 错误码转中文提示(供跳转后展示)。 + */ +function itstudio_join_settings_error_message($code) { + $map = array( + 'delete_active' => '无法删除“当前生效”学期,请先把其它学期设为生效。', + 'delete_last' => '至少要保留一个学期。', + 'delete_failed' => '删除失败,学期不存在。', + 'save_failed' => '保存失败,学期不存在。', + 'need_year' => '请填写学年或自定义名称。', + ); + return isset($map[$code]) ? $map[$code] : '操作失败。'; +} + +/** + * 保存某学期的配置(含学期元信息)。 + */ +function itstudio_join_handle_save_term_settings() { + if (!current_user_can('manage_options')) { + wp_die('权限不足'); + } + check_admin_referer('itstudio_join_save_term_settings'); + + $term_id = isset($_POST['itstudio_join_term_id']) ? sanitize_text_field(wp_unslash($_POST['itstudio_join_term_id'])) : ''; + $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(); + + if (!empty($meta)) { + itstudio_join_update_term_meta( + $term_id, + isset($meta['academic_year']) ? $meta['academic_year'] : '', + isset($meta['season']) ? $meta['season'] : 'fall', + isset($meta['label']) ? $meta['label'] : '' + ); + } + + if (!itstudio_join_save_term_settings($term_id, $raw)) { + itstudio_join_settings_redirect(array('term' => $term_id, 'error' => 'save_failed')); + } + + itstudio_join_settings_redirect(array('term' => $term_id, 'updated' => '1')); +} +add_action('admin_post_itstudio_join_save_term_settings', 'itstudio_join_handle_save_term_settings'); + +/** + * 新增学期。 + */ +function itstudio_join_handle_create_term() { + if (!current_user_can('manage_options')) { + wp_die('权限不足'); + } + check_admin_referer('itstudio_join_create_term'); + + $year = isset($_POST['academic_year']) ? sanitize_text_field(wp_unslash($_POST['academic_year'])) : ''; + $season = isset($_POST['season']) ? sanitize_text_field(wp_unslash($_POST['season'])) : 'fall'; + $label = isset($_POST['label']) ? sanitize_text_field(wp_unslash($_POST['label'])) : ''; + + if (trim($year) === '' && trim($label) === '') { + itstudio_join_settings_redirect(array('error' => 'need_year')); + } + + $id = itstudio_join_create_term($year, $season, $label, null); + itstudio_join_settings_redirect(array('term' => $id, 'created' => '1')); +} +add_action('admin_post_itstudio_join_create_term', 'itstudio_join_handle_create_term'); + +/** + * 把某学期设为当前生效。 + */ +function itstudio_join_handle_activate_term() { + if (!current_user_can('manage_options')) { + wp_die('权限不足'); + } + check_admin_referer('itstudio_join_activate_term'); + + $term_id = isset($_POST['term_id']) ? sanitize_text_field(wp_unslash($_POST['term_id'])) : ''; + itstudio_join_set_active_term($term_id); + itstudio_join_settings_redirect(array('term' => $term_id, 'activated' => '1')); +} +add_action('admin_post_itstudio_join_activate_term', 'itstudio_join_handle_activate_term'); + +/** + * 删除某学期(不能删当前生效或最后一个)。 + */ +function itstudio_join_handle_delete_term() { + if (!current_user_can('manage_options')) { + wp_die('权限不足'); + } + check_admin_referer('itstudio_join_delete_term'); + + $term_id = isset($_POST['term_id']) ? sanitize_text_field(wp_unslash($_POST['term_id'])) : ''; + $terms = itstudio_join_get_terms(); + if (count($terms) <= 1) { + itstudio_join_settings_redirect(array('term' => $term_id, 'error' => 'delete_last')); + } + if (itstudio_join_get_active_term_id() === $term_id) { + itstudio_join_settings_redirect(array('term' => $term_id, 'error' => 'delete_active')); + } + if (!itstudio_join_delete_term($term_id)) { + itstudio_join_settings_redirect(array('term' => $term_id, 'error' => 'delete_failed')); + } + itstudio_join_settings_redirect(array('deleted' => '1')); +} +add_action('admin_post_itstudio_join_delete_term', 'itstudio_join_handle_delete_term'); + function itstudio_join_render_photo_field_row($field_key, $label, $settings) { $attachment_id = isset($settings[$field_key]) ? absint($settings[$field_key]) : 0; $preview_url = $attachment_id > 0 ? wp_get_attachment_image_url($attachment_id, 'medium_large') : ''; @@ -3576,14 +2152,41 @@ function itstudio_join_render_settings_page() { wp_enqueue_media(); - $settings = itstudio_join_get_settings(); + $terms = itstudio_join_get_terms(); + $active_id = itstudio_join_get_active_term_id(); + $editing_term_id = isset($_GET['term']) ? sanitize_text_field(wp_unslash($_GET['term'])) : ''; + if ($editing_term_id === '' || !isset($terms[$editing_term_id])) { + $editing_term_id = $active_id; + } + $editing_term = isset($terms[$editing_term_id]) ? $terms[$editing_term_id] : null; + $is_editing_active = ($editing_term_id === $active_id); + $seasons = itstudio_join_get_seasons(); + + $settings = itstudio_join_get_settings($editing_term_id); $runtime = itstudio_join_get_runtime_data(); $formidable_active = shortcode_exists('formidable') || class_exists('FrmFormsController'); $smtp_active = class_exists('\WPMailSMTP\WP') || defined('WPMS_PLUGIN_VER'); + $admin_post_url = admin_url('admin-post.php'); ?>

爱特工作室招新设置

-

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

+

用于配置「加入我们」页面的时间节点、阶段结果和表单。前台始终展示当前生效学期。

+ + +

已保存。

+ + +

已新增学期。

+ + +

已设为当前生效学期。

+ + +

已删除学期。

+ + +

+
@@ -3597,8 +2200,84 @@ function itstudio_join_render_settings_page() {
-
- +
+

招新学期

+ + + + + + + + +
+ + + + +
+ 1) : ?> +
+ + + + +
+ + + ✔ 这是当前生效学期(前台展示的就是它) + + +
+
+ + + + + + +
+
+ +
+ + + +

学期信息(正在编辑:)

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

时间与结果配置

@@ -3903,57 +2582,3 @@ function itstudio_join_render_settings_page() { request) ? trim((string) $wp->request, '/') : ''; - if ($request !== 'join') { - return; - } - - $template = locate_template('page-join.php'); - if (!$template) { - return; - } - - global $wp_query; - if ($wp_query) { - $wp_query->is_404 = false; - $wp_query->is_page = true; - $wp_query->is_singular = true; - $virtual_post = new WP_Post((object) array( - 'ID' => 0, - 'post_type' => 'page', - 'post_parent' => 0, - 'post_title' => '加入我们', - 'post_status' => 'publish', - 'post_name' => 'join', - 'post_content' => '', - )); - $wp_query->post = $virtual_post; - $wp_query->posts = array($virtual_post); - $wp_query->queried_object = $virtual_post; - $wp_query->queried_object_id = 0; - $wp_query->post_count = 1; - $wp_query->found_posts = 1; - $wp_query->max_num_pages = 1; - global $post; - $post = $virtual_post; - setup_postdata($post); - } - - add_filter('document_title_parts', static function ($parts) { - $parts['title'] = '加入我们'; - return $parts; - }); - - status_header(200); - nocache_headers(); - include $template; - exit; -} -add_action('template_redirect', 'itstudio_join_fallback', 9); diff --git a/plugins/itstudio-recruitment/includes/recruitment-articles.php b/plugins/itstudio-recruitment/includes/recruitment-articles.php new file mode 100644 index 0000000..778093f --- /dev/null +++ b/plugins/itstudio-recruitment/includes/recruitment-articles.php @@ -0,0 +1,295 @@ + 'group_itstudio_recruitment_article', + 'title' => '招新文章标记', + 'fields' => array( + array( + 'key' => 'field_itstudio_is_recruitment_article', + 'label' => '是否为招新文章', + 'name' => 'itstudio_is_recruitment_article', + 'type' => 'true_false', + 'instructions' => '勾选后,该文章会在“加入我们”页面顶部新闻条中显示(按发布时间排序)。', + 'required' => 0, + 'default_value' => 0, + 'ui' => 1, + 'ui_on_text' => '是', + 'ui_off_text' => '否', + ), + ), + 'location' => array( + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'announcement', + ), + ), + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'news', + ), + ), + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'post', + ), + ), + ), + 'position' => 'side', + 'style' => 'default', + 'active' => true, + 'show_in_rest' => 1, + )); +} +add_action('acf/init', 'itstudio_register_acf_fields'); + +function itstudio_get_recruitment_article_meta_key() { + return 'itstudio_is_recruitment_article'; +} + +function itstudio_normalize_recruitment_flag($value) { + if (is_bool($value)) { + return $value; + } + + if (is_numeric($value)) { + return ((int) $value) > 0; + } + + $value = strtolower(trim((string) $value)); + return in_array($value, array('1', 'true', 'yes', 'on', 'y'), true); +} + +function itstudio_is_recruitment_article($post_id) { + $post_id = (int) $post_id; + if ($post_id <= 0) { + return false; + } + + $meta_key = itstudio_get_recruitment_article_meta_key(); + $meta_keys = array( + $meta_key, + '_itstudio_is_recruitment_article', + 'itstudio_join_article', + 'join_recruitment_article', + 'is_recruitment_article', + ); + + foreach ($meta_keys as $key) { + $raw = get_post_meta($post_id, $key, true); + if ($raw !== '' && $raw !== null && itstudio_normalize_recruitment_flag($raw)) { + return true; + } + } + + if (function_exists('get_field')) { + $acf_keys = array( + 'itstudio_is_recruitment_article', + 'is_recruitment_article', + 'itstudio_join_article', + ); + foreach ($acf_keys as $acf_key) { + $raw = get_field($acf_key, $post_id, false); + if ($raw !== '' && $raw !== null && itstudio_normalize_recruitment_flag($raw)) { + return true; + } + } + } + + return false; +} + +function itstudio_add_recruitment_meta_boxes() { + $screens = array('announcement', 'news', 'post'); + foreach ($screens as $screen) { + add_meta_box( + 'itstudio_recruitment_flag', + __('招新文章', 'itstudio'), + 'itstudio_render_recruitment_meta_box', + $screen, + 'side', + 'high' + ); + } +} +add_action('add_meta_boxes', 'itstudio_add_recruitment_meta_boxes'); + +function itstudio_render_recruitment_meta_box($post) { + $meta_key = itstudio_get_recruitment_article_meta_key(); + $checked = itstudio_is_recruitment_article((int) $post->ID); + wp_nonce_field('itstudio_save_recruitment_meta', 'itstudio_recruitment_meta_nonce'); + ?> +

+ +

+

+ +

+ + format('Y'); + $display_year = isset($join_runtime['recruitment_year']) && is_numeric($join_runtime['recruitment_year']) + ? (int) $join_runtime['recruitment_year'] + : $now_year; + + $season_start_ts = null; + $season_end_ts = null; + $stages = isset($join_runtime['stages']) && is_array($join_runtime['stages']) ? $join_runtime['stages'] : array(); + foreach ($stages as $stage) { + if (!is_array($stage)) { + continue; + } + $start_ts = isset($stage['start_ts']) && is_numeric($stage['start_ts']) ? (int) $stage['start_ts'] : null; + $end_ts = isset($stage['end_ts']) && is_numeric($stage['end_ts']) ? (int) $stage['end_ts'] : null; + if ($start_ts !== null && ($season_start_ts === null || $start_ts < $season_start_ts)) { + $season_start_ts = $start_ts; + } + if ($end_ts !== null && ($season_end_ts === null || $end_ts > $season_end_ts)) { + $season_end_ts = $end_ts; + } + } + + $now_ts = (int) (isset($join_runtime['now_ts']) && is_numeric($join_runtime['now_ts']) ? $join_runtime['now_ts'] : ((int) $now->format('U') * 1000)); + if ($season_start_ts !== null && $now_ts < $season_start_ts && $display_year > $now_year) { + // 下年招新未开始:继续展示本年招新资讯 + $display_year = $now_year; + } + + $query = new WP_Query(array( + 'post_type' => array('announcement', 'news', 'post'), + 'post_status' => 'publish', + 'posts_per_page' => 80, + 'orderby' => 'date', + 'order' => 'DESC', + 'no_found_rows' => true, + 'ignore_sticky_posts' => true, + 'date_query' => array( + array( + 'year' => $display_year, + ), + ), + )); + + $items = array(); + if ($query->have_posts()) { + foreach ($query->posts as $post) { + if (count($items) >= $limit) { + break; + } + + $post_id = (int) $post->ID; + if (!itstudio_is_recruitment_article($post_id)) { + continue; + } + + $title = trim((string) get_the_title($post_id)); + $url = (string) get_permalink($post_id); + if ($title === '' || $url === '') { + continue; + } + + $excerpt = function_exists('itstudio_get_post_excerpt_chars') + ? itstudio_get_post_excerpt_chars($post_id, 72) + : wp_html_excerpt(trim(wp_strip_all_tags((string) get_post_field('post_excerpt', $post_id))), 72, '...'); + + if ($excerpt === '') { + $excerpt = '...'; + } + + $author_id = (int) get_post_field('post_author', $post_id); + $author = trim((string) get_the_author_meta('display_name', $author_id)); + if ($author === '') { + $author = 'Unknown'; + } + + $items[] = array( + 'id' => $post_id, + 'title' => $title, + 'excerpt' => $excerpt, + 'url' => $url, + 'date' => get_the_date('Y-m-d H:i', $post_id), + 'date_iso' => get_the_date('c', $post_id), + 'author' => $author, + 'type' => get_post_type($post_id), + ); + } + } + wp_reset_postdata(); + + return array( + 'display_year' => $display_year, + 'items' => $items, + ); +} diff --git a/plugins/itstudio-recruitment/includes/terms.php b/plugins/itstudio-recruitment/includes/terms.php new file mode 100644 index 0000000..969aa31 --- /dev/null +++ b/plugins/itstudio-recruitment/includes/terms.php @@ -0,0 +1,261 @@ + '秋季', + 'spring' => '春季', + ); +} + +/** + * 读取全部学期(id => term 的关联数组)。 + */ +function itstudio_join_get_terms() { + $terms = get_option('itstudio_join_terms', array()); + return is_array($terms) ? $terms : array(); +} + +/** + * 当前生效学期 id。 + */ +function itstudio_join_get_active_term_id() { + $id = (string) get_option('itstudio_join_active_term_id', ''); + if ($id !== '') { + $terms = itstudio_join_get_terms(); + if (isset($terms[$id])) { + return $id; + } + } + + // 生效指针失效时,回退到第一个存在的学期。 + $terms = itstudio_join_get_terms(); + if (!empty($terms)) { + return (string) array_key_first($terms); + } + + return ''; +} + +/** + * 取单个学期结构(不存在返回 null)。 + */ +function itstudio_join_get_term($term_id) { + $term_id = (string) $term_id; + $terms = itstudio_join_get_terms(); + return isset($terms[$term_id]) ? $terms[$term_id] : null; +} + +/** + * 学期的展示名:优先自定义 label,否则“学年 + 季节招新”。 + */ +function itstudio_join_term_label($term) { + if (!is_array($term)) { + return ''; + } + + $custom = trim((string) ($term['label'] ?? '')); + if ($custom !== '') { + return $custom; + } + + $seasons = itstudio_join_get_seasons(); + $season_cn = $seasons[$term['season'] ?? ''] ?? ''; + $year = trim((string) ($term['academic_year'] ?? '')); + $label = trim($year . ' ' . $season_cn . '招新'); + return $label !== '招新' ? $label : '未命名招新期'; +} + +/** + * 由学年 + 季节生成稳定且唯一的学期 id(冲突时追加 -2、-3…)。 + */ +function itstudio_join_make_term_id($academic_year, $season, array $existing = array()) { + $base = sanitize_title($academic_year . '-' . $season); + if ($base === '') { + $base = 'term'; + } + + $id = $base; + $suffix = 2; + while (isset($existing[$id])) { + $id = $base . '-' . $suffix; + $suffix++; + } + + return $id; +} + +/** + * 新建学期。$settings 省略时用默认空配置。返回新学期 id。 + */ +function itstudio_join_create_term($academic_year, $season, $label = '', $settings = null) { + $terms = itstudio_join_get_terms(); + + $season = array_key_exists($season, itstudio_join_get_seasons()) ? $season : 'fall'; + $academic_year = itstudio_join_sanitize_shortcode_value($academic_year); + $label = itstudio_join_sanitize_shortcode_value($label); + + $id = itstudio_join_make_term_id($academic_year, $season, $terms); + + $terms[$id] = array( + 'id' => $id, + 'academic_year' => $academic_year, + 'season' => $season, + 'label' => $label, + 'created' => current_time('mysql'), + 'settings' => is_array($settings) + ? itstudio_join_sanitize_settings($settings) + : itstudio_join_get_default_settings(), + ); + + update_option('itstudio_join_terms', $terms); + + // 若尚无“显式”生效学期(读原始选项,不走会兜底的 getter),新建的即为生效。 + $active_raw = (string) get_option('itstudio_join_active_term_id', ''); + if ($active_raw === '' || !isset($terms[$active_raw])) { + itstudio_join_set_active_term($id); + } + + return $id; +} + +/** + * 更新某学期的元信息(学年/季节/自定义名)。 + */ +function itstudio_join_update_term_meta($term_id, $academic_year, $season, $label = '') { + $terms = itstudio_join_get_terms(); + $term_id = (string) $term_id; + if (!isset($terms[$term_id])) { + return false; + } + + $terms[$term_id]['academic_year'] = itstudio_join_sanitize_shortcode_value($academic_year); + $terms[$term_id]['season'] = array_key_exists($season, itstudio_join_get_seasons()) ? $season : 'fall'; + $terms[$term_id]['label'] = itstudio_join_sanitize_shortcode_value($label); + + update_option('itstudio_join_terms', $terms); + return true; +} + +/** + * 把一份配置写入指定学期。 + */ +function itstudio_join_save_term_settings($term_id, $raw_settings) { + $terms = itstudio_join_get_terms(); + $term_id = (string) $term_id; + if (!isset($terms[$term_id])) { + return false; + } + + $terms[$term_id]['settings'] = itstudio_join_sanitize_settings($raw_settings); + update_option('itstudio_join_terms', $terms); + return true; +} + +/** + * 设为当前生效学期。 + */ +function itstudio_join_set_active_term($term_id) { + $term_id = (string) $term_id; + $terms = itstudio_join_get_terms(); + if (!isset($terms[$term_id])) { + return false; + } + + update_option('itstudio_join_active_term_id', $term_id); + return true; +} + +/** + * 删除学期。不允许删“当前生效”或最后一个。 + */ +function itstudio_join_delete_term($term_id) { + $term_id = (string) $term_id; + $terms = itstudio_join_get_terms(); + + if (!isset($terms[$term_id])) { + return false; + } + if (count($terms) <= 1) { + return false; + } + if (itstudio_join_get_active_term_id() === $term_id) { + return false; + } + + unset($terms[$term_id]); + update_option('itstudio_join_terms', $terms); + return true; +} + +/** + * 由旧的单一配置推断学年与季节(报名开始月份 >= 7 视为秋季)。 + */ +function itstudio_join_guess_term_from_settings($settings) { + $settings = is_array($settings) ? $settings : array(); + $anchor = ''; + foreach (array('registration_start', 'notice_start_date', 'first_interview_date') as $key) { + if (!empty($settings[$key])) { + $anchor = (string) $settings[$key]; + break; + } + } + + if ($anchor === '' || !preg_match('/(\d{4})-(\d{2})/', $anchor, $m)) { + return array('academic_year' => '', 'season' => 'fall'); + } + + $year = (int) $m[1]; + $month = (int) $m[2]; + if ($month >= 7) { + return array('academic_year' => $year . '-' . ($year + 1), 'season' => 'fall'); + } + + return array('academic_year' => ($year - 1) . '-' . $year, 'season' => 'spring'); +} + +/** + * 一次性迁移:把旧的单一 itstudio_join_settings 归入首个学期并设为生效。 + * 前后台每个请求都会触发,但有标记,已迁移则立即返回。 + */ +function itstudio_join_maybe_migrate_terms() { + if (get_option('itstudio_join_terms_migrated')) { + return; + } + + $terms = itstudio_join_get_terms(); + if (empty($terms)) { + $legacy = get_option('itstudio_join_settings', array()); + $legacy = is_array($legacy) ? $legacy : array(); + + if (!empty($legacy)) { + $guess = itstudio_join_guess_term_from_settings($legacy); + $label = $guess['academic_year'] === '' ? '初始招新期' : ''; + itstudio_join_create_term($guess['academic_year'], $guess['season'], $label, $legacy); + } else { + // 全新安装:建一个空的当前学期占位。 + $now_year = (int) current_time('Y'); + itstudio_join_create_term($now_year . '-' . ($now_year + 1), 'fall', '', null); + } + } + + update_option('itstudio_join_terms_migrated', 1); +} +add_action('init', 'itstudio_join_maybe_migrate_terms', 1); diff --git a/plugins/itstudio-recruitment/itstudio-recruitment.php b/plugins/itstudio-recruitment/itstudio-recruitment.php new file mode 100644 index 0000000..86f8c77 --- /dev/null +++ b/plugins/itstudio-recruitment/itstudio-recruitment.php @@ -0,0 +1,36 @@ +ID, itstudio_get_service_url_meta_key(), true); + $title_cn = (string) get_post_meta($post->ID, itstudio_get_service_title_cn_meta_key(), true); + $title_en = (string) get_post_meta($post->ID, itstudio_get_service_title_en_meta_key(), true); + $excerpt_cn = (string) get_post_meta($post->ID, itstudio_get_service_excerpt_cn_meta_key(), true); + $excerpt_en = (string) get_post_meta($post->ID, itstudio_get_service_excerpt_en_meta_key(), true); + $is_campus_only = itstudio_is_service_campus_only($post->ID); + wp_nonce_field('itstudio_save_service_meta', 'itstudio_service_meta_nonce'); + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ trim($title_cn), + itstudio_get_service_title_en_meta_key() => trim($title_en), + itstudio_get_service_excerpt_cn_meta_key() => trim($excerpt_cn), + itstudio_get_service_excerpt_en_meta_key() => trim($excerpt_en), + ); + + foreach ($meta_updates as $meta_key => $value) { + if ($value === '') { + delete_post_meta($post_id, $meta_key); + } else { + update_post_meta($post_id, $meta_key, $value); + } + } + + $campus_only_meta_key = itstudio_get_service_campus_only_meta_key(); + $is_campus_only = isset($_POST['itstudio_service_campus_only']) && (string) wp_unslash($_POST['itstudio_service_campus_only']) === '1'; + if ($is_campus_only) { + update_post_meta($post_id, $campus_only_meta_key, '1'); + } else { + delete_post_meta($post_id, $campus_only_meta_key); + } + + $url_meta_key = itstudio_get_service_url_meta_key(); + $raw_url = isset($_POST['itstudio_service_url']) ? trim((string) wp_unslash($_POST['itstudio_service_url'])) : ''; + + if ($raw_url === '') { + delete_post_meta($post_id, $url_meta_key); + return; + } + + if (strpos($raw_url, '/') === 0) { + $raw_url = home_url($raw_url); + } + + $service_url = esc_url_raw($raw_url); + if ($service_url === '') { + delete_post_meta($post_id, $url_meta_key); + return; + } + + update_post_meta($post_id, $url_meta_key, $service_url); +} +add_action('save_post_service', 'itstudio_save_service_meta'); + +function itstudio_service_category_add_fields($taxonomy) { + wp_nonce_field('itstudio_save_service_category_meta', 'itstudio_service_category_meta_nonce'); + ?> +
+ + +
+
+ + +
+ term_id; + $name_cn = (string) get_term_meta($term_id, itstudio_get_service_category_name_cn_meta_key(), true); + $name_en = (string) get_term_meta($term_id, itstudio_get_service_category_name_en_meta_key(), true); + wp_nonce_field('itstudio_save_service_category_meta', 'itstudio_service_category_meta_nonce'); + ?> + + + + + + + + + trim($name_cn), + itstudio_get_service_category_name_en_meta_key() => trim($name_en), + ); + + foreach ($term_meta as $meta_key => $value) { + if ($value === '') { + delete_term_meta($term_id, $meta_key); + } else { + update_term_meta($term_id, $meta_key, $value); + } + } +} +add_action('created_service_category', 'itstudio_save_service_category_meta'); +add_action('edited_service_category', 'itstudio_save_service_category_meta'); + +function itstudio_service_admin_columns($columns) { + $columns['service_category'] = __('分类', 'itstudio'); + $columns['service_url'] = __('跳转链接', 'itstudio'); + return $columns; +} +add_filter('manage_service_posts_columns', 'itstudio_service_admin_columns'); + +function itstudio_service_admin_custom_column($column, $post_id) { + if ($column === 'service_category') { + $terms = get_the_terms($post_id, 'service_category'); + if (empty($terms) || is_wp_error($terms)) { + echo '' . esc_html__('未分类', 'itstudio') . ''; + return; + } + + $labels = array(); + foreach ($terms as $term) { + $i18n = itstudio_get_service_category_i18n_labels($term); + $label = $i18n['cn']; + if ($i18n['en'] !== $i18n['cn']) { + $label .= ' / ' . $i18n['en']; + } + $labels[] = $label; + } + + echo esc_html(implode(' | ', $labels)); + return; + } + + if ($column === 'service_url') { + $url = itstudio_get_service_target_url($post_id); + if ($url === '') { + echo '-'; + return; + } + + echo '' . esc_html($url) . ''; + } +} +add_action('manage_service_posts_custom_column', 'itstudio_service_admin_custom_column', 10, 2); diff --git a/plugins/itstudio-services/includes/meta.php b/plugins/itstudio-services/includes/meta.php new file mode 100644 index 0000000..55a1fad --- /dev/null +++ b/plugins/itstudio-services/includes/meta.php @@ -0,0 +1,141 @@ + '未分类', + 'en' => 'Uncategorized', + ); + } + + $term_id = (int) $term->term_id; + $name_cn = trim((string) get_term_meta($term_id, itstudio_get_service_category_name_cn_meta_key(), true)); + $name_en = trim((string) get_term_meta($term_id, itstudio_get_service_category_name_en_meta_key(), true)); + + if ($name_cn === '') { + $name_cn = (string) $term->name; + } + if ($name_en === '') { + $name_en = $name_cn; + } + + return array( + 'cn' => $name_cn, + 'en' => $name_en, + ); +} + +function itstudio_get_service_i18n_content($service_id, $excerpt_limit = 90) { + $service_id = (int) $service_id; + if ($service_id <= 0) { + return array( + 'title_cn' => '', + 'title_en' => '', + 'excerpt_cn' => '', + 'excerpt_en' => '', + ); + } + + $title_cn = trim((string) get_post_meta($service_id, itstudio_get_service_title_cn_meta_key(), true)); + $title_en = trim((string) get_post_meta($service_id, itstudio_get_service_title_en_meta_key(), true)); + $excerpt_cn = trim((string) get_post_meta($service_id, itstudio_get_service_excerpt_cn_meta_key(), true)); + $excerpt_en = trim((string) get_post_meta($service_id, itstudio_get_service_excerpt_en_meta_key(), true)); + + $fallback_title = trim((string) get_the_title($service_id)); + if ($fallback_title === '') { + $fallback_title = '未命名服务'; + } + + $fallback_excerpt = function_exists('itstudio_get_post_excerpt_chars') + ? itstudio_get_post_excerpt_chars($service_id, $excerpt_limit) + : wp_html_excerpt(wp_strip_all_tags((string) get_post_field('post_excerpt', $service_id)), $excerpt_limit, '...'); + $fallback_excerpt = trim((string) $fallback_excerpt); + if ($fallback_excerpt === '') { + $fallback_excerpt = '暂无简介'; + } + + if ($title_cn === '') { + $title_cn = $fallback_title; + } + if ($title_en === '') { + $title_en = $title_cn; + } + if ($excerpt_cn === '') { + $excerpt_cn = $fallback_excerpt; + } + if ($excerpt_en === '') { + $excerpt_en = $excerpt_cn; + } + + return array( + 'title_cn' => $title_cn, + 'title_en' => $title_en, + 'excerpt_cn' => $excerpt_cn, + 'excerpt_en' => $excerpt_en, + ); +} + +function itstudio_get_service_target_url($service_id) { + $service_id = (int) $service_id; + if ($service_id <= 0) { + return ''; + } + + $meta_key = itstudio_get_service_url_meta_key(); + $url = (string) get_post_meta($service_id, $meta_key, true); + if ($url !== '') { + return $url; + } + + return (string) get_permalink($service_id); +} + +function itstudio_is_service_campus_only($service_id) { + $service_id = (int) $service_id; + if ($service_id <= 0) { + return false; + } + + return (string) get_post_meta($service_id, itstudio_get_service_campus_only_meta_key(), true) === '1'; +} diff --git a/plugins/itstudio-services/includes/post-type.php b/plugins/itstudio-services/includes/post-type.php new file mode 100644 index 0000000..533473c --- /dev/null +++ b/plugins/itstudio-services/includes/post-type.php @@ -0,0 +1,53 @@ + array( + 'name' => __('服务分类', 'itstudio'), + 'singular_name' => __('服务分类', 'itstudio'), + ), + 'hierarchical' => true, + 'public' => true, + 'show_ui' => true, + 'show_admin_column' => false, + 'show_in_rest' => true, + 'rewrite' => array( + 'slug' => 'service-category', + 'with_front' => false, + ), + )); + + register_post_type('service', array( + 'labels' => array( + 'name' => __('便民服务', 'itstudio'), + 'singular_name' => __('便民服务', 'itstudio'), + 'menu_name' => __('便民服务', 'itstudio'), + 'add_new_item' => __('新增便民服务', 'itstudio'), + 'edit_item' => __('编辑便民服务', 'itstudio'), + ), + 'public' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'show_in_nav_menus' => false, + 'exclude_from_search' => true, + 'publicly_queryable' => true, + 'has_archive' => false, + 'rewrite' => array( + 'slug' => 'service', + 'with_front' => false, + ), + 'supports' => array('title', 'excerpt', 'thumbnail', 'page-attributes'), + 'taxonomies' => array('service_category'), + 'menu_icon' => 'dashicons-admin-tools', + 'show_in_rest' => true, + )); +} +add_action('init', 'itstudio_services_register_post_types'); diff --git a/plugins/itstudio-services/itstudio-services.php b/plugins/itstudio-services/itstudio-services.php new file mode 100644 index 0000000..e9c1d6a --- /dev/null +++ b/plugins/itstudio-services/itstudio-services.php @@ -0,0 +1,38 @@ + + + + + + + + \ No newline at end of file diff --git a/themes/ITStudioMainSite/.idea/modules.xml b/themes/ITStudioMainSite/.idea/modules.xml new file mode 100644 index 0000000..dda6700 --- /dev/null +++ b/themes/ITStudioMainSite/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/themes/ITStudioMainSite/.idea/vcs.xml b/themes/ITStudioMainSite/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/themes/ITStudioMainSite/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/404.php b/themes/ITStudioMainSite/404.php similarity index 100% rename from 404.php rename to themes/ITStudioMainSite/404.php diff --git a/themes/ITStudioMainSite/README.md b/themes/ITStudioMainSite/README.md new file mode 100644 index 0000000..7571d31 --- /dev/null +++ b/themes/ITStudioMainSite/README.md @@ -0,0 +1,291 @@ +
+ +
+
+

爱特工作室总站

+
+
+
+
+ + + + + +
+
+ +## 📖 项目简介 + +中国海洋大学爱特工作室官方网站 WordPress 主题。这是一个现代化、响应式的主题,支持暗色/亮色模式切换,融合海洋元素与 IT 风格。 + +## ✨ 主要特性 + +- 🎨 **现代简洁设计** - IT风格与海洋元素完美融合 +- 🌓 **主题切换** - 支持暗色/亮色模式,可根据系统主题自动切换 +- 📱 **响应式布局** - 完美适配 PC 端和移动端 +- 🌐 **多语言支持** - 支持中文/英文切换 +- ⚡ **性能优化** - 轻量级设计,加载快速 +- 🎯 **自定义内容类型** - 支持公告通知、社团新闻、便民服务独立管理 + +## 🚀 快速开始 + +### 安装方法 + +1. 克隆本仓库到 WordPress 主题目录: +```bash +cd wp-content/themes +git clone https://github.com/itstudio-2002/ITStudioMainSite.git +``` + +2. 在 WordPress 后台激活主题: + - 进入 **外观 > 主题** + - 找到 "IT Studio Theme" + - 点击 **激活** + +### 主题配置 + +#### 1. 导航菜单设置 + +进入 **外观 > 菜单**,创建主导航菜单并分配到 "Primary Menu" 位置。 + +推荐菜单结构: +- 首页 +- 公告通知 +- 博客 +- 便民服务 +- 社团介绍 +- 加入我们 + +#### 2. 发布公告 + +主题内置了"公告通知"自定义文章类型: +1. 进入 **Announcement > 新建 Announcement** +2. 填写标题和内容 +3. 发布即可在首页显示 + +#### 3. 发布新闻 + +主题内置了"社团新闻"自定义文章类型: +1. 进入 **News > 新建 News** +2. 填写标题和内容 +3. 发布即可在首页显示 + +#### 4. 新闻公告页权重字段(ACF) + +新闻公告页右侧“高权重文章”读取 ACF 数值字段 `itstudio_weight`(数值越大,排序越靠前)。 + +适用范围: +- 默认文章(`post`) +- 公告(`announcement`) +- 新闻(`news`) + +设置步骤: +1. 安装并启用插件 **Advanced Custom Fields (ACF)**。 +2. 主题会自动注册字段组“内容权重”,字段名为 `itstudio_weight`(显示名“权重”)。 +3. 编辑文章时在右侧填写“权重”数值(例如 `10`、`50`、`100`)。 +4. 更新或发布文章。 + +说明: +- 当前仅读取 ACF 字段值,不再使用 Gutenberg 的“自定义字段”面板作为权重来源。 +- 未设置 `itstudio_weight` 的文章按 `0` 处理。 +- 当高权重文章不足 4 篇时,页面会自动用最新文章补足。 + +#### 5. 维护便民服务目录 + +主题内置了“便民服务(service)”内容类型,`/services` 页面会自动读取并展示。 + +每条服务信息来源: +- 图标:文章特色图(Featured Image) +- 名称:服务双语字段(中文名称 / 英文名称) +- 简介:服务双语字段(中文简介 / 英文简介) +- 类别:服务分类双语字段(中文名称 / 英文名称) +- 跳转链接:编辑页中的“服务跳转链接”字段 +- 访问属性:编辑页中的“是否为校内服务”勾选项 + +后台新增步骤: +1. 进入 **便民服务 > 新增便民服务**。 +2. 在“服务双语与链接”填写: + - 中文名称 / 英文名称 + - 中文简介 / 英文简介 + - 服务跳转链接 + - 是否为校内服务(可选) +3. 设置特色图作为服务图标。 +4. 在右侧选择或新建“服务分类”。 +5. 发布后,该服务会自动出现在 `/services` 页面中。 + +分类双语设置: +1. 进入 **便民服务 > 服务分类**。 +2. 新增或编辑分类时,可填写“中文名称”和“英文名称”。 +3. 前台会根据语言切换自动显示对应分类名。 + +说明: +- 若未设置“服务跳转链接”,前端会回退到该服务文章自身链接。 +- 若未设置特色图,前端会使用 `resources/it_logo_2024.svg` 作为默认图标。 +- 勾选“是否为校内服务”后,服务卡片右上角会显示“仅校内访问”标签;不勾选则不显示该标签。 +- 新增内容类型后若前台路由未生效,请进入 **设置 > 固定链接** 点击一次“保存更改”刷新重写规则。 + +## 🎨 设计规范 + +### 主题色彩 + +- **海洋浅蓝色** (#f0f8ff) - 亮色模式背景 +- **海洋深蓝色** (#0a1929) - 暗色模式背景 +- **强调色** (#8e88c7) - 链接、按钮等 +- **中性色** - 灰度系列用于文本和边框 + +### 页面结构 + +- **Header** - 左侧 Logo,右侧导航栏、社交图标、主题切换按钮 +- **首页** - 工作室介绍、公告列表、博客列表双栏布局 +- **Footer** - 友情链接、联系方式、版权信息 + +## 📂 文件结构 + +``` +ITStudioMainSite/ +├── assets/ +│ ├── css/ +│ │ ├── content.css # 内容页样式 +│ │ ├── front-page.css # 首页样式 +│ │ └── services-page.css # 便民服务页样式 +│ └── js/ +│ ├── theme-toggle.js # 主题切换功能 +│ └── main.js # 主要 JavaScript +├── resources/ +│ ├── it_logo_2024.svg # 工作室 Logo +│ ├── ouc-logo.svg # 海大 Logo +│ └── ... +├── style.css # WordPress 主题样式表(必需) +├── functions.php # 主题功能文件 +├── index.php # 首页模板 +├── header.php # 头部模板 +├── footer.php # 底部模板 +├── single.php # 单篇文章模板 +├── page.php # 页面模板 +├── page-services.php # 便民服务页面模板 +├── archive.php # 归档页面模板 +└── 404.php # 404 错误页面 +``` + +## 🛠️ 技术栈 + +- **WordPress** - 内容管理系统 +- **PHP** - 后端开发语言 +- **HTML5/CSS3** - 前端标记与样式 +- **JavaScript** - 前端交互逻辑 +- **SVG** - 矢量图标和 Logo + +## 🔧 开发指南 + +### 本地开发环境 + +1. 安装 WordPress 本地开发环境(如 XAMPP、MAMP 等) +2. 克隆本仓库到 `wp-content/themes` 目录 +3. 激活主题开始开发 + +### 自定义开发 + +- 修改 `assets/css/main.css` 自定义样式 +- 编辑 `functions.php` 添加新功能 +- 创建新的模板文件扩展功能 + +## 📝 使用许可 + +MIT License - 详见 LICENSE 文件 + +## 👥 关于我们 + +**爱特工作室**成立于 2002 年,是中国海洋大学信息科学与工程学部领导主持下的技术性团队。 + +🎯 **使命**: 发现人才,培养人才,输送人才 + +🔗 **链接**: +- GitHub: [https://github.com/ITStudioOUC](https://github.com/ITStudioOUC) +- Email: contact@itstudio.club + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 📄 更新日志 + +### v1.0.0 (2026-02-05) +- 🎉 初始版本发布 +- ✅ 基础主题功能实现 +- ✅ 响应式设计完成 +- ✅ 主题切换功能 +- ✅ 自定义文章类型(公告) + +--- + +## 加入我们页面(/join)使用说明 + +主题已内置「加入我们」页面模板(`page-join.php`),并支持在没有创建 WordPress 页面时通过 `/join` 自动 fallback 渲染。 + +### 1. 需要安装的插件 + +1. **Formidable Forms** +用于报名表单渲染(前台报名)。 +2. **WP Mail SMTP** +用于 Formidable 提交邮件发送(站点邮件走 SMTP)。 + +### 2. 后台时间节点设置 + +进入:**设置 > 招新设置** + +可配置字段: +- 报名开始时间(datetime) +- 报名结束时间(datetime) +- 第一次面试开始/结束时间(datetime) +- 第一次面试地点(中/英) +- 第二次面试开始/结束时间(datetime) +- 第二次面试地点(中/英) +- 国庆能力摸底开始/结束日期(调试) +- 录取结果公布开始日期(date,系统自动延续 7 天) +- 报名表单 Shortcode + +说明: +- 「国庆能力摸底阶段」固定为每年 **10/01 - 10/07**。 +- 若填写“国庆能力摸底开始/结束日期(调试)”,将优先使用调试时间。 + +### 3. 阶段结果文件配置(重点) + +在同一页面中,为以下阶段上传结果文件(CSV 或 XLSX): +- 第一次面试结果文件 +- 国庆能力摸底结果文件 +- 第二次面试结果文件 +- 录取结果文件 + +文件列顺序必须为: + +`姓名,QQ,邮箱,学号,手机,是否通过` + +其中: +- `是否通过 = 1` 表示通过 +- 其他任意值(含 `0`、空)都按未通过处理 + +注意: +- 不需要配置任何“字段映射”或“阶段识别字段”。 +- 系统会自动按上述固定列进行识别与查询。 + +### 4. 前台显示逻辑 + +- 报名表单:仅在报名阶段显示。 +- 录取进度查询:报名阶段不显示;报名结束后按当前阶段显示对应结果(使用姓名/QQ/邮箱/学号查询)。 +- Canvas 进度条:根据当前时间自动高亮阶段(已完成/进行中/未开始)。 + +### 5. WP Mail SMTP 配置建议 + +1. 安装并启用 **WP Mail SMTP**。 +2. 在插件中填写 SMTP 服务信息(Host、Port、加密方式、账号密码)。 +3. 设置发件人邮箱与发件人名称。 +4. 使用插件自带发送测试邮件,确认可达后再开放报名。 + +## 加入我们页面快速说明 + +- 后台路径:`设置 -> 招新设置`。 +- 先配置时间节点:报名开始/结束、一面开始/结束、二面开始/结束、录取结果公布开始日期。 +- “国庆能力摸底”默认是每年 `10/01 - 10/07`(可用调试日期覆盖)。 +- 报名表单短代码使用 Formidable 表单,邮件发送建议配合 WP Mail SMTP。 +- 阶段结果统一使用 CSV/XLSX 上传,不再使用 Formidable Entries 做结果查询。 diff --git a/archive.php b/themes/ITStudioMainSite/archive.php similarity index 100% rename from archive.php rename to themes/ITStudioMainSite/archive.php diff --git a/assets/css/about-hero.css b/themes/ITStudioMainSite/assets/css/about-hero.css similarity index 100% rename from assets/css/about-hero.css rename to themes/ITStudioMainSite/assets/css/about-hero.css diff --git a/assets/css/content.css b/themes/ITStudioMainSite/assets/css/content.css similarity index 100% rename from assets/css/content.css rename to themes/ITStudioMainSite/assets/css/content.css diff --git a/assets/css/footer.css b/themes/ITStudioMainSite/assets/css/footer.css similarity index 100% rename from assets/css/footer.css rename to themes/ITStudioMainSite/assets/css/footer.css diff --git a/assets/css/front-page.css b/themes/ITStudioMainSite/assets/css/front-page.css similarity index 100% rename from assets/css/front-page.css rename to themes/ITStudioMainSite/assets/css/front-page.css diff --git a/assets/css/header.css b/themes/ITStudioMainSite/assets/css/header.css similarity index 100% rename from assets/css/header.css rename to themes/ITStudioMainSite/assets/css/header.css diff --git a/assets/css/intro.css b/themes/ITStudioMainSite/assets/css/intro.css similarity index 100% rename from assets/css/intro.css rename to themes/ITStudioMainSite/assets/css/intro.css diff --git a/assets/css/join-page.css b/themes/ITStudioMainSite/assets/css/join-page.css similarity index 100% rename from assets/css/join-page.css rename to themes/ITStudioMainSite/assets/css/join-page.css diff --git a/assets/css/services-page.css b/themes/ITStudioMainSite/assets/css/services-page.css similarity index 100% rename from assets/css/services-page.css rename to themes/ITStudioMainSite/assets/css/services-page.css diff --git a/assets/js/hero-waves.js b/themes/ITStudioMainSite/assets/js/hero-waves.js similarity index 100% rename from assets/js/hero-waves.js rename to themes/ITStudioMainSite/assets/js/hero-waves.js diff --git a/assets/js/home-hero.js b/themes/ITStudioMainSite/assets/js/home-hero.js similarity index 100% rename from assets/js/home-hero.js rename to themes/ITStudioMainSite/assets/js/home-hero.js diff --git a/assets/js/intro-scroll.js b/themes/ITStudioMainSite/assets/js/intro-scroll.js similarity index 100% rename from assets/js/intro-scroll.js rename to themes/ITStudioMainSite/assets/js/intro-scroll.js diff --git a/assets/js/join-canvas.js b/themes/ITStudioMainSite/assets/js/join-canvas.js similarity index 100% rename from assets/js/join-canvas.js rename to themes/ITStudioMainSite/assets/js/join-canvas.js diff --git a/assets/js/landing-hero-canvas.js b/themes/ITStudioMainSite/assets/js/landing-hero-canvas.js similarity index 100% rename from assets/js/landing-hero-canvas.js rename to themes/ITStudioMainSite/assets/js/landing-hero-canvas.js diff --git a/assets/js/lang-toggle.js b/themes/ITStudioMainSite/assets/js/lang-toggle.js similarity index 100% rename from assets/js/lang-toggle.js rename to themes/ITStudioMainSite/assets/js/lang-toggle.js diff --git a/assets/js/main.js b/themes/ITStudioMainSite/assets/js/main.js similarity index 100% rename from assets/js/main.js rename to themes/ITStudioMainSite/assets/js/main.js diff --git a/assets/js/single-title-fit.js b/themes/ITStudioMainSite/assets/js/single-title-fit.js similarity index 100% rename from assets/js/single-title-fit.js rename to themes/ITStudioMainSite/assets/js/single-title-fit.js diff --git a/assets/js/stream.js b/themes/ITStudioMainSite/assets/js/stream.js similarity index 100% rename from assets/js/stream.js rename to themes/ITStudioMainSite/assets/js/stream.js diff --git a/assets/js/theme-toggle.js b/themes/ITStudioMainSite/assets/js/theme-toggle.js similarity index 100% rename from assets/js/theme-toggle.js rename to themes/ITStudioMainSite/assets/js/theme-toggle.js diff --git a/assets/js/vendor/anime.min.js b/themes/ITStudioMainSite/assets/js/vendor/anime.min.js similarity index 100% rename from assets/js/vendor/anime.min.js rename to themes/ITStudioMainSite/assets/js/vendor/anime.min.js diff --git a/themes/ITStudioMainSite/docs/plugin-split-plan.md b/themes/ITStudioMainSite/docs/plugin-split-plan.md new file mode 100644 index 0000000..c93ff86 --- /dev/null +++ b/themes/ITStudioMainSite/docs/plugin-split-plan.md @@ -0,0 +1,87 @@ +# ITStudioMainSite 主题业务逻辑拆分方案 + +> 目标:把 `functions.php`(3959 行)里混入的插件级业务逻辑抽成 3 个独立插件,主题只保留展示层。 + +## 一、数据安全铁律 + +迁移只搬代码,不动数据库。以下标识符字符串必须一字不改,否则数据库中老数据失联: + +| 类型 | 必须保持不变的值 | +|---|---| +| 文章类型 slug | `news`、`announcement`、`service`、`post` | +| 分类法 slug | `service_category` | +| 选项名 | `itstudio_join_settings` | +| post meta / ACF 字段名 | `itstudio_weight`、`itstudio_is_recruitment_article`、各 service meta key | +| AJAX action / nonce | `itstudio_join_progress_lookup` | + +CPT 注册从主题挪到插件后,重写规则改变,必须在插件激活钩子 `flush_rewrite_rules()`,或手动访问一次“设置→固定链接”。 + +## 二、三个插件与函数清单 + +### 插件 1 `itstudio-content`(核心内容,地基,无依赖) +- news / announcement CPT(从 `itstudio_custom_post_types` 拆出)+ post_tag 绑定 +- ACF `group_itstudio_content_priority`(权重,从 `itstudio_register_acf_fields` 拆出) +- 浏览量统计:`get_post_views`(1206)、`is_probably_bot_request`(1394)、view cookie 组(1436-1494)、`should_count_post_view`(1494)、`track_post_views`(1534) +- 助手:`get_post_weight`(1232)、`get_post_char_count`(1250)、`get_post_excerpt_chars`(1269) +- 归档 URL:`get_archive_url_by_post_type`(1289)、`get_archive_tag_filter_url`(1311) +- 归档扩展搜索:`archive_extended_search_enabled/join/where`(1337-1392) +- 日期精度显示:`expand_news_notice_date_precision`(155) + +### 插件 2 `itstudio-services`(便民服务,独立,无依赖) +- service CPT + service_category(从 `itstudio_custom_post_types` 拆出) +- service meta key 助手(638-670) +- `get_service_category_i18n_labels`(670)、`get_service_i18n_content`(695) +- meta box:`add_service_meta_boxes`(745)、`render_service_link_meta_box`(757)、`save_service_meta`(847) +- 分类法字段:`service_category_add_fields`(914)、`edit_fields`(929)、`save_service_category_meta`(947) +- 前端读取:`get_service_target_url`(975)、`is_service_campus_only`(990) +- 后台列:`service_admin_columns`(999)、`service_admin_custom_column`(1006) + +### 插件 3 `itstudio-recruitment`(招新系统,依赖插件1 + Formidable + ACF) +- 招新文章标记 meta:`get_recruitment_article_meta_key`(406)、`normalize_recruitment_flag`(410)、`is_recruitment_article`(423)、meta box 462/477/501 +- ACF `group_itstudio_recruitment_article`(从 `itstudio_register_acf_fields` 拆出) +- 招新 feed:`join_get_recruitment_feed_items`(538) +- `is_join_page_context`(1562) +- 设置读写/清洗(1573-1748)、时间解析(1750-1811)、结果字段映射(1814-1848) +- CSV/XLSX 解析器(1863-2050) +- Formidable 耦合(2121-2427) +- 查询匹配/身份识别(2453-2678) +- 录取进度查询核心 `join_resolve_progress_lookup`(2693) +- 阶段状态机(2893-3336) +- AJAX 接口(3366 + 钩子 3402/3403) +- 强制 Formidable AJAX(3405 + 钩子 3418) +- 前端资源(3420 + 钩子 3450) +- 后台设置页(3452-3572 + 钩子 3463/3472/3483) + +## 三、需手术拆开的函数 +1. `itstudio_custom_post_types()`(231-306):news/announcement/post_tag → 插件1;service/service_category → 插件2。 +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_apply_site_identity()`(62-74)——每次强制覆写站点标题/副标题,反模式。 + +## 五、主题模板解耦 +- 插件用 `add_shortcode` 暴露内容(如 `[itstudio_join_app]`、`[itstudio_services_grid]`),模板用 `shortcode_exists()` 判断。 +- 助手函数调用处一律加 `function_exists()` 守卫,插件未启用时走降级,保证不白屏。 + +## 六、迁移顺序(按依赖,每步原子提交) +1. 建 `itstudio-content`(地基)→ 搬入 + 从主题删除 → 加模板守卫 → 刷新重写规则 → 验证。 +2. 建 `itstudio-services`(独立)→ 同上。 +3. 建 `itstudio-recruitment`(依赖插件1)→ 声明 `Requires Plugins` → 验证。 +4. 收尾:清理 `functions.php`,删除 `apply_site_identity`。 + +关键:每个插件“搬入 + 从主题删除”是一次原子提交。绝不能让同一函数名/CPT 注册在主题和插件同时存在(PHP `Cannot redeclare` 致命错误)。是剪切,不是复制双跑。 + +## 七、迁移期间不崩策略 +- 本地/staging 先做,别直上生产。 +- 每个插件搬完过验证清单再 commit,可单独 revert。 +- 生产上线:插件上传 + 启用 + 主题更新尽量同一操作。 +- 上线后立刻刷一次固定链接。 + +## 八、验证清单(每插件搬完都跑) +- 新闻/公告归档正常、按作者名搜索有效、浏览量在涨 +- 便民服务列表/分类/双语/仅校内标签正常,meta box 能存能读 +- 加入我们页:报名表单能提交、进度查询返回正确阶段、招新设置能存能读、CSV/XLSX 解析正常 +- 后台开 `WP_DEBUG` 无 PHP 错误 +- 停用任一插件对应页面走降级不白屏 diff --git a/footer.php b/themes/ITStudioMainSite/footer.php similarity index 100% rename from footer.php rename to themes/ITStudioMainSite/footer.php diff --git a/themes/ITStudioMainSite/functions.php b/themes/ITStudioMainSite/functions.php new file mode 100644 index 0000000..ab494e1 --- /dev/null +++ b/themes/ITStudioMainSite/functions.php @@ -0,0 +1,412 @@ + __('Primary Menu', 'itstudio'), + )); + + load_theme_textdomain('itstudio', get_template_directory() . '/languages'); +} +add_action('after_setup_theme', 'itstudio_theme_setup'); + +add_filter('comments_open', '__return_false', 20, 2); +add_filter('pings_open', '__return_false', 20, 2); +add_filter('comments_array', '__return_empty_array', 10, 2); + +function itstudio_disable_comments_post_types() { + $post_types = array('post', 'page', 'announcement', 'news', 'service'); + foreach ($post_types as $post_type) { + if (post_type_supports($post_type, 'comments')) { + remove_post_type_support($post_type, 'comments'); + } + if (post_type_supports($post_type, 'trackbacks')) { + remove_post_type_support($post_type, 'trackbacks'); + } + } +} +add_action('init', 'itstudio_disable_comments_post_types', 100); + +function itstudio_hide_comments_menu() { + remove_menu_page('edit-comments.php'); +} +add_action('admin_menu', 'itstudio_hide_comments_menu', 999); + +function itstudio_hide_admin_bar_comments() { + remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60); +} +add_action('init', 'itstudio_hide_admin_bar_comments'); + +function itstudio_redirect_comments_admin_pages() { + global $pagenow; + if ($pagenow === 'edit-comments.php' || $pagenow === 'comment.php') { + wp_safe_redirect(admin_url()); + exit; + } +} +add_action('admin_init', 'itstudio_redirect_comments_admin_pages'); + +function itstudio_remove_comments_dashboard_widget() { + remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal'); +} +add_action('wp_dashboard_setup', 'itstudio_remove_comments_dashboard_widget'); + +function itstudio_apply_site_identity() { + $site_name = '爱特工作室'; + $site_tagline = '爱特工作室官方网站'; + + if (get_option('blogname') !== $site_name) { + update_option('blogname', $site_name); + } + + if (get_option('blogdescription') !== $site_tagline) { + update_option('blogdescription', $site_tagline); + } +} +add_action('init', 'itstudio_apply_site_identity', 20); + +function itstudio_output_favicon() { + $favicon_url = get_template_directory_uri() . '/resources/it_logo_2024.svg'; + echo '' . "\n"; + echo '' . "\n"; +} + +function itstudio_output_theme_bootstrap_script() { + echo '' . "\n"; + echo '' . "\n"; +} + +function itstudio_output_lang_bootstrap_script() { + echo '' . "\n"; + echo '' . "\n"; +} +function itstudio_disable_default_site_icon() { + remove_action('wp_head', 'wp_site_icon', 99); + remove_action('admin_head', 'wp_site_icon', 99); + remove_action('login_head', 'wp_site_icon', 99); +} +add_action('init', 'itstudio_disable_default_site_icon'); +add_action('wp_head', 'itstudio_output_theme_bootstrap_script', 0); +add_action('wp_head', 'itstudio_output_lang_bootstrap_script', 1); +add_action('wp_head', 'itstudio_output_favicon', 1); +add_action('admin_head', 'itstudio_output_favicon', 1); +add_action('login_head', 'itstudio_output_favicon', 1); + +function itstudio_enqueue_scripts() { + // 基础样式 (Style.css) + wp_enqueue_style('itstudio-style', get_stylesheet_uri(), array(), '2.1.2'); + + wp_enqueue_style('itstudio-header', get_template_directory_uri() . '/assets/css/header.css', array('itstudio-style'), '2.1.2'); + wp_enqueue_style('itstudio-footer', get_template_directory_uri() . '/assets/css/footer.css', array('itstudio-style'), '2.1.2'); + wp_enqueue_style('itstudio-content', get_template_directory_uri() . '/assets/css/content.css', array('itstudio-style'), '2.1.2'); + + // 仅在首页加载首页样式 + 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_script('itstudio-landing-hero-canvas', get_template_directory_uri() . '/assets/js/landing-hero-canvas.js', array(), '1.0.0', true); + } + + // 仅在工作室介绍页加载(包含 /about fallback) + $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) { + wp_enqueue_style('itstudio-about-hero', get_template_directory_uri() . '/assets/css/about-hero.css', array('itstudio-content'), '2.1.2'); + 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) + $is_services = is_page('services') || is_page_template('page-services.php'); + if (!$is_services && is_404()) { + global $wp; + $request = isset($wp->request) ? trim($wp->request, '/') : ''; + $is_services = ($request === 'services'); + } + + if ($is_services) { + wp_enqueue_style('itstudio-services-page', get_template_directory_uri() . '/assets/css/services-page.css', array('itstudio-content'), '1.0.0'); + } + + // 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-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); + + if (is_singular(array('post', 'announcement', 'news'))) { + wp_enqueue_script('itstudio-single-title-fit', get_template_directory_uri() . '/assets/js/single-title-fit.js', array(), '1.0.0', true); + } +} +add_action('wp_enqueue_scripts', 'itstudio_enqueue_scripts'); + +function itstudio_register_sidebars() { + register_sidebar(array( + 'name' => __('Footer - Column 1', 'itstudio'), + 'id' => 'footer-1', + 'description' => __('Footer widget area 1', 'itstudio'), + 'before_widget' => '', + 'before_title' => '

', + 'after_title' => '

', + )); + + register_sidebar(array( + 'name' => __('Footer - Column 2', 'itstudio'), + 'id' => 'footer-2', + 'description' => __('Footer widget area 2', 'itstudio'), + 'before_widget' => '', + 'before_title' => '

', + 'after_title' => '

', + )); +} +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) { + if (is_post_type_archive('announcement')) { + $parts['title'] = '公告通知'; + } elseif (is_post_type_archive('news')) { + $parts['title'] = '社团新闻'; + } + + return $parts; +} +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. +function itstudio_news_fallback() { + if (!is_404()) { + return; + } + + global $wp; + $request = isset($wp->request) ? trim($wp->request, '/') : ''; + if ($request !== 'news') { + return; + } + + $template = locate_template('archive.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' => 'news', + '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; + }); + $GLOBALS['itstudio_archive_mode'] = 'news'; + status_header(200); + nocache_headers(); + include $template; + unset($GLOBALS['itstudio_archive_mode']); + exit; + } +} +add_action('template_redirect', 'itstudio_news_fallback'); + +// Fallback: render /services even if the page isn't created in WP admin. +function itstudio_services_fallback() { + if (!is_404()) { + return; + } + + global $wp; + $request = isset($wp->request) ? trim($wp->request, '/') : ''; + if ($request !== 'services') { + return; + } + + $template = locate_template('page-services.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' => 'services', + '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_services_fallback'); + +function itstudio_join_fallback() { + if (!is_404()) { + return; + } + + global $wp; + $request = isset($wp->request) ? trim((string) $wp->request, '/') : ''; + if ($request !== 'join') { + return; + } + + $template = locate_template('page-join.php'); + if (!$template) { + return; + } + + global $wp_query; + if ($wp_query) { + $wp_query->is_404 = false; + $wp_query->is_page = true; + $wp_query->is_singular = true; + $virtual_post = new WP_Post((object) array( + 'ID' => 0, + 'post_type' => 'page', + 'post_parent' => 0, + 'post_title' => '加入我们', + 'post_status' => 'publish', + 'post_name' => 'join', + 'post_content' => '', + )); + $wp_query->post = $virtual_post; + $wp_query->posts = array($virtual_post); + $wp_query->queried_object = $virtual_post; + $wp_query->queried_object_id = 0; + $wp_query->post_count = 1; + $wp_query->found_posts = 1; + $wp_query->max_num_pages = 1; + global $post; + $post = $virtual_post; + setup_postdata($post); + } + + add_filter('document_title_parts', static function ($parts) { + $parts['title'] = '加入我们'; + return $parts; + }); + + status_header(200); + nocache_headers(); + include $template; + exit; +} +add_action('template_redirect', 'itstudio_join_fallback', 9); diff --git a/header.php b/themes/ITStudioMainSite/header.php similarity index 100% rename from header.php rename to themes/ITStudioMainSite/header.php diff --git a/index.php b/themes/ITStudioMainSite/index.php similarity index 100% rename from index.php rename to themes/ITStudioMainSite/index.php diff --git a/page-about.php b/themes/ITStudioMainSite/page-about.php similarity index 100% rename from page-about.php rename to themes/ITStudioMainSite/page-about.php diff --git a/page-join.php b/themes/ITStudioMainSite/page-join.php similarity index 100% rename from page-join.php rename to themes/ITStudioMainSite/page-join.php diff --git a/page-services.php b/themes/ITStudioMainSite/page-services.php similarity index 100% rename from page-services.php rename to themes/ITStudioMainSite/page-services.php diff --git a/page.php b/themes/ITStudioMainSite/page.php similarity index 100% rename from page.php rename to themes/ITStudioMainSite/page.php diff --git a/resources/Organization-ITstudio-shield.svg b/themes/ITStudioMainSite/resources/Organization-ITstudio-shield.svg similarity index 100% rename from resources/Organization-ITstudio-shield.svg rename to themes/ITStudioMainSite/resources/Organization-ITstudio-shield.svg diff --git a/resources/head_logo.svg b/themes/ITStudioMainSite/resources/head_logo.svg similarity index 100% rename from resources/head_logo.svg rename to themes/ITStudioMainSite/resources/head_logo.svg diff --git a/resources/it_logo_2024.svg b/themes/ITStudioMainSite/resources/it_logo_2024.svg similarity index 100% rename from resources/it_logo_2024.svg rename to themes/ITStudioMainSite/resources/it_logo_2024.svg diff --git a/resources/mainpage.jpg b/themes/ITStudioMainSite/resources/mainpage.jpg similarity index 100% rename from resources/mainpage.jpg rename to themes/ITStudioMainSite/resources/mainpage.jpg diff --git a/resources/ouc-logo.svg b/themes/ITStudioMainSite/resources/ouc-logo.svg similarity index 100% rename from resources/ouc-logo.svg rename to themes/ITStudioMainSite/resources/ouc-logo.svg diff --git a/resources/title.svg b/themes/ITStudioMainSite/resources/title.svg similarity index 100% rename from resources/title.svg rename to themes/ITStudioMainSite/resources/title.svg diff --git a/single.php b/themes/ITStudioMainSite/single.php similarity index 100% rename from single.php rename to themes/ITStudioMainSite/single.php diff --git a/style.css b/themes/ITStudioMainSite/style.css similarity index 100% rename from style.css rename to themes/ITStudioMainSite/style.css