From a254cbd3e979ba2abc1a148cc824de7e8bf1454a Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 10 Nov 2026 22:31:11 +0800 Subject: [PATCH] feat(topics): restore topic taxonomy workflow --- .../src/constants/sections/page-display.js | 9 ++ .../src/schema/sections/global/search.js | 3 +- .../src/schema/sections/post-list/filter.js | 15 ++- .../src/schema/sections/post-list/list.js | 12 +- .../src/schema/sections/post-list/submit.js | 4 + .../src/components/category-select.tsx | 15 ++- .../src/components/publish-settings.tsx | 12 ++ assets/creator-studio/src/draft.ts | 3 + assets/creator-studio/src/index.tsx | 7 + functions.php | 2 +- inc/core-hooks.php | 5 - inc/helpers/homepage/header-components.php | 3 + inc/helpers/homepage/layout.php | 4 +- inc/helpers/jio-archive-filters.php | 15 ++- inc/helpers/jio-archive.php | 3 + inc/helpers/jio-homepage.php | 4 +- inc/helpers/jio-single-components.php | 11 +- inc/helpers/jio-widgets-markup.php | 48 ++++--- inc/helpers/posts-renderer/config.php | 4 +- inc/helpers/posts-renderer/post-data.php | 12 ++ inc/helpers/template-tags/search-meta.php | 3 +- inc/helpers/template-tags/site-layout.php | 9 +- src/Admin/Native/PostListEnhance.php | 9 +- src/Admin/PageDisplayController.php | 9 +- src/Admin/Schema/GlobalSchema.php | 3 +- .../Schema/PostList/SchemaPostListList.php | 7 +- src/Admin/Schema/SchemaPageDisplay.php | 29 +++- src/Admin/Schema/SchemaPostList.php | 8 ++ src/Core/CreatorRest.php | 22 ++++ src/Core/DomainModel.php | 1 - src/Core/FrontSubmitSave.php | 5 + src/Core/Setup.php | 26 ++++ src/Core/SubmissionVersion.php | 7 + src/Core/SubmitTopics.php | 88 +++++++++++++ src/Core/Templates.php | 7 - src/Core/TermMeta.php | 124 ++++++++++++++---- src/Settings/DisplayPageGroups.php | 2 + src/Settings/FacadeBulk.php | 40 +++++- src/Settings/PostDisplay.php | 15 ++- src/Settings/ThemeSearch.php | 6 +- src/WidgetSystem/FormRenderer.php | 11 +- src/WidgetSystem/PostQuery.php | 8 ++ .../Renderers/ContentRenderer.php | 9 +- src/WidgetSystem/WidgetSchemas.php | 9 +- taxonomy-topics.php | 91 +++++++++++++ templates/parts/submit/context.php | 8 ++ templates/parts/submit/sidebar-terms.php | 12 +- templates/parts/submit/term-checks.php | 23 ++-- ...ilter-settings-reference-contract.test.cjs | 37 +++--- ...ubmit-settings-reference-contract.test.cjs | 26 +--- tests/front-submit/topics-harness.php | 48 +++++++ tests/front-submit/topics.test.cjs | 22 ++++ .../creator-studio-contract.test.cjs | 3 +- .../modern-widgets-contract.test.cjs | 2 +- .../page-display-contract.test.cjs | 17 ++- .../post-list-quick-edit-contract.test.cjs | 8 +- .../post-visual-contract.test.cjs | 8 +- tests/widget-system/run.php | 9 +- .../tab-posts-render-harness.php | 1 + 59 files changed, 771 insertions(+), 192 deletions(-) create mode 100644 src/Core/SubmitTopics.php create mode 100644 taxonomy-topics.php create mode 100644 tests/front-submit/topics-harness.php create mode 100644 tests/front-submit/topics.test.cjs diff --git a/assets/admin/src/constants/sections/page-display.js b/assets/admin/src/constants/sections/page-display.js index 3a752a7e..273f9a48 100644 --- a/assets/admin/src/constants/sections/page-display.js +++ b/assets/admin/src/constants/sections/page-display.js @@ -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', diff --git a/assets/admin/src/schema/sections/global/search.js b/assets/admin/src/schema/sections/global/search.js index 81fb9e04..2825a918 100644 --- a/assets/admin/src/schema/sections/global/search.js +++ b/assets/admin/src/schema/sections/global/search.js @@ -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: [] }, ] }, ], }, diff --git a/assets/admin/src/schema/sections/post-list/filter.js b/assets/admin/src/schema/sections/post-list/filter.js index 578684d4..dc229cff 100644 --- a/assets/admin/src/schema/sections/post-list/filter.js +++ b/assets/admin/src/schema/sections/post-list/filter.js @@ -18,7 +18,7 @@ export const filterSchema = { key: 'filter_intro', type: 'content', ui_only: true, - content: '

文章高级筛选功能:通过此功能可实现更加复杂、精细化的内容筛选
添加筛选类型和内容值之后,编写文章时候,可以进行选择,且在前台也可以实现精准筛选
例如影视网站:[类型]有[剧情/喜剧/悬疑/惊悚/犯罪],[地区]有[大陆/美国/日韩/港台/印度],[年份]有[2022/2021/2020/10年代/00年代]等
注意事项:添加类型key时候,只能使用英文加下划线,不能有空格,且尽量复杂一点(不能为tag,category等系统已存在的类型),避免与其他mate_key重复!类型名称不能为分类、标签等系统已存在的类型

', + content: '

文章高级筛选功能:通过此功能可实现更加复杂、精细化的内容筛选
添加筛选类型和内容值之后,编写文章时候,可以进行选择,且在前台也可以实现精准筛选
例如影视网站:[类型]有[剧情/喜剧/悬疑/惊悚/犯罪],[地区]有[大陆/美国/日韩/港台/印度],[年份]有[2022/2021/2020/10年代/00年代]等
注意事项:添加类型key时候,只能使用英文加下划线,不能有空格,且尽量复杂一点(不能为tag,category,topics等系统已存在的类型),避免与其他mate_key重复!类型名称不能为分类、专题、标签等系统已存在的类型

', }, { 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', diff --git a/assets/admin/src/schema/sections/post-list/list.js b/assets/admin/src/schema/sections/post-list/list.js index 8104ebe3..1231d3ee 100644 --- a/assets/admin/src/schema/sections/post-list/list.js +++ b/assets/admin/src/schema/sections/post-list/list.js @@ -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' }, diff --git a/assets/admin/src/schema/sections/post-list/submit.js b/assets/admin/src/schema/sections/post-list/submit.js index a80caa50..998d1d55 100644 --- a/assets/admin/src/schema/sections/post-list/submit.js +++ b/assets/admin/src/schema/sections/post-list/submit.js @@ -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', diff --git a/assets/creator-studio/src/components/category-select.tsx b/assets/creator-studio/src/components/category-select.tsx index 8cebc69b..c5ada5e7 100644 --- a/assets/creator-studio/src/components/category-select.tsx +++ b/assets/creator-studio/src/components/category-select.tsx @@ -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({ @@ -71,16 +72,16 @@ export function CategorySelect({ options, value, onChange }: Props) { setQuery(event.target.value)} /> -
+
{filtered.length === 0 && ( -

{__('没有匹配的分类', 'jio')}

+

{sprintf(__('没有匹配的%s', 'jio'), label)}

)} {filtered.map((option) => { const active = selected.has(option.id); @@ -133,7 +134,7 @@ export function CategorySelect({ options, value, onChange }: Props) { > {chosen.length === 0 ? ( - {__('选择分类(可多选)', 'jio')} + {sprintf(__('选择%s(可多选)', 'jio'), label)} ) : ( chosen.map((option) => ( @@ -143,7 +144,7 @@ export function CategorySelect({ options, value, onChange }: Props) {
+ {taxonomy.topics.enabled && taxonomy.topics.items.length > 0 && ( +
+
{__('专题', 'jio')}
+ +
+ )} + {taxonomy.tags.enabled && (
diff --git a/assets/creator-studio/src/draft.ts b/assets/creator-studio/src/draft.ts index 68106068..18621829 100644 --- a/assets/creator-studio/src/draft.ts +++ b/assets/creator-studio/src/draft.ts @@ -57,6 +57,7 @@ export type Draft = { content: string; schedule: Schedule; categories: number[]; + topics: number[]; tags: string[]; custom: Record; featured: Featured; @@ -66,6 +67,7 @@ export type Draft = { export function initialDraft(bootstrap: Record): 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): 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: {} }, diff --git a/assets/creator-studio/src/index.tsx b/assets/creator-studio/src/index.tsx index 84804222..95d3793c 100644 --- a/assets/creator-studio/src/index.tsx +++ b/assets/creator-studio/src/index.tsx @@ -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)} diff --git a/functions.php b/functions.php index 758fd4e3..5246ba66 100644 --- a/functions.php +++ b/functions.php @@ -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()); diff --git a/inc/core-hooks.php b/inc/core-hooks.php index 55a9dd58..748318c2 100644 --- a/inc/core-hooks.php +++ b/inc/core-hooks.php @@ -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([ diff --git a/inc/helpers/homepage/header-components.php b/inc/helpers/homepage/header-components.php index 43f13588..79463fa1 100644 --- a/inc/helpers/homepage/header-components.php +++ b/inc/helpers/homepage/header-components.php @@ -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'; } diff --git a/inc/helpers/homepage/layout.php b/inc/helpers/homepage/layout.php index c71653d2..04a4a895 100644 --- a/inc/helpers/homepage/layout.php +++ b/inc/helpers/homepage/layout.php @@ -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 .= '
'; diff --git a/inc/helpers/jio-archive-filters.php b/inc/helpers/jio-archive-filters.php index e94be46b..114a5cc7 100644 --- a/inc/helpers/jio-archive-filters.php +++ b/inc/helpers/jio-archive-filters.php @@ -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 = '' . esc_html((string) $args['title']); + $icon = $is_topic ? 'fa-cube' : ($is_tag ? 'fa-tags' : 'fa-folder-open'); + $title = '' . 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 ? '' . esc_html($edit_label) . '' : ''; @@ -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 $option lists/dropdown/dropdown_lists fieldset. * @param string $label Visible group label. * @return array */ 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; } diff --git a/inc/helpers/jio-archive.php b/inc/helpers/jio-archive.php index 79fa38d5..2bcf1a2c 100644 --- a/inc/helpers/jio-archive.php +++ b/inc/helpers/jio-archive.php @@ -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', ]; diff --git a/inc/helpers/jio-homepage.php b/inc/helpers/jio-homepage.php index 8ed1f6b5..f7133aad 100644 --- a/inc/helpers/jio-homepage.php +++ b/inc/helpers/jio-homepage.php @@ -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; } diff --git a/inc/helpers/jio-single-components.php b/inc/helpers/jio-single-components.php index b87c81ec..53f679ea 100644 --- a/inc/helpers/jio-single-components.php +++ b/inc/helpers/jio-single-components.php @@ -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')) : ' ', ['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', + ' ', + ['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 ''; } diff --git a/inc/helpers/jio-widgets-markup.php b/inc/helpers/jio-widgets-markup.php index 649ac85f..9d046751 100644 --- a/inc/helpers/jio-widgets-markup.php +++ b/inc/helpers/jio-widgets-markup.php @@ -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 $args {term_id, target_blank, orderby, order, count}. + * @param array $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 = '' . esc_html($term->name) . ''; $description = wp_kses_post((string) $term->description); if ($description === '' && current_user_can('manage_categories')) { - $description = esc_html__('请在WordPress后台-文章-文章分类中添加描述!', 'jio') - . '' . esc_html__('立即编辑', 'jio') . ''; + $description = $taxonomy === 'topics' + ? esc_html__('请在WordPress后台-文章-专题中添加描述!', 'jio') + : esc_html__('请在WordPress后台-文章-文章分类中添加描述!', 'jio'); + $description .= '' . esc_html__('立即编辑', 'jio') . ''; } $description = '
' . $description . '
'; $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']), diff --git a/inc/helpers/posts-renderer/config.php b/inc/helpers/posts-renderer/config.php index 323f251e..8d690683 100644 --- a/inc/helpers/posts-renderer/config.php +++ b/inc/helpers/posts-renderer/config.php @@ -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(); diff --git a/inc/helpers/posts-renderer/post-data.php b/inc/helpers/posts-renderer/post-data.php index 72750c9f..c23708a0 100644 --- a/inc/helpers/posts-renderer/post-data.php +++ b/inc/helpers/posts-renderer/post-data.php @@ -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[] = '' . esc_html($category->name) . ''; } } + 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[] = '' . esc_html($topic->name) . ''; + } + } 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); diff --git a/inc/helpers/template-tags/search-meta.php b/inc/helpers/template-tags/search-meta.php index 7e246c2b..f2fc487f 100644 --- a/inc/helpers/template-tags/search-meta.php +++ b/inc/helpers/template-tags/search-meta.php @@ -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) { diff --git a/inc/helpers/template-tags/site-layout.php b/inc/helpers/template-tags/site-layout.php index 3c6ddd13..5fa1c1d5 100644 --- a/inc/helpers/template-tags/site-layout.php +++ b/inc/helpers/template-tags/site-layout.php @@ -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()) { diff --git a/src/Admin/Native/PostListEnhance.php b/src/Admin/Native/PostListEnhance.php index 765b80d5..711211b2 100644 --- a/src/Admin/Native/PostListEnhance.php +++ b/src/Admin/Native/PostListEnhance.php @@ -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; } diff --git a/src/Admin/PageDisplayController.php b/src/Admin/PageDisplayController.php index 78b2ed38..9822ff22 100644 --- a/src/Admin/PageDisplayController.php +++ b/src/Admin/PageDisplayController.php @@ -21,7 +21,7 @@ class PageDisplayController extends ControllerBase { } public function register_routes(): void { - register_rest_route(self::API_NAMESPACE, '/settings/schema/page_display/search/(?Pposts|categories|tags)', [ + register_rest_route(self::API_NAMESPACE, '/settings/schema/page_display/search/(?Pposts|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]); } diff --git a/src/Admin/Schema/GlobalSchema.php b/src/Admin/Schema/GlobalSchema.php index 4292f4c1..7786be9f 100644 --- a/src/Admin/Schema/GlobalSchema.php +++ b/src/Admin/Schema/GlobalSchema.php @@ -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' => [ diff --git a/src/Admin/Schema/PostList/SchemaPostListList.php b/src/Admin/Schema/PostList/SchemaPostListList.php index fb8e0c51..9273edbe 100644 --- a/src/Admin/Schema/PostList/SchemaPostListList.php +++ b/src/Admin/Schema/PostList/SchemaPostListList.php @@ -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 配置', diff --git a/src/Admin/Schema/SchemaPageDisplay.php b/src/Admin/Schema/SchemaPageDisplay.php index 734147dd..cdab52c7 100644 --- a/src/Admin/Schema/SchemaPageDisplay.php +++ b/src/Admin/Schema/SchemaPageDisplay.php @@ -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')]), ]); } diff --git a/src/Admin/Schema/SchemaPostList.php b/src/Admin/Schema/SchemaPostList.php index 5cb8c30f..cf5e8ca9 100644 --- a/src/Admin/Schema/SchemaPostList.php +++ b/src/Admin/Schema/SchemaPostList.php @@ -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' => '当选择投稿权限为无需登录就能投稿时,投稿文章的用户'], diff --git a/src/Core/CreatorRest.php b/src/Core/CreatorRest.php index 677578fe..0afb7273 100644 --- a/src/Core/CreatorRest.php +++ b/src/Core/CreatorRest.php @@ -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, diff --git a/src/Core/DomainModel.php b/src/Core/DomainModel.php index 35e6876b..1fa017bc 100644 --- a/src/Core/DomainModel.php +++ b/src/Core/DomainModel.php @@ -148,7 +148,6 @@ class DomainModel { 'description' => '', 'show_sidebar' => '', 'show_header' => true, - 'show_featured' => false, ]; } diff --git a/src/Core/FrontSubmitSave.php b/src/Core/FrontSubmitSave.php index a356c319..522431d1 100644 --- a/src/Core/FrontSubmitSave.php +++ b/src/Core/FrontSubmitSave.php @@ -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, []); } diff --git a/src/Core/Setup.php b/src/Core/Setup.php index d4247599..d7b0e7b2 100644 --- a/src/Core/Setup.php +++ b/src/Core/Setup.php @@ -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) * diff --git a/src/Core/SubmissionVersion.php b/src/Core/SubmissionVersion.php index d478b3f7..9a1f41d0 100644 --- a/src/Core/SubmissionVersion.php +++ b/src/Core/SubmissionVersion.php @@ -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))); diff --git a/src/Core/SubmitTopics.php b/src/Core/SubmitTopics.php new file mode 100644 index 00000000..8561d6f9 --- /dev/null +++ b/src/Core/SubmitTopics.php @@ -0,0 +1,88 @@ + */ + 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|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; + } +} diff --git a/src/Core/Templates.php b/src/Core/Templates.php index b83082a1..2d0b294e 100644 --- a/src/Core/Templates.php +++ b/src/Core/Templates.php @@ -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)) { diff --git a/src/Core/TermMeta.php b/src/Core/TermMeta.php index 6d2257ce..f22e3493 100644 --- a/src/Core/TermMeta.php +++ b/src/Core/TermMeta.php @@ -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 === '' + ? '—' + : ''; + } + + 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 */ 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'); ?>
-

+ + + +

term_id); + wp_nonce_field('jio_term_meta', 'jio_term_meta_nonce'); ?> -

+ taxonomy === 'topics') : ?> + + +

-

+

@@ -284,10 +365,6 @@ class TermMeta {
- @@ -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); diff --git a/src/Settings/DisplayPageGroups.php b/src/Settings/DisplayPageGroups.php index 08afc4d5..bb56c13d 100644 --- a/src/Settings/DisplayPageGroups.php +++ b/src/Settings/DisplayPageGroups.php @@ -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(), ]; diff --git a/src/Settings/FacadeBulk.php b/src/Settings/FacadeBulk.php index c82d95f6..883ec939 100644 --- a/src/Settings/FacadeBulk.php +++ b/src/Settings/FacadeBulk.php @@ -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} */ diff --git a/src/Settings/PostDisplay.php b/src/Settings/PostDisplay.php index 9c3af265..94f9351d 100644 --- a/src/Settings/PostDisplay.php +++ b/src/Settings/PostDisplay.php @@ -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) diff --git a/src/Settings/ThemeSearch.php b/src/Settings/ThemeSearch.php index d79c4d91..dc6fc089 100644 --- a/src/Settings/ThemeSearch.php +++ b/src/Settings/ThemeSearch.php @@ -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)"; diff --git a/src/WidgetSystem/FormRenderer.php b/src/WidgetSystem/FormRenderer.php index 633e115a..3a6a4999 100644 --- a/src/WidgetSystem/FormRenderer.php +++ b/src/WidgetSystem/FormRenderer.php @@ -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 '
'; + echo '
'; echo ''; echo '
'; foreach ($selected as $term_id) { @@ -226,7 +229,7 @@ final class FormRenderer { echo '' . esc_html((string) $by_id[$term_id]->name) . ''; } echo '
'; - echo ''; + echo ''; echo '
'; foreach ($terms as $term) { $term_id = (int) $term->term_id; diff --git a/src/WidgetSystem/PostQuery.php b/src/WidgetSystem/PostQuery.php index f3bd6f42..16086b1f 100644 --- a/src/WidgetSystem/PostQuery.php +++ b/src/WidgetSystem/PostQuery.php @@ -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) { diff --git a/src/WidgetSystem/Renderers/ContentRenderer.php b/src/WidgetSystem/Renderers/ContentRenderer.php index e9f23387..e210dd0e 100644 --- a/src/WidgetSystem/Renderers/ContentRenderer.php +++ b/src/WidgetSystem/Renderers/ContentRenderer.php @@ -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', diff --git a/src/WidgetSystem/WidgetSchemas.php b/src/WidgetSystem/WidgetSchemas.php index a4204df0..9a72a96b 100644 --- a/src/WidgetSystem/WidgetSchemas.php +++ b/src/WidgetSystem/WidgetSchemas.php @@ -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' => '
请确保所选的分类内的文章数量均超过此数量,否则会出现布局错位的问题!
'], diff --git a/taxonomy-topics.php b/taxonomy-topics.php new file mode 100644 index 00000000..223dd7bb --- /dev/null +++ b/taxonomy-topics.php @@ -0,0 +1,91 @@ +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(); diff --git a/templates/parts/submit/context.php b/templates/parts/submit/context.php index 4ff657c6..807ee9bf 100644 --- a/templates/parts/submit/context.php +++ b/templates/parts/submit/context.php @@ -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, diff --git a/templates/parts/submit/sidebar-terms.php b/templates/parts/submit/sidebar-terms.php index 3f833783..d9bfadac 100644 --- a/templates/parts/submit/sidebar-terms.php +++ b/templates/parts/submit/sidebar-terms.php @@ -1,6 +1,6 @@
+ + + 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 '
'; - } - return; - } - $render($parent, $depth); } endif; diff --git a/tests/admin/filter-settings-reference-contract.test.cjs b/tests/admin/filter-settings-reference-contract.test.cjs index 91aa54fc..0ccc2447 100644 --- a/tests/admin/filter-settings-reference-contract.test.cjs +++ b/tests/admin/filter-settings-reference-contract.test.cjs @@ -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\)/); diff --git a/tests/admin/submit-settings-reference-contract.test.cjs b/tests/admin/submit-settings-reference-contract.test.cjs index a3881a73..32eeb47b 100644 --- a/tests/admin/submit-settings-reference-contract.test.cjs +++ b/tests/admin/submit-settings-reference-contract.test.cjs @@ -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/); diff --git a/tests/front-submit/topics-harness.php b/tests/front-submit/topics-harness.php new file mode 100644 index 00000000..36bfbee8 --- /dev/null +++ b/tests/front-submit/topics-harness.php @@ -0,0 +1,48 @@ + 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); diff --git a/tests/front-submit/topics.test.cjs b/tests/front-submit/topics.test.cjs new file mode 100644 index 00000000..acc1e22d --- /dev/null +++ b/tests/front-submit/topics.test.cjs @@ -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, []); +}); diff --git a/tests/widget-system/creator-studio-contract.test.cjs b/tests/widget-system/creator-studio-contract.test.cjs index 27b9c13c..88731dfd 100644 --- a/tests/widget-system/creator-studio-contract.test.cjs +++ b/tests/widget-system/creator-studio-contract.test.cjs @@ -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\(/); diff --git a/tests/widget-system/modern-widgets-contract.test.cjs b/tests/widget-system/modern-widgets-contract.test.cjs index 03ce926f..5cbc6570 100644 --- a/tests/widget-system/modern-widgets-contract.test.cjs +++ b/tests/widget-system/modern-widgets-contract.test.cjs @@ -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); diff --git a/tests/widget-system/page-display-contract.test.cjs b/tests/widget-system/page-display-contract.test.cjs index 459a23ab..9d91a888 100644 --- a/tests/widget-system/page-display-contract.test.cjs +++ b/tests/widget-system/page-display-contract.test.cjs @@ -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\/\(\?Pposts\|categories\|tags\)/) + assert.match(controller, /search\/\(\?Pposts\|categories\|tags\|topics\|terms\)/) assert.match(controller, /'permission_callback' => \[\$this, 'check_permission'\]/) }) diff --git a/tests/widget-system/post-list-quick-edit-contract.test.cjs b/tests/widget-system/post-list-quick-edit-contract.test.cjs index 177dfe1f..3ba73e83 100644 --- a/tests/widget-system/post-list-quick-edit-contract.test.cjs +++ b/tests/widget-system/post-list-quick-edit-contract.test.cjs @@ -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', () => { diff --git a/tests/widget-system/post-visual-contract.test.cjs b/tests/widget-system/post-visual-contract.test.cjs index d18bf15b..4d0b1ebe 100644 --- a/tests/widget-system/post-visual-contract.test.cjs +++ b/tests/widget-system/post-visual-contract.test.cjs @@ -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/); diff --git a/tests/widget-system/run.php b/tests/widget-system/run.php index 2824eac9..85f81db1 100644 --- a/tests/widget-system/run.php +++ b/tests/widget-system/run.php @@ -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('栏目', $tab['tabs'][0]['title'], 'tab title keeps controlled HTML'); $this->false(str_contains($tab['tabs'][0]['title'], '