diff --git a/assets/css/content.css b/assets/css/content.css index 49cf65a..e7b84ff 100644 --- a/assets/css/content.css +++ b/assets/css/content.css @@ -160,6 +160,320 @@ } } +/* ========================================= + 文章页(简洁大气) + ========================================= */ + +.single-article-page { + padding: 46px 0 72px; +} + +.single-article-shell { + max-width: 1320px; +} + +.single-article { + width: 100%; + max-width: 1280px; + margin: 0 auto; +} + +.single-article-header { + text-align: center; +} + +.single-article-kicker { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + gap: 24px; + margin-bottom: 24px; +} + +.single-article-kicker-link { + color: var(--text-primary); + text-decoration: none; + font-size: 1.08rem; + font-weight: 600; + letter-spacing: 0.01em; +} + +.single-article-kicker-link:hover { + color: var(--color-primary); +} + +.single-article-title { + margin: 0; + font-size: clamp(2.3rem, 4.7vw, 5rem); + line-height: 1.1; + letter-spacing: -0.02em; + color: var(--text-primary); + text-wrap: balance; +} + +.single-article-meta { + margin-top: 28px; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + align-items: baseline; + column-gap: 16px; + color: var(--text-secondary); + font-family: var(--font-sans); + font-size: 0.94rem; + max-width: 980px; + margin-left: auto; + margin-right: auto; +} + +.single-article-meta-item { + display: inline-flex; + align-items: baseline; + justify-content: center; + gap: 6px; + white-space: nowrap; +} + +.single-article-meta-label { + color: var(--text-secondary); + font-weight: 500; +} + +.single-article-meta-value { + color: var(--text-primary); + font-family: var(--font-mono); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.single-article-body { + width: 100%; + max-width: 1080px; + margin: 52px auto 0; + color: var(--text-primary); + font-size: clamp(1.14rem, 1.2vw, 1.28rem); + line-height: 1.82; + font-family: "Times New Roman", Times, "Nimbus Roman No9 L", "Noto Sans SC", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", var(--font-sans); + overflow-wrap: break-word; + word-wrap: break-word; +} + +.single-article-body > * { + margin: 0 0 1.25em; +} + +.single-article-body h2, +.single-article-body h3, +.single-article-body h4 { + margin-top: 1.7em; + line-height: 1.35; + font-family: inherit; +} + +.single-article-body h2 { + font-size: clamp(1.45rem, 1.6vw, 2rem); +} + +.single-article-body h3 { + font-size: clamp(1.2rem, 1.2vw, 1.55rem); +} + +.single-article-body a { + color: var(--text-primary); + text-decoration-thickness: 1.5px; + text-decoration-color: color-mix(in srgb, var(--color-primary) 70%, transparent); +} + +.single-article-body a:hover { + color: var(--color-primary); +} + +.single-article-body blockquote { + margin: 1.5em 0; + padding: 0.2em 0 0.2em 1.1em; + border-left: 3px solid var(--border-default); + color: var(--text-secondary); +} + +.single-article-body img { + max-width: 100%; + height: auto; + border-radius: var(--radius-sm); +} + +.single-article-body pre { + padding: 16px; + border: 1px solid var(--border-default); + border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--bg-surface) 92%, transparent); + overflow: auto; + line-height: 1.45; + font-size: 0.92rem; +} + +.single-article-body code { + font-family: var(--font-mono); + font-size: 0.9em; + background: color-mix(in srgb, var(--bg-surface) 92%, transparent); + border-radius: 4px; + padding: 0.12em 0.35em; +} + +.single-article-tags { + width: 100%; + max-width: 1080px; + margin: 30px auto 0; + padding-top: 16px; + border-top: 1px solid var(--border-default); + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.single-article-tag { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 10px; + border: 1px solid var(--border-default); + border-radius: 999px; + text-decoration: none; + color: var(--text-secondary); + font-size: 0.78rem; + background: color-mix(in srgb, var(--bg-card) 88%, transparent); +} + +.single-article-tag:hover { + color: var(--color-primary); + border-color: var(--color-primary); +} + +.single-related { + max-width: 1180px; + margin: 62px auto 0; +} + +.single-divider { + border-top: 1px solid var(--border-default); +} + +.single-related-title { + margin: 44px 0 24px; + color: var(--text-primary); + font-size: clamp(1.8rem, 2vw, 2.5rem); + letter-spacing: -0.01em; +} + +.single-related-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 24px; +} + +.single-related-card { + min-width: 0; +} + +.single-related-card-title { + margin: 0; + font-size: clamp(1.2rem, 1.2vw, 1.75rem); + line-height: 1.3; +} + +.single-related-card-title a { + color: var(--text-primary); + text-decoration: none; +} + +.single-related-card-title a:hover { + color: var(--color-primary); +} + +.single-related-card-excerpt { + margin: 12px 0 0; + color: var(--text-secondary); + line-height: 1.66; + font-size: 1rem; + font-family: "Times New Roman", Times, "Nimbus Roman No9 L", "Noto Sans SC", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", var(--font-sans); +} + +.single-related-card-link { + margin-top: 16px; + display: inline-block; + color: var(--text-secondary); + text-decoration: none; + font-size: 1.02rem; +} + +.single-related-card-link:hover { + color: var(--color-primary); +} + +.single-related-empty { + color: var(--text-secondary); +} + +.single-article-comments { + max-width: 900px; + margin: 64px auto 0; +} + +@media (max-width: 980px) { + .single-article-page { + padding: 34px 0 56px; + } + + .single-article-body { + margin-top: 42px; + font-size: 1.1rem; + line-height: 1.75; + } + + .single-related-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 760px) { + .single-article-kicker { + gap: 14px; + margin-bottom: 16px; + } + + .single-article-meta { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 8px 14px; + margin-top: 18px; + font-size: 0.8rem; + } + + .single-article-body { + margin-top: 30px; + font-size: 1.02rem; + line-height: 1.72; + } + + .single-related { + margin-top: 44px; + } + + .single-related-title { + margin-top: 34px; + margin-bottom: 18px; + } + + .single-related-grid { + grid-template-columns: 1fr; + gap: 20px; + } + + .single-article-comments { + margin-top: 48px; + } +} + /* ========================================= GitHub 风格博客 ========================================= */ diff --git a/assets/js/single-title-fit.js b/assets/js/single-title-fit.js new file mode 100644 index 0000000..4cde4d9 --- /dev/null +++ b/assets/js/single-title-fit.js @@ -0,0 +1,128 @@ +(function () { + "use strict"; + + var TITLE_SELECTOR = ".single-article-title"; + var MAX_LINES = 2; + var MIN_FONT_SIZE = 12; + var MAX_FONT_SIZE = 92; + var SEARCH_STEPS = 16; + + function getLineHeightPx(element) { + var computed = window.getComputedStyle(element); + var lineHeight = computed.lineHeight; + var fontSize = parseFloat(computed.fontSize) || 16; + + if (lineHeight && lineHeight.indexOf("px") > -1) { + return parseFloat(lineHeight); + } + + if (lineHeight === "normal") { + return fontSize * 1.2; + } + + var ratio = parseFloat(lineHeight); + if (Number.isFinite(ratio)) { + return ratio * fontSize; + } + + return fontSize * 1.2; + } + + function getLineCount(element) { + var lineHeightPx = getLineHeightPx(element); + if (!lineHeightPx) { + return 0; + } + return element.getBoundingClientRect().height / lineHeightPx; + } + + function setFontSize(element, size) { + element.style.fontSize = size.toFixed(2) + "px"; + } + + function fitsWithinLines(element, size) { + setFontSize(element, size); + return getLineCount(element) <= MAX_LINES + 0.01; + } + + function fitTitle(title) { + if (!title) { + return; + } + + var header = title.closest(".single-article-header") || title.parentElement || title; + if (!header || header.getBoundingClientRect().width <= 0) { + return; + } + + title.style.fontSize = ""; + var computedSize = parseFloat(window.getComputedStyle(title).fontSize) || 48; + + var low = MIN_FONT_SIZE; + var high = Math.min(MAX_FONT_SIZE, Math.max(computedSize * 1.8, computedSize + 8)); + + while (high < MAX_FONT_SIZE && fitsWithinLines(title, high)) { + high = Math.min(MAX_FONT_SIZE, high + 8); + if (high === MAX_FONT_SIZE) { + break; + } + } + + var best = low; + for (var i = 0; i < SEARCH_STEPS; i += 1) { + var mid = (low + high) / 2; + if (fitsWithinLines(title, mid)) { + best = mid; + low = mid; + } else { + high = mid; + } + } + + if (!fitsWithinLines(title, best)) { + while (best > MIN_FONT_SIZE && !fitsWithinLines(title, best)) { + best -= 0.5; + } + } + + setFontSize(title, Math.max(MIN_FONT_SIZE, best - 0.05)); + } + + function initSingleTitleFitter() { + var title = document.querySelector(TITLE_SELECTOR); + if (!title) { + return; + } + + var scheduled = false; + var scheduleFit = function () { + if (scheduled) { + return; + } + scheduled = true; + window.requestAnimationFrame(function () { + scheduled = false; + fitTitle(title); + }); + }; + + scheduleFit(); + window.addEventListener("resize", scheduleFit, { passive: true }); + window.addEventListener("pageshow", scheduleFit); + + if ("ResizeObserver" in window) { + var resizeObserver = new ResizeObserver(scheduleFit); + resizeObserver.observe(title); + var header = title.closest(".single-article-header"); + if (header) { + resizeObserver.observe(header); + } + } + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initSingleTitleFitter); + } else { + initSingleTitleFitter(); + } +})(); diff --git a/functions.php b/functions.php index dc81d9d..05fdceb 100644 --- a/functions.php +++ b/functions.php @@ -81,6 +81,10 @@ function itstudio_enqueue_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'); @@ -484,6 +488,146 @@ function itstudio_get_post_excerpt_chars($post_id, $limit = 200) { return wp_html_excerpt($excerpt, $limit, '...'); } +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; @@ -502,6 +646,10 @@ function itstudio_track_post_views() { 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); diff --git a/single.php b/single.php index 40d2cd5..895c8d3 100644 --- a/single.php +++ b/single.php @@ -1,108 +1,138 @@ -
-
- - -
-
- term_id) . '" class="gh-label-category">' . $cat->name . ''; - endif; - ?> -
+
+
+ + labels->name : ''; + $post_type_label_cn = $post_type_label_en; + if ($post_type === 'announcement') { + $post_type_label_cn = '公告通知'; + $post_type_label_en = 'Announcements'; + } elseif ($post_type === 'news') { + $post_type_label_cn = '社团新闻'; + $post_type_label_en = 'News'; + } elseif ($post_type === 'post') { + $post_type_label_cn = '技术博客'; + $post_type_label_en = 'Blog'; + } + $views = function_exists('itstudio_get_post_views') ? (int) itstudio_get_post_views($post_id) : 0; -

+ ?> -
-
- - - / - +
> +
+
+ + +
-
- - - +

+ + -
-
+
- -
-
-
-
- - readme.md - - - ', - $read_time, - $read_time - ); - ?> - -
-
- -
+
+ +
+ + + + + + + + + +
+
+

-
- -
- -
- - - - - +
+
-
-
- - -
-
-

-
- -
- -
- - -
-
- + +