feat(topics): restore topic taxonomy workflow

This commit is contained in:
kate522 committed 2026-11-10 22:31:11 +08:00
1 parent f3dd1ec3b1
commit a254cbd3e9
59 files changed
+771 -192

No files matched your search

@@ -60,6 +60,15 @@ export const pageDisplaySection = {
canonicalDomain: 'page_display',
section: 'tag',
}),
schemaPage({
key: 'page-display-topics',
name: 'PageDisplayTopics',
label: '专题页面',
route: '/page-display/topics',
title: '专题页面',
canonicalDomain: 'page_display',
section: 'topics',
}),
schemaPage({
key: 'page-display-user-home',
name: 'PageDisplayUserHome',
@@ -63,12 +63,13 @@ export const searchSchema = {
{ key: 'search_custom_key_s', label: '自定义匹配范围', type: 'switch', default: true, help: '启用后可配置搜索匹配字段范围' },
{ key: 'search_custom_key', label: '匹配字段', type: 'checkbox', default: ['title', 'content', 'excerpt', 'category', 'post_tag'], dependsOn: { key: 'search_custom_key_s', value: true }, options: [
{ label: '标题', value: 'title' }, { label: '内容', value: 'content' }, { label: '简介', value: 'excerpt' },
{ label: '分类', value: 'category' }, { label: '标签', value: 'post_tag' },
{ label: '分类', value: 'category' }, { label: '标签', value: 'post_tag' }, { label: '专题', value: 'topics' },
{ label: '评论', value: 'coment' }, { label: '作者昵称', value: 'user' },
] },
{ key: 'search_facets', label: '搜索筛选', type: 'fieldset', sanitizeType: 'array', default: {}, dependsOn: { key: 'search_ms_s', value: false }, help: '搜索结果中显示的筛选列表;启用 Meilisearch 后由搜索引擎生成关联筛选。', fields: [
{ key: 'category', label: '[文章] 分类', type: 'taxonomy_select', taxonomy: 'category', multiple: true, default: [] },
{ key: 'post_tag', label: '[文章] 标签', type: 'taxonomy_select', taxonomy: 'post_tag', multiple: true, default: [] },
{ key: 'topics', label: '[文章] 专题', type: 'taxonomy_select', taxonomy: 'topics', multiple: true, default: [] },
] },
],
},
@@ -18,7 +18,7 @@ export const filterSchema = {
key: 'filter_intro',
type: 'content',
ui_only: true,
content: '<p class="jio-hint"><b>文章高级筛选功能:</b>通过此功能可实现更加复杂、精细化的内容筛选<br/>添加筛选类型和内容值之后,编写文章时候,可以进行选择,且在前台也可以实现精准筛选<br/>例如影视网站:[类型]有[剧情/喜剧/悬疑/惊悚/犯罪],[地区]有[大陆/美国/日韩/港台/印度],[年份]有[2022/2021/2020/10年代/00年代]等<br/><span class="jio-warn">注意事项:添加类型key时候,只能使用英文加下划线,不能有空格,且尽量复杂一点(不能为tag,category等系统已存在的类型),避免与其他mate_key重复!类型名称不能为分类、标签等系统已存在的类型</span></p>',
content: '<p class="jio-hint"><b>文章高级筛选功能:</b>通过此功能可实现更加复杂、精细化的内容筛选<br/>添加筛选类型和内容值之后,编写文章时候,可以进行选择,且在前台也可以实现精准筛选<br/>例如影视网站:[类型]有[剧情/喜剧/悬疑/惊悚/犯罪],[地区]有[大陆/美国/日韩/港台/印度],[年份]有[2022/2021/2020/10年代/00年代]等<br/><span class="jio-warn">注意事项:添加类型key时候,只能使用英文加下划线,不能有空格,且尽量复杂一点(不能为tag,category,topics等系统已存在的类型),避免与其他mate_key重复!类型名称不能为分类、专题、标签等系统已存在的类型</span></p>',
},
{
key: 'custom_filter_show',
@@ -28,6 +28,7 @@ export const filterSchema = {
options: [
{ label: '分类页面显示', value: 'cat' },
{ label: '标签页面显示', value: 'tag' },
{ label: '专题页面显示', value: 'topics' },
],
description: '勾选后将会在对应页面默认显示全部筛选,您可以在下方为单独的分类添加自定义筛选',
},
@@ -87,6 +88,18 @@ export const filterSchema = {
{ key: 'dropdown', label: '下拉列表', type: 'switch', default: false, description: '用下拉框显示更多内容' },
],
},
{ key: 'topics_s', label: '专题筛选', type: 'switch', default: false, description: '在所选分类页显示的专题筛选列表' },
{
key: 'topics_lists',
label: ' ',
type: 'fieldset',
default: { lists: [], dropdown: false },
dependsOn: { key: 'topics_s', value: true },
fields: [
{ key: 'lists', label: '按钮列表', type: 'async_select', source: 'topics', multiple: true, sortable: true, default: [] },
{ key: 'dropdown', label: '下拉列表', type: 'switch', default: false },
],
},
{ key: 'orderby_s', label: '排序方式', type: 'switch', default: true, description: '在所选分类页显示排序方式按钮' },
{
key: 'orderby_lists',
@@ -13,11 +13,12 @@ export const listSchema = {
post_list_author: true,
post_list_time: true,
list_meta_show: 'like',
list_badge_show: ['pay', 'cat', 'tag'],
list_badge_show: ['pay', 'tag', 'topics', 'cat'],
list_show_type: 'separate',
list_type: 'thumb',
list_card_home: false,
list_card_tag: false,
list_card_topics: false,
list_card_author: false,
list_card_cat: [],
list_list_option: { style: 'null', img_position: 'left', scale: 70 },
@@ -39,7 +40,7 @@ export const listSchema = {
{ label: '发布时间', value: 'date' }, { label: '更新时间', value: 'modified' },
] },
{ key: 'sticky_s', label: '显示置顶文章', type: 'checkbox', default: ['home', 'cat'], options: [
{ label: '首页', value: 'home' }, { label: '分类页', value: 'cat' }, { label: '标签页', value: 'tag' },
{ label: '首页', value: 'home' }, { label: '分类页', value: 'cat' }, { label: '标签页', value: 'tag' }, { label: '专题页', value: 'topics' },
] },
],
},
@@ -52,8 +53,8 @@ export const listSchema = {
{ key: 'list_meta_show', label: '列表小部件(移动端优先)', type: 'radio', default: 'like', options: [
{ label: '阅读量', value: 'view' }, { label: '点赞数', value: 'like' }, { label: '评论', value: 'comm' },
] },
{ key: 'list_badge_show', label: '显示列表标签', type: 'checkbox', default: ['pay', 'cat', 'tag'], options: [
{ label: '付费信息', value: 'pay' }, { label: '分类', value: 'cat' }, { label: '标签', value: 'tag' },
{ key: 'list_badge_show', label: '显示列表标签', type: 'checkbox', default: ['pay', 'tag', 'topics', 'cat'], options: [
{ label: '付费信息', value: 'pay' }, { label: '分类', value: 'cat' }, { label: '专题', value: 'topics' }, { label: '标签', value: 'tag' },
] },
],
},
@@ -70,8 +71,9 @@ export const listSchema = {
], help: '文字、自动图文和多图模式仅在开启侧栏的页面有效。' },
{ key: 'list_card_home', label: '首页列表卡片模式', type: 'switch', default: false, dependsOn: { key: 'list_type', not: true, value: 'card' } },
{ key: 'list_card_tag', label: '标签页列表卡片模式', type: 'switch', default: false, dependsOn: { key: 'list_type', not: true, value: 'card' } },
{ key: 'list_card_topics', label: '专题页列表卡片模式', type: 'switch', default: false, dependsOn: { key: 'list_type', not: true, value: 'card' } },
{ key: 'list_card_author', label: '用户页列表卡片模式', type: 'switch', default: false, dependsOn: { key: 'list_type', not: true, value: 'card' } },
{ key: 'list_card_cat', label: '分类页自定义卡片模式', type: 'taxonomy_select', taxonomy: 'category', multiple: true, default: [], dependsOn: { key: 'list_type', not: true, value: 'card' } },
{ key: 'list_card_cat', label: '分类、专题页自定义卡片模式', type: 'taxonomy_select', taxonomy: 'category,topics', multiple: true, default: [], dependsOn: { key: 'list_type', not: true, value: 'card' } },
{ key: 'list_list_option', label: '列表模式 UI 配置', type: 'fieldset', default: { style: 'null', img_position: 'left', scale: 70 }, fields: [
{ key: 'style', label: '列表样式', type: 'radio', default: 'null', options: [
{ label: '默认', value: 'null' }, { label: '样式 2', value: 'style2' },
@@ -7,6 +7,8 @@ export const submitSchema = {
post_article_s: true,
post_article_btn_txte: '发布文章',
post_article_cat: [],
post_article_topics_s: false,
post_article_topics: [],
post_article_custom_filter_s: true,
post_article_tag: true,
post_article_user: 1,
@@ -34,6 +36,8 @@ export const submitSchema = {
},
{ key: 'post_article_btn_txte', label: '按钮文字', type: 'input', default: '发布文章', dependsOn: { key: 'post_article_s', value: true }, description: '投稿按钮的显示的文字,推荐4个字更加协调' },
{ key: 'post_article_cat', label: '投稿允许选择的分类', type: 'taxonomy_select', taxonomy: 'category', multiple: true, default: [], placeholder: '输入关键词以搜索分类', dependsOn: { key: 'post_article_s', value: true }, description: '投稿允许选择的分类,为空则允许选择全部分类,所选分类下如果有子分类则会自动显示,如果选择了上级分类,则不要再选该分类下的子分类。注意:请确保所选分类包含子分类总数不要超过50个,否则可能会引起页面崩溃' },
{ key: 'post_article_topics_s', label: '允许选择专题', type: 'switch', default: false, dependsOn: { key: 'post_article_s', value: true } },
{ key: 'post_article_topics', label: '投稿允许选择的专题', type: 'taxonomy_select', taxonomy: 'topics', multiple: true, default: [], dependsOn: { all: [{ key: 'post_article_s', value: true }, { key: 'post_article_topics_s', value: true }] } },
{ key: 'post_article_custom_filter_s', label: '允许选择高级筛选', type: 'switch', default: true, dependsOn: { key: 'post_article_s', value: true } },
{
key: 'submit_filter_notice',
@@ -19,9 +19,10 @@ type Props = {
options: CategoryOption[];
value: number[];
onChange: (ids: number[]) => void;
label?: string;
};
export function CategorySelect({ options, value, onChange }: Props) {
export function CategorySelect({ options, value, onChange, label = __('分类', 'jio') }: Props) {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const { triggerRef, panelRef, position } = useAnchoredPanel<HTMLDivElement>({
@@ -71,16 +72,16 @@ export function CategorySelect({ options, value, onChange }: Props) {
<input
type="search"
value={query}
placeholder={__('搜索分类', 'jio')}
aria-label={__('搜索分类', 'jio')}
placeholder={sprintf(__('搜索%s', 'jio'), label)}
aria-label={sprintf(__('搜索%s', 'jio'), label)}
// 打开即可输入:分类列表通常很长,键盘操作比翻找更快。
autoFocus
onChange={(event) => setQuery(event.target.value)}
/>
</div>
<div className="jio-creator-picker__list" aria-label={__('分类', 'jio')}>
<div className="jio-creator-picker__list" aria-label={label}>
{filtered.length === 0 && (
<p className="jio-creator-picker__empty">{__('没有匹配的分类', 'jio')}</p>
<p className="jio-creator-picker__empty">{sprintf(__('没有匹配的%s', 'jio'), label)}</p>
)}
{filtered.map((option) => {
const active = selected.has(option.id);
@@ -133,7 +134,7 @@ export function CategorySelect({ options, value, onChange }: Props) {
>
<span className="jio-creator-select__tokens">
{chosen.length === 0 ? (
<span className="jio-creator-select__placeholder">{__('选择分类(可多选)', 'jio')}</span>
<span className="jio-creator-select__placeholder">{sprintf(__('选择%s(可多选)', 'jio'), label)}</span>
) : (
chosen.map((option) => (
<span key={option.id} className="jio-creator-select__token">
@@ -143,7 +144,7 @@ export function CategorySelect({ options, value, onChange }: Props) {
<button
type="button"
className="jio-creator-select__token-x"
aria-label={sprintf(__('移除分类 %s', 'jio'), option.name)}
aria-label={sprintf(__('移除%s %s', 'jio'), label, option.name)}
onClick={(event) => {
// 令牌上的叉号只移除这一项,不要顺带展开面板。
event.stopPropagation();
@@ -19,6 +19,7 @@ type Featured = { type: string; data: Record<string, any>; mediaId?: number };
export type TaxonomyDef = {
categories: CategoryOption[];
topics: { enabled: boolean; items: CategoryOption[] };
/** 标签是否开放;开放时附带站点已有标签,用于输入建议。 */
tags: { enabled: boolean; suggestions: string[] };
customDefs: Array<{ key: string; name: string; values: Array<{ key: string; name: string }> }>;
@@ -34,12 +35,14 @@ type Props = {
canPay: boolean;
taxonomy: TaxonomyDef;
categories: number[];
topics: number[];
tags: string[];
custom: Record<string, string[]>;
onFeaturedChange: (featured: Featured) => void;
onScheduleChange: (enabled: boolean, at: string) => void;
onPayEnabledChange: (enabled: boolean) => void;
onCategoriesChange: (categories: number[]) => void;
onTopicsChange: (topics: number[]) => void;
onTagsChange: (tags: string[]) => void;
onCustomChange: (custom: Record<string, string[]>) => void;
/** 打开发布抽屉并直接定位到付费配置。 */
@@ -69,12 +72,14 @@ export function PublishSettings({
canPay,
taxonomy,
categories,
topics,
tags,
custom,
onFeaturedChange,
onScheduleChange,
onPayEnabledChange,
onCategoriesChange,
onTopicsChange,
onTagsChange,
onCustomChange,
onOpenPaySettings,
@@ -141,6 +146,13 @@ export function PublishSettings({
)}
</div>
{taxonomy.topics.enabled && taxonomy.topics.items.length > 0 && (
<div className="jio-creator__setting-row jio-creator__setting-row--stack">
<div className="jio-creator__setting-copy"><span className="jio-creator__setting-label">{__('专题', 'jio')}</span></div>
<CategorySelect options={taxonomy.topics.items} value={topics} onChange={onTopicsChange} label={__('专题', 'jio')} />
</div>
)}
{taxonomy.tags.enabled && (
<div className="jio-creator__setting-row jio-creator__setting-row--stack">
<div className="jio-creator__setting-copy">
+3
View File
@@ -57,6 +57,7 @@ export type Draft = {
content: string;
schedule: Schedule;
categories: number[];
topics: number[];
tags: string[];
custom: Record<string, string[]>;
featured: Featured;
@@ -66,6 +67,7 @@ export type Draft = {
export function initialDraft(bootstrap: Record<string, any>): Draft {
const pay = bootstrap.pay || {};
const selected = (bootstrap.categories && bootstrap.categories.selected) || [];
const selectedTopics = (bootstrap.topics && bootstrap.topics.selected) || [];
const tags = (bootstrap.tags && bootstrap.tags.items) || [];
return {
postId: Number(bootstrap.sourceId || 0),
@@ -75,6 +77,7 @@ export function initialDraft(bootstrap: Record<string, any>): Draft {
content: String(bootstrap.content || ''),
schedule: { enabled: false, at: '' },
categories: selected.map(Number),
topics: selectedTopics.map(Number),
tags: tags.slice(),
custom: bootstrap.custom || {},
featured: bootstrap.featured || { type: '', data: {} },
+7
View File
@@ -207,6 +207,7 @@ function Studio(): JSX.Element {
title: current.title,
content: current.content,
categories: current.categories,
topics: current.topics,
tags: current.tags,
custom: current.custom,
featured: current.featured,
@@ -859,6 +860,10 @@ function Studio(): JSX.Element {
canPay={!!capabilities.canPay}
taxonomy={{
categories: (bootstrap.categories && bootstrap.categories.items) || [],
topics: {
enabled: !!bootstrap.topics?.enabled,
items: (bootstrap.topics && bootstrap.topics.items) || [],
},
tags: {
enabled: !!bootstrap.tags?.enabled,
suggestions: (bootstrap.tags && bootstrap.tags.suggestions) || [],
@@ -866,12 +871,14 @@ function Studio(): JSX.Element {
customDefs: bootstrap.customDefs || [],
}}
categories={draft.categories}
topics={draft.topics}
tags={draft.tags}
custom={draft.custom}
onFeaturedChange={(featured) => patchDraft({ featured })}
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
onPayEnabledChange={(enabled) => patchDraft({ pay: { ...draftRef.current.pay, enabled } })}
onCategoriesChange={(categories) => patchDraft({ categories })}
onTopicsChange={(topics) => patchDraft({ topics })}
onTagsChange={(tags) => patchDraft({ tags })}
onCustomChange={(custom) => patchDraft({ custom })}
onOpenPaySettings={() => setDrawerOpen(true)}
+1 -1
View File
@@ -16,7 +16,7 @@
defined('ABSPATH') || exit;
define('JIO_VERSION', '3.4.1-dev');
define('JIO_VERSION', '3.4.2-dev');
define('JIO_DIR', get_template_directory());
define('JIO_URI', get_template_directory_uri());
-5
View File
@@ -385,11 +385,6 @@ add_action('pre_get_posts', function ($query) {
// 站内搜索:分类/标签名搜索(内容类型始终为 post)
if ($query->is_search() && empty($query->get('post_type'))) {
$search_type = sanitize_key((string) $query->get('type'));
$trem = absint($query->get('trem'));
if ($trem > 0) {
$query->set('cat', $trem);
}
if (in_array($search_type, ['category', 'tag'], true)) {
$taxonomy = $search_type === 'category' ? 'category' : 'post_tag';
$term_ids = get_terms([
@@ -80,6 +80,9 @@ if (!function_exists('jio_header_slide_current_page_type')) {
if (is_tag()) {
return 'tag';
}
if (function_exists('is_tax') && is_tax('topics')) {
return 'topics';
}
if (is_singular('post')) {
return 'single';
}
+3 -1
View File
@@ -122,7 +122,9 @@ if (!function_exists('jio_homepage_get_tab_panes_html')) {
$html = '';
foreach ($tab_configs as $index => $tab_cfg) {
$config = jio_archive_get_render_config(['context' => 'category', 'term_id' => (int) $tab_cfg['term_id']]);
$term = $tab_cfg['term'] ?? null;
$context = $term instanceof WP_Term && $term->taxonomy === 'topics' ? 'topics' : 'category';
$config = jio_archive_get_render_config(['context' => $context, 'term_id' => (int) $tab_cfg['term_id']]);
$is_card = ($config['mode'] ?? 'list') === 'card';
$placeholder = jio_render_placeholder($is_card ? 'card' : 'list', $is_card ? (wp_is_mobile() ? 4 : 12) : (wp_is_mobile() ? 4 : 6));
$html .= '<div class="ajaxpager tab-pane fade" id="index-tab-' . (int) $index . '">';
+9 -6
View File
@@ -21,8 +21,10 @@ if (!function_exists('jio_archive_render_term_header')) :
}
$term = get_queried_object();
$is_tag = $term instanceof WP_Term && $term->taxonomy === 'post_tag';
$is_topic = $term instanceof WP_Term && $term->taxonomy === 'topics';
$paged = max(1, (int) get_query_var('paged', 1));
$title = '<i class="fa fas ' . ($is_tag ? 'fa-tags' : 'fa-folder-open') . ' em12 mr10 ml6" aria-hidden="true"></i>' . esc_html((string) $args['title']);
$icon = $is_topic ? 'fa-cube' : ($is_tag ? 'fa-tags' : 'fa-folder-open');
$title = '<i class="fa fas ' . $icon . ' em12 mr10 ml6" aria-hidden="true"></i>' . esc_html((string) $args['title']);
global $wp_query;
if (!empty($args['post_count']) && $paged === 1) {
$count = $wp_query instanceof WP_Query ? (int) $wp_query->found_posts : 0;
@@ -34,7 +36,7 @@ if (!function_exists('jio_archive_render_term_header')) :
$edit_url = !$is_tag && current_user_can('edit_term', (int) $term->term_id)
? get_edit_term_link((int) $term->term_id, $term->taxonomy)
: false;
$edit_label = __('编辑此分类', 'jio');
$edit_label = $is_topic ? __('编辑此专题', 'jio') : ($is_tag ? __('编辑此标签', 'jio') : __('编辑此分类', 'jio'));
$edit_button = $edit_url
? '<a class="admin-edit" href="' . esc_url($edit_url) . '" title="' . esc_attr($edit_label) . '"><i class="fa fas fa-edit" aria-hidden="true"></i><span class="screen-reader-text">' . esc_html($edit_label) . '</span></a>'
: '';
@@ -111,15 +113,15 @@ endif;
if (!function_exists('jio_archive_build_term_filter_group')) :
/**
* Build one ordered category/tag filter group from the page-display fieldset.
* Build one ordered term filter group from the page-display fieldset.
*
* @param string $taxonomy category|post_tag.
* @param string $taxonomy category|post_tag|topics.
* @param array<string,mixed> $option lists/dropdown/dropdown_lists fieldset.
* @param string $label Visible group label.
* @return array<string,mixed>
*/
function jio_archive_build_term_filter_group(string $taxonomy, array $option, string $label): array {
if (!in_array($taxonomy, ['category', 'post_tag'], true)) {
if (!in_array($taxonomy, ['category', 'post_tag', 'topics'], true)) {
return [];
}
@@ -169,7 +171,8 @@ if (!function_exists('jio_archive_build_term_filter_group')) :
}
}
if ($child_ids) {
$child_group = jio_archive_build_term_filter_group($taxonomy, ['lists' => array_unique($child_ids)], $taxonomy === 'category' ? __('子分类', 'jio') : __('子标签', 'jio'));
$child_label = $taxonomy === 'topics' ? __('子专题', 'jio') : ($taxonomy === 'category' ? __('子分类', 'jio') : __('子标签', 'jio'));
$child_group = jio_archive_build_term_filter_group($taxonomy, ['lists' => array_unique($child_ids)], $child_label);
if ($child_group) {
$group['children'][] = $child_group;
}
+3
View File
@@ -37,6 +37,8 @@ if (!function_exists('jio_resolve_post_list_render_config')) :
$context = 'category';
} elseif (is_tag()) {
$context = 'tag';
} elseif (function_exists('is_tax') && is_tax('topics')) {
$context = 'topics';
} elseif (is_author()) {
$context = 'author';
} elseif (is_search()) {
@@ -55,6 +57,7 @@ if (!function_exists('jio_resolve_post_list_render_config')) :
'is_home' => $context === 'home',
'is_category' => $context === 'category',
'is_tag' => $context === 'tag',
'is_topics' => $context === 'topics',
'is_author' => $context === 'author',
'is_search' => $context === 'search',
];
+2 -2
View File
@@ -48,7 +48,7 @@ if (!function_exists('jio_homepage_get_post_types')) {
if (!function_exists('jio_homepage_get_tab_configs')) {
/**
* Homepage category tabs with full config.
* Homepage term tabs with full config.
*
* Returns enabled tabs in config order, each with 'term', 'title', 'orderby'.
* Preserves every configured row in the saved order.
@@ -76,7 +76,7 @@ if (!function_exists('jio_homepage_get_tab_configs')) {
if (!$term || is_wp_error($term)) {
continue;
}
if ($term->taxonomy !== 'category' || (int) $term->count === 0) {
if (!in_array($term->taxonomy, ['category', 'topics'], true) || (int) $term->count === 0) {
continue;
}
+9 -2
View File
@@ -220,7 +220,7 @@ endif;
if (!function_exists('jio_get_single_article_tags_html')) :
/**
* Categories → tags.
* Categories, topics and tags.
*/
function jio_get_single_article_tags_html(int $post_id): string {
$cats = get_the_category($post_id) ?: [];
@@ -230,6 +230,13 @@ if (!function_exists('jio_get_single_article_tags_html')) :
'<i class="fa fas fa-folder-open" aria-hidden="true"></i> ',
['c-blue', 'c-yellow', 'c-green', 'c-purple', 'c-red']
);
$topics = get_the_terms($post_id, 'topics');
$topic_html = is_array($topics) ? jio_get_colored_term_tags(
$topics,
'but ml6 radius',
'<i class="fa fas fa-cube" aria-hidden="true"></i> ',
['c-blue', 'c-yellow', 'c-green', 'c-purple', 'c-red']
) : '';
$tags_html = '';
$tags = get_the_tags($post_id) ?: [];
@@ -246,7 +253,7 @@ if (!function_exists('jio_get_single_article_tags_html')) :
}
}
$top = $cat_html;
$top = $cat_html . $topic_html;
if ($top === '' && $tags_html === '') {
return '';
}
+32 -16
View File
@@ -40,13 +40,16 @@ endif;
if (!function_exists('jio_widget_term_cover_url')) :
/**
* Category cover image URL, resolved through the theme's canonical chain:
* Term cover image URL, resolved through the theme's canonical chain:
* explicit term cover → latest thumbnailed post inside the term → optional
* default placeholder. The reference hides term cards without a real cover
* while the aggregation card always falls back to a default image, so the
* last step is opt-in via $default_fallback.
* default placeholder. The last step is opt-in via $default_fallback.
*/
function jio_widget_term_cover_url(int $term_id, bool $default_fallback = false): string {
function jio_widget_term_cover_url(int $term_id, bool $default_fallback = false, string $taxonomy = 'category'): string {
$taxonomy = $taxonomy === 'topics' ? 'topics' : 'category';
$cover = class_exists('Jio\\Core\\TermMeta') ? \Jio\Core\TermMeta::get_meta_value($term_id, 'cover_url', '') : '';
if (is_string($cover) && $cover !== '') {
return esc_url_raw($cover);
}
$cover_id = (int) get_term_meta($term_id, 'jio_category_cover_id', true);
if ($cover_id > 0) {
$url = wp_get_attachment_url($cover_id);
@@ -54,21 +57,29 @@ if (!function_exists('jio_widget_term_cover_url')) :
return (string) $url;
}
}
$posts = get_posts([
'category__in' => [$term_id],
$term_query = $taxonomy === 'topics'
? ['tax_query' => [['taxonomy' => 'topics', 'field' => 'term_id', 'terms' => [$term_id]]]]
: ['category__in' => [$term_id]];
$posts = get_posts(array_merge($term_query, [
'posts_per_page' => 1,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
'meta_query' => [['key' => '_thumbnail_id', 'compare' => 'EXISTS']],
'fields' => 'ids',
]);
]));
if (!empty($posts)) {
$url = get_the_post_thumbnail_url((int) $posts[0], 'medium_large');
if ($url) {
return (string) $url;
}
}
if ($default_fallback && $taxonomy === 'topics' && class_exists('Jio\\Settings\\Settings')) {
$settings = \Jio\Settings\Settings::get_topics_page_settings();
if (!empty($settings['default_cover'])) {
return esc_url_raw((string) $settings['default_cover']);
}
}
return $default_fallback ? get_template_directory_uri() . '/assets/img/placeholder.svg' : '';
}
endif;
@@ -190,20 +201,22 @@ endif;
if (!function_exists('jio_widget_term_aggregation')) :
/**
* Category aggregation card: cover + description + latest posts list.
* Term aggregation card: cover + description + latest posts list.
*
* @param array<string,mixed> $args {term_id, target_blank, orderby, order, count}.
* @param array<string,mixed> $args {term_id, taxonomy, target_blank, orderby, order, count}.
*/
function jio_widget_term_aggregation(array $args = []): string {
$args = wp_parse_args($args, [
'term_id' => 0,
'taxonomy' => 'category',
'target_blank' => false,
'orderby' => 'modified',
'order' => 'DESC',
'count' => 4,
]);
$term_id = (int) $args['term_id'];
$term = $term_id > 0 ? get_term($term_id, 'category') : null;
$taxonomy = $args['taxonomy'] === 'topics' ? 'topics' : 'category';
$term = $term_id > 0 ? get_term($term_id, $taxonomy) : null;
if (!$term instanceof WP_Term || is_wp_error($term)) {
return '';
}
@@ -214,11 +227,11 @@ if (!function_exists('jio_widget_term_aggregation')) :
}
$href = esc_url((string) $href);
$img = jio_widget_term_cover_url((int) $term->term_id, true);
$img = jio_widget_term_cover_url((int) $term->term_id, true, $taxonomy);
$img_html = jio_graphic_card([
'class' => '',
'img' => $img,
'alt' => __('分类', 'jio') . '-' . $term->name,
'alt' => ($taxonomy === 'topics' ? __('专题', 'jio') : __('分类', 'jio')) . '-' . $term->name,
'link' => ['url' => $href, 'target' => !empty($args['target_blank']) ? '_blank' : ''],
'height_scale' => 0,
'mask_opacity' => 0,
@@ -229,13 +242,16 @@ if (!function_exists('jio_widget_term_aggregation')) :
$name = '<a class="em14 key-color text-ellipsis"' . $target . ' href="' . $href . '">' . esc_html($term->name) . '</a>';
$description = wp_kses_post((string) $term->description);
if ($description === '' && current_user_can('manage_categories')) {
$description = esc_html__('请在WordPress后台-文章-文章分类中添加描述!', 'jio')
. '<a href="' . esc_url(get_edit_term_link($term_id, 'category')) . '">' . esc_html__('立即编辑', 'jio') . '</a>';
$description = $taxonomy === 'topics'
? esc_html__('请在WordPress后台-文章-专题中添加描述!', 'jio')
: esc_html__('请在WordPress后台-文章-文章分类中添加描述!', 'jio');
$description .= '<a href="' . esc_url(get_edit_term_link($term_id, $taxonomy)) . '">' . esc_html__('立即编辑', 'jio') . '</a>';
}
$description = '<div class="text-ellipsis-2 muted-color">' . $description . '</div>';
$query_args = [
'cat' => (string) $term->term_id,
'topics' => $taxonomy === 'topics' ? (string) $term->term_id : '',
'cat' => $taxonomy === 'category' ? (string) $term->term_id : '',
'orderby' => (string) $args['orderby'],
'order' => strtoupper((string) $args['order']) === 'ASC' ? 'ASC' : 'DESC',
'count' => max(1, (int) $args['count']),
+3 -1
View File
@@ -23,6 +23,7 @@ if (!function_exists('jio_renderer_defaults')) {
'is_home' => is_home() || is_front_page(),
'is_author' => is_author(),
'is_tag' => is_tag(),
'is_topics' => function_exists('is_tax') && is_tax('topics'),
'highlight_query' => '',
'wrapper_class' => '',
'skip_wrapper' => false,
@@ -56,6 +57,7 @@ if (!function_exists('jio_renderer_config')) {
}
if (!$is_forced && !$is_card) {
$is_card = (!empty($args['is_tag']) && !empty($list['list_card_tag']))
|| (!empty($args['is_topics']) && !empty($list['list_card_topics']))
|| (!empty($args['is_home']) && !empty($list['list_card_home']))
|| (!empty($args['is_author']) && !empty($list['list_card_author']));
}
@@ -83,7 +85,7 @@ if (!function_exists('jio_renderer_config')) {
$args['show_time'] = !empty($list['post_list_time']);
$args['time_source'] = (string) ($list['post_time_source'] ?? 'modified');
$args['list_orderby'] = (string) ($list['list_orderby'] ?? 'modified');
$args['badge_types'] = (array) ($list['list_badge_show'] ?? ['pay', 'cat', 'tag']);
$args['badge_types'] = (array) ($list['list_badge_show'] ?? ['pay', 'topics', 'cat', 'tag']);
$args['mult_thumb_cat'] = (array) ($list['mult_thumb_cat'] ?? []);
$args['thumb'] = $thumb;
$args['allow_conditional_layout'] = jio_is_show_sidebar() || wp_is_mobile();
+12
View File
@@ -17,6 +17,8 @@ if (!function_exists('jio_renderer_collect_post_data')) {
$images = jio_renderer_resolve_images($post_id, $cfg);
$categories = get_the_category($post_id) ?: [];
$topics = get_the_terms($post_id, 'topics');
$topics = is_array($topics) ? $topics : [];
$tags = get_the_tags($post_id) ?: [];
$content_images = (array) $images['gallery'];
$gallery = array_slice($content_images, 0, 4);
@@ -84,6 +86,7 @@ if (!function_exists('jio_renderer_collect_post_data')) {
'is_mult_thumb' => $is_mult_thumb,
'is_no_thumb' => $is_no_thumb,
'categories' => $categories,
'topics' => $topics,
'tags' => $tags,
'author_id' => $author_id,
'author_name' => $author instanceof WP_User ? $author->display_name : '',
@@ -261,6 +264,15 @@ if (!function_exists('jio_renderer_badges')) {
$items[] = '<a class="but ' . esc_attr($colors[$index % count($colors)]) . '" title="' . esc_attr__('查看更多分类文章', 'jio') . '" href="' . esc_url(get_category_link($category->term_id)) . '"><i class="fa fas fa-folder-open" aria-hidden="true"></i>' . esc_html($category->name) . '</a>';
}
}
if (empty($cfg['is_topics']) && in_array('topics', $types, true)) {
foreach (array_slice((array) ($data['topics'] ?? []), 0, 3) as $topic) {
$link = get_term_link($topic);
if (is_wp_error($link)) {
continue;
}
$items[] = '<a class="but" title="' . esc_attr__('查看此专题更多文章', 'jio') . '" href="' . esc_url($link) . '"><i class="fa fas fa-cube" aria-hidden="true"></i>' . esc_html($topic->name) . '</a>';
}
}
if (empty($cfg['is_tag']) && in_array('tag', $types, true)) {
$tags = (array) $data['tags'];
usort($tags, static fn($left, $right): int => (int) $right->count <=> (int) $left->count);
+2 -1
View File
@@ -298,7 +298,7 @@ function jio_get_search_facets_data(): array {
return [];
}
$allowed = ['category', 'post_tag'];
$allowed = ['category', 'post_tag', 'topics'];
$data = [];
foreach ($raw as $taxonomy => $ids) {
$taxonomy = sanitize_key((string) $taxonomy);
@@ -400,6 +400,7 @@ function jio_get_search_tab_nav_filter(string $type): string {
$labels = [
'category' => __('分类', 'jio'),
'post_tag' => __('标签', 'jio'),
'topics' => __('专题', 'jio'),
];
$current = isset($_REQUEST['trem']) ? absint(wp_unslash($_REQUEST['trem'])) : 0;
foreach (jio_get_search_facets_data() as $taxonomy => $terms) {
+6 -3
View File
@@ -28,6 +28,9 @@ function jio_resolve_sidebar_id(): string {
if (is_category()) {
return 'cat_sidebar';
}
if (function_exists('is_tax') && is_tax('topics')) {
return 'cat_sidebar';
}
if (is_tag()) {
return 'tag_sidebar';
@@ -183,7 +186,7 @@ function jio_is_show_sidebar_filter(): bool {
if (is_page() && empty($cfg['page'])) {
return false;
}
if (is_category() && empty($cfg['category'])) {
if ((is_category() || (function_exists('is_tax') && is_tax('topics'))) && empty($cfg['category'])) {
return false;
}
if (is_tag() && empty($cfg['tag'])) {
@@ -194,7 +197,7 @@ function jio_is_show_sidebar_filter(): bool {
}
// Category/tag term meta can still force off (existing Jio contract).
if ((is_category() || is_tag()) && class_exists('\Jio\Core\TermMeta') && method_exists('\Jio\Core\TermMeta', 'resolve_archive_settings')) {
if ((is_category() || is_tag() || (function_exists('is_tax') && is_tax('topics'))) && class_exists('\Jio\Core\TermMeta') && method_exists('\Jio\Core\TermMeta', 'resolve_archive_settings')) {
$term = get_queried_object();
if ($term instanceof \WP_Term) {
$resolved = \Jio\Core\TermMeta::resolve_archive_settings($term);
@@ -214,7 +217,7 @@ function jio_is_show_sidebar_filter(): bool {
if (is_page()) {
return !empty($cfg['page']);
}
if (is_category()) {
if (is_category() || (function_exists('is_tax') && is_tax('topics'))) {
return !empty($cfg['category']);
}
if (is_tag()) {
+3 -6
View File
@@ -3,11 +3,8 @@
* Admin post list enhancements: interaction + sales columns and the
* quick/bulk edit fields behind them.
*
* Mirrors the reference theme's post list: a sortable “销售数据” column, a
* product-type filter, and four quick-edit field groups (interaction data,
* article config, pay parameters, custom filters). The reference theme hooks
* those boxes onto its topics taxonomy column; this project has no topics
* taxonomy, so they hang off the interaction-data column instead.
* Adds a sortable sales column, a product-type filter, and quick-edit fields
* for interaction data, article config, pay parameters, and custom filters.
*
* @package Jio
*/
@@ -280,7 +277,7 @@ class PostListEnhance {
* @param string $post_type
*/
public function render_edit_boxes($column_name, $post_type): void {
if ($post_type !== 'post' || (string) $column_name !== self::COLUMN_STATS) {
if ($post_type !== 'post' || (string) $column_name !== 'taxonomy-topics') {
return;
}
+5 -4
View File
@@ -21,7 +21,7 @@ class PageDisplayController extends ControllerBase {
}
public function register_routes(): void {
register_rest_route(self::API_NAMESPACE, '/settings/schema/page_display/search/(?P<source>posts|categories|tags)', [
register_rest_route(self::API_NAMESPACE, '/settings/schema/page_display/search/(?P<source>posts|categories|tags|topics|terms)', [
'methods' => 'GET',
'callback' => [$this, 'search_options'],
'permission_callback' => [$this, 'check_permission'],
@@ -52,7 +52,7 @@ class PageDisplayController extends ControllerBase {
return $this->success(['data' => $items]);
}
$taxonomy = $source === 'categories' ? 'category' : 'post_tag';
$taxonomy = ['categories' => 'category', 'tags' => 'post_tag', 'topics' => 'topics', 'terms' => ['category', 'topics']][$source] ?? 'category';
$terms = get_terms([
'taxonomy' => $taxonomy,
'hide_empty' => false,
@@ -64,8 +64,9 @@ class PageDisplayController extends ControllerBase {
if (is_wp_error($terms)) {
return $this->error($terms->get_error_message(), 400);
}
$items = array_map(static function ($term): array {
return ['label' => $term->name, 'value' => (int) $term->term_id];
$items = array_map(static function ($term) use ($source): array {
$label = $source === 'terms' ? ($term->taxonomy === 'topics' ? __('专题:', 'jio') : __('分类:', 'jio')) . $term->name : $term->name;
return ['label' => $label, 'value' => (int) $term->term_id];
}, $terms);
return $this->success(['data' => $items]);
}
+2 -1
View File
@@ -297,12 +297,13 @@ class GlobalSchema {
['key' => 'search_custom_key_s', 'label' => '自定义匹配范围', 'type' => 'switch', 'default' => true, 'help' => '启用后可配置搜索匹配字段范围(标题/内容/分类/标签等)'],
['key' => 'search_custom_key', 'label' => '匹配字段', 'type' => 'checkbox', 'default' => ['title', 'content', 'excerpt', 'category', 'post_tag'], 'options' => [
['label' => '标题', 'value' => 'title'], ['label' => '内容', 'value' => 'content'], ['label' => '简介', 'value' => 'excerpt'],
['label' => '分类', 'value' => 'category'], ['label' => '标签', 'value' => 'post_tag'],
['label' => '分类', 'value' => 'category'], ['label' => '标签', 'value' => 'post_tag'], ['label' => '专题', 'value' => 'topics'],
['label' => '评论', 'value' => 'coment'], ['label' => '作者昵称', 'value' => 'user'],
]],
['key' => 'search_facets', 'label' => '搜索筛选', 'type' => 'fieldset', 'sanitize_type' => 'array', 'default' => [], 'dependsOn' => ['key' => 'search_ms_s', 'value' => false], 'help' => '搜索结果中显示的筛选列表;启用 Meilisearch 后由搜索引擎生成关联筛选。', 'fields' => [
['key' => 'category', 'label' => '[文章] 分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => []],
['key' => 'post_tag', 'label' => '[文章] 标签', 'type' => 'taxonomy_select', 'taxonomy' => 'post_tag', 'multiple' => true, 'default' => []],
['key' => 'topics', 'label' => '[文章] 专题', 'type' => 'taxonomy_select', 'taxonomy' => 'topics', 'multiple' => true, 'default' => []],
]],
]],
['key' => 'category-search', 'title' => '分类搜索', 'fields' => [
@@ -33,6 +33,7 @@ class SchemaPostListList {
['label' => '首页', 'value' => 'home'],
['label' => '分类页', 'value' => 'cat'],
['label' => '标签页', 'value' => 'tag'],
['label' => '专题页', 'value' => 'topics'],
]],
],
],
@@ -49,10 +50,11 @@ class SchemaPostListList {
['label' => '点赞数', 'value' => 'like'],
['label' => '评论', 'value' => 'comm'],
]],
['key' => 'list_badge_show', 'label' => '显示列表标签', 'type' => 'checkbox', 'default' => ['pay', 'cat', 'tag'], 'options' => [
['key' => 'list_badge_show', 'label' => '显示列表标签', 'type' => 'checkbox', 'default' => ['pay', 'tag', 'topics', 'cat'], 'options' => [
['label' => '付费信息', 'value' => 'pay'],
['label' => '分类', 'value' => 'cat'],
['label' => '标签', 'value' => 'tag'],
['label' => '专题', 'value' => 'topics'],
]],
],
],
@@ -72,8 +74,9 @@ class SchemaPostListList {
], 'help' => '文字、自动图文和多图模式仅在开启侧栏的页面有效。'],
['key' => 'list_card_home', 'label' => '首页列表卡片模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'list_type', 'not' => true, 'value' => 'card']],
['key' => 'list_card_tag', 'label' => '标签页列表卡片模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'list_type', 'not' => true, 'value' => 'card']],
['key' => 'list_card_topics', 'label' => '专题页列表卡片模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'list_type', 'not' => true, 'value' => 'card']],
['key' => 'list_card_author', 'label' => '用户页列表卡片模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'list_type', 'not' => true, 'value' => 'card']],
['key' => 'list_card_cat', 'label' => '分类页自定义卡片模式', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => [], 'dependsOn' => ['key' => 'list_type', 'not' => true, 'value' => 'card']],
['key' => 'list_card_cat', 'label' => '分类、专题页自定义卡片模式', 'type' => 'taxonomy_select', 'taxonomy' => 'category,topics', 'multiple' => true, 'default' => [], 'dependsOn' => ['key' => 'list_type', 'not' => true, 'value' => 'card']],
[
'key' => 'list_list_option',
'label' => '列表模式 UI 配置',
+25 -4
View File
@@ -22,6 +22,7 @@ class SchemaPageDisplay {
'homepage' => self::homepage(),
'category' => self::category(),
'tag' => self::tag(),
'topics' => self::topics(),
'user-home' => self::user_home(),
'other' => self::other(),
],
@@ -60,7 +61,7 @@ class SchemaPageDisplay {
private static function header_components(): array {
return self::section('header-components', '顶部多功能组件', [
self::notice('顶部多功能组件显示在导航栏下方,支持视频、图片、幻灯片、搜索和图标卡片;各组件可按电脑端、移动端单独启用。'),
self::field('header_slider_show', '启用顶部多功能组件', 'checkbox', [], ['description' => '选择允许显示组件的页面;未选择任何页面时,前端不会显示。', 'options' => self::options(['home' => '首页', 'category' => '分类页', 'tag' => '标签页', 'author' => '公开用户主页', 'user_center' => '用户中心', 'single' => '文章页', 'search' => '搜索页', 'page' => '其它页面'])]),
self::field('header_slider_show', '启用顶部多功能组件', 'checkbox', [], ['description' => '选择允许显示组件的页面;未选择任何页面时,前端不会显示。', 'options' => self::options(['home' => '首页', 'category' => '分类页', 'tag' => '标签页', 'topics' => '专题页', 'author' => '公开用户主页', 'user_center' => '用户中心', 'single' => '文章页', 'search' => '搜索页', 'page' => '其它页面'])]),
self::field('header_slider_show_type', '顶部多功能组件的显示规则', 'radio', '', ['dependsOn' => self::not_empty('header_slider_show'), 'inline' => true, 'options' => self::options(['' => '全部显示', 'only_pc' => '仅在PC端显示', 'only_sm' => '仅在移动端显示'])]),
self::field('header_slider_nav_color', '导航栏文字颜色', 'color', '#ffffff', ['dependsOn' => self::not_empty('header_slider_show')]),
self::field('header_slider', '背景内容', 'group', self::header_slider_defaults(), ['dependsOn' => self::not_empty('header_slider_show'), 'min' => 1, 'button_title' => '添加背景项目', 'fields' => self::slider_item_fields()]),
@@ -112,7 +113,7 @@ class SchemaPageDisplay {
self::field('index_list_title', '显示标题', 'textarea', '', ['dependsOn' => self::enabled('home_posts_list_s'), 'sanitize_type' => 'html', 'rows' => 1]),
self::field('home_exclude_posts', '排除文章', 'async_select', '', ['dependsOn' => self::enabled('home_posts_list_s'), 'source' => 'posts', 'multiple' => true, 'sortable' => true]),
self::field('home_exclude_cats', '排除分类', 'async_select', '', ['dependsOn' => self::enabled('home_posts_list_s'), 'source' => 'categories', 'multiple' => true, 'sortable' => true]),
self::field('home_lists', '更多栏目', 'group', [], ['dependsOn' => self::enabled('home_posts_list_s'), 'button_title' => '添加栏目', 'fields' => [self::field('title', '自定义标题', 'textarea', '', ['sanitize_type' => 'html', 'rows' => 1]), self::field('term_id', '显示内容', 'async_select', '', ['source' => 'categories']), self::field('orderby', '排序方式设置', 'select', '', ['options' => self::post_order_options(true)])]]),
self::field('home_lists', '更多栏目', 'group', [], ['dependsOn' => self::enabled('home_posts_list_s'), 'button_title' => '添加栏目', 'fields' => [self::field('title', '自定义标题', 'textarea', '', ['sanitize_type' => 'html', 'rows' => 1]), self::field('term_id', '显示内容', 'async_select', '', ['source' => 'terms']), self::field('orderby', '排序方式设置', 'select', '', ['options' => self::post_order_options(true)])]]),
]);
}
@@ -123,11 +124,13 @@ class SchemaPageDisplay {
self::field('cat_default_cover', '封面图', 'upload', self::asset('assets/img/user_t.jpg'), ['dependsOn' => self::enabled('page_cover_cat_s'), 'help' => '默认封面图,建议尺寸2000x800。']),
self::field('cat_orderby_s', '排序方式按钮', 'switcher', true),
self::field('cat_orderby_option', '排序方式设置', 'fieldset', [], ['dependsOn' => self::enabled('cat_orderby_s'), 'fields' => self::orderby_fields()]),
self::notice('AJAX筛选菜单列表:显示分类、标签菜单,通过 AJAX 获取内容。'),
self::notice('AJAX筛选菜单列表:显示分类、标签、专题菜单,通过 AJAX 获取内容。'),
self::field('ajax_list_cat_cat', '分类筛选', 'switcher', true),
self::field('ajax_list_option_cat_cat', '分类筛选设置', 'fieldset', [], ['dependsOn' => self::enabled('ajax_list_cat_cat'), 'fields' => self::ajax_fields('categories')]),
self::field('ajax_list_cat_tag', '标签筛选', 'switcher', false),
self::field('ajax_list_option_cat_tag', '标签筛选设置', 'fieldset', [], ['dependsOn' => self::enabled('ajax_list_cat_tag'), 'fields' => self::ajax_fields('tags')]),
self::field('ajax_list_cat_topics', '专题筛选', 'switcher', false),
self::field('ajax_list_option_cat_topics', '专题筛选设置', 'fieldset', [], ['dependsOn' => self::enabled('ajax_list_cat_topics'), 'fields' => self::ajax_fields('topics')]),
]);
}
@@ -138,11 +141,29 @@ class SchemaPageDisplay {
self::field('tag_default_cover', '封面图', 'upload', self::asset('assets/img/user_t.jpg'), ['dependsOn' => self::enabled('page_cover_tag_s'), 'help' => '默认封面图,建议尺寸2000x800。']),
self::field('tag_orderby_s', '排序方式按钮', 'switcher', true),
self::field('tag_orderby_option', '排序方式设置', 'fieldset', [], ['dependsOn' => self::enabled('tag_orderby_s'), 'fields' => self::orderby_fields()]),
self::notice('AJAX筛选菜单列表:显示分类、标签菜单,通过 AJAX 获取内容。'),
self::notice('AJAX筛选菜单列表:显示分类、标签、专题菜单,通过 AJAX 获取内容。'),
self::field('ajax_list_tag_cat', '分类筛选', 'switcher', false),
self::field('ajax_list_option_tag_cat', '分类筛选设置', 'fieldset', [], ['dependsOn' => self::enabled('ajax_list_tag_cat'), 'fields' => self::ajax_fields('categories')]),
self::field('ajax_list_tag_tag', '标签筛选', 'switcher', true),
self::field('ajax_list_option_tag_tag', '标签筛选设置', 'fieldset', [], ['dependsOn' => self::enabled('ajax_list_tag_tag'), 'fields' => self::ajax_fields('tags')]),
self::field('ajax_list_tag_topics', '专题筛选', 'switcher', false),
self::field('ajax_list_option_tag_topics', '专题筛选设置', 'fieldset', [], ['dependsOn' => self::enabled('ajax_list_tag_topics'), 'fields' => self::ajax_fields('topics')]),
]);
}
private static function topics(): array {
return self::section('topics', '专题页面', [
self::field('topics_post_count_s', '显示文章数量', 'switcher', true),
self::field('topics_default_cover', '封面图', 'upload', self::asset('assets/img/user_t.jpg'), ['help' => '默认封面图,建议尺寸2000x800。']),
self::field('topics_orderby_s', '排序方式按钮', 'switcher', true),
self::field('topics_orderby_option', '排序方式设置', 'fieldset', [], ['dependsOn' => self::enabled('topics_orderby_s'), 'fields' => self::orderby_fields()]),
self::notice('AJAX筛选菜单列表:显示分类、标签、专题菜单,通过 AJAX 获取内容。'),
self::field('ajax_list_topics_cat', '分类筛选', 'switcher', false),
self::field('ajax_list_option_topics_cat', '分类筛选设置', 'fieldset', [], ['dependsOn' => self::enabled('ajax_list_topics_cat'), 'fields' => self::ajax_fields('categories')]),
self::field('ajax_list_topics_tag', '标签筛选', 'switcher', false),
self::field('ajax_list_option_topics_tag', '标签筛选设置', 'fieldset', [], ['dependsOn' => self::enabled('ajax_list_topics_tag'), 'fields' => self::ajax_fields('tags')]),
self::field('ajax_list_topics_topics', '专题筛选', 'switcher', true),
self::field('ajax_list_option_topics_topics', '专题筛选设置', 'fieldset', [], ['dependsOn' => self::enabled('ajax_list_topics_topics'), 'fields' => self::ajax_fields('topics')]),
]);
}
+8
View File
@@ -136,6 +136,7 @@ class SchemaPostList {
['key' => 'custom_filter_show', 'label' => '高级筛选默认显示', 'type' => 'checkbox', 'default' => [], 'options' => [
['label' => '分类页面显示', 'value' => 'cat'],
['label' => '标签页面显示', 'value' => 'tag'],
['label' => '专题页面显示', 'value' => 'topics'],
], 'description' => '勾选后将会在对应页面默认显示全部筛选,您可以在下方为单独的分类添加自定义筛选'],
['key' => 'custom_filter', 'label' => '自定义高级筛选明细', 'type' => 'repeater', 'default' => [], 'addText' => '添加筛选类别', 'description' => '添加筛选类型及明细', 'fields' => [
['key' => 'name', 'label' => '类型名称', 'type' => 'input', 'default' => ''],
@@ -157,6 +158,11 @@ class SchemaPostList {
['key' => 'lists', 'label' => '按钮列表', 'type' => 'async_select', 'source' => 'tags', 'multiple' => true, 'sortable' => true, 'default' => [], 'description' => '选择并排序需要显示的按钮'],
['key' => 'dropdown', 'label' => '下拉列表', 'type' => 'switch', 'default' => false, 'description' => '用下拉框显示更多内容'],
]],
['key' => 'topics_s', 'label' => '专题筛选', 'type' => 'switch', 'default' => false, 'description' => '在所选分类页显示的专题筛选列表'],
['key' => 'topics_lists', 'label' => ' ', 'type' => 'fieldset', 'default' => ['lists' => [], 'dropdown' => false], 'dependsOn' => ['key' => 'topics_s', 'value' => true], 'fields' => [
['key' => 'lists', 'label' => '按钮列表', 'type' => 'async_select', 'source' => 'topics', 'multiple' => true, 'sortable' => true, 'default' => []],
['key' => 'dropdown', 'label' => '下拉列表', 'type' => 'switch', 'default' => false],
]],
['key' => 'orderby_s', 'label' => '排序方式', 'type' => 'switch', 'default' => true, 'description' => '在所选分类页显示排序方式按钮'],
['key' => 'orderby_lists', 'label' => ' ', 'type' => 'fieldset', 'default' => ['lists' => ['modified', 'views', 'like', 'comment_count'], 'dropdown' => false], 'dependsOn' => ['key' => 'orderby_s', 'value' => true], 'fields' => [
['key' => 'lists', 'label' => '显示排序方式', 'type' => 'select', 'multiple' => true, 'sortable' => true, 'default' => ['modified', 'views', 'like', 'comment_count'], 'options' => [
@@ -264,6 +270,8 @@ class SchemaPostList {
['key' => 'post_article_s', 'label' => '启用前台投稿', 'type' => 'switcher', 'default' => true, 'description' => '此功能启用后,可以在权限管理中设置用户的发布、审核权限'],
['key' => 'post_article_btn_txte', 'label' => '按钮文字', 'type' => 'input', 'default' => '发布文章', 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '投稿按钮的显示的文字,推荐4个字更加协调'],
['key' => 'post_article_cat', 'label' => '投稿允许选择的分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => [], 'placeholder' => '输入关键词以搜索分类', 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '投稿允许选择的分类,为空则允许选择全部分类,所选分类下如果有子分类则会自动显示,如果选择了上级分类,则不要再选该分类下的子分类。注意:请确保所选分类包含子分类总数不要超过50个,否则可能会引起页面崩溃'],
['key' => 'post_article_topics_s', 'label' => '允许选择专题', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'post_article_s', 'value' => true]],
['key' => 'post_article_topics', 'label' => '投稿允许选择的专题', 'type' => 'taxonomy_select', 'taxonomy' => 'topics', 'multiple' => true, 'default' => [], 'dependsOn' => ['all' => [['key' => 'post_article_s', 'value' => true], ['key' => 'post_article_topics_s', 'value' => true]]]],
['key' => 'post_article_custom_filter_s', 'label' => '允许选择高级筛选', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '此功能启用后,请先在文章列表/高级筛选中添加高级筛选明细'],
['key' => 'post_article_tag', 'label' => '允许插入标签', 'type' => 'switcher', 'default' => true, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '前台投稿时候允许设置文章标签'],
['key' => 'post_article_user', 'label' => '免登陆投稿发布用户', 'type' => 'user_select', 'default' => 1, 'dependsOn' => ['key' => 'post_article_s', 'value' => true], 'description' => '当选择投稿权限为无需登录就能投稿时,投稿文章的用户'],
+22
View File
@@ -143,6 +143,7 @@ final class CreatorRest {
'permalink' => $source instanceof \WP_Post ? get_permalink($source) : '',
'isPublished'=> $source instanceof \WP_Post && $source->post_status === 'publish',
'categories' => $terms['categories'],
'topics' => $terms['topics'],
'tags' => $terms['tags'],
'custom' => $terms['custom'],
'customDefs' => $terms['customDefs'],
@@ -222,6 +223,7 @@ final class CreatorRest {
'title' => (string) $request->get_param('title'),
'content' => (string) $request->get_param('content'),
'categories' => (array) $request->get_param('categories'),
'topics' => (array) $request->get_param('topics'),
'tags' => (array) $request->get_param('tags'),
'custom' => (array) $request->get_param('custom'),
'featured' => (array) $request->get_param('featured'),
@@ -250,6 +252,7 @@ final class CreatorRest {
'post_title' => $payload['title'],
'post_content' => $payload['content'],
'category' => $payload['categories'],
'topics' => $payload['topics'],
'tags' => implode(',', array_map('strval', $payload['tags'])),
'featured_data'=> $payload['featured'],
'schedule' => $payload['schedule'],
@@ -295,6 +298,7 @@ final class CreatorRest {
'title' => (string) $request->get_param('title'),
'content' => (string) $request->get_param('content'),
'categories' => (array) $request->get_param('categories'),
'topics' => (array) $request->get_param('topics'),
'tags' => (array) $request->get_param('tags'),
'custom' => (array) $request->get_param('custom'),
'featured' => (array) $request->get_param('featured'),
@@ -334,6 +338,7 @@ final class CreatorRest {
'post_title' => $payload['title'],
'post_content' => $payload['content'],
'category' => $payload['categories'],
'topics' => $payload['topics'],
'tags' => implode(',', array_map('strval', $payload['tags'])),
'featured_data' => $payload['featured'],
'schedule' => $payload['schedule'],
@@ -519,6 +524,7 @@ final class CreatorRest {
return [
'categories' => array_values(array_filter(array_map('absint', (array) $payload['categories']))),
'topics' => array_values(array_filter(array_map('absint', (array) $payload['topics']))),
'tags' => array_values(array_filter(array_map('sanitize_text_field', (array) $payload['tags']))),
'custom' => is_array($payload['custom']) ? $payload['custom'] : [],
'featured' => is_array($payload['featured']) ? $payload['featured'] : [],
@@ -567,6 +573,21 @@ final class CreatorRest {
$current = $source instanceof \WP_Post && $snapshot === []
? array_map('intval', wp_get_post_categories($source->ID))
: array_map('intval', (array) ($snapshot['categories'] ?? []));
$topics = [];
if (!empty($settings['allow_topics'])) {
foreach (SubmitTopics::tree($settings) as $node) {
$topics[] = [
'id' => (int) $node['term']->term_id,
'name' => (string) $node['term']->name,
'depth' => (int) $node['depth'],
'path' => (string) $node['path'],
];
}
}
$selected_topics = $source instanceof \WP_Post && $snapshot === []
? wp_get_post_terms($source->ID, 'topics', ['fields' => 'ids'])
: ($snapshot['topics'] ?? []);
$selected_topics = is_array($selected_topics) ? SubmitTopics::filter_ids($selected_topics, $settings) : [];
$tags = $source instanceof \WP_Post && $snapshot === []
? wp_get_post_tags($source->ID, ['fields' => 'names'])
: array_map('strval', (array) ($snapshot['tags'] ?? []));
@@ -610,6 +631,7 @@ final class CreatorRest {
return [
'categories' => ['items' => $categories, 'selected' => $current],
'topics' => ['enabled' => !empty($settings['allow_topics']), 'items' => $topics, 'selected' => $selected_topics],
'tags' => [
'items' => is_array($tags) ? array_values($tags) : [],
'enabled' => $tag_enabled,
-1
View File
@@ -148,7 +148,6 @@ class DomainModel {
'description' => '',
'show_sidebar' => '',
'show_header' => true,
'show_featured' => false,
];
}
+5
View File
@@ -231,6 +231,11 @@ trait FrontSubmitSave {
if (isset($data['category'])) {
wp_set_post_categories((int) $in_id, $categories);
}
if (!empty($settings['allow_topics'])) {
$raw_topics = $data['topics'] ?? [];
$topic_ids = SubmitTopics::filter_ids(is_array($raw_topics) ? $raw_topics : [$raw_topics], $settings);
wp_set_post_terms((int) $in_id, $topic_ids, 'topics');
}
if (!empty($settings['allow_tags']) && array_key_exists('tags', $data) && $tags_raw === '') {
wp_set_post_tags((int) $in_id, []);
}
+26
View File
@@ -29,6 +29,7 @@ class Setup {
add_filter('pre_option_link_manager_enabled', '__return_true');
add_action('init', [$this, 'register_nav_menus'], 0);
add_action('init', [$this, 'register_post_types'], 1);
add_action('init', [$this, 'register_taxonomies'], 1);
add_action('init', [$this, 'register_meta_fields'], 5);
// 文章浏览量统计(单次访问)
add_action('wp', 'jio_set_post_views');
@@ -124,6 +125,31 @@ class Setup {
]);
}
public function register_taxonomies(): void {
register_taxonomy('topics', ['post'], [
'description' => __('添加文章专题', 'jio'),
'hierarchical' => true,
'labels' => [
'name' => __('专题', 'jio'),
'singular_name' => __('专题', 'jio'),
'search_items' => __('搜索专题', 'jio'),
'all_items' => __('所有专题', 'jio'),
'parent_item' => __('父专题', 'jio'),
'parent_item_colon' => __('父专题:', 'jio'),
'edit_item' => __('编辑专题', 'jio'),
'update_item' => __('更新专题', 'jio'),
'add_new_item' => __('添加新专题', 'jio'),
'new_item_name' => __('新专题名称', 'jio'),
'menu_name' => __('专题', 'jio'),
],
'show_ui' => true,
'show_in_menu' => true,
'show_in_rest' => true,
'show_admin_column' => true,
'query_var' => true,
]);
}
/**
* 注册资源 post meta + 分类 term meta(jio_flow)
*
+7
View File
@@ -441,6 +441,10 @@ final class SubmissionVersion {
}
wp_set_post_categories($post_id, $cats);
}
if (!empty($settings['allow_topics']) && array_key_exists('topics', $snapshot)) {
$topics = is_array($snapshot['topics']) ? $snapshot['topics'] : [];
wp_set_post_terms($post_id, SubmitTopics::filter_ids($topics, $settings), 'topics');
}
if (array_key_exists('tags', $snapshot)) {
$tags = is_array($snapshot['tags']) ? $snapshot['tags'] : [];
wp_set_post_tags($post_id, array_values(array_filter(array_map('sanitize_text_field', $tags))));
@@ -480,6 +484,9 @@ final class SubmissionVersion {
if (isset($snapshot['categories']) && is_array($snapshot['categories'])) {
$clean['categories'] = array_values(array_unique(array_filter(array_map('absint', $snapshot['categories']))));
}
if (isset($snapshot['topics']) && is_array($snapshot['topics'])) {
$clean['topics'] = array_values(array_unique(array_filter(array_map('absint', $snapshot['topics']))));
}
if (isset($snapshot['tags'])) {
$tags = is_array($snapshot['tags']) ? $snapshot['tags'] : preg_split('/[,,\n]+/u', (string) $snapshot['tags']);
$clean['tags'] = array_values(array_filter(array_map('sanitize_text_field', (array) $tags)));
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* Topics available to front-end authors.
*
* @package Jio
*/
namespace Jio\Core;
defined('ABSPATH') || exit;
final class SubmitTopics {
/** @return array<int, array{term:\WP_Term, depth:int, path:string}> */
public static function tree(array $settings): array {
if (empty($settings['allow_topics'])) {
return [];
}
$terms = get_terms([
'taxonomy' => 'topics',
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
'number' => 0,
]);
if (!is_array($terms)) {
return [];
}
$allowed = self::allowed_ids($settings);
if ($allowed !== null) {
$terms = array_values(array_filter($terms, static fn($term): bool => isset($allowed[(int) $term->term_id])));
}
$present = [];
foreach ($terms as $term) {
$present[(int) $term->term_id] = true;
}
$children = [];
foreach ($terms as $term) {
$parent = (int) $term->parent;
$children[isset($present[$parent]) ? $parent : 0][] = $term;
}
$tree = [];
$walk = static function (int $parent, int $depth, string $prefix) use (&$walk, &$tree, $children): void {
foreach ($children[$parent] ?? [] as $term) {
$path = $prefix === '' ? (string) $term->name : $prefix . ' / ' . $term->name;
$tree[] = ['term' => $term, 'depth' => $depth, 'path' => $path];
$walk((int) $term->term_id, $depth + 1, $path);
}
};
$walk(0, 0, '');
return $tree;
}
/** @return int[] */
public static function filter_ids(array $ids, array $settings): array {
if (empty($settings['allow_topics'])) {
return [];
}
$allowed = self::allowed_ids($settings);
$ids = array_values(array_unique(array_filter(array_map('absint', $ids))));
$valid = [];
foreach ($ids as $id) {
if (($allowed === null || isset($allowed[$id])) && term_exists($id, 'topics')) {
$valid[] = $id;
}
}
return $valid;
}
/** @return array<int,true>|null */
private static function allowed_ids(array $settings): ?array {
$roots = array_values(array_unique(array_filter(array_map('absint', (array) ($settings['allowed_topic_ids'] ?? [])))));
if ($roots === []) {
return null;
}
$allowed = [];
foreach ($roots as $id) {
$allowed[$id] = true;
$descendants = get_term_children($id, 'topics');
foreach (is_array($descendants) ? $descendants : [] as $child) {
$allowed[(int) $child] = true;
}
}
return $allowed;
}
}
-7
View File
@@ -91,13 +91,6 @@ class Templates {
return $templates;
}
/**
* Load dedicated taxonomy templates.
*/
public function load_taxonomy_template(string $template): string {
return $template;
}
public function render_archive_shortcode(array $attrs = [], string $content = ''): string {
$template = JIO_DIR . '/templates/shortcodes/archive.php';
if (!file_exists($template)) {
+102 -22
View File
@@ -12,7 +12,7 @@ class TermMeta {
private static $instance = null;
/** Supported taxonomies */
private $taxonomies = ['category', 'post_tag'];
private $taxonomies = ['category', 'post_tag', 'topics'];
/** Meta key for storing term-level settings */
const META_KEY = DomainModel::TERM_META_KEY;
@@ -31,9 +31,61 @@ class TermMeta {
add_action("edited_{$tax}", [$this, 'save_term_meta']);
}
add_action('admin_enqueue_scripts', [$this, 'enqueue_topic_media']);
add_filter('manage_edit-topics_columns', [$this, 'topic_columns']);
add_filter('manage_topics_custom_column', [$this, 'render_topic_column'], 10, 3);
add_filter('topics_row_actions', [$this, 'topic_row_actions'], 10, 2);
add_action('pre_get_posts', [$this, 'filter_archive_query']);
}
public function topic_columns(array $columns): array {
return ['jio_cover' => __('图像', 'jio')] + $columns;
}
public function render_topic_column(string $content, string $column, int $term_id): string {
if ($column !== 'jio_cover') {
return $content;
}
$url = esc_url_raw((string) self::get_meta_value($term_id, 'cover_url'));
return $url === ''
? '&mdash;'
: '<img src="' . esc_url($url) . '" alt="" style="width:55px;height:28px;object-fit:cover">';
}
public function topic_row_actions(array $actions, \WP_Term $term): array {
$actions['jio_term_id'] = sprintf(esc_html__('ID:%d', 'jio'), (int) $term->term_id);
return $actions;
}
public function enqueue_topic_media(string $hook): void {
if (!in_array($hook, ['edit-tags.php', 'term.php'], true)) {
return;
}
$screen = get_current_screen();
if (!$screen || $screen->taxonomy !== 'topics') {
return;
}
wp_enqueue_media();
wp_add_inline_script('media-views', <<<'JS'
document.addEventListener('click', function (event) {
const button = event.target.closest('[data-jio-topic-cover-select]');
if (!button || typeof wp === 'undefined' || !wp.media) return;
event.preventDefault();
const field = button.closest('.form-field');
const input = field && field.querySelector('[name="jio_term_meta[cover_url]"]');
if (!input) return;
const frame = wp.media({ title: button.textContent.trim(), library: { type: 'image' }, multiple: false });
frame.on('select', function () {
const image = frame.state().get('selection').first();
if (image) input.value = image.get('url') || '';
});
frame.open();
});
JS
);
}
/**
* Get term meta with defaults.
*/
@@ -59,17 +111,25 @@ class TermMeta {
* @return array<string,mixed>
*/
public static function resolve_archive_settings(\WP_Term $term): array {
$taxonomy = $term->taxonomy === 'post_tag' ? 'post_tag' : 'category';
$global = $taxonomy === 'post_tag' && class_exists('\Jio\Settings\Settings')
? \Jio\Settings\Settings::get_tag_page_settings()
: (class_exists('\Jio\Settings\Settings') ? \Jio\Settings\Settings::get_category_page_settings() : [
$taxonomy = in_array($term->taxonomy, ['category', 'post_tag', 'topics'], true) ? $term->taxonomy : 'category';
if (class_exists('\Jio\Settings\Settings')) {
if ($taxonomy === 'topics') {
$global = \Jio\Settings\Settings::get_topics_page_settings();
} elseif ($taxonomy === 'post_tag') {
$global = \Jio\Settings\Settings::get_tag_page_settings();
} else {
$global = \Jio\Settings\Settings::get_category_page_settings();
}
} else {
$global = [
'header_style' => 'cover',
'cover_enabled' => true,
'desc_enabled' => true,
'filter_bar' => true,
'sidebar' => true,
'sidebar' => $taxonomy !== 'topics',
'per_page' => 12,
]);
];
}
$saved = get_term_meta($term->term_id, self::META_KEY, true);
if (!is_array($saved)) {
@@ -96,7 +156,7 @@ class TermMeta {
$cover_url = isset($saved['cover_url']) ? esc_url_raw((string) $saved['cover_url']) : '';
if ($cover_url === '' && !empty($global['cover_enabled'])) {
$cover_id = (int) get_term_meta($term->term_id, 'jio_category_cover_id', true);
$cover_id = $taxonomy === 'category' ? (int) get_term_meta($term->term_id, 'jio_category_cover_id', true) : 0;
$cover_url = $cover_id > 0 ? (string) wp_get_attachment_url($cover_id) : '';
if ($cover_url === '') {
$cover_url = esc_url_raw((string) ($global['default_cover'] ?? ''));
@@ -108,9 +168,7 @@ class TermMeta {
$description = (string) $term->description;
}
// 分类高级筛选语义:cat 页按 cat_custom_filter 全量覆盖
// 筛选项目,未匹配或 tag 页回退默认配置,custom_filter_show 决定自定义筛选显示。
$page = $taxonomy === 'post_tag' ? 'tag' : 'cat';
$page = $taxonomy === 'post_tag' ? 'tag' : ($taxonomy === 'topics' ? 'topics' : 'cat');
$custom_filter = class_exists('\Jio\Settings\Settings')
? \Jio\Settings\Settings::get_archive_custom_filter_config((int) $term->term_id, $page)
: ['custom_enabled' => false, 'custom_filters' => 'all', 'overrides' => []];
@@ -120,6 +178,8 @@ class TermMeta {
$ajax_category = is_array($global['ajax_category'] ?? null) ? $global['ajax_category'] : [];
$ajax_tag_enabled = !empty($global['ajax_tag_enabled']);
$ajax_tag = is_array($global['ajax_tag'] ?? null) ? $global['ajax_tag'] : [];
$ajax_topics_enabled = !empty($global['ajax_topics_enabled']);
$ajax_topics = is_array($global['ajax_topics'] ?? null) ? $global['ajax_topics'] : [];
$orderby_enabled = !empty($global['orderby_enabled']);
$orderby_option = is_array($global['orderby_option'] ?? null) ? $global['orderby_option'] : [];
if (array_key_exists('cat_s', $overrides)) {
@@ -130,6 +190,10 @@ class TermMeta {
$ajax_tag_enabled = !empty($overrides['tag_s']);
$ajax_tag = is_array($overrides['tag_lists'] ?? null) ? $overrides['tag_lists'] : $ajax_tag;
}
if (array_key_exists('topics_s', $overrides)) {
$ajax_topics_enabled = !empty($overrides['topics_s']);
$ajax_topics = is_array($overrides['topics_lists'] ?? null) ? $overrides['topics_lists'] : $ajax_topics;
}
if (array_key_exists('orderby_s', $overrides)) {
$orderby_enabled = !empty($overrides['orderby_s']);
$orderby_option = is_array($overrides['orderby_lists'] ?? null) ? $overrides['orderby_lists'] : $orderby_option;
@@ -143,16 +207,18 @@ class TermMeta {
'header_style' => $header_style,
'show_sidebar' => $sidebar_enabled,
'show_cover' => $header_style === 'cover' && !empty($global['cover_enabled']),
'show_desc' => $taxonomy === 'post_tag' ? $description !== '' : (!empty($global['desc_enabled']) && $description !== ''),
'filter_bar' => !empty($global['filter_bar']) || $ajax_category_enabled || $ajax_tag_enabled || $orderby_enabled || !empty($custom_filter['custom_enabled']),
'show_desc' => $taxonomy !== 'category' ? $description !== '' : (!empty($global['desc_enabled']) && $description !== ''),
'filter_bar' => !empty($global['filter_bar']) || $ajax_category_enabled || $ajax_tag_enabled || $ajax_topics_enabled || $orderby_enabled || !empty($custom_filter['custom_enabled']),
'post_count' => !empty($global['post_count']),
'orderby_enabled' => $orderby_enabled,
'orderby' => $orderby,
'orderby_option'=> $orderby_option,
'ajax_category_enabled' => $ajax_category_enabled,
'ajax_tag_enabled' => $ajax_tag_enabled,
'ajax_topics_enabled' => $ajax_topics_enabled,
'ajax_category' => $ajax_category,
'ajax_tag' => $ajax_tag,
'ajax_topics' => $ajax_topics,
'custom_filter_enabled' => !empty($custom_filter['custom_enabled']),
'custom_filter_keys' => $custom_filter['custom_filters'] ?? 'all',
'custom_dimensions' => is_array($filter_all['custom_filter'] ?? null) ? $filter_all['custom_filter'] : [],
@@ -191,6 +257,13 @@ class TermMeta {
$term = get_term_by('slug', $slug, 'post_tag');
}
}
} elseif ($query->is_tax('topics')) {
$path = trim((string) ($query->get('topics') ?: $query->get('term')), '/');
$segments = explode('/', $path);
$slug = sanitize_title((string) end($segments));
if ($slug !== '') {
$term = get_term_by('slug', $slug, 'topics');
}
}
if (!$term instanceof \WP_Term) {
@@ -245,11 +318,15 @@ class TermMeta {
* Render fields for "Add new term" form.
*/
public function render_add_fields(string $taxonomy): void {
wp_nonce_field('jio_term_meta', 'jio_term_meta_nonce');
?>
<div class="form-field">
<label for="jio_term_cover_url"><?php esc_html_e('封面图 URL', 'jio'); ?></label>
<input type="url" name="jio_term_meta[cover_url]" id="jio_term_cover_url" value="" placeholder="https://...">
<p class="description"><?php esc_html_e('分类/标签页顶部封面图,留空则不显示。', 'jio'); ?></p>
<?php if ($taxonomy === 'topics') : ?>
<button type="button" class="button" data-jio-topic-cover-select><?php esc_html_e('上传/添加图像', 'jio'); ?></button>
<?php endif; ?>
<p class="description"><?php esc_html_e('归档页顶部封面图,留空则使用默认封面。', 'jio'); ?></p>
</div>
<?php
}
@@ -259,19 +336,23 @@ class TermMeta {
*/
public function render_edit_fields(\WP_Term $term): void {
$meta = self::get_meta($term->term_id);
wp_nonce_field('jio_term_meta', 'jio_term_meta_nonce');
?>
<tr class="form-field">
<th scope="row"><label for="jio_term_cover_url"><?php esc_html_e('封面图 URL', 'jio'); ?></label></th>
<td>
<input type="url" name="jio_term_meta[cover_url]" id="jio_term_cover_url" value="<?php echo esc_url($meta['cover_url']); ?>" class="regular-text" placeholder="https://...">
<p class="description"><?php esc_html_e('分类/标签页顶部封面图,留空则不显示。', 'jio'); ?></p>
<?php if ($term->taxonomy === 'topics') : ?>
<button type="button" class="button" data-jio-topic-cover-select><?php esc_html_e('上传/添加图像', 'jio'); ?></button>
<?php endif; ?>
<p class="description"><?php esc_html_e('归档页顶部封面图,留空则使用默认封面。', 'jio'); ?></p>
</td>
</tr>
<tr class="form-field">
<th scope="row"><label for="jio_term_desc"><?php esc_html_e('自定义简介', 'jio'); ?></label></th>
<td>
<textarea name="jio_term_meta[description]" id="jio_term_desc" rows="3" class="large-text"><?php echo esc_textarea($meta['description']); ?></textarea>
<p class="description"><?php esc_html_e('覆盖默认的分类/标签描述。', 'jio'); ?></p>
<p class="description"><?php esc_html_e('覆盖默认的术语描述。', 'jio'); ?></p>
</td>
</tr>
<tr class="form-field">
@@ -284,10 +365,6 @@ class TermMeta {
<label>
<input type="checkbox" name="jio_term_meta[show_sidebar]" value="1" <?php checked($meta['show_sidebar'] === '1'); ?>>
<?php esc_html_e('强制显示侧栏', 'jio'); ?>
</label><br>
<label>
<input type="checkbox" name="jio_term_meta[show_featured]" value="1" <?php checked(!empty($meta['show_featured'])); ?>>
<?php esc_html_e('显示推荐文章位', 'jio'); ?>
</label>
</td>
</tr>
@@ -301,17 +378,20 @@ class TermMeta {
if (!isset($_POST['jio_term_meta']) || !is_array($_POST['jio_term_meta'])) {
return;
}
$nonce = isset($_POST['jio_term_meta_nonce']) ? sanitize_text_field(wp_unslash((string) $_POST['jio_term_meta_nonce'])) : '';
if ($nonce === '' || !wp_verify_nonce($nonce, 'jio_term_meta')) {
return;
}
if (!current_user_can('manage_categories')) {
return;
}
$raw = $_POST['jio_term_meta'];
$raw = wp_unslash($_POST['jio_term_meta']);
$sanitized = [
'cover_url' => isset($raw['cover_url']) ? esc_url_raw($raw['cover_url']) : '',
'description' => isset($raw['description']) ? sanitize_textarea_field($raw['description']) : '',
'show_sidebar' => isset($raw['show_sidebar']) ? '1' : '',
'show_header' => isset($raw['show_header']) ? true : false,
'show_featured' => isset($raw['show_featured']) ? true : false,
];
update_term_meta($term_id, self::META_KEY, $sanitized);
+2
View File
@@ -51,6 +51,7 @@ trait DisplayPageGroups {
'home',
'category',
'tag',
'topics',
'author',
'user_center',
'single',
@@ -206,6 +207,7 @@ trait DisplayPageGroups {
'category_recommend' => self::get_category_recommend_settings(),
'category' => self::get_category_page_settings(),
'tag' => self::get_tag_page_settings(),
'topics' => self::get_topics_page_settings(),
'user_home' => self::get_user_home_settings(),
'other' => self::get_other_page_settings(),
];
+38 -2
View File
@@ -42,6 +42,7 @@ trait FacadeBulk {
$orderby = sanitize_key((string) (($orderby_option['lists'][0] ?? 'modified')));
$ajax_category_enabled = !empty($settings['ajax_list_cat_cat']);
$ajax_tag_enabled = !empty($settings['ajax_list_cat_tag']);
$ajax_topics_enabled = !empty($settings['ajax_list_cat_topics']);
$orderby_enabled = !empty($settings['cat_orderby_s']);
$post_count = !empty($settings['cat_post_count_s']);
@@ -51,7 +52,7 @@ trait FacadeBulk {
'desc_enabled' => true,
'default_cover' => esc_url_raw((string) ($settings['cat_default_cover'] ?? '')),
'list_mode' => 'grid',
'filter_bar' => $ajax_category_enabled || $ajax_tag_enabled || $orderby_enabled || $post_count,
'filter_bar' => $ajax_category_enabled || $ajax_tag_enabled || $ajax_topics_enabled || $orderby_enabled || $post_count,
'sidebar' => true,
'per_page' => max(1, (int) get_option('posts_per_page', 10)),
'post_count' => $post_count,
@@ -60,8 +61,10 @@ trait FacadeBulk {
'orderby_option' => $orderby_option,
'ajax_category_enabled' => $ajax_category_enabled,
'ajax_tag_enabled' => $ajax_tag_enabled,
'ajax_topics_enabled' => $ajax_topics_enabled,
'ajax_category' => is_array($settings['ajax_list_option_cat_cat'] ?? null) ? $settings['ajax_list_option_cat_cat'] : [],
'ajax_tag' => is_array($settings['ajax_list_option_cat_tag'] ?? null) ? $settings['ajax_list_option_cat_tag'] : [],
'ajax_topics' => is_array($settings['ajax_list_option_cat_topics'] ?? null) ? $settings['ajax_list_option_cat_topics'] : [],
];
}
@@ -78,6 +81,7 @@ trait FacadeBulk {
$orderby = sanitize_key((string) (($orderby_option['lists'][0] ?? 'modified')));
$ajax_category_enabled = !empty($settings['ajax_list_tag_cat']);
$ajax_tag_enabled = !empty($settings['ajax_list_tag_tag']);
$ajax_topics_enabled = !empty($settings['ajax_list_tag_topics']);
$orderby_enabled = !empty($settings['tag_orderby_s']);
$post_count = !empty($settings['tag_post_count_s']);
@@ -89,17 +93,49 @@ trait FacadeBulk {
'sidebar' => true,
'per_page' => max(1, (int) get_option('posts_per_page', 10)),
'post_count' => $post_count,
'filter_bar' => $ajax_category_enabled || $ajax_tag_enabled || $orderby_enabled || $post_count,
'filter_bar' => $ajax_category_enabled || $ajax_tag_enabled || $ajax_topics_enabled || $orderby_enabled || $post_count,
'orderby' => $orderby,
'orderby_enabled'=> $orderby_enabled,
'orderby_option' => $orderby_option,
'ajax_category_enabled' => $ajax_category_enabled,
'ajax_tag_enabled' => $ajax_tag_enabled,
'ajax_topics_enabled' => $ajax_topics_enabled,
'ajax_category' => is_array($settings['ajax_list_option_tag_cat'] ?? null) ? $settings['ajax_list_option_tag_cat'] : [],
'ajax_tag' => is_array($settings['ajax_list_option_tag_tag'] ?? null) ? $settings['ajax_list_option_tag_tag'] : [],
'ajax_topics' => is_array($settings['ajax_list_option_tag_topics'] ?? null) ? $settings['ajax_list_option_tag_topics'] : [],
];
}
public static function get_topics_page_settings(): array {
$settings = self::read_domain('page_display', 'topics');
$orderby_option = self::normalize_page_orderby_option($settings['topics_orderby_option'] ?? []);
$ajax_category_enabled = !empty($settings['ajax_list_topics_cat']);
$ajax_tag_enabled = !empty($settings['ajax_list_topics_tag']);
$ajax_topics_enabled = !array_key_exists('ajax_list_topics_topics', $settings) || !empty($settings['ajax_list_topics_topics']);
$orderby_enabled = !array_key_exists('topics_orderby_s', $settings) || !empty($settings['topics_orderby_s']);
$post_count = !array_key_exists('topics_post_count_s', $settings) || !empty($settings['topics_post_count_s']);
return [
'header_style' => 'cover',
'cover_enabled' => true,
'default_cover' => esc_url_raw((string) ($settings['topics_default_cover'] ?? get_template_directory_uri() . '/assets/img/user_t.jpg')),
'list_mode' => 'grid',
'sidebar' => false,
'per_page' => max(1, (int) get_option('posts_per_page', 10)),
'post_count' => $post_count,
'filter_bar' => $ajax_category_enabled || $ajax_tag_enabled || $ajax_topics_enabled || $orderby_enabled,
'orderby' => sanitize_key((string) ($orderby_option['lists'][0] ?? 'modified')),
'orderby_enabled' => $orderby_enabled,
'orderby_option' => $orderby_option,
'ajax_category_enabled' => $ajax_category_enabled,
'ajax_tag_enabled' => $ajax_tag_enabled,
'ajax_topics_enabled' => $ajax_topics_enabled,
'ajax_category' => is_array($settings['ajax_list_option_topics_cat'] ?? null) ? $settings['ajax_list_option_topics_cat'] : [],
'ajax_tag' => is_array($settings['ajax_list_option_topics_tag'] ?? null) ? $settings['ajax_list_option_topics_tag'] : [],
'ajax_topics' => is_array($settings['ajax_list_option_topics_topics'] ?? null) ? $settings['ajax_list_option_topics_topics'] : [],
];
}
/** @return array{header_style:string,header_cover_img:string} */
+11 -4
View File
@@ -27,11 +27,11 @@ class PostDisplay {
$card_style = $card_style === 'style3' ? 'style3' : 'null';
$sticky_pages = is_array($settings['sticky_s'] ?? null)
? array_values(array_intersect(array_map('sanitize_key', $settings['sticky_s']), ['home', 'cat', 'tag']))
? array_values(array_intersect(array_map('sanitize_key', $settings['sticky_s']), ['home', 'cat', 'tag', 'topics']))
: ['home', 'cat'];
$badge_types = is_array($settings['list_badge_show'] ?? null)
? array_values(array_intersect(array_map('sanitize_key', $settings['list_badge_show']), ['pay', 'cat', 'tag']))
: ['pay', 'cat', 'tag'];
? array_values(array_intersect(array_map('sanitize_key', $settings['list_badge_show']), ['pay', 'cat', 'tag', 'topics']))
: ['pay', 'tag', 'topics', 'cat'];
$card_cat_ids = is_array($settings['list_card_cat'] ?? null)
? array_values(array_filter(array_map('intval', $settings['list_card_cat'])))
: [];
@@ -58,6 +58,7 @@ class PostDisplay {
'list_show_type' => in_array($list_show_type, ['separate', 'no_margin'], true) ? $list_show_type : 'separate',
'list_card_home' => !empty($settings['list_card_home']),
'list_card_tag' => !empty($settings['list_card_tag']),
'list_card_topics' => !empty($settings['list_card_topics']),
'list_card_author' => !empty($settings['list_card_author']),
'list_card_cat' => $card_cat_ids,
'list_list_option' => [
@@ -104,7 +105,7 @@ class PostDisplay {
$settings = Settings::get_all();
return [
'custom_filter_show' => is_array($settings['custom_filter_show'] ?? null) ? array_values(array_intersect(array_map('sanitize_key', $settings['custom_filter_show']), ['cat', 'tag'])) : [],
'custom_filter_show' => is_array($settings['custom_filter_show'] ?? null) ? array_values(array_intersect(array_map('sanitize_key', $settings['custom_filter_show']), ['cat', 'tag', 'topics'])) : [],
'custom_filter' => self::sanitize_custom_filter_defs($settings['custom_filter'] ?? []),
'cat_custom_filter' => self::sanitize_cat_custom_filter($settings['cat_custom_filter'] ?? []),
];
@@ -132,6 +133,8 @@ class PostDisplay {
'cat_lists' => $rule['cat_lists'],
'tag_s' => $rule['tag_s'],
'tag_lists' => $rule['tag_lists'],
'topics_s' => $rule['topics_s'],
'topics_lists' => $rule['topics_lists'],
'orderby_s' => $rule['orderby_s'],
'orderby_lists' => $rule['orderby_lists'],
],
@@ -373,6 +376,8 @@ class PostDisplay {
'allowed_cat_ids' => is_array($settings['post_article_cat'] ?? null) ? array_values(array_filter(array_map('intval', $settings['post_article_cat']))) : [],
'default_user_id' => max(0, (int) ($settings['post_article_user'] ?? 1)),
'allow_tags' => !isset($settings['post_article_tag']) || !empty($settings['post_article_tag']),
'allow_topics' => !empty($settings['post_article_topics_s']),
'allowed_topic_ids' => is_array($settings['post_article_topics'] ?? null) ? array_values(array_filter(array_map('absint', $settings['post_article_topics']))) : [],
'custom_filter_s' => !isset($settings['post_article_custom_filter_s']) || !empty($settings['post_article_custom_filter_s']),
'pay_free_s' => !isset($settings['post_article_pay_free_s']) || !empty($settings['post_article_pay_free_s']),
'pay_price_min' => max(0, (int) ($cash_limit['min'] ?? 0)),
@@ -539,6 +544,8 @@ class PostDisplay {
'cat_lists' => self::sanitize_filter_option_lists($item['cat_lists'] ?? []),
'tag_s' => !empty($item['tag_s']),
'tag_lists' => self::sanitize_filter_option_lists($item['tag_lists'] ?? []),
'topics_s' => !empty($item['topics_s']),
'topics_lists' => self::sanitize_filter_option_lists($item['topics_lists'] ?? []),
'orderby_s' => !empty($item['orderby_s']),
'orderby_lists' => self::sanitize_filter_orderby_lists($item['orderby_lists'] ?? []),
'custom_filters' => is_array($item['custom_filters'] ?? null)
+3 -3
View File
@@ -108,7 +108,7 @@ class ThemeSearch {
$items = preg_split('/[,,\s]+/', (string) $raw) ?: [];
}
$items = array_map(static fn($k) => trim(sanitize_key((string) $k)), $items);
$valid = ['title', 'content', 'excerpt', 'category', 'post_tag', 'coment', 'user'];
$valid = ['title', 'content', 'excerpt', 'category', 'post_tag', 'topics', 'coment', 'user'];
return array_values(array_intersect(array_filter(array_unique($items)), $valid));
}
@@ -175,7 +175,7 @@ class ThemeSearch {
$terms = [(string) $query->get('s')];
}
$groups = [];
$taxonomies = ['post_tag', 'category'];
$taxonomies = ['post_tag', 'category', 'topics'];
foreach ($terms as $term) {
$like = (!empty($query->query_vars['exact']) ? '' : '%') . $wpdb->esc_like((string) $term) . (!empty($query->query_vars['exact']) ? '' : '%');
$parts = [];
@@ -212,7 +212,7 @@ class ThemeSearch {
if (in_array('user', $keys, true)) {
$join .= " LEFT JOIN {$wpdb->users} AS jio_search_users ON ({$wpdb->posts}.post_author = jio_search_users.ID)";
}
if (array_intersect(['post_tag', 'category'], $keys)) {
if (array_intersect(['post_tag', 'category', 'topics'], $keys)) {
$join .= " LEFT JOIN {$wpdb->term_relationships} AS jio_search_tr ON ({$wpdb->posts}.ID = jio_search_tr.object_id)";
$join .= " LEFT JOIN {$wpdb->term_taxonomy} AS jio_search_tt ON (jio_search_tr.term_taxonomy_id = jio_search_tt.term_taxonomy_id)";
$join .= " LEFT JOIN {$wpdb->terms} AS jio_search_terms ON (jio_search_tt.term_id = jio_search_terms.term_id)";
+7 -4
View File
@@ -206,8 +206,11 @@ final class FormRenderer {
}
private static function terms(string $id, string $name, array $field, $value): void {
$taxonomy = (string) ($field['taxonomy'] ?? 'category');
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 500, 'orderby' => 'name']);
$taxonomies = array_values(array_unique(array_filter(array_map('sanitize_key', (array) ($field['taxonomy'] ?? 'category')))));
if ($taxonomies === []) {
$taxonomies = ['category'];
}
$terms = get_terms(['taxonomy' => $taxonomies, 'hide_empty' => false, 'number' => 500, 'orderby' => 'name']);
if (is_wp_error($terms)) {
$terms = [];
}
@@ -216,7 +219,7 @@ final class FormRenderer {
foreach ($terms as $term) {
$by_id[(int) $term->term_id] = $term;
}
echo '<div class="jio-widget-terms" data-jio-terms="' . esc_attr($taxonomy) . '">';
echo '<div class="jio-widget-terms" data-jio-terms="' . esc_attr(implode(',', $taxonomies)) . '">';
echo '<input type="hidden" class="jio-widget-terms__value" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr(implode(',', $selected)) . '">';
echo '<div class="jio-widget-terms__selected">';
foreach ($selected as $term_id) {
@@ -226,7 +229,7 @@ final class FormRenderer {
echo '<span class="jio-widget-terms__chip" data-term-id="' . esc_attr((string) $term_id) . '"><span class="dashicons dashicons-move" aria-hidden="true"></span>' . esc_html((string) $by_id[$term_id]->name) . '<button type="button" class="button-link-delete jio-widget-terms__remove" aria-label="' . esc_attr__('移除', 'jio') . '">×</button></span>';
}
echo '</div>';
echo '<input type="search" class="widefat jio-widget-terms__search" placeholder="' . esc_attr__('输入关键词筛选', 'jio') . '" aria-label="' . esc_attr__('筛选可选分类', 'jio') . '">';
echo '<input type="search" class="widefat jio-widget-terms__search" placeholder="' . esc_attr__('输入关键词筛选', 'jio') . '" aria-label="' . esc_attr__('筛选可选术语', 'jio') . '">';
echo '<div class="jio-widget-terms__options">';
foreach ($terms as $term) {
$term_id = (int) $term->term_id;
+8
View File
@@ -53,6 +53,14 @@ final class PostQuery {
if ($tags) {
$args['tag__in'] = $tags;
}
$topics = RendererHelpers::ids((string) ($config['topics'] ?? ''));
if ($topics) {
$args['tax_query'] = [[
'taxonomy' => 'topics',
'field' => 'term_id',
'terms' => $topics,
]];
}
$days = max(0, (int) ($config['limit_day'] ?? 0));
if ($days > 0) {
@@ -110,7 +110,7 @@ final class ContentRenderer {
if (!$term_ids) {
return '';
}
$terms = get_terms(['taxonomy' => 'category', 'include' => $term_ids, 'orderby' => 'include']);
$terms = get_terms(['taxonomy' => ['category', 'topics'], 'include' => $term_ids, 'orderby' => 'include']);
if (is_wp_error($terms) || !$terms) {
return '';
}
@@ -134,7 +134,7 @@ final class ContentRenderer {
$card = [
'type' => (string) ($instance['type'] ?? 'style-1'),
'class' => 'mb10',
'img' => jio_widget_term_cover_url((int) $term->term_id),
'img' => jio_widget_term_cover_url((int) $term->term_id, false, (string) $term->taxonomy),
'alt' => $name . '-' . $description,
'link' => ['url' => esc_url((string) $href), 'target' => $target_blank],
'text1' => $name,
@@ -179,8 +179,13 @@ final class ContentRenderer {
$animation = (string) ($instance['obs_animation'] ?? '');
$html = '';
foreach ($term_ids as $key => $term_id) {
$term = get_term((int) $term_id);
if (!$term instanceof \WP_Term || !in_array($term->taxonomy, ['category', 'topics'], true)) {
continue;
}
$card = jio_widget_term_aggregation([
'term_id' => (int) $term_id,
'taxonomy' => (string) $term->taxonomy,
'target_blank' => !empty($instance['target_blank']),
'orderby' => (string) ($instance['orderby'] ?? 'modified'),
'order' => (($instance['order'] ?? 'desc') === 'asc') ? 'ASC' : 'DESC',
+6 -3
View File
@@ -76,7 +76,7 @@ final class WidgetSchemas {
];
case 'tag_cloud':
return [
['id' => 'taxonomy', 'type' => 'select', 'label' => '分类法', 'default' => 'post_tag', 'options' => ['post_tag' => '标签', 'category' => '分类']],
['id' => 'taxonomy', 'type' => 'select', 'label' => '分类法', 'default' => 'post_tag', 'options' => ['post_tag' => '标签', 'category' => '分类', 'topics' => '专题']],
['id' => 'number', 'type' => 'spinner', 'label' => '数量', 'default' => 20, 'min' => 1, 'max' => 100],
['id' => 'orderby', 'type' => 'select', 'label' => '排序', 'default' => 'count', 'options' => ['count' => '内容数量', 'name' => '名称', 'term_id' => 'ID', 'rand' => '随机']],
['id' => 'show_count', 'type' => 'switcher', 'label' => '显示数量', 'default' => false],
@@ -249,6 +249,7 @@ final class WidgetSchemas {
return [
['id' => 'load_mode', 'type' => 'radio', 'label' => '模块加载方式', 'default' => 'ajax', 'options' => ['detail' => '直接加载', 'ajax' => 'ajax懒加载'], 'description' => 'ajax懒加载:当页面加载完后,根据用户需要在加载当前模块内容,可提高页面渲染效率'],
['id' => 'cat', 'type' => 'text', 'label' => '分类限制', 'default' => '', 'sanitize' => 'reference_ids', 'description' => '限制请填写对应的ID,多个ID请用英文逗号隔开。如:1,2,3。支持负数进行排除,例如:-1,-2,-3。'],
['id' => 'topics', 'type' => 'text', 'label' => '专题限制', 'default' => '', 'sanitize' => 'ids', 'description' => '填写专题 ID,多个 ID 用逗号隔开'],
['id' => 'pay_type', 'type' => 'checkbox', 'label' => '商品类型筛选', 'default' => [], 'options' => self::pay_types(), 'description' => '不做其它筛选'],
['id' => 'limit_day', 'type' => 'spinner', 'label' => '发布时间限制', 'default' => 0, 'min' => 0, 'step' => 5, 'unit' => '天', 'description' => '仅显示最近多少天发布的文章,为0则不限制'],
['id' => 'orderby', 'type' => 'select', 'label' => '排序方式', 'default' => '', 'options' => self::posts_orderby()],
@@ -276,6 +277,7 @@ final class WidgetSchemas {
'fields' => [
['id' => 'title', 'type' => 'textarea', 'label' => '标题(必填)', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1], 'description' => '栏目显示的标题,支持HTML代码,注意代码规范'],
['id' => 'cat', 'type' => 'text', 'label' => '分类限制', 'default' => '', 'sanitize' => 'reference_ids'],
['id' => 'topics', 'type' => 'text', 'label' => '专题限制', 'default' => '', 'sanitize' => 'ids'],
['id' => 'pay_type', 'type' => 'checkbox', 'label' => '商品类型筛选', 'default' => [], 'options' => self::pay_types(), 'description' => '不做其它筛选'],
['id' => 'limit_day', 'type' => 'spinner', 'label' => '发布时间限制', 'default' => 0, 'min' => 0, 'step' => 5, 'unit' => '天', 'description' => '仅显示最近多少天发布的文章,为0则不限制'],
['id' => 'orderby', 'type' => 'select', 'label' => '排序方式', 'default' => '', 'options' => self::posts_orderby()],
@@ -288,6 +290,7 @@ final class WidgetSchemas {
private static function oneline_posts_fields(): array {
return [
['id' => 'cat', 'type' => 'text', 'label' => '分类限制', 'default' => '', 'sanitize' => 'reference_ids', 'description' => '限制请填写对应的ID,多个ID请用英文逗号隔开。如:1,2,3。支持负数进行排除,例如:-1,-2,-3。'],
['id' => 'topics', 'type' => 'text', 'label' => '专题限制', 'default' => '', 'sanitize' => 'ids', 'description' => '填写专题 ID,多个 ID 用逗号隔开'],
['id' => 'pay_type', 'type' => 'checkbox', 'label' => '商品类型筛选', 'default' => [], 'options' => self::pay_types(), 'description' => '不做其它筛选'],
['id' => 'orderby', 'type' => 'select', 'label' => '排序方式', 'default' => '', 'options' => self::posts_orderby()],
['id' => 'order', 'type' => 'radio', 'label' => '顺序', 'default' => 'desc', 'options' => ['desc' => '降序', 'asc' => '升序']],
@@ -312,7 +315,7 @@ final class WidgetSchemas {
private static function term_cards_fields(): array {
return array_merge(self::obs_animation_fields(), [
['id' => 'term_id', 'type' => 'terms', 'label' => '添加分类', 'taxonomy' => 'category', 'default' => [], 'description' => '选择并排序需要的分类,如选择的分类下没有封面则不会显示'],
['id' => 'term_id', 'type' => 'terms', 'label' => '添加分类、专题', 'taxonomy' => ['category', 'topics'], 'default' => [], 'description' => '选择并排序需要的分类、专题,如没有封面则不会显示'],
['id' => 'type', 'type' => 'radio', 'label' => '卡片样式', 'default' => 'style-1', 'options' => ['style-1' => '简单样式', 'style-2' => '样式二', 'style-3' => '样式三', 'style-4' => '样式四']],
['id' => 'height_scale', 'type' => 'spinner', 'label' => '卡片长宽比例', 'default' => 60, 'min' => 20, 'max' => 300, 'step' => 5, 'unit' => '%'],
['id' => 'pc_row', 'type' => 'button_set', 'label' => 'PC端单行排列数量', 'default' => 2, 'options' => self::row_options()],
@@ -325,7 +328,7 @@ final class WidgetSchemas {
private static function term_aggregation_fields(): array {
return array_merge(self::obs_animation_fields(), [
['id' => 'pc_row', 'type' => 'slider', 'label' => '单行布局', 'default' => 2, 'min' => 1, 'max' => 2, 'step' => 1, 'unit' => '个', 'description' => '请根据此模块放置位置的宽度合理调整单行数量'],
['id' => 'term_id', 'type' => 'terms', 'label' => '添加分类', 'taxonomy' => 'category', 'default' => [], 'description' => '选择并排序需要的分类,如选择的分类下没有文章则不会显示'],
['id' => 'term_id', 'type' => 'terms', 'label' => '添加分类、专题', 'taxonomy' => ['category', 'topics'], 'default' => [], 'description' => '选择并排序需要的分类、专题,如没有文章则不会显示'],
['id' => 'orderby', 'type' => 'select', 'label' => '排序方式', 'default' => 'modified', 'options' => self::posts_orderby(), 'dependency' => ['term_id', '!=', '']],
['id' => 'order', 'type' => 'radio', 'label' => '顺序', 'default' => 'desc', 'options' => ['desc' => '降序', 'asc' => '升序'], 'dependency' => ['term_id', '!=', '']],
['id' => 'count', 'type' => 'spinner', 'label' => '最大文章数量', 'default' => 4, 'min' => 1, 'max' => 20, 'step' => 1, 'unit' => '篇', 'dependency' => ['term_id', '!=', ''], 'description' => '<div class="c-yellow">请确保所选的分类内的文章数量均超过此数量,否则会出现布局错位的问题!</div>'],
+91
View File
@@ -0,0 +1,91 @@
<?php
/**
* Topic archive template.
*
* @package Jio
*/
defined('ABSPATH') || exit;
get_header();
$term = get_queried_object();
if (!$term instanceof WP_Term || $term->taxonomy !== 'topics') {
get_footer();
return;
}
$settings = \Jio\Core\TermMeta::resolve_archive_settings($term);
$has_sidebar = jio_archive_has_sidebar(!empty($settings['show_sidebar']));
$sort_state = jio_archive_get_sort_state((array) ($settings['orderby_option'] ?? []));
$filter_groups = [];
foreach ([
'category' => ['key' => 'ajax_category', 'label' => __('分类', 'jio')],
'post_tag' => ['key' => 'ajax_tag', 'label' => __('标签', 'jio')],
'topics' => ['key' => 'ajax_topics', 'label' => __('专题', 'jio')],
] as $taxonomy => $filter) {
$key = $filter['key'];
if (empty($settings[$key . '_enabled'])) {
continue;
}
$group = jio_archive_build_term_filter_group($taxonomy, (array) ($settings[$key] ?? []), $filter['label']);
if ($group) {
$filter_groups[] = $group;
}
}
if (!empty($settings['custom_filter_enabled'])) {
$filter_groups = array_merge(
$filter_groups,
jio_archive_build_custom_filter_groups(
(array) ($settings['custom_dimensions'] ?? []),
$settings['custom_filter_keys'] ?? 'all'
)
);
}
jio_archive_open_shell([
'main_class' => 'site-main container',
'has_sidebar' => $has_sidebar,
'top_fluid' => ['all_top_fluid', 'cat_top_fluid'],
'page_kind' => 'topics',
]);
jio_archive_render_widget_zone(['cat_top_content']);
jio_archive_render_term_header([
'header_style' => $settings['header_style'],
'show_cover' => !empty($settings['show_cover']),
'cover_url' => (string) ($settings['cover_url'] ?? ''),
'title' => $term->name,
'description' => !empty($settings['show_desc']) ? $settings['description'] : '',
'post_count' => !empty($settings['post_count']),
]);
ob_start();
if (!empty($settings['filter_bar'])) {
jio_archive_render_filter_bar([
'aria_label' => __('专题筛选', 'jio'),
'groups' => $filter_groups,
'sort_enabled' => !empty($settings['orderby_enabled']),
'sort_current' => $sort_state['current'],
'sort_links' => $sort_state['links'],
'sort_labels' => \Jio\Settings\Settings::homepage_sort_labels(),
'sort_option' => (array) ($settings['orderby_option'] ?? []),
]);
}
$filter_html = (string) ob_get_clean();
jio_archive_render_stream(null, jio_archive_get_render_config(['context' => 'topics']), [
'page_kind' => 'topics',
'query_context' => 'archive',
'empty_text' => __('该专题下暂无文章', 'jio'),
'filter_html' => $filter_html,
]);
jio_archive_render_widget_zone(['cat_bottom_content']);
jio_archive_close_shell([
'has_sidebar' => $has_sidebar,
'bottom_fluid' => ['cat_bottom_fluid', 'all_bottom_fluid'],
]);
get_footer();
+8
View File
@@ -96,6 +96,12 @@ $categories = array_map(
\Jio\Core\SubmitCategories::tree($settings)
);
$current_cats = $edit_post ? wp_get_post_categories($edit_post->ID) : [];
$topics = array_map(
static fn(array $node): \WP_Term => $node['term'],
\Jio\Core\SubmitTopics::tree($settings)
);
$current_topics = $edit_post ? wp_get_post_terms($edit_post->ID, 'topics', ['fields' => 'ids']) : [];
$current_topics = is_array($current_topics) ? array_map('intval', $current_topics) : [];
$custom_dimensions = [];
$current_cf = [];
@@ -181,6 +187,8 @@ return [
'featured_args' => $featured_args,
'categories' => $categories,
'current_cats' => $current_cats,
'topics' => $topics,
'current_topics' => $current_topics,
'custom_dimensions' => $custom_dimensions,
'current_cf' => $current_cf,
'current_title' => $current_title,
+11 -1
View File
@@ -1,6 +1,6 @@
<?php
/**
* Reference-theme article parameters sidebar.
* Article parameters sidebar.
*
* @var array $ctx
* @package Jio
@@ -41,6 +41,16 @@ require_once get_template_directory() . '/templates/parts/submit/term-checks.php
</div>
</div>
</div>
<?php if (!empty($settings['allow_topics']) && !empty($ctx['topics'])) : ?>
<div class="newpost-topics-drop drop-select dropdown dependency-box">
<button type="button" data-toggle-class="open" data-target=".newpost-topics-drop" class="flex ac jsb drop-btn form-control mb6">
<span class="title-theme222"><i class="fa fas fa-cube mr6" aria-hidden="true"></i><?php esc_html_e('专题', 'jio'); ?></span><i class="ml6 fa fas fa-angle-right em12"></i>
</button>
<div class="dropdown-menu fluid"><div class="mini-scrollbar padding-w10 select-drop-box">
<?php jio_submit_render_term_checks($ctx['topics'], $ctx['current_topics'], 'topics'); ?>
</div></div>
</div>
<?php endif; ?>
<?php if (!empty($settings['custom_filter_s']) && !empty($ctx['custom_dimensions'])) : ?>
<?php foreach ($ctx['custom_dimensions'] as $dimension) :
+9 -14
View File
@@ -13,11 +13,20 @@ defined('ABSPATH') || exit;
if (!function_exists('jio_submit_render_term_checks')) :
function jio_submit_render_term_checks(array $terms, array $checked, string $name, int $parent = 0, int $depth = 0): void {
$children_map = [];
$present = [];
foreach ($terms as $term) {
if ($term instanceof WP_Term) {
$present[(int) $term->term_id] = true;
}
}
foreach ($terms as $term) {
if (!$term instanceof WP_Term) {
continue;
}
$pid = (int) $term->parent;
if ($pid !== 0 && !isset($present[$pid])) {
$pid = 0;
}
if (!isset($children_map[$pid])) {
$children_map[$pid] = [];
}
@@ -38,20 +47,6 @@ if (!function_exists('jio_submit_render_term_checks')) :
}
};
// If include list was used without hierarchy, still render flat.
if (empty($children_map) && !empty($terms)) {
foreach ($terms as $term) {
if (!$term instanceof WP_Term) {
continue;
}
$tid = (int) $term->term_id;
echo '<div><label class="jio-submit-check">';
echo '<input type="checkbox" name="' . esc_attr($name) . '[]" value="' . esc_attr((string) $tid) . '"' . (in_array($tid, $checked, true) ? ' checked' : '') . '>';
echo '<span>' . esc_html($term->name) . '</span></label></div>';
}
return;
}
$render($parent, $depth);
}
endif;
@@ -1,9 +1,4 @@
/**
* 高级筛选设置页参考主题 1:1 契约。
*
* 参考实现「高级筛选」区块与前台链路(分类/标签归档的 AJAX 选项菜单、自定义
* 筛选按钮、每页筛选条数)。
*/
/** 高级筛选设置页与归档查询的字段契约。 */
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
@@ -23,9 +18,9 @@ const tagTpl = read('tag.php');
const listJs = read('assets/js/runtime/main.js');
const globalController = read('src/Admin/GlobalController.php');
// 参考主题字段键(顺序即参考主题定义顺序)。
// 保存字段的顺序。
const REFERENCE_KEYS = ['custom_filter_show', 'custom_filter', 'cat_custom_filter'];
// Jio 私有旧字段(参考主题无此配置,须移除避免双路)。
// 已移除的旧字段。
const LEGACY_KEYS = [
'filter_enabled',
'filter_categories',
@@ -37,8 +32,6 @@ const LEGACY_KEYS = [
'filter_tag_show_cat',
'filter_tag_show_tag',
];
// 参考主题字段中本项目不做的部分(无 topics 专题分类法)。
const EXCLUDED_KEYS = ['topics_s', 'topics_lists'];
test('JS schema 字段键与顺序对齐参考主题', () => {
const found = [...js.matchAll(/key:\s*'([a-z0-9_]+)',\s*(?:label|type):/g)].map((m) => m[1]);
@@ -53,7 +46,7 @@ test('JS schema 字段键与顺序对齐参考主题', () => {
assert.ok(new RegExp(`key:\\s*'${sub}',\\s*label:`).test(js), `custom_filter 缺少子字段 ${sub}`);
}
const catSub = js.slice(js.indexOf("key: 'cat_custom_filter'"));
const catOrder = ['cats', 'cat_s', 'cat_lists', 'tag_s', 'tag_lists', 'orderby_s', 'orderby_lists', 'custom_filters'];
const catOrder = ['cats', 'cat_s', 'cat_lists', 'tag_s', 'tag_lists', 'topics_s', 'topics_lists', 'orderby_s', 'orderby_lists', 'custom_filters'];
const catIdx = catOrder.map((k) => catSub.indexOf(`key: '${k}'`));
assert.ok(catIdx.every((v) => v !== -1), 'cat_custom_filter 缺少子字段');
assert.ok(catIdx.every((v, i) => i === 0 || v > catIdx[i - 1]), 'cat_custom_filter 子字段顺序应符合参考主题');
@@ -66,7 +59,7 @@ test('PHP schema 字段键与顺序对齐参考主题', () => {
const order = REFERENCE_KEYS.map((k) => filterSection.indexOf(`'${k}'`));
assert.ok(order.every((v) => v !== -1), `PHP schema 缺少字段: ${REFERENCE_KEYS.filter((k) => order.indexOf(-1) === -1)}`);
assert.ok(order.every((v, i) => i === 0 || v > order[i - 1]), 'PHP 字段顺序应符合参考主题');
const catOrder = ['cats', 'cat_s', 'cat_lists', 'tag_s', 'tag_lists', 'orderby_s', 'orderby_lists', 'custom_filters'];
const catOrder = ['cats', 'cat_s', 'cat_lists', 'tag_s', 'tag_lists', 'topics_s', 'topics_lists', 'orderby_s', 'orderby_lists', 'custom_filters'];
const catSub = filterSection.slice(filterSection.indexOf("'cat_custom_filter'"));
const catIdx = catOrder.map((k) => catSub.indexOf(`'${k}'`));
assert.ok(catIdx.every((v) => v !== -1), 'PHP cat_custom_filter 缺少子字段');
@@ -77,36 +70,36 @@ test('JS schema 默认值、选项与依赖对齐参考主题', () => {
assert.match(js, /custom_filter_show:\s*\[\]/);
assert.match(js, /custom_filter:\s*\[\]/);
assert.match(js, /cat_custom_filter:\s*\[\]/);
for (const option of ['分类页面显示', '标签页面显示']) {
for (const option of ['分类页面显示', '标签页面显示', '专题页面显示']) {
assert.ok(js.includes(option), `custom_filter_show 缺少选项 ${option}`);
}
// 排序方式默认值与参考主题一致(orderby() default)。
// 排序方式默认值。
assert.match(js, /default:\s*\[\s*'modified',\s*'views',\s*'like',\s*'comment_count'\s*\]/);
for (const option of ['更新', '发布', '浏览', '点赞', '评论', '收藏', '随机']) {
assert.ok(js.includes(`label: '${option}'`), `排序方式缺少选项 ${option}`);
}
// 依赖链(参考主题 dependency)。
// 表单依赖链。
assert.match(js, /key: 'cat_lists',[\s\S]{0,200}?dependsOn:\s*\{\s*key:\s*'cat_s',\s*value:\s*true\s*\}/);
assert.match(js, /key: 'tag_lists',[\s\S]{0,200}?dependsOn:\s*\{\s*key:\s*'tag_s',\s*value:\s*true\s*\}/);
assert.match(js, /key: 'topics_lists',[\s\S]{0,200}?dependsOn:\s*\{\s*key:\s*'topics_s',\s*value:\s*true\s*\}/);
assert.match(js, /key: 'orderby_lists',[\s\S]{0,220}?dependsOn:\s*\{\s*key:\s*'orderby_s',\s*value:\s*true\s*\}/);
// custom_filters 为动态选项 checkbox(引用 custom_filter 维度定义)。
assert.match(js, /key: 'custom_filters',[\s\S]{0,400}?options:\s*'custom_filter'/);
// 提示行承载筛选说明,且不得再指向第三方站点。
assert.match(js, /type:\s*'content'/);
assert.match(js, /ui_only:\s*true/);
assert.doesNotMatch(js, /zibll\.com/);
assert.match(js, /避免与其他mate_key重复/);
});
test('旧字段与 topics 在 schema 中均已移除(无双路)', () => {
test('旧字段已移除,专题筛选由单一字段组驱动', () => {
for (const legacy of LEGACY_KEYS) {
assert.ok(!js.includes(legacy), `JS schema 残留旧字段: ${legacy}`);
assert.ok(!php.includes(legacy), `PHP schema 残留旧字段: ${legacy}`);
assert.ok(!postDisplay.includes(`$settings['${legacy}']`), `PostDisplay 残留旧字段读取: ${legacy}`);
}
for (const excluded of EXCLUDED_KEYS) {
assert.ok(!js.includes(excluded), `JS schema 不应包含 ${excluded}(本项目无专题分类法)`);
assert.ok(!php.includes(excluded), `PHP schema 不应包含 ${excluded}`);
for (const key of ['topics_s', 'topics_lists']) {
assert.ok(js.includes(`key: '${key}'`), `JS schema 缺少 ${key}`);
assert.ok(php.includes(`'key' => '${key}'`), `PHP schema 缺少 ${key}`);
}
// 运行时只读新键。
assert.match(postDisplay, /\$settings\['custom_filter_show'\]/);
@@ -119,7 +112,7 @@ test('JS schema 与 PHP schema 一致性', () => {
for (const key of REFERENCE_KEYS) {
assert.ok(php.includes(`'${key}'`), `PHP schema 缺少 ${key}`);
}
// addText 与参考主题 button_title 一致。
// 添加按钮文案。
assert.match(js, /addText:\s*'添加筛选类别'/);
assert.match(js, /addText:\s*'添加值'/);
assert.match(js, /addText:\s*'添加自定义分类筛选'/);
@@ -147,7 +140,7 @@ test('前台链路:覆盖与门禁', () => {
});
test('前台链路:维度筛选渲染与查询过滤', () => {
// 渲染:维度 chip 组(全部 chip 参考主题 focus 语义)。
// 渲染维度按钮组。
assert.match(filtersHelper, /function jio_archive_build_custom_filter_groups\(/);
assert.match(filtersHelper, /ajax-replace="true" class="ajax-next/);
assert.match(filtersHelper, /\$keys !== 'all' && !in_array\(\$dim_key, \(array\) \$keys, true\)/);
@@ -1,20 +1,6 @@
'use strict';
/**
* 「前台投稿」后台设置页契约(对齐参考主题 1:1,不含专题):
* 1) PHP schema(SchemaPostList.php submit section)字段键、顺序、默认值与
* JS schema(assets/admin/src/schema/sections/post-list/submit.js)一致;
* 2) 参考主题字段全集存在:post_article_s / post_article_btn_txte / post_article_cat /
* post_article_custom_filter_s / post_article_tag / post_article_user /
* post_article_title_strlen_limit / post_article_pay_free_s /
* post_article_pay_price_limit / post_article_pay_vip_price_s /
* post_article_pay_vip_1_discount / post_article_pay_vip_2_discount /
* post_t_placeholder / post_c_placeholder;
* 3) 专题(topics)相关字段不存在;
* 4) 旧 Jio 私有字段(front_submit_*、post_article_title_strlen_limit_min/max、
* post_article_pay_price_min/max)已彻底移除,无双路兼容;
* 5) 运行时(PostDisplay::get_submit_settings)只读取新键。
*/
/** 前台投稿设置的字段顺序、默认值与运行时契约。 */
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -30,6 +16,8 @@ const REFERENCE_KEYS = [
'post_article_s',
'post_article_btn_txte',
'post_article_cat',
'post_article_topics_s',
'post_article_topics',
'post_article_custom_filter_s',
'post_article_tag',
'post_article_user',
@@ -114,9 +102,9 @@ test('PHP submit section 字段键与参考主题一致且顺序相同', () => {
assert.deepEqual(found, PHP_FIELD_ORDER);
});
test('PHP submit section 不含专题字段', () => {
assert.doesNotMatch(submitPhp, /post_article_topics/);
assert.doesNotMatch(submitPhp, /'topics'/);
test('PHP submit section 包含专题开关与可选专题', () => {
assert.match(submitPhp, /'post_article_topics_s',\s*'label'\s*=>\s*'允许选择专题',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
assert.match(submitPhp, /'post_article_topics',\s*'label'\s*=>\s*'投稿允许选择的专题',\s*'type'\s*=>\s*'taxonomy_select',\s*'taxonomy'\s*=>\s*'topics'/);
});
test('PHP submit section 默认值对齐参考主题', () => {
@@ -154,7 +142,7 @@ test('JS schema 与 PHP 字段键一致(ui_only content 行除外)', () => {
for (const legacy of LEGACY_KEYS) {
assert.ok(!keys.includes(legacy), `JS schema 残留旧字段: ${legacy}`);
}
// 两个 ui_only 提示行承载参考主题 desc 中的【去设置】链接。
// 两个 ui_only 提示行承载【去设置】链接。
assert.match(js, /permissions\/capabilities/);
assert.match(js, /post-list\/filter/);
assert.match(js, /ui_only:\s*true/);
+48
View File
@@ -0,0 +1,48 @@
<?php
define('ABSPATH', __DIR__);
class WP_Term {
public function __construct(
public int $term_id,
public int $parent,
public string $name
) {}
}
function absint($value): int { return abs((int) $value); }
function esc_attr($value): string { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); }
function esc_html($value): string { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); }
function get_terms(array $args): array {
if ($args['taxonomy'] !== 'topics') return [];
return [new WP_Term(1, 0, 'Parent'), new WP_Term(2, 1, 'Child'), new WP_Term(3, 1, 'Sibling'), new WP_Term(4, 0, 'Other')];
}
function get_term_children(int $id, string $taxonomy): array {
return $taxonomy === 'topics' && $id === 1 ? [2, 3] : [];
}
function term_exists(int $id, string $taxonomy): bool {
return $taxonomy === 'topics' && in_array($id, [1, 2, 3, 4], true);
}
require dirname(__DIR__, 2) . '/src/Core/SubmitTopics.php';
require dirname(__DIR__, 2) . '/templates/parts/submit/term-checks.php';
$root = ['allow_topics' => true, 'allowed_topic_ids' => [1]];
$child = ['allow_topics' => true, 'allowed_topic_ids' => [2]];
$disabled = ['allow_topics' => false, 'allowed_topic_ids' => []];
$tree = static fn(array $settings): array => array_map(
static fn(array $node): array => ['id' => $node['term']->term_id, 'depth' => $node['depth']],
\Jio\Core\SubmitTopics::tree($settings)
);
ob_start();
jio_submit_render_term_checks([new WP_Term(2, 1, 'Child')], [], 'topics');
$orphan_html = ob_get_clean();
echo json_encode([
'root_tree' => $tree($root),
'child_tree' => $tree($child),
'disabled_tree' => $tree($disabled),
'filtered' => \Jio\Core\SubmitTopics::filter_ids([2, 4, 999, 2], $child),
'disabled_filtered' => \Jio\Core\SubmitTopics::filter_ids([2], $disabled),
'orphan_html' => $orphan_html,
], JSON_THROW_ON_ERROR);
+22
View File
@@ -0,0 +1,22 @@
const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const path = require('node:path');
const test = require('node:test');
const result = JSON.parse(execFileSync('php', [path.join(__dirname, 'topics-harness.php')], { encoding: 'utf8' }));
test('topic whitelist includes descendants and preserves a selected child without its parent', () => {
assert.deepEqual(result.root_tree, [
{ id: 1, depth: 0 },
{ id: 2, depth: 1 },
{ id: 3, depth: 1 },
]);
assert.deepEqual(result.child_tree, [{ id: 2, depth: 0 }]);
assert.match(result.orphan_html, /name="topics\[\]" value="2"/);
});
test('topic submissions accept only existing terms within the configured whitelist', () => {
assert.deepEqual(result.filtered, [2]);
assert.deepEqual(result.disabled_tree, []);
assert.deepEqual(result.disabled_filtered, []);
});
@@ -548,7 +548,8 @@ test('publish settings use a multi-select category picker and a creatable tag in
assert.match(category, /createPortal/);
assert.match(category, /type="checkbox"/);
assert.match(category, /onChange\(/);
assert.match(category, /选择分类(可多选)/);
assert.match(category, /选择%s(可多选)/);
assert.match(settings, /label=\{__\('专题', 'jio'\)\}/);
// 旧的分类平铺复选列表已移除(自定义分类法仍是复选标签,不受影响)。
assert.doesNotMatch(settings, /toggleCategory/);
assert.doesNotMatch(settings, /categories\.includes\(/);
@@ -11,7 +11,7 @@ test('modern widget schemas expose reference field names and defaults', () => {
for (const field of ['mini_opt', 'paginate', 'tabs', "'id' => 'cat'", "'id' => 'pay_type'", 'layout_ratio', 'slide_scale', 'm_layout_type', 'm_cover_ratio', 'image_layer', 'image_select']) {
assert.match(source, new RegExp(field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
assert.doesNotMatch(source, /'id' => 'topics'/);
assert.match(source, /'id' => 'topics'/);
assert.match(source, /'热门推荐'.*'orderby' => 'views'/s);
assert.match(source, /'最近更新'.*'orderby' => 'modified'/s);
assert.match(source, /'猜你喜欢'.*'orderby' => 'rand'/s);
@@ -62,15 +62,15 @@ function topLevelFieldIds(name) {
return [...functionBody(name).matchAll(/self::field\('([^']+)'/g)].map(match => match[1])
}
test('page display exposes exactly eight ordered routes', () => {
const expected = ['header-nav', 'header-components', 'footer', 'homepage', 'category', 'tag', 'user-home', 'other']
test('page display exposes nine ordered routes', () => {
const expected = ['header-nav', 'header-components', 'footer', 'homepage', 'category', 'tag', 'topics', 'user-home', 'other']
const schemaOrder = [...schema.matchAll(/^\t\t\t\t'([^']+)'\s+=> self::/gm)].map(match => match[1])
assert.deepEqual(schemaOrder, expected)
const routeOrder = [...routes.matchAll(/section:\s*'([^']+)'/g)].map(match => match[1])
assert.deepEqual(routeOrder, expected)
})
test('top-level field IDs match the legacy V9.0 eight-page contract', () => {
test('top-level field IDs cover all nine page settings', () => {
assert.deepEqual(topLevelFieldIds('header_nav'), [
'header_theme_custom', 'header_theme_bg_custom', 'header_theme_bg', 'header_theme_color_custom',
'header_layout', 'mobile_header_layout', 'mobile_navbar_align', 'nav_fixed', 'nav_scroll_down_hide',
@@ -96,10 +96,17 @@ test('top-level field IDs match the legacy V9.0 eight-page contract', () => {
assert.deepEqual(topLevelFieldIds('category'), [
'page_cover_cat_s', 'cat_post_count_s', 'cat_default_cover', 'cat_orderby_s', 'cat_orderby_option',
'ajax_list_cat_cat', 'ajax_list_option_cat_cat', 'ajax_list_cat_tag', 'ajax_list_option_cat_tag',
'ajax_list_cat_topics', 'ajax_list_option_cat_topics',
])
assert.deepEqual(topLevelFieldIds('tag'), [
'tag_post_count_s', 'page_cover_tag_s', 'tag_default_cover', 'tag_orderby_s', 'tag_orderby_option',
'ajax_list_tag_cat', 'ajax_list_option_tag_cat', 'ajax_list_tag_tag', 'ajax_list_option_tag_tag',
'ajax_list_tag_topics', 'ajax_list_option_tag_topics',
])
assert.deepEqual(topLevelFieldIds('topics'), [
'topics_post_count_s', 'topics_default_cover', 'topics_orderby_s', 'topics_orderby_option',
'ajax_list_topics_cat', 'ajax_list_option_topics_cat', 'ajax_list_topics_tag',
'ajax_list_option_topics_tag', 'ajax_list_topics_topics', 'ajax_list_option_topics_topics',
])
assert.deepEqual(topLevelFieldIds('user_home'), ['author_main_tab_opt', 'author_main_tab_swiper'])
assert.deepEqual(topLevelFieldIds('other'), ['page_header_style', 'page_header_cover_img'])
@@ -115,7 +122,7 @@ test('defaults, nested structures, dependencies, and excluded product families a
assert.match(schema, /'header_slider_card_s'.*?'dependsOn' => self::not_empty\('header_slider_show'\)/)
assert.match(schema, /'header_slider_filter_blur'.*?'dependsOn' => self::not_empty\('header_slider_show'\)/)
assert.match(schema, /'all' => \[self::enabled\('home_posts_list_s'\), self::enabled\('home_list1_orderby_s'\)\]/)
for (const dead of ['nav_newposts_btns', 'forum_home', 'forum_plate', 'forum_post', 'ajax_list_cat_topics', 'ajax_list_tag_topics']) {
for (const dead of ['nav_newposts_btns', 'forum_home', 'forum_plate', 'forum_post']) {
assert.equal(schema.includes(dead), false, `legacy/excluded field remains: ${dead}`)
}
assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false)
@@ -209,7 +216,7 @@ test('schema REST saves are privileged, section-whitelisted, and deeply sanitize
assert.match(provider, /wp_kses_post/)
assert.match(provider, /esc_url_raw/)
assert.match(provider, /has an invalid value/)
assert.match(controller, /search\/\(\?P<source>posts\|categories\|tags\)/)
assert.match(controller, /search\/\(\?P<source>posts\|categories\|tags\|topics\|terms\)/)
assert.match(controller, /'permission_callback' => \[\$this, 'check_permission'\]/)
})
@@ -13,15 +13,11 @@ const pay = read('inc/helpers/jio-pay.php')
const metabox = read('src/Core/PostMetaBox.php')
const resourceBox = read('src/Core/ResourceMetaBox.php')
test('quick and bulk edit boxes hook the post list with no topics column', () => {
// 参考主题把配置项挂在「专题」分类法列上;本项目没有专题,统一挂在互动数据列。
test('quick and bulk edit boxes hook the topics taxonomy column', () => {
assert.match(list, /quick_edit_custom_box/)
assert.match(list, /bulk_edit_custom_box/)
assert.match(list, /COLUMN_STATS/)
// 代码里不得再出现专题分类法列(类文档块的说明除外,只看 namespace 之后的部分)。
const code = list.slice(list.indexOf('namespace Jio'))
assert.doesNotMatch(code, /taxonomy-topics/)
assert.doesNotMatch(code, /topics/)
assert.match(code, /\$column_name !== 'taxonomy-topics'/)
})
test('every quick-edit field group is rendered', () => {
@@ -87,8 +87,8 @@ test('settings use legacy list modes, grouped options, and destructive related k
for (const key of legacyKeys) {
assert.equal(source.includes(key), false, `legacy article key remains: ${key}`);
}
// bare `topics` is legal again: the related-posts query mirrors the legacy taxonomy OR.
assert.doesNotMatch(source, /list_card_topics|is_topics/);
assert.match(listSchema, /list_card_topics: false/);
assert.match(source, /list_card_topics/);
});
test('forced mode wins and thumb_if_has ignores spare and lazy placeholders', () => {
@@ -275,7 +275,7 @@ test('related posts, author page and widgets use their dedicated renderers', ()
// 翻页 / 切 tab 是整页替换语义(author.js 最终就是 panel.innerHTML = html),
// 骨架必须独占内容区。若骨架只是追加在旧卡片下方,加载期会先看到
// 「第 1 页卡片 + 脚下一摞骨架」再换页。对齐参考主题 post_ajax() 的
// 「第 1 页卡片 + 脚下一摞骨架」再换页。
// beforeSend:replace 时 $item.remove() + find(pag).remove()。
// 刻意不按 data-view 收窄:card 模式下真实卡片同样是直接子级 .posts-item,
// 也要一起收起,否则卡片面板旧内容不会让位。
@@ -572,7 +572,7 @@ test('profile routes load versioned leaf styles and settings dialogs use the sha
assert.match(userCenterCreditsCss, /#panel-credits\.dashboard-panel\s*\{[^}]*background:\s*transparent;[^}]*box-shadow:\s*none;/);
assert.match(userCenterCreditsCss, /\.uc-asset-tabs\s*\{[^}]*background:\s*var\(--surface-1,\s*#fff\);[^}]*border:[^;}]+;[^}]*box-shadow:[^;}]+;/);
assert.doesNotMatch(profileCredits, /credits-signin\.php|data-jio-signin-calendar/);
// 签到详情弹窗为参考主题 1:1 结构(src/Credits/Signin.php),日历已随
// 签到详情弹窗的日历已随
// 旧版 lite 弹窗一并移除;用户中心里的孤儿渲染器也已整包移除。
assert.equal(fs.existsSync(path.join(root, 'inc/helpers/user-center-renderers/credits-signin.php')), false);
assert.doesNotMatch(userCenterCreditsCss + read('assets/css/frontend/enhancements/components/custom-amount.css'), /uc-signin-calendar/);
+7 -2
View File
@@ -113,7 +113,10 @@ final class WidgetSystemTest {
], 3);
$this->same('1,-2,3', $reference['cat'], 'reference cat preserves cleaned signed IDs for WP_Query descendant semantics');
$this->false(isset($reference['category__in']) || isset($reference['category__not_in']), 'reference cat does not narrow matches to direct categories');
$this->false(isset($reference['tax_query']), 'topic filters are removed with the topics feature');
$this->false(isset($reference['tax_query']), 'queries without a topic filter remain unrestricted');
$topic_query = PostQuery::arguments(['count' => 6, 'topics' => '7,8'], 1);
$this->same('topics', $topic_query['tax_query'][0]['taxonomy'], 'topic query uses the topics taxonomy');
$this->same([7, 8], $topic_query['tax_query'][0]['terms'], 'topic IDs reach the taxonomy query');
$this->same('jio_post_views', $reference['meta_key'], 'reference view ordering uses numeric view meta');
$this->false(isset($reference['meta_query']), 'modern reference queries do not apply commerce filters by default');
$this->same(1, $reference['paged'], 'no-pagination queries stay on first page');
@@ -148,7 +151,9 @@ final class WidgetSystemTest {
'order' => 'asc',
]]]);
$this->same('1,-2,3', $tab['tabs'][0]['cat'], 'tab cat keeps signed reference IDs');
$this->false(isset($tab['tabs'][0]['topics']), 'topic filters are gone from tab configuration');
$this->same('', $tab['tabs'][0]['topics'], 'tab exposes the topic filter');
$topic_tab = FieldSchema::normalize($tab_fields, ['tabs' => [['title' => '专题', 'topics' => '7,8']]]);
$this->same('7,8', $topic_tab['tabs'][0]['topics'], 'tab preserves selected topic IDs');
$this->same(['1', '6'], $tab['tabs'][0]['pay_type'], 'tab commerce filters are preserved');
$this->contains('<b>栏目</b>', $tab['tabs'][0]['title'], 'tab title keeps controlled HTML');
$this->false(str_contains($tab['tabs'][0]['title'], '<script>'), 'tab title strips unsafe HTML');
@@ -120,6 +120,7 @@ namespace {
function get_the_ID(): int { return (int) ($GLOBALS['tab_fixture_current_post']->ID ?? 0); }
function strip_shortcodes(string $value): string { return $value; }
function get_the_category(int $id = 0): array { unset($id); return []; }
function get_the_terms(int $id, string $taxonomy): array { unset($id, $taxonomy); return []; }
function get_the_tags(int $id = 0): array { unset($id); return []; }
function get_the_excerpt(int $id = 0): string { unset($id); return '首页通用文章列表的长摘要,这里只用于卡片和列表样式的内容渲染'; }
function get_post_field(string $field, int $id = 0, string $context = 'display'): string {