feat: 允许自定义首页代码动画的代码

This commit is contained in:
2026-09-02 10:16:13 +08:00
parent e0bf7677a3
commit 49d07c21b0
2 changed files with 340 additions and 52 deletions
@@ -18,63 +18,129 @@
const EMPTY_SET = new Set();
const WORD_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
const VARIABLE_RE = /^\$[A-Za-z_][A-Za-z0-9_]*$/;
const NUMBER_RE = /^\d+(?:_\d+)*(?:\.\d+)?$/;
const STRING_RE = /^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)$/;
const CONSTANT_RE = /^[A-Z][A-Z0-9_]{2,}$/;
const TYPE_RE = /^[A-Z][A-Za-z0-9_]*$/;
const OPERATOR_RE = /^(::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.#])$/;
const TOKEN_RE = /(\/\/.*$|#\[.*$|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:_\d+)*(?:\.\d+)?\b|[A-Za-z_][A-Za-z0-9_]*|::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.,;()[\]{}#])/g;
const TOKEN_RE = /(\/\*.*?\*\/|<!--.*?-->|#\[[^\]]*\]|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|@[A-Za-z_][A-Za-z0-9_]*|\$[A-Za-z_][A-Za-z0-9_]*|\b\d+(?:_\d+)*(?:\.\d+)?\b|[A-Za-z_][A-Za-z0-9_]*|::|->|=>|==|!=|<=|>=|\+=|-=|\*=|\/=|&&|\|\||[+\-*/%=&|^!<>?:.,;()[\]{}#])/g;
const keywordSet = (words) => new Set(words.split(/\s+/).filter(Boolean));
const mergeKeywordSets = (...sets) => new Set(sets.flatMap((set) => [...set]));
const C_KEYWORDS = keywordSet('auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned void volatile while _Bool true false null');
const CPP_KEYWORDS = mergeKeywordSets(C_KEYWORDS, keywordSet('alignas alignof and and_eq asm bitand bitor bool catch char8_t char16_t char32_t class compl concept constexpr consteval constinit const_cast co_await co_return co_yield decltype delete dynamic_cast explicit export false friend mutable namespace new noexcept not not_eq nullptr operator or or_eq private protected public reinterpret_cast requires static_assert static_cast template this thread_local throw true try typeid typename using virtual wchar_t xor xor_eq std include'));
const JVM_KEYWORDS = keywordSet('abstract as assert boolean break byte case catch char class const constructor continue data default do double else enum extends false final finally float for fun goto if implements import in instanceof int interface internal is long native new null object open package private protected public record return sealed short static strictfp super switch synchronized this throw throws trait transient true try typealias typeof val var void volatile when while yield');
const JS_KEYWORDS = keywordSet('as async await break case catch class const continue debugger default delete do else export extends false finally for from function get if import in instanceof let new null of return set static super switch this throw true try typeof undefined var void while with yield interface implements enum private protected public readonly abstract declare namespace type keyof infer never unknown any number string boolean symbol bigint');
const PYTHON_KEYWORDS = keywordSet('and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield match case');
const SHELL_KEYWORDS = keywordSet('alias bg bind break builtin caller case cd command compgen complete continue declare dirs disown echo enable eval exec exit export false fc fg for function getopts hash help history if in jobs kill let local logout mapfile popd printf pushd pwd read readonly return select set shift shopt source suspend test then time times trap true type typeset ulimit umask unalias unset until wait while do done elif else esac fi');
const SQL_KEYWORDS = keywordSet('add all alter analyze and as asc begin between by case check column commit constraint create database default delete desc distinct drop else end exists explain false foreign from full grant group having index inner insert into is join key left like limit not null on or order outer primary procedure references return revoke right rollback row select set table then trigger true union unique update values view when where with');
const LANGUAGE_ALIASES = {
springboot: 'java', js: 'javascript', ts: 'typescript', py: 'python', rb: 'ruby',
sh: 'bash', shell: 'bash', ps1: 'powershell', cs: 'csharp', html5: 'html', yml: 'yaml',
};
const LANGUAGE_KEYWORDS = {
springboot: new Set([
'package', 'import', 'public', 'private', 'protected', 'class', 'interface',
'static', 'final', 'new', 'return', 'if', 'else', 'try', 'catch', 'finally',
'throw', 'throws', 'void', 'long', 'int', 'boolean', 'null', 'true', 'false',
'extends', 'implements', 'this',
]),
rust: new Set([
'use', 'pub', 'struct', 'enum', 'impl', 'async', 'await', 'fn', 'let', 'mut',
'loop', 'match', 'if', 'else', 'return', 'while', 'for', 'in', 'break', 'continue',
'crate', 'self', 'super', 'mod', 'where', 'Result', 'Ok', 'Err',
]),
cpp: new Set([
'include', 'template', 'typename', 'concept', 'requires', 'struct', 'class',
'public', 'private', 'protected', 'explicit', 'void', 'auto', 'const', 'constexpr',
'if', 'else', 'while', 'for', 'return', 'using', 'namespace', 'std', 'nullptr',
'true', 'false',
]),
python: new Set([
'import', 'from', 'as', 'class', 'def', 'return', 'if', 'elif', 'else', 'for',
'while', 'in', 'and', 'or', 'not', 'await', 'async', 'with', 'try', 'except',
'finally', 'lambda', 'None', 'True', 'False', 'yield',
]),
javascript: new Set([
'class', 'constructor', 'return', 'if', 'else', 'while', 'for', 'const', 'let',
'var', 'new', 'async', 'await', 'try', 'catch', 'finally', 'throw', 'export',
'import', 'from', 'this', 'null', 'true', 'false', 'yield', 'function',
]),
nim: new Set([
'import', 'type', 'enum', 'object', 'ref', 'proc', 'result', 'let', 'var',
'if', 'elif', 'else', 'for', 'in', 'while', 'return', 'await', 'defer',
'mod', 'echo', 'discard',
]),
c: C_KEYWORDS,
cpp: CPP_KEYWORDS,
objectivec: mergeKeywordSets(C_KEYWORDS, keywordSet('id self super nil YES NO interface implementation protocol property synthesize dynamic selector encode end autoreleasepool')),
csharp: keywordSet('abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly record ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void volatile while async await var dynamic get set init value yield'),
java: JVM_KEYWORDS,
kotlin: JVM_KEYWORDS,
scala: JVM_KEYWORDS,
groovy: JVM_KEYWORDS,
javascript: JS_KEYWORDS,
typescript: JS_KEYWORDS,
jsx: JS_KEYWORDS,
tsx: JS_KEYWORDS,
vue: JS_KEYWORDS,
python: PYTHON_KEYWORDS,
go: keywordSet('break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var true false nil'),
rust: keywordSet('as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while abstract become box do final macro override priv typeof unsized virtual yield try'),
php: keywordSet('abstract and array as break callable case catch class clone const continue declare default die do echo else elseif empty enddeclare endfor endforeach endif endswitch endwhile enum eval exit extends final finally fn for foreach function global goto if implements include include_once instanceof insteadof interface isset list match namespace new null or print private protected public readonly require require_once return static switch throw trait true try unset use var while xor yield'),
ruby: keywordSet('alias and begin break case class def defined do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield'),
swift: keywordSet('associatedtype class deinit enum extension fileprivate func import init inout internal let open operator private protocol public rethrows static struct subscript typealias var break case continue default defer do else fallthrough for guard if in repeat return switch where while as Any catch false is nil super self Self throw throws true try async await actor some'),
dart: keywordSet('abstract as assert async await break case catch class const continue covariant default deferred do dynamic else enum export extends extension external factory false final finally for function get hide if implements import in interface is late library mixin new null on operator part required rethrow return set show static super switch sync this throw true try typedef var void while with yield'),
lua: keywordSet('and break do else elseif end false for function goto if in local nil not or repeat return then true until while'),
perl: keywordSet('continue do else elsif eval for foreach given goto if last local my next no package redo require return state sub unless until use when while our'),
r: keywordSet('break else FALSE for function if Inf NA NaN next NULL repeat return TRUE while in'),
matlab: keywordSet('break case catch classdef continue else elseif end for function global if otherwise parfor persistent return spmd switch try while'),
julia: keywordSet('baremodule begin break catch const continue do else elseif end export false finally for function global if import let local macro module quote return struct true try using while abstract type primitive mutable where'),
nim: keywordSet('addr and as asm bind block break case cast concept const continue converter defer discard distinct div do elif else end enum except export finally for from func generic if import in include interface is isnot iterator let macro method mixin mod nil not object of or out proc ptr raise ref return shl shr static template try tuple type using var when while with without xor yield'),
haskell: keywordSet('as case class data default deriving do else family forall foreign hiding if import in infix infixl infixr instance let mdo module newtype of qualified rec then type where'),
elixir: keywordSet('after alias and case catch cond def defmodule defp defprotocol defstruct do else end fn for if import in not or quote raise receive require rescue try unless use when'),
erlang: keywordSet('after begin case catch cond end fun if let of query receive try when bnot not div rem band and bor bxor bsl bsr or xor'),
bash: SHELL_KEYWORDS,
powershell: keywordSet('begin break catch class continue data define do dynamicparam else elseif end enum exit filter finally for foreach from function hidden if in param process return static switch throw trap try until using var while workflow parallel sequence true false null'),
sql: SQL_KEYWORDS,
graphql: keywordSet('query mutation subscription fragment on schema scalar type interface union enum input directive extend implements repeatable true false null'),
solidity: keywordSet('abstract address after alias apply auto byte case catch constant constructor contract delete do else emit enum event external fallback false final finally for function if immutable implements import in indexed inline interface internal is mapping memory modifier new null of override payable private promise public pure receive reference relocatable return returns sealed sizeof static storage string struct supports switch this throw true try type typedef typeof unchecked using var view virtual while'),
html: keywordSet('doctype html head body main header footer nav section article aside div span canvas script style link meta title template slot form label input button select option textarea table thead tbody tr th td img svg path'),
xml: EMPTY_SET,
css: keywordSet('important inherit initial revert unset auto none block inline flex grid relative absolute fixed sticky hidden visible transparent currentColor media supports keyframes from to'),
scss: keywordSet('mixin include extend function return if else each for while use forward import debug warn error at-root'),
less: keywordSet('import media supports plugin inline reference optional multiple when extend all'),
dockerfile: keywordSet('ADD ARG CMD COPY ENTRYPOINT ENV EXPOSE FROM HEALTHCHECK LABEL MAINTAINER ONBUILD RUN SHELL STOPSIGNAL USER VOLUME WORKDIR AS'),
json: keywordSet('true false null'),
yaml: keywordSet('true false null yes no on off'),
toml: keywordSet('true false'),
markdown: EMPTY_SET,
plaintext: EMPTY_SET,
};
const LINE_COMMENT_MARKERS = {
bash: ['#'], powershell: ['#'], python: ['#'], ruby: ['#'], r: ['#'], perl: ['#'], yaml: ['#'], toml: ['#'], dockerfile: ['#'],
php: ['//', '#'], sql: ['--'], lua: ['--'], haskell: ['--'],
};
const normalizeLanguage = (language) => {
const key = String(language || 'plaintext').toLowerCase().replace(/[^a-z0-9#+-]/g, '');
return LANGUAGE_ALIASES[key] || key || 'plaintext';
};
const findLineCommentIndex = (line, language) => {
const markers = LINE_COMMENT_MARKERS[language]
|| (['html', 'xml', 'css', 'scss', 'less', 'json', 'markdown', 'plaintext'].includes(language) ? [] : ['//']);
if (!markers.length) return -1;
let quote = '';
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
if (quote) {
if (char === '\\') {
index += 1;
} else if (char === quote) {
quote = '';
}
continue;
}
if (char === '"' || char === "'" || char === '`') {
quote = char;
continue;
}
if (markers.some((marker) => line.slice(index, index + marker.length) === marker)) return index;
}
return -1;
};
const classifyToken = (token, language, nextToken, prevToken, atLineStart) => {
const keywords = LANGUAGE_KEYWORDS[language] || EMPTY_SET;
const normalizedLanguage = normalizeLanguage(language);
const keywords = LANGUAGE_KEYWORDS[normalizedLanguage] || EMPTY_SET;
if (token.startsWith('//')) return 'comment';
if (token.startsWith('/*') || token.startsWith('<!--')) return 'comment';
if (token.startsWith('#[') || token.startsWith('@')) return 'annotation';
if (STRING_RE.test(token)) return 'string';
if (NUMBER_RE.test(token)) return 'number';
if (OPERATOR_RE.test(token)) return 'operator';
if (VARIABLE_RE.test(token)) return 'field';
if (!WORD_RE.test(token)) {
return 'plain';
}
if (keywords.has(token)) return 'keyword';
if (language === 'cpp' && atLineStart && prevToken === '#' && token === 'include') return 'keyword';
if ((normalizedLanguage === 'c' || normalizedLanguage === 'cpp' || normalizedLanguage === 'objectivec') && atLineStart && prevToken === '#' && token === 'include') return 'keyword';
if (CONSTANT_RE.test(token)) return 'constant';
if (nextToken === '(') return 'method';
if (TYPE_RE.test(token)) return 'type';
@@ -84,8 +150,13 @@
const tokenizeLine = (line, language) => {
if (line.length === 0) return [['', 'plain']];
const matches = [...line.matchAll(TOKEN_RE)];
if (matches.length === 0) return [[line, 'plain']];
const normalizedLanguage = normalizeLanguage(language);
const commentIndex = findLineCommentIndex(line, normalizedLanguage);
const code = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
const comment = commentIndex >= 0 ? line.slice(commentIndex) : '';
const matches = [...code.matchAll(TOKEN_RE)];
if (matches.length === 0) return comment ? [[code, 'plain'], [comment, 'comment']] : [[line, 'plain']];
const tokens = [];
let cursor = 0;
@@ -96,24 +167,21 @@
const raw = match[0];
const nextToken = i + 1 < matches.length ? matches[i + 1][0] : '';
const prevToken = i > 0 ? matches[i - 1][0] : '';
const atLineStart = line.slice(0, start).trim().length === 0;
const atLineStart = code.slice(0, start).trim().length === 0;
if (start > cursor) {
tokens.push([line.slice(cursor, start), 'plain']);
tokens.push([code.slice(cursor, start), 'plain']);
}
if ((language === 'python' || language === 'nim') && raw === '#') {
tokens.push([line.slice(start), 'comment']);
cursor = line.length;
break;
}
tokens.push([raw, classifyToken(raw, language, nextToken, prevToken, atLineStart)]);
tokens.push([raw, classifyToken(raw, normalizedLanguage, nextToken, prevToken, atLineStart)]);
cursor = start + raw.length;
}
if (cursor < line.length) {
tokens.push([line.slice(cursor), 'plain']);
if (cursor < code.length) {
tokens.push([code.slice(cursor), 'plain']);
}
if (comment) {
tokens.push([comment, 'comment']);
}
if (tokens.length === 0) {
@@ -540,7 +608,22 @@
},
];
const snippetCatalog = rawSnippetCatalog.map((entry) => ({
const configuredSnippets = Array.isArray(window.ITStudioLandingHero?.snippets)
? window.ITStudioLandingHero.snippets
.filter((entry) => entry && typeof entry.code === 'string' && entry.code.trim() !== '')
.map((entry) => {
const lines = entry.code.replace(/\r\n?/g, '\n').split('\n').slice(0, 240);
return {
name: normalizeLanguage(entry.language),
title: typeof entry.title === 'string' ? entry.title : '',
repeat: Math.min(8, Math.max(1, Math.ceil(160 / Math.max(1, lines.length)))),
lines,
};
})
: [];
const sourceSnippetCatalog = configuredSnippets.length ? configuredSnippets : rawSnippetCatalog;
const snippetCatalog = sourceSnippetCatalog.map((entry) => ({
...entry,
lines: tokenizeSnippet(entry.lines, entry.name),
}));
+207 -2
View File
@@ -87,6 +87,7 @@ function itstudio_get_site_content_defaults() {
array('name_cn' => '五八工坊预约', 'name_en' => 'Workshop Booking', 'image_id' => 0),
array('name_cn' => 'OUC便民服务', 'name_en' => 'OUC Services', 'image_id' => 0),
),
'homepage_code_snippets' => array(),
);
}
@@ -105,6 +106,57 @@ function itstudio_get_site_content_settings() {
'footer_icp' => isset($stored['footer_icp']) && is_array($stored['footer_icp']) ? array_merge($defaults['footer_icp'], $stored['footer_icp']) : array_merge($defaults['footer_icp'], array()),
'footer_copyright' => isset($stored['footer_copyright']) && is_array($stored['footer_copyright']) ? array_merge($defaults['footer_copyright'], $stored['footer_copyright']) : array_merge($defaults['footer_copyright'], array()),
'homepage_services' => isset($stored['homepage_services']) && is_array($stored['homepage_services']) ? $stored['homepage_services'] : array(),
'homepage_code_snippets' => isset($stored['homepage_code_snippets']) && is_array($stored['homepage_code_snippets']) ? $stored['homepage_code_snippets'] : array(),
);
}
function itstudio_get_code_animation_languages() {
return array(
'plaintext' => __('纯文本', 'itstudio'),
'bash' => 'Bash / Shell',
'powershell' => 'PowerShell',
'c' => 'C',
'cpp' => 'C++',
'csharp' => 'C#',
'java' => 'Java / Spring Boot',
'kotlin' => 'Kotlin',
'scala' => 'Scala',
'groovy' => 'Groovy',
'javascript' => 'JavaScript',
'typescript' => 'TypeScript',
'jsx' => 'JSX',
'tsx' => 'TSX',
'vue' => 'Vue',
'python' => 'Python',
'go' => 'Go',
'rust' => 'Rust',
'php' => 'PHP',
'ruby' => 'Ruby',
'swift' => 'Swift',
'objectivec' => 'Objective-C',
'dart' => 'Dart',
'lua' => 'Lua',
'perl' => 'Perl',
'r' => 'R',
'matlab' => 'MATLAB',
'julia' => 'Julia',
'nim' => 'Nim',
'haskell' => 'Haskell',
'elixir' => 'Elixir',
'erlang' => 'Erlang',
'sql' => 'SQL',
'html' => 'HTML',
'xml' => 'XML',
'css' => 'CSS',
'scss' => 'SCSS',
'less' => 'Less',
'json' => 'JSON',
'yaml' => 'YAML',
'toml' => 'TOML',
'markdown' => 'Markdown',
'dockerfile' => 'Dockerfile',
'graphql' => 'GraphQL',
'solidity' => 'Solidity',
);
}
@@ -134,6 +186,7 @@ function itstudio_sanitize_site_content_settings($input) {
'text_en' => '',
),
'homepage_services' => array(),
'homepage_code_snippets' => array(),
);
foreach (array('footer_links' => 8, 'footer_contacts' => 4) as $key => $limit) {
@@ -199,6 +252,35 @@ function itstudio_sanitize_site_content_settings($input) {
}
}
$languages = itstudio_get_code_animation_languages();
$snippet_rows = isset($input['homepage_code_snippets']) && is_array($input['homepage_code_snippets']) ? $input['homepage_code_snippets'] : array();
foreach (array_slice($snippet_rows, 0, 12) as $row) {
if (!is_array($row)) {
continue;
}
$language = sanitize_key($row['language'] ?? 'plaintext');
if (!isset($languages[$language])) {
$language = 'plaintext';
}
$code = isset($row['code']) ? (string) wp_unslash($row['code']) : '';
$code = str_replace(array("\r\n", "\r", "\0"), array("\n", "\n", ''), $code);
if (strlen($code) > 20000) {
$code = substr($code, 0, 20000);
}
if (trim($code) === '') {
continue;
}
$clean['homepage_code_snippets'][] = array(
'title' => sanitize_text_field($row['title'] ?? ''),
'language' => $language,
'code' => $code,
);
}
return $clean;
}
@@ -213,7 +295,7 @@ function itstudio_register_site_content_settings() {
'itstudio_site_content_intro',
__('站点内容', 'itstudio'),
static function () {
echo '<p>' . esc_html__('管理页脚内容和首页“服务提供”模块。留空的项目不会在前台显示。', 'itstudio') . '</p>';
echo '<p>' . esc_html__('管理页脚内容、首页“服务提供”和代码打字动画。留空的项目不会在前台显示。', 'itstudio') . '</p>';
},
'itstudio-site-content'
);
@@ -223,6 +305,7 @@ function itstudio_register_site_content_settings() {
add_settings_field('itstudio_footer_addresses', __('办公地址', 'itstudio'), 'itstudio_render_footer_addresses_field', 'itstudio-site-content', 'itstudio_site_content_intro');
add_settings_field('itstudio_footer_bottom', __('备案与版权', 'itstudio'), 'itstudio_render_footer_bottom_field', 'itstudio-site-content', 'itstudio_site_content_intro');
add_settings_field('itstudio_homepage_services', __('首页服务提供', 'itstudio'), 'itstudio_render_homepage_services_field', 'itstudio-site-content', 'itstudio_site_content_intro');
add_settings_field('itstudio_homepage_code_snippets', __('首页代码动画', 'itstudio'), 'itstudio_render_homepage_code_snippets_field', 'itstudio-site-content', 'itstudio_site_content_intro');
}
add_action('admin_init', 'itstudio_register_site_content_settings');
@@ -316,6 +399,58 @@ function itstudio_render_homepage_services_field() {
}
}
function itstudio_render_homepage_code_snippet_row($index, $row = array()) {
$base = 'itstudio_site_content_settings[homepage_code_snippets][' . $index . ']';
$language = sanitize_key($row['language'] ?? 'javascript');
$languages = itstudio_get_code_animation_languages();
if (!isset($languages[$language])) {
$language = 'plaintext';
}
?>
<div class="itstudio-code-snippet-row">
<div class="itstudio-code-snippet-toolbar">
<input type="text" class="regular-text itstudio-code-snippet-title" name="<?php echo esc_attr($base); ?>[title]" value="<?php echo esc_attr($row['title'] ?? ''); ?>" placeholder="<?php esc_attr_e('程序名称,例如 app.py', 'itstudio'); ?>">
<select name="<?php echo esc_attr($base); ?>[language]" aria-label="<?php esc_attr_e('代码语言', 'itstudio'); ?>">
<?php foreach ($languages as $value => $label) : ?>
<option value="<?php echo esc_attr($value); ?>" <?php selected($language, $value); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
<span class="itstudio-code-snippet-actions">
<button type="button" class="button itstudio-move-code-snippet-up" aria-label="<?php esc_attr_e('上移程序', 'itstudio'); ?>">↑</button>
<button type="button" class="button itstudio-move-code-snippet-down" aria-label="<?php esc_attr_e('下移程序', 'itstudio'); ?>">↓</button>
<button type="button" class="button-link-delete itstudio-remove-code-snippet"><?php esc_html_e('删除', 'itstudio'); ?></button>
</span>
</div>
<textarea class="large-text code" rows="10" spellcheck="false" name="<?php echo esc_attr($base); ?>[code]" placeholder="<?php esc_attr_e('粘贴需要在首页动画中展示的代码', 'itstudio'); ?>"><?php echo esc_textarea($row['code'] ?? ''); ?></textarea>
</div>
<?php
}
function itstudio_render_homepage_code_snippets_field() {
$settings = itstudio_get_site_content_settings();
$rows = isset($settings['homepage_code_snippets']) && is_array($settings['homepage_code_snippets']) ? array_values($settings['homepage_code_snippets']) : array();
?>
<p class="description"><?php esc_html_e('最多 12 个程序。首页每次随机播放其中一个;未添加时继续使用主题内置示例。单个程序最多保存 20,000 个字符。', 'itstudio'); ?></p>
<div id="itstudio-code-snippets" data-max="12">
<?php foreach ($rows as $index => $row) : ?>
<?php itstudio_render_homepage_code_snippet_row($index, $row); ?>
<?php endforeach; ?>
</div>
<p><button type="button" class="button button-secondary" id="itstudio-add-code-snippet"><?php esc_html_e('添加程序', 'itstudio'); ?></button></p>
<template id="itstudio-code-snippet-template">
<?php itstudio_render_homepage_code_snippet_row('__INDEX__'); ?>
</template>
<style>
.itstudio-code-snippet-row{max-width:900px;margin:0 0 14px;padding:12px;border:1px solid #dcdcde;border-radius:4px;background:#fff}
.itstudio-code-snippet-toolbar{display:grid;grid-template-columns:minmax(180px,1fr) minmax(150px,220px) auto;gap:8px;align-items:center;margin-bottom:8px}
.itstudio-code-snippet-toolbar .regular-text{width:100%}
.itstudio-code-snippet-actions{display:flex;gap:6px;align-items:center;justify-content:flex-end}
.itstudio-code-snippet-row textarea{min-height:180px;resize:vertical;font-family:Consolas,Monaco,monospace}
@media (max-width:782px){.itstudio-code-snippet-toolbar{grid-template-columns:1fr}.itstudio-code-snippet-actions{justify-content:flex-start}}
</style>
<?php
}
function itstudio_render_site_content_page() {
if (!current_user_can('manage_options')) {
return;
@@ -352,7 +487,65 @@ function itstudio_site_content_admin_media_script() {
<script>
(function () {
var frame;
var snippetsContainer = document.getElementById('itstudio-code-snippets');
var snippetTemplate = document.getElementById('itstudio-code-snippet-template');
var addSnippetButton = document.getElementById('itstudio-add-code-snippet');
function refreshCodeSnippetRows() {
if (!snippetsContainer) return;
var rows = Array.prototype.slice.call(snippetsContainer.querySelectorAll('.itstudio-code-snippet-row'));
rows.forEach(function (row, index) {
row.querySelectorAll('[name]').forEach(function (field) {
field.name = field.name.replace(/\[homepage_code_snippets\]\[[^\]]+\]/, '[homepage_code_snippets][' + index + ']');
});
var upButton = row.querySelector('.itstudio-move-code-snippet-up');
var downButton = row.querySelector('.itstudio-move-code-snippet-down');
if (upButton) upButton.disabled = index === 0;
if (downButton) downButton.disabled = index === rows.length - 1;
});
if (addSnippetButton) {
addSnippetButton.disabled = rows.length >= Number(snippetsContainer.dataset.max || 12);
}
}
if (addSnippetButton && snippetsContainer && snippetTemplate) {
addSnippetButton.addEventListener('click', function () {
var count = snippetsContainer.querySelectorAll('.itstudio-code-snippet-row').length;
var max = Number(snippetsContainer.dataset.max || 12);
if (count >= max) return;
var html = snippetTemplate.innerHTML.replace(/__INDEX__/g, String(count));
snippetsContainer.insertAdjacentHTML('beforeend', html);
refreshCodeSnippetRows();
var rows = snippetsContainer.querySelectorAll('.itstudio-code-snippet-row');
var title = rows.length ? rows[rows.length - 1].querySelector('.itstudio-code-snippet-title') : null;
if (title) title.focus();
});
}
refreshCodeSnippetRows();
document.addEventListener('click', function (event) {
var removeSnippetButton = event.target.closest('.itstudio-remove-code-snippet');
var moveUpButton = event.target.closest('.itstudio-move-code-snippet-up');
var moveDownButton = event.target.closest('.itstudio-move-code-snippet-down');
if (removeSnippetButton && snippetsContainer) {
removeSnippetButton.closest('.itstudio-code-snippet-row').remove();
refreshCodeSnippetRows();
return;
}
if ((moveUpButton || moveDownButton) && snippetsContainer) {
var row = (moveUpButton || moveDownButton).closest('.itstudio-code-snippet-row');
var sibling = moveUpButton ? row.previousElementSibling : row.nextElementSibling;
if (sibling) {
if (moveUpButton) {
snippetsContainer.insertBefore(row, sibling);
} else {
snippetsContainer.insertBefore(sibling, row);
}
}
refreshCodeSnippetRows();
return;
}
var selectButton = event.target.closest('.itstudio-select-service-image');
var clearButton = event.target.closest('.itstudio-clear-service-image');
if (clearButton) {
@@ -496,7 +689,19 @@ function itstudio_enqueue_scripts() {
// 仅在首页加载首页样式
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.1', true);
wp_enqueue_script('itstudio-landing-hero-canvas', get_template_directory_uri() . '/assets/js/landing-hero-canvas.js', array(), '1.1.0', true);
$site_content = itstudio_get_site_content_settings();
$code_snippets = isset($site_content['homepage_code_snippets']) && is_array($site_content['homepage_code_snippets']) ? $site_content['homepage_code_snippets'] : array();
wp_localize_script('itstudio-landing-hero-canvas', 'ITStudioLandingHero', array(
'snippets' => array_values(array_map(static function ($snippet) {
return array(
'title' => (string) ($snippet['title'] ?? ''),
'language' => (string) ($snippet['language'] ?? 'plaintext'),
'code' => (string) ($snippet['code'] ?? ''),
);
}, $code_snippets)),
));
}
// 仅在工作室介绍页加载(包含 /about fallback)