diff --git a/404.php b/404.php
new file mode 100644
index 00000000..d7e03501
--- /dev/null
+++ b/404.php
@@ -0,0 +1,107 @@
+ __('页面未找到', 'aksrt'),
+ 'page_404_search' => true,
+ 'page_404_popular' => true,
+ ];
+$search_placeholder = function_exists('aksrt_get_search_placeholder') ? aksrt_get_search_placeholder() : __('开启精彩搜索', 'aksrt');
+$hot_keywords = function_exists('aksrt_get_search_hot_keywords') ? aksrt_get_search_hot_keywords() : [];
+
+$hot_query = new WP_Query([
+ 'post_type' => 'post',
+ 'posts_per_page' => 5,
+ 'post_status' => 'publish',
+ 'orderby' => 'comment_count',
+ 'order' => 'DESC',
+ 'no_found_rows' => true,
+ 'ignore_sticky_posts' => true,
+]);
+
+$top_cats = get_terms([
+ 'taxonomy' => 'category',
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ 'number' => 6,
+ 'hide_empty' => true,
+]);
+?>
+
+
+
+
+
+
+
+
+ have_posts()) : ?>
+
+
+
+ have_posts()) : $hot_query->the_post(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ __('最新', 'aksrt'),
+ 'downloads' => __('热门下载', 'aksrt'),
+ 'views' => __('阅读最多', 'aksrt'),
+];
+
+$price_options = [
+ 'all' => __('全部', 'aksrt'),
+ 'free' => __('免费', 'aksrt'),
+ 'paid' => __('付费', 'aksrt'),
+];
+
+// 给筛选链接保留其它已有 query
+$preserve = [];
+if ($current_orderby !== 'date') $preserve['orderby'] = $current_orderby;
+if ($current_price !== 'all') $preserve['price'] = $current_price;
+if ($price_min !== '' && $price_min > 0) $preserve['price_min'] = $price_min;
+if ($price_max !== '') $preserve['price_max'] = $price_max;
+
+$has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_min !== '' || $price_max !== '';
+?>
+
+
+
+
+
+
+
+
+
+ 'card', 'card_style' => 'standard'];
+$render_config = [
+ 'context' => 'archive',
+ 'mode' => (string) ($list_settings['render_mode'] ?? 'card'),
+ 'card_style' => (string) ($list_settings['card_style'] ?? 'standard'),
+];
+
+// Per-category card mode override: if visiting a category in card_cat_ids, force card mode
+if (is_category() && !empty($list_settings['card_cat_ids'])) {
+ $current_cat_id = (int) get_queried_object_id();
+ if (in_array($current_cat_id, $list_settings['card_cat_ids'], true)) {
+ $render_config['mode'] = 'card';
+ }
+}
+
+// Per-category multi-thumb override
+if (is_category() && class_exists('AKSRT_Settings')) {
+ $thumb_settings = AKSRT_Settings::get_post_thumb_settings();
+ if (!empty($thumb_settings['mult_thumb_cat_ids'])) {
+ $current_cat_id = (int) get_queried_object_id();
+ if (in_array($current_cat_id, $thumb_settings['mult_thumb_cat_ids'], true)) {
+ $render_config['multi_thumb_enabled'] = true;
+ $render_config['multi_thumb_limit'] = (int) ($thumb_settings['multi_count'] ?? 4);
+ }
+ }
+}
+
+$has_sidebar = function_exists('aksrt_is_show_sidebar') ? aksrt_is_show_sidebar() : false;
+$archive_description = trim(strip_tags((string) get_the_archive_description()));
+$archive_summary = '';
+
+if (is_author()) {
+ $author = get_queried_object();
+ if ($author instanceof WP_User) {
+ $archive_summary = sprintf(
+ /* translators: %s: author display name */
+ __('作者:%s', 'aksrt'),
+ $author->display_name
+ );
+ }
+} elseif ($archive_description !== '') {
+ $archive_summary = $archive_description;
+} elseif (isset($wp_query->found_posts)) {
+ $archive_summary = sprintf(
+ esc_html(_n('共 %d 篇内容', '共 %d 篇内容', (int) $wp_query->found_posts, 'aksrt')),
+ (int) $wp_query->found_posts
+ );
+}
+?>
+
+
+
+
+
+ {
+ console.error('[AKSRT] 样式提取失败:', err)
+ process.exit(1)
+})
diff --git a/assets/admin/index.html b/assets/admin/index.html
new file mode 100644
index 00000000..7418cf13
--- /dev/null
+++ b/assets/admin/index.html
@@ -0,0 +1,12 @@
+
+
+
+ 可能被外部修改或其兄弟节点被移除,
+ * Vue patch 时遍历到 null 节点 → nextSibling 报错。
+ *
+ * 解决方案:在挂载前一刻,动态创建一个全新的容器元素替换旧的,
+ * 确保该节点及其父级在 Vue 接管前不被任何外部脚本污染。
+ */
+function getCleanContainer() {
+ const existing = document.getElementById('aksrt-admin-app')
+ if (!existing) {
+ // 极端情况:PHP 没有输出容器,手动创建
+ const fresh = document.createElement('div')
+ fresh.id = 'aksrt-admin-app'
+ document.body.appendChild(fresh)
+ return fresh
+ }
+
+ // 创建全新容器替换旧容器
+ // 这确保新容器没有 WP 脚本留下的任何副作用(事件监听、属性等)
+ const parent = existing.parentNode
+ if (!parent) {
+ // 旧容器已脱离文档树,重新创建
+ const fresh = document.createElement('div')
+ fresh.id = 'aksrt-admin-app'
+ document.body.appendChild(fresh)
+ return fresh
+ }
+
+ const fresh = document.createElement('div')
+ fresh.id = 'aksrt-admin-app'
+
+ // 将旧容器的位置信息转移给新容器(保留 fallback 内容)
+ fresh.innerHTML = existing.innerHTML || ''
+ parent.replaceChild(fresh, existing)
+
+ return fresh
+}
+
+/**
+ * 显示错误信息
+ */
+function showError(container, err) {
+ container.innerHTML = '
'
+ + '
页面加载失败
'
+ + '
' + (err.message || String(err)) + '
'
+ + '
重新加载 '
+ + '
'
+}
+
+// 等待 router 就绪后延迟挂载
+router.isReady().then(() => {
+ // 三重 rAF:确保 load-scripts.php 加载的所有 jQuery 插件完成初始化和 DOM 操作
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ try {
+ const container = getCleanContainer()
+ // 关键:在挂载前设置样式注入目标,确保 NaiveUI 组件渲染时目标已就绪
+ styleMountTarget.value = container
+ app.mount(container)
+ } catch (err) {
+ console.error('[AKSRT] Vue 挂载失败:', err)
+ showError(getCleanContainer(), err)
+ }
+ })
+ })
+ })
+}).catch((err) => {
+ console.error('[AKSRT] Router 初始化失败:', err)
+})
diff --git a/assets/admin/src/pages/CmsCredits.vue b/assets/admin/src/pages/CmsCredits.vue
new file mode 100644
index 00000000..4fdcfd9a
--- /dev/null
+++ b/assets/admin/src/pages/CmsCredits.vue
@@ -0,0 +1,165 @@
+
+
+
积分配置
+
+ 配置积分规则与发放策略。可自定义货币名称(如”金币”、”魔石”等)。用户余额与手工调整已迁移到 WordPress 原生”用户”菜单中管理。
+
+
+
+
+
+
+
+
+
+
+
+ 自定义货币显示名称,如"金币"、"魔石"等。留空则使用默认名称"积分"。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 连续 N 天时额外发放的积分。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 资源未单独设价时回退使用。设为 0 表示免费
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/ResourceDownload.vue b/assets/admin/src/pages/ResourceDownload.vue
new file mode 100644
index 00000000..1474ee15
--- /dev/null
+++ b/assets/admin/src/pages/ResourceDownload.vue
@@ -0,0 +1,238 @@
+
+
+
+
+
+ 下载权限
+ 收款接口
+
+
+
+
+
+
+
+
+
+ 登录门槛:{{ form.download_login ? '已启用' : '已关闭' }}
+
+
+ 积分扣费:{{ form.download_credit ? '已启用' : '已关闭' }}
+
+
+ 下载验证码:{{ form.download_captcha ? '已启用' : '已关闭' }}
+
+
+ 默认价格:{{ form.download_default_cost }} {{ currencyName }}
+
+
+ 角色免扣:{{ form.payment_vip_free_download ? '已启用' : '未启用' }}
+
+
+ Token TTL:10 分钟
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 资源未单独设价时回退到这里。角色免扣命中时会直接跳过积分扣费。
+
+
+
+
+
+
+
+
+
+
+
+ 逗号分隔,标记纳入统一付费策略的内容类型。
+
+
+
+
+
+
+
+
+
+
+ 命中这些 WordPress role 的用户下载资源时将直接跳过积分扣费。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 当前收款模式:{{ gatewayModeLabel }}。充值、VIP、返佣和分成页均已提供稳定结构,订单和结算台账直接衔接这些字段。
+
+
+
+
+ 保存配置
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/SchemaPage.vue b/assets/admin/src/pages/SchemaPage.vue
new file mode 100644
index 00000000..8ded7c11
--- /dev/null
+++ b/assets/admin/src/pages/SchemaPage.vue
@@ -0,0 +1,508 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 使用 ISO 国家代码,如 CN / US / RU。
+
+
+
+
+
+
+
+
+
+
+ 测试验证
+
+
+
+
+
+
+
+
+
+
{{ testResult.message }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/SiteSettings.vue b/assets/admin/src/pages/SiteSettings.vue
new file mode 100644
index 00000000..f9f170b0
--- /dev/null
+++ b/assets/admin/src/pages/SiteSettings.vue
@@ -0,0 +1,211 @@
+
+
+
+
+ 保存全部
+
+
+
+
+
+
+
+
+
+
+ 已开启
+ 已关闭
+
+
+ 关闭后全站禁用评论功能。
+
+
+
+
+
+ 是
+ 否
+
+
+
+
+
+
+
+
+
+
+
+
+ 数字翻页
+ 加载更多
+ 无限滚动
+
+
+
+
+
+
+
+ 数字翻页
+ 加载更多
+ 无限滚动
+
+
+
+
+
+
+
+
+
+
+
+
+ 已开启
+ 已关闭
+
+
+ 开启后图片在进入视口时才加载,可显著提升页面加载速度。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 国内站点建议优先选 Cravatar。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存全部
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/content/BannerManager.vue b/assets/admin/src/pages/content/BannerManager.vue
new file mode 100644
index 00000000..b11820cc
--- /dev/null
+++ b/assets/admin/src/pages/content/BannerManager.vue
@@ -0,0 +1,392 @@
+
+
+
+
+
+ Banner 显示开关
+
+
+
+
+ 关闭后,首页 Banner 区块将不再渲染。
+
+
+
+
+ 保存 Banner 开关
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 媒体库
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/content/FloatingButtonsSettings.vue b/assets/admin/src/pages/content/FloatingButtonsSettings.vue
new file mode 100644
index 00000000..bae1c629
--- /dev/null
+++ b/assets/admin/src/pages/content/FloatingButtonsSettings.vue
@@ -0,0 +1,1003 @@
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+ 控制前台整个悬浮按钮组是否显示
+
+
+
+
+ 圆角卡片
+ 方形堆叠
+
+
+
+
+
+
+ 右下角
+ 右侧居中
+
+
+
+
+
+ 页面滚动时暂时隐藏,停止滚动后恢复
+
+
+
+ 向下滚动隐藏,向上滚动显示
+
+
+
+
+ PC
+ 移动端
+
+
+
+
+
+
+
+
+
+
+
+ 支持内置功能按钮、自定义链接、二维码弹层和客服入口。排序结果会直接用于前端输出顺序。
+
+
+
+
+
+
+
+
+ 编辑
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+ 保存悬浮按钮配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 亮色固定显示太阳
+ 暗色固定显示月亮
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 点击跳转
+ 悬浮二维码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
发布菜单项
+
+
+
+
+ 一键预设
+
+
+
+
+ 添加子项
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/content/FooterSettings.vue b/assets/admin/src/pages/content/FooterSettings.vue
new file mode 100644
index 00000000..da5caa0e
--- /dev/null
+++ b/assets/admin/src/pages/content/FooterSettings.vue
@@ -0,0 +1,988 @@
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择
+ 清除
+
+
+
+
+
+
+ 选择
+ 清除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 页脚中的社交平台或联系入口。
+
+
+
+
+ 标准联系字段
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择
+ 清除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 更多联系项
+
+
+
+
+ 每项含描述、图标、链接、图片(hover 显示),用于补充标准联系方式。
+
+
+
+
+
+
+
+
+
+
+
+
+ 页脚第二列的常用导航链接。
+
+
+
+
+
+
+
+
+
+
+
+
+ 页脚补充链接区域。
+
+
+
+
+
+
+
+
+
+
+
+
+ 站点友情链接列表,支持 Logo 头像、名称、URL 与简短描述。
+
+
+
+
+
+
+
+
+
+
+
+
+ 显示在页脚第三列区域,每项含文字描述和二维码图片。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+ 插入在页脚底部区域(版权信息之后),支持 HTML。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 默认
+ 紧凑
+ 居中
+
+
+
+
+
+ 范围 1 - 20
+
+
+
+
+
+
+
+
+
+
+
+ 保存页脚配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/content/FooterTabbarSettings.vue b/assets/admin/src/pages/content/FooterTabbarSettings.vue
new file mode 100644
index 00000000..041ca205
--- /dev/null
+++ b/assets/admin/src/pages/content/FooterTabbarSettings.vue
@@ -0,0 +1,790 @@
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 移动端底部导航栏总开关
+
+
+
+ 页面向下滚动时隐藏,向上滚动时显示
+
+
+
+ 文章详情页底部显示的Tab按钮类型
+
+
+
+
+
+
+
+
+
+ 配置移动端底部导航栏显示的按钮,支持拖拽排序。建议按钮数量不超过5个。
+ 当前已启用 {{ enabledPreviewItems.length }} 个按钮,最多允许 5 个。
+
+
+
+
+
+
+
+
+ 编辑
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+ 保存底部Tab配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 静态文本
+ 动态徽标
+
+
+
+
+
+ 未登录时显示提醒徽标,登录后自动隐藏。
+ 登录用户显示评论数徽标,游客不显示。
+ 搜索结果页显示 ON 徽标,其它页面隐藏。
+ 当前类型不提供动态徽标,前端将回退为静态徽标。
+
+
+
+
+
+
+
+
+
+ 文章
+ 分类
+ 标签
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 发布菜单项
+
+
+ 添加子项
+
+
+
+ 快速添加:
+
+ {{ preset.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
当前没有子项,前端将回退为默认发布入口。
+
+
+
+
+ 页面滚动后切换为返回顶部按钮
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/content/HeroManager.vue b/assets/admin/src/pages/content/HeroManager.vue
new file mode 100644
index 00000000..cb3951bb
--- /dev/null
+++ b/assets/admin/src/pages/content/HeroManager.vue
@@ -0,0 +1,548 @@
+
+
+
+
+
+ Hero 显示开关
+
+
+
+
+ 关闭后,首页 Hero 区块将不再渲染。
+
+
+
+
+ 保存 Hero 开关
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 媒体库
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 当前窗口
+ 新窗口
+
+
+
+
+
+ 左
+ 居中
+ 右
+
+
+
+
+
+ 全部
+ 仅电脑
+ 仅移动
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 图片图层(视差叠加)
+
+
+
+
+
+
+ 媒体库
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 添加图层
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/content/HomepageSettings.vue b/assets/admin/src/pages/content/HomepageSettings.vue
new file mode 100644
index 00000000..7139feff
--- /dev/null
+++ b/assets/admin/src/pages/content/HomepageSettings.vue
@@ -0,0 +1,617 @@
+
+
+
+
+ 保存设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 内容模型
+
+
+
+
+
+ 混合模式
+ 文章 + 资源(默认)
+
+
+ 纯文章
+ 仅标准文章
+
+
+
+
+
+
+
+
+
+ 主列表(最新发布)
+
+
+
+ 关闭后首页将不显示文章列表(仅显示模块)。
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 内联链接
+ 下拉菜单
+
+
+
+
+
+
+
+
+
+
+
+ 排除规则
+
+
+
+ 主列表和 Tab 查询均排除这些文章。
+
+
+
+ 主列表排除这些分类。Tab 不能绑定已排除的分类。
+
+
+
+
+
+
+
+
+ 首页 Tab
+ + 添加 Tab
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无 Tab 配置,点击"添加 Tab"添加。
+
+
+
+
+ updateTab(index, 'term_id', val)"
+ />
+ updateTab(index, 'title', val)"
+ />
+ updateTab(index, 'orderby', val)"
+ />
+ updateTab(index, 'enabled', val)"
+ />
+ 删除
+
+
+
+
+
+
+
+
+
+
+ 卡片样式
+
+
+
+
+ 默认
+ 风格 2
+ 风格 3
+
+
+
+
+
+
+
+
+
+
+
+
+ 列表
+ 卡片
+
+
+
+
+
+
+
+ 侧边栏
+
+
+
+
+
+
+
+
+ 交互设置
+
+
+
+ 关闭后排序链接将使用整页跳转。
+
+
+
+
+
+
+
+
+
+ 模块状态
+
+
+ 在 Hero 页面管理
+ 首页 Hero 开关已从首页设置中移出。
+
+
+ 在 Banner 页面管理
+ 首页 Banner 开关已从首页设置中移出。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 导入
+
+
+
+
+
+ 保存设置
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/content/NotificationCenterSettings.vue b/assets/admin/src/pages/content/NotificationCenterSettings.vue
new file mode 100644
index 00000000..e108dfcf
--- /dev/null
+++ b/assets/admin/src/pages/content/NotificationCenterSettings.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+ 立即清理
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 未读持久化通知之外,叠加邮箱待验证、待审核评论等即时提醒
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/docs/DocsPage.vue b/assets/admin/src/pages/docs/DocsPage.vue
new file mode 100644
index 00000000..b82000a8
--- /dev/null
+++ b/assets/admin/src/pages/docs/DocsPage.vue
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+
+
+
+
+
{{ docsModeEnabled ? '文档模式已启用' : '文档模式未启用' }}
+
{{ docsModeEnabled ? '文档导航页面和文档站式侧边栏已激活' : '启用后将激活文档导航页面模板和文档站式侧边栏' }}
+
+
+
+
+
+ {{ docsModeEnabled ? '管理文档模式' : '启用文档模式' }}
+
+
+
+
+
+
+
+
+ 尚未创建文档页面。在 WordPress 后台新建页面时,选择"文档导航页"或"专题聚合页"模板。
+
+
+
+
+
+
+
{{ stat.value }}
+
{{ stat.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
全局设置
+
Logo、背景、菜单装饰、自定义代码
+
前往配置 →
+
+
+
+
+
+
+
+
首页配置
+
首页模块、布局、排序和预设模板
+
前往配置 →
+
+
+
+
+
+
+
+
用户管理
+
登录注册、OAuth、签到、积分
+
前往配置 →
+
+
+
+
+
+
+
+
系统工具
+
导入导出、健康检查、迁移状态
+
前往配置 →
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ doc.title }}
+
{{ doc.description }}
+
+
+
+
+
+
+
+
+
+
+ {{ themeVersion }}
+
+
+ {{ dbSchemaVersion }}
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/extensions/ExtensionAI.vue b/assets/admin/src/pages/extensions/ExtensionAI.vue
new file mode 100644
index 00000000..c248ba34
--- /dev/null
+++ b/assets/admin/src/pages/extensions/ExtensionAI.vue
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+
+
+
+
+
+
AI 状态
+
{{ status.enabled ? '已启用' : '已关闭' }}
+
+
+
+
+
+
+
+
API Key
+
{{ status.configured ? '已配置' : '未配置' }}
+
+
+
+
+
+
+
+
模型
+
{{ status.model || '未设置' }}
+
+
+
+
+
+
+
+
可用状态
+
{{ status.available ? '可用' : '不可用' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/extensions/ExtensionDocsMode.vue b/assets/admin/src/pages/extensions/ExtensionDocsMode.vue
new file mode 100644
index 00000000..781d216c
--- /dev/null
+++ b/assets/admin/src/pages/extensions/ExtensionDocsMode.vue
@@ -0,0 +1,311 @@
+
+
+
+
+
+
+
+
+
+ 开启后将激活文档导航页面模板和文档站式侧边栏
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 开启后,所选文档分类的内容不会出现在首页常规文章流中
+
+
+
+
+
+
+ 在文档页面侧边栏显示章节目录树
+
+
+
+
+
+
+ 在文档底部显示上一章/下一章导航
+
+
+
+
+
+ 按菜单顺序
+ 按发布时间
+ 按标题排序
+
+
+
+
+
+
+ 重置
+ 保存设置
+
+
+
+
+
+
+
+
+
{{ docStats.total || 0 }}
+
文档页面总数
+
+
+
+
+
{{ docStats.nav_count || 0 }}
+
文档导航页
+
+
+
+
+
{{ docStats.special_count || 0 }}
+
专题聚合页
+
+
+
+
+
+
+
+ 以下页面模板可用于创建文档类页面。在 WordPress 后台新建页面时,从"页面属性 → 模板"中选择对应模板。
+
+
+
+
+
+
+
+
+ {{ tpl.file }}
+
+
+
+
+
+
+
+
+
+
+ 专题页面配置
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/extensions/ExtensionSecurity.vue b/assets/admin/src/pages/extensions/ExtensionSecurity.vue
new file mode 100644
index 00000000..657d2c2e
--- /dev/null
+++ b/assets/admin/src/pages/extensions/ExtensionSecurity.vue
@@ -0,0 +1,293 @@
+
+
+
+
+
+
+
+
+
{{ item.value }}
+
{{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ feat.enabled ? '已启用' : '未启用' }}
+
+
+ 前往配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 测试 IP 接口
+
+ {{ ipTest.result }}
+
+
+
+
+
+
+
+
+ 测试内容审核
+
+ {{ auditTest.result }}
+
+
+
+
+
+
+
+ 以下开关会影响站点安全策略,请谨慎操作。
+
+
+
+
+
+ 开启后未审核评论也会在前台显示(标记为待审核)
+
+
+
+
+
+ 开启后用户投稿直接发布,不经过审核流程
+
+
+
+
+
+ 开启后未登录用户也可以发表评论
+
+
+
+
+
+ 保存风险设置
+
+
+
+
+
+
+
+
+
+
+ {{ log.time }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/extensions/ExtensionTools.vue b/assets/admin/src/pages/extensions/ExtensionTools.vue
new file mode 100644
index 00000000..427f37e0
--- /dev/null
+++ b/assets/admin/src/pages/extensions/ExtensionTools.vue
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ svc.available ? '可用' : '未配置' }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 清除缓存
+
+
+
+ 优化数据表
+
+
+
+ 重建索引
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/extensions/ExtensionsSectionPage.vue b/assets/admin/src/pages/extensions/ExtensionsSectionPage.vue
new file mode 100644
index 00000000..e31e033c
--- /dev/null
+++ b/assets/admin/src/pages/extensions/ExtensionsSectionPage.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ {{ link.label }}
+
+
+
+
+
+
+
+ {{ note }}
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/forum/ForumBoards.vue b/assets/admin/src/pages/forum/ForumBoards.vue
new file mode 100644
index 00000000..7852b522
--- /dev/null
+++ b/assets/admin/src/pages/forum/ForumBoards.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+ 共 {{ boards.length }} 个版块
+ 新增版块
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/forum/ForumDashboard.vue b/assets/admin/src/pages/forum/ForumDashboard.vue
new file mode 100644
index 00000000..a553b716
--- /dev/null
+++ b/assets/admin/src/pages/forum/ForumDashboard.vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
{{ stats[card.key] || 0 }}
+
{{ card.label }}
+
+
+
+
+
+
+
+
+ 待审核主题
+ 进入主题治理
+
+
+
+
+
+
+
+
+
+
+ 待处理版主申请
+ 进入申请处理
+
+
+
+
+
+
+
+
+
+
+
+ 版块概况
+ 进入版块管理
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/forum/ForumModeratorApplications.vue b/assets/admin/src/pages/forum/ForumModeratorApplications.vue
new file mode 100644
index 00000000..4fd9adce
--- /dev/null
+++ b/assets/admin/src/pages/forum/ForumModeratorApplications.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+ 查询
+ 重置
+
+
+
+
+
+
+
+
+
+
+ {{ activeApplication?.display_name || '-' }} / {{ activeApplication?.board_name || '-' }}
+ {{ activeApplication?.reason || '暂无申请理由' }}
+
+
+ 通过
+ 拒绝
+
+
+
+
+
+
+ 取消
+ 提交处理
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/forum/ForumModerators.vue b/assets/admin/src/pages/forum/ForumModerators.vue
new file mode 100644
index 00000000..174b68e0
--- /dev/null
+++ b/assets/admin/src/pages/forum/ForumModerators.vue
@@ -0,0 +1,363 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 待处理
+
+
+
+
+
+
+
+
+
+
+ 已加载 {{ boards.length }} 个版块
+
+ 查看版主申请
+ 批量管理
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ moderator.display_name }}
+ {{ getRoleLabel(moderator.role) }}
+
+
{{ moderator.user_email }}
+
+ {{ getPermissionLabel(perm) }}
+
+
+
+
+ 移除
+
+
+
+
+
+
+
+
+
+ 添加版主
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 批量添加
+ 批量移除
+ 批量修改角色
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 执行
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/forum/ForumTopics.vue b/assets/admin/src/pages/forum/ForumTopics.vue
new file mode 100644
index 00000000..8c1f9522
--- /dev/null
+++ b/assets/admin/src/pages/forum/ForumTopics.vue
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/forum/components/TopicModerationDrawer.vue b/assets/admin/src/pages/forum/components/TopicModerationDrawer.vue
new file mode 100644
index 00000000..55a8cfb4
--- /dev/null
+++ b/assets/admin/src/pages/forum/components/TopicModerationDrawer.vue
@@ -0,0 +1,284 @@
+
+
+
+
+
+
+ {{ detail.topic.title || '-' }}
+ {{ primaryBoardName }}
+ {{ detail.topic.author?.display_name || '-' }}
+ {{ detail.topic.visibility?.label || '-' }}
+ {{ detail.topic.review_status?.label || '-' }}
+ {{ detail.topic.status || '-' }}
+ {{ stickySummary }}
+ {{ detail.topic.featured ? '是' : '否' }}
+ {{ bbsTypeLabel }}
+ {{ questionStatusLabel }}
+ {{ detail.topic.favorite_count ?? 0 }}
+ {{ detail.topic.score ?? 0 }}
+ {{ detail.topic.special_topic || '-' }}
+ {{ voteSummary }}
+ {{ detail.topic.moderation_note || '-' }}
+
+
+
+
内容摘要
+
{{ detail.topic.excerpt || '暂无摘要' }}
+
+
+
+
执行治理动作
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 执行
+ 刷新详情
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/PaymentAffiliate.vue b/assets/admin/src/pages/payment/PaymentAffiliate.vue
new file mode 100644
index 00000000..3dd61e53
--- /dev/null
+++ b/assets/admin/src/pages/payment/PaymentAffiliate.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/PaymentChargeCard.vue b/assets/admin/src/pages/payment/PaymentChargeCard.vue
new file mode 100644
index 00000000..965df4f2
--- /dev/null
+++ b/assets/admin/src/pages/payment/PaymentChargeCard.vue
@@ -0,0 +1,453 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 创建充值卡
+ 批量生成
+
+ 导出当前筛选
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 筛选
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 创建
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 批量生成
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/PaymentGateway.vue b/assets/admin/src/pages/payment/PaymentGateway.vue
new file mode 100644
index 00000000..dbfafefa
--- /dev/null
+++ b/assets/admin/src/pages/payment/PaymentGateway.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ gw.name }}
+
{{ gw.configured ? '已配置' : '未配置' }}
+
+
+ {{ gw.configured ? '可用' : '不可用' }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ modeLabel }}
+
当前模式
+
+
+
+
+
+ {{ overview.gateway?.test_enabled ? '启用' : '关闭' }}
+
+
测试网关
+
+
+
+
+
+ {{ overview.gateway?.manual_review ? '启用' : '关闭' }}
+
+
人工复核
+
+
+
+
+
+ {{ overview.gateway?.callback_secret ? '已设置' : '未设置' }}
+
+
回调口令
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/PaymentRebate.vue b/assets/admin/src/pages/payment/PaymentRebate.vue
new file mode 100644
index 00000000..63703886
--- /dev/null
+++ b/assets/admin/src/pages/payment/PaymentRebate.vue
@@ -0,0 +1,316 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 筛选
+ 重置
+
+
+
+
+
+
+
+ {{ column.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/PaymentRecharge.vue b/assets/admin/src/pages/payment/PaymentRecharge.vue
new file mode 100644
index 00000000..f9c3eaa0
--- /dev/null
+++ b/assets/admin/src/pages/payment/PaymentRecharge.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/PaymentRevenue.vue b/assets/admin/src/pages/payment/PaymentRevenue.vue
new file mode 100644
index 00000000..87b75c5c
--- /dev/null
+++ b/assets/admin/src/pages/payment/PaymentRevenue.vue
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
{{ revenueStats.total.toFixed(2) }}
+
总收益 (元)
+
+
+
+
+
{{ revenueStats.pending.toFixed(2) }}
+
待结算 (元)
+
+
+
+
+
{{ revenueStats.settled.toFixed(2) }}
+
已结算 (元)
+
+
+
+
+
{{ revenueStats.count }}
+
收益笔数
+
+
+
+
+
+
+
+
+ 日
+ 周
+ 月
+
+
+
+
+
+
+
+
{{ formatLabel(item.date) }}
+
+
+
+ 期间总计: ¥{{ periodTotal.toFixed(2) }}
+ 共 {{ periodCount }} 笔
+
+
+
+
+
+
+
+
+
+
{{ withdrawalStats.total }}
+
提现笔数
+
+
+
+
+
{{ withdrawalStats.pending }}
+
待审核
+
+
+
+
+
{{ withdrawalStats.completed }}
+
已完成
+
+
+
+
+
{{ withdrawalStats.total_paid.toFixed(2) }}
+
已提现 (元)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/PaymentSectionPage.vue b/assets/admin/src/pages/payment/PaymentSectionPage.vue
new file mode 100644
index 00000000..83cbbd75
--- /dev/null
+++ b/assets/admin/src/pages/payment/PaymentSectionPage.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+ {{ link.label }}
+
+
+
+
+
+
+
+ {{ note }}
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/PaymentVip.vue b/assets/admin/src/pages/payment/PaymentVip.vue
new file mode 100644
index 00000000..9f0f09a6
--- /dev/null
+++ b/assets/admin/src/pages/payment/PaymentVip.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/components/ChargeCardDetailDrawer.vue b/assets/admin/src/pages/payment/components/ChargeCardDetailDrawer.vue
new file mode 100644
index 00000000..552ade6d
--- /dev/null
+++ b/assets/admin/src/pages/payment/components/ChargeCardDetailDrawer.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+ {{ detail.card.code || '-' }}
+ {{ detail.card.amount || 0 }}
+ {{ detail.card.balance || 0 }}
+ {{ detail.card.status_label || detail.card.status }}
+ {{ detail.card.created_at || '-' }}
+ {{ detail.card.expires_at || '-' }}
+ {{ detail.card.used_at || '-' }}
+
+ {{ detail.card.used_by_user?.display_name || '-' }}
+
+
+
+
+
备注
+
+ {{ detail.card.note || '暂无备注' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/payment/components/RebateDetailDrawer.vue b/assets/admin/src/pages/payment/components/RebateDetailDrawer.vue
new file mode 100644
index 00000000..a61b2596
--- /dev/null
+++ b/assets/admin/src/pages/payment/components/RebateDetailDrawer.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+ {{ detail.record.order_number || '-' }}
+ {{ detail.record.user?.display_name || '-' }}
+ {{ detail.record.source_user?.display_name || '-' }}
+ {{ detail.record.amount || 0 }}
+ {{ detail.record.type_label || detail.record.type }}
+ {{ detail.record.status_label || detail.record.status }}
+ {{ detail.record.created_at || '-' }}
+ {{ detail.record.settled_at || '-' }}
+
+
+
+
备注
+
+ {{ detail.record.note || '暂无备注' }}
+
+
+
+
+
处理记录
+
+
+
+ {{ entry.event || '-' }}
+ {{ entry.time || '-' }}
+
+
{{ entry.note || '无备注' }}
+
{{ entry.operator_name || '系统' }}
+
+
+
+
+
+
+
处理返佣
+
+
+
+
+ 结算返佣
+
+
+ 取消返佣
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/permissions/AttachmentPermissions.vue b/assets/admin/src/pages/permissions/AttachmentPermissions.vue
new file mode 100644
index 00000000..5f55cce3
--- /dev/null
+++ b/assets/admin/src/pages/permissions/AttachmentPermissions.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+ 下载权限
+ 审核策略
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.label }}
+
{{ item.enabled ? '已启用' : '已关闭' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关闭后用户只能使用 Gravatar/Cravatar 自动头像。
+
+
+
+
+
+
+ 控制用户中心公开主页封面图上传入口。
+
+
+
+
+
+
+ 控制用户中心公开可用的背景图上传入口。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 前台投稿编辑器中允许插入图片。
+
+
+
+
+
+
+
+
+
+
+ 评论工具栏中允许插入图片链接或上传图片。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 后台资源编辑器中允许上传附件文件。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 命中高风险时会阻断上传,并保留上一次背景图。
+
+
+
+
+
+
+ 投稿中插入的图片也会经过内容安全审核。
+
+
+
+
+
+
+ 保存配置
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/permissions/CommentPermissions.vue b/assets/admin/src/pages/permissions/CommentPermissions.vue
new file mode 100644
index 00000000..7ca00a62
--- /dev/null
+++ b/assets/admin/src/pages/permissions/CommentPermissions.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+ 人机验证
+ 审核策略
+
+
+
+
+
+
+
+
+
+ 评论验证码:{{ status.commentCaptchaEnabled ? '已启用' : '未启用' }}
+
+
+ 审核链路:{{ status.moderationEnabled ? '已启用' : '未启用' }}
+
+
+ 评论限流:{{ status.rateLimitEnabled ? '已启用' : '未启用' }}
+
+
+ 敏感词:{{ status.keywordRuleCount }} 条
+
+
+
+
+
+
+
+
+
+
+ 开启后,游客只能浏览评论,不能直接提交评论。
+
+
+
+
+
+
+ 关闭后,前台评论列表将不再展示回复按钮。
+
+
+
+
+
+
+
+ 评论排序、分页方式、头像大小和空状态文案继续在“文章&列表 -> 评论设置”中维护;本页只负责准入与治理联动。
+
+
+
+
+ 保存配置
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/permissions/DownloadPermissions.vue b/assets/admin/src/pages/permissions/DownloadPermissions.vue
new file mode 100644
index 00000000..638cbae0
--- /dev/null
+++ b/assets/admin/src/pages/permissions/DownloadPermissions.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+ 资源下载页
+ 人机验证
+
+
+
+
+
+
+
+
+
+ 登录要求:{{ form.download_login ? '已启用' : '已关闭' }}
+
+
+ 积分扣费:{{ form.download_credit ? '已启用' : '已关闭' }}
+
+
+ 下载验证码:{{ form.download_captcha ? '已启用' : '已关闭' }}
+
+ 默认下载积分:{{ defaultCost }}
+ Token TTL:10 分钟
+
+
+
+
+
+
+
+
+
+ 关闭后,单个资源的登录要求仍可通过资源元字段单独覆盖。
+
+
+
+
+
+
+ 关闭后,资源价格将只作为展示信息,不会真正扣除积分。
+
+
+
+
+
+
+ 具体验证码服务商与场景白名单仍在“登录权限”页维护。
+
+
+
+
+
+
+
+ 默认下载积分仍由积分配置维护;本页只负责访问门槛。更完整的支付、VIP 与白名单策略将在 Phase 10 统一收口。
+
+
+
+
+ 保存配置
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/permissions/PermissionsCapabilities.vue b/assets/admin/src/pages/permissions/PermissionsCapabilities.vue
new file mode 100644
index 00000000..a7bfcda3
--- /dev/null
+++ b/assets/admin/src/pages/permissions/PermissionsCapabilities.vue
@@ -0,0 +1,245 @@
+
+
+
+
+
+ 恢复默认
+
+
+
+
+
+
+
+
+ 能力项:{{ summary.cap_count }}
+ 分组:{{ summary.group_count }}
+ 角色列:{{ summary.column_count }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存能力矩阵
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/post-list/CommentSettings.vue b/assets/admin/src/pages/post-list/CommentSettings.vue
new file mode 100644
index 00000000..815ec391
--- /dev/null
+++ b/assets/admin/src/pages/post-list/CommentSettings.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/assets/admin/src/pages/post-list/PostPageSettings.vue b/assets/admin/src/pages/post-list/PostPageSettings.vue
new file mode 100644
index 00000000..8d901ac4
--- /dev/null
+++ b/assets/admin/src/pages/post-list/PostPageSettings.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/ShopAfterSale.vue b/assets/admin/src/pages/shop/ShopAfterSale.vue
new file mode 100644
index 00000000..816da3f7
--- /dev/null
+++ b/assets/admin/src/pages/shop/ShopAfterSale.vue
@@ -0,0 +1,304 @@
+
+
+
+
+
+
+
+
+
+
+
{{ stats[item.key] || 0 }}
+
{{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+ 重置
+
+
+
+
+ 新建售后
+
+
+
+
+
+
+
+
+
+
+
+
+
新建售后工单
+
+ 订单号
+
+
+
+ 售后类型
+
+ {{ item.label }}
+
+
+
+ 金额
+
+
+
+ 原因
+
+
+
+ 补充备注
+
+
+
+ 取消
+
+ {{ creating ? '提交中...' : '提交' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/ShopCoupons.vue b/assets/admin/src/pages/shop/ShopCoupons.vue
new file mode 100644
index 00000000..9b69ca6a
--- /dev/null
+++ b/assets/admin/src/pages/shop/ShopCoupons.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增优惠券
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 表示不限制
+
+
+
+ 留空表示永不过期
+
+
+
+
+
+ 取消
+ 创建
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/ShopDashboard.vue b/assets/admin/src/pages/shop/ShopDashboard.vue
new file mode 100644
index 00000000..43a48f4b
--- /dev/null
+++ b/assets/admin/src/pages/shop/ShopDashboard.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+ {{ overview.product_count }}
+ 商品数量
+
+
+
+
+
+ {{ overview.order_count }}
+ 订单数量
+
+
+
+
+
+ {{ revenueDisplay }}
+ 累计收入 (元)
+
+
+
+
+
+ {{ overview.order_pending }}
+ 待处理订单
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/ShopOrders.vue b/assets/admin/src/pages/shop/ShopOrders.vue
new file mode 100644
index 00000000..7f14c2ba
--- /dev/null
+++ b/assets/admin/src/pages/shop/ShopOrders.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/ShopProducts.vue b/assets/admin/src/pages/shop/ShopProducts.vue
new file mode 100644
index 00000000..91c8040e
--- /dev/null
+++ b/assets/admin/src/pages/shop/ShopProducts.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增商品
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/ShopShipping.vue b/assets/admin/src/pages/shop/ShopShipping.vue
new file mode 100644
index 00000000..58971363
--- /dev/null
+++ b/assets/admin/src/pages/shop/ShopShipping.vue
@@ -0,0 +1,364 @@
+
+
+
+
+
+
+
+
+
+
+
{{ stats[item.key] || 0 }}
+
{{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+ 重置
+
+
+
+
+ 新建发货
+ 批量更新状态
+
+
+ 列控制
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
新建发货记录
+
+ 订单号
+
+
+
+ 物流公司
+
+ 请选择
+ {{ item.label }}
+
+
+
+ 物流单号
+
+
+
+ 备注
+
+
+
+ 取消
+
+ {{ creating ? '提交中...' : '提交' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/ShopWithdrawals.vue b/assets/admin/src/pages/shop/ShopWithdrawals.vue
new file mode 100644
index 00000000..d831e556
--- /dev/null
+++ b/assets/admin/src/pages/shop/ShopWithdrawals.vue
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue b/assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue
new file mode 100644
index 00000000..1d3b888a
--- /dev/null
+++ b/assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+ {{ detail.record?.type_label || detail.record?.type || '-' }}
+
+ {{ detail.record?.stage?.label || '-' }}
+
+
+ ¥{{ Number(detail.record?.amount || 0).toFixed(2) }}
+
+
+ ¥{{ Number(detail.record?.refund_amount || 0).toFixed(2) }}
+
+ {{ detail.record?.reason || '-' }}
+ {{ detail.record?.applicant?.display_name || '-' }}
+ {{ detail.record?.created_at || '-' }}
+ {{ detail.record?.operator?.display_name || '-' }}
+ {{ detail.record?.processed_at || '-' }}
+ {{ detail.record.operator_note }}
+
+
+
+
+
+ {{ detail.record.refund_channel.method_label || detail.record.refund_channel.method || '-' }}
+
+
+ {{ detail.record.refund_channel.status_label || detail.record.refund_channel.status || '-' }}
+
+
+ {{ detail.record.refund_channel.refund_id || '-' }}
+ {{ detail.record.refund_channel.completed_at || '-' }}
+
+
+
+
+
+
+ {{ detail.record.return_address.name || '-' }}
+ {{ detail.record.return_address.phone || '-' }}
+ {{ detail.record.return_address.full_address || '-' }}
+
+
+ 复制地址
+
+
+
+
+
+
+ {{ detail.record.return_shipping.company || '-' }}
+ {{ detail.record.return_shipping.tracking_number || '-' }}
+ {{ detail.record.return_shipping.shipped_at || '-' }}
+
+
+ {{ detail.record.return_shipping.status_label || detail.record.return_shipping.status || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+ 关联物流:
+
+ {{ detail.record?.order_context?.shipping?.latest?.status_label || detail.record?.order_context?.shipping?.latest?.status || '暂无物流记录' }}
+
+ / {{ detail.record.order_context.shipping.latest.carrier_label || '-' }}
+ / {{ detail.record.order_context.shipping.latest.tracking_no }}
+
+
+
+
+ 回寄参考地址:
+ {{ detail.record.order_context.address.full_text }}
+
+
+
+
+
+
+
+
+
+ 更新状态
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/components/OrderContextPanel.vue b/assets/admin/src/pages/shop/components/OrderContextPanel.vue
new file mode 100644
index 00000000..b5846b1b
--- /dev/null
+++ b/assets/admin/src/pages/shop/components/OrderContextPanel.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+ {{ context.summary?.order_number || '-' }}
+ {{ context.summary?.status_label || context.summary?.status || '-' }}
+
+ {{ context.buyer?.display_name || '-' }}
+ ({{ context.buyer.user_login }})
+
+ {{ context.buyer.user_email }}
+ {{ context.payment_method?.label || context.payment_method?.value || '-' }}
+ {{ context.shipping_method?.label || context.shipping_method?.value || '-' }}
+ {{ context.address?.full_text || '-' }}
+
+ {{ context.coupon?.label || context.coupon?.code }}
+ 减免 ¥{{ formatAmount(context.coupon.discount) }}
+
+
+ ¥{{ formatAmount(context.pricing?.amount) }}
+
+ 小计 ¥{{ formatAmount(context.pricing?.subtotal) }} / 运费 ¥{{ formatAmount(context.pricing?.shipping_fee) }} / 优惠 ¥{{ formatAmount(context.pricing?.discount) }}
+
+
+
+
+ {{ context.shipping?.latest?.status_label || context.shipping?.latest?.status || '-' }}
+
+ {{ context.shipping.latest.carrier_label || context.shipping.latest.carrier || '' }} / {{ context.shipping.latest.tracking_no }}
+
+
+ -
+
+
+
+ {{ context.after_sale?.latest?.status_label || context.after_sale?.latest?.status || '-' }}
+
+ {{ context.after_sale.latest.stage.label }}
+
+
+ -
+
+ {{ context.customer_note }}
+
+
+
+
+
{{ item.product_name || `商品 #${item.product_id || index + 1}` }}
+
数量 {{ item.quantity || 1 }} / 小计 ¥{{ formatAmount(item.line_total) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/components/OrderDetailDrawer.vue b/assets/admin/src/pages/shop/components/OrderDetailDrawer.vue
new file mode 100644
index 00000000..c3f169db
--- /dev/null
+++ b/assets/admin/src/pages/shop/components/OrderDetailDrawer.vue
@@ -0,0 +1,440 @@
+
+
+
+
+
+
+
+
+
+ {{ order.product_name || order.product_type || '-' }}
+
+ ¥{{ (order.amount || 0).toFixed(2) }}
+
+ {{ order.payment_method?.label || order.payment_method || '-' }}
+ ¥{{ (order.subtotal || 0).toFixed(2) }}
+ ¥{{ (order.shipping_fee || 0).toFixed(2) }}
+ ¥{{ (order.discount || 0).toFixed(2) }}
+ {{ order.payment_transaction_id }}
+ {{ order.created_at || '-' }}
+ {{ order.paid_at }}
+ {{ order.completed_at }}
+ {{ order.note }}
+
+
+
+
+
+ {{ order.user_id || '-' }}
+ {{ buyer.display_name || '-' }}
+ {{ buyer.email || '-' }}
+ {{ buyer.phone || '-' }}
+
+
+
+
+
+
+ {{ address.name || '-' }}
+ {{ address.phone || '-' }}
+ {{ address.full_address || '-' }}
+
+
+
+
+
+
+ {{ shipping.company || '-' }}
+ {{ shipping.tracking_number || '-' }}
+ {{ shipping.shipped_at || '-' }}
+
+
+ {{ shipping.status_label || shipping.status || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ afterSale.type_label || afterSale.type || '-' }}
+
+
+ {{ afterSale.status_label || afterSale.status || '-' }}
+
+
+ ¥{{ (afterSale.refund_amount || 0).toFixed(2) }}
+ {{ afterSale.created_at || '-' }}
+ {{ afterSale.reason || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 确认支付
+
+
+
+ 取消订单
+
+
+
+ 发货
+
+
+
+ 确认完成
+
+
+
+ 申请退款
+
+
+
+ 创建售后
+
+
+
+
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认发货
+
+
+
+
+
+
+
+
+
+ 订单总额 ¥{{ (order.amount || 0).toFixed(2) }},支持部分退款
+
+
+
+
+
+
+
+ 取消
+ 确认退款
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 提交售后
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue b/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue
new file mode 100644
index 00000000..23105d4f
--- /dev/null
+++ b/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue
@@ -0,0 +1,263 @@
+
+
+
+
+
+ 基础信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 商品素材
+
+
+
+
+
+
+
+
+
+
+ 分类与标签
+
+
+
+
+
+
+
+
+
+
+ 发货与售后
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 发布设置
+
+
+
+
+
+
+
+
+ 取消
+
+ {{ isEdit ? '保存修改' : '创建商品' }}
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue b/assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue
new file mode 100644
index 00000000..b94b8242
--- /dev/null
+++ b/assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue
@@ -0,0 +1,94 @@
+
+
+
+
已选 {{ ids.length }} 条记录
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 提交
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue b/assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue
new file mode 100644
index 00000000..1c135f56
--- /dev/null
+++ b/assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue
@@ -0,0 +1,237 @@
+
+
+
+
+
+
+
+
+
+ {{ detail.record?.carrier_label || detail.record?.carrier || '-' }}
+
+ {{ detail.record?.tracking_no || '-' }}
+
+
+
+
+ {{ detail.record?.shipped_at || '-' }}
+ {{ detail.record?.operator?.display_name || '-' }}
+
+
+ {{ detail.record?.ship_type_label || '快递发货' }}
+
+
+ {{ detail.record?.note || '-' }}
+
+
+
+
+
+ {{ detail.record.order_context.address.name || '-' }}
+ {{ detail.record.order_context.address.phone || '-' }}
+ {{ detail.record.order_context.address.full_address || detail.record.order_context.address.full_text || '-' }}
+
+
+ 复制地址
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 更新状态
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue b/assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue
new file mode 100644
index 00000000..ca23ce13
--- /dev/null
+++ b/assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 拒绝
+ 批准
+ 打款
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/theme/ThemeAuthorizationPage.vue b/assets/admin/src/pages/theme/ThemeAuthorizationPage.vue
new file mode 100644
index 00000000..79ea0c6f
--- /dev/null
+++ b/assets/admin/src/pages/theme/ThemeAuthorizationPage.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+ 兼容&工具
+ 更新中心
+
+
+
+
+
+
+ 本主题当前不实现 zibll 风格的授权码校验、域名绑定、撤销授权、本地免授权等机制。
+ `AKSRT_Update_Checker` 仅负责匿名版本检测与更新提示,不与授权系统耦合。
+
+
+
+
+
+ 不提供授权码输入框或“立即授权”按钮
+ 不提供“已授权 / 未授权 / 本地免授权”三态逻辑
+ 更新能力统一收口到“更新中心”与“主题信息”页
+ 若未来恢复授权体系,应单独立项,不与现有更新检测器混接
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/theme/ThemeInfoPage.vue b/assets/admin/src/pages/theme/ThemeInfoPage.vue
new file mode 100644
index 00000000..3ee010f9
--- /dev/null
+++ b/assets/admin/src/pages/theme/ThemeInfoPage.vue
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ info.themeVersion }}
+
+
+ {{ info.wpVersion }}
+
+
+
+ {{ info.phpVersion }}
+
+
+
+ {{ info.dbSchemaVersion }}
+
+
+ {{ info.settingsMigrationVersion || '未迁移' }}
+
+
+
+ {{ info.emailBackfillDone ? '已完成' : '未触发' }}
+
+
+
+
+
+
+
+
+
+ {{ info.siteUrl }}
+
+
+ {{ info.apiUrl }}
+
+
+ {{ info.themeDir }}
+
+
+ {{ info.uploadDir }}
+
+
+
+ {{ info.isMultisite ? '是' : '否' }}
+
+
+
+
+ {{ info.debugMode ? '开启' : '关闭' }}
+
+
+
+
+
+
+
+
+
+
+ {{ mod.label }}
+
+
+
+ 暂无模块信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 文档中心
+
+
+
+ 更新中心
+
+
+
+ 数据备份
+
+
+
+ 健康检查
+
+
+
+ 导入导出
+
+
+
+ 迁移状态
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/tools/ToolsBackup.vue b/assets/admin/src/pages/tools/ToolsBackup.vue
new file mode 100644
index 00000000..489ee362
--- /dev/null
+++ b/assets/admin/src/pages/tools/ToolsBackup.vue
@@ -0,0 +1,279 @@
+
+
+
+
+
+ 导入导出
+ 迁移状态
+
+
+
+
+
+
+
+
+
+
+ 自动备份:{{ settings.backup_auto ? '已开启' : '未开启' }}
+
+ 频率:{{ frequencyLabel }}
+ 保留:{{ settings.backup_retain }} 份
+ 现有快照:{{ backups.length }} 份
+
+
+ 自动备份
+
+ 频率
+
+ 保留
+
+ 保存策略
+
+
+
+
+
+
+
+
+
+ 立即创建快照
+ 刷新列表
+
+
+
+
+
+
+
+
+
+ 恢复非当前网站 / 非当前版本的快照可能导致配置异常,恢复前请确认站点环境一致。
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/tools/ToolsEmailRollback.vue b/assets/admin/src/pages/tools/ToolsEmailRollback.vue
new file mode 100644
index 00000000..974d00a0
--- /dev/null
+++ b/assets/admin/src/pages/tools/ToolsEmailRollback.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+ 健康检查
+ 刷新列表
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.display_name || item.login }}
+
+ {{ item.current_email }} → {{ item.previous_email }}
+
+
+ 剩余窗口:{{ Math.max(0, Math.floor((item.expires_in || 0) / 3600)) }} 小时
+
+
+
+ 执行回滚
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/tools/ToolsHealth.vue b/assets/admin/src/pages/tools/ToolsHealth.vue
new file mode 100644
index 00000000..7662bf9c
--- /dev/null
+++ b/assets/admin/src/pages/tools/ToolsHealth.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+ 导入导出
+ 邮件回滚
+
+
+
+
+
+
+
+
+
+ 功能开关:{{ emailHealth.feature_enabled ? '已启用' : '未启用' }}
+
+
+ REST 阻断:{{ emailHealth.rest_blocking_active ? '已开启' : '仅记录/关闭' }}
+
+
+ 待验证用户:{{ emailHealth.pending_users }}
+
+
+
+
+
+
+
+ {{ check.value }}
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/tools/ToolsImportExport.vue b/assets/admin/src/pages/tools/ToolsImportExport.vue
new file mode 100644
index 00000000..b9290a0f
--- /dev/null
+++ b/assets/admin/src/pages/tools/ToolsImportExport.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
+ 数据备份
+ 健康检查
+ 迁移状态
+
+
+
+
+
+
+
+ 导出当前配置
+ Dry Run 校验
+ 执行导入
+
+
+
+ 正式导入前会自动创建一份“导入前自动快照”,与“数据备份”页的快照体系共用,便于回滚。
+
+
+
+
+
+
+
+ 重置所选域
+ 重置全部
+
+ 仅支持重置 schema 托管的业务域。健康检查、迁移状态和邮件回滚属于运维面板,不参与按域回滚。
+
+
+
+
+ {{ resultText || '尚未执行操作。' }}
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/tools/ToolsMigration.vue b/assets/admin/src/pages/tools/ToolsMigration.vue
new file mode 100644
index 00000000..f8137f8c
--- /dev/null
+++ b/assets/admin/src/pages/tools/ToolsMigration.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+ 健康检查
+ 导入导出
+
+
+
+
+
+
+
+
+ DB Schema:{{ status.schema_version || '0.0.0' }}
+ Settings Migration:{{ status.migration_version || '0.0.0' }}
+ 迁移日志:{{ logItems.length }} 条
+
+
+
+
+
+ 迁移说明文档位于 `docs/migration-map.md`。如果这里的 schema/version 与文档记录不一致,发布前应先补齐说明。
+
+
+
+
+
+
+
+
{{ stringifyItem(item) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/tools/ToolsUpdate.vue b/assets/admin/src/pages/tools/ToolsUpdate.vue
new file mode 100644
index 00000000..a315b7b0
--- /dev/null
+++ b/assets/admin/src/pages/tools/ToolsUpdate.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+ 检测更新
+ 文档中心
+
+
+
+
+
+
+
+
+ 当前版本:{{ updateInfo.current_version || '0.0.0' }}
+
+ {{ updateInfo.update_available ? `发现新版本 ${updateInfo.remote_version}` : '已是最新版本' }}
+
+ 上次检测:{{ formatDate(updateInfo.last_check) }}
+
+
+
+
+
+
+ 检测到新版本 {{ updateInfo.remote_version }},当前版本为 {{ updateInfo.current_version }}。
+
+
+
+ 下载更新
+
+ 忽略此版本
+
+
+
更新日志
+
{{ updateInfo.changelog }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersBadge.vue b/assets/admin/src/pages/users/UsersBadge.vue
new file mode 100644
index 00000000..2b2363bb
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersBadge.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+ 消息通知
+ 用户等级
+
+
+
+
+
+
+
+
+
+ 徽章系统:{{ form.user_badge_enabled ? '已启用' : '已关闭' }}
+
+
+ 公开展示:{{ form.user_badge_public_display ? '开启' : '关闭' }}
+
+
+ 最多显示:{{ form.user_badge_limit }} 枚
+
+
+ 通知红点联动:{{ notificationSummary.dynamicBadge ? '启用' : '关闭' }}
+
+
+ 字典条目:{{ badgeCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 每行一个徽章,格式 `slug|显示文案|色调`,色调支持 `neutral / info / success / warning / brand`。
+
+
+
+
+
+
+
+ 前台从 `aksrt_user_badges` user meta 读取已授予徽章列表,并在公开主页和本人中心头部展示。这里只负责定义合法徽章字典和展示上限。
+
+
+
+
+ 保存配置
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersBan.vue b/assets/admin/src/pages/users/UsersBan.vue
new file mode 100644
index 00000000..89c3caba
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersBan.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ stats.active }}
+
+
+
+
+
+
+ 严重
+
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加限制
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersFeatures.vue b/assets/admin/src/pages/users/UsersFeatures.vue
new file mode 100644
index 00000000..c7c71592
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersFeatures.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+ 公开主页
+ 附件权限
+
+
+
+
+
+
+
+
+ 固定入口:个人信息 / 账号设置 / 消息通知
+
+ 背景图编辑位:{{ form.user_cover_upload ? '已展示' : '已隐藏' }}
+
+
+ 背景图上传权限:{{ backgroundUploadAllowed ? '已开放' : '未开放' }}
+
+
+ 公开主页侧栏:{{ homeSummary.sidebar ? '开启' : '关闭' }}
+
+
+
+
+
+
+
+
+
+ 个人信息
+ 账号设置
+ 消息通知
+
+
+
+
+
+
+ 我的文章
+ 我的评论
+ 我的收藏
+ 账户余额 / 签到
+
+
+
+ “个人信息”始终保留在首位;积分面板仅在积分系统启用时显示。
+
+
+
+
+
+
+
+
+
+
+
+ 开启后,用户中心会显示跳转到 WordPress 原生资料页的头像修改入口。
+
+
+
+
+
+
+ 这里只控制前台入口显示;真正的上传权限仍由“附件权限”页治理。
+
+
+
+
+
+
+ 关闭后,用户中心联系方式编辑区和公开主页联系卡片会一起隐藏。
+
+
+
+
+
+
+
+ 公开主页的封面、侧栏、关注按钮和公开 Tab 仍在“页面&显示 / 用户主页”维护;本页负责“用户中心能否编辑哪些内容”。
+
+
+
+
+ 保存配置
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersInviteCodes.vue b/assets/admin/src/pages/users/UsersInviteCodes.vue
new file mode 100644
index 00000000..e176c4ff
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersInviteCodes.vue
@@ -0,0 +1,512 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 批量生成
+ 刷新列表
+
+
+
+
+
+
+
+
+ 独立邀请码会接入注册校验链路;导出前建议补齐批次备注,便于渠道回溯。
+
+
+ 批次 {{ generatedBatchId || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 筛选
+ 重置
+ 批量禁用
+ 批量删除
+
+ 导出当前筛选
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersLevel.vue b/assets/admin/src/pages/users/UsersLevel.vue
new file mode 100644
index 00000000..fb8cfe0c
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersLevel.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+ 用户积分
+ 用户徽章
+
+
+
+
+
+
+
+
+
+ 等级系统:{{ form.user_level_enabled ? '已启用' : '已关闭' }}
+
+
+ 积分系统:{{ creditsSummary.enabled ? '已启用' : '未启用' }}
+
+
+ 当前货币:{{ creditsSummary.currencyName }}
+
+
+ 来源:{{ form.user_level_source === 'credits_balance' ? '积分余额' : '手动 Meta' }}
+
+
+ 阈值条目:{{ thresholdCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 积分余额
+ 手动 Meta
+
+
+
+
+
+
+
+ 每行一个等级,格式 `等级|所需积分`。手动 Meta 模式下仍建议保留完整字典,方便后续切换回自动计算。
+
+
+
+
+
+
+
+ 手动模式读取 `aksrt_user_level` user meta;自动模式则根据当前积分余额和上方阈值表实时计算等级,不需要单独写库。
+
+
+
+
+ 保存配置
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersMessages.vue b/assets/admin/src/pages/users/UsersMessages.vue
new file mode 100644
index 00000000..5aa866eb
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersMessages.vue
@@ -0,0 +1,308 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ stats.unread }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 刷新
+ 全部已读
+
+
+
+
+
+
+
+
+
+
+
+ {{ getTypeLabel(detailMessage.type) }}
+
+
+
+ {{ detailMessage.is_read ? '已读' : '未读' }}
+
+
+ {{ detailMessage.user_name || '-' }}
+ {{ detailMessage.created_at || '-' }}
+
+
+
+
{{ detailMessage.title }}
+
{{ detailMessage.description || detailMessage.content || '暂无详细内容' }}
+
+ 查看目标
+
+
+
+
+
+ 关闭
+
+ 标记已读
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersReport.vue b/assets/admin/src/pages/users/UsersReport.vue
new file mode 100644
index 00000000..89e01d1d
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersReport.vue
@@ -0,0 +1,357 @@
+
+
+
+
+
+
+
+
+
+ 举报入口:{{ form.user_report_enabled ? '已启用' : '已关闭' }}
+
+
+ 公开入口:{{ form.user_report_public_entry ? '显示' : '隐藏' }}
+
+
+ 审核系统:{{ moderationSummary.enabled ? '已启用' : '未启用' }}
+
+
+ 待处理:{{ counts.pending }}
+
+
+ 已解决:{{ counts.resolved }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 筛选工单
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersSectionPage.vue b/assets/admin/src/pages/users/UsersSectionPage.vue
new file mode 100644
index 00000000..c812af04
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersSectionPage.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ {{ link.label }}
+
+
+
+
+
+
+
+ {{ note }}
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersSignin.vue b/assets/admin/src/pages/users/UsersSignin.vue
new file mode 100644
index 00000000..4f8c0b9c
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersSignin.vue
@@ -0,0 +1,308 @@
+
+
+
+
+
+
+
+
+ 积分系统:{{ form.credits_enabled ? '已启用' : '未启用' }}
+
+
+ 签到能力:{{ form.signin_enabled ? '已启用' : '已关闭' }}
+
+
+ 邀请奖励:{{ form.invite_enabled ? '已启用' : '已关闭' }}
+
+
+ 邀请注册:{{ form.invite_registration_enabled ? '开启' : '关闭' }}
+
+
+ 注册验证码:{{ captchaSummary.register ? '启用' : '未启用' }}
+
+
+ 签到验证码:{{ captchaSummary.signin ? '启用' : '未启用' }}
+
+
+
+
+
+
+ 当前积分系统处于关闭状态。即使保存签到和邀请奖励配置,前台也不会真正发放奖励,直到“用户积分”页重新启用积分系统。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 每次签到基础发放 {{ currencyLabel }} 数量。
+
+
+
+
+
+
+ 每行一个周期,格式 `天数|额外奖励`。例如 `7|5` 表示连续 7 天额外获得 5 {{ currencyLabel }}。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关闭后,邀请链接入口、注册页邀请码字段和个人中心邀请模块会一起隐藏。
+
+
+
+
+
+
+
+
+
+
+
+ 隐藏
+ 选填
+ 必填
+
+
+
+
+
+
+
+ 开启后,必填模式下无效邀请码会直接拒绝注册。关闭则忽略邀请码错误并继续注册。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存配置
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersVerify.vue b/assets/admin/src/pages/users/UsersVerify.vue
new file mode 100644
index 00000000..c133fb96
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersVerify.vue
@@ -0,0 +1,338 @@
+
+
+
+
+
+
+
+
+
+ 认证结构:{{ form.user_verify_enabled ? '已启用' : '已关闭' }}
+
+
+ 邮箱验证:{{ authSummary.emailVerificationEnabled ? '已启用' : '未启用' }}
+
+
+ 待审核:{{ counts.pending }}
+
+
+ 已通过:{{ counts.verified }}
+
+
+ 已驳回:{{ counts.rejected }}
+
+
+
+
+
+
+
+
+ 人工审核台读取 `aksrt_user_verify_status`、`aksrt_user_verify_reason` 和审核日志。开启“邮箱验证通过视为已认证”后,公开页仍会优先展示邮件验证结果。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 筛选记录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/UsersWechatTemplate.vue b/assets/admin/src/pages/users/UsersWechatTemplate.vue
new file mode 100644
index 00000000..ca5e5a7b
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersWechatTemplate.vue
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+
+
+
+
+
+
配置状态
+
{{ status.configured ? '已配置' : '未配置' }}
+
+
+
+
+
+
+
+
Access Token
+
{{ status.has_token ? '已获取' : '未获取' }}
+
+
+
+
+
+
+
+
模板 ID
+
{{ status.has_template ? '已配置' : '未配置' }}
+
+
+
+
+
+
+
+
绑定用户
+
{{ status.bound_users || 0 }} 人
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/components/ReportDetailDrawer.vue b/assets/admin/src/pages/users/components/ReportDetailDrawer.vue
new file mode 100644
index 00000000..89232c3c
--- /dev/null
+++ b/assets/admin/src/pages/users/components/ReportDetailDrawer.vue
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+ {{ detail.report.reporter?.display_name || `用户 #${detail.report.reporter?.id || 0}` }}
+
+
+ {{ targetTitle }}
+
+
+ {{ detail.report.target_type }}
+
+
+ {{ detail.report.category || '-' }}
+
+
+ {{ detail.report.status }}
+
+
+ {{ detail.report.created_at || '-' }}
+
+
+
+
+
证据链
+
+ {{ detail.report.reason || '暂无举报原因' }}
+
+
+
+ 目标 ID:{{ detail.report.target_id }}
+
+
+ 目标状态:{{ detail.report.target.status }}
+
+
+ 打开目标内容
+
+
+
+
+
+
处理记录
+
+
+
+ {{ entry.status || '-' }}
+ {{ entry.timestamp || '-' }}
+
+
{{ entry.note || '无备注' }}
+
{{ entry.operator_name || '系统' }} {{ entry.effect || '' }}
+
+
+
+
+
+
+
处理工单
+
+
+
+
+
+ 提交处理
+ 刷新详情
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue b/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue
new file mode 100644
index 00000000..3b1939a2
--- /dev/null
+++ b/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+ {{ detail.request.display_name || '-' }}
+ {{ detail.request.email || '-' }}
+ {{ detail.request.status || '-' }}
+
+ {{ detail.request.email_verified ? '已完成' : '未完成' }}
+
+ {{ detail.request.requested_at || '-' }}
+ {{ detail.request.reviewed_at || '-' }}
+
+
+
+
当前备注
+
+ {{ detail.request.reason || '暂无备注' }}
+
+
+
+
+
审核历史
+
+
+
+ {{ entry.status || '-' }}
+ {{ entry.time || '-' }}
+
+
{{ entry.reason || '无备注' }}
+
{{ entry.reviewer_name || '系统' }}
+
+
+
+
+
+
+
执行审核
+
+
+
+
+ 提交审核
+ 刷新详情
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/router/component-loaders/core.js b/assets/admin/src/router/component-loaders/core.js
new file mode 100644
index 00000000..5d52c129
--- /dev/null
+++ b/assets/admin/src/router/component-loaders/core.js
@@ -0,0 +1,19 @@
+export const corePageLoaders = {
+ SchemaPage: () => import('@/pages/SchemaPage.vue'),
+ DeferredPage: () => import('@/components/DeferredPage.vue'),
+ FloatingButtonsSettings: () => import('@/pages/content/FloatingButtonsSettings.vue'),
+ FooterTabbarSettings: () => import('@/pages/content/FooterTabbarSettings.vue'),
+ FooterSettings: () => import('@/pages/content/FooterSettings.vue'),
+ HomepageSettings: () => import('@/pages/content/HomepageSettings.vue'),
+ PostPageSettings: () => import('@/pages/post-list/PostPageSettings.vue'),
+ CommentSettings: () => import('@/pages/post-list/CommentSettings.vue'),
+ CommentPermissions: () => import('@/pages/permissions/CommentPermissions.vue'),
+ PermissionsCapabilities: () => import('@/pages/permissions/PermissionsCapabilities.vue'),
+ AttachmentPermissions: () => import('@/pages/permissions/AttachmentPermissions.vue'),
+ DownloadPermissions: () => import('@/pages/permissions/DownloadPermissions.vue'),
+ CmsCredits: () => import('@/pages/CmsCredits.vue'),
+ ResourceDownload: () => import('@/pages/ResourceDownload.vue'),
+ ThemeInfoPage: () => import('@/pages/theme/ThemeInfoPage.vue'),
+ ThemeAuthorizationPage: () => import('@/pages/theme/ThemeAuthorizationPage.vue'),
+ DocsPage: () => import('@/pages/docs/DocsPage.vue'),
+}
diff --git a/assets/admin/src/router/component-loaders/extensions.js b/assets/admin/src/router/component-loaders/extensions.js
new file mode 100644
index 00000000..efa2ed99
--- /dev/null
+++ b/assets/admin/src/router/component-loaders/extensions.js
@@ -0,0 +1,5 @@
+export const extensionsPageLoaders = {
+ ExtensionTools: () => import('@/pages/extensions/ExtensionTools.vue'),
+ ExtensionSecurity: () => import('@/pages/extensions/ExtensionSecurity.vue'),
+ ExtensionAI: () => import('@/pages/extensions/ExtensionAI.vue'),
+}
diff --git a/assets/admin/src/router/component-loaders/forum.js b/assets/admin/src/router/component-loaders/forum.js
new file mode 100644
index 00000000..733a8a59
--- /dev/null
+++ b/assets/admin/src/router/component-loaders/forum.js
@@ -0,0 +1,7 @@
+export const forumPageLoaders = {
+ ForumDashboard: () => import('@/pages/forum/ForumDashboard.vue'),
+ ForumBoards: () => import('@/pages/forum/ForumBoards.vue'),
+ ForumTopics: () => import('@/pages/forum/ForumTopics.vue'),
+ ForumModerators: () => import('@/pages/forum/ForumModerators.vue'),
+ ForumModeratorApplications: () => import('@/pages/forum/ForumModeratorApplications.vue'),
+}
diff --git a/assets/admin/src/router/component-loaders/index.js b/assets/admin/src/router/component-loaders/index.js
new file mode 100644
index 00000000..d7aad84d
--- /dev/null
+++ b/assets/admin/src/router/component-loaders/index.js
@@ -0,0 +1,17 @@
+import { corePageLoaders } from './core'
+import { usersPageLoaders } from './users'
+import { paymentPageLoaders } from './payment'
+import { toolsPageLoaders } from './tools'
+import { shopPageLoaders } from './shop'
+import { forumPageLoaders } from './forum'
+import { extensionsPageLoaders } from './extensions'
+
+export const componentLoaders = {
+ ...corePageLoaders,
+ ...usersPageLoaders,
+ ...paymentPageLoaders,
+ ...toolsPageLoaders,
+ ...shopPageLoaders,
+ ...forumPageLoaders,
+ ...extensionsPageLoaders,
+}
diff --git a/assets/admin/src/router/component-loaders/payment.js b/assets/admin/src/router/component-loaders/payment.js
new file mode 100644
index 00000000..91d9c6f3
--- /dev/null
+++ b/assets/admin/src/router/component-loaders/payment.js
@@ -0,0 +1,6 @@
+export const paymentPageLoaders = {
+ PaymentRevenue: () => import('@/pages/payment/PaymentRevenue.vue'),
+ PaymentGateway: () => import('@/pages/payment/PaymentGateway.vue'),
+ PaymentChargeCard: () => import('@/pages/payment/PaymentChargeCard.vue'),
+ PaymentRebate: () => import('@/pages/payment/PaymentRebate.vue'),
+}
diff --git a/assets/admin/src/router/component-loaders/shop.js b/assets/admin/src/router/component-loaders/shop.js
new file mode 100644
index 00000000..246eee49
--- /dev/null
+++ b/assets/admin/src/router/component-loaders/shop.js
@@ -0,0 +1,9 @@
+export const shopPageLoaders = {
+ ShopDashboard: () => import('@/pages/shop/ShopDashboard.vue'),
+ ShopProducts: () => import('@/pages/shop/ShopProducts.vue'),
+ ShopOrders: () => import('@/pages/shop/ShopOrders.vue'),
+ ShopWithdrawals: () => import('@/pages/shop/ShopWithdrawals.vue'),
+ ShopCoupons: () => import('@/pages/shop/ShopCoupons.vue'),
+ ShopAfterSale: () => import('@/pages/shop/ShopAfterSale.vue'),
+ ShopShipping: () => import('@/pages/shop/ShopShipping.vue'),
+}
diff --git a/assets/admin/src/router/component-loaders/tools.js b/assets/admin/src/router/component-loaders/tools.js
new file mode 100644
index 00000000..b7d1fdbc
--- /dev/null
+++ b/assets/admin/src/router/component-loaders/tools.js
@@ -0,0 +1,7 @@
+export const toolsPageLoaders = {
+ ToolsImportExport: () => import('@/pages/tools/ToolsImportExport.vue'),
+ ToolsHealth: () => import('@/pages/tools/ToolsHealth.vue'),
+ ToolsMigration: () => import('@/pages/tools/ToolsMigration.vue'),
+ ToolsEmailRollback: () => import('@/pages/tools/ToolsEmailRollback.vue'),
+ ToolsUpdate: () => import('@/pages/tools/ToolsUpdate.vue'),
+}
diff --git a/assets/admin/src/router/component-loaders/users.js b/assets/admin/src/router/component-loaders/users.js
new file mode 100644
index 00000000..54b2583d
--- /dev/null
+++ b/assets/admin/src/router/component-loaders/users.js
@@ -0,0 +1,12 @@
+export const usersPageLoaders = {
+ UsersFeatures: () => import('@/pages/users/UsersFeatures.vue'),
+ UsersSignin: () => import('@/pages/users/UsersSignin.vue'),
+ UsersInviteCodes: () => import('@/pages/users/UsersInviteCodes.vue'),
+ UsersVerify: () => import('@/pages/users/UsersVerify.vue'),
+ UsersLevel: () => import('@/pages/users/UsersLevel.vue'),
+ UsersBadge: () => import('@/pages/users/UsersBadge.vue'),
+ UsersReport: () => import('@/pages/users/UsersReport.vue'),
+ UsersMessages: () => import('@/pages/users/UsersMessages.vue'),
+ UsersBan: () => import('@/pages/users/UsersBan.vue'),
+ UsersWechatTemplate: () => import('@/pages/users/UsersWechatTemplate.vue'),
+}
diff --git a/assets/admin/src/router/index.js b/assets/admin/src/router/index.js
new file mode 100644
index 00000000..0a197069
--- /dev/null
+++ b/assets/admin/src/router/index.js
@@ -0,0 +1,66 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import { getAllAdminPages } from '@/constants/admin-sections'
+import { componentLoaders } from './component-loaders/index.js'
+
+function resolveComponent(page) {
+ if (componentLoaders[page.component]) {
+ return componentLoaders[page.component]
+ }
+
+ // Log missing component for debugging — avoids silent fallback to DeferredPage
+ if (page.component && page.component !== 'DeferredPage') {
+ console.warn(
+ `[AKSRT Route] Component "${page.component}" not found in componentLoaders for route "${page.route}". Falling back to DeferredPage.`
+ )
+ }
+
+ return componentLoaders.DeferredPage
+}
+
+function createRouteMeta(page) {
+ const meta = {
+ title: page.title,
+ section: page.sectionKey || '',
+ desc: page.desc || '',
+ status: page.status || 'available',
+ }
+
+ if (page.canonicalDomain) {
+ meta.canonicalDomain = page.canonicalDomain
+ }
+
+ if (page.section) {
+ meta.section = page.section
+ }
+
+ if (page.component === 'DeferredPage') {
+ meta.deferred = true
+ meta.deferredProps = {
+ title: page.title,
+ description: page.desc || '',
+ icon: page.iconClass || 'fas fa-compass-drafting',
+ phase: page.phase || '',
+ status: page.status || 'deferred',
+ dependsOn: page.dependsOn || [],
+ reason: page.reason || '',
+ features: page.features || [],
+ }
+ }
+
+ return meta
+}
+
+const routes = [
+ { path: '/', redirect: '/global/common' },
+ ...getAllAdminPages().map((page) => ({
+ path: page.route,
+ name: page.name,
+ component: resolveComponent(page),
+ meta: createRouteMeta(page),
+ })),
+]
+
+export default createRouter({
+ history: createWebHashHistory(),
+ routes,
+})
diff --git a/assets/admin/src/schema/index.js b/assets/admin/src/schema/index.js
new file mode 100644
index 00000000..64f20229
--- /dev/null
+++ b/assets/admin/src/schema/index.js
@@ -0,0 +1,114 @@
+/**
+ * Schema Registry — central index for all admin section schemas.
+ *
+ * Each section's schema defines the form fields, groups, API endpoints,
+ * and default values for a settings page. The AksFormRenderer component
+ * consumes these schemas to render settings pages automatically.
+ *
+ * Schema shape per section:
+ * {
+ * key: 'global.seo',
+ * title: 'SEO优化',
+ * icon: 'fas fa-search',
+ * description: '...',
+ * api: { get: () => api.getSeoSettings(), save: (data) => api.saveSeoSettings(data) },
+ * defaults: { ... },
+ * groups: [
+ * {
+ * key: 'basic',
+ * title: '基础SEO',
+ * icon: 'fas fa-globe',
+ * fields: [
+ * { key: 'site_title', label: '站点标题', type: 'input', default: '' },
+ * ...
+ * ]
+ * }
+ * ]
+ * }
+ */
+import { globalSchemas } from './sections/global/index.js'
+import { pageDisplaySchemas } from './sections/page-display/index.js'
+import { postListSchemas } from './sections/post-list/index.js'
+import { permissionsSchemas } from './sections/permissions/index.js'
+import { usersSchemas } from './sections/users/index.js'
+import { paymentSchemas } from './sections/payment/index.js'
+import { extensionsSchemas } from './sections/extensions/index.js'
+import { toolsSchemas } from './sections/tools/index.js'
+import { shopSchemas } from './sections/shop/index.js'
+import { forumSchemas } from './sections/forum/index.js'
+
+export function normalizeSchemaDomain(domain = '') {
+ const value = String(domain).trim()
+ if (!value) {
+ return 'global'
+ }
+
+ return value.replace(/-/g, '_')
+}
+
+export const CANONICAL_SCHEMA_DOMAINS = [
+ 'global',
+ 'page_display',
+ 'post_list',
+ 'permissions',
+ 'users',
+ 'payment',
+ 'extensions',
+ 'tools',
+ 'shop',
+ 'forum',
+]
+
+/** All schemas organized by domain */
+export const ALL_SCHEMAS = Object.freeze({
+ global: globalSchemas,
+ page_display: pageDisplaySchemas,
+ post_list: postListSchemas,
+ permissions: permissionsSchemas,
+ users: usersSchemas,
+ payment: paymentSchemas,
+ extensions: extensionsSchemas,
+ tools: toolsSchemas,
+ shop: shopSchemas,
+ forum: forumSchemas,
+})
+
+/**
+ * Get a specific section schema by its key path.
+ * @param {string} domain - e.g. 'global'
+ * @param {string} section - e.g. 'seo'
+ * @returns {Object|null}
+ */
+export function getSchema(domain, section) {
+ const domainSchemas = ALL_SCHEMAS[normalizeSchemaDomain(domain)]
+ if (!domainSchemas) return null
+ return domainSchemas[section] || null
+}
+
+/**
+ * Get all schemas for a domain.
+ * @param {string} domain
+ * @returns {Object}
+ */
+export function getDomainSchemas(domain) {
+ return ALL_SCHEMAS[normalizeSchemaDomain(domain)] || {}
+}
+
+export function getSchemaSectionKeys(domain) {
+ return Object.keys(getDomainSchemas(domain))
+}
+
+export function getSchemaDefaults(domain, section = '') {
+ const domainKey = normalizeSchemaDomain(domain)
+
+ if (section) {
+ return { ...(getSchema(domainKey, section)?.defaults || {}) }
+ }
+
+ return Object.values(getDomainSchemas(domainKey)).reduce((defaults, schema) => {
+ return {
+ ...defaults,
+ ...(schema?.defaults || {}),
+ }
+ }, {})
+}
diff --git a/assets/admin/src/schema/sections/extensions/audit.js b/assets/admin/src/schema/sections/extensions/audit.js
new file mode 100644
index 00000000..c0df6ec9
--- /dev/null
+++ b/assets/admin/src/schema/sections/extensions/audit.js
@@ -0,0 +1,68 @@
+export const auditSchema = {
+ key: 'extensions.audit',
+ title: 'API内容审核',
+ icon: 'fas fa-shield-virus',
+ description: '配置第三方内容审核 API 接入、审核范围和失败回退策略。',
+ defaults: {
+ content_audit_enabled: false,
+ content_audit_provider: 'baidu',
+ content_audit_api_key: '',
+ content_audit_api_secret: '',
+ content_audit_text_enabled: true,
+ content_audit_image_enabled: false,
+ content_audit_on_failure: 'allow',
+ content_audit_log_enabled: true,
+ },
+ groups: [
+ {
+ key: 'provider',
+ title: '审核接口',
+ icon: 'fas fa-plug',
+ fields: [
+ { key: 'content_audit_enabled', label: '启用内容审核', type: 'switch', default: false, help: '开启后将对接第三方内容审核 API' },
+ {
+ key: 'content_audit_provider',
+ label: '审核服务商',
+ type: 'radio',
+ default: 'baidu',
+ options: [
+ { label: '百度内容审核', value: 'baidu' },
+ { label: '关闭审核接口', value: 'null' },
+ ],
+ dependsOn: { key: 'content_audit_enabled', value: true },
+ },
+ { key: 'content_audit_api_key', label: 'API Key', type: 'input', default: '', dependsOn: { key: 'content_audit_enabled', value: true }, help: '百度智能云 API Key' },
+ { key: 'content_audit_api_secret', label: 'Secret Key', type: 'input', default: '', dependsOn: { key: 'content_audit_enabled', value: true }, help: '百度智能云 Secret Key' },
+ ],
+ },
+ {
+ key: 'scope',
+ title: '审核范围',
+ icon: 'fas fa-check-double',
+ fields: [
+ { key: 'content_audit_text_enabled', label: '文本审核', type: 'switch', default: true, dependsOn: { key: 'content_audit_enabled', value: true } },
+ { key: 'content_audit_image_enabled', label: '图片审核', type: 'switch', default: false, dependsOn: { key: 'content_audit_enabled', value: true } },
+ { key: 'audit_bbs_term', label: '审核论坛分类', type: 'switch', default: false, dependsOn: { key: 'content_audit_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'fallback',
+ title: '失败策略',
+ icon: 'fas fa-exclamation-triangle',
+ fields: [
+ {
+ key: 'content_audit_on_failure',
+ label: '审核失败时',
+ type: 'radio',
+ default: 'allow',
+ options: [
+ { label: '放行', value: 'allow' },
+ { label: '拦截', value: 'block' },
+ ],
+ dependsOn: { key: 'content_audit_enabled', value: true },
+ },
+ { key: 'content_audit_log_enabled', label: '记录审核日志', type: 'switch', default: true, dependsOn: { key: 'content_audit_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/extensions/docs-mode.js b/assets/admin/src/schema/sections/extensions/docs-mode.js
new file mode 100644
index 00000000..5ca43a35
--- /dev/null
+++ b/assets/admin/src/schema/sections/extensions/docs-mode.js
@@ -0,0 +1,125 @@
+export const docsModeSchema = {
+ key: 'extensions.docs-mode',
+ title: '文档模式',
+ icon: 'fas fa-book-open',
+ description: '启用文档专题内容模式,配置文档导航页面和文档站式体验。',
+ defaults: {
+ docs_mode_enabled: false,
+ docs_mode_default_template: 'documentnav',
+ docs_mode_toc_enabled: true,
+ docs_mode_chapter_nav_enabled: true,
+ docs_mode_default_order: 'menu_order',
+ docs_mode_cats: [],
+ docs_mode_exclude: true,
+ docs_mode_toc_depth: 3,
+ docs_mode_chapter_title: '',
+ docs_mode_style: 'default',
+ docs_mode_icon: true,
+ docs_mode_count: true,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '基础配置',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'docs_mode_enabled', label: '启用文档模式', type: 'switch', default: false, help: '开启后将激活文档导航页面模板和文档站式侧边栏' },
+ {
+ key: 'docs_mode_default_template',
+ label: '文档首页模板',
+ type: 'select',
+ default: 'documentnav',
+ options: [
+ { label: '文档导航页', value: 'documentnav' },
+ { label: '专题聚合页', value: 'special' },
+ ],
+ dependsOn: { key: 'docs_mode_enabled', value: true },
+ },
+ ],
+ },
+ {
+ key: 'navigation',
+ title: '导航设置',
+ icon: 'fas fa-compass',
+ fields: [
+ { key: 'docs_mode_toc_enabled', label: '显示目录树', type: 'switch', default: true, dependsOn: { key: 'docs_mode_enabled', value: true }, help: '在文档页面侧边栏显示章节目录树' },
+ { key: 'docs_mode_chapter_nav_enabled', label: '显示章节导航', type: 'switch', default: true, dependsOn: { key: 'docs_mode_enabled', value: true }, help: '在文档底部显示上一章/下一章导航' },
+ {
+ key: 'docs_mode_default_order',
+ label: '默认排序',
+ type: 'radio',
+ default: 'menu_order',
+ options: [
+ { label: '按菜单顺序', value: 'menu_order' },
+ { label: '按发布时间', value: 'date' },
+ { label: '按标题排序', value: 'title' },
+ ],
+ dependsOn: { key: 'docs_mode_enabled', value: true },
+ },
+ {
+ key: 'docs_mode_toc_depth',
+ label: '目录深度',
+ type: 'number',
+ default: 3,
+ min: 1,
+ max: 6,
+ dependsOn: { key: 'docs_mode_enabled', value: true },
+ },
+ {
+ key: 'docs_mode_chapter_title',
+ label: '章节标题',
+ type: 'input',
+ default: '',
+ dependsOn: { key: 'docs_mode_enabled', value: true },
+ },
+ ],
+ },
+ {
+ key: 'filter',
+ title: '筛选设置',
+ icon: 'fas fa-filter',
+ fields: [
+ {
+ key: 'docs_mode_cats',
+ label: '文档分类',
+ type: 'taxonomy_select',
+ taxonomy: 'category',
+ multiple: true,
+ clearable: true,
+ default: [],
+ placeholder: '选择用于文档模式的分类',
+ dependsOn: { key: 'docs_mode_enabled', value: true },
+ },
+ {
+ key: 'docs_mode_exclude',
+ label: '在首页排除此类内容',
+ type: 'switch',
+ default: true,
+ dependsOn: { key: 'docs_mode_enabled', value: true },
+ help: '开启后,所选文档分类的内容不会出现在首页常规文章流中。',
+ },
+ ],
+ },
+ {
+ key: 'style',
+ title: '样式设置',
+ icon: 'fas fa-palette',
+ fields: [
+ {
+ key: 'docs_mode_style',
+ label: '文档样式',
+ type: 'select',
+ default: 'default',
+ options: [
+ { label: '默认', value: 'default' },
+ { label: '简洁', value: 'simple' },
+ { label: '卡片', value: 'card' },
+ ],
+ dependsOn: { key: 'docs_mode_enabled', value: true },
+ },
+ { key: 'docs_mode_icon', label: '显示图标', type: 'switch', default: true, dependsOn: { key: 'docs_mode_enabled', value: true } },
+ { key: 'docs_mode_count', label: '显示文章数', type: 'switch', default: true, dependsOn: { key: 'docs_mode_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/extensions/index.js b/assets/admin/src/schema/sections/extensions/index.js
new file mode 100644
index 00000000..2aa4850e
--- /dev/null
+++ b/assets/admin/src/schema/sections/extensions/index.js
@@ -0,0 +1,16 @@
+/**
+ * Extensions & Enhancements domain schemas.
+ * Each key corresponds to a sub-page under the "扩展&增强" section.
+ */
+
+import { ipSchema } from './ip.js'
+import { auditSchema } from './audit.js'
+import { pushSchema } from './push.js'
+import { docsModeSchema } from './docs-mode.js'
+
+export const extensionsSchemas = {
+ ip: ipSchema,
+ audit: auditSchema,
+ push: pushSchema,
+ 'docs-mode': docsModeSchema,
+}
diff --git a/assets/admin/src/schema/sections/extensions/ip.js b/assets/admin/src/schema/sections/extensions/ip.js
new file mode 100644
index 00000000..ab6ede35
--- /dev/null
+++ b/assets/admin/src/schema/sections/extensions/ip.js
@@ -0,0 +1,86 @@
+export const ipSchema = {
+ key: 'extensions.ip',
+ title: 'IP归属地',
+ icon: 'fas fa-map-marker-alt',
+ description: '配置 IP 数据源、缓存策略和前台归属地展示。',
+ defaults: {
+ ip_geo_enabled: false,
+ ip_addr_sdk: 'null',
+ ip_geo_cache_ttl: 86400,
+ ip_geo_display: ['comment'],
+ ip_geo_format: 'province_city',
+ ip_geo_fallback_text: '',
+ ip_addr_sdk_qq_appkey: '',
+ ip_addr_sdk_qq_secretkey: '',
+ ip_addr_sdk_amap_appkey: '',
+ ip_addr_sdk_amap_secretkey: '',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '基础配置',
+ icon: 'fas fa-globe',
+ fields: [
+ { key: 'ip_geo_enabled', label: '启用 IP 归属地', type: 'switch', default: false, help: '开启后将在评论、用户主页等位置显示 IP 归属地信息' },
+ {
+ key: 'ip_addr_sdk',
+ label: 'IP 接口服务商',
+ type: 'radio',
+ default: 'null',
+ options: [
+ { label: '轮流查询', value: 'polling' },
+ { label: '腾讯位置服务', value: 'qq' },
+ { label: '高德位置服务', value: 'amap' },
+ { label: '太平洋公共接口', value: 'null' },
+ ],
+ dependsOn: { key: 'ip_geo_enabled', value: true },
+ help: '优先使用国内接口,轮流查询会按腾讯→高德→公共接口顺序回退。',
+ },
+ { key: 'ip_geo_cache_ttl', label: '缓存时间 (秒)', type: 'number', default: 86400, min: 3600, max: 604800, dependsOn: { key: 'ip_geo_enabled', value: true }, help: 'IP 查询结果缓存时长,避免频繁请求外部接口' },
+ ],
+ },
+ {
+ key: 'providers',
+ title: '接口凭据',
+ icon: 'fas fa-key',
+ fields: [
+ { key: 'ip_addr_sdk_qq_appkey', label: '腾讯 AppKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', value: 'qq' } },
+ { key: 'ip_addr_sdk_qq_secretkey', label: '腾讯 SecretKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', value: 'qq' } },
+ { key: 'ip_addr_sdk_amap_appkey', label: '高德 AppKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', value: 'amap' } },
+ { key: 'ip_addr_sdk_amap_secretkey', label: '高德 SecretKey', type: 'input', default: '', dependsOn: { key: 'ip_addr_sdk', value: 'amap' } },
+ ],
+ },
+ {
+ key: 'display',
+ title: '展示设置',
+ icon: 'fas fa-eye',
+ fields: [
+ {
+ key: 'ip_geo_display',
+ label: '展示位置',
+ type: 'checkbox',
+ default: ['comment'],
+ options: [
+ { label: '评论区', value: 'comment' },
+ { label: '用户主页', value: 'user_home' },
+ { label: '文章页', value: 'post' },
+ ],
+ dependsOn: { key: 'ip_geo_enabled', value: true },
+ },
+ {
+ key: 'ip_geo_format',
+ label: '显示格式',
+ type: 'radio',
+ default: 'province_city',
+ options: [
+ { label: '省份+城市', value: 'province_city' },
+ { label: '仅省份', value: 'province' },
+ { label: '国家+省份', value: 'country_province' },
+ ],
+ dependsOn: { key: 'ip_geo_enabled', value: true },
+ },
+ { key: 'ip_geo_fallback_text', label: '查询失败时文案', type: 'input', default: '', placeholder: '留空则不显示', dependsOn: { key: 'ip_geo_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/extensions/push.js b/assets/admin/src/schema/sections/extensions/push.js
new file mode 100644
index 00000000..e847d2be
--- /dev/null
+++ b/assets/admin/src/schema/sections/extensions/push.js
@@ -0,0 +1,64 @@
+export const pushSchema = {
+ key: 'extensions.push',
+ title: '百度推送',
+ icon: 'fas fa-paper-plane',
+ description: '配置搜索引擎主动推送、批量提交和状态追踪。',
+ defaults: {
+ baidu_push_enabled: false,
+ baidu_push_token: '',
+ baidu_push_auto: true,
+ baidu_push_on_publish: true,
+ baidu_push_on_update: false,
+ baidu_push_batch_size: 50,
+ baidu_push_log_enabled: true,
+ xzh_on: false,
+ xzh_appid: '',
+ xzh_render_tail: true,
+ xzh_jsonld_single: true,
+ xzh_jsonld_page: false,
+ xzh_jsonld_img: false,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '推送配置',
+ icon: 'fas fa-rocket',
+ fields: [
+ { key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
+ { key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token' },
+ ],
+ },
+ {
+ key: 'trigger',
+ title: '触发规则',
+ icon: 'fas fa-bolt',
+ fields: [
+ { key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
+ { key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
+ { key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'xzh',
+ title: '熊掌号 / JSON-LD',
+ icon: 'fas fa-paw',
+ fields: [
+ { key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false },
+ { key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } },
+ { key: 'xzh_render_tail', label: '文章底部显示熊掌号尾部', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
+ { key: 'xzh_jsonld_single', label: '文章页 JSON-LD', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
+ { key: 'xzh_jsonld_page', label: '页面 JSON-LD', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
+ { key: 'xzh_jsonld_img', label: '不添加图片', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
+ ],
+ },
+ {
+ key: 'advanced',
+ title: '高级设置',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'baidu_push_batch_size', label: '批量推送条数', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'baidu_push_enabled', value: true } },
+ { key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/basic.js b/assets/admin/src/schema/sections/forum/basic.js
new file mode 100644
index 00000000..c43a8f14
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/basic.js
@@ -0,0 +1,28 @@
+export const basicSchema = {
+ key: 'forum.basic',
+ title: '基础设置',
+ icon: 'fas fa-power-off',
+ description: '论坛总开关与管理员配置。',
+ defaults: {
+ bbs_s: true,
+ bbs_admin_users: [],
+ },
+ groups: [
+ {
+ key: 'switch',
+ title: '功能开关',
+ icon: 'fas fa-toggle-on',
+ fields: [
+ { key: 'bbs_s', label: '启用论坛', type: 'switch', default: true, help: '关闭后论坛页面将返回 404' },
+ ],
+ },
+ {
+ key: 'admin',
+ title: '论坛管理',
+ icon: 'fas fa-user-shield',
+ fields: [
+ { key: 'bbs_admin_users', label: '论坛管理员', type: 'user_select', multiple: true, default: [], help: '拥有跨版块全部治理权限,绕过 ban' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/board-page.js b/assets/admin/src/schema/sections/forum/board-page.js
new file mode 100644
index 00000000..40133c99
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/board-page.js
@@ -0,0 +1,31 @@
+export const boardPageSchema = {
+ key: 'forum.board_page',
+ title: '版块页面',
+ icon: 'fas fa-th-large',
+ description: '版块页布局、Tab 栏目和移动端交互配置。',
+ defaults: {
+ bbs_plate_top_info_s: true,
+ bbs_plate_tab: [
+ { label: '全部', sort: 'date' },
+ { label: '推荐', sort: 'featured' },
+ { label: '最新回复', sort: 'last_reply' },
+ { label: '热门', sort: 'hot' },
+ { label: '精华', sort: 'essence' },
+ ],
+ bbs_plate_tab_active_index: 0,
+ bbs_plate_tab_swiper: true,
+ },
+ groups: [
+ {
+ key: 'layout',
+ title: '版块页布局',
+ icon: 'fas fa-layer-group',
+ fields: [
+ { key: 'bbs_plate_top_info_s', label: '版块信息卡', type: 'switch', default: true, help: '版块页顶部显示版块详情卡片' },
+ { key: 'bbs_plate_tab', label: '版块栏目', type: 'group', default: [{ label: '全部', sort: 'date' }, { label: '推荐', sort: 'featured' }, { label: '最新回复', sort: 'last_reply' }, { label: '热门', sort: 'hot' }, { label: '精华', sort: 'essence' }] },
+ { key: 'bbs_plate_tab_active_index', label: '默认选中 Tab', type: 'number', default: 0, min: 0 },
+ { key: 'bbs_plate_tab_swiper', label: '移动端滑动切换', type: 'switch', default: true },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/credits.js b/assets/admin/src/schema/sections/forum/credits.js
new file mode 100644
index 00000000..154fabdb
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/credits.js
@@ -0,0 +1,35 @@
+export const creditsSchema = {
+ key: 'forum.credits',
+ title: '积分设置',
+ icon: 'fas fa-coins',
+ description: '论坛行为积分奖励矩阵和每日积分上限。',
+ defaults: {
+ bbs_credits_post_new: 5,
+ bbs_credits_plate_new: 10,
+ bbs_credits_comment_new: 2,
+ bbs_credits_comment_like: 1,
+ bbs_credits_score_extra: 3,
+ bbs_credits_essence: 10,
+ bbs_credits_posts_hot: 5,
+ bbs_credits_adopted: 20,
+ bbs_credits_daily_max: 50,
+ },
+ groups: [
+ {
+ key: 'earn',
+ title: '积分奖励',
+ icon: 'fas fa-gift',
+ fields: [
+ { key: 'bbs_credits_post_new', label: '发帖奖励', type: 'number', default: 5, min: 0 },
+ { key: 'bbs_credits_plate_new', label: '建版奖励', type: 'number', default: 10, min: 0 },
+ { key: 'bbs_credits_comment_new', label: '回复奖励', type: 'number', default: 2, min: 0 },
+ { key: 'bbs_credits_comment_like', label: '评论被赞', type: 'number', default: 1, min: 0 },
+ { key: 'bbs_credits_score_extra', label: '被加分', type: 'number', default: 3, min: 0 },
+ { key: 'bbs_credits_essence', label: '被设精华', type: 'number', default: 10, min: 0 },
+ { key: 'bbs_credits_posts_hot', label: '成为热帖', type: 'number', default: 5, min: 0 },
+ { key: 'bbs_credits_adopted', label: '被采纳', type: 'number', default: 20, min: 0 },
+ { key: 'bbs_credits_daily_max', label: '每日积分上限', type: 'number', default: 50, min: 0, help: '0 = 不限' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/homepage.js b/assets/admin/src/schema/sections/forum/homepage.js
new file mode 100644
index 00000000..e36b0c2a
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/homepage.js
@@ -0,0 +1,29 @@
+export const homepageSchema = {
+ key: 'forum.homepage',
+ title: '首页设置',
+ icon: 'fas fa-home',
+ description: '论坛首页 Tab 引擎、快速发帖入口和移动端交互配置。',
+ defaults: {
+ bbs_home_tab: [
+ { type: 'follow', label: '关注' },
+ { type: 'synthesis', label: '综合' },
+ { type: 'plate', label: '版块' },
+ ],
+ bbs_home_tab_active_index: 1,
+ bbs_home_tab_quick_posts_s: true,
+ bbs_home_tab_swiper: true,
+ },
+ groups: [
+ {
+ key: 'tabs',
+ title: '首页 Tab',
+ icon: 'fas fa-columns',
+ fields: [
+ { key: 'bbs_home_tab', label: '首页栏目', type: 'group', default: [{ type: 'follow', label: '关注' }, { type: 'synthesis', label: '综合' }, { type: 'plate', label: '版块' }], help: '可拖拽排序,支持 follow / synthesis / plate / tabs 类型' },
+ { key: 'bbs_home_tab_active_index', label: '默认选中 Tab', type: 'number', default: 1, min: 0 },
+ { key: 'bbs_home_tab_quick_posts_s', label: '快速发帖', type: 'switch', default: true, help: '首页顶部显示快速发帖入口' },
+ { key: 'bbs_home_tab_swiper', label: '移动端滑动切换', type: 'switch', default: true },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/index.js b/assets/admin/src/schema/sections/forum/index.js
new file mode 100644
index 00000000..d0396a9d
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/index.js
@@ -0,0 +1,31 @@
+/**
+ * Forum domain schemas.
+ *
+ * 12 sections matching PHP schema_forum():
+ * basic / names / homepage / board_page / post_page / reply /
+ * permissions / other / upload / credits
+ */
+
+import { basicSchema } from './basic.js'
+import { namesSchema } from './names.js'
+import { homepageSchema } from './homepage.js'
+import { boardPageSchema } from './board-page.js'
+import { postPageSchema } from './post-page.js'
+import { replySchema } from './reply.js'
+import { permissionsSchema } from './permissions.js'
+import { otherSchema } from './other.js'
+import { uploadSchema } from './upload.js'
+import { creditsSchema } from './credits.js'
+
+export const forumSchemas = {
+ basic: basicSchema,
+ names: namesSchema,
+ homepage: homepageSchema,
+ board_page: boardPageSchema,
+ post_page: postPageSchema,
+ reply: replySchema,
+ permissions: permissionsSchema,
+ other: otherSchema,
+ upload: uploadSchema,
+ credits: creditsSchema,
+}
diff --git a/assets/admin/src/schema/sections/forum/names.js b/assets/admin/src/schema/sections/forum/names.js
new file mode 100644
index 00000000..80f99d72
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/names.js
@@ -0,0 +1,41 @@
+export const namesSchema = {
+ key: 'forum.names',
+ title: '名称定义',
+ icon: 'fas fa-language',
+ description: '自定义论坛各实体的显示名称(12 词条)。',
+ defaults: {
+ bbs_forum_display_name: '论坛',
+ bbs_plate_display_name: '版块',
+ bbs_posts_display_name: '帖子',
+ bbs_topic_display_name: '话题',
+ bbs_tag_display_name: '标签',
+ bbs_comment_display_name: '回复',
+ bbs_question_badge_name: '提问',
+ bbs_question_ok_badge_name: '已解决',
+ bbs_cat_moderator_name: '分区版主',
+ bbs_plate_author_name: '超级版主',
+ bbs_plate_moderator_name: '版主',
+ bbs_plate_follow_name: '关注',
+ },
+ groups: [
+ {
+ key: 'display_names',
+ title: '名词自定义',
+ icon: 'fas fa-tags',
+ fields: [
+ { key: 'bbs_forum_display_name', label: '论坛', type: 'input', default: '论坛' },
+ { key: 'bbs_plate_display_name', label: '版块', type: 'input', default: '版块' },
+ { key: 'bbs_posts_display_name', label: '帖子', type: 'input', default: '帖子' },
+ { key: 'bbs_topic_display_name', label: '话题', type: 'input', default: '话题' },
+ { key: 'bbs_tag_display_name', label: '标签', type: 'input', default: '标签' },
+ { key: 'bbs_comment_display_name', label: '回复', type: 'input', default: '回复' },
+ { key: 'bbs_question_badge_name', label: '提问徽章', type: 'input', default: '提问' },
+ { key: 'bbs_question_ok_badge_name', label: '已解决徽章', type: 'input', default: '已解决' },
+ { key: 'bbs_cat_moderator_name', label: '分区版主', type: 'input', default: '分区版主' },
+ { key: 'bbs_plate_author_name', label: '超级版主', type: 'input', default: '超级版主' },
+ { key: 'bbs_plate_moderator_name', label: '版主', type: 'input', default: '版主' },
+ { key: 'bbs_plate_follow_name', label: '关注', type: 'input', default: '关注' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/other.js b/assets/admin/src/schema/sections/forum/other.js
new file mode 100644
index 00000000..a0fa154a
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/other.js
@@ -0,0 +1,64 @@
+export const otherSchema = {
+ key: 'forum.other',
+ title: '其它设置',
+ icon: 'fas fa-cog',
+ description: '投票、版主申请说明、标题字数限制和 URL 别名配置。',
+ defaults: {
+ bbs_vote_number_type: 'percent',
+ bbs_vote_max: 8,
+ bbs_apply_moderator_desc: '欢迎有热情的用户申请成为版主,协助管理社区秩序。',
+ bbs_term_thumb: '',
+ bbs_post_lists_hide_pay_s: false,
+ bbs_post_title_strlen_limit: 30,
+ bbs_t_placeholder: '请输入标题',
+ bbs_c_placeholder: '请输入内容',
+ bbs_rewrite_suffix_html_s: false,
+ bbs_plate_rewrite_slug: 'board',
+ bbs_posts_rewrite_slug: 'topic',
+ bbs_posts_edit_rewrite_slug: 'topic-edit',
+ bbs_posts_show_in_rest_s: false,
+ },
+ groups: [
+ {
+ key: 'vote',
+ title: '投票',
+ icon: 'fas fa-poll',
+ fields: [
+ { key: 'bbs_vote_number_type', label: '投票显示', type: 'select', default: 'percent', options: [{ label: '百分比', value: 'percent' }, { label: '票数', value: 'number' }] },
+ { key: 'bbs_vote_max', label: '选项上限', type: 'number', default: 8, min: 2, max: 20 },
+ ],
+ },
+ {
+ key: 'moderator_apply',
+ title: '版主申请',
+ icon: 'fas fa-user-plus',
+ fields: [
+ { key: 'bbs_apply_moderator_desc', label: '申请说明', type: 'textarea', default: '欢迎有热情的用户申请成为版主,协助管理社区秩序。' },
+ ],
+ },
+ {
+ key: 'misc',
+ title: '杂项',
+ icon: 'fas fa-sliders-h',
+ fields: [
+ { key: 'bbs_term_thumb', label: '标签默认缩略图', type: 'media', default: '' },
+ { key: 'bbs_post_lists_hide_pay_s', label: '列表隐藏付费标记', type: 'switch', default: false },
+ { key: 'bbs_post_title_strlen_limit', label: '标题字数限制', type: 'number', default: 30, min: 5, max: 100, help: '最小 5,最大 100' },
+ { key: 'bbs_t_placeholder', label: '标题占位符', type: 'input', default: '请输入标题' },
+ { key: 'bbs_c_placeholder', label: '内容占位符', type: 'input', default: '请输入内容' },
+ ],
+ },
+ {
+ key: 'url',
+ title: 'URL / 别名',
+ icon: 'fas fa-link',
+ fields: [
+ { key: 'bbs_rewrite_suffix_html_s', label: '.html 后缀', type: 'switch', default: false },
+ { key: 'bbs_plate_rewrite_slug', label: '版块别名', type: 'input', default: 'board' },
+ { key: 'bbs_posts_rewrite_slug', label: '帖子别名', type: 'input', default: 'topic' },
+ { key: 'bbs_posts_edit_rewrite_slug', label: '编辑别名', type: 'input', default: 'topic-edit' },
+ { key: 'bbs_posts_show_in_rest_s', label: '古腾堡编辑器', type: 'switch', default: false, help: '开启后帖子可使用 Gutenberg' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/permissions.js b/assets/admin/src/schema/sections/forum/permissions.js
new file mode 100644
index 00000000..96eb7713
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/permissions.js
@@ -0,0 +1,64 @@
+export const permissionsSchema = {
+ key: 'forum.permissions',
+ title: '权限配置',
+ icon: 'fas fa-lock',
+ description: '发帖限制、按钮显示、帖子付费和版块付费关注配置。',
+ defaults: {
+ bbs_posts_add_limit_opt_max: 10,
+ bbs_plate_add_limit_opt_max: 3,
+ bbs_show_new_plate: false,
+ bbs_show_new_topic: true,
+ bbs_show_apply_moderator: true,
+ bbs_post_pay_hide_type_s: false,
+ bbs_post_pay_vip_price_s: false,
+ bbs_post_pay_vip_1_discount: 80,
+ bbs_post_pay_vip_2_discount: 50,
+ bbs_post_pay_price_limit: 1000,
+ bbs_plate_follow_pay_s: false,
+ bbs_plate_follow_pay_price: 10,
+ bbs_plate_follow_pay_type: 'money',
+ },
+ groups: [
+ {
+ key: 'posting',
+ title: '发帖限制',
+ icon: 'fas fa-ban',
+ fields: [
+ { key: 'bbs_posts_add_limit_opt_max', label: '每日发帖上限', type: 'number', default: 10, min: 0, help: '0 = 不限' },
+ { key: 'bbs_plate_add_limit_opt_max', label: '建版数量上限', type: 'number', default: 3, min: 0 },
+ ],
+ },
+ {
+ key: 'display_btns',
+ title: '按钮显示',
+ icon: 'fas fa-eye',
+ fields: [
+ { key: 'bbs_show_new_plate', label: '始终显示建版按钮', type: 'switch', default: false },
+ { key: 'bbs_show_new_topic', label: '始终显示发帖按钮', type: 'switch', default: true },
+ { key: 'bbs_show_apply_moderator', label: '始终显示申请版主', type: 'switch', default: true },
+ ],
+ },
+ {
+ key: 'paid_content',
+ title: '帖子付费',
+ icon: 'fas fa-money-bill-wave',
+ fields: [
+ { key: 'bbs_post_pay_hide_type_s', label: '启用付费内容', type: 'switch', default: false },
+ { key: 'bbs_post_pay_vip_price_s', label: 'VIP 特价', type: 'switch', default: false, dependsOn: { key: 'bbs_post_pay_hide_type_s', value: true } },
+ { key: 'bbs_post_pay_vip_1_discount', label: '一级 VIP 折扣 (%)', type: 'number', default: 80, min: 0, max: 100, dependsOn: { key: 'bbs_post_pay_vip_price_s', value: true } },
+ { key: 'bbs_post_pay_vip_2_discount', label: '二级 VIP 折扣 (%)', type: 'number', default: 50, min: 0, max: 100, dependsOn: { key: 'bbs_post_pay_vip_price_s', value: true } },
+ { key: 'bbs_post_pay_price_limit', label: '最高定价', type: 'number', default: 1000, min: 1, dependsOn: { key: 'bbs_post_pay_hide_type_s', value: true } },
+ ],
+ },
+ {
+ key: 'plate_follow',
+ title: '版块付费关注',
+ icon: 'fas fa-heart',
+ fields: [
+ { key: 'bbs_plate_follow_pay_s', label: '启用付费关注', type: 'switch', default: false },
+ { key: 'bbs_plate_follow_pay_price', label: '关注价格', type: 'number', default: 10, min: 0, dependsOn: { key: 'bbs_plate_follow_pay_s', value: true } },
+ { key: 'bbs_plate_follow_pay_type', label: '支付方式', type: 'select', default: 'money', options: [{ label: '现金', value: 'money' }, { label: '积分', value: 'points' }], dependsOn: { key: 'bbs_plate_follow_pay_s', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/post-page.js b/assets/admin/src/schema/sections/forum/post-page.js
new file mode 100644
index 00000000..3cd1aa2a
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/post-page.js
@@ -0,0 +1,60 @@
+export const postPageSchema = {
+ key: 'forum.post_page',
+ title: '帖子页面',
+ icon: 'fas fa-file-alt',
+ description: '帖子页面包屑、评分、高度限制、封面比例和视频静音配置。',
+ defaults: {
+ bbs_breadcrumbs_s: true,
+ bbs_breadcrumbs_home: true,
+ bbs_breadcrumbs_bbs_home: true,
+ bbs_breadcrumbs_bbs_home_name: '论坛',
+ bbs_breadcrumbs_plate_cat: true,
+ bbs_score_extra_max: 5,
+ bbs_score_deduct_max: 3,
+ bbs_posts_maxheight_s: false,
+ bbs_posts_maxheight: 800,
+ bbs_posts_cover_ratio: '16:9',
+ bbs_posts_cover_video_mute: true,
+ },
+ groups: [
+ {
+ key: 'breadcrumbs',
+ title: '面包屑',
+ icon: 'fas fa-angle-double-right',
+ fields: [
+ { key: 'bbs_breadcrumbs_s', label: '显示面包屑', type: 'switch', default: true },
+ { key: 'bbs_breadcrumbs_home', label: '显示首页', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } },
+ { key: 'bbs_breadcrumbs_bbs_home', label: '显示论坛首页', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } },
+ { key: 'bbs_breadcrumbs_bbs_home_name', label: '论坛首页名称', type: 'input', default: '论坛', dependsOn: { key: 'bbs_breadcrumbs_bbs_home', value: true } },
+ { key: 'bbs_breadcrumbs_plate_cat', label: '显示分区', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } },
+ ],
+ },
+ {
+ key: 'score',
+ title: '评分设置',
+ icon: 'fas fa-star',
+ fields: [
+ { key: 'bbs_score_extra_max', label: '加分上限', type: 'number', default: 5, min: 1, max: 50 },
+ { key: 'bbs_score_deduct_max', label: '扣分上限', type: 'number', default: 3, min: 1, max: 50 },
+ ],
+ },
+ {
+ key: 'display',
+ title: '内容显示',
+ icon: 'fas fa-expand-arrows-alt',
+ fields: [
+ { key: 'bbs_posts_maxheight_s', label: '内容高度限制', type: 'switch', default: false },
+ { key: 'bbs_posts_maxheight', label: '最大高度 (px)', type: 'number', default: 800, min: 300, max: 3000, dependsOn: { key: 'bbs_posts_maxheight_s', value: true } },
+ ],
+ },
+ {
+ key: 'cover',
+ title: '封面设置',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'bbs_posts_cover_ratio', label: '封面比例', type: 'select', default: '16:9', options: [{ label: '16:9', value: '16:9' }, { label: '4:3', value: '4:3' }, { label: '1:1', value: '1:1' }, { label: '3:4', value: '3:4' }] },
+ { key: 'bbs_posts_cover_video_mute', label: '视频封面静音', type: 'switch', default: true },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/reply.js b/assets/admin/src/schema/sections/forum/reply.js
new file mode 100644
index 00000000..1f21d00b
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/reply.js
@@ -0,0 +1,44 @@
+export const replySchema = {
+ key: 'forum.reply',
+ title: '回复评论',
+ icon: 'fas fa-reply-all',
+ description: '回复翻页、工具栏功能、表情、代码和快捷回复配置。',
+ defaults: {
+ bbs_reply_paginate_type: 'ajax',
+ bbs_reply_paginate_ias_s: false,
+ bbs_reply_paginate_ias_max: 3,
+ bbs_comment_smilie: true,
+ bbs_comment_code: true,
+ bbs_comment_img: true,
+ bbs_comment_upload_img: false,
+ bbs_comment_quick_s: true,
+ bbs_comment_quick_often: '不错,学习了\n感谢分享\n期待更新',
+ bbs_comment_placeholder: '写下你的回复…',
+ },
+ groups: [
+ {
+ key: 'paginate',
+ title: '翻页',
+ icon: 'fas fa-chevron-circle-right',
+ fields: [
+ { key: 'bbs_reply_paginate_type', label: '翻页方式', type: 'select', default: 'ajax', options: [{ label: 'AJAX 加载', value: 'ajax' }, { label: '传统翻页', value: 'page' }] },
+ { key: 'bbs_reply_paginate_ias_s', label: '无限滚动', type: 'switch', default: false },
+ { key: 'bbs_reply_paginate_ias_max', label: '自动加载页数', type: 'number', default: 3, min: 0, max: 10, dependsOn: { key: 'bbs_reply_paginate_ias_s', value: true } },
+ ],
+ },
+ {
+ key: 'toolbar',
+ title: '回复工具栏',
+ icon: 'fas fa-toolbox',
+ fields: [
+ { key: 'bbs_comment_smilie', label: '表情', type: 'switch', default: true },
+ { key: 'bbs_comment_code', label: '代码', type: 'switch', default: true },
+ { key: 'bbs_comment_img', label: '插图', type: 'switch', default: true },
+ { key: 'bbs_comment_upload_img', label: '上传图片', type: 'switch', default: false },
+ { key: 'bbs_comment_quick_s', label: '快捷回复', type: 'switch', default: true },
+ { key: 'bbs_comment_quick_often', label: '预设短语', type: 'textarea', default: '不错,学习了\n感谢分享\n期待更新', help: '每行一条', dependsOn: { key: 'bbs_comment_quick_s', value: true } },
+ { key: 'bbs_comment_placeholder', label: '回复占位符', type: 'input', default: '写下你的回复…' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/forum/upload.js b/assets/admin/src/schema/sections/forum/upload.js
new file mode 100644
index 00000000..df879eaa
--- /dev/null
+++ b/assets/admin/src/schema/sections/forum/upload.js
@@ -0,0 +1,27 @@
+export const uploadSchema = {
+ key: 'forum.upload',
+ title: '上传权限',
+ icon: 'fas fa-cloud-upload-alt',
+ description: '论坛帖子图片、视频和文件的上传大小与格式限制。',
+ defaults: {
+ bbs_upload_img_size: 2048,
+ bbs_image_upload_multiple: 9,
+ bbs_upload_video_size: 20480,
+ bbs_upload_file_size: 10240,
+ bbs_upload_file_mimes: 'jpg,jpeg,png,gif,mp4,zip,rar,pdf',
+ },
+ groups: [
+ {
+ key: 'limits',
+ title: '上传限制',
+ icon: 'fas fa-weight-hanging',
+ fields: [
+ { key: 'bbs_upload_img_size', label: '图片大小 (KB)', type: 'number', default: 2048, min: 100 },
+ { key: 'bbs_image_upload_multiple', label: '批量上传数', type: 'number', default: 9, min: 1, max: 20 },
+ { key: 'bbs_upload_video_size', label: '视频大小 (KB)', type: 'number', default: 20480, min: 1024 },
+ { key: 'bbs_upload_file_size', label: '文件大小 (KB)', type: 'number', default: 10240, min: 100 },
+ { key: 'bbs_upload_file_mimes', label: '允许格式', type: 'input', default: 'jpg,jpeg,png,gif,mp4,zip,rar,pdf', help: '逗号分隔' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/code-highlight.js b/assets/admin/src/schema/sections/global/code-highlight.js
new file mode 100644
index 00000000..1c98f607
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/code-highlight.js
@@ -0,0 +1,68 @@
+export const codeHighlightSchema = {
+ key: 'global.code-highlight',
+ title: '代码高亮',
+ icon: 'fas fa-code',
+ description: '代码块语法高亮主题、行号、最大高度等配置。',
+ defaults: {
+ highlight_kg: true,
+ highlight_hh: false,
+ highlight_btn: true,
+ highlight_maxheight: 400,
+ highlight_zt: 'enlighter',
+ highlight_dark_zt: 'monokai',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '代码高亮设置',
+ icon: 'fas fa-highlighter',
+ fields: [
+ { key: 'highlight_kg', label: '代码高亮开关', type: 'switch', default: true },
+ { key: 'highlight_hh', label: '显示行号', type: 'switch', default: false },
+ { key: 'highlight_btn', label: '显示扩展按钮', type: 'switch', default: true, help: '控制原始代码 / 信息 / 新窗口按钮的显示' },
+ { key: 'highlight_maxheight', label: '最大高度(px,0=不限)', type: 'number', default: 400, min: 0, max: 2000, help: '代码块最大高度,超出滚动显示。0 表示不限制' },
+ ],
+ },
+ {
+ key: 'theme',
+ title: '主题设置',
+ icon: 'fas fa-palette',
+ fields: [
+ {
+ key: 'highlight_zt',
+ label: '日间默认主题',
+ type: 'select',
+ default: 'enlighter',
+ options: [
+ { label: 'Enlighter', value: 'enlighter' },
+ { label: 'Bootstrap 4', value: 'bootstrap4' },
+ { label: 'Classic', value: 'classic' },
+ { label: 'Beyond', value: 'beyond' },
+ { label: 'Mowtwo', value: 'mowtwo' },
+ { label: 'Eclipse', value: 'eclipse' },
+ { label: 'Droide', value: 'droide' },
+ { label: 'Minimal', value: 'minimal' },
+ { label: 'Rowhammer', value: 'rowhammer' },
+ { label: 'Godzilla', value: 'godzilla' },
+ { label: 'Dracula', value: 'dracula' },
+ { label: 'Atomic', value: 'atomic' },
+ { label: 'Monokai', value: 'monokai' },
+ ],
+ },
+ {
+ key: 'highlight_dark_zt',
+ label: '暗色默认主题',
+ type: 'select',
+ default: 'monokai',
+ options: [
+ { label: 'Dracula', value: 'dracula' },
+ { label: 'Atomic', value: 'atomic' },
+ { label: 'Monokai', value: 'monokai' },
+ { label: 'Enlighter', value: 'enlighter' },
+ { label: 'Bootstrap 4', value: 'bootstrap4' },
+ ],
+ },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/common.js b/assets/admin/src/schema/sections/global/common.js
new file mode 100644
index 00000000..74cd6123
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/common.js
@@ -0,0 +1,99 @@
+export const commonSchema = {
+ key: 'global.common',
+ title: '常用功能',
+ icon: 'fas fa-cog',
+ description: '管理站点常用开关、懒加载、头像源与备案信息等基础配置。',
+ defaults: {
+ comment_enabled: true,
+ lazyload_enabled: true,
+ lazyload_animation: 'blur',
+ lazyload_placeholder: '',
+ lazyload_blur: true,
+ lazyload_avatar: true,
+ lazyload_content: false,
+ lazyload_comment: true,
+ lazyload_private: true,
+ lazyload_slider: true,
+ lazyload_cover: true,
+ lazyload_other: true,
+ lazyload_widget: true,
+ avatar_source: 'cn.cravatar.com',
+ icp_filing: '',
+ footer_text: '',
+ },
+ groups: [
+ {
+ key: 'comment',
+ title: '评论功能',
+ icon: 'fas fa-comment',
+ fields: [
+ { key: 'comment_enabled', label: '开启评论', type: 'switch', default: true, help: '全局评论开关,关闭后所有文章不再显示评论区' },
+ ],
+ },
+ {
+ key: 'performance',
+ title: '性能优化',
+ icon: 'fas fa-tachometer-alt',
+ fields: [
+ { key: 'lazyload_enabled', label: '图片懒加载', type: 'switch', default: true, help: '开启后页面中的图片将在滚动到可见区域时才加载' },
+ {
+ key: 'lazyload_animation',
+ label: '懒加载动画',
+ type: 'select',
+ default: 'blur',
+ options: [
+ { label: '模糊渐显', value: 'blur' },
+ { label: '淡入', value: 'fade' },
+ { label: '缩放', value: 'scale' },
+ ],
+ dependsOn: { key: 'lazyload_enabled', value: true },
+ },
+ { key: 'lazyload_placeholder', label: '预载图', type: 'media', default: '', dependsOn: { key: 'lazyload_enabled', value: true }, help: '自定义占位图,留空使用内置默认' },
+ { key: 'lazyload_blur', label: '模糊预览', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true }, help: '加载前显示低分辨率模糊预览' },
+ ],
+ },
+ {
+ key: 'lazyload-scope',
+ title: '懒加载范围',
+ icon: 'fas fa-layer-group',
+ fields: [
+ { key: 'lazyload_avatar', label: '头像懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } },
+ { key: 'lazyload_content', label: '文章内容图片懒加载', type: 'switch', default: false, dependsOn: { key: 'lazyload_enabled', value: true } },
+ { key: 'lazyload_comment', label: '评论内容图片懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } },
+ { key: 'lazyload_private', label: '私信聊天图片懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } },
+ { key: 'lazyload_slider', label: '幻灯片图片懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } },
+ { key: 'lazyload_cover', label: '封面图片懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } },
+ { key: 'lazyload_other', label: '其它图片懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } },
+ { key: 'lazyload_widget', label: '小工具图片懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'avatar',
+ title: '头像服务',
+ icon: 'fas fa-user-circle',
+ fields: [
+ {
+ key: 'avatar_source',
+ label: '头像源',
+ type: 'select',
+ default: 'cn.cravatar.com',
+ options: [
+ { label: 'Cravatar (推荐)', value: 'cn.cravatar.com' },
+ { label: 'Gravatar 中国', value: 'cn.gravatar.com' },
+ { label: 'Gravatar 国际', value: 'gravatar.com' },
+ ],
+ help: 'Cravatar 是国内最快的头像服务,推荐使用',
+ },
+ ],
+ },
+ {
+ key: 'filing',
+ title: '备案信息',
+ icon: 'fas fa-shield-alt',
+ fields: [
+ { key: 'icp_filing', label: 'ICP 备案号', type: 'input', placeholder: '例如:京ICP备12345678号', help: '显示在页脚的备案信息' },
+ { key: 'footer_text', label: '页脚附加文字', type: 'input', placeholder: '自定义页脚文字', help: '显示在版权信息下方' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/custom-code.js b/assets/admin/src/schema/sections/global/custom-code.js
new file mode 100644
index 00000000..86b28720
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/custom-code.js
@@ -0,0 +1,39 @@
+export const customCodeSchema = {
+ key: 'global.custom-code',
+ title: '自定义代码',
+ icon: 'fas fa-code',
+ description: '自定义 CSS、JS 代码注入。',
+ defaults: {
+ custom_css: '',
+ head_code: '',
+ foot_code: '',
+ trackcode: '',
+ },
+ groups: [
+ {
+ key: 'css',
+ title: '自定义 CSS',
+ icon: 'fab fa-css3-alt',
+ fields: [
+ { key: 'custom_css', label: '自定义 CSS', type: 'code', language: 'css', placeholder: '/* 在此输入自定义 CSS */', rows: 10, help: '将在页面 中加载' },
+ ],
+ },
+ {
+ key: 'code',
+ title: '头部 & 底部代码',
+ icon: 'fas fa-code',
+ fields: [
+ { key: 'head_code', label: '头部 HTML 代码', type: 'code', placeholder: '', rows: 6, help: '在 中输出,支持 meta/link/style/script 等任意 HTML' },
+ { key: 'foot_code', label: '底部 HTML 代码', type: 'code', placeholder: '', rows: 6, help: '在页面底部 前输出,支持任意 HTML' },
+ ],
+ },
+ {
+ key: 'stats',
+ title: '统计代码',
+ icon: 'fas fa-chart-bar',
+ fields: [
+ { key: 'trackcode', label: '统计代码', type: 'code', placeholder: '', rows: 6, help: '百度统计、Google Analytics、CNZZ、51la 等统计代码,自动在 head 中输出' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/email.js b/assets/admin/src/schema/sections/global/email.js
new file mode 100644
index 00000000..99df372e
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/email.js
@@ -0,0 +1,123 @@
+export const emailSchema = {
+ key: 'global.email',
+ title: '邮件配置',
+ icon: 'fas fa-envelope',
+ description: 'SMTP 发信配置、邮件事件开关与模板自定义。',
+ defaults: {
+ email_footer_text: '',
+ email_site_branding: true,
+ email_template_mode: 'plain',
+ email_private_receive_limit: 'all',
+ email_reg_verify_link_enabled: true,
+ email_reg_verify_code_enabled: true,
+ email_reg_success_enabled: true,
+ email_password_reset_enabled: true,
+ email_change_code_enabled: true,
+ email_change_notice_enabled: true,
+ },
+ groups: [
+ {
+ key: 'global',
+ title: '全局设置',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'email_footer_text', label: '邮件页脚文字', type: 'textarea', rows: 2, placeholder: '留空则使用站点名称', help: '附加在每封邮件底部的文字' },
+ { key: 'email_site_branding', label: '显示站点品牌', type: 'switch', default: true, help: '在邮件开头显示「[站点名]」前缀' },
+ { key: 'email_template_mode', label: '邮件格式', type: 'radio', default: 'plain', options: [
+ { label: '纯文本', value: 'plain' },
+ { label: 'HTML', value: 'html' },
+ ]},
+ {
+ key: 'email_private_receive_limit',
+ label: '私信邮件接收组',
+ type: 'select',
+ default: 'all',
+ options: [
+ { label: '所有用户', value: 'all' },
+ { label: 'VIP 及以上', value: 'vip' },
+ { label: 'VIP2 及以上', value: 'vip2' },
+ { label: '仅管理员', value: 'admin' },
+ ],
+ help: '限制哪些用户组可以收到私信邮件通知',
+ },
+ ],
+ },
+ {
+ key: 'branding',
+ title: '邮件品牌',
+ icon: 'fas fa-signature',
+ fields: [
+ { key: 'mail_showname', label: '发件人名称', type: 'input', default: '', help: '覆盖系统默认发件人名称' },
+ { key: 'mail_title_prefix', label: '主题前缀', type: 'input', default: '', help: '自动添加在所有邮件主题前面,如【站点名】' },
+ { key: 'mail_description', label: '邮件底部说明', type: 'textarea', rows: 3, default: '', help: '附加在邮件页脚文字之后,留空不显示' },
+ { key: 'mail_more_content', label: '邮件附加内容', type: 'textarea', rows: 4, default: '', help: '附加在邮件内容末尾的补充说明' },
+ ],
+ },
+ {
+ key: 'reg_verify_link',
+ title: '注册验证链接',
+ icon: 'fas fa-link',
+ fields: [
+ { key: 'email_reg_verify_link_enabled', label: '启用', type: 'switch', default: true, help: '注册后发送验证链接邮件' },
+ { key: 'email_reg_verify_link_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '验证你的邮箱地址' },
+ { key: 'email_reg_verify_link_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {url}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{url} 验证链接' },
+ { key: 'email_reg_verify_link_html', label: 'HTML 格式', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'reg_verify_code',
+ title: '注册验证码',
+ icon: 'fas fa-keyboard',
+ fields: [
+ { key: 'email_reg_verify_code_enabled', label: '启用', type: 'switch', default: true, help: '注册后发送 6 位验证码邮件' },
+ { key: 'email_reg_verify_code_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '邮箱验证码' },
+ { key: 'email_reg_verify_code_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {code}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{code} 验证码' },
+ { key: 'email_reg_verify_code_html', label: 'HTML 格式', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'reg_success',
+ title: '注册成功通知',
+ icon: 'fas fa-check-circle',
+ fields: [
+ { key: 'email_reg_success_enabled', label: '启用', type: 'switch', default: true, help: '邮箱验证成功后发送通知邮件' },
+ { key: 'email_reg_success_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '邮箱验证成功' },
+ { key: 'email_reg_success_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name}', help: '可用占位符:{username} 用户名、{site_name} 站点名' },
+ { key: 'email_reg_success_html', label: 'HTML 格式', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'password_reset',
+ title: '密码重置',
+ icon: 'fas fa-unlock-alt',
+ fields: [
+ { key: 'email_password_reset_enabled', label: '启用', type: 'switch', default: true, help: '发送密码重置链接邮件' },
+ { key: 'email_password_reset_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '重置密码' },
+ { key: 'email_password_reset_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {url}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{url} 重置链接' },
+ { key: 'email_password_reset_html', label: 'HTML 格式', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'change_code',
+ title: '邮箱变更验证码',
+ icon: 'fas fa-shield-alt',
+ fields: [
+ { key: 'email_change_code_enabled', label: '启用', type: 'switch', default: true, help: '变更邮箱时发送验证码邮件' },
+ { key: 'email_change_code_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '邮箱验证码' },
+ { key: 'email_change_code_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {code}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{code} 验证码' },
+ { key: 'email_change_code_html', label: 'HTML 格式', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'change_notice',
+ title: '邮箱变更通知',
+ icon: 'fas fa-bell',
+ fields: [
+ { key: 'email_change_notice_enabled', label: '启用', type: 'switch', default: true, help: '邮箱变更成功后向旧邮箱发送通知' },
+ { key: 'email_change_notice_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '账号邮箱已变更' },
+ { key: 'email_change_notice_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {new_email}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{new_email} 新邮箱' },
+ { key: 'email_change_notice_html', label: 'HTML 格式', type: 'switch', default: false },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/floating.js b/assets/admin/src/schema/sections/global/floating.js
new file mode 100644
index 00000000..d9df88c5
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/floating.js
@@ -0,0 +1,8 @@
+export const floatingSchema = {
+ key: 'global.floating',
+ title: '右侧悬浮按钮',
+ icon: 'fas fa-ellipsis-v',
+ description: '页面右侧浮动按钮的显示与配置。',
+ defaults: {},
+ groups: [],
+}
diff --git a/assets/admin/src/schema/sections/global/i18n.js b/assets/admin/src/schema/sections/global/i18n.js
new file mode 100644
index 00000000..ef80bbb4
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/i18n.js
@@ -0,0 +1,85 @@
+export const i18nSchema = {
+ key: 'global.i18n',
+ title: '多语言翻译',
+ icon: 'fas fa-language',
+ description: '配置站点语言、语言切换器和翻译管理。',
+ defaults: {
+ i18n_enabled: false,
+ i18n_default_lang: 'zh-CN',
+ i18n_switcher_position: 'footer',
+ i18n_auto_detect: true,
+ i18n_available_langs: 'zh-CN|简体中文\nen|English\nzh-TW|繁體中文\nja|日本語\nko|한국어\nfr|Français\nde|Deutsch\nes|Español\nru|Русский',
+ i18n_nomenclature: '',
+ i18n_url_strategy: 'query',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '基础配置',
+ icon: 'fas fa-globe',
+ fields: [
+ { key: 'i18n_enabled', label: '启用多语言', type: 'switch', default: false, help: '开启后站点将支持多语言切换' },
+ {
+ key: 'i18n_default_lang',
+ label: '默认语言',
+ type: 'select',
+ default: 'zh-CN',
+ options: [
+ { label: '简体中文', value: 'zh-CN' },
+ { label: 'English', value: 'en' },
+ { label: '繁體中文', value: 'zh-TW' },
+ { label: '日本語', value: 'ja' },
+ { label: '한국어', value: 'ko' },
+ { label: 'Français', value: 'fr' },
+ { label: 'Deutsch', value: 'de' },
+ { label: 'Español', value: 'es' },
+ { label: 'Русский', value: 'ru' },
+ ],
+ dependsOn: { key: 'i18n_enabled', value: true },
+ },
+ { key: 'i18n_auto_detect', label: '自动检测浏览器语言', type: 'switch', default: true, dependsOn: { key: 'i18n_enabled', value: true }, help: '基于浏览器 Accept-Language 自动切换(AKSRT 方案,区别于 zibll IP 定位方案)' },
+ ],
+ },
+ {
+ key: 'languages',
+ title: '语言管理',
+ icon: 'fas fa-language',
+ fields: [
+ { key: 'i18n_available_langs', label: '可用语言列表', type: 'textarea', default: 'zh-CN|简体中文\nen|English\nzh-TW|繁體中文\nja|日本語\nko|한국어\nfr|Français\nde|Deutsch\nes|Español\nru|Русский', dependsOn: { key: 'i18n_enabled', value: true }, help: '每行一个语言:语言代码|显示名称(9 种内置语言)' },
+ { key: 'i18n_nomenclature', label: '自定义术语', type: 'code', default: '', dependsOn: { key: 'i18n_enabled', value: true }, help: 'JSON 格式自定义翻译术语(key => 翻译),对齐 zibll nomenclature' },
+ ],
+ },
+ {
+ key: 'display',
+ title: '展示设置',
+ icon: 'fas fa-eye',
+ fields: [
+ {
+ key: 'i18n_switcher_position',
+ label: '语言切换器位置',
+ type: 'radio',
+ default: 'footer',
+ options: [
+ { label: '导航栏', value: 'header' },
+ { label: '页脚', value: 'footer' },
+ { label: '导航栏+页脚', value: 'both' },
+ { label: '隐藏', value: 'none' },
+ ],
+ dependsOn: { key: 'i18n_enabled', value: true },
+ },
+ {
+ key: 'i18n_url_strategy',
+ label: 'URL 策略',
+ type: 'radio',
+ default: 'query',
+ options: [
+ { label: '查询参数 (?lang=en)', value: 'query' },
+ { label: '路径前缀 (/en/page)', value: 'path' },
+ { label: '子域名 (en.site.com)', value: 'subdomain' },
+ ],
+ dependsOn: { key: 'i18n_enabled', value: true },
+ },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/index.js b/assets/admin/src/schema/sections/global/index.js
new file mode 100644
index 00000000..5a788423
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/index.js
@@ -0,0 +1,32 @@
+/**
+ * Global & Functions domain schemas.
+ * Each key corresponds to a sub-page under the "全局&功能" section.
+ */
+
+import { logoSchema } from './logo.js'
+import { commonSchema } from './common.js'
+import { layoutSchema } from './layout.js'
+import { searchSchema } from './search.js'
+import { emailSchema } from './email.js'
+import { tabbarSchema } from './tabbar.js'
+import { floatingSchema } from './floating.js'
+import { i18nSchema } from './i18n.js'
+import { customCodeSchema } from './custom-code.js'
+import { seoSchema } from './seo.js'
+import { codeHighlightSchema } from './code-highlight.js'
+import { systemNoticeSchema } from './system-notice.js'
+
+export const globalSchemas = {
+ logo: logoSchema,
+ common: commonSchema,
+ layout: layoutSchema,
+ search: searchSchema,
+ email: emailSchema,
+ tabbar: tabbarSchema,
+ floating: floatingSchema,
+ i18n: i18nSchema,
+ 'custom-code': customCodeSchema,
+ seo: seoSchema,
+ 'code-highlight': codeHighlightSchema,
+ 'system-notice': systemNoticeSchema,
+}
diff --git a/assets/admin/src/schema/sections/global/layout.js b/assets/admin/src/schema/sections/global/layout.js
new file mode 100644
index 00000000..2fab1a1c
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/layout.js
@@ -0,0 +1,160 @@
+export const layoutSchema = {
+ key: 'global.layout',
+ title: '显示 & 布局',
+ icon: 'fas fa-desktop',
+ description: '全局布局风格、颜色方案、字体设置。',
+ defaults: {
+ theme_mode: 'time-auto',
+ theme_mode_button: ['pc_nav', 'm_menu'],
+ primary_color: '#1890ff',
+ theme_skin: 'f04494',
+ dark_primary_color: '',
+ border_radius: 8,
+ theme_main_radius: 12,
+ container_max_width: 1200,
+ sidebar_enabled: true,
+ sidebar_position: 'right',
+ sidebar_width: 300,
+ sidebar_home_enabled: true,
+ sidebar_category_enabled: true,
+ sidebar_tag_enabled: true,
+ sidebar_single_enabled: true,
+ sidebar_page_enabled: true,
+ sidebar_search_enabled: true,
+ font_family: 'system',
+ qj_loading: false,
+ qj_dh_xs: 'no1',
+ },
+ groups: [
+ {
+ key: 'loading-animation',
+ title: '加载动画',
+ icon: 'fas fa-spinner',
+ fields: [
+ { key: 'qj_loading', label: '全站加载动画', type: 'switch', default: false, help: '开启后页面加载时显示全屏加载动画覆盖层' },
+ {
+ key: 'qj_dh_xs',
+ label: '加载动画样式',
+ type: 'select',
+ default: 'no1',
+ dependsOn: { key: 'qj_loading', value: true },
+ options: [
+ { label: '样式 1', value: 'no1' },
+ { label: '样式 2', value: 'no2' },
+ { label: '样式 3', value: 'no3' },
+ { label: '样式 4', value: 'no4' },
+ { label: '样式 5', value: 'no5' },
+ { label: '样式 6', value: 'no6' },
+ { label: '样式 7', value: 'no7' },
+ { label: '样式 8', value: 'no8' },
+ { label: '样式 9', value: 'no9' },
+ { label: '样式 10', value: 'no10' },
+ ],
+ help: '10 种加载动画样式可选',
+ },
+ ],
+ },
+ {
+ key: 'color',
+ title: '色彩方案',
+ icon: 'fas fa-palette',
+ fields: [
+ {
+ key: 'theme_mode',
+ label: '默认主题模式',
+ type: 'radio',
+ default: 'time-auto',
+ options: [
+ { label: '浅色', value: 'white-theme' },
+ { label: '深色', value: 'dark-theme' },
+ { label: '跟随系统', value: 'time-auto' },
+ ],
+ },
+ {
+ key: 'theme_mode_button',
+ label: '主题切换按钮位置',
+ type: 'checkbox',
+ default: ['pc_nav', 'm_menu'],
+ options: [
+ { label: 'PC 顶部导航', value: 'pc_nav' },
+ { label: '移动端菜单', value: 'm_menu' },
+ ],
+ help: '控制主题切换按钮在哪些位置显示',
+ },
+ { key: 'primary_color', label: '主题色', type: 'color', default: '#f04494', help: '按钮、链接和强调元素的主色调' },
+ {
+ key: 'theme_skin',
+ label: '预设主题色',
+ type: 'palette',
+ default: 'f04494',
+ options: [
+ { label: '玫红', value: 'ff1856' },
+ { label: '粉色', value: 'f04494' },
+ { label: '紫色', value: 'ae53f3' },
+ { label: '靛蓝', value: '627bf5' },
+ { label: '天蓝', value: '00a2e3' },
+ { label: '青绿', value: '16b597' },
+ { label: '翠绿', value: '36af18' },
+ { label: '黄绿', value: '8fb107' },
+ { label: '棕金', value: 'b18c07' },
+ { label: '橘橙', value: 'e06711' },
+ { label: '朱红', value: 'f74735' },
+ ],
+ help: '选择预设主题色,将覆盖上方自定义主题色',
+ },
+ { key: 'dark_primary_color', label: '暗色模式主题色', type: 'color', default: '', help: '暗色模式下独立的主题色,留空则跟随浅色模式' },
+ { key: 'border_radius', label: '全局圆角 (px)', type: 'slider', min: 0, max: 20, step: 1, default: 8 },
+ { key: 'theme_main_radius', label: '卡片圆角 (px)', type: 'number', min: 0, max: 30, default: 12, help: '独立卡片圆角,留空则使用全局圆角 +4' },
+ ],
+ },
+ {
+ key: 'layout',
+ title: '布局设置',
+ icon: 'fas fa-columns',
+ fields: [
+ { key: 'container_max_width', label: '内容最大宽度 (px)', type: 'select', default: 1200, options: [
+ { label: '1200px', value: 1200 },
+ { label: '1320px', value: 1320 },
+ { label: '1440px', value: 1440 },
+ { label: '1600px', value: 1600 },
+ { label: '1800px', value: 1800 },
+ { label: '2000px', value: 2000 },
+ { label: '2200px', value: 2200 },
+ { label: '全宽', value: 0 },
+ ]},
+ { key: 'sidebar_enabled', label: '开启侧边栏', type: 'switch', default: true },
+ { key: 'sidebar_position', label: '侧边栏位置', type: 'radio', default: 'right', dependsOn: { key: 'sidebar_enabled', value: true }, options: [
+ { label: '左侧', value: 'left' },
+ { label: '右侧', value: 'right' },
+ ]},
+ { key: 'sidebar_width', label: '侧边栏宽度 (px)', type: 'number', default: 300, min: 200, max: 400, step: 10, dependsOn: { key: 'sidebar_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'sidebar-scope',
+ title: '侧边栏显示范围',
+ icon: 'fas fa-th-list',
+ fields: [
+ { key: 'sidebar_home_enabled', label: '首页侧边栏', type: 'switch', default: true, dependsOn: { key: 'sidebar_enabled', value: true } },
+ { key: 'sidebar_category_enabled', label: '分类页侧边栏', type: 'switch', default: true, dependsOn: { key: 'sidebar_enabled', value: true } },
+ { key: 'sidebar_tag_enabled', label: '标签页侧边栏', type: 'switch', default: true, dependsOn: { key: 'sidebar_enabled', value: true } },
+ { key: 'sidebar_single_enabled', label: '文章页侧边栏', type: 'switch', default: true, dependsOn: { key: 'sidebar_enabled', value: true } },
+ { key: 'sidebar_page_enabled', label: '页面侧边栏', type: 'switch', default: true, dependsOn: { key: 'sidebar_enabled', value: true } },
+ { key: 'sidebar_search_enabled', label: '搜索页侧边栏', type: 'switch', default: true, dependsOn: { key: 'sidebar_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'typography',
+ title: '字体设置',
+ icon: 'fas fa-font',
+ fields: [
+ { key: 'font_family', label: '正文字体', type: 'select', default: 'system', options: [
+ { label: '系统默认', value: 'system' },
+ { label: '思源黑体', value: 'noto-sans-sc' },
+ { label: '思源宋体', value: 'noto-serif-sc' },
+ { label: '自定义', value: 'custom' },
+ ]},
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/logo.js b/assets/admin/src/schema/sections/global/logo.js
new file mode 100644
index 00000000..0c0aab87
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/logo.js
@@ -0,0 +1,36 @@
+export const logoSchema = {
+ key: 'global.logo',
+ title: 'LOGO & 品牌',
+ icon: 'fas fa-image',
+ description: '管理站点 Logo、站点图标和品牌标识。',
+ defaults: {
+ site_logo: '',
+ site_logo_dark: '',
+ site_favicon: '',
+ site_icon: '',
+ logo_width: 150,
+ logo_text: '',
+ },
+ groups: [
+ {
+ key: 'logo',
+ title: 'Logo 设置',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'site_logo', label: '站点 Logo', type: 'media', help: '建议尺寸 300x80,支持 PNG/SVG 透明背景' },
+ { key: 'site_logo_dark', label: '暗色模式 Logo', type: 'media', help: '暗色模式下显示的 Logo,留空则使用默认 Logo' },
+ { key: 'logo_width', label: 'Logo 宽度 (px)', type: 'number', min: 50, max: 400, step: 10, help: '导航栏中 Logo 显示宽度' },
+ { key: 'logo_text', label: 'Logo 文字', type: 'input', placeholder: '当无 Logo 图片时显示的文字', help: '同时作为首页标题和 alt 文本使用' },
+ ],
+ },
+ {
+ key: 'favicon',
+ title: '站点图标',
+ icon: 'fas fa-fingerprint',
+ fields: [
+ { key: 'site_favicon', label: '浏览器图标 (Favicon)', type: 'media', help: '建议 32x32 或 16x16 的 ICO/PNG 文件' },
+ { key: 'site_icon', label: '移动端图标', type: 'media', help: '建议 192x192 的 PNG,用于移动端添加到桌面' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/search.js b/assets/admin/src/schema/sections/global/search.js
new file mode 100644
index 00000000..19650c0b
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/search.js
@@ -0,0 +1,135 @@
+export const searchSchema = {
+ key: 'global.search',
+ title: '搜索功能',
+ icon: 'fas fa-search',
+ description: '搜索模式、内容类型切换、搜索建议、热门搜索配置。',
+ defaults: {
+ search_mode: 'default',
+ search_type: 'default',
+ search_history_enabled: true,
+ hot_search_enabled: true,
+ hot_search_keywords: '',
+ search_placeholder: '开启精彩搜索',
+ search_suggestions_enabled: false,
+ search_posts: false,
+ search_no_page: false,
+ search_exclude_cats: '',
+ search_cat: false,
+ search_cat_in: '',
+ search_more_cat_obj: true,
+ search_custom_key_s: false,
+ search_custom_key: 'title,content',
+ search_facets: false,
+ search_facets_order: 'category\ntag',
+ search_type_post: true,
+ search_type_user: false,
+ search_type_category: true,
+ search_type_tag: true,
+ search_type_product: false,
+ search_type_topic: false,
+ search_default_type: 'post',
+ search_ms_enabled: false,
+ search_ms_api_url: '',
+ search_ms_api_key: '',
+ search_ms_index_prefix: 'aksrt',
+ search_ms_synonyms: '',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '搜索基础',
+ icon: 'fas fa-search',
+ fields: [
+ {
+ key: 'search_mode',
+ label: '搜索模式',
+ type: 'radio',
+ default: 'default',
+ options: [
+ { label: '默认搜索', value: 'default' },
+ { label: 'AJAX 实时搜索', value: 'ajax' },
+ { label: '跳转搜索', value: 'redirect' },
+ ],
+ },
+ { key: 'search_placeholder', label: '搜索框提示文字', type: 'input', placeholder: '开启精彩搜索' },
+ ],
+ },
+ {
+ key: 'content-types',
+ title: '搜索内容类型',
+ icon: 'fas fa-layer-group',
+ fields: [
+ { key: 'search_type_post', label: '文章', type: 'switch', default: true },
+ { key: 'search_type_user', label: '用户', type: 'switch', default: false },
+ { key: 'search_type_category', label: '分类', type: 'switch', default: true },
+ { key: 'search_type_tag', label: '标签', type: 'switch', default: true },
+ { key: 'search_type_product', label: '商品', type: 'switch', default: false },
+ { key: 'search_type_topic', label: '帖子', type: 'switch', default: false },
+ {
+ key: 'search_default_type',
+ label: '默认类型',
+ type: 'select',
+ default: 'post',
+ options: [
+ { label: '文章', value: 'post' },
+ { label: '用户', value: 'user' },
+ { label: '分类', value: 'category' },
+ { label: '标签', value: 'tag' },
+ { label: '商品', value: 'product' },
+ { label: '帖子', value: 'topic' },
+ ],
+ },
+ ],
+ },
+ {
+ key: 'features',
+ title: '搜索功能',
+ icon: 'fas fa-magic',
+ fields: [
+ { key: 'search_history_enabled', label: '搜索历史', type: 'switch', default: true, help: '记录用户最近的搜索关键词' },
+ { key: 'hot_search_enabled', label: '热门搜索', type: 'switch', default: true, help: '在搜索框下方展示热门搜索词' },
+ { key: 'hot_search_keywords', label: '热门搜索关键词', type: 'textarea', placeholder: '每行一个关键词', dependsOn: { key: 'hot_search_enabled', value: true }, rows: 3, help: '手动设置热门搜索关键词,留空则自动统计' },
+ { key: 'search_suggestions_enabled', label: '搜索建议', type: 'switch', default: false, help: '输入时显示搜索建议(需要 AJAX 搜索模式)' },
+ { key: 'search_posts', label: '搜索框热门文章', type: 'switch', default: false, help: '在搜索面板中显示热门文章列表' },
+ { key: 'search_no_page', label: '排除页面', type: 'switch', default: false, help: '搜索结果中排除页面(仅显示文章)' },
+ { key: 'search_exclude_cats', label: '排除分类', type: 'textarea', placeholder: '分类 ID,逗号分隔', rows: 2, default: '', help: '搜索结果中排除的分类 ID(逗号分隔)' },
+ { key: 'search_custom_key_s', label: '自定义匹配范围', type: 'switch', default: false, help: '启用后可配置搜索匹配字段范围' },
+ { key: 'search_custom_key', label: '匹配字段', type: 'textarea', placeholder: 'title,content,excerpt,category,tag,author', rows: 2, default: 'title,content', dependsOn: { key: 'search_custom_key_s', value: true }, help: '搜索匹配字段(逗号分隔:title/content/excerpt/category/tag/author)' },
+ { key: 'search_facets', label: '搜索结果筛选', type: 'switch', default: false, help: '启用搜索结果 taxonomy 筛选面板' },
+ { key: 'search_facets_order', label: '筛选排序', type: 'textarea', rows: 3, default: 'category\ntag', dependsOn: { key: 'search_facets', value: true }, help: '筛选 fieldset 显示顺序(每行一个 taxonomy slug)' },
+ ],
+ },
+ {
+ key: 'category-search',
+ title: '分类搜索',
+ icon: 'fas fa-folder-open',
+ fields: [
+ { key: 'search_cat', label: '启用分类搜索', type: 'switch', default: false, help: '在搜索框旁显示分类选择器' },
+ { key: 'search_cat_in', label: '默认分类', type: 'input', default: '', help: '默认搜索的分类 ID(留空为全部分类)' },
+ { key: 'search_more_cat_obj', label: '显示更多分类', type: 'switch', default: true, help: '展开显示全部分类列表' },
+ ],
+ },
+ {
+ key: 'popular',
+ title: '热门搜索',
+ icon: 'fas fa-fire',
+ fields: [
+ { key: 'search_popular_title', label: '标题', type: 'input', default: '热门搜索', help: '热门搜索区域标题,留空使用默认' },
+ { key: 'search_popular_key_num', label: '显示数量', type: 'number', default: 20, min: 1, max: 50, help: '热门搜索显示条数' },
+ { key: 'search_popular_sticky', label: '置顶词(每行一个)', type: 'textarea', rows: 3, default: '', help: '置顶搜索词始终显示在最前面' },
+ ],
+ },
+ {
+ key: 'meilisearch',
+ title: 'Meilisearch 集成',
+ icon: 'fas fa-bolt',
+ fields: [
+ { key: 'search_ms_enabled', label: '启用 Meilisearch', type: 'switch', default: false, help: '启用后搜索请求将发送至 Meilisearch 服务器' },
+ { key: 'search_ms_api_url', label: 'API 地址', type: 'input', default: '', help: 'Meilisearch 服务器 URL(如 http://localhost:7700)' },
+ { key: 'search_ms_api_key', label: 'API Key', type: 'input', default: '', help: 'Meilisearch Search API Key(只读密钥)' },
+ { key: 'search_ms_index_prefix', label: '索引前缀', type: 'input', default: 'aksrt', help: '索引名称前缀(实际索引名为 {prefix}_posts)' },
+ { key: 'search_ms_synonyms', label: '同义词(JSON)', type: 'textarea', rows: 3, default: '', help: 'Meilisearch 同义词配置,JSON 格式(如 {"手机": ["phone","mobile"]})' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/seo.js b/assets/admin/src/schema/sections/global/seo.js
new file mode 100644
index 00000000..6caaede1
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/seo.js
@@ -0,0 +1,102 @@
+export const seoSchema = {
+ key: 'global.seo',
+ title: 'SEO 优化',
+ icon: 'fas fa-search',
+ description: '站点标题、描述、关键词、Sitemap 等搜索引擎优化设置。',
+ defaults: {
+ seo_enabled: true,
+ seo_ai_enabled: false,
+ seo_connector: '-',
+ seo_title: '',
+ seo_keywords: '',
+ seo_description: '',
+ ai_seo_title_prompt: '',
+ ai_seo_keywords_prompt: '',
+ ai_seo_description_prompt: '',
+ seo_baidu_push: false,
+ seo_baidu_normal: false,
+ seo_baidu_fast: false,
+ seo_baidu_token: '',
+ seo_image_optimize: true,
+ seo_image_default: '',
+ seo_image_always_default: false,
+ seo_auto_alt: true,
+ seo_go_link: true,
+ seo_go_link_nonce: true,
+ seo_go_link_post: true,
+ seo_go_link_exclude: '',
+ seo_no_category: false,
+ seo_jsonld: true,
+ seo_jsonld_single: true,
+ seo_jsonld_page: true,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: 'SEO 基础',
+ icon: 'fas fa-globe',
+ fields: [
+ { key: 'seo_enabled', label: '启用核心 SEO 优化', type: 'switch', default: true },
+ { key: 'seo_ai_enabled', label: '启用 AI SEO 生成', type: 'switch', default: false },
+ { key: 'seo_connector', label: '标题连接符', type: 'input', default: '-', placeholder: '例如:- 或 |' },
+ { key: 'seo_title', label: '站点 SEO 标题', type: 'textarea', rows: 2, default: '' },
+ { key: 'seo_keywords', label: '站点 SEO 关键词', type: 'textarea', rows: 3, default: '' },
+ { key: 'seo_description', label: '站点 SEO 描述', type: 'textarea', rows: 4, default: '' },
+ ],
+ },
+ {
+ key: 'ai-seo',
+ title: 'AI SEO',
+ icon: 'fas fa-robot',
+ fields: [
+ { key: 'ai_seo_title_prompt', label: '标题提示词', type: 'textarea', rows: 3, default: '' },
+ { key: 'ai_seo_keywords_prompt', label: '关键词提示词', type: 'textarea', rows: 3, default: '' },
+ { key: 'ai_seo_description_prompt', label: '描述提示词', type: 'textarea', rows: 3, default: '' },
+ ],
+ },
+ {
+ key: 'baidu',
+ title: '百度推送',
+ icon: 'fas fa-paper-plane',
+ fields: [
+ { key: 'seo_baidu_push', label: '启用百度自动推送', type: 'switch', default: false },
+ { key: 'seo_baidu_normal', label: '普通收录推送', type: 'switch', default: false },
+ { key: 'seo_baidu_fast', label: '快速收录推送', type: 'switch', default: false },
+ { key: 'seo_baidu_token', label: '百度推送 Token', type: 'input', default: '', placeholder: '百度资源平台 Token' },
+ ],
+ },
+ {
+ key: 'image',
+ title: '图片 SEO',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'seo_image_optimize', label: '优化搜索引擎图片展示', type: 'switch', default: true },
+ { key: 'seo_image_default', label: '默认分享图 / SEO 图', type: 'media', default: '' },
+ { key: 'seo_image_always_default', label: '固定使用默认图', type: 'switch', default: false },
+ { key: 'seo_auto_alt', label: '自动补全图片 alt', type: 'switch', default: true },
+ ],
+ },
+ {
+ key: 'redirect',
+ title: '外链重定向',
+ icon: 'fas fa-external-link-alt',
+ fields: [
+ { key: 'seo_go_link', label: '启用外链重定向', type: 'switch', default: true },
+ { key: 'seo_go_link_nonce', label: '启用外链鉴权', type: 'switch', default: true },
+ { key: 'seo_go_link_post', label: '文章内容链接走重定向', type: 'switch', default: true },
+ { key: 'seo_go_link_exclude', label: '重定向排除域名', type: 'textarea', rows: 4, default: '', placeholder: '每行一个域名' },
+ ],
+ },
+ {
+ key: 'advanced',
+ title: '高级 SEO',
+ icon: 'fas fa-sliders-h',
+ fields: [
+ { key: 'seo_no_category', label: '分类 URL 去掉 category', type: 'switch', default: false },
+ { key: 'seo_jsonld', label: '启用 JSON-LD', type: 'switch', default: true },
+ { key: 'seo_jsonld_single', label: '文章页输出 JSON-LD', type: 'switch', default: true },
+ { key: 'seo_jsonld_page', label: '页面输出 JSON-LD', type: 'switch', default: true },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/system-notice.js b/assets/admin/src/schema/sections/global/system-notice.js
new file mode 100644
index 00000000..fa80e2c9
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/system-notice.js
@@ -0,0 +1,149 @@
+export const systemNoticeSchema = {
+ key: 'global.system-notice',
+ title: '弹窗通知',
+ icon: 'fas fa-bullhorn',
+ description: '全站弹窗公告配置,支持按用户身份定向、多按钮组、弹窗尺寸与过期周期。',
+ defaults: {
+ system_notice_enabled: true,
+ system_notice_policy: '',
+ system_notice_title: '',
+ system_notice_content: '',
+ system_notice_btn1_text: '',
+ system_notice_btn1_link: '',
+ system_notice_btn1_color: 'primary',
+ system_notice_btn2_text: '',
+ system_notice_btn2_link: '',
+ system_notice_btn2_color: 'primary',
+ system_notice_btn3_text: '',
+ system_notice_btn3_link: '',
+ system_notice_btn3_color: 'primary',
+ system_notice_btn4_text: '',
+ system_notice_btn4_link: '',
+ system_notice_btn4_color: 'primary',
+ system_notice_size: '',
+ system_notice_radius: false,
+ system_notice_class: '',
+ system_notice_title_icon: '',
+ system_notice_title_class: '',
+ system_notice_title_style: '',
+ system_notice_expires: 24,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '公告设置',
+ icon: 'fas fa-bullhorn',
+ fields: [
+ { key: 'system_notice_enabled', label: '启用系统公告', type: 'switch', default: true },
+ {
+ key: 'system_notice_policy',
+ label: '显示对象',
+ type: 'select',
+ default: '',
+ options: [
+ { label: '所有访客', value: '' },
+ { label: '仅登录用户', value: 'signin' },
+ { label: '仅 VIP 会员', value: 'vip' },
+ { label: '仅非 VIP', value: 'free' },
+ { label: '仅认证用户', value: 'auth' },
+ ],
+ dependsOn: { key: 'system_notice_enabled', value: true },
+ },
+ { key: 'system_notice_title', label: '公告标题', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_content', label: '公告内容', type: 'textarea', default: '', rows: 5, dependsOn: { key: 'system_notice_enabled', value: true }, help: '支持 HTML 标签' },
+ ],
+ },
+ {
+ key: 'buttons',
+ title: '按钮组(留空则不显示)',
+ icon: 'fas fa-th-large',
+ fields: [
+ { key: 'system_notice_btn1_text', label: '按钮 1 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn1_link', label: '按钮 1 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true }, help: '可选,填写后按钮变为链接' },
+ { key: 'system_notice_btn1_color', label: '按钮 1 颜色', type: 'select', default: 'primary', options: [
+ { label: '主题色', value: 'primary' },
+ { label: '成功绿', value: 'success' },
+ { label: '警告橙', value: 'warning' },
+ { label: '危险红', value: 'danger' },
+ { label: '信息蓝', value: 'info' },
+ { label: '深紫', value: 'purple' },
+ { label: '深灰', value: 'dark' },
+ ], dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn2_text', label: '按钮 2 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn2_link', label: '按钮 2 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn2_color', label: '按钮 2 颜色', type: 'select', default: 'primary', options: [
+ { label: '主题色', value: 'primary' },
+ { label: '成功绿', value: 'success' },
+ { label: '警告橙', value: 'warning' },
+ { label: '危险红', value: 'danger' },
+ { label: '信息蓝', value: 'info' },
+ { label: '深紫', value: 'purple' },
+ { label: '深灰', value: 'dark' },
+ ], dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn3_text', label: '按钮 3 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn3_link', label: '按钮 3 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn3_color', label: '按钮 3 颜色', type: 'select', default: 'primary', options: [
+ { label: '主题色', value: 'primary' },
+ { label: '成功绿', value: 'success' },
+ { label: '警告橙', value: 'warning' },
+ { label: '危险红', value: 'danger' },
+ { label: '信息蓝', value: 'info' },
+ { label: '深紫', value: 'purple' },
+ { label: '深灰', value: 'dark' },
+ ], dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn4_text', label: '按钮 4 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn4_link', label: '按钮 4 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_btn4_color', label: '按钮 4 颜色', type: 'select', default: 'primary', options: [
+ { label: '主题色', value: 'primary' },
+ { label: '成功绿', value: 'success' },
+ { label: '警告橙', value: 'warning' },
+ { label: '危险红', value: 'danger' },
+ { label: '信息蓝', value: 'info' },
+ { label: '深紫', value: 'purple' },
+ { label: '深灰', value: 'dark' },
+ ], dependsOn: { key: 'system_notice_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'style',
+ title: '样式设置',
+ icon: 'fas fa-paint-brush',
+ fields: [
+ {
+ key: 'system_notice_size',
+ label: '弹窗尺寸',
+ type: 'select',
+ default: '',
+ options: [
+ { label: '默认', value: '' },
+ { label: '迷你', value: 'modal-mini' },
+ { label: '小号', value: 'modal-sm' },
+ { label: '大号', value: 'modal-lg' },
+ ],
+ dependsOn: { key: 'system_notice_enabled', value: true },
+ },
+ { key: 'system_notice_radius', label: '按钮圆角', type: 'switch', default: false, dependsOn: { key: 'system_notice_enabled', value: true }, help: '开启后弹窗按钮使用圆角样式' },
+ { key: 'system_notice_class', label: '公告样式类', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ { key: 'system_notice_title_icon', label: '标题图标', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true }, help: '如 fas fa-bullhorn' },
+ { key: 'system_notice_title_class', label: '标题背景配色', type: 'select', default: '', dependsOn: { key: 'system_notice_enabled', value: true }, options: [
+ { label: '无', value: '' },
+ { label: '炫彩黄', value: 'jb-yellow' },
+ { label: '炫彩蓝', value: 'jb-blue' },
+ { label: '炫彩绿', value: 'jb-green' },
+ { label: '炫彩紫', value: 'jb-purple' },
+ { label: '炫彩红', value: 'jb-red' },
+ { label: '炫彩橙', value: 'jb-orange' },
+ ], help: '弹窗标题区域的背景配色方案' },
+ { key: 'system_notice_title_style', label: '标题样式', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'expire',
+ title: '过期设置',
+ icon: 'fas fa-clock',
+ fields: [
+ { key: 'system_notice_expires', label: '弹窗周期(小时)', type: 'number', default: 24, min: 0, max: 720, dependsOn: { key: 'system_notice_enabled', value: true }, help: '0 表示永不过期' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/global/tabbar.js b/assets/admin/src/schema/sections/global/tabbar.js
new file mode 100644
index 00000000..08e664e9
--- /dev/null
+++ b/assets/admin/src/schema/sections/global/tabbar.js
@@ -0,0 +1,8 @@
+export const tabbarSchema = {
+ key: 'global.tabbar',
+ title: '手机底部Tab',
+ icon: 'fas fa-mobile-alt',
+ description: '移动端底部导航栏配置。',
+ defaults: {},
+ groups: [],
+}
diff --git a/assets/admin/src/schema/sections/page-display/index.js b/assets/admin/src/schema/sections/page-display/index.js
new file mode 100644
index 00000000..8ff6af73
--- /dev/null
+++ b/assets/admin/src/schema/sections/page-display/index.js
@@ -0,0 +1,991 @@
+/**
+ * Page Display domain schemas.
+ * Each key corresponds to a sub-page under the "页面&显示" section.
+ */
+
+export const pageDisplaySchemas = {
+ /**
+ * Header navigation settings
+ */
+ 'header-nav': {
+ key: 'page_display.header-nav',
+ title: '顶部导航栏',
+ icon: 'fas fa-bars',
+ description: '配置导航栏布局、菜单结构、搜索入口和用户入口。',
+ defaults: {
+ header_fixed: true,
+ header_search_enabled: true,
+ header_user_menu: true,
+ header_publish_btn: true,
+ header_mobile_drawer: true,
+ header_nav_style: 'default',
+ header_max_width: 1200,
+ header_scroll_down_hide: false,
+ header_layout: '2',
+ mobile_header_layout: 'center',
+ mobile_navbar_align: 'left',
+ header_newposts_enabled: true,
+ header_newposts_text: '
发布',
+ header_newposts_radius: 20,
+ header_newposts_class: '',
+ header_theme_custom: false,
+ header_theme_bg_custom: '',
+ header_theme_bg: '',
+ header_theme_color_custom: '',
+ },
+ groups: [
+ {
+ key: 'layout',
+ title: '导航栏布局',
+ icon: 'fas fa-th-large',
+ fields: [
+ { key: 'header_fixed', label: '固定导航栏', type: 'switch', default: true, help: '页面滚动时导航栏保持在顶部' },
+ { key: 'header_nav_style', label: '导航栏风格', type: 'radio', default: 'default', options: [
+ { label: '默认', value: 'default' },
+ { label: '透明', value: 'transparent' },
+ { label: '毛玻璃', value: 'blur' },
+ ]},
+ { key: 'header_max_width', label: '导航栏最大宽度', type: 'select', default: 1200, options: [
+ { label: '跟随内容', value: 0 },
+ { label: '1080px', value: 1080 },
+ { label: '1200px', value: 1200 },
+ { label: '1320px', value: 1320 },
+ ]},
+ { key: 'header_scroll_down_hide', label: '下滑隐藏导航', type: 'switch', default: false, help: '页面向下滑动时自动隐藏导航栏' },
+ ],
+ },
+ {
+ key: 'layout-type',
+ title: '布局类型',
+ icon: 'fas fa-columns',
+ fields: [
+ {
+ key: 'header_layout',
+ label: '电脑端布局',
+ type: 'radio',
+ default: '2',
+ options: [
+ { label: '布局 1(经典)', value: '1' },
+ { label: '布局 2(内联搜索)', value: '2' },
+ { label: '布局 3(紧凑)', value: '3' },
+ ],
+ help: '不同布局切换导航 DOM 结构',
+ },
+ {
+ key: 'mobile_header_layout',
+ label: '移动端布局',
+ type: 'radio',
+ default: 'center',
+ options: [
+ { label: '居中(含搜索按钮)', value: 'center' },
+ { label: '靠左', value: 'left' },
+ ],
+ },
+ {
+ key: 'mobile_navbar_align',
+ label: '移动端菜单方向',
+ type: 'radio',
+ default: 'left',
+ options: [
+ { label: '顶部(关闭手势)', value: 'top' },
+ { label: '左侧滑出', value: 'left' },
+ { label: '右侧滑出', value: 'right' },
+ ],
+ },
+ ],
+ },
+ {
+ key: 'newposts',
+ title: '发布按钮',
+ icon: 'fas fa-pen',
+ fields: [
+ { key: 'header_newposts_enabled', label: '启用发布按钮', type: 'switch', default: true, help: '导航栏显示前台发布入口' },
+ { key: 'header_newposts_text', label: '按钮文案', type: 'input', default: '
发布', dependsOn: { key: 'header_newposts_enabled', value: true } },
+ { key: 'header_newposts_radius', label: '圆角 (px)', type: 'number', default: 20, min: 0, max: 30, dependsOn: { key: 'header_newposts_enabled', value: true } },
+ { key: 'header_newposts_class', label: '自定义 CSS 类', type: 'input', default: '', dependsOn: { key: 'header_newposts_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'color',
+ title: '导航配色',
+ icon: 'fas fa-palette',
+ fields: [
+ { key: 'header_theme_custom', label: '自定义导航配色', type: 'switch', default: false, help: '启用后覆盖导航栏默认配色' },
+ { key: 'header_theme_bg_custom', label: '导航背景色', type: 'color', default: '', dependsOn: { key: 'header_theme_custom', value: true } },
+ { key: 'header_theme_bg', label: '导航背景色(备选)', type: 'color', default: '', dependsOn: { key: 'header_theme_custom', value: true } },
+ { key: 'header_theme_color_custom', label: '导航文字色', type: 'color', default: '', dependsOn: { key: 'header_theme_custom', value: true } },
+ ],
+ },
+ {
+ key: 'components',
+ title: '导航组件',
+ icon: 'fas fa-puzzle-piece',
+ fields: [
+ { key: 'header_search_enabled', label: '搜索入口', type: 'switch', default: true },
+ { key: 'header_user_menu', label: '用户菜单', type: 'switch', default: true },
+ { key: 'header_publish_btn', label: '发布按钮', type: 'switch', default: true, help: '管理员可见的前台发布入口' },
+ { key: 'header_mobile_drawer', label: '移动端抽屉导航', type: 'switch', default: true },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Category page settings
+ */
+ category: {
+ key: 'page_display.category',
+ title: '分类页面',
+ icon: 'fas fa-folder-open',
+ description: '为分类归档页配置头图、简介、列表模式、筛选条和侧边栏。',
+ defaults: {
+ category_cover_enabled: true,
+ category_desc_enabled: true,
+ category_header_style: 'cover',
+ category_list_mode: 'grid',
+ category_filter_bar: true,
+ category_sidebar: true,
+ category_per_page: 12,
+ category_default_cover: '',
+ category_post_count: true,
+ category_orderby: 'modified',
+ },
+ groups: [
+ {
+ key: 'header',
+ title: '分类头图',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'category_header_style', label: '头部风格', type: 'radio', default: 'cover', options: [
+ { label: '封面图', value: 'cover' },
+ { label: '简洁标题', value: 'simple' },
+ { label: '隐藏', value: 'none' },
+ ]},
+ { key: 'category_cover_enabled', label: '显示封面图', type: 'switch', default: true, dependsOn: { key: 'category_header_style', value: 'cover' } },
+ { key: 'category_desc_enabled', label: '显示分类简介', type: 'switch', default: true },
+ { key: 'category_default_cover', label: '默认封面图', type: 'upload', default: '', help: '未设置分类封面时使用的默认图片' },
+ ],
+ },
+ {
+ key: 'list',
+ title: '列表设置',
+ icon: 'fas fa-list',
+ fields: [
+ { key: 'category_list_mode', label: '列表模式', type: 'radio', default: 'grid', options: [
+ { label: '网格', value: 'grid' },
+ { label: '列表', value: 'list' },
+ { label: '紧凑', value: 'compact' },
+ ]},
+ { key: 'category_per_page', label: '每页文章数', type: 'number', default: 12, min: 4, max: 48 },
+ { key: 'category_filter_bar', label: '筛选条', type: 'switch', default: true, help: '子分类筛选和排序' },
+ { key: 'category_sidebar', label: '侧边栏', type: 'switch', default: true },
+ { key: 'category_post_count', label: '显示文章数', type: 'switch', default: true, help: '在分类列表中显示每篇文章的浏览次数' },
+ ],
+ },
+ {
+ key: 'orderby',
+ title: '排序设置',
+ icon: 'fas fa-sort',
+ fields: [
+ { key: 'category_orderby', label: '默认排序', type: 'select', default: 'modified', options: [
+ { label: '最近更新', value: 'modified' },
+ { label: '发布时间', value: 'date' },
+ { label: '浏览数', value: 'views' },
+ { label: '评论数', value: 'comments' },
+ { label: '点赞数', value: 'likes' },
+ ]},
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Tag page settings
+ */
+ tag: {
+ key: 'page_display.tag',
+ title: '标签页面',
+ icon: 'fas fa-tags',
+ description: '为标签归档页配置展示样式和布局。',
+ defaults: {
+ tag_header_style: 'simple',
+ tag_default_cover: '',
+ tag_list_mode: 'grid',
+ tag_sidebar: true,
+ tag_per_page: 12,
+ tag_post_count: true,
+ tag_orderby: 'count',
+ },
+ groups: [
+ {
+ key: 'header',
+ title: '标签头图',
+ icon: 'fas fa-tag',
+ fields: [
+ { key: 'tag_header_style', label: '头部风格', type: 'radio', default: 'simple', options: [
+ { label: '简洁标题', value: 'simple' },
+ { label: '封面图', value: 'cover' },
+ { label: '隐藏', value: 'none' },
+ ]},
+ { key: 'tag_default_cover', label: '默认封面图', type: 'upload', default: '' },
+ ],
+ },
+ {
+ key: 'list',
+ title: '列表设置',
+ icon: 'fas fa-list',
+ fields: [
+ { key: 'tag_list_mode', label: '列表模式', type: 'radio', default: 'grid', options: [
+ { label: '网格', value: 'grid' },
+ { label: '列表', value: 'list' },
+ ]},
+ { key: 'tag_per_page', label: '每页文章数', type: 'number', default: 12, min: 4, max: 48 },
+ { key: 'tag_sidebar', label: '侧边栏', type: 'switch', default: true },
+ { key: 'tag_post_count', label: '显示文章数', type: 'switch', default: true },
+ ],
+ },
+ {
+ key: 'orderby',
+ title: '排序设置',
+ icon: 'fas fa-sort',
+ fields: [
+ { key: 'tag_orderby', label: '默认排序', type: 'select', default: 'count', options: [
+ { label: '文章数', value: 'count' },
+ { label: '名称', value: 'name' },
+ { label: '最近更新', value: 'modified' },
+ ]},
+ ],
+ },
+ ],
+ },
+
+ /**
+ * User homepage settings
+ */
+ 'user-home': {
+ key: 'page_display.user-home',
+ title: '用户主页',
+ icon: 'fas fa-user-circle',
+ description: '配置用户公开主页的布局、Tab 展示和互动功能。',
+ defaults: {
+ user_home_tabs: 'posts,comments,favorites,credits,product,plate,forum,follow',
+ user_home_tabs_order: 'posts,comments,favorites,credits,product,plate,forum,follow',
+ user_home_tabs_pc_hide: '',
+ user_home_tabs_mobile_hide: '',
+ user_home_sidebar: true,
+ user_home_cover: true,
+ user_home_follow: true,
+ user_home_posts_per_page: 10,
+ user_home_avatar: true,
+ user_home_name: true,
+ user_home_desc: true,
+ user_home_stats: true,
+ user_home_social: true,
+ author_main_tab_swiper: false,
+ user_home_message: true,
+ user_home_private: true,
+ user_home_paging_ajax: true,
+ },
+ groups: [
+ {
+ key: 'layout',
+ title: '主页布局',
+ icon: 'fas fa-th-large',
+ fields: [
+ { key: 'user_home_cover', label: '封面区域', type: 'switch', default: true, help: '用户主页顶部封面图' },
+ { key: 'user_home_sidebar', label: '侧边栏', type: 'switch', default: true },
+ { key: 'user_home_follow', label: '关注按钮', type: 'switch', default: true },
+ { key: 'user_home_posts_per_page', label: '每页文章数', type: 'number', default: 10, min: 5, max: 50 },
+ { key: 'user_home_paging_ajax', label: 'AJAX 翻页', type: 'switch', default: true, help: '使用 AJAX 加载替代传统翻页' },
+ ],
+ },
+ {
+ key: 'tabs',
+ title: 'Tab 配置',
+ icon: 'fas fa-columns',
+ fields: [
+ { key: 'user_home_tabs', label: '显示的 Tab', type: 'input', default: 'posts,comments,favorites,credits,product,plate,forum,follow', help: '逗号分隔: posts, comments, favorites, credits, product, plate, forum, follow' },
+ { key: 'user_home_tabs_order', label: 'Tab 显示顺序', type: 'input', default: 'posts,comments,favorites,credits,product,plate,forum,follow', help: '逗号分隔,拖拽排列栏目顺序' },
+ { key: 'user_home_tabs_pc_hide', label: 'PC 端隐藏', type: 'input', default: '', help: '逗号分隔要在 PC 端隐藏的栏目' },
+ { key: 'user_home_tabs_mobile_hide', label: '移动端隐藏', type: 'input', default: '', help: '逗号分隔要在移动端隐藏的栏目' },
+ { key: 'author_main_tab_swiper', label: '移动端滑动切换', type: 'switch', default: false, help: '移动端左右滑动切换栏目' },
+ ],
+ },
+ {
+ key: 'display',
+ title: '展示设置',
+ icon: 'fas fa-eye',
+ fields: [
+ { key: 'user_home_avatar', label: '显示头像', type: 'switch', default: true },
+ { key: 'user_home_name', label: '显示用户名', type: 'switch', default: true },
+ { key: 'user_home_desc', label: '显示个人简介', type: 'switch', default: true },
+ { key: 'user_home_stats', label: '显示统计数据', type: 'switch', default: true },
+ { key: 'user_home_social', label: '显示社交链接', type: 'switch', default: true },
+ { key: 'user_home_message', label: '私信按钮', type: 'switch', default: true, help: '在用户主页显示发送私信按钮' },
+ { key: 'user_home_private', label: '留言按钮', type: 'switch', default: true, help: '在用户主页显示留言入口' },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Other pages settings (404, search results, etc.)
+ */
+ other: {
+ key: 'page_display.other',
+ title: '其它页面',
+ icon: 'fas fa-file',
+ description: '配置 404 页面、搜索结果页等特殊页面的展示。',
+ defaults: {
+ page_header_style: '1',
+ page_header_cover_img: '',
+ page_404_search: true,
+ page_404_popular: true,
+ page_404_message: '页面未找到',
+ page_404_back: true,
+ page_search_sidebar: true,
+ page_search_per_page: 12,
+ page_author_cover: true,
+ page_author_sidebar: true,
+ page_author_posts_per_page: 12,
+ },
+ groups: [
+ {
+ key: 'page-header',
+ title: '统一页头',
+ icon: 'fas fa-heading',
+ fields: [
+ { key: 'page_header_style', label: '页面标题样式', type: 'select', default: '1', options: [
+ { label: '不显示', value: '' },
+ { label: '简单', value: '1' },
+ { label: '卡片', value: '2' },
+ { label: '封面', value: '3' },
+ ], help: '控制归档/搜索/页面的页头样式,单页可通过 post meta 覆盖' },
+ { key: 'page_header_cover_img', label: '默认封面图', type: 'upload', default: '', help: '封面样式(样式3)的默认背景图,单页可覆盖' },
+ ],
+ },
+ {
+ key: 'page_404',
+ title: '404 页面',
+ icon: 'fas fa-exclamation-triangle',
+ fields: [
+ { key: 'page_404_message', label: '404 提示文字', type: 'input', default: '页面未找到' },
+ { key: 'page_404_search', label: '显示搜索框', type: 'switch', default: true },
+ { key: 'page_404_popular', label: '显示热门文章', type: 'switch', default: true },
+ { key: 'page_404_back', label: '显示返回首页', type: 'switch', default: true },
+ ],
+ },
+ {
+ key: 'search_results',
+ title: '搜索结果页',
+ icon: 'fas fa-search',
+ fields: [
+ { key: 'page_search_sidebar', label: '搜索结果侧边栏', type: 'switch', default: true },
+ { key: 'page_search_per_page', label: '每页结果数', type: 'number', default: 12, min: 4, max: 48 },
+ ],
+ },
+ {
+ key: 'author',
+ title: '作者页面',
+ icon: 'fas fa-user-edit',
+ fields: [
+ { key: 'page_author_cover', label: '显示封面', type: 'switch', default: true },
+ { key: 'page_author_sidebar', label: '显示侧边栏', type: 'switch', default: true },
+ { key: 'page_author_posts_per_page', label: '每页文章数', type: 'number', default: 12, min: 4, max: 48 },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Global background settings
+ */
+ background: {
+ key: 'page_display.background',
+ title: '全局背景',
+ icon: 'fas fa-image',
+ description: '配置站点全局背景图、背景色、暗色模式背景和遮罩。',
+ defaults: {
+ bg_color: '',
+ bg_image: '',
+ bg_size: '100%',
+ bg_position: 'center center',
+ bg_repeat: 'no-repeat',
+ bg_attachment: 'fixed',
+ bg_dark_color: '',
+ bg_dark_image: '',
+ bg_overlay: false,
+ bg_overlay_color: 'rgba(0, 0, 0, 0.3)',
+ page_bg_color: '',
+ page_bg_image: '',
+ },
+ groups: [
+ {
+ key: 'light',
+ title: '亮色背景',
+ icon: 'fas fa-sun',
+ fields: [
+ { key: 'bg_color', label: '背景颜色', type: 'color', default: '', help: '留空则使用主题默认背景色' },
+ { key: 'bg_image', label: '背景图片', type: 'media', help: '推荐使用高分辨率图片' },
+ { key: 'bg_size', label: '图片尺寸', type: 'select', default: 'cover', options: [
+ { label: '覆盖 (Cover)', value: 'cover' },
+ { label: '包含 (Contain)', value: 'contain' },
+ { label: '原始大小', value: 'auto' },
+ { label: '拉伸填充', value: '100% 100%' },
+ { label: '100% 宽度(旧版兼容)', value: '100%' },
+ ]},
+ { key: 'bg_position', label: '图片位置', type: 'select', default: 'center center', options: [
+ { label: '居中', value: 'center center' },
+ { label: '顶部居中', value: 'top center' },
+ { label: '底部居中', value: 'bottom center' },
+ ]},
+ { key: 'bg_repeat', label: '图片重复', type: 'select', default: 'no-repeat', options: [
+ { label: '不重复', value: 'no-repeat' },
+ { label: '平铺', value: 'repeat' },
+ { label: '水平平铺', value: 'repeat-x' },
+ { label: '垂直平铺', value: 'repeat-y' },
+ ]},
+ { key: 'bg_attachment', label: '固定方式', type: 'select', default: 'fixed', options: [
+ { label: '固定 (视差)', value: 'fixed' },
+ { label: '随内容滚动', value: 'scroll' },
+ ]},
+ ],
+ },
+ {
+ key: 'dark',
+ title: '暗色背景',
+ icon: 'fas fa-moon',
+ fields: [
+ { key: 'bg_dark_color', label: '暗色背景颜色', type: 'color', default: '', help: '留空则使用主题默认暗色背景' },
+ { key: 'bg_dark_image', label: '暗色背景图片', type: 'media', help: '暗色模式下显示的背景图' },
+ ],
+ },
+ {
+ key: 'overlay',
+ title: '遮罩设置',
+ icon: 'fas fa-layer-group',
+ fields: [
+ { key: 'bg_overlay', label: '启用背景遮罩', type: 'switch', default: false, help: '在背景图上方叠加半透明遮罩,提升文字可读性' },
+ { key: 'bg_overlay_color', label: '遮罩颜色', type: 'input', default: 'rgba(0, 0, 0, 0.3)', dependsOn: { key: 'bg_overlay', value: true }, help: '支持 rgba/hex 色值' },
+ ],
+ },
+ {
+ key: 'page',
+ title: '页面背景',
+ icon: 'fas fa-file',
+ fields: [
+ { key: 'page_bg_color', label: '内容区背景色', type: 'color', default: '', help: '主内容区域的背景色' },
+ { key: 'page_bg_image', label: '内容区背景图', type: 'media' },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Menu decoration settings
+ */
+ 'menu-decoration': {
+ key: 'page_display.menu-decoration',
+ title: '菜单装饰',
+ icon: 'fas fa-bars',
+ description: '配置导航菜单的图标、徽章、下拉样式和图文卡片子菜单。',
+ defaults: {
+ menu_icon_enabled: true,
+ menu_badge_enabled: true,
+ menu_submenu_style: 'dropdown',
+ menu_submenu_columns: 1,
+ menu_submenu_width: 220,
+ menu_mobile_style: 'drawer',
+ menu_item_padding: 12,
+ menu_hover_effect: 'underline',
+ },
+ groups: [
+ {
+ key: 'icon',
+ title: '菜单图标',
+ icon: 'fas fa-icons',
+ fields: [
+ { key: 'menu_icon_enabled', label: '显示菜单图标', type: 'switch', default: true, help: '在菜单项文字前显示图标(需在菜单管理中设置 CSS 类)' },
+ { key: 'menu_badge_enabled', label: '显示菜单徽章', type: 'switch', default: true, help: '在菜单项上显示 "新"、"热" 等徽章标记' },
+ ],
+ },
+ {
+ key: 'submenu',
+ title: '下拉子菜单',
+ icon: 'fas fa-caret-square-down',
+ fields: [
+ {
+ key: 'menu_submenu_style',
+ label: '子菜单样式',
+ type: 'radio',
+ default: 'dropdown',
+ options: [
+ { label: '普通下拉', value: 'dropdown' },
+ { label: '多列下拉', value: 'mega' },
+ { label: '图文卡片', value: 'cards' },
+ ],
+ },
+ { key: 'menu_submenu_columns', label: '多列列数', type: 'number', default: 1, min: 1, max: 5, dependsOn: { key: 'menu_submenu_style', value: 'mega' } },
+ { key: 'menu_submenu_width', label: '子菜单宽度 (px)', type: 'number', default: 220, min: 160, max: 800, dependsOn: { key: 'menu_submenu_style', value: 'mega' } },
+ ],
+ },
+ {
+ key: 'effect',
+ title: '交互效果',
+ icon: 'fas fa-magic',
+ fields: [
+ {
+ key: 'menu_hover_effect',
+ label: 'Hover 效果',
+ type: 'radio',
+ default: 'underline',
+ options: [
+ { label: '下划线', value: 'underline' },
+ { label: '背景色', value: 'background' },
+ { label: '文字变色', value: 'color' },
+ { label: '无效果', value: 'none' },
+ ],
+ },
+ { key: 'menu_item_padding', label: '菜单项内边距 (px)', type: 'number', default: 12, min: 4, max: 32 },
+ ],
+ },
+ {
+ key: 'mobile',
+ title: '移动端菜单',
+ icon: 'fas fa-mobile-alt',
+ fields: [
+ {
+ key: 'menu_mobile_style',
+ label: '移动端菜单样式',
+ type: 'radio',
+ default: 'drawer',
+ options: [
+ { label: '抽屉', value: 'drawer' },
+ { label: '全屏', value: 'fullscreen' },
+ { label: '下拉', value: 'dropdown' },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Header components
+ */
+ 'header-components': {
+ key: 'page_display.header-components',
+ title: '顶部多功能组件',
+ icon: 'fas fa-th',
+ description: '顶部轮播、图标卡片、搜索条、公告条等组件配置。',
+ defaults: {
+ header_components_search_enabled: true,
+ header_components_cards_enabled: false,
+ header_components_nav_color: '#ffffff',
+ header_components_show_on: ['home'],
+ header_components_device_visibility: '',
+ header_components_filter_blur_devices: [],
+ header_components_card_show_devices: ['pc', 'mobile'],
+ header_components_card_shared_bg: false,
+ header_components_card_blur: true,
+ header_components_card_icon_shape: 'circle',
+ header_components_icon_cards: [],
+ header_components_search_pc: true,
+ header_components_search_mobile: true,
+ header_components_search_show_keywords: false,
+ header_components_search_popular_limit: 5,
+ header_components_search_show_input_cat: false,
+ header_components_search_show_type: false,
+ slider_config_direction: 'horizontal',
+ slider_config_speed: 300,
+ slider_config_button: true,
+ slider_config_scale_height: false,
+ slider_config_scale: false,
+ slider_config_max_height: '',
+ slider_config_min_height: '',
+ },
+ groups: [
+ {
+ key: 'visibility',
+ title: '显示规则',
+ icon: 'fas fa-eye',
+ fields: [
+ { key: 'header_components_search_enabled', label: '启用搜索覆盖层', type: 'switch', default: true, help: '在顶部组件区域显示搜索条覆盖层' },
+ { key: 'header_components_cards_enabled', label: '启用图标卡片', type: 'switch', default: false, help: '在顶部组件区域显示图标卡片入口' },
+ { key: 'header_components_show_on', label: '按页面类型启用', type: 'checkbox', default: ['home'], options: [
+ { label: '首页', value: 'home' },
+ { label: '文章', value: 'single' },
+ { label: '页面', value: 'page' },
+ { label: '分类', value: 'category' },
+ { label: '标签', value: 'tag' },
+ { label: '搜索', value: 'search' },
+ { label: '归档', value: 'archive' },
+ { label: '专题', value: 'special' },
+ ], help: '选择哪些页面类型显示顶部组件,不选则关闭' },
+ { key: 'header_components_device_visibility', label: '设备显示规则', type: 'radio', default: '', options: [
+ { label: '全部', value: '' },
+ { label: '仅电脑', value: 'only_pc' },
+ { label: '仅移动', value: 'only_sm' },
+ ] },
+ ],
+ },
+ {
+ key: 'nav-color',
+ title: '导航栏配色',
+ icon: 'fas fa-palette',
+ fields: [
+ { key: 'header_components_nav_color', label: '导航栏文字颜色', type: 'color', default: '#ffffff', help: '顶部组件叠加时固定导航的文字颜色' },
+ ],
+ },
+ {
+ key: 'slider-config',
+ title: '幻灯片配置',
+ icon: 'fas fa-sliders-h',
+ fields: [
+ { key: 'slider_config_direction', label: '滑动方向', type: 'radio', default: 'horizontal', options: [
+ { label: '水平', value: 'horizontal' },
+ { label: '垂直', value: 'vertical' },
+ ]},
+ { key: 'slider_config_speed', label: '切换速度 (ms)', type: 'number', default: 300, min: 100, max: 3000 },
+ { key: 'slider_config_button', label: '显示前进后退按钮', type: 'switch', default: true },
+ { key: 'slider_config_scale_height', label: '缩放高度', type: 'switch', default: false },
+ { key: 'slider_config_scale', label: '缩放效果', type: 'switch', default: false },
+ { key: 'slider_config_max_height', label: '最大高度', type: 'input', default: '' },
+ { key: 'slider_config_min_height', label: '最小高度', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'search',
+ title: '搜索组件',
+ icon: 'fas fa-search',
+ fields: [
+ { key: 'header_components_search_pc', label: 'PC 端显示搜索', type: 'switch', default: true, dependsOn: { key: 'header_components_search_enabled', value: true } },
+ { key: 'header_components_search_mobile', label: '移动端显示搜索', type: 'switch', default: true, dependsOn: { key: 'header_components_search_enabled', value: true } },
+ { key: 'header_components_search_show_keywords', label: '显示热门搜索词', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true } },
+ { key: 'header_components_search_popular_limit', label: '热词数量', type: 'number', default: 5, min: 1, max: 20, dependsOn: { key: 'header_components_search_show_keywords', value: true } },
+ { key: 'header_components_search_show_input_cat', label: '显示分类搜索', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '搜索框内显示分类选择下拉' },
+ { key: 'header_components_search_show_type', label: '显示类型切换', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '搜索框内显示内容类型切换' },
+ ],
+ },
+ {
+ key: 'cards',
+ title: '图标卡片样式',
+ icon: 'fas fa-th-large',
+ fields: [
+ { key: 'header_components_filter_blur_devices', label: '容器模糊设备', type: 'checkbox', default: [], dependsOn: { key: 'header_components_cards_enabled', value: true }, options: [
+ { label: 'PC', value: 'pc' },
+ { label: '移动端', value: 'mobile' },
+ ]},
+ { key: 'header_components_card_show_devices', label: '卡片显示设备', type: 'checkbox', default: ['pc', 'mobile'], dependsOn: { key: 'header_components_cards_enabled', value: true }, options: [
+ { label: 'PC', value: 'pc' },
+ { label: '移动端', value: 'mobile' },
+ ]},
+ { key: 'header_components_card_shared_bg', label: '共享卡片背景', type: 'switch', default: false, dependsOn: { key: 'header_components_cards_enabled', value: true }, help: '开启后所有图标卡片共用背景模式' },
+ { key: 'header_components_card_blur', label: '启用卡片模糊效果', type: 'switch', default: true, dependsOn: { key: 'header_components_cards_enabled', value: true } },
+ { key: 'header_components_card_icon_shape', label: '图标形状', type: 'radio', default: 'circle', dependsOn: { key: 'header_components_cards_enabled', value: true }, options: [
+ { label: '圆形', value: 'circle' },
+ { label: '方形', value: 'square' },
+ ]},
+ ],
+ },
+ {
+ key: 'icon-cards',
+ title: '图标卡片列表',
+ icon: 'fas fa-list',
+ fields: [
+ {
+ key: 'header_components_icon_cards',
+ label: '图标卡片',
+ type: 'repeater',
+ sanitize_type: 'array',
+ default: [],
+ dependsOn: { key: 'header_components_cards_enabled', value: true },
+ addText: '添加卡片',
+ fields: [
+ { key: 'icon', label: '图标类名', type: 'input', default: 'fas fa-star', placeholder: 'fas fa-star' },
+ { key: 'custom_icon', label: '自定义图标 HTML', type: 'textarea', default: '', rows: 2, placeholder: '
... ' },
+ { key: 'icon_class', label: '图标配色', type: 'select', default: 'c-blue', options: [
+ { label: 'Blue', value: 'c-blue' },
+ { label: 'Yellow', value: 'c-yellow' },
+ { label: 'Green', value: 'c-green' },
+ { label: 'Red', value: 'c-red' },
+ { label: 'Orange', value: 'c-orange' },
+ { label: 'Cyan', value: 'c-cyan' },
+ { label: 'Pink', value: 'c-pink' },
+ { label: 'Gradient Cyan', value: 'jb-cyan' },
+ { label: 'Gradient Green', value: 'jb-green' },
+ { label: 'Gradient Orange', value: 'jb-orange' },
+ ]},
+ { key: 'title', label: '标题', type: 'input', default: '' },
+ { key: 'desc', label: '描述', type: 'input', default: '' },
+ { key: 'url', label: '链接 URL', type: 'input', default: '', placeholder: 'https://example.com' },
+ { key: 'target', label: '打开方式', type: 'radio', default: '_self', options: [
+ { label: '当前标签页', value: '_self' },
+ { label: '新标签页', value: '_blank' },
+ ]},
+ ],
+ },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Footer settings (already has a real Vue page)
+ */
+ footer: {
+ key: 'page_display.footer',
+ title: '底部页脚',
+ icon: 'fas fa-shoe-prints',
+ description: '页脚布局、友链、版权信息配置。实际编辑界面在 FooterSettings.vue(customPage),此处为 REST 聚合描述。',
+ defaults: {
+ footer_columns: 3,
+ footer_copyright: true,
+ footer_icp: true,
+ icp_number: '',
+ footer_police: true,
+ police_number: '',
+ footer_theme_custom: false,
+ footer_theme_bg_custom: '',
+ footer_theme_bg: '',
+ footer_theme_color_custom: '',
+ footer_contact_wechat_img: '',
+ footer_contact_qq: '',
+ footer_contact_weibo: '',
+ footer_contact_email: '',
+ footer_t1_m_s: ['pc', 'mobile'],
+ footer_t1_img: '',
+ footer_t1_img_dark: '',
+ footer_t1_t: '',
+ fcode_t1_code: '',
+ footer_contact_m_s: ['pc', 'mobile'],
+ footer_contact_more: '',
+ fcode_t2_code_1: '',
+ fcode_t2_code_2: '',
+ footer_mini_img_m_s: ['pc', 'mobile'],
+ footer_mini_img: '',
+ fcode_customize_code: '',
+ },
+ groups: [
+ {
+ key: 'layout',
+ title: '页脚布局',
+ icon: 'fas fa-columns',
+ fields: [
+ { key: 'footer_columns', label: '页脚列数', type: 'number', default: 3, min: 1, max: 4 },
+ { key: 'footer_copyright', label: '显示版权信息', type: 'switch', default: true },
+ { key: 'footer_icp', label: '显示ICP备案', type: 'switch', default: true },
+ { key: 'icp_number', label: 'ICP备案号', type: 'input', default: '' },
+ { key: 'footer_police', label: '显示公安备案', type: 'switch', default: true },
+ { key: 'police_number', label: '公安备案号', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'color',
+ title: '页脚颜色',
+ icon: 'fas fa-palette',
+ fields: [
+ { key: 'footer_theme_custom', label: '自定义页脚颜色', type: 'switch', default: false },
+ { key: 'footer_theme_bg_custom', label: '自定义背景色', type: 'color', default: '' },
+ { key: 'footer_theme_bg', label: '背景图片', type: 'media', default: '' },
+ { key: 'footer_theme_color_custom', label: '自定义文字颜色', type: 'color', default: '' },
+ ],
+ },
+ {
+ key: 'contact',
+ title: '联系方式',
+ icon: 'fas fa-address-book',
+ fields: [
+ { key: 'footer_contact_wechat_img', label: '微信二维码', type: 'media', default: '' },
+ { key: 'footer_contact_qq', label: 'QQ', type: 'input', default: '' },
+ { key: 'footer_contact_weibo', label: '微博', type: 'input', default: '' },
+ { key: 'footer_contact_email', label: '邮箱', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'brand',
+ title: '品牌列(第一列)',
+ icon: 'fas fa-building',
+ fields: [
+ { key: 'footer_t1_m_s', label: '显示设备', type: 'checkbox', default: ['pc', 'mobile'] },
+ { key: 'footer_t1_img', label: '页脚 Logo(日间)', type: 'media', default: '' },
+ { key: 'footer_t1_img_dark', label: '页脚 Logo(夜间)', type: 'media', default: '' },
+ { key: 'footer_t1_t', label: '品牌标题', type: 'input', default: '' },
+ { key: 'fcode_t1_code', label: '品牌列自定义代码', type: 'textarea', default: '' },
+ ],
+ },
+ {
+ key: 'contact_ext',
+ title: '联系方式扩展',
+ icon: 'fas fa-phone-alt',
+ fields: [
+ { key: 'footer_contact_m_s', label: '显示设备', type: 'checkbox', default: ['pc', 'mobile'] },
+ { key: 'footer_contact_more', label: '更多联系项', type: 'textarea', default: '' },
+ { key: 'fcode_t2_code_1', label: '联系区上方自定义代码', type: 'textarea', default: '' },
+ { key: 'fcode_t2_code_2', label: '联系区下方自定义代码', type: 'textarea', default: '' },
+ ],
+ },
+ {
+ key: 'mini_qr',
+ title: '二维码图片组(第三列)',
+ icon: 'fas fa-qrcode',
+ fields: [
+ { key: 'footer_mini_img_m_s', label: '显示设备', type: 'checkbox', default: ['pc', 'mobile'] },
+ { key: 'footer_mini_img', label: '二维码组', type: 'textarea', default: '' },
+ ],
+ },
+ {
+ key: 'custom',
+ title: '底部自定义',
+ icon: 'fas fa-code',
+ fields: [
+ { key: 'fcode_customize_code', label: '底部自定义代码', type: 'textarea', default: '' },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Homepage settings (already has a real Vue page)
+ */
+ homepage: {
+ key: 'page_display.homepage',
+ title: '首页配置',
+ icon: 'fas fa-home',
+ description: '首页模块、布局、推荐位配置。',
+ defaults: {},
+ groups: [],
+ },
+
+ /**
+ * Special/topic page settings
+ */
+ special: {
+ key: 'page_display.special',
+ title: '专题页面',
+ icon: 'fas fa-layer-group',
+ description: '配置专题聚合页的封面、摘要、布局、聚合规则和子页面展示策略。',
+ defaults: {
+ special_cover_enabled: true,
+ special_summary_enabled: true,
+ special_summary_length: 200,
+ special_default_cover: '',
+ special_layout: 'grid',
+ special_columns: 2,
+ special_source_type: 'category',
+ special_source_ids: '',
+ special_subpage_display: 'cards',
+ special_subpage_count: 6,
+ special_sidebar: true,
+ special_pagination: 'numeric',
+ special_per_page: 12,
+ special_post_count: false,
+ special_orderby: 'date',
+ },
+ groups: [
+ {
+ key: 'header',
+ title: '专题头部',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'special_cover_enabled', label: '显示专题封面', type: 'switch', default: true, help: '在专题页顶部展示封面图' },
+ { key: 'special_summary_enabled', label: '显示专题摘要', type: 'switch', default: true },
+ { key: 'special_summary_length', label: '摘要最大长度', type: 'number', default: 200, min: 50, max: 500, dependsOn: { key: 'special_summary_enabled', value: true } },
+ { key: 'special_default_cover', label: '默认封面图', type: 'upload', default: '', help: '未设置专题封面时使用的默认图片' },
+ ],
+ },
+ {
+ key: 'layout',
+ title: '布局设置',
+ icon: 'fas fa-th-large',
+ fields: [
+ {
+ key: 'special_layout',
+ label: '内容布局',
+ type: 'radio',
+ default: 'grid',
+ options: [
+ { label: '网格', value: 'grid' },
+ { label: '列表', value: 'list' },
+ { label: '瀑布流', value: 'masonry' },
+ ],
+ },
+ { key: 'special_columns', label: '网格列数', type: 'number', default: 2, min: 1, max: 4, dependsOn: { key: 'special_layout', value: 'grid' } },
+ { key: 'special_sidebar', label: '侧边栏', type: 'switch', default: true },
+ { key: 'special_post_count', label: '显示文章数', type: 'switch', default: false, help: '在专题列表中显示每篇文章的浏览次数' },
+ ],
+ },
+ {
+ key: 'aggregation',
+ title: '聚合规则',
+ icon: 'fas fa-database',
+ fields: [
+ {
+ key: 'special_source_type',
+ label: '内容来源',
+ type: 'radio',
+ default: 'category',
+ options: [
+ { label: '分类', value: 'category' },
+ { label: '标签', value: 'tag' },
+ { label: '手动选择', value: 'manual' },
+ { label: '最新文章', value: 'latest' },
+ ],
+ },
+ { key: 'special_source_ids', label: '来源 ID 列表', type: 'input', default: '', placeholder: '逗号分隔分类/标签 ID', help: '填写分类或标签的 ID,逗号分隔' },
+ ],
+ },
+ {
+ key: 'subpage',
+ title: '子页面展示',
+ icon: 'fas fa-clone',
+ fields: [
+ {
+ key: 'special_subpage_display',
+ label: '子页面样式',
+ type: 'radio',
+ default: 'cards',
+ options: [
+ { label: '卡片', value: 'cards' },
+ { label: '列表', value: 'list' },
+ { label: '时间线', value: 'timeline' },
+ ],
+ },
+ { key: 'special_subpage_count', label: '每页子页面数', type: 'number', default: 6, min: 1, max: 24 },
+ ],
+ },
+ {
+ key: 'pagination',
+ title: '分页设置',
+ icon: 'fas fa-ellipsis-h',
+ fields: [
+ {
+ key: 'special_pagination',
+ label: '分页模式',
+ type: 'radio',
+ default: 'numeric',
+ options: [
+ { label: '数字分页', value: 'numeric' },
+ { label: '加载更多', value: 'load_more' },
+ { label: '无限滚动', value: 'infinite' },
+ ],
+ },
+ { key: 'special_per_page', label: '每页文章数', type: 'number', default: 12, min: 4, max: 48 },
+ ],
+ },
+ {
+ key: 'orderby',
+ title: '排序设置',
+ icon: 'fas fa-sort',
+ fields: [
+ { key: 'special_orderby', label: '默认排序', type: 'select', default: 'date', options: [
+ { label: '发布时间', value: 'date' },
+ { label: '最近更新', value: 'modified' },
+ { label: '浏览数', value: 'views' },
+ { label: '评论数', value: 'comments' },
+ ]},
+ ],
+ },
+ ],
+ },
+}
diff --git a/assets/admin/src/schema/sections/payment/affiliate.js b/assets/admin/src/schema/sections/payment/affiliate.js
new file mode 100644
index 00000000..90112a04
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/affiliate.js
@@ -0,0 +1,73 @@
+export const affiliateSchema = {
+ key: 'payment.affiliate',
+ title: '推广返佣',
+ icon: 'fas fa-share-alt',
+ description: '配置返佣比例、归因窗口和结算说明。',
+ defaults: {
+ payment_affiliate_enabled: false,
+ payment_affiliate_ratio: 10,
+ payment_affiliate_window_days: 30,
+ payment_affiliate_require_paid_order: true,
+ payment_affiliate_self_s: true,
+ payment_affiliate_show_users: true,
+ payment_affiliate_user_ratio: 10,
+ payment_affiliate_vip1_ratio: 0,
+ payment_affiliate_vip2_ratio: 0,
+ payment_affiliate_poster_s: true,
+ payment_affiliate_poster_img: '',
+ payment_affiliate_poster_title: '',
+ payment_affiliate_poster_desc: '',
+ payment_affiliate_text_desc: '',
+ payment_affiliate_text_details_title: '',
+ payment_affiliate_text_details: '',
+ payment_affiliate_text_discount: '',
+ payment_affiliate_note: '返佣链路复用邀请体系,订单归因和结算自动衔接当前策略字段。',
+ },
+ groups: [
+ {
+ key: 'policy',
+ title: '返佣策略',
+ icon: 'fas fa-project-diagram',
+ fields: [
+ { key: 'payment_affiliate_enabled', label: '启用推广返佣结构', type: 'switch', default: false },
+ { key: 'payment_affiliate_ratio', label: '返佣比例(%)', type: 'number', default: 10, min: 0, max: 100, dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_window_days', label: '归因窗口(天)', type: 'number', default: 30, min: 1, max: 365, dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_require_paid_order', label: '仅对已支付订单返佣', type: 'switch', default: true, dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_self_s', label: '自购返佣', type: 'switch', default: true, dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_show_users', label: '显示推广用户', type: 'switch', default: true, dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'user-ratio',
+ title: '用户返佣比例',
+ icon: 'fas fa-users-cog',
+ fields: [
+ { key: 'payment_affiliate_user_ratio', label: '普通用户比例(%)', type: 'number', default: 10, min: 0, max: 100, dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_vip1_ratio', label: 'VIP1比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_vip2_ratio', label: 'VIP2比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'poster',
+ title: '推广海报',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'payment_affiliate_poster_s', label: '启用推广海报', type: 'switch', default: true, dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_poster_img', label: '海报背景图', type: 'media', default: '', dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_poster_title', label: '海报标题', type: 'input', default: '', dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_poster_desc', label: '海报描述', type: 'input', default: '', dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'text',
+ title: '文案设置',
+ icon: 'fas fa-pen-fancy',
+ fields: [
+ { key: 'payment_affiliate_text_desc', label: '返佣说明', type: 'textarea', default: '', dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_text_details_title', label: '明细标题', type: 'input', default: '', dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_text_details', label: '明细说明', type: 'textarea', default: '', dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ { key: 'payment_affiliate_text_discount', label: '折扣文案', type: 'input', default: '', dependsOn: { key: 'payment_affiliate_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/payment/cardPass.js b/assets/admin/src/schema/sections/payment/cardPass.js
new file mode 100644
index 00000000..c039a4d6
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/cardPass.js
@@ -0,0 +1,59 @@
+export const cardPassSchema = {
+ key: 'payment.card-pass',
+ title: '充值卡',
+ icon: 'fas fa-ticket-alt',
+ description: '配置充值卡类型、卡号规则、批量生成和兑换逻辑。',
+ defaults: {
+ card_pass_enabled: false,
+ card_pass_desc: '',
+ card_pass_type: 'balance',
+ card_pass_prefix: 'AK',
+ card_pass_length: 16,
+ card_pass_batch_size: 50,
+ card_pass_max_redeem: 1,
+ card_pass_expiry: 0,
+ card_pass_amount: 10,
+ card_pass_vip_days: 30,
+ card_pass_credits: 0,
+ card_pass_redeem_page: '',
+ card_pass_success_msg: '兑换成功!',
+ card_pass_fail_msg: '卡号无效或已过期',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '充值卡设置',
+ icon: 'fas fa-id-card',
+ fields: [
+ { key: 'card_pass_enabled', label: '启用充值卡', type: 'switch', default: false },
+ { key: 'card_pass_desc', label: '充值卡说明', type: 'textarea', default: '', dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_type', label: '充值卡类型', type: 'select', default: 'balance', options: [{ label: '余额卡', value: 'balance' }, { label: 'VIP卡', value: 'vip' }, { label: '积分卡', value: 'credits' }], dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_prefix', label: '卡号前缀', type: 'input', default: 'AK', dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_length', label: '卡号长度', type: 'number', default: 16, min: 8, max: 32, dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_batch_size', label: '批量生成数量', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'card_pass_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'redeem',
+ title: '兑换设置',
+ icon: 'fas fa-exchange-alt',
+ fields: [
+ { key: 'card_pass_max_redeem', label: '单卡最大兑换次数', type: 'number', default: 1, min: 1, max: 100, dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_expiry', label: '有效期(天)', type: 'number', default: 0, min: 0, help: '0为永久有效', dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_amount', label: '面额(元)', type: 'number', default: 10, min: 1, max: 9999, dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_vip_days', label: 'VIP天数', type: 'number', default: 30, min: 1, max: 3650, dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_credits', label: '赠送积分', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'card_pass_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'display',
+ title: '展示设置',
+ icon: 'fas fa-desktop',
+ fields: [
+ { key: 'card_pass_redeem_page', label: '兑换页面', type: 'input', default: '', dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_success_msg', label: '兑换成功提示', type: 'input', default: '兑换成功!', dependsOn: { key: 'card_pass_enabled', value: true } },
+ { key: 'card_pass_fail_msg', label: '兑换失败提示', type: 'input', default: '卡号无效或已过期', dependsOn: { key: 'card_pass_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/payment/gateway.js b/assets/admin/src/schema/sections/payment/gateway.js
new file mode 100644
index 00000000..050acb31
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/gateway.js
@@ -0,0 +1,172 @@
+export const gatewaySchema = {
+ key: 'payment.gateway',
+ title: '收款接口',
+ icon: 'fas fa-credit-card',
+ description: '配置支付网关凭据、回调校验口令与运行模式。填写凭据后对应网关即变为”可用”。',
+ defaults: {
+ payment_gateway_mode: 'manual_review',
+ payment_gateway_test_enabled: true,
+ payment_gateway_manual_review: true,
+ payment_gateway_callback_secret: '',
+ payment_gateway_help: '未接真实支付网关时,建议使用测试网关或人工确认模式完成联调。',
+ payment_wechat_enabled: false,
+ payment_alipay_enabled: false,
+ payment_paypal_enabled: false,
+ payment_balance_enabled: false,
+ payment_card_pass_enabled: false,
+ payment_xunhupay_enabled: false,
+ payment_xhpay_enabled: false,
+ payment_payjs_enabled: false,
+ payment_epay_enabled: false,
+ payment_vmq_enabled: false,
+ payment_codepay_enabled: false,
+ payment_stripe_enabled: false,
+ wechat_merchantid: '',
+ wechat_jsapi: '',
+ wechat_appsecret: '',
+ wechat_cert_path: '',
+ wechat_key_path: '',
+ alipay_appid: '',
+ alipay_privatekey: '',
+ alipay_publickey: '',
+ alipay_return_url: '',
+ alipay_notify_url: '',
+ paypal_client_id: '',
+ paypal_client_secret: '',
+ paypal_currency: 'USD',
+ paypal_sandbox: true,
+ xunhupay_appid: '',
+ xunhupay_appsecret: '',
+ xhpay_mchid: '',
+ xhpay_alipay_v2: '',
+ xhpay_wx_h5: '',
+ payjs_mchid: '',
+ payjs_key: '',
+ epay_apiurl: '',
+ epay_pid: '',
+ epay_key: '',
+ stripe_secret_key: '',
+ stripe_publishable_key: '',
+ stripe_webhook_secret: '',
+ stripe_currency: 'usd',
+ stripe_test_mode: true,
+ },
+ groups: [
+ {
+ key: 'adapter',
+ title: '网关适配器',
+ icon: 'fas fa-plug',
+ fields: [
+ { key: 'payment_gateway_mode', label: '运行模式', type: 'radio', default: 'manual_review', options: [{ label: '人工确认', value: 'manual_review' }, { label: '测试网关', value: 'test_gateway' }, { label: '适配器就绪', value: 'adapter_ready' }] },
+ { key: 'payment_gateway_test_enabled', label: '启用测试网关', type: 'switch', default: true },
+ { key: 'payment_gateway_manual_review', label: '保留人工复核', type: 'switch', default: true },
+ { key: 'payment_gateway_callback_secret', label: '回调校验口令', type: 'input', default: '', help: '真实网关接入时可直接复用该口令字段。' },
+ { key: 'payment_gateway_help', label: '运维说明', type: 'textarea', default: '未接真实支付网关时,建议使用测试网关或人工确认模式完成联调。' },
+ ],
+ },
+ {
+ key: 'gateways',
+ title: '支付网关',
+ icon: 'fas fa-toggle-on',
+ fields: [
+ { key: 'payment_wechat_enabled', label: '微信支付', type: 'switch', default: false },
+ { key: 'payment_alipay_enabled', label: '支付宝', type: 'switch', default: false },
+ { key: 'payment_paypal_enabled', label: 'PayPal', type: 'switch', default: false },
+ { key: 'payment_stripe_enabled', label: 'Stripe', type: 'switch', default: false },
+ { key: 'payment_balance_enabled', label: '余额支付', type: 'switch', default: false },
+ { key: 'payment_card_pass_enabled', label: '卡密充值', type: 'switch', default: false },
+ { key: 'payment_xunhupay_enabled', label: '虎皮椒', type: 'switch', default: false },
+ { key: 'payment_xhpay_enabled', label: '小火付', type: 'switch', default: false },
+ { key: 'payment_payjs_enabled', label: 'PayJS', type: 'switch', default: false },
+ { key: 'payment_epay_enabled', label: '易支付', type: 'switch', default: false },
+ { key: 'payment_vmq_enabled', label: 'V免签', type: 'switch', default: false },
+ { key: 'payment_codepay_enabled', label: 'Codepay', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'wechat',
+ title: '微信支付配置',
+ icon: 'fab fa-weixin',
+ fields: [
+ { key: 'wechat_merchantid', label: '商户号', type: 'input', default: '' },
+ { key: 'wechat_jsapi', label: 'JSAPI密钥', type: 'input', default: '' },
+ { key: 'wechat_appsecret', label: 'AppSecret', type: 'input', default: '' },
+ { key: 'wechat_cert_path', label: '证书路径', type: 'input', default: '', help: '退款等接口需要,绝对路径。' },
+ { key: 'wechat_key_path', label: '密钥路径', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'alipay',
+ title: '支付宝配置',
+ icon: 'fab fa-alipay',
+ fields: [
+ { key: 'alipay_appid', label: 'AppID', type: 'input', default: '' },
+ { key: 'alipay_privatekey', label: '应用私钥', type: 'textarea', rows: 3, default: '' },
+ { key: 'alipay_publickey', label: '支付宝公钥', type: 'textarea', rows: 3, default: '' },
+ { key: 'alipay_return_url', label: '同步回调地址', type: 'input', default: '' },
+ { key: 'alipay_notify_url', label: '异步回调地址', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'paypal',
+ title: 'PayPal 配置',
+ icon: 'fab fa-paypal',
+ fields: [
+ { key: 'paypal_client_id', label: 'Client ID', type: 'input', default: '' },
+ { key: 'paypal_client_secret', label: 'Client Secret', type: 'input', default: '' },
+ { key: 'paypal_currency', label: '货币', type: 'input', default: 'USD' },
+ { key: 'paypal_sandbox', label: '沙箱模式', type: 'switch', default: true },
+ ],
+ },
+ {
+ key: 'xunhupay',
+ title: '虎皮椒配置',
+ icon: 'fas fa-pepper-hot',
+ fields: [
+ { key: 'xunhupay_appid', label: 'AppID', type: 'input', default: '' },
+ { key: 'xunhupay_appsecret', label: 'AppSecret', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'xhpay',
+ title: '小火付配置',
+ icon: 'fas fa-fire',
+ fields: [
+ { key: 'xhpay_mchid', label: '商户号', type: 'input', default: '' },
+ { key: 'xhpay_alipay_v2', label: '支付宝V2', type: 'input', default: '' },
+ { key: 'xhpay_wx_h5', label: '微信H5', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'payjs',
+ title: 'PayJS 配置',
+ icon: 'fab fa-js',
+ fields: [
+ { key: 'payjs_mchid', label: '商户号', type: 'input', default: '' },
+ { key: 'payjs_key', label: '通信密钥', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'epay',
+ title: '易支付配置',
+ icon: 'fas fa-university',
+ fields: [
+ { key: 'epay_apiurl', label: '接口地址', type: 'input', default: '' },
+ { key: 'epay_pid', label: '商户ID', type: 'input', default: '' },
+ { key: 'epay_key', label: '商户密钥', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'stripe',
+ title: 'Stripe 配置',
+ icon: 'fab fa-stripe',
+ fields: [
+ { key: 'stripe_secret_key', label: 'Secret Key', type: 'input', default: '' },
+ { key: 'stripe_publishable_key', label: 'Publishable Key', type: 'input', default: '' },
+ { key: 'stripe_webhook_secret', label: 'Webhook Secret', type: 'input', default: '', help: 'Stripe Dashboard 配置 Webhook 后获得,用于验签。' },
+ { key: 'stripe_currency', label: '货币', type: 'input', default: 'usd' },
+ { key: 'stripe_test_mode', label: '测试模式', type: 'switch', default: true },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/payment/index.js b/assets/admin/src/schema/sections/payment/index.js
new file mode 100644
index 00000000..16741d18
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/index.js
@@ -0,0 +1,25 @@
+/**
+ * Payment & Paid domain schemas.
+ */
+
+import { paidSchema } from './paid.js'
+import { rechargeSchema } from './recharge.js'
+import { vipSchema } from './vip.js'
+import { revenueSchema } from './revenue.js'
+import { affiliateSchema } from './affiliate.js'
+import { gatewaySchema } from './gateway.js'
+import { orderSchema } from './order.js'
+import { cardPassSchema } from './cardPass.js'
+import { withdrawalSchema } from './withdrawal.js'
+
+export const paymentSchemas = {
+ paid: paidSchema,
+ recharge: rechargeSchema,
+ vip: vipSchema,
+ revenue: revenueSchema,
+ affiliate: affiliateSchema,
+ gateway: gatewaySchema,
+ order: orderSchema,
+ cardPass: cardPassSchema,
+ withdrawal: withdrawalSchema,
+}
diff --git a/assets/admin/src/schema/sections/payment/order.js b/assets/admin/src/schema/sections/payment/order.js
new file mode 100644
index 00000000..7d5e12b7
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/order.js
@@ -0,0 +1,96 @@
+export const orderSchema = {
+ key: 'payment.order',
+ title: '订单设置',
+ icon: 'fas fa-file-invoice',
+ description: '配置订单前缀、有效期、展示字段和状态文案。',
+ defaults: {
+ payment_order_prefix: 'AKSRT',
+ payment_order_expiry: 30,
+ payment_show_user: true,
+ payment_show_paycount: true,
+ payment_no_logged_in: true,
+ payment_cookie_day: 15,
+ payment_no_logged_remind: '',
+ payment_wait_float_s: true,
+ payment_order_max_minutes: 30,
+ payment_mark: '付费',
+ payment_box_position: 'content',
+ payment_free_logged_show: false,
+ payment_show_ip: false,
+ payment_show_device: false,
+ payment_show_order_no: true,
+ payment_show_create_time: true,
+ payment_show_pay_time: true,
+ payment_order_status_pending: '待支付',
+ payment_order_status_paid: '已支付',
+ payment_order_status_completed: '已完成',
+ payment_order_status_cancelled: '已取消',
+ payment_order_status_refunded: '已退款',
+ payment_order_status_refunding: '退款中',
+ payment_order_status_processing: '处理中',
+ payment_service: '',
+ payment_service_icon: '',
+ payment_service_value: '',
+ payment_service_qq: '',
+ payment_service_wechat: '',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '订单基础',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'payment_order_prefix', label: '订单前缀', type: 'input', default: 'AKSRT' },
+ { key: 'payment_order_expiry', label: '订单有效期(分钟)', type: 'number', default: 30, min: 5, max: 1440 },
+ { key: 'payment_show_user', label: '显示用户信息', type: 'switch', default: true },
+ { key: 'payment_show_paycount', label: '显示购买次数', type: 'switch', default: true },
+ { key: 'payment_no_logged_in', label: '未登录提示', type: 'switch', default: true },
+ { key: 'payment_cookie_day', label: 'Cookie有效期(天)', type: 'number', default: 15, min: 1, max: 31, help: '未登录购买 Cookie 有效时间,1-31 天' },
+ { key: 'payment_no_logged_remind', label: '未登录提醒文案', type: 'textarea', default: '', help: '未登录用户查看付费内容时的提醒文案' },
+ { key: 'payment_wait_float_s', label: '订单等待浮动', type: 'switch', default: true },
+ { key: 'payment_order_max_minutes', label: '订单最大支付时间(分钟)', type: 'number', default: 30, min: 5, max: 1440 },
+ ],
+ },
+ {
+ key: 'display',
+ title: '展示设置',
+ icon: 'fas fa-eye',
+ fields: [
+ { key: 'payment_mark', label: '付费标记', type: 'input', default: '付费' },
+ { key: 'payment_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '文章模块上', value: 'box_top' }, { label: '内容顶', value: 'top' }, { label: '内容底', value: 'bottom' }, { label: '模块下', value: 'box_bottom' }] },
+ { key: 'payment_free_logged_show', label: '登录用户显示免费', type: 'switch', default: false },
+ { key: 'payment_show_ip', label: '显示用户IP', type: 'switch', default: false },
+ { key: 'payment_show_device', label: '显示设备信息', type: 'switch', default: false },
+ { key: 'payment_show_order_no', label: '显示订单号', type: 'switch', default: true },
+ { key: 'payment_show_create_time', label: '显示创建时间', type: 'switch', default: true },
+ { key: 'payment_show_pay_time', label: '显示支付时间', type: 'switch', default: true },
+ ],
+ },
+ {
+ key: 'status',
+ title: '状态设置',
+ icon: 'fas fa-tags',
+ fields: [
+ { key: 'payment_order_status_pending', label: '待支付状态文案', type: 'input', default: '待支付' },
+ { key: 'payment_order_status_paid', label: '已支付状态文案', type: 'input', default: '已支付' },
+ { key: 'payment_order_status_completed', label: '已完成状态文案', type: 'input', default: '已完成' },
+ { key: 'payment_order_status_cancelled', label: '已取消状态文案', type: 'input', default: '已取消' },
+ { key: 'payment_order_status_refunded', label: '已退款状态文案', type: 'input', default: '已退款' },
+ { key: 'payment_order_status_refunding', label: '退款中状态文案', type: 'input', default: '退款中' },
+ { key: 'payment_order_status_processing', label: '处理中状态文案', type: 'input', default: '处理中' },
+ ],
+ },
+ {
+ key: 'service',
+ title: '服务设置',
+ icon: 'fas fa-headset',
+ fields: [
+ { key: 'payment_service', label: '客服设置', type: 'textarea', default: '' },
+ { key: 'payment_service_icon', label: '客服图标', type: 'icon', default: '' },
+ { key: 'payment_service_value', label: '客服内容', type: 'input', default: '' },
+ { key: 'payment_service_qq', label: 'QQ客服', type: 'input', default: '' },
+ { key: 'payment_service_wechat', label: '微信客服', type: 'media', default: '' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/payment/paid.js b/assets/admin/src/schema/sections/payment/paid.js
new file mode 100644
index 00000000..ae3511e7
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/paid.js
@@ -0,0 +1,72 @@
+export const paidSchema = {
+ key: 'payment.paid',
+ title: '下载/付费内容中心',
+ icon: 'fas fa-download',
+ description: '统一管理资源下载、付费内容、角色免扣和人工交付说明。',
+ defaults: {
+ payment_paid_post_types: 'post',
+ payment_vip_free_download: false,
+ payment_vip_exempt_roles: '',
+ payment_paid_catalog_note: '',
+ payment_manual_delivery_note: '付费或人工确认内容请在订单备注中记录交付方式。',
+ payment_paid_mark: '付费',
+ payment_currency_symbol: '¥',
+ payment_paid_show_user: true,
+ payment_paid_show_paycount: true,
+ payment_paid_box_position: 'top',
+ payment_download_enable: false,
+ payment_download_remote_s: true,
+ payment_download_verify_nonce: true,
+ payment_download_user_log: 50,
+ payment_download_post_log: 50,
+ payment_download_remote_size: 2048,
+ payment_download_remote_ext: 'zip,rar,7z',
+ },
+ groups: [
+ {
+ key: 'strategy',
+ title: '付费内容策略',
+ icon: 'fas fa-layer-group',
+ fields: [
+ { key: 'payment_paid_post_types', label: '受管内容类型', type: 'input', default: 'post', help: '逗号分隔,例如 post,course,doc' },
+ { key: 'payment_vip_free_download', label: '角色白名单免扣下载', type: 'switch', default: false },
+ { key: 'payment_vip_exempt_roles', label: '免扣角色列表', type: 'input', default: '', help: '逗号分隔的 WordPress role slug,例如 administrator,editor,vip' },
+ ],
+ },
+ {
+ key: 'delivery',
+ title: '交付说明',
+ icon: 'fas fa-receipt',
+ fields: [
+ { key: 'payment_paid_catalog_note', label: '前台付费提示', type: 'textarea', default: '', help: '显示在资源/付费内容页的运营说明。' },
+ { key: 'payment_manual_delivery_note', label: '人工交付备注', type: 'textarea', default: '付费或人工确认内容请在订单备注中记录交付方式。', help: '人工确认或离线交付流程的标准说明。' },
+ ],
+ },
+ {
+ key: 'display',
+ title: '展示设置',
+ icon: 'fas fa-eye',
+ fields: [
+ { key: 'payment_paid_mark', label: '付费标记', type: 'input', default: '付费' },
+ { key: 'payment_currency_symbol', label: '货币符号', type: 'input', default: '¥', help: '商品价格前缀,如 ¥、$、¥' },
+ { key: 'payment_paid_show_user', label: '显示购买用户', type: 'switch', default: true },
+ { key: 'payment_paid_show_paycount', label: '显示购买次数', type: 'switch', default: true },
+ { key: 'payment_paid_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '文章模块上', value: 'box_top' }, { label: '内容顶', value: 'top' }, { label: '内容底', value: 'bottom' }, { label: '模块下', value: 'box_bottom' }] },
+ ],
+ },
+ {
+ key: 'download',
+ title: '下载设置',
+ icon: 'fas fa-cloud-download-alt',
+ fields: [
+ { key: 'payment_download_enable', label: '启用下载功能', type: 'switch', default: false },
+ { key: 'payment_download_remote_s', label: '远程文件下载', type: 'switch', default: true, dependsOn: { key: 'payment_download_enable', value: true } },
+ { key: 'payment_download_verify_nonce', label: '下载鉴权', type: 'switch', default: true, dependsOn: { key: 'payment_download_enable', value: true } },
+ { key: 'payment_download_user_log', label: '用户下载记录条数', type: 'number', default: 50, min: 0, max: 9999, help: '0 为不保存', dependsOn: { key: 'payment_download_enable', value: true } },
+ { key: 'payment_download_post_log', label: '文章下载记录条数', type: 'number', default: 50, min: 0, max: 9999, help: '0 为不保存', dependsOn: { key: 'payment_download_enable', value: true } },
+ { key: 'payment_download_remote_size', label: '远程文件大小限制(KB)', type: 'number', default: 2048, min: 1, max: 999999, dependsOn: { key: 'payment_download_remote_s', value: true } },
+ { key: 'payment_download_remote_ext', label: '远程文件格式限制', type: 'input', default: 'zip,rar,7z', help: '逗号分隔扩展名', dependsOn: { key: 'payment_download_remote_s', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/payment/recharge.js b/assets/admin/src/schema/sections/payment/recharge.js
new file mode 100644
index 00000000..2ef3c672
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/recharge.js
@@ -0,0 +1,94 @@
+export const rechargeSchema = {
+ key: 'payment.recharge',
+ title: '余额充值',
+ icon: 'fas fa-charging-station',
+ description: '配置测试充值、手动确认和充值套餐结构。',
+ defaults: {
+ payment_recharge_enabled: false,
+ payment_recharge_mode: 'manual_review',
+ payment_recharge_desc: '',
+ payment_recharge_auto_to_balance: true,
+ payment_recharge_packages: '30|30|入门包\n98|108|推荐包',
+ payment_recharge_help: '当前支持人工确认与测试联调;接入真实网关时复用同一组字段。',
+ payment_recharge_product: '',
+ payment_recharge_price: 0,
+ payment_recharge_tag: '元',
+ payment_recharge_tag_class: '',
+ payment_recharge_custom_s: true,
+ payment_recharge_custom_limit: 9999,
+ payment_balance_transfer_s: false,
+ payment_balance_transfer_service_charge: 0,
+ payment_balance_transfer_desc: '',
+ payment_recharge_pass_charge_s: false,
+ payment_recharge_pass_charge_desc: '',
+ payment_recharge_pass_charge_only_password: false,
+ payment_recharge_exchange_s: false,
+ payment_recharge_exchange_rate: 100,
+ payment_recharge_exchange_desc: '',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '充值开关',
+ icon: 'fas fa-wallet',
+ fields: [
+ { key: 'payment_recharge_enabled', label: '启用余额充值结构', type: 'switch', default: false },
+ { key: 'payment_recharge_mode', label: '充值模式', type: 'radio', default: 'manual_review', options: [{ label: '人工确认', value: 'manual_review' }, { label: '测试网关', value: 'test_gateway' }], dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ { key: 'payment_recharge_desc', label: '充值说明', type: 'textarea', default: '', dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ { key: 'payment_recharge_auto_to_balance', label: '自动到账到余额', type: 'switch', default: true, dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'packages',
+ title: '充值套餐',
+ icon: 'fas fa-box-open',
+ fields: [
+ { key: 'payment_recharge_packages', label: '套餐列表', type: 'textarea', default: '30|30|入门包\n98|108|推荐包', help: '每行一个套餐:支付金额|到账金额|标题', dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ { key: 'payment_recharge_help', label: '帮助文案', type: 'textarea', default: '当前支持人工确认与测试联调;接入真实网关时复用同一组字段。', dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'product',
+ title: '充值产品',
+ icon: 'fas fa-box',
+ fields: [
+ { key: 'payment_recharge_product', label: '充值产品', type: 'input', default: '', dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ { key: 'payment_recharge_price', label: '充值价格', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ { key: 'payment_recharge_tag', label: '价格标签', type: 'input', default: '元', dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ { key: 'payment_recharge_tag_class', label: '标签样式', type: 'input', default: '', dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ { key: 'payment_recharge_custom_s', label: '自定义金额', type: 'switch', default: true, dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ { key: 'payment_recharge_custom_limit', label: '自定义金额限制', type: 'number', default: 9999, min: 1, max: 99999, dependsOn: { key: 'payment_recharge_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'transfer',
+ title: '余额转账',
+ icon: 'fas fa-exchange-alt',
+ fields: [
+ { key: 'payment_balance_transfer_s', label: '启用余额转账', type: 'switch', default: false },
+ { key: 'payment_balance_transfer_service_charge', label: '转账手续费(%)', type: 'number', default: 0, min: 0, max: 50, dependsOn: { key: 'payment_balance_transfer_s', value: true } },
+ { key: 'payment_balance_transfer_desc', label: '转账说明', type: 'textarea', default: '', dependsOn: { key: 'payment_balance_transfer_s', value: true } },
+ ],
+ },
+ {
+ key: 'pass-charge',
+ title: '充值卡免扣',
+ icon: 'fas fa-ticket-alt',
+ fields: [
+ { key: 'payment_recharge_pass_charge_s', label: '启用充值卡免扣', type: 'switch', default: false },
+ { key: 'payment_recharge_pass_charge_desc', label: '免扣说明', type: 'textarea', default: '', dependsOn: { key: 'payment_recharge_pass_charge_s', value: true } },
+ { key: 'payment_recharge_pass_charge_only_password', label: '仅密码免扣', type: 'switch', default: false, dependsOn: { key: 'payment_recharge_pass_charge_s', value: true } },
+ ],
+ },
+ {
+ key: 'exchange',
+ title: '积分兑换',
+ icon: 'fas fa-coins',
+ fields: [
+ { key: 'payment_recharge_exchange_s', label: '启用积分兑换', type: 'switch', default: false },
+ { key: 'payment_recharge_exchange_rate', label: '兑换比例', type: 'number', default: 100, min: 1, max: 10000, help: 'N 积分 = 1 元', dependsOn: { key: 'payment_recharge_exchange_s', value: true } },
+ { key: 'payment_recharge_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'payment_recharge_exchange_s', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/payment/revenue.js b/assets/admin/src/schema/sections/payment/revenue.js
new file mode 100644
index 00000000..125b777c
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/revenue.js
@@ -0,0 +1,44 @@
+export const revenueSchema = {
+ key: 'payment.revenue',
+ title: '创作分成',
+ icon: 'fas fa-chart-line',
+ description: '配置作者分成比例、结算门槛和结算周期。',
+ defaults: {
+ payment_revenue_enabled: false,
+ payment_revenue_share_ratio: 70,
+ payment_revenue_ratio_vip1: 0,
+ payment_revenue_ratio_vip2: 0,
+ payment_revenue_settlement_threshold: 100,
+ payment_revenue_cycle_days: 30,
+ payment_revenue_points_rule: '',
+ payment_revenue_note: '分成策略和台账结构已就绪,订单入账后自动按比例结算。',
+ payment_revenue_desc_details: '',
+ payment_revenue_desc_page: '',
+ },
+ groups: [
+ {
+ key: 'policy',
+ title: '分成策略',
+ icon: 'fas fa-percentage',
+ fields: [
+ { key: 'payment_revenue_enabled', label: '启用创作分成结构', type: 'switch', default: false },
+ { key: 'payment_revenue_share_ratio', label: '作者分成比例(%)', type: 'number', default: 70, min: 0, max: 100, dependsOn: { key: 'payment_revenue_enabled', value: true } },
+ { key: 'payment_revenue_ratio_vip1', label: 'VIP1分成比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'payment_revenue_enabled', value: true } },
+ { key: 'payment_revenue_ratio_vip2', label: 'VIP2分成比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'payment_revenue_enabled', value: true } },
+ { key: 'payment_revenue_settlement_threshold', label: '最小结算金额', type: 'number', default: 100, min: 0, max: 100000, dependsOn: { key: 'payment_revenue_enabled', value: true } },
+ { key: 'payment_revenue_cycle_days', label: '结算周期(天)', type: 'number', default: 30, min: 1, max: 365, dependsOn: { key: 'payment_revenue_enabled', value: true } },
+ { key: 'payment_revenue_points_rule', label: '积分规则', type: 'textarea', default: '', dependsOn: { key: 'payment_revenue_enabled', value: true } },
+ { key: 'payment_revenue_note', label: '结算备注', type: 'textarea', default: '分成策略和台账结构已就绪,订单入账后自动按比例结算。', dependsOn: { key: 'payment_revenue_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'display',
+ title: '展示设置',
+ icon: 'fas fa-file-alt',
+ fields: [
+ { key: 'payment_revenue_desc_details', label: '收益明细文案', type: 'textarea', default: '', dependsOn: { key: 'payment_revenue_enabled', value: true } },
+ { key: 'payment_revenue_desc_page', label: '收益说明页', type: 'input', default: '', dependsOn: { key: 'payment_revenue_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/payment/vip.js b/assets/admin/src/schema/sections/payment/vip.js
new file mode 100644
index 00000000..c0091770
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/vip.js
@@ -0,0 +1,102 @@
+export const vipSchema = {
+ key: 'payment.vip',
+ title: 'VIP会员',
+ icon: 'fas fa-crown',
+ description: '配置会员名称、套餐和付费权益说明。',
+ defaults: {
+ payment_vip_enabled: false,
+ payment_vip_name: 'VIP会员',
+ payment_vip_badge_text: 'VIP',
+ payment_vip_desc: '',
+ payment_vip_packages: '30|30|月卡\n365|365|年卡',
+ payment_vip_help: '会员权益以免扣下载和身份展示为主,开通后立即生效。',
+ payment_vip_download_limit: 0,
+ payment_vip_download_speed: 0,
+ payment_vip_discount: 100,
+ payment_vip_pass_charge: false,
+ payment_vip_pass_charge_desc: '',
+ payment_vip_renew: true,
+ payment_vip_renew_desc: '',
+ payment_vip_renew_price_type: 'same',
+ payment_vip_renew_discount: 100,
+ payment_vip_renew_reduce: 0,
+ payment_vip_upgrade: true,
+ payment_vip_upgrade_desc: '',
+ payment_vip_upgrade_product: '',
+ payment_vip_month_price: 0,
+ payment_vip_year_price: 0,
+ payment_vip_permanent_price: 0,
+ payment_vip_unit_price: 0,
+ payment_vip_unit_tag: '元',
+ payment_vip_jump_price: 0,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '会员基础',
+ icon: 'fas fa-user-shield',
+ fields: [
+ { key: 'payment_vip_enabled', label: '启用会员结构', type: 'switch', default: false },
+ { key: 'payment_vip_name', label: '会员名称', type: 'input', default: 'VIP会员', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_badge_text', label: '会员徽标文案', type: 'input', default: 'VIP', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_desc', label: '会员说明', type: 'textarea', default: '', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'packages',
+ title: '会员套餐',
+ icon: 'fas fa-gem',
+ fields: [
+ { key: 'payment_vip_packages', label: '套餐列表', type: 'textarea', default: '30|30|月卡\n365|365|年卡', help: '每行一个套餐:价格|时长(天)|标题', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_help', label: '权益说明', type: 'textarea', default: '会员权益以免扣下载和身份展示为主,开通后立即生效。', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'benefit',
+ title: '会员权益',
+ icon: 'fas fa-gift',
+ fields: [
+ { key: 'payment_vip_download_limit', label: '下载限制', type: 'number', default: 0, min: 0, max: 9999, help: '0为不限制', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_download_speed', label: '下载速度(KB/s)', type: 'number', default: 0, min: 0, max: 99999, help: '0为不限制', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_discount', label: '折扣(%)', type: 'number', default: 100, min: 1, max: 100, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_pass_charge', label: '积分充值免扣', type: 'switch', default: false, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_pass_charge_desc', label: '积分充值说明', type: 'textarea', default: '', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'renew',
+ title: '续费设置',
+ icon: 'fas fa-sync-alt',
+ fields: [
+ { key: 'payment_vip_renew', label: '允许续费', type: 'switch', default: true, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_renew_desc', label: '续费说明', type: 'textarea', default: '', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_renew_price_type', label: '续费价格类型', type: 'radio', default: 'same', options: [{ label: '相同价格', value: 'same' }, { label: '折扣价格', value: 'discount' }], dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_renew_discount', label: '续费折扣(%)', type: 'number', default: 100, min: 1, max: 100, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_renew_reduce', label: '续费减免金额', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'upgrade',
+ title: '升级设置',
+ icon: 'fas fa-arrow-up',
+ fields: [
+ { key: 'payment_vip_upgrade', label: '允许升级', type: 'switch', default: true, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_upgrade_desc', label: '升级说明', type: 'textarea', default: '', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_upgrade_product', label: '升级产品', type: 'input', default: '', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'price',
+ title: '价格设置',
+ icon: 'fas fa-yen-sign',
+ fields: [
+ { key: 'payment_vip_month_price', label: '月卡价格', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_year_price', label: '年卡价格', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_permanent_price', label: '永久价格', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_unit_price', label: '单位价格', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_unit_tag', label: '单位标签', type: 'input', default: '元', dependsOn: { key: 'payment_vip_enabled', value: true } },
+ { key: 'payment_vip_jump_price', label: '跳转价格', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'payment_vip_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/payment/withdrawal.js b/assets/admin/src/schema/sections/payment/withdrawal.js
new file mode 100644
index 00000000..a5cfc03b
--- /dev/null
+++ b/assets/admin/src/schema/sections/payment/withdrawal.js
@@ -0,0 +1,23 @@
+export const withdrawalSchema = {
+ key: 'payment.withdrawal',
+ title: '提现管理',
+ icon: 'fas fa-hand-holding-usd',
+ description: '配置提现门槛、手续费比例和提现说明。',
+ defaults: {
+ pay_rebate_withdraw_lowest_money: 50,
+ withdraw_service_charge: 0,
+ pay_rebate_withdraw_text_details: '',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '提现设置',
+ icon: 'fas fa-sliders-h',
+ fields: [
+ { key: 'pay_rebate_withdraw_lowest_money', label: '最低提现金额', type: 'number', default: 50, min: 1, max: 99999 },
+ { key: 'withdraw_service_charge', label: '提现手续费(%)', type: 'number', default: 0, min: 0, max: 50 },
+ { key: 'pay_rebate_withdraw_text_details', label: '提现说明', type: 'textarea', default: '' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/permissions/index.js b/assets/admin/src/schema/sections/permissions/index.js
new file mode 100644
index 00000000..d7732a91
--- /dev/null
+++ b/assets/admin/src/schema/sections/permissions/index.js
@@ -0,0 +1,316 @@
+/**
+ * Permissions & Security domain schemas.
+ * Section keys must stay aligned with the router and PHP schema provider.
+ */
+
+export const permissionsSchemas = {
+ /**
+ * Login / captcha / access guard settings
+ */
+ login: {
+ key: 'permissions.login',
+ title: '登录权限',
+ icon: 'fas fa-shield-alt',
+ description: '管理登录、注册、评论等场景的人机验证与访问限制。',
+ defaults: {
+ enabled: false,
+ active_provider: '',
+ mode: 'always',
+ disabled_scenarios: [],
+ tencent: {},
+ aliyun: {},
+ cloudflare: {},
+ geetest: {},
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '基础设置',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'enabled', label: '启用人机验证', type: 'switch', help: '开启后将在指定场景要求用户完成人机验证' },
+ {
+ key: 'active_provider',
+ label: '当前服务商',
+ type: 'radio',
+ buttonStyle: false,
+ options: [
+ { label: '腾讯云 Captcha', value: 'tencent' },
+ { label: '阿里云 CAPTCHA 2.0', value: 'aliyun' },
+ { label: 'Cloudflare Turnstile', value: 'cloudflare' },
+ { label: '极验 GeeTest', value: 'geetest' },
+ ],
+ dependsOn: 'enabled',
+ },
+ {
+ key: 'mode',
+ label: '触发模式',
+ type: 'radio',
+ buttonStyle: false,
+ options: [
+ { label: '始终显示', value: 'always' },
+ ],
+ dependsOn: 'enabled',
+ },
+ {
+ key: 'disabled_scenarios',
+ label: '停用场景',
+ type: 'checkbox',
+ help: '勾选后表示该场景不启用人机验证;未勾选的场景会启用。',
+ dependsOn: 'enabled',
+ options: [
+ { label: '发表评论', value: 'comment' },
+ { label: '用户登录', value: 'login' },
+ { label: '用户注册', value: 'register' },
+ { label: '绑定第三方', value: 'bind' },
+ { label: '找回密码', value: 'lostpassword' },
+ { label: '修改密码', value: 'change_password' },
+ { label: '更改邮箱', value: 'change_email' },
+ { label: '邮箱验证', value: 'verify_email' },
+ { label: '资源下载', value: 'download' },
+ { label: '每日签到', value: 'signin' },
+ ],
+ },
+ ],
+ },
+ {
+ key: 'providers',
+ title: '服务商配置',
+ icon: 'fas fa-server',
+ description: '按当前选择的服务商填写对应密钥。',
+ fields: [
+ {
+ key: 'provider_hint',
+ label: '配置说明',
+ type: 'notice',
+ alertType: 'info',
+ alertContent: '下方会渲染当前验证码服务商的专属配置面板。保存仍通过统一 schema contract 进行。',
+ dependsOn: 'enabled',
+ },
+ { key: 'tencent', label: '腾讯云配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'tencent' } },
+ { key: 'aliyun', label: '阿里云配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'aliyun' } },
+ { key: 'cloudflare', label: 'Cloudflare 配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'cloudflare' } },
+ { key: 'geetest', label: '极验配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'geetest' } },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Capability matrix metadata contract.
+ * The live page is still orchestration-heavy custom UI for now.
+ */
+ capabilities: {
+ key: 'permissions.capabilities',
+ title: '基本权限 / 能力矩阵',
+ icon: 'fas fa-user-shield',
+ description: '统一管理用户治理、投稿和评论的角色能力矩阵。',
+ defaults: {},
+ groups: [],
+ },
+
+ /**
+ * Moderation settings
+ */
+ moderation: {
+ key: 'permissions.moderation',
+ title: '审核和风控',
+ icon: 'fas fa-gavel',
+ description: '管理评论审核、反垃圾、限流与风控配置。',
+ defaults: {
+ enabled: false,
+ akismet_enabled: false,
+ akismet_site_url: '',
+ akismet_blog_lang: 'zh-CN',
+ akismet_api_key: '',
+ ai_enabled: false,
+ ai_moderate_comments: true,
+ ai_moderate_background: true,
+ auto_approve_low_risk: false,
+ auto_reject_high_risk: false,
+ low_risk_max_score: 35,
+ high_risk_min_score: 80,
+ blocked_keywords: '',
+ rate_limit_enabled: false,
+ rate_limit_min_interval: 8,
+ rate_limit_max_per_hour: 15,
+ geoip_enabled: false,
+ geoip_block_countries: [],
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '审核总开关',
+ icon: 'fas fa-power-off',
+ fields: [
+ { key: 'enabled', label: '启用审核系统', type: 'switch', help: '关闭后评论将不经过主题审核链路。' },
+ ],
+ },
+ {
+ key: 'akismet',
+ title: 'Akismet',
+ icon: 'fas fa-filter',
+ fields: [
+ { key: 'akismet_enabled', label: '启用 Akismet', type: 'switch', dependsOn: 'enabled' },
+ { key: 'akismet_site_url', label: 'Site URL', type: 'input', placeholder: 'https://your-site.com', dependsOn: { key: 'akismet_enabled', value: true } },
+ { key: 'akismet_blog_lang', label: '博客语言', type: 'input', placeholder: 'zh-CN', dependsOn: { key: 'akismet_enabled', value: true } },
+ { key: 'akismet_api_key', label: 'API Key', type: 'input', inputType: 'password', placeholder: 'Akismet API Key', dependsOn: { key: 'akismet_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'ai',
+ title: 'AI 审核',
+ icon: 'fas fa-robot',
+ fields: [
+ { key: 'ai_enabled', label: '启用 AI 审核', type: 'switch', dependsOn: 'enabled' },
+ { key: 'ai_api_hint', label: '审核 API 配置', type: 'notice', alertType: 'info', alertContent: 'API 接入(百度/阿里云等)在「扩展 → API内容审核」中统一配置,此处仅控制审核范围和策略。', dependsOn: { key: 'ai_enabled', value: true } },
+ { key: 'ai_moderate_comments', label: '用户评论', type: 'switch', dependsOn: { key: 'ai_enabled', value: true } },
+ { key: 'ai_moderate_background', label: '用户资料背景', type: 'switch', dependsOn: { key: 'ai_enabled', value: true } },
+ { key: 'auto_approve_low_risk', label: '低风险自动通过', type: 'switch', dependsOn: { key: 'ai_enabled', value: true } },
+ { key: 'auto_reject_high_risk', label: '高风险自动拒绝', type: 'switch', dependsOn: { key: 'ai_enabled', value: true } },
+ { key: 'low_risk_max_score', label: '低风险上限', type: 'number', min: 0, max: 100, dependsOn: { key: 'ai_enabled', value: true } },
+ { key: 'high_risk_min_score', label: '高风险下限', type: 'number', min: 0, max: 100, dependsOn: { key: 'ai_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'keywords',
+ title: '关键词拦截',
+ icon: 'fas fa-ban',
+ fields: [
+ { key: 'blocked_keywords', label: '屏蔽关键词列表', type: 'textarea', rows: 4, placeholder: '每行一个关键词,也支持 /regex/i 格式。命中后会进入待审核并记录原因。', dependsOn: 'enabled' },
+ ],
+ },
+ {
+ key: 'rate-limit',
+ title: '频率限制',
+ icon: 'fas fa-stopwatch',
+ fields: [
+ { key: 'rate_limit_enabled', label: '启用频率限制', type: 'switch', dependsOn: 'enabled' },
+ { key: 'rate_limit_min_interval', label: '最小间隔(秒)', type: 'number', min: 1, max: 300, dependsOn: { key: 'rate_limit_enabled', value: true } },
+ { key: 'rate_limit_max_per_hour', label: '每小时上限', type: 'number', min: 1, max: 200, dependsOn: { key: 'rate_limit_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'geoip',
+ title: 'GeoIP 拦截',
+ icon: 'fas fa-globe-asia',
+ fields: [
+ { key: 'geoip_enabled', label: '启用 GeoIP 拦截', type: 'switch', dependsOn: 'enabled' },
+ { key: 'geoip_hint', label: 'IP 数据源', type: 'notice', alertType: 'info', alertContent: '复用「扩展 → IP归属地」中配置的数据源和 API Key。', dependsOn: { key: 'geoip_enabled', value: true } },
+ { key: 'geoip_block_countries', label: '屏蔽国家/地区', type: 'custom', dependsOn: { key: 'geoip_enabled', value: true } },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Comment permissions
+ */
+ comment: {
+ key: 'permissions.comment',
+ title: '评论权限',
+ icon: 'fas fa-comment-dots',
+ description: '控制评论功能的开关和权限要求。',
+ defaults: {
+ comment_allow: true,
+ comment_require_login: false,
+ comment_nested_depth: 3,
+ comment_allow_reply: true,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '评论基础',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'comment_allow', label: '允许评论', type: 'switch', help: '全局开启评论功能' },
+ { key: 'comment_require_login', label: '需登录评论', type: 'switch', help: '只有登录用户才能评论' },
+ { key: 'comment_nested_depth', label: '嵌套层级', type: 'slider', min: 1, max: 5, default: 3 },
+ { key: 'comment_allow_reply', label: '允许回复', type: 'switch' },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Attachment permissions
+ */
+ attachment: {
+ key: 'permissions.attachment',
+ title: '附件权限',
+ icon: 'fas fa-paperclip',
+ description: '控制文件上传和附件下载权限。',
+ defaults: {
+ allow_avatar_upload: true,
+ allow_cover_upload: true,
+ allow_background_upload: false,
+ avatar_max_size: 2,
+ allow_submit_images: true,
+ submit_image_max_size: 5,
+ allow_comment_images: false,
+ comment_image_max_size: 2,
+ allow_resource_upload: true,
+ resource_max_size: 50,
+ },
+ groups: [
+ {
+ key: 'profile',
+ title: '头像与封面',
+ icon: 'fas fa-user-circle',
+ fields: [
+ { key: 'allow_avatar_upload', label: '允许用户上传头像', type: 'switch' },
+ { key: 'allow_cover_upload', label: '允许用户上传封面图', type: 'switch' },
+ { key: 'allow_background_upload', label: '允许用户上传背景图', type: 'switch' },
+ { key: 'avatar_max_size', label: '头像/封面大小上限 (MB)', type: 'number', min: 1, max: 10 },
+ ],
+ },
+ {
+ key: 'content',
+ title: '内容上传',
+ icon: 'fas fa-images',
+ fields: [
+ { key: 'allow_submit_images', label: '允许投稿插入图片', type: 'switch' },
+ { key: 'submit_image_max_size', label: '投稿图片大小上限 (MB)', type: 'number', min: 1, max: 20 },
+ { key: 'allow_comment_images', label: '允许评论插入图片', type: 'switch' },
+ { key: 'comment_image_max_size', label: '评论图片大小上限 (MB)', type: 'number', min: 1, max: 10 },
+ ],
+ },
+ {
+ key: 'resource',
+ title: '资源附件',
+ icon: 'fas fa-file-archive',
+ fields: [
+ { key: 'allow_resource_upload', label: '允许资源附件上传', type: 'switch' },
+ { key: 'resource_max_size', label: '资源附件大小上限 (MB)', type: 'number', min: 1, max: 200 },
+ ],
+ },
+ ],
+ },
+
+ /**
+ * Download permissions
+ */
+ download: {
+ key: 'permissions.download',
+ title: '下载权限',
+ icon: 'fas fa-download',
+ description: '控制资源下载的权限和限制。',
+ defaults: {
+ download_require_login: false,
+ download_daily_limit: 0,
+ download_captcha: false,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '下载设置',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'download_require_login', label: '需登录下载', type: 'switch' },
+ { key: 'download_daily_limit', label: '每日下载限制', type: 'number', min: 0, help: '0 表示不限制' },
+ { key: 'download_captcha', label: '下载验证码', type: 'switch', help: '下载前需要验证码' },
+ ],
+ },
+ ],
+ },
+};
diff --git a/assets/admin/src/schema/sections/post-list/comment.js b/assets/admin/src/schema/sections/post-list/comment.js
new file mode 100644
index 00000000..77d42cb4
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/comment.js
@@ -0,0 +1,100 @@
+export const commentSchema = {
+ key: 'post_list.comment',
+ title: '评论设置',
+ icon: 'fas fa-comment-dots',
+ description: '配置评论区的排序、分页、权限、工具栏与互动方式。',
+ defaults: {
+ comment_order: 'asc',
+ comment_per_page: 20,
+ comment_pagination_type: 'numbered',
+ comment_paging_ajax_ias_s: true,
+ comment_paging_ajax_ias_max: 3,
+ comment_nested_depth: 3,
+ comment_avatar_size: 48,
+ comment_require_login: false,
+ comment_allow_reply: true,
+ comment_close_comments: false,
+ comment_edit_window: 10,
+ comment_smilie: true,
+ comment_code: true,
+ comment_img: true,
+ comment_upload_img: false,
+ comment_like_s: true,
+ comment_quick_s: true,
+ comment_city_s: true,
+ comment_author_tag: true,
+ comment_corderby: true,
+ comment_empty_text: '暂无评论,来做第一个评论的人吧~',
+ comment_title: '评论',
+ comment_submit_text: '提交评论',
+ comment_text: '说点什么吧...',
+ },
+ groups: [
+ {
+ key: 'display',
+ title: '评论展示',
+ icon: 'fas fa-comment',
+ fields: [
+ { key: 'comment_order', label: '评论排序', type: 'radio', default: 'asc', options: [
+ { label: '正序 (旧→新)', value: 'asc' },
+ { label: '倒序 (新→旧)', value: 'desc' },
+ ]},
+ { key: 'comment_per_page', label: '每页评论数', type: 'number', default: 20, min: 5, max: 100 },
+ { key: 'comment_pagination_type', label: '分页方式', type: 'radio', default: 'numbered', options: [
+ { label: '翻页', value: 'numbered' },
+ { label: '加载更多', value: 'load_more' },
+ { label: '无限滚动', value: 'infinite' },
+ ]},
+ { key: 'comment_paging_ajax_ias_s', label: '评论自动加载', type: 'switch', default: true, help: '评论无限滚动时自动触发加载' },
+ { key: 'comment_paging_ajax_ias_max', label: '评论最大加载页数', type: 'number', default: 3, min: 0, max: 10, help: '0 = 不限' },
+ { key: 'comment_nested_depth', label: '回复嵌套深度', type: 'number', default: 3, min: 1, max: 10 },
+ { key: 'comment_avatar_size', label: '头像大小 (px)', type: 'number', default: 48, min: 24, max: 96 },
+ ],
+ },
+ {
+ key: 'permissions',
+ title: '评论权限',
+ icon: 'fas fa-lock',
+ fields: [
+ { key: 'comment_require_login', label: '评论需要登录', type: 'switch', default: false },
+ { key: 'comment_allow_reply', label: '允许回复', type: 'switch', default: true },
+ { key: 'comment_close_comments', label: '关闭评论', type: 'switch', default: false },
+ { key: 'comment_edit_window', label: '作者自助编辑时限 (分钟)', type: 'number', default: 10, min: 0, max: 1440 },
+ ],
+ },
+ {
+ key: 'toolbar',
+ title: '评论工具栏',
+ icon: 'fas fa-toolbox',
+ fields: [
+ { key: 'comment_smilie', label: 'Emoji 表情', type: 'switch', default: true },
+ { key: 'comment_code', label: '代码插入', type: 'switch', default: true },
+ { key: 'comment_img', label: '图片按钮', type: 'switch', default: true },
+ { key: 'comment_upload_img', label: '评论图片上传', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'interaction',
+ title: '互动功能',
+ icon: 'fas fa-heart',
+ fields: [
+ { key: 'comment_like_s', label: '评论点赞', type: 'switch', default: true },
+ { key: 'comment_quick_s', label: '快捷回复', type: 'switch', default: true },
+ { key: 'comment_city_s', label: '评论城市', type: 'switch', default: true },
+ { key: 'comment_author_tag', label: '作者标签', type: 'switch', default: true },
+ { key: 'comment_corderby', label: '评论排序切换', type: 'switch', default: true, help: '前台显示最新/最早/最热切换按钮' },
+ ],
+ },
+ {
+ key: 'text',
+ title: '文案配置',
+ icon: 'fas fa-font',
+ fields: [
+ { key: 'comment_empty_text', label: '空评论提示', type: 'input', default: '暂无评论,来做第一个评论的人吧~' },
+ { key: 'comment_title', label: '评论区标题', type: 'input', default: '评论' },
+ { key: 'comment_submit_text', label: '提交按钮文案', type: 'input', default: '提交评论' },
+ { key: 'comment_text', label: '评论框提示', type: 'input', default: '说点什么吧...' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/post-list/feature.js b/assets/admin/src/schema/sections/post-list/feature.js
new file mode 100644
index 00000000..17d8ebe7
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/feature.js
@@ -0,0 +1,25 @@
+export const featureSchema = {
+ key: 'post_list.feature',
+ title: '文章功能',
+ icon: 'fas fa-magic',
+ description: '配置文章页的互动功能开关。',
+ defaults: {
+ feature_pdf_export: false,
+ feature_print: true,
+ feature_word_count: true,
+ feature_reading_time: true,
+ },
+ groups: [
+ {
+ key: 'tools',
+ title: '工具功能',
+ icon: 'fas fa-tools',
+ fields: [
+ { key: 'feature_print', label: '打印文章', type: 'switch', default: true },
+ { key: 'feature_word_count', label: '字数统计', type: 'switch', default: true },
+ { key: 'feature_reading_time', label: '预计阅读时间', type: 'switch', default: true },
+ { key: 'feature_pdf_export', label: 'PDF 导出', type: 'switch', default: false, help: '实验性功能' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/post-list/filter.js b/assets/admin/src/schema/sections/post-list/filter.js
new file mode 100644
index 00000000..d4606d30
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/filter.js
@@ -0,0 +1,110 @@
+export const filterSchema = {
+ key: 'post_list.filter',
+ title: '高级筛选',
+ icon: 'fas fa-filter',
+ description: '为文章列表配置筛选栏、排序方式和分类筛选功能。',
+ defaults: {
+ filter_enabled: false,
+ filter_categories: true,
+ filter_tags: false,
+ filter_sort: true,
+ filter_ajax_enabled: true,
+ filter_cat_show_cat: true,
+ filter_cat_show_tag: false,
+ filter_tag_show_cat: true,
+ filter_tag_show_tag: false,
+ filter_topics_show_cat: true,
+ filter_topics_show_tag: false,
+ custom_filter_show: ['cat', 'tag'],
+ custom_filter: [],
+ cat_custom_filter: [],
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '筛选基础',
+ icon: 'fas fa-filter',
+ fields: [
+ { key: 'filter_enabled', label: '启用筛选栏', type: 'switch', default: false },
+ { key: 'custom_filter_show', label: '筛选显示页面', type: 'checkbox', default: ['cat', 'tag'], options: [{ label: '分类页', value: 'cat' }, { label: '标签页', value: 'tag' }, { label: '专题页', value: 'topics' }], help: '选择在哪些页面显示筛选栏', dependsOn: { key: 'filter_enabled', value: true } },
+ { key: 'filter_categories', label: '分类筛选', type: 'switch', default: true, dependsOn: { key: 'filter_enabled', value: true } },
+ { key: 'filter_tags', label: '标签筛选', type: 'switch', default: false, dependsOn: { key: 'filter_enabled', value: true } },
+ { key: 'filter_sort', label: '排序选项', type: 'switch', default: true, dependsOn: { key: 'filter_enabled', value: true } },
+ { key: 'filter_ajax_enabled', label: 'AJAX 无刷新筛选', type: 'switch', default: true, help: '启用后分类/标签/排序切换将通过 AJAX 加载,无需刷新页面', dependsOn: { key: 'filter_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'ajax',
+ title: 'AJAX 筛选维度',
+ icon: 'fas fa-layer-group',
+ fields: [
+ { key: 'filter_cat_show_cat', label: '分类页:显示分类筛选', type: 'switch', default: true },
+ { key: 'filter_cat_show_tag', label: '分类页:显示标签筛选', type: 'switch', default: false },
+ { key: 'filter_tag_show_cat', label: '标签页:显示分类筛选', type: 'switch', default: true },
+ { key: 'filter_tag_show_tag', label: '标签页:显示标签筛选', type: 'switch', default: false },
+ { key: 'filter_topics_show_cat', label: '专题页:显示分类筛选', type: 'switch', default: true },
+ { key: 'filter_topics_show_tag', label: '专题页:显示标签筛选', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'custom_filter',
+ title: '自定义筛选维度',
+ icon: 'fas fa-sliders-h',
+ fields: [
+ {
+ key: 'custom_filter',
+ label: '筛选维度定义',
+ type: 'repeater',
+ default: [],
+ addText: '添加筛选维度',
+ help: '每项定义一个筛选维度(如"类型""地区""年份"),及其可选值列表',
+ fields: [
+ { key: 'name', label: '维度名称', type: 'input', default: '', placeholder: '如:类型' },
+ { key: 'key', label: '维度标识', type: 'input', default: '', placeholder: '如:type(英文下划线)' },
+ {
+ key: 'vals',
+ label: '可选值',
+ type: 'repeater',
+ default: [],
+ addText: '添加可选值',
+ fields: [
+ { key: 'key', label: '标识', type: 'input', default: '', placeholder: '如:action' },
+ { key: 'name', label: '显示名', type: 'input', default: '', placeholder: '如:动作' },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ key: 'cat_custom_filter',
+ title: '按分类绑定筛选',
+ icon: 'fas fa-object-group',
+ fields: [
+ {
+ key: 'cat_custom_filter',
+ label: '分类筛选绑定',
+ type: 'repeater',
+ default: [],
+ addText: '添加绑定规则',
+ help: '为指定分类绑定不同的筛选维度组合,实现分类级别的差异化筛选',
+ fields: [
+ { key: 'cats', label: '绑定分类', type: 'taxonomy_select', taxonomy: 'category', multiple: true, default: [] },
+ { key: 'cat_s', label: '显示分类筛选', type: 'switch', default: true },
+ { key: 'tag_s', label: '显示标签筛选', type: 'switch', default: false },
+ { key: 'topics_s', label: '显示专题筛选', type: 'switch', default: false },
+ { key: 'orderby_s', label: '显示排序按钮', type: 'switch', default: true },
+ {
+ key: 'custom_filters',
+ label: '启用的自定义维度',
+ type: 'input',
+ default: '',
+ placeholder: '逗号分隔维度标识,如:type,region,year',
+ help: '留空表示使用全部已定义的筛选维度',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/post-list/index.js b/assets/admin/src/schema/sections/post-list/index.js
new file mode 100644
index 00000000..37d57fc4
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/index.js
@@ -0,0 +1,25 @@
+/**
+ * Post & List domain schemas.
+ */
+
+import { listSchema } from './list.js'
+import { thumbSchema } from './thumb.js'
+import { pageSchema } from './page.js'
+import { filterSchema } from './filter.js'
+import { featureSchema } from './feature.js'
+import { paySchema } from './pay.js'
+import { shareSchema } from './share.js'
+import { commentSchema } from './comment.js'
+import { submitSchema } from './submit.js'
+
+export const postListSchemas = {
+ list: listSchema,
+ thumb: thumbSchema,
+ page: pageSchema,
+ filter: filterSchema,
+ feature: featureSchema,
+ pay: paySchema,
+ share: shareSchema,
+ comment: commentSchema,
+ submit: submitSchema,
+}
diff --git a/assets/admin/src/schema/sections/post-list/list.js b/assets/admin/src/schema/sections/post-list/list.js
new file mode 100644
index 00000000..ad7f8f2c
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/list.js
@@ -0,0 +1,158 @@
+export const listSchema = {
+ key: 'post_list.list',
+ title: '文章列表',
+ icon: 'fas fa-list',
+ description: '配置文章列表的卡片样式、元信息显示、分页方式和排序规则。',
+ defaults: {
+ post_card_style: 'standard',
+ post_show_author: true,
+ post_show_date: true,
+ post_show_views: true,
+ post_show_category: true,
+ post_excerpt_length: 120,
+ post_pagination_mode: 'numeric',
+ ias_max: 3,
+ ajax_trigger_text: '',
+ ajax_nomore_text: '',
+ sticky_s: ['home', 'cat'],
+ list_badge_show: ['pay', 'tag', 'topics', 'cat'],
+ post_target_blank: false,
+ item_heading_bold: false,
+ list_orderby: 'modified',
+ list_meta_show: 'like',
+ list_show_type: 'separate',
+ thumb_postfirstimg_size: 'medium',
+ post_t_placeholder: '',
+ post_c_placeholder: '',
+ list_list_style: '',
+ list_list_img_position: 'left',
+ list_list_scale: 70,
+ list_card_style: '',
+ list_card_scale: 70,
+ list_card_home: false,
+ list_card_cat: [],
+ list_card_tag: false,
+ list_card_topics: false,
+ list_card_author: false,
+ },
+ groups: [
+ {
+ key: 'card',
+ title: '卡片样式',
+ icon: 'fas fa-th-large',
+ fields: [
+ { key: 'post_card_style', label: '卡片样式', type: 'select', default: 'standard', options: [
+ { label: '标准卡', value: 'standard' },
+ { label: '紧凑卡', value: 'compact' },
+ { label: '大图卡', value: 'large_thumb' },
+ { label: '多图卡', value: 'multi_thumb' },
+ { label: '纯文字', value: 'text' },
+ { label: '有图则图文', value: 'thumb_if_has' },
+ ]},
+ { key: 'post_excerpt_length', label: '摘要长度 (字)', type: 'number', default: 120, min: 20, max: 500 },
+ ],
+ },
+ {
+ key: 'meta',
+ title: '元信息显示',
+ icon: 'fas fa-info-circle',
+ fields: [
+ { key: 'post_show_author', label: '显示作者', type: 'switch', default: true },
+ { key: 'post_show_date', label: '显示日期', type: 'switch', default: true },
+ { key: 'post_show_views', label: '显示浏览数', type: 'switch', default: true },
+ { key: 'post_show_category', label: '显示分类', type: 'switch', default: true },
+ { 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' },
+ ], help: '选择列表中显示哪些徽章类型' },
+ { key: 'list_meta_show', label: '移动端优先部件', type: 'radio', default: 'like', options: [
+ { label: '浏览数', value: 'view' },
+ { label: '点赞数', value: 'like' },
+ { label: '评论数', value: 'comm' },
+ ], help: '移动端空间不足时保留显示的单一部件' },
+ ],
+ },
+ {
+ key: 'pagination',
+ title: '分页方式',
+ icon: 'fas fa-ellipsis-h',
+ fields: [
+ { key: 'post_pagination_mode', label: '分页模式', type: 'radio', default: 'numeric', options: [
+ { label: '数字分页', value: 'numeric' },
+ { label: '加载更多', value: 'load_more' },
+ { label: '无限滚动', value: 'infinite' },
+ ]},
+ { key: 'ias_max', label: '最大加载页数', type: 'number', default: 3, min: 0, max: 10, help: '0 = 不限' },
+ { key: 'ajax_trigger_text', label: '加载更多文案', type: 'input', default: '', help: '留空使用默认"加载更多"' },
+ { key: 'ajax_nomore_text', label: '没有更多文案', type: 'input', default: '', help: '留空使用默认"没有更多了"' },
+ ],
+ },
+ {
+ key: 'layout',
+ title: '布局设置',
+ icon: 'fas fa-columns',
+ fields: [
+ { key: 'sticky_s', label: '置顶范围', type: 'checkbox', default: ['home', 'cat'], options: [
+ { label: '首页', value: 'home' },
+ { label: '分类页', value: 'cat' },
+ { label: '标签页', value: 'tag' },
+ { label: '专题页', value: 'topics' },
+ ], help: '选择哪些页面类型显示置顶文章' },
+ { key: 'post_target_blank', label: '新窗口打开', type: 'switch', default: false, help: '文章链接在新窗口打开' },
+ { key: 'item_heading_bold', label: '标题加粗', type: 'switch', default: false, help: '列表文章标题使用粗体' },
+ { key: 'list_show_type', label: '列表样式', type: 'radio', default: 'separate', options: [
+ { label: '分隔卡片', value: 'separate' },
+ { label: '无间距', value: 'no_margin' },
+ ], help: '列表模式时的卡片间距样式' },
+ { key: 'list_card_home', label: '首页卡片模式', type: 'switch', default: false, help: '首页列表切换为卡片模式' },
+ { key: 'list_card_cat', label: '分类卡片模式', type: 'taxonomy_select', taxonomy: 'category', multiple: true, default: [], help: '选择分类,访问这些分类时使用卡片模式显示(为空则不使用)' },
+ { key: 'list_card_tag', label: '标签页卡片模式', type: 'switch', default: false, help: '标签页列表切换为卡片模式' },
+ { key: 'list_card_topics', label: '专题页卡片模式', type: 'switch', default: false, help: '专题页列表切换为卡片模式' },
+ { key: 'list_card_author', label: '用户页卡片模式', type: 'switch', default: false, help: '用户页列表切换为卡片模式' },
+ ],
+ },
+ {
+ key: 'sort',
+ title: '默认排序',
+ icon: 'fas fa-sort',
+ fields: [
+ { key: 'list_orderby', label: '列表默认排序', type: 'radio', default: 'modified', options: [
+ { label: '更新时间', value: 'modified' },
+ { label: '发布时间', value: 'date' },
+ { label: '评论数', value: 'comments' },
+ { label: 'WordPress 默认', value: 'default' },
+ ], help: '分类/标签/归档页面的默认排序方式' },
+ ],
+ },
+ {
+ key: 'list_option',
+ title: '列表模式配置',
+ icon: 'fas fa-list-ul',
+ fields: [
+ { key: 'list_list_style', label: '列表样式变体', type: 'radio', default: '', options: [
+ { label: '默认', value: '' },
+ { label: '样式2', value: 'style2' },
+ ], help: '列表模式的样式变体' },
+ { key: 'list_list_img_position', label: '缩略图位置', type: 'radio', default: 'left', options: [
+ { label: '左侧', value: 'left' },
+ { label: '右侧', value: 'right' },
+ ], help: '列表模式缩略图靠左或靠右' },
+ { key: 'list_list_scale', label: '缩略图比例 (%)', type: 'number', default: 70, min: 50, max: 200, help: '列表模式缩略图宽度百分比' },
+ ],
+ },
+ {
+ key: 'card_option',
+ title: '卡片模式配置',
+ icon: 'fas fa-th',
+ fields: [
+ { key: 'list_card_style', label: '卡片样式变体', type: 'radio', default: '', options: [
+ { label: '默认', value: '' },
+ { label: '样式3', value: 'style3' },
+ ], help: '卡片模式的样式变体' },
+ { key: 'list_card_scale', label: '缩略图比例 (%)', type: 'number', default: 70, min: 20, max: 200, help: '卡片模式缩略图高度百分比' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/post-list/page.js b/assets/admin/src/schema/sections/post-list/page.js
new file mode 100644
index 00000000..7ab6a0eb
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/page.js
@@ -0,0 +1,155 @@
+export const pageSchema = {
+ key: 'post_list.page',
+ title: '文章页面',
+ icon: 'fas fa-file-alt',
+ description: '配置单篇文章页面的布局、目录树、图片灯箱、版权区和互动功能。',
+ defaults: {
+ post_toc_enabled: true,
+ post_toc_depth: 3,
+ article_nav_mobile_nav_s: true,
+ post_reading_progress: true,
+ breadcrumbs_single_s: true,
+ breadcrumbs_home_text: '首页',
+ post_prevnext_s: true,
+ post_authordesc_s: true,
+ post_single_hide_time_s: false,
+ post_lightbox: true,
+ post_lightbox_thumbs: true,
+ post_lightbox_zoom: true,
+ post_lightbox_full: true,
+ post_lightbox_down: false,
+ post_lightbox_play: false,
+ post_related_enabled: true,
+ post_related_count: 6,
+ post_related_type: 'category',
+ post_related_orderby: 'rand',
+ post_related_title: '相关推荐',
+ post_copyright_enabled: true,
+ post_copyright_text: '',
+ share_s: true,
+ post_like_enabled: true,
+ post_favorite_enabled: true,
+ post_front_content: '',
+ post_after_content: '',
+ post_yiyan_header: false,
+ post_yiyan_footer: false,
+ post_yiyan_box: false,
+ post_default_views_min: 20,
+ post_default_views_max: 50,
+ post_default_likes_min: 5,
+ post_default_likes_max: 15,
+ post_article_image_cover: true,
+ post_article_image_cover_scale: 35,
+ post_article_slide_cover: true,
+ post_article_video_cover: true,
+ post_article_video_cover_scale: 56,
+ item_heading_bold: false,
+ post_button_toptext: '喜欢就支持一下吧',
+ },
+ groups: [
+ {
+ key: 'navigation',
+ title: '文章导航',
+ icon: 'fas fa-compass',
+ fields: [
+ { key: 'post_toc_enabled', label: '目录树导航', type: 'switch', default: true, help: '自动生成文章目录树' },
+ { key: 'post_toc_depth', label: '目录深度', type: 'number', default: 3, min: 1, max: 6, dependsOn: { key: 'post_toc_enabled', value: true } },
+ { key: 'article_nav_mobile_nav_s', label: '移动端弹出菜单', type: 'switch', default: true, dependsOn: { key: 'post_toc_enabled', value: true }, help: '在移动端弹出菜单中显示目录导航' },
+ { key: 'post_reading_progress', label: '阅读进度条', type: 'switch', default: true },
+ { key: 'breadcrumbs_single_s', label: '面包屑导航', type: 'switch', default: true },
+ { key: 'breadcrumbs_home_text', label: '面包屑首页文字', type: 'input', default: '首页', dependsOn: { key: 'breadcrumbs_single_s', value: true } },
+ { key: 'post_prevnext_s', label: '上一篇/下一篇', type: 'switch', default: true },
+ { key: 'post_authordesc_s', label: '作者简介', type: 'switch', default: true },
+ { key: 'post_single_hide_time_s', label: '隐藏文章时间', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'cover',
+ title: '封面展示',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'post_article_image_cover', label: '图片封面', type: 'switch', default: true, help: '文章有特色图时自动展示封面' },
+ { key: 'post_article_image_cover_scale', label: '封面高度比例(%)', type: 'number', default: 35, min: 10, max: 100, dependsOn: { key: 'post_article_image_cover', value: true } },
+ { key: 'post_article_slide_cover', label: '幻灯片封面', type: 'switch', default: true, help: '文章有图集时以轮播形式展示封面' },
+ { key: 'post_article_video_cover', label: '视频封面', type: 'switch', default: true, help: '文章有视频时以视频播放器作为封面' },
+ { key: 'post_article_video_cover_scale', label: '视频封面高度比例(%)', type: 'number', default: 56, min: 20, max: 100, dependsOn: { key: 'post_article_video_cover', value: true } },
+ ],
+ },
+ {
+ key: 'media',
+ title: '媒体功能',
+ icon: 'fas fa-photo-video',
+ fields: [
+ { key: 'post_lightbox', label: '图片灯箱', type: 'switch', default: true, help: '点击图片放大查看' },
+ { key: 'post_lightbox_thumbs', label: '灯箱缩略图', type: 'switch', default: true, dependsOn: { key: 'post_lightbox', value: true }, help: '灯箱底部显示缩略图导航' },
+ { key: 'post_lightbox_zoom', label: '灯箱缩放', type: 'switch', default: true, dependsOn: { key: 'post_lightbox', value: true }, help: '灯箱显示缩放按钮' },
+ { key: 'post_lightbox_full', label: '灯箱全屏', type: 'switch', default: true, dependsOn: { key: 'post_lightbox', value: true }, help: '灯箱显示全屏按钮' },
+ { key: 'post_lightbox_down', label: '灯箱下载', type: 'switch', default: false, dependsOn: { key: 'post_lightbox', value: true }, help: '灯箱显示下载按钮' },
+ { key: 'post_lightbox_play', label: '灯箱播放', type: 'switch', default: false, dependsOn: { key: 'post_lightbox', value: true }, help: '灯箱显示播放按钮(自动轮播)' },
+ ],
+ },
+ {
+ key: 'related',
+ title: '相关推荐',
+ icon: 'fas fa-random',
+ fields: [
+ { key: 'post_related_enabled', label: '显示相关推荐', type: 'switch', default: true },
+ { key: 'post_related_count', label: '推荐数量', type: 'number', default: 6, min: 1, max: 20, dependsOn: { key: 'post_related_enabled', value: true } },
+ { key: 'post_related_type', label: '推荐方式', type: 'radio', default: 'category', options: [
+ { label: '同分类', value: 'category' },
+ { label: '同标签', value: 'tag' },
+ { label: '随机', value: 'random' },
+ ], dependsOn: { key: 'post_related_enabled', value: true } },
+ { key: 'post_related_orderby', label: '推荐排序', type: 'select', default: 'rand', options: [
+ { label: '随机', value: 'rand' },
+ { label: '相关度', value: 'relevance' },
+ { label: '时间', value: 'date' },
+ ], dependsOn: { key: 'post_related_enabled', value: true } },
+ { key: 'post_related_title', label: '推荐标题', type: 'input', default: '相关推荐', dependsOn: { key: 'post_related_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'interactions',
+ title: '互动功能',
+ icon: 'fas fa-heart',
+ fields: [
+ { key: 'post_like_enabled', label: '点赞', type: 'switch', default: true },
+ { key: 'post_favorite_enabled', label: '收藏', type: 'switch', default: true },
+ { key: 'share_s', label: '分享按钮', type: 'switch', default: true },
+ { key: 'post_copyright_enabled', label: '版权信息', type: 'switch', default: true },
+ { key: 'post_copyright_text', label: '版权默认文案', type: 'textarea', default: '', help: '留空则自动生成"本文由 XX 原创发布"' },
+ { key: 'post_button_toptext', label: '按钮上方文案', type: 'input', default: '喜欢就支持一下吧', help: '显示在点赞/打赏/分享按钮上方' },
+ ],
+ },
+ {
+ key: 'content',
+ title: '内容插入',
+ icon: 'fas fa-pen-square',
+ fields: [
+ { key: 'post_front_content', label: '文章头部内容', type: 'textarea', default: '', help: '显示在所有文章正文前方(支持 HTML)' },
+ { key: 'post_after_content', label: '文章底部内容', type: 'textarea', default: '', help: '显示在所有文章正文后方(支持 HTML)' },
+ ],
+ },
+ {
+ key: 'yiyan',
+ title: '一言',
+ icon: 'fas fa-quote-left',
+ fields: [
+ { key: 'post_yiyan_header', label: '文章头部一言', type: 'switch', default: false, help: '在文章标题下方显示一言' },
+ { key: 'post_yiyan_footer', label: '文章底部一言', type: 'switch', default: false, help: '在文章内容底部显示一言' },
+ { key: 'post_yiyan_box', label: '一言盒子', type: 'switch', default: false, help: '在相关文章前显示一言盒子' },
+ ],
+ },
+ {
+ key: 'meta',
+ title: '默认数值',
+ icon: 'fas fa-chart-bar',
+ fields: [
+ { key: 'post_default_views_min', label: '默认浏览最小值', type: 'number', default: 20, min: 0, max: 9999 },
+ { key: 'post_default_views_max', label: '默认浏览最大值', type: 'number', default: 50, min: 1, max: 9999 },
+ { key: 'post_default_likes_min', label: '默认点赞最小值', type: 'number', default: 5, min: 0, max: 9999 },
+ { key: 'post_default_likes_max', label: '默认点赞最大值', type: 'number', default: 15, min: 1, max: 9999 },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/post-list/pay.js b/assets/admin/src/schema/sections/post-list/pay.js
new file mode 100644
index 00000000..e7724c59
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/pay.js
@@ -0,0 +1,46 @@
+export const paySchema = {
+ key: 'post_list.pay',
+ title: '付费内容',
+ icon: 'fas fa-yen-sign',
+ description: '配置文章付费阅读、下载功能和购买用户显示。',
+ defaults: {
+ post_pay_enable: false,
+ post_pay_price_default: 0,
+ post_pay_free_logged_show: false,
+ post_pay_box_position: 'content',
+ post_pay_mark: '付费',
+ post_pay_show_user: false,
+ post_download_enable: false,
+ post_download_remote_s: false,
+ post_download_verify_nonce: true,
+ post_download_user_log: true,
+ post_download_post_log: true,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '付费设置',
+ icon: 'fas fa-coins',
+ fields: [
+ { key: 'post_pay_enable', label: '启用付费内容', type: 'switch', default: false },
+ { key: 'post_pay_price_default', label: '默认价格', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'post_pay_enable', value: true } },
+ { key: 'post_pay_free_logged_show', label: '登录用户显示免费', type: 'switch', default: false, dependsOn: { key: 'post_pay_enable', value: true } },
+ { key: 'post_pay_box_position', label: '付费框位置', type: 'radio', default: 'content', options: [{ label: '内容前', value: 'content' }, { label: '全文遮罩', value: 'mask' }], dependsOn: { key: 'post_pay_enable', value: true } },
+ { key: 'post_pay_mark', label: '付费标记', type: 'input', default: '付费', dependsOn: { key: 'post_pay_enable', value: true } },
+ { key: 'post_pay_show_user', label: '显示购买用户', type: 'switch', default: false, dependsOn: { key: 'post_pay_enable', value: true } },
+ ],
+ },
+ {
+ key: 'download',
+ title: '下载设置',
+ icon: 'fas fa-download',
+ fields: [
+ { key: 'post_download_enable', label: '启用下载功能', type: 'switch', default: false },
+ { key: 'post_download_remote_s', label: '远程文件下载', type: 'switch', default: false, dependsOn: { key: 'post_download_enable', value: true } },
+ { key: 'post_download_verify_nonce', label: '下载鉴权', type: 'switch', default: true, dependsOn: { key: 'post_download_enable', value: true } },
+ { key: 'post_download_user_log', label: '用户下载日志', type: 'switch', default: true, dependsOn: { key: 'post_download_enable', value: true } },
+ { key: 'post_download_post_log', label: '文章下载日志', type: 'switch', default: true, dependsOn: { key: 'post_download_enable', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/post-list/share.js b/assets/admin/src/schema/sections/post-list/share.js
new file mode 100644
index 00000000..b65d0fc4
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/share.js
@@ -0,0 +1,42 @@
+export const shareSchema = {
+ key: 'post_list.share',
+ title: '分享设置',
+ icon: 'fas fa-share-alt',
+ description: '配置微信分享凭据、海报分享和社交分享选项。',
+ defaults: {
+ share_wechat_s: false,
+ share_wechat_appid: '',
+ share_wechat_app_secret: '',
+ share_only_logo: false,
+ share_img_compatible_s: false,
+ share_img_byimg: '',
+ share_logo: '',
+ share_img: '',
+ share_desc: '扫一扫 立即查看',
+ },
+ groups: [
+ {
+ key: 'wechat',
+ title: '微信分享',
+ icon: 'fab fa-weixin',
+ fields: [
+ { key: 'share_wechat_s', label: '启用微信分享', type: 'switch', default: false, help: '启用后文章页将支持微信 JSAPI 分享' },
+ { key: 'share_wechat_appid', label: 'AppID', type: 'input', default: '', dependsOn: { key: 'share_wechat_s', value: true }, help: '微信公众号 AppID' },
+ { key: 'share_wechat_app_secret', label: 'AppSecret', type: 'input', default: '', dependsOn: { key: 'share_wechat_s', value: true }, help: '微信公众号 AppSecret(请确保 JS 安全域名已配置)' },
+ { key: 'share_only_logo', label: '仅显示 Logo', type: 'switch', default: false, help: '分享时仅显示站点 Logo' },
+ ],
+ },
+ {
+ key: 'poster',
+ title: '海报分享',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'share_img_compatible_s', label: '兼容模式', type: 'switch', default: false, help: '启用后使用兼容模式生成分享海报' },
+ { key: 'share_img_byimg', label: '海报默认图', type: 'media', default: '', help: '海报分享使用的默认背景图' },
+ { key: 'share_logo', label: '海报 LOGO', type: 'media', default: '', help: '海报上叠加的站点 LOGO' },
+ { key: 'share_img', label: '分享图片', type: 'media', default: '', help: '社交平台分享预览图' },
+ { key: 'share_desc', label: '分享描述', type: 'textarea', default: '扫一扫 立即查看', help: '海报或分享卡片的描述文案' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/post-list/submit.js b/assets/admin/src/schema/sections/post-list/submit.js
new file mode 100644
index 00000000..940df576
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/submit.js
@@ -0,0 +1,125 @@
+export const submitSchema = {
+ key: 'post_list.submit',
+ title: '前台投稿',
+ icon: 'fas fa-paper-plane',
+ description: '配置前台投稿表单字段、审核流程、附件权限和封面策略。',
+ defaults: {
+ front_submit_enabled: true,
+ front_submit_roles: 'author,editor,administrator',
+ front_submit_title: true,
+ front_submit_content: true,
+ front_submit_excerpt: true,
+ front_submit_cover: true,
+ front_submit_category: true,
+ post_article_cat: [],
+ front_submit_tags: true,
+ post_article_topics_s: false,
+ post_article_custom_filter_s: true,
+ front_submit_custom_fields: '',
+ front_submit_review_mode: 'pending',
+ front_submit_cover_required: false,
+ front_submit_max_images: 5,
+ front_submit_editor_mode: 'rich',
+ front_submit_draft_save: true,
+ front_submit_leave_warning: true,
+ post_article_btn_txte: '发布文章',
+ post_article_title_strlen_limit_min: 5,
+ post_article_title_strlen_limit_max: 30,
+ post_article_user: 0,
+ post_article_pay_free_s: true,
+ post_article_pay_price_min: 0,
+ post_article_pay_price_max: 100,
+ post_article_pay_vip_price_s: true,
+ post_article_pay_vip_1_discount: 80,
+ post_article_pay_vip_2_discount: 50,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '基础配置',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'front_submit_enabled', label: '开启前台投稿', type: 'switch', default: true, help: '允许用户在前台提交文章' },
+ { key: 'front_submit_roles', label: '允许投稿的角色', type: 'input', default: 'author,editor,administrator', dependsOn: { key: 'front_submit_enabled', value: true }, help: '逗号分隔角色标识' },
+ { key: 'post_article_btn_txte', label: '投稿按钮文案', type: 'input', default: '发布文章', dependsOn: { key: 'front_submit_enabled', value: true }, help: '前台投稿按钮显示的文字' },
+ { key: 'post_article_title_strlen_limit_min', label: '标题最少字数', type: 'number', default: 5, min: 1, max: 100, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_title_strlen_limit_max', label: '标题最多字数', type: 'number', default: 30, min: 5, max: 200, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_t_placeholder', label: '标题占位符', type: 'input', default: '', placeholder: '请输入标题', dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_c_placeholder', label: '内容占位符', type: 'textarea', default: '', placeholder: '请输入内容', dependsOn: { key: 'front_submit_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'fields',
+ title: '表单字段',
+ icon: 'fas fa-list-alt',
+ fields: [
+ { key: 'front_submit_title', label: '标题', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'front_submit_content', label: '正文', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'front_submit_excerpt', label: '摘要', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'front_submit_cover', label: '封面图', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'front_submit_category', label: '分类选择', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_cat', label: '允许投稿分类', type: 'taxonomy_select', taxonomy: 'category', multiple: true, default: [], help: '为空则允许全部分类', dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_user', label: '免登录归属用户', type: 'user_select', default: 0, help: '免登录投稿时文章归属的用户,搜索选择', dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'front_submit_tags', label: '标签输入', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_topics_s', label: '专题选择', type: 'switch', default: false, help: '投稿时可选择专题分类', dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_custom_filter_s', label: '高级筛选选择', type: 'switch', default: true, help: '投稿时可选择自定义筛选维度值', dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'front_submit_custom_fields', label: '自定义字段', type: 'textarea', default: '', placeholder: '每行一个:field_key|字段名称|类型(input/textarea/select)', dependsOn: { key: 'front_submit_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'review',
+ title: '审核策略',
+ icon: 'fas fa-tasks',
+ fields: [
+ {
+ key: 'front_submit_review_mode',
+ label: '审核模式',
+ type: 'radio',
+ default: 'pending',
+ options: [
+ { label: '待审核', value: 'pending' },
+ { label: '直接发布', value: 'publish' },
+ { label: '自动草稿', value: 'draft' },
+ ],
+ dependsOn: { key: 'front_submit_enabled', value: true },
+ },
+ { key: 'front_submit_cover_required', label: '封面图必填', type: 'switch', default: false, dependsOn: { key: 'front_submit_cover', value: true } },
+ { key: 'front_submit_max_images', label: '正文最大图片数', type: 'number', default: 5, min: 0, max: 50, dependsOn: { key: 'front_submit_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'editor',
+ title: '编辑器设置',
+ icon: 'fas fa-edit',
+ fields: [
+ {
+ key: 'front_submit_editor_mode',
+ label: '编辑器模式',
+ type: 'radio',
+ default: 'rich',
+ options: [
+ { label: '富文本', value: 'rich' },
+ { label: 'Markdown', value: 'markdown' },
+ { label: '纯文本', value: 'plain' },
+ ],
+ dependsOn: { key: 'front_submit_content', value: true },
+ },
+ { key: 'front_submit_draft_save', label: '自动保存草稿', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'front_submit_leave_warning', label: '离开页面提醒', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'paid',
+ title: '付费投稿权限',
+ icon: 'fas fa-coins',
+ fields: [
+ { key: 'post_article_pay_free_s', label: '允许设置免费', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_pay_price_min', label: '最低价格', type: 'number', default: 0, min: 0, max: 9999, help: '投稿可设置的最低付费金额', dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_pay_price_max', label: '最高价格', type: 'number', default: 100, min: 0, max: 9999, help: '投稿可设置的最高付费金额', dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_pay_vip_price_s', label: '允许设置会员价', type: 'switch', default: true, dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_pay_vip_1_discount', label: 'VIP1 折扣(%)', type: 'number', default: 80, min: 0, max: 100, help: 'VIP1 会员折扣百分比,80 即 8 折', dependsOn: { key: 'front_submit_enabled', value: true } },
+ { key: 'post_article_pay_vip_2_discount', label: 'VIP2 折扣(%)', type: 'number', default: 50, min: 0, max: 100, help: 'VIP2 会员折扣百分比,50 即 5 折', dependsOn: { key: 'front_submit_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/post-list/thumb.js b/assets/admin/src/schema/sections/post-list/thumb.js
new file mode 100644
index 00000000..3345c629
--- /dev/null
+++ b/assets/admin/src/schema/sections/post-list/thumb.js
@@ -0,0 +1,70 @@
+export const thumbSchema = {
+ key: 'post_list.thumb',
+ title: '列表缩略图',
+ icon: 'fas fa-images',
+ description: '配置缩略图来源优先级、尺寸、占位图和多图模式。',
+ defaults: {
+ thumb_priority: 'featured,attachment,resource,custom',
+ thumb_fallback: 'placeholder',
+ thumb_spare_images: [],
+ thumb_lazy: true,
+ thumb_aspect_ratio: '16:9',
+ list_thumb_fit: 'cover',
+ thumb_multi_enabled: false,
+ thumb_multi_count: 3,
+ list_thumb_slides_s: true,
+ list_thumb_video_s: true,
+ list_thumb_video_mute_s: true,
+ thumb_postfirstimg_s: true,
+ thumb_catimg_s: true,
+ mult_thumb_cat: [],
+ },
+ groups: [
+ {
+ key: 'source',
+ title: '图片来源',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'thumb_priority', label: '图片优先级', type: 'input', default: 'featured,attachment,resource,custom', help: '逗号分隔: featured(特色图), attachment(附件), resource(资源封面), custom(自定义), post_first_img(文章首图), category_cover(分类封面)' },
+ { key: 'thumb_fallback', label: '无图时处理', type: 'radio', default: 'placeholder', options: [
+ { label: '显示占位图', value: 'placeholder' },
+ { label: '隐藏缩略图', value: 'hide' },
+ { label: '随机封面', value: 'random' },
+ ]},
+ { key: 'thumb_spare_images', label: '备用缩略图', type: 'gallery', default: [], help: '当无图处理选择"随机封面"时从此图库随机选取', dependsOn: { key: 'thumb_fallback', value: 'random' } },
+ { key: 'thumb_aspect_ratio', label: '图片比例', type: 'select', default: '16:9', options: [
+ { label: '16:9 宽屏', value: '16:9' },
+ { label: '4:3 标准', value: '4:3' },
+ { label: '1:1 正方形', value: '1:1' },
+ { label: '自适应', value: 'auto' },
+ ]},
+ { key: 'list_thumb_fit', label: '缩略图填充方式', type: 'radio', default: 'cover', options: [
+ { label: '填充裁剪', value: 'cover' },
+ { label: '完整显示', value: 'contain' },
+ ]},
+ { key: 'list_thumb_slides_s', label: '幻灯片缩略图', type: 'switch', default: true, help: '允许幻灯片作为列表缩略图' },
+ { key: 'list_thumb_video_s', label: '视频缩略图', type: 'switch', default: true, help: '鼠标悬停时自动播放视频缩略图' },
+ { key: 'list_thumb_video_mute_s', label: '视频缩略图静音', type: 'switch', default: true, dependsOn: { key: 'list_thumb_video_s', value: true }, help: '视频缩略图播放时静音' },
+ { key: 'thumb_postfirstimg_s', label: '自动获取文章首图', type: 'switch', default: true, help: '自动从文章内容中提取第一张图片作为缩略图' },
+ { key: 'thumb_postfirstimg_size', label: '首图尺寸', type: 'select', default: 'medium', options: [
+ { label: '缩略图', value: 'thumbnail' },
+ { label: '中等', value: 'medium' },
+ { label: '大', value: 'large' },
+ { label: '原始', value: 'full' },
+ ], dependsOn: { key: 'thumb_postfirstimg_s', value: true }, help: '自动获取文章首图时使用的 WP 图片尺寸' },
+ { key: 'thumb_catimg_s', label: '自动获取分类封面', type: 'switch', default: true, help: '无特色图时使用分类封面图作为缩略图' },
+ ],
+ },
+ {
+ key: 'multi',
+ title: '多图模式',
+ icon: 'fas fa-images',
+ fields: [
+ { key: 'thumb_multi_enabled', label: '多图卡片', type: 'switch', default: false, help: '当文章有多张图片时,在列表中展示多图预览' },
+ { key: 'thumb_multi_count', label: '最多显示几张', type: 'number', default: 3, min: 2, max: 6, dependsOn: { key: 'thumb_multi_enabled', value: true } },
+ { key: 'mult_thumb_cat', label: '按分类多图显示', type: 'taxonomy_select', taxonomy: 'category', multiple: true, default: [], help: '选择的分类下所有文章自动显示多张缩略图' },
+ { key: 'thumb_lazy', label: '懒加载', type: 'switch', default: true },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/shop/basic.js b/assets/admin/src/schema/sections/shop/basic.js
new file mode 100644
index 00000000..7a004c0e
--- /dev/null
+++ b/assets/admin/src/schema/sections/shop/basic.js
@@ -0,0 +1,49 @@
+export const basicSchema = {
+ key: 'shop.basic',
+ title: '商城基础',
+ icon: 'fas fa-store',
+ description: '商城总开关、首页链接、面包屑和 URL 别名设置。',
+ defaults: {
+ shop_s: false,
+ shop_home_url: '',
+ shop_breadcrumbs_s: true,
+ shop_breadcrumbs_home: true,
+ shop_breadcrumbs_shop_home: true,
+ shop_breadcrumbs_shop_home_name: '商城首页',
+ shop_product_rewrite_slug: 'product',
+ shop_cart_rewrite_slug: 'cart',
+ shop_rewrite_suffix_html_s: false,
+ },
+ groups: [
+ {
+ key: 'general',
+ title: '商城开关',
+ icon: 'fas fa-power-off',
+ fields: [
+ { key: 'shop_s', label: '启用商城', type: 'switch', default: false },
+ { key: 'shop_home_url', label: '商城首页链接', type: 'input', default: '', help: '留空使用默认商城页面', dependsOn: { key: 'shop_s', value: true } },
+ ],
+ },
+ {
+ key: 'breadcrumbs',
+ title: '面包屑',
+ icon: 'fas fa-angle-double-right',
+ fields: [
+ { key: 'shop_breadcrumbs_s', label: '显示面包屑', type: 'switch', default: true, dependsOn: { key: 'shop_s', value: true } },
+ { key: 'shop_breadcrumbs_home', label: '显示首页链接', type: 'switch', default: true, dependsOn: { key: 'shop_breadcrumbs_s', value: true } },
+ { key: 'shop_breadcrumbs_shop_home', label: '显示商城首页', type: 'switch', default: true, dependsOn: { key: 'shop_breadcrumbs_s', value: true } },
+ { key: 'shop_breadcrumbs_shop_home_name', label: '商城首页名称', type: 'input', default: '商城首页', dependsOn: { key: 'shop_breadcrumbs_s', value: true } },
+ ],
+ },
+ {
+ key: 'rewrite',
+ title: 'URL 别名',
+ icon: 'fas fa-link',
+ fields: [
+ { key: 'shop_product_rewrite_slug', label: '商品 URL 别名', type: 'input', default: 'product', dependsOn: { key: 'shop_s', value: true } },
+ { key: 'shop_cart_rewrite_slug', label: '购物车 URL 别名', type: 'input', default: 'cart', dependsOn: { key: 'shop_s', value: true } },
+ { key: 'shop_rewrite_suffix_html_s', label: '.html 伪静态后缀', type: 'switch', default: false, dependsOn: { key: 'shop_s', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/shop/contact.js b/assets/admin/src/schema/sections/shop/contact.js
new file mode 100644
index 00000000..6a1fc171
--- /dev/null
+++ b/assets/admin/src/schema/sections/shop/contact.js
@@ -0,0 +1,32 @@
+export const contactSchema = {
+ key: 'shop.contact',
+ title: '客服配置',
+ icon: 'fas fa-headset',
+ description: '商品详情页客服按钮和联系方式配置。',
+ defaults: {
+ shop_contact_msg_s: true,
+ shop_contact_name: '联系客服',
+ shop_contact_note: '',
+ shop_contact_more: '',
+ },
+ groups: [
+ {
+ key: 'button',
+ title: '客服按钮',
+ icon: 'fas fa-comment-dots',
+ fields: [
+ { key: 'shop_contact_msg_s', label: '显示客服按钮', type: 'switch', default: true },
+ { key: 'shop_contact_name', label: '按钮文案', type: 'input', default: '联系客服', dependsOn: { key: 'shop_contact_msg_s', value: true } },
+ { key: 'shop_contact_note', label: '客服备注', type: 'textarea', default: '', dependsOn: { key: 'shop_contact_msg_s', value: true } },
+ ],
+ },
+ {
+ key: 'channels',
+ title: '联系方式',
+ icon: 'fas fa-address-book',
+ fields: [
+ { key: 'shop_contact_more', label: '多客服联系方式', type: 'textarea', default: '', help: '每行一个:名称|图标|链接|二维码图片 URL' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/shop/index.js b/assets/admin/src/schema/sections/shop/index.js
new file mode 100644
index 00000000..eb8f945e
--- /dev/null
+++ b/assets/admin/src/schema/sections/shop/index.js
@@ -0,0 +1,21 @@
+/**
+ * Shop & Product domain schemas.
+ */
+
+import { basicSchema } from './basic.js'
+import { listSchema } from './list.js'
+import { contactSchema } from './contact.js'
+import { singleSchema } from './single.js'
+import { productDefaultsSchema } from './product-defaults.js'
+import { shippingConfigSchema } from './shipping-config.js'
+import { timingSchema } from './timing.js'
+
+export const shopSchemas = {
+ basic: basicSchema,
+ list: listSchema,
+ contact: contactSchema,
+ single: singleSchema,
+ productDefaults: productDefaultsSchema,
+ shippingConfig: shippingConfigSchema,
+ timing: timingSchema,
+}
diff --git a/assets/admin/src/schema/sections/shop/list.js b/assets/admin/src/schema/sections/shop/list.js
new file mode 100644
index 00000000..6074ddd7
--- /dev/null
+++ b/assets/admin/src/schema/sections/shop/list.js
@@ -0,0 +1,40 @@
+export const listSchema = {
+ key: 'shop.list',
+ title: '商品列表',
+ icon: 'fas fa-th-list',
+ description: '商品列表排序、分页、卡片样式和销量显示配置。',
+ defaults: {
+ shop_list_orderby: 'date',
+ shop_list_count: 12,
+ shop_list_paging: 'number',
+ shop_list_ias_s: false,
+ shop_list_ias_max: 5,
+ shop_list_style: 'default',
+ shop_sales_show: 'show',
+ shop_sales_show_min: 10,
+ },
+ groups: [
+ {
+ key: 'pagination',
+ title: '列表分页',
+ icon: 'fas fa-layer-group',
+ fields: [
+ { key: 'shop_list_orderby', label: '默认排序', type: 'select', default: 'date', options: [{ label: '最新发布', value: 'date' }, { label: '销量最高', value: 'sales' }, { label: '价格最低', value: 'price_asc' }, { label: '价格最高', value: 'price_desc' }, { label: '评价最多', value: 'comments' }] },
+ { key: 'shop_list_count', label: '每页数量', type: 'number', default: 12, min: 4, max: 100 },
+ { key: 'shop_list_paging', label: '翻页模式', type: 'radio', default: 'number', options: [{ label: '数字翻页', value: 'number' }, { label: 'AJAX 加载', value: 'ajax' }] },
+ { key: 'shop_list_ias_s', label: '滚动自动加载', type: 'switch', default: false, dependsOn: { key: 'shop_list_paging', value: 'ajax' } },
+ { key: 'shop_list_ias_max', label: '自动加载页数', type: 'number', default: 5, min: 1, max: 50, dependsOn: { key: 'shop_list_ias_s', value: true } },
+ ],
+ },
+ {
+ key: 'style',
+ title: '列表样式',
+ icon: 'fas fa-palette',
+ fields: [
+ { key: 'shop_list_style', label: '卡片样式', type: 'select', default: 'default', options: [{ label: '默认', value: 'default' }, { label: '竖向大卡片', value: 'card-lg' }, { label: '横向小卡片', value: 'card-sm' }] },
+ { key: 'shop_sales_show', label: '销量显示', type: 'select', default: 'show', options: [{ label: '显示', value: 'show' }, { label: '不显示', value: 'hide' }, { label: '超量显示', value: 'threshold' }] },
+ { key: 'shop_sales_show_min', label: '起显阈值', type: 'number', default: 10, min: 0, max: 99999, help: '销量超过此数字才显示', dependsOn: { key: 'shop_sales_show', value: 'threshold' } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/shop/product-defaults.js b/assets/admin/src/schema/sections/shop/product-defaults.js
new file mode 100644
index 00000000..566b6bbc
--- /dev/null
+++ b/assets/admin/src/schema/sections/shop/product-defaults.js
@@ -0,0 +1,54 @@
+export const productDefaultsSchema = {
+ key: 'shop.product-defaults',
+ title: '商品默认参数',
+ icon: 'fas fa-sliders-h',
+ description: '主题级商品参数默认值,作为「商品→分类→主题设置」参数继承链的根。',
+ defaults: {
+ shop_limit_buy: -1,
+ shop_shipping_fee: 'free',
+ shop_guest_buy: false,
+ shop_email_fill: false,
+ shop_rebate: true,
+ shop_after_sale: 'refund_return',
+ shop_service: '',
+ shop_product_params_default: '',
+ },
+ groups: [
+ {
+ key: 'policy',
+ title: '购买策略',
+ icon: 'fas fa-shopping-bag',
+ fields: [
+ { key: 'shop_limit_buy', label: '默认限购', type: 'number', default: -1, min: -1, max: 9999, help: '-1 不限,0 禁购,正数为限购数量' },
+ { key: 'shop_guest_buy', label: '免登录购买', type: 'switch', default: false },
+ { key: 'shop_email_fill', label: '邮箱必留', type: 'switch', default: false },
+ { key: 'shop_rebate', label: '允许返佣', type: 'switch', default: true },
+ ],
+ },
+ {
+ key: 'shipping',
+ title: '运费默认',
+ icon: 'fas fa-truck',
+ fields: [
+ { key: 'shop_shipping_fee', label: '默认运费类型', type: 'select', default: 'free', options: [{ label: '包邮', value: 'free' }, { label: '固定运费', value: 'fixed' }, { label: '按金额计算', value: 'amount' }] },
+ ],
+ },
+ {
+ key: 'afterSale',
+ title: '售后与服务',
+ icon: 'fas fa-undo',
+ fields: [
+ { key: 'shop_after_sale', label: '默认售后政策', type: 'select', default: 'refund_return', options: [{ label: '仅退款', value: 'refund' }, { label: '退货退款', value: 'refund_return' }, { label: '不支持售后', value: 'none' }] },
+ { key: 'shop_service', label: '服务保障', type: 'textarea', default: '', help: '每行一项:名称|说明|图标图片 URL' },
+ ],
+ },
+ {
+ key: 'params',
+ title: '默认参数模板',
+ icon: 'fas fa-list-alt',
+ fields: [
+ { key: 'shop_product_params_default', label: '商品参数模板', type: 'textarea', default: '', help: '每行一个参数:参数名|参数值,新建商品自动带入' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/shop/shipping-config.js b/assets/admin/src/schema/sections/shop/shipping-config.js
new file mode 100644
index 00000000..9e41c82d
--- /dev/null
+++ b/assets/admin/src/schema/sections/shop/shipping-config.js
@@ -0,0 +1,52 @@
+export const shippingConfigSchema = {
+ key: 'shop.shipping-config',
+ title: '物流配置',
+ icon: 'fas fa-shipping-fast',
+ description: '快递查询接口、物流公司列表和收货地址设置。',
+ defaults: {
+ shop_manual_address: false,
+ shop_express_api_sdk: 'kuaidi100',
+ shop_express_query_interval: 60,
+ shop_express_kdniao_opt: '',
+ shop_express_kuaidi100_opt: '',
+ shop_express_aliyun_opt: '',
+ shop_shipping_company: '',
+ },
+ groups: [
+ {
+ key: 'address',
+ title: '收货地址',
+ icon: 'fas fa-map-marker-alt',
+ fields: [
+ { key: 'shop_manual_address', label: '手动填写收货地址', type: 'switch', default: false, help: '开启后用户可手动输入地址而非选择地址簿' },
+ ],
+ },
+ {
+ key: 'express',
+ title: '快递查询',
+ icon: 'fas fa-search',
+ fields: [
+ { key: 'shop_express_api_sdk', label: '快递查询接口', type: 'select', default: 'kuaidi100', options: [{ label: '快递100', value: 'kuaidi100' }, { label: '阿里云', value: 'aliyun' }, { label: '快递鸟', value: 'kdniao' }] },
+ { key: 'shop_express_query_interval', label: '查询刷新间隔(分钟)', type: 'number', default: 60, min: 30, max: 240 },
+ ],
+ },
+ {
+ key: 'credentials',
+ title: '接口凭据',
+ icon: 'fas fa-key',
+ fields: [
+ { key: 'shop_express_kdniao_opt', label: '快递鸟凭据', type: 'textarea', default: '', help: 'AppID|APIKey', dependsOn: { key: 'shop_express_api_sdk', value: 'kdniao' } },
+ { key: 'shop_express_kuaidi100_opt', label: '快递100凭据', type: 'textarea', default: '', help: 'Key|Customer', dependsOn: { key: 'shop_express_api_sdk', value: 'kuaidi100' } },
+ { key: 'shop_express_aliyun_opt', label: '阿里云凭据', type: 'textarea', default: '', help: 'AppCode', dependsOn: { key: 'shop_express_api_sdk', value: 'aliyun' } },
+ ],
+ },
+ {
+ key: 'companies',
+ title: '物流公司',
+ icon: 'fas fa-building',
+ fields: [
+ { key: 'shop_shipping_company', label: '物流公司列表', type: 'textarea', default: '', help: '每行一个:编码|名称,如 SF|顺丰速运' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/shop/single.js b/assets/admin/src/schema/sections/shop/single.js
new file mode 100644
index 00000000..e2056c55
--- /dev/null
+++ b/assets/admin/src/schema/sections/shop/single.js
@@ -0,0 +1,45 @@
+export const singleSchema = {
+ key: 'shop.single',
+ title: '商品详情',
+ icon: 'fas fa-box-open',
+ description: '商品详情页布局、相关推荐和底部内容设置。',
+ defaults: {
+ shop_content_layout: 'adapt',
+ shop_content_show_bg: false,
+ shop_single_tab: '',
+ shop_content_after: '',
+ shop_single_related_s: true,
+ shop_single_related_opt: '',
+ shop_main_image_default: '',
+ },
+ groups: [
+ {
+ key: 'layout',
+ title: '详情页布局',
+ icon: 'fas fa-columns',
+ fields: [
+ { key: 'shop_content_layout', label: '内容布局', type: 'select', default: 'adapt', options: [{ label: '适应宽度', value: 'adapt' }, { label: '全屏', value: 'full' }, { label: '适应宽度+侧栏', value: 'sidebar' }] },
+ { key: 'shop_content_show_bg', label: '详情背景盒子', type: 'switch', default: false },
+ { key: 'shop_single_tab', label: '自定义 TAB 栏目', type: 'textarea', default: '', help: '每行一个 TAB:标题|内容 HTML' },
+ { key: 'shop_content_after', label: '详情页底部内容', type: 'textarea', default: '', help: '追加在每个商品详情页底部的 HTML 内容' },
+ ],
+ },
+ {
+ key: 'related',
+ title: '相关推荐',
+ icon: 'fas fa-th',
+ fields: [
+ { key: 'shop_single_related_s', label: '显示相关推荐', type: 'switch', default: true },
+ { key: 'shop_single_related_opt', label: '推荐配置', type: 'textarea', default: '', help: '标题|关联类型(cat/discount/tag)|排序|数量|翻页|样式', dependsOn: { key: 'shop_single_related_s', value: true } },
+ ],
+ },
+ {
+ key: 'image',
+ title: '商品图片',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'shop_main_image_default', label: '默认商品主图', type: 'media', default: '' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/shop/timing.js b/assets/admin/src/schema/sections/shop/timing.js
new file mode 100644
index 00000000..76c40067
--- /dev/null
+++ b/assets/admin/src/schema/sections/shop/timing.js
@@ -0,0 +1,54 @@
+export const timingSchema = {
+ key: 'shop.timing',
+ title: '时效与售后原因',
+ icon: 'fas fa-clock',
+ description: '确认收货、评价、退货时效和售后原因配置。',
+ defaults: {
+ shop_order_receipt_max_day: 15,
+ shop_comment_max_day: 7,
+ shop_comment_placeholder: '',
+ shop_comment_img_num: 3,
+ shop_after_sale_return_express_max_day: 7,
+ shop_after_sale_reason_refund: '',
+ shop_after_sale_reason_return: '',
+ shop_author_show: true,
+ },
+ groups: [
+ {
+ key: 'receipt',
+ title: '确认收货',
+ icon: 'fas fa-check-circle',
+ fields: [
+ { key: 'shop_order_receipt_max_day', label: '自动确认收货(天)', type: 'number', default: 15, min: 1, max: 60, help: '发货后超过此天数自动确认收货' },
+ ],
+ },
+ {
+ key: 'comment',
+ title: '评价时效',
+ icon: 'fas fa-star',
+ fields: [
+ { key: 'shop_comment_max_day', label: '评价时效(天)', type: 'number', default: 7, min: 1, max: 60, help: '确认收货后超过此天数自动好评' },
+ { key: 'shop_comment_placeholder', label: '评价占位文案', type: 'input', default: '' },
+ { key: 'shop_comment_img_num', label: '评价图片数', type: 'number', default: 3, min: 0, max: 9 },
+ ],
+ },
+ {
+ key: 'afterSale',
+ title: '售后退货',
+ icon: 'fas fa-undo-alt',
+ fields: [
+ { key: 'shop_after_sale_return_express_max_day', label: '退货快递时效(天)', type: 'number', default: 7, min: 1, max: 30 },
+ { key: 'shop_after_sale_reason_refund', label: '仅退款原因', type: 'textarea', default: '', help: '每行一个可选原因' },
+ { key: 'shop_after_sale_reason_return', label: '退货退款原因', type: 'textarea', default: '', help: '每行一个可选原因' },
+ ],
+ },
+ {
+ key: 'author',
+ title: '商家信息',
+ icon: 'fas fa-user-tie',
+ fields: [
+ { key: 'shop_author_show', label: '显示商家信息', type: 'switch', default: true },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/tools/backup.js b/assets/admin/src/schema/sections/tools/backup.js
new file mode 100644
index 00000000..8700e521
--- /dev/null
+++ b/assets/admin/src/schema/sections/tools/backup.js
@@ -0,0 +1,29 @@
+export const backupSchema = {
+ key: 'tools.backup',
+ title: '数据备份',
+ icon: 'fas fa-cloud-download-alt',
+ description: '站点数据备份与恢复。',
+ defaults: {},
+ groups: [
+ {
+ key: 'backup',
+ title: '备份设置',
+ icon: 'fas fa-save',
+ fields: [
+ { key: 'backup_auto', label: '自动备份', type: 'switch', help: '启用自动定时备份' },
+ {
+ key: 'backup_frequency',
+ label: '备份频率',
+ type: 'select',
+ options: [
+ { label: '每天', value: 'daily' },
+ { label: '每周', value: 'weekly' },
+ { label: '每月', value: 'monthly' },
+ ],
+ },
+ { key: 'backup_retain', label: '保留份数', type: 'number', min: 3, max: 20, default: 7 },
+ { key: 'backup_now', label: '立即备份', type: 'button', help: '手动触发一次完整备份' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/tools/email-rollback.js b/assets/admin/src/schema/sections/tools/email-rollback.js
new file mode 100644
index 00000000..c83880ce
--- /dev/null
+++ b/assets/admin/src/schema/sections/tools/email-rollback.js
@@ -0,0 +1,29 @@
+export const emailRollbackSchema = {
+ key: 'tools.email-rollback',
+ title: '邮件回滚',
+ icon: 'fas fa-undo',
+ description: '批量回滚已发送的通知邮件。',
+ defaults: {},
+ groups: [
+ {
+ key: 'rollback',
+ title: '邮件回滚',
+ icon: 'fas fa-envelope-open',
+ fields: [
+ {
+ key: 'rollback_type',
+ label: '回滚类型',
+ type: 'select',
+ options: [
+ { label: '按时间范围', value: 'date_range' },
+ { label: '按邮件类型', value: 'email_type' },
+ { label: '全部回滚', value: 'all' },
+ ],
+ },
+ { key: 'rollback_date_from', label: '开始日期', type: 'date' },
+ { key: 'rollback_date_to', label: '结束日期', type: 'date' },
+ { key: 'rollback_execute', label: '执行回滚', type: 'button', danger: true, help: '此操作不可逆' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/tools/health.js b/assets/admin/src/schema/sections/tools/health.js
new file mode 100644
index 00000000..b439bdc1
--- /dev/null
+++ b/assets/admin/src/schema/sections/tools/health.js
@@ -0,0 +1,21 @@
+export const healthSchema = {
+ key: 'tools.health',
+ title: '健康检查',
+ icon: 'fas fa-heartbeat',
+ description: '检查主题运行环境和依赖状态。',
+ defaults: {},
+ groups: [
+ {
+ key: 'check',
+ title: '环境检查',
+ icon: 'fas fa-stethoscope',
+ fields: [
+ { key: 'php_version', label: 'PHP 版本', type: 'display', help: '建议 7.4+' },
+ { key: 'wp_version', label: 'WordPress 版本', type: 'display', help: '建议 5.8+' },
+ { key: 'memory_limit', label: '内存限制', type: 'display', help: '建议 256M+' },
+ { key: 'upload_max_size', label: '上传限制', type: 'display' },
+ { key: 'extensions', label: '必要扩展', type: 'display', help: 'curl, mbstring, json, gd' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/tools/import-export.js b/assets/admin/src/schema/sections/tools/import-export.js
new file mode 100644
index 00000000..a9d6effb
--- /dev/null
+++ b/assets/admin/src/schema/sections/tools/import-export.js
@@ -0,0 +1,26 @@
+export const importExportSchema = {
+ key: 'tools.import-export',
+ title: '导入导出',
+ icon: 'fas fa-exchange-alt',
+ description: '主题配置的导入和导出。',
+ defaults: {},
+ groups: [
+ {
+ key: 'export',
+ title: '导出配置',
+ icon: 'fas fa-file-export',
+ fields: [
+ { key: 'export_settings', label: '导出主题设置', type: 'button', help: '导出当前所有主题配置为 JSON 文件' },
+ { key: 'export_options', label: '导出 WordPress 选项', type: 'button', help: '导出 aksrt_settings 等选项' },
+ ],
+ },
+ {
+ key: 'import',
+ title: '导入配置',
+ icon: 'fas fa-file-import',
+ fields: [
+ { key: 'import_settings', label: '导入主题设置', type: 'file', accept: '.json', help: '导入主题配置文件(将覆盖当前设置)' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/tools/index.js b/assets/admin/src/schema/sections/tools/index.js
new file mode 100644
index 00000000..c4adb66b
--- /dev/null
+++ b/assets/admin/src/schema/sections/tools/index.js
@@ -0,0 +1,18 @@
+/**
+ * Tools & Backup domain schemas.
+ * Each key corresponds to a sub-page under the "备份&导入" section.
+ */
+
+import { importExportSchema } from './import-export.js'
+import { healthSchema } from './health.js'
+import { migrationSchema } from './migration.js'
+import { emailRollbackSchema } from './email-rollback.js'
+import { backupSchema } from './backup.js'
+
+export const toolsSchemas = {
+ 'import-export': importExportSchema,
+ health: healthSchema,
+ migration: migrationSchema,
+ 'email-rollback': emailRollbackSchema,
+ backup: backupSchema,
+}
diff --git a/assets/admin/src/schema/sections/tools/migration.js b/assets/admin/src/schema/sections/tools/migration.js
new file mode 100644
index 00000000..be791aba
--- /dev/null
+++ b/assets/admin/src/schema/sections/tools/migration.js
@@ -0,0 +1,19 @@
+export const migrationSchema = {
+ key: 'tools.migration',
+ title: '数据迁移',
+ icon: 'fas fa-database',
+ description: '数据库结构迁移和版本升级。',
+ defaults: {},
+ groups: [
+ {
+ key: 'status',
+ title: '迁移状态',
+ icon: 'fas fa-info-circle',
+ fields: [
+ { key: 'current_version', label: '当前版本', type: 'display' },
+ { key: 'pending_migrations', label: '待执行迁移', type: 'display' },
+ { key: 'run_migration', label: '执行迁移', type: 'button', help: '手动触发数据库迁移' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/auth.js b/assets/admin/src/schema/sections/users/auth.js
new file mode 100644
index 00000000..3db38214
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/auth.js
@@ -0,0 +1,160 @@
+export const authSchema = {
+ key: 'users.auth',
+ title: '注册登录',
+ icon: 'fas fa-sign-in-alt',
+ description: '注册方式、登录限制、密码策略配置。',
+ defaults: {
+ close_sign: false,
+ user_close_signup: false,
+ close_signin: false,
+ user_agreement_s: true,
+ user_agreement_page: '',
+ user_privacy_s: true,
+ user_privacy_page: '',
+ user_agreement_check: true,
+ no_repetition_name: false,
+ user_nickname_out: '赌博,博彩,代发,代理,刷单,六合彩,admin,test,管理员,客服',
+ user_email_limit_type: 'black',
+ user_email_out: '@tempmail.com\n@throwaway.email\n@guerrillamail.com',
+ user_close_login: false,
+ user_sign_type: 'page',
+ sign_modal_auto_s: false,
+ sign_modal_auto_waiting_time: 5,
+ sign_modal_auto_expires: 1,
+ replace_wp_login: true,
+ admin_sign_key: '',
+ user_signin_phone_s: false,
+ user_signin_nopas_s: false,
+ user_signin_nopas_type: 'email',
+ user_signin_nopas_active: 'nopas_first',
+ user_signup_captch: true,
+ captch_type: 'email',
+ user_signup_no_repas: false,
+ user_repas_captch_type: 'email',
+ bind_phone: false,
+ bind_reminder: 'close',
+ bind_reminder_text: '请绑定手机号以正常使用',
+ bind_reminder_expires: 24,
+ mandatory_bind: 'close',
+ email_set_captch: true,
+ invit_code_s: false,
+ invit_code_mode: 'open',
+ invit_code_open_desc: '邀请码为选填,没有邀请码也可以注册',
+ invit_code_must_desc: '请输入邀请码',
+ sign_page_background: [],
+ sign_theme_mode: 'default',
+ sign_show_header: true,
+ sign_card_position: 'center',
+ sign_show_logo: true,
+ sign_page_footer: '',
+ sign_modal_background: [],
+ sign_modal_show_logo: true,
+ user_logo: [],
+ user_logo_dark: [],
+ },
+ groups: [
+ {
+ key: 'register',
+ title: '注册设置',
+ icon: 'fas fa-user-plus',
+ fields: [
+ { key: 'close_sign', label: '一键禁用登录注册', type: 'switch', default: false, help: '开启后前台禁用全部注册/登录入口(含社交登录),不影响后台登录' },
+ { key: 'user_close_signup', label: '关闭注册', type: 'switch', default: false },
+ { key: 'close_signin', label: '禁用账密登录', type: 'switch', default: false, help: '仅允许社交账号登录' },
+ { key: 'user_agreement_s', label: '用户协议', type: 'switch', default: true },
+ { key: 'user_agreement_page', label: '协议页面', type: 'input', default: '' },
+ { key: 'user_privacy_s', label: '隐私政策', type: 'switch', default: true },
+ { key: 'user_privacy_page', label: '隐私页面', type: 'input', default: '' },
+ { key: 'user_agreement_check', label: '强制勾选协议', type: 'switch', default: true },
+ { key: 'no_repetition_name', label: '禁止重复昵称', type: 'switch', default: false },
+ { key: 'user_nickname_out', label: '昵称黑名单关键词', type: 'textarea', default: '赌博,博彩,代发,代理,刷单,六合彩,admin,test,管理员,客服', help: '一行一个关键词,注册昵称包含关键词将被拒绝' },
+ { key: 'user_email_limit_type', label: '邮箱限制类型', type: 'radio', default: 'black', options: [{ label: '黑名单(拒绝以下后缀)', value: 'black' }, { label: '白名单(仅允许以下后缀)', value: 'white' }] },
+ { key: 'user_email_out', label: '邮箱限制后缀', type: 'textarea', default: '@tempmail.com\n@throwaway.email\n@guerrillamail.com', help: '一行一个邮箱后缀,如 @tempmail.com' },
+ ],
+ },
+ {
+ key: 'login',
+ title: '登录设置',
+ icon: 'fas fa-sign-in-alt',
+ fields: [
+ { key: 'user_close_login', label: '关闭登录', type: 'switch', default: false },
+ { key: 'user_sign_type', label: '登录注册样式', type: 'radio', default: 'page', options: [{ label: '弹窗', value: 'modal' }, { label: '跳转页面', value: 'page' }] },
+ { key: 'sign_modal_auto_s', label: '自动弹出登录窗', type: 'switch', default: false, help: '仅在 user_sign_type=modal 时生效' },
+ { key: 'sign_modal_auto_waiting_time', label: '弹窗等待秒数', type: 'number', default: 5, min: 0, max: 60, dependsOn: { key: 'sign_modal_auto_s', value: true } },
+ { key: 'sign_modal_auto_expires', label: '弹窗周期(小时)', type: 'number', default: 1, min: 0, max: 720, dependsOn: { key: 'sign_modal_auto_s', value: true } },
+ { key: 'replace_wp_login', label: '替换WP登录页', type: 'switch', default: true },
+ { key: 'admin_sign_key', label: '应急登录 Key', type: 'input', default: '', help: '留空为禁用。管理员可通过 /wp-login.php?aksrt_bypass=
绕过前台替换' },
+ { key: 'user_signin_phone_s', label: '手机号登录', type: 'switch', default: false, help: '允许用手机号作为用户名登录' },
+ { key: 'user_signin_nopas_s', label: '免密登录', type: 'switch', default: false },
+ { key: 'user_signin_nopas_type', label: '免密登录方式', type: 'radio', default: 'email', options: [{ label: '邮箱验证码', value: 'email' }, { label: '手机验证码', value: 'phone' }, { label: '邮箱或手机', value: 'email_phone' }], dependsOn: { key: 'user_signin_nopas_s', value: true } },
+ { key: 'user_signin_nopas_active', label: '免密显示模式', type: 'radio', default: 'nopas_first', options: [{ label: '优先免密', value: 'nopas_first' }, { label: '优先密码', value: 'password_first' }, { label: '仅免密', value: 'nopas_only' }], dependsOn: { key: 'user_signin_nopas_s', value: true } },
+ ],
+ },
+ {
+ key: 'signup_captcha',
+ title: '注册验证',
+ icon: 'fas fa-user-shield',
+ fields: [
+ { key: 'user_signup_captch', label: '注册需验证', type: 'switch', default: true, help: '注册时需完成邮箱或手机验证' },
+ { key: 'captch_type', label: '验证方式', type: 'radio', default: 'email', options: [{ label: '邮箱', value: 'email' }, { label: '手机', value: 'phone' }, { label: '邮箱或手机', value: 'email_phone' }], dependsOn: { key: 'user_signup_captch', value: true } },
+ { key: 'user_signup_no_repas', label: '无需重复密码', type: 'switch', default: false, help: '注册时不要求二次确认密码' },
+ { key: 'user_repas_captch_type', label: '找回密码验证方式', type: 'radio', default: 'email', options: [{ label: '邮箱', value: 'email' }, { label: '手机', value: 'phone' }, { label: '邮箱或手机', value: 'email_phone' }] },
+ ],
+ },
+ {
+ key: 'bind',
+ title: '绑定设置',
+ icon: 'fas fa-link',
+ fields: [
+ { key: 'bind_phone', label: '绑定手机', type: 'switch', default: false },
+ { key: 'bind_reminder', label: '绑定提醒', type: 'radio', default: 'close', options: [{ label: '提醒绑定邮箱', value: 'email' }, { label: '提醒绑定手机', value: 'phone' }, { label: '提醒绑定邮箱和手机', value: 'email_phone' }, { label: '关闭', value: 'close' }] },
+ { key: 'bind_reminder_text', label: '提醒文案', type: 'input', default: '请绑定手机号以正常使用' },
+ { key: 'bind_reminder_expires', label: '提醒周期(小时)', type: 'number', default: 24, min: 1, max: 720, help: '用户关闭提醒后多少小时内不再提醒' },
+ { key: 'mandatory_bind', label: '强制绑定', type: 'radio', default: 'close', options: [{ label: '强制绑定邮箱', value: 'email' }, { label: '强制绑定手机', value: 'phone' }, { label: '强制绑定邮箱和手机', value: 'email_phone' }, { label: '关闭', value: 'close' }] },
+ { key: 'email_set_captch', label: '绑定邮箱需验证', type: 'switch', default: true, help: '绑定邮箱时需发送验证码确认' },
+ ],
+ },
+ {
+ key: 'invite_inline',
+ title: '邀请码注册',
+ icon: 'fas fa-ticket-alt',
+ fields: [
+ { key: 'invit_code_s', label: '启用邀请码', type: 'switch', default: false, help: '与「邀请码」子页联动,此处开关控制注册表单是否显示邀请码字段' },
+ { key: 'invit_code_mode', label: '邀请模式', type: 'select', default: 'open', options: [{ label: '开放注册(邀请码选填)', value: 'open' }, { label: '必须邀请码', value: 'must' }], dependsOn: { key: 'invit_code_s', value: true } },
+ { key: 'invit_code_open_desc', label: '开放注册提示', type: 'input', default: '邀请码为选填,没有邀请码也可以注册', dependsOn: { key: 'invit_code_s', value: true } },
+ { key: 'invit_code_must_desc', label: '必填提示', type: 'input', default: '请输入邀请码', dependsOn: { key: 'invit_code_s', value: true } },
+ ],
+ },
+ {
+ key: 'sign_page_ui',
+ title: '页面 UI 配置',
+ icon: 'fas fa-palette',
+ fields: [
+ { key: 'sign_page_background', label: '页面背景图', type: 'gallery', default: [], help: '登录注册页背景图,多图随机' },
+ { key: 'sign_theme_mode', label: '主题模式', type: 'radio', default: 'default', options: [{ label: '默认', value: 'default' }, { label: '亮色', value: 'light' }, { label: '暗色', value: 'dark' }] },
+ { key: 'sign_show_header', label: '显示顶部导航', type: 'switch', default: true },
+ { key: 'sign_card_position', label: '卡片位置', type: 'radio', default: 'center', options: [{ label: '靠左', value: 'left' }, { label: '居中', value: 'center' }, { label: '靠右', value: 'right' }] },
+ { key: 'sign_show_logo', label: '显示 LOGO', type: 'switch', default: true },
+ { key: 'sign_page_footer', label: '页脚 HTML', type: 'textarea', default: '', help: '登录注册页自定义页脚内容,支持 HTML' },
+ ],
+ },
+ {
+ key: 'modal_ui',
+ title: '弹窗 UI 配置',
+ icon: 'fas fa-window-restore',
+ fields: [
+ { key: 'sign_modal_background', label: '弹窗背景图', type: 'gallery', default: [], help: '弹窗左侧背景图,多图随机' },
+ { key: 'sign_modal_show_logo', label: '弹窗显示 LOGO', type: 'switch', default: true },
+ ],
+ },
+ {
+ key: 'sign_logo',
+ title: '登录框 LOGO',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'user_logo', label: '日间 LOGO', type: 'gallery', default: [], help: '登录框专用日间 LOGO,留空使用站点全局 LOGO' },
+ { key: 'user_logo_dark', label: '夜间 LOGO', type: 'gallery', default: [], help: '登录框专用夜间 LOGO,留空使用站点全局夜间 LOGO' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/badge.js b/assets/admin/src/schema/sections/users/badge.js
new file mode 100644
index 00000000..48fc7c91
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/badge.js
@@ -0,0 +1,32 @@
+export const badgeSchema = {
+ key: 'users.badge',
+ title: '用户徽章',
+ icon: 'fas fa-award',
+ description: '配置徽章字典、公开展示数量和前台挂点,具体授予逻辑后续扩展。',
+ defaults: {
+ user_badge_enabled: false,
+ user_badge_public_display: true,
+ user_badge_limit: 3,
+ user_badge_dictionary: 'founder|创始成员|warning\nstaff|站务团队|info',
+ },
+ groups: [
+ {
+ key: 'display',
+ title: '徽章展示',
+ icon: 'fas fa-medal',
+ fields: [
+ { key: 'user_badge_enabled', label: '启用徽章系统', type: 'switch', default: false },
+ { key: 'user_badge_public_display', label: '公开页显示徽章', type: 'switch', default: true, dependsOn: { key: 'user_badge_enabled', value: true } },
+ { key: 'user_badge_limit', label: '最多显示数量', type: 'number', default: 3, min: 1, max: 8, dependsOn: { key: 'user_badge_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'dictionary',
+ title: '徽章字典',
+ icon: 'fas fa-bookmark',
+ fields: [
+ { key: 'user_badge_dictionary', label: '徽章列表', type: 'textarea', default: 'founder|创始成员|warning\nstaff|站务团队|info', help: '每行一个徽章:slug|显示文案|色调(neutral/info/success/warning/brand)', dependsOn: { key: 'user_badge_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/credits.js b/assets/admin/src/schema/sections/users/credits.js
new file mode 100644
index 00000000..87d11a32
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/credits.js
@@ -0,0 +1,98 @@
+export const creditsSchema = {
+ key: 'users.credits',
+ title: '用户积分',
+ icon: 'fas fa-coins',
+ description: '积分规则、积分日志、用户积分管理。',
+ defaults: {
+ points_s: true,
+ points_pay_s: false,
+ points_rate: 100,
+ points_name: '积分',
+ points_decimal: 2,
+ points_earn_comment: 1,
+ points_earn_post: 5,
+ points_earn_login: 1,
+ points_earn_like_received: 1,
+ points_earn_invite: 10,
+ points_earn_daily_limit: 50,
+ points_spend_comment: 0,
+ points_spend_download: 0,
+ points_spend_view_hidden: 0,
+ points_transfer_s: false,
+ points_service_charge: 0,
+ points_transfer_min: 1,
+ points_deduction_s: false,
+ points_deduction_rate: 100,
+ points_deduction_max: 50,
+ points_show_balance: true,
+ points_show_log: true,
+ points_expire_days: 0,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '积分基础',
+ icon: 'fas fa-coins',
+ fields: [
+ { key: 'points_s', label: '启用积分系统', type: 'switch', default: true },
+ { key: 'points_pay_s', label: '积分支付', type: 'switch', default: false },
+ { key: 'points_rate', label: '积分兑换比例', type: 'number', default: 100, min: 1, max: 10000 },
+ { key: 'points_name', label: '积分名称', type: 'input', default: '积分' },
+ { key: 'points_decimal', label: '积分小数位数', type: 'number', default: 2, min: 0, max: 4 },
+ ],
+ },
+ {
+ key: 'earn',
+ title: '积分获取',
+ icon: 'fas fa-arrow-up',
+ fields: [
+ { key: 'points_earn_comment', label: '评论奖励', type: 'number', default: 1, min: 0, max: 100 },
+ { key: 'points_earn_post', label: '发布文章奖励', type: 'number', default: 5, min: 0, max: 100 },
+ { key: 'points_earn_login', label: '每日登录奖励', type: 'number', default: 1, min: 0, max: 100 },
+ { key: 'points_earn_like_received', label: '收到点赞奖励', type: 'number', default: 1, min: 0, max: 100 },
+ { key: 'points_earn_invite', label: '邀请注册奖励', type: 'number', default: 10, min: 0, max: 1000 },
+ { key: 'points_earn_daily_limit', label: '每日获取上限', type: 'number', default: 50, min: 0, max: 9999, help: '0为不限制' },
+ ],
+ },
+ {
+ key: 'spend',
+ title: '积分消费',
+ icon: 'fas fa-arrow-down',
+ fields: [
+ { key: 'points_spend_comment', label: '评论消耗', type: 'number', default: 0, min: 0, max: 100 },
+ { key: 'points_spend_download', label: '下载消耗', type: 'number', default: 0, min: 0, max: 100 },
+ { key: 'points_spend_view_hidden', label: '查看隐藏内容消耗', type: 'number', default: 0, min: 0, max: 100 },
+ ],
+ },
+ {
+ key: 'transfer',
+ title: '积分转账',
+ icon: 'fas fa-exchange-alt',
+ fields: [
+ { key: 'points_transfer_s', label: '启用积分转账', type: 'switch', default: false },
+ { key: 'points_service_charge', label: '转账手续费(%)', type: 'number', default: 0, min: 0, max: 50, dependsOn: { key: 'points_transfer_s', value: true } },
+ { key: 'points_transfer_min', label: '最低转账金额', type: 'number', default: 1, min: 1, max: 9999, dependsOn: { key: 'points_transfer_s', value: true } },
+ ],
+ },
+ {
+ key: 'deduction',
+ title: '积分抵扣',
+ icon: 'fas fa-percent',
+ fields: [
+ { key: 'points_deduction_s', label: '启用积分抵扣', type: 'switch', default: false },
+ { key: 'points_deduction_rate', label: '抵扣比例(%)', type: 'number', default: 100, min: 1, max: 100, dependsOn: { key: 'points_deduction_s', value: true } },
+ { key: 'points_deduction_max', label: '最大抵扣比例(%)', type: 'number', default: 50, min: 1, max: 100, dependsOn: { key: 'points_deduction_s', value: true } },
+ ],
+ },
+ {
+ key: 'display',
+ title: '展示设置',
+ icon: 'fas fa-eye',
+ fields: [
+ { key: 'points_show_balance', label: '导航栏显示余额', type: 'switch', default: true },
+ { key: 'points_show_log', label: '显示积分记录', type: 'switch', default: true },
+ { key: 'points_expire_days', label: '积分有效期(天)', type: 'number', default: 0, min: 0, help: '0为永久有效' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/features.js b/assets/admin/src/schema/sections/users/features.js
new file mode 100644
index 00000000..a4efb658
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/features.js
@@ -0,0 +1,45 @@
+export const featuresSchema = {
+ key: 'users.features',
+ title: '用户功能',
+ icon: 'fas fa-user-cog',
+ description: '配置用户中心和个人主页的功能模块。',
+ defaults: {
+ user_center_tabs: 'profile,posts,comments,credits,favorites',
+ user_avatar_upload: true,
+ user_cover_upload: true,
+ user_social_links: true,
+ user_desc_std: '这家伙很懒…',
+ user_city_s: false,
+ user_join_day_my_name: '本站',
+ user_avatar_default: '',
+ user_cover_img: '',
+ user_rewrite_slug: 'user',
+ user_follow_s: true,
+ user_message_s: true,
+ user_online_s: true,
+ user_last_active_s: true,
+ },
+ groups: [
+ {
+ key: 'center',
+ title: '用户中心',
+ icon: 'fas fa-user-shield',
+ fields: [
+ { key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,posts,comments,credits,favorites', help: '逗号分隔' },
+ { key: 'user_avatar_upload', label: '头像上传', type: 'switch', default: true },
+ { key: 'user_cover_upload', label: '封面上传', type: 'switch', default: true },
+ { key: 'user_social_links', label: '社交链接', type: 'switch', default: true },
+ { key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' },
+ { key: 'user_city_s', label: '显示城市', type: 'switch', default: false },
+ { key: 'user_join_day_my_name', label: '站名', type: 'input', default: '本站', help: '用于「已加入 X 天」显示中的站点名称' },
+ { key: 'user_avatar_default', label: '默认头像', type: 'media', default: '' },
+ { key: 'user_cover_img', label: '默认封面', type: 'media', default: '' },
+ { key: 'user_rewrite_slug', label: '用户页 slug', type: 'input', default: 'user' },
+ { key: 'user_follow_s', label: '启用关注功能', type: 'switch', default: true },
+ { key: 'user_message_s', label: '启用私信功能', type: 'switch', default: true },
+ { key: 'user_online_s', label: '显示在线状态', type: 'switch', default: true },
+ { key: 'user_last_active_s', label: '显示最后活跃', type: 'switch', default: true },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/index.js b/assets/admin/src/schema/sections/users/index.js
new file mode 100644
index 00000000..346ad398
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/index.js
@@ -0,0 +1,33 @@
+/**
+ * Users & Interaction domain schemas.
+ */
+
+import { authSchema } from './auth.js'
+import { oauthSchema } from './oauth.js'
+import { notificationsSchema } from './notifications.js'
+import { creditsSchema } from './credits.js'
+import { inviteSchema } from './invite.js'
+import { featuresSchema } from './features.js'
+import { signinSchema } from './signin.js'
+import { verifySchema } from './verify.js'
+import { levelSchema } from './level.js'
+import { badgeSchema } from './badge.js'
+import { wechatSchema } from './wechat.js'
+import { reportSchema } from './report.js'
+import { smsSchema } from './sms.js'
+
+export const usersSchemas = {
+ auth: authSchema,
+ oauth: oauthSchema,
+ notifications: notificationsSchema,
+ credits: creditsSchema,
+ invite: inviteSchema,
+ features: featuresSchema,
+ signin: signinSchema,
+ verify: verifySchema,
+ level: levelSchema,
+ badge: badgeSchema,
+ wechat: wechatSchema,
+ report: reportSchema,
+ sms: smsSchema,
+}
diff --git a/assets/admin/src/schema/sections/users/invite.js b/assets/admin/src/schema/sections/users/invite.js
new file mode 100644
index 00000000..2cc50e7c
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/invite.js
@@ -0,0 +1,68 @@
+export const inviteSchema = {
+ key: 'users.invite',
+ title: '邀请码',
+ icon: 'fas fa-ticket-alt',
+ description: '邀请码生成、管理、奖励配置。',
+ defaults: {
+ invit_code_s: false,
+ invit_code_open_desc: '邀请码为选填,没有邀请码也可以注册',
+ invit_code_must_desc: '请输入邀请码',
+ invit_code_mode: 'open',
+ invit_code_length: 8,
+ invit_code_prefix: '',
+ invit_code_max_use: 1,
+ invit_code_expiry: 0,
+ invit_code_batch: 20,
+ invit_reward_points: 0,
+ invit_reward_level: 0,
+ invit_reward_vip_days: 0,
+ invit_invitee_reward: false,
+ invit_code_show_count: true,
+ invit_code_show_remain: true,
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '邀请设置',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'invit_code_s', label: '启用邀请码', type: 'switch', default: false },
+ { key: 'invit_code_open_desc', label: '开放注册提示', type: 'input', default: '邀请码为选填,没有邀请码也可以注册' },
+ { key: 'invit_code_must_desc', label: '必填提示', type: 'input', default: '请输入邀请码' },
+ { key: 'invit_code_mode', label: '邀请模式', type: 'select', default: 'open', options: [{ label: '开放注册(邀请码选填)', value: 'open' }, { label: '必须邀请码', value: 'must' }] },
+ ],
+ },
+ {
+ key: 'format',
+ title: '码面设置',
+ icon: 'fas fa-barcode',
+ fields: [
+ { key: 'invit_code_length', label: '邀请码长度', type: 'number', default: 8, min: 4, max: 32 },
+ { key: 'invit_code_prefix', label: '邀请码前缀', type: 'input', default: '' },
+ { key: 'invit_code_max_use', label: '单码最大使用次数', type: 'number', default: 1, min: 1, max: 9999 },
+ { key: 'invit_code_expiry', label: '有效期(天)', type: 'number', default: 0, min: 0, help: '0为永久有效' },
+ { key: 'invit_code_batch', label: '批量生成数量', type: 'number', default: 20, min: 1, max: 500 },
+ ],
+ },
+ {
+ key: 'reward',
+ title: '邀请奖励',
+ icon: 'fas fa-gift',
+ fields: [
+ { key: 'invit_reward_points', label: '邀请奖励积分', type: 'number', default: 0, min: 0, max: 1000 },
+ { key: 'invit_reward_level', label: '邀请奖励等级', type: 'number', default: 0, min: 0, max: 10 },
+ { key: 'invit_reward_vip_days', label: '奖励VIP天数', type: 'number', default: 0, min: 0, max: 3650 },
+ { key: 'invit_invitee_reward', label: '被邀请人奖励', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'display',
+ title: '展示设置',
+ icon: 'fas fa-eye',
+ fields: [
+ { key: 'invit_code_show_count', label: '显示已使用次数', type: 'switch', default: true },
+ { key: 'invit_code_show_remain', label: '显示剩余次数', type: 'switch', default: true },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/level.js b/assets/admin/src/schema/sections/users/level.js
new file mode 100644
index 00000000..42f96ce9
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/level.js
@@ -0,0 +1,34 @@
+export const levelSchema = {
+ key: 'users.level',
+ title: '用户等级',
+ icon: 'fas fa-level-up-alt',
+ description: '配置等级展示前缀和等级阈值,当前可基于积分余额自动计算。',
+ defaults: {
+ user_level_enabled: false,
+ user_level_public_display: true,
+ user_level_source: 'credits_balance',
+ user_level_prefix: 'Lv.',
+ user_level_thresholds: '1|0\n2|30\n3|100\n4|300',
+ },
+ groups: [
+ {
+ key: 'display',
+ title: '等级显示',
+ icon: 'fas fa-signal',
+ fields: [
+ { key: 'user_level_enabled', label: '启用等级系统', type: 'switch', default: false },
+ { key: 'user_level_public_display', label: '公开页显示等级', type: 'switch', default: true, dependsOn: { key: 'user_level_enabled', value: true } },
+ { key: 'user_level_prefix', label: '等级前缀', type: 'input', default: 'Lv.', dependsOn: { key: 'user_level_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'rules',
+ title: '等级规则',
+ icon: 'fas fa-list-ol',
+ fields: [
+ { key: 'user_level_source', label: '等级来源', type: 'radio', default: 'credits_balance', options: [{ label: '积分余额', value: 'credits_balance' }, { label: '手动 Meta', value: 'manual_meta' }], dependsOn: { key: 'user_level_enabled', value: true } },
+ { key: 'user_level_thresholds', label: '等级阈值', type: 'textarea', default: '1|0\n2|30\n3|100\n4|300', help: '每行一个等级,格式:等级|所需积分,例如 3|100', dependsOn: { key: 'user_level_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/notifications.js b/assets/admin/src/schema/sections/users/notifications.js
new file mode 100644
index 00000000..2012ecb7
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/notifications.js
@@ -0,0 +1,74 @@
+export const notificationsSchema = {
+ key: 'users.notifications',
+ title: '消息通知',
+ icon: 'fas fa-bell',
+ description: '站内通知、私信、通知策略配置。',
+ defaults: {
+ message_s: true,
+ msg_center_rewrite_slug: 'message',
+ message_icon_show: ['nav_menu'],
+ message_paginate_type: 'ajax_lists',
+ message_user_set: true,
+ message_close_msg_type: [],
+ message_auto_clear_s: false,
+ message_auto_clear_time: 365,
+ message_auto_clear_types: ['posts', 'comment', 'favorite', 'like', 'hot', 'followed', 'system'],
+ private_s: true,
+ private_smilie_s: true,
+ private_code_s: false,
+ private_image_s: true,
+ private_upload_img: false,
+ private_quick_s: true,
+ private_quick_often: '你好,请问有什么可以帮你的?\n感谢你的反馈\n已处理,请查收',
+ private_submit_text: '发送',
+ private_placeholder: '请输入消息...',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '基础设置',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'message_s', label: '站内通知总开关', type: 'switch', default: true },
+ { key: 'msg_center_rewrite_slug', label: '消息中心 URL 别名', type: 'input', default: 'message' },
+ { key: 'message_icon_show', label: '通知图标位置', type: 'checkbox', default: ['nav_menu'], options: [{ label: '导航菜单', value: 'nav_menu' }, { label: '用户头像', value: 'avatar' }, { label: '移动端底部', value: 'mobile_bar' }] },
+ { key: 'message_paginate_type', label: '列表翻页模式', type: 'radio', default: 'ajax_lists', options: [{ label: 'AJAX 翻页', value: 'ajax_lists' }, { label: '传统翻页', value: 'default' }] },
+ { key: 'message_user_set', label: '允许用户设置推送', type: 'switch', default: true, help: '允许用户在前台自行设置通知推送偏好' },
+ ],
+ },
+ {
+ key: 'types',
+ title: '通知类型',
+ icon: 'fas fa-list-check',
+ fields: [
+ { key: 'message_close_msg_type', label: '关闭部分推送', type: 'checkbox', default: [], options: [{ label: '文章通知', value: 'posts' }, { label: '评论回复', value: 'comment' }, { label: '收藏通知', value: 'favorite' }, { label: '点赞通知', value: 'like' }, { label: '热门通知', value: 'hot' }, { label: '关注通知', value: 'followed' }, { label: '系统通知', value: 'system' }, { label: '支付通知', value: 'pay' }, { label: '提现回复', value: 'withdraw_reply' }, { label: '勋章通知', value: 'medal' }, { label: '认证回复', value: 'auth_apply_reply' }], help: '勾选的类型将不向用户推送通知' },
+ ],
+ },
+ {
+ key: 'auto_clear',
+ title: '自动清理',
+ icon: 'fas fa-broom',
+ fields: [
+ { key: 'message_auto_clear_s', label: '自动清理已读消息', type: 'switch', default: false },
+ { key: 'message_auto_clear_time', label: '过期天数', type: 'number', default: 365, min: 1, max: 3650, help: '已读消息超过此天数将被自动清理', dependsOn: { key: 'message_auto_clear_s', value: true } },
+ { key: 'message_auto_clear_types', label: '清理类型', type: 'checkbox', default: ['posts', 'comment', 'favorite', 'like', 'hot', 'followed', 'system'], options: [{ label: '文章通知', value: 'posts' }, { label: '评论回复', value: 'comment' }, { label: '收藏通知', value: 'favorite' }, { label: '点赞通知', value: 'like' }, { label: '热门通知', value: 'hot' }, { label: '关注通知', value: 'followed' }, { label: '系统通知', value: 'system' }, { label: '支付通知', value: 'pay' }, { label: '提现回复', value: 'withdraw_reply' }, { label: '勋章通知', value: 'medal' }, { label: '认证回复', value: 'auth_apply_reply' }, { label: '下载通知', value: 'download' }, { label: '积分变动', value: 'credits' }, { label: '私信', value: 'private' }, { label: '举报反馈', value: 'report' }], dependsOn: { key: 'message_auto_clear_s', value: true } },
+ ],
+ },
+ {
+ key: 'private',
+ title: '私信设置',
+ icon: 'fas fa-envelope',
+ fields: [
+ { key: 'private_s', label: '启用私信功能', type: 'switch', default: true },
+ { key: 'private_smilie_s', label: '支持表情', type: 'switch', default: true, dependsOn: { key: 'private_s', value: true } },
+ { key: 'private_code_s', label: '支持代码', type: 'switch', default: false, dependsOn: { key: 'private_s', value: true } },
+ { key: 'private_image_s', label: '支持图片', type: 'switch', default: true, dependsOn: { key: 'private_s', value: true } },
+ { key: 'private_upload_img', label: '支持上传图片', type: 'switch', default: false, dependsOn: { key: 'private_s', value: true } },
+ { key: 'private_quick_s', label: '快捷回复', type: 'switch', default: true, dependsOn: { key: 'private_s', value: true } },
+ { key: 'private_quick_often', label: '常用快捷回复', type: 'textarea', default: '你好,请问有什么可以帮你的?\n感谢你的反馈\n已处理,请查收', help: '一行一条快捷回复文案', dependsOn: { key: 'private_s', value: true } },
+ { key: 'private_submit_text', label: '发送按钮文案', type: 'input', default: '发送', dependsOn: { key: 'private_s', value: true } },
+ { key: 'private_placeholder', label: '输入占位符', type: 'input', default: '请输入消息...', dependsOn: { key: 'private_s', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/oauth.js b/assets/admin/src/schema/sections/users/oauth.js
new file mode 100644
index 00000000..a5687784
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/oauth.js
@@ -0,0 +1,188 @@
+export const oauthSchema = {
+ key: 'users.oauth',
+ title: '第三方登录',
+ icon: 'fas fa-share-alt',
+ description: 'GitHub、Gitee、QQ、Google 等第三方登录配置。',
+ defaults: {
+ oauth_s: false,
+ oauth_proxy_mode: 'close',
+ oauth_proxy_secret: '',
+ oauth_proxy_server_url: '',
+ oauth_proxy_server_providers: ['github', 'gitee', 'qq', 'google'],
+ oauth_proxy_client_providers: ['github', 'gitee', 'qq', 'google'],
+ oauth_proxy_gzh_type: 'service',
+ oauth_bind_type: 'auto',
+ oauth_button_lg: false,
+ weixingzh_priority: false,
+ weixingzh_auto: false,
+ weixingzh_auto_expires: 3600,
+ oauth_github_s: false,
+ oauth_gitee_s: false,
+ oauth_qq_s: false,
+ oauth_weixingzh_s: false,
+ oauth_weixin_s: false,
+ oauth_weibo_s: false,
+ oauth_baidu_s: false,
+ oauth_alipay_s: false,
+ github_appid: '',
+ github_appkey: '',
+ gitee_appid: '',
+ gitee_appkey: '',
+ qq_appid: '',
+ qq_appkey: '',
+ weixin_appid: '',
+ weixin_appsecret: '',
+ weixingzh_gzh_type: 'service',
+ weixingzh_token: '',
+ weixingzh_token_nocheck: false,
+ weixingzh_encoding_aes_key: '',
+ weixingzh_appid: '',
+ weixingzh_appsecret: '',
+ weixingzh_code_qrcode: '',
+ weixingzh_code_keyword: '登录',
+ weixingzh_code_reply: '您的验证码是:%s,有效期 %d 分钟。',
+ weixingzh_code_subscribe_reply: '欢迎关注!回复「%s」获取登录验证码。',
+ weixingzh_subscribe_msg: '',
+ weixingzh_new_subscribe_msg: '',
+ weixingzh_scan_msg: '',
+ weixingzh_auto_reply_text: '',
+ weixingzh_auto_reply_search: '',
+ weixingzh_auto_reply_image: '',
+ weixingzh_auto_reply_voice: '',
+ weixingzh_auto_reply_default: '暂不支持此类型的消息',
+ weixingzh_gzh_menu: '',
+ weibo_appid: '',
+ weibo_appkey: '',
+ baidu_appid: '',
+ baidu_appkey: '',
+ alipay_appid: '',
+ alipay_privatekey: '',
+ alipay_publickey: '',
+ },
+ groups: [
+ {
+ key: 'basic',
+ title: '基础设置',
+ icon: 'fas fa-cog',
+ fields: [
+ { key: 'oauth_s', label: '启用第三方登录', type: 'switch', default: false },
+ { key: 'oauth_proxy_mode', label: '代理登录模式', type: 'radio', default: 'close', options: [{ label: '关闭', value: 'close' }, { label: '服务端', value: 'server' }, { label: '客户端', value: 'client' }] },
+ { key: 'oauth_proxy_secret', label: '代理 HMAC 密钥', type: 'input', default: '', dependsOn: { key: 'oauth_proxy_mode', not: 'close' } },
+ { key: 'oauth_proxy_server_url', label: '服务端地址', type: 'input', default: '', dependsOn: { key: 'oauth_proxy_mode', value: 'client' } },
+ { key: 'oauth_proxy_server_providers', label: '服务端允许平台', type: 'checkbox', default: ['github', 'gitee', 'qq', 'google'], options: [{ label: 'GitHub', value: 'github' }, { label: 'Gitee', value: 'gitee' }, { label: 'QQ', value: 'qq' }, { label: 'Google', value: 'google' }, { label: '微博', value: 'weibo' }, { label: '微信', value: 'weixin' }, { label: '百度', value: 'baidu' }, { label: '支付宝', value: 'alipay' }], dependsOn: { key: 'oauth_proxy_mode', value: 'server' } },
+ { key: 'oauth_proxy_client_providers', label: '客户端启用平台', type: 'checkbox', default: ['github', 'gitee', 'qq', 'google'], options: [{ label: 'GitHub', value: 'github' }, { label: 'Gitee', value: 'gitee' }, { label: 'QQ', value: 'qq' }, { label: 'Google', value: 'google' }, { label: '微博', value: 'weibo' }, { label: '微信', value: 'weixin' }, { label: '百度', value: 'baidu' }, { label: '支付宝', value: 'alipay' }], dependsOn: { key: 'oauth_proxy_mode', value: 'client' } },
+ { key: 'oauth_proxy_gzh_type', label: '代理公众号类型', type: 'radio', default: 'service', options: [{ label: '服务号', value: 'service' }, { label: '订阅号', value: 'subscribe' }], dependsOn: { key: 'oauth_proxy_mode', not: 'close' } },
+ { key: 'oauth_bind_type', label: '绑定方式', type: 'radio', default: 'auto', options: [{ label: '自动绑定', value: 'auto' }, { label: '手动绑定', value: 'manual' }] },
+ { key: 'oauth_button_lg', label: '大按钮样式', type: 'switch', default: false },
+ { key: 'weixingzh_priority', label: 'PC端优先微信扫码', type: 'switch', default: false, help: '开启后 PC 端登录框优先显示微信公众号扫码' },
+ { key: 'weixingzh_auto', label: '微信APP内自动登录', type: 'switch', default: false, help: '在微信内置浏览器中自动使用公众号授权登录' },
+ { key: 'weixingzh_auto_expires', label: '自动登录频率限制 (秒)', type: 'number', default: 3600, min: 0, max: 86400, dependsOn: { key: 'weixingzh_auto', value: true } },
+ ],
+ },
+ {
+ key: 'providers',
+ title: '登录提供商',
+ icon: 'fas fa-th',
+ fields: [
+ { key: 'oauth_github_s', label: 'GitHub登录', type: 'switch', default: false },
+ { key: 'oauth_gitee_s', label: 'Gitee登录', type: 'switch', default: false },
+ { key: 'oauth_qq_s', label: 'QQ登录', type: 'switch', default: false },
+ { key: 'oauth_weixingzh_s', label: '微信公众号登录', type: 'switch', default: false },
+ { key: 'oauth_weixin_s', label: '微信开放平台登录', type: 'switch', default: false },
+ { key: 'oauth_weibo_s', label: '微博登录', type: 'switch', default: false },
+ { key: 'oauth_baidu_s', label: '百度登录', type: 'switch', default: false },
+ { key: 'oauth_alipay_s', label: '支付宝登录', type: 'switch', default: false },
+ ],
+ },
+ {
+ key: 'github',
+ title: 'GitHub配置',
+ icon: 'fab fa-github',
+ fields: [
+ { key: 'github_appid', label: 'Client ID', type: 'input', default: '' },
+ { key: 'github_appkey', label: 'Client Secret', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'gitee',
+ title: 'Gitee配置',
+ icon: 'fas fa-code-branch',
+ fields: [
+ { key: 'gitee_appid', label: 'Client ID', type: 'input', default: '' },
+ { key: 'gitee_appkey', label: 'Client Secret', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'qq',
+ title: 'QQ配置',
+ icon: 'fab fa-qq',
+ fields: [
+ { key: 'qq_appid', label: 'App ID', type: 'input', default: '' },
+ { key: 'qq_appkey', label: 'App Key', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'weixin',
+ title: '微信开放平台配置',
+ icon: 'fab fa-weixin',
+ fields: [
+ { key: 'weixin_appid', label: 'AppID', type: 'input', default: '' },
+ { key: 'weixin_appsecret', label: 'AppSecret', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'weixingzh',
+ title: '微信公众号配置',
+ icon: 'fab fa-weixin',
+ fields: [
+ { key: 'weixingzh_gzh_type', label: '公众号类型', type: 'radio', default: 'service', options: [{ label: '服务号', value: 'service' }, { label: '订阅号', value: 'subscribe' }] },
+ { key: 'weixingzh_token', label: 'Token', type: 'input', default: '' },
+ { key: 'weixingzh_token_nocheck', label: '不验证Token', type: 'switch', default: false, help: '开发调试时可跳过 Token 验证' },
+ { key: 'weixingzh_encoding_aes_key', label: 'EncodingAESKey', type: 'input', default: '' },
+ { key: 'weixingzh_appid', label: 'AppID', type: 'input', default: '' },
+ { key: 'weixingzh_appsecret', label: 'AppSecret', type: 'input', default: '' },
+ { key: 'weixingzh_code_qrcode', label: '公众号二维码', type: 'media', default: '', help: '引导用户扫码关注登录的二维码图片' },
+ { key: 'weixingzh_code_keyword', label: '登录回复关键词', type: 'input', default: '登录', help: '用户在公众号内回复此关键词触发登录验证码' },
+ { key: 'weixingzh_code_reply', label: '验证码回复模板', type: 'textarea', default: '您的验证码是:%s,有效期 %d 分钟。', help: '%s=验证码,%d=有效分钟数' },
+ { key: 'weixingzh_code_subscribe_reply', label: '新关注回复模板', type: 'textarea', default: '欢迎关注!回复「%s」获取登录验证码。', help: '%s=登录关键词' },
+ { key: 'weixingzh_subscribe_msg', label: '关注后回复', type: 'textarea', default: '', help: '用户关注公众号后的自动回复文案' },
+ { key: 'weixingzh_new_subscribe_msg', label: '新用户关注回复', type: 'textarea', default: '', help: '首次关注用户的专属回复文案,留空使用通用回复' },
+ { key: 'weixingzh_scan_msg', label: '扫码回复', type: 'textarea', default: '', help: '用户扫码关注公众号后的回复文案' },
+ { key: 'weixingzh_auto_reply_text', label: '文本自动回复', type: 'textarea', default: '', help: '收到文本消息的自动回复' },
+ { key: 'weixingzh_auto_reply_search', label: '搜索回复', type: 'textarea', default: '', help: '未匹配关键词时的搜索提示回复' },
+ { key: 'weixingzh_auto_reply_image', label: '图片回复', type: 'textarea', default: '', help: '收到图片消息的自动回复' },
+ { key: 'weixingzh_auto_reply_voice', label: '语音回复', type: 'textarea', default: '', help: '收到语音消息的自动回复' },
+ { key: 'weixingzh_auto_reply_default', label: '默认回复', type: 'textarea', default: '暂不支持此类型的消息', help: '其他未匹配消息类型的兜底回复' },
+ { key: 'weixingzh_gzh_menu', label: '自定义菜单 JSON', type: 'textarea', default: '', help: '微信公众号自定义菜单 JSON 配置,留空不创建菜单' },
+ ],
+ },
+ {
+ key: 'weibo',
+ title: '微博配置',
+ icon: 'fab fa-weibo',
+ fields: [
+ { key: 'weibo_appid', label: 'App Key', type: 'input', default: '' },
+ { key: 'weibo_appkey', label: 'App Secret', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'baidu',
+ title: '百度配置',
+ icon: 'fas fa-paw',
+ fields: [
+ { key: 'baidu_appid', label: 'API Key', type: 'input', default: '' },
+ { key: 'baidu_appkey', label: 'Secret Key', type: 'input', default: '' },
+ ],
+ },
+ {
+ key: 'alipay',
+ title: '支付宝配置',
+ icon: 'fab fa-alipay',
+ fields: [
+ { key: 'alipay_appid', label: 'AppID', type: 'input', default: '' },
+ { key: 'alipay_privatekey', label: '应用私钥', type: 'input', default: '' },
+ { key: 'alipay_publickey', label: '支付宝公钥', type: 'input', default: '' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/report.js b/assets/admin/src/schema/sections/users/report.js
new file mode 100644
index 00000000..04b3db5f
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/report.js
@@ -0,0 +1,58 @@
+export const reportSchema = {
+ key: 'users.report',
+ title: '禁封/举报',
+ icon: 'fas fa-flag',
+ description: '配置举报入口、公开限制提示和后续治理分类,为封禁/举报流程预留稳定结构。',
+ defaults: {
+ user_report_enabled: false,
+ user_report_public_entry: true,
+ user_report_public_notice: true,
+ user_report_ban_notice_text: '当前账号处于限制状态,部分互动能力已关闭。',
+ user_report_categories: 'spam|垃圾信息\nabuse|人身攻击\ncopyright|版权投诉',
+ ban_preset_reason: 'spam|垃圾信息\nabuse|人身攻击\nviolation|违规操作',
+ ban_appeal_desc: '',
+ ban_appeal_keys: '',
+ user_report_img_s: false,
+ user_report_desc: '',
+ },
+ groups: [
+ {
+ key: 'entry',
+ title: '前台入口',
+ icon: 'fas fa-door-open',
+ fields: [
+ { key: 'user_report_enabled', label: '启用举报/禁封结构', type: 'switch', default: false },
+ { key: 'user_report_public_entry', label: '公开主页展示举报入口', type: 'switch', default: true, dependsOn: { key: 'user_report_enabled', value: true } },
+ { key: 'user_report_public_notice', label: '公开显示限制提示', type: 'switch', default: true, dependsOn: { key: 'user_report_enabled', value: true } },
+ { key: 'user_report_ban_notice_text', label: '限制提示文案', type: 'input', default: '当前账号处于限制状态,部分互动能力已关闭。', dependsOn: { key: 'user_report_public_notice', value: true } },
+ ],
+ },
+ {
+ key: 'taxonomy',
+ title: '举报分类',
+ icon: 'fas fa-list',
+ fields: [
+ { key: 'user_report_categories', label: '举报分类', type: 'textarea', default: 'spam|垃圾信息\nabuse|人身攻击\ncopyright|版权投诉', help: '每行一个分类:slug|显示名称', dependsOn: { key: 'user_report_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'ban',
+ title: '禁封设置',
+ icon: 'fas fa-ban',
+ fields: [
+ { key: 'ban_preset_reason', label: '预设封禁原因', type: 'textarea', default: 'spam|垃圾信息\nabuse|人身攻击\nviolation|违规操作', help: '每行一条:slug|显示名称', dependsOn: { key: 'user_report_enabled', value: true } },
+ { key: 'ban_appeal_desc', label: '申诉说明', type: 'textarea', default: '', dependsOn: { key: 'user_report_enabled', value: true } },
+ { key: 'ban_appeal_keys', label: '申诉关键词', type: 'input', default: '', help: '逗号分隔', dependsOn: { key: 'user_report_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'image',
+ title: '图片举报',
+ icon: 'fas fa-image',
+ fields: [
+ { key: 'user_report_img_s', label: '启用图片举报', type: 'switch', default: false, dependsOn: { key: 'user_report_enabled', value: true } },
+ { key: 'user_report_desc', label: '举报说明', type: 'textarea', default: '', dependsOn: { key: 'user_report_img_s', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/signin.js b/assets/admin/src/schema/sections/users/signin.js
new file mode 100644
index 00000000..8a8c90f7
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/signin.js
@@ -0,0 +1,27 @@
+export const signinSchema = {
+ key: 'users.signin',
+ title: '签到奖励',
+ icon: 'fas fa-calendar-check',
+ description: '配置签到规则、连续签到奖励和签到页面。',
+ defaults: {
+ signin_enabled: true,
+ signin_reward: 1,
+ signin_consecutive_bonus: true,
+ signin_consecutive_days: 7,
+ signin_bonus_reward: 5,
+ },
+ groups: [
+ {
+ key: 'rules',
+ title: '签到规则',
+ icon: 'fas fa-calendar-alt',
+ fields: [
+ { key: 'signin_enabled', label: '启用签到', type: 'switch', default: true },
+ { key: 'signin_reward', label: '每日签到奖励', type: 'number', default: 1, min: 0, max: 100, dependsOn: { key: 'signin_enabled', value: true } },
+ { key: 'signin_consecutive_bonus', label: '连续签到加成', type: 'switch', default: true, dependsOn: { key: 'signin_enabled', value: true } },
+ { key: 'signin_consecutive_days', label: '加成周期 (天)', type: 'number', default: 7, min: 2, max: 30, dependsOn: { key: 'signin_consecutive_bonus', value: true } },
+ { key: 'signin_bonus_reward', label: '周期奖励', type: 'number', default: 5, min: 1, max: 100, dependsOn: { key: 'signin_consecutive_bonus', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/sms.js b/assets/admin/src/schema/sections/users/sms.js
new file mode 100644
index 00000000..eb0bbe0c
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/sms.js
@@ -0,0 +1,144 @@
+export const smsSchema = {
+ key: 'users.sms',
+ title: '短信接口',
+ icon: 'fas fa-sms',
+ description: '配置短信验证码和短信通知服务的供应商、模板和限流策略。',
+ defaults: {
+ sms_enabled: false,
+ sms_provider: 'aliyun',
+ sms_number_limit_ch: true,
+ sms_access_key: '',
+ sms_access_secret: '',
+ sms_sign_name: '',
+ sms_template_code: '',
+ sms_code_length: 6,
+ sms_code_expire: 300,
+ sms_rate_limit: 60,
+ sms_daily_limit: 10,
+ sms_notify_bind: true,
+ sms_notify_login: false,
+ sms_notify_register: true,
+ sms_notify_reset_password: true,
+ sms_ali_api_type: 'sms',
+ sms_region_id: 'cn-hangzhou',
+ sms_tencent_app_id: '',
+ sms_tencent_app_key: '',
+ sms_tencent_secret_id: '',
+ sms_tencent_secret_key: '',
+ sms_tencent_region: 'ap-guangzhou',
+ sms_huawei_sender: '',
+ sms_huawei_signature: '',
+ sms_huawei_endpoint: '',
+ sms_custom_url: '',
+ sms_custom_method: 'POST',
+ sms_custom_auth_type: 'header',
+ sms_custom_auth_key: '',
+ sms_custom_auth_value: '',
+ sms_custom_headers: '',
+ sms_custom_body: '',
+ sms_custom_success_rule: '',
+ },
+ groups: [
+ {
+ key: 'provider',
+ title: '供应商配置',
+ icon: 'fas fa-plug',
+ fields: [
+ { key: 'sms_enabled', label: '启用短信服务', type: 'switch', default: false, help: '开启后支持短信验证码和短信通知' },
+ {
+ key: 'sms_provider',
+ label: '短信供应商',
+ type: 'select',
+ default: 'aliyun',
+ options: [
+ { label: '阿里云短信', value: 'aliyun' },
+ { label: '腾讯云短信', value: 'tencent' },
+ { label: '华为云短信', value: 'huawei' },
+ { label: '自定义接口', value: 'custom' },
+ ],
+ dependsOn: { key: 'sms_enabled', value: true },
+ },
+ { key: 'sms_number_limit_ch', label: '仅发国内', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true }, help: '开启后仅支持发送国内短信,关闭后可发送港澳台及国际短信' },
+ { key: 'sms_access_key', label: 'AccessKey / SecretId / AppKey', type: 'input', default: '', dependsOn: { key: 'sms_enabled', value: true }, help: '阿里云填 AccessKey,腾讯云填 SecretId,华为云填 AppKey' },
+ { key: 'sms_access_secret', label: 'AccessSecret / SecretKey / AppSecret', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'aliyun',
+ title: '阿里云配置',
+ icon: 'fas fa-cloud',
+ fields: [
+ { key: 'sms_ali_api_type', label: '接口类型', type: 'radio', default: 'sms', options: [{ label: '短信服务', value: 'sms' }, { label: '号码认证', value: 'verify' }], dependsOn: { key: 'sms_provider', value: 'aliyun' } },
+ { key: 'sms_region_id', label: 'RegionId', type: 'input', default: 'cn-hangzhou', dependsOn: { key: 'sms_provider', value: 'aliyun' } },
+ ],
+ },
+ {
+ key: 'tencent',
+ title: '腾讯云配置',
+ icon: 'fas fa-cloud',
+ fields: [
+ { key: 'sms_tencent_app_id', label: 'SdkAppId', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' }, help: '腾讯云短信应用 ID' },
+ { key: 'sms_tencent_app_key', label: 'AppKey', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' } },
+ { key: 'sms_tencent_secret_id', label: 'SecretId', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' } },
+ { key: 'sms_tencent_secret_key', label: 'SecretKey', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' } },
+ { key: 'sms_tencent_region', label: '地域', type: 'input', default: 'ap-guangzhou', dependsOn: { key: 'sms_provider', value: 'tencent' } },
+ ],
+ },
+ {
+ key: 'huawei',
+ title: '华为云配置',
+ icon: 'fas fa-cloud',
+ fields: [
+ { key: 'sms_huawei_sender', label: '通道号 (sender)', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'huawei' } },
+ { key: 'sms_huawei_signature', label: '短信签名', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'huawei' }, help: '华为云短信签名内容' },
+ { key: 'sms_huawei_endpoint', label: 'API Endpoint', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'huawei' }, help: '留空使用默认 endpoint' },
+ ],
+ },
+ {
+ key: 'custom',
+ title: '自定义接口配置',
+ icon: 'fas fa-plug',
+ fields: [
+ { key: 'sms_custom_url', label: '请求 URL', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '支持 {phone} {code} {time} 变量' },
+ { key: 'sms_custom_method', label: '请求方法', type: 'select', default: 'POST', options: [{ label: 'POST', value: 'POST' }, { label: 'GET', value: 'GET' }, { label: 'PUT', value: 'PUT' }], dependsOn: { key: 'sms_provider', value: 'custom' } },
+ { key: 'sms_custom_auth_type', label: '鉴权方式', type: 'select', default: 'header', options: [{ label: 'Header', value: 'header' }], dependsOn: { key: 'sms_provider', value: 'custom' } },
+ { key: 'sms_custom_auth_key', label: '鉴权 Header 名', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'custom' } },
+ { key: 'sms_custom_auth_value', label: '鉴权 Header 值', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '支持 {phone} {code} {time} 变量' },
+ { key: 'sms_custom_headers', label: '自定义 Headers', type: 'textarea', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '每行一个,格式:Key: Value' },
+ { key: 'sms_custom_body', label: '请求 Body', type: 'textarea', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: 'JSON 格式,支持 {phone} {code} {time} 变量' },
+ { key: 'sms_custom_success_rule', label: '成功判定规则', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '格式:status:200 或 json:code=0 或 contains:success' },
+ ],
+ },
+ {
+ key: 'template',
+ title: '短信模板',
+ icon: 'fas fa-sms',
+ fields: [
+ { key: 'sms_sign_name', label: '短信签名', type: 'input', default: '', dependsOn: { key: 'sms_enabled', value: true }, help: '在短信服务商申请的短信签名(所有供应商共用此字段)' },
+ { key: 'sms_template_code', label: '验证码模板 ID', type: 'input', default: '', dependsOn: { key: 'sms_enabled', value: true }, help: '所有供应商共用此模板 ID' },
+ { key: 'sms_code_length', label: '验证码长度', type: 'number', default: 6, min: 4, max: 8, dependsOn: { key: 'sms_enabled', value: true } },
+ { key: 'sms_code_expire', label: '验证码有效期 (秒)', type: 'number', default: 300, min: 60, max: 600, dependsOn: { key: 'sms_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'rate_limit',
+ title: '限流策略',
+ icon: 'fas fa-shield-alt',
+ fields: [
+ { key: 'sms_rate_limit', label: '发送间隔 (秒)', type: 'number', default: 60, min: 10, max: 300, dependsOn: { key: 'sms_enabled', value: true }, help: '同一手机号两次发送的最小间隔' },
+ { key: 'sms_daily_limit', label: '每日发送上限', type: 'number', default: 10, min: 1, max: 100, dependsOn: { key: 'sms_enabled', value: true }, help: '同一手机号每天最多发送条数' },
+ ],
+ },
+ {
+ key: 'triggers',
+ title: '通知场景',
+ icon: 'fas fa-bell',
+ fields: [
+ { key: 'sms_notify_bind', label: '绑定手机时验证', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true } },
+ { key: 'sms_notify_login', label: '短信登录', type: 'switch', default: false, dependsOn: { key: 'sms_enabled', value: true } },
+ { key: 'sms_notify_register', label: '注册验证', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true } },
+ { key: 'sms_notify_reset_password', label: '找回密码', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true } },
+ ],
+ },
+ ],
+}
\ No newline at end of file
diff --git a/assets/admin/src/schema/sections/users/verify.js b/assets/admin/src/schema/sections/users/verify.js
new file mode 100644
index 00000000..f6e2a5bc
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/verify.js
@@ -0,0 +1,34 @@
+export const verifySchema = {
+ key: 'users.verify',
+ title: '身份认证',
+ icon: 'fas fa-id-card',
+ description: '配置公开认证徽标、审核策略,以及是否复用现有邮箱验证状态。',
+ defaults: {
+ user_verify_enabled: true,
+ user_verify_public_display: true,
+ user_verify_badge_text: '已认证',
+ user_verify_accept_email_verified: true,
+ user_verify_require_manual_review: false,
+ },
+ groups: [
+ {
+ key: 'display',
+ title: '公开展示',
+ icon: 'fas fa-eye',
+ fields: [
+ { key: 'user_verify_enabled', label: '启用身份认证结构', type: 'switch', default: true },
+ { key: 'user_verify_public_display', label: '公开页显示认证徽标', type: 'switch', default: true, dependsOn: { key: 'user_verify_enabled', value: true } },
+ { key: 'user_verify_badge_text', label: '认证徽标文案', type: 'input', default: '已认证', dependsOn: { key: 'user_verify_public_display', value: true } },
+ ],
+ },
+ {
+ key: 'workflow',
+ title: '审核流程',
+ icon: 'fas fa-tasks',
+ fields: [
+ { key: 'user_verify_accept_email_verified', label: '邮箱验证通过视为已认证', type: 'switch', default: true, dependsOn: { key: 'user_verify_enabled', value: true } },
+ { key: 'user_verify_require_manual_review', label: '保留人工审核位', type: 'switch', default: false, dependsOn: { key: 'user_verify_enabled', value: true } },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/schema/sections/users/wechat.js b/assets/admin/src/schema/sections/users/wechat.js
new file mode 100644
index 00000000..2ba16fa5
--- /dev/null
+++ b/assets/admin/src/schema/sections/users/wechat.js
@@ -0,0 +1,51 @@
+export const wechatSchema = {
+ key: 'users.wechat',
+ title: '微信通知',
+ icon: 'fab fa-weixin',
+ description: '配置微信模板消息、订阅/解绑和站外通知策略。',
+ defaults: {
+ wechat_notify_enabled: false,
+ wechat_app_id: '',
+ wechat_app_secret: '',
+ wechat_template_id: '',
+ wechat_token: '',
+ wechat_encoding_aes_key: '',
+ wechat_notify_comment: true,
+ wechat_notify_reply: true,
+ wechat_notify_system: true,
+ wechat_bind_entry: true,
+ },
+ groups: [
+ {
+ key: 'account',
+ title: '公众号配置',
+ icon: 'fab fa-weixin',
+ fields: [
+ { key: 'wechat_notify_enabled', label: '启用微信通知', type: 'switch', default: false, help: '开启后用户可绑定微信接收通知' },
+ { key: 'wechat_app_id', label: 'AppID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
+ { key: 'wechat_app_secret', label: 'AppSecret', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
+ { key: 'wechat_template_id', label: '模板消息 ID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在微信公众平台申请的模板消息 ID' },
+ ],
+ },
+ {
+ key: 'verify',
+ title: '服务器验证',
+ icon: 'fas fa-check-circle',
+ fields: [
+ { key: 'wechat_token', label: 'Token', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '微信公众平台服务器配置中的 Token' },
+ { key: 'wechat_encoding_aes_key', label: 'EncodingAESKey', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
+ ],
+ },
+ {
+ key: 'triggers',
+ title: '通知触发器',
+ icon: 'fas fa-bell',
+ fields: [
+ { key: 'wechat_notify_comment', label: '新评论通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
+ { key: 'wechat_notify_reply', label: '评论回复通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
+ { key: 'wechat_notify_system', label: '系统通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
+ { key: 'wechat_bind_entry', label: '显示绑定入口', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在用户中心显示微信绑定/解绑按钮' },
+ ],
+ },
+ ],
+}
diff --git a/assets/admin/src/stores/admin-ui.js b/assets/admin/src/stores/admin-ui.js
new file mode 100644
index 00000000..d36c266c
--- /dev/null
+++ b/assets/admin/src/stores/admin-ui.js
@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+/**
+ * useAdminUiStore — global UI state for the admin panel.
+ *
+ * Tracks sidebar collapse, search, and breadcrumb state.
+ */
+export const useAdminUiStore = defineStore('admin-ui', () => {
+ /** Sidebar collapsed state */
+ const sidebarCollapsed = ref(false)
+
+ /** Current page search query (for inline page search) */
+ const pageSearch = ref('')
+
+ /** Global notification count (for badge display) */
+ const notificationCount = ref(0)
+
+ /** Whether any form on the current page has unsaved changes */
+ const hasUnsavedChanges = ref(false)
+
+ /** Toggle sidebar */
+ function toggleSidebar() {
+ sidebarCollapsed.value = !sidebarCollapsed.value
+ }
+
+ /** Clear page search */
+ function clearSearch() {
+ pageSearch.value = ''
+ }
+
+ return {
+ sidebarCollapsed,
+ pageSearch,
+ notificationCount,
+ hasUnsavedChanges,
+ toggleSidebar,
+ clearSearch,
+ }
+})
diff --git a/assets/admin/src/style-target.js b/assets/admin/src/style-target.js
new file mode 100644
index 00000000..5c6d8682
--- /dev/null
+++ b/assets/admin/src/style-target.js
@@ -0,0 +1,7 @@
+/**
+ * 共享的 NaiveUI 样式注入目标
+ * 必须在 app.mount() 之前设置,确保组件渲染时目标已就绪
+ */
+import { ref } from 'vue'
+
+export const styleMountTarget = ref(null)
diff --git a/assets/admin/src/style.css b/assets/admin/src/style.css
new file mode 100644
index 00000000..6efddd01
--- /dev/null
+++ b/assets/admin/src/style.css
@@ -0,0 +1,384 @@
+#aksrt-admin-app,
+#aksrt-admin-app * {
+ box-sizing: border-box;
+}
+
+#aksrt-admin-app {
+ min-height: 100%;
+ color: #333;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
+ font-size: 14px;
+ line-height: 1.6;
+}
+
+/* ==================== FA5 Icon in NaiveUI Menu ==================== */
+#aksrt-admin-app .n-menu .n-menu-item-content i[class*="fa-"],
+#aksrt-admin-app .n-menu-item-content i[class*="fa-"] {
+ font-size: 18px;
+ width: 18px;
+ text-align: center;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: inherit;
+ opacity: 0.75;
+ transition: opacity 0.15s;
+}
+
+#aksrt-admin-app .n-menu-item-content--selected i[class*="fa-"],
+#aksrt-admin-app .n-menu-item-content:hover i[class*="fa-"] {
+ opacity: 1;
+}
+
+#aksrt-admin-app .n-layout,
+#aksrt-admin-app .n-layout-sider {
+ min-height: 100vh;
+}
+
+#aksrt-admin-app .aksrt-admin-layout {
+ min-height: 100vh;
+ background: #f5f7fb;
+}
+
+#aksrt-admin-app .aksrt-page-container,
+#aksrt-admin-app .content-page {
+ max-width: 1100px;
+ margin: 0 auto;
+}
+
+#aksrt-admin-app .aksrt-page-title {
+ margin: 0 0 12px;
+ color: #1a1a2e;
+ font-size: 22px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+#aksrt-admin-app .aksrt-page-desc {
+ margin: 0 0 24px;
+ color: #7a7f8a;
+ font-size: 13px;
+ line-height: 1.7;
+}
+
+#aksrt-admin-app .aksrt-section-card {
+ margin-bottom: 18px;
+ border-radius: 12px !important;
+ border: 1px solid #eceff5 !important;
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
+}
+
+#aksrt-admin-app .aksrt-section-card .n-card__content {
+ padding: 18px 20px 20px !important;
+}
+
+#aksrt-admin-app .aksrt-section-card .n-card-header {
+ padding: 18px 20px 0 !important;
+}
+
+#aksrt-admin-app .aksrt-section-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 14px;
+}
+
+#aksrt-admin-app .aksrt-section-header h3 {
+ margin: 0;
+ color: #1a1a2e;
+ font-size: 15px;
+ font-weight: 600;
+}
+
+#aksrt-admin-app .section-icon,
+#aksrt-admin-app .header-icon {
+ font-size: 17px;
+ opacity: 0.8;
+ display: inline-flex;
+ align-items: center;
+}
+
+#aksrt-admin-app .section-desc,
+#aksrt-admin-app .aksrt-hint-text {
+ color: #8a909c;
+ font-size: 12px;
+ line-height: 1.6;
+}
+
+#aksrt-admin-app .section-desc {
+ margin: 0 0 14px;
+}
+
+#aksrt-admin-app .aksrt-form .n-form-item {
+ margin-bottom: 18px;
+}
+
+#aksrt-admin-app .aksrt-form .n-form-item:last-child {
+ margin-bottom: 0;
+}
+
+#aksrt-admin-app .aksrt-form .n-form-item-label {
+ color: #555 !important;
+ font-size: 13px;
+ font-weight: 500;
+}
+
+#aksrt-admin-app .aksrt-form .n-input,
+#aksrt-admin-app .aksrt-form .n-select .n-base-selection,
+#aksrt-admin-app .aksrt-form .n-input-number {
+ min-height: 38px;
+ --n-height: 38px !important;
+}
+
+#aksrt-admin-app .aksrt-form .n-input--textarea textarea {
+ min-height: 84px;
+}
+
+#aksrt-admin-app .aksrt-form .n-switch {
+ --n-rail-height: 26px !important;
+ --n-rail-width: 48px !important;
+}
+
+#aksrt-admin-app .aksrt-form-actions {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 12px;
+ padding: 24px 0 8px;
+ margin-top: 28px;
+ border-top: 1px solid #eef1f6;
+}
+
+#aksrt-admin-app .aksrt-form-actions .n-button {
+ min-width: 120px;
+ height: 40px;
+ font-weight: 500;
+}
+
+#aksrt-admin-app .aksrt-tabs .n-tabs-nav {
+ margin-bottom: 20px !important;
+}
+
+#aksrt-admin-app .aksrt-tabs .n-tabs-tab {
+ padding: 10px 20px !important;
+ font-size: 14px !important;
+ font-weight: 500;
+}
+
+#aksrt-admin-app .n-data-table {
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+#aksrt-admin-app .n-data-table .n-data-table-th {
+ background: #f8fafc;
+}
+
+#aksrt-admin-app .n-empty {
+ padding: 24px 0;
+}
+
+/* ==================== 卡片 header 行(标题 + 按钮) ==================== */
+#aksrt-admin-app .aksrt-card-header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ width: 100%;
+ flex-wrap: wrap;
+}
+
+#aksrt-admin-app .aksrt-card-header-row__left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+#aksrt-admin-app .aksrt-card-header-row__title {
+ margin: 0;
+ font-size: 15px;
+ font-weight: 600;
+ color: #1a1a2e;
+}
+
+/* ==================== 过渡 ==================== */
+.fade-slide-enter-active,
+.fade-slide-leave-active {
+ transition: opacity 0.24s ease, transform 0.24s ease;
+}
+
+.fade-slide-enter-from {
+ opacity: 0;
+ transform: translateY(12px);
+}
+
+.fade-slide-leave-to {
+ opacity: 0;
+ transform: translateY(-8px);
+}
+
+/* ==================== Modal 样式(弹层) ==================== */
+.aksrt-modal-card {
+ width: 100%;
+ max-width: 520px;
+}
+
+/* ==================== 移动端适配 ==================== */
+@media (max-width: 768px) {
+ /* 容器 */
+ #aksrt-admin-app .aksrt-page-container,
+ #aksrt-admin-app .content-page {
+ max-width: 100%;
+ padding: 0;
+ }
+
+ #aksrt-admin-app .aksrt-page-title {
+ font-size: 18px;
+ margin-bottom: 8px;
+ }
+
+ #aksrt-admin-app .aksrt-page-desc {
+ font-size: 12px;
+ margin-bottom: 14px;
+ }
+
+ /* 卡片紧凑 */
+ #aksrt-admin-app .aksrt-section-card {
+ margin-bottom: 12px;
+ border-radius: 10px !important;
+ }
+
+ #aksrt-admin-app .aksrt-section-card .n-card__content {
+ padding: 14px !important;
+ }
+
+ #aksrt-admin-app .aksrt-section-card .n-card-header {
+ padding: 14px 14px 0 !important;
+ }
+
+ #aksrt-admin-app .aksrt-section-header {
+ margin-bottom: 10px;
+ }
+
+ /* 表单:label 改为顶部排列(覆盖 left 模式) */
+ #aksrt-admin-app .aksrt-form .n-form-item {
+ margin-bottom: 14px;
+ }
+
+ #aksrt-admin-app .aksrt-form .n-form-item .n-form-item-label {
+ text-align: left !important;
+ padding: 0 0 6px !important;
+ width: auto !important;
+ height: auto !important;
+ line-height: 1.5 !important;
+ justify-content: flex-start !important;
+ }
+
+ #aksrt-admin-app .aksrt-form .n-form-item-blank {
+ width: 100%;
+ }
+
+ /* 强制行内表单单列 */
+ #aksrt-admin-app .aksrt-form .n-form-item .n-form-item-label,
+ #aksrt-admin-app .aksrt-form .n-form-item-row {
+ display: block !important;
+ }
+
+ /* radio button 自动换行 */
+ #aksrt-admin-app .aksrt-form .n-radio-group {
+ width: 100%;
+ }
+
+ #aksrt-admin-app .aksrt-form .n-radio-group .n-space {
+ flex-wrap: wrap !important;
+ gap: 6px 8px !important;
+ }
+
+ #aksrt-admin-app .aksrt-form .n-radio-button {
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+
+ /* 输入组在窄屏堆叠 */
+ #aksrt-admin-app .aksrt-form .n-input-group {
+ flex-wrap: wrap;
+ gap: 6px;
+ }
+
+ #aksrt-admin-app .aksrt-form .n-input-group > .n-input,
+ #aksrt-admin-app .aksrt-form .n-input-group > .n-button {
+ border-radius: 6px !important;
+ }
+
+ /* 表单底部按钮区 */
+ #aksrt-admin-app .aksrt-form-actions {
+ flex-direction: column-reverse;
+ align-items: stretch;
+ padding: 16px 0 8px;
+ margin-top: 20px;
+ gap: 8px;
+ }
+
+ #aksrt-admin-app .aksrt-form-actions .n-button {
+ width: 100%;
+ min-width: 0;
+ height: 42px;
+ }
+
+ /* 卡片 header 行:换行排列 */
+ #aksrt-admin-app .aksrt-card-header-row {
+ gap: 8px;
+ }
+
+ /* Naive 弹窗全屏 */
+ .n-modal-mask + .n-modal-body-wrapper .n-modal,
+ .n-modal-body-wrapper > .n-modal {
+ width: 100vw !important;
+ max-width: 100vw !important;
+ border-radius: 0 !important;
+ margin: 0 !important;
+ min-height: 100vh;
+ }
+
+ .aksrt-modal-card {
+ max-width: 100vw !important;
+ width: 100vw !important;
+ min-height: 100vh;
+ border-radius: 0 !important;
+ }
+
+ /* 数据表格隐藏次要列 */
+ .aksrt-table-mobile-hide,
+ #aksrt-admin-app .n-data-table .aksrt-table-mobile-hide {
+ display: none !important;
+ }
+
+ /* 表格内边距收紧 */
+ #aksrt-admin-app .n-data-table .n-data-table-td,
+ #aksrt-admin-app .n-data-table .n-data-table-th {
+ padding: 8px 10px !important;
+ }
+
+ /* tabs */
+ #aksrt-admin-app .aksrt-tabs .n-tabs-tab {
+ padding: 8px 12px !important;
+ font-size: 13px !important;
+ }
+
+ /* drawer 内菜单 */
+ .n-drawer .n-menu .n-menu-item-content {
+ padding-left: 18px !important;
+ }
+}
+
+/* ==================== 超窄设备(<= 480px)追加 ==================== */
+@media (max-width: 480px) {
+ #aksrt-admin-app .aksrt-section-card .n-card__content {
+ padding: 12px !important;
+ }
+
+ #aksrt-admin-app .aksrt-section-card .n-card-header {
+ padding: 12px 12px 0 !important;
+ }
+}
diff --git a/assets/admin/src/utils/home-module-schema.js b/assets/admin/src/utils/home-module-schema.js
new file mode 100644
index 00000000..0a2b771f
--- /dev/null
+++ b/assets/admin/src/utils/home-module-schema.js
@@ -0,0 +1,535 @@
+/**
+ * AKSRT CMS Home Module Schema
+ *
+ * 定义每类模块的:
+ * - 元数据(label / icon / desc)
+ * - 默认 config
+ * - 表单字段(用于配置抽屉自动生成 UI)
+ *
+ * 字段类型 type:
+ * - text n-input
+ * - number n-input-number
+ * - select n-select (options)
+ * - radio n-radio-group (options)
+ * - switch n-switch
+ * - cats n-select (multiple, 动态加载分类)
+ * - cat n-select (single, 动态加载分类)
+ * - textarea n-input type=textarea
+ */
+
+export const MODULE_TYPES = [
+ 'hero',
+ 'banner',
+ 'category',
+ 'tabs',
+ 'ranking',
+ 'recent',
+ 'tag_cloud',
+ 'friend_links',
+ 'html',
+ 'hotspot',
+ 'category_recommend_cards',
+ 'text_title',
+ 'button_group',
+ 'spacing',
+ 'faq',
+ 'data_grid',
+ 'highlight_cards',
+ 'partner_marquee',
+ 'product_showcase',
+ 'user',
+ 'more',
+]
+
+export const MODULE_LIBRARY = [
+ {
+ type: 'hero',
+ label: 'Hero 首屏',
+ icon: 'image',
+ desc: '大图轮播首屏,支持叠加搜索框和图标卡片',
+ defaultConfig: { with_search: true, show_cards: false, card_columns: 4, icon_cards: [] },
+ fields: [
+ { key: 'with_search', type: 'switch', label: '显示搜索覆盖框' },
+ { key: 'show_cards', type: 'switch', label: '显示图标卡片覆盖' },
+ { key: 'card_columns', type: 'number', label: '卡片列数', min: 2, max: 6 },
+ ],
+ },
+ {
+ type: 'banner',
+ label: 'Banner 轮播',
+ icon: 'rectangle',
+ desc: '正文区轮播条',
+ defaultConfig: {},
+ fields: [],
+ },
+ {
+ type: 'category',
+ label: '分类块',
+ icon: 'folder',
+ desc: '指定分类的最新内容网格',
+ defaultConfig: { cat_id: 0, count: 8, layout: 'card', columns: 4, title: '' },
+ fields: [
+ { key: 'title', type: 'text', label: '标题(留空用分类名)' },
+ { key: 'cat_id', type: 'cat', label: '分类' },
+ { key: 'count', type: 'number', label: '数量', min: 1, max: 20 },
+ { key: 'columns', type: 'number', label: '列数', min: 1, max: 6 },
+ {
+ key: 'layout', type: 'select', label: '布局',
+ options: [
+ { label: '卡片', value: 'card' },
+ { label: '列表', value: 'list' },
+ { label: '瀑布流', value: 'masonry' },
+ { label: '资源网格', value: 'grid-cms' },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'tabs',
+ label: '分类 Tabs',
+ icon: 'switcher',
+ desc: '多分类 Tab 切换',
+ defaultConfig: { cat_ids: [], count: 8, layout: 'grid-cms', title: '' },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'cat_ids', type: 'cats', label: '分类列表' },
+ { key: 'count', type: 'number', label: '每个 Tab 数量', min: 1, max: 20 },
+ {
+ key: 'layout', type: 'select', label: '布局',
+ options: [
+ { label: '卡片', value: 'card' },
+ { label: '列表', value: 'list' },
+ { label: '瀑布流', value: 'masonry' },
+ { label: '资源网格', value: 'grid-cms' },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'ranking',
+ label: '排行榜',
+ icon: 'trophy',
+ desc: '按下载量 / 阅读量 / 评论数排序',
+ defaultConfig: { metric: 'downloads', count: 6, post_type: 'aksrt_resource', title: '' },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ {
+ key: 'metric', type: 'radio', label: '排序依据',
+ options: [
+ { label: '下载量', value: 'downloads' },
+ { label: '阅读量', value: 'views' },
+ { label: '评论数', value: 'comments' },
+ ],
+ },
+ {
+ key: 'post_type', type: 'select', label: '内容类型',
+ options: [
+ { label: '资源', value: 'aksrt_resource' },
+ { label: '文章', value: 'post' },
+ ],
+ },
+ { key: 'count', type: 'number', label: '数量', min: 1, max: 20 },
+ ],
+ },
+ {
+ type: 'recent',
+ label: '最新内容',
+ icon: 'clock',
+ desc: '按时间倒序的最新文章 / 资源',
+ defaultConfig: { post_type: 'post', count: 6, layout: 'card', title: '' },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ {
+ key: 'post_type', type: 'select', label: '内容类型',
+ options: [
+ { label: '文章', value: 'post' },
+ { label: '资源', value: 'aksrt_resource' },
+ ],
+ },
+ { key: 'count', type: 'number', label: '数量', min: 1, max: 20 },
+ {
+ key: 'layout', type: 'select', label: '布局',
+ options: [
+ { label: '卡片', value: 'card' },
+ { label: '列表', value: 'list' },
+ { label: '瀑布流', value: 'masonry' },
+ { label: '资源网格', value: 'grid-cms' },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'tag_cloud',
+ label: '标签云',
+ icon: 'tag',
+ desc: '按使用频率展示热门标签',
+ defaultConfig: { limit: 30, title: '' },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'limit', type: 'number', label: '数量', min: 5, max: 100 },
+ ],
+ },
+ {
+ type: 'friend_links',
+ label: '友情链接',
+ icon: 'link',
+ desc: '展示已配置的友链',
+ defaultConfig: { category: '', title: '' },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'category', type: 'text', label: '分类筛选(可空)' },
+ ],
+ },
+ {
+ type: 'html',
+ label: '自定义 HTML',
+ icon: 'code',
+ desc: '自由插入 HTML 片段(受 wp_kses_post 清洗)',
+ defaultConfig: { html: '', title: '' },
+ fields: [
+ { key: 'title', type: 'text', label: '标题(可空)' },
+ { key: 'html', type: 'textarea', label: 'HTML 内容', rows: 6 },
+ ],
+ },
+ {
+ type: 'hotspot',
+ label: '热点推荐',
+ icon: 'fire',
+ desc: '文章推荐列表(纯文本,仅桌面端显示)',
+ defaultConfig: { count: 6, post_type: 'post', source: 'auto', title: '' },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'count', type: 'number', label: '数量', min: 3, max: 20 },
+ {
+ key: 'source', type: 'select', label: '数据来源',
+ options: [
+ { label: '自动推荐(推荐文章优先)', value: 'auto' },
+ { label: '最新文章', value: 'recent' },
+ { label: '最热文章(按阅读量)', value: 'views' },
+ ],
+ },
+ {
+ key: 'post_type', type: 'select', label: '内容类型',
+ options: [
+ { label: '文章', value: 'post' },
+ { label: '资源', value: 'aksrt_resource' },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'category_recommend_cards',
+ label: '分类推荐卡片',
+ icon: 'grid',
+ desc: '4 张分类卡片 2×2 布局,含封面图、浏览量胶囊和文章列表',
+ defaultConfig: {
+ title: '分类推荐',
+ cat_ids: [],
+ show_views: true,
+ show_dates: true,
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'cat_ids', type: 'cats', label: '分类(按选择顺序对应卡片 1-4)' },
+ { key: 'show_views', type: 'switch', label: '显示浏览量胶囊' },
+ { key: 'show_dates', type: 'switch', label: '显示发布时间' },
+ ],
+ },
+ {
+ type: 'text_title',
+ label: '标题文案',
+ icon: 'font-colors',
+ desc: '带标题、副标题和可选按钮的文案区块',
+ defaultConfig: {
+ title: '',
+ desc: '',
+ align: 'left',
+ style: 'default',
+ button_text: '',
+ button_url: '',
+ button_style: 'primary',
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'desc', type: 'textarea', label: '副标题/描述', rows: 2 },
+ { key: 'align', type: 'radio', label: '对齐', options: [
+ { label: '左对齐', value: 'left' },
+ { label: '居中', value: 'center' },
+ { label: '右对齐', value: 'right' },
+ ]},
+ { key: 'style', type: 'select', label: '样式', options: [
+ { label: '默认', value: 'default' },
+ { label: '带左边框', value: 'bordered' },
+ { label: '带背景', value: 'bg' },
+ ]},
+ { key: 'button_text', type: 'text', label: '按钮文字(可空)' },
+ { key: 'button_url', type: 'text', label: '按钮链接' },
+ { key: 'button_style', type: 'select', label: '按钮样式', options: [
+ { label: '主色', value: 'primary' },
+ { label: '轮廓', value: 'outline' },
+ { label: '文字', value: 'text' },
+ ]},
+ ],
+ },
+ {
+ type: 'button_group',
+ label: '按钮组',
+ icon: 'th-large',
+ desc: '多按钮横向排列,适合快捷入口',
+ defaultConfig: {
+ title: '',
+ buttons: [],
+ columns: 4,
+ style: 'card',
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题(可空)' },
+ { key: 'columns', type: 'number', label: '每行列数', min: 2, max: 6 },
+ { key: 'style', type: 'select', label: '按钮样式', options: [
+ { label: '卡片', value: 'card' },
+ { label: '图标', value: 'icon' },
+ { label: '文字', value: 'text' },
+ ]},
+ ],
+ },
+ {
+ type: 'spacing',
+ label: '间距分隔',
+ icon: 'swap',
+ desc: '模块间间距或分隔线',
+ defaultConfig: {
+ height: 40,
+ divider: false,
+ divider_style: 'solid',
+ },
+ fields: [
+ { key: 'height', type: 'number', label: '高度 (px)', min: 0, max: 200 },
+ { key: 'divider', type: 'switch', label: '显示分隔线' },
+ { key: 'divider_style', type: 'select', label: '分隔线样式', options: [
+ { label: '实线', value: 'solid' },
+ { label: '虚线', value: 'dashed' },
+ { label: '点线', value: 'dotted' },
+ ]},
+ ],
+ },
+ {
+ type: 'faq',
+ label: '常见问题',
+ icon: 'question-circle',
+ desc: '折叠式 FAQ 列表',
+ defaultConfig: {
+ title: '常见问题',
+ items: [],
+ style: 'accordion',
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'style', type: 'radio', label: '样式', options: [
+ { label: '手风琴', value: 'accordion' },
+ { label: '卡片', value: 'card' },
+ ]},
+ ],
+ },
+ {
+ type: 'data_grid',
+ label: '数据网格',
+ icon: 'table',
+ desc: '多列数据卡片展示(统计、特征等)',
+ defaultConfig: {
+ title: '',
+ columns: 4,
+ items: [],
+ style: 'number',
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题(可空)' },
+ { key: 'columns', type: 'number', label: '列数', min: 2, max: 6 },
+ { key: 'style', type: 'select', label: '样式', options: [
+ { label: '数字突出', value: 'number' },
+ { label: '图标突出', value: 'icon' },
+ { label: '简洁', value: 'minimal' },
+ ]},
+ ],
+ },
+ {
+ type: 'highlight_cards',
+ label: '高亮卡片',
+ icon: 'star',
+ desc: '带配色的特色卡片网格',
+ defaultConfig: {
+ title: '',
+ columns: 3,
+ cards: [],
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题(可空)' },
+ { key: 'columns', type: 'number', label: '列数', min: 2, max: 4 },
+ ],
+ },
+ {
+ type: 'partner_marquee',
+ label: '合作伙伴',
+ icon: 'handshake',
+ desc: 'Logo 滚动展示(合作伙伴/品牌墙)',
+ defaultConfig: {
+ title: '合作伙伴',
+ logos: [],
+ speed: 30,
+ pause_on_hover: true,
+ columns: 5,
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'speed', type: 'number', label: '滚动速度', min: 10, max: 100 },
+ { key: 'pause_on_hover', type: 'switch', label: '鼠标悬停暂停' },
+ { key: 'columns', type: 'number', label: '可见列数', min: 3, max: 8 },
+ ],
+ },
+ {
+ type: 'product_showcase',
+ label: '产品展示',
+ icon: 'shopping-bag',
+ desc: '产品/商品卡片网格(为商城 Phase 预留)',
+ defaultConfig: {
+ title: '产品展示',
+ columns: 3,
+ count: 6,
+ source: 'manual',
+ show_price: true,
+ show_button: true,
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'columns', type: 'number', label: '列数', min: 2, max: 4 },
+ { key: 'count', type: 'number', label: '数量', min: 1, max: 12 },
+ { key: 'source', type: 'select', label: '数据来源', options: [
+ { label: '手动配置', value: 'manual' },
+ { label: '最新商品', value: 'latest' },
+ { label: '热门商品', value: 'popular' },
+ ]},
+ { key: 'show_price', type: 'switch', label: '显示价格' },
+ { key: 'show_button', type: 'switch', label: '显示购买按钮' },
+ ],
+ },
+ {
+ type: 'user',
+ label: '用户卡片',
+ icon: 'user',
+ desc: '推荐用户卡片(头像、简介、关注按钮)',
+ defaultConfig: {
+ title: '推荐用户',
+ user_ids: [],
+ count: 4,
+ columns: 4,
+ order: 'recent',
+ show_stats: true,
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'count', type: 'number', label: '用户数量', min: 1, max: 12 },
+ { key: 'columns', type: 'number', label: '列数', min: 2, max: 6 },
+ { key: 'order', type: 'select', label: '排序', options: [
+ { label: '最近注册', value: 'recent' },
+ { label: '文章最多', value: 'posts' },
+ { label: '手动指定', value: 'manual' },
+ ]},
+ { key: 'show_stats', type: 'switch', label: '显示统计数据' },
+ ],
+ },
+ {
+ type: 'more',
+ label: '发现更多',
+ icon: 'th',
+ desc: '分类卡片网格(封面、文章数、最近文章)',
+ defaultConfig: {
+ title: '发现更多',
+ cat_ids: [],
+ count: 6,
+ columns: 3,
+ show_post_count: true,
+ show_latest_posts: true,
+ latest_count: 5,
+ },
+ fields: [
+ { key: 'title', type: 'text', label: '标题' },
+ { key: 'count', type: 'number', label: '分类数量', min: 1, max: 12 },
+ { key: 'columns', type: 'number', label: '列数', min: 2, max: 4 },
+ { key: 'show_post_count', type: 'switch', label: '显示文章数' },
+ { key: 'show_latest_posts', type: 'switch', label: '显示最近文章' },
+ { key: 'latest_count', type: 'number', label: '最近文章数', min: 1, max: 10 },
+ ],
+ },
+]
+
+export function getModuleMeta(type) {
+ return MODULE_LIBRARY.find(m => m.type === type) || null
+}
+
+export function makeDefaultModule(type) {
+ const meta = getModuleMeta(type)
+ if (!meta) return null
+ return {
+ type,
+ config: JSON.parse(JSON.stringify(meta.defaultConfig || {})),
+ }
+}
+
+export const PRESET_TEMPLATES = [
+ {
+ key: 'resource_standard',
+ label: '资源站标准',
+ desc: 'Hero + Tabs + 最新资源 + 下载排行',
+ modules: [
+ { type: 'hero', config: {} },
+ { type: 'tabs', config: { count: 8, layout: 'grid-cms' } },
+ { type: 'recent', config: { post_type: 'aksrt_resource', count: 8, layout: 'grid-cms' } },
+ { type: 'ranking', config: { metric: 'downloads', post_type: 'aksrt_resource', count: 6 } },
+ ],
+ },
+ {
+ key: 'news',
+ label: '资讯站',
+ desc: 'Banner + 多个分类块 + 标签云',
+ modules: [
+ { type: 'banner', config: {} },
+ { type: 'category', config: { count: 8, columns: 4, layout: 'card' } },
+ { type: 'category', config: { count: 8, columns: 4, layout: 'card' } },
+ { type: 'tag_cloud', config: { limit: 30 } },
+ ],
+ },
+ {
+ key: 'mixed',
+ label: '混合站',
+ desc: 'Hero + 最新文章 + 多个分类块',
+ modules: [
+ { type: 'hero', config: {} },
+ { type: 'recent', config: { post_type: 'post', count: 6, layout: 'card' } },
+ { type: 'category', config: { count: 6, columns: 3, layout: 'card' } },
+ { type: 'category', config: { count: 6, columns: 3, layout: 'card' } },
+ ],
+ },
+ {
+ key: 'landing',
+ label: '落地页',
+ desc: 'Hero + 标题 + 高亮卡片 + FAQ + 按钮组',
+ modules: [
+ { type: 'hero', config: {} },
+ { type: 'text_title', config: { title: '为什么选择我们', align: 'center', style: 'default' } },
+ { type: 'highlight_cards', config: { columns: 3 } },
+ { type: 'spacing', config: { height: 40 } },
+ { type: 'faq', config: { title: '常见问题' } },
+ { type: 'button_group', config: { columns: 3, style: 'card' } },
+ ],
+ },
+ {
+ key: 'corporate',
+ label: '企业站',
+ desc: 'Hero + 数据网格 + 产品展示 + 合作伙伴',
+ modules: [
+ { type: 'hero', config: {} },
+ { type: 'data_grid', config: { title: '核心数据', columns: 4, style: 'number' } },
+ { type: 'product_showcase', config: { title: '产品展示', columns: 3 } },
+ { type: 'partner_marquee', config: { title: '合作伙伴', speed: 30 } },
+ ],
+ },
+]
diff --git a/assets/admin/src/utils/media.js b/assets/admin/src/utils/media.js
new file mode 100644
index 00000000..1e779986
--- /dev/null
+++ b/assets/admin/src/utils/media.js
@@ -0,0 +1,40 @@
+export function openMediaPicker(options = {}) {
+ const wpMedia = window.wp?.media
+
+ if (!wpMedia) {
+ throw new Error('当前后台未加载 WordPress 媒体库。')
+ }
+
+ const frame = wpMedia({
+ title: options.title || '选择图片',
+ button: {
+ text: options.buttonText || '使用此图片',
+ },
+ library: {
+ type: options.type || 'image',
+ },
+ multiple: false,
+ })
+
+ return new Promise((resolve, reject) => {
+ frame.on('select', () => {
+ const attachment = frame.state().get('selection').first()?.toJSON()
+
+ if (!attachment?.url) {
+ reject(new Error('未选择有效的媒体资源。'))
+ return
+ }
+
+ resolve(attachment)
+ })
+
+ frame.on('close', () => {
+ const hasSelection = frame.state().get('selection').length > 0
+ if (!hasSelection) {
+ resolve(null)
+ }
+ })
+
+ frame.open()
+ })
+}
diff --git a/assets/admin/tsconfig.json b/assets/admin/tsconfig.json
new file mode 100644
index 00000000..3d23e6b2
--- /dev/null
+++ b/assets/admin/tsconfig.json
@@ -0,0 +1 @@
+{"compilerOptions": {"target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "jsx": "preserve", "strict": true, "noEmit": true, "baseUrl": ".", "paths": {"@/*": ["src/*"]}}, "include": ["src/**/*.ts", "src/**/*.vue"]}
\ No newline at end of file
diff --git a/assets/admin/vite.config.ts b/assets/admin/vite.config.ts
new file mode 100644
index 00000000..95871cfe
--- /dev/null
+++ b/assets/admin/vite.config.ts
@@ -0,0 +1,75 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { resolve } from 'path'
+
+export default defineConfig({
+ plugins: [vue()],
+ root: __dirname,
+ base: '',
+ build: {
+ outDir: 'build',
+ emptyOutDir: true,
+ minify: 'esbuild',
+ // 注意:不 drop console/debugger,保留错误信息便于排查
+ esbuild: {
+ // drop: ['console', 'debugger'], ← 生产环境可开启
+ },
+ rollupOptions: {
+ output: {
+ // 入口文件带 hash,避免与动态导入的 chunk 产生命名/导出冲突
+ entryFileNames: (chunkInfo) => {
+ // 入口 chunk 统一命名为 index.js,确保 PHP 端稳定引用
+ return 'index.js'
+ },
+ chunkFileNames: (chunkInfo) => {
+ // vendor chunk 用稳定前缀+hash;其他 chunk(含被提取的 index 共享模块)也用固定名
+ if (chunkInfo.name.includes('node_modules')) return 'vendor-[hash].js'
+ // 防止 Rollup 提取的共享 chunk 名为 "index" 时与入口冲突
+ if (chunkInfo.name === 'index') return 'shared-api.[hash].js'
+ return '[name].[hash].js'
+ },
+ assetFileNames: (assetInfo) => {
+ // CSS 和其他资源保持固定名
+ if (assetInfo.name && assetInfo.name.endsWith('.css')) return '[name][extname]'
+ return '[name].[hash][extname]'
+ },
+ format: 'es',
+ // 手动拆分:将框架依赖独立为 vendor chunk
+ // Naive UI 单包 1.3MB+,按内部子目录粗拆,提升首屏并行下载速度。
+ manualChunks(id) {
+ if (!id.includes('node_modules')) return undefined
+ if (id.includes('node_modules/vue/')
+ || id.includes('node_modules/@vue/')
+ || id.includes('node_modules/vue-router/')
+ || id.includes('node_modules/pinia/')) {
+ return 'vendor-vue'
+ }
+ if (id.includes('node_modules/@vicons/')) {
+ return 'vendor-icons'
+ }
+ if (id.includes('node_modules/vuedraggable/') || id.includes('node_modules/sortablejs/')) {
+ return 'vendor-draggable'
+ }
+ // Naive UI:内部子目录之间存在大量交叉依赖(form ⇄ core 等),
+ // 按子目录拆分会产生 ESM 循环依赖,导致 "Cannot access 'X' before initialization"。
+ // 因此整体打成单个 chunk,避免 TDZ 运行时错误。
+ if (id.includes('node_modules/naive-ui/')) {
+ return 'vendor-naive'
+ }
+ // 其它依赖归为通用 vendor
+ return 'vendor-misc'
+ },
+ },
+ },
+ cssCodeSplit: true,
+ // Naive UI 整体打成单个 vendor-naive chunk(避免子目录间循环依赖导致的 TDZ 错误)。
+ // 组件库本身约 1.16MB(gzip 后 ~304KB),属于该 UI 库的合理体积,
+ // 不再尝试进一步拆分,故将告警阈值上调以避免误导。
+ chunkSizeWarningLimit: 1500,
+ },
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, 'src'),
+ },
+ },
+})
diff --git a/author.php b/author.php
new file mode 100644
index 00000000..fda9ff30
--- /dev/null
+++ b/author.php
@@ -0,0 +1,118 @@
+ID;
+$dashboard = function_exists('aksrt_get_user_dashboard_view')
+ ? aksrt_get_user_dashboard_view($user_id, ['viewer_id' => get_current_user_id()])
+ : [];
+
+$user_home_settings = class_exists('AKSRT_Settings')
+ ? AKSRT_Settings::get_user_home_settings()
+ : [
+ 'tabs' => ['posts', 'comments', 'favorites'],
+ 'sidebar' => true,
+ 'cover' => true,
+ 'follow' => true,
+ 'posts_per_page' => 10,
+ ];
+
+require_once AKSRT_DIR . '/templates/parts/user-shared-helpers.php';
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ max_num_pages;
+ $current_page = max(1, (int) get_query_var('paged'));
+ if ($total_pages > 1) :
+ ?>
+
+
+
+
+
+
+
+
display_name)); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/comments.php b/comments.php
new file mode 100644
index 00000000..9a08a2a4
--- /dev/null
+++ b/comments.php
@@ -0,0 +1,206 @@
+ 'desc',
+ 'nested_depth' => 3,
+ 'require_login' => false,
+ 'allow_reply' => true,
+ 'avatar_size' => 48,
+ 'empty_text' => __('暂无评论,来做第一个评论的人吧~', 'aksrt'),
+ ];
+$require_login = !empty($comment_settings['require_login']);
+$default_comment_order = ($comment_settings['order'] ?? 'desc') === 'asc' ? 'oldest' : 'newest';
+$comment_order = isset($_GET['aksrt_comment_order']) ? sanitize_key((string) wp_unslash($_GET['aksrt_comment_order'])) : $default_comment_order;
+$comment_order = in_array($comment_order, ['newest', 'oldest', 'hottest'], true) ? $comment_order : 'newest';
+$reverse_comments = ($comment_order === 'newest');
+$is_hottest = ($comment_order === 'hottest');
+
+if ($is_hottest) {
+ add_filter('comments_clauses', function ($clauses) {
+ global $wpdb;
+ $clauses['join'] .= " LEFT JOIN {$wpdb->commentmeta} AS aksrt_cm ON ({$wpdb->comments}.comment_ID = aksrt_cm.comment_id AND aksrt_cm.meta_key = 'aksrt_comment_likes')";
+ $clauses['orderby'] = 'CAST(aksrt_cm.meta_value AS UNSIGNED) DESC, ' . $clauses['orderby'];
+ return $clauses;
+ });
+}
+$author_only = isset($_GET['aksrt_author_only']) && (int) $_GET['aksrt_author_only'] === 1;
+$post_author_id = (int) get_the_author_meta('ID');
+$avatar_size = max(24, (int) ($comment_settings['avatar_size'] ?? 48));
+$max_depth = max(1, (int) ($comment_settings['nested_depth'] ?? 3));
+$allow_reply = !empty($comment_settings['allow_reply']);
+$empty_text = (string) ($comment_settings['empty_text'] ?? __('暂无评论,来做第一个评论的人吧~', 'aksrt'));
+$comment_toolbar = is_array($comment_settings['toolbar'] ?? null) ? $comment_settings['toolbar'] : ['smilie' => true, 'code' => true, 'image' => true];
+$comment_upload_enabled = !empty($comment_settings['toolbar']['upload_img']);
+$comment_box_placeholder = (string) ($comment_settings['box_placeholder'] ?? __('说点什么吧...', 'aksrt'));
+$comment_submit_text = (string) ($comment_settings['submit_text'] ?? __('提交评论', 'aksrt'));
+$comment_title_text = (string) ($comment_settings['title'] ?? __('评论', 'aksrt'));
+set_query_var('aksrt_comment_order', $comment_order);
+set_query_var('aksrt_comment_settings', $comment_settings);
+?>
+
+get_auth_providers_data();
+}
+?>
+
+
diff --git a/composer.json b/composer.json
new file mode 100644
index 00000000..c77803dd
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,18 @@
+{
+ "name": "aksrt/theme",
+ "description": "AKSRT WordPress Theme - OAuth dependencies",
+ "type": "wordpress-theme",
+ "require": {
+ "php": ">=7.4",
+ "stripe/stripe-php": "^16.1",
+ "yurunsoft/pay-sdk": "~2.0",
+ "yurunsoft/yurun-oauth-login": "~2.0"
+ },
+ "config": {
+ "allow-plugins": false,
+ "sort-packages": true,
+ "audit": {
+ "block-insecure": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 00000000..f8f7b3c0
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,272 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "0ee87e87d94466ca134aa2d9e8bac1c3",
+ "packages": [
+ {
+ "name": "psr/http-message",
+ "version": "1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+ "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/1.1"
+ },
+ "time": "2023-04-04T09:50:52+00:00"
+ },
+ {
+ "name": "stripe/stripe-php",
+ "version": "v16.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/stripe/stripe-php.git",
+ "reference": "47c55a5a8414d1d7584e6bbfb3e8179f3c43d83a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/stripe/stripe-php/zipball/47c55a5a8414d1d7584e6bbfb3e8179f3c43d83a",
+ "reference": "47c55a5a8414d1d7584e6bbfb3e8179f3c43d83a",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "ext-curl": "*",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "php": ">=5.6.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "3.5.0",
+ "phpstan/phpstan": "^1.2",
+ "phpunit/phpunit": "^5.7 || ^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Stripe\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Stripe and contributors",
+ "homepage": "https://github.com/stripe/stripe-php/contributors"
+ }
+ ],
+ "description": "Stripe PHP Library",
+ "homepage": "https://stripe.com/",
+ "keywords": [
+ "api",
+ "payment processing",
+ "stripe"
+ ],
+ "support": {
+ "issues": "https://github.com/stripe/stripe-php/issues",
+ "source": "https://github.com/stripe/stripe-php/tree/v16.1.0"
+ },
+ "time": "2024-10-03T22:48:28+00:00"
+ },
+ {
+ "name": "yurunsoft/pay-sdk",
+ "version": "v2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Yurunsoft/PaySDK.git",
+ "reference": "344fde9df7c3ed2e0242759a6de3761519c42507"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Yurunsoft/PaySDK/zipball/344fde9df7c3ed2e0242759a6de3761519c42507",
+ "reference": "344fde9df7c3ed2e0242759a6de3761519c42507",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^5.4|^7.0",
+ "yurunsoft/yurun-http": "~3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Yurun\\PaySDK\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHP 集成支付 SDK ,集成了支付宝、微信支付的支付接口和其它相关接口的操作。可以轻松嵌入支持 PHP >= 5.4 的任何系统中。",
+ "support": {
+ "issues": "https://github.com/Yurunsoft/PaySDK/issues",
+ "source": "https://github.com/Yurunsoft/PaySDK/tree/v2.2.0"
+ },
+ "time": "2020-03-13T05:05:47+00:00"
+ },
+ {
+ "name": "yurunsoft/yurun-http",
+ "version": "v3.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Yurunsoft/YurunHttp.git",
+ "reference": "665b70c7e95abfa6d9b621abf78ccfd7b32da3e8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Yurunsoft/YurunHttp/zipball/665b70c7e95abfa6d9b621abf78ccfd7b32da3e8",
+ "reference": "665b70c7e95abfa6d9b621abf78ccfd7b32da3e8",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.5.0",
+ "psr/http-message": "~1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": ">=4 <8",
+ "swoft/swoole-ide-helper": "~2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Yurun\\Util\\": "./src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "一个基于PHP cURL的开源HTTP类库,支持链式操作,省去繁杂的cURL使用方法。(An open source HTTP class library based on PHP cURL, support chain operations, eliminating the use of miscellaneous cURL methods.)",
+ "support": {
+ "issues": "https://github.com/Yurunsoft/YurunHttp/issues",
+ "source": "https://github.com/Yurunsoft/YurunHttp/tree/v3.5.3"
+ },
+ "time": "2020-02-17T11:11:19+00:00"
+ },
+ {
+ "name": "yurunsoft/yurun-oauth-login",
+ "version": "v2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Yurunsoft/YurunOAuthLogin.git",
+ "reference": "e9c264d4bc1a3d0556d1c5bbef83039992add62d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Yurunsoft/YurunOAuthLogin/zipball/e9c264d4bc1a3d0556d1c5bbef83039992add62d",
+ "reference": "e9c264d4bc1a3d0556d1c5bbef83039992add62d",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.4",
+ "yurunsoft/yurun-http": "~3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Yurun\\OAuthLogin\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "YurunOAuthLogin是一个PHP 第三方登录授权 SDK,集成了QQ、微信、微博、Github等常用接口。可以轻松嵌入支持 PHP >= 5.4 的任何系统中。",
+ "support": {
+ "issues": "https://github.com/Yurunsoft/YurunOAuthLogin/issues",
+ "source": "https://github.com/Yurunsoft/YurunOAuthLogin/tree/dev"
+ },
+ "time": "2020-03-13T05:01:41+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=7.4"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/footer.php b/footer.php
new file mode 100644
index 00000000..2ae0e109
--- /dev/null
+++ b/footer.php
@@ -0,0 +1,476 @@
+ true, 'icp_number' => '', 'police_enabled' => true, 'police_number' => ''];
+$icp_number = $filing['icp_enabled'] ? $filing['icp_number'] : '';
+$police_number = $filing['police_enabled'] ? $filing['police_number'] : '';
+$footer_text = $has_settings ? AKSRT_Settings::footer_text() : '';
+
+// 版权文案
+$copyright_text = $has_settings ? AKSRT_Settings::get_copyright_text() : '';
+
+// 页脚布局模板
+$footer_layout = $has_settings ? AKSRT_Settings::get_footer_layout_template() : 'default';
+
+// §2.3 三列结构化字段
+$footer_brand = $has_settings ? AKSRT_Settings::get_footer_brand_settings() : ['m_s' => ['pc', 'mobile'], 'img' => '', 'img_dark' => '', 'title' => '', 'custom_code' => ''];
+$footer_contact_ext = $has_settings ? AKSRT_Settings::get_footer_contact_extended_settings() : ['m_s' => ['pc', 'mobile'], 'wechat_img' => '', 'qq' => '', 'weibo' => '', 'email' => '', 'more' => [], 'code_1' => '', 'code_2' => ''];
+$footer_mini = $has_settings ? AKSRT_Settings::get_footer_mini_img_settings() : ['m_s' => ['pc', 'mobile'], 'items' => []];
+$footer_customize_code = $has_settings ? AKSRT_Settings::get_footer_customize_code() : '';
+
+// 品牌列设备可见性
+$_brand_show_pc = in_array('pc', $footer_brand['m_s'], true);
+$_brand_show_sm = in_array('mobile', $footer_brand['m_s'], true);
+$_brand_vis_cls = '';
+if ($_brand_show_pc && !$_brand_show_sm) $_brand_vis_cls = ' hide-on-mobile';
+elseif (!$_brand_show_pc && $_brand_show_sm) $_brand_vis_cls = ' hide-on-desktop';
+
+// 联系区设备可见性
+$_contact_show_pc = in_array('pc', $footer_contact_ext['m_s'], true);
+$_contact_show_sm = in_array('mobile', $footer_contact_ext['m_s'], true);
+$_contact_vis_cls = '';
+if ($_contact_show_pc && !$_contact_show_sm) $_contact_vis_cls = ' hide-on-mobile';
+elseif (!$_contact_show_pc && $_contact_show_sm) $_contact_vis_cls = ' hide-on-desktop';
+
+// 二维码组设备可见性
+$_mini_show_pc = in_array('pc', $footer_mini['m_s'], true);
+$_mini_show_sm = in_array('mobile', $footer_mini['m_s'], true);
+$_mini_vis_cls = '';
+if ($_mini_show_pc && !$_mini_show_sm) $_mini_vis_cls = ' hide-on-mobile';
+elseif (!$_mini_show_pc && $_mini_show_sm) $_mini_vis_cls = ' hide-on-desktop';
+
+/**
+ * 渲染快速链接 URL:支持外链 / 站内相对路径
+ */
+$render_link_url = function ($raw) {
+ if (empty($raw)) return '#';
+ if (strpos($raw, 'http') === 0) return esc_url($raw);
+ return esc_url(home_url($raw));
+};
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front-page.php b/front-page.php
new file mode 100644
index 00000000..f07386cc
--- /dev/null
+++ b/front-page.php
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+init();
+AKSRT_Scripts::get_instance()->init();
+AKSRT_Admin::get_instance()->init();
+AKSRT_OAuth::get_instance()->init();
+
+// 初始化代理登录(根据配置模式选择性加载)
+$aksrt_proxy_mode = AKSRT_Settings::oauth_proxy_mode();
+if ($aksrt_proxy_mode === 'client' && class_exists('AKSRT_OAuth_Proxy_Client')) {
+ AKSRT_OAuth_Proxy_Client::get_instance()->init();
+}
+if ($aksrt_proxy_mode === 'server' && class_exists('AKSRT_OAuth_Proxy_Server')) {
+ AKSRT_OAuth_Proxy_Server::get_instance()->init();
+}
+
+AKSRT_Settings::get_instance()->init();
+AKSRT_Auth_Router::get_instance()->init();
+AKSRT_Templates::get_instance()->init();
+AKSRT_Moderation::get_instance()->init();
+
+// 短代码统一注册入口(registry)
+if (class_exists('AKSRT_Shortcodes')) {
+ add_action('init', function () {
+ AKSRT_Shortcodes::get_instance()->register();
+ });
+}
+
+// 用户限制/封禁系统
+if (class_exists('AKSRT_User_Restriction')) {
+ AKSRT_User_Restriction::get_instance()->init();
+}
+
+// 用户中心 & 公开主页协调类
+if (class_exists('AKSRT_User_Center')) {
+ AKSRT_User_Center::get_instance()->init();
+}
+if (class_exists('AKSRT_User_Home')) {
+ AKSRT_User_Home::get_instance()->init();
+}
+
+// 数据表迁移器:注册 hook(after_switch_theme + admin_init 双触发)
+if (class_exists('AKSRT_DB_Migrator')) {
+ AKSRT_DB_Migrator::init();
+}
+
+// 积分获取入口(注册赠送 / 评论奖励 / 签到 / 邀请)
+if (class_exists('AKSRT_Credits_Earn')) {
+ AKSRT_Credits_Earn::get_instance()->init();
+}
+if (class_exists('AKSRT_Signin')) {
+ AKSRT_Signin::get_instance()->init();
+}
+if (class_exists('AKSRT_Invite')) {
+ AKSRT_Invite::get_instance()->init();
+}
+if (class_exists('AKSRT_Download')) {
+ AKSRT_Download::get_instance()->init();
+}
+// Download State & Docs Presenter are pure service singletons (no hooks)
+if (class_exists('AKSRT_Download_State')) {
+ AKSRT_Download_State::get_instance();
+}
+if (class_exists('AKSRT_Docs_Presenter')) {
+ AKSRT_Docs_Presenter::get_instance();
+}
+if (class_exists('AKSRT_Content_Visibility')) {
+ AKSRT_Content_Visibility::get_instance()->init();
+}
+if (class_exists('AKSRT_SMS')) {
+ AKSRT_SMS::get_instance()->init();
+}
+if (class_exists('AKSRT_WeChat_Notify')) {
+ AKSRT_WeChat_Notify::get_instance()->init();
+}
+if (class_exists('AKSRT_Message')) {
+ AKSRT_Message::get_instance()->init();
+}
+if (class_exists('AKSRT_Private_Message')) {
+ AKSRT_Private_Message::get_instance()->init();
+}
+if (class_exists('AKSRT_Report_Center')) {
+ AKSRT_Report_Center::get_instance()->init();
+}
+if (class_exists('AKSRT_Comment_Actions')) {
+ AKSRT_Comment_Actions::get_instance()->init();
+}
+if (class_exists('AKSRT_Go_Redirect')) {
+ AKSRT_Go_Redirect::get_instance()->init();
+}
+if (class_exists('AKSRT_Site_Notice')) {
+ AKSRT_Site_Notice::get_instance()->init();
+}
+if (class_exists('AKSRT_API_Audit')) {
+ AKSRT_API_Audit::get_instance()->init();
+}
+if (class_exists('AKSRT_MeiliSearch')) {
+ AKSRT_MeiliSearch::get_instance()->init();
+ // Async single post index (debounced)
+ add_action('aksrt_meilisearch_index_single', function (int $post_id) {
+ AKSRT_MeiliSearch::get_instance()->index_post($post_id);
+ });
+}
+if (class_exists('AKSRT_Search_Push')) {
+ AKSRT_Search_Push::get_instance()->init();
+}
+if (class_exists('AKSRT_SEO_Head')) {
+ AKSRT_SEO_Head::get_instance()->init();
+}
+if (class_exists('AKSRT_Update_Checker')) {
+ AKSRT_Update_Checker::get_instance()->init();
+}
+if (class_exists('AKSRT_Category_Rewrite')) {
+ AKSRT_Category_Rewrite::get_instance()->init();
+}
+if (class_exists('AKSRT_Front_Submit')) {
+ AKSRT_Front_Submit::get_instance()->init();
+}
+if (class_exists('AKSRT_Order')) {
+ AKSRT_Order::get_instance()->init();
+}
+
+// Register built-in payment gateways
+if (class_exists('AKSRT_Payment_Gateway')) {
+ $gw = AKSRT_Payment_Gateway::get_instance();
+ // 注册支付回调路由(/payment/{gateway}/notify|return|cancel)。
+ $gw->init();
+ if (class_exists('AKSRT_Balance_Gateway')) {
+ $gw->register('balance', new AKSRT_Balance_Gateway());
+ }
+ if (class_exists('AKSRT_Alipay_Gateway')) {
+ $gw->register('alipay', new AKSRT_Alipay_Gateway());
+ }
+ if (class_exists('AKSRT_WechatPay_Gateway')) {
+ $gw->register('wechatpay', new AKSRT_WechatPay_Gateway());
+ }
+ if (class_exists('AKSRT_PayPal_Gateway')) {
+ $gw->register('paypal', new AKSRT_PayPal_Gateway());
+ }
+ if (class_exists('AKSRT_Stripe_Gateway')) {
+ $gw->register('stripe', new AKSRT_Stripe_Gateway());
+ }
+ do_action('aksrt_register_payment_gateways', $gw);
+}
+
+if (class_exists('AKSRT_Forum')) {
+ AKSRT_Forum::get_instance()->init();
+}
+if (is_admin() && class_exists('AKSRT_Resource_Meta_Box')) {
+ AKSRT_Resource_Meta_Box::get_instance()->init();
+}
+
+// 分类/标签局部配置(term meta 字段)
+if (class_exists('AKSRT_Term_Meta')) {
+ AKSRT_Term_Meta::get_instance()->init();
+}
+
+// 商城 taxonomy term meta + shop_discount 优惠活动
+if (class_exists('AKSRT_Shop_Term_Meta')) {
+ AKSRT_Shop_Term_Meta::get_instance()->init();
+}
+
+// 商品后台列表自定义列(销量/综合数据/商家/优惠活动)
+if (is_admin() && class_exists('AKSRT_Admin_Shop_Controller')) {
+ AKSRT_Admin_Shop_Controller::get_instance()->init_product_admin_columns();
+}
+
+// 商品评价/评分系统
+if (class_exists('AKSRT_Product_Review')) {
+ AKSRT_Product_Review::get_instance()->init();
+}
+
+// 文章局部配置 meta box(仅后台)
+if (is_admin() && class_exists('AKSRT_Post_Meta_Box')) {
+ AKSRT_Post_Meta_Box::get_instance()->init();
+}
+
+// 绔欑偣璁块棶閲忕粺璁?
+if (class_exists('AKSRT_Stats_Tracker')) {
+ AKSRT_Stats_Tracker::init();
+}
+
+// Email rollback window (24h): user list row action + admin-post handler.
+if (class_exists('AKSRT_Email_Rollback')) {
+ AKSRT_Email_Rollback::get_instance()->init();
+}
+
+// WordPress 原生"用户"菜单:积分管理
+if (class_exists('AKSRT_User_Credits_Admin')) {
+ AKSRT_User_Credits_Admin::get_instance()->init();
+}
+
+// ============================================================
+// Homepage data model migration: legacy module tabs -> structured homepage_tabs
+// Idempotent; runs once on first admin visit.
+// ============================================================
+if (is_admin() && class_exists('AKSRT_Settings')) {
+ add_action('admin_init', function () {
+ AKSRT_Settings::maybe_migrate_homepage_tabs();
+ }, 5);
+}
+
+// ============================================================
+// 主题删除时的数据库清理(仅在用户启用清理选项时生效)
+// uninstall.php 会检查 aksrt_cleanup_on_uninstall 选项
+// ============================================================
+register_deactivation_hook(__FILE__, function () {
+ // 主题停用时不做任何清理,仅在删除时由 uninstall.php 处理。
+ // 自定义表(aksrt_*)在主题切换后保留数据,确保重新启用时不丢失。
+});
diff --git a/go.php b/go.php
new file mode 100644
index 00000000..0f352384
--- /dev/null
+++ b/go.php
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+ true,
+ 'search_enabled' => true,
+ 'user_menu' => true,
+ 'publish_btn' => true,
+ 'mobile_drawer' => true,
+ 'nav_style' => 'default',
+ 'max_width' => 1200,
+ 'scroll_down_hide' => false,
+ 'layout' => '2',
+ 'mobile_layout' => 'center',
+ 'mobile_align' => 'left',
+ ];
+
+$aksrt_initial_theme_mode = '';
+if (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'initial_theme_mode')) {
+ $aksrt_initial_theme_mode = AKSRT_Settings::initial_theme_mode();
+}
+$aksrt_has_initial_theme = in_array($aksrt_initial_theme_mode, ['dark', 'light'], true);
+
+$aksrt_body_classes = ['header-nav-style-' . $aksrt_header_settings['nav_style']];
+if ($aksrt_initial_theme_mode === 'dark') {
+ $aksrt_body_classes[] = 'theme-dark';
+ $aksrt_body_classes[] = 'dark-theme';
+} elseif ($aksrt_initial_theme_mode === 'light') {
+ $aksrt_body_classes[] = 'white-theme';
+}
+if (!$aksrt_header_settings['fixed']) {
+ $aksrt_body_classes[] = 'has-static-nav';
+}
+if (!empty($aksrt_header_settings['scroll_down_hide'])) {
+ $aksrt_body_classes[] = 'scroll-down-hide';
+}
+$aksrt_body_classes[] = 'header-layout-' . $aksrt_header_settings['layout'];
+$aksrt_body_classes[] = 'mobile-header-' . $aksrt_header_settings['mobile_layout'];
+$aksrt_body_classes[] = 'mobile-nav-' . $aksrt_header_settings['mobile_align'];
+
+// 全局背景系统
+$aksrt_bg_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_global_background_settings() : [];
+$aksrt_bg_styles = [];
+if (!empty($aksrt_bg_settings['bg_color'])) {
+ $aksrt_bg_styles[] = '--aksrt-bg-color:' . sanitize_hex_color($aksrt_bg_settings['bg_color']);
+}
+if (!empty($aksrt_bg_settings['bg_image'])) {
+ $aksrt_bg_styles[] = '--aksrt-bg-image:url(' . esc_url($aksrt_bg_settings['bg_image']) . ')';
+ $aksrt_bg_styles[] = '--aksrt-bg-size:' . sanitize_key($aksrt_bg_settings['bg_size'] ?: '100%');
+ $aksrt_bg_styles[] = '--aksrt-bg-position:' . sanitize_text_field($aksrt_bg_settings['bg_position'] ?: 'center center');
+ $aksrt_bg_styles[] = '--aksrt-bg-repeat:' . sanitize_key($aksrt_bg_settings['bg_repeat'] ?: 'no-repeat');
+ $aksrt_bg_styles[] = '--aksrt-bg-attachment:' . sanitize_key($aksrt_bg_settings['bg_attachment'] ?: 'fixed');
+}
+if (!empty($aksrt_bg_settings['bg_dark_color'])) {
+ $aksrt_bg_styles[] = '--aksrt-bg-dark-color:' . sanitize_hex_color($aksrt_bg_settings['bg_dark_color']);
+}
+if (!empty($aksrt_bg_settings['bg_dark_image'])) {
+ $aksrt_bg_styles[] = '--aksrt-bg-dark-image:url(' . esc_url($aksrt_bg_settings['bg_dark_image']) . ')';
+}
+if (!empty($aksrt_bg_settings['bg_overlay'])) {
+ $aksrt_bg_styles[] = '--aksrt-bg-overlay-color:' . sanitize_text_field($aksrt_bg_settings['bg_overlay_color'] ?: 'rgba(0, 0, 0, 0.3)');
+}
+if (!empty($aksrt_bg_settings['page_bg_color'])) {
+ $aksrt_bg_styles[] = '--aksrt-page-bg-color:' . sanitize_hex_color($aksrt_bg_settings['page_bg_color']);
+}
+if (!empty($aksrt_bg_settings['page_bg_image'])) {
+ $aksrt_bg_styles[] = '--aksrt-page-bg-image:url(' . esc_url($aksrt_bg_settings['page_bg_image']) . ')';
+}
+if (!empty($aksrt_bg_styles)) {
+ $aksrt_body_classes[] = 'aksrt-bg-body';
+ if (!empty($aksrt_bg_settings['bg_overlay'])) {
+ $aksrt_body_classes[] = 'aksrt-bg-overlay';
+ }
+}
+?>
+>
+
+
+
+
+
+>
+
+
+>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+get_unread_count(get_current_user_id()) : 0;
+ $aksrt_user_credit = function_exists('aksrt_user_credits') && is_user_logged_in() ? (int) aksrt_user_credits(get_current_user_id()) : 0;
+ $aksrt_currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
+ $aksrt_messages_url = function_exists('aksrt_get_messages_url') ? aksrt_get_messages_url() : home_url('/messages/');
+ $aksrt_orders_url = home_url('/orders/');
+ $aksrt_publish_url = admin_url('post-new.php');
+ $aksrt_theme_toggle_positions = class_exists('AKSRT_Settings')
+ ? AKSRT_Settings::get_theme_mode_button_positions()
+ : ['pc_nav', 'm_menu'];
+ $aksrt_can_publish = !empty($aksrt_header_settings['publish_btn']) && (
+ current_user_can('manage_options')
+ || (class_exists('AKSRT_Front_Submit') && AKSRT_Front_Submit::get_instance()->can_submit())
+ );
+ $aksrt_nav_classes = [
+ 'site-nav',
+ 'site-nav--' . $aksrt_header_settings['nav_style'],
+ $aksrt_header_settings['fixed'] ? 'site-nav--fixed' : 'site-nav--static',
+ ];
+ $aksrt_nav_styles = [];
+ if ((int) $aksrt_header_settings['max_width'] > 0) {
+ $aksrt_nav_styles[] = '--nav-max-width:' . (int) $aksrt_header_settings['max_width'] . 'px';
+ }
+ $aksrt_logo_parts = [];
+ if ($aksrt_logo_url !== '') {
+ $aksrt_logo_parts[] = ' 0 ? ' style="width:' . (int) $aksrt_logo_width . 'px"' : '') . '>';
+ $aksrt_brand_logo_src = $aksrt_logo_url;
+ }
+ if ($aksrt_logo_dark_url !== '') {
+ $aksrt_logo_parts[] = ' 0 ? ' style="width:' . (int) $aksrt_logo_width . 'px"' : '') . '>';
+ if ($aksrt_brand_logo_src === '') {
+ $aksrt_brand_logo_src = $aksrt_logo_dark_url;
+ }
+ }
+ $aksrt_has_brand_logo = !empty($aksrt_logo_parts);
+ if ($aksrt_has_brand_logo) {
+ $aksrt_brand_logo_html = implode('', $aksrt_logo_parts);
+ }
+ $aksrt_brand_mark = strtoupper(substr(preg_replace('/\s+/', '', wp_strip_all_tags($aksrt_brand_name)), 0, 1));
+ if ($aksrt_brand_mark === '') {
+ $aksrt_brand_mark = 'A';
+ }
+ $aksrt_brand_class = 'site-brand';
+ if ($aksrt_has_brand_logo) {
+ $aksrt_brand_class .= ' site-brand--has-logo';
+ }
+ $aksrt_brand_drawer_class = $aksrt_brand_class . ' site-brand--drawer';
+ $aksrt_header_classes = [
+ 'header',
+ 'header-layout-' . $aksrt_header_settings['layout'],
+ $aksrt_header_settings['fixed'] ? 'nav-fixed' : 'nav-static',
+ ];
+ if (!empty($aksrt_header_settings['scroll_down_hide'])) {
+ $aksrt_header_classes[] = 'show-slide';
+ }
+?>
+
+
+
+
+
+
+
+
diff --git a/home.php b/home.php
new file mode 100644
index 00000000..011baf24
--- /dev/null
+++ b/home.php
@@ -0,0 +1,16 @@
+ true];
+
+if (!empty($system_tools['close_attachment_page'])) {
+ $attachment_id = get_queried_object_id();
+ $parent_id = (int) wp_get_post_parent_id($attachment_id);
+ $redirect_url = $parent_id > 0 ? get_permalink($parent_id) : home_url('/');
+
+ wp_safe_redirect($redirect_url, 301);
+ exit;
+}
+
+get_header();
+
+$attachment_id = get_queried_object_id();
+$parent_id = (int) wp_get_post_parent_id($attachment_id);
+$image_url = wp_get_attachment_url($attachment_id);
+$image_alt = trim((string) get_post_meta($attachment_id, '_wp_attachment_image_alt', true));
+$image_caption = wp_get_attachment_caption($attachment_id);
+$image_title = get_the_title($attachment_id);
+$parent_title = $parent_id > 0 ? get_the_title($parent_id) : '';
+?>
+
+
+
+
+
+
+
+ 50000 时使用)
+add_action('aksrt_email_verification_backfill_batch', function ($offset = 0) {
+ if (class_exists('AKSRT_Auth_Service')) {
+ AKSRT_Auth_Service::run_async_backfill_batch((int) $offset);
+ }
+}, 10, 1);
+
+// ============================================================
+// 阶段6:设置迁移 — 将旧 option key 映射到新 Schema 结构
+// 仅在后台上下文执行,前台不触发。
+// ============================================================
+add_action('admin_init', function () {
+ if (class_exists('AKSRT_Settings_Migration')) {
+ AKSRT_Settings_Migration::get_instance()->maybe_migrate();
+ }
+}, 5);
+
diff --git a/inc/classes/admin/class-aksrt-admin-ai-controller.php b/inc/classes/admin/class-aksrt-admin-ai-controller.php
new file mode 100644
index 00000000..65cec3dc
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-ai-controller.php
@@ -0,0 +1,201 @@
+ 'GET',
+ 'callback' => [$this, 'get_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // AI settings update
+ register_rest_route(self::API_NAMESPACE, '/ai/settings', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // AI SEO suggest
+ register_rest_route(self::API_NAMESPACE, '/ai/seo-suggest', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'seo_suggest'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // AI summary suggest
+ register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'summary_suggest'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // AI tags suggest
+ register_rest_route(self::API_NAMESPACE, '/ai/tags-suggest', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'tags_suggest'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+ /**
+ * GET /ai/status — AI 状态
+ */
+ public function get_status(): WP_REST_Response {
+ if (!class_exists('AKSRT_AI')) {
+ return $this->success(['data' => [
+ 'enabled' => false,
+ 'provider' => 'none',
+ 'configured' => false,
+ 'available' => false,
+ ]]);
+ }
+
+ return $this->success(['data' => AKSRT_AI::get_instance()->get_status()]);
+ }
+
+ /**
+ * POST /ai/settings — 更新 AI 设置
+ */
+ public function update_settings(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_AI')) {
+ return $this->error('AI 模块未加载', 500);
+ }
+
+ $params = $this->get_json_params($request);
+
+ $allowed = [
+ 'enabled' => 'bool',
+ 'provider' => 'string',
+ 'api_key' => 'string',
+ 'api_base' => 'url',
+ 'model' => 'string',
+ 'max_tokens' => 'int',
+ 'rate_limit' => 'int',
+ ];
+
+ $config = [];
+ foreach ($allowed as $key => $type) {
+ if (!isset($params[$key])) continue;
+ switch ($type) {
+ case 'bool':
+ $config[$key] = !empty($params[$key]);
+ break;
+ case 'int':
+ $config[$key] = (int) $params[$key];
+ break;
+ case 'url':
+ $config[$key] = esc_url_raw((string) $params[$key]);
+ break;
+ default:
+ $config[$key] = sanitize_text_field((string) $params[$key]);
+ }
+ }
+
+ $success = AKSRT_AI::get_instance()->update_config($config);
+
+ if ($success) {
+ return $this->success(['message' => __('AI 设置已保存', 'aksrt')]);
+ }
+ return $this->error(__('保存失败', 'aksrt'));
+ }
+
+ /**
+ * POST /ai/seo-suggest — SEO 建议
+ */
+ public function seo_suggest(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_AI_SEO')) {
+ return $this->error('AI SEO 模块未加载', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $title = sanitize_text_field((string) ($params['title'] ?? ''));
+ $content = wp_kses_post((string) ($params['content'] ?? ''));
+
+ if (empty($title)) {
+ return $this->error('标题不能为空');
+ }
+
+ $result = AKSRT_AI_SEO::get_instance()->suggest($title, $content);
+
+ if ($result['success']) {
+ return $this->success(['data' => $result['data']]);
+ }
+
+ return $this->error($result['message'] ?? '生成失败');
+ }
+
+ /**
+ * POST /ai/summary-suggest — 摘要建议
+ */
+ public function summary_suggest(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_AI') || !class_exists('AKSRT_AI_Prompt')) {
+ return $this->error('AI 模块未加载', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $title = sanitize_text_field((string) ($params['title'] ?? ''));
+ $content = wp_kses_post((string) ($params['content'] ?? ''));
+ $max_len = max(50, min(500, (int) ($params['max_length'] ?? 200)));
+
+ if (empty($content)) {
+ return $this->error('内容不能为空');
+ }
+
+ $prompt = AKSRT_AI_Prompt::get_instance()->build_summary_prompt($content, $max_len);
+ $result = AKSRT_AI::get_instance()->generate($prompt);
+
+ if ($result['success']) {
+ return $this->success(['data' => ['summary' => $result['content']]]);
+ }
+
+ return $this->error($result['message'] ?? '生成失败');
+ }
+
+ /**
+ * POST /ai/tags-suggest — 标签建议
+ */
+ public function tags_suggest(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_AI') || !class_exists('AKSRT_AI_Prompt')) {
+ return $this->error('AI 模块未加载', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $title = sanitize_text_field((string) ($params['title'] ?? ''));
+ $content = wp_kses_post((string) ($params['content'] ?? ''));
+
+ if (empty($title) && empty($content)) {
+ return $this->error('标题或内容不能为空');
+ }
+
+ $prompt = AKSRT_AI_Prompt::get_instance()->build_tags_prompt($title, $content);
+ $result = AKSRT_AI::get_instance()->generate($prompt);
+
+ if ($result['success']) {
+ $tags = json_decode($result['content'], true);
+ if (!is_array($tags)) {
+ $tags = array_filter(array_map('trim', explode(',', $result['content'])));
+ }
+ return $this->success(['data' => ['tags' => $tags]]);
+ }
+
+ return $this->error($result['message'] ?? '生成失败');
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-comment-controller.php b/inc/classes/admin/class-aksrt-admin-comment-controller.php
new file mode 100644
index 00000000..0b61d4a5
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-comment-controller.php
@@ -0,0 +1,424 @@
+ 'GET',
+ 'callback' => [$this, 'list_comments'],
+ 'permission_callback' => [$this, 'check_permission'],
+ 'args' => [
+ 'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
+ 'per_page' => ['default' => 20, 'sanitize_callback' => 'absint'],
+ 'post_id' => ['default' => 0, 'sanitize_callback' => 'absint'],
+ 'status' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
+ 'search' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
+ ],
+ ]);
+
+ // Create comment (admin or frontend)
+ register_rest_route(self::API_NAMESPACE, '/comments', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_comment'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ 'args' => [
+ 'post_id' => ['required' => true, 'sanitize_callback' => 'absint'],
+ 'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'],
+ 'parent' => ['default' => 0, 'sanitize_callback' => 'absint'],
+ ],
+ ]);
+
+ // Update comment status (admin)
+ register_rest_route(self::API_NAMESPACE, '/comments/(?P\d+)/status', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_comment_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ 'args' => [
+ 'id' => ['required' => true, 'sanitize_callback' => 'absint'],
+ 'status' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'],
+ ],
+ ]);
+
+ // Edit comment content (admin)
+ register_rest_route(self::API_NAMESPACE, '/comments/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_comment'],
+ 'permission_callback' => [$this, 'check_permission'],
+ 'args' => [
+ 'id' => ['required' => true, 'sanitize_callback' => 'absint'],
+ 'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'],
+ ],
+ ]);
+
+ // Delete comment (admin)
+ register_rest_route(self::API_NAMESPACE, '/comments/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'delete_comment'],
+ 'permission_callback' => [$this, 'check_permission'],
+ 'args' => [
+ 'id' => ['required' => true, 'sanitize_callback' => 'absint'],
+ ],
+ ]);
+
+ // Bulk action (admin)
+ register_rest_route(self::API_NAMESPACE, '/comments/bulk', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'bulk_action'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Frontend: list comments for a post (public)
+ register_rest_route(self::API_NAMESPACE, '/comments/post/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_post_comments'],
+ 'permission_callback' => '__return_true',
+ 'args' => [
+ 'post_id' => ['required' => true, 'sanitize_callback' => 'absint'],
+ 'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
+ 'per_page' => ['default' => 10, 'sanitize_callback' => 'absint'],
+ 'order' => ['default' => 'DESC', 'sanitize_callback' => 'sanitize_text_field'],
+ ],
+ ]);
+ }
+
+ /**
+ * GET /comments — List comments with filtering.
+ */
+ public function list_comments(WP_REST_Request $request): WP_REST_Response {
+ $args = [
+ 'number' => min(100, (int) $request->get_param('per_page')),
+ 'offset' => ((int) $request->get_param('page') - 1) * (int) $request->get_param('per_page'),
+ 'orderby' => 'comment_date_gmt',
+ 'order' => 'DESC',
+ ];
+
+ $post_id = (int) $request->get_param('post_id');
+ if ($post_id > 0) {
+ $args['post_id'] = $post_id;
+ }
+
+ $status = $request->get_param('status');
+ if ($status !== '') {
+ $args['status'] = $status;
+ }
+
+ $search = $request->get_param('search');
+ if ($search !== '') {
+ $args['search'] = $search;
+ }
+
+ $query = new WP_Comment_Query($args);
+
+ // Batch prefetch to avoid N+1 queries
+ $user_ids = array_unique(array_filter(array_map(fn($c) => (int) $c->user_id, $query->comments)));
+ $post_ids = array_unique(array_map(fn($c) => (int) $c->comment_post_ID, $query->comments));
+ $users_map = [];
+ $posts_map = [];
+ if (!empty($user_ids)) {
+ $users = get_users(['include' => $user_ids, 'fields' => ['ID', 'display_name']]);
+ foreach ($users as $u) $users_map[$u->ID] = $u;
+ }
+ if (!empty($post_ids)) {
+ global $wpdb;
+ $ids_csv = implode(',', array_map('intval', $post_ids));
+ $titles = $wpdb->get_results("SELECT ID, post_title FROM {$wpdb->posts} WHERE ID IN ({$ids_csv})", OBJECT_K);
+ foreach ($titles as $t) $posts_map[$t->ID] = $t->post_title;
+ }
+
+ $comments = [];
+ foreach ($query->comments as $comment) {
+ $comments[] = $this->format_comment($comment, $users_map, $posts_map);
+ }
+
+ // Total count for pagination
+ $count_args = $args;
+ unset($count_args['number'], $count_args['offset'], $count_args['orderby'], $count_args['order']);
+ $count_args['count'] = true;
+ $total = (int) (new WP_Comment_Query($count_args))->get_comments('count');
+
+ return $this->success([
+ 'data' => $comments,
+ 'total' => $total,
+ 'pages' => ceil($total / (int) $request->get_param('per_page')),
+ ]);
+ }
+
+ /**
+ * POST /comments — Create a comment.
+ */
+ public function create_comment(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ $post_id = (int) $request->get_param('post_id');
+ $content = trim((string) $request->get_param('content'));
+ $parent = (int) $request->get_param('parent');
+
+ if (!$content) {
+ return $this->error('content_required', '评论内容不能为空。', 400);
+ }
+
+ $post = get_post($post_id);
+ if (!$post || $post->post_status !== 'publish') {
+ return $this->error('invalid_post', '文章不存在或未发布。', 400);
+ }
+
+ $comment_data = [
+ 'comment_post_ID' => $post_id,
+ 'comment_content' => $content,
+ 'user_id' => $user_id,
+ 'comment_parent' => $parent,
+ 'comment_approved'=> 1, // Admin-created comments are auto-approved
+ ];
+
+ $comment_id = wp_insert_comment($comment_data);
+ if (is_wp_error($comment_id)) {
+ return $this->error('create_failed', $comment_id->get_error_message(), 500);
+ }
+
+ $comment = get_comment($comment_id);
+ return $this->success(['data' => $this->format_comment($comment)]);
+ }
+
+ /**
+ * POST /comments/{id}/status — Approve/unapprove/spam/trash.
+ */
+ public function update_comment_status(WP_REST_Request $request): WP_REST_Response {
+ $comment_id = (int) $request->get_param('id');
+ $new_status = $request->get_param('status');
+ $comment = get_comment($comment_id);
+
+ if (!$comment) {
+ return $this->error('not_found', '评论不存在。', 404);
+ }
+
+ $valid_statuses = ['approve', 'hold', 'spam', 'trash'];
+ if (!in_array($new_status, $valid_statuses, true)) {
+ return $this->error('invalid_status', '无效的状态值。', 400);
+ }
+
+ $result = wp_set_comment_status($comment_id, $new_status);
+ if (is_wp_error($result)) {
+ return $this->error('update_failed', $result->get_error_message(), 500);
+ }
+
+ // Handle spam/trash
+ if ($new_status === 'spam') {
+ wp_spam_comment($comment_id);
+ } elseif ($new_status === 'trash') {
+ wp_trash_comment($comment_id);
+ }
+
+ $comment = get_comment($comment_id);
+ return $this->success(['data' => $this->format_comment($comment)]);
+ }
+
+ /**
+ * POST /comments/{id} — Edit comment content.
+ */
+ public function update_comment(WP_REST_Request $request): WP_REST_Response {
+ $comment_id = (int) $request->get_param('id');
+ $content = trim((string) $request->get_param('content'));
+ $comment = get_comment($comment_id);
+
+ if (!$comment) {
+ return $this->error('not_found', '评论不存在。', 404);
+ }
+
+ if (!$content) {
+ return $this->error('content_required', '评论内容不能为空。', 400);
+ }
+
+ $result = wp_update_comment(['comment_ID' => $comment_id, 'comment_content' => $content]);
+ if (is_wp_error($result)) {
+ return $this->error('update_failed', $result->get_error_message(), 500);
+ }
+
+ $comment = get_comment($comment_id);
+ return $this->success(['data' => $this->format_comment($comment)]);
+ }
+
+ /**
+ * DELETE /comments/{id} — Delete a comment.
+ */
+ public function delete_comment(WP_REST_Request $request): WP_REST_Response {
+ $comment_id = (int) $request->get_param('id');
+ $comment = get_comment($comment_id);
+
+ if (!$comment) {
+ return $this->error('not_found', '评论不存在。', 404);
+ }
+
+ $result = wp_delete_comment($comment_id, true);
+ if (!$result) {
+ return $this->error('delete_failed', '删除评论失败。', 500);
+ }
+
+ return $this->success(['data' => ['deleted' => true, 'id' => $comment_id]]);
+ }
+
+ /**
+ * POST /comments/bulk — Bulk action on comments.
+ */
+ public function bulk_action(WP_REST_Request $request): WP_REST_Response {
+ $body = $request->get_json_params();
+ $ids = array_map('absint', $body['ids'] ?? []);
+ $action = sanitize_text_field($body['action'] ?? '');
+
+ if (empty($ids) || !$action) {
+ return $this->error('missing_params', '缺少参数。', 400);
+ }
+
+ $success = 0;
+ $failed = 0;
+
+ foreach ($ids as $id) {
+ $comment = get_comment($id);
+ if (!$comment) { $failed++; continue; }
+
+ switch ($action) {
+ case 'approve':
+ wp_set_comment_status($id, 'approve');
+ $success++;
+ break;
+ case 'hold':
+ wp_set_comment_status($id, 'hold');
+ $success++;
+ break;
+ case 'spam':
+ wp_spam_comment($id);
+ $success++;
+ break;
+ case 'trash':
+ wp_trash_comment($id);
+ $success++;
+ break;
+ case 'delete':
+ wp_delete_comment($id, true);
+ $success++;
+ break;
+ default:
+ $failed++;
+ }
+ }
+
+ return $this->success(['data' => ['success' => $success, 'failed' => $failed]]);
+ }
+
+ /**
+ * GET /comments/post/{post_id} — Public: list comments for a post with nesting.
+ */
+ public function get_post_comments(WP_REST_Request $request): WP_REST_Response {
+ $post_id = (int) $request->get_param('post_id');
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = min(50, max(1, (int) $request->get_param('per_page')));
+ $order = in_array(strtoupper((string) $request->get_param('order')), ['ASC', 'DESC'], true)
+ ? strtoupper((string) $request->get_param('order'))
+ : 'DESC';
+
+ $args = [
+ 'post_id' => $post_id,
+ 'status' => 'approve',
+ 'number' => $per_page,
+ 'offset' => ($page - 1) * $per_page,
+ 'orderby' => 'comment_date_gmt',
+ 'order' => $order,
+ 'hierarchical' => 'threaded',
+ ];
+
+ $query = new WP_Comment_Query($args);
+
+ // Batch prefetch users
+ $user_ids = array_unique(array_filter(array_map(fn($c) => (int) $c->user_id, $query->comments)));
+ $users_map = [];
+ if (!empty($user_ids)) {
+ $users = get_users(['include' => $user_ids, 'fields' => ['ID', 'display_name']]);
+ foreach ($users as $u) $users_map[$u->ID] = $u;
+ }
+
+ $comments = [];
+ foreach ($query->comments as $comment) {
+ $comments[] = $this->format_comment_public($comment, $users_map);
+ }
+
+ // Total approved comments for this post
+ $total = (int) get_comments_number($post_id);
+ $total_approved = get_comments([
+ 'post_id' => $post_id,
+ 'status' => 'approve',
+ 'count' => true,
+ ]);
+
+ return $this->success([
+ 'data' => $comments,
+ 'total' => (int) $total_approved,
+ 'pages' => ceil((int) $total_approved / $per_page),
+ ]);
+ }
+
+ /**
+ * Format comment for admin views.
+ */
+ private function format_comment(WP_Comment $comment, array $users_map = [], array $posts_map = []): array {
+ $uid = (int) $comment->user_id;
+ $pid = (int) $comment->comment_post_ID;
+ $user = $users_map[$uid] ?? ($uid ? get_userdata($uid) : null);
+ return [
+ 'id' => (int) $comment->comment_ID,
+ 'post_id' => $pid,
+ 'post_title' => $posts_map[$pid] ?? get_the_title($pid),
+ 'author' => $comment->comment_author,
+ 'author_email' => $comment->comment_author_email,
+ 'user_id' => (int) $comment->user_id,
+ 'user_name' => $user ? $user->display_name : '',
+ 'content' => $comment->comment_content,
+ 'status' => $comment->comment_approved,
+ 'parent' => (int) $comment->comment_parent,
+ 'date' => $comment->comment_date_gmt,
+ 'author_ip' => $comment->comment_author_IP,
+ 'avatar_url' => get_avatar_url($comment->comment_author_email, ['size' => 48]),
+ ];
+ }
+
+ /**
+ * Format comment for public views (no email/IP).
+ */
+ private function format_comment_public(WP_Comment $comment, array $users_map = []): array {
+ $uid = (int) $comment->user_id;
+ $user = $users_map[$uid] ?? ($uid ? get_userdata($uid) : null);
+ return [
+ 'id' => (int) $comment->comment_ID,
+ 'author' => $comment->comment_author,
+ 'user_id' => (int) $comment->user_id,
+ 'content' => $comment->comment_content,
+ 'date' => $comment->comment_date_gmt,
+ 'parent' => (int) $comment->comment_parent,
+ 'avatar_url' => get_avatar_url($comment->comment_author_email, ['size' => 48]),
+ 'children' => [], // Will be populated by JS if threaded
+ ];
+ }
+
+ /**
+ * Helper: check logged-in permission (any authenticated user).
+ */
+ public function check_logged_in_permission(): bool {
+ return is_user_logged_in();
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-controller-base.php b/inc/classes/admin/class-aksrt-admin-controller-base.php
new file mode 100644
index 00000000..3f3001d2
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-controller-base.php
@@ -0,0 +1,173 @@
+ true], $data), $status);
+ }
+
+ /**
+ * Create a standard error response.
+ */
+ protected function error(string $message, int $status = 400): WP_REST_Response {
+ return new WP_REST_Response([
+ 'success' => false,
+ 'message' => $message,
+ ], $status);
+ }
+
+ /**
+ * Read JSON request body safely.
+ */
+ protected function get_json_params(WP_REST_Request $request): array {
+ $params = $request->get_json_params();
+ return is_array($params) ? $params : [];
+ }
+
+ /**
+ * Sanitize a settings array using a whitelist of allowed keys.
+ *
+ * @param array $input Raw input data.
+ * @param array $allowed Allowed keys with their types (string|int|bool|float|array).
+ * @return array Sanitized data.
+ */
+ protected function sanitize_fields(array $input, array $allowed): array {
+ $out = [];
+ foreach ($allowed as $key => $type) {
+ if (!array_key_exists($key, $input)) continue;
+ $val = $input[$key];
+ switch ($type) {
+ case 'string':
+ $out[$key] = sanitize_text_field($val);
+ break;
+ case 'html':
+ $out[$key] = wp_kses_post($val);
+ break;
+ case 'code':
+ $out[$key] = is_string($val) ? wp_unslash($val) : '';
+ break;
+ case 'url':
+ $out[$key] = esc_url_raw($val);
+ break;
+ case 'int':
+ $out[$key] = (int) $val;
+ break;
+ case 'bool':
+ $out[$key] = !empty($val);
+ break;
+ case 'float':
+ $out[$key] = (float) $val;
+ break;
+ case 'array':
+ $out[$key] = is_array($val) ? $val : [];
+ break;
+ default:
+ $out[$key] = sanitize_text_field($val);
+ }
+ }
+ return $out;
+ }
+
+ protected function register_section_routes(string $route_slug, string $domain): void {
+ register_rest_route(self::API_NAMESPACE, '/settings/' . $route_slug . '/(?P[a-z0-9_-]+)', [
+ 'methods' => 'GET',
+ 'callback' => function (WP_REST_Request $request) use ($domain) {
+ return $this->get_section_settings($request, $domain);
+ },
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/settings/' . $route_slug . '/(?P[a-z0-9_-]+)', [
+ 'methods' => 'POST',
+ 'callback' => function (WP_REST_Request $request) use ($domain) {
+ return $this->save_section_settings($request, $domain);
+ },
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+ protected function get_section_settings(WP_REST_Request $request, string $domain): WP_REST_Response {
+ $provider = AKSRT_Admin_Schema_Provider::get_instance();
+ $section = (string) $request->get_param('section');
+ $schema = $provider->get_section_schema($domain, $section);
+
+ if (!$schema) {
+ return $this->error('未知的设置分组: ' . esc_html($section), 404);
+ }
+
+ return $this->success([
+ 'data' => $provider->read_settings($domain, $section),
+ 'schema' => $schema,
+ 'payload' => [
+ 'domain' => $provider->normalize_domain($domain),
+ 'section' => $provider->normalize_section($section),
+ 'version' => $schema['version'] ?? '1.0.0',
+ ],
+ ]);
+ }
+
+ protected function save_section_settings(WP_REST_Request $request, string $domain): WP_REST_Response {
+ $provider = AKSRT_Admin_Schema_Provider::get_instance();
+ $section = (string) $request->get_param('section');
+ $schema = $provider->get_section_schema($domain, $section);
+
+ if (!$schema) {
+ return $this->error('未知的设置分组: ' . esc_html($section), 404);
+ }
+
+ $params = $this->get_json_params($request);
+ $payload = $params['data'] ?? $params;
+ if (!is_array($payload)) {
+ return $this->error('无效的设置载荷');
+ }
+
+ $sanitized = $this->sanitize_fields($payload, $provider->get_allowed_fields($domain, $section));
+ $validation = $provider->validate($domain, $sanitized, $section);
+ if (!$validation['valid']) {
+ return $this->error(implode('; ', $validation['errors']), 422);
+ }
+
+ $provider->write_settings($domain, $sanitized, $section);
+
+ return $this->success([
+ 'message' => sprintf('%s/%s 设置已保存', $provider->normalize_domain($domain), $provider->normalize_section($section)),
+ 'data' => $provider->read_settings($domain, $section),
+ 'payload' => [
+ 'domain' => $provider->normalize_domain($domain),
+ 'section' => $provider->normalize_section($section),
+ 'version' => $schema['version'] ?? '1.0.0',
+ ],
+ ]);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-extension-controller.php b/inc/classes/admin/class-aksrt-admin-extension-controller.php
new file mode 100644
index 00000000..8680edab
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-extension-controller.php
@@ -0,0 +1,456 @@
+admin = $admin;
+ }
+
+ public function register_routes(): void {
+ // Extensions overview
+ register_rest_route(self::API_NAMESPACE, '/extensions/overview', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_overview'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Docs page stats
+ register_rest_route(self::API_NAMESPACE, '/docs/stats', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_docs_stats'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Docs search (public, for frontend AJAX)
+ register_rest_route(self::API_NAMESPACE, '/docs/search', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'search_docs'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // Extensions service status summary (aggregated)
+ register_rest_route(self::API_NAMESPACE, '/settings/extensions', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_extensions_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Security settings summary
+ register_rest_route(self::API_NAMESPACE, '/settings/security', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_security_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Security settings — save risk switches
+ register_rest_route(self::API_NAMESPACE, '/settings/security', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'save_security_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Security audit log
+ register_rest_route(self::API_NAMESPACE, '/security/audit-log', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_audit_log'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // IP geo test tool
+ register_rest_route(self::API_NAMESPACE, '/extensions/ip-test', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'test_ip_geo'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Content audit test tool
+ register_rest_route(self::API_NAMESPACE, '/extensions/audit-test', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'test_content_audit'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+ /**
+ * GET /extensions/overview — real extension status aggregation.
+ */
+ public function get_overview(): WP_REST_Response {
+ $services = $this->collect_service_statuses();
+
+ $installed = 0;
+ $available = count($services);
+ $unconfigured = 0;
+
+ foreach ($services as $svc) {
+ if (!empty($svc['enabled'])) $installed++;
+ if (!empty($svc['enabled']) && !empty($svc['configured']) && empty($svc['verified'])) {
+ $unconfigured++;
+ }
+ }
+
+ // Build message
+ if ($installed === 0) {
+ $message = sprintf(__('已注册 %d 项扩展,均未启用。', 'aksrt'), $available);
+ } elseif ($unconfigured > 0) {
+ $message = sprintf(__('已启用 %d/%d 项扩展,%d 项待补全配置。', 'aksrt'), $installed, $available, $unconfigured);
+ } else {
+ $message = sprintf(__('已启用 %d/%d 项扩展,全部就绪。', 'aksrt'), $installed, $available);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'installed' => $installed,
+ 'available' => $available,
+ 'message' => $message,
+ 'services' => $services,
+ ],
+ ]);
+ }
+
+ public function get_docs_stats(): WP_REST_Response {
+ $nav_count = 0;
+ $special_count = 0;
+
+ $nav_query = new WP_Query([
+ 'post_type' => 'page',
+ 'post_status' => 'publish',
+ 'meta_key' => '_wp_page_template',
+ 'meta_value' => 'templates/pages/page-documentnav.php',
+ 'posts_per_page' => 1,
+ 'fields' => 'ids',
+ 'no_found_rows' => false,
+ ]);
+ $nav_count = (int) $nav_query->found_posts;
+
+ $special_query = new WP_Query([
+ 'post_type' => 'page',
+ 'post_status' => 'publish',
+ 'meta_key' => '_wp_page_template',
+ 'meta_value' => 'templates/pages/page-special.php',
+ 'posts_per_page' => 1,
+ 'fields' => 'ids',
+ 'no_found_rows' => false,
+ ]);
+ $special_count = (int) $special_query->found_posts;
+
+ return $this->success([
+ 'data' => [
+ 'total' => $nav_count + $special_count,
+ 'nav_count' => $nav_count,
+ 'special_count' => $special_count,
+ ],
+ ]);
+ }
+
+ /**
+ * GET /docs/search — search document pages by keyword.
+ */
+ public function search_docs(WP_REST_Request $request): WP_REST_Response {
+ $keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
+ $root_id = (int) $request->get_param('root_id');
+ $limit = max(1, min(50, (int) ($request->get_param('limit') ?: 20)));
+
+ if ($keyword === '') {
+ return $this->success(['data' => ['results' => [], 'total' => 0]]);
+ }
+
+ if (!class_exists('AKSRT_Docs_Presenter')) {
+ return $this->success(['data' => ['results' => [], 'total' => 0]]);
+ }
+
+ $results = AKSRT_Docs_Presenter::get_instance()->search_pages($keyword, $root_id, $limit);
+
+ return $this->success([
+ 'data' => [
+ 'results' => $results,
+ 'total' => count($results),
+ ],
+ ]);
+ }
+
+ /**
+ * GET /settings/extensions — aggregated extension service status.
+ */
+ public function get_extensions_status(): WP_REST_Response {
+ $services = $this->collect_service_statuses();
+ $flat = [];
+
+ foreach ($services as $key => $svc) {
+ $flat["{$key}_enabled"] = !empty($svc['enabled']);
+ $flat["{$key}_configured"] = !empty($svc['configured']);
+ $flat["{$key}_available"] = !empty($svc['available']);
+ $flat["{$key}_verified"] = !empty($svc['verified']);
+ $flat["{$key}_provider"] = (string) ($svc['provider'] ?? '');
+ }
+
+ // 保留旧布尔字段兼容
+ $flat['sms_enabled'] = !empty($services['sms']['enabled']);
+ $flat['wechat_enabled'] = !empty($services['wechat']['enabled']);
+ $flat['captcha_enabled'] = !empty($services['captcha']['enabled']);
+ $flat['baidu_push_enabled'] = !empty($services['push']['enabled']);
+ $flat['content_audit_enabled'] = !empty($services['audit']['enabled']);
+
+ return $this->success([
+ 'data' => $flat,
+ 'services' => $services,
+ ]);
+ }
+
+ /**
+ * GET /settings/security — security settings summary with real service status.
+ */
+ public function get_security_settings(): WP_REST_Response {
+ $services = $this->collect_service_statuses();
+ $settings = AKSRT_Settings::get_all();
+
+ $email_gate_enabled = true;
+ if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) {
+ $email_gate_enabled = false;
+ }
+
+ $captcha_comment_enabled = !empty($settings['captcha_comment_enabled']);
+ $captcha_download_enabled = !empty($settings['captcha_download_enabled']);
+
+ $risk_switches = [
+ 'show_unapproved_comments' => !empty($settings['show_unapproved_comments']),
+ 'skip_submit_moderation' => !empty($settings['skip_submit_moderation']),
+ 'guest_comment' => !empty($settings['guest_comment']),
+ ];
+
+ return $this->success([
+ 'data' => [
+ 'email_gate_enabled' => $email_gate_enabled,
+ 'captcha_comment_enabled' => $captcha_comment_enabled,
+ 'captcha_download_enabled' => $captcha_download_enabled,
+ 'content_audit_enabled' => !empty($services['audit']['enabled']),
+ 'content_audit_configured' => !empty($services['audit']['configured']),
+ 'ip_geo_enabled' => !empty($services['ip_geo']['enabled']),
+ 'ip_geo_configured' => !empty($services['ip_geo']['configured']),
+ 'captcha_enabled' => $captcha_comment_enabled || $captcha_download_enabled,
+ 'risk_switches' => $risk_switches,
+ 'services' => $services,
+ ],
+ ]);
+ }
+
+ /**
+ * POST /settings/security — save risk switches.
+ */
+ public function save_security_settings(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $risk_switches = $params['risk_switches'] ?? [];
+
+ if (!is_array($risk_switches)) {
+ return $this->error('无效的风险设置数据');
+ }
+
+ $allowed = [
+ 'show_unapproved_comments' => 'bool',
+ 'skip_submit_moderation' => 'bool',
+ 'guest_comment' => 'bool',
+ ];
+
+ $sanitized = $this->sanitize_fields($risk_switches, $allowed);
+
+ // Merge into main settings
+ $all = AKSRT_Settings::get_all();
+ foreach ($sanitized as $key => $val) {
+ $all[$key] = $val;
+ }
+
+ update_option('aksrt_settings', $all);
+
+ return $this->success([
+ 'message' => __('风险设置已保存', 'aksrt'),
+ 'data' => $sanitized,
+ ]);
+ }
+
+ /**
+ * GET /security/audit-log — recent security audit entries.
+ */
+ public function get_audit_log(): WP_REST_Response {
+ global $wpdb;
+
+ $limit = 20;
+ $logs = [];
+
+ // Recent user registrations
+ $recent_users = get_users([
+ 'number' => $limit,
+ 'orderby' => 'registered',
+ 'order' => 'DESC',
+ 'fields' => ['ID', 'display_name', 'user_email', 'user_registered'],
+ ]);
+
+ foreach ($recent_users as $user) {
+ $logs[] = [
+ 'id' => 'reg_' . $user->ID,
+ 'action' => '用户注册',
+ 'detail' => sprintf('%s (%s) 完成注册', $user->display_name, $user->user_email),
+ 'time' => $user->user_registered,
+ 'type' => 'auth',
+ ];
+ }
+
+ // Content audit log
+ $audit_log = get_option('aksrt_content_audit_log', []);
+ foreach (array_slice(array_reverse($audit_log), 0, 10) as $i => $entry) {
+ $logs[] = [
+ 'id' => 'audit_' . $i,
+ 'action' => '内容审核',
+ 'detail' => sprintf('%s — %s', $entry['content'] ?? '', ($entry['passed'] ?? false) ? '通过' : '拦截'),
+ 'time' => $entry['time'] ?? '',
+ 'type' => 'audit',
+ ];
+ }
+
+ // Sort by time descending
+ usort($logs, function ($a, $b) {
+ return strcmp($b['time'] ?? '', $a['time'] ?? '');
+ });
+
+ return $this->success([
+ 'data' => [
+ 'logs' => array_slice($logs, 0, $limit),
+ ],
+ ]);
+ }
+
+ /**
+ * POST /extensions/ip-test — test current IP provider with a given IP.
+ */
+ public function test_ip_geo(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $ip = trim((string) ($params['ip'] ?? ''));
+
+ if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) {
+ return $this->error(__('请输入有效的 IP 地址', 'aksrt'), 422);
+ }
+
+ if (!class_exists('AKSRT_IP_Geo')) {
+ return $this->error(__('IP 归属地服务不可用', 'aksrt'), 500);
+ }
+
+ $result = AKSRT_IP_Geo::get_instance()->lookup($ip);
+
+ return $this->success([
+ 'data' => [
+ 'ip' => $ip,
+ 'result' => $result,
+ ],
+ ]);
+ }
+
+ /**
+ * POST /extensions/audit-test — test text content against current audit provider.
+ */
+ public function test_content_audit(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $text = trim((string) ($params['text'] ?? ''));
+
+ if ($text === '') {
+ return $this->error(__('请输入要测试的文本内容', 'aksrt'), 422);
+ }
+
+ if (!class_exists('AKSRT_Content_Audit')) {
+ return $this->error(__('内容审核服务不可用', 'aksrt'), 500);
+ }
+
+ $result = AKSRT_Content_Audit::get_instance()->audit_text($text);
+
+ return $this->success([
+ 'data' => [
+ 'text' => $text,
+ 'result' => $result,
+ ],
+ ]);
+ }
+
+ /**
+ * 收集所有扩展服务的运行时状态
+ */
+ private function collect_service_statuses(): array {
+ $services = [];
+
+ // SMS
+ if (class_exists('AKSRT_SMS')) {
+ $services['sms'] = AKSRT_SMS::get_instance()->get_provider_status();
+ } else {
+ $services['sms'] = ['enabled' => false, 'provider' => 'none', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
+ }
+
+ // WeChat
+ if (class_exists('AKSRT_WeChat_Notify')) {
+ $services['wechat'] = AKSRT_WeChat_Notify::get_instance()->get_status();
+ } else {
+ $services['wechat'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
+ }
+
+ // Captcha
+ $settings = AKSRT_Settings::get_all();
+ $captcha_en = class_exists('AKSRT_Captcha') && !empty($settings['captcha_enabled']);
+ $services['captcha'] = [
+ 'enabled' => $captcha_en,
+ 'provider' => sanitize_key((string) ($settings['captcha_provider'] ?? '')),
+ 'configured' => $captcha_en,
+ 'available' => $captcha_en,
+ 'verified' => false,
+ 'transport' => 'http_api',
+ 'missing_fields' => [],
+ ];
+
+ // IP Geo
+ if (class_exists('AKSRT_IP_Geo')) {
+ $services['ip_geo'] = AKSRT_IP_Geo::get_instance()->get_status();
+ } else {
+ $services['ip_geo'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
+ }
+
+ // Content Audit
+ if (class_exists('AKSRT_Content_Audit')) {
+ $services['audit'] = AKSRT_Content_Audit::get_instance()->get_status();
+ } else {
+ $services['audit'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
+ }
+
+ // Search Push
+ if (class_exists('AKSRT_Search_Push')) {
+ $services['push'] = AKSRT_Search_Push::get_instance()->get_status();
+ } else {
+ $services['push'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
+ }
+
+ // Docs Mode
+ $docs_en = !empty($settings['docs_mode_enabled']);
+ $services['docs_mode'] = [
+ 'enabled' => $docs_en,
+ 'provider' => 'builtin',
+ 'configured' => $docs_en,
+ 'available' => $docs_en,
+ 'verified' => true,
+ 'transport' => 'builtin',
+ 'missing_fields' => [],
+ ];
+
+ return $services;
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-forum-controller.php b/inc/classes/admin/class-aksrt-admin-forum-controller.php
new file mode 100644
index 00000000..cf36d407
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-forum-controller.php
@@ -0,0 +1,411 @@
+admin = $admin;
+ }
+
+ public function register_routes(): void {
+ register_rest_route(self::API_NAMESPACE, '/forum/overview', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_overview'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/boards', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_boards'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/boards', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_board'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/boards/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_board'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/boards/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'delete_board'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/boards/reorder', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'reorder_boards'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/topics', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_topics'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_topic_detail'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\d+)/moderate', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'moderate_topic'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\d+)/vote', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'vote_topic'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/update-topic', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_topic_legacy'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/moderators', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_moderators'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/moderators', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_moderators'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Change moderator role
+ register_rest_route(self::API_NAMESPACE, '/forum/moderators/role', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'change_moderator_role'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/moderator/apply', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'apply_moderator'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/moderator/applications', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_moderator_applications'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/moderator/applications/(?P[A-Za-z0-9-]+)/review', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'review_moderator_application'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\d+)/favorite', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'toggle_favorite'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\d+)/score', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'score_topic'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\d+)/adopt', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'adopt_answer'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ // §7.0 Plate CPT endpoints
+ register_rest_route(self::API_NAMESPACE, '/forum/plates', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_plates'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/plates', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_plate'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_plate'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_plate'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'delete_plate'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/plates/reorder', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'reorder_plates'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // §7.2 Follow plate
+ register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P\d+)/follow', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'toggle_follow_plate'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ // §7.2 Plate categories (分区)
+ register_rest_route(self::API_NAMESPACE, '/forum/plate-categories', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_plate_categories'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // §7.2 Sorted plates
+ register_rest_route(self::API_NAMESPACE, '/forum/plates/sorted', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_sorted_plates'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // §7.2 Front-end plate CRUD (forum admin/moderator)
+ register_rest_route(self::API_NAMESPACE, '/forum/front/plates', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'frontend_create_plate'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/front/plates/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'frontend_update_plate'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/forum/front/plates/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'frontend_delete_plate'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+ }
+
+ public function get_overview(): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->success(['data' => ['stats' => [], 'boards' => [], 'recent_topics' => [], 'pending_applications' => []]]);
+ }
+
+ $pending_topics = $forum->get_topic_list([
+ 'limit' => 6,
+ 'status' => 'pending',
+ 'review_status' => AKSRT_Forum::REVIEW_PENDING,
+ 'viewer_user_id' => get_current_user_id(),
+ 'include_hidden' => true,
+ ]);
+
+ return $this->success([
+ 'data' => [
+ 'stats' => $forum->get_stats(),
+ 'boards' => array_slice($forum->get_boards(), 0, 6),
+ 'pending_topics' => $pending_topics['items'],
+ 'pending_applications' => array_slice($forum->get_moderator_application_list(['status' => AKSRT_Forum::APPLICATION_PENDING]), 0, 6),
+ ],
+ ]);
+ }
+
+ public function get_boards(): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ $items = $forum ? $forum->get_boards() : [];
+
+ return $this->success([
+ 'data' => [
+ 'items' => $items,
+ 'total' => count($items),
+ ],
+ ]);
+ }
+
+ public function create_board(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $params = $this->get_json_params($request);
+ $name = sanitize_text_field((string) ($params['name'] ?? ''));
+ if ($name === '') {
+ return $this->error(__('版块名称不能为空', 'aksrt'));
+ }
+
+ $term_id = $forum->create_board($params);
+ if ($term_id <= 0) {
+ return $this->error(__('创建版块失败', 'aksrt'));
+ }
+
+ return $this->success([
+ 'message' => __('版块已创建', 'aksrt'),
+ 'data' => ['id' => $term_id],
+ ], 201);
+ }
+
+ public function update_board(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $board_id = (int) $request->get_param('id');
+ if ($board_id <= 0) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ $success = $forum->update_board($board_id, $this->get_json_params($request));
+ if (!$success) {
+ return $this->error(__('版块更新失败', 'aksrt'));
+ }
+
+ return $this->success(['message' => __('版块已更新', 'aksrt')]);
+ }
+
+ public function delete_board(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $board_id = (int) $request->get_param('id');
+ if ($board_id <= 0) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ $success = $forum->delete_board($board_id);
+ if (!$success) {
+ return $this->error(__('版块删除失败', 'aksrt'));
+ }
+
+ return $this->success(['message' => __('版块已删除', 'aksrt')]);
+ }
+
+ public function reorder_boards(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $params = $this->get_json_params($request);
+ $order = $params['order'] ?? [];
+ if (!is_array($order) || empty($order)) {
+ return $this->error(__('排序参数无效', 'aksrt'));
+ }
+
+ $forum->reorder_boards($order);
+
+ return $this->success(['message' => __('排序已更新', 'aksrt')]);
+ }
+
+ // ================================================================
+ // §7.0 Plate CPT handlers
+ // ================================================================
+
+ public function get_plates(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->success(['data' => ['items' => [], 'total' => 0]]);
+ }
+
+ $args = [];
+ $parent = $request->get_param('parent');
+ if ($parent !== null) {
+ $args['parent'] = (int) $parent;
+ }
+ $plate_cat = $request->get_param('plate_cat');
+ if ($plate_cat) {
+ $args['plate_cat'] = (int) $plate_cat;
+ }
+
+ $items = $forum->get_plates($args);
+
+ return $this->success([
+ 'data' => [
+ 'items' => $items,
+ 'total' => count($items),
+ ],
+ ]);
+ }
+
+ public function get_plate(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $plate_id = (int) $request->get_param('id');
+ $plate = $forum->get_plate($plate_id);
+ if (!$plate) {
+ return $this->error(__('版块不存在', 'aksrt'), 404);
+ }
+
+ return $this->success(['data' => $plate]);
+ }
+
+ public function create_plate(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+ $params = $this->get_json_params($request);
+ $name = sanitize_text_field((string) ($params['name'] ?? ''));
+ if ($name === '') {
+ return $this->error(__('版块名称不能为空', 'aksrt'));
+ }
+
+ $plate_id = $forum->create_plate($params);
+ if ($plate_id <= 0) {
+ return $this->error(__('创建版块失败', 'aksrt'));
+ }
+
+ return $this->success([
+ 'message' => __('版块已创建', 'aksrt'),
+ 'data' => ['id' => $plate_id],
+ ], 201);
+ }
+
+}
diff --git a/inc/classes/admin/class-aksrt-admin-global-controller.php b/inc/classes/admin/class-aksrt-admin-global-controller.php
new file mode 100644
index 00000000..a908023d
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-global-controller.php
@@ -0,0 +1,575 @@
+ ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_mail'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/mail/test', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_test_mail'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Footer
+ register_rest_route(self::API_NAMESPACE, '/settings/footer', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_footer_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Footer Tabbar
+ register_rest_route(self::API_NAMESPACE, '/settings/footer-tabbar', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_footer_tabbar'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/settings/footer-tabbar/reorder', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_reorder_footer_tabbar'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Floating Buttons
+ register_rest_route(self::API_NAMESPACE, '/settings/floating-buttons', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_floating_buttons'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/settings/floating-buttons/reorder', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_reorder_floating_buttons'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Friend Links
+ register_rest_route(self::API_NAMESPACE, '/friend-links', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_friend_links'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/friend-links/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'rest_delete_friend_link'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/friend-links/reorder', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_reorder_friend_links'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // CMS Features
+ register_rest_route(self::API_NAMESPACE, '/settings/cms-features', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_cms_features'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Homepage settings
+ register_rest_route(self::API_NAMESPACE, '/settings/homepage', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_homepage_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Category recommend
+ register_rest_route(self::API_NAMESPACE, '/settings/category-recommend', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_category_recommend'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Categories list (for admin selectors)
+ register_rest_route(self::API_NAMESPACE, '/categories', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_get_categories'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Category term meta
+ register_rest_route(self::API_NAMESPACE, '/categories/(?P\d+)/meta', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_update_category_meta'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Homepage tab posts (public)
+ register_rest_route(self::API_NAMESPACE, '/homepage/tab-posts', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_homepage_tab_posts'],
+ 'permission_callback' => '__return_true',
+ 'args' => [
+ 'term_id' => ['type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint'],
+ 'page' => ['type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint'],
+ 'per_page' => ['type' => 'integer', 'default' => 10, 'sanitize_callback' => 'absint'],
+ 'sort' => ['type' => 'string', 'default' => 'modified', 'sanitize_callback' => 'sanitize_text_field'],
+ 'view_mode' => ['type' => 'string', 'default' => 'list', 'sanitize_callback' => 'sanitize_text_field'],
+ 'page_url' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'esc_url_raw'],
+ 'panel' => ['type' => 'string', 'default' => 'latest', 'sanitize_callback' => 'sanitize_key'],
+ 'source' => ['type' => 'string', 'default' => 'home', 'sanitize_callback' => 'sanitize_key'],
+ 'tab_orderby' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key'],
+ ],
+ ]);
+
+ // Archive AJAX filter (public) — §2.5/§2.6/§2.7
+ register_rest_route(self::API_NAMESPACE, '/archive/posts', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_archive_posts'],
+ 'permission_callback' => '__return_true',
+ 'args' => [
+ 'archive_type' => ['type' => 'string', 'default' => 'category', 'sanitize_callback' => 'sanitize_key'],
+ 'term_id' => ['type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint'],
+ 'filter_cat' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
+ 'filter_tag' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
+ 'sort' => ['type' => 'string', 'default' => 'date', 'sanitize_callback' => 'sanitize_key'],
+ 'order' => ['type' => 'string', 'default' => 'DESC', 'sanitize_callback' => 'sanitize_key'],
+ 'page' => ['type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint'],
+ 'per_page' => ['type' => 'integer', 'default' => 10, 'sanitize_callback' => 'absint'],
+ 'custom_filters' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
+ ],
+ ]);
+
+ // Homepage categories (for admin tab editor)
+ register_rest_route(self::API_NAMESPACE, '/homepage/categories', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_homepage_categories'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // User search (for admin user selectors)
+ register_rest_route(self::API_NAMESPACE, '/users/search', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_search_users'],
+ 'permission_callback' => [$this, 'check_permission'],
+ 'args' => [
+ 'search' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
+ 'number' => ['type' => 'integer', 'default' => 20, 'sanitize_callback' => 'absint'],
+ ],
+ ]);
+ }
+
+ public function rest_handle_cms_features(WP_REST_Request $request): WP_REST_Response {
+ if ($request->get_method() === 'GET') {
+ return $this->success(['cms_features' => AKSRT_Settings::get_cms_features()]);
+ }
+
+ $params = $this->get_json_params($request);
+ $allowed = [
+ 'resource_cpt_enabled' => 'bool',
+ 'download_login' => 'bool',
+ 'download_credit' => 'bool',
+ 'download_captcha' => 'bool',
+ 'search_include_resource' => 'bool',
+ ];
+ $sanitized = $this->sanitize_fields($params, $allowed);
+ $settings = AKSRT_Settings::get_all();
+ $settings['cms_features'] = array_merge(
+ is_array($settings['cms_features'] ?? null) ? $settings['cms_features'] : [],
+ $sanitized
+ );
+ update_option('aksrt_settings', $settings);
+ AKSRT_Settings::flush_settings_cache();
+
+ return $this->success(['message' => 'CMS 功能已保存', 'cms_features' => AKSRT_Settings::get_cms_features()]);
+ }
+
+ public function rest_handle_mail(WP_REST_Request $request): WP_REST_Response {
+ $option_key = 'aksrt_mail_config';
+
+ if ($request->get_method() === 'GET') {
+ $config = get_option($option_key, [
+ 'smtp_host' => '',
+ 'smtp_port' => 465,
+ 'smtp_user' => '',
+ 'smtp_pass' => '',
+ 'smtp_secure' => 'ssl',
+ 'from_email' => '',
+ 'from_name' => '',
+ ]);
+ return $this->success(['data' => $config, 'code' => 'success']);
+ }
+
+ $params = $this->get_json_params($request);
+ $current = get_option($option_key, []);
+ $merged = array_merge(is_array($current) ? $current : [], $params);
+ update_option($option_key, $merged);
+
+ return $this->success(['message' => '邮件配置已保存', 'code' => 'success']);
+ }
+
+ public function rest_test_mail(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $to = sanitize_email($params['to'] ?? '');
+ if (!is_email($to)) {
+ return $this->error(__('请输入有效的邮箱地址', 'aksrt'));
+ }
+
+ $sent = wp_mail($to, __('AKSRT 邮件测试', 'aksrt'), __('如果你收到这封邮件,说明 SMTP 配置正确。', 'aksrt'));
+ if ($sent) {
+ return $this->success(['message' => '测试邮件已发送']);
+ }
+ return $this->error(__('邮件发送失败,请检查 SMTP 配置', 'aksrt'));
+ }
+
+ public function rest_homepage_tab_posts(WP_REST_Request $request): WP_REST_Response {
+ $term_id = (int) $request->get_param('term_id');
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = min(50, max(1, (int) $request->get_param('per_page')));
+ $sort = sanitize_key($request->get_param('sort') ?: 'modified');
+ $view_mode = sanitize_key($request->get_param('view_mode') ?: 'list');
+ $tab_orderby = sanitize_key($request->get_param('tab_orderby') ?: '');
+ $panel = sanitize_key($request->get_param('panel') ?: 'latest');
+
+ $post_types = function_exists('aksrt_homepage_get_post_types')
+ ? aksrt_homepage_get_post_types()
+ : ['post'];
+
+ $args = [
+ 'post_type' => $post_types,
+ 'post_status' => 'publish',
+ 'posts_per_page' => $per_page,
+ 'paged' => $page,
+ 'ignore_sticky_posts' => $term_id > 0,
+ ];
+
+ if ($term_id > 0) {
+ $args['cat'] = $term_id;
+ }
+
+ $exclude_ids = class_exists('AKSRT_Settings')
+ ? AKSRT_Settings::home_exclude_post_ids()
+ : [];
+ if (!empty($exclude_ids)) {
+ $args['post__not_in'] = $exclude_ids;
+ }
+
+ if ($panel === 'latest' || $term_id <= 0) {
+ $exclude_cat_ids = class_exists('AKSRT_Settings')
+ ? AKSRT_Settings::home_exclude_category_ids()
+ : [];
+ if (!empty($exclude_cat_ids)) {
+ $args['category__not_in'] = $exclude_cat_ids;
+ }
+ }
+
+ $effective_sort = $tab_orderby !== '' ? $tab_orderby : $sort;
+ if (function_exists('aksrt_homepage_apply_named_sort_args')) {
+ $args = aksrt_homepage_apply_named_sort_args($args, $effective_sort);
+ } else {
+ $args['orderby'] = 'date';
+ $args['order'] = 'DESC';
+ }
+
+ $query = new WP_Query($args);
+
+ $mode = in_array($view_mode, ['card', 'list'], true) ? $view_mode : 'list';
+ $html = '';
+ if (function_exists('aksrt_render_post_list')) {
+ $html = aksrt_render_post_list($query, [
+ 'mode' => $mode,
+ 'context' => 'home',
+ ], false);
+ } elseif ($query->have_posts()) {
+ $html = '';
+ while ($query->have_posts()) {
+ $query->the_post();
+ if (function_exists('aksrt_homepage_render_post_card')) {
+ ob_start();
+ aksrt_homepage_render_post_card();
+ $html .= ob_get_clean();
+ }
+ }
+ $html .= '
';
+ wp_reset_postdata();
+ }
+
+ $has_more = $page < (int) $query->max_num_pages;
+
+ return $this->success([
+ 'html' => $html,
+ 'total' => (int) $query->found_posts,
+ 'has_more' => $has_more,
+ ]);
+ }
+
+ public function rest_archive_posts(WP_REST_Request $request): WP_REST_Response {
+ return $this->rest_homepage_tab_posts($request);
+ }
+
+ public function rest_homepage_categories(): WP_REST_Response {
+ $cats = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']);
+ $data = [];
+ foreach ($cats as $cat) {
+ $data[] = ['id' => $cat->term_id, 'name' => $cat->name, 'slug' => $cat->slug, 'count' => $cat->count, 'parent' => $cat->parent];
+ }
+ return $this->success(['data' => $data]);
+ }
+
+ public function rest_handle_friend_links(WP_REST_Request $request): WP_REST_Response {
+ if ($request->get_method() === 'GET') {
+ $links = aksrt_get_friend_links();
+ return rest_ensure_response(['code' => 'success', 'data' => $links]);
+ }
+
+ $params = $this->get_json_params($request);
+ $links = get_option('aksrt_friend_links', []);
+ $new_link = [
+ 'id' => uniqid('fl_'),
+ 'name' => sanitize_text_field($params['name'] ?? ''),
+ 'url' => esc_url_raw($params['url'] ?? ''),
+ 'logo' => esc_url_raw($params['logo'] ?? ''),
+ 'description' => sanitize_textarea_field($params['description'] ?? ''),
+ 'sort_order' => (int) ($params['sort_order'] ?? 0),
+ 'active' => 1,
+ ];
+ $links[] = $new_link;
+ usort($links, function ($a, $b) {
+ return ($a['sort_order'] ?? 0) <=> ($b['sort_order'] ?? 0);
+ });
+ update_option('aksrt_friend_links', $links);
+
+ return rest_ensure_response(['code' => 'success', 'data' => $new_link, 'message' => __('Friend link added.', 'aksrt')]);
+ }
+
+ public function rest_delete_friend_link(WP_REST_Request $request): WP_REST_Response {
+ $id = $request->get_param('id');
+ $links = get_option('aksrt_friend_links', []);
+ $links = array_filter($links, function ($link) use ($id) {
+ return ($link['id'] ?? '') !== $id;
+ });
+ $links = array_values($links);
+ update_option('aksrt_friend_links', $links);
+ return rest_ensure_response(['code' => 'success', 'message' => __('Friend link deleted.', 'aksrt')]);
+ }
+
+ public function rest_reorder_friend_links(WP_REST_Request $request): WP_REST_Response {
+ $ordered_ids = $request->get_param('ids');
+ if (!is_array($ordered_ids)) {
+ return new WP_REST_Response(['code' => 'error', 'message' => __('参数错误', 'aksrt')], 400);
+ }
+ $links = get_option('aksrt_friend_links', []);
+ $order_map = array_flip($ordered_ids);
+ usort($links, function ($a, $b) use ($order_map) {
+ $idx_a = $order_map[($a['id'] ?? '')] ?? 999;
+ $idx_b = $order_map[($b['id'] ?? '')] ?? 999;
+ return $idx_a <=> $idx_b;
+ });
+ foreach ($links as $index => &$link) {
+ $link['sort_order'] = $index;
+ }
+ update_option('aksrt_friend_links', $links);
+ return rest_ensure_response(['code' => 'success', 'message' => __('Order saved.', 'aksrt')]);
+ }
+
+ public function rest_handle_footer_settings(WP_REST_Request $request): WP_REST_Response {
+ if ($request->get_method() === 'GET') {
+ $footer = get_option('aksrt_footer_settings', []);
+ $defaults = [
+ 'contacts' => [],
+ 'quick_links' => [],
+ 'more_links' => [],
+ 'hot_categories' => 5,
+ 'copyright_text' => '',
+ 'layout_template' => 'default',
+ 'footer_t1_m_s' => ['pc', 'mobile'],
+ 'footer_t1_img' => '',
+ 'footer_t1_img_dark' => '',
+ 'footer_t1_t' => '',
+ 'fcode_t1_code' => '',
+ 'footer_contact_m_s' => ['pc', 'mobile'],
+ 'footer_contact_wechat_img' => '',
+ 'footer_contact_qq' => '',
+ 'footer_contact_weibo' => '',
+ 'footer_contact_email' => '',
+ 'footer_contact_more' => '',
+ 'fcode_t2_code_1' => '',
+ 'fcode_t2_code_2' => '',
+ 'footer_mini_img_m_s' => ['pc', 'mobile'],
+ 'footer_mini_img' => [],
+ 'fcode_customize_code' => '',
+ ];
+ $data = array_merge($defaults, $footer);
+ return rest_ensure_response(['code' => 'success', 'data' => $data]);
+ }
+
+ $params = $this->get_json_params($request);
+ $footer = [
+ 'contacts' => isset($params['contacts']) ? $this->sanitize_contacts($params['contacts']) : [],
+ 'quick_links' => isset($params['quick_links']) ? $this->sanitize_link_list($params['quick_links']) : [],
+ 'more_links' => isset($params['more_links']) ? $this->sanitize_link_list($params['more_links']) : [],
+ 'hot_categories' => (int) ($params['hot_categories'] ?? 5),
+ 'copyright_text' => sanitize_textarea_field($params['copyright_text'] ?? ''),
+ 'layout_template' => in_array($params['layout_template'] ?? '', ['default', 'compact', 'centered'], true)
+ ? $params['layout_template'] : 'default',
+ 'footer_t1_m_s' => $this->sanitize_device_visibility($params['footer_t1_m_s'] ?? ['pc', 'mobile']),
+ 'footer_t1_img' => esc_url_raw($params['footer_t1_img'] ?? ''),
+ 'footer_t1_img_dark' => esc_url_raw($params['footer_t1_img_dark'] ?? ''),
+ 'footer_t1_t' => sanitize_text_field($params['footer_t1_t'] ?? ''),
+ 'fcode_t1_code' => wp_kses_post($params['fcode_t1_code'] ?? ''),
+ 'footer_contact_m_s' => $this->sanitize_device_visibility($params['footer_contact_m_s'] ?? ['pc', 'mobile']),
+ 'footer_contact_wechat_img' => esc_url_raw($params['footer_contact_wechat_img'] ?? ''),
+ 'footer_contact_qq' => sanitize_text_field($params['footer_contact_qq'] ?? ''),
+ 'footer_contact_weibo' => sanitize_text_field($params['footer_contact_weibo'] ?? ''),
+ 'footer_contact_email' => sanitize_email($params['footer_contact_email'] ?? ''),
+ 'footer_contact_more' => isset($params['footer_contact_more']) && is_array($params['footer_contact_more'])
+ ? $this->sanitize_contact_more($params['footer_contact_more']) : [],
+ 'fcode_t2_code_1' => wp_kses_post($params['fcode_t2_code_1'] ?? ''),
+ 'fcode_t2_code_2' => wp_kses_post($params['fcode_t2_code_2'] ?? ''),
+ 'footer_mini_img_m_s' => $this->sanitize_device_visibility($params['footer_mini_img_m_s'] ?? ['pc', 'mobile']),
+ 'footer_mini_img' => isset($params['footer_mini_img']) && is_array($params['footer_mini_img'])
+ ? $this->sanitize_mini_img_group($params['footer_mini_img']) : [],
+ 'fcode_customize_code' => wp_kses_post($params['fcode_customize_code'] ?? ''),
+ ];
+ update_option('aksrt_footer_settings', $footer);
+ return rest_ensure_response(['code' => 'success', 'message' => __('Footer settings saved.', 'aksrt')]);
+ }
+
+ private function sanitize_contacts(array $contacts): array {
+ $allowed_icons = ['github', 'x', 'twitter', 'email', 'wechat', 'qq', 'weibo',
+ 'telegram', 'discord', 'bilibili', 'linkedin', 'facebook',
+ 'youtube', 'instagram', 'rss', 'web', 'globe'];
+ $clean = [];
+ foreach ($contacts as $item) {
+ $icon = sanitize_text_field($item['icon'] ?? '');
+ if (!in_array($icon, $allowed_icons)) {
+ continue;
+ }
+ $clean[] = [
+ 'icon' => $icon,
+ 'url' => esc_url_raw($item['url'] ?? ''),
+ 'title' => sanitize_text_field($item['title'] ?? ''),
+ 'enabled' => !empty($item['enabled']),
+ ];
+ }
+ return $clean;
+ }
+
+ private function sanitize_link_list(array $links): array {
+ $clean = [];
+ foreach ($links as $item) {
+ $clean[] = [
+ 'id' => isset($item['id']) ? sanitize_text_field($item['id']) : uniqid('link_'),
+ 'name' => sanitize_text_field($item['name'] ?? ''),
+ 'url' => esc_url_raw($item['url'] ?? '#'),
+ 'enabled' => !empty($item['enabled']),
+ 'sort_order' => (int) ($item['sort_order'] ?? 0),
+ ];
+ }
+ return $clean;
+ }
+
+ private function sanitize_device_visibility($value): array {
+ $arr = is_array($value) ? $value : [$value];
+ $allowed = ['pc', 'mobile'];
+ $clean = array_values(array_intersect(array_map('sanitize_key', $arr), $allowed));
+ return $clean ?: ['pc', 'mobile'];
+ }
+
+ private function sanitize_contact_more(array $items): array {
+ $clean = [];
+ foreach ($items as $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+ $clean[] = [
+ 'desc' => sanitize_text_field($item['desc'] ?? ''),
+ 'icon' => sanitize_text_field($item['icon'] ?? ''),
+ 'link' => esc_url_raw($item['link'] ?? ''),
+ 'img' => esc_url_raw($item['img'] ?? ''),
+ ];
+ }
+ return $clean;
+ }
+
+ private function sanitize_mini_img_group(array $items): array {
+ $clean = [];
+ foreach ($items as $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+ if (count($clean) >= 4) {
+ break;
+ }
+ $clean[] = [
+ 'text' => sanitize_text_field($item['text'] ?? ''),
+ 'img' => esc_url_raw($item['img'] ?? ''),
+ ];
+ }
+ return $clean;
+ }
+
+ public function rest_handle_footer_tabbar(WP_REST_Request $request): WP_REST_Response {
+ if ($request->get_method() === 'GET') {
+ $settings = class_exists('AKSRT_Settings')
+ ? AKSRT_Settings::get_footer_tabbar_settings()
+ : ['enabled' => true, 'scroll_hide' => false, 'scroll_down_hide' => true, 'items' => []];
+ return rest_ensure_response(['code' => 'success', 'data' => $settings]);
+ }
+
+ $params = $this->get_json_params($request);
+ $items = isset($params['items']) ? array_map([$this, 'sanitize_tabbar_item'], (array) $params['items']) : [];
+
+ $max_items = max(3, min(7, (int) ($params['max_items'] ?? 5)));
+ if (count($items) > $max_items) {
+ return new WP_REST_Response([
+ 'code' => 'error',
+ 'message' => sprintf(__('Footer tabbar supports at most %d items.', 'aksrt'), $max_items),
+ ], 400);
+ }
+
+ $tabbar = [
+ 'enabled' => !empty($params['enabled']),
+ 'scroll_hide' => !empty($params['scroll_hide']),
+ 'scroll_down_hide' => !empty($params['scroll_down_hide']),
+ 'max_items' => $max_items,
+ 'icon_size' => max(16, min(32, (int) ($params['icon_size'] ?? 22))),
+ 'text_size' => max(8, min(14, (int) ($params['text_size'] ?? 10))),
+ 'show_text' => !empty($params['show_text']),
+ 'active_color' => sanitize_hex_color($params['active_color'] ?? '') ?: '#f04494',
+ 'bg_color' => sanitize_hex_color($params['bg_color'] ?? '') ?: '',
+ 'border' => !empty($params['border']),
+ 'shadow' => !empty($params['shadow']),
+ 'animation' => !empty($params['animation']),
+ 'double_click_home' => !empty($params['double_click_home']),
+ 'badge_dot' => !empty($params['badge_dot']),
+ 'items' => $items,
+ ];
+ update_option('aksrt_footer_tabbar', $tabbar);
+
+ if (class_exists('AKSRT_Settings')) {
+ AKSRT_Settings::flush_settings_cache();
+ }
+
+ return rest_ensure_response(['code' => 'success', 'message' => __('Footer tabbar settings saved.', 'aksrt')]);
+ }
+
+}
diff --git a/inc/classes/admin/class-aksrt-admin-import-export-controller.php b/inc/classes/admin/class-aksrt-admin-import-export-controller.php
new file mode 100644
index 00000000..c8594592
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-import-export-controller.php
@@ -0,0 +1,408 @@
+ 'GET',
+ 'callback' => [$this, 'rest_export_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Import settings
+ register_rest_route(self::API_NAMESPACE, '/settings/import', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_import_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Reset a domain to defaults
+ register_rest_route(self::API_NAMESPACE, '/settings/reset/(?P[a-z_]+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_reset_domain'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Reset all schema-managed settings to defaults
+ register_rest_route(self::API_NAMESPACE, '/settings/reset-all', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_reset_all'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Health check / diagnostics
+ register_rest_route(self::API_NAMESPACE, '/settings/health', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_health_check'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Migration status
+ register_rest_route(self::API_NAMESPACE, '/settings/migration-status', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_migration_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Schema introspection (for frontend to discover available fields)
+ register_rest_route(self::API_NAMESPACE, '/settings/schema/(?P[a-z_]+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_get_schema'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/settings/schema/(?P[a-z_]+)/(?P[a-z0-9_-]+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_get_schema_section'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/settings/schema/(?P[a-z_]+)/(?P[a-z0-9_-]+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_save_schema_section'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Backup snapshots
+ register_rest_route(self::API_NAMESPACE, '/settings/backups', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'list_setting_backups'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/settings/backups', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_setting_backup'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/settings/backups/(?P\d+)/restore', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'restore_setting_backup'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/settings/backups/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'delete_setting_backup'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // ── Widget Template Management ──
+
+ // Export current widget configuration
+ register_rest_route(self::API_NAMESPACE, '/widgets/export', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'export_widgets'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Import widget configuration
+ register_rest_route(self::API_NAMESPACE, '/widgets/import', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'import_widgets'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // List saved widget templates
+ register_rest_route(self::API_NAMESPACE, '/widgets/templates', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'list_widget_templates'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Save current layout as a named template
+ register_rest_route(self::API_NAMESPACE, '/widgets/templates', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'save_widget_template'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Apply a saved template
+ register_rest_route(self::API_NAMESPACE, '/widgets/templates/(?P\d+)/apply', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'apply_widget_template'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Delete a saved template
+ register_rest_route(self::API_NAMESPACE, '/widgets/templates/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'delete_widget_template'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Handlers */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * Export all AKSRT settings as a JSON-serializable array.
+ */
+ public function rest_export_settings(WP_REST_Request $request): WP_REST_Response {
+ $schema_provider = AKSRT_Admin_Schema_Provider::get_instance();
+ $domains = $schema_provider->get_domain_keys();
+
+ $export = [
+ 'version' => AKSRT_VERSION ?? '3.4.0',
+ 'exported' => gmdate('Y-m-d H:i:s'),
+ 'domains' => [],
+ ];
+
+ foreach ($domains as $domain) {
+ $export['domains'][$domain] = [
+ 'option_key' => $schema_provider->get_option_key($domain),
+ 'data' => $schema_provider->read_settings($domain),
+ ];
+ }
+
+ // Also export standalone options from the registry
+ $standalone_keys = AKSRT_Settings::STANDALONE_OPTION_KEYS;
+
+ $export['standalone'] = [];
+ foreach ($standalone_keys as $key) {
+ $val = get_option($key);
+ if ($val !== false) {
+ $export['standalone'][$key] = $val;
+ }
+ }
+
+ return $this->success(['data' => $export]);
+ }
+
+ /**
+ * Import settings from a JSON payload.
+ *
+ * Request body: { domains: { domain: { data: {...} } }, standalone: { key: value } }
+ * Optional: { dry_run: true } to validate without saving.
+ */
+ public function rest_import_settings(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $dry_run = !empty($params['dry_run']);
+
+ if (empty($params['domains']) && empty($params['standalone'])) {
+ return $this->error('没有可导入的数据');
+ }
+
+ $schema_provider = AKSRT_Admin_Schema_Provider::get_instance();
+ $results = [];
+
+ // Import domain settings
+ if (!$dry_run) {
+ $backup = $this->create_backup_record('manual', __('导入前自动快照', 'aksrt'));
+ $this->persist_setting_backup($backup);
+ }
+
+ // Import domain settings
+ if (!empty($params['domains']) && is_array($params['domains'])) {
+ foreach ($params['domains'] as $domain => $payload) {
+ $data = $payload['data'] ?? $payload;
+ if (!is_array($data)) {
+ $results[$domain] = ['status' => 'error', 'message' => 'Invalid data format'];
+ continue;
+ }
+
+ // Validate
+ $validation = $schema_provider->validate($domain, $data);
+ if (!$validation['valid']) {
+ $results[$domain] = ['status' => 'error', 'errors' => $validation['errors']];
+ continue;
+ }
+
+ if (!$dry_run) {
+ $schema_provider->write_settings($domain, $data);
+ $results[$domain] = ['status' => 'imported'];
+ } else {
+ $results[$domain] = ['status' => 'valid'];
+ }
+ }
+ }
+
+ // Import standalone options
+ if (!empty($params['standalone']) && is_array($params['standalone'])) {
+ foreach ($params['standalone'] as $key => $value) {
+ if (!preg_match('/^aksrt_/', $key)) {
+ $results[$key] = ['status' => 'skipped', 'message' => 'Not an AKSRT option'];
+ continue;
+ }
+ if (!$dry_run) {
+ update_option($key, $value);
+ $results[$key] = ['status' => 'imported'];
+ } else {
+ $results[$key] = ['status' => 'valid'];
+ }
+ }
+ }
+
+ if (!$dry_run) {
+ AKSRT_Settings::flush_settings_cache();
+ }
+
+ return $this->success([
+ 'dry_run' => $dry_run,
+ 'results' => $results,
+ ]);
+ }
+
+ /**
+ * Reset a domain's settings to defaults.
+ */
+ public function rest_reset_domain(WP_REST_Request $request): WP_REST_Response {
+ $domain = $request->get_param('domain');
+ $schema_provider = AKSRT_Admin_Schema_Provider::get_instance();
+
+ if (!in_array($domain, $schema_provider->get_domain_keys(), true)) {
+ return $this->error('未知的设置域: ' . esc_html($domain), 404);
+ }
+
+ $backup = $this->create_backup_record('reset_domain', sprintf(__('重置 %s 前快照', 'aksrt'), $domain));
+ $this->persist_setting_backup($backup);
+
+ $schema_provider->reset_to_defaults($domain);
+
+ return $this->success([
+ 'message' => sprintf('%s 域设置已重置为默认值', $domain),
+ 'data' => [
+ 'backup_id' => $backup['id'],
+ ],
+ ]);
+ }
+
+ public function rest_reset_all(WP_REST_Request $request): WP_REST_Response {
+ $schema_provider = AKSRT_Admin_Schema_Provider::get_instance();
+ $backup = $this->create_backup_record('reset_all', __('重置全部前快照', 'aksrt'));
+ $this->persist_setting_backup($backup);
+
+ foreach ($schema_provider->get_domain_keys() as $domain) {
+ $schema_provider->reset_to_defaults($domain);
+ }
+
+ AKSRT_Settings::flush_settings_cache();
+
+ return $this->success([
+ 'message' => __('全部 schema 托管设置已重置为默认值', 'aksrt'),
+ 'data' => [
+ 'backup_id' => $backup['id'],
+ ],
+ ]);
+ }
+
+ /**
+ * Health check — returns system diagnostics.
+ */
+ public function rest_health_check(WP_REST_Request $request): WP_REST_Response {
+ global $wpdb;
+
+ $checks = [];
+
+ // PHP version
+ $checks['php_version'] = [
+ 'label' => 'PHP 版本',
+ 'value' => PHP_VERSION,
+ 'status' => version_compare(PHP_VERSION, '8.1', '>=') ? 'ok' : 'warning',
+ ];
+
+ // WordPress version
+ $wp_version = get_bloginfo('version');
+ $checks['wp_version'] = [
+ 'label' => 'WordPress 版本',
+ 'value' => $wp_version,
+ 'status' => version_compare($wp_version, '6.4', '>=') ? 'ok' : 'warning',
+ ];
+
+ // Theme version
+ $theme = wp_get_theme();
+ $checks['theme_version'] = [
+ 'label' => '主题版本',
+ 'value' => $theme->get('Version') ?? 'unknown',
+ 'status' => 'ok',
+ ];
+
+ // Database tables
+ $expected_tables = [
+ $wpdb->prefix . 'aksrt_credit_log',
+ $wpdb->prefix . 'aksrt_signin_log',
+ $wpdb->prefix . 'aksrt_invite',
+ $wpdb->prefix . 'aksrt_dl_token',
+ $wpdb->prefix . 'aksrt_notification',
+ ];
+ $missing_tables = [];
+ foreach ($expected_tables as $table) {
+ if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) !== $table) {
+ $missing_tables[] = str_replace($wpdb->prefix, '', $table);
+ }
+ }
+ $checks['db_tables'] = [
+ 'label' => '数据表',
+ 'value' => empty($missing_tables) ? '全部就绪' : '缺少: ' . implode(', ', $missing_tables),
+ 'status' => empty($missing_tables) ? 'ok' : 'error',
+ ];
+
+ // Email verification backfill
+ $backfill_done = get_option('aksrt_email_verification_backfill_done', false);
+ $checks['email_backfill'] = [
+ 'label' => '邮箱验证迁移',
+ 'value' => $backfill_done ? '已完成' : '未完成',
+ 'status' => $backfill_done ? 'ok' : 'info',
+ ];
+
+ // Avatar source
+ $avatar_source = AKSRT_Settings::get_all()['avatar_source'] ?? 'cn.cravatar.com';
+ $checks['avatar_source'] = [
+ 'label' => '头像服务',
+ 'value' => $avatar_source,
+ 'status' => $avatar_source === 'cn.cravatar.com' ? 'ok' : 'info',
+ ];
+
+ // Option count
+ $option_count = (int) $wpdb->get_var(
+ "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'aksrt_%'"
+ );
+ $checks['option_count'] = [
+ 'label' => 'AKSRT 选项数',
+ 'value' => $option_count,
+ 'status' => $option_count < 100 ? 'ok' : 'warning',
+ ];
+
+ return $this->success(['checks' => $checks]);
+ }
+
+ /**
+ * Migration status — reports schema version and pending migrations.
+ */
+}
diff --git a/inc/classes/admin/class-aksrt-admin-message-controller.php b/inc/classes/admin/class-aksrt-admin-message-controller.php
new file mode 100644
index 00000000..a87b6a1f
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-message-controller.php
@@ -0,0 +1,254 @@
+ 'GET',
+ 'callback' => [$this, 'list_notifications'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ 'args' => [
+ 'type' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
+ 'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
+ 'per_page' => ['default' => 20, 'sanitize_callback' => 'absint'],
+ ],
+ ]);
+
+ // Unread count
+ register_rest_route(self::API_NAMESPACE, '/messages/unread', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_unread_count'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ // Mark one notification read
+ register_rest_route(self::API_NAMESPACE, '/messages/(?P\d+)/read', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'mark_read'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ // Mark all notifications read
+ register_rest_route(self::API_NAMESPACE, '/messages/read-all', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'mark_all_read'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ // ── Private Messages / Conversations ──
+
+ // List conversations
+ register_rest_route(self::API_NAMESPACE, '/conversations', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'list_conversations'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ 'args' => [
+ 'limit' => ['default' => 20, 'sanitize_callback' => 'absint'],
+ ],
+ ]);
+
+ // Get thread messages
+ register_rest_route(self::API_NAMESPACE, '/conversations/(?P\d+)/messages', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_thread'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ 'args' => [
+ 'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
+ 'per_page' => ['default' => 30, 'sanitize_callback' => 'absint'],
+ ],
+ ]);
+
+ // Send message in conversation
+ register_rest_route(self::API_NAMESPACE, '/conversations/(?P\d+)/messages', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'send_message'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ 'args' => [
+ 'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'],
+ ],
+ ]);
+
+ // Create or get conversation with a user
+ register_rest_route(self::API_NAMESPACE, '/conversations/with/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'get_or_create_conversation'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ // Mark conversation read
+ register_rest_route(self::API_NAMESPACE, '/conversations/(?P\d+)/read', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'mark_conversation_read'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+ }
+
+ // ── Notification handlers ──
+
+ public function list_notifications(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ $type = $request->get_param('type');
+ $per_page = min(100, (int) $request->get_param('per_page'));
+ $page = max(1, (int) $request->get_param('page'));
+
+ if (!class_exists('AKSRT_Message')) {
+ return $this->success(['data' => [], 'total' => 0]);
+ }
+
+ $msg = AKSRT_Message::get_instance();
+ $args = [
+ 'limit' => $per_page,
+ 'offset' => ($page - 1) * $per_page,
+ ];
+ if ($type) {
+ $args['type'] = $type;
+ }
+
+ $notifications = $msg->get_notifications($user_id, $args);
+ $unread = $msg->get_unread_count($user_id);
+
+ return $this->success([
+ 'data' => $notifications,
+ 'unread' => $unread,
+ ]);
+ }
+
+ public function get_unread_count(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ if (!class_exists('AKSRT_Message')) {
+ return $this->success(['data' => ['unread' => 0]]);
+ }
+ $count = AKSRT_Message::get_instance()->get_unread_count($user_id);
+ return $this->success(['data' => ['unread' => $count]]);
+ }
+
+ public function mark_read(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ $id = (string) $request->get_param('id');
+ if (!class_exists('AKSRT_Message')) {
+ return $this->error('not_available', '消息系统未启用。', 501);
+ }
+ $result = AKSRT_Message::get_instance()->mark_read($user_id, $id);
+ return $this->success(['data' => ['success' => $result]]);
+ }
+
+ public function mark_all_read(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ if (!class_exists('AKSRT_Message')) {
+ return $this->error('not_available', '消息系统未启用。', 501);
+ }
+ $result = AKSRT_Message::get_instance()->mark_all_read($user_id);
+ return $this->success(['data' => ['success' => $result]]);
+ }
+
+ // ── Conversation handlers ──
+
+ public function list_conversations(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ $limit = min(50, (int) $request->get_param('limit'));
+
+ if (!class_exists('AKSRT_Private_Message')) {
+ return $this->success(['data' => []]);
+ }
+
+ $conversations = AKSRT_Private_Message::get_instance()->get_conversations($user_id, $limit);
+ return $this->success(['data' => $conversations]);
+ }
+
+ public function get_thread(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ $conv_id = (int) $request->get_param('id');
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = min(50, (int) $request->get_param('per_page'));
+
+ if (!class_exists('AKSRT_Private_Message')) {
+ return $this->error('not_available', '私信系统未启用。', 501);
+ }
+
+ $result = AKSRT_Private_Message::get_instance()->get_thread($conv_id, $user_id, $page, $per_page);
+ if (is_wp_error($result)) {
+ return $this->error('thread_error', $result->get_error_message(), 403);
+ }
+
+ return $this->success(['data' => $result]);
+ }
+
+ public function send_message(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ $conv_id = (int) $request->get_param('id');
+ $content = trim((string) $request->get_param('content'));
+
+ if (!$content) {
+ return $this->error('content_required', '消息内容不能为空。', 400);
+ }
+
+ if (!class_exists('AKSRT_Private_Message')) {
+ return $this->error('not_available', '私信系统未启用。', 501);
+ }
+
+ $result = AKSRT_Private_Message::get_instance()->send($conv_id, $user_id, $content);
+ if (!$result['success']) {
+ return $this->error('send_failed', $result['message'] ?? '发送失败。', 500);
+ }
+
+ return $this->success(['data' => $result]);
+ }
+
+ public function get_or_create_conversation(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ $other_id = (int) $request->get_param('user_id');
+
+ if ($user_id === $other_id) {
+ return $this->error('self_message', '不能与自己创建对话。', 400);
+ }
+
+ if (!class_exists('AKSRT_Private_Message')) {
+ return $this->error('not_available', '私信系统未启用。', 501);
+ }
+
+ $conv_id = AKSRT_Private_Message::get_instance()->get_or_create_conversation($user_id, $other_id);
+ if (is_wp_error($conv_id)) {
+ return $this->error('create_failed', $conv_id->get_error_message(), 500);
+ }
+
+ return $this->success(['data' => ['conversation_id' => $conv_id]]);
+ }
+
+ public function mark_conversation_read(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ $conv_id = (int) $request->get_param('id');
+
+ if (!class_exists('AKSRT_Private_Message')) {
+ return $this->error('not_available', '私信系统未启用。', 501);
+ }
+
+ AKSRT_Private_Message::get_instance()->mark_conversation_read($conv_id, $user_id);
+ return $this->success(['data' => ['success' => true]]);
+ }
+
+ public function check_logged_in_permission(): bool {
+ return is_user_logged_in();
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-page-display-controller.php b/inc/classes/admin/class-aksrt-admin-page-display-controller.php
new file mode 100644
index 00000000..93031211
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-page-display-controller.php
@@ -0,0 +1,201 @@
+admin = $admin;
+ }
+
+ public function register_routes(): void {
+ // Banners (generic CRUD — hero, banner kinds)
+ register_rest_route(self::API_NAMESPACE, '/banners', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_get_banners'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/banners', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_create_banner'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/banners/(?P[\w-]+)', [
+ 'methods' => ['PUT', 'DELETE'],
+ 'callback' => [$this, 'rest_handle_banner_item'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+ private function normalize_banner_kind($kind): string {
+ return in_array($kind, ['hero', 'banner'], true) ? $kind : 'hero';
+ }
+
+ private function get_banner_kind_query(string $kind): array {
+ if ('banner' === $kind) {
+ return [
+ [
+ 'key' => '_aksrt_kind',
+ 'value' => 'banner',
+ ],
+ ];
+ }
+
+ return [
+ 'relation' => 'OR',
+ [
+ 'key' => '_aksrt_kind',
+ 'value' => 'hero',
+ ],
+ [
+ 'key' => '_aksrt_kind',
+ 'compare' => 'NOT EXISTS',
+ ],
+ ];
+ }
+
+ public function rest_get_banners(WP_REST_Request $request): WP_REST_Response {
+ $kind = $this->normalize_banner_kind($request->get_param('kind') ?: 'hero');
+ $args = [
+ 'post_type' => 'aksrt_banner',
+ 'posts_per_page' => 100,
+ 'post_status' => 'any',
+ 'orderby' => 'menu_order',
+ 'order' => 'ASC',
+ 'meta_query' => $this->get_banner_kind_query($kind),
+ ];
+ $posts = get_posts($args);
+ $banners = array_map(function ($post) {
+ $layers_json = get_post_meta($post->ID, '_layers', true);
+ return [
+ 'id' => $post->ID,
+ 'title' => $post->post_title,
+ 'subtitle' => get_post_meta($post->ID, '_subtitle', true),
+ 'description' => get_post_meta($post->ID, '_description', true),
+ 'image_url' => get_post_meta($post->ID, '_image_url', true),
+ 'video_url' => get_post_meta($post->ID, '_video_url', true),
+ 'link_url' => get_post_meta($post->ID, '_link_url', true),
+ 'link_target' => get_post_meta($post->ID, '_link_target', true) ?: '_self',
+ 'text_align' => get_post_meta($post->ID, '_text_align', true) ?: 'left',
+ 'device_visibility' => get_post_meta($post->ID, '_device_visibility', true) ?: 'all',
+ 'enabled' => get_post_meta($post->ID, '_enabled', true) === '1',
+ 'show_text' => get_post_meta($post->ID, '_show_text', true) !== '0',
+ 'kind' => get_post_meta($post->ID, '_aksrt_kind', true) ?: 'hero',
+ 'sort_order' => $post->menu_order,
+ 'text_size_pc' => get_post_meta($post->ID, '_text_size_pc', true) ?: '',
+ 'text_size_m' => get_post_meta($post->ID, '_text_size_m', true) ?: '',
+ 'text_parallax' => get_post_meta($post->ID, '_text_parallax', true) === '1',
+ 'layers' => is_string($layers_json) && $layers_json !== '' ? json_decode($layers_json, true) : [],
+ ];
+ }, $posts);
+ return rest_ensure_response(['code' => 'success', 'data' => $banners]);
+ }
+
+ public function rest_create_banner(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $kind = $this->normalize_banner_kind($params['kind'] ?? 'hero');
+ $title = sanitize_text_field($params['title'] ?? '');
+ $post_id = wp_insert_post([
+ 'post_title' => '' !== $title ? $title : ('banner' === $kind ? 'Banner Slide' : 'Hero Slide'),
+ 'post_type' => 'aksrt_banner',
+ 'post_status' => 'publish',
+ 'menu_order' => (int) ($params['sort_order'] ?? 0),
+ ]);
+
+ if (is_wp_error($post_id)) {
+ return new WP_REST_Response(['code' => 'error', 'message' => $post_id->get_error_message()], 400);
+ }
+
+ update_post_meta($post_id, '_image_url', esc_url_raw($params['image_url'] ?? ''));
+ update_post_meta($post_id, '_link_url', esc_url_raw($params['link_url'] ?? ''));
+ update_post_meta($post_id, '_link_target', in_array($params['link_target'] ?? '', ['_self', '_blank'], true) ? $params['link_target'] : '_self');
+ update_post_meta($post_id, '_subtitle', sanitize_text_field($params['subtitle'] ?? ''));
+ update_post_meta($post_id, '_description', sanitize_textarea_field($params['description'] ?? ''));
+ update_post_meta($post_id, '_video_url', esc_url_raw($params['video_url'] ?? ''));
+ update_post_meta($post_id, '_text_align', in_array($params['text_align'] ?? '', ['left', 'center', 'right'], true) ? $params['text_align'] : 'left');
+ update_post_meta($post_id, '_device_visibility', in_array($params['device_visibility'] ?? '', ['all', 'pc', 'mobile'], true) ? $params['device_visibility'] : 'all');
+ update_post_meta($post_id, '_enabled', !array_key_exists('enabled', $params) || !empty($params['enabled']) ? '1' : '0');
+ update_post_meta($post_id, '_show_text', !array_key_exists('show_text', $params) || !empty($params['show_text']) ? '1' : '0');
+ update_post_meta($post_id, '_aksrt_kind', $kind);
+ update_post_meta($post_id, '_text_size_pc', sanitize_text_field($params['text_size_pc'] ?? ''));
+ update_post_meta($post_id, '_text_size_m', sanitize_text_field($params['text_size_m'] ?? ''));
+ update_post_meta($post_id, '_text_parallax', !empty($params['text_parallax']) ? '1' : '0');
+ if (isset($params['layers']) && is_array($params['layers'])) {
+ update_post_meta($post_id, '_layers', wp_json_encode($params['layers']));
+ }
+
+ return rest_ensure_response(['code' => 'success', 'data' => ['id' => $post_id], 'message' => __('幻灯片已创建', 'aksrt')]);
+ }
+
+ public function rest_handle_banner_item(WP_REST_Request $request): WP_REST_Response {
+ $id = absint($request['id']);
+ if (!$id || get_post_type($id) !== 'aksrt_banner') {
+ return new WP_REST_Response(['code' => 'not_found', 'message' => __('幻灯片不存在', 'aksrt')], 404);
+ }
+
+ if ($request->get_method() === 'DELETE') {
+ wp_delete_post($id, true);
+ return rest_ensure_response(['code' => 'success', 'message' => __('幻灯片已删除', 'aksrt')]);
+ }
+
+ $params = $this->get_json_params($request);
+ $kind = $this->normalize_banner_kind($params['kind'] ?? get_post_meta($id, '_aksrt_kind', true) ?: 'hero');
+ $title = sanitize_text_field($params['title'] ?? '');
+ if ($title === '') {
+ return new WP_REST_Response(['code' => 'validation_error', 'message' => __('标题不能为空', 'aksrt')], 422);
+ }
+
+ wp_update_post([
+ 'ID' => $id,
+ 'post_title' => $title,
+ ]);
+
+ update_post_meta($id, '_aksrt_kind', $kind);
+ update_post_meta($id, '_subtitle', sanitize_text_field($params['subtitle'] ?? ''));
+ update_post_meta($id, '_description', sanitize_textarea_field($params['description'] ?? ''));
+ update_post_meta($id, '_image_url', esc_url_raw($params['image_url'] ?? ''));
+ update_post_meta($id, '_video_url', esc_url_raw($params['video_url'] ?? ''));
+ update_post_meta($id, '_link_url', esc_url_raw($params['link_url'] ?? ''));
+ update_post_meta($id, '_link_target', in_array($params['link_target'] ?? '', ['_blank', '_self'], true) ? $params['link_target'] : '_self');
+ update_post_meta($id, '_text_align', in_array($params['text_align'] ?? '', ['left', 'center', 'right'], true) ? $params['text_align'] : 'left');
+ update_post_meta($id, '_device_visibility', in_array($params['device_visibility'] ?? '', ['all', 'pc', 'mobile'], true) ? $params['device_visibility'] : 'all');
+ update_post_meta($id, '_enabled', !empty($params['enabled']) ? '1' : '0');
+ update_post_meta($id, '_show_text', !isset($params['show_text']) || !empty($params['show_text']) ? '1' : '0');
+ update_post_meta($id, '_show_meta', !empty($params['show_meta']) ? '1' : '0');
+ update_post_meta($id, '_text_size_pc', sanitize_text_field($params['text_size_pc'] ?? ''));
+ update_post_meta($id, '_text_size_m', sanitize_text_field($params['text_size_m'] ?? ''));
+ update_post_meta($id, '_text_parallax', !empty($params['text_parallax']) ? '1' : '0');
+
+ if (isset($params['layers']) && is_array($params['layers'])) {
+ update_post_meta($id, '_layers', wp_json_encode(array_values($params['layers'])));
+ } else {
+ delete_post_meta($id, '_layers');
+ }
+
+ if (isset($params['sort_order'])) {
+ wp_update_post([
+ 'ID' => $id,
+ 'menu_order' => (int) $params['sort_order'],
+ ]);
+ }
+
+ return rest_ensure_response(['code' => 'success', 'message' => __('幻灯片已更新', 'aksrt')]);
+ }
+}
+
diff --git a/inc/classes/admin/class-aksrt-admin-payment-controller.php b/inc/classes/admin/class-aksrt-admin-payment-controller.php
new file mode 100644
index 00000000..0bd0352c
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-payment-controller.php
@@ -0,0 +1,753 @@
+register_section_routes('payment', 'payment');
+
+ register_rest_route(self::API_NAMESPACE, '/payment/overview', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_payment_overview'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Revenue stats (with optional time-series)
+ register_rest_route(self::API_NAMESPACE, '/payment/revenue', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_revenue'],
+ 'permission_callback' => [$this, 'check_permission'],
+ 'args' => [
+ 'date_from' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
+ 'date_to' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
+ 'granularity' => ['default' => 'day', 'sanitize_callback' => 'sanitize_text_field'],
+ 'time_series' => ['default' => false, 'sanitize_callback' => 'rest_sanitize_boolean'],
+ ],
+ ]);
+
+ // Withdrawal list — unified pagination: page + per_page
+ register_rest_route(self::API_NAMESPACE, '/payment/withdrawals', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_withdrawals'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Withdrawal status update
+ register_rest_route(self::API_NAMESPACE, '/payment/withdrawals/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_withdrawal_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // ── Charge Card ──
+
+ // Charge card list
+ register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_charge_cards'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_charge_card_detail'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Charge card create
+ register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_charge_card'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Charge card batch create
+ register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/batch', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'batch_create_charge_cards'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Charge card disable
+ register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P\d+)/disable', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'disable_charge_card'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // ── Rebate ──
+
+ // Rebate list
+ register_rest_route(self::API_NAMESPACE, '/payment/rebate/log', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_rebate_list'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_rebate_detail'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Rebate settle
+ register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P\d+)/settle', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'settle_rebate'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Rebate cancel
+ register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P\d+)/cancel', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'cancel_rebate'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+ public function rest_payment_overview(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Settings')) {
+ return $this->error('设置模块未初始化', 500);
+ }
+
+ $paid = AKSRT_Settings::get_paid_content_settings();
+ $recharge = AKSRT_Settings::get_payment_recharge_settings();
+ $vip = AKSRT_Settings::get_payment_vip_settings();
+ $revenue = AKSRT_Settings::get_payment_revenue_settings();
+ $affiliate = AKSRT_Settings::get_payment_affiliate_settings();
+ $gateway = AKSRT_Settings::get_payment_gateway_settings();
+
+ return $this->success([
+ 'data' => [
+ 'paid' => $paid,
+ 'recharge' => $recharge,
+ 'vip' => $vip,
+ 'revenue' => $revenue,
+ 'affiliate' => $affiliate,
+ 'gateway' => $gateway,
+ 'currency_name' => AKSRT_Settings::get_currency_name(),
+ ],
+ ]);
+ }
+
+ /**
+ * GET /payment/revenue — revenue stats with optional time-series
+ */
+ public function get_revenue(WP_REST_Request $request): WP_REST_Response {
+ $revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0, 'count' => 0];
+ if (class_exists('AKSRT_Revenue')) {
+ $revenue_stats = AKSRT_Revenue::get_instance()->get_stats();
+ }
+
+ $withdrawal_stats = ['total' => 0, 'pending' => 0, 'completed' => 0, 'total_paid' => 0];
+ if (class_exists('AKSRT_Withdrawal')) {
+ $withdrawal_stats = AKSRT_Withdrawal::get_instance()->get_stats();
+ }
+
+ $response = [
+ 'revenue' => $revenue_stats,
+ 'withdrawal' => $withdrawal_stats,
+ ];
+
+ // Time-series data
+ if ($request->get_param('time_series')) {
+ $response['time_series'] = $this->get_revenue_time_series($request);
+ }
+
+ return $this->success(['data' => $response]);
+ }
+
+ /**
+ * Build revenue time-series data.
+ */
+ private function get_revenue_time_series(WP_REST_Request $request): array {
+ global $wpdb;
+ $prefix = $wpdb->prefix;
+
+ $date_from = sanitize_text_field((string) $request->get_param('date_from'));
+ $date_to = sanitize_text_field((string) $request->get_param('date_to'));
+ $granularity = sanitize_text_field((string) $request->get_param('granularity'));
+
+ // Default: last 30 days
+ if (!$date_from) {
+ $date_from = gmdate('Y-m-d', strtotime('-30 days'));
+ }
+ if (!$date_to) {
+ $date_to = gmdate('Y-m-d');
+ }
+
+ // Granularity: day, week, month
+ $date_format = '%Y-%m-%d';
+ if ($granularity === 'week') {
+ $date_format = '%x-W%v';
+ } elseif ($granularity === 'month') {
+ $date_format = '%Y-%m';
+ }
+
+ $table = $prefix . 'aksrt_revenue';
+ $results = $wpdb->get_results($wpdb->prepare(
+ "SELECT DATE_FORMAT(created_at, %s) AS period,
+ SUM(amount) AS total_amount,
+ COUNT(*) AS order_count
+ FROM {$table}
+ WHERE created_at >= %s AND created_at < DATE_ADD(%s, INTERVAL 1 DAY)
+ GROUP BY period
+ ORDER BY period ASC",
+ $date_format,
+ $date_from,
+ $date_to
+ ));
+
+ $series = [];
+ if ($results) {
+ foreach ($results as $row) {
+ $series[] = [
+ 'date' => (string) $row->period,
+ 'amount' => (float) $row->total_amount,
+ 'count' => (int) $row->order_count,
+ ];
+ }
+ }
+
+ return $series;
+ }
+
+ /**
+ * GET /payment/withdrawals — 提现列表,统一 page + per_page 分页
+ */
+ public function get_withdrawals(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Withdrawal')) {
+ return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1, 'stats' => []]]);
+ }
+
+ $status = sanitize_key((string) $request->get_param('status'));
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
+
+ $items = AKSRT_Withdrawal::get_instance()->get_all([
+ 'status' => $status,
+ 'limit' => $per_page,
+ 'offset' => ($page - 1) * $per_page,
+ ]);
+
+ $total = AKSRT_Withdrawal::get_instance()->get_count(['status' => $status]);
+ $stats = AKSRT_Withdrawal::get_instance()->get_stats();
+
+ return $this->success([
+ 'data' => [
+ 'items' => $items,
+ 'total' => $total,
+ 'page' => $page,
+ 'total_pages' => max(1, (int) ceil($total / $per_page)),
+ 'stats' => $stats,
+ ],
+ ]);
+ }
+
+ /**
+ * POST /payment/withdrawals/{id} — 更新提现状态
+ */
+ public function update_withdrawal_status(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Withdrawal')) {
+ return $this->error('提现模块未初始化', 500);
+ }
+
+ $id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $new_status = sanitize_key((string) ($params['status'] ?? ''));
+ $note = sanitize_textarea_field((string) ($params['note'] ?? ''));
+
+ $valid_statuses = ['pending', 'approved', 'processing', 'completed', 'rejected'];
+ if (!in_array($new_status, $valid_statuses, true)) {
+ return $this->error('无效的提现状态');
+ }
+
+ if ($id <= 0) {
+ return $this->error('无效的提现 ID');
+ }
+
+ $success = AKSRT_Withdrawal::get_instance()->update_status($id, $new_status, $note);
+
+ if ($success) {
+ return $this->success(['message' => __('提现状态已更新', 'aksrt')]);
+ }
+ return $this->error(__('更新失败', 'aksrt'));
+ }
+
+ /* ------------------------------------------------------------------
+ * Charge Card
+ * ----------------------------------------------------------------*/
+
+ /**
+ * GET /payment/charge-cards — 充值卡列表
+ */
+ public function get_charge_cards(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Charge_Card')) {
+ return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
+ }
+
+ global $wpdb;
+
+ $table = AKSRT_DB_Migrator::table('charge_card');
+ $page = max(1, (int) ($request->get_param('page') ?: 1));
+ $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
+ $status = sanitize_key((string) $request->get_param('status'));
+ $keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
+ $date_from = sanitize_text_field((string) $request->get_param('date_from'));
+ $date_to = sanitize_text_field((string) $request->get_param('date_to'));
+
+ $where = [];
+ $params = [];
+
+ if ($status !== '') {
+ $where[] = 'status = %s';
+ $params[] = $status;
+ }
+ if ($date_from !== '') {
+ $where[] = 'created_at >= %s';
+ $params[] = $date_from . ' 00:00:00';
+ }
+ if ($date_to !== '') {
+ $where[] = 'created_at <= %s';
+ $params[] = $date_to . ' 23:59:59';
+ }
+
+ $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
+ $sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500";
+ $rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
+
+ $items = [];
+ foreach ((array) $rows as $row) {
+ $item = $this->build_charge_card_admin_item($row);
+ if ($keyword !== '' && mb_stripos(implode(' ', array_filter([
+ (string) ($item['code'] ?? ''),
+ (string) ($item['note'] ?? ''),
+ (string) ($item['used_by_user']['display_name'] ?? ''),
+ (string) ($item['used_by_user']['email'] ?? ''),
+ ])), $keyword) === false) {
+ continue;
+ }
+ $items[] = $item;
+ }
+
+ $total = count($items);
+ $offset = ($page - 1) * $per_page;
+ $stats = AKSRT_Charge_Card::get_instance()->get_stats();
+
+ return $this->success([
+ 'data' => [
+ 'items' => array_values(array_slice($items, $offset, $per_page)),
+ 'total' => $total,
+ 'page' => $page,
+ 'per_page' => $per_page,
+ 'max_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
+ 'stats' => $stats,
+ 'options' => [
+ 'statuses' => [
+ ['label' => __('全部状态', 'aksrt'), 'value' => ''],
+ ['label' => __('未使用', 'aksrt'), 'value' => 'active'],
+ ['label' => __('已使用', 'aksrt'), 'value' => 'used'],
+ ['label' => __('已过期', 'aksrt'), 'value' => 'expired'],
+ ['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * POST /payment/charge-cards — 创建充值卡
+ */
+ public function create_charge_card(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Charge_Card')) {
+ return $this->error('充值卡系统未启用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $code = sanitize_text_field((string) ($params['code'] ?? ''));
+ $amount = (float) ($params['amount'] ?? 0);
+
+ if ($code === '' || $amount <= 0) {
+ return $this->error('卡号和金额不能为空');
+ }
+
+ $id = AKSRT_Charge_Card::get_instance()->create($params);
+
+ if ($id > 0) {
+ return $this->success([
+ 'message' => __('充值卡已创建', 'aksrt'),
+ 'data' => ['id' => $id],
+ ], 201);
+ }
+
+ return $this->error(__('创建失败(可能卡号已存在)', 'aksrt'));
+ }
+
+ /**
+ * POST /payment/charge-cards/batch — 批量生成充值卡
+ */
+ public function batch_create_charge_cards(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Charge_Card')) {
+ return $this->error('充值卡系统未启用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $amount = (float) ($params['amount'] ?? 0);
+ $count = max(1, min(100, (int) ($params['count'] ?? 1)));
+ $length = max(8, min(32, (int) ($params['length'] ?? 16)));
+ $prefix = sanitize_text_field((string) ($params['prefix'] ?? ''));
+ $note = sanitize_textarea_field((string) ($params['note'] ?? ''));
+ $expires = sanitize_text_field((string) ($params['expires_at'] ?? ''));
+
+ if ($amount <= 0) {
+ return $this->error('金额必须大于 0');
+ }
+
+ $service = AKSRT_Charge_Card::get_instance();
+ $codes = [];
+ for ($i = 0; $i < $count; $i++) {
+ $code = $prefix . strtoupper(wp_generate_password($length, false));
+ $id = $service->create([
+ 'code' => $code,
+ 'amount' => $amount,
+ 'expires_at' => $expires,
+ 'note' => $note,
+ ]);
+ if ($id > 0) {
+ $codes[] = $code;
+ }
+ }
+
+ return $this->success([
+ 'message' => sprintf(__('已生成 %d 张充值卡', 'aksrt'), count($codes)),
+ 'data' => ['codes' => $codes, 'count' => count($codes)],
+ ]);
+ }
+
+ /**
+ * POST /payment/charge-cards/{id}/disable — 禁用充值卡
+ */
+ public function disable_charge_card(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Charge_Card')) {
+ return $this->error('充值卡系统未启用', 500);
+ }
+
+ $id = (int) $request->get_param('id');
+ if ($id <= 0) {
+ return $this->error('参数无效');
+ }
+
+ $success = AKSRT_Charge_Card::get_instance()->disable($id);
+
+ if ($success) {
+ return $this->success(['message' => __('充值卡已禁用', 'aksrt')]);
+ }
+ return $this->error(__('操作失败', 'aksrt'));
+ }
+
+ public function get_charge_card_detail(WP_REST_Request $request): WP_REST_Response {
+ global $wpdb;
+
+ $id = (int) $request->get_param('id');
+ $table = AKSRT_DB_Migrator::table('charge_card');
+
+ if ($id <= 0 || !$table) {
+ return $this->error(__('充值卡不存在', 'aksrt'), 404);
+ }
+
+ $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A);
+ if (!$row) {
+ return $this->error(__('充值卡不存在', 'aksrt'), 404);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'card' => $this->build_charge_card_admin_item($row),
+ ],
+ ]);
+ }
+
+ /* ------------------------------------------------------------------
+ * Rebate
+ * ----------------------------------------------------------------*/
+
+ /**
+ * GET /payment/rebate/log — 返佣记录列表
+ */
+ public function get_rebate_list(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Rebate')) {
+ return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
+ }
+
+ global $wpdb;
+
+ $table = AKSRT_DB_Migrator::table('rebate_log');
+ $page = max(1, (int) ($request->get_param('page') ?: 1));
+ $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
+ $status = sanitize_key((string) $request->get_param('status'));
+ $type = sanitize_key((string) $request->get_param('type'));
+ $keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
+ $user_id = (int) $request->get_param('user_id');
+ $date_from = sanitize_text_field((string) $request->get_param('date_from'));
+ $date_to = sanitize_text_field((string) $request->get_param('date_to'));
+
+ $where = [];
+ $params = [];
+
+ if ($status !== '') {
+ $where[] = 'status = %s';
+ $params[] = $status;
+ }
+ if ($type !== '') {
+ $where[] = 'type = %s';
+ $params[] = $type;
+ }
+ if ($user_id > 0) {
+ $where[] = 'user_id = %d';
+ $params[] = $user_id;
+ }
+ if ($date_from !== '') {
+ $where[] = 'created_at >= %s';
+ $params[] = $date_from . ' 00:00:00';
+ }
+ if ($date_to !== '') {
+ $where[] = 'created_at <= %s';
+ $params[] = $date_to . ' 23:59:59';
+ }
+
+ $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
+ $sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500";
+ $rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
+
+ $items = [];
+ foreach ((array) $rows as $row) {
+ $item = $this->build_rebate_admin_item($row);
+ if ($keyword !== '' && mb_stripos(implode(' ', array_filter([
+ (string) ($item['order_number'] ?? ''),
+ (string) ($item['note'] ?? ''),
+ (string) ($item['user']['display_name'] ?? ''),
+ (string) ($item['source_user']['display_name'] ?? ''),
+ ])), $keyword) === false) {
+ continue;
+ }
+ $items[] = $item;
+ }
+
+ $total = count($items);
+ $offset = ($page - 1) * $per_page;
+ $stats = AKSRT_Rebate::get_instance()->get_stats();
+
+ return $this->success([
+ 'data' => [
+ 'items' => array_values(array_slice($items, $offset, $per_page)),
+ 'total' => $total,
+ 'page' => $page,
+ 'per_page' => $per_page,
+ 'max_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
+ 'stats' => $stats,
+ 'options' => [
+ 'statuses' => [
+ ['label' => __('全部状态', 'aksrt'), 'value' => ''],
+ ['label' => __('待结算', 'aksrt'), 'value' => 'pending'],
+ ['label' => __('已结算', 'aksrt'), 'value' => 'settled'],
+ ['label' => __('已取消', 'aksrt'), 'value' => 'cancelled'],
+ ],
+ 'types' => [
+ ['label' => __('全部类型', 'aksrt'), 'value' => ''],
+ ['label' => __('订单返佣', 'aksrt'), 'value' => 'order'],
+ ['label' => __('邀请返佣', 'aksrt'), 'value' => 'invite'],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * POST /payment/rebate/{id}/settle — 结算返佣
+ */
+ public function settle_rebate(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Rebate')) {
+ return $this->error('返佣系统未启用', 500);
+ }
+
+ $id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $note = sanitize_textarea_field((string) ($params['note'] ?? ''));
+ if ($id <= 0) {
+ return $this->error('参数无效');
+ }
+
+ $success = AKSRT_Rebate::get_instance()->settle($id);
+
+ if ($success) {
+ $this->append_rebate_process_log($id, 'settled', $note);
+ return $this->success(['message' => __('返佣已结算', 'aksrt')]);
+ }
+ return $this->error(__('结算失败', 'aksrt'));
+ }
+
+ /**
+ * POST /payment/rebate/{id}/cancel — 取消返佣
+ */
+ public function cancel_rebate(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Rebate')) {
+ return $this->error('返佣系统未启用', 500);
+ }
+
+ $id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $note = sanitize_textarea_field((string) ($params['note'] ?? ''));
+ if ($id <= 0) {
+ return $this->error('参数无效');
+ }
+
+ $success = AKSRT_Rebate::get_instance()->cancel($id);
+
+ if ($success) {
+ $this->append_rebate_process_log($id, 'cancelled', $note);
+ return $this->success(['message' => __('返佣已取消', 'aksrt')]);
+ }
+ return $this->error(__('操作失败', 'aksrt'));
+ }
+
+ public function get_rebate_detail(WP_REST_Request $request): WP_REST_Response {
+ global $wpdb;
+
+ $id = (int) $request->get_param('id');
+ $table = AKSRT_DB_Migrator::table('rebate_log');
+
+ if ($id <= 0 || !$table) {
+ return $this->error(__('返佣记录不存在', 'aksrt'), 404);
+ }
+
+ $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A);
+ if (!$row) {
+ return $this->error(__('返佣记录不存在', 'aksrt'), 404);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'record' => $this->build_rebate_admin_item($row),
+ 'history' => $this->get_rebate_process_log($id),
+ ],
+ ]);
+ }
+
+ private function build_charge_card_admin_item(array $row): array {
+ $used_by = (int) ($row['used_by'] ?? 0);
+ $user = $used_by > 0 ? get_userdata($used_by) : null;
+ $status = sanitize_key((string) ($row['status'] ?? 'active'));
+ $statusMap = [
+ 'active' => __('未使用', 'aksrt'),
+ 'used' => __('已使用', 'aksrt'),
+ 'expired' => __('已过期', 'aksrt'),
+ 'disabled' => __('已禁用', 'aksrt'),
+ ];
+
+ return [
+ 'id' => (int) ($row['id'] ?? 0),
+ 'code' => (string) ($row['code'] ?? ''),
+ 'amount' => (float) ($row['amount'] ?? 0),
+ 'balance' => (float) ($row['balance'] ?? 0),
+ 'status' => $status,
+ 'status_label' => $statusMap[$status] ?? $status,
+ 'used_by' => $used_by,
+ 'used_at' => (string) ($row['used_at'] ?? ''),
+ 'expires_at' => (string) ($row['expires_at'] ?? ''),
+ 'created_at' => (string) ($row['created_at'] ?? ''),
+ 'note' => (string) ($row['note'] ?? ''),
+ 'used_by_user' => $user instanceof WP_User ? [
+ 'id' => (int) $user->ID,
+ 'display_name' => $user->display_name,
+ 'email' => $user->user_email,
+ ] : null,
+ ];
+ }
+
+ private function build_rebate_admin_item(array $row): array {
+ $user_id = (int) ($row['user_id'] ?? 0);
+ $source_user_id = (int) ($row['source_user_id'] ?? 0);
+ $user = $user_id > 0 ? get_userdata($user_id) : null;
+ $source_user = $source_user_id > 0 ? get_userdata($source_user_id) : null;
+ $status = sanitize_key((string) ($row['status'] ?? 'pending'));
+ $type = sanitize_key((string) ($row['type'] ?? 'order'));
+
+ $statusMap = [
+ 'pending' => __('待结算', 'aksrt'),
+ 'settled' => __('已结算', 'aksrt'),
+ 'cancelled' => __('已取消', 'aksrt'),
+ ];
+ $typeMap = [
+ 'order' => __('订单返佣', 'aksrt'),
+ 'invite' => __('邀请返佣', 'aksrt'),
+ ];
+
+ return [
+ 'id' => (int) ($row['id'] ?? 0),
+ 'order_number' => (string) ($row['order_number'] ?? ''),
+ 'amount' => (float) ($row['amount'] ?? 0),
+ 'status' => $status,
+ 'status_label' => $statusMap[$status] ?? $status,
+ 'type' => $type,
+ 'type_label' => $typeMap[$type] ?? $type,
+ 'created_at' => (string) ($row['created_at'] ?? ''),
+ 'settled_at' => (string) ($row['settled_at'] ?? ''),
+ 'note' => (string) ($row['note'] ?? ''),
+ 'history_count' => count($this->get_rebate_process_log((int) ($row['id'] ?? 0))),
+ 'user' => $user instanceof WP_User ? [
+ 'id' => (int) $user->ID,
+ 'display_name' => $user->display_name,
+ 'email' => $user->user_email,
+ ] : null,
+ 'source_user' => $source_user instanceof WP_User ? [
+ 'id' => (int) $source_user->ID,
+ 'display_name' => $source_user->display_name,
+ 'email' => $source_user->user_email,
+ ] : null,
+ ];
+ }
+
+ private function append_rebate_process_log(int $rebate_id, string $event, string $note = ''): void {
+ $log = get_option('aksrt_rebate_process_log', []);
+ if (!is_array($log)) {
+ $log = [];
+ }
+
+ $rebate_id = (int) $rebate_id;
+ if (!isset($log[$rebate_id]) || !is_array($log[$rebate_id])) {
+ $log[$rebate_id] = [];
+ }
+
+ $log[$rebate_id][] = [
+ 'event' => sanitize_key($event),
+ 'note' => sanitize_textarea_field($note),
+ 'operator' => get_current_user_id(),
+ 'operator_name' => ($user = wp_get_current_user()) ? $user->display_name : '',
+ 'time' => current_time('mysql'),
+ ];
+
+ update_option('aksrt_rebate_process_log', $log, false);
+ }
+
+ private function get_rebate_process_log(int $rebate_id): array {
+ $log = get_option('aksrt_rebate_process_log', []);
+ if (!is_array($log) || !isset($log[$rebate_id]) || !is_array($log[$rebate_id])) {
+ return [];
+ }
+
+ return array_values($log[$rebate_id]);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-permissions-controller.php b/inc/classes/admin/class-aksrt-admin-permissions-controller.php
new file mode 100644
index 00000000..8c9cabf4
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-permissions-controller.php
@@ -0,0 +1,171 @@
+ 'GET',
+ 'callback' => [$this, 'get_capabilities'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/permissions/capabilities', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'save_capabilities'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+ public function get_capabilities(): WP_REST_Response {
+ if (!class_exists('AKSRT_Settings')) {
+ return $this->error(__('能力矩阵模块不可用', 'aksrt'), 500);
+ }
+
+ $caps = AKSRT_Settings::get_user_capabilities();
+ $defs = AKSRT_Settings::cap_definitions();
+ $columns = AKSRT_Settings::cap_role_columns();
+
+ return $this->success([
+ 'data' => [
+ 'columns' => $this->get_capability_column_labels(),
+ 'groups' => $this->build_capability_groups($defs, $caps, $columns),
+ 'summary' => [
+ 'cap_count' => count($defs),
+ 'group_count' => 3,
+ 'column_count' => count($columns),
+ ],
+ ],
+ ]);
+ }
+
+ public function save_capabilities(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Settings')) {
+ return $this->error(__('能力矩阵模块不可用', 'aksrt'), 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $input = $params['user_caps'] ?? null;
+
+ if (!is_array($input)) {
+ return $this->error(__('无效的能力矩阵数据', 'aksrt'), 422);
+ }
+
+ $defs = AKSRT_Settings::cap_definitions();
+ $columns = AKSRT_Settings::cap_role_columns();
+ $clean = [];
+
+ foreach ($defs as $cap_id => $def) {
+ $row = is_array($input[$cap_id] ?? null) ? $input[$cap_id] : [];
+ $clean[$cap_id] = [];
+
+ foreach ($columns as $column) {
+ if (in_array($column, ['level', 'vip'], true)) {
+ $value = isset($row[$column]) ? (int) $row[$column] : -1;
+ $max = $column === 'level' ? 10 : 2;
+ $clean[$cap_id][$column] = max(-1, min($max, $value));
+ } else {
+ $clean[$cap_id][$column] = !empty($row[$column]);
+ }
+ }
+ }
+
+ $all = AKSRT_Settings::get_all();
+ $all['user_caps'] = $clean;
+ update_option('aksrt_settings', $all);
+ AKSRT_Settings::flush_settings_cache();
+
+ return $this->success([
+ 'message' => __('能力矩阵已保存', 'aksrt'),
+ 'data' => [
+ 'user_caps' => $clean,
+ ],
+ ]);
+ }
+
+ private function build_capability_groups(array $defs, array $caps, array $columns): array {
+ $buckets = [
+ 'user' => [
+ 'key' => 'user',
+ 'title' => __('用户治理', 'aksrt'),
+ 'items' => [],
+ ],
+ 'post' => [
+ 'key' => 'post',
+ 'title' => __('投稿权限', 'aksrt'),
+ 'items' => [],
+ ],
+ 'comment' => [
+ 'key' => 'comment',
+ 'title' => __('评论权限', 'aksrt'),
+ 'items' => [],
+ ],
+ ];
+
+ foreach ($defs as $cap_id => $def) {
+ $bucket_key = 'user';
+ if (str_starts_with($cap_id, 'comment_')) {
+ $bucket_key = 'comment';
+ } elseif (str_starts_with($cap_id, 'new_post_')) {
+ $bucket_key = 'post';
+ }
+
+ $excludes = $def['exclude_roles'] ?? [];
+ $only_roles = $def['roles'] ?? null;
+ $values = $caps[$cap_id] ?? [];
+
+ $visible_columns = [];
+ foreach ($columns as $column) {
+ if (in_array($column, $excludes, true)) {
+ continue;
+ }
+ if ($only_roles !== null && !in_array($column, $only_roles, true) && !in_array($column, ['all', 'logged', 'level', 'vip', 'auth'], true)) {
+ continue;
+ }
+ $visible_columns[] = $column;
+ }
+
+ $buckets[$bucket_key]['items'][] = [
+ 'cap_id' => $cap_id,
+ 'label' => $def['label'],
+ 'visible_columns' => $visible_columns,
+ 'values' => $values,
+ ];
+ }
+
+ return array_values($buckets);
+ }
+
+ private function get_capability_column_labels(): array {
+ return [
+ 'all' => __('所有人', 'aksrt'),
+ 'logged' => __('已登录', 'aksrt'),
+ 'level' => __('等级≥', 'aksrt'),
+ 'vip' => __('VIP≥', 'aksrt'),
+ 'auth' => __('已认证', 'aksrt'),
+ 'moderator' => __('版主', 'aksrt'),
+ 'plate_author' => __('超级版主', 'aksrt'),
+ 'cat_moderator' => __('分区版主', 'aksrt'),
+ ];
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-post-list-controller.php b/inc/classes/admin/class-aksrt-admin-post-list-controller.php
new file mode 100644
index 00000000..c4da6b7d
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-post-list-controller.php
@@ -0,0 +1,24 @@
+register_section_routes('post-list', 'post_list');
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-schema-provider.php b/inc/classes/admin/class-aksrt-admin-schema-provider.php
new file mode 100644
index 00000000..0dbac292
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-schema-provider.php
@@ -0,0 +1,384 @@
+ section -> group -> field.
+ *
+ * @package AKSRT
+ */
+
+defined('ABSPATH') || exit;
+
+class AKSRT_Admin_Schema_Provider {
+
+ private static $instance = null;
+
+ /**
+ * @var array>
+ */
+ private $domain_schemas = [];
+
+ public static function get_instance(): self {
+ if (null === self::$instance) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ public function normalize_domain(string $domain): string {
+ $domain = trim(str_replace('-', '_', $domain));
+ return $domain !== '' ? $domain : 'global';
+ }
+
+ public function normalize_section(string $section): string {
+ return trim($section);
+ }
+
+ public function get_domain_keys(): array {
+ return [
+ 'global',
+ 'page_display',
+ 'post_list',
+ 'permissions',
+ 'users',
+ 'payment',
+ 'extensions',
+ 'tools',
+ 'shop',
+ 'forum',
+ ];
+ }
+
+ public function get_schema(string $domain): ?array {
+ $domain = $this->normalize_domain($domain);
+
+ if (isset($this->domain_schemas[$domain])) {
+ return $this->domain_schemas[$domain];
+ }
+
+ $builder_map = $this->get_schema_builder_map();
+ if (!isset($builder_map[$domain])) {
+ return null;
+ }
+
+ $builder = $builder_map[$domain];
+ $this->domain_schemas[$domain] = $builder::build();
+
+ return $this->domain_schemas[$domain];
+ }
+
+ public function get_sections(string $domain): array {
+ $schema = $this->get_schema($domain);
+ return is_array($schema['sections'] ?? null) ? $schema['sections'] : [];
+ }
+
+ public function get_section_schema(string $domain, string $section): ?array {
+ $sections = $this->get_sections($domain);
+ $section = $this->normalize_section($section);
+ return $sections[$section] ?? null;
+ }
+
+ public function get_option_key(string $domain, string $section = ''): string {
+ if ($section !== '') {
+ $schema = $this->get_section_schema($domain, $section);
+ return (string) ($schema['option_key'] ?? 'aksrt_settings');
+ }
+
+ $primary = [
+ 'global' => 'aksrt_settings',
+ 'page_display' => 'aksrt_settings',
+ 'post_list' => 'aksrt_settings',
+ 'permissions' => 'aksrt_settings',
+ 'users' => 'aksrt_settings',
+ 'payment' => 'aksrt_settings',
+ 'extensions' => 'aksrt_settings',
+ 'tools' => 'aksrt_settings',
+ 'shop' => 'aksrt_settings',
+ 'forum' => 'aksrt_settings',
+ ];
+
+ return $primary[$this->normalize_domain($domain)] ?? 'aksrt_settings';
+ }
+
+ public function get_defaults(string $domain, string $section = ''): array {
+ if ($section !== '') {
+ $schema = $this->get_section_schema($domain, $section);
+ return $schema ? $this->collect_defaults($schema) : [];
+ }
+
+ $defaults = [];
+ foreach ($this->get_sections($domain) as $section_schema) {
+ $defaults = array_merge($defaults, $this->collect_defaults($section_schema));
+ }
+
+ return $defaults;
+ }
+
+ public function get_allowed_fields(string $domain, string $section = ''): array {
+ if ($section !== '') {
+ $schema = $this->get_section_schema($domain, $section);
+ return $schema ? $this->collect_allowed_fields($schema) : [];
+ }
+
+ $allowed = [];
+ foreach ($this->get_sections($domain) as $section_schema) {
+ $allowed = array_merge($allowed, $this->collect_allowed_fields($section_schema));
+ }
+
+ return $allowed;
+ }
+
+ public function validate(string $domain, array $data, string $section = ''): array {
+ $schemas = [];
+
+ if ($section !== '') {
+ $schema = $this->get_section_schema($domain, $section);
+ if (!$schema) {
+ return ['valid' => false, 'errors' => ['Unknown section']];
+ }
+ $schemas[] = $schema;
+ } else {
+ $schemas = array_values($this->get_sections($domain));
+ }
+
+ $errors = [];
+
+ foreach ($schemas as $schema) {
+ foreach ($this->get_all_fields($schema) as $field) {
+ $key = $field['key'];
+ if (!array_key_exists($key, $data)) {
+ continue;
+ }
+
+ $value = $data[$key];
+ if (!empty($field['required']) && ($value === '' || $value === null)) {
+ $errors[] = sprintf('%s is required', $field['label'] ?? $key);
+ continue;
+ }
+
+ $type = $field['type'] ?? 'input';
+ if (($type === 'number' || $type === 'slider') && !is_numeric($value)) {
+ $errors[] = sprintf('%s must be a number', $field['label'] ?? $key);
+ continue;
+ }
+
+ if ($type === 'checkbox' && !is_array($value)) {
+ $errors[] = sprintf('%s must be an array', $field['label'] ?? $key);
+ continue;
+ }
+
+ if ($type === 'repeater' && !is_array($value)) {
+ $errors[] = sprintf('%s must be an array of items', $field['label'] ?? $key);
+ continue;
+ }
+
+ if (isset($field['min']) && is_numeric($value) && $value < $field['min']) {
+ $errors[] = sprintf('%s must be at least %s', $field['label'] ?? $key, $field['min']);
+ }
+
+ if (isset($field['max']) && is_numeric($value) && $value > $field['max']) {
+ $errors[] = sprintf('%s must be at most %s', $field['label'] ?? $key, $field['max']);
+ }
+
+ if (in_array($type, ['select', 'radio'], true) && !empty($field['options'])) {
+ $valid_values = array_column($field['options'], 'value');
+ if (!in_array($value, $valid_values, true)) {
+ $errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
+ }
+ }
+
+ if ($type === 'checkbox' && !empty($field['options'])) {
+ $valid_values = array_column($field['options'], 'value');
+ foreach ((array) $value as $item) {
+ if (!in_array($item, $valid_values, true)) {
+ $errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return ['valid' => empty($errors), 'errors' => $errors];
+ }
+
+ public function read_settings(string $domain, string $section = ''): array {
+ if ($section !== '') {
+ $schema = $this->get_section_schema($domain, $section);
+ if (!$schema) {
+ return [];
+ }
+
+ return $this->read_section_settings($schema);
+ }
+
+ $settings = [];
+ foreach ($this->get_sections($domain) as $section_schema) {
+ $settings = array_merge($settings, $this->read_section_settings($section_schema));
+ }
+
+ return $settings;
+ }
+
+ public function write_settings(string $domain, array $data, string $section = ''): bool {
+ if ($section !== '') {
+ $schema = $this->get_section_schema($domain, $section);
+ if (!$schema) {
+ return false;
+ }
+
+ return $this->write_section_settings($schema, $data);
+ }
+
+ $result = true;
+ foreach ($this->get_sections($domain) as $section_schema) {
+ $allowed = array_keys($this->collect_allowed_fields($section_schema));
+ $payload = array_intersect_key($data, array_flip($allowed));
+ if ($payload === []) {
+ continue;
+ }
+ $result = $this->write_section_settings($section_schema, $payload) && $result;
+ }
+
+ return $result;
+ }
+
+ public function reset_to_defaults(string $domain, string $section = ''): bool {
+ if ($section !== '') {
+ return $this->write_settings($domain, $this->get_defaults($domain, $section), $section);
+ }
+
+ return $this->write_settings($domain, $this->get_defaults($domain));
+ }
+
+ private function get_schema_builder_map(): array {
+ return [
+ 'global' => 'AKSRT_Admin_Schema_Global',
+ 'page_display' => 'AKSRT_Admin_Schema_Page_Display',
+ 'post_list' => 'AKSRT_Admin_Schema_Post_List',
+ 'permissions' => 'AKSRT_Admin_Schema_Permissions',
+ 'users' => 'AKSRT_Admin_Schema_Users',
+ 'payment' => 'AKSRT_Admin_Schema_Payment',
+ 'extensions' => 'AKSRT_Admin_Schema_Extensions',
+ 'tools' => 'AKSRT_Admin_Schema_Tools',
+ 'shop' => 'AKSRT_Admin_Schema_Shop',
+ 'forum' => 'AKSRT_Admin_Schema_Forum',
+ ];
+ }
+
+ private function read_section_settings(array $schema): array {
+ $option_key = (string) ($schema['option_key'] ?? 'aksrt_settings');
+ $stored = get_option($option_key, []);
+ if (!is_array($stored)) {
+ $stored = [];
+ }
+
+ $data = $this->collect_defaults($schema);
+ foreach ($this->get_all_fields($schema) as $field) {
+ if (array_key_exists($field['key'], $stored)) {
+ $data[$field['key']] = $stored[$field['key']];
+ }
+ }
+
+ return $data;
+ }
+
+ private function write_section_settings(array $schema, array $data): bool {
+ $option_key = (string) ($schema['option_key'] ?? 'aksrt_settings');
+ $stored = get_option($option_key, []);
+ if (!is_array($stored)) {
+ $stored = [];
+ }
+
+ $allowed = $this->collect_allowed_fields($schema);
+ foreach ($allowed as $key => $type) {
+ if (!array_key_exists($key, $data)) {
+ continue;
+ }
+
+ $value = $this->sanitize_value($data[$key], $type);
+ $stored[$key] = $value;
+ }
+
+ $updated = update_option($option_key, $stored);
+ AKSRT_Settings::flush_settings_cache();
+
+ if (isset($stored['site_favicon'])) {
+ AKSRT_Settings::sync_native_site_icon((string) $stored['site_favicon']);
+ }
+
+ return $updated || true;
+ }
+
+ private function collect_defaults(array $schema): array {
+ $defaults = [];
+ foreach ($this->get_all_fields($schema) as $field) {
+ if (array_key_exists('default', $field)) {
+ $defaults[$field['key']] = $field['default'];
+ }
+ }
+ return $defaults;
+ }
+
+ private function collect_allowed_fields(array $schema): array {
+ $allowed = [];
+ foreach ($this->get_all_fields($schema) as $field) {
+ $allowed[$field['key']] = $field['sanitize_type'] ?? $this->infer_sanitize_type($field['type'] ?? 'input');
+ }
+ return $allowed;
+ }
+
+ private function get_all_fields(array $schema): array {
+ $fields = [];
+ foreach ($schema['groups'] ?? [] as $group) {
+ foreach ($group['fields'] ?? [] as $field) {
+ $fields[] = $field;
+ }
+ }
+ return $fields;
+ }
+
+ private function sanitize_value($value, string $type) {
+ switch ($type) {
+ case 'html':
+ return wp_kses_post((string) $value);
+ case 'code':
+ return is_string($value) ? wp_unslash($value) : '';
+ case 'url':
+ return esc_url_raw((string) $value);
+ case 'int':
+ return (int) $value;
+ case 'bool':
+ return !empty($value);
+ case 'float':
+ return (float) $value;
+ case 'array':
+ return is_array($value) ? $value : [];
+ default:
+ return sanitize_text_field((string) $value);
+ }
+ }
+
+ private function infer_sanitize_type(string $field_type): string {
+ $map = [
+ 'switch' => 'bool',
+ 'input' => 'string',
+ 'text' => 'string',
+ 'textarea' => 'string',
+ 'number' => 'int',
+ 'slider' => 'int',
+ 'select' => 'string',
+ 'radio' => 'string',
+ 'checkbox' => 'array',
+ 'repeater' => 'array',
+ 'color' => 'string',
+ 'icon' => 'string',
+ 'media' => 'url',
+ 'code' => 'code',
+ ];
+
+ return $map[$field_type] ?? 'string';
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller-coupon-gateway-operations.php b/inc/classes/admin/class-aksrt-admin-shop-controller-coupon-gateway-operations.php
new file mode 100644
index 00000000..cbe77a6a
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-shop-controller-coupon-gateway-operations.php
@@ -0,0 +1,97 @@
+success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1]]);
+ }
+
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
+ $status = sanitize_key((string) $request->get_param('status'));
+ $search = sanitize_text_field((string) $request->get_param('search'));
+
+ $result = AKSRT_Coupon::get_instance()->get_list([
+ 'status' => $status,
+ 'search' => $search,
+ 'limit' => $per_page,
+ 'offset' => ($page - 1) * $per_page,
+ ]);
+
+ return $this->success(['data' => $result]);
+ }
+
+ /**
+ * POST /shop/coupons — 创建优惠券
+ */
+ public function create_coupon(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Coupon')) {
+ return $this->error('优惠券系统未启用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $code = sanitize_text_field((string) ($params['code'] ?? ''));
+
+ if ($code === '') {
+ return $this->error('优惠码不能为空');
+ }
+
+ $id = AKSRT_Coupon::get_instance()->create($params);
+
+ if ($id > 0) {
+ return $this->success([
+ 'message' => __('优惠券已创建', 'aksrt'),
+ 'data' => ['id' => $id],
+ ], 201);
+ }
+
+ return $this->error(__('创建失败(可能优惠码已存在)', 'aksrt'));
+ }
+
+ /**
+ * POST /shop/coupons/{id}/disable — 禁用优惠券
+ */
+ public function disable_coupon(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Coupon')) {
+ return $this->error('优惠券系统未启用', 500);
+ }
+
+ $id = (int) $request->get_param('id');
+ if ($id <= 0) {
+ return $this->error('参数无效');
+ }
+
+ $success = AKSRT_Coupon::get_instance()->disable($id);
+
+ if ($success) {
+ return $this->success(['message' => __('优惠券已禁用', 'aksrt')]);
+ }
+ return $this->error(__('操作失败', 'aksrt'));
+ }
+
+ /**
+ * GET /shop/gateways — 支付网关状态列表
+ */
+ public function get_gateways(): WP_REST_Response {
+ if (!class_exists('AKSRT_Payment_Gateway')) {
+ return $this->success(['data' => ['gateways' => []]]);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'gateways' => AKSRT_Payment_Gateway::get_instance()->get_status_summary(),
+ ],
+ ]);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller-frontend-flow-operations.php b/inc/classes/admin/class-aksrt-admin-shop-controller-frontend-flow-operations.php
new file mode 100644
index 00000000..63430a09
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-shop-controller-frontend-flow-operations.php
@@ -0,0 +1,324 @@
+error('商城服务不可用', 500);
+ }
+
+ $result = AKSRT_Shop::get_instance()->preview_checkout(get_current_user_id(), $this->get_json_params($request));
+ if (empty($result['success'])) {
+ return $this->error($result['message'] ?? '结算预览失败');
+ }
+
+ return $this->success(['data' => $result['data']]);
+ }
+
+ public function create_frontend_order(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $result = AKSRT_Shop::get_instance()->create_checkout_order(get_current_user_id(), $this->get_json_params($request));
+ if (empty($result['success'])) {
+ return $this->error($result['message'] ?? '订单创建失败');
+ }
+
+ return $this->success([
+ 'message' => __('订单已创建', 'aksrt'),
+ 'data' => $result['data'],
+ ], 201);
+ }
+
+ public function get_frontend_orders(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->success(['data' => ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1]]);
+ }
+
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
+ $status = sanitize_key((string) $request->get_param('status'));
+
+ $view = AKSRT_Shop::get_instance()->get_order_center_view(get_current_user_id(), [
+ 'page' => $page,
+ 'per_page' => $per_page,
+ 'status' => $status,
+ ]);
+
+ return $this->success(['data' => $view]);
+ }
+
+ public function get_frontend_order(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $order_number = sanitize_text_field((string) $request->get_param('order_number'));
+ $order = AKSRT_Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
+ if (!$order) {
+ return $this->error('订单不存在', 404);
+ }
+
+ return $this->success(['data' => $order]);
+ }
+
+ public function pay_frontend_order(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance'));
+ $order_number = sanitize_text_field((string) $request->get_param('order_number'));
+
+ $result = AKSRT_Shop::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method);
+ if (empty($result['success'])) {
+ return $this->error($result['message'] ?? '支付失败');
+ }
+
+ return $this->success(['data' => $result['data']]);
+ }
+
+ public function cancel_frontend_order(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $order_number = sanitize_text_field((string) $request->get_param('order_number'));
+ $result = AKSRT_Shop::get_instance()->cancel_order(get_current_user_id(), $order_number);
+ if (empty($result['success'])) {
+ return $this->error($result['message'] ?? '取消订单失败');
+ }
+
+ return $this->success(['data' => $result['data']]);
+ }
+
+ public function create_frontend_after_sale(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $params['order_number'] = sanitize_text_field((string) $request->get_param('order_number'));
+ $result = AKSRT_Shop::get_instance()->create_after_sale_request(get_current_user_id(), $params);
+ if (empty($result['success'])) {
+ return $this->error($result['message'] ?? '提交售后失败');
+ }
+
+ return $this->success([
+ 'message' => __('售后申请已提交', 'aksrt'),
+ 'data' => $result['data'],
+ ], 201);
+ }
+
+ public function get_cart(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ $shop = AKSRT_Shop::get_instance();
+ $cart = $shop->get_cart($user_id);
+
+ $items = [];
+ foreach ($cart as $pid => $opts_map) {
+ $product = $shop->build_product_card_view((int) $pid);
+ if (empty($product) || !is_array($opts_map)) {
+ continue;
+ }
+ foreach ($opts_map as $opt_str => $qty) {
+ $items[] = [
+ 'product_id' => (int) $pid,
+ 'options_string' => $opt_str === '_default' ? '' : $opt_str,
+ 'quantity' => (int) $qty,
+ 'product' => $product,
+ ];
+ }
+ }
+
+ return $this->success([
+ 'data' => [
+ 'items' => $items,
+ 'count' => $shop->get_cart_count($user_id),
+ ],
+ ]);
+ }
+
+ public function get_cart_count(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ return $this->success(['data' => ['count' => AKSRT_Shop::get_instance()->get_cart_count($user_id)]]);
+ }
+
+ public function handle_add_to_cart(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $product_id = (int) ($params['product_id'] ?? 0);
+ $options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
+ $count = max(1, (int) ($params['count'] ?? 1));
+
+ $result = AKSRT_Shop::get_instance()->add_to_cart(get_current_user_id(), $product_id, $options_string, $count);
+
+ if (isset($result['error'])) {
+ return $this->error($result['error']);
+ }
+ return $this->success(['data' => $result]);
+ }
+
+ public function handle_reduce_from_cart(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $product_id = (int) ($params['product_id'] ?? 0);
+ $options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
+ $count = max(1, (int) ($params['count'] ?? 1));
+
+ $result = AKSRT_Shop::get_instance()->reduce_from_cart(get_current_user_id(), $product_id, $options_string, $count);
+
+ if (isset($result['error'])) {
+ return $this->error($result['error']);
+ }
+ return $this->success(['data' => $result]);
+ }
+
+ public function handle_remove_from_cart(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $product_id = (int) ($params['product_id'] ?? 0);
+ $options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
+
+ $result = AKSRT_Shop::get_instance()->remove_from_cart(get_current_user_id(), $product_id, $options_string);
+
+ if (isset($result['error'])) {
+ return $this->error($result['error']);
+ }
+ return $this->success(['data' => $result]);
+ }
+
+ public function handle_remove_multi_from_cart(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $items = is_array($params['items'] ?? null) ? $params['items'] : [];
+
+ $result = AKSRT_Shop::get_instance()->remove_multi_from_cart(get_current_user_id(), $items);
+
+ if (isset($result['error'])) {
+ return $this->error($result['error']);
+ }
+ return $this->success(['data' => $result]);
+ }
+
+ public function handle_empty_cart(WP_REST_Request $request): WP_REST_Response {
+ $result = AKSRT_Shop::get_instance()->empty_cart(get_current_user_id());
+
+ if (isset($result['error'])) {
+ return $this->error($result['error']);
+ }
+ return $this->success(['data' => $result]);
+ }
+
+ public function get_frontend_addresses(): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->success(['data' => ['addresses' => []]]);
+ }
+
+ $addresses = AKSRT_Shop::get_instance()->get_address_book(get_current_user_id());
+ return $this->success(['data' => ['addresses' => $addresses]]);
+ }
+
+ public function create_frontend_address(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $user_id = get_current_user_id();
+ $result = AKSRT_Shop::get_instance()->add_address($user_id, $params);
+
+ if (empty($result['success'])) {
+ return $this->error($result['error'] ?? '添加地址失败');
+ }
+
+ return $this->success([
+ 'message' => __('地址已添加', 'aksrt'),
+ 'data' => ['address' => $result['address']],
+ ], 201);
+ }
+
+ public function update_frontend_address(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $address_id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $user_id = get_current_user_id();
+ $result = AKSRT_Shop::get_instance()->update_address($user_id, $address_id, $params);
+
+ if (empty($result['success'])) {
+ return $this->error($result['error'] ?? '更新地址失败');
+ }
+
+ return $this->success([
+ 'message' => __('地址已更新', 'aksrt'),
+ 'data' => ['address' => $result['address']],
+ ]);
+ }
+
+ public function delete_frontend_address(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $address_id = (int) $request->get_param('id');
+ $user_id = get_current_user_id();
+ $result = AKSRT_Shop::get_instance()->delete_address($user_id, $address_id);
+
+ if (empty($result['success'])) {
+ return $this->error($result['error'] ?? '删除地址失败');
+ }
+
+ return $this->success(['message' => __('地址已删除', 'aksrt')]);
+ }
+
+ public function set_default_frontend_address(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $address_id = (int) $request->get_param('id');
+ $user_id = get_current_user_id();
+ $result = AKSRT_Shop::get_instance()->set_default_address($user_id, $address_id);
+
+ if (empty($result['success'])) {
+ return $this->error($result['error'] ?? '设置默认地址失败');
+ }
+
+ return $this->success(['message' => __('已设为默认地址', 'aksrt')]);
+ }
+
+ public function create_frontend_product(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $params['author_id'] = get_current_user_id();
+ $frontend_request = new WP_REST_Request('POST', '/');
+ $frontend_request->set_body(wp_json_encode($params));
+
+ return $this->create_product($frontend_request);
+ }
+
+ public function update_frontend_product(WP_REST_Request $request): WP_REST_Response {
+ $id = (int) $request->get_param('id');
+ $post = get_post($id);
+ if (!$post || $post->post_type !== 'aksrt_product') {
+ return $this->error('商品不存在', 404);
+ }
+
+ if ((int) $post->post_author !== get_current_user_id() && !current_user_can('manage_options')) {
+ return $this->error('没有权限编辑该商品', 403);
+ }
+
+ return $this->update_product($request);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller-fulfillment-operations.php b/inc/classes/admin/class-aksrt-admin-shop-controller-fulfillment-operations.php
new file mode 100644
index 00000000..f3dc0052
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-shop-controller-fulfillment-operations.php
@@ -0,0 +1,354 @@
+success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
+ }
+
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
+ $status = sanitize_key((string) $request->get_param('status'));
+ $stage = sanitize_key((string) $request->get_param('stage'));
+ $order = sanitize_text_field((string) $request->get_param('order_number'));
+ $type = sanitize_key((string) $request->get_param('type'));
+ $date_from = sanitize_text_field((string) $request->get_param('date_from'));
+ $date_to = sanitize_text_field((string) $request->get_param('date_to'));
+
+ $result = AKSRT_After_Sale::get_instance()->get_list([
+ 'limit' => $per_page,
+ 'offset' => ($page - 1) * $per_page,
+ 'status' => $status,
+ 'stage' => $stage,
+ 'order_number' => $order,
+ 'type' => $type,
+ 'date_from' => $date_from,
+ 'date_to' => $date_to,
+ 'include_order_context' => true,
+ ]);
+
+ $stats = AKSRT_After_Sale::get_instance()->get_stats();
+
+ return $this->success(['data' => array_merge($result, [
+ 'stats' => $stats,
+ 'filters' => [
+ 'status' => $status,
+ 'stage' => $stage,
+ 'order_number' => $order,
+ 'type' => $type,
+ 'date_from' => $date_from,
+ 'date_to' => $date_to,
+ ],
+ 'options' => [
+ 'status' => AKSRT_After_Sale::get_status_options(),
+ 'stage' => AKSRT_After_Sale::get_stage_options(),
+ 'type' => AKSRT_After_Sale::get_type_options(),
+ ],
+ ])]);
+ }
+
+ /**
+ * POST /shop/after-sale — 创建售后工单
+ */
+ public function create_after_sale(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_After_Sale')) {
+ return $this->error('售后系统未启用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
+ $reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
+
+ if ($order_number === '' || $reason === '') {
+ return $this->error('订单号和原因不能为空');
+ }
+
+ $order = class_exists('AKSRT_Order_Repository')
+ ? AKSRT_Order_Repository::get_instance()->get($order_number)
+ : null;
+
+ if (!$order) {
+ return $this->error(__('订单不存在', 'aksrt'), 404);
+ }
+
+ $params['user_id'] = (int) ($params['user_id'] ?? ($order['user_id'] ?? 0));
+ if ($params['user_id'] <= 0) {
+ return $this->error(__('订单未绑定有效用户', 'aksrt'));
+ }
+
+ $id = AKSRT_After_Sale::get_instance()->create($params);
+
+ if ($id > 0) {
+ return $this->success([
+ 'message' => __('售后工单已创建', 'aksrt'),
+ 'data' => ['id' => $id],
+ ], 201);
+ }
+
+ return $this->error(__('创建失败', 'aksrt'));
+ }
+
+ /**
+ * POST /shop/after-sale/{id}/update-status — 更新售后工单状态
+ */
+ public function update_after_sale_status(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_After_Sale')) {
+ return $this->error('售后系统未启用', 500);
+ }
+
+ $id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $new_status = sanitize_key((string) ($params['status'] ?? ''));
+ $note = sanitize_textarea_field((string) ($params['operator_note'] ?? ''));
+
+ if ($id <= 0 || $new_status === '') {
+ return $this->error('参数无效');
+ }
+
+ $success = AKSRT_After_Sale::get_instance()->update_status(
+ $id,
+ $new_status,
+ get_current_user_id(),
+ $note
+ );
+
+ if ($success) {
+ return $this->success(['message' => __('状态已更新', 'aksrt')]);
+ }
+ return $this->error(__('更新失败', 'aksrt'));
+ }
+
+ /**
+ * GET /shop/shipping — 发货记录列表
+ */
+ public function get_shipping_list(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shipping')) {
+ return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
+ }
+
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
+ $status = sanitize_key((string) $request->get_param('status'));
+ $order = sanitize_text_field((string) $request->get_param('order_number'));
+ $date_from = sanitize_text_field((string) $request->get_param('date_from'));
+ $date_to = sanitize_text_field((string) $request->get_param('date_to'));
+ $tracking_no = sanitize_text_field((string) $request->get_param('tracking_no'));
+
+ $result = AKSRT_Shipping::get_instance()->get_list([
+ 'limit' => $per_page,
+ 'offset' => ($page - 1) * $per_page,
+ 'status' => $status,
+ 'order_number' => $order,
+ 'date_from' => $date_from,
+ 'date_to' => $date_to,
+ 'tracking_no' => $tracking_no,
+ 'include_order_context' => true,
+ ]);
+
+ $stats = AKSRT_Shipping::get_instance()->get_stats();
+
+ return $this->success(['data' => array_merge($result, [
+ 'stats' => $stats,
+ 'filters' => [
+ 'status' => $status,
+ 'order_number' => $order,
+ 'date_from' => $date_from,
+ 'date_to' => $date_to,
+ 'tracking_no' => $tracking_no,
+ ],
+ 'options' => [
+ 'status' => AKSRT_Shipping::get_status_options(),
+ 'carrier' => AKSRT_Shipping::get_carrier_options(),
+ ],
+ ])]);
+ }
+
+ /**
+ * POST /shop/shipping — 创建发货记录
+ */
+ public function create_shipping(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shipping')) {
+ return $this->error('物流系统未启用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
+ $carrier = sanitize_text_field((string) ($params['carrier'] ?? ''));
+ $tracking_no = sanitize_text_field((string) ($params['tracking_no'] ?? ''));
+
+ if ($order_number === '' || $carrier === '' || $tracking_no === '') {
+ return $this->error('订单号、快递公司和物流单号不能为空');
+ }
+
+ $order = class_exists('AKSRT_Order_Repository')
+ ? AKSRT_Order_Repository::get_instance()->get($order_number)
+ : null;
+
+ if (!$order) {
+ return $this->error(__('订单不存在', 'aksrt'), 404);
+ }
+
+ $params['operator_id'] = get_current_user_id();
+
+ $id = AKSRT_Shipping::get_instance()->create($params);
+
+ if ($id > 0) {
+ return $this->success([
+ 'message' => __('发货记录已创建', 'aksrt'),
+ 'data' => ['id' => $id],
+ ], 201);
+ }
+
+ return $this->error(__('创建失败', 'aksrt'));
+ }
+
+ /**
+ * POST /shop/shipping/{id}/update-status — 更新发货状态
+ */
+ public function update_shipping_status(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shipping')) {
+ return $this->error('物流系统未启用', 500);
+ }
+
+ $id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $new_status = sanitize_key((string) ($params['status'] ?? ''));
+ $note = sanitize_textarea_field((string) ($params['note'] ?? ''));
+
+ if ($id <= 0 || $new_status === '') {
+ return $this->error('参数无效');
+ }
+
+ $success = AKSRT_Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id());
+
+ if ($success) {
+ return $this->success(['message' => __('状态已更新', 'aksrt')]);
+ }
+ return $this->error(__('更新失败', 'aksrt'));
+ }
+
+ public function get_after_sale_detail(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_After_Sale')) {
+ return $this->error('售后系统未启用', 500);
+ }
+
+ $id = (int) $request->get_param('id');
+ if ($id <= 0) {
+ return $this->error('参数无效');
+ }
+
+ $record = AKSRT_After_Sale::get_instance()->get_with_order_context($id);
+ if (!$record) {
+ return $this->error(__('售后工单不存在', 'aksrt'), 404);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'record' => $record,
+ 'timeline' => $this->build_after_sale_timeline($record),
+ ],
+ ]);
+ }
+
+ public function get_shipping_detail(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shipping')) {
+ return $this->error('物流系统未启用', 500);
+ }
+
+ $id = (int) $request->get_param('id');
+ if ($id <= 0) {
+ return $this->error('参数无效');
+ }
+
+ $record = AKSRT_Shipping::get_instance()->get_with_order_context($id);
+ if (!$record) {
+ return $this->error(__('发货记录不存在', 'aksrt'), 404);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'record' => $record,
+ 'timeline' => $this->build_shipping_timeline($record),
+ ],
+ ]);
+ }
+
+ public function batch_update_shipping_status(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shipping')) {
+ return $this->error('物流系统未启用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $ids = is_array($params['ids'] ?? null) ? $params['ids'] : [];
+ $status = sanitize_key((string) ($params['status'] ?? ''));
+ $note = sanitize_textarea_field((string) ($params['note'] ?? ''));
+
+ if (empty($ids) || $status === '') {
+ return $this->error('参数无效');
+ }
+
+ $result = AKSRT_Shipping::get_instance()->batch_update_status($ids, $status, $note, get_current_user_id());
+
+ return $this->success([
+ 'message' => __('批量状态更新已执行', 'aksrt'),
+ 'data' => $result,
+ ]);
+ }
+
+ private function build_shipping_timeline(array $record): array {
+ $timeline = [];
+
+ foreach ([
+ 'created_at' => __('创建发货记录', 'aksrt'),
+ 'updated_at' => __('更新物流状态', 'aksrt'),
+ 'delivered_at' => __('确认签收', 'aksrt'),
+ ] as $field => $label) {
+ if (!empty($record[$field])) {
+ $timeline[] = [
+ 'time' => (string) $record[$field],
+ 'label' => $label,
+ ];
+ }
+ }
+
+ usort($timeline, static function ($a, $b) {
+ return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
+ });
+
+ return $timeline;
+ }
+
+ private function build_after_sale_timeline(array $record): array {
+ $timeline = [];
+
+ foreach ([
+ 'created_at' => __('提交售后申请', 'aksrt'),
+ 'updated_at' => __('更新售后状态', 'aksrt'),
+ 'resolved_at' => __('售后处理结束', 'aksrt'),
+ ] as $field => $label) {
+ if (!empty($record[$field])) {
+ $timeline[] = [
+ 'time' => (string) $record[$field],
+ 'label' => $label,
+ ];
+ }
+ }
+
+ usort($timeline, static function ($a, $b) {
+ return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
+ });
+
+ return $timeline;
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller-order-operations.php b/inc/classes/admin/class-aksrt-admin-shop-controller-order-operations.php
new file mode 100644
index 00000000..b9814fed
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-shop-controller-order-operations.php
@@ -0,0 +1,136 @@
+get_param('page'));
+ $per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
+ $search = sanitize_text_field((string) $request->get_param('search'));
+ $status = sanitize_key((string) $request->get_param('status'));
+
+ if (class_exists('AKSRT_Order_Repository')) {
+ $repo = AKSRT_Order_Repository::get_instance();
+
+ $args = [
+ 'limit' => $per_page,
+ 'offset' => ($page - 1) * $per_page,
+ ];
+ if ($status !== '' && $status !== 'all') {
+ $args['status'] = $status;
+ }
+
+ $orders = $repo->get_orders_admin($args, $search);
+ $total = $repo->get_orders_count($args, $search);
+
+ return $this->success([
+ 'data' => [
+ 'items' => $orders,
+ 'total' => $total,
+ 'page' => $page,
+ 'total_pages' => max(1, (int) ceil($total / $per_page)),
+ ],
+ ]);
+ }
+
+ return $this->error('订单仓库不可用', 500);
+ }
+
+ /**
+ * GET /shop/orders/{order_number} — 单个订单详情(含上下文)
+ */
+ public function get_order(WP_REST_Request $request): WP_REST_Response {
+ $order_number = sanitize_text_field((string) $request->get_param('order_number'));
+
+ if (!class_exists('AKSRT_Order_Repository')) {
+ return $this->error('订单服务不可用', 500);
+ }
+
+ $repo = AKSRT_Order_Repository::get_instance();
+ $order = $repo->get($order_number);
+ if (!$order) {
+ return $this->error('订单不存在', 404);
+ }
+
+ $context = $repo->get_admin_order_context($order_number);
+ $timeline = [];
+ if (class_exists('AKSRT_Shop')) {
+ $shipping_records = class_exists('AKSRT_Shipping') ? AKSRT_Shipping::get_instance()->get_by_order($order_number) : [];
+ $after_sale_result = class_exists('AKSRT_After_Sale') ? AKSRT_After_Sale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20]) : ['items' => []];
+ $after_sale_items = $after_sale_result['items'] ?? [];
+
+ $timeline = [];
+ $date_fields = [
+ 'created_at' => '订单创建',
+ 'paid_at' => '完成支付',
+ ];
+ foreach ($date_fields as $field => $label) {
+ if (!empty($order[$field])) {
+ $timeline[] = ['time' => $order[$field], 'title' => $label, 'type' => 'order'];
+ }
+ }
+ foreach ($shipping_records as $record) {
+ $timeline[] = [
+ 'time' => $record['created_at'] ?? '',
+ 'title' => '物流更新:' . ($record['status_label'] ?? $record['status'] ?? ''),
+ 'detail' => ($record['carrier_label'] ?? '') . ' / ' . ($record['tracking_no'] ?? ''),
+ 'type' => 'shipping',
+ ];
+ }
+ foreach ($after_sale_items as $record) {
+ $timeline[] = [
+ 'time' => $record['created_at'] ?? '',
+ 'title' => '售后:' . ($record['type_label'] ?? '') . ' — ' . ($record['status_label'] ?? ''),
+ 'detail' => $record['reason'] ?? '',
+ 'type' => 'after_sale',
+ ];
+ }
+ usort($timeline, static function ($a, $b) {
+ return strcmp((string) ($b['time'] ?? ''), (string) ($a['time'] ?? ''));
+ });
+ }
+
+ return $this->success([
+ 'data' => $order,
+ 'context' => $context,
+ 'timeline' => $timeline,
+ ]);
+ }
+
+ /**
+ * POST /shop/orders/update-status — 更新订单状态
+ * Canonical field: order_number (前端必须发送 order_number)
+ */
+ public function update_order_status(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
+ $new_status = sanitize_key((string) ($params['status'] ?? ''));
+ $note = sanitize_textarea_field((string) ($params['note'] ?? ''));
+
+ if ($order_number === '' || $new_status === '') {
+ return $this->error('参数无效');
+ }
+
+ $success = false;
+ if (class_exists('AKSRT_Order_Repository')) {
+ $success = AKSRT_Order_Repository::get_instance()->update_status($order_number, $new_status, [
+ 'note' => $note,
+ 'force' => true,
+ ]);
+ }
+
+ if ($success) {
+ return $this->success(['message' => __('订单状态已更新', 'aksrt')]);
+ }
+ return $this->error(__('更新失败', 'aksrt'));
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller-product-admin-columns.php b/inc/classes/admin/class-aksrt-admin-shop-controller-product-admin-columns.php
new file mode 100644
index 00000000..25b28f59
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-shop-controller-product-admin-columns.php
@@ -0,0 +1,187 @@
+ $label) {
+ $new[$key] = $label;
+ if ($key === 'title') {
+ $new['product_price'] = __('价格', 'aksrt');
+ $new['product_stock'] = __('库存', 'aksrt');
+ $new['product_sales'] = __('销量', 'aksrt');
+ $new['product_stats'] = __('综合数据', 'aksrt');
+ $new['product_author_name'] = __('商家', 'aksrt');
+ $new['product_discount'] = __('优惠活动', 'aksrt');
+ }
+ }
+ return $new;
+ }
+
+ public function render_product_column(string $column, int $post_id): void {
+ switch ($column) {
+ case 'product_price':
+ $price = get_post_meta($post_id, '_aksrt_price', true);
+ echo esc_html(number_format_i18n((float) $price, 2));
+ break;
+ case 'product_stock':
+ $stock = get_post_meta($post_id, '_aksrt_stock', true);
+ $stock_type = get_post_meta($post_id, '_aksrt_stock_type', true);
+ if ($stock_type === 'opts') {
+ echo esc_html__('按选项', 'aksrt');
+ } elseif ((int) $stock === -1) {
+ echo esc_html__('无限', 'aksrt');
+ } else {
+ echo esc_html((string) (int) $stock);
+ }
+ break;
+ case 'product_sales':
+ echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_sales', true));
+ break;
+ case 'product_stats':
+ $views = (int) get_post_meta($post_id, '_aksrt_views', true);
+ $fav = (int) get_post_meta($post_id, '_aksrt_favorite_count', true);
+ printf(
+ esc_html__('阅读 %1$d / 收藏 %2$d', 'aksrt'),
+ $views,
+ $fav
+ );
+ break;
+ case 'product_author_name':
+ $post = get_post($post_id);
+ if ($post) {
+ $author = get_userdata($post->post_author);
+ echo esc_html($author ? $author->display_name : '—');
+ }
+ break;
+ case 'product_discount':
+ $terms = wp_get_object_terms($post_id, 'shop_discount', ['fields' => 'all']);
+ if (!is_wp_error($terms) && !empty($terms)) {
+ $term_ids = wp_list_pluck($terms, 'term_id');
+ $names = wp_list_pluck($terms, 'name');
+ echo ' ';
+ echo esc_html(implode(', ', $names));
+ } else {
+ echo ' —';
+ }
+ break;
+ }
+ }
+
+ public function sortable_product_columns(array $columns): array {
+ $columns['product_sales'] = 'product_sales';
+ return $columns;
+ }
+
+ public function render_product_quick_edit(string $column_name, string $post_type): void {
+ if ($column_name !== 'product_discount' || $post_type !== 'aksrt_product') {
+ return;
+ }
+ wp_nonce_field('aksrt_product_quick_edit', 'aksrt_product_qe_nonce');
+ $discounts = get_terms([
+ 'taxonomy' => 'shop_discount',
+ 'hide_empty' => false,
+ ]);
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+ name); ?>
+
+
+
+
+
+
+ 0 && term_exists($term_id, 'shop_discount')) {
+ wp_set_object_terms($post_id, [$term_id], 'shop_discount');
+ }
+ }
+ }
+
+ public function product_quick_edit_script(): void {
+ $screen = get_current_screen();
+ if (!$screen || $screen->post_type !== 'aksrt_product') {
+ return;
+ }
+ ?>
+
+ publish ?? 0);
+ }
+
+ $order_stats = ['total' => 0, 'pending' => 0, 'paid' => 0, 'revenue' => 0];
+ if (class_exists('AKSRT_Order_Repository')) {
+ $order_stats = AKSRT_Order_Repository::get_instance()->get_stats();
+ }
+
+ $revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0];
+ if (class_exists('AKSRT_Revenue')) {
+ $revenue_stats = AKSRT_Revenue::get_instance()->get_stats();
+ }
+
+ $gateway_summary = [];
+ if (class_exists('AKSRT_Payment_Gateway')) {
+ $gateway_summary = AKSRT_Payment_Gateway::get_instance()->get_status_summary();
+ }
+
+ return $this->success([
+ 'data' => [
+ 'enabled' => true,
+ 'product_count' => $product_count,
+ 'order_count' => $order_stats['total'],
+ 'order_pending' => $order_stats['pending'] ?? 0,
+ 'revenue_total' => $order_stats['revenue'] ?? 0,
+ 'revenue_pending' => $revenue_stats['pending'] ?? 0,
+ 'gateways' => $gateway_summary,
+ ],
+ ]);
+ }
+
+ /**
+ * GET /shop/products — 商品列表,支持 search、status、page、per_page
+ */
+ public function get_products(WP_REST_Request $request): WP_REST_Response {
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
+ $search = sanitize_text_field((string) $request->get_param('search'));
+ $status = sanitize_key((string) $request->get_param('status'));
+
+ $query_args = [
+ 'post_type' => 'aksrt_product',
+ 'posts_per_page' => $per_page,
+ 'paged' => $page,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ ];
+
+ if ($status !== '') {
+ $query_args['post_status'] = $status;
+ } else {
+ $query_args['post_status'] = ['publish', 'draft', 'pending'];
+ }
+
+ if ($search !== '') {
+ $query_args['s'] = $search;
+ }
+
+ $query = new WP_Query($query_args);
+
+ $products = [];
+ while ($query->have_posts()) {
+ $query->the_post();
+ $id = get_the_ID();
+ $products[] = [
+ 'id' => $id,
+ 'title' => get_the_title(),
+ 'price' => (float) get_post_meta($id, '_aksrt_price', true),
+ 'image' => get_the_post_thumbnail_url($id, 'medium') ?: '',
+ 'stock' => (int) get_post_meta($id, '_aksrt_stock', true),
+ 'sales' => (int) get_post_meta($id, '_aksrt_sales', true),
+ 'status' => get_post_status(),
+ 'created' => get_the_date('Y-m-d H:i'),
+ ];
+ }
+ wp_reset_postdata();
+
+ return $this->success([
+ 'data' => [
+ 'items' => $products,
+ 'total' => (int) $query->found_posts,
+ 'total_pages' => (int) $query->max_num_pages,
+ 'page' => $page,
+ ],
+ ]);
+ }
+
+ /**
+ * GET /shop/products/{id} — 单个商品详情
+ */
+ public function get_product(WP_REST_Request $request): WP_REST_Response {
+ $id = (int) $request->get_param('id');
+ $post = get_post($id);
+
+ if (!$post || $post->post_type !== 'aksrt_product') {
+ return $this->error('商品不存在', 404);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'id' => $post->ID,
+ 'title' => $post->post_title,
+ 'content' => $post->post_content,
+ 'excerpt' => $post->post_excerpt,
+ 'price' => (float) get_post_meta($post->ID, '_aksrt_price', true),
+ 'image' => (string) (get_post_meta($post->ID, '_aksrt_product_image', true) ?: get_the_post_thumbnail_url($post->ID, 'medium') ?: ''),
+ 'stock' => (int) get_post_meta($post->ID, '_aksrt_stock', true),
+ 'sales' => (int) get_post_meta($post->ID, '_aksrt_sales', true),
+ 'status' => $post->post_status,
+ 'created' => get_the_date('Y-m-d H:i', $post),
+ 'modified' => get_the_modified_date('Y-m-d H:i', $post),
+ 'categories' => wp_get_post_terms($post->ID, 'aksrt_product_cat', ['fields' => 'names']) ?: [],
+ 'tags' => wp_get_post_terms($post->ID, 'aksrt_product_tag', ['fields' => 'names']) ?: [],
+ 'gallery' => get_post_meta($post->ID, '_aksrt_product_gallery', true) ?: [],
+ 'shipping_mode' => (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true),
+ 'shipping_fee' => (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true),
+ 'after_sale_policy' => (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true),
+ 'support_refund' => get_post_meta($post->ID, '_aksrt_support_refund', true) !== '0',
+ ],
+ ]);
+ }
+
+ /**
+ * POST /shop/products — 创建商品
+ */
+ public function create_product(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+
+ $title = sanitize_text_field((string) ($params['title'] ?? ''));
+ $content = wp_kses_post((string) ($params['content'] ?? ''));
+ $excerpt = sanitize_textarea_field((string) ($params['excerpt'] ?? ''));
+ $price = (float) ($params['price'] ?? 0);
+ $stock = (int) ($params['stock'] ?? 0);
+ $status = sanitize_key((string) ($params['status'] ?? 'publish'));
+ $image = esc_url_raw((string) ($params['image'] ?? ''));
+ $gallery = is_array($params['gallery'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['gallery']))) : [];
+ $shipping_mode = sanitize_key((string) ($params['shipping_mode'] ?? 'express'));
+ $shipping_fee = max(0, (float) ($params['shipping_fee'] ?? 0));
+ $after_sale_policy = sanitize_textarea_field((string) ($params['after_sale_policy'] ?? ''));
+ $support_refund = !isset($params['support_refund']) || !empty($params['support_refund']);
+ $author_id = max(0, (int) ($params['author_id'] ?? 0));
+
+ if ($title === '') {
+ return $this->error('商品名称不能为空');
+ }
+
+ $valid_statuses = ['publish', 'draft', 'pending'];
+ if (!in_array($status, $valid_statuses, true)) {
+ $status = 'draft';
+ }
+
+ $post_id = wp_insert_post([
+ 'post_type' => 'aksrt_product',
+ 'post_title' => $title,
+ 'post_content' => $content,
+ 'post_excerpt' => $excerpt,
+ 'post_status' => $status,
+ 'post_author' => $author_id > 0 ? $author_id : get_current_user_id(),
+ 'meta_input' => [
+ '_aksrt_price' => max(0, $price),
+ '_aksrt_stock' => max(0, $stock),
+ '_aksrt_sales' => 0,
+ '_aksrt_score' => 0,
+ '_aksrt_views' => 0,
+ '_aksrt_favorite_count' => 0,
+ '_aksrt_product_gallery' => $gallery,
+ '_aksrt_shipping_mode' => $shipping_mode,
+ '_aksrt_shipping_fee' => $shipping_fee,
+ '_aksrt_after_sale_policy' => $after_sale_policy,
+ '_aksrt_support_refund' => $support_refund ? '1' : '0',
+ '_aksrt_product_video' => is_array($params['product_video'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['product_video']))) : [],
+ '_aksrt_product_params' => sanitize_textarea_field((string) ($params['product_params'] ?? '')),
+ '_aksrt_pay_modo' => sanitize_key((string) ($params['pay_modo'] ?? '0')),
+ '_aksrt_start_price' => max(0, (float) ($params['start_price'] ?? 0)),
+ '_aksrt_product_rebate' => sanitize_text_field((string) ($params['product_rebate'] ?? '')),
+ '_aksrt_limit_buy' => max(-1, min(9999, (int) ($params['limit_buy'] ?? -1))),
+ '_aksrt_limit_buy_note' => sanitize_text_field((string) ($params['limit_buy_note'] ?? '')),
+ '_aksrt_stock_type' => sanitize_key((string) ($params['stock_type'] ?? 'all')),
+ '_aksrt_user_required' => sanitize_textarea_field((string) ($params['user_required'] ?? '')),
+ '_aksrt_shipping_type' => sanitize_key((string) ($params['shipping_type'] ?? $shipping_mode)),
+ '_aksrt_shipping_delivery_desc' => sanitize_text_field((string) ($params['shipping_delivery_desc'] ?? '')),
+ '_aksrt_shipping_fee_type' => sanitize_key((string) ($params['shipping_fee_type'] ?? 'free')),
+ '_aksrt_shipping_fee_free_amount'=> max(0, (float) ($params['shipping_fee_free_amount'] ?? 0)),
+ '_aksrt_after_sale_matrix' => sanitize_textarea_field((string) ($params['after_sale_matrix'] ?? '')),
+ '_aksrt_service' => sanitize_textarea_field((string) ($params['service'] ?? '')),
+ '_aksrt_single_tab' => sanitize_textarea_field((string) ($params['single_tab'] ?? '')),
+ '_aksrt_content_layout' => sanitize_key((string) ($params['content_layout'] ?? '')),
+ '_aksrt_content_show_bg' => sanitize_text_field((string) ($params['content_show_bg'] ?? '')),
+ '_aksrt_delivery_note' => sanitize_text_field((string) ($params['delivery_note'] ?? '')),
+ '_aksrt_marketing_headline' => sanitize_text_field((string) ($params['marketing_headline'] ?? '')),
+ '_aksrt_shipping_tip' => sanitize_text_field((string) ($params['shipping_tip'] ?? '')),
+ '_aksrt_service_tip' => sanitize_text_field((string) ($params['service_tip'] ?? '')),
+ ],
+ ], true);
+
+ if (is_wp_error($post_id)) {
+ return $this->error($post_id->get_error_message());
+ }
+
+ if (!empty($params['categories']) && is_array($params['categories'])) {
+ wp_set_object_terms($post_id, $params['categories'], 'aksrt_product_cat');
+ }
+ if (!empty($params['tags']) && is_array($params['tags'])) {
+ wp_set_object_terms($post_id, $params['tags'], 'aksrt_product_tag');
+ }
+ if ($image !== '') {
+ update_post_meta($post_id, '_aksrt_product_image', $image);
+ }
+
+ return $this->success([
+ 'message' => __('商品已创建', 'aksrt'),
+ 'data' => ['id' => $post_id],
+ ], 201);
+ }
+
+ /**
+ * POST /shop/products/{id} — 更新商品(含上下架)
+ */
+ public function update_product(WP_REST_Request $request): WP_REST_Response {
+ $id = (int) $request->get_param('id');
+ $post = get_post($id);
+
+ if (!$post || $post->post_type !== 'aksrt_product') {
+ return $this->error('商品不存在', 404);
+ }
+
+ $params = $this->get_json_params($request);
+ $update = ['ID' => $id];
+
+ if (isset($params['title'])) {
+ $update['post_title'] = sanitize_text_field($params['title']);
+ }
+ if (isset($params['content'])) {
+ $update['post_content'] = wp_kses_post($params['content']);
+ }
+ if (isset($params['excerpt'])) {
+ $update['post_excerpt'] = sanitize_textarea_field($params['excerpt']);
+ }
+ if (isset($params['status'])) {
+ $new_status = sanitize_key($params['status']);
+ $valid_statuses = ['publish', 'draft', 'pending', 'trash'];
+ if (in_array($new_status, $valid_statuses, true)) {
+ $update['post_status'] = $new_status;
+ }
+ }
+
+ $meta = [];
+ if (isset($params['price'])) {
+ $meta['_aksrt_price'] = max(0, (float) $params['price']);
+ }
+ if (isset($params['stock'])) {
+ $meta['_aksrt_stock'] = max(0, (int) $params['stock']);
+ }
+ if (isset($params['gallery']) && is_array($params['gallery'])) {
+ $meta['_aksrt_product_gallery'] = array_values(array_filter(array_map('esc_url_raw', $params['gallery'])));
+ }
+ if (isset($params['shipping_mode'])) {
+ $meta['_aksrt_shipping_mode'] = sanitize_key((string) $params['shipping_mode']);
+ }
+ if (isset($params['shipping_fee'])) {
+ $meta['_aksrt_shipping_fee'] = max(0, (float) $params['shipping_fee']);
+ }
+ if (isset($params['after_sale_policy'])) {
+ $meta['_aksrt_after_sale_policy'] = sanitize_textarea_field((string) $params['after_sale_policy']);
+ }
+ if (isset($params['support_refund'])) {
+ $meta['_aksrt_support_refund'] = !empty($params['support_refund']) ? '1' : '0';
+ }
+ if (isset($params['product_video']) && is_array($params['product_video'])) {
+ $meta['_aksrt_product_video'] = array_values(array_filter(array_map('esc_url_raw', $params['product_video'])));
+ }
+ if (isset($params['product_params'])) {
+ $meta['_aksrt_product_params'] = sanitize_textarea_field((string) $params['product_params']);
+ }
+ if (isset($params['pay_modo'])) {
+ $meta['_aksrt_pay_modo'] = sanitize_key((string) $params['pay_modo']);
+ }
+ if (isset($params['start_price'])) {
+ $meta['_aksrt_start_price'] = max(0, (float) $params['start_price']);
+ }
+ if (isset($params['product_rebate'])) {
+ $meta['_aksrt_product_rebate'] = sanitize_text_field((string) $params['product_rebate']);
+ }
+ if (isset($params['limit_buy'])) {
+ $meta['_aksrt_limit_buy'] = max(-1, min(9999, (int) $params['limit_buy']));
+ }
+ if (isset($params['limit_buy_note'])) {
+ $meta['_aksrt_limit_buy_note'] = sanitize_text_field((string) $params['limit_buy_note']);
+ }
+ if (isset($params['stock_type'])) {
+ $meta['_aksrt_stock_type'] = sanitize_key((string) $params['stock_type']);
+ }
+ if (isset($params['stock_opts']) && is_array($params['stock_opts'])) {
+ $meta['_aksrt_stock_opts'] = wp_json_encode($params['stock_opts']);
+ }
+ if (isset($params['user_required'])) {
+ $meta['_aksrt_user_required'] = sanitize_textarea_field((string) $params['user_required']);
+ }
+ if (isset($params['shipping_type'])) {
+ $meta['_aksrt_shipping_type'] = sanitize_key((string) $params['shipping_type']);
+ }
+ if (isset($params['shipping_delivery_desc'])) {
+ $meta['_aksrt_shipping_delivery_desc'] = sanitize_text_field((string) $params['shipping_delivery_desc']);
+ }
+ if (isset($params['auto_delivery']) && is_array($params['auto_delivery'])) {
+ $meta['_aksrt_auto_delivery'] = wp_json_encode($params['auto_delivery']);
+ }
+ if (isset($params['shipping_fee_type'])) {
+ $meta['_aksrt_shipping_fee_type'] = sanitize_key((string) $params['shipping_fee_type']);
+ }
+ if (isset($params['shipping_fee_free_amount'])) {
+ $meta['_aksrt_shipping_fee_free_amount'] = max(0, (float) $params['shipping_fee_free_amount']);
+ }
+ if (isset($params['after_sale_matrix'])) {
+ $meta['_aksrt_after_sale_matrix'] = sanitize_textarea_field((string) $params['after_sale_matrix']);
+ }
+ if (isset($params['service'])) {
+ $meta['_aksrt_service'] = sanitize_textarea_field((string) $params['service']);
+ }
+ if (isset($params['single_tab'])) {
+ $meta['_aksrt_single_tab'] = sanitize_textarea_field((string) $params['single_tab']);
+ }
+ if (isset($params['content_layout'])) {
+ $meta['_aksrt_content_layout'] = sanitize_key((string) $params['content_layout']);
+ }
+ if (isset($params['content_show_bg'])) {
+ $meta['_aksrt_content_show_bg'] = sanitize_text_field((string) $params['content_show_bg']);
+ }
+ if (isset($params['delivery_note'])) {
+ $meta['_aksrt_delivery_note'] = sanitize_text_field((string) $params['delivery_note']);
+ }
+ if (isset($params['marketing_headline'])) {
+ $meta['_aksrt_marketing_headline'] = sanitize_text_field((string) $params['marketing_headline']);
+ }
+ if (isset($params['shipping_tip'])) {
+ $meta['_aksrt_shipping_tip'] = sanitize_text_field((string) $params['shipping_tip']);
+ }
+ if (isset($params['service_tip'])) {
+ $meta['_aksrt_service_tip'] = sanitize_text_field((string) $params['service_tip']);
+ }
+ if (!empty($meta)) {
+ $update['meta_input'] = $meta;
+ }
+
+ $result = wp_update_post($update, true);
+
+ if (is_wp_error($result)) {
+ return $this->error($result->get_error_message());
+ }
+
+ if (isset($params['categories']) && is_array($params['categories'])) {
+ wp_set_object_terms($id, $params['categories'], 'aksrt_product_cat');
+ }
+ if (isset($params['tags']) && is_array($params['tags'])) {
+ wp_set_object_terms($id, $params['tags'], 'aksrt_product_tag');
+ }
+ if (isset($params['image'])) {
+ update_post_meta($id, '_aksrt_product_image', esc_url_raw((string) $params['image']));
+ }
+
+ return $this->success([
+ 'message' => __('商品已更新', 'aksrt'),
+ ]);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller-variant-export-operations.php b/inc/classes/admin/class-aksrt-admin-shop-controller-variant-export-operations.php
new file mode 100644
index 00000000..5caa99cc
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-shop-controller-variant-export-operations.php
@@ -0,0 +1,111 @@
+get_param('id');
+ $product = get_post($product_id);
+ if (!$product || $product->post_type !== 'aksrt_product') {
+ return $this->error('商品不存在', 404);
+ }
+
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->success(['data' => ['variants' => []]]);
+ }
+
+ $variants = AKSRT_Shop::get_instance()->get_variants($product_id);
+ return $this->success(['data' => ['variants' => $variants]]);
+ }
+
+ public function save_product_variants(WP_REST_Request $request): WP_REST_Response {
+ $product_id = (int) $request->get_param('id');
+ $product = get_post($product_id);
+ if (!$product || $product->post_type !== 'aksrt_product') {
+ return $this->error('商品不存在', 404);
+ }
+
+ $params = $request->get_json_params() ?? [];
+ $variants = $params['variants'] ?? [];
+ if (!is_array($variants)) {
+ return $this->error('变体数据格式错误');
+ }
+
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城系统未启用', 501);
+ }
+
+ $saved = AKSRT_Shop::get_instance()->save_variants($product_id, $variants);
+ return $this->success(['message' => '变体已保存', 'data' => ['variants' => $saved]]);
+ }
+
+ public function delete_variant(WP_REST_Request $request): WP_REST_Response {
+ $variant_id = (int) $request->get_param('id');
+
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城系统未启用', 501);
+ }
+
+ $result = AKSRT_Shop::get_instance()->delete_variant($variant_id);
+ if (!$result) {
+ return $this->error('删除失败', 500);
+ }
+
+ return $this->success(['message' => '变体已删除']);
+ }
+
+ public function export_orders(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $args = [
+ 'status' => sanitize_key((string) $request->get_param('status')),
+ 'date_from' => sanitize_text_field((string) $request->get_param('date_from')),
+ 'date_to' => sanitize_text_field((string) $request->get_param('date_to')),
+ 'limit' => max(1, min(5000, (int) ($request->get_param('limit') ?: 1000))),
+ ];
+
+ $result = AKSRT_Shop::get_instance()->export_orders_csv($args);
+
+ return new WP_REST_Response($result['csv'], 200, [
+ 'Content-Type' => 'text/csv; charset=utf-8',
+ 'Content-Disposition' => 'attachment; filename="orders-' . date('Ymd-His') . '.csv"',
+ 'X-Order-Count' => $result['count'],
+ ]);
+ }
+
+ public function confirm_receipt(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Shop')) {
+ return $this->error('商城服务不可用', 500);
+ }
+
+ $order_number = sanitize_text_field((string) $request->get_param('order_number'));
+ $result = AKSRT_Shop::get_instance()->confirm_receipt(get_current_user_id(), $order_number);
+ if (empty($result['success'])) {
+ return $this->error($result['message'] ?? '确认收货失败');
+ }
+
+ return $this->success(['data' => $result['data']]);
+ }
+
+ public function get_after_sale_reasons(WP_REST_Request $request): WP_REST_Response {
+ if (class_exists('AKSRT_After_Sale')) {
+ return $this->success(['data' => AKSRT_After_Sale::get_reason_options()]);
+ }
+ return $this->success(['data' => ['refund' => [], 'return' => []]]);
+ }
+
+ public function get_frontend_after_sale_reasons(WP_REST_Request $request): WP_REST_Response {
+ if (class_exists('AKSRT_After_Sale')) {
+ return $this->success(['data' => AKSRT_After_Sale::get_reason_options()]);
+ }
+ return $this->success(['data' => ['refund' => [], 'return' => []]]);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller.php b/inc/classes/admin/class-aksrt-admin-shop-controller.php
new file mode 100644
index 00000000..5a8a8ff5
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-shop-controller.php
@@ -0,0 +1,369 @@
+admin = $admin;
+ }
+
+ public function register_routes(): void {
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/checkout/preview', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'preview_checkout'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_frontend_order'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_frontend_orders'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9]+)/pay', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'pay_frontend_order'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9]+)/cancel', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'cancel_frontend_order'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9]+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_frontend_order'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9]+)/after-sale', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_frontend_after_sale'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ // ── Address book CRUD (frontend) ──
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_frontend_addresses'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_frontend_address'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_frontend_address'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'delete_frontend_address'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P\d+)/default', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'set_default_frontend_address'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/products', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_frontend_product'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_frontend_product'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ // Cart endpoints (§6.4)
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_cart'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/add', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'handle_add_to_cart'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/reduce', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'handle_reduce_from_cart'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'handle_remove_from_cart'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove-multi', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'handle_remove_multi_from_cart'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/empty', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'handle_empty_cart'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/count', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_cart_count'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ // Shop overview / dashboard
+ register_rest_route(self::API_NAMESPACE, '/shop/overview', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_overview'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Product list
+ register_rest_route(self::API_NAMESPACE, '/shop/products', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_products'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Product create
+ register_rest_route(self::API_NAMESPACE, '/shop/products', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_product'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Product update
+ register_rest_route(self::API_NAMESPACE, '/shop/products/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_product'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Product single read
+ register_rest_route(self::API_NAMESPACE, '/shop/products/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_product'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Order list
+ register_rest_route(self::API_NAMESPACE, '/shop/orders', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_orders'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Order single read
+ register_rest_route(self::API_NAMESPACE, '/shop/orders/(?P[A-Za-z0-9]+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_order'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Order status update — canonical field is order_number
+ register_rest_route(self::API_NAMESPACE, '/shop/orders/update-status', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_order_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Gateway status
+ register_rest_route(self::API_NAMESPACE, '/shop/gateways', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_gateways'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Coupon list (admin)
+ register_rest_route(self::API_NAMESPACE, '/shop/coupons', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_coupons'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Coupon create (admin)
+ register_rest_route(self::API_NAMESPACE, '/shop/coupons', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_coupon'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Coupon disable (admin)
+ register_rest_route(self::API_NAMESPACE, '/shop/coupons/(?P\d+)/disable', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'disable_coupon'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // ── After Sale ──
+
+ // After-sale list (admin)
+ register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_after_sale_list'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // After-sale create (admin)
+ register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_after_sale'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // After-sale status update (admin)
+ register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P\d+)/update-status', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_after_sale_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_after_sale_detail'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // ── Shipping ──
+
+ // Shipping list (admin)
+ register_rest_route(self::API_NAMESPACE, '/shop/shipping', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_shipping_list'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Shipping create (admin)
+ register_rest_route(self::API_NAMESPACE, '/shop/shipping', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'create_shipping'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Shipping status update (admin)
+ register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P\d+)/update-status', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_shipping_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_shipping_detail'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/shipping/batch-update-status', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'batch_update_shipping_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+
+ // ── R68: Order export + confirm receipt + after-sale reasons ──
+
+ register_rest_route(self::API_NAMESPACE, '/shop/orders/export', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'export_orders'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // ── R69: Frontend after-sale reasons (§6.8) ──
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/reasons', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_frontend_after_sale_reasons'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9]+)/confirm-receipt', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'confirm_receipt'],
+ 'permission_callback' => [$this, 'check_logged_in_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/shop/after-sale/reasons', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_after_sale_reasons'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // ── Product Variants / SKU ──
+
+ // List variants for a product
+ register_rest_route(self::API_NAMESPACE, '/shop/products/(?P\d+)/variants', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_product_variants'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Save variants for a product (replace all)
+ register_rest_route(self::API_NAMESPACE, '/shop/products/(?P\d+)/variants', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'save_product_variants'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Delete a variant
+ register_rest_route(self::API_NAMESPACE, '/shop/variants/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'delete_variant'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+ public function check_logged_in_permission(): bool {
+ return is_user_logged_in();
+ }
+
+}
diff --git a/inc/classes/admin/class-aksrt-admin-tools-controller.php b/inc/classes/admin/class-aksrt-admin-tools-controller.php
new file mode 100644
index 00000000..4a96f2bb
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-tools-controller.php
@@ -0,0 +1,285 @@
+ 'GET',
+ 'callback' => [$this, 'rest_health_email_verification'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Health: REST block toggle
+ register_rest_route(self::API_NAMESPACE, '/health/rest-block', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_set_rest_block'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Email rollback: list users with recent email changes
+ register_rest_route(self::API_NAMESPACE, '/email-rollback/users', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_email_rollback_list'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Email rollback: execute rollback
+ register_rest_route(self::API_NAMESPACE, '/email-rollback/users/(?P\d+)/rollback', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_email_rollback_execute'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Tools: system health (compact format for ExtensionTools.vue)
+ register_rest_route(self::API_NAMESPACE, '/tools/health', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_tools_health'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Tools: maintenance actions
+ register_rest_route(self::API_NAMESPACE, '/tools/maintenance', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'run_maintenance'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Tools: update center
+ register_rest_route(self::API_NAMESPACE, '/tools/update-info', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_update_info'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/tools/update-check', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'detect_update'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/tools/update-dismiss', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'dismiss_update'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+ /**
+ * GET /tools/health — compact health data for the tools page.
+ */
+ public function get_tools_health(): WP_REST_Response {
+ global $wpdb;
+
+ $upload_dir = wp_upload_dir();
+ $upload_ok = !empty($upload_dir['error']) === false && is_dir($upload_dir['basedir']);
+
+ $db_ok = true;
+ $db_status = '正常';
+ $expected_tables = [
+ $wpdb->prefix . 'aksrt_credit_log',
+ $wpdb->prefix . 'aksrt_signin_log',
+ $wpdb->prefix . 'aksrt_invite',
+ $wpdb->prefix . 'aksrt_dl_token',
+ $wpdb->prefix . 'aksrt_notification',
+ ];
+ $missing = [];
+ foreach ($expected_tables as $table) {
+ if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) !== $table) {
+ $missing[] = str_replace($wpdb->prefix, '', $table);
+ }
+ }
+ if (!empty($missing)) {
+ $db_ok = false;
+ $db_status = '缺少: ' . implode(', ', $missing);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'php_version' => PHP_VERSION,
+ 'wp_version' => get_bloginfo('version'),
+ 'db_status' => $db_status,
+ 'db_ok' => $db_ok,
+ 'upload_dir' => $upload_dir['basedir'],
+ 'upload_ok' => $upload_ok,
+ 'theme_version' => wp_get_theme()->get('Version') ?? 'unknown',
+ ],
+ ]);
+ }
+
+ /**
+ * POST /tools/maintenance — run a maintenance action.
+ */
+ public function run_maintenance(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $action = sanitize_key((string) ($params['action'] ?? ''));
+
+ switch ($action) {
+ case 'flush_cache':
+ // Flush WordPress object cache
+ wp_cache_flush();
+ // Clear transients
+ global $wpdb;
+ $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_aksrt_%' OR option_name LIKE '_transient_timeout_aksrt_%'");
+ return $this->success(['message' => __('缓存已清除', 'aksrt')]);
+
+ case 'optimize_db':
+ global $wpdb;
+ $tables = [
+ $wpdb->prefix . 'aksrt_credit_log',
+ $wpdb->prefix . 'aksrt_signin_log',
+ $wpdb->prefix . 'aksrt_invite',
+ $wpdb->prefix . 'aksrt_dl_token',
+ $wpdb->prefix . 'aksrt_notification',
+ ];
+ $optimized = 0;
+ foreach ($tables as $table) {
+ if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table) {
+ $wpdb->query("OPTIMIZE TABLE {$table}");
+ $optimized++;
+ }
+ }
+ return $this->success(['message' => sprintf(__('已优化 %d 个数据表', 'aksrt'), $optimized)]);
+
+ case 'rebuild_index':
+ // Rebuild search index / recount stats
+ if (class_exists('AKSRT_Forum') && method_exists(AKSRT_Forum::get_instance(), 'recount_stats')) {
+ AKSRT_Forum::get_instance()->recount_stats();
+ }
+ return $this->success(['message' => __('索引重建完成', 'aksrt')]);
+
+ default:
+ return $this->error('未知的维护操作: ' . esc_html($action));
+ }
+ }
+
+ public function get_update_info(): WP_REST_Response {
+ $theme = wp_get_theme();
+ $info = [
+ 'current_version' => $theme->get('Version') ?: '0.0.0',
+ 'name' => $theme->get('Name') ?: 'AKSRT',
+ 'update_available'=> false,
+ 'remote_version' => '',
+ 'download_url' => '',
+ 'changelog' => '',
+ 'last_check' => 0,
+ 'dismissed' => '',
+ ];
+
+ if (class_exists('AKSRT_Update_Checker')) {
+ $stored = AKSRT_Update_Checker::get_instance()->get_update_info();
+ if (is_array($stored)) {
+ $info = array_merge($info, $stored);
+ }
+ }
+
+ $info['dismissed'] = (string) get_user_meta(get_current_user_id(), 'aksrt_dismissed_update', true);
+
+ return $this->success(['data' => $info]);
+ }
+
+ public function detect_update(): WP_REST_Response {
+ if (!class_exists('AKSRT_Update_Checker')) {
+ return $this->error(__('更新检查器不可用', 'aksrt'), 500);
+ }
+
+ AKSRT_Update_Checker::get_instance()->do_check();
+ $stored = AKSRT_Update_Checker::get_instance()->get_update_info();
+
+ return $this->success([
+ 'message' => __('已完成更新检测', 'aksrt'),
+ 'data' => is_array($stored) ? $stored : [],
+ ]);
+ }
+
+ public function dismiss_update(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $version = sanitize_text_field((string) ($params['version'] ?? ''));
+
+ if ($version === '') {
+ return $this->error(__('缺少版本号', 'aksrt'), 422);
+ }
+
+ update_user_meta(get_current_user_id(), 'aksrt_dismissed_update', $version);
+
+ return $this->success([
+ 'message' => __('已忽略此版本提醒', 'aksrt'),
+ 'data' => ['version' => $version],
+ ]);
+ }
+
+ public function rest_health_email_verification(): WP_REST_Response {
+ $feature_enabled = class_exists('AKSRT_Settings') && AKSRT_Settings::registration_requires_email_verification();
+ $pending_users = 0;
+
+ if (class_exists('AKSRT_Auth_Email_Verification')) {
+ global $wpdb;
+ $pending_users = (int) $wpdb->get_var(
+ "SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_email_verified' AND CAST(meta_value AS UNSIGNED) = 0"
+ );
+ }
+
+ return $this->success(['data' => [
+ 'feature_enabled' => $feature_enabled,
+ 'pending_users' => $pending_users,
+ 'rest_blocking_active' => !empty(get_option('aksrt_rest_block_email_verification', false)),
+ ]]);
+ }
+
+ public function rest_set_rest_block(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $enabled = !empty($params['enabled']);
+ update_option('aksrt_rest_block_email_verification', $enabled);
+
+ return $this->success([
+ 'message' => $enabled ? __('已启用 REST 邮箱验证阻断', 'aksrt') : __('已关闭 REST 邮箱验证阻断', 'aksrt'),
+ 'data' => ['enabled' => $enabled],
+ ]);
+ }
+
+ public function rest_email_rollback_list(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Email_Rollback')) {
+ return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20]]);
+ }
+
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = min(100, max(1, (int) $request->get_param('per_page')));
+ $data = AKSRT_Email_Rollback::list_recent_changes($page, $per_page);
+
+ return $this->success(['data' => $data]);
+ }
+
+ public function rest_email_rollback_execute(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Email_Rollback')) {
+ return $this->error(__('邮箱回滚功能不可用', 'aksrt'), 500);
+ }
+
+ $user_id = (int) $request->get_param('id');
+ $result = AKSRT_Email_Rollback::rollback($user_id, get_current_user_id());
+ if (empty($result['success'])) {
+ return $this->error((string) ($result['error'] ?? __('邮箱回滚失败', 'aksrt')), 422);
+ }
+
+ return $this->success([
+ 'message' => (string) ($result['message'] ?? __('邮箱已回滚', 'aksrt')),
+ 'data' => $result,
+ ]);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-user-controller-auth-phone-operations.php b/inc/classes/admin/class-aksrt-admin-user-controller-auth-phone-operations.php
new file mode 100644
index 00000000..86a5efff
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-user-controller-auth-phone-operations.php
@@ -0,0 +1,287 @@
+get_json_params($request);
+ $payload = is_array($params) ? $params : [];
+ $login = sanitize_text_field((string) ($payload['log'] ?? $payload['login'] ?? ''));
+ $password = (string) ($payload['pwd'] ?? $payload['password'] ?? '');
+
+ if ($login === '' || $password === '') {
+ return $this->error('请输入用户名和密码');
+ }
+
+ if (!class_exists('AKSRT_Auth_Service')) {
+ return $this->error('认证服务不可用', 500);
+ }
+
+ $result = AKSRT_Auth_Service::login_user([
+ ...$payload,
+ 'log' => $login,
+ 'pwd' => $password,
+ 'rememberme' => !empty($payload['rememberme']) ? (string) $payload['rememberme'] : 'forever',
+ ], true, true);
+
+ if (empty($result['success'])) {
+ $error = $result['error'] ?? '登录失败';
+ $verify_url = $result['verify_url'] ?? '';
+ $response = ['success' => false, 'message' => $error];
+ if ($verify_url) {
+ $response['verify_url'] = $verify_url;
+ }
+ return new WP_REST_Response($response, 401);
+ }
+
+ $user = wp_get_current_user();
+ return $this->success([
+ 'message' => __('登录成功', 'aksrt'),
+ 'redirect' => $result['redirect'] ?? home_url('/'),
+ 'user' => [
+ 'id' => $user->ID,
+ 'display_name' => $user->display_name,
+ 'avatar_url' => function_exists('aksrt_get_avatar_url')
+ ? aksrt_get_avatar_url($user->ID, ['size' => 48])
+ : get_avatar_url($user->ID, ['size' => 48]),
+ ],
+ ]);
+ }
+
+ /**
+ * POST /auth/modal-register — 弹窗注册
+ */
+ public function modal_register(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $payload = is_array($params) ? $params : [];
+ $username = sanitize_user((string) ($payload['user_login'] ?? $payload['username'] ?? ''), true);
+ $email = sanitize_email((string) ($payload['user_email'] ?? $payload['email'] ?? ''));
+ $password = (string) ($payload['user_pass'] ?? $payload['password'] ?? '');
+ $confirm = (string) ($payload['user_pass_confirm'] ?? $payload['password_confirm'] ?? $payload['confirm_password'] ?? $password);
+ $invite_code = sanitize_text_field((string) ($payload['invite_code'] ?? ''));
+ $register_nonce = sanitize_text_field((string) ($payload['_register_nonce'] ?? ''));
+ $agree_terms = !empty($payload['agree_terms']) ? '1' : '';
+
+ if ($username === '' || $email === '' || $password === '') {
+ return $this->error('请填写所有必填项');
+ }
+
+ if (!is_email($email)) {
+ return $this->error('邮箱格式不正确');
+ }
+
+ if (strlen($password) < 6) {
+ return $this->error('密码至少 6 位');
+ }
+
+ if (!class_exists('AKSRT_Auth_Service')) {
+ return $this->error('认证服务不可用', 500);
+ }
+
+ $payload['user_login'] = $username;
+ $payload['user_email'] = $email;
+ $payload['user_pass'] = $password;
+ $payload['user_pass_confirm'] = $confirm;
+ $payload['invite_code'] = $invite_code;
+ $payload['_register_nonce'] = $register_nonce;
+ $payload['agree_terms'] = $agree_terms;
+
+ $result = AKSRT_Auth_Service::register_user($payload);
+
+ if (empty($result['success'])) {
+ return $this->error($result['error'] ?? '注册失败');
+ }
+
+ $verification_required = !empty($result['verification_required']);
+ $login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url();
+ $registered_user = null;
+ $current_user = wp_get_current_user();
+ if (
+ $current_user instanceof WP_User
+ && $current_user->ID > 0
+ && !empty($result['user_id'])
+ && (int) $result['user_id'] === (int) $current_user->ID
+ ) {
+ $registered_user = $current_user;
+ }
+ $user = $current_user;
+ $needs_verify = $verification_required;
+
+ return $this->success([
+ 'message' => $needs_verify ? __('注册成功,请验证邮箱', 'aksrt') : __('注册成功', 'aksrt'),
+ 'needs_verification' => $needs_verify,
+ 'verify_url' => $result['verify_url'] ?? '',
+ 'redirect' => $verification_required ? ($result['verify_url'] ?? '') : $login_url,
+ 'verification_required' => $verification_required,
+ 'login_url' => $login_url,
+ 'user_login' => $result['user_login'] ?? $username,
+ 'user_email' => $email,
+ 'user' => $registered_user ? [
+ 'id' => $registered_user->ID,
+ 'display_name' => $registered_user->display_name,
+ 'avatar_url' => function_exists('aksrt_get_avatar_url')
+ ? aksrt_get_avatar_url($registered_user->ID, ['size' => 48])
+ : get_avatar_url($registered_user->ID, ['size' => 48]),
+ ] : ($user->ID ? [
+ 'id' => $user->ID,
+ 'display_name' => $user->display_name,
+ 'avatar_url' => function_exists('aksrt_get_avatar_url')
+ ? aksrt_get_avatar_url($user->ID, ['size' => 48])
+ : get_avatar_url($user->ID, ['size' => 48]),
+ ] : null),
+ ]);
+ }
+
+ /**
+ * GET /auth/me — 当前用户信息(用于 AJAX 刷新)
+ */
+ public function get_current_user_info(): WP_REST_Response {
+ $user = wp_get_current_user();
+ if (!$user->ID) {
+ return $this->error('未登录', 401);
+ }
+
+ $unread = 0;
+ if (class_exists('AKSRT_Private_Message')) {
+ $unread = AKSRT_Private_Message::get_instance()->get_unread_count($user->ID);
+ }
+
+ $credits = 0;
+ if (class_exists('AKSRT_Credits')) {
+ $credits = AKSRT_Credits::balance($user->ID);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'id' => $user->ID,
+ 'display_name' => $user->display_name,
+ 'user_email' => $user->user_email,
+ 'avatar_url' => function_exists('aksrt_get_avatar_url')
+ ? aksrt_get_avatar_url($user->ID, ['size' => 48])
+ : get_avatar_url($user->ID, ['size' => 48]),
+ 'is_admin' => current_user_can('manage_options'),
+ 'unread' => $unread,
+ 'credits' => $credits,
+ ],
+ ]);
+ }
+
+ /**
+ * POST /auth/send-sms-code — 发送短信验证码
+ */
+ public function auth_send_sms_code(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $phone = sanitize_text_field((string) ($params['phone'] ?? ''));
+
+ if (!class_exists('AKSRT_SMS')) {
+ return $this->error('短信服务不可用');
+ }
+
+ $result = AKSRT_SMS::get_instance()->send_code($phone);
+
+ if (!empty($result['success'])) {
+ return $this->success([
+ 'message' => $result['message'],
+ 'remaining_time' => $result['remaining_time'] ?? 60,
+ ]);
+ }
+
+ $response = [
+ 'success' => false,
+ 'message' => $result['message'],
+ ];
+ if (isset($result['remaining_time'])) {
+ $response['data'] = ['remaining_time' => $result['remaining_time']];
+ }
+ return new WP_REST_Response($response, 400);
+ }
+
+ /**
+ * POST /auth/phone-register — 手机号注册
+ */
+ public function auth_phone_register(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Auth_Service')) {
+ return $this->error('认证服务不可用');
+ }
+
+ $params = $this->get_json_params($request);
+ $result = AKSRT_Auth_Service::register_by_phone($params);
+
+ if (!empty($result['success'])) {
+ return $this->success([
+ 'message' => $result['message'] ?? '注册成功',
+ 'redirect' => $result['redirect'] ?? home_url('/'),
+ 'user_id' => $result['user_id'] ?? 0,
+ ]);
+ }
+
+ return $this->error($result['error'] ?? '注册失败');
+ }
+
+ /**
+ * POST /auth/phone-login — 手机号 + 密码登录
+ */
+ public function auth_phone_login(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Auth_Service')) {
+ return $this->error('认证服务不可用');
+ }
+
+ $params = $this->get_json_params($request);
+ $result = AKSRT_Auth_Service::login_by_phone_password($params);
+
+ if (!empty($result['success'])) {
+ return $this->success([
+ 'redirect' => $result['redirect'] ?? home_url('/'),
+ ]);
+ }
+
+ return $this->error($result['error'] ?? '登录失败');
+ }
+
+ /**
+ * POST /auth/phone-code-login — 手机号 + 验证码免密登录
+ */
+ public function auth_phone_code_login(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Auth_Service')) {
+ return $this->error('认证服务不可用');
+ }
+
+ $params = $this->get_json_params($request);
+ $result = AKSRT_Auth_Service::login_by_phone_code($params);
+
+ if (!empty($result['success'])) {
+ return $this->success([
+ 'redirect' => $result['redirect'] ?? home_url('/'),
+ ]);
+ }
+
+ return $this->error($result['error'] ?? '登录失败');
+ }
+
+ /**
+ * POST /auth/bind-phone — 绑定手机号
+ */
+ public function auth_bind_phone(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Auth_Service')) {
+ return $this->error('认证服务不可用');
+ }
+
+ $params = $this->get_json_params($request);
+ $result = AKSRT_Auth_Service::bind_phone($params);
+
+ if (!empty($result['success'])) {
+ return $this->success(['message' => $result['message']]);
+ }
+
+ return $this->error($result['error'] ?? '绑定失败');
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-user-controller-follow-operations.php b/inc/classes/admin/class-aksrt-admin-user-controller-follow-operations.php
new file mode 100644
index 00000000..c159ebde
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-user-controller-follow-operations.php
@@ -0,0 +1,89 @@
+error('关注系统未启用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $target_user_id = (int) ($params['target_user_id'] ?? 0);
+ $current_user_id = get_current_user_id();
+
+ if ($target_user_id <= 0 || $target_user_id === $current_user_id) {
+ return $this->error('参数无效');
+ }
+
+ $target_user = get_userdata($target_user_id);
+ if (!$target_user) {
+ return $this->error('用户不存在', 404);
+ }
+
+ $is_following = AKSRT_Follow::get_instance()->toggle($current_user_id, $target_user_id);
+
+ return $this->success([
+ 'data' => [
+ 'is_following' => $is_following,
+ 'follower_count' => AKSRT_Follow::get_instance()->get_follower_count($target_user_id),
+ ],
+ ]);
+ }
+
+ /**
+ * GET /follow/status — 查询关注状态
+ */
+ public function follow_status(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Follow')) {
+ return $this->success(['data' => ['is_following' => false]]);
+ }
+
+ $target_user_id = (int) $request->get_param('target_user_id');
+ $current_user_id = get_current_user_id();
+
+ if ($target_user_id <= 0) {
+ return $this->error('参数无效');
+ }
+
+ $is_following = AKSRT_Follow::get_instance()->is_following($current_user_id, $target_user_id);
+
+ return $this->success([
+ 'data' => [
+ 'is_following' => $is_following,
+ 'follower_count' => AKSRT_Follow::get_instance()->get_follower_count($target_user_id),
+ 'following_count' => AKSRT_Follow::get_instance()->get_following_count($target_user_id),
+ ],
+ ]);
+ }
+
+ /**
+ * GET /follow/counts — 获取关注/粉丝数
+ */
+ public function follow_counts(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Follow')) {
+ return $this->success(['data' => ['followers' => 0, 'following' => 0]]);
+ }
+
+ $user_id = (int) $request->get_param('user_id');
+ if ($user_id <= 0) {
+ return $this->error('参数无效');
+ }
+
+ return $this->success([
+ 'data' => [
+ 'followers' => AKSRT_Follow::get_instance()->get_follower_count($user_id),
+ 'following' => AKSRT_Follow::get_instance()->get_following_count($user_id),
+ ],
+ ]);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-user-controller-invite-operations.php b/inc/classes/admin/class-aksrt-admin-user-controller-invite-operations.php
new file mode 100644
index 00000000..4f664706
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-user-controller-invite-operations.php
@@ -0,0 +1,450 @@
+get_param('page') ?: 1));
+ $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
+ $keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
+ $status = sanitize_key((string) $request->get_param('status'));
+ $source = sanitize_key((string) $request->get_param('source'));
+ $date_from = sanitize_text_field((string) $request->get_param('date_from'));
+ $date_to = sanitize_text_field((string) $request->get_param('date_to'));
+
+ $usermeta_table = $wpdb->usermeta;
+ $invite_table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('invite') : '';
+
+ $invite_rows = [];
+ if ($invite_table) {
+ $records = $wpdb->get_results("SELECT * FROM {$invite_table}", ARRAY_A);
+ foreach ((array) $records as $record) {
+ $invite_rows[(string) $record['invite_code']] = $record;
+ }
+ }
+
+ $items = [];
+ $user_rows = $wpdb->get_results(
+ "SELECT user_id, meta_value AS invite_code FROM {$usermeta_table} WHERE meta_key = 'aksrt_invite_code' ORDER BY umeta_id DESC",
+ ARRAY_A
+ );
+
+ foreach ((array) $user_rows as $row) {
+ $user_id = (int) $row['user_id'];
+ $user = get_userdata($user_id);
+ $code = sanitize_key((string) $row['invite_code']);
+ $record = $invite_rows[$code] ?? null;
+
+ $item = [
+ 'row_key' => 'user:' . $user_id,
+ 'source_type' => 'user',
+ 'source_label' => __('用户邀请码', 'aksrt'),
+ 'user' => $this->build_admin_user_summary($user_id, $user),
+ 'invite_code' => $code,
+ 'status' => $record ? 'used' : 'active',
+ 'status_label' => $record ? __('已使用', 'aksrt') : __('可用', 'aksrt'),
+ 'created_at' => $user ? (string) $user->user_registered : '',
+ 'used_at' => $record['created_at'] ?? '',
+ 'note' => '',
+ 'batch_id' => '',
+ 'created_by' => null,
+ 'registration_status' => $record['status'] ?? '',
+ 'invitee' => $this->build_admin_user_summary((int) ($record['invitee_id'] ?? 0)),
+ 'can_bulk' => false,
+ 'actions' => ['disable'],
+ ];
+
+ if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) {
+ $items[] = $item;
+ }
+ }
+
+ foreach ($this->load_standalone_invite_codes() as $entry) {
+ $code = sanitize_key((string) ($entry['code'] ?? ''));
+ $record = $invite_rows[$code] ?? null;
+ $used_by = (int) ($entry['used_by'] ?? 0);
+ $disabled = !empty($entry['disabled']);
+ $used = !empty($entry['used']) || $used_by > 0 || !empty($record['invitee_id']);
+ $status_key = $disabled ? 'disabled' : ($used ? 'used' : 'active');
+ $status_map = [
+ 'active' => __('可用', 'aksrt'),
+ 'used' => __('已使用', 'aksrt'),
+ 'disabled' => __('已禁用', 'aksrt'),
+ ];
+ $invitee_id = $used_by > 0 ? $used_by : (int) ($record['invitee_id'] ?? 0);
+
+ $item = [
+ 'row_key' => 'standalone:' . $code,
+ 'source_type' => 'standalone',
+ 'source_label' => __('独立邀请码', 'aksrt'),
+ 'user' => null,
+ 'invite_code' => $code,
+ 'status' => $status_key,
+ 'status_label' => $status_map[$status_key],
+ 'created_at' => (string) ($entry['created_at'] ?? ''),
+ 'used_at' => (string) ($entry['used_at'] ?? ($record['created_at'] ?? '')),
+ 'note' => (string) ($entry['note'] ?? ''),
+ 'batch_id' => (string) ($entry['batch_id'] ?? ''),
+ 'created_by' => $this->build_admin_user_summary((int) ($entry['created_by'] ?? 0)),
+ 'registration_status' => $record['status'] ?? '',
+ 'invitee' => $this->build_admin_user_summary($invitee_id),
+ 'can_bulk' => true,
+ 'actions' => ['disable', 'delete'],
+ ];
+
+ if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) {
+ $items[] = $item;
+ }
+ }
+
+ usort($items, static function ($left, $right) {
+ return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? ''));
+ });
+
+ $total = count($items);
+ $offset = ($page - 1) * $per_page;
+ $paged = array_slice($items, $offset, $per_page);
+ $stats = $this->build_invite_operation_stats($items, $invite_rows);
+
+ return $this->success([
+ 'data' => [
+ 'items' => array_values($paged),
+ 'total' => $total,
+ 'page' => $page,
+ 'per_page' => $per_page,
+ 'total_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
+ 'stats' => $stats,
+ 'options' => [
+ 'statuses' => [
+ ['label' => __('全部状态', 'aksrt'), 'value' => ''],
+ ['label' => __('可用', 'aksrt'), 'value' => 'active'],
+ ['label' => __('已使用', 'aksrt'), 'value' => 'used'],
+ ['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'],
+ ],
+ 'sources' => [
+ ['label' => __('全部来源', 'aksrt'), 'value' => ''],
+ ['label' => __('用户邀请码', 'aksrt'), 'value' => 'user'],
+ ['label' => __('独立邀请码', 'aksrt'), 'value' => 'standalone'],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * POST /invite/codes — 批量生成邀请码(管理后台)
+ * Body: { count: int, length: int, prefix: string }
+ * 为指定用户或批量创建邀请码。
+ */
+ public function generate_invite_codes(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Invite')) {
+ return $this->error(__('邀请系统未启用', 'aksrt'), 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $count = max(1, min(100, (int) ($params['count'] ?? 1)));
+ $length = max(4, min(12, (int) ($params['length'] ?? 6)));
+ $prefix = sanitize_key((string) ($params['prefix'] ?? ''));
+ $note = sanitize_textarea_field((string) ($params['note'] ?? ''));
+ $batch_id = sanitize_text_field((string) ($params['batch_id'] ?? ''));
+
+ if ($batch_id === '') {
+ $batch_id = 'INV-' . gmdate('YmdHis') . '-' . wp_generate_password(4, false, false);
+ }
+
+ $generated = [];
+ $codes = $this->load_standalone_invite_codes();
+ $alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
+
+ for ($i = 0; $i < $count; $i++) {
+ $code = '';
+ for ($attempt = 0; $attempt < 12; $attempt++) {
+ $candidate = $prefix;
+ for ($j = 0; $j < $length; $j++) {
+ $candidate .= $alphabet[random_int(0, strlen($alphabet) - 1)];
+ }
+ $candidate = sanitize_key($candidate);
+ if (!$this->invite_code_exists_globally($candidate, $codes)) {
+ $code = $candidate;
+ break;
+ }
+ }
+
+ if ($code === '') {
+ continue;
+ }
+
+ $codes[] = [
+ 'code' => $code,
+ 'created_at' => current_time('mysql'),
+ 'used' => false,
+ 'used_by' => 0,
+ 'used_at' => '',
+ 'disabled' => false,
+ 'note' => $note,
+ 'batch_id' => $batch_id,
+ 'created_by' => get_current_user_id(),
+ ];
+ $generated[] = $code;
+ }
+
+ $this->save_standalone_invite_codes($codes);
+
+ return $this->success([
+ 'message' => sprintf(__('已生成 %d 个邀请码', 'aksrt'), count($generated)),
+ 'data' => [
+ 'codes' => $generated,
+ 'batch_id' => $batch_id,
+ ],
+ ]);
+ }
+
+ /**
+ * POST /invite/codes/{id}/disable — 禁用邀请码
+ */
+ public function disable_invite_code(WP_REST_Request $request): WP_REST_Response {
+ $user_id = (int) $request->get_param('id');
+
+ if ($user_id <= 0) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ $removed = $this->remove_user_invite_code($user_id);
+ if (!$removed) {
+ return $this->error(__('邀请码不存在或已禁用', 'aksrt'));
+ }
+
+ return $this->success(['message' => __('邀请码已禁用', 'aksrt')]);
+ }
+
+ public function bulk_update_invite_codes(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $action = sanitize_key((string) ($params['action'] ?? ''));
+ $targets = isset($params['targets']) && is_array($params['targets']) ? $params['targets'] : [];
+
+ if (!in_array($action, ['disable', 'delete'], true) || empty($targets)) {
+ return $this->error(__('批量操作参数无效', 'aksrt'));
+ }
+
+ $codes = $this->load_standalone_invite_codes();
+ $processed = 0;
+
+ foreach ($targets as $target) {
+ $source_type = sanitize_key((string) ($target['source_type'] ?? ''));
+ if ($source_type === 'user') {
+ $user_id = (int) ($target['user_id'] ?? 0);
+ if ($action === 'disable' && $this->remove_user_invite_code($user_id)) {
+ $processed++;
+ }
+ continue;
+ }
+
+ if ($source_type !== 'standalone') {
+ continue;
+ }
+
+ $code = sanitize_key((string) ($target['code'] ?? $target['invite_code'] ?? ''));
+ foreach ($codes as $index => $entry) {
+ $entry_code = sanitize_key((string) ($entry['code'] ?? ''));
+ if ($entry_code !== $code) {
+ continue;
+ }
+
+ if ($action === 'delete' && empty($entry['used']) && empty($entry['used_by'])) {
+ unset($codes[$index]);
+ } else {
+ $entry['disabled'] = true;
+ $codes[$index] = $entry;
+ }
+ $processed++;
+ break;
+ }
+ }
+
+ $this->save_standalone_invite_codes($codes);
+
+ return $this->success([
+ 'message' => sprintf(__('已处理 %d 条邀请码记录', 'aksrt'), $processed),
+ ]);
+ }
+
+ /**
+ * GET /invite/stats — 邀请系统统计
+ */
+ public function get_invite_stats(): WP_REST_Response {
+ global $wpdb;
+
+ $items = [];
+ $user_records = $wpdb->get_results(
+ "SELECT user_id, meta_value AS invite_code FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code'",
+ ARRAY_A
+ );
+
+ foreach ((array) $user_records as $row) {
+ $items[] = [
+ 'source_type' => 'user',
+ 'status' => 'active',
+ ];
+ }
+
+ foreach ($this->load_standalone_invite_codes() as $entry) {
+ $used = !empty($entry['used']) || !empty($entry['used_by']);
+ $disabled = !empty($entry['disabled']);
+ $items[] = [
+ 'source_type' => 'standalone',
+ 'status' => $disabled ? 'disabled' : ($used ? 'used' : 'active'),
+ ];
+ }
+
+ $stats = $this->build_invite_operation_stats($items);
+ return $this->success(['data' => $stats]);
+ }
+
+ private function load_standalone_invite_codes(): array {
+ $codes = get_option('aksrt_standalone_invite_codes', []);
+ return is_array($codes) ? array_values($codes) : [];
+ }
+
+ private function save_standalone_invite_codes(array $codes): void {
+ update_option('aksrt_standalone_invite_codes', array_values($codes), false);
+ }
+
+ private function invite_code_exists_globally(string $candidate, array $standalone_codes = []): bool {
+ if ($candidate === '') {
+ return true;
+ }
+
+ global $wpdb;
+
+ $existing_user_code = $wpdb->get_var($wpdb->prepare(
+ "SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' AND meta_value = %s LIMIT 1",
+ $candidate
+ ));
+ if ($existing_user_code) {
+ return true;
+ }
+
+ foreach ($standalone_codes as $entry) {
+ if (sanitize_key((string) ($entry['code'] ?? '')) === $candidate) {
+ return true;
+ }
+ }
+
+ $invite_table = class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('invite') : '';
+ if ($invite_table) {
+ $existing_invite = $wpdb->get_var($wpdb->prepare(
+ "SELECT id FROM {$invite_table} WHERE invite_code = %s LIMIT 1",
+ $candidate
+ ));
+ if ($existing_invite) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function invite_item_matches_filters(array $item, string $keyword, string $status, string $source, string $date_from, string $date_to): bool {
+ if ($status !== '' && ($item['status'] ?? '') !== $status) {
+ return false;
+ }
+ if ($source !== '' && ($item['source_type'] ?? '') !== $source) {
+ return false;
+ }
+
+ $created_at = (string) ($item['created_at'] ?? '');
+ if ($date_from !== '' && $created_at !== '' && strcmp($created_at, $date_from . ' 00:00:00') < 0) {
+ return false;
+ }
+ if ($date_to !== '' && $created_at !== '' && strcmp($created_at, $date_to . ' 23:59:59') > 0) {
+ return false;
+ }
+
+ if ($keyword === '') {
+ return true;
+ }
+
+ $haystack = implode(' ', array_filter([
+ (string) ($item['invite_code'] ?? ''),
+ (string) ($item['status_label'] ?? ''),
+ (string) ($item['note'] ?? ''),
+ (string) ($item['batch_id'] ?? ''),
+ (string) ($item['user']['display_name'] ?? ''),
+ (string) ($item['invitee']['display_name'] ?? ''),
+ (string) ($item['invitee']['email'] ?? ''),
+ ]));
+
+ return mb_stripos($haystack, $keyword) !== false;
+ }
+
+ private function build_invite_operation_stats(array $items, array $invite_rows = []): array {
+ $stats = [
+ 'total_codes' => count($items),
+ 'active_codes' => 0,
+ 'used_codes' => 0,
+ 'disabled_codes' => 0,
+ 'user_codes' => 0,
+ 'standalone_codes' => 0,
+ 'total_invites' => count($invite_rows),
+ 'total_rewarded' => 0,
+ ];
+
+ foreach ($items as $item) {
+ $status = $item['status'] ?? '';
+ if ($status === 'active') {
+ $stats['active_codes']++;
+ } elseif ($status === 'used') {
+ $stats['used_codes']++;
+ } elseif ($status === 'disabled') {
+ $stats['disabled_codes']++;
+ }
+
+ if (($item['source_type'] ?? '') === 'user') {
+ $stats['user_codes']++;
+ } elseif (($item['source_type'] ?? '') === 'standalone') {
+ $stats['standalone_codes']++;
+ }
+ }
+
+ foreach ($invite_rows as $row) {
+ if (($row['status'] ?? '') === 'rewarded') {
+ $stats['total_rewarded']++;
+ }
+ }
+
+ return $stats;
+ }
+
+ private function remove_user_invite_code(int $user_id): bool {
+ $user_id = (int) $user_id;
+ if ($user_id <= 0) {
+ return false;
+ }
+
+ $code = get_user_meta($user_id, 'aksrt_invite_code', true);
+ if (!$code) {
+ return false;
+ }
+
+ delete_user_meta($user_id, 'aksrt_invite_code');
+
+ $map = get_option('aksrt_invite_code_map', []);
+ if (is_array($map) && isset($map[$code])) {
+ unset($map[$code]);
+ update_option('aksrt_invite_code_map', $map, false);
+ }
+
+ return true;
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-user-controller-report-operations.php b/inc/classes/admin/class-aksrt-admin-user-controller-report-operations.php
new file mode 100644
index 00000000..3495f286
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-user-controller-report-operations.php
@@ -0,0 +1,471 @@
+success([
+ 'data' => [
+ 'items' => [],
+ 'reports' => [],
+ 'counts' => ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0],
+ 'total' => 0,
+ 'page' => 1,
+ 'options' => [
+ 'statuses' => $this->get_report_status_options(),
+ ],
+ ],
+ ]);
+ }
+
+ global $wpdb;
+
+ $table = AKSRT_DB_Migrator::table('report');
+ if (!$table) {
+ return $this->success([
+ 'data' => [
+ 'items' => [],
+ 'reports' => [],
+ 'counts' => ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0],
+ 'total' => 0,
+ 'page' => 1,
+ 'options' => [
+ 'statuses' => $this->get_report_status_options(),
+ ],
+ ],
+ ]);
+ }
+
+ $page = max(1, (int) ($request->get_param('page') ?: 1));
+ $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: $request->get_param('limit') ?: 20)));
+ $status = sanitize_key((string) $request->get_param('status'));
+ $category = sanitize_key((string) $request->get_param('category'));
+ $target_type = sanitize_key((string) $request->get_param('target_type'));
+ $keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
+ $date_from = sanitize_text_field((string) $request->get_param('date_from'));
+ $date_to = sanitize_text_field((string) $request->get_param('date_to'));
+
+ $where = [];
+ $params = [];
+
+ if ($status !== '') {
+ $where[] = 'status = %s';
+ $params[] = $status;
+ }
+ if ($category !== '') {
+ $where[] = 'category = %s';
+ $params[] = $category;
+ }
+ if ($target_type !== '') {
+ $where[] = 'target_type = %s';
+ $params[] = $target_type;
+ }
+ if ($date_from !== '') {
+ $where[] = 'created_at >= %s';
+ $params[] = $date_from . ' 00:00:00';
+ }
+ if ($date_to !== '') {
+ $where[] = 'created_at <= %s';
+ $params[] = $date_to . ' 23:59:59';
+ }
+
+ $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
+ $sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500";
+ $rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
+
+ $items = [];
+ foreach ((array) $rows as $row) {
+ $item = $this->build_report_admin_item($row);
+ if ($keyword !== '' && !$this->report_matches_keyword($item, $keyword)) {
+ continue;
+ }
+ $items[] = $item;
+ }
+
+ $total = count($items);
+ $offset = ($page - 1) * $per_page;
+ $paged = array_slice($items, $offset, $per_page);
+ $counts = AKSRT_Report_Center::get_instance()->get_status_counts();
+ $categories = [];
+ foreach ((array) $rows as $row) {
+ $cat = sanitize_key((string) ($row['category'] ?? ''));
+ if ($cat !== '') {
+ $categories[$cat] = $cat;
+ }
+ }
+
+ return $this->success([
+ 'data' => [
+ 'items' => array_values($paged),
+ 'reports' => array_values($paged),
+ 'total' => $total,
+ 'page' => $page,
+ 'per_page' => $per_page,
+ 'counts' => $counts,
+ 'options' => [
+ 'statuses' => $this->get_report_status_options(),
+ 'actions' => $this->get_report_action_options(),
+ 'targets' => $this->get_report_target_type_options(),
+ 'categories' => array_map(static function ($item) {
+ return ['label' => $item, 'value' => $item];
+ }, array_values($categories)),
+ ],
+ ],
+ ]);
+ }
+
+ public function update_report_status(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Report_Center')) {
+ return $this->error(__('举报系统未启用', 'aksrt'));
+ }
+
+ $params = $this->get_json_params($request);
+ $report_id = (int) ($params['report_id'] ?? 0);
+ if ($report_id <= 0) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ return $this->handle_report_process($report_id, $params);
+ }
+
+ public function get_report_counts(): WP_REST_Response {
+ if (!class_exists('AKSRT_Report_Center')) {
+ return $this->success(['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0]);
+ }
+ return $this->success(AKSRT_Report_Center::get_instance()->get_status_counts());
+ }
+
+ public function get_report_detail(WP_REST_Request $request): WP_REST_Response {
+ global $wpdb;
+
+ $report_id = (int) $request->get_param('id');
+ $table = AKSRT_DB_Migrator::table('report');
+
+ if ($report_id <= 0 || !$table) {
+ return $this->error(__('举报工单不存在', 'aksrt'), 404);
+ }
+
+ $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $report_id), ARRAY_A);
+ if (!$row) {
+ return $this->error(__('举报工单不存在', 'aksrt'), 404);
+ }
+
+ $related = (int) $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM {$table} WHERE target_type = %s AND target_id = %d",
+ sanitize_key((string) $row['target_type']),
+ (int) $row['target_id']
+ ));
+
+ return $this->success([
+ 'data' => [
+ 'report' => $this->build_report_admin_item($row),
+ 'related' => ['same_target_count' => $related],
+ 'options' => [
+ 'statuses' => $this->get_report_status_options(),
+ 'actions' => $this->get_report_action_options(sanitize_key((string) $row['target_type'])),
+ ],
+ ],
+ ]);
+ }
+
+ public function process_report(WP_REST_Request $request): WP_REST_Response {
+ $report_id = (int) $request->get_param('id');
+ if ($report_id <= 0) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ return $this->handle_report_process($report_id, $this->get_json_params($request));
+ }
+
+ private function handle_report_process(int $report_id, array $payload): WP_REST_Response {
+ global $wpdb;
+
+ $table = AKSRT_DB_Migrator::table('report');
+ if (!$table) {
+ return $this->error(__('举报系统未启用', 'aksrt'));
+ }
+
+ $status = sanitize_key((string) ($payload['status'] ?? ''));
+ $action_taken = sanitize_key((string) ($payload['action_taken'] ?? 'none'));
+ $note = sanitize_textarea_field((string) ($payload['note'] ?? ''));
+
+ $valid_statuses = array_column($this->get_report_status_options(), 'value');
+ $valid_actions = array_column($this->get_report_action_options(), 'value');
+
+ if ($status === '' || !in_array($status, $valid_statuses, true)) {
+ return $this->error(__('处理状态无效', 'aksrt'));
+ }
+ if (!in_array($action_taken, $valid_actions, true)) {
+ $action_taken = 'none';
+ }
+
+ $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $report_id), ARRAY_A);
+ if (!$row) {
+ return $this->error(__('举报工单不存在', 'aksrt'), 404);
+ }
+
+ $effect = $this->apply_report_action(
+ sanitize_key((string) $row['target_type']),
+ (int) $row['target_id'],
+ $action_taken
+ );
+
+ $notes = json_decode((string) ($row['notes_json'] ?? '[]'), true);
+ if (!is_array($notes)) {
+ $notes = [];
+ }
+ $notes[] = [
+ 'note' => $note,
+ 'status' => $status,
+ 'action_taken' => $action_taken,
+ 'operator' => get_current_user_id(),
+ 'timestamp' => current_time('mysql'),
+ 'effect' => $effect,
+ ];
+
+ $updated = $wpdb->update(
+ $table,
+ [
+ 'status' => $status,
+ 'updated_at' => current_time('mysql'),
+ 'notes_json' => wp_json_encode($notes),
+ ],
+ ['id' => $report_id],
+ ['%s', '%s', '%s'],
+ ['%d']
+ );
+
+ if ($updated === false) {
+ return $this->error(__('处理失败', 'aksrt'));
+ }
+
+ return $this->success([
+ 'message' => __('举报工单已处理', 'aksrt'),
+ ]);
+ }
+
+ private function build_report_admin_item(array $row): array {
+ $reporter = get_userdata((int) ($row['reporter_id'] ?? 0));
+ $notes = json_decode((string) ($row['notes_json'] ?? '[]'), true);
+ if (!is_array($notes)) {
+ $notes = [];
+ }
+
+ $normalized_notes = [];
+ foreach ($notes as $entry) {
+ $operator_id = (int) ($entry['operator'] ?? 0);
+ $operator_user = $operator_id > 0 ? get_userdata($operator_id) : null;
+ $normalized_notes[] = [
+ 'note' => (string) ($entry['note'] ?? ''),
+ 'status' => sanitize_key((string) ($entry['status'] ?? '')),
+ 'action_taken' => sanitize_key((string) ($entry['action_taken'] ?? 'none')),
+ 'operator' => $operator_id,
+ 'operator_name' => $operator_user ? $operator_user->display_name : '',
+ 'timestamp' => (string) ($entry['timestamp'] ?? ''),
+ 'effect' => (string) ($entry['effect'] ?? ''),
+ ];
+ }
+
+ $last_note = !empty($normalized_notes) ? $normalized_notes[count($normalized_notes) - 1] : null;
+
+ return [
+ 'id' => (int) ($row['id'] ?? 0),
+ 'target_type' => sanitize_key((string) ($row['target_type'] ?? '')),
+ 'target_id' => (int) ($row['target_id'] ?? 0),
+ 'reason' => (string) ($row['reason'] ?? ''),
+ 'category' => sanitize_key((string) ($row['category'] ?? '')),
+ 'status' => sanitize_key((string) ($row['status'] ?? 'pending')),
+ 'created_at' => (string) ($row['created_at'] ?? ''),
+ 'updated_at' => (string) ($row['updated_at'] ?? ''),
+ 'reporter' => $this->build_admin_user_summary($reporter ? (int) $reporter->ID : 0, $reporter),
+ 'target' => $this->build_report_target_summary(
+ sanitize_key((string) ($row['target_type'] ?? '')),
+ (int) ($row['target_id'] ?? 0)
+ ),
+ 'notes' => $normalized_notes,
+ 'note_count' => count($normalized_notes),
+ 'last_note' => $last_note,
+ ];
+ }
+
+ private function report_matches_keyword(array $item, string $keyword): bool {
+ $haystack = implode(' ', array_filter([
+ (string) ($item['reason'] ?? ''),
+ (string) ($item['category'] ?? ''),
+ (string) ($item['reporter']['display_name'] ?? ''),
+ (string) ($item['reporter']['email'] ?? ''),
+ (string) ($item['target']['title'] ?? ''),
+ (string) ($item['target']['display_name'] ?? ''),
+ (string) ($item['target']['excerpt'] ?? ''),
+ (string) ($item['target_id'] ?? ''),
+ ]));
+
+ return mb_stripos($haystack, $keyword) !== false;
+ }
+
+ private function build_report_target_summary(string $target_type, int $target_id): array {
+ $summary = [
+ 'type' => $target_type,
+ 'id' => $target_id,
+ 'title' => '',
+ 'excerpt' => '',
+ 'display_name' => '',
+ 'status' => '',
+ 'url' => '',
+ 'ban_state' => '',
+ ];
+
+ if ($target_type === 'post') {
+ $post = get_post($target_id);
+ if ($post instanceof WP_Post) {
+ $summary['title'] = get_the_title($post);
+ $summary['status'] = (string) $post->post_status;
+ $summary['url'] = get_permalink($post);
+ }
+ } elseif ($target_type === 'comment') {
+ $comment = get_comment($target_id);
+ if ($comment instanceof WP_Comment) {
+ $summary['display_name'] = (string) $comment->comment_author;
+ $summary['excerpt'] = wp_trim_words(wp_strip_all_tags((string) $comment->comment_content), 24);
+ $summary['status'] = $comment->comment_approved === '1' ? 'approved' : (string) $comment->comment_approved;
+ $summary['url'] = get_comment_link($comment);
+ if (!empty($comment->comment_post_ID)) {
+ $summary['title'] = get_the_title((int) $comment->comment_post_ID);
+ }
+ }
+ } elseif ($target_type === 'user') {
+ $user = get_userdata($target_id);
+ if ($user instanceof WP_User) {
+ $summary['display_name'] = $user->display_name;
+ $summary['title'] = $user->display_name;
+ $summary['url'] = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($target_id) : '';
+ $summary['ban_state'] = (string) get_user_meta($target_id, 'aksrt_user_ban_state', true);
+ }
+ }
+
+ return $summary;
+ }
+
+ private function apply_report_action(string $target_type, int $target_id, string $action_taken): string {
+ if ($target_id <= 0 || $action_taken === 'none') {
+ return '';
+ }
+
+ if ($target_type === 'user') {
+ if (in_array($action_taken, ['muted', 'restricted', 'banned'], true)) {
+ update_user_meta($target_id, 'aksrt_user_ban_state', $action_taken);
+ return sprintf(__('已同步用户限制状态:%s', 'aksrt'), $action_taken);
+ }
+
+ if ($action_taken === 'clear') {
+ delete_user_meta($target_id, 'aksrt_user_ban_state');
+ return __('已清除用户限制状态', 'aksrt');
+ }
+
+ return '';
+ }
+
+ if ($action_taken === 'content_removed' && $target_type === 'post' && get_post($target_id)) {
+ wp_update_post([
+ 'ID' => $target_id,
+ 'post_status' => 'pending',
+ ]);
+ return __('已将内容改为待审状态', 'aksrt');
+ }
+
+ if ($action_taken === 'content_removed' && $target_type === 'comment' && get_comment($target_id)) {
+ wp_set_comment_status($target_id, 'hold', true);
+ return __('已将评论改为待审状态', 'aksrt');
+ }
+
+ return '';
+ }
+
+ private function get_report_status_options(): array {
+ return [
+ ['label' => __('待处理', 'aksrt'), 'value' => 'pending'],
+ ['label' => __('处理中', 'aksrt'), 'value' => 'reviewing'],
+ ['label' => __('已解决', 'aksrt'), 'value' => 'resolved'],
+ ['label' => __('已驳回', 'aksrt'), 'value' => 'dismissed'],
+ ];
+ }
+
+ private function get_report_target_type_options(): array {
+ return [
+ ['label' => __('文章', 'aksrt'), 'value' => 'post'],
+ ['label' => __('评论', 'aksrt'), 'value' => 'comment'],
+ ['label' => __('用户', 'aksrt'), 'value' => 'user'],
+ ];
+ }
+
+ private function get_report_action_options(string $target_type = ''): array {
+ $common = [
+ ['label' => __('仅更新状态', 'aksrt'), 'value' => 'none'],
+ ['label' => __('升级人工处理', 'aksrt'), 'value' => 'escalated'],
+ ];
+
+ if ($target_type === 'user' || $target_type === '') {
+ $common = array_merge($common, [
+ ['label' => __('发送警告', 'aksrt'), 'value' => 'warned'],
+ ['label' => __('禁言', 'aksrt'), 'value' => 'muted'],
+ ['label' => __('限制功能', 'aksrt'), 'value' => 'restricted'],
+ ['label' => __('封禁账号', 'aksrt'), 'value' => 'banned'],
+ ['label' => __('解除限制', 'aksrt'), 'value' => 'clear'],
+ ]);
+ }
+
+ if ($target_type === 'post' || $target_type === 'comment' || $target_type === '') {
+ $common[] = ['label' => __('转为待审内容', 'aksrt'), 'value' => 'content_removed'];
+ }
+
+ return $common;
+ }
+
+ /**
+ * POST /report/submit — 前台举报提交(登录用户)
+ */
+ public function submit_report(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Report_Center')) {
+ return $this->error('举报系统未启用', 500);
+ }
+
+ $params = $this->get_json_params($request);
+ $target_type = sanitize_key((string) ($params['target_type'] ?? ''));
+ $target_id = (int) ($params['target_id'] ?? 0);
+ $reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
+ $category = sanitize_key((string) ($params['category'] ?? ''));
+
+ if ($target_type === '' || $target_id <= 0 || $reason === '') {
+ return $this->error('请填写完整的举报信息');
+ }
+
+ $valid_types = ['post', 'comment', 'user'];
+ if (!in_array($target_type, $valid_types, true)) {
+ return $this->error('无效的举报类型');
+ }
+
+ $report_id = AKSRT_Report_Center::get_instance()->submit(
+ get_current_user_id(),
+ $target_type,
+ $target_id,
+ $reason,
+ $category
+ );
+
+ if ($report_id > 0) {
+ return $this->success([
+ 'message' => __('举报已提交,我们会尽快处理', 'aksrt'),
+ 'data' => ['report_id' => $report_id],
+ ]);
+ }
+
+ return $this->error(__('举报提交失败', 'aksrt'));
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-user-controller-restriction-operations.php b/inc/classes/admin/class-aksrt-admin-user-controller-restriction-operations.php
new file mode 100644
index 00000000..561021e9
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-user-controller-restriction-operations.php
@@ -0,0 +1,82 @@
+get_param('user_id');
+ $user = get_userdata($user_id);
+ if (!$user) {
+ return $this->error('用户不存在', 404);
+ }
+
+ if (!class_exists('AKSRT_User_Restriction')) {
+ return $this->success(['data' => ['restrictions' => []]]);
+ }
+
+ $restrictions = AKSRT_User_Restriction::get_instance()->get_user_restrictions($user_id, false);
+ return $this->success(['data' => ['restrictions' => $restrictions]]);
+ }
+
+ public function add_user_restriction(WP_REST_Request $request): WP_REST_Response {
+ $user_id = (int) $request->get_param('user_id');
+ $user = get_userdata($user_id);
+ if (!$user) {
+ return $this->error('用户不存在', 404);
+ }
+
+ $params = $request->get_json_params() ?? [];
+ $type = sanitize_key((string) ($params['type'] ?? ''));
+ $reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
+ $scope = sanitize_key((string) ($params['scope'] ?? 'global'));
+ $expires_at = sanitize_text_field((string) ($params['expires_at'] ?? ''));
+
+ if (!$type) {
+ return $this->error('请选择限制类型');
+ }
+
+ if (!class_exists('AKSRT_User_Restriction')) {
+ return $this->error('限制系统未启用', 501);
+ }
+
+ $result = AKSRT_User_Restriction::get_instance()->restrict(
+ $user_id, $type, $reason, $scope, $expires_at, get_current_user_id()
+ );
+
+ if (!$result['success']) {
+ return $this->error($result['message']);
+ }
+
+ return $this->success(['message' => '限制已添加', 'data' => $result]);
+ }
+
+ public function remove_user_restriction(WP_REST_Request $request): WP_REST_Response {
+ $restriction_id = (int) $request->get_param('id');
+
+ if (!class_exists('AKSRT_User_Restriction')) {
+ return $this->error('限制系统未启用', 501);
+ }
+
+ $result = AKSRT_User_Restriction::get_instance()->remove_restriction($restriction_id);
+ if (!$result) {
+ return $this->error('移除失败', 500);
+ }
+
+ return $this->success(['message' => '限制已移除']);
+ }
+
+ public function get_restriction_stats(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_User_Restriction')) {
+ return $this->success(['data' => ['total' => 0, 'active' => 0, 'banned' => 0]]);
+ }
+
+ $stats = AKSRT_User_Restriction::get_instance()->get_stats();
+ return $this->success(['data' => $stats]);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-user-controller-verify-integrations.php b/inc/classes/admin/class-aksrt-admin-user-controller-verify-integrations.php
new file mode 100644
index 00000000..70903e67
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-user-controller-verify-integrations.php
@@ -0,0 +1,379 @@
+get_verify_requests(new WP_REST_Request('GET', '/'));
+ $data = $response->get_data();
+ $items = isset($data['data']['items']) && is_array($data['data']['items']) ? $data['data']['items'] : [];
+ $pending = array_values(array_filter($items, static function ($item) {
+ return ($item['status'] ?? '') === 'pending';
+ }));
+
+ return $this->success([
+ 'data' => [
+ 'requests' => array_slice($pending, 0, 50),
+ 'total' => count($pending),
+ ],
+ ]);
+ }
+
+ public function get_verify_requests(WP_REST_Request $request): WP_REST_Response {
+ global $wpdb;
+
+ $page = max(1, (int) ($request->get_param('page') ?: 1));
+ $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
+ $keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
+ $status = sanitize_key((string) $request->get_param('status'));
+ $date_from = sanitize_text_field((string) $request->get_param('date_from'));
+ $date_to = sanitize_text_field((string) $request->get_param('date_to'));
+
+ $sql = "
+ SELECT u.ID, u.display_name, u.user_email, u.user_registered,
+ status.meta_value AS verify_status,
+ reason.meta_value AS verify_reason,
+ reviewed_at.meta_value AS reviewed_at,
+ reviewer.meta_value AS reviewer_id
+ FROM {$wpdb->users} u
+ INNER JOIN {$wpdb->usermeta} status
+ ON status.user_id = u.ID AND status.meta_key = 'aksrt_user_verify_status'
+ LEFT JOIN {$wpdb->usermeta} reason
+ ON reason.user_id = u.ID AND reason.meta_key = 'aksrt_user_verify_reason'
+ LEFT JOIN {$wpdb->usermeta} reviewed_at
+ ON reviewed_at.user_id = u.ID AND reviewed_at.meta_key = 'aksrt_user_verify_reviewed_at'
+ LEFT JOIN {$wpdb->usermeta} reviewer
+ ON reviewer.user_id = u.ID AND reviewer.meta_key = 'aksrt_user_verify_reviewer'
+ ORDER BY u.user_registered DESC
+ LIMIT 500
+ ";
+
+ $rows = $wpdb->get_results($sql, ARRAY_A);
+
+ $items = [];
+ $counts = ['pending' => 0, 'verified' => 0, 'rejected' => 0, 'total' => 0];
+
+ foreach ((array) $rows as $row) {
+ $item = $this->build_verify_request_item($row);
+ $item_status = $item['status'];
+ if (isset($counts[$item_status])) {
+ $counts[$item_status]++;
+ }
+ $counts['total']++;
+
+ if ($status !== '' && $item_status !== $status) {
+ continue;
+ }
+
+ if ($date_from !== '' && strcmp((string) $item['requested_at'], $date_from . ' 00:00:00') < 0) {
+ continue;
+ }
+ if ($date_to !== '' && strcmp((string) $item['requested_at'], $date_to . ' 23:59:59') > 0) {
+ continue;
+ }
+ if ($keyword !== '' && mb_stripos(implode(' ', [
+ (string) ($item['display_name'] ?? ''),
+ (string) ($item['email'] ?? ''),
+ (string) ($item['reason'] ?? ''),
+ ]), $keyword) === false) {
+ continue;
+ }
+
+ $items[] = $item;
+ }
+
+ $total = count($items);
+ $offset = ($page - 1) * $per_page;
+
+ return $this->success([
+ 'data' => [
+ 'items' => array_values(array_slice($items, $offset, $per_page)),
+ 'total' => $total,
+ 'page' => $page,
+ 'per_page' => $per_page,
+ 'counts' => $counts,
+ 'options' => [
+ 'statuses' => [
+ ['label' => __('待审核', 'aksrt'), 'value' => 'pending'],
+ ['label' => __('已通过', 'aksrt'), 'value' => 'verified'],
+ ['label' => __('已驳回', 'aksrt'), 'value' => 'rejected'],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ public function get_verify_request_detail(WP_REST_Request $request): WP_REST_Response {
+ $user_id = (int) $request->get_param('id');
+ if ($user_id <= 0) {
+ return $this->error(__('认证申请不存在', 'aksrt'), 404);
+ }
+
+ $user = get_userdata($user_id);
+ if (!$user) {
+ return $this->error(__('用户不存在', 'aksrt'), 404);
+ }
+
+ $detail = $this->build_verify_request_item([
+ 'ID' => $user->ID,
+ 'display_name' => $user->display_name,
+ 'user_email' => $user->user_email,
+ 'user_registered' => $user->user_registered,
+ 'verify_status' => get_user_meta($user_id, 'aksrt_user_verify_status', true),
+ 'verify_reason' => get_user_meta($user_id, 'aksrt_user_verify_reason', true),
+ 'reviewed_at' => get_user_meta($user_id, 'aksrt_user_verify_reviewed_at', true),
+ 'reviewer_id' => get_user_meta($user_id, 'aksrt_user_verify_reviewer', true),
+ ]);
+
+ return $this->success([
+ 'data' => [
+ 'request' => $detail,
+ 'history' => $this->get_verify_history_for_user($user_id),
+ 'options' => [
+ 'statuses' => [
+ ['label' => __('通过认证', 'aksrt'), 'value' => 'verified'],
+ ['label' => __('驳回申请', 'aksrt'), 'value' => 'rejected'],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * POST /verify/review — approve or reject a verification request.
+ */
+ public function review_verify_request(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $user_id = (int) ($params['user_id'] ?? 0);
+ if ($user_id <= 0) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ return $this->perform_verify_review($user_id, $params);
+ }
+
+ public function review_verify_request_by_id(WP_REST_Request $request): WP_REST_Response {
+ $user_id = (int) $request->get_param('id');
+ if ($user_id <= 0) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ return $this->perform_verify_review($user_id, $this->get_json_params($request));
+ }
+
+ private function perform_verify_review(int $user_id, array $payload): WP_REST_Response {
+ $status = sanitize_key((string) ($payload['status'] ?? ''));
+ $note = sanitize_textarea_field((string) ($payload['note'] ?? ($payload['reason'] ?? '')));
+
+ if (!in_array($status, ['verified', 'rejected'], true)) {
+ return $this->error(__('审核状态无效', 'aksrt'));
+ }
+
+ $user = get_userdata($user_id);
+ if (!$user) {
+ return $this->error(__('用户不存在', 'aksrt'), 404);
+ }
+
+ update_user_meta($user_id, 'aksrt_user_verify_status', $status);
+ update_user_meta($user_id, 'aksrt_user_verify_reviewed_at', current_time('mysql'));
+ update_user_meta($user_id, 'aksrt_user_verify_reviewer', get_current_user_id());
+
+ if ($note !== '') {
+ update_user_meta($user_id, 'aksrt_user_verify_reason', $note);
+ } elseif ($status === 'verified') {
+ delete_user_meta($user_id, 'aksrt_user_verify_reason');
+ }
+
+ $log = get_option('aksrt_verify_review_log', []);
+ if (!is_array($log)) {
+ $log = [];
+ }
+ $log[] = [
+ 'user_id' => $user_id,
+ 'reviewer' => get_current_user_id(),
+ 'status' => $status,
+ 'reason' => $note,
+ 'time' => current_time('mysql'),
+ ];
+ if (count($log) > 500) {
+ $log = array_slice($log, -500);
+ }
+ update_option('aksrt_verify_review_log', $log, false);
+
+ return $this->success([
+ 'message' => $status === 'verified'
+ ? __('已通过认证', 'aksrt')
+ : __('已驳回申请', 'aksrt'),
+ ]);
+ }
+
+ private function build_verify_request_item(array $row): array {
+ $user_id = (int) ($row['ID'] ?? 0);
+ $reviewer_id = (int) ($row['reviewer_id'] ?? 0);
+ $reviewer = $reviewer_id > 0 ? get_userdata($reviewer_id) : null;
+ $status = sanitize_key((string) ($row['verify_status'] ?? 'pending'));
+ $email_verified = (bool) get_user_meta($user_id, 'aksrt_email_verified', true);
+
+ return [
+ 'user_id' => $user_id,
+ 'display_name' => (string) ($row['display_name'] ?? ''),
+ 'email' => (string) ($row['user_email'] ?? ''),
+ 'status' => $status,
+ 'reason' => (string) ($row['verify_reason'] ?? ''),
+ 'requested_at' => (string) ($row['user_registered'] ?? ''),
+ 'reviewed_at' => (string) ($row['reviewed_at'] ?? ''),
+ 'email_verified' => $email_verified,
+ 'reviewer' => $this->build_admin_user_summary($reviewer_id, $reviewer),
+ 'history_count' => count($this->get_verify_history_for_user($user_id)),
+ ];
+ }
+
+ private function get_verify_history_for_user(int $user_id): array {
+ $log = get_option('aksrt_verify_review_log', []);
+ if (!is_array($log)) {
+ return [];
+ }
+
+ $history = [];
+ foreach ($log as $entry) {
+ if ((int) ($entry['user_id'] ?? 0) !== $user_id) {
+ continue;
+ }
+
+ $reviewer_id = (int) ($entry['reviewer'] ?? 0);
+ $history[] = [
+ 'status' => sanitize_key((string) ($entry['status'] ?? '')),
+ 'reason' => (string) ($entry['reason'] ?? ''),
+ 'time' => (string) ($entry['time'] ?? ''),
+ 'reviewer' => $reviewer_id,
+ 'reviewer_name' => $reviewer_id > 0 && ($user = get_userdata($reviewer_id)) ? $user->display_name : '',
+ ];
+ }
+
+ usort($history, static function ($left, $right) {
+ return strcmp((string) ($right['time'] ?? ''), (string) ($left['time'] ?? ''));
+ });
+
+ return $history;
+ }
+
+ private function build_admin_user_summary(int $user_id, $user = null): ?array {
+ if ($user_id <= 0 && !$user instanceof WP_User) {
+ return null;
+ }
+
+ if (!$user instanceof WP_User) {
+ $user = get_userdata($user_id);
+ }
+ if (!$user instanceof WP_User) {
+ return null;
+ }
+
+ return [
+ 'id' => (int) $user->ID,
+ 'display_name' => (string) $user->display_name,
+ 'email' => (string) $user->user_email,
+ 'avatar_url' => get_avatar_url((int) $user->ID, ['size' => 64]),
+ ];
+ }
+
+ /**
+ * GET /sms/status — SMS provider connection status.
+ */
+ public function get_sms_status(): WP_REST_Response {
+ if (class_exists('AKSRT_SMS')) {
+ return $this->success(['data' => AKSRT_SMS::get_instance()->get_provider_status()]);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'enabled' => false,
+ 'provider' => 'none',
+ 'configured' => false,
+ 'available' => false,
+ 'verified' => false,
+ 'transport' => '',
+ 'missing_fields' => [],
+ 'code_length' => 6,
+ 'code_expire' => 300,
+ 'rate_limit' => 60,
+ 'daily_limit' => 10,
+ 'notify_bind' => true,
+ 'notify_login' => false,
+ ],
+ ]);
+ }
+
+ /**
+ * POST /sms/test — Send a test SMS to verify provider configuration.
+ */
+ public function test_sms_send(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_SMS')) {
+ return $this->error('短信服务不可用');
+ }
+
+ $params = $this->get_json_params($request);
+ $phone = sanitize_text_field((string) ($params['phone'] ?? ''));
+
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return $this->error('手机号格式不正确');
+ }
+
+ $result = AKSRT_SMS::get_instance()->test_provider($phone);
+
+ if (!empty($result['success'])) {
+ return $this->success(['message' => $result['message'] ?? '发送成功']);
+ }
+
+ return $this->error($result['message'] ?? '发送失败');
+ }
+
+ /**
+ * GET /wechat/status — WeChat notification service status.
+ */
+ public function get_wechat_status(): WP_REST_Response {
+ if (class_exists('AKSRT_WeChat_Notify')) {
+ return $this->success(['data' => AKSRT_WeChat_Notify::get_instance()->get_status()]);
+ }
+
+ return $this->success([
+ 'data' => [
+ 'appid' => '',
+ 'configured' => false,
+ 'has_token' => false,
+ 'available' => false,
+ ],
+ ]);
+ }
+
+ /**
+ * POST /wechat/settings — 更新微信通知设置
+ */
+ public function update_wechat_settings(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+
+ $settings = [
+ 'enabled' => !empty($params['enabled']),
+ 'app_id' => sanitize_text_field((string) ($params['app_id'] ?? '')),
+ 'app_secret' => sanitize_text_field((string) ($params['app_secret'] ?? '')),
+ 'template_id' => sanitize_text_field((string) ($params['template_id'] ?? '')),
+ ];
+
+ $existing = AKSRT_Settings::get_wechat_settings();
+ if ($settings['app_secret'] === '***') {
+ $settings['app_secret'] = $existing['app_secret'] ?? '';
+ }
+
+ $merged = array_merge($existing, $settings);
+ AKSRT_Settings::update_wechat_settings($merged);
+ delete_transient('aksrt_wechat_token');
+
+ return $this->success(['message' => __('微信设置已保存', 'aksrt')]);
+ }
+}
diff --git a/inc/classes/admin/class-aksrt-admin-user-controller.php b/inc/classes/admin/class-aksrt-admin-user-controller.php
new file mode 100644
index 00000000..2ed5b0fd
--- /dev/null
+++ b/inc/classes/admin/class-aksrt-admin-user-controller.php
@@ -0,0 +1,708 @@
+ ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_auth_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // OAuth settings
+ register_rest_route(self::API_NAMESPACE, '/settings/oauth', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_oauth'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // OAuth proxy settings
+ register_rest_route(self::API_NAMESPACE, '/settings/oauth-proxy', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_oauth_proxy'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // User background upload (logged-in users)
+ register_rest_route(self::API_NAMESPACE, '/user/background', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_upload_user_background'],
+ 'permission_callback' => function () { return is_user_logged_in(); },
+ ]);
+
+ // Comment inline image upload (logged-in users)
+ register_rest_route(self::API_NAMESPACE, '/comment/upload-image', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_upload_comment_image'],
+ 'permission_callback' => function () { return is_user_logged_in(); },
+ ]);
+
+ // Notification settings
+ register_rest_route(self::API_NAMESPACE, '/settings/notifications', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_notification_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Keep message-center aliases on this controller so existing admin pages
+ // remain compatible while notifications runtime lives in Message controller.
+ register_rest_route(self::API_NAMESPACE, '/notifications/overview', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_get_notification_overview'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/notifications/mark-read', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_mark_notification_read'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/notifications/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'rest_delete_notification_item'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/notifications/cleanup', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_run_notification_cleanup'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Mail templates (email content templates)
+ register_rest_route(self::API_NAMESPACE, '/settings/mail-templates', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'handle_mail_templates'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Credits settings (admin)
+ register_rest_route(self::API_NAMESPACE, '/credits/settings', [
+ 'methods' => ['GET', 'POST'],
+ 'callback' => [$this, 'rest_handle_credit_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Credits — current user balance (frontend)
+ register_rest_route(self::API_NAMESPACE, '/credits/me', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_get_credits_me'],
+ 'permission_callback' => function () { return is_user_logged_in(); },
+ ]);
+
+ // Credits — user list (admin)
+ register_rest_route(self::API_NAMESPACE, '/credits/users', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_list_credit_users'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Credits — adjust user balance (admin)
+ register_rest_route(self::API_NAMESPACE, '/credits/users/(?P\d+)/adjust', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_adjust_user_credits'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Credits — transaction log (admin)
+ register_rest_route(self::API_NAMESPACE, '/credits/log', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_get_credits_log'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Report ticket list (admin)
+ register_rest_route(self::API_NAMESPACE, '/report/list', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_report_list'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/users/reports', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_report_list'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/users/reports/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_report_detail'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/users/reports/(?P\d+)/process', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'process_report'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Report status update (admin)
+ register_rest_route(self::API_NAMESPACE, '/report/update-status', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_report_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Report counts (admin)
+ register_rest_route(self::API_NAMESPACE, '/report/counts', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_report_counts'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Report submit (logged-in users, front-end)
+ register_rest_route(self::API_NAMESPACE, '/report/submit', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'submit_report'],
+ 'permission_callback' => function () { return is_user_logged_in(); },
+ ]);
+
+ // Follow toggle (logged-in users)
+ register_rest_route(self::API_NAMESPACE, '/follow/toggle', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'follow_toggle'],
+ 'permission_callback' => function () { return is_user_logged_in(); },
+ ]);
+
+ // Follow status check (logged-in users)
+ register_rest_route(self::API_NAMESPACE, '/follow/status', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'follow_status'],
+ 'permission_callback' => function () { return is_user_logged_in(); },
+ ]);
+
+ // Follow counts (public)
+ register_rest_route(self::API_NAMESPACE, '/follow/counts', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'follow_counts'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // Invite codes management (admin)
+ register_rest_route(self::API_NAMESPACE, '/invite/codes', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_invite_codes'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/invite/codes', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'generate_invite_codes'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/invite/codes/(?P\d+)/disable', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'disable_invite_code'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/invite/codes/bulk', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'bulk_update_invite_codes'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ register_rest_route(self::API_NAMESPACE, '/invite/stats', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_invite_stats'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // ── Modal Auth (弹窗登录/注册) ──
+
+ // Modal login
+ register_rest_route(self::API_NAMESPACE, '/auth/modal-login', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'modal_login'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // Modal register
+ register_rest_route(self::API_NAMESPACE, '/auth/modal-register', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'modal_register'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // Current user info (for AJAX refresh)
+ register_rest_route(self::API_NAMESPACE, '/auth/me', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_current_user_info'],
+ 'permission_callback' => function () { return is_user_logged_in(); },
+ ]);
+
+ // Verify: list pending verification requests
+ register_rest_route(self::API_NAMESPACE, '/verify/pending', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_verify_pending'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/verify/requests', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_verify_requests'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/verify/requests/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_verify_request_detail'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Verify: approve or reject a verification request
+ register_rest_route(self::API_NAMESPACE, '/verify/review', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'review_verify_request'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ register_rest_route(self::API_NAMESPACE, '/verify/requests/(?P\d+)/review', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'review_verify_request_by_id'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // SMS provider status
+ register_rest_route(self::API_NAMESPACE, '/sms/status', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_sms_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // SMS test send (admin only)
+ register_rest_route(self::API_NAMESPACE, '/sms/test', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'test_sms_send'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // WeChat notification status
+ register_rest_route(self::API_NAMESPACE, '/wechat/status', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_wechat_status'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // WeChat settings update
+ register_rest_route(self::API_NAMESPACE, '/wechat/settings', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'update_wechat_settings'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // SMS auth: send verification code (public, rate-limited)
+ register_rest_route(self::API_NAMESPACE, '/auth/send-sms-code', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'auth_send_sms_code'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // SMS auth: register by phone (public)
+ register_rest_route(self::API_NAMESPACE, '/auth/phone-register', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'auth_phone_register'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // SMS auth: login by phone + password (public)
+ register_rest_route(self::API_NAMESPACE, '/auth/phone-login', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'auth_phone_login'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // SMS auth: login by phone + SMS code (public)
+ register_rest_route(self::API_NAMESPACE, '/auth/phone-code-login', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'auth_phone_code_login'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // SMS auth: bind phone to current user (logged-in)
+ register_rest_route(self::API_NAMESPACE, '/auth/bind-phone', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'auth_bind_phone'],
+ 'permission_callback' => function () { return is_user_logged_in(); },
+ ]);
+
+ // ── User Restrictions / Ban ──
+
+ // List restrictions for a user (admin)
+ register_rest_route(self::API_NAMESPACE, '/users/(?P\d+)/restrictions', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_user_restrictions'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Add restriction (admin)
+ register_rest_route(self::API_NAMESPACE, '/users/(?P\d+)/restrict', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'add_user_restriction'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Remove restriction (admin)
+ register_rest_route(self::API_NAMESPACE, '/users/restrictions/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'remove_user_restriction'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+
+ // Restriction stats (admin)
+ register_rest_route(self::API_NAMESPACE, '/users/restrictions/stats', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_restriction_stats'],
+ 'permission_callback' => [$this, 'check_permission'],
+ ]);
+ }
+
+
+ /**
+ * GET/POST /settings/mail-templates — 邮件内容模板
+ */
+ public function handle_mail_templates(WP_REST_Request $request): WP_REST_Response {
+ $option_key = 'aksrt_mail_templates';
+ $fields = [
+ 'welcome_subject', 'welcome_body',
+ 'comment_notify_subject', 'comment_notify_body',
+ 'comment_reply_subject', 'comment_reply_body',
+ 'password_reset_subject', 'password_reset_body',
+ 'email_verify_subject', 'email_verify_body',
+ ];
+
+ if ($request->get_method() === 'GET') {
+ $stored = get_option($option_key, []);
+ if (!is_array($stored)) $stored = [];
+ $result = [];
+ foreach ($fields as $f) {
+ $result[$f] = sanitize_text_field((string) ($stored[$f] ?? ''));
+ }
+ return $this->success(['data' => $result]);
+ }
+
+ // POST
+ $params = $this->get_json_params($request);
+ $clean = [];
+ foreach ($fields as $f) {
+ $clean[$f] = wp_kses_post((string) ($params[$f] ?? ''));
+ }
+ update_option($option_key, $clean);
+
+ return $this->success(['message' => __('邮件模板已保存', 'aksrt')]);
+ }
+
+ public function rest_handle_credit_settings(WP_REST_Request $request): WP_REST_Response {
+ if ($request->get_method() === 'GET') {
+ $config = AKSRT_Settings_Credits_Runtime::get_credit_settings();
+ return $this->success(['data' => $config]);
+ }
+
+ $params = $this->get_json_params($request);
+ $current = get_option('aksrt_credit_settings', []);
+ if (!is_array($current)) {
+ $current = [];
+ }
+ $merged = array_replace_recursive($current, $params);
+ update_option('aksrt_credit_settings', $merged);
+
+ return $this->success(['message' => __('积分配置已保存', 'aksrt')]);
+ }
+
+ public function rest_handle_auth_settings(WP_REST_Request $request): WP_REST_Response {
+ $provider = AKSRT_Admin_Schema_Provider::get_instance();
+ $allowed = $provider->get_allowed_fields('users', 'auth');
+
+ if ($request->get_method() === 'GET') {
+ $data = AKSRT_Settings::get_all();
+ $invite = class_exists('AKSRT_Settings_Credits_Runtime')
+ ? AKSRT_Settings_Credits_Runtime::get_invite_registration_config()
+ : [];
+
+ return $this->success(['data' => [
+ 'registration_email_verification_enabled' => AKSRT_Settings::registration_requires_email_verification(),
+ 'registration_email_verification_method' => AKSRT_Settings::registration_verification_method(),
+ 'invite_registration_enabled' => $invite['enabled'] ?? true,
+ 'invite_code_length' => class_exists('AKSRT_Settings') ? AKSRT_Settings::invite_code_length() : 6,
+ 'invite_register_field' => $invite['register_field'] ?? 'optional',
+ 'invite_reject_invalid' => $invite['reject_invalid'] ?? false,
+ ] + array_intersect_key($data, $allowed)]);
+ }
+
+ $params = $this->get_json_params($request);
+ $settings = AKSRT_Settings::get_all();
+ $sanitized = $this->sanitize_fields($params, $allowed);
+ $settings = array_merge($settings, $sanitized);
+
+ if (array_key_exists('invite_registration_enabled', $params)) {
+ $settings['invite_registration_enabled'] = !empty($params['invite_registration_enabled']);
+ }
+ if (array_key_exists('invite_code_length', $params)) {
+ $settings['invite_code_length'] = max(4, min(12, (int) $params['invite_code_length']));
+ }
+ if (array_key_exists('invite_register_field', $params)) {
+ $field = (string) $params['invite_register_field'];
+ $settings['invit_code_mode'] = $field === 'required' ? 'must' : ($field === 'hidden' ? 'close' : 'open');
+ }
+ if (array_key_exists('invite_reject_invalid', $params)) {
+ $reward = is_array($settings['invite_reward'] ?? null) ? $settings['invite_reward'] : [];
+ $reward['reject_invalid'] = !empty($params['invite_reject_invalid']);
+ $settings['invite_reward'] = $reward;
+ }
+ if (array_key_exists('registration_email_verification_enabled', $params)) {
+ $settings['registration_email_verification_enabled'] = !empty($params['registration_email_verification_enabled']);
+ }
+ if (array_key_exists('registration_email_verification_method', $params)) {
+ $method = (string) $params['registration_email_verification_method'];
+ $settings['registration_email_verification_method'] = in_array($method, ['link', 'code'], true) ? $method : 'link';
+ }
+
+ update_option('aksrt_settings', $settings);
+ AKSRT_Settings::flush_settings_cache();
+
+ return $this->success(['message' => __('注册登录配置已保存', 'aksrt')]);
+ }
+
+ public function rest_handle_notification_settings(WP_REST_Request $request): WP_REST_Response {
+ $provider = AKSRT_Admin_Schema_Provider::get_instance();
+ $allowed = $provider->get_allowed_fields('users', 'notifications');
+
+ if ($request->get_method() === 'GET') {
+ $data = AKSRT_Settings::get_all();
+ return $this->success(['data' => array_intersect_key($data, $allowed)]);
+ }
+
+ $params = $this->get_json_params($request);
+ $settings = AKSRT_Settings::get_all();
+ $settings = array_merge($settings, $this->sanitize_fields($params, $allowed));
+ update_option('aksrt_settings', $settings);
+ AKSRT_Settings::flush_settings_cache();
+
+ return $this->success(['message' => __('消息通知配置已保存', 'aksrt')]);
+ }
+
+ public function rest_get_notification_overview(WP_REST_Request $request): WP_REST_Response {
+ $page = max(1, (int) $request->get_param('page'));
+ $per_page = min(100, max(1, (int) $request->get_param('per_page')));
+ $type = sanitize_key((string) $request->get_param('type'));
+ $is_read = $request->get_param('is_read');
+
+ $items = [];
+ $unread = 0;
+ if (class_exists('AKSRT_Message')) {
+ $args = ['limit' => $per_page, 'offset' => ($page - 1) * $per_page];
+ if ($type !== '') {
+ $args['type'] = $type;
+ }
+ $items = AKSRT_Message::get_instance()->get_notifications(get_current_user_id(), $args);
+ if ($is_read !== null && $is_read !== '') {
+ $want_read = (string) $is_read === '1';
+ $items = array_values(array_filter($items, static function ($item) use ($want_read) {
+ return !empty($item['is_read']) === $want_read;
+ }));
+ }
+ $unread = AKSRT_Message::get_instance()->get_unread_count(get_current_user_id());
+ }
+
+ $total = count($items);
+ $types = [];
+ $today = 0;
+ $system = 0;
+ foreach ($items as $item) {
+ $itemType = sanitize_key((string) ($item['type'] ?? 'system'));
+ $types[$itemType] = ($types[$itemType] ?? 0) + 1;
+ if ($itemType === 'system' || $itemType === 'notice') {
+ $system++;
+ }
+ $created = (string) ($item['created_at'] ?? '');
+ if ($created !== '' && str_starts_with($created, gmdate('Y-m-d'))) {
+ $today++;
+ }
+ }
+
+ return $this->success(['data' => [
+ 'recent' => $items,
+ 'types' => array_map(static fn($key, $count) => ['type' => $key, 'count' => $count], array_keys($types), array_values($types)),
+ 'summary' => [
+ 'total' => $total,
+ 'unread' => $unread,
+ 'today' => $today,
+ 'system' => $system,
+ ],
+ 'pagination' => [
+ 'page' => $page,
+ 'per_page' => $per_page,
+ 'total_items' => $total,
+ 'total_pages' => 1,
+ ],
+ ]]);
+ }
+
+ public function rest_mark_notification_read(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Message')) {
+ return $this->success(['message' => __('消息系统未启用', 'aksrt')]);
+ }
+
+ $params = $this->get_json_params($request);
+ $msg = AKSRT_Message::get_instance();
+ if (!empty($params['mark_all'])) {
+ $msg->mark_all_read(get_current_user_id());
+ } else {
+ $id = sanitize_text_field((string) ($params['id'] ?? ''));
+ if ($id !== '') {
+ $msg->mark_read(get_current_user_id(), $id);
+ }
+ }
+
+ return $this->success(['message' => __('已更新消息状态', 'aksrt')]);
+ }
+
+ public function rest_delete_notification_item(WP_REST_Request $request): WP_REST_Response {
+ return $this->success(['message' => __('当前版本暂未提供后台删除单条消息能力', 'aksrt')]);
+ }
+
+ public function rest_run_notification_cleanup(WP_REST_Request $request): WP_REST_Response {
+ return $this->success(['message' => __('消息清理任务已受理', 'aksrt')]);
+ }
+
+ public function rest_handle_oauth(WP_REST_Request $request): WP_REST_Response {
+ $provider = AKSRT_Admin_Schema_Provider::get_instance();
+ $allowed = $provider->get_allowed_fields('users', 'oauth');
+
+ if ($request->get_method() === 'GET') {
+ $data = AKSRT_Settings::get_all();
+ return $this->success(['data' => array_intersect_key($data, $allowed)]);
+ }
+
+ $params = $this->get_json_params($request);
+ $settings = AKSRT_Settings::get_all();
+ $settings = array_merge($settings, $this->sanitize_fields($params, $allowed));
+ update_option('aksrt_settings', $settings);
+ AKSRT_Settings::flush_settings_cache();
+
+ return $this->success(['message' => __('第三方登录配置已保存', 'aksrt')]);
+ }
+
+ public function rest_handle_oauth_proxy(WP_REST_Request $request): WP_REST_Response {
+ $keys = [
+ 'oauth_proxy_mode',
+ 'oauth_proxy_secret',
+ 'oauth_proxy_server_url',
+ 'oauth_proxy_server_providers',
+ 'oauth_proxy_client_providers',
+ 'oauth_proxy_gzh_type',
+ ];
+
+ if ($request->get_method() === 'GET') {
+ $all = AKSRT_Settings::get_all();
+ $data = [];
+ foreach ($keys as $key) {
+ $data[$key] = $all[$key] ?? null;
+ }
+ return $this->success(['data' => $data]);
+ }
+
+ $params = $this->get_json_params($request);
+ $settings = AKSRT_Settings::get_all();
+ foreach ($keys as $key) {
+ if (!array_key_exists($key, $params)) {
+ continue;
+ }
+ $settings[$key] = is_array($params[$key])
+ ? array_values(array_map('sanitize_key', (array) $params[$key]))
+ : sanitize_text_field((string) $params[$key]);
+ }
+ update_option('aksrt_settings', $settings);
+ AKSRT_Settings::flush_settings_cache();
+
+ return $this->success(['message' => __('OAuth 代理配置已保存', 'aksrt')]);
+ }
+
+ public function rest_upload_user_background(WP_REST_Request $request): WP_REST_Response {
+ return $this->error(__('当前版本暂未提供后台 REST 背景图上传,请使用标准媒体上传流程。', 'aksrt'), 501);
+ }
+
+ public function rest_upload_comment_image(WP_REST_Request $request): WP_REST_Response {
+ return $this->error(__('当前版本暂未提供后台 REST 评论图片上传,请使用标准媒体上传流程。', 'aksrt'), 501);
+ }
+
+ public function rest_get_credits_me(WP_REST_Request $request): WP_REST_Response {
+ $user_id = get_current_user_id();
+ return $this->success(['data' => [
+ 'user_id' => $user_id,
+ 'balance' => class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($user_id) : 0,
+ ]]);
+ }
+
+ public function rest_list_credit_users(WP_REST_Request $request): WP_REST_Response {
+ $args = [
+ 'search' => (string) $request->get_param('search'),
+ 'page' => (int) $request->get_param('page'),
+ 'per_page' => (int) $request->get_param('per_page'),
+ ];
+ $data = class_exists('AKSRT_Credits') ? AKSRT_Credits::list_users($args) : ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
+ return $this->success(['data' => $data]);
+ }
+
+ public function rest_adjust_user_credits(WP_REST_Request $request): WP_REST_Response {
+ if (!class_exists('AKSRT_Credits')) {
+ return $this->error(__('积分系统不可用', 'aksrt'), 500);
+ }
+
+ $user_id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $delta = (int) ($params['delta'] ?? 0);
+ $reason = sanitize_text_field((string) ($params['reason'] ?? 'admin'));
+
+ if ($user_id <= 0 || $delta === 0) {
+ return $this->error(__('用户或积分变动值无效', 'aksrt'), 422);
+ }
+
+ if ($delta > 0) {
+ $result = AKSRT_Credits::add($user_id, $delta, $reason, ['type' => 'user', 'id' => $user_id], 'Admin adjustment');
+ } else {
+ $result = AKSRT_Credits::deduct($user_id, abs($delta), $reason, ['type' => 'user', 'id' => $user_id], 'Admin adjustment');
+ }
+
+ if (is_wp_error($result)) {
+ return $this->error($result->get_error_message(), 422);
+ }
+
+ return $this->success(['data' => [
+ 'user_id' => $user_id,
+ 'balance' => (int) $result,
+ ], 'message' => __('积分调整已保存', 'aksrt')]);
+ }
+
+ public function rest_get_credits_log(WP_REST_Request $request): WP_REST_Response {
+ $args = [
+ 'user_id' => (int) $request->get_param('user_id'),
+ 'reason' => (string) $request->get_param('reason'),
+ 'page' => (int) $request->get_param('page'),
+ 'per_page' => (int) $request->get_param('per_page'),
+ 'date_from' => (string) $request->get_param('date_from'),
+ 'date_to' => (string) $request->get_param('date_to'),
+ ];
+ $data = class_exists('AKSRT_Credits') ? AKSRT_Credits::get_log($args) : ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
+ return $this->success(['data' => $data]);
+ }
+}
diff --git a/inc/classes/admin/forum-ctrl-traits/trait-aksrt-admin-forum-controller-part-1.php b/inc/classes/admin/forum-ctrl-traits/trait-aksrt-admin-forum-controller-part-1.php
new file mode 100644
index 00000000..374b0a4f
--- /dev/null
+++ b/inc/classes/admin/forum-ctrl-traits/trait-aksrt-admin-forum-controller-part-1.php
@@ -0,0 +1,384 @@
+get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $plate_id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+
+ $result = $forum->update_plate($plate_id, $params);
+ if (!$result) {
+ return $this->error(__('更新版块失败', 'aksrt'));
+ }
+
+ return $this->success(['message' => __('版块已更新', 'aksrt')]);
+ }
+
+ public function delete_plate(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $plate_id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $cascade = sanitize_key((string) ($params['cascade'] ?? 'trash'));
+ $move_to = (int) ($params['move_to'] ?? 0);
+
+ $result = $forum->delete_plate($plate_id, $cascade, $move_to);
+ if (!$result) {
+ return $this->error(__('删除版块失败', 'aksrt'));
+ }
+
+ return $this->success(['message' => __('版块已删除', 'aksrt')]);
+ }
+
+ public function reorder_plates(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $params = $this->get_json_params($request);
+ $order = $params['order'] ?? [];
+ if (!is_array($order) || empty($order)) {
+ return $this->error(__('排序参数无效', 'aksrt'));
+ }
+
+ $forum->reorder_plates($order);
+
+ return $this->success(['message' => __('排序已更新', 'aksrt')]);
+ }
+
+ public function toggle_follow_plate(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $plate_id = (int) $request->get_param('id');
+ $user_id = get_current_user_id();
+
+ $result = $forum->toggle_follow_plate($user_id, $plate_id);
+ return $this->success($result);
+ }
+
+ public function get_plate_categories(): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->success(['data' => []]);
+ }
+
+ return $this->success(['data' => $forum->get_plate_categories()]);
+ }
+
+ public function get_sorted_plates(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->success(['data' => []]);
+ }
+
+ $orderby = sanitize_key((string) ($request->get_param('orderby') ?: 'menu_order'));
+ $order = sanitize_key((string) ($request->get_param('order') ?: 'ASC'));
+ $limit = max(1, min(100, (int) ($request->get_param('limit') ?: 50)));
+
+ return $this->success(['data' => $forum->get_sorted_plates($orderby, $order, $limit)]);
+ }
+
+ public function get_topics(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1]]);
+ }
+
+ $args = [
+ 'page' => max(1, (int) ($request->get_param('page') ?: 1)),
+ 'per_page' => max(1, min(100, (int) ($request->get_param('per_page') ?: 20))),
+ 'status' => sanitize_key((string) ($request->get_param('status') ?: 'all')),
+ 'board_id' => (int) ($request->get_param('board_id') ?: 0),
+ 'visibility' => sanitize_key((string) ($request->get_param('visibility') ?: '')),
+ 'review_status' => sanitize_key((string) ($request->get_param('review_status') ?: '')),
+ 'keyword' => sanitize_text_field((string) ($request->get_param('keyword') ?: '')),
+ 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?: 'date')),
+ 'viewer_user_id' => get_current_user_id(),
+ 'include_hidden' => true,
+ ];
+
+ $view = $forum->get_topic_list($args);
+
+ return $this->success([
+ 'data' => [
+ 'items' => $view['items'],
+ 'total' => $view['total'],
+ 'page' => $view['page'],
+ 'total_pages' => $view['max_pages'],
+ 'options' => [
+ 'boards' => $forum->get_boards(),
+ 'visibility' => $forum->get_visibility_options(),
+ 'review' => $forum->get_review_options(),
+ 'status' => [
+ ['label' => __('全部', 'aksrt'), 'value' => 'all'],
+ ['label' => __('已发布', 'aksrt'), 'value' => 'publish'],
+ ['label' => __('待审核', 'aksrt'), 'value' => 'pending'],
+ ['label' => __('草稿/拒绝', 'aksrt'), 'value' => 'draft'],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ public function get_topic_detail(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $topic_id = (int) $request->get_param('id');
+ if ($topic_id <= 0) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ return $this->success([
+ 'data' => $forum->get_topic_detail($topic_id, get_current_user_id()),
+ ]);
+ }
+
+ public function moderate_topic(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $topic_id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $action = sanitize_key((string) ($params['action'] ?? ''));
+
+ if ($topic_id <= 0 || $action === '') {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ $result = $forum->moderate_topic($topic_id, $action, $params, get_current_user_id());
+ if (is_wp_error($result)) {
+ return $this->error($result->get_error_message(), $result->get_error_code() === 'forbidden' ? 403 : 400);
+ }
+
+ return $this->success([
+ 'message' => $result['message'] ?? __('治理动作已执行', 'aksrt'),
+ 'data' => $result,
+ ]);
+ }
+
+ public function vote_topic(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $topic_id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $option_ids = $params['option_ids'] ?? [];
+ if (!is_array($option_ids)) {
+ $option_ids = [];
+ }
+
+ $result = $forum->submit_vote($topic_id, get_current_user_id(), $option_ids);
+ if (is_wp_error($result)) {
+ $status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
+ return $this->error($result->get_error_message(), $status);
+ }
+
+ return $this->success([
+ 'message' => $result['message'] ?? __('投票成功', 'aksrt'),
+ 'data' => $result,
+ ]);
+ }
+
+ /**
+ * Legacy admin moderation endpoint kept for compatibility.
+ */
+ public function update_topic_legacy(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $topic_id = (int) ($params['topic_id'] ?? 0);
+ if ($topic_id <= 0) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $actions = [];
+ if (isset($params['sticky'])) {
+ $actions[] = ['action' => 'sticky', 'value' => !empty($params['sticky'])];
+ }
+ if (isset($params['featured'])) {
+ $actions[] = ['action' => 'featured', 'value' => !empty($params['featured'])];
+ }
+ if (isset($params['locked'])) {
+ $actions[] = ['action' => 'locked', 'value' => !empty($params['locked'])];
+ }
+
+ foreach ($actions as $action) {
+ $result = $forum->moderate_topic($topic_id, $action['action'], ['value' => $action['value']], get_current_user_id());
+ if (is_wp_error($result)) {
+ return $this->error($result->get_error_message());
+ }
+ }
+
+ return $this->success(['message' => __('治理状态已更新', 'aksrt')]);
+ }
+
+ public function get_moderators(): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->success(['data' => ['boards' => []]]);
+ }
+
+ $board_id = isset($_GET['board_id']) ? (int) $_GET['board_id'] : 0;
+ $result = [];
+ foreach ($forum->get_boards() as $board) {
+ $current_board_id = (int) ($board['id'] ?? 0);
+ if ($board_id > 0 && $current_board_id !== $board_id) {
+ continue;
+ }
+
+ $result[] = [
+ 'board_id' => $current_board_id,
+ 'board_name' => (string) ($board['name'] ?? ''),
+ 'topic_count' => (int) ($board['post_count'] ?? 0),
+ 'pending_application_count' => (int) ($board['pending_application_count'] ?? 0),
+ 'moderators' => $forum->get_board_moderator_profiles_with_roles($current_board_id),
+ ];
+ }
+
+ return $this->success(['data' => ['boards' => $result]]);
+ }
+
+ public function update_moderators(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $params = $this->get_json_params($request);
+ $board_id = (int) ($params['board_id'] ?? 0);
+ $user_id = (int) ($params['user_id'] ?? 0);
+ $action = sanitize_key((string) ($params['action'] ?? ''));
+ $role = sanitize_key((string) ($params['role'] ?? 'moderator'));
+
+ if ($board_id <= 0 || $user_id <= 0 || !in_array($action, ['add', 'remove'], true)) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ $valid_roles = ['super_moderator', 'moderator', 'junior_moderator'];
+ if (!in_array($role, $valid_roles, true)) {
+ $role = 'moderator';
+ }
+
+ $board = get_term($board_id, 'aksrt_forum_board');
+ if (!$board || is_wp_error($board)) {
+ return $this->error(__('版块不存在', 'aksrt'), 404);
+ }
+
+ $user = get_userdata($user_id);
+ if (!$user) {
+ return $this->error(__('用户不存在', 'aksrt'), 404);
+ }
+
+ if ($action === 'add') {
+ $forum->add_moderator($user_id, $board_id, $role);
+ } else {
+ $forum->remove_moderator($user_id, $board_id);
+ }
+
+ return $this->success([
+ 'message' => $action === 'add' ? __('版主已添加', 'aksrt') : __('版主已移除', 'aksrt'),
+ ]);
+ }
+
+ public function change_moderator_role(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $params = $this->get_json_params($request);
+ $board_id = (int) ($params['board_id'] ?? 0);
+ $user_id = (int) ($params['user_id'] ?? 0);
+ $role = sanitize_key((string) ($params['role'] ?? ''));
+
+ if ($board_id <= 0 || $user_id <= 0 || !$role) {
+ return $this->error(__('参数无效', 'aksrt'));
+ }
+
+ $valid_roles = ['super_moderator', 'moderator', 'junior_moderator'];
+ if (!in_array($role, $valid_roles, true)) {
+ return $this->error(__('无效的角色', 'aksrt'));
+ }
+
+ $result = $forum->set_moderator_role($user_id, $board_id, $role);
+ if (!$result) {
+ return $this->error(__('设置角色失败,用户可能不是该版块版主', 'aksrt'));
+ }
+
+ return $this->success(['message' => __('角色已更新', 'aksrt')]);
+ }
+
+ public function apply_moderator(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $params = $this->get_json_params($request);
+ $board_id = (int) ($params['board_id'] ?? 0);
+ $reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
+
+ $result = $forum->apply_moderator(get_current_user_id(), $board_id, $reason);
+ if (is_wp_error($result)) {
+ $status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
+ return $this->error($result->get_error_message(), $status);
+ }
+
+ return $this->success([
+ 'message' => $result['message'] ?? __('申请已提交', 'aksrt'),
+ 'data' => $result,
+ ]);
+ }
+
+ public function get_moderator_applications(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->success(['data' => ['items' => [], 'total' => 0]]);
+ }
+
+ $items = $forum->get_moderator_application_list([
+ 'status' => sanitize_key((string) ($request->get_param('status') ?: '')),
+ 'board_id' => (int) ($request->get_param('board_id') ?: 0),
+ 'keyword' => sanitize_text_field((string) ($request->get_param('keyword') ?: '')),
+ ]);
+
+ return $this->success([
+ 'data' => [
+ 'items' => $items,
+ 'total' => count($items),
+ 'options' => [
+ 'boards' => $forum->get_boards(),
+ 'status' => [
+ ['label' => __('全部', 'aksrt'), 'value' => ''],
+ ['label' => __('待处理', 'aksrt'), 'value' => AKSRT_Forum::APPLICATION_PENDING],
+ ['label' => __('已通过', 'aksrt'), 'value' => AKSRT_Forum::APPLICATION_APPROVED],
+ ['label' => __('已拒绝', 'aksrt'), 'value' => AKSRT_Forum::APPLICATION_REJECTED],
+ ],
+ ],
+ ],
+ ]);
+ }
+}
diff --git a/inc/classes/admin/forum-ctrl-traits/trait-aksrt-admin-forum-controller-part-2.php b/inc/classes/admin/forum-ctrl-traits/trait-aksrt-admin-forum-controller-part-2.php
new file mode 100644
index 00000000..8a50571a
--- /dev/null
+++ b/inc/classes/admin/forum-ctrl-traits/trait-aksrt-admin-forum-controller-part-2.php
@@ -0,0 +1,182 @@
+get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $application_id = (string) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $status = sanitize_key((string) ($params['status'] ?? ''));
+ $note = sanitize_textarea_field((string) ($params['note'] ?? ''));
+
+ $result = $forum->review_moderator_application($application_id, $status, $note, get_current_user_id());
+ if (is_wp_error($result)) {
+ return $this->error($result->get_error_message());
+ }
+
+ return $this->success([
+ 'message' => $result['message'] ?? __('申请状态已更新', 'aksrt'),
+ 'data' => $result,
+ ]);
+ }
+
+ public function toggle_favorite(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $topic_id = (int) $request->get_param('id');
+ $result = $forum->toggle_favorite(get_current_user_id(), $topic_id);
+ if (is_wp_error($result)) {
+ $status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
+ return $this->error($result->get_error_message(), $status);
+ }
+
+ return $this->success($result);
+ }
+
+ public function score_topic(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $topic_id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $amount = (int) ($params['amount'] ?? 0);
+
+ $result = $forum->score_topic(get_current_user_id(), $topic_id, $amount);
+ if (is_wp_error($result)) {
+ $status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
+ return $this->error($result->get_error_message(), $status);
+ }
+
+ return $this->success($result);
+ }
+
+ public function adopt_answer(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $topic_id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $comment_id = (int) ($params['comment_id'] ?? 0);
+
+ $result = $forum->adopt_answer($topic_id, $comment_id, get_current_user_id());
+ if (is_wp_error($result)) {
+ $status = $result->get_error_code() === 'forbidden' ? 403 : 400;
+ return $this->error($result->get_error_message(), $status);
+ }
+
+ return $this->success($result);
+ }
+
+ public function check_logged_in_permission(): bool {
+ return is_user_logged_in();
+ }
+
+ private function get_forum_service(): ?AKSRT_Forum {
+ return class_exists('AKSRT_Forum') ? AKSRT_Forum::get_instance() : null;
+ }
+
+ public function frontend_create_plate(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $user_id = get_current_user_id();
+ if (!$forum->is_forum_admin($user_id)) {
+ return $this->error(__('仅论坛管理员可创建版块', 'aksrt'), 403);
+ }
+
+ $params = $this->get_json_params($request);
+ $data = [
+ 'name' => sanitize_text_field((string) ($params['name'] ?? '')),
+ 'description' => sanitize_textarea_field((string) ($params['description'] ?? '')),
+ 'icon' => sanitize_text_field((string) ($params['icon'] ?? '')),
+ 'cover_url' => esc_url_raw((string) ($params['cover_url'] ?? '')),
+ 'parent_id' => (int) ($params['parent_id'] ?? 0),
+ 'plate_cat' => (int) ($params['plate_cat'] ?? 0),
+ ];
+
+ if ($data['name'] === '') {
+ return $this->error(__('版块名称不能为空', 'aksrt'));
+ }
+
+ $result = $forum->create_plate($data);
+ if (is_wp_error($result)) {
+ return $this->error($result->get_error_message());
+ }
+
+ return $this->success($result);
+ }
+
+ public function frontend_update_plate(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $user_id = get_current_user_id();
+ $plate_id = (int) $request->get_param('id');
+
+ $post = get_post($plate_id);
+ if (!$post || $post->post_type !== 'aksrt_forum_plate') {
+ return $this->error(__('版块不存在', 'aksrt'));
+ }
+
+ $is_author = (int) $post->post_author === $user_id;
+ if (!$is_author && !$forum->is_forum_admin($user_id)) {
+ return $this->error(__('无权编辑此版块', 'aksrt'), 403);
+ }
+
+ $params = $this->get_json_params($request);
+ $data = [
+ 'name' => sanitize_text_field((string) ($params['name'] ?? '')),
+ 'description' => sanitize_textarea_field((string) ($params['description'] ?? '')),
+ 'icon' => sanitize_text_field((string) ($params['icon'] ?? '')),
+ 'cover_url' => esc_url_raw((string) ($params['cover_url'] ?? '')),
+ ];
+
+ $result = $forum->update_plate($plate_id, $data);
+ if (is_wp_error($result)) {
+ return $this->error($result->get_error_message());
+ }
+
+ return $this->success($result);
+ }
+
+ public function frontend_delete_plate(WP_REST_Request $request): WP_REST_Response {
+ $forum = $this->get_forum_service();
+ if (!$forum) {
+ return $this->error(__('论坛系统未启用', 'aksrt'));
+ }
+
+ $user_id = get_current_user_id();
+ if (!$forum->is_forum_admin($user_id)) {
+ return $this->error(__('仅论坛管理员可删除版块', 'aksrt'), 403);
+ }
+
+ $plate_id = (int) $request->get_param('id');
+ $params = $this->get_json_params($request);
+ $cascade = sanitize_key((string) ($params['cascade'] ?? 'trash'));
+ $move_to = (int) ($params['move_to'] ?? 0);
+
+ $result = $forum->delete_plate($plate_id, $cascade, $move_to);
+ if (!$result) {
+ return $this->error(__('删除版块失败', 'aksrt'));
+ }
+
+ return $this->success(['message' => __('版块已删除', 'aksrt')]);
+ }
+}
diff --git a/inc/classes/admin/global-ctrl-traits/trait-aksrt-admin-global-controller-part-1.php b/inc/classes/admin/global-ctrl-traits/trait-aksrt-admin-global-controller-part-1.php
new file mode 100644
index 00000000..b51bdbb6
--- /dev/null
+++ b/inc/classes/admin/global-ctrl-traits/trait-aksrt-admin-global-controller-part-1.php
@@ -0,0 +1,264 @@
+get_json_params($request);
+ $ordered_ids = $params['ids'] ?? [];
+
+ if (!is_array($ordered_ids)) {
+ return new WP_REST_Response(['code' => 'error', 'message' => __('参数错误', 'aksrt')], 400);
+ }
+
+ if (count($ordered_ids) > 5) {
+ return new WP_REST_Response(['code' => 'error', 'message' => __('Footer tabbar supports at most 5 items.', 'aksrt')], 400);
+ }
+
+ $tabbar = get_option('aksrt_footer_tabbar', []);
+ $items = $tabbar['items'] ?? [];
+
+ $order_map = array_flip($ordered_ids);
+ usort($items, function ($a, $b) use ($order_map) {
+ $idx_a = $order_map[($a['id'] ?? '')] ?? 999;
+ $idx_b = $order_map[($b['id'] ?? '')] ?? 999;
+ return $idx_a <=> $idx_b;
+ });
+
+ foreach ($items as $index => &$item) {
+ $item['sort_order'] = $index;
+ }
+
+ $tabbar['items'] = $items;
+ update_option('aksrt_footer_tabbar', $tabbar);
+
+ return rest_ensure_response(['code' => 'success', 'message' => __('Order saved.', 'aksrt')]);
+ }
+
+ private function sanitize_tabbar_item(array $item): array {
+ $valid_types = ['home', 'category', 'link', 'user', 'auth', 'search', 'add'];
+ $type = sanitize_text_field($item['type'] ?? 'link');
+ if (!in_array($type, $valid_types, true)) {
+ $type = 'link';
+ }
+ $badge_mode = sanitize_text_field($item['badge_mode'] ?? 'static');
+ if (!in_array($badge_mode, ['static', 'dynamic'], true)) {
+ $badge_mode = 'static';
+ }
+
+ $add_items = [];
+ if (!empty($item['add_items']) && is_array($item['add_items'])) {
+ foreach ($item['add_items'] as $sub_item) {
+ $label = sanitize_text_field($sub_item['label'] ?? '');
+ $icon = sanitize_text_field($sub_item['icon'] ?? 'fas fa-pen');
+ $url = esc_url_raw($sub_item['url'] ?? '');
+ if ($label === '') {
+ continue;
+ }
+ $add_items[] = [
+ 'id' => sanitize_text_field($sub_item['id'] ?? uniqid('add_')),
+ 'label' => $label,
+ 'icon' => $icon,
+ 'url' => $url,
+ ];
+ }
+ }
+
+ return [
+ 'id' => sanitize_text_field($item['id'] ?? uniqid('tab_')),
+ 'type' => $type,
+ 'icon' => sanitize_text_field($item['icon'] ?? ''),
+ 'text' => sanitize_text_field($item['text'] ?? ''),
+ 'link' => esc_url_raw($item['link'] ?? ''),
+ 'badge' => sanitize_text_field($item['badge'] ?? ''),
+ 'badge_mode' => $badge_mode,
+ 'category_id' => absint($item['category_id'] ?? 0),
+ 'search_default_type' => (function() use ($item) {
+ $v = $item['search_default_type'] ?? 'post';
+ return in_array($v, ['post', 'category', 'tag'], true) ? $v : 'post';
+ })(),
+ 'new_window' => !empty($item['new_window']),
+ 'add_items' => $add_items,
+ 'enabled' => !empty($item['enabled']),
+ 'sort_order' => (int) ($item['sort_order'] ?? 0),
+ 'ontop' => !empty($item['ontop']),
+ ];
+ }
+
+ public function rest_handle_floating_buttons(WP_REST_Request $request): WP_REST_Response {
+ if ($request->get_method() === 'GET') {
+ $settings = class_exists('AKSRT_Settings')
+ ? AKSRT_Settings::get_floating_buttons_settings()
+ : [
+ 'enabled' => true,
+ 'style' => 'round',
+ 'position' => 'bottom',
+ 'scroll_hide' => false,
+ 'scroll_down_hide' => true,
+ 'filter_blur' => [],
+ 'items' => [],
+ ];
+
+ return rest_ensure_response(['code' => 'success', 'data' => $settings]);
+ }
+
+ $params = $this->get_json_params($request);
+ $items = isset($params['items']) ? array_map([$this, 'sanitize_floating_button_item'], (array) $params['items']) : [];
+
+ $filter_blur = [];
+ if (!empty($params['filter_blur']) && is_array($params['filter_blur'])) {
+ foreach ($params['filter_blur'] as $device) {
+ $device = sanitize_text_field((string) $device);
+ if (in_array($device, ['pc', 'mobile'], true)) {
+ $filter_blur[] = $device;
+ }
+ }
+ $filter_blur = array_values(array_unique($filter_blur));
+ }
+
+ $settings = [
+ 'enabled' => !empty($params['enabled']),
+ 'style' => in_array($params['style'] ?? '', ['round', 'square'], true) ? $params['style'] : 'round',
+ 'position' => in_array($params['position'] ?? '', ['bottom', 'center'], true) ? $params['position'] : 'bottom',
+ 'scroll_hide' => !empty($params['scroll_hide']),
+ 'scroll_down_hide' => !empty($params['scroll_down_hide']),
+ 'max' => max(1, min(10, (int) ($params['max'] ?? 5))),
+ 'size' => max(32, min(64, (int) ($params['size'] ?? 44))),
+ 'spacing' => max(4, min(24, (int) ($params['spacing'] ?? 10))),
+ 'shadow' => !empty($params['shadow']),
+ 'animation' => !empty($params['animation']),
+ 'opacity' => max(30, min(100, (int) ($params['opacity'] ?? 100))),
+ 'active_color' => sanitize_hex_color($params['active_color'] ?? '') ?: '',
+ 'bg_color' => sanitize_hex_color($params['bg_color'] ?? '') ?: '',
+ 'pc_show' => !empty($params['pc_show']),
+ 'm_show' => !empty($params['m_show']),
+ 'filter_blur' => $filter_blur,
+ 'items' => $items,
+ ];
+
+ update_option('aksrt_floating_buttons', $settings);
+
+ if (class_exists('AKSRT_Settings')) {
+ AKSRT_Settings::flush_settings_cache();
+ }
+
+ return rest_ensure_response(['code' => 'success', 'message' => __('Floating button settings saved.', 'aksrt')]);
+ }
+
+ public function rest_reorder_floating_buttons(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $ordered_ids = $params['ids'] ?? [];
+
+ if (!is_array($ordered_ids)) {
+ return new WP_REST_Response(['code' => 'error', 'message' => __('参数错误', 'aksrt')], 400);
+ }
+
+ $settings = get_option('aksrt_floating_buttons', []);
+ $items = $settings['items'] ?? [];
+
+ $order_map = array_flip($ordered_ids);
+ usort($items, function ($a, $b) use ($order_map) {
+ $idx_a = $order_map[($a['id'] ?? '')] ?? 999;
+ $idx_b = $order_map[($b['id'] ?? '')] ?? 999;
+ return $idx_a <=> $idx_b;
+ });
+
+ foreach ($items as $index => &$item) {
+ $item['sort_order'] = $index;
+ }
+ unset($item);
+
+ $settings['items'] = $items;
+ update_option('aksrt_floating_buttons', $settings);
+
+ if (class_exists('AKSRT_Settings')) {
+ AKSRT_Settings::flush_settings_cache();
+ }
+
+ return rest_ensure_response(['code' => 'success', 'message' => __('Order saved.', 'aksrt')]);
+ }
+
+ private function sanitize_floating_button_item(array $item): array {
+ $valid_types = ['back_top', 'theme_mode', 'qrcode', 'service_qq', 'service_wechat', 'pay_vip', 'custom', 'add', 'shop_cart'];
+ $type = sanitize_text_field($item['type'] ?? 'custom');
+ if (!in_array($type, $valid_types, true)) {
+ $type = 'custom';
+ }
+
+ $hover_width = (int) ($item['hover_width'] ?? 200);
+ if ($hover_width < 120) {
+ $hover_width = 120;
+ }
+ if ($hover_width > 480) {
+ $hover_width = 480;
+ }
+
+ $qq = preg_replace('/\D+/', '', (string) ($item['qq'] ?? ''));
+ $action_mode = sanitize_text_field($item['action_mode'] ?? 'link');
+ if (!in_array($action_mode, ['link', 'qrcode'], true)) {
+ $action_mode = 'link';
+ }
+ $qr_size = (int) ($item['qr_size'] ?? 120);
+ if ($qr_size < 80) {
+ $qr_size = 80;
+ }
+ if ($qr_size > 240) {
+ $qr_size = 240;
+ }
+
+ $add_items = [];
+ if (!empty($item['add_items']) && is_array($item['add_items'])) {
+ foreach ($item['add_items'] as $add_item) {
+ if (!is_array($add_item)) {
+ continue;
+ }
+ $label = sanitize_text_field($add_item['label'] ?? '');
+ $url = esc_url_raw($add_item['url'] ?? '');
+ $icon_item = sanitize_text_field($add_item['icon'] ?? '');
+ if ($label === '' && $url === '') {
+ continue;
+ }
+ $add_items[] = [
+ 'id' => sanitize_text_field($add_item['id'] ?? uniqid('add_')),
+ 'label' => $label,
+ 'url' => $url,
+ 'icon' => $icon_item,
+ ];
+ }
+ }
+
+ $icon = sanitize_text_field($item['icon'] ?? '');
+ $icon_dark = sanitize_text_field($item['icon_dark'] ?? '');
+ $icon_light = sanitize_text_field($item['icon_light'] ?? '');
+ if ($type === 'theme_mode') {
+ $icon = 'fas fa-sun';
+ $icon_dark = '';
+ $icon_light = '';
+ }
+
+ return [
+ 'id' => sanitize_text_field($item['id'] ?? uniqid('float_')),
+ 'type' => $type,
+ 'icon' => $icon,
+ 'icon_dark' => $icon_dark,
+ 'icon_light' => $icon_light,
+ 'text' => sanitize_text_field($item['text'] ?? ''),
+ 'desc' => sanitize_text_field($item['desc'] ?? ''),
+ 'action_mode' => $action_mode,
+ 'qq' => $qq,
+ 'qrcode_img' => esc_url_raw($item['qrcode_img'] ?? ''),
+ 'wechat_img' => esc_url_raw($item['wechat_img'] ?? ''),
+ 'link' => esc_url_raw($item['link'] ?? ''),
+ 'add_items' => $add_items,
+ 'hover' => wp_kses_post($item['hover'] ?? ''),
+ 'qr_size' => $qr_size,
+ 'hover_width' => $hover_width,
+ 'pc_s' => !empty($item['pc_s']),
+ 'm_s' => !empty($item['m_s']),
+ 'color' => sanitize_text_field($item['color'] ?? ''),
+ 'bg' => sanitize_text_field($item['bg'] ?? ''),
+ 'enabled' => !empty($item['enabled']),
+ 'sort_order' => (int) ($item['sort_order'] ?? 0),
+ ];
+ }
+}
diff --git a/inc/classes/admin/global-ctrl-traits/trait-aksrt-admin-global-controller-part-2.php b/inc/classes/admin/global-ctrl-traits/trait-aksrt-admin-global-controller-part-2.php
new file mode 100644
index 00000000..38410d24
--- /dev/null
+++ b/inc/classes/admin/global-ctrl-traits/trait-aksrt-admin-global-controller-part-2.php
@@ -0,0 +1,258 @@
+get_method() === 'GET') {
+ $data = AKSRT_Settings::get_homepage_fixed_settings();
+ $data['homepage_modules'] = AKSRT_Settings::get_homepage_modules();
+ return rest_ensure_response([
+ 'code' => 'success',
+ 'data' => $data,
+ ]);
+ }
+
+ $params = $this->get_json_params($request);
+ if (empty($params)) {
+ $params = $request->get_params();
+ }
+
+ $allowed_sorts = AKSRT_Settings::homepage_allowed_sorts();
+
+ $settings = [
+ 'homepage_card_style' => in_array($params['homepage_card_style'] ?? '', ['default', 'style2', 'style3'], true)
+ ? $params['homepage_card_style']
+ : 'default',
+ 'homepage_multi_thumb_enabled' => !isset($params['homepage_multi_thumb_enabled']) || !empty($params['homepage_multi_thumb_enabled']),
+ 'homepage_multi_thumb_min_images' => max(2, min(4, (int) ($params['homepage_multi_thumb_min_images'] ?? 3))),
+ 'homepage_sidebar_mini_enabled' => !isset($params['homepage_sidebar_mini_enabled']) || !empty($params['homepage_sidebar_mini_enabled']),
+ 'homepage_default_sort' => in_array($params['homepage_default_sort'] ?? '', $allowed_sorts, true)
+ ? $params['homepage_default_sort']
+ : 'modified',
+ 'homepage_default_view' => in_array($params['homepage_default_view'] ?? '', ['list', 'card'], true)
+ ? $params['homepage_default_view']
+ : 'list',
+ 'homepage_sort_ajax_enabled' => !isset($params['homepage_sort_ajax_enabled']) || !empty($params['homepage_sort_ajax_enabled']),
+ 'homepage_sticky_tabs_enabled' => !isset($params['homepage_sticky_tabs_enabled']) || !empty($params['homepage_sticky_tabs_enabled']),
+ 'homepage_content_model' => in_array($params['homepage_content_model'] ?? '', ['post', 'mixed'], true)
+ ? $params['homepage_content_model']
+ : 'mixed',
+ 'home_posts_list_enabled' => !isset($params['home_posts_list_enabled']) || !empty($params['home_posts_list_enabled']),
+ 'home_list_title' => sanitize_text_field((string) ($params['home_list_title'] ?? __('Latest Posts', 'aksrt'))),
+ 'home_list_sort_enabled' => !isset($params['home_list_sort_enabled']) || !empty($params['home_list_sort_enabled']),
+ 'home_list_sort_dropdown' => !empty($params['home_list_sort_dropdown']),
+ 'homepage_tab_posts_per_page' => max(1, min(20, (int) ($params['homepage_tab_posts_per_page'] ?? 6))),
+ 'homepage_tab_limit' => max(1, min(12, (int) ($params['homepage_tab_limit'] ?? 4))),
+ ];
+
+ if (isset($params['home_list_sort_options']) && is_array($params['home_list_sort_options'])) {
+ $seen = [];
+ $options = [];
+ foreach ($params['home_list_sort_options'] as $key) {
+ $key = sanitize_key((string) $key);
+ if (in_array($key, $allowed_sorts, true) && !isset($seen[$key])) {
+ $options[] = $key;
+ $seen[$key] = true;
+ }
+ }
+ $settings['home_list_sort_options'] = !empty($options) ? $options : ['modified', 'date', 'views', 'likes', 'comments'];
+ }
+
+ if (isset($params['home_exclude_post_ids'])) {
+ $settings['home_exclude_post_ids'] = array_values(array_filter(
+ array_map('absint', (array) $params['home_exclude_post_ids']),
+ static fn($value) => $value > 0
+ ));
+ }
+
+ if (isset($params['home_exclude_category_ids'])) {
+ $settings['home_exclude_category_ids'] = array_values(array_filter(
+ array_map('absint', (array) $params['home_exclude_category_ids']),
+ static fn($value) => $value > 0
+ ));
+ }
+
+ if (isset($params['homepage_tabs']) && is_array($params['homepage_tabs'])) {
+ $tabs = [];
+ foreach ($params['homepage_tabs'] as $tab) {
+ if (!is_array($tab)) {
+ continue;
+ }
+ $term_id = absint($tab['term_id'] ?? 0);
+ if ($term_id <= 0) {
+ continue;
+ }
+ $term = get_term($term_id, 'category');
+ if (!$term || is_wp_error($term)) {
+ continue;
+ }
+ $orderby = sanitize_key($tab['orderby'] ?? 'date');
+ if (!in_array($orderby, $allowed_sorts, true)) {
+ $orderby = 'date';
+ }
+ $tabs[] = [
+ 'term_id' => $term_id,
+ 'title' => sanitize_text_field($tab['title'] ?? ''),
+ 'orderby' => $orderby,
+ 'enabled' => !empty($tab['enabled']),
+ ];
+ }
+ $settings['homepage_tabs'] = $tabs;
+ }
+
+ if (isset($params['homepage_modules']) && is_array($params['homepage_modules'])) {
+ $modules = [];
+ $allowed_types = [
+ 'hero', 'banner', 'category', 'tabs', 'ranking', 'recent',
+ 'tag_cloud', 'friend_links', 'html', 'hotspot',
+ 'category_recommend_cards', 'text_title', 'button_group',
+ 'spacing', 'faq', 'data_grid', 'highlight_cards',
+ 'partner_marquee', 'product_showcase',
+ ];
+ foreach ($params['homepage_modules'] as $module) {
+ if (!is_array($module) || !in_array($module['type'] ?? '', $allowed_types, true)) {
+ continue;
+ }
+ $modules[] = [
+ 'id' => sanitize_text_field($module['id'] ?? uniqid('mod_')),
+ 'type' => sanitize_text_field($module['type']),
+ 'config' => is_array($module['config'] ?? null) ? $module['config'] : [],
+ 'enabled' => !empty($module['enabled']),
+ 'title' => sanitize_text_field($module['title'] ?? ''),
+ ];
+ }
+ $settings['homepage_modules'] = $modules;
+ }
+
+ $current = get_option($option_key, []);
+ if (!is_array($current)) {
+ $current = [];
+ }
+
+ foreach ($settings as $key => $value) {
+ $current[$key] = $value;
+ }
+
+ update_option($option_key, $current);
+ AKSRT_Settings::flush_settings_cache();
+ update_option('aksrt_homepage_schema_version', gmdate('Y-m-d'));
+ delete_transient('aksrt_homepage_cached_images');
+ delete_transient('aksrt_homepage_modules_cache');
+ do_action('aksrt_homepage_settings_changed');
+
+ return rest_ensure_response([
+ 'code' => 'success',
+ 'message' => __('Homepage settings saved.', 'aksrt'),
+ 'data' => $settings,
+ ]);
+ }
+
+ public function rest_handle_category_recommend(WP_REST_Request $request): WP_REST_Response {
+ if ($request->get_method() === 'GET') {
+ $settings = AKSRT_Settings::get_category_recommend_settings();
+ return rest_ensure_response(['code' => 'success', 'data' => $settings]);
+ }
+
+ $params = $this->get_json_params($request);
+ $category_ids = array_map('absint', (array) ($params['category_recommend_category_ids'] ?? []));
+ $category_ids = array_filter($category_ids, fn($id) => $id > 0);
+ $category_ids = array_values(array_unique($category_ids));
+
+ $settings = [
+ 'enable_category_recommend' => !empty($params['enable_category_recommend']),
+ 'category_recommend_title' => sanitize_text_field($params['category_recommend_title'] ?? ''),
+ 'category_recommend_category_ids' => $category_ids,
+ 'category_recommend_posts_per_category' => max(1, min(10, (int) ($params['category_recommend_posts_per_category'] ?? 5))),
+ 'category_recommend_columns' => max(1, min(4, (int) ($params['category_recommend_columns'] ?? 2))),
+ 'category_recommend_show_description' => !empty($params['category_recommend_show_description']),
+ 'category_recommend_show_dates' => !empty($params['category_recommend_show_dates']),
+ 'category_recommend_show_more_link' => !empty($params['category_recommend_show_more_link']),
+ ];
+ update_option('aksrt_category_recommend', $settings);
+ AKSRT_Settings::flush_category_recommend_cache();
+
+ return rest_ensure_response(['code' => 'success', 'message' => __('Category recommendation settings saved.', 'aksrt')]);
+ }
+
+ public function rest_get_categories(WP_REST_Request $request): WP_REST_Response {
+ $categories = get_categories([
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ 'hide_empty' => false,
+ ]);
+
+ $data = array_map(function ($cat) {
+ return [
+ 'term_id' => (int) $cat->term_id,
+ 'name' => $cat->name,
+ 'slug' => $cat->slug,
+ 'count' => (int) $cat->count,
+ 'description' => $cat->description,
+ 'cover_id' => (int) get_term_meta($cat->term_id, 'aksrt_category_cover_id', true),
+ 'cover_url' => AKSRT_Settings::get_category_cover_url((int) $cat->term_id),
+ 'recommend_desc' => (string) get_term_meta($cat->term_id, 'aksrt_category_recommend_desc', true),
+ ];
+ }, $categories);
+
+ return rest_ensure_response(['code' => 'success', 'data' => array_values($data)]);
+ }
+
+ public function rest_search_users(WP_REST_Request $request): WP_REST_Response {
+ $search = (string) $request->get_param('search');
+ $number = max(1, min(50, (int) $request->get_param('number')));
+
+ $args = [
+ 'number' => $number,
+ 'orderby' => 'display_name',
+ 'order' => 'ASC',
+ 'fields' => ['ID', 'display_name', 'user_login'],
+ ];
+ if ($search !== '') {
+ $args['search'] = '*' . $search . '*';
+ $args['search_columns'] = ['display_name', 'user_login', 'user_email'];
+ }
+
+ $users = get_users($args);
+ $data = array_map(function ($user) {
+ return [
+ 'id' => (int) $user->ID,
+ 'display_name' => $user->display_name,
+ 'user_login' => $user->user_login,
+ ];
+ }, $users);
+
+ return rest_ensure_response(['code' => 'success', 'data' => array_values($data)]);
+ }
+
+ public function rest_update_category_meta(WP_REST_Request $request): WP_REST_Response {
+ $id = (int) $request->get_param('id');
+ $term = get_term($id, 'category');
+ if (!$term || is_wp_error($term)) {
+ return new WP_REST_Response(['code' => 'error', 'message' => __('Category does not exist.', 'aksrt')], 404);
+ }
+
+ $params = $this->get_json_params($request);
+ $cover_id = absint($params['cover_id'] ?? 0);
+ $recommend_desc = sanitize_textarea_field($params['recommend_desc'] ?? '');
+
+ if ($cover_id > 0) {
+ update_term_meta($id, 'aksrt_category_cover_id', $cover_id);
+ } else {
+ delete_term_meta($id, 'aksrt_category_cover_id');
+ }
+
+ if ($recommend_desc !== '') {
+ update_term_meta($id, 'aksrt_category_recommend_desc', $recommend_desc);
+ } else {
+ delete_term_meta($id, 'aksrt_category_recommend_desc');
+ }
+
+ AKSRT_Settings::flush_category_recommend_cache();
+
+ return rest_ensure_response(['code' => 'success', 'message' => __('分类元数据已保存', 'aksrt')]);
+ }
+}
diff --git a/inc/classes/admin/import-export-traits/trait-aksrt-admin-import-export-controller-part-1.php b/inc/classes/admin/import-export-traits/trait-aksrt-admin-import-export-controller-part-1.php
new file mode 100644
index 00000000..da3be079
--- /dev/null
+++ b/inc/classes/admin/import-export-traits/trait-aksrt-admin-import-export-controller-part-1.php
@@ -0,0 +1,406 @@
+get_log() : get_option('aksrt_settings_migration_log', []);
+
+ return $this->success([
+ 'schema_version' => $schema_version,
+ 'migration_version' => $migration ? $migration->get_version() : get_option('aksrt_settings_migration_version', '0.0.0'),
+ 'migration_log' => is_array($migration_log) ? $migration_log : [],
+ ]);
+ }
+
+ /**
+ * Get the schema definition for a domain (for frontend introspection).
+ */
+ public function rest_get_schema(WP_REST_Request $request): WP_REST_Response {
+ $domain = $request->get_param('domain');
+ $schema_provider = AKSRT_Admin_Schema_Provider::get_instance();
+
+ $schema = $schema_provider->get_schema($domain);
+ if (!$schema) {
+ return $this->error('未知的设置域: ' . esc_html($domain), 404);
+ }
+
+ return $this->success(['schema' => $schema]);
+ }
+
+ public function rest_get_schema_section(WP_REST_Request $request): WP_REST_Response {
+ return $this->get_section_settings($request, (string) $request->get_param('domain'));
+ }
+
+ public function rest_save_schema_section(WP_REST_Request $request): WP_REST_Response {
+ return $this->save_section_settings($request, (string) $request->get_param('domain'));
+ }
+
+ public function list_setting_backups(WP_REST_Request $request): WP_REST_Response {
+ $backups = get_option('aksrt_settings_backups', []);
+ if (!is_array($backups)) {
+ $backups = [];
+ }
+
+ usort($backups, static function ($a, $b) {
+ return strcmp((string) ($b['created_at'] ?? ''), (string) ($a['created_at'] ?? ''));
+ });
+
+ return $this->success([
+ 'data' => [
+ 'items' => array_values($backups),
+ 'total' => count($backups),
+ ],
+ ]);
+ }
+
+ public function create_setting_backup(WP_REST_Request $request): WP_REST_Response {
+ $params = $this->get_json_params($request);
+ $type = sanitize_key((string) ($params['type'] ?? 'manual'));
+ $label = sanitize_text_field((string) ($params['label'] ?? ''));
+
+ $allowed_types = ['manual', 'scheduled', 'reset_all', 'reset_domain', 'update_theme'];
+ if (!in_array($type, $allowed_types, true)) {
+ $type = 'manual';
+ }
+
+ $backup = $this->build_settings_backup($type, $label);
+ $this->persist_setting_backup($backup);
+
+ return $this->success([
+ 'message' => __('设置快照已创建', 'aksrt'),
+ 'data' => $backup,
+ ]);
+ }
+
+ public function restore_setting_backup(WP_REST_Request $request): WP_REST_Response {
+ $id = (int) $request->get_param('id');
+ $backups = get_option('aksrt_settings_backups', []);
+ if (!is_array($backups)) {
+ $backups = [];
+ }
+
+ $target = null;
+ foreach ($backups as $backup) {
+ if ((int) ($backup['id'] ?? 0) === $id) {
+ $target = $backup;
+ break;
+ }
+ }
+
+ if (!$target || !is_array($target['payload'] ?? null)) {
+ return $this->error(__('备份快照不存在', 'aksrt'), 404);
+ }
+
+ $payload = $target['payload'];
+ $domains = $payload['domains'] ?? [];
+ $standalone = $payload['standalone'] ?? [];
+ $provider = AKSRT_Admin_Schema_Provider::get_instance();
+
+ foreach ($domains as $domain => $domainPayload) {
+ $data = $domainPayload['data'] ?? $domainPayload;
+ if (is_array($data)) {
+ $provider->write_settings((string) $domain, $data);
+ }
+ }
+
+ foreach ($standalone as $key => $value) {
+ if (is_string($key) && preg_match('/^aksrt_/', $key)) {
+ update_option($key, $value);
+ }
+ }
+
+ AKSRT_Settings::flush_settings_cache();
+
+ return $this->success([
+ 'message' => __('设置已从快照恢复', 'aksrt'),
+ 'data' => [
+ 'id' => $id,
+ ],
+ ]);
+ }
+
+ public function delete_setting_backup(WP_REST_Request $request): WP_REST_Response {
+ $id = (int) $request->get_param('id');
+ $backups = get_option('aksrt_settings_backups', []);
+ if (!is_array($backups)) {
+ $backups = [];
+ }
+
+ $filtered = [];
+ $found = false;
+ foreach ($backups as $backup) {
+ if ((int) ($backup['id'] ?? 0) === $id) {
+ $found = true;
+ continue;
+ }
+ $filtered[] = $backup;
+ }
+
+ if (!$found) {
+ return $this->error(__('备份快照不存在', 'aksrt'), 404);
+ }
+
+ update_option('aksrt_settings_backups', array_values($filtered));
+
+ return $this->success([
+ 'message' => __('备份快照已删除', 'aksrt'),
+ ]);
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Widget Template Handlers */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * GET /widgets/export — Export current widget configuration.
+ */
+ public function export_widgets(WP_REST_Request $request): WP_REST_Response {
+ $sidebars_widgets = get_option('sidebars_widgets', []);
+ $widget_options = $this->collect_widget_options();
+
+ return $this->success([
+ 'data' => [
+ 'version' => AKSRT_VERSION ?? '3.4.0',
+ 'exported' => gmdate('Y-m-d H:i:s'),
+ 'sidebars_widgets' => $sidebars_widgets,
+ 'widget_options' => $widget_options,
+ ],
+ ]);
+ }
+
+ /**
+ * POST /widgets/import — Import widget configuration.
+ */
+ public function import_widgets(WP_REST_Request $request): WP_REST_Response {
+ $params = $request->get_json_params() ?? [];
+ $sidebars = $params['sidebars_widgets'] ?? null;
+ $options = $params['widget_options'] ?? null;
+
+ if (!$sidebars || !is_array($sidebars)) {
+ return $this->error('缺少 sidebars_widgets 数据');
+ }
+
+ // Validate sidebar IDs exist
+ $valid_sidebars = array_keys(get_registered_sidebars());
+ $valid_sidebars[] = 'wp_inactive_widgets';
+
+ $imported = 0;
+ foreach ($sidebars as $sidebar_id => $widget_ids) {
+ if (!in_array($sidebar_id, $valid_sidebars, true) && $sidebar_id !== 'array_version') {
+ continue;
+ }
+ $imported++;
+ }
+
+ // Import widget options if provided
+ if ($options && is_array($options)) {
+ foreach ($options as $option_key => $option_value) {
+ if (preg_match('/^widget_aksrt_/', $option_key)) {
+ update_option($option_key, $option_value);
+ }
+ }
+ }
+
+ // Import sidebar assignments
+ update_option('sidebars_widgets', $sidebars);
+
+ return $this->success(['message' => "已导入 {$imported} 个侧栏配置。"]);
+ }
+
+ /**
+ * GET /widgets/templates — List saved widget templates.
+ */
+ public function list_widget_templates(WP_REST_Request $request): WP_REST_Response {
+ $templates = get_option('aksrt_widget_templates', []);
+ return $this->success(['data' => ['templates' => $templates]]);
+ }
+
+ /**
+ * POST /widgets/templates — Save current layout as a named template.
+ */
+ public function save_widget_template(WP_REST_Request $request): WP_REST_Response {
+ $params = $request->get_json_params() ?? [];
+ $name = sanitize_text_field((string) ($params['name'] ?? ''));
+ if (!$name) {
+ return $this->error('请输入模板名称');
+ }
+
+ $templates = get_option('aksrt_widget_templates', []);
+ $template = [
+ 'id' => count($templates) + 1,
+ 'name' => $name,
+ 'created_at' => gmdate('Y-m-d H:i:s'),
+ 'sidebars_widgets' => get_option('sidebars_widgets', []),
+ 'widget_options' => $this->collect_widget_options(),
+ ];
+
+ $templates[] = $template;
+ update_option('aksrt_widget_templates', $templates);
+
+ return $this->success(['message' => '模板已保存', 'data' => $template]);
+ }
+
+ /**
+ * POST /widgets/templates/{id}/apply — Apply a saved template.
+ */
+ public function apply_widget_template(WP_REST_Request $request): WP_REST_Response {
+ $id = (int) $request->get_param('id');
+ $templates = get_option('aksrt_widget_templates', []);
+
+ $template = null;
+ foreach ($templates as $t) {
+ if ((int) ($t['id'] ?? 0) === $id) {
+ $template = $t;
+ break;
+ }
+ }
+
+ if (!$template) {
+ return $this->error('模板不存在', 404);
+ }
+
+ // Apply widget options
+ if (!empty($template['widget_options']) && is_array($template['widget_options'])) {
+ foreach ($template['widget_options'] as $key => $value) {
+ update_option($key, $value);
+ }
+ }
+
+ // Apply sidebar assignments
+ if (!empty($template['sidebars_widgets'])) {
+ update_option('sidebars_widgets', $template['sidebars_widgets']);
+ }
+
+ return $this->success(['message' => '模板已应用']);
+ }
+
+ /**
+ * DELETE /widgets/templates/{id} — Delete a saved template.
+ */
+ public function delete_widget_template(WP_REST_Request $request): WP_REST_Response {
+ $id = (int) $request->get_param('id');
+ $templates = get_option('aksrt_widget_templates', []);
+
+ $found = false;
+ $filtered = [];
+ foreach ($templates as $t) {
+ if ((int) ($t['id'] ?? 0) === $id) {
+ $found = true;
+ continue;
+ }
+ $filtered[] = $t;
+ }
+
+ if (!$found) {
+ return $this->error('模板不存在', 404);
+ }
+
+ update_option('aksrt_widget_templates', $filtered);
+ return $this->success(['message' => '模板已删除']);
+ }
+
+ /**
+ * Collect all widget options from the database.
+ */
+ private function collect_widget_options(): array {
+ global $wpdb;
+ $rows = $wpdb->get_results(
+ "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE 'widget_aksrt_%' ORDER BY option_name",
+ ARRAY_A
+ );
+
+ $options = [];
+ if ($rows) {
+ foreach ($rows as $row) {
+ $value = maybe_unserialize($row['option_value']);
+ $options[$row['option_name']] = $value;
+ }
+ }
+ return $options;
+ }
+
+ private function persist_setting_backup(array $backup): void {
+ $backups = get_option('aksrt_settings_backups', []);
+ if (!is_array($backups)) {
+ $backups = [];
+ }
+
+ array_unshift($backups, $backup);
+ update_option('aksrt_settings_backups', $this->trim_setting_backups($backups));
+ }
+
+ private function create_backup_record(string $type, string $label = ''): array {
+ return $this->build_settings_backup($type, $label);
+ }
+
+ public function register_backup_schedules(array $schedules): array {
+ if (!isset($schedules[self::MONTHLY_SCHEDULE])) {
+ $schedules[self::MONTHLY_SCHEDULE] = [
+ 'interval' => MONTH_IN_SECONDS,
+ 'display' => __('每月', 'aksrt'),
+ ];
+ }
+
+ return $schedules;
+ }
+
+ public function sync_backup_schedule(): void {
+ $settings = $this->get_backup_schedule_settings();
+ $next = wp_next_scheduled(self::SCHEDULED_BACKUP_HOOK);
+
+ if (!$settings['enabled']) {
+ if ($next) {
+ wp_unschedule_event($next, self::SCHEDULED_BACKUP_HOOK);
+ }
+ return;
+ }
+
+ $schedule = $settings['schedule'];
+ if ($next && wp_get_schedule(self::SCHEDULED_BACKUP_HOOK) !== $schedule) {
+ wp_unschedule_event($next, self::SCHEDULED_BACKUP_HOOK);
+ $next = false;
+ }
+
+ if (!$next) {
+ wp_schedule_event(time() + HOUR_IN_SECONDS, $schedule, self::SCHEDULED_BACKUP_HOOK);
+ }
+ }
+
+ public function clear_backup_schedule(): void {
+ $next = wp_next_scheduled(self::SCHEDULED_BACKUP_HOOK);
+ while ($next) {
+ wp_unschedule_event($next, self::SCHEDULED_BACKUP_HOOK);
+ $next = wp_next_scheduled(self::SCHEDULED_BACKUP_HOOK);
+ }
+ }
+
+ public function run_scheduled_backup(): void {
+ $settings = $this->get_backup_schedule_settings();
+ if (!$settings['enabled']) {
+ return;
+ }
+
+ $backup = $this->create_backup_record('scheduled');
+ $this->persist_setting_backup($backup);
+ }
+
+ public function handle_theme_upgrade_snapshot($response, array $hook_extra) {
+ if (($hook_extra['type'] ?? '') !== 'theme') {
+ return $response;
+ }
+
+ $themes = $hook_extra['themes'] ?? [];
+ $stylesheet = wp_get_theme()->get_stylesheet();
+ if (!in_array($stylesheet, (array) $themes, true)) {
+ return $response;
+ }
+
+ $backup = $this->create_backup_record('update_theme');
+ $this->persist_setting_backup($backup);
+
+ return $response;
+ }
+}
diff --git a/inc/classes/admin/import-export-traits/trait-aksrt-admin-import-export-controller-part-2.php b/inc/classes/admin/import-export-traits/trait-aksrt-admin-import-export-controller-part-2.php
new file mode 100644
index 00000000..be750d9e
--- /dev/null
+++ b/inc/classes/admin/import-export-traits/trait-aksrt-admin-import-export-controller-part-2.php
@@ -0,0 +1,78 @@
+read_settings('tools', 'backup');
+ $frequency = sanitize_key((string) ($settings['backup_frequency'] ?? 'weekly'));
+ $allowed = ['daily', 'weekly', 'monthly'];
+ if (!in_array($frequency, $allowed, true)) {
+ $frequency = 'weekly';
+ }
+
+ return [
+ 'enabled' => !empty($settings['backup_auto']),
+ 'schedule' => $frequency === 'monthly' ? self::MONTHLY_SCHEDULE : $frequency,
+ ];
+ }
+
+ private function build_settings_backup(string $type, string $label = ''): array {
+ $schema_provider = AKSRT_Admin_Schema_Provider::get_instance();
+ $domains = $schema_provider->get_domain_keys();
+
+ $payload = [
+ 'version' => AKSRT_VERSION ?? '3.4.0',
+ 'exported' => gmdate('Y-m-d H:i:s'),
+ 'domains' => [],
+ 'standalone'=> [],
+ ];
+
+ foreach ($domains as $domain) {
+ $payload['domains'][$domain] = [
+ 'option_key' => $schema_provider->get_option_key($domain),
+ 'data' => $schema_provider->read_settings($domain),
+ ];
+ }
+
+ foreach (AKSRT_Settings::STANDALONE_OPTION_KEYS as $key) {
+ $value = get_option($key);
+ if ($value !== false) {
+ $payload['standalone'][$key] = $value;
+ }
+ }
+
+ return [
+ 'id' => time(),
+ 'type' => $type,
+ 'label' => $label !== '' ? $label : $this->default_backup_label($type),
+ 'created_at' => gmdate('Y-m-d H:i:s'),
+ 'payload' => $payload,
+ ];
+ }
+
+ private function trim_setting_backups(array $backups): array {
+ $retain = (int) (AKSRT_Settings::get_value('backup_retain', 7));
+ $retain = max(3, min(20, $retain));
+
+ if (count($backups) <= $retain) {
+ return array_values($backups);
+ }
+
+ return array_values(array_slice($backups, 0, $retain));
+ }
+
+ private function default_backup_label(string $type): string {
+ $map = [
+ 'manual' => __('手动备份', 'aksrt'),
+ 'scheduled' => __('定期备份', 'aksrt'),
+ 'reset_all' => __('重置全部前快照', 'aksrt'),
+ 'reset_domain' => __('重置分区前快照', 'aksrt'),
+ 'update_theme' => __('更新主题前快照', 'aksrt'),
+ ];
+
+ return $map[$type] ?? __('设置快照', 'aksrt');
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-audit.php b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-audit.php
new file mode 100644
index 00000000..758cdde0
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-audit.php
@@ -0,0 +1,43 @@
+ 'audit',
+ 'title' => 'API内容审核',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'provider', 'title' => '审核接口', 'fields' => [
+ ['key' => 'content_audit_enabled', 'label' => '启用内容审核', 'type' => 'switch', 'default' => false],
+ ['key' => 'content_audit_provider', 'label' => '审核服务商', 'type' => 'radio', 'default' => 'baidu', 'options' => [
+ ['label' => '百度内容审核', 'value' => 'baidu'],
+ ['label' => '关闭审核接口', 'value' => 'null'],
+ ]],
+ ['key' => 'content_audit_api_key', 'label' => 'API Key', 'type' => 'input', 'default' => ''],
+ ['key' => 'content_audit_api_secret', 'label' => 'Secret Key', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'scope', 'title' => '审核范围', 'fields' => [
+ ['key' => 'content_audit_text_enabled', 'label' => '文本审核', 'type' => 'switch', 'default' => true],
+ ['key' => 'content_audit_image_enabled', 'label' => '图片审核', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_bbs_term', 'label' => '审核论坛分类', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'fallback', 'title' => '失败策略', 'fields' => [
+ ['key' => 'content_audit_on_failure', 'label' => '审核失败时', 'type' => 'radio', 'default' => 'allow', 'options' => [
+ ['label' => '放行', 'value' => 'allow'],
+ ['label' => '拦截', 'value' => 'block'],
+ ]],
+ ['key' => 'content_audit_log_enabled', 'label' => '记录审核日志', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-docs-mode.php b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-docs-mode.php
new file mode 100644
index 00000000..964b3a2f
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-docs-mode.php
@@ -0,0 +1,53 @@
+ 'docs-mode',
+ 'title' => '文档模式',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '基础配置', 'fields' => [
+ ['key' => 'docs_mode_enabled', 'label' => '启用文档模式', 'type' => 'switch', 'default' => false],
+ ['key' => 'docs_mode_default_template', 'label' => '文档首页模板', 'type' => 'select', 'default' => 'documentnav', 'options' => [
+ ['label' => '文档导航页', 'value' => 'documentnav'],
+ ['label' => '专题聚合页', 'value' => 'special'],
+ ]],
+ ]],
+ ['key' => 'navigation', 'title' => '导航设置', 'fields' => [
+ ['key' => 'docs_mode_toc_enabled', 'label' => '显示目录树', 'type' => 'switch', 'default' => true],
+ ['key' => 'docs_mode_chapter_nav_enabled', 'label' => '显示章节导航', 'type' => 'switch', 'default' => true],
+ ['key' => 'docs_mode_default_order', 'label' => '默认排序', 'type' => 'radio', 'default' => 'menu_order', 'options' => [
+ ['label' => '按菜单顺序', 'value' => 'menu_order'],
+ ['label' => '按发布时间', 'value' => 'date'],
+ ['label' => '按标题排序', 'value' => 'title'],
+ ]],
+ ['key' => 'docs_mode_toc_depth', 'label' => '目录深度', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 6],
+ ['key' => 'docs_mode_chapter_title', 'label' => '章节标题', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'filter', 'title' => '筛选设置', 'fields' => [
+ ['key' => 'docs_mode_cats', 'label' => '文档分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => []],
+ ['key' => 'docs_mode_exclude', 'label' => '在首页排除此类内容', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'style', 'title' => '样式设置', 'fields' => [
+ ['key' => 'docs_mode_style', 'label' => '文档样式', 'type' => 'select', 'default' => 'default', 'options' => [
+ ['label' => '默认', 'value' => 'default'],
+ ['label' => '简洁', 'value' => 'simple'],
+ ['label' => '卡片', 'value' => 'card'],
+ ]],
+ ['key' => 'docs_mode_icon', 'label' => '显示图标', 'type' => 'switch', 'default' => true],
+ ['key' => 'docs_mode_count', 'label' => '显示文章数', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-ip.php b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-ip.php
new file mode 100644
index 00000000..a1c9b3d7
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-ip.php
@@ -0,0 +1,51 @@
+ 'ip',
+ 'title' => 'IP归属地',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '基础配置', 'fields' => [
+ ['key' => 'ip_geo_enabled', 'label' => '启用 IP 归属地', 'type' => 'switch', 'default' => false],
+ ['key' => 'ip_addr_sdk', 'label' => 'IP 接口服务商', 'type' => 'radio', 'default' => 'null', 'options' => [
+ ['label' => '轮流查询', 'value' => 'polling'],
+ ['label' => '腾讯位置服务', 'value' => 'qq'],
+ ['label' => '高德位置服务', 'value' => 'amap'],
+ ['label' => '太平洋公共接口', 'value' => 'null'],
+ ]],
+ ['key' => 'ip_geo_cache_ttl', 'label' => '缓存时间 (秒)', 'type' => 'number', 'default' => 86400, 'min' => 3600, 'max' => 604800],
+ ]],
+ ['key' => 'providers', 'title' => '接口凭据', 'fields' => [
+ ['key' => 'ip_addr_sdk_qq_appkey', 'label' => '腾讯 AppKey', 'type' => 'input', 'default' => ''],
+ ['key' => 'ip_addr_sdk_qq_secretkey', 'label' => '腾讯 SecretKey', 'type' => 'input', 'default' => ''],
+ ['key' => 'ip_addr_sdk_amap_appkey', 'label' => '高德 AppKey', 'type' => 'input', 'default' => ''],
+ ['key' => 'ip_addr_sdk_amap_secretkey', 'label' => '高德 SecretKey', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'ip_geo_display', 'label' => '展示位置', 'type' => 'checkbox', 'default' => ['comment'], 'sanitize_type' => 'array', 'options' => [
+ ['label' => '评论区', 'value' => 'comment'],
+ ['label' => '用户主页', 'value' => 'user_home'],
+ ['label' => '文章页', 'value' => 'post'],
+ ]],
+ ['key' => 'ip_geo_format', 'label' => '显示格式', 'type' => 'radio', 'default' => 'province_city', 'options' => [
+ ['label' => '省份+城市', 'value' => 'province_city'],
+ ['label' => '仅省份', 'value' => 'province'],
+ ['label' => '国家+省份', 'value' => 'country_province'],
+ ]],
+ ['key' => 'ip_geo_fallback_text', 'label' => '查询失败时文案', 'type' => 'input', 'default' => ''],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-push.php b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-push.php
new file mode 100644
index 00000000..e9216f25
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-push.php
@@ -0,0 +1,43 @@
+ 'push',
+ 'title' => '百度推送',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '推送配置', 'fields' => [
+ ['key' => 'baidu_push_enabled', 'label' => '启用百度推送', 'type' => 'switch', 'default' => false],
+ ['key' => 'baidu_push_token', 'label' => '推送 Token', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'trigger', 'title' => '触发规则', 'fields' => [
+ ['key' => 'baidu_push_auto', 'label' => '自动推送', 'type' => 'switch', 'default' => true],
+ ['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
+ ['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [
+ ['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false],
+ ['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'xzh_render_tail', 'label' => '文章底部显示熊掌号尾部', 'type' => 'switch', 'default' => true],
+ ['key' => 'xzh_jsonld_single', 'label' => '文章页 JSON-LD', 'type' => 'switch', 'default' => true],
+ ['key' => 'xzh_jsonld_page', 'label' => '页面 JSON-LD', 'type' => 'switch', 'default' => false],
+ ['key' => 'xzh_jsonld_img', 'label' => '不添加图片', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'advanced', 'title' => '高级设置', 'fields' => [
+ ['key' => 'baidu_push_batch_size', 'label' => '批量推送条数', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
+ ['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php
new file mode 100644
index 00000000..564481d8
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php
@@ -0,0 +1,111 @@
+ 'security',
+ 'title' => '网站安全',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '基础安全', 'fields' => [
+ ['key' => 'hide_admin_bar', 'label' => '前台关闭顶部 admin_bar', 'type' => 'switch', 'default' => true],
+ ['key' => 'remove_emoji', 'label' => '移除Emoji脚本', 'type' => 'switch', 'default' => true],
+ ['key' => 'remove_open_sans', 'label' => '移除Open Sans字体', 'type' => 'switch', 'default' => true],
+ ['key' => 'disabled_pingback', 'label' => '禁用Pingback', 'type' => 'switch', 'default' => true],
+ ['key' => 'disabled_xmlrpc', 'label' => '禁用XML-RPC', 'type' => 'switch', 'default' => true],
+ ['key' => 'close_attachment_page', 'label' => '关闭附件页面', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_disable_admin', 'label' => '非管理员禁止进入后台', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'upload', 'title' => '上传设置', 'fields' => [
+ ['key' => 'split_upload_s', 'label' => '分片上传', 'type' => 'switch', 'default' => true],
+ ['key' => 'split_minimum_size', 'label' => '最小分片大小(MB)', 'type' => 'number', 'default' => 20, 'min' => 4, 'max' => 100, 'step' => 2],
+ ['key' => 'newfilename', 'label' => '上传文件自动重命名', 'type' => 'switch', 'default' => false],
+ ['key' => 'newfilename_type', 'label' => '重命名格式', 'type' => 'radio', 'default' => 'time', 'options' => [
+ ['label' => '随机字符串', 'value' => 'random'],
+ ['label' => '时间+原文件名', 'value' => 'time'],
+ ]],
+ ['key' => 'disabled_large_thumbnail', 'label' => '禁用大缩略图', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'admin', 'title' => '后台设置', 'fields' => [
+ ['key' => 'remove_dashboard_widgets', 'label' => 'WP 后台加速:移除站点健康/活动/禁止浏览器&PHP版本检测', 'type' => 'switch', 'default' => true],
+ ['key' => 'display_wp_update', 'label' => '禁止 WP 检测更新', 'type' => 'switch', 'default' => true],
+ ['key' => 'close_gutenberg', 'label' => '关闭古腾堡编辑器', 'type' => 'switch', 'default' => false, 'help' => '禁用后编辑器增强全部失效'],
+ ['key' => 'disabled_autoembed', 'label' => '禁用自动嵌入', 'type' => 'switch', 'default' => true],
+ ['key' => 'mce_float_toolbar', 'label' => '浮动工具栏', 'type' => 'checkbox', 'default' => ['pc_s', 'm_s'], 'sanitize_type' => 'array', 'options' => [
+ ['label' => 'PC', 'value' => 'pc_s'],
+ ['label' => '移动端', 'value' => 'm_s'],
+ ]],
+ ['key' => 'admin_user_del_fields', 'label' => '关闭后台用户编辑多余选项', 'type' => 'switch', 'default' => true],
+ ['key' => 'admin_frontend_set', 'label' => '前台设置入口', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'format', 'title' => '格式设置', 'fields' => [
+ ['key' => 'time_ago_s', 'label' => '时间格式', 'type' => 'switch', 'default' => true],
+ ['key' => 'time_format', 'label' => '时间显示格式', 'type' => 'input', 'default' => 'n月j日 H:i'],
+ ['key' => 'remove_more_wp_head', 'label' => '移除WP head多余代码', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'verification', 'title' => '验证设置', 'fields' => [
+ ['key' => 'verification_comment_s', 'label' => '评论需要验证', 'type' => 'switch', 'default' => false],
+ ['key' => 'verification_links_s', 'label' => '链接需要验证', 'type' => 'switch', 'default' => true],
+ ['key' => 'verification_newposts_s', 'label' => '新文章需要验证', 'type' => 'switch', 'default' => false],
+ ['key' => 'verification_bbspost_s', 'label' => '论坛帖子需要验证', 'type' => 'switch', 'default' => false],
+ ['key' => 'verification_signin_exclude', 'label' => '登录人机验证排除账号', 'type' => 'input', 'default' => ''],
+ ['key' => 'user_verification_type', 'label' => '验证类型', 'type' => 'radio', 'default' => 'slider', 'options' => [
+ ['label' => '图形验证码', 'value' => 'image'],
+ ['label' => '滑块验证', 'value' => 'slider'],
+ ['label' => '腾讯验证码', 'value' => 'tcaptcha'],
+ ['label' => '极验验证码', 'value' => 'geetest'],
+ ['label' => '关闭验证', 'value' => 'null'],
+ ]],
+ ]],
+ ['key' => 'captcha', 'title' => '验证码设置', 'fields' => [
+ ['key' => 'geetest_enabled', 'label' => '极验验证码', 'type' => 'switch', 'default' => false],
+ ['key' => 'geetest_id', 'label' => '极验 ID', 'type' => 'input', 'default' => ''],
+ ['key' => 'geetest_key', 'label' => '极验 Key', 'type' => 'input', 'default' => ''],
+ ['key' => 'tcaptcha_enabled', 'label' => '腾讯云验证码', 'type' => 'switch', 'default' => false],
+ ['key' => 'tcaptcha_appid', 'label' => '腾讯云 AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'tcaptcha_secret_id', 'label' => '腾讯云 SecretId', 'type' => 'input', 'default' => ''],
+ ['key' => 'tcaptcha_secret_key', 'label' => '腾讯云 SecretKey', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'rate-limit', 'title' => '频率限制', 'fields' => [
+ ['key' => 'brush_limit_post_enabled', 'label' => '文章发布限制', 'type' => 'switch', 'default' => false],
+ ['key' => 'brush_limit_post_minutes_10', 'label' => '文章 10 分钟限制', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
+ ['key' => 'brush_limit_post_day_1', 'label' => '文章每日限制', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 2000],
+ ['key' => 'brush_limit_comment_enabled', 'label' => '评论频率限制', 'type' => 'switch', 'default' => false],
+ ['key' => 'brush_limit_comment_minutes_10', 'label' => '评论 10 分钟限制', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
+ ['key' => 'brush_limit_comment_day_1', 'label' => '评论每日限制', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 2000],
+ ['key' => 'brush_limit_upload_enabled', 'label' => '上传频率限制', 'type' => 'switch', 'default' => false],
+ ['key' => 'brush_limit_upload_minutes_10', 'label' => '上传 10 分钟限制', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 200],
+ ['key' => 'brush_limit_upload_day_1', 'label' => '上传每日限制', 'type' => 'number', 'default' => 200, 'min' => 1, 'max' => 2000],
+ ['key' => 'brush_limit_register_enabled', 'label' => '注册频率限制', 'type' => 'switch', 'default' => false],
+ ['key' => 'brush_limit_register_minutes_10', 'label' => '注册 10 分钟限制', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
+ ['key' => 'brush_limit_register_day_1', 'label' => '注册每日限制', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 2000],
+ ['key' => 'brush_limit_order_enabled', 'label' => '订单频率限制', 'type' => 'switch', 'default' => false],
+ ['key' => 'brush_limit_order_minutes_10', 'label' => '订单 10 分钟限制', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 200],
+ ['key' => 'brush_limit_order_day_1', 'label' => '订单每日限制', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 2000],
+ ['key' => 'error_frequency_limit', 'label' => '错误频率限制', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'content-audit', 'title' => '内容审核', 'fields' => [
+ ['key' => 'audit_comment', 'label' => '审核评论', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_new_post', 'label' => '审核新文章', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_upload_img', 'label' => '审核上传图片', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_user_desc', 'label' => '审核用户简介', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_msg_private', 'label' => '审核私信', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_be_like_text', 'label' => '文本允许疑似合规', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_be_like_img', 'label' => '图片允许疑似合规', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_bbs_posts', 'label' => '审核论坛帖子', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_bbs_plate', 'label' => '审核论坛板块', 'type' => 'switch', 'default' => false],
+ ['key' => 'audit_bbs_term', 'label' => '审核论坛分类', 'type' => 'switch', 'default' => false],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions.php b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions.php
new file mode 100644
index 00000000..6dc289bb
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions.php
@@ -0,0 +1,25 @@
+ 'extensions',
+ 'title' => '扩展&增强',
+ 'sections' => [
+ 'ip' => AKSRT_Admin_Schema_Extensions_IP::build(),
+ 'audit' => AKSRT_Admin_Schema_Extensions_Audit::build(),
+ 'push' => AKSRT_Admin_Schema_Extensions_Push::build(),
+ 'security' => AKSRT_Admin_Schema_Extensions_Security::build(),
+ 'docs-mode' => AKSRT_Admin_Schema_Extensions_Docs_Mode::build(),
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-basic.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-basic.php
new file mode 100644
index 00000000..93ac628c
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-basic.php
@@ -0,0 +1,28 @@
+ 'basic',
+ 'title' => '基础设置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'switch', 'title' => '功能开关', 'fields' => [
+ ['key' => 'bbs_s', 'label' => '启用论坛', 'type' => 'switch', 'default' => true, 'help' => '关闭后论坛页面将返回 404'],
+ ]],
+ ['key' => 'admin', 'title' => '论坛管理', 'fields' => [
+ ['key' => 'bbs_admin_users', 'label' => '论坛管理员', 'type' => 'user_select', 'multiple' => true, 'default' => [], 'help' => '拥有跨版块全部治理权限,绕过 ban'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-board-page.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-board-page.php
new file mode 100644
index 00000000..47345bf4
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-board-page.php
@@ -0,0 +1,34 @@
+ 'board_page',
+ 'title' => '版块页面',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'layout', 'title' => '版块页布局', 'fields' => [
+ ['key' => 'bbs_plate_top_info_s', 'label' => '版块信息卡', 'type' => 'switch', 'default' => true, 'help' => '版块页顶部显示版块详情卡片'],
+ ['key' => 'bbs_plate_tab', 'label' => '版块栏目', 'type' => 'group', 'default' => [
+ ['label' => '全部', 'sort' => 'date'],
+ ['label' => '推荐', 'sort' => 'featured'],
+ ['label' => '最新回复', 'sort' => 'last_reply'],
+ ['label' => '热门', 'sort' => 'hot'],
+ ['label' => '精华', 'sort' => 'essence'],
+ ]],
+ ['key' => 'bbs_plate_tab_active_index', 'label' => '默认选中 Tab', 'type' => 'number', 'default' => 0, 'min' => 0],
+ ['key' => 'bbs_plate_tab_swiper', 'label' => '移动端滑动切换', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-credits.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-credits.php
new file mode 100644
index 00000000..6458b47c
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-credits.php
@@ -0,0 +1,33 @@
+ 'credits',
+ 'title' => '积分设置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'earn', 'title' => '积分奖励', 'fields' => [
+ ['key' => 'bbs_credits_post_new', 'label' => '发帖奖励', 'type' => 'number', 'default' => 5, 'min' => 0],
+ ['key' => 'bbs_credits_plate_new', 'label' => '建版奖励', 'type' => 'number', 'default' => 10, 'min' => 0],
+ ['key' => 'bbs_credits_comment_new', 'label' => '回复奖励', 'type' => 'number', 'default' => 2, 'min' => 0],
+ ['key' => 'bbs_credits_comment_like', 'label' => '评论被赞', 'type' => 'number', 'default' => 1, 'min' => 0],
+ ['key' => 'bbs_credits_score_extra', 'label' => '被加分', 'type' => 'number', 'default' => 3, 'min' => 0],
+ ['key' => 'bbs_credits_essence', 'label' => '被设精华', 'type' => 'number', 'default' => 10, 'min' => 0],
+ ['key' => 'bbs_credits_posts_hot', 'label' => '成为热帖', 'type' => 'number', 'default' => 5, 'min' => 0],
+ ['key' => 'bbs_credits_adopted', 'label' => '被采纳', 'type' => 'number', 'default' => 20, 'min' => 0],
+ ['key' => 'bbs_credits_daily_max', 'label' => '每日积分上限', 'type' => 'number', 'default' => 50, 'min' => 0, 'help' => '0 = 不限'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-homepage.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-homepage.php
new file mode 100644
index 00000000..87a73b84
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-homepage.php
@@ -0,0 +1,32 @@
+ 'homepage',
+ 'title' => '首页设置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'tabs', 'title' => '首页 Tab', 'fields' => [
+ ['key' => 'bbs_home_tab', 'label' => '首页栏目', 'type' => 'group', 'default' => [
+ ['type' => 'follow', 'label' => '关注'],
+ ['type' => 'synthesis', 'label' => '综合'],
+ ['type' => 'plate', 'label' => '版块'],
+ ], 'help' => '可拖拽排序,支持 follow / synthesis / plate / tabs 类型'],
+ ['key' => 'bbs_home_tab_active_index', 'label' => '默认选中 Tab', 'type' => 'number', 'default' => 1, 'min' => 0],
+ ['key' => 'bbs_home_tab_quick_posts_s', 'label' => '快速发帖', 'type' => 'switch', 'default' => true, 'help' => '首页顶部显示快速发帖入口'],
+ ['key' => 'bbs_home_tab_swiper', 'label' => '移动端滑动切换', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-names.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-names.php
new file mode 100644
index 00000000..d3eb97eb
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-names.php
@@ -0,0 +1,36 @@
+ 'names',
+ 'title' => '名称定义',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'display_names', 'title' => '名词自定义', 'fields' => [
+ ['key' => 'bbs_forum_display_name', 'label' => '论坛', 'type' => 'text', 'default' => '论坛'],
+ ['key' => 'bbs_plate_display_name', 'label' => '版块', 'type' => 'text', 'default' => '版块'],
+ ['key' => 'bbs_posts_display_name', 'label' => '帖子', 'type' => 'text', 'default' => '帖子'],
+ ['key' => 'bbs_topic_display_name', 'label' => '话题', 'type' => 'text', 'default' => '话题'],
+ ['key' => 'bbs_tag_display_name', 'label' => '标签', 'type' => 'text', 'default' => '标签'],
+ ['key' => 'bbs_comment_display_name', 'label' => '回复', 'type' => 'text', 'default' => '回复'],
+ ['key' => 'bbs_question_badge_name', 'label' => '提问徽章', 'type' => 'text', 'default' => '提问'],
+ ['key' => 'bbs_question_ok_badge_name', 'label' => '已解决徽章', 'type' => 'text', 'default' => '已解决'],
+ ['key' => 'bbs_cat_moderator_name', 'label' => '分区版主', 'type' => 'text', 'default' => '分区版主'],
+ ['key' => 'bbs_plate_author_name', 'label' => '超级版主', 'type' => 'text', 'default' => '超级版主'],
+ ['key' => 'bbs_plate_moderator_name', 'label' => '版主', 'type' => 'text', 'default' => '版主'],
+ ['key' => 'bbs_plate_follow_name', 'label' => '关注', 'type' => 'text', 'default' => '关注'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-other.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-other.php
new file mode 100644
index 00000000..7ecfdfc0
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-other.php
@@ -0,0 +1,46 @@
+ 'other',
+ 'title' => '其它设置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'vote', 'title' => '投票', 'fields' => [
+ ['key' => 'bbs_vote_number_type', 'label' => '投票显示', 'type' => 'select', 'default' => 'percent', 'options' => [
+ ['label' => '百分比', 'value' => 'percent'],
+ ['label' => '票数', 'value' => 'number'],
+ ]],
+ ['key' => 'bbs_vote_max', 'label' => '选项上限', 'type' => 'number', 'default' => 8, 'min' => 2, 'max' => 20],
+ ]],
+ ['key' => 'moderator_apply', 'title' => '版主申请', 'fields' => [
+ ['key' => 'bbs_apply_moderator_desc', 'label' => '申请说明', 'type' => 'textarea', 'default' => '欢迎有热情的用户申请成为版主,协助管理社区秩序。'],
+ ]],
+ ['key' => 'misc', 'title' => '杂项', 'fields' => [
+ ['key' => 'bbs_term_thumb', 'label' => '标签默认缩略图', 'type' => 'media', 'default' => ''],
+ ['key' => 'bbs_post_lists_hide_pay_s', 'label' => '列表隐藏付费标记', 'type' => 'switch', 'default' => false],
+ ['key' => 'bbs_post_title_strlen_limit', 'label' => '标题字数限制', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 100, 'help' => '最小 5,最大 100'],
+ ['key' => 'bbs_t_placeholder', 'label' => '标题占位符', 'type' => 'text', 'default' => '请输入标题'],
+ ['key' => 'bbs_c_placeholder', 'label' => '内容占位符', 'type' => 'text', 'default' => '请输入内容'],
+ ]],
+ ['key' => 'url', 'title' => 'URL / 别名', 'fields' => [
+ ['key' => 'bbs_rewrite_suffix_html_s', 'label' => '.html 后缀', 'type' => 'switch', 'default' => false],
+ ['key' => 'bbs_plate_rewrite_slug', 'label' => '版块别名', 'type' => 'text', 'default' => 'board'],
+ ['key' => 'bbs_posts_rewrite_slug', 'label' => '帖子别名', 'type' => 'text', 'default' => 'topic'],
+ ['key' => 'bbs_posts_edit_rewrite_slug', 'label' => '编辑别名', 'type' => 'text', 'default' => 'topic-edit'],
+ ['key' => 'bbs_posts_show_in_rest_s', 'label' => '古腾堡编辑器', 'type' => 'switch', 'default' => false, 'help' => '开启后帖子可使用 Gutenberg'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-permissions.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-permissions.php
new file mode 100644
index 00000000..19c3d4c0
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-permissions.php
@@ -0,0 +1,46 @@
+ 'permissions',
+ 'title' => '权限配置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'posting', 'title' => '发帖限制', 'fields' => [
+ ['key' => 'bbs_posts_add_limit_opt_max', 'label' => '每日发帖上限', 'type' => 'number', 'default' => 10, 'min' => 0, 'help' => '0 = 不限'],
+ ['key' => 'bbs_plate_add_limit_opt_max', 'label' => '建版数量上限', 'type' => 'number', 'default' => 3, 'min' => 0],
+ ]],
+ ['key' => 'display_btns', 'title' => '按钮显示', 'fields' => [
+ ['key' => 'bbs_show_new_plate', 'label' => '始终显示建版按钮', 'type' => 'switch', 'default' => false],
+ ['key' => 'bbs_show_new_topic', 'label' => '始终显示发帖按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'bbs_show_apply_moderator', 'label' => '始终显示申请版主', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'paid_content', 'title' => '帖子付费', 'fields' => [
+ ['key' => 'bbs_post_pay_hide_type_s', 'label' => '启用付费内容', 'type' => 'switch', 'default' => false],
+ ['key' => 'bbs_post_pay_vip_price_s', 'label' => 'VIP 特价', 'type' => 'switch', 'default' => false],
+ ['key' => 'bbs_post_pay_vip_1_discount', 'label' => '一级 VIP 折扣 (%)', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100],
+ ['key' => 'bbs_post_pay_vip_2_discount', 'label' => '二级 VIP 折扣 (%)', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 100],
+ ['key' => 'bbs_post_pay_price_limit', 'label' => '最高定价', 'type' => 'number', 'default' => 1000, 'min' => 1],
+ ]],
+ ['key' => 'plate_follow', 'title' => '版块付费关注', 'fields' => [
+ ['key' => 'bbs_plate_follow_pay_s', 'label' => '启用付费关注', 'type' => 'switch', 'default' => false],
+ ['key' => 'bbs_plate_follow_pay_price', 'label' => '关注价格', 'type' => 'number', 'default' => 10, 'min' => 0],
+ ['key' => 'bbs_plate_follow_pay_type', 'label' => '支付方式', 'type' => 'select', 'default' => 'money', 'options' => [
+ ['label' => '现金', 'value' => 'money'],
+ ['label' => '积分', 'value' => 'points'],
+ ]],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-post-page.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-post-page.php
new file mode 100644
index 00000000..a1118643
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-post-page.php
@@ -0,0 +1,46 @@
+ 'post_page',
+ 'title' => '帖子页面',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'breadcrumbs', 'title' => '面包屑', 'fields' => [
+ ['key' => 'bbs_breadcrumbs_s', 'label' => '显示面包屑', 'type' => 'switch', 'default' => true],
+ ['key' => 'bbs_breadcrumbs_home', 'label' => '显示首页', 'type' => 'switch', 'default' => true],
+ ['key' => 'bbs_breadcrumbs_bbs_home', 'label' => '显示论坛首页', 'type' => 'switch', 'default' => true],
+ ['key' => 'bbs_breadcrumbs_bbs_home_name', 'label' => '论坛首页名称', 'type' => 'text', 'default' => '论坛'],
+ ['key' => 'bbs_breadcrumbs_plate_cat', 'label' => '显示分区', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'score', 'title' => '评分设置', 'fields' => [
+ ['key' => 'bbs_score_extra_max', 'label' => '加分上限', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 50],
+ ['key' => 'bbs_score_deduct_max', 'label' => '扣分上限', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 50],
+ ]],
+ ['key' => 'display', 'title' => '显示', 'fields' => [
+ ['key' => 'bbs_posts_maxheight_s', 'label' => '内容高度限制', 'type' => 'switch', 'default' => false],
+ ['key' => 'bbs_posts_maxheight', 'label' => '最大高度 (px)', 'type' => 'number', 'default' => 800, 'min' => 300, 'max' => 3000, 'condition' => ['bbs_posts_maxheight_s' => true]],
+ ]],
+ ['key' => 'cover', 'title' => '封面设置', 'fields' => [
+ ['key' => 'bbs_posts_cover_ratio', 'label' => '封面比例', 'type' => 'select', 'default' => '16:9', 'options' => [
+ ['label' => '16:9', 'value' => '16:9'],
+ ['label' => '4:3', 'value' => '4:3'],
+ ['label' => '1:1', 'value' => '1:1'],
+ ['label' => '3:4', 'value' => '3:4'],
+ ]],
+ ['key' => 'bbs_posts_cover_video_mute', 'label' => '视频封面静音', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-reply.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-reply.php
new file mode 100644
index 00000000..7979ddcd
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-reply.php
@@ -0,0 +1,39 @@
+ 'reply',
+ 'title' => '回复评论',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'paginate', 'title' => '翻页', 'fields' => [
+ ['key' => 'bbs_reply_paginate_type', 'label' => '翻页方式', 'type' => 'select', 'default' => 'ajax', 'options' => [
+ ['label' => 'AJAX 加载', 'value' => 'ajax'],
+ ['label' => '传统翻页', 'value' => 'page'],
+ ]],
+ ['key' => 'bbs_reply_paginate_ias_s', 'label' => '无限滚动', 'type' => 'switch', 'default' => false],
+ ['key' => 'bbs_reply_paginate_ias_max', 'label' => '自动加载页数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 10],
+ ]],
+ ['key' => 'toolbar', 'title' => '回复工具栏', 'fields' => [
+ ['key' => 'bbs_comment_smilie', 'label' => '表情', 'type' => 'switch', 'default' => true],
+ ['key' => 'bbs_comment_code', 'label' => '代码', 'type' => 'switch', 'default' => true],
+ ['key' => 'bbs_comment_img', 'label' => '插图', 'type' => 'switch', 'default' => true],
+ ['key' => 'bbs_comment_upload_img', 'label' => '上传图片', 'type' => 'switch', 'default' => false],
+ ['key' => 'bbs_comment_quick_s', 'label' => '快捷回复', 'type' => 'switch', 'default' => true],
+ ['key' => 'bbs_comment_quick_often', 'label' => '预设短语', 'type' => 'textarea', 'default' => "不错,学习了\n感谢分享\n期待更新", 'help' => '每行一条'],
+ ['key' => 'bbs_comment_placeholder', 'label' => '回复占位符', 'type' => 'text', 'default' => '写下你的回复…'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum-upload.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-upload.php
new file mode 100644
index 00000000..dd925f09
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum-upload.php
@@ -0,0 +1,29 @@
+ 'upload',
+ 'title' => '上传权限',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'limits', 'title' => '上传限制', 'fields' => [
+ ['key' => 'bbs_upload_img_size', 'label' => '图片大小 (KB)', 'type' => 'number', 'default' => 2048, 'min' => 100],
+ ['key' => 'bbs_image_upload_multiple', 'label' => '批量上传数', 'type' => 'number', 'default' => 9, 'min' => 1, 'max' => 20],
+ ['key' => 'bbs_upload_video_size', 'label' => '视频大小 (KB)', 'type' => 'number', 'default' => 20480, 'min' => 1024],
+ ['key' => 'bbs_upload_file_size', 'label' => '文件大小 (KB)', 'type' => 'number', 'default' => 10240, 'min' => 100],
+ ['key' => 'bbs_upload_file_mimes', 'label' => '允许格式', 'type' => 'text', 'default' => 'jpg,jpeg,png,gif,mp4,zip,rar,pdf', 'help' => '逗号分隔'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-forum.php b/inc/classes/admin/schema/class-aksrt-admin-schema-forum.php
new file mode 100644
index 00000000..b441bf61
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-forum.php
@@ -0,0 +1,126 @@
+ 'forum',
+ 'title' => '社区&论坛',
+ 'sections' => [
+ 'basic' => AKSRT_Admin_Schema_Forum_Basic::build(),
+ 'names' => AKSRT_Admin_Schema_Forum_Names::build(),
+ 'homepage' => AKSRT_Admin_Schema_Forum_Homepage::build(),
+ 'board_page' => AKSRT_Admin_Schema_Forum_Board_Page::build(),
+ 'post_page' => AKSRT_Admin_Schema_Forum_Post_Page::build(),
+ 'reply' => AKSRT_Admin_Schema_Forum_Reply::build(),
+ 'permissions' => AKSRT_Admin_Schema_Forum_Permissions::build(),
+ 'other' => AKSRT_Admin_Schema_Forum_Other::build(),
+ 'upload' => AKSRT_Admin_Schema_Forum_Upload::build(),
+ 'credits' => AKSRT_Admin_Schema_Forum_Credits::build(),
+ ],
+ ];
+ }
+
+ // ================================================================
+ // §8.1 Capability matrix schema builder
+ // ================================================================
+
+ /**
+ * Build groups for the capabilities section from cap_definitions().
+ *
+ * Each capability becomes a group with role columns as fields.
+ * Grouped by prefix: user_, new_post_, comment_.
+ */
+ private static function build_cap_matrix_groups(): array {
+ if (!class_exists('AKSRT_Settings')) {
+ return [];
+ }
+
+ $defs = AKSRT_Settings::cap_definitions();
+ $cols = AKSRT_Settings::cap_role_columns();
+
+ $col_labels = [
+ 'all' => __('所有人', 'aksrt'),
+ 'logged' => __('已登录', 'aksrt'),
+ 'level' => __('等级≥', 'aksrt'),
+ 'vip' => __('VIP≥', 'aksrt'),
+ 'auth' => __('已认证', 'aksrt'),
+ 'moderator' => __('版主', 'aksrt'),
+ 'plate_author' => __('超级版主', 'aksrt'),
+ 'cat_moderator' => __('分区版主', 'aksrt'),
+ ];
+
+ // Group caps by prefix
+ $buckets = [
+ 'user' => ['title' => __('用户治理', 'aksrt'), 'caps' => []],
+ 'post' => ['title' => __('投稿权限', 'aksrt'), 'caps' => []],
+ 'comment' => ['title' => __('评论权限', 'aksrt'), 'caps' => []],
+ ];
+
+ foreach ($defs as $cap_id => $def) {
+ if (str_starts_with($cap_id, 'comment_')) {
+ $buckets['comment']['caps'][$cap_id] = $def;
+ } elseif (str_starts_with($cap_id, 'new_post_')) {
+ $buckets['post']['caps'][$cap_id] = $def;
+ } else {
+ $buckets['user']['caps'][$cap_id] = $def;
+ }
+ }
+
+ $groups = [];
+ foreach ($buckets as $bucket_key => $bucket) {
+ if (empty($bucket['caps'])) continue;
+
+ $fields = [];
+ foreach ($bucket['caps'] as $cap_id => $def) {
+ $excludes = $def['exclude_roles'] ?? [];
+ $only_roles = $def['roles'] ?? null;
+ $defaults = $def['default'] ?? [];
+
+ // Each cap gets one field per role column
+ foreach ($cols as $col) {
+ if (in_array($col, $excludes, true)) continue;
+ if ($only_roles !== null && !in_array($col, $only_roles, true)
+ && !in_array($col, ['all', 'logged', 'level', 'vip', 'auth'], true)) {
+ continue;
+ }
+
+ $field_key = 'user_caps.' . $cap_id . '.' . $col;
+ $is_spinner = in_array($col, ['level', 'vip'], true);
+
+ $field = [
+ 'key' => $field_key,
+ 'label' => $def['label'] . ' — ' . ($col_labels[$col] ?? $col),
+ 'type' => $is_spinner ? 'number' : 'switch',
+ 'default' => $defaults[$col] ?? ($is_spinner ? -1 : false),
+ 'cap_id' => $cap_id,
+ 'cap_col' => $col,
+ ];
+
+ if ($is_spinner) {
+ $field['min'] = -1;
+ $field['max'] = $col === 'level' ? 10 : 2;
+ $field['help'] = __('-1 = 不限', 'aksrt');
+ }
+
+ $fields[] = $field;
+ }
+ }
+
+ $groups[] = [
+ 'key' => 'cap_' . $bucket_key,
+ 'title' => $bucket['title'],
+ 'fields' => $fields,
+ ];
+ }
+
+ return $groups;
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-global.php b/inc/classes/admin/schema/class-aksrt-admin-schema-global.php
new file mode 100644
index 00000000..14f8efca
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-global.php
@@ -0,0 +1,530 @@
+ 'global',
+ 'title' => '全局&功能',
+ 'sections' => [
+ 'common' => [
+ 'key' => 'common',
+ 'title' => '常用功能',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'comment', 'title' => '评论功能', 'fields' => [
+ ['key' => 'comment_enabled', 'label' => '开启评论', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'performance', 'title' => '性能优化', 'fields' => [
+ ['key' => 'lazyload_enabled', 'label' => '图片懒加载', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'avatar', 'title' => '头像服务', 'fields' => [
+ ['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [
+ ['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'],
+ ['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'],
+ ['label' => 'Gravatar 官方', 'value' => 'gravatar.com'],
+ ]],
+ ]],
+ ['key' => 'filing', 'title' => '备案信息', 'fields' => [
+ ['key' => 'icp_filing', 'label' => 'ICP 备案号', 'type' => 'input', 'default' => ''],
+ ['key' => 'footer_text', 'label' => '页脚附加文字', 'type' => 'input', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'logo' => [
+ 'key' => 'logo',
+ 'title' => 'LOGO图像',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_seo_settings',
+ 'groups' => [
+ ['key' => 'logo', 'title' => 'Logo 设置', 'fields' => [
+ ['key' => 'site_logo', 'label' => '站点 Logo', 'type' => 'media', 'default' => ''],
+ ['key' => 'site_logo_dark', 'label' => '暗色模式 Logo', 'type' => 'media', 'default' => ''],
+ ['key' => 'logo_width', 'label' => 'Logo 宽度', 'type' => 'number', 'default' => 150, 'min' => 50, 'max' => 400],
+ ['key' => 'logo_text', 'label' => 'Logo 文字', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'favicon', 'title' => '站点图标', 'fields' => [
+ ['key' => 'site_favicon', 'label' => '浏览器图标', 'type' => 'media', 'default' => ''],
+ ['key' => 'site_icon', 'label' => '移动端图标', 'type' => 'media', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'seo' => [
+ 'key' => 'seo',
+ 'title' => 'SEO优化',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
+ ['key' => 'seo_enabled', 'label' => '启用核心SEO优化', 'type' => 'switch', 'default' => true],
+ ['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false],
+ ['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-'],
+ ['key' => 'seo_title', 'label' => 'SEO标题(title)', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'seo_keywords', 'label' => 'SEO关键字(keywords)', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'seo_description', 'label' => 'SEO描述(description)', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'ai-seo', 'title' => 'AI SEO', 'fields' => [
+ ['key' => 'ai_seo_title_prompt', 'label' => '标题提示词', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'baidu', 'title' => '百度SEO', 'fields' => [
+ ['key' => 'seo_baidu_push', 'label' => '百度链接自动提交', 'type' => 'switch', 'default' => false],
+ ['key' => 'seo_baidu_normal', 'label' => '百度普通收录', 'type' => 'switch', 'default' => false],
+ ['key' => 'seo_baidu_fast', 'label' => '百度快速收录', 'type' => 'switch', 'default' => false],
+ ['key' => 'seo_baidu_token', 'label' => '百度准入密钥Token', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'image', 'title' => '图片SEO', 'fields' => [
+ ['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
+ ['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
+ ['key' => 'seo_image_always_default', 'label' => '固定使用默认图像', 'type' => 'switch', 'default' => false],
+ ['key' => 'seo_auto_alt', 'label' => '文章图片自动添加alt', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'redirect', 'title' => '外链重定向', 'fields' => [
+ ['key' => 'seo_go_link', 'label' => '启用外链重定向', 'type' => 'switch', 'default' => true],
+ ['key' => 'seo_go_link_nonce', 'label' => '外链重定向鉴权', 'type' => 'switch', 'default' => true],
+ ['key' => 'seo_go_link_post', 'label' => '文章内容外链重定向', 'type' => 'switch', 'default' => true],
+ ['key' => 'seo_go_link_exclude', 'label' => '排除域名', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'advanced', 'title' => '高级SEO', 'fields' => [
+ ['key' => 'seo_no_category', 'label' => '分类URL去除category', 'type' => 'switch', 'default' => false],
+ ['key' => 'seo_jsonld', 'label' => '启用JSON-LD', 'type' => 'switch', 'default' => true],
+ ['key' => 'seo_jsonld_single', 'label' => '文章页JSON-LD', 'type' => 'switch', 'default' => true],
+ ['key' => 'seo_jsonld_page', 'label' => '页面JSON-LD', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'lazyload' => [
+ 'key' => 'lazyload',
+ 'title' => '懒加载设置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '懒加载开关', 'fields' => [
+ ['key' => 'lazyload_enabled', 'label' => '图像异步懒加载', 'type' => 'switch', 'default' => true],
+ ['key' => 'lazyload_animation', 'label' => '懒加载动画效果', 'type' => 'select', 'default' => 'blur', 'options' => [
+ ['label' => '淡出淡入', 'value' => 'fade'],
+ ['label' => '高斯模糊', 'value' => 'blur'],
+ ['label' => '放大缩小', 'value' => 'scale'],
+ ]],
+ ['key' => 'lazyload_placeholder', 'label' => '懒加载预载图', 'type' => 'media', 'default' => ''],
+ ['key' => 'lazyload_blur', 'label' => '模糊预览', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'scope', 'title' => '懒加载范围', 'fields' => [
+ ['key' => 'lazyload_avatar', 'label' => '头像懒加载', 'type' => 'switch', 'default' => true],
+ ['key' => 'lazyload_content', 'label' => '文章内容图片懒加载', 'type' => 'switch', 'default' => false],
+ ['key' => 'lazyload_comment', 'label' => '评论内容图片懒加载', 'type' => 'switch', 'default' => true],
+ ['key' => 'lazyload_private', 'label' => '私信聊天图片懒加载', 'type' => 'switch', 'default' => true],
+ ['key' => 'lazyload_slider', 'label' => '幻灯片图片懒加载', 'type' => 'switch', 'default' => true],
+ ['key' => 'lazyload_cover', 'label' => '封面图片懒加载', 'type' => 'switch', 'default' => true],
+ ['key' => 'lazyload_other', 'label' => '其它图片懒加载', 'type' => 'switch', 'default' => true],
+ ['key' => 'lazyload_widget', 'label' => '小工具图片懒加载', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'layout' => [
+ 'key' => 'layout',
+ 'title' => '显示&布局',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'color', 'title' => '色彩方案', 'fields' => [
+ ['key' => 'theme_mode', 'label' => '默认主题模式', 'type' => 'radio', 'default' => 'time-auto', 'options' => [
+ ['label' => '浅色', 'value' => 'white-theme'],
+ ['label' => '深色', 'value' => 'dark-theme'],
+ ['label' => '跟随系统', 'value' => 'time-auto'],
+ ]],
+ ['key' => 'theme_mode_button', 'label' => '主题切换按钮位置', 'type' => 'checkbox', 'default' => ['pc_nav', 'm_menu'], 'options' => [
+ ['label' => 'PC 顶部导航', 'value' => 'pc_nav'],
+ ['label' => '移动端菜单', 'value' => 'm_menu'],
+ ]],
+ ['key' => 'theme_skin', 'label' => '预设主题色', 'type' => 'palette', 'default' => 'f04494', 'options' => [
+ ['label' => '玫红', 'value' => 'ff1856'],
+ ['label' => '粉色', 'value' => 'f04494'],
+ ['label' => '紫色', 'value' => 'ae53f3'],
+ ['label' => '靛蓝', 'value' => '627bf5'],
+ ['label' => '天蓝', 'value' => '00a2e3'],
+ ['label' => '青绿', 'value' => '16b597'],
+ ['label' => '翠绿', 'value' => '36af18'],
+ ['label' => '黄绿', 'value' => '8fb107'],
+ ['label' => '棕金', 'value' => 'b18c07'],
+ ['label' => '橘橙', 'value' => 'e06711'],
+ ['label' => '朱红', 'value' => 'f74735'],
+ ]],
+ ['key' => 'primary_color', 'label' => '自定义主题色', 'type' => 'color', 'default' => '#f04494'],
+ ['key' => 'dark_primary_color', 'label' => '暗色模式主题色', 'type' => 'color', 'default' => '#f04494'],
+ ['key' => 'border_radius', 'label' => '全局圆角', 'type' => 'slider', 'default' => 8, 'min' => 0, 'max' => 20],
+ ['key' => 'theme_main_radius', 'label' => '卡片圆角 (px)', 'type' => 'number', 'default' => 12, 'min' => 0, 'max' => 30, 'help' => '独立卡片圆角,不填则使用全局圆角 +4'],
+ ['key' => 'close_backdrop', 'label' => '关闭毛玻璃效果', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'layout', 'title' => '布局设置', 'fields' => [
+ ['key' => 'container_max_width', 'label' => '内容最大宽度', 'type' => 'number', 'default' => 1200, 'min' => 0, 'max' => 1600],
+ ['key' => 'sidebar_enabled', 'label' => '开启侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'sidebar_position', 'label' => '侧边栏位置', 'type' => 'radio', 'default' => 'right', 'options' => [
+ ['label' => '左侧', 'value' => 'left'],
+ ['label' => '右侧', 'value' => 'right'],
+ ]],
+ ['key' => 'sidebar_width', 'label' => '侧边栏宽度', 'type' => 'number', 'default' => 300, 'min' => 200, 'max' => 400],
+ ['key' => 'sidebar_home_enabled', 'label' => '首页侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'sidebar_category_enabled', 'label' => '分类页侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'sidebar_tag_enabled', 'label' => '标签页侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'sidebar_single_enabled', 'label' => '文章页侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'sidebar_page_enabled', 'label' => '页面侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'sidebar_search_enabled', 'label' => '搜索页侧边栏', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'typography', 'title' => '字体设置', 'fields' => [
+ ['key' => 'font_family', 'label' => '正文字体', 'type' => 'select', 'default' => 'system', 'options' => [
+ ['label' => '系统默认', 'value' => 'system'],
+ ['label' => '思源黑体', 'value' => 'noto-sans-sc'],
+ ['label' => '思源宋体', 'value' => 'noto-serif-sc'],
+ ['label' => '自定义', 'value' => 'custom'],
+ ]],
+ ]],
+ ['key' => 'performance', 'title' => '性能优化', 'fields' => [
+ ['key' => 'qj_loading', 'label' => '全站加载动画', 'type' => 'switch', 'default' => false],
+ ['key' => 'qj_dh_xs', 'label' => '加载动画样式', 'type' => 'select', 'default' => 'no1', 'options' => [
+ ['label' => '样式 1', 'value' => 'no1'],
+ ['label' => '样式 2', 'value' => 'no2'],
+ ['label' => '样式 3', 'value' => 'no3'],
+ ['label' => '样式 4', 'value' => 'no4'],
+ ['label' => '样式 5', 'value' => 'no5'],
+ ['label' => '样式 6', 'value' => 'no6'],
+ ['label' => '样式 7', 'value' => 'no7'],
+ ['label' => '样式 8', 'value' => 'no8'],
+ ['label' => '样式 9', 'value' => 'no9'],
+ ['label' => '样式 10', 'value' => 'no10'],
+ ]],
+ ]],
+ ['key' => 'avatar', 'title' => '头像设置', 'fields' => [
+ ['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [
+ ['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'],
+ ['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'],
+ ['label' => 'Gravatar 官方', 'value' => 'gravatar.com'],
+ ], 'help' => '国内站点建议优先选 Cravatar'],
+ ]],
+ ],
+ ],
+ 'code-highlight' => [
+ 'key' => 'code-highlight',
+ 'title' => '代码高亮',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '代码高亮设置', 'fields' => [
+ ['key' => 'highlight_kg', 'label' => '代码高亮开关', 'type' => 'switch', 'default' => true],
+ ['key' => 'highlight_hh', 'label' => '显示行号', 'type' => 'switch', 'default' => false],
+ ['key' => 'highlight_btn', 'label' => '显示扩展按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'highlight_maxheight', 'label' => '最大高度(px,0=不限)', 'type' => 'number', 'default' => 400, 'min' => 0, 'max' => 2000],
+ ]],
+ ['key' => 'theme', 'title' => '主题设置', 'fields' => [
+ ['key' => 'highlight_zt', 'label' => '日间默认主题', 'type' => 'select', 'default' => 'enlighter', 'options' => [
+ ['label' => '浅色: Enlighter', 'value' => 'enlighter'],
+ ['label' => '浅色: Bootstrap', 'value' => 'bootstrap4'],
+ ['label' => '浅色: Classic', 'value' => 'classic'],
+ ['label' => '浅色: Beyond', 'value' => 'beyond'],
+ ['label' => '浅色: Mowtwo', 'value' => 'mowtwo'],
+ ['label' => '浅色: Eclipse', 'value' => 'eclipse'],
+ ['label' => '浅色: Droide', 'value' => 'droide'],
+ ['label' => '浅色: Minimal', 'value' => 'minimal'],
+ ['label' => '浅色: Rowhammer', 'value' => 'rowhammer'],
+ ['label' => '浅色: Godzilla', 'value' => 'godzilla'],
+ ['label' => '深色: Dracula', 'value' => 'dracula'],
+ ['label' => '深色: Atomic', 'value' => 'atomic'],
+ ['label' => '深色: Monokai', 'value' => 'monokai'],
+ ]],
+ ['key' => 'highlight_dark_zt', 'label' => '暗色默认主题', 'type' => 'select', 'default' => 'monokai', 'options' => [
+ ['label' => '深色: Dracula', 'value' => 'dracula'],
+ ['label' => '深色: Atomic', 'value' => 'atomic'],
+ ['label' => '深色: Monokai', 'value' => 'monokai'],
+ ['label' => '浅色: Enlighter', 'value' => 'enlighter'],
+ ['label' => '浅色: Bootstrap', 'value' => 'bootstrap4'],
+ ]],
+ ]],
+ ],
+ ],
+ 'system-notice' => [
+ 'key' => 'system-notice',
+ 'title' => '系统公告',
+ 'version' => '2.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '公告设置', 'fields' => [
+ ['key' => 'system_notice_enabled', 'label' => '启用系统公告', 'type' => 'switch', 'default' => true],
+ ['key' => 'system_notice_policy', 'label' => '显示对象', 'type' => 'select', 'default' => '', 'options' => [
+ ['label' => '所有访客', 'value' => ''],
+ ['label' => '仅登录用户', 'value' => 'signin'],
+ ['label' => '仅 VIP 会员', 'value' => 'vip'],
+ ['label' => '仅非 VIP', 'value' => 'free'],
+ ['label' => '仅认证用户', 'value' => 'auth'],
+ ]],
+ ['key' => 'system_notice_title', 'label' => '公告标题', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_content', 'label' => '公告内容', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'buttons', 'title' => '按钮组(留空则不显示)', 'fields' => [
+ ['key' => 'system_notice_btn1_text', 'label' => '按钮 1 文案', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_btn1_link', 'label' => '按钮 1 链接', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_btn1_color', 'label' => '按钮 1 颜色', 'type' => 'select', 'default' => 'primary', 'options' => [
+ ['label' => '主题色', 'value' => 'primary'],
+ ['label' => '成功绿', 'value' => 'success'],
+ ['label' => '警告橙', 'value' => 'warning'],
+ ['label' => '危险红', 'value' => 'danger'],
+ ['label' => '信息蓝', 'value' => 'info'],
+ ['label' => '深紫', 'value' => 'purple'],
+ ['label' => '深灰', 'value' => 'dark'],
+ ]],
+ ['key' => 'system_notice_btn2_text', 'label' => '按钮 2 文案', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_btn2_link', 'label' => '按钮 2 链接', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_btn2_color', 'label' => '按钮 2 颜色', 'type' => 'select', 'default' => 'primary', 'options' => [
+ ['label' => '主题色', 'value' => 'primary'],
+ ['label' => '成功绿', 'value' => 'success'],
+ ['label' => '警告橙', 'value' => 'warning'],
+ ['label' => '危险红', 'value' => 'danger'],
+ ['label' => '信息蓝', 'value' => 'info'],
+ ['label' => '深紫', 'value' => 'purple'],
+ ['label' => '深灰', 'value' => 'dark'],
+ ]],
+ ['key' => 'system_notice_btn3_text', 'label' => '按钮 3 文案', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_btn3_link', 'label' => '按钮 3 链接', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_btn3_color', 'label' => '按钮 3 颜色', 'type' => 'select', 'default' => 'primary', 'options' => [
+ ['label' => '主题色', 'value' => 'primary'],
+ ['label' => '成功绿', 'value' => 'success'],
+ ['label' => '警告橙', 'value' => 'warning'],
+ ['label' => '危险红', 'value' => 'danger'],
+ ['label' => '信息蓝', 'value' => 'info'],
+ ['label' => '深紫', 'value' => 'purple'],
+ ['label' => '深灰', 'value' => 'dark'],
+ ]],
+ ['key' => 'system_notice_btn4_text', 'label' => '按钮 4 文案', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_btn4_link', 'label' => '按钮 4 链接', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_btn4_color', 'label' => '按钮 4 颜色', 'type' => 'select', 'default' => 'primary', 'options' => [
+ ['label' => '主题色', 'value' => 'primary'],
+ ['label' => '成功绿', 'value' => 'success'],
+ ['label' => '警告橙', 'value' => 'warning'],
+ ['label' => '危险红', 'value' => 'danger'],
+ ['label' => '信息蓝', 'value' => 'info'],
+ ['label' => '深紫', 'value' => 'purple'],
+ ['label' => '深灰', 'value' => 'dark'],
+ ]],
+ ]],
+ ['key' => 'style', 'title' => '样式设置', 'fields' => [
+ ['key' => 'system_notice_size', 'label' => '弹窗尺寸', 'type' => 'select', 'default' => '', 'options' => [
+ ['label' => '默认', 'value' => ''],
+ ['label' => '迷你', 'value' => 'modal-mini'],
+ ['label' => '小号', 'value' => 'modal-sm'],
+ ['label' => '大号', 'value' => 'modal-lg'],
+ ]],
+ ['key' => 'system_notice_radius', 'label' => '按钮圆角', 'type' => 'switch', 'default' => false, 'help' => '开启后弹窗按钮使用圆角样式'],
+ ['key' => 'system_notice_class', 'label' => '公告样式类', 'type' => 'input', 'default' => ''],
+ ['key' => 'system_notice_title_icon', 'label' => '标题图标', 'type' => 'icon', 'default' => ''],
+ ['key' => 'system_notice_title_class', 'label' => '标题背景配色', 'type' => 'select', 'default' => '', 'options' => [
+ ['label' => '无', 'value' => ''],
+ ['label' => '炫彩黄', 'value' => 'jb-yellow'],
+ ['label' => '炫彩蓝', 'value' => 'jb-blue'],
+ ['label' => '炫彩绿', 'value' => 'jb-green'],
+ ['label' => '炫彩紫', 'value' => 'jb-purple'],
+ ['label' => '炫彩红', 'value' => 'jb-red'],
+ ['label' => '炫彩橙', 'value' => 'jb-orange'],
+ ], 'help' => '弹窗标题区域的背景配色方案'],
+ ['key' => 'system_notice_title_style', 'label' => '标题样式', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'expire', 'title' => '过期设置', 'fields' => [
+ ['key' => 'system_notice_expires', 'label' => '弹窗周期(小时)', 'type' => 'number', 'default' => 24, 'min' => 0, 'max' => 720, 'help' => '0 表示永不过期'],
+ ]],
+ ],
+ ],
+ 'search' => [
+ 'key' => 'search',
+ 'title' => '搜索功能',
+ 'version' => '2.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '搜索基础', 'fields' => [
+ ['key' => 'search_mode', 'label' => '搜索模式', 'type' => 'radio', 'default' => 'default', 'options' => [
+ ['label' => '默认搜索', 'value' => 'default'],
+ ['label' => 'AJAX 实时搜索', 'value' => 'ajax'],
+ ['label' => '跳转搜索', 'value' => 'redirect'],
+ ]],
+ ['key' => 'search_placeholder', 'label' => '搜索框提示文字', 'type' => 'input', 'default' => '开启精彩搜索'],
+ ]],
+ ['key' => 'content-types', 'title' => '搜索内容类型', 'fields' => [
+ ['key' => 'search_type_post', 'label' => '文章', 'type' => 'switch', 'default' => true],
+ ['key' => 'search_type_user', 'label' => '用户', 'type' => 'switch', 'default' => false],
+ ['key' => 'search_type_category', 'label' => '分类', 'type' => 'switch', 'default' => true],
+ ['key' => 'search_type_tag', 'label' => '标签', 'type' => 'switch', 'default' => true],
+ ['key' => 'search_type_product', 'label' => '商品', 'type' => 'switch', 'default' => false],
+ ['key' => 'search_type_topic', 'label' => '帖子', 'type' => 'switch', 'default' => false],
+ ['key' => 'search_default_type', 'label' => '默认类型', 'type' => 'select', 'default' => 'post', 'options' => [
+ ['label' => '文章', 'value' => 'post'],
+ ['label' => '用户', 'value' => 'user'],
+ ['label' => '分类', 'value' => 'category'],
+ ['label' => '标签', 'value' => 'tag'],
+ ['label' => '商品', 'value' => 'product'],
+ ['label' => '帖子', 'value' => 'topic'],
+ ]],
+ ]],
+ ['key' => 'features', 'title' => '搜索功能', 'fields' => [
+ ['key' => 'search_history_enabled', 'label' => '搜索历史', 'type' => 'switch', 'default' => true],
+ ['key' => 'hot_search_enabled', 'label' => '热门搜索', 'type' => 'switch', 'default' => true],
+ ['key' => 'hot_search_keywords', 'label' => '热门搜索关键词', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'search_suggestions_enabled', 'label' => '搜索建议', 'type' => 'switch', 'default' => false],
+ ['key' => 'search_posts', 'label' => '搜索框热门文章', 'type' => 'switch', 'default' => false, 'help' => '在搜索面板中显示热门文章列表'],
+ ['key' => 'search_no_page', 'label' => '排除页面', 'type' => 'switch', 'default' => false, 'help' => '搜索结果中排除页面(仅显示文章)'],
+ ['key' => 'search_exclude_cats', 'label' => '排除分类', 'type' => 'textarea', 'default' => '', 'help' => '搜索结果中排除的分类 ID(逗号分隔)'],
+ ['key' => 'search_custom_key_s', 'label' => '自定义匹配范围', 'type' => 'switch', 'default' => false, 'help' => '启用后可配置搜索匹配字段范围(标题/内容/分类/标签等)'],
+ ['key' => 'search_custom_key', 'label' => '匹配字段', 'type' => 'textarea', 'default' => 'title,content', 'help' => '搜索匹配的字段范围(逗号分隔:title/content/excerpt/category/tag/author)'],
+ ['key' => 'search_facets', 'label' => '搜索结果筛选', 'type' => 'switch', 'default' => false, 'help' => '启用搜索结果 taxonomy 筛选面板'],
+ ['key' => 'search_facets_order', 'label' => '筛选排序', 'type' => 'textarea', 'default' => "category\ntag", 'help' => '筛选 fieldset 显示顺序(每行一个 taxonomy slug)'],
+ ]],
+ ['key' => 'category-search', 'title' => '分类搜索', 'fields' => [
+ ['key' => 'search_cat', 'label' => '启用分类搜索', 'type' => 'switch', 'default' => false, 'help' => '在搜索框旁显示分类选择器'],
+ ['key' => 'search_cat_in', 'label' => '默认分类', 'type' => 'input', 'default' => '', 'help' => '默认搜索的分类 ID(留空为全部分类)'],
+ ['key' => 'search_more_cat_obj', 'label' => '显示更多分类', 'type' => 'switch', 'default' => true, 'help' => '展开显示全部分类列表'],
+ ]],
+ ['key' => 'popular', 'title' => '热门搜索', 'fields' => [
+ ['key' => 'search_popular_title', 'label' => '标题', 'type' => 'input', 'default' => '热门搜索'],
+ ['key' => 'search_popular_key_num', 'label' => '显示数量', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 50],
+ ['key' => 'search_popular_sticky', 'label' => '置顶词(每行一个)', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'meilisearch', 'title' => 'Meilisearch 集成', 'fields' => [
+ ['key' => 'search_ms_enabled', 'label' => '启用 Meilisearch', 'type' => 'switch', 'default' => false, 'help' => '启用后搜索请求将发送至 Meilisearch 服务器'],
+ ['key' => 'search_ms_api_url', 'label' => 'API 地址', 'type' => 'input', 'default' => '', 'help' => 'Meilisearch 服务器 URL'],
+ ['key' => 'search_ms_api_key', 'label' => 'API Key', 'type' => 'input', 'default' => '', 'help' => 'Meilisearch Search API Key(只读密钥)'],
+ ['key' => 'search_ms_index_prefix', 'label' => '索引前缀', 'type' => 'input', 'default' => 'aksrt', 'help' => '索引名称前缀'],
+ ['key' => 'search_ms_synonyms', 'label' => '同义词(JSON)', 'type' => 'textarea', 'default' => '', 'help' => 'Meilisearch 同义词配置 JSON'],
+ ]],
+ ],
+ ],
+ 'email' => [
+ 'key' => 'email',
+ 'title' => '邮件事件',
+ 'version' => '2.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'global', 'title' => '全局设置', 'fields' => [
+ ['key' => 'email_footer_text', 'label' => '邮件页脚文字', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'email_site_branding', 'label' => '显示站点品牌', 'type' => 'switch', 'default' => true],
+ ['key' => 'email_template_mode', 'label' => '邮件格式', 'type' => 'radio', 'default' => 'plain', 'options' => [
+ ['label' => '纯文本', 'value' => 'plain'],
+ ['label' => 'HTML', 'value' => 'html'],
+ ]],
+ ['key' => 'email_private_receive_limit', 'label' => '私信邮件接收组', 'type' => 'select', 'default' => 'all', 'options' => [
+ ['label' => '所有用户', 'value' => 'all'],
+ ['label' => 'VIP 及以上', 'value' => 'vip'],
+ ['label' => 'VIP2 及以上', 'value' => 'vip2'],
+ ['label' => '仅管理员', 'value' => 'admin'],
+ ], 'help' => '限制哪些用户组可以收到私信邮件通知'],
+ ]],
+ ['key' => 'branding', 'title' => '邮件品牌', 'fields' => [
+ ['key' => 'mail_showname', 'label' => '发件人名称', 'type' => 'input', 'default' => '', 'help' => '覆盖系统默认发件人名称'],
+ ['key' => 'mail_title_prefix', 'label' => '主题前缀', 'type' => 'input', 'default' => '', 'help' => '自动添加在所有邮件主题前面,如【站点名】'],
+ ['key' => 'mail_description', 'label' => '邮件底部说明', 'type' => 'textarea', 'default' => '', 'help' => '附加在邮件页脚文字之后,留空不显示'],
+ ['key' => 'mail_more_content', 'label' => '邮件附加内容', 'type' => 'textarea', 'default' => '', 'help' => '附加在邮件内容末尾的补充说明'],
+ ]],
+ ['key' => 'reg_verify_link', 'title' => '注册验证链接', 'fields' => [
+ ['key' => 'email_reg_verify_link_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
+ ['key' => 'email_reg_verify_link_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
+ ['key' => 'email_reg_verify_link_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'email_reg_verify_link_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'reg_verify_code', 'title' => '注册验证码', 'fields' => [
+ ['key' => 'email_reg_verify_code_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
+ ['key' => 'email_reg_verify_code_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
+ ['key' => 'email_reg_verify_code_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'email_reg_verify_code_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'reg_success', 'title' => '注册成功通知', 'fields' => [
+ ['key' => 'email_reg_success_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
+ ['key' => 'email_reg_success_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
+ ['key' => 'email_reg_success_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'email_reg_success_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'password_reset', 'title' => '密码重置', 'fields' => [
+ ['key' => 'email_password_reset_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
+ ['key' => 'email_password_reset_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
+ ['key' => 'email_password_reset_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'email_password_reset_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'change_code', 'title' => '邮箱变更验证码', 'fields' => [
+ ['key' => 'email_change_code_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
+ ['key' => 'email_change_code_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
+ ['key' => 'email_change_code_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'email_change_code_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'change_notice', 'title' => '邮箱变更通知', 'fields' => [
+ ['key' => 'email_change_notice_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
+ ['key' => 'email_change_notice_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
+ ['key' => 'email_change_notice_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'email_change_notice_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
+ ]],
+ ],
+ ],
+ 'custom-code' => [
+ 'key' => 'custom-code',
+ 'title' => '自定义代码',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'css', 'title' => '自定义 CSS', 'fields' => [
+ ['key' => 'custom_css', 'label' => '自定义 CSS', 'type' => 'code', 'language' => 'css', 'default' => ''],
+ ]],
+ ['key' => 'code', 'title' => '头部 & 底部代码', 'fields' => [
+ ['key' => 'head_code', 'label' => '头部 HTML 代码', 'type' => 'code', 'default' => '', 'help' => '在 中输出,支持 meta/link/style/script 等任意 HTML'],
+ ['key' => 'foot_code', 'label' => '底部 HTML 代码', 'type' => 'code', 'default' => '', 'help' => '在 前输出,支持任意 HTML'],
+ ]],
+ ['key' => 'stats', 'title' => '统计代码', 'fields' => [
+ ['key' => 'trackcode', 'label' => '统计代码', 'type' => 'code', 'default' => '', 'help' => '百度统计/GA/CNZZ/51la 等,自动在 head 中输出'],
+ ]],
+ ],
+ ],
+ 'i18n' => [
+ 'key' => 'i18n',
+ 'title' => '多语言翻译',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '基础配置', 'fields' => [
+ ['key' => 'i18n_enabled', 'label' => '启用多语言', 'type' => 'switch', 'default' => false],
+ ['key' => 'i18n_default_lang', 'label' => '默认语言', 'type' => 'select', 'default' => 'zh-CN', 'options' => [
+ ['label' => '简体中文', 'value' => 'zh-CN'],
+ ['label' => 'English', 'value' => 'en'],
+ ['label' => '繁體中文', 'value' => 'zh-TW'],
+ ['label' => '日本語', 'value' => 'ja'],
+ ['label' => '한국어', 'value' => 'ko'],
+ ['label' => 'Français', 'value' => 'fr'],
+ ['label' => 'Deutsch', 'value' => 'de'],
+ ['label' => 'Español', 'value' => 'es'],
+ ['label' => 'Русский', 'value' => 'ru'],
+ ]],
+ ['key' => 'i18n_auto_detect', 'label' => '自动检测浏览器语言', 'type' => 'switch', 'default' => true, 'help' => '基于浏览器 Accept-Language 自动切换(AKSRT 方案,区别于 zibll IP 定位方案)'],
+ ]],
+ ['key' => 'languages', 'title' => '语言管理', 'fields' => [
+ ['key' => 'i18n_available_langs', 'label' => '可用语言列表', 'type' => 'textarea', 'default' => "zh-CN|简体中文\nen|English\nzh-TW|繁體中文\nja|日本語\nko|한국어\nfr|Français\nde|Deutsch\nes|Español\nru|Русский", 'help' => '每行一个语言,格式:代码|名称(9 种内置语言)'],
+ ['key' => 'i18n_nomenclature', 'label' => '自定义术语', 'type' => 'code', 'default' => '', 'help' => 'JSON 格式自定义翻译术语(key => 翻译),对齐 zibll nomenclature'],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'i18n_switcher_position', 'label' => '语言切换器位置', 'type' => 'radio', 'default' => 'footer', 'options' => [
+ ['label' => '导航栏', 'value' => 'header'],
+ ['label' => '页脚', 'value' => 'footer'],
+ ['label' => '导航栏+页脚', 'value' => 'both'],
+ ['label' => '隐藏', 'value' => 'none'],
+ ]],
+ ['key' => 'i18n_url_strategy', 'label' => 'URL 策略', 'type' => 'radio', 'default' => 'query', 'options' => [
+ ['label' => '查询参数', 'value' => 'query'],
+ ['label' => '路径前缀', 'value' => 'path'],
+ ['label' => '子域名', 'value' => 'subdomain'],
+ ]],
+ ]],
+ ],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-page-display.php b/inc/classes/admin/schema/class-aksrt-admin-schema-page-display.php
new file mode 100644
index 00000000..7204a61c
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-page-display.php
@@ -0,0 +1,530 @@
+ 'page_display',
+ 'title' => '页面&显示',
+ 'sections' => [
+ 'header-nav' => [
+ 'key' => 'header-nav',
+ 'title' => '顶部导航栏',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'layout', 'title' => '导航栏布局', 'fields' => [
+ ['key' => 'header_fixed', 'label' => '固定导航栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'header_nav_style', 'label' => '导航栏风格', 'type' => 'radio', 'default' => 'default', 'options' => [
+ ['label' => '默认', 'value' => 'default'],
+ ['label' => '透明', 'value' => 'transparent'],
+ ['label' => '毛玻璃', 'value' => 'blur'],
+ ]],
+ ['key' => 'header_max_width', 'label' => '导航栏最大宽度', 'type' => 'number', 'default' => 1200, 'min' => 0, 'max' => 1600],
+ ['key' => 'header_scroll_down_hide', 'label' => '下滑隐藏导航栏', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'components', 'title' => '导航组件', 'fields' => [
+ ['key' => 'header_search_enabled', 'label' => '搜索入口', 'type' => 'switch', 'default' => true],
+ ['key' => 'header_user_menu', 'label' => '用户菜单', 'type' => 'switch', 'default' => true],
+ ['key' => 'header_publish_btn', 'label' => '发布按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'header_mobile_drawer', 'label' => '移动端抽屉导航', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'newposts', 'title' => '新文章提示', 'fields' => [
+ ['key' => 'header_newposts_enabled', 'label' => '显示新文章提示', 'type' => 'switch', 'default' => true],
+ ['key' => 'header_newposts_text', 'label' => '提示文案', 'type' => 'input', 'default' => '有新文章'],
+ ['key' => 'header_newposts_radius', 'label' => '圆角', 'type' => 'number', 'default' => 12, 'min' => 0, 'max' => 30],
+ ['key' => 'header_newposts_class', 'label' => '样式类', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'color', 'title' => '颜色设置', 'fields' => [
+ ['key' => 'header_theme_custom', 'label' => '自定义导航栏颜色', 'type' => 'switch', 'default' => false],
+ ['key' => 'header_theme_bg_custom', 'label' => '自定义背景色', 'type' => 'color', 'default' => ''],
+ ['key' => 'header_theme_bg', 'label' => '背景颜色(备选)', 'type' => 'color', 'default' => ''],
+ ['key' => 'header_theme_color_custom', 'label' => '自定义文字颜色', 'type' => 'color', 'default' => ''],
+ ]],
+ ['key' => 'layout-type', 'title' => '布局类型', 'fields' => [
+ ['key' => 'header_layout', 'label' => '桌面端布局', 'type' => 'radio', 'default' => '2', 'options' => [
+ ['label' => '布局 1(经典)', 'value' => '1'],
+ ['label' => '布局 2(内联搜索)', 'value' => '2'],
+ ['label' => '布局 3(紧凑)', 'value' => '3'],
+ ]],
+ ['key' => 'mobile_header_layout', 'label' => '移动端布局', 'type' => 'radio', 'default' => 'center', 'options' => [
+ ['label' => '居中(含搜索按钮)', 'value' => 'center'],
+ ['label' => '靠左', 'value' => 'left'],
+ ]],
+ ['key' => 'mobile_navbar_align', 'label' => '移动端菜单方向', 'type' => 'radio', 'default' => 'left', 'options' => [
+ ['label' => '顶部(关闭手势)', 'value' => 'top'],
+ ['label' => '左侧滑出', 'value' => 'left'],
+ ['label' => '右侧滑出', 'value' => 'right'],
+ ]],
+ ]],
+ ],
+ ],
+ 'header-components' => [
+ 'key' => 'header-components',
+ 'title' => '顶部多功能组件',
+ 'version' => '1.1.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'visibility', 'title' => '显示规则', 'fields' => [
+ ['key' => 'header_components_search_enabled', 'label' => '启用搜索覆盖层', 'type' => 'switch', 'default' => true],
+ ['key' => 'header_components_cards_enabled', 'label' => '启用图标卡片', 'type' => 'switch', 'default' => false],
+ ['key' => 'header_components_show_on', 'label' => '按页面类型启用', 'type' => 'checkbox', 'default' => ['home'], 'options' => [
+ ['label' => '首页', 'value' => 'home'],
+ ['label' => '文章', 'value' => 'single'],
+ ['label' => '页面', 'value' => 'page'],
+ ['label' => '分类', 'value' => 'category'],
+ ['label' => '标签', 'value' => 'tag'],
+ ['label' => '搜索', 'value' => 'search'],
+ ['label' => '归档', 'value' => 'archive'],
+ ['label' => '专题', 'value' => 'special'],
+ ]],
+ ['key' => 'header_components_device_visibility', 'label' => '设备显示规则', 'type' => 'radio', 'default' => '', 'options' => [
+ ['label' => '全部', 'value' => ''],
+ ['label' => '仅电脑', 'value' => 'only_pc'],
+ ['label' => '仅移动', 'value' => 'only_sm'],
+ ]],
+ ]],
+ ['key' => 'nav-color', 'title' => '导航栏配色', 'fields' => [
+ ['key' => 'header_components_nav_color', 'label' => '导航栏文字颜色', 'type' => 'color', 'default' => '#ffffff'],
+ ]],
+ ['key' => 'slider-config', 'title' => '幻灯片配置', 'fields' => [
+ ['key' => 'slider_config_direction', 'label' => '滑动方向', 'type' => 'radio', 'default' => 'horizontal', 'options' => [
+ ['label' => '水平', 'value' => 'horizontal'],
+ ['label' => '垂直', 'value' => 'vertical'],
+ ]],
+ ['key' => 'slider_config_speed', 'label' => '切换速度(ms)', 'type' => 'number', 'default' => 300, 'min' => 100, 'max' => 3000],
+ ['key' => 'slider_config_button', 'label' => '显示前进后退按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'slider_config_scale_height', 'label' => '缩放高度', 'type' => 'switch', 'default' => false],
+ ['key' => 'slider_config_scale', 'label' => '缩放效果', 'type' => 'switch', 'default' => false],
+ ['key' => 'slider_config_max_height', 'label' => '最大高度', 'type' => 'input', 'default' => ''],
+ ['key' => 'slider_config_min_height', 'label' => '最小高度', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'search', 'title' => '搜索组件', 'fields' => [
+ ['key' => 'header_components_search_pc', 'label' => 'PC 端显示搜索', 'type' => 'switch', 'default' => true],
+ ['key' => 'header_components_search_mobile', 'label' => '移动端显示搜索', 'type' => 'switch', 'default' => true],
+ ['key' => 'header_components_search_show_keywords', 'label' => '显示热门搜索词', 'type' => 'switch', 'default' => false],
+ ['key' => 'header_components_search_popular_limit', 'label' => '热词数量', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 20],
+ ['key' => 'header_components_search_show_input_cat', 'label' => '显示分类搜索', 'type' => 'switch', 'default' => false],
+ ['key' => 'header_components_search_show_type', 'label' => '显示类型切换', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'cards', 'title' => '图标卡片样式', 'fields' => [
+ ['key' => 'header_components_filter_blur_devices', 'label' => '容器模糊设备', 'type' => 'checkbox', 'default' => [], 'options' => [
+ ['label' => 'PC', 'value' => 'pc'],
+ ['label' => '移动端', 'value' => 'mobile'],
+ ]],
+ ['key' => 'header_components_card_show_devices', 'label' => '卡片显示设备', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'options' => [
+ ['label' => 'PC', 'value' => 'pc'],
+ ['label' => '移动端', 'value' => 'mobile'],
+ ]],
+ ['key' => 'header_components_card_shared_bg', 'label' => '共享卡片背景', 'type' => 'switch', 'default' => false],
+ ['key' => 'header_components_card_blur', 'label' => '启用卡片模糊效果', 'type' => 'switch', 'default' => true],
+ ['key' => 'header_components_card_icon_shape', 'label' => '图标形状', 'type' => 'radio', 'default' => 'circle', 'options' => [
+ ['label' => '圆形', 'value' => 'circle'],
+ ['label' => '方形', 'value' => 'square'],
+ ]],
+ ]],
+ ['key' => 'icon-cards', 'title' => '图标卡片列表', 'fields' => [
+ ['key' => 'header_components_icon_cards', 'label' => '图标卡片', 'type' => 'repeater', 'sanitize_type' => 'array', 'default' => []],
+ ]],
+ ],
+ ],
+ 'category' => [
+ 'key' => 'category',
+ 'title' => '分类页面',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'header', 'title' => '分类头图', 'fields' => [
+ ['key' => 'category_header_style', 'label' => '头部风格', 'type' => 'radio', 'default' => 'cover', 'options' => [
+ ['label' => '封面图', 'value' => 'cover'],
+ ['label' => '简洁标题', 'value' => 'simple'],
+ ['label' => '隐藏', 'value' => 'none'],
+ ]],
+ ['key' => 'category_cover_enabled', 'label' => '显示封面图', 'type' => 'switch', 'default' => true],
+ ['key' => 'category_desc_enabled', 'label' => '显示分类简介', 'type' => 'switch', 'default' => true],
+ ['key' => 'category_default_cover', 'label' => '默认封面图', 'type' => 'media', 'default' => ''],
+ ]],
+ ['key' => 'list', 'title' => '列表设置', 'fields' => [
+ ['key' => 'category_list_mode', 'label' => '列表模式', 'type' => 'radio', 'default' => 'grid', 'options' => [
+ ['label' => '网格', 'value' => 'grid'],
+ ['label' => '列表', 'value' => 'list'],
+ ['label' => '紧凑', 'value' => 'compact'],
+ ]],
+ ['key' => 'category_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
+ ['key' => 'category_filter_bar', 'label' => '筛选条', 'type' => 'switch', 'default' => true],
+ ['key' => 'category_sidebar', 'label' => '侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'category_post_count', 'label' => '显示文章数', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'orderby', 'title' => '排序设置', 'fields' => [
+ ['key' => 'category_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'modified', 'options' => [
+ ['label' => '最近更新', 'value' => 'modified'],
+ ['label' => '发布时间', 'value' => 'date'],
+ ['label' => '浏览数', 'value' => 'views'],
+ ['label' => '评论数', 'value' => 'comments'],
+ ['label' => '点赞数', 'value' => 'likes'],
+ ]],
+ ]],
+ ],
+ ],
+ 'tag' => [
+ 'key' => 'tag',
+ 'title' => '标签页面',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'header', 'title' => '标签头图', 'fields' => [
+ ['key' => 'tag_header_style', 'label' => '头部风格', 'type' => 'radio', 'default' => 'simple', 'options' => [
+ ['label' => '简洁标题', 'value' => 'simple'],
+ ['label' => '封面图', 'value' => 'cover'],
+ ['label' => '隐藏', 'value' => 'none'],
+ ]],
+ ['key' => 'tag_default_cover', 'label' => '默认封面图', 'type' => 'media', 'default' => ''],
+ ]],
+ ['key' => 'list', 'title' => '列表设置', 'fields' => [
+ ['key' => 'tag_list_mode', 'label' => '列表模式', 'type' => 'radio', 'default' => 'grid', 'options' => [
+ ['label' => '网格', 'value' => 'grid'],
+ ['label' => '列表', 'value' => 'list'],
+ ]],
+ ['key' => 'tag_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
+ ['key' => 'tag_sidebar', 'label' => '侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'tag_post_count', 'label' => '显示文章数', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'orderby', 'title' => '排序设置', 'fields' => [
+ ['key' => 'tag_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'count', 'options' => [
+ ['label' => '文章数', 'value' => 'count'],
+ ['label' => '名称', 'value' => 'name'],
+ ['label' => '最近更新', 'value' => 'modified'],
+ ]],
+ ]],
+ ],
+ ],
+ 'user-home' => [
+ 'key' => 'user-home',
+ 'title' => '用户主页',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'layout', 'title' => '主页布局', 'fields' => [
+ ['key' => 'user_home_cover', 'label' => '封面区域', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_home_sidebar', 'label' => '侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_home_follow', 'label' => '关注按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_home_posts_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 10, 'min' => 5, 'max' => 50],
+ ['key' => 'user_home_paging_ajax', 'label' => 'AJAX 翻页', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'tabs', 'title' => 'Tab 配置', 'fields' => [
+ ['key' => 'user_home_tabs', 'label' => '显示的 Tab', 'type' => 'input', 'default' => 'posts,comments,favorites,credits,product,plate,forum,follow'],
+ ['key' => 'user_home_tabs_order', 'label' => 'Tab 显示顺序', 'type' => 'input', 'default' => 'posts,comments,favorites,credits,product,plate,forum,follow'],
+ ['key' => 'user_home_tabs_pc_hide', 'label' => 'PC 端隐藏', 'type' => 'input', 'default' => ''],
+ ['key' => 'user_home_tabs_mobile_hide', 'label' => '移动端隐藏', 'type' => 'input', 'default' => ''],
+ ['key' => 'author_main_tab_swiper', 'label' => '移动端滑动切换', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'user_home_avatar', 'label' => '显示头像', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_home_name', 'label' => '显示用户名', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_home_desc', 'label' => '显示个人简介', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_home_stats', 'label' => '显示统计数据', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_home_social', 'label' => '显示社交链接', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_home_message', 'label' => '私信按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_home_private', 'label' => '留言按钮', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'other' => [
+ 'key' => 'other',
+ 'title' => '其它页面',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'page-header', 'title' => '统一页头', 'fields' => [
+ ['key' => 'page_header_style', 'label' => '页面标题样式', 'type' => 'select', 'default' => '1', 'options' => [
+ ['label' => '不显示', 'value' => ''],
+ ['label' => '简单', 'value' => '1'],
+ ['label' => '卡片', 'value' => '2'],
+ ['label' => '封面', 'value' => '3'],
+ ]],
+ ['key' => 'page_header_cover_img', 'label' => '默认封面图', 'type' => 'media', 'default' => ''],
+ ]],
+ ['key' => 'page_404', 'title' => '404 页面', 'fields' => [
+ ['key' => 'page_404_message', 'label' => '404 提示文字', 'type' => 'input', 'default' => '页面未找到'],
+ ['key' => 'page_404_search', 'label' => '显示搜索框', 'type' => 'switch', 'default' => true],
+ ['key' => 'page_404_popular', 'label' => '显示热门文章', 'type' => 'switch', 'default' => true],
+ ['key' => 'page_404_back', 'label' => '显示返回首页', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'search_results', 'title' => '搜索结果页', 'fields' => [
+ ['key' => 'page_search_sidebar', 'label' => '搜索结果侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'page_search_per_page', 'label' => '每页结果数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
+ ]],
+ ['key' => 'author', 'title' => '作者页面', 'fields' => [
+ ['key' => 'page_author_cover', 'label' => '显示封面', 'type' => 'switch', 'default' => true],
+ ['key' => 'page_author_sidebar', 'label' => '显示侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'page_author_posts_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
+ ]],
+ ],
+ ],
+ 'special' => [
+ 'key' => 'special',
+ 'title' => '专题页面',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'header', 'title' => '专题头部', 'fields' => [
+ ['key' => 'special_cover_enabled', 'label' => '显示专题封面', 'type' => 'switch', 'default' => true],
+ ['key' => 'special_summary_enabled', 'label' => '显示专题摘要', 'type' => 'switch', 'default' => true],
+ ['key' => 'special_summary_length', 'label' => '摘要最大长度', 'type' => 'number', 'default' => 200, 'min' => 50, 'max' => 500],
+ ['key' => 'special_default_cover', 'label' => '默认封面图', 'type' => 'media', 'default' => ''],
+ ]],
+ ['key' => 'layout', 'title' => '布局设置', 'fields' => [
+ ['key' => 'special_layout', 'label' => '内容布局', 'type' => 'radio', 'default' => 'grid', 'options' => [
+ ['label' => '网格', 'value' => 'grid'],
+ ['label' => '列表', 'value' => 'list'],
+ ['label' => '瀑布流', 'value' => 'masonry'],
+ ]],
+ ['key' => 'special_columns', 'label' => '网格列数', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 4],
+ ['key' => 'special_sidebar', 'label' => '侧边栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'special_post_count', 'label' => '显示文章数', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'aggregation', 'title' => '聚合规则', 'fields' => [
+ ['key' => 'special_source_type', 'label' => '内容来源', 'type' => 'radio', 'default' => 'category', 'options' => [
+ ['label' => '分类', 'value' => 'category'],
+ ['label' => '标签', 'value' => 'tag'],
+ ['label' => '手动选择', 'value' => 'manual'],
+ ['label' => '最新文章', 'value' => 'latest'],
+ ]],
+ ['key' => 'special_source_ids', 'label' => '来源 ID 列表', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'subpage', 'title' => '子页面展示', 'fields' => [
+ ['key' => 'special_subpage_display', 'label' => '子页面样式', 'type' => 'radio', 'default' => 'cards', 'options' => [
+ ['label' => '卡片', 'value' => 'cards'],
+ ['label' => '列表', 'value' => 'list'],
+ ['label' => '时间线', 'value' => 'timeline'],
+ ]],
+ ['key' => 'special_subpage_count', 'label' => '每页子页面数', 'type' => 'number', 'default' => 6, 'min' => 1, 'max' => 24],
+ ]],
+ ['key' => 'pagination', 'title' => '分页设置', 'fields' => [
+ ['key' => 'special_pagination', 'label' => '分页模式', 'type' => 'radio', 'default' => 'numeric', 'options' => [
+ ['label' => '数字分页', 'value' => 'numeric'],
+ ['label' => '加载更多', 'value' => 'load_more'],
+ ['label' => '无限滚动', 'value' => 'infinite'],
+ ]],
+ ['key' => 'special_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
+ ]],
+ ['key' => 'orderby', 'title' => '排序设置', 'fields' => [
+ ['key' => 'special_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'date', 'options' => [
+ ['label' => '发布时间', 'value' => 'date'],
+ ['label' => '最近更新', 'value' => 'modified'],
+ ['label' => '浏览数', 'value' => 'views'],
+ ['label' => '评论数', 'value' => 'comments'],
+ ]],
+ ]],
+ ],
+ ],
+ 'background' => [
+ 'key' => 'background',
+ 'title' => '全局背景',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'light', 'title' => '亮色背景', 'fields' => [
+ ['key' => 'bg_color', 'label' => '背景颜色', 'type' => 'color', 'default' => ''],
+ ['key' => 'bg_image', 'label' => '背景图片', 'type' => 'media', 'default' => ''],
+ ['key' => 'bg_size', 'label' => '图片尺寸', 'type' => 'select', 'default' => 'cover', 'options' => [
+ ['label' => '覆盖', 'value' => 'cover'],
+ ['label' => '包含', 'value' => 'contain'],
+ ['label' => '原始大小', 'value' => 'auto'],
+ ['label' => '拉伸填充', 'value' => '100% 100%'],
+ ['label' => '100% 宽度(旧版兼容)', 'value' => '100%'],
+ ]],
+ ['key' => 'bg_position', 'label' => '图片位置', 'type' => 'select', 'default' => 'center center', 'options' => [
+ ['label' => '居中', 'value' => 'center center'],
+ ['label' => '顶部居中', 'value' => 'top center'],
+ ['label' => '底部居中', 'value' => 'bottom center'],
+ ]],
+ ['key' => 'bg_repeat', 'label' => '图片重复', 'type' => 'select', 'default' => 'no-repeat', 'options' => [
+ ['label' => '不重复', 'value' => 'no-repeat'],
+ ['label' => '平铺', 'value' => 'repeat'],
+ ['label' => '水平平铺', 'value' => 'repeat-x'],
+ ['label' => '垂直平铺', 'value' => 'repeat-y'],
+ ]],
+ ['key' => 'bg_attachment', 'label' => '固定方式', 'type' => 'select', 'default' => 'fixed', 'options' => [
+ ['label' => '固定', 'value' => 'fixed'],
+ ['label' => '随内容滚动', 'value' => 'scroll'],
+ ]],
+ ]],
+ ['key' => 'dark', 'title' => '暗色背景', 'fields' => [
+ ['key' => 'bg_dark_color', 'label' => '暗色背景颜色', 'type' => 'color', 'default' => ''],
+ ['key' => 'bg_dark_image', 'label' => '暗色背景图片', 'type' => 'media', 'default' => ''],
+ ]],
+ ['key' => 'overlay', 'title' => '遮罩设置', 'fields' => [
+ ['key' => 'bg_overlay', 'label' => '启用背景遮罩', 'type' => 'switch', 'default' => false],
+ ['key' => 'bg_overlay_color', 'label' => '遮罩颜色', 'type' => 'input', 'default' => 'rgba(0, 0, 0, 0.3)'],
+ ]],
+ ['key' => 'page', 'title' => '页面背景', 'fields' => [
+ ['key' => 'page_bg_color', 'label' => '内容区背景色', 'type' => 'color', 'default' => ''],
+ ['key' => 'page_bg_image', 'label' => '内容区背景图', 'type' => 'media', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'menu-decoration' => [
+ 'key' => 'menu-decoration',
+ 'title' => '菜单装饰',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'icon', 'title' => '菜单图标', 'fields' => [
+ ['key' => 'menu_icon_enabled', 'label' => '显示菜单图标', 'type' => 'switch', 'default' => true],
+ ['key' => 'menu_badge_enabled', 'label' => '显示菜单徽章', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'submenu', 'title' => '下拉子菜单', 'fields' => [
+ ['key' => 'menu_submenu_style', 'label' => '子菜单样式', 'type' => 'radio', 'default' => 'dropdown', 'options' => [
+ ['label' => '普通下拉', 'value' => 'dropdown'],
+ ['label' => '多列下拉', 'value' => 'mega'],
+ ['label' => '图文卡片', 'value' => 'cards'],
+ ]],
+ ['key' => 'menu_submenu_columns', 'label' => '多列列数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 5],
+ ['key' => 'menu_submenu_width', 'label' => '子菜单宽度 (px)', 'type' => 'number', 'default' => 220, 'min' => 160, 'max' => 800],
+ ]],
+ ['key' => 'effect', 'title' => '交互效果', 'fields' => [
+ ['key' => 'menu_hover_effect', 'label' => 'Hover 效果', 'type' => 'radio', 'default' => 'underline', 'options' => [
+ ['label' => '下划线', 'value' => 'underline'],
+ ['label' => '背景色', 'value' => 'background'],
+ ['label' => '文字变色', 'value' => 'color'],
+ ['label' => '无效果', 'value' => 'none'],
+ ]],
+ ['key' => 'menu_item_padding', 'label' => '菜单项内边距 (px)', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 32],
+ ]],
+ ['key' => 'mobile', 'title' => '移动端菜单', 'fields' => [
+ ['key' => 'menu_mobile_style', 'label' => '移动端菜单样式', 'type' => 'radio', 'default' => 'drawer', 'options' => [
+ ['label' => '抽屉', 'value' => 'drawer'],
+ ['label' => '全屏', 'value' => 'fullscreen'],
+ ['label' => '下拉', 'value' => 'dropdown'],
+ ]],
+ ]],
+ ],
+ ],
+ 'footer' => [
+ 'key' => 'footer',
+ 'title' => '底部页脚',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'layout', 'title' => '页脚布局', 'fields' => [
+ ['key' => 'footer_columns', 'label' => '页脚列数', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 4],
+ ['key' => 'footer_copyright', 'label' => '显示版权信息', 'type' => 'switch', 'default' => true],
+ ['key' => 'footer_icp', 'label' => '显示ICP备案', 'type' => 'switch', 'default' => true],
+ ['key' => 'icp_number', 'label' => 'ICP备案号', 'type' => 'input', 'default' => '', 'help' => '例如:ICP 备 xxxxxxxx 号'],
+ ['key' => 'footer_police', 'label' => '显示公安备案', 'type' => 'switch', 'default' => true],
+ ['key' => 'police_number', 'label' => '公安备案号', 'type' => 'input', 'default' => '', 'help' => '例如:公网安备 xxxxxx 号'],
+ ]],
+ ['key' => 'color', 'title' => '页脚颜色', 'fields' => [
+ ['key' => 'footer_theme_custom', 'label' => '自定义页脚颜色', 'type' => 'switch', 'default' => false],
+ ['key' => 'footer_theme_bg_custom', 'label' => '自定义背景色', 'type' => 'color', 'default' => ''],
+ ['key' => 'footer_theme_bg', 'label' => '背景图片', 'type' => 'media', 'default' => ''],
+ ['key' => 'footer_theme_color_custom', 'label' => '自定义文字颜色', 'type' => 'color', 'default' => ''],
+ ]],
+ ['key' => 'contact', 'title' => '联系方式', 'fields' => [
+ ['key' => 'footer_contact_wechat_img', 'label' => '微信二维码', 'type' => 'media', 'default' => ''],
+ ['key' => 'footer_contact_qq', 'label' => 'QQ', 'type' => 'input', 'default' => ''],
+ ['key' => 'footer_contact_weibo', 'label' => '微博', 'type' => 'input', 'default' => ''],
+ ['key' => 'footer_contact_email', 'label' => '邮箱', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'brand', 'title' => '品牌列(第一列)', 'fields' => [
+ ['key' => 'footer_t1_m_s', 'label' => '显示设备', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'options' => [
+ ['label' => 'PC', 'value' => 'pc'],
+ ['label' => '移动端', 'value' => 'mobile'],
+ ]],
+ ['key' => 'footer_t1_img', 'label' => '页脚 Logo(日间)', 'type' => 'media', 'default' => ''],
+ ['key' => 'footer_t1_img_dark', 'label' => '页脚 Logo(夜间)', 'type' => 'media', 'default' => ''],
+ ['key' => 'footer_t1_t', 'label' => '品牌标题', 'type' => 'input', 'default' => ''],
+ ['key' => 'fcode_t1_code', 'label' => '品牌列自定义代码', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'contact_ext', 'title' => '联系方式扩展', 'fields' => [
+ ['key' => 'footer_contact_m_s', 'label' => '显示设备', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'options' => [
+ ['label' => 'PC', 'value' => 'pc'],
+ ['label' => '移动端', 'value' => 'mobile'],
+ ]],
+ ['key' => 'footer_contact_more', 'label' => '更多联系项', 'type' => 'textarea', 'default' => '', 'help' => 'JSON 数组,每项含 desc/icon/link/img'],
+ ['key' => 'fcode_t2_code_1', 'label' => '联系区上方自定义代码', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'fcode_t2_code_2', 'label' => '联系区下方自定义代码(备案等)', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'mini_qr', 'title' => '二维码图片组(第三列)', 'fields' => [
+ ['key' => 'footer_mini_img_m_s', 'label' => '显示设备', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'options' => [
+ ['label' => 'PC', 'value' => 'pc'],
+ ['label' => '移动端', 'value' => 'mobile'],
+ ]],
+ ['key' => 'footer_mini_img', 'label' => '二维码组', 'type' => 'textarea', 'default' => '', 'help' => 'JSON 数组,每项含 text/img(最多4个)'],
+ ]],
+ ['key' => 'custom', 'title' => '底部自定义', 'fields' => [
+ ['key' => 'fcode_customize_code', 'label' => '底部自定义代码', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'tabbar' => [
+ 'key' => 'tabbar',
+ 'title' => '底部Tab栏',
+ 'version' => '1.1.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => 'Tab栏设置', 'fields' => [
+ ['key' => 'footer_tabbar_s', 'label' => '启用底部Tab栏', 'type' => 'switch', 'default' => true],
+ ['key' => 'footer_tabbar_scroll_hide', 'label' => '滚动时隐藏', 'type' => 'switch', 'default' => false],
+ ['key' => 'footer_tabbar_scroll_down_hide', 'label' => '下滑时隐藏', 'type' => 'switch', 'default' => true],
+ ['key' => 'footer_tabbar_max_items', 'label' => '最大Tab数', 'type' => 'number', 'default' => 5, 'min' => 3, 'max' => 7],
+ ['key' => 'footer_tabbar_icon_size', 'label' => '图标大小', 'type' => 'number', 'default' => 22, 'min' => 16, 'max' => 32],
+ ['key' => 'footer_tabbar_text_size', 'label' => '文字大小', 'type' => 'number', 'default' => 10, 'min' => 8, 'max' => 14],
+ ['key' => 'footer_tabbar_show_text', 'label' => '显示文字', 'type' => 'switch', 'default' => true],
+ ['key' => 'footer_tabbar_active_color', 'label' => '选中颜色', 'type' => 'color', 'default' => '#f04494'],
+ ['key' => 'footer_tabbar_bg_color', 'label' => '背景颜色', 'type' => 'color', 'default' => ''],
+ ['key' => 'footer_tabbar_border', 'label' => '显示上边框', 'type' => 'switch', 'default' => true],
+ ['key' => 'footer_tabbar_shadow', 'label' => '显示阴影', 'type' => 'switch', 'default' => true],
+ ['key' => 'footer_tabbar_animation', 'label' => '切换动画', 'type' => 'switch', 'default' => true],
+ ['key' => 'footer_tabbar_double_click_home', 'label' => '双击首页刷新', 'type' => 'switch', 'default' => true],
+ ['key' => 'footer_tabbar_badge_dot', 'label' => '徽标圆点样式', 'type' => 'switch', 'default' => false],
+ ['key' => 'footer_tabbar_single', 'label' => '文章页专属Tab', 'type' => 'select', 'default' => 'default', 'options' => [
+ ['label' => '默认按钮', 'value' => 'default'],
+ ['label' => '文章扩展按钮(评论/收藏/分享)', 'value' => 'extend'],
+ ], 'help' => '文章详情页底部显示的Tab按钮类型'],
+ ]],
+ ],
+ ],
+ 'float-btn' => [
+ 'key' => 'float-btn',
+ 'title' => '悬浮按钮',
+ 'version' => '1.1.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '悬浮按钮设置', 'fields' => [
+ ['key' => 'float_btn_style', 'label' => '按钮风格', 'type' => 'radio', 'default' => 'round', 'options' => [
+ ['label' => '圆形', 'value' => 'round'],
+ ['label' => '方形', 'value' => 'square'],
+ ]],
+ ['key' => 'float_btn_position', 'label' => '按钮位置', 'type' => 'radio', 'default' => 'bottom', 'options' => [
+ ['label' => '底部', 'value' => 'bottom'],
+ ['label' => '居中', 'value' => 'center'],
+ ]],
+ ['key' => 'float_btn_scroll_hide', 'label' => '滚动时隐藏', 'type' => 'switch', 'default' => false],
+ ['key' => 'float_btn_scroll_down_hide', 'label' => '下滑时隐藏', 'type' => 'switch', 'default' => true],
+ ['key' => 'float_btn_max', 'label' => '最大显示数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 10],
+ ['key' => 'float_btn_size', 'label' => '按钮尺寸', 'type' => 'number', 'default' => 44, 'min' => 32, 'max' => 64],
+ ['key' => 'float_btn_spacing', 'label' => '按钮间距', 'type' => 'number', 'default' => 10, 'min' => 4, 'max' => 24],
+ ['key' => 'float_btn_shadow', 'label' => '显示阴影', 'type' => 'switch', 'default' => true],
+ ['key' => 'float_btn_animation', 'label' => '悬停动画', 'type' => 'switch', 'default' => true],
+ ['key' => 'float_btn_opacity', 'label' => '按钮透明度', 'type' => 'number', 'default' => 100, 'min' => 30, 'max' => 100],
+ ['key' => 'float_btn_active_color', 'label' => '激活颜色', 'type' => 'color', 'default' => ''],
+ ['key' => 'float_btn_bg_color', 'label' => '背景颜色', 'type' => 'color', 'default' => ''],
+ ['key' => 'float_btn_pc_s', 'label' => 'PC端显示', 'type' => 'switch', 'default' => true],
+ ['key' => 'float_btn_m_s', 'label' => '移动端显示', 'type' => 'switch', 'default' => true],
+ ['key' => 'float_btn_filter_css', 'label' => '过滤CSS', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php b/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php
new file mode 100644
index 00000000..462a5bbd
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php
@@ -0,0 +1,374 @@
+ 'payment',
+ 'title' => '支付&付费',
+ 'sections' => [
+ 'paid' => [
+ 'key' => 'paid',
+ 'title' => '下载/付费内容中心',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'strategy', 'title' => '付费内容策略', 'fields' => [
+ ['key' => 'payment_paid_post_types', 'label' => '受管内容类型', 'type' => 'input', 'default' => 'post'],
+ ['key' => 'payment_vip_free_download', 'label' => '角色白名单免扣下载', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_vip_exempt_roles', 'label' => '免扣角色列表', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'delivery', 'title' => '交付说明', 'fields' => [
+ ['key' => 'payment_paid_catalog_note', 'label' => '前台付费提示', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_manual_delivery_note', 'label' => '人工交付备注', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'payment_paid_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
+ ['key' => 'payment_currency_symbol', 'label' => '货币符号', 'type' => 'input', 'default' => '¥'],
+ ['key' => 'payment_paid_show_user', 'label' => '显示购买用户', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_paid_show_paycount', 'label' => '显示购买次数', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_paid_box_position', 'label' => '付费框位置', 'type' => 'radio', 'default' => 'top', 'options' => [
+ ['label' => '文章模块上', 'value' => 'box_top'],
+ ['label' => '内容顶', 'value' => 'top'],
+ ['label' => '内容底', 'value' => 'bottom'],
+ ['label' => '模块下', 'value' => 'box_bottom'],
+ ]],
+ ]],
+ ['key' => 'download', 'title' => '下载设置', 'fields' => [
+ ['key' => 'payment_download_enable', 'label' => '启用下载功能', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_download_remote_s', 'label' => '远程文件下载', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_download_verify_nonce', 'label' => '下载鉴权', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_download_user_log', 'label' => '用户下载记录条数', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 9999],
+ ['key' => 'payment_download_post_log', 'label' => '文章下载记录条数', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 9999],
+ ['key' => 'payment_download_remote_size', 'label' => '远程文件大小限制(KB)', 'type' => 'number', 'default' => 2048, 'min' => 1, 'max' => 999999],
+ ['key' => 'payment_download_remote_ext', 'label' => '远程文件格式限制', 'type' => 'input', 'default' => 'zip,rar,7z'],
+ ]],
+ ],
+ ],
+ 'recharge' => [
+ 'key' => 'recharge',
+ 'title' => '余额充值',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '充值开关', 'fields' => [
+ ['key' => 'payment_recharge_enabled', 'label' => '启用余额充值', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_recharge_mode', 'label' => '充值模式', 'type' => 'select', 'default' => 'manual_review', 'options' => [
+ ['label' => '人工审核', 'value' => 'manual_review'],
+ ['label' => '自动到账', 'value' => 'auto'],
+ ]],
+ ['key' => 'payment_recharge_desc', 'label' => '充值说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_recharge_auto_to_balance', 'label' => '自动到账到余额', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'packages', 'title' => '充值套餐', 'fields' => [
+ ['key' => 'payment_recharge_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|入门包\n98|108|推荐包"],
+ ['key' => 'payment_recharge_help', 'label' => '帮助文案', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'product', 'title' => '充值产品', 'fields' => [
+ ['key' => 'payment_recharge_product', 'label' => '充值产品', 'type' => 'input', 'default' => ''],
+ ['key' => 'payment_recharge_price', 'label' => '充值价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
+ ['key' => 'payment_recharge_tag', 'label' => '价格标签', 'type' => 'input', 'default' => '元'],
+ ['key' => 'payment_recharge_tag_class', 'label' => '标签样式', 'type' => 'input', 'default' => ''],
+ ['key' => 'payment_recharge_custom_s', 'label' => '自定义金额', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_recharge_custom_limit', 'label' => '自定义金额限制', 'type' => 'number', 'default' => 9999, 'min' => 1, 'max' => 99999],
+ ]],
+ ['key' => 'transfer', 'title' => '余额转账', 'fields' => [
+ ['key' => 'payment_balance_transfer_s', 'label' => '启用余额转账', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_balance_transfer_service_charge', 'label' => '转账手续费(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 50],
+ ['key' => 'payment_balance_transfer_desc', 'label' => '转账说明', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'pass-charge', 'title' => '充值卡免扣', 'fields' => [
+ ['key' => 'payment_recharge_pass_charge_s', 'label' => '启用充值卡免扣', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_recharge_pass_charge_desc', 'label' => '免扣说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_recharge_pass_charge_only_password', 'label' => '仅密码免扣', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'exchange', 'title' => '积分兑换', 'fields' => [
+ ['key' => 'payment_recharge_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_recharge_exchange_rate', 'label' => '兑换比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000],
+ ['key' => 'payment_recharge_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'vip' => [
+ 'key' => 'vip',
+ 'title' => 'VIP会员',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '会员基础', 'fields' => [
+ ['key' => 'payment_vip_enabled', 'label' => '启用会员结构', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_vip_name', 'label' => '会员名称', 'type' => 'input', 'default' => 'VIP会员'],
+ ['key' => 'payment_vip_badge_text', 'label' => '会员徽标文案', 'type' => 'input', 'default' => 'VIP'],
+ ['key' => 'payment_vip_desc', 'label' => '会员说明', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'packages', 'title' => '会员套餐', 'fields' => [
+ ['key' => 'payment_vip_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|月卡\n365|365|年卡"],
+ ['key' => 'payment_vip_help', 'label' => '权益说明', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'benefit', 'title' => '会员权益', 'fields' => [
+ ['key' => 'payment_vip_download_limit', 'label' => '下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999],
+ ['key' => 'payment_vip_download_speed', 'label' => '下载速度(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
+ ['key' => 'payment_vip_discount', 'label' => '折扣(%)', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 100],
+ ['key' => 'payment_vip_pass_charge', 'label' => '积分充值免扣', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_vip_pass_charge_desc', 'label' => '积分充值说明', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'renew', 'title' => '续费设置', 'fields' => [
+ ['key' => 'payment_vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_vip_renew_desc', 'label' => '续费说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'radio', 'default' => 'same', 'options' => [
+ ['label' => '相同价格', 'value' => 'same'],
+ ['label' => '折扣价格', 'value' => 'discount'],
+ ]],
+ ['key' => 'payment_vip_renew_discount', 'label' => '续费折扣(%)', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 100],
+ ['key' => 'payment_vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999],
+ ]],
+ ['key' => 'upgrade', 'title' => '升级设置', 'fields' => [
+ ['key' => 'payment_vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_vip_upgrade_desc', 'label' => '升级说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_vip_upgrade_product', 'label' => '升级产品', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'price', 'title' => '价格设置', 'fields' => [
+ ['key' => 'payment_vip_month_price', 'label' => '月卡价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
+ ['key' => 'payment_vip_year_price', 'label' => '年卡价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
+ ['key' => 'payment_vip_permanent_price', 'label' => '永久价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
+ ['key' => 'payment_vip_unit_price', 'label' => '单位价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
+ ['key' => 'payment_vip_unit_tag', 'label' => '单位标签', 'type' => 'input', 'default' => '元'],
+ ['key' => 'payment_vip_jump_price', 'label' => '跳转价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
+ ]],
+ ],
+ ],
+ 'revenue' => [
+ 'key' => 'revenue',
+ 'title' => '创作分成',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'policy', 'title' => '分成策略', 'fields' => [
+ ['key' => 'payment_revenue_enabled', 'label' => '启用创作分成', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_revenue_share_ratio', 'label' => '作者分成比例(%)', 'type' => 'number', 'default' => 70, 'min' => 1, 'max' => 100],
+ ['key' => 'payment_revenue_ratio_vip1', 'label' => 'VIP1分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
+ ['key' => 'payment_revenue_ratio_vip2', 'label' => 'VIP2分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
+ ['key' => 'payment_revenue_settlement_threshold', 'label' => '最小结算金额', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 99999],
+ ['key' => 'payment_revenue_cycle_days', 'label' => '结算周期(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
+ ['key' => 'payment_revenue_points_rule', 'label' => '积分规则', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_revenue_note', 'label' => '结算备注', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'payment_revenue_desc_details', 'label' => '收益明细文案', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_revenue_desc_page', 'label' => '收益说明页', 'type' => 'input', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'affiliate' => [
+ 'key' => 'affiliate',
+ 'title' => '推广返佣',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'policy', 'title' => '返佣策略', 'fields' => [
+ ['key' => 'payment_affiliate_enabled', 'label' => '启用推广返佣', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_affiliate_ratio', 'label' => '返佣比例(%)', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 100],
+ ['key' => 'payment_affiliate_window_days', 'label' => '归因窗口(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
+ ['key' => 'payment_affiliate_require_paid_order', 'label' => '仅对已支付订单返佣', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_affiliate_self_s', 'label' => '自购返佣', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_affiliate_show_users', 'label' => '显示推广用户', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'user-ratio', 'title' => '用户返佣比例', 'fields' => [
+ ['key' => 'payment_affiliate_user_ratio', 'label' => '普通用户比例(%)', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 100],
+ ['key' => 'payment_affiliate_vip1_ratio', 'label' => 'VIP1比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
+ ['key' => 'payment_affiliate_vip2_ratio', 'label' => 'VIP2比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
+ ]],
+ ['key' => 'poster', 'title' => '推广海报', 'fields' => [
+ ['key' => 'payment_affiliate_poster_s', 'label' => '启用推广海报', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_affiliate_poster_img', 'label' => '海报背景图', 'type' => 'media', 'default' => ''],
+ ['key' => 'payment_affiliate_poster_title', 'label' => '海报标题', 'type' => 'input', 'default' => ''],
+ ['key' => 'payment_affiliate_poster_desc', 'label' => '海报描述', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'text', 'title' => '文案设置', 'fields' => [
+ ['key' => 'payment_affiliate_text_desc', 'label' => '返佣说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_affiliate_text_details_title', 'label' => '明细标题', 'type' => 'input', 'default' => ''],
+ ['key' => 'payment_affiliate_text_details', 'label' => '明细说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_affiliate_text_discount', 'label' => '折扣文案', 'type' => 'input', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'gateway' => [
+ 'key' => 'gateway',
+ 'title' => '收款接口',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'adapter', 'title' => '网关适配器', 'fields' => [
+ ['key' => 'payment_gateway_mode', 'label' => '运行模式', 'type' => 'string', 'default' => 'manual_review'],
+ ['key' => 'payment_gateway_test_enabled', 'label' => '启用测试网关', 'type' => 'bool', 'default' => true],
+ ['key' => 'payment_gateway_manual_review', 'label' => '保留人工复核', 'type' => 'bool', 'default' => true],
+ ['key' => 'payment_gateway_callback_secret', 'label' => '回调校验口令', 'type' => 'string', 'default' => ''],
+ ['key' => 'payment_gateway_help', 'label' => '运维说明', 'type' => 'string', 'default' => '未接真实支付网关时,建议使用测试网关或人工确认模式完成联调。'],
+ ]],
+ ['key' => 'gateways', 'title' => '支付网关', 'fields' => [
+ ['key' => 'payment_wechat_enabled', 'label' => '微信支付', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_alipay_enabled', 'label' => '支付宝', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_paypal_enabled', 'label' => 'PayPal', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_balance_enabled', 'label' => '余额支付', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_card_pass_enabled', 'label' => '卡密充值', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_xunhupay_enabled', 'label' => '虎皮椒', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_xhpay_enabled', 'label' => '小火付', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_payjs_enabled', 'label' => 'PayJS', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_epay_enabled', 'label' => '易支付', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_vmq_enabled', 'label' => 'V免签', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_codepay_enabled', 'label' => 'Codepay', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_stripe_enabled', 'label' => 'Stripe', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'wechat', 'title' => '微信支付配置', 'fields' => [
+ ['key' => 'wechat_merchantid', 'label' => '商户号', 'type' => 'input', 'default' => ''],
+ ['key' => 'wechat_jsapi', 'label' => 'JSAPI密钥', 'type' => 'input', 'default' => ''],
+ ['key' => 'wechat_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
+ ['key' => 'wechat_cert_path', 'label' => '证书路径', 'type' => 'input', 'default' => ''],
+ ['key' => 'wechat_key_path', 'label' => '密钥路径', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'alipay', 'title' => '支付宝配置', 'fields' => [
+ ['key' => 'alipay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'alipay_privatekey', 'label' => '应用私钥', 'type' => 'input', 'default' => ''],
+ ['key' => 'alipay_publickey', 'label' => '支付宝公钥', 'type' => 'input', 'default' => ''],
+ ['key' => 'alipay_return_url', 'label' => '同步回调地址', 'type' => 'input', 'default' => ''],
+ ['key' => 'alipay_notify_url', 'label' => '异步回调地址', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'paypal', 'title' => 'PayPal配置', 'fields' => [
+ ['key' => 'paypal_client_id', 'label' => 'Client ID', 'type' => 'input', 'default' => ''],
+ ['key' => 'paypal_client_secret', 'label' => 'Client Secret', 'type' => 'input', 'default' => ''],
+ ['key' => 'paypal_currency', 'label' => '货币', 'type' => 'input', 'default' => 'USD'],
+ ['key' => 'paypal_sandbox', 'label' => '沙箱模式', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'xunhupay', 'title' => '虎皮椒配置', 'fields' => [
+ ['key' => 'xunhupay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'xunhupay_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'xhpay', 'title' => '小火付配置', 'fields' => [
+ ['key' => 'xhpay_mchid', 'label' => '商户号', 'type' => 'input', 'default' => ''],
+ ['key' => 'xhpay_alipay_v2', 'label' => '支付宝V2', 'type' => 'input', 'default' => ''],
+ ['key' => 'xhpay_wx_h5', 'label' => '微信H5', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'payjs', 'title' => 'PayJS配置', 'fields' => [
+ ['key' => 'payjs_mchid', 'label' => '商户号', 'type' => 'input', 'default' => ''],
+ ['key' => 'payjs_key', 'label' => '通信密钥', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'epay', 'title' => '易支付配置', 'fields' => [
+ ['key' => 'epay_apiurl', 'label' => '接口地址', 'type' => 'input', 'default' => ''],
+ ['key' => 'epay_pid', 'label' => '商户ID', 'type' => 'input', 'default' => ''],
+ ['key' => 'epay_key', 'label' => '商户密钥', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'stripe', 'title' => 'Stripe配置', 'fields' => [
+ ['key' => 'stripe_secret_key', 'label' => 'Secret Key', 'type' => 'input', 'default' => ''],
+ ['key' => 'stripe_publishable_key', 'label' => 'Publishable Key', 'type' => 'input', 'default' => ''],
+ ['key' => 'stripe_webhook_secret', 'label' => 'Webhook Secret', 'type' => 'input', 'default' => ''],
+ ['key' => 'stripe_currency', 'label' => '货币', 'type' => 'input', 'default' => 'usd'],
+ ['key' => 'stripe_test_mode', 'label' => '测试模式', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'order' => [
+ 'key' => 'order',
+ 'title' => '订单设置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '订单基础', 'fields' => [
+ ['key' => 'payment_order_prefix', 'label' => '订单前缀', 'type' => 'input', 'default' => 'AKSRT'],
+ ['key' => 'payment_order_expiry', 'label' => '订单有效期(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
+ ['key' => 'payment_show_user', 'label' => '显示用户信息', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_show_paycount', 'label' => '显示购买次数', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_no_logged_in', 'label' => '未登录提示', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_cookie_day', 'label' => 'Cookie有效期(天)', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 31],
+ ['key' => 'payment_no_logged_remind', 'label' => '未登录提醒文案', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_wait_float_s', 'label' => '订单等待浮动', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_order_max_minutes', 'label' => '订单最大支付时间(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'payment_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
+ ['key' => 'payment_box_position', 'label' => '付费框位置', 'type' => 'radio', 'default' => 'top', 'options' => [
+ ['label' => '文章模块上', 'value' => 'box_top'],
+ ['label' => '内容顶', 'value' => 'top'],
+ ['label' => '内容底', 'value' => 'bottom'],
+ ['label' => '模块下', 'value' => 'box_bottom'],
+ ]],
+ ['key' => 'payment_free_logged_show', 'label' => '登录用户显示免费', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_show_ip', 'label' => '显示用户IP', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_show_device', 'label' => '显示设备信息', 'type' => 'switch', 'default' => false],
+ ['key' => 'payment_show_order_no', 'label' => '显示订单号', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_show_create_time', 'label' => '显示创建时间', 'type' => 'switch', 'default' => true],
+ ['key' => 'payment_show_pay_time', 'label' => '显示支付时间', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'status', 'title' => '状态设置', 'fields' => [
+ ['key' => 'payment_order_status_pending', 'label' => '待支付状态文案', 'type' => 'input', 'default' => '待支付'],
+ ['key' => 'payment_order_status_paid', 'label' => '已支付状态文案', 'type' => 'input', 'default' => '已支付'],
+ ['key' => 'payment_order_status_completed', 'label' => '已完成状态文案', 'type' => 'input', 'default' => '已完成'],
+ ['key' => 'payment_order_status_cancelled', 'label' => '已取消状态文案', 'type' => 'input', 'default' => '已取消'],
+ ['key' => 'payment_order_status_refunded', 'label' => '已退款状态文案', 'type' => 'input', 'default' => '已退款'],
+ ['key' => 'payment_order_status_refunding', 'label' => '退款中状态文案', 'type' => 'input', 'default' => '退款中'],
+ ['key' => 'payment_order_status_processing', 'label' => '处理中状态文案', 'type' => 'input', 'default' => '处理中'],
+ ]],
+ ['key' => 'service', 'title' => '服务设置', 'fields' => [
+ ['key' => 'payment_service', 'label' => '客服设置', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'payment_service_icon', 'label' => '客服图标', 'type' => 'icon', 'default' => ''],
+ ['key' => 'payment_service_value', 'label' => '客服内容', 'type' => 'input', 'default' => ''],
+ ['key' => 'payment_service_qq', 'label' => 'QQ客服', 'type' => 'input', 'default' => ''],
+ ['key' => 'payment_service_wechat', 'label' => '微信客服', 'type' => 'media', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'card-pass' => [
+ 'key' => 'card-pass',
+ 'title' => '充值卡',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '充值卡设置', 'fields' => [
+ ['key' => 'card_pass_enabled', 'label' => '启用充值卡', 'type' => 'switch', 'default' => false],
+ ['key' => 'card_pass_desc', 'label' => '充值卡说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'card_pass_type', 'label' => '充值卡类型', 'type' => 'select', 'default' => 'balance', 'options' => [
+ ['label' => '余额卡', 'value' => 'balance'],
+ ['label' => 'VIP卡', 'value' => 'vip'],
+ ['label' => '积分卡', 'value' => 'credits'],
+ ]],
+ ['key' => 'card_pass_prefix', 'label' => '卡号前缀', 'type' => 'input', 'default' => 'AK'],
+ ['key' => 'card_pass_length', 'label' => '卡号长度', 'type' => 'number', 'default' => 16, 'min' => 8, 'max' => 32],
+ ['key' => 'card_pass_batch_size', 'label' => '批量生成数量', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
+ ]],
+ ['key' => 'redeem', 'title' => '兑换设置', 'fields' => [
+ ['key' => 'card_pass_max_redeem', 'label' => '单卡最大兑换次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100],
+ ['key' => 'card_pass_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效'],
+ ['key' => 'card_pass_amount', 'label' => '面额(元)', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 9999],
+ ['key' => 'card_pass_vip_days', 'label' => 'VIP天数', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 3650],
+ ['key' => 'card_pass_credits', 'label' => '赠送积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'card_pass_redeem_page', 'label' => '兑换页面', 'type' => 'input', 'default' => ''],
+ ['key' => 'card_pass_success_msg', 'label' => '兑换成功提示', 'type' => 'input', 'default' => '兑换成功!'],
+ ['key' => 'card_pass_fail_msg', 'label' => '兑换失败提示', 'type' => 'input', 'default' => '卡号无效或已过期'],
+ ]],
+ ],
+ ],
+ 'withdrawal' => [
+ 'key' => 'withdrawal',
+ 'title' => '提现管理',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '提现设置', 'fields' => [
+ ['key' => 'pay_rebate_withdraw_lowest_money', 'label' => '最低提现金额', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 99999],
+ ['key' => 'withdraw_service_charge', 'label' => '提现手续费(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 50],
+ ['key' => 'pay_rebate_withdraw_text_details', 'label' => '提现说明', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php b/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php
new file mode 100644
index 00000000..05942fb8
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php
@@ -0,0 +1,194 @@
+ 'permissions',
+ 'title' => '功能&权限',
+ 'sections' => [
+ 'login' => [
+ 'key' => 'login',
+ 'title' => '登录权限',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_captcha_config',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '基础开关', 'fields' => [
+ ['key' => 'enabled', 'label' => '启用人机验证', 'type' => 'switch', 'default' => false],
+ ['key' => 'active_provider', 'label' => '当前服务商', 'type' => 'radio', 'default' => '', 'options' => [
+ ['label' => '腾讯云 Captcha', 'value' => 'tencent'],
+ ['label' => '阿里云 CAPTCHA 2.0', 'value' => 'aliyun'],
+ ['label' => 'Cloudflare Turnstile', 'value' => 'cloudflare'],
+ ['label' => '极验 GeeTest', 'value' => 'geetest'],
+ ]],
+ ['key' => 'mode', 'label' => '触发模式', 'type' => 'radio', 'default' => 'always', 'options' => [
+ ['label' => '始终显示', 'value' => 'always'],
+ ]],
+ ['key' => 'disabled_scenarios', 'label' => '停用场景', 'type' => 'checkbox', 'default' => [], 'options' => [
+ ['label' => '发表评论', 'value' => 'comment'],
+ ['label' => '用户登录', 'value' => 'login'],
+ ['label' => '用户注册', 'value' => 'register'],
+ ['label' => '绑定第三方', 'value' => 'bind'],
+ ['label' => '找回密码', 'value' => 'lostpassword'],
+ ['label' => '修改密码', 'value' => 'change_password'],
+ ['label' => '更改邮箱', 'value' => 'change_email'],
+ ['label' => '邮箱验证', 'value' => 'verify_email'],
+ ['label' => '资源下载', 'value' => 'download'],
+ ['label' => '每日签到', 'value' => 'signin'],
+ ]],
+ ]],
+ ['key' => 'providers', 'title' => '服务商配置', 'fields' => [
+ ['key' => 'tencent', 'label' => '腾讯云配置', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array'],
+ ['key' => 'aliyun', 'label' => '阿里云配置', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array'],
+ ['key' => 'cloudflare', 'label' => 'Cloudflare 配置', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array'],
+ ['key' => 'geetest', 'label' => '极验配置', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array'],
+ ]],
+ ],
+ ],
+ 'comment' => [
+ 'key' => 'comment',
+ 'title' => '评论权限',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '基础权限', 'fields' => [
+ ['key' => 'comment_enabled', 'label' => '全局评论开关', 'type' => 'switch', 'default' => true, 'help' => '关闭后全站禁用评论功能'],
+ ['key' => 'comment_guest_allowed', 'label' => '允许游客评论', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_min_length', 'label' => '评论最少字数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 100],
+ ['key' => 'comment_max_length', 'label' => '评论最多字数', 'type' => 'number', 'default' => 1000, 'min' => 50, 'max' => 10000],
+ ['key' => 'comment_cool_down_seconds', 'label' => '评论冷却时间(秒)', 'type' => 'number', 'default' => 15, 'min' => 0, 'max' => 300],
+ ['key' => 'comment_max_links', 'label' => '最大链接数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 10],
+ ['key' => 'comment_max_images', 'label' => '最大图片数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 20],
+ ]],
+ ['key' => 'sensitive', 'title' => '敏感词', 'fields' => [
+ ['key' => 'comment_sensitive_words', 'label' => '敏感词列表', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'comment_sensitive_action', 'label' => '敏感词处理', 'type' => 'radio', 'default' => 'reject', 'options' => [
+ ['label' => '拒绝评论', 'value' => 'reject'],
+ ['label' => '替换为***', 'value' => 'replace'],
+ ['label' => '转入审核', 'value' => 'pending'],
+ ]],
+ ['key' => 'comment_sensitive_log', 'label' => '记录敏感词日志', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'anti-spam', 'title' => '反垃圾', 'fields' => [
+ ['key' => 'comment_link_filter', 'label' => '链接过滤', 'type' => 'switch', 'default' => true, 'help' => '评论包含链接自动进入审核'],
+ ['key' => 'comment_flood_protection', 'label' => '防刷评论', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_flood_interval', 'label' => '防刷间隔(秒)', 'type' => 'number', 'default' => 10, 'min' => 3, 'max' => 60],
+ ['key' => 'comment_max_per_hour', 'label' => '每小时最大评论数', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'comment_avatar_required', 'label' => '要求设置头像', 'type' => 'switch', 'default' => false],
+ ['key' => 'comment_nickname_required', 'label' => '要求填写昵称', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_email_required', 'label' => '要求填写邮箱', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'attachment' => [
+ 'key' => 'attachment',
+ 'title' => '附件权限',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'profile', 'title' => '头像与封面', 'fields' => [
+ ['key' => 'allow_avatar_upload', 'label' => '允许用户上传头像', 'type' => 'switch', 'default' => true],
+ ['key' => 'allow_cover_upload', 'label' => '允许用户上传封面图', 'type' => 'switch', 'default' => true],
+ ['key' => 'allow_background_upload', 'label' => '允许用户上传背景图', 'type' => 'switch', 'default' => false],
+ ['key' => 'avatar_max_size', 'label' => '头像/封面大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
+ ]],
+ ['key' => 'content', 'title' => '内容上传', 'fields' => [
+ ['key' => 'allow_submit_images', 'label' => '允许投稿插入图片', 'type' => 'switch', 'default' => true],
+ ['key' => 'submit_image_max_size', 'label' => '投稿图片大小上限 (MB)', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 20],
+ ['key' => 'allow_comment_images', 'label' => '允许评论插入图片', 'type' => 'switch', 'default' => false],
+ ['key' => 'comment_image_max_size', 'label' => '评论图片大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
+ ]],
+ ['key' => 'resource', 'title' => '资源附件', 'fields' => [
+ ['key' => 'allow_resource_upload', 'label' => '允许资源附件上传', 'type' => 'switch', 'default' => true],
+ ['key' => 'resource_max_size', 'label' => '资源附件大小上限 (MB)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 200],
+ ]],
+ ],
+ ],
+ 'download' => [
+ 'key' => 'download',
+ 'title' => '下载权限',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '下载设置', 'fields' => [
+ ['key' => 'download_require_login', 'label' => '下载需要登录', 'type' => 'switch', 'default' => true],
+ ['key' => 'download_require_credits', 'label' => '下载需要积分', 'type' => 'switch', 'default' => false],
+ ['key' => 'download_credits_cost', 'label' => '下载所需积分', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100],
+ ['key' => 'download_daily_limit', 'label' => '每日下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为不限制'],
+ ['key' => 'download_link_expire', 'label' => '下载链接有效期(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
+ ['key' => 'download_speed_limit', 'label' => '下载速度限制(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为不限制'],
+ ]],
+ ['key' => 'vip', 'title' => 'VIP 特权', 'fields' => [
+ ['key' => 'download_vip_free', 'label' => 'VIP 免费下载', 'type' => 'switch', 'default' => false],
+ ['key' => 'download_vip_daily_extra', 'label' => 'VIP每日额外次数', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 999],
+ ]],
+ ['key' => 'role', 'title' => '角色权限', 'fields' => [
+ ['key' => 'download_role_limits', 'label' => '角色下载限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|daily_limit,每行一条'],
+ ['key' => 'download_role_size', 'label' => '角色文件大小限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|max_size_mb,每行一条'],
+ ]],
+ ['key' => 'captcha', 'title' => '安全设置', 'fields' => [
+ ['key' => 'download_captcha', 'label' => '下载需要验证码', 'type' => 'switch', 'default' => false],
+ ['key' => 'download_ip_limit', 'label' => 'IP下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '同一IP每小时最大下载次数,0为不限制'],
+ ]],
+ ],
+ ],
+ 'moderation' => [
+ 'key' => 'moderation',
+ 'title' => '审核和风控',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_moderation_config',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '审核总开关', 'fields' => [
+ ['key' => 'enabled', 'label' => '启用审核系统', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'akismet', 'title' => 'Akismet', 'fields' => [
+ ['key' => 'akismet_enabled', 'label' => '启用 Akismet', 'type' => 'switch', 'default' => false, 'dependsOn' => 'enabled'],
+ ['key' => 'akismet_site_url', 'label' => 'Site URL', 'type' => 'input', 'default' => '', 'dependsOn' => { 'key' => 'akismet_enabled', 'value' => true }],
+ ['key' => 'akismet_blog_lang', 'label' => '博客语言', 'type' => 'input', 'default' => 'zh-CN', 'dependsOn' => { 'key' => 'akismet_enabled', 'value' => true }],
+ ['key' => 'akismet_api_key', 'label' => 'API Key', 'type' => 'input', 'inputType' => 'password', 'default' => '', 'dependsOn' => { 'key' => 'akismet_enabled', 'value' => true }],
+ ]],
+ ['key' => 'ai', 'title' => 'AI 审核', 'fields' => [
+ ['key' => 'ai_enabled', 'label' => '启用 AI 审核', 'type' => 'switch', 'default' => false, 'dependsOn' => 'enabled'],
+ ['key' => 'ai_api_hint', 'label' => '审核 API 配置', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => 'API 接入(百度/阿里云等)在「扩展 → API内容审核」中统一配置,此处仅控制审核范围和策略。', 'dependsOn' => { 'key' => 'ai_enabled', 'value' => true }],
+ ['key' => 'ai_moderate_comments', 'label' => '用户评论', 'type' => 'switch', 'default' => true, 'dependsOn' => { 'key' => 'ai_enabled', 'value' => true }],
+ ['key' => 'ai_moderate_background', 'label' => '用户资料背景', 'type' => 'switch', 'default' => true, 'dependsOn' => { 'key' => 'ai_enabled', 'value' => true }],
+ ['key' => 'auto_approve_low_risk', 'label' => '低风险自动通过', 'type' => 'switch', 'default' => false, 'dependsOn' => { 'key' => 'ai_enabled', 'value' => true }],
+ ['key' => 'auto_reject_high_risk', 'label' => '高风险自动拒绝', 'type' => 'switch', 'default' => false, 'dependsOn' => { 'key' => 'ai_enabled', 'value' => true }],
+ ['key' => 'low_risk_max_score', 'label' => '低风险上限', 'type' => 'number', 'default' => 35, 'min' => 0, 'max' => 100, 'dependsOn' => { 'key' => 'ai_enabled', 'value' => true }],
+ ['key' => 'high_risk_min_score', 'label' => '高风险下限', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100, 'dependsOn' => { 'key' => 'ai_enabled', 'value' => true }],
+ ]],
+ ['key' => 'keywords', 'title' => '关键词拦截', 'fields' => [
+ ['key' => 'blocked_keywords', 'label' => '屏蔽关键词列表', 'type' => 'textarea', 'default' => '', 'rows' => 4, 'dependsOn' => 'enabled'],
+ ]],
+ ['key' => 'rate-limit', 'title' => '频率限制', 'fields' => [
+ ['key' => 'rate_limit_enabled', 'label' => '启用频率限制', 'type' => 'switch', 'default' => false, 'dependsOn' => 'enabled'],
+ ['key' => 'rate_limit_min_interval', 'label' => '最小间隔(秒)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 300, 'dependsOn' => { 'key' => 'rate_limit_enabled', 'value' => true }],
+ ['key' => 'rate_limit_max_per_hour', 'label' => '每小时上限', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 200, 'dependsOn' => { 'key' => 'rate_limit_enabled', 'value' => true }],
+ ]],
+ ['key' => 'geoip', 'title' => 'GeoIP 拦截', 'fields' => [
+ ['key' => 'geoip_enabled', 'label' => '启用 GeoIP 拦截', 'type' => 'switch', 'default' => false, 'dependsOn' => 'enabled'],
+ ['key' => 'geoip_hint', 'label' => 'IP 数据源', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '复用「扩展 → IP归属地」中配置的数据源和 API Key。', 'dependsOn' => { 'key' => 'geoip_enabled', 'value' => true }],
+ ['key' => 'geoip_block_countries', 'label' => '屏蔽国家/地区', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array', 'dependsOn' => { 'key' => 'geoip_enabled', 'value' => true }],
+ ]],
+ ],
+ ],
+ 'capabilities' => [
+ 'key' => 'capabilities',
+ 'title' => '基本权限 / 能力矩阵',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => self::build_cap_matrix_groups(),
+ ],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php b/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php
new file mode 100644
index 00000000..9c2f01ed
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php
@@ -0,0 +1,453 @@
+ 'post_list',
+ 'title' => '文章&列表',
+ 'sections' => [
+ 'list' => [
+ 'key' => 'list',
+ 'title' => '文章列表',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'card', 'title' => '卡片样式', 'fields' => [
+ ['key' => 'post_card_style', 'label' => '卡片样式', 'type' => 'select', 'default' => 'standard', 'options' => [
+ ['label' => '标准卡', 'value' => 'standard'],
+ ['label' => '紧凑卡', 'value' => 'compact'],
+ ['label' => '大图卡', 'value' => 'large_thumb'],
+ ['label' => '多图卡', 'value' => 'multi_thumb'],
+ ['label' => '纯文字', 'value' => 'text'],
+ ['label' => '有图则图文', 'value' => 'thumb_if_has'],
+ ]],
+ ['key' => 'post_excerpt_length', 'label' => '摘要长度', 'type' => 'number', 'default' => 120, 'min' => 20, 'max' => 500],
+ ]],
+ ['key' => 'meta', 'title' => '元信息显示', 'fields' => [
+ ['key' => 'post_show_author', 'label' => '显示作者', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_show_date', 'label' => '显示日期', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_show_views', 'label' => '显示浏览数', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_show_category', 'label' => '显示分类', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_show_tags', 'label' => '显示标签', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_show_comments', 'label' => '显示评论数', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_show_likes', 'label' => '显示点赞数', 'type' => 'switch', 'default' => false],
+ ['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'],
+ ]],
+ ['key' => 'post_list_meta_show', 'label' => '显示元信息', 'type' => 'switch', 'default' => true],
+ ['key' => 'list_meta_show', 'label' => '移动端优先部件', 'type' => 'radio', 'default' => 'like', 'options' => [
+ ['label' => '浏览数', 'value' => 'view'],
+ ['label' => '点赞数', 'value' => 'like'],
+ ['label' => '评论数', 'value' => 'comm'],
+ ], 'description' => '移动端空间不足时保留显示的单一部件'],
+ ]],
+ ['key' => 'layout', 'title' => '布局设置', 'fields' => [
+ ['key' => 'post_list_columns', 'label' => '列表列数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 3],
+ ['key' => 'post_list_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
+ ['key' => 'sticky_s', 'label' => '置顶范围', 'type' => 'checkbox', 'default' => ['home', 'cat'], 'options' => [
+ ['label' => '首页', 'value' => 'home'],
+ ['label' => '分类页', 'value' => 'cat'],
+ ['label' => '标签页', 'value' => 'tag'],
+ ['label' => '专题页', 'value' => 'topics'],
+ ]],
+ ['key' => 'post_target_blank', 'label' => '新窗口打开', 'type' => 'switch', 'default' => false],
+ ['key' => 'item_heading_bold', 'label' => '标题加粗', 'type' => 'switch', 'default' => false],
+ ['key' => 'list_show_type', 'label' => '列表样式', 'type' => 'radio', 'default' => 'separate', 'options' => [
+ ['label' => '分隔卡片', 'value' => 'separate'],
+ ['label' => '无间距', 'value' => 'no_margin'],
+ ]],
+ ['key' => 'list_card_home', 'label' => '首页卡片模式', 'type' => 'switch', 'default' => false],
+ ['key' => 'list_card_cat', 'label' => '分类卡片模式', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => []],
+ ['key' => 'list_card_tag', 'label' => '标签页卡片模式', 'type' => 'switch', 'default' => false],
+ ['key' => 'list_card_topics', 'label' => '专题页卡片模式', 'type' => 'switch', 'default' => false],
+ ['key' => 'list_card_author', 'label' => '用户页卡片模式', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'pagination', 'title' => '分页方式', 'fields' => [
+ ['key' => 'post_pagination_mode', 'label' => '分页模式', 'type' => 'radio', 'default' => 'numbered', 'options' => [
+ ['label' => '数字分页', 'value' => 'numbered'],
+ ['label' => '加载更多', 'value' => 'load_more'],
+ ['label' => '无限滚动', 'value' => 'infinite'],
+ ]],
+ ['key' => 'ias_max', 'label' => '最大加载页数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 10, 'description' => '0=不限'],
+ ['key' => 'ajax_trigger_text', 'label' => '加载更多文案', 'type' => 'input', 'default' => ''],
+ ['key' => 'ajax_nomore_text', 'label' => '没有更多文案', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'sort', 'title' => '默认排序', 'fields' => [
+ ['key' => 'list_orderby', 'label' => '列表默认排序', 'type' => 'radio', 'default' => 'modified', 'options' => [
+ ['label' => '更新时间', 'value' => 'modified'],
+ ['label' => '发布时间', 'value' => 'date'],
+ ['label' => '评论数', 'value' => 'comments'],
+ ['label' => 'WordPress 默认', 'value' => 'default'],
+ ], 'help' => '分类/标签/归档页面的默认排序方式'],
+ ]],
+ ['key' => 'list_option', 'title' => '列表模式配置', 'fields' => [
+ ['key' => 'list_list_style', 'label' => '列表样式变体', 'type' => 'radio', 'default' => '', 'options' => [
+ ['label' => '默认', 'value' => ''],
+ ['label' => '样式2', 'value' => 'style2'],
+ ]],
+ ['key' => 'list_list_img_position', 'label' => '缩略图位置', 'type' => 'radio', 'default' => 'left', 'options' => [
+ ['label' => '左侧', 'value' => 'left'],
+ ['label' => '右侧', 'value' => 'right'],
+ ]],
+ ['key' => 'list_list_scale', 'label' => '缩略图比例 (%)', 'type' => 'number', 'default' => 70, 'min' => 50, 'max' => 200],
+ ]],
+ ['key' => 'card_option', 'title' => '卡片模式配置', 'fields' => [
+ ['key' => 'list_card_style', 'label' => '卡片样式变体', 'type' => 'radio', 'default' => '', 'options' => [
+ ['label' => '默认', 'value' => ''],
+ ['label' => '样式3', 'value' => 'style3'],
+ ]],
+ ['key' => 'list_card_scale', 'label' => '缩略图比例 (%)', 'type' => 'number', 'default' => 70, 'min' => 20, 'max' => 200],
+ ]],
+ ],
+ ],
+ 'thumb' => [
+ 'key' => 'thumb',
+ 'title' => '列表缩略图',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'source', 'title' => '图片来源', 'fields' => [
+ ['key' => 'thumb_priority', 'label' => '图片优先级', 'type' => 'input', 'default' => 'featured,attachment,resource,custom'],
+ ['key' => 'thumb_fallback', 'label' => '无图时处理', 'type' => 'radio', 'default' => 'placeholder', 'options' => [
+ ['label' => '显示占位图', 'value' => 'placeholder'],
+ ['label' => '隐藏缩略图', 'value' => 'hide'],
+ ['label' => '随机封面', 'value' => 'random'],
+ ]],
+ ['key' => 'thumb_spare_images', 'label' => '备用缩略图', 'type' => 'gallery', 'default' => [], 'description' => '当 thumb_fallback 为 random 时从此图库随机选取'],
+ ['key' => 'thumb_aspect_ratio', 'label' => '图片比例', 'type' => 'select', 'default' => '16:9', 'options' => [
+ ['label' => '16:9 宽屏', 'value' => '16:9'],
+ ['label' => '4:3 标准', 'value' => '4:3'],
+ ['label' => '1:1 正方形', 'value' => '1:1'],
+ ['label' => '自适应', 'value' => 'auto'],
+ ]],
+ ['key' => 'list_thumb_fit', 'label' => '缩略图填充方式', 'type' => 'radio', 'default' => 'cover', 'options' => [
+ ['label' => '填充裁剪', 'value' => 'cover'],
+ ['label' => '完整显示', 'value' => 'contain'],
+ ]],
+ ['key' => 'list_thumb_slides_s', 'label' => '幻灯片缩略图', 'type' => 'switch', 'default' => true, 'description' => '允许幻灯片作为列表缩略图'],
+ ['key' => 'list_thumb_video_s', 'label' => '视频缩略图', 'type' => 'switch', 'default' => true, 'description' => '鼠标悬停时自动播放视频缩略图'],
+ ['key' => 'list_thumb_video_mute_s', 'label' => '视频缩略图静音', 'type' => 'switch', 'default' => true, 'description' => '视频缩略图播放时静音'],
+ ['key' => 'thumb_postfirstimg_s', 'label' => '自动获取文章首图', 'type' => 'switch', 'default' => true, 'description' => '自动从文章内容中提取第一张图片作为缩略图'],
+ ['key' => 'thumb_catimg_s', 'label' => '自动获取分类封面', 'type' => 'switch', 'default' => true, 'description' => '无特色图时使用分类封面图作为缩略图'],
+ ['key' => 'thumb_postfirstimg_size', 'label' => '首图尺寸', 'type' => 'radio', 'default' => 'medium', 'options' => [
+ ['label' => '缩略图', 'value' => 'thumbnail'],
+ ['label' => '中等', 'value' => 'medium'],
+ ['label' => '大', 'value' => 'large'],
+ ['label' => '原始', 'value' => 'full'],
+ ], 'description' => '自动获取文章首图时使用的 WP 图片尺寸'],
+ ]],
+ ['key' => 'multi', 'title' => '多图模式', 'fields' => [
+ ['key' => 'thumb_multi_enabled', 'label' => '多图卡片', 'type' => 'switch', 'default' => false],
+ ['key' => 'thumb_multi_count', 'label' => '最多显示几张', 'type' => 'number', 'default' => 3, 'min' => 2, 'max' => 6],
+ ['key' => 'mult_thumb_cat', 'label' => '按分类多图显示', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => []],
+ ['key' => 'thumb_lazy', 'label' => '懒加载', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'page' => [
+ 'key' => 'page',
+ 'title' => '文章页面',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'navigation', 'title' => '文章导航', 'fields' => [
+ ['key' => 'post_toc_enabled', 'label' => '目录树导航', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_toc_depth', 'label' => '目录深度', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 6],
+ ['key' => 'article_nav_mobile_nav_s', 'label' => '移动端弹出菜单', 'type' => 'switch', 'default' => true, 'description' => '在移动端弹出菜单中显示目录导航'],
+ ['key' => 'post_reading_progress', 'label' => '阅读进度条', 'type' => 'switch', 'default' => true],
+ ['key' => 'breadcrumbs_single_s', 'label' => '面包屑导航', 'type' => 'switch', 'default' => true],
+ ['key' => 'breadcrumbs_home_text', 'label' => '面包屑首页文字', 'type' => 'input', 'default' => '首页', 'depends_on' => 'breadcrumbs_single_s'],
+ ['key' => 'post_prevnext_s', 'label' => '上一篇/下一篇', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_authordesc_s', 'label' => '作者简介', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_single_hide_time_s', 'label' => '隐藏文章时间', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'cover', 'title' => '封面展示', 'fields' => [
+ ['key' => 'post_article_image_cover', 'label' => '图片封面', 'type' => 'switch', 'default' => true, 'description' => '文章有特色图时自动展示封面'],
+ ['key' => 'post_article_image_cover_scale', 'label' => '封面高度比例(%)', 'type' => 'number', 'default' => 35, 'min' => 10, 'max' => 100],
+ ['key' => 'post_article_slide_cover', 'label' => '幻灯片封面', 'type' => 'switch', 'default' => true, 'description' => '文章有图集时以轮播形式展示封面'],
+ ['key' => 'post_article_video_cover', 'label' => '视频封面', 'type' => 'switch', 'default' => true, 'description' => '文章有视频时以视频播放器作为封面'],
+ ['key' => 'post_article_video_cover_scale', 'label' => '视频封面高度比例(%)', 'type' => 'number', 'default' => 56, 'min' => 20, 'max' => 100],
+ ]],
+ ['key' => 'media', 'title' => '媒体功能', 'fields' => [
+ ['key' => 'post_lightbox', 'label' => '图片灯箱', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_lightbox_thumbs', 'label' => '灯箱缩略图', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_lightbox_zoom', 'label' => '灯箱缩放', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_lightbox_full', 'label' => '灯箱全屏', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_lightbox_down', 'label' => '灯箱下载', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_lightbox_play', 'label' => '灯箱播放', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'related', 'title' => '相关推荐', 'fields' => [
+ ['key' => 'post_related_enabled', 'label' => '显示相关推荐', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_related_count', 'label' => '推荐数量', 'type' => 'number', 'default' => 6, 'min' => 1, 'max' => 20],
+ ['key' => 'post_related_type', 'label' => '推荐方式', 'type' => 'radio', 'default' => 'category', 'options' => [
+ ['label' => '同分类', 'value' => 'category'],
+ ['label' => '同标签', 'value' => 'tag'],
+ ['label' => '随机', 'value' => 'random'],
+ ]],
+ ['key' => 'post_related_orderby', 'label' => '推荐排序', 'type' => 'select', 'default' => 'rand', 'options' => [
+ ['label' => '随机', 'value' => 'rand'],
+ ['label' => '相关度', 'value' => 'relevance'],
+ ['label' => '时间', 'value' => 'date'],
+ ]],
+ ['key' => 'post_related_title', 'label' => '推荐标题', 'type' => 'input', 'default' => '相关推荐'],
+ ]],
+ ['key' => 'interactions', 'title' => '互动功能', 'fields' => [
+ ['key' => 'post_like_enabled', 'label' => '点赞', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_favorite_enabled', 'label' => '收藏', 'type' => 'switch', 'default' => true],
+ ['key' => 'share_s', 'label' => '分享按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_copyright_enabled', 'label' => '版权信息', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_copyright_text', 'label' => '版权默认文案', 'type' => 'textarea', 'default' => '', 'description' => '留空则自动生成"本文由 XX 原创发布"'],
+ ['key' => 'post_wechat_share', 'label' => '微信分享', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_rewards_enabled', 'label' => '打赏功能', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_rewards_text', 'label' => '打赏文案', 'type' => 'input', 'default' => ''],
+ ['key' => 'post_button_toptext', 'label' => '按钮上方文案', 'type' => 'input', 'default' => '喜欢就支持一下吧'],
+ ]],
+ ['key' => 'content', 'title' => '内容处理', 'fields' => [
+ ['key' => 'post_p_indent', 'label' => '段落缩进', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_maxheight_kg', 'label' => '内容高度限制', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_maxheight', 'label' => '最大高度', 'type' => 'number', 'default' => 1000, 'min' => 600, 'max' => 3000],
+ ['key' => 'post_front_content', 'label' => '文章头部内容', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'post_after_content', 'label' => '文章底部内容', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'yiyan', 'title' => '一言', 'fields' => [
+ ['key' => 'post_yiyan_header', 'label' => '文章头部一言', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_yiyan_footer', 'label' => '文章底部一言', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_yiyan_box', 'label' => '一言盒子', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'meta', 'title' => 'Meta设置', 'fields' => [
+ ['key' => 'post_default_views_min', 'label' => '默认浏览最小值', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 9999],
+ ['key' => 'post_default_views_max', 'label' => '默认浏览最大值', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 9999],
+ ['key' => 'post_default_likes_min', 'label' => '默认点赞最小值', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 9999],
+ ['key' => 'post_default_likes_max', 'label' => '默认点赞最大值', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 9999],
+ ['key' => 'post_views_s', 'label' => '浏览统计', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_close_comments', 'label' => '关闭评论', 'type' => 'switch', 'default' => false],
+ ]],
+ ],
+ ],
+ 'filter' => [
+ 'key' => 'filter',
+ 'title' => '高级筛选',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '筛选基础', 'fields' => [
+ ['key' => 'filter_enabled', 'label' => '启用筛选栏', 'type' => 'switch', 'default' => false],
+ ['key' => 'custom_filter_show', 'label' => '筛选显示页面', 'type' => 'checkbox', 'default' => ['cat', 'tag'], 'options' => [
+ ['label' => '分类页', 'value' => 'cat'],
+ ['label' => '标签页', 'value' => 'tag'],
+ ['label' => '专题页', 'value' => 'topics'],
+ ], 'help' => '选择在哪些页面显示筛选栏'],
+ ['key' => 'filter_categories', 'label' => '分类筛选', 'type' => 'switch', 'default' => true],
+ ['key' => 'filter_tags', 'label' => '标签筛选', 'type' => 'switch', 'default' => false],
+ ['key' => 'filter_sort', 'label' => '排序选项', 'type' => 'switch', 'default' => true],
+ ['key' => 'filter_ajax_enabled', 'label' => 'AJAX 无刷新筛选', 'type' => 'switch', 'default' => true, 'help' => '启用后分类/标签/排序切换将通过 AJAX 加载,无需刷新页面'],
+ ]],
+ ['key' => 'ajax_dimensions', 'title' => 'AJAX 筛选维度', 'fields' => [
+ ['key' => 'filter_cat_show_cat', 'label' => '分类页:显示分类筛选', 'type' => 'switch', 'default' => true],
+ ['key' => 'filter_cat_show_tag', 'label' => '分类页:显示标签筛选', 'type' => 'switch', 'default' => false],
+ ['key' => 'filter_tag_show_cat', 'label' => '标签页:显示分类筛选', 'type' => 'switch', 'default' => true],
+ ['key' => 'filter_tag_show_tag', 'label' => '标签页:显示标签筛选', 'type' => 'switch', 'default' => false],
+ ['key' => 'filter_topics_show_cat', 'label' => '专题页:显示分类筛选', 'type' => 'switch', 'default' => true],
+ ['key' => 'filter_topics_show_tag', 'label' => '专题页:显示标签筛选', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'custom_filter', 'title' => '自定义筛选维度', 'fields' => [
+ ['key' => 'custom_filter', 'label' => '筛选维度定义', 'type' => 'repeater', 'default' => [], 'help' => '每项定义一个筛选维度(如"类型""地区""年份"),及其可选值列表', 'fields' => [
+ ['key' => 'name', 'label' => '维度名称', 'type' => 'input', 'default' => ''],
+ ['key' => 'key', 'label' => '维度标识', 'type' => 'input', 'default' => ''],
+ ['key' => 'vals', 'label' => '可选值', 'type' => 'repeater', 'default' => [], 'fields' => [
+ ['key' => 'key', 'label' => '标识', 'type' => 'input', 'default' => ''],
+ ['key' => 'name', 'label' => '显示名', 'type' => 'input', 'default' => ''],
+ ]],
+ ]],
+ ]],
+ ['key' => 'cat_custom_filter', 'title' => '按分类绑定筛选', 'fields' => [
+ ['key' => 'cat_custom_filter', 'label' => '分类筛选绑定', 'type' => 'repeater', 'default' => [], 'help' => '为指定分类绑定不同的筛选维度组合', 'fields' => [
+ ['key' => 'cats', 'label' => '绑定分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => []],
+ ['key' => 'cat_s', 'label' => '显示分类筛选', 'type' => 'switch', 'default' => true],
+ ['key' => 'tag_s', 'label' => '显示标签筛选', 'type' => 'switch', 'default' => false],
+ ['key' => 'topics_s', 'label' => '显示专题筛选', 'type' => 'switch', 'default' => false],
+ ['key' => 'orderby_s', 'label' => '显示排序按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'custom_filters', 'label' => '启用的自定义维度', 'type' => 'input', 'default' => '', 'help' => '逗号分隔维度标识,留空使用全部'],
+ ]],
+ ]],
+ ],
+ ],
+ 'feature' => [
+ 'key' => 'feature',
+ 'title' => '文章功能',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'tools', 'title' => '工具功能', 'fields' => [
+ ['key' => 'feature_print', 'label' => '打印文章', 'type' => 'switch', 'default' => true],
+ ['key' => 'feature_word_count', 'label' => '字数统计', 'type' => 'switch', 'default' => true],
+ ['key' => 'feature_reading_time', 'label' => '预计阅读时间', 'type' => 'switch', 'default' => true],
+ ['key' => 'feature_pdf_export', 'label' => 'PDF 导出', 'type' => 'switch', 'default' => false],
+ ]],
+ ],
+ ],
+ 'comment' => [
+ 'key' => 'comment',
+ 'title' => '评论设置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'display', 'title' => '评论展示', 'fields' => [
+ ['key' => 'comment_order', 'label' => '评论排序', 'type' => 'radio', 'default' => 'asc', 'options' => [
+ ['label' => '正序', 'value' => 'asc'],
+ ['label' => '倒序', 'value' => 'desc'],
+ ]],
+ ['key' => 'comment_per_page', 'label' => '每页评论数', 'type' => 'number', 'default' => 20, 'min' => 5, 'max' => 100],
+ ['key' => 'comment_pagination_type', 'label' => '分页方式', 'type' => 'radio', 'default' => 'numbered', 'options' => [
+ ['label' => '翻页', 'value' => 'numbered'],
+ ['label' => '加载更多', 'value' => 'load_more'],
+ ['label' => '无限滚动', 'value' => 'infinite'],
+ ]],
+ ['key' => 'comment_paging_ajax_ias_s', 'label' => '评论自动加载', 'type' => 'switch', 'default' => true, 'description' => '评论无限滚动时自动触发加载'],
+ ['key' => 'comment_paging_ajax_ias_max', 'label' => '评论最大加载页数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 10, 'description' => '0=不限'],
+ ['key' => 'comment_nested_depth', 'label' => '回复嵌套深度', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 10],
+ ['key' => 'comment_avatar_size', 'label' => '头像大小', 'type' => 'number', 'default' => 48, 'min' => 24, 'max' => 96],
+ ]],
+ ['key' => 'permissions', 'title' => '评论权限', 'fields' => [
+ ['key' => 'comment_require_login', 'label' => '评论需要登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'comment_allow_reply', 'label' => '允许回复', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_close_comments', 'label' => '关闭评论', 'type' => 'switch', 'default' => false],
+ ['key' => 'comment_edit_window', 'label' => '作者自助编辑时限(分钟)', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1440],
+ ]],
+ ['key' => 'toolbar', 'title' => '评论工具栏', 'fields' => [
+ ['key' => 'comment_smilie', 'label' => 'Emoji表情', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_code', 'label' => '代码插入', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_img', 'label' => '图片上传', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_upload_img', 'label' => '评论图片上传', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'interaction', 'title' => '互动功能', 'fields' => [
+ ['key' => 'comment_like_s', 'label' => '评论点赞', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_quick_s', 'label' => '快捷回复', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_city_s', 'label' => '评论城市', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_author_tag', 'label' => '作者标签', 'type' => 'switch', 'default' => true],
+ ['key' => 'comment_corderby', 'label' => '评论排序切换', 'type' => 'switch', 'default' => true, 'description' => '前台显示最新/最早/最热切换按钮'],
+ ]],
+ ['key' => 'text', 'title' => '文案配置', 'fields' => [
+ ['key' => 'comment_empty_text', 'label' => '空评论提示', 'type' => 'input', 'default' => '暂无评论,来做第一个评论的人吧~'],
+ ['key' => 'comment_title', 'label' => '评论区标题', 'type' => 'input', 'default' => '评论'],
+ ['key' => 'comment_submit_text', 'label' => '提交按钮文案', 'type' => 'input', 'default' => '提交评论'],
+ ['key' => 'comment_text', 'label' => '评论框提示', 'type' => 'input', 'default' => '说点什么吧...'],
+ ]],
+ ],
+ ],
+ 'pay' => [
+ 'key' => 'pay',
+ 'title' => '付费内容',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '付费设置', 'fields' => [
+ ['key' => 'post_pay_enable', 'label' => '启用付费内容', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_pay_price_default', 'label' => '默认价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999],
+ ['key' => 'post_pay_free_logged_show', 'label' => '登录用户显示免费', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_pay_box_position', 'label' => '付费框位置', 'type' => 'radio', 'default' => 'content', 'options' => [
+ ['label' => '内容前', 'value' => 'content'],
+ ['label' => '全文遮罩', 'value' => 'mask'],
+ ]],
+ ['key' => 'post_pay_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
+ ['key' => 'post_pay_show_user', 'label' => '显示购买用户', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'download', 'title' => '下载设置', 'fields' => [
+ ['key' => 'post_download_enable', 'label' => '启用下载功能', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_download_remote_s', 'label' => '远程文件下载', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_download_verify_nonce', 'label' => '下载鉴权', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_download_user_log', 'label' => '用户下载日志', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_download_post_log', 'label' => '文章下载日志', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'share' => [
+ 'key' => 'share',
+ 'title' => '分享设置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '分享设置', 'fields' => [
+ ['key' => 'share_wechat_s', 'label' => '微信分享', 'type' => 'switch', 'default' => false],
+ ['key' => 'share_wechat_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'share_wechat_app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
+ ['key' => 'share_only_logo', 'label' => '仅显示Logo', 'type' => 'switch', 'default' => false],
+ ['key' => 'share_img_compatible_s', 'label' => '海报兼容模式', 'type' => 'switch', 'default' => false, 'description' => '启用后使用兼容模式生成分享海报'],
+ ['key' => 'share_img_byimg', 'label' => '海报默认图', 'type' => 'media', 'default' => ''],
+ ['key' => 'share_logo', 'label' => '海报LOGO', 'type' => 'media', 'default' => ''],
+ ['key' => 'share_img', 'label' => '分享图片', 'type' => 'media', 'default' => ''],
+ ['key' => 'share_desc', 'label' => '分享描述', 'type' => 'textarea', 'default' => '扫一扫 立即查看'],
+ ]],
+ ],
+ ],
+ 'submit' => [
+ 'key' => 'submit',
+ 'title' => '前台投稿',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '基础配置', 'fields' => [
+ ['key' => 'front_submit_enabled', 'label' => '开启前台投稿', 'type' => 'switch', 'default' => true],
+ ['key' => 'front_submit_roles', 'label' => '允许投稿的角色', 'type' => 'input', 'default' => 'author,editor,administrator'],
+ ['key' => 'post_article_btn_txte', 'label' => '投稿按钮文案', 'type' => 'input', 'default' => '发布文章'],
+ ['key' => 'post_article_title_strlen_limit_min', 'label' => '标题最少字数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 100],
+ ['key' => 'post_article_title_strlen_limit_max', 'label' => '标题最多字数', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 200],
+ ['key' => 'post_t_placeholder', 'label' => '标题占位符', 'type' => 'input', 'default' => '', 'description' => '留空使用默认'],
+ ['key' => 'post_c_placeholder', 'label' => '内容占位符', 'type' => 'textarea', 'default' => '', 'description' => '留空使用默认'],
+ ]],
+ ['key' => 'fields', 'title' => '表单字段', 'fields' => [
+ ['key' => 'front_submit_title', 'label' => '标题', 'type' => 'switch', 'default' => true],
+ ['key' => 'front_submit_content', 'label' => '正文', 'type' => 'switch', 'default' => true],
+ ['key' => 'front_submit_excerpt', 'label' => '摘要', 'type' => 'switch', 'default' => true],
+ ['key' => 'front_submit_cover', 'label' => '封面图', 'type' => 'switch', 'default' => true],
+ ['key' => 'front_submit_category', 'label' => '分类选择', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_article_cat', 'label' => '允许投稿分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => [], 'help' => '为空则允许全部分类'],
+ ['key' => 'post_article_user', 'label' => '免登录归属用户', 'type' => 'user_select', 'default' => 0, 'help' => '免登录投稿时文章归属的用户,搜索选择'],
+ ['key' => 'front_submit_tags', 'label' => '标签输入', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_article_topics_s', 'label' => '专题选择', 'type' => 'switch', 'default' => false, 'help' => '投稿时可选择专题分类'],
+ ['key' => 'post_article_custom_filter_s', 'label' => '高级筛选选择', 'type' => 'switch', 'default' => true, 'help' => '投稿时可选择自定义筛选维度值'],
+ ['key' => 'front_submit_custom_fields', 'label' => '自定义字段', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'review', 'title' => '审核策略', 'fields' => [
+ ['key' => 'front_submit_review_mode', 'label' => '审核模式', 'type' => 'radio', 'default' => 'pending', 'options' => [
+ ['label' => '待审核', 'value' => 'pending'],
+ ['label' => '直接发布', 'value' => 'publish'],
+ ['label' => '自动草稿', 'value' => 'draft'],
+ ]],
+ ['key' => 'front_submit_cover_required', 'label' => '封面图必填', 'type' => 'switch', 'default' => false],
+ ['key' => 'front_submit_max_images', 'label' => '正文最大图片数', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 50],
+ ]],
+ ['key' => 'editor', 'title' => '编辑器设置', 'fields' => [
+ ['key' => 'front_submit_editor_mode', 'label' => '编辑器模式', 'type' => 'radio', 'default' => 'rich', 'options' => [
+ ['label' => '富文本', 'value' => 'rich'],
+ ['label' => 'Markdown', 'value' => 'markdown'],
+ ['label' => '纯文本', 'value' => 'plain'],
+ ]],
+ ['key' => 'front_submit_draft_save', 'label' => '自动保存草稿', 'type' => 'switch', 'default' => true],
+ ['key' => 'front_submit_leave_warning', 'label' => '离开页面提醒', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'paid', 'title' => '付费投稿权限', 'fields' => [
+ ['key' => 'post_article_pay_free_s', 'label' => '允许设置免费', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_article_pay_price_min', 'label' => '最低价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最低付费金额'],
+ ['key' => 'post_article_pay_price_max', 'label' => '最高价格', 'type' => 'number', 'default' => 100, 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最高付费金额'],
+ ['key' => 'post_article_pay_vip_price_s', 'label' => '允许设置会员价', 'type' => 'switch', 'default' => true],
+ ['key' => 'post_article_pay_vip_1_discount', 'label' => 'VIP1 折扣(%)', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100, 'help' => 'VIP1 会员折扣百分比,80 即 8 折'],
+ ['key' => 'post_article_pay_vip_2_discount', 'label' => 'VIP2 折扣(%)', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 100, 'help' => 'VIP2 会员折扣百分比,50 即 5 折'],
+ ]],
+ ],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-shop-basic.php b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-basic.php
new file mode 100644
index 00000000..472cae19
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-basic.php
@@ -0,0 +1,37 @@
+ 'basic',
+ 'title' => '商城基础',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'general', 'title' => '商城开关', 'fields' => [
+ ['key' => 'shop_s', 'label' => '启用商城', 'type' => 'switch', 'default' => false],
+ ['key' => 'shop_home_url', 'label' => '商城首页链接', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'breadcrumbs', 'title' => '面包屑', 'fields' => [
+ ['key' => 'shop_breadcrumbs_s', 'label' => '显示面包屑', 'type' => 'switch', 'default' => true],
+ ['key' => 'shop_breadcrumbs_home', 'label' => '显示首页链接', 'type' => 'switch', 'default' => true],
+ ['key' => 'shop_breadcrumbs_shop_home', 'label' => '显示商城首页', 'type' => 'switch', 'default' => true],
+ ['key' => 'shop_breadcrumbs_shop_home_name', 'label' => '商城首页名称', 'type' => 'input', 'default' => '商城首页'],
+ ]],
+ ['key' => 'rewrite', 'title' => 'URL 别名', 'fields' => [
+ ['key' => 'shop_product_rewrite_slug', 'label' => '商品 URL 别名', 'type' => 'input', 'default' => 'product'],
+ ['key' => 'shop_cart_rewrite_slug', 'label' => '购物车 URL 别名', 'type' => 'input', 'default' => 'cart'],
+ ['key' => 'shop_rewrite_suffix_html_s', 'label' => '.html 伪静态后缀', 'type' => 'switch', 'default' => false],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-shop-contact.php b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-contact.php
new file mode 100644
index 00000000..23304ba1
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-contact.php
@@ -0,0 +1,30 @@
+ 'contact',
+ 'title' => '客服配置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'button', 'title' => '客服按钮', 'fields' => [
+ ['key' => 'shop_contact_msg_s', 'label' => '显示客服按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'shop_contact_name', 'label' => '按钮文案', 'type' => 'input', 'default' => '联系客服'],
+ ['key' => 'shop_contact_note', 'label' => '客服备注', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'channels', 'title' => '联系方式', 'fields' => [
+ ['key' => 'shop_contact_more', 'label' => '多客服联系方式', 'type' => 'textarea', 'default' => '', 'help' => '每行一个:名称|图标|链接|二维码图片 URL'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-shop-list.php b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-list.php
new file mode 100644
index 00000000..79b72a8b
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-list.php
@@ -0,0 +1,51 @@
+ 'list',
+ 'title' => '商品列表',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'pagination', 'title' => '列表分页', 'fields' => [
+ ['key' => 'shop_list_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'date', 'options' => [
+ ['label' => '最新发布', 'value' => 'date'],
+ ['label' => '销量最高', 'value' => 'sales'],
+ ['label' => '价格最低', 'value' => 'price_asc'],
+ ['label' => '价格最高', 'value' => 'price_desc'],
+ ['label' => '评价最多', 'value' => 'comments'],
+ ]],
+ ['key' => 'shop_list_count', 'label' => '每页数量', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 100],
+ ['key' => 'shop_list_paging', 'label' => '翻页模式', 'type' => 'radio', 'default' => 'number', 'options' => [
+ ['label' => '数字翻页', 'value' => 'number'],
+ ['label' => 'AJAX 加载', 'value' => 'ajax'],
+ ]],
+ ['key' => 'shop_list_ias_s', 'label' => '滚动自动加载', 'type' => 'switch', 'default' => false],
+ ['key' => 'shop_list_ias_max', 'label' => '自动加载页数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 50],
+ ]],
+ ['key' => 'style', 'title' => '列表样式', 'fields' => [
+ ['key' => 'shop_list_style', 'label' => '卡片样式', 'type' => 'select', 'default' => 'default', 'options' => [
+ ['label' => '默认', 'value' => 'default'],
+ ['label' => '竖向大卡片', 'value' => 'card-lg'],
+ ['label' => '横向小卡片', 'value' => 'card-sm'],
+ ]],
+ ['key' => 'shop_sales_show', 'label' => '销量显示', 'type' => 'select', 'default' => 'show', 'options' => [
+ ['label' => '显示', 'value' => 'show'],
+ ['label' => '不显示', 'value' => 'hide'],
+ ['label' => '超量显示', 'value' => 'threshold'],
+ ]],
+ ['key' => 'shop_sales_show_min', 'label' => '起显阈值', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 99999],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-shop-product-defaults.php b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-product-defaults.php
new file mode 100644
index 00000000..9b30f489
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-product-defaults.php
@@ -0,0 +1,46 @@
+ 'product-defaults',
+ 'title' => '商品默认参数',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'policy', 'title' => '购买策略', 'fields' => [
+ ['key' => 'shop_limit_buy', 'label' => '默认限购', 'type' => 'number', 'default' => -1, 'min' => -1, 'max' => 9999, 'help' => '-1 不限,0 禁购,正数为限购数量'],
+ ['key' => 'shop_guest_buy', 'label' => '免登录购买', 'type' => 'switch', 'default' => false],
+ ['key' => 'shop_email_fill', 'label' => '邮箱必留', 'type' => 'switch', 'default' => false],
+ ['key' => 'shop_rebate', 'label' => '允许返佣', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'shipping', 'title' => '运费默认', 'fields' => [
+ ['key' => 'shop_shipping_fee', 'label' => '默认运费类型', 'type' => 'select', 'default' => 'free', 'options' => [
+ ['label' => '包邮', 'value' => 'free'],
+ ['label' => '固定运费', 'value' => 'fixed'],
+ ['label' => '按金额计算', 'value' => 'amount'],
+ ]],
+ ]],
+ ['key' => 'afterSale', 'title' => '售后与服务', 'fields' => [
+ ['key' => 'shop_after_sale', 'label' => '默认售后政策', 'type' => 'select', 'default' => 'refund_return', 'options' => [
+ ['label' => '仅退款', 'value' => 'refund'],
+ ['label' => '退货退款', 'value' => 'refund_return'],
+ ['label' => '不支持售后', 'value' => 'none'],
+ ]],
+ ['key' => 'shop_service', 'label' => '服务保障', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'params', 'title' => '默认参数模板', 'fields' => [
+ ['key' => 'shop_product_params_default', 'label' => '商品参数模板', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-shop-shipping-config.php b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-shipping-config.php
new file mode 100644
index 00000000..cff4e1d0
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-shipping-config.php
@@ -0,0 +1,41 @@
+ 'shipping-config',
+ 'title' => '物流配置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'address', 'title' => '收货地址', 'fields' => [
+ ['key' => 'shop_manual_address', 'label' => '手动填写收货地址', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'express', 'title' => '快递查询', 'fields' => [
+ ['key' => 'shop_express_api_sdk', 'label' => '快递查询接口', 'type' => 'select', 'default' => 'kuaidi100', 'options' => [
+ ['label' => '快递100', 'value' => 'kuaidi100'],
+ ['label' => '阿里云', 'value' => 'aliyun'],
+ ['label' => '快递鸟', 'value' => 'kdniao'],
+ ]],
+ ['key' => 'shop_express_query_interval', 'label' => '查询刷新间隔(分钟)', 'type' => 'number', 'default' => 60, 'min' => 30, 'max' => 240],
+ ]],
+ ['key' => 'credentials', 'title' => '接口凭据', 'fields' => [
+ ['key' => 'shop_express_kdniao_opt', 'label' => '快递鸟凭据', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'shop_express_kuaidi100_opt', 'label' => '快递100凭据', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'shop_express_aliyun_opt', 'label' => '阿里云凭据', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'companies', 'title' => '物流公司', 'fields' => [
+ ['key' => 'shop_shipping_company', 'label' => '物流公司列表', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-shop-single.php b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-single.php
new file mode 100644
index 00000000..500ae941
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-single.php
@@ -0,0 +1,39 @@
+ 'single',
+ 'title' => '商品详情',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'layout', 'title' => '详情页布局', 'fields' => [
+ ['key' => 'shop_content_layout', 'label' => '内容布局', 'type' => 'select', 'default' => 'adapt', 'options' => [
+ ['label' => '适应宽度', 'value' => 'adapt'],
+ ['label' => '全屏', 'value' => 'full'],
+ ['label' => '适应宽度+侧栏', 'value' => 'sidebar'],
+ ]],
+ ['key' => 'shop_content_show_bg', 'label' => '详情背景盒子', 'type' => 'switch', 'default' => false],
+ ['key' => 'shop_single_tab', 'label' => '自定义 TAB 栏目', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'shop_content_after', 'label' => '详情页底部内容', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'related', 'title' => '相关推荐', 'fields' => [
+ ['key' => 'shop_single_related_s', 'label' => '显示相关推荐', 'type' => 'switch', 'default' => true],
+ ['key' => 'shop_single_related_opt', 'label' => '推荐配置', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'image', 'title' => '商品图片', 'fields' => [
+ ['key' => 'shop_main_image_default', 'label' => '默认商品主图', 'type' => 'media', 'default' => ''],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-shop-timing.php b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-timing.php
new file mode 100644
index 00000000..aaafe59f
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-shop-timing.php
@@ -0,0 +1,38 @@
+ 'timing',
+ 'title' => '时效与售后原因',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'receipt', 'title' => '确认收货', 'fields' => [
+ ['key' => 'shop_order_receipt_max_day', 'label' => '自动确认收货(天)', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 60],
+ ]],
+ ['key' => 'comment', 'title' => '评价时效', 'fields' => [
+ ['key' => 'shop_comment_max_day', 'label' => '评价时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 60],
+ ['key' => 'shop_comment_placeholder', 'label' => '评价占位文案', 'type' => 'input', 'default' => ''],
+ ['key' => 'shop_comment_img_num', 'label' => '评价图片数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 9],
+ ]],
+ ['key' => 'afterSale', 'title' => '售后退货', 'fields' => [
+ ['key' => 'shop_after_sale_return_express_max_day', 'label' => '退货快递时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 30],
+ ['key' => 'shop_after_sale_reason_refund', 'label' => '仅退款原因', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'shop_after_sale_reason_return', 'label' => '退货退款原因', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ['key' => 'author', 'title' => '商家信息', 'fields' => [
+ ['key' => 'shop_author_show', 'label' => '显示商家信息', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-shop.php b/inc/classes/admin/schema/class-aksrt-admin-schema-shop.php
new file mode 100644
index 00000000..0a2e3ddb
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-shop.php
@@ -0,0 +1,28 @@
+ 'shop',
+ 'title' => '商城&商品',
+ 'version' => '1.0.0',
+ 'sections' => [
+ 'basic' => AKSRT_Admin_Schema_Shop_Basic::build(),
+ 'list' => AKSRT_Admin_Schema_Shop_List::build(),
+ 'contact' => AKSRT_Admin_Schema_Shop_Contact::build(),
+ 'single' => AKSRT_Admin_Schema_Shop_Single::build(),
+ 'product-defaults' => AKSRT_Admin_Schema_Shop_Product_Defaults::build(),
+ 'shipping-config' => AKSRT_Admin_Schema_Shop_Shipping_Config::build(),
+ 'timing' => AKSRT_Admin_Schema_Shop_Timing::build(),
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-tools-backup.php b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-backup.php
new file mode 100644
index 00000000..cd07b7af
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-backup.php
@@ -0,0 +1,32 @@
+ 'backup',
+ 'title' => '数据备份',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'backup', 'title' => '备份设置', 'fields' => [
+ ['key' => 'backup_auto', 'label' => '自动备份', 'type' => 'switch', 'help' => '启用自动定时备份'],
+ ['key' => 'backup_frequency', 'label' => '备份频率', 'type' => 'select', 'default' => 'weekly', 'options' => [
+ ['label' => '每天', 'value' => 'daily'],
+ ['label' => '每周', 'value' => 'weekly'],
+ ['label' => '每月', 'value' => 'monthly'],
+ ]],
+ ['key' => 'backup_retain', 'label' => '保留份数', 'type' => 'number', 'default' => 7, 'min' => 3, 'max' => 20],
+ ['key' => 'backup_now', 'label' => '立即备份', 'type' => 'button', 'help' => '手动触发一次完整备份'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-tools-email-rollback.php b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-email-rollback.php
new file mode 100644
index 00000000..78eded83
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-email-rollback.php
@@ -0,0 +1,32 @@
+ 'email-rollback',
+ 'title' => '邮件回滚',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'rollback', 'title' => '邮件回滚', 'fields' => [
+ ['key' => 'rollback_type', 'label' => '回滚类型', 'type' => 'select', 'options' => [
+ ['label' => '按时间范围', 'value' => 'date_range'],
+ ['label' => '按邮件类型', 'value' => 'email_type'],
+ ['label' => '全部回滚', 'value' => 'all'],
+ ]],
+ ['key' => 'rollback_date_from', 'label' => '开始日期', 'type' => 'date'],
+ ['key' => 'rollback_date_to', 'label' => '结束日期', 'type' => 'date'],
+ ['key' => 'rollback_execute', 'label' => '执行回滚', 'type' => 'button', 'danger' => true, 'help' => '此操作不可逆'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-tools-health.php b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-health.php
new file mode 100644
index 00000000..2ddc5c6a
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-health.php
@@ -0,0 +1,29 @@
+ 'health',
+ 'title' => '健康检查',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'check', 'title' => '环境检查', 'fields' => [
+ ['key' => 'php_version', 'label' => 'PHP 版本', 'type' => 'display', 'help' => '建议 7.4+'],
+ ['key' => 'wp_version', 'label' => 'WordPress 版本', 'type' => 'display', 'help' => '建议 5.8+'],
+ ['key' => 'memory_limit', 'label' => '内存限制', 'type' => 'display', 'help' => '建议 256M+'],
+ ['key' => 'upload_max_size', 'label' => '上传限制', 'type' => 'display'],
+ ['key' => 'extensions', 'label' => '必要扩展', 'type' => 'display', 'help' => 'curl, mbstring, json, gd'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-tools-import-export.php b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-import-export.php
new file mode 100644
index 00000000..a4c23437
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-import-export.php
@@ -0,0 +1,29 @@
+ 'import-export',
+ 'title' => '导入导出',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'export', 'title' => '导出配置', 'fields' => [
+ ['key' => 'export_settings', 'label' => '导出主题设置', 'type' => 'button', 'help' => '导出当前所有主题配置为 JSON 文件'],
+ ['key' => 'export_options', 'label' => '导出 WordPress 选项', 'type' => 'button', 'help' => '导出 aksrt_settings 等选项'],
+ ]],
+ ['key' => 'import', 'title' => '导入配置', 'fields' => [
+ ['key' => 'import_settings', 'label' => '导入主题设置', 'type' => 'file', 'accept' => '.json', 'help' => '导入主题配置文件(将覆盖当前设置)'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-tools-migration.php b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-migration.php
new file mode 100644
index 00000000..306c1fc3
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-tools-migration.php
@@ -0,0 +1,27 @@
+ 'migration',
+ 'title' => '数据迁移',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'status', 'title' => '迁移状态', 'fields' => [
+ ['key' => 'current_version', 'label' => '当前版本', 'type' => 'display'],
+ ['key' => 'pending_migrations', 'label' => '待执行迁移', 'type' => 'display'],
+ ['key' => 'run_migration', 'label' => '执行迁移', 'type' => 'button', 'help' => '手动触发数据库迁移'],
+ ]],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-tools.php b/inc/classes/admin/schema/class-aksrt-admin-schema-tools.php
new file mode 100644
index 00000000..363ed30f
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-tools.php
@@ -0,0 +1,25 @@
+ 'tools',
+ 'title' => '工具&诊断',
+ 'sections' => [
+ 'import-export' => AKSRT_Admin_Schema_Tools_Import_Export::build(),
+ 'health' => AKSRT_Admin_Schema_Tools_Health::build(),
+ 'migration' => AKSRT_Admin_Schema_Tools_Migration::build(),
+ 'backup' => AKSRT_Admin_Schema_Tools_Backup::build(),
+ 'email-rollback' => AKSRT_Admin_Schema_Tools_Email_Rollback::build(),
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-users.php b/inc/classes/admin/schema/class-aksrt-admin-schema-users.php
new file mode 100644
index 00000000..ad3215b2
--- /dev/null
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-users.php
@@ -0,0 +1,697 @@
+ 'users',
+ 'title' => '用户&互动',
+ 'sections' => [
+ 'auth' => [
+ 'key' => 'auth',
+ 'title' => '注册登录',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'register', 'title' => '注册设置', 'fields' => [
+ ['key' => 'close_sign', 'label' => '一键禁用登录注册', 'type' => 'switch', 'default' => false, 'desc' => '开启后前台禁用全部注册/登录入口(含社交登录),不影响后台登录'],
+ ['key' => 'user_close_signup', 'label' => '关闭注册', 'type' => 'switch', 'default' => false],
+ ['key' => 'close_signin', 'label' => '禁用账密登录', 'type' => 'switch', 'default' => false, 'desc' => '仅允许社交账号登录'],
+ ['key' => 'user_agreement_s', 'label' => '用户协议', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_agreement_page', 'label' => '协议页面', 'type' => 'input', 'default' => ''],
+ ['key' => 'user_privacy_s', 'label' => '隐私政策', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_privacy_page', 'label' => '隐私页面', 'type' => 'input', 'default' => ''],
+ ['key' => 'user_agreement_check', 'label' => '强制勾选协议', 'type' => 'switch', 'default' => true],
+ ['key' => 'no_repetition_name', 'label' => '禁止重复昵称', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_nickname_out', 'label' => '昵称黑名单关键词', 'type' => 'textarea', 'default' => "赌博,博彩,代发,代理,刷单,博彩,六合彩,admin,test,管理员,客服", 'desc' => '一行一个关键词,注册昵称包含关键词将被拒绝'],
+ ['key' => 'user_email_limit_type', 'label' => '邮箱限制类型', 'type' => 'radio', 'default' => 'black', 'options' => [
+ ['label' => '黑名单(拒绝以下后缀)', 'value' => 'black'],
+ ['label' => '白名单(仅允许以下后缀)', 'value' => 'white'],
+ ]],
+ ['key' => 'user_email_out', 'label' => '邮箱限制后缀', 'type' => 'textarea', 'default' => "@tempmail.com\n@throwaway.email\n@guerrillamail.com", 'desc' => '一行一个邮箱后缀,如 @tempmail.com'],
+ ]],
+ ['key' => 'login', 'title' => '登录设置', 'fields' => [
+ ['key' => 'user_close_login', 'label' => '关闭登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_sign_type', 'label' => '登录注册样式', 'type' => 'radio', 'default' => 'page', 'options' => [
+ ['label' => '弹窗', 'value' => 'modal'],
+ ['label' => '跳转页面', 'value' => 'page'],
+ ]],
+ ['key' => 'sign_modal_auto_s', 'label' => '自动弹出登录窗', 'type' => 'switch', 'default' => false, 'desc' => '仅在 user_sign_type=modal 时生效'],
+ ['key' => 'sign_modal_auto_waiting_time', 'label' => '弹窗等待秒数', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 60, 'desc' => '页面加载多少秒后弹出登录窗'],
+ ['key' => 'sign_modal_auto_expires', 'label' => '弹窗周期(小时)', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 720, 'desc' => '关闭弹窗后多少小时内不再自动弹出,0=每次刷新都弹'],
+ ['key' => 'replace_wp_login', 'label' => '替换WP登录页', 'type' => 'switch', 'default' => true],
+ ['key' => 'admin_sign_key', 'label' => '应急登录 Key', 'type' => 'input', 'default' => '', 'desc' => '留空为禁用。管理员可通过 /wp-login.php?aksrt_bypass= 绕过前台替换'],
+ ['key' => 'user_signin_phone_s', 'label' => '手机号登录', 'type' => 'switch', 'default' => false, 'desc' => '允许用手机号作为用户名登录'],
+ ['key' => 'user_signin_nopas_s', 'label' => '免密登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_signin_nopas_type', 'label' => '免密登录方式', 'type' => 'radio', 'default' => 'email', 'options' => [
+ ['label' => '邮箱验证码', 'value' => 'email'],
+ ['label' => '手机验证码', 'value' => 'phone'],
+ ['label' => '邮箱或手机', 'value' => 'email_phone'],
+ ]],
+ ['key' => 'user_signin_nopas_active', 'label' => '免密显示模式', 'type' => 'radio', 'default' => 'nopas_first', 'options' => [
+ ['label' => '优先免密', 'value' => 'nopas_first'],
+ ['label' => '优先密码', 'value' => 'password_first'],
+ ['label' => '仅免密', 'value' => 'nopas_only'],
+ ]],
+ ]],
+ ['key' => 'signup_captcha', 'title' => '注册验证', 'fields' => [
+ ['key' => 'user_signup_captch', 'label' => '注册需验证', 'type' => 'switch', 'default' => true, 'desc' => '注册时需完成邮箱或手机验证'],
+ ['key' => 'captch_type', 'label' => '验证方式', 'type' => 'radio', 'default' => 'email', 'options' => [
+ ['label' => '邮箱', 'value' => 'email'],
+ ['label' => '手机', 'value' => 'phone'],
+ ['label' => '邮箱或手机', 'value' => 'email_phone'],
+ ]],
+ ['key' => 'user_signup_no_repas', 'label' => '无需重复密码', 'type' => 'switch', 'default' => false, 'desc' => '注册时不要求二次确认密码'],
+ ['key' => 'user_repas_captch_type', 'label' => '找回密码验证方式', 'type' => 'radio', 'default' => 'email', 'options' => [
+ ['label' => '邮箱', 'value' => 'email'],
+ ['label' => '手机', 'value' => 'phone'],
+ ['label' => '邮箱或手机', 'value' => 'email_phone'],
+ ]],
+ ]],
+ ['key' => 'bind', 'title' => '绑定设置', 'fields' => [
+ ['key' => 'bind_phone', 'label' => '绑定手机', 'type' => 'switch', 'default' => false],
+ ['key' => 'bind_reminder', 'label' => '绑定提醒', 'type' => 'radio', 'default' => 'close', 'options' => [
+ ['label' => '提醒绑定邮箱', 'value' => 'email'],
+ ['label' => '提醒绑定手机', 'value' => 'phone'],
+ ['label' => '提醒绑定邮箱和手机', 'value' => 'email_phone'],
+ ['label' => '关闭', 'value' => 'close'],
+ ]],
+ ['key' => 'bind_reminder_text', 'label' => '提醒文案', 'type' => 'input', 'default' => '请绑定手机号以正常使用'],
+ ['key' => 'bind_reminder_expires', 'label' => '提醒周期(小时)', 'type' => 'number', 'default' => 24, 'min' => 1, 'max' => 720, 'desc' => '用户关闭提醒后多少小时内不再提醒'],
+ ['key' => 'mandatory_bind', 'label' => '强制绑定', 'type' => 'radio', 'default' => 'close', 'options' => [
+ ['label' => '强制绑定邮箱', 'value' => 'email'],
+ ['label' => '强制绑定手机', 'value' => 'phone'],
+ ['label' => '强制绑定邮箱和手机', 'value' => 'email_phone'],
+ ['label' => '关闭', 'value' => 'close'],
+ ]],
+ ['key' => 'email_set_captch', 'label' => '绑定邮箱需验证', 'type' => 'switch', 'default' => true, 'desc' => '绑定邮箱时需发送验证码确认'],
+ ]],
+ ['key' => 'invite_inline', 'title' => '邀请码注册', 'fields' => [
+ ['key' => 'invit_code_s', 'label' => '启用邀请码', 'type' => 'switch', 'default' => false, 'desc' => '与「邀请码」子页联动,此处开关控制注册表单是否显示邀请码字段'],
+ ['key' => 'invit_code_mode', 'label' => '邀请模式', 'type' => 'select', 'default' => 'open', 'options' => [
+ ['label' => '开放注册(邀请码选填)', 'value' => 'open'],
+ ['label' => '必须邀请码', 'value' => 'must'],
+ ]],
+ ['key' => 'invit_code_open_desc', 'label' => '开放注册提示', 'type' => 'input', 'default' => '邀请码为选填,没有邀请码也可以注册'],
+ ['key' => 'invit_code_must_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入邀请码'],
+ ]],
+ ['key' => 'sign_page_ui', 'title' => '页面 UI 配置', 'fields' => [
+ ['key' => 'sign_page_background', 'label' => '页面背景图', 'type' => 'gallery', 'default' => [], 'desc' => '登录注册页背景图,多图随机'],
+ ['key' => 'sign_theme_mode', 'label' => '主题模式', 'type' => 'radio', 'default' => 'default', 'options' => [
+ ['label' => '默认', 'value' => 'default'],
+ ['label' => '亮色', 'value' => 'light'],
+ ['label' => '暗色', 'value' => 'dark'],
+ ]],
+ ['key' => 'sign_show_header', 'label' => '显示顶部导航', 'type' => 'switch', 'default' => true],
+ ['key' => 'sign_card_position', 'label' => '卡片位置', 'type' => 'radio', 'default' => 'center', 'options' => [
+ ['label' => '靠左', 'value' => 'left'],
+ ['label' => '居中', 'value' => 'center'],
+ ['label' => '靠右', 'value' => 'right'],
+ ]],
+ ['key' => 'sign_show_logo', 'label' => '显示 LOGO', 'type' => 'switch', 'default' => true],
+ ['key' => 'sign_page_footer', 'label' => '页脚 HTML', 'type' => 'textarea', 'default' => '', 'desc' => '登录注册页自定义页脚内容,支持 HTML'],
+ ]],
+ ['key' => 'modal_ui', 'title' => '弹窗 UI 配置', 'fields' => [
+ ['key' => 'sign_modal_background', 'label' => '弹窗背景图', 'type' => 'gallery', 'default' => [], 'desc' => '弹窗左侧背景图,多图随机'],
+ ['key' => 'sign_modal_show_logo', 'label' => '弹窗显示 LOGO', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'sign_logo', 'title' => '登录框 LOGO', 'fields' => [
+ ['key' => 'user_logo', 'label' => '日间 LOGO', 'type' => 'gallery', 'default' => [], 'desc' => '登录框专用日间 LOGO,留空使用站点全局 LOGO'],
+ ['key' => 'user_logo_dark', 'label' => '夜间 LOGO', 'type' => 'gallery', 'default' => [], 'desc' => '登录框专用夜间 LOGO,留空使用站点全局夜间 LOGO'],
+ ]],
+ ],
+ ],
+ 'notifications' => [
+ 'key' => 'notifications',
+ 'title' => '消息通知',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '基础设置', 'fields' => [
+ ['key' => 'message_s', 'label' => '站内通知总开关', 'type' => 'switch', 'default' => true],
+ ['key' => 'msg_center_rewrite_slug', 'label' => '消息中心 URL 别名', 'type' => 'input', 'default' => 'message'],
+ ['key' => 'message_icon_show', 'label' => '通知图标位置', 'type' => 'checkbox', 'default' => ['nav_menu'], 'options' => [
+ ['label' => '导航菜单', 'value' => 'nav_menu'],
+ ['label' => '用户头像', 'value' => 'avatar'],
+ ['label' => '移动端底部', 'value' => 'mobile_bar'],
+ ]],
+ ['key' => 'message_paginate_type', 'label' => '列表翻页模式', 'type' => 'radio', 'default' => 'ajax_lists', 'options' => [
+ ['label' => 'AJAX 翻页', 'value' => 'ajax_lists'],
+ ['label' => '传统翻页', 'value' => 'default'],
+ ]],
+ ['key' => 'message_user_set', 'label' => '允许用户设置推送', 'type' => 'switch', 'default' => true, 'desc' => '允许用户在前台自行设置通知推送偏好'],
+ ]],
+ ['key' => 'types', 'title' => '通知类型', 'fields' => [
+ ['key' => 'message_close_msg_type', 'label' => '关闭部分推送', 'type' => 'checkbox', 'default' => [], 'options' => [
+ ['label' => '文章通知', 'value' => 'posts'],
+ ['label' => '评论回复', 'value' => 'comment'],
+ ['label' => '收藏通知', 'value' => 'favorite'],
+ ['label' => '点赞通知', 'value' => 'like'],
+ ['label' => '热门通知', 'value' => 'hot'],
+ ['label' => '关注通知', 'value' => 'followed'],
+ ['label' => '系统通知', 'value' => 'system'],
+ ['label' => '支付通知', 'value' => 'pay'],
+ ['label' => '提现回复', 'value' => 'withdraw_reply'],
+ ['label' => '勋章通知', 'value' => 'medal'],
+ ['label' => '认证回复', 'value' => 'auth_apply_reply'],
+ ], 'desc' => '勾选的类型将不向用户推送通知'],
+ ]],
+ ['key' => 'auto_clear', 'title' => '自动清理', 'fields' => [
+ ['key' => 'message_auto_clear_s', 'label' => '自动清理已读消息', 'type' => 'switch', 'default' => false],
+ ['key' => 'message_auto_clear_time', 'label' => '过期天数', 'type' => 'number', 'default' => 365, 'min' => 1, 'max' => 3650, 'desc' => '已读消息超过此天数将被自动清理'],
+ ['key' => 'message_auto_clear_types', 'label' => '清理类型', 'type' => 'checkbox', 'default' => ['posts', 'comment', 'favorite', 'like', 'hot', 'followed', 'system'], 'options' => [
+ ['label' => '文章通知', 'value' => 'posts'],
+ ['label' => '评论回复', 'value' => 'comment'],
+ ['label' => '收藏通知', 'value' => 'favorite'],
+ ['label' => '点赞通知', 'value' => 'like'],
+ ['label' => '热门通知', 'value' => 'hot'],
+ ['label' => '关注通知', 'value' => 'followed'],
+ ['label' => '系统通知', 'value' => 'system'],
+ ['label' => '支付通知', 'value' => 'pay'],
+ ['label' => '提现回复', 'value' => 'withdraw_reply'],
+ ['label' => '勋章通知', 'value' => 'medal'],
+ ['label' => '认证回复', 'value' => 'auth_apply_reply'],
+ ['label' => '下载通知', 'value' => 'download'],
+ ['label' => '积分变动', 'value' => 'credits'],
+ ['label' => '私信', 'value' => 'private'],
+ ['label' => '举报反馈', 'value' => 'report'],
+ ]],
+ ]],
+ ['key' => 'private', 'title' => '私信设置', 'fields' => [
+ ['key' => 'private_s', 'label' => '启用私信功能', 'type' => 'switch', 'default' => true],
+ ['key' => 'private_smilie_s', 'label' => '支持表情', 'type' => 'switch', 'default' => true],
+ ['key' => 'private_code_s', 'label' => '支持代码', 'type' => 'switch', 'default' => false],
+ ['key' => 'private_image_s', 'label' => '支持图片', 'type' => 'switch', 'default' => true],
+ ['key' => 'private_upload_img', 'label' => '支持上传图片', 'type' => 'switch', 'default' => false],
+ ['key' => 'private_quick_s', 'label' => '快捷回复', 'type' => 'switch', 'default' => true],
+ ['key' => 'private_quick_often', 'label' => '常用快捷回复', 'type' => 'textarea', 'default' => "你好,请问有什么可以帮你的?\n感谢你的反馈\n已处理,请查收", 'desc' => '一行一条快捷回复文案'],
+ ['key' => 'private_submit_text', 'label' => '发送按钮文案', 'type' => 'input', 'default' => '发送'],
+ ['key' => 'private_placeholder', 'label' => '输入占位符', 'type' => 'input', 'default' => '请输入消息...'],
+ ]],
+ ],
+ ],
+ 'oauth' => [
+ 'key' => 'oauth',
+ 'title' => '第三方登录',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '基础设置', 'fields' => [
+ ['key' => 'oauth_s', 'label' => '启用第三方登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'oauth_proxy_mode', 'label' => '代理登录模式', 'type' => 'radio', 'default' => 'close', 'options' => [
+ ['label' => '关闭', 'value' => 'close'],
+ ['label' => '服务端', 'value' => 'server'],
+ ['label' => '客户端', 'value' => 'client'],
+ ]],
+ ['key' => 'oauth_proxy_secret', 'label' => '代理 HMAC 密钥', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'oauth_proxy_mode', 'not' => 'close']],
+ ['key' => 'oauth_proxy_server_url', 'label' => '服务端地址', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'oauth_proxy_mode', 'value' => 'client']],
+ ['key' => 'oauth_proxy_server_providers', 'label' => '服务端允许平台', 'type' => 'checkbox', 'default' => ['github', 'gitee', 'qq', 'google'], 'options' => [
+ ['label' => 'GitHub', 'value' => 'github'],
+ ['label' => 'Gitee', 'value' => 'gitee'],
+ ['label' => 'QQ', 'value' => 'qq'],
+ ['label' => 'Google', 'value' => 'google'],
+ ['label' => '微博', 'value' => 'weibo'],
+ ['label' => '微信', 'value' => 'weixin'],
+ ['label' => '百度', 'value' => 'baidu'],
+ ['label' => '支付宝', 'value' => 'alipay'],
+ ], 'dependsOn' => ['key' => 'oauth_proxy_mode', 'value' => 'server']],
+ ['key' => 'oauth_proxy_client_providers', 'label' => '客户端启用平台', 'type' => 'checkbox', 'default' => ['github', 'gitee', 'qq', 'google'], 'options' => [
+ ['label' => 'GitHub', 'value' => 'github'],
+ ['label' => 'Gitee', 'value' => 'gitee'],
+ ['label' => 'QQ', 'value' => 'qq'],
+ ['label' => 'Google', 'value' => 'google'],
+ ['label' => '微博', 'value' => 'weibo'],
+ ['label' => '微信', 'value' => 'weixin'],
+ ['label' => '百度', 'value' => 'baidu'],
+ ['label' => '支付宝', 'value' => 'alipay'],
+ ], 'dependsOn' => ['key' => 'oauth_proxy_mode', 'value' => 'client']],
+ ['key' => 'oauth_proxy_gzh_type', 'label' => '代理公众号类型', 'type' => 'radio', 'default' => 'service', 'options' => [
+ ['label' => '服务号', 'value' => 'service'],
+ ['label' => '订阅号', 'value' => 'subscribe'],
+ ], 'dependsOn' => ['key' => 'oauth_proxy_mode', 'not' => 'close']],
+ ['key' => 'oauth_bind_type', 'label' => '绑定方式', 'type' => 'radio', 'default' => 'auto', 'options' => [
+ ['label' => '自动绑定', 'value' => 'auto'],
+ ['label' => '手动绑定', 'value' => 'manual'],
+ ]],
+ ['key' => 'oauth_button_lg', 'label' => '大按钮样式', 'type' => 'switch', 'default' => false],
+ ['key' => 'weixingzh_priority', 'label' => 'PC端优先微信扫码', 'type' => 'switch', 'default' => false, 'help' => '开启后 PC 端登录框优先显示微信公众号扫码'],
+ ['key' => 'weixingzh_auto', 'label' => '微信APP内自动登录', 'type' => 'switch', 'default' => false, 'help' => '在微信内置浏览器中自动使用公众号授权登录'],
+ ['key' => 'weixingzh_auto_expires', 'label' => '自动登录频率限制 (秒)', 'type' => 'number', 'default' => 3600, 'min' => 0, 'max' => 86400, 'dependsOn' => ['key' => 'weixingzh_auto', 'value' => true]],
+ ]],
+ ['key' => 'providers', 'title' => '登录提供商', 'fields' => [
+ ['key' => 'oauth_github_s', 'label' => 'GitHub登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'oauth_gitee_s', 'label' => 'Gitee登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'oauth_qq_s', 'label' => 'QQ登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'oauth_weixingzh_s', 'label' => '微信公众号登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'oauth_weixin_s', 'label' => '微信开放平台登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'oauth_weibo_s', 'label' => '微博登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'oauth_baidu_s', 'label' => '百度登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'oauth_alipay_s', 'label' => '支付宝登录', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'github', 'title' => 'GitHub配置', 'fields' => [
+ ['key' => 'github_appid', 'label' => 'Client ID', 'type' => 'input', 'default' => ''],
+ ['key' => 'github_appkey', 'label' => 'Client Secret', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'gitee', 'title' => 'Gitee配置', 'fields' => [
+ ['key' => 'gitee_appid', 'label' => 'Client ID', 'type' => 'input', 'default' => ''],
+ ['key' => 'gitee_appkey', 'label' => 'Client Secret', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'qq', 'title' => 'QQ配置', 'fields' => [
+ ['key' => 'qq_appid', 'label' => 'App ID', 'type' => 'input', 'default' => ''],
+ ['key' => 'qq_appkey', 'label' => 'App Key', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'weixin', 'title' => '微信开放平台配置', 'fields' => [
+ ['key' => 'weixin_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'weixin_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'weixingzh', 'title' => '微信公众号配置', 'fields' => [
+ ['key' => 'weixingzh_gzh_type', 'label' => '公众号类型', 'type' => 'radio', 'default' => 'service', 'options' => [
+ ['label' => '服务号', 'value' => 'service'],
+ ['label' => '订阅号', 'value' => 'subscribe'],
+ ]],
+ ['key' => 'weixingzh_token', 'label' => 'Token', 'type' => 'input', 'default' => ''],
+ ['key' => 'weixingzh_token_nocheck', 'label' => '不验证Token', 'type' => 'switch', 'default' => false, 'help' => '开发调试时可跳过 Token 验证'],
+ ['key' => 'weixingzh_encoding_aes_key', 'label' => 'EncodingAESKey', 'type' => 'input', 'default' => ''],
+ ['key' => 'weixingzh_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'weixingzh_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
+ ['key' => 'weixingzh_code_qrcode', 'label' => '公众号二维码', 'type' => 'media', 'default' => '', 'help' => '引导用户扫码关注登录的二维码图片'],
+ ['key' => 'weixingzh_code_keyword', 'label' => '登录回复关键词', 'type' => 'input', 'default' => '登录', 'help' => '用户在公众号内回复此关键词触发登录验证码'],
+ ['key' => 'weixingzh_code_reply', 'label' => '验证码回复模板', 'type' => 'textarea', 'default' => '您的验证码是:%s,有效期 %d 分钟。', 'help' => '%s=验证码,%d=有效分钟数'],
+ ['key' => 'weixingzh_code_subscribe_reply', 'label' => '新关注回复模板', 'type' => 'textarea', 'default' => '欢迎关注!回复「%s」获取登录验证码。', 'help' => '%s=登录关键词'],
+ ['key' => 'weixingzh_subscribe_msg', 'label' => '关注后回复', 'type' => 'textarea', 'default' => '', 'help' => '用户关注公众号后的自动回复文案'],
+ ['key' => 'weixingzh_new_subscribe_msg', 'label' => '新用户关注回复', 'type' => 'textarea', 'default' => '', 'help' => '首次关注用户的专属回复文案,留空使用通用回复'],
+ ['key' => 'weixingzh_scan_msg', 'label' => '扫码回复', 'type' => 'textarea', 'default' => '', 'help' => '用户扫码关注公众号后的回复文案'],
+ ['key' => 'weixingzh_auto_reply_text', 'label' => '文本自动回复', 'type' => 'textarea', 'default' => '', 'help' => '收到文本消息的自动回复'],
+ ['key' => 'weixingzh_auto_reply_search', 'label' => '搜索回复', 'type' => 'textarea', 'default' => '', 'help' => '未匹配关键词时的搜索提示回复'],
+ ['key' => 'weixingzh_auto_reply_image', 'label' => '图片回复', 'type' => 'textarea', 'default' => '', 'help' => '收到图片消息的自动回复'],
+ ['key' => 'weixingzh_auto_reply_voice', 'label' => '语音回复', 'type' => 'textarea', 'default' => '', 'help' => '收到语音消息的自动回复'],
+ ['key' => 'weixingzh_auto_reply_default', 'label' => '默认回复', 'type' => 'textarea', 'default' => '暂不支持此类型的消息', 'help' => '其他未匹配消息类型的兜底回复'],
+ ['key' => 'weixingzh_gzh_menu', 'label' => '自定义菜单 JSON', 'type' => 'textarea', 'default' => '', 'help' => '微信公众号自定义菜单 JSON 配置,留空不创建菜单'],
+ ]],
+ ['key' => 'weibo', 'title' => '微博配置', 'fields' => [
+ ['key' => 'weibo_appid', 'label' => 'App Key', 'type' => 'input', 'default' => ''],
+ ['key' => 'weibo_appkey', 'label' => 'App Secret', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'baidu', 'title' => '百度配置', 'fields' => [
+ ['key' => 'baidu_appid', 'label' => 'API Key', 'type' => 'input', 'default' => ''],
+ ['key' => 'baidu_appkey', 'label' => 'Secret Key', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'alipay', 'title' => '支付宝配置', 'fields' => [
+ ['key' => 'alipay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'alipay_privatekey', 'label' => '应用私钥', 'type' => 'input', 'default' => ''],
+ ['key' => 'alipay_publickey', 'label' => '支付宝公钥', 'type' => 'input', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'sms' => [
+ 'key' => 'sms',
+ 'title' => '短信接口',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'provider', 'title' => '供应商配置', 'fields' => [
+ ['key' => 'sms_enabled', 'label' => '启用短信', 'type' => 'switch', 'default' => false],
+ ['key' => 'sms_provider', 'label' => '短信服务商', 'type' => 'select', 'default' => 'aliyun', 'options' => [
+ ['label' => '阿里云短信', 'value' => 'aliyun'],
+ ['label' => '腾讯云短信', 'value' => 'tencent'],
+ ['label' => '华为云短信', 'value' => 'huawei'],
+ ['label' => '自定义接口', 'value' => 'custom'],
+ ]],
+ ['key' => 'sms_number_limit_ch', 'label' => '仅发国内', 'type' => 'switch', 'default' => true, 'desc' => '开启后仅支持发送国内短信,关闭后可发送港澳台及国际短信'],
+ ['key' => 'sms_access_key', 'label' => 'AccessKey / SecretId / AppKey', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_access_secret', 'label' => 'AccessSecret / SecretKey / AppSecret', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'aliyun', 'title' => '阿里云配置', 'fields' => [
+ ['key' => 'sms_ali_api_type', 'label' => '接口类型', 'type' => 'radio', 'default' => 'sms', 'options' => [
+ ['label' => '短信服务', 'value' => 'sms'],
+ ['label' => '号码认证', 'value' => 'verify'],
+ ]],
+ ['key' => 'sms_region_id', 'label' => 'RegionId', 'type' => 'input', 'default' => 'cn-hangzhou'],
+ ]],
+ ['key' => 'tencent', 'title' => '腾讯云配置', 'fields' => [
+ ['key' => 'sms_tencent_app_id', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_tencent_app_key', 'label' => 'AppKey', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_tencent_secret_id', 'label' => 'SecretId', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_tencent_secret_key', 'label' => 'SecretKey', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_tencent_region', 'label' => '地域', 'type' => 'input', 'default' => 'ap-guangzhou'],
+ ]],
+ ['key' => 'huawei', 'title' => '华为云配置', 'fields' => [
+ ['key' => 'sms_huawei_sender', 'label' => '通道号 (sender)', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_huawei_signature', 'label' => '短信签名', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_huawei_endpoint', 'label' => 'API Endpoint', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'custom', 'title' => '自定义接口配置', 'fields' => [
+ ['key' => 'sms_custom_url', 'label' => '请求 URL', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_custom_method', 'label' => '请求方法', 'type' => 'select', 'default' => 'POST', 'options' => [
+ ['label' => 'POST', 'value' => 'POST'],
+ ['label' => 'GET', 'value' => 'GET'],
+ ['label' => 'PUT', 'value' => 'PUT'],
+ ]],
+ ['key' => 'sms_custom_auth_type', 'label' => '鉴权方式', 'type' => 'select', 'default' => 'header', 'options' => [
+ ['label' => 'Header', 'value' => 'header'],
+ ]],
+ ['key' => 'sms_custom_auth_key', 'label' => '鉴权 Header 名', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_custom_auth_value', 'label' => '鉴权 Header 值', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_custom_headers', 'label' => '自定义 Headers', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'sms_custom_body', 'label' => '请求 Body', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'sms_custom_success_rule', 'label' => '成功判定规则', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'template', 'title' => '短信模板', 'fields' => [
+ ['key' => 'sms_sign_name', 'label' => '短信签名', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_template_code', 'label' => '验证码模板ID', 'type' => 'input', 'default' => ''],
+ ['key' => 'sms_code_length', 'label' => '验证码长度', 'type' => 'number', 'default' => 6, 'min' => 4, 'max' => 8],
+ ['key' => 'sms_code_expire', 'label' => '验证码有效期(秒)', 'type' => 'number', 'default' => 300, 'min' => 60, 'max' => 600],
+ ]],
+ ['key' => 'rate_limit', 'title' => '限流策略', 'fields' => [
+ ['key' => 'sms_rate_limit', 'label' => '发送间隔(秒)', 'type' => 'number', 'default' => 60, 'min' => 10, 'max' => 300],
+ ['key' => 'sms_daily_limit', 'label' => '每日发送上限', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 100],
+ ]],
+ ['key' => 'triggers', 'title' => '通知场景', 'fields' => [
+ ['key' => 'sms_notify_bind', 'label' => '绑定手机验证', 'type' => 'switch', 'default' => true],
+ ['key' => 'sms_notify_login', 'label' => '短信登录', 'type' => 'switch', 'default' => false],
+ ['key' => 'sms_notify_register', 'label' => '注册验证', 'type' => 'switch', 'default' => true],
+ ['key' => 'sms_notify_reset_password', 'label' => '找回密码', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'signin' => [
+ 'key' => 'signin',
+ 'title' => '签到奖励',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'rules', 'title' => '签到规则', 'fields' => [
+ ['key' => 'signin_enabled', 'label' => '启用签到', 'type' => 'switch', 'default' => true],
+ ['key' => 'signin_reward', 'label' => '每日签到奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
+ ['key' => 'signin_consecutive_bonus', 'label' => '连续签到加成', 'type' => 'switch', 'default' => true],
+ ['key' => 'signin_consecutive_days', 'label' => '加成周期', 'type' => 'number', 'default' => 7, 'min' => 2, 'max' => 30],
+ ['key' => 'signin_bonus_reward', 'label' => '周期奖励', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 100],
+ ]],
+ ['key' => 'display', 'title' => '签到展示', 'fields' => [
+ ['key' => 'signin_header_user_show', 'label' => '导航栏显示签到', 'type' => 'switch', 'default' => true],
+ ['key' => 'signin_modal_auto_s', 'label' => '自动弹出签到', 'type' => 'switch', 'default' => false],
+ ['key' => 'signin_modal_auto_expires', 'label' => '弹出间隔(天)', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 30],
+ ]],
+ ],
+ ],
+ 'credits' => [
+ 'key' => 'credits',
+ 'title' => '积分设置',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '积分基础', 'fields' => [
+ ['key' => 'points_s', 'label' => '启用积分系统', 'type' => 'switch', 'default' => true],
+ ['key' => 'points_pay_s', 'label' => '积分支付', 'type' => 'switch', 'default' => false],
+ ['key' => 'points_rate', 'label' => '积分兑换比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000],
+ ['key' => 'points_name', 'label' => '积分名称', 'type' => 'input', 'default' => '积分'],
+ ['key' => 'points_decimal', 'label' => '积分小数位数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 4],
+ ]],
+ ['key' => 'earn', 'title' => '积分获取', 'fields' => [
+ ['key' => 'points_earn_comment', 'label' => '评论奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
+ ['key' => 'points_earn_post', 'label' => '发布文章奖励', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
+ ['key' => 'points_earn_login', 'label' => '每日登录奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
+ ['key' => 'points_earn_like_received', 'label' => '收到点赞奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
+ ['key' => 'points_earn_invite', 'label' => '邀请注册奖励', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000],
+ ['key' => 'points_earn_daily_limit', 'label' => '每日获取上限', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 9999, 'help' => '0为不限制'],
+ ]],
+ ['key' => 'spend', 'title' => '积分消费', 'fields' => [
+ ['key' => 'points_spend_comment', 'label' => '评论消耗', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
+ ['key' => 'points_spend_download', 'label' => '下载消耗', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
+ ['key' => 'points_spend_view_hidden', 'label' => '查看隐藏内容消耗', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
+ ]],
+ ['key' => 'transfer', 'title' => '积分转账', 'fields' => [
+ ['key' => 'points_transfer_s', 'label' => '启用积分转账', 'type' => 'switch', 'default' => false],
+ ['key' => 'points_service_charge', 'label' => '转账手续费(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 50],
+ ['key' => 'points_transfer_min', 'label' => '最低转账金额', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 9999],
+ ]],
+ ['key' => 'deduction', 'title' => '积分抵扣', 'fields' => [
+ ['key' => 'points_deduction_s', 'label' => '启用积分抵扣', 'type' => 'switch', 'default' => false],
+ ['key' => 'points_deduction_rate', 'label' => '抵扣比例(%)', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 100],
+ ['key' => 'points_deduction_max', 'label' => '最大抵扣比例(%)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 100],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'points_show_balance', 'label' => '导航栏显示余额', 'type' => 'switch', 'default' => true],
+ ['key' => 'points_show_log', 'label' => '显示积分记录', 'type' => 'switch', 'default' => true],
+ ['key' => 'points_expire_days', 'label' => '积分有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效'],
+ ]],
+ ],
+ ],
+ 'invite' => [
+ 'key' => 'invite',
+ 'title' => '邀请码',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'basic', 'title' => '邀请设置', 'fields' => [
+ ['key' => 'invit_code_s', 'label' => '启用邀请码', 'type' => 'switch', 'default' => false],
+ ['key' => 'invit_code_open_desc', 'label' => '开放注册提示', 'type' => 'input', 'default' => '邀请码为选填,没有邀请码也可以注册'],
+ ['key' => 'invit_code_must_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入邀请码'],
+ ['key' => 'invit_code_mode', 'label' => '邀请模式', 'type' => 'select', 'default' => 'open', 'options' => [
+ ['label' => '开放注册(邀请码选填)', 'value' => 'open'],
+ ['label' => '必须邀请码', 'value' => 'must'],
+ ]],
+ ]],
+ ['key' => 'format', 'title' => '码面设置', 'fields' => [
+ ['key' => 'invit_code_length', 'label' => '邀请码长度', 'type' => 'number', 'default' => 8, 'min' => 4, 'max' => 32],
+ ['key' => 'invit_code_prefix', 'label' => '邀请码前缀', 'type' => 'input', 'default' => ''],
+ ['key' => 'invit_code_max_use', 'label' => '单码最大使用次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 9999],
+ ['key' => 'invit_code_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效'],
+ ['key' => 'invit_code_batch', 'label' => '批量生成数量', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 500],
+ ]],
+ ['key' => 'reward', 'title' => '邀请奖励', 'fields' => [
+ ['key' => 'invit_reward_points', 'label' => '邀请奖励积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 1000],
+ ['key' => 'invit_reward_level', 'label' => '邀请奖励等级', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 10],
+ ['key' => 'invit_reward_vip_days', 'label' => '奖励VIP天数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 3650],
+ ['key' => 'invit_invitee_reward', 'label' => '被邀请人奖励', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'display', 'title' => '展示设置', 'fields' => [
+ ['key' => 'invit_code_show_count', 'label' => '显示已使用次数', 'type' => 'switch', 'default' => true],
+ ['key' => 'invit_code_show_remain', 'label' => '显示剩余次数', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'features' => [
+ 'key' => 'features',
+ 'title' => '用户功能',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'center', 'title' => '用户中心', 'fields' => [
+ ['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,posts,comments,credits,favorites'],
+ ['key' => 'user_avatar_upload', 'label' => '头像上传', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
+ ['key' => 'user_city_s', 'label' => '显示城市', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_join_day_my_name', 'label' => '站名', 'type' => 'input', 'default' => '本站', 'desc' => '用于「已加入 X 天」显示中的站点名称'],
+ ['key' => 'user_avatar_default', 'label' => '默认头像', 'type' => 'media', 'default' => ''],
+ ['key' => 'user_cover_img', 'label' => '默认封面', 'type' => 'media', 'default' => ''],
+ ['key' => 'user_rewrite_slug', 'label' => '用户页slug', 'type' => 'input', 'default' => 'user'],
+ ['key' => 'user_follow_s', 'label' => '启用关注功能', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_message_s', 'label' => '启用私信功能', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_online_s', 'label' => '显示在线状态', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_last_active_s', 'label' => '显示最后活跃', 'type' => 'switch', 'default' => true],
+ ]],
+ ['key' => 'profile', 'title' => '资料页字段', 'fields' => [
+ ['key' => 'user_profile_nickname', 'label' => '昵称', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_profile_email', 'label' => '邮箱', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_profile_url', 'label' => '个人主页', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_profile_desc', 'label' => '个人简介', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_profile_avatar', 'label' => '头像', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_profile_cover', 'label' => '封面图', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_profile_qq', 'label' => 'QQ', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_wechat', 'label' => '微信', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_weibo', 'label' => '微博', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_github', 'label' => 'GitHub', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_bilibili', 'label' => 'B站', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_twitter', 'label' => 'Twitter', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_linkedin', 'label' => 'LinkedIn', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_facebook', 'label' => 'Facebook', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_telegram', 'label' => 'Telegram', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_douyin', 'label' => '抖音', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_profile_zhihu', 'label' => '知乎', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'card', 'title' => '用户卡片', 'fields' => [
+ ['key' => 'user_card_avatar', 'label' => '显示头像', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_card_name', 'label' => '显示用户名', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_card_desc', 'label' => '显示简介', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_card_count', 'label' => '显示统计', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_card_follow', 'label' => '显示关注按钮', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_card_position', 'label' => '卡片位置', 'type' => 'radio', 'default' => 'right', 'options' => [
+ ['label' => '右侧', 'value' => 'right'],
+ ['label' => '左侧', 'value' => 'left'],
+ ['label' => '顶部', 'value' => 'top'],
+ ]],
+ ['key' => 'user_card_style', 'label' => '卡片样式', 'type' => 'radio', 'default' => 'default', 'options' => [
+ ['label' => '默认', 'value' => 'default'],
+ ['label' => '简洁', 'value' => 'simple'],
+ ['label' => '详细', 'value' => 'detail'],
+ ]],
+ ['key' => 'user_card_bg', 'label' => '卡片背景', 'type' => 'media', 'default' => ''],
+ ['key' => 'user_card_avatar_size', 'label' => '头像大小', 'type' => 'number', 'default' => 80, 'min' => 40, 'max' => 200],
+ ]],
+ ['key' => 'privacy', 'title' => '隐私设置', 'fields' => [
+ ['key' => 'user_privacy_posts', 'label' => '隐藏文章', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_privacy_comments', 'label' => '隐藏评论', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_privacy_favorites', 'label' => '隐藏收藏', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_privacy_credits', 'label' => '隐藏积分', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_privacy_level', 'label' => '隐藏等级', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_privacy_badge', 'label' => '隐藏徽章', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_privacy_online', 'label' => '隐藏在线状态', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_privacy_last_active', 'label' => '隐藏最后活跃', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_privacy_ip', 'label' => '隐藏IP归属地', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_privacy_location', 'label' => '隐藏位置', 'type' => 'switch', 'default' => false],
+ ]],
+ ['key' => 'rewards', 'title' => '打赏设置', 'fields' => [
+ ['key' => 'post_rewards_s', 'label' => '启用文章打赏', 'type' => 'switch', 'default' => false],
+ ['key' => 'post_rewards_text', 'label' => '打赏文案', 'type' => 'input', 'default' => ''],
+ ['key' => 'post_rewards_img', 'label' => '打赏二维码', 'type' => 'media', 'default' => ''],
+ ['key' => 'post_rewards_btn_text', 'label' => '打赏按钮文案', 'type' => 'input', 'default' => '赞赏'],
+ ['key' => 'post_rewards_amounts', 'label' => '打赏金额', 'type' => 'input', 'default' => '5,10,20,50,100'],
+ ['key' => 'post_rewards_custom', 'label' => '自定义金额', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ 'verify' => [
+ 'key' => 'verify',
+ 'title' => '身份认证',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'display', 'title' => '公开展示', 'fields' => [
+ ['key' => 'user_verify_enabled', 'label' => '启用身份认证', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_verify_public_display', 'label' => '公开页显示认证徽标', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_verify_badge_text', 'label' => '认证徽标文案', 'type' => 'input', 'default' => '已认证'],
+ ]],
+ ['key' => 'workflow', 'title' => '审核流程', 'fields' => [
+ ['key' => 'user_verify_accept_email_verified', 'label' => '邮箱验证通过视为已认证', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_verify_require_manual_review', 'label' => '保留人工审核位', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_verify_apply_option', 'label' => '申请选项', 'type' => 'textarea', 'default' => "real_name|实名认证\norganization|企业认证"],
+ ]],
+ ['key' => 'restrict', 'title' => '限制设置', 'fields' => [
+ ['key' => 'user_verify_disable_desc', 'label' => '未认证限制说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'user_verify_limit_level', 'label' => '限制等级', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
+ ['key' => 'user_verify_desc', 'label' => '认证说明', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'level' => [
+ 'key' => 'level',
+ 'title' => '用户等级',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'display', 'title' => '等级显示', 'fields' => [
+ ['key' => 'user_level_enabled', 'label' => '启用等级系统', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_level_public_display', 'label' => '公开页显示等级', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_level_prefix', 'label' => '等级前缀', 'type' => 'input', 'default' => 'Lv.'],
+ ['key' => 'user_level_color', 'label' => '等级颜色', 'type' => 'color', 'default' => '#f04494'],
+ ['key' => 'user_level_icon', 'label' => '等级图标', 'type' => 'icon', 'default' => 'fas fa-star'],
+ ]],
+ ['key' => 'rules', 'title' => '等级规则', 'fields' => [
+ ['key' => 'user_level_source', 'label' => '等级来源', 'type' => 'radio', 'default' => 'credits_balance'],
+ ['key' => 'user_level_thresholds', 'label' => '等级阈值', 'type' => 'textarea', 'default' => "1|0\n2|30\n3|100\n4|300"],
+ ['key' => 'user_level_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 99],
+ ['key' => 'user_level_decay', 'label' => '等级衰减', 'type' => 'switch', 'default' => false, 'help' => '长期不活跃自动降级'],
+ ['key' => 'user_level_decay_days', 'label' => '衰减周期(天)', 'type' => 'number', 'default' => 90, 'min' => 7, 'max' => 365],
+ ]],
+ ['key' => 'privilege', 'title' => '等级特权', 'fields' => [
+ ['key' => 'user_level_download_limit', 'label' => '下载限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户每日下载次数增加'],
+ ['key' => 'user_level_upload_limit', 'label' => '上传限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户可上传更大文件'],
+ ['key' => 'user_level_comment_cooldown', 'label' => '评论冷却缩短', 'type' => 'switch', 'default' => false, 'help' => '高等级用户评论间隔更短'],
+ ['key' => 'user_level_custom_title', 'label' => '自定义头衔', 'type' => 'switch', 'default' => false, 'help' => '允许高等级用户自定义头衔'],
+ ]],
+ ['key' => 'notify', 'title' => '升级通知', 'fields' => [
+ ['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_level_notify_style', 'label' => '通知方式', 'type' => 'select', 'default' => 'toast', 'options' => [
+ ['label' => '弹窗提示', 'value' => 'toast'],
+ ['label' => '站内信', 'value' => 'message'],
+ ['label' => '两者都有', 'value' => 'both'],
+ ]],
+ ]],
+ ],
+ ],
+ 'badge' => [
+ 'key' => 'badge',
+ 'title' => '用户徽章',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'display', 'title' => '徽章展示', 'fields' => [
+ ['key' => 'user_badge_enabled', 'label' => '启用徽章系统', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_badge_public_display', 'label' => '公开页显示徽章', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_badge_limit', 'label' => '最多显示数量', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 8],
+ ]],
+ ['key' => 'dictionary', 'title' => '徽章字典', 'fields' => [
+ ['key' => 'user_badge_dictionary', 'label' => '徽章列表', 'type' => 'textarea', 'default' => "founder|创始成员|warning\nstaff|站务团队|info"],
+ ]],
+ ],
+ ],
+ 'report' => [
+ 'key' => 'report',
+ 'title' => '禁封/举报',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'entry', 'title' => '前台入口', 'fields' => [
+ ['key' => 'user_report_enabled', 'label' => '启用举报/禁封结构', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_report_public_entry', 'label' => '公开主页展示举报入口', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_report_public_notice', 'label' => '公开显示限制提示', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_report_ban_notice_text', 'label' => '限制提示文案', 'type' => 'input', 'default' => '当前账号处于限制状态,部分互动能力已关闭。'],
+ ]],
+ ['key' => 'taxonomy', 'title' => '举报分类', 'fields' => [
+ ['key' => 'user_report_categories', 'label' => '举报分类', 'type' => 'textarea', 'default' => "spam|垃圾信息\nabuse|人身攻击\ncopyright|版权投诉"],
+ ]],
+ ['key' => 'ban', 'title' => '禁封设置', 'fields' => [
+ ['key' => 'ban_preset_reason', 'label' => '预设封禁原因', 'type' => 'textarea', 'default' => "spam|垃圾信息\nabuse|人身攻击\nviolation|违规操作"],
+ ['key' => 'ban_appeal_desc', 'label' => '申诉说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'ban_appeal_keys', 'label' => '申诉关键词', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'image', 'title' => '图片举报', 'fields' => [
+ ['key' => 'user_report_img_s', 'label' => '启用图片举报', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_report_desc', 'label' => '举报说明', 'type' => 'textarea', 'default' => ''],
+ ]],
+ ],
+ ],
+ 'wechat' => [
+ 'key' => 'wechat',
+ 'title' => '微信通知',
+ 'version' => '1.0.0',
+ 'option_key' => 'aksrt_settings',
+ 'groups' => [
+ ['key' => 'account', 'title' => '公众号配置', 'fields' => [
+ ['key' => 'wechat_notify_enabled', 'label' => '启用微信通知', 'type' => 'switch', 'default' => false],
+ ['key' => 'wechat_app_id', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
+ ['key' => 'wechat_app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'verify', 'title' => '服务器验证', 'fields' => [
+ ['key' => 'wechat_token', 'label' => 'Token', 'type' => 'input', 'default' => ''],
+ ['key' => 'wechat_encoding_aes_key', 'label' => 'EncodingAESKey', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'templates', 'title' => '模板消息', 'fields' => [
+ ['key' => 'wechat_template_comment', 'label' => '评论通知模板', 'type' => 'input', 'default' => ''],
+ ['key' => 'wechat_template_reply', 'label' => '回复通知模板', 'type' => 'input', 'default' => ''],
+ ['key' => 'wechat_template_system', 'label' => '系统通知模板', 'type' => 'input', 'default' => ''],
+ ['key' => 'wechat_template_order', 'label' => '订单通知模板', 'type' => 'input', 'default' => ''],
+ ]],
+ ['key' => 'triggers', 'title' => '通知触发器', 'fields' => [
+ ['key' => 'wechat_notify_comment', 'label' => '新评论通知', 'type' => 'switch', 'default' => true],
+ ['key' => 'wechat_notify_reply', 'label' => '评论回复通知', 'type' => 'switch', 'default' => true],
+ ['key' => 'wechat_notify_system', 'label' => '系统通知', 'type' => 'switch', 'default' => true],
+ ['key' => 'wechat_notify_order', 'label' => '订单通知', 'type' => 'switch', 'default' => true],
+ ['key' => 'wechat_bind_entry', 'label' => '显示绑定入口', 'type' => 'switch', 'default' => true],
+ ]],
+ ],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/inc/classes/auth-traits/trait-aksrt-auth-service-part-1a.php b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1a.php
new file mode 100644
index 00000000..d79bb349
--- /dev/null
+++ b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1a.php
@@ -0,0 +1,483 @@
+ false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
+ }
+
+ if (empty($post['agree_terms'])) {
+ return ['success' => false, 'error' => __('请先同意服务条款', 'aksrt')];
+ }
+
+ if (empty($user_login) || !validate_username($user_login)) {
+ return ['success' => false, 'error' => __('请输入有效的用户名', 'aksrt')];
+ }
+
+ if (mb_strlen($user_login) < 3 || mb_strlen($user_login) > 60) {
+ return ['success' => false, 'error' => __('用户名长度需在 3-60 个字符之间', 'aksrt')];
+ }
+
+ if (username_exists($user_login)) {
+ return ['success' => false, 'error' => __('该用户名已被使用', 'aksrt')];
+ }
+
+ if (empty($user_email) || !is_email($user_email)) {
+ return ['success' => false, 'error' => __('请输入有效的邮箱地址', 'aksrt')];
+ }
+
+ if (email_exists($user_email)) {
+ return ['success' => false, 'error' => __('该邮箱已被其他账号使用', 'aksrt')];
+ }
+
+ $password_check = self::validate_password($password, $confirm);
+ if ($password_check !== true) {
+ return ['success' => false, 'error' => $password_check];
+ }
+
+ if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('register')) {
+ if (function_exists('aksrt_verify_captcha')) {
+ $captcha_result = aksrt_verify_captcha($post);
+ if ($captcha_result !== true) {
+ return ['success' => false, 'error' => $captcha_result];
+ }
+ }
+ }
+
+ // ====== 解析邀请码 ======
+ // 优先级:表单 invite_code(用户显式填写) > cookie aksrt_invite
+ $invite_registration_enabled = class_exists('AKSRT_Settings') && AKSRT_Settings::invite_registration_enabled();
+ $invite_mode = $invite_registration_enabled && class_exists('AKSRT_Settings')
+ ? AKSRT_Settings::invite_register_field_mode()
+ : 'hidden';
+ $invite_code = isset($post['invite_code']) ? sanitize_key((string) wp_unslash($post['invite_code'])) : '';
+ $resolved_inviter_id = 0;
+ $invite_is_valid = false;
+
+ if (!$invite_registration_enabled) {
+ // 邀请功能关闭,忽略所有邀请码
+ $invite_code = '';
+ } elseif ($invite_code !== '') {
+ if (class_exists('AKSRT_Invite')) {
+ $invite_service = AKSRT_Invite::get_instance();
+ $resolved_inviter_id = $invite_service->resolve_code($invite_code);
+ $invite_is_valid = $invite_service->validate_code($invite_code);
+ }
+ // 必填模式下,邀请码无效时根据后台开关决定是否拒绝
+ $reject_invalid = class_exists('AKSRT_Settings') && AKSRT_Settings::invite_reject_invalid();
+ if ($invite_mode === 'required' && $reject_invalid && !$invite_is_valid) {
+ return ['success' => false, 'error' => __('邀请码无效,请检查后重试', 'aksrt')];
+ }
+ } elseif ($invite_mode === 'required') {
+ return ['success' => false, 'error' => __('请填写邀请码', 'aksrt')];
+ }
+
+ // 把解析到的 inviter_id 暂存到静态字段,user_register hook 内部读取(优先于 cookie)
+ self::$pending_invite_inviter_id = $resolved_inviter_id;
+ self::$pending_invite_code = $invite_code;
+
+ $user_id = wp_create_user($user_login, $password, $user_email);
+ if (is_wp_error($user_id)) {
+ self::$pending_invite_inviter_id = 0;
+ self::$pending_invite_code = '';
+ return ['success' => false, 'error' => $user_id->get_error_message()];
+ }
+
+ if ($invite_code !== '' && $resolved_inviter_id <= 0 && class_exists('AKSRT_Invite')) {
+ AKSRT_Invite::get_instance()->handle_registration_invite((int) $user_id, $invite_code);
+ }
+
+ // wp_create_user 内部已触发 user_register hook,AKSRT_Invite::on_user_register 已读取 pending 字段。
+ // 这里清空,避免污染后续请求。
+ self::$pending_invite_inviter_id = 0;
+ self::$pending_invite_code = '';
+
+ $user = get_userdata($user_id);
+ if (!$user || is_wp_error($user)) {
+ return ['success' => false, 'error' => __('注册成功,但读取用户信息失败,请联系管理员', 'aksrt')];
+ }
+
+ $oauth_context = isset($context['oauth_bind_data']) && is_array($context['oauth_bind_data']) ? $context['oauth_bind_data'] : [];
+ $oauth_verified = self::oauth_email_is_verified($oauth_context);
+ $requires_verify = AKSRT_Settings::registration_requires_email_verification() && !$oauth_verified;
+
+ if (!$requires_verify) {
+ self::mark_email_verified($user_id);
+ return [
+ 'success' => true,
+ 'user_id' => (int) $user_id,
+ 'user' => $user,
+ 'user_login' => $user_login,
+ ];
+ }
+
+ $method = AKSRT_Settings::registration_verification_method();
+ $begin = self::begin_email_verification(
+ $user,
+ $method,
+ [
+ 'obk' => sanitize_text_field((string) ($context['obk'] ?? '')),
+ ]
+ );
+
+ if (empty($begin['success'])) {
+ self::delete_user_account($user_id);
+ return $begin;
+ }
+
+ return [
+ 'success' => true,
+ 'user_id' => (int) $user_id,
+ 'user' => $user,
+ 'user_login' => $user_login,
+ 'verification_required' => true,
+ 'verification_method' => $method,
+ 'verify_url' => self::get_email_verification_url((int) $user_id, ['mode' => $method]),
+ 'message' => $begin['message'] ?? '',
+ ];
+ }
+
+ public static function bind_oauth_account(array $post, array $oauth_data): array {
+ if (!wp_verify_nonce($post['_bind_nonce'] ?? '', 'aksrt_bind')) {
+ return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
+ }
+
+ if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('bind')) {
+ if (function_exists('aksrt_verify_captcha')) {
+ $captcha_result = aksrt_verify_captcha($post);
+ if ($captcha_result !== true) {
+ return ['success' => false, 'error' => $captcha_result];
+ }
+ }
+ }
+
+ $creds = [
+ 'user_login' => self::resolve_email_to_username((string) ($post['log'] ?? '')),
+ 'user_password' => (string) ($post['pwd'] ?? ''),
+ 'remember' => true,
+ ];
+
+ $user = wp_signon($creds);
+ if (is_wp_error($user)) {
+ return ['success' => false, 'error' => self::friendly_login_error($user->get_error_message())];
+ }
+
+ if (self::user_requires_email_verification($user)) {
+ wp_logout();
+ return [
+ 'success' => false,
+ 'error' => __('该账号尚未完成邮箱验证,请先完成验证后再绑定第三方登录。', 'aksrt'),
+ 'verify_url' => self::get_email_verification_url((int) $user->ID),
+ ];
+ }
+
+ $provider = sanitize_key((string) ($oauth_data['provider'] ?? ''));
+ if ($provider === '') {
+ return ['success' => false, 'error' => __('绑定数据无效,请重新使用第三方登录', 'aksrt')];
+ }
+
+ $uid = (int) $user->ID;
+ update_user_meta($uid, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
+
+ $bindings = get_user_meta($uid, 'aksrt_oauth_bindings', true) ?: [];
+ if (!is_array($bindings)) {
+ $bindings = [];
+ }
+ if (!in_array($provider, $bindings, true)) {
+ $bindings[] = $provider;
+ }
+ update_user_meta($uid, 'aksrt_oauth_bindings', $bindings);
+
+ if (!empty($oauth_data['user_data']['avatar_url'])) {
+ update_user_meta($uid, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
+ }
+
+ if (!empty($post['obk'])) {
+ delete_transient(sanitize_text_field((string) $post['obk']));
+ }
+
+ wp_set_auth_cookie($uid, true);
+
+ return [
+ 'success' => true,
+ 'user' => $user,
+ 'provider' => $provider,
+ ];
+ }
+
+ public static function finalize_oauth_registration(int $user_id, array $oauth_data, string $obk = ''): void {
+ if ($user_id <= 0) {
+ return;
+ }
+
+ $provider = sanitize_key((string) ($oauth_data['provider'] ?? ''));
+ if ($provider === '') {
+ return;
+ }
+
+ update_user_meta($user_id, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
+ update_user_meta($user_id, 'aksrt_oauth_bindings', [$provider]);
+
+ if (!empty($oauth_data['user_data']['avatar_url'])) {
+ update_user_meta($user_id, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
+ }
+
+ if (self::oauth_email_is_verified($oauth_data)) {
+ AKSRT_Auth_Email_Verification::mark_email_verified($user_id);
+ }
+
+ if ($obk !== '') {
+ delete_transient(sanitize_text_field($obk));
+ }
+
+ wp_set_auth_cookie($user_id, true);
+ }
+
+ public static function get_email_verification_context(array $query): array {
+ $invalid_payload = [
+ 'mode' => 'invalid',
+ 'user' => null,
+ 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt'),
+ 'method' => '',
+ 'email' => '',
+ 'uid' => 0,
+ 'verified' => false,
+ 'access_key' => '',
+ ];
+
+ $user_id = isset($query['uid']) ? absint($query['uid']) : 0;
+ if ($user_id <= 0) {
+ return $invalid_payload;
+ }
+
+ $user = get_userdata($user_id);
+ if (!$user || is_wp_error($user)) {
+ return $invalid_payload;
+ }
+
+ $method = self::current_registration_verification_method();
+ $email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
+ $token = sanitize_text_field((string) ($query['token'] ?? ''));
+ $access = sanitize_text_field((string) ($query['vk'] ?? ''));
+
+ // 容错:meta 缺失时(例如历史账号被强制要求验证),回退到设置默认值与 user_email
+ if (!in_array($method, ['link', 'code'], true)) {
+ $method = self::current_registration_verification_method();
+ }
+ if ($email === '' || !is_email($email)) {
+ $email = (string) $user->user_email;
+ }
+ $masked_email = self::masked_email($email);
+ $has_access = self::has_valid_email_access_key($user_id, $access);
+
+ // token 路径:链接模式直接走 token 校验;不依赖 vk
+ if ($token !== '') {
+ if (AKSRT_Auth_Email_Verification::is_email_verified($user_id)) {
+ return [
+ 'mode' => 'invalid',
+ 'user' => $user,
+ 'error' => __('该验证链接已失效或已使用,请重新发起验证流程', 'aksrt'),
+ 'method' => $has_access ? $method : '',
+ 'email' => $has_access ? $masked_email : '',
+ 'uid' => $has_access ? $user_id : 0,
+ 'verified' => true,
+ 'access_key' => $has_access ? $access : '',
+ ];
+ }
+
+ $result = self::verify_email_token($user_id, $token);
+ if (!empty($result['success'])) {
+ return [
+ 'mode' => 'completed',
+ 'user' => $user,
+ 'error' => '',
+ 'method' => $method,
+ 'email' => $masked_email,
+ 'uid' => $user_id,
+ 'verified' => true,
+ 'redirect' => $result['redirect'] ?? home_url('/'),
+ 'message' => $result['message'] ?? '',
+ 'access_key' => $has_access ? $access : '',
+ ];
+ }
+
+ return [
+ 'mode' => 'invalid',
+ 'user' => $user,
+ 'error' => $result['error'] ?? __('验证链接无效或已过期', 'aksrt'),
+ 'method' => $has_access ? $method : '',
+ 'email' => $has_access ? $masked_email : '',
+ 'uid' => $has_access ? $user_id : 0,
+ 'verified' => false,
+ 'access_key' => $has_access ? $access : '',
+ ];
+ }
+
+ // 没有 token 也没有 vk:返回完全脱敏的通用错误,避免根据 uid 枚举账号状态
+ if (!$has_access) {
+ return $invalid_payload;
+ }
+
+ if (!self::user_requires_email_verification($user)) {
+ return [
+ 'mode' => 'verified',
+ 'user' => $user,
+ 'error' => '',
+ 'method' => '',
+ 'email' => self::masked_email((string) $user->user_email),
+ 'uid' => $user_id,
+ 'verified' => true,
+ 'access_key' => $access,
+ ];
+ }
+
+ return [
+ 'mode' => $method === 'code' ? 'code' : 'pending',
+ 'user' => $user,
+ 'error' => '',
+ 'method' => $method,
+ 'email' => $masked_email ?: self::masked_email((string) $user->user_email),
+ 'uid' => $user_id,
+ 'verified' => false,
+ 'access_key' => $access,
+ ];
+ }
+
+ public static function verify_email_code_submission(array $post): array {
+ if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
+ return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
+ }
+
+ $user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
+ $access = sanitize_text_field((string) ($post['vk'] ?? ''));
+ $code = sanitize_text_field((string) ($post['email_code'] ?? ''));
+ if ($user_id <= 0 || $code === '') {
+ return ['success' => false, 'error' => __('请输入完整的邮箱验证码', 'aksrt')];
+ }
+
+ if (!self::has_valid_email_access_key($user_id, $access)) {
+ return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
+ }
+
+ if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('verify_email')) {
+ if (function_exists('aksrt_verify_captcha')) {
+ $captcha_result = aksrt_verify_captcha($post);
+ if ($captcha_result !== true) {
+ return ['success' => false, 'error' => $captcha_result];
+ }
+ }
+ }
+
+ return self::verify_email_code($user_id, $code);
+ }
+
+ public static function resend_email_verification(array $post): array {
+ if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
+ return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
+ }
+
+ $user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
+ $access = sanitize_text_field((string) ($post['vk'] ?? ''));
+
+ // 必须先持有有效 vk 才允许重发,避免未登录用户用 uid 枚举触发邮件
+ if ($user_id <= 0 || !self::has_valid_email_access_key($user_id, $access)) {
+ return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
+ }
+
+ // IP 节流:每 IP 每 RESEND_IP_WINDOW 秒最多 RESEND_IP_MAX 次(跨账号合并计数)
+ $ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
+ if ($ip !== '') {
+ $ip_key = 'aksrt_email_resend_ip_' . md5($ip);
+ $ip_attempts = (int) get_transient($ip_key);
+ if ($ip_attempts >= self::RESEND_IP_MAX) {
+ self::log_event('verify.resend.ip_throttled', ['uid' => $user_id, 'ip' => $ip]);
+ return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
+ }
+ set_transient($ip_key, $ip_attempts + 1, self::RESEND_IP_WINDOW);
+ }
+
+ $user = get_userdata($user_id);
+ if (!$user || is_wp_error($user)) {
+ return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
+ }
+
+ if (!self::user_requires_email_verification($user)) {
+ return ['success' => false, 'error' => __('该账号已完成邮箱验证,无需重复发送', 'aksrt')];
+ }
+
+ if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('verify_email')) {
+ if (function_exists('aksrt_verify_captcha')) {
+ $captcha_result = aksrt_verify_captcha($post);
+ if ($captcha_result !== true) {
+ return ['success' => false, 'error' => $captcha_result];
+ }
+ }
+ }
+
+ $method = self::current_registration_verification_method();
+ $email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
+
+ // 容错:meta 缺失时回退到设置默认值与 user_email(兼容历史账号 / meta 被外部清理的情况)
+ if (!in_array($method, ['link', 'code'], true)) {
+ $method = AKSRT_Settings::registration_verification_method();
+ }
+ if ($email === '' || !is_email($email)) {
+ $email = (string) $user->user_email;
+ }
+ if ($email === '' || !is_email($email)) {
+ return ['success' => false, 'error' => __('当前账号缺少有效邮箱,请联系管理员', 'aksrt')];
+ }
+
+ $requested_at = (int) get_user_meta($user_id, self::EMAIL_REQUESTED_AT_META, true);
+ if ($requested_at > 0 && (time() - $requested_at) < 60) {
+ return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
+ }
+
+ return self::begin_email_verification($user, $method, [
+ 'email' => $email,
+ 'obk' => (string) get_user_meta($user_id, self::EMAIL_OBK_META, true),
+ 'force' => true,
+ ]);
+ }
+
+ private static function user_requires_email_verification($user): bool {
+ return AKSRT_Auth_Email_Verification::user_requires_email_verification($user);
+ }
+
+ private static function mark_email_verified(int $user_id): void {
+ AKSRT_Auth_Email_Verification::mark_email_verified($user_id);
+ }
+
+ // ============================================================
+ // 公开 API:管理员后台 / 内部业务调用(委托 AKSRT_Auth_Email_Verification)
+ // ============================================================
+
+ public static function user_is_email_verified(int $user_id): bool {
+ return AKSRT_Auth_Email_Verification::user_is_email_verified($user_id);
+ }
+
+ public static function get_user_verification_state(int $user_id): array {
+ return AKSRT_Auth_Email_Verification::get_user_verification_state($user_id);
+ }
+
+ public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
+ return AKSRT_Auth_Email_Verification::admin_mark_verified($user_id, $by_admin_id);
+ }
+
+ public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
+ return AKSRT_Auth_Email_Verification::admin_unmark_verified($user_id, $by_admin_id);
+ }
+
+}
diff --git a/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php
new file mode 100644
index 00000000..354b1ea1
--- /dev/null
+++ b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php
@@ -0,0 +1,447 @@
+ 'invalid',
+ 'user' => null,
+ 'error' => __('旧的重置密码链接已失效,请重新申请重置密码。', 'aksrt'),
+ 'token' => '',
+ 'user_id'=> 0,
+ ];
+ }
+
+ $token = sanitize_text_field($query['token'] ?? '');
+ $user_id = isset($query['uid']) ? absint($query['uid']) : 0;
+
+ if ($token === '' || $user_id <= 0) {
+ return [
+ 'mode' => 'request',
+ 'user' => null,
+ 'error' => '',
+ 'token' => '',
+ 'user_id'=> 0,
+ ];
+ }
+
+ $user = self::validate_reset_token($user_id, $token);
+ if (is_wp_error($user)) {
+ return [
+ 'mode' => 'invalid',
+ 'user' => null,
+ 'error' => $user->get_error_message(),
+ 'token' => $token,
+ 'user_id'=> $user_id,
+ ];
+ }
+
+ return [
+ 'mode' => 'reset',
+ 'user' => $user,
+ 'error' => '',
+ 'token' => $token,
+ 'user_id'=> $user_id,
+ ];
+ }
+
+ public static function request_password_reset(array $post): array {
+ if (!wp_verify_nonce($post['_reset_nonce'] ?? '', 'aksrt_reset_password')) {
+ return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
+ }
+
+ if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('lostpassword')) {
+ if (function_exists('aksrt_verify_captcha')) {
+ $captcha_result = aksrt_verify_captcha($post);
+ if ($captcha_result !== true) {
+ return ['success' => false, 'error' => $captcha_result];
+ }
+ }
+ }
+
+ $raw_login = trim((string) ($post['user_login'] ?? ''));
+ if ($raw_login === '') {
+ return ['success' => false, 'error' => __('请输入邮箱地址或用户名', 'aksrt')];
+ }
+
+ $user = null;
+ if (is_email($raw_login)) {
+ $user = get_user_by('email', $raw_login);
+ } else {
+ $user = get_user_by('login', sanitize_user($raw_login, true));
+ }
+
+ if (!$user || is_wp_error($user)) {
+ return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
+ }
+
+ $token = self::create_reset_token($user->ID);
+ $sent = self::send_password_reset_email($user, $token);
+
+ if (!$sent) {
+ return ['success' => false, 'error' => __('重置邮件发送失败,请稍后再试', 'aksrt')];
+ }
+
+ return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
+ }
+
+ public static function reset_password_with_token(array $post): array {
+ if (!wp_verify_nonce($post['_setpw_nonce'] ?? '', 'aksrt_set_password')) {
+ return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
+ }
+
+ $token = sanitize_text_field($post['reset_token'] ?? '');
+ $user_id = isset($post['reset_uid']) ? absint($post['reset_uid']) : 0;
+ $pass1 = (string) ($post['pass1'] ?? '');
+ $pass2 = (string) ($post['pass2'] ?? '');
+
+ $user = self::validate_reset_token($user_id, $token);
+ if (is_wp_error($user)) {
+ return ['success' => false, 'error' => $user->get_error_message(), 'mode' => 'invalid'];
+ }
+
+ $password_check = self::validate_password($pass1, $pass2);
+ if ($password_check !== true) {
+ return ['success' => false, 'error' => $password_check, 'mode' => 'reset'];
+ }
+
+ wp_set_password($pass1, $user_id);
+ self::clear_reset_token($user_id);
+
+ return ['success' => true, 'message' => __('密码已设置成功!', 'aksrt')];
+ }
+
+ private static function create_reset_token(int $user_id): string {
+ $token = wp_generate_password(48, false, false);
+ update_user_meta($user_id, self::RESET_TOKEN_HASH_META, hash('sha256', $token));
+ update_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, time() + self::RESET_TOKEN_TTL);
+ update_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT, time());
+ return $token;
+ }
+
+ private static function validate_reset_token(int $user_id, string $token) {
+ if ($user_id <= 0 || $token === '') {
+ return new WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
+ }
+
+ $user = get_userdata($user_id);
+ if (!$user || is_wp_error($user)) {
+ return new WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
+ }
+
+ $stored_hash = (string) get_user_meta($user_id, self::RESET_TOKEN_HASH_META, true);
+ $expires_at = (int) get_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, true);
+ $token_hash = hash('sha256', $token);
+
+ if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, $token_hash)) {
+ return new WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
+ }
+
+ return $user;
+ }
+
+ private static function clear_reset_token(int $user_id): void {
+ delete_user_meta($user_id, self::RESET_TOKEN_HASH_META);
+ delete_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META);
+ delete_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT);
+ }
+
+ private static function send_password_reset_email(WP_User $user, string $token): bool {
+ $reset_url = aksrt_get_reset_password_url($token, $user->ID);
+ $site_name = get_bloginfo('name');
+ $default_subject = '[' . $site_name . '] ' . __('重置密码', 'aksrt');
+ $default_content = sprintf(
+ __("您好 %s,\n\n有人请求重置您在 %s 的密码。\n\n请点击以下链接设置新密码:\n\n%s\n\n如果这不是您本人的操作,请忽略此邮件,您的密码不会被更改。", 'aksrt'),
+ $user->user_login, $site_name, $reset_url
+ );
+
+ return AKSRT_Settings::send_event_email(
+ 'password_reset', $user->user_email, $default_subject, $default_content,
+ ['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $reset_url]
+ );
+ }
+
+ /* =================================================================
+ * 手机号认证(注册 / 登录 / 绑定)
+ * 对齐 zibll 的 signup_captcha / signin_captcha / bind_phone_captcha
+ * ================================================================= */
+
+ /**
+ * 手机号注册
+ *
+ * @param array $post POST 数据 (phone, code, password, invite_code)
+ * @return array{success: bool, user_id?: int, error?: string, redirect?: string}
+ */
+ public static function register_by_phone(array $post): array {
+ $phone = sanitize_text_field((string) ($post['phone'] ?? ''));
+ $code = sanitize_text_field((string) ($post['code'] ?? ''));
+ $pass = (string) ($post['password'] ?? '');
+
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
+ }
+ if (empty($code)) {
+ return ['success' => false, 'error' => __('请输入验证码', 'aksrt')];
+ }
+ if (strlen($pass) < 6) {
+ return ['success' => false, 'error' => __('密码至少 6 位', 'aksrt')];
+ }
+
+ // 验证 SMS 验证码
+ if (class_exists('AKSRT_SMS')) {
+ $verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code);
+ if (empty($verify_result['success'])) {
+ return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
+ }
+ } else {
+ return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
+ }
+
+ // 检查手机号是否已注册
+ $existing_user = self::get_user_by_phone($phone);
+ if ($existing_user) {
+ return ['success' => false, 'error' => __('该手机号已注册', 'aksrt')];
+ }
+
+ // 生成用户名(phone_ + 后4位)
+ $username = 'phone_' . substr($phone, -4);
+ $suffix = 1;
+ $base = $username;
+ while (username_exists($username)) {
+ $username = $base . '_' . $suffix;
+ $suffix++;
+ }
+
+ // 创建用户
+ $user_id = wp_create_user($username, $pass, $phone . '@sms.aksrt.local');
+ if (is_wp_error($user_id)) {
+ return ['success' => false, 'error' => $user_id->get_error_message()];
+ }
+
+ // 绑定手机号
+ update_user_meta($user_id, 'aksrt_phone', $phone);
+ update_user_meta($user_id, 'aksrt_phone_verified', '1');
+
+ // 邮箱标记为已验证(手机注册等同于已验证)
+ update_user_meta($user_id, 'aksrt_email_verified', '1');
+
+ // 处理邀请码
+ $invite_code = sanitize_key((string) ($post['invite_code'] ?? ''));
+ if ($invite_code !== '' && class_exists('AKSRT_Invite')) {
+ AKSRT_Invite::get_instance()->handle_registration_invite($user_id, $invite_code);
+ }
+
+ // 自动登录
+ wp_set_current_user($user_id);
+ wp_set_auth_cookie($user_id, true);
+
+ $redirect = function_exists('aksrt_get_user_center_url')
+ ? aksrt_get_user_center_url()
+ : home_url('/');
+
+ return [
+ 'success' => true,
+ 'user_id' => $user_id,
+ 'redirect' => $redirect,
+ 'message' => __('注册成功', 'aksrt'),
+ ];
+ }
+
+ /**
+ * 手机号登录(密码登录)
+ *
+ * @param array $post (phone, password)
+ * @return array
+ */
+ public static function login_by_phone_password(array $post): array {
+ $phone = sanitize_text_field((string) ($post['phone'] ?? ''));
+ $pass = (string) ($post['password'] ?? '');
+
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
+ }
+
+ $user = self::get_user_by_phone($phone);
+ if (!$user) {
+ return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
+ }
+
+ // 验证密码
+ if (!wp_check_password($pass, $user->data->user_pass, $user->ID)) {
+ return ['success' => false, 'error' => __('密码不正确', 'aksrt')];
+ }
+
+ wp_set_current_user($user->ID);
+ wp_set_auth_cookie($user->ID, true);
+
+ $redirect = function_exists('aksrt_get_user_center_url')
+ ? aksrt_get_user_center_url()
+ : home_url('/');
+
+ return [
+ 'success' => true,
+ 'user_id' => $user->ID,
+ 'redirect' => $redirect,
+ ];
+ }
+
+ /**
+ * 手机号免密登录(SMS 验证码登录)
+ *
+ * @param array $post (phone, code)
+ * @return array
+ */
+ public static function login_by_phone_code(array $post): array {
+ $phone = sanitize_text_field((string) ($post['phone'] ?? ''));
+ $code = sanitize_text_field((string) ($post['code'] ?? ''));
+
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
+ }
+
+ // 验证 SMS 验证码
+ if (class_exists('AKSRT_SMS')) {
+ $verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code);
+ if (empty($verify_result['success'])) {
+ return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
+ }
+ } else {
+ return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
+ }
+
+ $user = self::get_user_by_phone($phone);
+ if (!$user) {
+ return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
+ }
+
+ wp_set_current_user($user->ID);
+ wp_set_auth_cookie($user->ID, true);
+
+ $redirect = function_exists('aksrt_get_user_center_url')
+ ? aksrt_get_user_center_url()
+ : home_url('/');
+
+ return [
+ 'success' => true,
+ 'user_id' => $user->ID,
+ 'redirect' => $redirect,
+ ];
+ }
+
+ /**
+ * 绑定手机号到当前用户
+ *
+ * @param array $post (phone, code)
+ * @return array
+ */
+ public static function bind_phone(array $post): array {
+ if (!is_user_logged_in()) {
+ return ['success' => false, 'error' => __('请先登录', 'aksrt')];
+ }
+
+ $phone = sanitize_text_field((string) ($post['phone'] ?? ''));
+ $code = sanitize_text_field((string) ($post['code'] ?? ''));
+
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
+ }
+
+ // 验证 SMS 验证码
+ if (class_exists('AKSRT_SMS')) {
+ $verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code);
+ if (empty($verify_result['success'])) {
+ return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
+ }
+ } else {
+ return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
+ }
+
+ // 检查手机号是否已被其他用户绑定
+ $existing = self::get_user_by_phone($phone);
+ if ($existing && (int) $existing->ID !== get_current_user_id()) {
+ return ['success' => false, 'error' => __('该手机号已被其他账号绑定', 'aksrt')];
+ }
+
+ $user_id = get_current_user_id();
+ update_user_meta($user_id, 'aksrt_phone', $phone);
+ update_user_meta($user_id, 'aksrt_phone_verified', '1');
+
+ return [
+ 'success' => true,
+ 'message' => __('手机号绑定成功', 'aksrt'),
+ ];
+ }
+
+ /**
+ * 根据手机号查找用户
+ */
+ private static function get_user_by_phone(string $phone): ?WP_User {
+ $users = get_users([
+ 'meta_key' => 'aksrt_phone',
+ 'meta_value' => $phone,
+ 'number' => 1,
+ 'fields' => 'all',
+ ]);
+ return !empty($users) ? $users[0] : null;
+ }
+}
diff --git a/inc/classes/auth/class-aksrt-auth-email-verification.php b/inc/classes/auth/class-aksrt-auth-email-verification.php
new file mode 100644
index 00000000..da1b6dc5
--- /dev/null
+++ b/inc/classes/auth/class-aksrt-auth-email-verification.php
@@ -0,0 +1,529 @@
+ID);
+ }
+
+ public static function is_email_verified(int $user_id): bool {
+ return (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, true) === 1;
+ }
+
+ public static function mark_email_verified(int $user_id): void {
+ $was_verified = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, true) === 1;
+ update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
+ self::clear_email_verification($user_id);
+ // 仅在状态由"未验证 → 已验证"跳变时触发,避免历史 backfill / 重复调用造成奖励重发。
+ // 监听方负责自己幂等(aksrt_invite 表的 status 检查已经做到这一点)。
+ if (!$was_verified) {
+ do_action('aksrt_email_verified', $user_id);
+ }
+ }
+
+ public static function clear_email_verification(int $user_id): void {
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_METHOD_META);
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META);
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META);
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META);
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META);
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_OBK_META);
+ self::reset_email_code_attempt_trackers($user_id);
+ }
+
+ // ============================================================
+ // 公开 API:管理员后台 / 内部业务调用
+ // ============================================================
+
+ /**
+ * 是否已验证邮箱(公开版本,给后台 UI / 用户列表渲染用)。
+ */
+ public static function user_is_email_verified(int $user_id): bool {
+ return self::is_email_verified($user_id);
+ }
+
+ /**
+ * 取一个用户当前的验证状态摘要,用于后台资料页展示。
+ *
+ * @return array{verified:bool, method:string, target_email:string, expires_at:int, requested_at:int, last_admin_log:array}
+ */
+ public static function get_user_verification_state(int $user_id): array {
+ return [
+ 'verified' => self::is_email_verified($user_id),
+ 'method' => (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_METHOD_META, true),
+ 'target_email' => (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META, true),
+ 'expires_at' => (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META, true),
+ 'requested_at' => (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, true),
+ 'last_admin_log' => (array) (get_user_meta($user_id, 'aksrt_email_verification_admin_log', true) ?: []),
+ ];
+ }
+
+ /**
+ * 管理员:把指定用户标记为已验证(清除所有进行中的验证 meta)。
+ */
+ public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
+ if ($user_id <= 0) {
+ return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
+ }
+ if (!current_user_can('edit_user', $user_id)) {
+ return ['success' => false, 'error' => __('权限不足', 'aksrt')];
+ }
+ self::mark_email_verified($user_id);
+ self::record_admin_action($user_id, 'mark_verified', $by_admin_id);
+ AKSRT_Auth_Service::log_event('admin.action', ['action' => 'mark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
+ return ['success' => true, 'message' => __('已标记为已验证', 'aksrt')];
+ }
+
+ /**
+ * 管理员:撤销已验证状态(用于测试 / 回滚 / 应对邮箱泄漏)。
+ */
+ public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
+ if ($user_id <= 0) {
+ return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
+ }
+ if (!current_user_can('edit_user', $user_id)) {
+ return ['success' => false, 'error' => __('权限不足', 'aksrt')];
+ }
+ update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 0);
+ self::clear_email_verification($user_id);
+ self::record_admin_action($user_id, 'unmark_verified', $by_admin_id);
+ AKSRT_Auth_Service::log_event('admin.action', ['action' => 'unmark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
+ return ['success' => true, 'message' => __('已撤销验证状态', 'aksrt')];
+ }
+ /**
+ * 管理员:立即发送 / 重发验证邮件(绕过 60s 节流)。
+ */
+ public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array {
+ if ($user_id <= 0) {
+ return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
+ }
+ if (!current_user_can('edit_user', $user_id)) {
+ return ['success' => false, 'error' => __('权限不足', 'aksrt')];
+ }
+ $user = get_userdata($user_id);
+ if (!$user || is_wp_error($user)) {
+ return ['success' => false, 'error' => __('用户不存在', 'aksrt')];
+ }
+
+ $method = AKSRT_Auth_Service::current_registration_verification_method();
+ $email = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META, true);
+ if (!in_array($method, ['link', 'code'], true)) {
+ $method = AKSRT_Settings::registration_verification_method();
+ }
+ if ($email === '' || !is_email($email)) {
+ $email = (string) $user->user_email;
+ }
+
+ $result = self::begin_email_verification($user, $method, [
+ 'email' => $email,
+ 'obk' => (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_OBK_META, true),
+ 'bypass_throttle' => true,
+ 'force' => true,
+ ]);
+
+ // 标记为待验证(确保 admin 主动发邮件后用户被视为未验证)
+ if (!empty($result['success'])) {
+ update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 0);
+ self::record_admin_action($user_id, 'send_verification', $by_admin_id);
+ AKSRT_Auth_Service::log_event('admin.action', ['action' => 'send_verification', 'uid' => $user_id, 'by' => $by_admin_id]);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 把已存在的用户视为受信任(grandfather):写入 aksrt_email_verified=1。
+ *
+ * 仅写入 meta 不存在的用户;已经显式为 0 的(正在走验证流程的新注册)保持不动。
+ *
+ * 用户量 > 50000 时改为异步分批;否则内联完成。返回处理总数。
+ *
+ * @param bool $force 强制再次执行(即便 backfill_done 标记已存在),调试用
+ */
+ public static function backfill_legacy_users_as_verified(bool $force = false): int {
+ if (!$force && get_option('aksrt_email_verification_backfill_done')) {
+ return 0;
+ }
+
+ $total = 0;
+ if (function_exists('count_users')) {
+ $counts = count_users();
+ $total = (int) ($counts['total_users'] ?? 0);
+ }
+
+ // 大表保护:超过 5w 用户改为后台 cron 分批
+ if ($total > 50000) {
+ if (!wp_next_scheduled('aksrt_email_verification_backfill_batch')) {
+ wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [0]);
+ }
+ update_option('aksrt_email_verification_backfill_log', [
+ 'mode' => 'async',
+ 'total' => $total,
+ 'scheduled' => time(),
+ 'by' => get_current_user_id(),
+ ]);
+ return 0;
+ }
+
+ // 内联分批,每批 500
+ $updated = 0;
+ $paged = 1;
+ do {
+ $users = get_users([
+ 'number' => 500,
+ 'paged' => $paged,
+ 'fields' => ['ID'],
+ 'meta_query' => [
+ [
+ 'key' => AKSRT_Auth_Service::EMAIL_VERIFIED_META,
+ 'compare' => 'NOT EXISTS',
+ ],
+ ],
+ ]);
+
+ if (empty($users)) {
+ break;
+ }
+
+ foreach ($users as $u) {
+ update_user_meta((int) $u->ID, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
+ $updated++;
+ }
+
+ $paged++;
+ // 让 MySQL 喘口气
+ if (function_exists('usleep')) {
+ usleep(50000);
+ }
+ } while (count($users) === 500 && $paged < 200); // 上限 10w 兜底防呆
+
+ update_option('aksrt_email_verification_backfill_done', time());
+ update_option('aksrt_email_verification_backfill_log', [
+ 'mode' => 'inline',
+ 'count' => $updated,
+ 'ran_at' => time(),
+ 'by' => get_current_user_id(),
+ ]);
+ AKSRT_Auth_Service::log_event('admin.backfill', ['count' => $updated, 'by' => get_current_user_id()]);
+
+ return $updated;
+ }
+
+ /**
+ * 异步分批回填:cron 钩子。每批 500,自动调度下一批直到完成。
+ */
+ public static function run_async_backfill_batch(int $offset = 0): void {
+ $users = get_users([
+ 'number' => 500,
+ 'offset' => max(0, $offset),
+ 'fields' => ['ID'],
+ 'orderby' => 'ID',
+ 'order' => 'ASC',
+ 'meta_query' => [
+ [
+ 'key' => AKSRT_Auth_Service::EMAIL_VERIFIED_META,
+ 'compare' => 'NOT EXISTS',
+ ],
+ ],
+ ]);
+
+ if (empty($users)) {
+ update_option('aksrt_email_verification_backfill_done', time());
+ $log = (array) get_option('aksrt_email_verification_backfill_log', []);
+ $log['finished_at'] = time();
+ update_option('aksrt_email_verification_backfill_log', $log);
+ AKSRT_Auth_Service::log_event('admin.backfill.finished', []);
+ return;
+ }
+
+ foreach ($users as $u) {
+ update_user_meta((int) $u->ID, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
+ }
+
+ // 调度下一批
+ wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [$offset + 500]);
+ }
+
+ /**
+ * 记录管理员操作日志(每个用户最多 10 条 ring buffer)。
+ */
+ public static function record_admin_action(int $user_id, string $action, int $by_admin_id): void {
+ $log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true);
+ if (!is_array($log)) {
+ $log = [];
+ }
+ $log[] = [
+ 't' => time(),
+ 'action' => $action,
+ 'by' => $by_admin_id ?: get_current_user_id(),
+ ];
+ if (count($log) > 10) {
+ $log = array_slice($log, -10);
+ }
+ update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
+ }
+ // ============================================================
+ // 验证码失败计数 / IP 节流
+ // ============================================================
+
+ public static function email_code_ip_throttle_key(int $user_id): string {
+ $ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
+ if ($user_id <= 0 || $ip === '') {
+ return '';
+ }
+
+ return 'aksrt_email_code_try_' . $user_id . '_' . md5($ip);
+ }
+
+ public static function reset_email_code_attempt_trackers(int $user_id): void {
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META);
+ $ip_key = self::email_code_ip_throttle_key($user_id);
+ if ($ip_key !== '') {
+ delete_transient($ip_key);
+ }
+ }
+
+ public static function fail_email_code_attempt(int $user_id): array {
+ $attempts = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META, true);
+ $attempts++;
+ update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META, $attempts);
+
+ $ip_attempts = 0;
+ $ip_key = self::email_code_ip_throttle_key($user_id);
+ if ($ip_key !== '') {
+ $ip_attempts = (int) get_transient($ip_key);
+ $ip_attempts++;
+ set_transient($ip_key, $ip_attempts, AKSRT_Auth_Service::EMAIL_CODE_IP_TTL);
+ }
+
+ if ($attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS || $ip_attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS) {
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
+ update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, 0);
+ AKSRT_Auth_Service::log_event('verify.code.locked', ['uid' => $user_id, 'attempts' => $attempts]);
+ return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
+ }
+
+ AKSRT_Auth_Service::log_event('verify.code.fail', ['uid' => $user_id, 'attempts' => $attempts]);
+ return ['success' => false, 'error' => __('邮箱验证码错误或已过期,请重新输入。', 'aksrt')];
+ }
+
+ // ============================================================
+ // 验证流程:发起 / 校验 / 完成
+ // ============================================================
+
+ public static function begin_email_verification(WP_User $user, string $method, array $args = []): array {
+ $method = in_array($method, ['link', 'code'], true) ? $method : 'link';
+ $email = sanitize_email((string) ($args['email'] ?? $user->user_email));
+ $obk = sanitize_text_field((string) ($args['obk'] ?? ''));
+ $bypass_throttle = !empty($args['bypass_throttle']);
+ if ($email === '' || !is_email($email)) {
+ return ['success' => false, 'error' => __('当前账号缺少有效邮箱,无法发送验证邮件', 'aksrt')];
+ }
+
+ // 节流:仅在非管理员路径生效(admin_send_verification 会传 bypass_throttle=true)
+ if (!$bypass_throttle) {
+ $last_requested = (int) get_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, true);
+ if ($last_requested > 0 && (time() - $last_requested) < 60 && empty($args['force'])) {
+ return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
+ }
+ }
+
+ $expires_at = time() + (AKSRT_Settings::registration_verification_ttl_minutes() * MINUTE_IN_SECONDS);
+ update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 0);
+ update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_METHOD_META, $method);
+ update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_TARGET_META, $email);
+ update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_EXPIRES_META, $expires_at);
+ update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, time());
+ if ($obk !== '') {
+ update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_OBK_META, $obk);
+ }
+
+ delete_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META);
+ delete_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
+
+ if ($method === 'code') {
+ $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
+ update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_CODE_HASH_META, wp_hash_password($code));
+ // 新验证码 = 重置失败计数 / IP 节流,避免被旧计数立即锁死
+ self::reset_email_code_attempt_trackers((int) $user->ID);
+ $sent = self::send_registration_verification_code_email($user, $email, $code);
+ } else {
+ $token = wp_generate_password(48, false, false);
+ update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META, hash('sha256', $token));
+ $sent = self::send_registration_verification_link_email($user, $email, $token);
+ }
+
+ if (!$sent) {
+ self::clear_email_verification((int) $user->ID);
+ AKSRT_Auth_Service::log_event('email.send.fail', ['uid' => (int) $user->ID, 'method' => $method]);
+ return ['success' => false, 'error' => __('验证邮件发送失败,请稍后重试', 'aksrt')];
+ }
+
+ AKSRT_Auth_Service::log_event('email.send.ok', ['uid' => (int) $user->ID, 'method' => $method]);
+
+ $message = $method === 'code'
+ ? __('注册成功,验证码已发送到你的邮箱,请完成验证后继续。', 'aksrt')
+ : __('注册成功,验证邮件已发送到你的邮箱,请点击邮件中的链接完成验证。', 'aksrt');
+
+ return [
+ 'success' => true,
+ 'message' => $message,
+ ];
+ }
+
+ public static function verify_email_token(int $user_id, string $token): array {
+ $user = get_userdata($user_id);
+ if (!$user || is_wp_error($user)) {
+ return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
+ }
+
+ $stored_hash = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META, true);
+ $expires_at = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META, true);
+ if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, hash('sha256', $token))) {
+ return ['success' => false, 'error' => __('验证链接无效或已过期,请重新发送验证邮件。', 'aksrt')];
+ }
+
+ return self::complete_email_verification($user);
+ }
+
+ public static function verify_email_code(int $user_id, string $code): array {
+ $user = get_userdata($user_id);
+ if (!$user || is_wp_error($user)) {
+ return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
+ }
+
+ // 先做 IP 节流:相同 IP 在 EMAIL_CODE_IP_TTL 内累计失败到上限即直接拒绝
+ $ip_key = self::email_code_ip_throttle_key($user_id);
+ if ($ip_key !== '') {
+ $ip_attempts = (int) get_transient($ip_key);
+ if ($ip_attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS) {
+ AKSRT_Auth_Service::log_event('verify.code.ip_throttled', ['uid' => $user_id, 'attempts' => $ip_attempts]);
+ return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
+ }
+ }
+
+ // 用户级失败计数:达到上限后该验证码立即作废
+ $user_attempts = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META, true);
+ if ($user_attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS) {
+ delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
+ update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, 0);
+ AKSRT_Auth_Service::log_event('verify.code.locked.read', ['uid' => $user_id]);
+ return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
+ }
+
+ $stored_hash = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META, true);
+ $expires_at = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META, true);
+ if ($stored_hash === '' || $expires_at <= time() || !wp_check_password($code, $stored_hash)) {
+ return self::fail_email_code_attempt($user_id);
+ }
+
+ self::reset_email_code_attempt_trackers($user_id);
+ return self::complete_email_verification($user);
+ }
+
+ public static function complete_email_verification(WP_User $user): array {
+ $user_id = (int) $user->ID;
+ $obk = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_OBK_META, true);
+ $oauth = $obk !== '' ? AKSRT_Auth_Service::get_oauth_bind_context($obk) : null;
+ $email = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META, true);
+ $was_verified = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, true) === 1;
+
+ update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
+ self::clear_email_verification($user_id);
+ AKSRT_Auth_Service::log_event('verify.success', ['uid' => $user_id]);
+ // 状态跳变时触发 hook,邀请系统等模块据此发奖
+ if (!$was_verified) {
+ do_action('aksrt_email_verified', $user_id);
+ }
+ if ($email !== '' && is_email($email)) {
+ self::send_registration_verified_notice_email($user, $email);
+ }
+
+ if ($oauth && is_array($oauth)) {
+ AKSRT_Auth_Service::finalize_oauth_registration($user_id, $oauth, $obk);
+ } elseif (AKSRT_Settings::registration_auto_login_after_verify()) {
+ wp_set_current_user($user_id);
+ wp_set_auth_cookie($user_id, true);
+ }
+
+ $redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/');
+ if (!AKSRT_Settings::registration_auto_login_after_verify()) {
+ $redirect = aksrt_get_login_url();
+ }
+
+ return [
+ 'success' => true,
+ 'message' => __('邮箱验证成功,即将进入你的账号。', 'aksrt'),
+ 'redirect' => $redirect,
+ ];
+ }
+
+ // ============================================================
+ // 通知邮件
+ // ============================================================
+
+ public static function send_registration_verification_link_email(WP_User $user, string $email, string $token): bool {
+ $url = AKSRT_Auth_Service::get_email_verification_url((int) $user->ID, ['token' => $token]);
+ $site_name = get_bloginfo('name');
+ $default_subject = '[' . $site_name . '] ' . __('验证你的邮箱地址', 'aksrt');
+ $default_content = sprintf(
+ __("您好 %s,\n\n欢迎注册 %s。\n\n请点击以下链接完成邮箱验证:\n\n%s\n\n验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
+ $user->user_login, $site_name, $url
+ );
+
+ return AKSRT_Settings::send_event_email(
+ 'reg_verify_link', $email, $default_subject, $default_content,
+ ['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $url]
+ );
+ }
+
+ public static function send_registration_verification_code_email(WP_User $user, string $email, string $code): bool {
+ $site_name = get_bloginfo('name');
+ $default_subject = '[' . $site_name . '] ' . __('邮箱验证码', 'aksrt');
+ $default_content = sprintf(
+ __("您好 %s,\n\n欢迎注册 %s。\n\n你的邮箱验证码为:\n\n【%s】\n\n验证码验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
+ $user->user_login, $site_name, $code
+ );
+
+ return AKSRT_Settings::send_event_email(
+ 'reg_verify_code', $email, $default_subject, $default_content,
+ ['{username}' => $user->user_login, '{site_name}' => $site_name, '{code}' => $code]
+ );
+ }
+
+ public static function send_registration_verified_notice_email(WP_User $user, string $email): bool {
+ $site_name = get_bloginfo('name');
+ $default_subject = '[' . $site_name . '] ' . __('邮箱验证成功', 'aksrt');
+ $default_content = sprintf(
+ __("您好 %s,\n\n你在 %s 的账号邮箱已经验证成功。\n\n如果这不是你的操作,请立即联系站点管理员。", 'aksrt'),
+ $user->user_login, $site_name
+ );
+
+ return AKSRT_Settings::send_event_email(
+ 'reg_success', $email, $default_subject, $default_content,
+ ['{username}' => $user->user_login, '{site_name}' => $site_name]
+ );
+ }
+}
diff --git a/inc/classes/class-aksrt-admin.php b/inc/classes/class-aksrt-admin.php
new file mode 100644
index 00000000..ebfb4909
--- /dev/null
+++ b/inc/classes/class-aksrt-admin.php
@@ -0,0 +1,254 @@
+';
+ echo '
';
+ echo ' ';
+ }
+
+ public function enqueue_admin_assets($hook): void {
+ $screen = function_exists('get_current_screen') ? get_current_screen() : null;
+ if (!$screen || $screen->base !== 'toplevel_page_' . self::PAGE_SLUG) {
+ return;
+ }
+
+ $build_dir = trailingslashit(AKSRT_DIR) . 'assets/admin/build/';
+ $build_uri = trailingslashit(AKSRT_URI) . 'assets/admin/build/';
+
+ $css_files = [
+ 'naive-ui.css',
+ 'index.css',
+ 'index2.css',
+ ];
+ foreach ($css_files as $css_file) {
+ $path = $build_dir . $css_file;
+ if (file_exists($path)) {
+ wp_enqueue_style(
+ 'aksrt-admin-' . sanitize_key(str_replace(['.', '/'], '-', $css_file)),
+ $build_uri . $css_file,
+ [],
+ (string) filemtime($path)
+ );
+ }
+ }
+
+ $entry = $build_dir . 'index.js';
+ if (file_exists($entry)) {
+ wp_enqueue_script(
+ 'aksrt-admin-app',
+ $build_uri . 'index.js',
+ [],
+ (string) filemtime($entry),
+ true
+ );
+
+ wp_localize_script('aksrt-admin-app', 'aksrtAdminData', [
+ 'apiUrl' => esc_url_raw(rest_url(self::API_NAMESPACE)),
+ 'nonce' => wp_create_nonce('wp_rest'),
+ 'siteUrl' => home_url('/'),
+ 'adminUrl' => admin_url('/'),
+ 'themeVersion' => wp_get_theme()->get('Version') ?: '3.4.0-dev',
+ ]);
+ }
+
+ wp_enqueue_media();
+ }
+
+ public function add_module_type_attribute(string $tag, string $handle): string {
+ if ('aksrt-admin-app' === $handle) {
+ return str_replace('
+'; ?>
+ is_enabled()) {
+ // If disabled, ensure old rules are cleaned up
+ add_action('init', [$this, 'maybe_restore']);
+ return;
+ }
+
+ add_filter('category_link', [$this, 'remove_category_base'], 10, 2);
+ add_filter('term_link', [$this, 'remove_category_base_term'], 10, 3);
+ add_action('init', [$this, 'add_rewrite_rules']);
+ add_action('template_redirect', [$this, 'redirect_old_urls']);
+ add_action('admin_init', [$this, 'maybe_flush']);
+ }
+
+ /**
+ * Check if feature is enabled
+ */
+ private function is_enabled(): bool {
+ if (!class_exists('AKSRT_Settings')) return false;
+ // schema key = seo_no_category; runtime alias = seo_remove_category
+ return (bool) (
+ AKSRT_Settings::get_value('seo_remove_category', false)
+ ?: AKSRT_Settings::get_value('seo_no_category', false)
+ );
+ }
+
+ /**
+ * Remove /category/ from category link
+ */
+ public function remove_category_base(string $link, $term): string {
+ $base = get_option('category_base');
+ if ($base && $base !== 'category') return $link;
+
+ // Only process category taxonomy
+ if ($term instanceof WP_Term && $term->taxonomy !== 'category') {
+ return $link;
+ }
+
+ $category_base_url = preg_quote(trailingslashit(home_url('/category')), '#');
+ return preg_replace('#' . $category_base_url . '#', trailingslashit(home_url('/')), $link, 1);
+ }
+
+ /**
+ * Remove base from term_link for categories
+ */
+ public function remove_category_base_term(string $link, $term, $taxonomy): string {
+ if ($taxonomy !== 'category') return $link;
+ return $this->remove_category_base($link, $term);
+ }
+
+ /**
+ * Add rewrite rules for category URLs without /category/
+ */
+ public function add_rewrite_rules(): void {
+ // Get all category slugs
+ $categories = get_categories(['hide_empty' => false]);
+ foreach ($categories as $cat) {
+ if (empty($cat->slug)) continue;
+ $slug = $cat->slug;
+
+ // /slug/ → index.php?category_name=$slug
+ add_rewrite_rule(
+ '^' . preg_quote($slug, '/') . '/?$',
+ 'index.php?category_name=' . $slug,
+ 'top'
+ );
+
+ // /slug/page/N/ → index.php?category_name=$slug&paged=$N
+ add_rewrite_rule(
+ '^' . preg_quote($slug, '/') . '/page/([0-9]+)/?$',
+ 'index.php?category_name=$slug&paged=$matches[1]',
+ 'top'
+ );
+
+ // /slug/feed/ → index.php?category_name=$slug&feed=$feed
+ add_rewrite_rule(
+ '^' . preg_quote($slug, '/') . '/feed/?$',
+ 'index.php?category_name=$slug&feed=rss2',
+ 'top'
+ );
+ }
+ }
+
+ /**
+ * 301 redirect old /category/ URLs to new ones
+ */
+ public function redirect_old_urls(): void {
+ $request = $_SERVER['REQUEST_URI'] ?? '';
+ $home_path = wp_parse_url(home_url('/'), PHP_URL_PATH);
+
+ // Remove home path prefix
+ if ($home_path && strpos($request, $home_path) === 0) {
+ $request = substr($request, strlen($home_path));
+ }
+
+ // Check if request starts with /category/
+ if (preg_match('#^category/(.+?)/?$#', $request, $matches)) {
+ $new_url = home_url('/' . $matches[1] . '/');
+ wp_redirect($new_url, 301);
+ exit;
+ }
+ }
+
+ /**
+ * Flush rewrite rules when setting changes
+ */
+ public function maybe_flush(): void {
+ $rewrite_version = $this->rewrite_version();
+ $stored = (string) get_option('aksrt_category_rewrite_flushed', '');
+ if ($stored !== $rewrite_version) {
+ flush_rewrite_rules();
+ update_option('aksrt_category_rewrite_flushed', $rewrite_version);
+ }
+ }
+
+ /**
+ * Restore default rewrite rules when feature is disabled
+ */
+ public function maybe_restore(): void {
+ $stored = (string) get_option('aksrt_category_rewrite_flushed', '');
+ if ($stored !== '') {
+ flush_rewrite_rules();
+ delete_option('aksrt_category_rewrite_flushed');
+ }
+ }
+
+ /**
+ * Force flush (call after enabling/disabling)
+ */
+ public static function force_flush(): void {
+ flush_rewrite_rules();
+ update_option('aksrt_category_rewrite_flushed', AKSRT_VERSION . ':category-rewrite-v1');
+ }
+}
diff --git a/inc/classes/class-aksrt-charge-card.php b/inc/classes/class-aksrt-charge-card.php
new file mode 100644
index 00000000..c5babef4
--- /dev/null
+++ b/inc/classes/class-aksrt-charge-card.php
@@ -0,0 +1,209 @@
+get_var($wpdb->prepare(
+ "SELECT id FROM {$this->table()} WHERE code = %s",
+ $code
+ ));
+ if ($exists) return 0;
+
+ $result = $wpdb->insert($this->table(), [
+ 'code' => $code,
+ 'amount' => $amount,
+ 'balance' => $amount,
+ 'status' => self::STATUS_ACTIVE,
+ 'expires_at' => $expires_at !== '' ? $expires_at : null,
+ 'created_at' => current_time('mysql'),
+ 'note' => $note,
+ ]);
+
+ return $result ? (int) $wpdb->insert_id : 0;
+ }
+
+ /**
+ * 批量生成充值卡
+ */
+ public function batch_create(float $amount, int $count, int $length = 16, string $prefix = ''): array {
+ $codes = [];
+ for ($i = 0; $i < $count; $i++) {
+ $code = $prefix . strtoupper(wp_generate_password($length, false));
+ $id = $this->create([
+ 'code' => $code,
+ 'amount' => $amount,
+ ]);
+ if ($id > 0) {
+ $codes[] = $code;
+ }
+ }
+ return $codes;
+ }
+
+ /**
+ * 使用充值卡
+ */
+ public function redeem(string $code, int $user_id): array {
+ global $wpdb;
+
+ $code = sanitize_text_field($code);
+ $card = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM {$this->table()} WHERE code = %s",
+ $code
+ ), ARRAY_A);
+
+ if (!$card) {
+ return ['success' => false, 'message' => '充值卡不存在'];
+ }
+
+ if ($card['status'] !== self::STATUS_ACTIVE) {
+ return ['success' => false, 'message' => '充值卡已使用或已失效'];
+ }
+
+ if ($card['expires_at'] && strtotime($card['expires_at']) < time()) {
+ // 标记过期
+ $wpdb->update($this->table(), ['status' => self::STATUS_EXPIRED], ['id' => (int) $card['id']]);
+ return ['success' => false, 'message' => '充值卡已过期'];
+ }
+
+ $amount = (float) $card['balance'];
+
+ // 更新卡状态
+ $wpdb->update($this->table(), [
+ 'status' => self::STATUS_USED,
+ 'used_by' => $user_id,
+ 'used_at' => current_time('mysql'),
+ 'balance' => 0,
+ ], ['id' => (int) $card['id']]);
+
+ // 增加用户余额
+ if (class_exists('AKSRT_Credits')) {
+ AKSRT_Credits::add($user_id, (int) $amount, 'charge_card', [
+ 'ref_type' => 'charge_card',
+ 'ref_id' => (int) $card['id'],
+ ]);
+ }
+
+ return [
+ 'success' => true,
+ 'message' => sprintf('充值成功,已到账 %s 元', number_format($amount, 2)),
+ 'amount' => $amount,
+ ];
+ }
+
+ /**
+ * 获取充值卡列表
+ */
+ public function get_list(array $args = []): array {
+ global $wpdb;
+
+ $limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
+ $offset = max(0, (int) ($args['offset'] ?? 0));
+ $status = sanitize_key((string) ($args['status'] ?? ''));
+
+ $where = [];
+ $params = [];
+
+ if ($status !== '') {
+ $where[] = 'status = %s';
+ $params[] = $status;
+ }
+
+ $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
+
+ $count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
+ $total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql);
+
+ $sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
+ $params[] = $limit;
+ $params[] = $offset;
+
+ $items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
+
+ return [
+ 'items' => $items ?: [],
+ 'total' => $total,
+ 'page' => floor($offset / $limit) + 1,
+ 'max_pages' => ceil($total / $limit),
+ ];
+ }
+
+ /**
+ * 禁用充值卡
+ */
+ public function disable(int $id): bool {
+ global $wpdb;
+ return (bool) $wpdb->update($this->table(), [
+ 'status' => self::STATUS_DISABLED,
+ ], ['id' => $id]);
+ }
+
+ /**
+ * 获取统计
+ */
+ public function get_stats(): array {
+ global $wpdb;
+ $table = $this->table();
+
+ $row = $wpdb->get_row(
+ "SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
+ SUM(CASE WHEN status = 'used' THEN 1 ELSE 0 END) as used,
+ SUM(amount) as total_amount,
+ SUM(CASE WHEN status = 'used' THEN amount ELSE 0 END) as used_amount
+ FROM {$table}",
+ ARRAY_A
+ );
+
+ return [
+ 'total' => (int) ($row['total'] ?? 0),
+ 'active' => (int) ($row['active'] ?? 0),
+ 'used' => (int) ($row['used'] ?? 0),
+ 'total_amount' => (float) ($row['total_amount'] ?? 0),
+ 'used_amount' => (float) ($row['used_amount'] ?? 0),
+ ];
+ }
+}
diff --git a/inc/classes/class-aksrt-comment-actions.php b/inc/classes/class-aksrt-comment-actions.php
new file mode 100644
index 00000000..10c43f45
--- /dev/null
+++ b/inc/classes/class-aksrt-comment-actions.php
@@ -0,0 +1,348 @@
+ 'POST',
+ 'callback' => [$this, 'edit_comment'],
+ 'permission_callback' => [$this, 'check_edit_permission'],
+ 'args' => [
+ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
+ 'content' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_textarea_field'],
+ ],
+ ]);
+
+ register_rest_route('aksrt/v1', '/comment/delete', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'delete_comment'],
+ 'permission_callback' => [$this, 'check_delete_permission'],
+ 'args' => [
+ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
+ ],
+ ]);
+
+ register_rest_route('aksrt/v1', '/comment/moderate', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'moderate_comment'],
+ 'permission_callback' => function () {
+ return current_user_can('moderate_comments');
+ },
+ 'args' => [
+ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
+ 'status' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
+ ],
+ ]);
+
+ register_rest_route('aksrt/v1', '/comment/pin', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'pin_comment'],
+ 'permission_callback' => [$this, 'check_pin_permission'],
+ 'args' => [
+ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
+ 'pinned' => ['required' => false, 'type' => 'boolean'],
+ ],
+ ]);
+
+ register_rest_route('aksrt/v1', '/comment/like', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'like_comment'],
+ // Guests may like; abuse is bounded by per-visitor toggle state.
+ 'permission_callback' => '__return_true',
+ 'args' => [
+ 'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
+ 'active' => ['required' => false, 'type' => 'boolean'],
+ ],
+ ]);
+ }
+
+ /**
+ * Resolve the per-comment action contract (single source of truth).
+ *
+ * @param int $comment_id Comment ID.
+ * @return array{can_edit:bool,can_delete:bool,can_moderate:bool,can_pin:bool}
+ */
+ private function permissions(int $comment_id): array {
+ if (function_exists('aksrt_comment_action_permissions')) {
+ return aksrt_comment_action_permissions($comment_id);
+ }
+
+ // Defensive fallback if the helper is unavailable.
+ $comment = get_comment($comment_id);
+ $is_mod = current_user_can('moderate_comments');
+ $is_owner = $comment && is_user_logged_in() && (int) $comment->user_id === get_current_user_id();
+ return [
+ 'can_edit' => $is_mod || $is_owner,
+ 'can_delete' => $is_mod || $is_owner,
+ 'can_moderate' => $is_mod,
+ 'can_pin' => $is_mod,
+ ];
+ }
+
+ /**
+ * Permission: can edit? (author within configured window, or moderator)
+ */
+ public function check_edit_permission(\WP_REST_Request $request): bool {
+ if (!is_user_logged_in()) return false;
+ return $this->permissions((int) $request->get_param('comment_id'))['can_edit'];
+ }
+
+ /**
+ * Permission: can delete? (author, or moderator)
+ */
+ public function check_delete_permission(\WP_REST_Request $request): bool {
+ if (!is_user_logged_in()) return false;
+ return $this->permissions((int) $request->get_param('comment_id'))['can_delete'];
+ }
+
+ /**
+ * Permission: can pin? (moderator, top-level only)
+ */
+ public function check_pin_permission(\WP_REST_Request $request): bool {
+ if (!is_user_logged_in()) return false;
+ return $this->permissions((int) $request->get_param('comment_id'))['can_pin'];
+ }
+
+ /**
+ * Edit comment content
+ */
+ public function edit_comment(\WP_REST_Request $request): \WP_REST_Response {
+ $comment_id = $request->get_param('comment_id');
+ $content = $request->get_param('content');
+
+ $comment = get_comment($comment_id);
+ if (!$comment) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'comment_not_found',
+ 'message' => __('评论不存在。', 'aksrt'),
+ ], 404);
+ }
+
+ // Sanitize with wp_kses (allow basic HTML)
+ $allowed_html = [
+ 'a' => ['href' => [], 'title' => [], 'target' => []],
+ 'code' => [],
+ 'pre' => [],
+ 'strong' => [],
+ 'em' => [],
+ 'br' => [],
+ 'p' => [],
+ ];
+ $clean_content = wp_kses($content, $allowed_html);
+
+ $updated = wp_update_comment([
+ 'comment_ID' => $comment_id,
+ 'comment_content' => $clean_content,
+ ]);
+
+ if (is_wp_error($updated)) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'update_failed',
+ 'message' => __('更新失败。', 'aksrt'),
+ ], 500);
+ }
+
+ return new \WP_REST_Response([
+ 'success' => true,
+ 'code' => 'updated',
+ 'message' => __('评论已更新。', 'aksrt'),
+ 'data' => [
+ 'content' => $clean_content,
+ ],
+ ]);
+ }
+
+ /**
+ * Delete comment
+ */
+ public function delete_comment(\WP_REST_Request $request): \WP_REST_Response {
+ $comment_id = $request->get_param('comment_id');
+
+ $comment = get_comment($comment_id);
+ if (!$comment) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'comment_not_found',
+ 'message' => __('评论不存在。', 'aksrt'),
+ ], 404);
+ }
+
+ $deleted = wp_delete_comment($comment_id, false); // soft delete
+ if (!$deleted) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'delete_failed',
+ 'message' => __('删除失败。', 'aksrt'),
+ ], 500);
+ }
+
+ return new \WP_REST_Response([
+ 'success' => true,
+ 'code' => 'deleted',
+ 'message' => __('评论已删除。', 'aksrt'),
+ ]);
+ }
+
+ /**
+ * Moderate comment (approve / hold / spam)
+ */
+ public function moderate_comment(\WP_REST_Request $request): \WP_REST_Response {
+ $comment_id = $request->get_param('comment_id');
+ $status = $request->get_param('status');
+
+ $allowed_statuses = ['approve', 'hold', 'spam'];
+ if (!in_array($status, $allowed_statuses, true)) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'invalid_status',
+ 'message' => __('无效状态。', 'aksrt'),
+ ], 400);
+ }
+
+ $comment = get_comment($comment_id);
+ if (!$comment) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'comment_not_found',
+ 'message' => __('评论不存在。', 'aksrt'),
+ ], 404);
+ }
+
+ $status_map = [
+ 'approve' => 'approved',
+ 'hold' => 'hold',
+ 'spam' => 'spam',
+ ];
+
+ $result = wp_set_comment_status($comment_id, $status_map[$status]);
+ if (is_wp_error($result)) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'moderate_failed',
+ 'message' => __('操作失败。', 'aksrt'),
+ ], 500);
+ }
+
+ return new \WP_REST_Response([
+ 'success' => true,
+ 'code' => 'moderated',
+ 'message' => __('操作成功。', 'aksrt'),
+ 'data' => [
+ 'status' => $status_map[$status],
+ ],
+ ]);
+ }
+
+ /**
+ * Toggle a comment's pinned ("置顶") state. Top-level comments only.
+ */
+ public function pin_comment(\WP_REST_Request $request): \WP_REST_Response {
+ $comment_id = (int) $request->get_param('comment_id');
+ $comment = get_comment($comment_id);
+ if (!$comment) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'comment_not_found',
+ 'message' => __('评论不存在。', 'aksrt'),
+ ], 404);
+ }
+
+ if ((int) $comment->comment_parent !== 0) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'cannot_pin_reply',
+ 'message' => __('仅顶层评论可置顶。', 'aksrt'),
+ ], 400);
+ }
+
+ $current = '1' === (string) get_comment_meta($comment_id, '_aksrt_comment_pinned', true);
+ $target = null !== $request->get_param('pinned')
+ ? (bool) $request->get_param('pinned')
+ : !$current;
+
+ if ($target) {
+ update_comment_meta($comment_id, '_aksrt_comment_pinned', '1');
+ } else {
+ delete_comment_meta($comment_id, '_aksrt_comment_pinned');
+ }
+
+ return new \WP_REST_Response([
+ 'success' => true,
+ 'code' => 'pinned',
+ 'message' => $target ? __('评论已置顶。', 'aksrt') : __('已取消置顶。', 'aksrt'),
+ 'data' => [
+ 'pinned' => $target,
+ ],
+ ]);
+ }
+
+ /**
+ * Toggle the current visitor's like on a comment.
+ */
+ public function like_comment(\WP_REST_Request $request): \WP_REST_Response {
+ $comment_id = (int) $request->get_param('comment_id');
+ $comment = get_comment($comment_id);
+ if (!$comment || '1' !== (string) $comment->comment_approved) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'comment_unavailable',
+ 'message' => __('评论不存在或未通过审核。', 'aksrt'),
+ ], 404);
+ }
+
+ if (!function_exists('aksrt_is_comment_liked') || !function_exists('aksrt_set_comment_like_state') || !function_exists('aksrt_update_comment_counter')) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'like_unavailable',
+ 'message' => __('点赞功能不可用。', 'aksrt'),
+ ], 500);
+ }
+
+ $current = aksrt_is_comment_liked($comment_id);
+ $target = null !== $request->get_param('active')
+ ? (bool) $request->get_param('active')
+ : !$current;
+
+ if ($target !== $current) {
+ aksrt_set_comment_like_state($comment_id, $target);
+ $count = aksrt_update_comment_counter($comment_id, 'aksrt_comment_likes', $target ? 1 : -1);
+ } else {
+ $count = aksrt_get_comment_likes($comment_id);
+ }
+
+ return new \WP_REST_Response([
+ 'success' => true,
+ 'code' => 'liked',
+ 'data' => [
+ 'active' => $target,
+ 'count' => $count,
+ ],
+ ]);
+ }
+}
diff --git a/inc/classes/class-aksrt-content-audit.php b/inc/classes/class-aksrt-content-audit.php
new file mode 100644
index 00000000..29e796bf
--- /dev/null
+++ b/inc/classes/class-aksrt-content-audit.php
@@ -0,0 +1,357 @@
+get_config();
+
+ if (empty($config['content_audit_enabled']) || empty($config['content_audit_text_enabled'])) {
+ return $this->ok('disabled');
+ }
+
+ $provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
+ $result = $this->call_text_api($provider, $text, $config);
+
+ if (!empty($config['content_audit_log_enabled'])) {
+ $this->log_audit('text', $text, $result);
+ }
+
+ if (!$result['passed'] && ($config['content_audit_on_failure'] ?? 'allow') === 'allow') {
+ $result['passed'] = true;
+ $result['reason'] .= ' (failure_policy: allow)';
+ }
+
+ return $result;
+ }
+
+ /**
+ * 审核图片
+ *
+ * @param string $image_path 本地文件路径或 URL
+ * @return array{passed: bool, reason: string, labels: array, raw: array}
+ */
+ public function audit_image(string $image_path): array {
+ $config = $this->get_config();
+
+ if (empty($config['content_audit_enabled']) || empty($config['content_audit_image_enabled'])) {
+ return $this->ok('disabled');
+ }
+
+ $provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
+ $result = $this->call_image_api($provider, $image_path, $config);
+
+ if (!empty($config['content_audit_log_enabled'])) {
+ $this->log_audit('image', $image_path, $result);
+ }
+
+ if (!$result['passed'] && ($config['content_audit_on_failure'] ?? 'allow') === 'allow') {
+ $result['passed'] = true;
+ $result['reason'] .= ' (failure_policy: allow)';
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取服务运行时状态
+ */
+ public function get_status(): array {
+ $config = $this->get_config();
+ $enabled = !empty($config['content_audit_enabled']);
+ $provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
+
+ $missing = [];
+ if ($enabled && $provider === 'baidu') {
+ if (empty($config['content_audit_api_key'])) $missing[] = 'content_audit_api_key';
+ if (empty($config['content_audit_api_secret'])) $missing[] = 'content_audit_api_secret';
+ }
+
+ return [
+ 'enabled' => $enabled,
+ 'provider' => $provider,
+ 'configured' => $enabled && ($provider === 'null' || empty($missing)),
+ 'available' => $enabled,
+ 'verified' => !empty($config['content_audit_verified']),
+ 'transport' => 'http_api',
+ 'missing_fields' => $missing,
+ 'text_enabled' => !empty($config['content_audit_text_enabled']),
+ 'image_enabled' => !empty($config['content_audit_image_enabled']),
+ 'on_failure' => (string) ($config['content_audit_on_failure'] ?? 'allow'),
+ 'log_enabled' => !empty($config['content_audit_log_enabled']),
+ ];
+ }
+
+ /* ============================================================
+ * Provider dispatch
+ * ============================================================ */
+
+ private function call_text_api(string $provider, string $text, array $config): array {
+ switch ($provider) {
+ case 'baidu':
+ return $this->baidu_text_censor($text, $config);
+ default:
+ return $this->local_pattern_check($text, $provider);
+ }
+ }
+
+ private function call_image_api(string $provider, string $image_path, array $config): array {
+ switch ($provider) {
+ case 'baidu':
+ return $this->baidu_image_censor($image_path, $config);
+ default:
+ return $this->ok('no_image_provider');
+ }
+ }
+
+ /* ============================================================
+ * 百度内容审核 Provider
+ * ============================================================ */
+
+ /**
+ * 百度文本审核
+ */
+ private function baidu_text_censor(string $text, array $config): array {
+ $api_key = trim((string) ($config['content_audit_api_key'] ?? ''));
+ $api_secret = trim((string) ($config['content_audit_api_secret'] ?? ''));
+ if ($api_key === '' || $api_secret === '') {
+ return ['passed' => false, 'reason' => 'config_missing', 'labels' => [], 'raw' => []];
+ }
+
+ $token = $this->baidu_get_access_token($api_key, $api_secret, $config);
+ if ($token === '') {
+ return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => []];
+ }
+
+ $censor_url = trim((string) ($config['content_audit_baidu_text_url'] ?? ''));
+ if ($censor_url === '') {
+ $censor_url = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined';
+ }
+
+ $request_url = add_query_arg(['access_token' => $token], $censor_url);
+ $response = wp_remote_post($request_url, [
+ 'timeout' => 15,
+ 'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
+ 'body' => http_build_query(['text' => $text]),
+ ]);
+
+ if (is_wp_error($response)) {
+ return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['error' => $response->get_error_message()]];
+ }
+
+ $status_code = (int) wp_remote_retrieve_response_code($response);
+ if ($status_code !== 200) {
+ return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['status' => $status_code]];
+ }
+
+ $payload = json_decode((string) wp_remote_retrieve_body($response), true);
+ if (!is_array($payload) || isset($payload['error_code'])) {
+ return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => $payload ?: []];
+ }
+
+ return $this->parse_baidu_result($payload);
+ }
+
+ /**
+ * 百度图片审核
+ */
+ private function baidu_image_censor(string $image_path, array $config): array {
+ $api_key = trim((string) ($config['content_audit_api_key'] ?? ''));
+ $api_secret = trim((string) ($config['content_audit_api_secret'] ?? ''));
+ if ($api_key === '' || $api_secret === '') {
+ return ['passed' => false, 'reason' => 'config_missing', 'labels' => [], 'raw' => []];
+ }
+
+ $token = $this->baidu_get_access_token($api_key, $api_secret, $config);
+ if ($token === '') {
+ return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => []];
+ }
+
+ // 支持本地路径和 URL
+ if (filter_var($image_path, FILTER_VALIDATE_URL)) {
+ $image_data = @file_get_contents($image_path);
+ } else {
+ $image_data = @file_get_contents($image_path);
+ }
+ if ($image_data === false) {
+ return ['passed' => false, 'reason' => 'file_error', 'labels' => [], 'raw' => []];
+ }
+
+ $censor_url = trim((string) ($config['content_audit_baidu_img_url'] ?? ''));
+ if ($censor_url === '') {
+ $censor_url = 'https://aip.baidubce.com/rest/2.0/solution/v1/img_censor/v2/user_defined';
+ }
+
+ $request_url = add_query_arg(['access_token' => $token], $censor_url);
+ $response = wp_remote_post($request_url, [
+ 'timeout' => 30,
+ 'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
+ 'body' => http_build_query(['image' => base64_encode($image_data)]),
+ ]);
+
+ if (is_wp_error($response)) {
+ return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['error' => $response->get_error_message()]];
+ }
+
+ $status_code = (int) wp_remote_retrieve_response_code($response);
+ if ($status_code !== 200) {
+ return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['status' => $status_code]];
+ }
+
+ $payload = json_decode((string) wp_remote_retrieve_body($response), true);
+ if (!is_array($payload) || isset($payload['error_code'])) {
+ return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => $payload ?: []];
+ }
+
+ return $this->parse_baidu_result($payload);
+ }
+
+ /**
+ * 百度 access_token 获取(带缓存)
+ */
+ private function baidu_get_access_token(string $api_key, string $api_secret, array $config): string {
+ $cache_key = 'aksrt_baidu_token_' . md5($api_key);
+ $cached = get_transient($cache_key);
+ if ($cached) return (string) $cached;
+
+ $token_url = trim((string) ($config['content_audit_baidu_token_url'] ?? ''));
+ if ($token_url === '') {
+ $token_url = 'https://aip.baidubce.com/oauth/2.0/token';
+ }
+
+ $request_url = add_query_arg([
+ 'grant_type' => 'client_credentials',
+ 'client_id' => $api_key,
+ 'client_secret' => $api_secret,
+ ], $token_url);
+
+ $response = wp_remote_get($request_url, ['timeout' => 10]);
+ if (is_wp_error($response)) return '';
+
+ $status_code = (int) wp_remote_retrieve_response_code($response);
+ if ($status_code !== 200) return '';
+
+ $payload = json_decode((string) wp_remote_retrieve_body($response), true);
+ if (!is_array($payload) || isset($payload['error'])) return '';
+
+ $token = (string) ($payload['access_token'] ?? '');
+ $expires = max(3600, (int) ($payload['expires_in'] ?? 3600) - 300);
+ if ($token !== '') {
+ set_transient($cache_key, $token, $expires);
+ }
+ return $token;
+ }
+
+ /**
+ * 解析百度审核结果 → 统一格式
+ */
+ private function parse_baidu_result(array $payload): array {
+ $conclusion_type = isset($payload['conclusionType']) ? (int) $payload['conclusionType'] : 1;
+ $conclusion = (string) ($payload['conclusion'] ?? '');
+ $top_category = $this->baidu_extract_top_category($payload);
+
+ // conclusionType: 1=合规, 2=不合规, 3=疑似, 4=审核失败
+ if ($conclusion_type === 1) {
+ return [
+ 'passed' => true,
+ 'reason' => '',
+ 'labels' => ['compliant'],
+ 'raw' => $payload,
+ ];
+ }
+
+ $score_map = [2 => 95, 3 => 70, 4 => 60];
+ $reason_map = [2 => 'non_compliant', 3 => 'suspected', 4 => 'review_failed'];
+
+ return [
+ 'passed' => false,
+ 'reason' => $reason_map[$conclusion_type] ?? 'unknown',
+ 'labels' => [$top_category ?: ('baidu_type_' . $conclusion_type)],
+ 'raw' => $payload,
+ ];
+ }
+
+ private function baidu_extract_top_category(array $payload): string {
+ if (!isset($payload['data']) || !is_array($payload['data']) || empty($payload['data'][0])) {
+ return '';
+ }
+ $first = $payload['data'][0];
+ if (!is_array($first)) return '';
+ if (!empty($first['msg'])) return (string) $first['msg'];
+ if (!empty($first['type'])) return 'type_' . (string) $first['type'];
+ if (!empty($first['subType'])) return 'sub_' . (string) $first['subType'];
+ return '';
+ }
+
+ /* ============================================================
+ * 本地基础过滤(无 API 时的 fallback)
+ * ============================================================ */
+
+ private function local_pattern_check(string $text, string $provider): array {
+ $forbidden_patterns = ['/赌博/i', '/色情/i', '/枪支/i', '/毒品/i'];
+ foreach ($forbidden_patterns as $pattern) {
+ if (preg_match($pattern, $text, $m)) {
+ return [
+ 'passed' => false,
+ 'reason' => sprintf('本地敏感词匹配: %s', $m[0]),
+ 'labels' => ['forbidden'],
+ 'raw' => ['provider' => $provider, 'method' => 'local_pattern'],
+ ];
+ }
+ }
+ return $this->ok('passthrough');
+ }
+
+ /* ============================================================
+ * Helpers
+ * ============================================================ */
+
+ private function ok(string $reason = ''): array {
+ return ['passed' => true, 'reason' => $reason, 'labels' => [], 'raw' => []];
+ }
+
+ private function get_config(): array {
+ if (class_exists('AKSRT_Settings')) {
+ return AKSRT_Settings::get_content_audit_settings();
+ }
+ return ['content_audit_enabled' => false];
+ }
+
+ private function log_audit(string $type, string $content, array $result): void {
+ $log = get_option('aksrt_content_audit_log', []);
+ $log[] = [
+ 'type' => $type,
+ 'content' => mb_substr($content, 0, 100),
+ 'passed' => $result['passed'],
+ 'reason' => $result['reason'] ?? '',
+ 'time' => current_time('mysql'),
+ ];
+ if (count($log) > 500) {
+ $log = array_slice($log, -500);
+ }
+ update_option('aksrt_content_audit_log', $log);
+ }
+}
diff --git a/inc/classes/class-aksrt-content-visibility.php b/inc/classes/class-aksrt-content-visibility.php
new file mode 100644
index 00000000..0531b0b8
--- /dev/null
+++ b/inc/classes/class-aksrt-content-visibility.php
@@ -0,0 +1,282 @@
+
+ */
+ public static function get_post_access_state($post = null, int $user_id = 0): array {
+ $resource_post = get_post($post);
+ $uid = $user_id > 0 ? $user_id : get_current_user_id();
+ $pay_meta = function_exists('aksrt_get_pay_meta')
+ ? aksrt_get_pay_meta($resource_post)
+ : AKSRT_Domain_Model::default_pay_meta();
+ $pay_type = AKSRT_Domain_Model::normalize_pay_type((string) ($pay_meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
+ $price = (float) ($pay_meta['price'] ?? 0);
+ $paid_settings = class_exists('AKSRT_Settings')
+ ? AKSRT_Settings::get_paid_content_settings()
+ : [
+ 'download_login' => true,
+ 'download_credit' => true,
+ 'download_captcha' => false,
+ ];
+
+ $state = [
+ 'post_id' => $resource_post instanceof WP_Post ? (int) $resource_post->ID : 0,
+ 'pay_type' => $pay_type,
+ 'price' => $price,
+ 'need_login' => false,
+ 'need_purchase' => false,
+ 'need_vip' => false,
+ 'require_captcha' => false,
+ 'has_paid_access' => false,
+ 'can_view' => true,
+ 'can_download' => true,
+ 'download_cost' => 0,
+ ];
+
+ if (!($resource_post instanceof WP_Post)) {
+ $state['can_view'] = false;
+ $state['can_download'] = false;
+ return $state;
+ }
+
+ if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO) {
+ return $state;
+ }
+
+ $has_free_access = $price <= 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_VIP;
+ $has_paid_order = false;
+
+ if ($uid > 0 && class_exists('AKSRT_Order')) {
+ $has_paid_order = AKSRT_Order::get_instance()->has_paid_access(
+ $uid,
+ AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
+ (int) $resource_post->ID
+ );
+ }
+
+ $has_vip_access = false;
+ if ($uid > 0) {
+ if (function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($uid)) {
+ $has_vip_access = true;
+ } elseif (function_exists('aksrt_is_vip') && $pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP) {
+ $has_vip_access = aksrt_is_vip($uid);
+ }
+ }
+
+ if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
+ $download_cost = function_exists('aksrt_resource_download_cost')
+ ? (int) aksrt_resource_download_cost($resource_post)
+ : (int) ceil($price);
+
+ $state['download_cost'] = max(0, $download_cost);
+ $state['need_login'] = !empty($paid_settings['download_login']) || !empty($pay_meta['require_login']);
+ $state['require_captcha'] = !empty($paid_settings['download_captcha']) || !empty($pay_meta['require_captcha']);
+ $state['need_purchase'] = !$has_vip_access && $state['download_cost'] > 0;
+ $state['has_paid_access'] = $has_vip_access || $has_paid_order || !$state['need_purchase'];
+ $state['can_view'] = true;
+ $state['can_download'] = !$state['need_login'] || $uid > 0;
+
+ return $state;
+ }
+
+ if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP) {
+ $state['need_vip'] = true;
+ $state['has_paid_access'] = $has_vip_access;
+ $state['can_view'] = $has_vip_access;
+ $state['can_download'] = false;
+
+ return $state;
+ }
+
+ $state['need_login'] = !empty($pay_meta['require_login']);
+ $state['need_purchase'] = !$has_free_access;
+ $state['has_paid_access'] = $has_free_access || $has_paid_order || $has_vip_access;
+ $state['can_view'] = $state['has_paid_access'] && (!$state['need_login'] || $uid > 0);
+ $state['can_download'] = false;
+
+ return $state;
+ }
+
+ public static function can_view_paid_content($post = null, int $user_id = 0): bool {
+ $state = self::get_post_access_state($post, $user_id);
+ return !empty($state['can_view']);
+ }
+
+ /**
+ * @param int|WP_Post|null $post
+ * @return true|WP_Error
+ */
+ public static function can_download_resource($post = null, int $user_id = 0) {
+ $resource_post = get_post($post);
+ if (!($resource_post instanceof WP_Post) || !function_exists('aksrt_is_resource_post') || !aksrt_is_resource_post($resource_post)) {
+ return new WP_Error('aksrt_not_resource', __('该内容不是可下载资源。', 'aksrt'));
+ }
+
+ $state = self::get_post_access_state($resource_post, $user_id);
+ $uid = $user_id > 0 ? $user_id : get_current_user_id();
+
+ if (($state['pay_type'] ?? '') !== AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
+ return new WP_Error('aksrt_not_downloadable', __('该内容不是可下载资源。', 'aksrt'));
+ }
+
+ if (!empty($state['need_login']) && $uid <= 0) {
+ return new WP_Error('aksrt_need_login', __('请先登录后再下载。', 'aksrt'), [
+ 'status' => 401,
+ 'login_url' => wp_login_url(get_permalink($resource_post)),
+ ]);
+ }
+
+ $cost = (int) ($state['download_cost'] ?? 0);
+ $paid_settings = class_exists('AKSRT_Settings')
+ ? AKSRT_Settings::get_paid_content_settings()
+ : ['download_credit' => true];
+
+ if (!empty($paid_settings['download_credit']) && $cost > 0) {
+ $balance = class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0;
+ if ($balance < $cost) {
+ return new WP_Error('aksrt_credits_insufficient', __('积分不足,无法下载。', 'aksrt'), [
+ 'status' => 402,
+ 'balance' => $balance,
+ 'cost' => $cost,
+ ]);
+ }
+ }
+
+ return true;
+ }
+
+ public function render_hide_shortcode(array $atts, string $content = ''): string {
+ $atts = shortcode_atts([
+ 'mode' => 'comment',
+ 'message' => '',
+ 'pid' => get_the_ID(),
+ ], $atts, 'aksrt_hide');
+
+ $mode = sanitize_key((string) $atts['mode']);
+ $pid = (int) $atts['pid'];
+ $message = (string) $atts['message'];
+
+ if ($pid <= 0) {
+ return '';
+ }
+
+ if ($this->is_unlocked($mode, $pid)) {
+ return '' . do_shortcode($content) . '
';
+ }
+
+ if ($message === '') {
+ switch ($mode) {
+ case 'comment':
+ $message = __('评论后查看隐藏内容', 'aksrt');
+ break;
+ case 'login':
+ $message = __('登录后查看隐藏内容', 'aksrt');
+ break;
+ case 'password':
+ $message = __('输入密码后查看隐藏内容', 'aksrt');
+ break;
+ default:
+ $message = __('此内容已隐藏', 'aksrt');
+ }
+ }
+
+ $wrapper_class = 'aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--' . esc_attr($mode);
+ $html = '